Skip to content

Faster proof encoding: congruence via :merge + single self-referential UF (~1.8x)#6

Closed
oflatt-claude wants to merge 26 commits into
saulshanabrook:mainfrom
oflatt-claude:port-native-merge
Closed

Faster proof encoding: congruence via :merge + single self-referential UF (~1.8x)#6
oflatt-claude wants to merge 26 commits into
saulshanabrook:mainfrom
oflatt-claude:port-native-merge

Conversation

@oflatt-claude

@oflatt-claude oflatt-claude commented Jul 7, 2026

Copy link
Copy Markdown

Stacked on #3 (tuple-output functions) — review/merge that first. Until #3 merges the diff includes its commits; the encoding work here is the last 8 commits.

Brings the term/proof encoding much closer to egglog-duckdb's converged design, making proofs substantially faster. Two coordinated changes:

  1. Congruence via native :merge instead of a generated self-join rule.
  2. A single self-referential union-find per sort, replacing the two-table UF.

Result

Proofs on tests/math-microbenchmark.egg ((run 11), release):

version wall clock @rebuilding search
baseline (rule congruence, 2-table UF) ~43.3 s 13.7 s
+ congruence via :merge (2-table UF) ~36.7 s 9.6 s
+ single self-referential UF (this PR) ~23.7 s 3.8 s

≈1.83× faster than baseline. Output equivalent; full tests/files.rs suite 747/747 across all treatments (the proofs/proof_testing treatments run the proof checker, so proof validity is verified), proof unit tests pass, make nits clean.

How it works

  • View congruence is a :merge. A constructor's view is the FD tuple (children) -> (eclass, proof); on an FD conflict its :merge orients max→min and stages the congruence edge (with a composed Trans/Sym proof) into the sort's UF. The @congruence_rule self-join is gone.
  • One self-referential UF @UF : (S) -> (S, Proof) (native (values …), one table per sort). Union writes a single-key edge; the UF's own :merge keeps min and TableInserts the displaced edge back into itself, composing the transitivity proof. Single-parent is automatic (keyed by the node), so single_parent, uf_function_index, the @UF_Sf index, and the per-term self-loops are all dropped — only a proof-composing path_compress remains.
  • Rebuild is fanned out to one rule per eq-sort column ((= (values leader proof) (@UF ci))). A canonical (root) column has no @UF row, so its rule doesn't fire — which is exactly what lets us drop the self-loops, the Pair index, and any default-on-miss lookup (nothing needs find(root) to hit).
  • The encoding is self-contained: :internal-proof-names (ported from egglog #927) and :internal-uf re-register the proof constructors and the self-merge on a desugar re-parse.

Engine additions (small, general — reused everywhere)

  • egglog-bridge: general merge-action MergeFn::{Seq, TableInsert, Construct, IfEq} + lookup_or_insert_multi (following egglog #933); peek_next_function_id + an add_table reorder so a :merge can name its own table.
  • core-relations: pre-seed a merging table's own write-buffer (~16 lines) so a :merge can stage into its own table.

No DefaultVal::Identity, no backend-trait layer, no --native-uf, no dataflow backends.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for functions that return multiple values as tuples.
    • Added support for proof-related sort metadata and reserved keyword handling.
  • Bug Fixes

    • Improved handling of values expressions across parsing, type checking, execution, and proof flows.
    • Fixed self-referential merge behavior and tuple-output merging.
    • Tightened validation for reserved names and tuple-output definitions.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 96232a53-b8a4-46f7-8872-02e09b51e36b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds tuple-output function support to egglog (multi-value columns, a values tuple constructor, per-column merges) spanning parsing, typechecking, core IR, and the egglog-bridge merge engine, and reworks proof-mode union-find/congruence encoding around a single self-referential native-merge table.

Changes

Tuple outputs and proof encoding rework

Layer / File(s) Summary
AST/type contracts
egglog/src/ast/mod.rs
Schema gains extra_outputs with tuple helpers; GenericNCommand/GenericCommand::Sort gain a proof_ctors field propagated through display/mapping.
Parser support
egglog/src/ast/parse.rs
Reserved/command-only keyword enforcement, input/output call-head restriction, :internal-proof-names parsing, and tuple-output schema parsing via Schema::new_tuple.
Core IR values constructor
egglog/src/core.rs, egglog/src/exec_state.rs, egglog/src/util.rs, egglog/src/proofs/proof_checker.rs, egglog/src/proofs/proof_extraction.rs, egglog-experimental/src/primitive.rs
Adds ResolvedCall::Values, HeadOps trait, multi-value Set, and rejects values in single-value/proof contexts.
Typechecking
egglog/src/typechecking.rs, egglog/src/constraint.rs
FuncType.extra_outputs, typecheck_tuple_merge, new TypeError variants, and constraint generation for multi-output arity/groundedness.
Function declaration wiring
egglog/src/lib.rs
ResolvedSchema.extra_outputs, per-column merge placeholders, native congruence/UF self-merge construction, and merge selection in declare_function.
egglog-bridge merge engine
egglog/egglog-bridge/src/lib.rs, egglog/egglog-bridge/src/rule.rs, egglog/core-relations/src/free_join/mod.rs
SchemaMath reworked around n_keys; MergeFn/ResolvedMergeFn gain column and merge-action variants; lookup_or_insert_multi, subsume rewrite, and self-referential merge buffer seeding.
Proof encoding rework
egglog/src/proofs/proof_encoding.rs, egglog/src/proofs/proof_encoding_helpers.rs, egglog/src/proofs/proof_encoding.md
Replaces two-table UF/rulesets with a native self-merge UF and FD tuple views, plus updated headers/rulesets and documentation.
Extraction and serialization
egglog/src/extract.rs, egglog/src/serialize.rs
find_canonical supports two UF shapes; function_to_dag extracts all output columns; serialize splits rows by input arity.
Desugar/prelude/globals wiring
egglog/src/ast/desugar.rs, egglog/src/prelude.rs, egglog/src/ast/remove_globals.rs, egglog/src/ast/proof_global_remover.rs, egglog-experimental/src/fresh_macro.rs
Generated Schema/Command literals set extra_outputs/proof_ctors.
Tests and changelog
egglog/tests/tuple_outputs.rs, egglog/CHANGELOG.md
New integration tests and changelog entries for tuple outputs and reserved identifiers.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Rule as EgglogRule
  participant TableAction as TableAction
  participant MergeFn as ResolvedMergeFn
  participant Table as UF Table

  Rule->>TableAction: insert/merge row for key
  TableAction->>MergeFn: run(cur, new, n_keys, self_col, ts)
  MergeFn->>Table: TableInsert/Construct staged write
  MergeFn-->>TableAction: merged value column
  TableAction->>Table: write_table_row
Loading

Suggested reviewers: oflatt

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: faster proof encoding via native congruence merge and a single self-referential UF.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@oflatt-claude oflatt-claude changed the title Term-encoding congruence via native :merge (proof mode, ~15% faster) Faster proof encoding: congruence via :merge + single self-referential UF (~1.8x) Jul 7, 2026
oflatt and others added 2 commits July 7, 2026 20:27
Ports egraphs-good/egglog#938 ("Add tuple-output functions") into the
vendored egglog/ subtree. A function may declare more than one output
sort, stored as separate value columns (no boxing); outputs are
destructured in queries, written in actions, and merged per column with
a (values ...) clause. Restricted to plain functions and unsupported by
the term/proof encoding.

The upstream diff applied cleanly except for one context conflict in
src/proofs/proof_encoding_helpers.rs (this fork carries an extra
NaiveEqSortPrimitiveFact reason); the TupleOutputFunction variant and
guard were reapplied by hand.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The workspace root now builds egglog-experimental alongside egglog, so it must
handle the tuple-output additions from the #938 port: add `extra_outputs: vec![]`
to the fresh! constructor Schema, and a `ResolvedCall::Values(_) => Context::Pure`
arm (tuple construct/destructure reads/writes no tables).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@oflatt

oflatt commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Got this run :

th: /Users/oflatt/egglog
╭──────────────────────────────────────────────── Outcome: 321393178b22 ────────────────────────────────────────────────╮
│ <2x proof gate        [19.387x, 20.951x]  <2x not established                                                         │
│ equal-file geom mean              4.982x  descriptive                                                                 │
╰──────────────────── Within-target proof overhead. This is separate from target-vs-baseline speed. ────────────────────╯
                                     321393178b22: per-file wall time                                     
                                                                                                          
  File                                    off                  term                 proofs                
 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 
  egglog/tests/math-microbenchmark.egg    [0.4930s, 0.5161s]   [4.8234s, 4.9543s]   [10.2916s, 10.9804s]  
  egglog/tests/web-demo/rw-analysis.egg   [0.0115s, 0.0126s]   [0.0290s, 0.0296s]   [0.0372s, 0.0389s]    
  egglog/tests/integer_math.egg           [0.0081s, 0.0087s]   [0.0252s, 0.0258s]   [0.0312s, 0.0318s]    
  egglog/tests/web-demo/resolution.egg    [0.0067s, 0.0070s]   [0.0133s, 0.0147s]   [0.0164s, 0.0175s]    
                                                                                                          
Within-target wall-time estimates. These are not target-vs-baseline ratios.                               
                                   321393178b22: overhead ratios                                    
                                                                                                    
  File                                    term/off           proofs/off           proofs/term       
 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 
  egglog/tests/math-microbenchmark.egg    [9.438x, 9.951x]   [20.255x, 21.926x]   [2.100x, 2.252x]  
  egglog/tests/web-demo/rw-analysis.egg   [2.329x, 2.547x]   [3.009x, 3.319x]     [1.265x, 1.331x]  
  egglog/tests/integer_math.egg           [2.934x, 3.139x]   [3.631x, 3.876x]     [1.217x, 1.255x]  
  egglog/tests/web-demo/resolution.egg    [1.921x, 2.145x]   [2.367x, 2.570x]     [1.143x, 1.291x]  

Fix a doubled word and grammar in the `function` command doc, drop the
redundant sentence on `Schema::extra_outputs` (already covered by
`Schema::outputs`), and trim the memory-representation aside from the
CHANGELOG entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codspeed-hq

codspeed-hq Bot commented Jul 7, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 39.1%

⚡ 3 improved benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
files[resolution_proofs] 77 ms 52.6 ms +46.42%
files[integer_math_proofs] 188.4 ms 131.2 ms +43.62%
files[rw_analysis_proofs] 194.3 ms 151.8 ms +28%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing oflatt-claude:port-native-merge (ab88652) with main (d9b4001)

Open in CodSpeed

@oflatt

oflatt commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
egglog/tests/tuple_outputs.rs (1)

111-119: 🚀 Performance & Scalability | 🔵 Trivial

Consider adding relation/view-table tuple-output rejection tests.

CHANGELOG documents that tuple outputs are disallowed for constructors, relations, and view tables, but only the constructor case (constructor_tuple_output_rejected) is tested here. Adding parallel tests for relation and view-table declarations would close the coverage gap for the documented restriction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@egglog/tests/tuple_outputs.rs` around lines 111 - 119, Add test coverage for
the tuple-output restriction beyond constructor_tuple_output_rejected by adding
parallel rejection tests for relation and view-table declarations in
tuple_outputs.rs. Reuse the existing
EGraph::default().parse_and_run_program(None, ...) pattern and assert the same
TypeError::TupleOutputNotAllowed result for the relation and view-table symbols
so the documented restriction is covered consistently.
egglog/egglog-bridge/src/lib.rs (1)

548-559: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Consider a hard assert! for the reserved-table-id invariant.

The knot-tying relies on add_table_named assigning exactly the id returned by next_table_id(), and the merge callback is built against that reserved id. If this ever diverges (e.g. a future refactor adds a table between the reserve and the create), the self-referential merge would be wired to the wrong backing table and silently corrupt writes. Since debug_assert_eq! compiles out in release, the invariant is unchecked in production builds. A plain assert_eq! here is cheap insurance for a correctness-critical wiring step.

♻️ Proposed change
-        debug_assert_eq!(assigned_table_id, table_id);
+        assert_eq!(
+            assigned_table_id, table_id,
+            "reserved table id for self-referential merge diverged from assigned id"
+        );

Also applies to: 572-578

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@egglog/egglog-bridge/src/lib.rs` around lines 548 - 559, The
reserved-table-id check in the knot-tying flow is only a debug assertion, so the
invariant is unchecked in release builds. Update the
`add_table_named`/`FunctionInfo` wiring in `egglog-bridge/src/lib.rs` to use a
hard assertion (`assert_eq!`) for the `next_table_id()` versus created
function/table id match, including the same guarantee in the later
`add_table_named` merge setup referenced by the comment. This keeps the
self-referential merge callback bound to the reserved table id and prevents
silent miswiring if the ids ever diverge.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@egglog/src/lib.rs`:
- Around line 1084-1090: The TSV input parsing for custom functions is still
using only func.schema.output, so tuple-output functions can be rejected or
built with the wrong row shape. Update the input type check and the row_schema
construction in input_file to use func.schema.outputs() instead of the single
output field, and make sure the row width matches the full backend schema
registered by the function config.

---

Nitpick comments:
In `@egglog/egglog-bridge/src/lib.rs`:
- Around line 548-559: The reserved-table-id check in the knot-tying flow is
only a debug assertion, so the invariant is unchecked in release builds. Update
the `add_table_named`/`FunctionInfo` wiring in `egglog-bridge/src/lib.rs` to use
a hard assertion (`assert_eq!`) for the `next_table_id()` versus created
function/table id match, including the same guarantee in the later
`add_table_named` merge setup referenced by the comment. This keeps the
self-referential merge callback bound to the reserved table id and prevents
silent miswiring if the ids ever diverge.

In `@egglog/tests/tuple_outputs.rs`:
- Around line 111-119: Add test coverage for the tuple-output restriction beyond
constructor_tuple_output_rejected by adding parallel rejection tests for
relation and view-table declarations in tuple_outputs.rs. Reuse the existing
EGraph::default().parse_and_run_program(None, ...) pattern and assert the same
TypeError::TupleOutputNotAllowed result for the relation and view-table symbols
so the documented restriction is covered consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 929ef4c8-0ed1-46e4-b295-814acdee9c14

📥 Commits

Reviewing files that changed from the base of the PR and between d9b4001 and 61905dd.

⛔ Files ignored due to path filters (3)
  • egglog/src/proofs/snapshots/egglog__proofs__proof_tests__tests__doc_example_add_function1.snap is excluded by !**/*.snap
  • egglog/src/proofs/snapshots/egglog__proofs__proof_tests__tests__doc_example_add_function2.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__shared_snapshot_eqsat_basic_proof.snap is excluded by !**/*.snap
📒 Files selected for processing (26)
  • egglog-experimental/src/fresh_macro.rs
  • egglog-experimental/src/primitive.rs
  • egglog/CHANGELOG.md
  • egglog/core-relations/src/free_join/mod.rs
  • egglog/egglog-bridge/src/lib.rs
  • egglog/egglog-bridge/src/rule.rs
  • egglog/src/ast/desugar.rs
  • egglog/src/ast/mod.rs
  • egglog/src/ast/parse.rs
  • egglog/src/ast/proof_global_remover.rs
  • egglog/src/ast/remove_globals.rs
  • egglog/src/constraint.rs
  • egglog/src/core.rs
  • egglog/src/exec_state.rs
  • egglog/src/extract.rs
  • egglog/src/lib.rs
  • egglog/src/prelude.rs
  • egglog/src/proofs/proof_checker.rs
  • egglog/src/proofs/proof_encoding.md
  • egglog/src/proofs/proof_encoding.rs
  • egglog/src/proofs/proof_encoding_helpers.rs
  • egglog/src/proofs/proof_extraction.rs
  • egglog/src/serialize.rs
  • egglog/src/typechecking.rs
  • egglog/src/util.rs
  • egglog/tests/tuple_outputs.rs

Comment thread egglog/src/lib.rs
oflatt and others added 10 commits July 7, 2026 21:47
`input_file` built rows and validated types using only `schema.output`,
so loading a tuple-output custom function from a TSV rejected valid rows
(too many fields) and would have inserted too few columns. Use
`schema.outputs()` for both the type check and the row schema. Adds a
regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…/Construct/IfEq)

Add a general "run arbitrary actions" capability to the bridge merge language,
following egglog PR #933: MergeFn::{TableInsert, Seq, Construct, IfEq} plus
TableAction::lookup_or_insert_multi, wired through fill_deps/resolve/run. These
let a function's :merge stage a row into another table, build a value-tuple
constructor term, sequence effects, and branch — enough to express term-encoding
congruence (union edge + proof composition) as a native merge instead of a rule.

Dormant: no encoder emits these yet. Workspace builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add EGraph::native_congruence_merge, which builds the term-encoding congruence
merge for a proof-mode constructor view `(children) -> (eclass, proof)` using the
general merge-action variants: on an FD conflict it keeps the old (eclass, proof)
and stages `(@UF_S new_eclass old_eclass) = (Trans new_proof (Sym old_proof))`
into the per-sort UF table — the exact edge/proof the rule-encoded congruence
produces. declare_function calls it, falling back to the ordinary merge lowering.

Inert until the encoder emits 2-output views (next commit): the guard requires a
term-constructor view with two outputs, which nothing produces yet. Builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In proof mode each constructor's view becomes a functional-dependency tuple
`(children) -> (eclass, proof)` whose native :merge resolves congruence, instead
of the rule-encoded self-join. On an FD conflict the merge stages the congruence
edge `(@UF_S new old) = (Trans new_pf (Sym old_pf))` into the per-sort UF table
(built from the general merge-action variants) and keeps the current row; the
existing UF/path-compression/rebuild machinery is unchanged. The `@congruence_rule`
is dropped for constructors — the rebuild's re-`set` at canonical children triggers
the merge, subsuming it.

Encoder (proof_encoding.rs): FD view declaration, `update_fd_view`/`query_fd_view`,
FD-aware `add_term_and_view`, `rebuilding_rules_fd`, fact-reader destructuring, and
FD delete/subsume rules. Backend: `native_congruence_merge` builds the merge in
declare_function; serialization, extraction helpers, the delete-arity check, the
typecheck tuple-output/0-input carve-outs, and multi-output `subsume` all updated
to handle the tuple view.

Verified: all `proofs`, `proof_testing`, `term_encoding`, and normal file
treatments pass, plus the proof unit/extraction/regression suites.

KNOWN LIMITATION: the 7 `desugar_proof_testing` round-trip tests fail — the merge
is built from proof_state, so a dumped-and-re-run encoded program (fresh egraph,
no proof_state) loses the Trans/Sym/UF bindings and congruence degrades. Fix is to
carry those names in a view annotation so the program is self-contained.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port PR #927's annotation mechanism so a desugared proof program re-parses
self-contained. The Proof sort now carries :internal-proof-names <congr> <trans>
<sym>; declaring it repopulates proof_state.proof_names, and native_congruence_merge
is detected purely by view shape (term-constructor + eq-sort first output + a
second proof output) rather than a live proof_state flag. Together with the
existing :internal-uf -> uf_parent round-trip, a dumped encoded program rebuilds
the congruence merge in a fresh egraph.

Threads a proof_ctors field through the Sort command (parse/Display/desugar/
typecheck/declare), mirroring uf/proof_func.

Fixes the 7 desugar_proof_testing failures. Full files suite: 747/747 across all
treatments (normal, term_encoding, proofs, proof_testing, desugar).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In the merge_simple fast path, pre-seed the merging table's own write-buffer so a
:merge can stage a row back into the very table it is resolving. This is what a
self-referential union-find merge needs (redirect the losing edge into @UF_S
during @UF_S's own merge). Non-self-referential merges get an empty, unused
buffer; the strata path already handled this via write-deps.

Foundation for the single-table self-referential UF (no effect on its own).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add EGraph::peek_next_function_id and reorder add_table to reserve the table id
and push the new FunctionInfo BEFORE building the merge callback. This lets a
merge reference the very table it defines (e.g. the single-table UF whose :merge
does a TableInsert into itself), since merge resolution reads
self.funcs[self_id].table. Non-self-referential merges are unaffected — the new
ordering is a strict superset. Foundation for the self-referential UF (inert).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the two-table UF (@UF_S relation + @UF_Sf index) with one self-referential
function @uf : (S) -> S per sort, on the non-proof term-encoding path (proof mode
unchanged).

- Union writes a single-key edge `(set (@uf (ordering-max a b)) (ordering-min a b))`.
- @uf's :merge is native self-referential (build_uf_self_merge, from the committed
  Seq/TableInsert/IfEq actions + peek_next_function_id + core-relations self-write):
  keep min, and TableInsert the losing edge into its own table.
- Keep only path_compress; drop single_parent, uf_function_index, the @UF_Sf index,
  the per-term self-loop seed, and their schedule steps.
- Rebuild fans out to one rule per eq-sort view column (root columns don't fire, so
  no self-loop / default lookup / Pair needed).
- Extraction find_canonical chases the single-key @uf to a fixpoint (miss = self).

Validates the self-referential-merge mechanism end-to-end. Full files suite 747/747
across all treatments (normal, term_encoding, proofs, proof_testing, desugar);
proof unit tests pass; make nits clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the two-table proof UF (@UF_S (S S)->Proof relation + @UF_Sf (S)->Pair
index) with one self-referential native-tuple function @uf : (S) -> (S, Proof)
per sort, mirroring the term path.

- Union writes `(set (@uf larger) (values smaller proof))`.
- @uf's :merge is native, proof-composing self-referential (build_uf_self_merge
  proof branch): keep min eclass + its proof, and TableInsert the displaced edge
  `(@uf max) = (values min (Trans (Sym max_pf) min_pf))` into its own table.
- The constructor view's congruence merge (native_congruence_merge) now orients
  max->min and stages `(@uf max) = (values min (Trans max_vp (Sym min_vp)))` into
  the single @uf (was an unoriented edge into the relation; orientation is required
  since there is no single_parent rule to normalize).
- path_compress composes proofs `(Trans pb pc)`; single_parent, uf_function_index,
  the @UF_Sf pair index, self-loops, and their schedule steps are dropped.
- Rebuild reads the single @uf via `(= (values leader proof) (@uf ci))`, fanned out
  per eq-sort column. Extraction chases the 1-key @uf (unchanged).
- Sort re-registers its :internal-uf name in self_merge_uf_functions so a re-parsed
  desugared program reattaches the self-merge (self-contained).

Full files suite 747/747 across all treatments (proofs/proof_testing run the proof
checker); proof unit tests pass; make nits clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Update proof_encoding.md's Term Encoding section to match the current
single self-referential UF (was still describing the old two-table
UF_<Sort>/UF_<Sort>f design, single_parent/uf_function_index rulesets,
and per-term self-loops). Rewrite the proof-tracking instrumented-rule
example to the real FD-view / single-key UF shapes. Fix stale @UF_S
comment references to @uf, and correct extract.rs's find_canonical
comment to describe the two-key branch as a user-provided (child, parent)
:internal-uf rather than a proof-mode @UF_S relation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
oflatt and others added 2 commits July 10, 2026 15:48
- Add `TypeError::CannotExtractTupleOutput`, raised in the `extract`
  typecheck, so `(extract (f ...))` on a tuple-output function gives a
  clear, actionable error instead of a confusing arity mismatch.
- Correct the CHANGELOG wording on `:`-prefixed names: `:` is reserved
  as an identifier/definition name, but is still accepted in expression
  position so command macros can consume their own option markers.
- Add regression tests for tuple outputs: extract rejection, zero-input
  tuples, delete, proof-mode rejection, and unnumbered old/new merge vars.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
oflatt and others added 3 commits July 13, 2026 17:20
… merge

- `ifeq_merge_selects_branch` exercises both branches of `MergeFn::IfEq`.
- `self_referential_merge_union_find` builds a mini union-find whose merge
  writes back into its own table, covering `peek_next_function_id`,
  `MergeFn::TableInsert` into self, `Seq`, and the core-relations self-write
  buffer pre-seed in one test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Proposal (not implemented) for a follow-up: express `:merge` with the rule
action IR plus identity/payload columns and pure orientation primitives, so
the bespoke `Seq`/`TableInsert`/`Construct`/`IfEq` merge DSL and `If` can be
removed and merge typechecking becomes principled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add `FunctionConfig.n_identity_vals`: how many leading value columns are
identity (participate in FD-conflict detection); the rest are payload carried
alongside. The merge closure now keeps the existing row (skips the merge) when
a key collision leaves the identity columns unchanged. Inert for every current
table (all declare `n_identity_vals == n_vals`); this is the foundation that
lets the ported encoding merges drop their equality guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
oflatt and others added 8 commits July 13, 2026 17:50
When the identity columns are unchanged, keep the existing value columns
(skip the per-column merges) but still flow through the subsume/changed/write
logic, so a subsume-only update on a payload + subsumable function (the FD
view) is still applied instead of being dropped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`FunctionConfig.n_identity_vals` is now `Option<usize>`: `None` (the default,
used by every ordinary function) runs the merge on every collision as before,
preserving non-idempotent merge semantics; `Some(k)` opts a function into
skip-on-unchanged for its first `k` value columns. Only merges idempotent on
equal inputs should opt in — which is what lets the encoding's `@UF`/view
merges (and the term UF) drop their equality guard without changing any
ordinary function's behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The proof-mode `@UF`/view merges and the term UF no longer use a conditional
merge node:

- role 1 ("only act when the eclass/parent changed") is handled by opting the
  encoding tables into the identity-column guard (`n_identity_vals = Some(1)`),
  so the merge body runs only on a real conflict and stages unconditionally;
- role 2 (orient the composed proof) uses two pure `#`-polymorphic primitives
  `proof-of-min`/`proof-of-max` that return the proof paired with the smaller /
  larger value, resolved by name like `ordering-min`/`ordering-max`.

With no remaining users, `MergeFn::IfEq` / `ResolvedMergeFn::IfEq` are removed.
`Seq`/`TableInsert`/`Construct` still express the union-staging and proof
construction; converting those to the shared action IR is the remaining step.

Full release suite (incl. proof-checking treatments) passes with no snapshot
changes.

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

`:merge` now accepts a value-producing action block: `:merge (<action>* <expr>)`
— zero or more actions (run first, with `old`/`new`, or `old0`/`new0`/... for
tuple outputs, bound) followed by the merged-value expression. A bare
`:merge <expr>` is the no-actions case, unchanged; the wrapped form is
disambiguated by its first element (a list ⇒ action block, an atom ⇒ ordinary
result expression), so every existing merge keeps parsing as before.

- New `GenericMerge { actions, result }` replaces `Option<GenericExpr>` as the
  merge; threaded through visit/map/Display/aliases.
- Typechecking binds old/new and runs the block's actions through the SAME
  action typechecker as rule bodies (`Context::Write`) — principled, not a
  bespoke path — then typechecks the result.
- Lowering keeps the existing `MergeFn` interpreter: a result-only merge lowers
  as before; actions are lowered and sequenced before the result. Only `set`
  actions are supported for now (→ `TableInsert`); other actions error clearly,
  pending the interpreter rewrite (step 3).

make test + make nits green, no snapshot changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bespoke merge action DSL is gone. A `:merge` now lowers to a small,
action-block-shaped backend IR that mirrors the frontend `GenericMerge`:

- `MergeFn::Block { actions: Vec<MergeAction>, result }` (was `Seq`) — the
  value-producing block: run the effects once, then evaluate the per-column
  result. `to_callback` runs the actions once (skipped, with the old columns
  kept, when the identity-column guard fires) then computes the columns.
- `MergeAction::Set(f, args)` (was `TableInsert`) — stage a row into `f`.
- `MergeFn::Lookup(f, args)` (was `Construct` with empty value_args) — a
  constructor value expression (mint-on-miss), e.g. building a proof term.

Deps for the strata ordering are derived from the block (Set → write dep,
Lookup → read+write dep), preserving the read-dep behaviour. The encoding's
`@UF`/view merges (`build_uf_self_merge`, `native_congruence_merge`) now build
`Block { actions: [Set(@uf, ...)], result: Columns([...]) }` — exact semantics
preserved (full suite green, no snapshot changes, proofs bench flat).

`Let`/`Union` inside a `:merge` remain a clear error (only `set` actions are
lowered today; nothing exercises the others). The scalar value producers are
kept as the merge value language.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Whitespace-only. Fixes mis-indented n_vals/n_identity_vals in the
egglog-bridge examples (left by earlier scripted edits; make nits runs
from egglog/ and did not reach the nested example targets).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- `tests/tuple-output.egg`: a multi-output function (interval analysis,
  `(Math) -> (i64 i64)`), written/destructured with `(values ...)`.
- `tests/merge-action-block.egg`: the `:merge (<action>* <expr>)` form — a
  merge that records a discarded value via a `set` action, then keeps the max.

Both run off-only through the files.rs treatment harness: the proof encoding
already rejects tuple outputs, and this also marks a `:merge` action block
unsupported by the term/proof encoding (`ProofEncodingUnsupportedReason::
MergeActionBlock`) — it only instruments the merged value, so an action block
would be silently proof-incomplete. `file_supports_proofs` then omits both
files from the term/proofs treatments (and from the proof-support snapshot).

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

oflatt commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Pulled into #13

@oflatt oflatt closed this Jul 14, 2026
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