Iter 13b: codegen for parameterised ADTs

Closes the gap between Iter 13a (parameterised ADTs in the
checker) and end-to-end execution. ADT ctor code stays inlined at
every use site — no mono-queue for types, no new symbols — but
LLVM field types are now derived per use site via substitution.

`CtorRef` gains `type_vars: Vec<String>` so use sites can identify
which fields reference rigid vars. `cref.fields` (precomputed
LLVM strings) is documented as monomorphic-only and read past for
parameterised ADTs.

`lower_ctor`: derives a `BTreeMap<String, Type>` substitution from
`synth_arg_type` of each arg via `unify_for_subst`, then maps every
`ail_field` through `apply_subst_to_type` + `llvm_type` for the
per-store types. Monomorphic ADTs hit the original fast path.

`lower_match`: builds `arm_subst` from `s_ail.args` ↔ `cref.type_vars`,
substitutes through each `cref.ail_fields[idx]`, and uses the
substituted type both for the LLVM `load` AND as the AILang slot
of the local — so a downstream `unbox(b)` sees `b: Int`, not
`b: a`.

`synth_arg_type` for `Term::Ctor`: returns concrete type-args
derived from the ctor's term-args. Vars left unpinned (e.g. `None`
for `Maybe a`) fall back to `Type::unit()`.

`llvm_type(Type::Var)` now hard-errors. Earlier this silently
fell through to `ptr` (the ADT-via-ptr fallback), producing
garbage IR. Failing loudly here surfaces missed substitutions in
the test suite.

Two e2e tests (`box.ail.json`, `maybe_int.ail.json`) cover ctor
lower with substituted field types and match-arm field
substitution. 64 tests green; clippy clean (two pre-existing
warnings untouched). Hash invariant holds.

Out of scope per agent assignment: poly-fn-as-value, higher-rank
polymorphism, DESIGN/JOURNAL updates (deferred to 13c).
This commit is contained in:
2026-05-07 14:45:35 +02:00
parent 3df943d17f
commit 1631f6065c
4 changed files with 364 additions and 15 deletions
+23
View File
@@ -128,6 +128,29 @@ fn polymorphic_apply_with_fn_param() {
assert_eq!(stdout.trim(), "42");
}
/// Iter 13b: round-trip a primitive through a parameterised ADT and a
/// polymorphic-fn boundary. `type Box[a] = MkBox(a)` plus
/// `unbox : forall a. (Box<a>) -> a` plus `print_int(unbox(MkBox(42)))`.
/// Guards: ctor lower with substituted LLVM field types,
/// match-arm field bindings carrying the substituted AILang type
/// down into a polymorphic fn body.
#[test]
fn parameterised_box_round_trip() {
let stdout = build_and_run("box.ail.json");
assert_eq!(stdout.trim(), "42");
}
/// Iter 13b: pattern-match on `Maybe<Int>`. `or_else(Some(7), 99) == 7`
/// then `or_else(None, 99) == 99`. Guards: match-arm field
/// substitution at a parameterised ctor (the `Some(x)` arm must bind
/// `x : Int`, not `x : a`).
#[test]
fn parameterised_maybe_match() {
let stdout = build_and_run("maybe_int.ail.json");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["7", "99"]);
}
/// Guards `ail diff`: a modified body changes the hash of `sum`, while
/// `main` stays unchanged. Expects exit code 1, `changed` contains exactly
/// `sum`, `unchanged` contains `main`, `added`/`removed` empty.