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
+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 {