Iter 5c: Cross-Module-Codegen

Symbol-Mangling-Schema einheitlich auf @ail_<modul>_<def> umgestellt
(auch für Single-Modul-Programme), String-Globals als
@.str_<modul>_<idx>. main bleibt LLVM-/C-ABI-Eintrittspunkt und ist
ein Trampoline auf @ail_<entry>_main. lower_workspace emittiert eine
einzige .ll für den ganzen Workspace, alphabetisch nach Modulname,
Cross-Module-Calls über Import-Map aufgelöst. ail build / ail emit-ir
laufen jetzt durch den Workspace-Pfad. IR-Snapshots regeneriert,
neuer ws_main-Snapshot. E2E-Test workspace_build_runs_imported_fn
prüft, dass das Binary die importierte Funktion korrekt aufruft.
Schuld #19 (source_filename) durch einheitliches <entry>.ail-Schema
geschlossen.
This commit is contained in:
2026-05-07 11:39:59 +02:00
parent b2878fe655
commit a20ab93c66
11 changed files with 493 additions and 157 deletions
+47 -8
View File
@@ -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)
+9
View File
@@ -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
+9
View File
@@ -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");
}
+5 -5
View File
@@ -1,21 +1,21 @@
; AILang generated module: hello
; AILang generated workspace; entry: hello
source_filename = "hello.ail"
target triple = "<NORMALIZED>"
@.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
}
+8 -8
View File
@@ -1,14 +1,14 @@
; AILang generated module: list
; AILang generated workspace; entry: list
source_filename = "list.ail"
target triple = "<NORMALIZED>"
@.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
}
+8 -8
View File
@@ -1,14 +1,14 @@
; AILang generated module: max3
; AILang generated workspace; entry: max3
source_filename = "max3.ail"
target triple = "<NORMALIZED>"
@.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
}
+8 -8
View File
@@ -1,14 +1,14 @@
; AILang generated module: sum
; AILang generated workspace; entry: sum
source_filename = "sum.ail"
target triple = "<NORMALIZED>"
@.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
}
+28
View File
@@ -0,0 +1,28 @@
; AILang generated workspace; entry: ws_main
source_filename = "ws_main.ail"
target triple = "<NORMALIZED>"
@.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
}
+320 -120
View File
@@ -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_<modul>_<def>`. Das gilt
//! auch für Single-Modul-Programme. Die alte Form `@ail_<def>` entfällt.
//! - Globale String-/Konstanten-Symbole sind pro Modul gemangelt:
//! `@.str_<modul>_<idx>` bzw. `@ail_<modul>_<def>` für Konstanten-Globals.
//! - Eintrittspunkt bleibt `main` (LLVM-/C-ABI). Der Generator emittiert
//! `define i64 @main() { call @ail_<entry-modul>_main() ... }` als
//! Trampoline auf das `main` des Eintrittsmoduls. Fehlt dieses, scheitert
//! der Build mit `MissingEntryMain`.
//! - `source_filename` taucht genau einmal am Anfang auf, mit
//! `<entry-modul>.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<CodegenError>),
#[error("module `{0}`: {1}")]
InModule(String, Box<CodegenError>),
#[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<T> = std::result::Result<T, CodegenError>;
/// 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<String> {
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
/// (`<prefix>.<def>`) wird über die Import-Map des aufrufenden Moduls auf
/// `@ail_<echtes_modul>_<def>` 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<String> {
let mut header = String::new();
let mut body = String::new();
let mut all_strings: BTreeMap<String, Vec<(String, String)>> = 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<String, BTreeMap<String, FnSig>> = 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<Vec<String>> = 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<String, String> = 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_<entry>_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<String, (String, usize)>,
/// 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<String, FnSig>,
/// Top-Level-Funktionen pro Modul des Workspaces, für call-resolution.
module_user_fns: &'a BTreeMap<String, BTreeMap<String, FnSig>>,
/// Import-Map des aktuellen Moduls (Alias/Modulname → echter Modulname).
import_map: BTreeMap<String, String>,
/// 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<Vec<String>> =
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<String, BTreeMap<String, FnSig>>,
import_map: BTreeMap<String, String>,
) -> Self {
let mut types: BTreeMap<String, Vec<CtorInfo>> = BTreeMap::new();
let mut ctor_index: BTreeMap<String, CtorRef> = 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::<Vec<_>>()
.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::<Vec<_>>()
.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_<modul>_<hint>_<idx>`.
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_<modul>_<def>` 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:?}"),
}
}
}
+8
View File
@@ -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_<modul>_<def>` gemangelt — auch im
Single-Modul-Fall. Globale Strings/Konstanten zu `@.str_<modul>_<idx>`
bzw. `@ail_<modul>_<def>`. Eintrittspunkt ist eine `define i64 @main()`-
Trampoline, die `@ail_<entry-modul>_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
+43
View File
@@ -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_<modul>_<def>`, auch in Single-Modul-Programmen. Die alte Form
`@ail_<def>` ist weg. Strings/Const-Globals analog
(`@.str_<modul>_<hint>_<idx>`, `@ail_<modul>_<const>`). Eintrittspunkt
bleibt `main` als C-ABI: ein `define i32 @main()`-Trampoline ruft
`@ail_<entry-modul>_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<String>`
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 `<entry-modul>.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.