diff --git a/docs/journals/INDEX.md b/docs/journals/INDEX.md index 7e65d47..32df476 100644 --- a/docs/journals/INDEX.md +++ b/docs/journals/INDEX.md @@ -54,3 +54,4 @@ - 2026-05-13 — iter form-a.0: prelude pilot for Form-A-as-default-authoring milestone — examples/prelude.ail rendered (116 lines / 6386 bytes) via `ail render`; two auto-discovering roundtrip tests (every_ail_fixture_matches_its_json_counterpart + parse_then_print_then_parse_is_idempotent_on_every_ail_fixture) green on the new fixture without test-code changes; prelude.ail.json retained per spec §A2 (singular dual-form iter, deletion lands iter 1 with bulk test-infra refactor); cargo test --workspace green at 558 tests → 2026-05-13-iter-form-a.0.md - 2026-05-13 — iter form-a.1: Form-A-as-default-authoring milestone close (Boss-decided strategy C, big-bang) — 12 tasks across two implementer dispatches; T1 added 3 new tests (parse-determinism + CLI-pipeline-idempotency + carve_out_inventory#[ignore]) + T2 bulk-rendered 99 missing examples/.ail (corpus 58→157 .ail) + T3-4 migrated 26 fixture-loading test files (Group A load_workspace→ailang_surface::load_workspace + Group B subprocess suffix flip) + T5 relocated 23 #[cfg(test)] mod tests blocks from ailang-core/-codegen production source to integration test crates with ailang-surface dev-dep (hash_pin.rs + workspace_pin.rs + eq_primitives_pin.rs + prose snapshot migration) + T6 bench-driver suffix flips (4 Python + run.sh, both compile_check.py + cross_lang.py exit 0) + T7 re-authored 4 raw-JSON-inspect e2e tests via `ail parse`-derived tempfiles + retired 1 (render_parse_round_trip_canonical subsumed by T1) + T8 deleted 156 non-carve-out .ail.json (inventory 8 carve-outs + 157 .ail post-T8, alphabetical: broken_unbound + prelude + 3× test_22b2_* + 3× test_ct1_*) + 20 forward-pulled stale-.ail.json-ref repairs missed by T1-5 recon + T9 retired 3 obsolete roundtrip tests (print_then_parse_round_trips_every_fixture + every_ail_fixture_matches_its_json_counterpart + cli_render_then_parse_preserves_canonical_bytes_on_every_fixture) + dead helpers (list_json_fixtures ×2, round_trip_one, strip_trailing_newlines) + flipped schema_coverage corpus to .ail (forward-pulled into T8 for green-gate) + T10 §C3 DESIGN.md §Roundtrip Invariant restated with parse-determinism + idempotency + CLI-pipeline-idempotency + carve-out-anchor framing (5 surviving enforcement tests named) + T11 §A4 CLAUDE.md + DESIGN.md doctrine sentences replaced (canonical form is JSON-AST; authoring form is .ail) + T12 WhatsNew milestone-close entry + roadmap [milestone] form-a struck [x] (Closed 2026-05-13 by iter form-a.1). 4 carve-outs added beyond the original 7 §C4(a): only 1 — prelude.ail.json — landed as §C4(b) compile-time-embed (rationale: include_str! at workspace.rs:417 + main.rs:474; resolution queued as separate milestone Prelude embed: Form-A as compile-time source). Spec amendment + grounding-recheck PASS at 9fcda8b. Final tests: 557 green + 3 ignored (561 from T1 − 4 retired = 557); bench compile_check.py + cross_lang.py exit 0; check.py noise envelope continues 11th consecutive audit, baseline pristine. Milestone structurally closed → 2026-05-13-iter-form-a.1.md - 2026-05-13 — audit-form-a: milestone close (Form-A as the default authoring surface) — architect status drift_found with 4 documentary-only items (2× [medium] spec + plan "seven carve-outs" orphans contradicting the §C4(b) amendment commit 9fcda8b — the implementation is eight-carve-out-correct; 2× [low] hash.rs:57 empty `mod tests {}` placeholder after T5 relocation + round_trip.rs:16 stale "Direction 2" docstring contradicting T10's framing rewrite); architect explicit recommendation carry-on — no form-a.tidy queued, drift deferred to documentary cleanup at next opportunity (architect verbatim: "the implementation is correct and the spec orphans are post-hoc retro-clean-up, not active drift"). Bench: all 3 scripts exit 0, baseline pristine for the 12th consecutive audit. check.py 2 regressions in tail-latency max_us (latency.explicit_at_rc + latency.implicit_at_rc, both >120% over baseline, both paired with p99 improvements on same metric stem) — 12th consecutive observation of the metric-identity-migrating noise envelope since audit-cma; compile_check.py 2 regressions in sub-millisecond check_ms on hello + borrow_own_demo — timing-jitter on the corpus's two smallest fixtures, iter form-a.1 T6 already flagged the same pair as transient; cross_lang.py 25/25 stable, bench_tree_walk.rc_over_c improved 2.74→2.59 still inside noise. What holds (verified): §C3 §Roundtrip Invariant restated with 4 properties + 5 surviving enforcement tests named verbatim; §A4 CLAUDE.md + DESIGN.md doctrine reworded per spec; 8-carve-out inventory matches spec post-amendment (alphabetical: broken_unbound + prelude + 3× test_22b2_* + 3× test_ct1_*); 156 non-carve-out .ail.json deleted; 157 .ail post-iter; cargo test --workspace 557 green + 3 ignored; WhatsNew editorial-clean (user-facing); roadmap milestone struck [x] with close attribution + follow-up [Prelude embed] milestone present with motivation + 3 resolution options. Milestone fully closed → 2026-05-13-audit-form-a.md +- 2026-05-13 — fieldtest-form-a: 4 hand-authored `.ail` fixtures (factorial smoke, Show user-ADT, user-class Describe with two instances, two-module workspace) + spec report. Findings: 1 bug (instance-method-body unbound-var bypasses `ail check`; surfaces at mono with degraded "monomorphise_workspace: unknown identifier" diagnostic — `ail check` should fire `[unbound-var]` like at fn-body level → debug RED-first), 1 friction (`str_concat` primitive missing — first attempt at user-Show body wanted "prefix=" ++ int_to_str-style concatenation, repaired by dropping prefix → planner tidy symmetric to `str_clone`/`int_to_str` wiring), 2 spec_gaps (`form_a.md` has zero `class`/`instance`/`constraint`/`method` sections — the form-a milestone made `.ail` the authoring form but the form_a spec doc still only covers pre-22 surface; canonical-form rule for `(instance (class X))` class-qualifier not in form_a — `class X` vs `class some_module.X` reading is ambiguous from the spec, observed canonical reading via examples corpus). Plus 3 working (4 fixtures, 3 first-shot-clean + 1 first-attempt-bug-then-clean). Boss-side cleanup at commit time: deleted 8 stale `.ail.json` sidecars in `examples/fieldtest/` (4 forma_* derived by fieldtester per old workflow, 4 pre-existing floats_* from prior milestone that T8 missed because the bash deletion loop globbed only `examples/*.ail.json` direct children); plus updated `skills/fieldtest/agents/ailang-fieldtester.md` Phase 3 to remove the now-obsolete "generate canonical JSON" step (the agent was calibrated to dual-form pre-form-a; post-form-a `.ail` is the sole authoring form). Findings deferred to follow-up iters: bug → next `debug` dispatch, friction → small planner tidy, spec_gaps → form_a.md tightening pass → 2026-05-13-fieldtest-form-a.md diff --git a/docs/specs/2026-05-13-fieldtest-form-a.md b/docs/specs/2026-05-13-fieldtest-form-a.md new file mode 100644 index 0000000..ced2ced --- /dev/null +++ b/docs/specs/2026-05-13-fieldtest-form-a.md @@ -0,0 +1,284 @@ +# 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 check` → `ok (23 symbols across 2 modules)`; `ail build` + → `built`; stdout `120\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 }`, 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.ail` → + `ok (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` | diff --git a/examples/fieldtest/floats_1_newton_sqrt.ail.json b/examples/fieldtest/floats_1_newton_sqrt.ail.json deleted file mode 100644 index 43e75fd..0000000 --- a/examples/fieldtest/floats_1_newton_sqrt.ail.json +++ /dev/null @@ -1 +0,0 @@ -{"defs":[{"body":{"cond":{"args":[{"name":"k","t":"var"},{"lit":{"kind":"int","value":0},"t":"lit"}],"fn":{"name":"==","t":"var"},"t":"app"},"else":{"args":[{"name":"n","t":"var"},{"args":[{"lit":{"bits":"3fe0000000000000","kind":"float"},"t":"lit"},{"args":[{"name":"x","t":"var"},{"args":[{"name":"n","t":"var"},{"name":"x","t":"var"}],"fn":{"name":"/","t":"var"},"t":"app"}],"fn":{"name":"+","t":"var"},"t":"app"}],"fn":{"name":"*","t":"var"},"t":"app"},{"args":[{"name":"k","t":"var"},{"lit":{"kind":"int","value":1},"t":"lit"}],"fn":{"name":"-","t":"var"},"t":"app"}],"fn":{"name":"newton_iter","t":"var"},"t":"app","tail":true},"t":"if","then":{"name":"x","t":"var"}},"doc":"Recurse k times applying x' = 0.5 * (x + n/x).","kind":"fn","name":"newton_iter","params":["n","x","k"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"Float"},{"k":"con","name":"Float"},{"k":"con","name":"Int"}],"ret":{"k":"con","name":"Float"}}},{"body":{"args":[{"name":"n","t":"var"},{"name":"n","t":"var"},{"lit":{"kind":"int","value":20},"t":"lit"}],"fn":{"name":"newton_iter","t":"var"},"t":"app"},"doc":"20-step Newton iteration starting from x0 = n.","kind":"fn","name":"sqrt","params":["n"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"Float"}],"ret":{"k":"con","name":"Float"}}},{"body":{"args":[{"args":[{"lit":{"bits":"4000000000000000","kind":"float"},"t":"lit"}],"fn":{"name":"sqrt","t":"var"},"t":"app"}],"op":"io/print_float","t":"do"},"kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"floats_1_newton_sqrt","schema":"ailang/v0"} \ No newline at end of file diff --git a/examples/fieldtest/floats_2_average_int_list.ail.json b/examples/fieldtest/floats_2_average_int_list.ail.json deleted file mode 100644 index 0041518..0000000 --- a/examples/fieldtest/floats_2_average_int_list.ail.json +++ /dev/null @@ -1 +0,0 @@ -{"defs":[{"ctors":[{"fields":[],"name":"Nil"},{"fields":[{"k":"con","name":"Int"},{"k":"con","name":"IntList"}],"name":"Cons"}],"kind":"type","name":"IntList"},{"body":{"arms":[{"body":{"name":"acc","t":"var"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"args":[{"name":"t","t":"var"},{"args":[{"name":"acc","t":"var"},{"name":"h","t":"var"}],"fn":{"name":"+","t":"var"},"t":"app"}],"fn":{"name":"sum_acc","t":"var"},"t":"app","tail":true},"pat":{"ctor":"Cons","fields":[{"name":"h","p":"var"},{"name":"t","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"Tail-recursive sum with accumulator.","kind":"fn","name":"sum_acc","params":["xs","acc"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"IntList"},{"k":"con","name":"Int"}],"ret":{"k":"con","name":"Int"}}},{"body":{"arms":[{"body":{"name":"acc","t":"var"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"args":[{"name":"t","t":"var"},{"args":[{"name":"acc","t":"var"},{"lit":{"kind":"int","value":1},"t":"lit"}],"fn":{"name":"+","t":"var"},"t":"app"}],"fn":{"name":"count_acc","t":"var"},"t":"app","tail":true},"pat":{"ctor":"Cons","fields":[{"p":"wild"},{"name":"t","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"Tail-recursive length with accumulator.","kind":"fn","name":"count_acc","params":["xs","acc"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"IntList"},{"k":"con","name":"Int"}],"ret":{"k":"con","name":"Int"}}},{"body":{"args":[{"args":[{"args":[{"name":"xs","t":"var"},{"lit":{"kind":"int","value":0},"t":"lit"}],"fn":{"name":"sum_acc","t":"var"},"t":"app"}],"fn":{"name":"int_to_float","t":"var"},"t":"app"},{"args":[{"args":[{"name":"xs","t":"var"},{"lit":{"kind":"int","value":0},"t":"lit"}],"fn":{"name":"count_acc","t":"var"},"t":"app"}],"fn":{"name":"int_to_float","t":"var"},"t":"app"}],"fn":{"name":"/","t":"var"},"t":"app"},"doc":"Mean as Float = sum / count, both promoted via int_to_float.","kind":"fn","name":"mean","params":["xs"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"IntList"}],"ret":{"k":"con","name":"Float"}}},{"body":{"args":[{"args":[{"args":[{"lit":{"kind":"int","value":1},"t":"lit"},{"args":[{"lit":{"kind":"int","value":2},"t":"lit"},{"args":[{"lit":{"kind":"int","value":3},"t":"lit"},{"args":[{"lit":{"kind":"int","value":4},"t":"lit"},{"args":[{"lit":{"kind":"int","value":6},"t":"lit"},{"args":[],"ctor":"Nil","t":"ctor","type":"IntList"}],"ctor":"Cons","t":"ctor","type":"IntList"}],"ctor":"Cons","t":"ctor","type":"IntList"}],"ctor":"Cons","t":"ctor","type":"IntList"}],"ctor":"Cons","t":"ctor","type":"IntList"}],"ctor":"Cons","t":"ctor","type":"IntList"}],"fn":{"name":"mean","t":"var"},"t":"app"}],"op":"io/print_float","t":"do"},"kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"floats_2_average_int_list","schema":"ailang/v0"} \ No newline at end of file diff --git a/examples/fieldtest/floats_3_safe_division.ail.json b/examples/fieldtest/floats_3_safe_division.ail.json deleted file mode 100644 index 1db2cff..0000000 --- a/examples/fieldtest/floats_3_safe_division.ail.json +++ /dev/null @@ -1 +0,0 @@ -{"defs":[{"body":{"cond":{"args":[{"name":"x","t":"var"}],"fn":{"name":"is_nan","t":"var"},"t":"app"},"else":{"cond":{"args":[{"name":"x","t":"var"},{"lit":{"bits":"7fe1ccf385ebc8a0","kind":"float"},"t":"lit"}],"fn":{"name":">","t":"var"},"t":"app"},"else":{"cond":{"args":[{"name":"x","t":"var"},{"lit":{"bits":"ffe1ccf385ebc8a0","kind":"float"},"t":"lit"}],"fn":{"name":"<","t":"var"},"t":"app"},"else":{"lit":{"kind":"int","value":1},"t":"lit"},"t":"if","then":{"lit":{"kind":"int","value":0},"t":"lit"}},"t":"if","then":{"lit":{"kind":"int","value":0},"t":"lit"}},"t":"if","then":{"lit":{"kind":"int","value":-1},"t":"lit"}},"doc":"-1=NaN, 0=infinite, 1=finite. Uses is_nan + abs > huge.","kind":"fn","name":"classify","params":["x"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"Float"}],"ret":{"k":"con","name":"Int"}}},{"body":{"lhs":{"args":[{"args":[{"lit":{"bits":"4018000000000000","kind":"float"},"t":"lit"},{"lit":{"bits":"4008000000000000","kind":"float"},"t":"lit"}],"fn":{"name":"/","t":"var"},"t":"app"}],"op":"io/print_float","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"args":[{"lit":{"bits":"4018000000000000","kind":"float"},"t":"lit"},{"lit":{"bits":"4008000000000000","kind":"float"},"t":"lit"}],"fn":{"name":"/","t":"var"},"t":"app"}],"fn":{"name":"classify","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"lit":{"bits":"3ff0000000000000","kind":"float"},"t":"lit"},{"lit":{"bits":"0000000000000000","kind":"float"},"t":"lit"}],"fn":{"name":"/","t":"var"},"t":"app"}],"op":"io/print_float","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"args":[{"lit":{"bits":"3ff0000000000000","kind":"float"},"t":"lit"},{"lit":{"bits":"0000000000000000","kind":"float"},"t":"lit"}],"fn":{"name":"/","t":"var"},"t":"app"}],"fn":{"name":"classify","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"lit":{"bits":"0000000000000000","kind":"float"},"t":"lit"},{"lit":{"bits":"0000000000000000","kind":"float"},"t":"lit"}],"fn":{"name":"/","t":"var"},"t":"app"}],"op":"io/print_float","t":"do"},"rhs":{"args":[{"args":[{"args":[{"lit":{"bits":"0000000000000000","kind":"float"},"t":"lit"},{"lit":{"bits":"0000000000000000","kind":"float"},"t":"lit"}],"fn":{"name":"/","t":"var"},"t":"app"}],"fn":{"name":"classify","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"t":"seq"},"t":"seq"},"t":"seq"},"t":"seq"},"t":"seq"},"kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"floats_3_safe_division","schema":"ailang/v0"} \ No newline at end of file diff --git a/examples/fieldtest/floats_4_float_to_str_reach.ail.json b/examples/fieldtest/floats_4_float_to_str_reach.ail.json deleted file mode 100644 index 6e97ec5..0000000 --- a/examples/fieldtest/floats_4_float_to_str_reach.ail.json +++ /dev/null @@ -1 +0,0 @@ -{"defs":[{"body":{"args":[{"args":[{"lit":{"bits":"40091eb851eb851f","kind":"float"},"t":"lit"}],"fn":{"name":"float_to_str","t":"var"},"t":"app"}],"op":"io/print_str","t":"do"},"kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"floats_4_float_to_str_reach","schema":"ailang/v0"} \ No newline at end of file diff --git a/examples/fieldtest/forma_1_factorial.ail b/examples/fieldtest/forma_1_factorial.ail new file mode 100644 index 0000000..ad6dc2f --- /dev/null +++ b/examples/fieldtest/forma_1_factorial.ail @@ -0,0 +1,30 @@ +(module forma_1_factorial + + (fn fact_acc + (doc "Tail-recursive factorial accumulator: fact_acc(n, acc) = n!*acc.") + (type + (fn-type + (params (con Int) (con Int)) + (ret (con Int)))) + (params n acc) + (body + (if (app == n 0) + acc + (tail-app fact_acc (app - n 1) (app * acc n))))) + + (fn fact + (doc "Factorial of a non-negative Int.") + (type + (fn-type + (params (con Int)) + (ret (con Int)))) + (params n) + (body (app fact_acc n 1))) + + (fn main + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body + (seq + (do io/print_int (app fact 5)) + (do io/print_int (app fact 10)))))) diff --git a/examples/fieldtest/forma_2_show_color.ail b/examples/fieldtest/forma_2_show_color.ail new file mode 100644 index 0000000..8c6e662 --- /dev/null +++ b/examples/fieldtest/forma_2_show_color.ail @@ -0,0 +1,31 @@ +(module forma_2_show_color + + (data Color + (doc "A small 3-variant enum used to exercise instance Show on a user ADT.") + (ctor Red) + (ctor Green) + (ctor Blue)) + + (instance + (class prelude.Show) + (type (con Color)) + (method show + (body + (lam + (params (typed c (con Color))) + (ret (con Str)) + (body + (match c + (case (pat-ctor Red) "Red") + (case (pat-ctor Green) "Green") + (case (pat-ctor Blue) "Blue"))))))) + + (fn main + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body + (seq + (seq + (app print (term-ctor Color Red)) + (app print (term-ctor Color Green))) + (app print (term-ctor Color Blue)))))) diff --git a/examples/fieldtest/forma_3_user_class_describe.ail b/examples/fieldtest/forma_3_user_class_describe.ail new file mode 100644 index 0000000..e15d1d4 --- /dev/null +++ b/examples/fieldtest/forma_3_user_class_describe.ail @@ -0,0 +1,58 @@ +(module forma_3_user_class_describe + + (class Describe + (doc "A user-defined typeclass: produce a short Str describing a value.") + (param a) + (method describe + (type (fn-type (params (borrow a)) (ret (con Str)))))) + + (data Shape + (ctor Circle (con Int)) + (ctor Square (con Int))) + + (instance + (class Describe) + (type (con Int)) + (method describe + (body + (lam + (params (typed n (con Int))) + (ret (con Str)) + (body (app int_to_str n)))))) + + (instance + (class Describe) + (type (con Shape)) + (method describe + (body + (lam + (params (typed s (con Shape))) + (ret (con Str)) + (body + (match s + (case (pat-ctor Circle r) + (app int_to_str r)) + (case (pat-ctor Square w) + (app int_to_str w)))))))) + + (fn announce + (doc "Polymorphic announcer: describe a value then print the result.") + (type + (forall (vars a) + (constraints (constraint Describe a)) + (fn-type + (params (borrow a)) + (ret (con Unit)) + (effects IO)))) + (params x) + (body (do io/print_str (app describe x)))) + + (fn main + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body + (seq + (seq + (app announce 42) + (app announce (term-ctor Shape Circle 3))) + (app announce (term-ctor Shape Square 5)))))) diff --git a/examples/fieldtest/forma_4_intmath_lib.ail b/examples/fieldtest/forma_4_intmath_lib.ail new file mode 100644 index 0000000..1b4b8fd --- /dev/null +++ b/examples/fieldtest/forma_4_intmath_lib.ail @@ -0,0 +1,22 @@ +(module forma_4_intmath_lib + + (fn square + (doc "Multiply an Int by itself.") + (type (fn-type (params (con Int)) (ret (con Int)))) + (params x) + (body (app * x x))) + + (fn cube + (doc "Raise an Int to the third power.") + (type (fn-type (params (con Int)) (ret (con Int)))) + (params x) + (body (app * x (app square x)))) + + (fn abs_int + (doc "Absolute value of an Int.") + (type (fn-type (params (con Int)) (ret (con Int)))) + (params x) + (body + (if (app < x 0) + (app - 0 x) + x)))) diff --git a/examples/fieldtest/forma_4_intmath_main.ail b/examples/fieldtest/forma_4_intmath_main.ail new file mode 100644 index 0000000..7512742 --- /dev/null +++ b/examples/fieldtest/forma_4_intmath_main.ail @@ -0,0 +1,13 @@ +(module forma_4_intmath_main + + (import forma_4_intmath_lib) + + (fn main + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body + (seq + (seq + (do io/print_int (app forma_4_intmath_lib.square 7)) + (do io/print_int (app forma_4_intmath_lib.cube 3))) + (do io/print_int (app forma_4_intmath_lib.abs_int (app - 0 42))))))) diff --git a/skills/fieldtest/agents/ailang-fieldtester.md b/skills/fieldtest/agents/ailang-fieldtester.md index 3131c7e..8df96a4 100644 --- a/skills/fieldtest/agents/ailang-fieldtester.md +++ b/skills/fieldtest/agents/ailang-fieldtester.md @@ -46,9 +46,12 @@ Read in this order, before picking examples: 4. `docs/specs/.md` if one exists — the contract this milestone signed up for. 5. `examples/` — to learn the *form* of valid AIL. You may read any - `.ail` and `.ail.json` under `examples/` (these are the public - corpus). You may NOT use them as a hint about how the compiler - handles edge cases; only as a hint about the shape of the surface. + `.ail` under `examples/` (these are the public corpus; since the + form-a-default-authoring milestone closed 2026-05-13, `.ail` is + the sole authoring form; only eight `.ail.json` carve-outs remain + and are negative-test fixtures unsuitable for inspiration). You + may NOT use them as a hint about how the compiler handles edge + cases; only as a hint about the shape of the surface. ## Carrier contract — what the controller hands you @@ -66,7 +69,7 @@ spec; if both are also empty, return `NEEDS_CONTEXT`. ``` DESIGN.MD AND `examples/` ARE YOUR ONLY REFERENCE. CRATES/, RUNTIME/, BENCH/ ARE FORBIDDEN READS. -EVERY EXAMPLE IS WRITTEN IN .ail FIRST. NO HAND-WRITTEN .ail.json. +EVERY EXAMPLE IS WRITTEN IN .ail. NO `.ail.json` SIDECARS (since 2026-05-13). RECORD WHAT HAPPENS. DO NOT FIX. DO NOT WORK AROUND. ``` @@ -76,7 +79,7 @@ under `crates/`, `runtime/`, `bench/scripts/`, or `bench/reference/`, **stop**. The only file paths you may open are: - `CLAUDE.md`, `docs/**`, `examples/**`, `skills/**` -- the `.ail` and `.ail.json` files YOU create under `examples/` +- the `.ail` files YOU create under `examples/` (no `.ail.json` sidecars — see Phase 3) - the binaries YOU produce via `ail build` - the `.ll` files YOU produce via `ail emit-ir` if you want to inspect generated IR (the IR is part of the public surface per @@ -134,18 +137,18 @@ example to dodge it. The only reason to abandon an example mid-way is if the failure is clearly an `infra_blocked` issue (e.g. `ail` binary missing); in that case return `BLOCKED` with the cause. -### Phase 3 — Generate canonical JSON, only via tooling +### Phase 3 — No `.ail.json` sidecars (since form-a milestone) -The canonical form is `.ail.json`. You do NOT hand-write it. After -each `.ail` runs cleanly, generate the JSON via: - -```bash -ail render --json examples/fieldtest/<...>.ail > examples/fieldtest/<...>.ail.json -``` - -(or whichever subcommand `ail --help` lists for the surface→json -direction; if no such subcommand exists, that is itself a finding — -record it and skip the JSON for that example). +Since the form-a-default-authoring milestone closed (2026-05-13), the +working tree carries exactly **one** representation per program: the +`.ail` source. Tests and the CLI parse `.ail` in-process via +`ailang_surface::parse`; the JSON-AST is materialised in memory by +callers that need it, not as a checked-in file. Do NOT generate +`.ail.json` sidecars for your fieldtest fixtures. If you find yourself +about to run `ail parse .ail -o .ail.json` for any reason other +than a transient tempfile *inside* a single test run, stop — the only +JSON-AST files that remain on disk are the eight §C4 carve-outs of the +form-a-default-authoring spec. ### Phase 4 — Classify findings @@ -164,9 +167,9 @@ merged. ### Phase 5 — Write the spec, hand back Write `docs/specs/-fieldtest-.md` using the -spec structure below. Leave all artefacts (the `.ail` files, the -`.ail.json` files, and the spec file) in the working tree as -unstaged changes. You do NOT commit — the Boss commits after +spec structure below. Leave all artefacts (the `.ail` files and the +spec file) in the working tree as unstaged changes. You do NOT +commit — the Boss commits after reading the end-report (suggested commit subject: `fieldtest: examples, findings`). @@ -266,13 +269,13 @@ before committing. | Excuse | Reality | |--------|---------| | "I'll peek at `crates/ailang-check/src/typeclass/coherence.rs` to understand why this compiled" | Peeking ends the test. The downstream author cannot peek. If DESIGN.md does not explain it, that is a `spec_gap`. | -| "Hand-writing the .ail.json is faster" | The surface IS the test target. JSON-direct skips the test. If `ail render --json` is missing, record it; do NOT route around it. | +| "I'll write a `.ail.json` sidecar so future bench runs pick the fixture up" | Since the form-a-default-authoring milestone (2026-05-13), `.ail` is the sole authoring form. Tests parse `.ail` in-process; no sidecar is ever needed or wanted. The eight surviving `.ail.json` carve-outs are negative-test fixtures, NOT a pattern to copy. | | "Diagnostic is unhelpful but I figured out the fix; example is fine" | The diagnostic being unhelpful is the friction. Record verbatim. | | "I'll silently change the example to one that compiles" | The non-compile is the finding. Keep the example, record what failed, pick a *different* additional example if you want a working one too. | | "DESIGN.md is fuzzy on the typeclass instance ordering, I'll pick the natural reading and proceed" | Pick the reading, RUN the example, AND record `spec_gap` with the reading you picked and why another reading was equally plausible. | | "Bug found — I'll just fix it now, faster than handing off to debug" | Fix-in-place violates the skill split. The fix lands in a separate, RED-tested commit via `debug` → `implement`. | | "Two examples both ran clean, no findings — short report" | A clean run is itself a finding (`working`). Record what was reached for, what diagnostic showed up when wrong, what was easy. Wins protect the feature from drift. | -| "I'll skip the JSON file generation, the .ail is enough" | The committed pair (.ail + .ail.json) is the regression fixture. Without the JSON, future bench runs can't pick it up. If `ail render --json` doesn't exist, record it as a finding. | +| "I'll skip the JSON file generation, the .ail is enough" | Correct since 2026-05-13. The `.ail` IS the fixture. The build derives JSON-AST in-process. No JSON file is required and writing one is drift. | | "Three examples is enough, I'll skip the fourth axis" | Each axis the milestone touched needs at least one example. Skipping an axis silently turns the field test into a partial signal, which is worse than no signal because the orchestrator will read it as full coverage. | ## Red Flags — STOP and re-read DESIGN.md @@ -280,8 +283,9 @@ before committing. - About to open any path matching `crates/**`, `runtime/**`, `bench/scripts/**`, `bench/reference/**` - About to open a `.rs` file at all (no Rust source is in scope) -- About to write a `.ail.json` file by hand (only `ail render --json` - is allowed to produce these) +- About to write a `.ail.json` file at all — neither by hand nor + via `ail parse .ail -o .ail.json` (since 2026-05-13, the + authoring form is `.ail` and `.ail.json` sidecars are drift) - About to silently shrink an example to make it compile - About to merge a `bug` and a `friction` into one finding - About to attempt a fix in this dispatch