Skip to content

Relations-based term/proof encoding - #22

Merged
oflatt merged 65 commits into
saulshanabrook:mainfrom
oflatt-claude:relations-encoding
Jul 23, 2026
Merged

Relations-based term/proof encoding#22
oflatt merged 65 commits into
saulshanabrook:mainfrom
oflatt-claude:relations-encoding

Conversation

@oflatt-claude

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

Copy link
Copy Markdown

Rewrites the term/proof encoding from constructors to relations, makes container proofs sound, and extends proof checking to programs the old gate wrongly rejected.

The encoding

  • Terms, proofs, and ASTs are relations, not constructors: (function F (args id) Unit). E-class / proof-node ids are minted explicitly with a generic get-fresh! primitive and canonicalized into per-constructor FD "view" tables (children) -> (eclass, proof) via set-if-empty (get-or-insert on the view). Views stay canonical, so nothing needs re-keying at rebuild.
  • Children-free merge proofs (MergeFnIdx / MergeFnRow, index-based over the merge body) so a custom merge's proof doesn't need the merged term's children; custom merges run once in the FD view's own :merge.
  • Native (input …) loading: constructor/relation/custom-function inputs are loaded as data through a small backend SPI (fresh_eclass_id() + add_values(rows)) instead of a compiled loader rule.
  • Differential-dataflow backend runs the encoded program (no native union-find; congruence/rebuild lower to @UF function tables).

Container proofs

  • Fixes unsound rebuild proofs for reordering containers (Set/MultiSet/Map): rebuilds identified changed elements by their position in value order, which need not match the term's canonical child order. Rebuild proofs now use an element-matching CongrAll raw-proof form, desugared into positional Congr steps during proof conversion (user-facing proof format unchanged).
  • Containers are built over their elements' natural (as-built) ids, with each natural -> deduped equality recorded in the element's ordinary union-find — so a container's term-proof stays anchored on the shape the rule wrote (e.g. when the deduped e-class extracts as a birewrite partner), and the standard rebuild + path compression canonicalize it. This replaced an earlier UF-Aux side-table design.
  • Custom functions' container-valued outputs now rebuild (the FD view's value column canonicalizes through the container rebuild primitive; previously stale elements persisted — a soundness hole native egglog didn't have).

Proof checking coverage

  • file_supports_proofs no longer rejects programs whose (push)-scoped globals are read in actions; herbie, array, bdd, cyk, math, and typeinfer joined the term/proof-encoding test trials.
  • New Computed proof form for primitive computations in queries: proves t = t for a chain of primitive applications over literals/variables (no constructors/functions), checked structurally. Needed because BigInt/BigRat values termify as applications, not literals. Fiat is reserved for top-level equalities again.

Performance

Benchmarks vs main (bench.py, full reports below): suite wall time ~2× faster under the term encoding and ~1.5× faster under proofs (pointer-analysis ~10×, luminal --proofs 63s → 39s — the earlier luminal regression is resolved). Honest regressions: hardboiled_conv1d_32 is ~1.44× slower under proofs (many distinct small terms, little congruence to amortize the relation-row minting) and luminal peak RSS is ~2× under proofs.

Testing

  • egglog: files 783/0 (includes the new herbie/array/bdd/cyk/math/typeinfer proof trials and new container-soundness corpus files), lib 65/0, proof_mode_regression 10/10 (luminal + pointer-analysis proof checking green).
  • DD corpus: 133/0 — container-dependent files are tracked in a self-policing KNOWN_UNSUPPORTED list (DD lacks the registry-backed container-rebuild primitives; routing them through the backend SPI is a follow-up).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added backend-served term/proof encoding primitives: get-fresh!, set-if-empty, and view-proof support.
    • Enabled native loading for encoded (input ...) tables for faster ingestion.
  • Bug Fixes
    • Improved proof correctness (including container rebuild proofs) and strengthened proof determinism.
    • Fixed merge and staging behavior with correct fresh-id rollback, and improved proof-mode fail wrapping.
  • Performance
    • Reduced merge bookkeeping overhead and improved backend cleanup efficiency.
  • Documentation / Tests
    • Updated changelog and expanded proof-mode regression coverage and experimental unsupported-case expectations.

oflatt and others added 22 commits July 15, 2026 16:19
Foundation for the relation-based term/proof encoding:
- get-fresh! per-sort mint primitive (from the backend id counter)
- term tables: constructor -> relation (function ... Unit :no-merge),
  created via (let fresh (get-fresh!)) (name children fresh)
- extraction cost moved off term tables onto views via :internal-cost

Terms functionally working (term-mode file tests pass modulo snapshots).
Proof constructors still to convert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- proof-datatype + AST + prooflist constructors -> relations (function ... Unit)
- shared mint() helper (get-fresh! + set), returns the fresh var
- term_proof_for_justification emits mint sequences, returns proof var
Scattered nested proof-construction sites still to flatten.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every generated nested proof/AST/ProofList expression now builds bottom-up
via mint() (get-fresh! + relation assert). format_prooflist emits mints.
Term-mode parses/typechecks; proof-mode extraction still to update.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- extract_eq/prove_exists read the minted id from the last input column
  (is_relation_term / extraction_output_index)
- prove-exists expects a function (constructors are lowered to term relations)
- fix custom-function merge: bare-var merge result no longer wrapped into a call
- container-rebuild proof primitive mints+asserts proof relation rows instead
  of constructor lookup-or-insert
proof_mode_regression: 9/9 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduces per-view set-if-empty + view-proof primitives (proof_fresh.rs) and
threads freshly-built constructor terms through set-if-empty so parents build
with canonical children, keeping the FD views canonical (nothing for the encoded
rebuild to re-key).

Term-only mode: validated win on math-microbenchmark — @Rebuilding search+apply
2.67s -> 2.08s (-22%), total 4.45s -> 3.73s (-16%), tuples -10%. No new
regressions (term suite 98/2, both pre-existing relation-encoding snapshot regens).

Proof mode: canonicalization enabled but WIP. Deduped terms leave orphan
term-relation rows (acceptable); the remaining work is bridging the canonical
e-class back to a consumer's expected enode form via the view proof (Sym/Trans)
in prove-exists / remove_globals. Proof suite currently 151/37.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements the plan's natural-term + canonical-term + Congr + set-if-empty
threading in add_term_and_view: builds the natural term (children at as-built
ids) and canonical term (children at view-deduped ids), connects them with a
Congr chain over changed children, dedups via set-if-empty, and records
(natural, connector: natural = deduped) in nat_conn for the parent's Congr.
Returns the deduped e-class so parents/views stay canonical.

Fixes some proof tests (e.g. eqsat_basic). Remaining: the root union still
builds its Rule-proof AST from the deduped e-class (whose AST floats to a
unioned form), so it must consume the connector (Trans of rule[L=nat] with
connector[nat=dedup]); the UF-edge orientation makes that intricate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The root union now routes its proof through the operands' natural forms (whose
ASTs are pinned to the enode the rule built) instead of the deduped e-classes
(whose ASTs float once unioned), then orients to the larger=smaller UF edge with
proof-of-max/min over a shared natural form. Fixes proof checking for the
term-construction plan (the (Neg (Add a (Add b c))) example proves cleanly) and
simple rewrites. Remaining: complex/saturating rewrites where the *matched* LHS
(a body var, no natural form recorded) has itself been unioned and its AST
floats.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…en term

The canonical-children term (fv_can) was given a fresh @rule term proof, which
the checker validates against the rule head via check_rule_produces_equality.
But fv_can is built with *deduped* children, which can extract to a canonicalized
shape (a subterm rewritten elsewhere, e.g. matrix's nrows(NamedMat B) -> NamedDim
m) that no longer matches the syntactic rule head -> 'rule head doesn't produce
claimed equality'.

Per the plan, fv_can's proof should be the reflexive Trans(Sym(e-to-e'), e-to-e')
derived from the Congr chain, not a @rule -- so it is exempt from the rule-head
check and serves as the view's 'eclass = f(children)' proof. The natural term
keeps its @rule (its children are the as-matched forms, which do match the head).

Fixes matrix/naturals/fibonacci proof-testing; 'rule head' failures 14 -> 2
(remaining: luminal complex rule + pre-existing @p1 custom-merge).

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

Root cause of the proof-mode canonicalization failures on deeply-nested rules
(e.g. matrix, and the constructor parts of luminal/hardboiled_conv1d):

instrument_action's Let case bound the let variable to the built term's deduped
e-class var but did NOT carry the term's nat_conn entry onto the let name. So a
rule like `(let new-e (Bop ...)) (union e new-e)` looked up nat_conn["new-e"],
missed, and `union` fell into the no-connector branch: a bare @rule over
ordering-max/min(e, new-e) whose endpoint extracts the *deduped* (canonicalized)
shape, while process_actions builds the syntactic shape -> "rule head doesn't
produce claimed equality". Fix: propagate nat_conn from the result var to the
let-bound name.

Also keep the natural node *unseeded*: always mint a separate canonical-children
node to seed the view (never collapse to seeding fv_nat), so the natural is never
pulled into the view's congruence :merge (native UnionId) and stays as-built.
Otherwise a birewrite re-keying the view to a differently-shaped partner
(IntImm32 x <-> IntImm 32 x) natively merges the natural into the partner and its
@rule endpoint extracts the wrong shape.

Result: matrix, naturals, fib and all other constructor-based proofs prove
cleanly. Remaining: luminal still fails on its `vec-of` container (containers are
built with deduped elements and their @rule extracts the canonical shape rather
than threading the natural like constructors -- a separate follow-up), and the
pre-existing @p1 custom-merge bug is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The connector-threading fix changed the instrumented proof structure, so all
proof-testing snapshots for passing constructor-based proofs are regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nat_conn maps an instrumentation unit's freshly-minted vars (and let-bound
names) to their natural id + connector; it is local to a single generated
program. The connector-threading fix keys it by the user's let-variable names,
which repeat across rules, so without resetting it, stale entries leak between
rules/merges and can reference out-of-scope vars ("Unbound symbol @vn"). Clear
nat_conn at the start of each rule, merge function, and global action.

No current test changes result; this prevents cross-unit leaks (which a later
container change would otherwise surface).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes luminal's vec-of container proof failure with the container rebuild
itself, rather than an action-side connector.

A container built over deduped element ids has a term-proof whose endpoint
extracts the canonicalized (birewrite-partner) shape instead of the syntactic
one -> "rule head doesn't produce claimed equality" for a parent term. Building
over the *natural* (as-built) element ids fixes the proof, but naturals have no
union-find entry, so the container rebuild can't canonicalize them and the
computation breaks.

Fix: add a per-eq-sort auxiliary union-find `@UF-Aux-<Sort>` mapping a natural
id to its canonical dedup id plus the connector proof `natural = canonical`.
The container is built over natural element ids (so the term-proof extracts the
syntactic shape); at build time each element's `natural -> (canonical,
connector)` edge is written to `UF-Aux`. The Rust container rebuild
(`rebuild_container_value_rec` / `rebuild_container_proof_rec`) now consults
`UF-Aux` in addition to the main `UF`, chaining `natural ->(UF-Aux)-> canonical
->(UF)-> leader` and composing the connector with the UF proof via `Trans`. So
the rebuild canonicalizes the elements and proves it with the existing
machinery. `UF-Aux` is written only for container elements. Deterministic table
name (`@UF-Aux-<Sort>`) so the rebuild needs no extra plumbing.

Natural-element building is applied to `vec-of` only: it makes elements change
during rebuild, which triggers the rebuild's per-element `@Congr` fold, and that
fold is positional (sound only for ordered containers). Sets/maps keep the
deduped path; an order-independent rebuild proof for a birewrite-canonicalized
unordered-container element is a follow-up (no current test needs it).

Result: luminal (both variants) and all container tests prove cleanly, with no
proof-snapshot changes; only the pre-existing @p1 custom-merge failures remain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix the custom-merge (@p1) and lambda evals-to proof-encoding failures by
adopting egglog PR #933's children-free merge-proof mechanism while keeping this
branch's canonical-id (set-if-empty) encoding.

- Proof format/checker: add term-free MergeFnIdx(fn, old, new, idx) and
  MergeFnRow(fn, old, new) proof variants. The conclusion is reconstructed by
  evaluating the merge sub-expression (idx = pre-order position) on the premise
  outputs (subexpr_at_index + run_merge_subexpr), so a :merge action needs
  neither the function key nor an AST. Adds reflexivize_premise.
- Custom-merge encoding: the hidden `current` helper is now 2-column
  (Out, Proof) for eq-sort/eq-container outputs, carrying the view proof so its
  :merge builds MergeFnIdx per subterm + MergeFnRow for the row (via
  add_term_and_view / set-if-empty). No rule-scoped @p1/@p2 in the :merge.
- Reflexivize custom-function-fact rule premises: rebuild can rewrite a matched
  (= (f args) v) premise into a non-reflexive natural->canonical Congr; the
  checker's function-fact normal form requires a reflexive premise.

Proof suite: 180/8 -> 186/2. The 2 remaining (complex_merge_func, rw_analysis
proof-testing) fail only on a shared extraction-count snapshot that also differs
in term_encoding mode (no proofs) -- a separate pre-existing issue.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adopt egglog PR #933's approach for custom merge functions: the user merge runs
once, in the function's FD pair-valued view's own :merge, instead of a separate
rebuild rule + a `current` helper table. This eliminates a double-merge (the rule
fired on the view-key conflict AND `current`'s :merge fired on the current-key
conflict), which minted over-merged extra term rows -- encoded extraction now
matches normal mode. Canonical ids (set-if-empty) and the children-free
MergeFnIdx/MergeFnRow proofs are unchanged.

- Custom-with-merge functions now use the FD pair-valued view (is_fd_view /
  fd_custom_funcs). `custom_view_merge` runs the merge body once via
  instrument_merge_body (MergeFnIdx subterm proofs) + a MergeFnRow row proof,
  with NO @uf union (unlike constructor congruence).
- Deleted handle_merge_fn, the `current` helper, the rebuild rule, and the
  cleanup rules; removed the now-dead Justification::Merge variant.
- Added the `select-eq` primitive to keep the FD-view merge's proof column stable
  (reuse a premise proof when the merge is idempotent) so merges saturate.
- Routed the query (instrument_fact), write (add_term_and_view), and rebuild
  paths to the FD form for custom-with-merge functions.

Full files suite green (751 passed); proof-testing 188/0. Regenerated the
repro_equal_constant + rw_analysis proof snapshots.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Groundwork for running the relations/proofs encoding on the differential-dataflow
backend (egglog-experimental/dd):
- Fix the egglog-experimental build (set_cost.rs: add the new `cost` field to its
  Function commands).
- Unify DD's fresh-id minting onto a shared core-relations counter and add an
  eclass_id_counter() override, so the encoding's `(@get-fresh-<Sort>!)` primitive
  registers and its ids cannot collide with native FreshId ids.
- Add Database::set_counter for the DD merge-rollback path.

DD unit tests 34/34, core-relations 54/54; the non-eq-sort DD corpus passes. Full
eq-sort support still needs get-fresh!/set-if-empty exposed through the backend
SPI (so DD can service them against its host-side mirror) -- follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The term/proof encoding mints eq-sort ids via `(@get-fresh-<Sort>!)` and
canonicalizes constructor terms via `(@set-if-empty-<View>!)`. These were
`WritePrim`s that reached into core-relations db tables and were gated on the
backend having an `ActionRegistry`, so they only worked on the core-relations
reference backend. Expose them as backend operations instead:

- Add `Backend::register_get_fresh` / `register_set_if_empty` /
  `register_view_proof`. `register_get_fresh` has a uniform default minting from
  the backend's eq-class id counter; the other two default to a loud panic and
  are overridden per backend.
- The reference backend (egglog-bridge) implements set-if-empty/view-proof over
  its tables (behavior-preserving). The differential-dataflow backend implements
  them over its host-side `mirror` (intercepted in both `apply_head` and inside a
  view's `:merge` transaction).
- Route the encoder's primitives (proof_fresh.rs) through the SPI via a new
  `add_backend_op_primitive`, dropping the `action_registry()` gate for these
  three ops (retained for genuine registry reads, still unsupported on DD).

Reference backend: full `files` suite 756/0 (no regressions). DD backend now runs
eq-sort programs under term/proof encoding (full DD corpus 131/0; unit 34/34;
core-relations 54/54).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two fixes surfaced while validating the backend/merge work:

- prove_exists panicked ("expected rule justification for existence proof") when
  a constructor's witness row had a non-rule existence proof. A valid existence
  proof need not be rule-justified (it may be Fiat/Merge/Congr); the rule case is
  only an optimization to strip a single-premise wrapping rule. Use the proof
  as-is otherwise (check_proof still validates it). This was order-dependent: the
  witness is a constructor's first row, so a backend iterating rows in a different
  order (the differential-dataflow backend's HashSet mirror) surfaced a non-rule
  row and panicked nondeterministically. It also fixed container_rebuild (4 tests
  hit it deterministically on the reference backend: container witnesses have
  ContainerNormalize/Congr existence proofs) -> container_rebuild 16/0.

- Custom-with-merge FD views with an eq-sort output now canonicalize the output
  column when it is unioned to a smaller leader, via delete+set (re-setting the
  same key would re-run the user merge). Without this, a custom eq-sort output
  later unioned stayed non-canonical and `check` could miss it. (This block was
  authored during the merge re-architecture but landed after that commit.)

files suite 756/0; container_rebuild 16/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the per-sort `@get-fresh-<Sort>!` mint primitives with one generic
`get-fresh!` that takes the target sort as a string literal: `(get-fresh! "Math")`.
Its runtime ignores the arg (the shared eq-class id counter is sort-agnostic); the
string only types the output. This gives the desugared program a stable,
always-registered name instead of per-sort fresh names that a name-sanitizer
mangles on re-parse (so a plain e-graph can re-parse encoded output).

The type constraint has dual behavior: at type-check time it reads the string
literal and assigns the output eq-sort; at resolution/`accept` time (which runs
constraints over placeholder int literals, not the real string) it just requires
the first arg to be a String, letting the output sort come from the resolved
types. Registered once, idempotently, from the first eq-sort declaration.

files suite 756/0. (set-if-empty/view-proof are still per-view — a follow-up;
the eqsat_basic roundtrip needs those generic too.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
These per-view mint/canonicalize primitives were named `@set-if-empty-<view>!`
and `@view-proof-<view>`. The `@` is an internal-symbol marker, so a name
sanitizer (e.g. the term-encoding roundtrip test) rewrites it — but the
primitive is re-registered by re-deriving the name with the `@` intact, so the
sanitized reference (`___set-if-empty-...`) no longer resolves to the registered
`@set-if-empty-...`, and re-parsing the desugared program fails with
UnboundFunction.

Drop the `@`: the stable `set-if-empty-`/`view-proof-` prefix now carries no
marker (so the sanitizer leaves it alone), while the embedded view name — a
fresh internal symbol — is rewritten identically at both the reference and the
re-registration, so the primitive stays resolvable. This restores
self-containedness of the desugared program (a plain e-graph can re-parse it).

Fixes eqsat_basic_term_encoding_roundtrip. files 756/0, integration_test 48/0,
container_rebuild 16/0, proof_mode_regression 9/0.

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

In proof/term mode, `(input …)` lowered to one top-level fiat action per CSV row,
and term-encoding expanded each into ~40 top-level `(let @v …)` globals.
`proof_global_remover` turns every global into its own Constructor function, so
loading N input facts created ~O(N) functions and ran ~O(N) one-shot rules —
super-linear in practice (pointer-analysis proofs: ~9.2s, 380 MiB peak).

Now each `(input …)` lowers, for execution, to a single bodyless rule whose head
holds all rows' fiat actions, run once. The mints become local rule-head lets, so
no per-mint function is created and the input loads in one rule execution. The
head is instrumented with `Fiat`, so proofs are unchanged and no loader rule ever
appears in a proof; the proof checker keeps using the per-row top-level form
(`desugared_before_proofs`).

pointer-analysis-small: reference proofs 9.2s→0.90s, DD proofs 6.3s→1.25s; term
and peak RSS also improved. files 756/0, proof_mode_regression 9/0, and the other
proof suites stay green; `--proof-testing` passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In term/proof mode, `(input …)` now inserts the encoded term/view (and, in proof
mode, AST/proof) rows straight into the backend, rather than compiling and running
a rule. Adds a small backend SPI — `fresh_eclass_id()` + `add_values(rows)` —
serviced by each backend against its own storage (reference-bridge db buffers; the
DD host-side mirror, with no fallback). `EGraph::native_input` mints a term id per
row and plain-inserts the term relation, FD view, and (proof mode)
`@Ast`/`@Fiat`/`@…Proof` rows via `add_values`; duplicate view keys are resolved by
the view `:merge`, so there is no get-or-insert.

Only constructor-subtype functions take the native path — constructors and
relations (which desugar to constructors). Plain custom functions (with or without
`:merge`, including `:no-merge` Unit-output ones) still go through the loader rule;
`lower_inputs_for_execution` routes by subtype. The proof checker keeps consuming
the per-row top-level fiat actions (`desugared_before_proofs`), so proofs are
unchanged.

pointer-analysis-small proofs: 0.90s→0.098s (reference), 1.25s→0.47s (DD); term
0.17s→0.055s. files 756/0, proof_mode_regression (non-luminal) green, DD corpus
unchanged, and `--proof-testing` passes on both backends.

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

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds backend SPI support for shared fresh-id counters, native encoded input loading, set-if-empty, and view-proof. It also updates term/proof encoding, FD-view merges, container rebuild proofs, extraction, function costs, proof validation, and related bookkeeping.

Changes

Term/proof backend integration

Layer / File(s) Summary
Backend SPI, counters, and view storage
egglog/core-relations/..., egglog/egglog-backend-trait/..., egglog/egglog-bridge/..., egglog-experimental/dd/...
Backend hooks support shared counters, fresh ids, direct row loading, and staged or committed view operations.
Function contracts, extraction, and native input
egglog/src/ast/..., egglog/src/extract.rs, egglog/src/lib.rs, egglog-experimental/src/set_cost.rs
Function costs and multi-command fail forms propagate through parsing and transformations; extraction recognizes relation-term layouts, and encoded inputs load through backend rows.
Term and proof encoding pipeline
egglog/src/proofs/proof_encoding*.rs, egglog/src/proofs/proof_fresh.rs, egglog/src/typechecking.rs
Proof encoding adds connector tracking, minted proof rows, FD-view merge handling, canonicalization, fact instrumentation, and backend-registered primitives.
Proof validation, rebuilds, and supporting updates
egglog/src/proofs/..., egglog/tests/..., egglog/CHANGELOG.md, egglog-experimental/dd/tests/...
Merge justifications, container rebuild proofs, deterministic extraction, regression coverage, controlled diagnostics, and documentation are updated.

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

Possibly related PRs

Suggested reviewers: saulshanabrook

🚥 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 shift from constructor-based to relation-based term/proof encoding.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

@codspeed-hq

codspeed-hq Bot commented Jul 19, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 47.61%

❌ 6 regressed benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory files[rw_analysis_proofs] 10 MB 22.2 MB -55.03%
Memory files[resolution_proofs] 3.7 MB 7.7 MB -51.8%
Simulation files[rw_analysis_proofs] 171.7 ms 350.3 ms -50.98%
Simulation files[resolution_proofs] 59.8 ms 108.4 ms -44.84%
Memory files[integer_math_proofs] 8.8 MB 15.6 MB -43.49%
Simulation files[integer_math_proofs] 140.9 ms 225.8 ms -37.59%

Tip

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


Comparing oflatt-claude:relations-encoding (42e8402) with main (8f23213)

Open in CodSpeed

@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: 2

🤖 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-experimental/dd/src/lib.rs`:
- Around line 2401-2413: Update add_values to submit each function/row through
the existing merge-aware transaction path, such as apply_sets, instead of
calling insert_live_row directly. Preserve conversion of each Value to its
representative Row and ensure duplicate keys are collapsed according to the
view’s :merge behavior when loaded.

In `@egglog/src/lib.rs`:
- Around line 2666-2668: Remove the duplicate public zz_report accessor and
reuse the existing get_overall_run_report method at its call sites; if an
accessor is still required, rename it to a meaningful documented API instead of
exposing the zz_ debug-style name.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 21d66595-b5ce-4b83-965d-899c8eeeddb7

📥 Commits

Reviewing files that changed from the base of the PR and between ce6a69f and 5e15bb7.

⛔ Files ignored due to path filters (30)
  • Cargo.lock is excluded by !**/*.lock
  • egglog/tests/snapshots/files__proofs__antiunify_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__birewrite_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__calc_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__combinators_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__commute_collapse_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__container_proofs_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__container_set_collapse_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__eqsat_basic_proof_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__eqsat_basic_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__fibonacci_demand_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__filter_or_bool_neq_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__integer_math_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__intersection_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__matrix_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__merge_during_rebuild_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__naturals_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__nested_container_dirty_propagation_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__path_union_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__repro_define_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__repro_equal_constant_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__repro_noteqbug_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__repro_small_rebuild_fail_term_encoding_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__repro_typecheck_term_encoding_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__rule_head_fast_path_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__rw_analysis_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__typecheck_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__unification_points_to_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__unify_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__until_proof_testing.snap is excluded by !**/*.snap
📒 Files selected for processing (27)
  • egglog-experimental/dd/src/interpret.rs
  • egglog-experimental/dd/src/lib.rs
  • egglog-experimental/dd/tests/files.rs
  • egglog-experimental/src/set_cost.rs
  • egglog/CHANGELOG.md
  • egglog/core-relations/src/free_join/mod.rs
  • egglog/egglog-backend-trait/Cargo.toml
  • egglog/egglog-backend-trait/src/backend_impl.rs
  • egglog/egglog-backend-trait/src/lib.rs
  • egglog/egglog-bridge/src/lib.rs
  • egglog/src/ast/desugar.rs
  • egglog/src/ast/mod.rs
  • egglog/src/ast/parse.rs
  • egglog/src/extract.rs
  • egglog/src/lib.rs
  • egglog/src/prelude.rs
  • egglog/src/proofs/mod.rs
  • egglog/src/proofs/proof_checker.rs
  • egglog/src/proofs/proof_container_rebuild.rs
  • egglog/src/proofs/proof_encoding.rs
  • egglog/src/proofs/proof_encoding_helpers.rs
  • egglog/src/proofs/proof_extraction.rs
  • egglog/src/proofs/proof_extractor.rs
  • egglog/src/proofs/proof_format.rs
  • egglog/src/proofs/proof_fresh.rs
  • egglog/src/typechecking.rs
  • egglog/tests/proof_mode_regression.rs

Comment thread egglog-experimental/dd/src/lib.rs Outdated
Comment thread egglog/src/lib.rs Outdated
oflatt and others added 5 commits July 20, 2026 00:16
…coding

Switch the term/proof encoding to the function-style `remove_globals` pass:
a global `(let x e)` becomes a nullary `:internal-let` function `set` to its
value, instead of a constructor plus a top-level `union`. The global now gets
a functional-dependency view and rebuild rules like any other function, and
references become the lookup `(x)`.

- Encoder: globals are `output_is_eclass` (their output value *is* their
  e-class, like a constructor), so the term relation has no separate output
  column and the view is the congruence FD `() -> (eclass, proof)`. Added a
  global-lookup code path for actions/facts (the one custom lookup proof normal
  form allows) and a global-`set` path that stores value+proof in the view.
- Rebuild: route globals to the constructor-style e-class rebuild (which
  composes the union edge into the view proof) instead of the custom-output
  congruence rebuild, which emitted a nonsensical `Congr` on a nullary term and
  broke proof reconstruction (transitivity middle-term mismatch).
- Skip the post-command rebuild after every non-`union` top-level action: a
  `let`/`set`/insert aliases or dedups under a fresh view key and can't merge
  e-classes, so N global definitions no longer trigger N rebuilds. Any
  non-canonical id is fixed by the next real rebuild, as native defers it.
- Remove the now-unused `proof_global_remover` pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`Database::merge_all` reset the cached column/key indexes of *every* table and
re-summed every table's length on each call, regardless of which tables actually
changed. That is O(all tables) per call, and quadratic when many small tables
each trigger a merge — e.g. a long run of global-`let` definitions under the
term/proof encoding, where each global adds a one-row view table and runs a
one-shot action (→ merge_all). A 1600-global proof chain did ~1.1 billion
table-index resets.

Track the tables modified during the call (the notification list, accumulated
here and in `merge_simple`) and reset only those. Unmodified tables keep their
valid cached indexes — their table version is unchanged, so `Index::refresh`
would be a no-op anyway (same reasoning as `clear_table`). `total_size_estimate`
is maintained incrementally at each merge (mirroring `merge_table`) instead of
re-summed. Same 1600-global chain now does ~62k resets (~18,000x fewer); proof
time roughly halves and the remaining growth is the per-one-shot-action overhead,
not merge_all.

Correctness: core-relations 51/51, egglog + egglog-experimental suites show no
new failures (only pre-existing snapshot diffs), proof-mode file sweep identical
to baseline.

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

`free_external_func` scanned the entire `panic_funcs` map on every call
(`retain` looking for the entry whose `id` matches), because the map is keyed by
message but freed by id. That scan ran even for non-panic external functions
(e.g. the term/proof encoding's `get-fresh!` / `set-if-empty`), and `panic_funcs`
grows with the program, so freeing the many short-lived one-shot action rules —
one set per encoded global definition, ~38 per global — was O(panic_funcs) each,
i.e. quadratic overall. On a 4000-global proof chain this was the dominant cost.

Add a reverse `id -> message` index so a free finds its cache entry (or
determines the func is not a cached panic) in O(1). The index is a `BTreeMap` so
it introduces no new randomly-seeded hasher and leaves the seed sequence — and
thus other tables' iteration order, which order-dependent proof extraction reads
— unchanged.

Perf: 4000-global proof chain 37s -> 12s (on top of the merge_all fix); scaling
is now near-linear. Correctness: proof-mode file sweep identical to baseline,
`cargo test -p egglog` unchanged (only the 3 pre-existing snapshot diffs). The
free/keep semantics are preserved exactly: a cached panic with >1 reference is
decremented; its last reference, or any non-cached func, is freed from the db.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`prove_exists` took whichever row the backend yielded first as the witness, and
the root extractor reconstructed from the first matching row. On a backend with
nondeterministic row order — the differential-dataflow backend mirrors each
relation in a `hashbrown::HashSet`, whose iteration order varies per process —
the extracted (always-valid) existence proof therefore varied run to run, so the
`*_proof_testing` snapshot tests were flaky (~40% failure independent of any
recent change).

Select the lexicographically-smallest matching row in both places instead. The
proof is unchanged in validity; it is just now the same one every run. This
removes the flakiness: the full `egglog-experimental` proof suite is now 46/0
deterministically (was ~40-41 passing with 5-6 flaky). Regenerated the one
snapshot (`with_ruleset`) whose stored proof reflected an old nondeterministic
witness; its proof still checks (`--proof-testing` passes).

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

Three `proofs::proof_tests` tests were stale after the encoding moved terms and
proof constructors from egglog constructors to relations (`get-fresh!` mint +
`set-if-empty` view) and globals to function style:

- doc_example_add_function1/2: regenerated the encoding snapshots (now show the
  `(function Add (..) Unit)` term relation, `get-fresh!`/`set-if-empty` minting,
  and the function-style global) — the intended output, verified by reading the
  diff.
- proof_encoding_hoists_unnamed_rule_name_in_actions: `@Rule` proofs are now
  emitted as `(set (@rule ..) ())` actions rather than call expressions, so the
  `visit_exprs` count saw zero. Count the `@Rule` set actions instead; they still
  reuse the hoisted rule-name variable and there is more than one, so the test's
  intent is preserved. (The program's proof already checks under `--proof-testing`.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
oflatt and others added 2 commits July 21, 2026 20:14
The new corpus file exercises eq-container rebuilds, which need the
registry-backed rebuild primitives DD does not service.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The FD-view value-column rebuild only handled eq-sort outputs, so a
custom function's container output kept stale elements after unions
(native egglog rebuilds them). Add a ValueRebuild::ContainerOutput rule:
canonicalize the output with the container rebuild primitive (:naive),
delete-then-reinsert the row so the user merge does not rerun, compose
the row proof with a Congr at the output position, and anchor the
rebuilt container's reflexive <CSort>Proof.

On DD this newly emits the registry-backed rebuild primitive for such
programs, so repro-querybug3 returns to the known-unsupported list
alongside the new corpus file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@oflatt-claude

Copy link
Copy Markdown
Author

Benchmark Report

Comparison

Role Target Git Backend Treatment
Baseline 1cc0e8a 1cc0e8a main off
Candidate a453635 a453635 main off

6 file(s): math-microbenchmark.egg, eggcc-2mm-pass1.egg, pointer-analysis-small.egg (facts: /home/oflatt/work/egglog1/benchmarks/data/pointer-analysis-small), hardboiled_conv1d_32.egg, luminal-llama.egg, herbie.egg · 6 round(s) per endpoint/file · 300 s timeout per run · Report: /tmp/claude-1004/-home-oflatt-work-egglog1/6e62aa7f-4d6f-433c-8acb-b316f61c744c/scratchpad/reports.jsonl

Summary — a453635 main/off vs 1cc0e8a main/off

Metric Scope File(s) Ratio (95% CI) Result
Wall time Suite total 6 files 0.902–1.03x CI includes 1
Wall time Lowest-ratio file hardboiled_conv1d_32.egg 0.836–1.03x CI includes 1
Wall time Highest-ratio file math-microbenchmark.egg 0.923–1.18x CI includes 1
Peak RSS Lowest-ratio file eggcc-2mm-pass1.egg 1.00–1.00x CI includes 1
Peak RSS Highest-ratio file math-microbenchmark.egg 1.00–1.04x higher RSS

Ratios are candidate / baseline; below 1 is lower and above 1 is higher.


Benchmark Report

Comparison

Role Target Git Backend Treatment
Baseline 1cc0e8a 1cc0e8a main term
Candidate a453635 a453635 main term

6 file(s): math-microbenchmark.egg, eggcc-2mm-pass1.egg, pointer-analysis-small.egg (facts: /home/oflatt/work/egglog1/benchmarks/data/pointer-analysis-small), hardboiled_conv1d_32.egg, luminal-llama.egg, herbie.egg · 6 round(s) per endpoint/file · 300 s timeout per run · Report: /tmp/claude-1004/-home-oflatt-work-egglog1/6e62aa7f-4d6f-433c-8acb-b316f61c744c/scratchpad/reports.jsonl

Summary — a453635 main/term vs 1cc0e8a main/term

Metric Scope File(s) Ratio (95% CI) Result
Wall time Suite total 6 files 0.487–0.519x faster
Wall time Lowest-ratio file pointer-analysis-small.egg 0.105–0.130x faster
Wall time Highest-ratio file herbie.egg 0.864–0.988x faster
Peak RSS Lowest-ratio file luminal-llama.egg 0.675–0.679x lower RSS
Peak RSS Highest-ratio file eggcc-2mm-pass1.egg 1.05–1.07x higher RSS

Ratios are candidate / baseline; below 1 is lower and above 1 is higher.


Benchmark Report

Comparison

Role Target Git Backend Treatment
Baseline 1cc0e8a 1cc0e8a main proofs
Candidate a453635 a453635 main proofs

6 file(s): math-microbenchmark.egg, eggcc-2mm-pass1.egg, pointer-analysis-small.egg (facts: /home/oflatt/work/egglog1/benchmarks/data/pointer-analysis-small), hardboiled_conv1d_32.egg, luminal-llama.egg, herbie.egg · 6 round(s) per endpoint/file · 300 s timeout per run · Report: /tmp/claude-1004/-home-oflatt-work-egglog1/6e62aa7f-4d6f-433c-8acb-b316f61c744c/scratchpad/reports.jsonl

Summary — a453635 main/proofs vs 1cc0e8a main/proofs

Metric Scope File(s) Ratio (95% CI) Result
Wall time Suite total 6 files 0.659–0.683x faster
Wall time Lowest-ratio file pointer-analysis-small.egg 0.0850–0.106x faster
Wall time Highest-ratio file hardboiled_conv1d_32.egg 1.43–1.45x slower
Peak RSS Lowest-ratio file math-microbenchmark.egg 0.966–0.975x lower RSS
Peak RSS Highest-ratio file luminal-llama.egg 2.04–2.04x higher RSS

Ratios are candidate / baseline; below 1 is lower and above 1 is higher.

@oflatt
oflatt marked this pull request as ready for review July 21, 2026 21:39
Copilot AI review requested due to automatic review settings July 21, 2026 21:39
The dd crate's output.*.log files are written by (output ...) commands
during its corpus tests; untrack and ignore them so test runs stop
churning the diff. Reformat egglog-bridge/core-relations to pass the
root `cargo fmt --all --check` that CI runs (the egglog-scoped check
missed these files).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

This PR overhauls egglog’s term/proof encoding to represent terms and proofs as relations (not constructors), adds backend SPI hooks to support the new mint/canonicalization operations across backends, and introduces native (input …) loading for major proof-mode performance gains.

Changes:

  • Re-encode terms/proofs as relation rows with explicit id minting (get-fresh!) and canonicalization via FD “view” tables (set-if-empty + view-proof reads).
  • Load encoded (input …) facts natively into backend storage (bypassing generated loader rules) and make proof extraction deterministic across nondeterministic row-order backends.
  • Update proof/container rebuild machinery (including new CongrAll) and refresh/expand regression + snapshot coverage.

Reviewed changes

Copilot reviewed 75 out of 77 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
egglog/tests/snapshots/files__shared_snapshot_custom_container_output_rebuild.snap New shared snapshot output for custom container rebuild scenario.
egglog/tests/snapshots/files__shared_snapshot_container_reorder_proofs.snap New shared snapshot output for container reorder proof scenarios.
egglog/tests/snapshots/files__proofs__until_proof_testing.snap Updated proof snapshot due to relation-based proof encoding changes.
egglog/tests/snapshots/files__proofs__unify_proof_testing.snap Updated proof snapshot reflecting new proof term shapes.
egglog/tests/snapshots/files__proofs__unification_points_to_proof_testing.snap Updated proof snapshot for points-to/unification example under new encoding.
egglog/tests/snapshots/files__proofs__rule_head_fast_path_proof_testing.snap Updated proof snapshot for rule-head fast path under new proof term form.
egglog/tests/snapshots/files__proofs__repro_typecheck_term_encoding_proof_testing.snap Updated proof snapshot for term-encoding typecheck repro.
egglog/tests/snapshots/files__proofs__repro_small_rebuild_fail_term_encoding_proof_testing.snap Updated proof snapshot for small rebuild-failure repro.
egglog/tests/snapshots/files__proofs__repro_noteqbug_proof_testing.snap Updated proof snapshot for noteq bug repro.
egglog/tests/snapshots/files__proofs__repro_equal_constant_proof_testing.snap Updated proof snapshot for equal-constant repro.
egglog/tests/snapshots/files__proofs__repro_define_proof_testing.snap Updated proof snapshot for define repro.
egglog/tests/snapshots/files__proofs__path_union_proof_testing.snap Updated proof snapshot for path/union example (symmetry handling changes).
egglog/tests/snapshots/files__proofs__nested_container_dirty_propagation_proof_testing.snap Updated proof snapshot for nested container dirty propagation.
egglog/tests/snapshots/files__proofs__matrix_proof_testing.snap Updated proof snapshot for matrix example and determinism changes.
egglog/tests/snapshots/files__proofs__intersection_proof_testing.snap Updated proof snapshot for intersection example under relation-based encoding.
egglog/tests/snapshots/files__proofs__integer_math_proof_testing.snap Updated proof snapshot for integer math example.
egglog/tests/snapshots/files__proofs__filter_or_bool_neq_proof_testing.snap Updated proof snapshot IDs/substitutions after encoding changes.
egglog/tests/snapshots/files__proofs__fibonacci_demand_proof_testing.snap Updated proof snapshot for fib demand due to new proof construction layout.
egglog/tests/snapshots/files__proofs__eqsat_basic_proof_testing.snap Updated proof snapshot for eqsat basic example.
egglog/tests/snapshots/files__proofs__eqsat_basic_proof_proof_testing.snap Updated proof snapshot for eqsat basic “proof of proof” case.
egglog/tests/snapshots/files__proofs__custom_container_output_rebuild_proof_testing.snap New/updated proof snapshot covering custom container output rebuild.
egglog/tests/snapshots/files__proofs__container_set_collapse_proof_testing.snap Updated proof snapshot for set collapse under new proof primitives.
egglog/tests/snapshots/files__proofs__container_proofs_proof_testing.snap Updated container proofs snapshot for new relation-based proof terms.
egglog/tests/snapshots/files__proofs__commute_collapse_proof_testing.snap Updated commute/collapse proof snapshot.
egglog/tests/snapshots/files__proofs__calc_proof_testing.snap Updated calc proof snapshot reflecting new Rule term form.
egglog/tests/snapshots/files__proofs__birewrite_proof_testing.snap Updated birewrite proof snapshot under relation-based encoding.
egglog/tests/snapshots/files__proofs__antiunify_proof_testing.snap Updated antiunify proof snapshot.
egglog/tests/snapshots/files__proof_unsupported_files.snap Marks lambda.egg as proof-unsupported in snapshots list.
egglog/tests/proof_mode_regression.rs Adjusts regression tests for updated :no-merge/fail semantics and adds new container-proof regression.
egglog/tests/eggcc-2mm.egg Rewrites several :no-merge functions to :merge old to match encoding support.
egglog/tests/custom-container-output-rebuild.egg New test program for container-valued custom output rebuild behavior.
egglog/tests/container-reorder-proofs.egg New test program covering container rebuild proofs when container iteration order differs from canonical term order.
egglog/src/typechecking.rs Adds backend-op primitive registration helper; updates fail typing; loosens prove-exists target constraints.
egglog/src/proofs/snapshots/egglog__proofs__proof_tests__tests__doc_example_add_function2.snap Updates proof tests snapshot to relation-based term/proof encoding form.
egglog/src/proofs/snapshots/egglog__proofs__proof_tests__tests__doc_example_add_function1.snap Updates proof tests snapshot to relation-based term/proof encoding form.
egglog/src/proofs/proof_tests.rs Updates internal proof tests to count Rule proof relation insertions rather than constructor calls.
egglog/src/proofs/proof_fresh.rs New module defining/typing get-fresh!, set-if-empty, and view-proof primitives routed via backend SPI.
egglog/src/proofs/proof_extractor.rs Supports reconstruction from relation terms and makes selection deterministic by sorting matching rows.
egglog/src/proofs/proof_extraction.rs Makes prove-exists deterministic by choosing lexicographically smallest witness row and handling relation-term output indexing.
egglog/src/proofs/proof_encoding_helpers.rs Introduces CongrAll, split merge-proof constructors, proof-list minting changes, and new proof encoding constraints/ops.
egglog/src/proofs/proof_encoding_facts.rs New query-side instrumentation module for proof encoding fact handling.
egglog/src/proofs/proof_container_rebuild.rs Updates container rebuild proof minting for relation-based proof terms and adds CongrAll-based element matching.
egglog/src/proofs/proof_checker.rs Exposes eval_expr_with_subst for reuse in encoding components.
egglog/src/proofs/mod.rs Wires in new proof encoding modules (proof_fresh, proof_encoding_facts, etc.).
egglog/src/prelude.rs Updates function declaration helper to include new cost field.
egglog/src/lib.rs Adds native encoded (input …) loading path, multi-command fail, and supporting input parsing refactor.
egglog/src/extract.rs Updates extraction helpers for FD views/encoding relations and cost propagation via :internal-cost.
egglog/src/ast/remove_globals.rs Updates global removal to handle multi-command fail correctly.
egglog/src/ast/proof_global_remover.rs Removes obsolete proof-specific global remover (now unified under remove_globals).
egglog/src/ast/parse.rs Extends parsing for multi-command fail and :internal-cost; updates proof-name metadata parsing.
egglog/src/ast/mod.rs Updates AST model for multi-command fail, extended proof constructor metadata, and function cost.
egglog/src/ast/desugar.rs Desugars multi-command fail by wrapping the full flattened expansion.
egglog/src/ast/check_shadowing.rs Extends shadowing checks to iterate over multi-command fail.
egglog/egglog-bridge/src/lib.rs Adds O(1) reverse index for cached panic functions; adds backend support for set-if-empty and view-proof ops.
egglog/egglog-backend-trait/src/lib.rs Extends backend trait with counter access, native input batch insertion, and term-encoding SPI registrations.
egglog/egglog-backend-trait/src/backend_impl.rs Implements new backend-trait methods for the reference backend.
egglog/egglog-backend-trait/Cargo.toml Adds egglog-numeric-id dependency required by updated backend trait.
egglog/core-relations/src/free_join/mod.rs Optimizes merge_all by resetting indexes only for touched tables and adds counter overwrite support.
egglog/CHANGELOG.md Documents encoding rewrite highlights, performance wins, and behavioral changes (fail, no-merge support, determinism).
egglog-experimental/tests/snapshots/files__proofs__with_ruleset_proof_testing.snap Updates experimental snapshot to new proof term form.
egglog-experimental/tests/fixtures/eggcc-2mm-pass1.egg Rewrites no-merge declarations to :merge old for encoding compatibility.
egglog-experimental/tests/eggcc_2mm_proof.rs Updates assertions to match rewritten no-merge behavior.
egglog-experimental/src/set_cost.rs Updates macro-generated declarations to include the new cost field.
egglog-experimental/dd/tests/files.rs Updates known-unsupported list and comments for DD backend coverage changes.
egglog-experimental/dd/src/lib.rs Shares id minting counter with get-fresh! and adds mirror-based support for set-if-empty/view-proof ops and native input.
egglog-experimental/dd/src/interpret.rs Intercepts set-if-empty/view-proof primitives to service them against the DD mirror during interpretation.
egglog-experimental/dd/output.R3.log New DD output fixture for relation output rendering.
egglog-experimental/dd/output.R3.csv.log New DD CSV output fixture for relation output rendering.
egglog-experimental/dd/output.R.log New DD output fixture for relation output rendering (variant).
egglog-experimental/dd/output.R.csv.log New DD CSV output fixture for relation output rendering (variant).
egglog-experimental/dd/output.G3.log New DD output fixture for G output rendering.
egglog-experimental/dd/output.G3.csv.log New DD CSV output fixture for G output rendering.
egglog-experimental/dd/output.G.log New DD output fixture for G output rendering (variant).
egglog-experimental/dd/output.G.csv.log New DD CSV output fixture for G output rendering (variant).
Cargo.lock Adds egglog-numeric-id to lockfile due to backend-trait dependency update.
Comments suppressed due to low confidence (1)

egglog/src/lib.rs:2264

  • read_input_file panics on unsupported column sorts. Since this is triggered by user-provided programs (e.g., (input ...) targeting a schema with an unexpected sort), it should return a structured Error instead of aborting the process.
        for sort in &function_type.input {
            match sort.name() {
                "i64" | "f64" | "String" => {}
                name => panic!("Unsupported type {name} for input"),
            }
        }
        if function_type.subtype != FunctionSubtype::Constructor {
            for sort in &function_type.outputs {
                match sort.name() {
                    "i64" | "String" | "Unit" => {}
                    name => panic!("Unsupported type {name} for input"),
                }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread egglog/src/lib.rs
Comment on lines 2304 to 2316
let literal = match sort.name() {
"i64" => raw
.parse()
.map(Literal::Int)
.map_err(|_| Error::InputFileFormatError(file.to_owned()))?,
"f64" => raw
.parse::<f64>()
.map(ordered_float::OrderedFloat)
.map(Literal::Float)
.map_err(|_| Error::InputFileFormatError(file.to_owned()))?,
"String" => Literal::String(raw.to_owned()),
_ => unreachable!(),
name => panic!("Unsupported type {name} for input"),
};
file_supports_proofs checked every command against the final popped
scope, so a global defined in a (push)/(pop) block and read in an
action looked like an unsupported function lookup. Collect the
program's let-bound names up front and treat them as globals. Six
corpus files (herbie, array, bdd, cyk, math, typeinfer) join the
term/proof-encoding trials.

Herbie then exposed a checker gap: the encoder mints reflexive Fiat
proofs for base-primitive results, but BigInt/BigRat results are
term-shaped, not literals, so the checker rejected them unless a
global happened to establish them. The checker now accepts a
reflexive Fiat over any primitive-closed term by re-evaluating it
with the primitive validators (threaded into ProofStore like the
container normalizers), and from-string gains a validator.

typeinfer uses containers, so it joins DD's known-unsupported list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
egglog/src/proofs/proof_encoding_helpers.rs (1)

793-800: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve scoped globals while validating nested fail commands.

Line 798 calls the public wrapper, which replaces extra_globals with an empty set. Consequently, a (fail ...) command that reads a (push)-scoped global is still rejected in proof mode. Recurse through command_supports_proof_encoding_impl with the existing set.

Proposed fix
-                command_supports_proof_encoding(command, type_info)?;
+                command_supports_proof_encoding_impl(command, type_info, extra_globals)?;
🤖 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/src/proofs/proof_encoding_helpers.rs` around lines 793 - 800, Update
the GenericCommand::Fail branch to recurse through
command_supports_proof_encoding_impl using the existing extra_globals set
instead of the public command_supports_proof_encoding wrapper. Preserve the
FailInputCommand rejection and propagate validation errors for all nested
commands.
egglog/CHANGELOG.md (1)

5-26: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Condense the Unreleased changelog entries.

These bullets expose extensive implementation detail (FD views, helper tables, rebuild mechanics) rather than concise caller-facing changes. Keep the behavioral contract and breaking-change guidance, but move internals to dedicated documentation or PR notes.

🤖 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/CHANGELOG.md` around lines 5 - 26, Condense the Unreleased changelog
bullets into concise, caller-facing summaries, especially those describing FD
views, helper tables, rebuild mechanics, proof constructors, and backend
internals. Preserve each user-visible behavior change, performance improvement,
backend/API addition, and breaking or migration guidance; move detailed
implementation mechanics to dedicated documentation or PR notes.

Source: Coding guidelines

🤖 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.

Outside diff comments:
In `@egglog/CHANGELOG.md`:
- Around line 5-26: Condense the Unreleased changelog bullets into concise,
caller-facing summaries, especially those describing FD views, helper tables,
rebuild mechanics, proof constructors, and backend internals. Preserve each
user-visible behavior change, performance improvement, backend/API addition, and
breaking or migration guidance; move detailed implementation mechanics to
dedicated documentation or PR notes.

In `@egglog/src/proofs/proof_encoding_helpers.rs`:
- Around line 793-800: Update the GenericCommand::Fail branch to recurse through
command_supports_proof_encoding_impl using the existing extra_globals set
instead of the public command_supports_proof_encoding wrapper. Preserve the
FailInputCommand rejection and propagate validation errors for all nested
commands.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: bdcb22f4-a934-402c-83d7-da0bd97af79f

📥 Commits

Reviewing files that changed from the base of the PR and between c2691dc and ee022b6.

⛔ Files ignored due to path filters (6)
  • egglog/tests/snapshots/files__proof_unsupported_files.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__array_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__bdd_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__cyk_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__math_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__typeinfer_proof_testing.snap is excluded by !**/*.snap
📒 Files selected for processing (8)
  • egglog-experimental/dd/tests/files.rs
  • egglog/CHANGELOG.md
  • egglog/src/proofs/proof_checker.rs
  • egglog/src/proofs/proof_encoding_helpers.rs
  • egglog/src/proofs/proof_extraction.rs
  • egglog/src/proofs/proof_format.rs
  • egglog/src/sort/bigint.rs
  • egglog/src/typechecking.rs

oflatt and others added 4 commits July 22, 2026 15:31
A reflexive Fiat over a container-producing primitive applied to base
args (e.g. (set-of) or (vec-of 1 2)) would re-evaluate to itself and
fabricate the container term's existence. Exclude presort (container /
unstable-fn) primitives from the checker's validator map; eq-sort
inputs were already excluded structurally (constructor heads are not
validators). Legitimately built containers keep flowing through the
globals branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the checker's name-harvested primitive-validator map with an
explicit per-sort declaration: a base sort whose values termify as an
application (BigInt's (from-string ...), BigRat's (bigrat ...)) provides
its canonical head and recognizer, mirroring rebuild_container_normalizer
for containers. The Fiat rule then accepts reflexive proofs over exactly
these declared value term forms — no overload disjunction, and the
presort exclusion filter becomes unnecessary since existence-carrying
sorts simply do not declare a value form.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reflexive proofs of primitive results were minted as Fiat, whose
checker rule only accepted literals or global-established equalities —
wrong for BigInt/BigRat values, which termify as applications. Split
the concept: Fiat is now top-level (global) equalities only, and the
new Computed form proves t = t for a chain of primitive applications
over literals/variables, checked structurally in the checker itself
against the base-value primitive names (presort/container families
excluded, so no term-existence claim can be smuggled in). This
replaces the short-lived value_term_validator sort hook.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Computed rule accepted terms by shape without running anything.
Rework per review:

- Base-value primitive facts generalize the container side-condition
  mechanism: the fact is emitted verbatim (when free of function calls)
  with an Eval marker, and the checker re-evaluates it against the rule
  body with the program's typed validators — the computation actually
  runs where the types live. Facts containing global reads keep the
  instrumented path; the checker ignores side-condition premise slots,
  so only counts must align.
- Computed remains for termified base values at proposition leaves
  (literal facts, base-sort variables): canonical value spellings like
  BigInt's (from-string ...) that the program never wrote. It is now
  verified by executing the sort-declared value_term_validator
  recognizer and requiring the term to reproduce itself — a real
  computation, local to the node.
- prove_exists keeps the wrapping rule when a single premise is a
  side-condition marker (a bare Eval is only checkable in rule context).
- The structural computable-term rule and its primitive-name set are
  deleted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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: 3

🤖 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/CHANGELOG.md`:
- Line 6: The changelog entry is overly detailed and the proof facts
documentation repeats implementation mechanics. In egglog/CHANGELOG.md line 6,
condense the entry to concise caller-facing behavior, removing corpus status and
proof-internal details. In egglog/src/proofs/proof_encoding_facts.rs lines
21-32, retain the side-condition contract while removing repeated checker and
premise-slot mechanics.

In `@egglog/src/proofs/proof_checker.rs`:
- Around line 695-696: Update the primitive side-condition branch in the
proof-checking logic around check_side_condition to validate premise_id before
re-evaluating fact: require the referenced premise’s justification to be
Justification::Eval, and return a dedicated mismatch error when it is not.
Preserve the existing side-condition substitution and checking behavior after
this validation.

In `@egglog/src/proofs/proof_format.rs`:
- Around line 193-197: Preserve every validator for a canonical head rather than
allowing collection-order overwrites: update value_term_validators in
egglog/src/proofs/proof_format.rs:193-197 to hold grouped validators, and update
validator collection in egglog/src/proofs/proof_extraction.rs:113-118
accordingly so validation accepts only a validator reproducing the exact term.
In egglog/src/sort/mod.rs:96-104, no direct change is required unless
single-validator storage is retained; in that case, enforce and document global
head uniqueness.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f9523fef-200b-470d-8066-71e7b7e3b729

📥 Commits

Reviewing files that changed from the base of the PR and between ee022b6 and e2f1338.

⛔ Files ignored due to path filters (34)
  • egglog/tests/snapshots/files__proofs__array_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__bdd_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__bind_prim_result_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__bitwise_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__commute_collapse_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__container_proofs_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__cyk_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__f64_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__fail_wrong_assertion_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__fibonacci_demand_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__filter_or_bool_neq_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__i64_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__intersection_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__math_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__merge_during_rebuild_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__primitive_args_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__primitives_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__push_pop_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__repro_empty_query_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__repro_equal_constant2_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__repro_equal_constant_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__repro_filter_bug_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__repro_new_backend_prims_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__repro_querybug2_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__repro_querybug4_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__resolution_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__rw_analysis_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__schedule_demo_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__string_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__test_combined_steps_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__typecheck_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__typeinfer_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__unification_points_to_proof_testing.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__proofs__until_proof_testing.snap is excluded by !**/*.snap
📒 Files selected for processing (12)
  • egglog/CHANGELOG.md
  • egglog/src/prelude.rs
  • egglog/src/proofs/proof_checker.rs
  • egglog/src/proofs/proof_encoding_facts.rs
  • egglog/src/proofs/proof_encoding_helpers.rs
  • egglog/src/proofs/proof_extraction.rs
  • egglog/src/proofs/proof_format.rs
  • egglog/src/proofs/proof_simplification.rs
  • egglog/src/sort/bigint.rs
  • egglog/src/sort/bigrat.rs
  • egglog/src/sort/mod.rs
  • egglog/src/typechecking.rs
💤 Files with no reviewable changes (1)
  • egglog/src/typechecking.rs

Comment thread egglog/CHANGELOG.md Outdated
Comment thread egglog/src/proofs/proof_checker.rs Outdated
Comment thread egglog/src/proofs/proof_format.rs Outdated
@saulshanabrook

Copy link
Copy Markdown
Owner

Ran the benchmarks on the most recent version of this, seems like it makes some faster and some slower right now:

Benchmark Report

Comparison

Role Target Git Backend Treatment
Baseline 1cc0e8a 1cc0e8a main proofs
Candidate a1933da a1933da dirty main proofs

6 file(s): math-microbenchmark.egg, eggcc-2mm-pass1.egg, pointer-analysis-small.egg (facts: /Users/saul/p/egglog-encoding/benchmarks/data/pointer-analysis-small), hardboiled_conv1d_32.egg, luminal-llama.egg, herbie.egg · 6 round(s) per endpoint/file · 120 s timeout per run · Report: /Users/saul/p/egglog-encoding/.reports.jsonl

Summary — a1933da main/proofs vs 1cc0e8a main/proofs

Metric Scope File(s) Ratio (95% CI) Result
Wall time Suite total 6 files 0.846–0.891x faster
Wall time Lowest-ratio file pointer-analysis-small.egg 0.0931–0.102x faster
Wall time Highest-ratio file hardboiled_conv1d_32.egg 1.45–1.54x slower
Peak RSS Lowest-ratio file pointer-analysis-small.egg 0.260–0.262x lower RSS
Peak RSS Highest-ratio file hardboiled_conv1d_32.egg 1.81–1.83x higher RSS

Ratios are candidate / baseline; below 1 is lower and above 1 is higher.

Per-file results

Wall time

File Baseline (95% CI) Candidate (95% CI) Ratio (95% CI) Result
math-microbenchmark.egg 8.17–8.64 s 5.95–6.79 s 0.705–0.813x faster
eggcc-2mm-pass1.egg 5.71–5.88 s 5.26–5.86 s 0.907–1.01x CI includes 1
pointer-analysis-small.egg 373–399 ms 36.4–38.9 ms 0.0931–0.102x faster
hardboiled_conv1d_32.egg 601–626 ms 896–934 ms 1.45–1.54x slower
luminal-llama.egg 22.6–23.7 s 19.8–20.7 s 0.847–0.905x faster
herbie.egg 355–373 ms 425–477 ms 1.16–1.32x slower

Peak RSS

File Baseline (95% CI) Candidate (95% CI) Ratio (95% CI) Result
math-microbenchmark.egg 2.6–2.8 GiB 2.2–2.5 GiB 0.788–0.921x lower RSS
eggcc-2mm-pass1.egg 729.2–759.6 MiB 1.3–1.3 GiB 1.70–1.78x higher RSS
pointer-analysis-small.egg 103.9–104.5 MiB 27.1–27.2 MiB 0.260–0.262x lower RSS
hardboiled_conv1d_32.egg 205.7–208.0 MiB 375.7–377.0 MiB 1.81–1.83x higher RSS
luminal-llama.egg 1.3–1.6 GiB 1.3–2.3 GiB 0.877–1.59x CI includes 1
herbie.egg 155.8–158.6 MiB 171.3–174.8 MiB 1.09–1.12x higher RSS

@oflatt

oflatt commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

yep- though for hardboiled that may just be the container bug fix

this is a stepping stone for other optimizations, so I'd say worth the slowdown to herbie.egg

@saulshanabrook saulshanabrook left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I think it would be OK to merge as is. I like that we get rid of constructors, that makes a lot more sense to me (I am confused why we had them in the first place, if we didnt depend on a UF?)

Main concern is adding the set-if-doesnt-exist primitive... seems to require a bunch of special casing and curious if there is a way to write the encoding without it.

Comment thread egglog-experimental/dd/tests/files.rs
Comment thread egglog-experimental/dd/src/lib.rs
Comment thread egglog/core-relations/src/free_join/mod.rs
Comment thread egglog/egglog-backend-trait/src/lib.rs Outdated
Comment thread egglog/egglog-backend-trait/src/lib.rs Outdated
Comment thread egglog/src/prelude.rs Outdated
Comment thread egglog/tests/snapshots/files__proof_unsupported_files.snap
Comment thread egglog/tests/snapshots/files__proofs__bdd_proof_testing.snap Outdated
Comment thread egglog/tests/snapshots/files__proofs__math_proof_testing.snap Outdated
Comment thread egglog/tests/snapshots/files__proofs__typeinfer_proof_testing.snap Outdated
@saulshanabrook

Copy link
Copy Markdown
Owner

Oh also a little worried about the performance here... since it makes sense benchmarks quite a bit slower. but 🤷 I know this is setup to make things faster in the future

oflatt and others added 3 commits July 23, 2026 16:55
- Rename the backend SPI's `fresh_eclass_id`/`eclass_id_counter` to
  `fresh_id`/`id_counter`: they mint generic term/proof/AST ids, not
  e-class-specific ones (the backend has no e-class concept). Clarify the docs
  that a fresh id is just the next counter value, not stored anywhere until the
  encoding asserts a relation row that references it.
- Make the backend SPI proof-agnostic: `Backend::register_view_proof` becomes
  `register_view_column_read(view, n_keys, col_idx)`, a generic view-column
  read. The proof encoding (which owns the notion of "proof") reads column 1.
- Remove the last constructors from the encoding: the deferred delete/subsume
  marker tables are now `Unit :no-merge` relations tagged with a new
  `:internal-marker` function flag, so extraction never mistakes them for term
  relations (they carry no minted id and may be nullary).
- proof_encoding.md: clarify `:internal-identity-vals` (idempotent re-writes,
  not a key), describe the proof nodes as relations rather than constructors,
  and document the delete/subsume markers.
- Skip the oversized bdd/math/typeinfer proof-testing snapshots (2.6k-6.5k
  lines) via PROOF_TESTING_SNAPSHOT_DISABLED_FILES.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The proof checker never used the validator returned by
`Sort::prim_value_constructor` — it only needs to know that a term's head is a
base sort's value constructor so it can treat the term as a self-evident value
(the `reflexive_value_term` check). Return just the head name (`Option<String>`)
and drop the per-sort recognizer closures; the checker now accepts such a term
by head membership plus recursion into its arguments.

Add a sanity check when collecting these heads: a value constructor must resolve
to exactly one primitive (no overloads). The checker treats the head as an
unambiguous value marker, so an overloaded head would be unsound to ignore.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Under term encoding a user `(relation R …)` / constructor becomes an internal
term relation — a `(function R (children… id) Unit :no-merge)` whose eclass id
is the last input column, with a trailing `Unit` output. `EGraph::constructor_enodes`
rejected it (its subtype is `Custom`, not `Constructor`) and, had it not, split the
row assuming the eclass was the output column.

Accept term relations (`is_relation_term`) in `constructor_enodes`, and locate the
children/eclass with `extraction_num_children` / `extraction_output_index`, which are
correct for both real constructors (eclass = output) and term relations (eclass =
last input, trailing `Unit` ignored) — and reduce to the old `split_last` for real
constructors, so non-term reads are unchanged. Reject term relations from
`function_entries`, since they read as enodes rather than function entries.

Fixes `backend_generic_reads_work_without_an_action_registry`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread egglog/src/prelude.rs
/// literal (e.g. a BigInt's `(from-string "…")`): the head of that canonical
/// value term (see [`Sort::prim_value_constructor`]). `None` (the default)
/// for literal-backed sorts.
fn prim_value_constructor(&self) -> Option<String> {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This looks good!

The term/proof encoding's term/proof/AST/proof-list node relations are
structurally identical to the deferred delete/subsume bookkeeping markers (all
hidden `Unit :no-merge` relations), so extraction needs an explicit signal to
tell them apart. Instead of tagging the two markers negatively with
`:internal-marker`, tag the node relations positively with `:internal-term-node`
(their minted id is the last input) and leave the markers plain.
`is_relation_term()` is now just `decl.internal_term_node`.

The flag lives on the FunctionDecl, so it round-trips through desugaring and
re-parsing (verified by the `*_desugar_proof_testing` fixtures, which a
records-based positive scan could not satisfy); extraction reconstructs exactly
the flagged relations and never reads the markers as terms.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@oflatt
oflatt merged commit e6feb75 into saulshanabrook:main Jul 23, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants