Iter 14a: polymorphic List a end-to-end + monomorphisation fix
Dogfood payoff for parameterised ADTs (13a/b/c). Adds the first
program to nest a nullary ctor of a parameterised ADT inside a
parent ctor (Cons(Int, Nil)) and to call a polymorphic recursive
higher-order fn over a recursive parameterised ADT.
Fixture (examples/list_map_poly.ail.json):
- data List a = Nil | Cons a (List a)
- inc : (Int) -> Int = \x. x + 1
- map : forall a b. ((a) -> b, List<a>) -> List<b>
recursive, instantiated at (Int, Int)
- print_list : (List<Int>) -> Unit !{IO}, recursive
- main builds [1,2,3], maps inc, prints each: "2", "3", "4"
E2E test list_map_poly_inc_then_prints in crates/ail/tests/e2e.rs.
Bug fixed (crates/ailang-codegen/src/lib.rs, +30/-9):
synth_arg_type used Type::unit() as placeholder for ADT type
vars that the ctor's args couldn't pin (Nil for List<a>).
Inside Cons(Int, Nil), unify_for_subst then bound a=Int from
the head and collided with a=Unit from the tail. Replaced the
placeholder with a synth-only wildcard Type::Var{name:"$u"}
mirroring the checker's $m metavar convention; unify_for_subst
short-circuits on $u-prefixed arg-side vars (accept without
binding, let a sibling pin the var).
No schema or API change. No new variant. Tester's recursion
hypothesis was refuted by debugger via a non-recursive
Cons(7, Nil) repro before the fix landed.
Tests: 25/25 e2e (was 24). All Iter 12/13 regressions green.
cargo doc --no-deps zero warnings (workspace invariant from
13d/e/f preserved).
Three-agent flow (tester -> debugger, no implementer needed
since the fix was inside debugger's <50 LOC scope). Process
note in JOURNAL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+106
@@ -1407,3 +1407,109 @@ budget at the start of 14a is tight, alternative is 14b
|
||||
(GC/arena scaffolding) or 14c (poly fn as value); both have
|
||||
real design questions that need an orchestrator design pass
|
||||
first, so 14a stays the default.
|
||||
|
||||
## Iter 14a — polymorphic `List a` end-to-end + monomorphisation bug fixed
|
||||
|
||||
The dogfood payoff for parameterised ADTs (13a/b/c). Prior
|
||||
to this iter, the only programs exercising the feature were
|
||||
`box.ail.json` (single `MkBox(42)` round-trip) and
|
||||
`maybe_int.ail.json` (single `or_else` call). That is a thin
|
||||
slice — neither program builds a recursive parameterised ADT
|
||||
nor calls a polymorphic higher-order fn. 14a closes that gap
|
||||
with `data List a` + `map : forall a b. ((a) -> b, List<a>) ->
|
||||
List<b>` recursive, then prints the result.
|
||||
|
||||
Three-agent run (tester → debugger → no implementer needed):
|
||||
|
||||
1. **Tester** wrote `examples/list_map_poly.ail.json` (5 defs:
|
||||
`List`, `inc`, `map`, `print_list`, `main`) and a new e2e
|
||||
test `list_map_poly_inc_then_prints` that asserts stdout
|
||||
`["2", "3", "4"]`. Typecheck passed, build crashed:
|
||||
`internal: monomorphisation: var \`a\` bound to two
|
||||
distinct types`. Tester correctly stopped — fixture
|
||||
encodes the contract; bug is in the compiler — and
|
||||
reported with a hypothesis (recursion / shared
|
||||
substitution slot in `map`).
|
||||
|
||||
2. **Debugger** refuted the hypothesis with a non-recursive
|
||||
repro (`Cons(7, Nil)` triggers the same crash without
|
||||
`map` in the picture at all). Real cause was much smaller:
|
||||
in `synth_arg_type` (codegen, ~line 2087) for `Term::Ctor`,
|
||||
any type var of the parent ADT that the ctor's args
|
||||
couldn't pin was filled with `Type::unit()` as a
|
||||
placeholder. For nullary ctors of a parameterised ADT
|
||||
(`Nil : List<a>`, `None : Maybe<a>`) that placeholder
|
||||
leaked upward. Inside a parent like `Cons(Int, Nil) :
|
||||
List<a>`, `unify_for_subst` would walk
|
||||
`cref.ail_fields = [Var{a}, Con{List,[Var{a}]}]` against
|
||||
`[Int, Con{List,[Unit]}]`, bind `a = Int` from the head,
|
||||
then collide with `a = Unit` from the tail. The recursive
|
||||
`map` fixture surfaced it because it is the first program
|
||||
to nest a nullary ctor of a parameterised ADT inside a
|
||||
parent ctor — the existing 13b regressions never did. The
|
||||
tester's hypothesis was reasonable from the symptom but
|
||||
wrong on mechanism; refuting it via a smaller repro is
|
||||
exactly the discipline the debugger role is for.
|
||||
|
||||
3. **Fix** (`crates/ailang-codegen/src/lib.rs`, +30/-9 LOC,
|
||||
no new variant, no API change):
|
||||
- Replace `Type::unit()` placeholder with a synth-only
|
||||
wildcard `Type::Var { name: "$u" }`. The `$u` prefix is
|
||||
a reserved-namespace convention that mirrors the
|
||||
checker's `$m<id>` for instantiation metavars — same
|
||||
trick (source-level identifiers can't start with `$`),
|
||||
same goal (extra semantics without schema change).
|
||||
- `unify_for_subst` short-circuits on a `$u`-prefixed
|
||||
**arg-side** var: accept without binding, let a sibling
|
||||
arg pin the type var instead. Param-side semantics
|
||||
untouched. `derive_substitution`'s "var not pinned"
|
||||
check still fires for genuinely under-determined calls
|
||||
(those would have failed typechecking, so codegen never
|
||||
sees them, but the safety net stands).
|
||||
|
||||
**Tests:** 25/25 e2e (was 24, +1 new poly test). All five
|
||||
named regressions stayed green: `list_map_doubles_then_prints`,
|
||||
`parameterised_box_round_trip`, `parameterised_maybe_match`,
|
||||
`polymorphic_id_at_int_and_bool`, `polymorphic_apply_with_fn_param`.
|
||||
Insertion sort still green. `cargo doc --no-deps`: 0 warnings
|
||||
(workspace invariant from 13d/e/f preserved). `cargo build
|
||||
--workspace` green.
|
||||
|
||||
**Process note: tester→debugger→done in one iter.** No
|
||||
implementer dispatch needed — the debugger's mandate covers
|
||||
"propose **and apply** a minimally invasive fix" once the
|
||||
diagnosis is solid. 30 LOC across two sites in one file is
|
||||
inside the role's scope (the role doc says "stop and report"
|
||||
only on >50 LOC across multiple files). The orchestrator-side
|
||||
work was the design (the source program), the scoped briefs,
|
||||
and verification. This is the cleanest agent-flow shape so
|
||||
far: each agent did exactly its job, the tester's wrong
|
||||
hypothesis didn't propagate because the debugger tested it,
|
||||
and the orchestrator never wrote compiler code.
|
||||
|
||||
**Findings flagged** (judgement deferred, not fixed):
|
||||
|
||||
- `CodegenError::Internal` (the catch-all string variant the
|
||||
docwriter flagged in 13f) is now used by one more invariant
|
||||
— the `$u` short-circuit could in principle be reached by a
|
||||
malformed input; it's currently silent because typecheck
|
||||
rejects under-determined calls upstream. Worth splitting
|
||||
into typed variants the next time codegen tests get a real
|
||||
rewrite. (Same finding as 13f, now reinforced by another
|
||||
use site.)
|
||||
- The `$u` / `$m` reserved-prefix convention (`$u` for
|
||||
codegen synth wildcards, `$m<id>` for checker metavars)
|
||||
is undocumented as a project-wide naming rule. Two
|
||||
prefixes is fine; if a third appears, this should be
|
||||
promoted to a DESIGN.md note.
|
||||
|
||||
**Next.** Parameterised ADTs are now genuinely usable for
|
||||
real programs. The standard library starter set (an
|
||||
`examples/std_*` series with `List`, `Maybe`, `Either`,
|
||||
basic combinators `length`, `filter`, `fold`, `concat`)
|
||||
becomes worthwhile in a way it wasn't pre-14a — that's a
|
||||
plausible 14b alternative, smaller than GC/arena, and would
|
||||
itself surface more dogfood bugs. The original 14b
|
||||
(GC/arena) and 14c (poly fn as value) remain on the queue
|
||||
but have not had a design pass.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user