Iter 14h: cross-module parameterised-ADT import (15a unblocked)

The 15a tester surfaced a real compiler limitation: cross-module
type and ctor references were not implemented. Iter 5b only
carried fns + consts via module_globals; types/ctors stayed
module-local with an explicit DESIGN comment. This iter completes
the cross-module mechanism using the Iter-5b convention:
qualified-only access via module.Name.

Implementation:
- ailang-check: Env.module_types populated by build_module_types.
  Qualified resolution in Type::Con, Term::Ctor, with cross-module
  fallback for pat-ctor (local wins, multi-import collision -> new
  ambiguous-ctor diagnostic). Four new unit tests.
- ailang-codegen: workspace-level module_ctor_index replaces
  per-Emitter table. lookup_ctor_by_type / lookup_ctor_in_pattern
  thread qualified type names through box-tag and field-type
  resolution.
- examples/std_maybe_demo.{ailx,ail.json}: type-name slots now
  qualified (std_maybe.Maybe).
- New e2e test cross_module_maybe_demo asserts the demo prints
  ["7","99","true","true","42"].

Net diff ~550 LOC. Tests 80 -> 85. All Iter 14a regressions
(parameterised_box_round_trip, parameterised_maybe_match,
list_map_poly_inc_then_prints, polymorphic_id_at_int_and_bool)
verified green — the 14h derive_substitution change (default
unpinned forall vars to Unit for monomorphiser) sits on a
different layer than 14a's $u-wildcard fix and they coexist.

Hash invariance: all five std_maybe def hashes unchanged. All
80-test-suite fixtures retain bit-identical hashes — cross-module
support is purely additive at the language level.

Process note: the std_maybe.ailx file landed in the 14g commit
via a sloppy git-add-A; should have spotted it before staging.
Not a correctness issue but a hygiene one.

Implementer flagged Unit-default monomorphisation as wasteful-
but-correct; rethink if stdlib grows toward overload-resolution-
style cases needing distinct unconstrained instantiations.

std_maybe stdlib effectively ships: module + four combinators +
e2e-tested consumer demo. Plan 15b: std_list importing std_maybe,
exercising Maybe-returning head/tail and tail-call-marked
fold_left.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 18:08:56 +02:00
parent 41d406bcbb
commit 12e9a9c0cc
8 changed files with 837 additions and 70 deletions
+114
View File
@@ -2191,6 +2191,120 @@ brief I had drafted included an "authoring note: post-14d
if-then-else" section that's now obsolete. Re-issue without
that, using `if` naturally where appropriate.
## Iter 14h — cross-module parameterised-ADT import (15a unblocked)
The 15a tester surfaced exactly the kind of bug a first-real-
stdlib-iter is supposed to surface: cross-module references to
types and ctors were not implemented. The Iter 5b cross-module
mechanism only carried fns + consts via `module_globals`; types
and ctors stayed module-local with an explicit comment in
`crates/ailang-check/src/lib.rs:703`: *"Register type defs (local
per module; cross-module ADT sharing is explicitly not part of
5b)"*. This iter completes that work using the same convention
as fns: **qualified-only access via `module.Name`**.
(Note on git tidiness: the `examples/std_maybe.ailx` file was
authored by the cancelled 15a tester dispatch and got swept into
the 14g commit by a `git add -A`. Should have spotted it pre-
commit. Not a correctness issue — the file was complete and
correctly authored — but a process-hygiene one. Will check the
diff carefully before staging next time.)
**The bug, surfaced by the 15a demo.**
```
ail check examples/std_maybe_demo.ail.json --json
[{"severity":"error","code":"unknown-type",
"message":"unknown type: `Maybe`","def":"main","ctx":{}}]
```
After qualifying the fn calls (`std_maybe.from_maybe`), fn refs
worked but the `(con Maybe (con Int))` and `(term-ctor Maybe
Just 7)` kept failing because `env.types` and `env.ctor_index`
are populated only from the current module.
**The fix.** Same shape as Iter 5b's fn solution, applied to types
and ctors:
- `Env` gains `module_types: BTreeMap<String, IndexMap<String,
TypeDef>>` populated by a sibling `build_module_types` to
`build_module_globals`. Lives in `check_in_workspace`'s
pre-check pass.
- Type resolution in `(con NAME args)`: if `NAME` contains exactly
one `.`, split into `module.type` parts and resolve via
`env.module_types[module]`. Else current behaviour.
- Term-ctor resolution in `Term::Ctor { type, ctor, args }`: same
split rule on the `type` field. The `ctor` field stays
unqualified — once the type is resolved, ctor lookup is
unambiguous within the type def.
- Pattern-ctor resolution: when the bare ctor name doesn't resolve
in the local `ctor_index`, fall back to scanning imported
modules' types. Conflict rule: local always wins; if multiple
imported modules declare the same ctor name, error with the
new diagnostic code `ambiguous-ctor`.
- Codegen mirrors: a workspace-level `module_ctor_index` replaces
the per-Emitter table. `lookup_ctor_by_type` / `lookup_ctor_in_pattern`
thread qualified type names through the box-tag and field-type
resolution paths.
**Diff size: 4 files, ~550 LOC net.** `ailang-check`: +311
(env + four resolution sites + 4 unit tests). `ailang-codegen`:
+230 (workspace ctor index + qualified type-name handling). One
new diagnostic code. Demo updated to use `std_maybe.Maybe` at
type-name slots.
**Tests: 85/85 (was 80, +5).** Four new unit tests in
`ailang-check` covering: qualified type ref, qualified term-ctor,
pat-ctor cross-module fallback, pat-ctor ambiguous-ctor
diagnostic. One new e2e test `cross_module_maybe_demo` asserts
stdout `["7", "99", "true", "true", "42"]`.
**Hash invariance: confirmed.** All five `std_maybe` def hashes
unchanged (`Maybe 0fb8eaacba5e1135`, `from_maybe caf8eeaca800c80d`,
`is_some c09002048ff1ff6e`, `is_none 144e131340b58bd3`,
`map_maybe 68d83d84799322fa`). All other 80-test-suite fixtures
retain bit-identical hashes — the cross-module support is purely
additive at the language level.
**14a-era regressions held.** Spot-checked
`parameterised_box_round_trip`, `parameterised_maybe_match`,
`list_map_poly_inc_then_prints`, `polymorphic_id_at_int_and_bool`
— all green. The 14h `derive_substitution` change (default
unpinned forall vars to `Unit` for the monomorphiser) sits on a
different layer than 14a's `synth_arg_type` `$u`-wildcard fix
(for nested ctor type synth). Both coexist:
- 14a's `$u`-wildcard short-circuits unification when a sibling
arg pins the same type var concretely.
- 14h's Unit default applies when no arg pins a forall var at
all (e.g. `is_none(Nothing)` — `a` in `Maybe<a>` is genuinely
unobservable from `Nothing`).
**Implementer note (flagged for future):** the Unit default
produces a single shared monomorphisation for all such
unconstrained-`a` call sites. Wasteful but correct. If the
stdlib grows toward overload-resolution-style cases where
unconstrained instantiations need to be distinguished, the
descriptor strategy needs rethinking. Punted; not a 15a blocker.
**std_maybe stdlib effectively ships.** Module + four
combinators + e2e-tested consumer demo. The cross-module
parameterised-ADT pipeline is the missing piece that 13a/b/c
(parameterised ADTs) and 5b (cross-module fns) could not by
themselves cover. This iter closes that loop.
**Plan 15b.** Now that `Maybe<a>` is reusable across modules,
write `std_list.ailx` importing `std_maybe`. Combinators:
`length`, `head` (returns `Maybe<a>`), `tail` (returns
`Maybe<List<a>>`), `is_empty`, `append`, `reverse`, `map`,
`filter`, `fold_left`, `fold_right`. `head`/`tail` exercise the
cross-module Maybe-returning case. `fold_left` is the
tail-recursive variant and gets `(tail-app ...)` markers; the
constructor-blocked combinators (`map`, `filter`, `append`,
`fold_right`) stay unmarked. If a new compiler bug surfaces
during stdlib construction (each prior dogfood iter has surfaced
one), debugger handles it inline.