Merge pull request #2548 from kbrunham-intel/fix/2547
Fix formatter non-convergence for multi-line macro sums
diff --git a/verible/verilog/analysis/verilog-equivalence.cc b/verible/verilog/analysis/verilog-equivalence.cc
index 68c8c98..d5c12bea 100644
--- a/verible/verilog/analysis/verilog-equivalence.cc
+++ b/verible/verilog/analysis/verilog-equivalence.cc
@@ -89,11 +89,17 @@
return IsUnlexed(verilog_tokentype(token.token_enum()));
}
-// MacroIdentifier vs MacroIdItem depends only on whether the macro ends the
-// line (see POST_MACRO_ID in verilog.lex). Spelling-equal macros are
-// format-equivalent across that reclassification.
-static bool AreSpellingEqualLineEndingMacros(const TokenInfo &left,
- const TokenInfo &right) {
+// True when left/right differ only because surrounding whitespace changed
+// token classification (e.g. MacroCallCloseToEndLine vs ')', or
+// MacroIdentifier vs MacroIdItem with unchanged spelling).
+static bool TokensAreWhitespaceDependentFormatEquivalent(
+ const TokenInfo &left, const TokenInfo &right) {
+ if ((left.token_enum() == verilog_tokentype::MacroCallCloseToEndLine &&
+ right.text() == ")") ||
+ (right.token_enum() == verilog_tokentype::MacroCallCloseToEndLine &&
+ left.text() == ")")) {
+ return true;
+ }
const auto is_line_ending_macro = [](int token_enum) {
return token_enum == verilog_tokentype::MacroIdentifier ||
token_enum == verilog_tokentype::MacroIdItem;
@@ -184,14 +190,8 @@
// Some token enums differ only by surrounding whitespace (e.g. whether a
// macro or ')' ends a line). Treat those pairs as matching enums when the
// spelling is unchanged so FormatEquivalent tolerates re-wrapping.
- const bool whitespace_dependent_macro_enum_match =
- ((l->token_enum() == verilog_tokentype::MacroCallCloseToEndLine &&
- r->text() == ")") ||
- (r->token_enum() == verilog_tokentype::MacroCallCloseToEndLine &&
- l->text() == ")") ||
- AreSpellingEqualLineEndingMacros(*l, *r));
if (l->token_enum() != r->token_enum() &&
- !whitespace_dependent_macro_enum_match) {
+ !TokensAreWhitespaceDependentFormatEquivalent(*l, *r)) {
if (errstream != nullptr) {
*errstream << "Mismatched token enums. got: ";
token_printer(*l, *errstream);
@@ -270,15 +270,7 @@
return IsWhitespace(verilog_tokentype(t.token_enum()));
},
[=](const TokenInfo &l, const TokenInfo &r) {
- // MacroCallCloseToEndLine should be considered equivalent to ')', as
- // they are whitespace dependant
- if (((r.token_enum() == verilog_tokentype::MacroCallCloseToEndLine) &&
- (l.text() == ")")) ||
- ((l.token_enum() == verilog_tokentype::MacroCallCloseToEndLine) &&
- (r.text() == ")"))) {
- return true;
- }
- if (AreSpellingEqualLineEndingMacros(l, r)) {
+ if (TokensAreWhitespaceDependentFormatEquivalent(l, r)) {
return true;
}
return l.EquivalentWithoutLocation(r);
diff --git a/verible/verilog/formatting/formatter_test.cc b/verible/verilog/formatting/formatter_test.cc
index 13a9fb4..16e0572 100644
--- a/verible/verilog/formatting/formatter_test.cc
+++ b/verible/verilog/formatting/formatter_test.cc
@@ -19456,6 +19456,50 @@
}
}
+// Regression for https://github.com/chipsalliance/verible/issues/2547:
+// A localparam initialized to a sum of long macros must converge: infix `+`
+// stays with the following operand so re-format does not oscillate between
+// `+\n`MACRO` and `+ `MACRO`.
+TEST(FormatterEndToEndTest, LongMacroSumLocalparamConverges) {
+ static constexpr FormatterTestCase kTestCases[] = {
+ {"module m;\n"
+ " localparam N =\n"
+ " `MACRO_GEN3_SCRAMBLE_LFSR_REGOUT\n"
+ " + `MACRO_GEN3_SCRAMBLE_REGIN\n"
+ " + `MACRO_GEN3_SCRAMBLE_REGOUT\n"
+ " ;\n"
+ "endmodule\n",
+ "module m;\n"
+ " localparam N =\n"
+ " `MACRO_GEN3_SCRAMBLE_LFSR_REGOUT\n"
+ " + `MACRO_GEN3_SCRAMBLE_REGIN\n"
+ " + `MACRO_GEN3_SCRAMBLE_REGOUT;\n"
+ "endmodule\n"},
+ // Already in pass-1 form must stay stable.
+ {"module m;\n"
+ " localparam N =\n"
+ " `MACRO_GEN3_SCRAMBLE_LFSR_REGOUT\n"
+ " + `MACRO_GEN3_SCRAMBLE_REGIN\n"
+ " + `MACRO_GEN3_SCRAMBLE_REGOUT;\n"
+ "endmodule\n",
+ "module m;\n"
+ " localparam N =\n"
+ " `MACRO_GEN3_SCRAMBLE_LFSR_REGOUT\n"
+ " + `MACRO_GEN3_SCRAMBLE_REGIN\n"
+ " + `MACRO_GEN3_SCRAMBLE_REGOUT;\n"
+ "endmodule\n"},
+ };
+ FormatStyle style; // default column_limit (100)
+ for (const auto &test_case : kTestCases) {
+ VLOG(1) << "code-to-format:\n" << test_case.input << "<EOF>";
+ std::ostringstream stream;
+ const auto status =
+ FormatVerilog(test_case.input, "<filename>", style, stream);
+ EXPECT_OK(status) << status.message();
+ EXPECT_EQ(stream.str(), test_case.expected) << "code:\n" << test_case.input;
+ }
+}
+
// Regression for https://github.com/chipsalliance/verible/issues/2544:
// Wrapping a $bits(...)'(...) cast may leave `MACRO at EOL, reclassifying
// MacroIdentifier as MacroIdItem. FormatEquivalent must accept that, and
diff --git a/verible/verilog/formatting/tree-unwrapper.cc b/verible/verilog/formatting/tree-unwrapper.cc
index 2286f8c..09fa8c0 100644
--- a/verible/verilog/formatting/tree-unwrapper.cc
+++ b/verible/verilog/formatting/tree-unwrapper.cc
@@ -1641,6 +1641,53 @@
}
}
+// True when a leaf partition contains only a binary/infix operator (and
+// optional comments/attributes). Used to normalize wrapping of expressions
+// like `A + B + C` so operators always stay with the following operand.
+static bool PartitionIsInfixOperatorOnly(const TokenPartitionTree &partition) {
+ if (!is_leaf(partition)) return false;
+ const auto tokens = partition.Value().TokensRange();
+ if (tokens.empty()) return false;
+
+ const verible::PreFormatToken *op = nullptr;
+ for (const auto &token : tokens) {
+ switch (token.TokenEnum()) {
+ case verilog_tokentype::TK_COMMENT_BLOCK:
+ case verilog_tokentype::TK_EOL_COMMENT:
+ case verilog_tokentype::TK_ATTRIBUTE:
+ break;
+ default:
+ if (GetFormatTokenType(static_cast<verilog_tokentype>(
+ token.TokenEnum())) != FormatTokenType::binary_operator ||
+ op != nullptr) {
+ return false;
+ }
+ op = &token;
+ break;
+ }
+ }
+ return op != nullptr;
+}
+
+// Always attach infix-operator-only partitions to the following operand.
+// Attachment based on original newlines is unstable for macro sums:
+// `A\n+\n`B vs `A\n+ `B produce different partition shapes and oscillate
+// under re-format (GitHub issue 2547).
+static void AttachInfixOperatorsToFollowingOperands(
+ TokenPartitionTree *partition) {
+ // Iterate by index; merges invalidate sibling pointers.
+ for (int i = 0; i < static_cast<int>(partition->Children().size()); ++i) {
+ auto &child = partition->Children()[i];
+ if (!PartitionIsInfixOperatorOnly(child)) continue;
+ if (NextLeaf(child) == nullptr) continue;
+ VLOG(4) << "Attaching infix operator partition to following operand:\n"
+ << child;
+ verible::MergeLeafIntoNextLeaf(&child);
+ // Children shifted; re-check current index.
+ --i;
+ }
+}
+
static void AttachTrailingSemicolonToPreviousPartition(
TokenPartitionTree *partition) {
// TODO(mglb): Replace this function with
@@ -3137,6 +3184,7 @@
case NodeEnum::kParamDeclaration: {
AttachTrailingSemicolonToPreviousPartition(&partition);
AttachOpeningBraceToDeclarationsAssignmentOperator(&partition);
+ AttachInfixOperatorsToFollowingOperands(&partition);
break;
}