Files
AILang/crates/ailang-core/tests/schema_coverage.rs
T
Brummel 52ff8738b8 iter intrinsic-bodies.1-mechanism (DONE 8/8): Term::Intrinsic leaf + cross-crate wiring (refs #9)
First iteration of the intrinsic-bodies milestone. Introduces the
Form-A `(intrinsic)` body marker as a new leaf AST term and wires it
through surface, checker, codegen, and a kernel_stub ratifier. The
prelude migration + hard-lockstep pin + dead-path removal are .2.

What landed (8 tasks):

  Task 1 — Term::Intrinsic unit variant (ast.rs), tag "t":"intrinsic"
    via the enum's rename_all=lowercase. Additive: no existing fixture
    carries it, hashes bit-identical. In-core exhaustive-match arms
    (canonical/hash/visit/pretty/desugar/workspace) added as leaves.
  Task 2 — surface parse + print: (intrinsic) as a fn body-slot clause
    and a lambda positional body, mapped to/from Term::Intrinsic.
  Task 3 — cross-crate walker sweep (check/codegen/prose/ail-main):
    leaf no-op/identity arms at every no-wildcard Term match the
    compiler flagged.
  Task 4 — checker: new Env.current_module_kernel_tier flag (m.kernel
    || m.name=="prelude"), set alongside current_module. A def whose
    body is intrinsic (top-level fn OR instance-method lambda, via the
    shared is_intrinsic_body helper) is checked signature-only; an
    intrinsic body outside kernel-tier/prelude is rejected with
    intrinsic-outside-kernel-tier.
  Task 5 — codegen: an intrinsic-bodied fn routes through the existing
    try_emit_primitive_instance_body / intercepts::lookup path; if no
    intercept fired it is an internal error, never a lower_term
    fallthrough. lower_term and the synth walker get Term::Intrinsic
    internal-error arms (an intrinsic body reaching either is an
    escape bug).
  Task 6 — answer intercept (ret i64 42) registered in INTERCEPTS;
    the `answer : () -> Int` intrinsic added to STUB_AIL;
    examples/kernel_intrinsic_smoke.ail added so schema_coverage
    observes Term::Intrinsic in the examples/ corpus.
  Task 7 — E2E ratifier: examples/kernel_answer.ail calls
    kernel_stub.answer and prints 42; answer_intrinsic_builds_and_runs_printing_42
    asserts it end-to-end (source → native).
  Task 8 — design/contracts/0002-data-model.md gains the
    { "t": "intrinsic" } Term entry + fn/lam prose; form_a.md grammar
    note updated.

Verification:
  cargo test --workspace → 669 passed, 0 failed (baseline 667 +2:
    intrinsic_in_user_module_is_rejected, answer_intrinsic_builds_and_runs_printing_42).
  bench/check.py + bench/compile_check.py → 0 regressed.
  Reject E2E (subprocess ail check --json, exit 1, code
    intrinsic-outside-kernel-tier) GREEN.
  Round-trip + hash pins GREEN — Term::Intrinsic is additive, no
    existing fixture carries it, no hash moved.

Three implementation completions beyond the plan (all behaviour-
preserving, surfaced during execution):

1. The signature-only skip had to apply at the mono pass's two
   synth-on-body re-entry sites (collect_mono_targets,
   collect_residuals_ordered), not only check_fn — else an intrinsic
   body hits synth's Term::Intrinsic internal-error guard. Repaired by
   extracting the shared crate::is_intrinsic_body helper and applying
   it at all three synth-on-body paths. Not a representation surprise:
   the same signature-only treatment, more call sites.

2. The compiler-enumerated exhaustive-match set was broader than the
   plan's named grep set (the plan anticipated this and made the sweep
   compile-driven). Extra leaf arms in core desugar/workspace, check
   reuse-as + qualify_workspace_term, codegen synth_with_extras,
   ail/src/main.rs, and four test targets.

3. Fixture corrections: emit_answer needed the body-close
   (block terminator) the plan snippet omitted; kernel_answer.ail's
   main is (ret Unit)(effects IO) using (app print ...) since
   io/print_int does not exist (the plan flagged this for the
   implementer to resolve against real effect-op names).

IR snapshots (hello/sum/list/max3/ws_main.ll) refreshed: purely
additive @ail_kernel_stub_answer fn+adapter+closure, emitted into
every workspace exactly as the pre-existing @ail_kernel_stub_new
already was (kernel_stub is auto-injected; confirmed new was present
in the pre-iter hello.ll baseline). No user-fn IR changed.

The .2 iteration migrates the 18 prelude dummy bodies to (intrinsic),
upgrades registry_contains_all_legacy_arms to a source<->registry
bijection pin, and removes the dead body-lowering path.
2026-05-29 17:21:32 +02:00

438 lines
13 KiB
Rust

//! 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 std::path::PathBuf;
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,
TermIntrinsic,
// Pattern
PatternWild,
PatternVar,
PatternLit,
PatternCtor,
// Literal
LiteralInt,
LiteralBool,
LiteralStr,
LiteralUnit,
LiteralFloat,
// Type
TypeCon,
TypeFn,
TypeVar,
TypeForall,
// ParamMode
ParamModeImplicit,
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::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::ParamModeImplicit,
VariantTag::ParamModeOwn,
VariantTag::ParamModeBorrow,
];
fn visit_def(def: &Def, observed: &mut HashSet<VariantTag>) {
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<VariantTag>) {
visit_term(&m.body, observed);
}
fn visit_term(t: &Term, observed: &mut HashSet<VariantTag>) {
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, .. } => {
// prep.2 (kernel-extension-mechanics): functional construction.
// The variant is intentionally NOT added to `VariantTag` /
// `EXPECTED_VARIANTS` in prep.2 — no .ail fixture in the
// current corpus emits `"t": "new"` (per the iter's
// out-of-scope note), so an EXPECTED entry would fail the
// coverage check. A future milestone that ships a Term::New
// fixture extends both the enum and the expected list.
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<VariantTag>) {
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<VariantTag>) {
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<VariantTag>) {
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<VariantTag>) {
match m {
ParamMode::Implicit => {
observed.insert(VariantTag::ParamModeImplicit);
}
ParamMode::Own => {
observed.insert(VariantTag::ParamModeOwn);
}
ParamMode::Borrow => {
observed.insert(VariantTag::ParamModeBorrow);
}
}
}
fn visit_module(m: &Module, observed: &mut HashSet<VariantTag>) {
for def in &m.defs {
visit_def(def, observed);
}
}
fn examples_dir() -> PathBuf {
let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
crate_dir.parent().unwrap().parent().unwrap().join("examples")
}
fn list_ail_fixtures() -> Vec<PathBuf> {
let dir = examples_dir();
let mut paths: Vec<PathBuf> = std::fs::read_dir(&dir)
.unwrap_or_else(|e| panic!("read_dir({}): {e}", dir.display()))
.filter_map(|entry| entry.ok())
.map(|e| e.path())
.filter(|p| {
p.file_name()
.and_then(|n| n.to_str())
.map(|n| n.ends_with(".ail"))
.unwrap_or(false)
})
.collect();
paths.sort();
paths
}
#[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<VariantTag> = HashSet::new();
let mut load_failures = Vec::<String>::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<VariantTag> = EXPECTED_VARIANTS
.iter()
.copied()
.filter(|v| !observed.contains(v))
.collect();
if !missing.is_empty() {
let names: Vec<String> = 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()
);
}