luna_core/jit/abi.rs
1//! JIT-backend trait boundary (luna-core side).
2//!
3//! Defines the [`IntChunkCompiler`] + [`TraceCompiler`] traits that
4//! the interpreter dispatcher in [`crate::vm::exec`] routes through
5//! to invoke the JIT codegen. Two implementations exist:
6//!
7//! - [`NullJitBackend`] (in this file) — does nothing; the default
8//! for [`crate::vm::Vm::new_minimal`] in luna-core.
9//! - `CraneliftBackend` (in the `luna` crate, `luna::jit_backend`) —
10//! delegates to Cranelift. Installed by `luna::install_default_jit`
11//! and `luna::Vm::new_minimal_with_jit`.
12
13use crate::jit::JitVmGuard;
14use crate::jit::trace_types::{CompileOptions, CompiledTrace, TraceRecord};
15use crate::runtime::Gc;
16use crate::runtime::LuaClosure;
17use crate::runtime::function::Proto;
18
19/// Native entry-point produced by `try_compile_int_chunk` for a chunk
20/// with zero params. Returns the chunk's `Return1` value as i64;
21/// `Return0` chunks return 0 (the caller knows by inspecting
22/// `JitHandle::returns_one`).
23// SAFETY: offset is hand-computed against the `repr(C)` layout of the target struct in this same module; the Cranelift lowerer relies on it staying in sync.
24pub type IntChunkFn = unsafe extern "C" fn() -> i64;
25
26/// One-arg JIT entry signature. See [`IntChunkFn`] for the zero-arg
27/// shape and [`MAX_JIT_ARITY`] for the cap.
28// SAFETY: offset is hand-computed against the `repr(C)` layout of the target struct in this same module; the Cranelift lowerer relies on it staying in sync.
29pub type IntFn1 = unsafe extern "C" fn(i64) -> i64;
30/// Two-arg JIT entry signature.
31// SAFETY: offset is hand-computed against the `repr(C)` layout of the target struct in this same module; the Cranelift lowerer relies on it staying in sync.
32pub type IntFn2 = unsafe extern "C" fn(i64, i64) -> i64;
33/// Three-arg JIT entry signature.
34// SAFETY: offset is hand-computed against the `repr(C)` layout of the target struct in this same module; the Cranelift lowerer relies on it staying in sync.
35pub type IntFn3 = unsafe extern "C" fn(i64, i64, i64) -> i64;
36/// Four-arg JIT entry signature.
37// SAFETY: offset is hand-computed against the `repr(C)` layout of the target struct in this same module; the Cranelift lowerer relies on it staying in sync.
38pub type IntFn4 = unsafe extern "C" fn(i64, i64, i64, i64) -> i64;
39
40/// Max arity the int-chunk compiler accepts before bailing back to
41/// the interpreter. Tuned against the 5-arg-and-down distribution in
42/// the official PUC test suites — extending past 4 buys very little
43/// hit-rate and complicates the dispatch table in [`crate::vm::exec`].
44pub const MAX_JIT_ARITY: u8 = 4;
45
46/// Outcome of a closure-compilation attempt. Kept ABI-compatible with
47/// the legacy `crate::jit::cache_lookup_or_compile` return tuple so the
48/// `Vm::populate_jit_cache` call site doesn't have to re-shape its
49/// destructure.
50#[derive(Clone, Copy, Debug)]
51pub enum CompileResult {
52 /// The proto was lowered (or served from cache); the fields mirror
53 /// the legacy 7-tuple returned by `cache_lookup_or_compile`.
54 Compiled {
55 /// Mmap'd entry address; transmute to the matching `unsafe extern
56 /// "C" fn` shape at the call site.
57 entry: *const u8,
58 /// Argument count (0..=`MAX_JIT_ARITY`).
59 num_args: u8,
60 /// True when the compiled chunk produces a single observable
61 /// result (`Op::Return1`); false when it only side-effects.
62 returns_one: bool,
63 /// Bit `i` set ↔ argument slot `i` is `Value::Float`.
64 arg_float_mask: u8,
65 /// Bit `i` set ↔ argument slot `i` is `Value::Table`.
66 arg_table_mask: u8,
67 /// True when the chunk's return value is float (only meaningful
68 /// when `returns_one` is true).
69 ret_is_float: bool,
70 /// True when the chunk's return value is `Value::Table`.
71 ret_is_table: bool,
72 },
73 /// The proto fell outside the whitelist or its compile pass bailed.
74 /// The interpreter handles it unchanged.
75 Skipped,
76}
77
78/// Closure-compilation backend. The interpreter dispatcher (and the
79/// `Op::Call` JIT fast path inside `vm/exec.rs`) calls
80/// `chunk_compiler.try_compile(proto, pre53, float_only)` once per
81/// Proto on the cold path; subsequent hits read the result back from
82/// `Proto.jit: Cell<JitProtoState>` directly, so the vtable cost is
83/// bounded by Proto count, not by call count.
84///
85/// `enter` is the per-JIT-entry RAII guard that pins the active `Vm`
86/// pointer (and optional `LuaClosure`) into the thread-locals the JIT
87/// helpers read. Taking a raw `*mut Vm` keeps the trait object-safe
88/// and lets the dispatcher pass `self as *mut Vm` without holding a
89/// mutable borrow on `self` while reading `self.chunk_compiler`.
90pub trait IntChunkCompiler {
91 /// Attempt to compile `proto`. Returns [`CompileResult::Compiled`] on
92 /// a whitelist hit or [`CompileResult::Skipped`] when the body is
93 /// outside the JIT's supported shape.
94 ///
95 /// `storage` is the per-`Vm` JIT cache
96 /// + handle holder. The Cranelift backend downcasts it to its
97 /// concrete `CraneliftJitStorage`; the no-op [`NullJitBackend`]
98 /// ignores it (returns `Skipped` without touching storage).
99 fn try_compile(
100 &self,
101 storage: &mut dyn crate::jit::JitStorage,
102 proto: Gc<Proto>,
103 pre53: bool,
104 float_only: bool,
105 ) -> CompileResult;
106
107 /// Install the active `Vm` + closure pointer into the JIT
108 /// helpers' thread-local slots; the returned guard restores
109 /// state on drop. For [`NullJitBackend`] this is a no-op
110 /// (helpers never fire because nothing compiled).
111 ///
112 /// SAFETY: the caller must ensure `vm` is a live, exclusively
113 /// borrowed `Vm` for the duration of the returned guard.
114 fn enter(&self, vm: *mut crate::vm::Vm, cl: Option<Gc<LuaClosure>>) -> JitVmGuard;
115}
116
117/// Trace-JIT backend. Receives a closed [`TraceRecord`] from the
118/// interpreter's recorder; returns `Some(CompiledTrace)` on success or
119/// `None` when the lowerer bailed. `last_compile_checkpoint` exposes
120/// the lowerer's per-thread last-phase marker used by
121/// `Vm.trace_compile_failed_reasons`.
122pub trait TraceCompiler {
123 /// Attempt to lower `record` into native code under `opts`. Returns
124 /// `None` if the lowerer bailed at any checkpoint.
125 ///
126 /// `storage` is the per-`Vm` JIT cache
127 /// + handle holder; the Cranelift backend uses it to park each
128 /// compiled trace's `JITModule`. [`NullJitBackend`] ignores it.
129 fn try_compile_trace(
130 &self,
131 storage: &mut dyn crate::jit::JitStorage,
132 record: &TraceRecord,
133 opts: CompileOptions,
134 ) -> Option<CompiledTrace>;
135
136 /// Name of the lowerer's last-reached checkpoint (diagnostic; lets
137 /// the recorder bucket failures by phase).
138 fn last_compile_checkpoint(&self) -> &'static str;
139}
140
141/// No-op backend installed by [`crate::vm::Vm::new_minimal`] in
142/// luna-core. Both calls report "nothing compiled" so the interp
143/// dispatcher always takes the standard path. Embedders who want a
144/// real JIT either depend on the `luna` crate (whose
145/// `Vm::new_minimal_with_jit` swaps in `CraneliftBackend`) or write
146/// their own `IntChunkCompiler` / `TraceCompiler` implementation.
147#[derive(Clone, Copy, Debug, Default)]
148pub struct NullJitBackend;
149
150impl IntChunkCompiler for NullJitBackend {
151 fn try_compile(
152 &self,
153 _: &mut dyn crate::jit::JitStorage,
154 _: Gc<Proto>,
155 _: bool,
156 _: bool,
157 ) -> CompileResult {
158 CompileResult::Skipped
159 }
160
161 fn enter(&self, _: *mut crate::vm::Vm, _: Option<Gc<LuaClosure>>) -> JitVmGuard {
162 // The TLS slots stay whatever they were — no JIT mcode will
163 // execute under a NullJitBackend (try_compile returned
164 // Skipped, so no Proto reaches JitProtoState::Compiled, and
165 // try_compile_trace returns None so no CompiledTrace ever
166 // dispatches), so the helpers never read those TLS values.
167 // JitVmGuard's drop is already a no-op (see the comment on
168 // `JitVmGuard::drop` in `mod.rs`).
169 crate::jit::noop_jit_guard()
170 }
171}
172
173impl TraceCompiler for NullJitBackend {
174 fn try_compile_trace(
175 &self,
176 _: &mut dyn crate::jit::JitStorage,
177 _: &TraceRecord,
178 _: CompileOptions,
179 ) -> Option<CompiledTrace> {
180 None
181 }
182
183 fn last_compile_checkpoint(&self) -> &'static str {
184 "null-backend"
185 }
186}