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
+55
View File
@@ -23,6 +23,7 @@ pub struct Import {
pub enum Def {
Fn(FnDef),
Const(ConstDef),
Type(TypeDef),
}
impl Def {
@@ -30,10 +31,26 @@ impl Def {
match self {
Def::Fn(f) => &f.name,
Def::Const(c) => &c.name,
Def::Type(t) => &t.name,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TypeDef {
pub name: String,
pub ctors: Vec<Ctor>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub doc: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ctor {
pub name: String,
#[serde(default)]
pub fields: Vec<Type>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FnDef {
pub name: String,
@@ -80,6 +97,44 @@ pub enum Term {
op: String,
args: Vec<Term>,
},
/// Konstruktor-Anwendung. `type_name` bindet die ADT an, `ctor` den Variant.
/// Beispiel: `Some(42)` -> `{ "t": "ctor", "type": "Option", "ctor": "Some",
/// "args": [{"t":"lit","lit":{"kind":"int","value":42}}] }`.
Ctor {
#[serde(rename = "type")]
type_name: String,
ctor: String,
#[serde(default)]
args: Vec<Term>,
},
/// Pattern matching über einen Wert.
Match {
scrutinee: Box<Term>,
arms: Vec<Arm>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Arm {
pub pat: Pattern,
pub body: Term,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "p", rename_all = "lowercase")]
pub enum Pattern {
/// `_` — bindet nichts, matcht alles.
Wild,
/// `x` — bindet den Wert an einen Namen.
Var { name: String },
/// Match auf ein Literal.
Lit { lit: Literal },
/// Match auf einen Konstruktor mit Sub-Patterns für seine Felder.
Ctor {
ctor: String,
#[serde(default)]
fields: Vec<Pattern>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
+100
View File
@@ -40,6 +40,29 @@ pub fn manifest(m: &Module) -> String {
let (kw, ty) = match def {
Def::Fn(f) => ("fn", type_to_string(&f.ty)),
Def::Const(c) => ("const", type_to_string(&c.ty)),
Def::Type(t) => {
let ctors = t
.ctors
.iter()
.map(|c| {
if c.fields.is_empty() {
c.name.clone()
} else {
format!(
"{}({})",
c.name,
c.fields
.iter()
.map(type_to_string)
.collect::<Vec<_>>()
.join(", ")
)
}
})
.collect::<Vec<_>>()
.join(" | ");
("type", ctors)
}
};
writeln!(
s,
@@ -86,6 +109,26 @@ fn def_block(def: &Def, indent: usize) -> String {
s.push(')');
s
}
Def::Type(t) => {
let mut s = format!("{pad}(type {name}\n", pad = pad, name = t.name);
let inner = " ".repeat(indent + 2);
for ctor in &t.ctors {
if ctor.fields.is_empty() {
s.push_str(&format!("{inner}(| {})\n", ctor.name));
} else {
let fs = ctor
.fields
.iter()
.map(type_to_string)
.collect::<Vec<_>>()
.join(" ");
s.push_str(&format!("{inner}(| {name} {fs})\n", name = ctor.name));
}
}
s.push_str(&pad);
s.push(')');
s
}
}
}
@@ -131,6 +174,50 @@ fn term_block(t: &Term, indent: usize) -> String {
s.push(')');
s
}
Term::Ctor { type_name, ctor, args } => {
let mut s = format!("{pad}({type_name}/{ctor}");
for a in args {
s.push(' ');
s.push_str(&term_inline(a));
}
s.push(')');
s
}
Term::Match { scrutinee, arms } => {
let mut s = format!("{pad}(match {}\n", term_inline(scrutinee));
let inner = " ".repeat(indent + 2);
for arm in arms {
s.push_str(&format!(
"{inner}(case {} ->\n",
pattern_to_string(&arm.pat)
));
s.push_str(&term_block(&arm.body, indent + 4));
s.push_str(")\n");
}
s.push_str(&pad);
s.push(')');
s
}
}
}
pub fn pattern_to_string(p: &Pattern) -> String {
match p {
Pattern::Wild => "_".into(),
Pattern::Var { name } => name.clone(),
Pattern::Lit { lit } => lit_to_string(lit),
Pattern::Ctor { ctor, fields } => {
if fields.is_empty() {
ctor.clone()
} else {
let fs = fields
.iter()
.map(pattern_to_string)
.collect::<Vec<_>>()
.join(" ");
format!("({ctor} {fs})")
}
}
}
}
@@ -157,6 +244,19 @@ fn term_inline(t: &Term) -> String {
s.push(')');
s
}
Term::Ctor { type_name, ctor, args } => {
let mut s = format!("({type_name}/{ctor}");
for a in args {
s.push(' ');
s.push_str(&term_inline(a));
}
s.push(')');
s
}
Term::Match { scrutinee, .. } => {
// Match nicht inline darstellen — mit Marker.
format!("(match {} ...)", term_inline(scrutinee))
}
// Strukturelle Terms in Inline-Form rekursiv schwer; fallback:
Term::Let { name, value, body } => {
format!(