# 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<Float>` 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<Bool>` 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<Int>, got raw_buf.RawBuf<Int>`.
- **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<String>` 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<Int>` 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`.
