AGENTS.md · git:20261015.98a7e7c · 2026-10-15 · sha256 3fa17b5fe998fa8c

AGENTS.md git:20261015.98a7e7cA

Immutable. This exact content never changes and is served at /api/v1/blob/3fa17b5fe998fa8c.

# 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.
- **Simulator backend**: `lhd sim` defaults to `sim.tune.backend=llvm` (native
  color objects). A test that is about the Slop backend itself (its C++ output,
  slop-only knobs) pins `--set sim.tune.backend=slop`; a general sim test that
  fails under llvm is an LLVM-backend bug to fix in `inou/cgen`, not a reason
  to switch it to slop.
- **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`; off by default for every command, including `lhd synth`/`lhd lec`) 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/`: native register-rooted XAG synthesis (`pass.usyn`, `synth.mapper=usyn`), including logical region reuse; independent of ABC except optional technology mapping through the ABC provider
- `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 off (`compile.upass.inline=false`): comb calls stay module
instances, and upass splices one only when it must (tuple-shaped results the
instance boundary cannot carry, generic/untyped callees); `pass.legalize`
handles combinational loops through an instance.

`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/`, `usyn_cache/` including its optional `tmap/` stage,
`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` + `synth_salt` (no ABC dependency) --
`//pass/opentimer:sta_salt` hashes `pass/opentimer` + `pass/partition` +
`MODULE.bazel`, `//lhd:formal_salt` hashes the prover (`pass/lec`, `pass/semdiff`,
the cvc5 pin) + the proof preparation whose output the verdict cache digests
(`pass/single_edge`, graph, cprop, `lhd/lhd_kernel_formal.cpp`), 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`.

### Defaults are a contract

Every user-visible default (a pass/`--set` option default, a compile recipe
default, a CLI flag default) is part of the contract. Do **not** change a
default unless the user **explicitly approves that specific change** in the
conversation. Never auto-approve, and never infer approval from a related
request, a test that "expects" another value, or a performance win. If a fix
seems to need a different default, keep the default, add an opt-in option,
and ask.

Example: `compile.upass.inline` defaults to `false` (comb calls stay module
instances, so hierarchy, LEC per-def checks and debug keep their boundaries).
It was once flipped to `true` without approval; that is exactly what this rule
forbids. (`pass.legalize` inlining a state-free instance on a combinational
false loop is a structural repair, not a default, and stays.)

## 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`.