Files
AILang/docs/specs/0059-fieldtest-raw-buf-comprehensive.md
Brummel 1b2d23ec42 fieldtest: raw-buf comprehensive — 4 examples, 8 findings (refs #7)
Comprehensive usability re-test of the RawBuf kernel extension in
natural LLM-author decompositions the prior field test (0058) did not
exercise: a Float RawBuf in a user ADT read through two borrow helpers,
a Bool flag buffer filled with a computed value, an Int fib buffer whose
fill reads its own writes plus two composed borrow summaries, and a
forbidden core-primitive element. All three working fixtures build, run
to expected values, and report live=0 across Float/Bool/Int widths —
the core RawBuf surface (borrow-helper composition, fill loops,
RawBuf-in-ADT, element widths) genuinely holds together.

Orchestrator triage corrected the fieldtester's run, which executed a
stale target/release/ail (built before the #50 fix). Every outcome was
re-verified against a fresh build:

- F2 (bare RawBuf ADT field unresolved) — RETRACTED: a stale-binary
  false positive. On a fresh build the bare name resolves in every
  configuration (ctor name == or != type name; builds/runs live=0). The
  #50 fix is correct and general.
- F7 (NEW, issue #51) — a `check`-clean program crashes `build`: a
  buffer whose element type is never observed by a later get/set
  defaults its element to Unit in monomorphisation and hits an
  unregistered `RawBuf_new__Unit` intercept. Affects a legitimate
  `RawBuf<Int>` used only for its size AND a forbidden locally-
  constructed element. Root: the `(new T ...)` desugar drops the
  explicit element annotation; honouring it reverses the milestone's
  § Term::New desugar decision, so it routes through brainstorm.
- F8 (process) — the field test ran stale; the fieldtester agent now
  builds from the current tree before running (plugin fix).

F1 (spec_gap) resolved here: the design ledger's §Series substrate ctor
wrote the storage field `(own (RawBuf a))`, which does not parse (`own`
is fn-param/ret only). Rewritten to the verified-authorable `(con RawBuf
a)`. The rest of the §Series listing is illustrative and its element-
less `(new RawBuf lookback)` construction is entangled with #51; a full
compile-verification of that listing belongs with the Series-substrate
work.

F5 (param-in rejects a forbidden core primitive, message names the set)
and F4/F3 (borrow composition; substrate composite) carry on. F6
(param-in set rendered as a Rust-debug list) filed as #52.

refs #7
2026-05-30 18:29:00 +02:00

16 KiB

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.