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
+52 -5
View File
@@ -93,6 +93,29 @@ fn main() -> Result<()> {
ailang_core::pretty::type_to_string(&c.ty),
vec![],
),
ailang_core::Def::Type(t) => {
let s = t
.ctors
.iter()
.map(|c| {
if c.fields.is_empty() {
c.name.clone()
} else {
format!(
"{}({})",
c.name,
c.fields
.iter()
.map(ailang_core::pretty::type_to_string)
.collect::<Vec<_>>()
.join(", ")
)
}
})
.collect::<Vec<_>>()
.join(" | ");
("type", s, vec![])
}
};
serde_json::json!({
"name": d.name(),
@@ -233,11 +256,20 @@ fn main() -> Result<()> {
fn collect_refs(def: &ailang_core::Def) -> std::collections::BTreeSet<String> {
let mut out = std::collections::BTreeSet::new();
let body = match def {
ailang_core::Def::Fn(f) => &f.body,
ailang_core::Def::Const(c) => &c.value,
};
walk_term(body, &mut out);
match def {
ailang_core::Def::Fn(f) => walk_term(&f.body, &mut out),
ailang_core::Def::Const(c) => walk_term(&c.value, &mut out),
ailang_core::Def::Type(td) => {
// Eine Typedef referenziert die Typen ihrer Felder.
for c in &td.ctors {
for ft in &c.fields {
if let ailang_core::Type::Con { name } = ft {
out.insert(format!("type:{name}"));
}
}
}
}
}
out
}
@@ -271,5 +303,20 @@ fn walk_term(t: &ailang_core::Term, out: &mut std::collections::BTreeSet<String>
walk_term(a, out);
}
}
Term::Ctor { type_name, ctor, args } => {
out.insert(format!("ctor:{type_name}/{ctor}"));
for a in args {
walk_term(a, out);
}
}
Term::Match { scrutinee, arms } => {
walk_term(scrutinee, out);
for arm in arms {
if let ailang_core::ast::Pattern::Ctor { ctor, .. } = &arm.pat {
out.insert(format!("ctor:{ctor}"));
}
walk_term(&arm.body, out);
}
}
}
}
+7
View File
@@ -56,3 +56,10 @@ fn hello_world_str_lit() {
let stdout = build_and_run("hello.ail.json");
assert_eq!(stdout.trim(), "Hallo, AILang.");
}
/// Schützt ADT-Codegen + Match: rekursive Liste, sum_list via match auf Cons/Nil.
#[test]
fn list_sum_via_match() {
let stdout = build_and_run("list.ail.json");
assert_eq!(stdout.trim(), "42");
}
+378 -1
View File
@@ -54,6 +54,35 @@ pub enum CheckError {
#[error("const `{0}` may not have effects (got !{1:?})")]
ConstHasEffects(String, Vec<String>),
#[error("unknown type: `{0}`")]
UnknownType(String),
#[error("type `{ty}` has no constructor `{ctor}`")]
UnknownCtor { ty: String, ctor: String },
#[error("constructor `{ty}/{ctor}` arity: expected {expected} fields, got {got}")]
CtorArity {
ty: String,
ctor: String,
expected: usize,
got: usize,
},
#[error("non-exhaustive match on `{ty}`: missing cases {missing:?}")]
NonExhaustive { ty: String, missing: Vec<String> },
#[error("primitive type `{0}` requires a wildcard or variable arm in match")]
PrimitiveNeedsWildcard(String),
#[error("cannot match constructor pattern `{ctor}` against type `{ty}`")]
PatternTypeMismatch { ctor: String, ty: String },
#[error("duplicate type definition: `{0}`")]
DuplicateType(String),
#[error("duplicate constructor: `{ctor}` (in types `{a}` and `{b}`)")]
DuplicateCtor { ctor: String, a: String, b: String },
}
type Result<T> = std::result::Result<T, CheckError>;
@@ -69,7 +98,32 @@ pub fn check(m: &Module) -> Result<CheckedModule> {
let mut env = Env::new();
builtins::install(&mut env);
// Pass 1: alle Top-Level-Symbole registrieren (für Vorwärtsreferenzen).
// Pass 1a: alle Type-Defs registrieren.
for def in &m.defs {
if let Def::Type(td) = def {
if env.types.contains_key(&td.name) {
return Err(CheckError::DuplicateType(td.name.clone()));
}
for c in &td.ctors {
if let Some(prev) = env.ctor_index.get(&c.name) {
return Err(CheckError::DuplicateCtor {
ctor: c.name.clone(),
a: prev.type_name.clone(),
b: td.name.clone(),
});
}
env.ctor_index.insert(
c.name.clone(),
CtorRef {
type_name: td.name.clone(),
},
);
}
env.types.insert(td.name.clone(), td.clone());
}
}
// Pass 1b: alle Top-Level-Werte-Symbole registrieren.
for def in &m.defs {
match def {
Def::Fn(f) => {
@@ -78,6 +132,7 @@ pub fn check(m: &Module) -> Result<CheckedModule> {
Def::Const(c) => {
env.globals.insert(c.name.clone(), c.ty.clone());
}
Def::Type(_) => {}
}
}
@@ -89,6 +144,9 @@ pub fn check(m: &Module) -> Result<CheckedModule> {
let ty = match def {
Def::Fn(f) => f.ty.clone(),
Def::Const(c) => c.ty.clone(),
Def::Type(_) => Type::Con {
name: def.name().to_string(),
},
};
symbols.insert(def.name().to_string(), (ty, h));
}
@@ -100,6 +158,44 @@ fn check_def(def: &Def, env: &Env) -> Result<()> {
match def {
Def::Fn(f) => check_fn(f, env),
Def::Const(c) => check_const(c, env),
Def::Type(td) => check_type_def(td, env),
}
}
fn check_type_def(td: &TypeDef, env: &Env) -> Result<()> {
// Felder müssen alle bekannte Typen referenzieren (oder andere ADTs aus
// diesem Modul; rekursiv ist erlaubt).
for c in &td.ctors {
for f in &c.fields {
check_type_well_formed(f, env)?;
}
}
Ok(())
}
fn check_type_well_formed(t: &Type, env: &Env) -> Result<()> {
match t {
Type::Con { name } => {
if matches!(name.as_str(), "Int" | "Bool" | "Unit" | "Str") {
Ok(())
} else if env.types.contains_key(name) {
Ok(())
} else {
Err(CheckError::UnknownType(name.clone()))
}
}
Type::Fn { params, ret, .. } => {
for p in params {
check_type_well_formed(p, env)?;
}
check_type_well_formed(ret, env)
}
Type::Var { .. } | Type::Forall { .. } => {
// Im MVP keine Polymorphie auf Typebene innerhalb von ADT-Feldern.
Err(CheckError::PolymorphicNotSupported(
"type def".into(),
))
}
}
}
@@ -252,6 +348,179 @@ fn synth(
effects.insert(sig.effect.clone());
Ok(sig.ret)
}
Term::Ctor { type_name, ctor, args } => {
let td = env
.types
.get(type_name)
.ok_or_else(|| CheckError::UnknownType(type_name.clone()))?
.clone();
let cdef = td
.ctors
.iter()
.find(|c| &c.name == ctor)
.ok_or_else(|| CheckError::UnknownCtor {
ty: type_name.clone(),
ctor: ctor.clone(),
})?
.clone();
if args.len() != cdef.fields.len() {
return Err(CheckError::CtorArity {
ty: type_name.clone(),
ctor: ctor.clone(),
expected: cdef.fields.len(),
got: args.len(),
});
}
for (a, exp) in args.iter().zip(cdef.fields.iter()) {
let actual = synth(a, env, locals, effects, in_def)?;
expect_eq(exp, &actual)?;
}
Ok(Type::Con {
name: type_name.clone(),
})
}
Term::Match { scrutinee, arms } => {
let s_ty = synth(scrutinee, env, locals, effects, in_def)?;
if arms.is_empty() {
return Err(CheckError::NonExhaustive {
ty: ailang_core::pretty::type_to_string(&s_ty),
missing: vec!["(no arms)".into()],
});
}
let mut covered_ctors: BTreeSet<String> = BTreeSet::new();
let mut has_open_arm = false;
let mut result_ty: Option<Type> = None;
for arm in arms {
// Lokale Bindings sammeln und ins env pushen, body checken,
// wieder poppen — manuell, weil Patterns mehrere Bindings
// erzeugen können.
let bindings = type_check_pattern(&arm.pat, &s_ty, env)?;
let mut pushed = Vec::new();
for (n, t) in &bindings {
let prev = locals.insert(n.clone(), t.clone());
pushed.push((n.clone(), prev));
}
let body_ty = synth(&arm.body, env, locals, effects, in_def)?;
// Bindings rückgängig.
for (n, prev) in pushed.into_iter().rev() {
match prev {
Some(p) => {
locals.insert(n, p);
}
None => {
locals.shift_remove(&n);
}
}
}
if let Some(rt) = &result_ty {
expect_eq(rt, &body_ty)?;
} else {
result_ty = Some(body_ty);
}
match &arm.pat {
Pattern::Wild | Pattern::Var { .. } => {
has_open_arm = true;
}
Pattern::Ctor { ctor, .. } => {
covered_ctors.insert(ctor.clone());
}
Pattern::Lit { .. } => {
// Lit-Patterns decken nichts strukturell ab.
}
}
}
// Exhaustiveness.
if !has_open_arm {
match &s_ty {
Type::Con { name } if env.types.contains_key(name) => {
let td = &env.types[name];
let missing: Vec<String> = td
.ctors
.iter()
.filter(|c| !covered_ctors.contains(&c.name))
.map(|c| c.name.clone())
.collect();
if !missing.is_empty() {
return Err(CheckError::NonExhaustive {
ty: name.clone(),
missing,
});
}
}
_ => {
return Err(CheckError::PrimitiveNeedsWildcard(
ailang_core::pretty::type_to_string(&s_ty),
));
}
}
}
Ok(result_ty.expect("checked arms is non-empty"))
}
}
}
/// Prüft ein Pattern gegen einen Erwartungstyp und gibt die durch das
/// Pattern eingeführten Bindings zurück.
fn type_check_pattern(
p: &Pattern,
expected: &Type,
env: &Env,
) -> Result<Vec<(String, Type)>> {
match p {
Pattern::Wild => Ok(vec![]),
Pattern::Var { name } => Ok(vec![(name.clone(), expected.clone())]),
Pattern::Lit { lit } => {
let lt = match lit {
Literal::Int { .. } => Type::int(),
Literal::Bool { .. } => Type::bool_(),
Literal::Str { .. } => Type::str_(),
Literal::Unit => Type::unit(),
};
expect_eq(expected, &lt)?;
Ok(vec![])
}
Pattern::Ctor { ctor, fields } => {
let cref = env.ctor_index.get(ctor).ok_or_else(|| {
CheckError::UnknownCtor {
ty: "<unknown>".into(),
ctor: ctor.clone(),
}
})?;
// expected muss diese ADT sein.
match expected {
Type::Con { name } if name == &cref.type_name => {}
_ => {
return Err(CheckError::PatternTypeMismatch {
ctor: ctor.clone(),
ty: ailang_core::pretty::type_to_string(expected),
});
}
}
let td = &env.types[&cref.type_name];
let cdef = td
.ctors
.iter()
.find(|c| &c.name == ctor)
.expect("indexed ctor exists");
if fields.len() != cdef.fields.len() {
return Err(CheckError::CtorArity {
ty: cref.type_name.clone(),
ctor: ctor.clone(),
expected: cdef.fields.len(),
got: fields.len(),
});
}
let mut out = Vec::new();
for (sub, sub_ty) in fields.iter().zip(cdef.fields.iter()) {
out.extend(type_check_pattern(sub, sub_ty, env)?);
}
Ok(out)
}
}
}
@@ -277,6 +546,14 @@ fn expect_eq(expected: &Type, got: &Type) -> Result<()> {
pub struct Env {
pub globals: IndexMap<String, Type>,
pub effect_ops: IndexMap<String, builtins::EffectOpSig>,
pub types: IndexMap<String, TypeDef>,
/// Inverser Index: ctor-name -> Verweis auf die zugehörige ADT.
pub ctor_index: IndexMap<String, CtorRef>,
}
#[derive(Debug, Clone)]
pub struct CtorRef {
pub type_name: String,
}
impl Env {
@@ -403,6 +680,106 @@ mod tests {
check(&m).expect("should typecheck");
}
#[test]
fn match_must_be_exhaustive() {
// Type Maybe = None | Some(Int); fn f matches nur None -> Fehler.
let m = Module {
schema: SCHEMA.into(),
name: "t".into(),
imports: vec![],
defs: vec![
Def::Type(TypeDef {
name: "Maybe".into(),
ctors: vec![
Ctor { name: "None".into(), fields: vec![] },
Ctor {
name: "Some".into(),
fields: vec![Type::int()],
},
],
doc: None,
}),
fn_def(
"f",
Type::Fn {
params: vec![Type::Con { name: "Maybe".into() }],
ret: Box::new(Type::int()),
effects: vec![],
},
vec!["m"],
Term::Match {
scrutinee: Box::new(Term::Var { name: "m".into() }),
arms: vec![Arm {
pat: Pattern::Ctor {
ctor: "None".into(),
fields: vec![],
},
body: Term::Lit {
lit: Literal::Int { value: 0 },
},
}],
},
),
],
};
let err = check(&m).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("non-exhaustive"), "got: {msg}");
assert!(msg.contains("Some"), "got: {msg}");
}
#[test]
fn match_with_wildcard_is_exhaustive() {
let m = Module {
schema: SCHEMA.into(),
name: "t".into(),
imports: vec![],
defs: vec![
Def::Type(TypeDef {
name: "Maybe".into(),
ctors: vec![
Ctor { name: "None".into(), fields: vec![] },
Ctor {
name: "Some".into(),
fields: vec![Type::int()],
},
],
doc: None,
}),
fn_def(
"f",
Type::Fn {
params: vec![Type::Con { name: "Maybe".into() }],
ret: Box::new(Type::int()),
effects: vec![],
},
vec!["m"],
Term::Match {
scrutinee: Box::new(Term::Var { name: "m".into() }),
arms: vec![
Arm {
pat: Pattern::Ctor {
ctor: "None".into(),
fields: vec![],
},
body: Term::Lit {
lit: Literal::Int { value: 0 },
},
},
Arm {
pat: Pattern::Wild,
body: Term::Lit {
lit: Literal::Int { value: 1 },
},
},
],
},
),
],
};
check(&m).expect("wildcard must satisfy exhaustiveness");
}
#[test]
fn if_branches_must_match() {
let m = Module {
+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),
+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!(
+49
View File
@@ -67,3 +67,52 @@ wird, was eine andere Form ist als der AST.
**Architektur-Check:** Keine strukturellen Abweichungen. Codegen liest noch
direkt das Quell-AST (TIR-Stufe wird mit ADTs in Iteration 3 nötig).
## 2026-05-07 — Iteration 3 fertig: ADTs
- TypeDef im AST mit Ctors. Ein Ctor hat `name` und `fields: [Type...]`.
- Term::Ctor (Konstruktion) und Term::Match (Pattern Matching).
- Patterns: `Wild`, `Var`, `Lit`, `Ctor { ctor, fields }`. Im MVP sind nested
Ctor-Patterns NICHT erlaubt — Sub-Patterns müssen `Var` oder `Wild` sein.
- Typchecker mit Type-Registry und `ctor_index` (ctor-name → ADT). Im Match
wird Exhaustiveness gegen die volle Konstruktormenge geprüft. Negativ-Test
schützt das.
- Codegen: Boxed-Heap-Layout. Pro Ctor-Anwendung `malloc(8 + 8*n)` Bytes;
Tag in offset 0, Felder ab offset 8 (8-Byte-Slots, native typed
load/store). Match: load tag + switch + arm-blocks + phi am Join.
- `examples/list.ail.json` (Cons/Nil-Liste, sum_list über match) liefert 42.
**Erstaunlich problemlos.** Die Architekturentscheidungen aus Tag 0 haben
sich ausgezahlt: opaque ptr in LLVM 22 macht Boxed-Layout fast ohne
Glue-Code möglich; das Effekt-Tracking blieb von ADTs unberührt; der
JSON-AST nimmt neue Knoten-Typen sauber auf.
**Gewachsene Schulden:**
1. **Codegen liest immer noch direkt den Quell-AST.** Die Versuchung war
stark, ohne TIR weiterzumachen — und hat funktioniert, weil meine
Match-Restriktionen flach sind (keine nested Patterns). Sobald nested
Patterns kommen, braucht es ein Decision-Tree-Lowering, das ohne TIR
nicht sauber wird. Schulden anerkannt; nicht jetzt fällig.
2. **Kein GC.** Heap leakt. Akzeptabel für Demo-Programme; muss vor jedem
längerläufigen Programm angegangen werden. Optionen für Phase 4:
Refcount, Boehm-GC-Linkage, Region-Inference.
3. **Pretty-Printer für ADT-Werte zur Laufzeit fehlt.** `io/print_int`
reicht für Demos, aber ein generisches `show :: a -> Str` für ADTs
wäre wertvoll. Erfordert dispatch über tag — machbar, aber nicht jetzt.
**Plan Iteration 4:**
Die nächsten Schritte sind weniger eindeutig. Drei Kandidaten in
Prioritätsreihenfolge:
1. **Modulsystem (Imports).** Aktuell ist alles in einem Modul. Mit
mehreren Modulen + Cross-Module-Hashing wird die Sprache erst
praktikabel für mehrere Defs.
2. **Strukturierte Fehlerausgabe (`ail check --json`).** Damit Tools auf
Typfehler reagieren können, ohne Text zu parsen.
3. **Closures / höherwertige Funktionen.** Erfordert Closure-Konvertierung
und ist ein größerer Schritt.
Iteration 4 wird (1) + (2) — beides macht das LLM-Tooling stärker und
hat moderates Risiko.
+123
View File
@@ -0,0 +1,123 @@
{
"schema": "ailang/v0",
"name": "list",
"imports": [],
"defs": [
{
"kind": "type",
"name": "IntList",
"doc": "Einfach verkettete Int-Liste, boxed.",
"ctors": [
{ "name": "Nil", "fields": [] },
{
"name": "Cons",
"fields": [
{ "k": "con", "name": "Int" },
{ "k": "con", "name": "IntList" }
]
}
]
},
{
"kind": "fn",
"name": "sum_list",
"type": {
"k": "fn",
"params": [{ "k": "con", "name": "IntList" }],
"ret": { "k": "con", "name": "Int" },
"effects": []
},
"params": ["xs"],
"doc": "Summiert die Elemente einer Int-Liste rekursiv.",
"body": {
"t": "match",
"scrutinee": { "t": "var", "name": "xs" },
"arms": [
{
"pat": { "p": "ctor", "ctor": "Nil", "fields": [] },
"body": { "t": "lit", "lit": { "kind": "int", "value": 0 } }
},
{
"pat": {
"p": "ctor",
"ctor": "Cons",
"fields": [
{ "p": "var", "name": "h" },
{ "p": "var", "name": "t" }
]
},
"body": {
"t": "app",
"fn": { "t": "var", "name": "+" },
"args": [
{ "t": "var", "name": "h" },
{
"t": "app",
"fn": { "t": "var", "name": "sum_list" },
"args": [{ "t": "var", "name": "t" }]
}
]
}
}
]
}
},
{
"kind": "fn",
"name": "main",
"type": {
"k": "fn",
"params": [],
"ret": { "k": "con", "name": "Unit" },
"effects": ["IO"]
},
"params": [],
"doc": "Baut [10, 20, 12] und druckt die Summe (42).",
"body": {
"t": "let",
"name": "xs",
"value": {
"t": "ctor",
"type": "IntList",
"ctor": "Cons",
"args": [
{ "t": "lit", "lit": { "kind": "int", "value": 10 } },
{
"t": "ctor",
"type": "IntList",
"ctor": "Cons",
"args": [
{ "t": "lit", "lit": { "kind": "int", "value": 20 } },
{
"t": "ctor",
"type": "IntList",
"ctor": "Cons",
"args": [
{ "t": "lit", "lit": { "kind": "int", "value": 12 } },
{
"t": "ctor",
"type": "IntList",
"ctor": "Nil",
"args": []
}
]
}
]
}
]
},
"body": {
"t": "do",
"op": "io/print_int",
"args": [
{
"t": "app",
"fn": { "t": "var", "name": "sum_list" },
"args": [{ "t": "var", "name": "xs" }]
}
]
}
}
}
]
}