iter design-md-rolesplit.1 (DONE 9/9): DESIGN.md -> design/ ledger role-split
The 3020-line docs/DESIGN.md is replaced by the design/ ledger:
design/INDEX.md (sole addressable spine, typed Contracts+Models tables,
polymorphic links — prose file OR authoritative source //!), 14
design/contracts/*.md test-linked invariants + 3 source-link-only
contracts (mangling/env-construction/qualified-xref, no prose file —
code is SoT), 5 design/models/*.md whitepapers, and
docs/journals/2026-05-19-design-decision-records.md (the
relitigation-guard archive — every why/rejected/does-not-do/rollback/
empirical ### moved out at ###-granularity). Clean cut: git rm
docs/DESIGN.md, no stub.
RED-first crates/ailang-core/tests/design_index_pin.rs — the 4-clause
anti-regrowth spine (DESIGN.md-gone / every-INDEX-link-resolves /
every-contract-names-a-resolvable-ratifier /
contracts-carry-no-decision-record-prose) — demonstrably RED before,
GREEN after. Build-atomic by task ordering: design_schema_drift.rs's
include_str! (the only compile-time consumer) retargeted to
design/contracts/data-model.md BEFORE the deletion; its
## Data model/## Pipeline slicer dropped (a simplification the split
enables). 2 NoInstance diagnostics + 2 lockstep E2Es retargeted to
design/contracts/{float-semantics,typeclasses}.md. ~12 agent reading
lists + 5 SKILL bodies + CLAUDE.md + skills/README.md + ~25
code/C/.ail/spec comment xrefs retargeted; OQ7 dangling 'Iter 13b'
cite deleted (no forward target — a pointer would be fiction).
honesty-rule.md rewritten so the rule names the new home
(rationale->journals), resolving the recon-found internal
contradiction; the two docs_honesty_pin.rs:70,72 pinned phrases kept
verbatim+contiguous.
Boss-verified independently: cargo test --workspace 646 passed /
0 failed; design_index_pin 4/4; acceptance grep CLEAN of live
DESIGN.md refs (residuals = only the spec-mandated clause-4
deletion-enforcer). 2 DONE_WITH_CONCERNS routed to the mandatory
milestone-close audit: (a) str-abi.md:23 '(iter str-concat,
2026-05-13)' provenance stamp trips advisory architect_sweeps Sweep-1
— Boss-confirmed byte-identical to DESIGN.md@deeffb1:2062-2065, a
faithfully-migrated PRE-EXISTING anchor (regexes verbatim, only path
retargeted), NOT split-introduced — RATIFY-or-tidy at audit; (b) a
now stale-direction intra-prose 'see Str ABI below' cross-ref in
float-semantics.md — audit-adjudication candidate. Plan defect noted:
Task 9 Step 4's verbatim acceptance grep used a ^./ anchor not
matching the system's grep -rIn output; substance re-verified CLEAN.
Spec grounding-check PASS x2. Journals INDEX + decision-records
pointer appended (Boss-only).
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
# Typeclasses — resolution and monomorphisation whitepaper
|
||||
|
||||
## Decision 11: typeclasses — Haskell-lite, monomorphised, coherent
|
||||
|
||||
**The design pass for typeclasses. Codified after the Feature-acceptance
|
||||
criterion (this document, above) was committed; the criterion is
|
||||
the explicit basis for the choices below.**
|
||||
|
||||
AILang ships typeclasses to compress a real LLM-author redundancy:
|
||||
without them, every comparable function must be written per-type
|
||||
(`int_eq`, `string_eq`, `bool_eq`, `int_show`, `string_show`, …). With
|
||||
typeclasses behind a monomorphising compiler, the LLM author writes
|
||||
one signature with a class constraint and one method per concrete
|
||||
type, and the compiler emits the same machine code as the per-type
|
||||
version. No runtime cost, no dictionary passing, no vtables.
|
||||
|
||||
**Choice.** A deliberately narrow typeclass design — narrower than
|
||||
Haskell, narrower than Rust traits — calibrated to what an LLM
|
||||
author naturally produces. Five semantic axes are committed:
|
||||
|
||||
1. **Scope.** Multi-method, single-parameter, optional defaults,
|
||||
single-superclass relation. No multi-param classes, no
|
||||
functional dependencies, no associated types.
|
||||
2. **Constraints in signatures.** Explicit and mandatory. A
|
||||
function that calls a class method must declare the constraint
|
||||
in its `forall` block. No constraint inference.
|
||||
3. **Resolution.** Orphan-free coherence. An `instance C T` may be
|
||||
declared only in the module of `C` or in the module of `T`.
|
||||
Resolution is global type-directed against a workspace-built
|
||||
registry; coherence makes the lookup unambiguous.
|
||||
4. **Defaults.** Opt-in via an explicit `default` keyword in the
|
||||
class body. Methods without `default` are abstract-required;
|
||||
methods with `default` may be overridden or inherited per
|
||||
instance.
|
||||
5. **Class-parameter kind.** Concrete types only (kind `*`). No
|
||||
higher-kinded class params; `Functor`/`Monad`-style abstractions
|
||||
over type constructors are not expressible. The LLM-natural
|
||||
pattern is `List.map` / `Tree.map` as separate functions per
|
||||
type, which monomorphisation handles directly.
|
||||
|
||||
The five axes follow from the Feature-acceptance criterion: each
|
||||
rejected mechanism (multi-param, higher-kinded, FunDeps, assoc
|
||||
types) is one an LLM author does not unprompted produce.
|
||||
|
||||
## Resolution and monomorphisation
|
||||
|
||||
**Constraint collection (per function body).** During typechecking
|
||||
of a body, each method call generates a residual constraint of shape
|
||||
`<Class> <Type>` where `<Type>` may still contain type variables.
|
||||
After local typechecking, residual constraints are checked against
|
||||
the function's declared constraints (modulo α-conversion and modulo
|
||||
auto-expansion through superclasses; see below). Any residual not
|
||||
covered by declared constraints fires `MissingConstraint`.
|
||||
|
||||
**Instance registry (workspace-global).** At workspace load (see
|
||||
`crates/ailang-core/src/workspace.rs`), all `InstanceDef` nodes
|
||||
across all reachable modules are collected into a registry keyed by
|
||||
`(class-name, canonical-hash-of-instance-type)`. Registry build
|
||||
performs three checks:
|
||||
|
||||
- **Coherence.** Each instance's module must be either the class's
|
||||
defining module or the instance type's defining module. Otherwise
|
||||
→ `OrphanInstance`.
|
||||
- **Uniqueness.** No two entries share a key. Otherwise →
|
||||
`DuplicateInstance`.
|
||||
- **Method completeness.** Each instance specifies every required
|
||||
(non-default) method of its class. Otherwise → `MissingMethod`.
|
||||
|
||||
Registry build is a one-time-per-build pass that fires before any
|
||||
typechecking. Its errors are workspace-load errors, not per-call-site
|
||||
errors.
|
||||
|
||||
**Resolution at call sites with concrete types.** When the typechecker
|
||||
sees a method call where every type variable in the constraint is
|
||||
substituted to a concrete type, it queries the registry. Hit →
|
||||
resolved. Miss → `NoInstance`.
|
||||
|
||||
**Resolution at polymorphic call sites.** When type variables are
|
||||
still free, the constraint propagates into the surrounding function's
|
||||
constraint context — which the user MUST have declared explicitly
|
||||
(per axis 2). No constraint is implicitly hoisted.
|
||||
|
||||
**Monomorphisation (post-typecheck, pre-codegen).** A pass between
|
||||
typechecking and codegen replaces every call to a
|
||||
`Type::Forall`-quantified `Def::Fn` with a call to a synthesised
|
||||
monomorphic `FnDef`. Two source-body entry points share the same
|
||||
mechanics in one fixpoint:
|
||||
|
||||
1. **Class-method entry.** For each unique `(method, concrete-type)`
|
||||
pair produced by a class-constraint residual, the pass looks up
|
||||
the resolved instance body via `Registry::entries[(class,
|
||||
type-hash)]`, substitutes the class parameter to the concrete
|
||||
type, and synthesises a top-level `FnDef` named
|
||||
`<method>__<type-surface-name>`.
|
||||
2. **Free-fn entry.** For each call site to a polymorphic free
|
||||
`Def::Fn` with a fully-concrete substitution, the pass takes the
|
||||
source body directly from the polymorphic `Def::Fn`, applies
|
||||
rigid-var substitution on both the type AND the body (the body
|
||||
may contain inner `Term::Lam`s whose `param_tys` reference the
|
||||
outer Forall vars), and synthesises a top-level `FnDef` named
|
||||
`<name>__<type-surface-name-1>__<type-surface-name-2>__…`
|
||||
(concatenated in `Type::Forall.vars` declaration order; the
|
||||
N-ary case extends the single-type-var class-method shape
|
||||
bit-stably).
|
||||
|
||||
Both arms share:
|
||||
|
||||
- A fixpoint loop that keeps collecting targets until a round adds
|
||||
nothing new (a synthesised free-fn body may invoke class methods
|
||||
at concrete types, scheduling new class-method targets; a
|
||||
class-method body may invoke polymorphic free fns at concrete
|
||||
types, scheduling new free-fn targets).
|
||||
- A dedup cache keyed by `(kind, base-name,
|
||||
type-hash-or-joined-hashes)` where the first component
|
||||
(`"class"` / `"free"`) guarantees disjoint keying across the
|
||||
two kinds.
|
||||
- A call-site rewrite walker that rewrites bare polymorphic call
|
||||
sites — class-method-named OR poly-free-fn-named — to their
|
||||
mono symbols before codegen runs. The walker advances a single
|
||||
cursor over interleaved class-method and free-fn slots emitted
|
||||
in synth's traversal order.
|
||||
|
||||
After this pass, the IR contains no polymorphism, no class
|
||||
machinery, no polymorphic call sites — only ordinary monomorphic
|
||||
functions and direct calls. Codegen sees no difference between a
|
||||
hand-written `show_int` and a synthesised `show__Int`.
|
||||
|
||||
**Why mono, not virtual dispatch.** Monomorphisation makes the call
|
||||
target visible to the optimiser, unlocking inlining and downstream
|
||||
loop transformations that virtual dispatch prevents in principle.
|
||||
On a saturating branch predictor with a monomorphic indirect
|
||||
target, the indirect call itself is comparable in cost to a
|
||||
non-inlined direct call — the win is in what the optimiser can do
|
||||
with the visible target, not in the call instruction. The
|
||||
end-to-end gain shrinks toward zero on larger callee bodies and
|
||||
cold call sites, but the architectural claim — "mono enables
|
||||
optimisations vdisp forbids" — holds across the spectrum
|
||||
(`bench/mono_dispatch.py` and the corresponding JOURNAL bench-notes
|
||||
entry record the measured ratios).
|
||||
|
||||
The separator is `__` rather than `#` or `@` because `#` and `@`
|
||||
are invalid in LLVM IR global identifiers (the IR verifier rejects
|
||||
them inside `@ail_<module>_<def>` mangled names). `__` is legal in
|
||||
both LLVM IR and the C ABI used by the runtime glue, and parses
|
||||
unambiguously into `<method>__<type-surface-name>` because neither
|
||||
component contains `__` by project convention.
|
||||
|
||||
**No runtime dispatch, no dictionary passing.** The monomorphisation
|
||||
pass is the ONLY specialiser. Codegen sees only monomorphic
|
||||
`Def::Fn`s and direct calls; the pre-iter-23.4 codegen-time
|
||||
specialiser (`lower_polymorphic_call` + `module_polymorphic_fns` +
|
||||
`mono_queue`) was removed in iter 23.4. A call that cannot be
|
||||
monomorphised — for instance, because a constraint remains
|
||||
unresolved at the entry point — is a static error, not a runtime
|
||||
one. This is the LLVM-friendly form and is consistent with
|
||||
Decision 10's performance commitment.
|
||||
Reference in New Issue
Block a user