diff --git a/crates/ail/src/main.rs b/crates/ail/src/main.rs index 6f862af..ccf9c92 100644 --- a/crates/ail/src/main.rs +++ b/crates/ail/src/main.rs @@ -18,7 +18,11 @@ struct Cli { #[derive(Subcommand)] enum Cmd { /// Lädt ein Modul und gibt eine kompakte Symboltabelle aus. - Manifest { path: PathBuf }, + Manifest { + path: PathBuf, + #[arg(long)] + json: bool, + }, /// Gibt das Modul in Textform aus (Pretty-Printer). Render { path: PathBuf }, /// Gibt eine einzelne Definition als JSON oder Pretty-Text aus. @@ -28,6 +32,15 @@ enum Cmd { #[arg(long)] json: bool, }, + /// Listet, welche Symbole jede Definition aufruft (statisch). + Deps { + path: PathBuf, + /// Nur für ein Symbol; ohne Argument: für alle. + #[arg(long)] + of: Option, + #[arg(long)] + json: bool, + }, /// Typprüft ein Modul. Check { path: PathBuf }, /// Schreibt LLVM IR (.ll) für das Modul. @@ -46,15 +59,59 @@ enum Cmd { opt: String, }, /// Listet eingebaute Operationen mit ihren Signaturen. - Builtins, + Builtins { + #[arg(long)] + json: bool, + }, } fn main() -> Result<()> { let cli = Cli::parse(); match cli.cmd { - Cmd::Manifest { path } => { + Cmd::Manifest { path, json } => { let m = ailang_core::load_module(&path)?; - print!("{}", ailang_core::pretty::manifest(&m)); + if json { + let entries: Vec<_> = m + .defs + .iter() + .map(|d| { + let h = ailang_core::def_hash(d); + let (kind, ty, effects) = match d { + ailang_core::Def::Fn(f) => { + let effects = match &f.ty { + ailang_core::Type::Fn { effects, .. } => effects.clone(), + _ => vec![], + }; + ( + "fn", + ailang_core::pretty::type_to_string(&f.ty), + effects, + ) + } + ailang_core::Def::Const(c) => ( + "const", + ailang_core::pretty::type_to_string(&c.ty), + vec![], + ), + }; + serde_json::json!({ + "name": d.name(), + "kind": kind, + "type": ty, + "effects": effects, + "hash": h, + }) + }) + .collect(); + let out = serde_json::json!({ + "module": m.name, + "schema": m.schema, + "symbols": entries, + }); + println!("{}", serde_json::to_string_pretty(&out)?); + } else { + print!("{}", ailang_core::pretty::manifest(&m)); + } } Cmd::Render { path } => { let m = ailang_core::load_module(&path)?; @@ -127,11 +184,92 @@ fn main() -> Result<()> { } eprintln!("built {}", out_bin.display()); } - Cmd::Builtins => { - for (n, sig) in ailang_check::builtins::list() { - println!("{n:<16} {sig}"); + Cmd::Builtins { json } => { + let list = ailang_check::builtins::list(); + if json { + let arr: Vec<_> = list + .iter() + .map(|(n, s)| serde_json::json!({ "name": n, "sig": s })) + .collect(); + println!("{}", serde_json::to_string_pretty(&arr)?); + } else { + for (n, sig) in list { + println!("{n:<16} {sig}"); + } + } + } + Cmd::Deps { path, of, json } => { + let m = ailang_core::load_module(&path)?; + let mut entries = Vec::new(); + for d in &m.defs { + if let Some(filter) = &of { + if d.name() != filter { + continue; + } + } + let mut refs: Vec = collect_refs(d).into_iter().collect(); + refs.sort(); + entries.push((d.name().to_string(), refs)); + } + if json { + let arr: Vec<_> = entries + .iter() + .map(|(n, r)| serde_json::json!({ "name": n, "refs": r })) + .collect(); + println!("{}", serde_json::to_string_pretty(&arr)?); + } else { + for (n, refs) in entries { + if refs.is_empty() { + println!("{n:>20} -"); + } else { + println!("{n:>20} -> {}", refs.join(", ")); + } + } } } } Ok(()) } + +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); + out +} + +fn walk_term(t: &ailang_core::Term, out: &mut std::collections::BTreeSet) { + use ailang_core::Term; + match t { + Term::Lit { .. } => {} + Term::Var { name } => { + out.insert(name.clone()); + } + Term::App { callee, args } => { + walk_term(callee, out); + for a in args { + walk_term(a, out); + } + } + Term::Let { value, body, .. } => { + walk_term(value, out); + walk_term(body, out); + } + Term::If { cond, then, else_ } => { + walk_term(cond, out); + walk_term(then, out); + walk_term(else_, out); + } + Term::Do { op, args } => { + // Effekt-Ops als `effect:io/print_int` markieren, damit man sie + // von normalen Funktionsaufrufen trennen kann. + out.insert(format!("effect:{op}")); + for a in args { + walk_term(a, out); + } + } + } +} diff --git a/crates/ail/tests/e2e.rs b/crates/ail/tests/e2e.rs index 03fb2a9..11f6b0e 100644 --- a/crates/ail/tests/e2e.rs +++ b/crates/ail/tests/e2e.rs @@ -42,3 +42,17 @@ fn sum_1_to_10_is_55() { let stdout = build_and_run("sum.ail.json"); assert_eq!(stdout.trim(), "55"); } + +/// Schützt das Block-Tracking im Codegen: max3 hat verschachtelte `if`s, +/// und falsches phi-Block-Tracking würde hier zu falschem Ergebnis führen. +#[test] +fn max3_picks_largest() { + let stdout = build_and_run("max3.ail.json"); + assert_eq!(stdout.trim(), "17"); +} + +#[test] +fn hello_world_str_lit() { + let stdout = build_and_run("hello.ail.json"); + assert_eq!(stdout.trim(), "Hallo, AILang."); +} diff --git a/crates/ailang-check/src/builtins.rs b/crates/ailang-check/src/builtins.rs index ad0ebc0..a02d9d4 100644 --- a/crates/ailang-check/src/builtins.rs +++ b/crates/ailang-check/src/builtins.rs @@ -51,6 +51,14 @@ pub fn install(env: &mut crate::Env) { ret: Type::unit(), }, ); + env.effect_ops.insert( + "io/print_str".into(), + EffectOpSig { + effect: "IO".into(), + params: vec![Type::str_()], + ret: Type::unit(), + }, + ); } /// Liefert die Liste aller registrierten Built-ins. Praktisch für CLI-Subcommand @@ -71,5 +79,6 @@ pub fn list() -> Vec<(&'static str, &'static str)> { ("not", "(Bool) -> Bool"), ("io/print_int", "(Int) -> Unit !IO [effect op]"), ("io/print_bool", "(Bool) -> Unit !IO [effect op]"), + ("io/print_str", "(Str) -> Unit !IO [effect op]"), ] } diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 0ab616b..2edf7d3 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -166,6 +166,7 @@ fn synth( Term::Lit { lit } => Ok(match lit { Literal::Int { .. } => Type::int(), Literal::Bool { .. } => Type::bool_(), + Literal::Str { .. } => Type::str_(), Literal::Unit => Type::unit(), }), Term::Var { name } => { diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index 261b3bf..d4305ea 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -56,6 +56,9 @@ struct Emitter<'a> { str_counter: u64, /// Liste aller user-definierten Top-Level-Funktionen (für call-resolution). user_fns: 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)] @@ -88,9 +91,16 @@ impl<'a> Emitter<'a> { counter: 0, str_counter: 0, user_fns, + current_block: String::new(), } } + fn start_block(&mut self, label: &str) { + self.body.push_str(label); + self.body.push_str(":\n"); + self.current_block = label.to_string(); + } + fn finish(self) -> String { let mut out = String::new(); out.push_str("; AILang generated module: "); @@ -115,7 +125,8 @@ impl<'a> Emitter<'a> { out.push('\n'); } - out.push_str("declare i32 @printf(ptr, ...)\n\n"); + out.push_str("declare i32 @printf(ptr, ...)\n"); + out.push_str("declare i32 @puts(ptr)\n\n"); out.push_str(&self.header); out.push_str(&self.body); out @@ -159,20 +170,26 @@ impl<'a> Emitter<'a> { fn emit_const(&mut self, c: &ConstDef) -> Result<()> { let lty = llvm_type(&c.ty)?; - let (val_ty, val) = match &c.value { - Term::Lit { lit } => match lit { - Literal::Int { value } => ("i64".to_string(), value.to_string()), - Literal::Bool { value } => { - ("i1".to_string(), if *value { "true".into() } else { "false".into() }) - } - Literal::Unit => ("i8".to_string(), "0".to_string()), - }, + let lit = match &c.value { + Term::Lit { lit } => lit, _ => { return Err(CodegenError::Internal( "MVP: const muss Literal sein".into(), )); } }; + let (val_ty, val) = match lit { + Literal::Int { value } => ("i64".to_string(), value.to_string()), + Literal::Bool { value } => ( + "i1".to_string(), + if *value { "true".into() } else { "false".into() }, + ), + Literal::Unit => ("i8".to_string(), "0".to_string()), + Literal::Str { value } => { + let g = self.intern_string("str", value); + ("ptr".to_string(), format!("@{g}")) + } + }; if val_ty != lty { return Err(CodegenError::Internal(format!( "const type mismatch: {} vs {}", @@ -220,7 +237,7 @@ impl<'a> Emitter<'a> { } sig.push_str(") {\n"); self.body.push_str(&sig); - self.body.push_str("entry:\n"); + self.start_block("entry"); let (val, val_ty) = self.lower_term(&f.body)?; if val_ty != llvm_ret { @@ -243,6 +260,12 @@ impl<'a> Emitter<'a> { if *value { "true".into() } else { "false".into() }, "i1".into(), ), + Literal::Str { value } => { + // Globale Konstante anlegen; in opaque-pointer-LLVM + // ist `@name` direkt ein gültiger `ptr`. + let g = self.intern_string("str", value); + (format!("@{g}"), "ptr".into()) + } Literal::Unit => ("0".into(), "i8".into()), }), Term::Var { name } => { @@ -275,24 +298,24 @@ impl<'a> Emitter<'a> { " br i1 {cond_v}, label %{then_lbl}, label %{else_lbl}\n" )); - self.body.push_str(&format!("{then_lbl}:\n")); + self.start_block(&then_lbl); let (then_v, then_ty) = self.lower_term(then)?; - let then_block_end = self.current_block_label_for_phi(&then_lbl); - self.body - .push_str(&format!(" br label %{join_lbl}\n")); + // Verschachtelter Code im `then`-Body kann das Block-Label + // verändert haben — phi muss den letzten tatsächlichen Block sehen. + let then_block_end = self.current_block.clone(); + self.body.push_str(&format!(" br label %{join_lbl}\n")); - self.body.push_str(&format!("{else_lbl}:\n")); + self.start_block(&else_lbl); let (else_v, else_ty) = self.lower_term(else_)?; if then_ty != else_ty { return Err(CodegenError::Internal(format!( "if branches type mismatch: {then_ty} vs {else_ty}" ))); } - let else_block_end = self.current_block_label_for_phi(&else_lbl); - self.body - .push_str(&format!(" br label %{join_lbl}\n")); + let else_block_end = self.current_block.clone(); + self.body.push_str(&format!(" br label %{join_lbl}\n")); - self.body.push_str(&format!("{join_lbl}:\n")); + self.start_block(&join_lbl); let phi = self.fresh_ssa(); self.body.push_str(&format!( " {phi} = phi {ty} [ {tv}, %{tlbl} ], [ {ev}, %{elbl} ]\n", @@ -396,6 +419,22 @@ impl<'a> Emitter<'a> { )); Ok(("0".into(), "i8".into())) } + "io/print_str" => { + if args.len() != 1 { + return Err(CodegenError::Internal( + "io/print_str arity".into(), + )); + } + let (v, vty) = self.lower_term(&args[0])?; + if vty != "ptr" { + return Err(CodegenError::Internal( + "io/print_str needs ptr".into(), + )); + } + self.body + .push_str(&format!(" call i32 @puts(ptr {v})\n")); + Ok(("0".into(), "i8".into())) + } "io/print_bool" => { if args.len() != 1 { return Err(CodegenError::Internal( @@ -418,17 +457,17 @@ impl<'a> Emitter<'a> { self.body.push_str(&format!( " br i1 {v}, label %{then_lbl}, label %{else_lbl}\n" )); - self.body.push_str(&format!("{then_lbl}:\n")); + self.start_block(&then_lbl); self.body.push_str(&format!( " call i32 (ptr, ...) @printf(ptr @{fmt_t})\n" )); self.body.push_str(&format!(" br label %{join_lbl}\n")); - self.body.push_str(&format!("{else_lbl}:\n")); + self.start_block(&else_lbl); self.body.push_str(&format!( " call i32 (ptr, ...) @printf(ptr @{fmt_f})\n" )); self.body.push_str(&format!(" br label %{join_lbl}\n")); - self.body.push_str(&format!("{join_lbl}:\n")); + self.start_block(&join_lbl); Ok(("0".into(), "i8".into())) } other => Err(CodegenError::Internal(format!( @@ -446,25 +485,6 @@ impl<'a> Emitter<'a> { self.counter } - /// Im MVP haben wir keinen verschachtelten Control-Flow innerhalb von - /// `then`/`else` von `if`, also entspricht das End-Label dem Block-Anfang. - /// Das ändert sich, sobald `if` rekursiv andere `if`s enthält — dann muss - /// das tatsächlich aktuelle Label am Phi-Punkt verwendet werden. - fn current_block_label_for_phi(&self, fallback: &str) -> String { - // Heuristik: scanne self.body rückwärts nach dem letzten Label-Header. - // Das ist robuster als anzunehmen, dass der ursprüngliche Block-Header - // noch der aktuelle ist. - for line in self.body.lines().rev() { - let line = line.trim_end(); - if let Some(s) = line.strip_suffix(':') { - if !s.starts_with(' ') && !s.is_empty() && !s.contains(' ') { - return s.to_string(); - } - } - } - fallback.to_string() - } - fn intern_string(&mut self, hint: &str, content: &str) -> String { if let Some((name, _)) = self.strings.get(content) { return name.clone(); @@ -484,6 +504,7 @@ fn llvm_type(t: &Type) -> Result { "Int" => Ok("i64".into()), "Bool" => Ok("i1".into()), "Unit" => Ok("i8".into()), + "Str" => Ok("ptr".into()), other => Err(CodegenError::UnsupportedType(other.into())), }, other => Err(CodegenError::UnsupportedType( diff --git a/crates/ailang-core/src/ast.rs b/crates/ailang-core/src/ast.rs index e8a8777..18176be 100644 --- a/crates/ailang-core/src/ast.rs +++ b/crates/ailang-core/src/ast.rs @@ -87,6 +87,7 @@ pub enum Term { pub enum Literal { Int { value: i64 }, Bool { value: bool }, + Str { value: String }, Unit, } @@ -121,6 +122,9 @@ impl Type { pub fn unit() -> Type { Type::Con { name: "Unit".into() } } + pub fn str_() -> Type { + Type::Con { name: "Str".into() } + } } impl PartialEq for Type { diff --git a/crates/ailang-core/src/pretty.rs b/crates/ailang-core/src/pretty.rs index 391d1b1..a832d19 100644 --- a/crates/ailang-core/src/pretty.rs +++ b/crates/ailang-core/src/pretty.rs @@ -180,6 +180,11 @@ fn lit_to_string(l: &Literal) -> String { match l { Literal::Int { value } => value.to_string(), Literal::Bool { value } => value.to_string(), + Literal::Str { value } => { + // serde_json escapt für uns; das Ergebnis ist ein gültiges + // JSON-String-Literal, was für uns als kanonische Form ausreicht. + serde_json::to_string(value).unwrap() + } Literal::Unit => "()".to_string(), } } diff --git a/docs/JOURNAL.md b/docs/JOURNAL.md index d34f41e..d28a924 100644 --- a/docs/JOURNAL.md +++ b/docs/JOURNAL.md @@ -14,4 +14,56 @@ Hindernisse, Beobachtungen, die zukünftige Iterationen brauchen. - `crates/ailang-check` — Typchecker (kommt später) - `crates/ailang-codegen` — Lowering + LLVM IR Emit - `crates/ail` — CLI -- MVP-Ziel: `examples/sum.ail.json` → Binary, das 55 druckt. +- MVP-Ziel: `examples/sum.ail.json` → Binary, das 55 druckt. **Erreicht.** + +## 2026-05-07 — Architektur-Review nach MVP + +Wieder am Pfad? Im Großen ja. Konkrete Beobachtungen: + +**Was hält:** + +- JSON-AST + canonical form + content-hash sind alle Lego-Steine, auf denen + spätere Tools ohne Refactor aufsetzen können (`ail deps`, `ail diff`). +- LLVM-IR-Text-Pipeline arbeitet wie geplant. Keine libllvm-Versionsschmerzen. +- Effekt-Set ist im Typsystem von Anfang an verbaut. Erweiterbar zu row-poly, + ohne den Kern zu ändern. + +**Schulden, die Zinsen tragen:** + +1. **`current_block_label_for_phi` ist eine Heuristik** (siehe codegen). Bei + verschachtelten `if`-Termen wird sie das falsche Block-Label zurückgeben, + weil sie rückwärts den Body scannt. Tickend, weil keine Test-Fälle das + bisher triggern. Muss als nächstes fixiert werden, bevor neue Sprachfeatures + dazu kommen. +2. **Kein typed-AST.** Codegen liest direkt das Quell-AST und verlässt sich + darauf, dass der Typchecker vorher lief. Für MVP OK; sobald ADTs oder + Closures dazukommen, brauche ich eine getrennte typisierte IR-Stufe (TIR). +3. **`hash`-Feld ist nicht im AST.** Aktuell hashen wir das Def-Objekt direkt. + Wenn ich Hashes später als Felder mitserialisiere (Caching), muss der Hash + das Feld vor der Berechnung ausschließen. + +**Plan Iteration 2 (jetzt):** + +1. Block-Label-Tracking sauber machen, verschachteltes-if-Test. +2. Strings als Literal + `io/print_str`. +3. Hello-world-Beispiel als zweiten E2E-Test. +4. CLI: `--json`-Output für maschinelle Konsumenten überall, wo es passt. + +**Plan Iteration 3:** + +ADTs + Pattern Matching. Das ist der nächste große Sprung. Erfordert +typisierte IR-Stufe (TIR), weil Pattern Matching zu Decision-Trees lowered +wird, was eine andere Form ist als der AST. + +## 2026-05-07 — Iteration 2 fertig + +- Block-Label-Tracking robust (verschachtelte `if`s funktionieren). Test + `max3_picks_largest` schützt das. +- Strings als `Lit::Str { value }`, Type `Str` -> LLVM `ptr`, mit + `io/print_str` Effekt-Op. `examples/hello.ail.json` druckt einen String. +- CLI: `manifest --json`, `builtins --json` für Tool-Konsumenten. +- `ail deps [--of NAME] [--json]` listet Aufruf-Edges. Effekt-Ops sind als + `effect:NAME` markiert, damit ein Konsument sie filtern kann. + +**Architektur-Check:** Keine strukturellen Abweichungen. Codegen liest noch +direkt das Quell-AST (TIR-Stufe wird mit ADTs in Iteration 3 nötig). diff --git a/examples/hello.ail.json b/examples/hello.ail.json new file mode 100644 index 0000000..b68c768 --- /dev/null +++ b/examples/hello.ail.json @@ -0,0 +1,25 @@ +{ + "schema": "ailang/v0", + "name": "hello", + "imports": [], + "defs": [ + { + "kind": "fn", + "name": "main", + "type": { + "k": "fn", + "params": [], + "ret": { "k": "con", "name": "Unit" }, + "effects": ["IO"] + }, + "params": [], + "body": { + "t": "do", + "op": "io/print_str", + "args": [ + { "t": "lit", "lit": { "kind": "str", "value": "Hallo, AILang." } } + ] + } + } + ] +} diff --git a/examples/max3.ail.json b/examples/max3.ail.json new file mode 100644 index 0000000..b57964c --- /dev/null +++ b/examples/max3.ail.json @@ -0,0 +1,113 @@ +{ + "schema": "ailang/v0", + "name": "max3", + "imports": [], + "defs": [ + { + "kind": "fn", + "name": "max", + "type": { + "k": "fn", + "params": [ + { "k": "con", "name": "Int" }, + { "k": "con", "name": "Int" } + ], + "ret": { "k": "con", "name": "Int" }, + "effects": [] + }, + "params": ["a", "b"], + "body": { + "t": "if", + "cond": { + "t": "app", + "fn": { "t": "var", "name": ">" }, + "args": [ + { "t": "var", "name": "a" }, + { "t": "var", "name": "b" } + ] + }, + "then": { "t": "var", "name": "a" }, + "else": { "t": "var", "name": "b" } + } + }, + { + "kind": "fn", + "name": "max3", + "type": { + "k": "fn", + "params": [ + { "k": "con", "name": "Int" }, + { "k": "con", "name": "Int" }, + { "k": "con", "name": "Int" } + ], + "ret": { "k": "con", "name": "Int" }, + "effects": [] + }, + "params": ["a", "b", "c"], + "doc": "Demonstriert verschachteltes if (statt max-call) zum Test des Block-Trackings.", + "body": { + "t": "if", + "cond": { + "t": "app", + "fn": { "t": "var", "name": ">" }, + "args": [ + { "t": "var", "name": "a" }, + { "t": "var", "name": "b" } + ] + }, + "then": { + "t": "if", + "cond": { + "t": "app", + "fn": { "t": "var", "name": ">" }, + "args": [ + { "t": "var", "name": "a" }, + { "t": "var", "name": "c" } + ] + }, + "then": { "t": "var", "name": "a" }, + "else": { "t": "var", "name": "c" } + }, + "else": { + "t": "if", + "cond": { + "t": "app", + "fn": { "t": "var", "name": ">" }, + "args": [ + { "t": "var", "name": "b" }, + { "t": "var", "name": "c" } + ] + }, + "then": { "t": "var", "name": "b" }, + "else": { "t": "var", "name": "c" } + } + } + }, + { + "kind": "fn", + "name": "main", + "type": { + "k": "fn", + "params": [], + "ret": { "k": "con", "name": "Unit" }, + "effects": ["IO"] + }, + "params": [], + "body": { + "t": "do", + "op": "io/print_int", + "args": [ + { + "t": "app", + "fn": { "t": "var", "name": "max3" }, + "args": [ + { "t": "lit", "lit": { "kind": "int", "value": 3 } }, + { "t": "lit", "lit": { "kind": "int", "value": 17 } }, + { "t": "lit", "lit": { "kind": "int", "value": 9 } } + ] + } + ] + } + } + ] +}