fieldtest: raw-buf — 4 examples, 6 findings (refs #7)
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.
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
# 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+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<Tick>` — 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.
|
||||
Reference in New Issue
Block a user