# Fieldtest — raw-buf (comprehensive re-test) — 2026-05-30 **Status:** Triaged — orchestrator re-verified every outcome against a fresh build. **Author:** fieldtester (dispatched by fieldtest skill); finding triage and corrections by the orchestrator. ## Verification note (orchestrator) The fieldtester ran against `target/release/ail`, which was **stale** — built at 17:22, before the #50 fix landed at 17:48. Every outcome below was therefore re-verified against a freshly built binary during triage. The re-verification changed one finding materially: **F2 does not reproduce on a fresh build and is retracted** (it was an artefact of the stale binary). It also surfaced a real defect class the stale run masked (F7, now issue #51) and a process gap (F8). All other outcomes (F1, F3, F4, F5, F6) reproduce on the fresh binary and stand. ## Scope A comprehensive usability re-test of the `RawBuf` kernel base extension and the kernel-extension surface as it ships at HEAD (`4ad003d..HEAD`), *after* this session's usability repairs (#46 borrow-receiver, #47 fill-loop build, #48 param-in message, #50 RawBuf-in-ADT bare name, B5 loop-result drop, #49 superseded-heap-binder dec). The brief was explicitly *not* to re-run the prior four fixtures (`docs/specs/0058-fieldtest-raw-buf.md`) but to push into natural decompositions an LLM author would reach for that the prior test did NOT exercise — RawBuf wrapped in a user ADT with bookkeeping (the "Series substrate"), Bool-width fills through a loop, two borrow helpers composed over one buffer, and a forbidden *core-primitive* element type. Element width was varied across examples (Float, Bool, Int). All work was done as a downstream consumer with only `ail check` / `build --alloc=rc` / `run`, `AILANG_RC_STATS=1`, and the public interface (`design/INDEX.md`, `design/models/0007-kernel-extensions.md`, the `examples/` corpus). The prior four rawbuf fixtures were also re-run as a regression spot-check — all still build, run to their expected values, and report `live=0`. ## Examples ### examples/fieldtest/rbx_1_float_window_stats.ail — Float window in a user ADT, two borrow helpers - A `Window` ADT wraps a `RawBuf` plus an Int `count`. `build_window` allocates a 4-slot Float buffer, fills it via a `loop`/`recur` (one `RawBuf.set` per slot, `int_to_float (2*(i+1))`), and wraps it in `W`. Two `borrow (Window)` helpers — `mean` and `wmax` — each `match` the ADT and read the buffer through `RawBuf.get` on a borrow. - Why it fits: the §Series substrate shape (RawBuf in a user ADT field, axes 1+2+3+4 combined) on the Float width, with the borrow-receiver read behind a match and composed across two helpers — none of which the prior test exercised together. - Outcome: **builds + runs + leak-clean** (`allocs=4 frees=4 live=0`, prints `5.0` / `8.0`). One surface adjustment was needed: the `own` mode cannot appear on the storage field (the ledger's `(own (RawBuf a))` substrate form does not parse — F1), so the field is written `(con raw_buf.RawBuf (con Float))`. The bare `(con RawBuf (con Float))` field **also** resolves on a fresh build (the #50 fix); the fixture keeps the qualified spelling but the bare name is no longer required (retracted F2). ### examples/fieldtest/rbx_2_bool_sieve.ail — Bool flag buffer, computed fill, borrow count - `mark_even` fills an N-slot `RawBuf` via a loop with a *computed* Bool (`eq (i % 2) 0`) per slot; `count_set` (a `borrow (RawBuf Bool)` helper) reads the flags back through `RawBuf.get` used directly as an `if` condition and accumulates an Int. - Why it fits: the i1/i8 Bool packing edge driven through a runtime-N fill loop with a *computed* (not literal `true`) value — the prior W1 only stored one literal Bool at a literal index — plus the #46 borrow-receiver fix on the Bool width. - Outcome: **builds + runs + leak-clean** (`allocs=2 frees=2 live=0`, prints `3`). Clean on first try. See finding F4. ### examples/fieldtest/rbx_3_unit_element_reject.ail — forbidden core-primitive element - `(new RawBuf (con Unit) n)` — a RawBuf of `Unit`, a *core primitive* not in `{Int, Float, Bool}`. - Why it fits: the prior test rejected `Str` (heap primitive) and `Tick` (user ADT); this covers the third class, a core primitive, and re-verifies the #48 message fix names the allowed set. - Outcome: **rejects at `ail check` (exit 1)** with `[param-not-in-restricted-set] … type-arg \`Unit\` … (allowed: one of ["Bool", "Float", "Int"])`. The B3/#48 fix is visible. See findings F5, F6. ### examples/fieldtest/rbx_4_int_two_borrow_helpers.ail — Int fib buffer, fill reads itself, two borrow summaries - `fill` (own→own, across a fn boundary) fills slot i with fib(i), reading prior slots via `RawBuf.get` on the *owned* loop binder `b`. `total` and `argmax` are two distinct `borrow (RawBuf Int)` helpers, called back-to-back on the same owned buffer in `main`; `argmax` threads a 3-binder loop (`best_i`, `best_v`, `i`). - Why it fits: composition of the #46 borrow fix (two borrows of one buffer in sequence, buffer still owned + dropped by main), plus a fill loop that reads its own in-place writes. - Outcome: **builds + runs + leak-clean** (`allocs=3 frees=3 live=0`, prints `20` / `5`). Clean on first try. See finding F4. ## Findings ### [spec_gap] F1 — the ledger's §Series substrate form `(ctor … (own (RawBuf a)) …)` does not parse - Examples: `rbx_1_float_window_stats.ail` (original draft); `/tmp` probes confirming generality. - `design/models/0007` §Series writes the storage field of the Series ADT as `(ctor S (own (RawBuf a)) (con Int) …)` (line 448). Authoring that verbatim is rejected at the surface: ``` error: [surface-parse-error] parse error in type: `own` may only appear inside fn-type params or ret ``` This is **general, not RawBuf-specific**: an `own`-mode field on any plain user ADT (`(data Outer (ctor O (own (con Inner))))`) is rejected identically. So the canonical Series-substrate example in the design ledger cannot be authored as written, and the ledger gives no in-interface signpost to the working form. - Why spec_gap: the parser rule ("`own` only in fn-type params/ret") and the ledger's §Series ctor-field syntax are two incompatible readings of what a substrate ADT looks like; the milestone shipped the parser rule but the ledger still documents the forbidden form. A downstream author copying the Series example hits a parse error with no ledger guidance. - Recommended action: **ratify / tighten the ledger** — done this cycle: the §Series ctor field is rewritten `(con RawBuf a)` (verified authorable; the mode lives on fn params, not ADT fields). `own`-mode ADT fields are not a planned feature, so no grammar work. Note: the rest of the §Series listing is an illustrative sketch and its construction `(new RawBuf lookback)` omits the element type — that shape is entangled with F7/#51, so a full compile-verification of the §Series listing belongs with the Series-substrate work, not here. ### [retracted] F2 — bare `(con RawBuf (con Int))` in an ADT field — FALSE POSITIVE (stale binary) - The fieldtester reported that a bare `(con RawBuf (con Int))` ADT field still failed to resolve despite the #50 fix, citing `[type-mismatch] expected RawBuf, got raw_buf.RawBuf`. - **Retracted on triage.** The finding was produced against a stale `target/release/ail` (built 17:22, before the #50 fix at 17:48). On a freshly built binary the bare-name field resolves in every configuration tested: - ctor name == type name (`(data Box (ctor Box (con RawBuf (con Int))))`) — checks ok. - ctor name != type name (`(data Box (ctor B (con RawBuf (con Int))))`, the fieldtester's exact repro) — checks ok. - `examples/raw_buf_adt_field_bare.ail` — checks/builds/runs → `42`, `live=0`. - Confirmed mechanism: the stale binary reproduces the error; the rebuilt binary does not. The #50 fix (`a92bcaf`) is correct and general. No code action; the real lesson is the process gap (F8). ### [working] F3 — the Series-substrate composite works end-to-end (with the qualified-name workaround) - Example: `rbx_1_float_window_stats.ail`. - Once the field is spelled `(con raw_buf.RawBuf (con Float))`, the full composite the §Series substrate describes works: a Float RawBuf filled via loop, wrapped in a `Window` ADT with an Int `count`, read back through TWO `borrow (Window)` helpers that each `match` the ADT and drive a read loop off the bookkeeping count via `RawBuf.get` on a borrow. Builds, prints `5.0`/`8.0`, and is leak-clean (`allocs=4 frees=4 live=0`) — the Window ADT's drop cascade frees the buffer slab. This is the substrate the pending Series milestone needs, and (modulo F1/F2 surface friction) it holds together. - Recommended action: **carry-on** (and use this as the Series-milestone smoke fixture). ### [working] F4 — borrow-receiver helpers compose; Bool/Int fills + reads are correct across widths - Examples: `rbx_2_bool_sieve.ail`, `rbx_4_int_two_borrow_helpers.ail`. - The #46 borrow-receiver fix holds under composition: `rbx_4` borrows one owned Int buffer twice (`total` then `argmax`), each helper doing multiple `RawBuf.get`/`.size` calls on its borrow param, with the buffer still owned and dropped by main afterwards — clean, `live=0`, prints `20`/`5`. `rbx_2` drives the i1/i8 Bool edge through a runtime-N fill loop storing a *computed* Bool (`eq (i%2) 0`) and reads it back as an `if` condition in a borrow helper — correct (`3`), `live=0`. A fill loop that reads its own in-place writes (`rbx_4` fib: `RawBuf.get b (i-1)` inside the same loop that wrote slot i-1) works — the in-place `own→own` mutation makes prior slots visible to later iterations. - These are the milestone's load-bearing wins for the downstream Series decomposition; all clean on first or second try with no surprises. - Recommended action: **carry-on**. ### [working] F5 — `param-in` rejects a forbidden core primitive (`Unit`), message now names the allowed set - Example: `rbx_3_unit_element_reject.ail`. - `(new RawBuf (con Unit) n)` is rejected at `ail check` (exit 1): ``` [param-not-in-restricted-set] make_presence: type-arg `Unit` is not in the restricted set for type-variable `a` of `raw_buf.RawBuf` (allowed: one of ["Bool", "Float", "Int"]) ``` This extends the prior W2 (Str, user ADT) to the third class — a *core* primitive — and empirically confirms the #48/B3 fix: the allowed set now travels in the human-readable message. The restriction is on the real element type regardless of its tier (core / heap / user ADT). - **Caveat (re-verified):** this reject fires because the forbidden element appears in a written fn-type position (`make_presence`'s `ret` type). The restriction is NOT enforced at the bare construction site — see F7. So F5 is "works *when the type is written in a signature*", which the fixture happens to do. - Recommended action: **carry-on** (the signature-position reject is correct; the construction-site gap is F7 / issue #51). ### [friction] F6 — `param-in` message renders the allowed set as a Rust-debug list, not the ledger's `{Int, Float, Bool}` - Example: `rbx_3_unit_element_reject.ail` (and the re-run `rawbuf_4_paramin_reject.ail`). - The message renders `(allowed: one of ["Bool", "Float", "Int"])` — a Rust `Vec` Debug rendering (square brackets, double-quoted, alphabetised). The ledger (`design/models/0007` §4) and the prior B3 framing write the set as `{Int, Float, Bool}`. The information is now present (that was the #48 win), but the surface form is a leaked internal representation: quotes and brackets an LLM author would not expect, and the alphabetical order obscures the natural Int/Float/Bool grouping. - Why friction: the task (self-correct from the diagnostic) completes, but the rendering is verbose/leaky vs. the clean set notation the ledger implies. - Recommended action: **plan** (tidy) — render the set as `{Int, Float, Bool}` (brace-set, no quotes) to match the ledger. Filed as issue #52. ### [bug] F7 — `(new T …)` drops the explicit element annotation; an unobserved element type crashes codegen (issue #51) - Surfaced during triage of F2/F5 against the fresh binary, not in the fieldtester's original run (the stale binary masked it). - A buffer whose element type is never *observed* by a later `get`/`set` passes `ail check` and then crashes `ail build`: ``` (let b (new RawBuf (con Int) 3) (app print (app RawBuf.size b))) ; ail check -> ok ; ail build --alloc=rc -> Error: module raw_buf: def RawBuf_new__Unit: ; internal: intrinsic fn RawBuf_new__Unit has no registered intercept ``` This hits two shapes: a **legitimate** allowed element used only for its size (`RawBuf` above — the author wrote `(con Int)`, build still crashes), and a **forbidden** element constructed locally (`(new RawBuf (con Unit) 3)` — not rejected at check, same crash). - Root: the `(new T …)` desugar drops `NewArg::Type` (`crates/ailang-core/src/desugar.rs:1053`); `RawBuf.new`'s `a` appears only in its result, so the call-site args never pin it; an unpinned forall var defaults to `Unit` in monomorphisation (`crates/ailang-codegen/src/subst.rs:56`), and `RawBuf_new__Unit` has no registered intercept. Verified end-to-end. - Why bug: a `check`-clean program crashes `build` (the exact class the raw-buf usability mandate set out to eliminate), and the author's explicit type annotation is silently discarded — contrary to the language identity (mandatory annotations; every definition carries its full type). - Recommended action: **brainstorm** — honouring the annotation reverses the milestone's "recover element by inference" decision (§ Term::New desugar) and there is no type-ascription term in the AST today, so this is a spec-level decision, not a one-line patch. Full analysis and candidate directions in issue #51. ### [process] F8 — the field test ran against a stale release binary - The fieldtester executed `target/release/ail` without rebuilding it, so its entire run reflected pre-#50 code. This produced one false positive (F2) and masked a real defect class (F7) until the orchestrator re-verified against a fresh build. - Why it matters: a field test that runs stale silently inverts its own purpose — it reports the *previous* shipped state as current. Every "works"/"fails" line is suspect unless the binary under test is known to include the changes the test claims to exercise. - Recommended action: **plan** (process) — the field-test harness must build the binary from the current tree before running (a `cargo build --release -p ail` step, or run through `cargo run`), so the consumer always exercises HEAD. Tracked as a skill/profile fix to the fieldtester dispatch. ## Recommendation summary | Finding | Class | Action | |---|---|---| | F1 §Series `own`-field form unparsable | spec_gap | ratify / tighten ledger (done this cycle) | | F2 bare RawBuf ADT field unresolved despite #50 | retracted | none — stale-binary false positive | | F3 Series-substrate composite works (qualified) | working | carry-on | | F4 borrow helpers compose; Bool/Int fills correct | working | carry-on | | F5 param-in rejects core primitive; names set | working | carry-on (signature-position only; cf. F7) | | F6 param-in set rendered as Rust-debug list | friction | plan (tidy) — issue #52 | | F7 `(new T …)` drops element annotation; unobserved element crashes codegen | bug | brainstorm — issue #51 | | F8 field test ran against a stale release binary | process | plan (harness fix) | ### Orthogonal observations (not raw-buf findings, noted in passing) - Float comparison in a RawBuf read loop needs `float_gt` (not `gt`); the `no-instance` diagnostic is exemplary — it names the missing instance, the design rationale (IEEE-754 partial order), the exact replacement ops (`float_gt` and siblings), and the contract ref. A model diagnostic; no action. - The known #49 Str-accumulator-loop leak was not hit — every fill loop here uses `RawBuf.set` (in-place), so no superseded-heap-binder dec is needed and all four new fixtures report `live=0`.