//! 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 ailang_test_support::{examples_dir, list_ail_fixtures}; use ailang_core::ast::{ Def, InstanceMethod, Literal, Module, NewArg, 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, TermLoop, TermRecur, TermNew, TermIntrinsic, // Pattern PatternWild, PatternVar, PatternLit, PatternCtor, // Literal LiteralInt, LiteralBool, LiteralStr, LiteralUnit, LiteralFloat, // Type TypeCon, TypeFn, TypeVar, TypeForall, // ParamMode 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::TermLoop, VariantTag::TermRecur, VariantTag::TermNew, VariantTag::TermIntrinsic, 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::ParamModeOwn, VariantTag::ParamModeBorrow, ]; fn visit_def(def: &Def, observed: &mut HashSet) { 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) { visit_term(&m.body, observed); } fn visit_term(t: &Term, observed: &mut HashSet) { 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::Loop { binders, body } => { observed.insert(VariantTag::TermLoop); for b in binders { visit_type(&b.ty, observed); visit_term(&b.init, observed); } visit_term(body, observed); } Term::Recur { args } => { observed.insert(VariantTag::TermRecur); for a in args { visit_term(a, observed); } } Term::New { args, .. } => { // Functional construction. Exercised by the `new_*` / // `raw_buf_*` fixtures under examples/. observed.insert(VariantTag::TermNew); for arg in args { match arg { NewArg::Type(t) => visit_type(t, observed), NewArg::Value(v) => visit_term(v, observed), } } } Term::Intrinsic => { observed.insert(VariantTag::TermIntrinsic); } } } fn visit_pattern(p: &Pattern, observed: &mut HashSet) { 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) { 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) { 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) { match m { ParamMode::Own => { observed.insert(VariantTag::ParamModeOwn); } ParamMode::Borrow => { observed.insert(VariantTag::ParamModeBorrow); } } } fn visit_module(m: &Module, observed: &mut HashSet) { for def in &m.defs { visit_def(def, observed); } } #[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 = HashSet::new(); let mut load_failures = Vec::::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 = EXPECTED_VARIANTS .iter() .copied() .filter(|v| !observed.contains(v)) .collect(); if !missing.is_empty() { let names: Vec = 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() ); }