diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index 8cfa042..0891409 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -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 { - 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 { - if let Type::Fn { params, ret, .. } = t { - let p: Result> = 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 { - 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 { - 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> { - 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. -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`). -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 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) -> 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 = 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) -> 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). -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("_") -} - -/// Iter 12b: a stable, identifier-safe descriptor for a `Type`. -/// Maps `Int → I`, `Bool → B`, `Unit → U`, `Str → S`, ADT name `Foo → -/// FFoo`, fn → `FR` (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::*; diff --git a/crates/ailang-codegen/src/subst.rs b/crates/ailang-codegen/src/subst.rs new file mode 100644 index 0000000..b502b7c --- /dev/null +++ b/crates/ailang-codegen/src/subst.rs @@ -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> { + 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 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) -> 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 = 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) -> 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("_") +} diff --git a/crates/ailang-codegen/src/synth.rs b/crates/ailang-codegen/src/synth.rs new file mode 100644 index 0000000..16b94ad --- /dev/null +++ b/crates/ailang-codegen/src/synth.rs @@ -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 { + 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 { + if let Type::Fn { params, ret, .. } = t { + let p: Result> = 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 { + 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 { + 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 → `FR` (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 +}