Files
AILang/crates/ailang-codegen/src/lib.rs
T
Brummel 21606c9340 Iter 3: ADTs + Pattern Matching
- AST: Def::Type mit Ctors; Term::Ctor (Konstruktion) und Term::Match
  mit Arm/Pattern. Patterns: Wild, Var, Lit, Ctor { ctor, fields } —
  Sub-Patterns im MVP auf Var/Wild beschränkt.
- Typchecker: Type-Registry, ctor_index für O(1)-Resolution, Pattern-
  Bindings, Exhaustiveness-Check gegen volle Konstruktormenge plus
  Negativ-Tests.
- Codegen: Boxed-Heap-Layout via malloc; Tag in Offset 0, Felder ab
  Offset 8 in 8-Byte-Slots. Match lowert zu load tag + switch + Phi
  am Join. Default-Block ist unreachable, wenn vom Typchecker geprüft.
- examples/list.ail.json: rekursive Int-Liste mit sum_list via match.
  E2E-Test + Exhaustiveness-Tests. 19/19 Tests grün.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 10:38:07 +02:00

897 lines
32 KiB
Rust

//! 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<CodegenError>),
#[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<T> = std::result::Result<T, CodegenError>;
pub fn emit_ir(m: &Module) -> Result<String> {
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<String, (String, usize)>,
/// 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<String, FnSig>,
/// 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<String, Vec<CtorInfo>>,
/// Inverser Index: ctor-name -> (type_name, tag, field_llvm_types).
ctor_index: BTreeMap<String, CtorRef>,
/// 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<String>, // llvm types
}
#[derive(Debug, Clone)]
struct CtorRef {
type_name: String,
tag: u32,
fields: Vec<String>,
}
#[derive(Debug, Clone)]
struct FnSig {
params: Vec<String>, // 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<Vec<String>> =
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<String, Vec<CtorInfo>> = BTreeMap::new();
let mut ctor_index: BTreeMap<String, CtorRef> = 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<String> = 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<String> =
param_tys.iter().map(llvm_type).collect::<Result<_>>()?;
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_<name>
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<Option<String>>)> = Vec::new();
let mut open_arm: Option<&Arm> = None;
let mut open_var: Option<String> = 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<Option<String>> = 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<String> = 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<String> = 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::<Vec<_>>()
.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::<Vec<_>>()
.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<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()),
// 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"));
}
}