Files
AILang/docs/plans/0005-22c-user-class-e2e.md
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

13 KiB

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__<typehash> 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).

{
  "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:

//! 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


## 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. ✓