Iter 15b: std_list ships, three more compiler gaps closed
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<Int> accepts nullary-ctor's List<$u> wildcard). 3. Const codegen: emit_const rejected non-literal const bodies. The demo's xs : List<Int> = 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<a>`) 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.
|
||||
|
||||
@@ -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<String>;
|
||||
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<Type> = 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<String>;
|
||||
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<Type> = 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<String, Type> = 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.
|
||||
|
||||
@@ -187,6 +187,12 @@ pub fn lower_workspace(ws: &Workspace) -> Result<String> {
|
||||
// in that module). Cross-module ctor lookups resolve through this
|
||||
// table instead of the per-Emitter `ctor_index`.
|
||||
let mut module_ctor_index: BTreeMap<String, BTreeMap<String, CtorRef>> = BTreeMap::new();
|
||||
// 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<String, BTreeMap<String, ConstDef>> = 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<String> {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Iter 15b: collect const defs for this module so non-literal
|
||||
// consts can be inlined at `Term::Var` reference sites.
|
||||
let mut consts: BTreeMap<String, ConstDef> = 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<String> {
|
||||
&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<String, BTreeMap<String, CtorRef>>,
|
||||
/// 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_<m>_<name>`;
|
||||
/// non-literal consts are inlined at every reference site (sound
|
||||
/// because `check_const` rejects effects, so the body is pure).
|
||||
module_consts: &'a BTreeMap<String, BTreeMap<String, ConstDef>>,
|
||||
/// 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<String, BTreeMap<String, Type>>,
|
||||
module_polymorphic_fns: &'a BTreeMap<String, BTreeMap<String, FnDef>>,
|
||||
module_ctor_index: &'a BTreeMap<String, BTreeMap<String, CtorRef>>,
|
||||
module_consts: &'a BTreeMap<String, BTreeMap<String, ConstDef>>,
|
||||
import_map: BTreeMap<String, String>,
|
||||
) -> Self {
|
||||
let mut types: BTreeMap<String, Vec<CtorInfo>> = 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<Type> = 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<String> = 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<String, Type> = 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::<Result<_>>()?
|
||||
@@ -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<String> = 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<Type> = 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<Type> = 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<String, Type> = 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<Int>` 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(())
|
||||
|
||||
+118
@@ -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<Int>` accepts a
|
||||
nullary ctor's `List<$u>` wildcard).
|
||||
2. **Const codegen for non-literal values.** The demo defines
|
||||
a top-level `xs : List<Int>` 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.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,164 @@
|
||||
; Iter 15b — second stdlib module: polymorphic singly-linked lists.
|
||||
; Imports std_maybe so head/tail can return Maybe<a> / Maybe<List<a>>.
|
||||
; 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<a, List<a>>.")
|
||||
(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<a> 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<List<a>> 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)))))))
|
||||
File diff suppressed because one or more lines are too long
@@ -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<Int>` (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)))))))))))))))
|
||||
Reference in New Issue
Block a user