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

32 KiB
Raw Blame History

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.rspub 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-13mod raw_buf; + pub use raw_buf::SOURCE as RAW_BUF_AIL;.
  • Modify: crates/ailang-surface/src/loader.rs:54-57, 123-143parse_raw_buf + injection.
  • Modify: crates/ailang-surface/src/lib.rs:39 — re-export parse_raw_buf.
  • Modify: crates/ailang-core/src/desugar.rs:926-935Term::NewTerm::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 checkok):

(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.
//! 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:

/// 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:

    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:

/// 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 45; 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):

            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):

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):

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):

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):

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:

                    // 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):

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):

(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):

(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_e2ebuild_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_e2eail check examples/raw_buf_reject_str.ail exits non-zero, stderr contains param-not-in-restricted-set.

  • raw_buf_no_leakbuild_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 checkok under the injected workspace). All other inlined bodies are Rust — caught by the implement compile gate.