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:
2026-05-08 15:41:04 +02:00
parent d823be49df
commit 84ba83dda3
3 changed files with 546 additions and 506 deletions
+11 -506
View File
@@ -41,7 +41,18 @@ use std::collections::{BTreeMap, BTreeSet};
use ailang_check::uniqueness::{infer_module, UniquenessTable};
mod escape;
mod subst;
mod synth;
use escape::NonEscapeSet;
use subst::{
apply_subst_to_term, apply_subst_to_type, derive_substitution, descriptor_for_subst,
qualify_local_types_codegen, unify_for_subst,
};
use synth::{
builtin_ail_type, builtin_binop, builtin_effect_op_ret, c_byte_len, default_triple,
fn_sig_from_type, ll_string_literal, llvm_type,
};
/// Failure modes of [`emit_ir`] / [`lower_workspace`].
///
@@ -4488,512 +4499,6 @@ impl<'a> Emitter<'a> {
}
}
fn llvm_type(t: &Type) -> Result<String> {
match t {
Type::Con { name, .. } => match name.as_str() {
"Int" => Ok("i64".into()),
"Bool" => Ok("i1".into()),
"Unit" => Ok("i8".into()),
"Str" => Ok("ptr".into()),
// All other type names are treated as ADT (boxed).
// If the typechecker didn't reject this earlier, it's
// intentional — otherwise `ptr` would mask a wrong value.
_ => Ok("ptr".into()),
},
// Function values (Iter 7): all fn-pointers are opaque `ptr`
// at the LLVM level. The actual signature travels via the
// emitter's `ssa_fn_sigs` sidetable.
Type::Fn { .. } => Ok("ptr".into()),
// Iter 13b: an unresolved rigid `Type::Var` reaching codegen is
// a substitution bug. Earlier this silently lowered as `ptr`
// (via the ADT fallback) and produced garbage IR; failing loudly
// here surfaces the bug in the test suite.
Type::Var { name } => Err(CodegenError::UnsupportedType(format!(
"unresolved type var `{name}` in codegen"
))),
other => Err(CodegenError::UnsupportedType(
ailang_core::pretty::type_to_string(other),
)),
}
}
/// Builds an `FnSig` (LLVM types only) from an AILang `Type::Fn`.
/// Returns `None` for non-function types or if any param/ret type fails
/// to lower (e.g. a residual `Type::Var` or `Forall` that the typechecker
/// would reject before us).
fn fn_sig_from_type(t: &Type) -> Option<FnSig> {
if let Type::Fn { params, ret, .. } = t {
let p: Result<Vec<String>> = params.iter().map(llvm_type).collect();
let r = llvm_type(ret);
if let (Ok(p), Ok(r)) = (p, r) {
return Some(FnSig { params: p, ret: r });
}
}
None
}
/// Iter 12b: AILang type of a builtin operator. Used by
/// `synth_arg_type` for arg-type inference at polymorphic call sites.
/// Mirrors what the typechecker installs in its env via `builtins`.
fn builtin_ail_type(name: &str) -> Option<Type> {
let int_int_int = || Type::Fn {
params: vec![Type::int(), Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
};
let int_int_bool = || Type::Fn {
params: vec![Type::int(), Type::int()],
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
};
Some(match name {
"+" | "-" | "*" | "/" | "%" => int_int_int(),
"!=" | "<" | "<=" | ">" | ">=" => int_int_bool(),
// Iter 16e: `==` is polymorphic — `forall a. (a, a) -> Bool`.
// The mono pipeline asks `synth_arg_type` for the actual arg
// types at the call site; `lower_app` then dispatches to the
// right LLVM instruction (icmp eq i64 / i1, @strcmp, or
// constant i1 1) on those resolved types.
"==" => Type::Forall {
vars: vec!["a".into()],
body: Box::new(Type::Fn {
params: vec![
Type::Var { name: "a".into() },
Type::Var { name: "a".into() },
],
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
}),
},
"not" => Type::Fn {
params: vec![Type::bool_()],
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
// Iter 16d: `__unreachable__` is the polymorphic bottom value
// (`forall a. a`). Mirrors the typechecker's `builtins::install`.
"__unreachable__" => Type::Forall {
vars: vec!["a".into()],
body: Box::new(Type::Var { name: "a".into() }),
},
_ => return None,
})
}
/// Iter 12b: AILang return type of a built-in effect op. The op's
/// param signature is irrelevant here since we only consume the ret.
fn builtin_effect_op_ret(op: &str) -> Option<Type> {
Some(match op {
"io/print_int" | "io/print_bool" | "io/print_str" => Type::unit(),
_ => return None,
})
}
/// 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.
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.
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>`).
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.
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.
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).
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("_")
}
/// Iter 12b: a stable, identifier-safe descriptor for a `Type`.
/// Maps `Int → I`, `Bool → B`, `Unit → U`, `Str → S`, ADT name `Foo →
/// FFoo`, fn → `F<params...>R<ret>` (no recursion guard since types in
/// the MVP are non-recursive at the type level).
fn type_descriptor(t: &Type) -> String {
match t {
Type::Con { name, args } => {
let head = match name.as_str() {
"Int" => "I".into(),
"Bool" => "B".into(),
"Unit" => "U".into(),
"Str" => "S".into(),
other => format!("F{other}"),
};
if args.is_empty() {
head
} else {
// Iter 13a: parameterised ADTs get their type-arg
// descriptors appended, e.g. `FBox` of `Int` → `FBox_I`.
let mut s = head;
for a in args {
s.push('_');
s.push_str(&type_descriptor(a));
}
s
}
}
Type::Fn { params, ret, .. } => {
let mut s = String::from("Fn");
for p in params {
s.push('_');
s.push_str(&type_descriptor(p));
}
s.push_str("__r_");
s.push_str(&type_descriptor(ret));
s
}
Type::Var { name } => format!("V{name}"),
Type::Forall { .. } => "FORALL".into(),
}
}
fn builtin_binop(name: &str) -> Option<(&'static str, &'static str)> {
Some(match name {
"+" => ("add", "i64"),
"-" => ("sub", "i64"),
"*" => ("mul", "i64"),
"/" => ("sdiv", "i64"),
"%" => ("srem", "i64"),
"==" => ("icmp eq", "i1"),
"!=" => ("icmp ne", "i1"),
"<" => ("icmp slt", "i1"),
"<=" => ("icmp sle", "i1"),
">" => ("icmp sgt", "i1"),
">=" => ("icmp sge", "i1"),
_ => return None,
})
}
fn c_byte_len(s: &str) -> usize {
s.len() + 1 // + NUL terminator
}
/// Escapes a string for LLVM IR `c"..."`. All bytes outside
/// 0x20..0x7E are escaped as `\HH`; `"` and `\` likewise. Ends with `\00`.
fn default_triple() -> &'static str {
// In the MVP we query the compile host. For cross-compilation this
// would need to be configurable — not needed now.
if cfg!(target_os = "linux") && cfg!(target_arch = "x86_64") {
"x86_64-pc-linux-gnu"
} else if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") {
"arm64-apple-darwin"
} else if cfg!(target_os = "macos") && cfg!(target_arch = "x86_64") {
"x86_64-apple-darwin"
} else if cfg!(target_arch = "aarch64") {
"aarch64-unknown-linux-gnu"
} else {
"x86_64-pc-linux-gnu"
}
}
fn ll_string_literal(s: &str) -> String {
let mut out = String::new();
for &b in s.as_bytes() {
match b {
b'"' => out.push_str("\\22"),
b'\\' => out.push_str("\\5C"),
0x20..=0x7E => out.push(b as char),
_ => out.push_str(&format!("\\{:02X}", b)),
}
}
out.push_str("\\00");
out
}
#[cfg(test)]
mod tests {
use super::*;
+319
View File
@@ -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("_")
}
+216
View File
@@ -0,0 +1,216 @@
//! Pure type-synthesis and IR-shaping helpers.
//!
//! Free functions extracted from `lib.rs` during the 18g tidy split.
//! None of these touch the `Emitter` state — they map AILang `Type`s
//! to LLVM type strings, mangling descriptors, or built-in op
//! signatures. Submodule access to the parent module's private
//! `Result`, `CodegenError`, and `FnSig` works through normal Rust
//! visibility (a submodule sees its parent's private items).
use ailang_core::ast::*;
use super::{CodegenError, FnSig, Result};
pub(crate) fn llvm_type(t: &Type) -> Result<String> {
match t {
Type::Con { name, .. } => match name.as_str() {
"Int" => Ok("i64".into()),
"Bool" => Ok("i1".into()),
"Unit" => Ok("i8".into()),
"Str" => Ok("ptr".into()),
// All other type names are treated as ADT (boxed).
// If the typechecker didn't reject this earlier, it's
// intentional — otherwise `ptr` would mask a wrong value.
_ => Ok("ptr".into()),
},
// Function values (Iter 7): all fn-pointers are opaque `ptr`
// at the LLVM level. The actual signature travels via the
// emitter's `ssa_fn_sigs` sidetable.
Type::Fn { .. } => Ok("ptr".into()),
// Iter 13b: an unresolved rigid `Type::Var` reaching codegen is
// a substitution bug. Earlier this silently lowered as `ptr`
// (via the ADT fallback) and produced garbage IR; failing loudly
// here surfaces the bug in the test suite.
Type::Var { name } => Err(CodegenError::UnsupportedType(format!(
"unresolved type var `{name}` in codegen"
))),
other => Err(CodegenError::UnsupportedType(
ailang_core::pretty::type_to_string(other),
)),
}
}
/// Builds an `FnSig` (LLVM types only) from an AILang `Type::Fn`.
/// Returns `None` for non-function types or if any param/ret type fails
/// to lower (e.g. a residual `Type::Var` or `Forall` that the typechecker
/// would reject before us).
pub(crate) fn fn_sig_from_type(t: &Type) -> Option<FnSig> {
if let Type::Fn { params, ret, .. } = t {
let p: Result<Vec<String>> = params.iter().map(llvm_type).collect();
let r = llvm_type(ret);
if let (Ok(p), Ok(r)) = (p, r) {
return Some(FnSig { params: p, ret: r });
}
}
None
}
/// Iter 12b: AILang type of a builtin operator. Used by
/// `synth_arg_type` for arg-type inference at polymorphic call sites.
/// Mirrors what the typechecker installs in its env via `builtins`.
pub(crate) fn builtin_ail_type(name: &str) -> Option<Type> {
let int_int_int = || Type::Fn {
params: vec![Type::int(), Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
};
let int_int_bool = || Type::Fn {
params: vec![Type::int(), Type::int()],
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
};
Some(match name {
"+" | "-" | "*" | "/" | "%" => int_int_int(),
"!=" | "<" | "<=" | ">" | ">=" => int_int_bool(),
// Iter 16e: `==` is polymorphic — `forall a. (a, a) -> Bool`.
// The mono pipeline asks `synth_arg_type` for the actual arg
// types at the call site; `lower_app` then dispatches to the
// right LLVM instruction (icmp eq i64 / i1, @strcmp, or
// constant i1 1) on those resolved types.
"==" => Type::Forall {
vars: vec!["a".into()],
body: Box::new(Type::Fn {
params: vec![
Type::Var { name: "a".into() },
Type::Var { name: "a".into() },
],
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
}),
},
"not" => Type::Fn {
params: vec![Type::bool_()],
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
// Iter 16d: `__unreachable__` is the polymorphic bottom value
// (`forall a. a`). Mirrors the typechecker's `builtins::install`.
"__unreachable__" => Type::Forall {
vars: vec!["a".into()],
body: Box::new(Type::Var { name: "a".into() }),
},
_ => return None,
})
}
/// Iter 12b: AILang return type of a built-in effect op. The op's
/// param signature is irrelevant here since we only consume the ret.
pub(crate) fn builtin_effect_op_ret(op: &str) -> Option<Type> {
Some(match op {
"io/print_int" | "io/print_bool" | "io/print_str" => Type::unit(),
_ => return None,
})
}
/// Iter 12b: a stable, identifier-safe descriptor for a `Type`.
/// Maps `Int → I`, `Bool → B`, `Unit → U`, `Str → S`, ADT name `Foo →
/// FFoo`, fn → `F<params...>R<ret>` (no recursion guard since types in
/// the MVP are non-recursive at the type level).
pub(crate) fn type_descriptor(t: &Type) -> String {
match t {
Type::Con { name, args } => {
let head = match name.as_str() {
"Int" => "I".into(),
"Bool" => "B".into(),
"Unit" => "U".into(),
"Str" => "S".into(),
other => format!("F{other}"),
};
if args.is_empty() {
head
} else {
// Iter 13a: parameterised ADTs get their type-arg
// descriptors appended, e.g. `FBox` of `Int` → `FBox_I`.
let mut s = head;
for a in args {
s.push('_');
s.push_str(&type_descriptor(a));
}
s
}
}
Type::Fn { params, ret, .. } => {
let mut s = String::from("Fn");
for p in params {
s.push('_');
s.push_str(&type_descriptor(p));
}
s.push_str("__r_");
s.push_str(&type_descriptor(ret));
s
}
Type::Var { name } => format!("V{name}"),
Type::Forall { .. } => "FORALL".into(),
}
}
pub(crate) fn builtin_binop(name: &str) -> Option<(&'static str, &'static str)> {
Some(match name {
"+" => ("add", "i64"),
"-" => ("sub", "i64"),
"*" => ("mul", "i64"),
"/" => ("sdiv", "i64"),
"%" => ("srem", "i64"),
"==" => ("icmp eq", "i1"),
"!=" => ("icmp ne", "i1"),
"<" => ("icmp slt", "i1"),
"<=" => ("icmp sle", "i1"),
">" => ("icmp sgt", "i1"),
">=" => ("icmp sge", "i1"),
_ => return None,
})
}
pub(crate) fn c_byte_len(s: &str) -> usize {
s.len() + 1 // + NUL terminator
}
/// Escapes a string for LLVM IR `c"..."`. All bytes outside
/// 0x20..0x7E are escaped as `\HH`; `"` and `\` likewise. Ends with `\00`.
pub(crate) fn default_triple() -> &'static str {
// In the MVP we query the compile host. For cross-compilation this
// would need to be configurable — not needed now.
if cfg!(target_os = "linux") && cfg!(target_arch = "x86_64") {
"x86_64-pc-linux-gnu"
} else if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") {
"arm64-apple-darwin"
} else if cfg!(target_os = "macos") && cfg!(target_arch = "x86_64") {
"x86_64-apple-darwin"
} else if cfg!(target_arch = "aarch64") {
"aarch64-unknown-linux-gnu"
} else {
"x86_64-pc-linux-gnu"
}
}
pub(crate) fn ll_string_literal(s: &str) -> String {
let mut out = String::new();
for &b in s.as_bytes() {
match b {
b'"' => out.push_str("\\22"),
b'\\' => out.push_str("\\5C"),
0x20..=0x7E => out.push(b as char),
_ => out.push_str(&format!("\\{:02X}", b)),
}
}
out.push_str("\\00");
out
}