iter embedding-abi-m3.1 (PARTIAL 5/7 + Boss spec-defect repair): single-ctor scalar record crosses the C ABI, ownership follows declared mode

Tasks 1-5 GREEN. T1 baseline pins (re-point annotation + @ailang_rc_alloc
heap-box byte-pin: size=8+n*8, tag@0, fields@8/16). T2 export gate widened
(is_c_scalar -> two-level is_c_abi_type: single-ctor all-Int/Float record;
multi-ctor/Str/List/nested still RED; gate suite 10/10; M1 adt-ret must-fail
re-pointed to multi-ctor+Str Reading). T3 codegen forwarder widened
(llvm_scalar record Type::Con -> ptr; M2 forwarder body byte-unchanged;
3/3 staticlib pins). T4/T5 E2E record round-trip own+borrow, global
leak-freedom.

Boss spec-consistency repair (M2.1-precedent class): orchestrator
correctly BLOCKED Task 5 on a genuine spec defect -- the single-ctx-readback
allocs==frees proof model is unsatisfiable for borrow (and only
coincidentally passes for own) because M2's TLS-ctx is bound only during
the synchronous forwarder call, so host-side decs land on g_rc_*, not ctx.
Boss-verified globally leak-free + value-correct both modes. Spec + plan +
harness amended to the stronger global model (sum all ailang_rc_stats:
lines; the M2-TLS cross-attribution documented as correct behaviour). No
fresh grounding-check (removes an over-strong measurement assumption).

Tasks 6 (DESIGN.md frozen-layout SSOT + lockstep pointers + freeze wording
+ enforceability demo) and 7 (workspace-green gate) re-dispatched on the
amended plan. Bench/architect milestone-close is audit-owned.

iter embedding-abi-m3.1 (PARTIAL); INDEX.md line deferred to the DONE commit
This commit is contained in:
2026-05-18 21:16:41 +02:00
parent 15ee3c5c8f
commit d5c565d48d
19 changed files with 893 additions and 51 deletions
+77 -32
View File
@@ -688,8 +688,22 @@ Build `libbacktest.a` + `libailang_rt.a` from
`embed_backtest_step_record.ail`, link a C host that constructs the
input `State` via `ailang_rc_alloc`, calls `backtest_step` N times
(kernel consumes each `own` input), frees the final return via
`ailang_rc_dec`, and asserts `ailang_ctx_free`'s `AILANG_RC_STATS`
readback shows `allocs == frees` and the value is correct.
`ailang_rc_dec`, and asserts **global leak-freedom** — Σallocs ==
Σfrees summed across *every* `ailang_rc_stats:` line the run emits
(the `ailang_ctx_free` ctx readback AND the `g_rc_*` atexit line) —
plus exit 0.
> **Boss spec-consistency repair (2026-05-18 — M3.1 BLOCKED
> adjudication, see the spec's repair note).** The original
> single-ctx-readback `allocs == frees` model is unsatisfiable for
> borrow (and only coincidentally passes for own): by M2's TLS-ctx
> design the host's `make_state`/`dec` run outside the forwarder
> call and land on `g_rc_*`, not ctx. The corrected, stronger
> invariant is **global**: the harness sums *all* `ailang_rc_stats:`
> lines; Σallocs == Σfrees (Σ`live` = 0) + exit 0, for both modes.
> Verified empirically: own ctx `live=0`/g_rc `live=0`; borrow ctx
> `live=+N`/g_rc `live=N` (Σ=0) — the split is correct M2-TLS
> behaviour, not a leak.
**Files:**
- Create: `crates/ail/tests/embed/record_roundtrip.c`
@@ -747,31 +761,50 @@ int main(void) {
Create `crates/ail/tests/embed_record_e2e.rs`. Open
`crates/ail/tests/embed_e2e.rs` and **mirror its exact** build/link/
run helper (the `ail build --emit=staticlib … -o`, the `ar`/`clang`
link of the C host against `lib<entry>.a` + `libailang_rt.a`, the
`AILANG_RC_STATS=1` env + stderr `allocs=… frees=…` parse). Add:
run incantation (the `ail build --emit=staticlib … -o`, the `cc`
link of the C host against `lib<module>.a` + `libailang_rt.a`, the
`AILANG_RC_STATS=1` env). **The stats parse must sum ALL
`ailang_rc_stats:` lines, not just the first** (Boss repair — the
run emits two: the ctx readback and the `g_rc_*` atexit line; global
leak-freedom is the invariant). The helper:
```rust
#[derive(Debug)]
struct RcStats { allocs: u64, frees: u64, exit_code: i32 }
fn build_link_run_embed(fixture: &str, host_rel: &str, extra_cc: &[&str]) -> RcStats {
// <build --emit=staticlib + cc link + run AILANG_RC_STATS=1,
// mirrored verbatim from embed_e2e.rs's real incantation>
// SUM every "ailang_rc_stats:" line (ctx readback + g_rc atexit):
let (mut allocs, mut frees) = (0u64, 0u64);
for l in stderr.lines().filter(|l| l.starts_with("ailang_rc_stats:")) {
for tok in l.split_whitespace() {
if let Some(v) = tok.strip_prefix("allocs=") { allocs += v.parse::<u64>().unwrap(); }
else if let Some(v) = tok.strip_prefix("frees=") { frees += v.parse::<u64>().unwrap(); }
}
}
RcStats { allocs, frees, exit_code: run.status.code().unwrap_or(-1) }
}
#[test]
fn record_roundtrip_own_alloc_eq_free() {
// build embed_backtest_step_record.ail --emit=staticlib,
// link crates/ail/tests/embed/record_roundtrip.c (no -DBORROW),
// run with AILANG_RC_STATS=1, parse the ctx_free readback.
let stats = build_link_run_embed(
"embed_backtest_step_record.ail",
"embed/record_roundtrip.c",
&[/* no extra clang defines */],
);
// GLOBAL leak-freedom: Σallocs == Σfrees across ctx + g_rc lines.
assert_eq!(stats.allocs, stats.frees,
"own: every State box freed exactly once (kernel-consumed input + host-freed return); {stats:?}");
"own: globally leak-free — every State box freed exactly once \
(kernel-consumed `own` inputs on ctx; host make_state/final \
dec on g_rc); {stats:?}");
assert_eq!(stats.exit_code, 0, "C host assert(n==1000000) held");
}
```
(The `build_link_run_embed` helper + `stats` struct shape are
whatever `embed_e2e.rs` already defines; reuse it — extract it to a
shared `mod` only if `embed_e2e.rs` does not already expose it. Do
not invent a new harness.)
(Mirror `embed_e2e.rs`'s real build/link/run; only the **multi-line
sum** is the Boss-repair delta over a single-line parse. Do not
invent a new harness; reuse `embed_e2e.rs`'s incantation verbatim.)
- [ ] **Step 3: Run the `own` E2E to verify RED**
@@ -790,43 +823,52 @@ ownership contract is mechanically almost free"; ratified by
`crates/ail/tests/e2e.rs::alloc_rc_own_param_dec_at_fn_return:1855`
— **not** `borrow_own_demo_modes_are_metadata_only`, which is an
Iter-18a pre-enforcement pin and must not be cited). No production
code change is expected in this task — if `record_roundtrip_own_alloc_eq_free`
is RED for a real imbalance, that is a genuine bug: hand off to
`debug` (RED test already exists). If it is GREEN immediately once
code change is expected in this task — if the **global**
Σallocs==Σfrees is RED for a real imbalance, that is a genuine bug:
hand off to `debug` (RED test already exists). If it is GREEN once
Tasks 2+3 are in the tree, that *is* the spec's "mechanically almost
free" claim substantiated.
Run: `cargo test -p ail --test embed_record_e2e record_roundtrip_own_alloc_eq_free -- --exact`
Expected: PASS — `allocs == frees`, exit 0.
Expected: PASS — Σallocs == Σfrees (own: ctx `live=0` + g_rc
`live=0`; the two stat lines summed), exit 0.
---
## Task 5: E2E record round-trip — `borrow`
Same harness, the `-DBORROW` arm: the kernel does **not** consume the
input; the host retains and `ailang_rc_dec`s it itself. `allocs ==
frees` again. Tasks 4+5 together prove "ownership follows the
declared mode" in *both* directions.
input; the host retains and `ailang_rc_dec`s it itself. **Global**
Σallocs == Σfrees again (Boss repair — *not* per-ctx-line: in borrow
the ctx line is `live=+N` and the g_rc line `live=N`, summing to 0;
the host's decs land on g_rc because they run outside the forwarder's
TLS-ctx window — correct M2 behaviour, not a leak). Tasks 4+5
together prove "ownership follows the declared mode" both directions,
globally leak-free.
**Files:**
- Modify: `crates/ail/tests/embed_record_e2e.rs` (add the borrow test)
- [ ] **Step 1: Write the failing `borrow` E2E test (RED)**
In `crates/ail/tests/embed_record_e2e.rs` add:
In `crates/ail/tests/embed_record_e2e.rs` add (same multi-line-sum
`build_link_run_embed` as Task 4 — global invariant):
```rust
#[test]
fn record_roundtrip_borrow_alloc_eq_free() {
// same C host with -DBORROW: host frees the retained input each
// iter; kernel (borrow param) does NOT consume it.
// -DBORROW: kernel (borrow param) does NOT consume the input;
// host frees the retained input each iter + the final return.
let stats = build_link_run_embed(
"embed_backtest_step_record_borrow.ail",
"embed/record_roundtrip.c",
&["-DBORROW"],
);
// GLOBAL: ctx allocs (kernel returns) + g_rc frees (host decs)
// sum to a balanced total — Σallocs == Σfrees, Σlive == 0.
assert_eq!(stats.allocs, stats.frees,
"borrow: kernel kept input; host freed every input + the return; {stats:?}");
"borrow: globally leak-free — kernel allocs returns on ctx, \
host frees inputs+return on g_rc; the two summed balance; {stats:?}");
assert_eq!(stats.exit_code, 0, "C host assert(n==1000000) held");
}
```
@@ -834,19 +876,22 @@ fn record_roundtrip_borrow_alloc_eq_free() {
- [ ] **Step 2: Run the `borrow` E2E to verify RED then GREEN**
Run: `cargo test -p ail --test embed_record_e2e record_roundtrip_borrow_alloc_eq_free -- --exact --nocapture`
Expected: FAIL first if the borrow fixture is not yet built/lowered;
then PASS once `embed_backtest_step_record_borrow.ail` builds via the
Task-2/3 path. A `borrow` param is **not** dropped by the kernel
(ratified by `crates/ail/tests/e2e.rs::alloc_rc_borrow_only_recursive_list_drop:1671`),
so the host's per-iter `ailang_rc_dec(st)` is exactly what balances
`allocs == frees`. A real imbalance ⇒ genuine bug ⇒ hand to `debug`.
Expected: FAIL first if the borrow fixture is not yet built/lowered
or the harness still parses only the first stat line; then PASS once
the multi-line-sum harness is in place and
`embed_backtest_step_record_borrow.ail` builds via the Task-2/3
path. A `borrow` param is **not** dropped by the kernel (ratified by
`crates/ail/tests/e2e.rs::alloc_rc_borrow_only_recursive_list_drop:1671`);
the host's per-iter `ailang_rc_dec(st)` on g_rc is what balances the
**global** total. A real *global* imbalance ⇒ genuine bug ⇒ `debug`.
- [ ] **Step 3: Both modes green together**
Run: `cargo test -p ail --test embed_record_e2e`
Expected: PASS, `2 passed` — `record_roundtrip_own_alloc_eq_free`
and `record_roundtrip_borrow_alloc_eq_free`. The frozen ownership
contract is **proven both ways**, not asserted.
and `record_roundtrip_borrow_alloc_eq_free`, both at global
Σallocs==Σfrees. The frozen ownership contract is **proven both
ways**, not asserted.
---