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:
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user