diff --git a/crates/ailang-core/tests/design_schema_drift.rs b/crates/ailang-core/tests/design_schema_drift.rs new file mode 100644 index 0000000..498e724 --- /dev/null +++ b/crates/ailang-core/tests/design_schema_drift.rs @@ -0,0 +1,369 @@ +//! Sweep 3 / Task 4: 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 +//! DESIGN.md. + +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"); + +/// 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_int".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() }), + }, + ), + ]; + + 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", + }; + assert!( + DESIGN_MD.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!( + DESIGN_MD.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!( + DESIGN_MD.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), + ]; + + for (anchor, lit) in exemplars { + let _: &'static str = match lit { + Literal::Int { .. } => "int", + Literal::Bool { .. } => "bool", + Literal::Str { .. } => "str", + Literal::Unit => "unit", + }; + assert!( + DESIGN_MD.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`, +/// `ClassDef`, `InstanceDef`) 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": "ClassDef""#, Def::Class(class_def)), + (r#""kind": "InstanceDef""#, 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!( + DESIGN_MD.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!( + DESIGN_MD.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!( + DESIGN_MD.contains(anchor), + "DESIGN.md §Data-model is missing nested-struct-key anchor `{anchor}`" + ); + } +}