11 KiB
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 §1449–1759. The five committed semantic axes were re-checked against the Feature- Acceptance criterion at the time of writing:
- Haskell-lite scope (multi-method, single-param, optional
defaults, single-superclass) — holds. LLM produces
class Eq a where eq, nenaturally;Ord extends Eqis the textbook case. Multi-param classes and HKTs remain LLM-distant. - Constraints explicit + mandatory — holds. The "alles sichtbar" line is project-zementiert; constraints visible at the function boundary is the LLM-author advantage.
- Orphan-free coherence — holds, with one documentation
note: primitives (Int/Float/Bool/String) have no defining
module —
instance MyClass Intmust therefore live in MyClass's module. Test fixturetest_22b1_orphan_thirdconfirms. Workspace-closed authoring is the right precondition. - Defaults via explicit
defaultkeyword — holds. LLM writesdefault ne x y = not (eq x y)more naturally than mixed class bodies à la Haskell. - Class-param kind
*only — holds. LLM-natural pattern isList.map/Tree.mapper 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 FnDefs 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-instanceduplicate-instancemissing-method
Workspace-load (registry-build) — 22b.2:
missing-superclass-instanceoverriding-non-existent-methodmethod-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(superclasstype≠ class's ownparam)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-constraintrecovery (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 42exercisingprint→Show.show→show@Int). Round-trip filter retires; allexamples/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:
- 22b.1 + 22b.2 + 22b.3 + 22b.4 + 22c JOURNAL entries committed.
- Audit suite (architect drift review against DESIGN.md +
bench/check.py+bench/compile_check.py+bench/cross_lang.py) green. - The 22c user-class fixture compiles, runs, and produces correct stdout — with no Prelude class involvement (ensures the path is independently exercised).
- Round-trip filter
test_22b1_*is removed; allexamples/*.ail.jsonpass 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.