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:
+19
-6
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user