Iter 13c: docs for parameterised ADTs

DESIGN.md: removes "No parameterised ADTs" from the gap list,
adds an entry under "what is supported" that names the per-use-
site substitution scheme and the `llvm_type(Type::Var)` hard-error
defence. Smoke-test list extended with `box.ail.json` and
`maybe_int.ail.json`. Boundary snapshot moved from "end of Iter 12"
to "end of Iter 13".

JOURNAL.md: single Iter 13 entry covering 13a/b/c. Records the
hash-invariant regression test, the architect-flagged debt I
deliberately did not touch (poly-fn-as-value asymmetry, builtins
triple-source, `synth_arg_type` shortcuts on If/Match), the
KISS observation that 13b's "no mono-queue for types" was the
right call, and the process note that this was the first iter
worked strictly through `/agents/` after the role pin in 3df943d.
Plan iteration 14 queued: list_map-as-`List a` rewrite, GC/arena,
poly-fn-as-value.
This commit is contained in:
2026-05-07 14:47:41 +02:00
parent 1631f6065c
commit 3d9fbc68c6
2 changed files with 141 additions and 6 deletions
+19 -6
View File
@@ -244,16 +244,11 @@ ail run <module> — build + execute (tempdir), passthrough exit
## What is not (yet) supported
Snapshot of the boundary at the end of Iter 12. Items move out of this list
Snapshot of the boundary at the end of Iter 13. Items move out of this list
as iterations land; the JOURNAL records the exact iteration.
- No effect handlers — only the built-in IO and Diverge ops.
- No refinements / SMT escalation.
- No parameterised ADTs. `List a` / `Maybe a` aren't expressible —
ADTs are concrete (`IntList`, `Maybe_Int`, ...). Polymorphism over
primitives and fn-typed values works, but a generic `map :: forall
a b. ((a) -> b, List a) -> List b` is blocked on this. Queued
next.
- No HM inference inside bodies. Top-level def types are explicit;
polymorphism is opt-in via `Type::Forall { vars, body }`. Inside
a body, lambdas check monomorphically against their declared type.
@@ -295,6 +290,18 @@ What **is** supported (and used as the smoke test for the pipeline):
demand: each unique instantiation emits a specialised LLVM fn
mangled `@ail_<m>_<def>__<descriptor>` (e.g. `id__I` for `id` at
`Int`, `apply__I_I` for `apply` at `(Int, Int)`).
- **Parameterised ADTs** (Iter 13). `TypeDef.vars: Vec<String>`
declares type parameters; `Type::Con.args: Vec<Type>` carries the
type arguments at use sites. Both fields default to empty and are
skipped during serialization, so canonical-JSON hashes of every
pre-13a definition stay bit-identical (regression test in
`crates/ailang-core/src/hash.rs`). Ctor and match codegen stay
inline at every use site — there is no specialised ADT symbol —
but LLVM field types are derived per use site by substituting
through `cdef.ail_fields`. The substitution is read off the call's
arg types (ctor) or the scrutinee's `Type::Con.args` (match). An
unresolved `Type::Var` reaching `llvm_type` is a hard error
rather than a silent fallback to `ptr`.
Pipeline regression smoke tests:
@@ -310,3 +317,9 @@ Pipeline regression smoke tests:
identity at `Int` and `Bool`; two specialised fns emitted).
- `examples/poly_apply.ail.json` → prints 42 (polymorphic `apply`
with a fn-typed parameter; `apply(succ, 41)`).
- `examples/box.ail.json` → prints 42 (parameterised ADT round-
trip: `MkBox(42)` constructed, then projected by a polymorphic
`unbox : forall a. (Box<a>) -> a` and printed).
- `examples/maybe_int.ail.json` → prints 7 then 99 (pattern match
over `Maybe<Int>`: `or_else(Some(7), 99)` then
`or_else(None, 99)`).
+122
View File
@@ -1024,3 +1024,125 @@ for. Two candidates, in order of expected payoff:
Leaning 13a — it's the more interesting architectural step and
makes the "polymorphic map" rewrite from the original 12c plan
finally meaningful.
## 2026-05-07 — Iter 13 done: parameterised ADTs reach the binary
**Why now.** End of Iter 12 left polymorphism half-useful: `id`
and `apply` worked, but every container was monomorphic
(`IntList`, `Maybe_Int`). A generic `map :: forall a b. ((a) -> b,
List a) -> List b` was unwritable. 13 lifts that.
**Three commits:**
- `0782622` 13a — schema (`TypeDef.vars`, `Type::Con.args`) +
checker (substitution at ctor + match + arity validation in
`check_fn`).
- `1631f60` 13b — codegen: per-use-site substitution of LLVM
field types in `lower_ctor` and `lower_match`. No mono-queue
for types — ctor code was already inlined at every use site,
so 13b only had to thread substitution through, not invent a
symbol scheme. `synth_arg_type` for `Term::Ctor` now returns
concrete type-args, and `llvm_type(Type::Var)` is a hard
error instead of a silent `ptr` fallback (the latter was
flagged by the architect review and is the most defensive
single change in 13).
- `<this>` 13c — DESIGN.md flipped (parameterised ADTs out of
the gap list, into the supported list); two new example lines.
**Hash invariant.** Both new fields are
`#[serde(default, skip_serializing_if = "Vec::is_empty")]`. A new
regression test in `crates/ailang-core/src/hash.rs` deserialises
the actual `examples/sum.ail.json` and `examples/list.ail.json`
from disk and asserts `db33f57cb329935e` and `b082192bd0c99202` —
the recorded pre-13a hashes. It's deliberately phrased against
the on-disk JSON rather than reconstructed code, so the test
fails if anyone resaves the examples in a way that drifts the
canonical bytes.
**Architect-flagged debt I deliberately did NOT touch in 13:**
- `is_static_callee` returns true for poly fns but
`resolve_top_level_fn` only consults `module_user_fns`. A
poly fn used as a value (`let f = id in f(42)`) passes the
static check then surfaces as `UnknownVar`. Would need one
closure-pair global per instantiation. Out of 13 scope; same
hole that was queued at the end of Iter 12.
- Triple source of truth for builtins (`builtins::install`,
`builtins::list`, `codegen::builtin_ail_type` /
`builtin_effect_op_ret`). Every new operator costs three
edits. Low interest today, escalates with every effect op.
Worth a future tidy iter — not blocking expressivity.
- `synth_arg_type` for `Term::If` returns `synth(then)` only;
for `Term::Match`, the first arm. Masked today by the
typechecker having already unified, but it's the kind of
duplication that decays. Same fundamental cost as the absence
of a TIR.
**Architecture self-check.**
- *Would I use this language now?* For polymorphism over
primitives, fn-typed values, AND parameterised containers —
yes. The `box.ail.json` and `maybe_int.ail.json` examples
read like the textbook says they should. No type-arg
bookkeeping leaks into the source.
- *KISS.* 13b was much smaller than I feared at the start of
the design phase: ~150 LOC in codegen, no new structures, no
mono-queue. The reason: ADT ctor code is already inlined.
The architect's recommendation to *not* mutate `ctor_index`
but derive `CtorRef` per use site was the right call —
preserved the static template, made the substitution local.
- *Did I think of everything?* Two known gaps remain. **(1)**
Polymorphic ADTs as the type-arg of a polymorphic fn —
works today because `unify_for_subst` recurses through
`Type::Con.args` (added in 13a). **(2)** A polymorphic fn
taking a polymorphic ADT and returning a different
parameterised ADT (`map : forall a b. ((a)->b, List a) -> List
b`) — should also work, but I haven't dogfooded it yet
because `List a`-as-a-rewrite-of-`list_map` would need the
schema bumps elsewhere (paramaterised list builder). Queued
for Iter 14.
- *Visualisation.* `ail manifest examples/box.ail.json` shows
`type Box :: forall a. MkBox(a)` and
`fn unbox :: forall a. (Box<a>) -> a`. The pretty-printer
picked up `args` and `vars` cleanly (Iter 13a).
**Tests:** 64/64 (was 58/58). Added: 1 hash-stability regression
(13a), 3 checker unit tests for parameterised ADTs (13a),
2 e2e tests over `box.ail.json` and `maybe_int.ail.json` (13b).
**Process note (orchestration).** First iter where I worked
strictly through the agents in `/agents/`: `ailang-architect`
ran a drift review on HEAD before 13b started; `ailang-implementer`
got a fixed brief that incorporated the architect's three
recommendations (don't mutate `ctor_index`, fix `synth_arg_type`
for `Term::Ctor`, harden `llvm_type`); 13c (this) is the
orchestrator's own work. The role split landed in `3df943d` after
I caught myself doing implementer work on 13a directly. The
agents pay off in proportion to iter size — for 13b they were
clearly worth the round-trip; for 13a's checker work, marginal.
**Plan iteration 14 (queued, not started):**
Two candidates, in order of expected payoff:
14a. **Polymorphic `List a` rewrite of `list_map`.** Replaces
`IntList` with `List a`, rewrites `list_map` to return
`List b`, and lets the polymorphic-map version be the
dogfood smoke test. Pure exercise — should fall out of
13b — but worth the dogfood beat. Also: `Maybe a` used
in a non-trivial fn (e.g. `find : forall a. ((a) -> Bool,
List a) -> Maybe a`).
14b. **GC or arena.** Same pitch as before: every ADT box,
lambda env, closure pair leaks. For `box.ail.json` and
`maybe_int.ail.json`, fine. For anything that allocates
in a loop, required. Bumpalloc per top-level fn
invocation is the natural MVP.
14c. **Poly fn as value.** Closes the asymmetry the architect
flagged; gates `let f = id in f(42)`. One closure-pair
global per instantiation, emitted via the same mono-queue
drain path. Smaller surface than 14a/b.
Leaning 14a — the dogfood payoff for one iter of polish is
high, and `Maybe`-in-a-real-fn is a missing piece I haven't
exercised yet. 14b stays second; 14c is a candidate if I want
a small palate cleanser.