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:
+47
-8
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user