818177d835
Tasks 1-3 of docs/plans/embedding-abi-m1.1.md, a verified known-good subset (cargo build --workspace exit 0; full workspace green; the roundtrip_cli all-examples gate green): - Task 1: CLI build-path MissingEntryMain baseline pin (examples/embed_noentry_baseline.ail + embed_missing_main_baseline.rs; triple-assertion, layered on the green codegen-unit pin lib.rs:3314). - Task 2: additive FnDef.export: Option<String> (serde default + skip_serializing_if, modelled on FnDef.doc); compile-driven thread of export: None across ~85 FnDef/AstFnDef literals / 18 files incl. all in-source #[cfg(test)] mod tests + drift/hash/spec-drift literals + the codegen lib.rs:3320 in-source pin; hash-stability golden pin; DESIGN.md fn-JSON "export" line. DONE_WITH_CONCERNS (plan symbol content_hash_fn -> def_hash per hash_pin.rs, plan- pseudo-vs-reality class, plan corrected). - Task 3: Form-A (export "<sym>") modifier — parse_export + parse_fn thread + write_fn_def emission + form_a.md grammar; byte-identical round-trip. Task 4 BLOCKED (Boss plan-defect: fixtures declared module != file stem; AILang's loader hard-enforces module==stem at workspace.rs:438, so ail check panicked on load before the gate). Boss resolution (Option 2, overriding the orchestrator's Option 1, rationale in the journal + plan Design-decision 6): keep descriptive embed_* filenames, rename fixture MODULES to their stems — the C symbol backtest_step stays via (export ...), demonstrating spec Decision 2's mangling- decoupling rather than violating it; archive becomes libembed_backtest_step.a, internal symbol @ail_embed_backtest_step_step, host.c unchanged. Plan fixed (Design-decision 6 + Task-4 module==stem + Task-5/6/7 dependent strings + the no-*. Float-head + content_hash_fn concerns folded). Headline fixture corrected; blocked-Task-4 WIP discarded (recreated by the [4,7] re-dispatch from the fixed plan). PARTIAL commit (not the iter's final commit): the implement Repair path mandates committing the known-good subset so the [4,7] re-dispatch starts from a clean tree that includes Tasks 1-3 (Tasks 4-7 structurally depend on the schema field + surface). INDEX line added when the full iter lands post-re-dispatch.
342 lines
11 KiB
Rust
342 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() }),
|
|
},
|
|
),
|
|
(
|
|
"(loop",
|
|
Term::Loop {
|
|
binders: Vec::new(),
|
|
body: Box::new(Term::Lit { lit: Literal::Unit }),
|
|
},
|
|
),
|
|
(
|
|
"(recur",
|
|
Term::Recur { args: vec![] },
|
|
),
|
|
];
|
|
|
|
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",
|
|
Term::Loop { .. } => "loop",
|
|
Term::Recur { .. } => "recur",
|
|
};
|
|
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 } },
|
|
export: None,
|
|
};
|
|
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(),
|
|
};
|
|
}
|