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:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user