All 176 files in the four accumulating directories now use a zero-padded 4-digit counter prefix that reflects creation order (`NNNN-slug.md`). The counter is assigned per directory in strict git-log creation order; ties broken alphabetically by original name. The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is dropped — the date is recoverable from git log and the counter carries the ordering. A file's counter is stable for the life of the file: never reassigned, never reused, never compacted. Deleted files retire their counter; subsequent files do not fill the gap. This is the property that lets cross-references stay literal — refs use the full filename including the counter (`design/contracts/0007-honesty-rule.md`) so they grep cleanly and resolve directly without a glob step. 313 cross-references updated across .md/.rs/.toml/.c/.json files (test pins, include_str! paths, design-INDEX entries, baseline notes, runtime C comments, inter-contract markdown links incl. bare basename and `../models/foo.md` forms). CLAUDE.md gets a new "File-naming convention" section spelling out the rule and rationale. skills/brainstorm/SKILL.md and skills/planner/SKILL.md updated so new spec/plan creation produces counter-prefixed names from the start. The full test suite (cargo test --workspace) passes.
16 KiB
Fieldtest — form-a-default-authoring — 2026-05-13
Status: Draft — awaiting orchestrator triage Author: ailang-fieldtester (dispatched by skills/fieldtest)
Scope
Form A (.ail) became the sole authoring surface across the working
tree. 156 non-carve-out .ail.json files were rendered to .ail
siblings and deleted (8 carve-outs remain). The Roundtrip Invariant
was restated as parse-determinism + idempotency-under-print + CLI-
pipeline-idempotency. CLI subcommands ail check / build / run / parse / render accept .ail directly via extension dispatch (since
iter ext-cli.1). This fieldtest probes whether a downstream LLM-author
working only from DESIGN.md + crates/ailang-core/specs/form_a.md +
the public corpus can hand-author programs that exercise the new
authoring surface, the milestone-23/24 typeclass machinery (Eq,
Ord, Show, print), and multi-module workspaces.
Examples
examples/fieldtest/forma_1_factorial.ail — tail-recursive factorial
- What it does: two top-level fns (
fact_acc/2,fact/1) plusmainprintsfact 5andfact 10. Exercisestail-app,if, primitive arithmetic, no class machinery. The LLM-natural shape for "is the basic authoring path frictionless?". - Why it fits the milestone: pure-arithmetic smoke for the authoring-ergonomics axis. No class or workspace surface; just exercises parser → checker → codegen on a spontaneous LLM-author shape derived from the form_a.md few-shot corpus (sum_demo / hello).
- Outcome:
ail check→ok (23 symbols across 2 modules);ail build→built; stdout120\n3628800\n(expected). First-shot clean.
examples/fieldtest/forma_2_show_color.ail — instance Show Color + polymorphic print
- What it does: declares
data Color { Red | Green | Blue }, aninstance prelude.Show Colorwhose body pattern-matches and returns aStrliteral, and callsprintthree times. Exercises milestone 24's polymorphicprintend-to-end on a user ADT with a manually written Show instance. - Why it fits the milestone: the Show+print axis. Tests that the iter-24.3 cross-module-references-in-synthesised-bodies invariants fire correctly on a user-defined enum-like ADT.
- Outcome:
ail check→ ok;ail build→ built; stdoutRed\nGreen\nBlue\n(expected). First-shot clean.
examples/fieldtest/forma_3_user_class_describe.ail — user-defined class Describe with two instances
- What it does: declares
class Describe a where describe : (borrow a) -> Str, shipsinstance Describe Intandinstance Describe Shape(a 2-variant ADT), then a polymorphic free fnannounce : forall a. Describe a => (borrow a) -> Unit !IOthat callsdescribethenio/print_str.maincallsannounceat three concrete types. - Why it fits the milestone: stresses the user-typeclass authoring
surface,
(forall (vars a) (constraints (constraint Describe a)) ...)shape, monomorphisation of a polymorphic free fn through a user constraint, and dispatch to two distinct user instances. - Outcome: first attempt failed because I reached for
str_concat(a builtin I assumed existed for"Circle r=" ++ int_to_str r-style bodies);ail checkreturnedokbutail builddied withmonomorphise_workspace: unknown identifier: str_concat. Repaired by dropping the prefix-string concatenation. Repaired version runs clean: stdout42\n3\n5\n(expected). See findings below.
examples/fieldtest/forma_4_intmath_lib.ail + forma_4_intmath_main.ail — two-module workspace
- What it does:
forma_4_intmath_libexports three pure-Intfns (square,cube,abs_int);forma_4_intmath_mainimports it and calls each via qualified references (forma_4_intmath_lib.square 7, etc.) insidemain. - Why it fits the milestone: stresses extension-dispatch on a
workspace entry plus canonical-form qualified cross-module refs
(ct.1 / mq.1 mature features) on hand-authored
.ail. - Outcome:
ail check examples/fieldtest/forma_4_intmath_main.ail→ok (24 symbols across 3 modules)(lib + main + prelude);ail build→ built; stdout49\n27\n42\n(expected). First-shot clean.
Findings
[bug] ail check does NOT catch unbound identifiers inside (instance ... (method (body (lam ... (body APP)))))
-
Example: forma_3 first attempt; isolated repro in spec body below.
-
What happened (verbatim, isolated repro):
(module ft_concat_in_instance (class Describe (param a) (method describe (type (fn-type (params (borrow a)) (ret (con Str)))))) (instance (class Describe) (type (con Int)) (method describe (body (lam (params (typed n (con Int))) (ret (con Str)) (body (app str_concat "n=" (app int_to_str n)))))))) (fn main (type (fn-type (params) (ret (con Unit)) (effects IO))) (params) (body (do io/print_str (app describe 5)))))ail check:ok (23 symbols across 2 modules).ail build:Error: monomorphise_workspace: unknown identifier: str_concat.The same shape at fn-body level (a plain
(fn ... (body (app str_concat ...)))) correctly fireserror: [unbound-var] greet: unknown identifier: str_concatatail check. So the typechecker has the diagnostic machinery; it does not walk the lambda body inside an instance method's(method (body (lam ... (body ...))))when looking up identifiers (or it walks but does not produce an error at the unbound-var). The error eventually fires at the monomorphisation pass, whose diagnostic ("monomorphise_workspace: unknown identifier") is significantly less helpful than the typechecker's[unbound-var]— no source location, no symbol kind, nodid you mean ...?. -
Why it is a bug:
ail checkis documented as the typecheck step whose purpose is exactly to catch this class of error before codegen runs. The split-pipeline diagnostic surface is a regression in the user-facing contract. -
One-line repro: see the four-line isolated repro above.
-
Recommended downstream action:
debug→ write a RED test assertingail checkfires[unbound-var]on the instance-method-body shape, thenimplementmini-mode walks the lambda body during instance validation. (Plausible cause: instance method bodies are validated later than fn bodies, after the registry build, and the unbound-var check is not threaded through that path.)
[spec_gap] crates/ailang-core/specs/form_a.md does not document the typeclass surface
- Example(s): forma_2 (Show user ADT) and forma_3 (user-defined class
- constraint).
- What happened: I needed to write
(class ...),(instance ...),(method ...), and(constraints (constraint ClassName tyvar))inside aforall.form_a.md— the document explicitly billed as "the complete LLM-targeted specification. If you are an LLM and this is in your context, you have everything you need to produce valid Form-A" — has zero references to "class", "instance", "constraint", or "method". The grep is empty. I reverse-engineered the surface entirely fromexamples/show_user_adt.ail,examples/bench_mono_dispatch.ail, andexamples/mq1_xmod_constraint_class.ail. - Reading I picked: the
(class NAME (param TYVAR) (method NAME (type FN-TYPE)))form for class declarations;(instance (class CLASS- REF) (type TYPE) (method NAME (body LAM-TERM)))for instances;(constraints (constraint CLASS-REF TYVAR))inside a(forall ...)schema. These work but are not in the spec; another reading (e.g. whether(class CLASS-REF)must be qualified vs. bare for same- module instances) was not determinable from the spec text and had to be derived by example-mining. - Why spec_gap: a foreign LLM-author with form_a.md in context but
without the example corpus cannot author class / instance / poly-
constrained-fn programs at all. DESIGN.md §Decision-11 documents
the JSON-AST schema for
ClassDef/InstanceDef/FnDef.typeconstraints (lines ~1526-1582) but the Form-A textual projection for any of those keywords is not anchored anywhere. The milestone- 24 close ratifiedprint+ fourShowinstances ship in the prelude — without a documented Form-A surface for user-class authoring, the LLM-utility criterion for typeclasses (Decision 11 → "LLM author reaches for it unprompted") cannot be tested on foreign LLMs by the procedure that cma.3 / ms.2 used for non-class programs. - Recommended downstream action:
tighten DESIGN.md(or extendform_a.md) —planan iter that adds three sections to form_a.md: "Class declarations", "Instance declarations", and "Constraints on polymorphic fns". Each with the EBNF production and one or two few-shot fixtures lifted from the corpus (show_user_adt.ail,mq1_xmod_constraint_class.ail).
[spec_gap] Form-A grammar for prelude.Show qualifier in (instance (class ...)): when is the prefix required?
- Example: forma_2 (
(instance (class prelude.Show) (type (con Color)) ...)). - What happened: I copied the qualified
prelude.Showform fromexamples/show_user_adt.ail. It works. But I also note thatexamples/mq3_class_eq_vs_fn_eq_classmod.ailwrites(instance (class MyEq) ...)bare (no prefix) — because the class is declared in the SAME module. The rule appears to be the mq.1 canonical-form rule (qualify for cross-module, bare for same-module). DESIGN.md §mq.1 makes this explicit at the JSON-ASTInstanceDef.classfield level, but not at the Form-A surface level. - Reading I picked: a class reference inside
(instance (class ...))follows the same canonical-form rule asType::Con.name— bare for same-module,module.Classfor cross-module. Another plausible reading: always-qualified (sincepreludeis auto-imported, bareShowcould resolve to prelude.Show — but the empirical evidence inshow_user_adt.ailsays no, qualifier is required for cross- module). - Why spec_gap: a downstream LLM-author cannot determine whether
(instance (class Show) (type (con MyType)) ...)(bare, same module as class? cross-module to prelude?) is well-formed without reading the corpus. form_a.md doesn't carry the rule; DESIGN.md carries it at the JSON-AST level but not for the surface. - Recommended downstream action: same iter as the previous gap —
one paragraph in form_a.md's new "Class declarations" / "Instance
declarations" sections naming the canonical-form rule for
Constraint.class,InstanceDef.class,SuperclassRef.classat the Form-A surface.
[friction] No primitive str_concat / str_append in builtins
- Example: forma_3 first attempt.
- What happened: while writing a
Describe Shapeinstance, I reached for a string-concatenation primitive to format"Circle r=" ++ int_to_str r.ail builtinslistsint_to_str,bool_to_str,str_clone,float_to_str— nostr_concat, nostr_append, no++. The naturalShow MyTypebody shape for any non-trivial ADT (a record-like print, aname=valuestyle, anything with a label) requires string concatenation. Without it, the LLM-author either writes degenerate instance bodies (return just the inner Int as if it were the whole repr — what forma_3's repaired version does) or punts on milestone-24 Show-of-rich-ADTs entirely. - Why friction: milestone 24 shipped polymorphic
printand four preludeShowinstances on primitives; the natural LLM-author follow-up isShow MyRecord = "MyRecord { x = " ++ show x ++ " }"-shaped instance bodies. With no concatenation primitive, the shipped Show feature is observably under-powered for the very task it was advertised to enable (user-ADT pretty-printing throughprint). The friction is real for the milestone's downstream utility, not just for hypothetical foreign LLMs. - Recommended downstream action:
plana small follow-up iter that addsstr_concat : (Str, Str) -> Stras a runtime + checker + codegen primitive (symmetric tostr_clone/int_to_strplumbing in 24.1 / hs.4). The cost surface is small (single-fn slab- allocating C helper + four-site lockstep registration); the LLM- utility payoff is high because every realisticShow MyTypebody needs it.
[working] User-defined typeclass with forall + Constraint polymorphic fn dispatches correctly
- Example: forma_3 (repaired).
- What happened: declared
class Describe a where describe : (borrow a) -> Str, shipped two instances (IntandShape), wroteannounce : forall a. Describe a => (borrow a) -> Unit !IO, called it three times at two concrete types inmain.ail checkreported ok across 2 modules (entry + prelude);ail buildproduced a binary; stdout was correct. The mq.3 type-driven dispatch + the iter-23.4 unified mono pass + the milestone-24print-style polymorphic-fn pipeline all visibly cooperate on a workload they were not specifically calibrated against. No diagnostic surfaced spuriously; the surface was discoverable from three corpus examples in ~5 minutes. - Why working: the full Decision-11 trajectory (class declaration +
multiple instances + polymorphic free fn over a constraint set +
mono-pass synthesis of
describe__Intanddescribe__Shape+ call-site rewrite) ran end-to-end on a hand-authored.ailworkspace without any compiler crate hint. This is the LLM-utility acceptance test for milestone 22+23+24 cumulative, run on one spontaneous fresh-author shape, and it passes. - Recommended downstream action:
carry-on. Worth pinning the fixture so a future regression cannot silently break the trajectory. (Already a working-tree artefact;crates/ailang-core/tests/schema_coverage.rs's dynamic walk picks it up automatically per the form-a.1 refactor.)
[working] Multi-module workspace via (import ...) + qualified refs runs end-to-end on .ail
- Example: forma_4_intmath_main + forma_4_intmath_lib.
- What happened: two
.ailmodules inexamples/fieldtest/, one importing the other with(import forma_4_intmath_lib)and using qualifiedforma_4_intmath_lib.square/.cube/.abs_intreferences insidemain.ail check examples/fieldtest/forma_4_intmath_main.ailcorrectly resolved the import (workspace prefers.ailsibling over.ail.jsonper the ext-cli.1 doc), typechecked across 3 modules (main + lib + prelude), built, and ran. No friction from the canonical-form rule — qualified names worked first-shot. - Why working: the ext-cli.1 + form-a.1 stack (extension-dispatching
workspace loader +
.ail-preferred sibling resolution) functions exactly as DESIGN.md promises for a hand-authored two-module workspace. - Recommended downstream action:
carry-on.
[working] Polymorphic print on user ADT through instance prelude.Show MyType
- Example: forma_2.
- What happened: declared
data Color { Red | Green | Blue }, wroteinstance prelude.Show Colorwith a 3-arm match body returning a Str literal, called(app print (term-ctor Color Red))three times in main. Compiled clean; ran clean; producedRed\nGreen\nBlue\n. The whole iter-24.3 cross-module-references-in-synthesised-bodies trajectory (the three invariants in DESIGN.md §Cross-module references in synthesised bodies — canonical-form type_args, import_map-bypass fallback, FreeFnCall residual push) ran on a user-ADT that the implementation tests did not specifically pin. - Why working: the LLM-utility acceptance test for milestone-24's
flagship feature (polymorphic
printreaching user ADTs through a user-written Show instance) passes on a first-shot LLM-author example. - Recommended downstream action:
carry-on.
Recommendation summary
| Finding | Class | Action |
|---|---|---|
ail check misses unbound-var inside instance method lambda body |
bug | debug (RED test on instance-body unbound-var; mini implement) |
form_a.md lacks class/instance/constraint grammar |
spec_gap | plan (form_a.md addendum: 3 new sections + corpus fixtures) |
Form-A canonical-form rule for (instance (class X)) qualifier not anchored |
spec_gap | plan (paragraph in form_a.md's new class-section; tighten DESIGN.md §Decision 11 surface anchor) |
No str_concat builtin |
friction | plan (small follow-up iter; adds runtime + checker + codegen + lockstep wiring symmetric to str_clone) |
| User-typeclass + constraint poly fn end-to-end | working | carry-on |
Multi-module .ail workspace + qualified refs |
working | carry-on |
print on user ADT through user instance prelude.Show |
working | carry-on |