diff --git a/crates/ail/src/main.rs b/crates/ail/src/main.rs index 0703a2d..6f35ca6 100644 --- a/crates/ail/src/main.rs +++ b/crates/ail/src/main.rs @@ -249,9 +249,29 @@ fn main() -> Result<()> { } } Cmd::EmitIr { path, out } => { - let m = ailang_core::load_module(&path)?; - ailang_check::check(&m)?; - let ir = ailang_codegen::emit_ir(&m)?; + // Iter 5c: Workspace-Lowering. Bei Single-Modul-Programmen ist + // der Workspace effektiv ein Trivial-Workspace mit einem Modul. + let ws = ailang_core::load_workspace(&path)?; + let diags = ailang_check::check_workspace(&ws); + if !diags.is_empty() { + for d in &diags { + eprintln!( + "{}: [{}] {}{}", + match d.severity { + ailang_check::Severity::Error => "error", + ailang_check::Severity::Warning => "warning", + }, + d.code, + d.def + .as_ref() + .map(|n| format!("{n}: ")) + .unwrap_or_default(), + d.message, + ); + } + std::process::exit(1); + } + let ir = ailang_codegen::lower_workspace(&ws)?; match out { Some(p) => { std::fs::write(&p, ir)?; @@ -261,15 +281,34 @@ fn main() -> Result<()> { } } Cmd::Build { path, out, opt } => { - let m = ailang_core::load_module(&path)?; - ailang_check::check(&m)?; - let ir = ailang_codegen::emit_ir(&m)?; + // Iter 5c: gleiche Pipeline wie `emit-ir`, aber clang ruft am Ende. + let ws = ailang_core::load_workspace(&path)?; + let diags = ailang_check::check_workspace(&ws); + if !diags.is_empty() { + for d in &diags { + eprintln!( + "{}: [{}] {}{}", + match d.severity { + ailang_check::Severity::Error => "error", + ailang_check::Severity::Warning => "warning", + }, + d.code, + d.def + .as_ref() + .map(|n| format!("{n}: ")) + .unwrap_or_default(), + d.message, + ); + } + std::process::exit(1); + } + let ir = ailang_codegen::lower_workspace(&ws)?; let tmpdir = std::env::temp_dir().join(format!("ailang-{}", std::process::id())); std::fs::create_dir_all(&tmpdir)?; - let ll_path = tmpdir.join(format!("{}.ll", m.name)); + let ll_path = tmpdir.join(format!("{}.ll", ws.entry)); std::fs::write(&ll_path, &ir)?; let out_bin = out.unwrap_or_else(|| { - Path::new(".").join(&m.name).with_extension("") + Path::new(".").join(&ws.entry).with_extension("") }); let status = std::process::Command::new("clang") .arg(&opt) diff --git a/crates/ail/tests/e2e.rs b/crates/ail/tests/e2e.rs index ba27022..2853961 100644 --- a/crates/ail/tests/e2e.rs +++ b/crates/ail/tests/e2e.rs @@ -256,6 +256,15 @@ fn check_workspace_resolves_import() { assert_eq!(stdout.trim(), "[]", "expected empty diagnostic array"); } +/// Schützt Iter 5c (Cross-Module-Codegen): `ail build` über +/// `examples/ws_main.ail.json` muss das Workspace inkl. `ws_lib` lowern, +/// `@ail_ws_main_main` muss `@ail_ws_lib_add(2,3)` aufrufen und `5` drucken. +#[test] +fn workspace_build_runs_imported_fn() { + let stdout = build_and_run("ws_main.ail.json"); + assert_eq!(stdout.trim(), "5", "ws_lib.add(2,3) should print 5"); +} + /// Schützt das `--json`-Diagnostic-Format für Tooling-Konsumenten. /// `broken_unbound.ail.json` referenziert eine nicht-existente Variable; /// erwartet wird Exit-Code 1 und mindestens ein Diagnostic mit diff --git a/crates/ail/tests/ir_snapshot.rs b/crates/ail/tests/ir_snapshot.rs index 2f9ab42..d29001c 100644 --- a/crates/ail/tests/ir_snapshot.rs +++ b/crates/ail/tests/ir_snapshot.rs @@ -151,3 +151,12 @@ fn ir_snapshot_hello() { fn ir_snapshot_list() { check_ir_snapshot("list.ail.json", "list.ll"); } + +/// Schützt das Workspace-Lowering (Iter 5c): das Eintrittsmodul `ws_main` +/// importiert `ws_lib`, beide werden in derselben `.ll` emittiert, +/// `@ail_ws_main_main` ruft `@ail_ws_lib_add` auf, und das Trampoline +/// `@main` ruft `@ail_ws_main_main`. +#[test] +fn ir_snapshot_ws_main() { + check_ir_snapshot("ws_main.ail.json", "ws_main.ll"); +} diff --git a/crates/ail/tests/snapshots/hello.ll b/crates/ail/tests/snapshots/hello.ll index 306a509..f9c73f2 100644 --- a/crates/ail/tests/snapshots/hello.ll +++ b/crates/ail/tests/snapshots/hello.ll @@ -1,21 +1,21 @@ -; AILang generated module: hello +; AILang generated workspace; entry: hello source_filename = "hello.ail" target triple = "" -@.str_str_0 = private unnamed_addr constant [15 x i8] c"Hallo, AILang.\00", align 1 +@.str_hello_str_0 = private unnamed_addr constant [15 x i8] c"Hallo, AILang.\00", align 1 declare i32 @printf(ptr, ...) declare i32 @puts(ptr) declare ptr @malloc(i64) -define i8 @ail_main() { +define i8 @ail_hello_main() { entry: - call i32 @puts(ptr @.str_str_0) + call i32 @puts(ptr @.str_hello_str_0) ret i8 0 } define i32 @main() { - call i8 @ail_main() + call i8 @ail_hello_main() ret i32 0 } diff --git a/crates/ail/tests/snapshots/list.ll b/crates/ail/tests/snapshots/list.ll index 510f47b..c6238d6 100644 --- a/crates/ail/tests/snapshots/list.ll +++ b/crates/ail/tests/snapshots/list.ll @@ -1,14 +1,14 @@ -; AILang generated module: list +; AILang generated workspace; entry: list source_filename = "list.ail" target triple = "" -@.str_fmt_int_0 = private unnamed_addr constant [6 x i8] c"%lld\0A\00", align 1 +@.str_list_fmt_int_0 = private unnamed_addr constant [6 x i8] c"%lld\0A\00", align 1 declare i32 @printf(ptr, ...) declare i32 @puts(ptr) declare ptr @malloc(i64) -define i64 @ail_sum_list(ptr %arg_xs) { +define i64 @ail_list_sum_list(ptr %arg_xs) { entry: %v1 = load i64, ptr %arg_xs, align 8 switch i64 %v1, label %mdefault.2 [ @@ -22,7 +22,7 @@ marm.2.1: %v4 = load i64, ptr %v3, align 8 %v5 = getelementptr inbounds i8, ptr %arg_xs, i64 16 %v6 = load ptr, ptr %v5, align 8 - %v7 = call i64 @ail_sum_list(ptr %v6) + %v7 = call i64 @ail_list_sum_list(ptr %v6) %v8 = add i64 %v4, %v7 br label %mjoin.2 mdefault.2: @@ -32,7 +32,7 @@ mjoin.2: ret i64 %v9 } -define i8 @ail_main() { +define i8 @ail_list_main() { entry: %v1 = call ptr @malloc(i64 8) store i64 0, ptr %v1, align 8 @@ -54,13 +54,13 @@ entry: store i64 10, ptr %v9, align 8 %v10 = getelementptr inbounds i8, ptr %v8, i64 16 store ptr %v5, ptr %v10, align 8 - %v11 = call i64 @ail_sum_list(ptr %v8) - call i32 (ptr, ...) @printf(ptr @.str_fmt_int_0, i64 %v11) + %v11 = call i64 @ail_list_sum_list(ptr %v8) + call i32 (ptr, ...) @printf(ptr @.str_list_fmt_int_0, i64 %v11) ret i8 0 } define i32 @main() { - call i8 @ail_main() + call i8 @ail_list_main() ret i32 0 } diff --git a/crates/ail/tests/snapshots/max3.ll b/crates/ail/tests/snapshots/max3.ll index 41e204b..b7dca91 100644 --- a/crates/ail/tests/snapshots/max3.ll +++ b/crates/ail/tests/snapshots/max3.ll @@ -1,14 +1,14 @@ -; AILang generated module: max3 +; AILang generated workspace; entry: max3 source_filename = "max3.ail" target triple = "" -@.str_fmt_int_0 = private unnamed_addr constant [6 x i8] c"%lld\0A\00", align 1 +@.str_max3_fmt_int_0 = private unnamed_addr constant [6 x i8] c"%lld\0A\00", align 1 declare i32 @printf(ptr, ...) declare i32 @puts(ptr) declare ptr @malloc(i64) -define i64 @ail_max(i64 %arg_a, i64 %arg_b) { +define i64 @ail_max3_max(i64 %arg_a, i64 %arg_b) { entry: %v1 = icmp sgt i64 %arg_a, %arg_b br i1 %v1, label %then.2, label %else.2 @@ -21,7 +21,7 @@ join.2: ret i64 %v3 } -define i64 @ail_max3(i64 %arg_a, i64 %arg_b, i64 %arg_c) { +define i64 @ail_max3_max3(i64 %arg_a, i64 %arg_b, i64 %arg_c) { entry: %v1 = icmp sgt i64 %arg_a, %arg_b br i1 %v1, label %then.2, label %else.2 @@ -50,15 +50,15 @@ join.2: ret i64 %v9 } -define i8 @ail_main() { +define i8 @ail_max3_main() { entry: - %v1 = call i64 @ail_max3(i64 3, i64 17, i64 9) - call i32 (ptr, ...) @printf(ptr @.str_fmt_int_0, i64 %v1) + %v1 = call i64 @ail_max3_max3(i64 3, i64 17, i64 9) + call i32 (ptr, ...) @printf(ptr @.str_max3_fmt_int_0, i64 %v1) ret i8 0 } define i32 @main() { - call i8 @ail_main() + call i8 @ail_max3_main() ret i32 0 } diff --git a/crates/ail/tests/snapshots/sum.ll b/crates/ail/tests/snapshots/sum.ll index 4b72c6e..1e65528 100644 --- a/crates/ail/tests/snapshots/sum.ll +++ b/crates/ail/tests/snapshots/sum.ll @@ -1,14 +1,14 @@ -; AILang generated module: sum +; AILang generated workspace; entry: sum source_filename = "sum.ail" target triple = "" -@.str_fmt_int_0 = private unnamed_addr constant [6 x i8] c"%lld\0A\00", align 1 +@.str_sum_fmt_int_0 = private unnamed_addr constant [6 x i8] c"%lld\0A\00", align 1 declare i32 @printf(ptr, ...) declare i32 @puts(ptr) declare ptr @malloc(i64) -define i64 @ail_sum(i64 %arg_n) { +define i64 @ail_sum_sum(i64 %arg_n) { entry: %v1 = icmp eq i64 %arg_n, 0 br i1 %v1, label %then.2, label %else.2 @@ -16,7 +16,7 @@ then.2: br label %join.2 else.2: %v3 = sub i64 %arg_n, 1 - %v4 = call i64 @ail_sum(i64 %v3) + %v4 = call i64 @ail_sum_sum(i64 %v3) %v5 = add i64 %arg_n, %v4 br label %join.2 join.2: @@ -24,15 +24,15 @@ join.2: ret i64 %v6 } -define i8 @ail_main() { +define i8 @ail_sum_main() { entry: - %v1 = call i64 @ail_sum(i64 10) - call i32 (ptr, ...) @printf(ptr @.str_fmt_int_0, i64 %v1) + %v1 = call i64 @ail_sum_sum(i64 10) + call i32 (ptr, ...) @printf(ptr @.str_sum_fmt_int_0, i64 %v1) ret i8 0 } define i32 @main() { - call i8 @ail_main() + call i8 @ail_sum_main() ret i32 0 } diff --git a/crates/ail/tests/snapshots/ws_main.ll b/crates/ail/tests/snapshots/ws_main.ll new file mode 100644 index 0000000..9e5d567 --- /dev/null +++ b/crates/ail/tests/snapshots/ws_main.ll @@ -0,0 +1,28 @@ +; AILang generated workspace; entry: ws_main +source_filename = "ws_main.ail" +target triple = "" + +@.str_ws_main_fmt_int_0 = private unnamed_addr constant [6 x i8] c"%lld\0A\00", align 1 + +declare i32 @printf(ptr, ...) +declare i32 @puts(ptr) +declare ptr @malloc(i64) + +define i64 @ail_ws_lib_add(i64 %arg_a, i64 %arg_b) { +entry: + %v1 = add i64 %arg_a, %arg_b + ret i64 %v1 +} + +define i8 @ail_ws_main_main() { +entry: + %v1 = call i64 @ail_ws_lib_add(i64 2, i64 3) + call i32 (ptr, ...) @printf(ptr @.str_ws_main_fmt_int_0, i64 %v1) + ret i8 0 +} + + +define i32 @main() { + call i8 @ail_ws_main_main() + ret i32 0 +} diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index 5abd9c6..dd427d5 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -8,12 +8,20 @@ //! - `Bool` -> `i1` //! - `Unit` -> `i8` (Wert immer 0) //! -//! Named-Mangling: AILang-Fn `foo` wird zu LLVM `@ail_foo`. Wenn ein Modul -//! eine Funktion `main : () -> Unit !IO` hat, wird zusätzlich ein -//! `define i32 @main()` Wrapper erzeugt, der `@ail_main` aufruft und 0 -//! zurückgibt — damit das Binary direkt ausführbar ist. +//! Mangling-Schema (Iter 5c): +//! - **Alle** AILang-Funktionen werden zu `@ail__`. Das gilt +//! auch für Single-Modul-Programme. Die alte Form `@ail_` entfällt. +//! - Globale String-/Konstanten-Symbole sind pro Modul gemangelt: +//! `@.str__` bzw. `@ail__` für Konstanten-Globals. +//! - Eintrittspunkt bleibt `main` (LLVM-/C-ABI). Der Generator emittiert +//! `define i64 @main() { call @ail__main() ... }` als +//! Trampoline auf das `main` des Eintrittsmoduls. Fehlt dieses, scheitert +//! der Build mit `MissingEntryMain`. +//! - `source_filename` taucht genau einmal am Anfang auf, mit +//! `.ail` als Wert (pro Workspace). use ailang_core::ast::*; +use ailang_core::Workspace; use std::collections::BTreeMap; #[derive(Debug, thiserror::Error)] @@ -21,6 +29,9 @@ pub enum CodegenError { #[error("def `{0}`: {1}")] Def(String, Box), + #[error("module `{0}`: {1}")] + InModule(String, Box), + #[error("unsupported type: {0}")] UnsupportedType(String), @@ -30,32 +41,176 @@ pub enum CodegenError { #[error("expected fn type, got {0}")] NotFnType(String), + #[error("entry module `{0}` has no `main` def")] + MissingEntryMain(String), + #[error("internal: {0}")] Internal(String), } type Result = std::result::Result; +/// Lowert einen einzelnen Modul. Backwards-Kompatibilität für Tests / CLI- +/// Aufrufe, die ohne Workspace arbeiten möchten. Intern wird ein Trivial- +/// Workspace mit nur diesem Modul gebaut, sodass das Mangling-Schema +/// konsistent bleibt. pub fn emit_ir(m: &Module) -> Result { - let mut emitter = Emitter::new(m); - emitter.emit_module()?; - Ok(emitter.finish()) + let mut modules = BTreeMap::new(); + modules.insert(m.name.clone(), m.clone()); + let ws = Workspace { + entry: m.name.clone(), + modules, + root_dir: std::path::PathBuf::from("."), + }; + lower_workspace(&ws) +} + +/// Lowert einen kompletten Workspace zu einem `.ll`-String. Reihenfolge der +/// Module ist alphabetisch (BTreeMap-Order = deterministisch). Innerhalb +/// eines Moduls bleibt die Def-Reihenfolge wie im AST. +/// +/// Cross-Module-Calls: `Term::Var { name }` mit genau einem Punkt +/// (`.`) wird über die Import-Map des aufrufenden Moduls auf +/// `@ail__` aufgelöst. Lokale Var-Lookups (ohne Punkt) +/// bleiben Stack-Locals oder lokale Top-Level-Defs des aktuellen Moduls. +pub fn lower_workspace(ws: &Workspace) -> Result { + let mut header = String::new(); + let mut body = String::new(); + let mut all_strings: BTreeMap> = BTreeMap::new(); + // ^ pro Modul: Liste (global-name, content). Reihenfolge = Insert-Order. + + // Pass 1: Pro Modul die Top-Level-Symboltabelle (für lokale Var-Lookups). + let mut module_user_fns: BTreeMap> = BTreeMap::new(); + for (mname, m) in &ws.modules { + let mut user_fns = BTreeMap::new(); + for def in &m.defs { + if let Def::Fn(f) = def { + if let Type::Fn { params, ret, .. } = &f.ty { + let psig: Result> = params.iter().map(llvm_type).collect(); + let rsig = llvm_type(ret); + if let (Ok(params), Ok(ret)) = (psig, rsig) { + user_fns.insert(f.name.clone(), FnSig { params, ret }); + } + } + } + } + module_user_fns.insert(mname.clone(), user_fns); + } + + // Pass 2: pro Modul lowern. Globals/Strings werden pro Modul akkumuliert, + // weil sie pro-Modul gemangelt sind. + for (mname, m) in &ws.modules { + // Import-Map für Cross-Module-Resolution. Identisch zur + // Logik im Typchecker (siehe `check_in_workspace`): Alias oder + // Modulname als Key, echter Modulname als Value. + let mut import_map: BTreeMap = BTreeMap::new(); + for imp in &m.imports { + let key = imp.alias.clone().unwrap_or_else(|| imp.module.clone()); + import_map.insert(key, imp.module.clone()); + } + + let mut emitter = Emitter::new(m, mname, &module_user_fns, import_map); + emitter + .emit_module() + .map_err(|e| CodegenError::InModule(mname.clone(), Box::new(e)))?; + header.push_str(&emitter.header); + body.push_str(&emitter.body); + // Strings sammeln, in Insertion-Order. + let mut entries: Vec<(String, String)> = Vec::new(); + for (content, (name, _)) in &emitter.strings { + entries.push((name.clone(), content.clone())); + } + // sort by global name to stay deterministic across runs (intern_string + // benutzt einen monotonen Counter, also reicht alphabetisch). + entries.sort_by(|a, b| a.0.cmp(&b.0)); + all_strings.insert(mname.clone(), entries); + } + + // Trampoline: prüfe, dass das Eintrittsmodul ein `main : () -> Unit !IO` + // hat. Wenn nicht, ist der Workspace nicht ausführbar. + let entry_module = ws + .modules + .get(&ws.entry) + .ok_or_else(|| CodegenError::Internal(format!("entry module `{}` not in workspace", ws.entry)))?; + let has_main = entry_module + .defs + .iter() + .any(|d| matches!(d, Def::Fn(f) if f.name == "main" && main_is_void(&f.ty))); + if !has_main { + return Err(CodegenError::MissingEntryMain(ws.entry.clone())); + } + + let mut out = String::new(); + out.push_str("; AILang generated workspace; entry: "); + out.push_str(&ws.entry); + out.push('\n'); + out.push_str("source_filename = \""); + out.push_str(&ws.entry); + out.push_str(".ail\"\n"); + out.push_str("target triple = \""); + out.push_str(default_triple()); + out.push_str("\"\n\n"); + + // Globals: pro Modul, alphabetisch über Modulnamen (BTreeMap-Order), + // dann Insertion-Order pro Modul. + let mut emitted_global = false; + for (_mname, entries) in &all_strings { + for (name, content) in entries { + let escaped = ll_string_literal(content); + let len = c_byte_len(content); + out.push_str(&format!( + "@{name} = private unnamed_addr constant [{len} x i8] c\"{escaped}\", align 1\n", + )); + emitted_global = true; + } + } + if emitted_global { + out.push('\n'); + } + + out.push_str("declare i32 @printf(ptr, ...)\n"); + out.push_str("declare i32 @puts(ptr)\n"); + out.push_str("declare ptr @malloc(i64)\n\n"); + out.push_str(&header); + out.push_str(&body); + + // Trampoline @main → @ail__main. + out.push_str(&format!( + "\ndefine i32 @main() {{\n call i8 @ail_{}_main()\n ret i32 0\n}}\n", + ws.entry + )); + + Ok(out) +} + +fn main_is_void(t: &Type) -> bool { + match t { + Type::Fn { params, ret, .. } => { + params.is_empty() + && matches!(ret.as_ref(), Type::Con { name } if name == "Unit") + } + _ => false, + } } struct Emitter<'a> { module: &'a Module, + /// Name des aktuell gelowerten Moduls (für Mangling). + module_name: &'a str, header: String, body: String, - /// String-Konstanten: content -> (global-name, llvm-typ-länge inkl. \0) + /// String-Konstanten: content -> (global-name (ohne `@`), llvm-typ-länge inkl. \0) strings: BTreeMap, /// Lokale Symboltabelle pro Funktion: name -> (ssa-name inkl `%`, llvm-typ). locals: Vec<(String, String, String)>, /// fortlaufender Zähler für SSA-Werte und Labels. counter: u64, - /// fortlaufender Zähler für globale String-Namen. + /// fortlaufender Zähler für globale String-Namen (pro Modul). str_counter: u64, - /// Liste aller user-definierten Top-Level-Funktionen (für call-resolution). - user_fns: BTreeMap, + /// Top-Level-Funktionen pro Modul des Workspaces, für call-resolution. + module_user_fns: &'a BTreeMap>, + /// Import-Map des aktuellen Moduls (Alias/Modulname → echter Modulname). + import_map: 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, @@ -90,21 +245,12 @@ struct FnSig { } impl<'a> Emitter<'a> { - fn new(module: &'a Module) -> Self { - let mut user_fns = BTreeMap::new(); - for def in &module.defs { - if let Def::Fn(f) = def { - if let Type::Fn { params, ret, .. } = &f.ty { - let psig: Result> = - params.iter().map(llvm_type).collect(); - let rsig = llvm_type(ret); - if let (Ok(params), Ok(ret)) = (psig, rsig) { - user_fns.insert(f.name.clone(), FnSig { params, ret }); - } - } - } - } - + fn new( + module: &'a Module, + module_name: &'a str, + module_user_fns: &'a BTreeMap>, + import_map: BTreeMap, + ) -> Self { let mut types: BTreeMap> = BTreeMap::new(); let mut ctor_index: BTreeMap = BTreeMap::new(); for def in &module.defs { @@ -135,13 +281,15 @@ impl<'a> Emitter<'a> { Self { module, + module_name, header: String::new(), body: String::new(), strings: BTreeMap::new(), locals: Vec::new(), counter: 0, str_counter: 0, - user_fns, + module_user_fns, + import_map, types, ctor_index, current_block: String::new(), @@ -154,51 +302,17 @@ impl<'a> Emitter<'a> { self.current_block = label.to_string(); } - fn finish(self) -> String { - let mut out = String::new(); - out.push_str("; AILang generated module: "); - out.push_str(&self.module.name); - out.push('\n'); - out.push_str("source_filename = \""); - out.push_str(&self.module.name); - out.push_str(".ail\"\n"); - out.push_str("target triple = \""); - out.push_str(default_triple()); - out.push_str("\"\n\n"); - - // Globals first. - for (content, (name, _)) in &self.strings { - let escaped = ll_string_literal(content); - let len = c_byte_len(content); - out.push_str(&format!( - "@{name} = private unnamed_addr constant [{len} x i8] c\"{escaped}\", align 1\n", - )); - } - if !self.strings.is_empty() { - out.push('\n'); - } - - out.push_str("declare i32 @printf(ptr, ...)\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 - } - fn emit_module(&mut self) -> Result<()> { let defs: Vec<&Def> = self.module.defs.iter().collect(); for def in defs { match def { Def::Fn(f) => { - self.emit_fn(f).map_err(|e| { - CodegenError::Def(f.name.clone(), Box::new(e)) - })?; + self.emit_fn(f) + .map_err(|e| CodegenError::Def(f.name.clone(), Box::new(e)))?; } Def::Const(c) => { - self.emit_const(c).map_err(|e| { - CodegenError::Def(c.name.clone(), Box::new(e)) - })?; + self.emit_const(c) + .map_err(|e| CodegenError::Def(c.name.clone(), Box::new(e)))?; } Def::Type(_) => { // Keine LLVM-Definition nötig: die ADT existiert nur als @@ -207,23 +321,6 @@ impl<'a> Emitter<'a> { } } } - - // main-Wrapper, falls vorhanden. - if let Some(Def::Fn(main_fn)) = self - .module - .defs - .iter() - .find(|d| d.name() == "main") - { - if let Type::Fn { params, ret, .. } = &main_fn.ty { - if params.is_empty() && matches!(ret.as_ref(), Type::Con { name } if name == "Unit") - { - self.body.push_str( - "\ndefine i32 @main() {\n call i8 @ail_main()\n ret i32 0\n}\n", - ); - } - } - } Ok(()) } @@ -256,7 +353,8 @@ impl<'a> Emitter<'a> { ))); } self.header.push_str(&format!( - "@ail_{name} = constant {ty} {val}\n", + "@ail_{module}_{name} = constant {ty} {val}\n", + module = self.module_name, name = c.name, ty = lty, val = val, @@ -281,7 +379,12 @@ impl<'a> Emitter<'a> { self.locals.clear(); self.counter = 0; - let mut sig = format!("define {ret} @ail_{name}(", ret = llvm_ret, name = f.name); + let mut sig = format!( + "define {ret} @ail_{module}_{name}(", + ret = llvm_ret, + module = self.module_name, + name = f.name + ); for (i, (pname, pty)) in f.params.iter().zip(llvm_param_tys.iter()).enumerate() { if i > 0 { sig.push_str(", "); @@ -660,29 +763,42 @@ impl<'a> Emitter<'a> { return Ok((dst, "i1".into())); } - // User-Funktion? - if let Some(sig) = self.user_fns.get(name).cloned() { - let mut compiled_args = Vec::new(); - for (a, exp_ty) in args.iter().zip(sig.params.iter()) { - let (v, vty) = self.lower_term(a)?; - if &vty != exp_ty { - return Err(CodegenError::Internal(format!( - "call `{name}` arg type mismatch: expected {exp_ty}, got {vty}" - ))); - } - compiled_args.push((v, vty)); - } - let arglist = compiled_args - .iter() - .map(|(v, t)| format!("{t} {v}")) - .collect::>() - .join(", "); - let dst = self.fresh_ssa(); - self.body.push_str(&format!( - " {dst} = call {ret} @ail_{name}({arglist})\n", - ret = sig.ret, - )); - return Ok((dst, sig.ret)); + // Cross-Module-Call: genau ein Punkt im Namen → über Import-Map auflösen. + // Logik identisch zum Typchecker (siehe `synth` für `Term::Var`). + if name.matches('.').count() == 1 { + let (prefix, suffix) = name.split_once('.').expect("checked"); + let target_module = self.import_map.get(prefix).cloned().ok_or_else(|| { + CodegenError::Internal(format!( + "cross-module call `{name}`: prefix `{prefix}` not in import map" + )) + })?; + let target_fns = self + .module_user_fns + .get(&target_module) + .ok_or_else(|| { + CodegenError::Internal(format!( + "cross-module call `{name}`: target module `{target_module}` not found in workspace" + )) + })?; + let sig = target_fns + .get(suffix) + .cloned() + .ok_or_else(|| { + CodegenError::Internal(format!( + "cross-module call `{name}`: def `{suffix}` not in module `{target_module}`" + )) + })?; + return self.emit_call(&target_module, suffix, &sig, args); + } + + // User-Funktion im aktuellen Modul? + if let Some(sig) = self + .module_user_fns + .get(self.module_name) + .and_then(|m| m.get(name)) + .cloned() + { + return self.emit_call(self.module_name, name, &sig, args); } Err(CodegenError::Internal(format!( @@ -690,6 +806,38 @@ impl<'a> Emitter<'a> { ))) } + fn emit_call( + &mut self, + target_module: &str, + target_def: &str, + sig: &FnSig, + args: &[Term], + ) -> Result<(String, String)> { + let mut compiled_args = Vec::new(); + for (a, exp_ty) in args.iter().zip(sig.params.iter()) { + let (v, vty) = self.lower_term(a)?; + if &vty != exp_ty { + return Err(CodegenError::Internal(format!( + "call `{target_module}.{target_def}` arg type mismatch: expected {exp_ty}, got {vty}" + ))); + } + compiled_args.push((v, vty)); + } + let arglist = compiled_args + .iter() + .map(|(v, t)| format!("{t} {v}")) + .collect::>() + .join(", "); + let dst = self.fresh_ssa(); + self.body.push_str(&format!( + " {dst} = call {ret} @ail_{module}_{name}({arglist})\n", + ret = sig.ret, + module = target_module, + name = target_def, + )); + Ok((dst, sig.ret.clone())) + } + fn lower_effect_op(&mut self, op: &str, args: &[Term]) -> Result<(String, String)> { match op { "io/print_int" => { @@ -780,7 +928,8 @@ impl<'a> Emitter<'a> { if let Some((name, _)) = self.strings.get(content) { return name.clone(); } - let name = format!(".str_{}_{}", hint, self.str_counter); + // Mangling pro Modul: `.str___`. + let name = format!(".str_{}_{}_{}", self.module_name, hint, self.str_counter); self.str_counter += 1; let len = c_byte_len(content); self.strings @@ -867,30 +1016,81 @@ mod tests { #[test] fn emits_arith_fn() { + // Single-Modul wird via `emit_ir` zu Trivial-Workspace; das Mangling + // lautet `@ail__` auch im Single-File-Fall. let m = Module { schema: SCHEMA.into(), name: "t".into(), imports: vec![], + defs: vec![ + Def::Fn(FnDef { + name: "add".into(), + ty: Type::Fn { + params: vec![Type::int(), Type::int()], + ret: Box::new(Type::int()), + effects: vec![], + }, + params: vec!["a".into(), "b".into()], + body: Term::App { + callee: Box::new(Term::Var { name: "+".into() }), + args: vec![ + Term::Var { name: "a".into() }, + Term::Var { name: "b".into() }, + ], + }, + doc: None, + }), + // Eintrittsmodul braucht ein `main`, sonst liefert + // `lower_workspace` `MissingEntryMain`. + Def::Fn(FnDef { + name: "main".into(), + ty: Type::Fn { + params: vec![], + ret: Box::new(Type::unit()), + effects: vec![], + }, + params: vec![], + body: Term::Lit { lit: Literal::Unit }, + doc: None, + }), + ], + }; + let ir = emit_ir(&m).unwrap(); + assert!( + ir.contains("define i64 @ail_t_add(i64 %arg_a, i64 %arg_b)"), + "ir was: {ir}" + ); + assert!(ir.contains("add i64 %arg_a, %arg_b")); + assert!( + ir.contains("call i8 @ail_t_main()"), + "trampoline call missing: {ir}" + ); + } + + #[test] + fn missing_entry_main_is_error() { + let m = Module { + schema: SCHEMA.into(), + name: "noentry".into(), + imports: vec![], defs: vec![Def::Fn(FnDef { - name: "add".into(), + name: "helper".into(), ty: Type::Fn { - params: vec![Type::int(), Type::int()], + params: vec![], ret: Box::new(Type::int()), effects: vec![], }, - params: vec!["a".into(), "b".into()], - body: Term::App { - callee: Box::new(Term::Var { name: "+".into() }), - args: vec![ - Term::Var { name: "a".into() }, - Term::Var { name: "b".into() }, - ], + params: vec![], + body: Term::Lit { + lit: Literal::Int { value: 1 }, }, doc: None, })], }; - let ir = emit_ir(&m).unwrap(); - assert!(ir.contains("define i64 @ail_add(i64 %arg_a, i64 %arg_b)")); - assert!(ir.contains("add i64 %arg_a, %arg_b")); + let err = emit_ir(&m).unwrap_err(); + match err { + CodegenError::MissingEntryMain(name) => assert_eq!(name, "noentry"), + other => panic!("expected MissingEntryMain, got {other:?}"), + } } } diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 1afdd78..1a5edcb 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -100,6 +100,14 @@ Begründung: Trade-off: keine Inline-Optimierungen über die LLVM-API. Wir setzen auf `clang -O2` als Standard-Pipeline. +## Mangling-Schema (Iter 5c) + +Alle AILang-Funktionen werden zu `@ail__` gemangelt — auch im +Single-Modul-Fall. Globale Strings/Konstanten zu `@.str__` +bzw. `@ail__`. Eintrittspunkt ist eine `define i64 @main()`- +Trampoline, die `@ail__main()` aufruft. `source_filename` +existiert genau einmal pro Workspace und trägt den Eintrittsmodulnamen. + ## Konvention: Qualifizierte Cross-Module-Verweise (Iter 5b) Cross-Module-Aufrufe nutzen **keinen** neuen AST-Knoten. Stattdessen ist ein diff --git a/docs/JOURNAL.md b/docs/JOURNAL.md index d178b83..24cd325 100644 --- a/docs/JOURNAL.md +++ b/docs/JOURNAL.md @@ -249,3 +249,46 @@ IR-Snapshots. Punkt-Konvention deckt nur einen Punkt ab — verschachtelte Modulpfade (`a.b.c`) gibt es nicht; das wäre erst mit Hierarchie-Modulen ein Thema und fällt aktuell als `unbound-var` durch. + +## 2026-05-07 — Iter 5c fertig: Cross-Module-Codegen + +- **Mangling-Bruch (mit Bedacht).** Alle AILang-Funktionen heißen jetzt + `@ail__`, auch in Single-Modul-Programmen. Die alte Form + `@ail_` ist weg. Strings/Const-Globals analog + (`@.str___`, `@ail__`). Eintrittspunkt + bleibt `main` als C-ABI: ein `define i32 @main()`-Trampoline ruft + `@ail__main()`. Fehlt im Eintrittsmodul ein + `main : () -> Unit !IO`, fällt der Build mit `MissingEntryMain`. +- **Workspace-Lowering.** Neue Top-Level-API + `ailang_codegen::lower_workspace(ws: &Workspace) -> Result` + produziert eine einzelne `.ll` für den ganzen Workspace. Module + alphabetisch (BTreeMap-Order); Defs in AST-Reihenfolge. Cross-Module- + Calls werden im Codegen über die Import-Map des aufrufenden Moduls + aufgelöst — gleiche Logik wie im Typchecker, lokal dupliziert mit + Verweis (kein gemeinsames Helper-Modul, weil die Typ-Welten + unterschiedlich sind: Typchecker hantiert mit `Type`, Codegen mit + `FnSig` aus llvm-Typen). +- **CLI.** `ail build` und `ail emit-ir` laden jetzt immer den Workspace + und checken/lowern ihn vollständig. Single-Modul-Programme funktionieren + weiter (Trivial-Workspace mit einem Modul). `emit_ir(m)` bleibt im + Codegen-Crate als Bequemlichkeits-API erhalten und wrapt intern in + einen Trivial-Workspace. +- **Snapshots regeneriert.** `sum.ll`, `max3.ll`, `hello.ll`, `list.ll` + zeigen das neue Mangling. Neuer `ws_main.ll`-Snapshot dokumentiert den + Cross-Module-Build: `@ail_ws_main_main` ruft `@ail_ws_lib_add` auf. +- **Tests.** 40 grün (vorher 37). Neu: `workspace_build_runs_imported_fn` + (e2e: druckt 5), `ir_snapshot_ws_main`, `missing_entry_main_is_error` + (codegen-unit). Bestehende Verhaltens-Tests + (`sum_1_to_10_is_55`, `max3_picks_largest`, `hello_world_str_lit`, + `list_sum_via_match`) bleiben grün — Verhalten unverändert, nur das + Mangling ist neu. + +**Schulden geschlossen:** + +- **#19 (`source_filename` härten).** In der Workspace-Welt ist + `source_filename` jetzt einheitlich `.ail`, einmal pro + Workspace. Der bisherige hartkodierte Pfad-Punkt entfällt damit. + +**Stand:** Modulsystem ist Ende-zu-Ende geschlossen — Loader + Typcheck ++ Codegen + Build sehen den Workspace als kohärente Einheit. Multi- +Diagnose-Refactor und ggf. ADT-Cross-Module bleiben für später.