Skip to main content

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}