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
+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.