Skip to main content

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}