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:
+114
@@ -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.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user