spec: 22 milestone — typeclasses retrospective

This commit is contained in:
2026-05-09 16:27:50 +02:00
parent 9b97a862f6
commit 71dea8eab6
+237
View File
@@ -0,0 +1,237 @@
# 22 — Typeclasses Milestone — Design Spec
**Date:** 2026-05-09
**Status:** Draft — awaiting user spec review
**Authors:** Brummel (orchestrator) + Claude
## Goal
Ship typeclasses end-to-end as committed in DESIGN.md Decision 11
("typeclasses — Haskell-lite, monomorphised, coherent"). Validate
the user-defined-class path works end-to-end before declaring the
milestone closed — not just the Prelude path.
This is a retrospective milestone spec. 22a (decision iter) and
22b.1 (schema floor + workspace registry) shipped before the
brainstorm-skill pipeline existed. The spec ratifies the existing
design where it holds and corrects iter-slicing where Feature-
Acceptance points to a gap.
## Architecture
Design is fully specified in DESIGN.md Decision 11 §14491759. The
five committed semantic axes were re-checked against the Feature-
Acceptance criterion at the time of writing:
1. **Haskell-lite scope** (multi-method, single-param, optional
defaults, single-superclass) — holds. LLM produces `class Eq a
where eq, ne` naturally; `Ord extends Eq` is the textbook case.
Multi-param classes and HKTs remain LLM-distant.
2. **Constraints explicit + mandatory** — holds. The "alles
sichtbar" line is project-zementiert; constraints visible at
the function boundary is the LLM-author advantage.
3. **Orphan-free coherence** — holds, with one documentation
note: primitives (Int/Float/Bool/String) have no defining
module — `instance MyClass Int` must therefore live in
MyClass's module. Test fixture `test_22b1_orphan_third`
confirms. Workspace-closed authoring is the right precondition.
4. **Defaults via explicit `default` keyword** — holds. LLM
writes `default ne x y = not (eq x y)` more naturally than
mixed class bodies à la Haskell.
5. **Class-param kind `*` only** — holds. LLM-natural pattern is
`List.map`/`Tree.map` per type, not Functor abstraction.
Monomorphisation handles per-type directly.
No axis falls. Decision 11 stays unchanged. The brainstorm
correction is to iter-slicing, not to axis content.
## Components (iterations)
| Iter | Status | Scope | Closes when |
|---|---|---|---|
| 22b.1 | shipped 2026-05-09 | Schema floor (`ClassDef`, `InstanceDef`, `FnDef.type` extension), workspace registry with three coherence checks (orphan/duplicate/missing-method), hash-stability proof, fixture filter for round-trip gate. | JOURNAL entry committed (done) |
| 22b.2 | queued | Typecheck arms: `FnDef.type.constraints` field activation; class-schema validation (`kind-mismatch`, `invalid-superclass-param`, `constraint-references-unbound-type-var`); per-call-site `missing-constraint` and `no-instance`; `overriding-non-existent-method`, `method-name-collision`, `missing-superclass-instance`. | All new diagnostics fire on dedicated fixtures; bench gates green |
| 22b.3 | queued | Monomorphisation pass — synthesised `FnDef`s from `(method, type-hash)` pairs, call rewriting, cache by key. **Synthetic class+instance fixture** in the test suite for end-to-end validation of the mono pass before the Prelude lands. | Synthetic fixture compiles, runs, correct stdout; bench gates green |
| 22b.4 | queued | Prelude module (Show/Eq/Ord on Int/Float/Bool/String), `print` rewiring through `Show.show`, Form-B parser/printer arms for ClassDef/InstanceDef, `test_22b1_*` round-trip filter retired. | Prelude fixture compiles, runs; round-trip green for all `examples/`; bench gates green |
| 22c | **promoted** (was deferred) | Single end-to-end fixture: user defines `class Foo a`, user defines `data Bar`, user writes `instance Foo Bar`, calls `foo b`. Bench gate. JOURNAL entry = milestone close. | Fixture compiles, runs, correct stdout; full audit suite green |
22c was deferred in 22a's queue. This brainstorm flagged it as a
shipping gap: the milestone is "typeclasses" but the Prelude path
alone (compiler-internal setup) does not validate that an LLM-author
can in fact define a class and an instance and have them work.
Promoting 22c with tight scope (one fixture, no `deriving`) closes
the gap without expanding the milestone substantially.
22b.3's synthetic fixture is the matching addition: monomorphisation
must be testable before the Prelude lands, so its iter cannot rely
on 22b.4 for end-to-end validation.
## Data flow
Per DESIGN.md Decision 11 §"Resolution and monomorphisation":
```
.ail.json modules
↓ workspace load
↓ ↓ collect ClassDef per (class-name → defining-module)
↓ ↓ collect InstanceDef per (class-name, type-hash)
↓ ↓ coherence + uniqueness + completeness + superclass checks
↓ Workspace { defs, registry }
↓ per-FnDef typecheck
↓ ↓ collect residual constraints from method calls
↓ ↓ check residuals against declared (+ superclass-expanded)
↓ ↓ resolve fully-concrete constraints against registry
↓ monomorphisation pass
↓ ↓ for each (resolved-method, concrete-type) pair:
↓ ↓ synthesise FnDef with hash-deterministic name
↓ ↓ substitute class param to concrete type in body
↓ ↓ rewrite original Call to target synthesised name
↓ codegen sees only ordinary monomorphic FnDefs and direct calls
LLVM IR
```
No runtime dispatch, no dictionary passing. A class-method call
that cannot be monomorphised (constraint unresolved at entry point)
is a static error, not a runtime one.
## Error handling
Diagnostic catalogue, by phase. Codes follow the project's existing
kebab-case CLI convention.
**Workspace-load (registry-build) — landed in 22b.1:**
- `orphan-instance`
- `duplicate-instance`
- `missing-method`
**Workspace-load (registry-build) — 22b.2:**
- `missing-superclass-instance`
- `overriding-non-existent-method`
- `method-name-collision` (across two in-scope classes, or class
method vs top-level function)
**Class-schema validation — 22b.2:**
- `kind-mismatch` (class param appears in applied position in any
method signature)
- `invalid-superclass-param` (superclass `type` ≠ class's own `param`)
- `constraint-references-unbound-type-var`
**Per-FnDef typecheck — 22b.2:**
- `missing-constraint` (residual not covered by declared +
superclass-expanded constraints)
- `no-instance` (fully concrete constraint with no registry entry)
No `ambiguous-instance` diagnostic — coherence makes registry keys
globally unique by construction.
## Testing strategy
**Per iter:**
- **22b.1** — already shipped. Schema-extension hash-stability
tests + 5 registry-diagnostic fixtures.
- **22b.2** — one fixture per new diagnostic (one-fixture-one-
diagnostic rule). Plus a positive fixture exercising
`missing-constraint` recovery (constraint declared correctly
→ green).
- **22b.3** — synthetic class+instance fixture exercising the
mono pass end-to-end. Hand-written `class Foo a where foo : a
-> Int` + `instance Foo Int where foo i = i` + `fn main = print
(foo 5)`. Verifies the synthesised def is emitted, called, and
produces "5". Plus unit tests on the mono cache (no double
emission).
- **22b.4** — Prelude fixture (`fn main = print 42` exercising
`print``Show.show``show@Int`). Round-trip filter retires;
all `examples/test_22b1_*` plus the new prose round-trip passes.
- **22c** — single user-class e2e: `class Greet a where greet :
a borrow -> String` + `data Person { name : String }` +
`instance Greet Person where greet p = "Hello, " ++ p.name` +
`fn main = print (greet (Person { name = "world" }))`.
**Bench gates:** all three (`bench/check.py`,
`bench/compile_check.py`, `bench/cross_lang.py`) green at every
iter close. The mono pass is a no-op for fixtures without classes —
pre-22b modules stay bit-identical through compile.
## Acceptance criteria
The 22 milestone closes when **all** of the following hold:
1. 22b.1 + 22b.2 + 22b.3 + 22b.4 + 22c JOURNAL entries committed.
2. Audit suite (architect drift review against DESIGN.md +
`bench/check.py` + `bench/compile_check.py` +
`bench/cross_lang.py`) green.
3. The 22c user-class fixture compiles, runs, and produces correct
stdout — with no Prelude class involvement (ensures the path is
independently exercised).
4. Round-trip filter `test_22b1_*` is removed; all
`examples/*.ail.json` pass round-trip.
## Out of scope (deferred, with substantive rationale)
**Operator routing** (`==`, `<`, `<=`, `>`, `>=` through `Eq`/`Ord`).
Deferred to a post-22 milestone bundled with bench re-baselining.
Rationale: the entire bench corpus typechecks differently when
operators route through classes; the new monomorphised trampolines
may codegen non-equivalently to the primitive form, which is a new
baseline rather than a regression. Re-baselining is its own work,
not an iter detail. The 22a "would risk firing the bench gate"
phrasing was an effort-rationale; the substantive form is the new-
baseline argument above.
**`deriving`.** Deferred to a post-22 milestone, contingent on 22
shipping clean. Rationale: `deriving` requires a generic-synthesis
pass at workspace-load (parsing `deriving (Eq, Show)`, synthesising
`InstanceDef` AST nodes, feeding them into the same coherence
pipeline as hand-written instances). That is new infrastructure,
not a 22-arc detail. Synthesising instances for a typeclass
mechanism whose user-class path has not been e2e-validated would
also stack risk.
**`Num` class.** No concrete promotion path. Decision 11 rationale
holds (class-based numeric overloading invokes literal-defaulting,
which axis-7 excluded). Numeric operations stay primitive and
per-type. Trigger for re-evaluation: a concrete user case that
cannot be expressed otherwise.
## Open commitments (22a → 22b implementer)
Three items 22a explicitly punted to the 22b implementer. The
brainstorm position on each:
**Mono-symbol naming format.** Implementer call in 22b.3.
Constraints: must contain the method name and a type indicator
legible in diagnostics; must be hash-stable; must coexist with the
existing mangling scheme `@ail_<module>_<def>`. Recommendation:
`<method>#<typesurfacename>` for primitive types (`show#Int`),
hash-suffix for compound types — but the implementer chooses.
**Form-B prose projection for ClassDef/InstanceDef.** Implementer
call in 22b.4. Constraints: must round-trip exactly with the JSON
form; must follow the existing C-like prose convention.
Recommendation: `class <Name> <param> { <method> : <FnSig> }` and
`instance <Class> <Type> { <method> = <body> }` parallel to existing
FnDef/DataDef forms — but the implementer chooses.
**Mode annotations on class methods.** Not a "convention" in the
strict sense — class method signatures are full FnSigs and carry
mode annotations per Decision 10. Position for the Prelude (22b.4):
`borrow` for all read-only methods (`show`, `eq`, `ne`, `lt`, `le`,
`gt`, `ge`). User-defined classes choose modes per method.
## Known costs
**Reserved-name footprint of the Prelude.** Once 22b.4 ships, the
following names are class-method-occupied at the workspace top
level: `show`, `eq`, `ne`, `lt`, `le`, `gt`, `ge`. A user-defined
function or class method with any of these names fires
`method-name-collision`. The cost is a small lexicon of forbidden
names; the benefit is no namespace fragmentation across the Prelude
classes.