Downstream field test of the raw-buf surface as a consumer with only the public interface (design ledger + `ail` CLI; no crate source). Four fixtures under examples/fieldtest/, spec at docs/specs/0058-fieldtest-raw-buf.md. Every recorded outcome reproduced independently before commit. Findings (3 bug, 1 spec_gap, 2 working): - B1 [bug] `borrow (RawBuf a)` receiver is unusable. A fn taking `borrow (RawBuf Int)` that calls RawBuf.get/.size on the parameter even once is rejected `consume-while-borrowed` — the diagnostic names the wrong cause (the receiver is not consumed). The ledger advertises get/size as borrow-receiver ops, so the documented use is unreachable from any helper-function decomposition. This blocks Series (#8) — the milestone's own downstream raison d'être — whose at/total_count read through a `borrow (Series a)`. - B2 [bug] An owned RawBuf threaded through a `loop` binder passes check but panics codegen `unknown variable: b` at build. A plain Int loop binder builds fine, so the fault is specific to an owned RawBuf loop binder. Breaks the check↔codegen agreement. - B3 [bug] `param-not-in-restricted-set` omits the allowed set. design/models/0007 §4 promises the diagnostic "names the offending type and the allowed set"; the shipped message names the type, the type-var, and the home type but not `{Int, Float, Bool}`. A downstream author is told what is wrong but not what is right. - B4 [spec_gap] RawBuf.set receiver double-consume is not enforced; two set calls on the same owned buffer silently alias the slab. Not raw-buf-specific (owned Str/ADT behave identically) — a pre-existing linearity-non-enforcement property. Recorded because RawBuf's own→own mutation is the first place a silent alias yields a mutated-in-place surprise. Ratify in the ledger or open a backlog item for full linear enforcement. - W1 [working] `(new …)` sugar + inference-from-use across Int/Float/Bool builds and runs first try (sensor_pair prints 5.0); the Bool i1/i8 packing edge round-trips. The milestone's cleanest win. - W2 [working] param-in reject fires for both Str and a user TypeDef. Net: B1+B2 mean the only RawBuf programs that build today are straight-line owned let-threading with literal indices — the shipped-fixture shape. Helper decomposition (B1) and runtime-length loop fill (B2) both fail. fieldtest does not self-resolve; routing is the orchestrator's call.
11 KiB
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 <types…> <values…>) 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+1in aloop, then sum it back in atotal : borrow (RawBuf Int) -> Inthelper that drives offRawBuf.size. The LLM-natural decomposition: factor read-back into aborrow-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.
totalis rejected withconsume-while-borrowedalthough it only callsRawBuf.size/.geton aborrowreceiver. (Surfaces finding B1.) Authoring also hit two papercuts: aloopbinder may not carryown, and the prelude comparison isgenotgte.
examples/fieldtest/rawbuf_2_running_max.ail — fill-loop + running max
- Populate a 4-slot Int buffer of length-N in a
loop/recur(oneRawBuf.setper iteration, threading the owned buffer through the loop binderb), 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 atbuildwithdef 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
newsugar 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
TickADT and try to hold a batch inRawBuf<Tick>— the obvious move for batch storage. Probes the user-TypeDef path (distinct from the shippedStrfixture). - Fits axis 3 (
param-inrestriction). - Outcome: rejects at
ail check(exit 1) withparam-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 callsRawBuf.getorRawBuf.sizeon the parameter even once is rejected:Both ops take[consume-while-borrowed] read_one: `buf` is consumed while a borrow of it is still liveborrow (RawBuf a)perdesign/models/0007§RawBuf anddocs/specs/0054, so borrowing the receiver to read it is exactly the advertised use. An inline ownedlet-bound buffer reads fine (the shipped fixtures), but the moment the buffer is behind aborrow-mode parameter, the read ops are misclassified as consuming. This makes theborrowsignatures in the ledger unreachable from the natural decomposition (helper functions over a shared read-only buffer) — e.g. Series's ownSeries.at/Series.total_count(ledger §Series, which callRawBuf.getthrough aborrow (Series a)) would hit this. - Why a bug: the ledger advertises
get/sizeasborrow-receiver ops; the checker rejects the documented use with a borrow-related diagnostic naming the wrong cause (bufis not consumed). - Repro:
ail checkthe 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 oneRawBuf.getshould checkok).
[bug] B2 — owned RawBuf threaded through a loop binder passes check but fails codegen
- Examples:
rawbuf_2_running_max.ail; minimal repro below. - A
loopbinder holding an ownedRawBufvalue threaded throughrecur((loop (b (con RawBuf (con Int)) buf) (i …) … (recur (app RawBuf.set b i …) …))) checksokbut fails atail build:A plainError: module `…`: def `main`: unknown variable: `b`Intloop binder namedbbuilds 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 passingcheckis a program codegen can lower). - Why a bug:
checkaccepts, codegen panics withunknown variable; the loop binder is not registered in the codegen scope for an owned RawBuf value. - Repro:
ail checkok thenail buildfail 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: "ParamNotInRestrictedSetnames 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 toldTickis 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:
/tmpprobesp_double.ail,p_double2.ail. - An owned buffer consumed by two
RawBuf.setcalls ((let a (app RawBuf.set buf 0 1) (let b (app RawBuf.set buf 1 2) …))) checksokand builds;aandbalias the same slab (readinga[0]+b[1]printed3). The ledger presentssetas "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 forset'sownreceiver. Not RawBuf-specific: ownedStrand owned ADT values double-consumed through a consuming fn also checkok(controlsp_str.ail,p_adt.ail), so this is a pre-existing linearity-enforcement property, not a raw-buf regression. Recorded because RawBuf'sown → ownmutation 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
setand 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 anyRawBuf.set/.get; the element type is recovered from the stored values (1.5 : Float,true : Bool). Built and ran first try, printing5.0. The i1/i8 Bool packing edge round-trips correctly (truestored, 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-setreject fires atail check(exit 1) for a user-definedTickADT element, identically to the shippedStrfixture. The restriction is enforced on the real payload type, names the offending type, the type-var, and the home typeraw_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)
loopbinders reject anownmode 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(notgte/lte);ail builtinsdoes 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.