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}