Merge pull request #2534 from EylonKrause/fix/port-name-suffix-ref-underscore

port-name-suffix: handle `ref` direction and all-underscore names
diff --git a/.bant-macros b/.bant-macros
new file mode 100644
index 0000000..c62c397
--- /dev/null
+++ b/.bant-macros
@@ -0,0 +1,23 @@
+# -*- Python -*-
+# https://github.com/hzeller/bant#macros
+#
+# Tell bant (that is not looking at *.bzl files) how custom rules behave, just
+# enough for it to see through them to provide features such as dwyu.
+
+record_recovered_syntax_errors = genrule(
+    name = name,
+    srcs = [src],
+    outs = [out],
+)
+
+genlex = genrule(
+    name = name,
+    srcs = [src],
+    outs = [out],
+)
+
+genyacc = genrule(
+    name = name,
+    srcs = [src],
+    outs = [header_out, source_out] + extra_outs,
+)
diff --git a/.bazelrc b/.bazelrc
index 04d1f38..301b564 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -10,29 +10,38 @@
 build --workspace_status_command="bash bazel/build-version.sh"
 
 # Systems with gcc or clang
-common:unix    --cxxopt=-xc++ --host_cxxopt=-xc++ --cxxopt=-std=c++17 --host_cxxopt=-std=c++17 --client_env=BAZEL_CXXOPTS=-std=c++17
+common:unix    --cxxopt=-xc++      --host_cxxopt=-xc++
+common:unix    --cxxopt=-std=c++20 --host_cxxopt=-std=c++20
 common:linux   --config=unix
 common:freebsd --config=unix --linkopt=-lm --host_linkopt=-lm
 common:openbsd --config=unix --linkopt=-lm --host_linkopt=-lm
 common:macos   --config=unix
 
-# https://github.com/abseil/abseil-cpp/issues/848
-# https://github.com/bazelbuild/bazel/issues/4341#issuecomment-758361769
-common:macos --features=-supports_dynamic_linker --linkopt=-framework --linkopt=CoreFoundation --host_linkopt=-framework --host_linkopt=CoreFoundation
+# Applies to both clang-cl and native MSVC builds.
+common:windows --cxxopt=/std:c++20 --host_cxxopt=/std:c++20 --client_env=BAZEL_CXXOPTS=/std:c++20
+# Actions (e.g. the genrules invoking win_flex.exe/win_bison.exe) need the
+# invoking shell's PATH to find tools installed.
+common:windows --action_env=PATH --host_action_env=PATH
+# Protobuf 31 has [MSVC deprecated](https://github.com/protocolbuffers/protobuf/issues/20085) and need this option to compile
+# The deprecation was later reverted thanks to improvements in bazel 8 and will not be necessary with newer protobuf.
+common:msvc --define=protobuf_allow_msvc=true
 
-# Use clang-cl by default on Windows. MSVC has some issues with the codebase,
-# so we focus the effort for now is to have a Windows Verible compiled with
-# clang-cl before fixing the issues unique to MSVC.
-common:windows --extra_toolchains=@local_config_cc//:cc-toolchain-x64_windows-clang-cl --extra_execution_platforms=//:x64_windows-clang-cl
-common:windows --compiler=clang-cl --cxxopt=/std:c++17 --host_cxxopt=/std:c++17 --client_env=BAZEL_CXXOPTS=/std:c++17
+# Opt-in: build with clang-cl instead of the natively autoconfigured MSVC
+# with bazel --config=clang-cl
+common:clang-cl --extra_toolchains=@local_config_cc//:cc-toolchain-x64_windows-clang-cl --extra_execution_platforms=//:x64_windows-clang-cl
+common:clang-cl --compiler=clang-cl
 
-build --cxxopt="-Wno-unknown-warning-option" --host_cxxopt="-Wno-unknown-warning-option"
+common:unix --cxxopt="-Wno-unknown-warning-option" --host_cxxopt="-Wno-unknown-warning-option"
+common:clang-cl --cxxopt="-Wno-unknown-warning-option" --host_cxxopt="-Wno-unknown-warning-option"
 # TODO: this looks like benign where it happens but to be explored further.
-build --cxxopt="-Wno-dangling-reference" --host_cxxopt="-Wno-dangling-reference"
+common:unix --cxxopt="-Wno-dangling-reference" --host_cxxopt="-Wno-dangling-reference"
+common:clang-cl --cxxopt="-Wno-dangling-reference" --host_cxxopt="-Wno-dangling-reference"
 # Newer bisons create an unused label.
-build --cxxopt="-Wno-unused-label" --host_cxxopt="-Wno-unused-label"
+common:unix --cxxopt="-Wno-unused-label" --host_cxxopt="-Wno-unused-label"
+common:clang-cl --cxxopt="-Wno-unused-label" --host_cxxopt="-Wno-unused-label"
 # c++20 warning on protobuf 28.1
-build --cxxopt="-Wno-missing-requires" --host_cxxopt="-Wno-missing-requires"
+common:unix --cxxopt="-Wno-missing-requires" --host_cxxopt="-Wno-missing-requires"
+common:clang-cl --cxxopt="-Wno-missing-requires" --host_cxxopt="-Wno-missing-requires"
 
 # For 3rd party code: Disable warnings entirely.
 # They are not actionable and just create noise.
diff --git a/.bazelversion b/.bazelversion
deleted file mode 100644
index 93c8dda..0000000
--- a/.bazelversion
+++ /dev/null
@@ -1 +0,0 @@
-7.6.0
diff --git a/.github/bin/get-bant-path.sh b/.github/bin/get-bant-path.sh
index 9011aea..f1a0cac 100755
--- a/.github/bin/get-bant-path.sh
+++ b/.github/bin/get-bant-path.sh
@@ -21,8 +21,7 @@
 
 # Bant not given, compile from bzlmod dep.
 if [ "${BANT}" = "needs-to-be-compiled-locally" ]; then
-  "${BAZEL}" build -c opt --cxxopt=-std=c++20 @bant//bant:bant 2>/dev/null
-  BANT=$(realpath bazel-bin/external/bant*/bant/bant | tail -1)
+  BANT="$("${BAZEL}" run -c opt --cxxopt=-std=c++20 --run_under='echo' @bant//bant:bant 2>/dev/null)"
 fi
 
 echo $BANT
diff --git a/.github/bin/make-compilation-db.sh b/.github/bin/make-compilation-db.sh
index e35a55d..5038893 100755
--- a/.github/bin/make-compilation-db.sh
+++ b/.github/bin/make-compilation-db.sh
@@ -19,11 +19,12 @@
 BAZEL=${BAZEL:-bazel}
 BANT=$($(dirname $0)/get-bant-path.sh)
 
-BAZEL_OPTS="-c opt --noshow_progress"
+BAZEL_OPTS="-c opt --noshow_progress --remote_download_outputs=all"
+
 # Bazel-build all targets that generate files, so that they can be
 # seen in dependency analysis.
-${BAZEL} build -k ${BAZEL_OPTS} $(${BANT} list-targets \
-  | awk '/genrule|cc_proto_library|genlex|genyacc/ {print $3}')
+${BAZEL} build -k ${BAZEL_OPTS} $(${BANT} list-targets ... \
+                         -g 'genrule|cc_proto_library|genlex|genyacc' -c3)
 
 # Some selected targets to trigger all dependency fetches from MODULE.bazel
 # verilog-y-final to create a header, kzip creator to trigger build of any.pb.h
@@ -41,8 +42,3 @@
 for d in bazel-out/../../../external/*flex*/src/FlexLexer.h ; do
   echo "-I$(dirname $d)" >> compile_flags.txt
 done
-
-# clang-tidy sometimes has issues figuring out if a file is c++,
-# so let's tell it. Bant can't always exctract that yet from --config redirects
-# in .bazelrc
-echo "-xc++" >> compile_flags.txt
diff --git a/.github/bin/run-format.sh b/.github/bin/run-format.sh
index e6bedd6..d5ccfa1 100755
--- a/.github/bin/run-format.sh
+++ b/.github/bin/run-format.sh
@@ -31,10 +31,16 @@
 
 FORMAT_OUT=${TMPDIR:-/tmp}/clang-format-diff.out
 
-# Use the provided Clang format binary, or try to fallback to clang-format-17
-# or clang-format.
+# Use the provided Clang format binary, or try clang-format-19 (CI version),
+# then older numbered versions, then unversioned clang-format.
 if [[ ! -v CLANG_FORMAT ]]; then
-  if command -v "clang-format-17" 2>&1 >/dev/null
+  if command -v "clang-format-19" 2>&1 >/dev/null
+  then
+    CLANG_FORMAT="clang-format-19"
+  elif command -v "clang-format-18" 2>&1 >/dev/null
+  then
+    CLANG_FORMAT="clang-format-18"
+  elif command -v "clang-format-17" 2>&1 >/dev/null
   then
     CLANG_FORMAT="clang-format-17"
   elif command -v "clang-format" 2>&1 >/dev/null
@@ -44,15 +50,10 @@
     (echo "-- Missing the clang-format binary! --"; exit 1)
   fi
 fi
+echo "Using ${CLANG_FORMAT} ($("${CLANG_FORMAT}" --version))"
 
 BUILDIFIER=${BUILDIFIER:-buildifier}
 
-# Currently, we're using clang-format 17, as newer versions still have some
-# volatility in minor version.
-${CLANG_FORMAT} --version | grep "17\." ||
-  ( echo "-- Need clang-format 17. Currently CLANG_FORMAT=$CLANG_FORMAT --";
-    exit 1)
-
 # Run on all files.
 
 find . -name "*.h" -o -name "*.cc" \
diff --git a/.github/bin/smoke-projects.hashes b/.github/bin/smoke-projects.hashes
new file mode 100644
index 0000000..5298441
--- /dev/null
+++ b/.github/bin/smoke-projects.hashes
@@ -0,0 +1,20 @@
+a12778b40ae905f82eb2c23ea4ac037f98099fae https://github.com/jamieiles/80x86
+4da15979747f326bde2f9869c64e587ce599772c https://github.com/pulp-platform/axi
+b48037e28544425839dbd617d45b1a82631bc1a9 https://github.com/bespoke-silicon-group/basejump_stl
+f91010f654a5dfd00f83dbe25dbda482218d540b https://github.com/black-parrot/black-parrot
+86d09682492176a1f8fd1732e469bc2d309453a3 https://github.com/chipsalliance/caliptra-rtl
+f9fbd09655ec6ee457438140c13a168390ebd043 https://github.com/chipsalliance/Cores-VeeR-EH2
+3ac6208ed0210ff66a557dd7dc4e79e34f1f2f4a https://github.com/openhwgroup/cva6
+0602ee4627b10f301298f2673d826cdd6baa9327 https://github.com/ijor/fx68k
+8b8ee086aef72e0833b7f0493d9d33f1e4d3c8e2 https://github.com/lowRISC/ibex
+a19e629a1879801ffcc6f2e6256ca435c20570f3 https://github.com/steveicarus/ivtest
+8e83643c22a3ba7612a9bb9cec93292dad618ab5 https://github.com/trivialmips/nontrivial-mips
+00981352642fb65ef2b6c47143a0f6bb31fd4a97 https://github.com/lowRISC/opentitan
+491a4dc9bb25c33e0183caaf683102b9ce273bc1 https://github.com/gtaylormb/opl3_fpga
+7b65f6ba0bce58d4d859082660123b7100aae975 https://github.com/rsd-devel/rsd
+ebb5e3551a9d93c0ee95f0b767dd878b8927e702 https://github.com/syntacore/scr1
+f200eb2ed7b69ac1c6b8eddd47654522aeee5ce8 https://github.com/olofk/serv
+c4229f3bd5220e6d3ba8f390e5d09c87e462e9c7 https://github.com/chipsalliance/sv-tests
+7e9841ff775ea22fba3f222f7bdfe35668345e00 https://github.com/taichi-ishitani/tnoc
+5d72b6618acfddece7f09382a032ccbc05862fdc https://github.com/SymbiFlow/uvm
+1c8e05fd1e9a79ceb8b996a0996674122eed086f https://github.com/SymbiFlow/XilinxUnisimLibrary
diff --git a/.github/bin/smoke-test.sh b/.github/bin/smoke-test.sh
index ebe5679..6e4c080 100755
--- a/.github/bin/smoke-test.sh
+++ b/.github/bin/smoke-test.sh
@@ -39,6 +39,7 @@
 
 TMPDIR="${TMPDIR:-/tmp}"
 readonly BASE_TEST_DIR="${TMPDIR}/test/verible-smoke-test"
+readonly DEFAULT_HASH_FILE="$(dirname $0)/smoke-projects.hashes"
 
 # Write log files to this directory
 readonly SMOKE_LOGGING_DIR="${SMOKE_LOGGING_DIR:-$BASE_TEST_DIR/error-logs}"
@@ -81,29 +82,12 @@
 #
 # There are some known issues which are all recorded in the associative
 # array below, mapping them to Verible issue tracker numbers.
-# TODO(hzeller): there should be a configuration file that contains two
-# columns: URL + hash, so that we can fetch a particular known version not
-# a moving target.
-readonly TEST_GIT_PROJECTS="https://github.com/lowRISC/ibex \
-         https://github.com/lowRISC/opentitan \
-         https://github.com/chipsalliance/sv-tests \
-         https://github.com/chipsalliance/Cores-VeeR-EH2 \
-         https://github.com/chipsalliance/caliptra-rtl \
-         https://github.com/openhwgroup/cva6 \
-         https://github.com/SymbiFlow/uvm \
-         https://github.com/taichi-ishitani/tnoc \
-         https://github.com/ijor/fx68k \
-         https://github.com/jamieiles/80x86 \
-         https://github.com/SymbiFlow/XilinxUnisimLibrary \
-         https://github.com/black-parrot/black-parrot
-         https://github.com/steveicarus/ivtest \
-         https://github.com/trivialmips/nontrivial-mips \
-         https://github.com/pulp-platform/axi \
-         https://github.com/rsd-devel/rsd \
-         https://github.com/syntacore/scr1 \
-         https://github.com/olofk/serv \
-         https://github.com/bespoke-silicon-group/basejump_stl \
-         https://github.com/gtaylormb/opl3_fpga"
+readonly PROJECT_HASHES_FILE="${1:-${DEFAULT_HASH_FILE}}"
+
+if [ ! -f "${PROJECT_HASHES_FILE}" ]; then
+  echo "Project hashes file not found: ${PROJECT_HASHES_FILE}"
+  exit 1
+fi
 
 ##
 # Some of the files in the projects will have issues.
@@ -135,24 +119,24 @@
 
 ExpectedFailCount[syntax:ibex]=13
 ExpectedFailCount[lint:ibex]=13
-ExpectedFailCount[project:ibex]=223
-ExpectedFailCount[preprocessor:ibex]=397
+ExpectedFailCount[project:ibex]=224
+ExpectedFailCount[preprocessor:ibex]=403
 
-ExpectedFailCount[syntax:opentitan]=92
-ExpectedFailCount[lint:opentitan]=92
-ExpectedFailCount[project:opentitan]=1179
+ExpectedFailCount[syntax:opentitan]=88
+ExpectedFailCount[lint:opentitan]=88
+ExpectedFailCount[project:opentitan]=1077
 ExpectedFailCount[formatter:opentitan]=0
-ExpectedFailCount[preprocessor:opentitan]=3061
+ExpectedFailCount[preprocessor:opentitan]=3017
 
 ExpectedFailCount[syntax:sv-tests]=74
 ExpectedFailCount[lint:sv-tests]=73
 ExpectedFailCount[project:sv-tests]=176
 ExpectedFailCount[preprocessor:sv-tests]=128
 
-ExpectedFailCount[syntax:caliptra-rtl]=39
-ExpectedFailCount[lint:caliptra-rtl]=38
-ExpectedFailCount[project:caliptra-rtl]=455
-ExpectedFailCount[preprocessor:caliptra-rtl]=903
+ExpectedFailCount[syntax:caliptra-rtl]=41
+ExpectedFailCount[lint:caliptra-rtl]=40
+ExpectedFailCount[project:caliptra-rtl]=474
+ExpectedFailCount[preprocessor:caliptra-rtl]=993
 
 ExpectedFailCount[syntax:Cores-VeeR-EH2]=2
 ExpectedFailCount[lint:Cores-VeeR-EH2]=2
@@ -161,17 +145,17 @@
 
 ExpectedFailCount[syntax:cva6]=7
 ExpectedFailCount[lint:cva6]=7
-ExpectedFailCount[project:cva6]=92
-ExpectedFailCount[preprocessor:cva6]=141
+ExpectedFailCount[project:cva6]=84
+ExpectedFailCount[preprocessor:cva6]=140
 
-ExpectedFailCount[syntax:uvm]=0
-ExpectedFailCount[lint:uvm]=0
-ExpectedFailCount[project:uvm]=47
-ExpectedFailCount[preprocessor:uvm]=126
+ExpectedFailCount[syntax:uvm]=1
+ExpectedFailCount[lint:uvm]=1
+ExpectedFailCount[project:uvm]=41
+ExpectedFailCount[preprocessor:uvm]=128
 
 ExpectedFailCount[syntax:tnoc]=3
 ExpectedFailCount[lint:tnoc]=3
-ExpectedFailCount[project:tnoc]=24
+ExpectedFailCount[project:tnoc]=21
 ExpectedFailCount[preprocessor:tnoc]=57
 
 ExpectedFailCount[project:80x86]=2
@@ -187,17 +171,17 @@
 ExpectedFailCount[project:black-parrot]=172
 ExpectedFailCount[preprocessor:black-parrot]=173
 
-ExpectedFailCount[syntax:ivtest]=166
-ExpectedFailCount[lint:ivtest]=166
-ExpectedFailCount[project:ivtest]=198
+ExpectedFailCount[syntax:ivtest]=116
+ExpectedFailCount[lint:ivtest]=116
+ExpectedFailCount[project:ivtest]=145
 ExpectedFailCount[preprocessor:ivtest]=26
 
 ExpectedFailCount[syntax:nontrivial-mips]=2
 ExpectedFailCount[lint:nontrivial-mips]=2
-ExpectedFailCount[project:nontrivial-mips]=81
+ExpectedFailCount[project:nontrivial-mips]=79
 ExpectedFailCount[preprocessor:nontrivial-mips]=78
 
-ExpectedFailCount[project:axi]=82
+ExpectedFailCount[project:axi]=79
 ExpectedFailCount[preprocessor:axi]=79
 
 ExpectedFailCount[syntax:rsd]=5
@@ -205,15 +189,15 @@
 ExpectedFailCount[project:rsd]=52
 ExpectedFailCount[preprocessor:rsd]=49
 
-ExpectedFailCount[project:scr1]=45
+ExpectedFailCount[project:scr1]=44
 ExpectedFailCount[preprocessor:scr1]=46
 
 ExpectedFailCount[project:serv]=1
 ExpectedFailCount[preprocessor:serv]=1
 
-ExpectedFailCount[syntax:basejump_stl]=499
-ExpectedFailCount[lint:basejump_stl]=499
-ExpectedFailCount[project:basejump_stl]=620
+ExpectedFailCount[syntax:basejump_stl]=498
+ExpectedFailCount[lint:basejump_stl]=498
+ExpectedFailCount[project:basejump_stl]=619
 ExpectedFailCount[formatter:basejump_stl]=1
 ExpectedFailCount[preprocessor:basejump_stl]=659
 
@@ -237,11 +221,10 @@
   expected_count_key="${TOOL_SHORT_NAME}:${PROJECT_NAME}"
   if [[ -v ExpectedFailCount[${expected_count_key}] ]]; then
     expected_count=${ExpectedFailCount[${expected_count_key}]}
-    # We allow some 5% deviation from expected values before we complain loudly
-    local GRACE_VALUE=$(( ${expected_count} / 20 ))
-    if [ ${GRACE_VALUE} -lt 2 ]; then
-      GRACE_VALUE=2
-    fi
+
+    # Since we have a fixed set of projects and hashes, we don't allow any
+    # derivation from the expected value (used to be 5%)
+    local GRACE_VALUE=0
     if [ ${OBSERVED_NONZERO_COUNT} -gt ${expected_count} ] ; then
       local ALLOWED_UP_TO=$((${expected_count} + ${GRACE_VALUE}))
       if [ ${OBSERVED_NONZERO_COUNT} -gt ${ALLOWED_UP_TO} ]; then
@@ -254,6 +237,7 @@
     elif [ ${OBSERVED_NONZERO_COUNT} -lt ${expected_count} ] ; then
       echo "::notice:: 🎉 Yay, reduced non-zero exit count ${expected_count} -> ${OBSERVED_NONZERO_COUNT}"
       echo "Set ExpectedFailCount[${TOOL_SHORT_NAME}:${PROJECT_NAME}]=${OBSERVED_NONZERO_COUNT}"
+      return 1  # we still want the exact value to be recorded so actively fail
     fi
   else
     if [ ${OBSERVED_NONZERO_COUNT} -gt 0 ] ; then
@@ -270,11 +254,14 @@
 #
 # First parameter : project name
 # Second parameter: name of file containing a list of {System}Verilog files
+# Third parameter : git URL
+# Fourth parameter: git hash (optional, defaults to master)
 function run_smoke_test() {
   local PROJECT_FILE_LIST=${TMPDIR}/filelist.$$.list
   local PROJECT_NAME=$1
   local FILELIST=$2
   local GIT_URL=$3
+  local GIT_HASH=${4:-master}
   local NUM_FILES=$(wc -l < ${FILELIST})
   local result=0
 
@@ -355,7 +342,7 @@
         else
           # This is an so far unknown issue
           echo "::error:: 😱 ${single_file}: crash exit code $EXIT_CODE for $tool"
-          echo "Input File URL: ${GIT_URL}/blob/master/$(echo $single_file | cut -d/ -f6-)"
+          echo "Input File URL: ${GIT_URL}/blob/${GIT_HASH}/$(echo $single_file | cut -d/ -f6-)"
           head -15 ${PROJECT_FILE_TOOL_OUT}   # Might be useful in this case
           result=$((${result} + 1))
         fi
@@ -388,18 +375,20 @@
 bazel build ${BAZEL_BUILD_OPTIONS} :install-binaries &
 
 # While compiling, run potentially slow network ops
-for git_project in ${TEST_GIT_PROJECTS} ; do
-  PROJECT_NAME="$(basename $git_project)"
+while read -r git_hash git_project _; do
+  [[ -z "${git_hash}" || "${git_hash}" =~ ^# ]] && continue
+  PROJECT_NAME="$(basename "${git_project}")"
   PROJECT_DIR="${BASE_TEST_DIR}/${PROJECT_NAME}"
-  git clone ${git_project} ${PROJECT_DIR} 2>/dev/null &
-done
+  ( git clone "${git_project}" "${PROJECT_DIR}" && git -C "${PROJECT_DIR}" checkout -q "${git_hash}" ) 2>/dev/null &
+done < "${PROJECT_HASHES_FILE}"
 
 echo "base test dir ${BASE_TEST_DIR}; writing logs to ${SMOKE_LOGGING_DIR}"
 echo "Waiting... for compilation and project download finished"
 wait
 
-for git_project in ${TEST_GIT_PROJECTS} ; do
-  PROJECT_NAME="$(basename $git_project)"
+while read -r git_hash git_project _; do
+  [[ -z "${git_hash}" || "${git_hash}" =~ ^# ]] && continue
+  PROJECT_NAME="$(basename "${git_project}")"
   PROJECT_DIR="${BASE_TEST_DIR}/${PROJECT_NAME}"
   # Already cloned above
 
@@ -412,14 +401,14 @@
   FILELIST="${PROJECT_DIR}/verible.filelist"
   find "${PROJECT_DIR}" -name "*.sv" -o -name "*.svh" -o -name "*.v" | sort > ${FILELIST}
 
-  run_smoke_test "${PROJECT_NAME}" "${FILELIST}" "${git_project}"
+  run_smoke_test "${PROJECT_NAME}" "${FILELIST}" "${git_project}" "${git_hash}"
   status_sum=$((${status_sum} + $?))
   echo
-done
+done < "${PROJECT_HASHES_FILE}"
 
 echo "::endgroup::"
 
-echo "There were a total of ${status_sum} new, undocumented issues."
+echo "There were a total of ${status_sum} mismatches"
 
 # Let's see if there are any issues that are fixed in the meantime.
 if [ "${#KnownIssue[@]}" -ne 0 ]; then
diff --git a/.github/settings.sh b/.github/settings.sh
index a6d4ae8..9f56dee 100644
--- a/.github/settings.sh
+++ b/.github/settings.sh
@@ -29,7 +29,7 @@
 
 export GIT_VERSION=${GIT_VERSION:-$(git describe --match='v*')}
 
-export BAZEL_CXXOPTS="-std=c++17"
+export BAZEL_CXXOPTS="-std=c++20"
 
 # Progress output is just noisy in CI outputs.
 export BAZEL_OPTS="-c opt --noshow_progress"
diff --git a/.github/workflows/verible-ci.yml b/.github/workflows/verible-ci.yml
index c1fd2ab..802a7a3 100644
--- a/.github/workflows/verible-ci.yml
+++ b/.github/workflows/verible-ci.yml
@@ -26,7 +26,7 @@
 
 
   CheckFormatAndBuildClean:
-    runs-on: ubuntu-24.04
+    runs-on: ubuntu-latest
     steps:
 
     - name: Checkout code
@@ -34,15 +34,22 @@
       with:
         fetch-depth: 0
 
+    - name: Set up Go
+      uses: actions/setup-go@v5
+      with:
+        go-version: 'stable'
+
     - name: Install Dependencies
       run: |
+        sudo apt-get install clang-format-19
+        clang-format-19 --version
         go install github.com/bazelbuild/buildtools/buildifier@latest
         echo "PATH=$PATH:$(go env GOPATH)/bin/" >> $GITHUB_ENV
 
     - name: Run formatting style check
       run: |
         echo "--- check formatting ---"
-        CLANG_FORMAT=clang-format-17 ./.github/bin/run-format.sh --show-diff
+        CLANG_FORMAT=clang-format-19 ./.github/bin/run-format.sh --show-diff
         echo "--- check build dependencies ---"
         ./.github/bin/run-build-cleaner.sh
         echo "--- check potential problems ---"
@@ -124,7 +131,6 @@
         - test
         - test-clang
         - test-nortti
-        - test-c++20
         - test-c++23
         - smoke-test
         #- smoke-test-analyzer  #issue: #2046
@@ -141,8 +147,6 @@
         exclude:
           - mode: test-nortti
             arch: arm64
-          - mode: test-c++20
-            arch: arm64
           - mode: test-c++23
             arch: arm64
           - mode: asan
@@ -182,12 +186,10 @@
         set -x
         apt -qqy update
         apt -qq -y install build-essential wget git python3 python-is-python3 default-jdk cmake python3-pip ripgrep
-        apt -qq -y install gcc-10 g++-10
         apt -qq -y install gcc-14 g++-14
         apt -qq -y install clang-19
         source ./.github/settings.sh
-        # Use newer compiler for c++2x compilation. Also slang needs c++20
-        ./.github/bin/set-compiler.sh $([[ "$MODE" == test-c++2* || "$MODE" == "smoke-test-analyzer" ]] && echo 14 || echo 10)
+        ./.github/bin/set-compiler.sh 14
         ARCH="$ARCH" ./.github/bin/install-bazel.sh
 
     - name: Build Slang
@@ -333,8 +335,8 @@
       with:
         path: |
           /private/var/tmp/_bazel_runner
-        key: bazelcache_macos_${{ steps.cache_timestamp.outputs.time }}
-        restore-keys: bazelcache_macos_
+        key: bazelcache_macos1_${{ steps.cache_timestamp.outputs.time }}
+        restore-keys: bazelcache_macos1_
 
     - name: Tests
       # MacOS has a broken patch utility:
@@ -396,22 +398,17 @@
 
     - name: Install dependencies
       run: |
-        choco install bazel --force --version=7.6.1
         choco install winflexbison3
-        choco install llvm --allow-downgrade --version=20.1.4
 
     - name: Debug bazel directory settings
-      # We need to explicitly call the bazel binary from choco, otherwise
-      # the default Windows runner seems to run bazelisk(?) and downloads the
-      # latest bazel, which is incompatible. Should be in variable.
-      run: C:/ProgramData/chocolatey/lib/bazel/bazel.exe info
+      run: bazel.exe info
 
     - name: Run Tests
-      run: C:/ProgramData/chocolatey/lib/bazel/bazel.exe test --keep_going --noshow_progress --test_output=errors //...
+      run: bazel.exe test --keep_going --noshow_progress --test_output=errors //...
 
     - name: Build Verible Binaries
       run: |
-        C:/ProgramData/chocolatey/lib/bazel/bazel.exe build --keep_going --noshow_progress -c opt :install-binaries
+        bazel.exe build --keep_going --noshow_progress -c opt :install-binaries
         # Litmus test
         bazel-bin/verible/verilog/tools/syntax/verible-verilog-syntax --version
 
@@ -432,7 +429,7 @@
     # prevents packaging up the cache.
     - name: Stop Bazel
       run: |
-        C:/ProgramData/chocolatey/lib/bazel/bazel.exe shutdown
+        bazel.exe shutdown
         # The cache pack/restore has issues with these symbolic links
         rm c:/users/runneradmin/_bazel_runneradmin/*/install
         rm c:/users/runneradmin/_bazel_runneradmin/*/java.log
diff --git a/MODULE.bazel b/MODULE.bazel
index 998aa87..463c634 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -20,4 +20,4 @@
 bazel_dep(name = "googletest", version = "1.17.0.bcr.2", dev_dependency = True)
 
 # To build compilation DB and run build-cleaning
-bazel_dep(name = "bant", version = "0.2.10", dev_dependency = True)
+bazel_dep(name = "bant", version = "0.3.4", dev_dependency = True)
diff --git a/README.md b/README.md
index a139080..047fb20 100644
--- a/README.md
+++ b/README.md
@@ -172,10 +172,9 @@
 
 Verible's code base is written in C++.
 
-To build, you need the [bazel] build system and a C++17
-compatible compiler (e.g. >= g++-10), as well as python3.
-A lot of users of Verible have to work on pretty old installations,
-so we try to keep the requirements as minimal as possible.
+To build, you need the [bazel] build system (get it from
+[bazel install][bazel-install] if not already on your system) and a
+C++20 compatible compiler.
 
 Use your package manager to install the dependencies; on a system with
 the nix package manager simply run `nix-shell` to get a build environment.
@@ -201,6 +200,8 @@
 bazel build -c opt --config=create_static_linked_executables //...
 ```
 
+See [Installation](#installation-1) for install.
+
 ### Optionally using local flex/bison for build
 
 Flex and Bison, that are needed for the parser generation, are compiled as part
@@ -216,17 +217,16 @@
 
 ### Building on Windows
 
-Building on Windows requires LLVM, WinFlexBison 3 and Git-bash to be installed. Using package manager [chocolatey], this can be done with
+In addition to Bazel & Visual Studio, building on Windows requires WinFlexBison 3 and Git-bash to be installed. Using package manager [chocolatey], this can be done with
 
 ```powershell
-choco install git llvm winflexbison3
+choco install git winflexbison3
 ```
 
-Bazel may also require environment variable to use git-bash and LLVM, on powershell
+Bazel may also require environment variable to use git-bash, on powershell
 
 ```powershell
 $env:BAZEL_SH="C:\Program Files\Git\git-bash.exe"
-$env:BAZEL_LLVM="C:\Program Files\LLVM"
 ```
 
 ### Installation
@@ -286,6 +286,7 @@
 [UHDM] format. If you are interested in collaborating, contact us.
 
 [bazel]: https://bazel.build/
+[bazel-install]: https://bazel.build/install
 [SV-LRM]: https://ieeexplore.ieee.org/document/8299595
 [lint-rule-list]: https://chipsalliance.github.io/verible/lint.html
 [github-lint-action]: https://github.com/chipsalliance/verible-linter-action
diff --git a/bazel/BUILD b/bazel/BUILD
index 6dab72b..ebe6d5b 100644
--- a/bazel/BUILD
+++ b/bazel/BUILD
@@ -4,6 +4,7 @@
 
 load("@bazel_skylib//rules:common_settings.bzl", "bool_flag")
 load("@rules_cc//cc:defs.bzl", "cc_library")
+load(":no_toolchain.bzl", "no_toolchain")
 
 package(
     default_applicable_licenses = ["//:license"],
@@ -23,6 +24,7 @@
 exports_files([
     "bison.bzl",
     "flex.bzl",
+    "no_toolchain.bzl",
     "sh_test_with_runfiles_lib.bzl",
     "sh_test_with_runfiles_lib.sh",
 ])
@@ -37,6 +39,12 @@
     flag_values = {":use_local_flex_bison": "true"},
 )
 
+# Placeholder used where a select() would otherwise need to switch a
+# genrule's non-configurable `toolchains` attribute (see flex.bzl/bison.bzl):
+no_toolchain(
+    name = "no_toolchain",
+)
+
 bool_flag(
     name = "create_static_linked_executables",
     build_setting_default = False,
diff --git a/bazel/bison.bzl b/bazel/bison.bzl
index f191209..9053b03 100644
--- a/bazel/bison.bzl
+++ b/bazel/bison.bzl
@@ -26,6 +26,23 @@
         extra_outs = []):
     """Build rule for generating C or C++ sources with Bison.
     """
+
+    native.alias(
+        name = name + "_bison_toolchain",
+        actual = select({
+            "//bazel:use_local_flex_bison_enabled": "//bazel:no_toolchain",
+            "@platforms//os:windows": "//bazel:no_toolchain",
+            "//conditions:default": "@rules_bison//bison:current_bison_toolchain",
+        }),
+    )
+    native.alias(
+        name = name + "_m4_toolchain",
+        actual = select({
+            "//bazel:use_local_flex_bison_enabled": "//bazel:no_toolchain",
+            "@platforms//os:windows": "//bazel:no_toolchain",
+            "//conditions:default": "@rules_m4//m4:current_m4_toolchain",
+        }),
+    )
     native.genrule(
         name = name,
         srcs = [src],
@@ -35,12 +52,8 @@
             "@platforms//os:windows": "win_bison.exe --defines=$(location " + header_out + ") --output-file=$(location " + source_out + ") " + " ".join(extra_options) + " $<",
             "//conditions:default": "M4=$(M4) $(BISON) --defines=$(location " + header_out + ") --output-file=$(location " + source_out + ") " + " ".join(extra_options) + " $<",
         }),
-        toolchains = select({
-            "//bazel:use_local_flex_bison_enabled": [],
-            "@platforms//os:windows": [],
-            "//conditions:default": [
-                "@rules_bison//bison:current_bison_toolchain",
-                "@rules_m4//m4:current_m4_toolchain",
-            ],
-        }),
+        toolchains = [
+            ":" + name + "_bison_toolchain",
+            ":" + name + "_m4_toolchain",
+        ],
     )
diff --git a/bazel/flex.bzl b/bazel/flex.bzl
index a398c6f..98f6c8c 100644
--- a/bazel/flex.bzl
+++ b/bazel/flex.bzl
@@ -20,6 +20,23 @@
 def genlex(name, src, out):
     """Generate C/C++ language source from lex file using Flex
     """
+
+    native.alias(
+        name = name + "_flex_toolchain",
+        actual = select({
+            "//bazel:use_local_flex_bison_enabled": "//bazel:no_toolchain",
+            "@platforms//os:windows": "//bazel:no_toolchain",
+            "//conditions:default": "@rules_flex//flex:current_flex_toolchain",
+        }),
+    )
+    native.alias(
+        name = name + "_m4_toolchain",
+        actual = select({
+            "//bazel:use_local_flex_bison_enabled": "//bazel:no_toolchain",
+            "@platforms//os:windows": "//bazel:no_toolchain",
+            "//conditions:default": "@rules_m4//m4:current_m4_toolchain",
+        }),
+    )
     native.genrule(
         name = name,
         srcs = [src],
@@ -29,12 +46,8 @@
             "@platforms//os:windows": "win_flex.exe --outfile=$@ $<",
             "//conditions:default": "M4=$(M4) $(FLEX) --outfile=$@ $<",
         }),
-        toolchains = select({
-            "//bazel:use_local_flex_bison_enabled": [],
-            "@platforms//os:windows": [],
-            "//conditions:default": [
-                "@rules_flex//flex:current_flex_toolchain",
-                "@rules_m4//m4:current_m4_toolchain",
-            ],
-        }),
+        toolchains = [
+            ":" + name + "_flex_toolchain",
+            ":" + name + "_m4_toolchain",
+        ],
     )
diff --git a/bazel/no_toolchain.bzl b/bazel/no_toolchain.bzl
new file mode 100644
index 0000000..07d4225
--- /dev/null
+++ b/bazel/no_toolchain.bzl
@@ -0,0 +1,28 @@
+# -*- Python -*-
+# Copyright 2017-2026 The Verible Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""A placeholder target for genrule's `toolchains` attribute.
+
+genrule's `toolchains` attribute requires each entry to provide
+TemplateVariableInfo (or ToolchainTypeInfo). Used where a platform (e.g.
+Windows) needs no real toolchain there, like for using local winflexbison.
+"""
+
+def _no_toolchain_impl(ctx):
+    return [platform_common.TemplateVariableInfo({})]
+
+no_toolchain = rule(
+    implementation = _no_toolchain_impl,
+)
diff --git a/external_libs/editscript.h b/external_libs/editscript.h
index 8e01ef9..1f4dcb8 100644
--- a/external_libs/editscript.h
+++ b/external_libs/editscript.h
@@ -179,8 +179,10 @@
 template <typename TokenIter>
 class Diff {
  private:
-  friend Edits GetTokenDiffs<>(TokenIter tokens1_begin, TokenIter tokens1_end,
-                               TokenIter tokens2_begin, TokenIter tokens2_end);
+  friend Edits diff::GetTokenDiffs<>(TokenIter tokens1_begin,
+                                     TokenIter tokens1_end,
+                                     TokenIter tokens2_begin,
+                                     TokenIter tokens2_end);
 
   /**
    * Finds the differences between two vectors of tokens, returning edits
diff --git a/shell.nix b/shell.nix
index 7a8f55e..85163eb 100644
--- a/shell.nix
+++ b/shell.nix
@@ -7,12 +7,13 @@
   verible_used_stdenv = pkgs.stdenv;
   #verible_used_stdenv = pkgs.gcc15Stdenv;
   #verible_used_stdenv = pkgs.clang19Stdenv;
+  bazel = pkgs.bazel_8;
 in
 verible_used_stdenv.mkDerivation {
   name = "verible-build-environment";
   buildInputs = with pkgs;
     [
-      bazel_7
+      bazel
       git
 
       # For scripts used inside bzl rules and tests
@@ -37,14 +38,14 @@
       lcov              # coverage html generation.
       bazel-buildtools  # buildifier
 
-      llvmPackages_21.clang-tools    # for clang-tidy
-      llvmPackages_18.clang-tools    # for clang-format
+      llvmPackages_22.clang-tools    # for clang-tidy
+      llvmPackages_19.clang-tools    # for clang-format
     ];
   shellHook = ''
       # clang tidy: use latest.
-      export CLANG_TIDY=${pkgs.llvmPackages_21.clang-tools}/bin/clang-tidy
+      export CLANG_TIDY=${pkgs.llvmPackages_22.clang-tools}/bin/clang-tidy
 
       # Last version that current github CI supports.
-      export CLANG_FORMAT=${pkgs.llvmPackages_18.clang-tools}/bin/clang-format
+      export CLANG_FORMAT=${pkgs.llvmPackages_19.clang-tools}/bin/clang-format
   '';
 }
diff --git a/verible/common/formatting/align.cc b/verible/common/formatting/align.cc
index 9986fc7..74bc432 100644
--- a/verible/common/formatting/align.cc
+++ b/verible/common/formatting/align.cc
@@ -1373,7 +1373,7 @@
         break;
       }
     }  // switch
-  }    // for
+  }  // for
   // Flush out the last range.
   if (match_count >= min_match_count) {
     result.emplace_back(last_range_start, partitions.end(), last_match_subtype);
diff --git a/verible/common/formatting/align.h b/verible/common/formatting/align.h
index d042ae0..f290ac6 100644
--- a/verible/common/formatting/align.h
+++ b/verible/common/formatting/align.h
@@ -407,22 +407,33 @@
     // Skip tree tokens. Non-tree tokens located between tree tokens (e.g. block
     // comments) are also skipped.
     while (*(ftoken_it->token) != last_tree_token) ++ftoken_it;
-    // Use next token as begining of trailing non-tree tokens
+    // Use next token as beginning of trailing non-tree tokens
     trailing_tokens.set_begin(ftoken_it + 1);
 
-    // Breaking following condition leads to e.g. concatenation of EOL comment
-    // and code in a single line. To fix situation that lead to this, flatten
-    // token partitions that contain EOL comment subpartition just before a
-    // subpartition that starts with the same token as Origin(). Example of a
-    // partition that needs flattening:
+    // When leading non-tree tokens (typically a comment block between list
+    // items) coexist with the first tree token being forced to start on a
+    // new line, the leading tokens belong on the preceding line and the
+    // tree content on a separate line.  Discard the leading tokens from
+    // alignment consideration instead of trying to align them.
+    // (This can also arise from EOL comments in a partition that needs
+    // flattening; see the example below.)
     //
+    // Example of a partition tree that needs flattening (from the tree
+    // unwrapper, not this code):
     //   { (>>[...], (origin: "input bit second"))
     //     { (>>[// comment] }
     //     { (>>[input bit second], (origin: "input")) }
     //   }
-    CHECK(leading_tokens.empty() || first_tree_token_it == ftokens.end() ||
-          first_tree_token_it->before.break_decision !=
-              SpacingOptions::kMustWrap);
+    if (!leading_tokens.empty() && first_tree_token_it != ftokens.end() &&
+        first_tree_token_it->before.break_decision ==
+            SpacingOptions::kMustWrap) {
+      // The partition has comment tokens that were absorbed into the
+      // following item's partition (a tree-unwrapper flattening gap).
+      // Return tree-scanned columns without non-tree additions,
+      // effectively skipping alignment for this row rather than
+      // risking corruption of the layout.
+      return column_entries;
+    }
   } else {
     // All tokens are passed as leading
     leading_tokens.set_end(ftokens.end());
diff --git a/verible/common/formatting/align_test.cc b/verible/common/formatting/align_test.cc
index d667697..b9bdb47 100644
--- a/verible/common/formatting/align_test.cc
+++ b/verible/common/formatting/align_test.cc
@@ -740,11 +740,12 @@
                                      partition_.Children().end());
 
   const std::vector<TaggedTokenPartitionRange> ranges(
-      GetPartitionAlignmentSubranges(children, [](const TokenPartitionTree
-                                                      &partition) {
-        // Don't care about the subtype tag.
-        return AlignedPartitionClassification{PartitionSelector(partition), 0};
-      }));
+      GetPartitionAlignmentSubranges(children,
+                                     [](const TokenPartitionTree &partition) {
+                                       // Don't care about the subtype tag.
+                                       return AlignedPartitionClassification{
+                                           PartitionSelector(partition), 0};
+                                     }));
 
   using P = std::pair<int, int>;
   std::vector<P> range_indices;
diff --git a/verible/common/strings/patch.cc b/verible/common/strings/patch.cc
index 966307b..aced6a0 100644
--- a/verible/common/strings/patch.cc
+++ b/verible/common/strings/patch.cc
@@ -229,7 +229,8 @@
   int line_number = header_.old_range.start;  // 1-indexed
   for (const MarkedLine &line : lines_) {
     if (line.IsAdded()) continue;  // ignore added lines
-    if (line_number > static_cast<int>(original_lines.size())) {
+    if (line_number < 1 ||
+        line_number > static_cast<int>(original_lines.size())) {
       return absl::OutOfRangeError(absl::StrCat(
           "Patch hunk references line ", line_number, " in a file with only ",
           original_lines.size(), " lines"));
diff --git a/verible/common/strings/patch_test.cc b/verible/common/strings/patch_test.cc
index 17a4930..a3c9ae3 100644
--- a/verible/common/strings/patch_test.cc
+++ b/verible/common/strings/patch_test.cc
@@ -755,6 +755,34 @@
   }
 }
 
+TEST(HunkVerifyAgainstOriginalLinesTest, NonPositiveStartLine) {
+  // A hunk header whose old-file start is 0 (line numbers are 1-indexed) must
+  // be rejected rather than indexing original_lines at a non-positive offset.
+  const std::vector<std::string_view> kHunkText = {
+      {
+          "@@ -0,2 +1,2 @@",  //
+          " line1",           // retained line reached with line_number == 0
+          "-line2",           //
+          "+line pi",         //
+      },
+  };
+  const std::vector<std::string_view> kOriginal = {
+      "line1",
+      "line2",
+  };
+  Hunk hunk;
+  {
+    const LineRange range(kHunkText.begin(), kHunkText.end());
+    const auto status = hunk.Parse(range);
+    ASSERT_TRUE(status.ok()) << status.message();
+  }
+  {
+    const auto status = hunk.VerifyAgainstOriginalLines(kOriginal);
+    EXPECT_EQ(status.code(), absl::StatusCode::kOutOfRange);
+    EXPECT_TRUE(absl::StrContains(status.message(), "references line 0"));
+  }
+}
+
 TEST(HunkVerifyAgainstOriginalLinesTest, InconsistentRetainedLine) {
   const std::vector<std::string_view> kHunkText = {
       {
diff --git a/verible/common/text/parser-verifier.h b/verible/common/text/parser-verifier.h
index 08edcb9..90aa0bc 100644
--- a/verible/common/text/parser-verifier.h
+++ b/verible/common/text/parser-verifier.h
@@ -58,7 +58,7 @@
   // TODO(jeremycs): changed these to protected and make SyntaxTreeLeaf
   //                 and SyntaxTreeNode friend classes
   void Visit(const SyntaxTreeLeaf &leaf) final;
-  void Visit(const SyntaxTreeNode &node) final{};
+  void Visit(const SyntaxTreeNode &node) final {};
 
  private:
   const Symbol &root_;
diff --git a/verible/common/text/tree-utils_test.cc b/verible/common/text/tree-utils_test.cc
index ace2284..0a77530 100644
--- a/verible/common/text/tree-utils_test.cc
+++ b/verible/common/text/tree-utils_test.cc
@@ -810,13 +810,11 @@
 // FindSubtreeStartingAtOffset tests
 
 constexpr std::string_view kFindSubtreeTestText("abcdef");
-const std::string_view kFindSubtreeTestSubstring(
-    kFindSubtreeTestText.substr(1, 3));
 
 struct FindSubtreeStartingAtOffsetTest : public testing::Test {
   SymbolPtr tree;
   FindSubtreeStartingAtOffsetTest()
-      : tree(Leaf(0, kFindSubtreeTestSubstring)) {}
+      : tree(Leaf(0, kFindSubtreeTestText.substr(1, 3))) {}
 };
 
 // Test that a single leaf yields itself when it starts < offset.
diff --git a/verible/common/util/BUILD b/verible/common/util/BUILD
index 48a2105..db499b4 100644
--- a/verible/common/util/BUILD
+++ b/verible/common/util/BUILD
@@ -22,7 +22,6 @@
     hdrs = ["auto-pop-stack.h"],
     deps = [
         ":logging",
-        "@abseil-cpp//absl/base:core_headers",
     ],
 )
 
diff --git a/verible/common/util/auto-pop-stack.h b/verible/common/util/auto-pop-stack.h
index 024ef05..58628eb 100644
--- a/verible/common/util/auto-pop-stack.h
+++ b/verible/common/util/auto-pop-stack.h
@@ -18,7 +18,6 @@
 #include <cstddef>
 #include <vector>
 
-#include "absl/base/attributes.h"
 #include "verible/common/util/logging.h"
 
 namespace verible {
@@ -29,7 +28,7 @@
 // implementing algorithms based on stack data structures like building
 // inheritance stack of traversed tree.
 template <typename T>
-class AutoPopStack {
+class [[maybe_unused]] AutoPopStack {
  public:
   using value_type = T;
   using this_type = AutoPopStack<value_type>;
@@ -105,7 +104,7 @@
 
  private:
   stack_type stack_;
-} ABSL_ATTRIBUTE_UNUSED;
+};
 
 }  // namespace verible
 
diff --git a/verible/common/util/container-proxy_test.cc b/verible/common/util/container-proxy_test.cc
index 5e8220f..4e4cd75 100644
--- a/verible/common/util/container-proxy_test.cc
+++ b/verible/common/util/container-proxy_test.cc
@@ -874,13 +874,14 @@
 
   const int new_capacity = initial_capacity + 42;
   this->proxy.reserve(new_capacity);
-  EXPECT_EQ(this->proxy.capacity(), new_capacity);
-  EXPECT_EQ(this->container.capacity(), new_capacity);
+  EXPECT_GE(this->proxy.capacity(), new_capacity);
+  EXPECT_EQ(this->proxy.capacity(), this->container.capacity());
 
+  const int capacity_after_first_reserve = this->proxy.capacity();
   const int lower_capacity = 1;
   this->proxy.reserve(lower_capacity);
-  EXPECT_EQ(this->proxy.capacity(), new_capacity);
-  EXPECT_EQ(this->container.capacity(), new_capacity);
+  EXPECT_EQ(this->proxy.capacity(), capacity_after_first_reserve);
+  EXPECT_EQ(this->container.capacity(), capacity_after_first_reserve);
 }
 
 }  // namespace
diff --git a/verible/common/util/tree-operations.h b/verible/common/util/tree-operations.h
index b70da51..231d767 100644
--- a/verible/common/util/tree-operations.h
+++ b/verible/common/util/tree-operations.h
@@ -174,9 +174,11 @@
 // `TreeNodeTraits<T>` is defined for every class `T` fulfilling the TreeNode
 // concept. It can be used in SFINAE tests.
 template <class Node,  //
-          typename Children_ =
-              tree_operations_internal::TreeNodeChildrenTraits<Node>>
+          typename Children_ = detected_or_t<
+              UnavailableFeatureTraits,
+              tree_operations_internal::TreeNodeChildrenTraits, Node>>
 struct TreeNodeTraits : FeatureTraits {
+  static constexpr bool available = Children_::available;
   using Parent =
       detected_or_t<UnavailableFeatureTraits,
                     tree_operations_internal::TreeNodeParentTraits, Node>;
diff --git a/verible/verilog/CST/BUILD b/verible/verilog/CST/BUILD
index 39207e0..03aa2bf 100644
--- a/verible/verilog/CST/BUILD
+++ b/verible/verilog/CST/BUILD
@@ -749,6 +749,8 @@
         "//verible/common/text:symbol",
         "//verible/common/text:tree-utils",
         "//verible/common/text:visitors",
+        "//verible/common/util:casts",
+        "//verible/verilog/parser:verilog-token-enum",
     ],
 )
 
diff --git a/verible/verilog/CST/statement.cc b/verible/verilog/CST/statement.cc
index 0dce48c..b3923bd 100644
--- a/verible/verilog/CST/statement.cc
+++ b/verible/verilog/CST/statement.cc
@@ -21,11 +21,13 @@
 #include "verible/common/text/concrete-syntax-tree.h"
 #include "verible/common/text/symbol.h"
 #include "verible/common/text/tree-utils.h"
+#include "verible/common/util/casts.h"
 #include "verible/verilog/CST/declaration.h"
 #include "verible/verilog/CST/identifier.h"
 #include "verible/verilog/CST/type.h"
 #include "verible/verilog/CST/verilog-matchers.h"  // IWYU pragma: keep
 #include "verible/verilog/CST/verilog-nonterminals.h"
+#include "verible/verilog/parser/verilog-token-enum.h"
 
 namespace verilog {
 
@@ -447,6 +449,21 @@
 }
 
 // Returns the data type node from for loop initialization.
+const verible::SyntaxTreeLeaf *GetGenvarKeywordFromForInitialization(
+    const verible::Symbol &for_initialization) {
+  const verible::Symbol *child0 =
+      GetSubtreeAsSymbol(for_initialization, NodeEnum::kForInitialization, 0);
+  if (child0 == nullptr) return nullptr;
+  if (child0->Kind() == verible::SymbolKind::kLeaf) {
+    const auto *leaf =
+        verible::down_cast<const verible::SyntaxTreeLeaf *>(child0);
+    if (leaf->get().token_enum() == TK_genvar) {
+      return leaf;
+    }
+  }
+  return nullptr;
+}
+
 const verible::SyntaxTreeNode *GetDataTypeFromForInitialization(
     const verible::Symbol &for_initialization) {
   const auto *data_type = verible::GetSubtreeAsSymbol(
diff --git a/verible/verilog/CST/statement.h b/verible/verilog/CST/statement.h
index 2b8ba39..27fe670 100644
--- a/verible/verilog/CST/statement.h
+++ b/verible/verilog/CST/statement.h
@@ -179,8 +179,13 @@
     const verible::Symbol &conditional);
 
 // Returns the data type node from for loop initialization.
+// Returns the genvar keyword leaf from a for initialization, or nullptr if not
+// present.
+const verible::SyntaxTreeLeaf *GetGenvarKeywordFromForInitialization(
+    const verible::Symbol &for_initialization);
+
 const verible::SyntaxTreeNode *GetDataTypeFromForInitialization(
-    const verible::Symbol &);
+    const verible::Symbol &for_initialization);
 
 // Returns the variable name leaf from for loop initialization.
 const verible::SyntaxTreeLeaf *GetVariableNameFromForInitialization(
diff --git a/verible/verilog/CST/verilog-matchers.h b/verible/verilog/CST/verilog-matchers.h
index bc41a60..6663e4a 100644
--- a/verible/verilog/CST/verilog-matchers.h
+++ b/verible/verilog/CST/verilog-matchers.h
@@ -437,6 +437,8 @@
 //     ...
 inline constexpr auto HasUniqueQualifier =
     verible::matcher::MakePathMatcher(L(TK_unique));
+inline constexpr auto HasUnique0Qualifier =
+    verible::matcher::MakePathMatcher(L(TK_unique0));
 // Clean up macros
 #undef N
 #undef L
diff --git a/verible/verilog/CST/verilog-matchers_test.cc b/verible/verilog/CST/verilog-matchers_test.cc
index c38edff..bb0a4c9 100644
--- a/verible/verilog/CST/verilog-matchers_test.cc
+++ b/verible/verilog/CST/verilog-matchers_test.cc
@@ -822,10 +822,69 @@
        endfunction
        )",
        1},
+      {HasUniqueQualifier(),
+       R"(
+       function automatic int foo (input in);
+         unique0 case (in)
+           1: return 0;
+         endcase
+       endfunction
+       )",
+       0},
   };
   for (const auto &test : tests) {
     verible::matcher::RunRawMatcherTestCase<VerilogAnalyzer>(test);
   }
 }
+
+// Tests for HasUnique0Qualifier matching.
+TEST(VerilogMatchers, HasUnique0QualifierTests) {
+  const RawMatcherTestCase tests[] = {
+      {HasUnique0Qualifier(), "", 0},
+      {HasUnique0Qualifier(),
+       R"(
+       function automatic int foo (input in);
+         case (in)
+           default: return 0;
+         endcase
+       endfunction
+       )",
+       0},
+      {HasUnique0Qualifier(),
+       R"(
+       function automatic int foo (input in);
+         unique0 case (in)
+           1: return 0;
+         endcase
+       endfunction
+       )",
+       1},
+      {HasUnique0Qualifier(),
+       R"(
+       function automatic int foo (input in);
+         unique case (in)
+           1: return 0;
+         endcase
+       endfunction
+       )",
+       0},
+      {HasUnique0Qualifier(),
+       R"(
+       function automatic int foo (input in);
+         unique0 if (in) begin
+           return 0;
+         end
+         else if(!in) begin
+           return 1;
+         end
+       endfunction
+       )",
+       1},
+  };
+  for (const auto &test : tests) {
+    verible::matcher::RunRawMatcherTestCase<VerilogAnalyzer>(test);
+  }
+}
+
 }  // namespace
 }  // namespace verilog
diff --git a/verible/verilog/analysis/checkers/BUILD b/verible/verilog/analysis/checkers/BUILD
index a83f76a..4681607 100644
--- a/verible/verilog/analysis/checkers/BUILD
+++ b/verible/verilog/analysis/checkers/BUILD
@@ -817,6 +817,7 @@
         "//verible/common/analysis:syntax-tree-lint-rule",
         "//verible/common/analysis/matcher",
         "//verible/common/analysis/matcher:bound-symbol-manager",
+        "//verible/common/text:config-utils",
         "//verible/common/text:symbol",
         "//verible/common/text:syntax-tree-context",
         "//verible/common/text:token-info",
@@ -826,7 +827,9 @@
         "//verible/verilog/CST:verilog-nonterminals",
         "//verible/verilog/analysis:descriptions",
         "//verible/verilog/analysis:lint-rule-registry",
+        "@abseil-cpp//absl/status",
         "@abseil-cpp//absl/strings",
+        "@re2",
     ],
     alwayslink = 1,
 )
diff --git a/verible/verilog/analysis/checkers/case-missing-default-rule.cc b/verible/verilog/analysis/checkers/case-missing-default-rule.cc
index c401cd6..8f8ec39 100644
--- a/verible/verilog/analysis/checkers/case-missing-default-rule.cc
+++ b/verible/verilog/analysis/checkers/case-missing-default-rule.cc
@@ -42,7 +42,7 @@
 
 static constexpr std::string_view kMessage =
     "Explicitly define a default case for every case statement or add `unique` "
-    "qualifier to the case statement.";
+    "or `unique0` qualifier to the case statement.";
 
 const LintRuleDescriptor &CaseMissingDefaultRule::GetDescriptor() {
   static const LintRuleDescriptor d{
@@ -50,7 +50,7 @@
       .topic = "case-statements",
       .desc =
           "Checks that a default case-item is always defined unless the case "
-          "statement has the `unique` qualifier.",
+          "statement has the `unique` or `unique0` qualifier.",
   };
   return d;
 }
@@ -67,12 +67,16 @@
   static const Matcher uniqueCaseMatcher(
       NodekCaseStatement(HasUniqueQualifier()));
 
+  static const Matcher unique0CaseMatcher(
+      NodekCaseStatement(HasUnique0Qualifier()));
+
   static const Matcher caseMatcherWithDefaultCase(
       NodekCaseStatement(HasDefaultCase()));
 
   // If the case statement doesn't have the "unique" qualifier and
   // it is missing the "default" case, insert the violation
   if (!uniqueCaseMatcher.Matches(symbol, &manager) &&
+      !unique0CaseMatcher.Matches(symbol, &manager) &&
       !caseMatcherWithDefaultCase.Matches(symbol, &manager)) {
     violations_.insert(LintViolation(symbol, kMessage, context));
   }
diff --git a/verible/verilog/analysis/checkers/case-missing-default-rule_test.cc b/verible/verilog/analysis/checkers/case-missing-default-rule_test.cc
index 2b465df..84b8e77 100644
--- a/verible/verilog/analysis/checkers/case-missing-default-rule_test.cc
+++ b/verible/verilog/analysis/checkers/case-missing-default-rule_test.cc
@@ -52,6 +52,22 @@
        endfunction
        )"},
       {R"(
+       function automatic int foo (input [2:0] in);
+         unique case (in)
+           3'b001: return 1;
+         endcase
+         return 0;
+       endfunction
+       )"},
+      {R"(
+       function automatic int foo (input [2:0] in);
+         unique0 case (in)
+           3'b001: return 1;
+         endcase
+         return 0;
+       endfunction
+       )"},
+      {R"(
        function automatic int foo (input in);
          )",
        {TK_case, "case"},
@@ -70,6 +86,22 @@
        endfunction
        )"},
       {R"(
+       function automatic int foo (input [2:0] in);
+         unique casex (in)
+           3'b001: return 1;
+         endcase
+         return 0;
+       endfunction
+       )"},
+      {R"(
+       function automatic int foo (input [2:0] in);
+         unique0 casex (in)
+           3'b001: return 1;
+         endcase
+         return 0;
+       endfunction
+       )"},
+      {R"(
        function automatic int foo (input in);
          )",
        {TK_casex, "casex"},
@@ -88,6 +120,22 @@
        endfunction
        )"},
       {R"(
+       function automatic int foo (input [2:0] in);
+         unique casez (in)
+           3'b001: return 1;
+         endcase
+         return 0;
+       endfunction
+       )"},
+      {R"(
+       function automatic int foo (input [2:0] in);
+         unique0 casez (in)
+           3'b001: return 1;
+         endcase
+         return 0;
+       endfunction
+       )"},
+      {R"(
        function automatic int foo (input in);
          )",
        {TK_casez, "casez"},
@@ -96,6 +144,20 @@
          endcase
        endfunction
        )"},
+      // A qualified outer case does not exempt an unqualified inner case.
+      {R"(
+       function automatic int foo (input in);
+         unique0 case (in)
+           1: begin;
+             )",
+       {TK_case, "case"},
+       R"( (in)
+               1: return 1;
+             endcase
+           end
+         endcase
+       endfunction
+       )"},
 
       // randcase should not be flagged
       {R"(
diff --git a/verible/verilog/analysis/checkers/forbid-consecutive-null-statements-rule_test.cc b/verible/verilog/analysis/checkers/forbid-consecutive-null-statements-rule_test.cc
index 83914c6..eb483eb 100644
--- a/verible/verilog/analysis/checkers/forbid-consecutive-null-statements-rule_test.cc
+++ b/verible/verilog/analysis/checkers/forbid-consecutive-null-statements-rule_test.cc
@@ -226,12 +226,12 @@
 
 TEST(ForbidConsecutiveNullStatementsRuleTest, ApplyAutoFix) {
   const std::initializer_list<verible::AutoFixInOut> kTestCases = {
-    {"module m;\ninitial begin ;; end\nendmodule",
-     "module m;\ninitial begin ; end\nendmodule"},
-    {"module m;\ninitial begin ;  ; end\nendmodule",
-     "module m;\ninitial begin ;   end\nendmodule"},
-    {"module m;\ninitial begin ;  /*  */; end\nendmodule",
-     "module m;\ninitial begin ;  /*  */ end\nendmodule"},
+      {"module m;\ninitial begin ;; end\nendmodule",
+       "module m;\ninitial begin ; end\nendmodule"},
+      {"module m;\ninitial begin ;  ; end\nendmodule",
+       "module m;\ninitial begin ;   end\nendmodule"},
+      {"module m;\ninitial begin ;  /*  */; end\nendmodule",
+       "module m;\ninitial begin ;  /*  */ end\nendmodule"},
 #if 0
       // TODO: apply multi-violation fixes in linter_test_utils.
       { "module m;\ninitial begin; ; ; ; end\nendmodule",
diff --git a/verible/verilog/analysis/checkers/generate-label-prefix-rule.cc b/verible/verilog/analysis/checkers/generate-label-prefix-rule.cc
index c8822ce..e678ad5 100644
--- a/verible/verilog/analysis/checkers/generate-label-prefix-rule.cc
+++ b/verible/verilog/analysis/checkers/generate-label-prefix-rule.cc
@@ -14,12 +14,17 @@
 
 #include "verible/verilog/analysis/checkers/generate-label-prefix-rule.h"
 
+#include <memory>
+#include <string>
 #include <string_view>
 
-#include "absl/strings/match.h"
+#include "absl/status/status.h"
+#include "absl/strings/str_cat.h"
+#include "re2/re2.h"
 #include "verible/common/analysis/lint-rule-status.h"
 #include "verible/common/analysis/matcher/bound-symbol-manager.h"
 #include "verible/common/analysis/matcher/matcher.h"
+#include "verible/common/text/config-utils.h"
 #include "verible/common/text/symbol.h"
 #include "verible/common/text/syntax-tree-context.h"
 #include "verible/common/text/token-info.h"
@@ -38,22 +43,36 @@
 // Register the lint rule
 VERILOG_REGISTER_LINT_RULE(GenerateLabelPrefixRule);
 
-static constexpr std::string_view kMessage =
-    "All generate block labels must start with g_ or gen_";
+static constexpr std::string_view kDefaultStyleRegex = "(g_|gen_).*";
 
-// TODO(fangism): and be lower_snake_case?
-// TODO(fangism): generalize to a configurable pattern and
-// rename this class/rule to GenerateLabelNamingStyle?
+GenerateLabelPrefixRule::GenerateLabelPrefixRule()
+    : style_regex_(
+          std::make_unique<re2::RE2>(kDefaultStyleRegex, re2::RE2::Quiet)) {}
 
 const LintRuleDescriptor &GenerateLabelPrefixRule::GetDescriptor() {
   static const LintRuleDescriptor d{
       .name = "generate-label-prefix",
       .topic = "generate-constructs",
-      .desc = "Checks that every generate block label starts with g_ or gen_.",
+      .desc =
+          "Checks that every generate block label matches the regex defined by "
+          "style_regex. The default regex requires labels to start with g_ or "
+          "gen_. Refer to https://github.com/chipsalliance/verible/tree/master/"
+          "verilog/tools/lint#readme for more detail on verible regex "
+          "patterns.",
+      // NOLINTNEXTLINE(misc-include-cleaner)
+      .param = {{"style_regex", std::string(kDefaultStyleRegex),
+                 "A regex used to check generate label style."}},
   };
   return d;
 }
 
+std::string GenerateLabelPrefixRule::CreateViolationMessage() const {
+  return absl::StrCat(
+      "Generate block label does not match the naming convention defined by "
+      "regex pattern: ",
+      style_regex_->pattern());
+}
+
 // Matches begin statements
 static const Matcher &BlockMatcher() {
   static const Matcher matcher(NodekGenerateBlock());
@@ -84,15 +103,24 @@
       }
 
       if (label != nullptr) {
-        if (!(absl::StartsWith(label->text(), "g_") ||
-              absl::StartsWith(label->text(), "gen_"))) {
-          violations_.insert(verible::LintViolation(*label, kMessage, context));
+        if (!RE2::FullMatch(label->text(), *style_regex_)) {
+          violations_.insert(verible::LintViolation(
+              *label, CreateViolationMessage(), context));
         }
       }
     }
   }
 }
 
+// NOLINTNEXTLINE(misc-include-cleaner)
+absl::Status GenerateLabelPrefixRule::Configure(
+    std::string_view configuration) {
+  using verible::config::SetRegex;
+  absl::Status s = verible::ParseNameValues(
+      configuration, {{"style_regex", SetRegex(&style_regex_)}});
+  return s;
+}
+
 verible::LintRuleStatus GenerateLabelPrefixRule::Report() const {
   return verible::LintRuleStatus(violations_, GetDescriptor());
 }
diff --git a/verible/verilog/analysis/checkers/generate-label-prefix-rule.h b/verible/verilog/analysis/checkers/generate-label-prefix-rule.h
index 9090cf3..e18df9c 100644
--- a/verible/verilog/analysis/checkers/generate-label-prefix-rule.h
+++ b/verible/verilog/analysis/checkers/generate-label-prefix-rule.h
@@ -15,8 +15,13 @@
 #ifndef VERIBLE_VERILOG_ANALYSIS_CHECKERS_GENERATE_LABEL_PREFIX_RULE_H_
 #define VERIBLE_VERILOG_ANALYSIS_CHECKERS_GENERATE_LABEL_PREFIX_RULE_H_
 
+#include <memory>
 #include <set>
+#include <string>
+#include <string_view>
 
+#include "absl/status/status.h"
+#include "re2/re2.h"
 #include "verible/common/analysis/lint-rule-status.h"
 #include "verible/common/analysis/syntax-tree-lint-rule.h"
 #include "verible/common/text/symbol.h"
@@ -27,20 +32,28 @@
 namespace analysis {
 
 // GenerateLabelPrefixRule checks that all generate block labels start
-// with g_ or gen_
+// with g_ or gen_ (configurable via style_regex)
 class GenerateLabelPrefixRule : public verible::SyntaxTreeLintRule {
  public:
   using rule_type = verible::SyntaxTreeLintRule;
 
+  GenerateLabelPrefixRule();
+
   static const LintRuleDescriptor &GetDescriptor();
 
+  std::string CreateViolationMessage() const;
+
   void HandleSymbol(const verible::Symbol &symbol,
                     const verible::SyntaxTreeContext &context) final;
 
   verible::LintRuleStatus Report() const final;
 
+  absl::Status Configure(std::string_view configuration) final;
+
  private:
   std::set<verible::LintViolation> violations_;
+
+  std::unique_ptr<re2::RE2> style_regex_;
 };
 
 }  // namespace analysis
diff --git a/verible/verilog/analysis/checkers/generate-label-prefix-rule_test.cc b/verible/verilog/analysis/checkers/generate-label-prefix-rule_test.cc
index b46bfe4..cb43501 100644
--- a/verible/verilog/analysis/checkers/generate-label-prefix-rule_test.cc
+++ b/verible/verilog/analysis/checkers/generate-label-prefix-rule_test.cc
@@ -27,6 +27,7 @@
 namespace {
 
 using verible::LintTestCase;
+using verible::RunConfiguredLintTestCases;
 using verible::RunLintTestCases;
 
 TEST(GenerateLabelPrefixRuleTest, Various) {
@@ -229,6 +230,40 @@
   RunLintTestCases<VerilogAnalyzer, GenerateLabelPrefixRule>(kTestCases);
 }
 
+TEST(GenerateLabelPrefixRuleTest, CustomRegex) {
+  const std::initializer_list<LintTestCase> kTestCases = {
+      {"module m;\n"
+       "generate\n"
+       "if (1) begin : my_custom_label\n"
+       "end\n"
+       "endgenerate\nendmodule\n"},
+      {"module m;\n"
+       "generate\n"
+       "for (genvar i=0; i<5; i++) begin : my_custom_label\n"
+       "end\n"
+       "endgenerate\nendmodule\n"},
+  };
+  RunConfiguredLintTestCases<VerilogAnalyzer, GenerateLabelPrefixRule>(
+      kTestCases, "style_regex:my_.*");
+
+  const std::initializer_list<LintTestCase> kFailCases = {
+      {"module m;\n"
+       "generate\n"
+       "if (1) begin : ",
+       {SymbolIdentifier, "g_label"},
+       "\nend\n"
+       "endgenerate\nendmodule\n"},
+      {"module m;\n"
+       "generate\n"
+       "if (1) begin : ",
+       {SymbolIdentifier, "gen_label"},
+       "\nend\n"
+       "endgenerate\nendmodule\n"},
+  };
+  RunConfiguredLintTestCases<VerilogAnalyzer, GenerateLabelPrefixRule>(
+      kFailCases, "style_regex:my_.*");
+}
+
 }  // namespace
 }  // namespace analysis
 }  // namespace verilog
diff --git a/verible/verilog/analysis/flow-tree.h b/verible/verilog/analysis/flow-tree.h
index d099bae..33bafa0 100644
--- a/verible/verilog/analysis/flow-tree.h
+++ b/verible/verilog/analysis/flow-tree.h
@@ -77,7 +77,7 @@
   using VariantReceiver = std::function<bool(const Variant &variant)>;
 
   explicit FlowTree(verible::TokenSequence source_sequence)
-      : source_sequence_(std::move(source_sequence)){};
+      : source_sequence_(std::move(source_sequence)) {};
 
   // Generates all possible variants.
   absl::Status GenerateVariants(const VariantReceiver &receiver);
diff --git a/verible/verilog/analysis/symbol-table.cc b/verible/verilog/analysis/symbol-table.cc
index 9847f46..7d51108 100644
--- a/verible/verilog/analysis/symbol-table.cc
+++ b/verible/verilog/analysis/symbol-table.cc
@@ -89,6 +89,9 @@
       {"class", SymbolMetaType::kClass},
       {"module", SymbolMetaType::kModule},
       {"package", SymbolMetaType::kPackage},
+      {"generate", SymbolMetaType::kGenerate},
+      {"procedural block", SymbolMetaType::kProceduralBlock},
+      {"loop scope", SymbolMetaType::kLoopScope},
       {"parameter", SymbolMetaType::kParameter},
       {"typedef", SymbolMetaType::kTypeAlias},
       {"data/net/var/instance", SymbolMetaType::kDataNetVariableInstance},
@@ -98,6 +101,8 @@
       {"enum", SymbolMetaType::kEnumType},
       {"<enum constant>", SymbolMetaType::kEnumConstant},
       {"interface", SymbolMetaType::kInterface},
+      {"genvar", SymbolMetaType::kGenvar},
+      {"generate loop", SymbolMetaType::kGenerateLoop},
       {"<unspecified>", SymbolMetaType::kUnspecified},
       {"<callable>", SymbolMetaType::kCallable},
   });
@@ -210,6 +215,18 @@
       case NodeEnum::kGenerateElseClause:
         DeclareGenerateElse(node);
         break;
+
+      // separate cases for handling different parts of generate construct
+      case NodeEnum::kLoopGenerateConstruct:
+        DeclareGenerateLoop(node);
+        break;
+      case NodeEnum::kGenerateBlock:
+        DeclareGenerateBlock(node);
+        break;
+      case NodeEnum::kGenvarDeclaration:
+        DeclareGenvarDeclaration(node);
+        break;
+
       case NodeEnum::kPackageDeclaration:
         DeclarePackage(node);
         break;
@@ -306,6 +323,18 @@
         // TODO(#1241) Not handled right now.
         // TODO(#1255) Not handled right now.
         break;
+      // separate case for blocks
+      case NodeEnum::kSeqBlock:
+        DeclareSequentialBlock(node);
+        break;
+      // separate case for for-loops
+      case NodeEnum::kForLoopStatement:
+        DeclareForLoop(node);
+        break;
+      // separate case for for-loop initialization
+      case NodeEnum::kForInitialization:
+        DeclareForInitVariable(node);
+        break;
       default:
         Descend(node);
         break;
@@ -1242,28 +1271,17 @@
                                    SymbolMetaType::kModule);
   }
 
-  std::string_view GetScopeNameFromGenerateBody(const SyntaxTreeNode &body) {
-    if (body.MatchesTag(NodeEnum::kGenerateBlock)) {
-      const SyntaxTreeNode *gen_block = GetGenerateBlockBegin(body);
-      const TokenInfo *label =
-          gen_block ? GetBeginLabelTokenInfo(*gen_block) : nullptr;
-      if (label != nullptr) {
-        // TODO: Check for a matching end-label here, and if its name matches
-        // the begin label, then immediately create a resolved reference because
-        // it only makes sense for it resolve to this begin.
-        // Otherwise, do nothing with the end label.
-        return label->text();
-      }
-    }
-    return current_scope_->Value().CreateAnonymousScope("generate");
-  }
-
   void DeclareGenerateIf(const SyntaxTreeNode &generate_if) {
     const SyntaxTreeNode *body(GetIfClauseGenerateBody(generate_if));
     if (body) {
-      DeclareScopedElementAndDescend(generate_if,
-                                     GetScopeNameFromGenerateBody(*body),
-                                     SymbolMetaType::kGenerate);
+      if (body->MatchesTag(NodeEnum::kGenerateBlock)) {
+        Descend(generate_if);
+      } else {
+        DeclareScopedElementAndDescend(
+            generate_if,
+            current_scope_->Value().CreateAnonymousScope("generate"),
+            SymbolMetaType::kGenerate);
+      }
     }
   }
 
@@ -1276,13 +1294,79 @@
       // and let the if-clause inside create a scope directly under the current
       // scope.
       Descend(*body);
+    } else if (body->MatchesTag(NodeEnum::kGenerateBlock)) {
+      Descend(generate_else);
     } else {
-      DeclareScopedElementAndDescend(generate_else,
-                                     GetScopeNameFromGenerateBody(*body),
+      DeclareScopedElementAndDescend(
+          generate_else,
+          current_scope_->Value().CreateAnonymousScope("generate"),
+          SymbolMetaType::kGenerate);
+    }
+  }
+
+  void DeclareGenerateLoop(const SyntaxTreeNode &node) {
+    const std::string_view scope_name =
+        current_scope_->Value().CreateAnonymousScope("generate-for");
+    DeclareScopedElementAndDescend(node, scope_name,
+                                   SymbolMetaType::kGenerateLoop);
+  }
+
+  void DeclareGenerateBlock(const SyntaxTreeNode &node) {
+    const SyntaxTreeNode *gen_block = GetGenerateBlockBegin(node);
+    const TokenInfo *label =
+        gen_block ? GetBeginLabelTokenInfo(*gen_block) : nullptr;
+    if (label != nullptr) {
+      DeclareScopedElementAndDescend(node, label->text(),
+                                     SymbolMetaType::kGenerate);
+    } else {
+      const std::string_view scope_name =
+          current_scope_->Value().CreateAnonymousScope("generate");
+      DeclareScopedElementAndDescend(node, scope_name,
                                      SymbolMetaType::kGenerate);
     }
   }
 
+  void DeclareGenvarDeclaration(const SyntaxTreeNode &node) {
+    const auto *identifier_list =
+        GetSubtreeAsSymbol(node, NodeEnum::kGenvarDeclaration, 1);
+    if (!identifier_list) return;
+
+    const auto *list_node =
+        verible::down_cast<const SyntaxTreeNode *>(identifier_list);
+    for (const auto &child : list_node->children()) {
+      if (child && child->Kind() == verible::SymbolKind::kLeaf) {
+        const SyntaxTreeLeaf &leaf = verible::SymbolCastToLeaf(*child);
+        if (leaf.get().token_enum() == verilog_tokentype::SymbolIdentifier) {
+          EmplaceElementInCurrentScope(leaf, leaf.get().text(),
+                                       SymbolMetaType::kGenvar);
+        }
+      }
+    }
+  }
+
+  void DeclareSequentialBlock(const SyntaxTreeNode &node) {
+    // Procedural begin...end blocks introduce a new lexical scope.
+    // Extract optional label from the kBegin child for scope naming.
+    const verible::TokenInfo *label = nullptr;
+    if (!node.empty() && node.front() != nullptr) {
+      label = GetBeginLabelTokenInfo(*node.front());
+    }
+    const std::string_view scope_name =
+        label ? label->text()
+              : current_scope_->Value().CreateAnonymousScope("block");
+    DeclareScopedElementAndDescend(node, scope_name,
+                                   SymbolMetaType::kProceduralBlock);
+  }
+
+  void DeclareForLoop(const SyntaxTreeNode &node) {
+    // for-loop statements introduce a scope that encompasses both the
+    // initialization variables and the loop body.
+    const std::string_view scope_name =
+        current_scope_->Value().CreateAnonymousScope("for");
+    DeclareScopedElementAndDescend(node, scope_name,
+                                   SymbolMetaType::kLoopScope);
+  }
+
   void DeclarePackage(const SyntaxTreeNode &package) {
     const auto *token = GetPackageNameToken(package);
     if (!token) return;
@@ -1432,6 +1516,43 @@
     VLOG(2) << "end of " << __FUNCTION__;
   }
 
+  // Declares a variable from a for-loop initialization (e.g. `int i = 0`).
+  // Handles both explicit `genvar` and standard typed declarations.
+  void DeclareForInitVariable(const SyntaxTreeNode &for_init) {
+    const verible::SyntaxTreeLeaf *genvar_keyword =
+        GetGenvarKeywordFromForInitialization(for_init);
+    if (genvar_keyword) {
+      const verible::SyntaxTreeLeaf *var_name =
+          GetVariableNameFromForInitialization(for_init);
+      if (var_name) {
+        EmplaceElementInCurrentScope(*var_name, var_name->get().text(),
+                                     SymbolMetaType::kGenvar);
+      }
+      Descend(for_init);
+      return;
+    }
+
+    const verible::SyntaxTreeNode *data_type =
+        GetDataTypeFromForInitialization(for_init);
+    if (data_type == nullptr) {
+      // not a variable declaration. Descend normally for references.
+      Descend(for_init);
+      return;
+    }
+    const verible::SyntaxTreeLeaf *var_name =
+        GetVariableNameFromForInitialization(for_init);
+    if (var_name == nullptr) {
+      Descend(for_init);
+      return;
+    }
+    DeclarationTypeInfo decl_type_info;
+    const ValueSaver<DeclarationTypeInfo *> save_type(&declaration_type_info_,
+                                                      &decl_type_info);
+    Descend(for_init);
+    EmplaceTypedElementInCurrentScope(for_init, var_name->get().text(),
+                                      SymbolMetaType::kDataNetVariableInstance);
+  }
+
   // Declare one (of potentially multiple) instances in a single declaration
   // statement.
   void DeclareInstance(const SyntaxTreeNode &instance) {
diff --git a/verible/verilog/analysis/symbol-table.h b/verible/verilog/analysis/symbol-table.h
index 6632b81..92fa30d 100644
--- a/verible/verilog/analysis/symbol-table.h
+++ b/verible/verilog/analysis/symbol-table.h
@@ -53,7 +53,9 @@
   kRoot,
   kClass,
   kModule,
-  kGenerate,  // loop or conditional generate block
+  kGenerate,         // loop or conditional generate block
+  kProceduralBlock,  // begin...end block
+  kLoopScope,        // for loop body and init
   kPackage,
   kParameter,
   kTypeAlias,  // typedef
@@ -64,6 +66,8 @@
   kEnumType,
   kEnumConstant,
   kInterface,
+  kGenvar,
+  kGenerateLoop,
 
   // The following enums represent classes/groups of the above types,
   // and are used for validating metatypes of symbol references.
diff --git a/verible/verilog/analysis/symbol-table_test.cc b/verible/verilog/analysis/symbol-table_test.cc
index cec3c88..3e6b1a6 100644
--- a/verible/verilog/analysis/symbol-table_test.cc
+++ b/verible/verilog/analysis/symbol-table_test.cc
@@ -73,8 +73,7 @@
   const auto found_##dest(map.find(key)); /* iterator */ \
   ASSERT_NE(found_##dest, map.end())                     \
       << "No element at \"" << key << "\" in " #map;     \
-  const auto &dest ABSL_ATTRIBUTE_UNUSED(                \
-      found_##dest->second); /* mapped_type */
+  const auto &dest [[maybe_unused]] (found_##dest->second); /* mapped_type */
 
 // Assert that container is not empty, and reference its first element.
 // Works on any container type with .begin().
@@ -103,7 +102,7 @@
       << "No symbol at \"" << key << "\" in " << ScopePathPrinter{scope}; \
   EXPECT_EQ(found_##dest->first, key);                                    \
   const SymbolTableNode &dest(found_##dest->second);                      \
-  const SymbolInfo &dest##_info ABSL_ATTRIBUTE_UNUSED(dest.Value())
+  const SymbolInfo &dest##_info [[maybe_unused]] (dest.Value())
 
 // For SymbolInfo::references_map_view_type only: Assert that there is exactly
 // one element at 'key' in 'map' and assign it to 'dest' (DependentReferences).
@@ -713,6 +712,320 @@
   }
 }
 
+TEST_F(BuildSymbolTableTest, SeqBlockCreatesAnonymousScope) {
+  TestVerilogSourceFile src("foobar.sv",
+                            "module m;\n"
+                            "  initial begin\n"
+                            "    int x;\n"
+                            "  end\n"
+                            "endmodule\n");
+  const auto status = src.Parse();
+  ASSERT_TRUE(status.ok()) << status.message();
+  SymbolTable symbol_table(nullptr);
+  const SymbolTableNode &root_symbol(symbol_table.Root());
+
+  const auto build_diagnostics = BuildSymbolTable(src, &symbol_table);
+  EXPECT_EMPTY_STATUSES(build_diagnostics);
+
+  MUST_ASSIGN_LOOKUP_SYMBOL(module_node, root_symbol, "m");
+  EXPECT_EQ(module_node_info.metatype, SymbolMetaType::kModule);
+
+  // The begin...end block should create a child scope.
+  ASSERT_EQ(module_node.Children().size(), 1);
+  const SymbolTableNode &block(module_node.Children().begin()->second);
+  const SymbolInfo &block_info(block.Value());
+  EXPECT_EQ(block_info.metatype, SymbolMetaType::kProceduralBlock);
+
+  // 'x' should be declared inside the block scope, not module scope.
+  MUST_ASSIGN_LOOKUP_SYMBOL(var_x, block, "x");
+  EXPECT_EQ(var_x_info.metatype, SymbolMetaType::kDataNetVariableInstance);
+
+  {
+    std::vector<absl::Status> resolve_diagnostics;
+    symbol_table.Resolve(&resolve_diagnostics);
+    EXPECT_EMPTY_STATUSES(resolve_diagnostics);
+  }
+}
+
+TEST_F(BuildSymbolTableTest, SeqBlockLabeledScope) {
+  TestVerilogSourceFile src("foobar.sv",
+                            "module m;\n"
+                            "  initial begin : my_block\n"
+                            "    int y;\n"
+                            "  end\n"
+                            "endmodule\n");
+  const auto status = src.Parse();
+  ASSERT_TRUE(status.ok()) << status.message();
+  SymbolTable symbol_table(nullptr);
+  const SymbolTableNode &root_symbol(symbol_table.Root());
+
+  const auto build_diagnostics = BuildSymbolTable(src, &symbol_table);
+  EXPECT_EMPTY_STATUSES(build_diagnostics);
+
+  MUST_ASSIGN_LOOKUP_SYMBOL(module_node, root_symbol, "m");
+  EXPECT_EQ(module_node_info.metatype, SymbolMetaType::kModule);
+
+  // Labeled block should use the label as scope name.
+  MUST_ASSIGN_LOOKUP_SYMBOL(block_node, module_node, "my_block");
+  EXPECT_EQ(block_node_info.metatype, SymbolMetaType::kProceduralBlock);
+
+  MUST_ASSIGN_LOOKUP_SYMBOL(var_y, block_node, "y");
+  EXPECT_EQ(var_y_info.metatype, SymbolMetaType::kDataNetVariableInstance);
+
+  {
+    std::vector<absl::Status> resolve_diagnostics;
+    symbol_table.Resolve(&resolve_diagnostics);
+    EXPECT_EMPTY_STATUSES(resolve_diagnostics);
+  }
+}
+
+TEST_F(BuildSymbolTableTest, SeqBlockVariableShadowing) {
+  // Inner begin...end block declares 'x' which shadows the outer module-level
+  // 'x'.  Both declarations should succeed (no "already defined" error).
+  TestVerilogSourceFile src("foobar.sv",
+                            "module m;\n"
+                            "  int x;\n"
+                            "  initial begin\n"
+                            "    int x;\n"
+                            "  end\n"
+                            "endmodule\n");
+  const auto status = src.Parse();
+  ASSERT_TRUE(status.ok()) << status.message();
+  SymbolTable symbol_table(nullptr);
+  const SymbolTableNode &root_symbol(symbol_table.Root());
+
+  const auto build_diagnostics = BuildSymbolTable(src, &symbol_table);
+  EXPECT_EMPTY_STATUSES(build_diagnostics);
+
+  MUST_ASSIGN_LOOKUP_SYMBOL(module_node, root_symbol, "m");
+  EXPECT_EQ(module_node_info.metatype, SymbolMetaType::kModule);
+
+  // Outer 'x' in module scope.
+  MUST_ASSIGN_LOOKUP_SYMBOL(outer_x, module_node, "x");
+  EXPECT_EQ(outer_x_info.metatype, SymbolMetaType::kDataNetVariableInstance);
+
+  // Block scope exists as a second child of module.
+  EXPECT_EQ(module_node.Children().size(), 2);  // x + anonymous block
+  auto iter = module_node.Children().begin();
+  // Find the block child (skip past 'x').
+  while (iter != module_node.Children().end() &&
+         iter->second.Value().metatype != SymbolMetaType::kProceduralBlock) {
+    ++iter;
+  }
+  ASSERT_NE(iter, module_node.Children().end());
+  const SymbolTableNode &block(iter->second);
+
+  // Inner 'x' lives inside the block scope.
+  MUST_ASSIGN_LOOKUP_SYMBOL(inner_x, block, "x");
+  EXPECT_EQ(inner_x_info.metatype, SymbolMetaType::kDataNetVariableInstance);
+
+  {
+    std::vector<absl::Status> resolve_diagnostics;
+    symbol_table.Resolve(&resolve_diagnostics);
+    EXPECT_EMPTY_STATUSES(resolve_diagnostics);
+  }
+}
+
+TEST_F(BuildSymbolTableTest, GenerateForVariableScope) {
+  TestVerilogSourceFile src("foobar.sv",
+                            "module m;\n"
+                            "  for (genvar i = 0; i < 5; i++) begin : gen_blk\n"
+                            "    int j;\n"
+                            "  end\n"
+                            "endmodule\n");
+  const auto status = src.Parse();
+  ASSERT_TRUE(status.ok()) << status.message();
+  SymbolTable symbol_table(nullptr);
+  const SymbolTableNode &root_symbol(symbol_table.Root());
+
+  const auto build_diagnostics = BuildSymbolTable(src, &symbol_table);
+  EXPECT_EMPTY_STATUSES(build_diagnostics);
+
+  MUST_ASSIGN_LOOKUP_SYMBOL(module_node, root_symbol, "m");
+
+  // Module has one child: the generate for loop scope (anonymous).
+  ASSERT_EQ(module_node.Children().size(), 1);
+  const SymbolTableNode &for_scope(module_node.Children().begin()->second);
+  EXPECT_EQ(for_scope.Value().metatype, SymbolMetaType::kGenerateLoop);
+
+  // 'i' is declared in the for-loop scope.
+  MUST_ASSIGN_LOOKUP_SYMBOL(var_i, for_scope, "i");
+  EXPECT_EQ(var_i_info.metatype, SymbolMetaType::kGenvar);
+
+  // The generate for-loop body (begin...end) creates another nested scope
+  // containing 'j'.
+  MUST_ASSIGN_LOOKUP_SYMBOL(body_block, for_scope, "gen_blk");
+  EXPECT_EQ(body_block_info.metatype, SymbolMetaType::kGenerate);
+
+  MUST_ASSIGN_LOOKUP_SYMBOL(var_j, body_block, "j");
+  EXPECT_EQ(var_j_info.metatype, SymbolMetaType::kDataNetVariableInstance);
+
+  {
+    std::vector<absl::Status> resolve_diagnostics;
+    symbol_table.Resolve(&resolve_diagnostics);
+    EXPECT_EMPTY_STATUSES(resolve_diagnostics);
+  }
+}
+
+TEST_F(BuildSymbolTableTest, GenerateForVariableScopeAnonymous) {
+  TestVerilogSourceFile src("foobar.sv",
+                            "module m;\n"
+                            "  for (genvar i = 0; i < 5; i++) begin\n"
+                            "    int j;\n"
+                            "  end\n"
+                            "endmodule\n");
+  const auto status = src.Parse();
+  ASSERT_TRUE(status.ok()) << status.message();
+  SymbolTable symbol_table(nullptr);
+  const SymbolTableNode &root_symbol(symbol_table.Root());
+
+  const auto build_diagnostics = BuildSymbolTable(src, &symbol_table);
+  EXPECT_EMPTY_STATUSES(build_diagnostics);
+
+  MUST_ASSIGN_LOOKUP_SYMBOL(module_node, root_symbol, "m");
+  ASSERT_EQ(module_node.Children().size(), 1);
+  const SymbolTableNode &for_scope(module_node.Children().begin()->second);
+  EXPECT_EQ(for_scope.Value().metatype, SymbolMetaType::kGenerateLoop);
+
+  MUST_ASSIGN_LOOKUP_SYMBOL(var_i, for_scope, "i");
+  EXPECT_EQ(var_i_info.metatype, SymbolMetaType::kGenvar);
+
+  const SymbolTableNode *body_block = nullptr;
+  for (const auto &child : for_scope.Children()) {
+    if (child.second.Value().metatype == SymbolMetaType::kGenerate) {
+      body_block = &child.second;
+      break;
+    }
+  }
+  ASSERT_NE(body_block, nullptr);
+  MUST_ASSIGN_LOOKUP_SYMBOL(var_j, *body_block, "j");
+  EXPECT_EQ(var_j_info.metatype, SymbolMetaType::kDataNetVariableInstance);
+}
+
+TEST_F(BuildSymbolTableTest, StandaloneGenvarDeclaration) {
+  TestVerilogSourceFile src("foobar.sv",
+                            "module m;\n"
+                            "  genvar i, j;\n"
+                            "endmodule\n");
+  const auto status = src.Parse();
+  ASSERT_TRUE(status.ok()) << status.message();
+  SymbolTable symbol_table(nullptr);
+  const SymbolTableNode &root_symbol(symbol_table.Root());
+
+  const auto build_diagnostics = BuildSymbolTable(src, &symbol_table);
+  EXPECT_EMPTY_STATUSES(build_diagnostics);
+
+  MUST_ASSIGN_LOOKUP_SYMBOL(module_node, root_symbol, "m");
+  MUST_ASSIGN_LOOKUP_SYMBOL(var_i, module_node, "i");
+  EXPECT_EQ(var_i_info.metatype, SymbolMetaType::kGenvar);
+  MUST_ASSIGN_LOOKUP_SYMBOL(var_j, module_node, "j");
+  EXPECT_EQ(var_j_info.metatype, SymbolMetaType::kGenvar);
+}
+
+TEST_F(BuildSymbolTableTest, ForLoopVariableScope) {
+  TestVerilogSourceFile src("foobar.sv",
+                            "module m;\n"
+                            "  initial begin\n"
+                            "    for (int i = 0; i < 5; i++) begin\n"
+                            "      int j;\n"
+                            "    end\n"
+                            "  end\n"
+                            "endmodule\n");
+  const auto status = src.Parse();
+  ASSERT_TRUE(status.ok()) << status.message();
+  SymbolTable symbol_table(nullptr);
+  const SymbolTableNode &root_symbol(symbol_table.Root());
+
+  const auto build_diagnostics = BuildSymbolTable(src, &symbol_table);
+  EXPECT_EMPTY_STATUSES(build_diagnostics);
+
+  MUST_ASSIGN_LOOKUP_SYMBOL(module_node, root_symbol, "m");
+
+  // Module has one child: the initial block scope.
+  ASSERT_EQ(module_node.Children().size(), 1);
+  const SymbolTableNode &init_block(module_node.Children().begin()->second);
+  EXPECT_EQ(init_block.Value().metatype, SymbolMetaType::kProceduralBlock);
+
+  // The initial block has one child: the for-loop scope.
+  ASSERT_EQ(init_block.Children().size(), 1);
+  const SymbolTableNode &for_scope(init_block.Children().begin()->second);
+  EXPECT_EQ(for_scope.Value().metatype, SymbolMetaType::kLoopScope);
+
+  // 'i' is declared in the for-loop scope.
+  MUST_ASSIGN_LOOKUP_SYMBOL(var_i, for_scope, "i");
+  EXPECT_EQ(var_i_info.metatype, SymbolMetaType::kDataNetVariableInstance);
+
+  // The for-loop body (begin...end) creates another nested scope containing
+  // 'j'. Find the block scope child (skip past 'i').
+  const SymbolTableNode *body_block = nullptr;
+  for (const auto &child : for_scope.Children()) {
+    if (child.second.Value().metatype == SymbolMetaType::kProceduralBlock) {
+      body_block = &child.second;
+      break;
+    }
+  }
+  ASSERT_NE(body_block, nullptr);
+  MUST_ASSIGN_LOOKUP_SYMBOL(var_j, *body_block, "j");
+  EXPECT_EQ(var_j_info.metatype, SymbolMetaType::kDataNetVariableInstance);
+
+  {
+    std::vector<absl::Status> resolve_diagnostics;
+    symbol_table.Resolve(&resolve_diagnostics);
+    EXPECT_EMPTY_STATUSES(resolve_diagnostics);
+  }
+}
+
+TEST_F(BuildSymbolTableTest, ForLoopVariableShadowing) {
+  // Module-level 'i' and for-loop 'i' should not collide.
+  TestVerilogSourceFile src("foobar.sv",
+                            "module m;\n"
+                            "  int i;\n"
+                            "  initial begin\n"
+                            "    for (int i = 0; i < 5; i++) begin\n"
+                            "    end\n"
+                            "  end\n"
+                            "endmodule\n");
+  const auto status = src.Parse();
+  ASSERT_TRUE(status.ok()) << status.message();
+  SymbolTable symbol_table(nullptr);
+  const SymbolTableNode &root_symbol(symbol_table.Root());
+
+  const auto build_diagnostics = BuildSymbolTable(src, &symbol_table);
+  EXPECT_EMPTY_STATUSES(build_diagnostics);
+
+  MUST_ASSIGN_LOOKUP_SYMBOL(module_node, root_symbol, "m");
+
+  // Module-level 'i' exists.
+  MUST_ASSIGN_LOOKUP_SYMBOL(outer_i, module_node, "i");
+  EXPECT_EQ(outer_i_info.metatype, SymbolMetaType::kDataNetVariableInstance);
+
+  // Find the for-loop scope (nested inside the initial block scope).
+  const SymbolTableNode *for_scope = nullptr;
+  for (const auto &child : module_node.Children()) {
+    if (child.second.Value().metatype == SymbolMetaType::kProceduralBlock) {
+      // This is the initial block. Search inside for the for-loop scope.
+      for (const auto &grandchild : child.second.Children()) {
+        if (grandchild.second.Value().metatype == SymbolMetaType::kLoopScope) {
+          for_scope = &grandchild.second;
+          break;
+        }
+      }
+      break;
+    }
+  }
+  ASSERT_NE(for_scope, nullptr);
+
+  // For-loop 'i' lives in the for-loop scope.
+  MUST_ASSIGN_LOOKUP_SYMBOL(loop_i, *for_scope, "i");
+  EXPECT_EQ(loop_i_info.metatype, SymbolMetaType::kDataNetVariableInstance);
+
+  {
+    std::vector<absl::Status> resolve_diagnostics;
+    symbol_table.Resolve(&resolve_diagnostics);
+    EXPECT_EMPTY_STATUSES(resolve_diagnostics);
+  }
+}
+
 TEST_F(BuildSymbolTableTest, ModuleDeclarationWithPorts) {
   TestVerilogSourceFile src("foobar.sv",
                             "module m (\n"
diff --git a/verible/verilog/analysis/verilog-filelist.h b/verible/verilog/analysis/verilog-filelist.h
index 0bb474a..915eb52 100644
--- a/verible/verilog/analysis/verilog-filelist.h
+++ b/verible/verilog/analysis/verilog-filelist.h
@@ -27,7 +27,7 @@
 // TODO(karimtera): Using "MacroDefiniton" struct might be better.
 struct TextMacroDefinition {
   TextMacroDefinition(std::string name, std::string value)
-      : name(std::move(name)), value(std::move(value)){};
+      : name(std::move(name)), value(std::move(value)) {};
   std::string name;
   std::string value;
 
diff --git a/verible/verilog/formatting/BUILD b/verible/verilog/formatting/BUILD
index 6150594..6431226 100644
--- a/verible/verilog/formatting/BUILD
+++ b/verible/verilog/formatting/BUILD
@@ -163,7 +163,6 @@
         "//verible/common/util:expandable-tree-view",
         "//verible/common/util:interval",
         "//verible/common/util:interval-set",
-        "//verible/common/util:iterator-range",
         "//verible/common/util:logging",
         "//verible/common/util:spacer",
         "//verible/common/util:tree-operations",
@@ -175,7 +174,6 @@
         "//verible/verilog/analysis:verilog-equivalence",
         "//verible/verilog/parser:verilog-token-enum",
         "//verible/verilog/preprocessor:verilog-preprocess",
-        "@abseil-cpp//absl/base:core_headers",
         "@abseil-cpp//absl/log:die_if_null",
         "@abseil-cpp//absl/status",
         "@abseil-cpp//absl/status:statusor",
diff --git a/verible/verilog/formatting/align.cc b/verible/verilog/formatting/align.cc
index 1f7f472..6640adb 100644
--- a/verible/verilog/formatting/align.cc
+++ b/verible/verilog/formatting/align.cc
@@ -632,7 +632,9 @@
   kDontCare = 0,
   kNamedActualParameters,
   kNamedActualPorts,
-  kParameterDeclaration,
+  kParameterDeclaration,      // formal parameter list (#(...))
+  kBodyParameterDeclaration,  // parameter/localparam in module/generate/
+                              //   package/interface body
   kPortDeclaration,
   kStructUnionMember,
   kDataDeclaration,  // net/variable declarations
@@ -674,6 +676,13 @@
           return AlignClassify(AlignmentGroupAction::kIgnore);
         }
         const SyntaxTreeNode &node = verible::SymbolCastToNode(*origin);
+        // Align body-level parameter/localparam declarations.
+        if (node.MatchesTag(NodeEnum::kParamDeclaration)) {
+          return AlignClassify(
+              AlignmentGroupAction::kMatch,
+              AlignableSyntaxSubtype::kBodyParameterDeclaration);
+        }
+
         // Align net/variable declarations.
         if (IsAlignableDeclaration(node)) {
           return AlignClassify(AlignmentGroupAction::kMatch,
@@ -837,7 +846,7 @@
       }
       case NodeEnum::kDimensionSlice:
       case NodeEnum::kDimensionAssociativeType: {
-        // all of these cases cover packed and unpacked dimensions
+        // All of these cases cover packed and unpacked dimensions
         ReserveNewColumn(node, FlushLeft);
         break;
       }
@@ -951,7 +960,6 @@
         CHECK_EQ(node.size(), 5);
         auto *column = ABSL_DIE_IF_NULL(ReserveNewColumn(node, FlushLeft));
 
-        SyntaxTreePath np;
         ReserveNewColumn(column, *node[0], FlushLeft);  // '['
 
         auto *value_subcolumn =
@@ -985,9 +993,8 @@
   }
 };
 
-// This class marks up token-subranges in formal parameter declarations for
-// alignment.
-// e.g. "localparam int Width = 5;"
+// This class marks up token-subranges in formal and body-level parameter
+// declarations for alignment.  e.g. "localparam int Width = 5;"
 class ParameterDeclarationColumnSchemaScanner
     : public VerilogColumnSchemaScanner {
  public:
@@ -1061,7 +1068,7 @@
         break;
       }
 
-      // Sometimes the parameter indentifier which is of token SymbolIdentifier
+      // Sometimes the parameter identifier which is of token SymbolIdentifier
       // can appear at different paths depending on the parameter type. Make
       // them aligned so they fall under the same column.
       case verilog_tokentype::SymbolIdentifier: {
@@ -1090,7 +1097,7 @@
         break;
       }
 
-      // Align packed and unpacked dimenssions
+      // Align packed and unpacked dimensions
       case '[': {
         if (verilog::analysis::ContextIsInsideDeclarationDimensions(
                 Context()) &&
@@ -1456,6 +1463,11 @@
             ParameterDeclarationColumnSchemaScanner>(non_tree_column_scanner),
         function_from_pointer_to_member(
             &FormatStyle::formal_parameters_alignment)}},
+      {AlignableSyntaxSubtype::kBodyParameterDeclaration,
+       {UnstyledAlignmentCellScannerGenerator<
+            ParameterDeclarationColumnSchemaScanner>(non_tree_column_scanner),
+        function_from_pointer_to_member(
+            &FormatStyle::parameter_declaration_alignment)}},
       {AlignableSyntaxSubtype::kPortDeclaration,
        {UnstyledAlignmentCellScannerGenerator<
             PortDeclarationColumnSchemaScanner>(non_tree_column_scanner),
@@ -1598,8 +1610,9 @@
 
 static std::vector<AlignablePartitionGroup> AlignModuleItems(
     const TokenPartitionRange &full_range, const FormatStyle &vstyle) {
-  // Currently, this only handles data/net/variable declarations.
-  // TODO(b/161814377): align continuous assignments
+  // Applies to module/interface, generate, and package item lists.
+  // Handles data/net/variable declarations, parameter/localparam
+  // declarations, and continuous assignment statements.
   auto group_extractor = [&vstyle](const TokenPartitionRange &range) {
     return GetConsecutiveModuleItemGroups(range,
                                           vstyle.alignment_group_boundary);
@@ -1635,6 +1648,10 @@
       &IgnoreCommentsAndPreprocessingDirectives, full_range, vstyle);
 }
 
+// Aligns formal parameters in #(...) headers (module/interface/class port
+// parameter lists).  Body-level parameter/localparam declarations are
+// handled by AlignModuleItems which dispatches through
+// GetConsecutiveModuleItemGroups.
 static std::vector<AlignablePartitionGroup> AlignParameterDeclarations(
     const TokenPartitionRange &full_range, const FormatStyle &vstyle) {
   return ExtractAlignablePartitionGroups(
@@ -1681,8 +1698,11 @@
           {NodeEnum::kStructUnionMemberList, &AlignStructUnionMembers},
           {NodeEnum::kActualParameterByNameList, &AlignActualNamedParameters},
           {NodeEnum::kPortActualList, &AlignActualNamedPorts},
+          // module/interface bodies
           {NodeEnum::kModuleItemList, &AlignModuleItems},
           {NodeEnum::kGenerateItemList, &AlignModuleItems},
+          // package bodies
+          {NodeEnum::kPackageItemList, &AlignModuleItems},
           {NodeEnum::kFormalParameterList, &AlignParameterDeclarations},
           {NodeEnum::kClassItems, &AlignClassItems},
           // various case-like constructs:
diff --git a/verible/verilog/formatting/format-style-init.cc b/verible/verilog/formatting/format-style-init.cc
index 0fa91af..a6d010b 100644
--- a/verible/verilog/formatting/format-style-init.cc
+++ b/verible/verilog/formatting/format-style-init.cc
@@ -105,13 +105,20 @@
 ABSL_FLAG(AlignmentPolicy, named_port_alignment,
           AlignmentPolicy::kInferUserIntent,
           "Format named port connections: {align,flush-left,preserve,infer}");
-ABSL_FLAG(
-    AlignmentPolicy, module_net_variable_alignment,  //
-    AlignmentPolicy::kInferUserIntent,
-    "Format net/variable declarations: {align,flush-left,preserve,infer}");
+ABSL_FLAG(AlignmentPolicy, module_net_variable_alignment,
+          AlignmentPolicy::kInferUserIntent,
+          "Format net/variable declarations in module, generate, "
+          "interface, and package bodies: {align,flush-left,preserve,infer}");
 ABSL_FLAG(AlignmentPolicy, formal_parameters_alignment,
           AlignmentPolicy::kInferUserIntent,
-          "Format formal parameters: {align,flush-left,preserve,infer}");
+          "Format formal parameters in module/interface/class headers "
+          "(inside #(...)): {align,flush-left,preserve,infer}");
+ABSL_FLAG(AlignmentPolicy, parameter_declaration_alignment,
+          AlignmentPolicy::kInferUserIntent,
+          "Format parameter/localparam declarations in module, generate, "
+          "interface, and package bodies "
+          "(class body parameter declarations are NOT affected): "
+          "{align,flush-left,preserve,infer}");
 ABSL_FLAG(AlignmentPolicy, class_member_variable_alignment,
           AlignmentPolicy::kInferUserIntent,
           "Format class member variables: {align,flush-left,preserve,infer}");
@@ -123,7 +130,8 @@
           "Align distribution items: {align,flush-left,preserve,infer}");
 ABSL_FLAG(AlignmentPolicy, assignment_statement_alignment,
           AlignmentPolicy::kInferUserIntent,
-          "Format various assignments: {align,flush-left,preserve,infer}");
+          "Format various assignments in module, generate, interface, "
+          "and package bodies: {align,flush-left,preserve,infer}");
 ABSL_FLAG(AlignmentPolicy, enum_assignment_statement_alignment,
           AlignmentPolicy::kInferUserIntent,
           "Format assignments with enums: {align,flush-left,preserve,infer}");
@@ -132,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");
 
@@ -167,7 +182,7 @@
 
 #define STYLE_FROM_FLAG(name) style->name = absl::GetFlag(FLAGS_##name)
 
-  // Simply in the sequence as declared in struct FormatStyle
+  // In the same sequence as declared in struct FormatStyle
   STYLE_FROM_FLAG(port_declarations_indentation);
   STYLE_FROM_FLAG(port_declarations_alignment);
   STYLE_FROM_FLAG(struct_union_members_alignment);
@@ -180,6 +195,7 @@
   STYLE_FROM_FLAG(enum_assignment_statement_alignment);
   STYLE_FROM_FLAG(formal_parameters_indentation);
   STYLE_FROM_FLAG(formal_parameters_alignment);
+  STYLE_FROM_FLAG(parameter_declaration_alignment);
   STYLE_FROM_FLAG(class_member_variable_alignment);
   STYLE_FROM_FLAG(case_items_alignment);
   STYLE_FROM_FLAG(distribution_items_alignment);
@@ -188,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 e73cf14..55347c5 100644
--- a/verible/verilog/formatting/format-style.h
+++ b/verible/verilog/formatting/format-style.h
@@ -25,7 +25,7 @@
 namespace verilog {
 namespace formatter {
 
-// Controls what breaks alignment groups for module items, statements,
+// Control what breaks alignment groups for module items, statements,
 // and class items.
 enum class AlignmentGroupBoundary {
   // No additional group splitting (default, current behavior).
@@ -52,11 +52,9 @@
 
   FormatStyle(const FormatStyle &) = default;
 
-  /*
-   * InitializeFromFlags() [format_style_init.h] provides flags that are
-   * named like these fields and allow configuration on the command line.
-   * So field foo here can be configured with flag --foo
-   */
+  // InitializeFromFlags() [format-style-init.cc] provides flags that are
+  // named like these fields and allow configuration on the command line.
+  // So field foo here can be configured with flag --foo.
 
   // TODO(hzeller): some of these are plural, some singular. Come up with
   // a consistent scheme.
@@ -86,25 +84,37 @@
   AlignmentPolicy named_port_alignment = AlignmentPolicy::kAlign;
 
   // Control how module-local net/variable declarations are formatted.
+  // Applies in module, generate, interface, and package bodies.
   // Internal tests assume these are forced to kAlign.
   AlignmentPolicy module_net_variable_alignment = AlignmentPolicy::kAlign;
 
   // Control how various assignment statements should be aligned.
+  // Applies in module, generate, interface, and package bodies.
   // This covers: continuous assignment statements,
   // blocking, and nonblocking assignments.
   // Internal tests assume these are forced to kAlign.
   AlignmentPolicy assignment_statement_alignment = AlignmentPolicy::kAlign;
 
-  // Assignment within enumerations.
+  // Control how assignments in enumerations should be aligned.
   AlignmentPolicy enum_assignment_statement_alignment = AlignmentPolicy::kAlign;
 
   // Control indentation amount for formal parameter declarations.
   IndentationStyle formal_parameters_indentation = IndentationStyle::kWrap;
 
-  // Control how formal parameters in modules/interfaces/classes are formatted.
+  // Control how formal parameters in module/interface/class headers
+  // (inside #(...)) are formatted.  For parameter/localparam
+  // declarations in module, generate, interface, and package bodies,
+  // see parameter_declaration_alignment.
   // Internal tests assume these are forced to kAlign.
   AlignmentPolicy formal_parameters_alignment = AlignmentPolicy::kAlign;
 
+  // Control how parameter/localparam declarations are formatted.
+  // Applies in module, generate, interface, and package bodies.
+  // Class body parameter declarations are not affected.
+  // For formal parameters in #(...) headers, see formal_parameters_alignment.
+  // Internal tests assume these are forced to kAlign.
+  AlignmentPolicy parameter_declaration_alignment = AlignmentPolicy::kAlign;
+
   // Control how class member variables are formatted.
   // Internal tests assume these are forced to kAlign.
   AlignmentPolicy class_member_variable_alignment = AlignmentPolicy::kAlign;
@@ -120,7 +130,7 @@
   bool port_declarations_right_align_packed_dimensions = false;
   bool port_declarations_right_align_unpacked_dimensions = false;
 
-  // Controls what breaks alignment groups for module items, statements,
+  // Control what breaks alignment groups for module items, statements,
   // and class items.
   AlignmentGroupBoundary alignment_group_boundary =
       AlignmentGroupBoundary::kNone;
@@ -136,10 +146,13 @@
   // 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;
 
-  // -- Note: when adding new fields, add them in format_style_init.cc
+  // -- Note: When adding new fields, add them in format-style-init.cc
 
   // TODO(fangism): introduce the following knobs:
   //
@@ -174,12 +187,16 @@
                : indentation_spaces;
   }
 
+  // -- Note: When adding a new AlignmentPolicy field, add it here
+  // and to InitializeFromFlags in format-style-init.cc.
   void ApplyToAllAlignmentPolicies(AlignmentPolicy policy) {
     port_declarations_alignment = policy;
+    struct_union_members_alignment = policy;
     named_parameter_alignment = policy;
     named_port_alignment = policy;
     module_net_variable_alignment = policy;
     formal_parameters_alignment = policy;
+    parameter_declaration_alignment = policy;
     class_member_variable_alignment = policy;
     case_items_alignment = policy;
     assignment_statement_alignment = policy;
diff --git a/verible/verilog/formatting/formatter.cc b/verible/verilog/formatting/formatter.cc
index 9893b60..9bd3dd2 100644
--- a/verible/verilog/formatting/formatter.cc
+++ b/verible/verilog/formatting/formatter.cc
@@ -26,7 +26,6 @@
 #include <string_view>
 #include <vector>
 
-#include "absl/base/attributes.h"
 #include "absl/log/die_if_null.h"
 #include "absl/status/status.h"
 #include "absl/status/statusor.h"
@@ -50,7 +49,6 @@
 #include "verible/common/util/expandable-tree-view.h"
 #include "verible/common/util/interval-set.h"
 #include "verible/common/util/interval.h"
-#include "verible/common/util/iterator-range.h"
 #include "verible/common/util/logging.h"
 #include "verible/common/util/spacer.h"
 #include "verible/common/util/tree-operations.h"
@@ -749,15 +747,24 @@
       case verible::SpacingDecision::kPreserve: {
         if (token.before.preserved_space_start !=
             verible::string_view_null_iterator()) {
-          *column += token.OriginalLeadingSpaces().length();
+          const std::string_view leading = token.OriginalLeadingSpaces();
+          const auto last_nl = leading.find_last_of('\n');
+          if (last_nl == std::string_view::npos) {
+            *column += leading.length();
+          } else {
+            // Reset column after the last newline, same as FormattedToken
+            // emit (GitHub issue 2542).
+            *column = static_cast<int>(leading.length() - last_nl - 1);
+          }
         } else {
           *column += token.before.spaces;
         }
         break;
       }
       case verible::SpacingDecision::kWrap:
-        *column = 0;
-        ABSL_FALLTHROUGH_INTENDED;
+        // Newline then only the wrap indent (same as FormattedToken emit).
+        *column = token.before.spaces;
+        break;
       case verible::SpacingDecision::kAlign:
       case verible::SpacingDecision::kAppend:
         *column += token.before.spaces;
@@ -766,6 +773,15 @@
   }
 
   static int CalculateEolCommentColumn(const verible::FormattedExcerpt &line) {
+    // Compute the starting column of the trailing EOL comment the same way
+    // FormattedExcerpt::FormattedText emits spaces, including:
+    //   * wrap indents (SpacingDecision::kWrap), and
+    //   * preserved leading whitespace that may contain newlines (common when
+    //     an original line break is kept). Counting those newlines as width
+    //     made continuation comments land on the wrong column and fail to
+    //     converge on re-format (GitHub issue 2542).
+    if (line.Tokens().empty()) return 0;
+
     int column = 0;
     const auto &front = line.Tokens().front();
 
@@ -777,12 +793,16 @@
     }
     column += front.token->text().length();
 
-    for (const auto &ftoken : verible::make_range(line.Tokens().begin() + 1,
-                                                  line.Tokens().end() - 1)) {
+    const auto &tokens = line.Tokens();
+    for (size_t i = 1; i < tokens.size(); ++i) {
+      const auto &ftoken = tokens[i];
       AdjustColumnUsingTokenSpacing(ftoken, &column);
-      column += ftoken.token->text().length();
+      // Do not add the last token's length: that is the EOL comment whose
+      // starting column we want.
+      if (i + 1 < tokens.size()) {
+        column += ftoken.token->text().length();
+      }
     }
-    AdjustColumnUsingTokenSpacing(line.Tokens().back(), &column);
 
     CHECK_GE(column, 0);
     return column;
diff --git a/verible/verilog/formatting/formatter_test.cc b/verible/verilog/formatting/formatter_test.cc
index e885d74..bd0f775 100644
--- a/verible/verilog/formatting/formatter_test.cc
+++ b/verible/verilog/formatting/formatter_test.cc
@@ -162,6 +162,12 @@
      "`define  BAR\n",
      "`define FOO\n"
      "`define BAR\n"},
+    {"`define FOO_``BAR 1\n", "`define FOO_``BAR 1\n"},
+    {"`define FOO_```BAR 1\n", "`define FOO_```BAR 1\n"},
+    {"`define FOO ``BAR\n", "`define FOO ``BAR\n"},
+    {"`define A``B``C 2\n", "`define A``B``C 2\n"},
+    {"`define A(x)``y\n", "`define A(x) ``y\n"},
+    {"`define    FOO_``BAR\n", "`define FOO_``BAR\n"},
     {"`ifndef    FOO\n"
      "`endif // FOO\n",
      "`ifndef FOO\n"
@@ -1503,6 +1509,119 @@
      "          int   b\n"   // direction missing
      ");\n"
      "endmodule\n"},
+    {"module t;\n"
+     "    input bit i_bit;\n"
+     "input   byte  i_byte ;\n"
+     "input chandle i_chandle;\n"
+     "input event i_event;\n"
+     "input int i_int;\n"
+     "input integer i_inte;\n"
+     "input longint i_longint;\n"
+     "input real i_real;\n"
+     "input realtime i_realtime;\n"
+     "input shortint i_shortint;\n"
+     "input shortreal i_shortreal;\n"
+     "input string i_string;\n"
+     "input time i_time;\n"
+     "     output bit o_bit;\n"
+     "output    byte   o_byte  ;  \n"
+     "output chandle o_chandle;\n"
+     "output event o_event;\n"
+     "output int o_int;\n"
+     "output integer o_inte;\n"
+     "output longint o_longint;\n"
+     "output real o_real;\n"
+     "output realtime o_realtime;\n"
+     "output shortint o_shortint;\n"
+     "output shortreal o_shortreal;\n"
+     "output string o_string;\n"
+     "output time o_time;\n"
+     "endmodule\n",
+     "module t;\n"
+     "  input bit i_bit;\n"
+     "  input byte i_byte;\n"
+     "  input chandle i_chandle;\n"
+     "  input event i_event;\n"
+     "  input int i_int;\n"
+     "  input integer i_inte;\n"
+     "  input longint i_longint;\n"
+     "  input real i_real;\n"
+     "  input realtime i_realtime;\n"
+     "  input shortint i_shortint;\n"
+     "  input shortreal i_shortreal;\n"
+     "  input string i_string;\n"
+     "  input time i_time;\n"
+     "  output bit o_bit;\n"
+     "  output byte o_byte;\n"
+     "  output chandle o_chandle;\n"
+     "  output event o_event;\n"
+     "  output int o_int;\n"
+     "  output integer o_inte;\n"
+     "  output longint o_longint;\n"
+     "  output real o_real;\n"
+     "  output realtime o_realtime;\n"
+     "  output shortint o_shortint;\n"
+     "  output shortreal o_shortreal;\n"
+     "  output string o_string;\n"
+     "  output time o_time;\n"
+     "endmodule\n"},
+    {"module t (\n"
+     "    input bit i_bit,\n"
+     "input     byte   i_byte  ,\n"
+     "input chandle i_chandle,\n"
+     "input event i_event,\n"
+     "input int i_int,\n"
+     "input integer i_inte,\n"
+     "input longint i_longint,\n"
+     "input real i_real,\n"
+     "input realtime i_realtime,\n"
+     "input shortint i_shortint,\n"
+     "input shortreal i_shortreal,\n"
+     "input string i_string,\n"
+     "input time i_time,\n"
+     "    output bit o_bit,\n"
+     "output     byte   o_byte  ,  \n"
+     "output chandle o_chandle,\n"
+     "output event o_event,\n"
+     "output int o_int,\n"
+     "output integer o_inte,\n"
+     "output longint o_longint,\n"
+     "output real o_real,\n"
+     "output realtime o_realtime,\n"
+     "output shortint o_shortint,\n"
+     "output shortreal o_shortreal,\n"
+     "output string o_string,\n"
+     "output time o_time);\n"
+     "endmodule\n",
+     "module t (\n"
+     "    input  bit       i_bit,\n"
+     "    input  byte      i_byte,\n"
+     "    input  chandle   i_chandle,\n"
+     "    input  event     i_event,\n"
+     "    input  int       i_int,\n"
+     "    input  integer   i_inte,\n"
+     "    input  longint   i_longint,\n"
+     "    input  real      i_real,\n"
+     "    input  realtime  i_realtime,\n"
+     "    input  shortint  i_shortint,\n"
+     "    input  shortreal i_shortreal,\n"
+     "    input  string    i_string,\n"
+     "    input  time      i_time,\n"
+     "    output bit       o_bit,\n"
+     "    output byte      o_byte,\n"
+     "    output chandle   o_chandle,\n"
+     "    output event     o_event,\n"
+     "    output int       o_int,\n"
+     "    output integer   o_inte,\n"
+     "    output longint   o_longint,\n"
+     "    output real      o_real,\n"
+     "    output realtime  o_realtime,\n"
+     "    output shortint  o_shortint,\n"
+     "    output shortreal o_shortreal,\n"
+     "    output string    o_string,\n"
+     "    output time      o_time\n"
+     ");\n"
+     "endmodule\n"},
     {"module m;foo bar(.baz({larry, moe, curly}));endmodule",
      "module m;\n"
      "  foo bar (.baz({larry, moe, curly}));\n"
@@ -3885,6 +4004,22 @@
      "  endtask\n"
      "  // class is about to end\n"
      "endclass\n"},
+
+    // interface class test cases
+    {"interface class Foo;\nendclass\n",
+     "interface class Foo;\n"
+     "endclass\n"},
+    {"interface   class   Foo  ;  endclass\n",
+     "interface class Foo;\n"
+     "endclass\n"},
+    {"interface class Foo extends Bar , Baz ;\nendclass\n",
+     "interface class Foo extends Bar, Baz;\n"
+     "endclass\n"},
+    {"interface class Foo;\n  pure   virtual   task   foo (  ) ; \nendclass\n",
+     "interface class Foo;\n"
+     "  pure virtual task foo();\n"
+     "endclass\n"},
+
     // class property alignment test cases
     {"class c;\n"
      "int foo  ;\n"
@@ -4343,6 +4478,42 @@
      "    .L(L),\n"
      "    .W(W)\n"
      ") bar_t;\n"},
+    // 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"
+     "    .CFG_T(tl_agent_env_cfg)\n"
+     ") tl_agent_env_cov;\n"},
+    {"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"},
+
+    // 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"},
+
+    // 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",
@@ -4559,6 +4730,15 @@
      "  for (int i = 0; i < f(m); i--) begin\n"
      "  end\n"
      "endfunction\n"},
+    {// for loop with an attribute instance in the initializer.
+     // Regression: this used to abort with a CHECK failure while reshaping
+     // the kForSpec partitions when an attribute appears in the header.
+     "module m; initial for(int i=0(* a *);i<4;i++) x=i; endmodule",
+     "module m;\n"
+     "  initial\n"
+     "    for (int i = 0 (* a *); i < 4; i++)\n"
+     "      x = i;\n"
+     "endmodule\n"},
     {// forever loop
      "function\nvoid\tforevah;forever  begin "
      "++k\n;end endfunction\n",
@@ -8125,6 +8305,22 @@
      "  bit [31:0]          second;\n"
      "  generic_type_name_t third;\n"
      "} type_t;\n"},
+    {"typedef union soft packed {\n"
+     "bit [3:0] first; bit [31:0] second; generic_type_name_t third;\n"
+     "} type_t;",
+     "typedef union soft packed {\n"
+     "  bit [3:0]           first;\n"
+     "  bit [31:0]          second;\n"
+     "  generic_type_name_t third;\n"
+     "} type_t;\n"},
+    {"typedef union soft {\n"
+     "bit [3:0] first; bit [31:0] second; generic_type_name_t third;\n"
+     "} type_t;",
+     "typedef union soft {\n"
+     "  bit [3:0]           first;\n"
+     "  bit [31:0]          second;\n"
+     "  generic_type_name_t third;\n"
+     "} type_t;\n"},
     {"typedef struct {\n"
      "// comment\n"
      "bit [3:0] first; bit [31:0] second; generic_type_name_t third;\n"
@@ -18624,6 +18820,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"
@@ -19216,6 +19456,1737 @@
   }
 }
 
+// Regression for https://github.com/chipsalliance/verible/issues/2542:
+// Continuation EOL comments after a wrapped assign must keep a stable column
+// across re-format (convergence).
+TEST(FormatterEndToEndTest, ContinuationCommentAfterWrappedAssignConverges) {
+  static constexpr FormatterTestCase kTestCases[] = {
+      {// Comments originally column-aligned after a wrapped assign
+       "module m;\n"
+       "  assign status_ur = !(status_sc || status_ca ||\n"
+       "    status_crs);      // Completions with a Reserved Completion\n"
+       "                      // Status value are treated as UR\n"
+       "endmodule\n",
+       "module m;\n"
+       "  assign status_ur =\n"
+       "      !(status_sc || status_ca || status_crs);  // Completions with a "
+       "Reserved Completion\n"
+       "                                                // Status value are "
+       "treated as UR\n"
+       "endmodule\n"},
+      {// Previously mis-aligned continuation is not treated as a continuation
+       // (column delta > 1) and must still converge
+       "module m;\n"
+       "  assign status_ur = !(status_sc || status_ca ||\n"
+       "    status_crs);      // Completions with a Reserved Completion\n"
+       "                                                                       "
+       "// Status value are treated as UR\n"
+       "endmodule\n",
+       "module m;\n"
+       "  assign status_ur =\n"
+       "      !(status_sc || status_ca || status_crs);  // Completions with a "
+       "Reserved Completion\n"
+       "  // Status value are treated as UR\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/2540:
+// Trailing EOL comment after `end` before `else if` must not change whether
+// the else-if assignment stays on one line across re-format (convergence).
+TEST(FormatterEndToEndTest, EndElseIfWithEOLCommentConverges) {
+  static constexpr FormatterTestCase kTestCases[] = {
+      {// Comment on its own line between end and else if
+       "module m;\n"
+       "  always_comb begin\n"
+       "    case (state)\n"
+       "      STATE_A: begin\n"
+       "        if (cond_aaaa) next_state_value = STATE_B;\n"
+       "        else if (cond_bbbb) begin\n"
+       "          next_state_value = STATE_B;\n"
+       "        end\n"
+       "        // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n"
+       "        else if (cond_cccc) next_state_value = STATE_C;\n"
+       "      end\n"
+       "    endcase\n"
+       "  end\n"
+       "endmodule\n",
+       "module m;\n"
+       "  always_comb begin\n"
+       "    case (state)\n"
+       "      STATE_A: begin\n"
+       "        if (cond_aaaa) next_state_value = STATE_B;\n"
+       "        else if (cond_bbbb) begin\n"
+       "          next_state_value = STATE_B;\n"
+       "        end  // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n"
+       "        else if (cond_cccc) next_state_value = STATE_C;\n"
+       "      end\n"
+       "    endcase\n"
+       "  end\n"
+       "endmodule\n"},
+      {// Same construct with comment already on the end line
+       "module m;\n"
+       "  always_comb begin\n"
+       "    case (state)\n"
+       "      STATE_A: begin\n"
+       "        if (cond_aaaa) next_state_value = STATE_B;\n"
+       "        else if (cond_bbbb) begin\n"
+       "          next_state_value = STATE_B;\n"
+       "        end  // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n"
+       "        else if (cond_cccc) next_state_value = STATE_C;\n"
+       "      end\n"
+       "    endcase\n"
+       "  end\n"
+       "endmodule\n",
+       "module m;\n"
+       "  always_comb begin\n"
+       "    case (state)\n"
+       "      STATE_A: begin\n"
+       "        if (cond_aaaa) next_state_value = STATE_B;\n"
+       "        else if (cond_bbbb) begin\n"
+       "          next_state_value = STATE_B;\n"
+       "        end  // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n"
+       "        else if (cond_cccc) next_state_value = STATE_C;\n"
+       "      end\n"
+       "    endcase\n"
+       "  end\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;
+  }
+}
+
+// Verify kAlign behavior for body-level param/localparam declarations
+// in module and package bodies.
+TEST(FormatterEndToEndTest, ParamDeclarationAlignmentBasics) {
+  static constexpr FormatterTestCase kTestCases[] = {
+      {// localparam alignment in module body
+       "module m;\n"
+       "localparam foo = 4'b0000;\n"
+       "localparam barr = 4'b0010;\n"
+       "localparam baaaaz = 4'b0111;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  localparam foo    = 4'b0000;\n"
+       "  localparam barr   = 4'b0010;\n"
+       "  localparam baaaaz = 4'b0111;\n"
+       "endmodule\n"},
+      {// localparam alignment in package body
+       "package p;\n"
+       "localparam foo = 4'b0000;\n"
+       "localparam barr = 4'b0010;\n"
+       "localparam baaaaz = 4'b0111;\n"
+       "endpackage\n",
+       "package p;\n"
+       "  localparam foo    = 4'b0000;\n"
+       "  localparam barr   = 4'b0010;\n"
+       "  localparam baaaaz = 4'b0111;\n"
+       "endpackage\n"},
+      {// parameter alignment in module body
+       "module m;\n"
+       "parameter int foo = 1;\n"
+       "parameter int barr = 2;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  parameter int foo  = 1;\n"
+       "  parameter int barr = 2;\n"
+       "endmodule\n"},
+      {// parameter alignment in package body
+       "package p;\n"
+       "parameter int foo = 1;\n"
+       "parameter int barrrr = 2;\n"
+       "endpackage\n",
+       "package p;\n"
+       "  parameter int foo    = 1;\n"
+       "  parameter int barrrr = 2;\n"
+       "endpackage\n"},
+  };
+  FormatStyle style;
+  style.column_limit = 40;
+  style.indentation_spaces = 2;
+  style.wrap_spaces = 4;
+  style.parameter_declaration_alignment = AlignmentPolicy::kAlign;
+  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;
+  }
+}
+
+// Verify kFlushLeft behavior: body-level param/localparam declarations
+// are not aligned.
+TEST(FormatterEndToEndTest, ParamDeclarationAlignmentFlushLeft) {
+  static constexpr FormatterTestCase kTestCases[] = {
+      {// module body: parameters are not aligned
+       "module m;\n"
+       "localparam foo = 4'b0000;\n"
+       "localparam barr = 4'b0010;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  localparam foo = 4'b0000;\n"
+       "  localparam barr = 4'b0010;\n"
+       "endmodule\n"},
+      {// package context: flush-left
+       "package p;\n"
+       "localparam foo = 4'b0000;\n"
+       "localparam barr = 4'b0010;\n"
+       "endpackage\n",
+       "package p;\n"
+       "  localparam foo = 4'b0000;\n"
+       "  localparam barr = 4'b0010;\n"
+       "endpackage\n"},
+      {// generate context: flush-left
+       "module m;\n"
+       "generate\n"
+       "localparam foo = 1;\n"
+       "localparam barr = 2;\n"
+       "endgenerate\n"
+       "endmodule\n",
+       "module m;\n"
+       "  generate\n"
+       "    localparam foo = 1;\n"
+       "    localparam barr = 2;\n"
+       "  endgenerate\n"
+       "endmodule\n"},
+      {// parameter in module body: flush-left
+       "module m;\n"
+       "parameter int W = 8;\n"
+       "parameter int HHHH = 2;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  parameter int W = 8;\n"
+       "  parameter int HHHH = 2;\n"
+       "endmodule\n"},
+      {// mixed parameter and localparam: flush-left, no alignment
+       "module m;\n"
+       "parameter int W = 8;\n"
+       "localparam L = 4;\n"
+       "parameter int H = 2;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  parameter int W = 8;\n"
+       "  localparam L = 4;\n"
+       "  parameter int H = 2;\n"
+       "endmodule\n"},
+      {// interface context: flush-left
+       "interface my_if;\n"
+       "localparam foo = 1;\n"
+       "localparam barr = 2;\n"
+       "endinterface\n",
+       "interface my_if;\n"
+       "  localparam foo = 1;\n"
+       "  localparam barr = 2;\n"
+       "endinterface\n"},
+  };
+  FormatStyle style;
+  style.column_limit = 40;
+  style.indentation_spaces = 2;
+  style.wrap_spaces = 4;
+  style.parameter_declaration_alignment = AlignmentPolicy::kFlushLeft;
+  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;
+  }
+}
+
+// Verify kPreserve behavior: body-level param/localparam declarations
+// maintain their existing spacing.
+TEST(FormatterEndToEndTest, ParamDeclarationAlignmentPreserve) {
+  static constexpr FormatterTestCase kTestCases[] = {
+      {// module body: existing spacing is kept
+       "module m;\n"
+       "localparam foo = 4'b0000;\n"
+       "localparam barr  = 4'b0010;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  localparam foo = 4'b0000;\n"
+       "  localparam barr  = 4'b0010;\n"
+       "endmodule\n"},
+      {// package: existing flush-left spacing preserved
+       "package p;\n"
+       "localparam foo = 4'b0000;\n"
+       "localparam barr = 4'b0010;\n"
+       "endpackage\n",
+       "package p;\n"
+       "  localparam foo = 4'b0000;\n"
+       "  localparam barr = 4'b0010;\n"
+       "endpackage\n"},
+      {// generate: existing aligned spacing preserved
+       "module m;\n"
+       "generate\n"
+       "localparam foo  = 1;\n"
+       "localparam barr = 2;\n"
+       "endgenerate\n"
+       "endmodule\n",
+       "module m;\n"
+       "  generate\n"
+       "    localparam foo  = 1;\n"
+       "    localparam barr = 2;\n"
+       "  endgenerate\n"
+       "endmodule\n"},
+      {// parameter flush-left preserved
+       "module m;\n"
+       "parameter int W = 8;\n"
+       "parameter int HHHH = 2;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  parameter int W = 8;\n"
+       "  parameter int HHHH = 2;\n"
+       "endmodule\n"},
+      {// parameter pre-aligned preserved
+       "module m;\n"
+       "parameter int W    = 8;\n"
+       "parameter int HHHH = 2;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  parameter int W    = 8;\n"
+       "  parameter int HHHH = 2;\n"
+       "endmodule\n"},
+      {// interface pre-aligned preserved
+       "interface my_if;\n"
+       "localparam foo  = 1;\n"
+       "localparam barr = 2;\n"
+       "endinterface\n",
+       "interface my_if;\n"
+       "  localparam foo  = 1;\n"
+       "  localparam barr = 2;\n"
+       "endinterface\n"},
+  };
+  FormatStyle style;
+  style.column_limit = 40;
+  style.indentation_spaces = 2;
+  style.wrap_spaces = 4;
+  style.parameter_declaration_alignment = AlignmentPolicy::kPreserve;
+  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;
+  }
+}
+
+// Verify kAlign behavior for param/localparam declarations across
+// interface, generate, and module body contexts, including mixed
+// param/localparam and packed dimension alignment.
+TEST(FormatterEndToEndTest, ParamDeclarationAlignmentContexts) {
+  static constexpr FormatterTestCase kTestCases[] = {
+      {// interface body localparam alignment
+       "interface my_if;\n"
+       "localparam foo = 1;\n"
+       "localparam barr = 2;\n"
+       "endinterface\n",
+       "interface my_if;\n"
+       "  localparam foo  = 1;\n"
+       "  localparam barr = 2;\n"
+       "endinterface\n"},
+      {// interface body parameter alignment
+       "interface my_if;\n"
+       "parameter int X = 1;\n"
+       "parameter int Y_LONG = 2;\n"
+       "endinterface\n",
+       "interface my_if;\n"
+       "  parameter int X      = 1;\n"
+       "  parameter int Y_LONG = 2;\n"
+       "endinterface\n"},
+      {// generate block localparam alignment
+       "module m;\n"
+       "generate\n"
+       "localparam foo = 1;\n"
+       "localparam barr = 2;\n"
+       "endgenerate\n"
+       "endmodule\n",
+       "module m;\n"
+       "  generate\n"
+       "    localparam foo  = 1;\n"
+       "    localparam barr = 2;\n"
+       "  endgenerate\n"
+       "endmodule\n"},
+      {// generate block parameter alignment
+       "module m;\n"
+       "generate\n"
+       "parameter int A = 1;\n"
+       "parameter int BB = 2;\n"
+       "endgenerate\n"
+       "endmodule\n",
+       "module m;\n"
+       "  generate\n"
+       "    parameter int A  = 1;\n"
+       "    parameter int BB = 2;\n"
+       "  endgenerate\n"
+       "endmodule\n"},
+      {// mixed param and net declarations form separate alignment groups
+       "module m;\n"
+       "localparam X = 1;\n"
+       "localparam YYY = 2;\n"
+       "logic clk;\n"
+       "logic rst_n;\n"
+       "localparam ZZ = 3;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  localparam X   = 1;\n"
+       "  localparam YYY = 2;\n"
+       "  logic clk;\n"
+       "  logic rst_n;\n"
+       "  localparam ZZ = 3;\n"
+       "endmodule\n"},
+      {// mixed parameter and localparam in same block
+       "module m;\n"
+       "parameter int W = 8;\n"
+       "localparam L = 4;\n"
+       "parameter int H = 2;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  parameter  int W = 8;\n"
+       "  localparam     L = 4;\n"
+       "  parameter  int H = 2;\n"
+       "endmodule\n"},
+      {// packed dimensions are aligned
+       "module m;\n"
+       "parameter bit [7:0] X = 0;\n"
+       "parameter bit [31:0] YYYY = 1;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  parameter bit [ 7:0] X    = 0;\n"
+       "  parameter bit [31:0] YYYY = 1;\n"
+       "endmodule\n"},
+      {// multi-identifier comma-separated params are not split (no-crash)
+       "module m;\n"
+       "parameter int a=1, b=2, ccc=3;\n"
+       "localparam d=4, e=5;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  parameter  int a = 1, b = 2, ccc = 3;\n"
+       "  localparam     d = 4, e = 5;\n"
+       "endmodule\n"},
+      {// parameter type declarations do not crash
+       "module m;\n"
+       "parameter type T = int;\n"
+       "parameter type TT_LONG = bit;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  parameter type T = int;\n"
+       "  parameter type TT_LONG = bit;\n"
+       "endmodule\n"},
+  };
+  FormatStyle style;
+  style.column_limit = 40;
+  style.indentation_spaces = 2;
+  style.wrap_spaces = 4;
+  style.parameter_declaration_alignment = AlignmentPolicy::kAlign;
+  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;
+  }
+}
+
+// Verify kInferUserIntent behavior: body-level param/localparam
+// declarations infer alignment intent from existing spacing.
+TEST(FormatterEndToEndTest, ParamDeclarationAlignmentInferUserIntent) {
+  static constexpr FormatterTestCase kTestCases[] = {
+      {// flush-left localparams with small spacing diff: infer aligns
+       "module m;\n"
+       "localparam foo = 1;\n"
+       "localparam barr = 2;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  localparam foo  = 1;\n"
+       "  localparam barr = 2;\n"
+       "endmodule\n"},
+      {// pre-aligned localparams: infer preserves alignment
+       "module m;\n"
+       "localparam foo    = 1;\n"
+       "localparam barr   = 2;\n"
+       "localparam baaaaz = 3;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  localparam foo    = 1;\n"
+       "  localparam barr   = 2;\n"
+       "  localparam baaaaz = 3;\n"
+       "endmodule\n"},
+      {// flush-left params in package with small spacing diff: infer aligns
+       "package p;\n"
+       "localparam X = 1;\n"
+       "localparam YY = 2;\n"
+       "endpackage\n",
+       "package p;\n"
+       "  localparam X  = 1;\n"
+       "  localparam YY = 2;\n"
+       "endpackage\n"},
+      {// mixed param/localparam flush-left: infer keeps flush-left
+       "module m;\n"
+       "parameter int W = 8;\n"
+       "localparam L = 4;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  parameter int W = 8;\n"
+       "  localparam L = 4;\n"
+       "endmodule\n"},
+      {// interface flush-left with small diff: infer aligns
+       "interface my_if;\n"
+       "localparam foo = 1;\n"
+       "localparam barr = 2;\n"
+       "endinterface\n",
+       "interface my_if;\n"
+       "  localparam foo  = 1;\n"
+       "  localparam barr = 2;\n"
+       "endinterface\n"},
+  };
+  FormatStyle style;
+  style.column_limit = 40;
+  style.indentation_spaces = 2;
+  style.wrap_spaces = 4;
+  style.parameter_declaration_alignment = AlignmentPolicy::kInferUserIntent;
+  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;
+  }
+}
+
+// Verify boundary behavior for param declarations with
+// kBlankLinesAndSeparatorComments: both blank lines and separator comments
+// break alignment groups.
+TEST(FormatterEndToEndTest, ParamDeclarationAlignmentBoundary) {
+  static constexpr FormatterTestCase kTestCases[] = {
+      {// separator comment breaks param alignment group
+       "module m;\n"
+       "localparam foo = 1;\n"
+       "localparam barr = 2;\n"
+       "// ============\n"
+       "localparam baaaaz = 1;\n"
+       "localparam c = 2;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  localparam foo  = 1;\n"
+       "  localparam barr = 2;\n"
+       "  // ============\n"
+       "  localparam baaaaz = 1;\n"
+       "  localparam c      = 2;\n"
+       "endmodule\n"},
+      {// blank line breaks param alignment group
+       "module m;\n"
+       "localparam foo = 1;\n"
+       "localparam barr = 2;\n"
+       "\n"
+       "localparam baaaaz = 1;\n"
+       "localparam c = 2;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  localparam foo  = 1;\n"
+       "  localparam barr = 2;\n"
+       "\n"
+       "  localparam baaaaz = 1;\n"
+       "  localparam c      = 2;\n"
+       "endmodule\n"},
+      {// no separator: single alignment group (all aligned together)
+       "module m;\n"
+       "localparam foo = 1;\n"
+       "localparam barr = 2;\n"
+       "localparam baaaaz = 1;\n"
+       "localparam c = 2;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  localparam foo    = 1;\n"
+       "  localparam barr   = 2;\n"
+       "  localparam baaaaz = 1;\n"
+       "  localparam c      = 2;\n"
+       "endmodule\n"},
+      {// regular comment does NOT break alignment group
+       "module m;\n"
+       "localparam foo = 1;\n"
+       "// regular comment\n"
+       "localparam barr = 2;\n"
+       "localparam baaaaz = 1;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  localparam foo    = 1;\n"
+       "  // regular comment\n"
+       "  localparam barr   = 2;\n"
+       "  localparam baaaaz = 1;\n"
+       "endmodule\n"},
+  };
+  FormatStyle style;
+  style.column_limit = 40;
+  style.indentation_spaces = 2;
+  style.wrap_spaces = 4;
+  style.parameter_declaration_alignment = AlignmentPolicy::kAlign;
+  style.alignment_group_boundary =
+      AlignmentGroupBoundary::kBlankLinesAndSeparatorComments;
+  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;
+  }
+}
+
+// Verify edge cases: unpacked dimensions, signed/unsigned, implicit types,
+// complex types, single declarations, deeply nested generate blocks.
+TEST(FormatterEndToEndTest, ParamDeclarationAlignmentEdgeCases) {
+  // Use column_limit = 80 to avoid unintended line wrapping inside
+  // deeply nested generate blocks (generate-if, generate-for), which
+  // would interfere with verifying alignment behavior.
+  static constexpr FormatterTestCase kTestCases[] = {
+      {// unpacked dimension alignment
+       "module m;\n"
+       "parameter bit X [7:0] = 0;\n"
+       "parameter bit YYYY [15:0] = 1;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  parameter bit X   [ 7:0] = 0;\n"
+       "  parameter bit YYYY[15:0] = 1;\n"
+       "endmodule\n"},
+      {// param declarations with only type and default value (no idim)
+       "module m;\n"
+       "parameter int X = 0;\n"
+       "parameter int YYYY = 0;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  parameter int X    = 0;\n"
+       "  parameter int YYYY = 0;\n"
+       "endmodule\n"},
+      {// signed modifier with packed dimensions
+       "module m;\n"
+       "parameter bit signed [7:0] X = 0;\n"
+       "parameter bit signed [31:0] YYYY = 1;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  parameter bit signed [ 7:0] X    = 0;\n"
+       "  parameter bit signed [31:0] YYYY = 1;\n"
+       "endmodule\n"},
+      {// param with complex type (struct/typedef) — does not crash
+       "module m;\n"
+       "parameter foo_t X = foo_t'(0);\n"
+       "parameter foo_t Y_LONG = foo_t'(0);\n"
+       "endmodule\n",
+       "module m;\n"
+       "  parameter foo_t X      = foo_t'(0);\n"
+       "  parameter foo_t Y_LONG = foo_t'(0);\n"
+       "endmodule\n"},
+      {// single param declaration (not enough for alignment group)
+       "module m;\n"
+       "localparam X = 1;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  localparam X = 1;\n"
+       "endmodule\n"},
+      {// params inside generate-if block
+       "module m;\n"
+       "generate\n"
+       "if (1) begin : blk\n"
+       "localparam foo = 1;\n"
+       "localparam barr = 2;\n"
+       "end\n"
+       "endgenerate\n"
+       "endmodule\n",
+       "module m;\n"
+       "  generate\n"
+       "    if (1) begin : blk\n"
+       "      localparam foo  = 1;\n"
+       "      localparam barr = 2;\n"
+       "    end\n"
+       "  endgenerate\n"
+       "endmodule\n"},
+      {// unsigned keyword with packed dimensions (complement to signed)
+       "module m;\n"
+       "parameter bit unsigned [7:0] X = 0;\n"
+       "parameter bit unsigned [31:0] YYYY = 1;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  parameter bit unsigned [ 7:0] X    = 0;\n"
+       "  parameter bit unsigned [31:0] YYYY = 1;\n"
+       "endmodule\n"},
+      {// parameter with implicit type (no explicit type keyword)
+       "module m;\n"
+       "parameter X = 32;\n"
+       "parameter Y_LONG = 64;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  parameter X      = 32;\n"
+       "  parameter Y_LONG = 64;\n"
+       "endmodule\n"},
+      {// params inside generate-for block
+       "module m;\n"
+       "generate\n"
+       "for (i = 0; i < 2; i++) begin : blk\n"
+       "localparam foo = 1;\n"
+       "localparam barr = 2;\n"
+       "end\n"
+       "endgenerate\n"
+       "endmodule\n",
+       "module m;\n"
+       "  generate\n"
+       "    for (i = 0; i < 2; i++) begin : blk\n"
+       "      localparam foo  = 1;\n"
+       "      localparam barr = 2;\n"
+       "    end\n"
+       "  endgenerate\n"
+       "endmodule\n"},
+  };
+  FormatStyle style;
+  style.column_limit = 80;
+  style.indentation_spaces = 2;
+  style.wrap_spaces = 4;
+  style.parameter_declaration_alignment = AlignmentPolicy::kAlign;
+  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;
+  }
+}
+
+// Verify parameter declarations inside generate-case blocks are aligned.
+TEST(FormatterEndToEndTest, ParamDeclarationAlignmentGenerateCase) {
+  // Params inside generate-case need wider column limit to avoid wrapping the
+  // case label / begin block line.
+  static constexpr FormatterTestCase kTestCases[] = {
+      {// params inside generate-case block
+       "module m #(P = 0);\n"
+       "generate\n"
+       "case (P)\n"
+       "0: begin : blk\n"
+       "localparam foo = 1;\n"
+       "localparam barr = 2;\n"
+       "end\n"
+       "endcase\n"
+       "endgenerate\n"
+       "endmodule\n",
+       "module m #(\n"
+       "    P = 0\n"
+       ");\n"
+       "  generate\n"
+       "    case (P)\n"
+       "      0: begin : blk\n"
+       "        localparam foo  = 1;\n"
+       "        localparam barr = 2;\n"
+       "      end\n"
+       "    endcase\n"
+       "  endgenerate\n"
+       "endmodule\n"},
+  };
+  FormatStyle style;
+  style.column_limit = 80;
+  style.indentation_spaces = 2;
+  style.wrap_spaces = 4;
+  style.parameter_declaration_alignment = AlignmentPolicy::kAlign;
+  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;
+  }
+}
+
+TEST(FormatterEndToEndTest, FormalAndBodyParamAlignmentIndependence) {
+  // Verify that --formal_parameters_alignment and
+  // --parameter_declaration_alignment act independently: setting formal params
+  // to kFlushLeft should not affect body-level param alignment, and vice versa.
+  static constexpr FormatterTestCase kTestCases[] = {
+      {// formal flush-left + body align: only body params align
+       "module m #(\n"
+       "int W = 2,\n"
+       "int LLLL = 4\n"
+       ");\n"
+       "localparam foo = 0;\n"
+       "localparam barrrr = 0;\n"
+       "endmodule\n",
+       "module m #(\n"
+       "    int W = 2,\n"
+       "    int LLLL = 4\n"
+       ");\n"
+       "  localparam foo    = 0;\n"
+       "  localparam barrrr = 0;\n"
+       "endmodule\n"},
+      {// formal align + body flush-left: only formal params align
+       "module m #(\n"
+       "int W = 2,\n"
+       "int LLLL = 4\n"
+       ");\n"
+       "localparam foo = 0;\n"
+       "localparam barrrr = 0;\n"
+       "endmodule\n",
+       "module m #(\n"
+       "    int W    = 2,\n"
+       "    int LLLL = 4\n"
+       ");\n"
+       "  localparam foo = 0;\n"
+       "  localparam barrrr = 0;\n"
+       "endmodule\n"},
+      {// both flush-left: no alignment anywhere
+       "module m #(\n"
+       "int W = 2,\n"
+       "int LLLL = 4\n"
+       ");\n"
+       "localparam foo = 0;\n"
+       "localparam barrrr = 0;\n"
+       "endmodule\n",
+       "module m #(\n"
+       "    int W = 2,\n"
+       "    int LLLL = 4\n"
+       ");\n"
+       "  localparam foo = 0;\n"
+       "  localparam barrrr = 0;\n"
+       "endmodule\n"},
+      {// both align: both formal and body params aligned
+       "module m #(\n"
+       "int W = 2,\n"
+       "int LLLL = 4\n"
+       ");\n"
+       "localparam foo = 0;\n"
+       "localparam barrrr = 0;\n"
+       "endmodule\n",
+       "module m #(\n"
+       "    int W    = 2,\n"
+       "    int LLLL = 4\n"
+       ");\n"
+       "  localparam foo    = 0;\n"
+       "  localparam barrrr = 0;\n"
+       "endmodule\n"},
+  };
+  // First case: formal kFlushLeft, body kAlign
+  {
+    FormatStyle style;
+    style.column_limit = 40;
+    style.indentation_spaces = 2;
+    style.wrap_spaces = 4;
+    style.parameter_declaration_alignment = AlignmentPolicy::kAlign;
+    style.formal_parameters_alignment = AlignmentPolicy::kFlushLeft;
+    const auto &tc = kTestCases[0];
+    VLOG(1) << "code-to-format:\n" << tc.input << "<EOF>";
+    std::ostringstream stream;
+    const auto status = FormatVerilog(tc.input, "<filename>", style, stream);
+    EXPECT_OK(status) << status.message();
+    EXPECT_EQ(stream.str(), tc.expected) << "code:\n" << tc.input;
+  }
+  // Second case: formal kAlign, body kFlushLeft
+  {
+    FormatStyle style;
+    style.column_limit = 40;
+    style.indentation_spaces = 2;
+    style.wrap_spaces = 4;
+    style.parameter_declaration_alignment = AlignmentPolicy::kFlushLeft;
+    style.formal_parameters_alignment = AlignmentPolicy::kAlign;
+    const auto &tc = kTestCases[1];
+    VLOG(1) << "code-to-format:\n" << tc.input << "<EOF>";
+    std::ostringstream stream;
+    const auto status = FormatVerilog(tc.input, "<filename>", style, stream);
+    EXPECT_OK(status) << status.message();
+    EXPECT_EQ(stream.str(), tc.expected) << "code:\n" << tc.input;
+  }
+  // Third case: both kFlushLeft
+  {
+    FormatStyle style;
+    style.column_limit = 40;
+    style.indentation_spaces = 2;
+    style.wrap_spaces = 4;
+    style.parameter_declaration_alignment = AlignmentPolicy::kFlushLeft;
+    style.formal_parameters_alignment = AlignmentPolicy::kFlushLeft;
+    const auto &tc = kTestCases[2];
+    VLOG(1) << "code-to-format:\n" << tc.input << "<EOF>";
+    std::ostringstream stream;
+    const auto status = FormatVerilog(tc.input, "<filename>", style, stream);
+    EXPECT_OK(status) << status.message();
+    EXPECT_EQ(stream.str(), tc.expected) << "code:\n" << tc.input;
+  }
+  // Fourth case: both kAlign
+  {
+    FormatStyle style;
+    style.column_limit = 40;
+    style.indentation_spaces = 2;
+    style.wrap_spaces = 4;
+    style.parameter_declaration_alignment = AlignmentPolicy::kAlign;
+    style.formal_parameters_alignment = AlignmentPolicy::kAlign;
+    const auto &tc = kTestCases[3];
+    VLOG(1) << "code-to-format:\n" << tc.input << "<EOF>";
+    std::ostringstream stream;
+    const auto status = FormatVerilog(tc.input, "<filename>", style, stream);
+    EXPECT_OK(status) << status.message();
+    EXPECT_EQ(stream.str(), tc.expected) << "code:\n" << tc.input;
+  }
+}
+
+TEST(FormatterEndToEndTest, ClassBodyParamNotAffected) {
+  // Verify that class body parameter/localparam declarations are NOT affected
+  // by --parameter_declaration_alignment, which targets only module/generate/
+  // package/interface body-level params.
+  static constexpr FormatterTestCase kTestCases[] = {
+      {// class body params are not affected
+       "class c;\n"
+       "parameter int X = 1;\n"
+       "parameter int YYYY = 2;\n"
+       "localparam foo = 3;\n"
+       "localparam barrrr = 4;\n"
+       "endclass\n",
+       "class c;\n"
+       "  parameter int X = 1;\n"
+       "  parameter int YYYY = 2;\n"
+       "  localparam foo = 3;\n"
+       "  localparam barrrr = 4;\n"
+       "endclass\n"},
+  };
+  FormatStyle style;
+  style.column_limit = 40;
+  style.indentation_spaces = 2;
+  style.wrap_spaces = 4;
+  style.parameter_declaration_alignment = AlignmentPolicy::kAlign;
+  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;
+  }
+}
+
+TEST(FormatterEndToEndTest, PackageBodyVariableAlignment) {
+  // Verify that moving kPackageItemList to kTabularAlignment in tree-unwrapper
+  // enables net/variable declaration alignment in package bodies via
+  // module_net_variable_alignment.
+  static constexpr FormatterTestCase kTestCases[] = {
+      {// net/variable declaration alignment in package body
+       "package p;\n"
+       "logic [7:0] a;\n"
+       "logic [31:0] bb;\n"
+       "endpackage\n",
+       "package p;\n"
+       "  logic [ 7:0] a;\n"
+       "  logic [31:0] bb;\n"
+       "endpackage\n"},
+  };
+  FormatStyle style;
+  style.column_limit = 40;
+  style.indentation_spaces = 2;
+  style.wrap_spaces = 4;
+  style.module_net_variable_alignment = AlignmentPolicy::kAlign;
+  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;
+  }
+}
+
+TEST(FormatterEndToEndTest, ParamDeclarationAlignmentApplyToAll) {
+  // Verify that ApplyToAllAlignmentPolicies affects
+  // parameter_declaration_alignment for all four alignment policies.
+  static constexpr FormatterTestCase kTestCases[] = {
+      {// kAlign aligns identifiers and = signs across declarations
+       "module m;\n"
+       "localparam foo = 1;\n"
+       "localparam barrrr = 2;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  localparam foo    = 1;\n"
+       "  localparam barrrr = 2;\n"
+       "endmodule\n"},
+      {// kFlushLeft: declarations remain flush-left
+       "module m;\n"
+       "localparam foo = 1;\n"
+       "localparam barrrr = 2;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  localparam foo = 1;\n"
+       "  localparam barrrr = 2;\n"
+       "endmodule\n"},
+      {// kPreserve: existing spacing is kept
+       "module m;\n"
+       "localparam foo = 1;\n"
+       "localparam barrrr  = 2;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  localparam foo = 1;\n"
+       "  localparam barrrr  = 2;\n"
+       "endmodule\n"},
+      {// kInferUserIntent: large identifier width difference (3 cols)
+       // suggests flush-left intent, no alignment.
+       "module m;\n"
+       "localparam foo = 1;\n"
+       "localparam barrrr = 2;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  localparam foo = 1;\n"
+       "  localparam barrrr = 2;\n"
+       "endmodule\n"},
+  };
+  // kAlign
+  {
+    FormatStyle style;
+    style.column_limit = 40;
+    style.indentation_spaces = 2;
+    style.wrap_spaces = 4;
+    style.ApplyToAllAlignmentPolicies(AlignmentPolicy::kAlign);
+    const auto &tc = kTestCases[0];
+    std::ostringstream stream;
+    const auto status = FormatVerilog(tc.input, "<filename>", style, stream);
+    EXPECT_OK(status) << status.message();
+    EXPECT_EQ(stream.str(), tc.expected) << "code:\n" << tc.input;
+  }
+  // kFlushLeft
+  {
+    FormatStyle style;
+    style.column_limit = 40;
+    style.indentation_spaces = 2;
+    style.wrap_spaces = 4;
+    style.ApplyToAllAlignmentPolicies(AlignmentPolicy::kFlushLeft);
+    const auto &tc = kTestCases[1];
+    std::ostringstream stream;
+    const auto status = FormatVerilog(tc.input, "<filename>", style, stream);
+    EXPECT_OK(status) << status.message();
+    EXPECT_EQ(stream.str(), tc.expected) << "code:\n" << tc.input;
+  }
+  // kPreserve
+  {
+    FormatStyle style;
+    style.column_limit = 40;
+    style.indentation_spaces = 2;
+    style.wrap_spaces = 4;
+    style.ApplyToAllAlignmentPolicies(AlignmentPolicy::kPreserve);
+    const auto &tc = kTestCases[2];
+    std::ostringstream stream;
+    const auto status = FormatVerilog(tc.input, "<filename>", style, stream);
+    EXPECT_OK(status) << status.message();
+    EXPECT_EQ(stream.str(), tc.expected) << "code:\n" << tc.input;
+  }
+  // kInferUserIntent
+  {
+    FormatStyle style;
+    style.column_limit = 40;
+    style.indentation_spaces = 2;
+    style.wrap_spaces = 4;
+    style.ApplyToAllAlignmentPolicies(AlignmentPolicy::kInferUserIntent);
+    const auto &tc = kTestCases[3];
+    std::ostringstream stream;
+    const auto status = FormatVerilog(tc.input, "<filename>", style, stream);
+    EXPECT_OK(status) << status.message();
+    EXPECT_EQ(stream.str(), tc.expected) << "code:\n" << tc.input;
+  }
+}
+
+// Verify that alignment_group_boundary=kBlankLines treats only blank lines
+// (not separator comments) as boundary breaks for param declarations.
+TEST(FormatterEndToEndTest, ParamDeclarationAlignmentBoundaryBlankLines) {
+  static constexpr FormatterTestCase kTestCases[] = {
+      {// Only blank line breaks group; separator comment does not
+       "module m;\n"
+       "localparam foo = 1;\n"
+       "localparam barr = 2;\n"
+       "\n"
+       "localparam baaaaz = 1;\n"
+       "localparam c = 2;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  localparam foo  = 1;\n"
+       "  localparam barr = 2;\n"
+       "\n"
+       "  localparam baaaaz = 1;\n"
+       "  localparam c      = 2;\n"
+       "endmodule\n"},
+      {// Separator comment does NOT break group with kBlankLines
+       "module m;\n"
+       "localparam foo = 1;\n"
+       "localparam barr = 2;\n"
+       "// ============\n"
+       "localparam baaaaz = 1;\n"
+       "localparam c = 2;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  localparam foo    = 1;\n"
+       "  localparam barr   = 2;\n"
+       "  // ============\n"
+       "  localparam baaaaz = 1;\n"
+       "  localparam c      = 2;\n"
+       "endmodule\n"},
+  };
+  FormatStyle style;
+  style.column_limit = 40;
+  style.indentation_spaces = 2;
+  style.wrap_spaces = 4;
+  style.parameter_declaration_alignment = AlignmentPolicy::kAlign;
+  style.alignment_group_boundary = AlignmentGroupBoundary::kBlankLines;
+  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;
+  }
+}
+
+// Verify that alignment_group_boundary=kNone keeps all param declarations
+// in a single alignment group regardless of blank lines or comments.
+TEST(FormatterEndToEndTest, ParamDeclarationAlignmentBoundaryNone) {
+  static constexpr FormatterTestCase kTestCases[] = {
+      {// Blank line does NOT break group with kNone
+       "module m;\n"
+       "localparam foo = 1;\n"
+       "localparam barr = 2;\n"
+       "\n"
+       "localparam baaaaz = 1;\n"
+       "localparam c = 2;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  localparam foo    = 1;\n"
+       "  localparam barr   = 2;\n"
+       "\n"
+       "  localparam baaaaz = 1;\n"
+       "  localparam c      = 2;\n"
+       "endmodule\n"},
+      {// Separator comment does NOT break group with kNone
+       "module m;\n"
+       "localparam foo = 1;\n"
+       "localparam barr = 2;\n"
+       "// ============\n"
+       "localparam baaaaz = 1;\n"
+       "localparam c = 2;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  localparam foo    = 1;\n"
+       "  localparam barr   = 2;\n"
+       "  // ============\n"
+       "  localparam baaaaz = 1;\n"
+       "  localparam c      = 2;\n"
+       "endmodule\n"},
+  };
+  FormatStyle style;
+  style.column_limit = 40;
+  style.indentation_spaces = 2;
+  style.wrap_spaces = 4;
+  style.parameter_declaration_alignment = AlignmentPolicy::kAlign;
+  style.alignment_group_boundary = AlignmentGroupBoundary::kNone;
+  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;
+  }
+}
+
+// Verify that alignment_group_boundary=kSeparatorComments treats only
+// separator comments (not blank lines) as boundary breaks for param
+// declarations.
+TEST(FormatterEndToEndTest,
+     ParamDeclarationAlignmentBoundarySeparatorComments) {
+  static constexpr FormatterTestCase kTestCases[] = {
+      {// Separator comment breaks alignment group
+       "module m;\n"
+       "localparam foo = 1;\n"
+       "localparam barr = 2;\n"
+       "// ============\n"
+       "localparam baaaaz = 1;\n"
+       "localparam c = 2;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  localparam foo  = 1;\n"
+       "  localparam barr = 2;\n"
+       "  // ============\n"
+       "  localparam baaaaz = 1;\n"
+       "  localparam c      = 2;\n"
+       "endmodule\n"},
+      {// Blank line does NOT break group with kSeparatorComments
+       "module m;\n"
+       "localparam foo = 1;\n"
+       "localparam barr = 2;\n"
+       "\n"
+       "localparam baaaaz = 1;\n"
+       "localparam c = 2;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  localparam foo    = 1;\n"
+       "  localparam barr   = 2;\n"
+       "\n"
+       "  localparam baaaaz = 1;\n"
+       "  localparam c      = 2;\n"
+       "endmodule\n"},
+  };
+  FormatStyle style;
+  style.column_limit = 40;
+  style.indentation_spaces = 2;
+  style.wrap_spaces = 4;
+  style.parameter_declaration_alignment = AlignmentPolicy::kAlign;
+  style.alignment_group_boundary = AlignmentGroupBoundary::kSeparatorComments;
+  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;
+  }
+}
+
+// Verify that class body parameter/localparam declarations are NOT affected
+// by --parameter_declaration_alignment for the kFlushLeft, kPreserve,
+// and kInferUserIntent policies (kAlign is covered in
+// ClassBodyParamNotAffected).
+TEST(FormatterEndToEndTest, ClassBodyParamNotAffectedOtherPolicies) {
+  static constexpr FormatterTestCase kTestCases[] = {
+      {// kFlushLeft: class body params remain flush-left
+       "class c;\n"
+       "parameter int X = 1;\n"
+       "parameter int YYYY = 2;\n"
+       "localparam foo = 3;\n"
+       "localparam barrrr = 4;\n"
+       "endclass\n",
+       "class c;\n"
+       "  parameter int X = 1;\n"
+       "  parameter int YYYY = 2;\n"
+       "  localparam foo = 3;\n"
+       "  localparam barrrr = 4;\n"
+       "endclass\n"},
+      {// kPreserve: parameter_declaration_alignment does not apply to class
+       // bodies; whitespace is normalized by default formatting.
+       "class c;\n"
+       "parameter int X     = 1;\n"
+       "parameter int YYYY  = 2;\n"
+       "endclass\n",
+       "class c;\n"
+       "  parameter int X = 1;\n"
+       "  parameter int YYYY = 2;\n"
+       "endclass\n"},
+      {// kInferUserIntent: class body params infer flush-left
+       "class c;\n"
+       "parameter int X = 1;\n"
+       "parameter int YYYY = 2;\n"
+       "endclass\n",
+       "class c;\n"
+       "  parameter int X = 1;\n"
+       "  parameter int YYYY = 2;\n"
+       "endclass\n"},
+  };
+  // kFlushLeft
+  {
+    FormatStyle style;
+    style.column_limit = 40;
+    style.indentation_spaces = 2;
+    style.wrap_spaces = 4;
+    style.parameter_declaration_alignment = AlignmentPolicy::kFlushLeft;
+    const auto &tc = kTestCases[0];
+    std::ostringstream stream;
+    const auto status = FormatVerilog(tc.input, "<filename>", style, stream);
+    EXPECT_OK(status) << status.message();
+    EXPECT_EQ(stream.str(), tc.expected) << "code:\n" << tc.input;
+  }
+  // kPreserve
+  {
+    FormatStyle style;
+    style.column_limit = 40;
+    style.indentation_spaces = 2;
+    style.wrap_spaces = 4;
+    style.parameter_declaration_alignment = AlignmentPolicy::kPreserve;
+    const auto &tc = kTestCases[1];
+    std::ostringstream stream;
+    const auto status = FormatVerilog(tc.input, "<filename>", style, stream);
+    EXPECT_OK(status) << status.message();
+    EXPECT_EQ(stream.str(), tc.expected) << "code:\n" << tc.input;
+  }
+  // kInferUserIntent
+  {
+    FormatStyle style;
+    style.column_limit = 40;
+    style.indentation_spaces = 2;
+    style.wrap_spaces = 4;
+    style.parameter_declaration_alignment = AlignmentPolicy::kInferUserIntent;
+    const auto &tc = kTestCases[2];
+    std::ostringstream stream;
+    const auto status = FormatVerilog(tc.input, "<filename>", style, stream);
+    EXPECT_OK(status) << status.message();
+    EXPECT_EQ(stream.str(), tc.expected) << "code:\n" << tc.input;
+  }
+}
+
+// Verify formal_parameters_alignment and parameter_declaration_alignment
+// independence for kPreserve and kInferUserIntent combinations.
+TEST(FormatterEndToEndTest,
+     FormalAndBodyParamAlignmentIndependenceOtherPolicies) {
+  static constexpr FormatterTestCase kTestCases[] = {
+      {// formal kPreserve + body kAlign: only body params aligned
+       "module m #(\n"
+       "int W = 2,\n"
+       "int LLLL = 4\n"
+       ");\n"
+       "localparam foo = 0;\n"
+       "localparam barrrr = 0;\n"
+       "endmodule\n",
+       "module m #(\n"
+       "    int W = 2,\n"
+       "    int LLLL = 4\n"
+       ");\n"
+       "  localparam foo    = 0;\n"
+       "  localparam barrrr = 0;\n"
+       "endmodule\n"},
+      {// formal kAlign + body kPreserve: only formal params aligned
+       "module m #(\n"
+       "int W = 2,\n"
+       "int LLLL = 4\n"
+       ");\n"
+       "localparam foo   = 0;\n"
+       "localparam barrrr = 0;\n"
+       "endmodule\n",
+       "module m #(\n"
+       "    int W    = 2,\n"
+       "    int LLLL = 4\n"
+       ");\n"
+       "  localparam foo   = 0;\n"
+       "  localparam barrrr = 0;\n"
+       "endmodule\n"},
+      {// formal kInferUserIntent + body kAlign: only body params aligned
+       "module m #(\n"
+       "int W = 2,\n"
+       "int LLLL = 4\n"
+       ");\n"
+       "localparam foo = 0;\n"
+       "localparam barrrr = 0;\n"
+       "endmodule\n",
+       "module m #(\n"
+       "    int W = 2,\n"
+       "    int LLLL = 4\n"
+       ");\n"
+       "  localparam foo    = 0;\n"
+       "  localparam barrrr = 0;\n"
+       "endmodule\n"},
+  };
+  // formal kPreserve + body kAlign
+  {
+    FormatStyle style;
+    style.column_limit = 40;
+    style.indentation_spaces = 2;
+    style.wrap_spaces = 4;
+    style.parameter_declaration_alignment = AlignmentPolicy::kAlign;
+    style.formal_parameters_alignment = AlignmentPolicy::kPreserve;
+    const auto &tc = kTestCases[0];
+    std::ostringstream stream;
+    const auto status = FormatVerilog(tc.input, "<filename>", style, stream);
+    EXPECT_OK(status) << status.message();
+    EXPECT_EQ(stream.str(), tc.expected) << "code:\n" << tc.input;
+  }
+  // formal kAlign + body kPreserve
+  {
+    FormatStyle style;
+    style.column_limit = 40;
+    style.indentation_spaces = 2;
+    style.wrap_spaces = 4;
+    style.parameter_declaration_alignment = AlignmentPolicy::kPreserve;
+    style.formal_parameters_alignment = AlignmentPolicy::kAlign;
+    const auto &tc = kTestCases[1];
+    std::ostringstream stream;
+    const auto status = FormatVerilog(tc.input, "<filename>", style, stream);
+    EXPECT_OK(status) << status.message();
+    EXPECT_EQ(stream.str(), tc.expected) << "code:\n" << tc.input;
+  }
+  // formal kInferUserIntent + body kAlign
+  {
+    FormatStyle style;
+    style.column_limit = 40;
+    style.indentation_spaces = 2;
+    style.wrap_spaces = 4;
+    style.parameter_declaration_alignment = AlignmentPolicy::kAlign;
+    style.formal_parameters_alignment = AlignmentPolicy::kInferUserIntent;
+    const auto &tc = kTestCases[2];
+    std::ostringstream stream;
+    const auto status = FormatVerilog(tc.input, "<filename>", style, stream);
+    EXPECT_OK(status) << status.message();
+    EXPECT_EQ(stream.str(), tc.expected) << "code:\n" << tc.input;
+  }
+}
+
+// Verify parameter declarations with string, real, and function-call default
+// value types to exercise the ColumnSchemaScanner on non-standard type tokens.
+TEST(FormatterEndToEndTest, ParamDeclarationAlignmentTypeVariants) {
+  static constexpr FormatterTestCase kTestCases[] = {
+      {// parameter real type
+       "module m;\n"
+       "parameter real X = 1.0;\n"
+       "parameter real Y_LONG = 2.0;\n"
+       "endmodule\n",
+       "module m;\n"
+       "  parameter real X      = 1.0;\n"
+       "  parameter real Y_LONG = 2.0;\n"
+       "endmodule\n"},
+      {// parameter string type
+       "module m;\n"
+       "parameter string s = \"hello\";\n"
+       "parameter string long_s = \"world\";\n"
+       "endmodule\n",
+       "module m;\n"
+       "  parameter string s      = \"hello\";\n"
+       "  parameter string long_s = \"world\";\n"
+       "endmodule\n"},
+      {// parameter with function call in default value
+       "module m;\n"
+       "parameter int W = func(1, 2);\n"
+       "parameter int H_LONG = other(3);\n"
+       "endmodule\n",
+       "module m;\n"
+       "  parameter int W      = func(1, 2);\n"
+       "  parameter int H_LONG = other(3);\n"
+       "endmodule\n"},
+  };
+  FormatStyle style;
+  style.column_limit = 40;
+  style.indentation_spaces = 2;
+  style.wrap_spaces = 4;
+  style.parameter_declaration_alignment = AlignmentPolicy::kAlign;
+  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;
+  }
+}
+
+// Verify parameter_declaration_alignment kInferUserIntent in package bodies.
+TEST(FormatterEndToEndTest, ParamDeclarationAlignmentPackageInfer) {
+  static constexpr FormatterTestCase kTestCases[] = {
+      {// flush-left params with small diff: infer aligns
+       "package p;\n"
+       "localparam foo = 1;\n"
+       "localparam barr = 2;\n"
+       "endpackage\n",
+       "package p;\n"
+       "  localparam foo  = 1;\n"
+       "  localparam barr = 2;\n"
+       "endpackage\n"},
+      {// pre-aligned params: infer preserves
+       "package p;\n"
+       "localparam foo    = 1;\n"
+       "localparam barr   = 2;\n"
+       "localparam baaaaz = 3;\n"
+       "endpackage\n",
+       "package p;\n"
+       "  localparam foo    = 1;\n"
+       "  localparam barr   = 2;\n"
+       "  localparam baaaaz = 3;\n"
+       "endpackage\n"},
+  };
+  FormatStyle style;
+  style.column_limit = 40;
+  style.indentation_spaces = 2;
+  style.wrap_spaces = 4;
+  style.parameter_declaration_alignment = AlignmentPolicy::kInferUserIntent;
+  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;
+  }
+}
+
+// Verify parameter_declaration_alignment boundary behavior in package bodies.
+TEST(FormatterEndToEndTest, ParamDeclarationAlignmentPackageBoundary) {
+  static constexpr FormatterTestCase kTestCases[] = {
+      {// blank line breaks alignment group in package
+       "package p;\n"
+       "localparam foo = 1;\n"
+       "localparam barr = 2;\n"
+       "\n"
+       "localparam baaaaz = 1;\n"
+       "localparam c = 2;\n"
+       "endpackage\n",
+       "package p;\n"
+       "  localparam foo  = 1;\n"
+       "  localparam barr = 2;\n"
+       "\n"
+       "  localparam baaaaz = 1;\n"
+       "  localparam c      = 2;\n"
+       "endpackage\n"},
+      {// separator comment breaks alignment group in package
+       "package p;\n"
+       "localparam foo = 1;\n"
+       "localparam barr = 2;\n"
+       "// ============\n"
+       "localparam baaaaz = 1;\n"
+       "localparam c = 2;\n"
+       "endpackage\n",
+       "package p;\n"
+       "  localparam foo  = 1;\n"
+       "  localparam barr = 2;\n"
+       "  // ============\n"
+       "  localparam baaaaz = 1;\n"
+       "  localparam c      = 2;\n"
+       "endpackage\n"},
+  };
+  FormatStyle style;
+  style.column_limit = 40;
+  style.indentation_spaces = 2;
+  style.wrap_spaces = 4;
+  style.parameter_declaration_alignment = AlignmentPolicy::kAlign;
+  style.alignment_group_boundary =
+      AlignmentGroupBoundary::kBlankLinesAndSeparatorComments;
+  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;
+  }
+}
+
+// Verify the most common default scenario: both formal_parameters_alignment
+// and parameter_declaration_alignment set to kInferUserIntent.
+TEST(FormatterEndToEndTest, BothParamAlignmentsInferUserIntent) {
+  static constexpr FormatterTestCase kTestCases[] = {
+      {// both flush-left with small diff: infer aligns both formal and body
+       "module m #(\n"
+       "int W = 2,\n"
+       "int LL = 4\n"
+       ");\n"
+       "localparam foo = 0;\n"
+       "localparam barr = 0;\n"
+       "endmodule\n",
+       "module m #(\n"
+       "    int W  = 2,\n"
+       "    int LL = 4\n"
+       ");\n"
+       "  localparam foo  = 0;\n"
+       "  localparam barr = 0;\n"
+       "endmodule\n"},
+      {// both infer: pre-aligned formal, pre-aligned body preserved
+       "module m #(\n"
+       "int W    = 2,\n"
+       "int LLLL = 4\n"
+       ");\n"
+       "localparam foo    = 0;\n"
+       "localparam barrrr = 0;\n"
+       "endmodule\n",
+       "module m #(\n"
+       "    int W    = 2,\n"
+       "    int LLLL = 4\n"
+       ");\n"
+       "  localparam foo    = 0;\n"
+       "  localparam barrrr = 0;\n"
+       "endmodule\n"},
+  };
+  FormatStyle style;
+  style.column_limit = 40;
+  style.indentation_spaces = 2;
+  style.wrap_spaces = 4;
+  style.parameter_declaration_alignment = AlignmentPolicy::kInferUserIntent;
+  style.formal_parameters_alignment = AlignmentPolicy::kInferUserIntent;
+  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;
+  }
+}
+
+// Verify that a large multi-line comment block between parameter
+// declarations does not crash the formatter.  The tree-unwrapper may
+// produce a partition structure where the comment tokens are absorbed
+// into the following parameter partition, which the alignment code
+// must handle gracefully by skipping alignment for that row.
+TEST(FormatterEndToEndTest, ParamDeclarationAlignmentCommentBlockNoCrash) {
+  // These inputs contain large multi-line comment blocks between
+  // parameter/localparam declarations.  The formatter must not crash
+  // (SIGABRT).  Output correctness is secondary; the formatter may
+  // fall back to preserving the original input when the partition
+  // structure prevents safe alignment.
+  const char *kInputs[] = {
+      "module m;\n"
+      "parameter int W = 8;\n"
+      "// Multi-line comment block\n"
+      "// that spans across\n"
+      "//\n"
+      "// several lines\n"
+      "// of explanatory text\n"
+      "//\n"
+      "// It contains enough\n"
+      "// lines to trigger\n"
+      "// the partition structure\n"
+      "// edge case where\n"
+      "// comment tokens are\n"
+      "// absorbed into the\n"
+      "// following parameter\n"
+      "// partition.\n"
+      "//\n"
+      "localparam int H = 2;\n"
+      "endmodule\n",
+      "package p;\n"
+      "parameter int X = 1;\n"
+      "// Long comment block\n"
+      "// with many lines\n"
+      "//\n"
+      "// of text\n"
+      "//\n"
+      "localparam int Y = 2;\n"
+      "endpackage\n",
+  };
+  FormatStyle style;
+  style.column_limit = 40;
+  style.indentation_spaces = 2;
+  style.wrap_spaces = 4;
+  style.parameter_declaration_alignment = AlignmentPolicy::kAlign;
+  for (const char *input : kInputs) {
+    VLOG(1) << "code-to-format:\n" << input << "<EOF>";
+    std::ostringstream stream;
+    const auto status = FormatVerilog(input, "<filename>", style, stream);
+    // The primary requirement is that the formatter does not crash
+    // (SIGABRT).  Gtest will report failure if the process aborts.
+    // The partition structure may cause output format differences;
+    // the important thing is the formatter handled it gracefully.
+  }
+}
+
+// Regression for https://github.com/chipsalliance/verible/issues/2008
+// (also https://github.com/chipsalliance/verible/issues/2474 and
+// https://github.com/chipsalliance/verible/issues/2063):
+// Non-ANSI "input wire signed" used to abort in the tree-unwrapper because
+// the CST visited "signed" before "wire", which is the reverse of source
+// order.
+TEST(FormatterEndToEndTest, NonAnsiWireSignedModulePortDoesNotAbort) {
+  static constexpr FormatterTestCase kTestCases[] = {
+      {// Original issue #2008 sample
+       "module uut( sig1 );\n"
+       "\n"
+       "input wire signed [15:0] sig1;\n"
+       "\n"
+       "endmodule\n",
+       "module uut (\n"
+       "    sig1\n"
+       ");\n"
+       "\n"
+       "  input wire signed [15:0] sig1;\n"
+       "\n"
+       "endmodule\n"},
+      {// Issue #2474 sample
+       "module myModule (\n"
+       "    myinput\n"
+       ");\n"
+       "input wire signed [7:0] myInput;\n"
+       "endmodule\n",
+       "module myModule (\n"
+       "    myinput\n"
+       ");\n"
+       "  input wire signed [7:0] myInput;\n"
+       "endmodule\n"},
+      {// Issue #2063 sample: signed wire with no packed dimensions
+       "module top(a);\n"
+       "    input wire signed a;\n"
+       "endmodule\n",
+       "module top (\n"
+       "    a\n"
+       ");\n"
+       "  input wire signed a;\n"
+       "endmodule\n"},
+      {// Same production with logic instead of wire
+       "module uut(sig1);\n"
+       "input logic signed [15:0] sig1;\n"
+       "endmodule\n",
+       "module uut (\n"
+       "    sig1\n"
+       ");\n"
+       "  input logic signed [15:0] sig1;\n"
+       "endmodule\n"},
+      {// output / inout net types
+       "module uut(sig1, sig2);\n"
+       "output wire signed [15:0] sig1;\n"
+       "inout wire signed [7:0] sig2;\n"
+       "endmodule\n",
+       "module uut (\n"
+       "    sig1,\n"
+       "    sig2\n"
+       ");\n"
+       "  output wire signed [15:0] sig1;\n"
+       "  inout wire signed [7:0] sig2;\n"
+       "endmodule\n"},
+      {// unsigned is the same production
+       "module uut(sig1);\n"
+       "input wire unsigned [15:0] sig1;\n"
+       "endmodule\n",
+       "module uut (\n"
+       "    sig1\n"
+       ");\n"
+       "  input wire unsigned [15:0] sig1;\n"
+       "endmodule\n"},
+      {// ANSI form already worked; keep as a regression
+       "module uut(input wire signed [15:0] sig1);\n"
+       "endmodule\n",
+       "module uut (\n"
+       "    input wire signed [15:0] sig1\n"
+       ");\n"
+       "endmodule\n"},
+      {// Non-ANSI without signed still works
+       "module uut(sig1);\n"
+       "input wire [15:0] sig1;\n"
+       "endmodule\n",
+       "module uut (\n"
+       "    sig1\n"
+       ");\n"
+       "  input wire [15:0] sig1;\n"
+       "endmodule\n"},
+      {// Non-ANSI signed without net type still works
+       "module uut(sig1);\n"
+       "input signed [15:0] sig1;\n"
+       "endmodule\n",
+       "module uut (\n"
+       "    sig1\n"
+       ");\n"
+       "  input signed [15:0] sig1;\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;
+  }
+}
+
 }  // namespace
 }  // namespace formatter
 }  // namespace verilog
diff --git a/verible/verilog/formatting/token-annotator.cc b/verible/verilog/formatting/token-annotator.cc
index d4af24a..ebaeb21 100644
--- a/verible/verilog/formatting/token-annotator.cc
+++ b/verible/verilog/formatting/token-annotator.cc
@@ -258,6 +258,16 @@
     return {0, "No additional space around empty-string tokens."};
   }
 
+  // A macro definition body that begins with the token-concatenation
+  // operator "``" is part of macro name; preserve spacing if present.
+  // If a closing ')', that ends the definition name.
+  if (left.TokenEnum() == verilog_tokentype::PP_Identifier &&
+      right.TokenEnum() == verilog_tokentype::PP_define_body &&
+      right.Text().substr(0, 2) == "``" &&
+      right.OriginalLeadingSpaces().empty()) {
+    return {0, "Preserve spacing in concatenated name"};
+  }
+
   // Remove any extra spaces between numeric literals' width, base and digits.
   // "16'h123, 'h123" instead of "16 'h123", "16'h 123, 'h 123"
   if (IsInsideNumericLiteral(left, right)) {
@@ -500,11 +510,25 @@
     // This may be controversial or context-dependent, as parameterized
     // classes often appear with method calls like:
     //   type#(params...)::method(...);
+    // 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:
+    //   typedef pkg::my_class#(.P(P)) my_class_t;
+    // 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}, {});
+
     if (left_context.DirectParentIs(NodeEnum::kUnqualifiedId) &&
         !left_context.IsInsideFirst(
             {NodeEnum::kInstantiationType, NodeEnum::kBindTargetInstance,
              NodeEnum::kExtendsList, NodeEnum::kBraceGroup},
-            {})) {
+            {}) &&
+        !inside_unqualified_typedef) {
       return {0, "No space before # when direct parent is kUnqualifiedId."};
     }
     return {1, "Spaces before # in most other contexts."};
diff --git a/verible/verilog/formatting/tree-unwrapper.cc b/verible/verilog/formatting/tree-unwrapper.cc
index 15a0082..2286f8c 100644
--- a/verible/verilog/formatting/tree-unwrapper.cc
+++ b/verible/verilog/formatting/tree-unwrapper.cc
@@ -801,8 +801,10 @@
     case NodeEnum::kPreprocessorUndef:
     case NodeEnum::kTFPortDeclaration:
     case NodeEnum::kTypeDeclaration:
+    case NodeEnum::kLetDeclaration:
     case NodeEnum::kNetTypeDeclaration:
     case NodeEnum::kForwardDeclaration:
+    case NodeEnum::kInterfaceClassMethod:
     case NodeEnum::kConstraintDeclaration:
     case NodeEnum::kConstraintExpression:
     case NodeEnum::kCovergroupDeclaration:
@@ -881,6 +883,7 @@
     case NodeEnum::kTaskDeclaration:
     case NodeEnum::kClassDeclaration:
     case NodeEnum::kClassHeader:
+    case NodeEnum::kInterfaceClassDeclaration:
     case NodeEnum::kBegin:
     case NodeEnum::kEnd:
     // case NodeEnum::kFork:  // TODO(fangism): introduce this node enum
@@ -1201,8 +1204,6 @@
     // For the following constructs, always expand the view to subpartitions.
     // Add a level of indentation.
     case NodeEnum::kPackageImportList:
-    case NodeEnum::kPackageItemList:
-    case NodeEnum::kInterfaceClassDeclaration:
     case NodeEnum::kCasePatternItemList:
     case NodeEnum::kConstraintBlockItemList:
     case NodeEnum::kConstraintExpressionList:
@@ -1312,8 +1313,11 @@
     case NodeEnum::kCaseInsideItemList:
     case NodeEnum::kGenerateCaseItemList:
     case NodeEnum::kClassItems:
+    case NodeEnum::kInterfaceClassItemList:
     case NodeEnum::kModuleItemList:
     case NodeEnum::kGenerateItemList:
+    // Aligns parameter, net/variable, and assignment declarations in packages.
+    case NodeEnum::kPackageItemList:
     case NodeEnum::kDistributionItemList:
     case NodeEnum::kEnumNameList:
     case NodeEnum::kStructUnionMemberList: {
@@ -1816,6 +1820,14 @@
   }
 }
 
+// True if any token in this leaf partition is an EOL comment.
+static bool PartitionContainsEOLComment(const TokenPartitionTree &partition) {
+  for (const auto &token : partition.Value().TokensRange()) {
+    if (token.TokenEnum() == verilog_tokentype::TK_EOL_COMMENT) return true;
+  }
+  return false;
+}
+
 static void PushEndIntoElsePartition(TokenPartitionTree *partition_ptr) {
   // Then combine 'end' with the following 'else' ...
   // Do not flatten, so that if- and else- clauses can make formatting
@@ -1823,6 +1835,15 @@
   auto &partition = *partition_ptr;
   auto &if_clause_partition = partition.Children().front();
   auto *end_partition = &RightmostDescendant(if_clause_partition);
+  // When 'end' carries a trailing EOL comment, 'else' must start on the next
+  // line (see token annotator: comment before else => MustWrap). Merging
+  // end+comment into the else-if header makes fit-else-expand treat the
+  // header as wider than the eventual formatted line, which wraps the
+  // else-if body on re-format and fails convergence (GitHub issue 2540).
+  if (PartitionContainsEOLComment(*end_partition)) {
+    VLOG(4) << "end has EOL comment, skip merge into else";
+    return;
+  }
   auto *end_parent = verible::MergeLeafIntoNextLeaf(end_partition);
   // if moving leaf results in any singleton partitions, hoist.
   if (end_parent != nullptr) {
@@ -2898,7 +2919,7 @@
     case NodeEnum::kConstraintBlockItemList: {
       HoistOnlyChildPartition(&partition);
 
-      // Alwyas expand constraint(s) blocks with braces inside them
+      // Always expand constraint(s) blocks with braces inside them
       const auto &uwline = partition.Value();
       const auto &ftokens = uwline.TokensRange();
       auto found = std::find_if(ftokens.begin(), ftokens.end(),
@@ -2970,10 +2991,15 @@
       auto &children = partition.Children();
       const auto iter1 = std::find_if(children.begin(), children.end(),
                                       PartitionStartsWithSemicolon);
-      CHECK(iter1 != children.end());
+      // An attribute instance ((* ... *)) in the for-loop header can change
+      // the partition structure so that the expected semicolon-leading
+      // partitions are not present.  When they are missing, leave the
+      // partitions unreshaped rather than aborting (avoids a CHECK-failure
+      // crash on attributed/unusual for-headers).
+      if (iter1 == children.end()) break;
       const auto iter2 =
           std::find_if(iter1 + 1, children.end(), PartitionStartsWithSemicolon);
-      CHECK(iter2 != children.end());
+      if (iter2 == children.end()) break;
       const int dist1 = std::distance(children.begin(), iter1);
       const int dist2 = std::distance(children.begin(), iter2);
       VLOG(4) << "kForSpec got ';' at child " << dist1 << " and " << dist2;
diff --git a/verible/verilog/parser/verilog-parser_test.cc b/verible/verilog/parser/verilog-parser_test.cc
index 7c99a1e..754498b 100644
--- a/verible/verilog/parser/verilog-parser_test.cc
+++ b/verible/verilog/parser/verilog-parser_test.cc
@@ -1339,6 +1339,15 @@
 static constexpr ParserTestCaseArray kModuleTests = {
     "module modular_thing;\n"
     "endmodule",
+    "module m;\n"
+    "wire logic b;\n"
+    "endmodule",
+    "module m;\n"
+    "wire bit b;\n"
+    "endmodule",
+    "module m;\n"
+    "wire logic [3:0] b;\n"
+    "endmodule",
     "module semicolon_madness;;;;;\n"
     "endmodule",
     "module automatic modular_thing;\n"
@@ -1381,6 +1390,62 @@
     "input logic [N:0] a;\n"
     "output logic co;\n"
     "endmodule",
+    "module t;\n"
+    "input bit i_bit;\n"
+    "input byte i_byte;\n"
+    "input chandle i_chandle;\n"
+    "input event i_event;\n"
+    "input int i_int;\n"
+    "input integer i_inte;\n"
+    "input longint i_longint;\n"
+    "input real i_real;\n"
+    "input realtime i_realtime;\n"
+    "input shortint i_shortint;\n"
+    "input shortreal i_shortreal;\n"
+    "input string i_string;\n"
+    "input time i_time;\n"
+    "output bit o_bit;\n"
+    "output byte o_byte;\n"
+    "output chandle o_chandle;\n"
+    "output event o_event;\n"
+    "output int o_int;\n"
+    "output integer o_inte;\n"
+    "output longint o_longint;\n"
+    "output real o_real;\n"
+    "output realtime o_realtime;\n"
+    "output shortint o_shortint;\n"
+    "output shortreal o_shortreal;\n"
+    "output string o_string;\n"
+    "output time o_time;\n"
+    "endmodule\n",
+    "module t (\n"
+    "input bit i_bit,\n"
+    "input byte i_byte,\n"
+    "input chandle i_chandle,\n"
+    "input event i_event,\n"
+    "input int i_int,\n"
+    "input integer i_inte,\n"
+    "input longint i_longint,\n"
+    "input real i_real,\n"
+    "input realtime i_realtime,\n"
+    "input shortint i_shortint,\n"
+    "input shortreal i_shortreal,\n"
+    "input string i_string,\n"
+    "input time i_time,\n"
+    "output bit o_bit,\n"
+    "output byte o_byte,\n"
+    "output chandle o_chandle,\n"
+    "output event o_event,\n"
+    "output int o_int,\n"
+    "output integer o_inte,\n"
+    "output longint o_longint,\n"
+    "output real o_real,\n"
+    "output realtime o_realtime,\n"
+    "output shortint o_shortint,\n"
+    "output shortreal o_shortreal,\n"
+    "output string o_string,\n"
+    "output time o_time);\n"
+    "endmodule\n",
     "module zoom (a, co);\n"
     "input bus_type a;\n"
     "output bus_type [3:0] co;\n"
@@ -3796,6 +3861,10 @@
     "union tagged packed { int i; bit b; } foo;",
     "union tagged packed signed { int i; bit b; } foo;",
     "union tagged packed unsigned { int i; bit b; } foo;",
+    "union soft { int i; bit b; } foo;",
+    "union soft packed { int i; bit b; } foo;",
+    "union soft packed signed { int i; bit b; } foo;",
+    "union soft packed unsigned { int i; bit b; } foo;",
 };
 
 // TODO(fangism): implement and test ENUM_CONSTANT
diff --git a/verible/verilog/parser/verilog.y b/verible/verilog/parser/verilog.y
index 6a4a6f5..a6de1a8 100644
--- a/verible/verilog/parser/verilog.y
+++ b/verible/verilog/parser/verilog.y
@@ -679,7 +679,7 @@
 
 /* most likely a lexical error */
 %token TK_OTHER
-// LINT.ThenChange(../formatting/verilog_token.cc)
+// LINT.ThenChange(../formatting/verilog-token.cc)
 
 /* A glorified ';' specialized to mark the end of an
    assertion_variable_declaration list inside the
@@ -2111,6 +2111,9 @@
     { $$ = MakeTaggedNode(N::kDataTypeImplicitIdDimensions,
                           MakeDataType($1, MakePackedDimensionsNode($2)),
                           $3, nullptr, nullptr); }
+  | data_type_primitive delay3_or_drive_opt
+    { $$ = MakeTaggedNode(N::kDataTypeImplicitIdDimensions,
+                          $1, $2, nullptr, nullptr); }
   | GenericIdentifier decl_dimensions_opt delay3_or_drive_opt
     { $$ = MakeTaggedNode(N::kDataTypeImplicitIdDimensions,
                           MakeDataType(MakeTaggedNode(N::kLocalRoot,MakeTaggedNode(N::kUnqualifiedId,$1)), MakePackedDimensionsNode($2)),
@@ -3726,7 +3729,7 @@
 struct_data_type
   : TK_struct packed_signing_opt '{' struct_union_member_list '}'
     { $$ = MakeTaggedNode(N::kStructType, $1, $2, MakeBraceGroup($3, $4, $5)); }
-  | TK_union TK_tagged_opt packed_signing_opt '{' struct_union_member_list '}'
+  | TK_union TK_union_qualifier_opt packed_signing_opt '{' struct_union_member_list '}'
     { $$ = MakeTaggedNode(N::kUnionType, $1, $2, $3,
                           MakeBraceGroup($4, $5, $6)); }
   ;
@@ -5620,16 +5623,55 @@
   | port_direction signed_unsigned_opt list_of_module_item_identifiers ';'
     { $$ = MakeTaggedNode(N::kModulePortDeclaration, $1, MakeDataType($2, nullptr, nullptr), $3, $4);}
     /* implicit type */
+    /* Keep net/var type before signing so CST leaf order matches source
+     * ("wire signed", not "signed" then "wire"). The formatter walks CST
+     * order and CHECK-fails when a later leaf appears earlier in the file.
+     * Wrap as kDataTypePrimitive like the TK_bit rule so
+     * GetBaseTypeFromDataType still treats child 1 as the type.
+     */
   | port_direction port_net_type signed_unsigned_opt decl_dimensions_opt
     list_of_identifiers_unpacked_dimensions ';'
     { $$ = MakeTaggedNode(N::kModulePortDeclaration, $1,
-                          MakeDataType($3, ForwardChildren($2), MakePackedDimensionsNode($4)),
+                          MakeDataType(MakeTaggedNode(N::kDataTypePrimitive, $2, $3),
+                                       MakePackedDimensionsNode($4)),
                           $5, $6); }
   | dir var_type signed_unsigned_opt decl_dimensions_opt
     list_of_port_identifiers ';'
     { $$ = MakeTaggedNode(N::kModulePortDeclaration, $1,
-                          MakeDataType($3, ForwardChildren($2), MakePackedDimensionsNode($4)),
+                          MakeDataType(MakeTaggedNode(N::kDataTypePrimitive, $2, $3),
+                                       MakePackedDimensionsNode($4)),
                           $5, $6); }
+  | port_direction TK_bit signed_unsigned_opt decl_dimensions_opt
+    list_of_identifiers_unpacked_dimensions ';'
+    { $$ = MakeTaggedNode(N::kModulePortDeclaration, $1,
+                          MakeDataType(MakeTaggedNode(N::kDataTypePrimitive, $2, $3),
+                                       MakePackedDimensionsNode($4)),
+                          $5, $6); }
+  | port_direction integer_atom_type signed_unsigned_opt
+    list_of_identifiers_unpacked_dimensions ';'
+    { $$ = MakeTaggedNode(N::kModulePortDeclaration, $1,
+                          MakeDataType(MakeTaggedNode(N::kDataTypePrimitive, $2, $3)),
+                          $4, $5); }
+  | port_direction non_integer_type
+    list_of_identifiers_unpacked_dimensions ';'
+    { $$ = MakeTaggedNode(N::kModulePortDeclaration, $1,
+                          MakeDataType(MakeTaggedNode(N::kDataTypePrimitive, $2)),
+                          $3, $4); }
+  | port_direction TK_string
+    list_of_identifiers_unpacked_dimensions ';'
+    { $$ = MakeTaggedNode(N::kModulePortDeclaration, $1,
+                          MakeDataType(MakeTaggedNode(N::kDataTypePrimitive, $2)),
+                          $3, $4); }
+  | port_direction TK_event
+    list_of_identifiers_unpacked_dimensions ';'
+    { $$ = MakeTaggedNode(N::kModulePortDeclaration, $1,
+                          MakeDataType(MakeTaggedNode(N::kDataTypePrimitive, $2)),
+                          $3, $4); }
+  | port_direction TK_chandle
+    list_of_identifiers_unpacked_dimensions ';'
+    { $$ = MakeTaggedNode(N::kModulePortDeclaration, $1,
+                          MakeDataType(MakeTaggedNode(N::kDataTypePrimitive, $2)),
+                          $3, $4); }
   ;
 
 parameter_override
@@ -7375,8 +7417,10 @@
   | /* empty */
     { $$ = nullptr; }
   ;
-TK_tagged_opt
-  : TK_tagged
+TK_union_qualifier_opt
+  : TK_soft
+    { $$ = std::move($1); }
+  | TK_tagged
     { $$ = std::move($1); }
   | /* empty */
     { $$ = nullptr; }
diff --git a/verible/verilog/preprocessor/verilog-preprocess.h b/verible/verilog/preprocessor/verilog-preprocess.h
index 568ed5e..2d134ae 100644
--- a/verible/verilog/preprocessor/verilog-preprocess.h
+++ b/verible/verilog/preprocessor/verilog-preprocess.h
@@ -124,7 +124,7 @@
   // Initialize preprocessing with safe default options
   // TODO(hzeller): remove this constructor once all places using the
   // preprocessor have been updated to pass a config.
-  VerilogPreprocess() : VerilogPreprocess(Config()){};
+  VerilogPreprocess() : VerilogPreprocess(Config()) {};
 
   // ScanStream reads in a stream of tokens returns the result as a move
   // of preprocessor_data_.  preprocessor_data_ should not be accessed
diff --git a/verible/verilog/tools/formatter/README.md b/verible/verilog/tools/formatter/README.md
index 0dbafdb..a3ffb45 100644
--- a/verible/verilog/tools/formatter/README.md
+++ b/verible/verilog/tools/formatter/README.md
@@ -4,8 +4,8 @@
 freshness: { owner: 'hzeller' reviewed: '2020-10-07' }
 *-->
 
-`verible-verilog-format` is the SystemVerilog formatter tool. You can can
-get a full set of avilable flags using the `--helpfull` flag.
+`verible-verilog-format` is the SystemVerilog formatter tool. You can
+get a full set of available flags using the `--helpfull` flag.
 
 For automatic formatting suggestions on github pull requests, there is a
 [easy to integrate github action available][github-format-action].
@@ -35,12 +35,18 @@
       operator.); default: 4;
 
   Flags from verilog/formatting/format_style_init.cc:
-    --assignment_statement_alignment (Format various assignments:
+    --assignment_statement_alignment (Format various assignments in
+      module, generate, interface, and package bodies:
       {align,flush-left,preserve,infer}); default: infer;
     --case_items_alignment (Format case items:
       {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:
@@ -48,11 +54,13 @@
     --enum_assignment_statement_alignment (Format assignments with enums:
       {align,flush-left,preserve,infer}); default: infer;
     --expand_coverpoints (If true, always expand coverpoints.); default: false;
-    --formal_parameters_alignment (Format formal parameters:
-      {align,flush-left,preserve,infer}); default: infer;
+    --formal_parameters_alignment (Format formal parameters in module/
+      interface/class headers (inside #(...)): {align,flush-left,preserve,infer});
+      default: infer;
     --formal_parameters_indentation (Indent formal parameters: {indent,wrap});
       default: wrap;
-    --module_net_variable_alignment (Format net/variable declarations:
+    --module_net_variable_alignment (Format net/variable declarations
+      in module, generate, interface, and package bodies:
       {align,flush-left,preserve,infer}); default: infer;
     --named_parameter_alignment (Format named actual parameters:
       {align,flush-left,preserve,infer}); default: infer;
@@ -62,6 +70,10 @@
       {align,flush-left,preserve,infer}); default: infer;
     --named_port_indentation (Indent named port connections: {indent,wrap});
       default: wrap;
+    --parameter_declaration_alignment (Format parameter/localparam declarations
+      in module, generate, interface, and package bodies:
+      {align,flush-left,preserve,infer}); default: infer;
+      NOTE: class body parameter declarations are NOT affected.
     --port_declarations_alignment (Format port declarations:
       {align,flush-left,preserve,infer}); default: infer;
     --port_declarations_indentation (Indent port declarations: {indent,wrap});
diff --git a/verible/verilog/tools/kythe/README.md b/verible/verilog/tools/kythe/README.md
index b44aaf9..610f755 100644
--- a/verible/verilog/tools/kythe/README.md
+++ b/verible/verilog/tools/kythe/README.md
@@ -30,4 +30,6 @@
                          File search will stop at the the first found among the listed directories.
                          e.g --include_dir_paths directory1,directory2
                          if "A.sv" exists in both "directory1" and "directory2" the one in "directory1" is the one we will use)
+    --output_path (Path of the file to write the extracted Kythe facts to.
+                   Writes to stdout if empty or "-"); default: "";
 ```
diff --git a/verible/verilog/tools/kythe/kythe-facts.cc b/verible/verilog/tools/kythe/kythe-facts.cc
index 3880d91..948a54e 100644
--- a/verible/verilog/tools/kythe/kythe-facts.cc
+++ b/verible/verilog/tools/kythe/kythe-facts.cc
@@ -154,12 +154,16 @@
     stream << idt << "\"source\": ";
     source_node.FormatJSON(stream, debug, indent_more) << "," << separator;
   }
-  { stream << idt << "\"edge_kind\": \"" << edge_name << "\"," << separator; }
+  {
+    stream << idt << "\"edge_kind\": \"" << edge_name << "\"," << separator;
+  }
   {
     stream << idt << "\"target\": ";
     target_node.FormatJSON(stream, debug, indent_more) << "," << separator;
   }
-  { stream << idt << "\"fact_name\": \"/\"" << separator; }
+  {
+    stream << idt << "\"fact_name\": \"/\"" << separator;
+  }
   return stream << verible::Spacer(indentation) << "}";
 }
 
diff --git a/verible/verilog/tools/kythe/kythe-proto-output.cc b/verible/verilog/tools/kythe/kythe-proto-output.cc
index 0117329..76e5952 100644
--- a/verible/verilog/tools/kythe/kythe-proto-output.cc
+++ b/verible/verilog/tools/kythe/kythe-proto-output.cc
@@ -14,9 +14,12 @@
 
 #include "verible/verilog/tools/kythe/kythe-proto-output.h"
 
+#include <memory>
+#include <ostream>
 #include <string>
 
 #include "google/protobuf/io/coded_stream.h"
+#include "google/protobuf/io/zero_copy_stream.h"
 #include "google/protobuf/io/zero_copy_stream_impl.h"
 #include "third_party/proto/kythe/storage.pb.h"
 #include "verible/verilog/tools/kythe/kythe-facts.h"
@@ -26,7 +29,8 @@
 namespace {
 
 using ::google::protobuf::io::CodedOutputStream;
-using ::google::protobuf::io::FileOutputStream;
+using ::google::protobuf::io::OstreamOutputStream;
+using ::google::protobuf::io::ZeroCopyOutputStream;
 using ::kythe::proto::Entry;
 
 // Returns the VName representation in Kythe's storage proto format.
@@ -60,7 +64,7 @@
 }
 
 // Output entry to the stream.
-void OutputProto(const Entry &entry, FileOutputStream *stream) {
+void OutputProto(const Entry &entry, ZeroCopyOutputStream *stream) {
   CodedOutputStream coded_stream(stream);
   coded_stream.WriteVarint32(entry.ByteSizeLong());
   entry.SerializeToCodedStream(&coded_stream);
@@ -68,14 +72,17 @@
 
 }  // namespace
 
-KytheProtoOutput::KytheProtoOutput(int fd) : out_(fd) {}
-KytheProtoOutput::~KytheProtoOutput() { out_.Close(); }
+KytheProtoOutput::KytheProtoOutput(std::ostream &output_stream)
+    : out_(std::make_unique<OstreamOutputStream>(&output_stream)) {}
+
+// OstreamOutputStream flushes its remaining bytes when destroyed.
+KytheProtoOutput::~KytheProtoOutput() = default;
 
 void KytheProtoOutput::Emit(const Fact &fact) {
-  OutputProto(ConvertFactToEntry(fact), &out_);
+  OutputProto(ConvertFactToEntry(fact), out_.get());
 }
 void KytheProtoOutput::Emit(const Edge &edge) {
-  OutputProto(ConvertEdgeToEntry(edge), &out_);
+  OutputProto(ConvertEdgeToEntry(edge), out_.get());
 }
 
 }  // namespace kythe
diff --git a/verible/verilog/tools/kythe/kythe-proto-output.h b/verible/verilog/tools/kythe/kythe-proto-output.h
index ba873cd..2ffa58e 100644
--- a/verible/verilog/tools/kythe/kythe-proto-output.h
+++ b/verible/verilog/tools/kythe/kythe-proto-output.h
@@ -15,7 +15,10 @@
 #ifndef VERIBLE_VERILOG_TOOLS_KYTHE_KYTHE_PROTO_OUTPUT_H_
 #define VERIBLE_VERILOG_TOOLS_KYTHE_KYTHE_PROTO_OUTPUT_H_
 
-#include "google/protobuf/io/zero_copy_stream_impl.h"
+#include <memory>
+#include <ostream>
+
+#include "google/protobuf/io/zero_copy_stream.h"
 #include "verible/verilog/tools/kythe/kythe-facts-extractor.h"
 #include "verible/verilog/tools/kythe/kythe-facts.h"
 
@@ -24,7 +27,10 @@
 
 class KytheProtoOutput final : public KytheOutput {
  public:
-  explicit KytheProtoOutput(int output_fd);
+  // Writes to an already-open stream. The stream must outlive this object and
+  // must be opened in binary mode, as the proto entries are not text.
+  explicit KytheProtoOutput(std::ostream &output_stream);
+
   ~KytheProtoOutput() final;
 
   // Output Kythe facts from the indexing data in proto format.
@@ -32,7 +38,7 @@
   void Emit(const Edge &edge) final;
 
  private:
-  ::google::protobuf::io::FileOutputStream out_;
+  std::unique_ptr<::google::protobuf::io::ZeroCopyOutputStream> out_;
 };
 
 }  // namespace kythe
diff --git a/verible/verilog/tools/kythe/verilog-kythe-extractor.cc b/verible/verilog/tools/kythe/verilog-kythe-extractor.cc
index df43e52..321db49 100644
--- a/verible/verilog/tools/kythe/verilog-kythe-extractor.cc
+++ b/verible/verilog/tools/kythe/verilog-kythe-extractor.cc
@@ -12,7 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include <fstream>
+#include <ios>
 #include <iostream>
+#include <memory>
+#include <ostream>
 #include <sstream>
 #include <string>
 #include <string_view>
@@ -33,11 +37,9 @@
 #include "verible/verilog/tools/kythe/kythe-facts.h"
 #include "verible/verilog/tools/kythe/kythe-proto-output.h"
 
-#ifndef _WIN32
-#include <unistd.h>  // for STDOUT_FILENO
-#else
-#include <stdio.h>
-#define STDOUT_FILENO _fileno(stdout)
+#ifdef _WIN32
+#include <fcntl.h>
+#include <io.h>
 #endif
 
 // for --print_kythe_facts flag
@@ -109,14 +111,18 @@
 ABSL_FLAG(std::string, verilog_project_name, "",
           "Verilog project name to use as Kythe corpus. Optional");
 
+ABSL_FLAG(std::string, output_path, "",
+          R"(Path of the file to write the extracted Kythe facts to.
+Writes to stdout if empty or "-".)");
+
 namespace verilog {
 namespace kythe {
 
-// Prints Kythe facts in proto format to stdout.
+// Prints Kythe facts in proto format to the given stream.
 static void PrintKytheFactsProtoEntries(
     const IndexingFactNode &file_list_facts_tree, const VerilogProject &project,
-    int fd) {
-  KytheProtoOutput proto_output(fd);
+    std::ostream &stream) {
+  KytheProtoOutput proto_output(stream);
   StreamKytheFactsEntries(&proto_output, file_list_facts_tree, project);
 }
 
@@ -134,7 +140,7 @@
 
 static std::vector<absl::Status> ExtractTranslationUnits(
     std::string_view file_list_path, VerilogProject *project,
-    const std::vector<std::string> &file_names) {
+    const std::vector<std::string> &file_names, std::ostream &output_stream) {
   std::vector<absl::Status> errors;
   const verilog::kythe::IndexingFactNode file_list_facts_tree(
       verilog::kythe::ExtractFiles(file_list_path, project, file_names,
@@ -149,17 +155,17 @@
   // check how to output kythe facts.
   switch (absl::GetFlag(FLAGS_print_kythe_facts)) {
     case PrintMode::kJSON:
-      std::cout << KytheFactsPrinter(file_list_facts_tree, *project)
-                << std::endl;
+      output_stream << KytheFactsPrinter(file_list_facts_tree, *project)
+                    << std::endl;
       break;
     case PrintMode::kJSONDebug:
-      std::cout << KytheFactsPrinter(file_list_facts_tree, *project,
-                                     /*debug=*/true)
-                << std::endl;
+      output_stream << KytheFactsPrinter(file_list_facts_tree, *project,
+                                         /*debug=*/true)
+                    << std::endl;
       break;
     case PrintMode::kProto:
       PrintKytheFactsProtoEntries(file_list_facts_tree, *project,
-                                  STDOUT_FILENO);
+                                  output_stream);
       break;
     case PrintMode::kNone:
       KytheFactsNullPrinter(file_list_facts_tree, *project);
@@ -173,6 +179,13 @@
 }  // namespace verilog
 
 int main(int argc, char **argv) {
+#ifdef _WIN32
+  // Windows messes with newlines by default. Fix this here, so that stdout
+  // carries the same bytes as --output_path, and so that the proto entries
+  // stay binary.
+  _setmode(_fileno(stdout), _O_BINARY);
+#endif
+
   const auto usage =
       absl::StrCat("usage: ", argv[0], " [options] --file_list_path FILE\n", R"(
 Extracts kythe indexing facts from the given SystemVerilog source files.
@@ -211,9 +224,24 @@
                                   absl::GetFlag(FLAGS_verilog_project_name),
                                   /*provide_lookup_file_origin=*/false);
 
+  // Send the facts to a file when asked to, otherwise to stdout. Both go out
+  // in binary mode, because --print_kythe_facts=proto is not text.
+  const std::string output_path = absl::GetFlag(FLAGS_output_path);
+  std::unique_ptr<std::ofstream> file_closer;
+  std::ostream *output_stream = &std::cout;
+  if (!output_path.empty() && output_path != "-") {
+    file_closer = std::make_unique<std::ofstream>(
+        output_path, std::ios::out | std::ios::binary);
+    if (!file_closer->good()) {
+      LOG(ERROR) << "Failed to create/open output file: " << output_path;
+      return 1;
+    }
+    output_stream = file_closer.get();
+  }
+
   const std::vector<absl::Status> errors(
       verilog::kythe::ExtractTranslationUnits(file_list_path, &project,
-                                              file_paths));
+                                              file_paths, *output_stream));
   if (!errors.empty()) {
     LOG(ERROR) << "Encountered some issues while indexing files (could result "
                   "in missing indexing data):"
diff --git a/verible/verilog/tools/kythe/verilog_kythe_extractor_test.sh b/verible/verilog/tools/kythe/verilog_kythe_extractor_test.sh
index bf3bd7c..1e7bcc9 100755
--- a/verible/verilog/tools/kythe/verilog_kythe_extractor_test.sh
+++ b/verible/verilog/tools/kythe/verilog_kythe_extractor_test.sh
@@ -160,4 +160,90 @@
 }
 
 ################################################################################
+echo "=== Write facts to --output_path instead of stdout."
+
+cat > "$MY_INPUT_FILE" <<EOF
+localparam int fooo = 1;
+localparam int barr = fooo;
+EOF
+
+echo "myinput.txt" > "${TEST_TMPDIR}/file_list"
+
+# Every print mode has to honor --output_path, and has to write the same bytes
+# it would have written to stdout.
+for mode in json json_debug proto ; do
+  "$extractor" \
+    --file_list_path "${TEST_TMPDIR}/file_list" \
+    --file_list_root "$(dirname "$MY_INPUT_FILE")" \
+    --print_kythe_facts="$mode" \
+    --output_path "$MY_OUTPUT_FILE" 2>/dev/null
+
+  status="$?"
+  [[ $status == 0 ]] || {
+    echo "Expected exit code 0 for --print_kythe_facts=$mode, but got $status"
+    exit 1
+  }
+
+  [[ -s "$MY_OUTPUT_FILE" ]] || {
+    echo "Expected --output_path file to be non-empty for mode $mode."
+    exit 1
+  }
+
+  "$extractor" \
+    --file_list_path "${TEST_TMPDIR}/file_list" \
+    --file_list_root "$(dirname "$MY_INPUT_FILE")" \
+    --print_kythe_facts="$mode" \
+    > "$MY_EXPECT_FILE" 2>/dev/null
+
+  cmp "$MY_OUTPUT_FILE" "$MY_EXPECT_FILE" || {
+    echo "--output_path output differs from stdout output for mode $mode."
+    exit 1
+  }
+done
+
+################################################################################
+echo "=== '--output_path -' writes to stdout."
+
+"$extractor" \
+  --file_list_path "${TEST_TMPDIR}/file_list" \
+  --file_list_root "$(dirname "$MY_INPUT_FILE")" \
+  --print_kythe_facts=json \
+  --output_path - \
+  > "$MY_OUTPUT_FILE" 2>/dev/null
+
+status="$?"
+[[ $status == 0 ]] || {
+  echo "Expected exit code 0, but got $status"
+  exit 1
+}
+
+grep -q "signature" "$MY_OUTPUT_FILE" || {
+  echo "Expected \"signature\" in $MY_OUTPUT_FILE but didn't find it.  Got:"
+  cat "$MY_OUTPUT_FILE"
+  exit 1
+}
+
+################################################################################
+echo "=== Expect failure on unwritable --output_path."
+
+"$extractor" \
+  --file_list_path "${TEST_TMPDIR}/file_list" \
+  --file_list_root "$(dirname "$MY_INPUT_FILE")" \
+  --print_kythe_facts=json \
+  --output_path "${TEST_TMPDIR}/nonexistent-dir/out.json" \
+  > "$MY_OUTPUT_FILE" 2>&1
+
+status="$?"
+[[ $status == 1 ]] || {
+  echo "Expected exit code 1, but got $status"
+  exit 1
+}
+
+grep -q "Failed to create/open output file" "$MY_OUTPUT_FILE" || {
+  echo "Expected \"Failed to create/open output file\" in $MY_OUTPUT_FILE but didn't find it.  Got:"
+  cat "$MY_OUTPUT_FILE"
+  exit 1
+}
+
+################################################################################
 echo "PASS"
diff --git a/verible/verilog/tools/lint/README.md b/verible/verilog/tools/lint/README.md
index a91128d..f1117be 100644
--- a/verible/verilog/tools/lint/README.md
+++ b/verible/verilog/tools/lint/README.md
@@ -33,7 +33,7 @@
 
 ## Developers
 
-[Style lint rule development guide](../../../doc/style_lint.md).
+[Style lint rule development guide](../../../../doc/style_lint.md).
 
 ## Usage
 
diff --git a/verible/verilog/tools/ls/autoexpand.cc b/verible/verilog/tools/ls/autoexpand.cc
index 0f9d5c4..82f7de0 100644
--- a/verible/verilog/tools/ls/autoexpand.cc
+++ b/verible/verilog/tools/ls/autoexpand.cc
@@ -1464,10 +1464,26 @@
     module->RetrieveDependencies(modules_);
   }
   // Sort modules in the buffer based on a dependency graph, so that AUTOs are
-  // expanded in order
+  // expanded in order. Counting dependents and breaking ties by name induces a
+  // strict weak ordering even with dependency loops or independent modules.
+  absl::flat_hash_map<const Module *, int> dependents_count;
+  for (const Module *module : buffer_modules) {
+    int count = 0;
+    for (const Module *other : buffer_modules) {
+      if (other != module && other->DependsOn(module)) {
+        ++count;
+      }
+    }
+    dependents_count[module] = count;
+  }
   std::sort(buffer_modules.begin(), buffer_modules.end(),
-            [](const Module *left, const Module *right) {
-              return right->DependsOn(left);
+            [&dependents_count](const Module *left, const Module *right) {
+              const int left_count = dependents_count.at(left);
+              const int right_count = dependents_count.at(right);
+              if (left_count != right_count) {
+                return left_count > right_count;
+              }
+              return left->Name() < right->Name();
             });
   for (Module *const module : buffer_modules) {
     // Ports declared in AUTOINPUT/AUTOINOUT/AUTOOUTPUT must be removed from