fieldtest: raw-buf milestone — 4 examples, 3 findings (refs #7)

Milestone-close fieldtest for the raw-buf milestone, milestone-scope
variant: four end-to-end scenarios derived top-down from the milestone
promise (construct via (new RawBuf <elem> <size>), fill/read via
set/get, size-only buffers, element variety across {Int,Float,Bool},
forbidden-element reject at check, buffer threading under own/borrow).

Result confirms the milestone-promise hypothesis — RawBuf is clean
end to end, zero bugs:
- mrb_1 Int fill/read loop: check ok, run + native build both print 70
- mrb_2 Float own->own threading + borrow read: prints 3.5
- mrb_3 size-only Bool buffer (#51 legitimate-but-unobserved case): prints 8
- mrb_4 Unit element: rejected at check with a precise
  param-not-in-restricted-set diagnostic; build stops at the same
  error, codegen never reached (the #51 fix)

Two [working] findings (carry-on) and one [spec_gap]: the
kernel-extensions whitepaper (design/models/0007) teaches bare-mode
fn-type slots that the post-#55 parser rejects. Verified and triaged
separately; the ledger fix follows in its own commit.
This commit is contained in:
2026-06-02 10:46:16 +02:00
parent 2ca483dbfd
commit 9545257333
5 changed files with 210 additions and 0 deletions
@@ -0,0 +1,132 @@
# Fieldtest — raw-buf milestone (milestone-7) — 2026-06-02
**Status:** Draft — awaiting orchestrator triage
**Author:** fieldtester (dispatched by fieldtest skill, milestone-scope variant)
## Scope
The raw-buf milestone (#7) ships `RawBuf T` — AILang's first
kernel-tier *base* extension: a content-addressed, mode-tracked,
fixed-size flat buffer of primitive elements. The milestone promises
that an LLM author can write realistic RawBuf-centric programs and
hit no surprises: construct via `(new RawBuf <elem> <size>)`, fill
and read via `RawBuf.set` / `RawBuf.get`, use size-only buffers via
`RawBuf.size`, vary the element type across `{Int, Float, Bool}`,
have a forbidden element type (e.g. `Unit`) rejected at `check`
rather than crash at codegen (the #51 fix), and thread an owned
buffer through functions under the uniqueness/mode discipline.
This is the milestone-close fieldtest. Four end-to-end scenarios
were derived top-down from the milestone promise — each a realistic
downstream task — rather than re-running the per-cycle axes the
earlier per-iteration fieldtests (specs 0058, 0059) already covered.
Binary exercised: `cargo run -q -p ail` against the current working
tree (dev profile, freshly built from HEAD). Full native build of
scenario 1 additionally went through `ail build -o … + clang`.
## Examples
### examples/fieldtest/mrb_1_histogram.ail — Int fill/read loop with computed result
- Allocates a 5-slot `Int` RawBuf, fills it via an `own → own`
recursive `fill` helper (slots 0,7,14,21,28), then sums it back
through a `borrow` reader and prints the total.
- Fits the "buffer fill/read loop" + "threading through a function
under uniqueness/mode" axes: `fill` consumes and returns the owned
buffer per `RawBuf.set`; `sum` reads it through a borrow.
- Outcome: `check` ok (exit 0); `run` printed `70` (expected 70);
full `build` to native binary printed `70`. Matched.
### examples/fieldtest/mrb_2_float_smooth.ail — Float element variety + threading
- Allocates a 2-slot `Float` RawBuf, stores 3.0 and 4.0 via an
`own → own` `store_pair` helper, reads both slots through a
`borrow` `average` helper, prints the mean.
- Fits the "element-type variety (Float)" + "threading a buffer
through a function" axes; the buffer crosses two function
boundaries under own/borrow.
- Outcome: `check` ok; `run` printed `3.5` (expected 3.5). Matched.
### examples/fieldtest/mrb_3_bool_capacity.ail — size-only Bool buffer (#51 legitimate case)
- Allocates an 8-slot `Bool` RawBuf and prints its capacity via
`RawBuf.size` only — never `get`/`set`.
- Fits the "size-only buffer" + "element-type variety (Bool)" axes:
an explicit `Bool` element type is named at construction but only
`.size` is ever observed — the #51 legitimate-but-unobserved case.
- Outcome: `check` ok; `run` printed `8` (expected 8). Matched.
### examples/fieldtest/mrb_4_unit_element_reject.ail — forbidden element type (Unit)
- `main` names `Unit` as the RawBuf element type, then only sizes
the buffer and prints the size.
- Fits the "forbidden element type — must be rejected at check, not
crash at build" axis (the #51 fix).
- Outcome: `check` rejected at exit 1 with `param-not-in-restricted-set`
naming `Unit`; `build` also stopped at the same check error,
codegen never reached, no crash. Matched the promise exactly.
## Findings
### [working] RawBuf end-to-end is clean across all three permitted element types
- Examples: mrb_1 (Int), mrb_2 (Float), mrb_3 (Bool), plus the
build-to-native path on mrb_1.
- What happened: construct / set / get / size all behaved correctly;
computed results were exact (`70`, `3.5`, `8`); `check` reported
`ok (N symbols across 3 modules)`; the native binary produced the
same output as `run`. The `own → own` threading of a buffer
through `fill` / `store_pair` and the `borrow` reads in `sum` /
`average` / `capacity` all checked clean with no spurious
use-after-consume or clone friction — the author never had to
reach for a `clone` or an awkward re-bind beyond the natural
`(let buf (app … buf …) …)` linear-threading idiom shown in the
example corpus.
- Why working: the new surface was reached for naturally and
produced correct results on the first try once the fixtures were
in canonical form. This is the central milestone-promise win.
- Recommended action: carry-on.
### [working] Forbidden element type is rejected at check with a precise diagnostic (the #51 fix)
- Example: mrb_4.
- What happened, verbatim:
`error: [param-not-in-restricted-set] main: type-arg `Unit` is not in the restricted set for type-variable `a` of `raw_buf.RawBuf` (allowed: one of {Bool, Float, Int})`
The `--json` form carries a structured `ctx`
(`{"type":"raw_buf.RawBuf","var":"a","found":"Unit","allowed":["Bool","Float","Int"]}`).
`ail build` stops at the same error — codegen is never reached, no
crash, no `RawBuf_new__Unit`-style unregistered-intercept failure.
- Why working: this is precisely the #51 promise — reject at `check`,
not crash at codegen. The diagnostic names the offending type, the
type-variable, the type, and the allowed set; it is actionable.
- Recommended action: carry-on.
### [spec_gap] The kernel-extensions whitepaper shows bare-mode value params that the parser now rejects
- Surfaced while drafting mrb_1 / mrb_2 (first run before fix).
- What happened: I wrote scalar value params without a mode —
`(params (own (con RawBuf (con Int))) (con Int) (con Int) (con Int))`
— copying the shape shown in the public whitepaper
`design/models/0007-kernel-extensions.md` (the `sum_window_step` /
`emit_if_full` / `Series.at` signatures at lines 60-69, 84-86 use
bare `(con Int)` / `(con Float)` slots). The parser rejected it:
`error: [surface-parse-error] parse error in fn-type-slot: fn-type slot requires a mode: write (own T) or (borrow T) at byte 310`.
Adding `(own …)` to every value slot fixed it and the examples
then checked and ran clean.
- Why spec_gap: the parser is internally consistent — *every*
fn-type slot requires a mode, and that is a defensible contract
(it is the `bare_slot_reject` behaviour). But the whitepaper, which
is part of the public interface an LLM author reads to learn the
surface, still displays the bare-mode form throughout its worked
examples. An author who pattern-matches the whitepaper writes
programs that fail to parse. Two readings were plausible (value
params get an implied mode vs. every slot must be explicit); the
shipped reading is "every slot explicit", but the whitepaper
teaches the other.
- Recommended action: tighten the design ledger — update the
`design/models/0007-kernel-extensions.md` worked examples to put
explicit `(own …)` / `(borrow …)` on every value param slot so the
public surface matches the shipped parser contract. (Whitepaper
drift, not a code bug.)
## Recommendation summary
| Finding | Class | Action |
|---|---|---|
| RawBuf end-to-end clean across Int/Float/Bool | working | carry-on |
| Unit element rejected at check (#51 fix) | working | carry-on |
| Whitepaper shows bare-mode value params parser rejects | spec_gap | tighten the design ledger (models/0007) |
+29
View File
@@ -0,0 +1,29 @@
(module mrb_1_histogram
(fn fill
(doc "Fill an Int RawBuf at indices i..n with the running value v, stepping v by 7 each slot. own->own threading: the buffer is consumed and returned per RawBuf.set, so the recursion re-binds buf each step.")
(type (fn-type
(params (own (con RawBuf (con Int))) (own (con Int)) (own (con Int)) (own (con Int)))
(ret (own (con RawBuf (con Int))))))
(params buf i n v)
(body
(if (app eq i n)
buf
(tail-app fill (app RawBuf.set buf i v) (app + i 1) n (app + v 7)))))
(fn sum
(doc "Read back indices i..n of a borrow-mode RawBuf and total them. Reader: receiver is borrowed, never consumed.")
(type (fn-type
(params (borrow (con RawBuf (con Int))) (own (con Int)) (own (con Int)) (own (con Int)))
(ret (own (con Int)))))
(params buf i n acc)
(body
(if (app eq i n)
acc
(tail-app sum buf (app + i 1) n (app + acc (app RawBuf.get buf i))))))
(fn main
(doc "Allocate a 5-slot Int RawBuf, fill it (0,7,14,21,28), then sum it back and print 70.")
(type (fn-type (params) (ret (own (con Unit))) (effects IO)))
(params)
(body
(let buf (new RawBuf (con Int) 5)
(let buf (app fill buf 0 5 0)
(app print (app sum buf 0 5 0)))))))
+26
View File
@@ -0,0 +1,26 @@
(module mrb_2_float_smooth
(fn store_pair
(doc "Thread a Float RawBuf through an own->own helper that writes two slots and hands the buffer back. Tests threading a buffer through a function under the uniqueness/mode discipline: caller gives up own, callee returns own.")
(type (fn-type
(params (own (con RawBuf (con Float))) (own (con Float)) (own (con Float)))
(ret (own (con RawBuf (con Float))))))
(params buf a b)
(body
(let buf (app RawBuf.set buf 0 a)
(app RawBuf.set buf 1 b))))
(fn average
(doc "Read both slots through a borrow and return their mean. Float element variety: double load + float arithmetic.")
(type (fn-type
(params (borrow (con RawBuf (con Float))))
(ret (own (con Float)))))
(params buf)
(body
(app / (app + (app RawBuf.get buf 0) (app RawBuf.get buf 1)) 2.0)))
(fn main
(doc "Allocate a 2-slot Float RawBuf, store 3.0 and 4.0 via the own->own helper, then print their average (3.5).")
(type (fn-type (params) (ret (own (con Unit))) (effects IO)))
(params)
(body
(let buf (new RawBuf (con Float) 2)
(let buf (app store_pair buf 3.0 4.0)
(app print (app average buf)))))))
@@ -0,0 +1,15 @@
(module mrb_3_bool_capacity
(fn capacity
(doc "Read the size of a borrow-mode Bool RawBuf without ever touching get/set. The #51 legitimate-but-unobserved case: an explicit Bool element type is named at construction, but only .size is ever used.")
(type (fn-type
(params (borrow (con RawBuf (con Bool))))
(ret (own (con Int)))))
(params buf)
(body (app RawBuf.size buf)))
(fn main
(doc "Allocate an 8-slot Bool RawBuf and print its capacity (8) via RawBuf.size only — never get/set. Exercises a size-only Bool buffer.")
(type (fn-type (params) (ret (own (con Unit))) (effects IO)))
(params)
(body
(let buf (new RawBuf (con Bool) 8)
(app print (app capacity buf))))))
@@ -0,0 +1,8 @@
(module mrb_4_unit_element_reject
(fn main
(doc "An LLM author mistakenly names Unit as the element type of a RawBuf. Unit is not in RawBuf's permitted element set {Int, Float, Bool}, so this must be rejected at `ail check` with a param-in diagnostic naming Unit — NOT crash later at build/codegen (the #51 fix). The buffer is only sized and its size printed, so the error is purely the forbidden element type.")
(type (fn-type (params) (ret (own (con Unit))) (effects IO)))
(params)
(body
(let buf (new RawBuf (con Unit) 4)
(app print (app RawBuf.size buf))))))