diff --git a/crates/ail/tests/e2e.rs b/crates/ail/tests/e2e.rs index 5cf85ae..dbd5bd2 100644 --- a/crates/ail/tests/e2e.rs +++ b/crates/ail/tests/e2e.rs @@ -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. diff --git a/crates/ailang-check/src/diagnostic.rs b/crates/ailang-check/src/diagnostic.rs index bc0a2f2..c22d7c0 100644 --- a/crates/ailang-check/src/diagnostic.rs +++ b/crates/ailang-check/src/diagnostic.rs @@ -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": "", "candidates": ["m1.T", "m2.T"]}` (Iter 15a) use serde::Serialize; diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 757f6da..087b05a 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -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": "", "candidates": ["m1.T", "m2.T"]}`. + #[error("ambiguous constructor `{ctor}`: declared in {candidates:?}")] + AmbiguousCtor { + ctor: String, + candidates: Vec, + }, } type Result = std::result::Result; @@ -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 { 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> { + let mut out: BTreeMap> = 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 = 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) -> 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 = 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>, + /// 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>, /// 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`. + #[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. diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index 3adba84..0809f51 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -182,10 +182,16 @@ pub fn lower_workspace(ws: &Workspace) -> Result { let mut module_user_fns: BTreeMap> = BTreeMap::new(); let mut module_def_ail_types: BTreeMap> = BTreeMap::new(); let mut module_polymorphic_fns: BTreeMap> = 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> = 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 { _ => {} } } + if let Def::Type(td) = def { + for (i, c) in td.ctors.iter().enumerate() { + let fields: Vec = 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 { &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, /// 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>, - /// Inverse index: ctor name -> (type_name, tag, field_llvm_types). - ctor_index: BTreeMap, + /// 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>, /// 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>, module_def_ail_types: &'a BTreeMap>, module_polymorphic_fns: &'a BTreeMap>, + module_ctor_index: &'a BTreeMap>, import_map: BTreeMap, ) -> Self { let mut types: BTreeMap> = BTreeMap::new(); - let mut ctor_index: BTreeMap = 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 { + 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 { + self.module_ctor_index + .get(owner_module) + .map(|m| { + m.values() + .map(|c| c.type_name.clone()) + .collect::>() + }) + .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 { + 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> = 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::>(), + 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`) would fail to unify against the bare +/// signature (`Maybe`). +fn qualify_local_types_codegen( + t: &Type, + owner_module: &str, + owner_local_types: &BTreeSet, +) -> 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. diff --git a/docs/JOURNAL.md b/docs/JOURNAL.md index e8118f1..0436608 100644 --- a/docs/JOURNAL.md +++ b/docs/JOURNAL.md @@ -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>` 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` 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` is reusable across modules, +write `std_list.ailx` importing `std_maybe`. Combinators: +`length`, `head` (returns `Maybe`), `tail` (returns +`Maybe>`), `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. + diff --git a/examples/std_maybe.ail.json b/examples/std_maybe.ail.json new file mode 100644 index 0000000..a920447 --- /dev/null +++ b/examples/std_maybe.ail.json @@ -0,0 +1 @@ +{"defs":[{"ctors":[{"fields":[],"name":"Nothing"},{"fields":[{"k":"var","name":"a"}],"name":"Just"}],"doc":"Polymorphic optional value: either Just 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 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"} \ No newline at end of file diff --git a/examples/std_maybe_demo.ail.json b/examples/std_maybe_demo.ail.json new file mode 100644 index 0000000..9e6140e --- /dev/null +++ b/examples/std_maybe_demo.ail.json @@ -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"} \ No newline at end of file diff --git a/examples/std_maybe_demo.ailx b/examples/std_maybe_demo.ailx new file mode 100644 index 0000000..2b4e57d --- /dev/null +++ b/examples/std_maybe_demo.ailx @@ -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)))))))))))