luna_core/vm/exec.rs
1//! The interpreter. Dispatch is a plain match over opcodes (the P10 ceiling
2//! pass owns dispatch optimization). Lua→Lua calls share one loop and never
3//! recurse the Rust stack; only native↔Lua boundaries do (e.g. pcall).
4//!
5//! Varargs follow 5.5 semantics: a vararg call materializes a vararg table
6//! (fields 1..n plus "n") kept in the function's own stack slot; `...`
7//! expands from it and `...name` binds it. 5.1 LUAI_COMPAT_VARARG also
8//! materializes a local `arg` table (see `proto.has_compat_vararg_arg`).
9
10use crate::compiler::compile_chunk;
11use crate::frontend::{SyntaxError, parse};
12use crate::jit::send_compat::TArc;
13use crate::numeric::{self, Num};
14use crate::runtime::heap::GcHeader;
15use crate::runtime::{
16 AfterClose, CallFrame, CloseCont, ContKind, Coro, CoroStatus, Frame, Gc, Heap, LuaClosure,
17 MetaAction, MetaCont, NativeClosure, NativeCont, Table, TableError, UpvalState, Upvalue, Value,
18};
19use crate::version::LuaVersion;
20use crate::vm::builtins::{nat_pairs, nat_pcall, nat_xpcall};
21use crate::vm::error::LuaError;
22use crate::vm::isa::{Inst, Op};
23
24/// A Lua virtual machine: one OS thread's worth of Lua state.
25///
26/// # Threading model
27///
28/// `Vm` is **`!Send + !Sync`**. The GC uses `Gc<T> = NonNull<T>` over
29/// an intrusive mark-sweep heap (not `Rc<RefCell<T>>`), and the trace
30/// JIT side-table uses `Rc<CompiledTrace>` — both single-threaded by
31/// design. Embedders that want concurrency spawn one `Vm` per OS
32/// thread (or per single-thread Tokio worker) and exchange data via
33/// channels. See [`docs/threading.md`](../../docs/threading.md) for
34/// canonical embedding patterns including Tokio `current_thread`,
35/// `LocalSet` on multi-thread, and `Vm`-per-OS-thread + channels.
36///
37/// The constraint is enforced at compile time:
38///
39/// ```compile_fail
40/// fn must_be_send<T: Send>() {}
41/// must_be_send::<luna_core::Vm>(); // error[E0277]: `Vm` cannot be sent between threads safely
42/// ```
43///
44/// A future `feature = "send"` (post-v1.1 sprint) will gate an
45/// opt-in `Arc<RwLock<T>>` mode with a hard ≤8% perf regression
46/// budget. See `.dev/rfcs/v1.1-rfc-vm-send-sync.md` for the design.
47pub struct Vm {
48 /// The GC heap owned by this VM. Embedders normally interact via the
49 /// `Vm` methods (`load` / `call_value` / `set_global` / …) rather than
50 /// the heap directly.
51 pub heap: Heap,
52 stack: Vec<Value>,
53 frames: Vec<CallFrame>,
54 /// P17-D Week 1 shadow — frames_top mirrors `self.frames.len()`.
55 /// Synced on every push/pop in `frames_push_sync`/`frames_pop_sync`
56 /// helpers (debug-asserted on use). NOT consumed by readers yet;
57 /// week 1 is pure scaffold. Week 2-N migrations replace readers
58 /// one slice at a time, then remove `frames: Vec<CallFrame>` in
59 /// favour of a flat `[CallFrame; MAX_FRAMES]` indexed by frames_top.
60 frames_top: u32,
61 /// open upvalues, sorted ascending by stack slot
62 open_upvals: Vec<(u32, Gc<Upvalue>)>,
63 /// to-be-closed slots, ascending
64 tbc: Vec<u32>,
65 /// logical stack top for multi-result sequences
66 pub(crate) top: u32,
67 globals: Gc<Table>,
68 /// shared metatable for all strings (populated by the string lib, P04)
69 /// per-basic-type metatables (PUC luaT): indexed by `type_mt_slot`
70 /// (0 nil, 1 boolean, 2 number, 3 string, 4 function); tables carry their
71 /// own. Settable via debug.setmetatable.
72 type_mt: [Option<Gc<Table>>; 5],
73 /// pre-interned metamethod event names, indexed by `Mm`
74 mm_names: Vec<Gc<crate::runtime::LuaStr>>,
75 /// native↔Lua nesting depth (PUC C-stack guard analogue)
76 c_depth: u32,
77 /// number of live pcall/xpcall continuation frames on the running thread
78 /// (PUC counts these against nCcalls). Bounds protected-call recursion the
79 /// way `c_depth` bounds call_value recursion. Per-thread: saved/restored
80 /// with the coroutine context, since continuations survive a yield.
81 pcall_depth: u32,
82 /// number of non-yieldable C calls in flight on the running thread (PUC's
83 /// `L->nny`). A library callback that runs via synchronous Rust recursion
84 /// (sort comparator, gsub replacement) cannot be continued across a yield,
85 /// so it bumps this for its duration; `coroutine.yield` inside hits the
86 /// C-call boundary and errors. Always 0 at a suspend point (a yield can
87 /// never cross such a call), so it needs no per-thread save/restore.
88 nny: u32,
89 /// Nonzero while an xpcall message handler is on the Rust stack. Used so a
90 /// stack-overflow that surfaces *inside* the handler is reported as PUC's
91 /// "error in error handling" (LUA_ERRERR + `luaD_seterrorobj`), not the
92 /// plain "stack overflow" — errors.lua :606's `checkerr("error handling",
93 /// loop)` then matches. PUC tracks this via the soft-cap window
94 /// `nCcalls >= MAXCCALLS/10*11`; luna's c_depth is strict, so we mark the
95 /// scope explicitly.
96 msgh_depth: u32,
97 /// set by a coroutine closing itself (`coroutine.close()` on the running
98 /// thread): the to-be-closed handlers have already run; the thread must now
99 /// terminate. `Some(None)` is a clean close, `Some(Some(e))` a handler
100 /// raised `e`. Checked by `exec_with`/`resume_coro` to propagate (not
101 /// unwind, so a protecting pcall cannot catch it) the termination.
102 terminating: Option<Option<Value>>,
103 /// xoshiro256** state (math.random)
104 rng: [u64; 4],
105 /// VM creation time (os.clock)
106 started: std::time::Instant,
107 version: LuaVersion,
108 /// error object being threaded through a chain of __close handlers; a GC
109 /// root for the duration (a handler may trigger collection)
110 closing_err: Option<Value>,
111 /// the coroutine whose context is currently live in the fields above;
112 /// `None` while the main thread runs (P05)
113 current: Option<Gc<crate::runtime::Coro>>,
114 /// the main thread's saved execution context while a coroutine runs
115 main_ctx: Option<SavedCtx>,
116 /// set by `coroutine.yield` to suspend the running coroutine: the yielded
117 /// values plus the slot/result-count needed to finish the yielding call on
118 /// the next resume. Checked by `exec` to propagate (not unwind) on yield.
119 yielding: Option<(Vec<Value>, u32, i32)>,
120 /// results expected by the in-flight native call (so `yield` knows how many
121 /// values its call site wants when it suspends)
122 native_nresults: i32,
123 /// identity object for the main thread, returned by `coroutine.running`
124 /// (the main thread's context lives in the VM fields / `main_ctx`, not here)
125 main_coro: Option<Gc<Coro>>,
126 /// `collectgarbage` mode name ("incremental"/"generational"). The collector
127 /// itself is still stop-the-world mark-sweep; this tracks the mode so mode
128 /// switches report the previous one, as PUC does.
129 gc_mode: &'static str,
130 /// the live-register boundary of the running thread for GC rooting (PUC's
131 /// `L->top`): set precisely at each GC safe point so freed temporary
132 /// registers above it are not rooted. Without this the collector roots the
133 /// whole stack window, pinning weak-table values stranded in stale temps
134 /// (e.g. closure.lua's `while x[1]` GC-detection loop).
135 pub(crate) gc_top: u32,
136 /// `collectgarbage("param", name [,value])` pacing parameters. The collector
137 /// is still stop-the-world, so these are stored/returned for API fidelity
138 /// (PUC round-trips them via `setparam`/`getparam`). Defaults mirror PUC's
139 /// `LUAI_GC*` knobs: pause=200, stepmul=100, stepsize=13.
140 gc_pause: i64,
141 gc_stepmul: i64,
142 gc_stepsize: i64,
143 /// true while `__gc` finalizers are being run, so a finalizer that calls
144 /// `collectgarbage` gets a no-op (PUC's non-reentrancy: lua_gc returns -1 →
145 /// `collectgarbage` yields fail).
146 gc_finalizing: bool,
147 /// C ABI scratch (`capi` module): the host-visible value stack that C
148 /// callers operate on via `lua_pushinteger` / `lua_tostring` / etc.
149 /// Kept here (instead of in a separate `LuaState` wrapper) so the
150 /// trampoline that bridges to a `LuaCFunction` can safely cast the
151 /// Vm pointer it already holds to the public `*mut LuaState` type
152 /// without any aliasing of `&mut Vm` against `&mut LuaState.vm`.
153 pub capi_stack: Vec<crate::runtime::Value>,
154 /// Pinned CString backing the pointer last returned by `lua_tostring`;
155 /// valid until the next `lua_tostring` on the same Vm.
156 pub capi_cstr_pin: Option<std::ffi::CString>,
157 /// PUC 5.4+ warning system. Lua manual §6.1 `warn`: emitted messages
158 /// concatenate across continuation calls until a non-`tocont` call
159 /// flushes; the default warnf recognises `@on`/`@off` control messages
160 /// and starts disabled. luna's `emit_warn` mirrors the default warnf
161 /// behaviour and 5.4+ `__gc` errors are routed through it (5.1–5.3
162 /// keep the older raise semantics).
163 pub(crate) warn_state: WarnState,
164 pub(crate) warn_buf: Vec<u8>,
165 /// P09 embedding cooperative budget: a per-Vm tick counter that the run
166 /// loop decrements once per dispatch turn. When it hits zero the loop
167 /// raises a catchable "instruction budget exceeded" error so the embedder
168 /// can yield control back to its caller (short-script eval, game
169 /// frame budgets). `None` = unbounded; reset on each call via
170 /// `set_instr_budget`.
171 pub(crate) instr_budget: Option<i64>,
172 // v1.1 A2 — JIT-specific fields moved to `JitState` sidecar; see
173 // `self.jit` below + `crate::vm::jit_state` for field docs.
174 // (Was: jit_enabled here.)
175 // v1.1 A2 — was: trace_jit_enabled (moved to JitState).
176 // v1.1 A2 — was: p16_self_link_enabled (moved to JitState).
177 // v1.1 A2 — was: active_trace, recording_frame_base, trace_max_depth_seen,
178 // trace_closed_count, trace_aborted_count, trace_inline_abort_count,
179 // trace_dispatch_off_reasons, trace_compile_failed_reasons, trace_closed_lens,
180 // trace_compiled_count, trace_compile_failed_count, trace_dispatched_count,
181 // trace_deopt_count, trace_side_trace_{started,compiled,shape_mismatch}_count,
182 // trace_{sinkable,accum_bufferable}_seen_count, trace_{sunk_alloc,
183 // materialize_emit,closure_emit}_count — all moved to JitState.
184 /// Bytecode-loading gate. Default `true`. Sandbox embedders should
185 /// call `set_bytecode_loading(false)` so `load`/`loadstring` reject
186 /// precompiled chunks (which bypass the parser's depth / opcode
187 /// limits). When `false`, the loader rejects any source whose first
188 /// byte is the bytecode signature `\27` ("`\27Lua`").
189 pub(crate) bytecode_loading: bool,
190 /// PUC bytecode-loading gate. Default `false` — PUC `.luac` files are
191 /// a strictly larger trust surface than luna's own dump format
192 /// (third-party toolchain bugs, malformed chunks, unknown opcode
193 /// shapes). When `true`, the loader routes `\x1bLua\x{51..55}` inputs
194 /// through the per-dialect PUC translators in `crate::vm::dump::puc`
195 /// (Phase LB Wave 2 — currently returns "not yet implemented" stubs).
196 /// Embedder toggles via `set_puc_bytecode_loading`.
197 pub(crate) puc_bytecode_loading: bool,
198 /// Byte budget for source fed into `load` / `loadstring` / `Vm::load`.
199 /// Default [`Vm::DEFAULT_LOADER_INPUT_BUDGET`] (256 MiB). When the
200 /// accumulated reader output (`load(f, ...)`) or a one-shot `&[u8]`
201 /// source exceeds this, the loader returns the PUC-shaped
202 /// `not enough memory` error before the host allocator is asked to
203 /// hold the next chunk. Defends against `heavy.lua::loadrep`-style
204 /// 7 GB+ feeder loops that would otherwise SIGSEGV when `Vec::push`
205 /// crosses `isize::MAX` or the host runs out of RAM. Tracked at
206 /// `.dev/known-bugs/fixed/heavy-lua-sigsegv-under-128mb-loadrep.md`.
207 /// Embedders that genuinely need to load > 256 MiB sources widen the
208 /// cap via [`Vm::set_loader_input_budget`].
209 pub(crate) loader_input_budget: usize,
210 /// In-process log of fully-emitted warnings (each entry = one flushed
211 /// message, sans the "Lua warning: " prefix and trailing newline). Lets
212 /// tests assert what was warned without scraping stderr.
213 pub(crate) warn_log: Vec<Vec<u8>>,
214 /// PUC's `LUA_REGISTRYINDEX` table — a single Lua table the debug library
215 /// exposes via `debug.getregistry`. Used to hold `_HOOKKEY` (the weak-key
216 /// table PUC's `db_sethook` keys per-thread hooks under). luna stores hook
217 /// state directly in `Vm.hook`/`Coro.hook`, so the entry is largely a
218 /// shape stub for db.lua :328; if other registry-keyed APIs land later
219 /// they can share this table.
220 pub(crate) registry: Option<Gc<Table>>,
221 /// the shared `FILE*` metatable for io file handles (PUC's LUA_FILEHANDLE
222 /// registry entry); attached to every file userdata the io library makes
223 pub(crate) file_mt: Option<Gc<Table>>,
224 /// io library default input/output streams (PUC registry IO_INPUT/IO_OUTPUT)
225 pub(crate) io_input: Option<Gc<crate::runtime::Userdata>>,
226 pub(crate) io_output: Option<Gc<crate::runtime::Userdata>>,
227 /// the running thread's debug hook state (`debug.sethook`); per-thread,
228 /// swapped with the execution context on a coroutine resume/yield
229 pub(crate) hook: HookState,
230 /// true while the hook itself runs, so its own execution fires no events
231 /// (PUC clears the mask for the duration)
232 pub(crate) in_hook: bool,
233 /// arms the next Lua frame's `tailcalls` count (PUC `ci->u.l.tailcalls`),
234 /// consumed by `push_frame`. `OP_TailCall` sets it to the caller's
235 /// own tailcalls + 1 before begin_call so deeply tail-recursive chains
236 /// accumulate the count instead of capping at 1.
237 pub(crate) pending_tailcalls: u32,
238 /// Name of the C native that just propagated an error (captured before
239 /// the native is popped from `running_natives`). Lets a dying coroutine
240 /// preserve `[C]: in function '<name>'` at the top of its traceback
241 /// snapshot — PUC walks `luaG_funcnamefrompc` over a still-live ci, but
242 /// luna's native frames are off-stack so we stash the name explicitly.
243 pub(crate) errored_native: Option<String>,
244 /// PUC `CallInfo.u2.transferinfo`: index of the first transferred value
245 /// (relative to the activation's func slot) and the number transferred.
246 /// Set just before firing a call/return hook, read by `getinfo("r")`.
247 pub(crate) hook_ftransfer: u16,
248 pub(crate) hook_ntransfer: u16,
249 /// metamethod event tag (e.g. "close") to attach to the next Lua frame
250 /// pushed by `push_frame`; `close_slots` sets this before calling a
251 /// `__close` handler so `debug.traceback` names it "metamethod 'close'"
252 /// (PUC `CallInfo.u.l.tm`). Single-shot: `push_frame` consumes it.
253 pending_tm: Option<&'static str>,
254 /// `true` when the next `push_frame` is the user hook function itself,
255 /// so `debug.getinfo(1).namewhat` resolves to `"hook"` (PUC
256 /// `CIST_HOOKED`). `run_hook` arms it before dispatching the hook.
257 pending_is_hook: bool,
258 /// traceback snapshot taken at the error point (the first `unwind` entry
259 /// for the in-flight error), so that an `xpcall` msgh — which runs *after*
260 /// the failed frames are popped — can still see the error point's stack
261 /// via `debug.traceback`. PUC `luaG_errormsg` instead runs msgh with the
262 /// stack intact; we approximate by snapshotting the string and letting
263 /// `d_traceback` consume it. Cleared on Cont catch and at host-level
264 /// `call_value` entry (`public_call_depth == 0`).
265 pub(crate) error_traceback: Option<Vec<u8>>,
266 /// nesting depth of public `call_value` entries (host vs. internal). The
267 /// outermost entry (depth 0) resets per-error state (`error_traceback`);
268 /// internal calls (e.g. xpcall msgh, sort callback) preserve it.
269 public_call_depth: u32,
270 /// stack of native (`Value::Native`) closures currently running on the
271 /// Rust call stack. `begin_call` pushes the closure before invoking
272 /// `nc.f` and pops on return. Used by `arg_error` to detect a *nested*
273 /// native call (PUC `ar.name == NULL` at level 0 because the level-0
274 /// caller is C, not Lua) and qualify the running function's name via
275 /// `pushglobalfuncname` (e.g. `'sort'` → `'table.sort'`).
276 pub(crate) running_natives: Vec<Gc<NativeClosure>>,
277 /// Parallel to `running_natives`: each entry's `(func_slot, nargs)` is
278 /// the native's argument-window head and width, so `debug.getlocal`
279 /// can index it like PUC's `luaG_findlocal` `(C temporary)` path.
280 pub(crate) running_native_slots: Vec<(u32, u32)>,
281 // v1.1 A2 — was: jit_pending_err, jit_reg_state_buf, jit_str_buf_pool,
282 // jit_str_buf_pool_cap, jit_entry_tags_buf, chunk_compiler,
283 // trace_compiler — all moved to JitState. See `jit` below.
284 /// v1.1 A2 — JIT sidecar. Always present (never `Option`); inert
285 /// when `chunk_compiler` / `trace_compiler` are
286 /// [`crate::jit::NullJitBackend`]. See [`crate::vm::jit_state`].
287 ///
288 /// `#[doc(hidden)] pub` so the `luna` crate's
289 /// `extern "C"` JIT helpers can write `vm.jit.pending_err`
290 /// directly (same pattern as the pre-A2 `pub Vm::jit_pending_err`
291 /// field). Not part of the embedder-facing API surface.
292 #[doc(hidden)]
293 pub jit: crate::vm::jit_state::JitState,
294
295 /// B12 host roots — append-only `Vec<Value>` traced as an extra
296 /// GC root set. `Lua` facade handles (`LuaFunction`, `LuaTable`,
297 /// `LuaRoot`) hold indices into this vector so the underlying
298 /// `Gc<T>` stays alive across `eval` calls / yield boundaries.
299 ///
300 /// v1.1 strategy: append-only with explicit `unpin_all` / new Vm.
301 /// Slot recycling lands in Phase 3 alongside B8 LuaUserdata, when
302 /// the trade-offs between `Drop` plumbing and append-only memory
303 /// growth have a richer ergonomics envelope to live in.
304 pub(crate) host_roots: Vec<crate::vm::host_roots::HostRootSlot>,
305 /// v1.3 Phase SR — recycled-slot index pool. `pin_host` pops the
306 /// back if non-empty, else extends `host_roots`. Generation
307 /// overflow at `u32::MAX` retires the slot (NOT pushed here).
308 pub(crate) host_roots_free: Vec<u32>,
309
310 /// v2.1 — GC-rooted scratch stack for `table.sort` (and any other
311 /// builtin that needs a Rust-side `Vec<Value>` to outlive a user
312 /// callback). Each entry is one in-flight working buffer; `gc_roots`
313 /// extends with every contained `Value` so a `collectgarbage()`
314 /// inside the comparator cannot free strings/tables snapshotted
315 /// here. Nested sorts push a new buffer on entry, pop on exit
316 /// (sort.lua's `load(..)(); collectgarbage()` compare callback
317 /// regression).
318 pub(crate) sort_scratch: Vec<Vec<Value>>,
319
320 /// v1.3 Phase ML — MacroLua compile-time macro registry.
321 /// Pre-populated with built-in macros (`@quote` / `@unquote` /
322 /// `@if` / `@gensym`) at construction time when `version ==
323 /// LuaVersion::MacroLua`; embedders register custom macros via
324 /// [`Vm::define_macro`]. The expander runs once per `load()` call
325 /// between lexing and parsing (only when `is_macro_lua()`).
326 pub(crate) macro_registry: crate::frontend::macro_expander::MacroRegistry,
327
328 /// v1.2 Track B — per-Vm cache of `Gc<Table>` metatables keyed
329 /// by `TypeId::of::<T>()` for embedder types implementing
330 /// [`crate::vm::userdata_trait::LuaUserdata`]. Populated lazily by
331 /// [`Vm::register_userdata`]; metatables are pinned via
332 /// [`Vm::pin_host`] at registration time so the entry's
333 /// `Gc<Table>` stays live for the rest of the Vm's lifetime.
334 pub(crate) userdata_metatables:
335 std::collections::HashMap<std::any::TypeId, Gc<crate::runtime::table::Table>>,
336
337 /// B6 — classification of the most recent error raised on this Vm.
338 /// Embedders read via [`Vm::error_kind`]; the dispatcher sets it
339 /// at well-known sites (syntax errors, instr-budget trips, native
340 /// callback errors, type errors).
341 pub(crate) last_error_kind: crate::vm::error::LuaErrorKind,
342
343 /// B6 — `(source_name, line)` of the most recent error. Set by the
344 /// dispatcher / lexer / parser; cleared when a new call_value
345 /// enters cleanly.
346 pub(crate) last_error_source: Option<(String, u32)>,
347
348 /// v1.1 B10 Stage 1 — when `true`, `instr_budget` exhaustion in
349 /// the dispatcher hot loop yields cooperatively (sets
350 /// [`Vm::host_yield_pending`] + returns a sentinel `Err` walked up
351 /// to `EvalFuture::poll`) instead of returning a real
352 /// "instruction budget exceeded" error. Set by [`Vm::eval_async`]
353 /// for the duration of the future; restored to `false` on
354 /// `Poll::Ready`. The sync `Vm::eval` / `Vm::call_value` paths
355 /// leave it `false` so v1.0 behavior is preserved exactly.
356 pub(crate) async_mode: bool,
357
358 /// v1.1 B10 Stage 1 — host waker cloned by `EvalFuture::poll`
359 /// before driving a slice. The dispatcher itself does not call it
360 /// (the future's poll loop does `wake_by_ref` after observing
361 /// `BudgetExhausted`), but storing the waker keeps the door open
362 /// for Stage 2 async natives to wake the host directly from a
363 /// helper future.
364 pub(crate) async_waker: Option<std::task::Waker>,
365
366 /// v1.1 B10 Stage 1 — per-poll opcode quota loaded into
367 /// `instr_budget` at the start of each `EvalFuture::poll` slice.
368 /// Default 10_000 (RFC §D5). Tunable via
369 /// [`Vm::set_async_slice`].
370 pub(crate) async_slice_size: i64,
371
372 /// v1.1 B10 Stage 1 — set by the dispatcher when an async-mode
373 /// budget exhaustion fires; checked by `exec_with` (so the
374 /// sentinel propagates without `unwind` running, mirroring
375 /// `yielding.is_some()`) and by `call_value_impl` (so the call
376 /// frames survive for the next poll). Cleared by `drive_one`
377 /// after translating it to `DispatchOutcome::BudgetExhausted`.
378 pub(crate) host_yield_pending: bool,
379
380 /// v1.1 B10 Stage 2 — set by the dispatcher's native-call path
381 /// when an async-marked [`NativeClosure`] is invoked under
382 /// `async_mode`. The Vm pauses the dispatcher (same sentinel-Err
383 /// mechanism as `host_yield_pending` — see `exec_with` +
384 /// `call_value_impl`), stashes the in-flight future +
385 /// post-completion context here, and surfaces them to
386 /// `EvalFuture::poll` via `drive_one`. Cleared by `drive_one`
387 /// once the future is moved out into a
388 /// `DispatchOutcome::AsyncNativeAwaiting`.
389 pub(crate) pending_async_native_fut:
390 Option<std::pin::Pin<Box<dyn std::future::Future<Output = Result<u32, LuaError>>>>>,
391
392 /// v1.1 B10 Stage 2 — companion to `pending_async_native_fut`:
393 /// the `(func_slot, nargs, nresults, gc_top)` quad needed to
394 /// commit the future's eventual `Ok(nret)` back into the calling
395 /// frame's expected result slots. Recorded by the dispatcher;
396 /// consumed by [`Vm::commit_async_native_result`] after the
397 /// future resolves.
398 pub(crate) pending_async_native_ctx: Option<AsyncNativeCallCtx>,
399}
400
401/// v1.1 B10 Stage 2 — call-site context an in-flight async native
402/// needs preserved across the cooperative-yield boundary.
403///
404/// The dispatcher records this when it routes a `NativeClosure` with
405/// `is_async == true` through the cooperative path; `EvalFuture::poll`
406/// hands it back to [`Vm::commit_async_native_result`] once the
407/// awaited future resolves so `finish_results` (and the post-call GC
408/// checkpoint) can run as if the native had completed synchronously.
409#[derive(Clone, Copy)]
410pub(crate) struct AsyncNativeCallCtx {
411 pub func_slot: u32,
412 /// Recorded for parity with the sync native-call path's
413 /// `native_nresults`/`gc_top` bookkeeping; reserved for Stage 3+
414 /// hook firing + traceback shaping. Not yet read in Stage 2.
415 #[allow(dead_code)]
416 pub nargs: u32,
417 pub nresults: i32,
418 /// Recorded for Stage 3+ traceback + GC-root-window auditing.
419 /// Stage 2 reads `Vm.gc_top` directly post-resume, so this is
420 /// unread today; carried so an Stage 3 audit can confirm the
421 /// pre-suspend root window matches the post-resume one.
422 #[allow(dead_code)]
423 pub gc_top: u32,
424}
425
426/// Per-thread debug hook state (PUC `lua_State` hook/hookmask/basehookcount/
427/// hookcount). `func` is the Lua hook; the booleans are the PUC mask bits.
428#[derive(Clone, Copy, Default)]
429pub struct HookState {
430 /// the hook function (`None` when no hook is installed)
431 pub func: Option<Value>,
432 /// v1.1 B11 — Rust-side debug hook. Fires alongside the Lua hook
433 /// (Rust first); both can be installed simultaneously, but most
434 /// embedders pick one.
435 pub rust_func: Option<RustDebugHook>,
436 /// LUA_MASKCALL — fire on function entry
437 pub call: bool,
438 /// LUA_MASKRET — fire on function return
439 pub ret: bool,
440 /// LUA_MASKLINE — fire on source-line change
441 pub line: bool,
442 /// LUA_MASKCOUNT — fire every `count_base` instructions
443 pub count: bool,
444 /// instruction count between count events (PUC basehookcount)
445 pub count_base: i64,
446 /// instructions left until the next count event (PUC hookcount)
447 pub count_left: i64,
448}
449
450/// Rust-side debug hook callback (B11). Receives the `Vm` plus a
451/// classified event. The callback runs synchronously in the
452/// dispatcher; the hook flag (`in_hook`) is set for its duration so
453/// hook recursion is suppressed.
454pub type RustDebugHook = fn(&mut Vm, RustHookEvent);
455
456/// Classified debug event delivered to a [`RustDebugHook`].
457#[derive(Clone, Copy, Debug, PartialEq, Eq)]
458pub enum RustHookEvent {
459 /// Function entry (`hook_call` analogue).
460 Call,
461 /// Function return (`hook_return` analogue).
462 Return,
463 /// Tail call entry (PUC 5.2+ separates this from a plain Call).
464 TailCall,
465 /// Source-line change (the `u32` is the 1-based line number).
466 Line(u32),
467 /// Instruction count event (fires every `count_base` instructions).
468 Count,
469}
470
471/// Mask flags for [`Vm::set_rust_debug_hook`]. OR these to subscribe
472/// to multiple event categories with a single hook installation.
473pub const HOOK_MASK_CALL: u32 = 1;
474/// Subscribe to function-return events.
475pub const HOOK_MASK_RETURN: u32 = 2;
476/// Subscribe to line-change events.
477pub const HOOK_MASK_LINE: u32 = 4;
478/// Subscribe to instruction-count events.
479pub const HOOK_MASK_COUNT: u32 = 8;
480
481/// A thread's swapped-out execution context (PUC per-thread stack state).
482struct SavedCtx {
483 stack: Vec<Value>,
484 frames: Vec<CallFrame>,
485 open_upvals: Vec<(u32, Gc<Upvalue>)>,
486 tbc: Vec<u32>,
487 top: u32,
488 pcall_depth: u32,
489 hook: HookState,
490 /// PUC `L->l_gt` — the thread's own globals table. Carried alongside
491 /// the rest of the suspended state so each thread can keep its own
492 /// `setfenv(0, env)` rewire without the swap leaking into another
493 /// thread (5.1 closure.lua :177).
494 globals: Gc<Table>,
495}
496
497/// Outcome of unwinding the call stack on an error (see `Vm::unwind`).
498enum Unwound {
499 /// caught by a pcall/xpcall continuation; resume running its caller
500 Caught,
501 /// caught by a continuation that was the entry-level activation; these are
502 /// the call's (wrapped) results
503 CaughtReturn(Vec<Value>),
504 /// no protecting continuation up to `entry_depth`; propagate the error
505 Propagated(LuaError),
506}
507
508/// A resolved debug stack level: a real Lua frame (by index into `frames`) or a
509/// synthetic C frame for a call_value boundary.
510pub(crate) enum DbgKind {
511 Lua(usize),
512 /// a synthetic C level; the index is the `from_c` Lua frame it sits below,
513 /// used to name the native via its invoking call instruction.
514 C(usize),
515 /// PUC `CIST_TAIL` placeholder — a Lua-to-Lua tail call collapsed the
516 /// caller's activation, so `debug.getinfo(level)` at this slot returns
517 /// `what = "tail"` / `short_src = "(tail call)"` / `linedefined = -1` /
518 /// `func = nil` and `getfenv(level)` errors (5.1 db.lua :336/:341 pin
519 /// both shapes). The index points at the *tail-called* frame whose
520 /// `is_tail` flag induced this synthetic level.
521 Tail(#[allow(dead_code)] usize),
522}
523
524/// Outcome of an index/newindex/comparison fast path: either a directly
525/// computed result, or a metamethod (with the receiver it resolved against) the
526/// caller must invoke — synchronously (C context) or yieldably (VM opcode).
527enum MmOut {
528 /// index → the looked-up value; newindex → done (raw set performed);
529 /// comparison → the boolean result already known
530 Done(Value),
531 /// a metamethod to call; `recv` is the chain element it was found on (the
532 /// extra args — key / value — are supplied by the caller)
533 Mm { func: Value, recv: Value },
534 /// ≤5.3 `a <= b` synthesised via `not __lt(b, a)` when neither operand
535 /// carries `__le` — `op_compare` swaps the args and negates the result.
536 /// Lives separate from `Mm` so the synth path can stay yieldable without
537 /// every other Mm caller learning a swap flag they would never set.
538 CompareSynth { func: Value },
539}
540
541/// Metamethod events; discriminants index `Vm::mm_names`.
542#[derive(Clone, Copy, PartialEq, Eq)]
543#[repr(usize)]
544pub(crate) enum Mm {
545 Index,
546 NewIndex,
547 Call,
548 ToString,
549 Metatable,
550 Name,
551 Eq,
552 Lt,
553 Le,
554 Concat,
555 Len,
556 Add,
557 Sub,
558 Mul,
559 Div,
560 Mod,
561 Pow,
562 IDiv,
563 BAnd,
564 BOr,
565 BXor,
566 Shl,
567 Shr,
568 Unm,
569 BNot,
570 Close,
571 Gc,
572 Pairs,
573}
574
575const MM_NAMES: [&str; 28] = [
576 "__index",
577 "__newindex",
578 "__call",
579 "__tostring",
580 "__metatable",
581 "__name",
582 "__eq",
583 "__lt",
584 "__le",
585 "__concat",
586 "__len",
587 "__add",
588 "__sub",
589 "__mul",
590 "__div",
591 "__mod",
592 "__pow",
593 "__idiv",
594 "__band",
595 "__bor",
596 "__bxor",
597 "__shl",
598 "__shr",
599 "__unm",
600 "__bnot",
601 "__close",
602 "__gc",
603 "__pairs",
604];
605
606/// Debug-name spelling for a metamethod event tag (the bare `"index"` /
607/// `"gc"` / … stored in `Frame.tm`), as `getinfo("n").name` reports it.
608///
609/// PUC 5.2/5.3 keep the leading `"__"` for every event; 5.4+ strips it for
610/// every event *except* `__gc` (`funcnamefromcall` returns the literal
611/// `"__gc"` string for `CIST_FIN`, whereas `funcnamefromcode` does
612/// `getstr(tmname[tm]) + 2` to skip the `__`).
613fn tm_debug_name(version: LuaVersion, tm: &str) -> String {
614 if version <= LuaVersion::Lua53 {
615 format!("__{tm}")
616 } else if tm == "gc" {
617 "__gc".to_string()
618 } else {
619 tm.to_string()
620 }
621}
622
623/// The metamethod event an opcode dispatches, without the `__` prefix (PUC
624/// funcnamefromcode), for "(metamethod 'event')" call-error suffixes.
625fn mm_event_name(op: crate::vm::isa::Op) -> Option<&'static str> {
626 use crate::vm::isa::Op;
627 Some(match op {
628 Op::Add => "add",
629 Op::Sub => "sub",
630 Op::Mul => "mul",
631 Op::Div => "div",
632 Op::Mod => "mod",
633 Op::Pow => "pow",
634 Op::IDiv => "idiv",
635 Op::BAnd => "band",
636 Op::BOr => "bor",
637 Op::BXor => "bxor",
638 Op::Shl => "shl",
639 Op::Shr => "shr",
640 Op::Unm => "unm",
641 Op::BNot => "bnot",
642 Op::Concat => "concat",
643 Op::Len => "len",
644 Op::GetField | Op::GetTable | Op::GetI | Op::SelfOp => "index",
645 Op::SetField | Op::SetTable | Op::SetI => "newindex",
646 Op::Eq | Op::EqK => "eq",
647 Op::Lt => "lt",
648 Op::Le => "le",
649 _ => return None,
650 })
651}
652
653/// PUC MAXTAGLOOP: bound on `__index`/`__newindex` chains.
654const MAX_TAG_LOOP: u32 = 2000;
655/// PUC `MAXCCMT`: bound on a `__call` metamethod chain (lvm.c). 200 chains
656/// is more than any reasonable program needs and matches PUC 5.4/5.5; the
657/// earlier `15` here was tight enough to fire on calls.lua :194 (N=20).
658const MAX_CCMT: u32 = 200;
659/// PUC LUAI_MAXCCALLS analogue: native↔Lua nesting bound.
660const MAX_C_DEPTH: u32 = 200;
661/// luna's engine-level VM stack cap (used by call-site overflow checks).
662/// Slightly larger than PUC's `LUAI_MAXSTACK` so engine internals have a
663/// little headroom above any single library push.
664const MAX_LUA_STACK: u32 = 1 << 20;
665/// PUC `LUAI_MAXSTACK` (`luaconf.h`): the cap library code consults via
666/// `lua_checkstack` to refuse multi-value pushes (`table.unpack` returning
667/// N values, `string.pack` results, etc.). 5.3 coroutine.lua :530 pins
668/// this at one million — `for j in {lim-10, …}` expects every j ≥ lim-10
669/// to fail because the few slots already consumed in the coroutine push
670/// the effective cap below lim-10.
671const PUC_MAXSTACK: i64 = 1_000_000;
672
673/// PUC 5.4+ default warnf state. The base library's `warn` function flips
674/// between `Off` and `On` via the `@on` / `@off` control messages; any other
675/// `@<word>` control is silently ignored, mirroring `lauxlib.c::checkcontrol`.
676#[derive(Clone, Copy, PartialEq, Eq, Debug)]
677pub enum WarnState {
678 /// `warn` calls are silently dropped (default after `warn("@off")`).
679 Off,
680 /// `warn` calls are delivered to stderr (after `warn("@on")`).
681 On,
682}
683
684/// Best-effort extraction of a textual message from a `catch_unwind` payload.
685/// `panic!("msg")` arrives as `String`, `panic!(static)` as `&str`; anything
686/// else degrades to `"<non-string panic>"`. Used by the native-call
687/// catch_unwind to fold the panic into a Lua error.
688fn panic_payload_str(payload: &Box<dyn std::any::Any + Send>) -> String {
689 if let Some(s) = payload.downcast_ref::<String>() {
690 return s.clone();
691 }
692 if let Some(s) = payload.downcast_ref::<&'static str>() {
693 return (*s).to_string();
694 }
695 "<non-string panic>".to_string()
696}
697
698/// Combined error type returned by [`Vm::eval`] and friends — either the
699/// chunk failed to parse / compile, or it raised at runtime.
700#[derive(Debug)]
701pub enum Error {
702 /// Parse or compile failure.
703 Syntax(SyntaxError),
704 /// Runtime error raised during execution.
705 Runtime(LuaError),
706}
707
708impl From<SyntaxError> for Error {
709 fn from(e: SyntaxError) -> Error {
710 Error::Syntax(e)
711 }
712}
713
714impl From<LuaError> for Error {
715 fn from(e: LuaError) -> Error {
716 Error::Runtime(e)
717 }
718}
719
720impl Drop for Vm {
721 fn drop(&mut self) {
722 // state close: run `__gc` for every still-registered finalizable before
723 // the heap frees them (PUC separatetobefnz(g,1) + callallpending). A
724 // single pass — objects created by a closing finalizer are not
725 // re-finalized (they go to the heap's free list directly).
726 self.heap.queue_all_finalizers();
727 self.run_finalizers();
728 }
729}
730
731// P17-D Week 1 scaffold — split-borrow free fn helpers for frames
732// push/pop with shadow counter `frames_top: u32`. Free fns (not Vm
733// methods) so callers can pass `&mut self.frames` + `&mut self.frames_top`
734// as split borrows, allowing other `&mut self.field` reads inside the
735// CallFrame construction (e.g. `std::mem::take(&mut self.pending_tm)`).
736//
737// Week 1 has NO readers yet; the shadow just stays in sync + asserts.
738// Week 2 begins migrating hot-path readers (materialize_frames helper)
739// to consume `frames_top` and a flat array in place of the Vec.
740#[inline(always)]
741fn frames_push_sync(frames: &mut Vec<CallFrame>, frames_top: &mut u32, cf: CallFrame) {
742 frames.push(cf);
743 // Shadow maintenance is debug-only: release builds skip the
744 // increment + assertion entirely. The shadow's purpose in Week 1
745 // is to VERIFY the assumed invariant (frames_top == frames.len())
746 // across all push/pop sites; once Week 2+ migrates readers to
747 // consume the shadow, release will run the increment unconditionally.
748 #[cfg(debug_assertions)]
749 {
750 *frames_top += 1;
751 debug_assert_eq!(
752 *frames_top as usize,
753 frames.len(),
754 "P17-D frames_top out of sync after push",
755 );
756 }
757 #[cfg(not(debug_assertions))]
758 let _ = frames_top;
759}
760
761#[inline(always)]
762fn frames_pop_sync(frames: &mut Vec<CallFrame>, frames_top: &mut u32) -> Option<CallFrame> {
763 let r = frames.pop();
764 #[cfg(debug_assertions)]
765 {
766 if r.is_some() {
767 *frames_top = frames_top.saturating_sub(1);
768 }
769 debug_assert_eq!(
770 *frames_top as usize,
771 frames.len(),
772 "P17-D frames_top out of sync after pop",
773 );
774 }
775 #[cfg(not(debug_assertions))]
776 let _ = frames_top;
777 r
778}
779
780/// v1.3 Phase AOT Stage 7 sub-piece 4 — one-time env-var read for
781/// `LUNA_AOT_PROBE`. Returns `true` iff the env var is set to any
782/// non-empty value. The result is cached in a `OnceLock` so the
783/// dispatcher's hot path pays a single atomic load per process. Off
784/// by default — production deploys don't bleed diagnostic prints.
785fn jit_probe_enabled() -> bool {
786 static PROBE_ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
787 *PROBE_ON.get_or_init(|| {
788 std::env::var("LUNA_AOT_PROBE")
789 .ok()
790 .filter(|v| !v.is_empty())
791 .is_some()
792 })
793}
794
795impl Vm {
796 /// P17-D Week 1 — re-sync `frames_top` after a bulk `frames: Vec`
797 /// swap (take_ctx, put_ctx, load_coro_ctx). Must be called after
798 /// the Vec replacement to keep the shadow valid.
799 #[inline(always)]
800 fn frames_resync(&mut self) {
801 // Debug-only Week 1 — see `frames_push_sync` comment.
802 #[cfg(debug_assertions)]
803 {
804 self.frames_top = self.frames.len() as u32;
805 }
806 }
807
808 // ====================================================================
809 // P17-D v2 Phase 2 — stack-inline frame metadata accessors (unused).
810 //
811 // These methods read/write the LJ_FR2 marker slots at `stack[base-2]`
812 // (closure GCRef) and `stack[base-1]` (FrameMarker as i64). Phase 2
813 // ships them WITHOUT call-site usage; Phase 3 migrates push/pop
814 // sites to consume them. Phase 4 removes Vec<CallFrame>.
815 //
816 // Preconditions (debug-asserted):
817 // - base >= 2 (slots base-2 and base-1 must exist below the frame)
818 // - self.stack.len() > base + max_stack (caller has grown stack)
819 // - For Lua frames, stack[base-2] holds Value::Closure(cl)
820 // - For Lua frames, stack[base-1] holds Value::Int(marker.to_raw())
821 //
822 // No release-build cost when unused (LTO strips dead methods).
823 // ====================================================================
824
825 /// Write a Lua frame's closure pointer into `stack[base-2]`.
826 /// The caller must ensure `base >= 2` and the slot is within the
827 /// stack's allocated range.
828 #[inline]
829 #[allow(dead_code)] // Phase 2 — consumer is Phase 3.
830 fn write_frame_closure(&mut self, base: u32, cl: crate::runtime::Gc<LuaClosure>) {
831 debug_assert!(
832 base >= 2,
833 "frame closure slot needs base >= 2; got {}",
834 base
835 );
836 let idx = (base - 2) as usize;
837 debug_assert!(idx < self.stack.len(), "stack[base-2] out of range");
838 self.stack[idx] = Value::Closure(cl);
839 }
840
841 /// Read a Lua frame's closure pointer from `stack[base-2]`.
842 /// Returns `None` if the slot doesn't hold a closure (caller is
843 /// expected to treat that as a corrupt frame).
844 ///
845 /// P17-D v2 Direction E2 — uses E1's [`Value::tag_byte`] fast-path
846 /// to avoid the enum-match cost on the hot path. Tag check via
847 /// 1-byte load + branch + `as_closure_unchecked` payload load.
848 #[inline]
849 #[allow(dead_code)]
850 fn read_frame_closure(&self, base: u32) -> Option<crate::runtime::Gc<LuaClosure>> {
851 debug_assert!(base >= 2);
852 let v = self.stack.get((base - 2) as usize)?;
853 if v.tag_byte() == crate::runtime::value::tag::CLOSURE {
854 // SAFETY: tag byte just verified == CLOSURE.
855 Some(unsafe { v.as_closure_unchecked() })
856 } else {
857 None
858 }
859 }
860
861 /// Write a packed [`FrameMarker`] into `stack[base-1]`. The marker
862 /// encodes the frame kind (Lua / Cont) + PC-or-delta payload.
863 /// Stored as `Value::Int(marker.to_raw())` so it round-trips
864 /// cleanly through the value stack without losing bits.
865 #[inline]
866 #[allow(dead_code)]
867 fn write_frame_marker(&mut self, base: u32, marker: crate::runtime::frame_marker::FrameMarker) {
868 debug_assert!(base >= 1, "frame marker slot needs base >= 1; got {}", base);
869 let idx = (base - 1) as usize;
870 debug_assert!(idx < self.stack.len(), "stack[base-1] out of range");
871 self.stack[idx] = Value::Int(marker.to_raw());
872 }
873
874 /// Read a packed [`FrameMarker`] from `stack[base-1]`. Returns
875 /// `None` if the slot isn't a `Value::Int` (caller treats as a
876 /// corrupt frame); the kind tag itself may still be invalid, in
877 /// which case [`FrameMarker::kind`] returns `None` on the result.
878 ///
879 /// P17-D v2 Direction E2 — uses E1's [`Value::tag_byte`] fast-path
880 /// for the tag check + `as_int_unchecked` for the payload load.
881 #[inline]
882 #[allow(dead_code)]
883 fn read_frame_marker(&self, base: u32) -> Option<crate::runtime::frame_marker::FrameMarker> {
884 debug_assert!(base >= 1);
885 let v = self.stack.get((base - 1) as usize)?;
886 if v.tag_byte() == crate::runtime::value::tag::INT {
887 // SAFETY: tag byte just verified == INT.
888 Some(crate::runtime::frame_marker::FrameMarker::from_raw(
889 unsafe { v.as_int_unchecked() },
890 ))
891 } else {
892 None
893 }
894 }
895
896 /// Build the raw `Vm` struct without main coroutine / RNG seed / library
897 /// setup. Private helper shared by `Vm::new` and `Vm::new_minimal`; the
898 /// caller is responsible for the rest of the bring-up.
899 fn new_inner(version: LuaVersion) -> Vm {
900 let mut heap = Heap::new();
901 // PUC 5.1 had no ephemeron pass — `__mode='k'` tables marked their
902 // values strongly. gc.lua's "weak tables" section relies on that.
903 heap.no_ephemeron = version <= LuaVersion::Lua51;
904 // PUC 5.3 needs two GC cycles to finalize a table caught in a
905 // coroutine reference cycle (gc.lua :502); 5.4+ rewrote the GC and
906 // finalize in a single cycle (5.4/5.5 gc.lua :544 assert exactly one).
907 heap.defer_thread_cycle_finalize = version == LuaVersion::Lua53;
908 let globals = heap.new_table();
909 let mm_names = MM_NAMES.iter().map(|n| heap.intern(n.as_bytes())).collect();
910
911 Vm {
912 heap,
913 stack: Vec::new(),
914 frames: Vec::new(),
915 frames_top: 0,
916 open_upvals: Vec::new(),
917 tbc: Vec::new(),
918 top: 0,
919 globals,
920 type_mt: [None; 5],
921 mm_names,
922 c_depth: 0,
923 pcall_depth: 0,
924 nny: 0,
925 msgh_depth: 0,
926 terminating: None,
927 rng: [0; 4],
928 started: std::time::Instant::now(),
929 version,
930 closing_err: None,
931 current: None,
932 main_ctx: None,
933 yielding: None,
934 native_nresults: -1,
935 main_coro: None,
936 // PUC 5.4+ boots in GENERATIONAL mode (the first
937 // `collectgarbage("generational")` reports "generational"
938 // as the previous mode — v2.14 dialect fixture 5.4/549;
939 // 5.5 behaves the same, probed against lua5.5). luna's
940 // collector is a single incremental engine either way;
941 // this field is the MODE REPORT the stdlib exposes.
942 gc_mode: if version >= crate::version::LuaVersion::Lua54 {
943 "generational"
944 } else {
945 "incremental"
946 },
947 gc_top: 0,
948 gc_pause: 200,
949 gc_stepmul: 100,
950 gc_stepsize: 13,
951 gc_finalizing: false,
952 capi_stack: Vec::new(),
953 capi_cstr_pin: None,
954 warn_state: WarnState::Off,
955 warn_buf: Vec::new(),
956 warn_log: Vec::new(),
957 instr_budget: None,
958 bytecode_loading: true,
959 puc_bytecode_loading: false,
960 loader_input_budget: Vm::DEFAULT_LOADER_INPUT_BUDGET,
961 registry: None,
962 file_mt: None,
963 io_input: None,
964 io_output: None,
965 hook: HookState::default(),
966 in_hook: false,
967 pending_tailcalls: 0,
968 errored_native: None,
969 hook_ftransfer: 0,
970 hook_ntransfer: 0,
971 pending_tm: None,
972 pending_is_hook: false,
973 error_traceback: None,
974 public_call_depth: 0,
975 running_natives: Vec::new(),
976 running_native_slots: Vec::new(),
977 // v1.1 A2 — JIT-specific state factored into `JitState`
978 // sidecar. The `luna` crate's `Vm::new_minimal_with_jit` /
979 // `install_jit_backend` / `luaL_newstate` swap in
980 // `CraneliftBackend` for callers that want JIT acceleration.
981 jit: crate::vm::jit_state::JitState::with_null_backend(),
982 // v1.1 B12 — host roots ticket pool for the `Lua` facade.
983 host_roots: Vec::new(),
984 // v1.3 Phase ML — MacroLua registry. Pre-populated with
985 // built-ins (`@quote` / `@unquote` / `@if` / `@gensym`)
986 // when this Vm is constructed under `LuaVersion::MacroLua`.
987 macro_registry: if version == LuaVersion::MacroLua {
988 crate::frontend::macro_expander::MacroRegistry::with_builtins()
989 } else {
990 crate::frontend::macro_expander::MacroRegistry::new()
991 },
992 host_roots_free: Vec::new(),
993 sort_scratch: Vec::new(),
994 // v1.2 Track B — LuaUserdata trait sugar's per-Vm
995 // metatable cache. Populated lazily by register_userdata.
996 userdata_metatables: std::collections::HashMap::new(),
997 // v1.1 B6 — error classification metadata. Defaults to
998 // Runtime; set at known sites (syntax / budget trip /
999 // native error / type error).
1000 last_error_kind: crate::vm::error::LuaErrorKind::default(),
1001 last_error_source: None,
1002 // v1.1 B10 Stage 1 — async embedder fields. Defaults
1003 // preserve sync behavior bit-for-bit (`async_mode = false`
1004 // means the budget hot loop errors out exactly as v1.0).
1005 async_mode: false,
1006 async_waker: None,
1007 async_slice_size: 10_000,
1008 host_yield_pending: false,
1009 // v1.1 B10 Stage 2 — pending async-native state. Empty by
1010 // default; populated only by the dispatcher when an
1011 // async-marked NativeClosure is invoked under async_mode.
1012 pending_async_native_fut: None,
1013 pending_async_native_ctx: None,
1014 }
1015 }
1016
1017 /// Build a fully-loaded Vm — the default for embedders that want PUC's
1018 /// standard library surface. Equivalent to `Vm::new_minimal(version)`
1019 /// followed by `vm.open_all_libs()`.
1020 pub fn new(version: LuaVersion) -> Vm {
1021 let mut vm = Vm::new_minimal(version);
1022 vm.open_all_libs();
1023 vm
1024 }
1025
1026 /// P09 embedding: build a Vm with no standard libraries loaded. Embedders
1027 /// that want a sandbox (Redis-style scripts, in-game scripting with
1028 /// a curated API) call this and then `open_base` / `open_math` / etc.
1029 /// selectively. The Vm is otherwise fully initialized (main coroutine,
1030 /// RNG seed, GC) so `eval` and `call_value` are immediately usable.
1031 pub fn new_minimal(version: LuaVersion) -> Vm {
1032 let mut vm = Vm::new_inner(version);
1033 let mc = vm.heap.new_coro(Value::Nil, vm.globals);
1034 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
1035 unsafe { mc.as_mut() }.status = CoroStatus::Running;
1036 vm.main_coro = Some(mc);
1037 let (a, b) = vm.rng_auto_seed();
1038 vm.rng_seed(a as u64, b as u64);
1039 vm
1040 }
1041
1042 /// v1.1 A1 Session C — install a caller-supplied JIT backend. The
1043 /// `luna` crate uses this to swap in its `CraneliftBackend`; tests
1044 /// or third-party backends pass their own [`crate::jit::IntChunkCompiler`] /
1045 /// [`crate::jit::TraceCompiler`] implementations. Re-installing on a Vm whose
1046 /// closures already populated `Proto.jit: JitProtoState::Compiled`
1047 /// does NOT evict those cached entries — call right after
1048 /// construction for a clean swap.
1049 ///
1050 /// Naming: `install_jit_backend` (not `install_default_jit`)
1051 /// because the "default" in luna-core is `NullJitBackend`; the
1052 /// "default JIT" lives in the `luna` crate.
1053 pub fn install_jit_backend<C, T>(&mut self, chunk: C, trace: T)
1054 where
1055 C: crate::jit::IntChunkCompiler + 'static,
1056 T: crate::jit::TraceCompiler + 'static,
1057 {
1058 self.jit.chunk_compiler = Box::new(chunk);
1059 self.jit.trace_compiler = Box::new(trace);
1060 }
1061
1062 /// v2.0 Track J sub-step J-B — install a caller-supplied JIT
1063 /// storage holder. Default is [`crate::jit::NullJitStorage`];
1064 /// the `luna_jit` crate's `install_default_jit` pairs this with
1065 /// `install_jit_backend(CraneliftBackend, CraneliftBackend)` to
1066 /// also install a fresh `CraneliftJitStorage`. Storage holds
1067 /// the per-`Vm` JIT cache + handle collections that used to be
1068 /// `thread_local!`s in `luna_jit::jit_backend`.
1069 ///
1070 /// Idempotency: re-installing storage on a Vm that already
1071 /// holds compiled-trace pointers WILL evict their owners (the
1072 /// old `CraneliftJitStorage`'s `JITModule`s drop their mmap
1073 /// pages). Call right after construction for a clean swap.
1074 pub fn install_jit_storage<S>(&mut self, storage: S)
1075 where
1076 S: crate::jit::JitStorage + 'static,
1077 {
1078 self.jit.storage = Box::new(storage);
1079 }
1080
1081 /// v1.1 A1 Session A — install the no-op JIT backend. `try_compile`
1082 /// reports "skipped" so every closure stays on the interpreter
1083 /// path, and the trace recorder's compile attempt always returns
1084 /// `None`. Intended for tests that want to verify the trait
1085 /// boundary works in a JIT-free configuration, and for the future
1086 /// `luna-core` build path that ships without Cranelift.
1087 ///
1088 /// Calling this on a Vm whose closures already populated
1089 /// `Proto.jit: JitProtoState::Compiled` does NOT evict those
1090 /// cached entries — the dispatcher will still call into them. For
1091 /// a truly JIT-free run, call this immediately after construction.
1092 pub fn install_null_jit(&mut self) {
1093 self.jit.chunk_compiler = Box::new(crate::jit::NullJitBackend);
1094 self.jit.trace_compiler = Box::new(crate::jit::NullJitBackend);
1095 }
1096
1097 /// Open the entire 5.5 standard library on a `new_minimal`-built Vm.
1098 /// `Vm::new` calls this; sandboxed embedders open libraries one at a
1099 /// time instead (`open_base`, `open_math`, `open_table`, …).
1100 pub fn open_all_libs(&mut self) {
1101 self.open_base();
1102 self.open_math();
1103 self.open_table();
1104 self.open_string();
1105 self.open_utf8();
1106 self.open_os_io();
1107 self.open_debug();
1108 self.open_coroutine();
1109 self.open_package();
1110 // PUC 5.2 introduced `bit32`; 5.3 retired it in the manual BUT
1111 // the stock 5.3 build ships -DLUA_COMPAT_5_2, which keeps the
1112 // library loaded. The diff ground truth is the default build
1113 // (v2.14 dialect fixture 5.3/535), so expose it under 5.2 AND
1114 // 5.3; 5.4 dropped the compat default for real.
1115 if matches!(self.version, LuaVersion::Lua52 | LuaVersion::Lua53) {
1116 self.open_bit32();
1117 }
1118 }
1119
1120 /// Install the base library (`print`, `type`, `pairs`, `tostring`,
1121 /// `pcall`, `error`, `assert`, `select`, `setmetatable`, `getmetatable`,
1122 /// `rawequal`, `rawget`, `rawset`, `rawlen`, `next`, `tonumber`,
1123 /// `collectgarbage`, `warn` on 5.4+, `_VERSION`, `_G`, plus 5.1's
1124 /// retired globals `unpack`, `loadstring`, `setfenv`, `getfenv`,
1125 /// `newproxy`, `gcinfo` when version == 5.1). Safe to call at most
1126 /// once per Vm.
1127 pub fn open_base(&mut self) {
1128 crate::vm::builtins::open_base(self);
1129 }
1130 /// Install the `math` standard library.
1131 pub fn open_math(&mut self) {
1132 crate::vm::lib_math::open_math(self);
1133 }
1134 /// Install the `table` standard library.
1135 pub fn open_table(&mut self) {
1136 crate::vm::lib_table::open_table(self);
1137 }
1138 /// Install the `string` standard library (and the shared string metatable).
1139 pub fn open_string(&mut self) {
1140 crate::vm::lib_string::open_string(self);
1141 }
1142 /// Install the `utf8` standard library (5.3+).
1143 pub fn open_utf8(&mut self) {
1144 crate::vm::lib_utf8::open_utf8(self);
1145 }
1146 /// `os` and `io` are merged because file userdata shares state with both
1147 /// (`io.tmpname` and `os.tmpname` are the same function, `io.popen`
1148 /// wraps `os.execute`'s shell).
1149 pub fn open_os_io(&mut self) {
1150 crate::vm::lib_os_io::open_os_io(self);
1151 }
1152 /// Install the `debug` standard library (introspection / hooks). Off by
1153 /// default for sandbox embedders.
1154 pub fn open_debug(&mut self) {
1155 crate::vm::lib_debug::open_debug(self);
1156 }
1157 /// Install the `coroutine` standard library.
1158 pub fn open_coroutine(&mut self) {
1159 crate::vm::lib_coroutine::open_coroutine(self);
1160 }
1161 /// `package` plus the 5.1-only `module` and `package.seeall` aliases.
1162 pub fn open_package(&mut self) {
1163 crate::vm::lib_os_io::open_package(self);
1164 }
1165 /// 5.2-only `bit32` library (5.3+ retired in favour of native bitwise
1166 /// ops on 64-bit integers).
1167 pub fn open_bit32(&mut self) {
1168 crate::vm::lib_bit32::open_bit32(self);
1169 }
1170
1171 /// xoshiro256** next.
1172 pub(crate) fn rng_next(&mut self) -> u64 {
1173 let s = &mut self.rng;
1174 let result = s[1].wrapping_mul(5).rotate_left(7).wrapping_mul(9);
1175 let t = s[1] << 17;
1176 s[2] ^= s[0];
1177 s[3] ^= s[1];
1178 s[1] ^= s[2];
1179 s[0] ^= s[3];
1180 s[2] ^= t;
1181 s[3] = s[3].rotate_left(45);
1182 result
1183 }
1184
1185 /// Seed the RNG via splitmix64 expansion (PUC randseed shape).
1186 pub(crate) fn rng_seed(&mut self, a: u64, b: u64) {
1187 // PUC setseed: state = [n1, 0xff, n2, 0] (0xff avoids an all-zero
1188 // state), then 16 discards to spread the seed. Matches PUC's exact
1189 // sequence so the low-level conformance test passes.
1190 self.rng = [a, 0xff, b, 0];
1191 for _ in 0..16 {
1192 self.rng_next();
1193 }
1194 }
1195
1196 /// Wall-clock since VM creation (os.clock approximation).
1197 pub(crate) fn uptime(&self) -> std::time::Duration {
1198 self.started.elapsed()
1199 }
1200
1201 /// Entropy for math.randomseed() with no arguments.
1202 pub(crate) fn rng_auto_seed(&mut self) -> (i64, i64) {
1203 let t = std::time::SystemTime::now()
1204 .duration_since(std::time::UNIX_EPOCH)
1205 .map(|d| d.as_nanos() as u64)
1206 .unwrap_or(0);
1207 let addr = &self.rng as *const _ as u64;
1208 (t as i64, addr as i64)
1209 }
1210
1211 /// Allocate a native function object (no upvalues): builtin registration.
1212 pub fn native(&mut self, f: crate::runtime::value::NativeFn) -> Value {
1213 Value::Native(self.heap.new_native(f, Box::new([])))
1214 }
1215
1216 /// Allocate a native function object with captured upvalues.
1217 pub fn native_with(
1218 &mut self,
1219 f: crate::runtime::value::NativeFn,
1220 upvals: Box<[Value]>,
1221 ) -> Value {
1222 Value::Native(self.heap.new_native(f, upvals))
1223 }
1224
1225 /// Install the shared string metatable (string library, P04).
1226 pub fn set_string_metatable(&mut self, mt: Option<Gc<Table>>) {
1227 self.type_mt[3] = mt;
1228 }
1229
1230 /// The current globals table (`_G` / `_ENV` source for new chunks).
1231 pub fn globals(&self) -> Gc<Table> {
1232 self.globals
1233 }
1234
1235 /// Remaining VM stack slots (PUC `L->stack_last - L->top` analogue).
1236 /// Library code that pushes a known number of fresh slots — e.g.
1237 /// `table.unpack` returning N values — consults this to refuse when
1238 /// the push would blow past `LUAI_MAXSTACK`. 5.3 coroutine.lua :530's
1239 /// `for j in {lim-10, lim-5, …}` series pins this contract: the
1240 /// coroutine's already-built table eats a few slots, so an unpack of
1241 /// ~lim values can't fit.
1242 pub(crate) fn stack_room(&self) -> i64 {
1243 PUC_MAXSTACK - (self.stack.len() as i64)
1244 }
1245
1246 /// Repoint the thread's "global table" used by *future* `Vm::load` calls
1247 /// for the chunk's `_ENV` upvalue (PUC 5.1 `setfenv(0, env)` rewrites
1248 /// `L->l_gt`). Already-loaded chunks keep their own snapshot via the
1249 /// per-closure cell-0 clone in `Op::Closure`, so they are unaffected.
1250 pub(crate) fn set_globals(&mut self, env: Gc<Table>) {
1251 self.globals = env;
1252 }
1253
1254 /// The Lua dialect this VM was constructed for (5.1 / 5.2 / 5.3 / 5.4 /
1255 /// 5.5). Determines numeric semantics, available standard libraries, and
1256 /// metamethod behavior.
1257 pub fn version(&self) -> LuaVersion {
1258 self.version
1259 }
1260
1261 /// Set a global by name. `v` may be any `IntoValue`: a primitive
1262 /// (`i64`, `f64`, `bool`, `&str`, `String`, `Vec<u8>`), a `Value`
1263 /// directly, an `Option<T>`, or a `Gc<Table>` / `Gc<LuaClosure>` /
1264 /// `Gc<NativeClosure>` handle.
1265 ///
1266 /// Returns `Err(LuaError)` only if the globals table overflows
1267 /// (extremely unlikely in practice — `MAX_ASIZE = 1 << 27`).
1268 /// String interning + key construction cannot fail.
1269 ///
1270 /// ```
1271 /// # use luna_core::vm::Vm;
1272 /// # use luna_core::version::LuaVersion;
1273 /// let mut vm = Vm::sandbox(LuaVersion::Lua55).open_base().build();
1274 /// vm.set_global("answer", 42).unwrap();
1275 /// vm.set_global("ratio", 0.5_f64).unwrap();
1276 /// vm.set_global("hello", "world").unwrap();
1277 /// let r = vm.eval("return answer, ratio, hello").unwrap();
1278 /// assert_eq!(r.len(), 3);
1279 /// ```
1280 pub fn set_global<V: crate::vm::IntoValue>(
1281 &mut self,
1282 name: &str,
1283 v: V,
1284 ) -> Result<(), LuaError> {
1285 let v = v.into_value(self);
1286 let k = Value::Str(self.heap.intern(name.as_bytes()));
1287 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
1288 unsafe { self.globals.as_mut() }.set(&mut self.heap, k, v)?;
1289 self.heap
1290 .barrier_back(self.globals.as_ptr() as *mut crate::runtime::heap::GcHeader);
1291 Ok(())
1292 }
1293
1294 /// Backward write barrier shorthand for native lib code: demote `t` from
1295 /// BLACK back to gray so the next propagate step re-traces its fields.
1296 /// No-op outside Propagate (parent is never BLACK at mutation time).
1297 pub(crate) fn barrier_back_table(&mut self, t: Gc<Table>) {
1298 self.heap
1299 .barrier_back(t.as_ptr() as *mut crate::runtime::heap::GcHeader);
1300 }
1301
1302 /// Forward write barrier shorthand: a closed upvalue is a single-slot
1303 /// container — `barrier_forward` is cheaper than `barrier_back` here.
1304 /// No-op outside Propagate.
1305 pub(crate) fn barrier_forward_upvalue(&mut self, uv: Gc<Upvalue>, child: Value) {
1306 self.heap
1307 .barrier_forward(uv.as_ptr() as *mut crate::runtime::heap::GcHeader, child);
1308 }
1309
1310 /// v1.3 Phase ML — register a MacroLua macro under `name`. Inert
1311 /// under non-MacroLua dialects (the macro is stored but the load
1312 /// path only consults the registry when
1313 /// `self.version == LuaVersion::MacroLua`).
1314 ///
1315 /// `name` is stored without the leading `@` — source code writes
1316 /// `@double(x)` to invoke a macro registered as `"double"`.
1317 pub fn define_macro(&mut self, name: &str, m: Box<dyn crate::frontend::macro_expander::Macro>) {
1318 self.macro_registry.register(name, m);
1319 }
1320
1321 /// v1.3 Phase ML — drop all MacroLua macros (built-in + custom).
1322 /// Mostly useful for tests / dogfood resets.
1323 pub fn clear_macros(&mut self) {
1324 self.macro_registry.clear();
1325 }
1326
1327 /// Parse + compile a chunk and close it over the globals table.
1328 pub fn load(&mut self, src: &[u8], chunkname: &[u8]) -> Result<Gc<LuaClosure>, SyntaxError> {
1329 // Reject oversize input *before* handing the parser/lexer a
1330 // potentially multi-GB slice. The PUC-shaped `not enough memory`
1331 // message keeps `heavy.lua::loadrep` compatibility: that test
1332 // accepts either `string length overflow` or `not enough memory`
1333 // as the failure mode for a feeder loop that outruns the host
1334 // allocator. See `set_loader_input_budget`.
1335 if src.len() > self.loader_input_budget {
1336 return Err(SyntaxError {
1337 line: 0,
1338 msg: b"not enough memory".to_vec(),
1339 });
1340 }
1341 // a precompiled (binary) chunk is undumped; source is parsed + compiled
1342 let is_bytecode = crate::vm::dump::is_binary_chunk(src);
1343 if is_bytecode && !self.bytecode_loading {
1344 return Err(SyntaxError {
1345 line: 0,
1346 msg: b"attempt to load a binary chunk (bytecode loading disabled)".to_vec(),
1347 });
1348 }
1349 let proto = if is_bytecode {
1350 let allow_puc = self.puc_bytecode_loading;
1351 crate::vm::dump::undump(src, &mut self.heap, self.version, allow_puc).map_err(
1352 |msg| SyntaxError {
1353 line: 0,
1354 msg: msg.into_bytes(),
1355 },
1356 )?
1357 } else if self.version.is_macro_lua() {
1358 // v1.3 Phase ML — MacroLua dialect: drain the lexer into a
1359 // token vec, run the macro expander pre-pass against the
1360 // per-Vm registry, then hand the rewritten stream to
1361 // `parse_tokens`. The AST + compiler are dialect-agnostic
1362 // because by this point all `@`/quote tokens are gone.
1363 let mut lexer = crate::frontend::lexer::Lexer::new(src, self.version);
1364 let mut raw: Vec<crate::frontend::token::TokenInfo> = Vec::new();
1365 loop {
1366 let t = lexer.next_token()?;
1367 let eof = matches!(t.tok, crate::frontend::token::Token::Eof);
1368 raw.push(t);
1369 if eof {
1370 break;
1371 }
1372 }
1373 // Drop the trailing Eof — expander operates on the body and
1374 // `parse_tokens` reinserts Eof when it runs out of tokens.
1375 raw.pop();
1376 let expanded = self.macro_registry.expand(raw)?;
1377 let ast = crate::frontend::parse_tokens(expanded, src, self.version)?;
1378 compile_chunk(&ast, self.version, chunkname, &mut self.heap)?
1379 } else {
1380 let ast = parse(src, self.version)?;
1381 compile_chunk(&ast, self.version, chunkname, &mut self.heap)?
1382 };
1383 // PUC `lua_load` (lapi.c) only seeds the loaded closure's first
1384 // upvalue with the globals table when the closure has *exactly* one
1385 // upvalue — that's the main-chunk `_ENV` case. A dumped non-main
1386 // function with two-or-more upvalues keeps every cell at nil; the
1387 // host must use `debug.setupvalue` to wire them up. 5.2 calls.lua
1388 // :293's `assert(x() == nil)` pins this contract.
1389 let n = proto.upvals.len();
1390 let mut ups: Vec<Gc<Upvalue>> = Vec::with_capacity(n.max(1));
1391 if n == 0 {
1392 // synthetic main chunk has no declared upvalues, but the engine
1393 // still expects at least one cell so the host can probe via
1394 // `debug.upvalueid` etc. Match the historical luna shape.
1395 ups.push(
1396 self.heap
1397 .new_upvalue(UpvalState::Closed(Value::Table(self.globals))),
1398 );
1399 } else if n == 1 {
1400 ups.push(
1401 self.heap
1402 .new_upvalue(UpvalState::Closed(Value::Table(self.globals))),
1403 );
1404 } else {
1405 for _ in 0..n {
1406 ups.push(self.heap.new_upvalue(UpvalState::Closed(Value::Nil)));
1407 }
1408 }
1409 Ok(self.heap.new_closure(proto, ups.into_boxed_slice()))
1410 }
1411
1412 /// Compile and run `src` as an anonymous chunk; return its results.
1413 /// Source name in the traceback is `"=eval"`. Syntax errors are
1414 /// surfaced as `LuaError` carrying the formatted PUC-style message
1415 /// (interned through the heap so the error value composes with
1416 /// `pcall` / `error_text` like any runtime error).
1417 pub fn eval(&mut self, src: &str) -> Result<Vec<Value>, LuaError> {
1418 self.eval_chunk(src, "=eval")
1419 }
1420
1421 /// Render an error value for messages/tests. Non-string errors —
1422 /// `error({code=…})`, `error(42)`, etc. — collapse to a type tag
1423 /// (`"(error object is a table value)"`); embedders that need
1424 /// structured payloads should inspect `e.0` directly. Errors whose
1425 /// text starts with `"native panic:"` indicate a Rust panic
1426 /// crossed `catch_unwind` — the Vm may be inconsistent and should
1427 /// be dropped (do not reuse).
1428 pub fn error_text(&self, e: &LuaError) -> String {
1429 match e.0 {
1430 Value::Str(s) => String::from_utf8_lossy(s.as_bytes()).into_owned(),
1431 v => format!("(error object is a {} value)", v.type_name()),
1432 }
1433 }
1434
1435 /// Render an error value the way PUC's standalone `msghandler`
1436 /// does (lua.c): strings pass through, numbers stringify, and any
1437 /// other object is given a chance at its `__tostring` metamethod
1438 /// (the result must be a string) before collapsing to the
1439 /// `"(error object is a … value)"` tag. Needs `&mut self` because
1440 /// `__tostring` runs arbitrary Lua — `error_text` remains the
1441 /// non-executing variant (v2.14 CV.2, fixture 5.5/321).
1442 pub fn error_display(&mut self, e: &LuaError) -> String {
1443 match e.0 {
1444 Value::Str(s) => String::from_utf8_lossy(s.as_bytes()).into_owned(),
1445 v @ (Value::Int(_) | Value::Float(_)) => {
1446 String::from_utf8_lossy(&self.tostring_basic(v)).into_owned()
1447 }
1448 v => {
1449 let mm = self.get_mm(v, Mm::ToString);
1450 if !mm.is_nil()
1451 && let Ok(r) = self.call_value(mm, &[v])
1452 && let Some(Value::Str(s)) = r.first()
1453 {
1454 return String::from_utf8_lossy(s.as_bytes()).into_owned();
1455 }
1456 format!("(error object is a {} value)", v.type_name())
1457 }
1458 }
1459 }
1460
1461 /// Call any callable value from the host (or from natives like pcall).
1462 pub fn call_value(&mut self, f: Value, args: &[Value]) -> Result<Vec<Value>, LuaError> {
1463 // host-level entry (no enclosing exec): drop any error state from a
1464 // prior call that propagated uncaught (`error_traceback` would
1465 // otherwise leak into the next debug.traceback call).
1466 if self.public_call_depth == 0 {
1467 self.error_traceback = None;
1468 }
1469 self.public_call_depth += 1;
1470 // P11-S2 — JIT fast path. A host call with no args targeting a Lua
1471 // chunk whose body fits the S1 int-arith whitelist short-circuits
1472 // the whole interpreter dispatch and runs straight through the
1473 // mmap'd native code. The lookup is one Cell::get + one match —
1474 // the slow path (compile attempt on first reach) is paid once per
1475 // Proto.
1476 if args.is_empty()
1477 && let Value::Closure(cl) = f
1478 && let Some(vs) = self.try_jit_call(cl)
1479 {
1480 self.public_call_depth -= 1;
1481 return Ok(vs);
1482 }
1483 let r = self.call_value_impl(f, args, true);
1484 self.public_call_depth -= 1;
1485 r
1486 }
1487
1488 /// P11-S2 — peek/populate the Proto's JIT cache slot, returning
1489 /// `Some(values)` when the cached native fn is callable for a
1490 /// zero-arg call. (Non-zero-arg dispatch is handled by
1491 /// `try_jit_call_op` from inside `begin_call`.)
1492 fn try_jit_call(&mut self, cl: Gc<LuaClosure>) -> Option<Vec<Value>> {
1493 use crate::runtime::function::JitProtoState;
1494 if !self.jit.enabled {
1495 return None;
1496 }
1497 let proto = cl.proto;
1498 if let JitProtoState::Untried = proto.jit.get() {
1499 self.populate_jit_cache(proto);
1500 }
1501 match proto.jit.get() {
1502 JitProtoState::Compiled {
1503 entry,
1504 num_args: 0,
1505 returns_one,
1506 arg_float_mask: _,
1507 arg_table_mask: _,
1508 ret_is_float,
1509 ret_is_table,
1510 } => {
1511 // SAFETY: the source `*const u8` is a JIT-compiled function entry pointer produced by Cranelift with the target `fn`-pointer signature (IntChunkFn / IntFnN); the JitVmGuard above keeps the JIT_VM TLS slot live across the call.
1512 let f: crate::jit::IntChunkFn = unsafe { std::mem::transmute(entry) };
1513 // P11-S5c / S5d.J — install the active Vm + closure
1514 // for any Rust helper the JIT'd code may call (e.g.
1515 // `luna_jit_new_table`, `luna_jit_upval_get`) via
1516 // cranelift `Linkage::Import`. RAII clear on return.
1517 // Chunks with no upvalue reads don't touch the closure
1518 // slot, paying nothing.
1519 // v1.1 A1 Session A — route through chunk_compiler so
1520 // the NullJitBackend path stays inert. Raw-ptr arg
1521 // avoids the &mut self borrow conflict against the
1522 // shared self.jit.chunk_compiler read.
1523 let vm_ptr: *mut Vm = self;
1524 let _jit_vm_guard = self.jit.chunk_compiler.enter(vm_ptr, Some(cl));
1525 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
1526 let r = unsafe { f() };
1527 drop(_jit_vm_guard);
1528 // P11-S5d.E' — a JIT helper may have detected a metatable
1529 // on a table operand and parked a deopt request here.
1530 // Discard the sentinel value and return None so the caller
1531 // re-runs the call through the interpreter, which honours
1532 // __index/__newindex.
1533 if self.jit.pending_err.take().is_some() {
1534 return None;
1535 }
1536 Some(if returns_one {
1537 let v = if ret_is_float {
1538 Value::Float(f64::from_bits(r as u64))
1539 } else if ret_is_table {
1540 Value::Table(crate::runtime::Gc::from_ptr(
1541 r as *mut crate::runtime::Table,
1542 ))
1543 } else {
1544 Value::Int(r)
1545 };
1546 vec![v]
1547 } else {
1548 Vec::new()
1549 })
1550 }
1551 // Non-zero-arg Compiled state: call_value's empty-args
1552 // fast path can't drive it. Op::Call handles those.
1553 JitProtoState::Compiled { .. } | JitProtoState::Failed | JitProtoState::Untried => None,
1554 }
1555 }
1556
1557 /// P11-S2 / S2c — populate the cache slot. Flips `Untried` to either
1558 /// `Compiled { … }` or `Failed`; idempotent on already-populated
1559 /// states (call sites guard with a get before invoking).
1560 ///
1561 /// S4: consults a thread-local cross-`Vm` cache keyed by a hash of
1562 /// `proto.code`. Compiled artefacts live in the thread-local
1563 /// `JITModule` so their mmap pages outlive the `Vm`; subsequent
1564 /// `Vm`s loading the same source skip the cranelift compile step
1565 /// entirely.
1566 fn populate_jit_cache(&mut self, proto: Gc<crate::runtime::function::Proto>) {
1567 use crate::runtime::function::JitProtoState;
1568 let version = self.version();
1569 let pre53 = version <= crate::version::LuaVersion::Lua53;
1570 // P11-S5d.J — 5.1 and 5.2 have no Int subtype (all numbers
1571 // are Float). The JIT's `GetUpval` ValueRead path uses this
1572 // to default-pin upvalue reads to Float without a tag check.
1573 let float_only = version <= crate::version::LuaVersion::Lua52;
1574 // v2.0 Track J sub-step J-B — split-borrow JitState so the
1575 // trait method can take `&mut dyn JitStorage` without
1576 // double-borrowing self.jit.
1577 let jit = &mut self.jit;
1578 let storage: &mut dyn crate::jit::JitStorage = jit.storage.as_mut();
1579 match jit
1580 .chunk_compiler
1581 .try_compile(storage, proto, pre53, float_only)
1582 {
1583 crate::jit::CompileResult::Compiled {
1584 entry,
1585 num_args,
1586 returns_one,
1587 arg_float_mask,
1588 arg_table_mask,
1589 ret_is_float,
1590 ret_is_table,
1591 } => {
1592 proto.jit.set(JitProtoState::Compiled {
1593 entry,
1594 num_args,
1595 returns_one,
1596 arg_float_mask,
1597 arg_table_mask,
1598 ret_is_float,
1599 ret_is_table,
1600 });
1601 }
1602 crate::jit::CompileResult::Skipped => {
1603 proto.jit.set(JitProtoState::Failed);
1604 }
1605 }
1606 }
1607
1608 /// P11-S2c.B — `Op::Call` JIT fast path. Run inside `begin_call`
1609 /// before `push_frame`. Returns `true` when the call was handled
1610 /// in-place (no new Lua frame). Constraints: every arg slot must
1611 /// be `Value::Int`, the cached arity must match the call site's
1612 /// `nargs`, the host wanted-count `wanted` is honoured by
1613 /// `finish_results`. Also bails when a debug hook is armed —
1614 /// JIT'd code does not fire line / call / return hooks, so any
1615 /// active hook makes the interpreter the source of truth.
1616 fn try_jit_call_op(
1617 &mut self,
1618 cl: Gc<LuaClosure>,
1619 func_slot: u32,
1620 nargs: u32,
1621 wanted: i32,
1622 ) -> bool {
1623 use crate::runtime::function::JitProtoState;
1624 if !self.jit.enabled {
1625 return false;
1626 }
1627 // Any active debug hook means the interpreter has to run the
1628 // call so the hook gets the expected events.
1629 if self.hook.func.is_some() || self.hook.rust_func.is_some() {
1630 return false;
1631 }
1632 let proto = cl.proto;
1633 if let JitProtoState::Untried = proto.jit.get() {
1634 self.populate_jit_cache(proto);
1635 }
1636 let JitProtoState::Compiled {
1637 entry,
1638 num_args,
1639 returns_one,
1640 arg_float_mask,
1641 arg_table_mask,
1642 ret_is_float,
1643 ret_is_table,
1644 } = proto.jit.get()
1645 else {
1646 return false;
1647 };
1648 if num_args as u32 != nargs {
1649 return false;
1650 }
1651 // Pack args into i64 bit-patterns per the per-slot expected
1652 // kind. A Float-typed slot accepts Value::Float verbatim and
1653 // promotes Value::Int(x) via i64 → f64; a Table-typed slot
1654 // accepts only Value::Table and passes the raw Gc ptr; an
1655 // Int-typed slot accepts only Value::Int. Any other shape
1656 // bails to the interpreter so the call's actual dynamics
1657 // (metamethod dispatch / type-coerce) take over.
1658 let mut args: [i64; crate::jit::MAX_JIT_ARITY as usize] =
1659 [0; crate::jit::MAX_JIT_ARITY as usize];
1660 for i in 0..num_args as usize {
1661 let v = self.stack[(func_slot + 1) as usize + i];
1662 let want_float = (arg_float_mask >> i) & 1 == 1;
1663 let want_table = (arg_table_mask >> i) & 1 == 1;
1664 args[i] = match (want_table, want_float, v) {
1665 (true, _, Value::Table(t)) => t.as_ptr() as i64,
1666 (false, false, Value::Int(x)) => x,
1667 (false, true, Value::Float(f)) => f.to_bits() as i64,
1668 (false, true, Value::Int(x)) => (x as f64).to_bits() as i64,
1669 _ => return false,
1670 };
1671 }
1672 // P11-S5c / S5d.J — Vm + closure pin for helpers; see the
1673 // matching guard in `try_jit_call`.
1674 // v1.1 A1 Session A — route through chunk_compiler.
1675 let vm_ptr: *mut Vm = self;
1676 let _jit_vm_guard = self.jit.chunk_compiler.enter(vm_ptr, Some(cl));
1677 // SAFETY: the source `*const u8` is a JIT-compiled function entry pointer produced by Cranelift with the target `fn`-pointer signature (IntChunkFn / IntFnN); the JitVmGuard above keeps the JIT_VM TLS slot live across the call.
1678 let r = unsafe {
1679 match num_args {
1680 0 => (std::mem::transmute::<*const u8, crate::jit::IntChunkFn>(entry))(),
1681 1 => (std::mem::transmute::<*const u8, crate::jit::IntFn1>(entry))(args[0]),
1682 2 => {
1683 (std::mem::transmute::<*const u8, crate::jit::IntFn2>(entry))(args[0], args[1])
1684 }
1685 3 => (std::mem::transmute::<*const u8, crate::jit::IntFn3>(entry))(
1686 args[0], args[1], args[2],
1687 ),
1688 4 => (std::mem::transmute::<*const u8, crate::jit::IntFn4>(entry))(
1689 args[0], args[1], args[2], args[3],
1690 ),
1691 _ => unreachable!("MAX_JIT_ARITY enforces num_args <= 4"),
1692 }
1693 };
1694 drop(_jit_vm_guard);
1695 // P11-S5d.E' — see matching path in `try_jit_call`. A helper
1696 // flagged a metatable on a table operand; bail to the interpreter
1697 // so `push_frame` runs the call from scratch.
1698 if self.jit.pending_err.take().is_some() {
1699 return false;
1700 }
1701 // Write result at func_slot, replacing the closure value, then
1702 // hand to finish_results to pad/truncate per the call site's
1703 // `wanted` count.
1704 if returns_one {
1705 let v = if ret_is_float {
1706 Value::Float(f64::from_bits(r as u64))
1707 } else if ret_is_table {
1708 Value::Table(crate::runtime::Gc::from_ptr(
1709 r as *mut crate::runtime::Table,
1710 ))
1711 } else {
1712 Value::Int(r)
1713 };
1714 self.stack[func_slot as usize] = v;
1715 self.finish_results(func_slot, 1, wanted);
1716 } else {
1717 self.finish_results(func_slot, 0, wanted);
1718 }
1719 true
1720 }
1721
1722 /// `call_value` with control over the `from_c` debug boundary. A `__close`
1723 /// handler runs *within* the closing Lua frame's activation (PUC luaF_close
1724 /// invokes it inside that ci), so it is called with `from_c = false`: its
1725 /// debug parent is the closing function, not a synthetic C level.
1726 fn call_value_impl(
1727 &mut self,
1728 f: Value,
1729 args: &[Value],
1730 from_c: bool,
1731 ) -> Result<Vec<Value>, LuaError> {
1732 if self.c_depth >= MAX_C_DEPTH {
1733 return Err(self.rt_err("stack overflow"));
1734 }
1735 self.c_depth += 1;
1736 let func_slot = self.stack.len() as u32;
1737 self.stack.push(f);
1738 self.stack.extend_from_slice(args);
1739 self.top = self.stack.len() as u32;
1740 let r = self.call_at(func_slot, args.len() as u32, from_c);
1741 self.c_depth -= 1;
1742 if r.is_err()
1743 && self.yielding.is_none()
1744 && self.terminating.is_none()
1745 && !self.host_yield_pending
1746 && self.pending_async_native_fut.is_none()
1747 {
1748 // A `coroutine.yield` in flight raises a sentinel error to unwind the
1749 // Rust stack, but the suspended coroutine's frames/registers (which
1750 // sit at/above `func_slot`) must survive for the next resume — so we
1751 // only truncate on a real error. A self-close termination is in the
1752 // same boat: the dying thread's state is discarded wholesale.
1753 // v1.1 B10 — a `host_yield_pending` cooperative yield is in
1754 // the same boat as `yielding`: the next `EvalFuture::poll`
1755 // resumes the same call, so the in-flight frames must
1756 // survive.
1757 self.stack.truncate(func_slot as usize);
1758 self.top = func_slot;
1759 }
1760 r
1761 }
1762
1763 /// Invoke `f` with the running thread marked non-yieldable for the duration
1764 /// (PUC `luaD_callnoyield`): a `coroutine.yield` inside `f` hits the C-call
1765 /// boundary and errors instead of suspending. Used by library callbacks
1766 /// (sort comparator, gsub replacement) that run via synchronous Rust
1767 /// recursion and so could not be re-entered after a yield.
1768 pub(crate) fn call_noyield(
1769 &mut self,
1770 f: Value,
1771 args: &[Value],
1772 ) -> Result<Vec<Value>, LuaError> {
1773 self.nny += 1;
1774 let r = self.call_value(f, args);
1775 self.nny -= 1;
1776 r
1777 }
1778
1779 // ---- coroutines (P05) ----
1780
1781 pub(crate) fn new_coro(&mut self, body: Value) -> Gc<Coro> {
1782 // The new coroutine inherits the creating thread's current globals
1783 // (PUC `lua_newthread`: the new state copies `g->mainthread`'s
1784 // `l_gt`). `Vm.globals` always reflects the live thread, so reading
1785 // it here picks the creator regardless of which coro is running.
1786 self.heap.new_coro(body, self.globals)
1787 }
1788
1789 /// Is `t` the thread whose context is currently live in the VM?
1790 pub(crate) fn is_current_thread(&self, t: Option<Gc<Coro>>) -> bool {
1791 match (self.current, t) {
1792 (None, None) => true,
1793 (Some(a), Some(b)) => a.ptr_eq(b),
1794 _ => false,
1795 }
1796 }
1797
1798 /// Read an open-upvalue slot from its owning thread's stack (the live VM
1799 /// stack if that thread is current, else its saved context).
1800 #[doc(hidden)]
1801 pub fn read_slot(&self, slot: u32, thread: Option<Gc<Coro>>) -> Value {
1802 let s = slot as usize;
1803 if self.is_current_thread(thread) {
1804 self.stack[s]
1805 } else {
1806 match thread {
1807 Some(co) => co.stack[s],
1808 None => self.main_ctx.as_ref().expect("main context").stack[s],
1809 }
1810 }
1811 }
1812
1813 fn write_slot(&mut self, slot: u32, thread: Option<Gc<Coro>>, v: Value) {
1814 let s = slot as usize;
1815 if self.is_current_thread(thread) {
1816 self.stack[s] = v;
1817 } else {
1818 match thread {
1819 Some(co) => {
1820 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
1821 unsafe { co.as_mut() }.stack[s] = v;
1822 // co.stack is traced by Coro::trace; demote co back to
1823 // gray so propagate re-traces this slot if it was
1824 // already black.
1825 self.heap
1826 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
1827 }
1828 None => self.main_ctx.as_mut().expect("main context").stack[s] = v,
1829 }
1830 }
1831 }
1832
1833 /// Whether `co` is the main thread's identity object.
1834 pub(crate) fn is_main_coro(&self, co: Gc<Coro>) -> bool {
1835 self.main_coro.is_some_and(|m| m.ptr_eq(co))
1836 }
1837
1838 /// The status of `co` from the caller's view. The main thread's identity
1839 /// object has no stored status — it is "running" when nothing else runs,
1840 /// else "normal" (it resumed the active coroutine).
1841 pub(crate) fn effective_coro_status(&self, co: Gc<Coro>) -> CoroStatus {
1842 if self.is_main_coro(co) {
1843 if self.current.is_none() {
1844 CoroStatus::Running
1845 } else {
1846 CoroStatus::Normal
1847 }
1848 } else {
1849 co.status
1850 }
1851 }
1852
1853 /// `coroutine.close` (PUC `lua_closethread`): run the suspended coroutine's
1854 /// pending to-be-closed `__close` handlers, then mark it dead and drop its
1855 /// context. Handlers see the coroutine's death error (if it died by error)
1856 /// or nil; an error they raise propagates out. `Ok(Some(e))` means it died
1857 /// with error `e` and no handler overrode it; `Err` means a handler raised.
1858 pub(crate) fn close_coro(&mut self, co: Gc<Coro>) -> Result<Option<Value>, LuaError> {
1859 // re-entrant close: a __close handler closed its own coroutine while the
1860 // outer close is mid-flight (its context is live). Report success and let
1861 // the outer close finish — re-entering the swap would corrupt the stack.
1862 if self.current.is_some_and(|c| c.ptr_eq(co)) {
1863 return Ok(None);
1864 }
1865 // A chain of coroutines whose `__close` handlers each close the previous
1866 // one recurses on the C stack (PUC `luaD_callnoyield` in `lua_closethread`).
1867 // The calling handler's `call_value` has already pushed `c_depth` to the
1868 // cap, so here it reads as full first — report PUC's "C stack overflow"
1869 // before the next handler call would surface the plainer "stack overflow".
1870 if self.c_depth >= MAX_C_DEPTH {
1871 return Err(self.rt_err("C stack overflow"));
1872 }
1873 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
1874 let death_err = unsafe { co.as_mut() }.error_value.take();
1875 // swap the caller's live context out (into a GC-rooted home) and the
1876 // coroutine's in, mirroring resume_coro, so the __close handlers run on
1877 // the coroutine's stack while everything stays rooted.
1878 let resumer = self.current;
1879 let rctx = self.take_ctx();
1880 match resumer {
1881 Some(r) => {
1882 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
1883 let m = unsafe { r.as_mut() };
1884 m.stack = rctx.stack;
1885 m.frames = rctx.frames;
1886 m.open_upvals = rctx.open_upvals;
1887 m.tbc = rctx.tbc;
1888 m.top = rctx.top;
1889 m.pcall_depth = rctx.pcall_depth;
1890 }
1891 None => self.main_ctx = Some(rctx),
1892 }
1893 self.load_coro_ctx(co);
1894 self.current = Some(co);
1895 let result = self.close_slots(0, death_err);
1896 // discard the (now-closed) coroutine context and restore the caller
1897 let _ = self.take_ctx();
1898 match resumer {
1899 Some(r) => {
1900 self.load_coro_ctx(r);
1901 self.current = Some(r);
1902 }
1903 None => {
1904 let m = self.main_ctx.take().expect("main context saved");
1905 self.put_ctx(m);
1906 self.current = None;
1907 }
1908 }
1909 {
1910 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
1911 let m = unsafe { co.as_mut() };
1912 m.status = CoroStatus::Dead;
1913 m.stack = Vec::new();
1914 m.frames = Vec::new();
1915 m.open_upvals = Vec::new();
1916 m.tbc = Vec::new();
1917 m.top = 0;
1918 m.pcall_depth = 0;
1919 m.resume_at = None;
1920 m.error_value = None;
1921 }
1922 result.map(|()| death_err)
1923 }
1924
1925 /// `coroutine.running`: the running thread plus whether it is the main one.
1926 pub(crate) fn running_thread(&self) -> (Value, bool) {
1927 match self.current {
1928 Some(co) => (Value::Coro(co), false),
1929 None => (Value::Coro(self.main_coro.expect("main coro")), true),
1930 }
1931 }
1932
1933 /// `coroutine.isyieldable([co])`: whether `co` (default: the running
1934 /// thread) can yield. The main thread never can; any other coroutine can
1935 /// unless it is dead.
1936 pub(crate) fn is_yieldable(&self, co: Option<Gc<Coro>>) -> bool {
1937 match co {
1938 Some(c) => !self.main_coro.is_some_and(|m| m.ptr_eq(c)) && c.status != CoroStatus::Dead,
1939 // the running thread can yield only outside any non-yieldable C call
1940 None => self.current.is_some() && self.nny == 0,
1941 }
1942 }
1943
1944 /// Why `coroutine.yield` may not suspend the running thread right now, as a
1945 /// PUC error message — `None` if it may. Distinguishes "not in a coroutine"
1946 /// from "inside an unyieldable C call" (sort/gsub callback).
1947 pub(crate) fn yield_barrier(&self) -> Option<&'static str> {
1948 if self.current.is_none() {
1949 Some("attempt to yield from outside a coroutine")
1950 } else if self.nny > 0 {
1951 Some("attempt to yield across a C-call boundary")
1952 } else {
1953 None
1954 }
1955 }
1956
1957 /// The coroutine whose context is currently live (`None` on the main thread).
1958 pub(crate) fn current_coro(&self) -> Option<Gc<Coro>> {
1959 self.current
1960 }
1961
1962 /// `coroutine.close()` on the *running* thread (PUC 5.5 close-self): run all
1963 /// its pending `__close` handlers, then signal termination. The handlers run
1964 /// here, in place, with the thread still non-yieldable (a yield in one hits
1965 /// the C-call boundary). The returned sentinel unwinds the Rust stack the
1966 /// way a yield does — `exec_with` propagates it past any protecting pcall
1967 /// rather than letting `unwind` catch it — and `resume_coro` turns it into a
1968 /// clean death (or, if a handler raised, the coroutine's error).
1969 pub(crate) fn close_running(&mut self) -> LuaError {
1970 let death = match self.close_slots(0, None) {
1971 Ok(()) => None,
1972 Err(e) => Some(e.0),
1973 };
1974 self.terminating = Some(death);
1975 LuaError(Value::Nil)
1976 }
1977
1978 /// `coroutine.status` as seen by the caller.
1979 pub(crate) fn coro_status_str(&self, co: Gc<Coro>) -> &'static str {
1980 match self.effective_coro_status(co) {
1981 CoroStatus::Suspended => "suspended",
1982 CoroStatus::Running => "running",
1983 CoroStatus::Normal => "normal",
1984 CoroStatus::Dead => "dead",
1985 }
1986 }
1987
1988 fn take_ctx(&mut self) -> SavedCtx {
1989 let saved = SavedCtx {
1990 stack: std::mem::take(&mut self.stack),
1991 frames: std::mem::take(&mut self.frames),
1992 open_upvals: std::mem::take(&mut self.open_upvals),
1993 tbc: std::mem::take(&mut self.tbc),
1994 top: self.top,
1995 pcall_depth: self.pcall_depth,
1996 hook: self.hook,
1997 globals: self.globals,
1998 };
1999 self.frames_resync(); // P17-D Week 1 — frames now empty.
2000 saved
2001 }
2002
2003 fn put_ctx(&mut self, c: SavedCtx) {
2004 self.stack = c.stack;
2005 self.frames = c.frames;
2006 self.open_upvals = c.open_upvals;
2007 self.tbc = c.tbc;
2008 self.top = c.top;
2009 self.pcall_depth = c.pcall_depth;
2010 self.hook = c.hook;
2011 self.globals = c.globals;
2012 self.frames_resync(); // P17-D Week 1 — sync shadow to new Vec.
2013 }
2014
2015 /// Move a coroutine's saved context into the live VM fields.
2016 fn load_coro_ctx(&mut self, co: Gc<Coro>) {
2017 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
2018 let m = unsafe { co.as_mut() };
2019 self.stack = std::mem::take(&mut m.stack);
2020 self.frames = std::mem::take(&mut m.frames);
2021 self.open_upvals = std::mem::take(&mut m.open_upvals);
2022 self.tbc = std::mem::take(&mut m.tbc);
2023 self.top = m.top;
2024 self.frames_resync(); // P17-D Week 1 — sync shadow to coro's frames.
2025 self.pcall_depth = m.pcall_depth;
2026 self.hook = m.hook;
2027 self.globals = m.globals;
2028 }
2029
2030 /// Save the live VM context back into a coroutine object.
2031 fn store_coro_ctx(&mut self, co: Gc<Coro>) {
2032 let c = self.take_ctx();
2033 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
2034 let m = unsafe { co.as_mut() };
2035 m.stack = c.stack;
2036 m.frames = c.frames;
2037 m.open_upvals = c.open_upvals;
2038 m.tbc = c.tbc;
2039 m.top = c.top;
2040 m.pcall_depth = c.pcall_depth;
2041 m.hook = c.hook;
2042 m.globals = c.globals;
2043 // bulk-overwrite of every collectable field traced by Coro::trace:
2044 // demote the coro back to gray so propagate re-traces its new state.
2045 self.heap
2046 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
2047 }
2048
2049 /// `coroutine.resume` core: drive `co` with `args` until it yields, returns
2050 /// or errors. Ok(values) carries yielded or returned values; Err carries an
2051 /// error raised inside the coroutine (the coroutine becomes dead).
2052 pub(crate) fn resume_coro(
2053 &mut self,
2054 co: Gc<Coro>,
2055 args: Vec<Value>,
2056 ) -> Result<Vec<Value>, LuaError> {
2057 match co.status {
2058 CoroStatus::Suspended => {}
2059 CoroStatus::Dead => return Err(self.plain_err("cannot resume dead coroutine")),
2060 _ => return Err(self.plain_err("cannot resume non-suspended coroutine")),
2061 }
2062 if self.c_depth >= MAX_C_DEPTH {
2063 return Err(self.plain_err("C stack overflow"));
2064 }
2065 self.c_depth += 1;
2066 let resumer = self.current;
2067 // save the resumer's live context away
2068 let rctx = self.take_ctx();
2069 match resumer {
2070 Some(r) => {
2071 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
2072 let m = unsafe { r.as_mut() };
2073 m.stack = rctx.stack;
2074 m.frames = rctx.frames;
2075 m.open_upvals = rctx.open_upvals;
2076 m.tbc = rctx.tbc;
2077 m.top = rctx.top;
2078 m.pcall_depth = rctx.pcall_depth;
2079 m.globals = rctx.globals;
2080 m.status = CoroStatus::Normal;
2081 // bulk overwrite of every traced field on r — mirror
2082 // store_coro_ctx's barrier_back so propagate re-traces r.
2083 self.heap
2084 .barrier_back(r.as_ptr() as *mut crate::runtime::heap::GcHeader);
2085 }
2086 None => self.main_ctx = Some(rctx),
2087 }
2088 // swap the coroutine in
2089 self.load_coro_ctx(co);
2090 {
2091 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
2092 let m = unsafe { co.as_mut() };
2093 m.status = CoroStatus::Running;
2094 m.resumer = resumer;
2095 }
2096 // co.resumer is a traced Gc field; barrier_back covers the new
2097 // resumer reference and any future field writes during this call.
2098 self.heap
2099 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
2100 self.current = Some(co);
2101
2102 // drive it
2103 let drive = if co.started {
2104 self.coro_continue(&args)
2105 } else {
2106 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
2107 unsafe { co.as_mut() }.started = true;
2108 self.coro_first(co.body, &args)
2109 };
2110
2111 // classify: a self-close termination or a pending yield each win over
2112 // the (sentinel) error they raised to unwind the Rust stack.
2113 let (outcome, status) = if let Some(death) = self.terminating.take() {
2114 // the coroutine closed itself: it dies now, cleanly or with the
2115 // error a `__close` handler raised.
2116 match death {
2117 Some(e) => {
2118 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
2119 unsafe { co.as_mut() }.error_value = Some(e);
2120 self.heap
2121 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
2122 (Err(LuaError(e)), CoroStatus::Dead)
2123 }
2124 None => (Ok(Vec::new()), CoroStatus::Dead),
2125 }
2126 } else {
2127 match self.yielding.take() {
2128 Some((vals, fslot, nres)) => {
2129 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
2130 unsafe { co.as_mut() }.resume_at = Some((fslot, nres));
2131 (Ok(vals), CoroStatus::Suspended)
2132 }
2133 None => {
2134 // died: a return is clean, an error is remembered so a later
2135 // `coroutine.close` can report it (PUC lua_closethread).
2136 // Capture the error-point traceback (set by `unwind` before
2137 // popping the failing frames) and prepend a synthetic
2138 // top entry for the C native that initiated the error
2139 // (PUC `[C]: in function '<name>'`) so `debug.traceback(co)`
2140 // on the dead coroutine still shows the error site
2141 // (db.lua :848 family).
2142 if drive.is_err() {
2143 let mut tb = self.error_traceback.take().unwrap_or_default();
2144 if let Some(nm) = self.errored_native.take() {
2145 let mut prefixed: Vec<u8> = Vec::new();
2146 prefixed.extend_from_slice(
2147 format!("\n\t[C]: in function '{nm}'").as_bytes(),
2148 );
2149 prefixed.extend(tb);
2150 tb = prefixed;
2151 }
2152 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
2153 unsafe { co.as_mut() }.error_traceback = Some(tb);
2154 }
2155 if let Err(e) = drive {
2156 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
2157 unsafe { co.as_mut() }.error_value = Some(e.0);
2158 self.heap
2159 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
2160 }
2161 (drive, CoroStatus::Dead)
2162 }
2163 }
2164 };
2165
2166 // save the coroutine's context back and restore the resumer
2167 self.store_coro_ctx(co);
2168 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
2169 unsafe { co.as_mut() }.status = status;
2170 match resumer {
2171 Some(r) => {
2172 self.load_coro_ctx(r);
2173 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
2174 unsafe { r.as_mut() }.status = CoroStatus::Running;
2175 self.current = Some(r);
2176 }
2177 None => {
2178 let m = self.main_ctx.take().expect("main context saved");
2179 self.put_ctx(m);
2180 self.current = None;
2181 }
2182 }
2183 self.c_depth -= 1;
2184 outcome
2185 }
2186
2187 /// First resume: install the body function at slot 0 and run.
2188 fn coro_first(&mut self, body: Value, args: &[Value]) -> Result<Vec<Value>, LuaError> {
2189 self.stack.clear();
2190 self.stack.push(body);
2191 self.stack.extend_from_slice(args);
2192 self.top = self.stack.len() as u32;
2193 match self.begin_call(0, Some(args.len() as u32), -1, true) {
2194 Ok(true) => self.exec_with(1),
2195 Ok(false) => Ok(self.take_results(0)),
2196 Err(e) => Err(e),
2197 }
2198 }
2199
2200 /// Resume after a yield: deliver `args` as the results of the call that
2201 /// yielded, then continue the suspended thread.
2202 fn coro_continue(&mut self, args: &[Value]) -> Result<Vec<Value>, LuaError> {
2203 let (fslot, nres) = self.current.unwrap().resume_at.expect("resume point");
2204 let n = args.len() as u32;
2205 // Restore the full register window of the suspended top frame: a yield
2206 // that unwound through a native (call_value) may have left the stack
2207 // shorter than the frame needs. `base + max_stack` is what push_frame
2208 // allocates; `fslot + n` covers the delivered yield results.
2209 let frame_need = self
2210 .frames
2211 .last()
2212 .and_then(CallFrame::lua)
2213 .map(|f| (f.base + f.closure.proto.max_stack as u32) as usize)
2214 .unwrap_or(0);
2215 let need = frame_need.max((fslot + n) as usize);
2216 if self.stack.len() < need {
2217 self.stack.resize(need, Value::Nil);
2218 }
2219 for (i, &v) in args.iter().enumerate() {
2220 self.stack[fslot as usize + i] = v;
2221 }
2222 self.finish_results(fslot, n, nres);
2223 // the suspended `coroutine.yield` (a C call) now returns its resume
2224 // values: fire the matching "return" hook PUC defers until the resume.
2225 self.hook_return(true, 1, n)?;
2226 self.exec_with(1)
2227 }
2228
2229 /// `coroutine.yield`: suspend the running coroutine, recording where to
2230 /// resume. Errors if called outside a coroutine. Returns a sentinel error
2231 /// that `exec`/`resume_coro` recognise as a yield (never surfaced to Lua).
2232 pub(crate) fn do_yield(&mut self, func_slot: u32, vals: Vec<Value>) -> LuaError {
2233 let nres = self.native_nresults;
2234 self.yielding = Some((vals, func_slot, nres));
2235 // value is irrelevant: resume_coro consults `self.yielding`, not this
2236 LuaError(Value::Nil)
2237 }
2238
2239 /// Install or clear the debug hook on the running thread (`debug.sethook`
2240 /// without a thread argument). Arms the calling frame's `oldpc` to the
2241 /// sethook CALL's own pc (one less than the next-to-execute pc), mirroring
2242 /// PUC `rethook`'s `L->oldpc = pcRel(savedpc, p)` (= savedpc - code - 1) on
2243 /// native return: the very next traceexec compares against the sethook
2244 /// CALL's line. When the install statement and the following statement are
2245 /// on different source lines (db.lua :322), `changedline` fires for that
2246 /// first statement; when they share a line (db.lua :25 wrapper), they do
2247 /// not, so the wrapper line is not re-fired.
2248 pub(crate) fn install_hook(&mut self, hook: HookState) {
2249 self.hook = hook;
2250 if self.hook.line
2251 && let Some(f) = self.frames.last_mut().and_then(CallFrame::lua_mut)
2252 {
2253 f.hook_oldpc = f.pc.saturating_sub(1);
2254 }
2255 }
2256
2257 /// Install a hook on `target` (`None`/current thread → the live VM fields;
2258 /// another, suspended thread → its saved `Coro` state). PUC `debug.sethook`
2259 /// with an optional thread argument.
2260 ///
2261 /// `target == None` means "no explicit thread argument" — PUC binds that
2262 /// to `L` (the running thread). luna's live VM fields (`self.hook`,
2263 /// `self.frames`, `self.stack`) ARE the running thread's state, regardless
2264 /// of whether that's the main thread or a currently-resumed coroutine
2265 /// (save/restore happens at resume/yield boundaries via `load_coro_ctx`/
2266 /// `store_coro_ctx`). So a `None` target should always route to
2267 /// `install_hook` on the live fields. The pre-fix predicate gate
2268 /// `is_current_thread(target)` returned `false` when running inside a
2269 /// coroutine (`self.current = Some(co)`, `target = None` don't match)
2270 /// and silently dropped the hook on the floor — the install happened on
2271 /// no thread at all.
2272 pub(crate) fn set_hook(&mut self, target: Option<Gc<Coro>>, state: HookState) {
2273 if target.is_none() || self.is_current_thread(target) {
2274 self.install_hook(state);
2275 } else if let Some(co) = target {
2276 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
2277 let m = unsafe { co.as_mut() };
2278 m.hook = state;
2279 if state.line
2280 && let Some(f) = m.frames.last_mut().and_then(CallFrame::lua_mut)
2281 {
2282 f.hook_oldpc = u32::MAX;
2283 }
2284 // co.hook.func is a traced Value (Coro::trace covers it); demote
2285 // co back to gray so propagate sees the new hook function.
2286 self.heap
2287 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
2288 }
2289 }
2290
2291 /// The hook state of `target` (`None`/current → the live VM state).
2292 pub(crate) fn get_hook(&self, target: Option<Gc<Coro>>) -> HookState {
2293 match target {
2294 t if self.is_current_thread(t) => self.hook,
2295 Some(co) => co.hook,
2296 None => self.hook,
2297 }
2298 }
2299
2300 /// Invoke the debug hook for `event` (PUC `luaD_hook`). The hook runs with
2301 /// hooks disabled (PUC clears the mask) and its results/stack growth are
2302 /// discarded so the interrupted frame's register window is untouched.
2303 /// `line` is the source line for a "line" event, `None` (→ nil) otherwise.
2304 fn run_hook(
2305 &mut self,
2306 event: &[u8],
2307 line: Option<i64>,
2308 from_native: bool,
2309 ) -> Result<(), LuaError> {
2310 // v1.1 B11 — Rust hook fires first (no Vm reentrancy via call_value;
2311 // synchronous fn pointer call). Both Rust and Lua hooks may be
2312 // installed; both observe each event.
2313 if let Some(rh) = self.hook.rust_func {
2314 let evt = match event {
2315 b"call" => Some(RustHookEvent::Call),
2316 b"return" => Some(RustHookEvent::Return),
2317 b"tail call" | b"tail return" => Some(RustHookEvent::TailCall),
2318 b"line" => Some(RustHookEvent::Line(line.unwrap_or(0).max(0) as u32)),
2319 b"count" => Some(RustHookEvent::Count),
2320 _ => None,
2321 };
2322 if let Some(evt) = evt {
2323 let was_in_hook = self.in_hook;
2324 self.in_hook = true;
2325 rh(self, evt);
2326 self.in_hook = was_in_hook;
2327 }
2328 }
2329 let Some(hook) = self.hook.func else {
2330 return Ok(());
2331 };
2332 let saved_top = self.top;
2333 let saved_len = self.stack.len();
2334 let name = Value::Str(self.heap.intern(event));
2335 let lv = line.map_or(Value::Nil, Value::Int);
2336 self.in_hook = true;
2337 // PUC `db_sethook`'s C trampoline `hookf` sits between the engine and
2338 // the Lua hook — so `getinfo(2)` inside the hook resolves to whatever
2339 // ci sat below `hookf` (the function being hooked). When that hooked
2340 // function is native, no Lua frame for it exists in luna's `frames`;
2341 // model it as a synthetic C level by pushing the hook with
2342 // `from_c = true` (then `c_frame_name` reads the caller's call
2343 // instruction → e.g. `name = "sethook"`). When the hooked function is
2344 // Lua (its frame is still on the stack), push with `from_c = false`
2345 // so the level descent lands on it directly. The hook's own frame
2346 // carries `is_hook = true` so `getinfo(1).namewhat` reports "hook"
2347 // (PUC `CIST_HOOKED`).
2348 self.pending_is_hook = true;
2349 let r = self.call_value_impl(hook, &[name, lv], from_native);
2350 self.pending_is_hook = false;
2351 self.in_hook = false;
2352 self.stack.truncate(saved_len);
2353 self.top = saved_top;
2354 r.map(|_| ())
2355 }
2356
2357 /// Fire the "call" hook on entry to a function, if armed and not already in
2358 /// a hook (PUC clears the mask while a hook runs). PUC's transferinfo for
2359 /// a call hook is the param window: ftransfer = 1, ntransfer = nargs.
2360 /// `is_tail` selects the "tail call" event (PUC `LUA_HOOKTAILCALL`); a
2361 /// tail-call hook has no matching return hook (PUC luaD_pretailcall).
2362 fn hook_call_with(
2363 &mut self,
2364 from_native: bool,
2365 nargs: u32,
2366 is_tail: bool,
2367 ) -> Result<(), LuaError> {
2368 if self.hook.call
2369 && !self.in_hook
2370 && (self.hook.func.is_some() || self.hook.rust_func.is_some())
2371 {
2372 self.hook_ftransfer = 1;
2373 self.hook_ntransfer = nargs.min(u16::MAX as u32) as u16;
2374 // PUC 5.1 didn't distinguish tail-call events — every call,
2375 // including tail-calls, fired plain `"call"`. 5.2 introduced
2376 // the separate `"tail call"` event (mask `"c"` covers both).
2377 // 5.1 db.lua :366 pins this with `{"call","call","call","call",
2378 // "return","tail return","return","tail return"}`.
2379 let event: &[u8] = if is_tail && self.version >= LuaVersion::Lua52 {
2380 b"tail call"
2381 } else {
2382 b"call"
2383 };
2384 self.run_hook(event, None, from_native)?;
2385 }
2386 Ok(())
2387 }
2388
2389 pub(crate) fn hook_call(&mut self, from_native: bool, nargs: u32) -> Result<(), LuaError> {
2390 self.hook_call_with(from_native, nargs, false)
2391 }
2392
2393 /// Fire the "return" hook on exit from a function, if armed. ftransfer is
2394 /// the first result slot relative to the activation's func slot, ntransfer
2395 /// the number of results.
2396 pub(crate) fn hook_return(
2397 &mut self,
2398 from_native: bool,
2399 ftransfer: u32,
2400 nresults: u32,
2401 ) -> Result<(), LuaError> {
2402 if self.hook.ret
2403 && !self.in_hook
2404 && (self.hook.func.is_some() || self.hook.rust_func.is_some())
2405 {
2406 self.hook_ftransfer = ftransfer.min(u16::MAX as u32) as u16;
2407 self.hook_ntransfer = nresults.min(u16::MAX as u32) as u16;
2408 self.run_hook(b"return", None, from_native)?;
2409 }
2410 Ok(())
2411 }
2412
2413 /// PUC "tail return" event — fires once per tail call that collapsed
2414 /// into the activation now returning, *after* its own "return" event.
2415 /// 5.1 hook mask `"r"` covers both `return` and `tail return`.
2416 fn hook_tail_return(&mut self) -> Result<(), LuaError> {
2417 if self.hook.ret
2418 && !self.in_hook
2419 && (self.hook.func.is_some() || self.hook.rust_func.is_some())
2420 {
2421 self.run_hook(b"tail return", None, false)?;
2422 }
2423 Ok(())
2424 }
2425
2426 /// Call a metamethod with a single expected result.
2427 fn call_mm1(&mut self, f: Value, args: &[Value]) -> Result<Value, LuaError> {
2428 let mut r = self.call_value(f, args)?;
2429 Ok(if r.is_empty() {
2430 Value::Nil
2431 } else {
2432 r.swap_remove(0)
2433 })
2434 }
2435
2436 /// Begin a *yieldable* metamethod call from a VM instruction: `func(args…)`
2437 /// driven through the interpreter loop with a `Meta` continuation, so a
2438 /// `coroutine.yield` inside the metamethod suspends and resumes cleanly.
2439 /// On the metamethod's return the loop head runs `finish_meta(action, …)`.
2440 /// Returns to the caller with the call set up — the opcode arm must do no
2441 /// further work on the running frame and let the loop iterate. `tm` is
2442 /// the metamethod event name (e.g. "index", "add"); a Lua handler frame
2443 /// born from this call inherits it via `pending_tm`, so
2444 /// `debug.getinfo(1).namewhat == "metamethod"` and `.name == tm`
2445 /// (db.lua :878).
2446 fn begin_meta_call(
2447 &mut self,
2448 func: Value,
2449 args: &[Value],
2450 action: MetaAction,
2451 tm: &'static str,
2452 ) -> Result<(), LuaError> {
2453 let saved_top = self.top;
2454 let cont_slot = self.stack.len() as u32;
2455 self.stack.push(func);
2456 self.stack.extend_from_slice(args);
2457 self.top = self.stack.len() as u32;
2458 frames_push_sync(
2459 &mut self.frames,
2460 &mut self.frames_top,
2461 CallFrame::Cont(NativeCont {
2462 kind: ContKind::Meta(MetaCont { action, saved_top }),
2463 func_slot: cont_slot,
2464 nresults: 1,
2465 }),
2466 );
2467 let saved_tm = self.pending_tm.replace(tm);
2468 // begin_call drives a Lua metamethod through the loop (returns true) or
2469 // runs a native one inline (returns false, leaving results at cont_slot
2470 // for the loop head to pick up); either way the Meta cont resolves there.
2471 let r = self.begin_call(cont_slot, Some(args.len() as u32), 1, true);
2472 // Native callees never consumed pending_tm (push_frame is only hit on
2473 // a Lua callee); restore so it doesn't leak to a later push_frame.
2474 self.pending_tm = saved_tm;
2475 r?;
2476 Ok(())
2477 }
2478
2479 /// `R[dst] := t[key]` for a VM read opcode, resolving `__index` yieldably.
2480 fn op_index(&mut self, t: Value, key: Value, dst: u32) -> Result<(), LuaError> {
2481 // v2.13 WUC read-time probe: a collectable key must be live at
2482 // the moment it is used. O(1) membership test against the
2483 // freed-pointer log — gc-verify diagnostic builds only; exact
2484 // under quarantining allocators (ASAN).
2485 #[cfg(feature = "gc-verify")]
2486 if matches!(key, Value::Str(_)) {
2487 let h = match key {
2488 Value::Str(s) => s.as_ptr() as usize,
2489 _ => unreachable!(),
2490 };
2491 if self.heap.recently_freed.contains(&h) {
2492 let (pc, reg_info) = match self.frames.last() {
2493 Some(CallFrame::Lua(f)) => {
2494 let pc = f.pc as usize;
2495 let inst = f.closure.proto.code.get(pc.wrapping_sub(1));
2496 (
2497 pc,
2498 inst.map(|i| {
2499 format!(
2500 "op[pc-1]={:?} a={} b={} c={} base={}",
2501 i.op(),
2502 i.a(),
2503 i.b(),
2504 i.c(),
2505 f.base
2506 )
2507 })
2508 .unwrap_or_default(),
2509 )
2510 }
2511 _ => (0, String::new()),
2512 };
2513 panic!(
2514 "[gc-verify] op_index READ of dead string key {h:#x} \
2515 (gc_top {}, top {}, pc {pc}, {reg_info})",
2516 self.gc_top, self.top,
2517 );
2518 }
2519 }
2520 match self.index_step(t, key)? {
2521 MmOut::Done(v) => self.stack[dst as usize] = v,
2522 MmOut::Mm { func, recv } => {
2523 self.begin_meta_call(func, &[recv, key], MetaAction::Store { dst }, "index")?;
2524 }
2525 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from index_step"),
2526 }
2527 Ok(())
2528 }
2529
2530 /// `t[key] := v` for a VM write opcode, resolving `__newindex` yieldably.
2531 fn op_newindex(&mut self, t: Value, key: Value, v: Value) -> Result<(), LuaError> {
2532 match self.newindex_step(t, key, v)? {
2533 MmOut::Done(_) => {}
2534 MmOut::Mm { func, recv } => {
2535 self.begin_meta_call(func, &[recv, key, v], MetaAction::Discard, "newindex")?;
2536 }
2537 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from newindex_step"),
2538 }
2539 Ok(())
2540 }
2541
2542 /// Apply a comparison opcode's outcome: a known boolean drives the
2543 /// conditional skip directly; a metamethod is called yieldably, its
2544 /// truthiness driving the skip on return.
2545 fn op_compare(
2546 &mut self,
2547 step: MmOut,
2548 l: Value,
2549 r: Value,
2550 k: bool,
2551 tm: &'static str,
2552 ) -> Result<(), LuaError> {
2553 match step {
2554 MmOut::Done(v) => self.cond_skip(v.truthy(), k),
2555 MmOut::Mm { func, .. } => {
2556 self.begin_meta_call(func, &[l, r], MetaAction::Compare { k, negate: false }, tm)?;
2557 }
2558 MmOut::CompareSynth { func } => {
2559 // ≤5.3 `__le` falls back to `not __lt(r, l)`; the swap and
2560 // negation are driven through `MetaAction::Compare` so the
2561 // metamethod call can yield like any other compare.
2562 self.begin_meta_call(func, &[r, l], MetaAction::Compare { k, negate: true }, "lt")?;
2563 }
2564 }
2565 Ok(())
2566 }
2567
2568 /// Complete a VM instruction whose metamethod just returned `result` (PUC
2569 /// `luaV_finishOp`). The running frame is already back on top.
2570 fn finish_meta(&mut self, action: MetaAction, result: Value) -> Result<(), LuaError> {
2571 match action {
2572 MetaAction::Store { dst } => self.stack[dst as usize] = result,
2573 MetaAction::Discard => {}
2574 MetaAction::Compare { k, negate } => {
2575 let t = if negate {
2576 !result.truthy()
2577 } else {
2578 result.truthy()
2579 };
2580 self.cond_skip(t, k);
2581 }
2582 MetaAction::Concat { dst, base_a } => {
2583 self.stack[dst as usize] = result;
2584 self.top = dst + 1;
2585 self.concat_run(base_a)?;
2586 }
2587 }
2588 Ok(())
2589 }
2590
2591 // ---- metatables ----
2592
2593 pub(crate) fn metatable_of(&self, v: Value) -> Option<Gc<Table>> {
2594 match v {
2595 Value::Table(t) => t.metatable(),
2596 Value::Userdata(u) => u.metatable(),
2597 v => type_mt_slot(v).and_then(|i| self.type_mt[i]),
2598 }
2599 }
2600
2601 /// Set the shared metatable for `v`'s basic type (debug.setmetatable on a
2602 /// non-table). No-op for tables (they carry their own).
2603 pub(crate) fn set_type_metatable(&mut self, v: Value, mt: Option<Gc<Table>>) {
2604 if let Some(i) = type_mt_slot(v) {
2605 self.type_mt[i] = mt;
2606 }
2607 }
2608
2609 /// The metamethod of `v` for `mm`, or nil.
2610 pub(crate) fn get_mm(&self, v: Value, mm: Mm) -> Value {
2611 match self.metatable_of(v) {
2612 Some(mt) => mt.get(Value::Str(self.mm_names[mm as usize])),
2613 None => Value::Nil,
2614 }
2615 }
2616
2617 /// PUC 5.1 `get_compTM`: a comparison metamethod (`__eq` / `__lt` / `__le`)
2618 /// only fires when both operands carry a metatable that exposes the same
2619 /// implementation. Returns the metamethod to call, or `Nil` when no
2620 /// compatible match exists. Used to honour events.lua 5.1 :262's rule
2621 /// that `c == d` (where `d` has no metatable) falls back to raw equality.
2622 pub(crate) fn get_comp_mm(&self, l: Value, r: Value, mm: Mm) -> Value {
2623 let mt1 = self.metatable_of(l);
2624 let Some(mt1) = mt1 else { return Value::Nil };
2625 let key = Value::Str(self.mm_names[mm as usize]);
2626 let tm1 = mt1.get(key);
2627 if tm1.is_nil() {
2628 return Value::Nil;
2629 }
2630 let mt2 = self.metatable_of(r);
2631 let Some(mt2) = mt2 else { return Value::Nil };
2632 if mt1.as_ptr() == mt2.as_ptr() {
2633 return tm1;
2634 }
2635 let tm2 = mt2.get(key);
2636 if tm2.is_nil() {
2637 return Value::Nil;
2638 }
2639 if tm1.raw_eq(tm2) {
2640 return tm1;
2641 }
2642 Value::Nil
2643 }
2644
2645 /// PUC `luaT_objtypename`: the type name shown in error messages. A table
2646 /// or full userdata whose metatable carries a string `__name` reports that
2647 /// (e.g. "FILE*", "My Type") instead of the bare "table"/"userdata".
2648 pub(crate) fn obj_typename(&self, v: Value) -> String {
2649 if matches!(v, Value::Table(_) | Value::Userdata(_))
2650 && let Value::Str(s) = self.get_mm(v, Mm::Name)
2651 {
2652 return String::from_utf8_lossy(s.as_bytes()).into_owned();
2653 }
2654 v.type_name().to_string()
2655 }
2656
2657 fn call_at(
2658 &mut self,
2659 func_slot: u32,
2660 nargs: u32,
2661 from_c: bool,
2662 ) -> Result<Vec<Value>, LuaError> {
2663 if self.begin_call(func_slot, Some(nargs), -1, from_c)? {
2664 self.exec()
2665 } else {
2666 // native completed inline; results at func_slot..top
2667 Ok(self.take_results(func_slot))
2668 }
2669 }
2670
2671 /// Switch the `collectgarbage` mode, returning the previous mode name.
2672 pub(crate) fn gc_switch_mode(&mut self, new: &'static str) -> &'static str {
2673 std::mem::replace(&mut self.gc_mode, new)
2674 }
2675
2676 /// Whether the current `collectgarbage` mode is "generational" (where a
2677 /// "step" is a minor collection — a full atomic pass — rather than a paced
2678 /// incremental sweep).
2679 pub(crate) fn gc_mode_is_generational(&self) -> bool {
2680 self.gc_mode == "generational"
2681 }
2682
2683 /// Current `stepsize` pacing parameter (PUC: 0 means an unbounded step that
2684 /// completes a whole cycle at once).
2685 pub(crate) fn gc_stepsize(&self) -> i64 {
2686 self.gc_stepsize
2687 }
2688
2689 /// `collectgarbage("param", name [,value])`: read (or set, returning the
2690 /// previous value of) a pacing parameter. Returns `None` for an unknown
2691 /// name so the caller can raise PUC's `invalid parameter` error. The
2692 /// collector is stop-the-world, so these only round-trip for API fidelity.
2693 pub(crate) fn gc_param(&mut self, name: &[u8], set: Option<i64>) -> Option<i64> {
2694 let slot = match name {
2695 b"pause" => &mut self.gc_pause,
2696 b"stepmul" => &mut self.gc_stepmul,
2697 b"stepsize" => &mut self.gc_stepsize,
2698 _ => return None,
2699 };
2700 let prev = *slot;
2701 if let Some(v) = set {
2702 *slot = v;
2703 }
2704 Some(prev)
2705 }
2706
2707 /// Interpreter safe-point auto-GC: FULL incremental Propagate + adaptive
2708 /// paced sweep via `Vm::gc_step`.
2709 ///
2710 /// Round 1/2 of this attempt SIGABRT'd under coroutine + finalizer stress
2711 /// (suspected missed barrier). Round 3 (STW-mark + paced sweep) hung
2712 /// heavy.lua. With **born-black during Propagate** landed (@92b22b3) the
2713 /// suspected UAF is structurally closed — born objects no longer become
2714 /// dead-white at atomic flip — so Propagate is safe to re-enable here.
2715 ///
2716 /// Adaptive budget scales with heap size: 100M-object heap (heavy.lua's
2717 /// `loadrep` stress) gets a 25M-object budget so a cycle completes in
2718 /// O(SWEEP_DIVISOR) safe-points regardless of size.
2719 #[inline(always)]
2720 pub(crate) fn maybe_collect_garbage(&mut self, live_top: u32) {
2721 if self.gc_finalizing {
2722 return;
2723 }
2724 if !self.heap.gc_due() {
2725 return;
2726 }
2727 // v2.5 P1B-2E: tighten to bare `live_top`. The v2.2.0
2728 // `live_top.max(self.top)` workaround is now obsoleted by
2729 // v2.3's `finish_results` slot-clear + v2.5 P1B-2A
2730 // (Op::TailCall collapse slot-clear) + v2.5 P1B-2B
2731 // (pcall unwind slot-clear). PUC L->top discipline is now
2732 // mirrored at every frame-pop site.
2733 self.gc_top = live_top;
2734 // PUC stepmul: % of allocation rate. Higher = more GC work per
2735 // safe-point (lower memory, more CPU). Default 100 = `live / 4` per
2736 // step (~4 safe-points per cycle). stepmul=200 → `live / 2`, etc.
2737 const SWEEP_BASE: usize = 400; // 400 / stepmul=100 = divisor 4
2738 const MIN_BUDGET: usize = 64_000;
2739 let stepmul = self.gc_stepmul.max(1) as usize;
2740 let divisor = (SWEEP_BASE / stepmul).max(1);
2741 let budget = (self.heap.live_objects() / divisor).max(MIN_BUDGET);
2742 if self.gc_step(budget) {
2743 self.heap.rearm_gc_pause(self.gc_pause);
2744 }
2745 }
2746
2747 /// Enumerate the GC roots: first-class `Value` roots plus bare-object
2748 /// roots (open upvalues, which are not first-class Values). Shared by the
2749 /// full collector and the incremental-sweep driver so both snapshot the
2750 /// exact same live set.
2751 fn gc_roots(&self) -> (Vec<Value>, Vec<*mut GcHeader>) {
2752 let mut roots: Vec<Value> = Vec::with_capacity(self.stack.len() + 32);
2753 roots.push(Value::Table(self.globals));
2754 for mt in self.type_mt.into_iter().flatten() {
2755 roots.push(Value::Table(mt));
2756 }
2757 for &n in &self.mm_names {
2758 roots.push(Value::Str(n));
2759 }
2760 // Root the running thread's live registers (PUC marks [stack, top)).
2761 // `gc_top` is the instruction-level cursor of the last GC
2762 // safe-point: allocation safe-points set it via
2763 // `maybe_collect_garbage(live_top)`, and `begin_call` raises it
2764 // to the callee's argument top when entering a native — PUC's
2765 // `L->top = func + 1 + nargs` C-call discipline. Without that
2766 // raise, an explicit `collectgarbage()` collected with a STALE
2767 // cursor from some earlier (lower) safe-point and freed its own
2768 // caller's register-held strings — UAF-C
2769 // (STATUS_ACCESS_VIOLATION on Windows / ASAN heap-use-after-free
2770 // on Linux; the v2.13 WUC gc-verify frame audit pinpointed the
2771 // under-rooted slots). Values stranded above the cursor stay
2772 // excluded so weak-table entries are not spuriously pinned
2773 // (gc.lua:544 suspended-coroutine collection).
2774 let live = (self.gc_top as usize).min(self.stack.len());
2775 roots.extend_from_slice(&self.stack[..live]);
2776 for cf in &self.frames {
2777 match cf {
2778 CallFrame::Lua(f) => roots.push(Value::Closure(f.closure)),
2779 CallFrame::Cont(NativeCont {
2780 kind: ContKind::Xpcall { handler },
2781 ..
2782 }) => roots.push(*handler),
2783 CallFrame::Cont(NativeCont {
2784 kind: ContKind::Close(cc),
2785 ..
2786 }) => {
2787 // Root the error threaded through this close chain so a
2788 // `collectgarbage()` inside a sibling `__close` handler
2789 // does not free it before the next handler is invoked
2790 // (PUC L->ci->u.l.errfunc / the closing_err shadow).
2791 if let Some(e) = cc.pending {
2792 roots.push(e);
2793 }
2794 if let AfterClose::ResumeUnwind { err, .. } = cc.after {
2795 roots.push(err);
2796 }
2797 }
2798 CallFrame::Cont(_) => {}
2799 }
2800 }
2801 if let Some(e) = self.closing_err {
2802 roots.push(e);
2803 }
2804 // B12 host roots — Lua-facade handles keep their referenced
2805 // values alive across calls/yields. Trace the whole vector;
2806 // unused slots (post-`unpin_all`) carry Value::Nil which the
2807 // GC ignores.
2808 for slot in &self.host_roots {
2809 // v1.3 SR — free-list slots carry Value::Nil (GC no-op).
2810 roots.push(slot.value);
2811 }
2812 // v2.1 — `table.sort` and similar builtins stash their working
2813 // `Vec<Value>` here so a `collectgarbage()` invoked inside the
2814 // comparator callback doesn't free strings/tables snapshotted
2815 // off the live table (sort.lua's `load(..)(); collectgarbage()`
2816 // compare regression).
2817 for buf in &self.sort_scratch {
2818 roots.extend_from_slice(buf);
2819 }
2820 // v2.1 — the running-natives chain holds Gc<NativeClosure>s
2821 // mid-execution. Without rooting them here, a `collectgarbage()`
2822 // invoked inside the running native (sort.lua AA `load(..)();
2823 // collectgarbage()` compare callback regression) sweeps the
2824 // closure that's actively executing, leaving `nc.upvals`
2825 // dangling and the Rust local `nc` pointing at recycled memory
2826 // — the SIGSEGV pops on the very next field access or pop.
2827 for &nc in &self.running_natives {
2828 roots.push(Value::Native(nc));
2829 }
2830 // the running thread's debug hook (suspended threads root theirs via
2831 // Coro::trace / the main_ctx sweep below)
2832 if let Some(h) = self.hook.func {
2833 roots.push(h);
2834 }
2835 // the running coroutine (its saved-context fields live in the VM, but
2836 // the object itself + its resumer chain must stay reachable)
2837 if let Some(co) = self.current {
2838 roots.push(Value::Coro(co));
2839 }
2840 if let Some(mc) = self.main_coro {
2841 roots.push(Value::Coro(mc));
2842 }
2843 // debug.getregistry() and io library state
2844 if let Some(r) = self.registry {
2845 roots.push(Value::Table(r));
2846 }
2847 if let Some(mt) = self.file_mt {
2848 roots.push(Value::Table(mt));
2849 }
2850 if let Some(f) = self.io_input {
2851 roots.push(Value::Userdata(f));
2852 }
2853 if let Some(f) = self.io_output {
2854 roots.push(Value::Userdata(f));
2855 }
2856 // the main thread's saved context while a coroutine runs
2857 if let Some(m) = &self.main_ctx {
2858 roots.extend_from_slice(&m.stack);
2859 if let Some(h) = m.hook.func {
2860 roots.push(h);
2861 }
2862 for cf in &m.frames {
2863 match cf {
2864 CallFrame::Lua(f) => roots.push(Value::Closure(f.closure)),
2865 CallFrame::Cont(NativeCont {
2866 kind: ContKind::Xpcall { handler },
2867 ..
2868 }) => roots.push(*handler),
2869 CallFrame::Cont(_) => {}
2870 }
2871 }
2872 }
2873 let mut extra: Vec<*mut GcHeader> = self
2874 .open_upvals
2875 .iter()
2876 .map(|&(_, uv)| uv.as_ptr() as *mut GcHeader)
2877 .collect();
2878 if let Some(m) = &self.main_ctx {
2879 extra.extend(
2880 m.open_upvals
2881 .iter()
2882 .map(|&(_, uv)| uv.as_ptr() as *mut GcHeader),
2883 );
2884 }
2885 (roots, extra)
2886 }
2887
2888 /// Run a full collection with the VM's roots, then run any `__gc`
2889 /// finalizers the collection scheduled. A no-op (returns 0) when already
2890 /// inside a finalizer — the collector is not reentrant (PUC).
2891 pub fn collect_garbage(&mut self) -> usize {
2892 if self.gc_finalizing {
2893 return 0;
2894 }
2895 let (roots, extra) = self.gc_roots();
2896 let freed = self.heap.collect_ex(&roots, &extra);
2897 #[cfg(feature = "gc-verify")]
2898 self.verify_frame_regs_live("collect_garbage");
2899 self.run_finalizers();
2900 freed
2901 }
2902
2903 /// v2.13 WUC `gc-verify` — after a collect, every register slot the
2904 /// collector just rooted (`[0, max(gc_top, top))` — the same bound
2905 /// `gc_roots` uses) must hold a live value. A dead value inside the
2906 /// rooted range means the root snapshot and the sweep disagreed —
2907 /// the bug class behind UAF-C. (Slots ABOVE the bound may hold
2908 /// stale dead values legitimately; the interpreter's contract is
2909 /// that it writes them before reading.)
2910 #[cfg(feature = "gc-verify")]
2911 pub(crate) fn verify_frame_regs_live(&self, ctx: &str) {
2912 let live = self.heap.debug_live_set();
2913 let header = |v: Value| -> Option<usize> {
2914 match v {
2915 Value::Str(s) => Some(s.as_ptr() as usize),
2916 Value::Table(t) => Some(t.as_ptr() as usize),
2917 Value::Closure(c) => Some(c.as_ptr() as usize),
2918 Value::Native(n) => Some(n.as_ptr() as usize),
2919 Value::Coro(c) => Some(c.as_ptr() as usize),
2920 Value::Userdata(u) => Some(u.as_ptr() as usize),
2921 _ => None,
2922 }
2923 };
2924 let bound = (self.gc_top as usize).min(self.stack.len());
2925 for i in 0..bound {
2926 if let Some(h) = header(self.stack[i])
2927 && !live.contains(&h)
2928 {
2929 panic!(
2930 "[gc-verify] {ctx}: rooted stack slot {i} (gc_top {}, top {}) \
2931 holds a dead value {h:#x} after collect",
2932 self.gc_top, self.top,
2933 );
2934 }
2935 }
2936 // Diagnostic tier: a dead value ABOVE the cursor is only a bug if
2937 // that register is a named local still in scope (the interpreter
2938 // WILL read it). Cross-check against the proto's LocVar table.
2939 for (fi, cf) in self.frames.iter().enumerate() {
2940 if let CallFrame::Lua(f) = cf {
2941 let base = f.base as usize;
2942 let maxs = f.closure.proto.max_stack as usize;
2943 let hi = (base + maxs).min(self.stack.len());
2944 let pc = f.pc;
2945 for i in bound.max(base)..hi {
2946 if let Some(h) = header(self.stack[i])
2947 && !live.contains(&h)
2948 {
2949 let reg = (i - base) as u32;
2950 if let Some(lv) = f
2951 .closure
2952 .proto
2953 .locvars
2954 .iter()
2955 .find(|lv| lv.reg == reg && lv.start_pc <= pc && pc < lv.end_pc)
2956 {
2957 panic!(
2958 "[gc-verify] {ctx}: frame {fi} IN-SCOPE LOCAL '{}' \
2959 (reg {reg}, abs {i}, pc {pc}, gc_top {}) holds a \
2960 dead value {h:#x} — live_top cursor excluded a \
2961 live named local",
2962 lv.name, self.gc_top,
2963 );
2964 }
2965 }
2966 }
2967 }
2968 }
2969 }
2970
2971 /// PUC 5.1 `collectgarbage` re-raised the first error a `__gc` finalizer
2972 /// threw; gc.lua's "errors during collection" probe relies on it. This
2973 /// variant runs the same cycle but propagates the captured finalizer
2974 /// error to the explicit caller.
2975 pub(crate) fn collect_garbage_propagating(&mut self) -> Result<usize, LuaError> {
2976 if self.gc_finalizing {
2977 return Ok(0);
2978 }
2979 let (roots, extra) = self.gc_roots();
2980 let freed = self.heap.collect_ex(&roots, &extra);
2981 #[cfg(feature = "gc-verify")]
2982 self.verify_frame_regs_live("collect_garbage_propagating");
2983 self.run_finalizers_or_err()?;
2984 Ok(freed)
2985 }
2986
2987 /// Whether a `__gc` finalizer is currently running (so `collectgarbage`
2988 /// should report fail rather than collect).
2989 pub(crate) fn gc_is_finalizing(&self) -> bool {
2990 self.gc_finalizing
2991 }
2992
2993 /// PUC 5.4+ default warnf: emit one piece of a warning message. `to_cont`
2994 /// = true indicates more pieces follow (concatenated until the first
2995 /// `to_cont = false` call flushes the whole line). Mirrors
2996 /// `lauxlib.c::warnfon` + `warnfcont` + `checkcontrol`:
2997 /// * If the buffer is fresh, `to_cont` is false, and the message is
2998 /// `@<word>`, treat as a control message — only `@on` / `@off` are
2999 /// recognised; any other `@…` is silently ignored.
3000 /// * Otherwise, while the state is `Off`, drop the piece; while `On`,
3001 /// accumulate, and flush to stderr + `warn_log` on the
3002 /// non-continuation call.
3003 pub(crate) fn emit_warn(&mut self, msg: &[u8], to_cont: bool) {
3004 if self.warn_buf.is_empty()
3005 && !to_cont
3006 && let Some(b'@') = msg.first().copied()
3007 {
3008 match &msg[1..] {
3009 b"on" => self.warn_state = WarnState::On,
3010 b"off" => self.warn_state = WarnState::Off,
3011 _ => {} // unknown control — silently ignored (PUC checkcontrol)
3012 }
3013 return;
3014 }
3015 if self.warn_state == WarnState::Off {
3016 // drop continuation pieces too — PUC `warnfoff` is the trampoline
3017 return;
3018 }
3019 self.warn_buf.extend_from_slice(msg);
3020 if !to_cont {
3021 let line = std::mem::take(&mut self.warn_buf);
3022 eprintln!("Lua warning: {}", String::from_utf8_lossy(&line));
3023 self.warn_log.push(line);
3024 }
3025 }
3026
3027 /// Drain the in-process warning log (one entry per emitted message, sans
3028 /// `"Lua warning: "` prefix and newline). For test harnesses that want to
3029 /// assert on warn output without scraping stderr.
3030 pub fn warn_log_take(&mut self) -> Vec<Vec<u8>> {
3031 std::mem::take(&mut self.warn_log)
3032 }
3033
3034 /// Arm the cooperative instruction budget (P09 embedding). The run loop
3035 /// decrements this once per dispatch turn; on zero it raises a catchable
3036 /// `"instruction budget exceeded"` error and disarms itself so the host
3037 /// can resume with a fresh budget on the next call. `None` removes the
3038 /// cap. Pass `Some(n)` before `eval`/`call_value` for the embedder's
3039 /// short-script semantics.
3040 pub fn set_instr_budget(&mut self, budget: Option<i64>) {
3041 self.instr_budget = budget;
3042 }
3043
3044 /// Remaining instruction budget (None when unbounded).
3045 pub fn instr_budget_remaining(&self) -> Option<i64> {
3046 self.instr_budget
3047 }
3048
3049 /// Toggle the cranelift JIT (P11). Default `true`. Sandbox embedders
3050 /// **must** disable JIT when relying on `instr_budget` — see the
3051 /// `jit_enabled` field doc for the rationale.
3052 pub fn set_jit_enabled(&mut self, enabled: bool) {
3053 self.jit.enabled = enabled;
3054 }
3055
3056 /// Current JIT enable state.
3057 pub fn jit_enabled(&self) -> bool {
3058 self.jit.enabled
3059 }
3060
3061 /// Toggle the trace JIT (P12). Off by default while the sprint
3062 /// develops. When enabled, hot back-edges are counted on
3063 /// `Proto.trace_hot_count`; once the counter passes
3064 /// `TRACE_HOT_THRESHOLD`, the dispatch loop enters recording
3065 /// mode at the back-edge target. Stays a no-op until S2's
3066 /// trace lowerer and S3's dispatcher land.
3067 pub fn set_trace_jit_enabled(&mut self, enabled: bool) {
3068 self.jit.trace_enabled = enabled;
3069 }
3070
3071 /// P16-A — opt-in flag for the self-link cycle catch. See field
3072 /// docs for the correctness blocker. Default `false`.
3073 pub fn set_p16_self_link_enabled(&mut self, enabled: bool) {
3074 self.jit.p16_self_link_enabled = enabled;
3075 }
3076
3077 /// Current state of the P16-A self-link cycle catch.
3078 pub fn p16_self_link_enabled(&self) -> bool {
3079 self.jit.p16_self_link_enabled
3080 }
3081
3082 /// Current trace-JIT enable state.
3083 pub fn trace_jit_enabled(&self) -> bool {
3084 self.jit.trace_enabled
3085 }
3086
3087 /// Number of traces that have closed cleanly (looped back to the
3088 /// head PC) since this Vm was constructed. Cumulative; used by
3089 /// tests + tuning. Will become the dominant signal once S2's
3090 /// compile + cache lands.
3091 pub fn trace_closed_count(&self) -> u64 {
3092 self.jit.counters.closed
3093 }
3094
3095 /// Number of traces that have aborted (exceeded MAX_TRACE_LEN or
3096 /// hit an un-recordable op — the latter lands at S2).
3097 pub fn trace_aborted_count(&self) -> u64 {
3098 self.jit.counters.aborted
3099 }
3100
3101 /// P13-S13-G v2 — number of compiled traces whose close shape
3102 /// is `TraceEnd::InlineAbort` (depth>0 boundary). Such traces
3103 /// pin `dispatchable=false` because the dispatcher can't
3104 /// resume at a depth>0 PC without the matching CallFrames.
3105 /// S4-step4b's frame-mat helper could synthesise those, but
3106 /// the InlineAbort emit path isn't wired up yet — fresh
3107 /// pickup work for S13-G v2-full.
3108 pub fn trace_inline_abort_count(&self) -> u64 {
3109 self.jit.counters.inline_abort
3110 }
3111
3112 /// P13-S13-G v2.5 — see `JitCounters::dispatch_off_reasons`.
3113 pub fn trace_dispatch_off_reasons(&self) -> &[&'static str] {
3114 &self.jit.counters.dispatch_off_reasons
3115 }
3116
3117 /// P13-S13-G v2.6 — see `JitCounters::compile_failed_reasons`.
3118 pub fn trace_compile_failed_reasons(&self) -> &[&'static str] {
3119 &self.jit.counters.compile_failed_reasons
3120 }
3121
3122 /// P13-S13-H — see `JitCounters::closed_lens`. Returns
3123 /// `(is_call_triggered, ops_len)` for every trace that closed.
3124 pub fn trace_closed_lens(&self) -> &[(bool, usize)] {
3125 &self.jit.counters.closed_lens
3126 }
3127
3128 /// v2.0 Track-R R2 — see [`crate::vm::jit_state::JitCounters::close_cause_counts`].
3129 /// Per-reason close-cause counts (recorder-side abort/discard +
3130 /// lowerer-side dispatch_off labels) keyed by `&'static str`.
3131 pub fn trace_close_cause_counts(&self) -> &std::collections::HashMap<&'static str, u64> {
3132 &self.jit.counters.close_cause_counts
3133 }
3134
3135 /// v2.0 Track-R R3b — number of compiled traces whose
3136 /// `CompiledTrace.downrec_link` is `Some(_)` (lowerer's
3137 /// `downrec_idx_opt` arm emitted the stitch sentinel + caller-pc
3138 /// guard scaffold). R3b regression pin checks `>= 1` on a fib(3)
3139 /// hot loop with p16-on. R3b keeps `dispatchable = false` even
3140 /// when this count bumps; R3d will lift it.
3141 pub fn trace_downrec_link_compiled_count(&self) -> u64 {
3142 self.jit.counters.downrec_link_compiled
3143 }
3144
3145 /// v2.0 Track-R R3c — see
3146 /// [`crate::vm::jit_state::JitCounters::downrec_dispatched`]. Number
3147 /// of times the dispatcher's `is_downrec_sentinel` arm fired and
3148 /// classified the return as a caller-pc-guard HIT.
3149 pub fn trace_downrec_dispatched_count(&self) -> u64 {
3150 self.jit.counters.downrec_dispatched
3151 }
3152
3153 /// v2.0 Track-R R3c — see
3154 /// [`crate::vm::jit_state::JitCounters::downrec_deopt`]. Number of
3155 /// times the dispatcher entered a `downrec_link`-bearing trace and
3156 /// the trace returned via the lowerer's deopt block (caller-pc
3157 /// guard MISS), or the dispatcher itself force-deopted via the
3158 /// stitch-cycle checkpoint.
3159 pub fn trace_downrec_deopt_count(&self) -> u64 {
3160 self.jit.counters.downrec_deopt
3161 }
3162
3163 /// v2.0 Track-R R3d — see
3164 /// [`crate::vm::jit_state::JitCounters::multi_way_guard_emitted`].
3165 /// Number of compiled traces whose lowerer emitted a multi-way
3166 /// caller-pc guard chain (>= 2 distinct `caller_pc` candidates)
3167 /// at the `TraceEnd::DownRec` close + lifted `dispatchable = true`.
3168 pub fn trace_multi_way_guard_emitted_count(&self) -> u64 {
3169 self.jit.counters.multi_way_guard_emitted
3170 }
3171
3172 /// P12-S2.C — number of closed traces the lowerer compiled and
3173 /// parked on `Proto.traces`. Re-records of the same head_pc are
3174 /// deduped (the second close finds the head_pc already cached
3175 /// and skips compile), so this never exceeds `trace_closed_count`.
3176 pub fn trace_compiled_count(&self) -> u64 {
3177 self.jit.counters.compiled
3178 }
3179
3180 /// v2.1 Phase 1I.B — number of times the recorder captured a
3181 /// [`crate::jit::trace_types::FieldIcSnapshot`] under
3182 /// `LUNA_JIT_FIELD_IC=1`. Stays 0 on the env-default path. Used
3183 /// by the Phase 1I.B opt-in fire test to verify the env gate
3184 /// wiring round-trips end-to-end (env -> recorder -> snapshot
3185 /// -> counter -> getter -> assertion).
3186 pub fn trace_field_ic_snapshot_count(&self) -> u64 {
3187 self.jit.counters.field_ic_snapshot_captured
3188 }
3189
3190 /// P12-S2.C — number of closed traces the lowerer rejected
3191 /// (any of the bail conditions in
3192 /// `crate::jit::trace::try_compile_trace`).
3193 pub fn trace_compile_failed_count(&self) -> u64 {
3194 self.jit.counters.compile_failed
3195 }
3196
3197 /// P12-S3 — number of times the dispatcher jumped into a
3198 /// compiled trace. Bumps on every entry; `trace_deopt_count`
3199 /// counts the subset where the trace returned with a parked
3200 /// `jit_pending_err`.
3201 pub fn trace_dispatched_count(&self) -> u64 {
3202 self.jit.counters.dispatched
3203 }
3204
3205 /// P12-S3 — number of trace entries that came back with
3206 /// `jit_pending_err` set (typically a metatable shadowed an
3207 /// index inside a helper, forcing the dispatcher to fall back
3208 /// to the interpreter without committing the trace's result).
3209 pub fn trace_deopt_count(&self) -> u64 {
3210 self.jit.counters.deopt
3211 }
3212
3213 /// P15-A v1 — number of times the dispatcher started a side
3214 /// trace recording (an `exit_hit_counts` slot crossed
3215 /// [`crate::jit::trace::HOTEXIT_THRESHOLD`] while `active_trace`
3216 /// was None and trace JIT was enabled). Each unit is exactly one
3217 /// `start_side_trace` call; the actual compile success counts
3218 /// under [`Self::trace_compiled_count`] like any other trace.
3219 /// Probe use: distinguishes the "side-trace pipeline fired"
3220 /// signal from the "primary back-edge / call-trigger fired"
3221 /// signal so v0-v3 architectural progress is visible without
3222 /// reading per-counter histograms.
3223 pub fn trace_side_trace_started_count(&self) -> u64 {
3224 self.jit.counters.side_trace_started
3225 }
3226
3227 /// P15-A v2-A — number of side-trace recordings that closed,
3228 /// compiled successfully, AND patched their parent's
3229 /// `exit_side_trace_ptrs[exit_idx]`. The parent's IR doesn't
3230 /// dispatch through these ptrs yet (v2-B/C job), but the
3231 /// counter + ptr write proves the compile + link pipeline is
3232 /// complete end-to-end.
3233 pub fn trace_side_trace_compiled_count(&self) -> u64 {
3234 self.jit.counters.side_trace_compiled
3235 }
3236
3237 /// P15-A v2-C-A5-C — number of side traces that compiled
3238 /// successfully but were SHEDDED by the close-handler shape-
3239 /// match gate (`exit_tags_match_entry_tags`). High ratios
3240 /// vs. `trace_side_trace_compiled_count` indicate the
3241 /// architecture is shedding lots of would-be side traces;
3242 /// useful as a tuning probe for future relaxation of the
3243 /// gate or for child-IR re-specialisation against parent's
3244 /// exit shape.
3245 pub fn trace_side_trace_shape_mismatch_count(&self) -> u64 {
3246 self.jit.counters.side_trace_shape_mismatch
3247 }
3248
3249 /// P12-S5-A — sum of NewTable sites the pre-emit escape sweep
3250 /// classified as `crate::jit::trace::EscapeState::Sinkable`
3251 /// across every successfully compiled trace on this Vm. The
3252 /// count is post-demotion: sites pre-emit drops back to Escaped
3253 /// for not meeting v1 sunk-emit criteria are NOT counted.
3254 /// `trace_sunk_alloc_count` matches one-for-one today (every
3255 /// surviving Sinkable site goes through sunk emit).
3256 pub fn trace_sinkable_seen_count(&self) -> u64 {
3257 self.jit.counters.sinkable_seen
3258 }
3259
3260 /// P14-S14-B v1 — see `JitCounters::accum_bufferable_seen`.
3261 pub fn trace_accum_bufferable_seen_count(&self) -> u64 {
3262 self.jit.counters.accum_bufferable_seen
3263 }
3264
3265 /// P15-prep — total dispatch hits across all known traces,
3266 /// broken into hot-exit telemetry (max single-exit count,
3267 /// total dispatches, exit count). Used by probes to identify
3268 /// hot side-exits as side-trace candidates.
3269 ///
3270 /// Walks `cl.proto` AND all nested protos in `cl.proto.protos`
3271 /// recursively, so inner functions' traces are reported.
3272 pub fn trace_exit_hit_summary(
3273 &self,
3274 cl: crate::runtime::heap::Gc<crate::runtime::function::LuaClosure>,
3275 ) -> Vec<(u32, Vec<u32>)> {
3276 fn walk(
3277 proto: crate::runtime::heap::Gc<crate::runtime::function::Proto>,
3278 out: &mut Vec<(u32, Vec<u32>)>,
3279 ) {
3280 for ct in proto.traces.borrow().iter() {
3281 let counts: Vec<u32> = ct.exit_hit_counts.iter().map(|c| c.get()).collect();
3282 out.push((ct.head_pc, counts));
3283 }
3284 for inner in proto.protos.iter() {
3285 walk(*inner, out);
3286 }
3287 }
3288 let mut out: Vec<(u32, Vec<u32>)> = Vec::new();
3289 walk(cl.proto, &mut out);
3290 out
3291 }
3292
3293 /// P15-A v0 — surface every side-exit slot whose hit count is
3294 /// `>= HOTEXIT_THRESHOLD` across every trace reachable from
3295 /// `cl.proto` (recursively walking `proto.protos`). Returned
3296 /// entries are side-trace candidates: each carries the parent
3297 /// trace's `(head_proto, head_pc)`, the exit's index in the
3298 /// parent's `exit_hit_counts`, and the side trace's natural
3299 /// entry shape (`cont_pc` + `exit_tags`).
3300 ///
3301 /// Layout of `exit_hit_counts` (mirrored by the iter):
3302 /// - `[0..per_exit_inline.len())` → `InlineSideExit` (cont_pc +
3303 /// window-sized exit_tags).
3304 /// - `[per_exit_inline.len()..inline.len() + per_exit_tags.len())`
3305 /// → `per_exit_tags[i]` (per-cont_pc caller-window tags).
3306 /// - Last slot → global clean-tail (cont_pc = `head_pc`,
3307 /// exit_tags = `ct.exit_tags`).
3308 pub fn hot_exit_iter(
3309 &self,
3310 cl: crate::runtime::heap::Gc<crate::runtime::function::LuaClosure>,
3311 ) -> Vec<crate::jit::trace::HotExitInfo> {
3312 use crate::jit::trace::{HOTEXIT_THRESHOLD, HotExitInfo};
3313 fn walk(
3314 proto: crate::runtime::heap::Gc<crate::runtime::function::Proto>,
3315 out: &mut Vec<HotExitInfo>,
3316 ) {
3317 for ct in proto.traces.borrow().iter() {
3318 let inline_n = ct.per_exit_inline.len();
3319 let tags_n = ct.per_exit_tags.len();
3320 debug_assert_eq!(
3321 ct.exit_hit_counts.len(),
3322 inline_n + tags_n + 1,
3323 "exit_hit_counts layout invariant violated"
3324 );
3325 for (idx, cell) in ct.exit_hit_counts.iter().enumerate() {
3326 let hits = cell.get();
3327 if hits < HOTEXIT_THRESHOLD {
3328 continue;
3329 }
3330 let (cont_pc, exit_tags) = if idx < inline_n {
3331 let ent = &ct.per_exit_inline[idx];
3332 (ent.cont_pc, ent.exit_tags.clone())
3333 } else if idx < inline_n + tags_n {
3334 let (pc, tags) = &ct.per_exit_tags[idx - inline_n];
3335 (*pc, tags.clone())
3336 } else {
3337 (ct.head_pc, ct.exit_tags.clone())
3338 };
3339 out.push(HotExitInfo {
3340 head_proto: proto,
3341 head_pc: ct.head_pc,
3342 exit_idx: idx,
3343 hits,
3344 cont_pc,
3345 exit_tags,
3346 });
3347 }
3348 }
3349 for inner in proto.protos.iter() {
3350 walk(*inner, out);
3351 }
3352 }
3353 let mut out: Vec<HotExitInfo> = Vec::new();
3354 walk(cl.proto, &mut out);
3355 out
3356 }
3357
3358 /// P12-S5-B — sum of NewTable sites that actually took the
3359 /// sunk-emit path across every successfully compiled trace on
3360 /// this Vm. Each counted site skips its heap `Gc<Table>`
3361 /// allocation per dispatch; the array part lives as Cranelift
3362 /// `Variable`s for the duration of the trace.
3363 pub fn trace_sunk_alloc_count(&self) -> u64 {
3364 self.jit.counters.sunk_alloc
3365 }
3366
3367 /// P12-S5-C — sum of materialise-helper emit sites across every
3368 /// successfully compiled trace on this Vm. Each unit is a
3369 /// (site × cmp side-exit) pair whose IR reconstructs a heap
3370 /// `Gc<Table>` from the virt slots on deopt — proves S5-C
3371 /// emit is wiring materialise into the right side-exits.
3372 pub fn trace_materialize_emit_count(&self) -> u64 {
3373 self.jit.counters.materialize_emit
3374 }
3375
3376 /// P12-S7-A diagnostic — total `Op::Closure` ops the trace JIT
3377 /// lowered to the `luna_jit_op_closure` helper. Each emitted op
3378 /// replaces a `Heap::new_closure_inline` call on the dispatch
3379 /// path; the count is static (one per matching op per compiled
3380 /// trace), summed at compile success.
3381 pub fn trace_closure_emit_count(&self) -> u64 {
3382 self.jit.counters.closure_emit
3383 }
3384
3385 /// v2.0 Stage 7 polish 6 fire experiment — see
3386 /// [`crate::vm::jit_state::JitCounters::per_exit_inline_compiled`].
3387 /// Number of compiled traces whose `per_exit_inline.len() > 0`
3388 /// (depth>0 inlined cmp side-exits emitted).
3389 pub fn trace_per_exit_inline_compiled_count(&self) -> u64 {
3390 self.jit.counters.per_exit_inline_compiled
3391 }
3392
3393 /// v2.0 Stage 7 polish 6 fire experiment — see
3394 /// [`crate::vm::jit_state::JitCounters::per_exit_inline_dispatchable`].
3395 /// Number of compiled traces with `per_exit_inline.len() > 0` AND
3396 /// `dispatchable == true` — i.e. the count of compiled traces
3397 /// that would actually exercise the AOT polish 6 chain-reloc +
3398 /// deploy-resolver path.
3399 pub fn trace_per_exit_inline_dispatchable_count(&self) -> u64 {
3400 self.jit.counters.per_exit_inline_dispatchable
3401 }
3402
3403 /// P12-S4-step1 diagnostic — max `inline_depth` ever seen on any
3404 /// `RecordedOp` pushed by the recorder. Tells tests + tuning
3405 /// whether a self-recursive function actually walked the depth
3406 /// tracker past 0. Saturates at `MAX_INLINE_DEPTH`. Persists
3407 /// across traces and Vm activations; reset only on `Vm::new`.
3408 pub fn trace_max_depth_seen(&self) -> u8 {
3409 self.jit.max_depth_seen
3410 }
3411
3412 /// P12-S4-step4b — last live Lua frame (the trace head's frame at
3413 /// dispatch time). The frame-materialization helper reads `.base`
3414 /// to compute offsets for each inlined frame's window.
3415 #[doc(hidden)]
3416 pub fn jit_last_lua_frame(&self) -> Option<Frame> {
3417 match self.frames.last() {
3418 Some(CallFrame::Lua(f)) => Some(*f),
3419 _ => None,
3420 }
3421 }
3422
3423 /// v2.0 Track TL Phase 2 — read-only borrow of the current call
3424 /// stack, for the [`crate::vm::inspect`] pure-read accessors used
3425 /// by `luna-tools` (`luna-profile`'s sampler walks this from
3426 /// inside a `Count` hook). Sibling-module scope: not part of the
3427 /// public embedder surface, but `inspect::frames_for_profile` is.
3428 #[doc(hidden)]
3429 pub(super) fn inspect_frames(&self) -> &[CallFrame] {
3430 &self.frames
3431 }
3432
3433 /// P12-S4-step4b — ensure the value stack covers indices
3434 /// `[0..need)`. Extends with Nil if shorter. Called by the
3435 /// frame-materialization helper before pushing an inlined frame
3436 /// whose register window may exceed the current stack length.
3437 #[doc(hidden)]
3438 pub fn jit_ensure_stack(&mut self, need: usize) {
3439 if self.stack.len() < need {
3440 self.stack.resize(need, Value::Nil);
3441 }
3442 }
3443
3444 /// P12-S7-C — trace JIT path for `Op::Close A`. Predicts whether
3445 /// `__close` handlers would run (any active tbc slot ≥ from
3446 /// holding a non-nil/false Value); if so, parks a deopt sentinel
3447 /// in `jit_pending_err` and returns 1 (helper-side bool) so the
3448 /// IR branches to the deopt block. Otherwise performs the safe
3449 /// part of close — `close_from(from)` to close open upvals +
3450 /// drop any drained tbc entries ≥ from — and returns 0.
3451 ///
3452 /// Returns are i64-shaped so the cranelift import sig stays
3453 /// trivial (i64 → i64 mapping).
3454 #[doc(hidden)]
3455 pub fn jit_op_close(&mut self, start_offset: u32) -> i64 {
3456 if self.jit.pending_err.is_some() {
3457 return 1;
3458 }
3459 let Some(f) = self.jit_last_lua_frame() else {
3460 self.jit.pending_err = Some(self.rt_err("JIT op_close: no Lua frame"));
3461 return 1;
3462 };
3463 let from = f.base + start_offset;
3464 let has_handler = self.tbc.iter().any(|&s| {
3465 s >= from && {
3466 let v = self.stack[s as usize];
3467 !matches!(v, Value::Nil | Value::Bool(false))
3468 }
3469 });
3470 if has_handler {
3471 self.jit.pending_err =
3472 Some(self.rt_err("JIT deopt: Op::Close with active tbc handler"));
3473 return 1;
3474 }
3475 self.close_from(from);
3476 // Drain any tbc entries ≥ from (they're nil/false stubs the
3477 // interpreter's drive_close would have skipped silently).
3478 while let Some(&s) = self.tbc.last() {
3479 if s < from {
3480 break;
3481 }
3482 self.tbc.pop();
3483 }
3484 0
3485 }
3486
3487 /// P12-S7-B — spill the trace's current value for a register to
3488 /// the underlying `vm.stack[base + slot_offset]`. Required before
3489 /// an `Op::Closure` whose inner proto has an `in_stack: true`
3490 /// upval at `slot_offset` — the helper's `find_or_create_upval`
3491 /// captures a live pointer to `vm.stack[base + slot_offset]`,
3492 /// which must hold the right value at call time (trace IR's
3493 /// Variable hasn't yet been written back).
3494 ///
3495 /// Parameters arrive as i64 from the IR: `slot_offset` is the
3496 /// caller-frame register index (`u32` in practice, depth=0
3497 /// only — S7-B doesn't support depth>0 Closure); `tag` is the
3498 /// `crate::runtime::value::raw` byte for the slot's RegKind;
3499 /// `raw_bits` is the trace Variable's `use_var` payload
3500 /// (i64-shaped — Float is its bit-pattern, Table/Closure is the
3501 /// raw `Gc::as_ptr` cast).
3502 #[doc(hidden)]
3503 pub fn jit_spill_stack(&mut self, slot_offset: u32, tag: u8, raw_bits: u64) {
3504 let Some(f) = self.jit_last_lua_frame() else {
3505 self.jit.pending_err =
3506 Some(self.rt_err("JIT spill: no Lua frame on jit_last_lua_frame()"));
3507 return;
3508 };
3509 let idx = (f.base as usize) + (slot_offset as usize);
3510 if self.stack.len() <= idx {
3511 self.stack.resize(idx + 1, Value::Nil);
3512 }
3513 // SAFETY: caller (trace JIT IR emit) provides matching
3514 // `(tag, raw_bits)` — same shape produced by Value::unpack.
3515 let v = unsafe {
3516 crate::runtime::Value::pack(tag, crate::runtime::value::RawVal { zero: raw_bits })
3517 };
3518 self.stack[idx] = v;
3519 }
3520
3521 /// P12-S12-B-v2 — trace JIT path for `Op::TForCall A 0 C`.
3522 /// Mirrors the interp arm (this file ~L5316): copies the
3523 /// generator/state/control triple from `R[A..=A+2]` to
3524 /// `R[A+4..=A+6]` (resizing the stack if needed), then enters
3525 /// the iterator function via `begin_call`. v2 only handles
3526 /// `Value::Native` iterators (the canonical `ipairs_iter` /
3527 /// `next` builtins) — a Lua-closure iterator would push a Lua
3528 /// frame mid-trace, breaking `recording_frame_base`, so we
3529 /// deopt by parking a `pending_err` and returning `-1`.
3530 ///
3531 /// `slot_offset` is the caller-frame register index (=
3532 /// `inst.a()` decoded from a u32-wide field). `nvars` is
3533 /// `inst.c() as i32` — the caller's expected return count.
3534 /// P12-S12-C v1 — refresh only the raw payload of
3535 /// `vm.stack[base + slot_offset]`, preserving its existing
3536 /// `Value` tag. The caller (trace JIT Op::Concat body emit)
3537 /// uses this when the slot's `RegKind` is `Unset` (no compile-
3538 /// time tag info; commonly `Str` slots which the trace doesn't
3539 /// model). The interp's previous execution of the same op
3540 /// already populated the slot with the right tag — the trace
3541 /// only needs to swap in its current raw value.
3542 #[doc(hidden)]
3543 pub fn jit_stack_update_raw(&mut self, slot_offset: u32, raw_bits: u64) {
3544 let Some(f) = self.jit_last_lua_frame() else {
3545 return;
3546 };
3547 let idx = (f.base as usize) + (slot_offset as usize);
3548 if idx >= self.stack.len() {
3549 return;
3550 }
3551 let (tag, _) = self.stack[idx].unpack();
3552 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
3553 self.stack[idx] = unsafe {
3554 crate::runtime::Value::pack(tag, crate::runtime::value::RawVal { zero: raw_bits })
3555 };
3556 }
3557
3558 /// P12-S12-C v1 — trace JIT path for `Op::Concat A B`.
3559 ///
3560 /// Mirrors the interp arm (this file ~L5112): `self.top =
3561 /// base + a + n; concat_run(base + a)`. Result lands at
3562 /// `vm.stack[base + a]`. Returns `0` on success, `-1` on
3563 /// deopt (any error from `concat_run` OR detection that the
3564 /// metamethod path was taken — `concat_run` returns `Ok(())`
3565 /// after `begin_meta_call` which has pushed a Lua frame the
3566 /// trace can't safely continue past).
3567 ///
3568 /// The frame-push detection uses `pre/post frames.len()` and
3569 /// unwinds any pushed frames before deopting, so the
3570 /// dispatcher's existing deopt path sees a clean stack.
3571 #[doc(hidden)]
3572 pub fn jit_op_concat(&mut self, slot_offset: u32, n: i32) -> i64 {
3573 if self.jit.pending_err.is_some() {
3574 return -1;
3575 }
3576 let Some(f) = self.jit_last_lua_frame() else {
3577 self.jit.pending_err = Some(self.rt_err("JIT Concat: no Lua frame"));
3578 return -1;
3579 };
3580 let abs_a = f.base + slot_offset;
3581 self.top = abs_a + n as u32;
3582 let pre_frames = self.frames.len();
3583 let result = self.concat_run(abs_a);
3584 let post_frames = self.frames.len();
3585 // Frame-push = metamethod path taken (begin_meta_call pushed
3586 // a Lua frame). The trace can't continue past it; unwind +
3587 // deopt so interp redoes Op::Concat in the slow path.
3588 while self.frames.len() > pre_frames {
3589 frames_pop_sync(&mut self.frames, &mut self.frames_top);
3590 }
3591 if let Err(e) = result {
3592 self.jit.pending_err = Some(e);
3593 return -1;
3594 }
3595 if post_frames > pre_frames {
3596 self.jit.pending_err = Some(self.rt_err("JIT Concat: __concat metamethod path"));
3597 return -1;
3598 }
3599 0
3600 }
3601
3602 /// P14-S14-B v2 — pop a reusable `Vec<u8>` from the JIT
3603 /// accumulator buffer pool, returning a raw pointer. The trace
3604 /// fn's IR holds this pointer in a stack slot through the loop
3605 /// and calls `jit_str_buf_extend` per iter. If the pool is
3606 /// empty, allocate fresh.
3607 ///
3608 /// Safety: the returned pointer is valid until
3609 /// `jit_str_buf_release` is called or the Vm is dropped. The
3610 /// caller MUST not retain it across `enter_jit` boundaries.
3611 #[doc(hidden)]
3612 pub fn jit_str_buf_acquire(&mut self) -> *mut Vec<u8> {
3613 let buf = self.jit.str_buf_pool.pop().unwrap_or_default();
3614 // Move into a Box so the pointer is stable until release.
3615 Box::into_raw(Box::new(buf))
3616 }
3617
3618 /// P14-S14-B v2 — return a previously-acquired buffer to the
3619 /// pool, dropping any excess past `jit_str_buf_pool_cap`. The
3620 /// buffer is `clear`ed (capacity retained) so the next acquire
3621 /// gets a ready-to-extend Vec.
3622 ///
3623 /// Safety: `buf` must have been returned by a prior
3624 /// `jit_str_buf_acquire` on the same Vm.
3625 #[doc(hidden)]
3626 #[allow(clippy::not_unsafe_ptr_arg_deref)] // JIT helper: `buf` round-trips through `Box::into_raw`; SAFETY documented below.
3627 pub fn jit_str_buf_release(&mut self, buf: *mut Vec<u8>) {
3628 if buf.is_null() {
3629 return;
3630 }
3631 // SAFETY: `ptr` round-trips through `Box::into_raw` set up earlier in this dispatch (or owned by a long-lived VM handle); ownership re-acquired here.
3632 let mut owned = unsafe { Box::from_raw(buf) };
3633 owned.clear();
3634 if self.jit.str_buf_pool.len() < self.jit.str_buf_pool_cap {
3635 self.jit.str_buf_pool.push(*owned);
3636 }
3637 // Else: drop the buffer.
3638 }
3639
3640 /// P14-S14-B v2 — append a LuaStr's bytes to the accumulator
3641 /// buffer. The trace IR computes the `str_ptr` (= raw bits of
3642 /// the piece slot) and passes it through; we treat it as a
3643 /// `*mut LuaStr` and append its bytes.
3644 ///
3645 /// Returns 0 on success, -1 if the piece isn't a Str (would
3646 /// trip __concat metamethod path → deopt to interp).
3647 ///
3648 /// Safety: `buf` from prior `acquire`; `str_ptr` from the
3649 /// trace's piece slot raw bits.
3650 #[doc(hidden)]
3651 #[allow(clippy::not_unsafe_ptr_arg_deref)] // JIT helper: `buf` from prior `acquire`; `str_ptr` from trace piece slot; SAFETY documented below.
3652 pub fn jit_str_buf_extend(&mut self, buf: *mut Vec<u8>, str_ptr: i64) -> i64 {
3653 if buf.is_null() || str_ptr == 0 {
3654 return -1;
3655 }
3656 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
3657 let buf = unsafe { &mut *buf };
3658 let lua_str_ptr = str_ptr as *const crate::runtime::string::LuaStr;
3659 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
3660 let bytes = unsafe { crate::runtime::string::bytes_of(lua_str_ptr) };
3661 buf.extend_from_slice(bytes);
3662 0
3663 }
3664
3665 /// P14-S14-B v2 — drain the accumulator buffer into a fresh
3666 /// `LuaStr` via `heap.intern`, returning the raw ptr bits for
3667 /// the trace to write into the accumulator slot.
3668 ///
3669 /// Returns the LuaStr ptr as i64 on success, 0 on overflow
3670 /// (the v2 hard cap; the trace deopts).
3671 ///
3672 /// Safety: `buf` from prior `acquire`. The buffer is left
3673 /// CLEAR (drained) ready for `release`.
3674 #[doc(hidden)]
3675 #[allow(clippy::not_unsafe_ptr_arg_deref)] // JIT helper: `buf` from prior `acquire`; SAFETY documented below.
3676 pub fn jit_str_buf_intern(&mut self, buf: *mut Vec<u8>) -> i64 {
3677 if buf.is_null() {
3678 return 0;
3679 }
3680 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
3681 let buf = unsafe { &mut *buf };
3682 let bytes = std::mem::take(buf);
3683 // v2 hard cap at 256KB per RFC Q3.
3684 if bytes.len() > 256 * 1024 {
3685 return 0;
3686 }
3687 let gc = self.heap.intern(&bytes);
3688 gc.as_ptr() as i64
3689 }
3690
3691 /// P12-S12-B v2/v3/v4 — trace JIT helper for `Op::TForCall A 0 C`.
3692 ///
3693 /// v2 base: copy R[A..=A+2] → R[A+4..=A+6] + `begin_call`.
3694 /// v3: ipairs `inext` fast path at the top — skip begin_call
3695 /// when R[A]=Native(ipairs_iter), R[A+1]=Table no-mt,
3696 /// R[A+2]=Int.
3697 /// v4: batched out-ptr writeback — fill ctrl/key/val raws into
3698 /// caller-provided buffers + return R[A+4]'s tag byte. Lets
3699 /// emit skip 3 separate `luna_jit_stack_load` calls and 1
3700 /// `luna_jit_stack_tag` call by reading the buffer via
3701 /// cranelift `stack_load` IR instead. Returns -1 on deopt.
3702 #[doc(hidden)]
3703 #[allow(clippy::not_unsafe_ptr_arg_deref)] // JIT helper: `ctrl_out`/`key_out`/`val_out` are caller-stack buffers from Cranelift-emitted prologue; SAFETY documented below.
3704 pub fn jit_op_tforcall(
3705 &mut self,
3706 slot_offset: u32,
3707 nvars: i32,
3708 ctrl_out: *mut i64,
3709 key_out: *mut i64,
3710 val_out: *mut i64,
3711 ) -> i64 {
3712 if self.jit.pending_err.is_some() {
3713 return -1;
3714 }
3715 let Some(f) = self.jit_last_lua_frame() else {
3716 self.jit.pending_err = Some(self.rt_err("JIT TForCall: no Lua frame"));
3717 return -1;
3718 };
3719 let abs = f.base + slot_offset;
3720 let need = (abs + 7) as usize;
3721 if self.stack.len() < need {
3722 self.stack.resize(need, Value::Nil);
3723 }
3724 // v3 fast path.
3725 let took_fast_path = if let Value::Native(n) = self.stack[abs as usize]
3726 && std::ptr::fn_addr_eq(
3727 n.f,
3728 crate::vm::builtins::ipairs_iter as crate::runtime::value::NativeFn,
3729 )
3730 && let Value::Table(t) = self.stack[(abs + 1) as usize]
3731 && t.metatable().is_none()
3732 && let Value::Int(i) = self.stack[(abs + 2) as usize]
3733 {
3734 let next_i = i.wrapping_add(1);
3735 let v = t.get_int(next_i);
3736 if v.is_nil() {
3737 self.stack[(abs + 4) as usize] = Value::Nil;
3738 } else {
3739 self.stack[(abs + 4) as usize] = Value::Int(next_i);
3740 if (nvars as usize) >= 2 {
3741 self.stack[(abs + 5) as usize] = v;
3742 }
3743 for j in 2..nvars as usize {
3744 let slot = abs + 4 + j as u32;
3745 if (slot as usize) < self.stack.len() {
3746 self.stack[slot as usize] = Value::Nil;
3747 }
3748 }
3749 }
3750 true
3751 } else {
3752 false
3753 };
3754 if !took_fast_path {
3755 // v2 slow path: copy R[A..=A+2] → R[A+4..=A+6], then
3756 // route through begin_call. Lua-closure iters would push
3757 // a Lua frame mid-trace → deopt.
3758 self.stack[(abs + 4) as usize] = self.stack[abs as usize];
3759 self.stack[(abs + 5) as usize] = self.stack[(abs + 1) as usize];
3760 self.stack[(abs + 6) as usize] = self.stack[(abs + 2) as usize];
3761 if !matches!(self.stack[abs as usize], Value::Native(_)) {
3762 self.jit.pending_err = Some(self.rt_err("JIT TForCall: non-Native iter (v2 only)"));
3763 return -1;
3764 }
3765 if let Err(e) = self.begin_call(abs + 4, Some(2), nvars, false) {
3766 self.jit.pending_err = Some(e);
3767 return -1;
3768 }
3769 }
3770 // v4 batched writeback — fill the caller's buffers with the
3771 // raw bits of R[A+2] / R[A+4] / R[A+5] so the trace IR can
3772 // reload via cranelift `stack_load` instead of separate
3773 // `luna_jit_stack_load` helper calls.
3774 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
3775 let ctrl_raw = unsafe { self.stack[(abs + 2) as usize].unpack().1.zero };
3776 let (key_tag, key_rv) = self.stack[(abs + 4) as usize].unpack();
3777 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
3778 let key_raw = unsafe { key_rv.zero };
3779 let val_raw = if (nvars as usize) >= 2 {
3780 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
3781 unsafe { self.stack[(abs + 5) as usize].unpack().1.zero }
3782 } else {
3783 0u64
3784 };
3785 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
3786 unsafe {
3787 ctrl_out.write(ctrl_raw as i64);
3788 key_out.write(key_raw as i64);
3789 val_out.write(val_raw as i64);
3790 }
3791 key_tag as i64
3792 }
3793
3794 /// P12-S12-B-v2 — load the raw `i64` payload of
3795 /// `vm.stack[base + slot_offset]` for the active trace's head
3796 /// Lua frame. Used to reload trace IR `Variable`s after a
3797 /// helper has written to `vm.stack` directly (e.g. TForCall's
3798 /// iter results land at `R[A+4..A+4+nvars]`).
3799 #[doc(hidden)]
3800 pub fn jit_stack_load(&mut self, slot_offset: u32) -> i64 {
3801 let Some(f) = self.jit_last_lua_frame() else {
3802 return 0;
3803 };
3804 let idx = (f.base as usize) + (slot_offset as usize);
3805 if idx >= self.stack.len() {
3806 return 0;
3807 }
3808 let v = self.stack[idx];
3809 let (_, raw) = v.unpack();
3810 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
3811 unsafe { raw.zero as i64 }
3812 }
3813
3814 /// P12-S12-B-v2 — read the tag byte of
3815 /// `vm.stack[base + slot_offset]`. Used by `Op::TForLoop` emit
3816 /// to dispatch on the iterator's return-key tag at runtime
3817 /// (`raw::NIL` → loop end exit, `raw::INT` → continue, other →
3818 /// deopt for v2).
3819 #[doc(hidden)]
3820 pub fn jit_stack_tag(&mut self, slot_offset: u32) -> u8 {
3821 let Some(f) = self.jit_last_lua_frame() else {
3822 return crate::runtime::value::raw::NIL;
3823 };
3824 let idx = (f.base as usize) + (slot_offset as usize);
3825 if idx >= self.stack.len() {
3826 return crate::runtime::value::raw::NIL;
3827 }
3828 self.stack[idx].unpack().0
3829 }
3830
3831 /// P12-S4-step4b — push a Lua frame onto the call stack with
3832 /// JIT-known metadata. Used by `luna_jit_trace_materialize_frames`
3833 /// at trace side-exits to recreate the inlined call activations
3834 /// the lowerer compiled past. The contract (enforced by the
3835 /// lowerer's pre-emit pass): `cl.proto` is non-vararg,
3836 /// `nresults` is the caller's expected count (today always 1
3837 /// because the lowerer bails Op::Call C != 2), and the caller
3838 /// has already called `jit_ensure_stack` to cover
3839 /// `[0..base + cl.proto.max_stack)`.
3840 #[doc(hidden)]
3841 pub fn jit_push_inlined_frame(
3842 &mut self,
3843 cl: Gc<LuaClosure>,
3844 base: u32,
3845 pc: u32,
3846 nresults: i32,
3847 ) {
3848 frames_push_sync(
3849 &mut self.frames,
3850 &mut self.frames_top,
3851 CallFrame::Lua(Frame {
3852 closure: cl,
3853 base,
3854 pc,
3855 // Lua call ABI: callee R[0] sits at caller R[A+1], so
3856 // callee.base = caller.base + A + 1; func_slot is
3857 // caller.base + A = callee.base - 1.
3858 func_slot: base - 1,
3859 n_varargs: 0,
3860 nresults,
3861 hook_oldpc: u32::MAX,
3862 from_c: false,
3863 tm: None,
3864 is_hook: false,
3865 tailcalls: 0,
3866 }),
3867 );
3868 }
3869
3870 /// Toggle precompiled-chunk loading. Default `true`. Sandbox embedders
3871 /// should set to `false` so `load`/`loadstring` reject bytecode input
3872 /// (which bypasses parser limits and could exploit verifier gaps).
3873 pub fn set_bytecode_loading(&mut self, enabled: bool) {
3874 self.bytecode_loading = enabled;
3875 }
3876
3877 /// Current bytecode-loading gate state.
3878 pub fn bytecode_loading(&self) -> bool {
3879 self.bytecode_loading
3880 }
3881
3882 /// Toggle PUC `.luac` bytecode loading. Default `false` — PUC
3883 /// bytecode is a strictly larger trust surface than luna's own dump
3884 /// format (third-party toolchain bugs, malformed chunks, unknown
3885 /// opcode shapes). Enable only for trusted PUC chunks. Per-dialect
3886 /// translators (Phase LB Wave 2) live in `crate::vm::dump::puc`.
3887 pub fn set_puc_bytecode_loading(&mut self, enabled: bool) {
3888 self.puc_bytecode_loading = enabled;
3889 }
3890
3891 /// Current PUC bytecode-loading gate state.
3892 pub fn puc_bytecode_loading(&self) -> bool {
3893 self.puc_bytecode_loading
3894 }
3895
3896 /// Default loader input budget — 256 MiB.
3897 ///
3898 /// `Vm::load` and the Lua-level `load(reader, ...)` both refuse
3899 /// sources whose byte length crosses this cap, returning the
3900 /// PUC-shaped `not enough memory` error rather than letting the
3901 /// host allocator try (and crash) to hold the next chunk.
3902 pub const DEFAULT_LOADER_INPUT_BUDGET: usize = 256 * 1024 * 1024;
3903
3904 /// Set the loader input byte budget (see
3905 /// [`Vm::DEFAULT_LOADER_INPUT_BUDGET`]). Pass `usize::MAX` to
3906 /// effectively disable. Smaller caps are honored verbatim — a 0
3907 /// cap rejects every non-empty source.
3908 pub fn set_loader_input_budget(&mut self, bytes: usize) {
3909 self.loader_input_budget = bytes;
3910 }
3911
3912 /// Current loader input byte budget.
3913 pub fn loader_input_budget(&self) -> usize {
3914 self.loader_input_budget
3915 }
3916
3917 /// Take the error traceback captured at the latest error point and
3918 /// reset it. Embedders should call this immediately after a failed
3919 /// `call_value`/`eval`/`call`/etc. — the next public `call_value`
3920 /// entry clears it. Returns `None` if no error was in flight.
3921 pub fn take_error_traceback(&mut self) -> Option<String> {
3922 self.error_traceback
3923 .take()
3924 .map(|b| String::from_utf8_lossy(&b).into_owned())
3925 }
3926
3927 /// Arm the soft memory cap (P09 embedding). The run loop checks the
3928 /// heap's tracked byte usage between dispatch turns; on overshoot it
3929 /// first runs a full collect, and if `bytes` still exceeds the cap it
3930 /// raises a catchable `"memory cap exceeded"` Lua error and disarms
3931 /// itself (fire-once: re-arm before the next `call_value` if reusing
3932 /// the Vm across requests). `None` removes the cap. The accounting is
3933 /// approximate — internal Vec/Box capacity overhead is not tracked,
3934 /// so embedders should size the cap with ~2× margin over the desired
3935 /// hard limit and additionally bound the Vm's lifetime (drop after
3936 /// each request).
3937 pub fn set_memory_cap(&mut self, cap: Option<usize>) {
3938 self.heap.mem_cap = cap;
3939 }
3940
3941 /// Approximate bytes the heap is currently holding. Object shells plus
3942 /// every table's internal array/hash boxes (tracked via
3943 /// `Heap::apply_bytes_delta` in `set`/`rehash`/`ensure_*`). Proto
3944 /// bytecode and closure upvalue slices still go uncounted — this is a
3945 /// lower bound, not a precise `malloc_stats`-style total.
3946 pub fn memory_used(&self) -> usize {
3947 self.heap.bytes()
3948 }
3949
3950 /// Read upvalue slot `i` of the native function currently on top of the
3951 /// dispatch chain (the one whose body is executing). Returns `Value::Nil`
3952 /// when no native is running. Public so the C ABI trampoline can fetch
3953 /// the host C function pointer it stashed there at registration time.
3954 pub fn running_native_upvalue(&self, i: usize) -> Value {
3955 match self.running_natives.last() {
3956 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
3957 Some(nc) => unsafe {
3958 let upvals = &(*nc.as_ptr()).upvals;
3959 upvals.get(i).copied().unwrap_or(Value::Nil)
3960 },
3961 None => Value::Nil,
3962 }
3963 }
3964
3965 /// Register a table for finalization if its (just-set) metatable carries a
3966 /// `__gc` metamethod (PUC luaC_checkfinalizer at setmetatable time — adding
3967 /// `__gc` to the metatable afterwards does not retroactively register).
3968 pub(crate) fn check_finalizer(&mut self, t: Gc<Table>) {
3969 if !self.get_mm(Value::Table(t), Mm::Gc).is_nil() {
3970 self.heap.register_finalizable(t);
3971 }
3972 }
3973
3974 /// Same as [`Self::check_finalizer`] for a userdata. PUC 5.1 attaches the
3975 /// finalizer to the proxy produced by `newproxy(true)` once its metatable
3976 /// gains `__gc`. gc.lua's "testing userdata" section sets `__gc` on the
3977 /// metatable that `newproxy` returned, which then needs to flow through.
3978 /// Kept available for the future 5.2+ `lua_setmetatable` path (which
3979 /// would re-check at metatable-set time); luna's only userdata
3980 /// finalizables today come via `newproxy`, which registers itself.
3981 #[allow(dead_code)]
3982 pub(crate) fn check_finalizer_userdata(&mut self, u: Gc<crate::runtime::Userdata>) {
3983 if !self.get_mm(Value::Userdata(u), Mm::Gc).is_nil() {
3984 self.heap.register_finalizable_userdata(u);
3985 }
3986 }
3987
3988 /// Run pending `__gc` finalizers (objects the collector resurrected for
3989 /// finalization). Finalizer errors are swallowed — PUC turns them into a
3990 /// warning; they must never propagate to the mutator. Reentrancy-guarded.
3991 fn run_finalizers(&mut self) {
3992 let _ = self.run_finalizers_or_err();
3993 }
3994
3995 fn run_finalizers_or_err(&mut self) -> Result<(), LuaError> {
3996 if self.gc_finalizing {
3997 return Ok(());
3998 }
3999 let pending = self.heap.take_tobefnz();
4000 if pending.is_empty() {
4001 return Ok(());
4002 }
4003 self.gc_finalizing = true;
4004 let mut first_err: Option<LuaError> = None;
4005 for obj in pending {
4006 let gc = self.get_mm(obj, Mm::Gc);
4007 // PUC 5.2+ accepts any non-nil `__gc` at setmetatable time to
4008 // schedule the object for finalization (`__gc = true` is the
4009 // canonical placeholder); only call it at finalize time when it
4010 // is actually a function. gc.lua 5.2 :412 wires up exactly this
4011 // sentinel and then expects no call.
4012 let callable = matches!(gc, Value::Closure(_) | Value::Native(_));
4013 if callable {
4014 // PUC `GCTM` sets `CIST_FIN` on the new ci so
4015 // `funcnamefromfinalizer` reports `namewhat = "metamethod"`,
4016 // `name = "__gc"`. luna threads the same outcome through the
4017 // generic `pending_tm` slot: the Lua frame born from this
4018 // call consumes it in `push_frame`. Saved/restored around the
4019 // call in case the handler is a native (which never pops it).
4020 // Bare event name; `frame_name` / `c_frame_name` add the
4021 // `"__"` debug prefix for 5.2/5.3, drop it for 5.4+. Matches
4022 // the convention used by `__close`, `__index`, …
4023 let saved_tm = self.pending_tm.replace("gc");
4024 // PUC `GCTM` also sets `CIST_FIN` on the CALLER's ci before
4025 // pcall, so `getinfo(2).namewhat` inside the finalizer reads
4026 // "metamethod" (5.3 db.lua :720 wires up exactly this probe).
4027 // luna mirrors by temporarily tagging the current top Lua
4028 // frame's `tm` to "__gc" for the duration of the call.
4029 let caller_tm_idx = self
4030 .frames
4031 .iter()
4032 .rposition(|cf| matches!(cf, CallFrame::Lua(_)));
4033 let saved_caller_tm = caller_tm_idx.and_then(|i| {
4034 if let CallFrame::Lua(fr) = &mut self.frames[i] {
4035 let prev = fr.tm;
4036 fr.tm = Some("gc");
4037 Some(prev)
4038 } else {
4039 None
4040 }
4041 });
4042 if let Err(e) = self.call_value(gc, &[obj]) {
4043 // PUC 5.1 GCTM raised the finalizer's error to the
4044 // explicit `collectgarbage()` caller (`gc.lua 5.1 :255`
4045 // baselines on `not pcall(collectgarbage)`). 5.2/5.3
4046 // wrapped it in `error in __gc metamethod (msg)` first
4047 // (`callGCTM` → `luaG_runerror`) but still raised. 5.4
4048 // introduced the warning system and switched to "warn
4049 // then continue" — never re-raise, just route the
4050 // wrapped message through `warn`. gc.lua 5.5 :378 wires
4051 // up `_WARN` capture under the `if T then …` block to
4052 // baseline on the same wrapped string.
4053 if self.version >= LuaVersion::Lua54 {
4054 let inner = self.error_text(&e);
4055 let msg = format!("error in __gc metamethod ({inner})");
4056 self.emit_warn(msg.as_bytes(), false);
4057 } else if first_err.is_none() {
4058 let wrapped = if self.version >= LuaVersion::Lua52 {
4059 let inner = self.error_text(&e);
4060 let msg = format!("error in __gc metamethod ({inner})");
4061 let s = Value::Str(self.heap.intern(msg.as_bytes()));
4062 LuaError(s)
4063 } else {
4064 e
4065 };
4066 first_err = Some(wrapped);
4067 }
4068 }
4069 self.pending_tm = saved_tm;
4070 if let (Some(i), Some(prev)) = (caller_tm_idx, saved_caller_tm)
4071 && let Some(CallFrame::Lua(fr)) = self.frames.get_mut(i)
4072 {
4073 fr.tm = prev; // prev is Option<&'static str>; restore exactly
4074 }
4075 }
4076 }
4077 self.gc_finalizing = false;
4078 match first_err {
4079 Some(e) => Err(e),
4080 None => Ok(()),
4081 }
4082 }
4083
4084 /// Drive one incremental GC step (PUC `collectgarbage("step", n)`).
4085 /// Crosses up to three phases per call:
4086 /// 1. Pause → seed Propagate (`gc_start_propagate`)
4087 /// 2. Propagate → drain gray up to `budget`; on exhaustion run atomic
4088 /// (`gc_finish_atomic` → tobefnz populated; finalizers
4089 /// run via `run_finalizers`) and enter Sweep
4090 /// 3. Sweep → `gc_sweep_step` up to (residual) `budget`
4091 /// Returns true when this call completed the cycle's sweep (back to
4092 /// Pause). The budget is spent generously across phases — a large `n`
4093 /// can finish a whole cycle in one call (PUC stop-the-world step).
4094 pub(crate) fn gc_step(&mut self, budget: usize) -> bool {
4095 // Re-entry guard: never recurse — `run_finalizers` calls Lua code
4096 // that may hit a safe point and try to step again. Re-entry was OK
4097 // under STW (collect_garbage had its own guard) but here the
4098 // intermediate phase state would corrupt.
4099 if self.gc_finalizing {
4100 return false;
4101 }
4102 if self.heap.gc_phase_is_pause() {
4103 let (roots, extra) = self.gc_roots();
4104 self.heap.gc_start_propagate(&roots, &extra);
4105 }
4106 if self.heap.gc_phase_is_propagate() {
4107 if !self.heap.gc_step_propagate(budget) {
4108 return false;
4109 }
4110 self.heap.gc_finish_atomic();
4111 // any __gc scheduled by atomic — run before sweep so a finalizer
4112 // re-registering `self` re-enters the next cycle, not this sweep
4113 self.run_finalizers();
4114 }
4115 // either we just transitioned, or we entered already in Sweep, or
4116 // a finalizer started a new cycle (gc_sweep_step is a no-op then)
4117 self.heap.gc_sweep_step(budget)
4118 }
4119
4120 // ---- frames & calls ----
4121
4122 /// Begin calling stack[func_slot] with `nargs` (None: up to self.top).
4123 /// Returns true if a Lua frame was pushed (the dispatch loop continues
4124 /// there), false if a native completed inline.
4125 fn begin_call(
4126 &mut self,
4127 func_slot: u32,
4128 nargs: Option<u32>,
4129 nresults: i32,
4130 from_c: bool,
4131 ) -> Result<bool, LuaError> {
4132 let mut nargs = match nargs {
4133 Some(n) => n,
4134 None => self.top - (func_slot + 1),
4135 };
4136 // Consume `pending_is_tail` at the boundary: a tail-call op sets it
4137 // only for the immediately-following Lua activation. Native dispatch
4138 // (or `__call` resolution) below must not let it leak to the next
4139 // begin_call's frame; restore it just before push_frame for the Lua
4140 // arm so its meaning is preserved across __call chaining.
4141 let tailcalls = std::mem::take(&mut self.pending_tailcalls);
4142 // resolve __call handlers iteratively (PUC tryfuncTM loop): each handler
4143 // is inserted before the value so it becomes the first argument, and a
4144 // chain of `__call` tables resolves down to a real function.
4145 let mut chain = 0u32;
4146 loop {
4147 match self.stack[func_slot as usize] {
4148 Value::Closure(cl) => {
4149 // P11-S2c.B JIT fast path: if the Proto's body fits
4150 // the int-arith whitelist, every arg is `Value::Int`,
4151 // and the cached arity matches, skip frame setup and
4152 // run the cached native fn in-place.
4153 if self.try_jit_call_op(cl, func_slot, nargs, nresults) {
4154 self.pending_tailcalls = tailcalls;
4155 return Ok(false);
4156 }
4157 self.pending_tailcalls = tailcalls;
4158 self.push_frame(cl, func_slot, nargs, nresults, from_c)?;
4159 // P12-S4-step0 — trace-on-call trigger. The frame
4160 // we just pushed is the callee whose body the
4161 // recorder will trace. Bump the per-Proto call
4162 // counter; once it crosses `CALL_HOT_THRESHOLD`
4163 // and no other trace is in flight, snapshot the
4164 // callee's register window (R[0..max_stack]) and
4165 // begin recording at `pc=0`. This is what unlocks
4166 // tracing for functions whose body has no negative
4167 // `Op::Jmp` back-edge (`fib`, recursive helpers).
4168 //
4169 // Gated on `trace_jit_enabled`, so the default
4170 // dispatch pays a single not-taken branch.
4171 if self.jit.trace_enabled {
4172 let proto = cl.proto;
4173 let c = proto.call_hot_count.get();
4174 if c < u32::MAX / 2 {
4175 proto.call_hot_count.set(c + 1);
4176 }
4177 // P13-S13-H — relaxed call-trigger:
4178 // `c >= THRESHOLD` (was `c == THRESHOLD`) +
4179 // `!already_cached` short-circuit. Lets a
4180 // discarded short call-trigger close retry
4181 // on the next call (fib(10/15/20/25)
4182 // pathology — first capture is base-case
4183 // [Lt,Jmp,Return1]; coverage-heuristic
4184 // discards; next call gets to record at a
4185 // potentially deeper recursion point).
4186 // Without `already_cached`, the relaxed
4187 // condition would re-record over a cached
4188 // trace every call.
4189 //
4190 // P13-S13-K — additionally short-circuit on
4191 // `proto.trace_gave_up`. The S13-I discard
4192 // cap force-compiles a partial trace and
4193 // flips this flag; subsequent calls into
4194 // this Proto skip the RefCell borrow + Vec
4195 // scan entirely.
4196 if proto.trace_gave_up.get() {
4197 return Ok(true);
4198 }
4199 let call_already_cached =
4200 proto.traces.borrow().iter().any(|t| t.head_pc == 0);
4201 if c >= crate::jit::trace::CALL_HOT_THRESHOLD
4202 && self.jit.active_trace.is_none()
4203 && !call_already_cached
4204 {
4205 // The new frame is on top: index in
4206 // `self.frames` is `len() - 1`.
4207 let frame_idx = self.frames.len() - 1;
4208 // Snapshot R[0..max_stack] at the callee's
4209 // base. `push_frame` resized `self.stack`
4210 // to `base + max_stack`, so this window is
4211 // guaranteed in-bounds.
4212 let f = match &self.frames[frame_idx] {
4213 CallFrame::Lua(f) => f,
4214 _ => unreachable!("push_frame just pushed a Lua frame"),
4215 };
4216 let max_stack = cl.proto.max_stack as usize;
4217 let base_us = f.base as usize;
4218 let mut entry_tags = Vec::with_capacity(max_stack);
4219 for i in 0..max_stack {
4220 let (tag, _) = self.stack[base_us + i].unpack();
4221 entry_tags.push(tag);
4222 }
4223 self.jit.active_trace =
4224 Some(Box::new(crate::jit::trace::TraceRecord::start(
4225 cl.proto, 0, entry_tags, true,
4226 )));
4227 self.jit.recording_frame_base = frame_idx;
4228 }
4229 }
4230 return Ok(true);
4231 }
4232 Value::Native(nc) => {
4233 // v1.1 B10 Stage 2 — async-marked NativeClosure.
4234 // Route through the cooperative-yield mechanism
4235 // when async_mode is on; reject when called from
4236 // a sync `eval`/`call_value` path (would have no
4237 // executor to drive the returned future).
4238 if nc.is_async {
4239 if !self.async_mode {
4240 let s = Value::Str(
4241 self.heap.intern(b"async native called in sync context"),
4242 );
4243 self.last_error_kind = crate::vm::error::LuaErrorKind::Runtime;
4244 return Err(LuaError(s));
4245 }
4246 // Same root-up bookkeeping as the sync path:
4247 // pin args + result-count expectation so a
4248 // collection across the suspend boundary
4249 // keeps the arg window live.
4250 self.native_nresults = nresults;
4251 self.gc_top = func_slot + nargs + 1;
4252 // v1.3 Phase AS — fire the "call" hook BEFORE
4253 // building the future. Mirrors the sync native
4254 // path's `hook_call(true, nargs)` site
4255 // (`exec.rs` further down) so embedders with a
4256 // Rust debug hook installed see a Call event
4257 // for async natives identical to the sync
4258 // path. The matching "return" hook fires from
4259 // `commit_async_native_result` in
4260 // `async_drive.rs` after the future resolves.
4261 // Placement follows audit §"Open questions"
4262 // Q6: after the `native_nresults` / `gc_top`
4263 // pin, before the future is constructed, so a
4264 // hook body that triggers GC observes the
4265 // correct pinned window. On hook error the
4266 // sentinel never returns and
4267 // `pending_async_native_*` remain `None` —
4268 // the executor sees `DispatchOutcome::Error`
4269 // (audit §A.1 edge cases).
4270 self.hook_call(true, nargs)?;
4271 // Transmute the stored NativeFn back to its
4272 // real AsyncNativeFn shape. Sound because
4273 // `set_async_native` / `create_async_native`
4274 // installed an AsyncNativeFn through the
4275 // identically-sized fn-pointer slot, and the
4276 // `is_async` marker bit is what records that
4277 // fact.
4278 let async_fn: crate::vm::async_drive::AsyncNativeFn =
4279 // SAFETY: same-size fn pointers; provenance
4280 // preserved through `mem::transmute`. The
4281 // `is_async` marker is the only safe-to-call
4282 // gate, set exclusively by
4283 // `Vm::create_async_native`.
4284 unsafe { std::mem::transmute(nc.f) };
4285 let vm_ptr: *mut Vm = self;
4286 let fut = async_fn(vm_ptr, func_slot, nargs);
4287 // Stash the future + post-call context for
4288 // `drive_one` to surface to `EvalFuture::poll`.
4289 self.pending_async_native_fut = Some(fut);
4290 self.pending_async_native_ctx = Some(AsyncNativeCallCtx {
4291 func_slot,
4292 nargs,
4293 nresults,
4294 gc_top: self.gc_top,
4295 });
4296 // Sentinel Err walked up to `drive_one` (same
4297 // shape as `host_yield_pending`'s budget yield).
4298 // Value::Nil — never seen by user code.
4299 return Err(LuaError(Value::Nil));
4300 }
4301 // pcall/xpcall are yieldable: rather than calling the
4302 // protected function through the Rust stack (which cannot be
4303 // suspended), push a continuation frame and drive the call
4304 // through the interpreter loop (PUC lua_pcallk). A yield
4305 // inside it is preserved with the thread's saved frames.
4306 use crate::runtime::value::NativeFn;
4307 if std::ptr::fn_addr_eq(nc.f, nat_pcall as NativeFn) {
4308 return self.begin_pcall(func_slot, nargs, nresults);
4309 }
4310 if std::ptr::fn_addr_eq(nc.f, nat_xpcall as NativeFn) {
4311 return self.begin_xpcall(func_slot, nargs, nresults);
4312 }
4313 // pairs(t) with a __pairs metamethod calls it yieldably (PUC
4314 // luaB_pairs); without one, fall through to the plain native.
4315 if std::ptr::fn_addr_eq(nc.f, nat_pairs as NativeFn) && nargs >= 1 {
4316 let arg = self.stack[(func_slot + 1) as usize];
4317 if !self.get_mm(arg, Mm::Pairs).is_nil() {
4318 return self.begin_pairs(func_slot, nresults);
4319 }
4320 }
4321 // a native that collects (e.g. `collectgarbage`) roots up to
4322 // its own arguments — the caller's live registers all sit
4323 // below `func_slot` and stay rooted.
4324 self.native_nresults = nresults;
4325 self.gc_top = func_slot + nargs + 1;
4326 // Push the native onto the running-natives chain BEFORE
4327 // firing the call hook so that `debug.getinfo(level)` and
4328 // `arg_error` from inside the hook see this native as the
4329 // currently-running C function (db.lua :344 reads
4330 // `getinfo(2, "f").func` for the just-entered callee).
4331 // Popped after the matching return hook fires — even on
4332 // error, the pop must happen, so the body is bracketed
4333 // through a scope guard.
4334 self.running_natives.push(nc);
4335 self.running_native_slots.push((func_slot, nargs));
4336 // PUC C-call discipline: entering a C function sets
4337 // L->top to func + 1 + nargs, so a collect triggered
4338 // INSIDE the native (explicit `collectgarbage()`, or
4339 // an allocation crossing the GC threshold) roots the
4340 // whole caller window up to and including the
4341 // arguments. Without this raise the cursor is stale —
4342 // parked at some earlier, possibly much lower
4343 // safe-point — and the collect frees register-held
4344 // values of the native's own caller (UAF-C, v2.13
4345 // Track WUC). Never lower it: a re-entrant chain
4346 // (native → Lua → native) must keep the outermost
4347 // window rooted.
4348 self.gc_top = self.gc_top.max(func_slot + 1 + nargs);
4349 // PUC luaD_precall fires the "call" hook for C functions too.
4350 // A yield inside the native (coroutine.yield) propagates an
4351 // Err and the matching "return" hook fires on resume instead.
4352 if let Err(e) = self.hook_call(true, nargs) {
4353 self.running_natives.pop();
4354 self.running_native_slots.pop();
4355 return Err(e);
4356 }
4357 // P09: trap a Rust panic in the native and surface it as
4358 // a Lua error rather than letting it unwind through the
4359 // VM into the embedder. The VM's internal state may still
4360 // be inconsistent after a panic (half-pushed args,
4361 // dangling GC references), so embedders that catch this
4362 // class of error should drop and re-create the Vm — but
4363 // it's still better than tearing the host process down.
4364 // `AssertUnwindSafe` is sound because the caller is the
4365 // dispatch loop and any half-done state is fenced behind
4366 // the immediate Err return below.
4367 use std::panic::{AssertUnwindSafe, catch_unwind};
4368 let result =
4369 match catch_unwind(AssertUnwindSafe(|| (nc.f)(self, func_slot, nargs))) {
4370 Ok(r) => r,
4371 Err(payload) => {
4372 let msg = panic_payload_str(&payload);
4373 let s = Value::Str(
4374 self.heap.intern(format!("native panic: {msg}").as_bytes()),
4375 );
4376 Err(LuaError(s))
4377 }
4378 };
4379 let nret = match result {
4380 Ok(n) => n,
4381 Err(e) => {
4382 // Stash the offending native's name BEFORE the
4383 // pop so a dying coroutine's traceback snapshot
4384 // can prepend `[C]: in function '<name>'`. Use
4385 // pushglobalfuncname (PUC walks package.loaded
4386 // to qualify); fall back to "?".
4387 self.errored_native =
4388 Some(self.pushglobalfuncname(nc.f).unwrap_or_else(|| "?".into()));
4389 self.running_natives.pop();
4390 self.running_native_slots.pop();
4391 return Err(e);
4392 }
4393 };
4394 // PUC `luaD_poscall` fires the return hook BEFORE moving
4395 // results into the function's slot — at that point args
4396 // sit at `[func_slot + 1, func_slot + 1 + nargs)` and
4397 // results above them at `[func_slot + 1 + nargs, …)`.
4398 // luna's `nat_return` has already written the results
4399 // into `[func_slot, func_slot + nret)`, so we replay PUC's
4400 // layout by copying the results up past the preserved
4401 // args, firing the hook (with ftransfer = nargs + 1, so
4402 // `getlocal(2, ftransfer..)` reads results), and then
4403 // copying back for `finish_results`. db.lua :541 reads
4404 // `getinfo("r").ftransfer` + `getlocal` to inspect a
4405 // returning native's results this way.
4406 if self.hook.ret
4407 && !self.in_hook
4408 && (self.hook.func.is_some() || self.hook.rust_func.is_some())
4409 {
4410 let res_dst = func_slot + nargs + 1;
4411 let need = (res_dst + nret) as usize;
4412 if self.stack.len() < need {
4413 self.stack.resize(need, Value::Nil);
4414 }
4415 for i in (0..nret).rev() {
4416 self.stack[(res_dst + i) as usize] =
4417 self.stack[(func_slot + i) as usize];
4418 }
4419 // widen the C-frame's argument window for getlocal
4420 if let Some(slot) = self.running_native_slots.last_mut() {
4421 slot.1 = nargs + nret;
4422 }
4423 let hr = self.hook_return(true, nargs + 1, nret);
4424 if let Some(slot) = self.running_native_slots.last_mut() {
4425 slot.1 = nargs;
4426 }
4427 // restore results into the slot finish_results expects
4428 for i in 0..nret {
4429 self.stack[(func_slot + i) as usize] =
4430 self.stack[(res_dst + i) as usize];
4431 }
4432 self.running_natives.pop();
4433 self.running_native_slots.pop();
4434 hr?;
4435 } else {
4436 self.running_natives.pop();
4437 self.running_native_slots.pop();
4438 }
4439 self.finish_results(func_slot, nret, nresults);
4440 // the native may have allocated; collect with the results as
4441 // the live boundary (PUC checks GC after a call returns).
4442 self.maybe_collect_garbage(self.top);
4443 return Ok(false);
4444 }
4445 v => {
4446 let mm = self.get_mm(v, Mm::Call);
4447 if mm.is_nil() {
4448 return Err(self.call_err(v));
4449 }
4450 chain += 1;
4451 // PUC 5.5 dropped the chain cap from `MAXTAGRECUR = 200`
4452 // (the value 5.4's `lvm.c` uses) down to `MAXCCMT = 16`,
4453 // and the 5.5 test exercises the new tight bound directly
4454 // (calls.lua :225 builds a 16-deep chain and expects the
4455 // 16th to error). 5.4 calls.lua :194 instead builds a 20-
4456 // deep chain and expects it to succeed.
4457 let cap = if self.version >= crate::version::LuaVersion::Lua55 {
4458 15
4459 } else {
4460 MAX_CCMT
4461 };
4462 if chain > cap {
4463 return Err(self.rt_err("'__call' chain too long"));
4464 }
4465 // slots above shift by one; at a call site those are dead
4466 // temps of the current frame
4467 self.stack.insert(func_slot as usize, mm);
4468 if self.top > func_slot {
4469 self.top += 1;
4470 }
4471 nargs += 1;
4472 }
4473 }
4474 }
4475 }
4476
4477 fn push_frame(
4478 &mut self,
4479 cl: Gc<LuaClosure>,
4480 func_slot: u32,
4481 nargs: u32,
4482 nresults: i32,
4483 from_c: bool,
4484 ) -> Result<(), LuaError> {
4485 if func_slot + 256 > MAX_LUA_STACK {
4486 // PUC `stackerror`: a stack overflow that surfaces while the
4487 // current activation is inside an xpcall message handler is
4488 // translated by `luaD_seterrorobj` (LUA_ERRERR) to "error in
4489 // error handling". errors.lua :606 expects the inner pcall(loop)
4490 // it runs from within `xpcall(loop, msgh)`'s msgh to fail with a
4491 // message matching "error handling".
4492 let msg = if self.msgh_depth > 0 {
4493 "error in error handling"
4494 } else {
4495 "stack overflow"
4496 };
4497 return Err(self.rt_err(msg));
4498 }
4499 let proto = cl.proto;
4500 let nparams = proto.num_params as u32;
4501 // 5.5 vararg layout (PUC luaT_adjustvarargs): the extra args stay on the
4502 // stack just below the new `base`, so a named vararg can be indexed
4503 // virtually without allocating a table. Rotate `[p1..pn][e1..em]` to
4504 // `[e1..em][p1..pn]` so the fixed params land at the new base.
4505 let n_varargs = if proto.is_vararg {
4506 nargs.saturating_sub(nparams)
4507 } else {
4508 0
4509 };
4510 if n_varargs > 0 {
4511 let s = (func_slot + 1) as usize;
4512 self.stack[s..s + nargs as usize].rotate_left(nparams as usize);
4513 }
4514 let base = func_slot + 1 + n_varargs;
4515 let need = (base + proto.max_stack as u32) as usize;
4516 if self.stack.len() < need {
4517 self.stack.resize(need, Value::Nil);
4518 }
4519 // wipe the register window beyond the kept parameters (stale values —
4520 // required for GC-safety and codegen). The varargs below `base` survive.
4521 let kept = nargs.saturating_sub(n_varargs).min(nparams);
4522 // SAFETY: just resized above so `need <= stack.len()`; `base + kept <=
4523 // need` since `base + nparams <= base + max_stack = need` and `kept <=
4524 // nparams`. `slice::fill` lowers to a single memset on Copy types.
4525 unsafe {
4526 self.stack
4527 .get_unchecked_mut((base + kept) as usize..need)
4528 .fill(Value::Nil);
4529 }
4530 frames_push_sync(
4531 &mut self.frames,
4532 &mut self.frames_top,
4533 CallFrame::Lua(Frame {
4534 closure: cl,
4535 base,
4536 pc: 0,
4537 func_slot,
4538 nresults,
4539 hook_oldpc: u32::MAX,
4540 from_c,
4541 n_varargs,
4542 // single-shot consume: `close_slots` sets pending_tm before each
4543 // handler call; the next Lua frame born is that handler's.
4544 tm: self.pending_tm.take(),
4545 // `run_hook` sets `pending_is_hook` before dispatching the user
4546 // hook so its frame reports `namewhat = "hook"` via getinfo.
4547 is_hook: std::mem::take(&mut self.pending_is_hook),
4548 tailcalls: std::mem::take(&mut self.pending_tailcalls),
4549 }),
4550 );
4551 // PUC 5.1 `LUAI_COMPAT_VARARG`: populate the hidden `arg` local with
4552 // `{ n = n_varargs, [1] = e1, [2] = e2, … }`. The compiler reserved
4553 // the slot at `base + nparams`; the extras sit just below `base` from
4554 // the vararg rotate above. 5.1 db.lua :279 reads `arg.n` from a line
4555 // hook; vararg.lua's contradictory expectations were already going to
4556 // fail either way (some asserts want `arg == nil`).
4557 if proto.has_compat_vararg_arg {
4558 let arg_slot = (base + nparams) as usize;
4559 let t = self.heap.new_table();
4560 {
4561 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
4562 let tm = unsafe { t.as_mut() };
4563 for i in 0..n_varargs {
4564 let v = self.stack[(base - n_varargs + i) as usize];
4565 // bounded by `n_varargs` (≤ MAXUPVAL territory), well
4566 // below `MAX_ASIZE`
4567 let _ = tm.set_int(&mut self.heap, (i + 1) as i64, v);
4568 }
4569 let nk = Value::Str(self.heap.intern(b"n"));
4570 tm.set(&mut self.heap, nk, Value::Int(n_varargs as i64))
4571 .expect("'n' key");
4572 }
4573 // once-per-table barrier mirrors SETLIST: t is born BLACK during
4574 // Propagate and the bulk `set_int`/`set` calls above don't barrier
4575 self.heap
4576 .barrier_back(t.as_ptr() as *mut crate::runtime::heap::GcHeader);
4577 self.stack[arg_slot] = Value::Table(t);
4578 }
4579 // PUC luaD_precall fires the "call" hook with the new frame current, so
4580 // a hook calling debug.getinfo(2) sees the entered function. For a Lua
4581 // callee, PUC `luaD_hookcall` passes `p->numparams` as ntransfer (only
4582 // fixed params count — extras already live below `base`).
4583 // A frame born via OP_TailCall fires "tail call" instead (PUC
4584 // luaD_pretailcall) and skips the matching "return" hook on exit.
4585 let is_tail = self
4586 .frames
4587 .last()
4588 .and_then(|f| f.lua())
4589 .is_some_and(|f| f.tailcalls > 0);
4590 self.hook_call_with(false, nparams, is_tail)?;
4591 Ok(())
4592 }
4593
4594 /// `pcall(f, ...)` (PUC luaB_pcall): push a continuation frame, then drive
4595 /// the protected call `f` through the interpreter loop. The protected
4596 /// function and its arguments already sit at `func_slot+1..`, so calling `f`
4597 /// at `func_slot+1` lets its results land one slot above the continuation —
4598 /// the loop head then writes `true` at `func_slot` to form `true, results…`.
4599 /// Always returns `Ok(true)`: a continuation is now on the stack to be
4600 /// resolved by the loop (even when `f` is a native that already ran inline).
4601 fn begin_pcall(&mut self, func_slot: u32, nargs: u32, nresults: i32) -> Result<bool, LuaError> {
4602 if nargs == 0 {
4603 return Err(crate::vm::builtins::raise_str(
4604 self,
4605 "bad argument #1 to 'pcall' (value expected)",
4606 ));
4607 }
4608 if self.pcall_depth >= MAX_C_DEPTH {
4609 return Err(self.rt_err("C stack overflow"));
4610 }
4611 self.pcall_depth += 1;
4612 frames_push_sync(
4613 &mut self.frames,
4614 &mut self.frames_top,
4615 CallFrame::Cont(NativeCont {
4616 kind: ContKind::Pcall,
4617 func_slot,
4618 nresults,
4619 }),
4620 );
4621 // call f (slot func_slot+1) with the remaining args, asking for all
4622 // results; a yield or error inside propagates with the continuation kept
4623 // on the stack (caught by `unwind` / preserved across a yield).
4624 self.begin_call(func_slot + 1, Some(nargs - 1), -1, true)?;
4625 Ok(true)
4626 }
4627
4628 /// `xpcall(f, msgh, ...)` (PUC luaB_xpcall): like `begin_pcall`, but the
4629 /// message handler is stashed in the continuation and the arguments are
4630 /// shifted down over the handler's slot so `f`'s args are contiguous.
4631 fn begin_xpcall(
4632 &mut self,
4633 func_slot: u32,
4634 nargs: u32,
4635 nresults: i32,
4636 ) -> Result<bool, LuaError> {
4637 if nargs < 2 {
4638 return Err(crate::vm::builtins::raise_str(
4639 self,
4640 "bad argument #2 to 'xpcall' (value expected)",
4641 ));
4642 }
4643 if self.pcall_depth >= MAX_C_DEPTH {
4644 return Err(self.rt_err("C stack overflow"));
4645 }
4646 self.pcall_depth += 1;
4647 // layout: [xpcall@func_slot, f@+1, msgh@+2, a1@+3, ...]. Stash msgh and
4648 // close its gap so f's args become [f@+1, a1@+2, ...].
4649 let handler = self.stack[(func_slot + 2) as usize];
4650 // 5.1: `xpcall (f, err)` takes exactly two parameters — extra
4651 // arguments are NOT forwarded to `f` (5.2 added forwarding;
4652 // 5.1 calls f with zero args). v2.14 dialect fixture 5.1/519.
4653 let nfargs = if self.version <= crate::version::LuaVersion::Lua51 {
4654 0
4655 } else {
4656 nargs - 2
4657 };
4658 for i in 0..nfargs {
4659 self.stack[(func_slot + 2 + i) as usize] = self.stack[(func_slot + 3 + i) as usize];
4660 }
4661 self.top = func_slot + 2 + nfargs;
4662 frames_push_sync(
4663 &mut self.frames,
4664 &mut self.frames_top,
4665 CallFrame::Cont(NativeCont {
4666 kind: ContKind::Xpcall { handler },
4667 func_slot,
4668 nresults,
4669 }),
4670 );
4671 self.begin_call(func_slot + 1, Some(nfargs), -1, true)?;
4672 Ok(true)
4673 }
4674
4675 /// `pairs(t)` where `t` has a `__pairs` metamethod (PUC luaB_pairs's
4676 /// lua_callk path): drive `__pairs(t)` through the loop with a `Pairs`
4677 /// continuation so a `coroutine.yield` inside it suspends cleanly. The
4678 /// metamethod is called in `pairs`'s own slot, so its (≤4, nil-padded)
4679 /// results land exactly where `pairs`'s results belong.
4680 fn begin_pairs(&mut self, func_slot: u32, nresults: i32) -> Result<bool, LuaError> {
4681 let arg = self.stack[(func_slot + 1) as usize];
4682 let mm = self.get_mm(arg, Mm::Pairs);
4683 // layout becomes [mm@func_slot, t@func_slot+1]; call mm(t) wanting 4.
4684 self.stack[func_slot as usize] = mm;
4685 self.top = func_slot + 2;
4686 frames_push_sync(
4687 &mut self.frames,
4688 &mut self.frames_top,
4689 CallFrame::Cont(NativeCont {
4690 kind: ContKind::Pairs,
4691 func_slot,
4692 nresults,
4693 }),
4694 );
4695 self.begin_call(func_slot, Some(1), 4, true)?;
4696 Ok(true)
4697 }
4698
4699 /// The running (top) Lua frame. The interpreter only reads this while a Lua
4700 /// frame is on top — a continuation frame is never the running frame (it is
4701 /// consumed the instant the call it protects unwinds onto it).
4702 #[inline]
4703 fn top_frame(&self) -> &Frame {
4704 self.frames
4705 .last()
4706 .and_then(CallFrame::lua)
4707 .expect("running Lua frame")
4708 }
4709
4710 #[inline]
4711 fn top_frame_mut(&mut self) -> &mut Frame {
4712 self.frames
4713 .last_mut()
4714 .and_then(CallFrame::lua_mut)
4715 .expect("running Lua frame")
4716 }
4717
4718 /// Pad/announce results sitting at func_slot.
4719 pub(crate) fn finish_results(&mut self, func_slot: u32, nret: u32, wanted: i32) {
4720 // v2.3 P1B-A: capture the call's high-water-mark before
4721 // setting the new top so we can Nil-clear slots that the
4722 // call temporarily wrote but no longer holds — matching
4723 // PUC's `L->top` discipline (slots past L->top are "free"
4724 // and the next push overwrites them). Without this clear,
4725 // a stale `Value::Closure` (e.g. the called function
4726 // itself, when wanted = 0) sits at `func_slot` and a
4727 // later GC with wider `gc_top` traces it after the
4728 // closure has been freed by a previous narrow safe-point
4729 // GC → heap-buffer-overflow in `Marker::header` (UAF-A
4730 // sort.lua AA case).
4731 let prev_top = self.top as usize;
4732 if wanted < 0 {
4733 self.top = func_slot + nret;
4734 } else {
4735 let wanted = wanted as u32;
4736 let need = (func_slot + wanted) as usize;
4737 if self.stack.len() < need {
4738 self.stack.resize(need, Value::Nil);
4739 }
4740 for i in nret..wanted {
4741 self.stack[(func_slot + i) as usize] = Value::Nil;
4742 }
4743 self.top = func_slot + wanted;
4744 }
4745 let new_top = self.top as usize;
4746 let clear_end = prev_top.min(self.stack.len());
4747 if new_top < clear_end {
4748 for slot in &mut self.stack[new_top..clear_end] {
4749 *slot = Value::Nil;
4750 }
4751 }
4752 }
4753
4754 /// v1.1 B10 Stage 1 — current Lua call-frame depth (read-only).
4755 /// Used by `EvalFuture` on the bootstrap poll to compute the
4756 /// `entry_depth` it will pass to subsequent resume slices.
4757 pub(crate) fn frame_count(&self) -> usize {
4758 self.frames.len()
4759 }
4760
4761 fn take_results(&mut self, func_slot: u32) -> Vec<Value> {
4762 let nret = self.top - func_slot;
4763 let out = self.stack[func_slot as usize..(func_slot + nret) as usize].to_vec();
4764 self.stack.truncate(func_slot as usize);
4765 self.top = func_slot;
4766 out
4767 }
4768
4769 // ---- open upvalues ----
4770
4771 #[doc(hidden)]
4772 pub fn find_or_create_upval(&mut self, slot: u32) -> Gc<Upvalue> {
4773 match self.open_upvals.binary_search_by_key(&slot, |&(s, _)| s) {
4774 Ok(i) => self.open_upvals[i].1,
4775 Err(i) => {
4776 let uv = self.heap.new_upvalue(UpvalState::Open {
4777 slot,
4778 thread: self.current,
4779 });
4780 self.open_upvals.insert(i, (slot, uv));
4781 uv
4782 }
4783 }
4784 }
4785
4786 pub(crate) fn close_from(&mut self, slot: u32) {
4787 while let Some(&(s, uv)) = self.open_upvals.last() {
4788 if s < slot {
4789 break;
4790 }
4791 let v = self.stack[s as usize];
4792 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
4793 unsafe { uv.as_mut() }.set_closed(v);
4794 self.heap
4795 .barrier_forward(uv.as_ptr() as *mut crate::runtime::heap::GcHeader, v);
4796 self.open_upvals.pop();
4797 }
4798 }
4799
4800 /// Register a to-be-closed slot (TBC op / generic-for closing value).
4801 fn register_tbc(&mut self, slot: u32) -> Result<(), LuaError> {
4802 let v = self.stack[slot as usize];
4803 if matches!(v, Value::Nil | Value::Bool(false)) {
4804 return Ok(()); // nil and false are silently ignored
4805 }
4806 if self.get_mm(v, Mm::Close).is_nil() {
4807 // PUC `checkclosemth`: "variable '<name>' got a non-closable value
4808 // (a <type> value)"; the local's name comes from the running
4809 // frame's locvars at this pc.
4810 let tn = v.type_name();
4811 let f = self.top_frame();
4812 let reg = slot - f.base;
4813 let pc = (f.pc as usize).saturating_sub(1);
4814 let where_ = match crate::vm::objname::getlocalname(&f.closure.proto, reg, pc) {
4815 Some(n) => format!("variable '{n}'"),
4816 None => "to-be-closed slot".to_string(),
4817 };
4818 return Err(self.rt_err(&format!("{where_} got a non-closable value (a {tn} value)")));
4819 }
4820 debug_assert!(self.tbc.last().is_none_or(|&s| s < slot));
4821 self.tbc.push(slot);
4822 Ok(())
4823 }
4824
4825 /// Close upvalues and run `__close` handlers for slots ≥ `from`
4826 /// (handlers in reverse registration order; PUC luaF_close).
4827 fn close_slots(&mut self, from: u32, err: Option<Value>) -> Result<(), LuaError> {
4828 self.close_from(from);
4829 // PUC: handlers run in reverse declaration order; an error raised by a
4830 // handler becomes the error object passed to the remaining ones, and
4831 // the rest are still closed. The last raised error propagates.
4832 let mut pending = err;
4833 let mut result = Ok(());
4834 let saved_err = self.closing_err;
4835 // On a normal close the handler runs within the closing function's
4836 // activation (debug parent = that function); during error unwinding the
4837 // function's frame is already gone, so the handler sits at the C
4838 // boundary instead (PUC: luaF_close runs after the ci is restored).
4839 let error_close = err.is_some();
4840 while let Some(&s) = self.tbc.last() {
4841 if s < from {
4842 break;
4843 }
4844 self.tbc.pop();
4845 let v = self.stack[s as usize];
4846 if matches!(v, Value::Nil | Value::Bool(false)) {
4847 continue;
4848 }
4849 let mm = self.get_mm(v, Mm::Close);
4850 if mm.is_nil() {
4851 // PUC `prepclosingmethod`: the __close metamethod was present
4852 // at OP_TBC (else we would have errored there) but has since
4853 // been removed/replaced. Treat as a non-callable target.
4854 let tn = self.obj_typename(v);
4855 let e = self.rt_err(&format!(
4856 "attempt to call a {tn} value (metamethod 'close')"
4857 ));
4858 pending = Some(e.0);
4859 result = Err(e);
4860 continue;
4861 }
4862 // root the pending error: a handler may trigger a collection
4863 self.closing_err = pending;
4864 // PUC `luaF_close` sets `ci->u.l.tm = TM_CLOSE` so traceback /
4865 // getinfo report the handler as "in metamethod 'close'". Saved/
4866 // restored around the call to cover the path where `mm` is a
4867 // native (`push_frame` never consumes it) or it raises before
4868 // reaching push_frame.
4869 let saved_tm = self.pending_tm.replace("close");
4870 // PUC 5.4 `prepclosingmethod` always pushed (obj, errobj) — errobj
4871 // is nil on a normal close (5.4 locals.lua :875's
4872 // `func2close(coroutine.yield)` wrap pins `(self, nil)` back
4873 // through the yield). PUC 5.5 dropped the trailing nil: a clean
4874 // close passes only `obj`, the error case still passes both
4875 // (5.5 locals.lua :314 `select("#", ...) == n` with n=1 for the
4876 // normal-close arms, n=2 for the error arm).
4877 let call = match pending {
4878 Some(e) => self.call_value_impl(mm, &[v, e], error_close),
4879 None => {
4880 if self.version >= LuaVersion::Lua55 {
4881 self.call_value_impl(mm, &[v], error_close)
4882 } else {
4883 self.call_value_impl(mm, &[v, Value::Nil], error_close)
4884 }
4885 }
4886 };
4887 self.pending_tm = saved_tm;
4888 if let Err(e) = call {
4889 pending = Some(e.0);
4890 result = Err(e);
4891 }
4892 }
4893 self.closing_err = saved_err;
4894 result
4895 }
4896
4897 /// Yieldable variant of `close_slots`: drive the chain of `__close`
4898 /// handlers for slots ≥ `from` through the interpreter loop with a
4899 /// `Cont::Close` continuation, so a `coroutine.yield()` inside any handler
4900 /// suspends cleanly (the close iteration's state rides on the thread's
4901 /// frame/stack like any other suspended call) — PUC's `lua_callk` pattern
4902 /// applied to `luaF_close`. `after` runs when every slot is closed; if
4903 /// `after` is `Return` and we've returned past `entry_depth`,
4904 /// `Ok(Some(vals))` carries the result up to the host caller.
4905 fn begin_close(
4906 &mut self,
4907 from: u32,
4908 err: Option<Value>,
4909 after: AfterClose,
4910 entry_depth: usize,
4911 ) -> Result<Option<Vec<Value>>, LuaError> {
4912 self.close_from(from);
4913 self.drive_close(from, err, after, entry_depth)
4914 }
4915
4916 /// Pop tbc slots ≥ `from`, skipping nil/false and synthesising a
4917 /// non-callable-mm error for an `__close` that was reset to a bad value
4918 /// between OP_TBC and now (PUC `prepclosingmethod`). The first real
4919 /// handler pushes a `Cont::Close` + `begin_call` and returns `Ok(None)`;
4920 /// the interpreter then drives the handler and re-enters this driver via
4921 /// the `Cont::Close` consumer in `run()`. When the chain is exhausted,
4922 /// the threaded error (if any) propagates or `after` fires.
4923 fn drive_close(
4924 &mut self,
4925 from: u32,
4926 mut pending: Option<Value>,
4927 after: AfterClose,
4928 entry_depth: usize,
4929 ) -> Result<Option<Vec<Value>>, LuaError> {
4930 loop {
4931 let drained = match self.tbc.last() {
4932 None => true,
4933 Some(&s) => s < from,
4934 };
4935 if drained {
4936 return self.finish_close_after(after, pending, entry_depth);
4937 }
4938 let s = self.tbc.pop().expect("tbc non-empty");
4939 let v = self.stack[s as usize];
4940 if matches!(v, Value::Nil | Value::Bool(false)) {
4941 continue;
4942 }
4943 let mm = self.get_mm(v, Mm::Close);
4944 if mm.is_nil() {
4945 let tn = self.obj_typename(v);
4946 let e = self.rt_err(&format!(
4947 "attempt to call a {tn} value (metamethod 'close')"
4948 ));
4949 pending = Some(e.0);
4950 continue;
4951 }
4952 // A real handler: stage [mm, v, (err?)] above the current top,
4953 // record the close iteration state in a Cont::Close, and let the
4954 // interpreter dispatch the handler. On return the run() head
4955 // re-enters this driver via the Cont::Close consumer.
4956 let func_slot = self.top;
4957 let error_close = pending.is_some();
4958 let need = (func_slot + 3) as usize;
4959 if self.stack.len() < need {
4960 self.stack.resize(need, Value::Nil);
4961 }
4962 self.stack[func_slot as usize] = mm;
4963 self.stack[func_slot as usize + 1] = v;
4964 // PUC 5.4 always passes (obj, errobj=nil) on a normal close;
4965 // 5.5 drops the trailing nil. 5.4 locals.lua :875 vs 5.5 :314.
4966 let nargs = match pending {
4967 Some(e) => {
4968 self.stack[func_slot as usize + 2] = e;
4969 2u32
4970 }
4971 None => {
4972 if self.version >= LuaVersion::Lua55 {
4973 1u32
4974 } else {
4975 self.stack[func_slot as usize + 2] = Value::Nil;
4976 2u32
4977 }
4978 }
4979 };
4980 self.top = func_slot + 1 + nargs;
4981 // Root the pending error during the call (a handler may collect).
4982 let saved_err = self.closing_err;
4983 self.closing_err = pending;
4984 // PUC `luaF_close` flags the handler frame as "metamethod 'close'"
4985 // for traceback / getinfo.
4986 let saved_tm = self.pending_tm.replace("close");
4987 frames_push_sync(
4988 &mut self.frames,
4989 &mut self.frames_top,
4990 CallFrame::Cont(NativeCont {
4991 kind: ContKind::Close(CloseCont {
4992 from,
4993 pending,
4994 after,
4995 }),
4996 func_slot,
4997 nresults: 0,
4998 }),
4999 );
5000 // PUC luaF_close runs a normal close *within* the closing
5001 // function's activation (debug parent = that function); during an
5002 // error unwind the function's frame is already gone and the
5003 // handler sits at the C boundary instead.
5004 let r = self.begin_call(func_slot, Some(nargs), 0, error_close);
5005 self.pending_tm = saved_tm;
5006 self.closing_err = saved_err;
5007 r?;
5008 return Ok(None);
5009 }
5010 }
5011
5012 /// Fire `after` once every `__close` handler has run. `Block` propagates
5013 /// any remaining error or simply continues; `Return` performs OP_Return's
5014 /// tail (hook + frame pop + result delivery) and may surface results to
5015 /// the host when the function whose return triggered the close was the
5016 /// entry activation, but only on a clean drain — a pending error skips
5017 /// the return tail and propagates instead. `ResumeUnwind` pops the
5018 /// deferred Lua frame and re-raises, letting a handler's own error win
5019 /// over the original propagating one (PUC luaF_close).
5020 fn finish_close_after(
5021 &mut self,
5022 after: AfterClose,
5023 pending: Option<Value>,
5024 entry_depth: usize,
5025 ) -> Result<Option<Vec<Value>>, LuaError> {
5026 match after {
5027 AfterClose::Block => match pending {
5028 Some(e) => Err(LuaError(e)),
5029 None => Ok(None),
5030 },
5031 AfterClose::Return {
5032 abs_a,
5033 nret,
5034 from_native,
5035 } => match pending {
5036 Some(e) => Err(LuaError(e)),
5037 None => self.complete_return(abs_a, nret, from_native, entry_depth),
5038 },
5039 AfterClose::ResumeUnwind { func_slot, err } => {
5040 // The aborting Lua frame was popped before `begin_close`;
5041 // restore the catcher's stack window down to `func_slot` and
5042 // re-raise — preferring a handler-raised error over the
5043 // original (PUC luaF_close).
5044 self.stack.truncate(func_slot as usize);
5045 self.top = func_slot;
5046 self.tbc.retain(|&s| s < func_slot);
5047 Err(LuaError(pending.unwrap_or(err)))
5048 }
5049 }
5050 }
5051
5052 /// OP_Return's post-close tail: fire the "return" hook (frame still
5053 /// current), pop the Lua frame, slide results into `func_slot`, then
5054 /// either hand them to the host (`Ok(Some(vals))` when we've returned
5055 /// past `entry_depth`), leave them contiguous for an exposed
5056 /// pcall/xpcall continuation, or finish into the caller's expected
5057 /// result slot. Mirrors the synchronous OP_Return tail so both paths
5058 /// share semantics — the `from_native` flag selects the right "return"
5059 /// hook context for `hook_return`.
5060 fn complete_return(
5061 &mut self,
5062 abs_a: u32,
5063 nret: u32,
5064 from_native: bool,
5065 entry_depth: usize,
5066 ) -> Result<Option<Vec<Value>>, LuaError> {
5067 // ftransfer is the local index (1-based) of the first result, as
5068 // `getinfo("r").ftransfer + getlocal(level, k)` consumes it. luna
5069 // exposes locals starting at `frame.base` (= func_slot + 1 +
5070 // n_varargs for a vararg call), so the conversion is the absolute
5071 // result slot minus base, plus one to make it 1-based. db.lua 5.4
5072 // :542 (`foo1(); on=false; eqseq(out, {10, 0})`) pins the vararg
5073 // shape end-to-end.
5074 let ftransfer = self
5075 .frames
5076 .last()
5077 .and_then(CallFrame::lua)
5078 .map(|fr| {
5079 let raw = abs_a.saturating_sub(fr.base) + 1;
5080 // 5.5 anonymous-vararg functions get a `(vararg table)` pseudo
5081 // local injected at index `numparams + 1`, so getlocal
5082 // numbering shifts results past it (5.5 db.lua :539
5083 // `eqseq(out, {10, 0})`). 5.4 and earlier have no such pseudo.
5084 if fr.closure.proto.has_vararg_table_pseudo {
5085 raw + 1
5086 } else {
5087 raw
5088 }
5089 })
5090 .unwrap_or(1);
5091 // PUC 5.1 `luaD_poscall`: fire one extra "tail return" hook event
5092 // per tail call that collapsed into this activation, *after* its
5093 // own "return". `tailcalls` tracks that count exactly (PUC
5094 // `ci->u.l.tailcalls`). 5.2+ retired LUA_HOOKTAILRET, so the
5095 // "return" hook fires once even when the activation absorbed
5096 // multiple tail calls — only `istailcall` on getinfo surfaces the
5097 // collapse. 5.1 db.lua :366 pins the event ordering.
5098 let tailcalls = if self.version <= LuaVersion::Lua51 {
5099 self.frames
5100 .last()
5101 .and_then(|f| f.lua())
5102 .map(|f| f.tailcalls)
5103 .unwrap_or(0)
5104 } else {
5105 0
5106 };
5107 self.hook_return(from_native, ftransfer, nret)?;
5108 for _ in 0..tailcalls {
5109 self.hook_tail_return()?;
5110 }
5111 let CallFrame::Lua(fr) =
5112 frames_pop_sync(&mut self.frames, &mut self.frames_top).expect("no frame")
5113 else {
5114 unreachable!("returning from a non-Lua frame")
5115 };
5116 for i in 0..nret {
5117 self.stack[(fr.func_slot + i) as usize] = self.stack[(abs_a + i) as usize];
5118 }
5119 if self.frames.len() < entry_depth {
5120 self.top = fr.func_slot + nret;
5121 return Ok(Some(self.take_results(fr.func_slot)));
5122 } else if matches!(self.frames.last(), Some(CallFrame::Cont(_))) {
5123 self.top = fr.func_slot + nret;
5124 } else {
5125 self.finish_results(fr.func_slot, nret, fr.nresults);
5126 }
5127 Ok(None)
5128 }
5129
5130 #[doc(hidden)]
5131 pub fn upval_get(&self, cl: Gc<LuaClosure>, idx: u32) -> Value {
5132 match cl.upvals()[idx as usize].state() {
5133 UpvalState::Open { slot, thread } => self.read_slot(slot, thread),
5134 UpvalState::Closed(v) => v,
5135 }
5136 }
5137
5138 fn upval_set(&mut self, cl: Gc<LuaClosure>, idx: u32, v: Value) {
5139 let uv = cl.upvals()[idx as usize];
5140 match uv.state() {
5141 UpvalState::Open { slot, thread } => self.write_slot(slot, thread, v),
5142 UpvalState::Closed(_) => {
5143 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
5144 unsafe { uv.as_mut() }.set_closed(v);
5145 // forward barrier: a closed upvalue is single-slot, so the
5146 // forward variant is cheaper than barrier_back (PUC uses
5147 // `luaC_barrier_` for upvalues; `luaC_barrierback_` for
5148 // tables / threads).
5149 self.heap
5150 .barrier_forward(uv.as_ptr() as *mut crate::runtime::heap::GcHeader, v);
5151 }
5152 }
5153 }
5154
5155 // ---- register / error helpers ----
5156
5157 #[inline(always)]
5158 fn r(&self, base: u32, i: u32) -> Value {
5159 // SAFETY: the compiler reserves `proto.max_stack` slots above `base`
5160 // at frame entry (`push_frame` sizes the stack up to base + max_stack),
5161 // and every bytecode-generated reference falls within `[0, max_stack)`.
5162 // PUC's vmfetch uses raw `R(A)` (`s2v(L->base + A)`) for the same
5163 // reason. The bounds check would re-validate this invariant on every
5164 // op — the dispatch hot path can't afford it.
5165 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
5166 unsafe { *self.stack.get_unchecked((base + i) as usize) }
5167 }
5168
5169 #[inline(always)]
5170 fn set_r(&mut self, base: u32, i: u32, v: Value) {
5171 // SAFETY: see `r` — `base + i < base + max_stack <= stack.len()` by
5172 // frame-entry contract.
5173 unsafe {
5174 *self.stack.get_unchecked_mut((base + i) as usize) = v;
5175 }
5176 }
5177
5178 #[doc(hidden)]
5179 pub fn rt_err(&mut self, msg: &str) -> LuaError {
5180 let text = match self.position_prefix() {
5181 Some(p) => format!("{p}{msg}"),
5182 None => msg.to_string(),
5183 };
5184 LuaError(Value::Str(self.heap.intern(text.as_bytes())))
5185 }
5186
5187 /// Error without the `chunk:line:` position prefix. PUC's
5188 /// `resume_error` (ldo.c) pushes its message as a bare literal,
5189 /// so `cannot resume dead coroutine` etc. must not be prefixed.
5190 pub(crate) fn plain_err(&mut self, msg: &str) -> LuaError {
5191 LuaError(Value::Str(self.heap.intern(msg.as_bytes())))
5192 }
5193
5194 pub(crate) fn type_err(&mut self, what: &str, v: Value) -> LuaError {
5195 let extra = self.subject_varinfo(v);
5196 let tn = self.obj_typename(v);
5197 let msg = self.compose_type_err(what, &tn, &extra);
5198 self.rt_err(&msg)
5199 }
5200
5201 /// Assemble a `luaG_typeerror` / `luaG_callerror` message in the dialect's
5202 /// word order.
5203 ///
5204 /// PUC ≤5.2 names the operand first — `attempt to call field 'f' (a nil
5205 /// value)`. 5.3 flipped it to type-first — `attempt to call a nil value
5206 /// (field 'f')`. luna emitted the 5.3+ form on every dialect, so every
5207 /// such error was worded wrong under 5.1/5.2.
5208 ///
5209 /// Two shapes carry no operand name on ≤5.2 and must collapse to the bare
5210 /// message: an absent varinfo (identical across dialects), and a
5211 /// metamethod target — ≤5.2's `luaG_typeerror` only names locals, globals,
5212 /// fields, upvalues and methods, so `(metamethod 'add')` has no ≤5.2
5213 /// counterpart and is dropped rather than reworded. All four shapes were
5214 /// measured against stock 5.1.5 / 5.2.4 / 5.5.1 before this was written.
5215 fn compose_type_err(&self, what: &str, tn: &str, extra: &str) -> String {
5216 if self.version() > crate::version::LuaVersion::Lua52 {
5217 return format!("attempt to {what} a {tn} value{extra}");
5218 }
5219 // `extra` is "" or " (kind 'name')" — unwrap to "kind 'name'".
5220 let inner = extra
5221 .trim_start()
5222 .trim_start_matches('(')
5223 .trim_end_matches(')');
5224 if inner.is_empty() || inner.starts_with("metamethod") {
5225 format!("attempt to {what} a {tn} value")
5226 } else {
5227 format!("attempt to {what} {inner} (a {tn} value)")
5228 }
5229 }
5230
5231 /// Name the offending operand of the current instruction (PUC varinfo) for
5232 /// a type error, e.g. " (global 'x')". The faulting value `bad` is matched
5233 /// to the instruction's subject register(s); a native-raised error whose
5234 /// current instruction doesn't hold `bad` simply yields "".
5235 fn subject_varinfo(&self, bad: Value) -> String {
5236 use crate::vm::isa::Op;
5237 let Some(f) = self.frames.last().and_then(CallFrame::lua) else {
5238 return String::new();
5239 };
5240 let proto = f.closure.proto;
5241 let p: &crate::runtime::Proto = &proto;
5242 let pc = f.pc as usize;
5243 if pc == 0 || pc > p.code.len() {
5244 return String::new();
5245 }
5246 let instr = p.code[pc - 1];
5247 let mut cands: Vec<u32> = Vec::new();
5248 match instr.op() {
5249 // indexed reads / length / method: the table/object is in B
5250 Op::GetField | Op::GetI | Op::GetTable | Op::SelfOp | Op::Len => {
5251 cands.push(instr.b());
5252 }
5253 // indexed writes / calls: the table/function is in A
5254 Op::SetField | Op::SetI | Op::SetTable | Op::Call | Op::TailCall => {
5255 cands.push(instr.a());
5256 }
5257 // arithmetic/bitwise: a register operand (B, and C unless constant)
5258 Op::Add
5259 | Op::Sub
5260 | Op::Mul
5261 | Op::Div
5262 | Op::Mod
5263 | Op::Pow
5264 | Op::IDiv
5265 | Op::BAnd
5266 | Op::BOr
5267 | Op::BXor
5268 | Op::Shl
5269 | Op::Shr => {
5270 cands.push(instr.b());
5271 if !instr.k() {
5272 cands.push(instr.c());
5273 }
5274 }
5275 Op::Unm | Op::BNot => cands.push(instr.b()),
5276 Op::Concat => {
5277 let a = instr.a();
5278 for r in a..a + instr.b() {
5279 cands.push(r);
5280 }
5281 }
5282 _ => {}
5283 }
5284 for reg in cands {
5285 if self.r(f.base, reg).raw_eq(bad) {
5286 return match crate::vm::objname::getobjname(p, pc - 1, reg) {
5287 Some((kind, name)) => format!(" ({kind} '{name}')"),
5288 None => String::new(),
5289 };
5290 }
5291 }
5292 String::new()
5293 }
5294
5295 /// "attempt to call a X value", enriched (PUC luaG_callerror) with a name
5296 /// for the call target: "(global 'f')" for a direct call, or "(metamethod
5297 /// 'add')" when the call is a metamethod dispatched by the current opcode.
5298 fn call_err(&mut self, v: Value) -> LuaError {
5299 let extra = self.call_target_varinfo(v);
5300 let tn = self.obj_typename(v);
5301 let msg = self.compose_type_err("call", &tn, &extra);
5302 self.rt_err(&msg)
5303 }
5304
5305 /// Name the offending call target. A metamethod dispatch pushes a `Cont`
5306 /// frame before the call, so the opcode that triggered it lives in the
5307 /// nearest *Lua* frame — read that instruction: OP_CALL names the function
5308 /// register, any metamethod-bearing opcode yields "(metamethod 'event')".
5309 fn call_target_varinfo(&self, bad: Value) -> String {
5310 use crate::vm::isa::Op;
5311 let Some(f) = self.frames.iter().rev().find_map(CallFrame::lua) else {
5312 return String::new();
5313 };
5314 let proto = f.closure.proto;
5315 let p: &crate::runtime::Proto = &proto;
5316 let pc = f.pc as usize;
5317 if pc == 0 || pc > p.code.len() {
5318 return String::new();
5319 }
5320 let instr = p.code[pc - 1];
5321 match instr.op() {
5322 Op::Call | Op::TailCall => {
5323 let reg = instr.a();
5324 if self.r(f.base, reg).raw_eq(bad) {
5325 match crate::vm::objname::getobjname(p, pc - 1, reg) {
5326 Some((kind, name)) => format!(" ({kind} '{name}')"),
5327 None => String::new(),
5328 }
5329 } else {
5330 String::new()
5331 }
5332 }
5333 op => match mm_event_name(op) {
5334 Some(ev) => format!(" (metamethod '{ev}')"),
5335 None => String::new(),
5336 },
5337 }
5338 }
5339
5340 /// "number has no integer representation", enriched (PUC luaG_tointerror)
5341 /// with a "(field 'x')"-style suffix naming the offending operand of the
5342 /// current arithmetic instruction when it can be recovered from bytecode.
5343 fn no_int_rep_err(&mut self) -> LuaError {
5344 let extra = self.bad_operand_varinfo();
5345 self.rt_err(&format!("number{extra} has no integer representation"))
5346 }
5347
5348 /// Inspect the current frame's faulting instruction: find the register
5349 /// operand holding a float with no integer representation and name it.
5350 fn bad_operand_varinfo(&self) -> String {
5351 let Some(f) = self.frames.last().and_then(CallFrame::lua) else {
5352 return String::new();
5353 };
5354 let proto = f.closure.proto;
5355 let p: &crate::runtime::Proto = &proto;
5356 let pc = f.pc as usize;
5357 if pc == 0 || pc > p.code.len() {
5358 return String::new();
5359 }
5360 let instr = p.code[pc - 1];
5361 let mut regs = vec![instr.b()];
5362 if !instr.k() {
5363 regs.push(instr.c());
5364 }
5365 for reg in regs {
5366 let v = self.r(f.base, reg);
5367 if matches!(v, Value::Float(x) if crate::runtime::value::f2i_exact(x).is_none()) {
5368 return match crate::vm::objname::getobjname(p, pc - 1, reg) {
5369 Some((kind, name)) => format!(" ({kind} '{name}')"),
5370 None => String::new(),
5371 };
5372 }
5373 }
5374 String::new()
5375 }
5376
5377 /// Position prefix of the currently executing Lua frame. PUC `luaL_error`
5378 /// calls `luaL_where(L, 1)` which reads `L->ci->previous`. When the prior
5379 /// frame is a C function (e.g. a pcall Cont parked above `require`'s
5380 /// native call), PUC pushes no prefix — match that by looking only at the
5381 /// topmost frame directly and bailing if it is anything but a Lua frame.
5382 pub(crate) fn position_prefix(&self) -> Option<String> {
5383 let f = self.frames.last().and_then(CallFrame::lua)?;
5384 let proto = f.closure.proto;
5385 if proto.source.as_bytes().is_empty() {
5386 return Some(self.stripped_prefix());
5387 }
5388 if proto.lines.is_empty() {
5389 return None;
5390 }
5391 let line = proto.lines[(f.pc as usize).saturating_sub(1).min(proto.lines.len() - 1)];
5392 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
5393 let raw = unsafe { crate::runtime::string::bytes_of(proto.source.as_ptr()) };
5394 let display = crate::vm::lib_debug::chunk_id(raw);
5395 let src = String::from_utf8_lossy(&display).into_owned();
5396 Some(format!("{src}:{line}: "))
5397 }
5398
5399 /// PUC `luaG_addinfo` prefix for a stripped chunk. 5.5 substitutes "=?"
5400 /// for the source and renders the line as "?" (so the prefix reads
5401 /// `?:?: `). 5.4 and below leave the source NULL ("?") and use the raw
5402 /// `getfuncline = -1`, so the prefix reads `?:-1: ` (5.4 errors.lua :282
5403 /// matches `^%?:%-1:`).
5404 fn stripped_prefix(&self) -> String {
5405 if self.version >= crate::version::LuaVersion::Lua55 {
5406 "?:?: ".to_string()
5407 } else {
5408 "?:-1: ".to_string()
5409 }
5410 }
5411
5412 /// Position prefix of the Lua frame `level` steps up from the running C
5413 /// function (PUC `luaL_where(L, level)`): `level == 1` is the immediate
5414 /// Lua caller (skipping Cont/C-boundary frames the way `dbg_frame` does),
5415 /// `level == 2` its caller, and so on. Used by `error(msg, level)` so the
5416 /// caller's frame is reported even across pcall/xpcall continuations.
5417 /// `luaL_where(level)` for `error()`: unlike `dbg_frame` (whose 5.2+
5418 /// level numbering skips Cont activations to match db.lua's getinfo
5419 /// shape), PUC counts EVERY CallInfo — a C caller occupies a level of
5420 /// its own. `pcall(pcall, error, "msg")` must therefore resolve
5421 /// level 1 to the inner pcall (a C activation, no line info → no
5422 /// prefix), not tunnel through to the Lua frame below (v2.13
5423 /// CORPUS-IV fixture 239).
5424 pub(crate) fn position_prefix_at_level(&self, level: i64) -> Option<String> {
5425 if level < 1 {
5426 return None;
5427 }
5428 let v51 = self.version <= LuaVersion::Lua51;
5429 let mut lvl = level;
5430 let mut found: Option<usize> = None;
5431 'walk: for fi in (0..self.frames.len()).rev() {
5432 match &self.frames[fi] {
5433 CallFrame::Lua(f) => {
5434 lvl -= 1;
5435 if lvl == 0 {
5436 found = Some(fi);
5437 break 'walk;
5438 }
5439 if v51 {
5440 for _ in 0..f.tailcalls {
5441 lvl -= 1;
5442 if lvl == 0 {
5443 return None; // synthetic tail level: no line info
5444 }
5445 }
5446 }
5447 if f.from_c {
5448 lvl -= 1;
5449 if lvl == 0 {
5450 return None; // C activation: no line info
5451 }
5452 }
5453 }
5454 CallFrame::Cont(_) => {
5455 // A continuation-driven native (pcall/xpcall/close)
5456 // is a C activation — it takes a level and has no
5457 // line info.
5458 lvl -= 1;
5459 if lvl == 0 {
5460 return None;
5461 }
5462 }
5463 }
5464 }
5465 let fi = found?;
5466 let f = self.frames[fi].lua()?;
5467 let proto = f.closure.proto;
5468 // PUC luaG_addinfo: a stripped chunk has no source — see
5469 // `stripped_prefix` for the per-version wording (5.5 vs ≤5.4).
5470 if proto.source.as_bytes().is_empty() {
5471 return Some(self.stripped_prefix());
5472 }
5473 // a stripped chunk carries no per-instruction line info
5474 if proto.lines.is_empty() {
5475 return None;
5476 }
5477 let line = proto.lines[(f.pc as usize).saturating_sub(1).min(proto.lines.len() - 1)];
5478 // PUC `luaG_addinfo` renders source via `luaO_chunkid` (LUA_IDSIZE=60),
5479 // not the raw chunk name — handles `@file`/`=name` sigils + truncation.
5480 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
5481 let raw = unsafe { crate::runtime::string::bytes_of(proto.source.as_ptr()) };
5482 let display = crate::vm::lib_debug::chunk_id(raw);
5483 let src = String::from_utf8_lossy(&display).into_owned();
5484 Some(format!("{src}:{line}: "))
5485 }
5486
5487 // ---- the interpreter ----
5488
5489 fn exec(&mut self) -> Result<Vec<Value>, LuaError> {
5490 let entry_depth = self.frames.len();
5491 self.exec_with(entry_depth)
5492 }
5493
5494 /// Run from the current top frame down to (but not past) `entry_depth`
5495 /// frames. Coroutine driving passes `entry_depth = 1` so the whole thread
5496 /// runs to completion or a yield.
5497 /// v1.1 B10 Stage 1 — resume the dispatcher from the saved
5498 /// `entry_depth` (captured pre-yield by `drive_one`). Called by
5499 /// `EvalFuture::poll` on every poll after the first to walk the
5500 /// existing call frames until the next `BudgetExhausted` or
5501 /// terminal `Ok`/`Err`. Not a public-API surface in Stage 1; the
5502 /// embedder reaches it through `Vm::eval_async`.
5503 pub(crate) fn exec_with_async(&mut self, entry_depth: usize) -> Result<Vec<Value>, LuaError> {
5504 self.exec_with(entry_depth)
5505 }
5506
5507 fn exec_with(&mut self, entry_depth: usize) -> Result<Vec<Value>, LuaError> {
5508 loop {
5509 let r = self.run(entry_depth);
5510 if r.is_err()
5511 && (self.yielding.is_some()
5512 || self.terminating.is_some()
5513 || self.host_yield_pending
5514 || self.pending_async_native_fut.is_some())
5515 {
5516 // a `coroutine.yield` is in flight: keep the frames intact (they
5517 // are the suspended coroutine's saved state) and propagate to
5518 // resume. A self-close termination propagates the same way, so a
5519 // protecting pcall on the way out cannot catch (unwind) it.
5520 // v1.1 B10 — `host_yield_pending` is the async-mode
5521 // analogue: the sentinel must reach `drive_one` without
5522 // a protecting `pcall` swallowing it.
5523 return r;
5524 }
5525 match r {
5526 Ok(vals) => return Ok(vals),
5527 // unwind toward `entry_depth`. A protecting pcall/xpcall
5528 // continuation caught along the way turns the error into
5529 // `false, msg` and the loop resumes running its caller; an
5530 // uncaught error propagates out.
5531 Err(e) => match self.unwind(e.0, entry_depth) {
5532 Unwound::Caught => continue,
5533 Unwound::CaughtReturn(vals) => return Ok(vals),
5534 Unwound::Propagated(err) => return Err(err),
5535 },
5536 }
5537 }
5538 }
5539
5540 /// Unwind the call stack from the error point toward `entry_depth`, running
5541 /// `__close` handlers on each Lua frame. Stops at the first pcall/xpcall
5542 /// continuation frame at/above `entry_depth` (the error is *caught*: its
5543 /// slot receives `false, msg`); if none is reached, the error propagates.
5544 fn unwind(&mut self, mut err: Value, entry_depth: usize) -> Unwound {
5545 // The protected call runs in-place among the caller frames' registers,
5546 // so truncating the failed frames here cuts into caller windows below
5547 // the catcher. Snapshot the live length: at the error point the stack
5548 // already spans every surviving frame's window, so restoring it after a
5549 // catch reinstates them all (the reclaimed slots above are dead temps).
5550 // PUC handles overflow recovery via a separate EXTRA_STACK reserve;
5551 // we instead clamp the restore to the catcher's caller window when the
5552 // error point was at the stack limit (cause: the next `call_value_impl`
5553 // picks `func_slot = stack.len()` which would otherwise re-overflow).
5554 let saved_len = self.stack.len();
5555 // Snapshot the traceback at the error point — before any frame is
5556 // popped — so an `xpcall` msgh (which runs after the failed frames are
5557 // gone) can still describe the error site. The handler frame about to
5558 // be popped (e.g. a `__close` handler with `tm = Some("close")`) is
5559 // visible here; once popped, `debug.traceback` would miss it.
5560 // PUC instead runs msgh with the failed stack intact (luaG_errormsg);
5561 // but doing so when the stack is near `MAX_LUA_STACK` (true overflow
5562 // recovery — locals.lua:659) re-overflows. Capture-once propagates
5563 // through nested unwinds (inner→outer) without re-running msgh.
5564 if self.error_traceback.is_none() {
5565 self.error_traceback = Some(self.traceback_bytes(1));
5566 }
5567 while self.frames.len() >= entry_depth {
5568 match *self.frames.last().expect("frame") {
5569 // a yieldable-metamethod continuation does not catch: discard the
5570 // abandoned instruction and keep unwinding (PUC drops the partial
5571 // op on error).
5572 CallFrame::Cont(NativeCont {
5573 kind: ContKind::Meta(mc),
5574 func_slot,
5575 ..
5576 }) => {
5577 frames_pop_sync(&mut self.frames, &mut self.frames_top);
5578 self.stack.truncate(func_slot as usize);
5579 self.top = mc.saved_top.min(func_slot);
5580 self.tbc.retain(|&s| s < func_slot);
5581 }
5582 // a __pairs continuation does not catch either: an error inside
5583 // the metamethod propagates past `pairs`.
5584 CallFrame::Cont(NativeCont {
5585 kind: ContKind::Pairs,
5586 func_slot,
5587 ..
5588 }) => {
5589 frames_pop_sync(&mut self.frames, &mut self.frames_top);
5590 self.stack.truncate(func_slot as usize);
5591 self.top = func_slot;
5592 self.tbc.retain(|&s| s < func_slot);
5593 }
5594 // a __close continuation does not catch: drop the half-run
5595 // handler's window, then continue the close yieldably with
5596 // the new error threaded as `pending`. Preserve `cc.after`
5597 // verbatim — `Return`/`Block` originating from an aborting
5598 // OP_Return/OP_Close will be short-circuited by
5599 // `finish_close_after` (pending propagates as Err); a
5600 // `ResumeUnwind` originated by our own Lua-frame handler
5601 // must keep its deferred frame-pop semantics so that frame
5602 // is not orphaned. If a fresh handler yields, `drive_close`
5603 // pushes another `Cont::Close` and we return `Caught` so
5604 // `exec_with` re-enters the run loop.
5605 CallFrame::Cont(NativeCont {
5606 kind: ContKind::Close(cc),
5607 func_slot,
5608 ..
5609 }) => {
5610 frames_pop_sync(&mut self.frames, &mut self.frames_top);
5611 self.stack.truncate(func_slot as usize);
5612 self.top = func_slot;
5613 self.tbc.retain(|&s| s < func_slot);
5614 match self.drive_close(cc.from, Some(err), cc.after, entry_depth) {
5615 Ok(Some(_)) => {
5616 unreachable!(
5617 "Block / Return / ResumeUnwind never return host values mid-unwind"
5618 )
5619 }
5620 Ok(None) => return Unwound::Caught,
5621 Err(e) => {
5622 err = e.0;
5623 continue;
5624 }
5625 }
5626 }
5627 CallFrame::Cont(nc) => {
5628 frames_pop_sync(&mut self.frames, &mut self.frames_top);
5629 self.pcall_depth -= 1;
5630 let result = match nc.kind {
5631 ContKind::Pcall => err,
5632 ContKind::Xpcall { handler } => {
5633 // PUC keeps `L->errfunc` set across the handler's
5634 // call: `luaG_errormsg` re-fires the handler when
5635 // it raises (so `xpcall(error, err, 170)` lets the
5636 // chain bottom out at err(0) → "END"). luna mirrors
5637 // that by looping until the handler returns or
5638 // luna's `iters` cap forces termination.
5639 //
5640 // The cap models PUC's nCcalls soft window
5641 // (MAXCCALLS/10*11): once tripped, `stackerror`
5642 // raises "C stack overflow" via `luaG_runerror`
5643 // which itself re-enters `luaG_errormsg`, so the
5644 // handler runs once more with that string and
5645 // naturally returns it (errors.lua :637 at N=300).
5646 // We count iterations per Cont::Xpcall rather than
5647 // a global counter — nested xpcalls each get their
5648 // own budget, matching the way PUC's stack frames
5649 // accumulate per dispatch path.
5650 const MSGH_CAP: u32 = MAX_C_DEPTH;
5651 let mut cur_err = err;
5652 let mut iters: u32 = 0;
5653 let mut capped = false;
5654 loop {
5655 if iters >= MSGH_CAP && !capped {
5656 cur_err = Value::Str(self.heap.intern(b"C stack overflow"));
5657 capped = true;
5658 }
5659 iters += 1;
5660 self.msgh_depth += 1;
5661 let r = self.call_value(handler, &[cur_err]);
5662 self.msgh_depth -= 1;
5663 match r {
5664 Ok(hr) => {
5665 break hr.first().copied().unwrap_or(Value::Nil);
5666 }
5667 Err(_) if capped => {
5668 // the handler still errored on the
5669 // synthesized "C stack overflow"; fall
5670 // back to PUC's LUA_ERRERR string.
5671 break Value::Str(
5672 self.heap.intern(b"error in error handling"),
5673 );
5674 }
5675 Err(e) => {
5676 cur_err = e.0;
5677 }
5678 }
5679 }
5680 }
5681 ContKind::Meta(_) | ContKind::Pairs | ContKind::Close(_) => {
5682 unreachable!("Meta/Pairs/Close cont handled above")
5683 }
5684 };
5685 // PUC 5.5 `luaG_errormsg` substitutes "<no error object>"
5686 // for nil AFTER the message handler ran (ldebug.c:849) —
5687 // so it applies to the pcall-caught object and to an
5688 // xpcall HANDLER'S return value, while the handler itself
5689 // (and a top-level propagation into the host, whose
5690 // `error_display` plays msghandler) still sees the raw
5691 // nil. 5.4- keep nil everywhere (errors.lua :49 asserts
5692 // `doit("error()") == nil`). v2.14 fixture 5.5/334.
5693 let result = if matches!(result, Value::Nil)
5694 && self.version >= crate::version::LuaVersion::Lua55
5695 {
5696 Value::Str(self.heap.intern(b"<no error object>"))
5697 } else {
5698 result
5699 };
5700 // the error has been caught (pcall/xpcall): the captured
5701 // traceback was for that error and is no longer in flight.
5702 self.error_traceback = None;
5703 let fs = nc.func_slot as usize;
5704 if self.stack.len() < fs + 2 {
5705 self.stack.resize(fs + 2, Value::Nil);
5706 }
5707 self.stack[fs] = Value::Bool(false);
5708 self.stack[fs + 1] = result;
5709 self.top = nc.func_slot + 2;
5710 self.tbc.retain(|&s| s < nc.func_slot);
5711 if self.frames.len() < entry_depth {
5712 return Unwound::CaughtReturn(self.take_results(nc.func_slot));
5713 }
5714 self.finish_results(nc.func_slot, 2, nc.nresults);
5715 // reinstate the caller windows the unwind truncated into,
5716 // clamped to the catcher's caller window + a `MIN_STACK`
5717 // reserve. The clamp is a no-op for normal pcall catches
5718 // (saved_len lies within the caller's max_stack window),
5719 // and prevents the stack from staying near `MAX_LUA_STACK`
5720 // after an overflow-recovery catch — which would make the
5721 // next `call_value_impl` (e.g. a `__close` in the catcher's
5722 // errorh, locals.lua:659) pick `func_slot = stack.len()`
5723 // above the limit and re-overflow.
5724 // Restore the caller's full register window: opcodes
5725 // index it directly. The cap covers caller's base +
5726 // `max_stack` + a small reserve. We always resize to
5727 // exactly this window — previously this clamped
5728 // `saved_len` from above to prevent staying near
5729 // `MAX_LUA_STACK` after an overflow-recovery catch, and
5730 // a yieldable-unwind re-entry adds the dual case where
5731 // `saved_len` is *below* the window (a prior
5732 // `ResumeUnwind` truncated). Using the window directly
5733 // covers both.
5734 let restore = self
5735 .frames
5736 .iter()
5737 .rev()
5738 .find_map(CallFrame::lua)
5739 .map(|c| (c.base + c.closure.proto.max_stack as u32) as usize + 256)
5740 .unwrap_or(saved_len);
5741 if self.stack.len() < restore {
5742 self.stack.resize(restore, Value::Nil);
5743 } else if self.stack.len() > restore {
5744 self.stack.truncate(restore);
5745 }
5746 // v2.5 P1B-2B: clear slots vacated by the popped
5747 // frames the unwind walked over. finish_results
5748 // above clears `[nc.func_slot + nresults ..
5749 // nc.func_slot + 2)`, which only covers the
5750 // pcall's own result region — the unwind-popped
5751 // frames' locals in `[nc.func_slot + 2 .. restore)`
5752 // are still in place with whatever Gc-bearing
5753 // Values they last held. Without this clear, a
5754 // later GC marks the stale pointers (UAF-A family
5755 // analog of the v2.3 Op::Return finish_results
5756 // path). PUC's `luaD_pcall` similarly truncates
5757 // L->top to the catcher's level — luna's
5758 // truncate above resizes the Vec but doesn't
5759 // touch slots [func_slot+2..restore) that were
5760 // already present.
5761 let clear_lo = (nc.func_slot as usize + 2).min(self.stack.len());
5762 let clear_hi = restore.min(self.stack.len());
5763 if clear_lo < clear_hi {
5764 for slot in &mut self.stack[clear_lo..clear_hi] {
5765 *slot = Value::Nil;
5766 }
5767 }
5768 return Unwound::Caught;
5769 }
5770 CallFrame::Lua(f) => {
5771 // Yieldable error-unwind close, PUC luaG_errormsg shape:
5772 // (1) pop the Lua frame immediately so each `__close`
5773 // handler runs at the C boundary above — `debug.getinfo`
5774 // sees the next outer Lua frame's call site (typically
5775 // `pcall`), not this aborting function (locals.lua:480).
5776 // (2) drive the close yieldably with
5777 // `AfterClose::ResumeUnwind { func_slot, err }`; on drain
5778 // it truncates to `func_slot` and re-raises (letting a
5779 // handler-raised error win over `err`). If a handler
5780 // yields, `drive_close` pushes `Cont::Close` and we
5781 // return `Caught` so `exec_with` re-enters the run loop;
5782 // a synchronous drain returns Err exactly as the old
5783 // path did.
5784 frames_pop_sync(&mut self.frames, &mut self.frames_top);
5785 let after = AfterClose::ResumeUnwind {
5786 func_slot: f.func_slot,
5787 err,
5788 };
5789 match self.begin_close(f.base, Some(err), after, entry_depth) {
5790 Ok(Some(_)) => {
5791 unreachable!("ResumeUnwind never returns host values")
5792 }
5793 Ok(None) => return Unwound::Caught,
5794 Err(e) => {
5795 err = e.0;
5796 continue;
5797 }
5798 }
5799 }
5800 }
5801 }
5802 Unwound::Propagated(LuaError(err))
5803 }
5804
5805 fn run(&mut self, entry_depth: usize) -> Result<Vec<Value>, LuaError> {
5806 loop {
5807 // Fast-path slow-check gate: most embedders run with both
5808 // `instr_budget` and `mem_cap` as None, so a single combined
5809 // is_some test lets the hot loop skip both branches with one
5810 // load + branch instead of two.
5811 if self.instr_budget.is_some() || self.heap.mem_cap.is_some() {
5812 if let Some(b) = self.instr_budget.as_mut() {
5813 *b -= 1;
5814 if *b <= 0 {
5815 self.instr_budget = None;
5816 // v1.1 B10 Stage 1 — async-mode cooperative
5817 // yield. Set a sentinel flag so `exec_with`
5818 // propagates the Err without `unwind` running
5819 // (mirroring the `yielding.is_some()` path),
5820 // and `call_value_impl` preserves the call
5821 // frames for the next `poll`. Translation back
5822 // to `DispatchOutcome::BudgetExhausted` happens
5823 // in `drive_one`. The Err value itself is
5824 // `Value::Nil` — a pure sentinel, never seen by
5825 // user code.
5826 if self.async_mode {
5827 self.host_yield_pending = true;
5828 return Err(LuaError(Value::Nil));
5829 }
5830 // B6: classify the trip so embedders can
5831 // distinguish budget exhaustion from a
5832 // generic Runtime error and retry / give up
5833 // accordingly.
5834 self.last_error_kind = crate::vm::error::LuaErrorKind::InstrBudget;
5835 let s = Value::Str(self.heap.intern(b"instruction budget exceeded"));
5836 return Err(LuaError(s));
5837 }
5838 }
5839 if let Some(cap) = self.heap.mem_cap
5840 && self.heap.bytes() > cap
5841 {
5842 // First try a full collect — embedders set tight caps
5843 // and the overshoot may be reclaimable (closures kept
5844 // by short-lived frames, intermediate strings). Only
5845 // disarm + raise if the cap is still breached after
5846 // collection. PUC's `LUA_GCEMERGENCY` path matches.
5847 //
5848 // v2.6 A.2: tighten mem-cap-fire over-root from
5849 // entire `self.stack.len()` (whole heap) to the
5850 // deepest Lua frame's `base + max_stack` window
5851 // (covers register operands the current opcode
5852 // might reference). The cap fires during table
5853 // mutation in a tight `a[i] = i` loop where `a`
5854 // lives at a frame-register slot past `self.top`
5855 // (OP_NEWINDEX doesn't advance top); the deepest
5856 // frame's max_stack window provably covers it
5857 // since `a` is a register of the executing proto.
5858 //
5859 // Still over-roots caller frames' dead regs
5860 // (slots between caller.base and the callee
5861 // func_slot are live; slots past callee
5862 // func_slot in caller's frame are dead until
5863 // caller resumes). For fire-once cap path this
5864 // residual over-root is acceptable; full
5865 // per-frame walk was canceled per
5866 // `.dev/rfcs/v2.6-plan-state.md` amendments log
5867 // (charter §2.1's strong/weak pass split is
5868 // semantically impossible — weak pass depends on
5869 // strong-pass marks).
5870 let cap_root_top = self
5871 .frames
5872 .iter()
5873 .rev()
5874 .find_map(CallFrame::lua)
5875 .map(|f| f.base + f.closure.proto.max_stack as u32)
5876 .unwrap_or(self.top);
5877 self.gc_top = cap_root_top.max(self.top);
5878 self.collect_garbage();
5879 if self.heap.bytes() > cap {
5880 self.heap.mem_cap = None;
5881 let s = Value::Str(self.heap.intern(b"memory cap exceeded"));
5882 return Err(LuaError(s));
5883 }
5884 }
5885 }
5886 // Single combined frame fetch: continuation arm OR Lua arm. Saves
5887 // a second `self.frames.last()` slice access vs the prior split
5888 // form (LLVM doesn't always CSE these across the cont branch).
5889 // A continuation frame on top means the call it protected just
5890 // delivered its results — wrap as `true, results…` and hand to
5891 // the pcall/xpcall caller. The error path is handled by `unwind`;
5892 // this branch is only reached on success/resume completion.
5893 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
5894 let frame_peek = unsafe { self.frames.last().unwrap_unchecked() };
5895 if let &CallFrame::Cont(nc) = frame_peek {
5896 // a yieldable metamethod returned: complete the interrupted
5897 // instruction (PUC luaV_finishOp) and resume the running frame.
5898 if let ContKind::Meta(mc) = nc.kind {
5899 frames_pop_sync(&mut self.frames, &mut self.frames_top);
5900 let result = if self.top > nc.func_slot {
5901 self.stack[nc.func_slot as usize]
5902 } else {
5903 Value::Nil
5904 };
5905 self.stack.truncate(nc.func_slot as usize);
5906 self.top = mc.saved_top;
5907 self.finish_meta(mc.action, result)?;
5908 continue;
5909 }
5910 // a __close handler returned successfully: discard its
5911 // results, restore `top` to the slot the handler was called
5912 // at (the surrounding frame's register window above this slot
5913 // must stay alloc'd — never truncate the underlying stack),
5914 // then continue the close chain (next slot, or fire
5915 // AfterClose). When the close ends an entry activation,
5916 // drive_close hands the results up to exec_with directly.
5917 if let ContKind::Close(cc) = nc.kind {
5918 frames_pop_sync(&mut self.frames, &mut self.frames_top);
5919 self.top = nc.func_slot;
5920 if let Some(vals) =
5921 self.drive_close(cc.from, cc.pending, cc.after, entry_depth)?
5922 {
5923 return Ok(vals);
5924 }
5925 continue;
5926 }
5927 // __pairs returned: normalize its results to exactly four
5928 // (iterator, state, control, closing) at pairs's slot, where
5929 // the metamethod was called, and hand them to pairs's caller.
5930 if let ContKind::Pairs = nc.kind {
5931 frames_pop_sync(&mut self.frames, &mut self.frames_top);
5932 let total = 4u32;
5933 let need = (nc.func_slot + total) as usize;
5934 if self.stack.len() < need {
5935 self.stack.resize(need, Value::Nil);
5936 }
5937 for s in self.top..(nc.func_slot + total) {
5938 self.stack[s as usize] = Value::Nil;
5939 }
5940 self.top = nc.func_slot + total;
5941 if self.frames.len() < entry_depth {
5942 return Ok(self.take_results(nc.func_slot));
5943 }
5944 self.finish_results(nc.func_slot, total, nc.nresults);
5945 continue;
5946 }
5947 frames_pop_sync(&mut self.frames, &mut self.frames_top);
5948 self.pcall_depth -= 1;
5949 // f's results sit at nc.func_slot+1.. (f was called one slot
5950 // above the continuation), so writing `true` at the slot makes
5951 // `true, results…` already contiguous.
5952 let nret = self.top - (nc.func_slot + 1);
5953 self.stack[nc.func_slot as usize] = Value::Bool(true);
5954 let total = 1 + nret;
5955 self.top = nc.func_slot + total;
5956 if self.frames.len() < entry_depth {
5957 return Ok(self.take_results(nc.func_slot));
5958 }
5959 self.finish_results(nc.func_slot, total, nc.nresults);
5960 continue;
5961 }
5962 // GC runs only at the allocation safe points below (PUC's
5963 // `luaC_checkGC` sites), each with a precise `gc_top`; the loop head
5964 // no longer collects, so a stale full-window `gc_top` cannot leak in.
5965 //
5966 // Hot-path frame fetch: the Cont arm above continues the loop,
5967 // so reaching here means `frame_peek` is the Lua frame. Reuse it
5968 // rather than re-fetching `self.frames.last()`.
5969 let f = match frame_peek {
5970 CallFrame::Lua(f) => f,
5971 _ => unreachable!("Cont frame survived the dispatch loop head"),
5972 };
5973 let cl = f.closure;
5974 let base = f.base;
5975 let func_slot = f.func_slot;
5976 let n_varargs = f.n_varargs;
5977 let pc = f.pc;
5978 let oldpc = f.hook_oldpc;
5979
5980 // SAFETY: `pc` is bounded by the compiler against `proto.code.len()`
5981 // — every branch / call op only sets `pc` to a valid index, and
5982 // function entry initialises pc=0 with a non-empty body. PUC's
5983 // `vmfetch` uses the equivalent unchecked load.
5984 let inst = unsafe { *cl.proto.code.get_unchecked(pc as usize) };
5985
5986 // P12-S1.C/D — trace recording append + close detection.
5987 // Gated on `trace_jit_enabled` + `active_trace.is_some()`
5988 // so default dispatch keeps a single not-taken branch.
5989 //
5990 // - At the head PC with a non-empty record, the trace has
5991 // looped back to its start: mark `closed = true` and
5992 // take the record (S2 will compile + cache).
5993 // - Otherwise, capture the op. If the record overflows
5994 // MAX_TRACE_LEN, abort by dropping it.
5995 if self.jit.trace_enabled
5996 && let Some(_rec) = self.jit.active_trace.as_mut()
5997 {
5998 // P12-S4 — depth tracking. The trace head's frame is
5999 // at index `recording_frame_base`; every Op::Call that
6000 // pushes a new frame bumps the live depth, every
6001 // Op::Return that pops one decrements it.
6002 //
6003 // **Three clean-close conditions** (P12-S4-step4a):
6004 // - `at_head`: cur_depth == 0 AND about-to-execute the
6005 // trace's head_pc on its head_proto (loop closed back
6006 // to start). Same for loop-triggered and call-triggered
6007 // traces — step4a unified the gating so call-triggered
6008 // no longer closes on the first re-entry (that left
6009 // fib's body at 7 depth=0 ops; step4a lets it inline
6010 // up to MAX_INLINE_DEPTH levels before any close).
6011 // - `returned_past_head`: trace head's frame is gone
6012 // (callee returned past it, or the call-trigger
6013 // started a recording inside a callee that has now
6014 // returned). Whatever ops were recorded form the
6015 // trace body; the lowerer treats the partial trace
6016 // the same as InlineAbort (dispatchable=false until
6017 // step4b's frame materialization lands).
6018 // - `depth_cap_hit`: cur_depth > MAX_INLINE_DEPTH.
6019 // Recording any deeper would just bloat the IR; close
6020 // with the body we have. Lowerer's existing length
6021 // gate + InlineAbort path handles short bodies.
6022 let returned_past_head = self.frames.len() <= self.jit.recording_frame_base;
6023 let cur_depth = if returned_past_head {
6024 0
6025 } else {
6026 self.frames.len() - 1 - self.jit.recording_frame_base
6027 };
6028 let depth_cap_hit = cur_depth > crate::jit::trace::MAX_INLINE_DEPTH as usize;
6029 let rec = self.jit.active_trace.as_mut().expect("just checked Some");
6030 let at_head_loop = cur_depth == 0
6031 && !rec.ops.is_empty()
6032 && !returned_past_head
6033 && std::ptr::eq(cl.proto.as_ptr(), rec.head_proto.as_ptr())
6034 && pc == rec.head_pc;
6035 // P16-A — self-link cycle catch (mirrors LuaJIT's
6036 // `check_call_unroll` at `lj_record.c:1869`). Trips when:
6037 // 1. We're about to execute the head_pc on head_proto
6038 // at depth > 0 (we're re-entering the trace head
6039 // from inside an inlined recursion level — UpRec).
6040 // 2. The count of ancestor frames in the recording
6041 // window that share `head_proto` exceeds
6042 // [`RECUNROLL_THRESHOLD`] (default 2).
6043 // For fib(N): head_pc=0, head_proto=fib. After 2 inline
6044 // recursion levels are captured, the recorder enters
6045 // the 3rd nested fib frame, sees cur_depth=3 > 2, and
6046 // trips this catch — closing with `SelfRecKind::UpRec`.
6047 // The lowerer's `TraceEnd::SelfLink` tail emits the
6048 // bump-base + branch-to-self loop body.
6049 //
6050 // TailRec vs UpRec: LJ distinguishes via
6051 // `framedepth + retdepth == 0`. luna doesn't track
6052 // retdepth separately; cur_depth == 0 with a non-empty
6053 // call chain in tail position is rare (would require
6054 // explicit Lua TCO). We use cur_depth > 0 as the UpRec
6055 // condition (fib's case); cur_depth == 0 with positive
6056 // ancestor count would route to TailRec, but luna's
6057 // recorder doesn't currently produce that shape because
6058 // tail-call elision pops the caller frame and we'd
6059 // hit `at_head_loop` instead.
6060 let self_link_trip: Option<crate::jit::trace::SelfRecKind> = {
6061 if self.jit.p16_self_link_enabled
6062 && !returned_past_head
6063 && std::ptr::eq(cl.proto.as_ptr(), rec.head_proto.as_ptr())
6064 && pc == rec.head_pc
6065 && cur_depth > 0
6066 {
6067 // Count ancestor frames sharing head_proto.
6068 // self.frames[recording_frame_base..] currently
6069 // includes the just-pushed frame at the top
6070 // (the one about to execute head_pc). Ancestors
6071 // = the slice excluding the top frame.
6072 let head_proto_ptr = rec.head_proto.as_ptr();
6073 let last_idx = self.frames.len() - 1;
6074 let mut count = 0usize;
6075 for i in self.jit.recording_frame_base..last_idx {
6076 if let CallFrame::Lua(f) = &self.frames[i]
6077 && std::ptr::eq(f.closure.proto.as_ptr(), head_proto_ptr)
6078 {
6079 count += 1;
6080 }
6081 }
6082 if count > crate::jit::trace::RECUNROLL_THRESHOLD {
6083 // cur_depth > 0 → UpRec (fib pattern).
6084 // cur_depth == 0 wouldn't reach this arm.
6085 Some(crate::jit::trace::SelfRecKind::UpRec)
6086 } else {
6087 None
6088 }
6089 } else {
6090 None
6091 }
6092 };
6093 if let Some(kind) = self_link_trip {
6094 // v2.0 Track-R R3.3+ sub-0 — SelfLink relax for
6095 // self-recursive patterns at frame depth >= 2.
6096 //
6097 // Pre sub-0: a SelfLink trip at the head_pc re-entry
6098 // unconditionally stamped `self_link_kind`. The
6099 // R3a `downrec_close` marker can only fire from the
6100 // depth>0 Op::Return path (`rec.retfs` chain),
6101 // which never reaches the recorder for fib(28)-like
6102 // shapes that hit the SelfLink cycle catch BEFORE
6103 // any base-case Return — leaving `downrec_close`
6104 // None and routing the trace through R1's safe
6105 // `dispatchable=false` `"self-link-retf-r1"` path
6106 // (audit measured `trace_dispatched = 0`).
6107 //
6108 // Sub-0 lift: when the SelfLink trip fires AND
6109 // `cur_depth >= 2` (the count > RECUNROLL_THRESHOLD
6110 // gate already requires this — kept explicit as a
6111 // safety floor), route the close through `downrec_
6112 // close` INSTEAD of `self_link_kind`. The recorder
6113 // synthesises the close marker from the most
6114 // recent Op::Call at depth `cur_depth - 1`:
6115 // - `return_pc` = `call.pc + 1` (caller's resume
6116 // PC after the recursive call returns; mirror
6117 // of R3a's `caller_pc` derivation at the
6118 // depth>0 Op::Return capture path below).
6119 // - `target_proto` = `call.proto` (caller's
6120 // proto; equals `rec.head_proto` for self-
6121 // recursion).
6122 // - `depth_delta` = `1` (today's recorder always
6123 // unrolls one level; R3a uses the same
6124 // constant).
6125 //
6126 // The lowerer's `end_idx` picker (`trace.rs:3729`)
6127 // routes through `TraceEnd::DownRec` ahead of the
6128 // `self_link_kind` arm; the R3b/R3d lowerer arm
6129 // emits the stitch-sentinel + caller-pc-guard
6130 // scaffold. Single-candidate guard chain (sub-0's
6131 // recorder produces 1 caller_pc candidate because
6132 // `rec.retfs` is empty) keeps `dispatchable=false`
6133 // + `"downrec-stitch-pending"` label (per R3d's
6134 // `multi_way_candidate_count >= 2` gate at
6135 // `trace.rs:7385`). Net behaviour: trace compiles
6136 // under DownRec routing; interp runs the
6137 // recursion naturally → result 317811.
6138 //
6139 // The `cur_depth >= 2` gate is automatically
6140 // satisfied by the count > RECUNROLL_THRESHOLD=2
6141 // trip condition (3 ancestor frames sharing
6142 // head_proto implies cur_depth >= 3), kept
6143 // explicit so a future RECUNROLL_THRESHOLD tweak
6144 // doesn't silently flip shallow-recursion
6145 // shapes (cur_depth == 1) onto the DownRec arm.
6146 //
6147 // R3.3+ sub-1/2/3/4 will replace the depth-baked
6148 // op_offsets[] addressing with runtime base_var
6149 // threading so the trace's recorded body is
6150 // depth-relative and the DownRec dispatch
6151 // becomes wall-clock-positive. Sub-0 is the
6152 // routing scaffold; it does not aim for gain.
6153 let _ = kind;
6154 let relaxed_to_downrec = cur_depth >= 2 && rec.downrec_close.is_none() && {
6155 let caller_depth_u8 = (cur_depth - 1) as u8;
6156 if let Some(call_op) = rec.ops.iter().rev().find(|r| {
6157 r.inline_depth == caller_depth_u8
6158 && matches!(r.inst.op(), crate::vm::isa::Op::Call)
6159 }) {
6160 rec.downrec_close = Some(crate::jit::trace::DownRecClose {
6161 return_pc: call_op.pc + 1,
6162 target_proto: call_op.proto,
6163 depth_delta: 1,
6164 });
6165 true
6166 } else {
6167 false
6168 }
6169 };
6170 if relaxed_to_downrec {
6171 // R2 close-cause taxonomy: tag the lift so
6172 // probes can tally the fire rate. Mirrors
6173 // R3a's `"downrec-restart"` bump for the
6174 // depth>0 Op::Return path (different trip
6175 // origin, same downstream routing). The
6176 // existing `"self-link-retf-r1"` label still
6177 // fires for trips that DON'T relax (no
6178 // candidate Op::Call ancestor in rec.ops, or
6179 // cur_depth < 2) via the lowerer's
6180 // dispatch_off_reason mirror at the close
6181 // handler — kept as a regression safety net.
6182 self.jit
6183 .counters
6184 .bump_close_cause("selflink-yields-to-downrec");
6185 } else {
6186 rec.self_link_kind = Some(kind);
6187 }
6188 }
6189 let should_close =
6190 at_head_loop || returned_past_head || depth_cap_hit || self_link_trip.is_some();
6191 if should_close {
6192 // P13-S13-H — long-trace bias: a call-triggered
6193 // recording that closed with a very short body
6194 // (fib base case: `Lt`/`Jmp`/`Return1` = 3 ops,
6195 // binary_trees `make(0)`: 4 ops) is pathological.
6196 // Compiling + caching it pins `Proto.traces` to a
6197 // trace that the length gate will refuse to
6198 // dispatch (per `MIN_DISPATCHABLE_TRUNC_BODY_FLOOR
6199 // = 40`), AND blocks the back-edge / longer-call
6200 // path from re-recording the same head_pc (the
6201 // dedup `already_cached` check below short-
6202 // circuits). The fix: discard the short call-
6203 // triggered recording WITHOUT caching, and bias
6204 // the proto's `call_hot_count` back to
6205 // `THRESHOLD - HOT_RETRY_WINDOW` so the next
6206 // sequence of calls retries the trigger at a
6207 // different (hopefully deeper) recursion point.
6208 //
6209 // Back-edge triggered traces are exempt — a
6210 // tight numeric-for loop's body is legitimately
6211 // 3 ops (`Add`, ForLoop) and DOES dispatch
6212 // usefully when re-entered many times.
6213 // P13-S13-H — coverage heuristic to detect
6214 // pathologically partial call-triggered traces:
6215 // for self-recursive / branchy protos like
6216 // `fib` (~17 bytecode ops) or
6217 // `binary_trees.make` (~26 ops), the recorder
6218 // can fire at a BASE-case entry (`fib(0)` or
6219 // `make(0)`) producing a 3–4 op trace that
6220 // covers a tiny fraction of the proto's code.
6221 // That trace is doomed by the length gate
6222 // post-compile AND blocks any longer follow-up
6223 // (the dedup `already_cached` check below). The
6224 // fix: discard call-triggered closes where
6225 // `rec.ops.len() * 2 < head_proto.code.len()`
6226 // (less than half the proto's bytecode), so the
6227 // back-edge / longer call path can take over.
6228 //
6229 // Why coverage > raw length:protos with
6230 // intrinsically short bodies (closure
6231 // factories: `Closure + Return1` = 2 ops,
6232 // simple wrappers: `LoadI + Return1` = 2 ops)
6233 // record 100% coverage even at length 2 — those
6234 // ARE legitimately short and the closure /
6235 // sunk-emit lowering paths (S7-A / S9-C) make
6236 // them worth compiling. The heuristic admits
6237 // them. fib's `[Lt, Jmp, Return1]` (3 of ~17)
6238 // and make's `[Lt, Jmp, LoadI, Return1]` (4 of
6239 // ~26) get discarded.
6240 //
6241 // Back-edge triggered traces are unaffected —
6242 // a tight numeric-for body legitimately covers
6243 // 3 of ~3 proto ops it can dispatch from
6244 // (`Add + ForLoop`) and the recorder fires on
6245 // the back-edge, not call entry.
6246 //
6247 // `call_hot_count` is intentionally NOT reset
6248 // (an earlier draft tried `THRESHOLD - 32` but
6249 // caused active_trace contention with the
6250 // outer back-edge trigger — see
6251 // setlist_b_zero_with_call_c_zero_sunk_emits).
6252 // We give up on dispatching the pathological
6253 // shape on the same proto; the back-edge or a
6254 // longer call path on a deeper recursion point
6255 // can still record + cache a real trace.
6256 let proto_code_len = rec.head_proto.code.len();
6257 let is_partial_coverage = rec.ops.len() * 2 < proto_code_len;
6258 // P13-S13-I — per-Proto discard cap. The S13-H
6259 // relaxed trigger condition (`c >= THRESHOLD &&
6260 // !already_cached`) means a Proto whose every
6261 // recording is partial-coverage will re-fire the
6262 // trigger every call indefinitely (1500+ in
6263 // `binary_trees`-pattern test). The cap stops
6264 // discarding after `MAX_DISCARDS_PER_PROTO` —
6265 // the next close falls through to compile (even
6266 // if partial), caches the trace, and the
6267 // `already_cached` short-circuit kills the
6268 // storm. Dispatch may still be refused
6269 // post-compile (length gate), but the recorder
6270 // stops churning.
6271 const MAX_DISCARDS_PER_PROTO: u32 = 5;
6272 let prior_discards = rec.head_proto.trace_discard_count.get();
6273 let cap_reached = prior_discards >= MAX_DISCARDS_PER_PROTO;
6274 // P13-S13-K — flip the `gave_up` flag the
6275 // moment cap is reached (BEFORE the close-
6276 // dispatching branch below). The trigger gates
6277 // short-circuit on this flag, skipping the
6278 // RefCell + linear `already_cached` scan on
6279 // every subsequent call to this Proto. Useful
6280 // for `binary_trees_pattern`-class loads where
6281 // a single Proto sees ~20k calls post-cap.
6282 if cap_reached
6283 && rec.is_call_triggered
6284 && is_partial_coverage
6285 && !rec.head_proto.trace_gave_up.get()
6286 {
6287 rec.head_proto.trace_gave_up.set(true);
6288 }
6289 if rec.is_call_triggered && is_partial_coverage && !cap_reached {
6290 // Tally as closed (for visibility) but DROP
6291 // without compile/cache. Use the existing
6292 // closed-lens accumulator so probes can
6293 // observe the discarded shape.
6294 // P13-S13-I — bump discard count BEFORE
6295 // dropping the recording so the next
6296 // close sees the updated counter.
6297 rec.head_proto.trace_discard_count.set(prior_discards + 1);
6298 self.jit.counters.closed += 1;
6299 self.jit
6300 .counters
6301 .closed_lens
6302 .push((rec.is_call_triggered, rec.ops.len()));
6303 // v2.0 Track-R R2 — partial-coverage discard
6304 // close path. Pre-R2 this site bumped `closed`
6305 // + `closed_lens` (visibility) but no per-
6306 // reason label, so probes couldn't separate a
6307 // real successful close from a discard tally.
6308 // Tag explicitly to make the recorder-side
6309 // close-cause taxonomy single-source.
6310 self.jit
6311 .counters
6312 .bump_close_cause("partial-coverage-discard");
6313 self.jit.active_trace = None;
6314 // Continue with interp loop — don't
6315 // fall through to compile path.
6316 // The op at `pc` hasn't dispatched yet;
6317 // the outer loop iteration handles it.
6318 } else {
6319 rec.closed = true;
6320 // P12-S2.C — detach the closed record, then try
6321 // to compile it. Dedup by `head_pc`: a Proto
6322 // already carrying a CompiledTrace for this PC
6323 // skips recompile (the hot counter caps
6324 // re-recording at `u32::MAX / 2` anyway, but
6325 // explicit dedup keeps `Proto.traces` short
6326 // for the S3 dispatcher's linear scan).
6327 //
6328 // No `Vm::run` change for failure: we just bump
6329 // the failed counter and drop the record. S3
6330 // will read `Proto.traces` to decide whether to
6331 // dispatch — until then, this is bookkeeping.
6332 let head_pc_val = rec.head_pc;
6333 let closed_record = self
6334 .jit
6335 .active_trace
6336 .take()
6337 .expect("active_trace was Some this branch");
6338 self.jit.counters.closed += 1;
6339 self.jit
6340 .counters
6341 .closed_lens
6342 .push((closed_record.is_call_triggered, closed_record.ops.len()));
6343 // P12-S5-B fix: cache the trace on the
6344 // recorder's *head proto*, not the current
6345 // closure's proto. For non-recursive
6346 // call-triggered traces, close fires after
6347 // `Return1` pops the callee frame — `cl` at
6348 // that point is the CALLER's closure, while
6349 // `closed_record.head_proto` is the CALLEE's
6350 // proto (the one we actually want the trace
6351 // to be discoverable from on the next call).
6352 // Self-recursive fib closed via depth-cap
6353 // mid-recursion so `cl.proto == head_proto`
6354 // happened to coincide — this fix makes that
6355 // accidental coincidence intentional.
6356 let head_proto = closed_record.head_proto;
6357 let already_cached = head_proto
6358 .traces
6359 .borrow()
6360 .iter()
6361 .any(|t| t.head_pc == head_pc_val);
6362 if !already_cached {
6363 // Internal-loop = true: the trace runs in
6364 // a native loop until a cmp side-exits, so
6365 // the dispatcher's per-entry marshal cost
6366 // amortizes across the whole run of
6367 // iterations the loop's recorded direction
6368 // stays valid. The lowerer auto-downgrades
6369 // to one-shot for cmp-less or Call-truncating
6370 // traces.
6371 // P15-A v2-C-A6-5 — side traces MUST NOT
6372 // internal-loop. The parent's recorded prefix
6373 // (ops at PCs < side trace's head_pc) defines
6374 // values for registers the child's body reads
6375 // without re-writing each iter — e.g. for
6376 // s12_step_b, parent's `pc=19 Add R[12] = R[1]
6377 // + R[11]` sets R[12], and the child trace
6378 // (head_pc=24) re-runs `pc=20 Move R[1] =
6379 // R[12]` each iter via its outer ForLoop
6380 // internal-loop, ALWAYS reading the stale
6381 // entry-time R[12]. The parent's Add never
6382 // re-runs during child's loop, so R[1] gets
6383 // pinned to one stale value. Force one-shot
6384 // for side traces: each parent-exit round-
6385 // trips through dispatcher → parent's Add
6386 // runs → side trace runs ONE iter → return.
6387 let opts = crate::jit::trace::CompileOptions {
6388 internal_loop: closed_record.side_trace_parent.is_none(),
6389 pre53: self.version() <= LuaVersion::Lua53,
6390 aot: false,
6391 };
6392 // v1.1 A1 Session A — route through trace_compiler.
6393 // v2.0 Track J sub-step J-B — split-borrow JitState
6394 // so the trait method can take `&mut dyn JitStorage`.
6395 let result = {
6396 let jit = &mut self.jit;
6397 let storage: &mut dyn crate::jit::JitStorage = jit.storage.as_mut();
6398 jit.trace_compiler
6399 .try_compile_trace(storage, &closed_record, opts)
6400 };
6401 match result {
6402 Some(mut ct) => {
6403 // P12-S5-A/B/C — tally Sinkable sites
6404 // + actually-sunk-emit sites + materialise
6405 // emit sites before moving `ct` into
6406 // Proto.traces.
6407 self.jit.counters.sinkable_seen +=
6408 ct.sinkable_sites_seen as u64;
6409 self.jit.counters.accum_bufferable_seen +=
6410 ct.accum_bufferable_seen as u64;
6411 self.jit.counters.sunk_alloc += ct.sunk_alloc_seen as u64;
6412 self.jit.counters.materialize_emit +=
6413 ct.materialize_emit_count as u64;
6414 self.jit.counters.closure_emit += ct.closure_seen as u64;
6415 if ct.is_inline_abort_close {
6416 self.jit.counters.inline_abort += 1;
6417 }
6418 // v2.0 Stage 7 polish 6 fire
6419 // experiment — split tally so a
6420 // probe can answer the AOT
6421 // `accepted_with_per_exit_inline`
6422 // gate's question at the JIT
6423 // surface too: how many compiled
6424 // traces emitted depth>0 cmp
6425 // side-exits, and how many of
6426 // those survived all the
6427 // `dispatchable = false` pins
6428 // (`InlineAbort-gate`,
6429 // `self-link-retf-r1`,
6430 // `downrec-stitch-pending`, etc.).
6431 if !ct.per_exit_inline.is_empty() {
6432 self.jit.counters.per_exit_inline_compiled += 1;
6433 if ct.dispatchable {
6434 self.jit.counters.per_exit_inline_dispatchable += 1;
6435 }
6436 }
6437 if let Some(reason) = ct.dispatch_off_reason {
6438 self.jit.counters.dispatch_off_reasons.push(reason);
6439 // v2.0 Track-R R2 — mirror
6440 // the ordered Vec push into
6441 // the per-reason HashMap so
6442 // probes can answer "how many
6443 // of each dispatch_off label
6444 // fired" in O(1) without
6445 // walking the Vec. Same
6446 // bucket as the recorder-side
6447 // abort/discard tags above.
6448 self.jit.counters.bump_close_cause(reason);
6449 }
6450 // v2.0 Track-R R3b — count
6451 // compiled traces that carry a
6452 // down-recursion stitch link.
6453 // Bumped here (not at the lowerer
6454 // emit site) because the Vm's
6455 // JitCounters live on the Vm,
6456 // and the lowerer doesn't have a
6457 // Vm handle. R3b's regression
6458 // pin reads this via
6459 // `Vm::trace_downrec_link_compiled_count`.
6460 if ct.downrec_link.is_some() {
6461 self.jit.counters.downrec_link_compiled += 1;
6462 }
6463 // v2.0 Track-R R3d — multi-way
6464 // guard emit counter. Bumped when
6465 // the lowerer's R3d arm collected
6466 // >= 2 distinct caller_pc candidates
6467 // and lifted `dispatchable=true`.
6468 // R3c's single-CMP shape stores
6469 // `1` here without bumping; non-
6470 // DownRec closes store `0`.
6471 if ct.downrec_multi_way_count >= 2 {
6472 self.jit.counters.multi_way_guard_emitted += 1;
6473 }
6474 // P15-A v2-A — side-trace finalisation.
6475 // Pin `dispatchable=false` so the
6476 // primary lookup `traces.find(|t|
6477 // t.head_pc == pc && t.dispatchable)`
6478 // never matches this entry — the
6479 // side trace is meant to be entered
6480 // ONLY through the parent's exit
6481 // indirection (v2-B/C IR), not the
6482 // back-edge / call-trigger paths.
6483 // Then write the entry fn ptr into
6484 // the parent's `exit_side_trace_ptrs`
6485 // slot so v2-B/C IR can read it.
6486 if let Some((parent_proto, parent_head_pc, parent_exit_idx)) =
6487 closed_record.side_trace_parent
6488 {
6489 ct.dispatchable = false;
6490 let entry_ptr = ct.entry as *const () as *const u8;
6491 let _side_trace_head_pc = closed_record.head_pc;
6492 let parent_traces = parent_proto.traces.borrow();
6493 if let Some(parent_ct) = parent_traces
6494 .iter()
6495 .find(|t| t.head_pc == parent_head_pc)
6496 {
6497 // P15-A v2-C-A5-C — shape-match
6498 // gate. Find the parent's per-exit
6499 // tag snapshot at the wired exit
6500 // (inline / tag / global) and
6501 // check the child's entry_tags
6502 // match. If not, leave the cell
6503 // null + skip cache populate so
6504 // the future v2-C-A2 IR's
6505 // `call_indirect` stays inert at
6506 // this exit (the child's
6507 // shape-specialised IR would
6508 // mis-interpret raw bits the
6509 // parent writes to reg_state).
6510 let inline_n = parent_ct.per_exit_inline.len();
6511 let tags_n = parent_ct.per_exit_tags.len();
6512 let parent_exit_tags_slice: &[
6513 crate::jit::trace::ExitTag
6514 ] = if parent_exit_idx < inline_n {
6515 &parent_ct.per_exit_inline
6516 [parent_exit_idx]
6517 .exit_tags
6518 } else if parent_exit_idx
6519 < inline_n + tags_n
6520 {
6521 &parent_ct.per_exit_tags
6522 [parent_exit_idx - inline_n]
6523 .1
6524 } else {
6525 &parent_ct.exit_tags
6526 };
6527 let shape_ok =
6528 crate::jit::trace::exit_tags_match_entry_tags(
6529 &ct.entry_tags,
6530 parent_exit_tags_slice,
6531 &parent_ct.entry_tags,
6532 );
6533 if !shape_ok {
6534 self.jit.counters.side_trace_shape_mismatch += 1;
6535 }
6536 // P15-A v2-C-A4 — write the child's
6537 // entry fn ptr to BOTH the legacy
6538 // v2-A `exit_side_trace_ptrs[idx]`
6539 // cell (kept so v2-A's
6540 // walk_any_side_ptr_non_null tests
6541 // stay green) AND the per-kind cell
6542 // whose heap address the parent's
6543 // IR baked (v2-C-A2). The IR-baked
6544 // cell is what the call_indirect
6545 // gate actually reads. Only write
6546 // when A5-C shape gate passes.
6547 if shape_ok {
6548 if let Some(cell) = parent_ct
6549 .exit_side_trace_ptrs
6550 .get(parent_exit_idx)
6551 {
6552 cell.set(entry_ptr);
6553 }
6554 // Compute (kind, local) for the
6555 // IR-baked cell. Layout follows
6556 // exit_hit_counts: inline first,
6557 // then per_exit_tags, then the
6558 // global tail slot.
6559 let (sent_kind, sent_local) = if parent_exit_idx
6560 < inline_n
6561 {
6562 parent_ct.per_exit_inline[parent_exit_idx]
6563 .side_trace_ptr
6564 .set(entry_ptr);
6565 (
6566 crate::jit::trace::SIDE_SENT_KIND_INLINE,
6567 parent_exit_idx as u32,
6568 )
6569 } else if parent_exit_idx < inline_n + tags_n {
6570 let local = parent_exit_idx - inline_n;
6571 if let Some(b) =
6572 parent_ct.tags_side_trace_ptrs.get(local)
6573 {
6574 b.set(entry_ptr);
6575 }
6576 (
6577 crate::jit::trace::SIDE_SENT_KIND_TAG,
6578 local as u32,
6579 )
6580 } else {
6581 parent_ct.global_side_trace_ptr.set(entry_ptr);
6582 (crate::jit::trace::SIDE_SENT_KIND_GLOBAL, 0)
6583 };
6584 self.jit.counters.side_trace_compiled += 1;
6585 // P15-A v2-D-A8 — flip the
6586 // parent's fast-path hint so
6587 // the dispatcher knows to do
6588 // the tentative decode + cell
6589 // check on subsequent
6590 // dispatches. Set once and
6591 // stays true (we never unwire
6592 // a side trace today).
6593 parent_ct.has_any_side_wired.set(true);
6594
6595 // P15-A v2-C-A1/A4 — populate
6596 // the O(1) lookup cache the
6597 // dispatcher consults on
6598 // sentinel-bit-set returns.
6599 // Key is the encoded sentinel
6600 // (same encoding the IR ORs
6601 // into bits 56..=62 of the
6602 // child's i64 return).
6603 let sentinel =
6604 crate::jit::trace::encode_side_sentinel(
6605 sent_kind, sent_local,
6606 );
6607 let predicted_idx = if std::ptr::eq(
6608 parent_proto.as_ptr(),
6609 head_proto.as_ptr(),
6610 ) {
6611 parent_traces.len() as u32
6612 } else {
6613 head_proto.traces.borrow().len() as u32
6614 };
6615 parent_ct
6616 .side_trace_cache
6617 .borrow_mut()
6618 .insert(sentinel, predicted_idx);
6619 }
6620 }
6621 drop(parent_traces);
6622 }
6623 head_proto.traces.borrow_mut().push(TArc::new(ct));
6624 self.jit.counters.compiled += 1;
6625 }
6626 None => {
6627 self.jit.counters.compile_failed += 1;
6628 self.jit
6629 .counters
6630 .compile_failed_reasons
6631 .push(self.jit.trace_compiler.last_compile_checkpoint());
6632 }
6633 }
6634 }
6635 } // P13-S13-H — close the long-trace-bias else branch
6636 } else {
6637 // P12-S4-step1 + step4a — depth-aware push at the
6638 // current `cur_depth`. The `depth_cap_hit` /
6639 // `returned_past_head` early-exit is handled by
6640 // the `should_close` branch above; reaching here
6641 // means `cur_depth <= MAX_INLINE_DEPTH` and the
6642 // trace head's frame is still live.
6643 let depth_u8 = cur_depth as u8;
6644 if depth_u8 > self.jit.max_depth_seen {
6645 self.jit.max_depth_seen = depth_u8;
6646 }
6647 // P12-S9-A — fix up a prior `Op::Call C=0` (multi-
6648 // return / variable return count). Recorder pushed
6649 // it with var_count=None before the call dispatched;
6650 // now that the call has returned and we're about to
6651 // push the next op, top reflects the actual return
6652 // count. Snapshot top - (caller.base + call.a).
6653 if let Some(last) = rec.ops.last_mut()
6654 && matches!(last.inst.op(), crate::vm::isa::Op::Call)
6655 && last.inst.c() == 0
6656 && last.var_count.is_none()
6657 && let Some(f) = self.frames.last().and_then(CallFrame::lua)
6658 {
6659 let from = f.base + last.inst.a();
6660 if self.top >= from {
6661 last.var_count = Some(self.top - from);
6662 }
6663 }
6664 // P12-S9-A/C — for SetList B=0, snapshot the source
6665 // count = top - A - 1 (mirrors Lua's `n = top - ra
6666 // - 1` from lvm.c OP_SETLIST). Sources are
6667 // R[A+1..top), exclusive top. For Call C=0's
6668 // var_count (the return count = top - A inclusive),
6669 // see the prior-op fix-up above; here we
6670 // initialise the current Call op to None and let
6671 // the fix-up on the next op's push populate it.
6672 let var_count = if matches!(inst.op(), crate::vm::isa::Op::SetList)
6673 && inst.b() == 0
6674 && let Some(f) = self.frames.last().and_then(CallFrame::lua)
6675 {
6676 let from = f.base + inst.a();
6677 if self.top > from {
6678 Some(self.top - from - 1)
6679 } else {
6680 None
6681 }
6682 } else {
6683 None
6684 };
6685 let op = crate::jit::trace::RecordedOp {
6686 proto: cl.proto,
6687 pc,
6688 inst,
6689 inline_depth: depth_u8,
6690 var_count,
6691 };
6692 // v2.0 Track-R R1 — depth>0 Return0/Return1 mirrors
6693 // LuaJIT's `IR_RETF` (lj_record.c:922+ lj_record_ret).
6694 // Captured as a side-channel `RetfRecord` parallel to
6695 // `ops` when `p16_self_link_enabled` is on. R3's
6696 // down-rec stitch consumes these to guard side-trace
6697 // inlined-frame topology against the recorded shape.
6698 // Gated on the same flag as the cycle catch so the
6699 // ship-default path (p16 off) sees zero behavior
6700 // change. `caller_pc` is the recorded enclosing Call's
6701 // pc + 1 — interp's resume point after the inlined
6702 // frame pops.
6703 if self.jit.p16_self_link_enabled
6704 && depth_u8 > 0
6705 && matches!(
6706 inst.op(),
6707 crate::vm::isa::Op::Return0 | crate::vm::isa::Op::Return1
6708 )
6709 {
6710 let results: u8 = match inst.op() {
6711 crate::vm::isa::Op::Return0 => 0,
6712 crate::vm::isa::Op::Return1 => 1,
6713 _ => 0,
6714 };
6715 // Most recent Op::Call recorded at the caller's
6716 // depth (`depth_u8 - 1`) is the frame this Return
6717 // is unwinding from. Reverse scan stops at the
6718 // first match.
6719 let caller_depth = depth_u8 - 1;
6720 let caller_call = rec.ops.iter().rev().find(|r| {
6721 r.inline_depth == caller_depth
6722 && matches!(r.inst.op(), crate::vm::isa::Op::Call)
6723 });
6724 let caller_pc = caller_call.map(|r| r.pc + 1).unwrap_or(pc);
6725 // v2.0 Track-R R3a — capture the caller's proto
6726 // for the RetfRecord. LuaJIT `IR_RETF.op1`
6727 // equivalent. For fib(28) the caller's proto
6728 // equals the trace head; for future mutual
6729 // recursion the recorded Op::Call's proto is the
6730 // right target. Fallback to head_proto when no
6731 // enclosing Call op was captured (mirrors
6732 // `caller_pc`'s fallback to the Return's own pc).
6733 let caller_proto = caller_call.map(|r| r.proto).unwrap_or(rec.head_proto);
6734 rec.retfs.push(crate::jit::trace::RetfRecord {
6735 from_depth: depth_u8,
6736 to_depth: caller_depth,
6737 results,
6738 caller_pc,
6739 proto: caller_proto,
6740 });
6741 // v2.0 Track-R R3a — DownRec close trigger:
6742 // count RetfRecords on this recording whose
6743 // `proto` matches `caller_proto` (LuaJIT
6744 // `check_downrec_unroll` chain filter
6745 // `op1 == ptref`). Threshold mirrors
6746 // RECUNROLL_THRESHOLD; first trip stamps the
6747 // `downrec_close` marker, subsequent retfs
6748 // keep the marker without overwrite. The
6749 // lowerer's end_idx picker routes through
6750 // TraceEnd::DownRec when the marker is set;
6751 // R3a's tail emit still falls through to R1's
6752 // safe deopt path so fib(28) result stays
6753 // 317_811. R3b lifts.
6754 if rec.downrec_close.is_none() {
6755 let caller_proto_ptr = caller_proto.as_ptr();
6756 let prior_match_count = rec
6757 .retfs
6758 .iter()
6759 .filter(|r| r.proto.as_ptr() == caller_proto_ptr)
6760 .count();
6761 // Strictly-greater-than threshold matches
6762 // LuaJIT `count + J->tailcalled > recunroll`.
6763 // The newly-pushed retf is already counted.
6764 if prior_match_count > crate::jit::trace::RECUNROLL_THRESHOLD {
6765 rec.downrec_close = Some(crate::jit::trace::DownRecClose {
6766 return_pc: caller_pc,
6767 target_proto: caller_proto,
6768 depth_delta: 1,
6769 });
6770 // R2 close-cause taxonomy: tag the
6771 // restart with `"downrec-restart"`. R3b
6772 // adds `"downrec-stitch-failed"` when
6773 // the lifted back-edge falls back to
6774 // deopt.
6775 self.jit.counters.bump_close_cause("downrec-restart");
6776 }
6777 }
6778 }
6779 // v2.1 Phase 1I.B — capture FieldIcSnapshot for the
6780 // FIRST eligible Op::GetField site under env-gate
6781 // LUNA_JIT_FIELD_IC=1. "Eligible" means:
6782 // - R[B] is Value::Table with metatable.is_none()
6783 // - K[C] is Value::Str
6784 // - The string key actually occupies a hash slot
6785 // (so the IC's slot_idx is a real index, not
6786 // a probe sentinel).
6787 // Once captured, subsequent GetFields skip this
6788 // logic (rec.field_ic_snapshot.is_some() short-
6789 // circuits). Env-OFF short-circuits on the cached
6790 // atomic check inside field_ic_enabled().
6791 if rec.field_ic_snapshot.is_none()
6792 && matches!(inst.op(), crate::vm::isa::Op::GetField)
6793 && crate::jit::trace_types::field_ic_enabled()
6794 {
6795 let b = inst.b();
6796 let c_idx = inst.c() as usize;
6797 let r_b = self.stack[(base + b) as usize];
6798 if let Value::Table(g) = r_b
6799 && g.metatable().is_none()
6800 && c_idx < cl.proto.consts.len()
6801 && let Value::Str(s) = cl.proto.consts[c_idx]
6802 {
6803 let key = Value::Str(s);
6804 let tbl_ref = &*g;
6805 if let Some(slot_idx) = tbl_ref.find_node_idx(key)
6806 && let Some(val) = tbl_ref.node_val_at(slot_idx)
6807 {
6808 let op_idx = rec.ops.len() as u32;
6809 rec.field_ic_snapshot =
6810 Some(crate::jit::trace_types::FieldIcSnapshot {
6811 op_idx,
6812 nodes_len: tbl_ref.nodes_capacity() as u64,
6813 slot_idx: slot_idx as u64,
6814 key_ptr_bits: s.as_ptr() as u64,
6815 cached_val_tag: val.tag_byte(),
6816 });
6817 self.jit.counters.field_ic_snapshot_captured += 1;
6818 }
6819 }
6820 }
6821 if !rec.push(op) {
6822 // v2.0 Track-R R2 — recorder overflow
6823 // (MAX_TRACE_LEN). Pre-R2 this site bumped
6824 // `aborted` with no reason label, leaving the
6825 // overflow indistinguishable from any other
6826 // abort cause that might be added later.
6827 // Tag it explicitly under the close-cause
6828 // bucket so probes can tally overflow vs
6829 // other abort causes in O(1).
6830 self.jit.active_trace = None;
6831 self.jit.counters.aborted += 1;
6832 self.jit.counters.bump_close_cause("trace-overflow");
6833 }
6834 }
6835 }
6836
6837 // P12-S3 — trace JIT dispatcher.
6838 //
6839 // When the dispatch loop is about to execute the op at
6840 // `pc` and there's a `numeric_only` CompiledTrace cached
6841 // for that `head_pc`, marshal the live regs into an
6842 // i64 buffer, jump into the trace, and resume the
6843 // interpreter at the returned continuation PC.
6844 //
6845 // Skipped (zero overhead) when `trace_jit_enabled` is
6846 // false; the lookup is a borrow + scan over
6847 // `cl.proto.traces`, which is a `Vec` whose size is at
6848 // most one entry per back-edge per Proto in practice.
6849 //
6850 // Marshalling contract — only Int slots survive the
6851 // round-trip cleanly (the reg_state ABI is `*mut i64`
6852 // with no tag info). Any non-Int slot in the affected
6853 // window forces a skip; interp takes over for one op
6854 // and the back-edge brings us back to try again next
6855 // pass (slots that were Nil/Float at one moment can
6856 // settle to Int by the time the next back-edge fires).
6857 //
6858 // A trace that comes back with `vm.jit.pending_err`
6859 // parked is treated as a deopt: clear the err, leave
6860 // the stack as the trace wrote it, and let the
6861 // interpreter run from the same `pc`. The trace itself
6862 // is left cached — a future entry might find no
6863 // metatable in the way and succeed.
6864 // P17-A1 (Path C #3) — single Rc<CompiledTrace> clone instead
6865 // of 6 per-field Rc clones. proto.traces is now
6866 // Vec<Rc<CompiledTrace>>; the dispatcher clones ONE Rc and
6867 // reads fields via auto-deref. fib_28 saves ~5 Rc::clone
6868 // operations per dispatch × 434k = ~2.2M Rc atomic ops
6869 // (~1-2% gain measured separately).
6870 // v2.0 Track-R R3c — one-shot consume of the
6871 // `suppress_downrec_admit_once` flag. Set by the R3c
6872 // downrec post-invoke arm below when it force-deopts the
6873 // trace (caller-pc guard miss OR cycle-budget exhausted)
6874 // so the NEXT interpreter loop iteration skips the
6875 // downrec admit, lets interp run the op at `head_pc`,
6876 // advances `pc` past `head_pc`, and breaks the otherwise-
6877 // infinite admit loop. Reading + clearing here means a
6878 // single dispatch tick consumes the suppression — the
6879 // following tick re-admits naturally (with the budget
6880 // also reset by the deopt site).
6881 let downrec_admit_blocked = self.jit.suppress_downrec_admit_once;
6882 self.jit.suppress_downrec_admit_once = false;
6883 if self.jit.trace_enabled
6884 && let Some(ct) = {
6885 let traces = cl.proto.traces.borrow();
6886 traces
6887 .iter()
6888 .find(|t| {
6889 if t.head_pc != pc {
6890 return false;
6891 }
6892 let is_downrec = t.downrec_link.is_some();
6893 // v2.0 Track-R R3c — the one-shot suppress
6894 // flag blocks any admit (primary or fallback)
6895 // for `downrec_link`-bearing traces so the
6896 // next interp iter can run the natural op
6897 // at `head_pc` and advance past it. R3d's
6898 // `dispatchable=true` lift means the suppress
6899 // must also cover the primary `t.dispatchable`
6900 // arm — otherwise the lifted lookup would
6901 // immediately re-admit after a force-deopt
6902 // and the infinite loop returns.
6903 if is_downrec && downrec_admit_blocked {
6904 return false;
6905 }
6906 // Primary arm: `dispatchable=true` traces
6907 // (R3d-lifted DownRec or normal traces).
6908 // Fallback arm: R3c-shape `dispatchable=false`
6909 // DownRec traces (single-CMP guard kept
6910 // pinned because the 90% miss-rate would
6911 // make blind admit perf-negative).
6912 t.dispatchable || is_downrec
6913 })
6914 .cloned()
6915 }
6916 {
6917 // Path C #6 — borrow Rc<[T]> fields as &Rc<[T]> instead
6918 // of cloning. The outer `ct: Rc<CompiledTrace>` is held
6919 // across the entire dispatch block so the fields outlive
6920 // all consumers. Saves 5 Rc::clone per dispatch.
6921 let entry_fn = ct.entry;
6922 let head_pc_val = ct.head_pc;
6923 let window_size = ct.window_size;
6924 let exit_tags = &ct.exit_tags;
6925 let per_exit_tags = &ct.per_exit_tags;
6926 let per_exit_inline = &ct.per_exit_inline;
6927 let compile_entry_tags = &ct.entry_tags;
6928 let global_tag_res_kind = ct.global_tag_res_kind;
6929 let exit_hit_counts = &ct.exit_hit_counts;
6930 let max_stack = cl.proto.max_stack as usize;
6931 let window_size_us = window_size as usize;
6932 let base_us = base as usize;
6933 // P12-S4-step3a — `reg_state` sized to the trace's
6934 // `window_size`, which today equals max_stack but
6935 // S4-step3b will expand for inlined frames.
6936 // Marshal-in still only writes [0..max_stack); slots
6937 // [max_stack..window_size) are zero-initialised and
6938 // filled by the trace's own GetUpval / arith.
6939 // P13-S13-D — reuse the Vm's amortised buffers
6940 // instead of allocating fresh Vecs each dispatch.
6941 // mem::take leaves an empty placeholder we restore
6942 // at the end of the dispatch block (success +
6943 // deopt paths both fall through to the restore).
6944 let mut entry_tags: Vec<u8> = std::mem::take(&mut self.jit.entry_tags_buf);
6945 entry_tags.clear();
6946 entry_tags.reserve(max_stack);
6947 // v2.0 Track-R R3c — this trace was admitted via the
6948 // `downrec_link.is_some()` arm rather than the normal
6949 // `dispatchable=true` arm. The pre-invoke path
6950 // populates a reserved saved-PC slot just past the
6951 // normal register window so R3b's lowerer guard load
6952 // (`reg_state[window_size]`) compares the runtime
6953 // saved caller PC against the recorded `dr_return_pc`.
6954 //
6955 // v2.0 Track-R R3d — drop the `!ct.dispatchable`
6956 // gate. After R3d lifts `dispatchable = true` for
6957 // multi-way guards, the trace's body still emits the
6958 // R3b/R3d sentinel shape on return — the saved-PC slot
6959 // and post-invoke classifier must keep firing.
6960 // `downrec_link.is_some()` is the unique structural
6961 // signal that the trace closes via DownRec.
6962 let is_downrec_entry = ct.downrec_link.is_some();
6963 let mut reg_state: Vec<i64> = std::mem::take(&mut self.jit.reg_state_buf);
6964 reg_state.clear();
6965 // v2.0 Track-R R3c — when admitting a downrec trace,
6966 // size the buffer to `window_size + 1` so the lowerer
6967 // can `load(I64, ..., reg_state, window_size * 8)`
6968 // for the saved caller PC guard input. The extra slot
6969 // is the LAST element so cranelift's existing
6970 // `0..window_size` accesses are unaffected.
6971 let reg_state_len = if is_downrec_entry {
6972 window_size_us + 1
6973 } else {
6974 window_size_us
6975 };
6976 reg_state.resize(reg_state_len, 0i64);
6977 let mut dispatch_ok = true;
6978 for i in 0..max_stack {
6979 let v = self.stack[base_us + i];
6980 let (tag, raw) = v.unpack();
6981 entry_tags.push(tag);
6982 // P12-S12-C v3 — entry tag guard. The trace's IR
6983 // is specialised to the compile-time entry tags
6984 // (via current_kinds propagation from
6985 // from_entry_tag). A runtime tag mismatch means
6986 // body ops would mis-interpret raw bits (e.g.
6987 // treat a Str pointer as Int payload → garbage).
6988 // Skip dispatch on mismatch so interp handles
6989 // this entry shape; the trace stays cached for
6990 // future entries that match.
6991 if i < compile_entry_tags.len() && tag != compile_entry_tags[i] {
6992 dispatch_ok = false;
6993 break;
6994 }
6995 match tag {
6996 // Int / Float / Table / Nil all marshal
6997 // to raw payload cleanly; the trace's IR
6998 // treats the 8-byte slot as an i64 (with
6999 // f64 ops bitcasting around the boundary).
7000 crate::runtime::value::raw::INT
7001 | crate::runtime::value::raw::FLOAT
7002 | crate::runtime::value::raw::TABLE
7003 | crate::runtime::value::raw::CLOSURE
7004 // P12-S12-B-v2 — Native iter slots (e.g.
7005 // R[A] = ipairs_iter) are present in
7006 // generic-for traces; the raw bits are a
7007 // valid `*mut NativeClosure` and round-trip
7008 // cleanly.
7009 | crate::runtime::value::raw::NATIVE
7010 // P12-S12-C v1 — Str slots show up in
7011 // string-concat traces; raw bits = `*mut
7012 // LuaStr` (interned, GC-managed). Round-
7013 // trips cleanly as a heap pointer.
7014 | crate::runtime::value::raw::STR
7015 | crate::runtime::value::raw::NIL => {
7016 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
7017 reg_state[i] = unsafe { raw.zero as i64 };
7018 }
7019 _ => {
7020 dispatch_ok = false;
7021 break;
7022 }
7023 }
7024 }
7025
7026 if dispatch_ok {
7027 debug_assert_eq!(head_pc_val, pc, "trace cache hit's head_pc != pc");
7028 self.jit.pending_err = None;
7029 // P12-S4-step4b-C-2 — snapshot the pre-entry frame
7030 // count. A cmp@d>0 side-exit calls the materialize
7031 // helper which pushes inlined frames onto
7032 // `vm.frames`; on deopt those frames must be popped
7033 // before falling through to the interpreter, else
7034 // the stack grows unboundedly per deopted dispatch.
7035 let pre_frames = self.frames.len();
7036 // v2.0 Track-R R3c — saved-PC slot population. The
7037 // recorded `dr_return_pc` on the closing trace is
7038 // the caller's resume PC captured at a depth>0
7039 // Return push (recorder push site, see R3a verdict
7040 // §3). The natural runtime analogue for self-
7041 // stitch is the dispatching frame's PARENT frame's
7042 // PC: the trace's head_pc sits inside a Lua frame,
7043 // and the parent (caller) frame's `pc` is what
7044 // luna would observe as `[base-8]` in the LJ
7045 // `asm_retf` shape (`lj_asm_arm64.h:565`). When
7046 // the parent isn't a Lua frame (top-level dispatch
7047 // — first invocation through `call_value`), no
7048 // saved PC exists; we write 0, which always
7049 // mismatches the recorded `dr_return_pc != 0`
7050 // invariant pinned by R3b
7051 // (`crates/luna-jit/src/jit_backend/trace.rs:7206
7052 // debug_assert!(dr_return_pc != 0, ...)`).
7053 if is_downrec_entry {
7054 let saved_pc: i64 = if pre_frames >= 2 {
7055 match &self.frames[pre_frames - 2] {
7056 CallFrame::Lua(parent) => parent.pc as i64,
7057 CallFrame::Cont(_) => 0,
7058 }
7059 } else {
7060 0
7061 };
7062 reg_state[window_size_us] = saved_pc;
7063 }
7064 // v1.3 Phase AOT Stage 7 sub-piece 4 — `LUNA_AOT_PROBE`
7065 // diagnostic hook. The probe fires once per trace dispatch
7066 // (regardless of JIT vs AOT origin — both go through this
7067 // arm), letting the AOT smoke test verify mcode actually
7068 // executed. Guarded behind `OnceLock` so the env read is
7069 // a one-time cost per process; not gated on a particular
7070 // counter so the smoke test gets a deterministic single-
7071 // line `aot_trace_fired pc=N` per first dispatch.
7072 if jit_probe_enabled() && self.jit.counters.dispatched == 0 {
7073 eprintln!("luna-runtime-helpers: aot_trace_fired pc={head_pc_val}");
7074 }
7075 let continuation_pc = {
7076 // v1.1 A1 Session A — chunk_compiler.enter
7077 // (CraneliftBackend delegates to enter_jit;
7078 // NullJitBackend returns an inert guard).
7079 let vm_ptr: *mut Vm = self;
7080 let _guard = self.jit.chunk_compiler.enter(vm_ptr, Some(cl));
7081 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
7082 unsafe { entry_fn(reg_state.as_mut_ptr()) }
7083 };
7084 self.jit.counters.dispatched += 1;
7085
7086 if self.jit.pending_err.is_some() {
7087 self.jit.pending_err = None;
7088 self.jit.counters.deopt += 1;
7089 // P12-S4-step4b-C-2 — unwind any helper-pushed
7090 // inlined frames before the interpreter resumes.
7091 // Don't restore reg_state — the trace's partial
7092 // writes are discarded; interp re-executes from
7093 // the original `pc`.
7094 while self.frames.len() > pre_frames {
7095 frames_pop_sync(&mut self.frames, &mut self.frames_top);
7096 }
7097 if is_downrec_entry {
7098 // v2.0 Track-R R3c — pending_err observed
7099 // mid-trace inside a downrec admit. Treat
7100 // it as a guard miss: bump `downrec_deopt`
7101 // and suppress the next downrec admit so
7102 // interp can advance past `head_pc` and
7103 // the same trace doesn't immediately re-
7104 // fire on the next loop iteration.
7105 self.jit.counters.downrec_deopt += 1;
7106 self.jit.suppress_downrec_admit_once = true;
7107 }
7108 } else if is_downrec_entry && {
7109 // v2.0 Track-R R3d — only enter the R3c/R3d
7110 // downrec classifier for returns whose shape
7111 // matches the lowerer's `downrec_idx_opt` tail
7112 // emit: either the stitch_blk DOWNREC sentinel
7113 // (HIT) or the deopt_blk GLOBAL-sentinel-with-
7114 // body==head_pc (MISS via guard fail). Any
7115 // other return from a downrec trace (intermediate
7116 // body cmp side-exit, GetField inference fail,
7117 // etc.) carries a different sentinel/body shape
7118 // and means the body exited BEFORE reaching the
7119 // downrec close — classify those through the
7120 // normal decode path (else branch below) so
7121 // reg_state restores + pc advances correctly.
7122 // The pre-R3d behavior (R3c) classified them all
7123 // as MISS and skipped the normal restore, which
7124 // inflated `downrec_deopt` with non-downrec
7125 // events and lost the trace's mid-flight writes.
7126 let raw_ret = continuation_pc as u64;
7127 let from_side_trace = (raw_ret >> 63) & 1 == 1;
7128 let sentinel_code = if from_side_trace {
7129 ((raw_ret >> 56) & 0x7F) as u32
7130 } else {
7131 0
7132 };
7133 let raw_body = raw_ret & 0x00FF_FFFF_FFFF_FFFFu64;
7134 let global_deopt_code = crate::jit::trace_types::encode_side_sentinel(
7135 crate::jit::trace_types::SIDE_SENT_KIND_GLOBAL,
7136 0,
7137 );
7138 from_side_trace
7139 && (crate::jit::trace_types::is_downrec_sentinel(sentinel_code)
7140 || (sentinel_code == global_deopt_code
7141 && raw_body == head_pc_val as u64))
7142 } {
7143 // R3d downrec event classifier.
7144 let raw_ret = continuation_pc as u64;
7145 let sentinel_code = ((raw_ret >> 56) & 0x7F) as u32;
7146 if crate::jit::trace_types::is_downrec_sentinel(sentinel_code) {
7147 // Guard HIT — saved_pc matched one of the
7148 // baked candidates and the trace's
7149 // `stitch_blk` arm returned the DOWNREC
7150 // sentinel. Cycle-safety checkpoint:
7151 // decrement budget; on underflow,
7152 // reclassify as deopt + reset budget.
7153 // R3d's `STITCH_DEPTH_DEFAULT = 32` lets
7154 // ~all natural HITs in a hot loop fire
7155 // before reset pressure.
7156 if self.jit.stitch_depth_remaining > 0 {
7157 self.jit.stitch_depth_remaining -= 1;
7158 self.jit.counters.downrec_dispatched += 1;
7159 } else {
7160 self.jit.counters.downrec_deopt += 1;
7161 self.jit.stitch_depth_remaining =
7162 crate::vm::jit_state::JitState::STITCH_DEPTH_DEFAULT;
7163 }
7164 } else {
7165 // Guard MISS via the lowerer's deopt_blk
7166 // arm (GLOBAL sentinel + body == head_pc).
7167 // The deopt_blk emit performs the
7168 // store-back via `emit_store_back_and_return_pc`,
7169 // so the live stack already reflects the
7170 // body's writes; no extra restore needed
7171 // from the dispatcher side.
7172 self.jit.counters.downrec_deopt += 1;
7173 }
7174 self.jit.suppress_downrec_admit_once = true;
7175 // Pop helper-pushed inlined frames (defensive —
7176 // R3d's emit shape doesn't push frames in the
7177 // tail, but a body side-exit before reaching
7178 // the tail may have via the materialize helper).
7179 while self.frames.len() > pre_frames {
7180 frames_pop_sync(&mut self.frames, &mut self.frames_top);
7181 }
7182 self.jit.reg_state_buf = reg_state;
7183 self.jit.entry_tags_buf = entry_tags;
7184 continue;
7185 } else {
7186 // Restore each slot using the trace's
7187 // exit-tag analysis (see ExitTag docs).
7188 // P12-S4-step4b-C-2 — decode the IR's
7189 // side-exit shape. Upper 32 bits = (site_idx
7190 // + 1) for inline cmp side-exits, 0 for
7191 // legacy clean-tail / non-inline exits.
7192 // P15-A v2-C-A0 — decode lives in
7193 // `crate::jit::trace::decode_exit_shape` so
7194 // v2-C-A3 can reuse it with the SIDE TRACE's
7195 // shape inputs when the sentinel bit
7196 // (v2-C-A2) is set on `raw_ret`.
7197 let raw_ret = continuation_pc as u64;
7198 // P15-A v2-C-A3 — side-trace return decode.
7199 // Bit 63 of `raw_ret` is the side-trace
7200 // marker the parent's IR OR'd in when it
7201 // tail-called into a wired child trace.
7202 // Bits 56..=62 carry the sentinel code (the
7203 // cache key into the parent's
7204 // `side_trace_cache`); bits 0..=55 are the
7205 // child's own return value (encoded site or
7206 // plain cont_pc) which we MUST decode using
7207 // the CHILD's per_exit_inline / per_exit_tags
7208 // / exit_tags / exit_hit_counts — not the
7209 // parent's. The dispatcher snapshot read
7210 // above holds the parent's shapes; when bit
7211 // 63 is set we re-fetch the child's via the
7212 // sentinel-keyed cache.
7213 let from_side_trace = (raw_ret >> 63) & 1 == 1;
7214 let (
7215 decode_inline,
7216 decode_tags,
7217 decode_exit_tags,
7218 decode_hit_counts,
7219 decode_body,
7220 ) = if from_side_trace {
7221 let sentinel_code = ((raw_ret >> 56) & 0x7F) as u32;
7222 let body = raw_ret & 0x00FF_FFFF_FFFF_FFFFu64;
7223 let traces = cl.proto.traces.borrow();
7224 let child_idx = traces
7225 .iter()
7226 .find(|t| t.head_pc == head_pc_val)
7227 .and_then(|pct| {
7228 pct.side_trace_cache.borrow().get(&sentinel_code).copied()
7229 });
7230 if let Some(idx) = child_idx
7231 && let Some(child) = traces.get(idx as usize)
7232 {
7233 if crate::jit::trace::v2c_probe_enabled() {
7234 eprintln!(
7235 "[v2c-A3-decode] sentinel={:#04x} body={:#018x} child_idx={} child.n_ops={} child.head_pc={} child.window_size={} parent.pc={} parent.window_size={} child.dispatchable={} child.inline_abort={}",
7236 sentinel_code,
7237 body,
7238 idx,
7239 child.n_ops,
7240 child.head_pc,
7241 child.window_size,
7242 pc,
7243 window_size,
7244 child.dispatchable,
7245 child.is_inline_abort_close,
7246 );
7247 }
7248 (
7249 child.per_exit_inline.clone(),
7250 child.per_exit_tags.clone(),
7251 child.exit_tags.clone(),
7252 child.exit_hit_counts.clone(),
7253 body,
7254 )
7255 } else {
7256 if crate::jit::trace::v2c_probe_enabled() {
7257 eprintln!(
7258 "[v2c-A3-decode] sentinel={:#04x} body={:#018x} child MISS (fallback parent shapes)",
7259 sentinel_code, body,
7260 );
7261 }
7262 // Cache miss — fall back to parent
7263 // shapes with the body bits. Best-
7264 // effort; the trace_side_trace_
7265 // shape_mismatch_count records this
7266 // path indirectly (close-handler
7267 // skips wiring on mismatch so we
7268 // shouldn't reach here when shape
7269 // gate held).
7270 (
7271 per_exit_inline.clone(),
7272 per_exit_tags.clone(),
7273 exit_tags.clone(),
7274 exit_hit_counts.clone(),
7275 body,
7276 )
7277 }
7278 } else {
7279 // P15-A v2-D — dispatcher-level side-trace
7280 // invocation. Replaces v2-C's universal IR
7281 // gate (`load + icmp + brif` at every
7282 // emit_store_back callsite, which A6/A7
7283 // measured as a net perf regression).
7284 // A8 fast-path: skip the tentative decode +
7285 // child lookup entirely when `has_any_side
7286 // _wired == false` (the common case until
7287 // the first side trace compiles for this
7288 // parent). For fib_10_x10k and other tight
7289 // short-trace workloads where most parent
7290 // traces never get a wired child, this
7291 // collapses the v2-D overhead to a single
7292 // `Cell::get()` on the cold path.
7293 // A8-revert: A8 had `parent_has_side` short-
7294 // circuit + snapshot hoist; mini N=3 showed
7295 // A8 lost the btrees_d8 1.02× win (dropped
7296 // to 0.95×) WITHOUT helping fib_10 (same
7297 // 0.86×). Drop A8 — accept the always-run
7298 // v2-D path; the tentative decode + cell
7299 // load is cheaper than the cost A8 added.
7300 {
7301 let tentative = crate::jit::trace::decode_exit_shape(
7302 raw_ret,
7303 per_exit_inline,
7304 per_exit_tags,
7305 exit_tags,
7306 );
7307 let tentative_exit_idx = tentative.exit_hit_idx;
7308 let child_invoke = {
7309 let traces = cl.proto.traces.borrow();
7310 traces.iter().find(|t| t.head_pc == head_pc_val).and_then(
7311 |pct| {
7312 let cell =
7313 pct.exit_side_trace_ptrs.get(tentative_exit_idx)?;
7314 let fn_ptr = cell.get();
7315 if fn_ptr.is_null() {
7316 return None;
7317 }
7318 traces
7319 .iter()
7320 .find(|t| {
7321 t.entry as *const () as *const u8 == fn_ptr
7322 })
7323 .map(|child| {
7324 (
7325 child.entry,
7326 child.per_exit_inline.clone(),
7327 child.per_exit_tags.clone(),
7328 child.exit_tags.clone(),
7329 child.exit_hit_counts.clone(),
7330 )
7331 })
7332 },
7333 )
7334 };
7335 if let Some((cent, cpi, cpt, cet, chc)) = child_invoke {
7336 let child_raw_ret = {
7337 // v1.1 A1 Session A — chunk_compiler.enter
7338 // (side-trace entry).
7339 let vm_ptr: *mut Vm = self;
7340 let _guard =
7341 self.jit.chunk_compiler.enter(vm_ptr, Some(cl));
7342 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
7343 unsafe { cent(reg_state.as_mut_ptr()) }
7344 };
7345 (cpi, cpt, cet, chc, child_raw_ret as u64)
7346 } else {
7347 (
7348 per_exit_inline.clone(),
7349 per_exit_tags.clone(),
7350 exit_tags.clone(),
7351 exit_hit_counts.clone(),
7352 raw_ret,
7353 )
7354 }
7355 }
7356 };
7357 let decoded = crate::jit::trace::decode_exit_shape(
7358 decode_body,
7359 &decode_inline,
7360 &decode_tags,
7361 &decode_exit_tags,
7362 );
7363 let site_id = decoded.site_id;
7364 let cont_pc = decoded.cont_pc;
7365 let exit_hit_idx = decoded.exit_hit_idx;
7366 let exit_tags_for_pc = decoded.exit_tags_for_pc;
7367 // P15-A v2-C-A3 — for side-trace returns
7368 // force using_global_exit_tags=false so the
7369 // restore loop always takes the per-tag slow
7370 // path (the child's global_tag_res_kind
7371 // classification isn't plumbed through yet
7372 // — TODO for a future polish step).
7373 let using_global_exit_tags = if from_side_trace {
7374 false
7375 } else {
7376 decoded.using_global_exit_tags
7377 };
7378 // P15-prep — increment the counter (saturate
7379 // at u32::MAX to avoid wrap on long runs).
7380 // P15-A v1 — track whether this increment is
7381 // the one that crossed `HOTEXIT_THRESHOLD`
7382 // (transition: previous v < threshold, new v
7383 // == threshold). The side-trace start is
7384 // deferred to just before `continue;` so
7385 // vm.stack and frame.pc are fully restored
7386 // (the snapshot reads post-restore values).
7387 let mut side_trace_should_start = false;
7388 // P15-A v2-C-A3 — for side-trace returns the
7389 // counter to bump is the CHILD's (decoded
7390 // shape lookup) — `exit_hit_idx` is into the
7391 // decoded layout, so use the matching
7392 // `decode_hit_counts`. For parent decode
7393 // they're aliased (clone of the parent's
7394 // own Rc).
7395 if let Some(c) = decode_hit_counts.get(exit_hit_idx) {
7396 let v = c.get();
7397 if v < u32::MAX {
7398 c.set(v + 1);
7399 }
7400 if v + 1 == crate::jit::trace::HOTEXIT_THRESHOLD
7401 && self.jit.active_trace.is_none()
7402 && self.jit.trace_enabled
7403 {
7404 side_trace_should_start = true;
7405 }
7406 }
7407 // P12-S4-step4b-C-2 — at an inline cmp@d>0
7408 // side-exit, the helper has pushed N frames on
7409 // top of the trace head's frame and
7410 // `exit_tags_for_pc.len()` covers the full
7411 // window (caller + each inlined frame's
7412 // window). Slots beyond `max_stack` belong to
7413 // an inlined frame: their `Untouched` entries
7414 // default to Nil (no entry-tag fallback —
7415 // marshal-in only captured caller slots) and
7416 // we write to interp stack at `base + i` which
7417 // mirrors `op_offsets`-derived layout.
7418 let slot_count = exit_tags_for_pc.len();
7419 // P12-S4-step4b-C-2 — the helper only extends
7420 // vm.stack up to the deepest pushed frame's
7421 // window, but the exit_tags snapshot covers
7422 // the trace's full `window_size` (which
7423 // includes depth-N+1 scratch slots that the
7424 // trace's IR may have written without a
7425 // matching pushed frame). Extend with Nil so
7426 // the write at the tail doesn't panic; these
7427 // slots get overwritten by the writeback loop
7428 // and won't leak meaningful data past the
7429 // pushed frames' R[0..max_stack) windows.
7430 if self.stack.len() < base_us + slot_count {
7431 self.stack
7432 .resize(base_us + slot_count, crate::runtime::Value::Nil);
7433 }
7434 // P13-S13-E — fast-path restore loop. When
7435 // we landed on the global `exit_tags`,
7436 // dispatch on the compile-time
7437 // classification: skip the loop entirely
7438 // for `AllUntouched`, do a tag-free
7439 // `Value::Int(...)` write per slot for
7440 // `AllInt`, otherwise fall through to the
7441 // general match-arm loop. site_id > 0
7442 // (inline frame mat) and per_exit_tags
7443 // hits always take the general path —
7444 // their per-side-exit shapes aren't
7445 // pre-classified yet.
7446 let fast_path_taken = if using_global_exit_tags {
7447 match global_tag_res_kind {
7448 crate::jit::trace::TagResKind::AllUntouched => {
7449 // No-op: vm.stack already
7450 // matches the trace's post-
7451 // entry state for these
7452 // slots (entry values not
7453 // overridden, or already
7454 // spilled by helpers).
7455 true
7456 }
7457 crate::jit::trace::TagResKind::AllInt => {
7458 for i in 0..slot_count {
7459 self.stack[base_us + i] =
7460 crate::runtime::Value::Int(reg_state[i]);
7461 }
7462 true
7463 }
7464 crate::jit::trace::TagResKind::Mixed => false,
7465 }
7466 } else {
7467 false
7468 };
7469 if !fast_path_taken {
7470 for i in 0..slot_count {
7471 let tag = match exit_tags_for_pc[i] {
7472 crate::jit::trace::ExitTag::Untouched => {
7473 if i < max_stack {
7474 entry_tags[i]
7475 } else {
7476 crate::runtime::value::raw::NIL
7477 }
7478 }
7479 crate::jit::trace::ExitTag::Int => {
7480 crate::runtime::value::raw::INT
7481 }
7482 crate::jit::trace::ExitTag::Float => {
7483 crate::runtime::value::raw::FLOAT
7484 }
7485 crate::jit::trace::ExitTag::Table => {
7486 crate::runtime::value::raw::TABLE
7487 }
7488 crate::jit::trace::ExitTag::Closure => {
7489 crate::runtime::value::raw::CLOSURE
7490 }
7491 // P12-S6-A1 — trace actively wrote Nil
7492 // to this slot (e.g. via Op::LoadNil).
7493 // Restore as Nil regardless of the entry
7494 // tag, since the i64 payload is 0 and
7495 // packing as the entry tag (e.g. INT)
7496 // would mis-type the slot.
7497 crate::jit::trace::ExitTag::Nil => {
7498 crate::runtime::value::raw::NIL
7499 }
7500 // P12-S12-C v2 — trace wrote a Str ptr
7501 // to this slot (LoadK Str / Move from
7502 // Str / Concat result). Restore as
7503 // Value::Str with raw bits round-
7504 // tripped.
7505 crate::jit::trace::ExitTag::Str => {
7506 crate::runtime::value::raw::STR
7507 }
7508 };
7509 // SAFETY: tag is from a verified slot
7510 // (entry validated above) or pinned by
7511 // the exit-tag analysis to INT/TABLE.
7512 // The raw payload sits in reg_state[i].
7513 // Stack was extended by the materialize
7514 // helper for inline frames.
7515 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
7516 self.stack[base_us + i] = unsafe {
7517 Value::pack(
7518 tag,
7519 crate::runtime::value::RawVal {
7520 zero: reg_state[i] as u64,
7521 },
7522 )
7523 };
7524 }
7525 }
7526 // P12-S4-step4b-C-2 — for non-inline exits the
7527 // helper was never called (no metas chain for
7528 // this cont_pc), so `frames.last()` is the
7529 // trace head's frame and we set its pc to
7530 // cont_pc as before. For inline exits the
7531 // helper baked the side-exit PC into the
7532 // innermost frame's `pc` at push time
7533 // (chain.last().pc was overridden at emit),
7534 // so this assignment to `frames.last_mut().pc
7535 // = cont_pc` is a redundant-but-correct
7536 // confirmation.
7537 let _ = &per_exit_inline; // hold the Rc alive across dispatch
7538 // P12-S4-step4b-C-2 — for inline side-exits the
7539 // helper has pushed N frames on top. The trace
7540 // head frame is at `pre_frames - 1`; set its
7541 // pc to `head_resume_pc` so when the chain
7542 // eventually pops back to it, interp resumes
7543 // PAST the trace's depth-0 Op::Call instead of
7544 // restarting from `head_pc` and re-triggering
7545 // dispatch (infinite loop). The innermost
7546 // (helper-pushed) frame already has its pc
7547 // baked in at compile time, but we still
7548 // assign `cont_pc` below for parity with the
7549 // non-inline path (no-op).
7550 if site_id > 0 {
7551 let idx = (site_id - 1) as usize;
7552 let head_resume_pc = decode_inline[idx].head_resume_pc;
7553 if pre_frames > 0
7554 && let CallFrame::Lua(f) = &mut self.frames[pre_frames - 1]
7555 {
7556 f.pc = head_resume_pc;
7557 }
7558 }
7559 let frames_len_now = self.frames.len();
7560 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
7561 match unsafe { self.frames.last_mut().unwrap_unchecked() } {
7562 CallFrame::Lua(fmut) => {
7563 if crate::jit::trace::v2c_probe_enabled() {
7564 eprintln!(
7565 "[v2c-set-pc] from_side={} sentinel_or_raw={:#018x} prev_pc={} new_cont_pc={} site_id={} frames.len={} pre_frames={} max_stack={}",
7566 from_side_trace,
7567 raw_ret,
7568 fmut.pc,
7569 cont_pc,
7570 site_id,
7571 frames_len_now,
7572 pre_frames,
7573 max_stack,
7574 );
7575 }
7576 fmut.pc = cont_pc;
7577 }
7578 _ => unreachable!("Cont frame at trace dispatch"),
7579 }
7580 // P15-A v1 — deferred side-trace start. The
7581 // increment block above flagged this exit's
7582 // hit count crossing HOTEXIT_THRESHOLD; now
7583 // that vm.stack is restored and frame.pc is
7584 // settled, snapshot entry_tags from the
7585 // resume frame's window and create the
7586 // recorder. The recorder's first push fires
7587 // on the next interp iteration at cont_pc.
7588 //
7589 // `head_proto` for the side trace = cl.proto
7590 // (trace JIT only inlines self-recursive
7591 // calls today, so cont_pc always lands in
7592 // the same proto as the parent). Frame base
7593 // is the resume frame (top of `self.frames`
7594 // — inline-pushed frames moved this).
7595 if side_trace_should_start {
7596 let (resume_base, resume_proto) = match self.frames.last() {
7597 Some(CallFrame::Lua(f)) => (f.base as usize, f.closure.proto),
7598 _ => (base_us, cl.proto),
7599 };
7600 let resume_max_stack = resume_proto.max_stack as usize;
7601 let mut side_entry_tags: Vec<u8> = Vec::with_capacity(resume_max_stack);
7602 // Extend stack if cont_pc's frame window
7603 // overhangs the current stack len (rare,
7604 // but inline-pushed frame stack writes
7605 // only covered the trace's writeback).
7606 if self.stack.len() < resume_base + resume_max_stack {
7607 self.stack.resize(
7608 resume_base + resume_max_stack,
7609 crate::runtime::Value::Nil,
7610 );
7611 }
7612 for i in 0..resume_max_stack {
7613 let (tag, _) = self.stack[resume_base + i].unpack();
7614 side_entry_tags.push(tag);
7615 }
7616 self.jit.active_trace =
7617 Some(Box::new(crate::jit::trace::TraceRecord::start_side_trace(
7618 resume_proto,
7619 cont_pc,
7620 side_entry_tags,
7621 cl.proto,
7622 head_pc_val,
7623 exit_hit_idx,
7624 )));
7625 self.jit.recording_frame_base = self.frames.len() - 1;
7626 self.jit.counters.side_trace_started += 1;
7627 }
7628 // P13-S13-D — put the dispatch buffers back
7629 // before the `continue;` so the next
7630 // dispatch picks up the same allocation.
7631 self.jit.reg_state_buf = reg_state;
7632 self.jit.entry_tags_buf = entry_tags;
7633 continue;
7634 }
7635 }
7636 // P13-S13-D — !dispatch_ok / deopt path / non-cont
7637 // exit also restore the buffers before falling
7638 // through to the interp.
7639 self.jit.reg_state_buf = reg_state;
7640 self.jit.entry_tags_buf = entry_tags;
7641 }
7642
7643 // PUC `vmfetch` increments savedpc BEFORE firing traceexec, so
7644 // hook code that consults `currentpc = savedpc - 1` lands on the
7645 // instruction now executing. luna mirrors that by advancing
7646 // `f.pc` to `pc + 1` before the hook block — local_at /
7647 // getinfo / line attribution all read f.pc, and the existing
7648 // `pc - 1` convention in those helpers then yields the current
7649 // instruction's pc (db.lua :696: local `A` visible at the
7650 // chunk's return line once OP_CLOSURE has advanced pc).
7651 //
7652 // Inline `top_frame_mut` for the hot path: top is guaranteed Lua
7653 // (cont frames drained above) so the and_then/Option layers are
7654 // dead weight.
7655 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
7656 match unsafe { self.frames.last_mut().unwrap_unchecked() } {
7657 CallFrame::Lua(fmut) => fmut.pc = pc + 1,
7658 _ => unreachable!("Cont frame at pc bump"),
7659 }
7660
7661 // count + line hooks (PUC traceexec): before executing the
7662 // instruction. Skipped while the hook itself runs.
7663 // (Parens here are load-bearing — without them `&&` binds tighter
7664 // than `||` and the `!in_hook` guard only gates the rust-hook arm,
7665 // letting a Lua line hook recurse into itself → stack overflow
7666 // on db.lua line-hook assertions. Matches the `hook_call_with` /
7667 // `hook_return` predicate shape at lines 2245 / 2279 / 2294 / 4023.)
7668 if !self.in_hook && (self.hook.func.is_some() || self.hook.rust_func.is_some()) {
7669 let lines = &cl.proto.lines;
7670 let cur_line = if lines.is_empty() {
7671 None
7672 } else {
7673 Some(lines[(pc as usize).min(lines.len() - 1)] as i64)
7674 };
7675 // count hook: fire every `count_base` instructions
7676 if self.hook.count {
7677 self.hook.count_left -= 1;
7678 if self.hook.count_left <= 0 {
7679 self.hook.count_left = self.hook.count_base;
7680 // hooked function is the running Lua frame: its frame
7681 // is on the stack, so no synthetic C level is needed.
7682 self.run_hook(b"count", cur_line, false)?;
7683 }
7684 }
7685 // line hook: fire on a fresh frame, a backward jump (loop), or a
7686 // change of source line.
7687 if self.hook.line {
7688 if lines.is_empty() {
7689 // PUC: a stripped chunk has no line info, so
7690 // `getfuncline` returns -1. The line hook still fires
7691 // on the first instruction of the new frame (where
7692 // `npci <= oldpc` holds at oldpc=0), with the line
7693 // pushed as `nil` instead of an integer (db.lua :1030
7694 // "hook called without debug info for 1st instruction").
7695 if oldpc == u32::MAX {
7696 self.run_hook(b"line", None, false)?;
7697 self.top_frame_mut().hook_oldpc = pc;
7698 }
7699 } else {
7700 let newline = lines[(pc as usize).min(lines.len() - 1)];
7701 // PUC `traceexec`: fire on frame entry (`oldpc == MAX`),
7702 // on a backward jump (`pc < oldpc` — strict; an equal pc
7703 // would re-fire the install-site after `oldpc = pc`),
7704 // or when the source line changes.
7705 let fire = oldpc == u32::MAX
7706 || pc < oldpc
7707 || newline != lines[(oldpc as usize).min(lines.len() - 1)];
7708 if fire {
7709 self.run_hook(b"line", Some(newline as i64), false)?;
7710 }
7711 self.top_frame_mut().hook_oldpc = pc;
7712 }
7713 }
7714 }
7715
7716 match inst.op() {
7717 Op::Move => {
7718 let v = self.r(base, inst.b());
7719 self.set_r(base, inst.a(), v);
7720 }
7721 Op::LoadI => self.set_r(base, inst.a(), Value::Int(inst.sbx() as i64)),
7722 Op::LoadF => self.set_r(base, inst.a(), Value::Float(inst.sbx() as f64)),
7723 Op::LoadK => {
7724 let v = cl.proto.consts[inst.bx() as usize];
7725 self.set_r(base, inst.a(), v);
7726 }
7727 Op::LoadKx => {
7728 let extra = cl.proto.code[self.pc_of_top() as usize];
7729 self.bump_pc();
7730 let v = cl.proto.consts[extra.ax() as usize];
7731 self.set_r(base, inst.a(), v);
7732 }
7733 Op::LoadFalse => self.set_r(base, inst.a(), Value::Bool(false)),
7734 Op::LFalseSkip => {
7735 self.set_r(base, inst.a(), Value::Bool(false));
7736 self.bump_pc();
7737 }
7738 Op::LoadTrue => self.set_r(base, inst.a(), Value::Bool(true)),
7739 Op::LoadNil => {
7740 let a = inst.a();
7741 for i in 0..=inst.b() {
7742 self.set_r(base, a + i, Value::Nil);
7743 }
7744 }
7745 Op::GetUpval => {
7746 let v = self.upval_get(cl, inst.b());
7747 self.set_r(base, inst.a(), v);
7748 }
7749 Op::SetUpval => {
7750 let v = self.r(base, inst.a());
7751 self.upval_set(cl, inst.b(), v);
7752 }
7753 Op::GetTabUp => {
7754 let t = self.upval_get(cl, inst.b());
7755 let key = cl.proto.consts[inst.c() as usize];
7756 self.op_index(t, key, base + inst.a())?;
7757 }
7758 Op::GetTable => {
7759 let t = self.r(base, inst.b());
7760 let key = self.r(base, inst.c());
7761 self.op_index(t, key, base + inst.a())?;
7762 }
7763 Op::GetI => {
7764 let t = self.r(base, inst.b());
7765 self.op_index(t, Value::Int(inst.c() as i64), base + inst.a())?;
7766 }
7767 Op::GetField => {
7768 let t = self.r(base, inst.b());
7769 let key = cl.proto.consts[inst.c() as usize];
7770 // v1.2 D4 A1 — fast path: known-Str const key + no
7771 // metatable on the table → skip `op_index` /
7772 // `index_step`'s MAX_TAG_LOOP setup and the outer
7773 // `Value` match. Falls through to the slow path
7774 // unchanged when either invariant breaks (so
7775 // `__index` metamethods, non-Table receivers, and
7776 // non-Str keys behave exactly as before).
7777 if let Value::Table(tb) = t
7778 && tb.metatable().is_none()
7779 && let Value::Str(s) = key
7780 {
7781 let v = tb.get_str(s);
7782 self.stack[(base + inst.a()) as usize] = v;
7783 } else {
7784 self.op_index(t, key, base + inst.a())?;
7785 }
7786 }
7787 Op::SetTabUp => {
7788 let t = self.upval_get(cl, inst.a());
7789 let key = cl.proto.consts[inst.b() as usize];
7790 let v = self.r(base, inst.c());
7791 self.op_newindex(t, key, v)?;
7792 }
7793 Op::SetTable => {
7794 let t = self.r(base, inst.a());
7795 let key = self.r(base, inst.b());
7796 let v = self.r(base, inst.c());
7797 self.op_newindex(t, key, v)?;
7798 }
7799 Op::SetI => {
7800 let t = self.r(base, inst.a());
7801 let v = self.r(base, inst.c());
7802 self.op_newindex(t, Value::Int(inst.b() as i64), v)?;
7803 }
7804 Op::SetField => {
7805 let t = self.r(base, inst.a());
7806 let key = cl.proto.consts[inst.b() as usize];
7807 let v = self.r(base, inst.c());
7808 self.op_newindex(t, key, v)?;
7809 }
7810 Op::NewTable => {
7811 let t = self.heap.new_table();
7812 self.set_r(base, inst.a(), Value::Table(t));
7813 self.maybe_collect_garbage(base + inst.a() + 1);
7814 }
7815 Op::SetList => {
7816 let a = inst.a();
7817 let abs_a = base + a;
7818 let n = if inst.b() == 0 {
7819 self.top - (abs_a + 1)
7820 } else {
7821 inst.b()
7822 };
7823 let offset = if inst.k() {
7824 let extra = cl.proto.code[self.pc_of_top() as usize];
7825 self.bump_pc();
7826 extra.ax() as i64
7827 } else {
7828 inst.c() as i64
7829 };
7830 let Value::Table(t) = self.r(base, a) else {
7831 unreachable!("SETLIST on non-table");
7832 };
7833 for i in 1..=n {
7834 let v = self.r(base, a + i);
7835 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
7836 if let Err(TableError::Overflow) =
7837 unsafe { t.as_mut() }.set_int(&mut self.heap, offset + i as i64, v)
7838 {
7839 return Err(self.rt_err("table overflow"));
7840 }
7841 }
7842 // one barrier_back covers every store this op did — PUC's
7843 // `luaC_barrierback_` once-per-table optimisation
7844 self.heap
7845 .barrier_back(t.as_ptr() as *mut crate::runtime::heap::GcHeader);
7846 // the element temps above the table are now consumed
7847 self.maybe_collect_garbage(base + a + 1);
7848 }
7849 Op::SelfOp => {
7850 let o = self.r(base, inst.b());
7851 self.set_r(base, inst.a() + 1, o);
7852 // PUC OP_SELF's C is a constant index when the k-flag is
7853 // set; otherwise it points to a register that holds the
7854 // (constant-loaded) key. luna's compiler falls back to the
7855 // register form when the constant index exceeds OP_SELF's
7856 // 8-bit C field (5.1 big.lua's `a:findfield(...)` against
7857 // a table with 250+ string keys, where "findfield" lands
7858 // past const #255). The exec must honour the same split.
7859 let key = if inst.k() {
7860 cl.proto.consts[inst.c() as usize]
7861 } else {
7862 self.r(base, inst.c())
7863 };
7864 self.op_index(o, key, base + inst.a())?;
7865 }
7866 Op::Add => self.arith_rr(inst, base, ArithOp::Add)?,
7867 Op::Sub => self.arith_rr(inst, base, ArithOp::Sub)?,
7868 Op::Mul => self.arith_rr(inst, base, ArithOp::Mul)?,
7869 Op::Mod => self.arith_rr(inst, base, ArithOp::Mod)?,
7870 Op::Pow => self.arith_rr(inst, base, ArithOp::Pow)?,
7871 Op::Div => self.arith_rr(inst, base, ArithOp::Div)?,
7872 Op::IDiv => self.arith_rr(inst, base, ArithOp::IDiv)?,
7873 Op::BAnd => self.arith_rr(inst, base, ArithOp::BAnd)?,
7874 Op::BOr => self.arith_rr(inst, base, ArithOp::BOr)?,
7875 Op::BXor => self.arith_rr(inst, base, ArithOp::BXor)?,
7876 Op::Shl => self.arith_rr(inst, base, ArithOp::Shl)?,
7877 Op::Shr => self.arith_rr(inst, base, ArithOp::Shr)?,
7878 Op::Unm => {
7879 let v = self.r(base, inst.b());
7880 match coerce_num(v) {
7881 Some(Num::Int(i)) => {
7882 self.set_r(base, inst.a(), Value::Int(i.wrapping_neg()))
7883 }
7884 Some(Num::Float(f)) => self.set_r(base, inst.a(), Value::Float(-f)),
7885 None => {
7886 let mm = self.get_mm(v, Mm::Unm);
7887 if mm.is_nil() {
7888 return Err(self.type_err("perform arithmetic on", v));
7889 }
7890 let dst = base + inst.a();
7891 self.begin_meta_call(mm, &[v, v], MetaAction::Store { dst }, "unm")?;
7892 }
7893 }
7894 }
7895 Op::BNot => {
7896 let v = self.r(base, inst.b());
7897 match coerce_num(v) {
7898 Some(n) => {
7899 let i = self.int_from_num(n)?;
7900 self.set_r(base, inst.a(), Value::Int(!i));
7901 }
7902 None => {
7903 let mm = self.get_mm(v, Mm::BNot);
7904 if mm.is_nil() {
7905 return Err(self.type_err("perform bitwise operation on", v));
7906 }
7907 let dst = base + inst.a();
7908 self.begin_meta_call(mm, &[v, v], MetaAction::Store { dst }, "bnot")?;
7909 }
7910 }
7911 }
7912 Op::Not => {
7913 let v = self.r(base, inst.b());
7914 self.set_r(base, inst.a(), Value::Bool(!v.truthy()));
7915 }
7916 Op::Len => {
7917 let v = self.r(base, inst.b());
7918 match self.len_step(v)? {
7919 MmOut::Done(r) => self.set_r(base, inst.a(), r),
7920 MmOut::Mm { func, recv } => {
7921 let dst = base + inst.a();
7922 self.begin_meta_call(
7923 func,
7924 &[recv, recv],
7925 MetaAction::Store { dst },
7926 "len",
7927 )?;
7928 }
7929 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from len_step"),
7930 }
7931 }
7932 Op::Concat => {
7933 // right-associative fold over operands at base+a .. base+a+n,
7934 // in place on the stack so a yielding __concat can suspend.
7935 let a = inst.a();
7936 let n = inst.b();
7937 self.top = base + a + n;
7938 self.concat_run(base + a)?;
7939 }
7940 Op::Close => {
7941 // Yieldable: drive __close handlers through the
7942 // interpreter loop so a coroutine.yield() inside a
7943 // handler suspends cleanly (locals.lua block-end yield).
7944 // `drive_close` parks the handler call at `self.top`, so
7945 // raise `top` past this frame's full register window
7946 // first — a goto out of a nested for-loop can fire
7947 // OP_Close while `self.top` still sits at the inner
7948 // body's working top, which would let `push_frame`'s
7949 // wipe clobber the outer tbc slot before it could be
7950 // closed (locals.lua:1219 nested-for goto regression).
7951 self.top = self.top.max(base + cl.proto.max_stack as u32);
7952 let _ =
7953 self.begin_close(base + inst.a(), None, AfterClose::Block, entry_depth)?;
7954 }
7955 Op::Tbc => {
7956 self.register_tbc(base + inst.a())?;
7957 }
7958 Op::Jmp => {
7959 let off = inst.sj();
7960 // P12-S1.B — trace JIT back-edge counter. A negative
7961 // jump offset is a loop back-edge (the only canonical
7962 // backward jumps the compiler emits — `while`, `for`,
7963 // `repeat`). Tick the per-Proto counter and, once it
7964 // exceeds the threshold, log a stub promotion that
7965 // S1.C will turn into actual trace recording. The
7966 // whole block is gated on `trace_jit_enabled` so
7967 // existing benches see one branch-not-taken and no
7968 // counter writes.
7969 if self.jit.trace_enabled && off < 0 {
7970 let proto = cl.proto;
7971 let c = proto.trace_hot_count.get();
7972 if c < u32::MAX / 2 {
7973 proto.trace_hot_count.set(c + 1);
7974 }
7975 // P13-S13-H — relaxed back-edge trigger:
7976 // `c >= THRESHOLD` (was `c == THRESHOLD`) so
7977 // a missed crossing (active_trace busy with
7978 // a call-trigger, or the recorder slot
7979 // happened to be in use) doesn't permanently
7980 // lock this back-edge target out. The
7981 // `already_cached` short-circuit prevents
7982 // duplicate recordings: once a trace is
7983 // cached for this target, subsequent
7984 // crossings skip the start. This pairs with
7985 // S13-H's discard-on-partial-coverage close
7986 // handling — when a short call-trigger is
7987 // discarded, the back-edge can still find an
7988 // open slot at the next iteration.
7989 let target_pc = (pc as i32 + 1 + off as i32).max(0) as u32;
7990 // P13-S13-K — gave-up short-circuit. Skip
7991 // the RefCell borrow + scan when the
7992 // S13-I cap force-compiled a partial
7993 // trace on this Proto.
7994 let back_edge_already_cached = if proto.trace_gave_up.get() {
7995 true
7996 } else {
7997 proto.traces.borrow().iter().any(|t| t.head_pc == target_pc)
7998 };
7999 if c >= crate::jit::trace::TRACE_HOT_THRESHOLD
8000 && self.jit.active_trace.is_none()
8001 && !back_edge_already_cached
8002 {
8003 // Back-edge target = pc after `add_pc(off)`,
8004 // i.e. current `pc + 1 + off` (the dispatch
8005 // loop has already advanced f.pc to pc+1).
8006 let target = (pc as i32 + 1 + off as i32).max(0) as u32;
8007 // Snapshot per-slot Value tag at trace
8008 // entry so the lowerer's kind tracker
8009 // knows which arith path to lower
8010 // (iadd vs fadd, etc.).
8011 let max_stack = cl.proto.max_stack as usize;
8012 let base_us = base as usize;
8013 let mut entry_tags = Vec::with_capacity(max_stack);
8014 for i in 0..max_stack {
8015 let (tag, _) = self.stack[base_us + i].unpack();
8016 entry_tags.push(tag);
8017 }
8018 self.jit.active_trace =
8019 Some(Box::new(crate::jit::trace::TraceRecord::start(
8020 cl.proto, target, entry_tags, false,
8021 )));
8022 // P12-S4 — record the frame the trace
8023 // started in. `self.frames.len() - 1`
8024 // since we're inside the currently-running
8025 // Lua frame's dispatch.
8026 self.jit.recording_frame_base = self.frames.len() - 1;
8027 }
8028 }
8029 self.add_pc(off);
8030 }
8031 Op::Eq => {
8032 let l = self.r(base, inst.a());
8033 let r = self.r(base, inst.b());
8034 if let (Value::Int(a), Value::Int(b)) = (l, r) {
8035 if (a == b) != inst.k() {
8036 self.bump_pc();
8037 }
8038 } else {
8039 let step = self.eq_step(l, r);
8040 self.op_compare(step, l, r, inst.k(), "eq")?;
8041 }
8042 }
8043 Op::EqK => {
8044 let l = self.r(base, inst.a());
8045 let r = cl.proto.consts[inst.b() as usize];
8046 if let (Value::Int(a), Value::Int(b)) = (l, r) {
8047 if (a == b) != inst.k() {
8048 self.bump_pc();
8049 }
8050 } else {
8051 let step = self.eq_step(l, r);
8052 self.op_compare(step, l, r, inst.k(), "eq")?;
8053 }
8054 }
8055 Op::Lt => {
8056 let l = self.r(base, inst.a());
8057 let r = self.r(base, inst.b());
8058 // hot path: Int < Int — drops the MmOut + op_compare match
8059 if let (Value::Int(a), Value::Int(b)) = (l, r) {
8060 if (a < b) != inst.k() {
8061 self.bump_pc();
8062 }
8063 } else {
8064 let step = self.less_step(l, r, false)?;
8065 self.op_compare(step, l, r, inst.k(), "lt")?;
8066 }
8067 }
8068 Op::Le => {
8069 let l = self.r(base, inst.a());
8070 let r = self.r(base, inst.b());
8071 if let (Value::Int(a), Value::Int(b)) = (l, r) {
8072 if (a <= b) != inst.k() {
8073 self.bump_pc();
8074 }
8075 } else {
8076 let step = self.less_step(l, r, true)?;
8077 self.op_compare(step, l, r, inst.k(), "le")?;
8078 }
8079 }
8080 Op::Test => {
8081 let cond = self.r(base, inst.a()).truthy();
8082 self.cond_skip(cond, inst.k());
8083 }
8084 Op::TestSet => {
8085 let v = self.r(base, inst.b());
8086 if v.truthy() == inst.k() {
8087 self.set_r(base, inst.a(), v);
8088 } else {
8089 self.bump_pc();
8090 }
8091 }
8092 Op::Call => {
8093 let abs = base + inst.a();
8094 let nargs = if inst.b() == 0 {
8095 None
8096 } else {
8097 Some(inst.b() - 1)
8098 };
8099 let wanted = inst.c() as i32 - 1;
8100 self.begin_call(abs, nargs, wanted, false)?;
8101 }
8102 Op::TailCall => {
8103 let fr = *self.top_frame();
8104 let abs = base + inst.a();
8105 let mut nargs = if inst.b() == 0 {
8106 self.top - (abs + 1)
8107 } else {
8108 inst.b() - 1
8109 };
8110 // A tail call pops this frame before begin_call, so a
8111 // non-callable target would lose its name/position. Report
8112 // it now (PUC reads funcname from the still-current ci),
8113 // while the frame is intact, for "(field 'x')"-style info.
8114 let mut func = self.stack[abs as usize];
8115 if !matches!(func, Value::Closure(_) | Value::Native(_))
8116 && self.get_mm(func, Mm::Call).is_nil()
8117 {
8118 return Err(self.call_err(func));
8119 }
8120 // PUC `luaD_pretailcall` resolves a chain of `__call`
8121 // metamethods *in place* before deciding whether to
8122 // collapse this frame. Without that, each __call hop
8123 // would push a fresh Lua frame and a 10000-deep
8124 // tail-recursion through a 100-deep __call chain
8125 // (5.4 calls.lua :172) blows up. Mirror the PUC loop:
8126 // shift args right, install the handler at `abs`, retry.
8127 // Chain depth limit matches the call-site `begin_call`
8128 // version cap (5.5 calls.lua :223 — 15 max, then "too
8129 // long"; 16th wrap fails the call). An infinite
8130 // self-referential `__call` would otherwise spin.
8131 let chain_cap = if self.version >= LuaVersion::Lua55 {
8132 15
8133 } else {
8134 MAX_CCMT
8135 };
8136 let mut chain = 0u32;
8137 while !matches!(func, Value::Closure(_) | Value::Native(_)) {
8138 let mm = self.get_mm(func, Mm::Call);
8139 if mm.is_nil() {
8140 return Err(self.call_err(func));
8141 }
8142 chain += 1;
8143 if chain > chain_cap {
8144 return Err(self.rt_err("'__call' chain too long"));
8145 }
8146 let end = (abs + 1 + nargs) as usize;
8147 if self.stack.len() < end + 1 {
8148 self.stack.resize(end + 1, Value::Nil);
8149 }
8150 for i in (0..=nargs).rev() {
8151 self.stack[(abs + 1 + i) as usize] = self.stack[(abs + i) as usize];
8152 }
8153 self.stack[abs as usize] = mm;
8154 nargs += 1;
8155 self.top = abs + 1 + nargs;
8156 func = mm;
8157 }
8158 // PUC's tail-call collapse is Lua→Lua only. A tail call to
8159 // a C function runs the C function under the *current* Lua
8160 // activation (no frame fold — a C frame has nothing to
8161 // collapse into); after the C function returns, the
8162 // calling Lua function returns those results normally.
8163 // Mirror that: keep our Lua frame on the stack, call the
8164 // target through `begin_call(abs, …)` as a regular call,
8165 // and let the fallback `Op::Return` that the compiler
8166 // emits right after `Op::TailCall` forward the results.
8167 // 5.1 closure.lua :177's `return getfenv()` from inside
8168 // foo needs level 1 to resolve to foo, not to the
8169 // thread's globals fallback that happens when no Lua
8170 // frame is on the stack.
8171 let lua_target = matches!(func, Value::Closure(_));
8172 if lua_target {
8173 self.close_slots(fr.base, None)?;
8174 for i in 0..=nargs {
8175 self.stack[(fr.func_slot + i) as usize] =
8176 self.stack[(abs + i) as usize];
8177 }
8178 // v2.5 P1B-2A: clear the slot range that's now
8179 // stranded by the tail-call collapse. The args
8180 // were copied to `[fr.func_slot..fr.func_slot+
8181 // nargs+1)`; the source slots `[abs..abs+
8182 // nargs+1)` still hold the same `Value::Closure
8183 // / Value::Str / ...` entries, but they're past
8184 // the new call's window. Without this clear, a
8185 // later GC with wider gc_top would mark stale
8186 // pointers there (same UAF-A family the v2.3
8187 // finish_results slot-clear closed for the
8188 // Op::Return path).
8189 let new_top_lower_bound = fr.func_slot + nargs + 1;
8190 let prev_top = (self.top as usize).min(self.stack.len());
8191 if (new_top_lower_bound as usize) < prev_top {
8192 for slot in &mut self.stack[new_top_lower_bound as usize..prev_top] {
8193 *slot = Value::Nil;
8194 }
8195 }
8196 // PUC `CIST_TAIL`: the new Lua activation inherits
8197 // the popped frame's tailcalls count plus one for
8198 // this collapse. 5.1 db.lua :372 hammers 30000
8199 // recursive tail calls and expects to see the
8200 // synthetic tail level for every one of them.
8201 self.pending_tailcalls = fr.tailcalls.saturating_add(1);
8202 frames_pop_sync(&mut self.frames, &mut self.frames_top);
8203 if !self.begin_call(fr.func_slot, Some(nargs), fr.nresults, false)?
8204 && self.frames.len() < entry_depth
8205 {
8206 // a native completed what was this function's result
8207 return Ok(self.take_results(fr.func_slot));
8208 }
8209 } else {
8210 // Native (or __call-bearing) target: regular call. The
8211 // results land at `abs..self.top` and the next op (the
8212 // fallback `Op::Return`) forwards them. `wanted = -1`
8213 // because the caller will multret them through Return.
8214 self.begin_call(abs, Some(nargs), -1, false)?;
8215 }
8216 }
8217 Op::Return | Op::Return0 | Op::Return1 => {
8218 let (abs_a, nret) = match inst.op() {
8219 Op::Return0 => (base, 0),
8220 Op::Return1 => (base + inst.a(), 1),
8221 _ => {
8222 let abs_a = base + inst.a();
8223 let nret = if inst.b() == 0 {
8224 self.top - abs_a
8225 } else {
8226 inst.b() - 1
8227 };
8228 (abs_a, nret)
8229 }
8230 };
8231 // close before moving results: __close handlers run above
8232 // the stack top, so the result region [abs_a..abs_a+nret)
8233 // stays intact across any yields the close performs.
8234 // Fixed-count returns may leave `self.top` below the last
8235 // result slot (the compiler does not always re-bump it);
8236 // raise it past the result region so `drive_close` parks
8237 // the handler call *above* — landing at `self.top` would
8238 // otherwise clobber a result with the handler closure.
8239 self.top = self.top.max(abs_a + nret);
8240 if let Some(vals) = self.begin_close(
8241 base,
8242 None,
8243 AfterClose::Return {
8244 abs_a,
8245 nret,
8246 from_native: false,
8247 },
8248 entry_depth,
8249 )? {
8250 return Ok(vals);
8251 }
8252 }
8253 Op::ForPrep => self.for_prep(inst, base)?,
8254 Op::ForLoop => {
8255 // P12 — trace JIT back-edge counter on the
8256 // numeric-for back-edge. ForLoop is always at
8257 // a back-edge position (when it continues);
8258 // for the trace recorder we treat it as the
8259 // close-detection equivalent of `Op::Jmp` with
8260 // negative offset. Counter only ticks when the
8261 // back-edge will actually fire (count > 0 in
8262 // the 5.4+ Int form, comparable predicates in
8263 // pre-5.3 / Float). The cheap check up front
8264 // matches the for_loop helper's branch.
8265 if self.jit.trace_enabled {
8266 let a = inst.a();
8267 let pre53 = self.version() <= LuaVersion::Lua53;
8268 let take_back_edge =
8269 match (self.r(base, a), self.r(base, a + 1), self.r(base, a + 2)) {
8270 (Value::Int(_), Value::Int(count), Value::Int(_)) if !pre53 => {
8271 count > 0
8272 }
8273 (Value::Int(cur), Value::Int(lim), Value::Int(st)) if pre53 => {
8274 let next = cur.wrapping_add(st);
8275 if st > 0 { next <= lim } else { next >= lim }
8276 }
8277 (Value::Float(cur), Value::Float(lim), Value::Float(st)) => {
8278 let next = cur + st;
8279 if st > 0.0 { next <= lim } else { next >= lim }
8280 }
8281 _ => false,
8282 };
8283 if take_back_edge {
8284 let proto = cl.proto;
8285 let c = proto.trace_hot_count.get();
8286 if c < u32::MAX / 2 {
8287 proto.trace_hot_count.set(c + 1);
8288 }
8289 if c == crate::jit::trace::TRACE_HOT_THRESHOLD
8290 && self.jit.active_trace.is_none()
8291 {
8292 // ForLoop's back-edge target = pc
8293 // after `add_pc(-bx)` runs from the
8294 // already-bumped f.pc (= pc + 1).
8295 // So target = (pc + 1) - bx.
8296 let target = (pc as i32 + 1 - inst.bx() as i32).max(0) as u32;
8297 let max_stack = cl.proto.max_stack as usize;
8298 let base_us = base as usize;
8299 let mut entry_tags = Vec::with_capacity(max_stack);
8300 for i in 0..max_stack {
8301 let (tag, _) = self.stack[base_us + i].unpack();
8302 entry_tags.push(tag);
8303 }
8304 self.jit.active_trace =
8305 Some(Box::new(crate::jit::trace::TraceRecord::start(
8306 cl.proto, target, entry_tags, false,
8307 )));
8308 // P12-S4 — record the frame the trace
8309 // started in. The currently-running
8310 // Lua frame is at len() - 1.
8311 self.jit.recording_frame_base = self.frames.len() - 1;
8312 }
8313 }
8314 }
8315 self.for_loop(inst, base);
8316 }
8317 Op::TForPrep => {
8318 // the 4th control slot is the iterator's closing value
8319 self.register_tbc(base + inst.a() + 3)?;
8320 self.add_pc(inst.bx() as i32);
8321 }
8322 Op::TForCall => {
8323 let abs = base + inst.a();
8324 let need = (abs + 7) as usize;
8325 if self.stack.len() < need {
8326 self.stack.resize(need, Value::Nil);
8327 }
8328 self.stack[(abs + 4) as usize] = self.stack[abs as usize];
8329 self.stack[(abs + 5) as usize] = self.stack[(abs + 1) as usize];
8330 self.stack[(abs + 6) as usize] = self.stack[(abs + 2) as usize];
8331 let nvars = inst.c() as i32;
8332 self.begin_call(abs + 4, Some(2), nvars, false)?;
8333 }
8334 Op::TForLoop => {
8335 let a = inst.a();
8336 let ctrl = self.r(base, a + 4);
8337 if !ctrl.is_nil() {
8338 // P12-S12-B v1 — trace JIT back-edge counter on
8339 // generic-for back-edge. TForLoop sits at the
8340 // tail of `for k,v in expr do ... end`; recorder
8341 // treats it as the close-detection equivalent of
8342 // a negative Op::Jmp. Gate on `take_back_edge`
8343 // (= `ctrl != nil`) so empty-iter loops don't
8344 // pollute hot_count. v1 only adds the trigger;
8345 // whitelist + helper + emit live in v2.
8346 if self.jit.trace_enabled {
8347 let proto = cl.proto;
8348 let c = proto.trace_hot_count.get();
8349 if c < u32::MAX / 2 {
8350 proto.trace_hot_count.set(c + 1);
8351 }
8352 if c == crate::jit::trace::TRACE_HOT_THRESHOLD
8353 && self.jit.active_trace.is_none()
8354 {
8355 // TForLoop back-edge target = pc after
8356 // `add_pc(-bx)` runs from the already-
8357 // bumped f.pc (= pc + 1). So target =
8358 // (pc + 1) - bx, normally landing on
8359 // body_top (the op right after TForPrep).
8360 let target = (pc as i32 + 1 - inst.bx() as i32).max(0) as u32;
8361 let max_stack = cl.proto.max_stack as usize;
8362 let base_us = base as usize;
8363 let mut entry_tags = Vec::with_capacity(max_stack);
8364 for i in 0..max_stack {
8365 let (tag, _) = self.stack[base_us + i].unpack();
8366 entry_tags.push(tag);
8367 }
8368 // P12-S12-B-v5 — snapshot the iter
8369 // fn's address if Native, so the
8370 // lowerer can specialise ipairs into
8371 // inline Table aget IR.
8372 let iter_ptr =
8373 if let Value::Native(n) = self.stack[base_us + a as usize] {
8374 Some(n.f as usize)
8375 } else {
8376 None
8377 };
8378 // P12-S12-C v3 — snapshot R[A+5]'s
8379 // tag (= current iter's val from
8380 // the just-fired TForCall). The v5
8381 // inline aget fast_blk emits a
8382 // runtime guard against this tag;
8383 // mixed-tag arrays deopt rather
8384 // than producing garbage pointers
8385 // through the v2 spill path.
8386 let val_slot = base_us + (a as usize) + 5;
8387 let val_tag = if val_slot < self.stack.len() {
8388 Some(self.stack[val_slot].unpack().0)
8389 } else {
8390 None
8391 };
8392 let mut rec = crate::jit::trace::TraceRecord::start(
8393 cl.proto, target, entry_tags, false,
8394 );
8395 rec.tfor_iter_ptr = iter_ptr;
8396 rec.tfor_val_tag = val_tag;
8397 self.jit.active_trace = Some(Box::new(rec));
8398 self.jit.recording_frame_base = self.frames.len() - 1;
8399 }
8400 }
8401 self.set_r(base, a + 2, ctrl);
8402 self.add_pc(-(inst.bx() as i32));
8403 }
8404 }
8405 Op::Closure => {
8406 let proto = cl.proto.protos[inst.bx() as usize];
8407 let n_ups = proto.upvals.len();
8408 // P11-S5d.M — build upvals on the stack for small
8409 // closures, skipping the per-call Vec/Box alloc
8410 // that closure_alloc's 10k iters pay. INLINE_UPVALS_N
8411 // = 2 covers most Lua source (1 captured local, or
8412 // _ENV + a single capture). Beyond that, fall back
8413 // to a heap Vec.
8414 use crate::runtime::function::INLINE_UPVALS_N;
8415 let mut stack_buf: [std::mem::MaybeUninit<
8416 Gc<crate::runtime::function::Upvalue>,
8417 >; INLINE_UPVALS_N] = [std::mem::MaybeUninit::uninit(); INLINE_UPVALS_N];
8418 let mut heap_buf: Vec<Gc<crate::runtime::function::Upvalue>> = Vec::new();
8419 let use_inline = n_ups <= INLINE_UPVALS_N;
8420 if !use_inline {
8421 heap_buf.reserve_exact(n_ups);
8422 }
8423 for (i, d) in proto.upvals.iter().enumerate() {
8424 let uv = if d.in_stack {
8425 self.find_or_create_upval(base + d.index as u32)
8426 } else {
8427 cl.upvals()[d.index as usize]
8428 };
8429 if use_inline {
8430 stack_buf[i] = std::mem::MaybeUninit::new(uv);
8431 } else {
8432 heap_buf.push(uv);
8433 }
8434 }
8435 // Tiny shim around the two paths so the 5.1 _ENV
8436 // clone + cache check below see one uniform
8437 // `&mut [Gc<Upvalue>]`. The stack_buf slice points
8438 // into the local frame (still valid through the
8439 // rest of this Op::Closure handler).
8440 let ups: &mut [Gc<crate::runtime::function::Upvalue>] = if use_inline {
8441 // SAFETY: the first n_ups slots of stack_buf
8442 // were initialised above; we hand out a slice
8443 // covering exactly them.
8444 unsafe {
8445 std::slice::from_raw_parts_mut(
8446 stack_buf.as_mut_ptr()
8447 as *mut Gc<crate::runtime::function::Upvalue>,
8448 n_ups,
8449 )
8450 }
8451 } else {
8452 &mut heap_buf[..]
8453 };
8454 // PUC 5.1 had per-function environments: every Lua
8455 // function carried its own `env` slot, snapshotted from
8456 // the creating function's env at closure time, so a
8457 // `setfenv` on one closure never bled into a sibling.
8458 // luna models that by giving the 5.1 closure a *fresh*
8459 // closed upvalue for whichever cell holds `_ENV`, seeded
8460 // from the parent's current env value. Only that cell is
8461 // cloned — every other upvalue keeps its open/shared
8462 // identity (so e.g. `local function range(...) ...
8463 // range(...) ... end` still sees its self-reference). 5.2+
8464 // keeps the shared-upval model (and the proto cache that
8465 // depends on it).
8466 let v51 = self.version() <= LuaVersion::Lua51;
8467 if v51 && proto.env_upval_idx != u8::MAX {
8468 let i = proto.env_upval_idx as usize;
8469 let cur = match ups[i].state() {
8470 UpvalState::Open { slot, thread } => self.read_slot(slot, thread),
8471 UpvalState::Closed(v) => v,
8472 };
8473 ups[i] = self.heap.new_upvalue(UpvalState::Closed(cur));
8474 }
8475 let ups_slice: &[Gc<crate::runtime::function::Upvalue>] = ups;
8476 // PUC 5.2+ `getcached`: a Proto remembers its last LClosure
8477 // and reuses it when every fresh-upvalue binding still
8478 // points to the same Upvalue object as the cached one.
8479 // That keeps `function() return outer end` repeated in a
8480 // loop comparing equal across iterations (the captured
8481 // outer is a shared open upvalue), while `function()
8482 // return loop_var end` gets a fresh closure each round
8483 // because the loop var is re-created per iteration. PUC
8484 // 5.1 predated the cache, and the per-closure `_ENV`
8485 // clone above would defeat it anyway, so skip it.
8486 let nc = if v51 {
8487 self.heap.new_closure_inline(proto, ups_slice)
8488 } else {
8489 let cached = proto.cache.get().filter(|c| {
8490 c.upvals().len() == ups_slice.len()
8491 && c.upvals()
8492 .iter()
8493 .zip(ups_slice.iter())
8494 .all(|(a, b)| std::ptr::eq(a.as_ptr(), b.as_ptr()))
8495 });
8496 match cached {
8497 Some(c) => c,
8498 None => {
8499 let n = self.heap.new_closure_inline(proto, ups_slice);
8500 proto.cache.set(Some(n));
8501 n
8502 }
8503 }
8504 };
8505 self.set_r(base, inst.a(), Value::Closure(nc));
8506 self.maybe_collect_garbage(base + inst.a() + 1);
8507 }
8508 Op::Vararg => {
8509 let abs_a = base + inst.a();
8510 let wanted = inst.c() as i32 - 1;
8511 // A materialized named vararg lives in func_slot (its writes
8512 // must be visible to `...`); otherwise spread the extra args
8513 // straight off the stack at func_slot+1 .. +n_varargs.
8514 let vt = match self.stack[func_slot as usize] {
8515 Value::Table(t) => Some(t),
8516 _ => None,
8517 };
8518 let n = match vt {
8519 Some(t) => {
8520 let n_key = Value::Str(self.heap.intern(b"n"));
8521 // PUC getnumargs: a named vararg `t.n` set out of the
8522 // integer range [0, INT_MAX/2] is rejected here
8523 match t.get(n_key) {
8524 Value::Int(n) if (n as u64) <= (i32::MAX as u64 / 2) => n as u32,
8525 _ => return Err(self.rt_err("vararg table has no proper 'n'")),
8526 }
8527 }
8528 None => n_varargs,
8529 };
8530 let count = if wanted < 0 { n } else { wanted as u32 };
8531 let need = (abs_a + count) as usize;
8532 if self.stack.len() < need {
8533 self.stack.resize(need, Value::Nil);
8534 }
8535 for i in 0..count {
8536 let v = if i >= n {
8537 Value::Nil
8538 } else if let Some(t) = vt {
8539 t.get_int(i as i64 + 1)
8540 } else {
8541 self.stack[(func_slot + 1 + i) as usize]
8542 };
8543 self.stack[(abs_a + i) as usize] = v;
8544 }
8545 if wanted < 0 {
8546 self.top = abs_a + count;
8547 }
8548 }
8549 Op::GetVarg => {
8550 // materialize the vararg table (PUC table.pack shape) from the
8551 // stack varargs — used when the named vararg is written /
8552 // escapes / is `_ENV`. It is kept BOTH in func_slot (so `...`
8553 // sees later writes) and in the local register R[A].
8554 let n = n_varargs;
8555 let t = self.heap.new_table();
8556 {
8557 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
8558 let tm = unsafe { t.as_mut() };
8559 for i in 0..n {
8560 let _ = tm.set_int(
8561 &mut self.heap,
8562 i as i64 + 1,
8563 self.stack[(func_slot + 1 + i) as usize],
8564 );
8565 }
8566 }
8567 let n_key = Value::Str(self.heap.intern(b"n"));
8568 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
8569 unsafe { t.as_mut() }
8570 .set(&mut self.heap, n_key, Value::Int(n as i64))
8571 .expect("'n' is a valid key");
8572 // once-per-table barrier (mirror SETLIST): t is born BLACK
8573 // during Propagate; the bulk inserts above don't barrier.
8574 self.heap
8575 .barrier_back(t.as_ptr() as *mut crate::runtime::heap::GcHeader);
8576 self.stack[func_slot as usize] = Value::Table(t);
8577 self.set_r(base, inst.a(), Value::Table(t));
8578 }
8579 Op::VargIdx => {
8580 // R[A] := vararg[R[C]] without allocating: integer key in
8581 // [1,n] → that vararg, "n" → the count, else nil.
8582 let key = self.r(base, inst.c());
8583 let n = n_varargs;
8584 let v = match key {
8585 Value::Int(k) if k >= 1 && (k as u64) <= n as u64 => {
8586 self.stack[(func_slot + k as u32) as usize]
8587 }
8588 Value::Float(f) if f.fract() == 0.0 && f >= 1.0 && f <= n as f64 => {
8589 self.stack[(func_slot + f as u32) as usize]
8590 }
8591 Value::Str(s) if s.as_bytes() == b"n" => Value::Int(n as i64),
8592 _ => Value::Nil,
8593 };
8594 self.set_r(base, inst.a(), v);
8595 }
8596 Op::ErrNNil => {
8597 let v = self.r(base, inst.a());
8598 if !matches!(v, Value::Nil) {
8599 let bx = inst.bx();
8600 let name = if bx == 0 {
8601 "?".to_string()
8602 } else {
8603 match cl.proto.consts[(bx - 1) as usize] {
8604 Value::Str(s) => String::from_utf8_lossy(s.as_bytes()).into_owned(),
8605 _ => "?".to_string(),
8606 }
8607 };
8608 return Err(self.rt_err(&format!("global '{name}' already defined")));
8609 }
8610 }
8611 Op::ExtraArg => unreachable!("EXTRAARG executed directly"),
8612 }
8613 }
8614 }
8615
8616 #[inline(always)]
8617 fn pc_of_top(&self) -> u32 {
8618 self.top_frame().pc
8619 }
8620
8621 #[inline(always)]
8622 fn bump_pc(&mut self) {
8623 // Inline `top_frame_mut`: top is guaranteed Lua (continuation frames
8624 // drained at dispatch loop head). Avoids the and_then/lua_mut Option
8625 // layers — bump_pc fires per Jmp / cond_skip miss, so the savings add
8626 // up over `fib_28`'s ~500k jumps.
8627 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
8628 match unsafe { self.frames.last_mut().unwrap_unchecked() } {
8629 CallFrame::Lua(f) => f.pc += 1,
8630 _ => unreachable!("Cont frame at bump_pc"),
8631 }
8632 }
8633
8634 #[inline(always)]
8635 fn add_pc(&mut self, d: i32) {
8636 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
8637 match unsafe { self.frames.last_mut().unwrap_unchecked() } {
8638 CallFrame::Lua(f) => f.pc = (f.pc as i64 + d as i64) as u32,
8639 _ => unreachable!("Cont frame at add_pc"),
8640 }
8641 }
8642
8643 /// PUC conditional-skip convention: the JMP that follows is executed when
8644 /// `cond == k`; otherwise it is skipped.
8645 #[inline(always)]
8646 fn cond_skip(&mut self, cond: bool, k: bool) {
8647 if cond != k {
8648 self.bump_pc();
8649 }
8650 }
8651
8652 // ---- indexing (with __index/__newindex chains) ----
8653
8654 /// The `#` length operation: string byte length, `__len` if present, else
8655 /// the raw table border. Returns the raw length value (may be non-integer
8656 /// when `__len` is exotic).
8657 pub(crate) fn len_value(&mut self, v: Value) -> Result<Value, LuaError> {
8658 match self.len_step(v)? {
8659 MmOut::Done(n) => Ok(n),
8660 // PUC calls unary metamethods with the operand twice
8661 MmOut::Mm { func, recv } => self.call_mm1(func, &[recv, recv]),
8662 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from len_step"),
8663 }
8664 }
8665
8666 /// Length fast path: a string's byte count or a table's raw border when no
8667 /// `__len` is present (`Done`); otherwise the `__len` metamethod (`Mm`),
8668 /// called with the operand twice. Errors for a non-table with no `__len`.
8669 fn len_step(&mut self, v: Value) -> Result<MmOut, LuaError> {
8670 match v {
8671 Value::Str(s) => Ok(MmOut::Done(Value::Int(s.len() as i64))),
8672 Value::Table(t) => {
8673 // PUC 5.1's `__len` applies to userdata only — `luaV_objlen`
8674 // there takes the raw border for a table without consulting
8675 // the metatable, so `#setmetatable({}, {__len = f})` is 0 on
8676 // 5.1 and 7 on 5.2+. Verified against stock 5.1.5 / 5.2.4.
8677 if self.version() == crate::version::LuaVersion::Lua51 {
8678 return Ok(MmOut::Done(Value::Int(t.len())));
8679 }
8680 let mm = self.get_mm(v, Mm::Len);
8681 if mm.is_nil() {
8682 Ok(MmOut::Done(Value::Int(t.len())))
8683 } else {
8684 Ok(MmOut::Mm { func: mm, recv: v })
8685 }
8686 }
8687 _ => {
8688 let mm = self.get_mm(v, Mm::Len);
8689 if mm.is_nil() {
8690 Err(self.type_err("get length of", v))
8691 } else {
8692 Ok(MmOut::Mm { func: mm, recv: v })
8693 }
8694 }
8695 }
8696 }
8697
8698 /// PUC luaL_len: the length as an integer, erroring if `__len` returned a
8699 /// value with no integer representation.
8700 pub(crate) fn checked_len(&mut self, v: Value) -> Result<i64, LuaError> {
8701 match self.len_value(v)? {
8702 Value::Int(i) => Ok(i),
8703 Value::Float(f) => crate::runtime::value::f2i_exact(f)
8704 .ok_or_else(|| self.rt_err("object length is not an integer")),
8705 _ => Err(self.rt_err("object length is not an integer")),
8706 }
8707 }
8708
8709 pub(crate) fn index_value(&mut self, t: Value, key: Value) -> Result<Value, LuaError> {
8710 match self.index_step(t, key)? {
8711 MmOut::Done(v) => Ok(v),
8712 MmOut::Mm { func, recv } => self.call_mm1(func, &[recv, key]),
8713 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from index_step"),
8714 }
8715 }
8716
8717 /// Resolve `t[key]` through the `__index` chain, stopping at the first raw
8718 /// hit (`Done`) or function metamethod (`Mm`). Table-valued `__index` links
8719 /// are followed inline (no yield possible); only a function link can yield.
8720 fn index_step(&mut self, t: Value, key: Value) -> Result<MmOut, LuaError> {
8721 let mut cur = t;
8722 for _ in 0..MAX_TAG_LOOP {
8723 let mm = match cur {
8724 Value::Table(tb) => {
8725 let v = tb.get(key);
8726 if !v.is_nil() {
8727 return Ok(MmOut::Done(v));
8728 }
8729 let mm = self.get_mm(cur, Mm::Index);
8730 if mm.is_nil() {
8731 return Ok(MmOut::Done(Value::Nil));
8732 }
8733 mm
8734 }
8735 v => {
8736 let mm = self.get_mm(v, Mm::Index);
8737 if mm.is_nil() {
8738 return Err(self.type_err("index", v));
8739 }
8740 mm
8741 }
8742 };
8743 match mm {
8744 Value::Closure(_) | Value::Native(_) => {
8745 return Ok(MmOut::Mm {
8746 func: mm,
8747 recv: cur,
8748 });
8749 }
8750 next => cur = next,
8751 }
8752 }
8753 Err(self.rt_err("'__index' chain too long; possible loop"))
8754 }
8755
8756 pub(crate) fn newindex_value(
8757 &mut self,
8758 t: Value,
8759 key: Value,
8760 v: Value,
8761 ) -> Result<(), LuaError> {
8762 match self.newindex_step(t, key, v)? {
8763 MmOut::Done(_) => Ok(()),
8764 MmOut::Mm { func, recv } => {
8765 self.call_value(func, &[recv, key, v])?;
8766 Ok(())
8767 }
8768 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from newindex_step"),
8769 }
8770 }
8771
8772 /// Resolve `t[key] = v` through the `__newindex` chain. A raw assignment is
8773 /// performed inline (returning `Done`); only a function metamethod (`Mm`)
8774 /// needs an actual call — which the caller may run yieldably.
8775 fn newindex_step(&mut self, t: Value, key: Value, v: Value) -> Result<MmOut, LuaError> {
8776 // v2.13 WUC read-time probe (gc-verify): a dead query key at a
8777 // WRITE site, attributed to the instruction that produced it.
8778 #[cfg(feature = "gc-verify")]
8779 if let Some(p) = match key {
8780 Value::Str(s) => Some(s.as_ptr() as usize),
8781 Value::Table(t2) => Some(t2.as_ptr() as usize),
8782 _ => None,
8783 } && crate::runtime::gc_verify_probe::is_freed(p)
8784 {
8785 let detail = match self.frames.last() {
8786 Some(CallFrame::Lua(f)) => {
8787 let pc = f.pc as usize;
8788 let mut w = String::new();
8789 for q in pc.saturating_sub(6)..(pc + 2) {
8790 if let Some(inst) = f.closure.proto.code.get(q) {
8791 w.push_str(&format!(
8792 "\n [{q}] {:?} a={} b={} c={} k={}",
8793 inst.op(),
8794 inst.a(),
8795 inst.b(),
8796 inst.c(),
8797 inst.k()
8798 ));
8799 }
8800 }
8801 format!("pc={pc} base={} gc_top={} window:{w}", f.base, self.gc_top)
8802 }
8803 _ => "non-Lua frame".into(),
8804 };
8805 panic!("[gc-verify] newindex_step QUERY key {p:#x} freed. {detail}");
8806 }
8807 let mut cur = t;
8808 for _ in 0..MAX_TAG_LOOP {
8809 let mm = match cur {
8810 Value::Table(tb) => {
8811 // PI-A3 single-walk collapse — Table::try_set_existing
8812 // fuses the prior `tb.get(key).is_nil()` gate and
8813 // `raw_set` walk into one chain traversal when the
8814 // key is already present with a non-nil value. The
8815 // __newindex chain semantics are preserved by the
8816 // identity (slot_nil ⇔ fire_newindex); see
8817 // .dev/rfcs/v2.0-pi-phase2-a3-audit.md §4.
8818 //
8819 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the
8820 // heap is single-threaded and the pointer is live as
8821 // long as it is reachable from active roots (see
8822 // heap.rs:5-7). Mirrors the raw_set wrapper below.
8823 if unsafe { tb.as_mut() }.try_set_existing(key, v) {
8824 self.heap
8825 .barrier_back(tb.as_ptr() as *mut crate::runtime::heap::GcHeader);
8826 return Ok(MmOut::Done(Value::Nil));
8827 }
8828 let mm = self.get_mm(cur, Mm::NewIndex);
8829 if mm.is_nil() {
8830 self.raw_set(tb, key, v)?;
8831 return Ok(MmOut::Done(Value::Nil));
8832 }
8833 mm
8834 }
8835 bad => {
8836 let mm = self.get_mm(bad, Mm::NewIndex);
8837 if mm.is_nil() {
8838 return Err(self.type_err("index", bad));
8839 }
8840 mm
8841 }
8842 };
8843 match mm {
8844 Value::Closure(_) | Value::Native(_) => {
8845 return Ok(MmOut::Mm {
8846 func: mm,
8847 recv: cur,
8848 });
8849 }
8850 next => cur = next,
8851 }
8852 }
8853 Err(self.rt_err("'__newindex' chain too long; possible loop"))
8854 }
8855
8856 fn raw_set(&mut self, t: Gc<Table>, key: Value, v: Value) -> Result<(), LuaError> {
8857 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
8858 match unsafe { t.as_mut() }.set(&mut self.heap, key, v) {
8859 Ok(()) => {
8860 self.heap
8861 .barrier_back(t.as_ptr() as *mut crate::runtime::heap::GcHeader);
8862 Ok(())
8863 }
8864 Err(TableError::NilIndex) => Err(self.rt_err("table index is nil")),
8865 Err(TableError::NanIndex) => Err(self.rt_err("table index is NaN")),
8866 Err(TableError::Overflow) => Err(self.rt_err("table overflow")),
8867 Err(TableError::InvalidNext) => unreachable!(),
8868 }
8869 }
8870
8871 /// Decide equality, or surface the `__eq` metamethod to call. `Done` carries
8872 /// the boolean result; `Mm` (when raw equality fails and both are tables
8873 /// with an `__eq`) carries the metamethod — called with `(l, r)`.
8874 fn eq_step(&mut self, l: Value, r: Value) -> MmOut {
8875 if l.raw_eq(r) {
8876 return MmOut::Done(Value::Bool(true));
8877 }
8878 if let (Value::Table(_), Value::Table(_)) | (Value::Userdata(_), Value::Userdata(_)) =
8879 (l, r)
8880 {
8881 // PUC 5.2+ accepts any `__eq` reachable from either operand; 5.1
8882 // (and earlier) required the two operands' metatables to expose a
8883 // matching `__eq` (`get_compTM`) — `c == d` where `d` has no
8884 // metatable falls straight back to raw inequality. events.lua 5.1
8885 // :262 bakes this in.
8886 let mm = if self.version() <= LuaVersion::Lua51 {
8887 self.get_comp_mm(l, r, Mm::Eq)
8888 } else {
8889 let mut m = self.get_mm(l, Mm::Eq);
8890 if m.is_nil() {
8891 m = self.get_mm(r, Mm::Eq);
8892 }
8893 m
8894 };
8895 if !mm.is_nil() {
8896 return MmOut::Mm { func: mm, recv: l };
8897 }
8898 }
8899 MmOut::Done(Value::Bool(false))
8900 }
8901
8902 // ---- arithmetic ----
8903
8904 #[inline(always)]
8905 fn arith_rr(&mut self, inst: Inst, base: u32, op: ArithOp) -> Result<(), LuaError> {
8906 let l = self.r(base, inst.b());
8907 let r = self.r(base, inst.c());
8908 // hot path: Int + Int for Add / Sub / Mul — fib_28, loop_int_1m,
8909 // binary_trees all hammer these. Skipping coerce_num + the big
8910 // arith_fast match shaves several conditional moves per op.
8911 if let (Value::Int(a), Value::Int(b)) = (l, r) {
8912 let fast = match op {
8913 ArithOp::Add => Some(Value::Int(a.wrapping_add(b))),
8914 ArithOp::Sub => Some(Value::Int(a.wrapping_sub(b))),
8915 ArithOp::Mul => Some(Value::Int(a.wrapping_mul(b))),
8916 _ => None,
8917 };
8918 if let Some(v) = fast {
8919 self.set_r(base, inst.a(), v);
8920 return Ok(());
8921 }
8922 }
8923 // hot path: Float + Float for Add / Sub / Mul / Div — math_loop_100k
8924 // and any numeric workload with non-integer accumulators benefits.
8925 if let (Value::Float(a), Value::Float(b)) = (l, r) {
8926 let fast = match op {
8927 ArithOp::Add => Some(Value::Float(a + b)),
8928 ArithOp::Sub => Some(Value::Float(a - b)),
8929 ArithOp::Mul => Some(Value::Float(a * b)),
8930 ArithOp::Div => Some(Value::Float(a / b)),
8931 _ => None,
8932 };
8933 if let Some(v) = fast {
8934 self.set_r(base, inst.a(), v);
8935 return Ok(());
8936 }
8937 }
8938 match self.arith_fast(op, l, r)? {
8939 Some(v) => self.set_r(base, inst.a(), v),
8940 None => {
8941 let mm = self.arith_mm_func(op, l, r)?;
8942 let dst = base + inst.a();
8943 self.begin_meta_call(mm, &[l, r], MetaAction::Store { dst }, op.mm_name())?;
8944 }
8945 }
8946 Ok(())
8947 }
8948
8949 /// Fast path for an arithmetic/bitwise op: `Ok(Some(v))` when computed
8950 /// directly, `Ok(None)` when a metamethod is required (the caller decides
8951 /// whether to call it synchronously or yieldably).
8952 fn arith_fast(&mut self, op: ArithOp, l: Value, r: Value) -> Result<Option<Value>, LuaError> {
8953 use ArithOp::*;
8954 match op {
8955 BAnd | BOr | BXor | Shl | Shr => {
8956 // strings coerce for bitwise too (PUC tointegerns via cvt2num)
8957 match (coerce_num(l), coerce_num(r)) {
8958 (Some(a), Some(b)) => {
8959 let to_int = |n: Num| match n {
8960 Num::Int(i) => Some(i),
8961 Num::Float(f) => crate::runtime::value::f2i_exact(f),
8962 };
8963 let (Some(a), Some(b)) = (to_int(a), to_int(b)) else {
8964 // PUC luaG_tointerror: name the offending operand
8965 return Err(self.no_int_rep_err());
8966 };
8967 let v = match op {
8968 BAnd => a & b,
8969 BOr => a | b,
8970 BXor => a ^ b,
8971 Shl => shift_left(a, b),
8972 Shr => shift_left(a, b.wrapping_neg()),
8973 _ => unreachable!(),
8974 };
8975 return Ok(Some(Value::Int(v)));
8976 }
8977 _ => return Ok(None),
8978 }
8979 }
8980 _ => {}
8981 }
8982 let (ln, rn) = match (coerce_num(l), coerce_num(r)) {
8983 (Some(a), Some(b)) => (a, b),
8984 _ => return Ok(None),
8985 };
8986 let v = match (op, ln, rn) {
8987 (Add, Num::Int(a), Num::Int(b)) => Value::Int(a.wrapping_add(b)),
8988 (Sub, Num::Int(a), Num::Int(b)) => Value::Int(a.wrapping_sub(b)),
8989 (Mul, Num::Int(a), Num::Int(b)) => Value::Int(a.wrapping_mul(b)),
8990 (IDiv, Num::Int(a), Num::Int(b)) => {
8991 if b == 0 {
8992 return Err(self.rt_err("attempt to divide by zero"));
8993 }
8994 let mut q = a.wrapping_div(b);
8995 if (a ^ b) < 0 && q.wrapping_mul(b) != a {
8996 q -= 1;
8997 }
8998 Value::Int(q)
8999 }
9000 (Mod, Num::Int(a), Num::Int(b)) => {
9001 if b == 0 {
9002 return Err(self.rt_err("attempt to perform 'n%0'"));
9003 }
9004 let mut m = a.wrapping_rem(b);
9005 if m != 0 && (m ^ b) < 0 {
9006 m += b;
9007 }
9008 Value::Int(m)
9009 }
9010 (Add, a, b) => Value::Float(a.as_f64() + b.as_f64()),
9011 (Sub, a, b) => Value::Float(a.as_f64() - b.as_f64()),
9012 (Mul, a, b) => Value::Float(a.as_f64() * b.as_f64()),
9013 (Div, a, b) => Value::Float(a.as_f64() / b.as_f64()),
9014 (Pow, a, b) => Value::Float(a.as_f64().powf(b.as_f64())),
9015 (IDiv, a, b) => Value::Float((a.as_f64() / b.as_f64()).floor()),
9016 (Mod, a, b) => {
9017 let (x, y) = (a.as_f64(), b.as_f64());
9018 // PUC luai_nummod: correct fmod's sign without the `m*y`
9019 // product, which underflows to 0 for tiny denormals
9020 let mut m = x % y;
9021 if (m > 0.0 && y < 0.0) || (m < 0.0 && y > 0.0) {
9022 m += y;
9023 }
9024 Value::Float(m)
9025 }
9026 _ => unreachable!(),
9027 };
9028 Ok(Some(v))
9029 }
9030
9031 pub(crate) fn int_from(&mut self, v: Value, what: &str) -> Result<i64, LuaError> {
9032 match v {
9033 Value::Int(i) => Ok(i),
9034 Value::Float(f) => match crate::runtime::value::f2i_exact(f) {
9035 Some(i) => Ok(i),
9036 None => Err(self.rt_err("number has no integer representation")),
9037 },
9038 v => Err(self.type_err(what, v)),
9039 }
9040 }
9041
9042 fn int_from_num(&mut self, n: Num) -> Result<i64, LuaError> {
9043 match n {
9044 Num::Int(i) => Ok(i),
9045 Num::Float(f) => match crate::runtime::value::f2i_exact(f) {
9046 Some(i) => Ok(i),
9047 None => Err(self.rt_err("number has no integer representation")),
9048 },
9049 }
9050 }
9051
9052 /// Find the arithmetic/bitwise metamethod (left operand first), or raise the
9053 /// PUC type error when neither operand provides one.
9054 fn arith_mm_func(&mut self, op: ArithOp, l: Value, r: Value) -> Result<Value, LuaError> {
9055 use ArithOp::*;
9056 let event = match op {
9057 Add => Mm::Add,
9058 Sub => Mm::Sub,
9059 Mul => Mm::Mul,
9060 Div => Mm::Div,
9061 Mod => Mm::Mod,
9062 Pow => Mm::Pow,
9063 IDiv => Mm::IDiv,
9064 BAnd => Mm::BAnd,
9065 BOr => Mm::BOr,
9066 BXor => Mm::BXor,
9067 Shl => Mm::Shl,
9068 Shr => Mm::Shr,
9069 };
9070 let mut mm = self.get_mm(l, event);
9071 if mm.is_nil() {
9072 mm = self.get_mm(r, event);
9073 }
9074 if mm.is_nil() {
9075 let what = if matches!(op, BAnd | BOr | BXor | Shl | Shr) {
9076 "perform bitwise operation on"
9077 } else {
9078 // 5.4+ report string-involved arithmetic faults through
9079 // lstrlib's string-metatable arithmetic handlers, which
9080 // emit the per-op wording `attempt to add a 'string'
9081 // with a 'number'` (operands in syntactic order, quoted
9082 // type names, no varinfo). Non-string faults (nil+1,
9083 // {}+{}) keep the classic VM wording on every dialect —
9084 // v2.14 HC.4, probed against stock 5.1.5-5.5.0.
9085 if self.version >= crate::version::LuaVersion::Lua54
9086 && (matches!(l, Value::Str(_)) || matches!(r, Value::Str(_)))
9087 {
9088 let verb = match op {
9089 Add => "add",
9090 Sub => "sub",
9091 Mul => "mul",
9092 Div => "div",
9093 Mod => "mod",
9094 Pow => "pow",
9095 IDiv => "idiv",
9096 BAnd | BOr | BXor | Shl | Shr => unreachable!(),
9097 };
9098 let t1 = self.obj_typename(l);
9099 let t2 = self.obj_typename(r);
9100 return Err(self.rt_err(&format!("attempt to {verb} a '{t1}' with a '{t2}'")));
9101 }
9102 "perform arithmetic on"
9103 };
9104 let bad = if coerce_num(l).is_none() { l } else { r };
9105 return Err(self.type_err(what, bad));
9106 }
9107 Ok(mm)
9108 }
9109
9110 // ---- comparison ----
9111
9112 pub(crate) fn less_than(&mut self, l: Value, r: Value, or_eq: bool) -> Result<bool, LuaError> {
9113 match self.less_step(l, r, or_eq)? {
9114 MmOut::Done(v) => Ok(v.truthy()),
9115 MmOut::Mm { func, .. } => Ok(self.call_mm1(func, &[l, r])?.truthy()),
9116 MmOut::CompareSynth { func } => {
9117 // ≤5.3 `__le` via `not __lt(r, l)`. Synchronous helper used
9118 // by library code (sort comparator etc.) — no yield expected
9119 // here (a yield would have hit `call_noyield`'s C boundary).
9120 Ok(!self.call_mm1(func, &[r, l])?.truthy())
9121 }
9122 }
9123 }
9124
9125 /// Decide `l < r` / `l <= r`, or surface the `__lt`/`__le` metamethod. `Done`
9126 /// carries the boolean result; `Mm` (for non-number/string operands) carries
9127 /// the metamethod — called with `(l, r)`; raises the PUC compare error when
9128 /// neither operand provides one.
9129 fn less_step(&mut self, l: Value, r: Value, or_eq: bool) -> Result<MmOut, LuaError> {
9130 let b = match (l, r) {
9131 (Value::Int(a), Value::Int(b)) => {
9132 if or_eq {
9133 a <= b
9134 } else {
9135 a < b
9136 }
9137 }
9138 (Value::Float(a), Value::Float(b)) => {
9139 if or_eq {
9140 a <= b
9141 } else {
9142 a < b
9143 }
9144 }
9145 (Value::Int(a), Value::Float(b)) => {
9146 if or_eq {
9147 int_le_float(a, b)
9148 } else {
9149 int_lt_float(a, b)
9150 }
9151 }
9152 (Value::Float(a), Value::Int(b)) => {
9153 if a.is_nan() {
9154 false
9155 } else if or_eq {
9156 !int_lt_float(b, a)
9157 } else {
9158 !int_le_float(b, a)
9159 }
9160 }
9161 (Value::Str(a), Value::Str(b)) => {
9162 let (a, b) = (a.as_bytes(), b.as_bytes());
9163 if or_eq { a <= b } else { a < b }
9164 }
9165 (l, r) => {
9166 let event = if or_eq { Mm::Le } else { Mm::Lt };
9167 // PUC 5.1's `get_compTM` rule applies to ordered comparisons
9168 // too: both operands' metatables must expose the same
9169 // implementation for `__lt` / `__le` to fire. events.lua 5.1
9170 // :262 expects `c < d` (where `d` has no metatable) to error
9171 // with the default "attempt to compare two table values"
9172 // rather than running c's `__lt` blindly.
9173 let mm = if self.version() <= LuaVersion::Lua51 {
9174 self.get_comp_mm(l, r, event)
9175 } else {
9176 let mut m = self.get_mm(l, event);
9177 if m.is_nil() {
9178 m = self.get_mm(r, event);
9179 }
9180 m
9181 };
9182 // PUC ≤5.3: `a <= b` falls back to `not (b < a)` when neither
9183 // operand carries `__le`. 5.4 dropped the synthesis (now
9184 // requires an explicit `__le`). events.lua 5.2/5.3 :172 relies
9185 // on the synthesis — its metatable defines only `__lt`.
9186 // The fallback calls `__lt(r, l)` synchronously (the suite's
9187 // `__lt` doesn't yield) and negates the result; the yieldable
9188 // `__lt` path stays reserved for the explicit `<` operator.
9189 if mm.is_nil() && or_eq && self.version <= crate::version::LuaVersion::Lua53 {
9190 let lt = Mm::Lt;
9191 let mut mm_lt = self.get_mm(l, lt);
9192 if mm_lt.is_nil() {
9193 mm_lt = self.get_mm(r, lt);
9194 }
9195 if !mm_lt.is_nil() {
9196 return Ok(MmOut::CompareSynth { func: mm_lt });
9197 }
9198 }
9199 if mm.is_nil() {
9200 // PUC luaG_ordererror: "two X values" when the operand
9201 // types match, "X with Y" otherwise (objtypename-aware).
9202 let (t1, t2) = (self.obj_typename(l), self.obj_typename(r));
9203 return Err(self.rt_err(&if t1 == t2 {
9204 format!("attempt to compare two {t1} values")
9205 } else {
9206 format!("attempt to compare {t1} with {t2}")
9207 }));
9208 }
9209 return Ok(MmOut::Mm { func: mm, recv: l });
9210 }
9211 };
9212 Ok(MmOut::Done(Value::Bool(b)))
9213 }
9214
9215 // ---- numeric for ----
9216
9217 fn for_prep(&mut self, inst: Inst, base: u32) -> Result<(), LuaError> {
9218 let a = inst.a();
9219 let init = self.r(base, a);
9220 let limit = self.r(base, a + 1);
9221 let step = self.r(base, a + 2);
9222 let (Some(init_n), Some(limit_n), Some(step_n)) =
9223 (as_num(init), as_num(limit), as_num(step))
9224 else {
9225 // PUC luaG_forerror: "bad 'for' <what> (number expected, got <type>)".
9226 // PUC checks limit, then step, then initial value.
9227 let (what, bad) = if as_num(limit).is_none() {
9228 ("limit", limit)
9229 } else if as_num(step).is_none() {
9230 ("step", step)
9231 } else {
9232 ("initial value", init)
9233 };
9234 let tn = self.obj_typename(bad);
9235 return Err(self.rt_err(&format!("bad 'for' {what} (number expected, got {tn})")));
9236 };
9237 // PUC 5.1–5.3 `OP_FORPREP` stores `i = init - step` and *unconditionally*
9238 // jumps to the matching `OP_FORLOOP` — the body never runs ahead of the
9239 // first test, so each successful iteration emits a backward `OP_FORLOOP`
9240 // jump (db.lua's `for i=1,4 do a=1 end` ↦ 5 line-hook events instead of
9241 // 5.4's 4). 5.4+ collapsed that to a count-based fall-through. The skip
9242 // distance in luna's encoding is `loop_pc - prep_pc`; firing
9243 // `add_pc(bx - 1)` lands the running pc on OP_FORLOOP itself.
9244 let pre53 = self.version() <= LuaVersion::Lua53;
9245 match (init_n, step_n) {
9246 (Num::Int(i0), Num::Int(st)) => {
9247 if st == 0 {
9248 return Err(self.rt_err("'for' step is zero"));
9249 }
9250 if pre53 {
9251 // PUC 5.3 `forlimit`: int limit passes through; float limit
9252 // gets clamped to MIN/MAX with a `stopnow` flag set only
9253 // when the clamp is unreachable (positive float with a
9254 // negative step → limit=MAX, stopnow; negative float with
9255 // step>=0 → limit=MIN, stopnow). On `stopnow` PUC rewrites
9256 // `init = 0` so OP_FORLOOP's first test against the
9257 // unreachable clamp fails cleanly. An ordinary in-range
9258 // empty loop (e.g. `for i = 1, 0`) is *not* `stopnow` — it
9259 // lets OP_FORLOOP's natural test reject the first step.
9260 let (lim, stopnow) = match limit_n {
9261 Num::Int(l) => (l, false),
9262 Num::Float(f) => {
9263 if f.is_nan() {
9264 (0, true)
9265 } else if f >= i64::MAX as f64 + 1.0 {
9266 // beyond +MAX: unreachable for a decreasing loop
9267 (i64::MAX, st < 0)
9268 } else if f <= i64::MIN as f64 {
9269 // beyond -MIN: unreachable for an increasing loop
9270 (i64::MIN, st >= 0)
9271 } else if st > 0 {
9272 (f.floor() as i64, false)
9273 } else {
9274 (f.ceil() as i64, false)
9275 }
9276 }
9277 };
9278 let initv = if stopnow { 0 } else { i0 };
9279 let pre = initv.wrapping_sub(st);
9280 self.set_r(base, a, Value::Int(pre));
9281 self.set_r(base, a + 1, Value::Int(lim));
9282 self.set_r(base, a + 2, Value::Int(st));
9283 self.add_pc(inst.bx() as i32 - 1);
9284 return Ok(());
9285 }
9286 let (lim, empty) = int_for_limit(limit_n, i0, st);
9287 if empty {
9288 self.add_pc(inst.bx() as i32);
9289 return Ok(());
9290 }
9291 let count = if st > 0 {
9292 (lim as u64).wrapping_sub(i0 as u64) / (st as u64)
9293 } else {
9294 (i0 as u64).wrapping_sub(lim as u64) / (st as i128).unsigned_abs() as u64
9295 };
9296 self.set_r(base, a, Value::Int(i0));
9297 self.set_r(base, a + 1, Value::Int(count as i64));
9298 self.set_r(base, a + 2, Value::Int(st));
9299 self.set_r(base, a + 3, Value::Int(i0));
9300 }
9301 _ => {
9302 let (x0, lim, st) = (init_n.as_f64(), limit_n.as_f64(), step_n.as_f64());
9303 if st == 0.0 {
9304 return Err(self.rt_err("'for' step is zero"));
9305 }
9306 if pre53 {
9307 let pre = x0 - st;
9308 self.set_r(base, a, Value::Float(pre));
9309 self.set_r(base, a + 1, Value::Float(lim));
9310 self.set_r(base, a + 2, Value::Float(st));
9311 self.add_pc(inst.bx() as i32 - 1);
9312 return Ok(());
9313 }
9314 let runs = if st > 0.0 { x0 <= lim } else { x0 >= lim };
9315 if !runs {
9316 self.add_pc(inst.bx() as i32);
9317 return Ok(());
9318 }
9319 self.set_r(base, a, Value::Float(x0));
9320 self.set_r(base, a + 1, Value::Float(lim));
9321 self.set_r(base, a + 2, Value::Float(st));
9322 self.set_r(base, a + 3, Value::Float(x0));
9323 }
9324 }
9325 Ok(())
9326 }
9327
9328 #[inline(always)]
9329 fn for_loop(&mut self, inst: Inst, base: u32) {
9330 let a = inst.a();
9331 // PUC 5.1–5.3 `OP_FORLOOP` compares the post-step `i` to `limit`
9332 // directly (R[a+1] holds the limit, *not* a remaining-count) so the
9333 // first iteration's test fires through the same backward-jump path as
9334 // every later iteration. 5.4+ switched to the count-based form luna
9335 // already uses for `Int`; the float branch was already PUC-3.x-style.
9336 let pre53 = self.version() <= LuaVersion::Lua53;
9337 match self.r(base, a) {
9338 Value::Int(cur) if pre53 => {
9339 let Value::Int(lim) = self.r(base, a + 1) else {
9340 unreachable!()
9341 };
9342 let Value::Int(st) = self.r(base, a + 2) else {
9343 unreachable!()
9344 };
9345 let next = cur.wrapping_add(st);
9346 let cont = if st > 0 { next <= lim } else { next >= lim };
9347 if cont {
9348 self.set_r(base, a, Value::Int(next));
9349 self.set_r(base, a + 3, Value::Int(next));
9350 self.add_pc(-(inst.bx() as i32));
9351 }
9352 }
9353 Value::Int(cur) => {
9354 let Value::Int(count) = self.r(base, a + 1) else {
9355 unreachable!()
9356 };
9357 if count > 0 {
9358 let Value::Int(st) = self.r(base, a + 2) else {
9359 unreachable!()
9360 };
9361 let next = cur.wrapping_add(st);
9362 self.set_r(base, a, Value::Int(next));
9363 self.set_r(base, a + 1, Value::Int(count - 1));
9364 self.set_r(base, a + 3, Value::Int(next));
9365 self.add_pc(-(inst.bx() as i32));
9366 }
9367 }
9368 Value::Float(cur) => {
9369 let Value::Float(lim) = self.r(base, a + 1) else {
9370 unreachable!()
9371 };
9372 let Value::Float(st) = self.r(base, a + 2) else {
9373 unreachable!()
9374 };
9375 let next = cur + st;
9376 let cont = if st > 0.0 { next <= lim } else { next >= lim };
9377 if cont {
9378 self.set_r(base, a, Value::Float(next));
9379 self.set_r(base, a + 3, Value::Float(next));
9380 self.add_pc(-(inst.bx() as i32));
9381 }
9382 }
9383 _ => unreachable!("corrupt for-loop state"),
9384 }
9385 }
9386
9387 // ---- native helpers (used by builtins) ----
9388
9389 /// A native function's own captured upvalue (self lives at func_slot).
9390 ///
9391 /// Public so `native_typed` trampolines and embedders authoring
9392 /// stateful natives via `native_with(...)` can read their upvals.
9393 pub fn nat_upval(&self, func_slot: u32, i: usize) -> Value {
9394 let Value::Native(nc) = self.stack[func_slot as usize] else {
9395 unreachable!("native frame without native closure");
9396 };
9397 nc.upvals[i]
9398 }
9399
9400 /// Number of upvalues captured by the native at `func_slot` (variadic
9401 /// captures such as the `io.lines` format list).
9402 pub(crate) fn nat_upcount(&self, func_slot: u32) -> usize {
9403 let Value::Native(nc) = self.stack[func_slot as usize] else {
9404 unreachable!("native frame without native closure");
9405 };
9406 nc.upvals.len()
9407 }
9408
9409 /// Write a native function's own upvalue (stateful iterators).
9410 pub(crate) fn nat_set_upval(&mut self, func_slot: u32, i: usize, v: Value) {
9411 let Value::Native(nc) = self.stack[func_slot as usize] else {
9412 unreachable!("native frame without native closure");
9413 };
9414 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
9415 unsafe { nc.as_mut() }.upvals[i] = v;
9416 // NativeClosure.upvals is traced as part of its Trace; a long-lived
9417 // stateful iterator closure (e.g. string.gmatch) sees many writes —
9418 // barrier_back once-and-done is cheaper than per-child forward.
9419 self.heap
9420 .barrier_back(nc.as_ptr() as *mut crate::runtime::heap::GcHeader);
9421 }
9422
9423 /// Read the i-th positional argument inside a `NativeFn` body
9424 /// (analogous to `lua_tovalue(L, i + 1)`). `i >= nargs` yields `Nil`,
9425 /// matching PUC's "missing arg is nil" contract. Public so embedders
9426 /// can author their own natives.
9427 pub fn nat_arg(&self, func_slot: u32, nargs: u32, i: u32) -> Value {
9428 if i < nargs {
9429 self.stack[(func_slot + 1 + i) as usize]
9430 } else {
9431 Value::Nil
9432 }
9433 }
9434
9435 /// Push the return values of a `NativeFn` and return their count
9436 /// (analogous to pushing N values then `return N` from a C function).
9437 /// Public so embedders can author their own natives.
9438 pub fn nat_return(&mut self, func_slot: u32, vals: &[Value]) -> u32 {
9439 let need = func_slot as usize + vals.len();
9440 if self.stack.len() < need {
9441 self.stack.resize(need, Value::Nil);
9442 }
9443 for (i, &v) in vals.iter().enumerate() {
9444 self.stack[func_slot as usize + i] = v;
9445 }
9446 vals.len() as u32
9447 }
9448
9449 /// Fast string concatenation of an adjacent pair, or `None` when a
9450 /// `__concat` metamethod is required.
9451 fn concat_pair(&mut self, l: Value, r: Value) -> Result<Option<Value>, LuaError> {
9452 let legacy = self.float_fmt();
9453 // Length-check fast paths for both string operands BEFORE the
9454 // (expensive) copy in `concat_piece`, so a runaway `a..a..a..…`
9455 // chain (5.1 big.lua / 5.5 heavy.lua's `teststring`) raises the
9456 // overflow on the first pair that would exceed `INT_MAX` instead
9457 // of allocating multi-GB intermediates first.
9458 let max_str = i32::MAX as usize;
9459 if let (Value::Str(ls), Value::Str(rs)) = (l, r) {
9460 let a_len = ls.as_bytes().len();
9461 let b_len = rs.as_bytes().len();
9462 let new_len = a_len.checked_add(b_len);
9463 if new_len.is_none() || new_len.unwrap() > max_str {
9464 return Err(self.rt_err("string length overflow"));
9465 }
9466 }
9467 match (concat_piece(l, legacy), concat_piece(r, legacy)) {
9468 (Some(a), Some(b)) => {
9469 // PUC `MAX_SIZE` for Lua strings is `INT_MAX`; an attempt to
9470 // concat past it raises "string length overflow"
9471 // (5.5 heavy.lua `teststring` doubles `a..a..…` until it hits
9472 // exactly this wall).
9473 let new_len = a.len().checked_add(b.len());
9474 if new_len.is_none() || new_len.unwrap() > max_str {
9475 return Err(self.rt_err("string length overflow"));
9476 }
9477 let mut combined = a;
9478 combined.extend_from_slice(&b);
9479 Ok(Some(Value::Str(self.heap.intern(&combined))))
9480 }
9481 _ => Ok(None),
9482 }
9483 }
9484
9485 /// Fold the concat operands occupying `[base_a .. self.top)` right-to-left
9486 /// into a single result at `base_a` (PUC `luaV_concat`). Returns after
9487 /// either finishing (result at `base_a`) or arming a yieldable `__concat`
9488 /// call — its `Meta` continuation re-enters here on the metamethod's return.
9489 fn concat_run(&mut self, base_a: u32) -> Result<(), LuaError> {
9490 // Sum the lengths of all all-Str operands BEFORE starting the
9491 // right-associative fold so a 129-operand `a..a..…` chain
9492 // (5.1 big.lua's `rep129(longs)`) raises overflow immediately,
9493 // not after dozens of multi-GB intermediate intern+hash rounds.
9494 // A non-Str operand falls through to the per-pair check.
9495 let max_str = i32::MAX as usize;
9496 let mut total: usize = 0;
9497 let mut all_str = true;
9498 for slot in base_a..self.top {
9499 match self.stack[slot as usize] {
9500 Value::Str(s) => match total.checked_add(s.as_bytes().len()) {
9501 Some(t) if t <= max_str => total = t,
9502 _ => return Err(self.rt_err("string length overflow")),
9503 },
9504 _ => {
9505 all_str = false;
9506 break;
9507 }
9508 }
9509 }
9510 let _ = all_str; // discrimination already captured by early returns above
9511 while self.top.saturating_sub(base_a) >= 2 {
9512 let i = self.top - 1; // rightmost operand
9513 let x = self.stack[(i - 1) as usize];
9514 let y = self.stack[i as usize];
9515 match self.concat_pair(x, y)? {
9516 Some(s) => {
9517 self.stack[(i - 1) as usize] = s;
9518 self.top = i; // consumed y
9519 }
9520 None => {
9521 let mut mm = self.get_mm(x, Mm::Concat);
9522 if mm.is_nil() {
9523 mm = self.get_mm(y, Mm::Concat);
9524 }
9525 if mm.is_nil() {
9526 let legacy = self.float_fmt();
9527 let bad = if concat_piece(x, legacy).is_none() {
9528 x
9529 } else {
9530 y
9531 };
9532 return Err(self.type_err("concatenate", bad));
9533 }
9534 // result lands at i-1, dropping y (top→i); resume continues.
9535 let dst = i - 1;
9536 self.begin_meta_call(
9537 mm,
9538 &[x, y],
9539 MetaAction::Concat { dst, base_a },
9540 "concat",
9541 )?;
9542 return Ok(());
9543 }
9544 }
9545 }
9546 self.maybe_collect_garbage(base_a + 1);
9547 Ok(())
9548 }
9549
9550 /// tostring with __tostring / __name support.
9551 pub(crate) fn tostring_value(&mut self, v: Value) -> Result<Vec<u8>, LuaError> {
9552 let mm = self.get_mm(v, Mm::ToString);
9553 if !mm.is_nil() {
9554 return match self.call_mm1(mm, &[v])? {
9555 Value::Str(s) => Ok(s.as_bytes().to_vec()),
9556 _ => Err(self.rt_err("'__tostring' must return a string")),
9557 };
9558 }
9559 if let Value::Table(t) = v
9560 && let Value::Str(name) = self.get_mm(v, Mm::Name)
9561 {
9562 let mut out = name.as_bytes().to_vec();
9563 out.extend_from_slice(format!(": {:p}", t.as_ptr()).as_bytes());
9564 return Ok(out);
9565 }
9566 Ok(self.tostring_basic(v))
9567 }
9568
9569 /// The dialect's float-rendering flavor (v2.14 HD): ≤5.2 %.14g
9570 /// bare, 5.3/5.4 %.14g + ".0", 5.5 two-stage %.15g/%.17g + ".0".
9571 pub(crate) fn float_fmt(&self) -> numeric::FloatFmt {
9572 use crate::version::LuaVersion::*;
9573 match self.version {
9574 Lua51 | Lua52 => numeric::FloatFmt::Legacy14,
9575 Lua53 | Lua54 => numeric::FloatFmt::G14,
9576 _ => numeric::FloatFmt::TwoStage55,
9577 }
9578 }
9579
9580 /// Basic tostring (no metamethods).
9581 pub(crate) fn tostring_basic(&mut self, v: Value) -> Vec<u8> {
9582 match v {
9583 Value::Nil => b"nil".to_vec(),
9584 Value::Bool(true) => b"true".to_vec(),
9585 Value::Bool(false) => b"false".to_vec(),
9586 Value::Int(i) => numeric::num_to_string(Num::Int(i)).into_bytes(),
9587 // PUC ≤5.2 has no integer subtype — `tostring(2.0)` is `"2"`, not
9588 // `"2.0"`. The 5.3+ split needs the suffix so `print(2.0)` is
9589 // distinguishable from `print(2)`. pm.lua :13 builds patterns by
9590 // concatenating these renderings.
9591 Value::Float(f) => {
9592 numeric::num_to_string_for(Num::Float(f), self.float_fmt()).into_bytes()
9593 }
9594 Value::Str(s) => s.as_bytes().to_vec(),
9595 Value::Table(t) => format!("table: {:p}", t.as_ptr()).into_bytes(),
9596 Value::Closure(c) => format!("function: {:p}", c.as_ptr()).into_bytes(),
9597 Value::Native(n) => format!("function: builtin: {:p}", n.as_ptr()).into_bytes(),
9598 Value::Coro(co) => format!("thread: {:p}", co.as_ptr()).into_bytes(),
9599 // PUC names file handles `file (0x…)`; a bare userdata is
9600 // `userdata: 0x…`. The io library overrides this via __tostring.
9601 Value::Userdata(u) => format!("userdata: {:p}", u.as_ptr()).into_bytes(),
9602 // PUC `lua_topointer`/tostring on light udata: "userdata: 0x…"
9603 // (the "light" qualifier only appears in `luaL_typeerror`).
9604 Value::LightUserdata(p) => format!("userdata: {p:p}").into_bytes(),
9605 }
9606 }
9607}
9608
9609#[derive(Clone, Copy, PartialEq, Eq)]
9610enum ArithOp {
9611 Add,
9612 Sub,
9613 Mul,
9614 Mod,
9615 Pow,
9616 Div,
9617 IDiv,
9618 BAnd,
9619 BOr,
9620 BXor,
9621 Shl,
9622 Shr,
9623}
9624
9625impl ArithOp {
9626 /// PUC metamethod event name (`__add` → "add" etc.) used by
9627 /// `debug.getinfo(level, "n")` inside a metamethod handler.
9628 fn mm_name(self) -> &'static str {
9629 match self {
9630 ArithOp::Add => "add",
9631 ArithOp::Sub => "sub",
9632 ArithOp::Mul => "mul",
9633 ArithOp::Mod => "mod",
9634 ArithOp::Pow => "pow",
9635 ArithOp::Div => "div",
9636 ArithOp::IDiv => "idiv",
9637 ArithOp::BAnd => "band",
9638 ArithOp::BOr => "bor",
9639 ArithOp::BXor => "bxor",
9640 ArithOp::Shl => "shl",
9641 ArithOp::Shr => "shr",
9642 }
9643 }
9644}
9645
9646fn as_num(v: Value) -> Option<Num> {
9647 match v {
9648 Value::Int(i) => Some(Num::Int(i)),
9649 Value::Float(f) => Some(Num::Float(f)),
9650 // PUC forprep coerces numeric strings (`for i = "10", "1", "-2"`).
9651 Value::Str(s) => crate::numeric::str2num(s.as_bytes(), true, true),
9652 _ => None,
9653 }
9654}
9655
9656/// A concatenable operand's byte form (string, or a number coerced to its
9657/// string), or `None` when only a `__concat` metamethod can handle it.
9658/// `legacy_float = true` follows PUC ≤5.2's `%.14g` rendering (no `.0`
9659/// suffix on integer-valued floats) — see `num_to_string_for`.
9660fn concat_piece(v: Value, float_fmt: numeric::FloatFmt) -> Option<Vec<u8>> {
9661 match v {
9662 Value::Str(s) => Some(s.as_bytes().to_vec()),
9663 Value::Int(x) => Some(numeric::num_to_string(Num::Int(x)).into_bytes()),
9664 Value::Float(x) => Some(numeric::num_to_string_for(Num::Float(x), float_fmt).into_bytes()),
9665 _ => None,
9666 }
9667}
9668
9669/// Index into the per-basic-type metatable table for a non-table value
9670/// (None for tables, which carry their own metatable).
9671fn type_mt_slot(v: Value) -> Option<usize> {
9672 match v {
9673 Value::Nil => Some(0),
9674 Value::Bool(_) => Some(1),
9675 Value::Int(_) | Value::Float(_) => Some(2),
9676 Value::Str(_) => Some(3),
9677 Value::Closure(_) | Value::Native(_) => Some(4),
9678 // tables and full userdata carry their own metatable; threads and
9679 // light userdata have none (PUC keeps a shared per-type mt slot for
9680 // light, but luna doesn't expose it — no test gates on it yet).
9681 Value::Table(_) | Value::Coro(_) | Value::Userdata(_) | Value::LightUserdata(_) => None,
9682 }
9683}
9684
9685/// Number, or string coerced to number (5.5 default string-arith coercion).
9686fn coerce_num(v: Value) -> Option<Num> {
9687 match v {
9688 Value::Int(i) => Some(Num::Int(i)),
9689 Value::Float(f) => Some(Num::Float(f)),
9690 Value::Str(s) => numeric::str2num(s.as_bytes(), true, true),
9691 _ => None,
9692 }
9693}
9694
9695/// Lua shifts: logical on 64 bits; |shift| ≥ 64 yields 0; negative shifts
9696/// reverse direction.
9697fn shift_left(a: i64, b: i64) -> i64 {
9698 if b < 0 {
9699 if b <= -64 {
9700 0
9701 } else {
9702 ((a as u64) >> (-b as u32)) as i64
9703 }
9704 } else if b >= 64 {
9705 0
9706 } else {
9707 ((a as u64) << (b as u32)) as i64
9708 }
9709}
9710
9711/// i < f, exactly (PUC LTintfloat shape).
9712fn int_lt_float(i: i64, f: f64) -> bool {
9713 if f.is_nan() {
9714 return false;
9715 }
9716 if f >= 9_223_372_036_854_775_808.0 {
9717 return true;
9718 }
9719 if f < -9_223_372_036_854_775_808.0 {
9720 return false;
9721 }
9722 let ff = f.floor();
9723 let fi = ff as i64;
9724 if f == ff { i < fi } else { i <= fi }
9725}
9726
9727/// i <= f, exactly.
9728fn int_le_float(i: i64, f: f64) -> bool {
9729 if f.is_nan() {
9730 return false;
9731 }
9732 if f >= 9_223_372_036_854_775_808.0 {
9733 return true;
9734 }
9735 if f < -9_223_372_036_854_775_808.0 {
9736 return false;
9737 }
9738 i <= f.floor() as i64
9739}
9740
9741/// Clip a numeric `for` limit to the integer range (PUC forlimit). Returns
9742/// (clipped limit, loop-is-empty).
9743fn int_for_limit(limit: Num, init: i64, step: i64) -> (i64, bool) {
9744 match limit {
9745 Num::Int(l) => {
9746 let empty = if step > 0 { init > l } else { init < l };
9747 (l, empty)
9748 }
9749 Num::Float(f) => {
9750 if f.is_nan() {
9751 return (0, true);
9752 }
9753 if step > 0 {
9754 if f >= 9_223_372_036_854_775_808.0 {
9755 (i64::MAX, false)
9756 } else {
9757 let l = f.floor();
9758 if l < -9_223_372_036_854_775_808.0 {
9759 (i64::MIN, true)
9760 } else {
9761 let li = l as i64;
9762 (li, init > li)
9763 }
9764 }
9765 } else if f <= -9_223_372_036_854_775_808.0 {
9766 (i64::MIN, false)
9767 } else {
9768 let l = f.ceil();
9769 if l >= 9_223_372_036_854_775_808.0 {
9770 // PUC forlimit: a positive limit beyond the integer range
9771 // is unreachable for a decreasing loop — empty.
9772 (i64::MAX, true)
9773 } else {
9774 let li = l as i64;
9775 (li, init < li)
9776 }
9777 }
9778 }
9779 }
9780}
9781
9782/// Strip the load-prefix sigil from a chunk name for messages (PUC keeps
9783/// `@file` / `=name` markers in `source`).
9784fn chunk_display_name(p: *const crate::runtime::LuaStr) -> &'static [u8] {
9785 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
9786 let b = unsafe { crate::runtime::string::bytes_of(p) };
9787 match b.first() {
9788 Some(b'@') | Some(b'=') => &b[1..],
9789 _ => b,
9790 }
9791}
9792
9793impl Vm {
9794 /// Frame introspection for debug.getinfo: `level` 1 = the Lua function
9795 /// that called the current native. Returns (closure, current line,
9796 /// extra vararg count).
9797 /// Name (and kind: local/global/field/upvalue/method/for iterator) of the
9798 /// function running at `level`, recovered from the caller's call
9799 /// instruction (PUC funcnamefromcode). None for the main chunk or a
9800 /// tail/anonymous call with no recoverable name.
9801 /// A debug-level position: either a real Lua frame (by index) or a synthetic
9802 /// C frame standing for a call_value boundary (metamethod / pcall / __close /
9803 /// coroutine body), which `debug.getinfo` and traceback report as "C".
9804 /// PUC lua_getlocal: the `n`-th (1-based) local variable active at the Lua
9805 /// frame at `level`'s current pc, as (name, value). Locals are visited in
9806 /// registration order (start pc, then register) to match luaF_getlocalname.
9807 pub(crate) fn local_at(&self, level: i64, n: i64) -> Option<(String, Value)> {
9808 if n == 0 {
9809 return None;
9810 }
9811 let fi = match self.dbg_frame(level)? {
9812 DbgKind::Lua(fi) => fi,
9813 // Tail-call placeholder has no real frame backing it — no locals
9814 // exist to read or write here. PUC `findlocal` returns NULL on
9815 // a CIST_TAIL activation.
9816 DbgKind::Tail(_) => return None,
9817 // PUC's `luaG_findlocal` on a C activation returns `(C temporary)`
9818 // for slot `n` inside the argument window (db.lua :408-:413, and
9819 // the call/return hook reads of math.sin / select args via
9820 // `getinfo("r")` + `getlocal`). Negative `n` (vararg) is not
9821 // meaningful for a C frame here.
9822 DbgKind::C(fi) => {
9823 if n < 1 {
9824 return None;
9825 }
9826 let (func_slot, nargs) = self.c_frame_native_slots(fi)?;
9827 if (n as u32) > nargs {
9828 return None;
9829 }
9830 let slot = (func_slot + n as u32) as usize;
9831 let val = self.stack.get(slot).copied().unwrap_or(Value::Nil);
9832 return Some((self.temporary_locvar_name().to_string(), val));
9833 }
9834 };
9835 let f = self.frames[fi].lua()?;
9836 // PUC `lua_getlocal` with a negative `n` indexes the varargs: `-1`
9837 // is the first extra arg passed to the function (`...[1]`), `-2` the
9838 // second, etc. The 5.5 stack layout parks varargs in
9839 // [func_slot + 1, base), so the i-th is at `func_slot + i`.
9840 if n < 0 {
9841 let i = (-n) as u32;
9842 if i == 0 || i > f.n_varargs {
9843 return None;
9844 }
9845 let val = self
9846 .stack
9847 .get((f.func_slot + i) as usize)
9848 .copied()
9849 .unwrap_or(Value::Nil);
9850 return Some((self.vararg_locvar_name().to_string(), val));
9851 }
9852 let proto = f.closure.proto;
9853 // PUC's parser injects a hidden `(vararg table)` locvar for an
9854 // anonymous-vararg function (lparser.c new_localvarliteral), sitting
9855 // right after the fixed parameters (`numparams + 1`). Main chunks
9856 // and `(...t)` named-vararg funcs do NOT get one — gate on the
9857 // compiler-set flag, not on `is_vararg`. luna keeps user locals in
9858 // their declared registers (no shadow slot allocated), so we expose
9859 // that hidden index purely in this debug view.
9860 let num_params = proto.num_params as i64;
9861 let vararg_slot = if proto.has_vararg_table_pseudo {
9862 Some(num_params + 1)
9863 } else {
9864 None
9865 };
9866 if vararg_slot == Some(n) {
9867 return Some(("(vararg table)".to_string(), Value::Nil));
9868 }
9869 let pc = (f.pc as usize).saturating_sub(1);
9870 let mut active: Vec<&crate::runtime::LocVar> = proto
9871 .locvars
9872 .iter()
9873 .filter(|lv| (lv.start_pc as usize) <= pc && pc < lv.end_pc as usize)
9874 .collect();
9875 active.sort_by_key(|lv| (lv.start_pc, lv.reg));
9876 let mut idx: i64 = n - 1;
9877 if let Some(vs) = vararg_slot
9878 && n > vs
9879 {
9880 idx -= 1;
9881 }
9882 let idx = idx as usize;
9883 if let Some(lv) = active.get(idx) {
9884 let val = self
9885 .stack
9886 .get((f.base + lv.reg) as usize)
9887 .copied()
9888 .unwrap_or(Value::Nil);
9889 return Some((lv.name.to_string(), val));
9890 }
9891 // PUC `luaG_findlocal` fallback: `n` is past the named locals but
9892 // still inside the frame's live register window — report a
9893 // "(temporary)" (e.g. an arithmetic intermediate). The limit is
9894 // the next frame's func slot (`ci->next->func.p`) so the
9895 // temporary window stops where the callee's frame begins
9896 // (db.lua :416/:417 distinguish a live temporary `(a+1)` from
9897 // an out-of-range slot).
9898 let limit = self
9899 .frames
9900 .get(fi + 1)
9901 .and_then(|cf| cf.lua())
9902 .map(|nf| nf.func_slot)
9903 .unwrap_or_else(|| self.top.max(f.base));
9904 let temp_reg = idx as u32;
9905 if f.base + temp_reg < limit {
9906 let val = self
9907 .stack
9908 .get((f.base + temp_reg) as usize)
9909 .copied()
9910 .unwrap_or(Value::Nil);
9911 return Some((self.lua_temporary_locvar_name().to_string(), val));
9912 }
9913 None
9914 }
9915
9916 /// `debug.setlocal`'s underlying write (PUC `lua_setlocal`). Returns
9917 /// the local / vararg name on success, `None` when the slot does not
9918 /// resolve. Mirrors `local_at`'s indexing exactly.
9919 pub(crate) fn local_set(&mut self, level: i64, n: i64, v: Value) -> Option<String> {
9920 if n == 0 {
9921 return None;
9922 }
9923 let DbgKind::Lua(fi) = self.dbg_frame(level)? else {
9924 return None;
9925 };
9926 let f = self.frames[fi].lua()?;
9927 if n < 0 {
9928 let i = (-n) as u32;
9929 if i == 0 || i > f.n_varargs {
9930 return None;
9931 }
9932 let slot = (f.func_slot + i) as usize;
9933 if let Some(s) = self.stack.get_mut(slot) {
9934 *s = v;
9935 }
9936 return Some(self.vararg_locvar_name().to_string());
9937 }
9938 let proto = f.closure.proto;
9939 let num_params = proto.num_params as i64;
9940 let vararg_slot = if proto.has_vararg_table_pseudo {
9941 Some(num_params + 1)
9942 } else {
9943 None
9944 };
9945 if vararg_slot == Some(n) {
9946 // hidden (vararg table) slot has no real storage — accept the
9947 // write as a no-op for PUC parity (db.lua doesn't write to it).
9948 return Some("(vararg table)".to_string());
9949 }
9950 let pc = (f.pc as usize).saturating_sub(1);
9951 let mut active: Vec<&crate::runtime::LocVar> = proto
9952 .locvars
9953 .iter()
9954 .filter(|lv| (lv.start_pc as usize) <= pc && pc < lv.end_pc as usize)
9955 .collect();
9956 active.sort_by_key(|lv| (lv.start_pc, lv.reg));
9957 let mut idx: i64 = n - 1;
9958 if let Some(vs) = vararg_slot
9959 && n > vs
9960 {
9961 idx -= 1;
9962 }
9963 let idx = idx as usize;
9964 let (name, reg) = if let Some(lv) = active.get(idx) {
9965 (lv.name.to_string(), lv.reg)
9966 } else {
9967 // PUC `luaG_findlocal` fallback into the temporary window —
9968 // bounded by the next frame's func slot (see local_at).
9969 let limit = self
9970 .frames
9971 .get(fi + 1)
9972 .and_then(|cf| cf.lua())
9973 .map(|nf| nf.func_slot)
9974 .unwrap_or_else(|| self.top.max(f.base));
9975 let temp_reg = idx as u32;
9976 if f.base + temp_reg >= limit {
9977 return None;
9978 }
9979 (self.lua_temporary_locvar_name().to_string(), temp_reg)
9980 };
9981 let slot = (f.base + reg) as usize;
9982 if let Some(s) = self.stack.get_mut(slot) {
9983 *s = v;
9984 }
9985 Some(name)
9986 }
9987
9988 /// `debug.getlocal(thread, level, n)`: read frame `level` of the suspended
9989 /// coroutine `co`. Walks `co.frames` (the saved Lua activation stack) and
9990 /// reads from `co.stack`. Returns `None` for out-of-range, for negative
9991 /// vararg indexing past `n_varargs`, or for a register past the live
9992 /// window. Naming follows the same priority as `local_at`: named locals,
9993 /// then `(vararg)` for negative `n`, then `(vararg table)` for the
9994 /// explicit-`(...)` pseudo, else `(temporary)` in the live register
9995 /// window.
9996 pub(crate) fn local_at_coro(
9997 &self,
9998 co: Gc<crate::runtime::Coro>,
9999 level: i64,
10000 n: i64,
10001 ) -> Option<(String, Value)> {
10002 if level < 1 || n == 0 {
10003 return None;
10004 }
10005 let frames = &co.frames;
10006 // Logical level: iterate Lua frames from the top.
10007 let lua_indices: Vec<usize> = (0..frames.len())
10008 .rev()
10009 .filter(|&i| frames[i].lua().is_some())
10010 .collect();
10011 let fi = *lua_indices.get((level - 1) as usize)?;
10012 let f = frames[fi].lua()?;
10013 if n < 0 {
10014 let i = (-n) as u32;
10015 if i == 0 || i > f.n_varargs {
10016 return None;
10017 }
10018 let val = co
10019 .stack
10020 .get((f.func_slot + i) as usize)
10021 .copied()
10022 .unwrap_or(Value::Nil);
10023 return Some((self.vararg_locvar_name().to_string(), val));
10024 }
10025 let proto = f.closure.proto;
10026 let num_params = proto.num_params as i64;
10027 let vararg_slot = if proto.has_vararg_table_pseudo {
10028 Some(num_params + 1)
10029 } else {
10030 None
10031 };
10032 if vararg_slot == Some(n) {
10033 return Some(("(vararg table)".to_string(), Value::Nil));
10034 }
10035 let pc = (f.pc as usize).saturating_sub(1);
10036 let mut active: Vec<&crate::runtime::LocVar> = proto
10037 .locvars
10038 .iter()
10039 .filter(|lv| (lv.start_pc as usize) <= pc && pc < lv.end_pc as usize)
10040 .collect();
10041 active.sort_by_key(|lv| (lv.start_pc, lv.reg));
10042 let mut idx: i64 = n - 1;
10043 if let Some(vs) = vararg_slot
10044 && n > vs
10045 {
10046 idx -= 1;
10047 }
10048 let idx = idx as usize;
10049 if let Some(lv) = active.get(idx) {
10050 let val = co
10051 .stack
10052 .get((f.base + lv.reg) as usize)
10053 .copied()
10054 .unwrap_or(Value::Nil);
10055 return Some((lv.name.to_string(), val));
10056 }
10057 let limit = frames
10058 .get(fi + 1)
10059 .and_then(|cf| cf.lua())
10060 .map(|nf| nf.func_slot)
10061 .unwrap_or(co.top.max(f.base));
10062 let temp_reg = idx as u32;
10063 if f.base + temp_reg < limit {
10064 let val = co
10065 .stack
10066 .get((f.base + temp_reg) as usize)
10067 .copied()
10068 .unwrap_or(Value::Nil);
10069 return Some((self.lua_temporary_locvar_name().to_string(), val));
10070 }
10071 None
10072 }
10073
10074 /// `debug.setlocal(thread, level, n, value)`: write into frame `level` of
10075 /// suspended `co`. Mirrors `local_at_coro`'s indexing exactly.
10076 pub(crate) fn local_set_coro(
10077 &mut self,
10078 co: Gc<crate::runtime::Coro>,
10079 level: i64,
10080 n: i64,
10081 v: Value,
10082 ) -> Option<String> {
10083 if level < 1 || n == 0 {
10084 return None;
10085 }
10086 let lua_indices: Vec<usize> = (0..co.frames.len())
10087 .rev()
10088 .filter(|&i| co.frames[i].lua().is_some())
10089 .collect();
10090 let fi = *lua_indices.get((level - 1) as usize)?;
10091 let (func_slot, n_varargs, base, proto, top_for_temp, next_func_slot) = {
10092 let f = co.frames[fi].lua()?;
10093 (
10094 f.func_slot,
10095 f.n_varargs,
10096 f.base,
10097 f.closure.proto,
10098 co.top.max(f.base),
10099 co.frames
10100 .get(fi + 1)
10101 .and_then(|cf| cf.lua())
10102 .map(|nf| nf.func_slot),
10103 )
10104 };
10105 if n < 0 {
10106 let i = (-n) as u32;
10107 if i == 0 || i > n_varargs {
10108 return None;
10109 }
10110 let slot = (func_slot + i) as usize;
10111 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
10112 let stack = unsafe { &mut co.as_mut().stack };
10113 if let Some(s) = stack.get_mut(slot) {
10114 *s = v;
10115 }
10116 // co.stack values are traced — once-per-call barrier so propagate
10117 // sees the new value if co was already BLACK this cycle.
10118 self.heap
10119 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
10120 return Some(self.vararg_locvar_name().to_string());
10121 }
10122 let num_params = proto.num_params as i64;
10123 let vararg_slot = if proto.has_vararg_table_pseudo {
10124 Some(num_params + 1)
10125 } else {
10126 None
10127 };
10128 if vararg_slot == Some(n) {
10129 return Some("(vararg table)".to_string());
10130 }
10131 let pc = (co.frames[fi].lua().unwrap().pc as usize).saturating_sub(1);
10132 let mut active: Vec<&crate::runtime::LocVar> = proto
10133 .locvars
10134 .iter()
10135 .filter(|lv| (lv.start_pc as usize) <= pc && pc < lv.end_pc as usize)
10136 .collect();
10137 active.sort_by_key(|lv| (lv.start_pc, lv.reg));
10138 let mut idx: i64 = n - 1;
10139 if let Some(vs) = vararg_slot
10140 && n > vs
10141 {
10142 idx -= 1;
10143 }
10144 let idx = idx as usize;
10145 let (name, reg) = if let Some(lv) = active.get(idx) {
10146 (lv.name.to_string(), lv.reg)
10147 } else {
10148 let limit = next_func_slot.unwrap_or(top_for_temp);
10149 let temp_reg = idx as u32;
10150 if base + temp_reg >= limit {
10151 return None;
10152 }
10153 (self.lua_temporary_locvar_name().to_string(), temp_reg)
10154 };
10155 let slot = (base + reg) as usize;
10156 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
10157 let stack = unsafe { &mut co.as_mut().stack };
10158 if let Some(s) = stack.get_mut(slot) {
10159 *s = v;
10160 }
10161 // co.stack values are traced — once-per-call barrier so propagate
10162 // sees the new value if co was already BLACK this cycle.
10163 self.heap
10164 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
10165 Some(name)
10166 }
10167
10168 /// Frame info for a level on a suspended coroutine (PUC
10169 /// `lua_getinfo(L1, "Sl...", &ar)` after `lua_getstack(L1, level, &ar)`).
10170 /// Returns the closure + currentline + extraargs + istailcall for the
10171 /// level-th Lua activation in `co.frames`. None if level overshoots.
10172 pub(crate) fn coro_frame_info(
10173 &self,
10174 co: Gc<crate::runtime::Coro>,
10175 level: i64,
10176 ) -> Option<(Gc<LuaClosure>, u32, i64, bool)> {
10177 if level < 1 {
10178 return None;
10179 }
10180 let lua_indices: Vec<usize> = (0..co.frames.len())
10181 .rev()
10182 .filter(|&i| co.frames[i].lua().is_some())
10183 .collect();
10184 let fi = *lua_indices.get((level - 1) as usize)?;
10185 let f = co.frames[fi].lua()?;
10186 let proto = f.closure.proto;
10187 let pc = (f.pc as usize)
10188 .saturating_sub(1)
10189 .min(proto.lines.len().saturating_sub(1));
10190 let line = proto.lines.get(pc).copied().unwrap_or(0);
10191 Some((f.closure, line, f.n_varargs as i64, f.tailcalls > 0))
10192 }
10193
10194 /// Whether `level` resolves to any live activation (PUC lua_getstack).
10195 pub(crate) fn level_in_range(&self, level: i64) -> bool {
10196 self.dbg_frame(level).is_some()
10197 }
10198
10199 /// PUC's debug-API placeholder for an unnamed vararg slot returned by
10200 /// `debug.getlocal(_, -n)`. 5.2/5.3 spelled it `"(*vararg)"`; 5.4
10201 /// dropped the asterisk in favour of `"(vararg)"`. db.lua 5.2 :189 /
10202 /// 5.3 :195 / 5.4 :286 baseline on their respective form.
10203 pub(crate) fn vararg_locvar_name(&self) -> &'static str {
10204 if matches!(self.version, LuaVersion::Lua52 | LuaVersion::Lua53) {
10205 "(*vararg)"
10206 } else {
10207 "(vararg)"
10208 }
10209 }
10210
10211 /// PUC's debug-API placeholder for an unnamed temporary on a C
10212 /// activation. 5.2/5.3 reported `"(*temporary)"`; 5.4 switched to
10213 /// `"(C temporary)"`. db.lua 5.2 :288, 5.3 :312, 5.4 :404 each pin
10214 /// their spelling.
10215 pub(crate) fn temporary_locvar_name(&self) -> &'static str {
10216 if matches!(
10217 self.version,
10218 LuaVersion::Lua51 | LuaVersion::Lua52 | LuaVersion::Lua53
10219 ) {
10220 // PUC 5.1's `findlocal` C-frame branch reported `(*temporary)`
10221 // (db.lua :228 pins it). 5.2/5.3 kept the spelling, 5.4 changed
10222 // to `(C temporary)`.
10223 "(*temporary)"
10224 } else {
10225 "(C temporary)"
10226 }
10227 }
10228
10229 /// PUC's debug-API placeholder for an unnamed Lua-frame temporary
10230 /// (an arithmetic intermediate sitting past the last named local on a
10231 /// live register slot). 5.2/5.3 reported `"(*temporary)"`; 5.4 dropped
10232 /// the asterisk to `"(temporary)"`. db.lua 5.3 :786, 5.4 :966 pin the
10233 /// spelling.
10234 pub(crate) fn lua_temporary_locvar_name(&self) -> &'static str {
10235 if matches!(
10236 self.version,
10237 LuaVersion::Lua51 | LuaVersion::Lua52 | LuaVersion::Lua53
10238 ) {
10239 "(*temporary)"
10240 } else {
10241 "(temporary)"
10242 }
10243 }
10244
10245 /// The Lua closure running at `level` on the current thread, or `None`
10246 /// when the frame is a synthetic C boundary. PUC 5.1 `getfenv`/`setfenv`
10247 /// need this to reach the function whose env they read or rewrite.
10248 pub(crate) fn lua_closure_at_level(&self, level: i64) -> Option<Gc<LuaClosure>> {
10249 // `DbgKind::Tail` also falls into the else branch — a tail-call
10250 // placeholder has no closure of its own, so PUC's `lua_getstack` +
10251 // `getfunc` for that level returns no function, and `getfenv(level)`
10252 // / `setfenv(level)` raise an error (5.1 db.lua :336/:341).
10253 let DbgKind::Lua(fi) = self.dbg_frame(level)? else {
10254 return None;
10255 };
10256 Some(self.frames[fi].lua()?.closure)
10257 }
10258
10259 pub(crate) fn coro_level_in_range(&self, co: Gc<crate::runtime::Coro>, level: i64) -> bool {
10260 if level < 1 {
10261 return false;
10262 }
10263 let count = co.frames.iter().filter(|cf| cf.lua().is_some()).count();
10264 (level as usize) <= count
10265 }
10266
10267 pub(crate) fn dbg_frame(&self, level: i64) -> Option<DbgKind> {
10268 if level < 1 {
10269 return None;
10270 }
10271 // PUC 5.1's `lua_getstack` walks the full `ci` chain — each C
10272 // activation counts as a level, and each Lua activation's
10273 // `tailcalls` adds an extra synthetic level (CIST_TAIL). 5.2+
10274 // dropped the synthetic shape: `istailcall` becomes a flag on the
10275 // real frame and Cont activations no longer count separately.
10276 // 5.1 db.lua :336-:343 pin the 5.1 shape; 5.2/5.3/5.5 db.lua's
10277 // `getinfo(2).func == g1` pins the 5.2+ shape.
10278 let v51 = self.version <= LuaVersion::Lua51;
10279 let mut lvl = level;
10280 for fi in (0..self.frames.len()).rev() {
10281 match &self.frames[fi] {
10282 CallFrame::Lua(f) => {
10283 lvl -= 1;
10284 if lvl == 0 {
10285 return Some(DbgKind::Lua(fi));
10286 }
10287 if v51 {
10288 // 5.1 reports one synthetic CIST_TAIL level per
10289 // collapsed tail call (PUC `lua_getstack` subtracts
10290 // `ci->u.l.tailcalls` from the remaining level).
10291 for _ in 0..f.tailcalls {
10292 lvl -= 1;
10293 if lvl == 0 {
10294 return Some(DbgKind::Tail(fi));
10295 }
10296 }
10297 }
10298 if f.from_c {
10299 lvl -= 1;
10300 if lvl == 0 {
10301 return Some(DbgKind::C(fi));
10302 }
10303 }
10304 }
10305 CallFrame::Cont(_) => {
10306 if !v51 {
10307 continue;
10308 }
10309 lvl -= 1;
10310 if lvl == 0 {
10311 let parent = (0..fi)
10312 .rev()
10313 .find(|&j| matches!(self.frames[j], CallFrame::Lua(_)));
10314 return Some(DbgKind::C(parent.unwrap_or(fi.saturating_sub(1))));
10315 }
10316 }
10317 }
10318 }
10319 None
10320 }
10321
10322 pub(crate) fn frame_name(&self, fi: usize) -> Option<(&'static str, String)> {
10323 let f = self.frames[fi].lua()?;
10324 // metamethod handler frames carry the event tag (e.g. "close" for
10325 // `__close`); PUC `funcnamefromcall` reads `ci->u.l.tm`.
10326 if f.is_hook {
10327 return Some(("hook", "?".to_string()));
10328 }
10329 if let Some(tm) = f.tm {
10330 return Some(("metamethod", tm_debug_name(self.version, tm)));
10331 }
10332 // a frame entered across a C boundary has no naming call instruction
10333 if fi == 0 || f.from_c {
10334 return None;
10335 }
10336 // the caller's call instruction names this frame; a continuation frame
10337 // just below (pcall/xpcall) is itself a C boundary, so f.from_c above
10338 // already short-circuits those.
10339 let caller = self.frames[fi - 1].lua()?;
10340 let caller_proto = caller.closure.proto;
10341 let p: &crate::runtime::Proto = &caller_proto;
10342 let call_pc = (caller.pc as usize).checked_sub(1)?;
10343 let instr = *p.code.get(call_pc)?;
10344 match instr.op() {
10345 Op::Call | Op::TailCall => crate::vm::objname::getobjname(p, call_pc, instr.a()),
10346 Op::TForCall => Some(("for iterator", "for iterator".to_string())),
10347 _ => None,
10348 }
10349 }
10350
10351 /// Name the synthetic C level sitting below the `from_c` Lua frame at `fi`
10352 /// (PUC names a C function from the call instruction that invoked it). The
10353 /// native was called by the nearest Lua frame below `fi` (skipping pcall/
10354 /// xpcall continuations); that frame's call instruction names it.
10355 pub(crate) fn c_frame_name(&self, fi: usize) -> Option<(&'static str, String)> {
10356 // PUC `GCTM` sets `CIST_FIN` on the calling ci, so when getinfo names
10357 // the synthetic C edge between the __gc finalizer (top Lua frame, has
10358 // `tm = "gc"`) and its triggering Lua frame it reports "metamethod"
10359 // "__gc" — 5.3 db.lua :720's `getinfo(2).namewhat == "metamethod"`
10360 // pin. Restricted to the `__gc` event: `__close` (`tm = "close"`)
10361 // sets the tag on the handler frame only, so level 2 there still
10362 // names the calling Lua frame's call instruction (5.5 locals.lua
10363 // :514 pins `getinfo(2).name == "pcall"` from a __close handler).
10364 if let Some(fr) = self.frames.get(fi).and_then(|cf| cf.lua())
10365 && fr.tm == Some("gc")
10366 {
10367 let name = tm_debug_name(self.version, "gc");
10368 return Some(("metamethod", name));
10369 }
10370 let caller_fi = (0..fi).rev().find(|&i| self.frames[i].lua().is_some())?;
10371 let caller = self.frames[caller_fi].lua()?;
10372 let p = &caller.closure.proto;
10373 let call_pc = (caller.pc as usize).checked_sub(1)?;
10374 let instr = *p.code.get(call_pc)?;
10375 match instr.op() {
10376 Op::Call | Op::TailCall => crate::vm::objname::getobjname(p, call_pc, instr.a()),
10377 _ => None,
10378 }
10379 }
10380
10381 /// Native value currently sitting on the synthetic C edge identified by
10382 /// `DbgKind::C(fi)`. The walk counts how many `from_c` Lua frames live
10383 /// above `fi` (each one corresponds to one native pushing the hook) and
10384 /// indexes into `running_natives` from the top, also skipping the caller
10385 /// of `getinfo` itself (the native that is currently asking).
10386 /// db.lua :344 reads `debug.getinfo(2, "f").func` from a call hook and
10387 /// expects the just-entered C function.
10388 pub(crate) fn c_frame_func(&self, fi: usize) -> Option<Value> {
10389 let idx = self.c_frame_native_idx(fi)?;
10390 Some(Value::Native(self.running_natives[idx]))
10391 }
10392
10393 /// `(func_slot, nargs)` for the synthetic C edge identified by `C(fi)`,
10394 /// so `local_at` can index the native's argument window like PUC's
10395 /// `(C temporary)` path. Returns `None` when no matching native exists
10396 /// (e.g. the C edge corresponds to a non-native boundary).
10397 pub(crate) fn c_frame_native_slots(&self, fi: usize) -> Option<(u32, u32)> {
10398 let idx = self.c_frame_native_idx(fi)?;
10399 self.running_native_slots.get(idx).copied()
10400 }
10401
10402 fn c_frame_native_idx(&self, fi: usize) -> Option<usize> {
10403 let n_above = self.frames[fi..]
10404 .iter()
10405 .filter_map(CallFrame::lua)
10406 .filter(|f| f.from_c)
10407 .count();
10408 if n_above == 0 {
10409 return None;
10410 }
10411 // running_natives.last() is the native currently executing (the one
10412 // that called getinfo). Pop it conceptually, then take the n_above-th
10413 // entry from the top of what remains.
10414 let nr = self.running_natives.len().checked_sub(1)?;
10415 nr.checked_sub(n_above)
10416 }
10417
10418 /// PUC `pushglobalfuncname`: walk `package.loaded` to depth 2 looking for a
10419 /// native whose function pointer matches `target`, and return its qualified
10420 /// name (e.g. `"table.sort"`). A `_G.X` match is stripped to `"X"`. Returns
10421 /// `None` if no match is found. Used by `arg_error` when the running native
10422 /// was invoked from another native (PUC `ar.name == NULL` at level 0).
10423 /// True when the innermost call frame is a pcall/xpcall
10424 /// continuation — i.e. the currently-running native was invoked
10425 /// DIRECTLY by pcall/xpcall rather than by Lua code. PUC's
10426 /// luaL_argerror sees ar.name == NULL there (the caller is C)
10427 /// and qualifies the name via pushglobalfuncname — so
10428 /// `pcall(coroutine.resume, 42)` blames 'coroutine.resume'
10429 /// (v2.14 fixture 5.5/365).
10430 pub(crate) fn caller_is_protected_cont(&self) -> bool {
10431 matches!(
10432 self.frames.last(),
10433 Some(CallFrame::Cont(nc))
10434 if matches!(nc.kind, ContKind::Pcall | ContKind::Xpcall { .. })
10435 )
10436 }
10437
10438 pub(crate) fn pushglobalfuncname(
10439 &mut self,
10440 target: crate::runtime::value::NativeFn,
10441 ) -> Option<String> {
10442 let pkg_k = Value::Str(self.heap.intern(b"package"));
10443 let pkg = match self.globals().get(pkg_k) {
10444 Value::Table(t) => t,
10445 _ => return None,
10446 };
10447 let loaded_k = Value::Str(self.heap.intern(b"loaded"));
10448 let loaded = match pkg.get(loaded_k) {
10449 Value::Table(t) => t,
10450 _ => return None,
10451 };
10452 let matches = |v: Value| -> bool {
10453 matches!(v, Value::Native(nc) if std::ptr::fn_addr_eq(nc.f, target))
10454 };
10455 let mut k = Value::Nil;
10456 while let Ok(Some((nk, nv))) = loaded.next(k) {
10457 k = nk;
10458 let Value::Str(outer) = nk else { continue };
10459 let outer = String::from_utf8_lossy(outer.as_bytes()).into_owned();
10460 if matches(nv) {
10461 return Some(if outer == "_G" { String::new() } else { outer });
10462 }
10463 if let Value::Table(inner_t) = nv {
10464 let mut k2 = Value::Nil;
10465 while let Ok(Some((nk2, nv2))) = inner_t.next(k2) {
10466 k2 = nk2;
10467 if matches(nv2)
10468 && let Value::Str(inner) = nk2
10469 {
10470 let inner = String::from_utf8_lossy(inner.as_bytes()).into_owned();
10471 return Some(if outer == "_G" {
10472 inner
10473 } else {
10474 format!("{outer}.{inner}")
10475 });
10476 }
10477 }
10478 }
10479 }
10480 None
10481 }
10482
10483 /// Name and namewhat of the native currently running on behalf of the top
10484 /// Lua frame's call instruction (PUC `lua_getinfo("n")` at level 0). Lets
10485 /// `luaL_argerror` rewrite a method call's self-argument error.
10486 pub(crate) fn running_call_name(&self) -> Option<(&'static str, String)> {
10487 let caller = self.frames.iter().rev().find_map(CallFrame::lua)?;
10488 let p = &caller.closure.proto;
10489 let call_pc = (caller.pc as usize).checked_sub(1)?;
10490 let instr = *p.code.get(call_pc)?;
10491 match instr.op() {
10492 Op::Call | Op::TailCall => crate::vm::objname::getobjname(p, call_pc, instr.a()),
10493 _ => None,
10494 }
10495 }
10496
10497 pub(crate) fn frame_info(&mut self, fi: usize) -> (Gc<LuaClosure>, u32, i64, bool) {
10498 let f = self.frames[fi].lua().expect("Lua frame");
10499 let proto = f.closure.proto;
10500 let pc = (f.pc as usize)
10501 .saturating_sub(1)
10502 .min(proto.lines.len().saturating_sub(1));
10503 let line = proto.lines.get(pc).copied().unwrap_or(0);
10504 // PUC CallInfo.nextraargs: the original extra-arg count, fixed at call
10505 // (independent of any later write to a materialized vararg table's `n`).
10506 // `istailcall` mirrors PUC `CIST_TAIL` for `debug.getinfo(_, "t")` —
10507 // any nonzero `tailcalls` count flips it true.
10508 (f.closure, line, f.n_varargs as i64, f.tailcalls > 0)
10509 }
10510
10511 /// Read an upvalue cell of a closure (debug.getupvalue).
10512 pub(crate) fn upvalue_value(&self, cl: Gc<LuaClosure>, idx: usize) -> Value {
10513 match cl.upvals()[idx].state() {
10514 UpvalState::Open { slot, thread } => self.read_slot(slot, thread),
10515 UpvalState::Closed(v) => v,
10516 }
10517 }
10518
10519 /// Write an upvalue cell of a closure (debug.setupvalue).
10520 pub(crate) fn upvalue_set_value(&mut self, cl: Gc<LuaClosure>, idx: usize, v: Value) {
10521 let uv = cl.upvals()[idx];
10522 match uv.state() {
10523 UpvalState::Open { slot, thread } => self.write_slot(slot, thread, v),
10524 UpvalState::Closed(_) => {
10525 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
10526 unsafe { uv.as_mut() }.set_closed(v);
10527 self.heap
10528 .barrier_forward(uv.as_ptr() as *mut crate::runtime::heap::GcHeader, v);
10529 }
10530 }
10531 }
10532
10533 /// Lines for debug.traceback (PUC `luaL_traceback` / `pushfuncname`).
10534 /// Per Lua frame, emits `"\n\t<src>:<line>: in <funcname>"` where
10535 /// `<funcname>` is, in priority order: `"metamethod 'event'"` if the frame
10536 /// is a metamethod handler (e.g. `__close`); else `"<namewhat> '<name>'"`
10537 /// from the caller's call instruction (`getobjname`); else `"main chunk"`;
10538 /// else `"function <src:line_defined>"` for an anonymous Lua function.
10539 /// Traceback of a suspended coroutine (PUC `debug.traceback(L1, msg, lvl)`).
10540 /// Walks the coroutine's saved frames and prepends a synthetic C-level
10541 /// `'yield'` entry when the coroutine paused at a `coroutine.yield` call
10542 /// (its `resume_at` marker is set). `level` skips entries from the top
10543 /// (level 0 includes the yield frame; level 1 starts at the deepest Lua
10544 /// frame; etc.). db.lua :764-:768 sample several levels.
10545 pub(crate) fn coro_traceback(&self, co: Gc<crate::runtime::Coro>, mut level: i64) -> Vec<u8> {
10546 use crate::runtime::CoroStatus;
10547 const LEVELS1: usize = 10;
10548 const LEVELS2: usize = 11;
10549 #[derive(Clone, Copy)]
10550 enum VFrame<'a> {
10551 Lua(&'a crate::runtime::function::Frame),
10552 CPcall,
10553 CXpcall,
10554 CYield,
10555 /// Synthetic CIST_TAIL placeholder under 5.1 — one per tail
10556 /// call collapsed into the next Lua frame down the chain.
10557 Tail,
10558 }
10559 let v51 = self.version <= LuaVersion::Lua51;
10560 let mut visible: Vec<VFrame<'_>> = Vec::new();
10561 // PUC's level 0 entry on a suspended coroutine is the C call where it
10562 // paused — `coroutine.yield` for a yielded thread.
10563 if matches!(co.status, CoroStatus::Suspended) && co.resume_at.is_some() {
10564 visible.push(VFrame::CYield);
10565 }
10566 for cf in co.frames.iter().rev() {
10567 match cf {
10568 CallFrame::Lua(f) => {
10569 visible.push(VFrame::Lua(f));
10570 if v51 {
10571 for _ in 0..f.tailcalls {
10572 visible.push(VFrame::Tail);
10573 }
10574 }
10575 }
10576 CallFrame::Cont(nc) => match nc.kind {
10577 ContKind::Pcall => visible.push(VFrame::CPcall),
10578 ContKind::Xpcall { .. } => visible.push(VFrame::CXpcall),
10579 _ => {}
10580 },
10581 }
10582 }
10583 if level < 0 {
10584 level = 0;
10585 }
10586 if (level as usize) >= visible.len() {
10587 return Vec::new();
10588 }
10589 let visible = &visible[level as usize..];
10590 let total = visible.len();
10591 let mut out = Vec::new();
10592 // To name a Lua frame, PUC consults the caller's OP_CALL via
10593 // getobjname: find the index `fi` of the current frame in co.frames,
10594 // then look at frames[fi-1] (the caller) and read its `code[pc-1]`.
10595 let coro_frame_name = |frames: &[CallFrame],
10596 target: &crate::runtime::function::Frame|
10597 -> Option<(&'static str, String)> {
10598 let fi = frames
10599 .iter()
10600 .position(|cf| matches!(cf, CallFrame::Lua(f) if std::ptr::eq(f, target)))?;
10601 if fi == 0 || target.from_c {
10602 return None;
10603 }
10604 let caller = frames[fi - 1].lua()?;
10605 let p = &caller.closure.proto;
10606 let call_pc = (caller.pc as usize).checked_sub(1)?;
10607 let instr = *p.code.get(call_pc)?;
10608 match instr.op() {
10609 Op::Call | Op::TailCall => crate::vm::objname::getobjname(p, call_pc, instr.a()),
10610 Op::TForCall => Some(("for iterator", "for iterator".to_string())),
10611 _ => None,
10612 }
10613 };
10614 let frames = &co.frames;
10615 let emit = |out: &mut Vec<u8>, v: VFrame<'_>| match v {
10616 VFrame::Lua(f) => {
10617 let proto = f.closure.proto;
10618 let src = chunk_display_name(proto.source.as_ptr());
10619 let pc = (f.pc as usize)
10620 .saturating_sub(1)
10621 .min(proto.lines.len().saturating_sub(1));
10622 let line = proto.lines.get(pc).copied().unwrap_or(0);
10623 out.extend_from_slice(b"\n\t");
10624 out.extend_from_slice(src);
10625 out.extend_from_slice(format!(":{line}: in ").as_bytes());
10626 if let Some((namewhat, name)) = coro_frame_name(frames, f) {
10627 out.extend_from_slice(format!("{namewhat} '{name}'").as_bytes());
10628 } else if proto.line_defined == 0 {
10629 out.extend_from_slice(b"main chunk");
10630 } else {
10631 out.extend_from_slice(
10632 format!(
10633 "function <{}:{}>",
10634 String::from_utf8_lossy(src),
10635 proto.line_defined
10636 )
10637 .as_bytes(),
10638 );
10639 }
10640 }
10641 VFrame::CPcall => out.extend_from_slice(b"\n\t[C]: in function 'pcall'"),
10642 VFrame::CXpcall => out.extend_from_slice(b"\n\t[C]: in function 'xpcall'"),
10643 VFrame::CYield => {
10644 // PUC `pushglobalfuncname` reports `yield` as
10645 // `'coroutine.yield'` under 5.3 and 5.4 (5.3 :566 / 5.4 :830
10646 // `checktraceback` baselines). 5.1/5.2/5.5 emit the bare
10647 // `'yield'` (5.5 :841).
10648 let qualified = matches!(self.version, LuaVersion::Lua53 | LuaVersion::Lua54);
10649 if qualified {
10650 out.extend_from_slice(b"\n\t[C]: in function 'coroutine.yield'");
10651 } else {
10652 out.extend_from_slice(b"\n\t[C]: in function 'yield'");
10653 }
10654 }
10655 VFrame::Tail => {
10656 // 5.1 traceback synthetic CIST_TAIL entry — luaG_addinfo
10657 // / luaO_chunkid format: `(...tail calls...)`. 5.1 db.lua
10658 // :403 asserts these appear once per collapsed tail call.
10659 out.extend_from_slice(b"\n\t(...tail calls...)");
10660 }
10661 };
10662 if total <= LEVELS1 + LEVELS2 {
10663 for &v in visible {
10664 emit(&mut out, v);
10665 }
10666 } else {
10667 for &v in &visible[..LEVELS1] {
10668 emit(&mut out, v);
10669 }
10670 let skip = total - LEVELS1 - LEVELS2;
10671 out.extend_from_slice(format!("\n\t...\t(skipping {skip} levels)").as_bytes());
10672 for &v in &visible[total - LEVELS2..] {
10673 emit(&mut out, v);
10674 }
10675 }
10676 out
10677 }
10678
10679 pub(crate) fn traceback_bytes(&self, level: i64) -> Vec<u8> {
10680 // PUC `luaL_traceback` shows up to LEVELS1 (10) top frames + LEVELS2
10681 // (11) bottom frames; if there are more, the middle is collapsed into
10682 // a `"...\t(skipping N levels)"` marker. Without this, a stack-
10683 // overflow traceback would balloon to tens of megabytes (errors.lua's
10684 // stack-overflow test ran string.gmatch over the resulting buffer).
10685 const LEVELS1: usize = 10;
10686 const LEVELS2: usize = 11;
10687 // Collect visible frames in top-down order (deepest first). Both Lua
10688 // activations and pcall/xpcall continuations (which stand in for a
10689 // C-level pcall on the stack) are visible; PUC's traceback enumerates
10690 // both via lua_getstack. db.lua :715 expects "pcall" to appear.
10691 #[derive(Clone, Copy)]
10692 enum VFrame {
10693 Lua(usize),
10694 CPcall,
10695 CXpcall,
10696 }
10697 let mut visible: Vec<VFrame> = Vec::new();
10698 for (fi, cf) in self.frames.iter().enumerate().rev() {
10699 match cf {
10700 CallFrame::Lua(_) => visible.push(VFrame::Lua(fi)),
10701 CallFrame::Cont(nc) => match nc.kind {
10702 ContKind::Pcall => visible.push(VFrame::CPcall),
10703 ContKind::Xpcall { .. } => visible.push(VFrame::CXpcall),
10704 _ => {}
10705 },
10706 }
10707 }
10708 // PUC `luaL_traceback` starts enumerating at the given `level` (in
10709 // terms of L1's CallInfo chain). For the running-thread case the C
10710 // frame for debug.traceback itself is level 0 and luna's `visible`
10711 // doesn't include it — so level=1 (PUC default) means "emit from the
10712 // innermost Lua frame" (visible[0..]); level=k skips k-1 frames from
10713 // the top. level<=0 emits nothing extra here (d_traceback handles the
10714 // "[C]: in function 'traceback'" prefix for level==0 separately).
10715 let skip = (level - 1).max(0) as usize;
10716 if skip >= visible.len() {
10717 return Vec::new();
10718 }
10719 let visible = &visible[skip..];
10720 let total = visible.len();
10721 let mut out = Vec::new();
10722 let emit_frame = |out: &mut Vec<u8>, v: VFrame, this: &Vm| match v {
10723 VFrame::Lua(fi) => {
10724 let f = this.frames[fi].lua().expect("Lua frame");
10725 let proto = f.closure.proto;
10726 let src = chunk_display_name(proto.source.as_ptr());
10727 let pc = (f.pc as usize)
10728 .saturating_sub(1)
10729 .min(proto.lines.len().saturating_sub(1));
10730 let line = proto.lines.get(pc).copied().unwrap_or(0);
10731 out.extend_from_slice(b"\n\t");
10732 out.extend_from_slice(src);
10733 out.extend_from_slice(format!(":{line}: in ").as_bytes());
10734 if let Some((namewhat, name)) = this.frame_name(fi) {
10735 out.extend_from_slice(format!("{namewhat} '{name}'").as_bytes());
10736 } else if proto.line_defined == 0 {
10737 out.extend_from_slice(b"main chunk");
10738 } else {
10739 out.extend_from_slice(
10740 format!(
10741 "function <{}:{}>",
10742 String::from_utf8_lossy(src),
10743 proto.line_defined
10744 )
10745 .as_bytes(),
10746 );
10747 }
10748 }
10749 VFrame::CPcall => out.extend_from_slice(b"\n\t[C]: in function 'pcall'"),
10750 VFrame::CXpcall => out.extend_from_slice(b"\n\t[C]: in function 'xpcall'"),
10751 };
10752 if total <= LEVELS1 + LEVELS2 {
10753 for &v in visible {
10754 emit_frame(&mut out, v, self);
10755 }
10756 } else {
10757 for &v in &visible[..LEVELS1] {
10758 emit_frame(&mut out, v, self);
10759 }
10760 let dropped = total - LEVELS1 - LEVELS2;
10761 out.extend_from_slice(format!("\n\t...\t(skipping {dropped} levels)").as_bytes());
10762 for &v in &visible[total - LEVELS2..] {
10763 emit_frame(&mut out, v, self);
10764 }
10765 }
10766 out
10767 }
10768}
10769
10770// ────────────────────────────────────────────────────────────────────
10771// v1.3 Phase AOT Stage 7 sub-piece 4 — AOT trace dispatch install.
10772//
10773// The deploy-side resolver in `luna-runtime-helpers` walks the binary's
10774// trace-meta section after `vm.load`, resolves each entry's
10775// `(proto_hash, head_pc, fn_ptr)` triple against the loaded chunk's
10776// proto tree, and pushes a `CompiledTrace` onto the matching Proto's
10777// `traces` Vec via [`Vm::install_aot_trace`] below. The existing
10778// trace-dispatch loop (this file's `cl.proto.traces.borrow().iter()
10779// .find(|t| t.head_pc == pc && t.dispatchable)`) then fires the AOT
10780// mcode without further plumbing — same code path the runtime JIT
10781// uses.
10782//
10783// Why a separate impl block: keeps the AOT API surface (one fn) easy
10784// to locate when grep'ing for `install_aot_trace`, without dragging
10785// the 8500-line `impl Vm` block above.
10786// ────────────────────────────────────────────────────────────────────
10787
10788impl Vm {
10789 /// v1.3 Phase AOT Stage 7 sub-piece 4 — install a precompiled
10790 /// `CompiledTrace` onto `proto.traces` so the interp dispatcher
10791 /// fires it at the trace's `head_pc`. This is the runtime install
10792 /// API the deploy-side `luna-runtime-helpers` resolver calls once
10793 /// per AOT-emitted trace meta entry, after looking up `proto` by
10794 /// stable hash (see `crate::runtime::function::Proto::stable_hash`).
10795 ///
10796 /// # What this does
10797 ///
10798 /// Pushes `trace` onto `proto.traces` via the existing `RefCell`.
10799 /// The trace's `entry` fn ptr must already point at runnable
10800 /// machine code (the AOT linker resolved the symbol at link time;
10801 /// the deploy resolver passes the address verbatim).
10802 ///
10803 /// # What this does NOT do
10804 ///
10805 /// - **No deduplication.** Calling twice with the same `head_pc`
10806 /// pushes two entries; the dispatcher's `find` will pick the
10807 /// first match. The deploy resolver is responsible for not
10808 /// double-installing.
10809 /// - **No invalidation of the runtime JIT cache.** If the runtime
10810 /// JIT later records + compiles a trace for the same
10811 /// `(proto, head_pc)`, both coexist on `proto.traces` and the
10812 /// dispatcher's `find` picks whichever appears first. AOT
10813 /// traces install before any runtime recording is possible
10814 /// (resolver runs before `vm.load` returns its first closure),
10815 /// so AOT traces win the race for the same site.
10816 /// - **No coverage gating.** AOT traces are trusted by
10817 /// construction — they were validated at compile time. Setting
10818 /// `dispatchable: false` on the input would silently disable
10819 /// dispatch; the caller controls that flag.
10820 ///
10821 /// # Safety / soundness
10822 ///
10823 /// `trace.entry` is an `unsafe extern "C" fn` (mmap'd or linked
10824 /// machine code). Soundness contract:
10825 ///
10826 /// - The fn pointer must remain valid for the `Vm`'s lifetime.
10827 /// In the AOT-binary deploy shape this is trivially satisfied —
10828 /// the fn lives in the binary's `.text`.
10829 /// - `trace.entry_tags` / `exit_tags` / `window_size` must match
10830 /// what the trace's IR actually compiled against; the dispatcher
10831 /// uses them to marshal `reg_state` in and out without further
10832 /// validation. A mismatch corrupts vm.stack.
10833 ///
10834 /// The AOT pipeline (`luna-aot`) is responsible for ensuring these
10835 /// invariants hold; this fn is a plain push — no validation that
10836 /// would slow the dispatcher's hot path either.
10837 pub fn install_aot_trace(
10838 &mut self,
10839 proto: crate::runtime::Gc<crate::runtime::function::Proto>,
10840 trace: crate::jit::trace::CompiledTrace,
10841 ) {
10842 let _ = self; // resolver passes &mut Vm for symmetry with future
10843 // pending-install + hash-walk variants; nothing on `self` to
10844 // mutate today because the install target lives on the Proto.
10845 proto.traces.borrow_mut().push(TArc::new(trace));
10846 }
10847
10848 /// v1.3 Phase AOT Stage 7 sub-piece 4 — walk the proto tree
10849 /// reachable from `root` and return `(proto, stable_hash)` pairs
10850 /// for every Proto found. Used by the deploy-side resolver to
10851 /// match AOT-emitted `proto_hash` keys against the freshly
10852 /// `undump`'d chunk's protos.
10853 ///
10854 /// The walk is BFS over `Proto.protos`. Same-Proto deduplication
10855 /// is done via `Gc::as_ptr` identity — a Proto re-referenced from
10856 /// multiple nested closures (rare; the cache field would catch
10857 /// the closure-side dedup, not the Proto side) is reported once.
10858 ///
10859 /// # Why on `&Vm` and not a free fn
10860 ///
10861 /// Keeps the AOT install API discoverable on the Vm surface —
10862 /// `vm.collect_proto_hashes(root)` reads naturally next to
10863 /// `vm.install_aot_trace(proto, trace)`. Doesn't actually touch
10864 /// any Vm field, so `&self` (read-only) is enough.
10865 pub fn collect_proto_hashes(
10866 &self,
10867 root: crate::runtime::Gc<crate::runtime::function::Proto>,
10868 ) -> Vec<(
10869 crate::runtime::Gc<crate::runtime::function::Proto>,
10870 [u8; 16],
10871 )> {
10872 let _ = self;
10873 let mut out = Vec::new();
10874 let mut seen: std::collections::HashSet<*const crate::runtime::function::Proto> =
10875 std::collections::HashSet::new();
10876 let mut queue: std::collections::VecDeque<
10877 crate::runtime::Gc<crate::runtime::function::Proto>,
10878 > = std::collections::VecDeque::new();
10879 queue.push_back(root);
10880 while let Some(p) = queue.pop_front() {
10881 let key = p.as_ptr() as *const _;
10882 if !seen.insert(key) {
10883 continue;
10884 }
10885 out.push((p, p.stable_hash()));
10886 for &child in p.protos.iter() {
10887 queue.push_back(child);
10888 }
10889 }
10890 out
10891 }
10892}