328 lines
11 KiB
Rust
328 lines
11 KiB
Rust
//! Iter 20f: drift detection between the AST and `specs/form_a.md`.
|
|
//!
|
|
//! The spec is hand-curated, but it cannot silently fall behind the
|
|
//! language. Every AST enum (`Term`, `Pattern`, `Type`, `Def`, `Literal`,
|
|
//! `ParamMode`) discriminates on a `#[serde(rename = "...")]` tag.
|
|
//! These tests construct a sample of every variant, then check that the
|
|
//! corresponding tag string (or its parenthesised Form-A keyword) appears
|
|
//! in the spec.
|
|
//!
|
|
//! The exhaustive `match` is the load-bearing piece: adding a new variant
|
|
//! without a spec entry fails compilation here long before the test runs.
|
|
//! Once the variant is matched, the test asserts the spec mentions it.
|
|
|
|
use ailang_core::ast::{
|
|
ConstDef, Ctor, Def, FnDef, Literal, Pattern, Suppress, Term, Type, TypeDef,
|
|
};
|
|
use ailang_core::FORM_A_SPEC;
|
|
|
|
/// Every `Term` variant must be reachable from the spec. The Form-A
|
|
/// keyword for each variant is what the spec is supposed to teach an
|
|
/// LLM; if it is missing here, the LLM cannot produce that term.
|
|
#[test]
|
|
fn spec_mentions_every_term_variant() {
|
|
let exemplars: Vec<(&str, Term)> = vec![
|
|
("(lit-unit", Term::Lit { lit: Literal::Unit }),
|
|
// The Var form has no parenthesised keyword (a bare ident is a
|
|
// var-ref). The spec calls it out under "Atom forms"; we look for
|
|
// that anchor.
|
|
("Atom forms", Term::Var { name: "x".into() }),
|
|
(
|
|
"(app",
|
|
Term::App {
|
|
callee: Box::new(Term::Var { name: "f".into() }),
|
|
args: vec![Term::Var { name: "x".into() }],
|
|
tail: false,
|
|
},
|
|
),
|
|
(
|
|
"(let ",
|
|
Term::Let {
|
|
name: "x".into(),
|
|
value: Box::new(Term::Lit { lit: Literal::Int { value: 1 } }),
|
|
body: Box::new(Term::Var { name: "x".into() }),
|
|
},
|
|
),
|
|
(
|
|
"(let-rec",
|
|
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() }),
|
|
},
|
|
),
|
|
(
|
|
"(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 } }),
|
|
},
|
|
),
|
|
(
|
|
"(do ",
|
|
Term::Do {
|
|
op: "io/print_int".into(),
|
|
args: vec![],
|
|
tail: false,
|
|
},
|
|
),
|
|
(
|
|
"(term-ctor",
|
|
Term::Ctor {
|
|
type_name: "List".into(),
|
|
ctor: "Nil".into(),
|
|
args: vec![],
|
|
},
|
|
),
|
|
(
|
|
"(match",
|
|
Term::Match {
|
|
scrutinee: Box::new(Term::Var { name: "x".into() }),
|
|
arms: vec![],
|
|
},
|
|
),
|
|
(
|
|
"(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 } }),
|
|
},
|
|
),
|
|
(
|
|
"(seq",
|
|
Term::Seq {
|
|
lhs: Box::new(Term::Var { name: "a".into() }),
|
|
rhs: Box::new(Term::Var { name: "b".into() }),
|
|
},
|
|
),
|
|
(
|
|
"(clone",
|
|
Term::Clone {
|
|
value: Box::new(Term::Var { name: "x".into() }),
|
|
},
|
|
),
|
|
(
|
|
"(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 {
|
|
// Force the exhaustive match: the body is just a tag string that
|
|
// we will not actually use, but the compiler will refuse to
|
|
// compile this file once a new Term variant is added without a
|
|
// matching arm.
|
|
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!(
|
|
FORM_A_SPEC.contains(anchor),
|
|
"spec is missing anchor `{anchor}` for a Term variant — \
|
|
update crates/ailang-core/specs/form_a.md"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Every `Pattern` variant must appear in the spec.
|
|
#[test]
|
|
fn spec_mentions_every_pattern_variant() {
|
|
let exemplars: Vec<(&str, Pattern)> = vec![
|
|
("_", Pattern::Wild),
|
|
// pat-var is again the bare-ident form. The spec discusses it
|
|
// under "Patterns". Use the heading as the anchor.
|
|
("## Patterns", Pattern::Var { name: "x".into() }),
|
|
("(pat-lit", Pattern::Lit { lit: Literal::Int { value: 0 } }),
|
|
(
|
|
"(pat-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!(
|
|
FORM_A_SPEC.contains(anchor),
|
|
"spec is missing anchor `{anchor}` for a Pattern variant"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Every `Type` variant must appear in the spec.
|
|
#[test]
|
|
fn spec_mentions_every_type_variant() {
|
|
let exemplars: Vec<(&str, Type)> = vec![
|
|
("(con ", Type::int()),
|
|
(
|
|
"(fn-type",
|
|
Type::fn_implicit(vec![], Type::unit(), vec![]),
|
|
),
|
|
(
|
|
"TYVAR-NAME",
|
|
Type::Var { name: "a".into() },
|
|
),
|
|
(
|
|
"(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!(
|
|
FORM_A_SPEC.contains(anchor),
|
|
"spec is missing anchor `{anchor}` for a Type variant"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Every `Literal` variant must appear in the spec.
|
|
#[test]
|
|
fn spec_mentions_every_literal_variant() {
|
|
// Anchors describe how the literal renders in Form-A.
|
|
let exemplars: Vec<(&str, Literal)> = vec![
|
|
("`INT`", Literal::Int { value: 0 }),
|
|
("`true`, `false`", Literal::Bool { value: true }),
|
|
("`STRING`", Literal::Str { value: "x".into() }),
|
|
("(lit-unit)", Literal::Unit),
|
|
("`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!(
|
|
FORM_A_SPEC.contains(anchor),
|
|
"spec is missing anchor `{anchor}` for a Literal variant"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Every `Def` kind must appear in the spec.
|
|
#[test]
|
|
fn spec_mentions_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 exemplars: Vec<(&str, Def)> = vec![
|
|
("(fn ", Def::Fn(fn_def)),
|
|
("(const ", Def::Const(const_def)),
|
|
("(data ", Def::Type(type_def)),
|
|
];
|
|
|
|
for (anchor, def) in exemplars {
|
|
let _: &'static str = match def {
|
|
Def::Fn(_) => "fn",
|
|
Def::Const(_) => "const",
|
|
Def::Type(_) => "type",
|
|
// Iter 22b.1: class/instance Def variants exist but are
|
|
// not yet anchored in the prose-spec block. Once 22b.4
|
|
// adds prose projection for them, the FORM_A_SPEC text
|
|
// gains `(class ` / `(instance ` anchors and this match
|
|
// will be exercised. For 22b.1 the exemplars list above
|
|
// does not produce these variants.
|
|
Def::Class(_) => "class",
|
|
Def::Instance(_) => "instance",
|
|
};
|
|
assert!(
|
|
FORM_A_SPEC.contains(anchor),
|
|
"spec is missing anchor `{anchor}` for a Def kind"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// The mode keywords (Decision 10) must appear so an LLM knows the
|
|
/// Form-A wrapper syntax.
|
|
#[test]
|
|
fn spec_mentions_mode_keywords() {
|
|
assert!(FORM_A_SPEC.contains("(own"), "spec missing `(own ...)`");
|
|
assert!(FORM_A_SPEC.contains("(borrow"), "spec missing `(borrow ...)`");
|
|
}
|
|
|
|
/// `tail-app` and `tail-do` are distinct keywords from `app`/`do`. The
|
|
/// spec must mention both, otherwise an LLM cannot produce tail-correct
|
|
/// code at scale.
|
|
#[test]
|
|
fn spec_mentions_tail_variants() {
|
|
assert!(FORM_A_SPEC.contains("tail-app"), "spec missing `tail-app`");
|
|
assert!(FORM_A_SPEC.contains("tail-do"), "spec missing `tail-do`");
|
|
}
|
|
|
|
/// `suppress` is part of the surface and the LLM must know how to
|
|
/// preserve it. Empty-because is itself a diagnostic; the spec calls
|
|
/// it out so the LLM does not produce empty justifications.
|
|
#[test]
|
|
fn spec_mentions_suppress_clause() {
|
|
assert!(FORM_A_SPEC.contains("(suppress"), "spec missing `(suppress ...)`");
|
|
assert!(
|
|
FORM_A_SPEC.contains("empty-suppress-reason"),
|
|
"spec missing the empty-suppress-reason diagnostic"
|
|
);
|
|
// Make sure `Suppress` in the AST can still be constructed — the
|
|
// exhaustive-match property carries through to the surface.
|
|
let _ = Suppress {
|
|
code: "x".into(),
|
|
because: "y".into(),
|
|
};
|
|
}
|