diff --git a/docs/specs/0066-fieldtest-raw-buf-milestone.md b/docs/specs/0066-fieldtest-raw-buf-milestone.md new file mode 100644 index 0000000..7cdea75 --- /dev/null +++ b/docs/specs/0066-fieldtest-raw-buf-milestone.md @@ -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 )`, 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) | diff --git a/examples/fieldtest/mrb_1_histogram.ail b/examples/fieldtest/mrb_1_histogram.ail new file mode 100644 index 0000000..b1e5d17 --- /dev/null +++ b/examples/fieldtest/mrb_1_histogram.ail @@ -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))))))) diff --git a/examples/fieldtest/mrb_2_float_smooth.ail b/examples/fieldtest/mrb_2_float_smooth.ail new file mode 100644 index 0000000..c05586f --- /dev/null +++ b/examples/fieldtest/mrb_2_float_smooth.ail @@ -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))))))) diff --git a/examples/fieldtest/mrb_3_bool_capacity.ail b/examples/fieldtest/mrb_3_bool_capacity.ail new file mode 100644 index 0000000..88963d4 --- /dev/null +++ b/examples/fieldtest/mrb_3_bool_capacity.ail @@ -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)))))) diff --git a/examples/fieldtest/mrb_4_unit_element_reject.ail b/examples/fieldtest/mrb_4_unit_element_reject.ail new file mode 100644 index 0000000..4a16cc4 --- /dev/null +++ b/examples/fieldtest/mrb_4_unit_element_reject.ail @@ -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))))))