diff --git a/docs/plans/0110-raw-buf.4-rawbuf-payload-termnew-desugar.md b/docs/plans/0110-raw-buf.4-rawbuf-payload-termnew-desugar.md new file mode 100644 index 0000000..e4c5a52 --- /dev/null +++ b/docs/plans/0110-raw-buf.4-rawbuf-payload-termnew-desugar.md @@ -0,0 +1,521 @@ +# raw-buf.4 — RawBuf payload + Term::New desugar — Implementation Plan + +> **Parent spec:** `docs/specs/0054-raw-buf.md` +> +> **For agentic workers:** REQUIRED SUB-SKILL: use the +> `implement` skill to run this plan. Steps use `- [ ]` +> checkboxes for tracking. + +**Goal:** Ship `RawBuf a` end-to-end — the `raw_buf` kernel-tier +module, its 12 scope-qualified codegen intercepts (alloc / load / +store / size over an `@ailang_rc_alloc` slab), a flat drop, and the +general `Term::New` → `(app T.new …)` desugar — so the worked +consumer program builds and prints `60`. + +**Architecture:** Four tasks. Task 1 lands the `raw_buf` manifest + +`ailang-surface` wiring (checkable, no codegen yet — raw_buf's +intrinsic markers are not yet in the bijection collector's module +list, so they are simply uncollected). Task 2 lands the general +`Term::New` desugar (runs *before* check, so the type-scoped +`(app T.new …)` it produces flows through the raw-buf.3 scope +threading) and removes the two codegen `Term::New` deferral arms. +Task 3 lands the 12 intercept entries + emit fns + the flat +intrinsic-storage drop, and adds raw_buf to the bijection collector +(marker↔entry lockstep closes here). Task 4 lands the E2E + drop +leak tests. The raw-buf.3 scope-qualified mono mechanism is already +in place — RawBuf.{op}@T monos to `RawBuf_op__T`; the 12 entries are +a mechanical application. + +**Tech Stack:** Rust. `crates/ailang-kernel/` (raw_buf submodule), +`crates/ailang-surface/` (parse + inject), `crates/ailang-core/src/desugar.rs` +(Term::New rewrite), `crates/ailang-codegen/src/{intercepts.rs,lib.rs,drop.rs}` +(entries + emit + drop + arm removal), `crates/ail/tests/` + `examples/` (E2E). + +--- + +**Files this plan creates or modifies:** + +- Create: `crates/ailang-kernel/src/raw_buf/mod.rs` — `pub const SOURCE: &str = include_str!("source.ail");`. +- Create: `crates/ailang-kernel/src/raw_buf/source.ail` — the gate-verified raw_buf Form-A module. +- Create: `examples/raw_buf_int.ail`, `examples/raw_buf_float.ail`, `examples/raw_buf_bool.ail`, `examples/raw_buf_reject_str.ail` — E2E + reject fixtures. +- Modify: `crates/ailang-kernel/src/lib.rs:11-13` — `mod raw_buf;` + `pub use raw_buf::SOURCE as RAW_BUF_AIL;`. +- Modify: `crates/ailang-surface/src/loader.rs:54-57, 123-143` — `parse_raw_buf` + injection. +- Modify: `crates/ailang-surface/src/lib.rs:39` — re-export `parse_raw_buf`. +- Modify: `crates/ailang-core/src/desugar.rs:926-935` — `Term::New` → `Term::App` rewrite. +- Modify: `crates/ailang-codegen/src/lib.rs:2094-2097, 3296-3299` — remove both `Term::New` deferral arms. +- Modify: `crates/ailang-codegen/src/lib.rs:1071-1090` — intrinsic-storage flat-drop guard. +- Modify: `crates/ailang-codegen/src/intercepts.rs` — 12 entries + emit fns + `parse_raw_buf()` in the bijection collector module list. +- Modify: `crates/ailang-core/tests/workspace_pin.rs:56-58` — count 4 → 5 + `contains_key("raw_buf")`. +- Modify: `crates/ail/tests/e2e.rs:870-879` — count 4 → 5 + raw_buf name. +- Modify: `crates/ailang-core/tests/design_schema_drift.rs:744+` — `raw_buf_module_round_trips`. +- Test: `crates/ail/tests/e2e.rs` (append) — `raw_buf_int_e2e`, `raw_buf_float_e2e`, `raw_buf_bool_e2e`, `raw_buf_param_in_reject_e2e`, `raw_buf_no_leak`. + +--- + +### Task 1: raw_buf manifest + ailang-surface wiring + round-trip + +**Files:** +- Create: `crates/ailang-kernel/src/raw_buf/{mod.rs, source.ail}` +- Modify: `crates/ailang-kernel/src/lib.rs`, `crates/ailang-surface/src/loader.rs`, `crates/ailang-surface/src/lib.rs` +- Modify: `crates/ailang-core/tests/workspace_pin.rs`, `crates/ail/tests/e2e.rs` (count), `crates/ailang-core/tests/design_schema_drift.rs` (round-trip) + +- [ ] **Step 1: Create `crates/ailang-kernel/src/raw_buf/source.ail`.** + +Write exactly (gate-verified this milestone — `ail check` → `ok`): + +```text +(module raw_buf + (kernel) + (data RawBuf (vars a) + (doc "Mutable, indexed, fixed-size flat buffer of primitive elements. Opaque single-ctor handle; codegen intercepts emit raw alloc/load/store over an @ailang_rc_alloc slab. Element type restricted to {Int, Float, Bool} via param-in.") + (ctor B a) + (param-in (a Int Float Bool))) + (fn new + (doc "Allocate an uninitialised RawBuf of capacity n. Compiler-supplied (intrinsic) body; codegen intercept RawBuf_new__ emits @ailang_rc_alloc plus the i64 size header. Element bytes are uninitialised; caller must set before get.") + (type (forall (vars a) (fn-type (params (con Int)) (ret (own (con RawBuf a)))))) + (params n) + (intrinsic)) + (fn get + (doc "Indexed read. UB if i >= (size b); caller checks bounds via size. Compiler-supplied (intrinsic) body; codegen intercept RawBuf_get__ emits getelementptr plus load.") + (type (forall (vars a) (fn-type (params (borrow (con RawBuf a)) (con Int)) (ret a)))) + (params b i) + (intrinsic)) + (fn set + (doc "Indexed write. Linear: own in, own out. Under uniqueness in-place; under shared copy-on-write (Issue #22). Compiler-supplied (intrinsic) body; codegen intercept RawBuf_set__ emits getelementptr plus store.") + (type (forall (vars a) (fn-type (params (own (con RawBuf a)) (con Int) a) (ret (own (con RawBuf a)))))) + (params b i v) + (intrinsic)) + (fn size + (doc "Element count. Compiler-supplied (intrinsic) body; codegen intercept RawBuf_size__ emits a single i64 load from the slab header.") + (type (forall (vars a) (fn-type (params (borrow (con RawBuf a))) (ret (con Int))))) + (params b) + (intrinsic))) +``` + +End the file with a single trailing newline. (`text`-fenced: `raw_buf` is injected as a reserved kernel-tier module, so `ail check` on it standalone returns `reserved-module-name`; the round-trip test in Step 7 ratifies it.) + +- [ ] **Step 2: Create `crates/ailang-kernel/src/raw_buf/mod.rs`.** + +```rust +//! Raw-buf submodule — the Form-A source of the `raw_buf` kernel-tier +//! base-extension module (RawBuf: a mutable indexed flat buffer of +//! primitive elements). Parsed by `ailang_surface::parse_raw_buf`, +//! round-trip-pinned by `raw_buf_module_round_trips`. + +/// Source-of-truth Form-A text for the `raw_buf` kernel module. +/// Declares the `RawBuf` TypeDef (`param-in (a Int Float Bool)`) and +/// the four `(intrinsic)` ops `new`/`get`/`set`/`size`; codegen +/// supplies their bodies via the `RawBuf_op__` intercept entries. +pub const SOURCE: &str = include_str!("source.ail"); +``` + +- [ ] **Step 3: Add the `raw_buf` submodule + re-export to the crate hub.** + +Edit `crates/ailang-kernel/src/lib.rs`. After `mod kernel_stub;` add `mod raw_buf;`; after `pub use kernel_stub::SOURCE as STUB_AIL;` add `pub use raw_buf::SOURCE as RAW_BUF_AIL;`. + +- [ ] **Step 4: Add `parse_raw_buf` in `crates/ailang-surface/src/loader.rs`.** + +Insert after `parse_kernel_stub` (~line 57), mirroring it verbatim with `raw_buf`/`RAW_BUF_AIL` substituted: + +```rust +/// raw-buf.4: parse the embedded raw_buf kernel-tier base-extension +/// module bytes into a `Module`. Mirror of [`parse_kernel_stub`]. +/// +/// Source-of-truth: `ailang_kernel::RAW_BUF_AIL`. Declares the +/// `RawBuf` TypeDef with `param-in (a Int Float Bool)` and the four +/// `(intrinsic)` ops; codegen supplies their bodies via the +/// `RawBuf_op__` intercept registry. +/// +/// Panics on parse failure — build-time-validated by every drift run. +pub fn parse_raw_buf() -> Module { + crate::parse(ailang_kernel::RAW_BUF_AIL) + .expect("ailang_kernel::RAW_BUF_AIL must parse as a Module") +} +``` + +- [ ] **Step 5: Inject `raw_buf` into the workspace.** + +In `load_workspace` (`loader.rs:123-132`, the block that reserves `kernel_stub` and inserts `parse_kernel_stub()`), add a symmetric block immediately after the kernel_stub insert: + +```rust + if modules.contains_key("raw_buf") { + return Err(WorkspaceLoadError::ReservedModuleName { + name: "raw_buf".to_string(), + }); + } + modules.insert("raw_buf".to_string(), parse_raw_buf()); +``` + +(The `kernel: true` filter at `loader.rs:139-143` auto-imports it — no other change. Confirm the exact `WorkspaceLoadError::ReservedModuleName` variant + the surrounding block shape against the kernel_stub block before writing.) + +- [ ] **Step 6: Re-export `parse_raw_buf`.** + +Edit `crates/ailang-surface/src/lib.rs:39`. Add `parse_raw_buf` to the `pub use loader::{…}` list (alongside `parse_kernel_stub`). + +- [ ] **Step 7: Add `raw_buf_module_round_trips`.** + +Edit `crates/ailang-core/tests/design_schema_drift.rs`. After `kernel_stub_module_round_trips` (~764), append a mirror for raw_buf: + +```rust +/// raw-buf.4: pin the JSON byte-shape of the raw_buf kernel-tier +/// base-extension module. Mirror of `kernel_stub_module_round_trips`. +#[test] +fn raw_buf_module_round_trips() { + let m = ailang_surface::parse_raw_buf(); + assert!(m.kernel, "raw_buf is kernel-tier"); + assert_eq!(m.name, "raw_buf"); + let json = serde_json::to_value(&m).expect("serialise raw_buf"); + let recovered: Module = + serde_json::from_value(json).expect("raw_buf round-trips through serde"); + assert!(recovered.kernel); + assert_eq!(recovered.name, "raw_buf"); + let td = recovered.defs.iter().find_map(|d| match d { + Def::Type(t) if t.name == "RawBuf" => Some(t), + _ => None, + }).expect("RawBuf TypeDef present"); + let allowed = td.param_in.get("a").expect("RawBuf.a restricted"); + assert!(allowed.contains("Int") && allowed.contains("Float") && allowed.contains("Bool")); +} +``` + +(Confirm the in-scope `Module`/`Def` imports + the recovered-binding shape against the existing kernel_stub test before editing.) + +- [ ] **Step 8: Bump the workspace-count pins.** + +`crates/ailang-core/tests/workspace_pin.rs:56-58`: change `assert_eq!(ws.modules.len(), 4)` → `5`; add `assert!(ws.modules.contains_key("raw_buf"));` after the `kernel_stub` assertion. Update the adjacent comment (`prelude`, `kernel_stub`, and `raw_buf`). +`crates/ail/tests/e2e.rs:870`: change the `modules.len()` assertion `4` → `5`; after the `kernel_stub` name assertion (~879) add a `raw_buf` name assertion mirroring it. + +- [ ] **Step 9: Build + targeted test.** + +Run: `cargo build --workspace 2>&1 | tail -8` +Expected: build succeeds (a dead-code warning on `RAW_BUF_AIL` is gone once `parse_raw_buf` consumes it; if Step 4-6 are complete there is none). + +Run: `cargo test -p ailang-core --test design_schema_drift raw_buf_module_round_trips && cargo test -p ailang-core --test workspace_pin` +Expected: PASS — raw_buf round-trips; workspace count is 5. + +- [ ] **Step 10: Full suite — manifest landed, no codegen yet.** + +Run: `cargo test --workspace 2>&1 | grep -E "^test result:" | awk -F"[: ;]+" '{p+=$4; f+=$6; i+=$8} END{print "passed:",p," failed:",f," ignored:",i}'` +Expected: `passed: 671 failed: 0 ignored: 2` (670 baseline + the new `raw_buf_module_round_trips`; the count-pin edits modify existing assertions, no count change). The bijection stays green: raw_buf is NOT yet in `workspace_intrinsic_markers`'s module list (Task 3), so its 4 intrinsic markers are uncollected — no orphan-marker failure. No consumer builds RawBuf ops yet. + +--- + +### Task 2: Term::New desugar + remove codegen deferral arms + +**Files:** +- Modify: `crates/ailang-core/src/desugar.rs:926-935` +- Modify: `crates/ailang-codegen/src/lib.rs:2094-2097, 3296-3299` +- Test: `crates/ail/tests/e2e.rs` (append `new_stubt_builds_and_runs`) + +- [ ] **Step 1: Rewrite the `Term::New` desugar arm.** + +Edit `crates/ailang-core/src/desugar.rs`. Replace the current pass-through `Term::New { type_name, args } => Term::New { … }` arm (~926-935) with a rewrite to a type-scoped application of the type's `new` op, dropping the `NewArg::Type` element type (recovered by inference from use, per spec § Term::New desugar): + +```rust + Term::New { type_name, args } => { + // raw-buf.4: (new T ) desugars to + // (app T.new ) — a type-scoped call so synth's + // dotted-name branch (lib.rs ~3458) resolves it through + // the TypeDef-first ladder and the raw-buf.3 scope + // threading mints `T_new__`. The leading + // NewArg::Type is dropped: the AST carries no type-args + // on App, and the element type is recovered by ordinary + // inference from how the result is used (spec § Term::New + // desugar). Runs before check, so no Term::New reaches + // the type-checker or codegen. + let value_args: Vec = args + .iter() + .filter_map(|arg| match arg { + NewArg::Value(v) => Some(self.desugar_term(v, scope)), + NewArg::Type(_) => None, + }) + .collect(); + Term::App { + callee: Box::new(Term::Var { name: format!("{type_name}.new") }), + args: value_args, + tail: false, + } + } +``` + +(Confirm `Term::App`'s exact fields — `callee`, `args`, and the third field's name/default (`tail`?) — and `Term::Var`'s `{ name }` shape against `crates/ailang-core/src/ast.rs` before writing; mirror the existing `Term::App` construction in this file.) + +- [ ] **Step 2: Remove the `lower_term` Term::New deferral arm.** + +Edit `crates/ailang-codegen/src/lib.rs`. Delete the `Term::New { .. } => Err(CodegenError::Internal( "Term::New requires the type's `new` def to be desugared first … milestone raw-buf".into()))` arm (~2094-2097). The match stays exhaustive — desugar eliminates `Term::New` before codegen, so the variant is unreachable here; if the compiler now demands the arm for exhaustiveness, replace it with `Term::New { .. } => unreachable!("Term::New is desugared to (app T.new …) before codegen — raw-buf.4")` rather than an `Err`. + +- [ ] **Step 3: Remove the synth-path Term::New deferral arm.** + +Same treatment at `crates/ailang-codegen/src/lib.rs:3296-3299` (the sibling arm in the synth/escape path). Delete, or replace with the same `unreachable!`. + +- [ ] **Step 4: Add `new_stubt_builds_and_runs` E2E (ratifies the desugar independent of RawBuf).** + +`kernel_stub`'s `new` has a real `(term-ctor StubT Stub x)` body, so `(new StubT 42)` desugars to `(app StubT.new 42)` and builds. Append to `crates/ail/tests/e2e.rs`, mirroring `answer_intrinsic_builds_and_runs_printing_42` (~93) and its `build_and_run` harness. Fixture (inline string or a new `examples/new_stubt_smoke.ail`): a `main : () -> Int` that does `(new StubT 42)`, pattern-matches out the `Int`, and prints it → `42`. (Confirm the StubT field-extraction shape; the goal is only that the `(new StubT …)` path builds + runs, ratifying the desugar + arm removal.) + +- [ ] **Step 5: Run the desugar ratifier + full suite.** + +Run: `cargo test -p ail new_stubt_builds_and_runs 2>&1 | grep -E "test result|FAILED"` +Expected: PASS — `(new StubT 42)` builds and runs (the deferral arm is gone; desugar handles it). + +Run: `cargo test --workspace 2>&1 | grep -E "^test result:" | awk -F"[: ;]+" '{p+=$4; f+=$6; i+=$8} END{print "passed:",p," failed:",f," ignored:",i}'` +Expected: `passed: 672 failed: 0 ignored: 2` (671 + `new_stubt_builds_and_runs`). All existing `(new StubT 42)` round-trip/parse tests stay green (desugar runs at check/codegen entry, not at parse — the round-trip surface is untouched). + +--- + +### Task 3: 12 RawBuf intercept entries + emit fns + flat drop + bijection collector + +**Files:** +- Modify: `crates/ailang-codegen/src/intercepts.rs` (12 entries + emit fns + collector module list) +- Modify: `crates/ailang-codegen/src/lib.rs:1071-1090` (intrinsic-storage flat-drop guard) + +- [ ] **Step 1: Add `parse_raw_buf()` to the bijection collector's module list.** + +Edit `crates/ailang-codegen/src/intercepts.rs`, `workspace_intrinsic_markers` (the `for module in [ parse_prelude(), parse_kernel_stub() ]` list, ~523). Add `ailang_surface::parse_raw_buf(),`. The raw-buf.3 type-scoped-poly arm + `scope_typedef_and_elems` then expand RawBuf's 4 ops over `{Int,Float,Bool}` into the 12 `RawBuf_op__T` markers automatically. (Until Step 2 adds the entries, this makes the bijection RED — Steps 1-2 land together.) + +- [ ] **Step 2: Append the 12 `INTERCEPTS` entries.** + +The mono symbols are `RawBuf_{new,get,set,size}__{Int,Float,Bool}` (raw-buf.3 scope-qualified mangling; the bijection test reports the exact strings if any differ). LLVM-type table — element `T` → store type / width / alloc-element-bytes: + +| T | llvm elem type | width (bytes) | +|---|---|---| +| Int | `i64` | 8 | +| Float | `double` | 8 | +| Bool | `i1` | 1 | + +`own (con RawBuf T)` and `borrow (con RawBuf T)` both lower to `ptr`. Entry sigs per op: + +| op | expected_params | expected_ret | +|---|---|---| +| `RawBuf_new__T` | `["i64"]` (capacity n) | `"ptr"` | +| `RawBuf_get__T` | `["ptr","i64"]` (buf, i) | elem type (`i64`/`double`/`i1`) | +| `RawBuf_set__T` | `["ptr","i64", elem]` (buf, i, v) | `"ptr"` | +| `RawBuf_size__T` | `["ptr"]` (buf) | `"i64"` | + +Add 12 `Intercept { name, expected_params, expected_ret, wants_alwaysinline: false, emit }` rows. (Verify each `expected_*` against what `llvm_type` delivers at the dispatch — the `eq__Unit` entry documents a past sig-mismatch from exactly this; confirm `llvm_type(Bool)` = `"i1"` per `synth.rs:18`.) + +- [ ] **Step 3: Write the emit fns (Int variants in full; Float/Bool by the substitution table).** + +Mirror `emit_eq_str` (`intercepts.rs:226`) for the Emitter API: param SSAs via `emitter.locals[n-k].1`, `emitter.fresh_ssa()`, `emitter.body.push_str(format!(…))`, terminal `ret` + `}\n\n` + `emitter.block_terminated = true`. Confirm the exact `@ailang_rc_alloc` call signature against an existing ADT-allocation call site (it returns the payload `ptr` with the rc-header auto-prepended). Slab layout: `[ size:i64 @ off 0 ][ elem_0 @ off 8 ][ elem_1 @ off 8+width ]…`. + +`RawBuf_new__Int` (capacity `n` is the one param): +```rust +pub(crate) fn emit_rawbuf_new_int(emitter: &mut Emitter<'_>) -> Result<()> { + let n = emitter.locals.len(); + let cap = emitter.locals[n - 1].1.clone(); // i64 capacity + let elems_bytes = emitter.fresh_ssa(); + let total = emitter.fresh_ssa(); + let slab = emitter.fresh_ssa(); + // 8 (header) + cap * 8 (Int elements) + emitter.body.push_str(&format!(" {elems_bytes} = mul i64 {cap}, 8\n")); + emitter.body.push_str(&format!(" {total} = add i64 {elems_bytes}, 8\n")); + emitter.body.push_str(&format!(" {slab} = call ptr @ailang_rc_alloc(i64 {total})\n")); + emitter.body.push_str(&format!(" store i64 {cap}, ptr {slab}\n")); // size header at off 0 + emitter.body.push_str(&format!(" ret ptr {slab}\n")); + emitter.body.push_str("}\n\n"); + emitter.block_terminated = true; + Ok(()) +} +``` + +`RawBuf_get__Int` (buf `b`, index `i`): +```rust +pub(crate) fn emit_rawbuf_get_int(emitter: &mut Emitter<'_>) -> Result<()> { + let n = emitter.locals.len(); + let b = emitter.locals[n - 2].1.clone(); + let i = emitter.locals[n - 1].1.clone(); + let off = emitter.fresh_ssa(); + let byteoff = emitter.fresh_ssa(); + let ptr = emitter.fresh_ssa(); + let v = emitter.fresh_ssa(); + emitter.body.push_str(&format!(" {off} = mul i64 {i}, 8\n")); // i * width + emitter.body.push_str(&format!(" {byteoff} = add i64 {off}, 8\n")); // + header + emitter.body.push_str(&format!(" {ptr} = getelementptr inbounds i8, ptr {b}, i64 {byteoff}\n")); + emitter.body.push_str(&format!(" {v} = load i64, ptr {ptr}\n")); + emitter.body.push_str(&format!(" ret i64 {v}\n")); + emitter.body.push_str("}\n\n"); + emitter.block_terminated = true; + Ok(()) +} +``` + +`RawBuf_set__Int` (buf `b`, index `i`, value `v`; linear — returns `b`): +```rust +pub(crate) fn emit_rawbuf_set_int(emitter: &mut Emitter<'_>) -> Result<()> { + let n = emitter.locals.len(); + let b = emitter.locals[n - 3].1.clone(); + let i = emitter.locals[n - 2].1.clone(); + let v = emitter.locals[n - 1].1.clone(); + let off = emitter.fresh_ssa(); + let byteoff = emitter.fresh_ssa(); + let ptr = emitter.fresh_ssa(); + emitter.body.push_str(&format!(" {off} = mul i64 {i}, 8\n")); + emitter.body.push_str(&format!(" {byteoff} = add i64 {off}, 8\n")); + emitter.body.push_str(&format!(" {ptr} = getelementptr inbounds i8, ptr {b}, i64 {byteoff}\n")); + emitter.body.push_str(&format!(" store i64 {v}, ptr {ptr}\n")); + emitter.body.push_str(&format!(" ret ptr {b}\n")); + emitter.body.push_str("}\n\n"); + emitter.block_terminated = true; + Ok(()) +} +``` + +`RawBuf_size__Int` (buf `b`): +```rust +pub(crate) fn emit_rawbuf_size_int(emitter: &mut Emitter<'_>) -> Result<()> { + let n = emitter.locals.len(); + let b = emitter.locals[n - 1].1.clone(); + let sz = emitter.fresh_ssa(); + emitter.body.push_str(&format!(" {sz} = load i64, ptr {b}\n")); + emitter.body.push_str(&format!(" ret i64 {sz}\n")); + emitter.body.push_str("}\n\n"); + emitter.block_terminated = true; + Ok(()) +} +``` + +**Float variants** (`emit_rawbuf_{new,get,set,size}_float`): identical to the Int variants except the element load/store type is `double` and `get`/`set` return/take `double`; element width stays **8** (so `new`'s `mul i64 {cap}, 8` and `get`/`set`'s `mul i64 {i}, 8` are unchanged). `get` ends `load double` + `ret double`; `set` does `store double {v}`. `new`/`size` are byte-identical to Int (header is always i64). + +**Bool variants** (`emit_rawbuf_{new,get,set,size}_bool`): element type `i1`, width **1**. `new`: `mul i64 {cap}, 1` (or just `add i64 {cap}, 8` — element bytes = cap*1). `get`/`set`: `mul i64 {i}, 1` for the element offset, then `load i1` / `store i1 {v}`. `get` ret `i1`; `set` ret `ptr`; `size` byte-identical to Int. (Verify-first: the `store i1 %v, ptr %p` / `load i1, ptr %p` syntax is valid LLVM — i1 occupies one byte in memory; `raw_buf_bool_e2e` is the catch.) + +- [ ] **Step 4: Add the intrinsic-storage flat-drop guard.** + +Edit `crates/ailang-codegen/src/lib.rs`, the drop-dispatch loop (`for def in &self.module.defs { if let Def::Type(td) = def { … } }`, ~1071). Before the `td.drop_iterative` split, add a guard: if `td` is intrinsic-storage — its module declares an `(intrinsic)`-bodied `new` op that constructs `td` (i.e. a `Def::Fn` named `new`, body `Term::Intrinsic`, whose return type mentions `(con …)`) — emit a flat drop and `continue` past the generic ADT drop: + +```rust + // raw-buf.4: a TypeDef whose construction is + // compiler-supplied (its `new` op is an (intrinsic), + // not a real term-ctor body) has a flat slab layout + // [size:i64][elements], NOT a tagged-ADT layout. + // The generic drop loads a tag at offset 0 and + // switches on it — but offset 0 here is the size, + // so the generic drop is wrong. Emit a flat drop: a + // single rc-dec on the slab pointer (primitive + // elements carry no recursive drops). Distinguishes + // RawBuf (intrinsic new) from StubT (real-body new → + // generic ADT drop). + if self.module.defs.iter().any(|d| matches!(d, + Def::Fn(f) if f.name == "new" + && matches!(f.body, Term::Intrinsic) + && fn_returns_type(f, &td.name))) + { + self.emit_flat_intrinsic_drop_fn(td); + continue; + } +``` + +Add `fn_returns_type` (walk the fn's return type for a `Type::Con { name == td_name }`, peeling `Forall`/`own`/`borrow` via `ParamMode` on `Type::Fn`) and `emit_flat_intrinsic_drop_fn` (mirror the generic drop's `join:` tail at `drop.rs:153-159`): + +```rust +fn emit_flat_intrinsic_drop_fn(&mut self, td: &TypeDef) { + let sym = format!("drop_{}_{}", self.module.name, td.name); + self.body.push_str(&format!("define void @{sym}(ptr %p) {{\n")); + self.body.push_str(" %isnull = icmp eq ptr %p, null\n"); + self.body.push_str(" br i1 %isnull, label %done, label %dec\n"); + self.body.push_str("dec:\n"); + self.body.push_str(" call void @ailang_rc_dec(ptr %p)\n"); + self.body.push_str(" br label %done\n"); + self.body.push_str("done:\n"); + self.body.push_str(" ret void\n"); + self.body.push_str("}\n\n"); +} +``` + +(Confirm the exact drop-fn symbol naming `drop__`, the null-guard/SSA conventions, and the `@ailang_rc_dec` call form against `emit_drop_fn_for_type` in `drop.rs:77-161` before writing — match its conventions exactly so the call sites that reference `drop_raw_buf_RawBuf` resolve. Also confirm whether a `partial_drop` helper is likewise expected for this type; if the dispatch loop unconditionally also emits `emit_partial_drop_fn_for_type`, decide whether the flat type needs one — mirror what the generic path guarantees its callers.) + +- [ ] **Step 5: Build + bijection + a manual IR check.** + +Run: `cargo build --workspace 2>&1 | tail -8` +Expected: build succeeds. + +Run: `cargo test -p ailang-codegen intercepts_bijection_with_intrinsic_markers 2>&1 | grep -E "test result|FAILED"` +Expected: PASS — the 4 RawBuf markers expand to the 12 `RawBuf_op__T` entries, all matched (1 op → 3 entries × 4 ops = 12). + +Run: `cargo test --workspace 2>&1 | grep -E "^test result:" | awk -F"[: ;]+" '{p+=$4; f+=$6; i+=$8} END{print "passed:",p," failed:",f," ignored:",i}'` +Expected: `passed: 672 failed: 0 ignored: 2` — no new test yet (entries + drop are exercised by Task 4's E2E); existing suite unaffected (no consumer instantiates RawBuf ops yet, so the emits/drop are not yet codegen'd; bijection green). + +--- + +### Task 4: RawBuf E2E + drop leak test + +**Files:** +- Create: `examples/raw_buf_int.ail`, `examples/raw_buf_float.ail`, `examples/raw_buf_bool.ail`, `examples/raw_buf_reject_str.ail` +- Test: `crates/ail/tests/e2e.rs` (append 5 tests) + +- [ ] **Step 1: Create `examples/raw_buf_int.ail`** (the worked program, prints 60): + +```text +(module raw_buf_int + (fn main + (type (fn-type (params) (ret (con Int)))) + (params) + (body + (let buf (new RawBuf (con Int) 3) + (let buf (app RawBuf.set buf 0 10) + (let buf (app RawBuf.set buf 1 20) + (let buf (app RawBuf.set buf 2 30) + (app + (app RawBuf.get buf 0) + (app + (app RawBuf.get buf 1) + (app RawBuf.get buf 2)))))))))) +``` + +(`text`-fenced: references the injected `raw_buf` module, so not gate-checkable standalone against the pre-iteration tree; the E2E in Step 5 is the ratifier. Confirm `main`'s expected printed value — the harness for an `Int`-returning main; mirror how `answer_intrinsic_builds_and_runs_printing_42` asserts `42`.) + +- [ ] **Step 2: Create `examples/raw_buf_float.ail`** — a Float variant storing e.g. `1.5`, `2.5` and summing to a known Float; and **`examples/raw_buf_bool.ail`** — a Bool variant setting/getting a Bool and returning an Int discriminated on it (so `main : () -> Int` prints a known value, exercising the `i1` store/load + width-1 offsets). (Confirm the Float-print + Bool-branch shapes against existing float / bool E2E fixtures.) + +- [ ] **Step 3: Create `examples/raw_buf_reject_str.ail`** (must-fail at check): + +```text +(module raw_buf_reject_str + (fn cant_str + (type (fn-type (params (borrow (con RawBuf (con Str)))) (ret (con Str)))) + (params buf) + (body (app RawBuf.get buf 0)))) +``` + +- [ ] **Step 4: Append the E2E tests to `crates/ail/tests/e2e.rs`.** + +Mirror `answer_intrinsic_builds_and_runs_printing_42` + its `build_and_run` harness for the run cases, and the param-in reject test shape for the check-fail case: + +- `raw_buf_int_e2e` — `build_and_run("raw_buf_int.ail")` → stdout `60`. +- `raw_buf_float_e2e` — Float variant → its known value. +- `raw_buf_bool_e2e` — Bool variant → its known value. +- `raw_buf_param_in_reject_e2e` — `ail check examples/raw_buf_reject_str.ail` exits non-zero, stderr contains `param-not-in-restricted-set`. +- `raw_buf_no_leak` — `build_and_run_with_rc_stats("raw_buf_int.ail")` (`e2e.rs:2154`), assert the RC live-count is 0 at exit (the slab is dropped via the flat intrinsic drop). (Confirm the rc-stats harness's exact assertion shape.) + +- [ ] **Step 5: Run the RawBuf E2E + full suite.** + +Run: `cargo test -p ail raw_buf 2>&1 | grep -E "test result|FAILED"` +Expected: all five PASS — `raw_buf_int` prints `60`; Float/Bool variants print their known values; the Str fixture is rejected at check with `param-not-in-restricted-set`; the leak check shows 0 live. + +Run: `cargo test --workspace 2>&1 | grep -E "^test result:" | awk -F"[: ;]+" '{p+=$4; f+=$6; i+=$8} END{print "passed:",p," failed:",f," ignored:",i}'` +Expected: `passed: 677 failed: 0 ignored: 2` — 672 + 5 new E2E (`raw_buf_int_e2e`, `raw_buf_float_e2e`, `raw_buf_bool_e2e`, `raw_buf_param_in_reject_e2e`, `raw_buf_no_leak`). The `.ll` snapshot tests stay green (no snapshot fixture uses RawBuf). + +--- + +## Self-review + +1. **Spec coverage.** + - § Architecture pt 4 (raw_buf submodule + ops): Task 1. + - § Architecture pt 4 (12 scope-qualified entries + emit): Task 3. + - § Architecture pt 4 (Term::New desugar + remove both arms): Task 2. + - § Architecture pt 4 (drop): Task 3 Step 4. + - § Architecture pt 4 (ailang-surface wiring, count 4→5): Task 1 Steps 4-8. + - § Concrete (raw_buf module / worked program / reject fixture / INTERCEPTS shape / desugar / slab): Tasks 1/4/3/2. + - § Data-flow (build-time + codegen-time): Tasks 1-3. + - § Testing raw-buf.4 bullets (round-trip, int/float/bool E2E, reject, drop leak, bijection, count): Tasks 1/3/4. + +2. **Placeholder scan.** No "TBD/TODO/implement later". The Float/Bool emit variants are given as an exact substitution of the in-full Int emits (element type + width table), not "similar to" — every one of the 12 is fully determined. "Mirror " instructions (parse_raw_buf, the E2E harness, the round-trip test) name the authoritative verbatim source with the exact substitution. + +3. **Type consistency.** Symbols: `RAW_BUF_AIL`, `parse_raw_buf`, `raw_buf` (module), `RawBuf` (type), `RawBuf_{new,get,set,size}__{Int,Float,Bool}` (12 entries == mono-minted == bijection-collected), `emit_rawbuf_*` (12 fns), `drop_raw_buf_RawBuf`, `@ailang_rc_alloc`, `@ailang_rc_dec`. Consistent across tasks. + +4. **Step granularity.** Each step is one file create/edit or one command. Task 3 is the largest (12 emits + drop), but the emits are one mechanical family and the drop is one guard + two helpers. + +5. **No commit steps.** None. + +6. **Pin/replacement substring contiguity.** Asserted substrings: `param-not-in-restricted-set` (runtime stderr, not a plan-shipped body), `60` (program output), the workspace counts (`5`, in the verbatim replacement assertions — contiguous). The bijection symbol strings are produced at runtime by `mono_symbol_n`, not a shipped body. No soft-wrap split. + +7. **Compile-gate vs. deferred-caller ordering.** Task 2 removes two codegen match arms — exhaustiveness is preserved (desugar eliminates Term::New before codegen; the `unreachable!` fallback is named if the compiler still demands an arm). Task 3's 12 entries + collector-list change land together (the bijection is RED between Step 1 and Step 2, both in Task 3, green by Step 5). No signature change defers a caller past a build gate; each task ends green. + +8. **Verification-command filter strings.** `raw_buf_module_round_trips`, `intercepts_bijection_with_intrinsic_markers`, `new_stubt_builds_and_runs`, `raw_buf` (matches the 5 new E2E) — all name tests this plan creates or that recon confirmed exist. The per-task full-suite gates use the unfiltered suite + an explicit count assertion (awk), so "0 ran" cannot pass as success. + +9. **Parse-the-bytes gate.** The surface-language bodies inlined (`raw_buf/source.ail`, the four `examples/raw_buf_*.ail` fixtures, the worked program) all reference the `raw_buf` kernel-tier module, which is reserved + only injected by this iteration's own Task 1 — so `ail check` cannot validate them standalone against the pre-iteration tree (`reserved-module-name` for the module source; `RawBuf` unresolved for the consumers). Gate N/A; they are ratified by the round-trip test (module) and the E2E (consumers) after Task 1/4 land. The raw_buf module Form-A was nonetheless gate-verified earlier this milestone (`ail check` → `ok` under the injected workspace). All other inlined bodies are Rust — caught by the `implement` compile gate.