21606c9340
- 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>
897 lines
32 KiB
Rust
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"));
|
|
}
|
|
}
|