fieldtest: kernel-extension-mechanics — 6 examples, 9 findings (3 bugs, 1 friction, 1 spec_gap, 4 working)
Post-audit fieldtest for the kernel-extension-mechanics milestone. Fieldtester wrote 6 .ail consumer programs against the public interface only (design ledger + spec + ail CLI; no source crate reads), one per axis with two for the param-in pair (accept + reject). All artefacts in examples/fieldtest/ + the spec. **Working (4):** F5 NewTypeNotConstructible diagnostic is precise (names the home module + missing def in one sentence). F6 Term::New codegen-deferral diagnostic explicitly names the raw-buf milestone where support lands. F7 param-in accept-path is end-to-end (check + build + run prints 17). F8 ParamNotInRestrictedSet names type-arg + var + TypeDef. **Bugs (3, action: debug):** - F1 (axis 1) — Same-module type-scoped call `(app Type.member …)` rejected with phantom-qualified `expected <this>.T, got T`. Bare `(app member …)` from inside Type's home module works. Spec's "Canonical form decision" promises type-scoped works uniformly; the resolver appears to disagree with the workspace pre-pass on whether `<this-module>.T` equals bare `T`. Repro: examples/fieldtest/kem_2b_min_repro.ail. - F3 (axis 3) — Kernel-tier auto-import scopes the name but does not register the module as a resolvable qualifier prefix. Per the pre-pass, a bare `StubT` in a type slot is qualified to `kernel_stub.StubT`; the resolver then rejects `kernel_stub` as unknown. Asymmetric: prelude's free fns work (per prelude_free_fns.rs), but the stub's TypeDef is effectively unreachable from any consumer. Repro: examples/fieldtest/kem_3_stub_consumer.ail. - F4 (axis 3, spec/ship coherence) — The spec's prep.3 worked- consumer relies on `(new StubT 42)`, but the shipped STUB_AIL has only a TypeDef + ctor — the `(fn new ...)` the spec designed is missing from the implementation. The diagnostic the resolver does emit on the consumer (F5) is precise; the bug is the absent def itself. Resolution: re-add the `new` to STUB_AIL (the spec's design), refresh the drift pin. **Friction (1, action: plan-or-defer):** - F2 — Loader's sibling-only module resolution forces symlinks for any cross-module fixture sitting outside the canonical `examples/` directory. The kem_1 example required local symlinks for std_list.ail + std_maybe.ail. Secondary: the diagnostic message names only the .ail.json path even though .ail also resolves — actively misleading. Recommended: small tidy iter for loader workspace-search-path + diagnostic clarity. **Spec-gap (1, action: ratify):** - F9 — `design/models/0007-kernel-extensions.md:80` uses `unit` as a value literal of type Unit; the checker treats it as a Term::Var lookup and emits [unbound-var]. Either ratify `unit` as the canonical Unit-value literal in the language, or tighten the design model to use the existing idiom. The current model/ surface disagreement is a small but real LLM-author trap. **Symlinks committed** as evidence of the F2 workaround: examples/fieldtest/std_list.ail and std_maybe.ail are symlinks into ../. They are the fingerprint of the friction — removing them silently would erase the evidence. Per Iron Law, fieldtester did not work around bugs — they were all surfaced and recorded. The kem_2b_min_repro.ail fixture exists specifically because F1 surfaced mid-drafting and got minimised; kem_3_stub_consumer.ail also stays as the F3 RED-side fixture. Triage (next-step routing for /boss): F4 → small inline fix (add (fn new ...) to STUB_AIL + drift refresh) F9 → small inline ratify (whitepaper edit) F2 → backlog issue, deferred (single tidy iter someday) F1, F3 → debug skill dispatches, then implement mini-mode F5-F8 → carry-on, recorded as wins
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
; Fieldtest kem.1 (Axis 1, type-scoped namespacing) — a consumer of
|
||||
; std_list that drives several List.<fn> calls from one main. The
|
||||
; LLM-natural shape is to spell every type-associated op with the
|
||||
; type-name prefix (`List.length`, `List.fold_left`, `List.map`),
|
||||
; not the module qualifier. Bare `List`/`Int` in type positions; bare
|
||||
; ctor names `Cons`/`Nil` because the type is in scope via
|
||||
; `(import std_list)`. The cycle's worked example for axis 1 is the
|
||||
; std_maybe_demo migration; this example is the same shape over
|
||||
; List<Int>, exercising a different home module and a function
|
||||
; (fold_left) that takes a lambda — the LLM-natural shape there is to
|
||||
; keep the lambda inline at the call site.
|
||||
|
||||
(module kem_1_list_running_sum
|
||||
|
||||
(import std_list)
|
||||
|
||||
(fn add
|
||||
(doc "Plain Int addition. Used as the (b, a) -> b arg to List.fold_left.")
|
||||
(type (fn-type (params (con Int) (con Int)) (ret (con Int))))
|
||||
(params acc x)
|
||||
(body (app + acc x)))
|
||||
|
||||
(fn main
|
||||
(doc "Build a List<Int> of [1,2,3,4,5], print its length (5), then its sum (15).")
|
||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||
(params)
|
||||
(body
|
||||
(let xs
|
||||
(term-ctor List Cons 1
|
||||
(term-ctor List Cons 2
|
||||
(term-ctor List Cons 3
|
||||
(term-ctor List Cons 4
|
||||
(term-ctor List Cons 5
|
||||
(term-ctor List Nil))))))
|
||||
(seq (seq (app print (app List.length xs)) (do io/print_str "\n"))
|
||||
(seq (app print (app List.fold_left add 0 xs)) (do io/print_str "\n")))))))
|
||||
@@ -0,0 +1,34 @@
|
||||
; Fieldtest kem.2 (Axis 2, (new T args) term construct) — derived
|
||||
; from the spec's prep.2 § "Iteration prep.2 — `new` term construct"
|
||||
; worked example, kept literally identical to that example so the
|
||||
; outcome speaks to the spec's own promise.
|
||||
;
|
||||
; Outcome:
|
||||
; - `ail check` clean (the construct elaborates).
|
||||
; - `ail run` fails with the codegen-deferral error
|
||||
; "Term::New cannot be synthed at codegen — must be desugared
|
||||
; first; codegen support lands in the raw-buf milestone".
|
||||
; Per spec § Architecture this is the explicit out-of-scope of
|
||||
; the milestone; recorded as a `working` finding (the diagnostic
|
||||
; correctly names the future milestone and is precise enough that
|
||||
; an LLM author can route to the right next step).
|
||||
|
||||
(module kem_2_counter_new
|
||||
|
||||
(data Counter
|
||||
(ctor MkCounter (con Int)))
|
||||
|
||||
(fn new
|
||||
(doc "Build a Counter initialised to the given value.")
|
||||
(type (fn-type (params (con Int)) (ret (con Counter))))
|
||||
(params n)
|
||||
(body (term-ctor Counter MkCounter n)))
|
||||
|
||||
(fn main
|
||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||
(params)
|
||||
(body
|
||||
(let c (new Counter 42)
|
||||
(match c
|
||||
(case (pat-ctor MkCounter x)
|
||||
(seq (app print x) (do io/print_str "\n"))))))))
|
||||
@@ -0,0 +1,22 @@
|
||||
; Minimal repro of the same-module type-scoped call bug.
|
||||
; Calling `(app Counter.value c)` from within Counter's home
|
||||
; module raises [type-mismatch] expected `kem_2b_min_repro.Counter`,
|
||||
; got `Counter` — even though `value` belongs to *this* module.
|
||||
; Switching to bare `(app value c)` works.
|
||||
|
||||
(module kem_2b_min_repro
|
||||
|
||||
(data Counter
|
||||
(ctor MkCounter (con Int)))
|
||||
|
||||
(fn value
|
||||
(type (fn-type (params (con Counter)) (ret (con Int))))
|
||||
(params c)
|
||||
(body (match c (case (pat-ctor MkCounter x) x))))
|
||||
|
||||
(fn main
|
||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||
(params)
|
||||
(body
|
||||
(let c (term-ctor Counter MkCounter 41)
|
||||
(app print (app Counter.value c))))))
|
||||
@@ -0,0 +1,51 @@
|
||||
; Fieldtest kem.3 (Axis 3, kernel-tier modules + auto-import) — a
|
||||
; consumer that references `kernel_stub`'s TypeDef without an
|
||||
; `(import kernel_stub)` declaration. Per spec § "Iteration prep.3":
|
||||
;
|
||||
; - The module's top-level defs are accessible bare in every other
|
||||
; module of the workspace, with no `(import ...)` declaration
|
||||
; required.
|
||||
; - The module's types are accessible bare in type position.
|
||||
;
|
||||
; Outcome: `ail check` fails with
|
||||
;
|
||||
; [unknown-module] unwrap_int: unknown module prefix
|
||||
; `kernel_stub` in qualified reference
|
||||
;
|
||||
; The workspace pre-pass rewrites bare `StubT` to qualified
|
||||
; `kernel_stub.StubT` (correct per prep.1's `qualify_workspace_term`);
|
||||
; but the resolver does not accept `kernel_stub` as a valid module
|
||||
; prefix because the consumer has no explicit `(import kernel_stub)`.
|
||||
; That is, the qualification pre-pass and the auto-import scope are
|
||||
; not cooperating — the auto-import does NOT add the kernel module
|
||||
; to the resolver's known-prefixes set.
|
||||
;
|
||||
; Workarounds attempted:
|
||||
;
|
||||
; - Adding `(import kernel_stub)`: fails earlier with
|
||||
; [module-not-found] expected at <sibling>/kernel_stub.ail.json
|
||||
; — the kernel stub has no on-disk file, it is built into the
|
||||
; compiler binary.
|
||||
; - Bare `StubT` everywhere: same outcome as above (the pre-pass
|
||||
; qualifies it before the resolver sees it).
|
||||
;
|
||||
; Prelude's bare fns (`print`, `lt`, etc.) DO work — prelude_free_fns
|
||||
; ratifies that. So the bug is specifically about TypeDefs in type
|
||||
; position for kernel-tier modules other than prelude. The shipped
|
||||
; kernel_stub TypeDef appears unreachable from any consumer in
|
||||
; practice.
|
||||
|
||||
(module kem_3_stub_consumer
|
||||
|
||||
(fn unwrap_int
|
||||
(doc "Project the Int back out of a StubT<Int>. Bare `StubT` in the type slot exercises the kernel-tier auto-import — and currently fails to resolve.")
|
||||
(type (fn-type (params (con StubT (con Int))) (ret (con Int))))
|
||||
(params s)
|
||||
(body (match s (case (pat-ctor Stub x) x))))
|
||||
|
||||
(fn main
|
||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||
(params)
|
||||
(body
|
||||
(let s (term-ctor StubT Stub 7)
|
||||
(seq (app print (app unwrap_int s)) (do io/print_str "\n"))))))
|
||||
@@ -0,0 +1,29 @@
|
||||
; Fieldtest kem.4-green (Axis 4, param-in restriction, accepting path).
|
||||
; User-defined `NumBox a` ADT restricts its type parameter to
|
||||
; {Int, Float}. The consumer uses `(con NumBox (con Int))` — Int is in
|
||||
; the allowed set, so check is clean.
|
||||
;
|
||||
; Per spec § "Iteration prep.3 — Kernel-tier modules + param-in":
|
||||
; the param-in restriction is a generic, data-driven check; nothing
|
||||
; in the checker mentions any specific extension. So a user TypeDef
|
||||
; can declare it just as the stub kernel_stub.StubT does.
|
||||
|
||||
(module kem_4_paramin_box_green
|
||||
|
||||
(data NumBox (vars a)
|
||||
(doc "An Int-or-Float container — param-in restriction on `a`.")
|
||||
(param-in (a Int Float))
|
||||
(ctor MkNumBox a))
|
||||
|
||||
(fn unwrap
|
||||
(doc "Project the contained Int out of a NumBox<Int>.")
|
||||
(type (fn-type (params (con NumBox (con Int))) (ret (con Int))))
|
||||
(params b)
|
||||
(body (match b (case (pat-ctor MkNumBox x) x))))
|
||||
|
||||
(fn main
|
||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||
(params)
|
||||
(body
|
||||
(let b (term-ctor NumBox MkNumBox 17)
|
||||
(seq (app print (app unwrap b)) (do io/print_str "\n"))))))
|
||||
@@ -0,0 +1,23 @@
|
||||
; Fieldtest kem.4-red (Axis 4, param-in restriction, must-fail path).
|
||||
; Same NumBox<a> with `param-in (a Int Float)`, but the consumer
|
||||
; instantiates it with `(con Str)` — Str is not in the allowed set.
|
||||
; Expected diagnostic per spec § Error handling table:
|
||||
; `ParamNotInRestrictedSet`.
|
||||
|
||||
(module kem_4_paramin_box_red
|
||||
|
||||
(data NumBox (vars a)
|
||||
(doc "Int-or-Float container — param-in restriction on `a`.")
|
||||
(param-in (a Int Float))
|
||||
(ctor MkNumBox a))
|
||||
|
||||
(fn unwrap_str
|
||||
(doc "BAD: NumBox is restricted to {Int, Float}, but we ask for Str. Expected: ParamNotInRestrictedSet on the type signature.")
|
||||
(type (fn-type (params (con NumBox (con Str))) (ret (con Str))))
|
||||
(params b)
|
||||
(body (match b (case (pat-ctor MkNumBox x) x))))
|
||||
|
||||
(fn main
|
||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||
(params)
|
||||
(body (do io/print_str ""))))
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../std_list.ail
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../std_maybe.ail
|
||||
Reference in New Issue
Block a user