Skip to content

Commit f9289da

Browse files
Add eggcc container benchmark and proof coverage
Stacked on the subtree-sync branch. This preserves the final reviewed tree from codex/eggcc-container-benchmark while separating local benchmark, proof, CI, and review-fix changes from the subtree merge commits. The hash index ordering fix from egraphs-good/egglog#914 lives in the subtree-sync branch.
1 parent cf26722 commit f9289da

88 files changed

Lines changed: 16186 additions & 144 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build.yml

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ jobs:
4141
- run: rustup component add clippy
4242
- uses: Swatinem/rust-cache@v2
4343
- run: cargo test --workspace
44-
- run: cargo test --workspace proof
4544
- run: cargo clippy --workspace --all-targets -- -D warnings
4645

4746
benchmark-smoke:
@@ -59,22 +58,29 @@ jobs:
5958
- run: uv run --locked ./bench.py --rounds 1 --treatments off --timeout-sec 120 --report .reports.ci.jsonl egglog/tests/integer_math.egg
6059

6160
codspeed:
61+
name: codspeed (${{ matrix.mode }})
6262
permissions:
6363
contents: read
6464
id-token: write
6565
runs-on: ubuntu-latest
6666
timeout-minutes: 20
67+
strategy:
68+
fail-fast: false
69+
matrix:
70+
mode:
71+
- simulation
72+
- memory
6773
steps:
6874
- run: echo "CARGO_INCREMENTAL=0" >> "$GITHUB_ENV"
6975
- uses: actions/checkout@v4
7076
with:
7177
persist-credentials: false
7278
- uses: taiki-e/install-action@v2
7379
with:
74-
tool: cargo-codspeed@4.0.1
80+
tool: cargo-codspeed@5.0.1
7581
- uses: Swatinem/rust-cache@v2
76-
- run: cargo codspeed build -p egglog-experimental --bench files
82+
- run: cargo codspeed build --measurement-mode ${{ matrix.mode }} -p egglog-experimental --bench files
7783
- uses: CodSpeedHQ/action@v4
7884
with:
79-
run: cargo codspeed run files
80-
mode: simulation
85+
run: cargo codspeed run --measurement-mode ${{ matrix.mode }} files
86+
mode: ${{ matrix.mode }}

AGENTS.md

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,10 @@ session's `xhigh` reasoning effort instead.
1717

1818
## Tests
1919

20-
Use this order for normal changes:
20+
Use this order for normal full validation:
2121

2222
```bash
2323
cargo test --workspace
24-
cargo test --workspace proof
2524
cargo clippy --workspace --all-targets -- -D warnings
2625
uv lock --check
2726
uv run --locked ruff format --check bench.py test_bench.py
@@ -30,31 +29,38 @@ uv run --locked mypy bench.py test_bench.py
3029
uv run --locked pytest -q
3130
```
3231

33-
For benchmark-runner changes, also smoke the CLI with a temporary report:
32+
For proof-focused changes, the filtered proof snapshot test is useful while
33+
iterating or when you want a focused proof rerun:
3434

3535
```bash
36-
uv run --locked ./bench.py --rounds 1 --treatments off --report .reports.smoke.jsonl \
37-
egglog/tests/integer_math.egg
36+
cargo test --workspace --test files 'proofs/'
3837
```
3938

40-
For agent-readable benchmark smoke output, pipe report rows from stdout and keep
41-
runner status and build diagnostics on stderr:
39+
`proofs/` runs explicit `(prove ...)` fixtures under `tests/proofs` and every
40+
proof-compatible file under proof-testing mode, snapshotting generated proofs.
41+
It is a name-filtered subset of `cargo test --workspace`, so do not run it after
42+
a full workspace test unless you want a focused proof rerun.
43+
44+
For benchmark-runner changes, smoke the public CLI entrypoint with a temporary,
45+
machine-readable report. Use stdout report mode so the run does not read or
46+
append to the default benchmark cache; runner status and build diagnostics stay
47+
on stderr.
4248

4349
```bash
44-
uv run --locked ./bench.py --rounds 1 --treatments off --report - \
50+
./bench.py --rounds 1 --treatments off --report - \
4551
egglog/tests/integer_math.egg > /tmp/egglog-encoding-bench-smoke.jsonl
4652
```
4753

4854
## Benchmarking
4955

5056
- Use `./bench.py` as the public benchmark entrypoint.
51-
- Do not pass egglog's `--save-report` during timed benchmark collection; it
52-
changes the measured work.
5357
- Keep `.reports.jsonl` append-only and ignored by git.
5458
- Use `--report -` when report rows should be streamed to stdout instead of
5559
appended to a cache file.
5660
- Runner status output always goes to stderr, including Rich progress and
5761
summary tables.
62+
- Benchmark inputs should not contain executable `(prove ...)` commands; use
63+
`(check ...)` in benchmark fixtures and cover proof extraction in proof tests.
5864
- Benchmark files are resolved relative to the command invocation directory,
5965
not relative to comparison targets.
6066
- Cache reuse is decided by binary SHA-256, file SHA-256, treatment, and

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ The development loop is:
1919
cargo test --workspace
2020

2121
# Run proof-focused tests while iterating.
22-
cargo test --workspace proof
22+
cargo test --workspace --test files 'proofs/'
2323

2424
# Run lint checks across the workspace.
2525
cargo clippy --workspace --all-targets
@@ -35,6 +35,15 @@ The root workspace includes both `egglog` and `egglog-experimental`.
3535
`egglog-experimental` depends on the workspace `egglog` crate, which keeps proof
3636
changes and downstream experimental behavior in the same reviewable unit.
3737

38+
Proof-specific file tests use one filter prefix:
39+
40+
- `proofs/`: explicit `(prove ...)` fixtures under `tests/proofs` plus every
41+
proof-compatible file under proof-testing mode. Checks are treated as prove
42+
commands and generated proofs are saved as snapshots.
43+
44+
This filter is a subset of the full workspace test run. Use it for fast proof
45+
iteration and reserve `cargo test --workspace` for the final compatibility gate.
46+
3847
## Benchmarking
3948

4049
The public benchmark entrypoint is an executable Python script:
@@ -120,10 +129,12 @@ If no files are provided, the default target benchmark suite is:
120129
- `egglog/tests/web-demo/rw-analysis.egg`
121130
- `egglog/tests/integer_math.egg`
122131
- `egglog/tests/web-demo/resolution.egg`
132+
- `egglog-experimental/tests/fixtures/eggcc-2mm-pass1-merge-old.egg`
123133

124-
These four files are proof-compatible representative examples under the current
134+
These five files are proof-compatible representative examples under the current
125135
`egglog-experimental` CLI and run under the default `off`, `term`, and `proofs`
126-
treatment matrix.
136+
treatment matrix. The eggcc fixture is the heavy container/proof benchmark in
137+
the default suite.
127138
Relative file paths are resolved relative to the directory where `./bench.py`
128139
was invoked, not relative to each target checkout. The same file contents are
129140
used for every target, and `file.sha256` records the exact benchmark input.
@@ -513,17 +524,19 @@ Each benchmark report row records the binary hash used for that observation.
513524

514525
## CI
515526

516-
CI runs on pull requests, manual dispatch, and pushes to `main`. It runs four
517-
jobs:
527+
CI runs on pull requests, manual dispatch, and pushes to `main`. It runs these
528+
job groups:
518529

519-
- `python`: `uv lock --check`, ruff formatting, ruff linting, mypy, and pytest.
530+
- `python`: `uv lock --check`, ruff/mypy checks for the top-level benchmark
531+
runner files, and pytest.
520532
- `rust`: workspace tests, proof-focused tests, and clippy.
521533
- `benchmark-smoke`: a one-round `./bench.py` run on
522534
`egglog/tests/integer_math.egg`.
523535
- `codspeed`: an in-process, proofs-only `egglog-experimental` benchmark
524-
harness over a smaller representative file set. CodSpeed tracks proof-mode
525-
movement without invoking `./bench.py`; the CLI benchmark report remains the
526-
source for the full off/term/proofs comparison.
536+
harness over a smaller representative file set, run through CodSpeed in both
537+
simulation and memory modes. CodSpeed tracks proof-mode movement without
538+
invoking `./bench.py`; the CLI benchmark report remains the source for the
539+
full off/term/proofs comparison.
527540

528541
Python checks are run as separate commands:
529542

bench.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"egglog/tests/web-demo/rw-analysis.egg",
5151
"egglog/tests/integer_math.egg",
5252
"egglog/tests/web-demo/resolution.egg",
53+
"egglog-experimental/tests/fixtures/eggcc-2mm-pass1-merge-old.egg",
5354
)
5455
TARGET_SPEED_CAPTION = (
5556
"Ratio is target / baseline. Values below 1x are faster; above 1x are slower. Intervals are 95% CIs."
@@ -434,6 +435,25 @@ def resolve_files(raw_files: Sequence[str], invocation_cwd: Path) -> tuple[FileS
434435
return tuple(files)
435436

436437

438+
def file_contains_executable_prove_command(path: Path) -> bool:
439+
for line in path.read_text(encoding="utf-8").splitlines():
440+
stripped = line.lstrip()
441+
if stripped.startswith(";"):
442+
continue
443+
if re.match(r"\(prove(?:\s|\))", stripped):
444+
return True
445+
return False
446+
447+
448+
def validate_spec(spec: BenchmarkSpec) -> None:
449+
for file_spec in spec.files:
450+
if file_contains_executable_prove_command(file_spec.absolute_path):
451+
raise ValueError(
452+
f"{file_spec.display_path} contains an explicit prove command; "
453+
"benchmark files should use check so proof extraction is not included in timed runs"
454+
)
455+
456+
437457
def resolve_report_destination(raw_report: str, invocation_cwd: Path) -> ReportDestination:
438458
if raw_report == "-":
439459
return ReportDestination(path=None, stream=sys.stdout)
@@ -1779,6 +1799,7 @@ def main(argv: Sequence[str] | None = None) -> int:
17791799
rounds=args.rounds,
17801800
timeout_sec=args.timeout_sec,
17811801
)
1802+
validate_spec(spec)
17821803
target_specs = args.target if args.target is not None else ["."]
17831804
target_requests = tuple(parse_target(raw) for raw in target_specs)
17841805
rows = load_report(report_destination)

egglog-experimental/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,5 @@ log = "0.4"
3232
[dev-dependencies]
3333
divan = { workspace = true }
3434
glob = "0.3.1"
35+
insta = "1.40"
3536
libtest-mimic = "0.8.1"

egglog-experimental/src/container_primitives.rs

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,22 @@ enum BoundMergeKind {
190190
Max,
191191
}
192192

193+
impl BoundMergeKind {
194+
fn keep_i64_old(self, old: i64, new: i64) -> bool {
195+
match self {
196+
BoundMergeKind::Min => old <= new,
197+
BoundMergeKind::Max => old >= new,
198+
}
199+
}
200+
201+
fn keep_bool_old(self, old: bool, new: bool) -> bool {
202+
match self {
203+
BoundMergeKind::Min => !old || new,
204+
BoundMergeKind::Max => old || !new,
205+
}
206+
}
207+
}
208+
193209
fn bound_merge<'a, 'db>(
194210
state: PureState<'a, 'db>,
195211
args: &[Value],
@@ -225,19 +241,13 @@ fn bound_merge<'a, 'db>(
225241
(EitherData::Left(old_int), EitherData::Left(new_int)) => {
226242
let old_int = state.base_values().unwrap::<i64>(old_int);
227243
let new_int = state.base_values().unwrap::<i64>(new_int);
228-
let keep_old = match kind {
229-
BoundMergeKind::Min => old_int <= new_int,
230-
BoundMergeKind::Max => old_int >= new_int,
231-
};
244+
let keep_old = kind.keep_i64_old(old_int, new_int);
232245
Some(if keep_old { args[0] } else { args[1] })
233246
}
234247
(EitherData::Right(old_bool), EitherData::Right(new_bool)) => {
235248
let old_bool = state.base_values().unwrap::<bool>(old_bool);
236249
let new_bool = state.base_values().unwrap::<bool>(new_bool);
237-
let keep_old = match kind {
238-
BoundMergeKind::Min => !old_bool || new_bool,
239-
BoundMergeKind::Max => old_bool || !new_bool,
240-
};
250+
let keep_old = kind.keep_bool_old(old_bool, new_bool);
241251
Some(if keep_old { args[0] } else { args[1] })
242252
}
243253
_ => None,
@@ -313,17 +323,11 @@ fn validate_maybe_either_i64_bool_merge(
313323
(BoundTerm::Dead, _) => Some(*old),
314324
(_, BoundTerm::Dead) => Some(*new),
315325
(BoundTerm::Int(old_int), BoundTerm::Int(new_int)) => {
316-
let keep_old = match kind {
317-
BoundMergeKind::Min => old_int <= new_int,
318-
BoundMergeKind::Max => old_int >= new_int,
319-
};
326+
let keep_old = kind.keep_i64_old(old_int, new_int);
320327
Some(if keep_old { *old } else { *new })
321328
}
322329
(BoundTerm::Bool(old_bool), BoundTerm::Bool(new_bool)) => {
323-
let keep_old = match kind {
324-
BoundMergeKind::Min => !old_bool || new_bool,
325-
BoundMergeKind::Max => old_bool || !new_bool,
326-
};
330+
let keep_old = kind.keep_bool_old(old_bool, new_bool);
327331
Some(if keep_old { *old } else { *new })
328332
}
329333
_ => None,

egglog-experimental/src/lib.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,15 @@ fn new_experimental_egraph_with_options(extended_run_schedule: bool) -> EGraph {
8282
}
8383

8484
pub fn new_experimental_egraph_with_term_encoding() -> EGraph {
85-
let mut egraph = EGraph::new_with_term_encoding();
86-
add_experimental_extensions(&mut egraph, false);
87-
egraph
85+
new_experimental_egraph_for_proofs().with_term_encoding_enabled()
8886
}
8987

9088
pub fn new_experimental_egraph_with_proofs() -> EGraph {
91-
let mut egraph = EGraph::new_with_proofs();
92-
add_experimental_extensions(&mut egraph, false);
93-
egraph
89+
new_experimental_egraph_for_proofs().with_proofs_enabled()
90+
}
91+
92+
pub fn new_experimental_egraph_with_proof_testing() -> EGraph {
93+
new_experimental_egraph_with_proofs().with_proof_testing()
9494
}
9595

9696
fn add_experimental_extensions(egraph: &mut EGraph, extended_run_schedule: bool) {

egglog-experimental/src/maybe.rs

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -190,13 +190,7 @@ impl ContainerSort for MaybeSort {
190190
(Some(old_value), Some(new_value)) => {
191191
let old_f = state.base_values().unwrap::<F>(old_value).0.0;
192192
let new_f = state.base_values().unwrap::<F>(new_value).0.0;
193-
let tolerance = tol.0.0.abs();
194-
let merged =
195-
old_f == new_f ||
196-
(old_f == 0.0 && new_f == -0.0) ||
197-
(old_f == -0.0 && new_f == 0.0) ||
198-
(old_f - new_f).abs() <= tolerance;
199-
merged.then_some(old)
193+
maybe_f64_values_merge_with_tol(old_f, new_f, tol.0.0).then_some(old)
200194
}
201195
}
202196
}},
@@ -253,16 +247,19 @@ fn validate_maybe_f64_merge_with_tol(termdag: &mut TermDag, args: &[TermId]) ->
253247
let Term::Lit(Literal::Float(tolerance)) = termdag.get(*tol) else {
254248
return None;
255249
};
256-
let tolerance = tolerance.0.abs();
257-
let merged = old_f == new_f
258-
|| (old_f == 0.0 && new_f == -0.0)
259-
|| (old_f == -0.0 && new_f == 0.0)
260-
|| (old_f - new_f).abs() <= tolerance;
261-
merged.then_some(*old)
250+
maybe_f64_values_merge_with_tol(old_f, new_f, tolerance.0).then_some(*old)
262251
}
263252
}
264253
}
265254

255+
fn maybe_f64_values_merge_with_tol(old_f: f64, new_f: f64, tolerance: f64) -> bool {
256+
let tolerance = tolerance.abs();
257+
old_f == new_f
258+
|| (old_f == 0.0 && new_f == -0.0)
259+
|| (old_f == -0.0 && new_f == 0.0)
260+
|| (old_f - new_f).abs() <= tolerance
261+
}
262+
266263
fn maybe_f64(termdag: &TermDag, term: TermId) -> Option<Option<f64>> {
267264
match termdag.get(term) {
268265
Term::App(head, children) if head == "maybe-none" && children.is_empty() => Some(None),

egglog-experimental/tests/eggcc_2mm_proof.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ fn run_fixture_with_proofs(fixture_name: &str) -> String {
3131
fn eggcc_2mm_full_export_uses_container_helpers() {
3232
let (_fixture, program) = read_fixture("eggcc-2mm-pass1-merge-old.egg");
3333

34+
let non_comment_program = program
35+
.lines()
36+
.filter(|line| !line.trim_start().starts_with(';'))
37+
.collect::<Vec<_>>()
38+
.join("\n");
39+
3440
for required in [
3541
"pair-min-by-second-i64",
3642
"maybe-either-i64-bool-min",
@@ -40,16 +46,27 @@ fn eggcc_2mm_full_export_uses_container_helpers() {
4046
"either-right",
4147
"either-unwrap-left",
4248
"either-unwrap-right",
49+
"(check (FunctionHasType \"main\"",
4350
] {
4451
assert!(
45-
program.contains(required),
52+
non_comment_program.contains(required),
4653
"fixture should exercise {required}"
4754
);
4855
}
4956

57+
for required in [
58+
"https://github.com/egraphs-good/eggcc/pull/796",
59+
"https://github.com/egraphs-good/egglog-experimental/pull/56",
60+
] {
61+
assert!(
62+
program.contains(required),
63+
"fixture should document {required}"
64+
);
65+
}
66+
5067
assert!(
51-
!program.contains(":no-merge"),
52-
"full eggcc export should not use :no-merge"
68+
!non_comment_program.contains(":no-merge"),
69+
"full eggcc export should not use :no-merge declarations"
5370
);
5471
assert!(
5572
program.contains(":merge old"),

0 commit comments

Comments
 (0)