iter emit-ir-staticlib: ail emit-ir --emit=staticlib (M1 fieldtest spec_gap#2)
Restores the Decision-5 IR-readability affordance for main-free
kernels: ail emit-ir gains --emit=staticlib, symmetric with
ail build, reusing the M1-audited Target::StaticLib path via a new
one-line lower_workspace_staticlib convenience. Zero-export guard
byte-identical to build_staticlib's. DESIGN.md widened (not
narrowed) + a pre-existing M1 synopsis omission corrected. 3 new
E2E tests; no fixture minted, no doc pin (E2E is the coverage).
Plan 03493c9.
This commit is contained in:
+21
-2
@@ -116,6 +116,14 @@ enum Cmd {
|
||||
path: PathBuf,
|
||||
#[arg(short, long)]
|
||||
out: Option<PathBuf>,
|
||||
/// IR shape. `exe` (default): whole-program IR (requires an
|
||||
/// entry `main`). `staticlib`: kernel IR for a `main`-free
|
||||
/// module — one external C forwarder per `(export "<sym>")`
|
||||
/// fn, no `@main` (Embedding ABI M1). Same `Target` the
|
||||
/// `build --emit=staticlib` path uses; output is the IR text
|
||||
/// (stdout, or `-o <file>`), not archives.
|
||||
#[arg(long, default_value = "exe", value_parser = ["exe", "staticlib"])]
|
||||
emit: String,
|
||||
},
|
||||
/// Full pipeline: check + emit-ir + clang -> binary.
|
||||
Build {
|
||||
@@ -637,7 +645,7 @@ fn main() -> Result<()> {
|
||||
);
|
||||
}
|
||||
}
|
||||
Cmd::EmitIr { path, out } => {
|
||||
Cmd::EmitIr { path, out, emit } => {
|
||||
// Iter 5c: workspace lowering. For single-module programs the
|
||||
// workspace is effectively a trivial workspace with one module.
|
||||
let ws = load_workspace_human(&path)?;
|
||||
@@ -687,7 +695,18 @@ fn main() -> Result<()> {
|
||||
};
|
||||
let ws = ailang_check::monomorphise_workspace(&ws)
|
||||
.map_err(|e| anyhow::anyhow!("monomorphise_workspace: {e}"))?;
|
||||
let ir = ailang_codegen::lower_workspace(&ws)?;
|
||||
let ir = if emit == "staticlib" {
|
||||
let has_export = ws.modules.values().any(|m| m.defs.iter().any(|d|
|
||||
matches!(d, ailang_core::Def::Fn(f) if f.export.is_some())));
|
||||
if !has_export {
|
||||
anyhow::bail!(
|
||||
"staticlib target needs at least one `(export \"<sym>\")` fn"
|
||||
);
|
||||
}
|
||||
ailang_codegen::lower_workspace_staticlib(&ws)?
|
||||
} else {
|
||||
ailang_codegen::lower_workspace(&ws)?
|
||||
};
|
||||
match out {
|
||||
Some(p) => {
|
||||
std::fs::write(&p, ir)?;
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
//! Embedding-ABI M1 (M1 fieldtest spec_gap#2): `ail emit-ir
|
||||
//! --emit=staticlib` prints a `main`-free kernel's LLVM IR (the
|
||||
//! external `@<sym>` forwarders, no `@main`) instead of the
|
||||
//! executable-path `MissingEntryMain` rejection — the Decision-5
|
||||
//! IR-readability affordance for the artefact M1 introduced.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
fn ail_bin() -> &'static str { env!("CARGO_BIN_EXE_ail") }
|
||||
fn ws_root() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent().unwrap().parent().unwrap().to_path_buf()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_ir_staticlib_prints_kernel_forwarder_no_main() {
|
||||
let fixture = ws_root().join("examples").join("embed_backtest_step.ail");
|
||||
let out = Command::new(ail_bin())
|
||||
.args(["emit-ir", fixture.to_str().unwrap(), "--emit=staticlib"])
|
||||
.output().expect("ail emit-ir --emit=staticlib");
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(out.status.success(),
|
||||
"emit-ir --emit=staticlib on a main-free kernel must succeed; stderr={stderr}");
|
||||
assert!(!stderr.contains("has no `main` def"),
|
||||
"must NOT hit the executable-path MissingEntryMain rejection; stderr={stderr}");
|
||||
assert!(stdout.contains("@backtest_step("),
|
||||
"kernel IR must contain the external `(export \"backtest_step\")` forwarder; stdout={stdout}");
|
||||
assert!(stdout.contains("@ail_embed_backtest_step_step"),
|
||||
"kernel IR must contain the internal mangled symbol the forwarder calls; stdout={stdout}");
|
||||
assert!(!stdout.contains("@main("),
|
||||
"staticlib kernel IR must NOT contain an @main trampoline; stdout={stdout}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_ir_staticlib_requires_an_export() {
|
||||
// embed_noentry_baseline has no `(export …)` fn — symmetric with
|
||||
// build's zero-export guard (embed_staticlib_cli.rs).
|
||||
let fixture = ws_root().join("examples").join("embed_noentry_baseline.ail");
|
||||
let out = Command::new(ail_bin())
|
||||
.args(["emit-ir", fixture.to_str().unwrap(), "--emit=staticlib"])
|
||||
.output().expect("ail emit-ir --emit=staticlib");
|
||||
assert!(!out.status.success(),
|
||||
"emit-ir --emit=staticlib with zero exports must fail");
|
||||
assert!(String::from_utf8_lossy(&out.stderr)
|
||||
.contains("at least one `(export"),
|
||||
"expected the zero-export staticlib error; got {}",
|
||||
String::from_utf8_lossy(&out.stderr));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_ir_default_exe_still_requires_main() {
|
||||
// Regression: --emit defaults to "exe"; emit-ir on a main-free
|
||||
// module WITHOUT --emit=staticlib must still hit MissingEntryMain
|
||||
// (the new staticlib branch must not alter the default path).
|
||||
let fixture = ws_root().join("examples").join("embed_backtest_step.ail");
|
||||
let out = Command::new(ail_bin())
|
||||
.args(["emit-ir", fixture.to_str().unwrap()])
|
||||
.output().expect("ail emit-ir");
|
||||
assert!(!out.status.success(),
|
||||
"default-exe emit-ir on a main-free kernel must still fail");
|
||||
assert!(String::from_utf8_lossy(&out.stderr)
|
||||
.contains("has no `main` def"),
|
||||
"default path must still surface MissingEntryMain; got {}",
|
||||
String::from_utf8_lossy(&out.stderr));
|
||||
}
|
||||
@@ -276,6 +276,18 @@ pub fn lower_workspace(ws: &Workspace) -> Result<String> {
|
||||
lower_workspace_inner(ws, AllocStrategy::Gc, Target::Executable)
|
||||
}
|
||||
|
||||
/// Embedding-ABI M1 single-call entry point symmetric with
|
||||
/// [`lower_workspace`]: lowers a [`Workspace`] for the static-library
|
||||
/// target with the default `AllocStrategy::Gc`. This is what
|
||||
/// `ail emit-ir --emit=staticlib` calls so an author can read a
|
||||
/// `main`-free kernel's IR (the external `@<sym>` forwarders, no
|
||||
/// `@main`) — the Decision-5 IR-readability affordance for the
|
||||
/// artefact M1 introduced. Equivalent to
|
||||
/// `lower_workspace_staticlib_with_alloc(ws, AllocStrategy::Gc)`.
|
||||
pub fn lower_workspace_staticlib(ws: &Workspace) -> Result<String> {
|
||||
lower_workspace_inner(ws, AllocStrategy::Gc, Target::StaticLib)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user