Skip to content

Commit 0e91507

Browse files
test,bench: exercise the real lattice engine, not the compat shim (#27)
The benches and property tests that shipped in #26 only drove the IO-free `affine` shim (println-only build_lattice/query_lattice), so they measured and tested nothing about the actual algorithms. Re-point both at the real engine. benches (criterion): condense (parametric SCC, ~37..2.3k nodes), zoom over a deep containment forest, meet over deep chains, precedes end-to-end (condenses internally per call), and ingest of this repo's own src/ tree. property tests (proptest): quantify the laws the mod.rs fixtures only spot-check -- P2a condensation is always a DAG with a well-formed component map; P1a precedes is reflexive; P4 zoom is sound AND complete vs. an independent descendant oracle; P1b meet is commutative and a genuine common ancestor. Plus robustness: the engine never panics on wild input with cyclic parents (exercises the ancestor-chain guard), and one retained proptest keeps the shim's arbitrary-string resilience. Verified: clippy --all-targets zero warnings; cargo test (13 + 6 properties) green; cargo bench --no-run compiles. https://claude.ai/code/session_01JNCDaWMB8NV6nAPrvmTg4w
1 parent 6316bf2 commit 0e91507

2 files changed

Lines changed: 264 additions & 148 deletions

File tree

benches/git_reticulator_bench.rs

Lines changed: 89 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -2,117 +2,115 @@
22
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
33
//
44
// benches/git_reticulator_bench.rs
5-
// Criterion benchmarks for git-reticulator.
5+
// Criterion benchmarks for the git-reticulator lattice engine.
66
//
7-
// Baselines measured:
8-
// B1 - build_lattice with a short repo path and db URI
9-
// B2 - query_lattice with a short zoom node and db URI
10-
// B3 - build_lattice with a long (4 KiB) repo path
11-
// B4 - query_lattice with a long (4 KiB) zoom node
12-
// B5 - Sequential build-then-query pipeline
7+
// These measure the *real* algorithmic core (PROOF-NEEDS P1/P2/P4) rather than
8+
// the IO-free `affine` compat shim, so the numbers are behavioural baselines
9+
// for the algorithms a future AffineScript core must match:
10+
// engine/condense - Kosaraju SCC condensation, parametric over lattice size
11+
// engine/zoom - LOD descendant selection on a large containment forest
12+
// engine/meet - lowest-common-ancestor over deep ancestor chains
13+
// engine/precedes - node partial-order query (condenses internally per call)
14+
// engine/ingest - std-only filesystem walk of this repo's own `src/` tree
1315

1416
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
15-
use git_reticulator::lattice::affine;
17+
use git_reticulator::ingest;
18+
use git_reticulator::lattice::{Lattice, LatticeBuilder, SemanticLevel};
1619

17-
// ---------------------------------------------------------------------------
18-
// B1: build_lattice — short inputs
19-
// ---------------------------------------------------------------------------
20-
fn bench_build_lattice_short(c: &mut Criterion) {
21-
c.bench_function("build_lattice/short", |b| {
22-
b.iter(|| {
23-
affine::build_lattice(
24-
black_box("github.com/hyperpolymath/git-reticulator"),
25-
black_box("postgres://localhost:5432/reticulator"),
26-
)
27-
})
28-
});
20+
/// Build a synthetic lattice of `modules` modules, each with `files_per` files,
21+
/// each with `defs_per` definitions (Module ⊃ File ⊃ Definition). Definitions
22+
/// are wired into a forward call-chain with periodic back-edges, so the SCC
23+
/// condensation has genuine cycles to collapse rather than a trivial DAG.
24+
fn synthetic(modules: usize, files_per: usize, defs_per: usize) -> Lattice {
25+
let mut b = LatticeBuilder::new();
26+
let mut defs = Vec::new();
27+
for mi in 0..modules {
28+
let m = b.add_keyword(
29+
format!("mod{mi}"),
30+
format!("/mod{mi}"),
31+
SemanticLevel::Module,
32+
None,
33+
);
34+
for fi in 0..files_per {
35+
let f = b.add_keyword(
36+
format!("file{mi}_{fi}"),
37+
format!("/mod{mi}/file{fi}.rs"),
38+
SemanticLevel::File,
39+
Some(m),
40+
);
41+
for di in 0..defs_per {
42+
let d = b.add_keyword(
43+
format!("def{mi}_{fi}_{di}"),
44+
format!("/mod{mi}/file{fi}.rs"),
45+
SemanticLevel::Definition,
46+
Some(f),
47+
);
48+
defs.push(d);
49+
}
50+
}
51+
}
52+
// forward call chain over every definition
53+
for w in defs.windows(2) {
54+
b.add_relationship(w[0], w[1], 1.0, "calls".into());
55+
}
56+
// periodic back-edges (every 4th def calls back 3 steps) → real SCCs
57+
let mut i = 3;
58+
while i < defs.len() {
59+
b.add_relationship(defs[i], defs[i - 3], 1.0, "calls".into());
60+
i += 4;
61+
}
62+
b.build()
2963
}
3064

31-
// ---------------------------------------------------------------------------
32-
// B2: query_lattice — short inputs
33-
// ---------------------------------------------------------------------------
34-
fn bench_query_lattice_short(c: &mut Criterion) {
35-
c.bench_function("query_lattice/short", |b| {
36-
b.iter(|| {
37-
affine::query_lattice(
38-
black_box("module::lattice::affine"),
39-
black_box("postgres://localhost:5432/reticulator"),
40-
)
41-
})
42-
});
65+
fn bench_condense(c: &mut Criterion) {
66+
let mut group = c.benchmark_group("engine/condense");
67+
for &scale in &[1usize, 4, 16, 64] {
68+
let lat = synthetic(scale, 4, 8); // ~37 nodes per scale unit
69+
let n = lat.len();
70+
group.bench_with_input(BenchmarkId::from_parameter(n), &lat, |b, lat| {
71+
b.iter(|| black_box(lat.condense()))
72+
});
73+
}
74+
group.finish();
4375
}
4476

45-
// ---------------------------------------------------------------------------
46-
// B3: build_lattice — long inputs (4 KiB)
47-
// ---------------------------------------------------------------------------
48-
fn bench_build_lattice_long(c: &mut Criterion) {
49-
let long_repo: String = "repo/".repeat(819); // ~4 KiB
50-
let long_db: String = "db://".repeat(819);
51-
52-
c.bench_function("build_lattice/long_4kib", |b| {
53-
b.iter(|| affine::build_lattice(black_box(&long_repo), black_box(&long_db)))
77+
fn bench_zoom(c: &mut Criterion) {
78+
let lat = synthetic(16, 8, 16); // ~2.2k nodes, deep containment forest
79+
c.bench_function("engine/zoom_definitions_from_root", |b| {
80+
b.iter(|| black_box(lat.zoom(black_box(0), SemanticLevel::Definition)))
5481
});
5582
}
5683

57-
// ---------------------------------------------------------------------------
58-
// B4: query_lattice — long inputs (4 KiB)
59-
// ---------------------------------------------------------------------------
60-
fn bench_query_lattice_long(c: &mut Criterion) {
61-
let long_node: String = "node::".repeat(682); // ~4 KiB
62-
let long_db: String = "db://".repeat(819);
63-
64-
c.bench_function("query_lattice/long_4kib", |b| {
65-
b.iter(|| affine::query_lattice(black_box(&long_node), black_box(&long_db)))
84+
fn bench_meet(c: &mut Criterion) {
85+
let lat = synthetic(8, 8, 16);
86+
let last = lat.len() - 1;
87+
c.bench_function("engine/meet_across_forest", |b| {
88+
b.iter(|| black_box(lat.meet(black_box(1), black_box(last))))
6689
});
6790
}
6891

69-
// ---------------------------------------------------------------------------
70-
// B5: Pipeline — build followed immediately by query
71-
// ---------------------------------------------------------------------------
72-
fn bench_pipeline(c: &mut Criterion) {
73-
c.bench_function("pipeline/build_then_query", |b| {
74-
b.iter(|| {
75-
affine::build_lattice(
76-
black_box("pipeline-repo"),
77-
black_box("pipeline://db"),
78-
);
79-
affine::query_lattice(
80-
black_box("pipeline-node"),
81-
black_box("pipeline://db"),
82-
);
83-
})
92+
fn bench_precedes(c: &mut Criterion) {
93+
// precedes() condenses internally on every call — bench that end-to-end cost.
94+
let lat = synthetic(4, 4, 8);
95+
let last = lat.len() - 1;
96+
c.bench_function("engine/precedes_end_to_end", |b| {
97+
b.iter(|| black_box(lat.precedes(black_box(0), black_box(last))))
8498
});
8599
}
86100

87-
// ---------------------------------------------------------------------------
88-
// B6: Parametric — varying input lengths
89-
// ---------------------------------------------------------------------------
90-
fn bench_build_lattice_parametric(c: &mut Criterion) {
91-
let mut group = c.benchmark_group("build_lattice/by_input_length");
92-
93-
for size in [8usize, 64, 256, 1024, 4096] {
94-
let repo = "x".repeat(size);
95-
let db = "d".repeat(size);
96-
group.bench_with_input(
97-
BenchmarkId::from_parameter(size),
98-
&(repo, db),
99-
|b, (r, d)| b.iter(|| affine::build_lattice(black_box(r), black_box(d))),
100-
);
101-
}
102-
103-
group.finish();
101+
fn bench_ingest(c: &mut Criterion) {
102+
// Real fixture: this repo's own source tree (always present in-tree).
103+
c.bench_function("engine/ingest_src_tree", |b| {
104+
b.iter(|| black_box(ingest::from_path(black_box("src"))))
105+
});
104106
}
105107

106-
// ---------------------------------------------------------------------------
107-
// Criterion entry points
108-
// ---------------------------------------------------------------------------
109108
criterion_group!(
110109
benches,
111-
bench_build_lattice_short,
112-
bench_query_lattice_short,
113-
bench_build_lattice_long,
114-
bench_query_lattice_long,
115-
bench_pipeline,
116-
bench_build_lattice_parametric,
110+
bench_condense,
111+
bench_zoom,
112+
bench_meet,
113+
bench_precedes,
114+
bench_ingest,
117115
);
118116
criterion_main!(benches);

0 commit comments

Comments
 (0)