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).
344 lines
11 KiB
Rust
344 lines
11 KiB
Rust
//! Iter 20f: drift detection between the AST and `specs/form_a.md`.
|
|
//!
|
|
//! The spec is hand-curated, but it cannot silently fall behind the
|
|
//! language. Every AST enum (`Term`, `Pattern`, `Type`, `Def`, `Literal`,
|
|
//! `ParamMode`) discriminates on a `#[serde(rename = "...")]` tag.
|
|
//! These tests construct a sample of every variant, then check that the
|
|
//! corresponding tag string (or its parenthesised Form-A keyword) appears
|
|
//! in the spec.
|
|
//!
|
|
//! The exhaustive `match` is the load-bearing piece: adding a new variant
|
|
//! without a spec entry fails compilation here long before the test runs.
|
|
//! Once the variant is matched, the test asserts the spec mentions it.
|
|
|
|
use ailang_core::ast::{
|
|
ConstDef, Ctor, Def, FnDef, Literal, Pattern, Suppress, Term, Type, TypeDef,
|
|
};
|
|
use ailang_core::FORM_A_SPEC;
|
|
|
|
/// Every `Term` variant must be reachable from the spec. The Form-A
|
|
/// keyword for each variant is what the spec is supposed to teach an
|
|
/// LLM; if it is missing here, the LLM cannot produce that term.
|
|
#[test]
|
|
fn spec_mentions_every_term_variant() {
|
|
let exemplars: Vec<(&str, Term)> = vec![
|
|
("(lit-unit", Term::Lit { lit: Literal::Unit }),
|
|
// The Var form has no parenthesised keyword (a bare ident is a
|
|
// var-ref). The spec calls it out under "Atom forms"; we look for
|
|
// that anchor.
|
|
("Atom forms", Term::Var { name: "x".into() }),
|
|
(
|
|
"(app",
|
|
Term::App {
|
|
callee: Box::new(Term::Var { name: "f".into() }),
|
|
args: vec![Term::Var { name: "x".into() }],
|
|
tail: false,
|
|
},
|
|
),
|
|
(
|
|
"(let ",
|
|
Term::Let {
|
|
name: "x".into(),
|
|
value: Box::new(Term::Lit { lit: Literal::Int { value: 1 } }),
|
|
body: Box::new(Term::Var { name: "x".into() }),
|
|
},
|
|
),
|
|
(
|
|
"(let-rec",
|
|
Term::LetRec {
|
|
name: "f".into(),
|
|
ty: Type::fn_implicit(vec![], Type::int(), vec![]),
|
|
params: vec![],
|
|
body: Box::new(Term::Lit { lit: Literal::Int { value: 0 } }),
|
|
in_term: Box::new(Term::Var { name: "f".into() }),
|
|
},
|
|
),
|
|
(
|
|
"(if",
|
|
Term::If {
|
|
cond: Box::new(Term::Lit { lit: Literal::Bool { value: true } }),
|
|
then: Box::new(Term::Lit { lit: Literal::Int { value: 1 } }),
|
|
else_: Box::new(Term::Lit { lit: Literal::Int { value: 0 } }),
|
|
},
|
|
),
|
|
(
|
|
"(do ",
|
|
Term::Do {
|
|
op: "io/print_str".into(),
|
|
args: vec![],
|
|
tail: false,
|
|
},
|
|
),
|
|
(
|
|
"(term-ctor",
|
|
Term::Ctor {
|
|
type_name: "List".into(),
|
|
ctor: "Nil".into(),
|
|
args: vec![],
|
|
},
|
|
),
|
|
(
|
|
"(match",
|
|
Term::Match {
|
|
scrutinee: Box::new(Term::Var { name: "x".into() }),
|
|
arms: vec![],
|
|
},
|
|
),
|
|
(
|
|
"(lam",
|
|
Term::Lam {
|
|
params: vec![],
|
|
param_tys: vec![],
|
|
ret_ty: Box::new(Type::int()),
|
|
effects: vec![],
|
|
body: Box::new(Term::Lit { lit: Literal::Int { value: 0 } }),
|
|
},
|
|
),
|
|
(
|
|
"(seq",
|
|
Term::Seq {
|
|
lhs: Box::new(Term::Var { name: "a".into() }),
|
|
rhs: Box::new(Term::Var { name: "b".into() }),
|
|
},
|
|
),
|
|
(
|
|
"(clone",
|
|
Term::Clone {
|
|
value: Box::new(Term::Var { name: "x".into() }),
|
|
},
|
|
),
|
|
(
|
|
"(reuse-as",
|
|
Term::ReuseAs {
|
|
source: Box::new(Term::Var { name: "x".into() }),
|
|
body: Box::new(Term::Var { name: "y".into() }),
|
|
},
|
|
),
|
|
(
|
|
"(mut",
|
|
Term::Mut {
|
|
vars: Vec::new(),
|
|
body: Box::new(Term::Lit { lit: Literal::Unit }),
|
|
},
|
|
),
|
|
(
|
|
"(assign",
|
|
Term::Assign {
|
|
name: "x".into(),
|
|
value: Box::new(Term::Lit { lit: Literal::Unit }),
|
|
},
|
|
),
|
|
];
|
|
|
|
for (anchor, term) in exemplars {
|
|
// Force the exhaustive match: the body is just a tag string that
|
|
// we will not actually use, but the compiler will refuse to
|
|
// compile this file once a new Term variant is added without a
|
|
// matching arm.
|
|
let _: &'static str = match term {
|
|
Term::Lit { .. } => "lit",
|
|
Term::Var { .. } => "var",
|
|
Term::App { .. } => "app",
|
|
Term::Let { .. } => "let",
|
|
Term::LetRec { .. } => "letrec",
|
|
Term::If { .. } => "if",
|
|
Term::Do { .. } => "do",
|
|
Term::Ctor { .. } => "ctor",
|
|
Term::Match { .. } => "match",
|
|
Term::Lam { .. } => "lam",
|
|
Term::Seq { .. } => "seq",
|
|
Term::Clone { .. } => "clone",
|
|
Term::ReuseAs { .. } => "reuse-as",
|
|
Term::Mut { .. } => "mut",
|
|
Term::Assign { .. } => "assign",
|
|
};
|
|
assert!(
|
|
FORM_A_SPEC.contains(anchor),
|
|
"spec is missing anchor `{anchor}` for a Term variant — \
|
|
update crates/ailang-core/specs/form_a.md"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Every `Pattern` variant must appear in the spec.
|
|
#[test]
|
|
fn spec_mentions_every_pattern_variant() {
|
|
let exemplars: Vec<(&str, Pattern)> = vec![
|
|
("_", Pattern::Wild),
|
|
// pat-var is again the bare-ident form. The spec discusses it
|
|
// under "Patterns". Use the heading as the anchor.
|
|
("## Patterns", Pattern::Var { name: "x".into() }),
|
|
("(pat-lit", Pattern::Lit { lit: Literal::Int { value: 0 } }),
|
|
(
|
|
"(pat-ctor",
|
|
Pattern::Ctor { ctor: "Nil".into(), fields: vec![] },
|
|
),
|
|
];
|
|
|
|
for (anchor, pat) in exemplars {
|
|
let _: &'static str = match pat {
|
|
Pattern::Wild => "wild",
|
|
Pattern::Var { .. } => "var",
|
|
Pattern::Lit { .. } => "lit",
|
|
Pattern::Ctor { .. } => "ctor",
|
|
};
|
|
assert!(
|
|
FORM_A_SPEC.contains(anchor),
|
|
"spec is missing anchor `{anchor}` for a Pattern variant"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Every `Type` variant must appear in the spec.
|
|
#[test]
|
|
fn spec_mentions_every_type_variant() {
|
|
let exemplars: Vec<(&str, Type)> = vec![
|
|
("(con ", Type::int()),
|
|
(
|
|
"(fn-type",
|
|
Type::fn_implicit(vec![], Type::unit(), vec![]),
|
|
),
|
|
(
|
|
"TYVAR-NAME",
|
|
Type::Var { name: "a".into() },
|
|
),
|
|
(
|
|
"(forall",
|
|
Type::Forall {
|
|
vars: vec!["a".into()],
|
|
constraints: vec![],
|
|
body: Box::new(Type::Var { name: "a".into() }),
|
|
},
|
|
),
|
|
];
|
|
|
|
for (anchor, ty) in exemplars {
|
|
let _: &'static str = match ty {
|
|
Type::Con { .. } => "con",
|
|
Type::Fn { .. } => "fn",
|
|
Type::Var { .. } => "var",
|
|
Type::Forall { .. } => "forall",
|
|
};
|
|
assert!(
|
|
FORM_A_SPEC.contains(anchor),
|
|
"spec is missing anchor `{anchor}` for a Type variant"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Every `Literal` variant must appear in the spec.
|
|
#[test]
|
|
fn spec_mentions_every_literal_variant() {
|
|
// Anchors describe how the literal renders in Form-A.
|
|
let exemplars: Vec<(&str, Literal)> = vec![
|
|
("`INT`", Literal::Int { value: 0 }),
|
|
("`true`, `false`", Literal::Bool { value: true }),
|
|
("`STRING`", Literal::Str { value: "x".into() }),
|
|
("(lit-unit)", Literal::Unit),
|
|
("`FLOAT`", Literal::Float { bits: 0 }),
|
|
];
|
|
|
|
for (anchor, lit) in exemplars {
|
|
let _: &'static str = match lit {
|
|
Literal::Int { .. } => "int",
|
|
Literal::Bool { .. } => "bool",
|
|
Literal::Str { .. } => "str",
|
|
Literal::Unit => "unit",
|
|
Literal::Float { .. } => "float",
|
|
};
|
|
assert!(
|
|
FORM_A_SPEC.contains(anchor),
|
|
"spec is missing anchor `{anchor}` for a Literal variant"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Every `Def` kind must appear in the spec.
|
|
#[test]
|
|
fn spec_mentions_every_def_kind() {
|
|
let fn_def = FnDef {
|
|
name: "f".into(),
|
|
doc: None,
|
|
suppress: vec![],
|
|
ty: Type::fn_implicit(vec![], Type::int(), vec![]),
|
|
params: vec![],
|
|
body: Term::Lit { lit: Literal::Int { value: 0 } },
|
|
};
|
|
let const_def = ConstDef {
|
|
name: "k".into(),
|
|
doc: None,
|
|
ty: Type::int(),
|
|
value: Term::Lit { lit: Literal::Int { value: 0 } },
|
|
};
|
|
let type_def = TypeDef {
|
|
name: "T".into(),
|
|
doc: None,
|
|
vars: vec![],
|
|
ctors: vec![Ctor {
|
|
name: "C".into(),
|
|
fields: vec![],
|
|
}],
|
|
drop_iterative: false,
|
|
};
|
|
let exemplars: Vec<(&str, Def)> = vec![
|
|
("(fn ", Def::Fn(fn_def)),
|
|
("(const ", Def::Const(const_def)),
|
|
("(data ", Def::Type(type_def)),
|
|
];
|
|
|
|
for (anchor, def) in exemplars {
|
|
let _: &'static str = match def {
|
|
Def::Fn(_) => "fn",
|
|
Def::Const(_) => "const",
|
|
Def::Type(_) => "type",
|
|
// Iter 22b.1: class/instance Def variants exist but are
|
|
// not yet anchored in the prose-spec block. Once 22b.4
|
|
// adds prose projection for them, the FORM_A_SPEC text
|
|
// gains `(class ` / `(instance ` anchors and this match
|
|
// will be exercised. For 22b.1 the exemplars list above
|
|
// does not produce these variants.
|
|
Def::Class(_) => "class",
|
|
Def::Instance(_) => "instance",
|
|
};
|
|
assert!(
|
|
FORM_A_SPEC.contains(anchor),
|
|
"spec is missing anchor `{anchor}` for a Def kind"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// The mode keywords (Decision 10) must appear so an LLM knows the
|
|
/// Form-A wrapper syntax.
|
|
#[test]
|
|
fn spec_mentions_mode_keywords() {
|
|
assert!(FORM_A_SPEC.contains("(own"), "spec missing `(own ...)`");
|
|
assert!(FORM_A_SPEC.contains("(borrow"), "spec missing `(borrow ...)`");
|
|
}
|
|
|
|
/// `tail-app` and `tail-do` are distinct keywords from `app`/`do`. The
|
|
/// spec must mention both, otherwise an LLM cannot produce tail-correct
|
|
/// code at scale.
|
|
#[test]
|
|
fn spec_mentions_tail_variants() {
|
|
assert!(FORM_A_SPEC.contains("tail-app"), "spec missing `tail-app`");
|
|
assert!(FORM_A_SPEC.contains("tail-do"), "spec missing `tail-do`");
|
|
}
|
|
|
|
/// `suppress` is part of the surface and the LLM must know how to
|
|
/// preserve it. Empty-because is itself a diagnostic; the spec calls
|
|
/// it out so the LLM does not produce empty justifications.
|
|
#[test]
|
|
fn spec_mentions_suppress_clause() {
|
|
assert!(FORM_A_SPEC.contains("(suppress"), "spec missing `(suppress ...)`");
|
|
assert!(
|
|
FORM_A_SPEC.contains("empty-suppress-reason"),
|
|
"spec missing the empty-suppress-reason diagnostic"
|
|
);
|
|
// Make sure `Suppress` in the AST can still be constructed — the
|
|
// exhaustive-match property carries through to the surface.
|
|
let _ = Suppress {
|
|
code: "x".into(),
|
|
because: "y".into(),
|
|
};
|
|
}
|