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:
2026-05-07 15:32:48 +02:00
parent 1918fbee7f
commit 747b7cd05c
4 changed files with 380 additions and 9 deletions
+30 -9
View File
@@ -2076,18 +2076,27 @@ impl<'a> Emitter<'a> {
for (exp, actual) in cref.ail_fields.iter().zip(arg_tys.iter()) {
unify_for_subst(exp, actual, &var_set, &mut subst)?;
}
// Vars not pinned by ctor args (e.g. `None` for
// `Maybe a`) are filled with `Type::unit()` as a
// placeholder. That's harmless for monomorphic call
// sites — codegen never instantiates a polymorphic fn
// off this un-pinned shape because the typechecker
// would have rejected an under-determined call —
// and keeps the function total for the well-formed
// ones (e.g. `Some(7)` pins `a` from the arg).
// Vars not pinned by ctor args (e.g. `Nil` for `List a`,
// `None` for `Maybe a`) are filled with a synth-only
// wildcard `Type::Var { name: "$u" }`. The `$u`-prefix
// is reserved here (mirrors the checker's `$m` for
// metavars) and is treated as a match-anything wildcard
// by `unify_for_subst` on the arg side. This matters
// when a nullary ctor like `Nil` is nested inside a
// parent ctor whose other args pin the same type var
// concretely — e.g. `Cons(Int, Nil) : List<a>` must
// pin `a = Int` from the head and let the tail's
// unconstrained `a` defer rather than collide on
// `Type::unit()` as it would have pre-fix.
let resolved: Vec<Type> = cref
.type_vars
.iter()
.map(|v| subst.get(v).cloned().unwrap_or_else(Type::unit))
.map(|v| {
subst
.get(v)
.cloned()
.unwrap_or_else(|| Type::Var { name: "$u".into() })
})
.collect();
Ok(Type::Con {
name: type_name.clone(),
@@ -2237,6 +2246,18 @@ fn unify_for_subst(
vars: &BTreeSet<&str>,
subst: &mut BTreeMap<String, Type>,
) -> Result<()> {
// Iter 14a fix: an arg-side `$u`-prefixed var is a synth-only
// wildcard produced by `synth_arg_type` for nullary ctors of a
// parameterised ADT (e.g. `Nil : List<$u>`). It carries no real
// constraint — accept without binding so a sibling arg can pin
// the type var instead. Without this, `Cons(Int, Nil)` synth
// would unify `a = Int` (from head) and then `a = $u` (from
// tail's recursive `List<a>` slot) and falsely error.
if let Type::Var { name } = arg {
if name.starts_with("$u") {
return Ok(());
}
}
match (param, arg) {
(Type::Var { name }, _) if vars.contains(name.as_str()) => {
if let Some(prev) = subst.get(name) {