Skip to content

Development happens here#57

Merged
msoos merged 134 commits into
masterfrom
develop
Jun 11, 2026
Merged

Development happens here#57
msoos merged 134 commits into
masterfrom
develop

Conversation

@msoos

@msoos msoos commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

msoos and others added 30 commits May 24, 2026 19:34
Move the "Download Verilog output" button below the output box in the
web UI, and drop the stray blank lines from the emitted Verilog so small
modules render compactly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Runs arjun --synth on a 3-clause CNF and fails if any blank line appears
in the emitted Verilog. Guards against regressions of the prior cleanup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Craig interpolant reconstructed from a successful cadical proof trace
is mathematically correct; the only outcomes are "valid interpolant" or
"no interpolant available" (empty/no-input conflict, oversize AIG, budget
exhausted). Reword CLAUDE.md, the verification-helper docstrings, the
two release_assert messages, and the A->I / sample_check assertion-fail
notes to reflect that — and re-frame the public check helpers as
SLOW_DEBUG / test-only sanity helpers (which is how they are actually
called: only from SLOW_DEBUG_DO blocks and unit tests).

Comments and assertion messages only — no behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The probe finds defs of the form t = L or t = ~L (equivalence to a
single literal), which is what "equiv" names. The "conditional" label
was misleading. Affects the config fields, public API setters/getters,
internal identifiers, CLI flags (--unatedefcond* → --unatedefeq*),
fuzzers, and comments.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plumbing only: declares the new Config knob and exposes it as
--interprepairfullconf in main.cpp. No behavior change yet; the
follow-up commits wire it through interp_repair and manthan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generalises the McMillan tracer to operate on an arbitrary shared-var
set rather than hard-coding input_vars. compute_interpolant() now takes
a full_conflict flag that, when set, moves every conflict unit to the
B side (input + y_other) and tells the tracer the shared set is every
conflict var. The resulting interpolant covers the whole conflict
clause as-is — a smaller AIG over all conflict vars, not just the
input projection.

setup_mini_cnf, solve_one_interpolant, slow_check_a_implies_i, and
the SLOW_DEBUG leaf-check all branch on the new flag. The
quick/sample CEX-exclusion checks pin every conflict lit (input +
y_other) so they work for both modes uniformly.

Adds calls_full_conflict / calls_full_conflict_succeeded stats and
extends print_stats accordingly. No caller switches the flag on yet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Forwards the new flag into compute_interpolant() so the full-conflict
partition (B = every conflict unit, not just inputs) is used when the
config knob is on. build_interp_branch_formula skips the y_other-leaf
ANDs in that mode — the interpolant already encodes them, so the AND
would just duplicate the constraint and bloat b1.

The SLOW_DEBUG sanity calls and the startup InterpRepair-enabled
verb_print are updated to take/show the flag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the new full-conflict flag to the per-iteration random choice in
both fuzz_interp_repair.py (which forces interp_repair on every run)
and fuzz_synth.py (which enables interp_repair ~25% of the time). Both
fuzzers now flip the partition randomly so we exercise the new code
path alongside the existing input-only one.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The TOP REPAIRED VARS block conflated repair attempts with successful
repairs and printed AIG-node vs conflict-lit averages with the same
label, making "avg_confl_sz: 0.0" silently mean "no conflict-branch
successes" rather than "size was zero". Split the bookkeeping by path:

- attempts counter (= repair() calls incl. cost-zero failures)
- per-var interp-branch successes + AIG-node sum
- per-var conflict-branch successes + conflict-lit sum
- cost-zero count derived as attempts - (interp + confl)

Print attempts, the three path counts (i / c / z), interp% over
successful repairs only, and avg sizes printed as "-" when their
denominator is zero. Column names shortened ([m-stats], att/i/c/z/i%/
acl/ain/cl/an/ad/cf) so the line fits 1 row. Legend block follows
the table.

Drops the now-redundant "top vars driven by interp" block and the
interp_conflict_lits_per_var field it relied on.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The per-pass stats line was wrapping in terminals and printed the
"[aig-rewrite]" prefix twice (caller printed T:, then stats.print
printed the rest with its own prefix on the same logical line). Fold
the time into AIGRewriteStats (new total_time field) so print()
emits exactly one prefixed line; shorten the prefix to [aig-rw] /
[aig-rw/sweep] / [aig-rw/sweep-setup] / [aig-rw/sweep-prog] and
abbreviate field names (n: cp: cmp: idm: abs: dst: xor: hh: for
rewrite; g/chk/m/cm/r/cxf/to/ca/be/srv/cyc for sweep). Reduction is
shown as "-X.X%" inline rather than "(X.X% reduction)".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Deep proof-driven interpolant AIGs (200k+ nodes on real benchmarks)
overflow the program stack in the std::function-based recursive
rebuild lambdas. Failure mode: SIGSEGV in synth-extend's simplify_aig
on the first big interpolant, and a second crash in rewrite_aigs after
unate_def. Repro:

  ./arjun --mstrategy bve --synth --interprepairfullconf 0 \
          --interprepair 1 benchmarks-qdimacs/load_full_3_comp5_REAL.sat.qdimacs

Converted the recursion sites to explicit stack-based post-order. The
algebraic bodies (simplify_pass's absorption / distribution / XOR
patterns, etc.) are unchanged — only the recursion shape is rewritten.

Touched:
  arjun.cpp:       AIG::simplify, AIG::simplify_cse,
                   SimplifiedCNF::map_aigs_to_orig's rebuild lambda
  aig_rewrite.cpp: AIGRewriter::simplify_pass / hash_cons /
                   deep_absorb / flatten_ite_chains, plus rebuild_node
                   in sat_sweep (same pattern, fix in one pass)

The above repro now runs to completion with exit 0; Manthan finishes
with all 475 vars defined. fuzz_aig_rewrite/1000, fuzz_aig_to_cnf/200,
fuzz_synth/200, test-aig-rewrite, test-interp-repair all pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the table sorted by total repair() attempts (att = i + c + z),
which let cost-zero-heavy vars dominate the "top" list even though their
formulas were never actually repaired. Sort by i+c instead (interp +
conflict-branch successes), tie-break on att, and rename the heading to
TOP SUCCESSFULLY REPAIRED VARS so it matches what it shows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Scaffolding for an in-tree port of CADET (Markus N. Rabe, SAT 2016) as
an alternative to Manthan for the final synthesis step. Enabled with
--cadet 1.

Phase A (this commit): when |inputs| <= 16, build each Skolem function
by enumerating every input assignment, calling cadical under the
assumption, and OR-ing input-minterms into a per-y AIG. Slow but
correct — sufficient to exercise the wiring on fuzzer-sized inputs.

Phase B (unique-consequence propagation) and Phase C (decisions +
conflict analysis), which together make up the actual incremental-
determinization algorithm, are the next milestones; they will replace
Phase A as the primary path and let CADET handle benchmarks with too
many inputs to enumerate.

Wiring:
- new Cadet class in src/cadet.{h,cpp}, mirroring Manthan's API
- Arjun::standalone_cadet() in arjun.{h,cpp}
- --cadet flag in main.cpp; do_synthesis() dispatches to standalone_cadet
  instead of run_manthan_strategies when set

Verified manually on out/fuzzTest_1.cnf with --synthbve 0 --extend 0
--minimize 0 --unatedef 0 to force the cadet path (49 to_define vars,
4 inputs, 16 SAT calls); test-synth reports the result CORRECT.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two changes:

1. Enumeration scope: Phase A was iterating over `VarTypes.input`, which
   in addition to the orig sampling vars also includes extend-defined
   vars (vars whose AIG def already depends only on orig sampling
   vars). Those are not free — F constrains them via their AIG — so
   most of the 2^|input| assumption sets we'd build would be
   inconsistent and immediately rejected, and the iteration count
   would explode past the threshold. Track the orig sampling vars in
   CNF numbering separately (`orig_sampl_cnf`) and enumerate only over
   those; the SAT solver propagates the dependent extend-defined vars
   for free.

2. Dedicated fuzzer at scripts/fuzz_cadet.py (symlinked into build/).
   Mirrors fuzz_synth.py's shape but trims the option matrix to flags
   CADET actually consumes, smaller Brummayer CNFs, and clamps the
   projection size so |orig sampling| stays under Phase A's
   enumeration threshold.

   Verified by running ./fuzz_cadet.py --num 50 — all iterations pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase A previously OR'd one minterm per SAT call into the running
Skolem AIG. After AIG-rewrite that compresses, but the intermediate
representation is O(2^n_inputs) per to_define var — slow to construct
and a lot of churn for the rewriter to undo.

Now: collect a value table per y across all input assignments, then
build the Skolem AIG by bottom-up Shannon decomposition over the
sorted inputs. ITE(b, l, r) already collapses identical-children at
the AIG layer, so constant regions of the function vanish naturally
and shared subtrees are not built twice. Faster construction, far
smaller intermediate AIG, identical post-rewrite results.

Verified by ./fuzz_cadet.py --num 30 (all CORRECT) and by direct
comparison on a hand-rolled 8-input CNF (1000 nodes for 12 vars,
correctness check passes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B decomposes F's clause graph into connected components (only
orig sampling vars are sinks; extend-defined and backward-defined
vars are traversed through, otherwise different components would
implicitly share orig sampling vars via their AIG defs and produce
mutually-inconsistent Skolems), then enumerates each component's
to_define vars jointly over the component's restricted input set.

When F decomposes naturally this is much cheaper than Phase A's global
enumeration (a component with 3 inputs needs 2^3, not 2^|all-inputs|),
and each component's to_defines come from the SAME SAT call so their
Skolems are mutually consistent.

A first version of this commit treated extend-defined/backward-defined
vars as sinks too; that was incorrect and produced wrong AIGs on
seed 9340754230162260130 (test-synth: "Sample does not satisfy the
CNF"). The fix — only stop BFS at orig sampling vars — is also why
Phase B is a stepping stone, not the endpoint: components grow
through every defined intermediate, so this still won't help on
CNFs where backward/extend has already linked everything to
everything else. Phase C will be the answer for those.

Verified by ./fuzz_cadet.py --num 100 — all 95 reachable iterations
report AIGs CORRECT (the other 5 hit UNSAT CNFs or <2-var CNFs and
are skipped).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase C is the actual CADET-flavored algorithm: for each undetermined
to_define var y, iterate its clauses; when every other literal in a
clause is already a function of inputs / earlier-determined vars, the
clause forces y to a specific value over the "forced region" (where
all other literals are false). OR positive-force regions across all
positive-y clauses to get y's Skolem candidate. Iterate to fixpoint.
Falls through to Phase B / Phase A if some y can't be propagated this
way — Phase D (decisions + conflict analysis) would handle those.

Cycle hazard fix: even when pos_force_y's direct cnf-space leaves are
all "non-y", the leaves' orig-space AIG defs (from earlier extend /
backward passes) may transitively reference y_orig. Committing then
closes a defs[] cycle. Caught seed 12845165257551977592: extend had
defined orig-19 via an AIG referencing orig-18 (then undef), and
Phase C tried to define orig-18 via a clause involving orig-19. Fix:
walk pos_force's cnf leaves, map each to orig, follow the transitive
defs[] chain via get_dependent_vars_recursive, and reject the commit
if y_orig is reachable.

A separate one-line bug along the way: get_dependent_vars_recursive
returns its result in insertion order, not sorted, so a binary_search
on it silently always returns false — fixed to use a linear find.

Fuzzer adjustments (fuzz_cadet.py):
- Force all toggleable preprocessing passes (synthbve, autarky,
  extend, minimize, unate_def, ...) OFF, so the upstream pipeline
  can't always finish synthesis before cadet sees the CNF — otherwise
  the fuzzer never exercises Phase C/B.
- Project size still clamped under Phase A's enumeration threshold.

Verified: ./fuzz_cadet.py --num 100 → 92 CORRECT, 0 BUGs. Phase C is
the primary entry; falls through to Phase B for the cases where
propagation gets stuck (most fuzzer CNFs, since to_defines tangle).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously every iteration forced all toggleable preprocessing passes
OFF, so the fuzzer never exercised cadet with a partially-preprocessed
CNF — the more realistic case. Now: 50% of iterations keep everything
off (cadet does the whole synthesis), 50% randomize each toggle
independently with 80% off / 20% on (cadet sees a post-preprocessing
CNF, like real pipelines).

Verified: ./fuzz_cadet.py --num 150 → 140 CORRECT, 0 BUGs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When Phase C's propagation fixpoint leaves vars undetermined, pick the
undet var with the fewest clauses and ask the SAT solver whether F
(plus already-committed constant decisions) FORCES it to a constant:

  if F + (y=true) is UNSAT     →  commit skol[y] := false
  elif F + (y=false) is UNSAT  →  commit skol[y] := true
  else                          →  y is genuinely input-dependent,
                                   fall back (Phase B/A)

The "F forces y = constant" check is what makes this sound: a looser
"F + (y=c) sat for some input" check would commit y to a constant
even when y's correct Skolem is non-constant, producing wrong AIGs
on other inputs (caught on seed 1152065877721653320 — wrong final.aig
under -u verification).

Trade-off: the sound check is also strict, so Phase D rarely fires on
fuzzer CNFs (most to_define vars after preprocessing are not constant
in F). When that happens, Phase C+D falls through to Phase B / Phase A
as before. The proper next step — non-constant decisions with conflict
analysis and backtracking — is left for future work.

Fuzzer changes (fuzz_cadet.py):
- Bigger CNFs half the time (Brummayer -i 12 -I 30), and a wider
  projection-size range, so the run is forced past Phase A's 16-input
  threshold and exercises Phase C+D + Phase B.
- New exit-code-42 protocol: cadet prints "c o [cadet] LIMITATION:"
  and exits 42 when no implemented phase can finish. The fuzzer reads
  exit 42 as a soft skip (with a "[skip]" log line), not a bug.
  Without this the fuzzer would gate on every too-hard CNF.

Verified: ./fuzz_cadet.py --num 200 → 133 CORRECT, 0 BUGs, 54 skips.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three changes that together turn --cadet 1 from "cadet OR fail" into
"cadet does what it can, Manthan finishes the rest":

1. Phase D loops over all undet candidates. The previous version
   broke on the first failed candidate, which on real benchmarks
   (sdlx-fixpoint-5) caused cadet to give up without trying any of
   the other 80 vars. Now: try every candidate ordered by ascending
   clause count, commit any that F forces, only fall through when
   no candidate is forced.

2. Cadet returns partial results instead of exit(42). When some phase
   can't finish, commit whatever skol[y] entries are non-null and
   return a CNF that may not be synth_done(). Phase B/A still reset
   skol[], so they only run when Phase C+D committed nothing —
   otherwise their reset would clobber Phase C+D's partial work.

3. main.cpp falls through to run_manthan_strategies() when --cadet 1
   left vars undetermined. Manthan starts from the partial CNF where
   cadet's commits already live in cnf.defs (so they appear as
   backward_defined to Manthan), and only synthesizes the remaining
   vars. Strictly ≥ Manthan-alone: cadet can only ADD definitions,
   never remove them.

User's sdlx-fixpoint-5 case: cadet commits 0 (no forced constants
after preprocessing), hands off to Manthan, Manthan synthesizes all
81 vars in 156 s. test-synth reports CORRECT.

Fuzzer changes (fuzz_cadet.py):
- run_synth returns an `info` dict with cadet stats (committed count,
  partial vs all, handoff triggered) parsed from arjun's verb=1
  output.
- Main loop accumulates per-iteration stats and prints a summary at
  the end. The fuzzer FAILS if ≥ 20 iterations succeeded but ZERO
  of them had cadet commit ≥ 1 var AND hand off to Manthan — the
  whole point of the new coverage being to exercise that path.
- Exit code 42 / LIMITATION handling removed (cadet no longer exits
  on incomplete synthesis).

100-iter fuzz: 93 succeeded, 0 BUGs, 22 fully-by-cadet, 58 cadet-
partial-then-handoff (the new path we wanted to cover), 0 cadet-
did-nothing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cross-tests the cadet→Manthan handoff against fuzz_synth.py's full
Manthan flag matrix (which fuzz_cadet.py keeps fixed). Cadet does
what it can, main.cpp falls through to the Manthan strategy ladder
for the rest. Verified by running ./fuzz_synth.py --num 30 — 11/30
ran --cadet 1, all 28 successful runs verified CORRECT, 0 BUGs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the gap between Phase C+D's partial commits and the Manthan
handoff. Previously, when Phase C+D committed some but not all
to_define vars, Phase B/A couldn't help (they reset skol[] and would
clobber the partial work), so everything not handled by Phase C+D
fell through to Manthan even when the orig sampling space was small.

Phase E:
  1. Builds a fresh SAT solver with F.
  2. Tseitin-encodes every prior skol[] entry into the solver
     — Phase C's pos_force AIGs via the existing AIGToCNF encoder,
     Phase D's constant commits as unit clauses — and links each
     y to its encoded skol via y ↔ root.
  3. Enumerates orig sampling assignments via repeated SAT solving,
     forbidding each found input pattern. Every model already
     respects prior commits, so the y values it gives for the
     still-undet vars are jointly consistent with what was
     committed.
  4. Stops when the solver returns UNSAT (every consistent input
     pattern visited). Builds undet y's Skolems by Shannon
     decomposition over the resulting value tables — same shape
     as Phase A's output.

Threshold: |orig_sampl_cnf| <= kSmallInputThreshold (16). Above that
Phase E gives up and lets the caller hand off to Manthan, as before.

Fuzzer (fuzz_cadet.py) now tracks Phase E specifically:
- info["phase_e_ran"], info["phase_e_committed"] per iteration.
- Stats counters phase_e_ran and phase_e_finished_synthesis.
- Asserts that with >=30 successful iterations, Phase E ran at least
  once AND finished synthesis at least once — otherwise the fuzzer
  is misconfigured and not exercising Phase E.

Verified: ./fuzz_cadet.py --num 100 → 91 succeeded, 0 BUGs.
  cadet_committed_all: 59 (was 22 pre-Phase-E)
  cadet_committed_partial_then_handoff: 21 (was 58 pre-Phase-E)
  phase_e_ran: 44, phase_e_finished_synthesis: 44.

Phase E shifts ~half of the previously-handoff iterations to fully-by-
cadet finishes. Manthan still picks up the cases Phase E can't reach
(large orig sampling space).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
msoos and others added 28 commits May 27, 2026 21:45
The pass labeled "CADET" was a brute-force Shannon-tree synthesizer, not
incremental determinization — rename file, class (Cadet → ShannonSynth),
public API (standalone_cadet → standalone_shannon_synth), config field
(cadet_phase_e_threshold → shannon_synth_threshold), CLI flags
(--cadet → --shannonsynth, --cadetphaseeth → --shannonsynththresh),
log tags ([cadet] → [shannon_synth]), and debug AIG suffix accordingly.

Flip the binary default to --shannonsynth 1 and update fuzz_synth.py to
toggle 50/50 across both terminal paths. Delete fuzz_cadet.py — its
--cadetcegar*/--cadetpartial knobs were removed in b164dff and the
remaining coverage is folded into fuzz_synth.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Topology + 64-bit signature + used_vars build moves out of
aig_rewrite.cpp into a standalone SimState class so the upcoming
interp_resub pass can share it. SweepState now inherits from SimState
and keeps only the sat-sweep-specific candidate-class / substitution
scratch. sat_sweep stats line is bit-identical for a fixed-seed CNF
before and after the refactor.

Also fix up references to incorrect interpolants in code/comments

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
By the time shannon_synth runs, the CNF has been through extend_synth,
backward_round_synth, AIG rewrite, and unate_def — passes that can
introduce dependencies among orig sampling vars that didn't exist at
initial Minimize::run_minimize time. Run Backward::minimize_subset on
the current CNF with orig_sampl_cnf as the candidate set to shrink the
enumeration domain before the 2^N truth-table allocation.

Smaller domain → smaller tables, fewer SAT calls, and more cases stay
under shannon_synth_threshold (16). E.g. on a fuzz CNF this trims
|orig_sampl_cnf| 31 → 27, cutting 2^N rows 16×.

The new Backward::minimize_subset is a single-use, dry-run entry: it
inlines fill_solver's body to bypass the opt_sampl_vars guard and
overrides sampling_vars with the caller's candidate, then runs the
usual init → get_incidence → duplicate_problem → add_fixed_clauses →
backward_round chain. Does not touch cnf state.

Gated by --shannonsynthminim (default 1). fuzz_synth.py randomizes
the flag 0/1 independently of --shannonsynth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two behavior changes plus cleanup:

1. Soft fallback. When the enum set (after minim) exceeds
   shannon_synth_threshold, shannon_synth now returns the CNF unchanged
   (synth_done() stays false) rather than release_asserting. main.cpp
   runs shannon first and falls through to Manthan when it declines, so
   --shannonsynth being default-on no longer crashes on inputs whose
   independent support is too large to brute-force.

2. Minim cap. --shannonsynthminimmax (default 40) bounds the candidate
   size at which the dry-run backward minim is attempted; above it the
   doubled-CNF minim is too costly and unlikely to reach the threshold,
   so it's skipped.

Also: trim shannon_synth comments to WHY-only one-liners and replace the
clunky `if (conf.verb >= 1) cout << "c o ..."` blocks with verb_print.
fuzz_synth.py drops the now-dead threshold-skip branch and randomizes
--shannonsynthminimmax.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zlib.net DNS resolution fails in CI; download the identical zlib-1.3.1
tarball from the madler/zlib GitHub release instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the AIGRewriter::sat_sweep pass and all its plumbing: the sweep
implementation and helpers in aig_rewrite.{h,cpp}, the now-dead SimState
helper (aig_sim.{h,cpp}, only ever a base for SweepState), the
--sat-sweep / --interprepairb1satsweep flags and their config, the
post-sweep opt-sampl revert logic in arjun.cpp, and the sweep calls in
manthan.cpp. Also strips the fuzzer's sweep/multi-def modes and the
dead flags from the fuzz scripts.

Build is clean; all four fuzzers and test-interp-repair pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
deep_absorb's per-node computation was a ~220-line inline block inside the
traversal loop, hard to review. Extract it into absorb_and_node plus one
helper per stage (absorb_local_and, fold_and_children, absorb_wide_or,
absorb_cross_level, resolve_or_pairs). The traversal skeleton now just calls
absorb_and_node(l, r). No behavior change; fuzzers and test-aig-rewrite pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@msoos msoos merged commit 0a58a05 into master Jun 11, 2026
7 of 18 checks passed
@msoos msoos deleted the develop branch June 11, 2026 18:10
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