Files
AILang/docs/specs/2026-05-09-22-typeclasses.md
T

11 KiB
Raw Blame History

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 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-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 printShow.showshow@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.