//! LLVM-IR-Text-Emitter für AILang (MVP). //! //! Strategie: Wir erzeugen LLVM-IR als String, schreiben sie als `.ll` und //! linken sie mit `clang`. Keine Bindung an eine bestimmte libllvm-Version. //! //! Typ-Mapping: //! - `Int` -> `i64` //! - `Bool` -> `i1` //! - `Unit` -> `i8` (Wert immer 0) //! //! Named-Mangling: AILang-Fn `foo` wird zu LLVM `@ail_foo`. Wenn ein Modul //! eine Funktion `main : () -> Unit !IO` hat, wird zusätzlich ein //! `define i32 @main()` Wrapper erzeugt, der `@ail_main` aufruft und 0 //! zurückgibt — damit das Binary direkt ausführbar ist. use ailang_core::ast::*; use std::collections::BTreeMap; #[derive(Debug, thiserror::Error)] pub enum CodegenError { #[error("def `{0}`: {1}")] Def(String, Box), #[error("unsupported type: {0}")] UnsupportedType(String), #[error("unknown variable: `{0}`")] UnknownVar(String), #[error("expected fn type, got {0}")] NotFnType(String), #[error("internal: {0}")] Internal(String), } type Result = std::result::Result; pub fn emit_ir(m: &Module) -> Result { let mut emitter = Emitter::new(m); emitter.emit_module()?; Ok(emitter.finish()) } struct Emitter<'a> { module: &'a Module, header: String, body: String, /// String-Konstanten: content -> (global-name, llvm-typ-länge inkl. \0) strings: BTreeMap, /// Lokale Symboltabelle pro Funktion: name -> (ssa-name inkl `%`, llvm-typ). locals: Vec<(String, String, String)>, /// fortlaufender Zähler für SSA-Werte und Labels. counter: u64, /// fortlaufender Zähler für globale String-Namen. str_counter: u64, /// Liste aller user-definierten Top-Level-Funktionen (für call-resolution). user_fns: BTreeMap, /// ADT-Tabelle: type_name -> Liste von ctors in Definition-Reihenfolge. /// Tag eines ctors = Index in dieser Liste. Wird in `ctor_index` /// repliziert; behalten für künftige Tools (Pretty-Printer für ADT-Werte, /// Decision-Tree-Optimierung). #[allow(dead_code)] types: BTreeMap>, /// Inverser Index: ctor-name -> (type_name, tag, field_llvm_types). ctor_index: BTreeMap, /// Aktuelles Basic-Block-Label. Wird von `start_block` gesetzt und ist /// die einzige Quelle der Wahrheit für `phi`-Operanden. current_block: String, } #[derive(Debug, Clone)] #[allow(dead_code)] struct CtorInfo { name: String, fields: Vec, // llvm types } #[derive(Debug, Clone)] struct CtorRef { type_name: String, tag: u32, fields: Vec, } #[derive(Debug, Clone)] struct FnSig { params: Vec, // llvm types ret: String, // llvm type } impl<'a> Emitter<'a> { fn new(module: &'a Module) -> Self { let mut user_fns = BTreeMap::new(); for def in &module.defs { if let Def::Fn(f) = def { if let Type::Fn { params, ret, .. } = &f.ty { let psig: Result> = params.iter().map(llvm_type).collect(); let rsig = llvm_type(ret); if let (Ok(params), Ok(ret)) = (psig, rsig) { user_fns.insert(f.name.clone(), FnSig { params, ret }); } } } } let mut types: BTreeMap> = BTreeMap::new(); let mut ctor_index: BTreeMap = BTreeMap::new(); for def in &module.defs { if let Def::Type(td) = def { let mut infos = Vec::new(); for (i, c) in td.ctors.iter().enumerate() { let fields: Vec = c .fields .iter() .map(|t| llvm_type(t).unwrap_or_else(|_| "i64".into())) .collect(); infos.push(CtorInfo { name: c.name.clone(), fields: fields.clone(), }); ctor_index.insert( c.name.clone(), CtorRef { type_name: td.name.clone(), tag: i as u32, fields, }, ); } types.insert(td.name.clone(), infos); } } Self { module, header: String::new(), body: String::new(), strings: BTreeMap::new(), locals: Vec::new(), counter: 0, str_counter: 0, user_fns, types, ctor_index, current_block: String::new(), } } fn start_block(&mut self, label: &str) { self.body.push_str(label); self.body.push_str(":\n"); self.current_block = label.to_string(); } fn finish(self) -> String { let mut out = String::new(); out.push_str("; AILang generated module: "); out.push_str(&self.module.name); out.push('\n'); out.push_str("source_filename = \""); out.push_str(&self.module.name); out.push_str(".ail\"\n"); out.push_str("target triple = \""); out.push_str(default_triple()); out.push_str("\"\n\n"); // Globals first. for (content, (name, _)) in &self.strings { let escaped = ll_string_literal(content); let len = c_byte_len(content); out.push_str(&format!( "@{name} = private unnamed_addr constant [{len} x i8] c\"{escaped}\", align 1\n", )); } if !self.strings.is_empty() { out.push('\n'); } out.push_str("declare i32 @printf(ptr, ...)\n"); out.push_str("declare i32 @puts(ptr)\n"); out.push_str("declare ptr @malloc(i64)\n\n"); out.push_str(&self.header); out.push_str(&self.body); out } fn emit_module(&mut self) -> Result<()> { let defs: Vec<&Def> = self.module.defs.iter().collect(); for def in defs { match def { Def::Fn(f) => { self.emit_fn(f).map_err(|e| { CodegenError::Def(f.name.clone(), Box::new(e)) })?; } Def::Const(c) => { self.emit_const(c).map_err(|e| { CodegenError::Def(c.name.clone(), Box::new(e)) })?; } Def::Type(_) => { // Keine LLVM-Definition nötig: die ADT existiert nur als // logischer Typ. Heap-Boxen werden ad-hoc per malloc // angelegt. } } } // main-Wrapper, falls vorhanden. if let Some(Def::Fn(main_fn)) = self .module .defs .iter() .find(|d| d.name() == "main") { if let Type::Fn { params, ret, .. } = &main_fn.ty { if params.is_empty() && matches!(ret.as_ref(), Type::Con { name } if name == "Unit") { self.body.push_str( "\ndefine i32 @main() {\n call i8 @ail_main()\n ret i32 0\n}\n", ); } } } Ok(()) } fn emit_const(&mut self, c: &ConstDef) -> Result<()> { let lty = llvm_type(&c.ty)?; let lit = match &c.value { Term::Lit { lit } => lit, _ => { return Err(CodegenError::Internal( "MVP: const muss Literal sein".into(), )); } }; let (val_ty, val) = match lit { Literal::Int { value } => ("i64".to_string(), value.to_string()), Literal::Bool { value } => ( "i1".to_string(), if *value { "true".into() } else { "false".into() }, ), Literal::Unit => ("i8".to_string(), "0".to_string()), Literal::Str { value } => { let g = self.intern_string("str", value); ("ptr".to_string(), format!("@{g}")) } }; if val_ty != lty { return Err(CodegenError::Internal(format!( "const type mismatch: {} vs {}", lty, val_ty ))); } self.header.push_str(&format!( "@ail_{name} = constant {ty} {val}\n", name = c.name, ty = lty, val = val, )); Ok(()) } fn emit_fn(&mut self, f: &FnDef) -> Result<()> { let (param_tys, ret_ty) = match &f.ty { Type::Fn { params, ret, .. } => (params.clone(), (**ret).clone()), other => { return Err(CodegenError::NotFnType( ailang_core::pretty::type_to_string(other), )); } }; let llvm_param_tys: Vec = param_tys.iter().map(llvm_type).collect::>()?; let llvm_ret = llvm_type(&ret_ty)?; self.locals.clear(); self.counter = 0; let mut sig = format!("define {ret} @ail_{name}(", ret = llvm_ret, name = f.name); for (i, (pname, pty)) in f.params.iter().zip(llvm_param_tys.iter()).enumerate() { if i > 0 { sig.push_str(", "); } // SSA-Argumentname: %arg_ sig.push_str(&format!("{} %arg_{}", pty, pname)); self.locals.push(( pname.clone(), format!("%arg_{}", pname), pty.clone(), )); } sig.push_str(") {\n"); self.body.push_str(&sig); self.start_block("entry"); let (val, val_ty) = self.lower_term(&f.body)?; if val_ty != llvm_ret { return Err(CodegenError::Internal(format!( "fn `{}`: body type {val_ty} != return type {llvm_ret}", f.name ))); } self.body .push_str(&format!(" ret {val_ty} {val}\n}}\n\n")); Ok(()) } /// Lowert einen Term zu (SSA-Value-String, LLVM-Typ). fn lower_term(&mut self, t: &Term) -> Result<(String, String)> { match t { Term::Lit { lit } => Ok(match lit { Literal::Int { value } => (value.to_string(), "i64".into()), Literal::Bool { value } => ( if *value { "true".into() } else { "false".into() }, "i1".into(), ), Literal::Str { value } => { // Globale Konstante anlegen; in opaque-pointer-LLVM // ist `@name` direkt ein gültiger `ptr`. let g = self.intern_string("str", value); (format!("@{g}"), "ptr".into()) } Literal::Unit => ("0".into(), "i8".into()), }), Term::Var { name } => { if let Some((_, ssa, ty)) = self.locals.iter().rev().find(|(n, _, _)| n == name) { Ok((ssa.clone(), ty.clone())) } else { Err(CodegenError::UnknownVar(name.clone())) } } Term::Let { name, value, body } => { let (val_ssa, val_ty) = self.lower_term(value)?; self.locals.push((name.clone(), val_ssa, val_ty)); let r = self.lower_term(body); self.locals.pop(); r } Term::If { cond, then, else_ } => { let (cond_v, cond_ty) = self.lower_term(cond)?; if cond_ty != "i1" { return Err(CodegenError::Internal(format!( "if cond not i1: {cond_ty}" ))); } let id = self.fresh_id(); let then_lbl = format!("then.{id}"); let else_lbl = format!("else.{id}"); let join_lbl = format!("join.{id}"); self.body.push_str(&format!( " br i1 {cond_v}, label %{then_lbl}, label %{else_lbl}\n" )); self.start_block(&then_lbl); let (then_v, then_ty) = self.lower_term(then)?; // Verschachtelter Code im `then`-Body kann das Block-Label // verändert haben — phi muss den letzten tatsächlichen Block sehen. let then_block_end = self.current_block.clone(); self.body.push_str(&format!(" br label %{join_lbl}\n")); self.start_block(&else_lbl); let (else_v, else_ty) = self.lower_term(else_)?; if then_ty != else_ty { return Err(CodegenError::Internal(format!( "if branches type mismatch: {then_ty} vs {else_ty}" ))); } let else_block_end = self.current_block.clone(); self.body.push_str(&format!(" br label %{join_lbl}\n")); self.start_block(&join_lbl); let phi = self.fresh_ssa(); self.body.push_str(&format!( " {phi} = phi {ty} [ {tv}, %{tlbl} ], [ {ev}, %{elbl} ]\n", ty = then_ty, tv = then_v, tlbl = then_block_end, ev = else_v, elbl = else_block_end, )); Ok((phi, then_ty)) } Term::App { callee, args } => { let name = match callee.as_ref() { Term::Var { name } => name.clone(), _ => { return Err(CodegenError::Internal( "MVP: callee muss Variable sein".into(), )); } }; self.lower_app(&name, args) } Term::Do { op, args } => self.lower_effect_op(op, args), Term::Ctor { type_name, ctor, args } => self.lower_ctor(type_name, ctor, args), Term::Match { scrutinee, arms } => self.lower_match(scrutinee, arms), } } /// Heap-Box-Layout: 8 Bytes Tag (i64) gefolgt von je 8 Bytes pro Feld. /// Auch i1- und i8-Felder belegen einen vollen 8-Byte-Slot — die typed /// load/store-Instruktionen schreiben/lesen nur die erforderliche Größe. fn lower_ctor( &mut self, type_name: &str, ctor_name: &str, args: &[Term], ) -> Result<(String, String)> { let cref = self .ctor_index .get(ctor_name) .cloned() .ok_or_else(|| { CodegenError::Internal(format!( "unknown ctor `{ctor_name}`" )) })?; if cref.type_name != type_name { return Err(CodegenError::Internal(format!( "ctor `{ctor_name}` belongs to `{}`, not `{type_name}`", cref.type_name ))); } if args.len() != cref.fields.len() { return Err(CodegenError::Internal(format!( "ctor `{type_name}/{ctor_name}` arity" ))); } // Argumente vorab auswerten, damit Allocation und Store nahe beieinander // bleiben. let mut compiled = Vec::new(); for (a, exp) in args.iter().zip(cref.fields.iter()) { let (v, vty) = self.lower_term(a)?; if &vty != exp { return Err(CodegenError::Internal(format!( "ctor `{ctor_name}` field type {vty} != expected {exp}" ))); } compiled.push((v, vty)); } let size_bytes = 8 + (compiled.len() * 8) as i64; let p = self.fresh_ssa(); self.body.push_str(&format!( " {p} = call ptr @malloc(i64 {size_bytes})\n" )); // Tag schreiben. self.body.push_str(&format!( " store i64 {tag}, ptr {p}, align 8\n", tag = cref.tag )); // Felder schreiben. for (i, (v, ty)) in compiled.iter().enumerate() { let off = 8 + i as i64 * 8; let addr = self.fresh_ssa(); self.body.push_str(&format!( " {addr} = getelementptr inbounds i8, ptr {p}, i64 {off}\n" )); self.body .push_str(&format!(" store {ty} {v}, ptr {addr}, align 8\n")); } Ok((p, "ptr".into())) } fn lower_match( &mut self, scrutinee: &Term, arms: &[Arm], ) -> Result<(String, String)> { let (s_val, s_ty) = self.lower_term(scrutinee)?; if s_ty != "ptr" { return Err(CodegenError::Internal(format!( "match auf nicht-ADT scrutinee (got {s_ty}); MVP unterstützt nur ADTs" ))); } // Tag laden. let tag = self.fresh_ssa(); self.body .push_str(&format!(" {tag} = load i64, ptr {s_val}, align 8\n")); // Arms separieren. let mut ctor_arms: Vec<(CtorRef, &Arm, Vec>)> = Vec::new(); let mut open_arm: Option<&Arm> = None; let mut open_var: Option = None; for arm in arms { match &arm.pat { Pattern::Wild => { open_arm = Some(arm); } Pattern::Var { name } => { open_arm = Some(arm); open_var = Some(name.clone()); } Pattern::Ctor { ctor, fields } => { let cref = self .ctor_index .get(ctor) .cloned() .ok_or_else(|| { CodegenError::Internal(format!( "unknown ctor in pattern: `{ctor}`" )) })?; let bindings: Vec> = fields .iter() .map(|p| match p { Pattern::Var { name } => Some(name.clone()), Pattern::Wild => None, _ => None, // MVP: nested ctor/lit patterns nicht supported }) .collect(); ctor_arms.push((cref, arm, bindings)); } Pattern::Lit { .. } => { return Err(CodegenError::Internal( "MVP: Lit-Patterns in Match nicht unterstützt".into(), )); } } } let id = self.fresh_id(); let join_lbl = format!("mjoin.{id}"); let default_lbl = format!("mdefault.{id}"); // switch let mut sw = format!( " switch i64 {tag}, label %{default_lbl} [\n", tag = tag ); let mut arm_labels: Vec = Vec::new(); for (i, (cref, _, _)) in ctor_arms.iter().enumerate() { let lbl = format!("marm.{id}.{i}"); sw.push_str(&format!(" i64 {}, label %{}\n", cref.tag, lbl)); arm_labels.push(lbl); } sw.push_str(" ]\n"); self.body.push_str(&sw); let mut phi_inputs: Vec<(String, String)> = Vec::new(); // (value, block) let mut result_ty: Option = None; for (i, (cref, arm, bindings)) in ctor_arms.iter().enumerate() { self.start_block(&arm_labels[i]); // Felder laden und als locals binden. let mut pushed = 0usize; for (idx, (binding, fty)) in bindings.iter().zip(cref.fields.iter()).enumerate() { if let Some(bname) = binding { let off = 8 + idx as i64 * 8; let addr = self.fresh_ssa(); self.body.push_str(&format!( " {addr} = getelementptr inbounds i8, ptr {s_val}, i64 {off}\n" )); let v = self.fresh_ssa(); self.body.push_str(&format!( " {v} = load {fty}, ptr {addr}, align 8\n" )); self.locals .push((bname.clone(), v, fty.clone())); pushed += 1; } } let (val, vty) = self.lower_term(&arm.body)?; // bindings poppen for _ in 0..pushed { self.locals.pop(); } phi_inputs.push((val, self.current_block.clone())); self.body .push_str(&format!(" br label %{join_lbl}\n")); if let Some(rt) = &result_ty { if rt != &vty { return Err(CodegenError::Internal(format!( "match arm result type {vty} != {rt}" ))); } } else { result_ty = Some(vty); } } // default-block self.start_block(&default_lbl); if let Some(arm) = open_arm { // ggf. var-binding einrichten let pushed = if let Some(name) = open_var.take() { self.locals.push((name, s_val.clone(), "ptr".into())); 1 } else { 0 }; let (val, vty) = self.lower_term(&arm.body)?; for _ in 0..pushed { self.locals.pop(); } phi_inputs.push((val, self.current_block.clone())); self.body .push_str(&format!(" br label %{join_lbl}\n")); if let Some(rt) = &result_ty { if rt != &vty { return Err(CodegenError::Internal(format!( "match default arm result type {vty} != {rt}" ))); } } else { result_ty = Some(vty); } } else { // Typchecker garantiert Exhaustiveness, also unreachable. self.body.push_str(" unreachable\n"); } // join self.start_block(&join_lbl); let phi = self.fresh_ssa(); let rt = result_ty.unwrap_or_else(|| "i64".into()); let phi_args = phi_inputs .iter() .map(|(v, b)| format!("[ {v}, %{b} ]")) .collect::>() .join(", "); self.body.push_str(&format!( " {phi} = phi {rt} {phi_args}\n" )); Ok((phi, rt)) } fn lower_app(&mut self, name: &str, args: &[Term]) -> Result<(String, String)> { // Built-in arithmetic / comparison. if let Some((instr, ret_ty)) = builtin_binop(name) { if args.len() != 2 { return Err(CodegenError::Internal(format!( "builtin `{name}` expected 2 args" ))); } let (a, _) = self.lower_term(&args[0])?; let (b, _) = self.lower_term(&args[1])?; let dst = self.fresh_ssa(); self.body.push_str(&format!( " {dst} = {instr} i64 {a}, {b}\n" )); return Ok((dst, ret_ty.into())); } if name == "not" { if args.len() != 1 { return Err(CodegenError::Internal("not arity".into())); } let (a, _) = self.lower_term(&args[0])?; let dst = self.fresh_ssa(); self.body .push_str(&format!(" {dst} = xor i1 {a}, true\n")); return Ok((dst, "i1".into())); } // User-Funktion? if let Some(sig) = self.user_fns.get(name).cloned() { let mut compiled_args = Vec::new(); for (a, exp_ty) in args.iter().zip(sig.params.iter()) { let (v, vty) = self.lower_term(a)?; if &vty != exp_ty { return Err(CodegenError::Internal(format!( "call `{name}` arg type mismatch: expected {exp_ty}, got {vty}" ))); } compiled_args.push((v, vty)); } let arglist = compiled_args .iter() .map(|(v, t)| format!("{t} {v}")) .collect::>() .join(", "); let dst = self.fresh_ssa(); self.body.push_str(&format!( " {dst} = call {ret} @ail_{name}({arglist})\n", ret = sig.ret, )); return Ok((dst, sig.ret)); } Err(CodegenError::Internal(format!( "unknown callee: `{name}`" ))) } fn lower_effect_op(&mut self, op: &str, args: &[Term]) -> Result<(String, String)> { match op { "io/print_int" => { if args.len() != 1 { return Err(CodegenError::Internal( "io/print_int arity".into(), )); } let (v, vty) = self.lower_term(&args[0])?; if vty != "i64" { return Err(CodegenError::Internal( "io/print_int needs i64".into(), )); } let fmt = self.intern_string("fmt_int", "%lld\n"); self.body.push_str(&format!( " call i32 (ptr, ...) @printf(ptr @{fmt}, i64 {v})\n" )); Ok(("0".into(), "i8".into())) } "io/print_str" => { if args.len() != 1 { return Err(CodegenError::Internal( "io/print_str arity".into(), )); } let (v, vty) = self.lower_term(&args[0])?; if vty != "ptr" { return Err(CodegenError::Internal( "io/print_str needs ptr".into(), )); } self.body .push_str(&format!(" call i32 @puts(ptr {v})\n")); Ok(("0".into(), "i8".into())) } "io/print_bool" => { if args.len() != 1 { return Err(CodegenError::Internal( "io/print_bool arity".into(), )); } let (v, vty) = self.lower_term(&args[0])?; if vty != "i1" { return Err(CodegenError::Internal( "io/print_bool needs i1".into(), )); } // Drucke "true\n" oder "false\n". let fmt_t = self.intern_string("fmt_true", "true\n"); let fmt_f = self.intern_string("fmt_false", "false\n"); let id = self.fresh_id(); let then_lbl = format!("ptbl_t.{id}"); let else_lbl = format!("ptbl_f.{id}"); let join_lbl = format!("ptbl_j.{id}"); self.body.push_str(&format!( " br i1 {v}, label %{then_lbl}, label %{else_lbl}\n" )); self.start_block(&then_lbl); self.body.push_str(&format!( " call i32 (ptr, ...) @printf(ptr @{fmt_t})\n" )); self.body.push_str(&format!(" br label %{join_lbl}\n")); self.start_block(&else_lbl); self.body.push_str(&format!( " call i32 (ptr, ...) @printf(ptr @{fmt_f})\n" )); self.body.push_str(&format!(" br label %{join_lbl}\n")); self.start_block(&join_lbl); Ok(("0".into(), "i8".into())) } other => Err(CodegenError::Internal(format!( "unknown effect op: {other}" ))), } } fn fresh_ssa(&mut self) -> String { self.counter += 1; format!("%v{}", self.counter) } fn fresh_id(&mut self) -> u64 { self.counter += 1; self.counter } fn intern_string(&mut self, hint: &str, content: &str) -> String { if let Some((name, _)) = self.strings.get(content) { return name.clone(); } let name = format!(".str_{}_{}", hint, self.str_counter); self.str_counter += 1; let len = c_byte_len(content); self.strings .insert(content.to_string(), (name.clone(), len)); name } } 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()), // Alle anderen Type-Namen werden als ADT (Boxed) behandelt. // Falls der Typchecker nicht vorher abgelehnt hat, ist das // beabsichtigt — sonst würde `ptr` einen falschen Wert maskieren. _ => Ok("ptr".into()), }, other => Err(CodegenError::UnsupportedType( ailang_core::pretty::type_to_string(other), )), } } 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.as_bytes().len() + 1 // + NUL } /// Escapt einen String für LLVM IR `c"..."`. Alle Bytes außerhalb von /// 0x20..0x7E werden als `\HH` escapt; `"` und `\` ebenfalls. Endet mit `\00`. fn default_triple() -> &'static str { // Im MVP fragen wir den Compile-Host. Für Cross-Compilation müsste man das // konfigurierbar machen — kein Bedarf jetzt. 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::*; use ailang_core::SCHEMA; #[test] fn emits_arith_fn() { let m = Module { schema: SCHEMA.into(), name: "t".into(), imports: vec![], defs: vec![Def::Fn(FnDef { name: "add".into(), ty: Type::Fn { params: vec![Type::int(), Type::int()], ret: Box::new(Type::int()), effects: vec![], }, params: vec!["a".into(), "b".into()], body: Term::App { callee: Box::new(Term::Var { name: "+".into() }), args: vec![ Term::Var { name: "a".into() }, Term::Var { name: "b".into() }, ], }, doc: None, })], }; let ir = emit_ir(&m).unwrap(); assert!(ir.contains("define i64 @ail_add(i64 %arg_a, i64 %arg_b)")); assert!(ir.contains("add i64 %arg_a, %arg_b")); } }