Files
AILang/docs/specs/0026-fieldtest-form-a.md
T
Brummel 832375f2ac convention: counter-prefix file naming across docs/specs/, docs/plans/, design/contracts/, design/models/
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.
2026-05-28 13:31:31 +02:00

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) plus main prints fact 5 and fact 10. Exercises tail-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 checkok (23 symbols across 2 modules); ail buildbuilt; stdout 120\n3628800\n (expected). First-shot clean.

examples/fieldtest/forma_2_show_color.ailinstance Show Color + polymorphic print

  • What it does: declares data Color { Red | Green | Blue }, an instance prelude.Show Color whose body pattern-matches and returns a Str literal, and calls print three times. Exercises milestone 24's polymorphic print end-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; stdout Red\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, ships instance Describe Int and instance Describe Shape (a 2-variant ADT), then a polymorphic free fn announce : forall a. Describe a => (borrow a) -> Unit !IO that calls describe then io/print_str. main calls announce at 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 check returned ok but ail build died with monomorphise_workspace: unknown identifier: str_concat. Repaired by dropping the prefix-string concatenation. Repaired version runs clean: stdout 42\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_lib exports three pure-Int fns (square, cube, abs_int); forma_4_intmath_main imports it and calls each via qualified references (forma_4_intmath_lib.square 7, etc.) inside main.
  • 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.ailok (24 symbols across 3 modules) (lib + main + prelude); ail build → built; stdout 49\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 fires error: [unbound-var] greet: unknown identifier: str_concat at ail 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, no did you mean ...?.

  • Why it is a bug: ail check is 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 asserting ail check fires [unbound-var] on the instance-method-body shape, then implement mini-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 a forall. 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 from examples/show_user_adt.ail, examples/bench_mono_dispatch.ail, and examples/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.type constraints (lines ~1526-1582) but the Form-A textual projection for any of those keywords is not anchored anywhere. The milestone- 24 close ratified print + four Show instances 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 extend form_a.md) — plan an 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.Show form from examples/show_user_adt.ail. It works. But I also note that examples/mq3_class_eq_vs_fn_eq_classmod.ail writes (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-AST InstanceDef.class field level, but not at the Form-A surface level.
  • Reading I picked: a class reference inside (instance (class ...)) follows the same canonical-form rule as Type::Con.name — bare for same-module, module.Class for cross-module. Another plausible reading: always-qualified (since prelude is auto-imported, bare Show could resolve to prelude.Show — but the empirical evidence in show_user_adt.ail says 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.class at the Form-A surface.

[friction] No primitive str_concat / str_append in builtins

  • Example: forma_3 first attempt.
  • What happened: while writing a Describe Shape instance, I reached for a string-concatenation primitive to format "Circle r=" ++ int_to_str r. ail builtins lists int_to_str, bool_to_str, str_clone, float_to_str — no str_concat, no str_append, no ++. The natural Show MyType body shape for any non-trivial ADT (a record-like print, a name=value style, 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 print and four prelude Show instances on primitives; the natural LLM-author follow-up is Show 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 through print). The friction is real for the milestone's downstream utility, not just for hypothetical foreign LLMs.
  • Recommended downstream action: plan a small follow-up iter that adds str_concat : (Str, Str) -> Str as a runtime + checker + codegen primitive (symmetric to str_clone / int_to_str plumbing 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 realistic Show MyType body 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 (Int and Shape), wrote announce : forall a. Describe a => (borrow a) -> Unit !IO, called it three times at two concrete types in main. ail check reported ok across 2 modules (entry + prelude); ail build produced a binary; stdout was correct. The mq.3 type-driven dispatch + the iter-23.4 unified mono pass + the milestone-24 print-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__Int and describe__Shape + call-site rewrite) ran end-to-end on a hand-authored .ail workspace 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 .ail modules in examples/fieldtest/, one importing the other with (import forma_4_intmath_lib) and using qualified forma_4_intmath_lib.square / .cube / .abs_int references inside main. ail check examples/fieldtest/forma_4_intmath_main.ail correctly resolved the import (workspace prefers .ail sibling over .ail.json per 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 }, wrote instance prelude.Show Color with a 3-arm match body returning a Str literal, called (app print (term-ctor Color Red)) three times in main. Compiled clean; ran clean; produced Red\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 print reaching 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