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
+1
View File
@@ -0,0 +1 @@
{"defs":[{"ctors":[{"fields":[],"name":"Nothing"},{"fields":[{"k":"var","name":"a"}],"name":"Just"}],"doc":"Polymorphic optional value: either Just<a> or Nothing.","kind":"type","name":"Maybe","vars":["a"]},{"body":{"arms":[{"body":{"name":"x","t":"var"},"pat":{"ctor":"Just","fields":[{"name":"x","p":"var"}],"p":"ctor"}},{"body":{"name":"default","t":"var"},"pat":{"ctor":"Nothing","fields":[],"p":"ctor"}}],"scrutinee":{"name":"m","t":"var"},"t":"match"},"doc":"Project out of Maybe<a> with a default for the Nothing case.","kind":"fn","name":"from_maybe","params":["default","m"],"type":{"body":{"effects":[],"k":"fn","params":[{"k":"var","name":"a"},{"args":[{"k":"var","name":"a"}],"k":"con","name":"Maybe"}],"ret":{"k":"var","name":"a"}},"k":"forall","vars":["a"]}},{"body":{"arms":[{"body":{"lit":{"kind":"bool","value":true},"t":"lit"},"pat":{"ctor":"Just","fields":[{"p":"wild"}],"p":"ctor"}},{"body":{"lit":{"kind":"bool","value":false},"t":"lit"},"pat":{"ctor":"Nothing","fields":[],"p":"ctor"}}],"scrutinee":{"name":"m","t":"var"},"t":"match"},"doc":"Returns true iff m is Just<_>.","kind":"fn","name":"is_some","params":["m"],"type":{"body":{"effects":[],"k":"fn","params":[{"args":[{"k":"var","name":"a"}],"k":"con","name":"Maybe"}],"ret":{"k":"con","name":"Bool"}},"k":"forall","vars":["a"]}},{"body":{"arms":[{"body":{"lit":{"kind":"bool","value":false},"t":"lit"},"pat":{"ctor":"Just","fields":[{"p":"wild"}],"p":"ctor"}},{"body":{"lit":{"kind":"bool","value":true},"t":"lit"},"pat":{"ctor":"Nothing","fields":[],"p":"ctor"}}],"scrutinee":{"name":"m","t":"var"},"t":"match"},"doc":"Returns true iff m is Nothing.","kind":"fn","name":"is_none","params":["m"],"type":{"body":{"effects":[],"k":"fn","params":[{"args":[{"k":"var","name":"a"}],"k":"con","name":"Maybe"}],"ret":{"k":"con","name":"Bool"}},"k":"forall","vars":["a"]}},{"body":{"arms":[{"body":{"args":[{"args":[{"name":"x","t":"var"}],"fn":{"name":"f","t":"var"},"t":"app"}],"ctor":"Just","t":"ctor","type":"Maybe"},"pat":{"ctor":"Just","fields":[{"name":"x","p":"var"}],"p":"ctor"}},{"body":{"args":[],"ctor":"Nothing","t":"ctor","type":"Maybe"},"pat":{"ctor":"Nothing","fields":[],"p":"ctor"}}],"scrutinee":{"name":"m","t":"var"},"t":"match"},"doc":"Apply f to the wrapped value, or pass Nothing through.","kind":"fn","name":"map_maybe","params":["f","m"],"type":{"body":{"effects":[],"k":"fn","params":[{"effects":[],"k":"fn","params":[{"k":"var","name":"a"}],"ret":{"k":"var","name":"b"}},{"args":[{"k":"var","name":"a"}],"k":"con","name":"Maybe"}],"ret":{"args":[{"k":"var","name":"b"}],"k":"con","name":"Maybe"}},"k":"forall","vars":["a","b"]}}],"imports":[],"name":"std_maybe","schema":"ailang/v0"}
+1
View File
@@ -0,0 +1 @@
{"defs":[{"body":{"args":[{"name":"x","t":"var"},{"lit":{"kind":"int","value":1},"t":"lit"}],"fn":{"name":"+","t":"var"},"t":"app"},"doc":"Add 1 to an Int. Used as the (a -> b) arg to map_maybe.","kind":"fn","name":"inc","params":["x"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"Int"}],"ret":{"k":"con","name":"Int"}}},{"body":{"lhs":{"args":[{"args":[{"lit":{"kind":"int","value":99},"t":"lit"},{"args":[{"lit":{"kind":"int","value":7},"t":"lit"}],"ctor":"Just","t":"ctor","type":"std_maybe.Maybe"}],"fn":{"name":"std_maybe.from_maybe","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"lit":{"kind":"int","value":99},"t":"lit"},{"args":[],"ctor":"Nothing","t":"ctor","type":"std_maybe.Maybe"}],"fn":{"name":"std_maybe.from_maybe","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"args":[{"lit":{"kind":"int","value":5},"t":"lit"}],"ctor":"Just","t":"ctor","type":"std_maybe.Maybe"}],"fn":{"name":"std_maybe.is_some","t":"var"},"t":"app"}],"op":"io/print_bool","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"args":[],"ctor":"Nothing","t":"ctor","type":"std_maybe.Maybe"}],"fn":{"name":"std_maybe.is_none","t":"var"},"t":"app"}],"op":"io/print_bool","t":"do"},"rhs":{"args":[{"args":[{"lit":{"kind":"int","value":0},"t":"lit"},{"args":[{"name":"inc","t":"var"},{"args":[{"lit":{"kind":"int","value":41},"t":"lit"}],"ctor":"Just","t":"ctor","type":"std_maybe.Maybe"}],"fn":{"name":"std_maybe.map_maybe","t":"var"},"t":"app"}],"fn":{"name":"std_maybe.from_maybe","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"t":"seq"},"t":"seq"},"t":"seq"},"t":"seq"},"doc":"Drive each combinator once and print 7, 99, true, true, 42.","kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[{"module":"std_maybe"}],"name":"std_maybe_demo","schema":"ailang/v0"}
+30
View File
@@ -0,0 +1,30 @@
; Iter 15a — first consumer of an stdlib module.
; Imports std_maybe, exercises from_maybe, is_some, is_none, map_maybe.
; First program to import a parameterised ADT (Maybe a) across module
; boundaries. Per the project's qualified-only convention (Iter 5b for
; fns, Iter 15a for types/ctors), every cross-module reference is
; qualified: `std_maybe.from_maybe` for the fn, `std_maybe.Maybe` for
; the type-name slot of `term-ctor`. The bare ctor names (`Just`,
; `Nothing`) stay unqualified — once the type is resolved, the ctor
; lookup is unambiguous within it.
(module std_maybe_demo
(import std_maybe)
(fn inc
(doc "Add 1 to an Int. Used as the (a -> b) arg to map_maybe.")
(type (fn-type (params (con Int)) (ret (con Int))))
(params x)
(body (app + x 1)))
(fn main
(doc "Drive each combinator once and print 7, 99, true, true, 42.")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body
(seq (do io/print_int (app std_maybe.from_maybe 99 (term-ctor std_maybe.Maybe Just 7)))
(seq (do io/print_int (app std_maybe.from_maybe 99 (term-ctor std_maybe.Maybe Nothing)))
(seq (do io/print_bool (app std_maybe.is_some (term-ctor std_maybe.Maybe Just 5)))
(seq (do io/print_bool (app std_maybe.is_none (term-ctor std_maybe.Maybe Nothing)))
(do io/print_int (app std_maybe.from_maybe 0 (app std_maybe.map_maybe inc (term-ctor std_maybe.Maybe Just 41)))))))))))