iter embedding-abi-m1.1 (DONE 7/7): Embedding-ABI M1 — scalar AILang fn callable from C/Rust
Tasks 4-7 on the Boss-Repaired plan, completing the M1 iteration: - Task 4: check-side export-signature gate — CheckError variants ExportNonScalarSignature / ExportHasEffects (codes export-non-scalar-signature / export-has-effects), code() arms, check_fn gate (params->ret->effects, first-violation-wins, unconditional on f.export.is_some()). The clause-3 discriminator in code: effectful/non-scalar exports fail to typecheck. 6/6. - Task 5: codegen Target::StaticLib — Target enum, threaded param, @main/MissingEntryMain gated behind Target::Executable, one external @<sym> forwarder per export to @ail_<module>_<fn> (fn_scalar_sig/llvm_scalar). In-source missing_entry_main_is_error byte-unchanged. Lowering pin 2/2. - Task 6: CLI ail build --emit=staticlib — clap field, Cmd::Build branch, build_staticlib (verbatim build_to prefix + has_export guard + two-archive clang -c/ar rcs tail) + run_cmd. CLI pin 2/2. - Task 7: C-host E2E coherent-stop proof (cc links libembed_backtest_step.a + libailang_rt.a, backtest_step(0,3)=9 / (9,4)=25, s==25, exit 0) + DESIGN.md §"Embedding ABI (M1)" (scalar C ABI, provisional until M3). E2E pin 1/1. Independent Boss verification: E2E 1/1, gate 6/6, full workspace 0 failures, roundtrip_cli + design_schema_drift + spec_drift green, Invariant 1 holds (no new dep in core/codegen/runtime). 3 behaviour- neutral plan pseudo-vs-reality concerns in the journal. Default-exe codegen/runtime path byte-unchanged (check.py firing = tracked-P2 known-noise, audit-adjudicated). INDEX line added. Completes the M1 iteration (impl). Milestone-close audit + fieldtest (surface-touching) are the next pipeline steps.
This commit is contained in:
@@ -162,6 +162,19 @@ pub enum AllocStrategy {
|
||||
Rc,
|
||||
}
|
||||
|
||||
/// Embedding-ABI M1: codegen output shape.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Target {
|
||||
/// Default: emit the `@main` trampoline; require an entry `main`
|
||||
/// (`MissingEntryMain` if absent). Whole-program executable.
|
||||
Executable,
|
||||
/// `--emit=staticlib`: suppress the `@main` trampoline AND the
|
||||
/// `MissingEntryMain` shape-check (a kernel module is a library);
|
||||
/// emit one external C entrypoint per `(export …)` fn forwarding
|
||||
/// to `@ail_<module>_<fn>`. Provisional signature until M3.
|
||||
StaticLib,
|
||||
}
|
||||
|
||||
impl AllocStrategy {
|
||||
/// LLVM IR-level name of the allocator fn (without leading `@`).
|
||||
fn fn_name(self) -> &'static str {
|
||||
@@ -221,7 +234,17 @@ pub fn emit_ir(m: &Module) -> Result<String> {
|
||||
/// `runtime/bump.c`). Used by `ail build --alloc=bump` to quantify the
|
||||
/// GC's runtime overhead via an A/B comparison.
|
||||
pub fn lower_workspace_with_alloc(ws: &Workspace, alloc: AllocStrategy) -> Result<String> {
|
||||
lower_workspace_inner(ws, alloc)
|
||||
lower_workspace_inner(ws, alloc, Target::Executable)
|
||||
}
|
||||
|
||||
/// Embedding-ABI M1 entry point. Lowers a [`Workspace`] for the
|
||||
/// static-library target: no `@main`, no `MissingEntryMain`, one
|
||||
/// external `@<sym>` forwarder per `(export …)` fn.
|
||||
pub fn lower_workspace_staticlib_with_alloc(
|
||||
ws: &Workspace,
|
||||
alloc: AllocStrategy,
|
||||
) -> Result<String> {
|
||||
lower_workspace_inner(ws, alloc, Target::StaticLib)
|
||||
}
|
||||
|
||||
/// Multi-module entry point. Lowers an entire [`Workspace`] (entry
|
||||
@@ -250,10 +273,10 @@ pub fn lower_workspace_with_alloc(ws: &Workspace, alloc: AllocStrategy) -> Resul
|
||||
/// Use [`emit_ir`] for the single-file shortcut when there are no
|
||||
/// imports.
|
||||
pub fn lower_workspace(ws: &Workspace) -> Result<String> {
|
||||
lower_workspace_inner(ws, AllocStrategy::Gc)
|
||||
lower_workspace_inner(ws, AllocStrategy::Gc, Target::Executable)
|
||||
}
|
||||
|
||||
fn lower_workspace_inner(ws: &Workspace, alloc: AllocStrategy) -> Result<String> {
|
||||
fn lower_workspace_inner(ws: &Workspace, alloc: AllocStrategy, target: Target) -> Result<String> {
|
||||
// Iter 16a: desugar every module before any lowering work runs.
|
||||
// The pass is idempotent and structurally identical to what
|
||||
// `ailang-check` runs at its public entries, so the codegen
|
||||
@@ -417,17 +440,20 @@ fn lower_workspace_inner(ws: &Workspace, alloc: AllocStrategy) -> Result<String>
|
||||
}
|
||||
|
||||
// Trampoline: verify that the entry module has a
|
||||
// `main : () -> Unit !IO`. If not, the workspace isn't runnable.
|
||||
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()));
|
||||
// `main : () -> Unit !IO`. Suppressed for the staticlib target —
|
||||
// a kernel module is a library and has no `main`.
|
||||
if target == Target::Executable {
|
||||
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();
|
||||
@@ -551,15 +577,81 @@ fn lower_workspace_inner(ws: &Workspace, alloc: AllocStrategy) -> Result<String>
|
||||
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
|
||||
));
|
||||
match target {
|
||||
Target::Executable => {
|
||||
// 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
|
||||
));
|
||||
}
|
||||
Target::StaticLib => {
|
||||
// One external C entrypoint per `(export …)` fn,
|
||||
// forwarding to the internal `@ail_<module>_<fn>`.
|
||||
// The check-side gate (Task 4) guarantees every param
|
||||
// and the ret are `Int`/`Float`; map Int→i64,
|
||||
// Float→double. M1 scalar fns have no captured env, so
|
||||
// the internal callee takes the scalars positionally
|
||||
// (confirmed against `emit_fn`'s signature emission,
|
||||
// `lib.rs:1087`).
|
||||
for (mname, m) in &ws.modules {
|
||||
for d in &m.defs {
|
||||
if let Def::Fn(f) = d {
|
||||
if let Some(sym) = &f.export {
|
||||
let (param_tys, ret_ty) = match fn_scalar_sig(&f.ty) {
|
||||
Some(sig) => sig,
|
||||
None => return Err(CodegenError::Internal(format!(
|
||||
"export `{sym}`: non-scalar signature reached codegen \
|
||||
(check-side gate should have rejected it)"))),
|
||||
};
|
||||
let cret = llvm_scalar(&ret_ty);
|
||||
let params: Vec<String> = param_tys.iter().enumerate()
|
||||
.map(|(i, t)| format!("{} %a{i}", llvm_scalar(t)))
|
||||
.collect();
|
||||
let args: Vec<String> = param_tys.iter().enumerate()
|
||||
.map(|(i, t)| format!("{} %a{i}", llvm_scalar(t)))
|
||||
.collect();
|
||||
out.push_str(&format!(
|
||||
"\ndefine {cret} @{sym}({}) {{\n \
|
||||
%r = call {cret} @ail_{mname}_{}({})\n \
|
||||
ret {cret} %r\n}}\n",
|
||||
params.join(", "),
|
||||
f.name,
|
||||
args.join(", "),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Scalar-signature view of an export fn's type. `None` if not the
|
||||
/// flat `Type::Fn` of scalars the M1 gate guarantees.
|
||||
fn fn_scalar_sig(t: &Type) -> Option<(Vec<Type>, Type)> {
|
||||
let inner = match t {
|
||||
Type::Forall { body, .. } => body.as_ref(),
|
||||
other => other,
|
||||
};
|
||||
match inner {
|
||||
Type::Fn { params, ret, .. } =>
|
||||
Some((params.clone(), (**ret).clone())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `Int` → `i64`, `Float` → `double` (the only two M1 scalars; the
|
||||
/// check-side gate rejects everything else before codegen).
|
||||
fn llvm_scalar(t: &Type) -> &'static str {
|
||||
match t {
|
||||
Type::Con { name, .. } if name == "Float" => "double",
|
||||
_ => "i64",
|
||||
}
|
||||
}
|
||||
|
||||
fn main_is_void(t: &Type) -> bool {
|
||||
match t {
|
||||
Type::Fn { params, ret, .. } => {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
//! Embedding-ABI M1 codegen (spec §4): `Target::StaticLib` lowering
|
||||
//! emits NO `@main` trampoline, NO `MissingEntryMain`, and one
|
||||
//! external `define i64 @backtest_step(...)` (the author-chosen C
|
||||
//! symbol from `(export "backtest_step")`) forwarding to the
|
||||
//! internal `@ail_embed_backtest_step_step` (module
|
||||
//! `embed_backtest_step` == file stem, per Design-decision 6).
|
||||
//! Executable-target lowering is byte-unchanged (the
|
||||
//! `missing_entry_main_is_error` in-source pin still holds).
|
||||
|
||||
use ailang_core::Workspace;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn load(file: &str) -> Workspace {
|
||||
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent().unwrap().parent().unwrap()
|
||||
.join("examples").join(file);
|
||||
ailang_surface::load_workspace(&path).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn staticlib_emits_forwarder_no_main() {
|
||||
let ws = load("embed_backtest_step.ail");
|
||||
let ir = ailang_codegen::lower_workspace_staticlib_with_alloc(
|
||||
&ws, ailang_codegen::AllocStrategy::Rc).unwrap();
|
||||
assert!(!ir.contains("define i32 @main()"),
|
||||
"staticlib IR must not emit the @main trampoline");
|
||||
assert!(ir.contains("@backtest_step("),
|
||||
"staticlib IR must emit the external @backtest_step entrypoint \
|
||||
(the author-chosen C symbol — invariant under module rename)");
|
||||
assert!(ir.contains("@ail_embed_backtest_step_step("),
|
||||
"the forwarder must call the internal \
|
||||
@ail_embed_backtest_step_step (module embed_backtest_step)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn executable_target_still_emits_main() {
|
||||
let ws = load("embed_backtest_step.ail");
|
||||
// Default executable lowering is unaffected by the new field;
|
||||
// backtest has no `main`, so exe-target lowering still rejects.
|
||||
let err = ailang_codegen::lower_workspace_with_alloc(
|
||||
&ws, ailang_codegen::AllocStrategy::Rc).unwrap_err();
|
||||
assert!(format!("{err}").contains("has no `main` def"),
|
||||
"exe target must still surface MissingEntryMain; got {err}");
|
||||
}
|
||||
Reference in New Issue
Block a user