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
17 KiB
Fieldtest — kernel-extension-mechanics — 2026-05-28
Status: Draft — awaiting orchestrator triage Author: fieldtester (dispatched by fieldtest skill)
Scope
The kernel-extension-mechanics milestone (commits b586999, 078c39a,
9339279, tidy 9d2e752) ships four language-level mechanisms for
plugin-style extension types: type-scoped namespacing
(TypeName.member), the (new T args) term construct, kernel-tier
modules with (kernel) auto-import flag, and (param-in (a T1 T2 ...))
closed-set type-parameter restriction. The milestone touches the
schema, surface parser/printer, checker, and workspace loader.
Codegen for Term::New is explicitly out of scope (deferred to
raw-buf). The stub crate ailang-kernel-stub ships as an
auto-injected kernel-tier module purely as a ratifying fixture.
Examples
examples/fieldtest/kem_1_list_running_sum.ail — type-scoped namespacing cross-module
- Builds a
List<Int>viaterm-ctor, prints its length via(app List.length xs)and its sum via(app List.fold_left add 0 xs). Importsstd_listexplicitly; bareList/Intin type slots. - Why fits: this is the spec's prep.1 canonical-form example over a different home module and a function (fold_left) that takes a lambda arg — a shape an LLM author naturally produces.
- Outcome:
ail checkclean (ok (48 symbols across 5 modules));ail runprints5\n15\n. Required two local symlinks (std_list.ail,std_maybe.ail) inexamples/fieldtest/because the loader resolves cross-module imports against the entry file's directory — see Finding F2.
examples/fieldtest/kem_2_counter_new.ail — (new T args) term construct
- The spec's literal prep.2 worked example (a local Counter ADT
with a
newdef and(new Counter 42)consumer). - Outcome:
ail checkclean (ok (31 symbols across 3 modules)).ail runfails with the explicit codegen-deferral diagnosticinternal: 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 aworkingfinding (Finding F6).
examples/fieldtest/kem_2b_min_repro.ail — minimal repro for the same-module type-scoped-call bug
- Same Counter shape minus the
newdef: onevalueprojector, one consumer site that calls(app Counter.value c)from within Counter's home module. - Why fits: surfaced while extending kem_2 with a
valueprojector — turned out to be a clean repro of an axis-1 corner the spec didn't carve out. - Outcome:
ail checkfails with[type-mismatch] main: type mismatch: expected kem_2b_min_repro.Counter, got Counter. Switching the call to bare(app value c)makes the program check clean — see Finding F1.
examples/fieldtest/kem_3_stub_consumer.ail — kernel-tier auto-import in type position
- A consumer that uses
(con StubT (con Int))in a fn type signature without(import kernel_stub). The stub kernel module is auto-injected by the workspace loader (ail workspacelists it) and per spec § "Iteration prep.3" "the module's types are accessible bare in type position". - Outcome:
ail checkfails with[unknown-module] unwrap_int: unknown module prefixkernel_stubin qualified reference(twice). Adding an explicit(import kernel_stub)fails earlier with[module-not-found] expected at <sibling>/kernel_stub.ail.jsonbecause the stub has no on-disk file — see Finding F3.
examples/fieldtest/kem_4_paramin_box_green.ail — param-in restriction, accepting path
- A user-defined
NumBox (vars a) (param-in (a Int Float))ADT, instantiated with(con NumBox (con Int)). Verifies the param-in check is permissive when the type-arg is in the allowed set. - Outcome:
ail checkclean;ail runprints17\n. See Finding F7.
examples/fieldtest/kem_4_paramin_box_red.ail — param-in restriction, must-fail path
- Same
NumBoxwithparam-in (a Int Float), instantiated with(con NumBox (con Str)). Expected diagnostic per spec table:ParamNotInRestrictedSet. - Outcome:
ail checkfails with[param-not-in-restricted-set] unwrap_str: type-argStris not in the restricted set for type-variableaofNumBox``. Diagnostic is precise — names the offending type, the type-variable, and the TypeDef that owns the restriction. See Finding F8.
Findings
[bug] F1 — Type-scoped call to a same-module def is rejected with a phantom-qualified type-mismatch
- Example:
kem_2b_min_repro.ail(minimal); also originally surfaced inkem_2_counter_new.ailwhile drafting. - What happened (verbatim):
Switching the call site from
$ ail check examples/fieldtest/kem_2b_min_repro.ail error: [type-mismatch] main: type mismatch: expected kem_2b_min_repro.Counter, got Counter(app Counter.value c)to bare(app value c)makes the same program check clean. - Why it is a bug: Per spec § "Iteration prep.1 — Type-scoped
namespacing" → "Canonical form decision": "Type-scoped form is
the canonical form for type-associated operations". The spec
does NOT carve out same-module calls as forbidden / different.
An LLM author who's just written
valuenext todata Counterin the same file will naturally reach forCounter.valuefrom inside main — the same way they spell every cross-module call per Axis 1. The current resolver appears to produce a qualified<this-module>.Counterfor the return type ofvaluereached throughCounter.value, but compares against an unqualifiedCounterproduced by the caller's local-scopevaluelookup. The diagnostic itself is misleading — the two types are the same, just written differently. - Repro:
ail check examples/fieldtest/kem_2b_min_repro.ail. - Recommended downstream action:
debug— write a RED test that pins the same-module type-scoped-call expects-clean behaviour, thenimplementmini-mode fixes the resolver to treat<this-module>.<Type>and bare<Type>as one type.
[bug] F3 — Kernel-tier auto-import does not register the module as a valid qualifier prefix
- Example:
kem_3_stub_consumer.ail. - What happened (verbatim):
$ ail check examples/fieldtest/kem_3_stub_consumer.ail error: [unknown-module] unwrap_int: unknown module prefix `kernel_stub` in qualified reference error: [unknown-module] main: unknown module prefix `kernel_stub` in qualified referenceail workspace examples/fieldtest/kem_3_stub_consumer.aillistskernel_stubas a loaded module with 1 def. Adding an explicit(import kernel_stub)fails earlier with[module-not-found] expected at /home/brummel/dev/ailang/examples/fieldtest/kernel_stub.ail.json— the stub has no on-disk manifest, it is built into the compiler. - Why it is a bug: Per spec § "Iteration prep.3 — Kernel-tier
modules + param-in" the kernel-tier
Module.kernel: trueflag promises: "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." Prelude's free fns (print,lt, ...) do satisfy that promise —prelude_free_fns.rsratifies it. But the stub kernel module's TypeDefStubT, referenced bare in a type slot, is qualified by the pre-pass tokernel_stub.StubT, and the resolver then rejectskernel_stubas an unknown module prefix. The auto-import mechanism is asymmetric: it scopes the names but does not register the kernel module as a resolvable qualifier. Two layers (the pre-pass that qualifies bare type names, and the resolver that validates qualified module prefixes) disagree on whether kernel-tier modules count. The shipped kernel_stub TypeDef appears effectively unreachable from any consumer. - Repro:
ail check examples/fieldtest/kem_3_stub_consumer.ail. - Recommended downstream action:
debug— write a RED test that a consumer using(con StubT (con Int))(or(con kernel_stub.StubT (con Int))explicitly) checks clean; thenimplementmini-mode either teaches the resolver to recognise every kernel-tier module as an implicit qualifier, or teaches the workspace pre-pass to leave bare references to kernel-tier types un-qualified.
[bug] F4 — Spec's prep.3 worked consumer cannot compile against the actual shipped stub
- Example: observed via probe (see § Examples kem_3 narrative); not a standalone fixture because F3 already blocks the type signature.
- What happened (verbatim):
$ cat /tmp/stub_consumer.ail (module stub_consumer (fn main (type (fn-type (params) (ret (con Unit)) (effects IO))) (params) (body (let s (new StubT 42) (match s (case (pat-ctor Stub x) (app print x))))))) $ ail check /tmp/stub_consumer.ail error: [new-type-not-constructible] main: type `StubT` is not constructible: home module `kernel_stub` declares no `new` def - Why it is a bug: Spec § "Iteration prep.3" → "Worked author
example — kernel-tier stub module" specifies the stub as a
TypeDef PLUS a
(fn new ...)def, and the immediately following "Worked consumer" exercises(new StubT 42). The shipped stub (perail describe --workspace … StubT) has only the TypeDef with one ctorStub— nonewfn. The implementation diverged from the spec's stub design; the consumer-side worked example the spec promises no longer compiles. The diagnostic the resolver emits is excellent (precise, names the home module, names the missing def) — that part isworkingand is separately recorded as F5. - Repro: see narrative.
- Recommended downstream action:
ratify— either add thenewdef to the stub (the spec's design) and refresh the STUB_AIL constant + drift pin, or amend the design model and spec to say "the stub has only a TypeDef; consumers must useterm-ctor" (which is also a coherent reading because the stub's purpose is to exercise auto-import + param-in, notTerm::New). Either reading restores spec/ship coherence; the current state has a drift.
[working] F5 — NewTypeNotConstructible diagnostic is precise
- Example: observed via the F4 probe.
- What happened: The diagnostic
[new-type-not-constructible] main: typeStubTis not constructible: home modulekernel_stubdeclares nonewdefnames the offending type, the home module, and the missing def in one sentence. An LLM author reading this routes correctly to one of two natural next actions: add anewdef, or use a different construction path. Worth recording as a win: prep.2's diagnostic is doing its job. - Recommended downstream action: carry-on.
[working] F6 — Term::New codegen-deferral diagnostic names the future milestone
- Example:
kem_2_counter_new.ailviaail run. - What happened:
Error: module `kem_2_counter_new`: def `main`: internal: Term::New cannot be synthed at codegen — must be desugared first; codegen support lands in the raw-buf milestone - Why a win: The spec explicitly carves codegen out of scope
for this milestone. The error message explicitly names the
future milestone (
raw-buf) where codegen support will land. An LLM author sees the message and routes correctly: "this is expected at this stage, the path forward is the raw-buf milestone" — no guessing, no false leads. The contrast with the phantom-qualified mismatch (F1) is sharp: F6 is what a precise out-of-scope diagnostic looks like. - Recommended downstream action: carry-on.
[working] F7 — param-in accepts allowed type silently and end-to-end
- Example:
kem_4_paramin_box_green.ail. - What happened:
(con NumBox (con Int))whereNumBox'sparam-inis(a Int Float)checks clean; the program also builds and runs (ail runprints17). The param-in mechanism is data-driven from the TypeDef as spec promises — nothing in the checker mentionsNumBoxspecifically, same enforcement path that firesParamNotInRestrictedSeton the RED case. A user TypeDef gets the same treatment as the built-in stub, confirming the generic data-driven story. - Recommended downstream action: carry-on.
[working] F8 — ParamNotInRestrictedSet diagnostic is precise
- Example:
kem_4_paramin_box_red.ail. - What happened: Diagnostic
[param-not-in-restricted-set] unwrap_str: type-argStris not in the restricted set for type-variableaofNumBox`` names the type-arg, the type-variable, and the TypeDef. An LLM author reading this routes correctly to either (a) change the type-arg to one in the allowed set or (b) extend the TypeDef'sparam-in. The message does NOT name the allowed set explicitly — that would make it a one-line full-context diagnostic; recorded as a potential future polish in F9. - Recommended downstream action: carry-on.
[friction] F2 — Loader's sibling-only module resolution makes the fieldtest dir awkward
- Example:
kem_1_list_running_sum.ailand its symlinkedstd_list.ail/std_maybe.ailinexamples/fieldtest/. - What happened (verbatim):
The same consumer module sitting in
$ ail check examples/fieldtest/kem_1_list_running_sum.ail error: [module-not-found] module `std_list` not found (expected at /home/brummel/dev/ailang/examples/fieldtest/std_list.ail.json)examples/checks clean; insideexamples/fieldtest/it does not, because the loader resolves(import std_list)against the entry file's directory. - Why friction (not bug): The loader behaviour is consistent
with its existing one-directory-per-workspace model; the spec
doesn't promise multi-directory loading. But the
fieldtest_examples path is
examples/fieldtest/per the project profile — every cross-module fixture either has to be entirely self-contained (no(import std_*)) or pull in symlinks to files inexamples/. That tax falls on every future fieldtest. An LLM author writing a new program for the project would hit the same wall. - Secondary friction: the error message lists only the
.ail.jsonpath even though the loader does in fact look for both.ailand.ail.json(the existingexamples/tree has no.ail.jsonfiles forstd_list, yet check there works). A user reading the diagnostic would conclude they need to author a.ail.json— a wrong reading. - Recommended downstream action:
plan— small tidy iteration: (a) loader looks up modules through a workspace-config search path (analogous toexamples/itself), so fixtures can sit outside the canonical example directory without symlinks; AND/OR (b) diagnostic message updated to list both expected paths and to suggest the workspace config addition. Either half-fix removes the symlink-as-workaround pattern.
[spec_gap] F9 — unit literal: design model uses it, checker rejects it
- Example:
kem_4_paramin_box_red.ailfirst draft (before fix). - What happened:
; before fix (fn main (type (fn-type (params) (ret (con Unit)) (effects IO))) (params) (body unit)) ; resulting diagnostic: ; [unbound-var] main: unknown identifier: `unit`design/models/0007-kernel-extensions.mdline 80 usesunitas a value literal of typeUnit. The checker treats it as a Term::Var lookup and emits[unbound-var]. The fix inkem_4_paramin_box_red.ailwas to replace the body with(do io/print_str "")— a small awkwardness for any "intentionally empty effectful main" pattern. - Why spec_gap (not bug): The whitepaper and design ledger
may simply have been imprecise — there's no language contract
pinning
unitas a value literal. But the design model uses it as if it were canonical; an LLM author readingdesign/models/0007will reach for it. - Recommended downstream action:
ratifyortighten the design ledger— either addunitas the canonical Unit-value literal (a one-line resolver change) and document it, or remove the usage fromdesign/models/0007-kernel-extensions.mdand replace with the canonical idiom. The current state has the model and the surface disagreeing.
Recommendation summary
| Finding | Class | Recommended action |
|---|---|---|
| F1 | bug | debug → implement (resolver: equate <this>.T with bare T) |
| F2 | friction | plan (tidy iteration: loader search path + diagnostic) |
| F3 | bug | debug → implement (auto-import registers module as qualifier) |
| F4 | bug | debug or ratify (stub: add new def, OR amend spec) |
| F5 | working | carry-on |
| F6 | working | carry-on |
| F7 | working | carry-on |
| F8 | working | carry-on |
| F9 | spec_gap | ratify OR tighten design model |
Concerns / blockers
None. The two bugs (F1, F3) are diagnosis-clear and have minimal repros in the working tree. F4 is a spec/ship coherence call — either reading is coherent; needs an orchestrator decision.