Iter 15b: std_list ships, three more compiler gaps closed

Second stdlib module. Tester wrote std_list.ailx (10 combinators,
164 LOC) and a consumer demo. std_list typechecked standalone;
demo did not, surfacing three compiler bugs:

1. Check-side: Iter 14h's qualify_local_types was applied to
   Term::Var cross-module lookup but not to ctor-field types in
   Term::Ctor synth or Pattern::Ctor resolution. First recursive
   cross-module ADT (List has Cons a (List a) — recursive Con
   self-ref) triggers the bug. std_maybe slipped through because
   Maybe's ctors have no recursive Con field.
2. Codegen-side: same gap mirrored across 4 sites in codegen
   (Term::Ctor synth, lower_ctor, lower_match) plus a tweak to
   unify_for_subst (recurse on re-bind instead of strict equality
   so sibling-derived List<Int> accepts nullary-ctor's List<$u>
   wildcard).
3. Const codegen: emit_const rejected non-literal const bodies.
   The demo's xs : List<Int> = Cons 1 (...) requires it. Fix:
   per-module const table, Term::Var resolution loads literal
   consts from global, inlines non-literal bodies. Bare and
   qualified refs both supported.

All three fixes carry an "Iter 15b" code comment at their site.
~349/25 LOC across ailang-check, ailang-codegen, e2e.rs.

Tests 85 -> 87. New e2e std_list_demo asserts 11-line stdout:
length 5, is_empty false/true, head via from_maybe, tail length,
append length, reverse head, map double head, filter is_even
length, fold_left sum, fold_right sum. New ailang-check unit
test cross_module_recursive_adt_term_and_pat_ctor covers both
the original bug and the symmetric pat-ctor latent twin.

Hash invariance: all pre-15b fixtures + std_maybe defs
bit-identical. 14a / 14e / 14h regressions all green.

Cumulative state: 2 stdlib modules (std_maybe, std_list), 14
combinators, cross-module recursive ADT working end-to-end.
Three compiler bugs surfaced + fixed in dogfood since 14a (each
dogfood iter has surfaced ≥1).

Authoring observation: form (A) at 10 combinators is fine; main
friction is paren-counting in nested seq chains, not the form
itself. n-ary seq would help but is sugar.

Plan 15c: 1000-element list stress test for fold_left (tail-call-
marked) vs fold_right (constructor-blocked).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 18:34:49 +02:00
parent 12e9a9c0cc
commit 92f4b4f8c7
8 changed files with 694 additions and 25 deletions
+118
View File
@@ -2305,6 +2305,124 @@ constructor-blocked combinators (`map`, `filter`, `append`,
during stdlib construction (each prior dogfood iter has surfaced
one), debugger handles it inline.
## Iter 15b — `std_list` ships, three more compiler gaps closed
Second stdlib module. Tester wrote `std_list.ailx` (164 LOC, 10
combinators) plus `std_list_demo.ailx` (consumer importing both
`std_maybe` and `std_list`). `std_list.ail.json` typechecked
cleanly in isolation. The demo did not — the prediction "every
dogfood iter surfaces at least one compiler bug" held three
times over.
**Tester's diagnosis** was sharp enough that the orchestrator
could go straight to implementer without a debugger round:
> Iter 14h's `qualify_local_types` is applied at `Term::Var`
> cross-module lookup but **not** to ctor-field types when
> `Term::Ctor` is synthesized. For `Cons a (List a)`, `List`
> stays unqualified in `cdef.fields`. `std_maybe` slipped
> through because its ctors have no recursive Con field. First
> recursive ADT shared cross-module triggers it.
**The original-spec fix** was ~10 LOC across two sites in
`ailang-check/src/lib.rs` — apply `qualify_local_types` over
`cdef.fields` before substituting forall vars, in both
`Term::Ctor` synth and `Pattern::Ctor` resolution (the latter
symmetric, no current consumer hit it but it's the same
underlying gap).
**Two more bugs surfaced during implementation** of that fix:
1. **Codegen-side qualify-fields, four sites.** `ailang-codegen`
has its own field-type tracking that mirrored the check-side
bug. Symmetric fix needed in `Term::Ctor` synth + `lower_ctor`,
in `lower_match` for cross-module ADT scrutinees, and a tweak
to `unify_for_subst` to recurse instead of strict-equality
when re-binding a forall var that already has a previous
concrete binding (so a sibling-derived `List<Int>` accepts a
nullary ctor's `List<$u>` wildcard).
2. **Const codegen for non-literal values.** The demo defines
a top-level `xs : List<Int>` const whose body is a Cons
chain. `check_const` already accepts pure non-literal const
bodies, but `emit_const` rejected them. Fix: register
`module_consts` in pass 1, resolve const refs in `Term::Var`
(load from global for literal consts, inline body for
non-literal). Both bare and qualified const refs (`xs`
and `module.xs`) supported.
All three fixes together: ~349 / 25 LOC across `ailang-check`,
`ailang-codegen`, and the new e2e test. Each fix carries an
`Iter 15b` code comment at its site.
**Tests: 87/87 (was 85, +2).** New e2e `std_list_demo` asserts
the 11-line stdout sequence:
```
5 (length [1..5])
false (is_empty [1..5])
true (is_empty [])
1 (head [1..5] via from_maybe)
4 (length of tail)
10 (length of [1..5] ++ [1..5])
5 (head of reverse [1..5])
2 (head of map double [1..5] = [2,4,6,8,10])
2 (length of filter is_even [1..5] = [2,4])
15 (fold_left + 0 [1..5])
15 (fold_right + 0 [1..5])
```
New unit test `cross_module_recursive_adt_term_and_pat_ctor` in
`ailang-check` covers both Term::Ctor and Pattern::Ctor paths
against a recursive cross-module ADT. Catches the original bug
+ its symmetric pat-ctor latent twin.
**Hash invariance.** All 22 pre-15b fixture hashes plus
`std_maybe`'s five def hashes bit-identical. The 15b changes
were purely additive on the language side.
**14a-era and 14h-era regressions held.** Spot-checked
`parameterised_box_round_trip` (14a), `cross_module_maybe_demo`
(14h), `list_map_poly_inc_then_prints` (14e). All green.
**Authoring observation from the tester.** Form (A) holds up to
10 combinators in one module without breaking. The highest-
overhead pieces are typed `lam` (each closure carries `(typed
name type)` triples + return-type + effects-clause) and the
outer `(forall (vars a b) (fn-type ...))` wrapper. Repeated
paren-counting at the bottom of nested `seq` chains was the only
real friction during demo authoring. Suggests a future iter
might add a `seq*` n-ary form, but the n-ary case is sugar over
the current `seq` shape and not load-bearing.
**Cumulative state, post-15b.**
- Stdlib modules: 2 (`std_maybe`, `std_list`).
- Combinators: 14 (`Maybe` + 4; `List` + 10).
- Cross-module imports: type-side, ctor-side, fn-side all working.
- Recursive cross-module ADTs working.
- Tail-call markers used in production: `fold_left` in `std_list`,
`print_list` in two existing fixtures.
- Compiler bugs surfaced and fixed in dogfood:
- 14a: monomorphisation `Type::unit()` placeholder collision.
- 14h: cross-module type/ctor not implemented.
- 15b: qualify-fields gap (3 layers: check, codegen, plus const).
**Plan 15c.** Stress test on real-shape data. Build a list of
~1000 elements, run `fold_left` and `fold_right` over it, verify
both produce the expected sum. The point: empirically confirm
that `fold_left`'s tail-call marker actually prevents stack
overflow under load, while `fold_right` (constructor-blocked,
unmarked) can still run at this scale because it's only ~1000
deep, not 1M. If `fold_right` segfaults on this scale, that's a
useful boundary; if it works, we know the stack budget on this
host is at least a few thousand frames.
After 15c, optional: `std_pair` (2-tuple ADT), `std_either`
(disjoint union for error handling). Or move on to a
non-stdlib feature like nested patterns — at this scale of
language, the case for adding a feature can be made directly
from a stdlib annoyance.