//! 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()), "Float" => Ok("double".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 { // Iter 22-floats.3: same widening as `crates/ailang-check/src/ // builtins.rs` — `+`/`-`/`*`/`/` and `!=`/`<`/`<=`/`>`/`>=` are // polymorphic. `%` stays monomorphic-Int. Codegen lowering for // these ops still goes through `builtin_binop` (Int-only) in // iter 3; iter 4 converts that to type-dispatched. let poly_a_a_to_a = || Type::Forall { vars: vec!["a".into()], constraints: vec![], body: Box::new(Type::Fn { params: vec![ Type::Var { name: "a".into() }, Type::Var { name: "a".into() }, ], ret: Box::new(Type::Var { name: "a".into() }), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }), }; let poly_a_a_to_bool = || Type::Forall { vars: vec!["a".into()], constraints: vec![], 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, }), }; 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, }; Some(match name { "+" | "-" | "*" | "/" => poly_a_a_to_a(), "%" => int_int_int(), "!=" | "<" | "<=" | ">" | ">=" => poly_a_a_to_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()], constraints: vec![], 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()], constraints: vec![], body: Box::new(Type::Var { name: "a".into() }), }, // Iter 22-floats.3: Float-conversion and inspection builtins. // Same lockstep with `crates/ailang-check/src/builtins.rs`. "neg" => Type::Forall { vars: vec!["a".into()], constraints: vec![], body: Box::new(Type::Fn { params: vec![Type::Var { name: "a".into() }], ret: Box::new(Type::Var { name: "a".into() }), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }), }, "int_to_float" => Type::Fn { params: vec![Type::int()], ret: Box::new(Type::float()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, "float_to_int_truncate" => Type::Fn { params: vec![Type::float()], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, "float_to_str" => Type::Fn { params: vec![Type::float()], ret: Box::new(Type::str_()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, "is_nan" => Type::Fn { params: vec![Type::float()], ret: Box::new(Type::bool_()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, // Iter 22-floats.3: Float bit-pattern constants. Bare values, // not fns — referenced via `Term::Var`. Mirrors the // typechecker's `builtins::install`. Codegen emits LLVM hex- // float literals at the use site in iter 4. "nan" | "inf" | "neg_inf" => Type::float(), _ => 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" | "io/print_float" => 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(), "Float" => "Fl".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(), } } /// Floats iter 4.2: arithmetic / comparison ops are type-dispatched /// over `{Int, Float}`. Caller (`lower_app`) resolves the arg type /// via `synth_arg_type` and passes it here. Returns the /// `(instruction, operand_llvm_type, result_llvm_type)` triple to /// emit. `%` stays Int-only. /// /// The triple carries operand and result types separately because /// comparison ops produce `i1` regardless of operand width /// (e.g. `icmp slt i64 ..., ... -> i1`); arithmetic produces the /// same type as its operands. Keeping both in the table localises /// the comparison-vs-arithmetic distinction here — the caller no /// longer has to second-guess which arm fired. /// /// Comparison-op Int arms are kept here in iter 4.2 to preserve /// the no-regression invariant — pre-iter-4 codegen routed /// `<`/`<=`/`>`/`>=`/`!=` through the same Int-only `builtin_binop` /// table. Iter 4.3 adds the Float arms (`("fcmp olt", "double", "i1")` /// and friends). pub(crate) fn builtin_binop_typed( name: &str, arg_ty: &Type, ) -> Option<(&'static str, &'static str, &'static str)> { let is_int = matches!(arg_ty, Type::Con { name, .. } if name == "Int"); let is_float = matches!(arg_ty, Type::Con { name, .. } if name == "Float"); match (name, is_int, is_float) { ("+", true, _) => Some(("add", "i64", "i64")), ("+", _, true) => Some(("fadd", "double", "double")), ("-", true, _) => Some(("sub", "i64", "i64")), ("-", _, true) => Some(("fsub", "double", "double")), ("*", true, _) => Some(("mul", "i64", "i64")), ("*", _, true) => Some(("fmul", "double", "double")), ("/", true, _) => Some(("sdiv", "i64", "i64")), ("/", _, true) => Some(("fdiv", "double", "double")), ("%", true, _) => Some(("srem", "i64", "i64")), // Comparison ops: operand types differ, result is always `i1`. // Int-arm comparisons preserved from pre-iter-4 `builtin_binop`; // Float arms land in iter 4.3. ("!=", true, _) => Some(("icmp ne", "i64", "i1")), ("<", true, _) => Some(("icmp slt", "i64", "i1")), ("<=", true, _) => Some(("icmp sle", "i64", "i1")), (">", true, _) => Some(("icmp sgt", "i64", "i1")), (">=", true, _) => Some(("icmp sge", "i64", "i1")), // Float comparison arms (Floats iter 4.3). Note: `!=` Float // uses `fcmp une` ("unordered or not equal") — NOT `one` // ("ordered and not equal"), which would return `false` for // `nan != nan` and violate IEEE / spec A5. ("!=", _, true) => Some(("fcmp une", "double", "i1")), ("<", _, true) => Some(("fcmp olt", "double", "i1")), ("<=", _, true) => Some(("fcmp ole", "double", "i1")), (">", _, true) => Some(("fcmp ogt", "double", "i1")), (">=", _, true) => Some(("fcmp oge", "double", "i1")), _ => 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 }