luna_core/vm/exec.rs
1//! The interpreter. Dispatch is a plain match over opcodes. Lua→Lua calls
2//! share one loop and never recurse the Rust stack; only native↔Lua boundaries do (e.g. pcall).
3//!
4//! Varargs follow 5.5 semantics: a vararg call materializes a vararg table
5//! (fields 1..n plus "n") kept in the function's own stack slot; `...`
6//! expands from it and `...name` binds it. 5.1 LUAI_COMPAT_VARARG also
7//! materializes a local `arg` table (see `proto.has_compat_vararg_arg`).
8
9use crate::frontend::SyntaxError;
10use crate::jit::send_compat::TArc;
11use crate::numeric::{self, Num};
12use crate::runtime::heap::GcHeader;
13use crate::runtime::{
14 AfterClose, CallFrame, CloseCont, ContKind, Coro, CoroStatus, Frame, Gc, Heap, LuaClosure,
15 MetaAction, MetaCont, NativeClosure, NativeCont, Table, TableError, UpvalState, Upvalue, Value,
16};
17use crate::version::LuaVersion;
18use crate::vm::builtins::{nat_host_xpcall, nat_pairs, nat_pcall, nat_xpcall};
19use crate::vm::callstack::DbgKind;
20use crate::vm::error::LuaError;
21use crate::vm::isa::{Inst, Op};
22
23/// A Lua virtual machine: one OS thread's worth of Lua state.
24///
25/// # Threading model
26///
27/// `Vm` is **`!Send + !Sync`**. The GC uses `Gc<T> = NonNull<T>` over
28/// an intrusive mark-sweep heap (not `Rc<RefCell<T>>`), and the trace
29/// JIT side-table uses `Rc<CompiledTrace>` — both single-threaded by
30/// design. Embedders that want concurrency spawn one `Vm` per OS
31/// thread (or per single-thread Tokio worker) and exchange data via
32/// channels. See [`docs/threading.md`](../../docs/threading.md) for
33/// canonical embedding patterns including Tokio `current_thread`,
34/// `LocalSet` on multi-thread, and `Vm`-per-OS-thread + channels.
35///
36/// The constraint is enforced at compile time:
37///
38/// ```compile_fail
39/// fn must_be_send<T: Send>() {}
40/// must_be_send::<luna_core::Vm>(); // error[E0277]: `Vm` cannot be sent between threads safely
41/// ```
42///
43/// A future `feature = "send"` will gate an
44/// opt-in `Arc<RwLock<T>>` mode with a hard ≤8% perf regression
45/// budget.
46pub struct Vm {
47 /// The GC heap owned by this VM. Embedders normally interact via the
48 /// `Vm` methods (`load` / `call_value` / `set_global` / …) rather than
49 /// the heap directly.
50 pub heap: Heap,
51 pub(crate) stack: Vec<Value>,
52 pub(crate) frames: Vec<CallFrame>,
53 /// How many `__call` metamethods were resolved to reach each Lua frame,
54 /// by index into `frames`; each adds one argument (PUC 5.5 `CIST_CCMT`
55 /// bits, reported as `getinfo("t").extraargs`). Written whenever a Lua
56 /// frame is pushed, so the entry for a live frame is always its own;
57 /// entries past `frames.len()` are stale and never read. Kept beside
58 /// `frames` rather than in `Frame` because `Frame` is public with all
59 /// fields public, where a new field is a breaking change.
60 pub(crate) frame_ccmt: Vec<u8>,
61 /// Shadow of `self.frames.len()`. Synced on every push/pop in the
62 /// `frames_push_sync`/`frames_pop_sync` helpers (debug-asserted on
63 /// use). Not consumed by readers yet; it is scaffolding for replacing
64 /// `frames: Vec<CallFrame>` with a flat `[CallFrame; MAX_FRAMES]`
65 /// indexed by frames_top.
66 frames_top: u32,
67 /// open upvalues, sorted ascending by stack slot
68 open_upvals: Vec<(u32, Gc<Upvalue>)>,
69 /// to-be-closed slots, ascending
70 tbc: Vec<u32>,
71 /// logical stack top for multi-result sequences
72 pub(crate) top: u32,
73 globals: Gc<Table>,
74 /// shared metatable for all strings (populated by the string lib)
75 /// per-basic-type metatables (PUC luaT): indexed by `type_mt_slot`
76 /// (0 nil, 1 boolean, 2 number, 3 string, 4 function); tables carry their
77 /// own. Settable via debug.setmetatable.
78 type_mt: [Option<Gc<Table>>; 5],
79 /// pre-interned metamethod event names, indexed by `Mm`
80 mm_names: Vec<Gc<crate::runtime::LuaStr>>,
81 /// native↔Lua nesting depth (PUC C-stack guard analogue)
82 c_depth: u32,
83 /// number of live pcall/xpcall continuation frames on the running thread
84 /// (PUC counts these against nCcalls). Bounds protected-call recursion the
85 /// way `c_depth` bounds call_value recursion. Per-thread: saved/restored
86 /// with the coroutine context, since continuations survive a yield.
87 pcall_depth: u32,
88 /// number of non-yieldable C calls in flight on the running thread (PUC's
89 /// `L->nny`). A library callback that runs via synchronous Rust recursion
90 /// (sort comparator, gsub replacement) cannot be continued across a yield,
91 /// so it bumps this for its duration; `coroutine.yield` inside hits the
92 /// C-call boundary and errors. Always 0 at a suspend point (a yield can
93 /// never cross such a call), so it needs no per-thread save/restore.
94 nny: u32,
95 /// Nonzero while an xpcall message handler is on the Rust stack. Used so a
96 /// stack-overflow that surfaces *inside* the handler is reported as PUC's
97 /// "error in error handling" (LUA_ERRERR + `luaD_seterrorobj`), not the
98 /// plain "stack overflow" — errors.lua :606's `checkerr("error handling",
99 /// loop)` then matches. PUC tracks this via the soft-cap window
100 /// `nCcalls >= MAXCCALLS/10*11`; luna's c_depth is strict, so we mark the
101 /// scope explicitly.
102 pub(crate) msgh_depth: u32,
103 /// set by a coroutine closing itself (`coroutine.close()` on the running
104 /// thread): the to-be-closed handlers have already run; the thread must now
105 /// terminate. `Some(None)` is a clean close, `Some(Some(e))` a handler
106 /// raised `e`. Checked by `exec_with`/`resume_coro` to propagate (not
107 /// unwind, so a protecting pcall cannot catch it) the termination.
108 terminating: Option<Option<Value>>,
109 /// xoshiro256** state (math.random)
110 rng: [u64; 4],
111 /// VM creation time (os.clock)
112 started: std::time::Instant,
113 version: LuaVersion,
114 /// error object being threaded through a chain of __close handlers; a GC
115 /// root for the duration (a handler may trigger collection)
116 closing_err: Option<Value>,
117 /// the coroutine whose context is currently live in the fields above;
118 /// `None` while the main thread runs
119 pub(crate) current: Option<Gc<crate::runtime::Coro>>,
120 /// the main thread's saved execution context while a coroutine runs
121 main_ctx: Option<SavedCtx>,
122 /// set by `coroutine.yield` to suspend the running coroutine: the yielded
123 /// values plus the slot/result-count needed to finish the yielding call on
124 /// the next resume. Checked by `exec` to propagate (not unwind) on yield.
125 yielding: Option<(Vec<Value>, u32, i32)>,
126 /// results expected by the in-flight native call (so `yield` knows how many
127 /// values its call site wants when it suspends)
128 native_nresults: i32,
129 /// identity object for the main thread, returned by `coroutine.running`
130 /// (the main thread's context lives in the VM fields / `main_ctx`, not here)
131 main_coro: Option<Gc<Coro>>,
132 /// `collectgarbage` mode name ("incremental"/"generational"). The collector
133 /// itself is still stop-the-world mark-sweep; this tracks the mode so mode
134 /// switches report the previous one, as PUC does.
135 gc_mode: &'static str,
136 /// the live-register boundary of the running thread for GC rooting (PUC's
137 /// `L->top`): set precisely at each GC safe point so freed temporary
138 /// registers above it are not rooted. Without this the collector roots the
139 /// whole stack window, pinning weak-table values stranded in stale temps
140 /// (e.g. closure.lua's `while x[1]` GC-detection loop).
141 pub(crate) gc_top: u32,
142 /// `collectgarbage("param", name [,value])` pacing parameters. The collector
143 /// is still stop-the-world, so these are stored/returned for API fidelity
144 /// (PUC round-trips them via `setparam`/`getparam`). Defaults mirror PUC's
145 /// `LUAI_GC*` knobs: pause=200, stepmul=100, stepsize=13.
146 gc_pause: i64,
147 gc_stepmul: i64,
148 gc_stepsize: i64,
149 /// `collectgarbage`'s parameters as the dialect stores them; they set
150 /// the three knobs above through `set_gc_pacing`.
151 pub(crate) gc_params: crate::vm::lib_gc::GcParams,
152 /// true while `__gc` finalizers are being run, so a finalizer that calls
153 /// `collectgarbage` gets a no-op (PUC's non-reentrancy: lua_gc returns -1 →
154 /// `collectgarbage` yields fail).
155 gc_finalizing: bool,
156 /// C ABI scratch (`capi` module): the host-visible value stack that C
157 /// callers operate on via `lua_pushinteger` / `lua_tostring` / etc.
158 /// Kept here (instead of in a separate `LuaState` wrapper) so the
159 /// trampoline that bridges to a `LuaCFunction` can safely cast the
160 /// Vm pointer it already holds to the public `*mut LuaState` type
161 /// without any aliasing of `&mut Vm` against `&mut LuaState.vm`.
162 pub capi_stack: Vec<crate::runtime::Value>,
163 /// Pinned CString backing the pointer last returned by `lua_tostring`;
164 /// valid until the next `lua_tostring` on the same Vm.
165 pub capi_cstr_pin: Option<std::ffi::CString>,
166 /// PUC 5.4+ warning system. Lua manual §6.1 `warn`: emitted messages
167 /// concatenate across continuation calls until a non-`tocont` call
168 /// flushes; the default warnf recognises `@on`/`@off` control messages
169 /// and starts disabled. luna's `emit_warn` mirrors the default warnf
170 /// behaviour and 5.4+ `__gc` errors are routed through it (5.1–5.3
171 /// keep the older raise semantics).
172 pub(crate) warn_state: WarnState,
173 pub(crate) warn_buf: Vec<u8>,
174 /// Embedding cooperative budget: a per-Vm tick counter that the run
175 /// loop decrements once per dispatch turn. When it hits zero the loop
176 /// raises a catchable "instruction budget exceeded" error so the embedder
177 /// can yield control back to its caller (short-script eval, game
178 /// frame budgets). `None` = unbounded; reset on each call via
179 /// `set_instr_budget`.
180 pub(crate) instr_budget: Option<i64>,
181 // JIT-specific state lives in the `JitState` sidecar; see `self.jit`
182 // below and `crate::vm::jit_state` for field docs.
183 /// Bytecode-loading gate. Default `true`. Sandbox embedders should
184 /// call `set_bytecode_loading(false)` so `load`/`loadstring` reject
185 /// precompiled chunks (which bypass the parser's depth / opcode
186 /// limits). When `false`, the loader rejects any source whose first
187 /// byte is the bytecode signature `\27` ("`\27Lua`").
188 pub(crate) bytecode_loading: bool,
189 /// PUC bytecode-loading gate. Default `false` — PUC `.luac` files are
190 /// a strictly larger trust surface than luna's own dump format
191 /// (third-party toolchain bugs, malformed chunks, unknown opcode
192 /// shapes). When `true`, the loader routes `\x1bLua\x{51..55}` inputs
193 /// through the per-dialect PUC translators in `crate::vm::dump::puc`.
194 /// Embedder toggles via `set_puc_bytecode_loading`.
195 pub(crate) puc_bytecode_loading: bool,
196 /// Byte budget for source fed into `load` / `loadstring` / `Vm::load`.
197 /// Default [`Vm::DEFAULT_LOADER_INPUT_BUDGET`] (256 MiB). When the
198 /// accumulated reader output (`load(f, ...)`) or a one-shot `&[u8]`
199 /// source exceeds this, the loader returns the PUC-shaped
200 /// `not enough memory` error before the host allocator is asked to
201 /// hold the next chunk. Defends against `heavy.lua::loadrep`-style
202 /// 7 GB+ feeder loops that would otherwise SIGSEGV when `Vec::push`
203 /// crosses `isize::MAX` or the host runs out of RAM.
204 /// Embedders that genuinely need to load > 256 MiB sources widen the
205 /// cap via [`Vm::set_loader_input_budget`].
206 pub(crate) loader_input_budget: usize,
207 /// In-process log of fully-emitted warnings (each entry = one flushed
208 /// message, sans the "Lua warning: " prefix and trailing newline). Lets
209 /// tests assert what was warned without scraping stderr.
210 pub(crate) warn_log: Vec<Vec<u8>>,
211 /// PUC's `LUA_REGISTRYINDEX` table — a single Lua table the debug library
212 /// exposes via `debug.getregistry`. Used to hold `_HOOKKEY` (the weak-key
213 /// table PUC's `db_sethook` keys per-thread hooks under). luna stores hook
214 /// state directly in `Vm.hook`/`Coro.hook`, so the entry is largely a
215 /// shape stub for db.lua :328; if other registry-keyed APIs land later
216 /// they can share this table.
217 pub(crate) registry: Option<Gc<Table>>,
218 /// the shared `FILE*` metatable for io file handles (PUC's LUA_FILEHANDLE
219 /// registry entry); attached to every file userdata the io library makes
220 pub(crate) file_mt: Option<Gc<Table>>,
221 /// io library default input/output streams (PUC registry IO_INPUT/IO_OUTPUT)
222 pub(crate) io_input: Option<Gc<crate::runtime::Userdata>>,
223 pub(crate) io_output: Option<Gc<crate::runtime::Userdata>>,
224 /// `io.stdin` as the io library made it, whatever the script later does
225 /// to the `io` table: the stream a host's line reads share
226 pub(crate) io_stdin: Option<Gc<crate::runtime::Userdata>>,
227 /// lua.c's `-E`: libraries opened from now on ignore the environment
228 pub(crate) ignore_env: bool,
229 /// the running thread's debug hook state (`debug.sethook`); per-thread,
230 /// swapped with the execution context on a coroutine resume/yield
231 pub(crate) hook: HookState,
232 /// true while the hook itself runs, so its own execution fires no events
233 /// (PUC clears the mask for the duration)
234 pub(crate) in_hook: bool,
235 /// arms the next Lua frame's `tailcalls` count (PUC `ci->u.l.tailcalls`),
236 /// consumed by `push_frame`. `OP_TailCall` sets it to the caller's
237 /// own tailcalls + 1 before begin_call so deeply tail-recursive chains
238 /// accumulate the count instead of capping at 1.
239 pub(crate) pending_tailcalls: u32,
240 /// arms the next Lua frame's `ccmt` (its `__call` chain length), consumed
241 /// by `push_frame`. `OP_TailCall` sets it to the reused activation's
242 /// count; `begin_call` otherwise sets the chain it just resolved.
243 pending_ccmt: u8,
244 /// Name of the C native that just propagated an error (captured before
245 /// the native is popped from `running_natives`). Lets a dying coroutine
246 /// preserve `[C]: in function '<name>'` at the top of its traceback
247 /// snapshot — PUC walks `luaG_funcnamefrompc` over a still-live ci, but
248 /// luna's native frames are off-stack so we stash the name explicitly.
249 pub(crate) errored_natives: Vec<crate::vm::callstack::ErroredNative>,
250 /// Frames below this index are out of reach of the error handler of
251 /// an `xpcall` (PUC `L->errfunc`): a protected call made from Rust — a
252 /// finalizer, the handler itself — starts a fresh `errfunc` scope.
253 pub(crate) msgh_floor: usize,
254 /// The message handler that is running, if any. PUC's `luaG_errormsg`
255 /// calls the handler with `L->errfunc` still set, so an error inside
256 /// the handler (and not caught within it) calls the handler again, at
257 /// the point of that error. Per thread, like `L->errfunc`.
258 pub(crate) msgh_running: Option<Value>,
259 /// How many message-handler runs have started; lets a run tell whether
260 /// the error it got back was already handled by a nested run.
261 pub(crate) msgh_runs: u64,
262 /// The value the last `xpcall` handler produced for the error in
263 /// flight, so the unwind that carries it to the `xpcall` does not
264 /// run the handler again.
265 pub(crate) msgh_applied: Option<Value>,
266 /// Whether an error nothing catches should keep its traceback: not
267 /// inside a protected call made from Rust, which discards it.
268 pub(crate) keep_error_traceback: bool,
269 /// PUC `CallInfo.u2.transferinfo`: index of the first transferred value
270 /// (relative to the activation's func slot) and the number transferred.
271 /// Set just before firing a call/return hook, read by `getinfo("r")`.
272 pub(crate) hook_ftransfer: u16,
273 pub(crate) hook_ntransfer: u16,
274 /// metamethod event tag (e.g. "close") to attach to the next Lua frame
275 /// pushed by `push_frame`; `close_slots` sets this before calling a
276 /// `__close` handler so `debug.traceback` names it "metamethod 'close'"
277 /// (PUC `CallInfo.u.l.tm`). Single-shot: `push_frame` consumes it.
278 pending_tm: Option<&'static str>,
279 /// `true` when the next `push_frame` is the user hook function itself,
280 /// so `debug.getinfo(1).namewhat` resolves to `"hook"` (PUC
281 /// `CIST_HOOKED`). `run_hook` arms it before dispatching the hook.
282 pending_is_hook: bool,
283 /// traceback of an error nothing in its thread catches, one line per
284 /// stack level, taken where it was raised (see `raise_to_handler`): what
285 /// the host gets from `take_error_traceback`, and what `debug.traceback`
286 /// shows of the coroutine it kills. Cleared on a catch and at host-level
287 /// `call_value` entry (`public_call_depth == 0`).
288 pub(crate) error_traceback: Option<Vec<Vec<u8>>>,
289 /// nesting depth of public `call_value` entries (host vs. internal). The
290 /// outermost entry (depth 0) resets per-error state (`error_traceback`);
291 /// internal calls (e.g. xpcall msgh, sort callback) preserve it.
292 public_call_depth: u32,
293 /// stack of native (`Value::Native`) closures currently running on the
294 /// Rust call stack. `begin_call` pushes the closure before invoking
295 /// `nc.f` and pops on return. Used by `arg_error` to detect a *nested*
296 /// native call (PUC `ar.name == NULL` at level 0 because the level-0
297 /// caller is C, not Lua) and qualify the running function's name via
298 /// `pushglobalfuncname` (e.g. `'sort'` → `'table.sort'`).
299 pub(crate) running_natives: Vec<Gc<NativeClosure>>,
300 /// Parallel to `running_natives`: where each native sits on the value
301 /// and frame stacks, so the debug interface can place it among the Lua
302 /// activations as PUC's CallInfo chain would (see `callstack`).
303 pub(crate) running_native_acts: Vec<crate::vm::callstack::NativeAct>,
304 /// Index into `running_natives` where the running thread's own natives
305 /// begin; the ones below belong to the threads that resumed it.
306 pub(crate) natives_base: usize,
307 /// JIT sidecar. Always present (never `Option`); inert
308 /// when `chunk_compiler` / `trace_compiler` are
309 /// [`crate::jit::NullJitBackend`]. See [`crate::vm::jit_state`].
310 ///
311 /// `#[doc(hidden)] pub` so the `luna` crate's
312 /// `extern "C"` JIT helpers can write `vm.jit.pending_err`
313 /// directly. Not part of the embedder-facing API surface.
314 #[doc(hidden)]
315 pub jit: crate::vm::jit_state::JitState,
316
317 /// Host roots — a `Vec<Value>` traced as an extra GC root set.
318 /// `Lua` facade handles (`LuaFunction`, `LuaTable`, `LuaRoot`) hold
319 /// indices into this vector so the underlying `Gc<T>` stays alive
320 /// across `eval` calls / yield boundaries. Freed slots are recycled
321 /// through `host_roots_free`.
322 pub(crate) host_roots: Vec<crate::vm::host_roots::HostRootSlot>,
323 /// Recycled-slot index pool. `pin_host` pops the
324 /// back if non-empty, else extends `host_roots`. Generation
325 /// overflow at `u32::MAX` retires the slot (NOT pushed here).
326 pub(crate) host_roots_free: Vec<u32>,
327
328 /// GC-rooted scratch stack for `table.sort` (and any other
329 /// builtin that needs a Rust-side `Vec<Value>` to outlive a user
330 /// callback). Each entry is one in-flight working buffer; `gc_roots`
331 /// extends with every contained `Value` so a `collectgarbage()`
332 /// inside the comparator cannot free strings/tables snapshotted
333 /// here. Nested sorts push a new buffer on entry, pop on exit
334 /// (sort.lua's `load(..)(); collectgarbage()` compare callback
335 /// regression).
336 pub(crate) sort_scratch: Vec<Vec<Value>>,
337
338 /// MacroLua compile-time macro registry.
339 /// Pre-populated with built-in macros (`@quote` / `@unquote` /
340 /// `@if` / `@gensym`) at construction time when `version ==
341 /// LuaVersion::MacroLua`; embedders register custom macros via
342 /// [`Vm::define_macro`]. The expander runs once per `load()` call
343 /// between lexing and parsing (only when `is_macro_lua()`).
344 pub(crate) macro_registry: crate::frontend::macro_expander::MacroRegistry,
345
346 /// Per-Vm cache of `Gc<Table>` metatables keyed
347 /// by `TypeId::of::<T>()` for embedder types implementing
348 /// [`crate::vm::userdata_trait::LuaUserdata`]. Populated lazily by
349 /// [`Vm::register_userdata`]; metatables are pinned via
350 /// [`Vm::pin_host`] at registration time so the entry's
351 /// `Gc<Table>` stays live for the rest of the Vm's lifetime.
352 pub(crate) userdata_metatables:
353 std::collections::HashMap<std::any::TypeId, Gc<crate::runtime::table::Table>>,
354
355 /// Classification of the most recent error raised on this Vm.
356 /// Embedders read via [`Vm::error_kind`]; the dispatcher sets it
357 /// at well-known sites (syntax errors, instr-budget trips, native
358 /// callback errors, type errors).
359 pub(crate) last_error_kind: crate::vm::error::LuaErrorKind,
360
361 /// `(source_name, line)` of the most recent error. Set by the
362 /// dispatcher / lexer / parser; cleared when a new call_value
363 /// enters cleanly.
364 pub(crate) last_error_source: Option<(String, u32)>,
365
366 /// When `true`, `instr_budget` exhaustion in
367 /// the dispatcher hot loop yields cooperatively (sets
368 /// [`Vm::host_yield_pending`] + returns a sentinel `Err` walked up
369 /// to `EvalFuture::poll`) instead of returning a real
370 /// "instruction budget exceeded" error. Set by [`Vm::eval_async`]
371 /// for the duration of the future; restored to `false` on
372 /// `Poll::Ready`. The sync `Vm::eval` / `Vm::call_value` paths
373 /// leave it `false` so budget exhaustion stays a real error there.
374 pub(crate) async_mode: bool,
375
376 /// Host waker cloned by `EvalFuture::poll` before driving a slice.
377 /// The dispatcher itself does not call it (the future's poll loop
378 /// does `wake_by_ref` after observing `BudgetExhausted`); it is kept
379 /// so async natives can wake the host directly from a helper future.
380 pub(crate) async_waker: Option<std::task::Waker>,
381
382 /// Per-poll opcode quota loaded into
383 /// `instr_budget` at the start of each `EvalFuture::poll` slice.
384 /// Default 10_000. Tunable via
385 /// [`Vm::set_async_slice`].
386 pub(crate) async_slice_size: i64,
387
388 /// Set by the dispatcher when an async-mode
389 /// budget exhaustion fires; checked by `exec_with` (so the
390 /// sentinel propagates without `unwind` running, mirroring
391 /// `yielding.is_some()`) and by `call_value_impl` (so the call
392 /// frames survive for the next poll). Cleared by `drive_one`
393 /// after translating it to `DispatchOutcome::BudgetExhausted`.
394 pub(crate) host_yield_pending: bool,
395
396 /// Set by the dispatcher's native-call path
397 /// when an async-marked [`NativeClosure`] is invoked under
398 /// `async_mode`. The Vm pauses the dispatcher (same sentinel-Err
399 /// mechanism as `host_yield_pending` — see `exec_with` +
400 /// `call_value_impl`), stashes the in-flight future +
401 /// post-completion context here, and surfaces them to
402 /// `EvalFuture::poll` via `drive_one`. Cleared by `drive_one`
403 /// once the future is moved out into a
404 /// `DispatchOutcome::AsyncNativeAwaiting`.
405 pub(crate) pending_async_native_fut:
406 Option<std::pin::Pin<Box<dyn std::future::Future<Output = Result<u32, LuaError>>>>>,
407
408 /// Companion to `pending_async_native_fut`:
409 /// the `(func_slot, nargs, nresults, gc_top)` quad needed to
410 /// commit the future's eventual `Ok(nret)` back into the calling
411 /// frame's expected result slots. Recorded by the dispatcher;
412 /// consumed by [`Vm::commit_async_native_result`] after the
413 /// future resolves.
414 pub(crate) pending_async_native_ctx: Option<AsyncNativeCallCtx>,
415}
416
417/// Call-site context an in-flight async native
418/// needs preserved across the cooperative-yield boundary.
419///
420/// The dispatcher records this when it routes a `NativeClosure` with
421/// `is_async == true` through the cooperative path; `EvalFuture::poll`
422/// hands it back to [`Vm::commit_async_native_result`] once the
423/// awaited future resolves so `finish_results` (and the post-call GC
424/// checkpoint) can run as if the native had completed synchronously.
425#[derive(Clone, Copy)]
426pub(crate) struct AsyncNativeCallCtx {
427 pub func_slot: u32,
428 /// Recorded for parity with the sync native-call path's
429 /// `native_nresults`/`gc_top` bookkeeping; reserved for hook
430 /// firing + traceback shaping. Not read yet.
431 #[allow(dead_code)]
432 pub nargs: u32,
433 pub nresults: i32,
434 /// Recorded for traceback + GC-root-window checks. The resume path
435 /// reads `Vm.gc_top` directly, so this is unread today; carried so a
436 /// check can confirm the pre-suspend root window matches the
437 /// post-resume one.
438 #[allow(dead_code)]
439 pub gc_top: u32,
440}
441
442/// Per-thread debug hook state (PUC `lua_State` hook/hookmask/basehookcount/
443/// hookcount). `func` is the Lua hook; the booleans are the PUC mask bits.
444#[derive(Clone, Copy, Default)]
445pub struct HookState {
446 /// the hook function (`None` when no hook is installed)
447 pub func: Option<Value>,
448 /// Rust-side debug hook. Fires alongside the Lua hook
449 /// (Rust first); both can be installed simultaneously, but most
450 /// embedders pick one.
451 pub rust_func: Option<RustDebugHook>,
452 /// LUA_MASKCALL — fire on function entry
453 pub call: bool,
454 /// LUA_MASKRET — fire on function return
455 pub ret: bool,
456 /// LUA_MASKLINE — fire on source-line change
457 pub line: bool,
458 /// LUA_MASKCOUNT — fire every `count_base` instructions
459 pub count: bool,
460 /// instruction count between count events (PUC basehookcount)
461 pub count_base: i64,
462 /// instructions left until the next count event (PUC hookcount)
463 pub count_left: i64,
464}
465
466/// Rust-side debug hook callback. Receives the `Vm` plus a
467/// classified event. The callback runs synchronously in the
468/// dispatcher; the hook flag (`in_hook`) is set for its duration so
469/// hook recursion is suppressed.
470pub type RustDebugHook = fn(&mut Vm, RustHookEvent);
471
472/// Classified debug event delivered to a [`RustDebugHook`].
473#[derive(Clone, Copy, Debug, PartialEq, Eq)]
474pub enum RustHookEvent {
475 /// Function entry (`hook_call` analogue).
476 Call,
477 /// Function return (`hook_return` analogue).
478 Return,
479 /// Tail call entry (PUC 5.2+ separates this from a plain Call).
480 TailCall,
481 /// Source-line change (the `u32` is the 1-based line number).
482 Line(u32),
483 /// Instruction count event (fires every `count_base` instructions).
484 Count,
485}
486
487/// Mask flags for [`Vm::set_rust_debug_hook`]. OR these to subscribe
488/// to multiple event categories with a single hook installation.
489pub const HOOK_MASK_CALL: u32 = 1;
490/// Subscribe to function-return events.
491pub const HOOK_MASK_RETURN: u32 = 2;
492/// Subscribe to line-change events.
493pub const HOOK_MASK_LINE: u32 = 4;
494/// Subscribe to instruction-count events.
495pub const HOOK_MASK_COUNT: u32 = 8;
496
497/// A thread's swapped-out execution context (PUC per-thread stack state).
498struct SavedCtx {
499 stack: Vec<Value>,
500 frames: Vec<CallFrame>,
501 frame_ccmt: Vec<u8>,
502 open_upvals: Vec<(u32, Gc<Upvalue>)>,
503 tbc: Vec<u32>,
504 top: u32,
505 pcall_depth: u32,
506 hook: HookState,
507 /// PUC `L->l_gt` — the thread's own globals table. Carried alongside
508 /// the rest of the suspended state so each thread can keep its own
509 /// `setfenv(0, env)` rewire without the swap leaking into another
510 /// thread (5.1 closure.lua :177).
511 globals: Gc<Table>,
512}
513
514/// Outcome of unwinding the call stack on an error (see `Vm::unwind`).
515enum Unwound {
516 /// caught by a pcall/xpcall continuation; resume running its caller
517 Caught,
518 /// caught by a continuation that was the entry-level activation; these are
519 /// the call's (wrapped) results
520 CaughtReturn(Vec<Value>),
521 /// no protecting continuation up to `entry_depth`; propagate the error
522 Propagated(LuaError),
523}
524
525/// Outcome of an index/newindex/comparison fast path: either a directly
526/// computed result, or a metamethod (with the receiver it resolved against) the
527/// caller must invoke — synchronously (C context) or yieldably (VM opcode).
528enum MmOut {
529 /// index → the looked-up value; newindex → done (raw set performed);
530 /// comparison → the boolean result already known
531 Done(Value),
532 /// a metamethod to call; `recv` is the chain element it was found on (the
533 /// extra args — key / value — are supplied by the caller)
534 Mm { func: Value, recv: Value },
535 /// ≤5.3 `a <= b` synthesised via `not __lt(b, a)` when neither operand
536 /// carries `__le` — `op_compare` swaps the args and negates the result.
537 /// Lives separate from `Mm` so the synth path can stay yieldable without
538 /// every other Mm caller learning a swap flag they would never set.
539 CompareSynth { func: Value },
540}
541
542/// Metamethod events; discriminants index `Vm::mm_names`.
543#[derive(Clone, Copy, PartialEq, Eq)]
544#[repr(usize)]
545pub(crate) enum Mm {
546 Index,
547 NewIndex,
548 Call,
549 ToString,
550 Metatable,
551 Name,
552 Eq,
553 Lt,
554 Le,
555 Concat,
556 Len,
557 Add,
558 Sub,
559 Mul,
560 Div,
561 Mod,
562 Pow,
563 IDiv,
564 BAnd,
565 BOr,
566 BXor,
567 Shl,
568 Shr,
569 Unm,
570 BNot,
571 Close,
572 Gc,
573 Pairs,
574}
575
576const MM_NAMES: [&str; 28] = [
577 "__index",
578 "__newindex",
579 "__call",
580 "__tostring",
581 "__metatable",
582 "__name",
583 "__eq",
584 "__lt",
585 "__le",
586 "__concat",
587 "__len",
588 "__add",
589 "__sub",
590 "__mul",
591 "__div",
592 "__mod",
593 "__pow",
594 "__idiv",
595 "__band",
596 "__bor",
597 "__bxor",
598 "__shl",
599 "__shr",
600 "__unm",
601 "__bnot",
602 "__close",
603 "__gc",
604 "__pairs",
605];
606
607/// The metamethod event an opcode dispatches, without the `__` prefix (PUC
608/// funcnamefromcode), for "(metamethod 'event')" call-error suffixes.
609fn mm_event_name(op: crate::vm::isa::Op) -> Option<&'static str> {
610 use crate::vm::isa::Op;
611 Some(match op {
612 Op::Add => "add",
613 Op::Sub => "sub",
614 Op::Mul => "mul",
615 Op::Div => "div",
616 Op::Mod => "mod",
617 Op::Pow => "pow",
618 Op::IDiv => "idiv",
619 Op::BAnd => "band",
620 Op::BOr => "bor",
621 Op::BXor => "bxor",
622 Op::Shl => "shl",
623 Op::Shr => "shr",
624 Op::Unm => "unm",
625 Op::BNot => "bnot",
626 Op::Concat => "concat",
627 Op::Len => "len",
628 Op::GetField | Op::GetTable | Op::GetI | Op::SelfOp => "index",
629 Op::SetField | Op::SetTable | Op::SetI => "newindex",
630 Op::Eq | Op::EqK => "eq",
631 Op::Lt => "lt",
632 Op::Le => "le",
633 _ => return None,
634 })
635}
636
637/// PUC MAXTAGLOOP (5.3+): bound on `__index`/`__newindex` chains.
638const MAX_TAG_LOOP: u32 = 2000;
639/// PUC `MAXCCMT`: bound on a `__call` metamethod chain (lvm.c). 200 chains
640/// is more than any reasonable program needs and matches PUC 5.4/5.5; a
641/// bound of `15` is tight enough to fire on calls.lua :194 (N=20).
642const MAX_CCMT: u32 = 200;
643/// PUC LUAI_MAXCCALLS analogue: native↔Lua nesting bound.
644pub(crate) const MAX_C_DEPTH: u32 = 200;
645/// Stack an xpcall handler may use past `MAX_LUA_STACK` while handling a
646/// stack overflow: PUC's 200 extra `ERRORSTACKSIZE` slots, plus the frame
647/// reserve (256) the overflowing call was refused, so the handler's first
648/// frame fits where that one did not.
649const ERROR_STACK_EXTRA: u32 = 200 + 256;
650/// luna's engine-level VM stack cap (used by call-site overflow checks).
651/// Slightly larger than PUC's `LUAI_MAXSTACK` so engine internals have a
652/// little headroom above any single library push.
653const MAX_LUA_STACK: u32 = 1 << 20;
654/// PUC `LUAI_MAXSTACK` (`luaconf.h`): the cap library code consults via
655/// `lua_checkstack` to refuse multi-value pushes (`table.unpack` returning
656/// N values, `string.pack` results, etc.). 5.3 coroutine.lua :530 pins
657/// this at one million — `for j in {lim-10, …}` expects every j ≥ lim-10
658/// to fail because the few slots already consumed in the coroutine push
659/// the effective cap below lim-10.
660const PUC_MAXSTACK: i64 = 1_000_000;
661
662/// PUC 5.4+ default warnf state. The base library's `warn` function flips
663/// between `Off` and `On` via the `@on` / `@off` control messages; any other
664/// `@<word>` control is silently ignored, mirroring `lauxlib.c::checkcontrol`.
665#[derive(Clone, Copy, PartialEq, Eq, Debug)]
666pub enum WarnState {
667 /// `warn` calls are silently dropped (default after `warn("@off")`).
668 Off,
669 /// `warn` calls are delivered to stderr (after `warn("@on")`).
670 On,
671}
672
673/// Best-effort extraction of a textual message from a `catch_unwind` payload.
674/// `panic!("msg")` arrives as `String`, `panic!(static)` as `&str`; anything
675/// else degrades to `"<non-string panic>"`. Used by the native-call
676/// catch_unwind to fold the panic into a Lua error.
677fn panic_payload_str(payload: &Box<dyn std::any::Any + Send>) -> String {
678 if let Some(s) = payload.downcast_ref::<String>() {
679 return s.clone();
680 }
681 if let Some(s) = payload.downcast_ref::<&'static str>() {
682 return (*s).to_string();
683 }
684 "<non-string panic>".to_string()
685}
686
687/// Combined error type returned by [`Vm::eval`] and friends — either the
688/// chunk failed to parse / compile, or it raised at runtime.
689#[derive(Debug)]
690pub enum Error {
691 /// Parse or compile failure.
692 Syntax(SyntaxError),
693 /// Runtime error raised during execution.
694 Runtime(LuaError),
695}
696
697impl From<SyntaxError> for Error {
698 fn from(e: SyntaxError) -> Error {
699 Error::Syntax(e)
700 }
701}
702
703impl From<LuaError> for Error {
704 fn from(e: LuaError) -> Error {
705 Error::Runtime(e)
706 }
707}
708
709impl Vm {
710 /// `lua_close` from inside a running script (`os.exit(code, true)`):
711 /// close the main thread's pending to-be-closed variables, then run every
712 /// finalizer. Both run protected, so their errors are dropped as PUC's
713 /// `close_state` drops them. Inside a coroutine the main thread's stack is
714 /// parked, and only the finalizers run.
715 pub(crate) fn close_state(&mut self) {
716 if self.current.is_none() {
717 let _ = self.close_slots(0, None);
718 }
719 self.heap.queue_all_finalizers();
720 self.run_finalizers();
721 }
722}
723
724impl Drop for Vm {
725 fn drop(&mut self) {
726 // state close: run `__gc` for every still-registered finalizable before
727 // the heap frees them (PUC separatetobefnz(g,1) + callallpending). A
728 // single pass — objects created by a closing finalizer are not
729 // re-finalized (they go to the heap's free list directly).
730 self.heap.queue_all_finalizers();
731 self.run_finalizers();
732 }
733}
734
735// Split-borrow free fn helpers for frames push/pop with shadow counter
736// `frames_top: u32`. Free fns (not Vm methods) so callers can pass
737// `&mut self.frames` + `&mut self.frames_top` as split borrows, allowing
738// other `&mut self.field` reads inside the CallFrame construction (e.g.
739// `std::mem::take(&mut self.pending_tm)`).
740//
741// The shadow has no readers yet; it just stays in sync + asserts.
742#[inline(always)]
743fn frames_push_sync(frames: &mut Vec<CallFrame>, frames_top: &mut u32, cf: CallFrame) {
744 frames.push(cf);
745 // Shadow maintenance is debug-only: release builds skip the
746 // increment + assertion entirely. While nothing reads the shadow,
747 // its purpose is to VERIFY the assumed invariant
748 // (frames_top == frames.len()) across all push/pop sites; once readers
749 // consume it, release must run the increment unconditionally.
750 #[cfg(debug_assertions)]
751 {
752 *frames_top += 1;
753 debug_assert_eq!(
754 *frames_top as usize,
755 frames.len(),
756 "P17-D frames_top out of sync after push",
757 );
758 }
759 #[cfg(not(debug_assertions))]
760 let _ = frames_top;
761}
762
763#[inline(always)]
764fn frames_pop_sync(frames: &mut Vec<CallFrame>, frames_top: &mut u32) -> Option<CallFrame> {
765 let r = frames.pop();
766 #[cfg(debug_assertions)]
767 {
768 if r.is_some() {
769 *frames_top = frames_top.saturating_sub(1);
770 }
771 debug_assert_eq!(
772 *frames_top as usize,
773 frames.len(),
774 "P17-D frames_top out of sync after pop",
775 );
776 }
777 #[cfg(not(debug_assertions))]
778 let _ = frames_top;
779 r
780}
781
782/// One-time env-var read for
783/// `LUNA_AOT_PROBE`. Returns `true` iff the env var is set to any
784/// non-empty value. The result is cached in a `OnceLock` so the
785/// dispatcher's hot path pays a single atomic load per process. Off
786/// by default — production deploys don't bleed diagnostic prints.
787fn jit_probe_enabled() -> bool {
788 static PROBE_ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
789 *PROBE_ON.get_or_init(|| {
790 std::env::var("LUNA_AOT_PROBE")
791 .ok()
792 .filter(|v| !v.is_empty())
793 .is_some()
794 })
795}
796
797impl Vm {
798 /// Re-sync `frames_top` after a bulk `frames: Vec`
799 /// swap (take_ctx, put_ctx, load_coro_ctx). Must be called after
800 /// the Vec replacement to keep the shadow valid.
801 #[inline(always)]
802 fn frames_resync(&mut self) {
803 // Debug-only — see `frames_push_sync` comment.
804 #[cfg(debug_assertions)]
805 {
806 self.frames_top = self.frames.len() as u32;
807 }
808 }
809
810 // ====================================================================
811 // Stack-inline frame metadata accessors (unused).
812 //
813 // These methods read/write the LJ_FR2 marker slots at `stack[base-2]`
814 // (closure GCRef) and `stack[base-1]` (FrameMarker as i64). No call
815 // site uses them yet.
816 //
817 // Preconditions (debug-asserted):
818 // - base >= 2 (slots base-2 and base-1 must exist below the frame)
819 // - self.stack.len() > base + max_stack (caller has grown stack)
820 // - For Lua frames, stack[base-2] holds Value::Closure(cl)
821 // - For Lua frames, stack[base-1] holds Value::Int(marker.to_raw())
822 //
823 // No release-build cost when unused (LTO strips dead methods).
824 // ====================================================================
825
826 /// Write a Lua frame's closure pointer into `stack[base-2]`.
827 /// The caller must ensure `base >= 2` and the slot is within the
828 /// stack's allocated range.
829 #[inline]
830 #[allow(dead_code)] // no consumer yet
831 fn write_frame_closure(&mut self, base: u32, cl: crate::runtime::Gc<LuaClosure>) {
832 debug_assert!(
833 base >= 2,
834 "frame closure slot needs base >= 2; got {}",
835 base
836 );
837 let idx = (base - 2) as usize;
838 debug_assert!(idx < self.stack.len(), "stack[base-2] out of range");
839 self.stack[idx] = Value::Closure(cl);
840 }
841
842 /// Read a Lua frame's closure pointer from `stack[base-2]`.
843 /// Returns `None` if the slot doesn't hold a closure (caller is
844 /// expected to treat that as a corrupt frame).
845 ///
846 /// Uses the [`Value::tag_byte`] fast-path
847 /// to avoid the enum-match cost on the hot path. Tag check via
848 /// 1-byte load + branch + `as_closure_unchecked` payload load.
849 #[inline]
850 #[allow(dead_code)]
851 fn read_frame_closure(&self, base: u32) -> Option<crate::runtime::Gc<LuaClosure>> {
852 debug_assert!(base >= 2);
853 let v = self.stack.get((base - 2) as usize)?;
854 if v.tag_byte() == crate::runtime::value::tag::CLOSURE {
855 // SAFETY: tag byte just verified == CLOSURE.
856 Some(unsafe { v.as_closure_unchecked() })
857 } else {
858 None
859 }
860 }
861
862 /// Write a packed [`FrameMarker`] into `stack[base-1]`. The marker
863 /// encodes the frame kind (Lua / Cont) + PC-or-delta payload.
864 /// Stored as `Value::Int(marker.to_raw())` so it round-trips
865 /// cleanly through the value stack without losing bits.
866 #[inline]
867 #[allow(dead_code)]
868 fn write_frame_marker(&mut self, base: u32, marker: crate::runtime::frame_marker::FrameMarker) {
869 debug_assert!(base >= 1, "frame marker slot needs base >= 1; got {}", base);
870 let idx = (base - 1) as usize;
871 debug_assert!(idx < self.stack.len(), "stack[base-1] out of range");
872 self.stack[idx] = Value::Int(marker.to_raw());
873 }
874
875 /// Read a packed [`FrameMarker`] from `stack[base-1]`. Returns
876 /// `None` if the slot isn't a `Value::Int` (caller treats as a
877 /// corrupt frame); the kind tag itself may still be invalid, in
878 /// which case [`FrameMarker::kind`] returns `None` on the result.
879 ///
880 /// Uses the [`Value::tag_byte`] fast-path
881 /// for the tag check + `as_int_unchecked` for the payload load.
882 #[inline]
883 #[allow(dead_code)]
884 fn read_frame_marker(&self, base: u32) -> Option<crate::runtime::frame_marker::FrameMarker> {
885 debug_assert!(base >= 1);
886 let v = self.stack.get((base - 1) as usize)?;
887 if v.tag_byte() == crate::runtime::value::tag::INT {
888 // SAFETY: tag byte just verified == INT.
889 Some(crate::runtime::frame_marker::FrameMarker::from_raw(
890 unsafe { v.as_int_unchecked() },
891 ))
892 } else {
893 None
894 }
895 }
896
897 /// Build the raw `Vm` struct without main coroutine / RNG seed / library
898 /// setup. Private helper shared by `Vm::new` and `Vm::new_minimal`; the
899 /// caller is responsible for the rest of the bring-up.
900 fn new_inner(version: LuaVersion) -> Vm {
901 let mut heap = Heap::new();
902 // PUC 5.1 had no ephemeron pass — `__mode='k'` tables marked their
903 // values strongly. gc.lua's "weak tables" section relies on that.
904 heap.no_ephemeron = version <= LuaVersion::Lua51;
905 // PUC 5.3 needs two GC cycles to finalize a table caught in a
906 // coroutine reference cycle (gc.lua :502); 5.4+ rewrote the GC and
907 // finalize in a single cycle (5.4/5.5 gc.lua :544 assert exactly one).
908 heap.defer_thread_cycle_finalize = version == LuaVersion::Lua53;
909 let globals = heap.new_table();
910 let mm_names = MM_NAMES.iter().map(|n| heap.intern(n.as_bytes())).collect();
911
912 Vm {
913 heap,
914 stack: Vec::new(),
915 frames: Vec::new(),
916 frame_ccmt: Vec::new(),
917 frames_top: 0,
918 open_upvals: Vec::new(),
919 tbc: Vec::new(),
920 top: 0,
921 globals,
922 type_mt: [None; 5],
923 mm_names,
924 c_depth: 0,
925 pcall_depth: 0,
926 nny: 0,
927 msgh_depth: 0,
928 terminating: None,
929 rng: [0; 4],
930 started: std::time::Instant::now(),
931 version,
932 closing_err: None,
933 current: None,
934 main_ctx: None,
935 yielding: None,
936 native_nresults: -1,
937 main_coro: None,
938 // PUC 5.4+ boots in GENERATIONAL mode (the first
939 // `collectgarbage("generational")` reports "generational"
940 // as the previous mode on stock lua5.4;
941 // 5.5 behaves the same, probed against lua5.5). luna's
942 // collector is a single incremental engine either way;
943 // this field is the MODE REPORT the stdlib exposes.
944 gc_mode: if version >= crate::version::LuaVersion::Lua54 {
945 "generational"
946 } else {
947 "incremental"
948 },
949 gc_top: 0,
950 gc_pause: 200,
951 gc_stepmul: 100,
952 gc_stepsize: 13,
953 gc_params: crate::vm::lib_gc::GcParams::new(version),
954 gc_finalizing: false,
955 capi_stack: Vec::new(),
956 capi_cstr_pin: None,
957 warn_state: WarnState::Off,
958 warn_buf: Vec::new(),
959 warn_log: Vec::new(),
960 instr_budget: None,
961 bytecode_loading: true,
962 puc_bytecode_loading: false,
963 loader_input_budget: Vm::DEFAULT_LOADER_INPUT_BUDGET,
964 registry: None,
965 file_mt: None,
966 io_input: None,
967 io_output: None,
968 io_stdin: None,
969 ignore_env: false,
970 hook: HookState::default(),
971 in_hook: false,
972 pending_tailcalls: 0,
973 pending_ccmt: 0,
974 errored_natives: Vec::new(),
975 msgh_floor: 0,
976 msgh_running: None,
977 msgh_runs: 0,
978 msgh_applied: None,
979 keep_error_traceback: true,
980 hook_ftransfer: 0,
981 hook_ntransfer: 0,
982 pending_tm: None,
983 pending_is_hook: false,
984 error_traceback: None,
985 public_call_depth: 0,
986 running_natives: Vec::new(),
987 running_native_acts: Vec::new(),
988 natives_base: 0,
989 // JIT-specific state lives in the `JitState`
990 // sidecar. The `luna` crate's `Vm::new_minimal_with_jit` /
991 // `install_jit_backend` / `luaL_newstate` swap in
992 // `CraneliftBackend` for callers that want JIT acceleration.
993 jit: crate::vm::jit_state::JitState::with_null_backend(),
994 // host roots ticket pool for the `Lua` facade
995 host_roots: Vec::new(),
996 // MacroLua registry. Pre-populated with
997 // built-ins (`@quote` / `@unquote` / `@if` / `@gensym`)
998 // when this Vm is constructed under `LuaVersion::MacroLua`.
999 macro_registry: if version == LuaVersion::MacroLua {
1000 crate::frontend::macro_expander::MacroRegistry::with_builtins()
1001 } else {
1002 crate::frontend::macro_expander::MacroRegistry::new()
1003 },
1004 host_roots_free: Vec::new(),
1005 sort_scratch: Vec::new(),
1006 // LuaUserdata trait sugar's per-Vm
1007 // metatable cache. Populated lazily by register_userdata.
1008 userdata_metatables: std::collections::HashMap::new(),
1009 // Error classification metadata. Defaults to
1010 // Runtime; set at known sites (syntax / budget trip /
1011 // native error / type error).
1012 last_error_kind: crate::vm::error::LuaErrorKind::default(),
1013 last_error_source: None,
1014 // Async embedder fields. Defaults preserve sync behavior
1015 // bit-for-bit (`async_mode = false` means the budget hot loop
1016 // errors out instead of yielding).
1017 async_mode: false,
1018 async_waker: None,
1019 async_slice_size: 10_000,
1020 host_yield_pending: false,
1021 // Pending async-native state. Empty by
1022 // default; populated only by the dispatcher when an
1023 // async-marked NativeClosure is invoked under async_mode.
1024 pending_async_native_fut: None,
1025 pending_async_native_ctx: None,
1026 }
1027 }
1028
1029 /// Build a fully-loaded Vm — the default for embedders that want PUC's
1030 /// standard library surface. Equivalent to `Vm::new_minimal(version)`
1031 /// followed by `vm.open_all_libs()`.
1032 pub fn new(version: LuaVersion) -> Vm {
1033 let mut vm = Vm::new_minimal(version);
1034 vm.open_all_libs();
1035 vm
1036 }
1037
1038 /// Build a Vm with no standard libraries loaded. Embedders
1039 /// that want a sandbox (Redis-style scripts, in-game scripting with
1040 /// a curated API) call this and then `open_base` / `open_math` / etc.
1041 /// selectively. The Vm is otherwise fully initialized (main coroutine,
1042 /// RNG seed, GC) so `eval` and `call_value` are immediately usable.
1043 pub fn new_minimal(version: LuaVersion) -> Vm {
1044 let mut vm = Vm::new_inner(version);
1045 let mc = vm.heap.new_coro(Value::Nil, vm.globals);
1046 // 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).
1047 unsafe { mc.as_mut() }.status = CoroStatus::Running;
1048 vm.main_coro = Some(mc);
1049 let (a, b) = vm.rng_auto_seed();
1050 vm.rng_seed(a as u64, b as u64);
1051 vm
1052 }
1053
1054 /// Install a caller-supplied JIT backend. The
1055 /// `luna` crate uses this to swap in its `CraneliftBackend`; tests
1056 /// or third-party backends pass their own [`crate::jit::IntChunkCompiler`] /
1057 /// [`crate::jit::TraceCompiler`] implementations. Re-installing on a Vm whose
1058 /// closures already populated `Proto.jit: JitProtoState::Compiled`
1059 /// does NOT evict those cached entries — call right after
1060 /// construction for a clean swap.
1061 ///
1062 /// Naming: `install_jit_backend` (not `install_default_jit`)
1063 /// because the "default" in luna-core is `NullJitBackend`; the
1064 /// "default JIT" lives in the `luna` crate.
1065 pub fn install_jit_backend<C, T>(&mut self, chunk: C, trace: T)
1066 where
1067 C: crate::jit::IntChunkCompiler + 'static,
1068 T: crate::jit::TraceCompiler + 'static,
1069 {
1070 self.jit.chunk_compiler = Box::new(chunk);
1071 self.jit.trace_compiler = Box::new(trace);
1072 }
1073
1074 /// Install a caller-supplied JIT
1075 /// storage holder. Default is [`crate::jit::NullJitStorage`];
1076 /// the `luna_jit` crate's `install_default_jit` pairs this with
1077 /// `install_jit_backend(CraneliftBackend, CraneliftBackend)` to
1078 /// also install a fresh `CraneliftJitStorage`. Storage holds
1079 /// the per-`Vm` JIT cache + handle collections.
1080 ///
1081 /// Idempotency: re-installing storage on a Vm that already
1082 /// holds compiled-trace pointers WILL evict their owners (the
1083 /// old `CraneliftJitStorage`'s `JITModule`s drop their mmap
1084 /// pages). Call right after construction for a clean swap.
1085 pub fn install_jit_storage<S>(&mut self, storage: S)
1086 where
1087 S: crate::jit::JitStorage + 'static,
1088 {
1089 self.jit.storage = Box::new(storage);
1090 }
1091
1092 /// Install the no-op JIT backend. `try_compile`
1093 /// reports "skipped" so every closure stays on the interpreter
1094 /// path, and the trace recorder's compile attempt always returns
1095 /// `None`. Intended for tests that want to verify the trait
1096 /// boundary works in a JIT-free configuration.
1097 ///
1098 /// Calling this on a Vm whose closures already populated
1099 /// `Proto.jit: JitProtoState::Compiled` does NOT evict those
1100 /// cached entries — the dispatcher will still call into them. For
1101 /// a truly JIT-free run, call this immediately after construction.
1102 pub fn install_null_jit(&mut self) {
1103 self.jit.chunk_compiler = Box::new(crate::jit::NullJitBackend);
1104 self.jit.trace_compiler = Box::new(crate::jit::NullJitBackend);
1105 }
1106
1107 /// Open the entire 5.5 standard library on a `new_minimal`-built Vm.
1108 /// `Vm::new` calls this; sandboxed embedders open libraries one at a
1109 /// time instead (`open_base`, `open_math`, `open_table`, …).
1110 pub fn open_all_libs(&mut self) {
1111 self.open_base();
1112 self.open_math();
1113 self.open_table();
1114 self.open_string();
1115 self.open_utf8();
1116 self.open_os_io();
1117 self.open_debug();
1118 self.open_coroutine();
1119 // PUC 5.2 introduced `bit32`; 5.3 retired it in the manual BUT
1120 // the stock 5.3 build ships -DLUA_COMPAT_5_2, which keeps the
1121 // library loaded. The diff ground truth is the default build
1122 // (stock lua5.3), so expose it under 5.2 AND
1123 // 5.3; 5.4 dropped the compat default for real.
1124 if matches!(self.version, LuaVersion::Lua52 | LuaVersion::Lua53) {
1125 self.open_bit32();
1126 }
1127 // last, so `package.loaded` lists every library opened before it
1128 self.open_package();
1129 }
1130
1131 /// Install the base library (`print`, `type`, `pairs`, `tostring`,
1132 /// `pcall`, `error`, `assert`, `select`, `setmetatable`, `getmetatable`,
1133 /// `rawequal`, `rawget`, `rawset`, `rawlen`, `next`, `tonumber`,
1134 /// `collectgarbage`, `warn` on 5.4+, `_VERSION`, `_G`, plus 5.1's
1135 /// retired globals `unpack`, `loadstring`, `setfenv`, `getfenv`,
1136 /// `newproxy`, `gcinfo` when version == 5.1). Safe to call at most
1137 /// once per Vm.
1138 pub fn open_base(&mut self) {
1139 crate::vm::builtins::open_base(self);
1140 }
1141 /// Install the `math` standard library.
1142 pub fn open_math(&mut self) {
1143 crate::vm::lib_math::open_math(self);
1144 }
1145 /// Install the `table` standard library.
1146 pub fn open_table(&mut self) {
1147 crate::vm::lib_table::open_table(self);
1148 }
1149 /// Install the `string` standard library (and the shared string metatable).
1150 pub fn open_string(&mut self) {
1151 crate::vm::lib_string::open_string(self);
1152 }
1153 /// Install the `utf8` standard library (5.3+).
1154 pub fn open_utf8(&mut self) {
1155 crate::vm::lib_utf8::open_utf8(self);
1156 }
1157 /// `os` and `io` are merged because file userdata shares state with both
1158 /// (`io.tmpname` and `os.tmpname` are the same function, `io.popen`
1159 /// wraps `os.execute`'s shell).
1160 pub fn open_os_io(&mut self) {
1161 crate::vm::lib_os_io::open_os_io(self);
1162 }
1163 /// Install the `debug` standard library (introspection / hooks). Off by
1164 /// default for sandbox embedders.
1165 pub fn open_debug(&mut self) {
1166 crate::vm::lib_debug::open_debug(self);
1167 }
1168 /// Install the `coroutine` standard library.
1169 pub fn open_coroutine(&mut self) {
1170 crate::vm::lib_coroutine::open_coroutine(self);
1171 }
1172 /// `package` plus the 5.1-only `module` and `package.seeall` aliases.
1173 pub fn open_package(&mut self) {
1174 crate::vm::lib_package::open_package(self);
1175 }
1176 /// 5.2-only `bit32` library (5.3+ retired in favour of native bitwise
1177 /// ops on 64-bit integers).
1178 pub fn open_bit32(&mut self) {
1179 crate::vm::lib_bit32::open_bit32(self);
1180 }
1181
1182 /// xoshiro256** next.
1183 pub(crate) fn rng_next(&mut self) -> u64 {
1184 let s = &mut self.rng;
1185 let result = s[1].wrapping_mul(5).rotate_left(7).wrapping_mul(9);
1186 let t = s[1] << 17;
1187 s[2] ^= s[0];
1188 s[3] ^= s[1];
1189 s[1] ^= s[2];
1190 s[0] ^= s[3];
1191 s[2] ^= t;
1192 s[3] = s[3].rotate_left(45);
1193 result
1194 }
1195
1196 /// Seed the RNG via splitmix64 expansion (PUC randseed shape).
1197 pub(crate) fn rng_seed(&mut self, a: u64, b: u64) {
1198 // PUC setseed: state = [n1, 0xff, n2, 0] (0xff avoids an all-zero
1199 // state), then 16 discards to spread the seed. Matches PUC's exact
1200 // sequence so the low-level conformance test passes.
1201 self.rng = [a, 0xff, b, 0];
1202 for _ in 0..16 {
1203 self.rng_next();
1204 }
1205 }
1206
1207 /// Wall-clock since VM creation (os.clock approximation).
1208 pub(crate) fn uptime(&self) -> std::time::Duration {
1209 self.started.elapsed()
1210 }
1211
1212 /// Entropy for math.randomseed() with no arguments.
1213 pub(crate) fn rng_auto_seed(&mut self) -> (i64, i64) {
1214 let t = std::time::SystemTime::now()
1215 .duration_since(std::time::UNIX_EPOCH)
1216 .map(|d| d.as_nanos() as u64)
1217 .unwrap_or(0);
1218 let addr = &self.rng as *const _ as u64;
1219 (t as i64, addr as i64)
1220 }
1221
1222 /// Allocate a native function object (no upvalues): builtin registration.
1223 pub fn native(&mut self, f: crate::runtime::value::NativeFn) -> Value {
1224 Value::Native(self.heap.new_native(f, Box::new([])))
1225 }
1226
1227 /// Allocate a native function object with captured upvalues.
1228 pub fn native_with(
1229 &mut self,
1230 f: crate::runtime::value::NativeFn,
1231 upvals: Box<[Value]>,
1232 ) -> Value {
1233 Value::Native(self.heap.new_native(f, upvals))
1234 }
1235
1236 /// Install the shared string metatable (string library).
1237 pub fn set_string_metatable(&mut self, mt: Option<Gc<Table>>) {
1238 self.type_mt[3] = mt;
1239 }
1240
1241 /// The current globals table (`_G` / `_ENV` source for new chunks).
1242 pub fn globals(&self) -> Gc<Table> {
1243 self.globals
1244 }
1245
1246 /// Remaining VM stack slots (PUC `L->stack_last - L->top` analogue).
1247 /// Library code that pushes a known number of fresh slots — e.g.
1248 /// `table.unpack` returning N values — consults this to refuse when
1249 /// the push would blow past `LUAI_MAXSTACK`. 5.3 coroutine.lua :530's
1250 /// `for j in {lim-10, lim-5, …}` series pins this contract: the
1251 /// coroutine's already-built table eats a few slots, so an unpack of
1252 /// ~lim values can't fit.
1253 pub(crate) fn stack_room(&self) -> i64 {
1254 PUC_MAXSTACK - (self.stack.len() as i64)
1255 }
1256
1257 /// Repoint the thread's "global table" used by *future* `Vm::load` calls
1258 /// for the chunk's `_ENV` upvalue (PUC 5.1 `setfenv(0, env)` rewrites
1259 /// `L->l_gt`). Already-loaded chunks keep their own snapshot via the
1260 /// per-closure cell-0 clone in `Op::Closure`, so they are unaffected.
1261 pub(crate) fn set_globals(&mut self, env: Gc<Table>) {
1262 self.globals = env;
1263 }
1264
1265 /// The Lua dialect this VM was constructed for (5.1 / 5.2 / 5.3 / 5.4 /
1266 /// 5.5). Determines numeric semantics, available standard libraries, and
1267 /// metamethod behavior.
1268 pub fn version(&self) -> LuaVersion {
1269 self.version
1270 }
1271
1272 /// Set a global by name. `v` may be any `IntoValue`: a primitive
1273 /// (`i64`, `f64`, `bool`, `&str`, `String`, `Vec<u8>`), a `Value`
1274 /// directly, an `Option<T>`, or a `Gc<Table>` / `Gc<LuaClosure>` /
1275 /// `Gc<NativeClosure>` handle.
1276 ///
1277 /// Returns `Err(LuaError)` only if the globals table overflows
1278 /// (extremely unlikely in practice — `MAX_ASIZE = 1 << 27`).
1279 /// String interning + key construction cannot fail.
1280 ///
1281 /// ```
1282 /// # use luna_core::vm::Vm;
1283 /// # use luna_core::version::LuaVersion;
1284 /// let mut vm = Vm::sandbox(LuaVersion::Lua55).open_base().build();
1285 /// vm.set_global("answer", 42).unwrap();
1286 /// vm.set_global("ratio", 0.5_f64).unwrap();
1287 /// vm.set_global("hello", "world").unwrap();
1288 /// let r = vm.eval("return answer, ratio, hello").unwrap();
1289 /// assert_eq!(r.len(), 3);
1290 /// ```
1291 pub fn set_global<V: crate::vm::IntoValue>(
1292 &mut self,
1293 name: &str,
1294 v: V,
1295 ) -> Result<(), LuaError> {
1296 let v = v.into_value(self);
1297 let k = Value::Str(self.heap.intern(name.as_bytes()));
1298 // 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).
1299 unsafe { self.globals.as_mut() }.set(&mut self.heap, k, v)?;
1300 self.heap
1301 .barrier_back(self.globals.as_ptr() as *mut crate::runtime::heap::GcHeader);
1302 Ok(())
1303 }
1304
1305 /// Backward write barrier shorthand for native lib code: demote `t` from
1306 /// BLACK back to gray so the next propagate step re-traces its fields.
1307 /// No-op outside Propagate (parent is never BLACK at mutation time).
1308 pub(crate) fn barrier_back_table(&mut self, t: Gc<Table>) {
1309 self.heap
1310 .barrier_back(t.as_ptr() as *mut crate::runtime::heap::GcHeader);
1311 }
1312
1313 /// Forward write barrier shorthand: a closed upvalue is a single-slot
1314 /// container — `barrier_forward` is cheaper than `barrier_back` here.
1315 /// No-op outside Propagate.
1316 pub(crate) fn barrier_forward_upvalue(&mut self, uv: Gc<Upvalue>, child: Value) {
1317 self.heap
1318 .barrier_forward(uv.as_ptr() as *mut crate::runtime::heap::GcHeader, child);
1319 }
1320
1321 /// Register a MacroLua macro under `name`. Inert
1322 /// under non-MacroLua dialects (the macro is stored but the load
1323 /// path only consults the registry when
1324 /// `self.version == LuaVersion::MacroLua`).
1325 ///
1326 /// `name` is stored without the leading `@` — source code writes
1327 /// `@double(x)` to invoke a macro registered as `"double"`.
1328 pub fn define_macro(&mut self, name: &str, m: Box<dyn crate::frontend::macro_expander::Macro>) {
1329 self.macro_registry.register(name, m);
1330 }
1331
1332 /// Drop all MacroLua macros (built-in + custom).
1333 /// Mostly useful for tests.
1334 pub fn clear_macros(&mut self) {
1335 self.macro_registry.clear();
1336 }
1337
1338 /// PUC `luaL_loadfilex`: compile the file `name` (standard input when
1339 /// `None`, named `stdin`) into a function. A first line starting with
1340 /// `#` is skipped; `mode` (`"t"`, `"b"`, `"bt"`, `None` for both)
1341 /// limits the chunk to text and/or binary. The error is the message
1342 /// PUC's function leaves: `cannot open <name>: <reason>` when the file
1343 /// cannot be read, or the positioned syntax error.
1344 pub fn load_file(
1345 &mut self,
1346 name: Option<&[u8]>,
1347 mode: Option<&[u8]>,
1348 ) -> Result<Value, LuaError> {
1349 crate::vm::lib_os_io::load_path(self, name, mode).map_err(LuaError)
1350 }
1351
1352 /// PUC `luaL_loadbufferx`: compile `src` under `chunkname`, the chunk
1353 /// kind limited by `mode` as in [`Vm::load_file`]. A syntax error comes
1354 /// back as its positioned message (`<chunk id>:<line>: <message>`), the
1355 /// string `load` returns.
1356 pub fn load_buffer(
1357 &mut self,
1358 src: &[u8],
1359 chunkname: &[u8],
1360 mode: Option<&[u8]>,
1361 ) -> Result<Value, LuaError> {
1362 crate::vm::lib_os_io::load_chunk(self, src, chunkname, mode).map_err(LuaError)
1363 }
1364
1365 /// Parse + compile a chunk and close it over the globals table.
1366 pub fn load(&mut self, src: &[u8], chunkname: &[u8]) -> Result<Gc<LuaClosure>, SyntaxError> {
1367 // Reject oversize input *before* handing the parser/lexer a
1368 // potentially multi-GB slice. The PUC-shaped `not enough memory`
1369 // message keeps `heavy.lua::loadrep` compatibility: that test
1370 // accepts either `string length overflow` or `not enough memory`
1371 // as the failure mode for a feeder loop that outruns the host
1372 // allocator. See `set_loader_input_budget`.
1373 if src.len() > self.loader_input_budget {
1374 return Err(SyntaxError {
1375 line: 0,
1376 msg: b"not enough memory".to_vec(),
1377 });
1378 }
1379 // a precompiled (binary) chunk is undumped; source is parsed + compiled
1380 let is_bytecode = crate::vm::dump::is_binary_chunk(src);
1381 if is_bytecode && !self.bytecode_loading {
1382 return Err(SyntaxError {
1383 line: 0,
1384 msg: b"attempt to load a binary chunk (bytecode loading disabled)".to_vec(),
1385 });
1386 }
1387 let proto = if is_bytecode {
1388 let allow_puc = self.puc_bytecode_loading;
1389 crate::vm::dump::undump_named(src, &mut self.heap, self.version, allow_puc, chunkname)
1390 .map_err(SyntaxError::unpositioned)?
1391 } else if self.version.is_macro_lua() {
1392 // MacroLua dialect: drain the lexer into a
1393 // token vec, run the macro expander pre-pass against the
1394 // per-Vm registry, then hand the rewritten stream to
1395 // `parse_tokens`. The AST + compiler are dialect-agnostic
1396 // because by this point all `@`/quote tokens are gone.
1397 let mut lexer = crate::frontend::lexer::Lexer::new(src, self.version);
1398 let mut raw: Vec<crate::frontend::token::TokenInfo> = Vec::new();
1399 loop {
1400 let t = lexer.next_token()?;
1401 let eof = matches!(t.tok, crate::frontend::token::Token::Eof);
1402 raw.push(t);
1403 if eof {
1404 break;
1405 }
1406 }
1407 // Drop the trailing Eof — expander operates on the body and
1408 // `parse_tokens` reinserts Eof when it runs out of tokens.
1409 raw.pop();
1410 let expanded = self.macro_registry.expand(raw)?;
1411 let depth = self.c_depth + self.pcall_depth;
1412 let parsed =
1413 crate::frontend::parser::parse_tokens_at_depth(expanded, src, self.version, depth)?;
1414 crate::compiler::compile_parsed(
1415 &parsed.chunk,
1416 &parsed.end_lines,
1417 self.version,
1418 chunkname,
1419 &mut self.heap,
1420 )?
1421 } else {
1422 // PUC's `nCcalls` counts protected calls as well
1423 let depth = self.c_depth + self.pcall_depth;
1424 let parsed = crate::frontend::parser::parse_at_depth(src, self.version, depth)?;
1425 crate::compiler::compile_parsed(
1426 &parsed.chunk,
1427 &parsed.end_lines,
1428 self.version,
1429 chunkname,
1430 &mut self.heap,
1431 )?
1432 };
1433 // PUC `lua_load` (lapi.c) only seeds the loaded closure's first
1434 // upvalue with the globals table when the closure has *exactly* one
1435 // upvalue — that's the main-chunk `_ENV` case. A dumped non-main
1436 // function with two-or-more upvalues keeps every cell at nil; the
1437 // host must use `debug.setupvalue` to wire them up. 5.2 calls.lua
1438 // :293's `assert(x() == nil)` pins this contract.
1439 let n = proto.upvals.len();
1440 let mut ups: Vec<Gc<Upvalue>> = Vec::with_capacity(n.max(1));
1441 if n == 0 {
1442 // synthetic main chunk has no declared upvalues, but the engine
1443 // still expects at least one cell so the host can probe via
1444 // `debug.upvalueid` etc. Match the historical luna shape.
1445 ups.push(
1446 self.heap
1447 .new_upvalue(UpvalState::Closed(Value::Table(self.globals))),
1448 );
1449 } else if n == 1 {
1450 ups.push(
1451 self.heap
1452 .new_upvalue(UpvalState::Closed(Value::Table(self.globals))),
1453 );
1454 } else {
1455 for _ in 0..n {
1456 ups.push(self.heap.new_upvalue(UpvalState::Closed(Value::Nil)));
1457 }
1458 }
1459 Ok(self.heap.new_closure(proto, ups.into_boxed_slice()))
1460 }
1461
1462 /// Compile and run `src` as an anonymous chunk; return its results.
1463 /// Source name in the traceback is `"=eval"`. Syntax errors are
1464 /// surfaced as `LuaError` carrying the formatted PUC-style message
1465 /// (interned through the heap so the error value composes with
1466 /// `pcall` / `error_text` like any runtime error).
1467 pub fn eval(&mut self, src: &str) -> Result<Vec<Value>, LuaError> {
1468 self.eval_chunk(src, "=eval")
1469 }
1470
1471 /// Render an error value for messages/tests. Non-string errors —
1472 /// `error({code=…})`, `error(42)`, etc. — collapse to a type tag
1473 /// (`"(error object is a table value)"`); embedders that need
1474 /// structured payloads should inspect `e.0` directly. Errors whose
1475 /// text starts with `"native panic:"` indicate a Rust panic
1476 /// crossed `catch_unwind` — the Vm may be inconsistent and should
1477 /// be dropped (do not reuse).
1478 pub fn error_text(&self, e: &LuaError) -> String {
1479 match e.0 {
1480 Value::Str(s) => String::from_utf8_lossy(s.as_bytes()).into_owned(),
1481 v => format!("(error object is a {} value)", v.type_name()),
1482 }
1483 }
1484
1485 /// Render an error value the way PUC's standalone `msghandler`
1486 /// does (lua.c): strings pass through, numbers stringify, and any
1487 /// other object is given a chance at its `__tostring` metamethod
1488 /// (the result must be a string) before collapsing to the
1489 /// `"(error object is a … value)"` tag. Needs `&mut self` because
1490 /// `__tostring` runs arbitrary Lua — `error_text` remains the
1491 /// non-executing variant.
1492 pub fn error_display(&mut self, e: &LuaError) -> String {
1493 match e.0 {
1494 Value::Str(s) => String::from_utf8_lossy(s.as_bytes()).into_owned(),
1495 v @ (Value::Int(_) | Value::Float(_)) => {
1496 String::from_utf8_lossy(&self.tostring_basic(v)).into_owned()
1497 }
1498 v => {
1499 let mm = self.get_mm(v, Mm::ToString);
1500 if !mm.is_nil()
1501 && let Ok(r) = self.call_value(mm, &[v])
1502 && let Some(Value::Str(s)) = r.first()
1503 {
1504 return String::from_utf8_lossy(s.as_bytes()).into_owned();
1505 }
1506 format!("(error object is a {} value)", v.type_name())
1507 }
1508 }
1509 }
1510
1511 /// Call `f` with `args` in protected mode with the message handler
1512 /// `msgh`: PUC `lua_pcall(L, nargs, LUA_MULTRET, msgh)` made from a C
1513 /// function of the host's, as lua.c's `docall` does from `pmain`.
1514 ///
1515 /// `msgh` runs where the error was raised, before the stack unwinds, so
1516 /// it can take a traceback of the failing call ([`Vm::traceback`]); an
1517 /// error inside it calls it again with the new error, as in PUC. The
1518 /// returned error carries what the handler returned.
1519 ///
1520 /// The call counts as one C level on the stack, the host function
1521 /// making it: `debug.getinfo` finds it below `f`, and a traceback taken
1522 /// inside ends with `[C]: in ?` (`[C]: ?` in 5.1).
1523 pub fn call_value_with_handler(
1524 &mut self,
1525 f: Value,
1526 args: &[Value],
1527 msgh: Value,
1528 ) -> Result<Vec<Value>, LuaError> {
1529 let level = self.native(crate::vm::builtins::nat_host_xpcall);
1530 let mut call_args = Vec::with_capacity(args.len() + 2);
1531 call_args.push(f);
1532 call_args.push(msgh);
1533 call_args.extend_from_slice(args);
1534 let mut results = self.call_value(level, &call_args)?;
1535 // the protected call's `true, results...` or `false, handled error`
1536 if results.first().is_some_and(|ok| ok.truthy()) {
1537 results.remove(0);
1538 Ok(results)
1539 } else {
1540 Err(LuaError(results.get(1).copied().unwrap_or(Value::Nil)))
1541 }
1542 }
1543
1544 /// PUC `luaL_getmetafield`: the field `event` of `v`'s metatable, read
1545 /// raw; nil when `v` has no metatable or the field is absent.
1546 pub fn metafield(&mut self, v: Value, event: &str) -> Value {
1547 match self.metatable_of(v) {
1548 Some(mt) => {
1549 let key = Value::Str(self.heap.intern(event.as_bytes()));
1550 mt.get(key)
1551 }
1552 None => Value::Nil,
1553 }
1554 }
1555
1556 /// Call any callable value from the host (or from natives like pcall).
1557 pub fn call_value(&mut self, f: Value, args: &[Value]) -> Result<Vec<Value>, LuaError> {
1558 // host-level entry (no enclosing exec): drop any error state from a
1559 // prior call that propagated uncaught (`error_traceback` would
1560 // otherwise leak into the next debug.traceback call).
1561 if self.public_call_depth == 0 {
1562 self.error_traceback = None;
1563 }
1564 self.public_call_depth += 1;
1565 // JIT fast path. A host call with no args targeting a Lua
1566 // chunk whose body fits the int-arith whitelist short-circuits
1567 // the whole interpreter dispatch and runs straight through the
1568 // mmap'd native code. The lookup is one Cell::get + one match —
1569 // the slow path (compile attempt on first reach) is paid once per
1570 // Proto.
1571 if args.is_empty()
1572 && let Value::Closure(cl) = f
1573 && let Some(vs) = self.try_jit_call(cl)
1574 {
1575 self.public_call_depth -= 1;
1576 return Ok(vs);
1577 }
1578 let r = self.call_value_impl(f, args, true);
1579 self.public_call_depth -= 1;
1580 r
1581 }
1582
1583 /// Peek/populate the Proto's JIT cache slot, returning
1584 /// `Some(values)` when the cached native fn is callable for a
1585 /// zero-arg call. (Non-zero-arg dispatch is handled by
1586 /// `try_jit_call_op` from inside `begin_call`.)
1587 fn try_jit_call(&mut self, cl: Gc<LuaClosure>) -> Option<Vec<Value>> {
1588 use crate::runtime::function::JitProtoState;
1589 if !self.jit.enabled {
1590 return None;
1591 }
1592 let proto = cl.proto;
1593 if let JitProtoState::Untried = proto.jit.get() {
1594 self.populate_jit_cache(proto);
1595 }
1596 match proto.jit.get() {
1597 JitProtoState::Compiled {
1598 entry,
1599 num_args: 0,
1600 returns_one,
1601 arg_float_mask: _,
1602 arg_table_mask: _,
1603 ret_is_float,
1604 ret_is_table,
1605 } => {
1606 // 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.
1607 let f: crate::jit::IntChunkFn = unsafe { std::mem::transmute(entry) };
1608 // Install the active Vm + closure
1609 // for any Rust helper the JIT'd code may call (e.g.
1610 // `luna_jit_new_table`, `luna_jit_upval_get`) via
1611 // cranelift `Linkage::Import`. RAII clear on return.
1612 // Chunks with no upvalue reads don't touch the closure
1613 // slot, paying nothing.
1614 // Route through chunk_compiler so
1615 // the NullJitBackend path stays inert. Raw-ptr arg
1616 // avoids the &mut self borrow conflict against the
1617 // shared self.jit.chunk_compiler read.
1618 let vm_ptr: *mut Vm = self;
1619 let _jit_vm_guard = self.jit.chunk_compiler.enter(vm_ptr, Some(cl));
1620 // 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).
1621 let r = unsafe { f() };
1622 drop(_jit_vm_guard);
1623 // A JIT helper may have detected a metatable
1624 // on a table operand and parked a deopt request here.
1625 // Discard the sentinel value and return None so the caller
1626 // re-runs the call through the interpreter, which honours
1627 // __index/__newindex.
1628 if self.jit.pending_err.take().is_some() {
1629 return None;
1630 }
1631 Some(if returns_one {
1632 let v = if ret_is_float {
1633 Value::Float(f64::from_bits(r as u64))
1634 } else if ret_is_table {
1635 Value::Table(crate::runtime::Gc::from_ptr(
1636 r as *mut crate::runtime::Table,
1637 ))
1638 } else {
1639 Value::Int(r)
1640 };
1641 vec![v]
1642 } else {
1643 Vec::new()
1644 })
1645 }
1646 // Non-zero-arg Compiled state: call_value's empty-args
1647 // fast path can't drive it. Op::Call handles those.
1648 JitProtoState::Compiled { .. } | JitProtoState::Failed | JitProtoState::Untried => None,
1649 }
1650 }
1651
1652 /// Populate the cache slot. Flips `Untried` to either
1653 /// `Compiled { … }` or `Failed`; idempotent on already-populated
1654 /// states (call sites guard with a get before invoking).
1655 ///
1656 /// Consults a thread-local cross-`Vm` cache keyed by a hash of
1657 /// `proto.code`. Compiled artefacts live in the thread-local
1658 /// `JITModule` so their mmap pages outlive the `Vm`; subsequent
1659 /// `Vm`s loading the same source skip the cranelift compile step
1660 /// entirely.
1661 fn populate_jit_cache(&mut self, proto: Gc<crate::runtime::function::Proto>) {
1662 use crate::runtime::function::JitProtoState;
1663 let version = self.version();
1664 let pre53 = version <= crate::version::LuaVersion::Lua53;
1665 // 5.1 and 5.2 have no Int subtype (all numbers
1666 // are Float). The JIT's `GetUpval` ValueRead path uses this
1667 // to default-pin upvalue reads to Float without a tag check.
1668 let float_only = version <= crate::version::LuaVersion::Lua52;
1669 // Split-borrow JitState so the
1670 // trait method can take `&mut dyn JitStorage` without
1671 // double-borrowing self.jit.
1672 let jit = &mut self.jit;
1673 let storage: &mut dyn crate::jit::JitStorage = jit.storage.as_mut();
1674 match jit
1675 .chunk_compiler
1676 .try_compile(storage, proto, pre53, float_only)
1677 {
1678 crate::jit::CompileResult::Compiled {
1679 entry,
1680 num_args,
1681 returns_one,
1682 arg_float_mask,
1683 arg_table_mask,
1684 ret_is_float,
1685 ret_is_table,
1686 } => {
1687 proto.jit.set(JitProtoState::Compiled {
1688 entry,
1689 num_args,
1690 returns_one,
1691 arg_float_mask,
1692 arg_table_mask,
1693 ret_is_float,
1694 ret_is_table,
1695 });
1696 }
1697 crate::jit::CompileResult::Skipped => {
1698 proto.jit.set(JitProtoState::Failed);
1699 }
1700 }
1701 }
1702
1703 /// `Op::Call` JIT fast path. Run inside `begin_call`
1704 /// before `push_frame`. Returns `true` when the call was handled
1705 /// in-place (no new Lua frame). Constraints: every arg slot must
1706 /// be `Value::Int`, the cached arity must match the call site's
1707 /// `nargs`, the host wanted-count `wanted` is honoured by
1708 /// `finish_results`. Also bails when a debug hook is armed —
1709 /// JIT'd code does not fire line / call / return hooks, so any
1710 /// active hook makes the interpreter the source of truth.
1711 fn try_jit_call_op(
1712 &mut self,
1713 cl: Gc<LuaClosure>,
1714 func_slot: u32,
1715 nargs: u32,
1716 wanted: i32,
1717 ) -> bool {
1718 use crate::runtime::function::JitProtoState;
1719 if !self.jit.enabled {
1720 return false;
1721 }
1722 // Any active debug hook means the interpreter has to run the
1723 // call so the hook gets the expected events.
1724 if self.hook.func.is_some() || self.hook.rust_func.is_some() {
1725 return false;
1726 }
1727 let proto = cl.proto;
1728 if let JitProtoState::Untried = proto.jit.get() {
1729 self.populate_jit_cache(proto);
1730 }
1731 let JitProtoState::Compiled {
1732 entry,
1733 num_args,
1734 returns_one,
1735 arg_float_mask,
1736 arg_table_mask,
1737 ret_is_float,
1738 ret_is_table,
1739 } = proto.jit.get()
1740 else {
1741 return false;
1742 };
1743 if num_args as u32 != nargs {
1744 return false;
1745 }
1746 // Pack args into i64 bit-patterns per the per-slot expected
1747 // kind. A Float-typed slot accepts Value::Float verbatim (and on
1748 // 5.1/5.2 promotes Value::Int(x) via i64 → f64); a Table-typed slot
1749 // accepts only Value::Table and passes the raw Gc ptr; an
1750 // Int-typed slot accepts only Value::Int. Any other shape
1751 // bails to the interpreter so the call's actual dynamics
1752 // (metamethod dispatch / type-coerce) take over.
1753 let mut args: [i64; crate::jit::MAX_JIT_ARITY as usize] =
1754 [0; crate::jit::MAX_JIT_ARITY as usize];
1755 // From 5.3 an integer is its own subtype: turned into a float for
1756 // a float-typed parameter, it would come back out (returned,
1757 // stored, printed) as a float. Only 5.1/5.2, where every number
1758 // is a float, may convert it.
1759 let int_as_float = self.version() <= crate::version::LuaVersion::Lua52;
1760 for i in 0..num_args as usize {
1761 let v = self.stack[(func_slot + 1) as usize + i];
1762 let want_float = (arg_float_mask >> i) & 1 == 1;
1763 let want_table = (arg_table_mask >> i) & 1 == 1;
1764 args[i] = match (want_table, want_float, v) {
1765 (true, _, Value::Table(t)) => t.as_ptr() as i64,
1766 (false, false, Value::Int(x)) => x,
1767 (false, true, Value::Float(f)) => f.to_bits() as i64,
1768 (false, true, Value::Int(x)) if int_as_float => (x as f64).to_bits() as i64,
1769 _ => return false,
1770 };
1771 }
1772 // Vm + closure pin for helpers, routed through chunk_compiler;
1773 // see the matching guard in `try_jit_call`.
1774 let vm_ptr: *mut Vm = self;
1775 let _jit_vm_guard = self.jit.chunk_compiler.enter(vm_ptr, Some(cl));
1776 // 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.
1777 let r = unsafe {
1778 match num_args {
1779 0 => (std::mem::transmute::<*const u8, crate::jit::IntChunkFn>(entry))(),
1780 1 => (std::mem::transmute::<*const u8, crate::jit::IntFn1>(entry))(args[0]),
1781 2 => {
1782 (std::mem::transmute::<*const u8, crate::jit::IntFn2>(entry))(args[0], args[1])
1783 }
1784 3 => (std::mem::transmute::<*const u8, crate::jit::IntFn3>(entry))(
1785 args[0], args[1], args[2],
1786 ),
1787 4 => (std::mem::transmute::<*const u8, crate::jit::IntFn4>(entry))(
1788 args[0], args[1], args[2], args[3],
1789 ),
1790 _ => unreachable!("MAX_JIT_ARITY enforces num_args <= 4"),
1791 }
1792 };
1793 drop(_jit_vm_guard);
1794 // See matching path in `try_jit_call`. A helper
1795 // flagged a metatable on a table operand; bail to the interpreter
1796 // so `push_frame` runs the call from scratch.
1797 if self.jit.pending_err.take().is_some() {
1798 return false;
1799 }
1800 // Write result at func_slot, replacing the closure value, then
1801 // hand to finish_results to pad/truncate per the call site's
1802 // `wanted` count.
1803 if returns_one {
1804 let v = if ret_is_float {
1805 Value::Float(f64::from_bits(r as u64))
1806 } else if ret_is_table {
1807 Value::Table(crate::runtime::Gc::from_ptr(
1808 r as *mut crate::runtime::Table,
1809 ))
1810 } else {
1811 Value::Int(r)
1812 };
1813 self.stack[func_slot as usize] = v;
1814 self.finish_results(func_slot, 1, wanted);
1815 } else {
1816 self.finish_results(func_slot, 0, wanted);
1817 }
1818 true
1819 }
1820
1821 /// `call_value` with control over the `from_c` debug boundary. A `__close`
1822 /// handler runs *within* the closing Lua frame's activation (PUC luaF_close
1823 /// invokes it inside that ci), so it is called with `from_c = false`: its
1824 /// debug parent is the closing function, not a synthetic C level.
1825 fn call_value_impl(
1826 &mut self,
1827 f: Value,
1828 args: &[Value],
1829 from_c: bool,
1830 ) -> Result<Vec<Value>, LuaError> {
1831 if self.c_depth >= MAX_C_DEPTH {
1832 // PUC `luaE_checkcstack`: at the limit the call fails; an xpcall
1833 // handler running on the error gets a tenth more room before its
1834 // own failure is "error in error handling"
1835 if self.msgh_depth == 0 {
1836 return Err(self.runerror("C stack overflow"));
1837 }
1838 if self.c_depth >= MAX_C_DEPTH / 10 * 11 {
1839 return Err(self.plain_err("error in error handling"));
1840 }
1841 }
1842 self.c_depth += 1;
1843 let func_slot = self.stack.len() as u32;
1844 self.stack.push(f);
1845 self.stack.extend_from_slice(args);
1846 self.top = self.stack.len() as u32;
1847 let r = self.call_at(func_slot, args.len() as u32, from_c);
1848 self.c_depth -= 1;
1849 if r.is_err()
1850 && self.yielding.is_none()
1851 && self.terminating.is_none()
1852 && !self.host_yield_pending
1853 && self.pending_async_native_fut.is_none()
1854 {
1855 // A `coroutine.yield` in flight raises a sentinel error to unwind the
1856 // Rust stack, but the suspended coroutine's frames/registers (which
1857 // sit at/above `func_slot`) must survive for the next resume — so we
1858 // only truncate on a real error. A self-close termination is in the
1859 // same boat: the dying thread's state is discarded wholesale.
1860 // A `host_yield_pending` cooperative yield is in
1861 // the same boat as `yielding`: the next `EvalFuture::poll`
1862 // resumes the same call, so the in-flight frames must
1863 // survive.
1864 self.stack.truncate(func_slot as usize);
1865 self.top = func_slot;
1866 }
1867 r
1868 }
1869
1870 /// Invoke `f` with the running thread marked non-yieldable for the duration
1871 /// (PUC `luaD_callnoyield`): a `coroutine.yield` inside `f` hits the C-call
1872 /// boundary and errors instead of suspending. Used by library callbacks
1873 /// (sort comparator, gsub replacement) that run via synchronous Rust
1874 /// recursion and so could not be re-entered after a yield.
1875 pub(crate) fn call_noyield(
1876 &mut self,
1877 f: Value,
1878 args: &[Value],
1879 ) -> Result<Vec<Value>, LuaError> {
1880 self.nny += 1;
1881 let r = self.call_value(f, args);
1882 self.nny -= 1;
1883 r
1884 }
1885
1886 // ---- coroutines ----
1887
1888 pub(crate) fn new_coro(&mut self, body: Value) -> Gc<Coro> {
1889 // The new coroutine inherits the creating thread's current globals
1890 // (PUC `lua_newthread`: the new state copies `g->mainthread`'s
1891 // `l_gt`). `Vm.globals` always reflects the live thread, so reading
1892 // it here picks the creator regardless of which coro is running.
1893 self.heap.new_coro(body, self.globals)
1894 }
1895
1896 /// Is `t` the thread whose context is currently live in the VM?
1897 pub(crate) fn is_current_thread(&self, t: Option<Gc<Coro>>) -> bool {
1898 match (self.current, t) {
1899 (None, None) => true,
1900 (Some(a), Some(b)) => a.ptr_eq(b),
1901 _ => false,
1902 }
1903 }
1904
1905 /// Read an open-upvalue slot from its owning thread's stack (the live VM
1906 /// stack if that thread is current, else its saved context).
1907 #[doc(hidden)]
1908 pub fn read_slot(&self, slot: u32, thread: Option<Gc<Coro>>) -> Value {
1909 let s = slot as usize;
1910 if self.is_current_thread(thread) {
1911 self.stack[s]
1912 } else {
1913 match thread {
1914 Some(co) => co.stack[s],
1915 None => self.main_ctx.as_ref().expect("main context").stack[s],
1916 }
1917 }
1918 }
1919
1920 fn write_slot(&mut self, slot: u32, thread: Option<Gc<Coro>>, v: Value) {
1921 let s = slot as usize;
1922 if self.is_current_thread(thread) {
1923 self.stack[s] = v;
1924 } else {
1925 match thread {
1926 Some(co) => {
1927 // 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).
1928 unsafe { co.as_mut() }.stack[s] = v;
1929 // co.stack is traced by Coro::trace; demote co back to
1930 // gray so propagate re-traces this slot if it was
1931 // already black.
1932 self.heap
1933 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
1934 }
1935 None => self.main_ctx.as_mut().expect("main context").stack[s] = v,
1936 }
1937 }
1938 }
1939
1940 /// Whether `co` is the main thread's identity object.
1941 pub(crate) fn is_main_coro(&self, co: Gc<Coro>) -> bool {
1942 self.main_coro.is_some_and(|m| m.ptr_eq(co))
1943 }
1944
1945 /// The status of `co` from the caller's view. The main thread's identity
1946 /// object has no stored status — it is "running" when nothing else runs,
1947 /// else "normal" (it resumed the active coroutine).
1948 pub(crate) fn effective_coro_status(&self, co: Gc<Coro>) -> CoroStatus {
1949 if self.is_main_coro(co) {
1950 if self.current.is_none() {
1951 CoroStatus::Running
1952 } else {
1953 CoroStatus::Normal
1954 }
1955 } else {
1956 co.status
1957 }
1958 }
1959
1960 /// `coroutine.close` (PUC `lua_closethread`): run the suspended coroutine's
1961 /// pending to-be-closed `__close` handlers, then mark it dead and drop its
1962 /// context. Handlers see the coroutine's death error (if it died by error)
1963 /// or nil; an error they raise propagates out. `Ok(Some(e))` means it died
1964 /// with error `e` and no handler overrode it; `Err` means a handler raised.
1965 pub(crate) fn close_coro(&mut self, co: Gc<Coro>) -> Result<Option<Value>, LuaError> {
1966 // re-entrant close: a __close handler closed its own coroutine while the
1967 // outer close is mid-flight (its context is live). Report success and let
1968 // the outer close finish — re-entering the swap would corrupt the stack.
1969 if self.current.is_some_and(|c| c.ptr_eq(co)) {
1970 return Ok(None);
1971 }
1972 // A chain of coroutines whose `__close` handlers each close the previous
1973 // one recurses on the C stack (PUC `luaD_callnoyield` in `lua_closethread`).
1974 // The calling handler's `call_value` has already pushed `c_depth` to the
1975 // cap, so here it reads as full first — report PUC's "C stack overflow"
1976 // before the next handler call would surface the plainer "stack overflow".
1977 if self.c_depth >= MAX_C_DEPTH {
1978 return Err(self.rt_err("C stack overflow"));
1979 }
1980 // 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).
1981 let death_err = unsafe { co.as_mut() }.error_value.take();
1982 // swap the caller's live context out (into a GC-rooted home) and the
1983 // coroutine's in, mirroring resume_coro, so the __close handlers run on
1984 // the coroutine's stack while everything stays rooted.
1985 let resumer = self.current;
1986 let rctx = self.take_ctx();
1987 match resumer {
1988 Some(r) => {
1989 // 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).
1990 let m = unsafe { r.as_mut() };
1991 m.stack = rctx.stack;
1992 m.frames = rctx.frames;
1993 m.frame_ccmt = rctx.frame_ccmt;
1994 m.open_upvals = rctx.open_upvals;
1995 m.tbc = rctx.tbc;
1996 m.top = rctx.top;
1997 m.pcall_depth = rctx.pcall_depth;
1998 }
1999 None => self.main_ctx = Some(rctx),
2000 }
2001 self.load_coro_ctx(co);
2002 self.current = Some(co);
2003 // PUC `luaE_resetthread` closes with no message handler, whatever
2004 // xpcall the coroutine was suspended in
2005 let natives_base = std::mem::replace(&mut self.natives_base, self.running_natives.len());
2006 let msgh_floor = std::mem::replace(&mut self.msgh_floor, self.frames.len());
2007 let result = self.close_slots(0, death_err);
2008 self.natives_base = natives_base;
2009 self.msgh_floor = msgh_floor;
2010 // discard the (now-closed) coroutine context and restore the caller
2011 let _ = self.take_ctx();
2012 match resumer {
2013 Some(r) => {
2014 self.load_coro_ctx(r);
2015 self.current = Some(r);
2016 }
2017 None => {
2018 let m = self.main_ctx.take().expect("main context saved");
2019 self.put_ctx(m);
2020 self.current = None;
2021 }
2022 }
2023 {
2024 // 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).
2025 let m = unsafe { co.as_mut() };
2026 m.status = CoroStatus::Dead;
2027 m.stack = Vec::new();
2028 m.frames = Vec::new();
2029 m.frame_ccmt = Vec::new();
2030 m.open_upvals = Vec::new();
2031 m.tbc = Vec::new();
2032 m.top = 0;
2033 m.pcall_depth = 0;
2034 m.resume_at = None;
2035 m.error_value = None;
2036 m.error_traceback = None;
2037 m.error_levels = None;
2038 }
2039 result.map(|()| death_err)
2040 }
2041
2042 /// `coroutine.running`: the running thread plus whether it is the main one.
2043 pub(crate) fn running_thread(&self) -> (Value, bool) {
2044 match self.current {
2045 Some(co) => (Value::Coro(co), false),
2046 None => (Value::Coro(self.main_coro.expect("main coro")), true),
2047 }
2048 }
2049
2050 /// `coroutine.isyieldable([co])`: whether `co` (default: the running
2051 /// thread) can yield. The main thread never can; any other coroutine can
2052 /// unless it is dead.
2053 pub(crate) fn is_yieldable(&self, co: Option<Gc<Coro>>) -> bool {
2054 match co {
2055 Some(c) => !self.main_coro.is_some_and(|m| m.ptr_eq(c)) && c.status != CoroStatus::Dead,
2056 // the running thread can yield only outside any non-yieldable C call
2057 None => self.current.is_some() && self.nny == 0,
2058 }
2059 }
2060
2061 /// Why `coroutine.yield` may not suspend the running thread right now, as a
2062 /// PUC error message — `None` if it may. Distinguishes "not in a coroutine"
2063 /// from "inside an unyieldable C call" (sort/gsub callback).
2064 pub(crate) fn yield_barrier(&self) -> Option<&'static str> {
2065 // 5.1's pcall/xpcall are plain C calls (no continuations), so a yield
2066 // below one crosses the boundary like any other; 5.1 also has a single
2067 // wording for every case, the main thread included.
2068 // 5.1 also calls every metamethod and generic-for iterator through
2069 // `luaD_call`, which counts as a C level, so a yield from inside one
2070 // is refused as well.
2071 if self.version <= LuaVersion::Lua51 {
2072 let inside_call = self.frames.iter().enumerate().any(|(i, f)| match f {
2073 CallFrame::Cont(nc) => matches!(nc.kind, ContKind::Meta(_)),
2074 CallFrame::Lua(fr) => {
2075 fr.tm.is_some()
2076 || (i > 0
2077 && self.frames[i - 1].lua().is_some_and(|c| {
2078 let pc = (c.pc as usize).wrapping_sub(1);
2079 c.closure
2080 .proto
2081 .code
2082 .get(pc)
2083 .is_some_and(|ins| ins.op() == Op::TForCall)
2084 }))
2085 }
2086 });
2087 if self.current.is_none() || self.nny > 0 || self.pcall_depth > 0 || inside_call {
2088 return Some("attempt to yield across metamethod/C-call boundary");
2089 }
2090 return None;
2091 }
2092 if self.current.is_none() {
2093 Some("attempt to yield from outside a coroutine")
2094 } else if self.nny > 0 {
2095 Some("attempt to yield across a C-call boundary")
2096 } else {
2097 None
2098 }
2099 }
2100
2101 /// The coroutine whose context is currently live (`None` on the main thread).
2102 pub(crate) fn current_coro(&self) -> Option<Gc<Coro>> {
2103 self.current
2104 }
2105
2106 /// `coroutine.close()` on the *running* thread (PUC 5.5 close-self): run all
2107 /// its pending `__close` handlers, then signal termination. The handlers run
2108 /// here, in place, with the thread still non-yieldable (a yield in one hits
2109 /// the C-call boundary). The returned sentinel unwinds the Rust stack the
2110 /// way a yield does — `exec_with` propagates it past any protecting pcall
2111 /// rather than letting `unwind` catch it — and `resume_coro` turns it into a
2112 /// clean death (or, if a handler raised, the coroutine's error).
2113 pub(crate) fn close_running(&mut self) -> LuaError {
2114 let death = match self.close_slots(0, None) {
2115 Ok(()) => None,
2116 Err(e) => Some(e.0),
2117 };
2118 self.terminating = Some(death);
2119 LuaError(Value::Nil)
2120 }
2121
2122 /// `coroutine.status` as seen by the caller.
2123 pub(crate) fn coro_status_str(&self, co: Gc<Coro>) -> &'static str {
2124 match self.effective_coro_status(co) {
2125 CoroStatus::Suspended => "suspended",
2126 CoroStatus::Running => "running",
2127 CoroStatus::Normal => "normal",
2128 CoroStatus::Dead => "dead",
2129 }
2130 }
2131
2132 fn take_ctx(&mut self) -> SavedCtx {
2133 let saved = SavedCtx {
2134 stack: std::mem::take(&mut self.stack),
2135 frames: std::mem::take(&mut self.frames),
2136 frame_ccmt: std::mem::take(&mut self.frame_ccmt),
2137 open_upvals: std::mem::take(&mut self.open_upvals),
2138 tbc: std::mem::take(&mut self.tbc),
2139 top: self.top,
2140 pcall_depth: self.pcall_depth,
2141 hook: self.hook,
2142 globals: self.globals,
2143 };
2144 self.frames_resync(); // frames now empty
2145 saved
2146 }
2147
2148 fn put_ctx(&mut self, c: SavedCtx) {
2149 self.stack = c.stack;
2150 self.frames = c.frames;
2151 self.frame_ccmt = c.frame_ccmt;
2152 self.open_upvals = c.open_upvals;
2153 self.tbc = c.tbc;
2154 self.top = c.top;
2155 self.pcall_depth = c.pcall_depth;
2156 self.hook = c.hook;
2157 self.globals = c.globals;
2158 self.frames_resync(); // sync shadow to new Vec
2159 }
2160
2161 /// Move a coroutine's saved context into the live VM fields.
2162 fn load_coro_ctx(&mut self, co: Gc<Coro>) {
2163 // 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).
2164 let m = unsafe { co.as_mut() };
2165 self.stack = std::mem::take(&mut m.stack);
2166 self.frames = std::mem::take(&mut m.frames);
2167 self.frame_ccmt = std::mem::take(&mut m.frame_ccmt);
2168 self.open_upvals = std::mem::take(&mut m.open_upvals);
2169 self.tbc = std::mem::take(&mut m.tbc);
2170 self.top = m.top;
2171 self.frames_resync(); // sync shadow to coro's frames
2172 self.pcall_depth = m.pcall_depth;
2173 self.hook = m.hook;
2174 self.globals = m.globals;
2175 }
2176
2177 /// Save the live VM context back into a coroutine object.
2178 fn store_coro_ctx(&mut self, co: Gc<Coro>) {
2179 let c = self.take_ctx();
2180 // 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).
2181 let m = unsafe { co.as_mut() };
2182 m.stack = c.stack;
2183 m.frames = c.frames;
2184 m.frame_ccmt = c.frame_ccmt;
2185 m.open_upvals = c.open_upvals;
2186 m.tbc = c.tbc;
2187 m.top = c.top;
2188 m.pcall_depth = c.pcall_depth;
2189 m.hook = c.hook;
2190 m.globals = c.globals;
2191 // bulk-overwrite of every collectable field traced by Coro::trace:
2192 // demote the coro back to gray so propagate re-traces its new state.
2193 self.heap
2194 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
2195 }
2196
2197 /// `coroutine.resume` core: drive `co` with `args` until it yields, returns
2198 /// or errors. Ok(values) carries yielded or returned values; Err carries an
2199 /// error raised inside the coroutine (the coroutine becomes dead).
2200 pub(crate) fn resume_coro(
2201 &mut self,
2202 co: Gc<Coro>,
2203 args: Vec<Value>,
2204 ) -> Result<Vec<Value>, LuaError> {
2205 match co.status {
2206 CoroStatus::Suspended => {}
2207 CoroStatus::Dead => return Err(self.plain_err("cannot resume dead coroutine")),
2208 _ => return Err(self.plain_err("cannot resume non-suspended coroutine")),
2209 }
2210 if self.c_depth >= MAX_C_DEPTH {
2211 return Err(self.plain_err("C stack overflow"));
2212 }
2213 self.c_depth += 1;
2214 let resumer = self.current;
2215 // save the resumer's live context away
2216 let rctx = self.take_ctx();
2217 match resumer {
2218 Some(r) => {
2219 // 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).
2220 let m = unsafe { r.as_mut() };
2221 m.stack = rctx.stack;
2222 m.frames = rctx.frames;
2223 m.frame_ccmt = rctx.frame_ccmt;
2224 m.open_upvals = rctx.open_upvals;
2225 m.tbc = rctx.tbc;
2226 m.top = rctx.top;
2227 m.pcall_depth = rctx.pcall_depth;
2228 m.globals = rctx.globals;
2229 m.status = CoroStatus::Normal;
2230 m.natives = self.natives_base..self.running_natives.len();
2231 // bulk overwrite of every traced field on r — mirror
2232 // store_coro_ctx's barrier_back so propagate re-traces r.
2233 self.heap
2234 .barrier_back(r.as_ptr() as *mut crate::runtime::heap::GcHeader);
2235 }
2236 None => self.main_ctx = Some(rctx),
2237 }
2238 // swap the coroutine in
2239 self.load_coro_ctx(co);
2240 {
2241 // 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).
2242 let m = unsafe { co.as_mut() };
2243 m.status = CoroStatus::Running;
2244 m.resumer = resumer;
2245 }
2246 // co.resumer is a traced Gc field; barrier_back covers the new
2247 // resumer reference and any future field writes during this call.
2248 self.heap
2249 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
2250 self.current = Some(co);
2251 let resumer_natives_base = self.natives_base;
2252 self.natives_base = self.running_natives.len();
2253 // the coroutine's own frames start a fresh reach for xpcall handlers
2254 let resumer_msgh_floor = std::mem::replace(&mut self.msgh_floor, 0);
2255 let resumer_msgh_running = self.msgh_running.take();
2256 // a coroutine that dies keeps its traceback for `debug.traceback(co)`
2257 let resumer_keeps_traceback = std::mem::replace(&mut self.keep_error_traceback, true);
2258
2259 // drive it
2260 let drive = if co.started {
2261 self.coro_continue(&args)
2262 } else {
2263 // 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).
2264 unsafe { co.as_mut() }.started = true;
2265 self.coro_first(co.body, &args)
2266 };
2267
2268 // classify: a self-close termination or a pending yield each win over
2269 // the (sentinel) error they raised to unwind the Rust stack.
2270 let (outcome, status) = if let Some(death) = self.terminating.take() {
2271 // the coroutine closed itself: it dies now, cleanly or with the
2272 // error a `__close` handler raised.
2273 match death {
2274 Some(e) => {
2275 // 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).
2276 unsafe { co.as_mut() }.error_value = Some(e);
2277 self.heap
2278 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
2279 (Err(LuaError(e)), CoroStatus::Dead)
2280 }
2281 None => (Ok(Vec::new()), CoroStatus::Dead),
2282 }
2283 } else {
2284 match self.yielding.take() {
2285 Some((vals, fslot, nres)) => {
2286 // 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).
2287 unsafe { co.as_mut() }.resume_at = Some((fslot, nres));
2288 (Ok(vals), CoroStatus::Suspended)
2289 }
2290 None => {
2291 // died: a return is clean, an error is remembered so a later
2292 // `coroutine.close` can report it (PUC lua_closethread).
2293 // Keep the error-point traceback (taken by `unwind` before
2294 // popping the failing frames) so `debug.traceback(co)` on
2295 // the dead coroutine still shows the error site, as PUC's
2296 // untouched dead stack does (db.lua :848 family).
2297 if drive.is_err() {
2298 let levels = self.error_traceback.take().unwrap_or_default();
2299 let tb =
2300 crate::vm::callstack::traceback_from_lines(self.version, &levels, 0);
2301 // 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).
2302 let m = unsafe { co.as_mut() };
2303 m.error_traceback = Some(tb);
2304 m.error_levels = Some(levels);
2305 }
2306 if let Err(e) = drive {
2307 // 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).
2308 unsafe { co.as_mut() }.error_value = Some(e.0);
2309 self.heap
2310 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
2311 }
2312 (drive, CoroStatus::Dead)
2313 }
2314 }
2315 };
2316
2317 // save the coroutine's context back and restore the resumer
2318 self.natives_base = resumer_natives_base;
2319 self.msgh_floor = resumer_msgh_floor;
2320 self.msgh_running = resumer_msgh_running;
2321 self.keep_error_traceback = resumer_keeps_traceback;
2322 self.store_coro_ctx(co);
2323 // 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).
2324 unsafe { co.as_mut() }.status = status;
2325 match resumer {
2326 Some(r) => {
2327 self.load_coro_ctx(r);
2328 // 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).
2329 unsafe { r.as_mut() }.status = CoroStatus::Running;
2330 self.current = Some(r);
2331 }
2332 None => {
2333 let m = self.main_ctx.take().expect("main context saved");
2334 self.put_ctx(m);
2335 self.current = None;
2336 }
2337 }
2338 self.c_depth -= 1;
2339 outcome
2340 }
2341
2342 /// First resume: install the body function at slot 0 and run.
2343 fn coro_first(&mut self, body: Value, args: &[Value]) -> Result<Vec<Value>, LuaError> {
2344 self.stack.clear();
2345 self.stack.push(body);
2346 self.stack.extend_from_slice(args);
2347 self.top = self.stack.len() as u32;
2348 match self.begin_call(0, Some(args.len() as u32), -1, true) {
2349 Ok(true) => self.exec_with(1),
2350 Ok(false) => Ok(self.take_results(0)),
2351 Err(e) => Err(e),
2352 }
2353 }
2354
2355 /// Resume after a yield: deliver `args` as the results of the call that
2356 /// yielded, then continue the suspended thread.
2357 fn coro_continue(&mut self, args: &[Value]) -> Result<Vec<Value>, LuaError> {
2358 let (fslot, nres) = self.current.unwrap().resume_at.expect("resume point");
2359 let n = args.len() as u32;
2360 // Restore the full register window of the suspended top frame: a yield
2361 // that unwound through a native (call_value) may have left the stack
2362 // shorter than the frame needs. `base + max_stack` is what push_frame
2363 // allocates; `fslot + n` covers the delivered yield results.
2364 let frame_need = self
2365 .frames
2366 .last()
2367 .and_then(CallFrame::lua)
2368 .map(|f| (f.base + f.closure.proto.max_stack as u32) as usize)
2369 .unwrap_or(0);
2370 let need = frame_need.max((fslot + n) as usize);
2371 if self.stack.len() < need {
2372 self.stack.resize(need, Value::Nil);
2373 }
2374 for (i, &v) in args.iter().enumerate() {
2375 self.stack[fslot as usize + i] = v;
2376 }
2377 self.finish_results(fslot, n, nres);
2378 // the suspended `coroutine.yield` (a C call) now returns its resume
2379 // values: fire the matching "return" hook PUC defers until the resume.
2380 self.hook_return(true, 1, n)?;
2381 self.exec_with(1)
2382 }
2383
2384 /// `coroutine.yield`: suspend the running coroutine, recording where to
2385 /// resume. Errors if called outside a coroutine. Returns a sentinel error
2386 /// that `exec`/`resume_coro` recognise as a yield (never surfaced to Lua).
2387 pub(crate) fn do_yield(&mut self, func_slot: u32, vals: Vec<Value>) -> LuaError {
2388 let nres = self.native_nresults;
2389 self.yielding = Some((vals, func_slot, nres));
2390 // value is irrelevant: resume_coro consults `self.yielding`, not this
2391 LuaError(Value::Nil)
2392 }
2393
2394 /// Install or clear the debug hook on the running thread (`debug.sethook`
2395 /// without a thread argument). Arms the calling frame's `oldpc` to the
2396 /// sethook CALL's own pc (one less than the next-to-execute pc), mirroring
2397 /// PUC `rethook`'s `L->oldpc = pcRel(savedpc, p)` (= savedpc - code - 1) on
2398 /// native return: the very next traceexec compares against the sethook
2399 /// CALL's line. When the install statement and the following statement are
2400 /// on different source lines (db.lua :322), `changedline` fires for that
2401 /// first statement; when they share a line (db.lua :25 wrapper), they do
2402 /// not, so the wrapper line is not re-fired.
2403 pub(crate) fn install_hook(&mut self, hook: HookState) {
2404 self.hook = hook;
2405 if self.hook.line
2406 && let Some(f) = self.frames.last_mut().and_then(CallFrame::lua_mut)
2407 {
2408 f.hook_oldpc = f.pc.saturating_sub(1);
2409 }
2410 }
2411
2412 /// Install a hook on `target` (`None`/current thread → the live VM fields;
2413 /// another, suspended thread → its saved `Coro` state). PUC `debug.sethook`
2414 /// with an optional thread argument.
2415 ///
2416 /// `target == None` means "no explicit thread argument" — PUC binds that
2417 /// to `L` (the running thread). luna's live VM fields (`self.hook`,
2418 /// `self.frames`, `self.stack`) ARE the running thread's state, regardless
2419 /// of whether that's the main thread or a currently-resumed coroutine
2420 /// (save/restore happens at resume/yield boundaries via `load_coro_ctx`/
2421 /// `store_coro_ctx`). So a `None` target should always route to
2422 /// `install_hook` on the live fields. The pre-fix predicate gate
2423 /// `is_current_thread(target)` returned `false` when running inside a
2424 /// coroutine (`self.current = Some(co)`, `target = None` don't match)
2425 /// and silently dropped the hook on the floor — the install happened on
2426 /// no thread at all.
2427 pub(crate) fn set_hook(&mut self, target: Option<Gc<Coro>>, state: HookState) {
2428 if target.is_none() || self.is_current_thread(target) {
2429 self.install_hook(state);
2430 } else if let Some(co) = target {
2431 // 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).
2432 let m = unsafe { co.as_mut() };
2433 m.hook = state;
2434 if state.line
2435 && let Some(f) = m.frames.last_mut().and_then(CallFrame::lua_mut)
2436 {
2437 f.hook_oldpc = u32::MAX;
2438 }
2439 // co.hook.func is a traced Value (Coro::trace covers it); demote
2440 // co back to gray so propagate sees the new hook function.
2441 self.heap
2442 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
2443 }
2444 }
2445
2446 /// The hook state of `target` (`None`/current → the live VM state).
2447 pub(crate) fn get_hook(&self, target: Option<Gc<Coro>>) -> HookState {
2448 match target {
2449 t if self.is_current_thread(t) => self.hook,
2450 Some(co) => co.hook,
2451 None => self.hook,
2452 }
2453 }
2454
2455 /// Invoke the debug hook for `event` (PUC `luaD_hook`). The hook runs with
2456 /// hooks disabled (PUC clears the mask) and its results/stack growth are
2457 /// discarded so the interrupted frame's register window is untouched.
2458 /// `line` is the source line for a "line" event, `None` (→ nil) otherwise.
2459 fn run_hook(
2460 &mut self,
2461 event: &[u8],
2462 line: Option<i64>,
2463 from_native: bool,
2464 ) -> Result<(), LuaError> {
2465 // line and count events transfer no values (PUC `luaD_hook(L,
2466 // event, line, 0, 0)`); call and return hooks set theirs first
2467 if matches!(event, b"line" | b"count") {
2468 self.hook_ftransfer = 0;
2469 self.hook_ntransfer = 0;
2470 }
2471 // Rust hook fires first (no Vm reentrancy via call_value;
2472 // synchronous fn pointer call). Both Rust and Lua hooks may be
2473 // installed; both observe each event.
2474 if let Some(rh) = self.hook.rust_func {
2475 let evt = match event {
2476 b"call" => Some(RustHookEvent::Call),
2477 b"return" => Some(RustHookEvent::Return),
2478 b"tail call" | b"tail return" => Some(RustHookEvent::TailCall),
2479 b"line" => Some(RustHookEvent::Line(line.unwrap_or(0).max(0) as u32)),
2480 b"count" => Some(RustHookEvent::Count),
2481 _ => None,
2482 };
2483 if let Some(evt) = evt {
2484 let was_in_hook = self.in_hook;
2485 self.in_hook = true;
2486 rh(self, evt);
2487 self.in_hook = was_in_hook;
2488 }
2489 }
2490 let Some(hook) = self.hook.func else {
2491 return Ok(());
2492 };
2493 let saved_top = self.top;
2494 let saved_len = self.stack.len();
2495 let name = Value::Str(self.heap.intern(event));
2496 let lv = line.map_or(Value::Nil, Value::Int);
2497 self.in_hook = true;
2498 // PUC `db_sethook`'s C trampoline `hookf` sits between the engine and
2499 // the Lua hook — so `getinfo(2)` inside the hook resolves to whatever
2500 // ci sat below `hookf` (the function being hooked). When that hooked
2501 // function is native, no Lua frame for it exists in luna's `frames`;
2502 // model it as a synthetic C level by pushing the hook with
2503 // `from_c = true` (then `c_frame_name` reads the caller's call
2504 // instruction → e.g. `name = "sethook"`). When the hooked function is
2505 // Lua (its frame is still on the stack), push with `from_c = false`
2506 // so the level descent lands on it directly. The hook's own frame
2507 // carries `is_hook = true` so `getinfo(1).namewhat` reports "hook"
2508 // (PUC `CIST_HOOKED`).
2509 self.pending_is_hook = true;
2510 let r = self.call_value_impl(hook, &[name, lv], from_native);
2511 self.pending_is_hook = false;
2512 self.in_hook = false;
2513 self.stack.truncate(saved_len);
2514 self.top = saved_top;
2515 r.map(|_| ())
2516 }
2517
2518 /// Fire the "call" hook on entry to a function, if armed and not already in
2519 /// a hook (PUC clears the mask while a hook runs). PUC's transferinfo for
2520 /// a call hook is the param window: ftransfer = 1, ntransfer = nargs.
2521 /// `is_tail` selects the "tail call" event (PUC `LUA_HOOKTAILCALL`); a
2522 /// tail-call hook has no matching return hook (PUC luaD_pretailcall).
2523 fn hook_call_with(
2524 &mut self,
2525 from_native: bool,
2526 nargs: u32,
2527 is_tail: bool,
2528 ) -> Result<(), LuaError> {
2529 if self.hook.call
2530 && !self.in_hook
2531 && (self.hook.func.is_some() || self.hook.rust_func.is_some())
2532 {
2533 self.hook_ftransfer = 1;
2534 self.hook_ntransfer = nargs.min(u16::MAX as u32) as u16;
2535 // PUC 5.1 didn't distinguish tail-call events — every call,
2536 // including tail-calls, fired plain `"call"`. 5.2 introduced
2537 // the separate `"tail call"` event (mask `"c"` covers both).
2538 // 5.1 db.lua :366 pins this with `{"call","call","call","call",
2539 // "return","tail return","return","tail return"}`.
2540 let event: &[u8] = if is_tail && self.version >= LuaVersion::Lua52 {
2541 b"tail call"
2542 } else {
2543 b"call"
2544 };
2545 self.run_hook(event, None, from_native)?;
2546 }
2547 Ok(())
2548 }
2549
2550 pub(crate) fn hook_call(&mut self, from_native: bool, nargs: u32) -> Result<(), LuaError> {
2551 self.hook_call_with(from_native, nargs, false)
2552 }
2553
2554 /// Fire the "return" hook on exit from a function, if armed. ftransfer is
2555 /// the first result slot relative to the activation's func slot, ntransfer
2556 /// the number of results.
2557 pub(crate) fn hook_return(
2558 &mut self,
2559 from_native: bool,
2560 ftransfer: u32,
2561 nresults: u32,
2562 ) -> Result<(), LuaError> {
2563 if self.hook.ret
2564 && !self.in_hook
2565 && (self.hook.func.is_some() || self.hook.rust_func.is_some())
2566 {
2567 self.hook_ftransfer = ftransfer.min(u16::MAX as u32) as u16;
2568 self.hook_ntransfer = nresults.min(u16::MAX as u32) as u16;
2569 self.run_hook(b"return", None, from_native)?;
2570 }
2571 Ok(())
2572 }
2573
2574 /// PUC "tail return" event — fires once per tail call that collapsed
2575 /// into the activation now returning, *after* its own "return" event.
2576 /// 5.1 hook mask `"r"` covers both `return` and `tail return`.
2577 fn hook_tail_return(&mut self) -> Result<(), LuaError> {
2578 if self.hook.ret
2579 && !self.in_hook
2580 && (self.hook.func.is_some() || self.hook.rust_func.is_some())
2581 {
2582 self.run_hook(b"tail return", None, false)?;
2583 }
2584 Ok(())
2585 }
2586
2587 /// Call a metamethod with a single expected result.
2588 fn call_mm1(&mut self, f: Value, args: &[Value]) -> Result<Value, LuaError> {
2589 let mut r = self.call_value(f, args)?;
2590 Ok(if r.is_empty() {
2591 Value::Nil
2592 } else {
2593 r.swap_remove(0)
2594 })
2595 }
2596
2597 /// Begin a *yieldable* metamethod call from a VM instruction: `func(args…)`
2598 /// driven through the interpreter loop with a `Meta` continuation, so a
2599 /// `coroutine.yield` inside the metamethod suspends and resumes cleanly.
2600 /// On the metamethod's return the loop head runs `finish_meta(action, …)`.
2601 /// Returns to the caller with the call set up — the opcode arm must do no
2602 /// further work on the running frame and let the loop iterate. `tm` is
2603 /// the metamethod event name (e.g. "index", "add"); a Lua handler frame
2604 /// born from this call inherits it via `pending_tm`, so
2605 /// `debug.getinfo(1).namewhat == "metamethod"` and `.name == tm`
2606 /// (db.lua :878).
2607 fn begin_meta_call(
2608 &mut self,
2609 func: Value,
2610 args: &[Value],
2611 action: MetaAction,
2612 tm: &'static str,
2613 ) -> Result<(), LuaError> {
2614 let saved_top = self.top;
2615 let cont_slot = self.stack.len() as u32;
2616 self.stack.push(func);
2617 self.stack.extend_from_slice(args);
2618 self.top = self.stack.len() as u32;
2619 frames_push_sync(
2620 &mut self.frames,
2621 &mut self.frames_top,
2622 CallFrame::Cont(NativeCont {
2623 kind: ContKind::Meta(MetaCont { action, saved_top }),
2624 func_slot: cont_slot,
2625 nresults: 1,
2626 }),
2627 );
2628 let saved_tm = self.pending_tm.replace(tm);
2629 // begin_call drives a Lua metamethod through the loop (returns true) or
2630 // runs a native one inline (returns false, leaving results at cont_slot
2631 // for the loop head to pick up); either way the Meta cont resolves there.
2632 let r = self.begin_call(cont_slot, Some(args.len() as u32), 1, true);
2633 // Native callees never consumed pending_tm (push_frame is only hit on
2634 // a Lua callee); restore so it doesn't leak to a later push_frame.
2635 self.pending_tm = saved_tm;
2636 r?;
2637 Ok(())
2638 }
2639
2640 /// `R[dst] := t[key]` for a VM read opcode, resolving `__index` yieldably.
2641 fn op_index(&mut self, t: Value, key: Value, dst: u32) -> Result<(), LuaError> {
2642 // Read-time probe: a collectable key must be live at
2643 // the moment it is used. O(1) membership test against the
2644 // freed-pointer log — gc-verify diagnostic builds only; exact
2645 // under quarantining allocators (ASAN).
2646 #[cfg(feature = "gc-verify")]
2647 if matches!(key, Value::Str(_)) {
2648 let h = match key {
2649 Value::Str(s) => s.as_ptr() as usize,
2650 _ => unreachable!(),
2651 };
2652 if self.heap.recently_freed.contains(&h) {
2653 let (pc, reg_info) = match self.frames.last() {
2654 Some(CallFrame::Lua(f)) => {
2655 let pc = f.pc as usize;
2656 let inst = f.closure.proto.code.get(pc.wrapping_sub(1));
2657 (
2658 pc,
2659 inst.map(|i| {
2660 format!(
2661 "op[pc-1]={:?} a={} b={} c={} base={}",
2662 i.op(),
2663 i.a(),
2664 i.b(),
2665 i.c(),
2666 f.base
2667 )
2668 })
2669 .unwrap_or_default(),
2670 )
2671 }
2672 _ => (0, String::new()),
2673 };
2674 panic!(
2675 "[gc-verify] op_index READ of dead string key {h:#x} \
2676 (gc_top {}, top {}, pc {pc}, {reg_info})",
2677 self.gc_top, self.top,
2678 );
2679 }
2680 }
2681 match self.index_step(t, key)? {
2682 MmOut::Done(v) => self.stack[dst as usize] = v,
2683 MmOut::Mm { func, recv } => {
2684 self.begin_meta_call(func, &[recv, key], MetaAction::Store { dst }, "index")?;
2685 }
2686 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from index_step"),
2687 }
2688 Ok(())
2689 }
2690
2691 /// `t[key] := v` for a VM write opcode, resolving `__newindex` yieldably.
2692 fn op_newindex(&mut self, t: Value, key: Value, v: Value) -> Result<(), LuaError> {
2693 match self.newindex_step(t, key, v)? {
2694 MmOut::Done(_) => {}
2695 MmOut::Mm { func, recv } => {
2696 self.begin_meta_call(func, &[recv, key, v], MetaAction::Discard, "newindex")?;
2697 }
2698 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from newindex_step"),
2699 }
2700 Ok(())
2701 }
2702
2703 /// Apply a comparison opcode's outcome: a known boolean drives the
2704 /// conditional skip directly; a metamethod is called yieldably, its
2705 /// truthiness driving the skip on return.
2706 fn op_compare(
2707 &mut self,
2708 step: MmOut,
2709 l: Value,
2710 r: Value,
2711 k: bool,
2712 tm: &'static str,
2713 ) -> Result<(), LuaError> {
2714 match step {
2715 MmOut::Done(v) => self.cond_skip(v.truthy(), k),
2716 MmOut::Mm { func, .. } => {
2717 self.begin_meta_call(func, &[l, r], MetaAction::Compare { k, negate: false }, tm)?;
2718 }
2719 MmOut::CompareSynth { func } => {
2720 // ≤5.3 `__le` falls back to `not __lt(r, l)`; the swap and
2721 // negation are driven through `MetaAction::Compare` so the
2722 // metamethod call can yield like any other compare.
2723 self.begin_meta_call(func, &[r, l], MetaAction::Compare { k, negate: true }, "lt")?;
2724 }
2725 }
2726 Ok(())
2727 }
2728
2729 /// Complete a VM instruction whose metamethod just returned `result` (PUC
2730 /// `luaV_finishOp`). The running frame is already back on top.
2731 fn finish_meta(&mut self, action: MetaAction, result: Value) -> Result<(), LuaError> {
2732 match action {
2733 MetaAction::Store { dst } => self.stack[dst as usize] = result,
2734 MetaAction::Discard => {}
2735 MetaAction::Compare { k, negate } => {
2736 let t = if negate {
2737 !result.truthy()
2738 } else {
2739 result.truthy()
2740 };
2741 self.cond_skip(t, k);
2742 }
2743 MetaAction::Concat { dst, base_a } => {
2744 self.stack[dst as usize] = result;
2745 self.top = dst + 1;
2746 self.concat_run(base_a)?;
2747 }
2748 }
2749 Ok(())
2750 }
2751
2752 // ---- metatables ----
2753
2754 pub(crate) fn metatable_of(&self, v: Value) -> Option<Gc<Table>> {
2755 match v {
2756 Value::Table(t) => t.metatable(),
2757 Value::Userdata(u) => u.metatable(),
2758 v => type_mt_slot(v).and_then(|i| self.type_mt[i]),
2759 }
2760 }
2761
2762 /// Set the shared metatable for `v`'s basic type (debug.setmetatable on a
2763 /// non-table). No-op for tables (they carry their own).
2764 pub(crate) fn set_type_metatable(&mut self, v: Value, mt: Option<Gc<Table>>) {
2765 if let Some(i) = type_mt_slot(v) {
2766 self.type_mt[i] = mt;
2767 }
2768 }
2769
2770 /// The metamethod of `v` for `mm`, or nil.
2771 pub(crate) fn get_mm(&self, v: Value, mm: Mm) -> Value {
2772 match self.metatable_of(v) {
2773 Some(mt) => mt.get(Value::Str(self.mm_names[mm as usize])),
2774 None => Value::Nil,
2775 }
2776 }
2777
2778 /// PUC 5.1 `get_compTM`: a comparison metamethod (`__eq` / `__lt` / `__le`)
2779 /// only fires when both operands carry a metatable that exposes the same
2780 /// implementation. Returns the metamethod to call, or `Nil` when no
2781 /// compatible match exists. Used to honour events.lua 5.1 :262's rule
2782 /// that `c == d` (where `d` has no metatable) falls back to raw equality.
2783 pub(crate) fn get_comp_mm(&self, l: Value, r: Value, mm: Mm) -> Value {
2784 let mt1 = self.metatable_of(l);
2785 let Some(mt1) = mt1 else { return Value::Nil };
2786 let key = Value::Str(self.mm_names[mm as usize]);
2787 let tm1 = mt1.get(key);
2788 if tm1.is_nil() {
2789 return Value::Nil;
2790 }
2791 let mt2 = self.metatable_of(r);
2792 let Some(mt2) = mt2 else { return Value::Nil };
2793 if mt1.as_ptr() == mt2.as_ptr() {
2794 return tm1;
2795 }
2796 let tm2 = mt2.get(key);
2797 if tm2.is_nil() {
2798 return Value::Nil;
2799 }
2800 if tm1.raw_eq(tm2) {
2801 return tm1;
2802 }
2803 Value::Nil
2804 }
2805
2806 /// PUC `luaT_objtypename`: the type name shown in error messages. A table
2807 /// or full userdata whose metatable carries a string `__name` reports that
2808 /// (e.g. "FILE*", "My Type") instead of the bare "table"/"userdata".
2809 pub(crate) fn obj_typename(&self, v: Value) -> String {
2810 // `__name` (luaT_objtypename) arrived in 5.3
2811 if self.version >= LuaVersion::Lua53
2812 && matches!(v, Value::Table(_) | Value::Userdata(_))
2813 && let Value::Str(s) = self.get_mm(v, Mm::Name)
2814 {
2815 return String::from_utf8_lossy(s.as_bytes()).into_owned();
2816 }
2817 v.type_name().to_string()
2818 }
2819
2820 fn call_at(
2821 &mut self,
2822 func_slot: u32,
2823 nargs: u32,
2824 from_c: bool,
2825 ) -> Result<Vec<Value>, LuaError> {
2826 let depth = self.frames.len();
2827 match self.begin_call(func_slot, Some(nargs), -1, from_c) {
2828 // run until every frame the call pushed has returned: a pcall /
2829 // xpcall / __pairs continuation sits below the frame of the
2830 // function it called, and it is the continuation that produces
2831 // the call's results (`true, ...`, or `false, msg` on an error)
2832 Ok(true) => self.exec_with(depth + 1),
2833 // native completed inline; results at func_slot..top
2834 Ok(false) => Ok(self.take_results(func_slot)),
2835 // pcall / xpcall pushed their continuation and then failed to
2836 // call their function (`pcall("x")`): the continuation catches
2837 // that error, as it does in the dispatch loop
2838 Err(e)
2839 if self.frames.len() > depth
2840 && self.yielding.is_none()
2841 && self.terminating.is_none()
2842 && !self.host_yield_pending
2843 && self.pending_async_native_fut.is_none() =>
2844 {
2845 match self.unwind(e.0, depth + 1) {
2846 Unwound::Caught => self.exec_with(depth + 1),
2847 Unwound::CaughtReturn(vals) => Ok(vals),
2848 Unwound::Propagated(err) => Err(err),
2849 }
2850 }
2851 Err(e) => Err(e),
2852 }
2853 }
2854
2855 /// Switch the `collectgarbage` mode, returning the previous mode name.
2856 pub(crate) fn gc_switch_mode(&mut self, new: &'static str) -> &'static str {
2857 std::mem::replace(&mut self.gc_mode, new)
2858 }
2859
2860 /// Whether the current `collectgarbage` mode is "generational" (where a
2861 /// "step" is a minor collection — a full atomic pass — rather than a paced
2862 /// incremental sweep).
2863 pub(crate) fn gc_mode_is_generational(&self) -> bool {
2864 self.gc_mode == "generational"
2865 }
2866
2867 /// Current `stepsize` pacing parameter (PUC: 0 means an unbounded step that
2868 /// completes a whole cycle at once).
2869 pub(crate) fn gc_stepsize(&self) -> i64 {
2870 self.gc_stepsize
2871 }
2872
2873 /// Set luna's collector knobs: heap growth before a new cycle (%), sweep
2874 /// work per safe point, and the default step size (0 = a step completes
2875 /// the cycle).
2876 pub(crate) fn set_gc_pacing(&mut self, pause: i64, stepmul: i64, stepsize: i64) {
2877 self.gc_pause = pause;
2878 self.gc_stepmul = stepmul;
2879 self.gc_stepsize = stepsize;
2880 }
2881
2882 /// Interpreter safe-point auto-GC: FULL incremental Propagate + adaptive
2883 /// paced sweep via `Vm::gc_step`.
2884 ///
2885 /// Running Propagate from a safe-point relies on objects being **born
2886 /// black during Propagate**: a newly allocated object never becomes
2887 /// dead-white at the atomic flip.
2888 ///
2889 /// Adaptive budget scales with heap size: 100M-object heap (heavy.lua's
2890 /// `loadrep` stress) gets a 25M-object budget so a cycle completes in
2891 /// O(SWEEP_DIVISOR) safe-points regardless of size.
2892 #[inline(always)]
2893 pub(crate) fn maybe_collect_garbage(&mut self, live_top: u32) {
2894 if self.gc_finalizing {
2895 return;
2896 }
2897 if !self.heap.gc_due() {
2898 return;
2899 }
2900 // Bare `live_top`, no `max(self.top)` widening: every frame-pop
2901 // site (`finish_results`, the Op::TailCall collapse, pcall
2902 // unwind) clears the slots it vacates, mirroring PUC's L->top
2903 // discipline.
2904 self.gc_top = live_top;
2905 // PUC stepmul: % of allocation rate. Higher = more GC work per
2906 // safe-point (lower memory, more CPU). Default 100 = `live / 4` per
2907 // step (~4 safe-points per cycle). stepmul=200 → `live / 2`, etc.
2908 const SWEEP_BASE: usize = 400; // 400 / stepmul=100 = divisor 4
2909 const MIN_BUDGET: usize = 64_000;
2910 let stepmul = self.gc_stepmul.max(1) as usize;
2911 let divisor = (SWEEP_BASE / stepmul).max(1);
2912 let budget = (self.heap.live_objects() / divisor).max(MIN_BUDGET);
2913 if self.gc_step(budget) {
2914 self.heap.rearm_gc_pause(self.gc_pause);
2915 }
2916 }
2917
2918 /// Enumerate the GC roots: first-class `Value` roots plus bare-object
2919 /// roots (open upvalues, which are not first-class Values). Shared by the
2920 /// full collector and the incremental-sweep driver so both snapshot the
2921 /// exact same live set.
2922 fn gc_roots(&self) -> (Vec<Value>, Vec<*mut GcHeader>) {
2923 let mut roots: Vec<Value> = Vec::with_capacity(self.stack.len() + 32);
2924 roots.push(Value::Table(self.globals));
2925 for mt in self.type_mt.into_iter().flatten() {
2926 roots.push(Value::Table(mt));
2927 }
2928 for &n in &self.mm_names {
2929 roots.push(Value::Str(n));
2930 }
2931 // Root the running thread's live registers (PUC marks [stack, top)).
2932 // `gc_top` is the instruction-level cursor of the last GC
2933 // safe-point: allocation safe-points set it via
2934 // `maybe_collect_garbage(live_top)`, and `begin_call` raises it
2935 // to the callee's argument top when entering a native — PUC's
2936 // `L->top = func + 1 + nargs` C-call discipline. Without that
2937 // raise, an explicit `collectgarbage()` collected with a STALE
2938 // cursor from some earlier (lower) safe-point and freed its own
2939 // caller's register-held strings
2940 // (STATUS_ACCESS_VIOLATION on Windows / ASAN heap-use-after-free
2941 // on Linux). Values stranded above the cursor stay
2942 // excluded so weak-table entries are not spuriously pinned
2943 // (gc.lua:544 suspended-coroutine collection).
2944 let live = (self.gc_top as usize).min(self.stack.len());
2945 roots.extend_from_slice(&self.stack[..live]);
2946 for cf in &self.frames {
2947 match cf {
2948 CallFrame::Lua(f) => roots.push(Value::Closure(f.closure)),
2949 CallFrame::Cont(NativeCont {
2950 kind: ContKind::Xpcall { handler },
2951 ..
2952 }) => roots.push(*handler),
2953 CallFrame::Cont(NativeCont {
2954 kind: ContKind::Close(cc),
2955 ..
2956 }) => {
2957 // Root the error threaded through this close chain so a
2958 // `collectgarbage()` inside a sibling `__close` handler
2959 // does not free it before the next handler is invoked
2960 // (PUC L->ci->u.l.errfunc / the closing_err shadow).
2961 if let Some(e) = cc.pending {
2962 roots.push(e);
2963 }
2964 if let AfterClose::ResumeUnwind { err, .. } = cc.after {
2965 roots.push(err);
2966 }
2967 }
2968 CallFrame::Cont(_) => {}
2969 }
2970 }
2971 if let Some(e) = self.closing_err {
2972 roots.push(e);
2973 }
2974 // Host roots — Lua-facade handles keep their referenced
2975 // values alive across calls/yields. Trace the whole vector;
2976 // unused slots (post-`unpin_all`) carry Value::Nil which the
2977 // GC ignores.
2978 for slot in &self.host_roots {
2979 // free-list slots carry Value::Nil (GC no-op)
2980 roots.push(slot.value);
2981 }
2982 // `table.sort` and similar builtins stash their working
2983 // `Vec<Value>` here so a `collectgarbage()` invoked inside the
2984 // comparator callback doesn't free strings/tables snapshotted
2985 // off the live table (sort.lua's `load(..)(); collectgarbage()`
2986 // compare regression).
2987 for buf in &self.sort_scratch {
2988 roots.extend_from_slice(buf);
2989 }
2990 // The running-natives chain holds Gc<NativeClosure>s
2991 // mid-execution. Without rooting them here, a `collectgarbage()`
2992 // invoked inside the running native (sort.lua's `load(..)();
2993 // collectgarbage()` compare callback regression) sweeps the
2994 // closure that's actively executing, leaving `nc.upvals`
2995 // dangling and the Rust local `nc` pointing at recycled memory
2996 // — the SIGSEGV pops on the very next field access or pop.
2997 for &nc in &self.running_natives {
2998 roots.push(Value::Native(nc));
2999 }
3000 // the running thread's debug hook (suspended threads root theirs via
3001 // Coro::trace / the main_ctx sweep below)
3002 if let Some(h) = self.hook.func {
3003 roots.push(h);
3004 }
3005 // the running coroutine (its saved-context fields live in the VM, but
3006 // the object itself + its resumer chain must stay reachable)
3007 if let Some(co) = self.current {
3008 roots.push(Value::Coro(co));
3009 }
3010 if let Some(mc) = self.main_coro {
3011 roots.push(Value::Coro(mc));
3012 }
3013 // debug.getregistry() and io library state
3014 if let Some(r) = self.registry {
3015 roots.push(Value::Table(r));
3016 }
3017 if let Some(mt) = self.file_mt {
3018 roots.push(Value::Table(mt));
3019 }
3020 if let Some(f) = self.io_input {
3021 roots.push(Value::Userdata(f));
3022 }
3023 if let Some(f) = self.io_output {
3024 roots.push(Value::Userdata(f));
3025 }
3026 if let Some(f) = self.io_stdin {
3027 roots.push(Value::Userdata(f));
3028 }
3029 // the main thread's saved context while a coroutine runs
3030 if let Some(m) = &self.main_ctx {
3031 roots.extend_from_slice(&m.stack);
3032 if let Some(h) = m.hook.func {
3033 roots.push(h);
3034 }
3035 for cf in &m.frames {
3036 match cf {
3037 CallFrame::Lua(f) => roots.push(Value::Closure(f.closure)),
3038 CallFrame::Cont(NativeCont {
3039 kind: ContKind::Xpcall { handler },
3040 ..
3041 }) => roots.push(*handler),
3042 CallFrame::Cont(_) => {}
3043 }
3044 }
3045 }
3046 let mut extra: Vec<*mut GcHeader> = self
3047 .open_upvals
3048 .iter()
3049 .map(|&(_, uv)| uv.as_ptr() as *mut GcHeader)
3050 .collect();
3051 if let Some(m) = &self.main_ctx {
3052 extra.extend(
3053 m.open_upvals
3054 .iter()
3055 .map(|&(_, uv)| uv.as_ptr() as *mut GcHeader),
3056 );
3057 }
3058 (roots, extra)
3059 }
3060
3061 /// Run a full collection with the VM's roots, then run any `__gc`
3062 /// finalizers the collection scheduled. A no-op (returns 0) when already
3063 /// inside a finalizer — the collector is not reentrant (PUC).
3064 pub fn collect_garbage(&mut self) -> usize {
3065 if self.gc_finalizing {
3066 return 0;
3067 }
3068 let (roots, extra) = self.gc_roots();
3069 let freed = self.heap.collect_ex(&roots, &extra);
3070 #[cfg(feature = "gc-verify")]
3071 self.verify_frame_regs_live("collect_garbage");
3072 self.run_finalizers();
3073 freed
3074 }
3075
3076 /// `gc-verify`: after a collect, every register slot the
3077 /// collector just rooted (`[0, max(gc_top, top))` — the same bound
3078 /// `gc_roots` uses) must hold a live value. A dead value inside the
3079 /// rooted range means the root snapshot and the sweep disagreed —
3080 /// a use-after-free waiting to happen. (Slots ABOVE the bound may hold
3081 /// stale dead values legitimately; the interpreter's contract is
3082 /// that it writes them before reading.)
3083 #[cfg(feature = "gc-verify")]
3084 pub(crate) fn verify_frame_regs_live(&self, ctx: &str) {
3085 let live = self.heap.debug_live_set();
3086 let header = |v: Value| -> Option<usize> {
3087 match v {
3088 Value::Str(s) => Some(s.as_ptr() as usize),
3089 Value::Table(t) => Some(t.as_ptr() as usize),
3090 Value::Closure(c) => Some(c.as_ptr() as usize),
3091 Value::Native(n) => Some(n.as_ptr() as usize),
3092 Value::Coro(c) => Some(c.as_ptr() as usize),
3093 Value::Userdata(u) => Some(u.as_ptr() as usize),
3094 _ => None,
3095 }
3096 };
3097 let bound = (self.gc_top as usize).min(self.stack.len());
3098 for i in 0..bound {
3099 if let Some(h) = header(self.stack[i])
3100 && !live.contains(&h)
3101 {
3102 panic!(
3103 "[gc-verify] {ctx}: rooted stack slot {i} (gc_top {}, top {}) \
3104 holds a dead value {h:#x} after collect",
3105 self.gc_top, self.top,
3106 );
3107 }
3108 }
3109 // Diagnostic tier: a dead value ABOVE the cursor is only a bug if
3110 // that register is a named local still in scope (the interpreter
3111 // WILL read it). Cross-check against the proto's LocVar table.
3112 for (fi, cf) in self.frames.iter().enumerate() {
3113 if let CallFrame::Lua(f) = cf {
3114 let base = f.base as usize;
3115 let maxs = f.closure.proto.max_stack as usize;
3116 let hi = (base + maxs).min(self.stack.len());
3117 let pc = f.pc;
3118 for i in bound.max(base)..hi {
3119 if let Some(h) = header(self.stack[i])
3120 && !live.contains(&h)
3121 {
3122 let reg = (i - base) as u32;
3123 if let Some(lv) = f
3124 .closure
3125 .proto
3126 .locvars
3127 .iter()
3128 .find(|lv| lv.reg == reg && lv.start_pc <= pc && pc < lv.end_pc)
3129 {
3130 panic!(
3131 "[gc-verify] {ctx}: frame {fi} IN-SCOPE LOCAL '{}' \
3132 (reg {reg}, abs {i}, pc {pc}, gc_top {}) holds a \
3133 dead value {h:#x} — live_top cursor excluded a \
3134 live named local",
3135 lv.name, self.gc_top,
3136 );
3137 }
3138 }
3139 }
3140 }
3141 }
3142 }
3143
3144 /// PUC 5.1 `collectgarbage` re-raised the first error a `__gc` finalizer
3145 /// threw; gc.lua's "errors during collection" probe relies on it. This
3146 /// variant runs the same cycle but propagates the captured finalizer
3147 /// error to the explicit caller.
3148 pub(crate) fn collect_garbage_propagating(&mut self) -> Result<usize, LuaError> {
3149 if self.gc_finalizing {
3150 return Ok(0);
3151 }
3152 let (roots, extra) = self.gc_roots();
3153 let freed = self.heap.collect_ex(&roots, &extra);
3154 #[cfg(feature = "gc-verify")]
3155 self.verify_frame_regs_live("collect_garbage_propagating");
3156 self.run_finalizers_or_err()?;
3157 Ok(freed)
3158 }
3159
3160 /// Whether a `__gc` finalizer is currently running (so `collectgarbage`
3161 /// should report fail rather than collect).
3162 pub(crate) fn gc_is_finalizing(&self) -> bool {
3163 self.gc_finalizing
3164 }
3165
3166 /// PUC 5.4+ default warnf: emit one piece of a warning message. `to_cont`
3167 /// = true indicates more pieces follow (concatenated until the first
3168 /// `to_cont = false` call flushes the whole line). Mirrors
3169 /// `lauxlib.c::warnfon` + `warnfcont` + `checkcontrol`:
3170 /// * If the buffer is fresh, `to_cont` is false, and the message is
3171 /// `@<word>`, treat as a control message — only `@on` / `@off` are
3172 /// recognised; any other `@…` is silently ignored.
3173 /// * Otherwise, while the state is `Off`, drop the piece; while `On`,
3174 /// accumulate, and flush to stderr + `warn_log` on the
3175 /// non-continuation call.
3176 pub(crate) fn emit_warn(&mut self, msg: &[u8], to_cont: bool) {
3177 if self.warn_buf.is_empty()
3178 && !to_cont
3179 && let Some(b'@') = msg.first().copied()
3180 {
3181 match &msg[1..] {
3182 b"on" => self.warn_state = WarnState::On,
3183 b"off" => self.warn_state = WarnState::Off,
3184 _ => {} // unknown control — silently ignored (PUC checkcontrol)
3185 }
3186 return;
3187 }
3188 if self.warn_state == WarnState::Off {
3189 // drop continuation pieces too — PUC `warnfoff` is the trampoline
3190 return;
3191 }
3192 self.warn_buf.extend_from_slice(msg);
3193 if !to_cont {
3194 let line = std::mem::take(&mut self.warn_buf);
3195 eprintln!("Lua warning: {}", String::from_utf8_lossy(&line));
3196 self.warn_log.push(line);
3197 }
3198 }
3199
3200 /// Drain the in-process warning log (one entry per emitted message, sans
3201 /// `"Lua warning: "` prefix and newline). For test harnesses that want to
3202 /// assert on warn output without scraping stderr.
3203 pub fn warn_log_take(&mut self) -> Vec<Vec<u8>> {
3204 std::mem::take(&mut self.warn_log)
3205 }
3206
3207 /// Arm the cooperative instruction budget. The run loop
3208 /// decrements this once per dispatch turn; on zero it raises a catchable
3209 /// `"instruction budget exceeded"` error and disarms itself so the host
3210 /// can resume with a fresh budget on the next call. `None` removes the
3211 /// cap. Pass `Some(n)` before `eval`/`call_value` for the embedder's
3212 /// short-script semantics.
3213 pub fn set_instr_budget(&mut self, budget: Option<i64>) {
3214 self.instr_budget = budget;
3215 }
3216
3217 /// Remaining instruction budget (None when unbounded).
3218 pub fn instr_budget_remaining(&self) -> Option<i64> {
3219 self.instr_budget
3220 }
3221
3222 /// Toggle the cranelift JIT. Default `true`. Sandbox embedders
3223 /// **must** disable JIT when relying on `instr_budget` — see the
3224 /// `jit_enabled` field doc for the rationale.
3225 pub fn set_jit_enabled(&mut self, enabled: bool) {
3226 self.jit.enabled = enabled;
3227 }
3228
3229 /// Current JIT enable state.
3230 pub fn jit_enabled(&self) -> bool {
3231 self.jit.enabled
3232 }
3233
3234 /// Toggle the trace JIT. Off by default. When enabled, hot
3235 /// back-edges are counted on `Proto.trace_hot_count`; once the
3236 /// counter passes `TRACE_HOT_THRESHOLD`, the dispatch loop enters
3237 /// recording mode at the back-edge target.
3238 pub fn set_trace_jit_enabled(&mut self, enabled: bool) {
3239 self.jit.trace_enabled = enabled;
3240 }
3241
3242 /// Opt-in flag for the self-link cycle catch. See field
3243 /// docs for the correctness blocker. Default `false`.
3244 pub fn set_self_link_enabled(&mut self, enabled: bool) {
3245 self.jit.self_link_enabled = enabled;
3246 }
3247
3248 /// Current state of the self-link cycle catch.
3249 pub fn self_link_enabled(&self) -> bool {
3250 self.jit.self_link_enabled
3251 }
3252
3253 #[doc(hidden)]
3254 #[deprecated(since = "3.2.0", note = "renamed to `set_self_link_enabled`")]
3255 pub fn set_p16_self_link_enabled(&mut self, enabled: bool) {
3256 self.set_self_link_enabled(enabled);
3257 }
3258
3259 #[doc(hidden)]
3260 #[deprecated(since = "3.2.0", note = "renamed to `self_link_enabled`")]
3261 pub fn p16_self_link_enabled(&self) -> bool {
3262 self.self_link_enabled()
3263 }
3264
3265 /// Current trace-JIT enable state.
3266 pub fn trace_jit_enabled(&self) -> bool {
3267 self.jit.trace_enabled
3268 }
3269
3270 /// Number of traces that have closed cleanly (looped back to the
3271 /// head PC) since this Vm was constructed. Cumulative; used by
3272 /// tests + tuning.
3273 pub fn trace_closed_count(&self) -> u64 {
3274 self.jit.counters.closed
3275 }
3276
3277 /// Number of traces that have aborted (exceeded MAX_TRACE_LEN or
3278 /// hit an un-recordable op).
3279 pub fn trace_aborted_count(&self) -> u64 {
3280 self.jit.counters.aborted
3281 }
3282
3283 /// Number of compiled traces whose close shape
3284 /// is `TraceEnd::InlineAbort` (depth>0 boundary). Such traces
3285 /// pin `dispatchable=false` because the dispatcher can't
3286 /// resume at a depth>0 PC without the matching CallFrames.
3287 /// The frame-materialisation helper could synthesise those, but
3288 /// the InlineAbort emit path isn't wired up to it.
3289 pub fn trace_inline_abort_count(&self) -> u64 {
3290 self.jit.counters.inline_abort
3291 }
3292
3293 /// See `JitCounters::dispatch_off_reasons`.
3294 pub fn trace_dispatch_off_reasons(&self) -> &[&'static str] {
3295 &self.jit.counters.dispatch_off_reasons
3296 }
3297
3298 /// See `JitCounters::compile_failed_reasons`.
3299 pub fn trace_compile_failed_reasons(&self) -> &[&'static str] {
3300 &self.jit.counters.compile_failed_reasons
3301 }
3302
3303 /// See `JitCounters::closed_lens`. Returns
3304 /// `(is_call_triggered, ops_len)` for every trace that closed.
3305 pub fn trace_closed_lens(&self) -> &[(bool, usize)] {
3306 &self.jit.counters.closed_lens
3307 }
3308
3309 /// See [`crate::vm::jit_state::JitCounters::close_cause_counts`].
3310 /// Per-reason close-cause counts (recorder-side abort/discard +
3311 /// lowerer-side dispatch_off labels) keyed by `&'static str`.
3312 pub fn trace_close_cause_counts(&self) -> &std::collections::HashMap<&'static str, u64> {
3313 &self.jit.counters.close_cause_counts
3314 }
3315
3316 /// Number of compiled traces whose
3317 /// `CompiledTrace.downrec_link` is `Some(_)` (lowerer's
3318 /// `downrec_idx_opt` arm emitted the stitch sentinel + caller-pc
3319 /// guard scaffold).
3320 pub fn trace_downrec_link_compiled_count(&self) -> u64 {
3321 self.jit.counters.downrec_link_compiled
3322 }
3323
3324 /// See
3325 /// [`crate::vm::jit_state::JitCounters::downrec_dispatched`]. Number
3326 /// of times the dispatcher's `is_downrec_sentinel` arm fired and
3327 /// classified the return as a caller-pc-guard HIT.
3328 pub fn trace_downrec_dispatched_count(&self) -> u64 {
3329 self.jit.counters.downrec_dispatched
3330 }
3331
3332 /// See
3333 /// [`crate::vm::jit_state::JitCounters::downrec_deopt`]. Number of
3334 /// times the dispatcher entered a `downrec_link`-bearing trace and
3335 /// the trace returned via the lowerer's deopt block (caller-pc
3336 /// guard MISS), or the dispatcher itself force-deopted via the
3337 /// stitch-cycle checkpoint.
3338 pub fn trace_downrec_deopt_count(&self) -> u64 {
3339 self.jit.counters.downrec_deopt
3340 }
3341
3342 /// See
3343 /// [`crate::vm::jit_state::JitCounters::multi_way_guard_emitted`].
3344 /// Number of compiled traces whose lowerer emitted a multi-way
3345 /// caller-pc guard chain (>= 2 distinct `caller_pc` candidates)
3346 /// at the `TraceEnd::DownRec` close + lifted `dispatchable = true`.
3347 pub fn trace_multi_way_guard_emitted_count(&self) -> u64 {
3348 self.jit.counters.multi_way_guard_emitted
3349 }
3350
3351 /// Number of closed traces the lowerer compiled and
3352 /// parked on `Proto.traces`. Re-records of the same head_pc are
3353 /// deduped (the second close finds the head_pc already cached
3354 /// and skips compile), so this never exceeds `trace_closed_count`.
3355 pub fn trace_compiled_count(&self) -> u64 {
3356 self.jit.counters.compiled
3357 }
3358
3359 /// Number of times the recorder captured a
3360 /// [`crate::jit::trace_types::FieldIcSnapshot`] under
3361 /// `LUNA_JIT_FIELD_IC=1`. Stays 0 on the env-default path. Used
3362 /// by the opt-in fire test to verify the env gate
3363 /// wiring round-trips end-to-end (env -> recorder -> snapshot
3364 /// -> counter -> getter -> assertion).
3365 pub fn trace_field_ic_snapshot_count(&self) -> u64 {
3366 self.jit.counters.field_ic_snapshot_captured
3367 }
3368
3369 /// Number of closed traces the lowerer rejected
3370 /// (any of the bail conditions in
3371 /// `crate::jit::trace::try_compile_trace`).
3372 pub fn trace_compile_failed_count(&self) -> u64 {
3373 self.jit.counters.compile_failed
3374 }
3375
3376 /// Number of times the dispatcher jumped into a
3377 /// compiled trace. Bumps on every entry; `trace_deopt_count`
3378 /// counts the subset where the trace returned with a parked
3379 /// `jit_pending_err`.
3380 pub fn trace_dispatched_count(&self) -> u64 {
3381 self.jit.counters.dispatched
3382 }
3383
3384 /// Number of trace entries that came back with
3385 /// `jit_pending_err` set (typically a metatable shadowed an
3386 /// index inside a helper, forcing the dispatcher to fall back
3387 /// to the interpreter without committing the trace's result).
3388 pub fn trace_deopt_count(&self) -> u64 {
3389 self.jit.counters.deopt
3390 }
3391
3392 /// Number of times the dispatcher started a side
3393 /// trace recording (an `exit_hit_counts` slot crossed
3394 /// [`crate::jit::trace::HOTEXIT_THRESHOLD`] while `active_trace`
3395 /// was None and trace JIT was enabled). Each unit is exactly one
3396 /// `start_side_trace` call; the actual compile success counts
3397 /// under [`Self::trace_compiled_count`] like any other trace.
3398 /// Probe use: distinguishes the "side-trace pipeline fired"
3399 /// signal from the "primary back-edge / call-trigger fired"
3400 /// signal without reading per-counter histograms.
3401 pub fn trace_side_trace_started_count(&self) -> u64 {
3402 self.jit.counters.side_trace_started
3403 }
3404
3405 /// Number of side-trace recordings that closed,
3406 /// compiled successfully, AND patched their parent's
3407 /// `exit_side_trace_ptrs[exit_idx]`.
3408 pub fn trace_side_trace_compiled_count(&self) -> u64 {
3409 self.jit.counters.side_trace_compiled
3410 }
3411
3412 /// Number of side traces that compiled
3413 /// successfully but were SHEDDED by the close-handler shape-
3414 /// match gate (`exit_tags_match_entry_tags`). High ratios
3415 /// vs. `trace_side_trace_compiled_count` indicate the
3416 /// architecture is shedding lots of would-be side traces;
3417 /// useful as a tuning probe for future relaxation of the
3418 /// gate or for child-IR re-specialisation against parent's
3419 /// exit shape.
3420 pub fn trace_side_trace_shape_mismatch_count(&self) -> u64 {
3421 self.jit.counters.side_trace_shape_mismatch
3422 }
3423
3424 /// Sum of NewTable sites the pre-emit escape sweep
3425 /// classified as `crate::jit::trace::EscapeState::Sinkable`
3426 /// across every successfully compiled trace on this Vm. The
3427 /// count is post-demotion: sites pre-emit drops back to Escaped
3428 /// for not meeting the sunk-emit criteria are NOT counted.
3429 /// `trace_sunk_alloc_count` matches one-for-one today (every
3430 /// surviving Sinkable site goes through sunk emit).
3431 pub fn trace_sinkable_seen_count(&self) -> u64 {
3432 self.jit.counters.sinkable_seen
3433 }
3434
3435 /// See `JitCounters::accum_bufferable_seen`.
3436 pub fn trace_accum_bufferable_seen_count(&self) -> u64 {
3437 self.jit.counters.accum_bufferable_seen
3438 }
3439
3440 /// Total dispatch hits across all known traces,
3441 /// broken into hot-exit telemetry (max single-exit count,
3442 /// total dispatches, exit count). Used by probes to identify
3443 /// hot side-exits as side-trace candidates.
3444 ///
3445 /// Walks `cl.proto` AND all nested protos in `cl.proto.protos`
3446 /// recursively, so inner functions' traces are reported.
3447 pub fn trace_exit_hit_summary(
3448 &self,
3449 cl: crate::runtime::heap::Gc<crate::runtime::function::LuaClosure>,
3450 ) -> Vec<(u32, Vec<u32>)> {
3451 fn walk(
3452 proto: crate::runtime::heap::Gc<crate::runtime::function::Proto>,
3453 out: &mut Vec<(u32, Vec<u32>)>,
3454 ) {
3455 for ct in proto.traces.borrow().iter() {
3456 let counts: Vec<u32> = ct.exit_hit_counts.iter().map(|c| c.get()).collect();
3457 out.push((ct.head_pc, counts));
3458 }
3459 for inner in proto.protos.iter() {
3460 walk(*inner, out);
3461 }
3462 }
3463 let mut out: Vec<(u32, Vec<u32>)> = Vec::new();
3464 walk(cl.proto, &mut out);
3465 out
3466 }
3467
3468 /// Surface every side-exit slot whose hit count is
3469 /// `>= HOTEXIT_THRESHOLD` across every trace reachable from
3470 /// `cl.proto` (recursively walking `proto.protos`). Returned
3471 /// entries are side-trace candidates: each carries the parent
3472 /// trace's `(head_proto, head_pc)`, the exit's index in the
3473 /// parent's `exit_hit_counts`, and the side trace's natural
3474 /// entry shape (`cont_pc` + `exit_tags`).
3475 ///
3476 /// Layout of `exit_hit_counts` (mirrored by the iter):
3477 /// - `[0..per_exit_inline.len())` → `InlineSideExit` (cont_pc +
3478 /// window-sized exit_tags).
3479 /// - `[per_exit_inline.len()..inline.len() + per_exit_tags.len())`
3480 /// → `per_exit_tags[i]` (per-cont_pc caller-window tags).
3481 /// - Last slot → global clean-tail (cont_pc = `head_pc`,
3482 /// exit_tags = `ct.exit_tags`).
3483 pub fn hot_exit_iter(
3484 &self,
3485 cl: crate::runtime::heap::Gc<crate::runtime::function::LuaClosure>,
3486 ) -> Vec<crate::jit::trace::HotExitInfo> {
3487 use crate::jit::trace::{HOTEXIT_THRESHOLD, HotExitInfo};
3488 fn walk(
3489 proto: crate::runtime::heap::Gc<crate::runtime::function::Proto>,
3490 out: &mut Vec<HotExitInfo>,
3491 ) {
3492 for ct in proto.traces.borrow().iter() {
3493 let inline_n = ct.per_exit_inline.len();
3494 let tags_n = ct.per_exit_tags.len();
3495 debug_assert_eq!(
3496 ct.exit_hit_counts.len(),
3497 inline_n + tags_n + 1,
3498 "exit_hit_counts layout invariant violated"
3499 );
3500 for (idx, cell) in ct.exit_hit_counts.iter().enumerate() {
3501 let hits = cell.get();
3502 if hits < HOTEXIT_THRESHOLD {
3503 continue;
3504 }
3505 let (cont_pc, exit_tags) = if idx < inline_n {
3506 let ent = &ct.per_exit_inline[idx];
3507 (ent.cont_pc, ent.exit_tags.clone())
3508 } else if idx < inline_n + tags_n {
3509 let (pc, tags) = &ct.per_exit_tags[idx - inline_n];
3510 (*pc, tags.clone())
3511 } else {
3512 (ct.head_pc, ct.exit_tags.clone())
3513 };
3514 out.push(HotExitInfo {
3515 head_proto: proto,
3516 head_pc: ct.head_pc,
3517 exit_idx: idx,
3518 hits,
3519 cont_pc,
3520 exit_tags,
3521 });
3522 }
3523 }
3524 for inner in proto.protos.iter() {
3525 walk(*inner, out);
3526 }
3527 }
3528 let mut out: Vec<HotExitInfo> = Vec::new();
3529 walk(cl.proto, &mut out);
3530 out
3531 }
3532
3533 /// Sum of NewTable sites that actually took the
3534 /// sunk-emit path across every successfully compiled trace on
3535 /// this Vm. Each counted site skips its heap `Gc<Table>`
3536 /// allocation per dispatch; the array part lives as Cranelift
3537 /// `Variable`s for the duration of the trace.
3538 pub fn trace_sunk_alloc_count(&self) -> u64 {
3539 self.jit.counters.sunk_alloc
3540 }
3541
3542 /// Sum of materialise-helper emit sites across every
3543 /// successfully compiled trace on this Vm. Each unit is a
3544 /// (site × cmp side-exit) pair whose IR reconstructs a heap
3545 /// `Gc<Table>` from the virt slots on deopt.
3546 pub fn trace_materialize_emit_count(&self) -> u64 {
3547 self.jit.counters.materialize_emit
3548 }
3549
3550 /// Diagnostic: total `Op::Closure` ops the trace JIT
3551 /// lowered to the `luna_jit_op_closure` helper. Each emitted op
3552 /// replaces a `Heap::new_closure_inline` call on the dispatch
3553 /// path; the count is static (one per matching op per compiled
3554 /// trace), summed at compile success.
3555 pub fn trace_closure_emit_count(&self) -> u64 {
3556 self.jit.counters.closure_emit
3557 }
3558
3559 /// See
3560 /// [`crate::vm::jit_state::JitCounters::per_exit_inline_compiled`].
3561 /// Number of compiled traces whose `per_exit_inline.len() > 0`
3562 /// (depth>0 inlined cmp side-exits emitted).
3563 pub fn trace_per_exit_inline_compiled_count(&self) -> u64 {
3564 self.jit.counters.per_exit_inline_compiled
3565 }
3566
3567 /// See
3568 /// [`crate::vm::jit_state::JitCounters::per_exit_inline_dispatchable`].
3569 /// Number of compiled traces with `per_exit_inline.len() > 0` AND
3570 /// `dispatchable == true` — i.e. the count of compiled traces
3571 /// that would actually exercise the AOT chain-reloc +
3572 /// deploy-resolver path.
3573 pub fn trace_per_exit_inline_dispatchable_count(&self) -> u64 {
3574 self.jit.counters.per_exit_inline_dispatchable
3575 }
3576
3577 /// Diagnostic: max `inline_depth` ever seen on any
3578 /// `RecordedOp` pushed by the recorder. Tells tests + tuning
3579 /// whether a self-recursive function actually walked the depth
3580 /// tracker past 0. Saturates at `MAX_INLINE_DEPTH`. Persists
3581 /// across traces and Vm activations; reset only on `Vm::new`.
3582 pub fn trace_max_depth_seen(&self) -> u8 {
3583 self.jit.max_depth_seen
3584 }
3585
3586 /// Last live Lua frame (the trace head's frame at
3587 /// dispatch time). The frame-materialization helper reads `.base`
3588 /// to compute offsets for each inlined frame's window.
3589 #[doc(hidden)]
3590 pub fn jit_last_lua_frame(&self) -> Option<Frame> {
3591 match self.frames.last() {
3592 Some(CallFrame::Lua(f)) => Some(*f),
3593 _ => None,
3594 }
3595 }
3596
3597 /// Read-only borrow of the current call
3598 /// stack, for the [`crate::vm::inspect`] pure-read accessors used
3599 /// by `luna-tools` (`luna-profile`'s sampler walks this from
3600 /// inside a `Count` hook). Sibling-module scope: not part of the
3601 /// public embedder surface, but `inspect::frames_for_profile` is.
3602 #[doc(hidden)]
3603 pub(super) fn inspect_frames(&self) -> &[CallFrame] {
3604 &self.frames
3605 }
3606
3607 /// Ensure the value stack covers indices
3608 /// `[0..need)`. Extends with Nil if shorter. Called by the
3609 /// frame-materialization helper before pushing an inlined frame
3610 /// whose register window may exceed the current stack length.
3611 #[doc(hidden)]
3612 pub fn jit_ensure_stack(&mut self, need: usize) {
3613 if self.stack.len() < need {
3614 self.stack.resize(need, Value::Nil);
3615 }
3616 }
3617
3618 /// Trace JIT path for `Op::Close A`. Predicts whether
3619 /// `__close` handlers would run (any active tbc slot ≥ from
3620 /// holding a non-nil/false Value); if so, returns 1 without doing
3621 /// anything and the trace side-exits at the op, so the interpreter
3622 /// runs the handlers. Otherwise performs the safe part of close —
3623 /// `close_from(from)` to close open upvals + drop any drained tbc
3624 /// entries ≥ from — and returns 0.
3625 ///
3626 /// Returns are i64-shaped so the cranelift import sig stays
3627 /// trivial (i64 → i64 mapping).
3628 #[doc(hidden)]
3629 pub fn jit_op_close(&mut self, start_offset: u32) -> i64 {
3630 let Some(f) = self.jit_last_lua_frame() else {
3631 return 1;
3632 };
3633 let from = f.base + start_offset;
3634 let has_handler = self.tbc.iter().any(|&s| {
3635 s >= from && {
3636 let v = self.stack[s as usize];
3637 !matches!(v, Value::Nil | Value::Bool(false))
3638 }
3639 });
3640 if has_handler {
3641 self.jit.counters.deopt += 1;
3642 return 1;
3643 }
3644 self.close_from(from);
3645 // Drain any tbc entries ≥ from (they're nil/false stubs the
3646 // interpreter's drive_close would have skipped silently).
3647 while let Some(&s) = self.tbc.last() {
3648 if s < from {
3649 break;
3650 }
3651 self.tbc.pop();
3652 }
3653 0
3654 }
3655
3656 /// Spill the trace's current value for a register to
3657 /// the underlying `vm.stack[base + slot_offset]`. Required before
3658 /// an `Op::Closure` whose inner proto has an `in_stack: true`
3659 /// upval at `slot_offset` — the helper's `find_or_create_upval`
3660 /// captures a live pointer to `vm.stack[base + slot_offset]`,
3661 /// which must hold the right value at call time (trace IR's
3662 /// Variable hasn't yet been written back).
3663 ///
3664 /// Parameters arrive as i64 from the IR: `slot_offset` is the
3665 /// caller-frame register index (`u32` in practice, depth=0
3666 /// only — depth>0 Closure is not supported); `tag` is the
3667 /// `crate::runtime::value::raw` byte for the slot's RegKind;
3668 /// `raw_bits` is the trace Variable's `use_var` payload
3669 /// (i64-shaped — Float is its bit-pattern, Table/Closure is the
3670 /// raw `Gc::as_ptr` cast).
3671 #[doc(hidden)]
3672 pub fn jit_spill_stack(&mut self, slot_offset: u32, tag: u8, raw_bits: u64) {
3673 let Some(f) = self.jit_last_lua_frame() else {
3674 self.jit.pending_err =
3675 Some(self.rt_err("JIT spill: no Lua frame on jit_last_lua_frame()"));
3676 return;
3677 };
3678 let idx = (f.base as usize) + (slot_offset as usize);
3679 if self.stack.len() <= idx {
3680 self.stack.resize(idx + 1, Value::Nil);
3681 }
3682 // SAFETY: caller (trace JIT IR emit) provides matching
3683 // `(tag, raw_bits)` — same shape produced by Value::unpack.
3684 let v = unsafe {
3685 crate::runtime::Value::pack(tag, crate::runtime::value::RawVal { zero: raw_bits })
3686 };
3687 self.stack[idx] = v;
3688 }
3689
3690 /// Refresh only the raw payload of
3691 /// `vm.stack[base + slot_offset]`, preserving its existing
3692 /// `Value` tag. The caller (trace JIT Op::Concat body emit)
3693 /// uses this when the slot's `RegKind` is `Unset` (no compile-
3694 /// time tag info; commonly `Str` slots which the trace doesn't
3695 /// model). The interp's previous execution of the same op
3696 /// already populated the slot with the right tag — the trace
3697 /// only needs to swap in its current raw value.
3698 #[doc(hidden)]
3699 pub fn jit_stack_update_raw(&mut self, slot_offset: u32, raw_bits: u64) {
3700 let Some(f) = self.jit_last_lua_frame() else {
3701 return;
3702 };
3703 let idx = (f.base as usize) + (slot_offset as usize);
3704 if idx >= self.stack.len() {
3705 return;
3706 }
3707 let (tag, _) = self.stack[idx].unpack();
3708 // 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).
3709 self.stack[idx] = unsafe {
3710 crate::runtime::Value::pack(tag, crate::runtime::value::RawVal { zero: raw_bits })
3711 };
3712 }
3713
3714 /// Trace JIT path for `Op::Concat A B`.
3715 ///
3716 /// Mirrors the interp arm (this file ~L5112): `self.top =
3717 /// base + a + n; concat_run(base + a)`. Result lands at
3718 /// `vm.stack[base + a]`. Returns `0` on success, `-1` when the
3719 /// interpreter must do it (any error from `concat_run` OR
3720 /// detection that the metamethod path was taken — `concat_run`
3721 /// returns `Ok(())` after `begin_meta_call` which has pushed a Lua
3722 /// frame the trace can't safely continue past); the trace then
3723 /// side-exits at the op and the interpreter redoes it, raising the
3724 /// error or calling `__concat` itself.
3725 ///
3726 /// The frame-push detection uses `pre/post frames.len()` and
3727 /// unwinds any pushed frames first, so the exit sees a clean stack.
3728 #[doc(hidden)]
3729 pub fn jit_op_concat(&mut self, slot_offset: u32, n: i32) -> i64 {
3730 let Some(f) = self.jit_last_lua_frame() else {
3731 return -1;
3732 };
3733 let abs_a = f.base + slot_offset;
3734 self.top = abs_a + n as u32;
3735 let pre_frames = self.frames.len();
3736 let result = self.concat_run(abs_a);
3737 let post_frames = self.frames.len();
3738 // Frame-push = metamethod path taken (begin_meta_call pushed
3739 // a Lua frame). The trace can't continue past it; unwind +
3740 // deopt so interp redoes Op::Concat in the slow path.
3741 while self.frames.len() > pre_frames {
3742 frames_pop_sync(&mut self.frames, &mut self.frames_top);
3743 }
3744 if result.is_err() || post_frames > pre_frames {
3745 self.jit.counters.deopt += 1;
3746 return -1;
3747 }
3748 0
3749 }
3750
3751 /// Pop a reusable `Vec<u8>` from the JIT
3752 /// accumulator buffer pool, returning a raw pointer. The trace
3753 /// fn's IR holds this pointer in a stack slot through the loop
3754 /// and calls `jit_str_buf_extend` per iter. If the pool is
3755 /// empty, allocate fresh.
3756 ///
3757 /// Safety: the returned pointer is valid until
3758 /// `jit_str_buf_release` is called or the Vm is dropped. The
3759 /// caller MUST not retain it across `enter_jit` boundaries.
3760 #[doc(hidden)]
3761 pub fn jit_str_buf_acquire(&mut self) -> *mut Vec<u8> {
3762 let buf = self.jit.str_buf_pool.pop().unwrap_or_default();
3763 // Move into a Box so the pointer is stable until release.
3764 Box::into_raw(Box::new(buf))
3765 }
3766
3767 /// Return a previously-acquired buffer to the
3768 /// pool, dropping any excess past `jit_str_buf_pool_cap`. The
3769 /// buffer is `clear`ed (capacity retained) so the next acquire
3770 /// gets a ready-to-extend Vec.
3771 ///
3772 /// Safety: `buf` must have been returned by a prior
3773 /// `jit_str_buf_acquire` on the same Vm.
3774 #[doc(hidden)]
3775 #[allow(clippy::not_unsafe_ptr_arg_deref)] // JIT helper: `buf` round-trips through `Box::into_raw`; SAFETY documented below.
3776 pub fn jit_str_buf_release(&mut self, buf: *mut Vec<u8>) {
3777 if buf.is_null() {
3778 return;
3779 }
3780 // 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.
3781 let mut owned = unsafe { Box::from_raw(buf) };
3782 owned.clear();
3783 if self.jit.str_buf_pool.len() < self.jit.str_buf_pool_cap {
3784 self.jit.str_buf_pool.push(*owned);
3785 }
3786 // Else: drop the buffer.
3787 }
3788
3789 /// Append a LuaStr's bytes to the accumulator
3790 /// buffer. The trace IR computes the `str_ptr` (= raw bits of
3791 /// the piece slot) and passes it through; we treat it as a
3792 /// `*mut LuaStr` and append its bytes.
3793 ///
3794 /// Returns 0 on success, -1 if the piece isn't a Str (would
3795 /// trip __concat metamethod path → deopt to interp).
3796 ///
3797 /// Safety: `buf` from prior `acquire`; `str_ptr` from the
3798 /// trace's piece slot raw bits.
3799 #[doc(hidden)]
3800 #[allow(clippy::not_unsafe_ptr_arg_deref)] // JIT helper: `buf` from prior `acquire`; `str_ptr` from trace piece slot; SAFETY documented below.
3801 pub fn jit_str_buf_extend(&mut self, buf: *mut Vec<u8>, str_ptr: i64) -> i64 {
3802 if buf.is_null() || str_ptr == 0 {
3803 return -1;
3804 }
3805 // 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).
3806 let buf = unsafe { &mut *buf };
3807 let lua_str_ptr = str_ptr as *const crate::runtime::string::LuaStr;
3808 // 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).
3809 let bytes = unsafe { crate::runtime::string::bytes_of(lua_str_ptr) };
3810 buf.extend_from_slice(bytes);
3811 0
3812 }
3813
3814 /// Drain the accumulator buffer into a fresh
3815 /// `LuaStr` via `heap.intern`, returning the raw ptr bits for
3816 /// the trace to write into the accumulator slot.
3817 ///
3818 /// Returns the LuaStr ptr as i64 on success, 0 on overflow
3819 /// (the hard cap; the trace deopts).
3820 ///
3821 /// Safety: `buf` from prior `acquire`. The buffer is left
3822 /// CLEAR (drained) ready for `release`.
3823 #[doc(hidden)]
3824 #[allow(clippy::not_unsafe_ptr_arg_deref)] // JIT helper: `buf` from prior `acquire`; SAFETY documented below.
3825 pub fn jit_str_buf_intern(&mut self, buf: *mut Vec<u8>) -> i64 {
3826 if buf.is_null() {
3827 return 0;
3828 }
3829 // 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).
3830 let buf = unsafe { &mut *buf };
3831 let bytes = std::mem::take(buf);
3832 // hard cap at 256KB
3833 if bytes.len() > 256 * 1024 {
3834 return 0;
3835 }
3836 let gc = self.heap.intern(&bytes);
3837 gc.as_ptr() as i64
3838 }
3839
3840 /// Trace JIT helper for `Op::TForCall A 0 C`.
3841 ///
3842 /// Base path: copy R[A..=A+2] → R[A+4..=A+6] + `begin_call`.
3843 /// ipairs `inext` fast path at the top — skip begin_call
3844 /// when R[A]=Native(ipairs_iter), R[A+1]=Table no-mt,
3845 /// R[A+2]=Int.
3846 /// Batched out-ptr writeback — fill ctrl/key/val raws into
3847 /// caller-provided buffers + return R[A+4]'s tag byte. Lets
3848 /// emit skip 3 separate `luna_jit_stack_load` calls and 1
3849 /// `luna_jit_stack_tag` call by reading the buffer via
3850 /// cranelift `stack_load` IR instead. Returns -1 on deopt,
3851 /// else R[A+4]'s tag byte | R[A+5]'s tag byte << 8 (the value's
3852 /// tag only when `nvars >= 2`, 0 otherwise).
3853 #[doc(hidden)]
3854 #[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.
3855 pub fn jit_op_tforcall(
3856 &mut self,
3857 slot_offset: u32,
3858 nvars: i32,
3859 ctrl_out: *mut i64,
3860 key_out: *mut i64,
3861 val_out: *mut i64,
3862 ) -> i64 {
3863 let Some(f) = self.jit_last_lua_frame() else {
3864 return -1;
3865 };
3866 let abs = f.base + slot_offset;
3867 let need = (abs + 7) as usize;
3868 if self.stack.len() < need {
3869 self.stack.resize(need, Value::Nil);
3870 }
3871 // ipairs fast path
3872 let took_fast_path = if let Value::Native(n) = self.stack[abs as usize]
3873 && std::ptr::fn_addr_eq(
3874 n.f,
3875 crate::vm::builtins::ipairs_iter as crate::runtime::value::NativeFn,
3876 )
3877 && let Value::Table(t) = self.stack[(abs + 1) as usize]
3878 && t.metatable().is_none()
3879 && let Value::Int(i) = self.stack[(abs + 2) as usize]
3880 {
3881 let next_i = i.wrapping_add(1);
3882 let v = t.get_int(next_i);
3883 if v.is_nil() {
3884 self.stack[(abs + 4) as usize] = Value::Nil;
3885 } else {
3886 self.stack[(abs + 4) as usize] = Value::Int(next_i);
3887 if (nvars as usize) >= 2 {
3888 self.stack[(abs + 5) as usize] = v;
3889 }
3890 for j in 2..nvars as usize {
3891 let slot = abs + 4 + j as u32;
3892 if (slot as usize) < self.stack.len() {
3893 self.stack[slot as usize] = Value::Nil;
3894 }
3895 }
3896 }
3897 true
3898 } else {
3899 false
3900 };
3901 if !took_fast_path {
3902 // slow path: copy R[A..=A+2] → R[A+4..=A+6], then
3903 // route through begin_call. Lua-closure iters would push
3904 // a Lua frame mid-trace → deopt.
3905 self.stack[(abs + 4) as usize] = self.stack[abs as usize];
3906 self.stack[(abs + 5) as usize] = self.stack[(abs + 1) as usize];
3907 self.stack[(abs + 6) as usize] = self.stack[(abs + 2) as usize];
3908 // the interpreter raises the call's error itself; and a native
3909 // that `begin_call` hands to the interpreter loop (pcall, xpcall,
3910 // pairs, an async native) pushes frames or parks a future
3911 // instead of returning its results here
3912 let runs_to_completion = match self.stack[abs as usize] {
3913 Value::Native(nc) => {
3914 use crate::runtime::value::NativeFn;
3915 !nc.is_async
3916 && ![
3917 nat_pcall as NativeFn,
3918 nat_xpcall as NativeFn,
3919 nat_host_xpcall as NativeFn,
3920 nat_pairs as NativeFn,
3921 ]
3922 .iter()
3923 .any(|&g| std::ptr::fn_addr_eq(nc.f, g))
3924 }
3925 _ => false,
3926 };
3927 if !runs_to_completion || self.begin_call(abs + 4, Some(2), nvars, false).is_err() {
3928 self.jit.counters.deopt += 1;
3929 return -1;
3930 }
3931 }
3932 // Batched writeback — fill the caller's buffers with the
3933 // raw bits of R[A+2] / R[A+4] / R[A+5] so the trace IR can
3934 // reload via cranelift `stack_load` instead of separate
3935 // `luna_jit_stack_load` helper calls.
3936 // 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).
3937 let ctrl_raw = unsafe { self.stack[(abs + 2) as usize].unpack().1.zero };
3938 let (key_tag, key_rv) = self.stack[(abs + 4) as usize].unpack();
3939 // 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).
3940 let key_raw = unsafe { key_rv.zero };
3941 let (val_tag, val_raw) = if (nvars as usize) >= 2 {
3942 let (tag, rv) = self.stack[(abs + 5) as usize].unpack();
3943 // 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).
3944 (tag, unsafe { rv.zero })
3945 } else {
3946 (0, 0u64)
3947 };
3948 // 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).
3949 unsafe {
3950 ctrl_out.write(ctrl_raw as i64);
3951 key_out.write(key_raw as i64);
3952 val_out.write(val_raw as i64);
3953 }
3954 i64::from(key_tag) | i64::from(val_tag) << 8
3955 }
3956
3957 /// Load the raw `i64` payload of
3958 /// `vm.stack[base + slot_offset]` for the active trace's head
3959 /// Lua frame. Used to reload trace IR `Variable`s after a
3960 /// helper has written to `vm.stack` directly (e.g. TForCall's
3961 /// iter results land at `R[A+4..A+4+nvars]`).
3962 #[doc(hidden)]
3963 pub fn jit_stack_load(&mut self, slot_offset: u32) -> i64 {
3964 let Some(f) = self.jit_last_lua_frame() else {
3965 return 0;
3966 };
3967 let idx = (f.base as usize) + (slot_offset as usize);
3968 if idx >= self.stack.len() {
3969 return 0;
3970 }
3971 let v = self.stack[idx];
3972 let (_, raw) = v.unpack();
3973 // 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).
3974 unsafe { raw.zero as i64 }
3975 }
3976
3977 /// Read the tag byte of
3978 /// `vm.stack[base + slot_offset]`. Used by `Op::TForLoop` emit
3979 /// to dispatch on the iterator's return-key tag at runtime
3980 /// (`raw::NIL` → loop end exit, `raw::INT` → continue, other →
3981 /// deopt).
3982 #[doc(hidden)]
3983 pub fn jit_stack_tag(&mut self, slot_offset: u32) -> u8 {
3984 let Some(f) = self.jit_last_lua_frame() else {
3985 return crate::runtime::value::raw::NIL;
3986 };
3987 let idx = (f.base as usize) + (slot_offset as usize);
3988 if idx >= self.stack.len() {
3989 return crate::runtime::value::raw::NIL;
3990 }
3991 self.stack[idx].unpack().0
3992 }
3993
3994 /// Push a Lua frame onto the call stack with
3995 /// JIT-known metadata. Used by `luna_jit_trace_materialize_frames`
3996 /// at trace side-exits to recreate the inlined call activations
3997 /// the lowerer compiled past. The contract (enforced by the
3998 /// lowerer's pre-emit pass): `cl.proto` is non-vararg,
3999 /// `nresults` is the caller's expected count (today always 1
4000 /// because the lowerer bails Op::Call C != 2), and the caller
4001 /// has already called `jit_ensure_stack` to cover
4002 /// `[0..base + cl.proto.max_stack)`.
4003 #[doc(hidden)]
4004 pub fn jit_push_inlined_frame(
4005 &mut self,
4006 cl: Gc<LuaClosure>,
4007 base: u32,
4008 pc: u32,
4009 nresults: i32,
4010 ) {
4011 self.set_frame_ccmt(0);
4012 frames_push_sync(
4013 &mut self.frames,
4014 &mut self.frames_top,
4015 CallFrame::Lua(Frame {
4016 closure: cl,
4017 base,
4018 pc,
4019 // Lua call ABI: callee R[0] sits at caller R[A+1], so
4020 // callee.base = caller.base + A + 1; func_slot is
4021 // caller.base + A = callee.base - 1.
4022 func_slot: base - 1,
4023 n_varargs: 0,
4024 nresults,
4025 hook_oldpc: u32::MAX,
4026 from_c: false,
4027 tm: None,
4028 is_hook: false,
4029 tailcalls: 0,
4030 }),
4031 );
4032 }
4033
4034 /// Toggle precompiled-chunk loading. Default `true`. Sandbox embedders
4035 /// should set to `false` so `load`/`loadstring` reject bytecode input
4036 /// (which bypasses parser limits and could exploit verifier gaps).
4037 pub fn set_bytecode_loading(&mut self, enabled: bool) {
4038 self.bytecode_loading = enabled;
4039 }
4040
4041 /// Current bytecode-loading gate state.
4042 pub fn bytecode_loading(&self) -> bool {
4043 self.bytecode_loading
4044 }
4045
4046 /// Toggle PUC `.luac` bytecode loading. Default `false` — PUC
4047 /// bytecode is a strictly larger trust surface than luna's own dump
4048 /// format (third-party toolchain bugs, malformed chunks, unknown
4049 /// opcode shapes). Enable only for trusted PUC chunks. Per-dialect
4050 /// translators live in `crate::vm::dump::puc`.
4051 pub fn set_puc_bytecode_loading(&mut self, enabled: bool) {
4052 self.puc_bytecode_loading = enabled;
4053 }
4054
4055 /// Current PUC bytecode-loading gate state.
4056 pub fn puc_bytecode_loading(&self) -> bool {
4057 self.puc_bytecode_loading
4058 }
4059
4060 /// Default loader input budget — 256 MiB.
4061 ///
4062 /// `Vm::load` and the Lua-level `load(reader, ...)` both refuse
4063 /// sources whose byte length crosses this cap, returning the
4064 /// PUC-shaped `not enough memory` error rather than letting the
4065 /// host allocator try (and crash) to hold the next chunk.
4066 pub const DEFAULT_LOADER_INPUT_BUDGET: usize = 256 * 1024 * 1024;
4067
4068 /// Set the loader input byte budget (see
4069 /// [`Vm::DEFAULT_LOADER_INPUT_BUDGET`]). Pass `usize::MAX` to
4070 /// effectively disable. Smaller caps are honored verbatim — a 0
4071 /// cap rejects every non-empty source.
4072 pub fn set_loader_input_budget(&mut self, bytes: usize) {
4073 self.loader_input_budget = bytes;
4074 }
4075
4076 /// Current loader input byte budget.
4077 pub fn loader_input_budget(&self) -> usize {
4078 self.loader_input_budget
4079 }
4080
4081 /// Take the error traceback captured at the latest error point and
4082 /// reset it. Embedders should call this immediately after a failed
4083 /// `call_value`/`eval`/`call`/etc. — the next public `call_value`
4084 /// entry clears it. Returns `None` if no error was in flight.
4085 pub fn take_error_traceback(&mut self) -> Option<String> {
4086 let levels = self.error_traceback.take()?;
4087 let tb = crate::vm::callstack::traceback_from_lines(self.version, &levels, 0);
4088 Some(String::from_utf8_lossy(&tb).into_owned())
4089 }
4090
4091 /// PUC `luaL_traceback(L, L, msg, level)` on the running thread: `msg`
4092 /// (when given) and a newline, then `stack traceback:` and one line per
4093 /// stack level from `level` on, level 0 being the running function (the
4094 /// native calling this, when a native does).
4095 pub fn traceback(&mut self, msg: Option<&[u8]>, level: i64) -> Vec<u8> {
4096 let mut out = match msg {
4097 Some(m) => {
4098 let mut out = m.to_vec();
4099 out.push(b'\n');
4100 out
4101 }
4102 None => Vec::new(),
4103 };
4104 out.extend_from_slice(b"stack traceback:");
4105 out.extend(self.traceback_lines(None, level));
4106 out
4107 }
4108
4109 /// Arm the soft memory cap. The run loop checks the
4110 /// heap's tracked byte usage between dispatch turns; on overshoot it
4111 /// first runs a full collect, and if `bytes` still exceeds the cap it
4112 /// raises a catchable `"memory cap exceeded"` Lua error and disarms
4113 /// itself (fire-once: re-arm before the next `call_value` if reusing
4114 /// the Vm across requests). `None` removes the cap. The accounting is
4115 /// approximate — internal Vec/Box capacity overhead is not tracked,
4116 /// so embedders should size the cap with ~2× margin over the desired
4117 /// hard limit and additionally bound the Vm's lifetime (drop after
4118 /// each request).
4119 pub fn set_memory_cap(&mut self, cap: Option<usize>) {
4120 self.heap.mem_cap = cap;
4121 }
4122
4123 /// Approximate bytes the heap is currently holding. Object shells plus
4124 /// every table's internal array/hash boxes (tracked via
4125 /// `Heap::apply_bytes_delta` in `set`/`rehash`/`ensure_*`). Proto
4126 /// bytecode and closure upvalue slices still go uncounted — this is a
4127 /// lower bound, not a precise `malloc_stats`-style total.
4128 pub fn memory_used(&self) -> usize {
4129 self.heap.bytes()
4130 }
4131
4132 /// Read upvalue slot `i` of the native function currently on top of the
4133 /// dispatch chain (the one whose body is executing). Returns `Value::Nil`
4134 /// when no native is running. Public so the C ABI trampoline can fetch
4135 /// the host C function pointer it stashed there at registration time.
4136 pub fn running_native_upvalue(&self, i: usize) -> Value {
4137 match self.running_natives.last() {
4138 // 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).
4139 Some(nc) => unsafe {
4140 let upvals = &(*nc.as_ptr()).upvals;
4141 upvals.get(i).copied().unwrap_or(Value::Nil)
4142 },
4143 None => Value::Nil,
4144 }
4145 }
4146
4147 /// Register a table for finalization if its (just-set) metatable carries a
4148 /// `__gc` metamethod (PUC luaC_checkfinalizer at setmetatable time — adding
4149 /// `__gc` to the metatable afterwards does not retroactively register).
4150 pub(crate) fn check_finalizer(&mut self, t: Gc<Table>) {
4151 // Tables gained finalizers in 5.2; PUC 5.1 runs `__gc` for userdata only.
4152 if self.version == crate::version::LuaVersion::Lua51 {
4153 return;
4154 }
4155 if !self.get_mm(Value::Table(t), Mm::Gc).is_nil() {
4156 self.heap.register_finalizable(t);
4157 }
4158 }
4159
4160 /// Same as [`Self::check_finalizer`] for a userdata. PUC 5.1 attaches the
4161 /// finalizer to the proxy produced by `newproxy(true)` once its metatable
4162 /// gains `__gc`. gc.lua's "testing userdata" section sets `__gc` on the
4163 /// metatable that `newproxy` returned, which then needs to flow through.
4164 /// Kept available for the future 5.2+ `lua_setmetatable` path (which
4165 /// would re-check at metatable-set time); luna's only userdata
4166 /// finalizables today come via `newproxy`, which registers itself.
4167 #[allow(dead_code)]
4168 pub(crate) fn check_finalizer_userdata(&mut self, u: Gc<crate::runtime::Userdata>) {
4169 if !self.get_mm(Value::Userdata(u), Mm::Gc).is_nil() {
4170 self.heap.register_finalizable_userdata(u);
4171 }
4172 }
4173
4174 /// Run pending `__gc` finalizers (objects the collector resurrected for
4175 /// finalization). Finalizer errors are swallowed — PUC turns them into a
4176 /// warning; they must never propagate to the mutator. Reentrancy-guarded.
4177 fn run_finalizers(&mut self) {
4178 let _ = self.run_finalizers_or_err();
4179 }
4180
4181 fn run_finalizers_or_err(&mut self) -> Result<(), LuaError> {
4182 if self.gc_finalizing {
4183 return Ok(());
4184 }
4185 let pending = self.heap.take_tobefnz();
4186 if pending.is_empty() {
4187 return Ok(());
4188 }
4189 self.gc_finalizing = true;
4190 let mut first_err: Option<LuaError> = None;
4191 for obj in pending {
4192 let gc = self.get_mm(obj, Mm::Gc);
4193 // PUC 5.2+ accepts any non-nil `__gc` at setmetatable time to
4194 // schedule the object for finalization (`__gc = true` is the
4195 // canonical placeholder); only call it at finalize time when it
4196 // is actually a function. gc.lua 5.2 :412 wires up exactly this
4197 // sentinel and then expects no call.
4198 let callable = matches!(gc, Value::Closure(_) | Value::Native(_));
4199 if callable {
4200 // PUC `GCTM` sets `CIST_FIN` on the new ci so
4201 // `funcnamefromfinalizer` reports `namewhat = "metamethod"`,
4202 // `name = "__gc"`. luna threads the same outcome through the
4203 // generic `pending_tm` slot: the Lua frame born from this
4204 // call consumes it in `push_frame`. Saved/restored around the
4205 // call in case the handler is a native (which never pops it).
4206 // Bare event name; `frame_name` / `c_frame_name` add the
4207 // `"__"` debug prefix for 5.2/5.3, drop it for 5.4+. Matches
4208 // the convention used by `__close`, `__index`, …
4209 let saved_tm = self.pending_tm.replace("gc");
4210 // PUC `GCTM` runs the finalizer with `luaD_pcall` and no
4211 // message handler
4212 if let Err(e) = self.call_protected(gc, &[obj]) {
4213 // PUC 5.1 GCTM raised the finalizer's error to the
4214 // explicit `collectgarbage()` caller (`gc.lua 5.1 :255`
4215 // baselines on `not pcall(collectgarbage)`). 5.2/5.3
4216 // wrapped it in `error in __gc metamethod (msg)` first
4217 // (`callGCTM` → `luaG_runerror`) but still raised. 5.4
4218 // introduced the warning system and switched to "warn
4219 // then continue" — never re-raise, just route the
4220 // wrapped message through `warn`. gc.lua 5.5 :378 wires
4221 // up `_WARN` capture under the `if T then …` block to
4222 // baseline on the same wrapped string.
4223 if self.version >= LuaVersion::Lua54 {
4224 let inner = self.error_text(&e);
4225 let msg = format!("error in __gc metamethod ({inner})");
4226 self.emit_warn(msg.as_bytes(), false);
4227 } else if first_err.is_none() {
4228 let wrapped = if self.version >= LuaVersion::Lua52 {
4229 let inner = self.error_text(&e);
4230 let msg = format!("error in __gc metamethod ({inner})");
4231 let s = Value::Str(self.heap.intern(msg.as_bytes()));
4232 LuaError(s)
4233 } else {
4234 e
4235 };
4236 first_err = Some(wrapped);
4237 }
4238 }
4239 self.pending_tm = saved_tm;
4240 }
4241 }
4242 self.gc_finalizing = false;
4243 match first_err {
4244 Some(e) => Err(e),
4245 None => Ok(()),
4246 }
4247 }
4248
4249 /// Drive one incremental GC step (PUC `collectgarbage("step", n)`).
4250 /// Crosses up to three phases per call:
4251 /// 1. Pause → seed Propagate (`gc_start_propagate`)
4252 /// 2. Propagate → drain gray up to `budget`; on exhaustion run atomic
4253 /// (`gc_finish_atomic` → tobefnz populated; finalizers
4254 /// run via `run_finalizers`) and enter Sweep
4255 /// 3. Sweep → `gc_sweep_step` up to (residual) `budget`
4256 /// Returns true when this call completed the cycle's sweep (back to
4257 /// Pause). The budget is spent generously across phases — a large `n`
4258 /// can finish a whole cycle in one call (PUC stop-the-world step).
4259 pub(crate) fn gc_step(&mut self, budget: usize) -> bool {
4260 // Re-entry guard: never recurse — `run_finalizers` calls Lua code
4261 // that may hit a safe point and try to step again. Re-entry was OK
4262 // under STW (collect_garbage had its own guard) but here the
4263 // intermediate phase state would corrupt.
4264 if self.gc_finalizing {
4265 return false;
4266 }
4267 if self.heap.gc_phase_is_pause() {
4268 let (roots, extra) = self.gc_roots();
4269 self.heap.gc_start_propagate(&roots, &extra);
4270 }
4271 if self.heap.gc_phase_is_propagate() {
4272 if !self.heap.gc_step_propagate(budget) {
4273 return false;
4274 }
4275 self.heap.gc_finish_atomic();
4276 // any __gc scheduled by atomic — run before sweep so a finalizer
4277 // re-registering `self` re-enters the next cycle, not this sweep
4278 self.run_finalizers();
4279 }
4280 // either we just transitioned, or we entered already in Sweep, or
4281 // a finalizer started a new cycle (gc_sweep_step is a no-op then)
4282 self.heap.gc_sweep_step(budget)
4283 }
4284
4285 // ---- frames & calls ----
4286
4287 /// Begin calling stack[func_slot] with `nargs` (None: up to self.top).
4288 /// Returns true if a Lua frame was pushed (the dispatch loop continues
4289 /// there), false if a native completed inline.
4290 fn begin_call(
4291 &mut self,
4292 func_slot: u32,
4293 nargs: Option<u32>,
4294 nresults: i32,
4295 from_c: bool,
4296 ) -> Result<bool, LuaError> {
4297 let mut nargs = match nargs {
4298 Some(n) => n,
4299 None => self.top - (func_slot + 1),
4300 };
4301 // Consume `pending_is_tail` at the boundary: a tail-call op sets it
4302 // only for the immediately-following Lua activation. Native dispatch
4303 // (or `__call` resolution) below must not let it leak to the next
4304 // begin_call's frame; restore it just before push_frame for the Lua
4305 // arm so its meaning is preserved across __call chaining.
4306 let tailcalls = std::mem::take(&mut self.pending_tailcalls);
4307 let tail_ccmt = std::mem::take(&mut self.pending_ccmt);
4308 // resolve __call handlers iteratively (PUC tryfuncTM loop): each handler
4309 // is inserted before the value so it becomes the first argument, and a
4310 // chain of `__call` tables resolves down to a real function.
4311 let mut chain = 0u32;
4312 loop {
4313 match self.stack[func_slot as usize] {
4314 Value::Closure(cl) => {
4315 // JIT fast path: if the Proto's body fits
4316 // the int-arith whitelist, every arg is `Value::Int`,
4317 // and the cached arity matches, skip frame setup and
4318 // run the cached native fn in-place.
4319 if self.try_jit_call_op(cl, func_slot, nargs, nresults) {
4320 self.pending_tailcalls = tailcalls;
4321 return Ok(false);
4322 }
4323 self.pending_tailcalls = tailcalls;
4324 self.pending_ccmt = if tailcalls > 0 {
4325 tail_ccmt
4326 } else {
4327 chain as u8
4328 };
4329 self.push_frame(cl, func_slot, nargs, nresults, from_c)?;
4330 // Trace-on-call trigger. The frame
4331 // we just pushed is the callee whose body the
4332 // recorder will trace. Bump the per-Proto call
4333 // counter; once it crosses `CALL_HOT_THRESHOLD`
4334 // and no other trace is in flight, snapshot the
4335 // callee's register window (R[0..max_stack]) and
4336 // begin recording at `pc=0`. This is what unlocks
4337 // tracing for functions whose body has no negative
4338 // `Op::Jmp` back-edge (`fib`, recursive helpers).
4339 //
4340 // Gated on `trace_jit_enabled`, so the default
4341 // dispatch pays a single not-taken branch.
4342 if self.jit.trace_enabled {
4343 let proto = cl.proto;
4344 let c = proto.call_hot_count.get();
4345 if c < u32::MAX / 2 {
4346 proto.call_hot_count.set(c + 1);
4347 }
4348 // Relaxed call-trigger:
4349 // `c >= THRESHOLD` (not `c == THRESHOLD`) +
4350 // `!already_cached` short-circuit. Lets a
4351 // discarded short call-trigger close retry
4352 // on the next call (fib(10/15/20/25)
4353 // pathology — first capture is base-case
4354 // [Lt,Jmp,Return1]; coverage-heuristic
4355 // discards; next call gets to record at a
4356 // potentially deeper recursion point).
4357 // Without `already_cached`, the relaxed
4358 // condition would re-record over a cached
4359 // trace every call.
4360 //
4361 // Additionally short-circuit on
4362 // `proto.trace_gave_up`. The per-Proto discard
4363 // cap force-compiles a partial trace and
4364 // flips this flag; subsequent calls into
4365 // this Proto skip the RefCell borrow + Vec
4366 // scan entirely.
4367 if proto.trace_gave_up.get() {
4368 return Ok(true);
4369 }
4370 let call_already_cached =
4371 proto.traces.borrow().iter().any(|t| t.head_pc == 0)
4372 || trace_head_abandoned(proto, 0);
4373 if c >= self.jit.call_hot_threshold
4374 && self.jit.active_trace.is_none()
4375 && !call_already_cached
4376 {
4377 // The new frame is on top: index in
4378 // `self.frames` is `len() - 1`.
4379 let frame_idx = self.frames.len() - 1;
4380 // Snapshot R[0..max_stack] at the callee's
4381 // base. `push_frame` resized `self.stack`
4382 // to `base + max_stack`, so this window is
4383 // guaranteed in-bounds.
4384 let f = match &self.frames[frame_idx] {
4385 CallFrame::Lua(f) => f,
4386 _ => unreachable!("push_frame just pushed a Lua frame"),
4387 };
4388 let max_stack = cl.proto.max_stack as usize;
4389 let base_us = f.base as usize;
4390 let mut entry_tags = Vec::with_capacity(max_stack);
4391 for i in 0..max_stack {
4392 let (tag, _) = self.stack[base_us + i].unpack();
4393 entry_tags.push(tag);
4394 }
4395 self.jit.active_trace =
4396 Some(Box::new(crate::jit::trace::TraceRecord::start(
4397 cl.proto, 0, entry_tags, true,
4398 )));
4399 self.jit.recording_frame_base = frame_idx;
4400 }
4401 }
4402 return Ok(true);
4403 }
4404 Value::Native(nc) => {
4405 // Async-marked NativeClosure.
4406 // Route through the cooperative-yield mechanism
4407 // when async_mode is on; reject when called from
4408 // a sync `eval`/`call_value` path (would have no
4409 // executor to drive the returned future).
4410 if nc.is_async {
4411 if !self.async_mode {
4412 let s = Value::Str(
4413 self.heap.intern(b"async native called in sync context"),
4414 );
4415 self.last_error_kind = crate::vm::error::LuaErrorKind::Runtime;
4416 return Err(LuaError(s));
4417 }
4418 // Same root-up bookkeeping as the sync path:
4419 // pin args + result-count expectation so a
4420 // collection across the suspend boundary
4421 // keeps the arg window live.
4422 self.native_nresults = nresults;
4423 self.gc_top = func_slot + nargs + 1;
4424 // Fire the "call" hook BEFORE
4425 // building the future. Mirrors the sync native
4426 // path's `hook_call(true, nargs)` site
4427 // (`exec.rs` further down) so embedders with a
4428 // Rust debug hook installed see a Call event
4429 // for async natives identical to the sync
4430 // path. The matching "return" hook fires from
4431 // `commit_async_native_result` in
4432 // `async_drive.rs` after the future resolves.
4433 // Placement: after the `native_nresults` / `gc_top`
4434 // pin, before the future is constructed, so a
4435 // hook body that triggers GC observes the
4436 // correct pinned window. On hook error the
4437 // sentinel never returns and
4438 // `pending_async_native_*` remain `None` —
4439 // the executor sees `DispatchOutcome::Error`.
4440 self.hook_call(true, nargs)?;
4441 // Transmute the stored NativeFn back to its
4442 // real AsyncNativeFn shape. Sound because
4443 // `set_async_native` / `create_async_native`
4444 // installed an AsyncNativeFn through the
4445 // identically-sized fn-pointer slot, and the
4446 // `is_async` marker bit is what records that
4447 // fact.
4448 let async_fn: crate::vm::async_drive::AsyncNativeFn =
4449 // SAFETY: same-size fn pointers; provenance
4450 // preserved through `mem::transmute`. The
4451 // `is_async` marker is the only safe-to-call
4452 // gate, set exclusively by
4453 // `Vm::create_async_native`.
4454 unsafe { std::mem::transmute(nc.f) };
4455 let vm_ptr: *mut Vm = self;
4456 let fut = async_fn(vm_ptr, func_slot, nargs);
4457 // Stash the future + post-call context for
4458 // `drive_one` to surface to `EvalFuture::poll`.
4459 self.pending_async_native_fut = Some(fut);
4460 self.pending_async_native_ctx = Some(AsyncNativeCallCtx {
4461 func_slot,
4462 nargs,
4463 nresults,
4464 gc_top: self.gc_top,
4465 });
4466 // Sentinel Err walked up to `drive_one` (same
4467 // shape as `host_yield_pending`'s budget yield).
4468 // Value::Nil — never seen by user code.
4469 return Err(LuaError(Value::Nil));
4470 }
4471 // pcall/xpcall are yieldable: rather than calling the
4472 // protected function through the Rust stack (which cannot be
4473 // suspended), push a continuation frame and drive the call
4474 // through the interpreter loop (PUC lua_pcallk). A yield
4475 // inside it is preserved with the thread's saved frames.
4476 use crate::runtime::value::NativeFn;
4477 if std::ptr::fn_addr_eq(nc.f, nat_pcall as NativeFn) {
4478 return self.begin_pcall(func_slot, nargs, nresults);
4479 }
4480 if std::ptr::fn_addr_eq(nc.f, nat_xpcall as NativeFn) {
4481 // 5.1 `xpcall(f, err)` calls `f` with no arguments
4482 let forward = self.version > LuaVersion::Lua51;
4483 return self.begin_xpcall(func_slot, nargs, nresults, forward);
4484 }
4485 if std::ptr::fn_addr_eq(nc.f, nat_host_xpcall as NativeFn) {
4486 return self.begin_xpcall(func_slot, nargs, nresults, true);
4487 }
4488 // From 5.4 on, pairs(t) calls a __pairs metamethod yieldably
4489 // (PUC luaB_pairs uses lua_callk). 5.2/5.3 use a plain
4490 // lua_call, and 5.1 has no `__pairs`: the native handles those.
4491 if std::ptr::fn_addr_eq(nc.f, nat_pairs as NativeFn)
4492 && nargs >= 1
4493 && self.version >= LuaVersion::Lua54
4494 {
4495 let arg = self.stack[(func_slot + 1) as usize];
4496 if !self.get_mm(arg, Mm::Pairs).is_nil() {
4497 return self.begin_pairs(func_slot, nresults);
4498 }
4499 }
4500 // a native that collects (e.g. `collectgarbage`) roots up to
4501 // its own arguments — the caller's live registers all sit
4502 // below `func_slot` and stay rooted.
4503 self.native_nresults = nresults;
4504 self.gc_top = func_slot + nargs + 1;
4505 // Push the native onto the running-natives chain BEFORE
4506 // firing the call hook so that `debug.getinfo(level)` and
4507 // `arg_error` from inside the hook see this native as the
4508 // currently-running C function (db.lua :344 reads
4509 // `getinfo(2, "f").func` for the just-entered callee).
4510 // Popped after the matching return hook fires — even on
4511 // error, the pop must happen, so the body is bracketed
4512 // through a scope guard.
4513 self.running_natives.push(nc);
4514 self.running_native_acts
4515 .push(crate::vm::callstack::NativeAct {
4516 func_slot,
4517 nargs,
4518 depth: self.frames.len() as u32,
4519 // a tail call resolved its `__call` chain before
4520 // calling here and passed the count in tail_ccmt
4521 ccmt: tail_ccmt + chain as u8,
4522 });
4523 // PUC C-call discipline: entering a C function sets
4524 // L->top to func + 1 + nargs, so a collect triggered
4525 // INSIDE the native (explicit `collectgarbage()`, or
4526 // an allocation crossing the GC threshold) roots the
4527 // whole caller window up to and including the
4528 // arguments. Without this raise the cursor is stale —
4529 // parked at some earlier, possibly much lower
4530 // safe-point — and the collect frees register-held
4531 // values of the native's own caller (use-after-free).
4532 // Never lower it: a re-entrant chain
4533 // (native → Lua → native) must keep the outermost
4534 // window rooted.
4535 self.gc_top = self.gc_top.max(func_slot + 1 + nargs);
4536 // PUC luaD_precall fires the "call" hook for C functions too.
4537 // A yield inside the native (coroutine.yield) propagates an
4538 // Err and the matching "return" hook fires on resume instead.
4539 if let Err(e) = self.hook_call(true, nargs) {
4540 self.running_natives.pop();
4541 self.running_native_acts.pop();
4542 return Err(e);
4543 }
4544 // Trap a Rust panic in the native and surface it as
4545 // a Lua error rather than letting it unwind through the
4546 // VM into the embedder. The VM's internal state may still
4547 // be inconsistent after a panic (half-pushed args,
4548 // dangling GC references), so embedders that catch this
4549 // class of error should drop and re-create the Vm — but
4550 // it's still better than tearing the host process down.
4551 // `AssertUnwindSafe` is sound because the caller is the
4552 // dispatch loop and any half-done state is fenced behind
4553 // the immediate Err return below.
4554 use std::panic::{AssertUnwindSafe, catch_unwind};
4555 let result =
4556 match catch_unwind(AssertUnwindSafe(|| (nc.f)(self, func_slot, nargs))) {
4557 Ok(r) => r,
4558 Err(payload) => {
4559 let msg = panic_payload_str(&payload);
4560 let s = Value::Str(
4561 self.heap.intern(format!("native panic: {msg}").as_bytes()),
4562 );
4563 Err(LuaError(s))
4564 }
4565 };
4566 let nret = match result {
4567 Ok(n) => n,
4568 Err(e) => {
4569 // PUC raises with the native still on the stack;
4570 // remember it for the handler and traceback of the
4571 // error (see `raise_to_handler`)
4572 let act = self.running_native_acts.pop().expect("pushed above");
4573 self.running_natives.pop();
4574 self.note_errored_native(nc, act, e.0);
4575 return Err(e);
4576 }
4577 };
4578 // PUC `luaD_poscall` fires the return hook BEFORE moving
4579 // results into the function's slot — at that point args
4580 // sit at `[func_slot + 1, func_slot + 1 + nargs)` and
4581 // results above them at `[func_slot + 1 + nargs, …)`.
4582 // luna's `nat_return` has already written the results
4583 // into `[func_slot, func_slot + nret)`, so we replay PUC's
4584 // layout by copying the results up past the preserved
4585 // args, firing the hook (with ftransfer = nargs + 1, so
4586 // `getlocal(2, ftransfer..)` reads results), and then
4587 // copying back for `finish_results`. db.lua :541 reads
4588 // `getinfo("r").ftransfer` + `getlocal` to inspect a
4589 // returning native's results this way.
4590 if self.hook.ret
4591 && !self.in_hook
4592 && (self.hook.func.is_some() || self.hook.rust_func.is_some())
4593 {
4594 let res_dst = func_slot + nargs + 1;
4595 let need = (res_dst + nret) as usize;
4596 if self.stack.len() < need {
4597 self.stack.resize(need, Value::Nil);
4598 }
4599 for i in (0..nret).rev() {
4600 self.stack[(res_dst + i) as usize] =
4601 self.stack[(func_slot + i) as usize];
4602 }
4603 // widen the C-frame's argument window for getlocal
4604 if let Some(act) = self.running_native_acts.last_mut() {
4605 act.nargs = nargs + nret;
4606 }
4607 let hr = self.hook_return(true, nargs + 1, nret);
4608 if let Some(act) = self.running_native_acts.last_mut() {
4609 act.nargs = nargs;
4610 }
4611 // restore results into the slot finish_results expects
4612 for i in 0..nret {
4613 self.stack[(func_slot + i) as usize] =
4614 self.stack[(res_dst + i) as usize];
4615 }
4616 self.running_natives.pop();
4617 self.running_native_acts.pop();
4618 hr?;
4619 } else {
4620 self.running_natives.pop();
4621 self.running_native_acts.pop();
4622 }
4623 self.finish_results(func_slot, nret, nresults);
4624 // the native may have allocated; collect with the results as
4625 // the live boundary (PUC checks GC after a call returns).
4626 self.maybe_collect_garbage(self.top);
4627 return Ok(false);
4628 }
4629 v => {
4630 let mm = self.get_mm(v, Mm::Call);
4631 if mm.is_nil() || self.call_mm_unusable(mm) {
4632 return Err(self.call_err(v));
4633 }
4634 chain += 1;
4635 // PUC 5.5 dropped the chain cap from `MAXTAGRECUR = 200`
4636 // (the value 5.4's `lvm.c` uses) down to `MAXCCMT = 16`,
4637 // and the 5.5 test exercises the new tight bound directly
4638 // (calls.lua :225 builds a 16-deep chain and expects the
4639 // 16th to error). 5.4 calls.lua :194 instead builds a 20-
4640 // deep chain and expects it to succeed.
4641 let cap = if self.version >= crate::version::LuaVersion::Lua55 {
4642 15
4643 } else {
4644 MAX_CCMT
4645 };
4646 if chain > cap {
4647 return Err(self.rt_err("'__call' chain too long"));
4648 }
4649 // slots above shift by one; at a call site those are dead
4650 // temps of the current frame
4651 self.stack.insert(func_slot as usize, mm);
4652 if self.top > func_slot {
4653 self.top += 1;
4654 }
4655 nargs += 1;
4656 }
4657 }
4658 }
4659 }
4660
4661 /// Up to 5.3 `tryfuncTM` takes one `__call` hop and needs a function
4662 /// there; anything else is a call error on the original object. 5.4
4663 /// retries the call with whatever `__call` holds, so chains resolve.
4664 fn call_mm_unusable(&self, mm: Value) -> bool {
4665 self.version <= LuaVersion::Lua53 && !matches!(mm, Value::Closure(_) | Value::Native(_))
4666 }
4667
4668 fn push_frame(
4669 &mut self,
4670 cl: Gc<LuaClosure>,
4671 func_slot: u32,
4672 nargs: u32,
4673 nresults: i32,
4674 from_c: bool,
4675 ) -> Result<(), LuaError> {
4676 if func_slot + 256 > MAX_LUA_STACK {
4677 // PUC `luaD_growstack`: the overflow raises "stack overflow" and
4678 // leaves ERRORSTACKSIZE's extra slots for the xpcall handler that
4679 // runs on it; overflowing those is LUA_ERRERR, "error in error
4680 // handling" (errors.lua :606, cstack.lua :29).
4681 if self.msgh_depth == 0 {
4682 return Err(self.rt_err("stack overflow"));
4683 }
4684 if func_slot + 256 > MAX_LUA_STACK + ERROR_STACK_EXTRA {
4685 return Err(self.plain_err("error in error handling"));
4686 }
4687 }
4688 let proto = cl.proto;
4689 let nparams = proto.num_params as u32;
4690 // 5.5 vararg layout (PUC luaT_adjustvarargs): the extra args stay on the
4691 // stack just below the new `base`, so a named vararg can be indexed
4692 // virtually without allocating a table. Rotate `[p1..pn][e1..em]` to
4693 // `[e1..em][p1..pn]` so the fixed params land at the new base.
4694 let n_varargs = if proto.is_vararg {
4695 nargs.saturating_sub(nparams)
4696 } else {
4697 0
4698 };
4699 if n_varargs > 0 {
4700 let s = (func_slot + 1) as usize;
4701 self.stack[s..s + nargs as usize].rotate_left(nparams as usize);
4702 }
4703 let base = func_slot + 1 + n_varargs;
4704 let need = (base + proto.max_stack as u32) as usize;
4705 if self.stack.len() < need {
4706 self.stack.resize(need, Value::Nil);
4707 }
4708 // wipe the register window beyond the kept parameters (stale values —
4709 // required for GC-safety and codegen). The varargs below `base` survive.
4710 let kept = nargs.saturating_sub(n_varargs).min(nparams);
4711 // SAFETY: just resized above so `need <= stack.len()`; `base + kept <=
4712 // need` since `base + nparams <= base + max_stack = need` and `kept <=
4713 // nparams`. `slice::fill` lowers to a single memset on Copy types.
4714 unsafe {
4715 self.stack
4716 .get_unchecked_mut((base + kept) as usize..need)
4717 .fill(Value::Nil);
4718 }
4719 let ccmt = std::mem::take(&mut self.pending_ccmt);
4720 self.set_frame_ccmt(ccmt);
4721 frames_push_sync(
4722 &mut self.frames,
4723 &mut self.frames_top,
4724 CallFrame::Lua(Frame {
4725 closure: cl,
4726 base,
4727 pc: 0,
4728 func_slot,
4729 nresults,
4730 hook_oldpc: u32::MAX,
4731 from_c,
4732 n_varargs,
4733 // single-shot consume: `close_slots` sets pending_tm before each
4734 // handler call; the next Lua frame born is that handler's.
4735 tm: self.pending_tm.take(),
4736 // `run_hook` sets `pending_is_hook` before dispatching the user
4737 // hook so its frame reports `namewhat = "hook"` via getinfo.
4738 is_hook: std::mem::take(&mut self.pending_is_hook),
4739 tailcalls: std::mem::take(&mut self.pending_tailcalls),
4740 }),
4741 );
4742 // PUC 5.1 `LUAI_COMPAT_VARARG`: populate the hidden `arg` local with
4743 // `{ n = n_varargs, [1] = e1, [2] = e2, … }`. The compiler reserved
4744 // the slot at `base + nparams`; the extras sit just below `base` from
4745 // the vararg rotate above. 5.1 db.lua :279 reads `arg.n` from a line
4746 // hook; vararg.lua's contradictory expectations were already going to
4747 // fail either way (some asserts want `arg == nil`).
4748 if proto.has_compat_vararg_arg {
4749 let arg_slot = (base + nparams) as usize;
4750 let t = self.heap.new_table();
4751 {
4752 // 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).
4753 let tm = unsafe { t.as_mut() };
4754 for i in 0..n_varargs {
4755 let v = self.stack[(base - n_varargs + i) as usize];
4756 // bounded by `n_varargs` (≤ MAXUPVAL territory), well
4757 // below `MAX_ASIZE`
4758 let _ = tm.set_int(&mut self.heap, (i + 1) as i64, v);
4759 }
4760 let nk = Value::Str(self.heap.intern(b"n"));
4761 tm.set(&mut self.heap, nk, Value::Int(n_varargs as i64))
4762 .expect("'n' key");
4763 }
4764 // once-per-table barrier mirrors SETLIST: t is born BLACK during
4765 // Propagate and the bulk `set_int`/`set` calls above don't barrier
4766 self.heap
4767 .barrier_back(t.as_ptr() as *mut crate::runtime::heap::GcHeader);
4768 self.stack[arg_slot] = Value::Table(t);
4769 }
4770 // PUC luaD_precall fires the "call" hook with the new frame current, so
4771 // a hook calling debug.getinfo(2) sees the entered function. For a Lua
4772 // callee, PUC `luaD_hookcall` passes `p->numparams` as ntransfer (only
4773 // fixed params count — extras already live below `base`).
4774 // A frame born via OP_TailCall fires "tail call" instead (PUC
4775 // luaD_pretailcall) and skips the matching "return" hook on exit.
4776 let is_tail = self
4777 .frames
4778 .last()
4779 .and_then(|f| f.lua())
4780 .is_some_and(|f| f.tailcalls > 0);
4781 self.hook_call_with(false, nparams, is_tail)?;
4782 Ok(())
4783 }
4784
4785 /// `pcall(f, ...)` (PUC luaB_pcall): push a continuation frame, then drive
4786 /// the protected call `f` through the interpreter loop. The protected
4787 /// function and its arguments already sit at `func_slot+1..`, so calling `f`
4788 /// at `func_slot+1` lets its results land one slot above the continuation —
4789 /// the loop head then writes `true` at `func_slot` to form `true, results…`.
4790 /// Always returns `Ok(true)`: a continuation is now on the stack to be
4791 /// resolved by the loop (even when `f` is a native that already ran inline).
4792 fn begin_pcall(&mut self, func_slot: u32, nargs: u32, nresults: i32) -> Result<bool, LuaError> {
4793 if nargs == 0 {
4794 // `luaL_checkany` fails here: there is no function to call.
4795 self.with_native_running(func_slot, nargs, |vm| {
4796 let a = crate::vm::argcheck::Args::new(func_slot, nargs);
4797 crate::vm::argcheck::check_any(vm, a, 0).map(drop)
4798 })?;
4799 }
4800 if self.pcall_depth >= MAX_C_DEPTH {
4801 // raised inside pcall, a C function: no position
4802 return Err(self.plain_err("C stack overflow"));
4803 }
4804 self.pcall_depth += 1;
4805 frames_push_sync(
4806 &mut self.frames,
4807 &mut self.frames_top,
4808 CallFrame::Cont(NativeCont {
4809 kind: ContKind::Pcall,
4810 func_slot,
4811 nresults,
4812 }),
4813 );
4814 // call f (slot func_slot+1) with the remaining args, asking for all
4815 // results; a yield or error inside propagates with the continuation kept
4816 // on the stack (caught by `unwind` / preserved across a yield).
4817 self.begin_call(func_slot + 1, Some(nargs - 1), -1, true)?;
4818 Ok(true)
4819 }
4820
4821 /// `xpcall(f, msgh, ...)` (PUC luaB_xpcall): like `begin_pcall`, but the
4822 /// message handler is stashed in the continuation and the arguments are
4823 /// shifted down over the handler's slot so `f`'s args are contiguous.
4824 /// `forward` is false for 5.1's `xpcall`, which passes `f` none of them.
4825 fn begin_xpcall(
4826 &mut self,
4827 func_slot: u32,
4828 nargs: u32,
4829 nresults: i32,
4830 forward: bool,
4831 ) -> Result<bool, LuaError> {
4832 self.with_native_running(func_slot, nargs, |vm| {
4833 let a = crate::vm::argcheck::Args::new(func_slot, nargs);
4834 crate::vm::builtins::xpcall_handler(vm, a).map(drop)
4835 })?;
4836 if self.pcall_depth >= MAX_C_DEPTH {
4837 // raised inside pcall, a C function: no position
4838 return Err(self.plain_err("C stack overflow"));
4839 }
4840 self.pcall_depth += 1;
4841 // layout: [xpcall@func_slot, f@+1, msgh@+2, a1@+3, ...]. Stash msgh and
4842 // close its gap so f's args become [f@+1, a1@+2, ...].
4843 let handler = self.stack[(func_slot + 2) as usize];
4844 // 5.1: `xpcall (f, err)` takes exactly two parameters — extra
4845 // arguments are NOT forwarded to `f` (5.2 added forwarding;
4846 // 5.1 calls f with zero args).
4847 let nfargs = if forward { nargs - 2 } else { 0 };
4848 for i in 0..nfargs {
4849 self.stack[(func_slot + 2 + i) as usize] = self.stack[(func_slot + 3 + i) as usize];
4850 }
4851 self.top = func_slot + 2 + nfargs;
4852 frames_push_sync(
4853 &mut self.frames,
4854 &mut self.frames_top,
4855 CallFrame::Cont(NativeCont {
4856 kind: ContKind::Xpcall { handler },
4857 func_slot,
4858 nresults,
4859 }),
4860 );
4861 self.begin_call(func_slot + 1, Some(nfargs), -1, true)?;
4862 Ok(true)
4863 }
4864
4865 /// `pairs(t)` where `t` has a `__pairs` metamethod (PUC luaB_pairs's
4866 /// lua_callk path): drive `__pairs(t)` through the loop with a `Pairs`
4867 /// continuation so a `coroutine.yield` inside it suspends cleanly. The
4868 /// metamethod is called in `pairs`'s own slot, so its (≤4, nil-padded)
4869 /// results land exactly where `pairs`'s results belong.
4870 /// Run a check of the native at `func_slot` while it counts as the running
4871 /// C function, so an argument error names it the way PUC does. pcall and
4872 /// xpcall check their arguments in the dispatcher, before the native
4873 /// would otherwise be entered.
4874 fn with_native_running(
4875 &mut self,
4876 func_slot: u32,
4877 nargs: u32,
4878 check: impl FnOnce(&mut Vm) -> Result<(), LuaError>,
4879 ) -> Result<(), LuaError> {
4880 let Value::Native(nc) = self.stack[func_slot as usize] else {
4881 unreachable!("pcall/xpcall dispatch sits on a native")
4882 };
4883 self.running_natives.push(nc);
4884 self.running_native_acts
4885 .push(crate::vm::callstack::NativeAct {
4886 func_slot,
4887 nargs,
4888 depth: self.frames.len() as u32,
4889 ccmt: 0,
4890 });
4891 let r = check(self);
4892 self.running_natives.pop();
4893 self.running_native_acts.pop();
4894 r
4895 }
4896
4897 fn begin_pairs(&mut self, func_slot: u32, nresults: i32) -> Result<bool, LuaError> {
4898 let arg = self.stack[(func_slot + 1) as usize];
4899 let mm = self.get_mm(arg, Mm::Pairs);
4900 // layout becomes [pairs@func_slot, mm@func_slot+1, t@func_slot+2]:
4901 // `pairs` keeps its slot so the debug interface can report it as the
4902 // C function running below the metamethod. Call mm(t) wanting 4.
4903 let need = (func_slot + 3) as usize;
4904 if self.stack.len() < need {
4905 self.stack.resize(need, Value::Nil);
4906 }
4907 self.stack[(func_slot + 2) as usize] = arg;
4908 self.stack[(func_slot + 1) as usize] = mm;
4909 self.top = func_slot + 3;
4910 frames_push_sync(
4911 &mut self.frames,
4912 &mut self.frames_top,
4913 CallFrame::Cont(NativeCont {
4914 kind: ContKind::Pairs,
4915 func_slot,
4916 nresults,
4917 }),
4918 );
4919 let want = crate::vm::builtins::pairs_mm_results(self) as i32;
4920 self.begin_call(func_slot + 1, Some(1), want, true)?;
4921 Ok(true)
4922 }
4923
4924 /// The running (top) Lua frame. The interpreter only reads this while a Lua
4925 /// frame is on top — a continuation frame is never the running frame (it is
4926 /// consumed the instant the call it protects unwinds onto it).
4927 #[inline]
4928 /// Records the `__call` count of the Lua frame about to be pushed.
4929 fn set_frame_ccmt(&mut self, ccmt: u8) {
4930 let i = self.frames.len();
4931 if self.frame_ccmt.len() <= i {
4932 self.frame_ccmt.resize(i + 1, 0);
4933 }
4934 self.frame_ccmt[i] = ccmt;
4935 }
4936
4937 fn top_frame(&self) -> &Frame {
4938 self.frames
4939 .last()
4940 .and_then(CallFrame::lua)
4941 .expect("running Lua frame")
4942 }
4943
4944 #[inline]
4945 fn top_frame_mut(&mut self) -> &mut Frame {
4946 self.frames
4947 .last_mut()
4948 .and_then(CallFrame::lua_mut)
4949 .expect("running Lua frame")
4950 }
4951
4952 /// Pad/announce results sitting at func_slot.
4953 pub(crate) fn finish_results(&mut self, func_slot: u32, nret: u32, wanted: i32) {
4954 // Capture the call's high-water-mark before
4955 // setting the new top so we can Nil-clear slots that the
4956 // call temporarily wrote but no longer holds — matching
4957 // PUC's `L->top` discipline (slots past L->top are "free"
4958 // and the next push overwrites them). Without this clear,
4959 // a stale `Value::Closure` (e.g. the called function
4960 // itself, when wanted = 0) sits at `func_slot` and a
4961 // later GC with wider `gc_top` traces it after the
4962 // closure has been freed by a previous narrow safe-point
4963 // GC → heap-buffer-overflow in `Marker::header` (sort.lua
4964 // comparator case).
4965 let prev_top = self.top as usize;
4966 if wanted < 0 {
4967 self.top = func_slot + nret;
4968 } else {
4969 let wanted = wanted as u32;
4970 let need = (func_slot + wanted) as usize;
4971 if self.stack.len() < need {
4972 self.stack.resize(need, Value::Nil);
4973 }
4974 for i in nret..wanted {
4975 self.stack[(func_slot + i) as usize] = Value::Nil;
4976 }
4977 self.top = func_slot + wanted;
4978 }
4979 let new_top = self.top as usize;
4980 let clear_end = prev_top.min(self.stack.len());
4981 if new_top < clear_end {
4982 for slot in &mut self.stack[new_top..clear_end] {
4983 *slot = Value::Nil;
4984 }
4985 }
4986 }
4987
4988 /// Current Lua call-frame depth (read-only).
4989 /// Used by `EvalFuture` on the bootstrap poll to compute the
4990 /// `entry_depth` it will pass to subsequent resume slices.
4991 pub(crate) fn frame_count(&self) -> usize {
4992 self.frames.len()
4993 }
4994
4995 fn take_results(&mut self, func_slot: u32) -> Vec<Value> {
4996 let nret = self.top - func_slot;
4997 let out = self.stack[func_slot as usize..(func_slot + nret) as usize].to_vec();
4998 self.stack.truncate(func_slot as usize);
4999 self.top = func_slot;
5000 out
5001 }
5002
5003 // ---- open upvalues ----
5004
5005 #[doc(hidden)]
5006 pub fn find_or_create_upval(&mut self, slot: u32) -> Gc<Upvalue> {
5007 match self.open_upvals.binary_search_by_key(&slot, |&(s, _)| s) {
5008 Ok(i) => self.open_upvals[i].1,
5009 Err(i) => {
5010 let uv = self.heap.new_upvalue(UpvalState::Open {
5011 slot,
5012 thread: self.current,
5013 });
5014 self.open_upvals.insert(i, (slot, uv));
5015 uv
5016 }
5017 }
5018 }
5019
5020 pub(crate) fn close_from(&mut self, slot: u32) {
5021 while let Some(&(s, uv)) = self.open_upvals.last() {
5022 if s < slot {
5023 break;
5024 }
5025 let v = self.stack[s as usize];
5026 // 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).
5027 unsafe { uv.as_mut() }.set_closed(v);
5028 self.heap
5029 .barrier_forward(uv.as_ptr() as *mut crate::runtime::heap::GcHeader, v);
5030 self.open_upvals.pop();
5031 }
5032 }
5033
5034 /// Register a to-be-closed slot (TBC op / generic-for closing value).
5035 fn register_tbc(&mut self, slot: u32) -> Result<(), LuaError> {
5036 let v = self.stack[slot as usize];
5037 if matches!(v, Value::Nil | Value::Bool(false)) {
5038 return Ok(()); // nil and false are silently ignored
5039 }
5040 if self.get_mm(v, Mm::Close).is_nil() {
5041 // PUC `checkclosemth`: "variable '<name>' got a non-closable
5042 // value", the name as `luaG_findlocal` gives it — the frame's
5043 // locvars at this pc, else "(temporary)".
5044 let f = self.top_frame();
5045 let reg = slot - f.base;
5046 let pc = (f.pc as usize).saturating_sub(1);
5047 let name = crate::vm::objname::getlocalname(&f.closure.proto, reg, pc)
5048 .unwrap_or("(temporary)");
5049 return Err(self.rt_err(&format!("variable '{name}' got a non-closable value")));
5050 }
5051 // compiled code registers in register order and closes before it
5052 // registers a slot again; only a crafted chunk (`TBC R0; TBC R0`)
5053 // breaks that, which PUC's list cannot represent either
5054 if self.tbc.last().is_some_and(|&s| s >= slot) {
5055 return Err(self.rt_err("'<close>' state corrupted"));
5056 }
5057 self.tbc.push(slot);
5058 Ok(())
5059 }
5060
5061 /// Close upvalues and run `__close` handlers for slots ≥ `from`
5062 /// (handlers in reverse registration order; PUC luaF_close).
5063 fn close_slots(&mut self, from: u32, err: Option<Value>) -> Result<(), LuaError> {
5064 self.close_from(from);
5065 // PUC: handlers run in reverse declaration order; an error raised by a
5066 // handler becomes the error object passed to the remaining ones, and
5067 // the rest are still closed. The last raised error propagates.
5068 let mut pending = err;
5069 let mut result = Ok(());
5070 let saved_err = self.closing_err;
5071 // On a normal close the handler runs within the closing function's
5072 // activation (debug parent = that function); during error unwinding the
5073 // function's frame is already gone, so the handler sits at the C
5074 // boundary instead (PUC: luaF_close runs after the ci is restored).
5075 let error_close = err.is_some();
5076 while let Some(&s) = self.tbc.last() {
5077 if s < from {
5078 break;
5079 }
5080 self.tbc.pop();
5081 let v = self.stack[s as usize];
5082 if matches!(v, Value::Nil | Value::Bool(false)) {
5083 continue;
5084 }
5085 let mm = self.get_mm(v, Mm::Close);
5086 if mm.is_nil() {
5087 // PUC `prepclosingmethod`: the __close metamethod was present
5088 // at OP_TBC (else we would have errored there) but has since
5089 // been removed/replaced. Treat as a non-callable target.
5090 let tn = self.obj_typename(v);
5091 let e = self.rt_err(&format!(
5092 "attempt to call a {tn} value (metamethod 'close')"
5093 ));
5094 pending = Some(e.0);
5095 result = Err(e);
5096 continue;
5097 }
5098 // root the pending error: a handler may trigger a collection
5099 self.closing_err = pending;
5100 // PUC `luaF_close` sets `ci->u.l.tm = TM_CLOSE` so traceback /
5101 // getinfo report the handler as "in metamethod 'close'". Saved/
5102 // restored around the call to cover the path where `mm` is a
5103 // native (`push_frame` never consumes it) or it raises before
5104 // reaching push_frame.
5105 let saved_tm = self.pending_tm.replace("close");
5106 // PUC 5.4 `prepclosingmethod` always pushed (obj, errobj) — errobj
5107 // is nil on a normal close (5.4 locals.lua :875's
5108 // `func2close(coroutine.yield)` wrap pins `(self, nil)` back
5109 // through the yield). PUC 5.5 dropped the trailing nil: a clean
5110 // close passes only `obj`, the error case still passes both
5111 // (5.5 locals.lua :314 `select("#", ...) == n` with n=1 for the
5112 // normal-close arms, n=2 for the error arm).
5113 let call = match pending {
5114 Some(e) => self.call_value_impl(mm, &[v, e], error_close),
5115 None => {
5116 if self.version >= LuaVersion::Lua55 {
5117 self.call_value_impl(mm, &[v], error_close)
5118 } else {
5119 self.call_value_impl(mm, &[v, Value::Nil], error_close)
5120 }
5121 }
5122 };
5123 self.pending_tm = saved_tm;
5124 if let Err(e) = call {
5125 pending = Some(e.0);
5126 result = Err(e);
5127 }
5128 }
5129 self.closing_err = saved_err;
5130 result
5131 }
5132
5133 /// Yieldable variant of `close_slots`: drive the chain of `__close`
5134 /// handlers for slots ≥ `from` through the interpreter loop with a
5135 /// `Cont::Close` continuation, so a `coroutine.yield()` inside any handler
5136 /// suspends cleanly (the close iteration's state rides on the thread's
5137 /// frame/stack like any other suspended call) — PUC's `lua_callk` pattern
5138 /// applied to `luaF_close`. `after` runs when every slot is closed; if
5139 /// `after` is `Return` and we've returned past `entry_depth`,
5140 /// `Ok(Some(vals))` carries the result up to the host caller.
5141 fn begin_close(
5142 &mut self,
5143 from: u32,
5144 err: Option<Value>,
5145 after: AfterClose,
5146 entry_depth: usize,
5147 ) -> Result<Option<Vec<Value>>, LuaError> {
5148 self.close_from(from);
5149 self.drive_close(from, err, after, entry_depth)
5150 }
5151
5152 /// Pop tbc slots ≥ `from`, skipping nil/false and synthesising a
5153 /// non-callable-mm error for an `__close` that was reset to a bad value
5154 /// between OP_TBC and now (PUC `prepclosingmethod`). The first real
5155 /// handler pushes a `Cont::Close` + `begin_call` and returns `Ok(None)`;
5156 /// the interpreter then drives the handler and re-enters this driver via
5157 /// the `Cont::Close` consumer in `run()`. When the chain is exhausted,
5158 /// the threaded error (if any) propagates or `after` fires.
5159 fn drive_close(
5160 &mut self,
5161 from: u32,
5162 mut pending: Option<Value>,
5163 after: AfterClose,
5164 entry_depth: usize,
5165 ) -> Result<Option<Vec<Value>>, LuaError> {
5166 loop {
5167 let drained = match self.tbc.last() {
5168 None => true,
5169 Some(&s) => s < from,
5170 };
5171 if drained {
5172 return self.finish_close_after(after, pending, entry_depth);
5173 }
5174 let s = self.tbc.pop().expect("tbc non-empty");
5175 let v = self.stack[s as usize];
5176 if matches!(v, Value::Nil | Value::Bool(false)) {
5177 continue;
5178 }
5179 let mm = self.get_mm(v, Mm::Close);
5180 if mm.is_nil() {
5181 let tn = self.obj_typename(v);
5182 let e = self.rt_err(&format!(
5183 "attempt to call a {tn} value (metamethod 'close')"
5184 ));
5185 pending = Some(e.0);
5186 continue;
5187 }
5188 // A real handler: stage [mm, v, (err?)] above the current top,
5189 // record the close iteration state in a Cont::Close, and let the
5190 // interpreter dispatch the handler. On return the run() head
5191 // re-enters this driver via the Cont::Close consumer.
5192 let func_slot = self.top;
5193 let error_close = pending.is_some();
5194 let need = (func_slot + 3) as usize;
5195 if self.stack.len() < need {
5196 self.stack.resize(need, Value::Nil);
5197 }
5198 self.stack[func_slot as usize] = mm;
5199 self.stack[func_slot as usize + 1] = v;
5200 // PUC 5.4 always passes (obj, errobj=nil) on a normal close;
5201 // 5.5 drops the trailing nil. 5.4 locals.lua :875 vs 5.5 :314.
5202 let nargs = match pending {
5203 Some(e) => {
5204 self.stack[func_slot as usize + 2] = e;
5205 2u32
5206 }
5207 None => {
5208 if self.version >= LuaVersion::Lua55 {
5209 1u32
5210 } else {
5211 self.stack[func_slot as usize + 2] = Value::Nil;
5212 2u32
5213 }
5214 }
5215 };
5216 self.top = func_slot + 1 + nargs;
5217 // Root the pending error during the call (a handler may collect).
5218 let saved_err = self.closing_err;
5219 self.closing_err = pending;
5220 // PUC `luaF_close` flags the handler frame as "metamethod 'close'"
5221 // for traceback / getinfo.
5222 let saved_tm = self.pending_tm.replace("close");
5223 frames_push_sync(
5224 &mut self.frames,
5225 &mut self.frames_top,
5226 CallFrame::Cont(NativeCont {
5227 kind: ContKind::Close(CloseCont {
5228 from,
5229 pending,
5230 after,
5231 }),
5232 func_slot,
5233 nresults: 0,
5234 }),
5235 );
5236 // PUC luaF_close runs a normal close *within* the closing
5237 // function's activation (debug parent = that function); during an
5238 // error unwind the function's frame is already gone and the
5239 // handler sits at the C boundary instead.
5240 let r = self.begin_call(func_slot, Some(nargs), 0, error_close);
5241 self.pending_tm = saved_tm;
5242 self.closing_err = saved_err;
5243 r?;
5244 return Ok(None);
5245 }
5246 }
5247
5248 /// Fire `after` once every `__close` handler has run. `Block` propagates
5249 /// any remaining error or simply continues; `Return` performs OP_Return's
5250 /// tail (hook + frame pop + result delivery) and may surface results to
5251 /// the host when the function whose return triggered the close was the
5252 /// entry activation, but only on a clean drain — a pending error skips
5253 /// the return tail and propagates instead. `ResumeUnwind` pops the
5254 /// deferred Lua frame and re-raises, letting a handler's own error win
5255 /// over the original propagating one (PUC luaF_close).
5256 fn finish_close_after(
5257 &mut self,
5258 after: AfterClose,
5259 pending: Option<Value>,
5260 entry_depth: usize,
5261 ) -> Result<Option<Vec<Value>>, LuaError> {
5262 match after {
5263 AfterClose::Block => match pending {
5264 Some(e) => Err(LuaError(e)),
5265 None => Ok(None),
5266 },
5267 AfterClose::Return {
5268 abs_a,
5269 nret,
5270 from_native,
5271 } => match pending {
5272 Some(e) => Err(LuaError(e)),
5273 None => self.complete_return(abs_a, nret, from_native, entry_depth),
5274 },
5275 AfterClose::ResumeUnwind { func_slot, err } => {
5276 // The aborting Lua frame was popped before `begin_close`;
5277 // restore the catcher's stack window down to `func_slot` and
5278 // re-raise — preferring a handler-raised error over the
5279 // original (PUC luaF_close).
5280 self.stack.truncate(func_slot as usize);
5281 self.top = func_slot;
5282 self.tbc.retain(|&s| s < func_slot);
5283 Err(LuaError(pending.unwrap_or(err)))
5284 }
5285 }
5286 }
5287
5288 /// OP_Return's post-close tail: fire the "return" hook (frame still
5289 /// current), pop the Lua frame, slide results into `func_slot`, then
5290 /// either hand them to the host (`Ok(Some(vals))` when we've returned
5291 /// past `entry_depth`), leave them contiguous for an exposed
5292 /// pcall/xpcall continuation, or finish into the caller's expected
5293 /// result slot. Mirrors the synchronous OP_Return tail so both paths
5294 /// share semantics — the `from_native` flag selects the right "return"
5295 /// hook context for `hook_return`.
5296 fn complete_return(
5297 &mut self,
5298 abs_a: u32,
5299 nret: u32,
5300 from_native: bool,
5301 entry_depth: usize,
5302 ) -> Result<Option<Vec<Value>>, LuaError> {
5303 // ftransfer is the local index (1-based) of the first result, as
5304 // `getinfo("r").ftransfer + getlocal(level, k)` consumes it. luna
5305 // exposes locals starting at `frame.base` (= func_slot + 1 +
5306 // n_varargs for a vararg call), so the conversion is the absolute
5307 // result slot minus base, plus one to make it 1-based. db.lua 5.4
5308 // :542 (`foo1(); on=false; eqseq(out, {10, 0})`) pins the vararg
5309 // shape end-to-end.
5310 let ftransfer = self
5311 .frames
5312 .last()
5313 .and_then(CallFrame::lua)
5314 .map(|fr| {
5315 let raw = abs_a.saturating_sub(fr.base) + 1;
5316 // 5.5 anonymous-vararg functions get a `(vararg table)` pseudo
5317 // local injected at index `numparams + 1`, so getlocal
5318 // numbering shifts results past it (5.5 db.lua :539
5319 // `eqseq(out, {10, 0})`). 5.4 and earlier have no such pseudo.
5320 if fr.closure.proto.has_vararg_table_pseudo {
5321 raw + 1
5322 } else {
5323 raw
5324 }
5325 })
5326 .unwrap_or(1);
5327 // PUC 5.1 `luaD_poscall`: fire one extra "tail return" hook event
5328 // per tail call that collapsed into this activation, *after* its
5329 // own "return". `tailcalls` tracks that count exactly (PUC
5330 // `ci->u.l.tailcalls`). 5.2+ retired LUA_HOOKTAILRET, so the
5331 // "return" hook fires once even when the activation absorbed
5332 // multiple tail calls — only `istailcall` on getinfo surfaces the
5333 // collapse. 5.1 db.lua :366 pins the event ordering.
5334 let tailcalls = if self.version <= LuaVersion::Lua51 {
5335 self.frames
5336 .last()
5337 .and_then(|f| f.lua())
5338 .map(|f| f.tailcalls)
5339 .unwrap_or(0)
5340 } else {
5341 0
5342 };
5343 self.hook_return(from_native, ftransfer, nret)?;
5344 for _ in 0..tailcalls {
5345 self.hook_tail_return()?;
5346 }
5347 let CallFrame::Lua(fr) =
5348 frames_pop_sync(&mut self.frames, &mut self.frames_top).expect("no frame")
5349 else {
5350 unreachable!("returning from a non-Lua frame")
5351 };
5352 for i in 0..nret {
5353 self.stack[(fr.func_slot + i) as usize] = self.stack[(abs_a + i) as usize];
5354 }
5355 if self.frames.len() < entry_depth {
5356 self.top = fr.func_slot + nret;
5357 return Ok(Some(self.take_results(fr.func_slot)));
5358 } else if matches!(self.frames.last(), Some(CallFrame::Cont(_))) {
5359 self.top = fr.func_slot + nret;
5360 } else {
5361 self.finish_results(fr.func_slot, nret, fr.nresults);
5362 }
5363 Ok(None)
5364 }
5365
5366 #[doc(hidden)]
5367 pub fn upval_get(&self, cl: Gc<LuaClosure>, idx: u32) -> Value {
5368 match cl.upvals()[idx as usize].state() {
5369 UpvalState::Open { slot, thread } => self.read_slot(slot, thread),
5370 UpvalState::Closed(v) => v,
5371 }
5372 }
5373
5374 fn upval_set(&mut self, cl: Gc<LuaClosure>, idx: u32, v: Value) {
5375 let uv = cl.upvals()[idx as usize];
5376 match uv.state() {
5377 UpvalState::Open { slot, thread } => self.write_slot(slot, thread, v),
5378 UpvalState::Closed(_) => {
5379 // 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).
5380 unsafe { uv.as_mut() }.set_closed(v);
5381 // forward barrier: a closed upvalue is single-slot, so the
5382 // forward variant is cheaper than barrier_back (PUC uses
5383 // `luaC_barrier_` for upvalues; `luaC_barrierback_` for
5384 // tables / threads).
5385 self.heap
5386 .barrier_forward(uv.as_ptr() as *mut crate::runtime::heap::GcHeader, v);
5387 }
5388 }
5389 }
5390
5391 // ---- register / error helpers ----
5392
5393 #[inline(always)]
5394 fn r(&self, base: u32, i: u32) -> Value {
5395 // SAFETY: the compiler reserves `proto.max_stack` slots above `base`
5396 // at frame entry (`push_frame` sizes the stack up to base + max_stack),
5397 // and every bytecode-generated reference falls within `[0, max_stack)`.
5398 // PUC's vmfetch uses raw `R(A)` (`s2v(L->base + A)`) for the same
5399 // reason. The bounds check would re-validate this invariant on every
5400 // op — the dispatch hot path can't afford it.
5401 // 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).
5402 unsafe { *self.stack.get_unchecked((base + i) as usize) }
5403 }
5404
5405 #[inline(always)]
5406 fn set_r(&mut self, base: u32, i: u32, v: Value) {
5407 // SAFETY: see `r` — `base + i < base + max_stack <= stack.len()` by
5408 // frame-entry contract.
5409 unsafe {
5410 *self.stack.get_unchecked_mut((base + i) as usize) = v;
5411 }
5412 }
5413
5414 #[doc(hidden)]
5415 pub fn rt_err(&mut self, msg: &str) -> LuaError {
5416 let text = match self.position_prefix() {
5417 Some(p) => format!("{p}{msg}"),
5418 None => msg.to_string(),
5419 };
5420 LuaError(Value::Str(self.heap.intern(text.as_bytes())))
5421 }
5422
5423 /// Error without the `chunk:line:` position prefix. PUC's
5424 /// `resume_error` (ldo.c) pushes its message as a bare literal,
5425 /// so `cannot resume dead coroutine` etc. must not be prefixed.
5426 pub(crate) fn plain_err(&mut self, msg: &str) -> LuaError {
5427 LuaError(Value::Str(self.heap.intern(msg.as_bytes())))
5428 }
5429
5430 /// A string a library built from pieces of any size: one longer than a
5431 /// string can hold raises, as the concatenation operator does.
5432 pub(crate) fn built_str(&mut self, bytes: &[u8]) -> Result<Value, LuaError> {
5433 if bytes.len() > crate::runtime::string::MAX_LEN {
5434 return Err(self.rt_err("string length overflow"));
5435 }
5436 Ok(Value::Str(self.heap.intern(bytes)))
5437 }
5438
5439 pub(crate) fn type_err(&mut self, what: &str, v: Value) -> LuaError {
5440 let extra = self.subject_varinfo(v);
5441 let tn = self.obj_typename(v);
5442 let msg = self.compose_type_err(what, &tn, &extra);
5443 self.runerror(&msg)
5444 }
5445
5446 /// Assemble a `luaG_typeerror` / `luaG_callerror` message in the dialect's
5447 /// word order.
5448 ///
5449 /// PUC ≤5.2 names the operand first — `attempt to call field 'f' (a nil
5450 /// value)`. 5.3 flipped it to type-first — `attempt to call a nil value
5451 /// (field 'f')`. luna emitted the 5.3+ form on every dialect, so every
5452 /// such error was worded wrong under 5.1/5.2.
5453 ///
5454 /// Two shapes carry no operand name on ≤5.2 and must collapse to the bare
5455 /// message: an absent varinfo (identical across dialects), and a
5456 /// metamethod target — ≤5.2's `luaG_typeerror` only names locals, globals,
5457 /// fields, upvalues and methods, so `(metamethod 'add')` has no ≤5.2
5458 /// counterpart and is dropped rather than reworded. All four shapes were
5459 /// measured against stock 5.1.5 / 5.2.4 / 5.5.1 before this was written.
5460 fn compose_type_err(&self, what: &str, tn: &str, extra: &str) -> String {
5461 if self.version() > crate::version::LuaVersion::Lua52 {
5462 return format!("attempt to {what} a {tn} value{extra}");
5463 }
5464 // `extra` is "" or " (kind 'name')" — unwrap to "kind 'name'".
5465 let inner = extra
5466 .trim_start()
5467 .trim_start_matches('(')
5468 .trim_end_matches(')');
5469 if inner.is_empty() || inner.starts_with("metamethod") {
5470 format!("attempt to {what} a {tn} value")
5471 } else {
5472 format!("attempt to {what} {inner} (a {tn} value)")
5473 }
5474 }
5475
5476 /// Name the offending operand of the current instruction (PUC varinfo) for
5477 /// a type error, e.g. " (global 'x')". The faulting value `bad` is matched
5478 /// to the instruction's subject register(s); a native-raised error whose
5479 /// current instruction doesn't hold `bad` simply yields "".
5480 fn subject_varinfo(&self, bad: Value) -> String {
5481 use crate::vm::isa::Op;
5482 // PUC `varinfo` names a variable only for a Lua activation
5483 if self.native_on_top() {
5484 return String::new();
5485 }
5486 let Some(f) = self.frames.last().and_then(CallFrame::lua) else {
5487 return String::new();
5488 };
5489 let proto = f.closure.proto;
5490 let p: &crate::runtime::Proto = &proto;
5491 let pc = f.pc as usize;
5492 if pc == 0 || pc > p.code.len() {
5493 return String::new();
5494 }
5495 let instr = p.code[pc - 1];
5496 let mut cands: Vec<u32> = Vec::new();
5497 match instr.op() {
5498 // indexed reads / length / method: the table/object is in B
5499 Op::GetField | Op::GetI | Op::GetTable | Op::SelfOp | Op::Len => {
5500 cands.push(instr.b());
5501 }
5502 // indexed writes / calls: the table/function is in A
5503 Op::SetField | Op::SetI | Op::SetTable | Op::Call | Op::TailCall => {
5504 cands.push(instr.a());
5505 }
5506 // arithmetic/bitwise: a register operand (B, and C unless constant)
5507 Op::Add
5508 | Op::Sub
5509 | Op::Mul
5510 | Op::Div
5511 | Op::Mod
5512 | Op::Pow
5513 | Op::IDiv
5514 | Op::BAnd
5515 | Op::BOr
5516 | Op::BXor
5517 | Op::Shl
5518 | Op::Shr => {
5519 cands.push(instr.b());
5520 if !instr.k() {
5521 cands.push(instr.c());
5522 }
5523 }
5524 Op::Unm | Op::BNot => cands.push(instr.b()),
5525 // indexing an upvalue table (`_ENV` for a global): PUC
5526 // `getupvalname` finds the value among the closure's upvalues
5527 Op::GetTabUp | Op::SetTabUp => {
5528 let u = if instr.op() == Op::GetTabUp {
5529 instr.b()
5530 } else {
5531 instr.a()
5532 };
5533 if self.upval_get(f.closure, u).raw_eq(bad)
5534 && let Some(d) = p.upvals.get(u as usize)
5535 {
5536 return format!(" (upvalue '{}')", d.name);
5537 }
5538 }
5539 Op::Concat => {
5540 let a = instr.a();
5541 for r in a..a + instr.b() {
5542 cands.push(r);
5543 }
5544 }
5545 _ => {}
5546 }
5547 // Up to 5.3 a binary operator takes a constant operand straight
5548 // from the constant table (RK), where `varinfo` cannot see it, so a
5549 // string constant is not named there; unary operators load it into
5550 // a register first and do name it.
5551 let rk_operands = self.version <= LuaVersion::Lua53
5552 && matches!(
5553 instr.op(),
5554 Op::Add
5555 | Op::Sub
5556 | Op::Mul
5557 | Op::Div
5558 | Op::Mod
5559 | Op::Pow
5560 | Op::IDiv
5561 | Op::BAnd
5562 | Op::BOr
5563 | Op::BXor
5564 | Op::Shl
5565 | Op::Shr
5566 );
5567 for reg in cands {
5568 if self.r(f.base, reg).raw_eq(bad) {
5569 return match crate::vm::objname::getobjname_in(p, pc - 1, reg, self.version) {
5570 Some(("constant", _)) if rk_operands => String::new(),
5571 Some((kind, name)) => format!(" ({kind} '{name}')"),
5572 None => String::new(),
5573 };
5574 }
5575 }
5576 String::new()
5577 }
5578
5579 /// "attempt to call a X value", enriched (PUC luaG_callerror) with a name
5580 /// for the call target: "(global 'f')" for a direct call, or "(metamethod
5581 /// 'add')" when the call is a metamethod dispatched by the current opcode.
5582 fn call_err(&mut self, v: Value) -> LuaError {
5583 let extra = self.call_target_varinfo(v);
5584 let tn = self.obj_typename(v);
5585 let msg = self.compose_type_err("call", &tn, &extra);
5586 self.runerror(&msg)
5587 }
5588
5589 /// Name the offending call target. A metamethod dispatch pushes a `Cont`
5590 /// frame before the call, so the opcode that triggered it lives in the
5591 /// nearest *Lua* frame — read that instruction: OP_CALL names the function
5592 /// register, any metamethod-bearing opcode yields "(metamethod 'event')".
5593 fn call_target_varinfo(&self, bad: Value) -> String {
5594 use crate::vm::isa::Op;
5595 if self.native_on_top() {
5596 return String::new();
5597 }
5598 let Some(f) = self.frames.iter().rev().find_map(CallFrame::lua) else {
5599 return String::new();
5600 };
5601 let proto = f.closure.proto;
5602 let p: &crate::runtime::Proto = &proto;
5603 let pc = f.pc as usize;
5604 if pc == 0 || pc > p.code.len() {
5605 return String::new();
5606 }
5607 let instr = p.code[pc - 1];
5608 match instr.source_op() {
5609 Op::Call | Op::TailCall => {
5610 let reg = instr.a();
5611 if self.r(f.base, reg).raw_eq(bad) {
5612 match crate::vm::objname::getobjname_in(p, pc - 1, reg, self.version) {
5613 Some((kind, name)) => format!(" ({kind} '{name}')"),
5614 None => String::new(),
5615 }
5616 } else {
5617 String::new()
5618 }
5619 }
5620 // 5.4 `funcnamefromcode` names the generic-for iterator call
5621 // (5.3 had the entry but raised through plain `luaG_typeerror`)
5622 Op::TForCall if self.version >= LuaVersion::Lua54 => {
5623 " (for iterator 'for iterator')".to_string()
5624 }
5625 // 5.4 `funcnamefromcall` names the metamethod; up to 5.3 the
5626 // call raised through `luaG_typeerror`, whose `varinfo` does not
5627 op if self.version >= LuaVersion::Lua54 => match mm_event_name(op) {
5628 Some(ev) => format!(" (metamethod '{ev}')"),
5629 None => String::new(),
5630 },
5631 _ => String::new(),
5632 }
5633 }
5634
5635 /// "number has no integer representation", enriched (PUC luaG_tointerror)
5636 /// with a "(field 'x')"-style suffix naming the offending operand of the
5637 /// current arithmetic instruction when it can be recovered from bytecode.
5638 fn no_int_rep_err(&mut self) -> LuaError {
5639 let extra = self.bad_operand_varinfo();
5640 self.runerror(&format!("number{extra} has no integer representation"))
5641 }
5642
5643 /// Inspect the current frame's faulting instruction: find the register
5644 /// operand holding a float with no integer representation and name it.
5645 fn bad_operand_varinfo(&self) -> String {
5646 if self.native_on_top() {
5647 return String::new();
5648 }
5649 let Some(f) = self.frames.last().and_then(CallFrame::lua) else {
5650 return String::new();
5651 };
5652 let proto = f.closure.proto;
5653 let p: &crate::runtime::Proto = &proto;
5654 let pc = f.pc as usize;
5655 if pc == 0 || pc > p.code.len() {
5656 return String::new();
5657 }
5658 let instr = p.code[pc - 1];
5659 let mut regs = vec![instr.b()];
5660 if !instr.k() {
5661 regs.push(instr.c());
5662 }
5663 let no_int = |n: Option<Num>| matches!(n, Some(Num::Float(x)) if crate::runtime::value::f2i_exact(x).is_none());
5664 for reg in regs {
5665 let v = self.r(f.base, reg);
5666 // before 5.4 a numeric string is converted first, so "2.5" is
5667 // the operand without an integer value
5668 let n = self.arith_operand()(v);
5669 if no_int(n) {
5670 return match crate::vm::objname::getobjname_in(p, pc - 1, reg, self.version) {
5671 Some((kind, name)) => format!(" ({kind} '{name}')"),
5672 None => String::new(),
5673 };
5674 }
5675 }
5676 String::new()
5677 }
5678
5679 /// Position prefix of the currently executing Lua frame. PUC `luaL_error`
5680 /// calls `luaL_where(L, 1)` which reads `L->ci->previous`. When the prior
5681 /// frame is a C function (e.g. a pcall Cont parked above `require`'s
5682 /// native call), PUC pushes no prefix — match that by looking only at the
5683 /// topmost frame directly and bailing if it is anything but a Lua frame.
5684 pub(crate) fn position_prefix(&self) -> Option<String> {
5685 let f = match self.frames.last()? {
5686 CallFrame::Lua(f) => f,
5687 // a native metamethod runs above the Meta continuation of the
5688 // instruction that triggered it: that Lua function is its caller
5689 CallFrame::Cont(NativeCont {
5690 kind: ContKind::Meta(_),
5691 ..
5692 }) => self.frames.iter().rev().nth(1)?.lua()?,
5693 CallFrame::Cont(_) => return None,
5694 };
5695 let proto = f.closure.proto;
5696 // a stripped chunk: no source in luna's own format, no line info in
5697 // PUC's (whose loader names the missing source "=?")
5698 if proto.source.as_bytes().is_empty() || proto.lines.is_empty() {
5699 return Some(self.stripped_prefix());
5700 }
5701 let line = proto.lines[(f.pc as usize).saturating_sub(1).min(proto.lines.len() - 1)];
5702 // 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).
5703 let raw = unsafe { crate::runtime::string::bytes_of(proto.source.as_ptr()) };
5704 let display = crate::vm::lib_debug::chunk_id(self.version, raw);
5705 let src = String::from_utf8_lossy(&display).into_owned();
5706 Some(format!("{src}:{line}: "))
5707 }
5708
5709 /// PUC `luaG_addinfo` prefix for a stripped chunk. 5.5 substitutes "=?"
5710 /// for the source and renders the line as "?" (so the prefix reads
5711 /// `?:?: `). 5.4 and below leave the source NULL ("?") and use the raw
5712 /// `getfuncline = -1`, so the prefix reads `?:-1: ` (5.4 errors.lua :282
5713 /// matches `^%?:%-1:`).
5714 fn stripped_prefix(&self) -> String {
5715 if self.version >= crate::version::LuaVersion::Lua55 {
5716 "?:?: ".to_string()
5717 } else {
5718 "?:-1: ".to_string()
5719 }
5720 }
5721
5722 /// PUC `luaL_where(L, level)`: `"short_src:line: "` for the function at
5723 /// `level` (0 = the running native), or `None` when that level does not
5724 /// exist or has no line information (a C function, a stripped chunk).
5725 pub(crate) fn position_prefix_at_level(&self, level: i64) -> Option<String> {
5726 let ts = self.thread_stack(None);
5727 let i = usize::try_from(level).ok()?;
5728 if i >= ts.levels.len() {
5729 return None;
5730 }
5731 let line = ts.currentline(i);
5732 if line <= 0 {
5733 return None;
5734 }
5735 let DbgKind::Lua(fi) = ts.levels[i] else {
5736 return None;
5737 };
5738 let ar = self.closure_ar(ts.lua(fi).closure);
5739 Some(format!(
5740 "{}:{line}: ",
5741 String::from_utf8_lossy(&ar.short_src)
5742 ))
5743 }
5744
5745 // ---- the interpreter ----
5746
5747 /// Run from the current top frame down to (but not past) `entry_depth`
5748 /// frames. Coroutine driving passes `entry_depth = 1` so the whole thread
5749 /// runs to completion or a yield.
5750 /// Resume the dispatcher from the saved
5751 /// `entry_depth` (captured pre-yield by `drive_one`). Called by
5752 /// `EvalFuture::poll` on every poll after the first to walk the
5753 /// existing call frames until the next `BudgetExhausted` or
5754 /// terminal `Ok`/`Err`. Not a public-API surface; the
5755 /// embedder reaches it through `Vm::eval_async`.
5756 pub(crate) fn exec_with_async(&mut self, entry_depth: usize) -> Result<Vec<Value>, LuaError> {
5757 self.exec_with(entry_depth)
5758 }
5759
5760 fn exec_with(&mut self, entry_depth: usize) -> Result<Vec<Value>, LuaError> {
5761 loop {
5762 let r = self.run(entry_depth);
5763 if r.is_err()
5764 && (self.yielding.is_some()
5765 || self.terminating.is_some()
5766 || self.host_yield_pending
5767 || self.pending_async_native_fut.is_some())
5768 {
5769 // a `coroutine.yield` is in flight: keep the frames intact (they
5770 // are the suspended coroutine's saved state) and propagate to
5771 // resume. A self-close termination propagates the same way, so a
5772 // protecting pcall on the way out cannot catch (unwind) it.
5773 // `host_yield_pending` is the async-mode
5774 // analogue: the sentinel must reach `drive_one` without
5775 // a protecting `pcall` swallowing it.
5776 return r;
5777 }
5778 match r {
5779 Ok(vals) => return Ok(vals),
5780 // unwind toward `entry_depth`. A protecting pcall/xpcall
5781 // continuation caught along the way turns the error into
5782 // `false, msg` and the loop resumes running its caller; an
5783 // uncaught error propagates out.
5784 Err(e) => match self.unwind(e.0, entry_depth) {
5785 Unwound::Caught => continue,
5786 Unwound::CaughtReturn(vals) => return Ok(vals),
5787 Unwound::Propagated(err) => return Err(err),
5788 },
5789 }
5790 }
5791 }
5792
5793 /// Unwind the call stack from the error point toward `entry_depth`, running
5794 /// `__close` handlers on each Lua frame. Stops at the first pcall/xpcall
5795 /// continuation frame at/above `entry_depth` (the error is *caught*: its
5796 /// slot receives `false, msg`); if none is reached, the error propagates.
5797 fn unwind(&mut self, mut err: Value, entry_depth: usize) -> Unwound {
5798 // The protected call runs in-place among the caller frames' registers,
5799 // so truncating the failed frames here cuts into caller windows below
5800 // the catcher. Snapshot the live length: at the error point the stack
5801 // already spans every surviving frame's window, so restoring it after a
5802 // catch reinstates them all (the reclaimed slots above are dead temps).
5803 // PUC handles overflow recovery via a separate EXTRA_STACK reserve;
5804 // we instead clamp the restore to the catcher's caller window when the
5805 // error point was at the stack limit (cause: the next `call_value_impl`
5806 // picks `func_slot = stack.len()` which would otherwise re-overflow).
5807 let saved_len = self.stack.len();
5808 err = self.raise_to_handler(err);
5809 // An error that no protected call inside the running coroutine will
5810 // catch kills it without unwinding: PUC's `lua_resume` leaves the
5811 // dead thread's stack as it was, so its pending to-be-closed
5812 // variables run only when it is closed (`coroutine.close`, or
5813 // `coroutine.wrap` closing it before re-raising). Scoped to the
5814 // coroutine's own run (`entry_depth == 1`); a run nested under a
5815 // native unwinds as before.
5816 if entry_depth == 1
5817 && self.version >= LuaVersion::Lua54
5818 && self
5819 .current
5820 .is_some_and(|c| c.status == crate::runtime::CoroStatus::Running)
5821 && !self.frames.iter().any(|f| {
5822 matches!(
5823 f,
5824 CallFrame::Cont(NativeCont {
5825 kind: ContKind::Pcall | ContKind::Xpcall { .. } | ContKind::Close(_),
5826 ..
5827 })
5828 )
5829 })
5830 {
5831 while self.frames.len() >= entry_depth {
5832 frames_pop_sync(&mut self.frames, &mut self.frames_top);
5833 }
5834 return Unwound::Propagated(LuaError(err));
5835 }
5836 while self.frames.len() >= entry_depth {
5837 match *self.frames.last().expect("frame") {
5838 // a yieldable-metamethod continuation does not catch: discard the
5839 // abandoned instruction and keep unwinding (PUC drops the partial
5840 // op on error).
5841 CallFrame::Cont(NativeCont {
5842 kind: ContKind::Meta(mc),
5843 func_slot,
5844 ..
5845 }) => {
5846 frames_pop_sync(&mut self.frames, &mut self.frames_top);
5847 self.stack.truncate(func_slot as usize);
5848 self.top = mc.saved_top.min(func_slot);
5849 self.tbc.retain(|&s| s < func_slot);
5850 }
5851 // a __pairs continuation does not catch either: an error inside
5852 // the metamethod propagates past `pairs`.
5853 CallFrame::Cont(NativeCont {
5854 kind: ContKind::Pairs,
5855 func_slot,
5856 ..
5857 }) => {
5858 frames_pop_sync(&mut self.frames, &mut self.frames_top);
5859 self.stack.truncate(func_slot as usize);
5860 self.top = func_slot;
5861 self.tbc.retain(|&s| s < func_slot);
5862 }
5863 // a __close continuation does not catch: drop the half-run
5864 // handler's window, then continue the close yieldably with
5865 // the new error threaded as `pending`. Preserve `cc.after`
5866 // verbatim — `Return`/`Block` originating from an aborting
5867 // OP_Return/OP_Close will be short-circuited by
5868 // `finish_close_after` (pending propagates as Err); a
5869 // `ResumeUnwind` originated by our own Lua-frame handler
5870 // must keep its deferred frame-pop semantics so that frame
5871 // is not orphaned. If a fresh handler yields, `drive_close`
5872 // pushes another `Cont::Close` and we return `Caught` so
5873 // `exec_with` re-enters the run loop.
5874 CallFrame::Cont(NativeCont {
5875 kind: ContKind::Close(cc),
5876 func_slot,
5877 ..
5878 }) => {
5879 frames_pop_sync(&mut self.frames, &mut self.frames_top);
5880 self.stack.truncate(func_slot as usize);
5881 self.top = func_slot;
5882 self.tbc.retain(|&s| s < func_slot);
5883 match self.drive_close(cc.from, Some(err), cc.after, entry_depth) {
5884 Ok(Some(_)) => {
5885 unreachable!(
5886 "Block / Return / ResumeUnwind never return host values mid-unwind"
5887 )
5888 }
5889 Ok(None) => return Unwound::Caught,
5890 Err(e) => {
5891 // the drained close re-raises `err`; only an
5892 // error a handler raised is new
5893 if !e.0.raw_eq(err) {
5894 err = self.raise_to_handler(e.0);
5895 }
5896 continue;
5897 }
5898 }
5899 }
5900 CallFrame::Cont(nc) => {
5901 frames_pop_sync(&mut self.frames, &mut self.frames_top);
5902 self.pcall_depth -= 1;
5903 let result = match nc.kind {
5904 ContKind::Pcall => {
5905 self.msgh_applied = None;
5906 err
5907 }
5908 // the handler ran where the error was raised (see
5909 // `raise_to_handler`); one raised past the handler's
5910 // reach (by the unwind itself) meets it here
5911 ContKind::Xpcall { handler } => {
5912 if self.msgh_applied.take().is_some_and(|v| v.raw_eq(err)) {
5913 err
5914 } else {
5915 self.call_msgh(handler, err)
5916 }
5917 }
5918 ContKind::Meta(_) | ContKind::Pairs | ContKind::Close(_) => {
5919 unreachable!("Meta/Pairs/Close cont handled above")
5920 }
5921 };
5922 // PUC 5.5 `luaG_errormsg` substitutes "<no error object>"
5923 // for nil AFTER the message handler ran (ldebug.c:849) —
5924 // so it applies to the pcall-caught object and to an
5925 // xpcall HANDLER'S return value, while the handler itself
5926 // (and a top-level propagation into the host, whose
5927 // `error_display` plays msghandler) still sees the raw
5928 // nil. 5.4- keep nil everywhere (errors.lua :49 asserts
5929 // `doit("error()") == nil`).
5930 let result = if matches!(result, Value::Nil)
5931 && self.version >= crate::version::LuaVersion::Lua55
5932 {
5933 Value::Str(self.heap.intern(b"<no error object>"))
5934 } else {
5935 result
5936 };
5937 // the error has been caught (pcall/xpcall): the captured
5938 // traceback was for that error and is no longer in flight.
5939 self.error_traceback = None;
5940 let fs = nc.func_slot as usize;
5941 if self.stack.len() < fs + 2 {
5942 self.stack.resize(fs + 2, Value::Nil);
5943 }
5944 self.stack[fs] = Value::Bool(false);
5945 self.stack[fs + 1] = result;
5946 self.top = nc.func_slot + 2;
5947 self.tbc.retain(|&s| s < nc.func_slot);
5948 if self.frames.len() < entry_depth {
5949 return Unwound::CaughtReturn(self.take_results(nc.func_slot));
5950 }
5951 self.finish_results(nc.func_slot, 2, nc.nresults);
5952 // reinstate the caller windows the unwind truncated into,
5953 // clamped to the catcher's caller window + a `MIN_STACK`
5954 // reserve. The clamp is a no-op for normal pcall catches
5955 // (saved_len lies within the caller's max_stack window),
5956 // and prevents the stack from staying near `MAX_LUA_STACK`
5957 // after an overflow-recovery catch — which would make the
5958 // next `call_value_impl` (e.g. a `__close` in the catcher's
5959 // errorh, locals.lua:659) pick `func_slot = stack.len()`
5960 // above the limit and re-overflow.
5961 // Restore the caller's full register window: opcodes
5962 // index it directly. The cap covers caller's base +
5963 // `max_stack` + a small reserve. We always resize to
5964 // exactly this window — previously this clamped
5965 // `saved_len` from above to prevent staying near
5966 // `MAX_LUA_STACK` after an overflow-recovery catch, and
5967 // a yieldable-unwind re-entry adds the dual case where
5968 // `saved_len` is *below* the window (a prior
5969 // `ResumeUnwind` truncated). Using the window directly
5970 // covers both.
5971 let restore = self
5972 .frames
5973 .iter()
5974 .rev()
5975 .find_map(CallFrame::lua)
5976 .map(|c| (c.base + c.closure.proto.max_stack as u32) as usize + 256)
5977 .unwrap_or(saved_len);
5978 if self.stack.len() < restore {
5979 self.stack.resize(restore, Value::Nil);
5980 } else if self.stack.len() > restore {
5981 self.stack.truncate(restore);
5982 }
5983 // Clear slots vacated by the popped
5984 // frames the unwind walked over. finish_results
5985 // above clears `[nc.func_slot + nresults ..
5986 // nc.func_slot + 2)`, which only covers the
5987 // pcall's own result region — the unwind-popped
5988 // frames' locals in `[nc.func_slot + 2 .. restore)`
5989 // are still in place with whatever Gc-bearing
5990 // Values they last held. Without this clear, a
5991 // later GC marks the stale pointers (same hazard as
5992 // the Op::Return finish_results path). PUC's `luaD_pcall` similarly truncates
5993 // L->top to the catcher's level — luna's
5994 // truncate above resizes the Vec but doesn't
5995 // touch slots [func_slot+2..restore) that were
5996 // already present.
5997 let clear_lo = (nc.func_slot as usize + 2).min(self.stack.len());
5998 let clear_hi = restore.min(self.stack.len());
5999 if clear_lo < clear_hi {
6000 for slot in &mut self.stack[clear_lo..clear_hi] {
6001 *slot = Value::Nil;
6002 }
6003 }
6004 return Unwound::Caught;
6005 }
6006 CallFrame::Lua(f) => {
6007 // Yieldable error-unwind close, PUC luaG_errormsg shape:
6008 // (1) pop the Lua frame immediately so each `__close`
6009 // handler runs at the C boundary above — `debug.getinfo`
6010 // sees the next outer Lua frame's call site (typically
6011 // `pcall`), not this aborting function (locals.lua:480).
6012 // (2) drive the close yieldably with
6013 // `AfterClose::ResumeUnwind { func_slot, err }`; on drain
6014 // it truncates to `func_slot` and re-raises (letting a
6015 // handler-raised error win over `err`). If a handler
6016 // yields, `drive_close` pushes `Cont::Close` and we
6017 // return `Caught` so `exec_with` re-enters the run loop;
6018 // a synchronous drain returns Err exactly as the old
6019 // path did.
6020 frames_pop_sync(&mut self.frames, &mut self.frames_top);
6021 let after = AfterClose::ResumeUnwind {
6022 func_slot: f.func_slot,
6023 err,
6024 };
6025 match self.begin_close(f.base, Some(err), after, entry_depth) {
6026 Ok(Some(_)) => {
6027 unreachable!("ResumeUnwind never returns host values")
6028 }
6029 Ok(None) => return Unwound::Caught,
6030 Err(e) => {
6031 // the drained close re-raises `err`; only an
6032 // error a handler raised is new
6033 if !e.0.raw_eq(err) {
6034 err = self.raise_to_handler(e.0);
6035 }
6036 continue;
6037 }
6038 }
6039 }
6040 }
6041 }
6042 Unwound::Propagated(LuaError(err))
6043 }
6044
6045 fn run(&mut self, entry_depth: usize) -> Result<Vec<Value>, LuaError> {
6046 loop {
6047 // Fast-path slow-check gate: most embedders run with both
6048 // `instr_budget` and `mem_cap` as None, so a single combined
6049 // is_some test lets the hot loop skip both branches with one
6050 // load + branch instead of two.
6051 if self.instr_budget.is_some() || self.heap.mem_cap.is_some() {
6052 if let Some(b) = self.instr_budget.as_mut() {
6053 *b -= 1;
6054 if *b <= 0 {
6055 self.instr_budget = None;
6056 // Async-mode cooperative
6057 // yield. Set a sentinel flag so `exec_with`
6058 // propagates the Err without `unwind` running
6059 // (mirroring the `yielding.is_some()` path),
6060 // and `call_value_impl` preserves the call
6061 // frames for the next `poll`. Translation back
6062 // to `DispatchOutcome::BudgetExhausted` happens
6063 // in `drive_one`. The Err value itself is
6064 // `Value::Nil` — a pure sentinel, never seen by
6065 // user code.
6066 if self.async_mode {
6067 self.host_yield_pending = true;
6068 return Err(LuaError(Value::Nil));
6069 }
6070 // Classify the trip so embedders can
6071 // distinguish budget exhaustion from a
6072 // generic Runtime error and retry / give up
6073 // accordingly.
6074 self.last_error_kind = crate::vm::error::LuaErrorKind::InstrBudget;
6075 let s = Value::Str(self.heap.intern(b"instruction budget exceeded"));
6076 return Err(LuaError(s));
6077 }
6078 }
6079 if let Some(cap) = self.heap.mem_cap
6080 && self.heap.bytes() > cap
6081 {
6082 // First try a full collect — embedders set tight caps
6083 // and the overshoot may be reclaimable (closures kept
6084 // by short-lived frames, intermediate strings). Only
6085 // disarm + raise if the cap is still breached after
6086 // collection. PUC's `LUA_GCEMERGENCY` path matches.
6087 //
6088 // Root up to the deepest Lua frame's
6089 // `base + max_stack` window rather than the entire
6090 // `self.stack.len()`
6091 // (covers register operands the current opcode
6092 // might reference). The cap fires during table
6093 // mutation in a tight `a[i] = i` loop where `a`
6094 // lives at a frame-register slot past `self.top`
6095 // (OP_NEWINDEX doesn't advance top); the deepest
6096 // frame's max_stack window provably covers it
6097 // since `a` is a register of the executing proto.
6098 //
6099 // Still over-roots caller frames' dead regs
6100 // (slots between caller.base and the callee
6101 // func_slot are live; slots past callee
6102 // func_slot in caller's frame are dead until
6103 // caller resumes). For fire-once cap path this
6104 // residual over-root is acceptable; there is no
6105 // full per-frame walk because a strong/weak pass
6106 // split is semantically impossible — the weak pass
6107 // depends on strong-pass marks.
6108 let cap_root_top = self
6109 .frames
6110 .iter()
6111 .rev()
6112 .find_map(CallFrame::lua)
6113 .map(|f| f.base + f.closure.proto.max_stack as u32)
6114 .unwrap_or(self.top);
6115 self.gc_top = cap_root_top.max(self.top);
6116 self.collect_garbage();
6117 if self.heap.bytes() > cap {
6118 self.heap.mem_cap = None;
6119 let s = Value::Str(self.heap.intern(b"memory cap exceeded"));
6120 return Err(LuaError(s));
6121 }
6122 }
6123 }
6124 // Single combined frame fetch: continuation arm OR Lua arm. Saves
6125 // a second `self.frames.last()` slice access vs the prior split
6126 // form (LLVM doesn't always CSE these across the cont branch).
6127 // A continuation frame on top means the call it protected just
6128 // delivered its results — wrap as `true, results…` and hand to
6129 // the pcall/xpcall caller. The error path is handled by `unwind`;
6130 // this branch is only reached on success/resume completion.
6131 // 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).
6132 let frame_peek = unsafe { self.frames.last().unwrap_unchecked() };
6133 if let &CallFrame::Cont(nc) = frame_peek {
6134 // a yieldable metamethod returned: complete the interrupted
6135 // instruction (PUC luaV_finishOp) and resume the running frame.
6136 if let ContKind::Meta(mc) = nc.kind {
6137 frames_pop_sync(&mut self.frames, &mut self.frames_top);
6138 let result = if self.top > nc.func_slot {
6139 self.stack[nc.func_slot as usize]
6140 } else {
6141 Value::Nil
6142 };
6143 self.stack.truncate(nc.func_slot as usize);
6144 self.top = mc.saved_top;
6145 self.finish_meta(mc.action, result)?;
6146 continue;
6147 }
6148 // a __close handler returned successfully: discard its
6149 // results, restore `top` to the slot the handler was called
6150 // at (the surrounding frame's register window above this slot
6151 // must stay alloc'd — never truncate the underlying stack),
6152 // then continue the close chain (next slot, or fire
6153 // AfterClose). When the close ends an entry activation,
6154 // drive_close hands the results up to exec_with directly.
6155 if let ContKind::Close(cc) = nc.kind {
6156 frames_pop_sync(&mut self.frames, &mut self.frames_top);
6157 self.top = nc.func_slot;
6158 if let Some(vals) =
6159 self.drive_close(cc.from, cc.pending, cc.after, entry_depth)?
6160 {
6161 return Ok(vals);
6162 }
6163 continue;
6164 }
6165 // __pairs returned: normalize its results to exactly the
6166 // dialect's count (iterator, state, control, and on 5.5 the
6167 // closing value) at pairs's slot, where the metamethod was
6168 // called, and hand them to pairs's caller.
6169 if let ContKind::Pairs = nc.kind {
6170 frames_pop_sync(&mut self.frames, &mut self.frames_top);
6171 let total = crate::vm::builtins::pairs_mm_results(self) as u32;
6172 let need = (nc.func_slot + total) as usize;
6173 if self.stack.len() < need {
6174 self.stack.resize(need, Value::Nil);
6175 }
6176 // the metamethod ran one slot above pairs's own
6177 let first = nc.func_slot + 1;
6178 let n = (self.top - first).min(total);
6179 for i in 0..n {
6180 self.stack[(nc.func_slot + i) as usize] = self.stack[(first + i) as usize];
6181 }
6182 for s in (nc.func_slot + n)..(nc.func_slot + total) {
6183 self.stack[s as usize] = Value::Nil;
6184 }
6185 self.top = nc.func_slot + total;
6186 if self.frames.len() < entry_depth {
6187 return Ok(self.take_results(nc.func_slot));
6188 }
6189 self.finish_results(nc.func_slot, total, nc.nresults);
6190 continue;
6191 }
6192 frames_pop_sync(&mut self.frames, &mut self.frames_top);
6193 self.pcall_depth -= 1;
6194 // f's results sit at nc.func_slot+1.. (f was called one slot
6195 // above the continuation), so writing `true` at the slot makes
6196 // `true, results…` already contiguous.
6197 let nret = self.top - (nc.func_slot + 1);
6198 self.stack[nc.func_slot as usize] = Value::Bool(true);
6199 let total = 1 + nret;
6200 self.top = nc.func_slot + total;
6201 if self.frames.len() < entry_depth {
6202 return Ok(self.take_results(nc.func_slot));
6203 }
6204 self.finish_results(nc.func_slot, total, nc.nresults);
6205 continue;
6206 }
6207 // GC runs only at the allocation safe points below (PUC's
6208 // `luaC_checkGC` sites), each with a precise `gc_top`; the loop head
6209 // no longer collects, so a stale full-window `gc_top` cannot leak in.
6210 //
6211 // Hot-path frame fetch: the Cont arm above continues the loop,
6212 // so reaching here means `frame_peek` is the Lua frame. Reuse it
6213 // rather than re-fetching `self.frames.last()`.
6214 let f = match frame_peek {
6215 CallFrame::Lua(f) => f,
6216 _ => unreachable!("Cont frame survived the dispatch loop head"),
6217 };
6218 let cl = f.closure;
6219 let base = f.base;
6220 let func_slot = f.func_slot;
6221 let n_varargs = f.n_varargs;
6222 let pc = f.pc;
6223 let oldpc = f.hook_oldpc;
6224
6225 // SAFETY: `pc` is bounded by the compiler against `proto.code.len()`
6226 // — every branch / call op only sets `pc` to a valid index, and
6227 // function entry initialises pc=0 with a non-empty body. PUC's
6228 // `vmfetch` uses the equivalent unchecked load.
6229 let inst = unsafe { *cl.proto.code.get_unchecked(pc as usize) };
6230
6231 // Trace recording append + close detection.
6232 // Gated on `trace_jit_enabled` + `active_trace.is_some()`
6233 // so default dispatch keeps a single not-taken branch.
6234 //
6235 // - At the head PC with a non-empty record, the trace has
6236 // looped back to its start: mark `closed = true` and
6237 // take the record for compile + cache.
6238 // - Otherwise, capture the op. If the record overflows
6239 // MAX_TRACE_LEN, abort by dropping it.
6240 if self.jit.trace_enabled
6241 && let Some(_rec) = self.jit.active_trace.as_mut()
6242 {
6243 // Depth tracking. The trace head's frame is
6244 // at index `recording_frame_base`; every Op::Call that
6245 // pushes a new frame bumps the live depth, every
6246 // Op::Return that pops one decrements it.
6247 //
6248 // **Three clean-close conditions**:
6249 // - `at_head`: cur_depth == 0 AND about-to-execute the
6250 // trace's head_pc on its head_proto (loop closed back
6251 // to start). Same for loop-triggered and call-triggered
6252 // traces, so a call-triggered trace does not close on
6253 // the first re-entry (that would leave fib's body at 7
6254 // depth=0 ops); it inlines up to MAX_INLINE_DEPTH
6255 // levels before any close.
6256 // - `returned_past_head`: trace head's frame is gone
6257 // (callee returned past it, or the call-trigger
6258 // started a recording inside a callee that has now
6259 // returned). Whatever ops were recorded form the
6260 // trace body; the lowerer treats the partial trace
6261 // the same as InlineAbort.
6262 // - `depth_cap_hit`: cur_depth > MAX_INLINE_DEPTH.
6263 // Recording any deeper would just bloat the IR; close
6264 // with the body we have. Lowerer's existing length
6265 // gate + InlineAbort path handles short bodies.
6266 let returned_past_head = self.frames.len() <= self.jit.recording_frame_base;
6267 let cur_depth = if returned_past_head {
6268 0
6269 } else {
6270 self.frames.len() - 1 - self.jit.recording_frame_base
6271 };
6272 let depth_cap_hit = cur_depth > crate::jit::trace::MAX_INLINE_DEPTH as usize;
6273 let rec = self.jit.active_trace.as_mut().expect("just checked Some");
6274 let at_head_loop = cur_depth == 0
6275 && !rec.ops.is_empty()
6276 && !returned_past_head
6277 && std::ptr::eq(cl.proto.as_ptr(), rec.head_proto.as_ptr())
6278 && pc == rec.head_pc;
6279 // Self-link cycle catch (mirrors LuaJIT's
6280 // `check_call_unroll` at `lj_record.c:1869`). Trips when:
6281 // 1. We're about to execute the head_pc on head_proto
6282 // at depth > 0 (we're re-entering the trace head
6283 // from inside an inlined recursion level — UpRec).
6284 // 2. The count of ancestor frames in the recording
6285 // window that share `head_proto` exceeds
6286 // [`RECUNROLL_THRESHOLD`] (default 2).
6287 // For fib(N): head_pc=0, head_proto=fib. After 2 inline
6288 // recursion levels are captured, the recorder enters
6289 // the 3rd nested fib frame, sees cur_depth=3 > 2, and
6290 // trips this catch — closing with `SelfRecKind::UpRec`.
6291 // The lowerer's `TraceEnd::SelfLink` tail emits the
6292 // bump-base + branch-to-self loop body.
6293 //
6294 // TailRec vs UpRec: LJ distinguishes via
6295 // `framedepth + retdepth == 0`. luna doesn't track
6296 // retdepth separately; cur_depth == 0 with a non-empty
6297 // call chain in tail position is rare (would require
6298 // explicit Lua TCO). We use cur_depth > 0 as the UpRec
6299 // condition (fib's case); cur_depth == 0 with positive
6300 // ancestor count would route to TailRec, but luna's
6301 // recorder doesn't currently produce that shape because
6302 // tail-call elision pops the caller frame and we'd
6303 // hit `at_head_loop` instead.
6304 let self_link_trip: Option<crate::jit::trace::SelfRecKind> = {
6305 if self.jit.self_link_enabled
6306 && !returned_past_head
6307 && std::ptr::eq(cl.proto.as_ptr(), rec.head_proto.as_ptr())
6308 && pc == rec.head_pc
6309 && cur_depth > 0
6310 {
6311 // Count ancestor frames sharing head_proto.
6312 // self.frames[recording_frame_base..] currently
6313 // includes the just-pushed frame at the top
6314 // (the one about to execute head_pc). Ancestors
6315 // = the slice excluding the top frame.
6316 let head_proto_ptr = rec.head_proto.as_ptr();
6317 let last_idx = self.frames.len() - 1;
6318 let mut count = 0usize;
6319 for i in self.jit.recording_frame_base..last_idx {
6320 if let CallFrame::Lua(f) = &self.frames[i]
6321 && std::ptr::eq(f.closure.proto.as_ptr(), head_proto_ptr)
6322 {
6323 count += 1;
6324 }
6325 }
6326 if count > crate::jit::trace::RECUNROLL_THRESHOLD {
6327 // cur_depth > 0 → UpRec (fib pattern).
6328 // cur_depth == 0 wouldn't reach this arm.
6329 Some(crate::jit::trace::SelfRecKind::UpRec)
6330 } else {
6331 None
6332 }
6333 } else {
6334 None
6335 }
6336 };
6337 if let Some(kind) = self_link_trip {
6338 // SelfLink relax for self-recursive patterns at frame
6339 // depth >= 2.
6340 //
6341 // Stamping `self_link_kind` unconditionally at the
6342 // head_pc re-entry would never dispatch: the
6343 // `downrec_close` marker can only fire from the
6344 // depth>0 Op::Return path (`rec.retfs` chain),
6345 // which never reaches the recorder for fib(28)-like
6346 // shapes that hit the SelfLink cycle catch BEFORE
6347 // any base-case Return — leaving `downrec_close`
6348 // None and routing the trace through the safe
6349 // `dispatchable=false` `"self-link-retf-r1"` path.
6350 //
6351 // So when the SelfLink trip fires AND
6352 // `cur_depth >= 2` (the count > RECUNROLL_THRESHOLD
6353 // gate already requires this — kept explicit as a
6354 // safety floor), route the close through `downrec_
6355 // close` INSTEAD of `self_link_kind`. The recorder
6356 // synthesises the close marker from the most
6357 // recent Op::Call at depth `cur_depth - 1`:
6358 // - `return_pc` = `call.pc + 1` (caller's resume
6359 // PC after the recursive call returns; mirror
6360 // of the `caller_pc` derivation at the
6361 // depth>0 Op::Return capture path below).
6362 // - `target_proto` = `call.proto` (caller's
6363 // proto; equals `rec.head_proto` for self-
6364 // recursion).
6365 // - `depth_delta` = `1` (the recorder always
6366 // unrolls one level; the Op::Return path uses
6367 // the same constant).
6368 //
6369 // The lowerer's `end_idx` picker routes through
6370 // `TraceEnd::DownRec` ahead of the `self_link_kind`
6371 // arm and emits the stitch-sentinel +
6372 // caller-pc-guard scaffold. A single-candidate guard
6373 // chain (this path produces 1 caller_pc candidate
6374 // because `rec.retfs` is empty) keeps
6375 // `dispatchable=false` + `"downrec-stitch-pending"`
6376 // label (the lowerer requires
6377 // `multi_way_candidate_count >= 2`). Net behaviour:
6378 // trace compiles under DownRec routing; interp runs
6379 // the recursion naturally.
6380 //
6381 // The `cur_depth >= 2` gate is automatically
6382 // satisfied by the count > RECUNROLL_THRESHOLD=2
6383 // trip condition (3 ancestor frames sharing
6384 // head_proto implies cur_depth >= 3), kept
6385 // explicit so a future RECUNROLL_THRESHOLD tweak
6386 // doesn't silently flip shallow-recursion
6387 // shapes (cur_depth == 1) onto the DownRec arm.
6388 //
6389 // The recorded body still uses depth-baked
6390 // op_offsets[] addressing, so this is routing
6391 // scaffolding and gives no speedup by itself.
6392 let _ = kind;
6393 let relaxed_to_downrec = cur_depth >= 2 && rec.downrec_close.is_none() && {
6394 let caller_depth_u8 = (cur_depth - 1) as u8;
6395 if let Some(call_op) = rec.ops.iter().rev().find(|r| {
6396 r.inline_depth == caller_depth_u8
6397 && matches!(r.inst.op(), crate::vm::isa::Op::Call)
6398 }) {
6399 rec.downrec_close = Some(crate::jit::trace::DownRecClose {
6400 return_pc: call_op.pc + 1,
6401 target_proto: call_op.proto,
6402 depth_delta: 1,
6403 });
6404 true
6405 } else {
6406 false
6407 }
6408 };
6409 if relaxed_to_downrec {
6410 // Close-cause taxonomy: tag the lift so
6411 // probes can tally the fire rate. Mirrors
6412 // the `"downrec-restart"` bump for the
6413 // depth>0 Op::Return path (different trip
6414 // origin, same downstream routing). The
6415 // existing `"self-link-retf-r1"` label still
6416 // fires for trips that DON'T relax (no
6417 // candidate Op::Call ancestor in rec.ops, or
6418 // cur_depth < 2) via the lowerer's
6419 // dispatch_off_reason mirror at the close
6420 // handler — kept as a regression safety net.
6421 self.jit
6422 .counters
6423 .bump_close_cause("selflink-yields-to-downrec");
6424 } else {
6425 rec.self_link_kind = Some(kind);
6426 }
6427 }
6428 let should_close =
6429 at_head_loop || returned_past_head || depth_cap_hit || self_link_trip.is_some();
6430 if should_close {
6431 // Long-trace bias: a call-triggered
6432 // recording that closed with a very short body
6433 // (fib base case: `Lt`/`Jmp`/`Return1` = 3 ops,
6434 // binary_trees `make(0)`: 4 ops) is pathological.
6435 // Compiling + caching it pins `Proto.traces` to a
6436 // trace that the length gate will refuse to
6437 // dispatch (per `MIN_DISPATCHABLE_TRUNC_BODY_FLOOR
6438 // = 40`), AND blocks the back-edge / longer-call
6439 // path from re-recording the same head_pc (the
6440 // dedup `already_cached` check below short-
6441 // circuits). The fix: discard the short call-
6442 // triggered recording WITHOUT caching, and bias
6443 // the proto's `call_hot_count` back to
6444 // `THRESHOLD - HOT_RETRY_WINDOW` so the next
6445 // sequence of calls retries the trigger at a
6446 // different (hopefully deeper) recursion point.
6447 //
6448 // Back-edge triggered traces are exempt — a
6449 // tight numeric-for loop's body is legitimately
6450 // 3 ops (`Add`, ForLoop) and DOES dispatch
6451 // usefully when re-entered many times.
6452 // Coverage heuristic to detect
6453 // pathologically partial call-triggered traces:
6454 // for self-recursive / branchy protos like
6455 // `fib` (~17 bytecode ops) or
6456 // `binary_trees.make` (~26 ops), the recorder
6457 // can fire at a BASE-case entry (`fib(0)` or
6458 // `make(0)`) producing a 3–4 op trace that
6459 // covers a tiny fraction of the proto's code.
6460 // That trace is doomed by the length gate
6461 // post-compile AND blocks any longer follow-up
6462 // (the dedup `already_cached` check below). The
6463 // fix: discard call-triggered closes where
6464 // `rec.ops.len() * 2 < head_proto.code.len()`
6465 // (less than half the proto's bytecode), so the
6466 // back-edge / longer call path can take over.
6467 //
6468 // Why coverage > raw length:protos with
6469 // intrinsically short bodies (closure
6470 // factories: `Closure + Return1` = 2 ops,
6471 // simple wrappers: `LoadI + Return1` = 2 ops)
6472 // record 100% coverage even at length 2 — those
6473 // ARE legitimately short and the closure /
6474 // sunk-emit lowering paths make
6475 // them worth compiling. The heuristic admits
6476 // them. fib's `[Lt, Jmp, Return1]` (3 of ~17)
6477 // and make's `[Lt, Jmp, LoadI, Return1]` (4 of
6478 // ~26) get discarded.
6479 //
6480 // Back-edge triggered traces are unaffected —
6481 // a tight numeric-for body legitimately covers
6482 // 3 of ~3 proto ops it can dispatch from
6483 // (`Add + ForLoop`) and the recorder fires on
6484 // the back-edge, not call entry.
6485 //
6486 // `call_hot_count` is intentionally NOT reset
6487 // (an earlier draft tried `THRESHOLD - 32` but
6488 // caused active_trace contention with the
6489 // outer back-edge trigger — see
6490 // setlist_b_zero_with_call_c_zero_sunk_emits).
6491 // We give up on dispatching the pathological
6492 // shape on the same proto; the back-edge or a
6493 // longer call path on a deeper recursion point
6494 // can still record + cache a real trace.
6495 let proto_code_len = rec.head_proto.code.len();
6496 let is_partial_coverage = rec.ops.len() * 2 < proto_code_len;
6497 // Per-Proto discard cap. The relaxed
6498 // trigger condition (`c >= THRESHOLD &&
6499 // !already_cached`) means a Proto whose every
6500 // recording is partial-coverage will re-fire the
6501 // trigger every call indefinitely (1500+ in
6502 // `binary_trees`-pattern test). The cap stops
6503 // discarding after `MAX_DISCARDS_PER_PROTO` —
6504 // the next close falls through to compile (even
6505 // if partial), caches the trace, and the
6506 // `already_cached` short-circuit kills the
6507 // storm. Dispatch may still be refused
6508 // post-compile (length gate), but the recorder
6509 // stops churning.
6510 const MAX_DISCARDS_PER_PROTO: u32 = 5;
6511 let prior_discards = rec.head_proto.trace_discard_count.get();
6512 let cap_reached = prior_discards >= MAX_DISCARDS_PER_PROTO;
6513 // Flip the `gave_up` flag the
6514 // moment cap is reached (BEFORE the close-
6515 // dispatching branch below). The trigger gates
6516 // short-circuit on this flag, skipping the
6517 // RefCell + linear `already_cached` scan on
6518 // every subsequent call to this Proto. Useful
6519 // for `binary_trees_pattern`-class loads where
6520 // a single Proto sees ~20k calls post-cap.
6521 if cap_reached
6522 && rec.is_call_triggered
6523 && is_partial_coverage
6524 && !rec.head_proto.trace_gave_up.get()
6525 {
6526 rec.head_proto.trace_gave_up.set(true);
6527 }
6528 if rec.is_call_triggered && is_partial_coverage && !cap_reached {
6529 // Tally as closed (for visibility) but DROP
6530 // without compile/cache. Use the existing
6531 // closed-lens accumulator so probes can
6532 // observe the discarded shape.
6533 // Bump discard count BEFORE
6534 // dropping the recording so the next
6535 // close sees the updated counter.
6536 rec.head_proto.trace_discard_count.set(prior_discards + 1);
6537 self.jit.counters.closed += 1;
6538 self.jit
6539 .counters
6540 .closed_lens
6541 .push((rec.is_call_triggered, rec.ops.len()));
6542 // Partial-coverage discard close path.
6543 // `closed` + `closed_lens` alone can't separate
6544 // a real successful close from a discard tally,
6545 // so tag explicitly to keep the recorder-side
6546 // close-cause taxonomy single-source.
6547 self.jit
6548 .counters
6549 .bump_close_cause("partial-coverage-discard");
6550 self.jit.active_trace = None;
6551 // Continue with interp loop — don't
6552 // fall through to compile path.
6553 // The op at `pc` hasn't dispatched yet;
6554 // the outer loop iteration handles it.
6555 } else {
6556 rec.closed = true;
6557 // Detach the closed record, then try
6558 // to compile it. Dedup by `head_pc`: a Proto
6559 // already carrying a CompiledTrace for this PC
6560 // skips recompile (the hot counter caps
6561 // re-recording at `u32::MAX / 2` anyway, but
6562 // explicit dedup keeps `Proto.traces` short
6563 // for the dispatcher's linear scan).
6564 //
6565 // On failure we just bump the failed counter
6566 // and drop the record.
6567 let head_pc_val = rec.head_pc;
6568 let closed_record = self
6569 .jit
6570 .active_trace
6571 .take()
6572 .expect("active_trace was Some this branch");
6573 self.jit.counters.closed += 1;
6574 self.jit
6575 .counters
6576 .closed_lens
6577 .push((closed_record.is_call_triggered, closed_record.ops.len()));
6578 // Cache the trace on the
6579 // recorder's *head proto*, not the current
6580 // closure's proto. For non-recursive
6581 // call-triggered traces, close fires after
6582 // `Return1` pops the callee frame — `cl` at
6583 // that point is the CALLER's closure, while
6584 // `closed_record.head_proto` is the CALLEE's
6585 // proto (the one we actually want the trace
6586 // to be discoverable from on the next call).
6587 // Self-recursive fib closed via depth-cap
6588 // mid-recursion, so `cl.proto == head_proto`
6589 // there, but only by coincidence.
6590 let head_proto = closed_record.head_proto;
6591 let already_cached = head_proto
6592 .traces
6593 .borrow()
6594 .iter()
6595 .any(|t| t.head_pc == head_pc_val);
6596 if !already_cached {
6597 // Internal-loop = true: the trace runs in
6598 // a native loop until a cmp side-exits, so
6599 // the dispatcher's per-entry marshal cost
6600 // amortizes across the whole run of
6601 // iterations the loop's recorded direction
6602 // stays valid. The lowerer auto-downgrades
6603 // to one-shot for cmp-less or Call-truncating
6604 // traces.
6605 // Side traces MUST NOT
6606 // internal-loop. The parent's recorded prefix
6607 // (ops at PCs < side trace's head_pc) defines
6608 // values for registers the child's body reads
6609 // without re-writing each iter — e.g. for
6610 // s12_step_b, parent's `pc=19 Add R[12] = R[1]
6611 // + R[11]` sets R[12], and the child trace
6612 // (head_pc=24) re-runs `pc=20 Move R[1] =
6613 // R[12]` each iter via its outer ForLoop
6614 // internal-loop, ALWAYS reading the stale
6615 // entry-time R[12]. The parent's Add never
6616 // re-runs during child's loop, so R[1] gets
6617 // pinned to one stale value. Force one-shot
6618 // for side traces: each parent-exit round-
6619 // trips through dispatcher → parent's Add
6620 // runs → side trace runs ONE iter → return.
6621 let opts = crate::jit::trace::CompileOptions {
6622 internal_loop: closed_record.side_trace_parent.is_none(),
6623 pre53: self.version() <= LuaVersion::Lua53,
6624 aot: false,
6625 };
6626 // Route through trace_compiler; split-borrow JitState
6627 // so the trait method can take `&mut dyn JitStorage`.
6628 let result = {
6629 let jit = &mut self.jit;
6630 let storage: &mut dyn crate::jit::JitStorage = jit.storage.as_mut();
6631 jit.trace_compiler
6632 .try_compile_trace(storage, &closed_record, opts)
6633 };
6634 match result {
6635 Some(mut ct) => {
6636 // Tally Sinkable sites
6637 // + actually-sunk-emit sites + materialise
6638 // emit sites before moving `ct` into
6639 // Proto.traces.
6640 self.jit.counters.sinkable_seen +=
6641 ct.sinkable_sites_seen as u64;
6642 self.jit.counters.accum_bufferable_seen +=
6643 ct.accum_bufferable_seen as u64;
6644 self.jit.counters.sunk_alloc += ct.sunk_alloc_seen as u64;
6645 self.jit.counters.materialize_emit +=
6646 ct.materialize_emit_count as u64;
6647 self.jit.counters.closure_emit += ct.closure_seen as u64;
6648 if ct.is_inline_abort_close {
6649 self.jit.counters.inline_abort += 1;
6650 }
6651 // Split tally so a
6652 // probe can answer the AOT
6653 // `accepted_with_per_exit_inline`
6654 // gate's question at the JIT
6655 // surface too: how many compiled
6656 // traces emitted depth>0 cmp
6657 // side-exits, and how many of
6658 // those survived all the
6659 // `dispatchable = false` pins
6660 // (`InlineAbort-gate`,
6661 // `self-link-retf-r1`,
6662 // `downrec-stitch-pending`, etc.).
6663 if !ct.per_exit_inline.is_empty() {
6664 self.jit.counters.per_exit_inline_compiled += 1;
6665 if ct.dispatchable {
6666 self.jit.counters.per_exit_inline_dispatchable += 1;
6667 }
6668 }
6669 if let Some(reason) = ct.dispatch_off_reason {
6670 self.jit.counters.dispatch_off_reasons.push(reason);
6671 // Mirror
6672 // the ordered Vec push into
6673 // the per-reason HashMap so
6674 // probes can answer "how many
6675 // of each dispatch_off label
6676 // fired" in O(1) without
6677 // walking the Vec. Same
6678 // bucket as the recorder-side
6679 // abort/discard tags above.
6680 self.jit.counters.bump_close_cause(reason);
6681 }
6682 // Count compiled traces that
6683 // carry a down-recursion stitch
6684 // link. Bumped here (not at the
6685 // lowerer emit site) because the
6686 // Vm's JitCounters live on the Vm,
6687 // and the lowerer doesn't have a
6688 // Vm handle. Read via
6689 // `Vm::trace_downrec_link_compiled_count`.
6690 if ct.downrec_link.is_some() {
6691 self.jit.counters.downrec_link_compiled += 1;
6692 }
6693 // Multi-way guard emit counter.
6694 // Bumped when the lowerer collected
6695 // >= 2 distinct caller_pc candidates
6696 // and lifted `dispatchable=true`.
6697 // The single-CMP shape stores
6698 // `1` here without bumping; non-
6699 // DownRec closes store `0`.
6700 if ct.downrec_multi_way_count >= 2 {
6701 self.jit.counters.multi_way_guard_emitted += 1;
6702 }
6703 // Side-trace finalisation.
6704 // Pin `dispatchable=false` so the
6705 // primary lookup `traces.find(|t|
6706 // t.head_pc == pc && t.dispatchable)`
6707 // never matches this entry — the
6708 // side trace is meant to be entered
6709 // ONLY through the parent's exit
6710 // indirection, not the
6711 // back-edge / call-trigger paths.
6712 // Then write the entry fn ptr into
6713 // the parent's `exit_side_trace_ptrs`
6714 // slot so the parent's IR can read it.
6715 if let Some((parent_proto, parent_head_pc, parent_exit_idx)) =
6716 closed_record.side_trace_parent
6717 {
6718 // The lowerer's own verdict: a trace it
6719 // compiled but would not dispatch (an
6720 // untyped table read, an inline abort)
6721 // is not safe to enter from the parent's
6722 // exit either — unless the only reason
6723 // was the length gate, which weighs
6724 // dispatch overhead, not soundness (the
6725 // lowerer records the other reasons
6726 // first).
6727 let runnable = ct.dispatchable
6728 || ct.dispatch_off_reason == Some("length-gate");
6729 ct.dispatchable = false;
6730 let entry_ptr = ct.entry as *const () as *const u8;
6731 let _side_trace_head_pc = closed_record.head_pc;
6732 let parent_traces = parent_proto.traces.borrow();
6733 if let Some(parent_ct) = parent_traces
6734 .iter()
6735 .find(|t| t.head_pc == parent_head_pc)
6736 {
6737 // Shape-match
6738 // gate. Find the parent's per-exit
6739 // tag snapshot at the wired exit
6740 // (inline / tag / global) and
6741 // check the child's entry_tags
6742 // match. If not, leave the cell
6743 // null + skip cache populate so
6744 // the parent IR's
6745 // `call_indirect` stays inert at
6746 // this exit (the child's
6747 // shape-specialised IR would
6748 // mis-interpret raw bits the
6749 // parent writes to reg_state).
6750 let inline_n = parent_ct.per_exit_inline.len();
6751 let tags_n = parent_ct.per_exit_tags.len();
6752 let parent_exit_tags_slice: &[
6753 crate::jit::trace::ExitTag
6754 ] = if parent_exit_idx < inline_n {
6755 &parent_ct.per_exit_inline
6756 [parent_exit_idx]
6757 .exit_tags
6758 } else if parent_exit_idx
6759 < inline_n + tags_n
6760 {
6761 &parent_ct.per_exit_tags
6762 [parent_exit_idx - inline_n]
6763 .1
6764 } else {
6765 &parent_ct.exit_tags
6766 };
6767 let shape_matches =
6768 crate::jit::trace::exit_tags_match_entry_tags(
6769 &ct.entry_tags,
6770 parent_exit_tags_slice,
6771 &parent_ct.entry_tags,
6772 );
6773 if !shape_matches {
6774 self.jit.counters.side_trace_shape_mismatch += 1;
6775 }
6776 let shape_ok = runnable && shape_matches;
6777 // Write the child's
6778 // entry fn ptr to BOTH the legacy
6779 // `exit_side_trace_ptrs[idx]`
6780 // cell (read by the
6781 // walk_any_side_ptr_non_null tests)
6782 // AND the per-kind cell
6783 // whose heap address the parent's
6784 // IR baked. The IR-baked
6785 // cell is what the call_indirect
6786 // gate actually reads. Only write
6787 // when the shape gate passes.
6788 if shape_ok {
6789 if let Some(cell) = parent_ct
6790 .exit_side_trace_ptrs
6791 .get(parent_exit_idx)
6792 {
6793 cell.set(entry_ptr);
6794 }
6795 // Compute (kind, local) for the
6796 // IR-baked cell. Layout follows
6797 // exit_hit_counts: inline first,
6798 // then per_exit_tags, then the
6799 // global tail slot.
6800 let (sent_kind, sent_local) = if parent_exit_idx
6801 < inline_n
6802 {
6803 parent_ct.per_exit_inline[parent_exit_idx]
6804 .side_trace_ptr
6805 .set(entry_ptr);
6806 (
6807 crate::jit::trace::SIDE_SENT_KIND_INLINE,
6808 parent_exit_idx as u32,
6809 )
6810 } else if parent_exit_idx < inline_n + tags_n {
6811 let local = parent_exit_idx - inline_n;
6812 if let Some(b) =
6813 parent_ct.tags_side_trace_ptrs.get(local)
6814 {
6815 b.set(entry_ptr);
6816 }
6817 (
6818 crate::jit::trace::SIDE_SENT_KIND_TAG,
6819 local as u32,
6820 )
6821 } else {
6822 parent_ct.global_side_trace_ptr.set(entry_ptr);
6823 (crate::jit::trace::SIDE_SENT_KIND_GLOBAL, 0)
6824 };
6825 self.jit.counters.side_trace_compiled += 1;
6826 // Flip the
6827 // parent's fast-path hint so
6828 // the dispatcher knows to do
6829 // the tentative decode + cell
6830 // check on subsequent
6831 // dispatches. Set once and
6832 // stays true (we never unwire
6833 // a side trace today).
6834 parent_ct.has_any_side_wired.set(true);
6835
6836 // Populate
6837 // the O(1) lookup cache the
6838 // dispatcher consults on
6839 // sentinel-bit-set returns.
6840 // Key is the encoded sentinel
6841 // (same encoding the IR ORs
6842 // into bits 56..=62 of the
6843 // child's i64 return).
6844 let sentinel =
6845 crate::jit::trace::encode_side_sentinel(
6846 sent_kind, sent_local,
6847 );
6848 let predicted_idx = if std::ptr::eq(
6849 parent_proto.as_ptr(),
6850 head_proto.as_ptr(),
6851 ) {
6852 parent_traces.len() as u32
6853 } else {
6854 head_proto.traces.borrow().len() as u32
6855 };
6856 parent_ct
6857 .side_trace_cache
6858 .borrow_mut()
6859 .insert(sentinel, predicted_idx);
6860 }
6861 }
6862 drop(parent_traces);
6863 }
6864 head_proto.traces.borrow_mut().push(TArc::new(ct));
6865 self.jit.counters.compiled += 1;
6866 }
6867 None => {
6868 self.jit.counters.compile_failed += 1;
6869 note_trace_compile_failure(head_proto, closed_record.head_pc);
6870 self.jit
6871 .counters
6872 .compile_failed_reasons
6873 .push(self.jit.trace_compiler.last_compile_checkpoint());
6874 }
6875 }
6876 }
6877 } // close the long-trace-bias else branch
6878 } else {
6879 // Depth-aware push at the
6880 // current `cur_depth`. The `depth_cap_hit` /
6881 // `returned_past_head` early-exit is handled by
6882 // the `should_close` branch above; reaching here
6883 // means `cur_depth <= MAX_INLINE_DEPTH` and the
6884 // trace head's frame is still live.
6885 let depth_u8 = cur_depth as u8;
6886 if depth_u8 > self.jit.max_depth_seen {
6887 self.jit.max_depth_seen = depth_u8;
6888 }
6889 // Fix up a prior `Op::Call C=0` (multi-
6890 // return / variable return count). Recorder pushed
6891 // it with var_count=None before the call dispatched;
6892 // now that the call has returned and we're about to
6893 // push the next op, top reflects the actual return
6894 // count. Snapshot top - (caller.base + call.a).
6895 if let Some(last) = rec.ops.last_mut()
6896 && matches!(last.inst.op(), crate::vm::isa::Op::Call)
6897 && last.inst.c() == 0
6898 && last.var_count.is_none()
6899 && let Some(f) = self.frames.last().and_then(CallFrame::lua)
6900 {
6901 let from = f.base + last.inst.a();
6902 if self.top >= from {
6903 last.var_count = Some(self.top - from);
6904 }
6905 }
6906 // For SetList B=0, snapshot the source
6907 // count = top - A - 1 (mirrors Lua's `n = top - ra
6908 // - 1` from lvm.c OP_SETLIST). Sources are
6909 // R[A+1..top), exclusive top. For Call C=0's
6910 // var_count (the return count = top - A inclusive),
6911 // see the prior-op fix-up above; here we
6912 // initialise the current Call op to None and let
6913 // the fix-up on the next op's push populate it.
6914 let var_count = if matches!(inst.op(), crate::vm::isa::Op::SetList)
6915 && inst.b() == 0
6916 && let Some(f) = self.frames.last().and_then(CallFrame::lua)
6917 {
6918 let from = f.base + inst.a();
6919 if self.top > from {
6920 Some(self.top - from - 1)
6921 } else {
6922 None
6923 }
6924 } else {
6925 None
6926 };
6927 let op = crate::jit::trace::RecordedOp {
6928 proto: cl.proto,
6929 pc,
6930 inst,
6931 inline_depth: depth_u8,
6932 var_count,
6933 };
6934 // Depth>0 Return0/Return1 mirrors
6935 // LuaJIT's `IR_RETF` (lj_record.c:922+ lj_record_ret).
6936 // Captured as a side-channel `RetfRecord` parallel to
6937 // `ops` when `self_link_enabled` is on. The
6938 // down-rec stitch consumes these to guard side-trace
6939 // inlined-frame topology against the recorded shape.
6940 // Gated on the same flag as the cycle catch so the
6941 // ship-default path (p16 off) sees zero behavior
6942 // change. `caller_pc` is the recorded enclosing Call's
6943 // pc + 1 — interp's resume point after the inlined
6944 // frame pops.
6945 if self.jit.self_link_enabled
6946 && depth_u8 > 0
6947 && matches!(
6948 inst.op(),
6949 crate::vm::isa::Op::Return0 | crate::vm::isa::Op::Return1
6950 )
6951 {
6952 let results: u8 = match inst.op() {
6953 crate::vm::isa::Op::Return0 => 0,
6954 crate::vm::isa::Op::Return1 => 1,
6955 _ => 0,
6956 };
6957 // Most recent Op::Call recorded at the caller's
6958 // depth (`depth_u8 - 1`) is the frame this Return
6959 // is unwinding from. Reverse scan stops at the
6960 // first match.
6961 let caller_depth = depth_u8 - 1;
6962 let caller_call = rec.ops.iter().rev().find(|r| {
6963 r.inline_depth == caller_depth
6964 && matches!(r.inst.op(), crate::vm::isa::Op::Call)
6965 });
6966 let caller_pc = caller_call.map(|r| r.pc + 1).unwrap_or(pc);
6967 // Capture the caller's proto
6968 // for the RetfRecord. LuaJIT `IR_RETF.op1`
6969 // equivalent. For fib(28) the caller's proto
6970 // equals the trace head; for future mutual
6971 // recursion the recorded Op::Call's proto is the
6972 // right target. Fallback to head_proto when no
6973 // enclosing Call op was captured (mirrors
6974 // `caller_pc`'s fallback to the Return's own pc).
6975 let caller_proto = caller_call.map(|r| r.proto).unwrap_or(rec.head_proto);
6976 rec.retfs.push(crate::jit::trace::RetfRecord {
6977 from_depth: depth_u8,
6978 to_depth: caller_depth,
6979 results,
6980 caller_pc,
6981 proto: caller_proto,
6982 });
6983 // DownRec close trigger:
6984 // count RetfRecords on this recording whose
6985 // `proto` matches `caller_proto` (LuaJIT
6986 // `check_downrec_unroll` chain filter
6987 // `op1 == ptref`). Threshold mirrors
6988 // RECUNROLL_THRESHOLD; first trip stamps the
6989 // `downrec_close` marker, subsequent retfs
6990 // keep the marker without overwrite. The
6991 // lowerer's end_idx picker routes through
6992 // TraceEnd::DownRec when the marker is set.
6993 if rec.downrec_close.is_none() {
6994 let caller_proto_ptr = caller_proto.as_ptr();
6995 let prior_match_count = rec
6996 .retfs
6997 .iter()
6998 .filter(|r| r.proto.as_ptr() == caller_proto_ptr)
6999 .count();
7000 // Strictly-greater-than threshold matches
7001 // LuaJIT `count + J->tailcalled > recunroll`.
7002 // The newly-pushed retf is already counted.
7003 if prior_match_count > crate::jit::trace::RECUNROLL_THRESHOLD {
7004 rec.downrec_close = Some(crate::jit::trace::DownRecClose {
7005 return_pc: caller_pc,
7006 target_proto: caller_proto,
7007 depth_delta: 1,
7008 });
7009 // Close-cause taxonomy: tag the
7010 // restart with `"downrec-restart"`. The
7011 // lowerer adds `"downrec-stitch-failed"` when
7012 // the lifted back-edge falls back to
7013 // deopt.
7014 self.jit.counters.bump_close_cause("downrec-restart");
7015 }
7016 }
7017 }
7018 // Capture FieldIcSnapshot for the
7019 // FIRST eligible Op::GetField site under env-gate
7020 // LUNA_JIT_FIELD_IC=1. "Eligible" means:
7021 // - R[B] is Value::Table with metatable.is_none()
7022 // - K[C] is Value::Str
7023 // - The string key actually occupies a hash slot
7024 // (so the IC's slot_idx is a real index, not
7025 // a probe sentinel).
7026 // Once captured, subsequent GetFields skip this
7027 // logic (rec.field_ic_snapshot.is_some() short-
7028 // circuits). Env-OFF short-circuits on the cached
7029 // atomic check inside field_ic_enabled().
7030 if rec.field_ic_snapshot.is_none()
7031 && matches!(inst.op(), crate::vm::isa::Op::GetField)
7032 && crate::jit::trace_types::field_ic_enabled()
7033 {
7034 let b = inst.b();
7035 let c_idx = inst.c() as usize;
7036 let r_b = self.stack[(base + b) as usize];
7037 if let Value::Table(g) = r_b
7038 && g.metatable().is_none()
7039 && c_idx < cl.proto.consts.len()
7040 && let Value::Str(s) = cl.proto.consts[c_idx]
7041 {
7042 let key = Value::Str(s);
7043 let tbl_ref = &*g;
7044 if let Some(slot_idx) = tbl_ref.find_node_idx(key)
7045 && let Some(val) = tbl_ref.node_val_at(slot_idx)
7046 {
7047 let op_idx = rec.ops.len() as u32;
7048 rec.field_ic_snapshot =
7049 Some(crate::jit::trace_types::FieldIcSnapshot {
7050 op_idx,
7051 nodes_len: tbl_ref.nodes_capacity() as u64,
7052 slot_idx: slot_idx as u64,
7053 key_ptr_bits: s.as_ptr() as u64,
7054 cached_val_tag: val.tag_byte(),
7055 });
7056 self.jit.counters.field_ic_snapshot_captured += 1;
7057 }
7058 }
7059 }
7060 if !rec.push(op) {
7061 // Recorder overflow (MAX_TRACE_LEN). Tag it
7062 // explicitly under the close-cause bucket so
7063 // probes can tally overflow vs other abort
7064 // causes in O(1).
7065 self.jit.active_trace = None;
7066 self.jit.counters.aborted += 1;
7067 self.jit.counters.bump_close_cause("trace-overflow");
7068 }
7069 }
7070 }
7071
7072 // Trace JIT dispatcher.
7073 //
7074 // When the dispatch loop is about to execute the op at
7075 // `pc` and there's a `numeric_only` CompiledTrace cached
7076 // for that `head_pc`, marshal the live regs into an
7077 // i64 buffer, jump into the trace, and resume the
7078 // interpreter at the returned continuation PC.
7079 //
7080 // Skipped (zero overhead) when `trace_jit_enabled` is
7081 // false; the lookup is a borrow + scan over
7082 // `cl.proto.traces`, which is a `Vec` whose size is at
7083 // most one entry per back-edge per Proto in practice.
7084 //
7085 // Marshalling contract — only Int slots survive the
7086 // round-trip cleanly (the reg_state ABI is `*mut i64`
7087 // with no tag info). Any non-Int slot in the affected
7088 // window forces a skip; interp takes over for one op
7089 // and the back-edge brings us back to try again next
7090 // pass (slots that were Nil/Float at one moment can
7091 // settle to Int by the time the next back-edge fires).
7092 //
7093 // A trace that comes back with `vm.jit.pending_err`
7094 // parked is treated as a deopt: clear the err, leave
7095 // the stack as the trace wrote it, and let the
7096 // interpreter run from the same `pc`. The trace itself
7097 // is left cached — a future entry might find no
7098 // metatable in the way and succeed.
7099 // Single Rc<CompiledTrace> clone instead of per-field Rc
7100 // clones: proto.traces is Vec<Rc<CompiledTrace>>; the
7101 // dispatcher clones ONE Rc and reads fields via auto-deref.
7102 // One-shot consume of the
7103 // `suppress_downrec_admit_once` flag. Set by the
7104 // downrec post-invoke arm below when it force-deopts the
7105 // trace (caller-pc guard miss OR cycle-budget exhausted)
7106 // so the NEXT interpreter loop iteration skips the
7107 // downrec admit, lets interp run the op at `head_pc`,
7108 // advances `pc` past `head_pc`, and breaks the otherwise-
7109 // infinite admit loop. Reading + clearing here means a
7110 // single dispatch tick consumes the suppression — the
7111 // following tick re-admits naturally (with the budget
7112 // also reset by the deopt site).
7113 let downrec_admit_blocked = self.jit.suppress_downrec_admit_once;
7114 self.jit.suppress_downrec_admit_once = false;
7115 if self.jit.trace_enabled
7116 && let Some(ct) = {
7117 let traces = cl.proto.traces.borrow();
7118 traces
7119 .iter()
7120 .find(|t| {
7121 if t.head_pc != pc {
7122 return false;
7123 }
7124 let is_downrec = t.downrec_link.is_some();
7125 // The one-shot suppress
7126 // flag blocks any admit (primary or fallback)
7127 // for `downrec_link`-bearing traces so the
7128 // next interp iter can run the natural op
7129 // at `head_pc` and advance past it. The multi-way
7130 // `dispatchable=true` lift means the suppress
7131 // must also cover the primary `t.dispatchable`
7132 // arm — otherwise the lifted lookup would
7133 // immediately re-admit after a force-deopt
7134 // and the infinite loop returns.
7135 if downrec_admit_blocked {
7136 return false;
7137 }
7138 // Primary arm: `dispatchable=true` traces
7139 // (lifted multi-way DownRec or normal traces).
7140 // Fallback arm: single-CMP `dispatchable=false`
7141 // DownRec traces (single-CMP guard kept
7142 // pinned because the 90% miss-rate would
7143 // make blind admit perf-negative).
7144 t.dispatchable || is_downrec
7145 })
7146 .cloned()
7147 }
7148 {
7149 // Borrow Rc<[T]> fields as &Rc<[T]> instead
7150 // of cloning. The outer `ct: Rc<CompiledTrace>` is held
7151 // across the entire dispatch block so the fields outlive
7152 // all consumers.
7153 let entry_fn = ct.entry;
7154 let head_pc_val = ct.head_pc;
7155 let window_size = ct.window_size;
7156 let exit_tags = &ct.exit_tags;
7157 let per_exit_tags = &ct.per_exit_tags;
7158 let per_exit_inline = &ct.per_exit_inline;
7159 let compile_entry_tags = &ct.entry_tags;
7160 let global_tag_res_kind = ct.global_tag_res_kind;
7161 let exit_hit_counts = &ct.exit_hit_counts;
7162 let max_stack = cl.proto.max_stack as usize;
7163 let window_size_us = window_size as usize;
7164 let base_us = base as usize;
7165 // `reg_state` sized to the trace's `window_size`, which
7166 // may exceed max_stack.
7167 // Marshal-in still only writes [0..max_stack); slots
7168 // [max_stack..window_size) are zero-initialised and
7169 // filled by the trace's own GetUpval / arith.
7170 // Reuse the Vm's amortised buffers
7171 // instead of allocating fresh Vecs each dispatch.
7172 // mem::take leaves an empty placeholder we restore
7173 // at the end of the dispatch block (success +
7174 // deopt paths both fall through to the restore).
7175 let mut entry_tags: Vec<u8> = std::mem::take(&mut self.jit.entry_tags_buf);
7176 entry_tags.clear();
7177 entry_tags.reserve(max_stack);
7178 // This trace was admitted via the
7179 // `downrec_link.is_some()` arm rather than the normal
7180 // `dispatchable=true` arm. The pre-invoke path
7181 // populates a reserved saved-PC slot just past the
7182 // normal register window so the lowerer's guard load
7183 // (`reg_state[window_size]`) compares the runtime
7184 // saved caller PC against the recorded `dr_return_pc`.
7185 //
7186 // No `!ct.dispatchable` gate: when the lowerer lifts
7187 // `dispatchable = true` for multi-way guards, the
7188 // trace's body still emits the downrec sentinel shape
7189 // on return — the saved-PC slot
7190 // and post-invoke classifier must keep firing.
7191 // `downrec_link.is_some()` is the unique structural
7192 // signal that the trace closes via DownRec.
7193 let is_downrec_entry = ct.downrec_link.is_some();
7194 let mut reg_state: Vec<i64> = std::mem::take(&mut self.jit.reg_state_buf);
7195 reg_state.clear();
7196 // When admitting a downrec trace,
7197 // size the buffer to `window_size + 1` so the lowerer
7198 // can `load(I64, ..., reg_state, window_size * 8)`
7199 // for the saved caller PC guard input. The extra slot
7200 // is the LAST element so cranelift's existing
7201 // `0..window_size` accesses are unaffected.
7202 let reg_state_len = if is_downrec_entry {
7203 window_size_us + 1
7204 } else {
7205 window_size_us
7206 };
7207 reg_state.resize(reg_state_len, 0i64);
7208 let mut dispatch_ok = true;
7209 for i in 0..max_stack {
7210 let v = self.stack[base_us + i];
7211 let (tag, raw) = v.unpack();
7212 entry_tags.push(tag);
7213 // Entry tag guard. The trace's IR
7214 // is specialised to the compile-time entry tags
7215 // (via current_kinds propagation from
7216 // from_entry_tag). A runtime tag mismatch means
7217 // body ops would mis-interpret raw bits (e.g.
7218 // treat a Str pointer as Int payload → garbage).
7219 // Skip dispatch on mismatch so interp handles
7220 // this entry shape; the trace stays cached for
7221 // future entries that match.
7222 if i < compile_entry_tags.len() && tag != compile_entry_tags[i] {
7223 dispatch_ok = false;
7224 break;
7225 }
7226 match tag {
7227 // Int / Float / Table / Nil all marshal
7228 // to raw payload cleanly; the trace's IR
7229 // treats the 8-byte slot as an i64 (with
7230 // f64 ops bitcasting around the boundary).
7231 crate::runtime::value::raw::INT
7232 | crate::runtime::value::raw::FLOAT
7233 | crate::runtime::value::raw::TABLE
7234 | crate::runtime::value::raw::CLOSURE
7235 // Native iter slots (e.g.
7236 // R[A] = ipairs_iter) are present in
7237 // generic-for traces; the raw bits are a
7238 // valid `*mut NativeClosure` and round-trip
7239 // cleanly.
7240 | crate::runtime::value::raw::NATIVE
7241 // Str slots show up in
7242 // string-concat traces; raw bits = `*mut
7243 // LuaStr` (interned, GC-managed). Round-
7244 // trips cleanly as a heap pointer.
7245 | crate::runtime::value::raw::STR
7246 | crate::runtime::value::raw::NIL => {
7247 // 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).
7248 reg_state[i] = unsafe { raw.zero as i64 };
7249 }
7250 _ => {
7251 dispatch_ok = false;
7252 break;
7253 }
7254 }
7255 }
7256
7257 if dispatch_ok {
7258 debug_assert_eq!(head_pc_val, pc, "trace cache hit's head_pc != pc");
7259 // A recording in progress cannot see what the trace runs
7260 // natively: it would resume after the trace with ops
7261 // missing, and could close as a loop that never ran (a
7262 // side trace of two ops returning its own head, which
7263 // the dispatcher then entered forever). Drop it.
7264 if self.jit.active_trace.take().is_some() {
7265 self.jit.counters.aborted += 1;
7266 self.jit.counters.bump_close_cause("reached-compiled-trace");
7267 }
7268 self.jit.pending_err = None;
7269 // Snapshot the pre-entry frame
7270 // count. A cmp@d>0 side-exit calls the materialize
7271 // helper which pushes inlined frames onto
7272 // `vm.frames`; on deopt those frames must be popped
7273 // before falling through to the interpreter, else
7274 // the stack grows unboundedly per deopted dispatch.
7275 let pre_frames = self.frames.len();
7276 // Saved-PC slot population. The
7277 // recorded `dr_return_pc` on the closing trace is
7278 // the caller's resume PC captured at a depth>0
7279 // Return push (recorder push site). The natural runtime analogue for self-
7280 // stitch is the dispatching frame's PARENT frame's
7281 // PC: the trace's head_pc sits inside a Lua frame,
7282 // and the parent (caller) frame's `pc` is what
7283 // luna would observe as `[base-8]` in the LJ
7284 // `asm_retf` shape (`lj_asm_arm64.h:565`). When
7285 // the parent isn't a Lua frame (top-level dispatch
7286 // — first invocation through `call_value`), no
7287 // saved PC exists; we write 0, which always
7288 // mismatches the recorded `dr_return_pc != 0`
7289 // invariant (debug-asserted in the luna-jit trace
7290 // lowerer).
7291 if is_downrec_entry {
7292 let saved_pc: i64 = if pre_frames >= 2 {
7293 match &self.frames[pre_frames - 2] {
7294 CallFrame::Lua(parent) => parent.pc as i64,
7295 CallFrame::Cont(_) => 0,
7296 }
7297 } else {
7298 0
7299 };
7300 reg_state[window_size_us] = saved_pc;
7301 }
7302 // `LUNA_AOT_PROBE`
7303 // diagnostic hook. The probe fires once per trace dispatch
7304 // (regardless of JIT vs AOT origin — both go through this
7305 // arm), letting the AOT smoke test verify mcode actually
7306 // executed. Guarded behind `OnceLock` so the env read is
7307 // a one-time cost per process; not gated on a particular
7308 // counter so the smoke test gets a deterministic single-
7309 // line `aot_trace_fired pc=N` per first dispatch.
7310 if jit_probe_enabled() && self.jit.counters.dispatched == 0 {
7311 eprintln!("luna-runtime-helpers: aot_trace_fired pc={head_pc_val}");
7312 }
7313 let continuation_pc = {
7314 // chunk_compiler.enter
7315 // (CraneliftBackend delegates to enter_jit;
7316 // NullJitBackend returns an inert guard).
7317 let vm_ptr: *mut Vm = self;
7318 let _guard = self.jit.chunk_compiler.enter(vm_ptr, Some(cl));
7319 // 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).
7320 unsafe { entry_fn(reg_state.as_mut_ptr()) }
7321 };
7322 self.jit.counters.dispatched += 1;
7323
7324 if self.jit.pending_err.is_some() {
7325 self.jit.pending_err = None;
7326 self.jit.counters.deopt += 1;
7327 // Unwind any helper-pushed
7328 // inlined frames before the interpreter resumes.
7329 // Don't restore reg_state — the trace's partial
7330 // writes are discarded; interp re-executes from
7331 // the original `pc`.
7332 while self.frames.len() > pre_frames {
7333 frames_pop_sync(&mut self.frames, &mut self.frames_top);
7334 }
7335 if is_downrec_entry {
7336 // pending_err observed
7337 // mid-trace inside a downrec admit. Treat
7338 // it as a guard miss: bump `downrec_deopt`
7339 // and suppress the next downrec admit so
7340 // interp can advance past `head_pc` and
7341 // the same trace doesn't immediately re-
7342 // fire on the next loop iteration.
7343 self.jit.counters.downrec_deopt += 1;
7344 self.jit.suppress_downrec_admit_once = true;
7345 }
7346 } else if is_downrec_entry && {
7347 // Only enter the
7348 // downrec classifier for returns whose shape
7349 // matches the lowerer's `downrec_idx_opt` tail
7350 // emit: either the stitch_blk DOWNREC sentinel
7351 // (HIT) or the deopt_blk GLOBAL-sentinel-with-
7352 // body==head_pc (MISS via guard fail). Any
7353 // other return from a downrec trace (intermediate
7354 // body cmp side-exit, GetField inference fail,
7355 // etc.) carries a different sentinel/body shape
7356 // and means the body exited BEFORE reaching the
7357 // downrec close — classify those through the
7358 // normal decode path (else branch below) so
7359 // reg_state restores + pc advances correctly.
7360 // Classifying them all as MISS would skip the
7361 // normal restore, inflating `downrec_deopt` with
7362 // non-downrec events and losing the trace's
7363 // mid-flight writes.
7364 let raw_ret = continuation_pc as u64;
7365 let from_side_trace = (raw_ret >> 63) & 1 == 1;
7366 let sentinel_code = if from_side_trace {
7367 ((raw_ret >> 56) & 0x7F) as u32
7368 } else {
7369 0
7370 };
7371 let raw_body = raw_ret & 0x00FF_FFFF_FFFF_FFFFu64;
7372 let global_deopt_code = crate::jit::trace_types::encode_side_sentinel(
7373 crate::jit::trace_types::SIDE_SENT_KIND_GLOBAL,
7374 0,
7375 );
7376 from_side_trace
7377 && (crate::jit::trace_types::is_downrec_sentinel(sentinel_code)
7378 || (sentinel_code == global_deopt_code
7379 && raw_body == head_pc_val as u64))
7380 } {
7381 // Downrec event classifier.
7382 let raw_ret = continuation_pc as u64;
7383 let sentinel_code = ((raw_ret >> 56) & 0x7F) as u32;
7384 if crate::jit::trace_types::is_downrec_sentinel(sentinel_code) {
7385 // Guard HIT — saved_pc matched one of the
7386 // baked candidates and the trace's
7387 // `stitch_blk` arm returned the DOWNREC
7388 // sentinel. Cycle-safety checkpoint:
7389 // decrement budget; on underflow,
7390 // reclassify as deopt + reset budget.
7391 // `STITCH_DEPTH_DEFAULT = 32` lets
7392 // ~all natural HITs in a hot loop fire
7393 // before reset pressure.
7394 if self.jit.stitch_depth_remaining > 0 {
7395 self.jit.stitch_depth_remaining -= 1;
7396 self.jit.counters.downrec_dispatched += 1;
7397 } else {
7398 self.jit.counters.downrec_deopt += 1;
7399 self.jit.stitch_depth_remaining =
7400 crate::vm::jit_state::JitState::STITCH_DEPTH_DEFAULT;
7401 }
7402 } else {
7403 // Guard MISS via the lowerer's deopt_blk
7404 // arm (GLOBAL sentinel + body == head_pc).
7405 // The deopt_blk emit performs the
7406 // store-back via `emit_store_back_and_return_pc`,
7407 // so the live stack already reflects the
7408 // body's writes; no extra restore needed
7409 // from the dispatcher side.
7410 self.jit.counters.downrec_deopt += 1;
7411 }
7412 self.jit.suppress_downrec_admit_once = true;
7413 // Pop helper-pushed inlined frames (defensive —
7414 // the downrec emit shape doesn't push frames in the
7415 // tail, but a body side-exit before reaching
7416 // the tail may have via the materialize helper).
7417 while self.frames.len() > pre_frames {
7418 frames_pop_sync(&mut self.frames, &mut self.frames_top);
7419 }
7420 self.jit.reg_state_buf = reg_state;
7421 self.jit.entry_tags_buf = entry_tags;
7422 continue;
7423 } else {
7424 // Restore each slot using the trace's
7425 // exit-tag analysis (see ExitTag docs).
7426 // Decode the IR's
7427 // side-exit shape. Upper 32 bits = (site_idx
7428 // + 1) for inline cmp side-exits, 0 for
7429 // legacy clean-tail / non-inline exits.
7430 // The decode lives in
7431 // `crate::jit::trace::decode_exit_shape` so
7432 // side-trace returns can reuse it with the SIDE
7433 // TRACE's shape inputs when the sentinel bit
7434 // is set on `raw_ret`.
7435 let raw_ret = continuation_pc as u64;
7436 // Side-trace return decode.
7437 // Bit 63 of `raw_ret` is the side-trace
7438 // marker the parent's IR OR'd in when it
7439 // tail-called into a wired child trace.
7440 // Bits 56..=62 carry the sentinel code (the
7441 // cache key into the parent's
7442 // `side_trace_cache`); bits 0..=55 are the
7443 // child's own return value (encoded site or
7444 // plain cont_pc) which we MUST decode using
7445 // the CHILD's per_exit_inline / per_exit_tags
7446 // / exit_tags / exit_hit_counts — not the
7447 // parent's. The dispatcher snapshot read
7448 // above holds the parent's shapes; when bit
7449 // 63 is set we re-fetch the child's via the
7450 // sentinel-keyed cache.
7451 let from_side_trace = (raw_ret >> 63) & 1 == 1;
7452 let (
7453 decode_inline,
7454 decode_tags,
7455 decode_exit_tags,
7456 decode_hit_counts,
7457 decode_body,
7458 child_ran,
7459 ) = if from_side_trace {
7460 let sentinel_code = ((raw_ret >> 56) & 0x7F) as u32;
7461 let body = raw_ret & 0x00FF_FFFF_FFFF_FFFFu64;
7462 let traces = cl.proto.traces.borrow();
7463 let child_idx = traces
7464 .iter()
7465 .find(|t| t.head_pc == head_pc_val)
7466 .and_then(|pct| {
7467 pct.side_trace_cache.borrow().get(&sentinel_code).copied()
7468 });
7469 if let Some(idx) = child_idx
7470 && let Some(child) = traces.get(idx as usize)
7471 {
7472 if crate::jit::trace::v2c_probe_enabled() {
7473 eprintln!(
7474 "[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={}",
7475 sentinel_code,
7476 body,
7477 idx,
7478 child.n_ops,
7479 child.head_pc,
7480 child.window_size,
7481 pc,
7482 window_size,
7483 child.dispatchable,
7484 child.is_inline_abort_close,
7485 );
7486 }
7487 (
7488 child.per_exit_inline.clone(),
7489 child.per_exit_tags.clone(),
7490 child.exit_tags.clone(),
7491 child.exit_hit_counts.clone(),
7492 body,
7493 true,
7494 )
7495 } else {
7496 if crate::jit::trace::v2c_probe_enabled() {
7497 eprintln!(
7498 "[v2c-A3-decode] sentinel={:#04x} body={:#018x} child MISS (fallback parent shapes)",
7499 sentinel_code, body,
7500 );
7501 }
7502 // Cache miss — fall back to parent
7503 // shapes with the body bits. Best-
7504 // effort; the trace_side_trace_
7505 // shape_mismatch_count records this
7506 // path indirectly (close-handler
7507 // skips wiring on mismatch so we
7508 // shouldn't reach here when shape
7509 // gate held).
7510 (
7511 per_exit_inline.clone(),
7512 per_exit_tags.clone(),
7513 exit_tags.clone(),
7514 exit_hit_counts.clone(),
7515 body,
7516 true,
7517 )
7518 }
7519 } else {
7520 // Dispatcher-level side-trace invocation,
7521 // rather than an IR gate (`load + icmp +
7522 // brif`) at every emit_store_back callsite,
7523 // which measured as a net slowdown. The
7524 // tentative decode + cell load always runs:
7525 // short-circuiting it on a
7526 // `parent_has_side` hint measured slower on
7527 // btrees_d8 and no faster on fib_10.
7528 {
7529 let tentative = crate::jit::trace::decode_exit_shape(
7530 raw_ret,
7531 per_exit_inline,
7532 per_exit_tags,
7533 exit_tags,
7534 );
7535 let tentative_exit_idx = tentative.exit_hit_idx;
7536 let child_invoke = {
7537 let traces = cl.proto.traces.borrow();
7538 traces.iter().find(|t| t.head_pc == head_pc_val).and_then(
7539 |pct| {
7540 let cell =
7541 pct.exit_side_trace_ptrs.get(tentative_exit_idx)?;
7542 let fn_ptr = cell.get();
7543 if fn_ptr.is_null() {
7544 return None;
7545 }
7546 traces
7547 .iter()
7548 .find(|t| {
7549 t.entry as *const () as *const u8 == fn_ptr
7550 })
7551 .map(|child| {
7552 (
7553 child.entry,
7554 child.per_exit_inline.clone(),
7555 child.per_exit_tags.clone(),
7556 child.exit_tags.clone(),
7557 child.exit_hit_counts.clone(),
7558 )
7559 })
7560 },
7561 )
7562 };
7563 if let Some((cent, cpi, cpt, cet, chc)) = child_invoke {
7564 let child_raw_ret = {
7565 // chunk_compiler.enter
7566 // (side-trace entry).
7567 let vm_ptr: *mut Vm = self;
7568 let _guard =
7569 self.jit.chunk_compiler.enter(vm_ptr, Some(cl));
7570 // 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).
7571 unsafe { cent(reg_state.as_mut_ptr()) }
7572 };
7573 (cpi, cpt, cet, chc, child_raw_ret as u64, true)
7574 } else {
7575 (
7576 per_exit_inline.clone(),
7577 per_exit_tags.clone(),
7578 exit_tags.clone(),
7579 exit_hit_counts.clone(),
7580 raw_ret,
7581 false,
7582 )
7583 }
7584 }
7585 };
7586 let decoded = crate::jit::trace::decode_exit_shape(
7587 decode_body,
7588 &decode_inline,
7589 &decode_tags,
7590 &decode_exit_tags,
7591 );
7592 let site_id = decoded.site_id;
7593 let cont_pc = decoded.cont_pc;
7594 let exit_hit_idx = decoded.exit_hit_idx;
7595 let exit_tags_for_pc = decoded.exit_tags_for_pc;
7596 // When a side trace ran (tail-called by the
7597 // parent's code or invoked here), force
7598 // using_global_exit_tags=false so the restore
7599 // loop takes the per-tag slow path:
7600 // `global_tag_res_kind` classifies the parent's
7601 // exit tags, not the child's.
7602 let using_global_exit_tags = if child_ran {
7603 false
7604 } else {
7605 decoded.using_global_exit_tags
7606 };
7607 // Increment the counter (saturate
7608 // at u32::MAX to avoid wrap on long runs).
7609 // Track whether this increment is
7610 // the one that crossed `HOTEXIT_THRESHOLD`
7611 // (transition: previous v < threshold, new v
7612 // == threshold). The side-trace start is
7613 // deferred to just before `continue;` so
7614 // vm.stack and frame.pc are fully restored
7615 // (the snapshot reads post-restore values).
7616 let mut side_trace_should_start = false;
7617 // For side-trace returns the
7618 // counter to bump is the CHILD's (decoded
7619 // shape lookup) — `exit_hit_idx` is into the
7620 // decoded layout, so use the matching
7621 // `decode_hit_counts`. For parent decode
7622 // they're aliased (clone of the parent's
7623 // own Rc).
7624 if let Some(c) = decode_hit_counts.get(exit_hit_idx) {
7625 let v = c.get();
7626 if v < u32::MAX {
7627 c.set(v + 1);
7628 }
7629 if v + 1 == crate::jit::trace::HOTEXIT_THRESHOLD
7630 && self.jit.active_trace.is_none()
7631 && self.jit.trace_enabled
7632 {
7633 side_trace_should_start = true;
7634 }
7635 }
7636 // At an inline cmp@d>0
7637 // side-exit, the helper has pushed N frames on
7638 // top of the trace head's frame and
7639 // `exit_tags_for_pc.len()` covers the full
7640 // window (caller + each inlined frame's
7641 // window). Slots beyond `max_stack` belong to
7642 // an inlined frame: their `Untouched` entries
7643 // default to Nil (no entry-tag fallback —
7644 // marshal-in only captured caller slots) and
7645 // we write to interp stack at `base + i` which
7646 // mirrors `op_offsets`-derived layout.
7647 let slot_count = exit_tags_for_pc.len();
7648 // The helper only extends
7649 // vm.stack up to the deepest pushed frame's
7650 // window, but the exit_tags snapshot covers
7651 // the trace's full `window_size` (which
7652 // includes depth-N+1 scratch slots that the
7653 // trace's IR may have written without a
7654 // matching pushed frame). Extend with Nil so
7655 // the write at the tail doesn't panic; these
7656 // slots get overwritten by the writeback loop
7657 // and won't leak meaningful data past the
7658 // pushed frames' R[0..max_stack) windows.
7659 if self.stack.len() < base_us + slot_count {
7660 self.stack
7661 .resize(base_us + slot_count, crate::runtime::Value::Nil);
7662 }
7663 // Fast-path restore loop. When
7664 // we landed on the global `exit_tags`,
7665 // dispatch on the compile-time
7666 // classification: skip the loop entirely
7667 // for `AllUntouched`, do a tag-free
7668 // `Value::Int(...)` write per slot for
7669 // `AllInt`, otherwise fall through to the
7670 // general match-arm loop. site_id > 0
7671 // (inline frame mat) and per_exit_tags
7672 // hits always take the general path —
7673 // their per-side-exit shapes aren't
7674 // pre-classified yet.
7675 // A generic-for exit whose TForCall wrote the loop
7676 // variables to the stack with tags the trace did not
7677 // compile for: leave those slots as they are.
7678 let keep_tfor =
7679 if decode_body & crate::jit::trace_types::EXIT_KEEP_TFOR_VARS != 0 {
7680 let call = cl.proto.code[cont_pc as usize - 1];
7681 debug_assert!(matches!(call.op(), crate::vm::isa::Op::TForCall));
7682 let first = call.a() as usize + 4;
7683 first..first + call.c() as usize
7684 } else {
7685 0..0
7686 };
7687 let fast_path_taken = if using_global_exit_tags && keep_tfor.is_empty() {
7688 match global_tag_res_kind {
7689 crate::jit::trace::TagResKind::AllUntouched => {
7690 // No-op: vm.stack already
7691 // matches the trace's post-
7692 // entry state for these
7693 // slots (entry values not
7694 // overridden, or already
7695 // spilled by helpers).
7696 true
7697 }
7698 crate::jit::trace::TagResKind::AllInt => {
7699 for i in 0..slot_count {
7700 self.stack[base_us + i] =
7701 crate::runtime::Value::Int(reg_state[i]);
7702 }
7703 true
7704 }
7705 crate::jit::trace::TagResKind::Mixed => false,
7706 }
7707 } else {
7708 false
7709 };
7710 if !fast_path_taken {
7711 for i in 0..slot_count {
7712 if keep_tfor.contains(&i) {
7713 continue;
7714 }
7715 let tag = match exit_tags_for_pc[i] {
7716 crate::jit::trace::ExitTag::Untouched => {
7717 if i < max_stack {
7718 entry_tags[i]
7719 } else {
7720 crate::runtime::value::raw::NIL
7721 }
7722 }
7723 crate::jit::trace::ExitTag::Int => {
7724 crate::runtime::value::raw::INT
7725 }
7726 crate::jit::trace::ExitTag::Float => {
7727 crate::runtime::value::raw::FLOAT
7728 }
7729 crate::jit::trace::ExitTag::Table => {
7730 crate::runtime::value::raw::TABLE
7731 }
7732 crate::jit::trace::ExitTag::Closure => {
7733 crate::runtime::value::raw::CLOSURE
7734 }
7735 // Trace actively wrote Nil
7736 // to this slot (e.g. via Op::LoadNil).
7737 // Restore as Nil regardless of the entry
7738 // tag, since the i64 payload is 0 and
7739 // packing as the entry tag (e.g. INT)
7740 // would mis-type the slot.
7741 crate::jit::trace::ExitTag::Nil => {
7742 crate::runtime::value::raw::NIL
7743 }
7744 // Trace wrote a Str ptr
7745 // to this slot (LoadK Str / Move from
7746 // Str / Concat result). Restore as
7747 // Value::Str with raw bits round-
7748 // tripped.
7749 crate::jit::trace::ExitTag::Str => {
7750 crate::runtime::value::raw::STR
7751 }
7752 };
7753 // SAFETY: tag is from a verified slot
7754 // (entry validated above) or pinned by
7755 // the exit-tag analysis to INT/TABLE.
7756 // The raw payload sits in reg_state[i].
7757 // Stack was extended by the materialize
7758 // helper for inline frames.
7759 // 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).
7760 self.stack[base_us + i] = unsafe {
7761 Value::pack(
7762 tag,
7763 crate::runtime::value::RawVal {
7764 zero: reg_state[i] as u64,
7765 },
7766 )
7767 };
7768 }
7769 }
7770 // For non-inline exits the
7771 // helper was never called (no metas chain for
7772 // this cont_pc), so `frames.last()` is the
7773 // trace head's frame and we set its pc to
7774 // cont_pc as before. For inline exits the
7775 // helper baked the side-exit PC into the
7776 // innermost frame's `pc` at push time
7777 // (chain.last().pc was overridden at emit),
7778 // so this assignment to `frames.last_mut().pc
7779 // = cont_pc` is a redundant-but-correct
7780 // confirmation.
7781 let _ = &per_exit_inline; // hold the Rc alive across dispatch
7782 // For inline side-exits the
7783 // helper has pushed N frames on top. The trace
7784 // head frame is at `pre_frames - 1`; set its
7785 // pc to `head_resume_pc` so when the chain
7786 // eventually pops back to it, interp resumes
7787 // PAST the trace's depth-0 Op::Call instead of
7788 // restarting from `head_pc` and re-triggering
7789 // dispatch (infinite loop). The innermost
7790 // (helper-pushed) frame already has its pc
7791 // baked in at compile time, but we still
7792 // assign `cont_pc` below for parity with the
7793 // non-inline path (no-op).
7794 if site_id > 0 {
7795 let idx = (site_id - 1) as usize;
7796 let head_resume_pc = decode_inline[idx].head_resume_pc;
7797 if pre_frames > 0
7798 && let CallFrame::Lua(f) = &mut self.frames[pre_frames - 1]
7799 {
7800 f.pc = head_resume_pc;
7801 }
7802 }
7803 let frames_len_now = self.frames.len();
7804 // 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).
7805 match unsafe { self.frames.last_mut().unwrap_unchecked() } {
7806 CallFrame::Lua(fmut) => {
7807 if crate::jit::trace::v2c_probe_enabled() {
7808 eprintln!(
7809 "[v2c-set-pc] from_side={} sentinel_or_raw={:#018x} prev_pc={} new_cont_pc={} site_id={} frames.len={} pre_frames={} max_stack={}",
7810 from_side_trace,
7811 raw_ret,
7812 fmut.pc,
7813 cont_pc,
7814 site_id,
7815 frames_len_now,
7816 pre_frames,
7817 max_stack,
7818 );
7819 }
7820 fmut.pc = cont_pc;
7821 }
7822 _ => unreachable!("Cont frame at trace dispatch"),
7823 }
7824 // Deferred side-trace start. The
7825 // increment block above flagged this exit's
7826 // hit count crossing HOTEXIT_THRESHOLD; now
7827 // that vm.stack is restored and frame.pc is
7828 // settled, snapshot entry_tags from the
7829 // resume frame's window and create the
7830 // recorder. The recorder's first push fires
7831 // on the next interp iteration at cont_pc.
7832 //
7833 // `head_proto` for the side trace = cl.proto
7834 // (trace JIT only inlines self-recursive
7835 // calls today, so cont_pc always lands in
7836 // the same proto as the parent). Frame base
7837 // is the resume frame (top of `self.frames`
7838 // — inline-pushed frames moved this).
7839 if side_trace_should_start {
7840 let (resume_base, resume_proto) = match self.frames.last() {
7841 Some(CallFrame::Lua(f)) => (f.base as usize, f.closure.proto),
7842 _ => (base_us, cl.proto),
7843 };
7844 let resume_max_stack = resume_proto.max_stack as usize;
7845 let mut side_entry_tags: Vec<u8> = Vec::with_capacity(resume_max_stack);
7846 // Extend stack if cont_pc's frame window
7847 // overhangs the current stack len (rare,
7848 // but inline-pushed frame stack writes
7849 // only covered the trace's writeback).
7850 if self.stack.len() < resume_base + resume_max_stack {
7851 self.stack.resize(
7852 resume_base + resume_max_stack,
7853 crate::runtime::Value::Nil,
7854 );
7855 }
7856 for i in 0..resume_max_stack {
7857 let (tag, _) = self.stack[resume_base + i].unpack();
7858 side_entry_tags.push(tag);
7859 }
7860 self.jit.active_trace =
7861 Some(Box::new(crate::jit::trace::TraceRecord::start_side_trace(
7862 resume_proto,
7863 cont_pc,
7864 side_entry_tags,
7865 cl.proto,
7866 head_pc_val,
7867 exit_hit_idx,
7868 )));
7869 self.jit.recording_frame_base = self.frames.len() - 1;
7870 self.jit.counters.side_trace_started += 1;
7871 }
7872 // Put the dispatch buffers back
7873 // before the `continue;` so the next
7874 // dispatch picks up the same allocation.
7875 self.jit.reg_state_buf = reg_state;
7876 self.jit.entry_tags_buf = entry_tags;
7877 continue;
7878 }
7879 }
7880 // !dispatch_ok / deopt path / non-cont
7881 // exit also restore the buffers before falling
7882 // through to the interp.
7883 self.jit.reg_state_buf = reg_state;
7884 self.jit.entry_tags_buf = entry_tags;
7885 }
7886
7887 // PUC `vmfetch` increments savedpc BEFORE firing traceexec, so
7888 // hook code that consults `currentpc = savedpc - 1` lands on the
7889 // instruction now executing. luna mirrors that by advancing
7890 // `f.pc` to `pc + 1` before the hook block — local_at /
7891 // getinfo / line attribution all read f.pc, and the existing
7892 // `pc - 1` convention in those helpers then yields the current
7893 // instruction's pc (db.lua :696: local `A` visible at the
7894 // chunk's return line once OP_CLOSURE has advanced pc).
7895 //
7896 // Inline `top_frame_mut` for the hot path: top is guaranteed Lua
7897 // (cont frames drained above) so the and_then/Option layers are
7898 // dead weight.
7899 // 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).
7900 match unsafe { self.frames.last_mut().unwrap_unchecked() } {
7901 CallFrame::Lua(fmut) => fmut.pc = pc + 1,
7902 _ => unreachable!("Cont frame at pc bump"),
7903 }
7904
7905 // count + line hooks (PUC traceexec): before executing the
7906 // instruction. Skipped while the hook itself runs.
7907 // (Parens here are load-bearing — without them `&&` binds tighter
7908 // than `||` and the `!in_hook` guard only gates the rust-hook arm,
7909 // letting a Lua line hook recurse into itself → stack overflow
7910 // on db.lua line-hook assertions. Matches the `hook_call_with` /
7911 // `hook_return` predicate shape at lines 2245 / 2279 / 2294 / 4023.)
7912 if !self.in_hook && (self.hook.func.is_some() || self.hook.rust_func.is_some()) {
7913 let lines = &cl.proto.lines;
7914 let cur_line = if lines.is_empty() {
7915 None
7916 } else {
7917 Some(lines[(pc as usize).min(lines.len() - 1)] as i64)
7918 };
7919 // count hook: fire every `count_base` instructions
7920 if self.hook.count {
7921 self.hook.count_left -= 1;
7922 if self.hook.count_left <= 0 {
7923 self.hook.count_left = self.hook.count_base;
7924 // hooked function is the running Lua frame: its frame
7925 // is on the stack, so no synthetic C level is needed.
7926 self.run_hook(b"count", cur_line, false)?;
7927 }
7928 }
7929 // line hook: fire on a fresh frame, a backward jump (loop), or a
7930 // change of source line.
7931 if self.hook.line {
7932 if lines.is_empty() {
7933 // PUC: a stripped chunk has no line info, so
7934 // `getfuncline` returns -1. The line hook still fires
7935 // on the first instruction of the new frame (where
7936 // `npci <= oldpc` holds at oldpc=0), with the line
7937 // pushed as `nil` instead of an integer (db.lua :1030
7938 // "hook called without debug info for 1st instruction").
7939 if oldpc == u32::MAX {
7940 self.run_hook(b"line", None, false)?;
7941 self.top_frame_mut().hook_oldpc = pc;
7942 }
7943 } else {
7944 let newline = lines[(pc as usize).min(lines.len() - 1)];
7945 // PUC `traceexec`: fire on frame entry (`oldpc == MAX`),
7946 // on a backward jump (`pc < oldpc` — strict; an equal pc
7947 // would re-fire the install-site after `oldpc = pc`),
7948 // or when the source line changes.
7949 let fire = oldpc == u32::MAX
7950 || pc < oldpc
7951 || newline != lines[(oldpc as usize).min(lines.len() - 1)];
7952 if fire {
7953 self.run_hook(b"line", Some(newline as i64), false)?;
7954 }
7955 self.top_frame_mut().hook_oldpc = pc;
7956 }
7957 }
7958 }
7959
7960 match inst.op() {
7961 Op::Move => {
7962 let v = self.r(base, inst.b());
7963 self.set_r(base, inst.a(), v);
7964 }
7965 Op::LoadI => self.set_r(base, inst.a(), Value::Int(inst.sbx() as i64)),
7966 Op::LoadF => self.set_r(base, inst.a(), Value::Float(inst.sbx() as f64)),
7967 Op::LoadK => {
7968 let v = cl.proto.consts[inst.bx() as usize];
7969 self.set_r(base, inst.a(), v);
7970 }
7971 Op::LoadKx => {
7972 let extra = cl.proto.code[self.pc_of_top() as usize];
7973 self.bump_pc();
7974 let v = cl.proto.consts[extra.ax() as usize];
7975 self.set_r(base, inst.a(), v);
7976 }
7977 Op::LoadFalse => self.set_r(base, inst.a(), Value::Bool(false)),
7978 Op::LFalseSkip => {
7979 self.set_r(base, inst.a(), Value::Bool(false));
7980 self.bump_pc();
7981 }
7982 Op::LoadTrue => self.set_r(base, inst.a(), Value::Bool(true)),
7983 Op::LoadNil => {
7984 let a = inst.a();
7985 for i in 0..=inst.b() {
7986 self.set_r(base, a + i, Value::Nil);
7987 }
7988 }
7989 Op::GetUpval => {
7990 let v = self.upval_get(cl, inst.b());
7991 self.set_r(base, inst.a(), v);
7992 }
7993 Op::SetUpval => {
7994 let v = self.r(base, inst.a());
7995 self.upval_set(cl, inst.b(), v);
7996 }
7997 Op::GetTabUp => {
7998 let t = self.upval_get(cl, inst.b());
7999 let key = cl.proto.consts[inst.c() as usize];
8000 self.op_index(t, key, base + inst.a())?;
8001 }
8002 Op::GetTable => {
8003 let t = self.r(base, inst.b());
8004 let key = self.r(base, inst.c());
8005 self.op_index(t, key, base + inst.a())?;
8006 }
8007 Op::GetI => {
8008 let t = self.r(base, inst.b());
8009 self.op_index(t, Value::Int(inst.c() as i64), base + inst.a())?;
8010 }
8011 Op::GetField => {
8012 let t = self.r(base, inst.b());
8013 let key = cl.proto.consts[inst.c() as usize];
8014 // Fast path: known-Str const key + no
8015 // metatable on the table → skip `op_index` /
8016 // `index_step`'s MAX_TAG_LOOP setup and the outer
8017 // `Value` match. Falls through to the slow path
8018 // when either invariant breaks (`__index`
8019 // metamethods, non-Table receivers, non-Str keys).
8020 if let Value::Table(tb) = t
8021 && tb.metatable().is_none()
8022 && let Value::Str(s) = key
8023 {
8024 let v = tb.get_str(s);
8025 self.stack[(base + inst.a()) as usize] = v;
8026 } else {
8027 self.op_index(t, key, base + inst.a())?;
8028 }
8029 }
8030 Op::SetTabUp => {
8031 let t = self.upval_get(cl, inst.a());
8032 let key = cl.proto.consts[inst.b() as usize];
8033 let v = self.r(base, inst.c());
8034 self.op_newindex(t, key, v)?;
8035 }
8036 Op::SetTable => {
8037 let t = self.r(base, inst.a());
8038 let key = self.r(base, inst.b());
8039 let v = self.r(base, inst.c());
8040 self.op_newindex(t, key, v)?;
8041 }
8042 Op::SetI => {
8043 let t = self.r(base, inst.a());
8044 let v = self.r(base, inst.c());
8045 self.op_newindex(t, Value::Int(inst.b() as i64), v)?;
8046 }
8047 Op::SetField => {
8048 let t = self.r(base, inst.a());
8049 let key = cl.proto.consts[inst.b() as usize];
8050 let v = self.r(base, inst.c());
8051 self.op_newindex(t, key, v)?;
8052 }
8053 Op::NewTable => {
8054 let t = self.heap.new_table();
8055 self.set_r(base, inst.a(), Value::Table(t));
8056 self.maybe_collect_garbage(base + inst.a() + 1);
8057 }
8058 Op::SetList => {
8059 let a = inst.a();
8060 let abs_a = base + a;
8061 // only `debug.setlocal` or crafted bytecode can put a
8062 // non-table here; PUC crashes, luna raises
8063 let t = match self.r(base, a) {
8064 Value::Table(t) => t,
8065 v => return Err(self.type_err("index", v)),
8066 };
8067 let n = if inst.b() == 0 {
8068 self.top - (abs_a + 1)
8069 } else {
8070 inst.b()
8071 };
8072 let offset = if inst.k() {
8073 let extra = cl.proto.code[self.pc_of_top() as usize];
8074 self.bump_pc();
8075 extra.ax() as i64
8076 } else {
8077 inst.c() as i64
8078 };
8079 for i in 1..=n {
8080 let v = self.r(base, a + i);
8081 // 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).
8082 if let Err(TableError::Overflow) =
8083 unsafe { t.as_mut() }.set_int(&mut self.heap, offset + i as i64, v)
8084 {
8085 return Err(self.rt_err("table overflow"));
8086 }
8087 }
8088 // one barrier_back covers every store this op did — PUC's
8089 // `luaC_barrierback_` once-per-table optimisation
8090 self.heap
8091 .barrier_back(t.as_ptr() as *mut crate::runtime::heap::GcHeader);
8092 // the element temps above the table are now consumed
8093 self.maybe_collect_garbage(base + a + 1);
8094 }
8095 Op::SelfOp => {
8096 let o = self.r(base, inst.b());
8097 self.set_r(base, inst.a() + 1, o);
8098 // PUC OP_SELF's C is a constant index when the k-flag is
8099 // set; otherwise it points to a register that holds the
8100 // (constant-loaded) key. luna's compiler falls back to the
8101 // register form when the constant index exceeds OP_SELF's
8102 // 8-bit C field (5.1 big.lua's `a:findfield(...)` against
8103 // a table with 250+ string keys, where "findfield" lands
8104 // past const #255). The exec must honour the same split.
8105 let key = if inst.k() {
8106 cl.proto.consts[inst.c() as usize]
8107 } else {
8108 self.r(base, inst.c())
8109 };
8110 self.op_index(o, key, base + inst.a())?;
8111 }
8112 Op::Add => self.arith_rr(inst, base, ArithOp::Add)?,
8113 Op::Sub => self.arith_rr(inst, base, ArithOp::Sub)?,
8114 Op::Mul => self.arith_rr(inst, base, ArithOp::Mul)?,
8115 Op::Mod => self.arith_rr(inst, base, ArithOp::Mod)?,
8116 Op::Pow => self.arith_rr(inst, base, ArithOp::Pow)?,
8117 Op::Div => self.arith_rr(inst, base, ArithOp::Div)?,
8118 Op::IDiv => self.arith_rr(inst, base, ArithOp::IDiv)?,
8119 Op::BAnd => self.arith_rr(inst, base, ArithOp::BAnd)?,
8120 Op::BOr => self.arith_rr(inst, base, ArithOp::BOr)?,
8121 Op::BXor => self.arith_rr(inst, base, ArithOp::BXor)?,
8122 Op::Shl => self.arith_rr(inst, base, ArithOp::Shl)?,
8123 Op::Shr => self.arith_rr(inst, base, ArithOp::Shr)?,
8124 Op::Unm => {
8125 let v = self.r(base, inst.b());
8126 match self.unary_operand(v) {
8127 Some(Num::Int(i)) => {
8128 self.set_r(base, inst.a(), Value::Int(i.wrapping_neg()))
8129 }
8130 Some(Num::Float(f)) => self.set_r(base, inst.a(), Value::Float(-f)),
8131 None => {
8132 let mm = self.get_mm(v, Mm::Unm);
8133 if mm.is_nil() {
8134 return Err(self.type_err("perform arithmetic on", v));
8135 }
8136 let dst = base + inst.a();
8137 self.begin_meta_call(mm, &[v, v], MetaAction::Store { dst }, "unm")?;
8138 }
8139 }
8140 }
8141 Op::BNot => {
8142 let v = self.r(base, inst.b());
8143 match self.arith_operand()(v) {
8144 Some(n) => {
8145 let Some(i) = int_of(n) else {
8146 return Err(self.no_int_rep_err());
8147 };
8148 self.set_r(base, inst.a(), Value::Int(!i));
8149 }
8150 None => {
8151 let mm = self.get_mm(v, Mm::BNot);
8152 if mm.is_nil() {
8153 return Err(self.type_err("perform bitwise operation on", v));
8154 }
8155 let dst = base + inst.a();
8156 self.begin_meta_call(mm, &[v, v], MetaAction::Store { dst }, "bnot")?;
8157 }
8158 }
8159 }
8160 Op::Not => {
8161 let v = self.r(base, inst.b());
8162 self.set_r(base, inst.a(), Value::Bool(!v.truthy()));
8163 }
8164 Op::Len => {
8165 let v = self.r(base, inst.b());
8166 match self.len_step(v)? {
8167 MmOut::Done(r) => self.set_r(base, inst.a(), r),
8168 MmOut::Mm { func, recv } => {
8169 let dst = base + inst.a();
8170 self.begin_meta_call(
8171 func,
8172 &[recv, recv],
8173 MetaAction::Store { dst },
8174 "len",
8175 )?;
8176 }
8177 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from len_step"),
8178 }
8179 }
8180 Op::Concat => {
8181 // right-associative fold over operands at base+a .. base+a+n,
8182 // in place on the stack so a yielding __concat can suspend.
8183 let a = inst.a();
8184 let n = inst.b();
8185 self.top = base + a + n;
8186 self.concat_run(base + a)?;
8187 }
8188 Op::Close => {
8189 // Yieldable: drive __close handlers through the
8190 // interpreter loop so a coroutine.yield() inside a
8191 // handler suspends cleanly (locals.lua block-end yield).
8192 // `drive_close` parks the handler call at `self.top`, so
8193 // raise `top` past this frame's full register window
8194 // first — a goto out of a nested for-loop can fire
8195 // OP_Close while `self.top` still sits at the inner
8196 // body's working top, which would let `push_frame`'s
8197 // wipe clobber the outer tbc slot before it could be
8198 // closed (locals.lua:1219 nested-for goto regression).
8199 self.top = self.top.max(base + cl.proto.max_stack as u32);
8200 let _ =
8201 self.begin_close(base + inst.a(), None, AfterClose::Block, entry_depth)?;
8202 }
8203 Op::Tbc => {
8204 self.register_tbc(base + inst.a())?;
8205 }
8206 Op::Jmp => {
8207 let off = inst.sj();
8208 // Trace JIT back-edge counter. A negative
8209 // jump offset is a loop back-edge (the only canonical
8210 // backward jumps the compiler emits — `while`, `for`,
8211 // `repeat`). Tick the per-Proto counter and, once it
8212 // exceeds the threshold, start a trace recording. The
8213 // whole block is gated on `trace_jit_enabled` so
8214 // existing benches see one branch-not-taken and no
8215 // counter writes.
8216 if self.jit.trace_enabled && off < 0 {
8217 let proto = cl.proto;
8218 let c = proto.trace_hot_count.get();
8219 if c < u32::MAX / 2 {
8220 proto.trace_hot_count.set(c + 1);
8221 }
8222 // Relaxed back-edge trigger:
8223 // `c >= THRESHOLD` (not `c == THRESHOLD`) so
8224 // a missed crossing (active_trace busy with
8225 // a call-trigger, or the recorder slot
8226 // happened to be in use) doesn't permanently
8227 // lock this back-edge target out. The
8228 // `already_cached` short-circuit prevents
8229 // duplicate recordings: once a trace is
8230 // cached for this target, subsequent
8231 // crossings skip the start. This pairs with
8232 // the discard-on-partial-coverage close
8233 // handling — when a short call-trigger is
8234 // discarded, the back-edge can still find an
8235 // open slot at the next iteration.
8236 let target_pc = (pc as i32 + 1 + off as i32).max(0) as u32;
8237 // Gave-up short-circuit. Skip
8238 // the RefCell borrow + scan when the
8239 // discard cap force-compiled a partial
8240 // trace on this Proto.
8241 let back_edge_already_cached = if proto.trace_gave_up.get() {
8242 true
8243 } else {
8244 proto.traces.borrow().iter().any(|t| t.head_pc == target_pc)
8245 || trace_head_abandoned(proto, target_pc)
8246 };
8247 if c >= self.jit.trace_hot_threshold
8248 && self.jit.active_trace.is_none()
8249 && !back_edge_already_cached
8250 {
8251 // Back-edge target = pc after `add_pc(off)`,
8252 // i.e. current `pc + 1 + off` (the dispatch
8253 // loop has already advanced f.pc to pc+1).
8254 let target = (pc as i32 + 1 + off as i32).max(0) as u32;
8255 // Snapshot per-slot Value tag at trace
8256 // entry so the lowerer's kind tracker
8257 // knows which arith path to lower
8258 // (iadd vs fadd, etc.).
8259 let max_stack = cl.proto.max_stack as usize;
8260 let base_us = base as usize;
8261 let mut entry_tags = Vec::with_capacity(max_stack);
8262 for i in 0..max_stack {
8263 let (tag, _) = self.stack[base_us + i].unpack();
8264 entry_tags.push(tag);
8265 }
8266 self.jit.active_trace =
8267 Some(Box::new(crate::jit::trace::TraceRecord::start(
8268 cl.proto, target, entry_tags, false,
8269 )));
8270 // Record the frame the trace
8271 // started in. `self.frames.len() - 1`
8272 // since we're inside the currently-running
8273 // Lua frame's dispatch.
8274 self.jit.recording_frame_base = self.frames.len() - 1;
8275 }
8276 }
8277 self.add_pc(off);
8278 }
8279 Op::Eq => {
8280 let l = self.r(base, inst.a());
8281 let r = self.r(base, inst.b());
8282 if let (Value::Int(a), Value::Int(b)) = (l, r) {
8283 if (a == b) != inst.k() {
8284 self.bump_pc();
8285 }
8286 } else {
8287 let step = self.eq_step(l, r);
8288 self.op_compare(step, l, r, inst.k(), "eq")?;
8289 }
8290 }
8291 Op::EqK => {
8292 let l = self.r(base, inst.a());
8293 let r = cl.proto.consts[inst.b() as usize];
8294 if let (Value::Int(a), Value::Int(b)) = (l, r) {
8295 if (a == b) != inst.k() {
8296 self.bump_pc();
8297 }
8298 } else {
8299 let step = self.eq_step(l, r);
8300 self.op_compare(step, l, r, inst.k(), "eq")?;
8301 }
8302 }
8303 Op::Lt => {
8304 let l = self.r(base, inst.a());
8305 let r = self.r(base, inst.b());
8306 // hot path: Int < Int — drops the MmOut + op_compare match
8307 if let (Value::Int(a), Value::Int(b)) = (l, r) {
8308 if (a < b) != inst.k() {
8309 self.bump_pc();
8310 }
8311 } else {
8312 let step = self.less_step(l, r, false)?;
8313 self.op_compare(step, l, r, inst.k(), "lt")?;
8314 }
8315 }
8316 Op::Le => {
8317 let l = self.r(base, inst.a());
8318 let r = self.r(base, inst.b());
8319 if let (Value::Int(a), Value::Int(b)) = (l, r) {
8320 if (a <= b) != inst.k() {
8321 self.bump_pc();
8322 }
8323 } else {
8324 let step = self.less_step(l, r, true)?;
8325 self.op_compare(step, l, r, inst.k(), "le")?;
8326 }
8327 }
8328 Op::Test => {
8329 let cond = self.r(base, inst.a()).truthy();
8330 self.cond_skip(cond, inst.k());
8331 }
8332 Op::TestSet => {
8333 let v = self.r(base, inst.b());
8334 if v.truthy() == inst.k() {
8335 self.set_r(base, inst.a(), v);
8336 } else {
8337 self.bump_pc();
8338 }
8339 }
8340 Op::Call => {
8341 let abs = base + inst.a();
8342 let nargs = if inst.b() == 0 {
8343 None
8344 } else {
8345 Some(inst.b() - 1)
8346 };
8347 let wanted = inst.c() as i32 - 1;
8348 self.begin_call(abs, nargs, wanted, false)?;
8349 }
8350 Op::TailCall => {
8351 let fr = *self.top_frame();
8352 let abs = base + inst.a();
8353 let mut nargs = if inst.b() == 0 {
8354 self.top - (abs + 1)
8355 } else {
8356 inst.b() - 1
8357 };
8358 // A tail call pops this frame before begin_call, so a
8359 // non-callable target would lose its name/position. Report
8360 // it now (PUC reads funcname from the still-current ci),
8361 // while the frame is intact, for "(field 'x')"-style info.
8362 let mut func = self.stack[abs as usize];
8363 if !matches!(func, Value::Closure(_) | Value::Native(_))
8364 && self.get_mm(func, Mm::Call).is_nil()
8365 {
8366 return Err(self.call_err(func));
8367 }
8368 // PUC `luaD_pretailcall` resolves a chain of `__call`
8369 // metamethods *in place* before deciding whether to
8370 // collapse this frame. Without that, each __call hop
8371 // would push a fresh Lua frame and a 10000-deep
8372 // tail-recursion through a 100-deep __call chain
8373 // (5.4 calls.lua :172) blows up. Mirror the PUC loop:
8374 // shift args right, install the handler at `abs`, retry.
8375 // Chain depth limit matches the call-site `begin_call`
8376 // version cap (5.5 calls.lua :223 — 15 max, then "too
8377 // long"; 16th wrap fails the call). An infinite
8378 // self-referential `__call` would otherwise spin.
8379 let chain_cap = if self.version >= LuaVersion::Lua55 {
8380 15
8381 } else {
8382 MAX_CCMT
8383 };
8384 let mut chain = 0u32;
8385 while !matches!(func, Value::Closure(_) | Value::Native(_)) {
8386 let mm = self.get_mm(func, Mm::Call);
8387 if mm.is_nil() || self.call_mm_unusable(mm) {
8388 return Err(self.call_err(func));
8389 }
8390 chain += 1;
8391 if chain > chain_cap {
8392 return Err(self.rt_err("'__call' chain too long"));
8393 }
8394 let end = (abs + 1 + nargs) as usize;
8395 if self.stack.len() < end + 1 {
8396 self.stack.resize(end + 1, Value::Nil);
8397 }
8398 for i in (0..=nargs).rev() {
8399 self.stack[(abs + 1 + i) as usize] = self.stack[(abs + i) as usize];
8400 }
8401 self.stack[abs as usize] = mm;
8402 nargs += 1;
8403 self.top = abs + 1 + nargs;
8404 func = mm;
8405 }
8406 // PUC's tail-call collapse is Lua→Lua only. A tail call to
8407 // a C function runs the C function under the *current* Lua
8408 // activation (no frame fold — a C frame has nothing to
8409 // collapse into); after the C function returns, the
8410 // calling Lua function returns those results normally.
8411 // Mirror that: keep our Lua frame on the stack, call the
8412 // target through `begin_call(abs, …)` as a regular call,
8413 // and let the fallback `Op::Return` that the compiler
8414 // emits right after `Op::TailCall` forward the results.
8415 // 5.1 closure.lua :177's `return getfenv()` from inside
8416 // foo needs level 1 to resolve to foo, not to the
8417 // thread's globals fallback that happens when no Lua
8418 // frame is on the stack.
8419 let lua_target = matches!(func, Value::Closure(_));
8420 if lua_target {
8421 self.close_slots(fr.base, None)?;
8422 for i in 0..=nargs {
8423 self.stack[(fr.func_slot + i) as usize] =
8424 self.stack[(abs + i) as usize];
8425 }
8426 // Clear the slot range that's now
8427 // stranded by the tail-call collapse. The args
8428 // were copied to `[fr.func_slot..fr.func_slot+
8429 // nargs+1)`; the source slots `[abs..abs+
8430 // nargs+1)` still hold the same `Value::Closure
8431 // / Value::Str / ...` entries, but they're past
8432 // the new call's window. Without this clear, a
8433 // later GC with wider gc_top would mark stale
8434 // pointers there (same hazard the
8435 // finish_results slot-clear closes for the
8436 // Op::Return path).
8437 let new_top_lower_bound = fr.func_slot + nargs + 1;
8438 let prev_top = (self.top as usize).min(self.stack.len());
8439 if (new_top_lower_bound as usize) < prev_top {
8440 for slot in &mut self.stack[new_top_lower_bound as usize..prev_top] {
8441 *slot = Value::Nil;
8442 }
8443 }
8444 // PUC `CIST_TAIL`: the new Lua activation inherits
8445 // the popped frame's tailcalls count plus one for
8446 // this collapse. 5.1 db.lua :372 hammers 30000
8447 // recursive tail calls and expects to see the
8448 // synthetic tail level for every one of them.
8449 self.pending_tailcalls = fr.tailcalls.saturating_add(1);
8450 self.pending_ccmt = self.frame_ccmt[self.frames.len() - 1];
8451 frames_pop_sync(&mut self.frames, &mut self.frames_top);
8452 if !self.begin_call(fr.func_slot, Some(nargs), fr.nresults, false)?
8453 && self.frames.len() < entry_depth
8454 {
8455 // a native completed what was this function's result
8456 return Ok(self.take_results(fr.func_slot));
8457 }
8458 } else {
8459 // Native (or __call-bearing) target: regular call. The
8460 // results land at `abs..self.top` and the next op (the
8461 // fallback `Op::Return`) forwards them. `wanted = -1`
8462 // because the caller will multret them through Return.
8463 // PUC's precallC gives the C call this tail call's own
8464 // `__call` count (5.5 extraargs); the chain was already
8465 // resolved above, so hand it over.
8466 self.pending_ccmt = chain as u8;
8467 self.begin_call(abs, Some(nargs), -1, false)?;
8468 }
8469 }
8470 Op::Return | Op::Return0 | Op::Return1 => {
8471 let (abs_a, nret) = match inst.op() {
8472 Op::Return0 => (base, 0),
8473 Op::Return1 => (base + inst.a(), 1),
8474 _ => {
8475 let abs_a = base + inst.a();
8476 let nret = if inst.b() == 0 {
8477 self.top - abs_a
8478 } else {
8479 inst.b() - 1
8480 };
8481 (abs_a, nret)
8482 }
8483 };
8484 // close before moving results: __close handlers run above
8485 // the stack top, so the result region [abs_a..abs_a+nret)
8486 // stays intact across any yields the close performs.
8487 // Fixed-count returns may leave `self.top` below the last
8488 // result slot (the compiler does not always re-bump it);
8489 // raise it past the result region so `drive_close` parks
8490 // the handler call *above* — landing at `self.top` would
8491 // otherwise clobber a result with the handler closure.
8492 self.top = self.top.max(abs_a + nret);
8493 if let Some(vals) = self.begin_close(
8494 base,
8495 None,
8496 AfterClose::Return {
8497 abs_a,
8498 nret,
8499 from_native: false,
8500 },
8501 entry_depth,
8502 )? {
8503 return Ok(vals);
8504 }
8505 }
8506 Op::ForPrep => self.for_prep(inst, base)?,
8507 Op::ForLoop => {
8508 // Trace JIT back-edge counter on the
8509 // numeric-for back-edge. ForLoop is always at
8510 // a back-edge position (when it continues);
8511 // for the trace recorder we treat it as the
8512 // close-detection equivalent of `Op::Jmp` with
8513 // negative offset. Counter only ticks when the
8514 // back-edge will actually fire (count > 0 in
8515 // the 5.4+ Int form, comparable predicates in
8516 // pre-5.3 / Float). The cheap check up front
8517 // matches the for_loop helper's branch.
8518 if self.jit.trace_enabled {
8519 let a = inst.a();
8520 let pre53 = self.version() <= LuaVersion::Lua53;
8521 let take_back_edge =
8522 match (self.r(base, a), self.r(base, a + 1), self.r(base, a + 2)) {
8523 (Value::Int(_), Value::Int(count), Value::Int(_)) if !pre53 => {
8524 count != 0
8525 }
8526 (Value::Int(cur), Value::Int(lim), Value::Int(st)) if pre53 => {
8527 let next = cur.wrapping_add(st);
8528 if st > 0 { next <= lim } else { next >= lim }
8529 }
8530 (Value::Float(cur), Value::Float(lim), Value::Float(st)) => {
8531 let next = cur + st;
8532 if st > 0.0 { next <= lim } else { next >= lim }
8533 }
8534 _ => false,
8535 };
8536 if take_back_edge {
8537 let proto = cl.proto;
8538 let c = proto.trace_hot_count.get();
8539 if c < u32::MAX / 2 {
8540 proto.trace_hot_count.set(c + 1);
8541 }
8542 if c == self.jit.trace_hot_threshold && self.jit.active_trace.is_none()
8543 {
8544 // ForLoop's back-edge target = pc
8545 // after `add_pc(-bx)` runs from the
8546 // already-bumped f.pc (= pc + 1).
8547 // So target = (pc + 1) - bx.
8548 let target = (pc as i32 + 1 - inst.bx() as i32).max(0) as u32;
8549 let max_stack = cl.proto.max_stack as usize;
8550 let base_us = base as usize;
8551 let mut entry_tags = Vec::with_capacity(max_stack);
8552 for i in 0..max_stack {
8553 let (tag, _) = self.stack[base_us + i].unpack();
8554 entry_tags.push(tag);
8555 }
8556 self.jit.active_trace =
8557 Some(Box::new(crate::jit::trace::TraceRecord::start(
8558 cl.proto, target, entry_tags, false,
8559 )));
8560 // Record the frame the trace
8561 // started in. The currently-running
8562 // Lua frame is at len() - 1.
8563 self.jit.recording_frame_base = self.frames.len() - 1;
8564 }
8565 }
8566 }
8567 self.for_loop(inst, base)?;
8568 }
8569 Op::TForPrep => {
8570 // the 4th control slot is the iterator's closing value
8571 self.register_tbc(base + inst.a() + 3)?;
8572 self.add_pc(inst.bx() as i32);
8573 }
8574 Op::TForCall => {
8575 let abs = base + inst.a();
8576 let need = (abs + 7) as usize;
8577 if self.stack.len() < need {
8578 self.stack.resize(need, Value::Nil);
8579 }
8580 self.stack[(abs + 4) as usize] = self.stack[abs as usize];
8581 self.stack[(abs + 5) as usize] = self.stack[(abs + 1) as usize];
8582 self.stack[(abs + 6) as usize] = self.stack[(abs + 2) as usize];
8583 let nvars = inst.c() as i32;
8584 self.begin_call(abs + 4, Some(2), nvars, false)?;
8585 }
8586 Op::TForLoop => {
8587 let a = inst.a();
8588 let ctrl = self.r(base, a + 4);
8589 if !ctrl.is_nil() {
8590 // Trace JIT back-edge counter on
8591 // generic-for back-edge. TForLoop sits at the
8592 // tail of `for k,v in expr do ... end`; recorder
8593 // treats it as the close-detection equivalent of
8594 // a negative Op::Jmp. Gate on `take_back_edge`
8595 // (= `ctrl != nil`) so empty-iter loops don't
8596 // pollute hot_count.
8597 if self.jit.trace_enabled {
8598 let proto = cl.proto;
8599 let c = proto.trace_hot_count.get();
8600 if c < u32::MAX / 2 {
8601 proto.trace_hot_count.set(c + 1);
8602 }
8603 if c == self.jit.trace_hot_threshold && self.jit.active_trace.is_none()
8604 {
8605 // TForLoop back-edge target = pc after
8606 // `add_pc(-bx)` runs from the already-
8607 // bumped f.pc (= pc + 1). So target =
8608 // (pc + 1) - bx, normally landing on
8609 // body_top (the op right after TForPrep).
8610 let target = (pc as i32 + 1 - inst.bx() as i32).max(0) as u32;
8611 let max_stack = cl.proto.max_stack as usize;
8612 let base_us = base as usize;
8613 let mut entry_tags = Vec::with_capacity(max_stack);
8614 for i in 0..max_stack {
8615 let (tag, _) = self.stack[base_us + i].unpack();
8616 entry_tags.push(tag);
8617 }
8618 // Snapshot the iter
8619 // fn's address if Native, so the
8620 // lowerer can specialise ipairs into
8621 // inline Table aget IR.
8622 let iter_ptr =
8623 if let Value::Native(n) = self.stack[base_us + a as usize] {
8624 Some(n.f as usize)
8625 } else {
8626 None
8627 };
8628 // Snapshot R[A+5]'s
8629 // tag (= current iter's val from
8630 // the just-fired TForCall). The
8631 // inline aget fast_blk emits a
8632 // runtime guard against this tag;
8633 // mixed-tag arrays deopt rather
8634 // than producing garbage pointers
8635 // through the spill path.
8636 let val_slot = base_us + (a as usize) + 5;
8637 let val_tag = if val_slot < self.stack.len() {
8638 Some(self.stack[val_slot].unpack().0)
8639 } else {
8640 None
8641 };
8642 let mut rec = crate::jit::trace::TraceRecord::start(
8643 cl.proto, target, entry_tags, false,
8644 );
8645 rec.tfor_iter_ptr = iter_ptr;
8646 rec.tfor_val_tag = val_tag;
8647 self.jit.active_trace = Some(Box::new(rec));
8648 self.jit.recording_frame_base = self.frames.len() - 1;
8649 }
8650 }
8651 self.set_r(base, a + 2, ctrl);
8652 self.add_pc(-(inst.bx() as i32));
8653 }
8654 }
8655 Op::Closure => {
8656 let proto = cl.proto.protos[inst.bx() as usize];
8657 let n_ups = proto.upvals.len();
8658 // Build upvals on the stack for small
8659 // closures, skipping the per-call Vec/Box alloc
8660 // that closure_alloc's 10k iters pay. INLINE_UPVALS_N
8661 // = 2 covers most Lua source (1 captured local, or
8662 // _ENV + a single capture). Beyond that, fall back
8663 // to a heap Vec.
8664 use crate::runtime::function::INLINE_UPVALS_N;
8665 let mut stack_buf: [std::mem::MaybeUninit<
8666 Gc<crate::runtime::function::Upvalue>,
8667 >; INLINE_UPVALS_N] = [std::mem::MaybeUninit::uninit(); INLINE_UPVALS_N];
8668 let mut heap_buf: Vec<Gc<crate::runtime::function::Upvalue>> = Vec::new();
8669 let use_inline = n_ups <= INLINE_UPVALS_N;
8670 if !use_inline {
8671 heap_buf.reserve_exact(n_ups);
8672 }
8673 for (i, d) in proto.upvals.iter().enumerate() {
8674 let uv = if d.in_stack {
8675 self.find_or_create_upval(base + d.index as u32)
8676 } else {
8677 cl.upvals()[d.index as usize]
8678 };
8679 if use_inline {
8680 stack_buf[i] = std::mem::MaybeUninit::new(uv);
8681 } else {
8682 heap_buf.push(uv);
8683 }
8684 }
8685 // Tiny shim around the two paths so the 5.1 _ENV
8686 // clone + cache check below see one uniform
8687 // `&mut [Gc<Upvalue>]`. The stack_buf slice points
8688 // into the local frame (still valid through the
8689 // rest of this Op::Closure handler).
8690 let ups: &mut [Gc<crate::runtime::function::Upvalue>] = if use_inline {
8691 // SAFETY: the first n_ups slots of stack_buf
8692 // were initialised above; we hand out a slice
8693 // covering exactly them.
8694 unsafe {
8695 std::slice::from_raw_parts_mut(
8696 stack_buf.as_mut_ptr()
8697 as *mut Gc<crate::runtime::function::Upvalue>,
8698 n_ups,
8699 )
8700 }
8701 } else {
8702 &mut heap_buf[..]
8703 };
8704 // PUC 5.1 had per-function environments: every Lua
8705 // function carried its own `env` slot, snapshotted from
8706 // the creating function's env at closure time, so a
8707 // `setfenv` on one closure never bled into a sibling.
8708 // luna models that by giving the 5.1 closure a *fresh*
8709 // closed upvalue for whichever cell holds `_ENV`, seeded
8710 // from the parent's current env value. Only that cell is
8711 // cloned — every other upvalue keeps its open/shared
8712 // identity (so e.g. `local function range(...) ...
8713 // range(...) ... end` still sees its self-reference). 5.2+
8714 // keeps the shared-upval model (and the proto cache that
8715 // depends on it).
8716 let v51 = self.version() <= LuaVersion::Lua51;
8717 if v51 && proto.env_upval_idx != u8::MAX {
8718 let i = proto.env_upval_idx as usize;
8719 let cur = match ups[i].state() {
8720 UpvalState::Open { slot, thread } => self.read_slot(slot, thread),
8721 UpvalState::Closed(v) => v,
8722 };
8723 ups[i] = self.heap.new_upvalue(UpvalState::Closed(cur));
8724 }
8725 let ups_slice: &[Gc<crate::runtime::function::Upvalue>] = ups;
8726 // PUC 5.2+ `getcached`: a Proto remembers its last LClosure
8727 // and reuses it when every fresh-upvalue binding still
8728 // points to the same Upvalue object as the cached one.
8729 // That keeps `function() return outer end` repeated in a
8730 // loop comparing equal across iterations (the captured
8731 // outer is a shared open upvalue), while `function()
8732 // return loop_var end` gets a fresh closure each round
8733 // because the loop var is re-created per iteration. PUC
8734 // 5.1 predated the cache, and the per-closure `_ENV`
8735 // clone above would defeat it anyway, so skip it.
8736 let nc = if v51 {
8737 self.heap.new_closure_inline(proto, ups_slice)
8738 } else {
8739 let cached = proto.cache.get().filter(|c| {
8740 c.upvals().len() == ups_slice.len()
8741 && c.upvals()
8742 .iter()
8743 .zip(ups_slice.iter())
8744 .all(|(a, b)| std::ptr::eq(a.as_ptr(), b.as_ptr()))
8745 });
8746 match cached {
8747 Some(c) => c,
8748 None => {
8749 let n = self.heap.new_closure_inline(proto, ups_slice);
8750 proto.cache.set(Some(n));
8751 n
8752 }
8753 }
8754 };
8755 self.set_r(base, inst.a(), Value::Closure(nc));
8756 self.maybe_collect_garbage(base + inst.a() + 1);
8757 }
8758 Op::Vararg => {
8759 let abs_a = base + inst.a();
8760 let wanted = inst.c() as i32 - 1;
8761 // A materialized named vararg lives in func_slot (its writes
8762 // must be visible to `...`); otherwise spread the extra args
8763 // straight off the stack at func_slot+1 .. +n_varargs.
8764 let vt = match self.stack[func_slot as usize] {
8765 Value::Table(t) => Some(t),
8766 _ => None,
8767 };
8768 let n = match vt {
8769 Some(t) => {
8770 let n_key = Value::Str(self.heap.intern(b"n"));
8771 // PUC getnumargs: a named vararg `t.n` set out of the
8772 // integer range [0, INT_MAX/2] is rejected here
8773 match t.get(n_key) {
8774 Value::Int(n) if (n as u64) <= (i32::MAX as u64 / 2) => n as u32,
8775 _ => return Err(self.rt_err("vararg table has no proper 'n'")),
8776 }
8777 }
8778 None => n_varargs,
8779 };
8780 let count = if wanted < 0 { n } else { wanted as u32 };
8781 // a named vararg's `n` can be set to anything up to
8782 // INT_MAX/2; PUC's `luaD_checkstack` refuses what the
8783 // stack cannot hold
8784 if abs_a + count > MAX_LUA_STACK {
8785 return Err(self.rt_err("stack overflow"));
8786 }
8787 let need = (abs_a + count) as usize;
8788 if self.stack.len() < need {
8789 self.stack.resize(need, Value::Nil);
8790 }
8791 for i in 0..count {
8792 let v = if i >= n {
8793 Value::Nil
8794 } else if let Some(t) = vt {
8795 t.get_int(i as i64 + 1)
8796 } else {
8797 self.stack[(func_slot + 1 + i) as usize]
8798 };
8799 self.stack[(abs_a + i) as usize] = v;
8800 }
8801 if wanted < 0 {
8802 self.top = abs_a + count;
8803 }
8804 }
8805 Op::GetVarg => {
8806 // materialize the vararg table (PUC table.pack shape) from the
8807 // stack varargs — used when the named vararg is written /
8808 // escapes / is `_ENV`. It is kept BOTH in func_slot (so `...`
8809 // sees later writes) and in the local register R[A].
8810 let n = n_varargs;
8811 let t = self.heap.new_table();
8812 {
8813 // 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).
8814 let tm = unsafe { t.as_mut() };
8815 for i in 0..n {
8816 let _ = tm.set_int(
8817 &mut self.heap,
8818 i as i64 + 1,
8819 self.stack[(func_slot + 1 + i) as usize],
8820 );
8821 }
8822 }
8823 let n_key = Value::Str(self.heap.intern(b"n"));
8824 // 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).
8825 unsafe { t.as_mut() }
8826 .set(&mut self.heap, n_key, Value::Int(n as i64))
8827 .expect("'n' is a valid key");
8828 // once-per-table barrier (mirror SETLIST): t is born BLACK
8829 // during Propagate; the bulk inserts above don't barrier.
8830 self.heap
8831 .barrier_back(t.as_ptr() as *mut crate::runtime::heap::GcHeader);
8832 self.stack[func_slot as usize] = Value::Table(t);
8833 self.set_r(base, inst.a(), Value::Table(t));
8834 }
8835 Op::VargIdx => {
8836 // R[A] := vararg[R[C]] without allocating: integer key in
8837 // [1,n] → that vararg, "n" → the count, else nil.
8838 let key = self.r(base, inst.c());
8839 let n = n_varargs;
8840 let v = match key {
8841 Value::Int(k) if k >= 1 && (k as u64) <= n as u64 => {
8842 self.stack[(func_slot + k as u32) as usize]
8843 }
8844 Value::Float(f) if f.fract() == 0.0 && f >= 1.0 && f <= n as f64 => {
8845 self.stack[(func_slot + f as u32) as usize]
8846 }
8847 Value::Str(s) if s.as_bytes() == b"n" => Value::Int(n as i64),
8848 _ => Value::Nil,
8849 };
8850 self.set_r(base, inst.a(), v);
8851 }
8852 Op::ErrNNil => {
8853 let v = self.r(base, inst.a());
8854 if !matches!(v, Value::Nil) {
8855 let bx = inst.bx();
8856 let name = if bx == 0 {
8857 "?".to_string()
8858 } else {
8859 match cl.proto.consts[(bx - 1) as usize] {
8860 Value::Str(s) => String::from_utf8_lossy(s.as_bytes()).into_owned(),
8861 _ => "?".to_string(),
8862 }
8863 };
8864 return Err(self.rt_err(&format!("global '{name}' already defined")));
8865 }
8866 }
8867 Op::ExtraArg => unreachable!("EXTRAARG executed directly"),
8868 }
8869 }
8870 }
8871
8872 #[inline(always)]
8873 fn pc_of_top(&self) -> u32 {
8874 self.top_frame().pc
8875 }
8876
8877 #[inline(always)]
8878 fn bump_pc(&mut self) {
8879 // Inline `top_frame_mut`: top is guaranteed Lua (continuation frames
8880 // drained at dispatch loop head). Avoids the and_then/lua_mut Option
8881 // layers — bump_pc fires per Jmp / cond_skip miss, so the savings add
8882 // up over `fib_28`'s ~500k jumps.
8883 // 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).
8884 match unsafe { self.frames.last_mut().unwrap_unchecked() } {
8885 CallFrame::Lua(f) => f.pc += 1,
8886 _ => unreachable!("Cont frame at bump_pc"),
8887 }
8888 }
8889
8890 #[inline(always)]
8891 fn add_pc(&mut self, d: i32) {
8892 // 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).
8893 match unsafe { self.frames.last_mut().unwrap_unchecked() } {
8894 CallFrame::Lua(f) => f.pc = (f.pc as i64 + d as i64) as u32,
8895 _ => unreachable!("Cont frame at add_pc"),
8896 }
8897 }
8898
8899 /// PUC conditional-skip convention: the JMP that follows is executed when
8900 /// `cond == k`; otherwise it is skipped.
8901 #[inline(always)]
8902 fn cond_skip(&mut self, cond: bool, k: bool) {
8903 if cond != k {
8904 self.bump_pc();
8905 }
8906 }
8907
8908 // ---- indexing (with __index/__newindex chains) ----
8909
8910 /// The `#` length operation: string byte length, `__len` if present, else
8911 /// the raw table border. Returns the raw length value (may be non-integer
8912 /// when `__len` is exotic).
8913 pub(crate) fn len_value(&mut self, v: Value) -> Result<Value, LuaError> {
8914 match self.len_step(v)? {
8915 MmOut::Done(n) => Ok(n),
8916 // PUC calls unary metamethods with the operand twice
8917 MmOut::Mm { func, recv } => self.call_mm1(func, &[recv, recv]),
8918 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from len_step"),
8919 }
8920 }
8921
8922 /// Length fast path: a string's byte count or a table's raw border when no
8923 /// `__len` is present (`Done`); otherwise the `__len` metamethod (`Mm`),
8924 /// called with the operand twice. Errors for a non-table with no `__len`.
8925 fn len_step(&mut self, v: Value) -> Result<MmOut, LuaError> {
8926 match v {
8927 Value::Str(s) => Ok(MmOut::Done(Value::Int(s.len() as i64))),
8928 Value::Table(t) => {
8929 // PUC 5.1's `__len` applies to userdata only — `luaV_objlen`
8930 // there takes the raw border for a table without consulting
8931 // the metatable, so `#setmetatable({}, {__len = f})` is 0 on
8932 // 5.1 and 7 on 5.2+. Verified against stock 5.1.5 / 5.2.4.
8933 if self.version() == crate::version::LuaVersion::Lua51 {
8934 return Ok(MmOut::Done(Value::Int(t.len())));
8935 }
8936 let mm = self.get_mm(v, Mm::Len);
8937 if mm.is_nil() {
8938 Ok(MmOut::Done(Value::Int(t.len())))
8939 } else {
8940 Ok(MmOut::Mm { func: mm, recv: v })
8941 }
8942 }
8943 _ => {
8944 let mm = self.get_mm(v, Mm::Len);
8945 if mm.is_nil() {
8946 Err(self.type_err("get length of", v))
8947 } else {
8948 Ok(MmOut::Mm { func: mm, recv: v })
8949 }
8950 }
8951 }
8952 }
8953
8954 pub(crate) fn index_value(&mut self, t: Value, key: Value) -> Result<Value, LuaError> {
8955 match self.index_step(t, key)? {
8956 MmOut::Done(v) => Ok(v),
8957 MmOut::Mm { func, recv } => self.call_mm1(func, &[recv, key]),
8958 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from index_step"),
8959 }
8960 }
8961
8962 /// PUC `MAXTAGLOOP`: 100 links of `__index`/`__newindex` in 5.1/5.2,
8963 /// 2000 from 5.3.
8964 fn tag_loop_limit(&self) -> u32 {
8965 if self.version <= LuaVersion::Lua52 {
8966 100
8967 } else {
8968 MAX_TAG_LOOP
8969 }
8970 }
8971
8972 /// Resolve `t[key]` through the `__index` chain, stopping at the first raw
8973 /// hit (`Done`) or function metamethod (`Mm`). Table-valued `__index` links
8974 /// are followed inline (no yield possible); only a function link can yield.
8975 fn index_step(&mut self, t: Value, key: Value) -> Result<MmOut, LuaError> {
8976 let mut cur = t;
8977 for _ in 0..self.tag_loop_limit() {
8978 let mm = match cur {
8979 Value::Table(tb) => {
8980 let v = tb.get(key);
8981 if !v.is_nil() {
8982 return Ok(MmOut::Done(v));
8983 }
8984 let mm = self.get_mm(cur, Mm::Index);
8985 if mm.is_nil() {
8986 return Ok(MmOut::Done(Value::Nil));
8987 }
8988 mm
8989 }
8990 v => {
8991 let mm = self.get_mm(v, Mm::Index);
8992 if mm.is_nil() {
8993 return Err(self.type_err("index", v));
8994 }
8995 mm
8996 }
8997 };
8998 match mm {
8999 Value::Closure(_) | Value::Native(_) => {
9000 return Ok(MmOut::Mm {
9001 func: mm,
9002 recv: cur,
9003 });
9004 }
9005 next => cur = next,
9006 }
9007 }
9008 Err(self.runerror(if self.version <= LuaVersion::Lua52 {
9009 "loop in gettable"
9010 } else {
9011 "'__index' chain too long; possible loop"
9012 }))
9013 }
9014
9015 pub(crate) fn newindex_value(
9016 &mut self,
9017 t: Value,
9018 key: Value,
9019 v: Value,
9020 ) -> Result<(), LuaError> {
9021 match self.newindex_step(t, key, v)? {
9022 MmOut::Done(_) => Ok(()),
9023 MmOut::Mm { func, recv } => {
9024 self.call_value(func, &[recv, key, v])?;
9025 Ok(())
9026 }
9027 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from newindex_step"),
9028 }
9029 }
9030
9031 /// Resolve `t[key] = v` through the `__newindex` chain. A raw assignment is
9032 /// performed inline (returning `Done`); only a function metamethod (`Mm`)
9033 /// needs an actual call — which the caller may run yieldably.
9034 fn newindex_step(&mut self, t: Value, key: Value, v: Value) -> Result<MmOut, LuaError> {
9035 // Read-time probe (gc-verify): a dead query key at a
9036 // WRITE site, attributed to the instruction that produced it.
9037 #[cfg(feature = "gc-verify")]
9038 if let Some(p) = match key {
9039 Value::Str(s) => Some(s.as_ptr() as usize),
9040 Value::Table(t2) => Some(t2.as_ptr() as usize),
9041 _ => None,
9042 } && crate::runtime::gc_verify_probe::is_freed(p)
9043 {
9044 let detail = match self.frames.last() {
9045 Some(CallFrame::Lua(f)) => {
9046 let pc = f.pc as usize;
9047 let mut w = String::new();
9048 for q in pc.saturating_sub(6)..(pc + 2) {
9049 if let Some(inst) = f.closure.proto.code.get(q) {
9050 w.push_str(&format!(
9051 "\n [{q}] {:?} a={} b={} c={} k={}",
9052 inst.op(),
9053 inst.a(),
9054 inst.b(),
9055 inst.c(),
9056 inst.k()
9057 ));
9058 }
9059 }
9060 format!("pc={pc} base={} gc_top={} window:{w}", f.base, self.gc_top)
9061 }
9062 _ => "non-Lua frame".into(),
9063 };
9064 panic!("[gc-verify] newindex_step QUERY key {p:#x} freed. {detail}");
9065 }
9066 let mut cur = t;
9067 for _ in 0..self.tag_loop_limit() {
9068 let mm = match cur {
9069 Value::Table(tb) => {
9070 // Single-walk collapse — Table::try_set_existing
9071 // fuses the prior `tb.get(key).is_nil()` gate and
9072 // `raw_set` walk into one chain traversal when the
9073 // key is already present with a non-nil value. The
9074 // __newindex chain semantics are preserved by the
9075 // identity (slot_nil ⇔ fire_newindex).
9076 //
9077 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the
9078 // heap is single-threaded and the pointer is live as
9079 // long as it is reachable from active roots (see
9080 // heap.rs:5-7). Mirrors the raw_set wrapper below.
9081 if unsafe { tb.as_mut() }.try_set_existing(key, v) {
9082 self.heap
9083 .barrier_back(tb.as_ptr() as *mut crate::runtime::heap::GcHeader);
9084 return Ok(MmOut::Done(Value::Nil));
9085 }
9086 let mm = self.get_mm(cur, Mm::NewIndex);
9087 if mm.is_nil() {
9088 self.raw_set(tb, key, v)?;
9089 return Ok(MmOut::Done(Value::Nil));
9090 }
9091 mm
9092 }
9093 bad => {
9094 let mm = self.get_mm(bad, Mm::NewIndex);
9095 if mm.is_nil() {
9096 return Err(self.type_err("index", bad));
9097 }
9098 mm
9099 }
9100 };
9101 match mm {
9102 Value::Closure(_) | Value::Native(_) => {
9103 return Ok(MmOut::Mm {
9104 func: mm,
9105 recv: cur,
9106 });
9107 }
9108 next => cur = next,
9109 }
9110 }
9111 Err(self.runerror(if self.version <= LuaVersion::Lua52 {
9112 "loop in settable"
9113 } else {
9114 "'__newindex' chain too long; possible loop"
9115 }))
9116 }
9117
9118 pub(crate) fn raw_set(&mut self, t: Gc<Table>, key: Value, v: Value) -> Result<(), LuaError> {
9119 // 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).
9120 match unsafe { t.as_mut() }.set(&mut self.heap, key, v) {
9121 Ok(()) => {
9122 self.heap
9123 .barrier_back(t.as_ptr() as *mut crate::runtime::heap::GcHeader);
9124 Ok(())
9125 }
9126 Err(TableError::NilIndex) => Err(self.runerror("table index is nil")),
9127 Err(TableError::NanIndex) => Err(self.runerror("table index is NaN")),
9128 Err(TableError::Overflow) => Err(self.runerror("table overflow")),
9129 Err(TableError::InvalidNext) => unreachable!(),
9130 }
9131 }
9132
9133 /// Decide equality, or surface the `__eq` metamethod to call. `Done` carries
9134 /// the boolean result; `Mm` (when raw equality fails and both are tables
9135 /// with an `__eq`) carries the metamethod — called with `(l, r)`.
9136 fn eq_step(&mut self, l: Value, r: Value) -> MmOut {
9137 if l.raw_eq(r) {
9138 return MmOut::Done(Value::Bool(true));
9139 }
9140 if let (Value::Table(_), Value::Table(_)) | (Value::Userdata(_), Value::Userdata(_)) =
9141 (l, r)
9142 {
9143 // PUC 5.3+ accepts any `__eq` reachable from either operand; 5.1
9144 // and 5.2 require the two operands' metatables to expose the same
9145 // `__eq` (`get_compTM` / `get_equalTM`) — `c == d` where `d` has
9146 // no metatable falls straight back to raw inequality. events.lua
9147 // 5.1 :262 bakes this in.
9148 let mm = if self.version() <= LuaVersion::Lua52 {
9149 self.get_comp_mm(l, r, Mm::Eq)
9150 } else {
9151 let mut m = self.get_mm(l, Mm::Eq);
9152 if m.is_nil() {
9153 m = self.get_mm(r, Mm::Eq);
9154 }
9155 m
9156 };
9157 if !mm.is_nil() {
9158 return MmOut::Mm { func: mm, recv: l };
9159 }
9160 }
9161 MmOut::Done(Value::Bool(false))
9162 }
9163
9164 // ---- arithmetic ----
9165
9166 #[inline(always)]
9167 fn arith_rr(&mut self, inst: Inst, base: u32, op: ArithOp) -> Result<(), LuaError> {
9168 let l = self.r(base, inst.b());
9169 let r = self.r(base, inst.c());
9170 // hot path: Int + Int for Add / Sub / Mul — fib_28, loop_int_1m,
9171 // binary_trees all hammer these. Skipping coerce_num + the big
9172 // arith_fast match shaves several conditional moves per op.
9173 if let (Value::Int(a), Value::Int(b)) = (l, r) {
9174 let fast = match op {
9175 ArithOp::Add => Some(Value::Int(a.wrapping_add(b))),
9176 ArithOp::Sub => Some(Value::Int(a.wrapping_sub(b))),
9177 ArithOp::Mul => Some(Value::Int(a.wrapping_mul(b))),
9178 _ => None,
9179 };
9180 if let Some(v) = fast {
9181 self.set_r(base, inst.a(), v);
9182 return Ok(());
9183 }
9184 }
9185 // hot path: Float + Float for Add / Sub / Mul / Div — math_loop_100k
9186 // and any numeric workload with non-integer accumulators benefits.
9187 if let (Value::Float(a), Value::Float(b)) = (l, r) {
9188 let fast = match op {
9189 ArithOp::Add => Some(Value::Float(a + b)),
9190 ArithOp::Sub => Some(Value::Float(a - b)),
9191 ArithOp::Mul => Some(Value::Float(a * b)),
9192 ArithOp::Div => Some(Value::Float(a / b)),
9193 _ => None,
9194 };
9195 if let Some(v) = fast {
9196 self.set_r(base, inst.a(), v);
9197 return Ok(());
9198 }
9199 }
9200 // An `Add` with k set is a 5.4+ `x - 0` (see `Op::Add`): the right
9201 // operand is the integer 0, so it adds only when the left is a number.
9202 let op = if inst.k() && op == ArithOp::Add && !matches!(l, Value::Int(_) | Value::Float(_))
9203 {
9204 ArithOp::Sub
9205 } else {
9206 op
9207 };
9208 match self.arith_fast(op, l, r)? {
9209 Some(v) => self.set_r(base, inst.a(), v),
9210 None => {
9211 let mm = self.arith_mm_func(op, l, r)?;
9212 let dst = base + inst.a();
9213 self.begin_meta_call(mm, &[l, r], MetaAction::Store { dst }, op.mm_name())?;
9214 }
9215 }
9216 Ok(())
9217 }
9218
9219 /// The number a unary `-` operand stands for: 5.4+ leaves strings to
9220 /// the string metatable, 5.3 converts them to floats (PUC `tonumber`).
9221 fn unary_operand(&self, v: Value) -> Option<Num> {
9222 let n = self.arith_operand()(v);
9223 if self.version == LuaVersion::Lua53 && matches!(v, Value::Str(_)) {
9224 n.map(|n| Num::Float(n.as_f64()))
9225 } else {
9226 n
9227 }
9228 }
9229
9230 /// How an arithmetic operand becomes a number: 5.4+ takes numbers only
9231 /// (strings go to their metatable), 5.3 converts numeric strings, and
9232 /// 5.1/5.2, which have only floats, convert them to floats (5.1 with C
9233 /// `strtod`, so `"inf"` and `"0x1p4"` count).
9234 fn arith_operand(&self) -> fn(Value) -> Option<Num> {
9235 if self.version >= LuaVersion::Lua54 {
9236 as_number
9237 } else if self.version == LuaVersion::Lua53 {
9238 coerce_num
9239 } else if self.version == LuaVersion::Lua52 {
9240 coerce_num_float
9241 } else {
9242 coerce_num_51
9243 }
9244 }
9245
9246 /// Fast path for an arithmetic/bitwise op: `Ok(Some(v))` when computed
9247 /// directly, `Ok(None)` when a metamethod is required (the caller decides
9248 /// whether to call it synchronously or yieldably).
9249 fn arith_fast(&mut self, op: ArithOp, l: Value, r: Value) -> Result<Option<Value>, LuaError> {
9250 use ArithOp::*;
9251 // 5.4 moved string->number coercion out of the VM: a string operand
9252 // goes to the string metatable's `__add` etc., and bitwise operators
9253 // have no string metamethods at all.
9254 let num = self.arith_operand();
9255 if let BAnd | BOr | BXor | Shl | Shr = op {
9256 let (Some(a), Some(b)) = (num(l), num(r)) else {
9257 return Ok(None);
9258 };
9259 let (Some(a), Some(b)) = (int_of(a), int_of(b)) else {
9260 // PUC luaG_tointerror: name the offending operand
9261 return Err(self.no_int_rep_err());
9262 };
9263 let v = match op {
9264 BAnd => a & b,
9265 BOr => a | b,
9266 BXor => a ^ b,
9267 Shl => shift_left(a, b),
9268 Shr => shift_left(a, b.wrapping_neg()),
9269 _ => unreachable!(),
9270 };
9271 return Ok(Some(Value::Int(v)));
9272 }
9273 let (Some(mut ln), Some(mut rn)) = (num(l), num(r)) else {
9274 return Ok(None);
9275 };
9276 // PUC 5.3 takes the integer path only when both operands are
9277 // integers (`ttisinteger`); a converted string goes through
9278 // `tonumber`, which yields a float.
9279 if self.version == LuaVersion::Lua53
9280 && (matches!(l, Value::Str(_)) || matches!(r, Value::Str(_)))
9281 {
9282 ln = Num::Float(ln.as_f64());
9283 rn = Num::Float(rn.as_f64());
9284 }
9285 match arith_num(self.version, op, ln, rn) {
9286 Ok(v) => Ok(Some(v)),
9287 Err(msg) => Err(self.runerror(msg)),
9288 }
9289 }
9290
9291 /// Find the arithmetic/bitwise metamethod (left operand first), or raise the
9292 /// PUC type error when neither operand provides one.
9293 fn arith_mm_func(&mut self, op: ArithOp, l: Value, r: Value) -> Result<Value, LuaError> {
9294 use ArithOp::*;
9295 let event = match op {
9296 Add => Mm::Add,
9297 Sub => Mm::Sub,
9298 Mul => Mm::Mul,
9299 Div => Mm::Div,
9300 Mod => Mm::Mod,
9301 Pow => Mm::Pow,
9302 IDiv => Mm::IDiv,
9303 BAnd => Mm::BAnd,
9304 BOr => Mm::BOr,
9305 BXor => Mm::BXor,
9306 Shl => Mm::Shl,
9307 Shr => Mm::Shr,
9308 };
9309 let mut mm = self.get_mm(l, event);
9310 if mm.is_nil() {
9311 mm = self.get_mm(r, event);
9312 }
9313 if mm.is_nil() {
9314 let what = if matches!(op, BAnd | BOr | BXor | Shl | Shr) {
9315 "perform bitwise operation on"
9316 } else {
9317 "perform arithmetic on"
9318 };
9319 // luaG_opinterror blames the first operand that is not a number;
9320 // before 5.4 a numeric string counts as one.
9321 let bad = if self.arith_operand()(l).is_none() {
9322 l
9323 } else {
9324 r
9325 };
9326 return Err(self.type_err(what, bad));
9327 }
9328 Ok(mm)
9329 }
9330
9331 // ---- comparison ----
9332
9333 /// `lua_compare(L, a, b, LUA_OPEQ)`: equality including `__eq`.
9334 pub(crate) fn equal(&mut self, l: Value, r: Value) -> Result<bool, LuaError> {
9335 match self.eq_step(l, r) {
9336 MmOut::Done(v) => Ok(v.truthy()),
9337 MmOut::Mm { func, .. } => Ok(self.call_mm1(func, &[l, r])?.truthy()),
9338 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from eq_step"),
9339 }
9340 }
9341
9342 pub(crate) fn less_than(&mut self, l: Value, r: Value, or_eq: bool) -> Result<bool, LuaError> {
9343 match self.less_step(l, r, or_eq)? {
9344 MmOut::Done(v) => Ok(v.truthy()),
9345 MmOut::Mm { func, .. } => Ok(self.call_mm1(func, &[l, r])?.truthy()),
9346 MmOut::CompareSynth { func } => {
9347 // ≤5.3 `__le` via `not __lt(r, l)`. Synchronous helper used
9348 // by library code (sort comparator etc.) — no yield expected
9349 // here (a yield would have hit `call_noyield`'s C boundary).
9350 Ok(!self.call_mm1(func, &[r, l])?.truthy())
9351 }
9352 }
9353 }
9354
9355 /// Decide `l < r` / `l <= r`, or surface the `__lt`/`__le` metamethod. `Done`
9356 /// carries the boolean result; `Mm` (for non-number/string operands) carries
9357 /// the metamethod — called with `(l, r)`; raises the PUC compare error when
9358 /// neither operand provides one.
9359 fn less_step(&mut self, l: Value, r: Value, or_eq: bool) -> Result<MmOut, LuaError> {
9360 let b = match (l, r) {
9361 (Value::Int(a), Value::Int(b)) => {
9362 if or_eq {
9363 a <= b
9364 } else {
9365 a < b
9366 }
9367 }
9368 (Value::Float(a), Value::Float(b)) => {
9369 if or_eq {
9370 a <= b
9371 } else {
9372 a < b
9373 }
9374 }
9375 (Value::Int(a), Value::Float(b)) => {
9376 if or_eq {
9377 int_le_float(a, b)
9378 } else {
9379 int_lt_float(a, b)
9380 }
9381 }
9382 (Value::Float(a), Value::Int(b)) => {
9383 if a.is_nan() {
9384 false
9385 } else if or_eq {
9386 !int_lt_float(b, a)
9387 } else {
9388 !int_le_float(b, a)
9389 }
9390 }
9391 (Value::Str(a), Value::Str(b)) => {
9392 let (a, b) = (a.as_bytes(), b.as_bytes());
9393 if or_eq { a <= b } else { a < b }
9394 }
9395 (l, r) => {
9396 let event = if or_eq { Mm::Le } else { Mm::Lt };
9397 // PUC 5.1's `get_compTM` rule applies to ordered comparisons
9398 // too: both operands' metatables must expose the same
9399 // implementation for `__lt` / `__le` to fire. events.lua 5.1
9400 // :262 expects `c < d` (where `d` has no metatable) to error
9401 // with the default "attempt to compare two table values"
9402 // rather than running c's `__lt` blindly.
9403 let mm = if self.version() <= LuaVersion::Lua51 {
9404 self.get_comp_mm(l, r, event)
9405 } else {
9406 let mut m = self.get_mm(l, event);
9407 if m.is_nil() {
9408 m = self.get_mm(r, event);
9409 }
9410 m
9411 };
9412 // PUC ≤5.4: `a <= b` falls back to `not (b < a)` when neither
9413 // operand carries `__le` (5.4 through its default build's
9414 // LUA_COMPAT_LT_LE); 5.5 requires an explicit `__le`.
9415 // events.lua 5.2/5.3 :172 relies on the synthesis — its
9416 // metatable defines only `__lt`. The `__lt` is looked up as
9417 // for `b < a`: on `b` first (5.1: the same one on both). The
9418 // fallback calls `__lt(r, l)` synchronously (the suite's
9419 // `__lt` doesn't yield) and negates the result; the yieldable
9420 // `__lt` path stays reserved for the explicit `<` operator.
9421 if mm.is_nil() && or_eq && self.version < LuaVersion::Lua55 {
9422 let mm_lt = if self.version <= LuaVersion::Lua51 {
9423 self.get_comp_mm(r, l, Mm::Lt)
9424 } else {
9425 let m = self.get_mm(r, Mm::Lt);
9426 if m.is_nil() {
9427 self.get_mm(l, Mm::Lt)
9428 } else {
9429 m
9430 }
9431 };
9432 if !mm_lt.is_nil() {
9433 return Ok(MmOut::CompareSynth { func: mm_lt });
9434 }
9435 }
9436 if mm.is_nil() {
9437 // PUC luaG_ordererror: "two X values" when the operand
9438 // types match, "X with Y" otherwise (objtypename-aware).
9439 let (t1, t2) = (self.obj_typename(l), self.obj_typename(r));
9440 return Err(self.runerror(&if t1 == t2 {
9441 format!("attempt to compare two {t1} values")
9442 } else {
9443 format!("attempt to compare {t1} with {t2}")
9444 }));
9445 }
9446 return Ok(MmOut::Mm { func: mm, recv: l });
9447 }
9448 };
9449 Ok(MmOut::Done(Value::Bool(b)))
9450 }
9451
9452 // ---- numeric for ----
9453
9454 /// Check and convert a numeric for's control values the way the
9455 /// dialect's `OP_FORPREP` does. The integer loop is chosen by the
9456 /// values' tags (a numeric string makes it a float loop, 5.3+); the
9457 /// check order, wording and the zero-step error differ per version:
9458 /// 5.1/5.2 test initial value, limit, step; 5.3+ limit, step, initial
9459 /// value; only 5.4+ reject a zero step, and an integer loop does that
9460 /// before looking at the limit.
9461 fn for_operands(&mut self, base: u32, a: u32) -> Result<(Num, Num, Num), LuaError> {
9462 let (init, limit, step) = (self.r(base, a), self.r(base, a + 1), self.r(base, a + 2));
9463 let v = self.version();
9464 let order = if v <= LuaVersion::Lua52 {
9465 [("initial value", init), ("limit", limit), ("step", step)]
9466 } else {
9467 [("limit", limit), ("step", step), ("initial value", init)]
9468 };
9469 if v >= LuaVersion::Lua54 && matches!((init, step), (Value::Int(_), Value::Int(0))) {
9470 return Err(self.rt_err("'for' step is zero"));
9471 }
9472 for (what, val) in order {
9473 if as_num(val, v).is_none() {
9474 return Err(self.rt_err(&if v >= LuaVersion::Lua54 {
9475 format!(
9476 "bad 'for' {what} (number expected, got {})",
9477 self.obj_typename(val)
9478 )
9479 } else {
9480 format!("'for' {what} must be a number")
9481 }));
9482 }
9483 }
9484 let n = |val| as_num(val, v).expect("checked above");
9485 let int_loop =
9486 v <= LuaVersion::Lua52 || matches!((init, step), (Value::Int(_), Value::Int(_)));
9487 if int_loop {
9488 return Ok((n(init), n(limit), n(step)));
9489 }
9490 let (i, l, st) = (n(init).as_f64(), n(limit).as_f64(), n(step).as_f64());
9491 if v >= LuaVersion::Lua54 && st == 0.0 {
9492 return Err(self.rt_err("'for' step is zero"));
9493 }
9494 Ok((Num::Float(i), Num::Float(l), Num::Float(st)))
9495 }
9496
9497 fn for_prep(&mut self, inst: Inst, base: u32) -> Result<(), LuaError> {
9498 let a = inst.a();
9499 let (init_n, limit_n, step_n) = self.for_operands(base, a)?;
9500 // PUC 5.1–5.3 `OP_FORPREP` stores `i = init - step` and *unconditionally*
9501 // jumps to the matching `OP_FORLOOP` — the body never runs ahead of the
9502 // first test, so each successful iteration emits a backward `OP_FORLOOP`
9503 // jump (db.lua's `for i=1,4 do a=1 end` ↦ 5 line-hook events instead of
9504 // 5.4's 4). 5.4+ collapsed that to a count-based fall-through. The skip
9505 // distance in luna's encoding is `loop_pc - prep_pc`; firing
9506 // `add_pc(bx - 1)` lands the running pc on OP_FORLOOP itself.
9507 let pre53 = self.version() <= LuaVersion::Lua53;
9508 match (init_n, step_n) {
9509 (Num::Int(i0), Num::Int(st)) => {
9510 if pre53 {
9511 // PUC 5.3 `forlimit`: int limit passes through; float limit
9512 // gets clamped to MIN/MAX with a `stopnow` flag set only
9513 // when the clamp is unreachable (positive float with a
9514 // negative step → limit=MAX, stopnow; negative float with
9515 // step>=0 → limit=MIN, stopnow). On `stopnow` PUC rewrites
9516 // `init = 0` so OP_FORLOOP's first test against the
9517 // unreachable clamp fails cleanly. An ordinary in-range
9518 // empty loop (e.g. `for i = 1, 0`) is *not* `stopnow` — it
9519 // lets OP_FORLOOP's natural test reject the first step.
9520 let (lim, stopnow) = match limit_n {
9521 Num::Int(l) => (l, false),
9522 Num::Float(f) => {
9523 // `luaV_tointeger` floors (ceils for a negative
9524 // step); a float it cannot fit is clamped on
9525 // the side of its sign, NaN counting as
9526 // negative (`0 < n` is false).
9527 let conv = if st < 0 { f.ceil() } else { f.floor() };
9528 if (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0)
9529 .contains(&conv)
9530 {
9531 (conv as i64, false)
9532 } else if f > 0.0 {
9533 (i64::MAX, st < 0)
9534 } else {
9535 (i64::MIN, st > 0)
9536 }
9537 }
9538 };
9539 let initv = if stopnow { 0 } else { i0 };
9540 let pre = initv.wrapping_sub(st);
9541 self.set_r(base, a, Value::Int(pre));
9542 self.set_r(base, a + 1, Value::Int(lim));
9543 self.set_r(base, a + 2, Value::Int(st));
9544 self.add_pc(inst.bx() as i32 - 1);
9545 return Ok(());
9546 }
9547 let (lim, empty) = int_for_limit(limit_n, i0, st);
9548 if empty {
9549 self.add_pc(inst.bx() as i32);
9550 return Ok(());
9551 }
9552 let count = if st > 0 {
9553 (lim as u64).wrapping_sub(i0 as u64) / (st as u64)
9554 } else {
9555 (i0 as u64).wrapping_sub(lim as u64) / (st as i128).unsigned_abs() as u64
9556 };
9557 self.set_r(base, a, Value::Int(i0));
9558 self.set_r(base, a + 1, Value::Int(count as i64));
9559 self.set_r(base, a + 2, Value::Int(st));
9560 self.set_r(base, a + 3, Value::Int(i0));
9561 }
9562 _ => {
9563 let (x0, lim, st) = (init_n.as_f64(), limit_n.as_f64(), step_n.as_f64());
9564 if pre53 {
9565 let pre = x0 - st;
9566 self.set_r(base, a, Value::Float(pre));
9567 self.set_r(base, a + 1, Value::Float(lim));
9568 self.set_r(base, a + 2, Value::Float(st));
9569 self.add_pc(inst.bx() as i32 - 1);
9570 return Ok(());
9571 }
9572 // lvm.c `forprep`: skip only when `0 < step ? limit < init :
9573 // init < limit`; a NaN makes both false, so the body runs
9574 // once (with a NaN step, on the second test's side)
9575 let skip = if 0.0 < st { lim < x0 } else { x0 < lim };
9576 let runs = !skip;
9577 if !runs {
9578 self.add_pc(inst.bx() as i32);
9579 return Ok(());
9580 }
9581 self.set_r(base, a, Value::Float(x0));
9582 self.set_r(base, a + 1, Value::Float(lim));
9583 self.set_r(base, a + 2, Value::Float(st));
9584 self.set_r(base, a + 3, Value::Float(x0));
9585 }
9586 }
9587 Ok(())
9588 }
9589
9590 #[inline(always)]
9591 fn for_loop(&mut self, inst: Inst, base: u32) -> Result<(), LuaError> {
9592 let a = inst.a();
9593 // PUC 5.1–5.3 `OP_FORLOOP` compares the post-step `i` to `limit`
9594 // directly (R[a+1] holds the limit, *not* a remaining-count) so the
9595 // first iteration's test fires through the same backward-jump path as
9596 // every later iteration. 5.4+ switched to the count-based form luna
9597 // already uses for `Int`; the float branch was already PUC-3.x-style.
9598 let v = self.version();
9599 let pre53 = v <= LuaVersion::Lua53;
9600 // `for_prep` leaves the three slots all Int or all Float; anything
9601 // else was written by `debug.setlocal` or by crafted bytecode. PUC
9602 // reads such slots unchecked (garbage or a crash); luna raises.
9603 match (self.r(base, a), self.r(base, a + 1), self.r(base, a + 2)) {
9604 (Value::Int(cur), Value::Int(lim), Value::Int(st)) if pre53 => {
9605 let next = cur.wrapping_add(st);
9606 let cont = if st > 0 { next <= lim } else { next >= lim };
9607 if cont {
9608 self.set_r(base, a, Value::Int(next));
9609 self.set_r(base, a + 3, Value::Int(next));
9610 self.add_pc(-(inst.bx() as i32));
9611 }
9612 }
9613 // the count is unsigned (PUC `lua_Unsigned`): a loop over
9614 // more than 2^63 values stores a "negative" one
9615 (Value::Int(cur), Value::Int(count), Value::Int(st)) => {
9616 if count != 0 {
9617 let next = cur.wrapping_add(st);
9618 self.set_r(base, a, Value::Int(next));
9619 self.set_r(base, a + 1, Value::Int(count.wrapping_sub(1)));
9620 self.set_r(base, a + 3, Value::Int(next));
9621 self.add_pc(-(inst.bx() as i32));
9622 }
9623 }
9624 (Value::Float(cur), Value::Float(lim), Value::Float(st)) => {
9625 self.float_for_step(inst, base, cur, lim, st);
9626 }
9627 // 5.1/5.2 have one number type, so a number of the other
9628 // representation stored into a slot is still a valid state
9629 (x, l, s) if v <= LuaVersion::Lua52 => {
9630 match (as_number(x), as_number(l), as_number(s)) {
9631 (Some(cur), Some(lim), Some(st)) => {
9632 self.float_for_step(inst, base, cur.as_f64(), lim.as_f64(), st.as_f64())
9633 }
9634 _ => return Err(self.rt_err("'for' state corrupted")),
9635 }
9636 }
9637 _ => return Err(self.rt_err("'for' state corrupted")),
9638 }
9639 Ok(())
9640 }
9641
9642 #[inline(always)]
9643 fn float_for_step(&mut self, inst: Inst, base: u32, cur: f64, lim: f64, st: f64) {
9644 let a = inst.a();
9645 let next = cur + st;
9646 let cont = if st > 0.0 { next <= lim } else { next >= lim };
9647 if cont {
9648 self.set_r(base, a, Value::Float(next));
9649 self.set_r(base, a + 3, Value::Float(next));
9650 self.add_pc(-(inst.bx() as i32));
9651 }
9652 }
9653
9654 // ---- native helpers (used by builtins) ----
9655
9656 /// A native function's own captured upvalue (self lives at func_slot).
9657 ///
9658 /// Public so `native_typed` trampolines and embedders authoring
9659 /// stateful natives via `native_with(...)` can read their upvals.
9660 pub fn nat_upval(&self, func_slot: u32, i: usize) -> Value {
9661 let Value::Native(nc) = self.stack[func_slot as usize] else {
9662 unreachable!("native frame without native closure");
9663 };
9664 nc.upvals[i]
9665 }
9666
9667 /// Number of upvalues captured by the native at `func_slot` (variadic
9668 /// captures such as the `io.lines` format list).
9669 pub(crate) fn nat_upcount(&self, func_slot: u32) -> usize {
9670 let Value::Native(nc) = self.stack[func_slot as usize] else {
9671 unreachable!("native frame without native closure");
9672 };
9673 nc.upvals.len()
9674 }
9675
9676 /// Write a native function's own upvalue (stateful iterators).
9677 pub(crate) fn nat_set_upval(&mut self, func_slot: u32, i: usize, v: Value) {
9678 let Value::Native(nc) = self.stack[func_slot as usize] else {
9679 unreachable!("native frame without native closure");
9680 };
9681 // 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).
9682 unsafe { nc.as_mut() }.upvals[i] = v;
9683 // NativeClosure.upvals is traced as part of its Trace; a long-lived
9684 // stateful iterator closure (e.g. string.gmatch) sees many writes —
9685 // barrier_back once-and-done is cheaper than per-child forward.
9686 self.heap
9687 .barrier_back(nc.as_ptr() as *mut crate::runtime::heap::GcHeader);
9688 }
9689
9690 /// Read the i-th positional argument inside a `NativeFn` body
9691 /// (analogous to `lua_tovalue(L, i + 1)`). `i >= nargs` yields `Nil`,
9692 /// matching PUC's "missing arg is nil" contract. Public so embedders
9693 /// can author their own natives.
9694 pub fn nat_arg(&self, func_slot: u32, nargs: u32, i: u32) -> Value {
9695 if i < nargs {
9696 self.stack[(func_slot + 1 + i) as usize]
9697 } else {
9698 Value::Nil
9699 }
9700 }
9701
9702 /// Overwrite the i-th argument slot of the running native (the in-place
9703 /// conversion `lua_tolstring` performs on a number argument).
9704 pub(crate) fn nat_set_arg(&mut self, func_slot: u32, i: u32, v: Value) {
9705 self.stack[(func_slot + 1 + i) as usize] = v;
9706 }
9707
9708 /// Push the return values of a `NativeFn` and return their count
9709 /// (analogous to pushing N values then `return N` from a C function).
9710 /// Public so embedders can author their own natives.
9711 pub fn nat_return(&mut self, func_slot: u32, vals: &[Value]) -> u32 {
9712 let need = func_slot as usize + vals.len();
9713 if self.stack.len() < need {
9714 self.stack.resize(need, Value::Nil);
9715 }
9716 for (i, &v) in vals.iter().enumerate() {
9717 self.stack[func_slot as usize + i] = v;
9718 }
9719 vals.len() as u32
9720 }
9721
9722 /// Fast string concatenation of an adjacent pair, or `None` when a
9723 /// `__concat` metamethod is required.
9724 fn concat_pair(&mut self, l: Value, r: Value) -> Result<Option<Value>, LuaError> {
9725 let legacy = self.float_fmt();
9726 // Length-check fast paths for both string operands BEFORE the
9727 // (expensive) copy in `concat_piece`, so a runaway `a..a..a..…`
9728 // chain (5.1 big.lua / 5.5 heavy.lua's `teststring`) raises the
9729 // overflow on the first pair that would exceed `INT_MAX` instead
9730 // of allocating multi-GB intermediates first.
9731 let max_str = i32::MAX as usize;
9732 if let (Value::Str(ls), Value::Str(rs)) = (l, r) {
9733 let a_len = ls.as_bytes().len();
9734 let b_len = rs.as_bytes().len();
9735 let new_len = a_len.checked_add(b_len);
9736 if new_len.is_none() || new_len.unwrap() > max_str {
9737 return Err(self.rt_err("string length overflow"));
9738 }
9739 }
9740 match (concat_piece(l, legacy), concat_piece(r, legacy)) {
9741 (Some(a), Some(b)) => {
9742 // PUC `MAX_SIZE` for Lua strings is `INT_MAX`; an attempt to
9743 // concat past it raises "string length overflow"
9744 // (5.5 heavy.lua `teststring` doubles `a..a..…` until it hits
9745 // exactly this wall).
9746 let new_len = a.len().checked_add(b.len());
9747 if new_len.is_none() || new_len.unwrap() > max_str {
9748 return Err(self.rt_err("string length overflow"));
9749 }
9750 let mut combined = a;
9751 combined.extend_from_slice(&b);
9752 Ok(Some(Value::Str(self.heap.intern(&combined))))
9753 }
9754 _ => Ok(None),
9755 }
9756 }
9757
9758 /// Fold the concat operands occupying `[base_a .. self.top)` right-to-left
9759 /// into a single result at `base_a` (PUC `luaV_concat`). Returns after
9760 /// either finishing (result at `base_a`) or arming a yieldable `__concat`
9761 /// call — its `Meta` continuation re-enters here on the metamethod's return.
9762 fn concat_run(&mut self, base_a: u32) -> Result<(), LuaError> {
9763 // Sum the lengths of all all-Str operands BEFORE starting the
9764 // right-associative fold so a 129-operand `a..a..…` chain
9765 // (5.1 big.lua's `rep129(longs)`) raises overflow immediately,
9766 // not after dozens of multi-GB intermediate intern+hash rounds.
9767 // A non-Str operand falls through to the per-pair check.
9768 let max_str = i32::MAX as usize;
9769 let mut total: usize = 0;
9770 let mut all_str = true;
9771 for slot in base_a..self.top {
9772 match self.stack[slot as usize] {
9773 Value::Str(s) => match total.checked_add(s.as_bytes().len()) {
9774 Some(t) if t <= max_str => total = t,
9775 _ => return Err(self.rt_err("string length overflow")),
9776 },
9777 _ => {
9778 all_str = false;
9779 break;
9780 }
9781 }
9782 }
9783 let _ = all_str; // discrimination already captured by early returns above
9784 while self.top.saturating_sub(base_a) >= 2 {
9785 let i = self.top - 1; // rightmost operand
9786 let x = self.stack[(i - 1) as usize];
9787 let y = self.stack[i as usize];
9788 match self.concat_pair(x, y)? {
9789 Some(s) => {
9790 self.stack[(i - 1) as usize] = s;
9791 self.top = i; // consumed y
9792 }
9793 None => {
9794 let mut mm = self.get_mm(x, Mm::Concat);
9795 if mm.is_nil() {
9796 mm = self.get_mm(y, Mm::Concat);
9797 }
9798 if mm.is_nil() {
9799 let legacy = self.float_fmt();
9800 let bad = if concat_piece(x, legacy).is_none() {
9801 x
9802 } else {
9803 y
9804 };
9805 return Err(self.type_err("concatenate", bad));
9806 }
9807 // result lands at i-1, dropping y (top→i); resume continues.
9808 let dst = i - 1;
9809 self.begin_meta_call(
9810 mm,
9811 &[x, y],
9812 MetaAction::Concat { dst, base_a },
9813 "concat",
9814 )?;
9815 return Ok(());
9816 }
9817 }
9818 }
9819 self.maybe_collect_garbage(base_a + 1);
9820 Ok(())
9821 }
9822
9823 /// `luaL_tolstring`: `__tostring` (whose result must be a string or a
9824 /// number, rendered), else the basic rendering, where 5.3+ names a value
9825 /// by a string `__name` metafield.
9826 pub fn tostring_value(&mut self, v: Value) -> Result<Vec<u8>, LuaError> {
9827 let mm = self.get_mm(v, Mm::ToString);
9828 if !mm.is_nil() {
9829 // `luaL_callmeta` is a plain `lua_call`: `__tostring` cannot yield.
9830 let r = self.call_noyield(mm, &[v])?;
9831 return match r.first().copied().unwrap_or(Value::Nil) {
9832 Value::Str(s) => Ok(s.as_bytes().to_vec()),
9833 r @ (Value::Int(_) | Value::Float(_)) => Ok(self.tostring_basic(r)),
9834 // luaL_error: positioned at whatever called the library function
9835 _ => Err(crate::vm::builtins::raise_str(
9836 self,
9837 "'__tostring' must return a string",
9838 )),
9839 };
9840 }
9841 if self.version >= LuaVersion::Lua53
9842 && !matches!(
9843 v,
9844 Value::Nil | Value::Bool(_) | Value::Int(_) | Value::Float(_) | Value::Str(_)
9845 )
9846 && let Value::Str(name) = self.get_mm(v, Mm::Name)
9847 {
9848 let basic = self.tostring_basic(v);
9849 let at = basic
9850 .iter()
9851 .position(|&c| c == b':')
9852 .expect("an object renders as `kind: address`");
9853 let mut out = name.as_bytes().to_vec();
9854 out.extend_from_slice(&basic[at..]);
9855 return Ok(out);
9856 }
9857 Ok(self.tostring_basic(v))
9858 }
9859
9860 /// The dialect's float-rendering flavor: ≤5.2 %.14g
9861 /// bare, 5.3/5.4 %.14g + ".0", 5.5 two-stage %.15g/%.17g + ".0".
9862 pub(crate) fn float_fmt(&self) -> numeric::FloatFmt {
9863 use crate::version::LuaVersion::*;
9864 match self.version {
9865 Lua51 | Lua52 => numeric::FloatFmt::Legacy14,
9866 Lua53 | Lua54 => numeric::FloatFmt::G14,
9867 _ => numeric::FloatFmt::TwoStage55,
9868 }
9869 }
9870
9871 /// Basic tostring (no metamethods).
9872 pub(crate) fn tostring_basic(&mut self, v: Value) -> Vec<u8> {
9873 match v {
9874 Value::Nil => b"nil".to_vec(),
9875 Value::Bool(true) => b"true".to_vec(),
9876 Value::Bool(false) => b"false".to_vec(),
9877 Value::Int(i) => numeric::num_to_string(Num::Int(i)).into_bytes(),
9878 // PUC ≤5.2 has no integer subtype — `tostring(2.0)` is `"2"`, not
9879 // `"2.0"`. The 5.3+ split needs the suffix so `print(2.0)` is
9880 // distinguishable from `print(2)`. pm.lua :13 builds patterns by
9881 // concatenating these renderings.
9882 Value::Float(f) => {
9883 numeric::num_to_string_for(Num::Float(f), self.float_fmt()).into_bytes()
9884 }
9885 Value::Str(s) => s.as_bytes().to_vec(),
9886 Value::Table(t) => format!("table: {:p}", t.as_ptr()).into_bytes(),
9887 Value::Closure(c) => format!("function: {:p}", c.as_ptr()).into_bytes(),
9888 Value::Native(n) => format!("function: {:p}", n.as_ptr()).into_bytes(),
9889 Value::Coro(co) => format!("thread: {:p}", co.as_ptr()).into_bytes(),
9890 // PUC names file handles `file (0x…)`; a bare userdata is
9891 // `userdata: 0x…`. The io library overrides this via __tostring.
9892 Value::Userdata(u) => format!("userdata: {:p}", u.as_ptr()).into_bytes(),
9893 // PUC `lua_topointer`/tostring on light udata: "userdata: 0x…"
9894 // (the "light" qualifier only appears in `luaL_typeerror`).
9895 Value::LightUserdata(p) => format!("userdata: {p:p}").into_bytes(),
9896 }
9897 }
9898}
9899
9900#[derive(Clone, Copy, PartialEq, Eq)]
9901pub(crate) enum ArithOp {
9902 Add,
9903 Sub,
9904 Mul,
9905 Mod,
9906 Pow,
9907 Div,
9908 IDiv,
9909 BAnd,
9910 BOr,
9911 BXor,
9912 Shl,
9913 Shr,
9914}
9915
9916impl ArithOp {
9917 /// PUC metamethod event name (`__add` → "add" etc.) used by
9918 /// `debug.getinfo(level, "n")` inside a metamethod handler.
9919 fn mm_name(self) -> &'static str {
9920 match self {
9921 ArithOp::Add => "add",
9922 ArithOp::Sub => "sub",
9923 ArithOp::Mul => "mul",
9924 ArithOp::Mod => "mod",
9925 ArithOp::Pow => "pow",
9926 ArithOp::Div => "div",
9927 ArithOp::IDiv => "idiv",
9928 ArithOp::BAnd => "band",
9929 ArithOp::BOr => "bor",
9930 ArithOp::BXor => "bxor",
9931 ArithOp::Shl => "shl",
9932 ArithOp::Shr => "shr",
9933 }
9934 }
9935}
9936
9937fn as_num(v: Value, version: LuaVersion) -> Option<Num> {
9938 match v {
9939 Value::Int(i) => Some(Num::Int(i)),
9940 Value::Float(f) => Some(Num::Float(f)),
9941 // PUC forprep coerces numeric strings (`for i = "10", "1", "-2"`).
9942 Value::Str(s) => str_to_num(s.as_bytes(), version),
9943 _ => None,
9944 }
9945}
9946
9947/// Float `%` as each dialect's `luai_nummod` defines it. 5.1/5.2 compute
9948/// `a - floor(a/b)*b` (so `5 % math.huge` is nan); 5.3 fixes `fmod`'s sign
9949/// when `m*b < 0`; 5.4 compares signs instead, because that product
9950/// underflows to zero for tiny operands.
9951fn float_mod(version: LuaVersion, a: f64, b: f64) -> f64 {
9952 if version <= LuaVersion::Lua52 {
9953 return a - (a / b).floor() * b;
9954 }
9955 let m = a % b;
9956 let fix = if version == LuaVersion::Lua53 {
9957 m * b < 0.0
9958 } else {
9959 (m > 0.0 && b < 0.0) || (m < 0.0 && b > 0.0)
9960 };
9961 if fix { m + b } else { m }
9962}
9963
9964/// A concatenable operand's byte form (string, or a number coerced to its
9965/// string), or `None` when only a `__concat` metamethod can handle it.
9966/// `legacy_float = true` follows PUC ≤5.2's `%.14g` rendering (no `.0`
9967/// suffix on integer-valued floats) — see `num_to_string_for`.
9968fn concat_piece(v: Value, float_fmt: numeric::FloatFmt) -> Option<Vec<u8>> {
9969 match v {
9970 Value::Str(s) => Some(s.as_bytes().to_vec()),
9971 Value::Int(x) => Some(numeric::num_to_string(Num::Int(x)).into_bytes()),
9972 Value::Float(x) => Some(numeric::num_to_string_for(Num::Float(x), float_fmt).into_bytes()),
9973 _ => None,
9974 }
9975}
9976
9977/// Index into the per-basic-type metatable table for a non-table value
9978/// (None for tables, which carry their own metatable).
9979fn type_mt_slot(v: Value) -> Option<usize> {
9980 match v {
9981 Value::Nil => Some(0),
9982 Value::Bool(_) => Some(1),
9983 Value::Int(_) | Value::Float(_) => Some(2),
9984 Value::Str(_) => Some(3),
9985 Value::Closure(_) | Value::Native(_) => Some(4),
9986 // tables and full userdata carry their own metatable; threads and
9987 // light userdata have none (PUC keeps a shared per-type mt slot for
9988 // light, but luna doesn't expose it — no test gates on it yet).
9989 Value::Table(_) | Value::Coro(_) | Value::Userdata(_) | Value::LightUserdata(_) => None,
9990 }
9991}
9992
9993/// A number operand as-is; strings stay non-numbers (5.4+ `tonumberns`).
9994fn as_number(v: Value) -> Option<Num> {
9995 match v {
9996 Value::Int(i) => Some(Num::Int(i)),
9997 Value::Float(f) => Some(Num::Float(f)),
9998 _ => None,
9999 }
10000}
10001
10002/// Arithmetic (not bitwise) on two numbers, PUC `luaO_rawarith`: integer
10003/// results for integer operands except `/` and `^`. The error is the
10004/// message of a zero integer divisor.
10005pub(crate) fn arith_num(
10006 version: LuaVersion,
10007 op: ArithOp,
10008 ln: Num,
10009 rn: Num,
10010) -> Result<Value, &'static str> {
10011 use ArithOp::*;
10012 Ok(match (op, ln, rn) {
10013 (Add, Num::Int(a), Num::Int(b)) => Value::Int(a.wrapping_add(b)),
10014 (Sub, Num::Int(a), Num::Int(b)) => Value::Int(a.wrapping_sub(b)),
10015 (Mul, Num::Int(a), Num::Int(b)) => Value::Int(a.wrapping_mul(b)),
10016 (IDiv, Num::Int(a), Num::Int(b)) => {
10017 if b == 0 {
10018 return Err("attempt to divide by zero");
10019 }
10020 let mut q = a.wrapping_div(b);
10021 if (a ^ b) < 0 && q.wrapping_mul(b) != a {
10022 q -= 1;
10023 }
10024 Value::Int(q)
10025 }
10026 (Mod, Num::Int(a), Num::Int(b)) => {
10027 if b == 0 {
10028 return Err("attempt to perform 'n%0'");
10029 }
10030 let mut m = a.wrapping_rem(b);
10031 if m != 0 && (m ^ b) < 0 {
10032 m += b;
10033 }
10034 Value::Int(m)
10035 }
10036 (Add, a, b) => Value::Float(a.as_f64() + b.as_f64()),
10037 (Sub, a, b) => Value::Float(a.as_f64() - b.as_f64()),
10038 (Mul, a, b) => Value::Float(a.as_f64() * b.as_f64()),
10039 (Div, a, b) => Value::Float(a.as_f64() / b.as_f64()),
10040 // 5.4+ `luai_numpow` squares by multiplying, which can differ from
10041 // the C library's `pow` in the last bit
10042 (Pow, a, b) => {
10043 let (a, b) = (a.as_f64(), b.as_f64());
10044 Value::Float(if b == 2.0 && version >= LuaVersion::Lua54 {
10045 a * a
10046 } else {
10047 a.powf(b)
10048 })
10049 }
10050 (IDiv, a, b) => Value::Float((a.as_f64() / b.as_f64()).floor()),
10051 (Mod, a, b) => Value::Float(float_mod(version, a.as_f64(), b.as_f64())),
10052 (BAnd | BOr | BXor | Shl | Shr, ..) => unreachable!("bitwise op in arith_num"),
10053 })
10054}
10055
10056/// A number's integer value, if it has one.
10057fn int_of(n: Num) -> Option<i64> {
10058 match n {
10059 Num::Int(i) => Some(i),
10060 Num::Float(f) => crate::runtime::value::f2i_exact(f),
10061 }
10062}
10063
10064/// A number, or a numeric string read as a float (5.2).
10065fn coerce_num_float(v: Value) -> Option<Num> {
10066 match v {
10067 Value::Str(s) => numeric::str2num(s.as_bytes(), false, true),
10068 v => as_number(v),
10069 }
10070}
10071
10072/// Number, or string coerced to number (5.3 string-arith coercion).
10073fn coerce_num(v: Value) -> Option<Num> {
10074 match v {
10075 Value::Str(s) => numeric::str2num(s.as_bytes(), true, true),
10076 v => as_number(v),
10077 }
10078}
10079
10080/// A number, or a numeric string read by C `strtod` (5.1 `luaO_str2d`).
10081fn coerce_num_51(v: Value) -> Option<Num> {
10082 match v {
10083 Value::Str(s) => numeric::strtod_str(s.as_bytes()).map(Num::Float),
10084 v => as_number(v),
10085 }
10086}
10087
10088/// A string's numeric value as the dialect converts it: 5.1 runs C
10089/// `strtod` over it (so `inf`/`nan` count and a NUL ends it), 5.2 has its
10090/// own reader but still only floats, 5.3+ `luaO_str2num` with integers.
10091pub(crate) fn str_to_num(s: &[u8], version: LuaVersion) -> Option<Num> {
10092 match version {
10093 LuaVersion::Lua51 => numeric::strtod_str(s).map(Num::Float),
10094 LuaVersion::Lua52 => numeric::str2num(s, false, true),
10095 _ => numeric::str2num(s, true, true),
10096 }
10097}
10098
10099/// Lua shifts: logical on 64 bits; |shift| ≥ 64 yields 0; negative shifts
10100/// reverse direction.
10101fn shift_left(a: i64, b: i64) -> i64 {
10102 if b < 0 {
10103 if b <= -64 {
10104 0
10105 } else {
10106 ((a as u64) >> (-b as u32)) as i64
10107 }
10108 } else if b >= 64 {
10109 0
10110 } else {
10111 ((a as u64) << (b as u32)) as i64
10112 }
10113}
10114
10115/// i < f, exactly (PUC LTintfloat shape).
10116fn int_lt_float(i: i64, f: f64) -> bool {
10117 if f.is_nan() {
10118 return false;
10119 }
10120 if f >= 9_223_372_036_854_775_808.0 {
10121 return true;
10122 }
10123 if f < -9_223_372_036_854_775_808.0 {
10124 return false;
10125 }
10126 let ff = f.floor();
10127 let fi = ff as i64;
10128 if f == ff { i < fi } else { i <= fi }
10129}
10130
10131/// i <= f, exactly.
10132fn int_le_float(i: i64, f: f64) -> bool {
10133 if f.is_nan() {
10134 return false;
10135 }
10136 if f >= 9_223_372_036_854_775_808.0 {
10137 return true;
10138 }
10139 if f < -9_223_372_036_854_775_808.0 {
10140 return false;
10141 }
10142 i <= f.floor() as i64
10143}
10144
10145/// Clip a numeric `for` limit to the integer range (PUC forlimit). Returns
10146/// (clipped limit, loop-is-empty).
10147fn int_for_limit(limit: Num, init: i64, step: i64) -> (i64, bool) {
10148 match limit {
10149 Num::Int(l) => {
10150 let empty = if step > 0 { init > l } else { init < l };
10151 (l, empty)
10152 }
10153 Num::Float(f) => {
10154 // PUC `forlimit` treats NaN like a limit below the integer
10155 // range (`0 < flim` is false): no run upward, down to minint
10156 // otherwise.
10157 if f.is_nan() {
10158 return if step > 0 {
10159 (0, true)
10160 } else {
10161 (i64::MIN, false)
10162 };
10163 }
10164 if step > 0 {
10165 if f >= 9_223_372_036_854_775_808.0 {
10166 (i64::MAX, false)
10167 } else {
10168 let l = f.floor();
10169 if l < -9_223_372_036_854_775_808.0 {
10170 (i64::MIN, true)
10171 } else {
10172 let li = l as i64;
10173 (li, init > li)
10174 }
10175 }
10176 } else if f <= -9_223_372_036_854_775_808.0 {
10177 (i64::MIN, false)
10178 } else {
10179 let l = f.ceil();
10180 if l >= 9_223_372_036_854_775_808.0 {
10181 // PUC forlimit: a positive limit beyond the integer range
10182 // is unreachable for a decreasing loop — empty.
10183 (i64::MAX, true)
10184 } else {
10185 let li = l as i64;
10186 (li, init < li)
10187 }
10188 }
10189 }
10190 }
10191}
10192
10193impl Vm {
10194 /// PUC's debug-API placeholder for an unnamed vararg slot returned by
10195 /// `debug.getlocal(_, -n)`. 5.2/5.3 spelled it `"(*vararg)"`; 5.4
10196 /// dropped the asterisk in favour of `"(vararg)"`. db.lua 5.2 :189 /
10197 /// 5.3 :195 / 5.4 :286 baseline on their respective form.
10198 pub(crate) fn vararg_locvar_name(&self) -> &'static str {
10199 if matches!(self.version, LuaVersion::Lua52 | LuaVersion::Lua53) {
10200 "(*vararg)"
10201 } else {
10202 "(vararg)"
10203 }
10204 }
10205
10206 /// PUC's debug-API placeholder for an unnamed temporary on a C
10207 /// activation. 5.2/5.3 reported `"(*temporary)"`; 5.4 switched to
10208 /// `"(C temporary)"`. db.lua 5.2 :288, 5.3 :312, 5.4 :404 each pin
10209 /// their spelling.
10210 pub(crate) fn temporary_locvar_name(&self) -> &'static str {
10211 if matches!(
10212 self.version,
10213 LuaVersion::Lua51 | LuaVersion::Lua52 | LuaVersion::Lua53
10214 ) {
10215 // PUC 5.1's `findlocal` C-frame branch reported `(*temporary)`
10216 // (db.lua :228 pins it). 5.2/5.3 kept the spelling, 5.4 changed
10217 // to `(C temporary)`.
10218 "(*temporary)"
10219 } else {
10220 "(C temporary)"
10221 }
10222 }
10223
10224 /// PUC's debug-API placeholder for an unnamed Lua-frame temporary
10225 /// (an arithmetic intermediate sitting past the last named local on a
10226 /// live register slot). 5.2/5.3 reported `"(*temporary)"`; 5.4 dropped
10227 /// the asterisk to `"(temporary)"`. db.lua 5.3 :786, 5.4 :966 pin the
10228 /// spelling.
10229 pub(crate) fn lua_temporary_locvar_name(&self) -> &'static str {
10230 if matches!(
10231 self.version,
10232 LuaVersion::Lua51 | LuaVersion::Lua52 | LuaVersion::Lua53
10233 ) {
10234 "(*temporary)"
10235 } else {
10236 "(temporary)"
10237 }
10238 }
10239
10240 /// PUC `pushglobalfuncname`: walk `package.loaded` to depth 2 looking for a
10241 /// native whose function pointer matches `target`, and return its qualified
10242 /// name (e.g. `"table.sort"`). A `_G.X` match is stripped to `"X"`. Returns
10243 /// `None` if no match is found. Used by `arg_error` when the running native
10244 /// was invoked from another native (PUC `ar.name == NULL` at level 0).
10245 pub(crate) fn pushglobalfuncname(
10246 &mut self,
10247 target: crate::runtime::value::NativeFn,
10248 ) -> Option<String> {
10249 let pkg_k = Value::Str(self.heap.intern(b"package"));
10250 let pkg = match self.globals().get(pkg_k) {
10251 Value::Table(t) => t,
10252 _ => return None,
10253 };
10254 let loaded_k = Value::Str(self.heap.intern(b"loaded"));
10255 let loaded = match pkg.get(loaded_k) {
10256 Value::Table(t) => t,
10257 _ => return None,
10258 };
10259 let matches = |v: Value| -> bool {
10260 matches!(v, Value::Native(nc) if std::ptr::fn_addr_eq(nc.f, target))
10261 };
10262 let mut k = Value::Nil;
10263 while let Ok(Some((nk, nv))) = loaded.next(k) {
10264 k = nk;
10265 let Value::Str(outer) = nk else { continue };
10266 let outer = String::from_utf8_lossy(outer.as_bytes()).into_owned();
10267 if matches(nv) {
10268 return Some(if outer == "_G" { String::new() } else { outer });
10269 }
10270 if let Value::Table(inner_t) = nv {
10271 let mut k2 = Value::Nil;
10272 while let Ok(Some((nk2, nv2))) = inner_t.next(k2) {
10273 k2 = nk2;
10274 if matches(nv2)
10275 && let Value::Str(inner) = nk2
10276 {
10277 let inner = String::from_utf8_lossy(inner.as_bytes()).into_owned();
10278 return Some(if outer == "_G" {
10279 inner
10280 } else {
10281 format!("{outer}.{inner}")
10282 });
10283 }
10284 }
10285 }
10286 }
10287 None
10288 }
10289
10290 /// How the caller named the running native (PUC `lua_getinfo("n")` at
10291 /// level 0): `None` when it gives no name, as when the caller is C.
10292 pub(crate) fn running_call_name(&self) -> Option<(&'static str, String)> {
10293 let ts = self.thread_stack(None);
10294 if ts.levels.is_empty() {
10295 return None;
10296 }
10297 self.level_name(&ts, 0)
10298 }
10299
10300 /// Read an upvalue cell of a closure (debug.getupvalue).
10301 pub(crate) fn upvalue_value(&self, cl: Gc<LuaClosure>, idx: usize) -> Value {
10302 match cl.upvals()[idx].state() {
10303 UpvalState::Open { slot, thread } => self.read_slot(slot, thread),
10304 UpvalState::Closed(v) => v,
10305 }
10306 }
10307
10308 /// Write an upvalue cell of a closure (debug.setupvalue).
10309 pub(crate) fn upvalue_set_value(&mut self, cl: Gc<LuaClosure>, idx: usize, v: Value) {
10310 let uv = cl.upvals()[idx];
10311 match uv.state() {
10312 UpvalState::Open { slot, thread } => self.write_slot(slot, thread, v),
10313 UpvalState::Closed(_) => {
10314 // 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).
10315 unsafe { uv.as_mut() }.set_closed(v);
10316 self.heap
10317 .barrier_forward(uv.as_ptr() as *mut crate::runtime::heap::GcHeader, v);
10318 }
10319 }
10320 }
10321}
10322
10323// ────────────────────────────────────────────────────────────────────
10324// AOT trace dispatch install.
10325//
10326// The deploy-side resolver in `luna-runtime-helpers` walks the binary's
10327// trace-meta section after `vm.load`, resolves each entry's
10328// `(proto_hash, head_pc, fn_ptr)` triple against the loaded chunk's
10329// proto tree, and pushes a `CompiledTrace` onto the matching Proto's
10330// `traces` Vec via [`Vm::install_aot_trace`] below. The existing
10331// trace-dispatch loop (this file's `cl.proto.traces.borrow().iter()
10332// .find(|t| t.head_pc == pc && t.dispatchable)`) then fires the AOT
10333// mcode without further plumbing — same code path the runtime JIT
10334// uses.
10335//
10336// Why a separate impl block: keeps the AOT API surface (one fn) easy
10337// to locate when grep'ing for `install_aot_trace`, without dragging
10338// the 8500-line `impl Vm` block above.
10339// ────────────────────────────────────────────────────────────────────
10340
10341impl Vm {
10342 /// Install a precompiled
10343 /// `CompiledTrace` onto `proto.traces` so the interp dispatcher
10344 /// fires it at the trace's `head_pc`. This is the runtime install
10345 /// API the deploy-side `luna-runtime-helpers` resolver calls once
10346 /// per AOT-emitted trace meta entry, after looking up `proto` by
10347 /// stable hash (see `crate::runtime::function::Proto::stable_hash`).
10348 ///
10349 /// # What this does
10350 ///
10351 /// Pushes `trace` onto `proto.traces` via the existing `RefCell`.
10352 /// The trace's `entry` fn ptr must already point at runnable
10353 /// machine code (the AOT linker resolved the symbol at link time;
10354 /// the deploy resolver passes the address verbatim).
10355 ///
10356 /// # What this does NOT do
10357 ///
10358 /// - **No deduplication.** Calling twice with the same `head_pc`
10359 /// pushes two entries; the dispatcher's `find` will pick the
10360 /// first match. The deploy resolver is responsible for not
10361 /// double-installing.
10362 /// - **No invalidation of the runtime JIT cache.** If the runtime
10363 /// JIT later records + compiles a trace for the same
10364 /// `(proto, head_pc)`, both coexist on `proto.traces` and the
10365 /// dispatcher's `find` picks whichever appears first. AOT
10366 /// traces install before any runtime recording is possible
10367 /// (resolver runs before `vm.load` returns its first closure),
10368 /// so AOT traces win the race for the same site.
10369 /// - **No coverage gating.** AOT traces are trusted by
10370 /// construction — they were validated at compile time. Setting
10371 /// `dispatchable: false` on the input would silently disable
10372 /// dispatch; the caller controls that flag.
10373 ///
10374 /// # Safety / soundness
10375 ///
10376 /// `trace.entry` is an `unsafe extern "C" fn` (mmap'd or linked
10377 /// machine code). Soundness contract:
10378 ///
10379 /// - The fn pointer must remain valid for the `Vm`'s lifetime.
10380 /// In the AOT-binary deploy shape this is trivially satisfied —
10381 /// the fn lives in the binary's `.text`.
10382 /// - `trace.entry_tags` / `exit_tags` / `window_size` must match
10383 /// what the trace's IR actually compiled against; the dispatcher
10384 /// uses them to marshal `reg_state` in and out without further
10385 /// validation. A mismatch corrupts vm.stack.
10386 ///
10387 /// The AOT pipeline (`luna-aot`) is responsible for ensuring these
10388 /// invariants hold; this fn is a plain push — no validation that
10389 /// would slow the dispatcher's hot path either.
10390 pub fn install_aot_trace(
10391 &mut self,
10392 proto: crate::runtime::Gc<crate::runtime::function::Proto>,
10393 trace: crate::jit::trace::CompiledTrace,
10394 ) {
10395 let _ = self; // resolver passes &mut Vm for symmetry with future
10396 // pending-install + hash-walk variants; nothing on `self` to
10397 // mutate today because the install target lives on the Proto.
10398 proto.traces.borrow_mut().push(TArc::new(trace));
10399 }
10400
10401 /// Walk the proto tree
10402 /// reachable from `root` and return `(proto, stable_hash)` pairs
10403 /// for every Proto found. Used by the deploy-side resolver to
10404 /// match AOT-emitted `proto_hash` keys against the freshly
10405 /// `undump`'d chunk's protos.
10406 ///
10407 /// The walk is BFS over `Proto.protos`. Same-Proto deduplication
10408 /// is done via `Gc::as_ptr` identity — a Proto re-referenced from
10409 /// multiple nested closures (rare; the cache field would catch
10410 /// the closure-side dedup, not the Proto side) is reported once.
10411 ///
10412 /// # Why on `&Vm` and not a free fn
10413 ///
10414 /// Keeps the AOT install API discoverable on the Vm surface —
10415 /// `vm.collect_proto_hashes(root)` reads naturally next to
10416 /// `vm.install_aot_trace(proto, trace)`. Doesn't actually touch
10417 /// any Vm field, so `&self` (read-only) is enough.
10418 pub fn collect_proto_hashes(
10419 &self,
10420 root: crate::runtime::Gc<crate::runtime::function::Proto>,
10421 ) -> Vec<(
10422 crate::runtime::Gc<crate::runtime::function::Proto>,
10423 [u8; 16],
10424 )> {
10425 let _ = self;
10426 let mut out = Vec::new();
10427 let mut seen: std::collections::HashSet<*const crate::runtime::function::Proto> =
10428 std::collections::HashSet::new();
10429 let mut queue: std::collections::VecDeque<
10430 crate::runtime::Gc<crate::runtime::function::Proto>,
10431 > = std::collections::VecDeque::new();
10432 queue.push_back(root);
10433 while let Some(p) = queue.pop_front() {
10434 let key = p.as_ptr() as *const _;
10435 if !seen.insert(key) {
10436 continue;
10437 }
10438 out.push((p, p.stable_hash()));
10439 for &child in p.protos.iter() {
10440 queue.push_back(child);
10441 }
10442 }
10443 out
10444 }
10445}
10446
10447/// Recordings of one trace head that may fail to compile before the head
10448/// is no longer recorded (LuaJIT likewise blacklists a trace start after
10449/// repeated failures). A few tries, since a later recording can see
10450/// different register kinds.
10451const MAX_TRACE_COMPILE_FAILURES: u8 = 3;
10452
10453fn note_trace_compile_failure(proto: Gc<crate::runtime::function::Proto>, head_pc: u32) {
10454 let mut failures = proto.trace_compile_failures.borrow_mut();
10455 match failures.iter_mut().find(|(pc, _)| *pc == head_pc) {
10456 Some((_, n)) => *n = n.saturating_add(1),
10457 None => failures.push((head_pc, 1)),
10458 }
10459}
10460
10461fn trace_head_abandoned(proto: Gc<crate::runtime::function::Proto>, head_pc: u32) -> bool {
10462 proto
10463 .trace_compile_failures
10464 .borrow()
10465 .iter()
10466 .any(|&(pc, n)| pc == head_pc && n >= MAX_TRACE_COMPILE_FAILURES)
10467}