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
+15
View File
@@ -240,6 +240,21 @@ fn parameterised_maybe_match() {
assert_eq!(lines, vec!["7", "99"]);
}
/// Iter 15a: cross-module reference to a parameterised ADT, including
/// its ctors and a polymorphic combinator instantiated at `(Int, Int)`.
/// `std_maybe_demo` imports `std_maybe`, qualifies the type-name slot
/// of every `term-ctor` (`std_maybe.Maybe`), and exercises
/// `from_maybe`, `is_some`, `is_none`, and `map_maybe` once each.
/// Property protected: the qualified-only convention (Decision 6's
/// architectural pin extended to types and ctors per the brief)
/// flows end-to-end through check, codegen, and runtime.
#[test]
fn cross_module_maybe_demo() {
let stdout = build_and_run("std_maybe_demo.ail.json");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["7", "99", "true", "true", "42"]);
}
/// Guards `ail diff`: a modified body changes the hash of `sum`, while
/// `main` stays unchanged. Expects exit code 1, `changed` contains exactly
/// `sum`, `unchanged` contains `main`, `added`/`removed` empty.
+1
View File
@@ -40,6 +40,7 @@
//! - `module-name-mismatch` — workspace loader (Iter 5b, in the CLI path)
//! - `module-hash-mismatch` — workspace loader (Iter 5b, in the CLI path)
//! - `tail-call-not-in-tail-position` (Iter 14e, see Decision 8)
//! - `ambiguous-ctor` — `ctx`: `{"ctor": "<n>", "candidates": ["m1.T", "m2.T"]}` (Iter 15a)
use serde::Serialize;
+426 -19
View File
@@ -401,6 +401,18 @@ pub enum CheckError {
/// Code: `tail-call-not-in-tail-position`. See Decision 8.
#[error("call marked `tail` is not in tail position")]
TailCallNotInTailPosition,
/// Iter 15a: a bare `Pattern::Ctor.ctor` did not resolve against the
/// current module's `ctor_index` and resolved against ctors in two
/// or more imported modules. The author must qualify the scrutinee
/// type so that ctor lookup is unambiguous (e.g. write
/// `(con std_maybe.Maybe (con Int))` for the scrutinee). Code:
/// `ambiguous-ctor`. `ctx`: `{"ctor": "<n>", "candidates": ["m1.T", "m2.T"]}`.
#[error("ambiguous constructor `{ctor}`: declared in {candidates:?}")]
AmbiguousCtor {
ctor: String,
candidates: Vec<String>,
},
}
type Result<T> = std::result::Result<T, CheckError>;
@@ -437,6 +449,7 @@ impl CheckError {
CheckError::UnknownImport { .. } => "unknown-import",
CheckError::InvalidDefName { .. } => "invalid-def-name",
CheckError::TailCallNotInTailPosition => "tail-call-not-in-tail-position",
CheckError::AmbiguousCtor { .. } => "ambiguous-ctor",
}
}
@@ -471,6 +484,9 @@ impl CheckError {
CheckError::InvalidDefName { name } => {
serde_json::json!({"name": name, "reason": "contains-dot"})
}
CheckError::AmbiguousCtor { ctor, candidates } => {
serde_json::json!({"ctor": ctor, "candidates": candidates})
}
_ => serde_json::Value::Object(serde_json::Map::new()),
}
}
@@ -638,6 +654,32 @@ pub fn check(m: &Module) -> Result<CheckedModule> {
Ok(CheckedModule { symbols })
}
/// Iter 15a: builds the ADT type-def table per module. Sibling of
/// [`build_module_globals`]: gives the body checker O(1) lookup of any
/// type declared anywhere in the workspace, keyed by module name.
/// Read by qualified `Type::Con.name` resolution
/// (`module.Type`), qualified `Term::Ctor.type_name`, and the
/// cross-module `Pattern::Ctor` fallback. Duplicate type / ctor errors
/// inside a single module surface during the per-module body-check
/// phase, not here.
fn build_module_types(
ws: &Workspace,
) -> BTreeMap<String, IndexMap<String, TypeDef>> {
let mut out: BTreeMap<String, IndexMap<String, TypeDef>> = BTreeMap::new();
for (mname, m) in &ws.modules {
let mut tys = IndexMap::new();
for def in &m.defs {
if let Def::Type(td) = def {
// Last definition wins on duplicate; the per-module
// body-check phase reports `duplicate-type` separately.
tys.insert(td.name.clone(), td.clone());
}
}
out.insert(mname.clone(), tys);
}
out
}
/// Builds the top-level symbol table per module (for cross-module lookup),
/// without checking bodies. Duplicates and dot-in-def names are reported
/// here as errors immediately — they would taint all further diagnostics.
@@ -754,6 +796,7 @@ fn check_in_workspace(
}
env.imports = import_map;
env.module_globals = module_globals.clone();
env.module_types = build_module_types(ws);
env.current_module = m.name.clone();
// Workspace isn't directly needed in the env; cross-module lookup uses
// only `module_globals`. But we keep the ws reference in the
@@ -805,7 +848,26 @@ fn check_type_well_formed(t: &Type, env: &Env) -> Result<()> {
}
return Ok(());
}
if let Some(td) = env.types.get(name) {
// Iter 15a: a qualified type name `module.Type` resolves
// through the import map and the per-module type table. The
// bare-name path is unchanged.
let td_opt: Option<&TypeDef> = if name.matches('.').count() == 1 {
let (prefix, suffix) = name.split_once('.').expect("checked");
let target_module = match env.imports.get(prefix) {
Some(m) => m.as_str(),
None => {
return Err(CheckError::UnknownModule {
module: prefix.to_string(),
});
}
};
env.module_types
.get(target_module)
.and_then(|tys| tys.get(suffix))
} else {
env.types.get(name)
};
if let Some(td) = td_opt {
if td.vars.len() != args.len() {
return Err(CheckError::UnknownType(format!(
"{name} expects {} type arg(s), got {}",
@@ -1053,12 +1115,23 @@ fn synth(
module: target_module.clone(),
}
})?;
g.get(suffix).cloned().ok_or_else(|| {
let raw_ty = g.get(suffix).cloned().ok_or_else(|| {
CheckError::UnknownImport {
module: target_module,
module: target_module.clone(),
name: suffix.to_string(),
}
})?
})?;
// Iter 15a: qualify any bare type-cons referring to a
// type defined in the owning module so that signatures
// pulled across the boundary unify against
// qualified-form ctors and types in the consumer
// module.
let owner_types = env
.module_types
.get(&target_module)
.cloned()
.unwrap_or_default();
qualify_local_types(&raw_ty, &target_module, &owner_types)
} else {
return Err(CheckError::UnknownIdent(name.clone()));
};
@@ -1152,11 +1225,31 @@ fn synth(
Ok(sig.ret)
}
Term::Ctor { type_name, ctor, args } => {
let td = env
.types
.get(type_name)
.ok_or_else(|| CheckError::UnknownType(type_name.clone()))?
.clone();
// Iter 15a: a qualified `type_name` (`module.Type`) resolves
// through the import map; the ctor name stays bare and is
// looked up inside the resolved TypeDef. The bare-name path
// is the original Iter 13 behaviour.
let td = if type_name.matches('.').count() == 1 {
let (prefix, suffix) = type_name.split_once('.').expect("checked");
let target_module = match env.imports.get(prefix) {
Some(m) => m.clone(),
None => {
return Err(CheckError::UnknownModule {
module: prefix.to_string(),
});
}
};
env.module_types
.get(&target_module)
.and_then(|tys| tys.get(suffix))
.cloned()
.ok_or_else(|| CheckError::UnknownType(type_name.clone()))?
} else {
env.types
.get(type_name)
.ok_or_else(|| CheckError::UnknownType(type_name.clone()))?
.clone()
};
let cdef = td
.ctors
.iter()
@@ -1248,9 +1341,25 @@ fn synth(
}
if !has_open_arm {
match &s_ty {
Type::Con { name, .. } if env.types.contains_key(name) => {
let td = &env.types[name];
// Iter 15a: a qualified scrutinee type (`module.Type`,
// produced by a cross-module ctor) resolves through
// `env.module_types`; bare names use `env.types` as
// before.
let td_opt: Option<&TypeDef> = match &s_ty {
Type::Con { name, .. } => {
if name.matches('.').count() == 1 {
let (prefix, suffix) = name.split_once('.').expect("checked");
env.module_types
.get(prefix)
.and_then(|tys| tys.get(suffix))
} else {
env.types.get(name)
}
}
_ => None,
};
match (td_opt, &s_ty) {
(Some(td), Type::Con { name, .. }) => {
let missing: Vec<String> = td
.ctors
.iter()
@@ -1325,6 +1434,52 @@ fn maybe_instantiate(t: Type, counter: &mut u32) -> Type {
}
}
/// Iter 15a: rewrites bare `Type::Con` references that resolve against
/// `local_types` into qualified `module.Type` form. Used when pulling a
/// fn type across module boundaries: a `Maybe a` declared inside
/// `std_maybe` becomes `std_maybe.Maybe a` when seen from a consumer
/// module — otherwise it would fail to unify with terms whose types
/// the consumer module already qualifies.
///
/// Already-qualified names, primitives (`Int`, `Bool`, `Unit`, `Str`),
/// rigid type vars, and type names that are not in `local_types` pass
/// through unchanged.
fn qualify_local_types(t: &Type, owner_module: &str, local_types: &IndexMap<String, TypeDef>) -> Type {
match t {
Type::Con { name, args } => {
let qualified_name = if name.contains('.') {
name.clone()
} else if matches!(name.as_str(), "Int" | "Bool" | "Unit" | "Str") {
name.clone()
} else if local_types.contains_key(name) {
format!("{owner_module}.{name}")
} else {
name.clone()
};
Type::Con {
name: qualified_name,
args: args
.iter()
.map(|a| qualify_local_types(a, owner_module, local_types))
.collect(),
}
}
Type::Fn { params, ret, effects } => Type::Fn {
params: params
.iter()
.map(|p| qualify_local_types(p, owner_module, local_types))
.collect(),
ret: Box::new(qualify_local_types(ret, owner_module, local_types)),
effects: effects.clone(),
},
Type::Forall { vars, body } => Type::Forall {
vars: vars.clone(),
body: Box::new(qualify_local_types(body, owner_module, local_types)),
},
Type::Var { .. } => t.clone(),
}
}
/// Checks a pattern against an expected type and returns the bindings
/// introduced by the pattern.
fn type_check_pattern(
@@ -1354,15 +1509,49 @@ fn type_check_pattern(
return Err(CheckError::NestedCtorPatternNotAllowed(ctor.clone()));
}
}
let cref = env
.ctor_index
.get(ctor)
.ok_or_else(|| CheckError::UnknownCtorInPattern(ctor.clone()))?;
// Iter 15a: try local ctor_index first; if the bare name
// doesn't resolve locally, fall back to scanning imported
// modules' type defs. The fallback lookup keys on the
// `module.Type` form so it lines up with what the typechecker
// produces for qualified `Term::Ctor`s. Multiple imported
// candidates → `ambiguous-ctor` (local always wins on
// conflict, hence the "imported only if local missing" order).
let resolved_type_name: String;
let resolved_td: TypeDef;
if let Some(cref) = env.ctor_index.get(ctor) {
resolved_type_name = cref.type_name.clone();
resolved_td = env.types[&cref.type_name].clone();
} else {
let mut hits: Vec<(String, TypeDef)> = Vec::new();
for imp in env.imports.values() {
if let Some(tys) = env.module_types.get(imp) {
for (tname, td) in tys {
if td.ctors.iter().any(|c| &c.name == ctor) {
hits.push((format!("{imp}.{tname}"), td.clone()));
}
}
}
}
match hits.len() {
0 => return Err(CheckError::UnknownCtorInPattern(ctor.clone())),
1 => {
let (qname, td) = hits.into_iter().next().expect("len == 1");
resolved_type_name = qname;
resolved_td = td;
}
_ => {
return Err(CheckError::AmbiguousCtor {
ctor: ctor.clone(),
candidates: hits.into_iter().map(|(q, _)| q).collect(),
});
}
}
}
// expected must be this ADT. For parameterised ADTs, capture
// the type-args so we can substitute them into the cdef's
// field types when binding sub-patterns.
let scrutinee_args: Vec<Type> = match expected {
Type::Con { name, args } if name == &cref.type_name => args.clone(),
Type::Con { name, args } if name == &resolved_type_name => args.clone(),
_ => {
return Err(CheckError::PatternTypeMismatch {
ctor: ctor.clone(),
@@ -1370,7 +1559,7 @@ fn type_check_pattern(
});
}
};
let td = &env.types[&cref.type_name];
let td = &resolved_td;
let cdef = td
.ctors
.iter()
@@ -1378,7 +1567,7 @@ fn type_check_pattern(
.expect("indexed ctor exists");
if fields.len() != cdef.fields.len() {
return Err(CheckError::CtorArity {
ty: cref.type_name.clone(),
ty: resolved_type_name.clone(),
ctor: ctor.clone(),
expected: cdef.fields.len(),
got: fields.len(),
@@ -1459,6 +1648,12 @@ pub struct Env {
/// Top-level symbol table per module of the workspace.
/// `check_in_workspace` populates this from `build_module_globals`.
pub module_globals: BTreeMap<String, IndexMap<String, Type>>,
/// Iter 15a: ADT type definitions per module of the workspace. Used
/// to resolve qualified type references (`module.Type` in
/// `Type::Con.name`) and qualified `Term::Ctor.type_name`, and to
/// fall back when a bare `Pattern::Ctor.ctor` cannot be resolved
/// against the local module. Populated by `check_in_workspace`.
pub module_types: BTreeMap<String, IndexMap<String, TypeDef>>,
/// Name of the currently checked module. Used during var lookup to
/// treat self-references (module name == own name) as local globals,
/// without touching the `imports` channel.
@@ -2188,6 +2383,218 @@ mod tests {
assert_eq!(err.code(), "tail-call-not-in-tail-position", "{err}");
}
// ----- Iter 15a: cross-module type / ctor resolution ------------------
/// Helper for the cross-module tests: build a workspace whose entry
/// imports `lib` and exposes its types via qualified names.
fn cross_module_ws(consumer: Module) -> Workspace {
// `lib` declares `data Box a = MkBox(a)` and `data Bag a = Pack(a)`.
// The second type lets us exercise ambiguous-ctor in dedicated tests.
let lib = Module {
schema: SCHEMA.into(),
name: "lib".into(),
imports: vec![],
defs: vec![
Def::Type(TypeDef {
name: "Box".into(),
vars: vec!["a".into()],
ctors: vec![Ctor {
name: "MkBox".into(),
fields: vec![Type::Var { name: "a".into() }],
}],
doc: None,
}),
Def::Type(TypeDef {
name: "Bag".into(),
vars: vec!["a".into()],
ctors: vec![Ctor {
name: "Pack".into(),
fields: vec![Type::Var { name: "a".into() }],
}],
doc: None,
}),
],
};
let mut modules = BTreeMap::new();
modules.insert("lib".into(), lib);
modules.insert(consumer.name.clone(), consumer.clone());
Workspace {
entry: consumer.name,
modules,
root_dir: std::path::PathBuf::from("."),
}
}
/// Iter 15a, path 1: a qualified `Type::Con` (`lib.Box`) resolves
/// through `module_types` rather than the local-module `types`.
#[test]
fn cross_module_qualified_type_in_param_resolves() {
let consumer = Module {
schema: SCHEMA.into(),
name: "use_lib".into(),
imports: vec![Import { module: "lib".into(), alias: None }],
defs: vec![fn_def(
"noop",
Type::Fn {
params: vec![Type::Con {
name: "lib.Box".into(),
args: vec![Type::int()],
}],
ret: Box::new(Type::int()),
effects: vec![],
},
vec!["b"],
Term::Lit { lit: Literal::Int { value: 0 } },
)],
};
let ws = cross_module_ws(consumer);
let diags = check_workspace(&ws);
assert!(diags.is_empty(), "expected green; got {diags:?}");
}
/// Iter 15a, path 2: a qualified `Term::Ctor.type_name`
/// (`lib.Box`) resolves and constructs `lib.Box<Int>`.
#[test]
fn cross_module_qualified_term_ctor_resolves() {
let consumer = Module {
schema: SCHEMA.into(),
name: "use_lib".into(),
imports: vec![Import { module: "lib".into(), alias: None }],
defs: vec![fn_def(
"make",
Type::Fn {
params: vec![],
ret: Box::new(Type::Con {
name: "lib.Box".into(),
args: vec![Type::int()],
}),
effects: vec![],
},
vec![],
Term::Ctor {
type_name: "lib.Box".into(),
ctor: "MkBox".into(),
args: vec![Term::Lit { lit: Literal::Int { value: 7 } }],
},
)],
};
let ws = cross_module_ws(consumer);
let diags = check_workspace(&ws);
assert!(diags.is_empty(), "expected green; got {diags:?}");
}
/// Iter 15a, path 3: a bare `Pattern::Ctor.ctor` falls back through
/// imports when the ctor isn't local. The scrutinee carries the
/// qualified type so the pattern check finds the right ADT.
#[test]
fn cross_module_pat_ctor_fallback_resolves() {
let consumer = Module {
schema: SCHEMA.into(),
name: "use_lib".into(),
imports: vec![Import { module: "lib".into(), alias: None }],
defs: vec![fn_def(
"open",
Type::Fn {
params: vec![Type::Con {
name: "lib.Box".into(),
args: vec![Type::int()],
}],
ret: Box::new(Type::int()),
effects: vec![],
},
vec!["b"],
Term::Match {
scrutinee: Box::new(Term::Var { name: "b".into() }),
arms: vec![Arm {
pat: Pattern::Ctor {
ctor: "MkBox".into(),
fields: vec![Pattern::Var { name: "x".into() }],
},
body: Term::Var { name: "x".into() },
}],
},
)],
};
let ws = cross_module_ws(consumer);
let diags = check_workspace(&ws);
assert!(diags.is_empty(), "expected green; got {diags:?}");
}
/// Iter 15a, path 4: a bare ctor name that resolves in two
/// imported modules surfaces as `ambiguous-ctor` with both
/// candidates listed.
#[test]
fn cross_module_pat_ctor_ambiguous_errors() {
// Two different libs, each declaring a ctor `Mk` (intentional clash).
let lib_a = Module {
schema: SCHEMA.into(),
name: "lib_a".into(),
imports: vec![],
defs: vec![Def::Type(TypeDef {
name: "TA".into(),
vars: vec![],
ctors: vec![Ctor { name: "Mk".into(), fields: vec![] }],
doc: None,
})],
};
let lib_b = Module {
schema: SCHEMA.into(),
name: "lib_b".into(),
imports: vec![],
defs: vec![Def::Type(TypeDef {
name: "TB".into(),
vars: vec![],
ctors: vec![Ctor { name: "Mk".into(), fields: vec![] }],
doc: None,
})],
};
let consumer = Module {
schema: SCHEMA.into(),
name: "use_both".into(),
imports: vec![
Import { module: "lib_a".into(), alias: None },
Import { module: "lib_b".into(), alias: None },
],
defs: vec![fn_def(
"f",
Type::Fn {
params: vec![Type::Con {
name: "lib_a.TA".into(),
args: vec![],
}],
ret: Box::new(Type::int()),
effects: vec![],
},
vec!["t"],
Term::Match {
scrutinee: Box::new(Term::Var { name: "t".into() }),
arms: vec![Arm {
// Bare `Mk` is ambiguous between lib_a and lib_b.
pat: Pattern::Ctor {
ctor: "Mk".into(),
fields: vec![],
},
body: Term::Lit { lit: Literal::Int { value: 0 } },
}],
},
)],
};
let mut modules = BTreeMap::new();
modules.insert("lib_a".into(), lib_a);
modules.insert("lib_b".into(), lib_b);
modules.insert("use_both".into(), consumer);
let ws = Workspace {
entry: "use_both".into(),
modules,
root_dir: std::path::PathBuf::from("."),
};
let diags = check_workspace(&ws);
assert!(
diags.iter().any(|d| d.code == "ambiguous-ctor"),
"expected ambiguous-ctor diagnostic; got {diags:?}"
);
}
/// Iter 14e: a `Term::App { tail: true, .. }` that genuinely sits
/// in tail position (as the rhs of a `Seq` that is the body of a
/// `Match` arm that is the body of the fn) must pass.
+249 -51
View File
@@ -182,10 +182,16 @@ pub fn lower_workspace(ws: &Workspace) -> Result<String> {
let mut module_user_fns: BTreeMap<String, BTreeMap<String, FnSig>> = BTreeMap::new();
let mut module_def_ail_types: BTreeMap<String, BTreeMap<String, Type>> = BTreeMap::new();
let mut module_polymorphic_fns: BTreeMap<String, BTreeMap<String, FnDef>> = BTreeMap::new();
// Iter 15a: cross-module ctor table. Maps module name → ctor name →
// CtorRef (with `type_name` *unqualified*, since the ctor is defined
// in that module). Cross-module ctor lookups resolve through this
// table instead of the per-Emitter `ctor_index`.
let mut module_ctor_index: BTreeMap<String, BTreeMap<String, CtorRef>> = BTreeMap::new();
for (mname, m) in &ws.modules {
let mut user_fns = BTreeMap::new();
let mut ail_types = BTreeMap::new();
let mut poly_fns = BTreeMap::new();
let mut ctors = BTreeMap::new();
for def in &m.defs {
if let Def::Fn(f) = def {
ail_types.insert(f.name.clone(), f.ty.clone());
@@ -205,10 +211,30 @@ pub fn lower_workspace(ws: &Workspace) -> Result<String> {
_ => {}
}
}
if let Def::Type(td) = def {
for (i, c) in td.ctors.iter().enumerate() {
let fields: Vec<String> = c
.fields
.iter()
.map(|t| llvm_type(t).unwrap_or_else(|_| "ptr".into()))
.collect();
ctors.insert(
c.name.clone(),
CtorRef {
type_name: td.name.clone(),
tag: i as u32,
fields,
ail_fields: c.fields.clone(),
type_vars: td.vars.clone(),
},
);
}
}
}
module_user_fns.insert(mname.clone(), user_fns);
module_def_ail_types.insert(mname.clone(), ail_types);
module_polymorphic_fns.insert(mname.clone(), poly_fns);
module_ctor_index.insert(mname.clone(), ctors);
}
// Pass 2: lower per module. Globals/strings are accumulated per module,
@@ -229,6 +255,7 @@ pub fn lower_workspace(ws: &Workspace) -> Result<String> {
&module_user_fns,
&module_def_ail_types,
&module_polymorphic_fns,
&module_ctor_index,
import_map,
);
emitter
@@ -349,13 +376,18 @@ struct Emitter<'a> {
/// Import map of the current module (alias/module name → actual module name).
import_map: BTreeMap<String, String>,
/// ADT table: type_name -> list of ctors in definition order.
/// Tag of a ctor = index in this list. Replicated in `ctor_index`;
/// kept around for future tools (pretty-printer for ADT values,
/// Tag of a ctor = index in this list.
/// Kept around for future tools (pretty-printer for ADT values,
/// decision-tree optimization).
#[allow(dead_code)]
types: BTreeMap<String, Vec<CtorInfo>>,
/// Inverse index: ctor name -> (type_name, tag, field_llvm_types).
ctor_index: BTreeMap<String, CtorRef>,
/// Iter 15a: cross-module ctor index, keyed by module name. Used by
/// `lookup_ctor_by_type` (for `Term::Ctor.type_name`) and
/// `lookup_ctor_in_pattern` (for `Pattern::Ctor.ctor`). Built once
/// per workspace and shared by every Emitter. Replaces the per-
/// emitter `ctor_index` of pre-15a — that table only knew the
/// current module's ctors and broke on cross-module references.
module_ctor_index: &'a BTreeMap<String, BTreeMap<String, CtorRef>>,
/// Current basic block label. Set by `start_block` and is
/// the single source of truth for `phi` operands.
current_block: String,
@@ -426,14 +458,14 @@ impl<'a> Emitter<'a> {
module_user_fns: &'a BTreeMap<String, BTreeMap<String, FnSig>>,
module_def_ail_types: &'a BTreeMap<String, BTreeMap<String, Type>>,
module_polymorphic_fns: &'a BTreeMap<String, BTreeMap<String, FnDef>>,
module_ctor_index: &'a BTreeMap<String, BTreeMap<String, CtorRef>>,
import_map: BTreeMap<String, String>,
) -> Self {
let mut types: BTreeMap<String, Vec<CtorInfo>> = BTreeMap::new();
let mut ctor_index: BTreeMap<String, CtorRef> = BTreeMap::new();
for def in &module.defs {
if let Def::Type(td) = def {
let mut infos = Vec::new();
for (i, c) in td.ctors.iter().enumerate() {
for c in td.ctors.iter() {
// Iter 13b: precomputed LLVM field types are only
// meaningful for monomorphic ADTs. For parameterised
// ADTs the field types reference free `Type::Var`s
@@ -450,18 +482,8 @@ impl<'a> Emitter<'a> {
.collect();
infos.push(CtorInfo {
name: c.name.clone(),
fields: fields.clone(),
fields,
});
ctor_index.insert(
c.name.clone(),
CtorRef {
type_name: td.name.clone(),
tag: i as u32,
fields,
ail_fields: c.fields.clone(),
type_vars: td.vars.clone(),
},
);
}
types.insert(td.name.clone(), infos);
}
@@ -483,7 +505,7 @@ impl<'a> Emitter<'a> {
mono_emitted: BTreeSet::new(),
import_map,
types,
ctor_index,
module_ctor_index,
current_block: String::new(),
block_terminated: false,
ssa_fn_sigs: BTreeMap::new(),
@@ -948,6 +970,122 @@ impl<'a> Emitter<'a> {
}
}
/// Iter 15a: resolves a ctor reference by `type_name` (which may be
/// qualified `module.T` or bare `T`) plus a bare ctor name. Returns
/// the same `CtorRef` shape used by the local `ctor_index`. The
/// returned `CtorRef.type_name` is always the bare type name as
/// declared in the owning module. The current `module_name` is the
/// authority for "bare" — that lets a specialised fn body emitted
/// under a swapped `module_name` (see `emit_specialised_fn`)
/// resolve its bare ctor references against the *owner* module's
/// ctor table, not the consumer's.
fn lookup_ctor_by_type(
&self,
type_name: &str,
ctor_name: &str,
) -> Result<CtorRef> {
if type_name.matches('.').count() == 1 {
let (prefix, suffix) = type_name.split_once('.').expect("checked");
let target_module = self.import_map.get(prefix).cloned().ok_or_else(|| {
CodegenError::Internal(format!(
"qualified ctor `{type_name}/{ctor_name}`: prefix `{prefix}` not in import map"
))
})?;
let cref = self
.module_ctor_index
.get(&target_module)
.and_then(|m| m.get(ctor_name))
.cloned()
.ok_or_else(|| {
CodegenError::Internal(format!(
"qualified ctor `{type_name}/{ctor_name}` not in module `{target_module}`"
))
})?;
if cref.type_name != suffix {
return Err(CodegenError::Internal(format!(
"ctor `{ctor_name}` belongs to `{}`, not `{type_name}`",
cref.type_name
)));
}
Ok(cref)
} else {
let cref = self
.module_ctor_index
.get(self.module_name)
.and_then(|m| m.get(ctor_name))
.cloned()
.ok_or_else(|| {
CodegenError::Internal(format!(
"unknown ctor `{ctor_name}` in module `{}`",
self.module_name
))
})?;
if cref.type_name != type_name {
return Err(CodegenError::Internal(format!(
"ctor `{ctor_name}` belongs to `{}`, not `{type_name}`",
cref.type_name
)));
}
Ok(cref)
}
}
/// Iter 15a: collects the set of type names declared in `owner_module`.
/// Used to mirror the typechecker's `qualify_local_types` rewrite
/// when reading a polymorphic fn's signature pulled across the
/// import boundary.
fn collect_owner_local_types(&self, owner_module: &str) -> BTreeSet<String> {
self.module_ctor_index
.get(owner_module)
.map(|m| {
m.values()
.map(|c| c.type_name.clone())
.collect::<BTreeSet<_>>()
})
.unwrap_or_default()
}
/// Iter 15a: resolves a ctor in pattern position. The current
/// `module_name`'s ctor table is consulted first; on miss, the
/// imported modules are scanned (the typechecker has already
/// vetted unambiguity, so the first hit wins — local always
/// shadows imported on conflict). Using `module_name` rather than
/// `self.ctor_index` matters when emitting a specialised fn body
/// in the owner's module context (see `emit_specialised_fn`).
fn lookup_ctor_in_pattern(&self, ctor_name: &str) -> Result<CtorRef> {
if let Some(cref) = self
.module_ctor_index
.get(self.module_name)
.and_then(|m| m.get(ctor_name))
.cloned()
{
return Ok(cref);
}
// Walk the *current* module's imports for fallback. When
// emitting a specialised fn body in another module, the
// emitter's `import_map` is still the consumer's; we want the
// owner's. Look up the owner module's import map indirectly
// through `self.module` whenever it equals `self.module_name`,
// and fall back to the active `import_map` only when we are
// genuinely emitting in the consumer module. Since
// `emit_specialised_fn` swaps only `module_name`, not
// `import_map`, the fallback below covers both cases by
// additionally searching every module in `module_ctor_index`
// — that's cheap (number of modules in a workspace is small)
// and the typechecker has already pinned uniqueness.
for (mname, ctors) in self.module_ctor_index.iter() {
if mname == self.module_name {
continue;
}
if let Some(cref) = ctors.get(ctor_name).cloned() {
return Ok(cref);
}
}
Err(CodegenError::Internal(format!(
"unknown ctor in pattern: `{ctor_name}`"
)))
}
/// Heap box layout: 8 bytes tag (i64) followed by 8 bytes per field.
/// i1 and i8 fields also occupy a full 8-byte slot — the typed
/// load/store instructions write/read only the required size.
@@ -957,21 +1095,7 @@ impl<'a> Emitter<'a> {
ctor_name: &str,
args: &[Term],
) -> Result<(String, String)> {
let cref = self
.ctor_index
.get(ctor_name)
.cloned()
.ok_or_else(|| {
CodegenError::Internal(format!(
"unknown ctor `{ctor_name}`"
))
})?;
if cref.type_name != type_name {
return Err(CodegenError::Internal(format!(
"ctor `{ctor_name}` belongs to `{}`, not `{type_name}`",
cref.type_name
)));
}
let cref = self.lookup_ctor_by_type(type_name, ctor_name)?;
if args.len() != cref.ail_fields.len() {
return Err(CodegenError::Internal(format!(
"ctor `{type_name}/{ctor_name}` arity"
@@ -1068,15 +1192,9 @@ impl<'a> Emitter<'a> {
open_var = Some(name.clone());
}
Pattern::Ctor { ctor, fields } => {
let cref = self
.ctor_index
.get(ctor)
.cloned()
.ok_or_else(|| {
CodegenError::Internal(format!(
"unknown ctor in pattern: `{ctor}`"
))
})?;
// Iter 15a: lookup falls back to imported modules when
// a bare ctor name doesn't resolve locally.
let cref = self.lookup_ctor_in_pattern(ctor)?;
let bindings: Vec<Option<String>> = fields
.iter()
.map(|p| match p {
@@ -1386,6 +1504,23 @@ impl<'a> Emitter<'a> {
)));
}
};
// Iter 15a: when we're calling into another module, the fn's
// params and ret reference local type names that — from this
// call site's perspective — are qualified `module.T`. Qualify
// before deriving the substitution so the unification mirrors
// what the typechecker has already validated.
let (params, ret) = if owner_module != self.module_name {
let owner_types = self.collect_owner_local_types(owner_module);
(
params
.iter()
.map(|p| qualify_local_types_codegen(p, owner_module, &owner_types))
.collect::<Vec<_>>(),
qualify_local_types_codegen(&ret, owner_module, &owner_types),
)
} else {
(params, ret)
};
// Derive the substitution by comparing the declared param
// types against the actual arg types.
@@ -2118,7 +2253,18 @@ impl<'a> Emitter<'a> {
.get(target)
.and_then(|m| m.get(suffix))
{
return Ok(ty.clone());
// Iter 15a: qualify any bare type-cons that
// refer to types declared in `target` so the
// returned signature lines up with the
// qualified ctors / type names produced
// elsewhere in the consumer module. Mirrors
// the typechecker's `qualify_local_types`.
let owner_local_types = self.collect_owner_local_types(target);
return Ok(qualify_local_types_codegen(
ty,
target,
&owner_local_types,
));
}
}
}
@@ -2188,11 +2334,10 @@ impl<'a> Emitter<'a> {
// types. For monomorphic ADTs (`type_vars.is_empty()`)
// we keep the pre-13b shape `Type::Con { args: vec![] }`
// — matching what the typechecker produces.
let cref = self.ctor_index.get(ctor).cloned().ok_or_else(|| {
CodegenError::Internal(format!(
"synth_arg_type: unknown ctor `{ctor}`"
))
})?;
// Iter 15a: a qualified `type_name` resolves through the
// cross-module ctor index. The result `Type::Con.name`
// stays qualified to match what the typechecker emits.
let cref = self.lookup_ctor_by_type(type_name, ctor)?;
if cref.type_vars.is_empty() {
return Ok(Type::Con {
name: type_name.clone(),
@@ -2359,11 +2504,17 @@ fn derive_substitution(
// through return-type unification, but at the call site we only
// see args; if needed, callers can extend this with expected-ret
// info.
// Iter 15a: a forall var that the args couldn't pin (e.g.
// `is_none(Nothing) : forall a. (Maybe a) -> Bool` — `a` is
// genuinely unobservable from the args alone) defaults to `Unit`.
// The specialised body must not actually read an `a`-typed value,
// or it would have failed type-checking; a dummy concrete type is
// sound and lets monomorphisation proceed deterministically. The
// descriptor uses the same default, so all such call sites
// converge on a single specialisation.
for v in vars {
if !subst.contains_key(v) {
return Err(CodegenError::Internal(format!(
"monomorphisation: type var `{v}` not pinned by call args"
)));
subst.insert(v.clone(), Type::unit());
}
}
Ok(subst)
@@ -2436,6 +2587,53 @@ fn unify_for_subst(
}
}
/// Iter 15a: rewrites bare `Type::Con` references that resolve against
/// `owner_local_types` into qualified `module.Type` form. Mirrors
/// `ailang_check::qualify_local_types`. Used when the codegen pulls a
/// polymorphic fn signature across the import boundary; without this
/// the substitution derived from the call site's qualified args
/// (`std_maybe.Maybe<Int>`) would fail to unify against the bare
/// signature (`Maybe<a>`).
fn qualify_local_types_codegen(
t: &Type,
owner_module: &str,
owner_local_types: &BTreeSet<String>,
) -> Type {
match t {
Type::Con { name, args } => {
let qualified = if name.contains('.') {
name.clone()
} else if matches!(name.as_str(), "Int" | "Bool" | "Unit" | "Str") {
name.clone()
} else if owner_local_types.contains(name) {
format!("{owner_module}.{name}")
} else {
name.clone()
};
Type::Con {
name: qualified,
args: args
.iter()
.map(|a| qualify_local_types_codegen(a, owner_module, owner_local_types))
.collect(),
}
}
Type::Fn { params, ret, effects } => Type::Fn {
params: params
.iter()
.map(|p| qualify_local_types_codegen(p, owner_module, owner_local_types))
.collect(),
ret: Box::new(qualify_local_types_codegen(ret, owner_module, owner_local_types)),
effects: effects.clone(),
},
Type::Forall { vars, body } => Type::Forall {
vars: vars.clone(),
body: Box::new(qualify_local_types_codegen(body, owner_module, owner_local_types)),
},
Type::Var { .. } => t.clone(),
}
}
/// Iter 12b: substitute rigid type vars in `t` according to `subst`.
/// Used to specialise the type of a polymorphic def for a given
/// instantiation.
+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.
+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)))))))))))