Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions egglog-experimental/benches/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::{
path::{Path, PathBuf},
};

use egglog_experimental::{EGraph, new_experimental_egraph_with_proofs};
use egglog_experimental::new_experimental_egraph_with_proofs;

const CODSPEED_FILES: &[&str] = &[
"egglog/tests/web-demo/rw-analysis.egg",
Expand Down Expand Up @@ -53,13 +53,14 @@ fn benchmark_cases() -> Vec<BenchCase> {
#[divan::bench(args = benchmark_cases())]
fn files(case: &BenchCase) {
let mut egraph = new_experimental_egraph_with_proofs();
// Benchmark the serial path, independent of the host's core count.
egraph.set_num_threads(1);
egraph
.parse_and_run_program(Some(case.filename.clone()), &case.program)
.unwrap_or_else(|err| panic!("{} failed: {err}", case.path.display()));
std::mem::forget(egraph);
}

fn main() {
EGraph::set_num_threads(1);
divan::main();
}
8 changes: 6 additions & 2 deletions egglog-experimental/src/either.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use egglog::ast::Expr;
use egglog::ast::{Expr, Span};
use egglog::prelude::ContainerSort;
use egglog::sort::{ContainerValues, Presort, ValueRebuilder};
use egglog::{
Expand Down Expand Up @@ -81,6 +81,7 @@ impl Presort for EitherSort {
typeinfo: &mut TypeInfo,
name: String,
args: &[Expr],
span: Span,
) -> Result<ArcSort, TypeError> {
if let [Expr::Var(left_span, left), Expr::Var(right_span, right)] = args {
let left = typeinfo
Expand All @@ -97,7 +98,10 @@ impl Presort for EitherSort {
}
.to_arcsort())
} else {
panic!("Either sort requires exactly two arguments")
Err(TypeError::BadPresortArguments(
Self::presort_name().to_owned(),
span,
))
}
}
}
Expand Down
8 changes: 6 additions & 2 deletions egglog-experimental/src/maybe.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use egglog::ast::{Expr, Literal};
use egglog::ast::{Expr, Literal, Span};
use egglog::prelude::ContainerSort;
use egglog::sort::{ContainerValues, F, Presort, ValueRebuilder};
use egglog::{
Expand Down Expand Up @@ -64,6 +64,7 @@ impl Presort for MaybeSort {
typeinfo: &mut TypeInfo,
name: String,
args: &[Expr],
span: Span,
) -> Result<ArcSort, TypeError> {
if let [Expr::Var(span, element)] = args {
let element = typeinfo
Expand All @@ -76,7 +77,10 @@ impl Presort for MaybeSort {
}
.to_arcsort())
} else {
panic!("Maybe sort requires exactly one argument")
Err(TypeError::BadPresortArguments(
Self::presort_name().to_owned(),
span,
))
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions egglog/.github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ jobs:
tool: nextest,cargo-insta
- uses: Swatinem/rust-cache@v2
- run: make test
# Build the library the way downstream crates consume it: no default
# features (so `clap`/`clap_derive` are absent). This guards against
# relying on another crate to unify a `syn` feature onto ours — the bug
# that broke downstream builds when clap_derive moved to syn 3.x.
no-default-features:
if: github.event_name != 'issue_comment'
runs-on: ubuntu-latest
steps:
- run: echo "CARGO_INCREMENTAL=0" >> "$GITHUB_ENV"
- uses: actions/checkout@v3
- uses: Swatinem/rust-cache@v2
- run: cargo check -p egglog --no-default-features --lib
coverage:
if: github.event_name != 'issue_comment'
runs-on: ubuntu-latest
Expand Down
8 changes: 8 additions & 0 deletions egglog/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
- Fix user-defined primitives (registered through the Rust API after construction) being reported as unbound under term encoding / proofs: primitive registration now also reaches the term-encoding typechecker, so the encoder can typecheck the encoded program. Previously callers had to manually register the primitive on `proof_state.original_typechecking` as well.
- **Pluggable backend SPI.** `EGraph::with_backend(Box<dyn Backend>)` lets a third party drive the egglog frontend with their own backend (see the `egglog-backend-trait` crate and the `egglog-experimental-dd` example). A backend without a native union-find declares `Backend::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 a term-encoding-only backend without it now errors with `Error::BackendRequiresTermEncoding` instead of silently dropping `union`s.
- Route the term/proof encoding's `get-fresh!` (id minting) and `set-if-empty` (view canonicalization) primitives through the backend SPI (`Backend::register_get_fresh` / `register_set_if_empty` / `register_view_column_read`), so a backend can service them against its own storage instead of reaching into core-relations tables. The backend SPI stays proof-agnostic: reading a view's proof column is just a generic view-column read. The differential-dataflow backend implements these over its host-side mirror and now runs eq-sort programs under term/proof encoding.
- Fix a build failure when egglog is compiled without default features (as a library dependency). The `egglog-add-primitive` proc macro parses full Rust expressions and now declares `syn`'s `full` feature directly, instead of relying on another crate (`clap_derive`, via the `bin` feature) to unify it onto our `syn`. This surfaced when `clap_derive` moved to `syn` 3.x. A CI job now builds `-p egglog --no-default-features` to catch regressions.
- Convert many reachable `panic!`/`unwrap`/`expect`/`todo!` sites into recoverable errors, so malformed or edge-case programs report an error instead of aborting the process. Examples now returning `Error`/`TypeError`/`ParseError`/`ProveExistsError`: malformed sort-constructor declarations like `(sort S (Vec))` or `(sort S (UnstableFn))`; negative `extract` variant counts; duplicate rule names; `unstable-fn` referencing an unknown, non-literal, or mis-typed target; `(fail ...)` wrapping `include` or an empty expansion; subsuming a non-call rewrite; running `prove`/`prove-exists` without proofs enabled; and missing/unreadable files for `input`, `print-function`, `print-overall-statistics`, and the CLI. Several primitives became partial (returning no result instead of panicking) on out-of-range input: `vec-set`/`vec-remove` indices, `multiset-pick` on an empty multiset, count overflow in `multiset` operations, and the numeric primitives `bigint <<`/`>>`, `bigrat`, and `log2`. A few scheduler edge cases (unknown ruleset, rules with no free variables) no longer panic; variable-free rules now correctly apply their actions when scheduled. Primitive resolution now returns `TypeError::AmbiguousPrimitive`/`TypeError::UnresolvedPrimitive` instead of panicking when duplicate same-signature registrations are indistinguishable or nothing resolves; both direct calls and `unstable-fn` primitive targets report the same variants. `step_rules_with_scheduler` now restores its `rulesets`/`schedulers` on every fallible path, so an error during scheduled rule compilation no longer leaves the `EGraph` in a corrupted state.
Comment thread
oflatt-claude marked this conversation as resolved.
- **Breaking:** `EGraph::print_function` now takes its output sink as `Option<(File, PathBuf)>` plus a `Span`, so write failures return `Error::IoError` instead of panicking.
- Speed up query evaluation by building on-the-fly per-subset column indexes as sorted arrays (`SortedColumnIndex`) instead of hash maps. These indexes are typically iterated once and probed a bounded number of times over high-cardinality columns, so skipping hash-table construction is a large win (e.g. ~33% faster on the `gemma` benchmark).
- Share trie roots (and their cached sub-indexes and child nodes) across query plans within a single `run_rule_set` instead of rebuilding a fresh trie per plan. Plans that scan the same table under the same header (fast) constraints reuse one root, so on-the-fly per-subset index builds happen once rather than per plan; only roots that more than one plan uses are shared, so workloads that would not benefit keep the per-plan behavior. Large speedups on transformer workloads (e.g. ~15% faster on `whisper`, ~12% on `gemma`, ~8% on `qwen3_moe`).
- Add `make nightly` and `scripts/nightly_bench.py`, a hyperfine-based benchmark harness that measures every `tests/**/*.egg` program at 1/2/4/8 threads and (where supported) in proof-testing mode, caps each run at a 2-minute timeout, skips sub-50ms programs, and emits an HTML dashboard (one row per benchmark, one column per configuration) for nightly.cs.washington.edu. The dashboard uses [eval-live](https://github.com/oflatt/eval-live) for interactive filtering and sorting.
- Rework the term/proof encoding's union-find and congruence maintenance,
substantially reducing proof-mode time and memory.
Expand Down Expand Up @@ -54,9 +59,12 @@
or as the head of a call expression.
- Add typed `EGraph` extension state that clones with `EGraph` and is restored by `push`/`pop`.
- Fix custom scheduler queries so subsumed rows are not offered as fresh matches.
- Replace the global Rayon thread pool with an `egglog-concurrency` scoped `ThreadPool`; configure parallelism per `EGraph` via `with_num_threads` / `set_num_threads`.
Comment thread
oflatt-claude marked this conversation as resolved.
- Report full source file paths in egglog span and error messages.
- Fix seminaive matching after nested containers rebuild in place by propagating dirty container ids through parent containers.
- Fix multi-column secondary index rebuilds so each value's rows come back sorted by row id, and make all rebuild paths (serial, parallel, and bulk) record a row once even when its value repeats across covered columns (#914).
- Render nullary AST calls without a trailing space, e.g. (foo) instead of (foo ).
- Escape `"` and `\` when displaying string literals so printed/serialized programs round-trip through the parser.
- Add a BigRat to-i64 primitive for integral rationals.
- Add f64 exp, log, and sqrt primitives.
- Add `RunReport::can_stop` so scheduler progress can be reported separately from database updates.
Expand Down
7 changes: 2 additions & 5 deletions egglog/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion egglog/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ egglog-numeric-id = { workspace = true }
egglog-add-primitive = { workspace = true }
egglog-reports = { workspace = true }
egglog-backend-trait = { workspace = true }
rayon = { workspace = true }
serde_json = { workspace = true }

[build-dependencies]
Expand Down
10 changes: 7 additions & 3 deletions egglog/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ TESTS=$(shell find tests/ -type f -name '*.egg' -not -name '*repro-*')

WWW=${PWD}/target/www

# Keep release-mode test builds checking debug assertions without changing the
# normal release profile used by CodSpeed benchmarks and production binaries.
TEST_PROFILE_ENV=CARGO_PROFILE_RELEASE_DEBUG_ASSERTIONS=true

all: test nits docs

# Build egglog and benchmark every tests/*.egg file with hyperfine, writing an
Expand All @@ -18,14 +22,14 @@ nightly:
nightly/.venv/bin/python scripts/nightly_bench.py

test: doctest
cargo insta test --test-runner nextest --release --workspace --unreferenced reject
$(TEST_PROFILE_ENV) cargo insta test --test-runner nextest --release --workspace --unreferenced reject

coverage:
cargo llvm-cov nextest --release --workspace --lcov --output-path lcov.info
$(TEST_PROFILE_ENV) cargo llvm-cov nextest --release --workspace --lcov --output-path lcov.info
# Note: doctests are not included in coverage reports

doctest:
cargo test --doc --release --workspace
$(TEST_PROFILE_ENV) cargo test --doc --release --workspace


nits:
Expand Down
19 changes: 1 addition & 18 deletions egglog/benches/common.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
#![allow(dead_code)]

use egglog::EGraph;
use std::{fmt, sync::Once};
use std::fmt;

#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;

static CONFIGURE_RAYON: Once = Once::new();

pub fn run_example(filename: &str, program: &str, proof_testing: bool) {
let mut egraph = if proof_testing {
EGraph::new_with_proofs().with_proof_testing()
Expand Down Expand Up @@ -38,8 +36,6 @@ impl fmt::Display for BenchCase {
}

pub fn bench_cases(glob: &str) -> Vec<BenchCase> {
configure_rayon_once();

let mut cases = Vec::new();

// Add regular test cases
Expand Down Expand Up @@ -83,8 +79,6 @@ const PROOF_UNSUPPORTED_FILES: &[&str] = &[
];

pub fn bench_cases_proof_testing(glob: &str) -> Vec<BenchCase> {
configure_rayon_once();

glob::glob(glob)
.unwrap()
.filter_map(Result::ok)
Expand Down Expand Up @@ -114,16 +108,5 @@ fn proof_benchmark_supported(path: &std::path::Path) -> bool {
}

pub fn bench_case(case: &BenchCase) {
configure_rayon_once();

run_example(&case.filename, &case.program, case.proof_testing);
}

pub fn configure_rayon_once() {
CONFIGURE_RAYON.call_once(|| {
rayon::ThreadPoolBuilder::new()
.num_threads(1)
.build_global()
.unwrap();
});
}
14 changes: 1 addition & 13 deletions egglog/benches/rust_api_benchmarking.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
mod common;

#[derive(Clone, Copy)]
struct RustRuleBenchCase {
n_facts_input: Option<usize>,
Expand All @@ -23,8 +21,6 @@ struct RustRuleBenchInput {
fn match_only_rust_rule_setup(case: RustRuleBenchCase) -> RustRuleBenchInput {
use egglog::prelude::*;

common::configure_rayon_once();

let mut program = String::new();
program.push_str("(relation R (i64))\n");

Expand Down Expand Up @@ -120,8 +116,6 @@ fn rust_rule_match_with_serialize(bencher: divan::Bencher, case: RustRuleBenchCa
fn insert_loop_setup(case: RustRuleInsertLoopBenchCase) -> RustRuleBenchInput {
use egglog::prelude::*;

common::configure_rayon_once();

let mut program = String::new();
program.push_str("(relation R (i64))\n");
program.push_str("(function f (i64) i64 :no-merge)\n");
Expand Down Expand Up @@ -162,8 +156,6 @@ fn insert_loop_setup(case: RustRuleInsertLoopBenchCase) -> RustRuleBenchInput {
fn tableaction_hot_path_setup(case: RustRuleTableActionBenchCase) -> RustRuleBenchInput {
use egglog::prelude::*;

common::configure_rayon_once();

let mut program = String::new();
program.push_str("(relation R (i64))\n");
program.push_str("(function f (i64) i64 :no-merge)\n");
Expand Down Expand Up @@ -299,14 +291,12 @@ impl std::fmt::Display for ReadScanBenchCase {
fn read_scan_setup(case: ReadScanBenchCase) -> egglog::EGraph {
use std::fmt::Write;

common::configure_rayon_once();

let mut program = String::from("(sort Math)\n(constructor Add (i64 i64) Math)\n");
for i in 0..case.n_enodes {
let _ = writeln!(&mut program, "(Add {} {})", i as i64, (i + 1) as i64);
}

let mut egraph = egglog::EGraph::default();
let mut egraph = egglog::EGraph::new(1);
Comment thread
oflatt-claude marked this conversation as resolved.
egraph.parse_and_run_program(None, &program).unwrap();
egraph
}
Expand Down Expand Up @@ -340,8 +330,6 @@ fn main() {

fn fib_setup() -> RustRuleBenchInput {
use egglog::prelude::*;
common::configure_rayon_once();

let mut program = String::new();
program.push_str("(function fib (i64) i64 :no-merge)");
program.push_str("(set (fib 0) 0)\n");
Expand Down
6 changes: 5 additions & 1 deletion egglog/concurrency/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ readme = { workspace = true }
[dependencies]
arc-swap = { workspace = true}
bumpalo = { workspace = true}
rayon = { workspace = true}
crossbeam = { workspace = true}
egglog-numeric-id = { workspace = true }
smallvec = { workspace = true }

Expand All @@ -23,3 +23,7 @@ rayon = { workspace = true}
[[bench]]
name = "simple_read"
harness = false

[[bench]]
name = "sum_vector"
harness = false
Loading
Loading