Skip to content

Search equivalent NOR netlists with an e-graph - #61

Merged
rollrat merged 17 commits into
claude/rsdsl-v2from
claude/egraph-netlist
Oct 9, 2026
Merged

rollrat merged 17 commits into
claude/rsdsl-v2from
claude/egraph-netlist

Conversation

@rollrat

@rollrat rollrat commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Stacked on #59.

Why

The exact placer builds whatever NOR netlist it is given. A hand-written netlist (the full adder's nor9) therefore fixes the torch count and the logic depth before placement starts. This PR searches the equivalent netlists with an e-graph and measures whether placement gains from them.

What

  • Fix (construction): a step that placed a new input sometimes failed as infeasible within 0.1 s for every window, then restarted. The new input was offered the cell of a switch kept from an earlier step, and dsl.rs then read that fixed switch as the new input's. New inputs now skip placed switches, and a fixed switch belongs to the input whose only site it is. EXACT_DIAGNOSE_INFEASIBLE re-solves an infeasible step with one requirement dropped at a time; that is how this was found.
  • E-graph search (exact/egraph.rs, egg 0.9):
    • Rewrites: De Morgan, distribution and factoring, XOR expansions, and the like.
    • Classes with the same truth table merge as soon as the second appears.
    • The full adder reaches 230 of the 256 three-input functions in 73 ms.
  • Exact extraction (exact/egraph_extract.rsdsl):
    • Grounded with rsdsl and solved with the placer's CaDiCaL binding.
    • Picks one node per needed class: NOT is a torch, OR is free, outputs are torches.
    • Options: a depth bound, a cost per OR node, and two-input NORs only.
    • Each result is proven optimal for the e-graph in 0.1-10 s.
  • OR nets: NetDriver::Or and ExtractOptions::split_wide build a wide NOR from OR nets placed as steps of their own. Netlists from a logic graph never contain OR nets, and the CNF hashes are unchanged.
  • Support block (chain_wide_gates, solid_observations): a wide NOR is built in stages: its first OR sits on a block on the step's last slice, and later stages feed that block.
  • Exact synthesis (exact/synthesis.rs, exact/nor_synthesis.rsdsl): builds a NOR netlist and its construction order together. Each step is a NOR or an OR stage of a support-block chain. The live nets between steps are a constraint, so everything it returns can be placed. SynthesisOutcome separates a proven infeasible result from a time-out.
  • NorNetlist::to_text/from_text and a CIRCUIT=netlist harness mode, which places any netlist given as text.
  • GateOrder::MinLive: places the ready gate after which the fewest nets stay alive.

Findings (full adder, docs/egraph_netlists.md)

Netlist Gates Depth cout / s Max fan-in
nor9 9 5 / 6 2
fewest gates 8 3 / 4 4
depth ≤ 3 9 2 / 3 4
or_cost 1 9 4 / 5 3
two-input NORs only 9: nor9, proven optimal 5 / 6 2
  • Every gain over nor9 uses NORs of 3-4 signals. Construction cannot place those in a window two cells wide: the steps time out, and the layouts take many restarts or none.
  • Splitting wide NORs into OR nets did not help. The OR nets are signals too, so the cross-section stays as wide (6 nets against nor9's 4, whatever the order). And an OR carried on to later steps needs a block and a repeater, since merged dust powers both of its lines back.
  • Split, the 8-gate netlist placed once at 327 blocks and 14/25 ticks. nor9 placed at about 147 blocks and 13-17/17-18 ticks.
  • Building a wide NOR in stages on one support block (EGRAPH_CHAIN) gets further, but steps with 6-7 nets alive still time out, with 60 s or 240 s per step.
  • A taller box does not help: at heights 12 and 14, with windows up to 6 slices, the same steps stall on every seed. nor9 builds in the same settings in 1.5-2.5 minutes. The same g4 that fails as one 3-input NOR builds in two short steps when chained, so the obstacle is merging a new input and three signals in one step.
  • No timeout was proven infeasible. EXACT_DIAGNOSE_UNKNOWN re-solves a timed-out step with one requirement dropped at a time. Dropping the new gate's own output solves it in 1-5 s, and dropping any other net mostly does not. The hard part is getting the new torch's output out past inputs that branch on to later gates.
  • Synthesis within the bounds proves the logic limits of a buildable full adder: 9 torches at the least (up to 14 steps, even with 6 live nets), and depth 4/5 at best for 9 torches, against nor9's 5/6. That netlist is the chained or_cost 1 one, found again from scratch. Placement runs showed that 600 s compaction varies more by seed (nor9 at height 12: 90-237 blocks) than by netlist (docs/nor_synthesis.md).
  • Other circuits, all buildable (at most 4 live nets):
    • half adder 5 torches and 2:1 mux 4, both proven, against 7 gates each from expressions;
    • 4:1 mux 8 torches, against 20 gates that never built. It now builds and compacts to 142-168 blocks;
    • 2-bit adder 14 torches, against the hand-written 15. It compacts to 122/134 blocks, against 160/163 with the same settings, and is faster.
  • Both are fixtures (test/adder2-synth-exact-2x16x8.rcell, test/mux4-synth-exact-2x16x10.rcell). Their tests check every case and every settled transition.
  • With 1800 s of compaction, nor9 and the chained or_cost 1 full adder end up close (84 against 80 blocks on average over three seeds).
  • Screening rule: at most 4 live nets and at most 2 continuing inputs per step (nor9 has 4 and 2). The or_cost 1 netlist chained on a support block meets it. As predicted, it built on the first try on both seeds (110-119 s) and compacted to 115-116 blocks, against nor9's 146-148 in the same 2x10 box.

Test plan

  • cargo test --release --lib: 372 passed; the one failure (world_to_logic_extracts_d_latch_nbt_as_sequential_node) fails on master too.
  • print_cnf_hashes: unchanged for netlists from logic graphs.
  • Placements via diagnose_construct_circuit (CIRCUIT=egraph-full-adder, EGRAPH_* knobs): every cell that was placed passes its RCELL check.

🤖 Generated with Claude Code

rollrat and others added 7 commits October 5, 2026 15:38
Exact placer on an rsdsl v2 model: optimization, carry tiles, timing, dust physics
Construction steps that place a new input sometimes failed as infeasible
within 0.1 s for every window, then restarted with a new seed (three of
five seeds of one full-adder netlist, and earlier the carry tile's carry
entry). The step diagnosis (EXACT_DIAGNOSE_INFEASIBLE) showed the culprit:
freeing an earlier input's kept switch made the step solvable.

The new input's candidate sites covered the whole window, including the
overlap slice where a switch placed in an earlier step is kept as a fixed
cell. dsl.rs gave a fixed switch to the first input with a site there, so
the kept switch became the new input's: the earlier input lost its only
site and the frozen gates it drove contradicted their given signals.

New inputs no longer get sites where a switch is already placed, and a
fixed switch belongs to the input whose only site it is when several
inputs could claim it (an error if that is still ambiguous). The same
seed now places that step in 9 s. The diagnosis hook stays: with
EXACT_DIAGNOSE_INFEASIBLE, an infeasible step is re-solved with given
signals, each observation, each single-site input, and frozen air dropped
in turn, and the placed inputs' surroundings are logged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The exact placer builds whatever NOR netlist it is handed, so a
hand-written netlist such as the full adder's nor9 fixes its torch count
and logic depth before placement starts. egraph.rs explores the
equivalent netlists with egg and extracts ones worth placing.

Saturation runs De Morgan, double negation, commutativity and
associativity, distribution and factoring both ways, absorption, and XOR
and XNOR expansions. Each e-class carries its truth table, and classes
with equal tables are unioned in modify, so different forms of one
function always meet. The full adder reaches 230 of the 256 three-input
functions in 73 ms.

Greedy extraction decides class by class and missed the shared NORs (13
gates against nor9's 9), so extraction is exact: egraph_extract.rsdsl,
grounded with rsdsl and solved with the placer's CaDiCaL binding, picks
one node per needed class (outputs are torches, a NOT adds a tick, an OR
none) and minimizes gates, plus or_cost per OR node since every NOR input
is a signal to route, within an optional depth bound. Each extraction is
proven optimal for its e-graph in 0.1-10 s. On the full adder:
- fewest gates: 8 (nor9 has 9), depth 3/4, but NORs of up to 4 inputs;
- depth 3: 9 gates, depth 2/3;
- or_cost 1: 9 gates, depth 4/5 against nor9's 5/6, fan-in 3, and the
  same 4 nets alive at once as nor9.

The circuit harness places them (CIRCUIT=egraph-full-adder with
EGRAPH_DEPTH and EGRAPH_OR_COST); explore_egraph_netlists sweeps depth
bounds and OR costs. ExactLocalPlacer::from_netlist takes a netlist built
elsewhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The e-graph finds full adders with fewer gates (8) or less depth than
nor9, so the question was whether placement gains from them. Placed with
the same settings, the wide NORs they rely on are what construction
cannot handle: steps with a 3- or 4-input NOR ran out of their 60 s
repeatedly. The 8-gate and depth-3 netlists found no layout in 3 and 5
restarts; the or_cost 1 netlist (9 gates, depths 4/5, fan-in 3) took
15-16 minutes to construct against nor9's 1-1.5, and then compacted to
the best cell of the runs on one seed (88 blocks, 11 ticks) and a poor
one on the other.

A `binary` extraction option allows only two-input NORs (an OR may not
join classes built as ORs). Under it the minimum is nor9 itself, 9 gates
at depth 5/6, proven optimal for the e-graph, so every gain over nor9
needs wider NORs and therefore a construction that can build them.
docs/egraph_netlists.md records the method, the extractions, the
placements, and that as the next step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The e-graph's smaller and shallower full adders rely on NORs of 3 and 4
signals, and construction kept running out of time on those steps: every
input has to reach the torch's block within one window two cells wide.
Splitting such a NOR lets construction merge its inputs first and place
the torch on a two-signal OR later.

Netlists gain NetDriver::Or: a net that is the OR of its inputs, built
from dust and blocks without a torch. Netlists from a logic graph never
contain one (their ORs fold into gates as before, and the CNF hashes are
unchanged). Construction orders OR nets as steps like gates, step timing
gives them their latest input's arrival, RCELL expressions write them as
(a|b), and netlist depths count them free.

ExtractOptions::split_wide builds every NOR of more than two signals from
the two halves of its OR, each an OR net when wider than one signal, with
OR nets shared between gates through their e-class. The 8-gate full adder
becomes 8 two-input NORs and 5 OR nets. Splitting does not cut the nets
alive at once (6 against nor9's 4, the OR nets being signals too), so
placement will tell whether the narrower steps pay for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every live net crosses a construction step's last slice, and the
e-graph's netlists keep more nets alive than nor9 (6 against 4), so the
order construction places gates in might narrow them. GateOrder::MinLive
picks, among the ready gates, the one after which the fewest nets are
still needed, ties going to the smaller cone (CIRCUIT_ORDER=min-live).

It does not help these netlists: nor9 and the OR-cost netlist stay at 4,
and the 8-gate netlist split into two-input NORs stays at 6, which is a
property of the netlist and not of the order. The order is kept as an
option and as part of the topological-order test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Splitting was meant to let construction build the e-graph's wide NORs in
narrow steps. Placed, it made the 8-gate full adder placeable once (2x23x10,
327 blocks, 14/25 ticks after compaction, against nor9's ~147 blocks and
13-17/17-18) and the OR-cost netlist about equal to nor9 (138 blocks,
13/18), still with several restarts.

The doc records why: the cross-section stays as wide (OR nets are signals
too, and no gate order narrows it), and an OR is only free on the block a
torch reads. Carried on as a net, dust would power both merged lines back,
so it needs a block and a repeater. The next step it names is building a
wide NOR around a reserved support block instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rollrat rollrat changed the title Search equivalent NOR netlists with an e-graph Build wide NORs from OR nets, and order gates by live nets Oct 5, 2026
@rollrat
rollrat changed the base branch from master to claude/egraph-search October 5, 2026 08:26
@rollrat
rollrat added this pull request to stack #64 October 5, 2026 08:26
@rollrat
rollrat removed this pull request from stack #64 October 5, 2026 08:30
@rollrat
rollrat deleted the branch claude/rsdsl-v2 October 5, 2026 08:30
@rollrat rollrat closed this Oct 5, 2026
@rollrat rollrat reopened this Oct 5, 2026
@rollrat rollrat changed the title Build wide NORs from OR nets, and order gates by live nets Search equivalent NOR netlists with an e-graph Oct 5, 2026
@rollrat
rollrat changed the base branch from claude/egraph-search to claude/rsdsl-v2 October 5, 2026 08:31
@rollrat
rollrat added this pull request to stack #58 October 5, 2026 08:31
rollrat and others added 10 commits October 5, 2026 17:52
Splitting a wide NOR into OR nets did not help, because an OR carried to
later steps is not free: merged dust powers both of its lines back, so it
needs a block and a repeater. The OR is only free on the block the torch
reads. This builds a wide NOR around that block instead, one more signal
per step, so the OR never has to travel.

NorNetlist::chain_wide_gates turns every NOR of more than max_fan_in
signals into a left-leaning chain of OR nets, inputs first and the rest in
topological order: NOR(x1, x2, x3, x4) becomes s1 = OR(x1, x2),
s2 = OR(s1, x3), NOR(s2, x4). Construction treats an OR net read by exactly
one net that reads no other OR net (chained_ors) as a stage of a support
block:
- its first OR is observed on a block of the last slice
  (ExactPlacerConfig::solid_observations, the mechanism the carry out
  uses), and the block's position is kept in the step state;
- every later stage, and the NOR that ends the chain, fixes that block,
  keeps its slice open (the frozen boundary stops there), and observes the
  next OR on the same block, so the new signal has to reach it;
- gate orders move each chain to just before the NOR it ends in, so its
  stages are consecutive steps.

Nothing changes for netlists without OR nets (the CNF hashes are the
same). The circuit harness takes EGRAPH_CHAIN=<max fan-in>; a test checks
that chaining keeps every function and that orders keep chains together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The support-block construction does what it was built for: a chain's OR
lands on a block and the next stage reaches the same block, each such step
in 9-28 s. It did not make the e-graph's netlists constructible. With two
seeds of the 8-gate netlist and one of the depth-3 netlist, 35-50% of the
step attempts ran out of their 60 s, at a different step each attempt, and
none was proven infeasible. A 3-wide box made every step slower, nor9
included.

The doc records the result and what it points to: these netlists keep 6-7
nets alive where nor9 keeps 4, and windowed construction pays for every
crossing net in every step. Next steps: longer steps to see whether they
place at all, and crossing width as an extraction cost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every failed step of the e-graph netlists ran out of its 60 s, none was
proven infeasible, so the next question is whether they place at all
with more time. Longer steps alone would not show it: an attempt still
restarted after the default 600 s. CIRCUIT_RESTART_SECONDS sets that
limit (default 600, as before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The last open question was whether the e-graph's wide-NOR full adders
place at all with more time. With 240 s per step and an hour per attempt
they do not: the flat 8-gate netlist stuck at its first 4-input NOR after
three 240 s windows, and the chained 8-gate and depth-3 netlists stuck at
cout, with chaining getting further than flat.

The doc's conclusion: under two-input NORs nor9 is the proven minimum,
and every better netlist needs wider NORs and more nets alive at once,
which windowed construction cannot place however the NORs are split or
staged. The search and exact extraction remain useful to prove netlists
optimal and to look for better two-input netlists of other circuits,
which is the next step it names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 8-gate and depth-3 full-adder netlists from the e-graph never
constructed in a 2x10 box. Their steps only ever timed out, so it was
open whether the box was simply too small. This raises the height to
12 and 14 and lets windows grow to 6 slices (new CIRCUIT_MAX_WINDOW
knob) to answer that.

They still stall at the same steps on every seed, while nor9 builds in
the same settings in 1.5-2.5 minutes. The same g4 that fails as one
3-input NOR builds in two short steps when chained, with the same nets
alive afterwards. So the obstacle is merging a new input and three
signals in one step, not the cross-section. Whether a layout exists at
all is still open and needs a solve without windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The e-graph's full-adder netlists with wide NORs never constructed, even
in taller boxes, and no step was ever proven infeasible. To see what
makes a step hard, EXACT_DIAGNOSE_UNKNOWN re-solves the first timed-out
step with one requirement dropped at a time.

Dropping the new gate's own output from the window's last slice solves
the step in 1-5 s. Dropping any other net mostly does not. The gate is
easy; getting its output out past inputs that branch to later gates is
not. The steps are hard rather than impossible: the same flat g4 step
was placed in 54 s in one run and stayed undecided for 30 min in
another.

The harness now prints, per step, how many of the gate's inputs are
used again later. With the peak live-net count, it separates the
netlists that build from those that do not: nor9 has 4 and 2. The
or_cost 1 netlist chained on a support block has the same numbers. It
was predicted to build easily and did, first try on both seeds, and
compacted to 115-116 blocks against nor9's 146-148 in the same box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Explaining why a step such as g5 fails needed the netlist in the order
construction places it: which gate each step builds, what it computes,
which of its inputs later steps need again, and what stays alive. The
harness only printed the gates by net index and the counts. It now
prints one entry per step with all four.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Construction only places netlists that keep few nets alive between steps
and few continuing inputs per gate. The e-graph's cheaper full adders
break both, and listing its netlists one by one to screen them was
impractical: ORs nest and pad with redundant inputs, so a minute found 8.

nor_synthesis.rsdsl builds the netlist and its construction order
together, with those counts as constraints. Each step is a NOR or an OR
stage of a support-block chain, reads at most two nets, and wastes
nothing. The model minimizes torches per step count.

Findings for the full adder, each proven per step count:
- No buildable full adder of up to 14 steps has fewer than 9 torches,
  even with 6 nets alive.
- At 9 torches the best depth is 4/5 (cout/s). It needs 11 steps, and
  the netlist is the e-graph's or_cost 1 one chained, found again from
  scratch.
- Depth 4 for s is impossible within 14 steps, and 3 live nets within 13.

Every synthesized netlist built, all steps in window 2. Placement runs
also showed that 600 s compaction varies more by seed (nor9 at height
12: 90-237 blocks) than by netlist, so blocks need the full pipeline and
more seeds to rank netlists.

Also: NorNetlist::to_text/from_text with a CIRCUIT=netlist harness mode,
and a refactor of e-graph extraction into GroundedExtraction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Synthesis proved what a buildable full adder needs. This applies it to
the harness's other circuits, settles the full adder's block comparison
with longer compaction, and checks the proofs' reach.

- Other circuits (synthesize_buildable_circuits, SYNTH_CIRCUIT):
  - half adder 5 torches and 2:1 mux 4, both proven, against 7 gates
    each from expressions;
  - 4:1 mux 8 torches, against 20 gates and 6 live nets. That netlist
    never built. The synthesized one built in all three runs and
    compacted to 142-168 blocks, RCELL-verified;
  - 2-bit adder 14 torches, against the hand-written 15, and shallower
    (not proven).
- The full adder's 9-torch floor holds to 16 steps at 4 live nets. With
  7 live nets the model finds 8 torches, so the bound excludes them, not
  a modeling error.
- With 1800 s of compaction (3 seeds each), nor9 averages 84 blocks and
  the chained or_cost 1 netlist 80, best 72. The 30-block gap at 600 s
  was mostly unfinished compaction.
- The doc no longer says the harness ran without step and repair
  minimization; both are on by default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Synthesis should give smaller cells than hand-written or decomposed
netlists, not only fewer torches on paper. The synthesized 14-torch 2-bit
adder was placed against the hand-written 15-gate adder2-nor with the
same settings (7 workers, 1800 s of compaction, two seeds). It came out
about a fifth smaller and faster on both seeds:
- 122 and 134 blocks, against 160 and 163;
- s1 in 12-15 ticks, against 16-22.

The 4:1 mux's 20-gate netlist never built. The 8-torch synthesized one
compacted to 142 blocks.

Both cells are now fixtures with tests that check the arithmetic or the
selection in every case and every settled input transition. They are
also viewer examples. The manifest refresh picks up sizes of NBTs
regenerated earlier, and full-adder.nbt's stale copy. export_rcell_cell
writes any RCELL's .nbt and .outputs.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rollrat
rollrat marked this pull request as ready for review October 9, 2026 23:26
@rollrat
rollrat merged commit f39ecf2 into master Oct 9, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant