Add --class_parameter_space (#2571)

Signed-off-by: Wilson Snyder <wsnyder@wsnyder.org>
diff --git a/verible/verilog/formatting/format-style-init.cc b/verible/verilog/formatting/format-style-init.cc
index 2dde818..a6d010b 100644
--- a/verible/verilog/formatting/format-style-init.cc
+++ b/verible/verilog/formatting/format-style-init.cc
@@ -140,6 +140,13 @@
           "Use compact binary expressions inside indexing / bit selection "
           "operators");
 
+ABSL_FLAG(bool, class_parameter_space, false,
+          "If true, keep/insert a space before '#' in a class"
+          "parameterized typedef, e.g. \"typedef my_class #(.P(P)) "
+          "my_class_t;\".  If false (default), no space is inserted, "
+          "matching the convention used for IEEE parameterized class "
+          "instantiations, e.g. \"type#(params...)::method(...)\".");
+
 ABSL_FLAG(bool, wrap_end_else_clauses, false,
           "Split end and else keywords into separate lines");
 
@@ -197,6 +204,7 @@
   STYLE_FROM_FLAG(try_wrap_long_lines);
   STYLE_FROM_FLAG(expand_coverpoints);
   STYLE_FROM_FLAG(compact_indexing_and_selections);
+  STYLE_FROM_FLAG(class_parameter_space);
   STYLE_FROM_FLAG(wrap_end_else_clauses);
   STYLE_FROM_FLAG(alignment_group_boundary);
 
diff --git a/verible/verilog/formatting/format-style.h b/verible/verilog/formatting/format-style.h
index 2fad329..55347c5 100644
--- a/verible/verilog/formatting/format-style.h
+++ b/verible/verilog/formatting/format-style.h
@@ -146,6 +146,9 @@
   // Compact binary expressions inside indexing / bit selection operators
   bool compact_indexing_and_selections = true;
 
+  // Keep/insert a space before '#' in a parameterized class typedef
+  bool class_parameter_space = false;
+
   // Split with a \n end and else clauses
   bool wrap_end_else_clauses = false;
 
diff --git a/verible/verilog/formatting/formatter_test.cc b/verible/verilog/formatting/formatter_test.cc
index 5205a01..8bd8e72 100644
--- a/verible/verilog/formatting/formatter_test.cc
+++ b/verible/verilog/formatting/formatter_test.cc
@@ -4478,19 +4478,30 @@
      "    .L(L),\n"
      "    .W(W)\n"
      ") bar_t;\n"},
-    // unqualified parameterized type keeps a space before '#'
+    // By default (class_parameter_space == false), no space before '#'
     {"typedef dv_base_env_cov #(.CFG_T(tl_agent_env_cfg)) tl_agent_env_cov;\n",
-     "typedef dv_base_env_cov #(\n"
+     "typedef dv_base_env_cov#(\n"
      "    .CFG_T(tl_agent_env_cfg)\n"
      ") tl_agent_env_cov;\n"},
-    // ... and is inserted when absent
     {"typedef dv_base_env_cov#(.CFG_T(tl_agent_env_cfg)) tl_agent_env_cov;\n",
-     "typedef dv_base_env_cov #(\n"
+     "typedef dv_base_env_cov#(\n"
      "    .CFG_T(tl_agent_env_cfg)\n"
      ") tl_agent_env_cov;\n"},
     // single short parameter stays on one line
     {"typedef my_class #(.P(P)) my_class_t;\n",
-     "typedef my_class #(.P(P)) my_class_t;\n"},
+     "typedef my_class#(.P(P)) my_class_t;\n"},
+
+    // let declarations each stay on their own line
+    {"module t;\n"
+     "let OFF = 4;\n"
+     "let UNIQUE = 32;\n"
+     "let PP(a) = 30 + a;\n"
+     "endmodule\n",
+     "module t;\n"
+     "  let OFF = 4;\n"
+     "  let UNIQUE = 32;\n"
+     "  let PP(a) = 30 + a;\n"
+     "endmodule\n"},
 
     // package test cases
     {"package fedex;localparam  int  www=3 ;endpackage   :  fedex\n",
@@ -18788,6 +18799,50 @@
   }
 }
 
+static constexpr FormatterTestCase
+    kSpaceBeforeHashInUnqualifiedTypedefTestCases[] = {
+        // unqualified parameterized type keeps a space before '#'
+        {"typedef dv_base_env_cov #(.CFG_T(tl_agent_env_cfg)) "
+         "tl_agent_env_cov;\n",
+         "typedef dv_base_env_cov #(\n"
+         "    .CFG_T(tl_agent_env_cfg)\n"
+         ") tl_agent_env_cov;\n"},
+        // ... and is inserted when absent
+        {"typedef dv_base_env_cov#(.CFG_T(tl_agent_env_cfg)) "
+         "tl_agent_env_cov;\n",
+         "typedef dv_base_env_cov #(\n"
+         "    .CFG_T(tl_agent_env_cfg)\n"
+         ") tl_agent_env_cov;\n"},
+        // single short parameter stays on one line
+        {"typedef my_class #(.P(P)) my_class_t;\n",
+         "typedef my_class #(.P(P)) my_class_t;\n"},
+        // package-qualified types are unaffected (no space before '#')
+        {"typedef foo_pkg::baz_t#(.L(L), .W(W)) bar_t;\n",
+         "typedef foo_pkg::baz_t#(\n"
+         "    .L(L),\n"
+         "    .W(W)\n"
+         ") bar_t;\n"},
+};
+
+TEST(FormatterEndToEndTest, SpaceBeforeHashInUnqualifiedTypedefTestCases) {
+  // Use a fixed style.
+  FormatStyle style;
+  style.column_limit = 40;
+  style.indentation_spaces = 2;
+  style.wrap_spaces = 4;
+  style.class_parameter_space = true;
+
+  for (const auto &test_case : kSpaceBeforeHashInUnqualifiedTypedefTestCases) {
+    VLOG(1) << "code-to-format:\n" << test_case.input << "<EOF>";
+    std::ostringstream stream;
+    const auto status =
+        FormatVerilog(test_case.input, "<filename>", style, stream);
+    // Require these test cases to be valid.
+    EXPECT_OK(status) << status.message();
+    EXPECT_EQ(stream.str(), test_case.expected) << "code:\n" << test_case.input;
+  }
+}
+
 static constexpr FormatterTestCase kFunctionCallsWithComments[] = {
     {// no comments
      "module foo;\n"
diff --git a/verible/verilog/formatting/token-annotator.cc b/verible/verilog/formatting/token-annotator.cc
index def1349..ebaeb21 100644
--- a/verible/verilog/formatting/token-annotator.cc
+++ b/verible/verilog/formatting/token-annotator.cc
@@ -510,7 +510,8 @@
     // This may be controversial or context-dependent, as parameterized
     // classes often appear with method calls like:
     //   type#(params...)::method(...);
-    // A parameterized type in a typedef keeps the space before '#':
+    // If style.class_parameter_space is enabled, a
+    // parameterized type in a typedef keeps the space before '#':
     //   typedef my_class #(.P(P)) my_class_t;
     // but a package-qualified type does not, matching the existing
     // "type#(params...)::method(...)" convention:
@@ -518,6 +519,7 @@
     // Kept as separate IsInsideFirst() calls because MatchesTagAnyOf()
     // only unrolls up to four tags.
     const bool inside_unqualified_typedef =
+        style.class_parameter_space &&
         left_context.IsInsideFirst({NodeEnum::kTypeDeclaration}, {}) &&
         !left_context.IsInsideFirst({NodeEnum::kQualifiedId}, {});
 
diff --git a/verible/verilog/formatting/tree-unwrapper.cc b/verible/verilog/formatting/tree-unwrapper.cc
index a6601ac..bc1af07 100644
--- a/verible/verilog/formatting/tree-unwrapper.cc
+++ b/verible/verilog/formatting/tree-unwrapper.cc
@@ -801,6 +801,7 @@
     case NodeEnum::kPreprocessorUndef:
     case NodeEnum::kTFPortDeclaration:
     case NodeEnum::kTypeDeclaration:
+    case NodeEnum::kLetDeclaration:
     case NodeEnum::kNetTypeDeclaration:
     case NodeEnum::kForwardDeclaration:
     case NodeEnum::kInterfaceClassMethod:
diff --git a/verible/verilog/tools/formatter/README.md b/verible/verilog/tools/formatter/README.md
index 01b2869..a3ffb45 100644
--- a/verible/verilog/tools/formatter/README.md
+++ b/verible/verilog/tools/formatter/README.md
@@ -42,6 +42,11 @@
       {align,flush-left,preserve,infer}); default: infer;
     --class_member_variable_alignment (Format class member variables:
       {align,flush-left,preserve,infer}); default: infer;
+    --class_parameter_space (If true, keep/insert a space
+      before '#' in a class parameterized typedef, e.g. "typedef
+      my_class #(.P(P)) my_class_t;". If false (default), no space is
+      inserted, matching the IEEE convention used for parameterized class
+      instantiations, e.g. "type#(params...)::method(...)".); default: false;
     --compact_indexing_and_selections (Use compact binary expressions inside
       indexing / bit selection operators); default: true;
     --distribution_items_alignment (Align distribution items: