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