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