iter embedding-abi-m1.1 (PARTIAL 3/7): schema + surface + baseline prerequisites; plan Repair

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.
This commit is contained in:
2026-05-18 14:32:39 +02:00
parent 123ae1be5c
commit 818177d835
27 changed files with 709 additions and 38 deletions
+9
View File
@@ -70,6 +70,7 @@ Five kinds, matched on the leading keyword:
```
(fn NAME
(doc STRING)?
(export STRING)?
(suppress (code STRING) (because STRING))*
(type FN-TYPE)
(params NAME*)
@@ -79,6 +80,14 @@ Five kinds, matched on the leading keyword:
`doc` is optional but recommended — it appears in the prose projection
and helps downstream readers (human and LLM).
`export` is optional. When present, the string is the C symbol under
which codegen's `--emit=staticlib` mode exposes this fn as an
externally-callable entrypoint (Embedding ABI M1). The symbol is
author-chosen and decoupled from the internal `ail_<module>_<def>`
mangling. M1 requires the fn's parameter and return types to be
scalar (`Int`/`Float`) and its effect set to be empty — see
DESIGN.md §"Embedding ABI (M1)".
`suppress` clauses silence advisory diagnostics for this def. The
`code` MUST be one of the registered codes (today: `over-strict-mode`).
The `because` MUST be a non-empty justification — empty / whitespace-
+13
View File
@@ -205,6 +205,19 @@ pub struct FnDef {
/// Optional source-level documentation string.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub doc: Option<String>,
/// Iter embedding-abi-m1: when `Some(sym)`, this fn is the
/// embedding boundary. Codegen's `Target::StaticLib` mode
/// additionally emits an externally-visible C entrypoint
/// `@<sym>` (scalar signature, provisional until M3) forwarding
/// to `@ail_<module>_<fn>`. The symbol is author-chosen and
/// decoupled from the `ail_<module>_<def>` mangling so the
/// M3-frozen ABI survives module/fn refactors.
///
/// Serialised `skip_serializing_if = "Option::is_none"` so every
/// pre-M1 fixture's canonical-JSON hash stays bit-identical —
/// the same additive-schema pattern as [`FnDef::doc`].
#[serde(default, skip_serializing_if = "Option::is_none")]
pub export: Option<String>,
/// Iter 19b: structured-diagnostic suppressions opted into for this
/// fn. Each entry names a diagnostic code and an author-asserted
/// reason. Currently the only consumer is `over-strict-mode`
+16
View File
@@ -910,6 +910,7 @@ impl Desugarer {
body: body_full,
suppress: vec![],
doc: None,
export: None,
}));
in_full
}
@@ -1733,6 +1734,7 @@ mod tests {
body: body_match,
suppress: vec![],
doc: None,
export: None,
})],
};
let out = desugar_module(&m);
@@ -1789,6 +1791,7 @@ mod tests {
body: original.clone(),
suppress: vec![],
doc: None,
export: None,
})],
};
let out = desugar_module(&m);
@@ -1846,6 +1849,7 @@ mod tests {
body: fact_letrec_term(),
suppress: vec![],
doc: None,
export: None,
})],
};
let out = desugar_module(&m);
@@ -1934,6 +1938,7 @@ mod tests {
},
suppress: vec![],
doc: None,
export: None,
})],
};
let out = desugar_module(&m);
@@ -2059,6 +2064,7 @@ mod tests {
},
suppress: vec![],
doc: None,
export: None,
})],
};
let out = desugar_module(&m);
@@ -2173,6 +2179,7 @@ mod tests {
body: outer_body,
suppress: vec![],
doc: None,
export: None,
}),
],
};
@@ -2251,6 +2258,7 @@ mod tests {
body: letrec,
suppress: vec![],
doc: None,
export: None,
})],
};
let _ = desugar_module(&m);
@@ -2312,6 +2320,7 @@ mod tests {
body: letrec,
suppress: vec![],
doc: None,
export: None,
})],
};
let out = desugar_module(&m);
@@ -2450,6 +2459,7 @@ mod tests {
body: letrec,
suppress: vec![],
doc: None,
export: None,
})],
};
let out = desugar_module(&m);
@@ -2553,6 +2563,7 @@ mod tests {
body: letrec,
suppress: vec![],
doc: None,
export: None,
})],
};
let _ = desugar_module(&m);
@@ -2634,6 +2645,7 @@ mod tests {
body: outer,
suppress: vec![],
doc: None,
export: None,
})],
};
let _ = desugar_module(&m);
@@ -2714,6 +2726,7 @@ mod tests {
body: outer,
suppress: vec![],
doc: None,
export: None,
})],
};
let out = desugar_module(&m);
@@ -2839,6 +2852,7 @@ mod tests {
body: body_match,
suppress: vec![],
doc: None,
export: None,
})],
};
let out = desugar_module(&m);
@@ -2912,6 +2926,7 @@ mod tests {
body: body_match,
suppress: vec![],
doc: None,
export: None,
})],
};
let out = desugar_module(&m);
@@ -2993,6 +3008,7 @@ mod tests {
body: body_match,
suppress: vec![],
doc: None,
export: None,
})],
};
let out = desugar_module(&m);
+1
View File
@@ -191,6 +191,7 @@ mod tests {
},
suppress: vec![],
doc: None,
export: None,
}),
],
}
@@ -282,6 +282,7 @@ fn design_md_anchors_every_def_kind() {
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(),
@@ -0,0 +1,35 @@
//! Additive-field hash stability (Embedding-ABI M1): adding
//! `FnDef.export: Option<String>` with
//! `#[serde(default, skip_serializing_if = "Option::is_none")]`
//! must leave every pre-M1 canonical-JSON hash bit-identical when
//! `export` is `None`. A round-trip-able fixture WITHOUT `export`
//! hashes identically to a hand-frozen pre-M1 golden.
//!
//! The hashing entry point is `ailang_core::hash::def_hash(&Def)` —
//! the exact one `crates/ailang-core/tests/hash_pin.rs` uses (the
//! plan's `content_hash_fn(&FnDef)` symbol does not exist; the plan
//! NOTE authorises mirroring `hash_pin.rs`'s entry point when the
//! name differs). The golden below was captured pre-field via
//! `def_hash` on the same `f : (Int)->Int / body=x` shape.
use ailang_core::ast::*;
use ailang_core::hash::def_hash;
#[test]
fn fn_without_export_hash_is_unchanged() {
// A minimal pre-M1-shaped fn (no export). Its content hash must
// equal the value captured before the field existed.
let def = Def::Fn(FnDef {
name: "f".into(),
ty: Type::fn_implicit(vec![Type::int()], Type::int(), vec![]),
params: vec!["x".into()],
body: Term::Var { name: "x".into() },
doc: None,
suppress: vec![],
export: None,
});
// GOLDEN: captured pre-field (Step 2) from `def_hash` on this shape.
let golden = "b4662aa70839f60b";
assert_eq!(def_hash(&def), golden,
"pre-M1 fn hash drifted — additive-field invariant violated");
}
+1
View File
@@ -45,6 +45,7 @@ fn sample_fn() -> Def {
},
suppress: vec![],
doc: None,
export: None,
})
}
+1
View File
@@ -260,6 +260,7 @@ fn spec_mentions_every_def_kind() {
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(),