Open and answer this issue on GitHub.
performance, decision · opened 2026-10-20 · updated 2026-11-03 · 1 comment
Lowering a file runs the checker’s walk over the same syntax tree twice, with different settings:
check_for_the_ledger, crates/nikaia/src/contracts/mod.rs) runs before the ledger is finished. Its consumers read only methods and stored_code. Since #727 it skips prove, views, view_changes and the tether check.sync::infer, throws::infer, keep inference, write_result_sources).project::check) walks again, against the finished ledger. Some of its rules read the finished columns, for example traits::check reads sync.The emitter then needs facts of that check (propagation, keep plans, bounds). buffer_plans runs twice, against two different ledgers.
Cost today (cold nikaia lower --no-cache crates/rs2nika/convert/src/main.nika, benches/lowering/lower.sh, 3 096 M Ir in all):
| Step | Ir |
|---|---|
check::walked, both walks |
1 111 M |
| of that, the ledger walk | 390 M |
infer_package_noted |
735 M |
keep::buffer_plans, twice |
217 M |
To get under a third of the cost, #727 added reuse shortcuts that apply only under a condition:
propagation_against asks: no modules, no changed contract, the promised ledger equal to program.contracts;std_ledger() alone;lower-std passes the facts on only when no described name is also a std name.Each of these is a quiet coupling between two places. Today the output is byte for byte the same. But a change to one side can make the other differ without any test noticing, and whoever adds a rule has to know which of the two walks sees which state of the ledger. Merging the two walks as they are would make this worse: checking would then run against a half-finished ledger.
Split lowering into phases with a clear contract. Each phase only reads what the phases before it wrote, and what it writes is frozen when it ends.
stored_code, the types of locals and expressions). It reads no inferred column.sync, throws, keeps and result sources are computed as fixpoints over the facts and the call graph, not by walking the tree again.buffer_plans and prove run once.What this gains for maintainability:
What it gains elsewhere:
Performance (estimated, not measured): the second walk goes, and so do one buffer_plans and the conditional reuses. Probably 15–25 % of today’s cost.
throws for ?, that part has to move into B or stay iterative. Step 1 checks this before anything is built.check (Rust and tools/check_*.nika).check::walked and its callees, record whether it is needed for typing (A) or only for a rule (C). Result: the list of reads that stand in A’s way. Decide here whether to go on.UnitFacts as the result of one walk. The ledger pass reads methods and stored_code from it, and the second walk reads its facts from it. Output byte for byte the same (lower.sh all), all tests green.UnitFacts and the finished ledger, and remove the second walk.buffer_plans and prove once, after the owner has answered the std-versus-described question.benches/lowering/lower.sh and record the result in #727.Owner, 2026-11-03: this is the target architecture: collect the facts first, then run every later phase on them. The timing is not decided yet.
Until then, a new design should not add hidden or swapped state that this split would have to undo. Context passes as data, as #1123 decided (D).
Still open before building: step 1’s inventory, and the question of whether buffers are planned against std alone or against std plus the described crates.
Generated by Claude Code