From 71dea8eab65c0674aac07fa87115a5c7d558688f Mon Sep 17 00:00:00 2001 From: Brummel Date: Sat, 9 May 2026 16:27:50 +0200 Subject: [PATCH] =?UTF-8?q?spec:=2022=20milestone=20=E2=80=94=20typeclasse?= =?UTF-8?q?s=20retrospective?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/specs/2026-05-09-22-typeclasses.md | 237 ++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 docs/specs/2026-05-09-22-typeclasses.md diff --git a/docs/specs/2026-05-09-22-typeclasses.md b/docs/specs/2026-05-09-22-typeclasses.md new file mode 100644 index 0000000..e1b9d40 --- /dev/null +++ b/docs/specs/2026-05-09-22-typeclasses.md @@ -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 §1449–1759. 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__`. Recommendation: +`#` 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 { : }` and +`instance { = }` 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.