# Fieldtest — raw-buf — 2026-05-30 **Status:** Draft — awaiting orchestrator triage **Author:** fieldtester (dispatched by fieldtest skill) ## Scope The raw-buf milestone (#7, range `a163c8c..f37bd95`) shipped `RawBuf a`, the first kernel-tier *base* extension: a mutable, indexed, fixed-size flat buffer of primitive elements (`a ∈ {Int, Float, Bool}`). New user-visible surface: the four type-scoped ops `(app RawBuf.new n)`, `(app RawBuf.get b i)`, `(app RawBuf.set b i v)`, `(app RawBuf.size b)` (`set` linear `own → own`; `get`/`size` `borrow`); the general `(new T )` construction sugar with element type recovered by inference-from-use; kernel-tier auto-import (bare `RawBuf`, no `(import …)`); the `param-in (a Int Float Bool)` element-type restriction rejected at `ail check`; and owned-RawBuf drop at scope close. I exercised the four axes from the carrier as a downstream consumer with only `ail check`/`build`/`run` and the design ledger (`design/models/0007`, `docs/specs/0054`). ## Examples ### examples/fieldtest/rawbuf_1_score_table.ail — score table via borrow-helpers - Fill a 5-slot Int buffer with `i*i+1` in a `loop`, then sum it back in a `total : borrow (RawBuf Int) -> Int` helper that drives off `RawBuf.size`. The LLM-natural decomposition: factor read-back into a `borrow`-receiver helper. - Fits axis 1 (mutable indexed storage) and exercises `size`, which the ledger's worked program (literal 0/1/2 indices) never does. - Outcome: **does not check.** `total` is rejected with `consume-while-borrowed` although it only calls `RawBuf.size`/`.get` on a `borrow` receiver. (Surfaces finding B1.) Authoring also hit two papercuts: a `loop` binder may not carry `own`, and the prelude comparison is `ge` not `gte`. ### examples/fieldtest/rawbuf_2_running_max.ail — fill-loop + running max - Populate a 4-slot Int buffer of length-N in a `loop`/`recur` (one `RawBuf.set` per iteration, threading the owned buffer through the loop binder `b`), then read it back for the running maximum. The natural way to fill a buffer whose length is not three literals. - Fits axis 1 + axis 4 (loop-threaded linear ownership). - Outcome: **checks `ok`, then fails at `build`** with `def main: unknown variable: b`. (Surfaces finding B2.) ### examples/fieldtest/rawbuf_3_sensor_pair.ail — Float + Bool buffers - Store three Float readings and one Bool alarm flag in two buffers, average the readings, double if the alarm is set. Drives `(new RawBuf (con Float) 3)` and `(new RawBuf (con Bool) 1)`; element type recovered purely from stored values (`1.5`, `true`). - Fits axis 2 (the `new` sugar across element types; Bool is the i1/i8 packing edge). - Outcome: **check + build + run clean; prints `5.0`** (expected). (Finding W1.) ### examples/fieldtest/rawbuf_4_paramin_reject.ail — user-TypeDef element reject - Model a `Tick` ADT and try to hold a batch in `RawBuf` — the obvious move for batch storage. Probes the user-TypeDef path (distinct from the shipped `Str` fixture). - Fits axis 3 (`param-in` restriction). - Outcome: **rejects at `ail check` (exit 1)** with `param-not-in-restricted-set` — the reject fires correctly, but the message omits the allowed set the ledger promises. (Findings W2 + B3.) ## Findings ### [bug] B1 — `borrow (RawBuf a)` receiver is unusable: a single get/size is rejected - Examples: `rawbuf_1_score_table.ail`; minimal repros below. - A function taking `borrow (RawBuf Int)` that calls `RawBuf.get` or `RawBuf.size` on the parameter even **once** is rejected: ``` [consume-while-borrowed] read_one: `buf` is consumed while a borrow of it is still live ``` Both ops take `borrow (RawBuf a)` per `design/models/0007` §RawBuf and `docs/specs/0054`, so borrowing the receiver to read it is exactly the advertised use. An inline owned `let`-bound buffer reads fine (the shipped fixtures), but the moment the buffer is behind a `borrow`-mode *parameter*, the read ops are misclassified as consuming. This makes the `borrow` signatures in the ledger unreachable from the natural decomposition (helper functions over a shared read-only buffer) — e.g. Series's own `Series.at`/`Series.total_count` (ledger §Series, which call `RawBuf.get` through a `borrow (Series a)`) would hit this. - Why a bug: the ledger advertises `get`/`size` as `borrow`-receiver ops; the checker rejects the documented use with a borrow-related diagnostic naming the wrong cause (`buf` is not consumed). - Repro: `ail check` the module `(fn read_one (type (fn-type (params (borrow (con RawBuf (con Int)))) (ret (con Int)))) (params buf) (body (app RawBuf.get buf 0)))` → `[consume-while-borrowed]`. - Recommended action: **debug** (RED test: a `borrow (RawBuf Int)` helper doing one `RawBuf.get` should check `ok`). ### [bug] B2 — owned RawBuf threaded through a `loop` binder passes check but fails codegen - Examples: `rawbuf_2_running_max.ail`; minimal repro below. - A `loop` binder holding an owned `RawBuf` value threaded through `recur` (`(loop (b (con RawBuf (con Int)) buf) (i …) … (recur (app RawBuf.set b i …) …))`) checks `ok` but fails at `ail build`: ``` Error: module `…`: def `main`: unknown variable: `b` ``` A plain `Int` loop binder named `b` builds and runs fine, so the fault is specific to threading an owned RawBuf through a loop binder. This breaks the check↔codegen agreement (the CLAUDE.md discipline that a program passing `check` is a program codegen can lower). - Why a bug: `check` accepts, codegen panics with `unknown variable`; the loop binder is not registered in the codegen scope for an owned RawBuf value. - Repro: `ail check` ok then `ail build` fail on `(let buf (new RawBuf (con Int) 3) (loop (b (con RawBuf (con Int)) buf) (i (con Int) 0) (if (app ge i 3) b (recur (app RawBuf.set b i 0) (app + i 1)))))`. - Recommended action: **debug** (RED test: build the fill-loop fixture → no `unknown variable`). ### [bug] B3 — `param-not-in-restricted-set` omits the allowed set the ledger promises - Example: `rawbuf_4_paramin_reject.ail` (and the shipped Str fixture). - Verbatim: ``` [param-not-in-restricted-set] first_price: type-arg `Tick` is not in the restricted set for type-variable `a` of `raw_buf.RawBuf` ``` `design/models/0007` §4 states: "`ParamNotInRestrictedSet` names the offending type **and the allowed set**." The shipped message names the offending type, the type-var, and the home type, but **not** the allowed `{Int, Float, Bool}`. A downstream author is told `Tick` is wrong but not what is right; self-correction requires reading the kernel source, which a downstream consumer cannot do. - Why a bug: diagnostic content is narrower than the ledger's stated diagnostic contract. (Borderline friction, but it is a concrete ledger-vs-implementation discrepancy → classified `bug`.) - Repro: `ail check examples/fieldtest/rawbuf_4_paramin_reject.ail`. - Recommended action: **debug** (small) — append the allowed set to the message; OR if the ledger over-promised, **tighten the ledger** (drop "and the allowed set"). The former is the author-useful fix. ### [spec_gap] B4 — `RawBuf.set` linearity of the receiver is not enforced (double-consume silently aliases) - Examples: `/tmp` probes `p_double.ail`, `p_double2.ail`. - An owned buffer consumed by two `RawBuf.set` calls (`(let a (app RawBuf.set buf 0 1) (let b (app RawBuf.set buf 1 2) …))`) checks `ok` and builds; `a` and `b` alias the same slab (reading `a[0]`+`b[1]` printed `3`). The ledger presents `set` as "linear: caller hands over the buffer, callee returns it" — implying the handed-over buffer can't be used again. The checker does not enforce that for `set`'s `own` receiver. **Not RawBuf-specific:** owned `Str` and owned ADT values double-consumed through a consuming fn also check `ok` (controls `p_str.ail`, `p_adt.ail`), so this is a pre-existing linearity-enforcement property, not a raw-buf regression. Recorded because RawBuf's `own → own` mutation surface is the first place a silent alias produces a *mutated-in-place* surprise rather than a benign re-read. - Why spec_gap: the ledger's "linear" framing for `set` and the actual non-enforcement of receiver single-use are two equally plausible readings; the milestone picked non-enforcement (consistent with the rest of the language) without the ledger saying so for RawBuf. - Recommended action: **ratify** the current behaviour in the ledger (state that mode annotations are not full linearity enforcement and double-consume of an owned RawBuf is the author's responsibility), OR open a backlog item if full linear enforcement is wanted. Either way a ledger note; not urgent. ### [working] W1 — `(new …)` sugar + inference-from-use across Int/Float/Bool - Example: `rawbuf_3_sensor_pair.ail` (Float + Bool); also every fixture uses `(new RawBuf (con Int) n)`. - `(new RawBuf (con Float) 3)` and `(new RawBuf (con Bool) 1)` both construct cleanly with no element-type annotation on any `RawBuf.set`/`.get`; the element type is recovered from the stored values (`1.5 : Float`, `true : Bool`). Built and ran first try, printing `5.0`. The i1/i8 Bool packing edge round-trips correctly (`true` stored, read back, discriminated). This is the milestone's cleanest win — the sugar reads exactly as the ledger's worked example promised and the inference-from-use story holds across all three element widths. - Recommended action: **carry-on**. ### [working] W2 — `param-in` reject fires for both Str and a user TypeDef - Example: `rawbuf_4_paramin_reject.ail`. - The `param-not-in-restricted-set` reject fires at `ail check` (exit 1) for a user-defined `Tick` ADT element, identically to the shipped `Str` fixture. The restriction is enforced on the real payload type, names the offending type, the type-var, and the home type `raw_buf.RawBuf`. (Its one shortfall — the missing allowed set — is B3; the *firing* itself is a clean win.) - Recommended action: **carry-on**. ## Recommendation summary | Finding | Class | Action | |---|---|---| | B1 borrow-receiver get/size rejected | bug | debug | | B2 owned-RawBuf loop binder fails codegen | bug | debug | | B3 param-in message omits allowed set | bug | debug (small) / or tighten ledger | | B4 set receiver double-consume not enforced | spec_gap | ratify (ledger note) | | W1 `new` sugar + inference across types | working | carry-on | | W2 param-in reject on Str + user TypeDef | working | carry-on | ### Orthogonal observations (not raw-buf findings, noted in passing) - `loop` binders reject an `own` mode annotation (`own may only appear inside fn-type params or ret`); combined with B2, there is currently no working way to thread an owned RawBuf through a loop at all. - Prelude comparison ops are `ge`/`le`/`gt`/`lt`/`ne` (not `gte`/`lte`); `ail builtins` does not list them (they live in the prelude), so an author guesses the spelling. Minor surface-discoverability friction, pre-existing, outside the raw-buf axes.