Skip to content

Differential Dataflow Backend#4

Merged
oflatt merged 22 commits into
saulshanabrook:mainfrom
oflatt-claude:backend-interface
Jul 13, 2026
Merged

Differential Dataflow Backend#4
oflatt merged 22 commits into
saulshanabrook:mainfrom
oflatt-claude:backend-interface

Conversation

@oflatt-claude

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

Copy link
Copy Markdown

AI disclosure: This description was written by AI (OpenAI Codex) from the current branch, implementation, review history, and validation results.

Summary

This PR adds a pluggable backend SPI to egglog and an experimental backend implemented with Differential Dataflow (DD).

The default remains egglog's reference in-memory backend. The DD backend is a research prototype for testing whether the frontend, term encoding, and proof mode can run over a separately implemented relational backend. It is not proposed as the default backend, and it does not currently outperform the main backend.

The PR also extends the benchmark runner so the two backends can be compared and CPU-profiled with the same workload construction.

Backend boundary

  • Adds the egglog-backend-trait crate and makes the frontend own a Box<dyn Backend>.
  • Adds EGraph::with_backend(...) for supplying another implementation.
  • Adapts the reference implementation in egglog-bridge to the same trait.
  • Passes a backend-neutral RuleSpec, built from generic core rule types in egglog-ast, instead of exposing frontend-only resolved types or adding a DD-specific rule IR.
  • Represents optional action-registry support directly as an optional capability; generic table reads use backend scans and do not require a bridge downcast.
  • Lets a backend require term encoding when it has no native union-find implementation.
  • Makes backend/resource construction transactional and returns capability failures instead of panicking where the public flow is fallible.
  • Keeps backend configuration table-driven in the benchmark runner for future implementations.

Differential Dataflow backend

The experimental backend lives in egglog-experimental/dd and depends on the public backend SPI, the neutral shared rule AST, and core-relations value/primitive support.

Its architecture is deliberately hybrid:

  • DD performs incremental table-atom joins for rule bodies.
  • Each active ruleset has one persistent Timely worker and one fused dataflow, with shared input sessions and arrangements across its rules.
  • The host feeds signed, versioned row deltas into that dataflow. Retractions and remove-then-reinsert freshness are explicit.
  • Body primitives are evaluated host-side after DD produces bindings.
  • Head actions, lookups, merge resolution, subsumption, row-version tracking, and rebuild-sensitive mirror updates are host-side.
  • A Rust-side live/subsumed mirror is authoritative for backend reads; an embedded core-relations Database owns base values, container values, and ordinary primitive execution.
  • There is no host nested-loop join fallback. Unsupported plans return a specific error.
  • There are no ruleset-name special cases or environment-variable execution modes.

The join planner uses a fixed-width row with liveness-based column allocation. It reuses columns across wide variable chains, but rejects a rule whose simultaneously live variable frontier exceeds 48. Table registration, row packing, and rule validation share that same width bound. Cached fused outputs are mapped back to caller rules by stable rule ID, including when callers reverse rule order.

Current semantic coverage

  • Runs through term encoding, including proof mode; there is no native union-find implementation.
  • Supports relations and functional tables with old, new, union-id/min, and computed primitive/function/constant merge trees.
  • Preserves ordered merge behavior for emitted writes.
  • Represents live and subsumed rows separately while retaining term-encoding rebuild visibility.
  • Supports ordinary external primitives and direct container-value storage through the embedded database.
  • Converts unsupported registry-backed primitive execution into a controlled backend error.
  • Rejects an unexpected native union and malformed/unsupported rule shapes during rule registration or planning.
  • Implements clone_boxed by cloning authoritative relation, rule, mirror, value-registry, and version state while rebuilding transient Timely workers lazily. Frontend push/pop is covered end-to-end.
  • Uses egglog's shared snapshots as the parity source of truth; the DD crate keeps no duplicate snapshot set.

The release DD harness runs 129 trials: 127 selected corpus cases plus two corpus-discovery self-checks. Four selected files are expected to return their exact documented action-registry capability error, and all other selected cases must match egglog's shared snapshots. Panics, wrong errors, unexpected failures, stale unsupported entries, unreadable discovery, and empty discovery all fail the harness.

Known limitations

  • DD does not expose egglog's ActionRegistry. Registry-backed container rebuild/read primitives therefore cannot read the authoritative DD mirror. The explicit expected-error cases are container-proofs, datatypes, nested-container-dirty-propagation, and repro-querybug3.
  • Seven larger saturation cases are excluded as too slow for the DD parity harness: eggcc-2mm, eqsolve, math-microbenchmark, rectangle, repro-herbie-vanilla, resolution, and until.
  • The full math workload and eggcc remain impractical on this backend. Current broader samples show materially higher wall time and peak RSS than the main backend.
  • The fixed-width join row imposes the 48-value live-frontier limit described above.
  • DD currently owns joins, not the full egraph iteration semantics. Moving more work into dataflow would require faithfully representing merge ordering, subsumption, rebuild retractions, freshness, and staged RHS effects.

Benchmarking and profiling

Benchmark either or both backends while recording the backend in every JSON row:

./bench.py --backend main,dd --treatments proofs \
  egglog/tests/math-microbenchmark-mini.egg

Reports include per-file and aggregate backend comparisons, best-case visibility, Rich and GitHub Markdown output, append-only result caching, and backend/treatment compatibility checks.

Samply profiles use the same workload construction and a key-addressed local cache:

./bench.py profile egglog/tests/math-microbenchmark-mini.egg \
  --backend dd --treatment proofs

Validation on 47c469c7

  • cargo test --workspace passes, including all 712 core file tests and DD's 24 unit, 8 debug-corpus, 2 rewrite, and 7 smoke tests.
  • cargo test --release -p egglog-experimental-dd --test files passes all 129 trials.
  • cargo clippy --workspace --all-targets -- -D warnings passes.
  • Strict rustdoc passes for egglog-backend-trait and egglog-experimental-dd. Strict workspace rustdoc remains blocked by two pre-existing origin/main link warnings outside this PR's changed documentation.
  • uv lock --check, Ruff formatting/linting, mypy, and all 96 Python tests pass.
  • The public benchmark smoke and a DD/proofs Samply recording smoke pass.
  • A clean-worktree comparison against pre-cleanup efd5b703 on math-microbenchmark-mini found no measurable cleanup regression: wall-time CI [-20.4%, +19.5%], RSS CI [-0.9%, +1.3%].
  • git diff --check and cargo fmt --all -- --check pass.
  • proof_encoding.rs and proof_checker.rs have no diff from main; unrelated proof-encoding edits were removed.

Suggested review order

  1. egglog/egglog-backend-trait and the backend-neutral rule types.
  2. Frontend and bridge adaptation, capability/error handling, and resource rollback.
  3. egglog-experimental/dd state model and host/DD execution split.
  4. DD join planning, fused cache mapping, cloning, and parity harness.
  5. Benchmarking/profiling support.

The main design question is whether this backend trait and hybrid DD/host implementation are a useful experimental baseline for future backends, given the explicit capability and performance limits above.

Summary by CodeRabbit

  • New Features
    • Added pluggable backend support via a new backend trait, enabling custom execution engines and backend-specific capabilities.
    • Added a Differential Dataflow experimental backend with CLI backend selection and backend-aware profiling/benchmark reporting (including Markdown output and backend comparisons).
    • Added Samply profile analysis with rich and Markdown CPU breakdown reporting.
  • Bug Fixes
    • Improved rollback/cleanup during failed rule, table, and external-function setup.
    • Fixed term-encoding behavior for Rust-registered primitives and converted ambiguous primitive issues into surfaced errors.
  • Documentation
    • Updated unreleased notes to reflect pluggable backend SPI and term-encoding behavior.

@oflatt-claude

Copy link
Copy Markdown
Author

Per review feedback, differential-dogs3 is no longer vendored in-tree. It's now a git dependency on a patched fork (oflatt-claude/differential-dogs3, pinned by rev), matching how flowlog-runtime is already sourced. The fork is stock crates.io differential-dogs3 0.24.0 plus one upstreamable bug fix (lookup_map.rs index(1)index(0), which otherwise panics on every lookup). It's only pulled in by the opt-in --wcoj triangle join. This drops ~1400 lines of vendored third-party code from the PR (now 40 files, was 51).

@oflatt-claude

Copy link
Copy Markdown
Author

Correction/update: the lookup_map fix is already upstreamed as an open PR (TimelyDataflow/differential-dataflow#765). So flowlog now points differential-dogs3 (the crates.io name of dogsdogsdogs, which lives in the differential-dataflow repo) at that PR branch, with a [patch.crates-io] redirecting differential-dataflow to the same source (the fix only touches dogsdogsdogs; dd itself is stock 0.24.0, so all of flowlog / flowlog-runtime / differential-dogs3 share one dd). No separate vendored copy or throwaway fork. Once #765 merges and a fixed differential-dogs3 is published, this can drop to a plain crates.io version + remove the patch.

@oflatt-claude

Copy link
Copy Markdown
Author

Cleanup pass (two commits on top):

Tidy egglog-backend-trait docs — fixed stale references (BackendExt sugar; the one-line bridge re-export).

Simplify the flowlog backend + snapshot harness

  • Dropped the dead InProcess/ShellOut execution modes (only Interpret, the differential-dataflow path, is used). This deletes engine.rs/codegen.rs/subprocess.rs/build.rs/transitive_step.dl and both flowlog-rs git build-deps (flowlog-runtime, flowlog-build) — the crate now depends only on differential-dataflow/timely + the differential-dogs3 fix branch.
  • Removed the per-rule PersistentDdJoin and the write-only dd_native/dd_native_fed fields (the interpreter drives the fused per-ruleset join).
  • Trimmed stale port docs.
  • Replaced the ad-hoc corpus test with tests/files.rs — a libtest-mimic + insta harness mirroring egglog's own tests/files.rs: one trial per supported .egg file that runs on both the reference and FlowLog, asserts they agree, and snapshots the (print-size) output.

Net: ~1300 fewer lines in the flowlog crate; all suites green (egglog clippy --tests -D warnings + fmt clean; flowlog 12 tests incl. 7 snapshot files). The two commits are separate for reviewability but can be folded into the originals if a linear history is preferred.

@oflatt-claude

Copy link
Copy Markdown
Author

tests/files.rs now runs against egglog's whole .egg corpus (not a hand-picked list), mirroring egglog's own files.rs: each file runs on both the reference backend and FlowLog. Classification over the corpus:

  • 47 files: FlowLog runs them and the (print-size) matches the reference exactly → snapshotted.
  • 73 files: use features FlowLog doesn't support (proofs / containers / subsumption / complex merges / non-DD-lowerable rules) → skipped (not covered).
  • 6 files: saturate too slowly on the DD path → skipped (HANG).
  • 3 files (merge-during-rebuild, luminal-llama, prims): FlowLog runs them but produces different results than the reference — genuine correctness gaps in the simplified merge/rebuild handling (e.g. merge-during-rebuild keeps a stale distance row the reference merges away: 2 vs 1). These are pinned in KNOWN_MISMATCH; the harness fails if that set changes in either direction, so new divergences (or fixes) are caught.

Runs in ~4s under --release (differential-dataflow is impractically slow in debug, so cargo test debug covers a fast subset and --release runs the full corpus).

Worth flagging: those 3 mismatches mean the stripped-down FlowLog is not correct on merge-during-rebuild-style programs — a real limitation of this demo backend, not just "unsupported." Happy to dig into it if it's worth fixing, but I've left it documented rather than silently excluded.

@coderabbitai

coderabbitai Bot commented Jul 6, 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 a pluggable backend interface, integrates an in-process Differential Dataflow backend, updates Egglog rule execution and resource handling, and extends benchmark tooling with backend-aware collection, Markdown reports, profiling workflows, and Samply CPU analysis.

Changes

Backend platform and frontend integration

Layer / File(s) Summary
Backend contract and frontend integration
egglog/egglog-backend-trait/*, egglog/src/*, egglog/egglog-ast/*
Adds backend trait objects, generic lowered rule structures, backend-aware rule compilation, capability checks, CLI argument forwarding, proof/typechecking updates, and reference-backend adapters.
Bridge lifecycle and rollback
egglog/egglog-bridge/*, egglog/core-relations/*, egglog/numeric-id/*
Adds owned external-function cleanup, panic reference counting, last-table rollback APIs, and associated tests.
Experimental DD wiring
egglog-experimental/*
Adds DD backend constructors, feature wiring, backend CLI selection, and conditional action-registry primitive registration.

Differential-dataflow backend

Layer / File(s) Summary
Join planning and execution
egglog-experimental/dd/src/compile.rs, egglog-experimental/dd/src/dd_native.rs
Defines DD physical values and merge plans, plans bounded-width liveness-aware joins, and executes shared persistent timely dataflows.
Iteration interpreter and EGraph
egglog-experimental/dd/src/interpret.rs, egglog-experimental/dd/src/lib.rs
Feeds relation deltas into fused joins, evaluates primitives and head actions, maintains live/subsumed mirror state, and implements the backend lifecycle.
DD validation
egglog-experimental/dd/tests/*
Adds smoke, proof, rewrite-join, scheduler-capability, and corpus snapshot tests.

Backend-aware benchmarking and profiling

Layer / File(s) Summary
Benchmark planning and reports
bench.py
Adds backend-scoped benchmark cells, cache keys, target builds, workload flags, comparisons, diagnostics, and Rich/Markdown report rendering.
Samply analysis
samply_analysis.py
Validates compressed profile artifacts, summarizes CPU usage, symbolicates functions, and renders profile reports.
Benchmark and profiling tests
test_bench.py, .gitignore
Covers backend selection, cache compatibility, Markdown output, profiling commands, artifact handling, symbolization, and profile control flow; ignores generated profile artifacts.

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

Sequence Diagram(s)

sequenceDiagram
  participant EgglogFrontend
  participant BackendRule
  participant FusedDdJoin
  participant DdEGraph
  EgglogFrontend->>BackendRule: compile RuleSpec
  BackendRule->>FusedDdJoin: register rule join plan
  DdEGraph->>FusedDdJoin: submit relation deltas
  FusedDdJoin-->>DdEGraph: return binding deltas
  DdEGraph->>EgglogFrontend: apply staged rule updates
Loading

Possibly related PRs

Suggested reviewers: saulshanabrook

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 90.22% 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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: introducing a Differential Dataflow backend.

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.

Add a backend-agnostic SPI so a third party can drive the egglog frontend
with their own execution engine, and ship a differential-dataflow ("FlowLog")
backend as the worked example.

Backend SPI (new `egglog-backend-trait` crate):
- `egglog::EGraph` holds a `Box<dyn Backend>` and performs all state access
  through the `Backend` trait; `EGraph::with_backend(..)` plugs in a custom one.
- Rules are built through `RuleBuilderOps`, mirroring `egglog_bridge::RuleBuilder`.
- A backend without a native union-find declares `requires_term_encoding()`;
  `EGraph::with_term_encoding()` opts such an e-graph into the term-encoding
  pipeline (congruence and rebuild lower to rules over `@uf` tables), and running
  one without it errors (`Error::BackendRequiresTermEncoding`) rather than
  silently dropping `union`s.

FlowLog backend (`egglog-experimental/flowlog`):
- Runs rule bodies on an in-process differential-dataflow join; body primitives
  and head actions apply host-side into a Rust mirror.
- Correct under term encoding: congruence closure folds merge-function sets in
  insertion order (a sort-based fold left the `@UF_Mf` find-index stale);
  computed merges evaluate the retained merge tree (primitives like `(or old
  new)`, constructor merges like `(C2 (C1 old new) ...)`); subsumption is a soft
  delete (subsumed rows move to a side set, hidden from ordinary matching but
  visible to `for_each` and the include-subsumed rebuild rules).
- A corpus harness (`tests/files.rs`) runs egglog's `.egg` corpus on FlowLog vs a
  term-encoded reference for cross-backend parity + insta snapshots; only
  genuinely-unsupported files (push/pop) are excluded.

Both new crates join the root workspace, so `cargo test --workspace` and
`clippy --workspace` cover them. The FlowLog `[patch.crates-io]` pinning
differential-dataflow lives in the workspace-root Cargo.toml.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A primitive registered through the Rust API (`add_pure_primitive` /
`add_*_primitive` / the `add_primitive!` macros) after e-graph construction
was only registered on the running e-graph, not on the separate
`original_typechecking` e-graph that term encoding / proofs use to typecheck
the encoded program. The encoder then reported the primitive as an unbound
function, so callers had to manually register it on
`proof_state.original_typechecking` as well (a proof test did exactly that).

`register_per_context` now walks the `original_typechecking` chain and
registers the primitive on each e-graph. A typechecker only typechecks and
never evaluates, so the wrapper's runtime state is irrelevant there, and both
e-graphs register the same built-ins at construction, so a primitive added to
both gets the same `ExternalFunctionId`.

A sort's primitives already reach the typechecker through its own `add_arcsort`
when the typechecker typechecks the sort command, so `add_arcsort` detaches the
typechecker while a sort registers to avoid double-registering (which would make
primitive resolution ambiguous).

Upstreamed as egraphs-good/egglog#945.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
egglog's `tests/files.rs` appends `(print-size)` to every corpus file and stores
one snapshot per file whose content is normalized to be identical across the
native / term-encoding / proofs treatments
(`CommandOutput::snapshot_stable_under_proof_encoding`). FlowLog is term-encoded,
so its normalized output must equal that snapshot.

The FlowLog corpus harness now checks against those shared snapshots directly
(`egglog/tests/snapshots/files__shared_snapshot_<stem>.snap`) instead of an
in-process reference plus its own snapshots — egglog is the single source of
truth, and FlowLog's own snapshots are dropped (as is the `insta` dev-dep). Files
are compared only when `file_supports_proofs` (egglog ran the term-encoding
treatment, so the snapshot is term-encoding-valid); native-only snapshots are
skipped.

Comparing the FULL normalized output (not just the last `print-size`) is stricter
and surfaced two real FlowLog divergences the old check missed — `antiunify` and
`integer_math` leave a few extra un-collapsed constructor e-nodes (incomplete
congruence closure). They are documented in `KNOWN_MISMATCH` for follow-up.

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

codspeed-hq Bot commented Jul 8, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 6 untouched benchmarks


Comparing oflatt-claude:backend-interface (47c469c) with main (ef9fe20)

Open in CodSpeed

@saulshanabrook

saulshanabrook commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Handoff comment from @oflatt

Handoff — flowlog backend + egglog term-encoding fix Two PRs, both green (except one documented divergence): - #4 (#4) (branch backend-interface, pushed to oflatt-claude fork): pluggable Backend SPI + FlowLog backend, rebased on current main, both crates are root-workspace members (in cargo test/clippy --workspace). - egraphs-good/egglog#945 (egraphs-good/egglog#945) (upstream): fix so Rust-API primitives register on the term-encoding typechecker (was "Unbound function"). Full upstream suite green. Just landed: FlowLog's tests/files.rs now checks against egglog's own golden snapshots (egglog/tests/snapshots/files__shared_snapshot_*.snap) — egglog is the single source of truth. Files are compared only when file_supports_proofs (snapshot is term-encoding-valid); FlowLog's own snapshots + insta dep removed. ⚠️ Open item (the one thing to pick up): the stricter full-output comparison surfaced 2 real FlowLog divergences in KNOWN_MISMATCH — antiunify (AU 23 vs egglog 21) and integer_math (Add/Mul/MathU a few high, looks mildly nondeterministic). FlowLog leaves a handful of un-collapsed constructor e-nodes → incomplete congruence closure. Start at egglog-experimental/flowlog/src/interpret.rs (the @congruence_rule/rebuild path). Remove from KNOWN_MISMATCH once fixed. Notes: FlowLog is edition-2021 in the 2024 workspace (fine, resolver 3); its differential-dataflow [patch] lives in the root Cargo.toml. CI's cargo test --workspace (debug) runs a fast DEBUG_SUBSET; full corpus is --release. Safety backups: branches backend-interface-prerebase-backup / -backup; upstream clone at /home/oflatt/egglog-upstream (deletable).

Comment thread egglog-experimental/dd/src/compile.rs
Comment thread egglog-experimental/flowlog/src/dd_native.rs Outdated
Comment thread egglog-experimental/dd/src/compile.rs Outdated
Comment thread egglog-experimental/flowlog/src/external_func.rs Outdated
/// leading `@` and trailing digits don't matter. Order: most specific first
/// (`singleparent` and the `_subsume` variant before their bare forms;
/// `uf_function_index` before `uf_update`).
pub(crate) fn rule_category(name: &str) -> &'static str {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

red flag- special casing rule sets

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Perhaps the problem here is more with the backend trait not preserving the ruleset name? Then we could remove this if we fixed that?

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.

Added RuleSetRun which carries the name through

Comment thread egglog-experimental/flowlog/src/interpret.rs Outdated
Comment thread egglog-experimental/flowlog/src/lib.rs Outdated
@saulshanabrook saulshanabrook changed the title Pluggable Backend interface + FlowLog backend as an experimental crate Differential Dataflow Backend Jul 9, 2026
Resolve main conflicts while keeping the FlowLog DD backend experimental, remove prototype WCOJ/env-toggle paths, and document current unsupported FlowLog parity boundaries.
@saulshanabrook

Copy link
Copy Markdown
Owner

I have addressed some of the comments and we can see how its a bit slower than current egglog on all files test. I will now work on profiling to see if there is any low hanging fruit around fixing these:

$ ./bench.py --backend main,flowlog --treatments proofs egglog/tests/web-demo/matrix.egg egglog/tests/web-demo/resolution.egg egglog/tests/integer_math.egg egglog/tests/web-demo/rw-analysis.egg --format markdown

Benchmark Report

  • Report: /Users/saul/p/egglog-encoding/.reports.jsonl
  • Selected rows per cell: 6

Targets

Role Label Git Dirty Binary Path
target 230bf52 230bf52 yes 194866e2e21b /Users/saul/p/egglog-encoding

Comparisons

Per-file backend wall-time change vs main: 230bf52 flowlog

File Treatment Time ratio Wall-time change Result
egglog/tests/web-demo/matrix.egg proofs [2.840x, 2.992x] [184.0%, 199.2%] slower
egglog/tests/web-demo/resolution.egg proofs [1.859x, 1.967x] [85.9%, 96.7%] slower
egglog/tests/integer_math.egg proofs [3.591x, 4.607x] [259.1%, 360.7%] slower
egglog/tests/web-demo/rw-analysis.egg proofs [2.857x, 3.127x] [185.7%, 212.7%] slower

Ratio is candidate backend / baseline backend for the same target and treatment. Values below 1x are faster; above 1x are slower. Intervals are 95% CIs.

Per-file backend peak RSS change vs main: 230bf52 flowlog

File Treatment RSS ratio RSS change Result
egglog/tests/web-demo/matrix.egg proofs [6.028x, 6.098x] [502.8%, 509.8%] more
egglog/tests/web-demo/resolution.egg proofs [3.130x, 3.158x] [213.0%, 215.8%] more
egglog/tests/integer_math.egg proofs [8.286x, 8.392x] [728.6%, 739.2%] more
egglog/tests/web-demo/rw-analysis.egg proofs [3.201x, 3.219x] [220.1%, 221.9%] more

Ratio is candidate backend / baseline backend for the same target and treatment. Values below 1x use less peak RSS; above 1x use more. Intervals are 95% CIs.

Target Diagnostics

230bf52: per-file wall time

File main/proofs flowlog/proofs
egglog/tests/web-demo/matrix.egg [0.0554s, 0.0580s] [0.1632s, 0.1672s]
egglog/tests/web-demo/resolution.egg [0.0558s, 0.0584s] [0.1073s, 0.1109s]
egglog/tests/integer_math.egg [0.0564s, 0.0579s] [0.2053s, 0.2630s]
egglog/tests/web-demo/rw-analysis.egg [0.0530s, 0.0579s] [0.1637s, 0.1675s]

Within-target wall-time estimates. These are not target-vs-baseline ratios.

230bf52: per-file peak RSS

File main/proofs flowlog/proofs
egglog/tests/web-demo/matrix.egg [16.7 MiB, 16.9 MiB] [101.8 MiB, 102.3 MiB]
egglog/tests/web-demo/resolution.egg [14.0 MiB, 14.1 MiB] [44.2 MiB, 44.4 MiB]
egglog/tests/integer_math.egg [21.8 MiB, 22.0 MiB] [182.3 MiB, 183.5 MiB]
egglog/tests/web-demo/rw-analysis.egg [21.7 MiB, 21.7 MiB] [69.5 MiB, 69.8 MiB]

Within-target peak resident set size estimates. These are separate from wall-time ratios.

Benchmark Summary

FlowLog vs main wall time

Backend Mode main total FlowLog total FlowLog/main Change File geomean Faster files Best file Best ratio Best result
flowlog proofs 0.2263s 0.6741s [2.844x, 3.114x] [184.4%, 211.4%] 2.874x 0/4 egglog/tests/web-demo/resolution.egg [1.859x, 1.967x] slower

Ratio is candidate backend / baseline backend for the same target and treatment. Values below 1x are faster; above 1x are slower. Intervals are 95% CIs.

FlowLog vs main peak RSS

Backend Mode main total FlowLog total FlowLog/main Change File geomean Lower-RSS files Best file Best ratio Best result
flowlog proofs 74.6 MiB 398.9 MiB [5.335x, 5.365x] [433.5%, 436.5%] 4.752x 0/4 egglog/tests/web-demo/resolution.egg [3.130x, 3.158x] more

Ratio is candidate backend / baseline backend for the same target and treatment. Values below 1x use less peak RSS; above 1x use more. Intervals are 95% CIs.

230bf52: proof overhead summary

Metric Ratio Change Worst file Worst ratio Result
main no proof baseline - - - - select off and proofs
flowlog no proof baseline - - - - select off and proofs

Within-backend proof overhead. This is not backend-vs-main performance.

@saulshanabrook

Copy link
Copy Markdown
Owner

This is now ready for review. It is about 4x slower than main on a reduced math microbenchmark, which is a lot better than before. I think we should get this in for now, and keep iterating, either by replacing this with duckdb or doing a larger refactor to make it faster.

I think we should merge this in for now, to get the code that allows benchmarking against multiple backends, I also update the benchmark script to output markdown (helpful for posting to github), and to easily profile.

I have also renamed it to a dd backend, since it is not flowlog.

Here is an update from the agent on what further performance improvements would take:

Performance

   Version               Wall time       Peak RSS
  ━━━━━━━━━━━━━━━━━  ━━━━━━━━━━━━━━  ━━━━━━━━━━━━━
   Initial FlowLog      2.89–3.32s    811–824 MiB
  ─────────────────  ──────────────  ─────────────
   Current FlowLog    0.512–0.569s    292–295 MiB
  ─────────────────  ──────────────  ─────────────
   Main               0.106–0.138s      54–55 MiB

The accumulated optimizations make FlowLog 83–85% faster and use about 64% less memory than the initial implementation. It remains 3.88–5.15x slower and 5.31–5.40x larger than main.

What Is Happening

  • Proofs are not the fundamental problem. FlowLog term mode was already around 6x slower than main; proofs add roughly 19%.

  • The workload creates six persistent DD workers, 90 rules, and 112 binary join stages.

  • Those 112 joins previously built 112 right-side arrangements. Only 40 projection signatures were unique. The new cache in egglog-experimental/flowlog/src/dd_native.rs:585 removes 72 duplicate arrangements.

  • Every rule-local left intermediate is still separately arranged because DD’s collection-level join_core arranges the left input. Those 112 retained intermediate traces remain.

  • Both relation rows and join keys are [u32; 48], or 192 bytes, regardless of their actual width: egglog-experimental/flowlog/src/dd_native.rs:70.

  • An experimental 12-column build with arrangement sharing reached approximately 0.413s and 142 MiB. That was still about 3.5x slower and 2.6x larger than main, so width specialization is necessary but
    insufficient.

  • The run performs 363 DD epochs carrying about 110,000 deltas. Only 13 epochs came from separated removal/reinsertion batches, so batching those does not close the gap.

  • Main and FlowLog produce identical logical table sizes. The excess is retained physical DD state, not additional e-nodes.

  • The full 11-round benchmark still times out after 20 seconds.

The final profile is dominated by DD trace merging, Row::cmp, OrdVal construction/seeking/copying, and host-side binding hash maps. The profile artifact is:

.profiles/v1/800136d1aae3751bd130dc9525d046b9edb62e5b642c4c5482cee07a931d0a8a/3e02f6cfed2f99fc7479a27226f3499d6c9efafa8113d2ce3f28ff16a2452a19/flowlog-proofs-auto10s.json.gz

A simple cardinality-based atom reorder produced no improvement. Exact prefix sharing could eliminate only 27 of 202 prefixes, so neither is a sufficient local fix.

What Beating Main Requires

  1. Use width-specialized relation, key, and binding types. This likely means monomorphized FusedDdJoin variants or true FlowLog-style generated tuple types.

  2. Share base arrangements globally across rulesets rather than retaining one independent worker and relation integral per ruleset. This must preserve rule-local freshness and schedule visibility.

  3. Stop representing every rule as persistent binary DD intermediates. A hybrid planner should use main’s stateless Generic/Free Join kernel for small, rapidly changing rules and DD only for derived views whose
    maintenance is actually amortized.

  4. Replace full per-ruleset version snapshots in egglog-experimental/flowlog/src/lib.rs:92 with relation mutation logs and cursors. The new unchanged-snapshot shortcut in egglog-experimental/flowlog/src/
    interpret.rs:301 helps CPU modestly, but snapshots are not the dominant memory cost.

  5. Move multiple logical iterations into DD only after modeling action, merge, rebuild, and freshness barriers. Most observed epochs represent real schedule boundaries.

True FlowLog demonstrates generated tuples and stratum-scoped arrangement caching, but its current planner is still source-order left-deep. Main already has tree decomposition, Generic/Free Join choices, lazy
indexes, and runtime cardinality ordering.

My conclusion is that pure DD is unlikely to beat main on this math workload without a substantial execution redesign. The promising architecture is a hybrid join interface where DD maintains selected high-reuse
relational views while main’s join kernel handles the many small term/proof maintenance rules. DD’s best opportunity to win is a workload with a few stable recursive rules over large relations, not this many-
small-iteration workload.

@saulshanabrook saulshanabrook marked this pull request as ready for review July 11, 2026 16:11

@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

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_extractor.rs (1)

176-194: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard against panics when function tables are empty.

Both first_output and first_input call value.unwrap() after iterating with for_each_while. If the function's backend table has no rows, value remains None and the unwrap() panics. With the new pluggable backend SPI, an alternate backend that hasn't populated rows yet (or a query that matches nothing) will trigger a runtime panic instead of a graceful error.

Consider returning a Result<Value, Error> or providing a descriptive expect() message that includes the function name for debuggability.

🛡️ Suggested improvement for `first_output`
     fn first_output(egraph: &EGraph, function_name: &str) -> Value {
         let func = egraph.functions.get(function_name).unwrap();
         let mut value = None;
         egraph.backend.for_each_while(func.backend_id, |row| {
             value = Some(row.vals[func.extraction_output_index()]);
             false
         });
-        value.unwrap()
+        value.unwrap_or_else(|| panic!("no rows in function '{function_name}'"))
     }
🤖 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_extractor.rs` around lines 176 - 194, Update
first_output and first_input to handle empty backend tables without an
uninformative unwrap panic. Prefer propagating a Result<Value, Error> through
their callers; otherwise replace each unwrap with a descriptive expect message
that includes function_name, while preserving the existing first-row extraction
behavior.
egglog/src/lib.rs (1)

1214-1271: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Rule is built in the backend before the target ruleset is validated — leaks the rule on NoSuchRuleset/CombinedRulesetError.

compiled (which calls self.backend.new_rule(...) and translator.build(), allocating/registering a backend-side rule) is computed unconditionally, before checking whether rule.ruleset exists and is a Ruleset::Rules (not Combined). If either check fails, the function returns an Err without ever calling self.backend.free_rule(rule_id) on the rule that was just built — the backend resource is silently leaked. This is reachable via a plain user typo (referencing an undeclared or combined ruleset in :ruleset), and other call sites in this same file (eval_actions, check_facts, query) are careful to free rules they build; this path isn't.

🔧 Proposed fix: validate the ruleset before compiling
         let union_to_set = self.proof_state.original_typechecking.is_none();
 
+        match self.rulesets.get(&rule.ruleset) {
+            Some(Ruleset::Rules(_)) => {}
+            Some(Ruleset::Combined(_)) => {
+                return Err(Error::CombinedRulesetError(rule.ruleset, rule.span));
+            }
+            None => return Err(Error::NoSuchRuleset(rule.ruleset, rule.span)),
+        }
+
         let compiled: Vec<(String, core::ResolvedCoreRule, egglog_bridge::RuleId)> = {
             ...
         };
 
-        if let Some(rules) = self.rulesets.get_mut(&rule.ruleset) {
-            match rules {
-                Ruleset::Rules(rules) => {
-                    for (name, core_rule, rule_id) in compiled {
-                        match rules.entry(name.clone()) {
-                            indexmap::map::Entry::Occupied(_) => {
-                                panic!("Rule '{name}' was already present")
-                            }
-                            indexmap::map::Entry::Vacant(e) => e.insert((core_rule, rule_id)),
-                        };
-                    }
-                    Ok(rule.name)
-                }
-                Ruleset::Combined(_) => Err(Error::CombinedRulesetError(rule.ruleset, rule.span)),
-            }
-        } else {
-            Err(Error::NoSuchRuleset(rule.ruleset, rule.span))
-        }
+        let Some(Ruleset::Rules(rules)) = self.rulesets.get_mut(&rule.ruleset) else {
+            unreachable!("ruleset validated above")
+        };
+        for (name, core_rule, rule_id) in compiled {
+            match rules.entry(name.clone()) {
+                indexmap::map::Entry::Occupied(_) => panic!("Rule '{name}' was already present"),
+                indexmap::map::Entry::Vacant(e) => e.insert((core_rule, rule_id)),
+            };
+        }
+        Ok(rule.name)
     }
🤖 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/lib.rs` around lines 1214 - 1271, Validate that rule.ruleset
exists and is a Ruleset::Rules before constructing compiled in add_rule. Move
the self.rulesets lookup and Combined/NoSuchRuleset error handling ahead of
self.backend.new_rule and translator.build, then compile and insert only after
validation succeeds, preserving duplicate-name handling.
🧹 Nitpick comments (6)
egglog-experimental/Cargo.toml (1)

21-28: 🧹 Nitpick | 🔵 Trivial

dd-backend is pulled into default via bin, adding DD/Timely to default builds.

Since default = ["bin"] and bin now includes dd-backend, every consumer building this crate with default features (including pure library users) compiles in egglog-experimental-dd and its Timely/Differential-Dataflow dependency tree, even if they never use the DD backend. Given DD is explicitly experimental (slower, higher memory, known correctness gaps per PR notes), consider whether main.rs's reliance on it could be cfg-gated so dd-backend is opt-in rather than bundled into the default feature set.

🤖 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-experimental/Cargo.toml` around lines 21 - 28, Remove dd-backend from
the default bin feature set so default builds do not enable
egglog-experimental-dd or its Timely/Differential-Dataflow dependencies. Keep
the dd-backend feature available as an explicit opt-in, and cfg-gate the main.rs
DD backend integration and related binary behavior behind that feature.
egglog/egglog-backend-trait/src/lib.rs (2)

384-397: 📐 Maintainability & Code Quality | 🔵 Trivial

Hardcoded min merge for container registration on non-bridge backends.

Line 390 hardcodes std::cmp::min(old, new) as the merge function for all container types on non-bridge backends. If different container sorts require different merge semantics, this will produce incorrect results. Consider whether this should be configurable per container type.

🤖 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-backend-trait/src/lib.rs` around lines 384 - 397, Update
register_container_ty so non-bridge backends obtain and use merge semantics
configurable for each container type instead of always applying std::cmp::min to
old and new values. Preserve the existing bridge registration path and container
registry validation, and route the selected merge function through the container
registration API.

253-267: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Default supports_action_registry() to false. Backends that actually expose an ActionRegistry can opt in explicitly; leaving this true makes the action_registry() panic reachable if a backend misses the override.

🤖 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-backend-trait/src/lib.rs` around lines 253 - 267, Change the
default implementation of Backend::supports_action_registry() to return false,
while leaving action_registry() and backend-specific opt-in overrides unchanged.
Backends that expose an ActionRegistry should continue to override
supports_action_registry() explicitly.
egglog/src/proofs/proof_extraction.rs (1)

87-108: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Proof-value lookup still does a linear scan. for_each_while walks the proof table row by row here, and the backend’s keyed lookup API is RHS-only, so it can’t replace this read path. If this is on a hot path, a read-only keyed accessor would remove the per-call scan.

🤖 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_extraction.rs` around lines 87 - 108, Replace the
linear proof-table scan in the proof-value lookup block with a read-only keyed
accessor that retrieves the row associated with witness_value directly. Add or
reuse a backend API that supports lookup by the proof table’s left-hand key
while preserving the existing behavior of returning the second value or None;
leave proof-function resolution unchanged.
egglog/src/proofs/proof_checker.rs (1)

23-50: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

New container-var-equality branch lacks test coverage.

The added is_container_var(lhs) && is_container_var(rhs) branch (two container-sorted variables equated without a container-producing primitive on either side) is a genuinely new code path. The existing regression test (proof_mode_allows_pair_container_side_conditions in proof_tests.rs) only exercises the pre-existing container-primitive case ((= c (pair e 1))), not this new var/var case. Since containers have no UF proof rows, a mismatch between what the encoder classifies as a side condition and what the checker re-validates would fail silently or error confusingly — worth a dedicated test.

Could you add a case where two already-bound container-sorted variables (or one bound/one produced only via variable propagation, not directly from a primitive call) are compared for equality in a rule body, to exercise this new branch end-to-end?

🤖 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_checker.rs` around lines 23 - 50, Add a dedicated
proof regression test covering the is_container_side_condition branch where an
Eq compares two container-sorted ResolvedExpr::Var values with no
container-producing primitive on either side. Build a rule body using two
already-bound container variables (or variable propagation), assert their
equality, and verify proof checking succeeds end-to-end alongside
proof_mode_allows_pair_container_side_conditions.
egglog/src/lib.rs (1)

1488-1494: 🩺 Stability & Availability | 🔵 Trivial

Bridge-only capability mismatches panic via .expect() instead of returning a recoverable Error.

unstable-fn resolution (here and in BackendRule::prim) and input_file's fact loading all hard-panic when the backend isn't egglog_bridge::EGraph, whereas this same PR introduces a graceful Error::BackendRequiresTermEncoding for a different backend-capability mismatch (Line 2215-2218). For a library whose whole point is now a pluggable backend SPI, an embedder using a custom backend that hits (input ...) or unstable-fn gets an unrecoverable panic instead of a catchable error. This is likely an acceptable interim tradeoff for these currently bridge-only features, but worth tracking as the backend SPI matures — panics here will crash any host application, not just the egglog CLI.

Also applies to: 2020-2025, 2765-2769

🤖 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/lib.rs` around lines 1488 - 1494, Replace the bridge downcast
`.expect()` calls in unstable-function resolution, `BackendRule::prim`, and
`input_file` fact loading with recoverable backend-capability errors. Propagate
an appropriate `Error` when the backend is not `egglog_bridge::EGraph`,
preserving normal behavior for the reference bridge backend and avoiding any
panic for custom backends.
🤖 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/interpret.rs`:
- Around line 218-260: Replace the release-disabled debug_assert_eq! in the
fused join mapping block with an unconditional validation, or derive caller
positions by looking up each value from fused.rule_indices() in atom_rule_idxs
rather than assuming positional equality with atom_positions. Ensure cached
FusedDdJoin entries cannot silently map DD outputs to the wrong caller rule, and
preserve fail-fast behavior on any ordering mismatch.

In `@egglog/src/proofs/proof_encoding.rs`:
- Around line 950-972: Remove the local is_container_primitive,
is_container_var, and is_container_side_condition helpers in the proof encoding
logic, and reuse the corresponding shared pub(super) predicate from
proof_checker.rs. Update the call sites to reference that shared implementation
so encoding and verification classify container side conditions identically.

In `@egglog/src/scheduler.rs`:
- Around line 237-242: Update step_rules_with_scheduler and its
with_execution_state closure to avoid the expect on the downcast to
egglog_bridge::EGraph. Hoist the bridge conversion outside the per-rule
iteration where practical, convert a non-bridge backend failure into
Error::BackendError, and propagate it through the existing Result<RunReport,
Error> flow.

---

Outside diff comments:
In `@egglog/src/lib.rs`:
- Around line 1214-1271: Validate that rule.ruleset exists and is a
Ruleset::Rules before constructing compiled in add_rule. Move the self.rulesets
lookup and Combined/NoSuchRuleset error handling ahead of self.backend.new_rule
and translator.build, then compile and insert only after validation succeeds,
preserving duplicate-name handling.

In `@egglog/src/proofs/proof_extractor.rs`:
- Around line 176-194: Update first_output and first_input to handle empty
backend tables without an uninformative unwrap panic. Prefer propagating a
Result<Value, Error> through their callers; otherwise replace each unwrap with a
descriptive expect message that includes function_name, while preserving the
existing first-row extraction behavior.

---

Nitpick comments:
In `@egglog-experimental/Cargo.toml`:
- Around line 21-28: Remove dd-backend from the default bin feature set so
default builds do not enable egglog-experimental-dd or its
Timely/Differential-Dataflow dependencies. Keep the dd-backend feature available
as an explicit opt-in, and cfg-gate the main.rs DD backend integration and
related binary behavior behind that feature.

In `@egglog/egglog-backend-trait/src/lib.rs`:
- Around line 384-397: Update register_container_ty so non-bridge backends
obtain and use merge semantics configurable for each container type instead of
always applying std::cmp::min to old and new values. Preserve the existing
bridge registration path and container registry validation, and route the
selected merge function through the container registration API.
- Around line 253-267: Change the default implementation of
Backend::supports_action_registry() to return false, while leaving
action_registry() and backend-specific opt-in overrides unchanged. Backends that
expose an ActionRegistry should continue to override supports_action_registry()
explicitly.

In `@egglog/src/lib.rs`:
- Around line 1488-1494: Replace the bridge downcast `.expect()` calls in
unstable-function resolution, `BackendRule::prim`, and `input_file` fact loading
with recoverable backend-capability errors. Propagate an appropriate `Error`
when the backend is not `egglog_bridge::EGraph`, preserving normal behavior for
the reference bridge backend and avoiding any panic for custom backends.

In `@egglog/src/proofs/proof_checker.rs`:
- Around line 23-50: Add a dedicated proof regression test covering the
is_container_side_condition branch where an Eq compares two container-sorted
ResolvedExpr::Var values with no container-producing primitive on either side.
Build a rule body using two already-bound container variables (or variable
propagation), assert their equality, and verify proof checking succeeds
end-to-end alongside proof_mode_allows_pair_container_side_conditions.

In `@egglog/src/proofs/proof_extraction.rs`:
- Around line 87-108: Replace the linear proof-table scan in the proof-value
lookup block with a read-only keyed accessor that retrieves the row associated
with witness_value directly. Add or reuse a backend API that supports lookup by
the proof table’s left-hand key while preserving the existing behavior of
returning the second value or None; leave proof-function resolution unchanged.
🪄 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: 7a970c91-5347-4804-a2d6-4b36fe195b3d

📥 Commits

Reviewing files that changed from the base of the PR and between ef9fe20 and 7e53fe5.

⛔ Files ignored due to path filters (5)
  • Cargo.lock is excluded by !**/*.lock
  • egglog-experimental/tests/fixtures/eggcc-2mm-pass1-merge-old.egg is excluded by !**/*.egg
  • egglog/tests/math-microbenchmark-mini.egg is excluded by !**/*.egg
  • egglog/tests/snapshots/files__proof_unsupported_files.snap is excluded by !**/*.snap
  • egglog/tests/snapshots/files__shared_snapshot_math_microbenchmark_mini.snap is excluded by !**/*.snap
📒 Files selected for processing (42)
  • .gitignore
  • Cargo.toml
  • bench.py
  • egglog-experimental/Cargo.toml
  • egglog-experimental/dd/.gitignore
  • egglog-experimental/dd/Cargo.toml
  • egglog-experimental/dd/src/compile.rs
  • egglog-experimental/dd/src/dd_native.rs
  • egglog-experimental/dd/src/external_func.rs
  • egglog-experimental/dd/src/interpret.rs
  • egglog-experimental/dd/src/lib.rs
  • egglog-experimental/dd/src/rule_builder.rs
  • egglog-experimental/dd/tests/files.rs
  • egglog-experimental/dd/tests/rewrite_join.rs
  • egglog-experimental/dd/tests/smoke.rs
  • egglog-experimental/src/lib.rs
  • egglog-experimental/src/main.rs
  • egglog/CHANGELOG.md
  • egglog/Cargo.toml
  • 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/cli.rs
  • egglog/src/lib.rs
  • egglog/src/prelude.rs
  • egglog/src/proofs/proof_checker.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_simplification.rs
  • egglog/src/proofs/proof_tests.rs
  • egglog/src/scheduler.rs
  • egglog/src/serialize.rs
  • egglog/src/sort/fn.rs
  • egglog/src/sort/mod.rs
  • egglog/src/sort/pair.rs
  • egglog/src/sort/set.rs
  • egglog/src/typechecking.rs
  • samply_analysis.py
  • test_bench.py

Comment thread egglog-experimental/dd/src/interpret.rs
Comment thread egglog/src/proofs/proof_encoding.rs Outdated
Comment thread egglog/src/scheduler.rs Outdated

@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

🤖 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/scheduler.rs`:
- Line 378: Make rule construction transactional across qrule_builder.query and
the later query-rule build: if either operation fails, release the previously
registered external function, decided table, and query rule handles before
propagating the error. Update the surrounding rule-construction flow so
successful construction retains all registrations, while any failure cleans up
every resource allocated during that attempt.
🪄 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: 2f7c126e-25f3-4e16-98c0-133216f784c2

📥 Commits

Reviewing files that changed from the base of the PR and between 7e53fe5 and 9990f68.

📒 Files selected for processing (15)
  • bench.py
  • egglog-experimental/Cargo.toml
  • egglog-experimental/dd/src/interpret.rs
  • egglog-experimental/dd/src/lib.rs
  • egglog-experimental/dd/tests/smoke.rs
  • egglog-experimental/src/main.rs
  • egglog/egglog-backend-trait/src/backend_impl.rs
  • egglog/egglog-backend-trait/src/lib.rs
  • egglog/src/lib.rs
  • egglog/src/proofs/proof_encoding.rs
  • egglog/src/proofs/proof_extraction.rs
  • egglog/src/proofs/proof_extractor.rs
  • egglog/src/proofs/proof_tests.rs
  • egglog/src/scheduler.rs
  • test_bench.py
🚧 Files skipped from review as they are similar to previous changes (12)
  • egglog/src/proofs/proof_extractor.rs
  • egglog/src/proofs/proof_tests.rs
  • egglog/egglog-backend-trait/src/backend_impl.rs
  • egglog-experimental/dd/tests/smoke.rs
  • egglog-experimental/src/main.rs
  • egglog/egglog-backend-trait/src/lib.rs
  • egglog-experimental/dd/src/interpret.rs
  • test_bench.py
  • egglog-experimental/dd/src/lib.rs
  • egglog/src/lib.rs
  • egglog/src/proofs/proof_encoding.rs
  • bench.py

Comment thread egglog/src/scheduler.rs Outdated

@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

Caution

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

⚠️ Outside diff range comments (1)
egglog/src/lib.rs (1)

2807-2831: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

unstable-fn should propagate resolution failures instead of panicking
This branch turns a recoverable lookup error into a panic. To match the other unstable-fn paths, return the Err here and add a way to release the pre-registered panic_id first; RuleBuilderOps doesn’t expose that cleanup hook today.

🤖 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/lib.rs` around lines 2807 - 2831, The unstable-fn resolution path
currently panics on errors instead of returning them. Update
resolve_function_container_target_with_context handling to propagate its Err
result, and extend RuleBuilderOps with the cleanup operation needed to release
the pre-registered panic_id before returning the failure.
🤖 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/egglog-bridge/src/lib.rs`:
- Around line 426-469: Add tests covering both failure paths in
remove_last_table: unknown or non-last FunctionId requests must return an error
without altering registrations, and a failed db.remove_last_table(info.table)
must return an error while restoring the removed function through
self.funcs.insert. Verify the relevant function/table state remains intact after
each failure, alongside the existing success-path test.

---

Outside diff comments:
In `@egglog/src/lib.rs`:
- Around line 2807-2831: The unstable-fn resolution path currently panics on
errors instead of returning them. Update
resolve_function_container_target_with_context handling to propagate its Err
result, and extend RuleBuilderOps with the cleanup operation needed to release
the pre-registered panic_id before returning the failure.
🪄 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: 0fa8838a-fabf-4b85-ab97-1bd290d9d3bc

📥 Commits

Reviewing files that changed from the base of the PR and between 9990f68 and 405fa4d.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (25)
  • Cargo.toml
  • bench.py
  • egglog-experimental/dd/Cargo.toml
  • egglog-experimental/dd/src/compile.rs
  • egglog-experimental/dd/src/dd_native.rs
  • egglog-experimental/dd/src/interpret.rs
  • egglog-experimental/dd/src/lib.rs
  • egglog-experimental/dd/src/rule_builder.rs
  • egglog-experimental/src/main.rs
  • egglog/core-relations/src/dependency_graph.rs
  • 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/egglog-bridge/src/rule.rs
  • egglog/egglog-bridge/src/tests.rs
  • egglog/numeric-id/src/lib.rs
  • egglog/numeric-id/src/tests.rs
  • egglog/src/lib.rs
  • egglog/src/proofs/proof_encoding.rs
  • egglog/src/proofs/proof_extraction.rs
  • egglog/src/scheduler.rs
  • egglog/src/typechecking.rs
  • test_bench.py
💤 Files with no reviewable changes (1)
  • egglog/egglog-backend-trait/Cargo.toml

Comment thread egglog/egglog-bridge/src/lib.rs

@oflatt oflatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We're getting close! Some scattered feedback, hopefully we can get a clean diff and not touch proof checker / proof encoding

/// leading `@` and trailing digits don't matter. Order: most specific first
/// (`singleparent` and the `_subsume` variant before their bare forms;
/// `uf_function_index` before `uf_update`).
pub(crate) fn rule_category(name: &str) -> &'static str {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Perhaps the problem here is more with the backend trait not preserving the ruleset name? Then we could remove this if we fixed that?

Comment thread egglog-experimental/dd/tests/files.rs Outdated

/// The stable content of egglog's shared snapshot for `stem`, if egglog produced
/// one. Strips insta's `---`-delimited YAML header.
fn egglog_snapshot(stem: &str) -> Option<String> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

perhaps instead of reading the snapshot and doing string mangling, we could use insta the normal way? That probably works even if the files are in another dir

use crate::EGraph;

/// Accumulates a rule's body ops and head ops, then registers them.
pub struct FlowlogRuleBuilder<'a> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Optional: I wonder if we should reconsider the rule builder API we inherited from the old backend. If flowlog has to construct a AST anyways, perhaps we should just send the AST to the backend? The existing backend could use a rule builder internally if it wants to.

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.

Moves generic rules to egglog-ast and sent in rule spec to backends

Comment thread egglog/src/proofs/proof_checker.rs Outdated
/// (set-of b))`, etc. Its proof is a bare [`Justification::Eval`] marker and it
/// is verified by re-evaluation in [`ProofStore::check_side_condition`] rather
/// than by matching a premise proposition.
/// A container side condition is a rule-body fact whose equality is over

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Did you mean to change the proof checker? I thought the change to container side conditions was not necessary after all?

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.

reverted changes

Comment thread egglog/src/proofs/proof_encoding.rs Outdated
pub uf_function: HashMap<String, String>,
/// Maps sort name -> proof function name (set from :internal-proof-func annotation).
pub proof_func_parent: HashMap<String, String>,
/// Function name -> (hidden current-value function, input arity). The

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What are these proof encoding changes? Perhaps the diff got messed up or something with an old PR?

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.

reverted

@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

Caution

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

⚠️ Outside diff range comments (2)
egglog/egglog-bridge/src/rule.rs (1)

207-214: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

new_panic/panic don't track their allocated external function for cleanup, unlike new_panic_lazy.

new_panic_lazy pushes its allocated id into self.resources.external_funcs so it gets freed via owned_external_funcs when the rule is freed or rolled back. new_panic (line 208) and panic (line 739) instead call self.resources.egraph.new_panic(...) directly and never record the id anywhere. Since EGraph::new_panic increments a message-keyed reference count on every call, each call through these two RuleBuilder methods permanently bumps that count with no corresponding decrement when the rule is later freed via EGraph::free_rule/rollback — undermining the reference-counted cleanup this diff otherwise establishes (bounded leak, growing with the set of distinct messages passed to .panic()/.new_panic() across freed rules).

🩹 Suggested fix
     pub fn new_panic(&mut self, message: String) -> crate::ExternalFunctionId {
-        self.resources.egraph.new_panic(message)
+        let panic = self.resources.egraph.new_panic(message);
+        self.resources.external_funcs.push(panic);
+        panic
     }
     pub fn panic(&mut self, message: String) {
-        let panic = self.resources.egraph.new_panic(message.clone());
+        let panic = self.new_panic(message.clone());
         let ret_ty = ColumnTy::Id;

Also applies to: 737-747

🤖 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/rule.rs` around lines 207 - 214, Update
RuleBuilder::new_panic and RuleBuilder::panic to record each returned
ExternalFunctionId in self.resources.external_funcs, matching new_panic_lazy.
Preserve returning the allocated id while ensuring owned_external_funcs can
release these registrations during rule cleanup or rollback.
egglog/src/typechecking.rs (1)

329-357: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Avoid the raw panic!() fallback for registry-free backends. register_registry_primitive still installs a panic-on-call stub when supports_action_registry() is false, and nothing guards add_read_primitive / add_write_primitive / add_full_primitive from being used on such a backend. That turns a reachable primitive call into a process abort instead of a controlled error. Route this through the backend’s deferred-panic path or another Result-based failure channel.

🤖 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/typechecking.rs` around lines 329 - 357, Update
register_registry_primitive so registry-free backends do not install a raw
panic-on-call stub. Route attempts to invoke the unsupported primitive through
the backend’s existing deferred-panic mechanism or another Result-based error
path, and ensure add_read_primitive, add_write_primitive, and add_full_primitive
remain controlled failures rather than process aborts.
🤖 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/compile.rs`:
- Around line 7-8: Align compile::MAX_ARITY with dd_native::W so add_table
rejects any arity unsupported by fixed-width join rows, including arities 49–64;
derive one constant from the other if practical. In
egglog-experimental/dd/src/dd_native.rs lines 184-185, retain the existing
oversized-atom check and ensure validation occurs before plan_join can receive
an admitted unsupported relation; no direct change is required there if the
MAX_ARITY fix guarantees this ordering.

---

Outside diff comments:
In `@egglog/egglog-bridge/src/rule.rs`:
- Around line 207-214: Update RuleBuilder::new_panic and RuleBuilder::panic to
record each returned ExternalFunctionId in self.resources.external_funcs,
matching new_panic_lazy. Preserve returning the allocated id while ensuring
owned_external_funcs can release these registrations during rule cleanup or
rollback.

In `@egglog/src/typechecking.rs`:
- Around line 329-357: Update register_registry_primitive so registry-free
backends do not install a raw panic-on-call stub. Route attempts to invoke the
unsupported primitive through the backend’s existing deferred-panic mechanism or
another Result-based error path, and ensure add_read_primitive,
add_write_primitive, and add_full_primitive remain controlled failures rather
than process aborts.
🪄 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: 6bc351c7-5498-4072-935c-9beabc0a696a

📥 Commits

Reviewing files that changed from the base of the PR and between 405fa4d and efd5b70.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (21)
  • egglog-experimental/dd/Cargo.toml
  • egglog-experimental/dd/src/compile.rs
  • egglog-experimental/dd/src/dd_native.rs
  • egglog-experimental/dd/src/interpret.rs
  • egglog-experimental/dd/src/lib.rs
  • egglog-experimental/dd/tests/files.rs
  • egglog/egglog-ast/src/core.rs
  • egglog/egglog-ast/src/lib.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/egglog-bridge/src/rule.rs
  • egglog/egglog-bridge/src/tests.rs
  • egglog/src/constraint.rs
  • egglog/src/core.rs
  • egglog/src/lib.rs
  • egglog/src/proofs/proof_tests.rs
  • egglog/src/scheduler.rs
  • egglog/src/typechecking.rs
  • egglog/tests/typed_primitive.rs
💤 Files with no reviewable changes (1)
  • egglog/src/proofs/proof_tests.rs

Comment thread egglog-experimental/dd/src/compile.rs Outdated

@oflatt oflatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

good job

@oflatt oflatt merged commit 22f4840 into saulshanabrook:main Jul 13, 2026
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.

3 participants