07f080256c
mut/var/assign removed from AILang entirely and atomically. Deleted: Term::Mut/Term::Assign/struct MutVar; the three Form-A keywords + parse_mut/parse_assign + grammar EBNF; the 4 mut CheckError variants; the mut_scope_stack synth threading (param dropped from synth + every internal/external/test caller); the two lower_term arms; and every exhaustive no-_ Term::Mut/Term::Assign match arm across 17 source files — cut in lockstep with DESIGN.md, fixtures, the drift trio, carve-out and roadmap so the schema is honest at every commit. No catch-all wildcard introduced (verified). loop/recur + let/if are the surviving forms. The shared codegen alloca machinery survives (loop reuses it): mut_var_allocas renamed binder_allocas (representation-only, loop codegen byte-identical) and the shared Term::Lam escape guard simplified to !loop_stack.is_empty() with the loop half (LoopBinderCapturedByLambda) byte-equivalent. Feature-acceptance applied inverted: the removed feature fails clause 2 (redundant) and clause 3 (IS the iterated-mutable-state bug class). Behaviour preservation is executable: mut_counter/mut_sum_floats still print 55 after the faithful let/if rewrite. The removal is made executable by the new mut_removed_pin.rs (4 must-fail pins). Independent verification: cargo test --workspace 605/0, zero residual mut symbols in any crate source, loop/recur non-regression all green (55 / 500000500000 / infinite-compiles / the lambda_capturing_loop_binder pin), roundtrip_cli PASS. One DONE_WITH_CONCERNS: a 4th recurrence of the recon-undercount class (in-source mod tests + a drift-pin fn + 5 orphaned mut .ail.json carve-outs + a non-enumerated E0599); all resolved within implementer remit, no behaviour change. Milestone-close audit then fieldtest remain. spec docs/specs/2026-05-18-remove-mut-var-assign.md (grounding PASS) plan docs/plans/remove-mut-var-assign.1.md
431 lines
15 KiB
Rust
431 lines
15 KiB
Rust
//! Drift detection between ast.rs and `docs/DESIGN.md` §"Data model".
|
|
//!
|
|
//! DESIGN.md is the canonical schema source-of-truth. Every AST enum
|
|
//! (`Term`, `Pattern`, `Type`, `Def`, `Literal`, `ParamMode`) must have a
|
|
//! JSON-schema anchor (e.g. `"t": "lit"`, `"k": "fn"`) present in that
|
|
//! document. These tests enforce the property.
|
|
//!
|
|
//! The exhaustive `match` per enum is the load-bearing mechanism: adding a
|
|
//! new variant without a matching arm fails compilation before the test runs.
|
|
//! Once the variant is matched, the test asserts the anchor is present in
|
|
//! §"Data model" specifically — not anywhere in DESIGN.md. The narrow scope
|
|
//! is load-bearing: before this tightening, the test scanned the whole
|
|
//! document, so an anchor that appeared only in §"Decision 11" (or any
|
|
//! other discussion section) was treated as "present" even though
|
|
//! §"Data model" — the canonical schema reference — was missing it.
|
|
|
|
use ailang_core::ast::{
|
|
ClassDef, ClassMethod, Constraint, ConstDef, Ctor, Def, FnDef, InstanceDef,
|
|
InstanceMethod, Literal, Pattern, ParamMode, Suppress, Term, Type, TypeDef,
|
|
};
|
|
|
|
const DESIGN_MD: &str = include_str!("../../../docs/DESIGN.md");
|
|
|
|
/// Slice of `DESIGN_MD` covering only §"Data model": from the `## Data model`
|
|
/// header to the next top-level `## ` header. Returned as a `&'static str`
|
|
/// because `DESIGN_MD` is itself static. Panics if §"Data model" is missing
|
|
/// — that itself would be drift.
|
|
fn data_model_section() -> &'static str {
|
|
let start = DESIGN_MD
|
|
.find("## Data model")
|
|
.expect("DESIGN.md must contain `## Data model` header");
|
|
let from_start = &DESIGN_MD[start..];
|
|
match from_start.find("\n## ") {
|
|
Some(end) => &from_start[..end],
|
|
None => from_start,
|
|
}
|
|
}
|
|
|
|
/// Every `Term` variant must have its JSON-schema anchor present in
|
|
/// DESIGN.md §"Data model". An LLM author cannot produce a term variant
|
|
/// whose `"t"` tag is absent from the canonical schema document.
|
|
#[test]
|
|
fn design_md_anchors_every_term_variant() {
|
|
let exemplars: Vec<(&str, Term)> = vec![
|
|
(
|
|
r#""t": "lit""#,
|
|
Term::Lit { lit: Literal::Unit },
|
|
),
|
|
(
|
|
r#""t": "var""#,
|
|
Term::Var { name: "x".into() },
|
|
),
|
|
(
|
|
r#""t": "app""#,
|
|
Term::App {
|
|
callee: Box::new(Term::Var { name: "f".into() }),
|
|
args: vec![],
|
|
tail: false,
|
|
},
|
|
),
|
|
(
|
|
r#""t": "let""#,
|
|
Term::Let {
|
|
name: "x".into(),
|
|
value: Box::new(Term::Lit { lit: Literal::Int { value: 0 } }),
|
|
body: Box::new(Term::Var { name: "x".into() }),
|
|
},
|
|
),
|
|
(
|
|
r#""t": "letrec""#,
|
|
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() }),
|
|
},
|
|
),
|
|
(
|
|
r#""t": "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 } }),
|
|
},
|
|
),
|
|
(
|
|
r#""t": "do""#,
|
|
Term::Do {
|
|
op: "io/print_str".into(),
|
|
args: vec![],
|
|
tail: false,
|
|
},
|
|
),
|
|
(
|
|
r#""t": "ctor""#,
|
|
Term::Ctor {
|
|
type_name: "List".into(),
|
|
ctor: "Nil".into(),
|
|
args: vec![],
|
|
},
|
|
),
|
|
(
|
|
r#""t": "match""#,
|
|
Term::Match {
|
|
scrutinee: Box::new(Term::Var { name: "x".into() }),
|
|
arms: vec![],
|
|
},
|
|
),
|
|
(
|
|
r#""t": "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 } }),
|
|
},
|
|
),
|
|
(
|
|
r#""t": "seq""#,
|
|
Term::Seq {
|
|
lhs: Box::new(Term::Var { name: "a".into() }),
|
|
rhs: Box::new(Term::Var { name: "b".into() }),
|
|
},
|
|
),
|
|
(
|
|
r#""t": "clone""#,
|
|
Term::Clone {
|
|
value: Box::new(Term::Var { name: "x".into() }),
|
|
},
|
|
),
|
|
(
|
|
r#""t": "reuse-as""#,
|
|
Term::ReuseAs {
|
|
source: Box::new(Term::Var { name: "x".into() }),
|
|
body: Box::new(Term::Var { name: "y".into() }),
|
|
},
|
|
),
|
|
(
|
|
r#""t": "loop""#,
|
|
Term::Loop {
|
|
binders: Vec::new(),
|
|
body: Box::new(Term::Lit { lit: Literal::Unit }),
|
|
},
|
|
),
|
|
(
|
|
r#""t": "recur""#,
|
|
Term::Recur { args: vec![] },
|
|
),
|
|
];
|
|
|
|
for (anchor, term) in exemplars {
|
|
// Exhaustive match: compiler rejects this file if a new Term
|
|
// variant lacks an arm, catching drift at compile time.
|
|
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::Loop { .. } => "loop",
|
|
Term::Recur { .. } => "recur",
|
|
};
|
|
assert!(
|
|
data_model_section().contains(anchor),
|
|
"DESIGN.md §Data-model is missing anchor `{anchor}` for a Term variant — \
|
|
add it to docs/DESIGN.md"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Every `Pattern` variant must have its JSON-schema anchor present in
|
|
/// DESIGN.md §"Data model". Missing anchors mean an LLM cannot produce
|
|
/// the corresponding pattern form.
|
|
#[test]
|
|
fn design_md_anchors_every_pattern_variant() {
|
|
let exemplars: Vec<(&str, Pattern)> = vec![
|
|
(r#""p": "wild""#, Pattern::Wild),
|
|
(r#""p": "var""#, Pattern::Var { name: "x".into() }),
|
|
(r#""p": "lit""#, Pattern::Lit { lit: Literal::Int { value: 0 } }),
|
|
(
|
|
r#""p": "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!(
|
|
data_model_section().contains(anchor),
|
|
"DESIGN.md §Data-model is missing anchor `{anchor}` for a Pattern variant"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Every `Type` variant must have its JSON-schema anchor present in
|
|
/// DESIGN.md §"Data model". The `"k"` discriminator is load-bearing for
|
|
/// the codegen and typechecker; an LLM must know all four forms.
|
|
#[test]
|
|
fn design_md_anchors_every_type_variant() {
|
|
let exemplars: Vec<(&str, Type)> = vec![
|
|
(r#""k": "con""#, Type::int()),
|
|
(r#""k": "fn""#, Type::fn_implicit(vec![], Type::unit(), vec![])),
|
|
(r#""k": "var""#, Type::Var { name: "a".into() }),
|
|
(
|
|
r#""k": "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!(
|
|
data_model_section().contains(anchor),
|
|
"DESIGN.md §Data-model is missing anchor `{anchor}` for a Type variant"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Every `Literal` variant must have its JSON-schema anchor present in
|
|
/// DESIGN.md §"Data model". The `"kind"` discriminator identifies the
|
|
/// literal type; an LLM cannot produce a literal it hasn't seen in the
|
|
/// schema.
|
|
#[test]
|
|
fn design_md_anchors_every_literal_variant() {
|
|
let exemplars: Vec<(&str, Literal)> = vec![
|
|
(r#""kind": "int""#, Literal::Int { value: 0 }),
|
|
(r#""kind": "bool""#, Literal::Bool { value: true }),
|
|
(r#""kind": "str""#, Literal::Str { value: "x".into() }),
|
|
(r#""kind": "unit""#, Literal::Unit),
|
|
(r#""kind": "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!(
|
|
data_model_section().contains(anchor),
|
|
"DESIGN.md §Data-model is missing anchor `{anchor}` for a Literal variant"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Every `Def` kind must have its JSON-schema anchor present in
|
|
/// DESIGN.md §"Data model". All five kinds (`fn`, `const`, `type`,
|
|
/// `class`, `instance`) must be documented so an LLM can write
|
|
/// any kind of top-level definition.
|
|
#[test]
|
|
fn design_md_anchors_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 class_def = ClassDef {
|
|
name: "Show".into(),
|
|
param: "a".into(),
|
|
superclass: None,
|
|
methods: vec![ClassMethod {
|
|
name: "show".into(),
|
|
ty: Type::fn_implicit(vec![Type::Var { name: "a".into() }], Type::str_(), vec![]),
|
|
default: None,
|
|
}],
|
|
doc: None,
|
|
};
|
|
let instance_def = InstanceDef {
|
|
class: "Show".into(),
|
|
type_: Type::int(),
|
|
methods: vec![InstanceMethod {
|
|
name: "show".into(),
|
|
body: Term::Lit { lit: Literal::Str { value: "0".into() } },
|
|
}],
|
|
doc: None,
|
|
};
|
|
|
|
let exemplars: Vec<(&str, Def)> = vec![
|
|
(r#""kind": "fn""#, Def::Fn(fn_def)),
|
|
(r#""kind": "const""#, Def::Const(const_def)),
|
|
(r#""kind": "type""#, Def::Type(type_def)),
|
|
(r#""kind": "class""#, Def::Class(class_def)),
|
|
(r#""kind": "instance""#, Def::Instance(instance_def)),
|
|
];
|
|
|
|
for (anchor, def) in exemplars {
|
|
let _: &'static str = match def {
|
|
Def::Fn(_) => "fn",
|
|
Def::Const(_) => "const",
|
|
Def::Type(_) => "type",
|
|
Def::Class(_) => "class",
|
|
Def::Instance(_) => "instance",
|
|
};
|
|
assert!(
|
|
data_model_section().contains(anchor),
|
|
"DESIGN.md §Data-model is missing anchor `{anchor}` for a Def kind"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Every `ParamMode` variant must have its serialized string form present
|
|
/// in DESIGN.md §"Data model". The mode annotations are load-bearing for
|
|
/// ownership checking; an LLM author must know all three forms.
|
|
#[test]
|
|
fn design_md_anchors_every_parammode_variant() {
|
|
let exemplars: Vec<(&str, ParamMode)> = vec![
|
|
(r#""implicit""#, ParamMode::Implicit),
|
|
(r#""own""#, ParamMode::Own),
|
|
(r#""borrow""#, ParamMode::Borrow),
|
|
];
|
|
|
|
for (anchor, mode) in exemplars {
|
|
let _: &'static str = match mode {
|
|
ParamMode::Implicit => "implicit",
|
|
ParamMode::Own => "own",
|
|
ParamMode::Borrow => "borrow",
|
|
};
|
|
assert!(
|
|
data_model_section().contains(anchor),
|
|
"DESIGN.md §Data-model is missing anchor `{anchor}` for a ParamMode variant"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Nested struct key anchors must be present in DESIGN.md §"Data model".
|
|
/// These keys appear inside `Suppress`, `ClassMethod`, `InstanceMethod`,
|
|
/// and `Type::Forall` — they are not discriminators but they ARE part
|
|
/// of the canonical JSON schema and must remain documented.
|
|
#[test]
|
|
fn design_md_anchors_nested_struct_keys() {
|
|
// Constructors exercised here ensure ast.rs field names are correct.
|
|
let _ = Suppress { code: "x".into(), because: "y".into() };
|
|
let _ = ClassMethod {
|
|
name: "m".into(),
|
|
ty: Type::fn_implicit(vec![], Type::int(), vec![]),
|
|
default: None,
|
|
};
|
|
let _ = InstanceMethod {
|
|
name: "m".into(),
|
|
body: Term::Lit { lit: Literal::Unit },
|
|
};
|
|
let _ = Constraint {
|
|
class: "Show".into(),
|
|
type_: Type::int(),
|
|
};
|
|
|
|
let anchors = [
|
|
r#""code""#,
|
|
r#""because""#,
|
|
r#""methods""#,
|
|
r#""constraints""#,
|
|
];
|
|
|
|
for anchor in anchors {
|
|
assert!(
|
|
data_model_section().contains(anchor),
|
|
"DESIGN.md §Data-model is missing nested-struct-key anchor `{anchor}`"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Pin the §"Data model" section extractor itself: it must return a
|
|
/// non-empty slice starting with `## Data model`, and it must NOT bleed
|
|
/// into the next top-level section (`## Pipeline` today). A bug in the
|
|
/// extractor would otherwise silently widen every drift test's scope
|
|
/// back toward full-document scanning — the failure mode this file's
|
|
/// tightening exists to prevent.
|
|
#[test]
|
|
fn data_model_section_is_bounded() {
|
|
let section = data_model_section();
|
|
assert!(
|
|
section.starts_with("## Data model"),
|
|
"section must start at the §Data model header; got first 40 bytes: {:?}",
|
|
§ion[..40.min(section.len())]
|
|
);
|
|
assert!(
|
|
!section.contains("\n## Pipeline"),
|
|
"section must stop before the next top-level header (\\n## Pipeline); \
|
|
extractor regressed to whole-document scanning"
|
|
);
|
|
assert!(
|
|
section.len() > 1000,
|
|
"§Data model is the canonical schema reference; a slice shorter than \
|
|
1 KB suggests the extractor truncated; got {} bytes",
|
|
section.len()
|
|
);
|
|
}
|
|
|