Iter 15g — std_either_list: first 3-way cross-module stdlib fn

First stdlib fn that imports three other stdlib modules (std_list,
std_either, std_pair) and returns a compound polymorphic ADT tree
(Pair<List<e>, List<a>>). Stresses monomorphisation across nested
parameterised ADTs.

What shipped:
- examples/std_either_list.{ailx,ail.json}: 3 combinators —
  - lefts : forall e a. (List<Either<e, a>>) -> List<e>
  - rights : forall e a. (List<Either<e, a>>) -> List<a>
  - partition_eithers : forall e a. (List<Either<e, a>>)
                        -> Pair<List<e>, List<a>>
  lefts/rights use depth-2 nested Ctor patterns (Cons (Left l) t)
  — exercises 16a's desugar pass at a depth not reached by any
  prior fixture.
- examples/std_either_list_demo.{ailx,ail.json}: drives all three
  combinators on a five-element List<Either<Int, Int>>; expected
  output one per line: 2, 3, 2, 3.
- crates/ail/tests/e2e.rs::std_either_list_demo: e2e count 34 → 35.
- docs/JOURNAL.md: Iter 15g entry.

Compiler bug surfaced (queued as 15g-aux, not fixed):
unify_for_subst in ailang-codegen/src/lib.rs accepts $u wildcards
only on the arg side. Mixing inline (Either Left n) and (Either
Right n) in a list literal lands $u on the param side via the
outer List<a>'s binding and errors. Reduced repro: `length [Left
1, Right 10]`. Demo works around with monomorphic mkleft/mkright
helpers; workaround documented inline. Fix is a one-line symmetric
extension of the early-return.

Tests: 94/94. Stdlib: 5 modules, 27 combinators.
This commit is contained in:
2026-05-07 19:57:06 +02:00
parent 20b412342d
commit e1587ebdae
6 changed files with 243 additions and 0 deletions
+102
View File
@@ -2889,3 +2889,105 @@ section. Spot-checked each — all match current implementation.
17a (per-fn arena). 17a remains the explicit checkpoint before
any broader memory-management work — that decision is gated on
a joint conversation with the user.
---
## Iter 15g — std_either_list: first 3-way cross-module stdlib fn
**Goal.** Through 15f the stdlib had 4 modules but no fn imported
more than one foreign module (e.g. `std_list.head` returning
`std_maybe.Maybe<a>`). 15g introduces the first stdlib module whose
every fn imports three others — `std_list`, `std_either`,
`std_pair` — and returns a compound polymorphic ADT tree
(`Pair<List<e>, List<a>>`). Designed to stress monomorphisation
across `List × Either × Pair`, cross-module ctor resolution at
qualified `term-ctor` sites, and the 16a desugar layer's nested-
Ctor pattern handling at depth 2.
**What shipped.**
- `examples/std_either_list.ailx` — three combinators, all
`forall (vars e a)`, all using qualified cross-module names at
`con` and `term-ctor` sites:
- `lefts : List<Either<e, a>> -> List<e>` — depth-2 nested-Ctor
match per Cons arm (`Cons (Left l) t` / `Cons (Right _) t`)
plus a wildcard tail to anchor the desugar's fall-through
chain in `List<e>` rather than the synthetic `Unit` fallback.
- `rights : List<Either<e, a>> -> List<a>` — symmetric to
`lefts`.
- `partition_eithers : List<Either<e, a>> -> Pair<List<e>,
List<a>>` — `Nil → MkPair(Nil, Nil)`; `Cons h t →` let-bind
`rest = partition_eithers t`, then a flat match on `h` to
splice `l` / `r` onto `fst rest` / `snd rest`. Single pass.
- `examples/std_either_list_demo.ailx` — first demo importing four
stdlib modules. Drives all three combinators on the same five-
element list `[Left 1, Right 10, Left 2, Right 20, Right 30]`;
prints lengths via `std_list.length`. Output: `2 / 3 / 2 / 3`.
- `crates/ail/tests/e2e.rs::std_either_list_demo` (e2e count
34 → 35).
**16a desugar exercised.** Yes — `lefts` and `rights` use depth-2
nested Ctor patterns (`(pat-ctor Cons (pat-ctor Left l) t)`). Each
expands into a chain of single-level matches via the 16a pass; the
explicit trailing wildcard arm prevents the synthetic `Unit`
fallback (the desugar's documented "valid programs never reach it"
terminator) from leaking into the function's return type. Without
the wildcard the checker reports `expected std_list.List<e>, got
Unit` because the chain's `else`-arm body is `Lit Unit` — confirms
the design note in `desugar.rs::desugar_match` that exhaustiveness
is the caller's responsibility, not the desugar's.
**New compiler bug surfaced.** Yes — and it is **not** in the new
combinators themselves but in the existing monomorphiser, surfaced
the moment one tries to construct an inline list literal mixing
`(term-ctor std_either.Either Left n)` and `(term-ctor
std_either.Either Right n)`. Reduced repro is just `(app
std_list.length (term-ctor std_list.List Cons (Left 1) (term-ctor
std_list.List Cons (Right 10) (term-ctor std_list.List Nil))))` —
no `lefts` / `rights` / `partition_eithers` involved.
Cause: `synth_arg_type` produces `Either<Int, $u>` for `Left 1` and
`Either<$u, Int>` for `Right 10`. The outer `Cons`'s parameter
type `List<a>` unifies first against `List<Either<Int, $u>>`
(binds `a = Either<Int, $u>`) and then against `List<Either<$u,
Int>>` for the tail. `unify_for_subst` recurses into the prev
binding and ends up unifying param `$u` (from `Either<Int, $u>`)
against arg `Int` — the existing `if name.starts_with("$u") {
return Ok(()); }` early-return only fires when `$u` is on the
**arg** side. Param-side `$u` falls through to the catch-all error
`cannot match param `$u` to arg `Int``.
**Workaround in the demo.** Two monomorphic helpers `mkleft : Int
-> Either<Int, Int>` and `mkright : Int -> Either<Int, Int>` pin
both type vars at the call site, so each list element arrives with
fully concrete `Either<Int, Int>` and the synth-time `$u` never
appears. Documented in the demo's header comment with a pointer
back to this entry. The combinators themselves (`lefts`, `rights`,
`partition_eithers`) are bug-free — the demo just couldn't build
the input list inline without dodging the `$u`-on-param-side path.
**Bug fix sketch (queued, not landed).** A symmetric early-return
in `unify_for_subst` for param-side `$u` would close this — the
wildcard semantics ("don't care, defer") are direction-agnostic.
Filed as candidate iter 15g-aux. Single-line patch + a unit test
under `ailang-codegen/src/lib.rs`.
**Tests: 94/94.**
- e2e: 35 (was 34, +1 for `std_either_list_demo`).
- All other crates unchanged.
**Cumulative state, post-15g.**
- Stdlib: 5 modules (`std_maybe`, `std_list`, `std_either`,
`std_pair`, `std_either_list`); 27 combinators total (24 + 3).
- Deepest cross-module composition reached so far: `List × Either
× Pair` in a single fn (`partition_eithers`).
- Compiler bugs surfaced and fixed in dogfood since 14a: still 4
fixed; 1 new (the `$u`-on-param-side case above), worked around
in the demo, queued as 15g-aux.
**Queue update.** 15g done. Remaining: 15g-aux (param-side `$u`
acceptance — small, one-line in `unify_for_subst`); 16b (local
recursive `let`); 16c (Lit-in-Ctor patterns); 17a (per-fn arena,
gated on user discussion).