convention: counter-prefix file naming across docs/specs/, docs/plans/, design/contracts/, design/models/
All 176 files in the four accumulating directories now use a zero-padded 4-digit counter prefix that reflects creation order (`NNNN-slug.md`). The counter is assigned per directory in strict git-log creation order; ties broken alphabetically by original name. The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is dropped — the date is recoverable from git log and the counter carries the ordering. A file's counter is stable for the life of the file: never reassigned, never reused, never compacted. Deleted files retire their counter; subsequent files do not fill the gap. This is the property that lets cross-references stay literal — refs use the full filename including the counter (`design/contracts/0007-honesty-rule.md`) so they grep cleanly and resolve directly without a glob step. 313 cross-references updated across .md/.rs/.toml/.c/.json files (test pins, include_str! paths, design-INDEX entries, baseline notes, runtime C comments, inter-contract markdown links incl. bare basename and `../models/foo.md` forms). CLAUDE.md gets a new "File-naming convention" section spelling out the rule and rationale. skills/brainstorm/SKILL.md and skills/planner/SKILL.md updated so new spec/plan creation produces counter-prefixed names from the start. The full test suite (cargo test --workspace) passes.
This commit is contained in:
@@ -0,0 +1,395 @@
|
||||
# 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 | shipped 2026-05-09 | 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 | shipped 2026-05-09 | Monomorphisation pass — synthesised `FnDef`s from `(method, type-hash)` pairs, call rewriting, cache by key. Synthetic class+instance fixture for e2e validation. `__` separator chosen over `#` for LLVM IR identifier legality. | Synthetic fixture compiles, runs, correct stdout; bench gates green |
|
||||
| 22b.4a | shipped 2026-05-09 | Form-A parser+printer arms for ClassDef / InstanceDef; `Type::Forall.constraints` round-trip gap fix; round-trip skip-list retired (70 → 106 fixtures green). | Round-trip green for every `examples/*.ail.json`; bench gates green |
|
||||
| 22b.4b | dropped (post-22) | Originally: Prelude module Show#Int + `int_to_str` C-runtime primitive. Removed from milestone-22 scope per Amendments §"22b.4b dropped" — substantively separable runtime work; LLM-utility for primitive Show is weak. | (deferred) |
|
||||
| 22c | shipped 2026-05-09 | Single end-to-end fixture: `class Foo a` + `data IntBox` + `instance Foo IntBox` + `main` printing 42. Surfaced and fixed a mono-pass user-ADT bug as part of close. | Fixture compiles, runs, correct stdout; bench gates 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.
|
||||
|
||||
## Amendments
|
||||
|
||||
### 2026-05-09 — Iteration split: 22b.4 → 22b.4a + 22b.4b
|
||||
|
||||
Original 22b.4 scope ("Prelude module Show/Eq/Ord on Int/Float/Bool/String,
|
||||
print rewiring, Form-B parser/printer arms, round-trip filter retired")
|
||||
splits into two iterations:
|
||||
|
||||
- **22b.4a (this iter):** Form-A parser+printer arms for ClassDef
|
||||
and InstanceDef; round-trip skip-list retired for `test_22b1_*`,
|
||||
`test_22b2_*`, `test_22b3_*`. Pure surface plumbing, no codegen
|
||||
or runtime change.
|
||||
- **22b.4b (queued):** Prelude module containing `class Show a where
|
||||
show : (a borrow) -> Str`, `instance Show Int`, the new
|
||||
`int_to_str` C-runtime primitive, codegen wiring for it, and an
|
||||
end-to-end `show 42` fixture that prints `42` through the existing
|
||||
mono pass.
|
||||
|
||||
Substantive rationale: 22b.4a touches one crate (`ailang-surface`)
|
||||
with no runtime/codegen risk; 22b.4b touches `runtime/`,
|
||||
`ailang-codegen`, `ailang-check::builtins`, plus a new fixture, with
|
||||
a different review surface and a different bench-gate exposure.
|
||||
Bundling them was a brainstorm-time underestimate of the
|
||||
runtime-side work; running them as separate iterations preserves
|
||||
the bite-sized cycle the skill system enforces.
|
||||
|
||||
### 2026-05-09 — Form-A vs Form-B terminology fix
|
||||
|
||||
The original 22 spec ("Form-B parser/printer arms for ClassDef
|
||||
/InstanceDef") and the 22b.1 round-trip-skip-list comment both
|
||||
loosely call the s-expression projection "Form-B". That is a
|
||||
terminology error: `crates/ailang-surface` is **Form-A** (the
|
||||
parseable s-expression). `crates/ailang-prose` is **Form-B**
|
||||
(human-readable, one-way projection — no parser by design). The
|
||||
22b.4a arms are Form-A. Form-B printer arms for ClassDef /
|
||||
InstanceDef are NOT required for the round-trip gate (it is
|
||||
unparser-side); they are queued as an audit-cycle nicety, not as
|
||||
a milestone-22 acceptance criterion.
|
||||
|
||||
The skip-list comment that read "the surface (Form-B) parser/printer
|
||||
arms" was retired alongside the filter (Task 5).
|
||||
|
||||
### 2026-05-09 — Mono-symbol separator: `__` chosen over `#`
|
||||
|
||||
22a's brainstorm recommended `<method>#<typesurfacename>` for the
|
||||
mono-symbol naming format with the implementer free to choose. 22b.3
|
||||
implemented `<method>__<typesurfacename>` instead (e.g. `show__Int`,
|
||||
`eq__Bool`). Reason: `#` is an invalid character in LLVM IR global
|
||||
identifiers (`@ail_..._foo#Int_clos` is rejected by the LLVM
|
||||
verifier). `__` is verifier-legal, identifier-legal in C-glue
|
||||
contexts, and parses unambiguously into method + type surface name
|
||||
(neither method names nor type surface names contain `__` by
|
||||
convention). The recommendation in §"Open commitments (22a → 22b
|
||||
implementer)" is updated to read `<method>__<typesurfacename>`.
|
||||
|
||||
Hash-suffix form for compound types (`<method>__<8-hex>`) follows
|
||||
the same separator.
|
||||
|
||||
### 2026-05-09 — Forall-constraints surface arm gap (22b.4a.4.5)
|
||||
|
||||
Surfaced during 22b.4a Task 5: the existing Form-A parser/printer
|
||||
arms dropped `Type::Forall.constraints` (the field added in 22b.2 to
|
||||
carry class constraints on polymorphic types). Four `test_22b2_*`
|
||||
fixtures regressed when the round-trip skip-list was lifted. Fixed
|
||||
in 22b.4a.4.5 by extending `parse_forall_type` and the `Type::Forall`
|
||||
arm of `write_type` to handle an optional `(constraints (constraint
|
||||
<Class> <type>)+ )` clause. Round-trip is now bit-stable for every
|
||||
forall-with-constraints fixture in `examples/`.
|
||||
|
||||
This was not in the original 22b.4 plan because the brainstorm did
|
||||
not anticipate the gap — the skip-list was thought to mask only the
|
||||
ClassDef/InstanceDef arms, not the forall-constraints arm too. The
|
||||
forall-constraints fix is properly part of 22b.4a (cannot retire the
|
||||
filter without it) and is recorded here for completeness.
|
||||
|
||||
### 2026-05-09 — Acceptance criteria 4 still binding
|
||||
|
||||
Acceptance criterion 4 ("Round-trip filter `test_22b1_*` is removed;
|
||||
all `examples/*.ail.json` pass round-trip") is satisfied by 22b.4a.
|
||||
The `test_22b2_*` and `test_22b3_*` filters added subsequently are
|
||||
also retired here. No further round-trip gate work is required for
|
||||
22 milestone close.
|
||||
|
||||
### 2026-05-09 — 22b.4b dropped: Prelude-for-primitives deferred to post-22
|
||||
|
||||
The 22b.4 → 22b.4a + 22b.4b split (recorded above) further reduces:
|
||||
22b.4b is removed from milestone-22 scope entirely. Acceptance
|
||||
criterion 1 amends to read "22b.1 + 22b.2 + 22b.3 + 22b.4a + 22c
|
||||
JOURNAL entries committed".
|
||||
|
||||
Substantive rationale, per the project's feature-acceptance
|
||||
criterion (LLM utility):
|
||||
|
||||
1. **No runtime primitive for `int_to_str` yet.** Show#Int's body
|
||||
needs to convert an `i64` to a heap-allocated `Str`. AILang
|
||||
currently only has constant interned strings (`@.str_*` LLVM
|
||||
globals). A heap-string ABI — runtime alloc, RC-count, drop —
|
||||
is independently new infrastructure. Wiring it through a class
|
||||
method first stacks risk: typeclass cascade + heap-string ABI
|
||||
+ new C-runtime function + new codegen arm in one iter.
|
||||
|
||||
2. **Low LLM-utility for primitive Show.** An LLM that needs to
|
||||
render an `Int` writes `int_to_str x` (a direct primitive call)
|
||||
more naturally than `show x` (which routes through a Prelude
|
||||
class with one instance). Class dispatch shines when the LLM
|
||||
defines a user type and wants polymorphism; for primitives, the
|
||||
Prelude class is a thin wrapper with negative entropy (extra
|
||||
indirection, no new property).
|
||||
|
||||
3. **22c covers the user-class e2e independently.** The milestone's
|
||||
typeclass-machinery property is exercised end-to-end by 22c's
|
||||
user-class fixture (class Foo a + data Bar + instance Foo Bar
|
||||
+ main calling foo). The Prelude path adds no semantic capability
|
||||
beyond what 22c demonstrates.
|
||||
|
||||
When does Prelude-for-primitives return? When concrete LLM-author
|
||||
code surfaces a case that genuinely benefits from `Show a` /
|
||||
`Eq a` / `Ord a` over per-type primitive functions — that case
|
||||
becomes the brainstorm trigger for a post-22 Prelude milestone.
|
||||
Until then, `int_to_str` ships as a plain primitive (or not at
|
||||
all) the day a fixture needs it.
|
||||
|
||||
### 2026-05-09 — 22c scope tightened to vocabulary AILang already has
|
||||
|
||||
The original 22c spec (§"Components" row) sketched the e2e fixture
|
||||
as `class Greet a where greet : a borrow -> String` + `data Person
|
||||
{ name : String }` + `instance Greet Person where greet p = "Hello,
|
||||
" ++ p.name`. That sketch references three constructs AILang does
|
||||
not currently support:
|
||||
|
||||
- **`++` for `Str`** — no concat operator in `crates/ailang-check/src/builtins.rs::list()`.
|
||||
- **Named-field record syntax `Person { name = "world" }`** — AILang's `data`/`ctor` form is positional (`(ctor MkPerson Str)`), not record-with-named-fields.
|
||||
- **`print` polymorphic on `Show`** — see preceding amendment.
|
||||
|
||||
22c's acceptance property is "user-defined class + user-defined data
|
||||
type + instance + call site, monomorphised, runs, prints right value".
|
||||
That property holds with any vocabulary AILang has. The fixture is
|
||||
re-scoped to:
|
||||
|
||||
```
|
||||
class Foo a where
|
||||
foo : (a borrow) -> Int
|
||||
|
||||
data IntBox = MkIntBox Int
|
||||
|
||||
instance Foo IntBox where
|
||||
foo = λb. (match b { MkIntBox v => v })
|
||||
|
||||
fn main : () -> Unit !IO
|
||||
body: do io/print_int (foo (MkIntBox 42))
|
||||
```
|
||||
|
||||
Stdout: `42\n`. No new primitives, no new surface syntax, no new
|
||||
codegen paths beyond what 22b.3 shipped. The property the original
|
||||
sketch was reaching for (typeclass dispatch over a user-defined ADT,
|
||||
end-to-end, with a hand-readable expected output) holds identically.
|
||||
Reference in New Issue
Block a user