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>
This commit is contained in:
2026-05-07 10:38:07 +02:00
parent 6e6b6a14fb
commit 21606c9340
8 changed files with 1060 additions and 8 deletions
+296 -2
View File
@@ -56,11 +56,33 @@ struct Emitter<'a> {
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
@@ -82,6 +104,35 @@ impl<'a> Emitter<'a> {
}
}
}
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(),
@@ -91,6 +142,8 @@ impl<'a> Emitter<'a> {
counter: 0,
str_counter: 0,
user_fns,
types,
ctor_index,
current_block: String::new(),
}
}
@@ -126,7 +179,8 @@ impl<'a> Emitter<'a> {
}
out.push_str("declare i32 @printf(ptr, ...)\n");
out.push_str("declare i32 @puts(ptr)\n\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
@@ -146,6 +200,11 @@ impl<'a> Emitter<'a> {
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.
}
}
}
@@ -339,9 +398,241 @@ impl<'a> Emitter<'a> {
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) {
@@ -505,7 +796,10 @@ fn llvm_type(t: &Type) -> Result<String> {
"Bool" => Ok("i1".into()),
"Unit" => Ok("i8".into()),
"Str" => Ok("ptr".into()),
other => Err(CodegenError::UnsupportedType(other.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),