Fast Congruence Closure Implementation Note#
Reference#
Robert Nieuwenhuis and Albert Oliveras, “Fast Congruence Closure and Extensions”, Information and Computation 205, 2007.
Implemented material: Sections 3.1 through 3.5 in the default incremental EUF engine. Default-off explanation modes implement Section 4/5.2-style reoriented proof forests and the Section 5.5 structural-class refinement, but these remain experimental because the benchmark evidence is mixed and Primo does not emit proof output.
Paper Operations To Primo Types#
The paper first Curryfies applications and then flattens non-constant subterms
so the congruence-closure module receives constants and flat equations
f(a, b) = c. Primo keeps SMT-LIB out of the theory layer: FlatTermIndex
reads core::TermId values owned by core::TermManager, converts every
application into deterministic binary apply definitions, and maps original
equality atoms to flat endpoints.
Paper data structures map as follows:
Pending:FlatCongruenceClosure::pending_, a LIFO merge worklist containing explicit equality merges and congruence merges discovered through lookup hits. The worklist order is sound for closure but can affect proof shape and SAT search.Representative:FlatCongruenceClosure::parent_, where every class member eagerly stores the current representative.ClassList:class_members_andclass_size_, used to merge the smaller class into the larger one.UseList:use_lists_, indexed by representative and containing flat definition results whose function or argument representative includes that class.Lookup:lookup_, keyed by(representative(function), representative(argument)), storing one flat definition with that current signature.Disequality candidates:
disequalities_stores asserted disequalities in assertion order, whiledisequality_lists_indexes those rows by current representative. This is an implementation-local index for conflict checks, not a separate paper operation.
Merge is implemented by assert_equal, enqueue, propagate, and
merge_classes. Constant equalities are pushed to pending_; application
congruence is discovered when a flat definition’s normalized signature hits an
existing lookup entry. process_use_list implements the Section 3.3 lines that
only revisit definitions affected by the representative that just disappeared.
AreCongruent? is implemented by are_equal, which drains pending_ and
compares current flat representatives. Current Primo terms remain hash-consed
TermId values outside the theory engine; applications are normalized through
the flat signature table.
Disequality conflicts are checked immediately when a disequality is asserted.
After a merge, FlatCongruenceClosure only revisits disequalities indexed by
the two merged representatives. Candidate indices are sorted before checking so
the first conflict follows original disequality assertion order even though
unrelated disequalities are skipped. Direct disequality conflicts are checked
before the affected use list is saturated; once the branch is already
inconsistent, the closure can report that conflict without spending work on
congruence consequences that SAT backtracking will discard.
Invariants#
Every inspected object is a
core::TermId; SMT-LIB AST nodes never enter the equality engine.parent_[t]is the eager representative for every membertof a live class.class_members_[r]contains exactly the members of representativer; non-representative class lists are empty after a merge.Backtracking uses the class-list merge record to restore both
class_members_and the eagerparent_representative for every moved member. This is an implementation-local sparse rollback representation of the Section 3 class-list operation; it does not change the paper’s merge semantics. Class sizes, use-list swaps, lookup insertions, disequality lists, edge adjacency, and proof-parent diagnostics remain separately trailed because their rollback lifetimes are not identical to the moved class suffix.lookup_entries are keyed by current flat representatives at insertion time. Stale keys may remain after a merge but are not queried because new probes are built from current representatives.Each flat result id has at most one definition. Re-adding the same
(function, argument, result)triple is ignored; adding the same result with a different signature is rejected as an invariant violation before it can corrupt lookup witnesses or congruence explanations.use_lists_[r]contains definitions that must be reconsidered whenrstops being a representative. Duplicates are tolerated while appending and are suppressed with a generation-stamp scratch vector when a list is processed.Disequalities are stored as theory literals and checked after each merge.
Explanations are sound SAT-literal sets collected from an explicit equality graph. They are not the optimized proof forests of Sections 4 and 5.
Explanation adjacency lists are chronological: each endpoint list receives edge identifiers in the same append order as
edges_. Bounded recursive explanations may therefore stop scanning a node’s adjacency list when the first edge at or after the causal bound is reached; all later entries are also outside the allowed proof prefix. Checkpoint rollback removes adjacency entries from the back, preserving this order.
Complexity Expectations#
The implemented congruence discovery follows the Section 3 affected-use-list
shape rather than rescanning all registered term pairs. Class merging uses the
paper’s smaller-into-larger rule, so each term changes representative
logarithmically often. The current lookup table is a sparse hash table.
Use-list maintenance deliberately avoids a linear duplicate scan on every
append. Each process_use_list invocation increments a generation stamp and
skips repeated result identifiers in the affected list; this keeps duplicate
discipline at the point where duplicates would otherwise cause repeated lookup
work. The stamp vector is scratch metadata sized with the flat node arrays and
does not participate in checkpoint rollback.
Two deviations matter for performance claims:
Backtracking uses
push/popcheckpoints plus a sparse undo trail for the mutable flat-closure state that changes after a checkpoint: representatives, class sizes and members, use lists, disequality lists, explanation adjacency, and lookup maps. This is correct for SAT integration and avoids copying the whole closure on every checkpoint, but it is still not the paper’s timestamped lookup-table backtracking policy. Hot undo entries store only scalar rollback data; operations that need full vector snapshots, currently use-list restore and disequality-list restore, place those snapshots in side payload arrays whose sizes are checkpointed with the main trail. This is a storage-layout choice for the current rollback implementation, not a separate paper operation.Final model checks and model-time propagation still rebuild the flat equality state from a complete SAT assignment.
Opt-in online propagation with
--euf-engine=incrementalowns a persistent flat closure and asserts theory literals as CaDiCaL reports assignments. It creates those checkpoints lazily, immediately before the first theory assertion in a SAT decision level, and restores the first discarded checkpoint on SAT backtrack. This avoids replaying the retained assignment trail and avoids allocating rollback state on Boolean-only levels, but still is not the paper’s timestamped lookup-table backtracking policy.Online propagation candidate discovery uses the same affected-class idea at the SMT callback boundary: flat-class merges record terms whose representative changed, and the callback maps those terms to existing theory atoms before falling back to a full scan. This is an implementation-local work queue, not a new theory rule; reason construction still checks every delivered candidate.
Counters#
The engine exposes counters for:
registered terms and flat application definitions;
explicit and congruence merges;
pending queue pops;
lookup probes and hits;
disequality checks and conflicts;
explanation calls and per-explanation cache hits;
explanation length;
explanation-shape diagnostics: top-level equality queries, recursive congruence-premise queries, repeated pair queries served from the per-reason cache, and maximum recursive explanation depth;
explanation diagnostics: BFS searches, BFS nodes visited, BFS path edges, maximum BFS queue size, recursive causal edge skips, direct equality edges expanded, congruence edges expanded, and duplicate reason literals skipped;
conflict-reason size diagnostics: maximum emitted reason size and buckets for reasons of length at most 4, 8, 16, 32, 64, and greater than 64;
backtracks;
use-list appends, process-time duplicate skips, affected-list duplicate skips, and maximum observed use-list size;
compact class-member rollback volume via
:euf.incremental.class-member-trail-entries-saved.
The SMT layer exports these under :euf.incremental.* when
--euf-engine=incremental is used for final conflict checks, model-time
propagation, or online propagation.
Tests Derived From The Algorithm#
The unit tests in tests/unit/euf_solver_tests.cpp cover:
unary and binary congruence discovered by lookup hits;
shared-subterm congruence explanations;
disequality conflict clauses;
backtrack undo followed by rediscovery;
nested checkpoint rollback, disequality-conflict rollback, and post-checkpoint definition rollback;
deterministic Curry-style flat definitions;
order-preserving flat disequality candidate scans;
intersection-only scans for disequalities that can newly conflict after a merge;
direct disequality conflicts before affected use-list scans, including checkpoint rollback after that early exit;
repeated sub-explanation reuse inside one flat congruence reason;
causal recursive explanations for congruence edges: child premises ignore equality-graph edges introduced at or after the congruence edge being justified;
independent reason checking: every produced flat explanation in a deterministic push/pop sequence must entail the queried equality under a naive saturation oracle using only the explicit equality literals named by the reason;
changed-term draining after flat merges and rollback of undrained changed-term records on backtrack;
process-time use-list duplicate handling across checkpoint rollback;
lookup-hit definitions remain represented by the existing lookup/use-list witness and still participate in future congruence discovery;
deterministic randomized flat-closure sequences checked against a naive saturation oracle under checkpoint rollback;
nonzero lookup-hit and congruence-merge counters.
The SMT tests in tests/unit/boolean_solver_tests.cpp cover conflict checks,
model-time propagation, and persistent online propagation with
--euf-engine=incremental. The online test requires nonzero flat-term,
flat-definition, lookup-hit, and congruence-merge counters.
Known Limitations#
The default EUF engine is the flat incremental checker.
Final conflict checks and model-time propagation still rebuild flat closures from complete SAT assignments.
Online and conflict callback modes do not yet implement the paper’s timestamped backtracking structure, and are not default on the hard Goel regressions.
Explanations are intentionally non-minimal.
Proof output is deferred. Section 4/5-style proof-forest explanation modes are implemented only as opt-in modes and are not the default QF_UF explanation policy.
2026-07-19 Audit Notes#
Two local experiments checked whether the remaining PEQ/NEQ slowdown could be closed by smaller patches around the Section 3 use-list and the current explanation graph:
A stricter reading of Section 3.3 would avoid moving an affected flat definition to the new representative’s use list after its normalized signature hits
Lookup. In Primo’s current online/backtracking setting this reduced lookup work onPEQ019_size7andiso_brn_repgen_sk032, but regressed harder PEQ rows such asPEQ002_size8; the change was rejected.A bounded per-branch memo table for completed
explain_equalreasons was also rejected. Existing repeated-explanation counters did not move materially on the local PEQ/SEQ/NEQ/QG matrix, while wall time became noisier or worse.
The audit therefore points back to the paper’s Section 4-5 proof-forest
mechanism as the plausible missing congruence-closure component. The current
FlatCongruenceClosure explanation path is still an adjacency-list BFS over
the equality edge graph with recursive expansion of congruence labels. The
paper instead maintains a proof forest during Propagate and answers
Explain by walking to a nearest common ancestor, using an auxiliary
union-find/highest-node structure to avoid repeated visits across recursive
subproofs. That implementation remains future work; it must be designed from
the paper rather than from any reference solver code.
A follow-up local attempt implemented a Section 4-style reoriented proof forest
and then replaced recursive congruence-label expansion with a Section 5.2-style
pending-proof worklist plus auxiliary union-find/highest-node scratch state.
Focused flat-CC tests passed, but the local PEQ/SEQ/NEQ/QG matrix was mixed:
PEQ019_size7 improved, while PEQ002_size8 regressed badly because the proof
forest produced substantially larger conflict reasons and changed SAT search.
The attempt was rejected and reverted. The lesson is that the missing optimized
Explain machinery is not just connectivity; proof-tree shape and reason
quality must be measured directly before replacing the current BFS reason path.
Next CC review items:
Add diagnostic-only counters for per-conflict reason length distribution and compare BFS path length against proof-forest path length without changing the emitted clause.
Revisit the paper’s proof-forest orientation together with explanation length/age tradeoffs from Section 5.4 before enabling it in the default online EUF path.
Do not replace per-append
UseListPoprollback with a naive per-levelUseListResizetrail entry. A local prototype preserved the focused oracle tests but regressedSEQ035_size6from about 1.2 seconds to about 21.7 seconds because use-list appends exploded from about 4.3 million to about 84.9 million after backtracking. Any batching of use-list rollback must account forUseListRestoreswaps inprocess_use_list, not only append-only growth.Keep reference-solver CC code as behavioral evidence only. The implemented Primo design must remain paper-derived and tested through local invariants.
The accepted parent-restore batching removes per-member
Parenttrail entries and restores moved parents from the existingClassMembersMergesuffix record. On a local NEQ/PEQ/QG/Goel matrix it reduced trail-entry counts on every completed row, for exampleNEQ016_size7default 109,691,307 -> 84,296,375, while wall-time medians were mixed and should not be reported as a headline speedup without fresh SLURM evidence.
The follow-up audit on the same date rechecked the current FlatCongruenceClosure
implementation against the Section 3 data-structure checklist and the
LLM2SMT behavior notes without copying reference code. No rollback or
congruence-discovery soundness bug was found in the inspected paths:
definitions are trailed, lookup insertions are removed on pop, use-list
duplicates are tolerated and handled at process time, changed-term queues are
truncated by checkpoints, and explanation edges are removed with the merge edge
that introduced them. The local gprof profile for PEQ002_size8 still puts
FlatCongruenceClosure::explain_equal_into first by self time, with 25.6M
explanation calls, 7.0M BFS searches, 105.9M BFS nodes visited, and 21.4M
recursive congruence-edge expansions in the ordinary stats run. This reinforces
the earlier conclusion: the current closure is a Section 3 congruence-discovery
engine with a sound adjacency-BFS explanation layer, not the Section 4-5
optimized proof-forest explanation algorithm.
The next diagnostic patch added explanation-shape counters without changing the emitted conflict or propagation clauses. These counters expose how much of the current explanation work is top-level disequality/propagation explanation versus recursive congruence-premise explanation, how often a recursive pair is repeated inside one reason, and the maximum recursion depth reached. They are not a Section 4-5 proof-forest implementation; they are instrumentation to decide whether a future proof-forest design is targeting repeated subproof traversal, deep congruence nesting, or reason quality.
The audit also checked NEQ027_size8 through NEQ027_size11 against the tuned
local LLM2SMT command used in prior benchmark notes. Those rows no longer
reproduce as local slow-Primo CC gaps after the raw-AMO and use-list work:
Primo solved size11 in about 2.2 seconds, while the tuned local LLM2SMT binary
took about 4.0 seconds. The remaining difference on that family is more likely
preprocessing policy, especially finite-domain equality-definition selection,
than congruence-closure implementation correctness.
The next diagnostic pass added conflict-reason length buckets without changing
the emitted clauses. On the local matrix, PEQ002_size8 had a large-reason tail
with 31,562 conflicts over 64 literals and maximum reason length 173, while
SEQ035_size6 had mostly short reasons but still performed 1,040,412 BFS
explanation searches. This separates the remaining CC work into two mechanisms:
reason quality for PEQ-style operation-table rows, and explanation traversal
cost for formulas that generate many short conflict reasons.
A follow-up direct-edge fast-path prototype was rejected. The idea was to scan the left endpoint’s explanation adjacency list before BFS and immediately use a one-edge reason when the requested endpoints were directly connected. This reduced BFS search counters, but the per-call adjacency scan was too expensive: the local PEQ/SEQ/NEQ/QG matrix slowed down across the board. Future traversal work should avoid adding per-explanation scans before the main search.
The next review pass hardened the current explanation graph without replacing
it. Recursive expansion of a congruence edge now propagates failure from child
explanation queries instead of assuming those invariants silently hold; direct
disequality conflicts fail closed if the equal endpoints cannot be explained.
The new explanation-oracle unit test checks returned reason literals against an
independent naive saturation model under deterministic checkpoint rollback.
This did not claim a performance improvement. Local NEQ015_size4 and
PEQ019_size7 solver smoke runs preserved the expected unsat stats shape; on
PEQ019_size7, the remaining hot signature is still explanation traversal and
large conflict reasons, with 1,261,146 explain calls, 320,435 BFS searches, and
1,239 conflicts over 64 reason literals.
The next CC review found that recursive explanation of a congruence edge was
searching the full current equality graph. Congruence edges are causal proof
steps: a child equality premise must be explained using equality edges available
before the congruence merge was added. FlatCongruenceClosure now passes the
congruence edge index as a recursive BFS bound, so child explanations ignore
edges added at or after that proof step. This is still the current adjacency-BFS
explanation layer, not the paper’s Section 4-5 proof forest. The local
a228e9d baseline comparison showed unchanged conflict counts, explanation
calls, BFS search counts, and reason-length buckets on the sampled rows, but
fewer BFS nodes visited: PEQ002_size8 dropped from 105,904,124 to 79,293,756,
PEQ019_size7 from 4,634,102 to 3,529,805, SEQ035_size6 from 5,035,533 to
5,013,254, and qg7/iso_icl_repgen006 from 8,754,428 to 5,960,538.
A subsequent cache audit found that the per-reason repeated-pair cache also has
to respect the causal edge bound. A pair explained with a later edge prefix is
not necessarily valid for an earlier congruence edge’s premise. The cache now
records the smallest edge bound under which each pair has been successfully
explained and only reuses the entry when the current recursive bound is equal
or looser. The regression test directly exercises a broad successful query
followed by a stricter failed query in one reason collection. This is a
correctness and diagnostics tightening, not a performance win: on local
NEQ016_size8, the stricter cache exposed millions of bound misses and more
BFS work while preserving the same timeout outcome. That reinforces the earlier
conclusion that the remaining CC-specific performance gap is the missing
Section 4-5 proof-forest explanation mechanism and reason-quality policy.
A fresh local audit on commit 5621a24 repeated the NEQ016_size8 stats and
perf profile after the cache-bound fix. The 45-second capped stats run still
timed out with 72.3M explanation calls, but only 366k top-level explanation
queries; nearly all work came from recursive congruence-premise explanations.
The same run reported 23.9M BFS searches, 253.6M BFS nodes visited, 65.7M
expanded congruence edges, 29.5M causal edge skips, 7.0M cache-bound misses,
833M trailed closure mutations, and zero affected-use-list duplicate skips.
The corresponding perf sample put FlatCongruenceClosure::explain_equal_into
first at 19.0 percent self time, followed by CaDiCaL propagation, class merges,
rollback, and use-list processing. This review did not find a new Section 3
congruence-discovery or rollback bug. It narrows the next CC work to a
paper-derived explanation design: either implement and validate the Section 4-5
proof-forest machinery with reason-length safeguards, or add a diagnostic mode
that computes proof-forest path lengths without changing emitted clauses before
attempting another replacement.
A rejected micro-optimization tried to cache failed bounded explanations during
one reason collection: if no path exists using edges before bound N, stricter
bounds cannot succeed either. The focused unit test passed, but local probes on
NEQ016_size8, NEQ016_size7, NEQ023_size7, PEQ019_size7,
PEQ002_size8, and qg7/iso_brn_repgen033 produced zero failure-cache hits.
The patch would therefore add a hash lookup to the hottest explanation path
without reducing BFS work on the sampled rows, so it was reverted. This leaves
successful bounded-pair reuse as the only current per-reason cache and again
points at proof-tree shape, not failed-query repetition, as the next CC target.
The next accepted source change kept the default explanation path unchanged and
added --euf-explain-search=bidirectional as an explicit experiment. This mode
uses bidirectional BFS over the same causal equality-edge graph. It is a
baseline graph-search alternative, not a proof-forest implementation. Local
same-binary probes showed why it is not a default: it improved NEQ016_size7
and PEQ019_size7 and reduced BFS visits on NEQ016_size8, but it regressed
PEQ002_size8 badly by changing proof paths and SAT search. The option exists
so SMAC and focused local runs can test whether bidirectional search is useful
inside a family-specific profile while the validated default remains
one-sided BFS.
The next review step added --euf-proof-forest-diagnostics, a default-off
measurement mode that maintains a rollback-aware proof-parent forest beside the
existing explanation graph. This is still not the Section 4-5 Explain
algorithm: emitted conflict and propagation clauses continue to use the current
causally bounded BFS reasons. The diagnostic records how many cache-miss
explanation queries have a proof-forest path, the total proof-path edge count,
the maximum path length, missing-LCA cases, and per-query shorter/equal/longer
comparisons against the BFS path that actually emitted the reason. A local probe
found no missing LCAs on the sampled rows. It also showed why a direct
proof-forest replacement needs reason-quality safeguards: proof-path edge counts
were lower than BFS path edge counts on NEQ016_size7 and PEQ019_size7, but
higher on PEQ002_size8, the same row where earlier reason-shape experiments
regressed.
A subsequent CC review against Section 5 found no new rollback or explanation
soundness defect in the current adjacency-BFS implementation, but it did close
two test gaps and one local performance hygiene issue. propagate() now stops
draining pending merges after conflict_ is set, because those merges cannot
change the current theory answer and only add class, use-list, explanation-edge,
and trail work. The unit suite now covers bidirectional BFS causal-bound and
cache-bound behavior, and checks a nested-congruence disequality conflict reason
against the naive flat oracle. This still leaves the important paper gap
unchanged: Primo does not store labeled reoriented proof-forest edges for
Explain, and it does not use the per-explanation auxiliary union-find with
HighestNode from Section 5.2 to avoid repeated subproof traversal.
The next rollback audit split rare vector snapshots out of TrailEntry and
kept them in checkpointed side payload arrays. Focused tests and local
PEQ/SEQ/NEQ/QG smoke runs preserved answers and the main search counters. This
reduces hot trail-entry memory traffic in the existing checkpoint trail, but it
is not the missing Section 4-5 explanation algorithm and should not be reported
as reference parity.
A follow-up CC review tightened two invariants. First, add_definition now
rejects conflicting duplicate flat definitions for the same result id and
ignores exact duplicates; FlatTermIndex normally creates unique definitions,
but the closure now enforces that precondition directly. Second, proof-forest
diagnostics now trail the merge-edge index attached to each diagnostic parent
link and stop parent walks at the same causal edge bound used by recursive BFS
explanations. The diagnostic is still only a path-length comparison beside the
current BFS reason engine; it is not the Section 5 Explain algorithm and does
not emit proof-forest reasons.
A clean-room comparison against the allowed local reference checkout and a fresh
read of Sections 3 and 5 of Nieuwenhuis-Oliveras refined that limitation. The
reference behavior includes a proof-producing explanation path with a per-call
auxiliary union-find, while Primo’s active implementation still emits reasons
from the causally bounded equality-edge BFS. Primo’s
--euf-proof-forest-diagnostics mode is useful for checking whether
representative proof-parent LCAs exist, but it is not an exact predictor of
Section 5 explanation cost: a real proof edge must also account for the original
endpoint in the merged class when that endpoint is not the representative. The
current diagnostic stores only the representative parent edge and therefore can
understate the extra sub-explanation work a full proof-forest replacement would
need.
The same review did not find a new Section 3 congruence-discovery or rollback bug in the emitted-default path. The lookup table still has the intended Primo-specific backtracking discipline: entries inserted after a checkpoint are trailed and removed, older keys may become stale after merges, and current probes use current representatives so stale keys containing dead representatives are not reachable until rollback restores those representatives. Use-list entries remain duplicate-tolerant and are deduplicated only when an affected representative is processed.
Local proof-diagnostic probes on NEQ023_size7 and iso_icl_repgen005 showed
why the next CC implementation needs a guarded paper-derived design rather than
a direct parent-path substitution. On NEQ023_size7, diagnostic proof-parent
paths were shorter than BFS on 3907 queries, equal on 22025, longer on 796, and
had no missing LCAs. On iso_icl_repgen005, they were shorter on 331218
queries, equal on 4547349, longer on 463354, and had no missing LCAs. The
iso_icl_repgen005 run still spent 7.83M calls and 5.34M BFS searches in
explanation, so the hotspot is real, but the representative-only diagnostic
does not justify replacing the emitted reason path without implementing the
full Section 5 endpoint-label and auxiliary-union-find obligations.
The next implementation step added --euf-explain-search=proof-forest as a
default-off Section 5.2 experiment. This mode no longer uses the earlier
representative-only diagnostic as an explanation. Every maintained proof-parent
edge stores the original endpoint from the disappearing class, and the explainer
uses a per-explanation auxiliary union-find with HighestNode jumps and a
pending-proof worklist. When the stored endpoint differs from the disappearing
representative, the worklist receives a bounded endpoint-detour obligation
before the edge label is accepted. Congruence labels add bounded child
obligations for the function and argument premises instead of recursively
calling the BFS explainer.
Implemented subset and limitations: the mode follows the Section 5.2
pending-proof/auxiliary-union-find explanation shape over Primo’s current
representative-oriented proof-parent forest. It does not yet implement the full
Section 4 reorientation policy, structural-class proof forests from Section
5.5, or a reason-quality guard from Section 5.4. The emitted reasons are tested
against the existing independent naive flat-oracle checks, including a
non-representative endpoint-detour case and a nested congruence conflict. Local
same-binary probes kept the option default-off: NEQ023_size7 regressed from
about 0.38s to 0.93s, while NEQ027_size8, PEQ019_size7, SEQ035_size6,
and iso_brn_repgen_sk011 improved or stayed close to default but were often
still behind the existing bidirectional BFS option. The option is therefore a
research/tuning hook, not a reference-parity claim.
A later CC review pass profiled current head after parent-restore batching on
NEQ016_size7. The remaining current-head gprof sample still put
FlatCongruenceClosure::explain_equal_into first, followed by CaDiCaL
propagation, merge_classes, process_use_list, and append_use_tracked.
The profile also exposed avoidable overhead: every explanation called the
proof-forest path-length diagnostic helper even when
--euf-proof-forest-diagnostics was disabled. The implementation now guards
that call at the caller, and the hot merge/node-growth paths test the
proof-forest mode flags directly instead of calling a tiny helper. Default
mode therefore avoids disabled proof-forest diagnostic/maintenance calls. This
is a small implementation cleanup only; it does not change emitted reasons,
does not implement the Section 4-5 optimized proof forest, and does not close
the NEQ/reference gap by itself.
The next CC-focused review re-ran the comparison on current commit 0be41eb
after the diagnostic-guard cleanup. Source inspection again found no new
Section 3 representative, lookup, use-list, or rollback invariant violation in
FlatCongruenceClosure: current probes are built from current representatives,
lookup insertions after checkpoints are removed on pop, use-list duplicate
metadata is scratch-only, and causal explanation bounds are applied to recursive
congruence-premise explanations.
The new evidence sharpens the remaining gap. On a same-binary local matrix,
disabling EUF propagation timed out at 20 seconds on NEQ016_size7,
NEQ023_size7, and iso_icl_repgen005, so the persistent online closure is
not optional for these rows. The experimental proof-forest explainer improved
NEQ016_size7 from about 4.38 seconds to about 2.43 seconds, but regressed
NEQ023_size7 from about 0.35 seconds to about 1.02 seconds and left
iso_icl_repgen005 around 2 seconds with more explanation calls. On
NEQ016_size8 with --max-theory-conflicts 50000, proof-forest reduced the
fixed-conflict time from about 3.64 seconds to about 2.49 seconds, but a direct
solve attempt was still running after 68.8 seconds and was interrupted. This
rejects both extremes: the proof-forest experiment is a real CC direction for
NEQ, but it is not yet a default or reference-parity solution.
The current paper-derived implementation target therefore remains narrower than “rewrite CC”: preserve the Section 3 discovery engine, then design a reason-quality guard or a fuller Section 4-5 proof-forest/reorientation policy that improves recursive congruence-premise explanation without changing SAT search badly on nearby NEQ/PEQ/QG rows. Any accepted change needs independent reason-oracle tests, QF_UF fuzzing, and SLURM evidence before it can be promoted from local diagnostic evidence.
A follow-up micro-optimization tried to replace the proof-forest explainer’s
full per-query auxiliary union-find initialization with generation-stamped lazy
node initialization. This is compatible with the paper’s scratch Explain
union-find idea, but the local evidence rejected it as an implementation
priority. The focused EUF oracle tests passed after sentinel handling was fixed,
yet the same matrix did not improve: NEQ016_size7 proof-forest remained around
2.5 seconds, NEQ023_size7 still regressed relative to default, and
iso_icl_repgen005 became slightly worse in that run. On NEQ016_size8 with
--max-theory-conflicts 50000, proof-forest moved from the previous 2.49
seconds to about 2.80 seconds. The patch was reverted. The remaining cost is
therefore not dominated by zeroing the auxiliary arrays; it is still the proof
path obligations, reason shape, and resulting SAT search.
A fresh size8 review after the rejected hybrid experiment checked
NEQ016_size8 and PEQ002_size8 with 20-second option ablations and
50,000-theory-conflict capped proof diagnostics. The result again separates
path traversal from clause quality. On NEQ016_size8, proof-forest emission
was faster at the fixed conflict cap than default BFS, but increased total
explanation length and the number of conflicts with more than 64 reason
literals. On PEQ002_size8, the capped proof-forest run was also faster, but
the emitted reasons became much larger: the over-64-literal conflict bucket and
maximum reason length both grew substantially, matching the full-run timeout
seen when proof forest is used directly. The current conclusion is therefore
not that Section 3 congruence discovery or rollback is visibly unsound, and not
that the existing proof-forest switch should become a default. The next
paper-grounded CC implementation target is a reason-quality policy around
Section 5-style explanations: preserve the cheaper proof traversal only when it
does not emit clauses that are materially worse for SAT search, or implement a
fuller Section 4/5 proof-forest and reorientation design whose invariants
control both explanation cost and learned-clause shape.
A direct follow-up tried the simplest such guard as a temporary default-off
mode: construct the proof-forest reason first, emit it only when it has at most
64 literals, and otherwise fall back to the current BFS reason. The focused
flat-CC oracle test passed, but the mixed local matrix rejected the idea.
Guarded proof forest still timed out on PEQ002_size8 and PEQ003_size10,
regressed NEQ023_size7 and SEQ026_size7, and had worse PAR2 than both the
default and unguarded proof-forest modes. The lesson is that post-construction
reason-length fallback is too late and too costly. A useful guard must predict
or shape proof obligations before constructing a large proof reason, or the
implementation must move closer to the full Section 4/5 reorientation policy
so that path cost and emitted clause shape improve together.
The next temporary patch moved that cutoff into proof construction itself:
proof-forest explanation aborted as soon as the partial reason crossed the
64-literal threshold, then retried the same query with BFS. This also lost the
same mixed guard matrix. It preserved the PEQ timeouts, regressed
NEQ023_size7 and SEQ026_size7, and had worse PAR2 than the default and the
unguarded proof-forest mode. Fixed literal-count fallback is therefore not the
right abstraction for this gap. Further CC work should either implement a
paper-derived reorientation/premise policy that improves proof path and clause
shape together, or move to a different ranked performance lever before spending
more time on proof-forest fallback variants.
A fresh review at commit 3a152f8 rechecked the implemented Section 3 closure
against the current code and the allowed local reference checkout. The reviewed
default path still has the expected class-list/use-list/lookup shape: current
signature probes use current representatives, lookup insertions after a
checkpoint are trailed, use-list duplicates are tolerated and removed when the
affected list is processed, and randomized flat-oracle tests cover equality
reachability and explanations under push/pop. This pass again did not identify
a concrete representative, use-list, or rollback bug that would explain the
NEQ/PEQ gap.
The same review found two explanation-shape levers that do change search but
remain mixed. First, proof-forest diagnostics on NEQ023_size7,
NEQ027_size8, PEQ019_size7, and PEQ002_size8 showed no missing LCAs, so
the maintained parent forest usually has a path. The path-quality counters are
not decisive: on PEQ019_size7, proof-forest emission improved runtime from
about 0.90s to 0.64s, but on PEQ002_size8 it timed out at 25s while default
solved in about 15.7s. Second, a temporary FIFO pending-merge order changed the
proof graph and search substantially. It improved PEQ019_size7 and slightly
improved NEQ027_size8, but regressed NEQ023_size7 from about 0.37s to
0.85s and left PEQ002_size8 slower than default. The temporary FIFO patch was
reverted. Pending order is therefore a real CC tuning axis, but not a default
fix without a family-specific reason-shape policy and mixed-suite evidence.
Another proof-forest micro-experiment at commit 53a6442 added a bounded
per-reason cache for completed proof obligations inside
explain_equal_with_proof_forest_into, mirroring the cache already used by the
BFS explainer. The focused EUF oracle tests passed, but every measured
proof-forest row reported zero cache hits: NEQ023_size7, NEQ027_size8,
PEQ019_size7, PEQ002_size8, iso_icl_repgen006, and
iso_icl_repgen005. The patch did not fix PEQ002_size8, which still timed
out at 25s in proof-forest mode, and it did not improve the QG rows. The patch
was reverted. This rules out missing repeated-obligation caching as the main
proof-forest regression source; the remaining issue is first-time proof
obligation shape and the SAT effect of the emitted clauses.