plan: 22c user-class e2e fixture
This commit is contained in:
@@ -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__<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).
|
||||
|
||||
```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. ✓
|
||||
Reference in New Issue
Block a user