MVP: AILang-Sprache mit JSON-AST, Typchecker, LLVM-IR-Backend

Erste lauffähige Iteration. examples/sum.ail.json wird zu nativem Binary
kompiliert und druckt 55 (Summe 1..10) als End-to-End-Test.

Architektur:
- ailang-core: hashbares JSON-AST + canonical-form + pretty-printer
- ailang-check: monomorpher HM-Subset + Effekt-Set-Tracking
- ailang-codegen: LLVM-IR-Text-Emitter (kein libllvm-link)
- ail: CLI mit check/manifest/render/describe/emit-ir/build/builtins

Designentscheidungen sind in docs/DESIGN.md dokumentiert; der Verlauf
in docs/JOURNAL.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 10:18:32 +02:00
commit 2fbcdba0b1
21 changed files with 2633 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "ailang-codegen"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
ailang-core.workspace = true
ailang-check.workspace = true
thiserror.workspace = true
indexmap.workspace = true
+581
View File
@@ -0,0 +1,581 @@
//! 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>,
}
#[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 });
}
}
}
}
Self {
module,
header: String::new(),
body: String::new(),
strings: BTreeMap::new(),
locals: Vec::new(),
counter: 0,
str_counter: 0,
user_fns,
}
}
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\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))
})?;
}
}
}
// 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 (val_ty, val) = match &c.value {
Term::Lit { lit } => 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()),
},
_ => {
return Err(CodegenError::Internal(
"MVP: const muss Literal sein".into(),
));
}
};
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.body.push_str("entry:\n");
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::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.body.push_str(&format!("{then_lbl}:\n"));
let (then_v, then_ty) = self.lower_term(then)?;
let then_block_end = self.current_block_label_for_phi(&then_lbl);
self.body
.push_str(&format!(" br label %{join_lbl}\n"));
self.body.push_str(&format!("{else_lbl}:\n"));
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_label_for_phi(&else_lbl);
self.body
.push_str(&format!(" br label %{join_lbl}\n"));
self.body.push_str(&format!("{join_lbl}:\n"));
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),
}
}
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_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.body.push_str(&format!("{then_lbl}:\n"));
self.body.push_str(&format!(
" call i32 (ptr, ...) @printf(ptr @{fmt_t})\n"
));
self.body.push_str(&format!(" br label %{join_lbl}\n"));
self.body.push_str(&format!("{else_lbl}:\n"));
self.body.push_str(&format!(
" call i32 (ptr, ...) @printf(ptr @{fmt_f})\n"
));
self.body.push_str(&format!(" br label %{join_lbl}\n"));
self.body.push_str(&format!("{join_lbl}:\n"));
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
}
/// Im MVP haben wir keinen verschachtelten Control-Flow innerhalb von
/// `then`/`else` von `if`, also entspricht das End-Label dem Block-Anfang.
/// Das ändert sich, sobald `if` rekursiv andere `if`s enthält — dann muss
/// das tatsächlich aktuelle Label am Phi-Punkt verwendet werden.
fn current_block_label_for_phi(&self, fallback: &str) -> String {
// Heuristik: scanne self.body rückwärts nach dem letzten Label-Header.
// Das ist robuster als anzunehmen, dass der ursprüngliche Block-Header
// noch der aktuelle ist.
for line in self.body.lines().rev() {
let line = line.trim_end();
if let Some(s) = line.strip_suffix(':') {
if !s.starts_with(' ') && !s.is_empty() && !s.contains(' ') {
return s.to_string();
}
}
}
fallback.to_string()
}
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()),
other => Err(CodegenError::UnsupportedType(other.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"));
}
}