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:
+120
-3
@@ -137,6 +137,14 @@ enum Cmd {
|
||||
/// every allocation by design.
|
||||
#[arg(long, default_value = "rc", value_parser = ["gc", "bump", "rc"])]
|
||||
alloc: String,
|
||||
/// Output shape. `exe` (default): whole-program executable
|
||||
/// (requires an entry `main`). `staticlib`: a relocatable
|
||||
/// `lib<entry>.a` (program objects only) plus a separate
|
||||
/// `libailang_rt.a` (RC runtime), with one external C
|
||||
/// entrypoint per `(export "<sym>")` fn and no `@main`
|
||||
/// (Embedding ABI M1). With `staticlib`, `-o` is a directory.
|
||||
#[arg(long, default_value = "exe", value_parser = ["exe", "staticlib"])]
|
||||
emit: String,
|
||||
},
|
||||
/// Build into a tempdir and execute. Exits with the binary's exit code.
|
||||
/// Convenience wrapper around `build` + invocation of the resulting
|
||||
@@ -688,10 +696,15 @@ fn main() -> Result<()> {
|
||||
None => print!("{ir}"),
|
||||
}
|
||||
}
|
||||
Cmd::Build { path, out, opt, alloc } => {
|
||||
Cmd::Build { path, out, opt, alloc, emit } => {
|
||||
let strategy = parse_alloc_strategy(&alloc)?;
|
||||
let bin = build_to(&path, out, &opt, strategy)?;
|
||||
eprintln!("built {}", bin.display());
|
||||
if emit == "staticlib" {
|
||||
let (lib, rt) = build_staticlib(&path, out, &opt, strategy)?;
|
||||
eprintln!("built {} + {}", lib.display(), rt.display());
|
||||
} else {
|
||||
let bin = build_to(&path, out, &opt, strategy)?;
|
||||
eprintln!("built {}", bin.display());
|
||||
}
|
||||
}
|
||||
Cmd::Run { path, opt, alloc, args } => {
|
||||
// Iter 9b: build into a fresh tempdir per run, exec, propagate
|
||||
@@ -2404,6 +2417,110 @@ fn build_to(
|
||||
Ok(out_bin)
|
||||
}
|
||||
|
||||
/// Embedding-ABI M1: `ail build --emit=staticlib`. Produces
|
||||
/// `<outdir>/lib<entry>.a` (program objects only) and a separate
|
||||
/// `<outdir>/libailang_rt.a` (RC runtime: rc.c + str.c). No
|
||||
/// executable, no `@main`. `out` is the output directory.
|
||||
fn build_staticlib(
|
||||
path: &Path,
|
||||
out: Option<PathBuf>,
|
||||
opt: &str,
|
||||
alloc: ailang_codegen::AllocStrategy,
|
||||
) -> Result<(PathBuf, PathBuf)> {
|
||||
// ---- shared front matter: identical to build_to:2239-2294 ----
|
||||
let ws = load_workspace_human(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,
|
||||
);
|
||||
}
|
||||
if diags.iter().any(|d| matches!(d.severity, ailang_check::Severity::Error)) {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
let mut lifted_modules = std::collections::BTreeMap::new();
|
||||
for (mname, m) in &ws.modules {
|
||||
let desugared = ailang_core::desugar::desugar_module(m);
|
||||
let lifted = ailang_check::lift_letrecs(&desugared)
|
||||
.map_err(|e| anyhow::anyhow!("lift_letrecs in module `{mname}`: {e}"))?;
|
||||
lifted_modules.insert(mname.clone(), lifted);
|
||||
}
|
||||
let ws = ailang_core::Workspace {
|
||||
entry: ws.entry.clone(),
|
||||
modules: lifted_modules,
|
||||
root_dir: ws.root_dir.clone(),
|
||||
registry: ws.registry.clone(),
|
||||
};
|
||||
let ws = ailang_check::monomorphise_workspace(&ws)
|
||||
.map_err(|e| anyhow::anyhow!("monomorphise_workspace: {e}"))?;
|
||||
|
||||
// ---- staticlib-specific: require ≥1 export, then lower ----
|
||||
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"
|
||||
);
|
||||
}
|
||||
let ir = ailang_codegen::lower_workspace_staticlib_with_alloc(&ws, alloc)?;
|
||||
|
||||
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", ws.entry));
|
||||
std::fs::write(&ll_path, &ir)?;
|
||||
|
||||
let outdir = out.unwrap_or_else(|| PathBuf::from("."));
|
||||
std::fs::create_dir_all(&outdir)?;
|
||||
|
||||
// program object → lib<entry>.a (program objects ONLY — no rt)
|
||||
let prog_obj = tmpdir.join(format!("{}.o", ws.entry));
|
||||
run_cmd("clang", &[opt, "-c", "-o",
|
||||
prog_obj.to_str().unwrap(), ll_path.to_str().unwrap()],
|
||||
"compiling program .ll")?;
|
||||
let prog_archive = outdir.join(format!("lib{}.a", ws.entry));
|
||||
let _ = std::fs::remove_file(&prog_archive);
|
||||
run_cmd("ar", &["rcs", prog_archive.to_str().unwrap(),
|
||||
prog_obj.to_str().unwrap()], "archiving lib<entry>.a")?;
|
||||
|
||||
// RC runtime → libailang_rt.a (rc.c + str.c), separate archive
|
||||
let rc_src = locate_rc_runtime()?;
|
||||
let str_src = locate_str_runtime()?;
|
||||
let rc_obj = tmpdir.join("rc.o");
|
||||
let str_obj = tmpdir.join("str.o");
|
||||
run_cmd("clang", &["-O2", "-c", "-o",
|
||||
rc_obj.to_str().unwrap(), rc_src.to_str().unwrap()],
|
||||
"compiling runtime/rc.c")?;
|
||||
run_cmd("clang", &["-O2", "-c", "-o",
|
||||
str_obj.to_str().unwrap(), str_src.to_str().unwrap()],
|
||||
"compiling runtime/str.c")?;
|
||||
let rt_archive = outdir.join("libailang_rt.a");
|
||||
let _ = std::fs::remove_file(&rt_archive);
|
||||
run_cmd("ar", &["rcs", rt_archive.to_str().unwrap(),
|
||||
rc_obj.to_str().unwrap(), str_obj.to_str().unwrap()],
|
||||
"archiving libailang_rt.a")?;
|
||||
|
||||
Ok((prog_archive, rt_archive))
|
||||
}
|
||||
|
||||
/// Run an external command, bailing with stderr on non-zero exit.
|
||||
fn run_cmd(bin: &str, args: &[&str], ctx: &str) -> Result<()> {
|
||||
let status = std::process::Command::new(bin)
|
||||
.args(args).status().context(ctx.to_string())?;
|
||||
if !status.success() {
|
||||
anyhow::bail!("{ctx}: `{bin}` exited {status}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ct.1 migration helper: rewrite bare cross-module Type::Con names
|
||||
/// and Term::Ctor.type_name to their qualified form. Sets
|
||||
/// `*changed` to `true` if any rewrite happened. Uses the
|
||||
|
||||
Reference in New Issue
Block a user