//! 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), (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!( 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`, /// `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!( 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}`" ); } }