1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
//! `JitState` sidecar: JIT-specific Vm state factored out
//! of the [`crate::vm::Vm`] struct.
//!
//! The interpreter dispatch loop reads `self.heap`,
//! `self.stack`, `self.frames`, ... as inherent fields; JIT state
//! lives one field hop away (`self.jit.active_trace` instead of
//! `self.active_trace`). The goal is physical separation between
//! interp and JIT bookkeeping at the field level.
//!
//! `JitState` is always present on a Vm — even an embedder that
//! never runs JIT'd code holds an inert `JitState` whose
//! `chunk_compiler` / `trace_compiler` are
//! [`crate::jit::NullJitBackend`]. The dispatcher reads
//! `self.jit.chunk_compiler` on every JIT entry, so the indirection
//! is fixed; `Option<JitState>` would impose an `unwrap()` on the
//! hot path for no benefit.
//!
//! Visibility: `#[doc(hidden)] pub` mirrors the existing pattern
//! used by the `#[doc(hidden)] pub fn jit_*` Vm helper methods.
//! Cross-crate access from `luna::jit_backend::*`
//! (which writes `vm.jit.pending_err` from `extern "C"` Cranelift
//! helpers) requires the struct + field to be `pub` somewhere
//! reachable, and `#[doc(hidden)]` keeps it out of the public
//! rustdoc surface.
use crate::vm::error::LuaError;
/// JIT-specific Vm state. See module docs.
#[doc(hidden)]
pub struct JitState {
/// Master JIT switch. Default `true`.
/// Sandbox embedders that rely on `instr_budget` for DoS
/// containment **must** call `Vm::set_jit_enabled(false)` —
/// JIT'd counted-for loops compile to native Cranelift IR
/// that does not tick the budget.
pub enabled: bool,
/// Trace JIT subswitch.
pub trace_enabled: bool,
/// Back-edge visits before a loop is recorded as a trace, and calls
/// before a function is. [`crate::jit::trace::TRACE_HOT_THRESHOLD`]
/// and [`crate::jit::trace::CALL_HOT_THRESHOLD`] by default; tests
/// lower them so that short programs exercise the trace JIT.
pub trace_hot_threshold: u32,
/// See [`Self::trace_hot_threshold`].
pub call_hot_threshold: u32,
/// Opt-in flag for the self-link cycle catch. Default `false`:
/// the catch has a known correctness problem, so it ships disabled.
pub self_link_enabled: bool,
/// The trace currently being recorded, or `None` if
/// the dispatch loop is in normal interpretation mode.
pub active_trace: Option<Box<crate::jit::trace::TraceRecord>>,
/// Index into `Vm.frames` of the Lua frame that the
/// recorder started in.
pub recording_frame_base: usize,
/// Running max of `inline_depth` observed on
/// any `RecordedOp` pushed by the recorder.
pub max_depth_seen: u8,
/// Diagnostic counters; see [`JitCounters`].
pub counters: JitCounters,
/// JIT-side error inbox set by a JIT table helper
/// when it detects a metatable on the target table. Taken by
/// the dispatcher after the JIT entry returns; the interp path
/// re-executes the call with proper `__index`/`__newindex`
/// semantics. Always `None` outside a JIT entry window.
/// Written from `luna::jit_backend::*` Cranelift helpers.
pub pending_err: Option<LuaError>,
/// Reusable buffer for the trace JIT dispatcher's
/// per-entry `reg_state`.
pub reg_state_buf: Vec<i64>,
/// Pool of reusable per-trace string accumulator
/// buffers.
pub str_buf_pool: Vec<Vec<u8>>,
/// Cap on the buffer pool size.
pub str_buf_pool_cap: usize,
/// Companion buffer for `entry_tags` (one u8 per
/// register at trace dispatch entry).
pub entry_tags_buf: Vec<u8>,
/// Closure-compile backend the dispatcher
/// routes through. Default is [`crate::jit::NullJitBackend`];
/// `Vm::install_jit_backend` swaps in caller-supplied
/// backends (the `luna` crate installs `CraneliftBackend`).
pub chunk_compiler: Box<dyn crate::jit::IntChunkCompiler>,
/// Trace-JIT backend.
pub trace_compiler: Box<dyn crate::jit::TraceCompiler>,
/// Bounded stitch-back depth remaining for
/// the dispatcher's `is_downrec_sentinel` admit path. Cycle-
/// safety checkpoint: a `downrec_link`-bearing
/// trace whose stitch target is itself can in principle keep
/// returning the DOWNREC sentinel forever, and the dispatcher
/// would forever re-admit it on the next interpreter loop
/// iteration. The counter is consulted BEFORE every downrec
/// admit; each admit decrements; when it would reach a negative
/// value the dispatcher refuses entry and force-deopts via
/// [`Self::suppress_downrec_admit_once`]. Reset to
/// [`JitState::STITCH_DEPTH_DEFAULT`] each natural deopt or
/// when the suppress flag fires (so a subsequent interp tick
/// past `head_pc` re-arms the budget). Default = the constant.
pub stitch_depth_remaining: u32,
/// One-shot suppression flag for the
/// dispatcher's trace admit. Set when a trace hands control back
/// at its own `head_pc` without having run the op there: the
/// dispatcher when it force-deopts a downrec entry (guard miss OR
/// cycle-budget exhausted), and a trace side exit taken before the
/// head op (through `luna_jit_suppress_trace_admit`). The NEXT
/// interpreter loop iteration skips the admit and lets interp run
/// the op at `head_pc`, advancing `pc` past `head_pc` and breaking
/// the otherwise-infinite admit loop. Consumed (cleared) the first
/// time the dispatcher reads it.
pub suppress_downrec_admit_once: bool,
/// Per-`Vm` JIT storage holder.
/// Default is [`crate::jit::NullJitStorage`]; the `luna_jit`
/// crate's `install_default_jit` swaps in a
/// `CraneliftJitStorage` carrying the cache + compiled-handle
/// collections. Accessed via downcast
/// from the `CraneliftBackend` trait impls.
pub storage: Box<dyn crate::jit::JitStorage>,
}
impl JitState {
/// Default per-dispatch stitch-back depth. The downrec admit
/// is gated by a multi-way CMP-chain that is a real runtime
/// guard (not constant-folded), so the only
/// way a downrec trace HITs is when `saved_pc` from the parent
/// frame matches one of the recorded `caller_pc` candidates;
/// each natural admit corresponds to ONE Lua call chain pop, so
/// the budget can safely grow to cover ~all consecutive HITs
/// expected in a hot loop without infinite-loop risk. `32` lets
/// 31 HITs accumulate before a forced-deopt resets the budget;
/// fib(3) hot loop's per-outer-iter pattern shows 1 HIT every
/// 5 admits, so `32` covers ~32 outer iters before any
/// false-classify pressure.
pub const STITCH_DEPTH_DEFAULT: u32 = 32;
}
/// Diagnostic counters and probe lists. All fields here are
/// diagnostic-only — they never affect dispatch correctness, and
/// can be cleared/snapshotted as a unit by tests.
#[doc(hidden)]
#[derive(Default)]
pub struct JitCounters {
/// Number of traces that have closed cleanly.
pub closed: u64,
/// Number of traces that have aborted.
pub aborted: u64,
/// Number of compiled traces that closed at a
/// `TraceEnd::InlineAbort` exit.
pub inline_abort: u64,
/// Count of closed traces the lowerer compiled.
pub compiled: u64,
/// Count of closed traces the lowerer rejected.
pub compile_failed: u64,
/// Number of trace dispatch entries.
pub dispatched: u64,
/// Number of trace entries that came back with
/// `jit_pending_err` set.
pub deopt: u64,
/// Count of side-trace recordings the dispatcher
/// started.
pub side_trace_started: u64,
/// Count of side-trace recordings that closed
/// AND reached the lowerer with a non-None outcome.
pub side_trace_compiled: u64,
/// Count of side traces that compiled but
/// failed the shape-match gate.
pub side_trace_shape_mismatch: u64,
/// Tally of NewTable sites flagged Sinkable.
pub sinkable_seen: u64,
/// Cumulative count of `BufferState::Bufferable`
/// accumulator sites.
pub accum_bufferable_seen: u64,
/// Tally of Sinkable sites that took the sunk-emit
/// path.
pub sunk_alloc: u64,
/// Tally of materialise-helper emit sites.
pub materialize_emit: u64,
/// Number of compiled
/// traces whose `CompiledTrace.per_exit_inline.len() > 0` (depth>0
/// inlined cmp side-exits were emitted). Probed via
/// `Vm::trace_per_exit_inline_compiled_count`. Together with
/// `per_exit_inline_dispatchable`, lets a diag distinguish
/// "recorder + lowerer can produce inline side-exits" from
/// "compiled trace is dispatchable enough to exercise the AOT
/// inline-chain reloc + deploy-resolver path".
pub per_exit_inline_compiled: u64,
/// Subset of
/// `per_exit_inline_compiled` that ALSO has `dispatchable == true`.
/// This is the count of traces that would actually exercise the
/// AOT inline-chain reloc + deploy-resolver path. Probed
/// via `Vm::trace_per_exit_inline_dispatchable_count`.
pub per_exit_inline_dispatchable: u64,
/// Total `Op::Closure` ops the trace JIT lowered to
/// `luna_jit_op_closure` helper calls.
pub closure_emit: u64,
/// Every compiled trace's `dispatch_off_reason`
/// pushed at compile time.
pub dispatch_off_reasons: Vec<&'static str>,
/// Every `try_compile_trace_with_options` None
/// return's last checkpoint.
pub compile_failed_reasons: Vec<&'static str>,
/// Every closed trace's `(is_call_triggered, ops_len)`.
pub closed_lens: Vec<(bool, usize)>,
/// Close-cause counts. Single per-reason bucket
/// that covers BOTH recorder-side abort/discard outcomes AND
/// lowerer-side dispatch_off (`dispatchable=false` post-compile)
/// outcomes, so probes can answer "how many of each reason fired"
/// in O(1); `aborted`, `closed_lens` and `dispatch_off_reasons`
/// carry no per-reason count.
///
/// Labels currently bumped (see `bump_close_cause` callers):
/// - `"trace-overflow"` (recorder MAX_TRACE_LEN overflow)
/// - `"partial-coverage-discard"` (recorder cap-not-reached discard)
/// - `"self-link-retf-r1"` (lowerer self-link dispatchable=false)
/// - `"selflink-yields-to-downrec"` (recorder self-link
/// trip rerouted to `downrec_close` when `cur_depth >= 2` AND a
/// parent `Op::Call` ancestor exists in `rec.ops`; moves fib(28)-
/// like shapes onto the DownRec lowerer arm — a single-candidate
/// guard chain keeps dispatchable=false + the
/// `"downrec-stitch-pending"` label)
/// - `"length-gate"` / `"InlineAbort-gate"` / `"GetI:inference-fail"`
/// / `"GetTable:inference-fail"` / `"GetField:inference-fail"`
/// / `"GetTabUp:inference-fail"` / `"GetUpval:not-Closure-use"`
/// (every lowerer-side dispatch_off label that already exists
/// on `CompiledTrace.dispatch_off_reason`)
pub close_cause_counts: std::collections::HashMap<&'static str, u64>,
/// Number of times the trace recorder captured
/// a [`crate::jit::trace_types::FieldIcSnapshot`] for the first
/// eligible `Op::GetField` site under `LUNA_JIT_FIELD_IC=1`.
/// Bumped exactly once per recording (the snapshot field is
/// `Option<_>` so subsequent GetFields short-circuit). 0 on the
/// env-default path.
pub field_ic_snapshot_captured: u64,
/// Number of compiled traces whose
/// `CompiledTrace.downrec_link` is `Some(_)`. Bumped at trace
/// finalisation alongside the `dispatch_off_reasons.push` site
/// (`exec.rs` close handler) when the lowerer's
/// `downrec_idx_opt` arm emitted the stitch sentinel + caller-pc
/// guard scaffold.
pub downrec_link_compiled: u64,
/// Number of times the dispatcher's
/// `is_downrec_sentinel` arm in
/// `crates/luna-core/src/vm/exec.rs` fired with the caller-pc
/// guard reporting a HIT (saved-PC at `reg_state[window_size]`
/// matched the recorded `dr_return_pc`). Each bump corresponds
/// to one stitch-back round: the trace returned the
/// `SIDE_SENT_DOWNREC_CODE` sentinel and the dispatcher fed the
/// trace's `head_pc` back to the interpreter loop so the
/// admit-by-`downrec_link` gate re-enters the trace (bounded by
/// the dispatcher's `stitch_depth_remaining` checkpoint).
pub downrec_dispatched: u64,
/// Number of times the dispatcher's
/// `is_downrec_sentinel` arm observed a guard MISS (the trace
/// invocation returned with `downrec_link.is_some()` but the
/// returned sentinel was NOT [`SIDE_SENT_DOWNREC_CODE`] — i.e.
/// the lowerer's `deopt_blk` arm fired, returning `head_pc` via
/// the GLOBAL sentinel). Bumped on the dispatcher side via the
/// post-invoke check so the caller-pc guard miss-rate can be
/// measured via `downrec_dispatched + downrec_deopt`.
pub downrec_deopt: u64,
/// Number of compiled traces whose
/// `CompiledTrace.downrec_multi_way_count >= 2`. Bumped at the
/// close handler in `crates/luna-core/src/vm/exec.rs` alongside
/// `downrec_link_compiled`. A single-CMP guard never bumps this
/// counter. Independent of the dispatcher's `downrec_dispatched` /
/// `downrec_deopt` counters, which measure runtime guard hit-rate.
pub multi_way_guard_emitted: u64,
}
impl JitCounters {
/// Bump the close-cause bucket for `reason`.
/// Mirrors the existing per-site pattern (`aborted += 1`,
/// `dispatch_off_reasons.push(reason)`) but with O(1) per-reason
/// access via a `HashMap`. Single source of truth for the
/// close-cause taxonomy probe surface
/// (`Vm::trace_close_cause_counts`).
#[inline]
pub fn bump_close_cause(&mut self, reason: &'static str) {
*self.close_cause_counts.entry(reason).or_insert(0) += 1;
}
}
impl JitState {
/// Build an inert `JitState` whose backends are
/// [`crate::jit::NullJitBackend`], with `enabled = true` and
/// `trace_enabled = true`. Embedders that want an interp-only
/// Vm call `vm.set_trace_jit_enabled(false)` explicitly.
/// `Vm::new_inner` calls this; the `luna` crate's
/// `Vm::new_minimal_with_jit` then swaps the backends to
/// `CraneliftBackend` via `Vm::install_jit_backend`.
pub fn with_null_backend() -> JitState {
JitState {
enabled: true,
trace_enabled: true,
trace_hot_threshold: crate::jit::trace::TRACE_HOT_THRESHOLD,
call_hot_threshold: crate::jit::trace::CALL_HOT_THRESHOLD,
self_link_enabled: false,
active_trace: None,
recording_frame_base: 0,
max_depth_seen: 0,
counters: JitCounters::default(),
pending_err: None,
reg_state_buf: Vec::new(),
str_buf_pool: Vec::new(),
str_buf_pool_cap: 4,
entry_tags_buf: Vec::new(),
chunk_compiler: Box::new(crate::jit::NullJitBackend),
trace_compiler: Box::new(crate::jit::NullJitBackend),
stitch_depth_remaining: JitState::STITCH_DEPTH_DEFAULT,
suppress_downrec_admit_once: false,
storage: Box::new(crate::jit::NullJitStorage),
}
}
}