37ac704bf3
One forward iteration; main never rewound.1ff7e81(the pre-9973546 commit) is the per-region byte oracle — every reverted source/test file is byte-identical to it; crates/ailang-check/src/lib.rs fully so. Root cause being corrected: the Iteration-discipline milestone was an over-escalation of fieldtest finding F1 (a [friction] item whose own minimal recommendation was a DESIGN.md note). Its totality dichotomy made the maximally-LLM-natural build(d:Int)=Node(1,build(d-1), build(d-1)) inexpressible (it.3 BLOCKED); the only in-thesis escape (A1/it.2b) conceded the language's first documented-unenforced totality precondition — a purity-pillar dilution the user rejected in favour of a full revert + rebuild. Removed: Term::Loop/Term::Recur/LoopBinder; the verify_structural_ recursion guardedness pass + term_contains_loop + Diverge-injection + the transitively-it.2 module_fns plumbing; the five Recur*/ NonStructuralRecursion CheckError variants (+ code() + the 3 dedicated ctx() arms); the it.1 codegen loop-header/phi/back-edge + parallel block_terminated setter; all Loop/Recur walker arms; 16 it.1/it.2 fixtures; 2 pin files; bench/it3-oracle/. Restored: 2 RC fixtures to1ff7e81content. Surgically kept (not in1ff7e81, landed with the milestone but independently sound): feature-acceptance clause 3 in DESIGN.md and skills/brainstorm/SKILL.md, with its worked example de-claimed from "shipped" to hypothetical-illustration form; the F3 P2 todo. bench/orchestrator-stats/2026-05-15-iter-it.{1,2,3}.json kept as historical record (like journals/plans). Sole net addition: an honest F1/F4 documented-idiom note in DESIGN.md (the tail-recursive accumulator fallback; examples/mut_counter.ail), guarded by a doc-presence test — "a documentation note is not a reshape", asserts nothing at the typecheck level. Roadmap: the Iteration-discipline block + blocking-fork section removed; the genuine total-Int-recursion ambition preserved as a deferred P2 milestone sequenced behind a future Nat/refinement-types milestone (not abandoned — correctly sequenced after the type machinery it needs). 2026-05-15-iteration-discipline.md carries a superseded header; it.1/it.2/it.3 journals + plans stay as history. Correctness gate PRISTINE: 164 surviving 1ff7e81-era fixtures ail check/ail run byte-identical to pre-milestone behaviour (verified against a1ff7e81worktree reference compiler, zero drift); cargo test --workspace 600/0; zero residual it.1/it.2 production surface. Spec docs/specs/2026-05-16-iteration-discipline-revert.md (b3853bf), plan docs/plans/2026-05-16-iter-revert.md (abf0013).
416 lines
12 KiB
Rust
416 lines
12 KiB
Rust
//! Schema-coverage audit: every AST enum variant of `Def`, `Term`,
|
|
//! `Pattern`, `Literal`, `Type`, `ParamMode` is exercised by at
|
|
//! least one fixture under `examples/`.
|
|
//!
|
|
//! Why this exists: the round-trip tests in `ailang-surface` are
|
|
//! only as strong as the fixture corpus that drives them. A schema
|
|
//! variant with zero fixture coverage round-trips trivially — there
|
|
//! is nothing to round-trip. This test makes the corpus's coverage
|
|
//! observable, so a gap surfaces as a fail-loud test, not as silent
|
|
//! confidence.
|
|
//!
|
|
//! How it stays in sync with the AST: every match arm below is
|
|
//! exhaustive (no `_ => ...` wildcard). When a new AST variant
|
|
//! lands, the compiler rejects this file until the visitor and the
|
|
//! `VariantTag` enum are extended in lockstep.
|
|
//!
|
|
//! Pure reader: this test loads fixtures but does not modify any
|
|
//! committed content. A failure means a fixture must be added (in
|
|
//! a separate iteration); the test must not be relaxed.
|
|
|
|
use std::collections::HashSet;
|
|
use std::path::PathBuf;
|
|
|
|
use ailang_core::ast::{
|
|
Def, InstanceMethod, Literal, Module, ParamMode, Pattern, Term, Type,
|
|
};
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
enum VariantTag {
|
|
// Def
|
|
DefFn,
|
|
DefConst,
|
|
DefType,
|
|
DefClass,
|
|
DefInstance,
|
|
// Term
|
|
TermLit,
|
|
TermVar,
|
|
TermApp,
|
|
TermLet,
|
|
TermLetRec,
|
|
TermIf,
|
|
TermDo,
|
|
TermCtor,
|
|
TermMatch,
|
|
TermLam,
|
|
TermSeq,
|
|
TermClone,
|
|
TermReuseAs,
|
|
TermMut,
|
|
TermAssign,
|
|
// Pattern
|
|
PatternWild,
|
|
PatternVar,
|
|
PatternLit,
|
|
PatternCtor,
|
|
// Literal
|
|
LiteralInt,
|
|
LiteralBool,
|
|
LiteralStr,
|
|
LiteralUnit,
|
|
LiteralFloat,
|
|
// Type
|
|
TypeCon,
|
|
TypeFn,
|
|
TypeVar,
|
|
TypeForall,
|
|
// ParamMode
|
|
ParamModeImplicit,
|
|
ParamModeOwn,
|
|
ParamModeBorrow,
|
|
}
|
|
|
|
/// Lists every expected variant. Stored as a const array so that
|
|
/// adding a new `VariantTag` here and forgetting to add the
|
|
/// corresponding match arm in the visitor is a Rust compile error
|
|
/// (and vice versa).
|
|
const EXPECTED_VARIANTS: &[VariantTag] = &[
|
|
VariantTag::DefFn,
|
|
VariantTag::DefConst,
|
|
VariantTag::DefType,
|
|
VariantTag::DefClass,
|
|
VariantTag::DefInstance,
|
|
VariantTag::TermLit,
|
|
VariantTag::TermVar,
|
|
VariantTag::TermApp,
|
|
VariantTag::TermLet,
|
|
VariantTag::TermLetRec,
|
|
VariantTag::TermIf,
|
|
VariantTag::TermDo,
|
|
VariantTag::TermCtor,
|
|
VariantTag::TermMatch,
|
|
VariantTag::TermLam,
|
|
VariantTag::TermSeq,
|
|
VariantTag::TermClone,
|
|
VariantTag::TermReuseAs,
|
|
VariantTag::TermMut,
|
|
VariantTag::TermAssign,
|
|
VariantTag::PatternWild,
|
|
VariantTag::PatternVar,
|
|
VariantTag::PatternLit,
|
|
VariantTag::PatternCtor,
|
|
VariantTag::LiteralInt,
|
|
VariantTag::LiteralBool,
|
|
VariantTag::LiteralStr,
|
|
VariantTag::LiteralUnit,
|
|
VariantTag::LiteralFloat,
|
|
VariantTag::TypeCon,
|
|
VariantTag::TypeFn,
|
|
VariantTag::TypeVar,
|
|
VariantTag::TypeForall,
|
|
VariantTag::ParamModeImplicit,
|
|
VariantTag::ParamModeOwn,
|
|
VariantTag::ParamModeBorrow,
|
|
];
|
|
|
|
fn visit_def(def: &Def, observed: &mut HashSet<VariantTag>) {
|
|
match def {
|
|
Def::Fn(fd) => {
|
|
observed.insert(VariantTag::DefFn);
|
|
visit_type(&fd.ty, observed);
|
|
visit_term(&fd.body, observed);
|
|
}
|
|
Def::Const(cd) => {
|
|
observed.insert(VariantTag::DefConst);
|
|
visit_type(&cd.ty, observed);
|
|
visit_term(&cd.value, observed);
|
|
}
|
|
Def::Type(td) => {
|
|
observed.insert(VariantTag::DefType);
|
|
for ctor in &td.ctors {
|
|
for field_ty in &ctor.fields {
|
|
visit_type(field_ty, observed);
|
|
}
|
|
}
|
|
}
|
|
Def::Class(cd) => {
|
|
observed.insert(VariantTag::DefClass);
|
|
for m in &cd.methods {
|
|
visit_type(&m.ty, observed);
|
|
if let Some(body) = &m.default {
|
|
visit_term(body, observed);
|
|
}
|
|
}
|
|
}
|
|
Def::Instance(id) => {
|
|
observed.insert(VariantTag::DefInstance);
|
|
visit_type(&id.type_, observed);
|
|
for m in &id.methods {
|
|
visit_instance_method(m, observed);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn visit_instance_method(m: &InstanceMethod, observed: &mut HashSet<VariantTag>) {
|
|
visit_term(&m.body, observed);
|
|
}
|
|
|
|
fn visit_term(t: &Term, observed: &mut HashSet<VariantTag>) {
|
|
match t {
|
|
Term::Lit { lit } => {
|
|
observed.insert(VariantTag::TermLit);
|
|
visit_literal(lit, observed);
|
|
}
|
|
Term::Var { .. } => {
|
|
observed.insert(VariantTag::TermVar);
|
|
}
|
|
Term::App { callee, args, .. } => {
|
|
observed.insert(VariantTag::TermApp);
|
|
visit_term(callee, observed);
|
|
for a in args {
|
|
visit_term(a, observed);
|
|
}
|
|
}
|
|
Term::Let { value, body, .. } => {
|
|
observed.insert(VariantTag::TermLet);
|
|
visit_term(value, observed);
|
|
visit_term(body, observed);
|
|
}
|
|
Term::LetRec { ty, body, in_term, .. } => {
|
|
observed.insert(VariantTag::TermLetRec);
|
|
visit_type(ty, observed);
|
|
visit_term(body, observed);
|
|
visit_term(in_term, observed);
|
|
}
|
|
Term::If { cond, then, else_ } => {
|
|
observed.insert(VariantTag::TermIf);
|
|
visit_term(cond, observed);
|
|
visit_term(then, observed);
|
|
visit_term(else_, observed);
|
|
}
|
|
Term::Do { args, .. } => {
|
|
observed.insert(VariantTag::TermDo);
|
|
for a in args {
|
|
visit_term(a, observed);
|
|
}
|
|
}
|
|
Term::Ctor { args, .. } => {
|
|
observed.insert(VariantTag::TermCtor);
|
|
for a in args {
|
|
visit_term(a, observed);
|
|
}
|
|
}
|
|
Term::Match { scrutinee, arms } => {
|
|
observed.insert(VariantTag::TermMatch);
|
|
visit_term(scrutinee, observed);
|
|
for arm in arms {
|
|
visit_pattern(&arm.pat, observed);
|
|
visit_term(&arm.body, observed);
|
|
}
|
|
}
|
|
Term::Lam { param_tys, ret_ty, body, .. } => {
|
|
observed.insert(VariantTag::TermLam);
|
|
for ty in param_tys {
|
|
visit_type(ty, observed);
|
|
}
|
|
visit_type(ret_ty, observed);
|
|
visit_term(body, observed);
|
|
}
|
|
Term::Seq { lhs, rhs } => {
|
|
observed.insert(VariantTag::TermSeq);
|
|
visit_term(lhs, observed);
|
|
visit_term(rhs, observed);
|
|
}
|
|
Term::Clone { value } => {
|
|
observed.insert(VariantTag::TermClone);
|
|
visit_term(value, observed);
|
|
}
|
|
Term::ReuseAs { source, body } => {
|
|
observed.insert(VariantTag::TermReuseAs);
|
|
visit_term(source, observed);
|
|
visit_term(body, observed);
|
|
}
|
|
Term::Mut { vars, body } => {
|
|
observed.insert(VariantTag::TermMut);
|
|
for v in vars {
|
|
visit_type(&v.ty, observed);
|
|
visit_term(&v.init, observed);
|
|
}
|
|
visit_term(body, observed);
|
|
}
|
|
Term::Assign { value, .. } => {
|
|
observed.insert(VariantTag::TermAssign);
|
|
visit_term(value, observed);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn visit_pattern(p: &Pattern, observed: &mut HashSet<VariantTag>) {
|
|
match p {
|
|
Pattern::Wild => {
|
|
observed.insert(VariantTag::PatternWild);
|
|
}
|
|
Pattern::Var { .. } => {
|
|
observed.insert(VariantTag::PatternVar);
|
|
}
|
|
Pattern::Lit { lit } => {
|
|
observed.insert(VariantTag::PatternLit);
|
|
visit_literal(lit, observed);
|
|
}
|
|
Pattern::Ctor { fields, .. } => {
|
|
observed.insert(VariantTag::PatternCtor);
|
|
for f in fields {
|
|
visit_pattern(f, observed);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn visit_literal(l: &Literal, observed: &mut HashSet<VariantTag>) {
|
|
match l {
|
|
Literal::Int { .. } => {
|
|
observed.insert(VariantTag::LiteralInt);
|
|
}
|
|
Literal::Bool { .. } => {
|
|
observed.insert(VariantTag::LiteralBool);
|
|
}
|
|
Literal::Str { .. } => {
|
|
observed.insert(VariantTag::LiteralStr);
|
|
}
|
|
Literal::Unit => {
|
|
observed.insert(VariantTag::LiteralUnit);
|
|
}
|
|
Literal::Float { .. } => {
|
|
observed.insert(VariantTag::LiteralFloat);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn visit_type(t: &Type, observed: &mut HashSet<VariantTag>) {
|
|
match t {
|
|
Type::Con { args, .. } => {
|
|
observed.insert(VariantTag::TypeCon);
|
|
for a in args {
|
|
visit_type(a, observed);
|
|
}
|
|
}
|
|
Type::Fn { params, param_modes, ret, ret_mode, .. } => {
|
|
observed.insert(VariantTag::TypeFn);
|
|
for p in params {
|
|
visit_type(p, observed);
|
|
}
|
|
for m in param_modes {
|
|
visit_param_mode(m, observed);
|
|
}
|
|
visit_type(ret, observed);
|
|
visit_param_mode(ret_mode, observed);
|
|
}
|
|
Type::Var { .. } => {
|
|
observed.insert(VariantTag::TypeVar);
|
|
}
|
|
Type::Forall { body, .. } => {
|
|
observed.insert(VariantTag::TypeForall);
|
|
visit_type(body, observed);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn visit_param_mode(m: &ParamMode, observed: &mut HashSet<VariantTag>) {
|
|
match m {
|
|
ParamMode::Implicit => {
|
|
observed.insert(VariantTag::ParamModeImplicit);
|
|
}
|
|
ParamMode::Own => {
|
|
observed.insert(VariantTag::ParamModeOwn);
|
|
}
|
|
ParamMode::Borrow => {
|
|
observed.insert(VariantTag::ParamModeBorrow);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn visit_module(m: &Module, observed: &mut HashSet<VariantTag>) {
|
|
for def in &m.defs {
|
|
visit_def(def, observed);
|
|
}
|
|
}
|
|
|
|
fn examples_dir() -> PathBuf {
|
|
let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
crate_dir.parent().unwrap().parent().unwrap().join("examples")
|
|
}
|
|
|
|
fn list_ail_fixtures() -> Vec<PathBuf> {
|
|
let dir = examples_dir();
|
|
let mut paths: Vec<PathBuf> = std::fs::read_dir(&dir)
|
|
.unwrap_or_else(|e| panic!("read_dir({}): {e}", dir.display()))
|
|
.filter_map(|entry| entry.ok())
|
|
.map(|e| e.path())
|
|
.filter(|p| {
|
|
p.file_name()
|
|
.and_then(|n| n.to_str())
|
|
.map(|n| n.ends_with(".ail"))
|
|
.unwrap_or(false)
|
|
})
|
|
.collect();
|
|
paths.sort();
|
|
paths
|
|
}
|
|
|
|
#[test]
|
|
fn every_ast_variant_is_observed_in_the_fixture_corpus() {
|
|
let fixtures = list_ail_fixtures();
|
|
assert!(
|
|
!fixtures.is_empty(),
|
|
"no .ail fixtures found under {}",
|
|
examples_dir().display()
|
|
);
|
|
|
|
let mut observed: HashSet<VariantTag> = HashSet::new();
|
|
let mut load_failures = Vec::<String>::new();
|
|
|
|
for path in &fixtures {
|
|
match ailang_surface::load_module(path) {
|
|
Ok(m) => visit_module(&m, &mut observed),
|
|
Err(e) => load_failures.push(format!("{}: {e}", path.display())),
|
|
}
|
|
}
|
|
|
|
// Load failures fail the test on their own — a fixture that
|
|
// does not load is a corpus bug, surface it loud.
|
|
if !load_failures.is_empty() {
|
|
panic!(
|
|
"{} fixture(s) failed to load during coverage scan:\n{}",
|
|
load_failures.len(),
|
|
load_failures.join("\n")
|
|
);
|
|
}
|
|
|
|
let missing: Vec<VariantTag> = EXPECTED_VARIANTS
|
|
.iter()
|
|
.copied()
|
|
.filter(|v| !observed.contains(v))
|
|
.collect();
|
|
|
|
if !missing.is_empty() {
|
|
let names: Vec<String> = missing.iter().map(|v| format!("{v:?}")).collect();
|
|
panic!(
|
|
"{} AST variant(s) have NO fixture coverage in {}:\n {}\n\n\
|
|
every variant must be exercised by at least one fixture; \
|
|
add a fixture that uses each missing variant (do NOT \
|
|
weaken this test).",
|
|
missing.len(),
|
|
examples_dir().display(),
|
|
names.join("\n ")
|
|
);
|
|
}
|
|
|
|
eprintln!(
|
|
"schema coverage ok: {} variants observed across {} fixtures",
|
|
observed.len(),
|
|
fixtures.len()
|
|
);
|
|
}
|