Imported from masc-ucsc/livehd (
AGENTS.md). Install upstream withnpx skills add masc-ucsc/livehd. Copyright stays with the author.
AGENTS.md
Doc map:
STRUCTURE.mddescribes where all documentation lives. In short — current/pending work is thetodo/hub (one HTML page per task); the human-readable LiveHD/Pyrope reference (the contract) is the docs site (../docs); directory specifics live in<dir>/README.md; and this file holds agent build/test/debug how-to plus the change-gated coding rules.
Build & Test
- Build:
bazel build -c dbg //... - Test:
bazel test //... - Test runtime: Keep each test under 20 seconds with
-c optand under 60 seconds with-c dbg. Simplify oversized fixtures and avoid repeated compilation; keep large design benchmarks in their own repositories. - Independent tests: LiveHD scripts and BUILD rules must not access sibling benchmark repositories. Test inputs must be provided by this repository or declared build dependencies.
- lhd CLI:
./bazel-bin/lhd/lhd— the only driver, for all flows (lhd help,lhd describe <cmd>);lhd pyrope lspserves the Pyrope LSP andlhd pyrope fmtformats Pyrope source. The oldlgshellREPL was removed (2026-06-04) - C++ formatted with
clang-format - Contract tests/benchmarks: Any test or benchmark file whose name contains the word
contractis immutable to the coding agent. Do NOT modify these files — they define the expected behavior contract. If a contract test fails, fix the implementation, not the test. - Test file naming: use the
_test.cppsuffix only when a matchingfoo.cppexists in the same or parent directory (foo_test.cpptestsfoo.cpp); it is a suffix, never a prefix. Standalone checks with no single source counterpart must not carry_test(use e.g.*_smoke.cpp).
Sibling Repositories (DO NOT search the filesystem for these)
LiveHD depends on several sibling repos. Always look in these exact paths — do NOT run find, fd, locate, or any other filesystem search to find them or files inside them:
../hhds/— HHDS library (Graph, Tree, Forest, flat_storage attributes). Headers:hhds/graph.hpp,hhds/tree.hpp,hhds/attr.hpp.../hlop/— HLOP library (dlop / slop helpers).../iassert/— iassert library (I(...),GI(...)invariant macros).../tree-sitter-pyrope/— Tree-sitter grammar for Pyrope. The grammar lives at../tree-sitter-pyrope/grammar.js; generated parser sources are under../tree-sitter-pyrope/src/.
Rule: if you need anything from one of these libraries (e.g. grammar.js, tree.hpp, graph.hpp, attr.hpp), open it directly at the path above. Do not start a find / ..., find . ..., or recursive grep to locate them — they are always at these fixed sibling paths. The bazel build fetches them as git-pinned plain repos (use_repo_rule git_repository in MODULE.bazel, NOT bazel_dep + git_override, so livehd stays self-contained when consumed as a bazel dependency); each has a commented local_repository swap for co-development against the sibling checkout. Running a broad find is wasteful and frequently the wrong tool; go straight to the known path.
Key Directories
graph/: LGraph IR (HHDS-backed) — LiveHD cell semantics + per-pin attributes + graph library overhhds::Graph(nodes, pins, edges). Seegraph/README.md. (This is the oldlgraph/, now migrated onto HHDS.)lnast/: LNAST high-level IR (HHDS-backedhhds::Tree+ flat-storage attributes)parser/: AST built onhhds::Treeinou/yosys/: Yosys integration (lgyosys_tolg.cpp= Yosys→LGraph,inou_yosys_read.ys= Yosys script)inou/cgen/: Verilog code generation from LGraphpass/cprop/: Constant propagation passpass/synth/: the shared, ABC-free synthesis pipeline (private copy, ware/memory modules, region driver,Lnettranslation, region cache, read-back); seepass/synth/README.mdpass/satopt/:pass.satopt, bounded proof-backed simplification with selectable stages, run only by the compile step (--set pass.satopt=true; on by default forlhd synth/lhd leccompiling a source) andlhd pass satopt; seepass/satopt/README.mdpass/abc/: the ABC backend andpass.abc— the only synthesis code that includes or calls ABCpass/usyn/: unate synthesis (pass.usyn,synth.mapper=usyn): a domino LUT cover as a region hook, mapped by the ABC backendware/rtl/: Memory RTL modules (cgen_memory_*.v,cgen_memory_multiclock_*.v)
Tree library
LiveHD's tree IRs (LNAST, parser AST) sit on top of HHDS (@hhds//hhds:core,
headers hhds/tree.hpp, hhds/attr.hpp). New tree code should use
hhds::Tree + hhds::Forest and attach per-node payload via flat_storage
attribute tags — see lnast/lnast_attrs.hpp for the pattern. Pass-local
state should live in absl::flat_hash_map<Tree_class_index, T> side maps
rather than registering throwaway attributes. Legacy core/lhtree.hpp is
gone; do not reintroduce lh::tree / lh::Tree_index.
lhd CLI (EPRP underneath)
One stateless invocation per flow; pass flags ride --set pass.flag=value
(or a --config lhd.toml), outputs are typed --emit/--emit-dir slots,
per-step logs land under --workdir. Examples:
lhd compile foo.v --top foo --emit verilog:out.v
lhd compile foo.prp --emit-dir lg:foo_lgs/ --emit-dir lnast-dump:dumps/
lhd lec --impl verilog:out.v --ref verilog:foo.v --top foo
lhd synth foo.prp --top foo --workdir W --stats # compile -> color synth -> abc -> opentimer, one shot
Graph compilation always runs constant propagation followed by bitwidth
inference. There is no optimization-level or --recipe selection. Comb
inlining defaults on; use --set compile.upass.inline=false when preserving
comb module boundaries is part of the flow.
lhd synth is the fused synthesis flow (reports in W/synth/; --emit-dir lg: / verilog: for the mapped netlist, report: for the sidecars); the
individual lhd pass color|abc|opentimer steps remain for any other
coloring or for inspecting intermediates. Every persistent reuse tier (the
compile cache, abc_cache/, sta_cache/, the formal verdict cache) lives under a
user-named --workdir and follows the ONE switch --set lhd.incremental=true|false (default true) — there is no per-tier cache flag.
Internally lhd drives the registered EPRP methods (conceptually the pipe
inou.yosys.tolg |> pass.cprop |> inou.cgen.verilog); pass/inou names in
--set and the step logs use that vocabulary.
Measuring reuse after a rebuild — read this before believing a cache miss.
Each tier is salted by a build-time content hash of the code that produces what
it stores, so the first run after you touch that code is a full miss, by
design: the synthesis salts are layered -- //pass/synth:synth_salt hashes
pass/synth + the passes a mapped region depends on (graph, memory RTL,
cprop, enableopt, bitwidth, color, the DFF pick, formal, pass/partition),
//pass/abc:abc_salt hashes pass/abc + synth_salt + MODULE.bazel + the
ABC patch, and //pass/usyn:usyn_salt hashes pass/usyn + abc_salt --
//pass/opentimer:sta_salt hashes pass/opentimer + pass/partition +
MODULE.bazel, //lhd:formal_salt and //lhd:compile_salt likewise. Even a clang-format -i counts. Always run the warm command twice
after a rebuild and read the second number, and never rebuild in the middle of a
measurement sweep — a cold pass stored under salt A and a warm pass loaded under
salt B looks exactly like "the cache forgot everything".
Compiler Warnings Policy
Always fix source code — never add -Wno-* flags to BUILD files. Exception: external deps in MODULE.bazel.
Contracts
Git branches
Do not create a git branch unless the user explicitly asks for one. Work
on the current branch by default; only run git checkout -b / git switch -c
(or otherwise create a branch) when the user clearly indicates to do so.
Compiler warning options
Unless the user explicitly indicates otherwise, do not change compiler warning options to make warnings or errors go away. Always fix the source code instead.
This includes (non-exhaustive):
- Adding or modifying
-W*,-Wno-*,-Werror,-pedantic, or-wflags inBUILD,BUILD.bazel,*.bzl, or any other build configuration. - Removing warning flags from
tools/copt_default.bzlor a target'scopts. - Disabling diagnostics via
#pragma GCC diagnostic/#pragma clang diagnosticpushes around live code.
The only built-in exception is MODULE.bazel (external-dep warning
suppression — see "Compiler Warnings Policy" above).
Enforced by scripts/contracts/diff_no_compile_flags_touched.sh.
Running Pyrope Tests
- Single test (harness):
python3 inou/prp/tests/pyrope_test.py -i inou/prp/tests/<dir>/<test>.prp - Direct pipeline (comptime):
./bazel-bin/lhd/lhd compile <test>.prp --set upass.verifier=true --set upass.verifier_pass=1 --set upass.verifier_fail=0 --emit-dir lnast-dump:dumps/ --workdir w(the post-upass LNAST text lands indumps/*.lnast; uPass stdout diagnostics inw/logs/*pass_upass*.log) - Test header
:type:selects the pipeline (parsing,lnast,upass,comptime,lgraph,compile);:verifier_pass:/:verifier_fail:set expected cassert tallies forcomptime. - Expected-failure tests (
:type: error, ininou/prp/tests/errors/): the program must emit a compile error (a diagnostic — see the Diagnostics section of the LiveHD docs). The header's:error:and:help:values are matched (Pythonre.search, with a literal-substring fallback when the value is not a valid regex — so')'works) against the emitted diagnostic'smessageandhint. A compile error exits non-zero cleanly (no abort) in every build mode. To pin the line, put alocate_error_herecomment on the expected-error line — the harness checks the diagnostic'sstart_linematches it (a marker survives inserting/removing lines above, unlike a hard-coded number; the tokenlocate_error_hereis reserved — don't use it in prose, and only use it when the diagnostic actually carries a span — manyupasserrors don't yet). Diagnostics are read from the JSONL file declared vialhd --emit diagnostics:PATH(the hermetic kernel ignores the oldLIVEHD_DIAGenv). Eachtests/errors/*.prpauto-generates aprp-<name>bazel testtarget. Example:/* :name: unbalance :type: error :error: ')' :help: unbalance */ comb foo( -> (z) { z = b#[1,4] } // locate_error_here (missing ')') - Equivalence tests (
inou/prp/tests/equiv/): two axes, and a design can sit on both.foo.prp+ a hand-writtenfoo.vis the Pyrope↔Verilog pair (:type: equiv, targetprp-equiv-foo).foo.prp+foo_1.prp,foo_2.prp, … is a Pyrope↔Pyrope group: every numbered sibling is LEC'd against the base byinou/prp/tests/prplec.py(targetprp-lec-foo_1), wired by the file NAME alone — a new claim is one new file, no script and no BUILD edit. A HEADER-ONLY variant means "the base source with MY:set:flags" (that is how rolled-vs-unrolled is written without duplicating the design). Per-variant header tags:lec_expect: proven|refuted,:lec_sweep:,:lec_grep:/:lec_grep_not:; run one by hand with./inou/prp/tests/prplec.py inou/prp/tests/equiv/foo.prp. Seeinou/prp/tests/equiv/README.md. - Expected-warning tests (
:type: warning, ininou/prp/tests/warnings/): the lint counterpart oftests/errors/. The program must compile cleanly (exit 0, noerrordiagnostic) yet emit at least one warning diagnostic. The header's:warning:and:help:values are matched (samere.search+ literal fallback) against the warning'smessage/hint; alocate_warning_herecomment pins the warning'sstart_line. Eachtests/warnings/*.prpauto-generates aprp-warn-<name>bazel testtarget. Seeinou/prp/tests/warnings/README.md. Example: a pure expression used as a statement (a + 1) triggersunused-expression.
Debugging Yosys-to-LGraph Flow
Running Yosys tests
- Single test:
./inou/yosys/tests/yosys_compile.sh ./inou/yosys/tests/<test>.v(orbazel test //inou/yosys:yosys_compile-<test>) - Full suite:
./inou/yosys/tests/yosys_compile.sh - The script drives
lhd compile/lhd lec; each test gets a fresh scratch--workdir(no sharedlgdbstate to clean — the kernel is stateless). Per-step logs (yosys chatter included) are undertmp_yosys/<top>/logs/.
Inspecting intermediates
- Yosys RTLIL dump: After running tolg, check
pp.ilfor what Yosys produced (cell types, port connections, parameters). - LGraph dump:
./bazel-bin/lhd/lhd compile <file> --reader yosys-verilog --top <top> --emit-dir verilog:out/ --workdir wthen read the per-step logs inw/logs/and grep the cgen output inout/*.vfor cell types (e.g.,grep -i mem). (The REPL-onlylgraph.dumptext dump has no lhd emit yet.) - Generated Verilog: Check the
--emit-dir verilog:per-module output;tmp_yosys_mix/all_<top>.vis the concatenated file used by LEC inyosys_compile.sh.
Yosys memory pass
inou/yosys/inou_yosys_read.yscontrols the Yosys script. Thememory -nomappass collects$memrd/$memwr/$meminitinto$mem_v2cells without decomposing into DFFs. Using plainmemorydecomposes small memories into DFFs+muxes, bypassing LGraph memory handling.
Memory cell types
- Modern Yosys produces
$mem_v2(not$mem). Match both:cell->type == "$mem" || cell->type == "$mem_v2". - Do NOT use
strncmp("$mem", 4)— it catches$memrd/$memwr/$meminitwhich have different port structures. - Memory RTL modules are in
ware/rtl/cgen_memory_*.v. Themulticlockvariants have per-port clock inputs instead of a sharedclk.
