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
+97 -37
View File
@@ -95,6 +95,45 @@ finance/`data-server` knowledge or dependency (Invariant 1).
ABI (M1)" subsection documents a capability that only fully exists
after codegen+CLI+E2E → Task 7 (DESIGN.md = current-state mirror:
document the ABI once it demonstrably works).
6. **Fixture module name == file stem (Option 2; revised
post-Task-4-BLOCK 2026-05-18).** AILang's workspace loader hard-
enforces `module-name == file-stem`
(`crates/ailang-core/src/workspace.rs:438`,
`WorkspaceLoadError::ModuleNameMismatch`, no exception) — so every
fixture's `(module …)` MUST equal its filename stem or `ail check`
panics on load before any gate runs. The original plan's headline
used `(module backtest)` in `embed_backtest_step.ail` (and
`bad`/`bad_io`/… in the gate fixtures) — malformed under that
invariant; the implement-orchestrator correctly BLOCKED Task 4 on
it. Resolution: **rename the fixture *modules* to their file
stems, keep the descriptive `embed_*` filenames** (NOT the
orchestrator-suggested Option 1 of renaming files to bare module
stems). Rationale, from semantics not effort: (a) the spec's
load-bearing contract (Decision 2, spec lines 5259/167169) is
that the exported C symbol is *author-chosen and decoupled from
`ail_<module>` mangling* — `(export "backtest_step")` is the
contract; the host asserts on `backtest_step`, which is invariant
under module rename. So spec-faithfulness does not discriminate;
`libbacktest.a`/`@ail_backtest_step` are *illustrative* (build-
output filename + internal mangling — the latter is exactly what
the spec decouples from), not frozen-ABI contracts. (b) `examples/`
is a flat shared corpus auto-enrolled in whole-corpus tests;
Option 1's `examples/bad.ail`/`backtest.ail` pollute it with
collision-prone non-self-describing names — Option 2 keeps the
self-describing grouped `embed_*` set. (c) Option 2 *is* a
concrete demonstration of the spec's own Decision-2 decoupling
thesis (module `embed_backtest_step`, C symbol `backtest_step`),
not a violation of it. The spec's `(module backtest)`/`(module
bad)` blocks are shape-illustrative (planner contract: spec owns
shape, not bytes — same as the spec naming a filename while the
contract is the `(export …)` symbol). Consequences threaded below:
entry module `embed_backtest_step` ⇒ archive
`libembed_backtest_step.a`, internal symbol
`@ail_embed_backtest_step_step`; the external C symbol
`backtest_step` and `host.c` are UNCHANGED. **Architect-at-close
note:** a fixture module name differing from the spec's
illustrative block is expected and correct under the module==stem
invariant + Decision 2 — not drift.
---
@@ -266,9 +305,15 @@ fn fn_without_export_hash_is_unchanged() {
}
```
> NOTE: the `content_hash_fn` symbol + `Type::fn_implicit` /
> `Term::Var` constructors are the ones already used by
> `crates/ailang-core/tests/hash_pin.rs` and
> NOTE (corrected post-iter-1): there is **no**
> `ailang_core::hash::content_hash_fn`. The real entry point is
> `def_hash(&Def) -> String` — wrap the `FnDef` in `Def::Fn(...)`
> and call `def_hash`, exactly as `crates/ailang-core/tests/hash_pin.rs`
> does. (Task 2 already executed this correctly per this NOTE's
> original "mirror hash_pin.rs" license; the symbol name above is
> left only so the diff to the as-executed test is legible.)
> `Type::fn_implicit` / `Term::Var` constructors are the ones
> already used by `hash_pin.rs` and
> `design_schema_drift.rs:282`; the implementer mirrors the exact
> hashing entry point that `hash_pin.rs` uses if the name differs.
> The `<<GOLDEN-PLACEHOLDER>>` is resolved deterministically in
@@ -381,11 +426,13 @@ constructs/serialises identically because `export: None` is omitted).
- [ ] **Step 1: Write the headline round-trip RED fixture**
Create `examples/embed_backtest_step.ail` (the spec headline,
verbatim):
Create `examples/embed_backtest_step.ail` (the spec headline; module
name == file stem per Design-decision 6 — the C symbol stays
`backtest_step` via the `(export …)` string, demonstrating spec
Decision 2's mangling-decoupling):
```
(module backtest
(module embed_backtest_step
(fn step
(export "backtest_step")
@@ -577,7 +624,7 @@ Expected: PASS (form_a.md is not hashed; this confirms no
`examples/embed_export_str_param_rejected.ail` (Str param, else
scalar+pure → `export-non-scalar-signature`):
```
(module bad_str_param
(module embed_export_str_param_rejected
(fn f
(export "f")
(type
@@ -591,7 +638,7 @@ scalar+pure → `export-non-scalar-signature`):
`examples/embed_export_adt_ret_rejected.ail` (ADT return →
`export-non-scalar-signature`):
```
(module bad_adt_ret
(module embed_export_adt_ret_rejected
(data Pair
(ctor Pair (con Int) (con Int)))
(fn f
@@ -607,7 +654,7 @@ scalar+pure → `export-non-scalar-signature`):
`examples/embed_export_io_rejected.ail` (scalar params+ret but `!IO`
→ `export-has-effects`):
```
(module bad_io
(module embed_export_io_rejected
(fn f
(export "f")
(type
@@ -624,7 +671,7 @@ scalar+pure → `export-non-scalar-signature`):
Str param **and** `!IO`; gate-order params→ret→effects ⇒ fails
`export-non-scalar-signature`):
```
(module bad
(module embed_export_effectful_rejected
(fn log_step
(export "log_step")
(type
@@ -640,7 +687,7 @@ Str param **and** `!IO`; gate-order params→ret→effects ⇒ fails
`examples/embed_export_float_ok.ail` (positive — `(Float,Float)->Float`,
pure; the second scalar type beyond the Int headline):
```
(module embed_float_ok
(module embed_export_float_ok
(fn scale
(export "embed_scale")
(type
@@ -649,13 +696,15 @@ pure; the second scalar type beyond the Int headline):
(ret (con Float))))
(params a b)
(body
(app *. a b))))
(app * a b))))
```
> The implementer verifies `*.` is the Form-A Float-multiply head
> against an existing Float example (`examples/floats.ail`); if the
> Float-multiply surface differs, use the same head that example
> uses. The fixture's *role* is "scalar+pure export passes the gate".
> NOTE (folded from the iter-1 BLOCK concern): AILang has **no**
> `*.` Float-multiply head — Float arithmetic uses the same
> typeclass-generic head as Int (`examples/floats.ail` uses
> `(app + …)`; codegen lowers to `fmul double` by type). The
> fixture above already uses `(app * a b)`. The fixture's *role* is
> "scalar+pure Float export passes the gate".
- [ ] **Step 2: Write the gate pin — verify RED**
@@ -858,9 +907,12 @@ Create `crates/ailang-codegen/tests/embed_staticlib_lowering.rs`:
```rust
//! Embedding-ABI M1 codegen (spec §4): `Target::StaticLib` lowering
//! emits NO `@main` trampoline, NO `MissingEntryMain`, and one
//! external `define i64 @backtest_step(...)` forwarding to
//! `@ail_backtest_step`. Executable-target lowering is byte-unchanged
//! (the `missing_entry_main_is_error` in-source pin still holds).
//! external `define i64 @backtest_step(...)` (the author-chosen C
//! symbol from `(export "backtest_step")`) forwarding to the
//! internal `@ail_embed_backtest_step_step` (module
//! `embed_backtest_step` == file stem, per Design-decision 6).
//! Executable-target lowering is byte-unchanged (the
//! `missing_entry_main_is_error` in-source pin still holds).
use ailang_core::Workspace;
use std::path::PathBuf;
@@ -880,9 +932,11 @@ fn staticlib_emits_forwarder_no_main() {
assert!(!ir.contains("define i32 @main()"),
"staticlib IR must not emit the @main trampoline");
assert!(ir.contains("@backtest_step("),
"staticlib IR must emit the external @backtest_step entrypoint");
assert!(ir.contains("@ail_backtest_step("),
"the forwarder must call the internal @ail_backtest_step");
"staticlib IR must emit the external @backtest_step entrypoint \
(the author-chosen C symbol — invariant under module rename)");
assert!(ir.contains("@ail_embed_backtest_step_step("),
"the forwarder must call the internal \
@ail_embed_backtest_step_step (module embed_backtest_step)");
}
#[test]
@@ -1077,8 +1131,9 @@ fn llvm_scalar(t: &Type) -> &'static str {
Run: `cargo test -p ailang-codegen --test embed_staticlib_lowering`
Expected: PASS (2 passed): staticlib IR has no `@main`, has external
`@backtest_step`, forwards to `@ail_backtest_step`; exe target still
returns `MissingEntryMain`.
`@backtest_step` (C symbol), forwards to the internal
`@ail_embed_backtest_step_step`; exe target still returns
`MissingEntryMain`.
- [ ] **Step 7: Codegen in-source baseline pin still green**
@@ -1138,8 +1193,8 @@ fn staticlib_emit_produces_two_archives() {
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr));
assert!(outdir.join("libbacktest.a").exists(),
"expected libbacktest.a in {outdir:?}");
assert!(outdir.join("libembed_backtest_step.a").exists(),
"expected libembed_backtest_step.a in {outdir:?}");
assert!(outdir.join("libailang_rt.a").exists(),
"expected separate libailang_rt.a in {outdir:?}");
}
@@ -1328,8 +1383,9 @@ fn run_cmd(bin: &str, args: &[&str], ctx: &str) -> Result<()> {
- [ ] **Step 5: Run the CLI pin — verify GREEN**
Run: `cargo test -p ail --test embed_staticlib_cli`
Expected: PASS (2 passed): the headline produces `libbacktest.a` +
`libailang_rt.a`; the export-less module is rejected with the
Expected: PASS (2 passed): the headline produces
`libembed_backtest_step.a` + `libailang_rt.a`; the export-less
module is rejected with the
zero-export error.
- [ ] **Step 6: Default-exe path regression**
@@ -1373,8 +1429,8 @@ Create `crates/ail/tests/embed_e2e.rs`:
```rust
//! Embedding-ABI M1 coherent-stop proof (spec §"Testing strategy" 5
//! + Acceptance criteria): build `examples/embed_backtest_step.ail`
//! as a staticlib, link the C host against `libbacktest.a` +
//! `libailang_rt.a`, run it, assert the `s == 25` assertion holds
//! as a staticlib, link the C host against `libembed_backtest_step.a`
//! + `libailang_rt.a`, run it, assert the `s == 25` assertion holds
//! (exit 0). This is the whole-milestone existence proof.
use std::path::{Path, PathBuf};
@@ -1405,7 +1461,7 @@ fn c_host_calls_exported_scalar_kernel() {
let host_bin = outdir.join("host");
let cc = Command::new("cc")
.arg(&host_c)
.arg(outdir.join("libbacktest.a"))
.arg(outdir.join("libembed_backtest_step.a"))
.arg(outdir.join("libailang_rt.a"))
.arg("-o").arg(&host_bin)
.output().expect("cc host link");
@@ -1427,8 +1483,9 @@ Task 5's forwarder loop and the link is undefined-symbol). Then PASS.
- [ ] **Step 3: Run the E2E — verify GREEN**
Run: `cargo test -p ail --test embed_e2e`
Expected: PASS — `libbacktest.a` + `libailang_rt.a` link with the C
host; `backtest_step(0,3)=9`, `backtest_step(9,4)=25`; assertion
Expected: PASS — `libembed_backtest_step.a` + `libailang_rt.a` link
with the C host; `backtest_step(0,3)=9`, `backtest_step(9,4)=25`;
assertion
holds; exit 0. This is the coherent stop.
- [ ] **Step 4: DESIGN.md §"Embedding ABI (M1)" — current-state honesty**
@@ -1518,10 +1575,13 @@ plan-level gate.
`lower_workspace_staticlib_with_alloc` / `Target::StaticLib` /
`ExportNonScalarSignature` / `export-non-scalar-signature` /
`ExportHasEffects` / `export-has-effects` / `build_staticlib` /
`libbacktest.a` / `libailang_rt.a` / `backtest_step` — used
identically across every task that references them. Module
`backtest` ⇒ entry `backtest``lib backtest.a` = `libbacktest.a`
(spec-exact).
`libembed_backtest_step.a` / `libailang_rt.a` / `backtest_step`
(the C symbol) / `@ail_embed_backtest_step_step` (internal) —
used identically across every task that references them. Module
== file stem `embed_backtest_step` ⇒ entry `embed_backtest_step`
`lib embed_backtest_step.a` = `libembed_backtest_step.a`
(Design-decision 6; the C symbol `backtest_step` is decoupled
and invariant per spec Decision 2).
4. **Step granularity.** Every step is one action ≤5 min; the only
long one (Task 2 Step 5, the ~95-site thread) is a single
compile-driven mechanical loop with a hard terminating gate — it