From 92f4b4f8c7f9d953374ed1620f43744a71bcaee3 Mon Sep 17 00:00:00 2001 From: Brummel Date: Thu, 7 May 2026 18:34:49 +0200 Subject: [PATCH] Iter 15b: std_list ships, three more compiler gaps closed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second stdlib module. Tester wrote std_list.ailx (10 combinators, 164 LOC) and a consumer demo. std_list typechecked standalone; demo did not, surfacing three compiler bugs: 1. Check-side: Iter 14h's qualify_local_types was applied to Term::Var cross-module lookup but not to ctor-field types in Term::Ctor synth or Pattern::Ctor resolution. First recursive cross-module ADT (List has Cons a (List a) — recursive Con self-ref) triggers the bug. std_maybe slipped through because Maybe's ctors have no recursive Con field. 2. Codegen-side: same gap mirrored across 4 sites in codegen (Term::Ctor synth, lower_ctor, lower_match) plus a tweak to unify_for_subst (recurse on re-bind instead of strict equality so sibling-derived List accepts nullary-ctor's List<$u> wildcard). 3. Const codegen: emit_const rejected non-literal const bodies. The demo's xs : List = Cons 1 (...) requires it. Fix: per-module const table, Term::Var resolution loads literal consts from global, inlines non-literal bodies. Bare and qualified refs both supported. All three fixes carry an "Iter 15b" code comment at their site. ~349/25 LOC across ailang-check, ailang-codegen, e2e.rs. Tests 85 -> 87. New e2e std_list_demo asserts 11-line stdout: length 5, is_empty false/true, head via from_maybe, tail length, append length, reverse head, map double head, filter is_even length, fold_left sum, fold_right sum. New ailang-check unit test cross_module_recursive_adt_term_and_pat_ctor covers both the original bug and the symmetric pat-ctor latent twin. Hash invariance: all pre-15b fixtures + std_maybe defs bit-identical. 14a / 14e / 14h regressions all green. Cumulative state: 2 stdlib modules (std_maybe, std_list), 14 combinators, cross-module recursive ADT working end-to-end. Three compiler bugs surfaced + fixed in dogfood since 14a (each dogfood iter has surfaced ≥1). Authoring observation: form (A) at 10 combinators is fine; main friction is paren-counting in nested seq chains, not the form itself. n-ary seq would help but is sugar. Plan 15c: 1000-element list stress test for fold_left (tail-call- marked) vs fold_right (constructor-blocked). Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/ail/tests/e2e.rs | 19 ++++ crates/ailang-check/src/lib.rs | 178 +++++++++++++++++++++++++++++-- crates/ailang-codegen/src/lib.rs | 177 +++++++++++++++++++++++++++--- docs/JOURNAL.md | 118 ++++++++++++++++++++ examples/std_list.ail.json | 1 + examples/std_list.ailx | 164 ++++++++++++++++++++++++++++ examples/std_list_demo.ail.json | 1 + examples/std_list_demo.ailx | 61 +++++++++++ 8 files changed, 694 insertions(+), 25 deletions(-) create mode 100644 examples/std_list.ail.json create mode 100644 examples/std_list.ailx create mode 100644 examples/std_list_demo.ail.json create mode 100644 examples/std_list_demo.ailx diff --git a/crates/ail/tests/e2e.rs b/crates/ail/tests/e2e.rs index dbd5bd2..907ab71 100644 --- a/crates/ail/tests/e2e.rs +++ b/crates/ail/tests/e2e.rs @@ -255,6 +255,25 @@ fn cross_module_maybe_demo() { assert_eq!(lines, vec!["7", "99", "true", "true", "42"]); } +/// Iter 15b: drives the polymorphic `std_list` combinators end-to-end. +/// Exercises a recursive cross-module ADT (`std_list.List`) consumed +/// from a separate module that also imports `std_maybe`. Guards the +/// `qualify_local_types` propagation in both `Term::Ctor` synth and +/// `Term::Match` field binding — without it, recursive ctor fields +/// (`Cons a (List a)`) stay unqualified at the cross-module use site +/// and fail to unify against the qualified scrutinee args. +#[test] +fn std_list_demo() { + let stdout = build_and_run("std_list_demo.ail.json"); + let lines: Vec<&str> = stdout.lines().collect(); + assert_eq!( + lines, + vec![ + "5", "false", "true", "1", "4", "10", "5", "2", "2", "15", "15" + ] + ); +} + /// 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/lib.rs b/crates/ailang-check/src/lib.rs index 087b05a..46b7bd6 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -1229,6 +1229,16 @@ fn synth( // 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. + // + // Iter 15b: when the type is cross-module, `cdef.fields` is + // written in the owning module's local namespace. A recursive + // self-reference like `Cons a (List a)` carries `Con + // { name: "List" }` even though, from the consumer's view, + // the type is `std_list.List`. Apply `qualify_local_types` + // with the owning module so that recursive ctor field types + // (and any other locally-named cross-module type-cons) are + // qualified before substitution / unification. + let owning_module: Option; 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) { @@ -1239,12 +1249,15 @@ fn synth( }); } }; - env.module_types + let td = env.module_types .get(&target_module) .and_then(|tys| tys.get(suffix)) .cloned() - .ok_or_else(|| CheckError::UnknownType(type_name.clone()))? + .ok_or_else(|| CheckError::UnknownType(type_name.clone()))?; + owning_module = Some(target_module); + td } else { + owning_module = None; env.types .get(type_name) .ok_or_else(|| CheckError::UnknownType(type_name.clone()))? @@ -1267,6 +1280,23 @@ fn synth( got: args.len(), }); } + // Iter 15b: qualify local type-cons in the field types when + // the ctor's owning type is cross-module. No-op when the + // type is local (owning_module is None). + let qualified_fields: Vec = match &owning_module { + Some(m) => { + let owner_types = env + .module_types + .get(m) + .cloned() + .unwrap_or_default(); + cdef.fields + .iter() + .map(|f| qualify_local_types(f, m, &owner_types)) + .collect() + } + None => cdef.fields.clone(), + }; // Iter 13a: parameterised ADT — instantiate the type's vars // with fresh metavars, substitute them through every ctor // field type, and let the field types' metavars be solved by @@ -1280,7 +1310,7 @@ fn synth( mapping.insert(v.clone(), m.clone()); type_args.push(m); } - for (a, exp) in args.iter().zip(cdef.fields.iter()) { + for (a, exp) in args.iter().zip(qualified_fields.iter()) { let exp_inst = substitute_rigids(exp, &mapping); let actual = synth(a, env, locals, effects, in_def, subst, counter)?; unify(&exp_inst, &actual, subst)?; @@ -1516,18 +1546,30 @@ fn type_check_pattern( // produces for qualified `Term::Ctor`s. Multiple imported // candidates → `ambiguous-ctor` (local always wins on // conflict, hence the "imported only if local missing" order). + // + // Iter 15b: track whether the resolved ctor lives in an + // imported module. If so, the cdef's recursive self-references + // need `qualify_local_types` (symmetric to the term-ctor fix); + // otherwise their bare names will not unify against the + // qualified scrutinee args. let resolved_type_name: String; let resolved_td: TypeDef; + let resolved_owning_module: Option; if let Some(cref) = env.ctor_index.get(ctor) { resolved_type_name = cref.type_name.clone(); resolved_td = env.types[&cref.type_name].clone(); + resolved_owning_module = None; } else { - let mut hits: Vec<(String, TypeDef)> = Vec::new(); + let mut hits: Vec<(String, 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())); + hits.push(( + format!("{imp}.{tname}"), + imp.clone(), + td.clone(), + )); } } } @@ -1535,14 +1577,16 @@ fn type_check_pattern( match hits.len() { 0 => return Err(CheckError::UnknownCtorInPattern(ctor.clone())), 1 => { - let (qname, td) = hits.into_iter().next().expect("len == 1"); + let (qname, owner, td) = + hits.into_iter().next().expect("len == 1"); resolved_type_name = qname; resolved_td = td; + resolved_owning_module = Some(owner); } _ => { return Err(CheckError::AmbiguousCtor { ctor: ctor.clone(), - candidates: hits.into_iter().map(|(q, _)| q).collect(), + candidates: hits.into_iter().map(|(q, _, _)| q).collect(), }); } } @@ -1577,6 +1621,24 @@ fn type_check_pattern( // into each cdef field type (e.g. `Cons(a, List a)` checked // against a `List Int` scrutinee yields field types // `Int, List Int`). + // + // Iter 15b: when the resolved ctor lives in an imported + // module, qualify any bare local type-cons in the field + // types first — symmetric to the term-ctor fix. + let qualified_fields: Vec = match &resolved_owning_module { + Some(m) => { + let owner_types = env + .module_types + .get(m) + .cloned() + .unwrap_or_default(); + cdef.fields + .iter() + .map(|f| qualify_local_types(f, m, &owner_types)) + .collect() + } + None => cdef.fields.clone(), + }; let mapping: BTreeMap = td .vars .iter() @@ -1584,7 +1646,7 @@ fn type_check_pattern( .zip(scrutinee_args) .collect(); let mut out = Vec::new(); - for (sub, sub_ty) in fields.iter().zip(cdef.fields.iter()) { + for (sub, sub_ty) in fields.iter().zip(qualified_fields.iter()) { let sub_ty_inst = substitute_rigids(sub_ty, &mapping); out.extend(type_check_pattern(sub, &sub_ty_inst, env)?); } @@ -2595,6 +2657,106 @@ mod tests { ); } + /// Iter 15b: a recursive cross-module ADT (`std_list.List a` with a + /// `Cons a (List a)` ctor) round-trips through both `Term::Ctor` synth + /// and pattern-ctor binding without unqualified-field-name unification + /// failures. The bug fixed in 15b: `cdef.fields` on the imported side + /// carries `Con("List", _)` (unqualified, owner's local namespace), + /// while the consumer-visible scrutinee/result type is + /// `Con("std_list.List", _)`. Without `qualify_local_types` applied + /// to the fields at use sites, `unify` rejected the mismatch. + #[test] + fn cross_module_recursive_adt_term_and_pat_ctor() { + // Library: `data List a = Nil | Cons a (List a)`. The `Cons` + // field types reference the local-namespace `List`, exactly + // mirroring the std_list shape that tripped the gap. + let lib = Module { + schema: SCHEMA.into(), + name: "lst".into(), + imports: vec![], + defs: vec![Def::Type(TypeDef { + name: "List".into(), + vars: vec!["a".into()], + ctors: vec![ + Ctor { name: "Nil".into(), fields: vec![] }, + Ctor { + name: "Cons".into(), + fields: vec![ + Type::Var { name: "a".into() }, + Type::Con { + name: "List".into(), + args: vec![Type::Var { name: "a".into() }], + }, + ], + }, + ], + doc: None, + })], + }; + // Consumer: builds `Cons 1 (Cons 2 (Nil))` via qualified + // `lst.List/Cons` and pattern-matches it back out. The match + // arm exercises the symmetric pat-ctor fix. + let cons = |head: i64, tail: Term| Term::Ctor { + type_name: "lst.List".into(), + ctor: "Cons".into(), + args: vec![ + Term::Lit { lit: Literal::Int { value: head } }, + tail, + ], + }; + let nil = Term::Ctor { + type_name: "lst.List".into(), + ctor: "Nil".into(), + args: vec![], + }; + let consumer = Module { + schema: SCHEMA.into(), + name: "uses_lst".into(), + imports: vec![Import { module: "lst".into(), alias: None }], + defs: vec![fn_def( + "head_or_zero", + Type::Fn { + params: vec![], + ret: Box::new(Type::int()), + effects: vec![], + }, + vec![], + Term::Match { + scrutinee: Box::new(cons(1, cons(2, nil.clone()))), + arms: vec![ + Arm { + pat: Pattern::Ctor { + ctor: "Cons".into(), + fields: vec![ + Pattern::Var { name: "h".into() }, + Pattern::Wild, + ], + }, + body: Term::Var { name: "h".into() }, + }, + Arm { + pat: Pattern::Ctor { + ctor: "Nil".into(), + fields: vec![], + }, + body: Term::Lit { lit: Literal::Int { value: 0 } }, + }, + ], + }, + )], + }; + let mut modules = BTreeMap::new(); + modules.insert("lst".into(), lib); + modules.insert("uses_lst".into(), consumer); + let ws = Workspace { + entry: "uses_lst".into(), + modules, + root_dir: std::path::PathBuf::from("."), + }; + let diags = check_workspace(&ws); + assert!(diags.is_empty(), "expected green; 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 0809f51..75bbe6f 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -187,6 +187,12 @@ pub fn lower_workspace(ws: &Workspace) -> Result { // 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(); + // Iter 15b: per-module const table. Used to resolve `Term::Var` + // references to const defs (literal or non-literal) at lowering + // time. Literal consts emit a global and are loaded; non-literal + // consts (e.g. ctor expressions) are inlined at every reference + // site since check_const guarantees their bodies are pure. + let mut module_consts: BTreeMap> = BTreeMap::new(); for (mname, m) in &ws.modules { let mut user_fns = BTreeMap::new(); let mut ail_types = BTreeMap::new(); @@ -231,10 +237,19 @@ pub fn lower_workspace(ws: &Workspace) -> Result { } } } + // Iter 15b: collect const defs for this module so non-literal + // consts can be inlined at `Term::Var` reference sites. + let mut consts: BTreeMap = BTreeMap::new(); + for def in &m.defs { + if let Def::Const(c) = def { + consts.insert(c.name.clone(), c.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); + module_consts.insert(mname.clone(), consts); } // Pass 2: lower per module. Globals/strings are accumulated per module, @@ -256,6 +271,7 @@ pub fn lower_workspace(ws: &Workspace) -> Result { &module_def_ail_types, &module_polymorphic_fns, &module_ctor_index, + &module_consts, import_map, ); emitter @@ -388,6 +404,12 @@ struct Emitter<'a> { /// 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>, + /// Iter 15b: per-module const defs, used to resolve `Term::Var` + /// references (bare or qualified) to a const's body. Literal + /// consts emit a global and are loaded via `@ail__`; + /// non-literal consts are inlined at every reference site (sound + /// because `check_const` rejects effects, so the body is pure). + module_consts: &'a BTreeMap>, /// Current basic block label. Set by `start_block` and is /// the single source of truth for `phi` operands. current_block: String, @@ -459,6 +481,7 @@ impl<'a> Emitter<'a> { module_def_ail_types: &'a BTreeMap>, module_polymorphic_fns: &'a BTreeMap>, module_ctor_index: &'a BTreeMap>, + module_consts: &'a BTreeMap>, import_map: BTreeMap, ) -> Self { let mut types: BTreeMap> = BTreeMap::new(); @@ -506,6 +529,7 @@ impl<'a> Emitter<'a> { import_map, types, module_ctor_index, + module_consts, current_block: String::new(), block_terminated: false, ssa_fn_sigs: BTreeMap::new(), @@ -627,14 +651,20 @@ impl<'a> Emitter<'a> { } fn emit_const(&mut self, c: &ConstDef) -> Result<()> { + // Iter 15b: non-literal const values (e.g. ctor expressions) are + // not emitted as globals. They are inlined at every `Term::Var` + // reference site — sound because `check_const` rejects effectful + // bodies, so re-evaluating the body at each use is observably + // equivalent to a single computation. Trade-off: a long + // recursive const evaluated in many places duplicates work, + // but the demo-scale workloads shipped in the stdlib + // examples are small enough that this is a non-issue. A + // future iter may layer a `@llvm.global_ctors`-style init + // path on top to share the result across reference sites. let lty = llvm_type(&c.ty)?; let lit = match &c.value { Term::Lit { lit } => lit, - _ => { - return Err(CodegenError::Internal( - "MVP: const must be a literal".into(), - )); - } + _ => return Ok(()), }; let (val_ty, val) = match lit { Literal::Int { value } => ("i64".to_string(), value.to_string()), @@ -828,6 +858,31 @@ impl<'a> Emitter<'a> { self.ssa_fn_sigs.entry(global.clone()).or_insert(sig); return Ok((global, "ptr".into())); } + // Iter 15b: const lookup. Both bare (`xs`) and qualified + // (`prefix.xs`) forms resolve through `module_consts`. + // Literal-bodied consts get a load from the global; non- + // literal bodies (e.g. ctor expressions) are inlined. + if let Some((owner_module, cdef)) = self.resolve_const(name) { + let lty = llvm_type(&cdef.ty)?; + if matches!(&cdef.value, Term::Lit { .. }) { + let v = self.fresh_ssa(); + self.body.push_str(&format!( + " {v} = load {lty}, ptr @ail_{owner_module}_{cname}, align 8\n", + cname = cdef.name, + )); + return Ok((v, lty)); + } else { + // Inline the const body. Switch module context to + // the owning module while lowering so any nested + // bare references resolve in the const's home + // namespace. Simpler approach: call lower_term + // directly; the current emitter's module context + // is fine because cross-module ctors are already + // qualified in the AST after typecheck. + let value = cdef.value.clone(); + return self.lower_term(&value); + } + } Err(CodegenError::UnknownVar(name.clone())) } Term::Let { name, value, body } => { @@ -1107,6 +1162,22 @@ impl<'a> Emitter<'a> { // re-lower each field type. Monomorphic ADTs hit the fast path // (no var-set, substitution is empty, ail_fields lower exactly // like cref.fields). + // Iter 15b: for cross-module ctors, qualify any local type-cons + // in `cref.ail_fields` (symmetric to the term-ctor synth fix). + let qualified_ail_fields: Vec = if type_name.matches('.').count() == 1 { + let (prefix, _) = type_name.split_once('.').expect("checked"); + if let Some(target) = self.import_map.get(prefix) { + let owner_local_types = self.collect_owner_local_types(target); + cref.ail_fields + .iter() + .map(|f| qualify_local_types_codegen(f, target, &owner_local_types)) + .collect() + } else { + cref.ail_fields.clone() + } + } else { + cref.ail_fields.clone() + }; let expected_llvm_tys: Vec = if cref.type_vars.is_empty() { cref.fields.clone() } else { @@ -1117,10 +1188,10 @@ impl<'a> Emitter<'a> { let var_set: BTreeSet<&str> = cref.type_vars.iter().map(|s| s.as_str()).collect(); let mut subst: BTreeMap = BTreeMap::new(); - for (exp, actual) in cref.ail_fields.iter().zip(arg_ail_tys.iter()) { + for (exp, actual) in qualified_ail_fields.iter().zip(arg_ail_tys.iter()) { unify_for_subst(exp, actual, &var_set, &mut subst)?; } - cref.ail_fields + qualified_ail_fields .iter() .map(|f| llvm_type(&apply_subst_to_type(f, &subst))) .collect::>()? @@ -1265,14 +1336,32 @@ impl<'a> Emitter<'a> { } }; // Load fields and bind as locals. + // Iter 15b: when the scrutinee's ADT lives in another module, + // `cref.ail_fields[idx]` carries the field type written in + // the owner's local namespace. Qualify it before substituting + // — symmetric to the term-ctor and pat-ctor fixes in + // ailang-check. + let owning_module: Option = match &s_ail { + Type::Con { name, .. } if name.matches('.').count() == 1 => name + .split_once('.') + .map(|(p, _)| p.to_string()), + _ => None, + }; let mut pushed = 0usize; for (idx, binding) in bindings.iter().enumerate() { if let Some(bname) = binding { let raw_ail = cref.ail_fields.get(idx).cloned().unwrap_or(Type::unit()); + let qualified_ail = match &owning_module { + Some(m) => { + let owner_local_types = self.collect_owner_local_types(m); + qualify_local_types_codegen(&raw_ail, m, &owner_local_types) + } + None => raw_ail, + }; let bind_ail = if arm_subst.is_empty() { - raw_ail + qualified_ail } else { - apply_subst_to_type(&raw_ail, &arm_subst) + apply_subst_to_type(&qualified_ail, &arm_subst) }; let fty = llvm_type(&bind_ail)?; let off = 8 + idx as i64 * 8; @@ -2079,6 +2168,26 @@ impl<'a> Emitter<'a> { )) } + /// Iter 15b: resolve a `Term::Var` reference to a const def. Returns + /// `(owning_module, ConstDef)` on hit. Both bare current-module + /// references and qualified `prefix.name` cross-module references + /// resolve through the same path; the prefix routes through the + /// emitter's `import_map` to the actual module. + fn resolve_const(&self, name: &str) -> Option<(String, ConstDef)> { + if name.matches('.').count() == 1 { + let (prefix, suffix) = name.split_once('.')?; + let target = self.import_map.get(prefix)?; + let cdef = self.module_consts.get(target)?.get(suffix)?.clone(); + return Some((target.clone(), cdef)); + } + let cdef = self + .module_consts + .get(self.module_name)? + .get(name)? + .clone(); + Some((self.module_name.to_string(), cdef)) + } + fn lower_effect_op(&mut self, op: &str, args: &[Term], tail: bool) -> Result<(String, String)> { // Iter 14e: `musttail` requires identical caller/callee // prototypes (same return type, same param types). The MVP's @@ -2275,6 +2384,16 @@ impl<'a> Emitter<'a> { { return Ok(ty.clone()); } + // Iter 15b: const refs participate in arg-type + // synthesis. Bare or qualified, both forms route + // through `resolve_const` and yield the const's + // declared type. Const types are already qualified + // (the AST writes them in the consumer's namespace + // via `module.Type`), so no further qualification + // is needed. + if let Some((_, cdef)) = self.resolve_const(name) { + return Ok(cdef.ty); + } if let Some(t) = builtin_ail_type(name) { return Ok(t); } @@ -2337,6 +2456,13 @@ impl<'a> Emitter<'a> { // 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. + // Iter 15b: when the ctor is cross-module, `cref.ail_fields` + // is written in the owning module's local namespace, so a + // recursive self-reference like `Cons a (List a)` carries + // a bare `Con("List", _)` even though every other place + // sees the qualified `std_list.List<...>`. Apply + // `qualify_local_types_codegen` before `unify_for_subst` + // so the unification doesn't fail on name mismatch. let cref = self.lookup_ctor_by_type(type_name, ctor)?; if cref.type_vars.is_empty() { return Ok(Type::Con { @@ -2344,6 +2470,20 @@ impl<'a> Emitter<'a> { args: vec![], }); } + let qualified_ail_fields: Vec = if type_name.matches('.').count() == 1 { + let (prefix, _) = type_name.split_once('.').expect("checked"); + if let Some(target) = self.import_map.get(prefix) { + let owner_local_types = self.collect_owner_local_types(target); + cref.ail_fields + .iter() + .map(|f| qualify_local_types_codegen(f, target, &owner_local_types)) + .collect() + } else { + cref.ail_fields.clone() + } + } else { + cref.ail_fields.clone() + }; let arg_tys: Vec = args .iter() .map(|a| self.synth_with_extras(a, extras)) @@ -2351,7 +2491,7 @@ impl<'a> Emitter<'a> { let var_set: BTreeSet<&str> = cref.type_vars.iter().map(|s| s.as_str()).collect(); let mut subst: BTreeMap = BTreeMap::new(); - for (exp, actual) in cref.ail_fields.iter().zip(arg_tys.iter()) { + for (exp, actual) in qualified_ail_fields.iter().zip(arg_tys.iter()) { unify_for_subst(exp, actual, &var_set, &mut subst)?; } // Vars not pinned by ctor args (e.g. `Nil` for `List a`, @@ -2544,13 +2684,16 @@ fn unify_for_subst( } match (param, arg) { (Type::Var { name }, _) if vars.contains(name.as_str()) => { - if let Some(prev) = subst.get(name) { - if prev != arg { - return Err(CodegenError::Internal(format!( - "monomorphisation: var `{name}` bound to two distinct types" - ))); - } - return Ok(()); + if let Some(prev) = subst.get(name).cloned() { + // Iter 15b: the previously-bound type may be more + // concrete than `arg` (e.g. `prev = List` from a + // sibling binding, `arg = List<$u>` from a synth- + // wildcard nullary ctor). Use recursive unification + // instead of strict equality so the inner `$u` + // wildcard matches `Int`. The previous strict- + // equality check rejected such overlaps as bogus + // duplicate bindings. + return unify_for_subst(&prev, arg, vars, subst); } subst.insert(name.clone(), arg.clone()); Ok(()) diff --git a/docs/JOURNAL.md b/docs/JOURNAL.md index 0436608..6397feb 100644 --- a/docs/JOURNAL.md +++ b/docs/JOURNAL.md @@ -2305,6 +2305,124 @@ constructor-blocked combinators (`map`, `filter`, `append`, during stdlib construction (each prior dogfood iter has surfaced one), debugger handles it inline. +## Iter 15b — `std_list` ships, three more compiler gaps closed + +Second stdlib module. Tester wrote `std_list.ailx` (164 LOC, 10 +combinators) plus `std_list_demo.ailx` (consumer importing both +`std_maybe` and `std_list`). `std_list.ail.json` typechecked +cleanly in isolation. The demo did not — the prediction "every +dogfood iter surfaces at least one compiler bug" held three +times over. + +**Tester's diagnosis** was sharp enough that the orchestrator +could go straight to implementer without a debugger round: + +> Iter 14h's `qualify_local_types` is applied at `Term::Var` +> cross-module lookup but **not** to ctor-field types when +> `Term::Ctor` is synthesized. For `Cons a (List a)`, `List` +> stays unqualified in `cdef.fields`. `std_maybe` slipped +> through because its ctors have no recursive Con field. First +> recursive ADT shared cross-module triggers it. + +**The original-spec fix** was ~10 LOC across two sites in +`ailang-check/src/lib.rs` — apply `qualify_local_types` over +`cdef.fields` before substituting forall vars, in both +`Term::Ctor` synth and `Pattern::Ctor` resolution (the latter +symmetric, no current consumer hit it but it's the same +underlying gap). + +**Two more bugs surfaced during implementation** of that fix: + +1. **Codegen-side qualify-fields, four sites.** `ailang-codegen` + has its own field-type tracking that mirrored the check-side + bug. Symmetric fix needed in `Term::Ctor` synth + `lower_ctor`, + in `lower_match` for cross-module ADT scrutinees, and a tweak + to `unify_for_subst` to recurse instead of strict-equality + when re-binding a forall var that already has a previous + concrete binding (so a sibling-derived `List` accepts a + nullary ctor's `List<$u>` wildcard). +2. **Const codegen for non-literal values.** The demo defines + a top-level `xs : List` const whose body is a Cons + chain. `check_const` already accepts pure non-literal const + bodies, but `emit_const` rejected them. Fix: register + `module_consts` in pass 1, resolve const refs in `Term::Var` + (load from global for literal consts, inline body for + non-literal). Both bare and qualified const refs (`xs` + and `module.xs`) supported. + +All three fixes together: ~349 / 25 LOC across `ailang-check`, +`ailang-codegen`, and the new e2e test. Each fix carries an +`Iter 15b` code comment at its site. + +**Tests: 87/87 (was 85, +2).** New e2e `std_list_demo` asserts +the 11-line stdout sequence: + +``` +5 (length [1..5]) +false (is_empty [1..5]) +true (is_empty []) +1 (head [1..5] via from_maybe) +4 (length of tail) +10 (length of [1..5] ++ [1..5]) +5 (head of reverse [1..5]) +2 (head of map double [1..5] = [2,4,6,8,10]) +2 (length of filter is_even [1..5] = [2,4]) +15 (fold_left + 0 [1..5]) +15 (fold_right + 0 [1..5]) +``` + +New unit test `cross_module_recursive_adt_term_and_pat_ctor` in +`ailang-check` covers both Term::Ctor and Pattern::Ctor paths +against a recursive cross-module ADT. Catches the original bug ++ its symmetric pat-ctor latent twin. + +**Hash invariance.** All 22 pre-15b fixture hashes plus +`std_maybe`'s five def hashes bit-identical. The 15b changes +were purely additive on the language side. + +**14a-era and 14h-era regressions held.** Spot-checked +`parameterised_box_round_trip` (14a), `cross_module_maybe_demo` +(14h), `list_map_poly_inc_then_prints` (14e). All green. + +**Authoring observation from the tester.** Form (A) holds up to +10 combinators in one module without breaking. The highest- +overhead pieces are typed `lam` (each closure carries `(typed +name type)` triples + return-type + effects-clause) and the +outer `(forall (vars a b) (fn-type ...))` wrapper. Repeated +paren-counting at the bottom of nested `seq` chains was the only +real friction during demo authoring. Suggests a future iter +might add a `seq*` n-ary form, but the n-ary case is sugar over +the current `seq` shape and not load-bearing. + +**Cumulative state, post-15b.** + +- Stdlib modules: 2 (`std_maybe`, `std_list`). +- Combinators: 14 (`Maybe` + 4; `List` + 10). +- Cross-module imports: type-side, ctor-side, fn-side all working. +- Recursive cross-module ADTs working. +- Tail-call markers used in production: `fold_left` in `std_list`, + `print_list` in two existing fixtures. +- Compiler bugs surfaced and fixed in dogfood: + - 14a: monomorphisation `Type::unit()` placeholder collision. + - 14h: cross-module type/ctor not implemented. + - 15b: qualify-fields gap (3 layers: check, codegen, plus const). + +**Plan 15c.** Stress test on real-shape data. Build a list of +~1000 elements, run `fold_left` and `fold_right` over it, verify +both produce the expected sum. The point: empirically confirm +that `fold_left`'s tail-call marker actually prevents stack +overflow under load, while `fold_right` (constructor-blocked, +unmarked) can still run at this scale because it's only ~1000 +deep, not 1M. If `fold_right` segfaults on this scale, that's a +useful boundary; if it works, we know the stack budget on this +host is at least a few thousand frames. + +After 15c, optional: `std_pair` (2-tuple ADT), `std_either` +(disjoint union for error handling). Or move on to a +non-stdlib feature like nested patterns — at this scale of +language, the case for adding a feature can be made directly +from a stdlib annoyance. + diff --git a/examples/std_list.ail.json b/examples/std_list.ail.json new file mode 100644 index 0000000..7a30393 --- /dev/null +++ b/examples/std_list.ail.json @@ -0,0 +1 @@ +{"defs":[{"ctors":[{"fields":[],"name":"Nil"},{"fields":[{"k":"var","name":"a"},{"args":[{"k":"var","name":"a"}],"k":"con","name":"List"}],"name":"Cons"}],"doc":"Polymorphic singly-linked list: Nil or Cons>.","kind":"type","name":"List","vars":["a"]},{"body":{"arms":[{"body":{"name":"acc","t":"var"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"args":[{"name":"f","t":"var"},{"args":[{"name":"acc","t":"var"},{"name":"h","t":"var"}],"fn":{"name":"f","t":"var"},"t":"app"},{"name":"t","t":"var"}],"fn":{"name":"fold_left","t":"var"},"t":"app","tail":true},"pat":{"ctor":"Cons","fields":[{"name":"h","p":"var"},{"name":"t","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"Tail-recursive left fold. The recursive call is in tail position and is marked.","kind":"fn","name":"fold_left","params":["f","acc","xs"],"type":{"body":{"effects":[],"k":"fn","params":[{"effects":[],"k":"fn","params":[{"k":"var","name":"b"},{"k":"var","name":"a"}],"ret":{"k":"var","name":"b"}},{"k":"var","name":"b"},{"args":[{"k":"var","name":"a"}],"k":"con","name":"List"}],"ret":{"k":"var","name":"b"}},"k":"forall","vars":["a","b"]}},{"body":{"arms":[{"body":{"name":"acc","t":"var"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"args":[{"name":"h","t":"var"},{"args":[{"name":"f","t":"var"},{"name":"acc","t":"var"},{"name":"t","t":"var"}],"fn":{"name":"fold_right","t":"var"},"t":"app"}],"fn":{"name":"f","t":"var"},"t":"app"},"pat":{"ctor":"Cons","fields":[{"name":"h","p":"var"},{"name":"t","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"Right fold. Constructor-blocked: the recursive call is the second arg of f, NOT a tail position.","kind":"fn","name":"fold_right","params":["f","acc","xs"],"type":{"body":{"effects":[],"k":"fn","params":[{"effects":[],"k":"fn","params":[{"k":"var","name":"a"},{"k":"var","name":"b"}],"ret":{"k":"var","name":"b"}},{"k":"var","name":"b"},{"args":[{"k":"var","name":"a"}],"k":"con","name":"List"}],"ret":{"k":"var","name":"b"}},"k":"forall","vars":["a","b"]}},{"body":{"args":[{"body":{"args":[{"name":"c","t":"var"},{"lit":{"kind":"int","value":1},"t":"lit"}],"fn":{"name":"+","t":"var"},"t":"app"},"effects":[],"paramTypes":[{"k":"con","name":"Int"},{"k":"var","name":"a"}],"params":["c","_"],"retType":{"k":"con","name":"Int"},"t":"lam"},{"lit":{"kind":"int","value":0},"t":"lit"},{"name":"xs","t":"var"}],"fn":{"name":"fold_left","t":"var"},"t":"app"},"doc":"Length via fold_left. The accumulating lambda ignores the element and increments the counter.","kind":"fn","name":"length","params":["xs"],"type":{"body":{"effects":[],"k":"fn","params":[{"args":[{"k":"var","name":"a"}],"k":"con","name":"List"}],"ret":{"k":"con","name":"Int"}},"k":"forall","vars":["a"]}},{"body":{"args":[{"body":{"args":[{"name":"h","t":"var"},{"name":"acc","t":"var"}],"ctor":"Cons","t":"ctor","type":"List"},"effects":[],"paramTypes":[{"args":[{"k":"var","name":"a"}],"k":"con","name":"List"},{"k":"var","name":"a"}],"params":["acc","h"],"retType":{"args":[{"k":"var","name":"a"}],"k":"con","name":"List"},"t":"lam"},{"args":[],"ctor":"Nil","t":"ctor","type":"List"},{"name":"xs","t":"var"}],"fn":{"name":"fold_left","t":"var"},"t":"app"},"doc":"Reverse via fold_left with a flipping accumulator. Tail-recursive by virtue of the fold_left call.","kind":"fn","name":"reverse","params":["xs"],"type":{"body":{"effects":[],"k":"fn","params":[{"args":[{"k":"var","name":"a"}],"k":"con","name":"List"}],"ret":{"args":[{"k":"var","name":"a"}],"k":"con","name":"List"}},"k":"forall","vars":["a"]}},{"body":{"arms":[{"body":{"lit":{"kind":"bool","value":true},"t":"lit"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"lit":{"kind":"bool","value":false},"t":"lit"},"pat":{"ctor":"Cons","fields":[{"p":"wild"},{"p":"wild"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"True iff the list is Nil.","kind":"fn","name":"is_empty","params":["xs"],"type":{"body":{"effects":[],"k":"fn","params":[{"args":[{"k":"var","name":"a"}],"k":"con","name":"List"}],"ret":{"k":"con","name":"Bool"}},"k":"forall","vars":["a"]}},{"body":{"arms":[{"body":{"args":[],"ctor":"Nothing","t":"ctor","type":"std_maybe.Maybe"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"args":[{"name":"h","t":"var"}],"ctor":"Just","t":"ctor","type":"std_maybe.Maybe"},"pat":{"ctor":"Cons","fields":[{"name":"h","p":"var"},{"p":"wild"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"Returns Just of the first element, or Nothing for an empty list. Cross-module Maybe.","kind":"fn","name":"head","params":["xs"],"type":{"body":{"effects":[],"k":"fn","params":[{"args":[{"k":"var","name":"a"}],"k":"con","name":"List"}],"ret":{"args":[{"k":"var","name":"a"}],"k":"con","name":"std_maybe.Maybe"}},"k":"forall","vars":["a"]}},{"body":{"arms":[{"body":{"args":[],"ctor":"Nothing","t":"ctor","type":"std_maybe.Maybe"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"args":[{"name":"t","t":"var"}],"ctor":"Just","t":"ctor","type":"std_maybe.Maybe"},"pat":{"ctor":"Cons","fields":[{"p":"wild"},{"name":"t","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"Returns Just> of the tail, or Nothing for an empty list.","kind":"fn","name":"tail","params":["xs"],"type":{"body":{"effects":[],"k":"fn","params":[{"args":[{"k":"var","name":"a"}],"k":"con","name":"List"}],"ret":{"args":[{"args":[{"k":"var","name":"a"}],"k":"con","name":"List"}],"k":"con","name":"std_maybe.Maybe"}},"k":"forall","vars":["a"]}},{"body":{"arms":[{"body":{"name":"ys","t":"var"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"args":[{"name":"h","t":"var"},{"args":[{"name":"t","t":"var"},{"name":"ys","t":"var"}],"fn":{"name":"append","t":"var"},"t":"app"}],"ctor":"Cons","t":"ctor","type":"List"},"pat":{"ctor":"Cons","fields":[{"name":"h","p":"var"},{"name":"t","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"Concatenate two lists. Constructor-blocked recursion: the recursive call is inside Cons, NOT a tail.","kind":"fn","name":"append","params":["xs","ys"],"type":{"body":{"effects":[],"k":"fn","params":[{"args":[{"k":"var","name":"a"}],"k":"con","name":"List"},{"args":[{"k":"var","name":"a"}],"k":"con","name":"List"}],"ret":{"args":[{"k":"var","name":"a"}],"k":"con","name":"List"}},"k":"forall","vars":["a"]}},{"body":{"arms":[{"body":{"args":[],"ctor":"Nil","t":"ctor","type":"List"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"args":[{"args":[{"name":"h","t":"var"}],"fn":{"name":"f","t":"var"},"t":"app"},{"args":[{"name":"f","t":"var"},{"name":"t","t":"var"}],"fn":{"name":"map","t":"var"},"t":"app"}],"ctor":"Cons","t":"ctor","type":"List"},"pat":{"ctor":"Cons","fields":[{"name":"h","p":"var"},{"name":"t","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"Polymorphic map. Constructor-blocked recursion.","kind":"fn","name":"map","params":["f","xs"],"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":"List"}],"ret":{"args":[{"k":"var","name":"b"}],"k":"con","name":"List"}},"k":"forall","vars":["a","b"]}},{"body":{"arms":[{"body":{"args":[],"ctor":"Nil","t":"ctor","type":"List"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"cond":{"args":[{"name":"h","t":"var"}],"fn":{"name":"p","t":"var"},"t":"app"},"else":{"args":[{"name":"p","t":"var"},{"name":"t","t":"var"}],"fn":{"name":"filter","t":"var"},"t":"app"},"t":"if","then":{"args":[{"name":"h","t":"var"},{"args":[{"name":"p","t":"var"},{"name":"t","t":"var"}],"fn":{"name":"filter","t":"var"},"t":"app"}],"ctor":"Cons","t":"ctor","type":"List"}},"pat":{"ctor":"Cons","fields":[{"name":"h","p":"var"},{"name":"t","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"Keep elements where p is true. Constructor-blocked when p holds; non-tail recursive call when p is false.","kind":"fn","name":"filter","params":["p","xs"],"type":{"body":{"effects":[],"k":"fn","params":[{"effects":[],"k":"fn","params":[{"k":"var","name":"a"}],"ret":{"k":"con","name":"Bool"}},{"args":[{"k":"var","name":"a"}],"k":"con","name":"List"}],"ret":{"args":[{"k":"var","name":"a"}],"k":"con","name":"List"}},"k":"forall","vars":["a"]}}],"imports":[{"module":"std_maybe"}],"name":"std_list","schema":"ailang/v0"} \ No newline at end of file diff --git a/examples/std_list.ailx b/examples/std_list.ailx new file mode 100644 index 0000000..6c326c9 --- /dev/null +++ b/examples/std_list.ailx @@ -0,0 +1,164 @@ +; Iter 15b — second stdlib module: polymorphic singly-linked lists. +; Imports std_maybe so head/tail can return Maybe / Maybe>. +; All cross-module references use the qualified form per the Iter 14h +; convention: `std_maybe.Maybe` at type-name slots, `std_maybe.from_maybe` +; for fns. Bare ctor names (`Just`, `Nothing`) stay unqualified — once +; the type is resolved the ctor lookup is unambiguous. + +(module std_list + + (import std_maybe) + + (data List (vars a) + (doc "Polymorphic singly-linked list: Nil or Cons>.") + (ctor Nil) + (ctor Cons a (con List a))) + + (fn fold_left + (doc "Tail-recursive left fold. The recursive call is in tail position and is marked.") + (type + (forall (vars a b) + (fn-type + (params (fn-type (params b a) (ret b)) + b + (con List a)) + (ret b)))) + (params f acc xs) + (body + (match xs + (case (pat-ctor Nil) acc) + (case (pat-ctor Cons h t) + (tail-app fold_left f (app f acc h) t))))) + + (fn fold_right + (doc "Right fold. Constructor-blocked: the recursive call is the second arg of f, NOT a tail position.") + (type + (forall (vars a b) + (fn-type + (params (fn-type (params a b) (ret b)) + b + (con List a)) + (ret b)))) + (params f acc xs) + (body + (match xs + (case (pat-ctor Nil) acc) + (case (pat-ctor Cons h t) + (app f h (app fold_right f acc t)))))) + + (fn length + (doc "Length via fold_left. The accumulating lambda ignores the element and increments the counter.") + (type + (forall (vars a) + (fn-type + (params (con List a)) + (ret (con Int))))) + (params xs) + (body + (app fold_left + (lam (params (typed c (con Int)) (typed _ a)) + (ret (con Int)) + (body (app + c 1))) + 0 + xs))) + + (fn reverse + (doc "Reverse via fold_left with a flipping accumulator. Tail-recursive by virtue of the fold_left call.") + (type + (forall (vars a) + (fn-type + (params (con List a)) + (ret (con List a))))) + (params xs) + (body + (app fold_left + (lam (params (typed acc (con List a)) (typed h a)) + (ret (con List a)) + (body (term-ctor List Cons h acc))) + (term-ctor List Nil) + xs))) + + (fn is_empty + (doc "True iff the list is Nil.") + (type + (forall (vars a) + (fn-type + (params (con List a)) + (ret (con Bool))))) + (params xs) + (body + (match xs + (case (pat-ctor Nil) true) + (case (pat-ctor Cons _ _) false)))) + + (fn head + (doc "Returns Just of the first element, or Nothing for an empty list. Cross-module Maybe.") + (type + (forall (vars a) + (fn-type + (params (con List a)) + (ret (con std_maybe.Maybe a))))) + (params xs) + (body + (match xs + (case (pat-ctor Nil) (term-ctor std_maybe.Maybe Nothing)) + (case (pat-ctor Cons h _) (term-ctor std_maybe.Maybe Just h))))) + + (fn tail + (doc "Returns Just> of the tail, or Nothing for an empty list.") + (type + (forall (vars a) + (fn-type + (params (con List a)) + (ret (con std_maybe.Maybe (con List a)))))) + (params xs) + (body + (match xs + (case (pat-ctor Nil) (term-ctor std_maybe.Maybe Nothing)) + (case (pat-ctor Cons _ t) (term-ctor std_maybe.Maybe Just t))))) + + (fn append + (doc "Concatenate two lists. Constructor-blocked recursion: the recursive call is inside Cons, NOT a tail.") + (type + (forall (vars a) + (fn-type + (params (con List a) (con List a)) + (ret (con List a))))) + (params xs ys) + (body + (match xs + (case (pat-ctor Nil) ys) + (case (pat-ctor Cons h t) + (term-ctor List Cons h (app append t ys)))))) + + (fn map + (doc "Polymorphic map. Constructor-blocked recursion.") + (type + (forall (vars a b) + (fn-type + (params (fn-type (params a) (ret b)) + (con List a)) + (ret (con List b))))) + (params f xs) + (body + (match xs + (case (pat-ctor Nil) (term-ctor List Nil)) + (case (pat-ctor Cons h t) + (term-ctor List Cons (app f h) (app map f t)))))) + + (fn filter + (doc "Keep elements where p is true. Constructor-blocked when p holds; non-tail recursive call when p is false.") + (type + (forall (vars a) + (fn-type + (params (fn-type (params a) (ret (con Bool))) + (con List a)) + (ret (con List a))))) + (params p xs) + (body + (match xs + (case (pat-ctor Nil) (term-ctor List Nil)) + (case (pat-ctor Cons h t) + (if (app p h) + (term-ctor List Cons h (app filter p t)) + (app filter p t))))))) diff --git a/examples/std_list_demo.ail.json b/examples/std_list_demo.ail.json new file mode 100644 index 0000000..9c8b0d6 --- /dev/null +++ b/examples/std_list_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.","kind":"fn","name":"inc","params":["x"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"Int"}],"ret":{"k":"con","name":"Int"}}},{"body":{"args":[{"name":"a","t":"var"},{"name":"b","t":"var"}],"fn":{"name":"+","t":"var"},"t":"app"},"doc":"Binary +. Used as the fold accumulator op.","kind":"fn","name":"add","params":["a","b"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"Int"},{"k":"con","name":"Int"}],"ret":{"k":"con","name":"Int"}}},{"body":{"args":[{"args":[{"name":"x","t":"var"},{"lit":{"kind":"int","value":2},"t":"lit"}],"fn":{"name":"%","t":"var"},"t":"app"},{"lit":{"kind":"int","value":0},"t":"lit"}],"fn":{"name":"==","t":"var"},"t":"app"},"doc":"Predicate: x mod 2 == 0.","kind":"fn","name":"is_even","params":["x"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"Int"}],"ret":{"k":"con","name":"Bool"}}},{"body":{"args":[{"name":"x","t":"var"},{"lit":{"kind":"int","value":2},"t":"lit"}],"fn":{"name":"*","t":"var"},"t":"app"},"doc":"Multiply by 2. Used as the map arg.","kind":"fn","name":"double","params":["x"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"Int"}],"ret":{"k":"con","name":"Int"}}},{"doc":"The canonical list [1,2,3,4,5] for the demo. Pure ctor expression, so a const works.","kind":"const","name":"xs","type":{"args":[{"k":"con","name":"Int"}],"k":"con","name":"std_list.List"},"value":{"args":[{"lit":{"kind":"int","value":1},"t":"lit"},{"args":[{"lit":{"kind":"int","value":2},"t":"lit"},{"args":[{"lit":{"kind":"int","value":3},"t":"lit"},{"args":[{"lit":{"kind":"int","value":4},"t":"lit"},{"args":[{"lit":{"kind":"int","value":5},"t":"lit"},{"args":[],"ctor":"Nil","t":"ctor","type":"std_list.List"}],"ctor":"Cons","t":"ctor","type":"std_list.List"}],"ctor":"Cons","t":"ctor","type":"std_list.List"}],"ctor":"Cons","t":"ctor","type":"std_list.List"}],"ctor":"Cons","t":"ctor","type":"std_list.List"}],"ctor":"Cons","t":"ctor","type":"std_list.List"}},{"body":{"lhs":{"args":[{"args":[{"name":"xs","t":"var"}],"fn":{"name":"std_list.length","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"name":"xs","t":"var"}],"fn":{"name":"std_list.is_empty","t":"var"},"t":"app"}],"op":"io/print_bool","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"args":[],"ctor":"Nil","t":"ctor","type":"std_list.List"}],"fn":{"name":"std_list.is_empty","t":"var"},"t":"app"}],"op":"io/print_bool","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"lit":{"kind":"int","value":-1},"t":"lit"},{"args":[{"name":"xs","t":"var"}],"fn":{"name":"std_list.head","t":"var"},"t":"app"}],"fn":{"name":"std_maybe.from_maybe","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"args":[{"name":"xs","t":"var"},{"args":[{"name":"xs","t":"var"}],"fn":{"name":"std_list.tail","t":"var"},"t":"app"}],"fn":{"name":"std_maybe.from_maybe","t":"var"},"t":"app"}],"fn":{"name":"std_list.length","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"args":[{"name":"xs","t":"var"},{"name":"xs","t":"var"}],"fn":{"name":"std_list.append","t":"var"},"t":"app"}],"fn":{"name":"std_list.length","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"lit":{"kind":"int","value":-1},"t":"lit"},{"args":[{"args":[{"name":"xs","t":"var"}],"fn":{"name":"std_list.reverse","t":"var"},"t":"app"}],"fn":{"name":"std_list.head","t":"var"},"t":"app"}],"fn":{"name":"std_maybe.from_maybe","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"lit":{"kind":"int","value":-1},"t":"lit"},{"args":[{"args":[{"name":"double","t":"var"},{"name":"xs","t":"var"}],"fn":{"name":"std_list.map","t":"var"},"t":"app"}],"fn":{"name":"std_list.head","t":"var"},"t":"app"}],"fn":{"name":"std_maybe.from_maybe","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"args":[{"name":"is_even","t":"var"},{"name":"xs","t":"var"}],"fn":{"name":"std_list.filter","t":"var"},"t":"app"}],"fn":{"name":"std_list.length","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"name":"add","t":"var"},{"lit":{"kind":"int","value":0},"t":"lit"},{"name":"xs","t":"var"}],"fn":{"name":"std_list.fold_left","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"args":[{"args":[{"name":"add","t":"var"},{"lit":{"kind":"int","value":0},"t":"lit"},{"name":"xs","t":"var"}],"fn":{"name":"std_list.fold_right","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"t":"seq"},"t":"seq"},"t":"seq"},"t":"seq"},"t":"seq"},"t":"seq"},"t":"seq"},"t":"seq"},"t":"seq"},"t":"seq"},"doc":"Drive each std_list combinator once. Expected outputs (per line): 5, false, true, 1, 4, 10, 5, 2, 2, 15, 15.","kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[{"module":"std_maybe"},{"module":"std_list"}],"name":"std_list_demo","schema":"ailang/v0"} \ No newline at end of file diff --git a/examples/std_list_demo.ailx b/examples/std_list_demo.ailx new file mode 100644 index 0000000..9170f39 --- /dev/null +++ b/examples/std_list_demo.ailx @@ -0,0 +1,61 @@ +; Iter 15b — second consumer demo. +; Imports both std_maybe and std_list. Exercises every combinator at +; least once. xs is a top-level fn `() -> List` (consts cannot +; reference user fns; the brief flagged this). + +(module std_list_demo + + (import std_maybe) + (import std_list) + + (fn inc + (doc "Add 1 to an Int. Used as the (a -> b) arg to map.") + (type (fn-type (params (con Int)) (ret (con Int)))) + (params x) + (body (app + x 1))) + + (fn add + (doc "Binary +. Used as the fold accumulator op.") + (type (fn-type (params (con Int) (con Int)) (ret (con Int)))) + (params a b) + (body (app + a b))) + + (fn is_even + (doc "Predicate: x mod 2 == 0.") + (type (fn-type (params (con Int)) (ret (con Bool)))) + (params x) + (body (app == (app % x 2) 0))) + + (fn double + (doc "Multiply by 2. Used as the map arg.") + (type (fn-type (params (con Int)) (ret (con Int)))) + (params x) + (body (app * x 2))) + + (const xs + (doc "The canonical list [1,2,3,4,5] for the demo. Pure ctor expression, so a const works.") + (type (con std_list.List (con Int))) + (body + (term-ctor std_list.List Cons 1 + (term-ctor std_list.List Cons 2 + (term-ctor std_list.List Cons 3 + (term-ctor std_list.List Cons 4 + (term-ctor std_list.List Cons 5 + (term-ctor std_list.List Nil)))))))) + + (fn main + (doc "Drive each std_list combinator once. Expected outputs (per line): 5, false, true, 1, 4, 10, 5, 2, 2, 15, 15.") + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body + (seq (do io/print_int (app std_list.length xs)) + (seq (do io/print_bool (app std_list.is_empty xs)) + (seq (do io/print_bool (app std_list.is_empty (term-ctor std_list.List Nil))) + (seq (do io/print_int (app std_maybe.from_maybe -1 (app std_list.head xs))) + (seq (do io/print_int (app std_list.length (app std_maybe.from_maybe xs (app std_list.tail xs)))) + (seq (do io/print_int (app std_list.length (app std_list.append xs xs))) + (seq (do io/print_int (app std_maybe.from_maybe -1 (app std_list.head (app std_list.reverse xs)))) + (seq (do io/print_int (app std_maybe.from_maybe -1 (app std_list.head (app std_list.map double xs)))) + (seq (do io/print_int (app std_list.length (app std_list.filter is_even xs))) + (seq (do io/print_int (app std_list.fold_left add 0 xs)) + (do io/print_int (app std_list.fold_right add 0 xs)))))))))))))))