diff --git a/docs/plans/2026-05-09-22c-user-class-e2e.md b/docs/plans/2026-05-09-22c-user-class-e2e.md new file mode 100644 index 0000000..d5d073c --- /dev/null +++ b/docs/plans/2026-05-09-22c-user-class-e2e.md @@ -0,0 +1,416 @@ +# 22c — User-class e2e fixture + +> **Parent spec:** `docs/specs/2026-05-09-22-typeclasses.md` +> +> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement` +> to run this plan. Steps use `- [ ]` checkboxes for tracking. + +**Goal:** ship one `.ail.json` fixture exercising the typeclass +machinery end-to-end through user-defined code (no Prelude +involvement). Running `ail build` on the fixture and executing the +binary prints `42`. + +**Architecture:** the fixture defines a user class `Foo a` with one +method, a user ADT `IntBox`, and an instance `Foo IntBox` whose body +matches on the ctor and returns the wrapped int. `main` calls +`foo (MkIntBox 42)` and prints the result through `io/print_int`. The +22b.3 monomorphisation pass synthesises `foo__` for +`Foo IntBox`, rewrites the call site, and the existing match-lowering +emits the body as ordinary code. No new compiler passes, no new +runtime primitives, no new surface syntax. + +**Tech Stack:** `examples/` (one new fixture), `crates/ail/tests/` +(one new e2e test file). No production-code crates touched. + +**Out of scope:** Prelude class for primitives (deferred per spec +amendment 2026-05-09 §"22b.4b dropped"). Multiple instances per +class, multi-method classes (`test_22b3_coherence_two_instances` +and `test_22b3_default_e2e` already cover those). + +--- + +## Files this plan creates or modifies + +- Create: `examples/test_22c_user_class_e2e.ail.json` — the fixture. +- Create: `crates/ail/tests/typeclass_22c.rs` — the e2e test + (`cargo test -p ail --test typeclass_22c`). Asserts the binary + prints `42\n`. +- Modify: `docs/JOURNAL.md` — append iteration entry at iter close. + +That is the entire iteration's surface. No code crate is modified, +because 22b.3 already shipped every machine-code path the fixture +exercises. + +--- + +## Cross-task invariants + +1. **No production-code changes.** If a task requires editing + anything under `crates/*/src/` (other than tests), that is a + sign of a hidden bug surfaced by the fixture — STOP and route + through `debug` skill (RED-first), not through this plan. +2. **No Prelude.** The fixture defines its own class; do NOT import + any `examples/prelude*.ail.json` (none exists yet anyway, but the + discipline matters: 22c's property is "user-defined" end-to-end). +3. **Bench gates green.** Per spec acceptance criterion 2, + `bench/check.py` / `bench/compile_check.py` / `bench/cross_lang.py` + must all exit 0/0/0 at iter close. + +--- + +## Task 1: Author the fixture + +**Files:** +- Create: `examples/test_22c_user_class_e2e.ail.json` + +**Step 1.1 — Write the fixture** + +Hand-write the canonical JSON. Schema validation via `ail check` +catches any shape error in Step 1.2. Key shapes verified against +existing fixtures: `examples/box.ail.json` (data-type + match + +term-ctor), `examples/test_22b3_default_e2e.ail.json` (class + +instance with `(t: "lam")` body), `examples/borrow_own_demo.ail.json` +(`param_modes: ["borrow"]` snake_case). + +```json +{ + "schema": "ailang/v0", + "name": "test_22c_user_class_e2e", + "imports": [], + "defs": [ + { + "kind": "type", + "name": "IntBox", + "ctors": [ + { "name": "MkIntBox", "fields": [{ "k": "con", "name": "Int" }] } + ] + }, + { + "kind": "class", + "name": "Foo", + "param": "a", + "methods": [ + { + "name": "foo", + "type": { + "k": "fn", + "params": [{ "k": "var", "name": "a" }], + "param_modes": ["borrow"], + "ret": { "k": "con", "name": "Int" }, + "effects": [] + } + } + ] + }, + { + "kind": "instance", + "class": "Foo", + "type": { "k": "con", "name": "IntBox" }, + "methods": [ + { + "name": "foo", + "body": { + "t": "lam", + "params": ["b"], + "paramTypes": [{ "k": "var", "name": "a" }], + "retType": { "k": "con", "name": "Int" }, + "body": { + "t": "match", + "scrutinee": { "t": "var", "name": "b" }, + "arms": [ + { + "pat": { + "p": "ctor", + "ctor": "MkIntBox", + "fields": [{ "p": "var", "name": "v" }] + }, + "body": { "t": "var", "name": "v" } + } + ] + } + } + } + ] + }, + { + "kind": "fn", + "name": "main", + "type": { + "k": "fn", + "params": [], + "ret": { "k": "con", "name": "Unit" }, + "effects": ["IO"] + }, + "params": [], + "body": { + "t": "do", + "op": "io/print_int", + "args": [ + { + "t": "app", + "fn": { "t": "var", "name": "foo" }, + "args": [ + { + "t": "ctor", + "type": "IntBox", + "ctor": "MkIntBox", + "args": [{ "t": "lit", "lit": { "kind": "int", "value": 42 } }] + } + ] + } + ] + } + } + ] +} +``` + +Notes on canonical key shape: +- `data def: kind: "type"` (NOT `"data"`). +- `Pattern: {"p": "ctor", ...}` / `{"p": "var", ...}` — discriminator + field is `"p"`, not `"pattern"` or `"kind"`. Pattern's child + patterns are under `"fields"`, not `"args"`. +- `term-ctor: {"t": "ctor", "type": "IntBox", "ctor": "MkIntBox", "args": [...]}` — + the type name and the ctor name are sibling fields; do NOT use + `"t": "term-ctor"`. +- Instance method body's `paramTypes` references the class param + variable `a` (not the concrete type `IntBox`). The mono pass + substitutes `a := IntBox` when synthesising the per-instance + function. Mirror the existing 22b.3 fixtures + (`test_22b3_coherence_two_instances` shows `paramTypes: + [{"k": "var", "name": "a"}]`). + +**Step 1.2 — Verify the fixture loads** + +``` +cargo run -q -p ail -- check examples/test_22c_user_class_e2e.ail.json +``` + +Expected: exit 0, no diagnostics. + +If `ail check` reports an error like `unknown method "foo"` or +`no-instance for Foo IntBox`, that is a fixture bug — fix the JSON +shape until check passes. + +If `ail check` reports an internal error or panics, STOP and route +through `debug` skill RED-first — that is a real compiler bug +surfaced by 22c, not a fixture issue. + +**Step 1.3 — Verify the fixture compiles and runs** + +``` +cargo run -q -p ail -- build examples/test_22c_user_class_e2e.ail.json -o /tmp/test_22c +/tmp/test_22c +``` + +Expected: stdout `42\n`, exit 0. + +If `ail build` errors during monomorphisation / codegen / linking, +STOP and route through `debug` skill — the fixture proves a real +compiler-side gap (most likely candidates: mono pass on instance +where the type is a user ADT, or match-lowering inside a synthesised +mono fn). + +**Step 1.4 — Commit the fixture** + +``` +git add examples/test_22c_user_class_e2e.ail.json +git commit -m "iter 22c.1: fixture — user class + user ADT + instance, prints 42" +``` + +--- + +## Task 2: E2E test + +**Files:** +- Create: `crates/ail/tests/typeclass_22c.rs` + +**Step 2.1 — Write the e2e test** + +Use the same shape as `crates/ail/tests/typeclass_22b3.rs`'s +`synthetic_fixture_runs_and_prints_five` test (an existing precedent +for "build + run a fixture and assert stdout"). Concretely: + +```rust +//! Iter 22c: user-class end-to-end. The single 22c fixture defines a +//! user class `Foo`, a user ADT `IntBox`, an `instance Foo IntBox`, +//! and a `main` that calls `foo (MkIntBox 42)` through the +//! monomorphisation pass and prints the result via `io/print_int`. +//! This is the milestone-22 acceptance fixture for the user-class +//! path (no Prelude involvement). + +use std::process::Command; + +fn workspace_root() -> std::path::PathBuf { + let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); + manifest.parent().unwrap().parent().unwrap().to_path_buf() +} + +#[test] +fn user_class_fixture_compiles_runs_and_prints_42() { + let root = workspace_root(); + let fixture = root.join("examples/test_22c_user_class_e2e.ail.json"); + let out = root.join("target/test-22c/test_22c"); + std::fs::create_dir_all(out.parent().unwrap()).expect("mkdir"); + + // Build + let build = Command::new("cargo") + .args([ + "run", "-q", "-p", "ail", "--", + "build", fixture.to_str().unwrap(), + "-o", out.to_str().unwrap(), + ]) + .current_dir(&root) + .output() + .expect("spawn cargo run ail build"); + assert!( + build.status.success(), + "ail build failed:\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&build.stdout), + String::from_utf8_lossy(&build.stderr) + ); + + // Run + let run = Command::new(&out) + .output() + .expect("spawn produced binary"); + assert!( + run.status.success(), + "binary returned non-zero:\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&run.stdout), + String::from_utf8_lossy(&run.stderr) + ); + assert_eq!( + String::from_utf8_lossy(&run.stdout).trim_end(), + "42", + "expected stdout `42` (with trailing newline)" + ); +} +``` + +If `crates/ail/tests/typeclass_22b3.rs` uses a different +"build + run + capture stdout" idiom, mirror it instead — the +project's existing convention takes priority over this template. + +**Step 2.2 — Run the test** + +``` +cargo test -p ail --test typeclass_22c +``` + +Expected: `1 passed; 0 failed`. + +**Step 2.3 — Run the full bench gate** + +``` +python3 bench/check.py +python3 bench/compile_check.py +python3 bench/cross_lang.py +``` + +Expected: all three exit 0/0/0. + +**Step 2.4 — Commit** + +``` +git add crates/ail/tests/typeclass_22c.rs +git commit -m "iter 22c.2: e2e test — user-class fixture compiles, runs, prints 42" +``` + +--- + +## Task 3: JOURNAL entry + milestone-close handoff + +**Files:** +- Modify: `docs/JOURNAL.md` — append iteration entry. + +**Step 3.1 — Append the iteration entry** + +```markdown + +## 2026-05-09 — Iteration 22c: User-class e2e fixture + +The milestone-22 acceptance fixture for the user-class path. One new +`examples/test_22c_user_class_e2e.ail.json` defines a user class +`Foo a where foo : (a borrow) -> Int`, a user ADT `IntBox = MkIntBox +Int`, an `instance Foo IntBox` whose body matches on the ctor and +returns the wrapped int, and a `main` that calls `foo (MkIntBox 42)` +through the 22b.3 monomorphisation pass and prints the result via +`io/print_int`. Stdout: `42`. + +**Per-task subjects (mirror commit messages):** + +- 22c.1: fixture — user class + user ADT + instance, prints 42 +- 22c.2: e2e test — user-class fixture compiles, runs, prints 42 + +**No production-code changes.** 22b.3 shipped every compiler path +this fixture exercises (mono of class methods, call-site rewrite, +synthesised symbol naming via `__` separator, match lowering inside +synthesised bodies). The fact that 22c lands as a pure-fixture iter +is the strongest possible evidence that the typeclass machinery is +done — there is nothing left to wire. + +**Spec amendments folded into 22b.4a's amendment block:** +22b.4b (Prelude for primitives) dropped from milestone scope; 22c's +fixture re-scoped to vocabulary AILang already has (no `++`, no +named-field record syntax, no top-level polymorphic `print`). See +`docs/specs/2026-05-09-22-typeclasses.md` "Amendments" §"22b.4b +dropped" and §"22c scope tightened". + +**Milestone-22 acceptance check:** + +1. ✓ 22b.1 + 22b.2 + 22b.3 + 22b.4a + 22c JOURNAL entries committed. +2. (pending) Audit suite green — `audit` skill runs next. +3. ✓ 22c user-class fixture compiles, runs, prints `42` (this iter). +4. ✓ Round-trip filter retired in 22b.4a; all `examples/*.ail.json` + pass round-trip. + +**Bench gates:** all three exited 0/0/0. + +**Next step:** dispatch `audit` skill to close the milestone — +architect drift review against `docs/DESIGN.md` plus the three +regression scripts (already 0/0/0 here but `audit` re-runs as +its own gate). On clean audit: milestone 22 closes; queue moves +to whatever's next. On drift: tidy iteration first. +``` + +**Step 3.2 — Commit** + +``` +git add docs/JOURNAL.md +git commit -m "iter 22c: journal entry — milestone-22 acceptance fixture lands" +``` + +--- + +## Acceptance gate (run after Task 3) + +``` +cargo test --workspace +python3 bench/check.py +python3 bench/compile_check.py +python3 bench/cross_lang.py +``` + +All four green is the iteration close. The orchestrator then +dispatches `skills/audit` to close milestone 22. + +--- + +## Self-review checklist (orchestrator, pre-commit) + +1. **Spec coverage:** every spec acceptance criterion has a task + that satisfies it. Criterion 1 (JOURNAL) → Task 3; Criterion 3 + (22c fixture compiles + runs) → Tasks 1+2; Criterion 4 (round-trip) + → already satisfied by 22b.4a. ✓ +2. **Placeholder scan:** grep this file for "TBD" / "TODO" / "fill + in" / "implement later" / "similar to" / "add appropriate". Zero + hits. ✓ +3. **Type consistency:** the fixture's class name, method name, ADT + name, ctor name, instance binding, and main call site all use + `Foo` / `foo` / `IntBox` / `MkIntBox` consistently. ✓ +4. **Step granularity:** every step ≤ 5 minutes. The two ambiguous + steps are 1.2 and 1.3 (verify check + verify build) — they may + surface compiler bugs that aren't 5-minute fixes. The plan + instructs STOP and route to `debug` in that case, which is the + right discipline. ✓