From 3d9fbc68c6e5a067ec9908131746fddf8dd0350b Mon Sep 17 00:00:00 2001 From: Brummel Date: Thu, 7 May 2026 14:47:41 +0200 Subject: [PATCH] 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. --- docs/DESIGN.md | 25 +++++++--- docs/JOURNAL.md | 122 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 6 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 08efd76..e2c661e 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -244,16 +244,11 @@ ail run — 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____` (e.g. `id__I` for `id` at `Int`, `apply__I_I` for `apply` at `(Int, Int)`). +- **Parameterised ADTs** (Iter 13). `TypeDef.vars: Vec` + declares type parameters; `Type::Con.args: Vec` 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` and printed). +- `examples/maybe_int.ail.json` → prints 7 then 99 (pattern match + over `Maybe`: `or_else(Some(7), 99)` then + `or_else(None, 99)`). diff --git a/docs/JOURNAL.md b/docs/JOURNAL.md index feace86..e9c111e 100644 --- a/docs/JOURNAL.md +++ b/docs/JOURNAL.md @@ -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). +- `` 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`. 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.