6fdb45d2f2
Single iter shipping the post-milestone-24 follow-up named in
docs/specs/2026-05-14-retire-per-type-print-effects.md. After this
iter the only surviving direct-output effect-op is `io/print_str`;
all per-type print primitives are replaced by the polymorphic
`print` helper (prelude, iter 24.3).
Components:
- 92 examples/*.ail fixtures migrated (do io/print_<T> x) →
(app print x); 6 .prose.txt snapshots regenerated via `ail prose`.
- Four-site lockstep compiler deletion: crates/ailang-check/src/builtins.rs
(3 effect_ops.insert blocks + 3 list() rows + the
install_io_print_float_signature test + module + EffectOpSig
doc-comments); crates/ailang-codegen/src/lib.rs lower_app
(3 arms + lowers_io_print_float test); crates/ailang-codegen/src/synth.rs
builtin_effect_op_ret match-arm pattern. Dead `intern_string`
helper removed as a follow-up.
- Five incidental test-body migrations (ailang-check x2, ailang-core
spec_drift + design_schema_drift, ailang-surface/src/lex.rs,
ailang-prose/src/lib.rs round-trip test).
- Cat B test-harness patch: six IR-shape tests in
crates/ail/tests/e2e.rs gained a monomorphise_workspace call
before lower_workspace_with_alloc so they follow the same
pipeline as `ail build` (the home-rolled desugar+lift loop
stayed because mono's precondition is "already lifted"; mono
inserts after lift).
- Six doc-comment touch-ups (lex.rs module doc, parse.rs
diagnostic example, ail/src/main.rs x2, runtime/str.c %g anchor,
crates/ailang-core/specs/form_a.md surface-spec example).
- DESIGN.md seven-site sweep (Decision 11 example, Polymorphic
print past-tense, Heap-Str output sentence, effect-op
invocation comment, Float NaN paragraph re-anchored on
float_to_str, two "What is supported" lists).
- Three E2E test-comment polish + four IR-snapshot refresh + one
canonical-hash pin update (plan-unanticipated downstream
consequences of the corpus migration).
- bench/{check,compile_check,cross_lang}.py: all exit 0; no
ratification needed.
- Roadmap entry struck through; per-iter journal at
docs/journals/2026-05-14-iter-rpe.1.md.
Tests 564/0/3. cargo clippy and cargo doc: zero warnings.
Two upstream codegen bugs surfaced during the first BLOCKED
attempt and were fixed in separate iters before this retry:
- 1fb225e bugfix: mono cursor misalignment at poly-free-fn Var
with class-constrained Forall.
- feb9413 bugfix: print leak — propagate ret_mode through rigid
substitution + prelude Show.show ret_mode.
Known debt (carried forward for next /audit):
- Emitter.strings field is functionally dead post-iter (orphan
after intern_string removal); cycles over empty map harmlessly.
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_str".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(),
|
|
};
|
|
}
|