AGENTS.md · diff
git:20260924.58dbfdc to git:20260926.472579c
5 added, 0 removed. Audit A to A.
# AGENTS.md
> **Doc map:** [`STRUCTURE.md`](STRUCTURE.md) describes where all documentation
> lives. In short — current/pending work is the [`todo/`](todo/index.html) hub
> (one HTML page per task); the human-readable LiveHD/Pyrope reference (the
> contract) is the [docs site](https://masc-ucsc.github.io/docs/) (`../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 opt` and 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.
+ - **External simulators**: the default suite must not need iverilog, vvp,
+ verilator or ninja. Optional independent event-simulator legs are skipped
+ unless `LHD_EXTERNAL_SIM` is set:
+ `bazel test --test_env=LHD_EXTERNAL_SIM=1 --test_env=PATH=<dir-with-tools>:$PATH <target>`.
+ A requested leg whose tool is missing FAILS rather than skipping.
- **lhd CLI**: `./bazel-bin/lhd/lhd` — the only driver, for all flows
(`lhd help`, `lhd describe <cmd>`); `lhd pyrope lsp` serves the Pyrope LSP
and `lhd pyrope fmt` formats Pyrope source. The old `lgshell` REPL was
**removed** (2026-06-04)
- C++ formatted with `clang-format`
- **Contract tests/benchmarks**: Any test or benchmark file whose name contains the word `contract` is 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.cpp` suffix only when a matching `foo.cpp` exists in the same or parent directory (`foo_test.cpp` tests `foo.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 over `hhds::Graph` (nodes, pins, edges). See `graph/README.md`. (This is the old `lgraph/`, now migrated onto HHDS.)
- `lnast/`: LNAST high-level IR (HHDS-backed `hhds::Tree` + flat-storage attributes)
- `parser/`: AST built on `hhds::Tree`
- `inou/yosys/`: Yosys integration (`lgyosys_tolg.cpp` = Yosys→LGraph, `inou_yosys_read.ys` = Yosys script)
- `inou/cgen/`: Verilog code generation from LGraph
- `pass/cprop/`: Constant propagation pass
- `pass/synth/`: the shared, ABC-free synthesis pipeline (private copy, ware/memory modules, region driver, `Lnet` translation, region cache, read-back); see `pass/synth/README.md`
- `pass/satopt/`: `pass.satopt`, bounded proof-backed simplification with selectable stages, run only by the compile step (`--set pass.satopt=true`; on by default for `lhd synth`/`lhd lec` compiling a source) and `lhd pass satopt`; see `pass/satopt/README.md`
- `pass/abc/`: the ABC backend and `pass.abc` — the only synthesis code that includes or calls ABC
- `pass/usyn/`: unate synthesis (`pass.usyn`, `synth.mapper=usyn`): a domino LUT cover as a region hook, mapped by the ABC backend
- `ware/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 `-w` flags
in `BUILD`, `BUILD.bazel`, `*.bzl`, or any other build configuration.
- Removing warning flags from `tools/copt_default.bzl` or a target's `copts`.
- Disabling diagnostics via `#pragma GCC diagnostic` / `#pragma clang diagnostic`
pushes 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 in `dumps/*.lnast`; uPass stdout diagnostics in `w/logs/*pass_upass*.log`)
- Test header `:type:` selects the pipeline (`parsing`, `lnast`, `upass`, `comptime`, `lgraph`, `compile`); `:verifier_pass:` / `:verifier_fail:` set expected cassert tallies for `comptime`.
- **Expected-failure tests** (`:type: error`, in `inou/prp/tests/errors/`): the program **must** emit a compile error (a diagnostic — see the *Diagnostics* section of the [LiveHD docs](https://masc-ucsc.github.io/docs/livehd/02-usage/)). The header's `:error:` and `:help:` values are matched (Python `re.search`, with a literal-substring fallback when the value is not a valid regex — so `')'` works) against the emitted diagnostic's `message` and `hint`. A compile error exits non-zero cleanly (no abort) in every build mode. To pin the **line**, put a `locate_error_here` comment on the expected-error line — the harness checks the diagnostic's `start_line` matches it (a marker survives inserting/removing lines above, unlike a hard-coded number; the token `locate_error_here` is reserved — don't use it in prose, and only use it when the diagnostic actually carries a span — many `upass` errors don't yet). Diagnostics are read from the JSONL file declared via `lhd --emit diagnostics:PATH` (the hermetic kernel ignores the old `LIVEHD_DIAG` env). Each `tests/errors/*.prp` auto-generates a `prp-<name>` `bazel test` target. 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-written `foo.v` is the Pyrope↔Verilog pair (`:type: equiv`, target `prp-equiv-foo`). `foo.prp` + `foo_1.prp`, `foo_2.prp`, … is a Pyrope↔**Pyrope** group: every numbered sibling is LEC'd against the base by `inou/prp/tests/prplec.py` (target `prp-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`. See `inou/prp/tests/equiv/README.md`.
- **Expected-warning tests** (`:type: warning`, in `inou/prp/tests/warnings/`): the lint counterpart of `tests/errors/`. The program **must compile cleanly** (exit 0, no `error` diagnostic) yet emit at least one **warning** diagnostic. The header's `:warning:` and `:help:` values are matched (same `re.search` + literal fallback) against the warning's `message` / `hint`; a `locate_warning_here` comment pins the warning's `start_line`. Each `tests/warnings/*.prp` auto-generates a `prp-warn-<name>` `bazel test` target. See `inou/prp/tests/warnings/README.md`. Example: a pure expression used as a statement (`a + 1`) triggers `unused-expression`.
## Debugging Yosys-to-LGraph Flow
### Running Yosys tests
- **Single test**: `./inou/yosys/tests/yosys_compile.sh ./inou/yosys/tests/<test>.v` (or `bazel 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 shared `lgdb` state to clean — the kernel is stateless).
Per-step logs (yosys chatter included) are under `tmp_yosys/<top>/logs/`.
### Inspecting intermediates
- **Yosys RTLIL dump**: After running tolg, check `pp.il` for 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 w` then read the per-step logs in `w/logs/` and grep the cgen output in `out/*.v` for cell types (e.g., `grep -i mem`). (The REPL-only `lgraph.dump` text dump has no lhd emit yet.)
- **Generated Verilog**: Check the `--emit-dir verilog:` per-module output; `tmp_yosys_mix/all_<top>.v` is the concatenated file used by LEC in `yosys_compile.sh`.
### Yosys memory pass
- `inou/yosys/inou_yosys_read.ys` controls the Yosys script. The `memory -nomap` pass collects `$memrd`/`$memwr`/`$meminit` into `$mem_v2` cells without decomposing into DFFs. Using plain `memory` decomposes 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`/`$meminit` which have different port structures.
- Memory RTL modules are in `ware/rtl/cgen_memory_*.v`. The `multiclock` variants have per-port clock inputs instead of a shared `clk`.