diff --git a/compiler/cpp/verilog.cpp b/compiler/cpp/verilog.cpp
index 510f78a4..d5f7a9b6 100644
--- a/compiler/cpp/verilog.cpp
+++ b/compiler/cpp/verilog.cpp
@@ -7134,6 +7134,14 @@ class VerilogCompiler
coreModule.FinishPorts();
+ // Mark this module as a Verilator hierarchical block. The
+ // /*verilator hier_block*/ metacomment must appear inside the module
+ // body, after the port list.
+ coreModule.AddVerbatimOp(GetUnknownLocation(), [&](VerbatimWriter &writer)
+ {
+ writer << "/*verilator hier_block*/";
+ });
+
DeclareDebugVariables();
DeclareStringTable();
diff --git a/compiler/hs/app/Options.hs b/compiler/hs/app/Options.hs
index 4ac42373..5d335a6f 100644
--- a/compiler/hs/app/Options.hs
+++ b/compiler/hs/app/Options.hs
@@ -145,4 +145,3 @@ data Options
, log_file :: Maybe String
}
deriving (Show, Data, Typeable)
-
diff --git a/doc/mapping-to-hardware.md b/doc/mapping-to-hardware.md
index f6e5dc45..56e1c065 100644
--- a/doc/mapping-to-hardware.md
+++ b/doc/mapping-to-hardware.md
@@ -34,6 +34,12 @@ class Foo
+## Verilator hierarchical blocks
+The generated core design module for each `export` class includes the
+`/*verilator hier_block*/` metacomment immediately after the module port list.
+This enables [hierarchical Verilation](https://veripool.org/guide/latest/verilating.html#hierarchical-verilation)
+for Verilator users while leaving helper modules (such as wrappers) unannotated.
+
## Threads
Threads in Kanagawa are runtime constructs. The source does not specify the
number of threads that will be created. A thread is defined by:
diff --git a/test/compiler/cli/CMakeLists.txt b/test/compiler/cli/CMakeLists.txt
index 1ac3f2a7..7ab328f7 100644
--- a/test/compiler/cli/CMakeLists.txt
+++ b/test/compiler/cli/CMakeLists.txt
@@ -105,6 +105,17 @@ add_cli_test(skip_circt_lowering
"python3 ${CMAKE_CURRENT_SOURCE_DIR}/check_skip_circt_lowering.py ${CMAKE_CURRENT_BINARY_DIR}/skip_circt_lowering"
)
+add_cli_test(verilator_hier_blocks
+ SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/verilator_hier_blocks.k
+ OPTIONS
+ --backend=sv
+ --base-library=${CMAKE_SOURCE_DIR}/library/mini-base.k
+ --import-dir=${CMAKE_SOURCE_DIR}/library
+ --place-iterations=1
+ TEST
+ "python3 ${CMAKE_CURRENT_SOURCE_DIR}/check_verilator_hier_blocks.py ${CMAKE_CURRENT_BINARY_DIR}/verilator_hier_blocks"
+)
+
# `list-deps` resolves imports without running the frontend or codegen, so it
# does not fit add_cli_test's compile-and-inspect shape.
add_golden_test(cli.list_deps
diff --git a/test/compiler/cli/check_generated_sv_modules.py b/test/compiler/cli/check_generated_sv_modules.py
new file mode 100644
index 00000000..d7927780
--- /dev/null
+++ b/test/compiler/cli/check_generated_sv_modules.py
@@ -0,0 +1,100 @@
+#!/usr/bin/env python3
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+"""
+Generic checks for generated SystemVerilog modules.
+
+This script parses all `module` declarations in `*.sv` files under an output
+directory and applies generic pattern-based assertions.
+"""
+import argparse
+import re
+import sys
+from pathlib import Path
+
+MODULE_DECL = re.compile(r'(?:^|\s)module\s+(\w+)', re.MULTILINE)
+
+
+def collect_modules(sv_files):
+ modules = []
+ for sv in sv_files:
+ text = sv.read_text()
+ decls = list(MODULE_DECL.finditer(text))
+ for i, decl in enumerate(decls):
+ end = decls[i + 1].start() if i + 1 < len(decls) else len(text)
+ modules.append((sv.name, decl.group(1), text[decl.start():end]))
+ return modules
+
+
+def check_modules(
+ output_dir,
+ expect_any_regex=(),
+ expect_any_regex_after_port_list=(),
+ expect_any_without_regex=(),
+):
+ sv_files = sorted(output_dir.glob('*.sv'))
+ if not sv_files:
+ print("Expected at least one generated .sv file, but none was found.")
+ return 1
+
+ modules = collect_modules(sv_files)
+ if not modules:
+ names = ', '.join(s.name for s in sv_files)
+ print(f"No SystemVerilog modules found in {names}.")
+ return 1
+
+ module_names = ', '.join(m[1] for m in modules)
+
+ for pattern in expect_any_regex:
+ regex = re.compile(pattern, re.MULTILINE)
+ if not any(regex.search(m[2]) for m in modules):
+ print(f"Expected at least one module to match regex {pattern!r}: {module_names}.")
+ return 1
+
+ for pattern in expect_any_regex_after_port_list:
+ regex = re.compile(pattern, re.MULTILINE)
+ matched = False
+ for _, _, body in modules:
+ port_list_end = body.find(');')
+ if port_list_end != -1 and regex.search(body[port_list_end + 2:]):
+ matched = True
+ break
+ if not matched:
+ print(
+ f"Expected at least one module to match regex {pattern!r} after "
+ f"the port list (`);`): {module_names}."
+ )
+ return 1
+
+ for pattern in expect_any_without_regex:
+ regex = re.compile(pattern, re.MULTILINE)
+ if not any(not regex.search(m[2]) for m in modules):
+ print(f"Expected at least one module not to match regex {pattern!r}: {module_names}.")
+ return 1
+
+ return 0
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('output_dir', help='Directory containing compiler outputs')
+ parser.add_argument('--expect-any-regex', action='append', default=[])
+ parser.add_argument('--expect-any-regex-after-port-list', action='append', default=[])
+ parser.add_argument('--expect-any-without-regex', action='append', default=[])
+ args = parser.parse_args()
+
+ output_dir = Path(args.output_dir)
+ if not output_dir.is_dir():
+ print(f"Output directory does not exist: {output_dir}")
+ return 1
+
+ return check_modules(
+ output_dir,
+ expect_any_regex=args.expect_any_regex,
+ expect_any_regex_after_port_list=args.expect_any_regex_after_port_list,
+ expect_any_without_regex=args.expect_any_without_regex,
+ )
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/test/compiler/cli/check_verilator_hier_blocks.py b/test/compiler/cli/check_verilator_hier_blocks.py
new file mode 100644
index 00000000..4f5228d9
--- /dev/null
+++ b/test/compiler/cli/check_verilator_hier_blocks.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+"""Verify hier_block annotations in generated SystemVerilog output."""
+import sys
+from pathlib import Path
+
+from check_generated_sv_modules import check_modules
+
+HIER_BLOCK_REGEX = r'/\*verilator hier_block\*/'
+
+
+def main():
+ if len(sys.argv) != 2:
+ print("Usage: check_verilator_hier_blocks.py ")
+ return 1
+
+ output_dir = Path(sys.argv[1])
+ if not output_dir.is_dir():
+ print(f"Output directory does not exist: {output_dir}")
+ return 1
+
+ return check_modules(
+ output_dir,
+ expect_any_regex=[HIER_BLOCK_REGEX],
+ expect_any_regex_after_port_list=[HIER_BLOCK_REGEX],
+ expect_any_without_regex=[HIER_BLOCK_REGEX],
+ )
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/test/compiler/cli/verilator_hier_blocks.k b/test/compiler/cli/verilator_hier_blocks.k
new file mode 100644
index 00000000..d43698c0
--- /dev/null
+++ b/test/compiler/cli/verilator_hier_blocks.k
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+// Minimal program used by the Verilator hier_block CLI test.
+class VerilatorHierBlock
+{
+public:
+ uint32 TimesTwo(uint32 x)
+ {
+ return x + x;
+ }
+}
+
+export VerilatorHierBlock;