luna_aot/embed.rs
1// CARVE-OUT: pre-existing god file, shrinking on every touch
2//! The AOT pipeline:
3//! parse + compile Lua source → dump luna bytecode → embed into an
4//! object file's `.luna.bytecode` data section → link.
5//!
6//! [`embed_bytecode`] is the **scaffold** path: it ends after the
7//! bytecode is in a `.luna.bytecode` section bracketed by the two
8//! public symbols [`crate::BYTECODE_START_SYMBOL`] and
9//! [`crate::BYTECODE_END_SYMBOL`], linked against a C entry that only
10//! prints the section size. [`compile_and_link`] is the full path: it
11//! also links the `luna-runtime-helpers` staticlib and any AOT trace
12//! mcode harvested from a warmup run.
13//!
14//! # Scaffold pipeline
15//!
16//! ```text
17//! foo.lua
18//! │ luna_core::frontend::parser::parse
19//! ▼
20//! Chunk (AST)
21//! │ luna_core::compiler::compile_chunk
22//! ▼
23//! Gc<Proto> (bytecode tree)
24//! │ luna_core::vm::dump::dump
25//! ▼
26//! Vec<u8> ── luna body, "\x1bLua" + dialect header + "\x00LunaV1\x00" sentinel + body
27//! │ object::write::Object (this module)
28//! ▼
29//! foo.luna_bytecode.o (ELF / Mach-O / PE — host triple)
30//! │ cc foo.luna_bytecode.o entry_stub.o -o foo
31//! ▼
32//! foo (native binary; scaffold entry prints the section size)
33//! ```
34
35use std::fs;
36use std::io;
37use std::path::{Path, PathBuf};
38use std::process::Command;
39
40use object::write::{Object, Symbol, SymbolSection};
41use object::{Architecture, BinaryFormat, Endianness, SectionKind, SymbolKind, SymbolScope};
42
43use luna_core::compiler::compile_chunk;
44use luna_core::frontend::parser::parse;
45use luna_core::jit::aot_meta::{
46 AotTraceMetaHeader, PerExitInlineEntry, PerExitTagsEntry, encode_meta_blob, pack_exit_tag,
47 pack_tag_res_kind,
48};
49use luna_core::jit::trace_types::{CompileOptions, CompiledTrace, TraceRecord};
50use luna_core::runtime::Heap;
51use luna_core::runtime::Value;
52use luna_core::version::LuaVersion;
53use luna_core::vm::dump;
54
55use crate::{BYTECODE_END_SYMBOL, BYTECODE_SECTION_NAME, BYTECODE_START_SYMBOL};
56
57mod msvc_link;
58use msvc_link::link_aot_binary_msvc;
59
60/// Errors surfaced by the AOT pipeline. Variants intentionally carry
61/// the upstream message verbatim so the CLI can pass it through to
62/// `stderr` without re-formatting (and so future structured-error
63/// consumers can match on the variant tag).
64#[derive(Debug)]
65pub enum AotError {
66 /// Reading the Lua source file failed (missing, permission, ...).
67 Io(io::Error),
68 /// Parser or compiler rejected the source (PUC-style line/message).
69 Syntax(String),
70 /// Object-file emission failed (unsupported target triple,
71 /// internal `object`-crate error).
72 Object(String),
73 /// Linker (`cc` / user-supplied driver) failed. Carries the
74 /// linker's stderr verbatim so users can diagnose toolchain
75 /// issues without re-running.
76 Link(String),
77 /// The target triple isn't supported. The scaffold path rejects
78 /// anything other than the host triple; [`compile_and_link`]
79 /// rejects triples [`TargetSpec::from_triple`] can't describe.
80 UnsupportedTarget(String),
81}
82
83impl std::fmt::Display for AotError {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 match self {
86 AotError::Io(e) => write!(f, "io error: {e}"),
87 AotError::Syntax(msg) => write!(f, "syntax error: {msg}"),
88 AotError::Object(msg) => write!(f, "object-file emission failed: {msg}"),
89 AotError::Link(msg) => write!(f, "linker failed: {msg}"),
90 AotError::UnsupportedTarget(t) => {
91 write!(f, "unsupported target triple in scaffold session: {t}")
92 }
93 }
94 }
95}
96
97impl std::error::Error for AotError {
98 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
99 match self {
100 AotError::Io(e) => Some(e),
101 _ => None,
102 }
103 }
104}
105
106impl From<io::Error> for AotError {
107 fn from(e: io::Error) -> Self {
108 AotError::Io(e)
109 }
110}
111
112/// Compile `source_path` into `out_path` (a native binary embedding
113/// the dumped luna bytecode in a `.luna.bytecode` section).
114///
115/// `target_triple` is parsed only for the host-vs-cross check; the
116/// scaffold only supports the host triple. Pass `None` to default to
117/// the host.
118///
119/// `version` selects the Lua dialect for parsing + bytecode emit
120/// (defaults to [`LuaVersion::Lua55`] when called via the CLI).
121///
122/// # End-to-end behaviour
123///
124/// The produced binary is **runnable**: it prints the embedded
125/// bytecode length to `stderr` and exits 0. It does not execute the
126/// bytecode; use [`compile_and_link`] for a binary that runs the
127/// script through a `Vm`.
128pub fn embed_bytecode(
129 source_path: &Path,
130 out_path: &Path,
131 target_triple: Option<&str>,
132 version: LuaVersion,
133) -> Result<(), AotError> {
134 if let Some(t) = target_triple
135 && t != host_triple()
136 {
137 return Err(AotError::UnsupportedTarget(t.to_string()));
138 }
139
140 // source → AST → Proto. Uses the same path the runtime
141 // `Vm::load` walks (`luna-core/src/vm/exec.rs:1298`).
142 let src = fs::read(source_path)?;
143 let ast = parse(&src, version).map_err(|e| {
144 AotError::Syntax(format!(
145 "{}:{}: {}",
146 source_path.display(),
147 e.line,
148 String::from_utf8_lossy(&e.msg)
149 ))
150 })?;
151
152 // Build-host Heap. Only used for compile_chunk's interning; we
153 // drop it after dumping (the Proto's interned strings are
154 // serialised into the dump bytes, the heap itself isn't needed
155 // beyond this scope).
156 let mut heap = Heap::new();
157 let chunk_name = source_path
158 .file_name()
159 .and_then(|n| n.to_str())
160 .unwrap_or("aot-chunk")
161 .as_bytes()
162 .to_vec();
163 let proto = compile_chunk(&ast, version, &chunk_name, &mut heap).map_err(|e| {
164 AotError::Syntax(format!(
165 "{}:{}: {}",
166 source_path.display(),
167 e.line,
168 String::from_utf8_lossy(&e.msg)
169 ))
170 })?;
171
172 // `Gc<T>` implements `Deref<Target = T>`; the `&*proto` reborrow
173 // is safe for the same reason `Vm::load` (`exec.rs:1267-1274`)
174 // takes the proto reference for `undump` immediately after
175 // construction — single-threaded heap, no concurrent mutator.
176 let dump_bytes = dump::dump(&proto, false, version);
177
178 // Write the bytecode object file.
179 let workdir = out_path
180 .parent()
181 .map(Path::to_path_buf)
182 .unwrap_or_else(|| PathBuf::from("."));
183 let stem = out_path
184 .file_name()
185 .and_then(|n| n.to_str())
186 .unwrap_or("luna_aot");
187 let bytecode_obj_path = workdir.join(format!("{stem}.luna_bytecode.o"));
188 let stub_obj_path = workdir.join(format!("{stem}.luna_stub.o"));
189
190 write_bytecode_object(&dump_bytes, &bytecode_obj_path)?;
191 write_scaffold_entry_object(&stub_obj_path)?;
192
193 // Link via system `cc`. The scaffold uses a minimal C entry that
194 // references the bracket symbols (proves the section is reachable
195 // end-to-end).
196 link_with_cc(&[&bytecode_obj_path, &stub_obj_path], out_path)?;
197
198 Ok(())
199}
200
201/// Emit `.o` containing a single `.luna.bytecode` data section with
202/// the dump bytes, bracketed by two **global** symbols
203/// `__luna_bytecode_start` / `__luna_bytecode_end`. Mach-O symbols
204/// are prefixed with `_` so the C linker resolves the bare names.
205fn write_bytecode_object(bytecode: &[u8], out: &Path) -> Result<(), AotError> {
206 let (format, arch, endian) = host_object_target();
207 let mut obj = Object::new(format, arch, endian);
208
209 // Single read-only data section. We avoid `StandardSection::ReadOnlyData`
210 // (which would land us in `.rodata` / `__DATA,__const`) so the section
211 // name is preserved verbatim and `objdump -j .luna.bytecode` finds it.
212 let section_id = obj.add_section(
213 Vec::new(),
214 BYTECODE_SECTION_NAME.as_bytes().to_vec(),
215 SectionKind::ReadOnlyData,
216 );
217
218 // append section data first, then point the start symbol at offset 0
219 let _start_offset = obj.append_section_data(section_id, bytecode, 1);
220
221 // The `object` crate auto-prefixes Mach-O global symbols with `_`
222 // per `Mangling::global_prefix`.
223 // We pass the bare name; the output `.o` ends up with the correct
224 // per-format mangling.
225 let _ = format; // marker for the per-format mangling discussed above
226 let start_name = BYTECODE_START_SYMBOL.to_string();
227 let end_name = BYTECODE_END_SYMBOL.to_string();
228
229 let _start_sym = obj.add_symbol(Symbol {
230 name: start_name.into_bytes(),
231 value: 0,
232 size: 0,
233 kind: SymbolKind::Data,
234 // `Dynamic` exposes the symbol as a regular `N_EXT` extern on
235 // Mach-O / a global on ELF. `Linkage` would add `N_PEXT` on
236 // Mach-O (private extern → `.hidden`), which the static linker
237 // can't resolve from another object file.
238 scope: SymbolScope::Dynamic,
239 weak: false,
240 section: SymbolSection::Section(section_id),
241 flags: object::SymbolFlags::None,
242 });
243 let _end_sym = obj.add_symbol(Symbol {
244 name: end_name.into_bytes(),
245 value: bytecode.len() as u64,
246 size: 0,
247 kind: SymbolKind::Data,
248 // `Dynamic` exposes the symbol as a regular `N_EXT` extern on
249 // Mach-O / a global on ELF. `Linkage` would add `N_PEXT` on
250 // Mach-O (private extern → `.hidden`), which the static linker
251 // can't resolve from another object file.
252 scope: SymbolScope::Dynamic,
253 weak: false,
254 section: SymbolSection::Section(section_id),
255 flags: object::SymbolFlags::None,
256 });
257
258 let bytes = obj
259 .write()
260 .map_err(|e| AotError::Object(format!("Object::write: {e}")))?;
261 fs::write(out, bytes)?;
262 Ok(())
263}
264
265/// Emit a tiny C-style entry-point object that references the bytecode
266/// bracket symbols and prints the embedded length to stderr.
267///
268/// This is the **scaffold runtime**. [`compile_and_link`] links the
269/// real runtime instead, which constructs a `Vm`, loads the embedded
270/// bytecode, and runs it.
271fn write_scaffold_entry_object(out: &Path) -> Result<(), AotError> {
272 // Generate a C source file in a tempfile, then invoke `cc -c` to
273 // produce the `.o`. This is simpler than hand-rolling the entry
274 // point in `object` (which would mean writing per-arch assembly
275 // for `main`'s ABI).
276 let c_src = format!(
277 r#"#include <stdio.h>
278#include <stdint.h>
279
280extern uint8_t __luna_bytecode_start[];
281extern uint8_t __luna_bytecode_end[];
282
283int main(int argc, char **argv) {{
284 size_t len = (size_t)(__luna_bytecode_end - __luna_bytecode_start);
285 fprintf(stderr,
286 "luna-aot scaffold: embedded bytecode length = %zu bytes (section %s)\n"
287 " (interp dispatch wiring is a follow-up session)\n",
288 len, "{section}");
289 (void)argc; (void)argv;
290 return 0;
291}}
292"#,
293 section = BYTECODE_SECTION_NAME
294 );
295
296 let mut c_path = out.to_path_buf();
297 c_path.set_extension("c");
298 fs::write(&c_path, c_src)?;
299
300 let cc = std::env::var("CC").unwrap_or_else(|_| "cc".into());
301 let status = Command::new(&cc)
302 .arg("-c")
303 .arg(&c_path)
304 .arg("-o")
305 .arg(out)
306 .output()
307 .map_err(|e| AotError::Link(format!("spawn {cc}: {e}")))?;
308 if !status.status.success() {
309 return Err(AotError::Link(format!(
310 "cc -c {} failed (exit {:?}):\n{}",
311 c_path.display(),
312 status.status.code(),
313 String::from_utf8_lossy(&status.stderr)
314 )));
315 }
316 // best-effort: leave the .c around for diagnosis
317 Ok(())
318}
319
320/// Invoke `cc` (or `$CC`) to link object files into `out_path`.
321fn link_with_cc(objects: &[&Path], out_path: &Path) -> Result<(), AotError> {
322 let cc = std::env::var("CC").unwrap_or_else(|_| "cc".into());
323 let mut cmd = Command::new(&cc);
324 for obj in objects {
325 cmd.arg(obj);
326 }
327 cmd.arg("-o").arg(out_path);
328 let output = cmd
329 .output()
330 .map_err(|e| AotError::Link(format!("spawn {cc}: {e}")))?;
331 if !output.status.success() {
332 return Err(AotError::Link(format!(
333 "cc link failed (exit {:?}):\n{}",
334 output.status.code(),
335 String::from_utf8_lossy(&output.stderr)
336 )));
337 }
338 Ok(())
339}
340
341// ────────────────────────────────────────────────────────────────────
342// Interp-runtime link path
343//
344// This is the "real" deploy shape: the produced binary embeds the
345// bytecode, links against the `luna-runtime-helpers` staticlib (which
346// bundles luna-core + rust stdlib), and runs the embedded chunk
347// through a `Vm` at process start.
348//
349// Pipeline:
350//
351// 1. Parse + compile + dump (shared with `embed_bytecode`).
352// 2. Write `.luna.bytecode` object (shared).
353// 3. Build `libluna_runtime_helpers.a` via `cargo build -p
354// luna-runtime-helpers --release` (idempotent — cargo caches).
355// 4. Write a tiny C `main.c` that extern-decls the bracket symbols
356// + extern-decls `luna_aot_run`, then calls
357// `luna_aot_run(start, end - start)`. Compile via `cc -c`.
358// 5. Link bytecode.o + main.o + libluna_runtime_helpers.a +
359// platform libs (`-lpthread -ldl -lm -framework CoreFoundation`
360// on Mac) into the final binary.
361//
362// Cranelift trace mcode emission is a separate concern — it adds a
363// third object file (containing the lowered traces) to the
364// link line. The interp-runtime fallback path lives in the staticlib
365// either way, so adding the trace.o is purely additive.
366// ────────────────────────────────────────────────────────────────────
367
368/// End-to-end AOT compile: produces a self-contained binary that, when
369/// run, loads the embedded bytecode through a luna `Vm` and executes
370/// it. Traces harvested from a warmup run are linked in as AOT mcode.
371///
372/// Differs from [`embed_bytecode`]:
373/// - Builds and links `luna-runtime-helpers` (staticlib carrying
374/// luna-core + a `luna_aot_run` C-ABI entry).
375/// - Produced binary actually **runs** the script — `print(...)` lands
376/// on stdout, runtime errors print to stderr + exit 1, etc.
377///
378/// `target_triple` selects a cross target via [`TargetSpec::from_triple`];
379/// cross-compile builds a per-triple staticlib (`cargo build
380/// --target=<triple> -p luna-runtime-helpers`) and uses the matching
381/// cc driver.
382pub fn compile_and_link(
383 source_path: &Path,
384 out_path: &Path,
385 target_triple: Option<&str>,
386 version: LuaVersion,
387) -> Result<(), AotError> {
388 // Resolve the target: explicit triple if supplied, else host. Anything
389 // we can't describe (object-file format, cc invocation, lib set)
390 // surfaces as `UnsupportedTarget` here — no silent fallback to host.
391 let target = match target_triple {
392 Some(t) => TargetSpec::from_triple(t)?,
393 None => TargetSpec::host(),
394 };
395
396 // Parse + compile + dump: shared with `embed_bytecode`.
397 let dump_bytes = compile_to_dump(source_path, version)?;
398
399 let workdir = out_path
400 .parent()
401 .map(Path::to_path_buf)
402 .unwrap_or_else(|| PathBuf::from("."));
403 let stem = out_path
404 .file_name()
405 .and_then(|n| n.to_str())
406 .unwrap_or("luna_aot");
407 let bytecode_obj_path = workdir.join(format!("{stem}.luna_bytecode.o"));
408 let cmain_obj_path = workdir.join(format!("{stem}.luna_cmain.o"));
409
410 // Bytecode object — target-aware format/arch.
411 write_bytecode_object_for(&dump_bytes, &bytecode_obj_path, &target)?;
412
413 // Tiny C main that calls into the staticlib. The C source
414 // is target-independent (extern decls only); the `cc -c` invocation
415 // routes through the target-aware cc driver so the .o has the right
416 // ABI / object-format magic.
417 write_aot_cmain_object_for(&cmain_obj_path, &target)?;
418
419 // Offline trace recorder + AOT trace mcode emission. The warmup `Vm` always runs on the **host**
420 // (we can't dispatch target mcode at warmup time), but the
421 // trace-mcode `.o` we emit is keyed off `TargetSpec`:
422 //
423 // - `TargetSpec::cranelift_isa_builder()` resolves the right
424 // Cranelift `TargetIsa` (`x86_64`, `aarch64`, etc.) so the
425 // `ObjectModule` codegens for the deploy ABI, not the host's.
426 // - The recorded `TraceRecord`s are luna-IR-level (op + guard +
427 // reg moves); pointer-width / endianness are encoded as IR
428 // types (`I64` / little-endian) which are stable across every
429 // target in our tier set. So the same `TraceRecord` re-lowers
430 // correctly for any cross target.
431 //
432 // Returns Ok(None) when no traces close (small / non-loopy source)
433 // or the target Cranelift backend isn't compiled in (resolved by
434 // the `cranelift-codegen = { features = ["all-arch"] }` dep, but
435 // we self-skip rather than panic if it ever shrinks).
436 let traces_obj_path = {
437 let path = workdir.join(format!("{stem}.luna_traces.o"));
438 match harvest_and_emit_aot_traces(&dump_bytes, version, &path, &target)? {
439 HarvestedTraces::None => None,
440 HarvestedTraces::Some => Some(path),
441 }
442 };
443
444 // Ensure the runtime staticlib exists for `target`.
445 // For the host triple this is a workspace cargo build; for a cross
446 // triple it's `cargo build --target=<triple>` and the resulting
447 // staticlib lives under `target/<triple>/release-aot-helpers/`.
448 // The `release-aot-helpers` profile (workspace `Cargo.toml`) has
449 // `lto = "off"` so the 39 `luna_jit_*` Cranelift trace-mcode
450 // helpers survive the rlib → staticlib bundling step.
451 let staticlib = build_runtime_helpers_staticlib(target.triple_for_cargo())?;
452
453 // Final link via the target's cc driver. Order matters on
454 // some toolchains: bytecode + main first (they reference symbols
455 // from the staticlib), then the staticlib, then system libs.
456 link_aot_binary_for(
457 &bytecode_obj_path,
458 &cmain_obj_path,
459 traces_obj_path.as_deref(),
460 &staticlib,
461 out_path,
462 &target,
463 )?;
464
465 Ok(())
466}
467
468/// Return shape for [`harvest_and_emit_aot_traces`]. `None` = warmup recorded zero
469/// dispatchable traces (small / non-loopy source); `Some` = at least
470/// one trace .o was written.
471enum HarvestedTraces {
472 /// Warmup didn't produce any dispatchable traces. Pipeline
473 /// continues without a trace `.o` on the link line; AOT binary
474 /// runs through interp + runtime JIT only.
475 None,
476 /// Trace `.o` was written at the path passed in. Link step adds it
477 /// to the cc invocation; deploy walker installs the contained
478 /// traces at startup.
479 Some,
480}
481
482/// Convenience wrapper for callers that want explicit host-target builds.
483pub fn compile_and_link_host(
484 source_path: &Path,
485 out_path: &Path,
486 version: LuaVersion,
487) -> Result<(), AotError> {
488 compile_and_link(source_path, out_path, None, version)
489}
490
491/// Parse + compile and produce the dump bytes the
492/// bytecode object holds. Factored out so [`embed_bytecode`] and
493/// [`compile_and_link`] share the front-end exactly.
494fn compile_to_dump(source_path: &Path, version: LuaVersion) -> Result<Vec<u8>, AotError> {
495 let src = fs::read(source_path)?;
496 let ast = parse(&src, version).map_err(|e| {
497 AotError::Syntax(format!(
498 "{}:{}: {}",
499 source_path.display(),
500 e.line,
501 String::from_utf8_lossy(&e.msg)
502 ))
503 })?;
504
505 let mut heap = Heap::new();
506 let chunk_name = source_path
507 .file_name()
508 .and_then(|n| n.to_str())
509 .unwrap_or("aot-chunk")
510 .as_bytes()
511 .to_vec();
512 let proto = compile_chunk(&ast, version, &chunk_name, &mut heap).map_err(|e| {
513 AotError::Syntax(format!(
514 "{}:{}: {}",
515 source_path.display(),
516 e.line,
517 String::from_utf8_lossy(&e.msg)
518 ))
519 })?;
520
521 Ok(dump::dump(&proto, false, version))
522}
523
524/// Build `libluna_runtime_helpers.a` for the optional `target_triple`
525/// (host build when `None`) and return the path to the produced staticlib.
526///
527/// Resolution rules:
528///
529/// 1. If `LUNA_AOT_RUNTIME_HELPERS_STATICLIB` is set, take it as the
530/// absolute path of a pre-built `.a` and skip the cargo build.
531/// Useful for distribution scenarios where the staticlib is shipped
532/// out-of-band. Only honoured for the
533/// host triple — cross triples must build their own staticlib so
534/// the override doesn't accidentally mix ABIs.
535/// 2. Otherwise, look up `CARGO_MANIFEST_DIR`, ascend to the workspace
536/// root (two `..`), and invoke
537/// `cargo build -p luna-runtime-helpers --profile=release-aot-helpers [--target T]`.
538/// The staticlib lands at
539/// `target/<T or default>/release-aot-helpers/libluna_runtime_helpers.a`.
540/// The dedicated profile (workspace `Cargo.toml`) turns LTO off so
541/// the `luna_jit_*` helper symbols survive bundling — see
542/// `[profile.release-aot-helpers]` for the rationale.
543///
544/// Cross-target builds require the matching `rustup target add <triple>`
545/// to have been run beforehand; failures (missing rust-std) are
546/// reported via the cargo stderr with a helpful hint.
547///
548/// The `cargo` invocation is idempotent — cargo caches across runs.
549/// On a clean workspace the first call takes ~3s; subsequent calls
550/// are sub-second.
551fn build_runtime_helpers_staticlib(target_triple: Option<&str>) -> Result<PathBuf, AotError> {
552 if target_triple.is_none() {
553 // Honour the override only for host builds — for cross we
554 // must control the ABI to match the linker invocation.
555 if let Ok(prebuilt) = std::env::var("LUNA_AOT_RUNTIME_HELPERS_STATICLIB") {
556 let p = PathBuf::from(prebuilt);
557 if !p.exists() {
558 return Err(AotError::Link(format!(
559 "LUNA_AOT_RUNTIME_HELPERS_STATICLIB points at {} but the file does not exist",
560 p.display()
561 )));
562 }
563 return Ok(p);
564 }
565 }
566
567 // Serialize concurrent in-process callers (e.g. cargo's parallel
568 // integration tests). Cargo's own target-dir lock handles
569 // inter-process serialization, but in-process callers can race
570 // each other against cargo's "file briefly absent during atomic
571 // rename" window. A `Mutex` collapses that to one sequential
572 // build at a time per process.
573 use std::sync::Mutex;
574 static BUILD_LOCK: Mutex<()> = Mutex::new(());
575 let _guard = BUILD_LOCK.lock().unwrap_or_else(|p| p.into_inner());
576
577 // `CARGO_MANIFEST_DIR` is the directory containing the
578 // **luna-aot** Cargo.toml. The workspace root is two levels up.
579 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
580 // The advice has to differ by build kind. Suggesting the env-var
581 // override for a cross target is actively misleading: the check at
582 // the top of this function honours it only when `target_triple`
583 // is None, so a user following that hint would set it, see the
584 // identical error, and have nothing left to try.
585 AotError::Link(match target_triple {
586 None => "CARGO_MANIFEST_DIR not set — cannot locate workspace to \
587 build luna-runtime-helpers. Set \
588 LUNA_AOT_RUNTIME_HELPERS_STATICLIB to a prebuilt \
589 staticlib to bypass."
590 .to_string(),
591 Some(t) => format!(
592 "CARGO_MANIFEST_DIR not set — cannot locate workspace to build \
593 luna-runtime-helpers for target {t}. Cross-compilation \
594 requires running luna-aot from inside its workspace (e.g. \
595 `cargo run -p luna-aot -- compile ...`), because the \
596 staticlib must be built for the target ABI. \
597 LUNA_AOT_RUNTIME_HELPERS_STATICLIB does NOT apply here — it \
598 is honoured for host builds only, so that a cross link is \
599 never handed a staticlib built for the wrong ABI."
600 ),
601 })
602 })?;
603 let workspace_root = PathBuf::from(&manifest_dir)
604 .parent()
605 .and_then(Path::parent)
606 .map(Path::to_path_buf)
607 .ok_or_else(|| {
608 AotError::Link(format!(
609 "could not derive workspace root from CARGO_MANIFEST_DIR={manifest_dir}"
610 ))
611 })?;
612
613 let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
614 let mut cmd = Command::new(&cargo);
615 cmd.current_dir(&workspace_root)
616 .arg("build")
617 .arg("-p")
618 .arg("luna-runtime-helpers")
619 // Dedicated `release-aot-helpers` profile (defined in
620 // workspace `Cargo.toml`) turns LTO off
621 // for this staticlib build. Workspace `[profile.release]`
622 // has `lto = true`, which strips the 39 `luna_jit_*`
623 // Cranelift trace-mcode helper symbols from the staticlib
624 // bundle (the cross-crate optimizer correctly observes they
625 // are never *called* from the staticlib's Rust-side surface
626 // and treats their cgus as unreachable). The AOT-binary
627 // link step then fails with "undefined reference to
628 // `_luna_jit_table_get_field`" et al. for any trace mcode
629 // that touches a table.
630 //
631 // Trade-off: no cross-crate inlining into the helper bodies.
632 // Cranelift emits the calls as `Linkage::Import` indirect
633 // jumps regardless, so the inlining wouldn't apply at the
634 // call site anyway — the runtime-JIT path is unaffected.
635 .arg("--profile=release-aot-helpers")
636 // Don't inherit RUSTFLAGS that might pollute the staticlib
637 // (e.g. coverage instrumentation from the parent test build).
638 // Acceptable since the staticlib build is deterministic
639 // independent of the parent crate's profile.
640 .env_remove("RUSTFLAGS");
641 if let Some(t) = target_triple {
642 cmd.arg("--target").arg(t);
643 }
644 let output = cmd
645 .output()
646 .map_err(|e| AotError::Link(format!("spawn cargo: {e}")))?;
647 if !output.status.success() {
648 let stderr = String::from_utf8_lossy(&output.stderr);
649 // Detect the most common cross-compile failure mode (missing
650 // rust-std for the target) and translate to a concrete fix.
651 let hint = if let Some(t) = target_triple {
652 if stderr.contains("can't find crate for `std`")
653 || stderr.contains("the `std` crate is not available")
654 || stderr.contains("target may not be installed")
655 {
656 format!(
657 "\nhint: cross-compiling to {t} requires the rust-std component — \
658 run `rustup target add {t}` and retry."
659 )
660 } else {
661 String::new()
662 }
663 } else {
664 String::new()
665 };
666 return Err(AotError::Link(format!(
667 "cargo build -p luna-runtime-helpers{target_suffix} failed (exit {:?}):\nstdout:\n{}\nstderr:\n{}{hint}",
668 output.status.code(),
669 String::from_utf8_lossy(&output.stdout),
670 stderr,
671 target_suffix = target_triple
672 .map(|t| format!(" --target={t}"))
673 .unwrap_or_default(),
674 )));
675 }
676
677 let mut staticlib = workspace_root.join("target");
678 if let Some(t) = target_triple {
679 staticlib.push(t);
680 }
681 // Profile name mirrors the `--profile=release-aot-helpers` arg
682 // above. Cargo's target dir layout uses the profile name verbatim
683 // for non-`dev`/`release` profiles.
684 staticlib.push("release-aot-helpers");
685 // On Windows the staticlib is named `luna_runtime_helpers.lib`
686 // rather than `lib*.a`. We try both so the same code path covers
687 // both ABIs.
688 let unix_name = "libluna_runtime_helpers.a";
689 let windows_name = "luna_runtime_helpers.lib";
690 let unix_path = staticlib.join(unix_name);
691 let windows_path = staticlib.join(windows_name);
692 if unix_path.exists() {
693 Ok(unix_path)
694 } else if windows_path.exists() {
695 Ok(windows_path)
696 } else {
697 Err(AotError::Link(format!(
698 "cargo build succeeded but neither {} nor {} exist",
699 unix_path.display(),
700 windows_path.display(),
701 )))
702 }
703}
704
705/// Map the host triple to `object::{BinaryFormat, Architecture, Endianness}`.
706/// Used by the scaffold path; cross-compile triples go through
707/// [`TargetSpec`].
708fn host_object_target() -> (BinaryFormat, Architecture, Endianness) {
709 let format = BinaryFormat::native_object();
710 let arch = if cfg!(target_arch = "x86_64") {
711 Architecture::X86_64
712 } else if cfg!(target_arch = "aarch64") {
713 Architecture::Aarch64
714 } else if cfg!(target_arch = "x86") {
715 Architecture::I386
716 } else if cfg!(target_arch = "riscv64") {
717 Architecture::Riscv64
718 } else {
719 Architecture::Unknown
720 };
721 // Every tier-1 target object supports is little-endian; if we ever
722 // ship s390x or big-endian PPC, this needs a `cfg!(target_endian = "big")`
723 // branch.
724 let endian = Endianness::Little;
725 (format, arch, endian)
726}
727
728/// Best-effort host triple guess from compile-time `cfg!`. Only used
729/// for the host-vs-cross check; the scaffold rejects anything that
730/// doesn't match this string.
731fn host_triple() -> &'static str {
732 // Match the most common rust-toolchain triple spellings. Anything
733 // not enumerated falls back to "unknown" so a `--target` that
734 // happens to equal "unknown" is still rejected.
735 if cfg!(all(target_arch = "aarch64", target_os = "macos")) {
736 "aarch64-apple-darwin"
737 } else if cfg!(all(target_arch = "x86_64", target_os = "macos")) {
738 "x86_64-apple-darwin"
739 } else if cfg!(all(target_arch = "x86_64", target_os = "linux")) {
740 "x86_64-unknown-linux-gnu"
741 } else if cfg!(all(target_arch = "aarch64", target_os = "linux")) {
742 "aarch64-unknown-linux-gnu"
743 } else if cfg!(all(target_arch = "x86_64", target_os = "windows")) {
744 "x86_64-pc-windows-msvc"
745 } else {
746 "unknown"
747 }
748}
749
750// ────────────────────────────────────────────────────────────────────
751// Target-aware emission + cross-compile + Windows linker
752//
753// `TargetSpec` is the per-triple bundle of facts the AOT pipeline
754// needs:
755//
756// - `BinaryFormat` / `Architecture` / `Endianness` so `object::write`
757// emits the right `.o` magic
758// - the `cc` invocation (`cc`, `clang -target ...`, or
759// `<triple>-gcc`) and whether the C entry-point object needs
760// extra flags to land in the right ABI
761// - the lib set the staticlib transitively pulls (libpthread, libm,
762// CoreFoundation, ws2_32, ...) so the final link resolves all
763// externs without leaving the user to figure it out from cargo's
764// `--print native-static-libs` output
765//
766// Adding a new tier just means a new `from_triple` arm. The host arm
767// keeps its `cfg!`-derived defaults so the host path stays
768// unchanged.
769// ────────────────────────────────────────────────────────────────────
770
771/// Per-target bundle of facts the AOT pipeline needs to emit a
772/// runnable binary. Constructed via [`TargetSpec::host`] or
773/// [`TargetSpec::from_triple`].
774#[derive(Debug, Clone)]
775pub struct TargetSpec {
776 /// The rustc triple string (e.g. "aarch64-apple-darwin"). For the
777 /// host build this matches the value returned by the private
778 /// `host_triple` helper; for cross builds it's whatever the caller
779 /// passed via `--target`.
780 pub triple: String,
781 /// `true` when the triple matches the build host's rust triple. The
782 /// staticlib-build step shortcuts the `--target` flag in this case,
783 /// landing the `.a` under `target/release-aot-helpers/` (the
784 /// workspace default dir for the dedicated AOT-helpers profile,
785 /// not `target/<triple>/release-aot-helpers/`).
786 pub is_host: bool,
787 /// Object-file binary format for `object::write::Object::new`.
788 pub format: BinaryFormat,
789 /// Object-file architecture for `object::write::Object::new`.
790 pub arch: Architecture,
791 /// Object-file endianness for `object::write::Object::new`. Every
792 /// tier-1 cranelift target is little-endian; if we ever ship s390x
793 /// or big-endian PPC, this needs to flip.
794 pub endian: Endianness,
795 /// `cfg!(target_os = "...")` style family for selecting which lib
796 /// set to pass at link time. Decoupled from `cc!` so a Linux host
797 /// can describe a Windows target without rebuilding luna-aot.
798 pub os: TargetOs,
799 /// libc flavour — distinguishes glibc vs musl on Linux. Drives
800 /// `-lgcc_s` (glibc) vs no-such-lib (musl).
801 pub libc: TargetLibc,
802}
803
804/// Coarse target-OS family used by [`TargetSpec`]. Granular enough to
805/// pick the right `cc` driver and lib set.
806#[derive(Debug, Clone, Copy, PartialEq, Eq)]
807pub enum TargetOs {
808 /// macOS / Darwin (`*-apple-darwin`).
809 MacOs,
810 /// Linux (any libc).
811 Linux,
812 /// Windows (MSVC or MinGW; the libc distinction is on `TargetLibc`).
813 Windows,
814}
815
816/// libc flavour for the target. Drives the lib set passed at link time.
817#[derive(Debug, Clone, Copy, PartialEq, Eq)]
818pub enum TargetLibc {
819 /// glibc on Linux, Apple libc on Darwin, MSVCRT on Windows MSVC.
820 Default,
821 /// musl on Linux (Alpine, static deploys). Skips `-lgcc_s` and
822 /// `-lutil`, which aren't present in musl's lib set.
823 Musl,
824 /// MinGW on Windows (gcc-based toolchain producing PE-COFF that
825 /// still links against the system C runtime via the mingwex shim).
826 MinGw,
827}
828
829impl TargetSpec {
830 /// Host-triple spec: object format from `cfg!` derivation, libc from `cfg!(target_env)`.
831 pub fn host() -> Self {
832 let (format, arch, endian) = host_object_target();
833 let triple = host_triple().to_string();
834 let os = if cfg!(target_os = "macos") {
835 TargetOs::MacOs
836 } else if cfg!(target_os = "linux") {
837 TargetOs::Linux
838 } else if cfg!(target_os = "windows") {
839 TargetOs::Windows
840 } else {
841 // Unknown OS: pick Linux as the least-surprising default
842 // for unix-y systems; the caller will see a clear cc error
843 // if the assumption fails.
844 TargetOs::Linux
845 };
846 let libc = if cfg!(target_env = "musl") {
847 TargetLibc::Musl
848 } else if cfg!(all(target_os = "windows", target_env = "gnu")) {
849 TargetLibc::MinGw
850 } else {
851 TargetLibc::Default
852 };
853 TargetSpec {
854 triple,
855 is_host: true,
856 format,
857 arch,
858 endian,
859 os,
860 libc,
861 }
862 }
863
864 /// Parse a triple string into a `TargetSpec`. Unrecognised triples
865 /// return [`AotError::UnsupportedTarget`].
866 ///
867 /// Tier 1 (verified end-to-end on macOS aarch64 host): the host
868 /// triple, plus the same-OS cross to `x86_64-apple-darwin` /
869 /// `aarch64-apple-darwin` (Apple's universal clang handles both).
870 ///
871 /// Tier 2 (codegen-verified, link requires the matching cross-cc
872 /// toolchain on PATH): `*-unknown-linux-gnu`, `*-unknown-linux-musl`,
873 /// `x86_64-pc-windows-gnu` (MinGW). The cargo staticlib build
874 /// succeeds when the rust-std for the triple is installed; the
875 /// final `cc` link fails with a clear error if the cross-gcc is
876 /// missing.
877 pub fn from_triple(triple: &str) -> Result<Self, AotError> {
878 // Short-circuit: if the requested triple matches the host
879 // triple, route through `host()` so we get the host lib
880 // detection (which uses the actual cfg! the binary was built
881 // under, not the parsed triple string).
882 if triple == host_triple() {
883 return Ok(Self::host());
884 }
885
886 let parts: Vec<&str> = triple.split('-').collect();
887 if parts.len() < 3 {
888 return Err(AotError::UnsupportedTarget(format!(
889 "triple {triple:?} has fewer than 3 components (expected arch-vendor-os[-env])"
890 )));
891 }
892 let arch_str = parts[0];
893 // parts[1] is the vendor (unknown / apple / pc / ...); we don't
894 // gate on it, only inform the object-format pick via os.
895 let os_str = parts[2];
896 let env_str = parts.get(3).copied().unwrap_or("");
897
898 let arch = match arch_str {
899 "x86_64" => Architecture::X86_64,
900 "aarch64" => Architecture::Aarch64,
901 "i686" | "i586" | "x86" => Architecture::I386,
902 "riscv64" | "riscv64gc" => Architecture::Riscv64,
903 other => {
904 return Err(AotError::UnsupportedTarget(format!(
905 "arch component {other:?} of triple {triple:?} not in tier 1/2 set \
906 (supported: x86_64, aarch64, i686, riscv64)"
907 )));
908 }
909 };
910
911 let (os, format, libc) = match os_str {
912 "darwin" => (TargetOs::MacOs, BinaryFormat::MachO, TargetLibc::Default),
913 "linux" => {
914 let libc = if env_str.contains("musl") {
915 TargetLibc::Musl
916 } else {
917 TargetLibc::Default
918 };
919 (TargetOs::Linux, BinaryFormat::Elf, libc)
920 }
921 "windows" => {
922 // env can be "gnu" (MinGW) or "msvc"; we route by env.
923 let libc = if env_str == "gnu" {
924 TargetLibc::MinGw
925 } else {
926 TargetLibc::Default
927 };
928 (TargetOs::Windows, BinaryFormat::Coff, libc)
929 }
930 other => {
931 return Err(AotError::UnsupportedTarget(format!(
932 "os component {other:?} of triple {triple:?} not in tier 1/2 set \
933 (supported: darwin, linux, windows)"
934 )));
935 }
936 };
937
938 Ok(TargetSpec {
939 triple: triple.to_string(),
940 is_host: false,
941 format,
942 arch,
943 endian: Endianness::Little,
944 os,
945 libc,
946 })
947 }
948
949 /// Cargo `--target` value, or `None` for the host build (cargo
950 /// defaults to the host triple when `--target` is omitted).
951 pub fn triple_for_cargo(&self) -> Option<&str> {
952 if self.is_host {
953 None
954 } else {
955 Some(&self.triple)
956 }
957 }
958
959 /// `true` when this target uses the MSVC toolchain (Windows + the
960 /// default Microsoft libc). Routes `write_aot_cmain_object_for` to
961 /// the `clang-cl` / `cl.exe` driver shape and `link_aot_binary_for`
962 /// to the `lld-link` / `link.exe` driver shape (vs the gcc-style
963 /// `cc -o foo foo.o ...` shape used for every other target).
964 pub fn is_msvc(&self) -> bool {
965 self.os == TargetOs::Windows && self.libc == TargetLibc::Default
966 }
967
968 /// Pick the MSVC-style C compiler driver. Returns `None` when none is on PATH (caller skips with a
969 /// clear error message). Resolution:
970 ///
971 /// 1. `$CC` env var wins (consistent with `cc_command`).
972 /// 2. `clang-cl` — cross-platform: macOS/Linux hosts get it via
973 /// `brew install llvm` / `apt install clang`, accepts the same
974 /// `__attribute__((section(...)))` syntax we emit for MinGW.
975 /// 3. `cl.exe` — Microsoft Build Tools, Windows-host only. Requires
976 /// `vcvarsall.bat` to have set up INCLUDE / LIB env vars.
977 fn msvc_cc_command(&self) -> Option<Command> {
978 if let Ok(cc) = std::env::var("CC") {
979 return Some(Command::new(cc));
980 }
981 for candidate in &["clang-cl", "cl.exe", "cl"] {
982 if which_on_path(candidate) {
983 return Some(Command::new(candidate));
984 }
985 }
986 None
987 }
988
989 /// Pick the MSVC-style PE/COFF linker driver. Returns `None` when none is on PATH. Resolution:
990 ///
991 /// 1. `$LD` env var wins (advanced override for embedders shipping a
992 /// pinned linker).
993 /// 2. `lld-link` — LLVM's PE/COFF linker. Cross-platform: macOS gets
994 /// it via `brew install llvm`, Linux via `apt install lld`. Works
995 /// without a Windows host or vcvarsall setup.
996 /// 3. `link.exe` — Microsoft's linker. Windows-only; requires
997 /// Developer Command Prompt (sets PATH + LIB env vars).
998 fn msvc_link_command(&self) -> Option<Command> {
999 if let Ok(ld) = std::env::var("LD") {
1000 return Some(Command::new(ld));
1001 }
1002 for candidate in &["lld-link", "link.exe", "link"] {
1003 if which_on_path(candidate) {
1004 return Some(Command::new(candidate));
1005 }
1006 }
1007 None
1008 }
1009
1010 /// Pick the `cc` driver invocation for this target. Returns the
1011 /// command (already constructed with the driver name and any
1012 /// `-target` / `--target` flags) ready for the caller to add
1013 /// inputs / outputs / lib flags.
1014 ///
1015 /// Resolution order:
1016 ///
1017 /// 1. `$CC` environment variable wins, full stop (matches the
1018 /// host path).
1019 /// 2. For non-host targets we try the toolchain-named cross
1020 /// compiler first (e.g. `aarch64-linux-gnu-gcc`,
1021 /// `x86_64-w64-mingw32-gcc`, `x86_64-linux-musl-gcc`).
1022 /// 3. Fall through to `cc -target <triple>` (works on macOS where
1023 /// Apple's clang is the system cc and supports cross-darwin
1024 /// natively).
1025 ///
1026 /// The returned `Command` already has any `-target`/`--target`
1027 /// flag set; the caller adds the remaining args.
1028 pub fn cc_command(&self) -> Command {
1029 if let Ok(cc) = std::env::var("CC") {
1030 return Command::new(cc);
1031 }
1032
1033 if self.is_host {
1034 return Command::new("cc");
1035 }
1036
1037 // Non-host: try the named cross-cc first.
1038 let cross_candidates: &[&str] = match (self.os, self.arch, self.libc) {
1039 (TargetOs::Linux, Architecture::Aarch64, TargetLibc::Default) => {
1040 &["aarch64-linux-gnu-gcc"]
1041 }
1042 (TargetOs::Linux, Architecture::X86_64, TargetLibc::Default) => {
1043 &["x86_64-linux-gnu-gcc"]
1044 }
1045 (TargetOs::Linux, Architecture::Aarch64, TargetLibc::Musl) => {
1046 &["aarch64-linux-musl-gcc", "musl-gcc"]
1047 }
1048 (TargetOs::Linux, Architecture::X86_64, TargetLibc::Musl) => {
1049 &["x86_64-linux-musl-gcc", "musl-gcc"]
1050 }
1051 (TargetOs::Windows, Architecture::X86_64, TargetLibc::MinGw) => {
1052 &["x86_64-w64-mingw32-gcc"]
1053 }
1054 (TargetOs::Windows, Architecture::I386, TargetLibc::MinGw) => &["i686-w64-mingw32-gcc"],
1055 _ => &[],
1056 };
1057 for candidate in cross_candidates {
1058 if which_on_path(candidate) {
1059 return Command::new(candidate);
1060 }
1061 }
1062
1063 // Apple cross-darwin: clang -target accepts e.g.
1064 // `x86_64-apple-darwin` directly when the SDK is installed.
1065 if self.os == TargetOs::MacOs {
1066 let mut cmd = Command::new("cc");
1067 cmd.arg("-target").arg(&self.triple);
1068 return cmd;
1069 }
1070
1071 // Last resort: `cc -target` and hope the host cc is clang.
1072 // Will error at link time on gcc hosts; the error message
1073 // includes the triple so the user knows what to install.
1074 let mut cmd = Command::new("cc");
1075 cmd.arg("-target").arg(&self.triple);
1076 cmd
1077 }
1078
1079 /// Resolve the Cranelift `TargetIsa` builder for this target. Used by
1080 /// `harvest_and_emit_aot_traces` so the offline trace lowerer
1081 /// codegens for the deploy ABI rather than the build host's.
1082 ///
1083 /// Two layers:
1084 ///
1085 /// 1. Parse `self.triple` with `target_lexicon::Triple::from_str`.
1086 /// Both Cranelift's `isa::lookup_by_name` and `isa::lookup` go
1087 /// through the same path; the explicit `from_str` here surfaces
1088 /// a clean `AotError::Object` on malformed triples rather than
1089 /// Cranelift's internal `expect` panic.
1090 /// 2. `cranelift_codegen::isa::lookup(triple)` returns an `isa::
1091 /// Builder` configured for the requested arch — provided the
1092 /// Cranelift feature for that arch (`x86`, `arm64`, etc.) is
1093 /// enabled at compile time. We turn `all-arch` on in
1094 /// `Cargo.toml` so any tier-1/2 target resolves. If somebody
1095 /// later shrinks the feature set, the `SupportDisabled` arm
1096 /// surfaces a clear error.
1097 ///
1098 /// On the host triple we still go through `cranelift_native::
1099 /// builder()` (rather than the per-triple path) so we inherit the
1100 /// CPU-feature autodetection (`SSE4.1`, `AVX2`, …). Host warmup +
1101 /// host deploy ⇒ identical mcode.
1102 // cranelift types in the signature: internal to luna crates, not covered by semver
1103 #[doc(hidden)]
1104 pub fn cranelift_isa_builder(&self) -> Result<cranelift_codegen::isa::Builder, AotError> {
1105 use std::str::FromStr;
1106 if self.is_host {
1107 return cranelift_native::builder().map_err(|e| {
1108 AotError::Object(format!(
1109 "cranelift_native::builder for host triple {}: {e}",
1110 self.triple
1111 ))
1112 });
1113 }
1114 let triple = target_lexicon::Triple::from_str(&self.triple).map_err(|e| {
1115 AotError::Object(format!(
1116 "target_lexicon could not parse triple {:?}: {e}",
1117 self.triple
1118 ))
1119 })?;
1120 cranelift_codegen::isa::lookup(triple).map_err(|e| {
1121 AotError::Object(format!(
1122 "cranelift_codegen::isa::lookup for triple {}: {e:?} \
1123 (re-build luna-aot with `cranelift-codegen` feature \
1124 `all-arch` or the per-arch feature for {})",
1125 self.triple, self.triple,
1126 ))
1127 })
1128 }
1129}
1130
1131/// Check whether `binary` resolves on `PATH`. Used by
1132/// [`TargetSpec::cc_command`] to prefer a named cross-cc over the
1133/// host `cc`.
1134fn which_on_path(binary: &str) -> bool {
1135 if let Ok(path) = std::env::var("PATH") {
1136 for dir in path.split(if cfg!(windows) { ';' } else { ':' }) {
1137 if dir.is_empty() {
1138 continue;
1139 }
1140 let candidate = std::path::Path::new(dir).join(binary);
1141 if candidate.exists() {
1142 return true;
1143 }
1144 }
1145 }
1146 false
1147}
1148
1149/// Target-aware variant of [`write_bytecode_object`]. Emits a `.o`
1150/// using the target's format/arch/endian rather than the host's.
1151fn write_bytecode_object_for(
1152 bytecode: &[u8],
1153 out: &Path,
1154 target: &TargetSpec,
1155) -> Result<(), AotError> {
1156 let mut obj = Object::new(target.format, target.arch, target.endian);
1157
1158 let section_id = obj.add_section(
1159 Vec::new(),
1160 BYTECODE_SECTION_NAME.as_bytes().to_vec(),
1161 SectionKind::ReadOnlyData,
1162 );
1163 let _start_offset = obj.append_section_data(section_id, bytecode, 1);
1164
1165 let start_name = BYTECODE_START_SYMBOL.to_string();
1166 let end_name = BYTECODE_END_SYMBOL.to_string();
1167 let _start_sym = obj.add_symbol(Symbol {
1168 name: start_name.into_bytes(),
1169 value: 0,
1170 size: 0,
1171 kind: SymbolKind::Data,
1172 scope: SymbolScope::Dynamic,
1173 weak: false,
1174 section: SymbolSection::Section(section_id),
1175 flags: object::SymbolFlags::None,
1176 });
1177 let _end_sym = obj.add_symbol(Symbol {
1178 name: end_name.into_bytes(),
1179 value: bytecode.len() as u64,
1180 size: 0,
1181 kind: SymbolKind::Data,
1182 scope: SymbolScope::Dynamic,
1183 weak: false,
1184 section: SymbolSection::Section(section_id),
1185 flags: object::SymbolFlags::None,
1186 });
1187
1188 let bytes = obj
1189 .write()
1190 .map_err(|e| AotError::Object(format!("Object::write: {e}")))?;
1191 fs::write(out, bytes)?;
1192 Ok(())
1193}
1194
1195/// Target-aware variant of [`write_aot_cmain_object`]. Generates the
1196/// same C source but invokes the target-specific cc driver so the
1197/// produced `.o` has the right ABI.
1198fn write_aot_cmain_object_for(out: &Path, target: &TargetSpec) -> Result<(), AotError> {
1199 // Guarantee the `luna_strkey_idx` section exists in the link
1200 // image even when the binary linked zero AOT trace `.o`s. Without
1201 // a defining input the bracket symbols `__start_luna_strkey_idx` /
1202 // `__stop_luna_strkey_idx` (or the Mach-O `section$start$...`
1203 // equivalents) are undefined and the link fails. Defining an
1204 // empty placeholder lets the deploy resolver see `start == end`
1205 // and short-circuit cleanly.
1206 //
1207 // The placeholder uses a `static` zero-length array marked
1208 // `used` so the C compiler emits the section header even though
1209 // nothing references it. On Mach-O the `section` attribute
1210 // takes a `"__SEG,__SECT"` pair; we use `__DATA,luna_strkey_idx`,
1211 // the same segment the lowerer puts its entries in. On ELF the
1212 // `section` attribute takes just the section name.
1213 // Also guarantee the `luna_trace_meta` section exists in the
1214 // link image when zero trace `.o`s linked in (small / non-loopy
1215 // sources where the warmup recorder didn't close any traces).
1216 // Same placeholder pattern as `luna_strkey_idx`.
1217 //
1218 // **Sized at 48 bytes** (matching `AotTraceIndexEntry::SIZE`) and
1219 // 8-byte aligned so when a real AOT-emitted trace .o lands in the
1220 // same section, the linker's section merge concatenates the
1221 // entries without misaligning anyone. The placeholder bytes are
1222 // all-zero; the deploy walker sees a single entry whose `fn_ptr`
1223 // is NULL and skips it (via the `entry.fn_ptr.is_null()` guard).
1224 //
1225 // `aligned(8)` is needed because the section's other entries
1226 // carry pointer relocations at offsets 24 / 32 — a misaligned
1227 // placeholder shifts those into bad lanes.
1228 // Strkey idx placeholder: sized to a full `IndexEntry` (16 bytes,
1229 // 8-byte aligned) so the deploy resolver's
1230 // `start + N * sizeof::<IndexEntry>` iteration lines up with
1231 // any real trace-emitted entries that follow. A `[1]`-sized
1232 // placeholder would land 7 bytes of zero pad between itself and
1233 // the first trace entry (the trace lower sets `align(8)`), which
1234 // mis-aligns the divide-by-16 entry count: the walker sees the
1235 // placeholder as half an entry and misses the real one by 8
1236 // bytes. Sizing the placeholder to 16 means the section
1237 // is exactly N+1 entries for N real traces; the placeholder's
1238 // zero-valued `bytes_ptr` short-circuits via the resolver's
1239 // `entry.bytes_ptr.is_null()` guard.
1240 // Also guarantee the `luna_inline_chnx` section exists when the
1241 // binary linked zero depth>0-inlined-cmp trace `.o`s. Same shape as the strkey idx
1242 // placeholder (16 bytes = one IndexEntry-sized slot) so the deploy
1243 // resolver's `start + N * sizeof::<IndexEntry>` walk lines up with
1244 // any real trace-emitted entries. Zero `bytes_ptr` field short-
1245 // circuits via the resolver's null guard.
1246 //
1247 // Mach-O sectname max is 16 chars; `luna_inline_chnx` is 15
1248 // (matches `luna_strkey_idx` sizing). Windows COFF short name cap
1249 // is 8 — `.lt_chai` mirrors `.lt_skix` / `.lt_meta`. Both names
1250 // must match the lowerer's section choice in
1251 // `emit_chain_ptr_arg` and the deploy resolver's bracket /
1252 // section-walker needles.
1253 let placeholder = match target.os {
1254 TargetOs::MacOs => {
1255 "__attribute__((used, section(\"__DATA,luna_strkey_idx\"), aligned(8)))\n\
1256 static const char luna_strkey_idx_placeholder[16] = {0};\n\
1257 __attribute__((used, section(\"__DATA,luna_trace_meta\"), aligned(8)))\n\
1258 static const char luna_trace_meta_placeholder[48] = {0};\n\
1259 __attribute__((used, section(\"__DATA,luna_inline_chnx\"), aligned(8)))\n\
1260 static const char luna_inline_chnx_placeholder[16] = {0};\n"
1261 }
1262 TargetOs::Linux => {
1263 "__attribute__((used, section(\"luna_strkey_idx\"), aligned(8)))\n\
1264 static const char luna_strkey_idx_placeholder[16] = {0};\n\
1265 __attribute__((used, section(\"luna_trace_meta\"), aligned(8)))\n\
1266 static const char luna_trace_meta_placeholder[48] = {0};\n\
1267 __attribute__((used, section(\"luna_inline_chnx\"), aligned(8)))\n\
1268 static const char luna_inline_chnx_placeholder[16] = {0};\n"
1269 }
1270 // Windows COFF.
1271 //
1272 // PE/COFF section name headers are fixed 8 bytes
1273 // (`IMAGE_SECTION_HEADER::Name`), so we use deliberately
1274 // short names: `.lt_skix` for the strkey index (mirrors
1275 // `luna_strkey_idx`), `.lt_meta` for trace meta. Each is
1276 // exactly 8 bytes including the leading `.`, matching the
1277 // 8-byte PE section name field byte-for-byte without truncation
1278 // or string-table fallback (the COFF string-table mechanism
1279 // for long names is an object-file feature only — `link.exe`
1280 // / `lld-link` drop the long-name table when producing the
1281 // final PE image).
1282 //
1283 // Mirror placeholders so the sections exist even when the
1284 // binary linked zero AOT trace `.o`s — same shape as the
1285 // Mach-O / ELF placeholders above. The deploy walker
1286 // (`luna-runtime-helpers::windows_section::find_section`)
1287 // sees the placeholder bytes via the runtime PE-header
1288 // parse and short-circuits on the all-zero entry.
1289 //
1290 // MinGW's gcc accepts the `__attribute__((section(...)))`
1291 // syntax verbatim with the section name as-is (no leading
1292 // `__DATA,` prefix — that's Mach-O specific). For MSVC we
1293 // emit the equivalent `#pragma section` + `__declspec(allocate(...))`
1294 // form so the same placeholder data lands in the same
1295 // `.lt_skix` / `.lt_meta` sections regardless of toolchain;
1296 // the deploy walker (`luna-runtime-helpers::windows_section`)
1297 // looks up by section name and is toolchain-agnostic.
1298 //
1299 // clang-cl accepts both syntaxes (`__attribute__((section()))`
1300 // and the MSVC `__declspec(allocate())` form), but `cl.exe`
1301 // only accepts the MSVC form — so we emit the MSVC form for
1302 // both, which keeps a single source path covering both drivers.
1303 TargetOs::Windows if target.is_msvc() => {
1304 // `#pragma section` declares the section + its
1305 // characteristics (R = readable). The 8-byte alignment
1306 // matches the MinGW arm so the deploy walker's pointer
1307 // arithmetic over the section is identical across
1308 // toolchains.
1309 "#pragma section(\".lt_skix\", read)\n\
1310 __declspec(allocate(\".lt_skix\")) __declspec(align(8))\n\
1311 static const char luna_strkey_idx_placeholder[16] = {0};\n\
1312 #pragma section(\".lt_meta\", read)\n\
1313 __declspec(allocate(\".lt_meta\")) __declspec(align(8))\n\
1314 static const char luna_trace_meta_placeholder[48] = {0};\n\
1315 #pragma section(\".lt_chai\", read)\n\
1316 __declspec(allocate(\".lt_chai\")) __declspec(align(8))\n\
1317 static const char luna_inline_chnx_placeholder[16] = {0};\n"
1318 }
1319 TargetOs::Windows => {
1320 "__attribute__((used, section(\".lt_skix\"), aligned(8)))\n\
1321 static const char luna_strkey_idx_placeholder[16] = {0};\n\
1322 __attribute__((used, section(\".lt_meta\"), aligned(8)))\n\
1323 static const char luna_trace_meta_placeholder[48] = {0};\n\
1324 __attribute__((used, section(\".lt_chai\"), aligned(8)))\n\
1325 static const char luna_inline_chnx_placeholder[16] = {0};\n"
1326 }
1327 };
1328
1329 let c_src = format!(
1330 r#"#include <stddef.h>
1331#include <stdint.h>
1332
1333extern uint8_t __luna_bytecode_start[];
1334extern uint8_t __luna_bytecode_end[];
1335extern int luna_aot_run(const uint8_t *bytecode, size_t len);
1336
1337{placeholder}
1338
1339int main(int argc, char **argv) {{
1340 (void)argc; (void)argv;
1341 size_t len = (size_t)(__luna_bytecode_end - __luna_bytecode_start);
1342 return luna_aot_run(__luna_bytecode_start, len);
1343}}
1344"#
1345 );
1346
1347 let mut c_path = out.to_path_buf();
1348 c_path.set_extension("c");
1349 fs::write(&c_path, c_src)?;
1350
1351 // MSVC needs `clang-cl` / `cl.exe` (different flag shape: `/c` +
1352 // `/Fo:` vs gcc-style `-c` + `-o`). All other targets keep the
1353 // existing gcc-style cc driver path.
1354 let mut cmd = if target.is_msvc() {
1355 let Some(mut cl) = target.msvc_cc_command() else {
1356 return Err(AotError::Link(format!(
1357 "MSVC C compiler not on PATH for target {} — install one of: \
1358 (a) `clang-cl` via LLVM (`brew install llvm` on macOS; \
1359 `apt install clang` on Linux), or (b) Visual Studio Build \
1360 Tools 2022 (`cl.exe`, Windows host only — invoke luna-aot \
1361 from a Developer Command Prompt). Override with `CC=...` \
1362 to point at a custom driver.",
1363 target.triple
1364 )));
1365 };
1366 // `clang-cl` / `cl.exe`: `/c` compile-only, `/Fo:<obj>` output.
1367 // Some Linux distros' clang-cl wrappers also accept gcc-style
1368 // flags, but the MSVC shape works on every supported driver.
1369 cl.arg("/c");
1370 cl.arg(&c_path);
1371 // `/Fo:` and the output path are a single token when no space —
1372 // we use the safe two-arg form via `arg(format!("/Fo:{}", ..))`
1373 // which avoids whitespace-in-path issues.
1374 cl.arg(format!("/Fo:{}", out.display()));
1375 // Suppress the cl.exe banner (clang-cl no-ops on this flag).
1376 cl.arg("/nologo");
1377 // Cross-compile target: clang-cl accepts `--target=<triple>` to
1378 // override the default host. cl.exe rejects this; we only set it
1379 // for clang-cl by detecting the program name (heuristic — first
1380 // arg defaulted via `Command::new`). The simplest robust path
1381 // is to always set it when not on a Windows host, since cl.exe
1382 // can't realistically run there anyway. Skipping for now: cmd
1383 // here is `clang-cl` only when reachable on a Unix host.
1384 if !cfg!(target_os = "windows") {
1385 cl.arg(format!("--target={}", target.triple));
1386 }
1387 cl
1388 } else {
1389 let mut cmd = target.cc_command();
1390 cmd.arg("-c").arg(&c_path).arg("-o").arg(out);
1391 cmd
1392 };
1393 let status = cmd
1394 .output()
1395 .map_err(|e| AotError::Link(format!("spawn cc for target {}: {e}", target.triple)))?;
1396 if !status.status.success() {
1397 return Err(AotError::Link(format!(
1398 "cc -c {} (target {}) failed (exit {:?}):\n{}",
1399 c_path.display(),
1400 target.triple,
1401 status.status.code(),
1402 String::from_utf8_lossy(&status.stderr)
1403 )));
1404 }
1405 Ok(())
1406}
1407
1408/// Target-aware variant of [`link_aot_binary`]. Picks the cc driver,
1409/// per-OS lib set, and (for Windows) the MinGW vs MSVC path.
1410///
1411/// `traces_obj`: optional AOT-trace mcode `.o`
1412/// emitted by [`harvest_and_emit_aot_traces`]. When `Some`, the linker
1413/// pulls in the trace mcode + the `luna_trace_meta` / `luna_trace_blob`
1414/// data sections that the deploy walker reads at startup. When `None`,
1415/// the binary runs through interp + runtime-JIT fallback only.
1416fn link_aot_binary_for(
1417 bytecode_obj: &Path,
1418 cmain_obj: &Path,
1419 traces_obj: Option<&Path>,
1420 staticlib: &Path,
1421 out_path: &Path,
1422 target: &TargetSpec,
1423) -> Result<(), AotError> {
1424 // MSVC has a completely different linker surface (`link.exe` /
1425 // `lld-link.exe`: `/OUT:foo.exe`, `/SUBSYSTEM:CONSOLE`, `.lib`
1426 // system libs, no `-l` flag). Route through a dedicated path;
1427 // everything else (Mach-O, ELF, MinGW PE-COFF) shares the gcc-style cc-driver path below.
1428 if target.is_msvc() {
1429 return link_aot_binary_msvc(
1430 bytecode_obj,
1431 cmain_obj,
1432 traces_obj,
1433 staticlib,
1434 out_path,
1435 target,
1436 );
1437 }
1438
1439 let mut cmd = target.cc_command();
1440
1441 // Object files first (they reference symbols defined in the
1442 // staticlib). Order matters for some traditional Unix linkers
1443 // (`ld` resolves left-to-right; modern `lld` is order-independent
1444 // but we keep the canonical order for portability).
1445 cmd.arg(cmain_obj).arg(bytecode_obj);
1446 if let Some(traces) = traces_obj {
1447 // Trace mcode `.o`. Placed after the
1448 // bytecode object (which references the AOT trace `luna_aot_
1449 // trace_*` symbols via its bracketed meta section's
1450 // relocations) so resolution flows correctly under traditional
1451 // left-to-right ld.
1452 cmd.arg(traces);
1453 }
1454 cmd.arg(staticlib);
1455
1456 // Per-OS lib set — what `rustc --print native-static-libs` reports
1457 // for a `crate-type = ["staticlib"]` on each platform that pulls
1458 // std. The macOS + linux sets match the host path verbatim so the
1459 // cross arm links exactly what the host build does.
1460 match target.os {
1461 TargetOs::MacOs => {
1462 cmd.args(["-framework", "CoreFoundation"]);
1463 cmd.args(["-framework", "Security"]);
1464 cmd.arg("-liconv");
1465 }
1466 TargetOs::Linux => {
1467 cmd.arg("-lpthread");
1468 cmd.arg("-ldl");
1469 cmd.arg("-lm");
1470 // glibc-only libs; musl ships these symbols inside libc
1471 // so naming them here would be a `cannot find -lgcc_s` /
1472 // `cannot find -lutil` failure on Alpine.
1473 if target.libc != TargetLibc::Musl {
1474 cmd.arg("-lrt");
1475 cmd.arg("-lgcc_s");
1476 cmd.arg("-lutil");
1477 }
1478 }
1479 TargetOs::Windows => {
1480 // MinGW: rust stdlib's std::sys::windows shim needs these.
1481 // The set comes from `rustc --print native-static-libs --target=x86_64-pc-windows-gnu`
1482 // run on a probe staticlib in CI; we replicate the typical
1483 // dependency list here.
1484 cmd.arg("-luserenv");
1485 cmd.arg("-lkernel32");
1486 cmd.arg("-lws2_32");
1487 cmd.arg("-lbcrypt");
1488 cmd.arg("-ladvapi32");
1489 cmd.arg("-lntdll");
1490 // MinGW gcc adds its own startup; nothing more needed.
1491 }
1492 }
1493
1494 cmd.arg("-o").arg(out_path);
1495
1496 let output = cmd
1497 .output()
1498 .map_err(|e| AotError::Link(format!("spawn cc for target {}: {e}", target.triple)))?;
1499 if !output.status.success() {
1500 return Err(AotError::Link(format!(
1501 "cc link failed (target {}, exit {:?}):\ncommand: {:?}\nstderr:\n{}",
1502 target.triple,
1503 output.status.code(),
1504 cmd,
1505 String::from_utf8_lossy(&output.stderr),
1506 )));
1507 }
1508 Ok(())
1509}
1510
1511// ────────────────────────────────────────────────────────────────────
1512// Offline trace harvester + AOT trace mcode emission.
1513//
1514// The pipeline:
1515// 1. Build a JIT-equipped warmup `Vm` (luna_jit::new_with_jit) and
1516// swap in a `RecordingTraceCompiler` wrapper that forwards every
1517// compile attempt to the real Cranelift backend AND captures the
1518// input `TraceRecord` into a thread-local. The recorder runs at
1519// every back-edge that crosses the const `TRACE_HOT_THRESHOLD = 64`,
1520// so simple counted loops with iteration counts in the thousands
1521// will close at least one trace.
1522// 2. Load the dump and call the chunk's root closure. The dispatcher
1523// records + compiles + dispatches as normal; we only care about
1524// the captured records.
1525// 3. For each captured (proto, record) pair, re-lower the record via
1526// `lower_trace_into_named` against a fresh `ObjectModule` per .o,
1527// writing the emitted bytes + a per-trace `luna_trace_blob` payload
1528// + a `luna_trace_meta` 48-byte index entry. All three sections are
1529// bracket-symbol enumerated by the deploy walker at startup.
1530// 4. Bail-tolerantly: if no traces close (small / non-loopy source),
1531// return `HarvestedTraces::None` and the pipeline skips the trace .o.
1532// ────────────────────────────────────────────────────────────────────
1533
1534/// Records every `TraceRecord` the dispatcher tries to compile; forwards
1535/// the actual compile to the wrapped real Cranelift backend so the
1536/// warmup run dispatches normally afterwards.
1537///
1538/// Records are appended to a thread-local Vec — read out after the
1539/// warmup `vm.call_value` returns.
1540struct RecordingTraceCompiler {
1541 inner: luna_jit::jit_backend::CraneliftBackend,
1542}
1543
1544thread_local! {
1545 /// Thread-local capture buffer for `TraceRecord`s observed during
1546 /// the AOT warmup run. Each entry is `(head_proto_hash,
1547 /// head_pc, TraceRecord)`. Cleared at the start of every
1548 /// [`harvest_and_emit_aot_traces`] call.
1549 static AOT_CAPTURED_RECORDS: std::cell::RefCell<Vec<([u8; 16], u32, TraceRecord)>> =
1550 const { std::cell::RefCell::new(Vec::new()) };
1551}
1552
1553/// Capture a cloneable image of the record so we can re-lower at AOT
1554/// emit time. TraceRecord is Clone since every field is Clone
1555/// (Gc<Proto> = NonNull copy; Vec<RecordedOp> is Clone).
1556fn capture_record(record: &TraceRecord) {
1557 let hash = record.head_proto.stable_hash();
1558 AOT_CAPTURED_RECORDS.with(|cell| {
1559 cell.borrow_mut()
1560 .push((hash, record.head_pc, record.clone()));
1561 });
1562}
1563
1564impl luna_core::jit::TraceCompiler for RecordingTraceCompiler {
1565 // `storage` is passed through to the inner backend.
1566 fn try_compile_trace(
1567 &self,
1568 storage: &mut dyn luna_core::jit::JitStorage,
1569 record: &TraceRecord,
1570 opts: CompileOptions,
1571 ) -> Option<CompiledTrace> {
1572 capture_record(record);
1573 self.inner.try_compile_trace(storage, record, opts)
1574 }
1575
1576 fn try_compile_trace_for(
1577 &self,
1578 storage: &mut dyn luna_core::jit::JitStorage,
1579 record: &TraceRecord,
1580 opts: CompileOptions,
1581 version: LuaVersion,
1582 ) -> Option<CompiledTrace> {
1583 capture_record(record);
1584 self.inner
1585 .try_compile_trace_for(storage, record, opts, version)
1586 }
1587
1588 fn last_compile_checkpoint(&self) -> &'static str {
1589 self.inner.last_compile_checkpoint()
1590 }
1591}
1592
1593/// Run a warmup `Vm` on the dumped bytecode, harvest closed
1594/// `TraceRecord`s the trace JIT compiled, re-lower each through
1595/// `lower_trace_into_named` into a fresh `ObjectModule`, and write the
1596/// produced bytes (plus a `luna_trace_meta` index + `luna_trace_blob`
1597/// payload) to `out`.
1598///
1599/// Returns `Ok(HarvestedTraces::None)` if the warmup recorded zero
1600/// AOT-installable traces — the calling pipeline then skips the .o on
1601/// the link line entirely (no placeholder .o on disk).
1602///
1603/// `dump_bytes` is the same chunk the deploy binary will execute, so
1604/// the proto identities (and therefore `stable_hash`) match between
1605/// AOT compile and deploy load.
1606fn harvest_and_emit_aot_traces(
1607 dump_bytes: &[u8],
1608 version: LuaVersion,
1609 out: &Path,
1610 target: &TargetSpec,
1611) -> Result<HarvestedTraces, AotError> {
1612 use cranelift_codegen::settings::{self, Configurable};
1613 use cranelift_module::{DataDescription, Linkage, Module, default_libcall_names};
1614 use cranelift_object::{ObjectBuilder, ObjectModule};
1615
1616 // Reset the capture buffer for this harvest call. A previous run
1617 // in the same process (e.g. unit tests running back-to-back) must
1618 // not leak its records into this one.
1619 AOT_CAPTURED_RECORDS.with(|cell| cell.borrow_mut().clear());
1620
1621 // Build the warmup Vm + install the recording compiler. We use
1622 // `new_with_jit` (= new_minimal_with_jit + open_all_libs) so the
1623 // script can call `print`, `math.*`, etc. — typical hot loops in
1624 // realistic programs touch these libs.
1625 let mut vm = luna_jit::new_with_jit(version);
1626 vm.install_jit_backend(
1627 luna_jit::jit_backend::CraneliftBackend,
1628 RecordingTraceCompiler {
1629 inner: luna_jit::jit_backend::CraneliftBackend,
1630 },
1631 );
1632 vm.set_trace_jit_enabled(true);
1633 // Chunk JIT short-circuits recursive `Op::Call` at exec.rs:1567 before
1634 // push_frame, hiding the helper body from the trace recorder and
1635 // suppressing the inline side-exit chain. Trace JIT
1636 // subsumes chunk JIT's coverage and adds the inline-side-exit support
1637 // chunk JIT lacks entirely, so harvest skips chunk JIT.
1638 vm.set_jit_enabled(false);
1639 vm.set_bytecode_loading(true);
1640
1641 // Load + call the root closure. Errors here are **non-fatal** —
1642 // the script may rely on host-side state we don't have (CLI args,
1643 // env vars), may `error()` intentionally as part of normal control
1644 // flow, or may diverge from any reproducible path. The warmup's
1645 // job is to surface hot traces, not to validate the script —
1646 // produce 0 AOT traces and move on if anything goes sideways.
1647 //
1648 // The deploy binary still runs the script through interp + JIT;
1649 // a missing AOT fast-path doesn't break correctness, only perf.
1650 let closure = match vm.load(dump_bytes, b"=embedded-aot-warmup") {
1651 Ok(c) => c,
1652 Err(_) => return Ok(HarvestedTraces::None),
1653 };
1654 let _ = vm.call_value(Value::Closure(closure), &[]);
1655
1656 // Snapshot the captured records. Take ownership so the thread-
1657 // local Vec is empty going forward (matches the test-isolation
1658 // invariant established at the top of this fn).
1659 let captured: Vec<([u8; 16], u32, TraceRecord)> =
1660 AOT_CAPTURED_RECORDS.with(|cell| std::mem::take(&mut *cell.borrow_mut()));
1661
1662 let probe_on = std::env::var_os("LUNA_AOT_HARVEST_PROBE").is_some();
1663 if probe_on {
1664 eprintln!("luna-aot harvest: captured {} TraceRecords", captured.len());
1665 }
1666 if captured.is_empty() {
1667 return Ok(HarvestedTraces::None);
1668 }
1669
1670 // Pair each captured record with the actual CompiledTrace that
1671 // landed in the proto's traces vec — we need the post-lower fields
1672 // (window_size, entry_tags, exit_tags, dispatchable,
1673 // global_tag_res_kind) for the meta blob. The record-by-record
1674 // lookup keys on (proto_ptr, head_pc); records that didn't survive
1675 // compile (lowerer bailed, returned None) drop here.
1676 //
1677 // Filter to AOT-installable shapes. Wire format v2 carries
1678 // `per_exit_tags` (typed-register side-exit guards — GetUpval-
1679 // heavy traces). Wire format v3 carries `per_exit_inline`
1680 // (depth>0 inlined cmp side-exits).
1681 //
1682 // The wire format also doesn't ship sunk-alloc materialize
1683 // sites yet; `materialize_emit_count > 0` traces need that
1684 // path too — JIT-only.
1685 let mut installable: Vec<(
1686 usize,
1687 [u8; 16],
1688 u32,
1689 TraceRecord,
1690 luna_core::jit::send_compat::TArc<CompiledTrace>,
1691 )> = Vec::new();
1692 let mut filter_stats = (0usize, 0usize, 0usize, 0usize);
1693 for (i, (hash, head_pc, record)) in captured.into_iter().enumerate() {
1694 let proto = record.head_proto;
1695 let traces_ref = proto.traces.borrow();
1696 let Some(ct) = traces_ref.iter().find(|c| c.head_pc == head_pc).cloned() else {
1697 filter_stats.0 += 1;
1698 continue;
1699 };
1700 drop(traces_ref);
1701 if !ct.dispatchable {
1702 filter_stats.1 += 1;
1703 continue;
1704 }
1705 if !ct.per_exit_inline.is_empty() {
1706 // depth>0 inlined cmp side-exits are supported. The lowerer's
1707 // `emit_chain_ptr_arg` routes the `FrameMaterializeInfo`
1708 // chain pointer through a relocatable data slot the
1709 // deploy-side `aot_inline_chain_resolver` populates at
1710 // startup; the v3 wire format's `per_exit_inline` tail
1711 // (cont_pc / head_resume_pc / packed exit_tags / packed
1712 // chain bytes) round-trips into a fresh
1713 // `Rc<[InlineSideExit]>` on the install side. The JIT path
1714 // bakes that chain pointer as a raw `iconst`, which under
1715 // AOT would be the warmup VM's heap address, invalid in the
1716 // deploy binary. Stat counter for diagnostics: how many of
1717 // the captured traces went through the v3 inline path.
1718 filter_stats.2 += 1;
1719 }
1720 // per_exit_tags is supported by wire format v2 — accept.
1721 // (Counter for diagnostics: how many of the captured traces
1722 // went through the v2 path.)
1723 if !ct.per_exit_tags.is_empty() {
1724 filter_stats.3 += 1;
1725 }
1726 installable.push((i, hash, head_pc, record, ct));
1727 }
1728
1729 if probe_on {
1730 eprintln!(
1731 "luna-aot harvest: installable={}, filtered: no_ct={}, undispatchable={}, accepted_with_per_exit_inline={}, accepted_with_per_exit_tags={}",
1732 installable.len(),
1733 filter_stats.0,
1734 filter_stats.1,
1735 filter_stats.2,
1736 filter_stats.3,
1737 );
1738 }
1739 if installable.is_empty() {
1740 return Ok(HarvestedTraces::None);
1741 }
1742
1743 // Build the ObjectModule for the trace .o. PIC required for ELF/
1744 // Mach-O linker relocations.
1745 //
1746 // `cranelift_isa_builder()` resolves the per-target ISA (host =
1747 // `cranelift_native` for CPU-feature detection; cross =
1748 // `isa::lookup` over the parsed triple). For cross targets we get
1749 // a `TargetIsa` whose codegen matches the deploy ABI rather than
1750 // the build host's.
1751 let isa = {
1752 let mut flag_builder = settings::builder();
1753 flag_builder
1754 .set("use_colocated_libcalls", "false")
1755 .expect("flag");
1756 flag_builder.set("is_pic", "true").expect("flag");
1757 flag_builder.set("opt_level", "speed").expect("flag");
1758 target
1759 .cranelift_isa_builder()?
1760 .finish(settings::Flags::new(flag_builder))
1761 .map_err(|e| {
1762 AotError::Object(format!(
1763 "Cranelift ISA finish for target {}: {e}",
1764 target.triple
1765 ))
1766 })?
1767 };
1768 let object_builder = ObjectBuilder::new(isa, "luna_aot_traces", default_libcall_names())
1769 .map_err(|e| AotError::Object(format!("ObjectBuilder: {e}")))?;
1770 let mut module = ObjectModule::new(object_builder);
1771
1772 // Per-trace emission: lower IR + meta blob + index entry.
1773 // We accumulate the meta blobs into one combined `luna_trace_blob`
1774 // data symbol (so the linker has a single object per pipeline,
1775 // not N), and emit one `luna_trace_meta_<idx>` entry per trace.
1776 let mut blob_payload: Vec<u8> = Vec::new();
1777 // Per-trace `(fn_name, meta_blob_offset_within_combined, meta_blob_len)`.
1778 let mut per_trace_meta: Vec<(String, u32, u32)> = Vec::new();
1779 for (idx, _hash, _head_pc, record, ct) in installable.iter() {
1780 let fn_name = format!("luna_aot_trace_{idx:08x}");
1781 let opts = CompileOptions {
1782 internal_loop: true,
1783 // the same dialect flag the JIT compiled these records with
1784 pre53: version <= LuaVersion::Lua53,
1785 aot: true,
1786 };
1787 // Re-lower this record into the ObjectModule under a unique
1788 // exported name. Any bail here = the record was lowerable at
1789 // warmup time (it appeared in the captured list AND the
1790 // matched ct is non-None) but failed under aot=true codegen —
1791 // most likely a relocation path that fails for some opcode the
1792 // AOT lowerer's strkey resolver doesn't cover. Skip + continue;
1793 // the trace will fall back to JIT at deploy time.
1794 let lower_res = luna_jit::jit_backend::trace::lower_trace_into_named_for(
1795 &mut module,
1796 record,
1797 opts,
1798 Some(&fn_name),
1799 version,
1800 );
1801 if lower_res.is_none() {
1802 if probe_on {
1803 eprintln!(
1804 "luna-aot harvest: AOT lower bailed for trace idx={idx} head_pc={}",
1805 ct.head_pc
1806 );
1807 }
1808 continue;
1809 }
1810
1811 // Serialize the meta blob for this trace.
1812 let entry_tags_vec: Vec<u8> = ct.entry_tags.iter().copied().collect();
1813 let exit_tags_vec: Vec<u8> = ct.exit_tags.iter().copied().map(pack_exit_tag).collect();
1814 // v2 tail — per-cont_pc typed-register side-exit shapes.
1815 // Each entry's `tags_packed` mirrors the JIT-time
1816 // `(cont_pc, Rc<[ExitTag]>)` pair, packed through
1817 // `pack_exit_tag` so the deploy-side reader unpacks via the
1818 // same byte → ExitTag mapping.
1819 let per_exit_tags_entries: Vec<PerExitTagsEntry> = ct
1820 .per_exit_tags
1821 .iter()
1822 .map(|(cont_pc, tags)| PerExitTagsEntry {
1823 cont_pc: *cont_pc,
1824 tags_packed: tags.iter().copied().map(pack_exit_tag).collect(),
1825 })
1826 .collect();
1827 let header = AotTraceMetaHeader {
1828 magic: luna_core::jit::aot_meta::AOT_META_MAGIC,
1829 version: luna_core::jit::aot_meta::AOT_META_VERSION,
1830 head_pc: ct.head_pc,
1831 n_ops: ct.n_ops,
1832 window_size: ct.window_size,
1833 dispatchable: u8::from(ct.dispatchable),
1834 tag_res_kind: pack_tag_res_kind(ct.global_tag_res_kind),
1835 entry_tags_len: entry_tags_vec.len() as u16,
1836 exit_tags_len: exit_tags_vec.len() as u32,
1837 };
1838 // Populate the v3 inline tail from the live trace's
1839 // `per_exit_inline`. Each `InlineSideExit` round-trips into a
1840 // `PerExitInlineEntry` via the byte-stable converter: tags pack
1841 // through `pack_exit_tag` and the chain serialises as raw
1842 // `repr(C)` bytes (12 per record). The deploy install path decodes
1843 // the entries back into fresh `Rc<[FrameMaterializeInfo]>` /
1844 // `Rc<[ExitTag]>` allocations whose contents match what the
1845 // JIT-time recorder produced; the IR's chain pointer is fed
1846 // from a separate `aot_inline_chain_resolver`-populated
1847 // slot (no shared ownership with the dispatcher field —
1848 // neither side compares pointers).
1849 let per_exit_inline_entries: Vec<PerExitInlineEntry> = ct
1850 .per_exit_inline
1851 .iter()
1852 .map(PerExitInlineEntry::from_inline_side_exit)
1853 .collect();
1854 let blob = encode_meta_blob(
1855 &header,
1856 &entry_tags_vec,
1857 &exit_tags_vec,
1858 &per_exit_tags_entries,
1859 &per_exit_inline_entries,
1860 );
1861 let blob_offset = blob_payload.len() as u32;
1862 let blob_len = blob.len() as u32;
1863 blob_payload.extend_from_slice(&blob);
1864 per_trace_meta.push((fn_name, blob_offset, blob_len));
1865 }
1866
1867 if per_trace_meta.is_empty() {
1868 // Everything filtered out by the AOT lower bail. Same shape as
1869 // "no traces at all".
1870 return Ok(HarvestedTraces::None);
1871 }
1872
1873 // Emit the combined `luna_trace_blob` data symbol. Single object;
1874 // each per-trace meta entry references it via offset.
1875 let blob_data_id = module
1876 .declare_data("__luna_trace_blob_combined", Linkage::Local, false, false)
1877 .map_err(|e| AotError::Object(format!("declare_data blob: {e}")))?;
1878 {
1879 let mut desc = DataDescription::new();
1880 desc.define(blob_payload.into_boxed_slice());
1881 // Read-only data section. Mach-O caps section names at 16 chars;
1882 // `luna_trace_blob` is 15 — fits with one byte of headroom for
1883 // the implicit comparator NUL.
1884 // `__DATA` segment on Mach-O — the deploy walker doesn't
1885 // bracket this section (each meta entry references it by
1886 // pointer relocation), but keeping it next to `luna_trace_
1887 // meta` in `__DATA` is the path of least surprise for ld /
1888 // strip.
1889 //
1890 // PE section names are capped at 8 bytes in the final image;
1891 // COFF uses `.lt_blob` (7 chars + leading `.`) so the post-link
1892 // PE preserves the name byte-for-byte. The deploy walker
1893 // doesn't bracket-look this section (it's only referenced via
1894 // pointer relocations from `.lt_meta` entries), so the name
1895 // choice is mostly for consistency / debuggability.
1896 desc.set_custom_section(&aot_data_section(
1897 module.isa().triple(),
1898 "luna_trace_blob",
1899 ".lt_blob",
1900 ));
1901 module
1902 .define_data(blob_data_id, &desc)
1903 .map_err(|e| AotError::Object(format!("define_data blob: {e}")))?;
1904 }
1905
1906 // Emit one 48-byte `luna_trace_meta_<idx>` index entry per trace
1907 // into the dedicated `luna_trace_meta` section. The static linker
1908 // auto-brackets via `__start_luna_trace_meta` / `__stop_luna_trace_
1909 // meta` (ELF) or `section$start$__DATA$luna_trace_meta` (Mach-O).
1910 for (idx, (fn_name, blob_offset, blob_len)) in per_trace_meta.iter().enumerate() {
1911 let hash = installable[idx].1;
1912 let head_pc = installable[idx].2;
1913
1914 let entry_data_id = module
1915 .declare_data(
1916 &format!("__luna_trace_meta_entry_{idx:08x}"),
1917 Linkage::Local,
1918 false,
1919 false,
1920 )
1921 .map_err(|e| AotError::Object(format!("declare_data meta entry: {e}")))?;
1922 let mut desc = DataDescription::new();
1923 // 48 bytes: [hash 16] [head_pc 4] [_pad 4] [fn_ptr 8] [meta_ptr 8] [meta_len 4] [_pad2 4]
1924 let mut payload = [0u8; 48];
1925 payload[0..16].copy_from_slice(&hash);
1926 payload[16..20].copy_from_slice(&head_pc.to_le_bytes());
1927 // payload[20..24] _pad
1928 // payload[24..32] fn_ptr — relocation
1929 // payload[32..40] meta_ptr — relocation
1930 payload[40..44].copy_from_slice(&blob_len.to_le_bytes());
1931 desc.define(Box::new(payload));
1932 // On Mach-O the section must be in `__DATA` so it merges with
1933 // the cmain shim's `__DATA,luna_trace_meta` placeholder; in any
1934 // other segment the deploy walker's
1935 // `section$start$__DATA$luna_trace_meta` sees only the
1936 // placeholder.
1937 //
1938 // COFF: short name `.lt_meta` matches the cmain shim's
1939 // placeholder section and the deploy walker's
1940 // [`windows_section::find_section`] needle. PE section names
1941 // are capped at 8 bytes in the final linked image —
1942 // `luna_trace_meta` (15 chars) would either truncate
1943 // unpredictably or land in the COFF string table that the
1944 // linker drops.
1945 desc.set_custom_section(&aot_data_section(
1946 module.isa().triple(),
1947 "luna_trace_meta",
1948 ".lt_meta",
1949 ));
1950 // 8-byte alignment for the fn_ptr / meta_ptr relocations at
1951 // offsets 24 and 32. Without explicit `set_align(8)` the
1952 // entries can land at odd offsets in the .o, and Mach-O's
1953 // `ld` rejects the unaligned pointer slots ("pointer not
1954 // aligned in `___luna_trace_meta_entry_…`+0x20").
1955 desc.set_align(8);
1956
1957 // Declare the trace fn as Import so we can relocate against it.
1958 // (It's Export'd by `lower_trace_into_named` above.) The
1959 // declare_function call here re-uses the same name; cranelift
1960 // module name-interning matches the two so the relocation
1961 // resolves to the lowerer-emitted body.
1962 let trace_fn_sig = {
1963 let mut sig = module.make_signature();
1964 sig.params.push(cranelift_codegen::ir::AbiParam::new(
1965 cranelift_codegen::ir::types::I64,
1966 ));
1967 sig.returns.push(cranelift_codegen::ir::AbiParam::new(
1968 cranelift_codegen::ir::types::I64,
1969 ));
1970 sig
1971 };
1972 let fn_id = module
1973 .declare_function(fn_name, Linkage::Import, &trace_fn_sig)
1974 .map_err(|e| AotError::Object(format!("declare_function for reloc: {e}")))?;
1975 let fn_gv = module.declare_func_in_data(fn_id, &mut desc);
1976 desc.write_function_addr(24, fn_gv);
1977 let blob_gv = module.declare_data_in_data(blob_data_id, &mut desc);
1978 desc.write_data_addr(32, blob_gv, *blob_offset as i64);
1979
1980 module
1981 .define_data(entry_data_id, &desc)
1982 .map_err(|e| AotError::Object(format!("define_data meta entry: {e}")))?;
1983 }
1984
1985 let product = module.finish();
1986 let bytes = product
1987 .emit()
1988 .map_err(|e| AotError::Object(format!("ObjectProduct::emit: {e}")))?;
1989 fs::write(out, &bytes)?;
1990 Ok(HarvestedTraces::Some)
1991}
1992
1993/// Custom section name for trace data on the target's object format.
1994/// Mach-O takes `segment,section`; COFF gets the 8-byte name because
1995/// the final PE keeps only short names; ELF takes the name as is.
1996fn aot_data_section(triple: &target_lexicon::Triple, name: &str, coff_name: &str) -> String {
1997 match triple.binary_format {
1998 target_lexicon::BinaryFormat::Macho => format!("__DATA,{name}"),
1999 target_lexicon::BinaryFormat::Coff => coff_name.to_owned(),
2000 _ => name.to_owned(),
2001 }
2002}