Files
AILang/docs/plans/0110-raw-buf.4-rawbuf-payload-termnew-desugar.md
T
Brummel 9ff1f22d42 plan: raw-buf.4 RawBuf payload + Term::New desugar (refs #7)
Four tasks. Task 1: raw_buf manifest (source.ail + mod + lib re-export)
+ ailang-surface wiring (parse_raw_buf + injection) + round-trip +
workspace count 4->5 — checkable, no codegen (raw_buf not yet in the
bijection collector list, so its markers are uncollected, bijection
stays green). Task 2: the general Term::New -> (app T.new <values>)
desugar (runs before check, so the type-scoped callee flows through the
raw-buf.3 scope threading to RawBuf_new__T), drops the NewArg::Type
(element type inferred from use — no type-ascription term exists),
removes both codegen deferral arms; ratified by a (new StubT 42)
build-and-run. Task 3: the 12 scope-qualified RawBuf_op__T entries +
emit fns (alloc/gep/load/store over an @ailang_rc_alloc slab, Int emits
in full + Float/Bool by an exact element-type/width substitution table)
+ a flat intrinsic-storage drop + raw_buf added to the bijection
collector module list (marker<->entry lockstep closes here). Task 4:
the E2E (int -> 60, float, bool, param-in reject, drop leak).

plan-recon resolved both make-or-break risks favourably: (A) the
alloc/load/store emit has a clean mirror (emit_eq_str does
locals-by-position -> fresh_ssa -> gep -> call -> ret; @ailang_rc_alloc
returns the payload ptr with rc-header auto-prepended); (D) desugar runs
BEFORE check/synth/mono (prepare_workspace_for_check), so Term::New
becomes (app RawBuf.new ..) before synth's type-scoped resolution sets
scope=Some("RawBuf") -> reaches the raw-buf.3 RawBuf_new__Int mint. No
checker tweak needed.

Three orchestrator design calls (spec underspecified the codegen
detail): (1) drop discriminator = derived signal "the TypeDef's `new`
op is (intrinsic)-bodied" -> flat @ailang_rc_dec drop; correctly
separates RawBuf (intrinsic new) from StubT (real-body new), where a
param_in heuristic would misclassify StubT (also param_in). No schema
change, no hash-pin. (2) @ailang_rc_release does not exist -> the real
symbol is @ailang_rc_dec (spec slip corrected). (3) Bool = 1 byte per
spec; the 12 emits are per-element-specialised so each hardcodes its
width (Int/Float 8, Bool 1) + store type (i64/double/i1); the i1
store/load syntax is verify-first, raw_buf_bool_e2e is the catch.

Baseline after raw-buf.3 is 670; gates step 670->671 (round-trip)
->672 (StubT desugar) ->677 (5 RawBuf E2E). kernel_stub stays
(retirement is raw-buf.5).
2026-05-29 19:53:36 +02:00

522 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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__<T> 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__<T> 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__<T> 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__<T> 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__<T>` 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__<T>` 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 <types…> <values…>) desugars to
// (app T.new <values…>) — 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__<elem>`. 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<Term> = 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 <td.name> …)`) — 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_<m>_<T>`, 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 <named site>" 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.