# 22c — User-class e2e fixture > **Parent spec:** `docs/specs/0002-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/0002-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. ✓