Skip to main content

luna_runtime_helpers/
lib.rs

1#![warn(missing_docs)]
2//! luna-runtime-helpers — the static-link runtime entry for the
3//! binaries that `luna-aot` produces.
4//!
5//! # Role in the v1.3 Phase AOT pipeline
6//!
7//! `luna-aot compile foo.lua --out foo` walks:
8//!
9//! 1. Parse + compile `foo.lua` to a luna bytecode dump (Stages 1-2).
10//! 2. Emit a `.luna.bytecode` data section in a fresh `.o` (Stage 5).
11//! 3. **Build this crate as a `staticlib`** — `libluna_runtime_helpers.a`
12//!    bundles the rust stdlib + luna-core + this thin C-ABI entry.
13//! 4. Emit a tiny C `main` that calls into [`luna_aot_run`] passing
14//!    the bracket-symbol bounds of the bytecode section (Stage 6).
15//! 5. `cc` links `bytecode.o` + `main.o` + `libluna_runtime_helpers.a`
16//!    + `-lpthread -ldl -lm` into the final executable.
17//!
18//! The produced binary at run time:
19//!
20//! - the C `main` calls [`luna_aot_run(bytecode_ptr, len)`][luna_aot_run]
21//! - [`luna_aot_run`] constructs a `Vm`, allows bytecode loading,
22//!   calls `Vm::load(slice, b"=embedded")` (which routes through
23//!   `luna_core::vm::dump::undump` because the slice starts with
24//!   `\x1bLua`), then `Vm::call_value` on the resulting root closure
25//! - normal `print(...)` from the script lands on stdout via
26//!   `std::io::stdout` inside luna-core's builtins (no surprises)
27//! - exit code 0 on success, 1 on load / runtime error
28//!
29//! # Why a separate crate (not folded into `luna-aot`)
30//!
31//! `luna-aot` is the **build-time** tool — it pulls `object` + `clap`
32//! and eventually all of cranelift. The **deploy-side** binary must
33//! not pull cranelift; it only needs the luna interp. Splitting this
34//! entry into its own crate keeps the deploy-side `.a` tight (rust
35//! stdlib + luna-core only) and lets `luna-aot` invoke
36//! `cargo build -p luna-runtime-helpers --release` without dragging
37//! its own dep tree into the link.
38//!
39//! # luna-core 0-third-party-dep contract
40//!
41//! Unchanged. `cargo tree -p luna-core --prefix none | grep -cE " v[0-9]"`
42//! continues to report 1. This crate sits **above** luna-core in the
43//! dep graph; nothing here flows back into luna-core.
44
45use std::panic;
46use std::slice;
47
48use luna_core::runtime::Value;
49use luna_core::version::LuaVersion;
50use luna_core::vm::Vm;
51
52// v1.3 Phase AOT Stage 7 polish 3 — Windows PE/COFF section walker.
53// Used by `aot_strkey_resolver` and `aot_trace_registry` to enumerate
54// the deploy-side `lt_meta` / `lt_skix` sections on Windows, where
55// the Unix-style `__start_/__stop_` bracket symbol convention isn't
56// synthesized by `link.exe` / `lld-link`. Hand-rolled winapi externs
57// keep the dep story unchanged (no `windows-sys` / `winapi` crate
58// added). See module docs for the design rationale.
59#[cfg(all(target_os = "windows", feature = "jit-helpers"))]
60mod windows_section;
61
62/// AOT-binary C-ABI entry. The auto-generated C `main` calls this
63/// once with a pointer + length pair derived from the bracket
64/// symbols `__luna_bytecode_start` / `__luna_bytecode_end` that
65/// `luna-aot` emits into the `.luna.bytecode` section.
66///
67/// Returns the process exit code:
68///
69/// - `0` — script ran to completion (any `return` values are ignored,
70///   matching `lua foo.lua` semantics: PUC discards top-level returns)
71/// - `1` — bytecode load failed (header mismatch, truncated dump,
72///   unsupported opcode), runtime error, or a panic escaped luna-core
73///
74/// # Safety
75///
76/// `bytecode` must point at `len` bytes of a valid luna bytecode dump
77/// (the bytes that `luna_core::vm::dump::dump` produces). The slice
78/// must remain live and unmutated for the duration of the call —
79/// in the AOT-binary use case the bytes live in the read-only data
80/// segment of the binary itself, so this is trivially satisfied.
81///
82/// `len` must not be 0 (an empty dump is rejected by `Vm::load` with
83/// a clear error, but we early-out before constructing the slice to
84/// avoid a `from_raw_parts(null, 0)` UB corner). `len == 0` returns 1.
85///
86/// Panics inside luna-core (which would normally tear down a Rust
87/// host process) are caught here and turned into exit code 1 with the
88/// payload printed to stderr.
89#[unsafe(no_mangle)]
90pub unsafe extern "C" fn luna_aot_run(bytecode: *const u8, len: usize) -> i32 {
91    // Defensive: a null/zero-len section means the linker didn't wire
92    // the bytecode object — clearer error than a slice deref.
93    if bytecode.is_null() || len == 0 {
94        eprintln!(
95            "luna-runtime-helpers: embedded bytecode section is empty \
96             (ptr={bytecode:p}, len={len}) — was the bytecode .o linked in?"
97        );
98        return 1;
99    }
100
101    // v1.3 Stage 7 follow-on — pin the `luna_jit_*` helper symbols
102    // into the staticlib's link graph by way of a runtime call edge
103    // from this entry. Without a call edge, fat-LTO observes that
104    // `force_link_jit_helpers` is unreferenced from the staticlib's
105    // exported API surface and elides the entire pin module — which
106    // cascades and lets the staticlib bundling step drop every
107    // `luna_jit_*`-defining cgu from `luna-jit`'s rlib. The result
108    // would be a clean `cargo build` followed by an unresolved-symbol
109    // failure at the AOT binary's link step ("undefined reference to
110    // `_luna_jit_table_get_field`"). `black_box` on the return value
111    // is what makes LTO unable to fold the call to a no-op.
112    #[cfg(feature = "jit-helpers")]
113    {
114        let n = jit_helpers_pin::force_link_jit_helpers();
115        std::hint::black_box(n);
116    }
117
118    // v1.3 Phase AOT Stage 7 sub-piece 3 (PENDING) — interned-string
119    // slot resolver.
120    //
121    // Sub-piece 2 (commits adding `CompileOptions { aot: true }`)
122    // changed the trace lowerer to emit data symbols of the form
123    // `__luna_aot_strkey_slot_<hex>` (writable, 8-byte) and
124    // `__luna_aot_strkey_bytes_<hex>` (read-only, `[u64 len ||
125    // utf8...]`). The IR loads through the slot to get a
126    // `Gc<LuaStr>::as_ptr()`. Slots are zero-initialised at link
127    // time; reading through one without a resolver write would
128    // dereference NULL on the first trace dispatch.
129    //
130    // Sub-piece 3 must, BEFORE `run_inner` reaches any AOT trace
131    // dispatch:
132    //
133    // 1. Walk every `__luna_aot_strkey_bytes_*` symbol present in
134    //    the link image. Two options for enumeration:
135    //    a) Bracket the bytes section with linker-provided start/end
136    //       symbols (`__start___luna_aot_strkey_bytes` /
137    //       `__stop___luna_aot_strkey_bytes`, available on
138    //       gnu-ld / lld / Mach-O via `__section$start$...`).
139    //    b) Use cranelift's `Module::declare_data` to ALSO emit a
140    //       small registry section listing `(slot_id, bytes_id)`
141    //       pairs and walk that — strip-friendly across all targets.
142    // 2. For each entry: read len from `[0..8]`, bytes from `[8..]`,
143    //    call `vm.heap.intern(bytes)`, write the resulting
144    //    `Gc<LuaStr>::as_ptr()` (as `i64`) into the matching slot.
145    // 3. The resolver runs once, idempotent. Slots staying NULL
146    //    after resolve = bug (most likely missing `_bytes_*` for a
147    //    given `_slot_*`).
148    //
149    // Effort: 1-2 dev-days. Blocker: sub-piece 4 (trace registry +
150    // dispatch install).
151
152    // SAFETY: caller contract — `bytecode` points at `len` valid bytes
153    // for the duration of this call. In the AOT-binary deploy shape
154    // these bytes live in the binary's `.rodata` and are immutable
155    // for the lifetime of the process.
156    let bytecode_slice: &'static [u8] = unsafe { slice::from_raw_parts(bytecode, len) };
157
158    let result = panic::catch_unwind(panic::AssertUnwindSafe(|| run_inner(bytecode_slice)));
159    match result {
160        Ok(code) => code,
161        Err(payload) => {
162            // Mirror the std panic hook's payload-shape extraction so
163            // users see roughly the same message a panic would print
164            // when not caught.
165            let msg = panic_payload_text(&payload);
166            eprintln!("luna-runtime-helpers: vm panicked: {msg}");
167            1
168        }
169    }
170}
171
172/// The Rust-side body of [`luna_aot_run`]. Split out so the C-ABI
173/// boundary stays minimal and the `panic::catch_unwind` closure has
174/// a clear, self-contained body.
175fn run_inner(bytecode: &[u8]) -> i32 {
176    // The dialect picked here governs which header bytes `Vm::load`
177    // accepts. v1.3 floor pins this to 5.5 — the `luna-aot` CLI
178    // default. A `--dialect 5.4` invocation would compile against
179    // 5.4's header; the v1.3 floor relies on the embedder running the
180    // AOT pipeline with a matching dialect on both sides. Stage 5+
181    // can embed a `__luna_version` byte and dispatch dynamically.
182    let mut vm = Vm::new(LuaVersion::Lua55);
183
184    // `Vm::new` defaults to `bytecode_loading = true` (see luna-core
185    // `exec.rs:910`), but a future sandbox-default flip would break
186    // silently here. Setting it explicitly makes the intent legible
187    // and survives any default change.
188    vm.set_bytecode_loading(true);
189
190    // v1.3 Phase AOT Stage 7 trace-coverage follow-up — install the
191    // real Cranelift JIT backend (= `enter_jit` that pins `JIT_VM` /
192    // `JIT_CL` TLS) BEFORE any AOT-emitted trace mcode dispatches.
193    //
194    // Without this swap, the deploy `Vm` runs `NullJitBackend.enter`,
195    // which is a no-op — `JIT_VM` TLS stays null, and the first AOT
196    // trace that calls any `luna_jit_*` helper (e.g. `_table_get_field`,
197    // `_op_get_tab_up`, `_table_set_int`) hits `debug_assert!(!p.is_null
198    // ())` in debug builds or dereferences null in release →
199    // SIGSEGV.
200    //
201    // Recorder is irrelevant on the deploy side (AOT traces install
202    // before any record fires; the active `trace_compiler` would never
203    // get called), but `IntChunkCompiler::enter` IS load-bearing —
204    // it's the function the dispatcher calls right before
205    // `entry_fn(reg_state)`.
206    //
207    // `install_jit_backend` is luna-core API; `CraneliftBackend`
208    // implements both `IntChunkCompiler` (whose `enter` is what we
209    // actually need) and `TraceCompiler`. Wrap behind `jit-helpers`
210    // feature so a future no-JIT-on-deploy build can opt out (in
211    // which case AOT traces that touch helpers would have to be
212    // filtered at AOT-compile time — currently all of them do).
213    #[cfg(feature = "jit-helpers")]
214    {
215        vm.install_jit_backend(
216            luna_jit::jit_backend::CraneliftBackend,
217            luna_jit::jit_backend::CraneliftBackend,
218        );
219        // NOTE: `trace_enabled = true` (the TA3 ship default) is
220        // load-bearing for AOT dispatch too — `Vm::run`'s trace
221        // lookup gate is `if self.jit.trace_enabled`, used for BOTH
222        // runtime-compiled traces AND AOT-installed traces.
223        // Disabling here would silently skip the AOT install's
224        // dispatch. Runtime re-recording for back-edges the AOT
225        // didn't cover is fine — same pattern interp + JIT uses.
226    }
227
228    // v1.3 Phase AOT Stage 7 sub-piece 3 — interned-string slot
229    // resolver. Runs BEFORE `vm.load` so the resulting closure's
230    // first dispatch into AOT mcode sees populated slots. Idempotent
231    // and tolerates the empty-section case (binary linked zero AOT
232    // traces): both bracket symbols collapse to the same address, the
233    // walk terminates immediately.
234    //
235    // `vm.load` interns its own strings into `vm.heap`'s string table,
236    // which the resolver also populates here; intern is idempotent, so
237    // an AOT-time and load-time intern of the same UTF-8 bytes
238    // resolve to the same `Gc<LuaStr>` pointer — the load-bearing
239    // invariant that lets trace mcode pass interned-key pointers to
240    // the `luna_jit_*_field` helpers.
241    #[cfg(feature = "jit-helpers")]
242    {
243        let resolved = aot_strkey_resolver::resolve_all(&mut vm);
244        if std::env::var_os("LUNA_AOT_PROBE").is_some() {
245            eprintln!("luna-runtime-helpers: aot_strkey_resolved = {resolved}");
246        }
247        // v1.3 Phase AOT Stage 7 polish 6 — inline chain slot
248        // population. Must run BEFORE `aot_trace_registry::install_all`
249        // so the dispatcher's first AOT-mcode dispatch finds populated
250        // chain slots (the IR's `luna_jit_trace_materialize_frames(n,
251        // ptr)` call would otherwise deref NULL). No Vm interaction
252        // needed — the chains are pure metadata, owned by leaked Rcs.
253        let chains_resolved = aot_inline_chain_resolver::resolve_all();
254        if std::env::var_os("LUNA_AOT_PROBE").is_some() {
255            eprintln!("luna-runtime-helpers: aot_inline_chains_resolved = {chains_resolved}");
256        }
257    }
258
259    let closure = match vm.load(bytecode, b"=embedded") {
260        Ok(c) => c,
261        Err(e) => {
262            eprintln!(
263                "luna-runtime-helpers: load failed at line {}: {}",
264                e.line,
265                String::from_utf8_lossy(&e.msg)
266            );
267            return 1;
268        }
269    };
270
271    // v1.3 Phase AOT Stage 7 sub-piece 4 — install AOT-emitted traces
272    // against the loaded chunk's proto tree. Runs after `vm.load`
273    // (the resolver needs the closure's proto as the BFS root) and
274    // BEFORE `vm.call_value` (so the dispatcher's first back-edge
275    // visit finds the installed trace and fires AOT mcode, instead
276    // of bumping `trace_hot_count` from zero and going through the
277    // runtime recorder again). Empty-section tolerant: a binary with
278    // zero linked AOT trace `.o`s sees `installed == 0`, fall through
279    // to runtime JIT.
280    #[cfg(feature = "jit-helpers")]
281    {
282        // SAFETY: closure is a live Gc<LuaClosure>; reading .proto is
283        // a NonNull pointer copy. The heap is single-threaded so no
284        // concurrent mutation is possible during this read.
285        let root_proto = unsafe { (*closure.as_ptr()).proto };
286        let installed = aot_trace_registry::install_all(&mut vm, root_proto);
287        if std::env::var_os("LUNA_AOT_PROBE").is_some() {
288            eprintln!("luna-runtime-helpers: aot_trace_install_count = {installed}");
289        }
290    }
291
292    let rc = match vm.call_value(Value::Closure(closure), &[]) {
293        Ok(_results) => 0,
294        Err(err) => {
295            let msg = vm.error_text(&err);
296            eprintln!("luna-runtime-helpers: runtime error: {msg}");
297            if let Some(tb) = vm.take_error_traceback() {
298                eprintln!("{tb}");
299            }
300            1
301        }
302    };
303
304    // v2.0 Phase 5 Track AO sub-track AO-PF — post-run probe for the
305    // Stage 7 polish 6 inline-chain reloc fire path. Counts every
306    // entry to `luna_jit_trace_materialize_frames` from trace mcode
307    // (JIT-baked OR AOT polish-6 slot-loaded). In an AOT-only binary
308    // any non-zero value is direct evidence that the polish-6 chain
309    // reloc path actually fires at runtime — the resolver-side probe
310    // (`aot_inline_chains_resolved`) only confirms the slot got
311    // populated, not that any AOT mcode dispatch ever loaded it.
312    #[cfg(feature = "jit-helpers")]
313    if std::env::var_os("LUNA_AOT_PROBE").is_some() {
314        let fires = luna_jit::jit_backend::trace_materialize_frames_fires();
315        eprintln!("luna-runtime-helpers: trace_materialize_frames_fires = {fires}");
316    }
317
318    rc
319}
320
321/// Best-effort extraction of a panic payload's display text. Matches
322/// the rust stdlib's payload-shape handling so users see the same
323/// "panicked at … : <msg>" snippet shape they would expect.
324fn panic_payload_text(payload: &(dyn std::any::Any + Send)) -> String {
325    if let Some(s) = payload.downcast_ref::<&'static str>() {
326        (*s).to_string()
327    } else if let Some(s) = payload.downcast_ref::<String>() {
328        s.clone()
329    } else {
330        "(non-string panic payload)".to_string()
331    }
332}
333
334/// Convenience entry for in-process Rust drivers (`luna-aot`'s
335/// integration tests, embedders that want to invoke the same code
336/// path without going through `cc` link).
337///
338/// Identical semantics to [`luna_aot_run`] but skips the raw-ptr +
339/// `catch_unwind` shim. Panics propagate.
340pub fn run_bytecode(bytecode: &[u8]) -> i32 {
341    run_inner(bytecode)
342}
343
344// v1.3 Stage 7 follow-on — re-export of the 27 `luna_jit_*` Cranelift
345// trace-mcode helpers from `luna-jit::jit_backend`. AOT binaries whose
346// embedded `.o` calls these helpers (any trace that does table get/set,
347// upvalue read, concat, etc.) needs them resolvable as strong externs
348// at static-link time.
349//
350// The challenge: `luna-runtime-helpers` does not call these symbols
351// itself, so a plain `pub use luna_jit::jit_backend::luna_jit_*` would
352// be dead-stripped by `rustc`'s rlib → staticlib bundling step (Rust's
353// `staticlib` crate-type only preserves transitive `#[no_mangle]`
354// symbols that are reachable via a `pub` re-export chain whose roots
355// are themselves marked `#[used]` or referenced from a kept root).
356//
357// Strategy: a single `#[used] static` whose contents is an array of
358// raw fn pointers — one per helper. The static is itself reachable via
359// a `pub` from `lib.rs` (`force_link_jit_helpers`), which gives the
360// `staticlib` linker a strong reason to keep the array's contents,
361// which in turn pins each helper's `#[no_mangle] pub unsafe extern "C"`
362// definition through the rlib graph. The array is never *read* at run
363// time; it's a link-time anchor only.
364//
365// Verified post-build:
366//   `nm target/release/libluna_runtime_helpers.a | grep " T _luna_jit_" | wc -l`
367//   reports 27 (one per helper).
368// Re-export the helpers at the crate root. This pulls them into our
369// `pub` surface so rustc treats them as kept symbols. The
370// `extern "C"` + `#[no_mangle]` on the upstream definitions means
371// the linker sees them under their bare names (`luna_jit_*`) — the
372// `pub use` doesn't introduce a mangled wrapper. Combined with the
373// runtime call edge from `luna_aot_run` → `force_link_jit_helpers`
374// → helper calls (see `jit_helpers_pin` below), the staticlib
375// bundling step is forced to pull in the defining cgus.
376#[cfg(feature = "jit-helpers")]
377pub use luna_jit::jit_backend::{
378    luna_jit_materialize_sunk_table, luna_jit_new_table, luna_jit_new_table_sized,
379    luna_jit_op_close, luna_jit_op_closure, luna_jit_op_concat, luna_jit_op_get_tab_up,
380    luna_jit_op_tforcall, luna_jit_spill_to_stack, luna_jit_stack_load, luna_jit_stack_tag,
381    luna_jit_stack_update_raw, luna_jit_str_buf_acquire, luna_jit_str_buf_extend,
382    luna_jit_str_buf_intern, luna_jit_str_buf_release, luna_jit_table_get_field,
383    luna_jit_table_get_float, luna_jit_table_get_int, luna_jit_table_len, luna_jit_table_set_field,
384    luna_jit_table_set_float_float, luna_jit_table_set_int, luna_jit_table_set_nil,
385    luna_jit_table_set_raw, luna_jit_trace_materialize_frames, luna_jit_upval_get,
386};
387
388#[cfg(feature = "jit-helpers")]
389mod jit_helpers_pin {
390    use luna_jit::jit_backend as jb;
391
392    /// Type-erased fn-pointer slot. Cast site is link-time only —
393    /// nothing in this crate actually invokes the pointers.
394    type AnyFn = *const u8;
395
396    /// SAFETY: a `*const u8` of a `fn` symbol is `Send + Sync` (the
397    /// address is a process-global text-section constant). The `Sync`
398    /// impl is needed so the `static` below typechecks.
399    #[repr(transparent)]
400    struct PinnedFn(AnyFn);
401    // SAFETY: fn pointer addresses are immutable globals, safe to share
402    // across threads — they're only ever read, never dereferenced.
403    unsafe impl Sync for PinnedFn {}
404
405    /// The link-anchor array. `#[used]` (and `#[unsafe(no_mangle)]` so
406    /// nothing in the rustc dead-code pass can elide it across the rlib
407    /// → staticlib step) tells rustc + the system linker to keep this
408    /// static alive in the final object — which transitively pins each
409    /// `luna_jit_*` symbol the static references.
410    ///
411    /// The number of entries (27) must match the number of
412    /// `pub unsafe extern "C" fn luna_jit_*` in
413    /// `crates/luna-jit/src/jit_backend/mod.rs`. If a future luna-jit
414    /// commit adds a 28th helper, this array must grow in lock-step
415    /// or AOT trace `.o`s referencing the new symbol will fail to
416    /// link with `undefined reference to luna_jit_<new>`.
417    #[used]
418    #[unsafe(no_mangle)]
419    static LUNA_AOT_HELPER_PIN: [PinnedFn; 27] = [
420        PinnedFn(jb::luna_jit_new_table as AnyFn),
421        PinnedFn(jb::luna_jit_new_table_sized as AnyFn),
422        PinnedFn(jb::luna_jit_materialize_sunk_table as AnyFn),
423        PinnedFn(jb::luna_jit_table_set_int as AnyFn),
424        PinnedFn(jb::luna_jit_table_set_raw as AnyFn),
425        PinnedFn(jb::luna_jit_table_set_field as AnyFn),
426        PinnedFn(jb::luna_jit_table_get_field as AnyFn),
427        PinnedFn(jb::luna_jit_op_get_tab_up as AnyFn),
428        PinnedFn(jb::luna_jit_table_set_nil as AnyFn),
429        PinnedFn(jb::luna_jit_table_set_float_float as AnyFn),
430        PinnedFn(jb::luna_jit_table_get_int as AnyFn),
431        PinnedFn(jb::luna_jit_table_get_float as AnyFn),
432        PinnedFn(jb::luna_jit_upval_get as AnyFn),
433        PinnedFn(jb::luna_jit_op_close as AnyFn),
434        PinnedFn(jb::luna_jit_stack_update_raw as AnyFn),
435        PinnedFn(jb::luna_jit_op_concat as AnyFn),
436        PinnedFn(jb::luna_jit_str_buf_acquire as AnyFn),
437        PinnedFn(jb::luna_jit_str_buf_release as AnyFn),
438        PinnedFn(jb::luna_jit_str_buf_extend as AnyFn),
439        PinnedFn(jb::luna_jit_str_buf_intern as AnyFn),
440        PinnedFn(jb::luna_jit_op_tforcall as AnyFn),
441        PinnedFn(jb::luna_jit_stack_load as AnyFn),
442        PinnedFn(jb::luna_jit_stack_tag as AnyFn),
443        PinnedFn(jb::luna_jit_spill_to_stack as AnyFn),
444        PinnedFn(jb::luna_jit_op_closure as AnyFn),
445        PinnedFn(jb::luna_jit_trace_materialize_frames as AnyFn),
446        PinnedFn(jb::luna_jit_table_len as AnyFn),
447    ];
448
449    /// Pulls the link-anchor static into the public API surface so
450    /// downstream `cargo build --release -p luna-runtime-helpers`
451    /// keeps it through the rlib → staticlib bundling step.
452    ///
453    /// Returns the count of pinned helper slots. The body calls each
454    /// helper through `std::hint::black_box`'d branches that are
455    /// gated on an always-false runtime flag — the calls never
456    /// execute, but rustc + LTO can't prove that without inlining
457    /// every helper, so the call edges remain in the call graph and
458    /// the staticlib bundler pulls in the cgus that define each
459    /// helper.
460    ///
461    /// Pure-pointer references (the `LUNA_AOT_HELPER_PIN` static)
462    /// alone are not enough — Rust's staticlib bundling step only
463    /// picks up cgus that are reachable through the call graph, not
464    /// through "address taken" graphs (verified empirically:
465    /// `nm` reports `T _luna_jit_*` count = 0 when only the static
466    /// references the helpers).
467    ///
468    /// # Safety
469    ///
470    /// All `luna_jit_*` helpers are `unsafe extern "C"` and must be
471    /// called under an active [`luna_jit::jit_backend::enter_jit`]
472    /// guard. The branches below are gated on
473    /// `black_box(false)` so the calls never execute at run time;
474    /// they exist solely as link-time anchors. Calling
475    /// `force_link_jit_helpers` is therefore safe despite invoking
476    /// `unsafe` functions inside the (unreachable) branch body.
477    /// Run-time-mutable flag that defeats LTO's branch elimination on
478    /// the `if NEVER.load(...) { /* call helpers */ }` guard below.
479    ///
480    /// `black_box(false)` alone is not enough under `lto = true` —
481    /// the cross-crate LTO inliner observes the branch as dead and
482    /// strips the calls (verified empirically: with the
483    /// `if black_box(false)` form the cgu containing
484    /// `force_link_jit_helpers` had zero `U _luna_jit_*` refs).
485    ///
486    /// `AtomicBool` with default `false` + `Ordering::Relaxed` load
487    /// is opaque to LTO — the optimizer cannot prove the atomic is
488    /// never written by another translation unit, so the branch
489    /// survives. The atomic IS never written (nobody calls a
490    /// setter), so the branch is dynamically dead at run time.
491    static NEVER_TRIP: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
492
493    #[allow(unreachable_code)]
494    pub fn force_link_jit_helpers() -> usize {
495        // Address-table touch keeps `LUNA_AOT_HELPER_PIN` live.
496        let mut sum: usize = 0;
497        for slot in LUNA_AOT_HELPER_PIN.iter() {
498            sum = sum.wrapping_add(std::hint::black_box(slot.0 as usize));
499        }
500
501        // Call-graph anchor — gated by an atomic load LTO can't
502        // constant-fold. Branch never executes at run time
503        // (`NEVER_TRIP` is never written), but the call edges to each
504        // `luna_jit_*` helper survive into the staticlib bundling.
505        if NEVER_TRIP.load(std::sync::atomic::Ordering::Relaxed) {
506            // SAFETY: the surrounding `if black_box(false)` is
507            // never entered at run time. The calls exist solely to
508            // pin the helper symbols' cgus into the staticlib
509            // bundling step's reachable set.
510            unsafe {
511                let _ = jb::luna_jit_new_table();
512                let _ = jb::luna_jit_new_table_sized(0);
513                let _ = jb::luna_jit_materialize_sunk_table(
514                    0,
515                    std::ptr::null(),
516                    std::ptr::null(),
517                    0,
518                    std::ptr::null(),
519                    std::ptr::null(),
520                    std::ptr::null(),
521                );
522                jb::luna_jit_table_set_int(0, 0, 0);
523                jb::luna_jit_table_set_raw(0, 0, 0, 0);
524                jb::luna_jit_table_set_field(0, 0, 0, 0);
525                let _ = jb::luna_jit_table_get_field(0, 0);
526                let _ = jb::luna_jit_op_get_tab_up(0, 0);
527                jb::luna_jit_table_set_nil(0, 0);
528                jb::luna_jit_table_set_float_float(0, 0, 0);
529                let _ = jb::luna_jit_table_get_int(0, 0);
530                let _ = jb::luna_jit_table_get_float(0, 0);
531                let _ = jb::luna_jit_upval_get(0);
532                let _ = jb::luna_jit_op_close(0);
533                jb::luna_jit_stack_update_raw(0, 0);
534                let _ = jb::luna_jit_op_concat(0, 0);
535                let _ = jb::luna_jit_str_buf_acquire();
536                jb::luna_jit_str_buf_release(0);
537                let _ = jb::luna_jit_str_buf_extend(0, 0);
538                let _ = jb::luna_jit_str_buf_intern(0);
539                let _ = jb::luna_jit_op_tforcall(
540                    0,
541                    0,
542                    std::ptr::null_mut(),
543                    std::ptr::null_mut(),
544                    std::ptr::null_mut(),
545                );
546                let _ = jb::luna_jit_stack_load(0);
547                let _ = jb::luna_jit_stack_tag(0);
548                jb::luna_jit_spill_to_stack(0, 0, 0);
549                let _ = jb::luna_jit_op_closure(0);
550                let _ = jb::luna_jit_trace_materialize_frames(0, std::ptr::null());
551                let _ = jb::luna_jit_table_len(0);
552            }
553        }
554
555        std::hint::black_box(sum);
556        LUNA_AOT_HELPER_PIN.len()
557    }
558}
559
560/// v1.3 Stage 7 follow-on — pull all 27 `luna_jit_*` Cranelift
561/// trace-mcode helper symbols into the deploy-side staticlib's
562/// linkmap. Called by the AOT-generated C `main` stub or by the
563/// integration tests to make sure the helper symbols are still
564/// resolvable after `cargo build -p luna-runtime-helpers --release`.
565///
566/// Available only when the `jit-helpers` Cargo feature is enabled
567/// (default). When disabled, the staticlib excludes both
568/// `luna-jit` from its dep graph and this function from its API
569/// surface — interp-only AOT binaries pay zero cranelift cost.
570///
571/// Returns the number of helper symbols pinned (always 27 with the
572/// current `luna-jit` shape; will need to be bumped in lock-step
573/// any time `crates/luna-jit/src/jit_backend/mod.rs` adds a 28th
574/// `pub unsafe extern "C" fn luna_jit_*`).
575///
576/// # Implementation note
577///
578/// Re-exports alone (`pub use luna_jit::jit_backend::*`) are not
579/// enough: rustc's staticlib pipeline drops `#[no_mangle]` symbols
580/// from upstream rlibs unless they're reached via a kept root. The
581/// `LUNA_AOT_HELPER_PIN` static + this fn together form that kept
582/// root.
583#[cfg(feature = "jit-helpers")]
584pub fn force_link_jit_helpers() -> usize {
585    jit_helpers_pin::force_link_jit_helpers()
586}
587
588/// Force-link the C-ABI symbol so a `cargo build` of a dependent
589/// rlib doesn't dead-strip it. Without this, the symbol is technically
590/// reachable (no_mangle + extern "C"), but rustc / lld can be over-
591/// eager in some pipelines; calling this from `lib.rs::pre_main` or
592/// from a `build.rs` artifact ensures the staticlib export survives.
593///
594/// This is a `pub fn` so dependent test binaries that build against
595/// the `rlib` crate-type pull the symbol via the live reference here.
596/// The staticlib `crate-type` path doesn't need it (staticlib emit
597/// preserves no_mangle externs by construction), but the dual-crate-
598/// type setup gives us both for free.
599pub const fn force_link_aot_entry() -> unsafe extern "C" fn(*const u8, usize) -> i32 {
600    luna_aot_run
601}
602
603/// v1.3 Phase AOT Stage 7 sub-piece 3 — deploy-side interned-string
604/// slot resolver.
605///
606/// AOT trace mcode emitted by [`luna_jit::jit_backend::trace::
607/// lower_trace_into`] with `CompileOptions { aot: true }` reads
608/// interned-string-key pointers indirectly through writable 8-byte
609/// slots (`__luna_aot_strkey_slot_<hex>`). Each unique key
610/// contributes a 16-byte `[bytes_addr, slot_addr]` entry to a
611/// dedicated `luna_strkey_idx` section. The deploy binary's static
612/// linker auto-brackets that section via
613/// `__start_luna_strkey_idx` / `__stop_luna_strkey_idx` (ELF) or
614/// `section$start$__DATA$luna_strkey_idx` /
615/// `section$end$__DATA$luna_strkey_idx` (Mach-O), and this resolver
616/// walks the bracketed range to: (a) intern each bytes block into
617/// the deploy `Vm`'s heap, and (b) write the resulting
618/// `Gc<LuaStr>::as_ptr()` into the matching slot.
619///
620/// # Safety contract (called from `run_inner` only)
621///
622/// - Must run **once**, BEFORE the deploy `Vm` dispatches into any
623///   AOT mcode. `run_inner` calls it after `Vm::new` /
624///   `set_bytecode_loading` and before `vm.load`.
625/// - Idempotent under second-call: the slots already hold valid
626///   `Gc<LuaStr>` pointers, the section walk re-interns the bytes
627///   (cheap — string-table dedup), and re-writes the slot with the
628///   same pointer.
629/// - Empty-section tolerant: a deploy binary that linked zero AOT
630///   trace `.o`s has both bracket symbols collapsing to the same
631///   address; the walk loop terminates with zero entries.
632///
633/// # Why feature-gated on `jit-helpers`
634///
635/// The whole AOT-trace path is jit-helper-gated. With
636/// `default-features = false` the staticlib excludes
637/// `luna-jit` and the resolver becomes a no-op — interp-only AOT
638/// binaries pay zero scan cost and the bracket-symbol references are
639/// elided.
640#[cfg(feature = "jit-helpers")]
641pub mod aot_strkey_resolver {
642    use luna_core::vm::Vm;
643
644    /// Index entry layout — must match the cranelift-emit shape in
645    /// `crates/luna-jit/src/jit_backend/trace.rs::emit_str_key_arg`:
646    /// two pointer-sized fields, `bytes_ptr` and `slot_ptr`, both
647    /// resolved by the static linker before process load completes.
648    #[repr(C)]
649    struct IndexEntry {
650        /// Address of the `__luna_aot_strkey_bytes_<hex>` symbol:
651        /// `[u64 len | utf8...]` payload, read-only.
652        bytes_ptr: *const u8,
653        /// Address of the `__luna_aot_strkey_slot_<hex>` symbol:
654        /// writable 8-byte slot, zero-initialised at link time, this
655        /// resolver writes the interned `Gc<LuaStr>` pointer in.
656        slot_ptr: *mut *const u8,
657    }
658
659    // ELF / lld auto-creates `__start_<name>` / `__stop_<name>` for
660    // sections whose name is a valid C identifier. Our section is
661    // `luna_strkey_idx` (set via cranelift's `set_segment_section`).
662    //
663    // Mach-O uses a different convention: `section$start$<seg>$<sect>`
664    // / `section$end$<seg>$<sect>`, synthesized by Apple `ld`. We
665    // declare per-platform externs and the dead-strip pass discards
666    // whichever doesn't match.
667    //
668    // Windows / COFF has no bracket-symbol convention (Stage 7 polish
669    // 3): `link.exe` / `lld-link` don't synthesize `__start_` / `__stop_`
670    // externs. Instead the deploy walker calls into the parent crate's
671    // [`crate::windows_section::find_section`] which does a runtime
672    // PE-header parse via `GetModuleHandleW(NULL)`. The Windows path
673    // uses the short section name `.lt_skix` (8 bytes, COFF
674    // section-name max) — see `crates/luna-aot/src/embed.rs` for the
675    // emit-side choice. Empty-section (no AOT traces linked in) is
676    // handled by `find_section` returning `None` and `resolve_all`
677    // short-circuiting to 0.
678    #[cfg(all(unix, not(target_vendor = "apple")))]
679    unsafe extern "C" {
680        #[link_name = "__start_luna_strkey_idx"]
681        static mut LUNA_STRKEY_IDX_START: u8;
682        #[link_name = "__stop_luna_strkey_idx"]
683        static mut LUNA_STRKEY_IDX_END: u8;
684    }
685
686    #[cfg(target_vendor = "apple")]
687    unsafe extern "C" {
688        #[link_name = "\u{1}section$start$__DATA$luna_strkey_idx"]
689        static mut LUNA_STRKEY_IDX_START: u8;
690        #[link_name = "\u{1}section$end$__DATA$luna_strkey_idx"]
691        static mut LUNA_STRKEY_IDX_END: u8;
692    }
693
694    /// Walk the bracketed `luna_strkey_idx` section (Unix / Mach-O) or
695    /// the PE-header-located `.lt_skix` section (Windows), intern each
696    /// bytes block into `vm.heap`, write the resulting pointer into
697    /// the matching slot. Returns the number of slots populated
698    /// (zero on a binary that linked zero AOT trace `.o`s).
699    pub fn resolve_all(vm: &mut Vm) -> usize {
700        // Locate the strkey-idx section + length, dispatching on
701        // target platform. Windows: runtime PE header walk via
702        // [`crate::windows_section::find_section`] for the short name
703        // `.lt_skix` (mirrors the emit-side choice in
704        // `crates/luna-aot/src/embed.rs::write_aot_cmain_object_for`
705        // Windows arm + the harvester's `set_segment_section`).
706        // Unix/Mach-O: bracket symbols supplied by the linker. Either
707        // dispatch path can produce a zero-length section (binary
708        // linked no AOT traces) — `walk_index_bytes` short-circuits.
709        let (base, len_bytes): (*const u8, usize) = {
710            #[cfg(target_os = "windows")]
711            {
712                match crate::windows_section::find_section(b".lt_skix") {
713                    Some((b, l)) => (b, l),
714                    None => return 0,
715                }
716            }
717            #[cfg(all(not(target_os = "windows"), unix, not(target_vendor = "apple")))]
718            {
719                let start = &raw mut LUNA_STRKEY_IDX_START as *mut IndexEntry;
720                let end = &raw mut LUNA_STRKEY_IDX_END as *mut IndexEntry;
721                // start == end on a binary with zero AOT traces. Section
722                // length = end - start in bytes; divide by entry size
723                // gives the count.
724                let len = (end as isize) - (start as isize);
725                if len <= 0 {
726                    return 0;
727                }
728                (start as *const u8, len as usize)
729            }
730            #[cfg(all(not(target_os = "windows"), target_vendor = "apple"))]
731            {
732                let start = &raw mut LUNA_STRKEY_IDX_START as *mut IndexEntry;
733                let end = &raw mut LUNA_STRKEY_IDX_END as *mut IndexEntry;
734                let len = (end as isize) - (start as isize);
735                if len <= 0 {
736                    return 0;
737                }
738                (start as *const u8, len as usize)
739            }
740            // Platforms without a section enumeration path (e.g.
741            // wasm32) — no AOT install possible, return 0.
742            #[cfg(not(any(
743                target_os = "windows",
744                all(unix, not(target_vendor = "apple")),
745                target_vendor = "apple"
746            )))]
747            {
748                let _ = vm;
749                return 0;
750            }
751        };
752        walk_index_bytes(vm, base, len_bytes)
753    }
754
755    /// Common per-entry walk shared by the Unix/Mach-O bracket-symbol
756    /// path and the Windows PE-header-located section path. Takes the
757    /// section base + length in bytes (as the two enumeration paths
758    /// produce different types — a pair of bracket symbol addresses on
759    /// Unix, a `(*const u8, usize)` tuple from [`crate::windows_section
760    /// ::find_section`] on Windows) and walks `len / sizeof(IndexEntry)`
761    /// entries.
762    ///
763    /// Tolerant of trailing-zero placeholder entries (the cmain shim
764    /// emits one zero-filled IndexEntry to guarantee the section exists
765    /// even when no real traces are linked in) via the
766    /// `entry.bytes_ptr.is_null() || entry.slot_ptr.is_null()` skip.
767    fn walk_index_bytes(vm: &mut Vm, base: *const u8, len_bytes: usize) -> usize {
768        if base.is_null() || len_bytes == 0 {
769            return 0;
770        }
771        let n_entries = len_bytes / core::mem::size_of::<IndexEntry>();
772        let start = base as *const IndexEntry;
773        let mut populated = 0usize;
774        // SAFETY: caller guarantees `[base, base + len_bytes)` is
775        // mapped readable memory owned by a linker-defined section.
776        // Each IndexEntry read is bounded by n_entries. Slot writes
777        // target the `slot_ptr` field which the lowerer guarantees
778        // points at a writable 8-byte slot in the same image.
779        unsafe {
780            for i in 0..n_entries {
781                let entry = &*start.add(i);
782                if entry.bytes_ptr.is_null() || entry.slot_ptr.is_null() {
783                    continue;
784                }
785                let len = core::ptr::read_unaligned(entry.bytes_ptr as *const u64) as usize;
786                let payload = entry.bytes_ptr.add(8);
787                let bytes = core::slice::from_raw_parts(payload, len);
788                let interned = vm.heap.intern(bytes);
789                core::ptr::write(entry.slot_ptr, interned.as_ptr() as *const u8);
790                populated += 1;
791            }
792        }
793        populated
794    }
795}
796
797// v1.3 Phase AOT Stage 7 polish 6 — deploy-side inline-chain resolver.
798//
799// Mirrors `aot_strkey_resolver`'s shape. The trace lowerer's
800// `emit_chain_ptr_arg` (`crates/luna-jit/src/jit_backend/trace.rs`)
801// emits one `(slot, bytes, idx)` triple per unique
802// `FrameMaterializeInfo` chain when `opts.aot == true`; this resolver
803// walks the bracketed `luna_inline_chnx` section (Unix / Mach-O) or the
804// PE-header-located `.lt_chai` section (Windows), parses each bytes
805// payload into a `Vec<FrameMaterializeInfo>`, leaks it as a
806// process-lifetime `Rc<[...]>` (so the IR's load yields a valid pointer
807// for the binary's lifetime — there is no per-trace tear-down on the
808// AOT path), and writes the chain's first-element pointer into the
809// matching slot.
810//
811// Why a separate Rc instead of pointing the slot at the bytes section
812// directly:
813//   - The IR loads the slot, then passes the value as `*const
814//     FrameMaterializeInfo` to `luna_jit_trace_materialize_frames`,
815//     which interprets the bytes as a fully-aligned array of `repr(C)`
816//     structs. The bytes section is already 8-byte aligned with the
817//     same packing, so a `bytes_ptr + 8` (skip the count prefix) would
818//     work, but a future change to `FrameMaterializeInfo` layout would
819//     silently misinterpret stale bytes. Going through an explicit
820//     `Vec → Rc<[...]>` conversion lets us validate `chain_bytes.len()
821//     % 12 == 0` (via the same `PerExitInlineEntry::FRAME_MATERIALIZE
822//     _INFO_SIZE` constant the v3 wire format uses) and surface
823//     corruption with a probe message rather than dispatching into
824//     garbage.
825//   - Keeping the chain's ownership on the Rust side mirrors the JIT
826//     path's `per_exit_inline_vec.push((..., chain_rc, ...))` — the
827//     dispatcher's `CompiledTrace::per_exit_inline[i].chain` field can
828//     hold its own Rc rebuilt from the same bytes (decoded by the
829//     trace install path); the IR pointer and the dispatcher field
830//     point at independently-allocated copies of the same data, but
831//     neither side compares pointers, only reads through them.
832#[cfg(feature = "jit-helpers")]
833pub mod aot_inline_chain_resolver {
834    //! v1.3 Phase AOT Stage 7 polish 6 — `FrameMaterializeInfo` chain
835    //! pointer reloc resolver. See parent-module preamble for the full
836    //! design rationale; this module owns the deploy-side walk +
837    //! per-chain Rc materialization + slot write.
838    use luna_core::jit::trace_types::FrameMaterializeInfo;
839
840    /// Wire-size of one `FrameMaterializeInfo` record on disk and in
841    /// the bytes section payload. Asserted at compile time in
842    /// `luna_core::jit::aot_meta::FRAME_MATERIALIZE_INFO_WIRE_SIZE_CHECK`.
843    const FRAME_MATERIALIZE_INFO_SIZE: usize = 12;
844
845    /// Index entry layout — must match the cranelift-emit shape in
846    /// `crates/luna-jit/src/jit_backend/trace.rs::emit_chain_ptr_arg`:
847    /// two pointer-sized fields, `bytes_ptr` and `slot_ptr`, both
848    /// resolved by the static linker before process load completes.
849    #[repr(C)]
850    struct IndexEntry {
851        /// Address of the `__luna_aot_inline_chain_bytes_<hex>` symbol:
852        /// `[u64 count | packed_records...]` payload, read-only. The
853        /// records are tightly packed 12-byte
854        /// `(base_offset, pc, nresults)` triples.
855        bytes_ptr: *const u8,
856        /// Address of the `__luna_aot_inline_chain_slot_<hex>` symbol:
857        /// writable 8-byte slot, zero-initialised at link time. This
858        /// resolver writes the leaked chain's first-element pointer
859        /// here.
860        slot_ptr: *mut *const FrameMaterializeInfo,
861    }
862
863    // ELF / lld auto-creates `__start_<name>` / `__stop_<name>` for
864    // sections whose name is a valid C identifier (`luna_inline_chnx`).
865    // Mach-O uses `section$start$<seg>$<sect>` /
866    // `section$end$<seg>$<sect>`, synthesised by Apple `ld`. Windows
867    // COFF has neither — see the runtime PE-header walker in the
868    // `resolve_all` arm.
869    #[cfg(all(unix, not(target_vendor = "apple")))]
870    unsafe extern "C" {
871        #[link_name = "__start_luna_inline_chnx"]
872        static mut LUNA_INLINE_CHNX_START: u8;
873        #[link_name = "__stop_luna_inline_chnx"]
874        static mut LUNA_INLINE_CHNX_END: u8;
875    }
876
877    #[cfg(target_vendor = "apple")]
878    unsafe extern "C" {
879        #[link_name = "\u{1}section$start$__DATA$luna_inline_chnx"]
880        static mut LUNA_INLINE_CHNX_START: u8;
881        #[link_name = "\u{1}section$end$__DATA$luna_inline_chnx"]
882        static mut LUNA_INLINE_CHNX_END: u8;
883    }
884
885    /// Walk the bracketed `luna_inline_chnx` section (Unix / Mach-O) or
886    /// the PE-header-located `.lt_chai` section (Windows). For each
887    /// entry: rebuild a `Vec<FrameMaterializeInfo>` from the bytes
888    /// payload, materialise as a `Rc<[...]>`, leak ownership (process-
889    /// lifetime — AOT traces never tear down), write the chain's
890    /// first-element pointer into the matching slot.
891    ///
892    /// Returns the number of slots populated (zero on a binary that
893    /// linked zero AOT traces with inline cmp@d>0 side-exits).
894    ///
895    /// Tolerant of trailing-zero placeholder entries (the cmain shim
896    /// emits one zero-filled IndexEntry to guarantee the section exists
897    /// even when no real chain symbols are linked) via the null-pointer
898    /// guard in `walk_index_bytes`.
899    pub fn resolve_all() -> usize {
900        let (base, len_bytes): (*const u8, usize) = {
901            #[cfg(target_os = "windows")]
902            {
903                match crate::windows_section::find_section(b".lt_chai") {
904                    Some((b, l)) => (b, l),
905                    None => return 0,
906                }
907            }
908            #[cfg(all(not(target_os = "windows"), unix, not(target_vendor = "apple")))]
909            {
910                let start = &raw mut LUNA_INLINE_CHNX_START as *mut IndexEntry;
911                let end = &raw mut LUNA_INLINE_CHNX_END as *mut IndexEntry;
912                let len = (end as isize) - (start as isize);
913                if len <= 0 {
914                    return 0;
915                }
916                (start as *const u8, len as usize)
917            }
918            #[cfg(all(not(target_os = "windows"), target_vendor = "apple"))]
919            {
920                let start = &raw mut LUNA_INLINE_CHNX_START as *mut IndexEntry;
921                let end = &raw mut LUNA_INLINE_CHNX_END as *mut IndexEntry;
922                let len = (end as isize) - (start as isize);
923                if len <= 0 {
924                    return 0;
925                }
926                (start as *const u8, len as usize)
927            }
928            #[cfg(not(any(
929                target_os = "windows",
930                all(unix, not(target_vendor = "apple")),
931                target_vendor = "apple"
932            )))]
933            {
934                return 0;
935            }
936        };
937        walk_index_bytes(base, len_bytes)
938    }
939
940    /// Common per-entry walk shared by the Unix/Mach-O bracket-symbol
941    /// path and the Windows PE-header-located section path.
942    ///
943    /// Each entry's `bytes_ptr` points at `[u64 count, records...]`;
944    /// we decode the count, validate `count * 12` doesn't overflow,
945    /// parse `count` `FrameMaterializeInfo` triples, materialise them
946    /// as an `Rc<[FrameMaterializeInfo]>`, leak ownership via
947    /// `core::mem::forget(rc.clone())` (the inner buffer stays alive
948    /// for process lifetime), and write the first-element pointer into
949    /// the slot. Subsequent AOT mcode dispatches read the slot and pass
950    /// the pointer to `luna_jit_trace_materialize_frames(n, ptr)`.
951    ///
952    /// Corrupt entries (null pointers, unaligned count, count overflow)
953    /// are skipped silently with an `LUNA_AOT_PROBE` line on stderr —
954    /// the trace's first inline side-exit dispatch will then deopt via
955    /// the helper's `pending_err` path because the slot stays NULL.
956    fn walk_index_bytes(base: *const u8, len_bytes: usize) -> usize {
957        if base.is_null() || len_bytes == 0 {
958            return 0;
959        }
960        let probe_on = std::env::var_os("LUNA_AOT_PROBE").is_some();
961        let n_entries = len_bytes / core::mem::size_of::<IndexEntry>();
962        let start = base as *const IndexEntry;
963        let mut populated = 0usize;
964        // SAFETY: caller guarantees `[base, base + len_bytes)` is
965        // mapped readable memory owned by a linker-defined section.
966        // Each IndexEntry read is bounded by n_entries. Bytes-payload
967        // reads are bounded by the per-entry count (validated for
968        // overflow before the slice constructor). Slot writes target
969        // the `slot_ptr` field which the lowerer guarantees points at
970        // a writable 8-byte slot in the same image.
971        unsafe {
972            for i in 0..n_entries {
973                let entry = &*start.add(i);
974                if entry.bytes_ptr.is_null() || entry.slot_ptr.is_null() {
975                    continue;
976                }
977                // Bytes layout: little-endian u64 record count, then
978                // `count * FRAME_MATERIALIZE_INFO_SIZE` packed bytes.
979                let count = core::ptr::read_unaligned(entry.bytes_ptr as *const u64) as usize;
980                let Some(bytes_len) = count.checked_mul(FRAME_MATERIALIZE_INFO_SIZE) else {
981                    if probe_on {
982                        eprintln!(
983                            "luna-runtime-helpers: aot_inline_chain skip entry {i} reason=count_overflow count={count}"
984                        );
985                    }
986                    continue;
987                };
988                let payload = entry.bytes_ptr.add(8);
989                let raw = core::slice::from_raw_parts(payload, bytes_len);
990                let mut vec: Vec<FrameMaterializeInfo> = Vec::with_capacity(count);
991                for j in 0..count {
992                    let off = j * FRAME_MATERIALIZE_INFO_SIZE;
993                    let base_offset = u32::from_le_bytes(raw[off..off + 4].try_into().unwrap());
994                    let pc = u32::from_le_bytes(raw[off + 4..off + 8].try_into().unwrap());
995                    let nresults = i32::from_le_bytes(raw[off + 8..off + 12].try_into().unwrap());
996                    vec.push(FrameMaterializeInfo {
997                        base_offset,
998                        pc,
999                        nresults,
1000                    });
1001                }
1002                let rc: luna_core::jit::send_compat::TArc<[FrameMaterializeInfo]> = vec.into();
1003                // `Rc<[T]>::as_ptr` returns a fat `*const [T]`; the
1004                // first-element address is what the IR's
1005                // `luna_jit_trace_materialize_frames` consumes. For a
1006                // non-empty chain, `rc[0]` is the data pointer; for an
1007                // empty chain (count == 0) the IR never reaches the
1008                // helper (the side-exit's `if !call_chain.is_empty()`
1009                // gate at compile time would have routed through the
1010                // d=0 arm), so the slot stays at a dangling-but-unused
1011                // value. Guard anyway for paranoia.
1012                let chain_ptr: *const FrameMaterializeInfo = if count == 0 {
1013                    core::ptr::null()
1014                } else {
1015                    &rc[0] as *const FrameMaterializeInfo
1016                };
1017                // Leak ownership so the chain bytes stay alive for the
1018                // process. AOT-installed traces never tear down (no
1019                // `proto.traces.borrow_mut().remove(...)` path on
1020                // deploy), so a single leak per unique chain matches
1021                // the lifetime requirement exactly.
1022                core::mem::forget(rc);
1023                core::ptr::write(entry.slot_ptr, chain_ptr);
1024                populated += 1;
1025            }
1026        }
1027        populated
1028    }
1029}
1030
1031// v1.3 Phase AOT Stage 7 sub-piece 4 — trace dispatch registry.
1032// See module-level docs inside the block for the deploy-side walker
1033// shape; the AOT-compile-side recorder + emitter lives in
1034// `crates/luna-aot/src/embed.rs::harvest_and_emit_aot_traces`.
1035#[cfg(feature = "jit-helpers")]
1036pub mod aot_trace_registry {
1037    //! v1.3 Phase AOT Stage 7 sub-piece 4 — deploy-side trace-meta
1038    //! walker.
1039    //!
1040    //! `luna-aot::embed::harvest_and_emit_aot_traces` emits a 48-byte
1041    //! [`luna_core::jit::aot_meta::AotTraceIndexEntry`] per AOT-installable
1042    //! trace into the `luna_trace_meta` bracketed section, plus a
1043    //! combined meta-blob payload in `luna_trace_blob`. This walker
1044    //! runs once at startup (between `vm.set_bytecode_loading(true)`
1045    //! and `vm.load`), iterates the bracket-bounded section, matches
1046    //! each entry's `proto_hash` against the loaded chunk's proto
1047    //! tree via [`Vm::collect_proto_hashes`], and calls
1048    //! [`Vm::install_aot_trace`] with a freshly constructed
1049    //! [`CompiledTrace`] whose `entry` points at the linker-resolved
1050    //! AOT mcode.
1051    //!
1052    //! Empty-section tolerant: a binary with zero linked trace `.o`s
1053    //! has both bracket symbols collapse to the same address; the walk
1054    //! short-circuits with `Ok(0)`.
1055
1056    use luna_core::jit::aot_meta::{
1057        AotTraceIndexEntry, decode_meta_blob, unpack_exit_tag, unpack_tag_res_kind,
1058    };
1059    use luna_core::jit::trace_types::{CompiledTrace, ExitTag, TraceFn};
1060    use luna_core::vm::Vm;
1061
1062    // Bracket symbols — same pattern as sub-piece 3's strkey_idx
1063    // walker. ELF / lld auto-create `__start_<name>` / `__stop_<name>`
1064    // for sections whose name is a valid C identifier; Mach-O uses
1065    // `section$start$<seg>$<sect>` / `section$end$<seg>$<sect>`.
1066    //
1067    // Windows COFF has no bracket-symbol convention (Stage 7 polish 3):
1068    // the Windows path uses a runtime PE-header walk via
1069    // [`crate::windows_section::find_section`] for the short-name
1070    // section `.lt_meta` instead. See `windows_section` module docs.
1071    #[cfg(all(unix, not(target_vendor = "apple")))]
1072    unsafe extern "C" {
1073        #[link_name = "__start_luna_trace_meta"]
1074        static mut LUNA_TRACE_META_START: u8;
1075        #[link_name = "__stop_luna_trace_meta"]
1076        static mut LUNA_TRACE_META_END: u8;
1077    }
1078
1079    #[cfg(target_vendor = "apple")]
1080    unsafe extern "C" {
1081        #[link_name = "\u{1}section$start$__DATA$luna_trace_meta"]
1082        static mut LUNA_TRACE_META_START: u8;
1083        #[link_name = "\u{1}section$end$__DATA$luna_trace_meta"]
1084        static mut LUNA_TRACE_META_END: u8;
1085    }
1086
1087    /// Walk the `luna_trace_meta` section, install one `CompiledTrace`
1088    /// per entry whose `proto_hash` matches a Proto reachable from
1089    /// `root`. Returns the count installed.
1090    ///
1091    /// Entries whose meta blob fails to decode (magic / version
1092    /// mismatch, truncation) are skipped silently — the trace falls
1093    /// back to JIT at runtime. `LUNA_AOT_PROBE=1` surfaces the count
1094    /// + per-entry skip reasons on stderr for diagnosis.
1095    ///
1096    /// The deploy `Vm` never side-traces an AOT-installed parent
1097    /// (recorder is invoked from the dispatch path; AOT install
1098    /// happens BEFORE the first dispatch), so the bare
1099    /// [`CompiledTrace::from_aot_meta`] constructor with empty
1100    /// `per_exit_inline` / `per_exit_tags` is sufficient.
1101    pub fn install_all(
1102        vm: &mut Vm,
1103        root: luna_core::runtime::Gc<luna_core::runtime::function::Proto>,
1104    ) -> usize {
1105        // Locate the trace-meta section + length, dispatching on
1106        // target platform. Unix/Mach-O: linker-synthesised bracket
1107        // symbols. Windows: runtime PE-header walk via
1108        // [`crate::windows_section::find_section`] (Stage 7 polish 3)
1109        // for the short name `.lt_meta`. Either path can produce a
1110        // zero-length section (binary linked no AOT traces) —
1111        // `walk_meta_section` short-circuits.
1112        let (base, len_bytes): (*const u8, usize) = {
1113            #[cfg(target_os = "windows")]
1114            {
1115                match crate::windows_section::find_section(b".lt_meta") {
1116                    Some((b, l)) => (b, l),
1117                    None => return 0,
1118                }
1119            }
1120            #[cfg(all(not(target_os = "windows"), unix, not(target_vendor = "apple")))]
1121            {
1122                let start = &raw mut LUNA_TRACE_META_START as *mut AotTraceIndexEntry;
1123                let end = &raw mut LUNA_TRACE_META_END as *mut AotTraceIndexEntry;
1124                let len = (end as isize) - (start as isize);
1125                if len <= 0 {
1126                    return 0;
1127                }
1128                (start as *const u8, len as usize)
1129            }
1130            #[cfg(all(not(target_os = "windows"), target_vendor = "apple"))]
1131            {
1132                let start = &raw mut LUNA_TRACE_META_START as *mut AotTraceIndexEntry;
1133                let end = &raw mut LUNA_TRACE_META_END as *mut AotTraceIndexEntry;
1134                let len = (end as isize) - (start as isize);
1135                if len <= 0 {
1136                    return 0;
1137                }
1138                (start as *const u8, len as usize)
1139            }
1140            #[cfg(not(any(
1141                target_os = "windows",
1142                all(unix, not(target_vendor = "apple")),
1143                target_vendor = "apple"
1144            )))]
1145            {
1146                let _ = (vm, root);
1147                return 0;
1148            }
1149        };
1150        // SAFETY: `(base, len_bytes)` was produced either by a linker-
1151        // synthesised bracket symbol pair bounding a contiguous run of
1152        // `AotTraceIndexEntry`, or by [`windows_section::find_section`]
1153        // which returns the section's run-time base + virtual_size for
1154        // a PE section we ourselves emit via cranelift_object. Both
1155        // shapes satisfy `walk_meta_section`'s unsafe contract.
1156        unsafe { walk_meta_section(vm, root, base as *const AotTraceIndexEntry, len_bytes) }
1157    }
1158
1159    /// Common walk shared by the Unix/Mach-O bracket-symbol path and
1160    /// the Windows PE-header-located section path. Iterates one
1161    /// [`AotTraceIndexEntry`] at a time, decoding the meta blob and
1162    /// installing on the matched proto.
1163    ///
1164    /// # Safety
1165    ///
1166    /// `start` must point at the first byte of a `len_bytes`-long
1167    /// run of `AotTraceIndexEntry` instances, all in readable memory
1168    /// (linker-defined section or PE-mapped section data). Per-entry
1169    /// `meta_ptr` / `fn_ptr` are validated by the per-entry null
1170    /// checks; meta blob bytes are bounded by the entry's `meta_len`.
1171    unsafe fn walk_meta_section(
1172        vm: &mut Vm,
1173        root: luna_core::runtime::Gc<luna_core::runtime::function::Proto>,
1174        start: *const AotTraceIndexEntry,
1175        len_bytes: usize,
1176    ) -> usize {
1177        if start.is_null() || len_bytes < core::mem::size_of::<AotTraceIndexEntry>() {
1178            return 0;
1179        }
1180        let n_entries = len_bytes / core::mem::size_of::<AotTraceIndexEntry>();
1181        let proto_hashes = vm.collect_proto_hashes(root);
1182        let probe_on = std::env::var_os("LUNA_AOT_PROBE").is_some();
1183        let mut installed = 0usize;
1184        // SAFETY: caller invariant — [start, start+n_entries) is
1185        // mapped readable memory containing valid AotTraceIndexEntry
1186        // instances or zero-fill placeholder bytes (skipped via the
1187        // fn_ptr == 0 guard).
1188        unsafe {
1189            for i in 0..n_entries {
1190                let entry = &*start.add(i);
1191                // The placeholder entry in `luna_trace_meta` is a single
1192                // zero byte from the cmain shim — the section walk steps
1193                // past it via the size-rounding above. An entry whose
1194                // `fn_ptr` is null came from that placeholder and must
1195                // be skipped (not from a real AOT-emitted trace).
1196                if entry.fn_ptr == 0 || entry.meta_ptr == 0 {
1197                    continue;
1198                }
1199                let meta_bytes = core::slice::from_raw_parts(
1200                    entry.meta_ptr as *const u8,
1201                    entry.meta_len as usize,
1202                );
1203                let decoded = match decode_meta_blob(meta_bytes) {
1204                    Ok(d) => d,
1205                    Err(reason) => {
1206                        if probe_on {
1207                            eprintln!(
1208                                "luna-runtime-helpers: aot_trace skip head_pc={} reason={reason}",
1209                                entry.head_pc
1210                            );
1211                        }
1212                        continue;
1213                    }
1214                };
1215                // Find the matching Proto by hash.
1216                let matched = proto_hashes
1217                    .iter()
1218                    .find(|(_p, h)| *h == entry.proto_hash)
1219                    .map(|(p, _h)| *p);
1220                let Some(proto) = matched else {
1221                    if probe_on {
1222                        eprintln!(
1223                            "luna-runtime-helpers: aot_trace skip head_pc={} reason=proto_hash_unmatched",
1224                            entry.head_pc
1225                        );
1226                    }
1227                    continue;
1228                };
1229                // Reconstruct exit_tags + entry_tags + global_tag_res_kind.
1230                let mut exit_tags_vec: Vec<ExitTag> = Vec::with_capacity(decoded.exit_tags.len());
1231                let mut tag_decode_ok = true;
1232                for raw in decoded.exit_tags.iter().copied() {
1233                    if let Some(t) = unpack_exit_tag(raw) {
1234                        exit_tags_vec.push(t);
1235                    } else {
1236                        tag_decode_ok = false;
1237                        break;
1238                    }
1239                }
1240                let Some(tag_res_kind) = unpack_tag_res_kind(decoded.header.tag_res_kind) else {
1241                    if probe_on {
1242                        eprintln!(
1243                            "luna-runtime-helpers: aot_trace skip head_pc={} reason=tag_res_kind_invalid",
1244                            entry.head_pc
1245                        );
1246                    }
1247                    continue;
1248                };
1249                if !tag_decode_ok {
1250                    if probe_on {
1251                        eprintln!(
1252                            "luna-runtime-helpers: aot_trace skip head_pc={} reason=exit_tag_invalid",
1253                            entry.head_pc
1254                        );
1255                    }
1256                    continue;
1257                }
1258                let entry_tags_rc: luna_core::jit::send_compat::TArc<[u8]> =
1259                    decoded.entry_tags.into();
1260                let exit_tags_rc: luna_core::jit::send_compat::TArc<[ExitTag]> =
1261                    exit_tags_vec.into();
1262                // v2 per_exit_tags decode: reconstruct
1263                // `Vec<(cont_pc, Rc<[ExitTag]>)>` matching the
1264                // dispatcher's `decode_exit_shape` shape lookup. Each
1265                // entry's packed-byte `ExitTag` array unpacks via
1266                // [`unpack_exit_tag`]; an invalid byte = skip the
1267                // whole trace (matches the existing exit-tag handling).
1268                let mut per_exit_tags_decoded: Vec<(
1269                    u32,
1270                    luna_core::jit::send_compat::TArc<[ExitTag]>,
1271                )> = Vec::with_capacity(decoded.per_exit_tags.len());
1272                let mut per_exit_tags_ok = true;
1273                for ent in &decoded.per_exit_tags {
1274                    let mut tags: Vec<ExitTag> = Vec::with_capacity(ent.tags_packed.len());
1275                    for raw in ent.tags_packed.iter().copied() {
1276                        if let Some(t) = unpack_exit_tag(raw) {
1277                            tags.push(t);
1278                        } else {
1279                            per_exit_tags_ok = false;
1280                            break;
1281                        }
1282                    }
1283                    if !per_exit_tags_ok {
1284                        break;
1285                    }
1286                    per_exit_tags_decoded.push((ent.cont_pc, tags.into()));
1287                }
1288                if !per_exit_tags_ok {
1289                    if probe_on {
1290                        eprintln!(
1291                            "luna-runtime-helpers: aot_trace skip head_pc={} reason=per_exit_tag_invalid",
1292                            entry.head_pc
1293                        );
1294                    }
1295                    continue;
1296                }
1297                // v1.3 Phase AOT Stage 7 polish 6 — v3 per_exit_inline
1298                // decode is NOW load-bearing. Each wire entry's
1299                // `chain_bytes` rebuilds into a fresh
1300                // `Vec<FrameMaterializeInfo>` → `Rc<[...]>` for the
1301                // dispatcher's `per_exit_inline[i].chain` field; the
1302                // `tags_packed` array unpacks through `unpack_exit_tag`
1303                // into `Rc<[ExitTag]>` (same shape as the v2
1304                // per_exit_tags pattern above). A failing chain rebuild
1305                // (length not a multiple of 12 — corruption) or an
1306                // invalid packed tag (out-of-range byte) means the
1307                // trace skips install; the trace then falls back to
1308                // JIT at runtime via the recorder.
1309                //
1310                // The IR-baked chain pointer lives in a separate slot
1311                // populated by `aot_inline_chain_resolver::resolve_all`
1312                // (called from `run_inner` BEFORE this install path).
1313                // The two chain owners (this Rc and the leaked Rc
1314                // behind the slot) are independent allocations of the
1315                // same byte content — neither side compares pointers.
1316                let mut per_exit_inline_decoded: Vec<luna_core::jit::trace_types::InlineSideExit> =
1317                    Vec::with_capacity(decoded.per_exit_inline.len());
1318                let mut inline_ok = true;
1319                for ent in &decoded.per_exit_inline {
1320                    let Some(chain_vec) = ent.rebuild_chain() else {
1321                        inline_ok = false;
1322                        if probe_on {
1323                            eprintln!(
1324                                "luna-runtime-helpers: aot_trace skip head_pc={} reason=per_exit_inline_chain_invalid (cont_pc={})",
1325                                entry.head_pc, ent.cont_pc
1326                            );
1327                        }
1328                        break;
1329                    };
1330                    let mut tags: Vec<ExitTag> = Vec::with_capacity(ent.tags_packed.len());
1331                    for raw in ent.tags_packed.iter().copied() {
1332                        if let Some(t) = unpack_exit_tag(raw) {
1333                            tags.push(t);
1334                        } else {
1335                            inline_ok = false;
1336                            break;
1337                        }
1338                    }
1339                    if !inline_ok {
1340                        if probe_on {
1341                            eprintln!(
1342                                "luna-runtime-helpers: aot_trace skip head_pc={} reason=per_exit_inline_tag_invalid (cont_pc={})",
1343                                entry.head_pc, ent.cont_pc
1344                            );
1345                        }
1346                        break;
1347                    }
1348                    per_exit_inline_decoded.push(luna_core::jit::trace_types::InlineSideExit {
1349                        cont_pc: ent.cont_pc,
1350                        head_resume_pc: ent.head_resume_pc,
1351                        exit_tags: tags.into(),
1352                        chain: chain_vec.into(),
1353                        side_trace_ptr: Box::new(luna_core::jit::send_compat::TCellPtr::null()),
1354                    });
1355                }
1356                if !inline_ok {
1357                    continue;
1358                }
1359                // Transmute the C-ABI fn ptr from u64 (wire-width-
1360                // stable) to `TraceFn`. Safe because the trace .o was
1361                // emitted by `lower_trace_into_named` with sig
1362                // `(I64) -> I64`, matching `TraceFn`. AOT-binary
1363                // deploy is always 64-bit so the u64 narrows to a
1364                // valid pointer on this target.
1365                let fn_ptr_raw = entry.fn_ptr as *const u8;
1366                let trace_entry: TraceFn = core::mem::transmute::<*const u8, TraceFn>(fn_ptr_raw);
1367                let ct = CompiledTrace::from_aot_meta(
1368                    trace_entry,
1369                    decoded.header.head_pc,
1370                    decoded.header.n_ops,
1371                    decoded.header.dispatchable != 0,
1372                    decoded.header.window_size,
1373                    entry_tags_rc,
1374                    exit_tags_rc,
1375                    tag_res_kind,
1376                    per_exit_tags_decoded,
1377                    per_exit_inline_decoded,
1378                );
1379                vm.install_aot_trace(proto, ct);
1380                installed += 1;
1381                if probe_on {
1382                    eprintln!(
1383                        "luna-runtime-helpers: aot_trace_installed head_pc={}",
1384                        decoded.header.head_pc
1385                    );
1386                }
1387            }
1388        }
1389        installed
1390    }
1391}