From 21606c93403a3b08fbf83bde4f4fb63f9791dab8 Mon Sep 17 00:00:00 2001 From: Brummel Date: Thu, 7 May 2026 10:38:07 +0200 Subject: [PATCH] Iter 3: ADTs + Pattern Matching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- crates/ail/src/main.rs | 57 ++++- crates/ail/tests/e2e.rs | 7 + crates/ailang-check/src/lib.rs | 379 ++++++++++++++++++++++++++++++- crates/ailang-codegen/src/lib.rs | 298 +++++++++++++++++++++++- crates/ailang-core/src/ast.rs | 55 +++++ crates/ailang-core/src/pretty.rs | 100 ++++++++ docs/JOURNAL.md | 49 ++++ examples/list.ail.json | 123 ++++++++++ 8 files changed, 1060 insertions(+), 8 deletions(-) create mode 100644 examples/list.ail.json diff --git a/crates/ail/src/main.rs b/crates/ail/src/main.rs index ccf9c92..e0579c1 100644 --- a/crates/ail/src/main.rs +++ b/crates/ail/src/main.rs @@ -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::>() + .join(", ") + ) + } + }) + .collect::>() + .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 { 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 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); + } + } } } diff --git a/crates/ail/tests/e2e.rs b/crates/ail/tests/e2e.rs index 11f6b0e..63e0dc0 100644 --- a/crates/ail/tests/e2e.rs +++ b/crates/ail/tests/e2e.rs @@ -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"); +} diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 2edf7d3..6ffdd0e 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -54,6 +54,35 @@ pub enum CheckError { #[error("const `{0}` may not have effects (got !{1:?})")] ConstHasEffects(String, Vec), + + #[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 }, + + #[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 = std::result::Result; @@ -69,7 +98,32 @@ pub fn check(m: &Module) -> Result { 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 { Def::Const(c) => { env.globals.insert(c.name.clone(), c.ty.clone()); } + Def::Type(_) => {} } } @@ -89,6 +144,9 @@ pub fn check(m: &Module) -> Result { 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 = BTreeSet::new(); + let mut has_open_arm = false; + let mut result_ty: Option = 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 = 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> { + 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: "".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, pub effect_ops: IndexMap, + pub types: IndexMap, + /// Inverser Index: ctor-name -> Verweis auf die zugehörige ADT. + pub ctor_index: IndexMap, +} + +#[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 { diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index d4305ea..5abd9c6 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -56,11 +56,33 @@ struct Emitter<'a> { str_counter: u64, /// Liste aller user-definierten Top-Level-Funktionen (für call-resolution). user_fns: BTreeMap, + /// 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>, + /// Inverser Index: ctor-name -> (type_name, tag, field_llvm_types). + ctor_index: BTreeMap, /// 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, // llvm types +} + +#[derive(Debug, Clone)] +struct CtorRef { + type_name: String, + tag: u32, + fields: Vec, +} + #[derive(Debug, Clone)] struct FnSig { params: Vec, // llvm types @@ -82,6 +104,35 @@ impl<'a> Emitter<'a> { } } } + + let mut types: BTreeMap> = BTreeMap::new(); + let mut ctor_index: BTreeMap = 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 = 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>)> = Vec::new(); + let mut open_arm: Option<&Arm> = None; + let mut open_var: Option = 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> = 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 = 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 = 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::>() + .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 { "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), diff --git a/crates/ailang-core/src/ast.rs b/crates/ailang-core/src/ast.rs index 18176be..8fa8273 100644 --- a/crates/ailang-core/src/ast.rs +++ b/crates/ailang-core/src/ast.rs @@ -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, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub doc: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Ctor { + pub name: String, + #[serde(default)] + pub fields: Vec, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FnDef { pub name: String, @@ -80,6 +97,44 @@ pub enum Term { op: String, args: Vec, }, + /// 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, + }, + /// Pattern matching über einen Wert. + Match { + scrutinee: Box, + arms: Vec, + }, +} + +#[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, + }, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/ailang-core/src/pretty.rs b/crates/ailang-core/src/pretty.rs index a832d19..26cf659 100644 --- a/crates/ailang-core/src/pretty.rs +++ b/crates/ailang-core/src/pretty.rs @@ -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::>() + .join(", ") + ) + } + }) + .collect::>() + .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::>() + .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::>() + .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!( diff --git a/docs/JOURNAL.md b/docs/JOURNAL.md index d28a924..a678793 100644 --- a/docs/JOURNAL.md +++ b/docs/JOURNAL.md @@ -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. diff --git a/examples/list.ail.json b/examples/list.ail.json new file mode 100644 index 0000000..0966a88 --- /dev/null +++ b/examples/list.ail.json @@ -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" }] + } + ] + } + } + } + ] +}