//! Type substitution + unification helpers for monomorphisation. //! //! Free functions extracted from `lib.rs` during the 18g tidy split. //! The four-step pipeline is: `derive_substitution` walks declared //! params against actual arg types, calling `unify_for_subst` to bind //! `Type::Var`s; `apply_subst_to_type` / `apply_subst_to_term` //! specialise a polymorphic def under that binding; //! `qualify_local_types_codegen` rewrites bare ADT names into //! `module.Type` form when a sig crosses an import boundary; //! `descriptor_for_subst` produces the stable mangling suffix used in //! the specialised symbol's name. use ailang_core::ast::*; use std::collections::{BTreeMap, BTreeSet}; use super::{CodegenError, Result}; use crate::synth::type_descriptor; /// Iter 12b: derive a name → concrete-type substitution from the /// declared params of a `Forall` body and the actual arg types at a /// call site. Walks both sides in parallel; whenever a `Type::Var` /// (rigid name) appears on the params side, binds it to the /// corresponding concrete type. Conflicts (same var bound to two /// different types) surface as an internal error — the typechecker /// would already have rejected such a call. pub(crate) fn derive_substitution( vars: &[String], params: &[Type], arg_tys: &[Type], ) -> Result> { if params.len() != arg_tys.len() { return Err(CodegenError::Internal(format!( "derive_substitution: arity mismatch ({} params vs {} args)", params.len(), arg_tys.len(), ))); } let var_set: BTreeSet<&str> = vars.iter().map(|s| s.as_str()).collect(); let mut subst: BTreeMap = BTreeMap::new(); for (p, a) in params.iter().zip(arg_tys.iter()) { unify_for_subst(p, a, &var_set, &mut subst)?; } // Any forall var not pinned by the args is left unbound. For the // MVP this is an error — we can't specialise without a concrete // type. The typechecker's body should have constrained it already // through return-type unification, but at the call site we only // see args; if needed, callers can extend this with expected-ret // info. // Iter 15a: a forall var that the args couldn't pin (e.g. // `is_none(Nothing) : forall a. (Maybe a) -> Bool` — `a` is // genuinely unobservable from the args alone) defaults to `Unit`. // The specialised body must not actually read an `a`-typed value, // or it would have failed type-checking; a dummy concrete type is // sound and lets monomorphisation proceed deterministically. The // descriptor uses the same default, so all such call sites // converge on a single specialisation. for v in vars { if !subst.contains_key(v) { subst.insert(v.clone(), Type::unit()); } } Ok(subst) } /// Walks `param` and `arg` in parallel, treating any `Type::Var { name }` /// on the param side whose name is in `vars` as an unknown to be bound /// in `subst`. Identical concrete shapes pass through; structural /// mismatches yield an internal error. pub(crate) fn unify_for_subst( param: &Type, arg: &Type, vars: &BTreeSet<&str>, subst: &mut BTreeMap, ) -> Result<()> { // Iter 14a fix, extended in 15g-aux: a `$u`-prefixed var is a // synth-only wildcard produced by `synth_arg_type` for nullary // ctors of a parameterised ADT (e.g. `Nil : List<$u>`). It // carries no real constraint — accept without binding so a // sibling arg can pin the type var instead. Without this, // `Cons(Int, Nil)` synth would unify `a = Int` (from head) and // then `a = $u` (from tail's recursive `List` slot) and // falsely error. // // 15g-aux: the early-return must accept `$u` on **either** side. // `$u` enters in arg position from synth, but the prev-binding // recursion below (`unify_for_subst(&prev, arg, ...)`) can swap // a `$u` onto the param side when a previously-bound type is // unified against a fresher arg whose roles differ. Reduced // repro: `length [Left 1, Right 10]` — `a` first binds to // `Either` from `Left 1`, then a recursive unification // against `Either<$u, Int>` from `Right 10` lands `$u` in the // param-pos[1] slot. Symmetric early-return is correct because // `$u` is a synth-only wildcard regardless of which side carries // it after the prev-binding swap. if let Type::Var { name } = arg { if name.starts_with("$u") { return Ok(()); } } if let Type::Var { name } = param { if name.starts_with("$u") { return Ok(()); } } match (param, arg) { (Type::Var { name }, _) if vars.contains(name.as_str()) => { 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(()) } ( Type::Con { name: pn, args: pa }, Type::Con { name: an, args: aa }, ) if pn == an && pa.len() == aa.len() => { for (p, a) in pa.iter().zip(aa.iter()) { unify_for_subst(p, a, vars, subst)?; } Ok(()) } ( Type::Fn { params: pp, ret: pr, .. }, Type::Fn { params: ap, ret: ar, .. }, ) => { if pp.len() != ap.len() { return Err(CodegenError::Internal( "monomorphisation: fn arity mismatch in arg".into(), )); } for (p, a) in pp.iter().zip(ap.iter()) { unify_for_subst(p, a, vars, subst)?; } unify_for_subst(pr, ar, vars, subst) } (Type::Var { name: pn }, Type::Var { name: an }) if pn == an => Ok(()), _ => Err(CodegenError::Internal(format!( "monomorphisation: cannot match param `{}` to arg `{}`", ailang_core::pretty::type_to_string(param), ailang_core::pretty::type_to_string(arg), ))), } } /// Iter 15a: rewrites bare `Type::Con` references that resolve against /// `owner_local_types` into qualified `module.Type` form. Mirrors /// `ailang_check::qualify_local_types`. Used when the codegen pulls a /// polymorphic fn signature across the import boundary; without this /// the substitution derived from the call site's qualified args /// (`std_maybe.Maybe`) would fail to unify against the bare /// signature (`Maybe`). pub(crate) fn qualify_local_types_codegen( t: &Type, owner_module: &str, owner_local_types: &BTreeSet, ) -> Type { match t { Type::Con { name, args } => { let qualified = if name.contains('.') { name.clone() } else if ailang_core::primitives::is_primitive_name(name) { name.clone() } else if owner_local_types.contains(name) { format!("{owner_module}.{name}") } else { name.clone() }; Type::Con { name: qualified, args: args .iter() .map(|a| qualify_local_types_codegen(a, owner_module, owner_local_types)) .collect(), } } Type::Fn { params, ret, effects, .. } => Type::Fn { params: params .iter() .map(|p| qualify_local_types_codegen(p, owner_module, owner_local_types)) .collect(), ret: Box::new(qualify_local_types_codegen(ret, owner_module, owner_local_types)), effects: effects.clone(), param_modes: vec![], ret_mode: ParamMode::Implicit, }, Type::Forall { vars, constraints, body } => Type::Forall { vars: vars.clone(), constraints: constraints.clone(), body: Box::new(qualify_local_types_codegen(body, owner_module, owner_local_types)), }, Type::Var { .. } => t.clone(), } } /// Iter 12b: substitute rigid type vars in `t` according to `subst`. /// Used to specialise the type of a polymorphic def for a given /// instantiation. pub(crate) fn apply_subst_to_type(t: &Type, subst: &BTreeMap) -> Type { match t { Type::Var { name } => subst.get(name).cloned().unwrap_or_else(|| t.clone()), Type::Con { name, args } => Type::Con { name: name.clone(), args: args.iter().map(|a| apply_subst_to_type(a, subst)).collect(), }, Type::Fn { params, ret, effects, .. } => Type::Fn { params: params.iter().map(|p| apply_subst_to_type(p, subst)).collect(), ret: Box::new(apply_subst_to_type(ret, subst)), effects: effects.clone(), param_modes: vec![], ret_mode: ParamMode::Implicit, }, Type::Forall { vars, constraints, body } => { // Inner forall shadows: don't substitute re-bound names. let inner: BTreeMap = subst .iter() .filter(|(k, _)| !vars.contains(k)) .map(|(k, v)| (k.clone(), v.clone())) .collect(); Type::Forall { vars: vars.clone(), constraints: constraints.clone(), body: Box::new(apply_subst_to_type(body, &inner)), } } } } /// Iter 12b: substitute rigid type vars throughout a Term. Only /// `Term::Lam` carries types in the AST (params/ret), so most arms /// just recurse. `Term::Var` contains a name string only and is /// left untouched. pub(crate) fn apply_subst_to_term(t: &Term, subst: &BTreeMap) -> Term { match t { Term::Lit { .. } | Term::Var { .. } => t.clone(), Term::App { callee, args, tail } => Term::App { callee: Box::new(apply_subst_to_term(callee, subst)), args: args.iter().map(|a| apply_subst_to_term(a, subst)).collect(), tail: *tail, }, Term::Let { name, value, body } => Term::Let { name: name.clone(), value: Box::new(apply_subst_to_term(value, subst)), body: Box::new(apply_subst_to_term(body, subst)), }, Term::If { cond, then, else_ } => Term::If { cond: Box::new(apply_subst_to_term(cond, subst)), then: Box::new(apply_subst_to_term(then, subst)), else_: Box::new(apply_subst_to_term(else_, subst)), }, Term::Do { op, args, tail } => Term::Do { op: op.clone(), args: args.iter().map(|a| apply_subst_to_term(a, subst)).collect(), tail: *tail, }, Term::Ctor { type_name, ctor, args } => Term::Ctor { type_name: type_name.clone(), ctor: ctor.clone(), args: args.iter().map(|a| apply_subst_to_term(a, subst)).collect(), }, Term::Match { scrutinee, arms } => Term::Match { scrutinee: Box::new(apply_subst_to_term(scrutinee, subst)), arms: arms .iter() .map(|a| Arm { pat: a.pat.clone(), body: apply_subst_to_term(&a.body, subst), }) .collect(), }, Term::Lam { params, param_tys, ret_ty, effects, body } => Term::Lam { params: params.clone(), param_tys: param_tys .iter() .map(|t| apply_subst_to_type(t, subst)) .collect(), ret_ty: Box::new(apply_subst_to_type(ret_ty, subst)), effects: effects.clone(), body: Box::new(apply_subst_to_term(body, subst)), }, Term::Seq { lhs, rhs } => Term::Seq { lhs: Box::new(apply_subst_to_term(lhs, subst)), rhs: Box::new(apply_subst_to_term(rhs, subst)), }, Term::LetRec { .. } => { // Iter 16b.1: eliminated by desugar before any // monomorphisation pass runs. unreachable!("Term::LetRec eliminated by desugar") } Term::Clone { value } => Term::Clone { // Iter 18c.1: structural recursion through the wrapper. value: Box::new(apply_subst_to_term(value, subst)), }, Term::ReuseAs { source, body } => Term::ReuseAs { // Iter 18d.1: structural recursion through both children. source: Box::new(apply_subst_to_term(source, subst)), body: Box::new(apply_subst_to_term(body, subst)), }, } } /// Iter 12b: deterministic descriptor string for a substitution. Used /// as the suffix in the mangled name `@ail____`. /// Vars are emitted in the order given by the FnDef's forall vars /// (so two call sites with the same instantiation map to the same /// descriptor regardless of internal BTreeMap ordering). pub(crate) fn descriptor_for_subst(vars: &[String], subst: &BTreeMap) -> String { let mut parts: Vec = Vec::with_capacity(vars.len()); for v in vars { let ty = subst.get(v).cloned().unwrap_or_else(|| Type::unit()); parts.push(type_descriptor(&ty)); } parts.join("_") }