codegen tidy: extract pure helpers into synth.rs + subst.rs
First slice of the codegen module split. Pulls the run of free functions at the bottom of lib.rs into two purpose-named submodules, with no behaviour change: - synth.rs (216 lines): LLVM-IR shaping helpers — llvm_type, fn_sig_from_type, builtin_ail_type, builtin_effect_op_ret, type_descriptor, builtin_binop, c_byte_len, default_triple, ll_string_literal. - subst.rs (319 lines): monomorphisation pipeline — the derive_substitution + unify_for_subst + apply_subst_to_* family, plus qualify_local_types_codegen and descriptor_for_subst. lib.rs drops from 5295 → 4800 lines. Visibility is unchanged (submodules see lib.rs's private Result/CodegenError/FnSig through normal Rust scoping); call sites are unchanged in behaviour, only re-imported via 'use' at the lib.rs head. Motivation is the codebase-control conversation: lib.rs was the one navigability outlier flagged in the size sanity- check, and the 18g family closing left a clean window before the next iter. The remaining bulk (drop emission, match lowering, lambda lowering) will come out in follow-up commits, each an independent move-only refactor with full cargo test --workspace between.
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
//! 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<BTreeMap<String, Type>> {
|
||||
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<String, Type> = 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<String, Type>,
|
||||
) -> 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<a>` 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<Int, $u>` 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<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(())
|
||||
}
|
||||
(
|
||||
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<Int>`) would fail to unify against the bare
|
||||
/// signature (`Maybe<a>`).
|
||||
pub(crate) fn qualify_local_types_codegen(
|
||||
t: &Type,
|
||||
owner_module: &str,
|
||||
owner_local_types: &BTreeSet<String>,
|
||||
) -> Type {
|
||||
match t {
|
||||
Type::Con { name, args } => {
|
||||
let qualified = if name.contains('.') {
|
||||
name.clone()
|
||||
} else if matches!(name.as_str(), "Int" | "Bool" | "Unit" | "Str") {
|
||||
name.clone()
|
||||
} else if owner_local_types.contains(name) {
|
||||
format!("{owner_module}.{name}")
|
||||
} else {
|
||||
name.clone()
|
||||
};
|
||||
Type::Con {
|
||||
name: qualified,
|
||||
args: args
|
||||
.iter()
|
||||
.map(|a| qualify_local_types_codegen(a, owner_module, owner_local_types))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
Type::Fn { params, ret, effects, .. } => Type::Fn {
|
||||
params: params
|
||||
.iter()
|
||||
.map(|p| qualify_local_types_codegen(p, owner_module, owner_local_types))
|
||||
.collect(),
|
||||
ret: Box::new(qualify_local_types_codegen(ret, owner_module, owner_local_types)),
|
||||
effects: effects.clone(),
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
},
|
||||
Type::Forall { vars, body } => Type::Forall {
|
||||
vars: vars.clone(),
|
||||
body: Box::new(qualify_local_types_codegen(body, owner_module, owner_local_types)),
|
||||
},
|
||||
Type::Var { .. } => t.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Iter 12b: substitute rigid type vars in `t` according to `subst`.
|
||||
/// Used to specialise the type of a polymorphic def for a given
|
||||
/// instantiation.
|
||||
pub(crate) fn apply_subst_to_type(t: &Type, subst: &BTreeMap<String, Type>) -> 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, body } => {
|
||||
// Inner forall shadows: don't substitute re-bound names.
|
||||
let inner: BTreeMap<String, Type> = subst
|
||||
.iter()
|
||||
.filter(|(k, _)| !vars.contains(k))
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect();
|
||||
Type::Forall {
|
||||
vars: vars.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<String, Type>) -> 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_<m>_<def>__<descriptor>`.
|
||||
/// 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, Type>) -> String {
|
||||
let mut parts: Vec<String> = 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("_")
|
||||
}
|
||||
Reference in New Issue
Block a user