Skip to content

Commit 92cd278

Browse files
test(corpus): whole-repo .eph ratchet gate — only 13/119 files compile today (#343)
## Ground truth (2026-07-07) Running every `.eph` file in the repo through the real pipeline (`ephapax compile`): **13 of 119 compile**. `conformance/valid/` passes **0/9** (even `hello.eph`), stdlib 2/9 check + 1/9 compile, examples almost entirely fail. The corpus is v1-era or speculative syntax (braces-form `fn` bodies, `&T` borrows, ADT `type X = A | B` declarations) that predates the v2 grammar — and nothing gated it, so it rotted silently while the 488 Rust-level tests stayed green. ## The gate (ratchet, both directions) `src/ephapax-cli/tests/eph_corpus_gate.rs` — runs under `cargo test`, so `rust-ci.yml` picks it up with no workflow change: - **`eph_corpus_ratchet`** — every file in `tests/eph-corpus-compiles.txt` must compile (regression gate), **and** every repo `.eph` that compiles must be listed (a newly-supported file must be flipped deliberately, in the PR that enables it). The known-debt count is printed on every run — no silent caps. - **`invalid_corpus_stays_invalid`** — `conformance/invalid/*.eph` must keep failing. Verified green locally. The 106-file debt is the v1→v2 corpus-migration backlog; much of it awaits grammar phases that don't exist yet, so this PR enforces current truth and makes the backlog visible rather than attempting wholesale migration. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 84a7bdc commit 92cd278

2 files changed

Lines changed: 192 additions & 0 deletions

File tree

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// Owner: Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
3+
//
4+
// eph_corpus_gate.rs — the whole-repo .eph corpus ratchet gate.
5+
//
6+
// Ground truth 2026-07-07: of the 119 .eph files in the repo, only 13
7+
// compile through the real pipeline (`ephapax compile`). The rest are
8+
// v1-era or speculative syntax (braces-form fn bodies, `&T` borrows,
9+
// ADT `type X = A | B` declarations) awaiting v2 grammar phases. Nothing
10+
// gated any of this, so the corpus rotted silently.
11+
//
12+
// This gate enforces the current truth in BOTH directions:
13+
// - every file in tests/eph-corpus-compiles.txt must compile
14+
// (regression gate), and
15+
// - every repo .eph that compiles must be in the list
16+
// (ratchet: a newly-supported file must be flipped deliberately,
17+
// in the same PR as the feature that enabled it).
18+
//
19+
// It also enforces the negative corpus: conformance/invalid/*.eph must
20+
// NOT compile.
21+
//
22+
// The known-debt count is printed on every run — no silent caps.
23+
24+
use std::collections::BTreeSet;
25+
use std::path::{Path, PathBuf};
26+
use std::process::Command;
27+
28+
fn repo_root() -> PathBuf {
29+
Path::new(env!("CARGO_MANIFEST_DIR"))
30+
.join("../..")
31+
.canonicalize()
32+
.expect("repo root")
33+
}
34+
35+
fn ephapax_bin() -> String {
36+
env!("CARGO_BIN_EXE_ephapax").to_string()
37+
}
38+
39+
/// All .eph files under the repo, repo-root-relative with '/' separators,
40+
/// excluding build/VCS/agent-scratch directories.
41+
fn corpus(root: &Path) -> BTreeSet<String> {
42+
fn walk(dir: &Path, root: &Path, out: &mut BTreeSet<String>) {
43+
let Ok(entries) = std::fs::read_dir(dir) else {
44+
return;
45+
};
46+
for entry in entries.flatten() {
47+
let path = entry.path();
48+
let name = entry.file_name();
49+
let name = name.to_string_lossy();
50+
if path.is_dir() {
51+
if matches!(
52+
name.as_ref(),
53+
"target" | ".git" | ".claude" | "node_modules" | ".zig-cache-global"
54+
) {
55+
continue;
56+
}
57+
walk(&path, root, out);
58+
} else if name.ends_with(".eph") {
59+
let rel = path
60+
.strip_prefix(root)
61+
.expect("under root")
62+
.to_string_lossy()
63+
.replace('\\', "/");
64+
out.insert(rel);
65+
}
66+
}
67+
}
68+
let mut out = BTreeSet::new();
69+
walk(root, root, &mut out);
70+
out
71+
}
72+
73+
fn compiles(root: &Path, rel: &str, out_dir: &Path) -> bool {
74+
let out = out_dir.join("gate.wasm");
75+
Command::new(ephapax_bin())
76+
.current_dir(root)
77+
.args([
78+
"compile",
79+
rel,
80+
"-o",
81+
out.to_str().expect("utf8 tmp path"),
82+
])
83+
.output()
84+
.map(|o| o.status.success())
85+
.unwrap_or(false)
86+
}
87+
88+
fn allowlist(root: &Path) -> BTreeSet<String> {
89+
let text = std::fs::read_to_string(root.join("tests/eph-corpus-compiles.txt"))
90+
.expect("read tests/eph-corpus-compiles.txt");
91+
text.lines()
92+
.map(str::trim)
93+
.filter(|l| !l.is_empty() && !l.starts_with('#'))
94+
.map(str::to_string)
95+
.collect()
96+
}
97+
98+
#[test]
99+
fn eph_corpus_ratchet() {
100+
let root = repo_root();
101+
let tmp = tempfile::tempdir().expect("tempdir");
102+
let expected = allowlist(&root);
103+
let all = corpus(&root);
104+
105+
for listed in &expected {
106+
assert!(
107+
all.contains(listed),
108+
"allowlist entry does not exist on disk: {listed}"
109+
);
110+
}
111+
112+
let mut passing = BTreeSet::new();
113+
for rel in &all {
114+
if compiles(&root, rel, tmp.path()) {
115+
passing.insert(rel.clone());
116+
}
117+
}
118+
119+
let regressions: Vec<_> = expected.difference(&passing).collect();
120+
let unexpected: Vec<_> = passing.difference(&expected).collect();
121+
let debt = all.len() - passing.len();
122+
println!(
123+
"eph corpus: {} files, {} compile, {} known debt (v1->v2 migration backlog)",
124+
all.len(),
125+
passing.len(),
126+
debt
127+
);
128+
129+
assert!(
130+
regressions.is_empty(),
131+
"REGRESSION — allowlisted files no longer compile: {regressions:?}"
132+
);
133+
assert!(
134+
unexpected.is_empty(),
135+
"RATCHET — newly compiling files must be added to \
136+
tests/eph-corpus-compiles.txt in the enabling PR: {unexpected:?}"
137+
);
138+
}
139+
140+
#[test]
141+
fn invalid_corpus_stays_invalid() {
142+
let root = repo_root();
143+
let tmp = tempfile::tempdir().expect("tempdir");
144+
let invalid: Vec<_> = corpus(&root)
145+
.into_iter()
146+
.filter(|p| p.starts_with("conformance/invalid/"))
147+
.collect();
148+
assert!(
149+
!invalid.is_empty(),
150+
"conformance/invalid corpus missing — gate misconfigured"
151+
);
152+
let wrongly_accepted: Vec<_> = invalid
153+
.iter()
154+
.filter(|rel| compiles(&root, rel, tmp.path()))
155+
.collect();
156+
assert!(
157+
wrongly_accepted.is_empty(),
158+
"negative-corpus files were ACCEPTED by the compiler: {wrongly_accepted:?}"
159+
);
160+
}

tests/eph-corpus-compiles.txt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# Owner: Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
3+
#
4+
# eph-corpus-compiles.txt — the RATCHET allowlist for the .eph corpus gate
5+
# (src/ephapax-cli/tests/eph_corpus_gate.rs).
6+
#
7+
# Every path here MUST compile (surface-parse → desugar → typecheck →
8+
# wasm) — a regression on any of them fails CI. Every .eph file in the
9+
# repo that compiles MUST be listed here — an unexpected pass also fails
10+
# CI, so the list can only be updated deliberately. The gate prints the
11+
# known-debt count (files that do not yet compile) on every run; that
12+
# debt is the v1→v2 corpus-migration backlog (much of it is speculative
13+
# syntax for features that do not exist yet: braces-form fn bodies,
14+
# borrows, ADT type declarations).
15+
#
16+
# Ratchet direction: when a grammar/codegen phase lands, flip the newly
17+
# passing files IN THE SAME PR.
18+
#
19+
# Paths are repo-root-relative, one per line, '#' comments allowed.
20+
examples/pattern_matching.eph
21+
stdlib/Argv.eph
22+
tests/v2-grammar/fixtures/extern-abstract-types.eph
23+
tests/v2-grammar/fixtures/extern-callsite.eph
24+
tests/v2-grammar/fixtures/hypatia-port/bridge.eph
25+
tests/v2-grammar/fixtures/hypatia-port/hypatia_gui.eph
26+
tests/v2-grammar/fixtures/implicit-in.eph
27+
tests/v2-grammar/fixtures/implicit-in-tuple.eph
28+
tests/v2-grammar/fixtures/let-pair-explicit-in.eph
29+
tests/v2-grammar/fixtures/multi-module/app.eph
30+
tests/v2-grammar/fixtures/multi-module/lib/math.eph
31+
tests/v2-grammar/fixtures/qualified-import/app.eph
32+
tests/v2-grammar/fixtures/qualified-import/lib.eph

0 commit comments

Comments
 (0)