diff --git a/crates/ail/tests/typeclass_22b3.rs b/crates/ail/tests/typeclass_22b3.rs index e874075..6b4a626 100644 --- a/crates/ail/tests/typeclass_22b3.rs +++ b/crates/ail/tests/typeclass_22b3.rs @@ -442,3 +442,37 @@ fn fixpoint_handles_chained_class_method_calls() { — multi-round fixpoint property" ); } + +/// After the mono pass, the body of `main` in +/// test_22b2_instance_present must reference `show#Int` instead +/// of `show`. +#[test] +fn rewrite_replaces_class_method_var_with_mono_symbol_same_module() { + let entry = examples_dir().join("test_22b2_instance_present.ail.json"); + let ws = ailang_core::load_workspace(&entry).expect("load"); + let diags = ailang_check::check_workspace(&ws); + assert!(diags.is_empty(), "fixture must typecheck: {:?}", diags); + + let ws = ailang_check::monomorphise_workspace(&ws).expect("mono"); + + let main_module = ws.modules.get("test_22b2_instance_present").unwrap(); + let main_fn = main_module + .defs + .iter() + .find_map(|d| match d { + ailang_core::ast::Def::Fn(f) if f.name == "main" => Some(f), + _ => None, + }) + .expect("main"); + + // The body is `App { fn: Var "show", args: [Lit 42] }`. After + // rewrite, the callee Var must be `show#Int`. + let callee_name: &str = match &main_fn.body { + ailang_core::ast::Term::App { callee, .. } => match callee.as_ref() { + ailang_core::ast::Term::Var { name } => name.as_str(), + other => panic!("callee is not Var: {:?}", other), + }, + other => panic!("body is not App: {:?}", other), + }; + assert_eq!(callee_name, "show#Int"); +} diff --git a/crates/ailang-check/src/mono.rs b/crates/ailang-check/src/mono.rs index 28ecce3..3e9bfd4 100644 --- a/crates/ailang-check/src/mono.rs +++ b/crates/ailang-check/src/mono.rs @@ -51,7 +51,7 @@ //! the implementation so the constraint is visible to anyone //! extending the skeleton. -use ailang_core::ast::{ClassDef, Def, FnDef as AstFnDef, InstanceDef, Term, Type}; +use ailang_core::ast::{Arm, ClassDef, Def, FnDef as AstFnDef, InstanceDef, Term, Type}; use ailang_core::workspace::Workspace; use crate::Result; use indexmap::IndexMap; @@ -148,6 +148,71 @@ pub fn monomorphise_workspace(ws: &Workspace) -> Result { } } + // Phase 3: rewrite call sites in every fn / const body. Walk in + // the same pre-order as collect_residuals_ordered so the cursor + // and the per-callsite target list align position-by-position. + let env = build_workspace_env(&ws_owned); // re-build: ws_owned has new defs. + let module_names: Vec = ws_owned.modules.keys().cloned().collect(); + for mname in &module_names { + let n_defs = ws_owned.modules[mname].defs.len(); + for i in 0..n_defs { + let (ordered, defining_module_name): (Vec>, String) = { + let m = &ws_owned.modules[mname]; + let d = &m.defs[i]; + match d { + Def::Fn(f) => (collect_residuals_ordered(f, mname, &env)?, mname.clone()), + Def::Const(c) => { + let pseudo = AstFnDef { + name: c.name.clone(), + ty: c.ty.clone(), + params: Vec::new(), + body: c.value.clone(), + doc: None, + suppress: Vec::new(), + }; + (collect_residuals_ordered(&pseudo, mname, &env)?, mname.clone()) + } + _ => continue, + } + }; + let m = ws_owned.modules.get_mut(mname).unwrap(); + let d = &mut m.defs[i]; + let ordered_concrete: Vec = ordered + .into_iter() + .map(|o| { + o.unwrap_or_else(|| MonoTarget { + class: String::new(), + method: String::new(), + type_: Type::unit(), + defining_module: defining_module_name.clone(), + }) + }) + .collect(); + let mut cursor = 0usize; + match d { + Def::Fn(f) => { + rewrite_class_method_calls( + &mut f.body, + &env.class_methods, + mname, + &ordered_concrete, + &mut cursor, + ); + } + Def::Const(c) => { + rewrite_class_method_calls( + &mut c.value, + &env.class_methods, + mname, + &ordered_concrete, + &mut cursor, + ); + } + _ => {} + } + } + } + Ok(ws_owned) } @@ -502,3 +567,167 @@ pub fn synthesise_mono_fn( suppress: Vec::new(), }) } + +/// Iter 22b.3: rewrite every class-method call site in `body` to +/// the corresponding mono symbol, using `ordered_targets` — +/// the per-call-site resolved targets collected by a parallel +/// synth-replay run on the same body. +/// +/// The walker increments a positional counter at each +/// class-method-named `Term::Var` it encounters. The counter must +/// match the order in which `synth` pushes residuals (pre-order +/// AST walk; child evaluation order matches the `match` arms in +/// `crate::synth`). When this invariant holds, `ordered_targets[idx]` +/// is the target resolved at the i-th class-method call site. +/// +/// `caller_module`: name of the module enclosing this body. If +/// the target's `defining_module` differs, the rewritten name is +/// qualified `.` (cross-module +/// resolution path); otherwise unqualified. Targets whose `class` +/// is empty are sentinel placeholders for non-concrete residuals +/// — the cursor advances over them and the name is left unchanged. +fn rewrite_class_method_calls( + body: &mut Term, + class_methods: &BTreeMap, + caller_module: &str, + ordered_targets: &[MonoTarget], + cursor: &mut usize, +) { + match body { + Term::Var { name } => { + if class_methods.contains_key(name) { + if let Some(t) = ordered_targets.get(*cursor) { + if !t.class.is_empty() { + let sym = mono_symbol(&t.method, &t.type_); + let new_name = if t.defining_module == caller_module { + sym + } else { + format!("{}.{}", t.defining_module, sym) + }; + *name = new_name; + } + // else: residual was non-concrete → leave name unchanged. + } + *cursor += 1; + } + } + Term::App { callee, args, .. } => { + rewrite_class_method_calls(callee, class_methods, caller_module, ordered_targets, cursor); + for a in args { + rewrite_class_method_calls(a, class_methods, caller_module, ordered_targets, cursor); + } + } + Term::Let { value, body, .. } => { + rewrite_class_method_calls(value, class_methods, caller_module, ordered_targets, cursor); + rewrite_class_method_calls(body, class_methods, caller_module, ordered_targets, cursor); + } + Term::LetRec { body, in_term, .. } => { + rewrite_class_method_calls(body, class_methods, caller_module, ordered_targets, cursor); + rewrite_class_method_calls(in_term, class_methods, caller_module, ordered_targets, cursor); + } + Term::If { cond, then, else_ } => { + rewrite_class_method_calls(cond, class_methods, caller_module, ordered_targets, cursor); + rewrite_class_method_calls(then, class_methods, caller_module, ordered_targets, cursor); + rewrite_class_method_calls(else_, class_methods, caller_module, ordered_targets, cursor); + } + Term::Do { args, .. } => { + for a in args { + rewrite_class_method_calls(a, class_methods, caller_module, ordered_targets, cursor); + } + } + Term::Ctor { args, .. } => { + for a in args { + rewrite_class_method_calls(a, class_methods, caller_module, ordered_targets, cursor); + } + } + Term::Match { scrutinee, arms } => { + rewrite_class_method_calls(scrutinee, class_methods, caller_module, ordered_targets, cursor); + for Arm { body, .. } in arms { + rewrite_class_method_calls(body, class_methods, caller_module, ordered_targets, cursor); + } + } + Term::Lam { body, .. } => { + rewrite_class_method_calls(body, class_methods, caller_module, ordered_targets, cursor); + } + Term::Seq { lhs, rhs } => { + rewrite_class_method_calls(lhs, class_methods, caller_module, ordered_targets, cursor); + rewrite_class_method_calls(rhs, class_methods, caller_module, ordered_targets, cursor); + } + Term::Clone { value } => { + rewrite_class_method_calls(value, class_methods, caller_module, ordered_targets, cursor); + } + Term::ReuseAs { source, body } => { + rewrite_class_method_calls(source, class_methods, caller_module, ordered_targets, cursor); + rewrite_class_method_calls(body, class_methods, caller_module, ordered_targets, cursor); + } + Term::Lit { .. } => {} + } +} + +/// Iter 22b.3: traversal-ordered residual collection — used by +/// the rewrite walker to align cursor positions. Unlike +/// [`collect_mono_targets`], this includes non-concrete residuals +/// as `None`, preserving one-entry-per-callsite alignment. +pub(crate) fn collect_residuals_ordered( + f: &AstFnDef, + module_name: &str, + env: &crate::Env, +) -> Result>> { + let (rigids, inner_ty): (Vec, Type) = match &f.ty { + Type::Forall { vars, constraints: _, body } => (vars.clone(), (**body).clone()), + other => (vec![], other.clone()), + }; + let param_tys: Vec = match &inner_ty { + Type::Fn { params, .. } => params.clone(), + _ => return Ok(Vec::new()), + }; + + let mut env = env.clone(); + for v in &rigids { + env.rigid_vars.insert(v.clone()); + } + env.current_module = module_name.to_string(); + + let mut locals: IndexMap = IndexMap::new(); + for (n, t) in f.params.iter().zip(param_tys.iter()) { + locals.insert(n.clone(), t.clone()); + } + let mut effects: BTreeSet = BTreeSet::new(); + let mut subst = crate::Subst::default(); + let mut counter: u32 = 0; + let mut residuals: Vec = Vec::new(); + crate::synth( + &f.body, + &env, + &mut locals, + &mut effects, + &f.name, + &mut subst, + &mut counter, + &mut residuals, + )?; + + let mut out: Vec> = Vec::new(); + for r in residuals { + let r_ty = subst.apply(&r.type_); + if !crate::is_fully_concrete(&r_ty) { + out.push(None); + continue; + } + let key = (r.class.clone(), ailang_core::canonical::type_hash(&r_ty)); + let entry = match env.workspace_registry.entries.get(&key) { + Some(e) => e, + None => { + out.push(None); + continue; + } + }; + out.push(Some(MonoTarget { + class: r.class.clone(), + method: r.method.clone(), + type_: r_ty, + defining_module: entry.defining_module.clone(), + })); + } + Ok(out) +} diff --git a/examples/test_22b2_instance_present.ail.json b/examples/test_22b2_instance_present.ail.json index 9090b86..1b6634a 100644 --- a/examples/test_22b2_instance_present.ail.json +++ b/examples/test_22b2_instance_present.ail.json @@ -19,7 +19,13 @@ "type": { "k": "con", "name": "Int" }, "methods": [{ "name": "show", - "body": { "t": "lit", "lit": { "kind": "str", "value": "n" } } + "body": { + "t": "lam", + "params": ["x"], + "paramTypes": [{ "k": "var", "name": "a" }], + "retType": { "k": "con", "name": "Str" }, + "body": { "t": "lit", "lit": { "kind": "str", "value": "n" } } + } }] }, {