Skip to content

Speed up proof encoding using egglog's :merge and unboxing pairs#933

Draft
oflatt wants to merge 23 commits into
mainfrom
fd-proof-encoding
Draft

Speed up proof encoding using egglog's :merge and unboxing pairs#933
oflatt wants to merge 23 commits into
mainfrom
fd-proof-encoding

Conversation

@oflatt

@oflatt oflatt commented Jun 24, 2026

Copy link
Copy Markdown
Member

This PR:

  • Auto-unboxes the Pair container automatically to create extra columns. This allows proofs to efficiently hold a value alongside a proof
  • Adds a :identity-values annotation so that proofs are not counted in the identity of the output. This makes :merge not care which proof it gets so we can match egglog's normal semantics
  • Changes the proof encoding to use a Pair of a proof and a value for each table the user defined

oflatt and others added 19 commits June 23, 2026 21:28
… foundation

Constructor congruence as a functional-dependency view `:merge` instead of a
separate congruence rule.

- New primitives: begin, uf-set, select-eq (src/lib.rs).
- Backend MergeFn::TableInsert (declares its table as a write-dep) and
  MergeFn::Seq, so a merge can write a table on the side safely during
  batched merges (egglog-bridge).
- Block-form `:merge ((set ...) ... value)`: parser + `merge_action` AST field
  + typecheck (reuse typecheck_standalone_actions) + lowering to Seq/TableInsert.
- Term-encoding mode: constructor view is now FD `(children) -> output`, with
  congruence handled by the view merge (no separate congruence rule). At
  baseline parity; normal mode unaffected.
- Multi-value bridge foundation: FunctionConfig/FunctionInfo/SchemaMath carry
  `num_values`; column math generalized (behavior-preserving at num_values=1).

Known WIP: proof-mode const-fold proofs (e.g. commute-collapse) still panic on
the interim term_proof routing; to be replaced by the multi-value (output,proof)
view once the bridge merge/lookup are generalized.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ResolvedMergeFn::run now takes the old/new value-column slices instead of a
single value; MergeFn gains OldCol(i)/NewCol(i) (Old/New == col 0) and a
top-level Tuple(Vec<MergeFn>) that produces one value per column (each may read
any column, so cross-column merges like the proof view's are expressible).
to_callback writes each value column. Behavior-preserving at num_values==1
(18 egglog-bridge tests pass; test/example FunctionConfig literals updated).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Detect a Pair container output sort in declare_function and store the
function with two value columns (its component sorts). At the rule
lowering boundary (BackendRule), transparently box the two value columns
back into a (pair ..) value on reads/lookups and unbox a pair value into
the two columns on writes. Adds RuleBuilder::lookup_value_col for
selecting a value column of a multi-value function. Behavior-preserving:
no existing test uses Pair outputs, files suite unchanged (43 baseline
proof-mode fails, no new regressions).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Re-encode proof-mode constructor views as functional dependencies
(children) -> (output, proof), stored via the multi-value front-end as
two value columns. The FD :merge now reads both colliding rows' proofs
directly (oriented with select-eq) and builds Trans(p_large, Sym(p_small))
for the congruence union, replacing the stale term_proof-based proof
routing that produced wrong proofs when an eclass held multiple distinct
constructor terms (the const-fold/commute-collapse "expected Num, got
Add" panic).

Changes:
- proof_encoding: view value = (Pair out proof) in proof mode;
  constructor_view_merge, update_view, query_view_and_get_proof, and the
  constructor case of instrument_fact_expr source the proof from the pair
  (pair-second) instead of term_proof. term_proof is kept only as an
  arbitrary per-eclass existence proof for bare eq-sort fact variables.
- lib.rs: pair-aware merge lowering (Tuple of per-column MergeFns; old/new
  and pair-first/second map to OldCol/NewCol; (pair a b) -> Tuple). Fuse
  pair-first/pair-second of a pair-valued view result to a direct value
  column read (pre-scan records projections so fusion is order-independent),
  keeping the output an indexed column for fast rebuild canonicalization
  (rebuild_rule6: 14.5s -> 0.9s; rectangle --proofs 11s).
- egglog-bridge: multi-value subsume (subsume_multi_value +
  lookup_value_col[_with_subsumed]); RuleBuilder::assert_eq_entries.
- extract.rs / serialize.rs: a pair-valued view's extracted/serialized
  output is the first value column with the pair's first component sort.

Results: files suite 733 passed / 0 failed (was 690/43); whole test suite
green (one proof snapshot updated via INSTA_UPDATE after confirming
program-run correctness); commute-collapse --proofs rc 0; rectangle
--proofs ~11s.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a term-free merge proof constructor `MergeFree (name p_old p_new)` and the
corresponding RawProof::MergeFnFree, whose conclusion term is reconstructed during
proof conversion from the two premise proofs plus running the merge function
(mirroring the proof checker's MergeFn reconstruction). This is the enabler for the
FD custom-function view merge, which runs without access to the children and so
cannot embed the conclusion term. Not yet emitted by the encoding; the existing
term-carrying Merge constructor is unchanged so all current proofs are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MergeFnIdx(name, p_old, p_new, idx) carries a deterministic pre-order
index identifying WHICH subexpression of f's merge body the proof is
for, so nested merge-body subexpressions (which share the same premises
p_old/p_new) become distinguishable. Reconstruction navigates to
subexpression[idx] of the merge body and evaluates it on the premise
outputs via the new run_merge_subexpr helper.

Encoder does not emit MergeFnIdx yet, so behavior is unchanged (files
733/0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move custom (non-constructor) functions whose `:merge` body is a pure
primitive call (min/max/or/set-union/vec-union/...) onto the SAME FD
pair-valued view that constructors use: `(children) -> (pair output proof)`,
keyed on children only. The user's primitive merge runs on the output column;
the proof column carries a single-value `MergeIdx(name, p_old, p_new, 0)`
justification (proof-datatype constructors are single-value, so no backend
mint primitive is needed). No union: a custom function's output is a merged
value, not an eclass collision.

A single predicate `is_fd_pair_view(fdecl)` routes every site (view decl,
delete/subsume, merge-or-congruence, rebuild, instrument_fact, update_view,
query_view_and_get_proof, add_term_and_view, PrintSize, extract is_legacy_view).
Custom is FD iff its merge body is all-primitive AND output is non-eq-sort AND
inputs are non-eq-sort. Function/constructor-bodied merges, eq-sort outputs,
eq-sort children, and no-merge customs stay on the legacy handle_merge_fn path
(a hybrid end state).

Saturation: the proof column uses `select-eq` to keep a premise's existing
proof when the merged output equals that premise (the common min/max/or case),
so the surviving pair is byte-identical and the merge does not re-fire forever;
a fresh MergeIdx is minted only for a value distinct from both premises.

Eq-sort children are excluded because rebuild canonicalization rewrites the
per-row pair proof to a non-reflexive congruence proof, which the MergeFn
checker (requiring reflexive premises) rejects.

Also fixes a pre-existing query-lowering bug: `(= n (pair-first (f ...)))`
followed by `(= n <literal>)` silently dropped the constraint for
non-eq-sort pair projections (the fusion aliased the literal to the column).
Now a literal projection result is constrained via assert_eq instead of aliased.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The single-parent union-find invariant (each source term has at most one
parent) was maintained by a separate `single_parent` ruleset that joined two
UF rows sharing a source. The UF function index `(UF_<sort>f source)` is
already source-keyed and is the sole writer of its rows, so a key collision on
the index is exactly the single-parent situation. Fold the union into the
index's `:merge`:

- The merge unions the two colliding parents by writing the edge
  `(UF (ordering-max b c) (ordering-min b c))` back into the UF table (via the
  existing block-merge `TableInsert` machinery) and keeps the smaller leader as
  the surviving index value.
- Path compression (kept as a rule, since it chases a 2-row join) then removes
  the now-redundant `(UF a larger)` row, so the UF table converges to one
  parent per source.
- In proof mode the index value is a `(Pair leader proof)`; the union proof is
  composed via `Trans`/`Sym` and oriented with `select-eq`. The surviving row
  keeps an existing premise proof verbatim, keeping the value stable so the
  merge saturates (no fresh proof minted each iteration).

Removes the `single_parent` ruleset (rule, ruleset declaration, schedule
saturate step, and the `single_parent_ruleset_name` field). Path compression
and the (larger, smaller) UF table are unchanged. No backend mint primitive
needed — uses only existing Trans/Sym (single-value proof constructors),
TableInsert, and the pair-merge lowering.

files suite 733/0; lib/extraction_proof_mode/terms green; clippy/fmt clean.
Regenerated the two doc_example proof_tests snapshots (single_parent removed
from schedule + the UF index merge now block-form; function2 also picks up the
already-pending Phase A FD-view shape for `add`).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hem)

The `begin` and `uf-set` primitives were scaffolding for an earlier
merge-side-effect design. The block-form `:merge` (Seq + TableInsert)
replaced both: a `set` action names its table statically (exact
write-dep) and the block sequences effects before the value. Neither
primitive is emitted anywhere in the encoding. `select-eq` stays
(load-bearing for FD-merge proof orientation + saturation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase B foundation: a :merge can now mint a pair-valued constructor
e-class and use it as a child of the merged value. TableAction::
lookup_or_insert_multi mints the first value column (FreshId) and writes
the remaining value columns (e.g. the proof) from provided values,
returning the minted output; idempotent on an existing key. MergeFn::
Construct(func, key_args, value_args) wires it through fill_deps
(read+write the func table), resolve, and run. predict_val already
supports arbitrary-length rows, so no core-relations change. Unused
until the proof encoder emits it for function-bodied custom merges.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase B step 2. Adds MergeRow(name,p_old,p_new) proving the FD view row
f(children)=eval(whole body); switches Phase A primitive-bodied FD emission
to it. MergeIdx now indexes ALL body nodes (pre-order, incl the top node as a
bare term) and always reconstructs the bare node term, dropping the
idx==0==row special case. No behavior change (lib 38/0, files 733/0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…roof linkage

Recovers the function-bodied custom-merge FD encoder (fd-mint surface form,
fd_mint_to_mergefn lowering, constructor-bodied FD view emitter) on top of the
MergeRow/MergeIdx proof-format split.

Root-cause fix for complex_merge_func_desugar_proof_testing: fd_mint_to_mergefn
sourced the per-sort term-proof table from proof_state.proof_names.term_proof_name,
which is only populated during on-the-fly proof instrumentation and is EMPTY when
the resolved-to-string program is re-parsed in a fresh (non-instrumented) egraph.
That made the fd-mint merge skip the (term_proof minted_output = proof) insert in
the desugar-rerun, so the merged constructor terms (C1/C2/top-C2) had no @treeproof
row, and the @prove_exists rule's `(= p (@treeproof out))` premise never matched ->
prove-exists could not find the witness.

Fix: source the term-proof table from proof_state.proof_func_parent, which is
re-populated from each sort's `:internal-proof-func` annotation on every parse, so
it survives the round-trip. Both maps name the identical table for any eq-sort
(proof encoding sets `:internal-proof-func` = term_proof_name).

Also drops the leftover no-op `dump_ctor_view_proof` debug test (it only eprintln'd,
tripping clippy::disallowed-macros and adding a non-asserting lib test).

complex-merge-func now passes in normal/term/proofs/proof_testing/desugar_proof_testing.
lib 38/0, files 733/0, bridge 19/0, clippy + fmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bodied)

Manufacture the reflexive premise MergeFn needs from the rebuild-rewritten
congruence proof via Trans(Sym(p), p) at resugaring time. This lets
primitive-bodied custom functions with eq-sort INPUTS move onto the FD
pair-view (e.g. merge-during-rebuild's distance: min over N N).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…GELOG

Keep eq-sort-input constructor-bodied customs (e.g. rw-analysis const-prop)
on the legacy handle_merge_fn path: the reflexivity wall is lifted by
reflexivize_premise, but their merge mints constructor intermediates into FD
views that persist as rows, diverging from normal-mode (print-size) and
breaking the across-treatments snapshot. Primitive-bodied eq-sort-input
customs (merge-during-rebuild) are on FD.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…column merge short-circuit

A constructor-bodied custom :merge (e.g. rw-analysis const-prop's
(merge-val old new)) mints constructor enodes. In a fixpoint analysis
almost all collisions are no-ops (merged output == existing output),
which normal egglog short-circuits via its cur == new check so it mints
nothing. The FD view's value carries an extra proof column (proof mode)
or runs a minting body (term mode), so an equal-output collision would
still run the body and mint a spurious merge-val(out, out), diverging
(print-size) from normal mode (0 vs 4 merge-val rows) and breaking the
across-treatments snapshot.

Fix: add FunctionConfig::identity_values: Option<usize> to egglog-bridge.
When Some(k), a key collision whose leading k value columns are unchanged
is a no-op merge: the existing row is kept verbatim and the merge body is
NOT evaluated, so no side effects (mints/inserts) run. None (the default)
preserves the classic behavior for ordinary functions. egglog declares
every function with Some(1) so the output column alone determines merge
identity (proof/term columns are passengers) — matching normal egglog's
cur == new short-circuit.

Then remove the eq-sort-input bail in is_constructor_bodied_fd_custom so
const-prop routes to FD. rw-analysis now produces merge-val 0 in all
modes (normal/term/proofs/proof-testing), matching the shared snapshot
with NO snapshot change. The FD MergeRow/MergeIdx proof path is exercised
end-to-end on const-prop's eq-sort inputs and verifies (rc 0).

lib 38/0, files 733/0, bridge 20/0 (added a bridge test for the
short-circuit). make nits clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…alues annotation

The Phase C-2 backend identity-column merge short-circuit
(FunctionConfig::identity_values) was wired in too broadly: declare_function
set identity_values: Some(1) for EVERY function, changing merge semantics for
user functions and making a user :merge <literal> (MergeFn::Const) latently
unsound (an equal-value collision would keep the old value instead of applying
the merge body).

Scope it via a new internal function annotation :identity-values <n>:
- New Option<usize> field on Command::Function / GenericFunctionDecl, parsed
  in parse.rs and emitted by the function decl's Display so it round-trips
  through the desugar resolve->string->re-parse harness.
- proof_encoding stamps :identity-values 1 onto every generated FD view
  declaration (constructors AND FD custom views, term + proof modes).
- declare_function now reads decl.identity_values (None when absent) instead
  of the universal Some(1) -- user functions keep classic merge semantics.

Added lib test test_user_merge_not_identity_short_circuited: a user
(function counter () i64 :merge (+ old 1)) re-set with an equal value still
increments (would fail under the old universal Some(1)). Kept the backend
identity_column_short_circuit bridge test.

Snapshots doc_example_add_function{1,2} updated: the FD view decls now carry
:identity-values 1 (the annotation is explicit in the generated encoding; no
behavior change). lib 39/0, files 733/0 (rw_analysis + merge_during_rebuild +
complex_merge_func pass in all 7 modes incl _desugar_proof_testing), bridge
20/0, make nits clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Trivial value-replacement custom merges with an eq-sort OUTPUT
(`:merge old` / `:merge new` / `:merge <literal>` for a datatype output)
now route onto the FD pair-valued view like every other proof-supported
merge. `is_primitive_bodied_fd_custom` no longer bars an eq-sort output
for a bare-var/literal body (a new `merge_body_is_trivial` predicate);
such a merge just keeps one of the two colliding eq-sort outputs (no new
term, no union), so it works through the existing FD machinery
(custom_fd_view_merge / rebuild congruence / pair extraction).

With this, every proof-supported custom merge is FD, so:
- the non-FD branch in `handle_merge_or_congruence` is now `unreachable!`
  (function-reading merges -> FunctionLookupInAction; non-global
  :no-merge -> NoMergeOnNonGlobalFunction are rejected at file level);
- `handle_merge_fn` is deleted entirely, along with the now-dead
  `Justification::Merge` variant and its match arms, and the unreachable
  legacy view-decl branch in `term_and_view`.

Synthetic coverage: tests/merge-eq-sort-trivial.egg (`:merge old` and
`:merge new` over an eq-sort datatype) runs in all treatments including
proofs/proof_testing/desugar_proof_testing; verified that reverting the
predicate makes it hit the new `unreachable!`. Added lib test
`trivial_eq_sort_output_merge_is_fd`.

lib 40/0, files 740/0, egglog-bridge 20/0, make nits clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…unreachable! gap)

A custom function whose :merge READS a non-constructor function (e.g.
`:merge (foo)`) is a live-DB read — merges are pure writes — so it is not
FD-encodable and would reach the handle_merge_or_congruence unreachable!.
command_supports_proof_encoding's action-lookup check uses visit_actions,
which does not descend into a Function decl's merge body, so a proof-
eligible function-reading merge slipped past it and PANICKED at the
unreachable! instead of being cleanly rejected. Add an explicit check of
the merge body (and block-merge actions) via expr_has_function_lookup
(which flags only Custom, non-global calls, so constructor-/primitive-/
trivial-bodied merges are unaffected). Such programs now return a graceful
UnsupportedProofCommand error. Surfaced by the origin/main merge audit;
the bug predates the merge (handle_merge_fn-deletion commit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.53538% with 231 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.25%. Comparing base (7d102db) to head (7f713c7).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/lib.rs 83.58% 87 Missing ⚠️
egglog-bridge/src/rule.rs 70.52% 51 Missing ⚠️
src/proofs/proof_encoding.rs 89.25% 43 Missing ⚠️
src/proofs/proof_format.rs 81.44% 18 Missing ⚠️
egglog-bridge/src/lib.rs 91.83% 16 Missing ⚠️
src/proofs/proof_checker.rs 76.92% 12 Missing ⚠️
src/prelude.rs 0.00% 2 Missing ⚠️
src/extract.rs 97.43% 1 Missing ⚠️
src/typechecking.rs 85.71% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #933      +/-   ##
==========================================
- Coverage   86.54%   86.25%   -0.29%     
==========================================
  Files          89       89              
  Lines       26804    28059    +1255     
==========================================
+ Hits        23197    24202    +1005     
- Misses       3607     3857     +250     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codspeed-hq

codspeed-hq Bot commented Jun 24, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 12.59%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 5 regressed benchmarks
✅ 23 untouched benchmarks
⏩ 219 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation tests[proof_testing_unify] 14.9 ms 18.5 ms -19.3%
Simulation tests[proof_testing_typecheck] 123.7 ms 145.3 ms -14.85%
Simulation tests[proof_testing_eqsat-basic-proof] 23.4 ms 26.4 ms -11.4%
Simulation tests[proof_testing_eqsat-basic] 23.4 ms 26.4 ms -11.24%
Simulation tests[taylor51] 5.3 s 5.6 s -6.26%
Simulation tests[proof_testing_math] 59.3 s 14.7 s ×4

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing fd-proof-encoding (7f713c7) with main (7d102db)

Open in CodSpeed

Footnotes

  1. 219 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

oflatt and others added 4 commits June 24, 2026 21:19
A `:merge` must be a PURE WRITE: it may use old/new, call primitives, or
mint constructor terms, but it must NOT read the live value of another
(non-constructor) function -- such a read is untracked by seminaive rule
execution. egglog previously let a merge READ a non-constructor function
(e.g. `(function bar () i64 :merge (foo))` for a non-constructor `foo`):
it silently TOOK EFFECT in normal mode and PANICKED at the proof encoder's
`handle_merge_or_congruence` unreachable!. There was an analogous check for
rule bodies (LookupInRuleDisallowed) but it never ran on merges.

Add a dedicated `TypeError::LookupInMergeDisallowed(String, Span)` and check
the merge value expression + every block-merge action in `resolve_function_decl`
via the same predicate as the rule check (`expr_function_lookup`, which flags
ONLY `ResolvedCall::Func` with subtype==Custom && !is_global). So
constructor-bodied (`:merge (Ctor old new)`), primitive-bodied
(`:merge (min old new)`), and trivial (`:merge old`) merges are NOT affected.
This fires in ALL modes (normal/term/proofs) since plain typechecking runs
before proof encoding.

Revert the now-dead proof-only band-aid added to
`command_supports_proof_encoding` (the typecheck error fires first, so a
merge-read never reaches proof encoding) and update the
`handle_merge_or_congruence` unreachable! comment to cite the new typecheck
error as the guard.

Relocate the now-correctly-rejected `tests/merge_read.egg` (foo a
non-constructor `:no-merge` function, `bar :merge (foo)`) to
`tests/fail-typecheck/merge_reads_function.egg`, asserting the error message;
drop its obsolete shared snapshot (which documented the silent-accept bug) and
remove it from the proof-unsupported file list. Rename the lib test
`function_reading_merge_is_proof_unsupported` ->
`function_reading_merge_is_rejected`, now asserting normal-mode
`resolve_program` returns `LookupInMergeDisallowed`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rites)

A merge typechecks in Context::Write, so a Read/Full-context primitive has
no Write entrypoint and is rejected by the context-id filtering. Locks in
that behavior with the FullOnly (full-only) test primitive: using it in a
:merge yields UnboundFunction. Complements LookupInMergeDisallowed (which
rejects function LOOKUPS in merges) — together they enforce that merges
cannot read the database.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Trim long, blow-by-blow comments to 1-2 line intent/invariant statements and
remove ALL-CAPS emphasis words. Comments only; no behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
oflatt added a commit to oflatt/egglog-duckdb that referenced this pull request Jun 30, 2026
Port the non-proof half of egglog PR egraphs-good#933 to the FlowLog backend:
replace the rule-encoded @congruence_rule* with an FD view keyed on
children + a UnionId-style side-effecting merge that unions colliding
outputs at FD-conflict time (drained at the iteration boundary). Behind
a --native-merge flag (FlowLog-only, term mode, implies --native-uf);
off by default with byte-identical rule encoding. Bit-exact to flowlog's
rule encoding; agent-measured ~1.16x (1.28x with --fast-rebuild) faster
than --native-uf on math-microbenchmark. Also fixes a pre-existing
clone_boxed panic on --native-uf --flowlog/feldera.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
oflatt added a commit to oflatt/egglog-duckdb that referenced this pull request Jun 30, 2026
…rt/Construct (bridge, term)

Follow PR egraphs-good#933 to extend :merge with multiple actions, eliminating merge_rule
for term-building custom :merge on the bridge (term mode). Port
MergeFn::{Seq, TableInsert, Construct} to the shared egglog-backend-trait +
bridge resolve/run/fill_deps (+ TableAction::lookup_or_insert_multi). Lower the
desugared merge body to a Seq: nested constructors -> Construct, view writes ->
TableInsert (respects the target view's own merge, so nested-view congruence
resolves by construction), canon-at-creation -> Function(@UF_Sf) find-or-self.
New native_term_build_views gate (mutually exclusive with value-fold); drops
handle_merge_fn's 2 rules for these views. complex-merge-func bit-exact to the
merge_rule oracle (merge_rule gone); nested-view congruence bit-exact; no
regression (value-fold native, congruence unchanged, tuple_outputs 12/12).
Proof mode deferred (needs MergeJustification through Construct value_args).
Other backends keep merge_rule (inert arms).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

2 participants