luna_core/vm/exec.rs
1// CARVE-OUT: pre-existing god file, shrinking on every touch
2//! The interpreter. Dispatch is a plain match over opcodes. Lua→Lua calls
3//! share one loop and never recurse the Rust stack; only native↔Lua boundaries do (e.g. pcall).
4//!
5//! Varargs follow 5.5 semantics: a vararg call materializes a vararg table
6//! (fields 1..n plus "n") kept in the function's own stack slot; `...`
7//! expands from it and `...name` binds it. 5.1 LUAI_COMPAT_VARARG also
8//! materializes a local `arg` table (see `proto.has_compat_vararg_arg`).
9
10use crate::frontend::SyntaxError;
11use crate::jit::send_compat::TArc;
12use crate::numeric::{self, Num};
13use crate::runtime::heap::GcHeader;
14use crate::runtime::{
15 AfterClose, CallFrame, CloseCont, ContKind, Coro, CoroStatus, Frame, Gc, Heap, LuaClosure,
16 MetaAction, MetaCont, NativeClosure, NativeCont, Table, TableError, UpvalState, Upvalue, Value,
17};
18use crate::version::LuaVersion;
19use crate::vm::builtins::{nat_host_xpcall, nat_pairs, nat_pcall, nat_xpcall};
20use crate::vm::callstack::DbgKind;
21use crate::vm::error::LuaError;
22use crate::vm::isa::{Inst, Op};
23
24mod num;
25use num::*;
26pub(crate) use num::{ArithOp, arith_num, str_to_num};
27
28/// A Lua virtual machine: one OS thread's worth of Lua state.
29///
30/// # Threading model
31///
32/// `Vm` is **`!Send + !Sync`**. The GC uses `Gc<T> = NonNull<T>` over
33/// an intrusive mark-sweep heap (not `Rc<RefCell<T>>`), and the trace
34/// JIT side-table uses `Rc<CompiledTrace>` — both single-threaded by
35/// design. Embedders that want concurrency spawn one `Vm` per OS
36/// thread (or per single-thread Tokio worker) and exchange data via
37/// channels. See [`docs/threading.md`](../../docs/threading.md) for
38/// canonical embedding patterns including Tokio `current_thread`,
39/// `LocalSet` on multi-thread, and `Vm`-per-OS-thread + channels.
40///
41/// The constraint is enforced at compile time:
42///
43/// ```compile_fail
44/// fn must_be_send<T: Send>() {}
45/// must_be_send::<luna_core::Vm>(); // error[E0277]: `Vm` cannot be sent between threads safely
46/// ```
47///
48/// A future `feature = "send"` will gate an
49/// opt-in `Arc<RwLock<T>>` mode with a hard ≤8% perf regression
50/// budget.
51pub struct Vm {
52 /// The GC heap owned by this VM. Embedders normally interact via the
53 /// `Vm` methods (`load` / `call_value` / `set_global` / …) rather than
54 /// the heap directly.
55 pub heap: Heap,
56 pub(crate) stack: Vec<Value>,
57 pub(crate) frames: Vec<CallFrame>,
58 /// How many `__call` metamethods were resolved to reach each Lua frame,
59 /// by index into `frames`; each adds one argument (PUC 5.5 `CIST_CCMT`
60 /// bits, reported as `getinfo("t").extraargs`). Written whenever a Lua
61 /// frame is pushed, so the entry for a live frame is always its own;
62 /// entries past `frames.len()` are stale and never read. Kept beside
63 /// `frames` rather than in `Frame` because `Frame` is public with all
64 /// fields public, where a new field is a breaking change.
65 pub(crate) frame_ccmt: Vec<u8>,
66 /// Shadow of `self.frames.len()`. Synced on every push/pop in the
67 /// `frames_push_sync`/`frames_pop_sync` helpers (debug-asserted on
68 /// use). Not consumed by readers yet; it is scaffolding for replacing
69 /// `frames: Vec<CallFrame>` with a flat `[CallFrame; MAX_FRAMES]`
70 /// indexed by frames_top.
71 frames_top: u32,
72 /// open upvalues, sorted ascending by stack slot
73 open_upvals: Vec<(u32, Gc<Upvalue>)>,
74 /// to-be-closed slots, ascending
75 tbc: Vec<u32>,
76 /// logical stack top for multi-result sequences
77 pub(crate) top: u32,
78 globals: Gc<Table>,
79 /// shared metatable for all strings (populated by the string lib)
80 /// per-basic-type metatables (PUC luaT): indexed by `type_mt_slot`
81 /// (0 nil, 1 boolean, 2 number, 3 string, 4 function); tables carry their
82 /// own. Settable via debug.setmetatable.
83 type_mt: [Option<Gc<Table>>; 5],
84 /// pre-interned metamethod event names, indexed by `Mm`
85 mm_names: Vec<Gc<crate::runtime::LuaStr>>,
86 /// native↔Lua nesting depth (PUC C-stack guard analogue)
87 c_depth: u32,
88 /// number of live pcall/xpcall continuation frames on the running thread
89 /// (PUC counts these against nCcalls). Bounds protected-call recursion the
90 /// way `c_depth` bounds call_value recursion. Per-thread: saved/restored
91 /// with the coroutine context, since continuations survive a yield.
92 pcall_depth: u32,
93 /// number of non-yieldable C calls in flight on the running thread (PUC's
94 /// `L->nny`). A library callback that runs via synchronous Rust recursion
95 /// (sort comparator, gsub replacement) cannot be continued across a yield,
96 /// so it bumps this for its duration; `coroutine.yield` inside hits the
97 /// C-call boundary and errors. Always 0 at a suspend point (a yield can
98 /// never cross such a call), so it needs no per-thread save/restore.
99 nny: u32,
100 /// Nonzero while an xpcall message handler is on the Rust stack. Used so a
101 /// stack-overflow that surfaces *inside* the handler is reported as PUC's
102 /// "error in error handling" (LUA_ERRERR + `luaD_seterrorobj`), not the
103 /// plain "stack overflow" — errors.lua :606's `checkerr("error handling",
104 /// loop)` then matches. PUC tracks this via the soft-cap window
105 /// `nCcalls >= MAXCCALLS/10*11`; luna's c_depth is strict, so we mark the
106 /// scope explicitly.
107 pub(crate) msgh_depth: u32,
108 /// set by a coroutine closing itself (`coroutine.close()` on the running
109 /// thread): the to-be-closed handlers have already run; the thread must now
110 /// terminate. `Some(None)` is a clean close, `Some(Some(e))` a handler
111 /// raised `e`. Checked by `exec_with`/`resume_coro` to propagate (not
112 /// unwind, so a protecting pcall cannot catch it) the termination.
113 terminating: Option<Option<Value>>,
114 /// xoshiro256** state (math.random)
115 rng: [u64; 4],
116 /// VM creation time (os.clock)
117 started: std::time::Instant,
118 version: LuaVersion,
119 /// error object being threaded through a chain of __close handlers; a GC
120 /// root for the duration (a handler may trigger collection)
121 closing_err: Option<Value>,
122 /// the coroutine whose context is currently live in the fields above;
123 /// `None` while the main thread runs
124 pub(crate) current: Option<Gc<crate::runtime::Coro>>,
125 /// the main thread's saved execution context while a coroutine runs
126 main_ctx: Option<SavedCtx>,
127 /// set by `coroutine.yield` to suspend the running coroutine: the yielded
128 /// values plus the slot/result-count needed to finish the yielding call on
129 /// the next resume. Checked by `exec` to propagate (not unwind) on yield.
130 yielding: Option<(Vec<Value>, u32, i32)>,
131 /// results expected by the in-flight native call (so `yield` knows how many
132 /// values its call site wants when it suspends)
133 native_nresults: i32,
134 /// identity object for the main thread, returned by `coroutine.running`
135 /// (the main thread's context lives in the VM fields / `main_ctx`, not here)
136 main_coro: Option<Gc<Coro>>,
137 /// `collectgarbage` mode name ("incremental"/"generational"). The collector
138 /// itself is still stop-the-world mark-sweep; this tracks the mode so mode
139 /// switches report the previous one, as PUC does.
140 gc_mode: &'static str,
141 /// the live-register boundary of the running thread for GC rooting (PUC's
142 /// `L->top`): set precisely at each GC safe point so freed temporary
143 /// registers above it are not rooted. Without this the collector roots the
144 /// whole stack window, pinning weak-table values stranded in stale temps
145 /// (e.g. closure.lua's `while x[1]` GC-detection loop).
146 pub(crate) gc_top: u32,
147 /// `collectgarbage("param", name [,value])` pacing parameters. The collector
148 /// is still stop-the-world, so these are stored/returned for API fidelity
149 /// (PUC round-trips them via `setparam`/`getparam`). Defaults mirror PUC's
150 /// `LUAI_GC*` knobs: pause=200, stepmul=100, stepsize=13.
151 gc_pause: i64,
152 gc_stepmul: i64,
153 gc_stepsize: i64,
154 /// `collectgarbage`'s parameters as the dialect stores them; they set
155 /// the three knobs above through `set_gc_pacing`.
156 pub(crate) gc_params: crate::vm::lib_gc::GcParams,
157 /// true while `__gc` finalizers are being run, so a finalizer that calls
158 /// `collectgarbage` gets a no-op (PUC's non-reentrancy: lua_gc returns -1 →
159 /// `collectgarbage` yields fail).
160 gc_finalizing: bool,
161 /// C ABI scratch (`capi` module): the host-visible value stack that C
162 /// callers operate on via `lua_pushinteger` / `lua_tostring` / etc.
163 /// Kept here (instead of in a separate `LuaState` wrapper) so the
164 /// trampoline that bridges to a `LuaCFunction` can safely cast the
165 /// Vm pointer it already holds to the public `*mut LuaState` type
166 /// without any aliasing of `&mut Vm` against `&mut LuaState.vm`.
167 pub capi_stack: Vec<crate::runtime::Value>,
168 /// Pinned CString backing the pointer last returned by `lua_tostring`;
169 /// valid until the next `lua_tostring` on the same Vm.
170 pub capi_cstr_pin: Option<std::ffi::CString>,
171 /// PUC 5.4+ warning system. Lua manual §6.1 `warn`: emitted messages
172 /// concatenate across continuation calls until a non-`tocont` call
173 /// flushes; the default warnf recognises `@on`/`@off` control messages
174 /// and starts disabled. luna's `emit_warn` mirrors the default warnf
175 /// behaviour and 5.4+ `__gc` errors are routed through it (5.1–5.3
176 /// keep the older raise semantics).
177 pub(crate) warn_state: WarnState,
178 pub(crate) warn_buf: Vec<u8>,
179 /// Embedding cooperative budget: a per-Vm tick counter that the run
180 /// loop decrements once per dispatch turn. When it hits zero the loop
181 /// raises a catchable "instruction budget exceeded" error so the embedder
182 /// can yield control back to its caller (short-script eval, game
183 /// frame budgets). `None` = unbounded; reset on each call via
184 /// `set_instr_budget`.
185 pub(crate) instr_budget: Option<i64>,
186 // JIT-specific state lives in the `JitState` sidecar; see `self.jit`
187 // below and `crate::vm::jit_state` for field docs.
188 /// Bytecode-loading gate. Default `true`. Sandbox embedders should
189 /// call `set_bytecode_loading(false)` so `load`/`loadstring` reject
190 /// precompiled chunks (which bypass the parser's depth / opcode
191 /// limits). When `false`, the loader rejects any source whose first
192 /// byte is the bytecode signature `\27` ("`\27Lua`").
193 pub(crate) bytecode_loading: bool,
194 /// PUC bytecode-loading gate. Default `false` — PUC `.luac` files are
195 /// a strictly larger trust surface than luna's own dump format
196 /// (third-party toolchain bugs, malformed chunks, unknown opcode
197 /// shapes). When `true`, the loader routes `\x1bLua\x{51..55}` inputs
198 /// through the per-dialect PUC translators in `crate::vm::dump::puc`.
199 /// Embedder toggles via `set_puc_bytecode_loading`.
200 pub(crate) puc_bytecode_loading: bool,
201 /// Byte budget for source fed into `load` / `loadstring` / `Vm::load`.
202 /// Default [`Vm::DEFAULT_LOADER_INPUT_BUDGET`] (256 MiB). When the
203 /// accumulated reader output (`load(f, ...)`) or a one-shot `&[u8]`
204 /// source exceeds this, the loader returns the PUC-shaped
205 /// `not enough memory` error before the host allocator is asked to
206 /// hold the next chunk. Defends against `heavy.lua::loadrep`-style
207 /// 7 GB+ feeder loops that would otherwise SIGSEGV when `Vec::push`
208 /// crosses `isize::MAX` or the host runs out of RAM.
209 /// Embedders that genuinely need to load > 256 MiB sources widen the
210 /// cap via [`Vm::set_loader_input_budget`].
211 pub(crate) loader_input_budget: usize,
212 /// In-process log of fully-emitted warnings (each entry = one flushed
213 /// message, sans the "Lua warning: " prefix and trailing newline). Lets
214 /// tests assert what was warned without scraping stderr.
215 pub(crate) warn_log: Vec<Vec<u8>>,
216 /// PUC's `LUA_REGISTRYINDEX` table — a single Lua table the debug library
217 /// exposes via `debug.getregistry`. Used to hold `_HOOKKEY` (the weak-key
218 /// table PUC's `db_sethook` keys per-thread hooks under). luna stores hook
219 /// state directly in `Vm.hook`/`Coro.hook`, so the entry is largely a
220 /// shape stub for db.lua :328; if other registry-keyed APIs land later
221 /// they can share this table.
222 pub(crate) registry: Option<Gc<Table>>,
223 /// the shared `FILE*` metatable for io file handles (PUC's LUA_FILEHANDLE
224 /// registry entry); attached to every file userdata the io library makes
225 pub(crate) file_mt: Option<Gc<Table>>,
226 /// io library default input/output streams (PUC registry IO_INPUT/IO_OUTPUT)
227 pub(crate) io_input: Option<Gc<crate::runtime::Userdata>>,
228 pub(crate) io_output: Option<Gc<crate::runtime::Userdata>>,
229 /// `io.stdin` as the io library made it, whatever the script later does
230 /// to the `io` table: the stream a host's line reads share
231 pub(crate) io_stdin: Option<Gc<crate::runtime::Userdata>>,
232 /// lua.c's `-E`: libraries opened from now on ignore the environment
233 pub(crate) ignore_env: bool,
234 /// the running thread's debug hook state (`debug.sethook`); per-thread,
235 /// swapped with the execution context on a coroutine resume/yield
236 pub(crate) hook: HookState,
237 /// true while the hook itself runs, so its own execution fires no events
238 /// (PUC clears the mask for the duration)
239 pub(crate) in_hook: bool,
240 /// arms the next Lua frame's `tailcalls` count (PUC `ci->u.l.tailcalls`),
241 /// consumed by `push_frame`. `OP_TailCall` sets it to the caller's
242 /// own tailcalls + 1 before begin_call so deeply tail-recursive chains
243 /// accumulate the count instead of capping at 1.
244 pub(crate) pending_tailcalls: u32,
245 /// arms the next Lua frame's `ccmt` (its `__call` chain length), consumed
246 /// by `push_frame`. `OP_TailCall` sets it to the reused activation's
247 /// count; `begin_call` otherwise sets the chain it just resolved.
248 pending_ccmt: u8,
249 /// Name of the C native that just propagated an error (captured before
250 /// the native is popped from `running_natives`). Lets a dying coroutine
251 /// preserve `[C]: in function '<name>'` at the top of its traceback
252 /// snapshot — PUC walks `luaG_funcnamefrompc` over a still-live ci, but
253 /// luna's native frames are off-stack so we stash the name explicitly.
254 pub(crate) errored_natives: Vec<crate::vm::callstack::ErroredNative>,
255 /// Frames below this index are out of reach of the error handler of
256 /// an `xpcall` (PUC `L->errfunc`): a protected call made from Rust — a
257 /// finalizer, the handler itself — starts a fresh `errfunc` scope.
258 pub(crate) msgh_floor: usize,
259 /// The message handler that is running, if any. PUC's `luaG_errormsg`
260 /// calls the handler with `L->errfunc` still set, so an error inside
261 /// the handler (and not caught within it) calls the handler again, at
262 /// the point of that error. Per thread, like `L->errfunc`.
263 pub(crate) msgh_running: Option<Value>,
264 /// How many message-handler runs have started; lets a run tell whether
265 /// the error it got back was already handled by a nested run.
266 pub(crate) msgh_runs: u64,
267 /// The value the last `xpcall` handler produced for the error in
268 /// flight, so the unwind that carries it to the `xpcall` does not
269 /// run the handler again.
270 pub(crate) msgh_applied: Option<Value>,
271 /// Whether an error nothing catches should keep its traceback: not
272 /// inside a protected call made from Rust, which discards it.
273 pub(crate) keep_error_traceback: bool,
274 /// PUC `CallInfo.u2.transferinfo`: index of the first transferred value
275 /// (relative to the activation's func slot) and the number transferred.
276 /// Set just before firing a call/return hook, read by `getinfo("r")`.
277 pub(crate) hook_ftransfer: u16,
278 pub(crate) hook_ntransfer: u16,
279 /// metamethod event tag (e.g. "close") to attach to the next Lua frame
280 /// pushed by `push_frame`; `close_slots` sets this before calling a
281 /// `__close` handler so `debug.traceback` names it "metamethod 'close'"
282 /// (PUC `CallInfo.u.l.tm`). Single-shot: `push_frame` consumes it.
283 pending_tm: Option<&'static str>,
284 /// `true` when the next `push_frame` is the user hook function itself,
285 /// so `debug.getinfo(1).namewhat` resolves to `"hook"` (PUC
286 /// `CIST_HOOKED`). `run_hook` arms it before dispatching the hook.
287 pending_is_hook: bool,
288 /// traceback of an error nothing in its thread catches, one line per
289 /// stack level, taken where it was raised (see `raise_to_handler`): what
290 /// the host gets from `take_error_traceback`, and what `debug.traceback`
291 /// shows of the coroutine it kills. Cleared on a catch and at host-level
292 /// `call_value` entry (`public_call_depth == 0`).
293 pub(crate) error_traceback: Option<Vec<Vec<u8>>>,
294 /// nesting depth of public `call_value` entries (host vs. internal). The
295 /// outermost entry (depth 0) resets per-error state (`error_traceback`);
296 /// internal calls (e.g. xpcall msgh, sort callback) preserve it.
297 public_call_depth: u32,
298 /// stack of native (`Value::Native`) closures currently running on the
299 /// Rust call stack. `begin_call` pushes the closure before invoking
300 /// `nc.f` and pops on return. Used by `arg_error` to detect a *nested*
301 /// native call (PUC `ar.name == NULL` at level 0 because the level-0
302 /// caller is C, not Lua) and qualify the running function's name via
303 /// `pushglobalfuncname` (e.g. `'sort'` → `'table.sort'`).
304 pub(crate) running_natives: Vec<Gc<NativeClosure>>,
305 /// Parallel to `running_natives`: where each native sits on the value
306 /// and frame stacks, so the debug interface can place it among the Lua
307 /// activations as PUC's CallInfo chain would (see `callstack`).
308 pub(crate) running_native_acts: Vec<crate::vm::callstack::NativeAct>,
309 /// Index into `running_natives` where the running thread's own natives
310 /// begin; the ones below belong to the threads that resumed it.
311 pub(crate) natives_base: usize,
312 /// JIT sidecar. Always present (never `Option`); inert
313 /// when `chunk_compiler` / `trace_compiler` are
314 /// [`crate::jit::NullJitBackend`]. See [`crate::vm::jit_state`].
315 ///
316 /// `#[doc(hidden)] pub` so the `luna` crate's
317 /// `extern "C"` JIT helpers can write `vm.jit.pending_err`
318 /// directly. Not part of the embedder-facing API surface.
319 #[doc(hidden)]
320 pub jit: crate::vm::jit_state::JitState,
321
322 /// Host roots — a `Vec<Value>` traced as an extra GC root set.
323 /// `Lua` facade handles (`LuaFunction`, `LuaTable`, `LuaRoot`) hold
324 /// indices into this vector so the underlying `Gc<T>` stays alive
325 /// across `eval` calls / yield boundaries. Freed slots are recycled
326 /// through `host_roots_free`.
327 pub(crate) host_roots: Vec<crate::vm::host_roots::HostRootSlot>,
328 /// Recycled-slot index pool. `pin_host` pops the
329 /// back if non-empty, else extends `host_roots`. Generation
330 /// overflow at `u32::MAX` retires the slot (NOT pushed here).
331 pub(crate) host_roots_free: Vec<u32>,
332
333 /// GC-rooted scratch stack for `table.sort` (and any other
334 /// builtin that needs a Rust-side `Vec<Value>` to outlive a user
335 /// callback). Each entry is one in-flight working buffer; `gc_roots`
336 /// extends with every contained `Value` so a `collectgarbage()`
337 /// inside the comparator cannot free strings/tables snapshotted
338 /// here. Nested sorts push a new buffer on entry, pop on exit
339 /// (sort.lua's `load(..)(); collectgarbage()` compare callback
340 /// regression).
341 pub(crate) sort_scratch: Vec<Vec<Value>>,
342
343 /// MacroLua compile-time macro registry.
344 /// Pre-populated with built-in macros (`@quote` / `@unquote` /
345 /// `@if` / `@gensym`) at construction time when `version ==
346 /// LuaVersion::MacroLua`; embedders register custom macros via
347 /// [`Vm::define_macro`]. The expander runs once per `load()` call
348 /// between lexing and parsing (only when `is_macro_lua()`).
349 pub(crate) macro_registry: crate::frontend::macro_expander::MacroRegistry,
350
351 /// Per-Vm cache of `Gc<Table>` metatables keyed
352 /// by `TypeId::of::<T>()` for embedder types implementing
353 /// [`crate::vm::userdata_trait::LuaUserdata`]. Populated lazily by
354 /// [`Vm::register_userdata`]; metatables are pinned via
355 /// [`Vm::pin_host`] at registration time so the entry's
356 /// `Gc<Table>` stays live for the rest of the Vm's lifetime.
357 pub(crate) userdata_metatables:
358 std::collections::HashMap<std::any::TypeId, Gc<crate::runtime::table::Table>>,
359
360 /// Classification of the most recent error raised on this Vm.
361 /// Embedders read via [`Vm::error_kind`]; the dispatcher sets it
362 /// at well-known sites (syntax errors, instr-budget trips, native
363 /// callback errors, type errors).
364 pub(crate) last_error_kind: crate::vm::error::LuaErrorKind,
365
366 /// `(source_name, line)` of the most recent error. Set by the
367 /// dispatcher / lexer / parser; cleared when a new call_value
368 /// enters cleanly.
369 pub(crate) last_error_source: Option<(String, u32)>,
370
371 /// When `true`, `instr_budget` exhaustion in
372 /// the dispatcher hot loop yields cooperatively (sets
373 /// [`Vm::host_yield_pending`] + returns a sentinel `Err` walked up
374 /// to `EvalFuture::poll`) instead of returning a real
375 /// "instruction budget exceeded" error. Set by [`Vm::eval_async`]
376 /// for the duration of the future; restored to `false` on
377 /// `Poll::Ready`. The sync `Vm::eval` / `Vm::call_value` paths
378 /// leave it `false` so budget exhaustion stays a real error there.
379 pub(crate) async_mode: bool,
380
381 /// Host waker cloned by `EvalFuture::poll` before driving a slice.
382 /// The dispatcher itself does not call it (the future's poll loop
383 /// does `wake_by_ref` after observing `BudgetExhausted`); it is kept
384 /// so async natives can wake the host directly from a helper future.
385 pub(crate) async_waker: Option<std::task::Waker>,
386
387 /// Per-poll opcode quota loaded into
388 /// `instr_budget` at the start of each `EvalFuture::poll` slice.
389 /// Default 10_000. Tunable via
390 /// [`Vm::set_async_slice`].
391 pub(crate) async_slice_size: i64,
392
393 /// Set by the dispatcher when an async-mode
394 /// budget exhaustion fires; checked by `exec_with` (so the
395 /// sentinel propagates without `unwind` running, mirroring
396 /// `yielding.is_some()`) and by `call_value_impl` (so the call
397 /// frames survive for the next poll). Cleared by `drive_one`
398 /// after translating it to `DispatchOutcome::BudgetExhausted`.
399 pub(crate) host_yield_pending: bool,
400
401 /// Set by the dispatcher's native-call path
402 /// when an async-marked [`NativeClosure`] is invoked under
403 /// `async_mode`. The Vm pauses the dispatcher (same sentinel-Err
404 /// mechanism as `host_yield_pending` — see `exec_with` +
405 /// `call_value_impl`), stashes the in-flight future +
406 /// post-completion context here, and surfaces them to
407 /// `EvalFuture::poll` via `drive_one`. Cleared by `drive_one`
408 /// once the future is moved out into a
409 /// `DispatchOutcome::AsyncNativeAwaiting`.
410 pub(crate) pending_async_native_fut:
411 Option<std::pin::Pin<Box<dyn std::future::Future<Output = Result<u32, LuaError>>>>>,
412
413 /// Companion to `pending_async_native_fut`:
414 /// the `(func_slot, nargs, nresults, gc_top)` quad needed to
415 /// commit the future's eventual `Ok(nret)` back into the calling
416 /// frame's expected result slots. Recorded by the dispatcher;
417 /// consumed by [`Vm::commit_async_native_result`] after the
418 /// future resolves.
419 pub(crate) pending_async_native_ctx: Option<AsyncNativeCallCtx>,
420
421 /// Identifies this Vm to the JIT storages it compiles through
422 /// ([`crate::jit::JitStorage::claim`]).
423 jit_owner_id: u64,
424 /// Storages [`Vm::install_jit_storage`] replaced: code compiled into
425 /// them may still be referenced by this Vm's functions, so they live
426 /// as long as the Vm.
427 retired_jit_storage: Vec<Box<dyn crate::jit::JitStorage>>,
428}
429
430/// Call-site context an in-flight async native
431/// needs preserved across the cooperative-yield boundary.
432///
433/// The dispatcher records this when it routes a `NativeClosure` with
434/// `is_async == true` through the cooperative path; `EvalFuture::poll`
435/// hands it back to [`Vm::commit_async_native_result`] once the
436/// awaited future resolves so `finish_results` (and the post-call GC
437/// checkpoint) can run as if the native had completed synchronously.
438#[derive(Clone, Copy)]
439pub(crate) struct AsyncNativeCallCtx {
440 pub func_slot: u32,
441 /// Recorded for parity with the sync native-call path's
442 /// `native_nresults`/`gc_top` bookkeeping; reserved for hook
443 /// firing + traceback shaping. Not read yet.
444 #[allow(dead_code)]
445 pub nargs: u32,
446 pub nresults: i32,
447 /// Recorded for traceback + GC-root-window checks. The resume path
448 /// reads `Vm.gc_top` directly, so this is unread today; carried so a
449 /// check can confirm the pre-suspend root window matches the
450 /// post-resume one.
451 #[allow(dead_code)]
452 pub gc_top: u32,
453}
454
455/// Per-thread debug hook state (PUC `lua_State` hook/hookmask/basehookcount/
456/// hookcount). `func` is the Lua hook; the booleans are the PUC mask bits.
457#[derive(Clone, Copy, Default)]
458pub struct HookState {
459 /// the hook function (`None` when no hook is installed)
460 pub func: Option<Value>,
461 /// Rust-side debug hook. Fires alongside the Lua hook
462 /// (Rust first); both can be installed simultaneously, but most
463 /// embedders pick one.
464 pub rust_func: Option<RustDebugHook>,
465 /// LUA_MASKCALL — fire on function entry
466 pub call: bool,
467 /// LUA_MASKRET — fire on function return
468 pub ret: bool,
469 /// LUA_MASKLINE — fire on source-line change
470 pub line: bool,
471 /// LUA_MASKCOUNT — fire every `count_base` instructions
472 pub count: bool,
473 /// instruction count between count events (PUC basehookcount)
474 pub count_base: i64,
475 /// instructions left until the next count event (PUC hookcount)
476 pub count_left: i64,
477}
478
479/// Rust-side debug hook callback. Receives the `Vm` plus a
480/// classified event. The callback runs synchronously in the
481/// dispatcher; the hook flag (`in_hook`) is set for its duration so
482/// hook recursion is suppressed.
483pub type RustDebugHook = fn(&mut Vm, RustHookEvent);
484
485/// Classified debug event delivered to a [`RustDebugHook`].
486#[derive(Clone, Copy, Debug, PartialEq, Eq)]
487pub enum RustHookEvent {
488 /// Function entry (`hook_call` analogue).
489 Call,
490 /// Function return (`hook_return` analogue).
491 Return,
492 /// Tail call entry (PUC 5.2+ separates this from a plain Call).
493 TailCall,
494 /// Source-line change (the `u32` is the 1-based line number).
495 Line(u32),
496 /// Instruction count event (fires every `count_base` instructions).
497 Count,
498}
499
500/// Mask flags for [`Vm::set_rust_debug_hook`]. OR these to subscribe
501/// to multiple event categories with a single hook installation.
502pub const HOOK_MASK_CALL: u32 = 1;
503/// Subscribe to function-return events.
504pub const HOOK_MASK_RETURN: u32 = 2;
505/// Subscribe to line-change events.
506pub const HOOK_MASK_LINE: u32 = 4;
507/// Subscribe to instruction-count events.
508pub const HOOK_MASK_COUNT: u32 = 8;
509
510/// A thread's swapped-out execution context (PUC per-thread stack state).
511struct SavedCtx {
512 stack: Vec<Value>,
513 frames: Vec<CallFrame>,
514 frame_ccmt: Vec<u8>,
515 open_upvals: Vec<(u32, Gc<Upvalue>)>,
516 tbc: Vec<u32>,
517 top: u32,
518 pcall_depth: u32,
519 hook: HookState,
520 /// PUC `L->l_gt` — the thread's own globals table. Carried alongside
521 /// the rest of the suspended state so each thread can keep its own
522 /// `setfenv(0, env)` rewire without the swap leaking into another
523 /// thread (5.1 closure.lua :177).
524 globals: Gc<Table>,
525}
526
527/// Outcome of unwinding the call stack on an error (see `Vm::unwind`).
528enum Unwound {
529 /// caught by a pcall/xpcall continuation; resume running its caller
530 Caught,
531 /// caught by a continuation that was the entry-level activation; these are
532 /// the call's (wrapped) results
533 CaughtReturn(Vec<Value>),
534 /// no protecting continuation up to `entry_depth`; propagate the error
535 Propagated(LuaError),
536}
537
538/// Outcome of an index/newindex/comparison fast path: either a directly
539/// computed result, or a metamethod (with the receiver it resolved against) the
540/// caller must invoke — synchronously (C context) or yieldably (VM opcode).
541enum MmOut {
542 /// index → the looked-up value; newindex → done (raw set performed);
543 /// comparison → the boolean result already known
544 Done(Value),
545 /// a metamethod to call; `recv` is the chain element it was found on (the
546 /// extra args — key / value — are supplied by the caller)
547 Mm { func: Value, recv: Value },
548 /// ≤5.3 `a <= b` synthesised via `not __lt(b, a)` when neither operand
549 /// carries `__le` — `op_compare` swaps the args and negates the result.
550 /// Lives separate from `Mm` so the synth path can stay yieldable without
551 /// every other Mm caller learning a swap flag they would never set.
552 CompareSynth { func: Value },
553}
554
555/// Metamethod events; discriminants index `Vm::mm_names`.
556#[derive(Clone, Copy, PartialEq, Eq)]
557#[repr(usize)]
558pub(crate) enum Mm {
559 Index,
560 NewIndex,
561 Call,
562 ToString,
563 Metatable,
564 Name,
565 Eq,
566 Lt,
567 Le,
568 Concat,
569 Len,
570 Add,
571 Sub,
572 Mul,
573 Div,
574 Mod,
575 Pow,
576 IDiv,
577 BAnd,
578 BOr,
579 BXor,
580 Shl,
581 Shr,
582 Unm,
583 BNot,
584 Close,
585 Gc,
586 Pairs,
587}
588
589const MM_NAMES: [&str; 28] = [
590 "__index",
591 "__newindex",
592 "__call",
593 "__tostring",
594 "__metatable",
595 "__name",
596 "__eq",
597 "__lt",
598 "__le",
599 "__concat",
600 "__len",
601 "__add",
602 "__sub",
603 "__mul",
604 "__div",
605 "__mod",
606 "__pow",
607 "__idiv",
608 "__band",
609 "__bor",
610 "__bxor",
611 "__shl",
612 "__shr",
613 "__unm",
614 "__bnot",
615 "__close",
616 "__gc",
617 "__pairs",
618];
619
620/// The metamethod event an opcode dispatches, without the `__` prefix (PUC
621/// funcnamefromcode), for "(metamethod 'event')" call-error suffixes.
622fn mm_event_name(op: crate::vm::isa::Op) -> Option<&'static str> {
623 use crate::vm::isa::Op;
624 Some(match op {
625 Op::Add => "add",
626 Op::Sub => "sub",
627 Op::Mul => "mul",
628 Op::Div => "div",
629 Op::Mod => "mod",
630 Op::Pow => "pow",
631 Op::IDiv => "idiv",
632 Op::BAnd => "band",
633 Op::BOr => "bor",
634 Op::BXor => "bxor",
635 Op::Shl => "shl",
636 Op::Shr => "shr",
637 Op::Unm => "unm",
638 Op::BNot => "bnot",
639 Op::Concat => "concat",
640 Op::Len => "len",
641 Op::GetField | Op::GetTable | Op::GetI | Op::SelfOp => "index",
642 Op::SetField | Op::SetTable | Op::SetI => "newindex",
643 Op::Eq | Op::EqK => "eq",
644 Op::Lt => "lt",
645 Op::Le => "le",
646 _ => return None,
647 })
648}
649
650/// PUC MAXTAGLOOP (5.3+): bound on `__index`/`__newindex` chains.
651const MAX_TAG_LOOP: u32 = 2000;
652/// PUC `MAXCCMT`: bound on a `__call` metamethod chain (lvm.c). 200 chains
653/// is more than any reasonable program needs and matches PUC 5.4/5.5; a
654/// bound of `15` is tight enough to fire on calls.lua :194 (N=20).
655const MAX_CCMT: u32 = 200;
656/// PUC LUAI_MAXCCALLS analogue: native↔Lua nesting bound.
657pub(crate) const MAX_C_DEPTH: u32 = 200;
658/// Stack an xpcall handler may use past `MAX_LUA_STACK` while handling a
659/// stack overflow: PUC's 200 extra `ERRORSTACKSIZE` slots, plus the frame
660/// reserve (256) the overflowing call was refused, so the handler's first
661/// frame fits where that one did not.
662const ERROR_STACK_EXTRA: u32 = 200 + 256;
663/// luna's engine-level VM stack cap (used by call-site overflow checks).
664/// Slightly larger than PUC's `LUAI_MAXSTACK` so engine internals have a
665/// little headroom above any single library push.
666const MAX_LUA_STACK: u32 = 1 << 20;
667/// PUC `LUAI_MAXSTACK` (`luaconf.h`): the cap library code consults via
668/// `lua_checkstack` to refuse multi-value pushes (`table.unpack` returning
669/// N values, `string.pack` results, etc.). 5.3 coroutine.lua :530 pins
670/// this at one million — `for j in {lim-10, …}` expects every j ≥ lim-10
671/// to fail because the few slots already consumed in the coroutine push
672/// the effective cap below lim-10.
673const PUC_MAXSTACK: i64 = 1_000_000;
674
675/// PUC 5.4+ default warnf state. The base library's `warn` function flips
676/// between `Off` and `On` via the `@on` / `@off` control messages; any other
677/// `@<word>` control is silently ignored, mirroring `lauxlib.c::checkcontrol`.
678#[derive(Clone, Copy, PartialEq, Eq, Debug)]
679pub enum WarnState {
680 /// `warn` calls are silently dropped (default after `warn("@off")`).
681 Off,
682 /// `warn` calls are delivered to stderr (after `warn("@on")`).
683 On,
684}
685
686/// Best-effort extraction of a textual message from a `catch_unwind` payload.
687/// `panic!("msg")` arrives as `String`, `panic!(static)` as `&str`; anything
688/// else degrades to `"<non-string panic>"`. Used by the native-call
689/// catch_unwind to fold the panic into a Lua error.
690fn panic_payload_str(payload: &Box<dyn std::any::Any + Send>) -> String {
691 if let Some(s) = payload.downcast_ref::<String>() {
692 return s.clone();
693 }
694 if let Some(s) = payload.downcast_ref::<&'static str>() {
695 return (*s).to_string();
696 }
697 "<non-string panic>".to_string()
698}
699
700/// Combined error type returned by [`Vm::eval`] and friends — either the
701/// chunk failed to parse / compile, or it raised at runtime.
702#[derive(Debug)]
703pub enum Error {
704 /// Parse or compile failure.
705 Syntax(SyntaxError),
706 /// Runtime error raised during execution.
707 Runtime(LuaError),
708}
709
710impl From<SyntaxError> for Error {
711 fn from(e: SyntaxError) -> Error {
712 Error::Syntax(e)
713 }
714}
715
716impl From<LuaError> for Error {
717 fn from(e: LuaError) -> Error {
718 Error::Runtime(e)
719 }
720}
721
722impl Vm {
723 /// `lua_close` from inside a running script (`os.exit(code, true)`):
724 /// close the main thread's pending to-be-closed variables, then run every
725 /// finalizer. Both run protected, so their errors are dropped as PUC's
726 /// `close_state` drops them. Inside a coroutine the main thread's stack is
727 /// parked, and only the finalizers run.
728 pub(crate) fn close_state(&mut self) {
729 if self.current.is_none() {
730 let _ = self.close_slots(0, None);
731 }
732 self.heap.queue_all_finalizers();
733 self.run_finalizers();
734 }
735}
736
737impl Drop for Vm {
738 fn drop(&mut self) {
739 // state close: run `__gc` for every still-registered finalizable before
740 // the heap frees them (PUC separatetobefnz(g,1) + callallpending). A
741 // single pass — objects created by a closing finalizer are not
742 // re-finalized (they go to the heap's free list directly).
743 self.heap.queue_all_finalizers();
744 self.run_finalizers();
745 let id = self.jit_owner_id;
746 // SAFETY: the finalizers were the last Lua code this Vm runs, and
747 // its functions (the only holders of entry points compiled for it)
748 // go away with its heap.
749 unsafe {
750 self.jit.storage.release_code(id);
751 for s in &mut self.retired_jit_storage {
752 s.release_code(id);
753 }
754 }
755 }
756}
757
758// Split-borrow free fn helpers for frames push/pop with shadow counter
759// `frames_top: u32`. Free fns (not Vm methods) so callers can pass
760// `&mut self.frames` + `&mut self.frames_top` as split borrows, allowing
761// other `&mut self.field` reads inside the CallFrame construction (e.g.
762// `std::mem::take(&mut self.pending_tm)`).
763//
764// The shadow has no readers yet; it just stays in sync + asserts.
765#[inline(always)]
766fn frames_push_sync(frames: &mut Vec<CallFrame>, frames_top: &mut u32, cf: CallFrame) {
767 frames.push(cf);
768 // Shadow maintenance is debug-only: release builds skip the
769 // increment + assertion entirely. While nothing reads the shadow,
770 // its purpose is to VERIFY the assumed invariant
771 // (frames_top == frames.len()) across all push/pop sites; once readers
772 // consume it, release must run the increment unconditionally.
773 #[cfg(debug_assertions)]
774 {
775 *frames_top += 1;
776 debug_assert_eq!(
777 *frames_top as usize,
778 frames.len(),
779 "P17-D frames_top out of sync after push",
780 );
781 }
782 #[cfg(not(debug_assertions))]
783 let _ = frames_top;
784}
785
786#[inline(always)]
787fn frames_pop_sync(frames: &mut Vec<CallFrame>, frames_top: &mut u32) -> Option<CallFrame> {
788 let r = frames.pop();
789 #[cfg(debug_assertions)]
790 {
791 if r.is_some() {
792 *frames_top = frames_top.saturating_sub(1);
793 }
794 debug_assert_eq!(
795 *frames_top as usize,
796 frames.len(),
797 "P17-D frames_top out of sync after pop",
798 );
799 }
800 #[cfg(not(debug_assertions))]
801 let _ = frames_top;
802 r
803}
804
805/// One-time env-var read for
806/// `LUNA_AOT_PROBE`. Returns `true` iff the env var is set to any
807/// non-empty value. The result is cached in a `OnceLock` so the
808/// dispatcher's hot path pays a single atomic load per process. Off
809/// by default — production deploys don't bleed diagnostic prints.
810fn jit_probe_enabled() -> bool {
811 static PROBE_ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
812 *PROBE_ON.get_or_init(|| {
813 std::env::var("LUNA_AOT_PROBE")
814 .ok()
815 .filter(|v| !v.is_empty())
816 .is_some()
817 })
818}
819
820impl Vm {
821 /// Re-sync `frames_top` after a bulk `frames: Vec`
822 /// swap (take_ctx, put_ctx, load_coro_ctx). Must be called after
823 /// the Vec replacement to keep the shadow valid.
824 #[inline(always)]
825 fn frames_resync(&mut self) {
826 // Debug-only — see `frames_push_sync` comment.
827 #[cfg(debug_assertions)]
828 {
829 self.frames_top = self.frames.len() as u32;
830 }
831 }
832
833 // ====================================================================
834 // Stack-inline frame metadata accessors (unused).
835 //
836 // These methods read/write the LJ_FR2 marker slots at `stack[base-2]`
837 // (closure GCRef) and `stack[base-1]` (FrameMarker as i64). No call
838 // site uses them yet.
839 //
840 // Preconditions (debug-asserted):
841 // - base >= 2 (slots base-2 and base-1 must exist below the frame)
842 // - self.stack.len() > base + max_stack (caller has grown stack)
843 // - For Lua frames, stack[base-2] holds Value::Closure(cl)
844 // - For Lua frames, stack[base-1] holds Value::Int(marker.to_raw())
845 //
846 // No release-build cost when unused (LTO strips dead methods).
847 // ====================================================================
848
849 /// Write a Lua frame's closure pointer into `stack[base-2]`.
850 /// The caller must ensure `base >= 2` and the slot is within the
851 /// stack's allocated range.
852 #[inline]
853 #[allow(dead_code)] // no consumer yet
854 fn write_frame_closure(&mut self, base: u32, cl: crate::runtime::Gc<LuaClosure>) {
855 debug_assert!(
856 base >= 2,
857 "frame closure slot needs base >= 2; got {}",
858 base
859 );
860 let idx = (base - 2) as usize;
861 debug_assert!(idx < self.stack.len(), "stack[base-2] out of range");
862 self.stack[idx] = Value::Closure(cl);
863 }
864
865 /// Read a Lua frame's closure pointer from `stack[base-2]`.
866 /// Returns `None` if the slot doesn't hold a closure (caller is
867 /// expected to treat that as a corrupt frame).
868 ///
869 /// Uses the [`Value::tag_byte`] fast-path
870 /// to avoid the enum-match cost on the hot path. Tag check via
871 /// 1-byte load + branch + `as_closure_unchecked` payload load.
872 #[inline]
873 #[allow(dead_code)]
874 fn read_frame_closure(&self, base: u32) -> Option<crate::runtime::Gc<LuaClosure>> {
875 debug_assert!(base >= 2);
876 let v = self.stack.get((base - 2) as usize)?;
877 if v.tag_byte() == crate::runtime::value::tag::CLOSURE {
878 // SAFETY: tag byte just verified == CLOSURE.
879 Some(unsafe { v.as_closure_unchecked() })
880 } else {
881 None
882 }
883 }
884
885 /// Write a packed [`FrameMarker`] into `stack[base-1]`. The marker
886 /// encodes the frame kind (Lua / Cont) + PC-or-delta payload.
887 /// Stored as `Value::Int(marker.to_raw())` so it round-trips
888 /// cleanly through the value stack without losing bits.
889 #[inline]
890 #[allow(dead_code)]
891 fn write_frame_marker(&mut self, base: u32, marker: crate::runtime::frame_marker::FrameMarker) {
892 debug_assert!(base >= 1, "frame marker slot needs base >= 1; got {}", base);
893 let idx = (base - 1) as usize;
894 debug_assert!(idx < self.stack.len(), "stack[base-1] out of range");
895 self.stack[idx] = Value::Int(marker.to_raw());
896 }
897
898 /// Read a packed [`FrameMarker`] from `stack[base-1]`. Returns
899 /// `None` if the slot isn't a `Value::Int` (caller treats as a
900 /// corrupt frame); the kind tag itself may still be invalid, in
901 /// which case [`FrameMarker::kind`] returns `None` on the result.
902 ///
903 /// Uses the [`Value::tag_byte`] fast-path
904 /// for the tag check + `as_int_unchecked` for the payload load.
905 #[inline]
906 #[allow(dead_code)]
907 fn read_frame_marker(&self, base: u32) -> Option<crate::runtime::frame_marker::FrameMarker> {
908 debug_assert!(base >= 1);
909 let v = self.stack.get((base - 1) as usize)?;
910 if v.tag_byte() == crate::runtime::value::tag::INT {
911 // SAFETY: tag byte just verified == INT.
912 Some(crate::runtime::frame_marker::FrameMarker::from_raw(
913 unsafe { v.as_int_unchecked() },
914 ))
915 } else {
916 None
917 }
918 }
919
920 /// Build the raw `Vm` struct without main coroutine / RNG seed / library
921 /// setup. Private helper shared by `Vm::new` and `Vm::new_minimal`; the
922 /// caller is responsible for the rest of the bring-up.
923 fn new_inner(version: LuaVersion) -> Vm {
924 let mut heap = Heap::new();
925 // PUC 5.1 had no ephemeron pass — `__mode='k'` tables marked their
926 // values strongly. gc.lua's "weak tables" section relies on that.
927 heap.no_ephemeron = version <= LuaVersion::Lua51;
928 // PUC 5.3 needs two GC cycles to finalize a table caught in a
929 // coroutine reference cycle (gc.lua :502); 5.4+ rewrote the GC and
930 // finalize in a single cycle (5.4/5.5 gc.lua :544 assert exactly one).
931 heap.defer_thread_cycle_finalize = version == LuaVersion::Lua53;
932 let globals = heap.new_table();
933 let mm_names = MM_NAMES.iter().map(|n| heap.intern(n.as_bytes())).collect();
934
935 Vm {
936 heap,
937 stack: Vec::new(),
938 frames: Vec::new(),
939 frame_ccmt: Vec::new(),
940 frames_top: 0,
941 open_upvals: Vec::new(),
942 tbc: Vec::new(),
943 top: 0,
944 globals,
945 type_mt: [None; 5],
946 mm_names,
947 c_depth: 0,
948 pcall_depth: 0,
949 nny: 0,
950 msgh_depth: 0,
951 terminating: None,
952 rng: [0; 4],
953 started: std::time::Instant::now(),
954 version,
955 closing_err: None,
956 current: None,
957 main_ctx: None,
958 yielding: None,
959 native_nresults: -1,
960 main_coro: None,
961 // PUC 5.4+ boots in GENERATIONAL mode (the first
962 // `collectgarbage("generational")` reports "generational"
963 // as the previous mode on stock lua5.4;
964 // 5.5 behaves the same, probed against lua5.5). luna's
965 // collector is a single incremental engine either way;
966 // this field is the MODE REPORT the stdlib exposes.
967 gc_mode: if version >= crate::version::LuaVersion::Lua54 {
968 "generational"
969 } else {
970 "incremental"
971 },
972 gc_top: 0,
973 gc_pause: 200,
974 gc_stepmul: 100,
975 gc_stepsize: 13,
976 gc_params: crate::vm::lib_gc::GcParams::new(version),
977 gc_finalizing: false,
978 capi_stack: Vec::new(),
979 capi_cstr_pin: None,
980 warn_state: WarnState::Off,
981 warn_buf: Vec::new(),
982 warn_log: Vec::new(),
983 instr_budget: None,
984 bytecode_loading: true,
985 puc_bytecode_loading: false,
986 loader_input_budget: Vm::DEFAULT_LOADER_INPUT_BUDGET,
987 registry: None,
988 file_mt: None,
989 io_input: None,
990 io_output: None,
991 io_stdin: None,
992 ignore_env: false,
993 hook: HookState::default(),
994 in_hook: false,
995 pending_tailcalls: 0,
996 pending_ccmt: 0,
997 errored_natives: Vec::new(),
998 msgh_floor: 0,
999 msgh_running: None,
1000 msgh_runs: 0,
1001 msgh_applied: None,
1002 keep_error_traceback: true,
1003 hook_ftransfer: 0,
1004 hook_ntransfer: 0,
1005 pending_tm: None,
1006 pending_is_hook: false,
1007 error_traceback: None,
1008 public_call_depth: 0,
1009 running_natives: Vec::new(),
1010 running_native_acts: Vec::new(),
1011 natives_base: 0,
1012 // JIT-specific state lives in the `JitState`
1013 // sidecar. The `luna` crate's `Vm::new_minimal_with_jit` /
1014 // `install_jit_backend` / `luaL_newstate` swap in
1015 // `CraneliftBackend` for callers that want JIT acceleration.
1016 jit: crate::vm::jit_state::JitState::with_null_backend(),
1017 // host roots ticket pool for the `Lua` facade
1018 host_roots: Vec::new(),
1019 // MacroLua registry. Pre-populated with
1020 // built-ins (`@quote` / `@unquote` / `@if` / `@gensym`)
1021 // when this Vm is constructed under `LuaVersion::MacroLua`.
1022 macro_registry: if version == LuaVersion::MacroLua {
1023 crate::frontend::macro_expander::MacroRegistry::with_builtins()
1024 } else {
1025 crate::frontend::macro_expander::MacroRegistry::new()
1026 },
1027 host_roots_free: Vec::new(),
1028 sort_scratch: Vec::new(),
1029 // LuaUserdata trait sugar's per-Vm
1030 // metatable cache. Populated lazily by register_userdata.
1031 userdata_metatables: std::collections::HashMap::new(),
1032 // Error classification metadata. Defaults to
1033 // Runtime; set at known sites (syntax / budget trip /
1034 // native error / type error).
1035 last_error_kind: crate::vm::error::LuaErrorKind::default(),
1036 last_error_source: None,
1037 // Async embedder fields. Defaults preserve sync behavior
1038 // bit-for-bit (`async_mode = false` means the budget hot loop
1039 // errors out instead of yielding).
1040 async_mode: false,
1041 async_waker: None,
1042 async_slice_size: 10_000,
1043 host_yield_pending: false,
1044 // Pending async-native state. Empty by
1045 // default; populated only by the dispatcher when an
1046 // async-marked NativeClosure is invoked under async_mode.
1047 pending_async_native_fut: None,
1048 pending_async_native_ctx: None,
1049 jit_owner_id: {
1050 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
1051 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
1052 },
1053 retired_jit_storage: Vec::new(),
1054 }
1055 }
1056
1057 /// Build a fully-loaded Vm — the default for embedders that want PUC's
1058 /// standard library surface. Equivalent to `Vm::new_minimal(version)`
1059 /// followed by `vm.open_all_libs()`.
1060 pub fn new(version: LuaVersion) -> Vm {
1061 let mut vm = Vm::new_minimal(version);
1062 vm.open_all_libs();
1063 vm
1064 }
1065
1066 /// Build a Vm with no standard libraries loaded. Embedders
1067 /// that want a sandbox (Redis-style scripts, in-game scripting with
1068 /// a curated API) call this and then `open_base` / `open_math` / etc.
1069 /// selectively. The Vm is otherwise fully initialized (main coroutine,
1070 /// RNG seed, GC) so `eval` and `call_value` are immediately usable.
1071 pub fn new_minimal(version: LuaVersion) -> Vm {
1072 let mut vm = Vm::new_inner(version);
1073 let mc = vm.heap.new_coro(Value::Nil, vm.globals);
1074 // 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).
1075 unsafe { mc.as_mut() }.status = CoroStatus::Running;
1076 vm.main_coro = Some(mc);
1077 let (a, b) = vm.rng_auto_seed();
1078 vm.rng_seed(a as u64, b as u64);
1079 vm
1080 }
1081
1082 /// Install a caller-supplied JIT backend. The
1083 /// `luna` crate uses this to swap in its `CraneliftBackend`; tests
1084 /// or third-party backends pass their own [`crate::jit::IntChunkCompiler`] /
1085 /// [`crate::jit::TraceCompiler`] implementations. Re-installing on a Vm whose
1086 /// closures already populated `Proto.jit: JitProtoState::Compiled`
1087 /// does NOT evict those cached entries — call right after
1088 /// construction for a clean swap.
1089 ///
1090 /// Naming: `install_jit_backend` (not `install_default_jit`)
1091 /// because the "default" in luna-core is `NullJitBackend`; the
1092 /// "default JIT" lives in the `luna` crate.
1093 pub fn install_jit_backend<C, T>(&mut self, chunk: C, trace: T)
1094 where
1095 C: crate::jit::IntChunkCompiler + 'static,
1096 T: crate::jit::TraceCompiler + 'static,
1097 {
1098 self.jit.chunk_compiler = Box::new(chunk);
1099 self.jit.trace_compiler = Box::new(trace);
1100 }
1101
1102 /// Install a caller-supplied JIT
1103 /// storage holder. Default is [`crate::jit::NullJitStorage`];
1104 /// the `luna_jit` crate's `install_default_jit` pairs this with
1105 /// `install_jit_backend(CraneliftBackend, CraneliftBackend)` to
1106 /// also install a fresh `CraneliftJitStorage`. Storage holds
1107 /// the per-`Vm` JIT cache + handle collections.
1108 ///
1109 /// The storage it replaces is kept until the Vm drops: functions
1110 /// compiled through it may still be called.
1111 pub fn install_jit_storage<S>(&mut self, storage: S)
1112 where
1113 S: crate::jit::JitStorage + 'static,
1114 {
1115 let old = std::mem::replace(&mut self.jit.storage, Box::new(storage));
1116 self.retired_jit_storage.push(old);
1117 }
1118
1119 /// Install the no-op JIT backend. `try_compile`
1120 /// reports "skipped" so every closure stays on the interpreter
1121 /// path, and the trace recorder's compile attempt always returns
1122 /// `None`. Intended for tests that want to verify the trait
1123 /// boundary works in a JIT-free configuration.
1124 ///
1125 /// Calling this on a Vm whose closures already populated
1126 /// `Proto.jit: JitProtoState::Compiled` does NOT evict those
1127 /// cached entries — the dispatcher will still call into them. For
1128 /// a truly JIT-free run, call this immediately after construction.
1129 pub fn install_null_jit(&mut self) {
1130 self.jit.chunk_compiler = Box::new(crate::jit::NullJitBackend);
1131 self.jit.trace_compiler = Box::new(crate::jit::NullJitBackend);
1132 }
1133
1134 /// Open the entire 5.5 standard library on a `new_minimal`-built Vm.
1135 /// `Vm::new` calls this; sandboxed embedders open libraries one at a
1136 /// time instead (`open_base`, `open_math`, `open_table`, …).
1137 pub fn open_all_libs(&mut self) {
1138 self.open_base();
1139 self.open_math();
1140 self.open_table();
1141 self.open_string();
1142 self.open_utf8();
1143 self.open_os_io();
1144 self.open_debug();
1145 self.open_coroutine();
1146 // PUC 5.2 introduced `bit32`; 5.3 retired it in the manual BUT
1147 // the stock 5.3 build ships -DLUA_COMPAT_5_2, which keeps the
1148 // library loaded. The diff ground truth is the default build
1149 // (stock lua5.3), so expose it under 5.2 AND
1150 // 5.3; 5.4 dropped the compat default for real.
1151 if matches!(self.version, LuaVersion::Lua52 | LuaVersion::Lua53) {
1152 self.open_bit32();
1153 }
1154 // last, so `package.loaded` lists every library opened before it
1155 self.open_package();
1156 }
1157
1158 /// Install the base library (`print`, `type`, `pairs`, `tostring`,
1159 /// `pcall`, `error`, `assert`, `select`, `setmetatable`, `getmetatable`,
1160 /// `rawequal`, `rawget`, `rawset`, `rawlen`, `next`, `tonumber`,
1161 /// `collectgarbage`, `warn` on 5.4+, `_VERSION`, `_G`, plus 5.1's
1162 /// retired globals `unpack`, `loadstring`, `setfenv`, `getfenv`,
1163 /// `newproxy`, `gcinfo` when version == 5.1). Safe to call at most
1164 /// once per Vm.
1165 pub fn open_base(&mut self) {
1166 crate::vm::builtins::open_base(self);
1167 }
1168 /// Install the `math` standard library.
1169 pub fn open_math(&mut self) {
1170 crate::vm::lib_math::open_math(self);
1171 }
1172 /// Install the `table` standard library.
1173 pub fn open_table(&mut self) {
1174 crate::vm::lib_table::open_table(self);
1175 }
1176 /// Install the `string` standard library (and the shared string metatable).
1177 pub fn open_string(&mut self) {
1178 crate::vm::lib_string::open_string(self);
1179 }
1180 /// Install the `utf8` standard library (5.3+).
1181 pub fn open_utf8(&mut self) {
1182 crate::vm::lib_utf8::open_utf8(self);
1183 }
1184 /// `os` and `io` are merged because file userdata shares state with both
1185 /// (`io.tmpname` and `os.tmpname` are the same function, `io.popen`
1186 /// wraps `os.execute`'s shell).
1187 pub fn open_os_io(&mut self) {
1188 crate::vm::lib_os_io::open_os_io(self);
1189 }
1190 /// Install the `debug` standard library (introspection / hooks). Off by
1191 /// default for sandbox embedders.
1192 pub fn open_debug(&mut self) {
1193 crate::vm::lib_debug::open_debug(self);
1194 }
1195 /// Install the `coroutine` standard library.
1196 pub fn open_coroutine(&mut self) {
1197 crate::vm::lib_coroutine::open_coroutine(self);
1198 }
1199 /// `package` plus the 5.1-only `module` and `package.seeall` aliases.
1200 pub fn open_package(&mut self) {
1201 crate::vm::lib_package::open_package(self);
1202 }
1203 /// 5.2-only `bit32` library (5.3+ retired in favour of native bitwise
1204 /// ops on 64-bit integers).
1205 pub fn open_bit32(&mut self) {
1206 crate::vm::lib_bit32::open_bit32(self);
1207 }
1208
1209 /// xoshiro256** next.
1210 pub(crate) fn rng_next(&mut self) -> u64 {
1211 let s = &mut self.rng;
1212 let result = s[1].wrapping_mul(5).rotate_left(7).wrapping_mul(9);
1213 let t = s[1] << 17;
1214 s[2] ^= s[0];
1215 s[3] ^= s[1];
1216 s[1] ^= s[2];
1217 s[0] ^= s[3];
1218 s[2] ^= t;
1219 s[3] = s[3].rotate_left(45);
1220 result
1221 }
1222
1223 /// Seed the RNG via splitmix64 expansion (PUC randseed shape).
1224 pub(crate) fn rng_seed(&mut self, a: u64, b: u64) {
1225 // PUC setseed: state = [n1, 0xff, n2, 0] (0xff avoids an all-zero
1226 // state), then 16 discards to spread the seed. Matches PUC's exact
1227 // sequence so the low-level conformance test passes.
1228 self.rng = [a, 0xff, b, 0];
1229 for _ in 0..16 {
1230 self.rng_next();
1231 }
1232 }
1233
1234 /// Wall-clock since VM creation (os.clock approximation).
1235 pub(crate) fn uptime(&self) -> std::time::Duration {
1236 self.started.elapsed()
1237 }
1238
1239 /// Entropy for math.randomseed() with no arguments.
1240 pub(crate) fn rng_auto_seed(&mut self) -> (i64, i64) {
1241 let t = std::time::SystemTime::now()
1242 .duration_since(std::time::UNIX_EPOCH)
1243 .map(|d| d.as_nanos() as u64)
1244 .unwrap_or(0);
1245 let addr = &self.rng as *const _ as u64;
1246 (t as i64, addr as i64)
1247 }
1248
1249 /// Allocate a native function object (no upvalues): builtin registration.
1250 pub fn native(&mut self, f: crate::runtime::value::NativeFn) -> Value {
1251 Value::Native(self.heap.new_native(f, Box::new([])))
1252 }
1253
1254 /// Allocate a native function object with captured upvalues.
1255 pub fn native_with(
1256 &mut self,
1257 f: crate::runtime::value::NativeFn,
1258 upvals: Box<[Value]>,
1259 ) -> Value {
1260 Value::Native(self.heap.new_native(f, upvals))
1261 }
1262
1263 /// Install the shared string metatable (string library).
1264 pub fn set_string_metatable(&mut self, mt: Option<Gc<Table>>) {
1265 self.type_mt[3] = mt;
1266 }
1267
1268 /// The current globals table (`_G` / `_ENV` source for new chunks).
1269 pub fn globals(&self) -> Gc<Table> {
1270 self.globals
1271 }
1272
1273 /// Remaining VM stack slots (PUC `L->stack_last - L->top` analogue).
1274 /// Library code that pushes a known number of fresh slots — e.g.
1275 /// `table.unpack` returning N values — consults this to refuse when
1276 /// the push would blow past `LUAI_MAXSTACK`. 5.3 coroutine.lua :530's
1277 /// `for j in {lim-10, lim-5, …}` series pins this contract: the
1278 /// coroutine's already-built table eats a few slots, so an unpack of
1279 /// ~lim values can't fit.
1280 pub(crate) fn stack_room(&self) -> i64 {
1281 PUC_MAXSTACK - (self.stack.len() as i64)
1282 }
1283
1284 /// Repoint the thread's "global table" used by *future* `Vm::load` calls
1285 /// for the chunk's `_ENV` upvalue (PUC 5.1 `setfenv(0, env)` rewrites
1286 /// `L->l_gt`). Already-loaded chunks keep their own snapshot via the
1287 /// per-closure cell-0 clone in `Op::Closure`, so they are unaffected.
1288 pub(crate) fn set_globals(&mut self, env: Gc<Table>) {
1289 self.globals = env;
1290 }
1291
1292 /// The Lua dialect this VM was constructed for (5.1 / 5.2 / 5.3 / 5.4 /
1293 /// 5.5). Determines numeric semantics, available standard libraries, and
1294 /// metamethod behavior.
1295 pub fn version(&self) -> LuaVersion {
1296 self.version
1297 }
1298
1299 /// Set a global by name. `v` may be any `IntoValue`: a primitive
1300 /// (`i64`, `f64`, `bool`, `&str`, `String`, `Vec<u8>`), a `Value`
1301 /// directly, an `Option<T>`, or a `Gc<Table>` / `Gc<LuaClosure>` /
1302 /// `Gc<NativeClosure>` handle.
1303 ///
1304 /// Returns `Err(LuaError)` only if the globals table overflows
1305 /// (extremely unlikely in practice — `MAX_ASIZE = 1 << 27`).
1306 /// String interning + key construction cannot fail.
1307 ///
1308 /// ```
1309 /// # use luna_core::vm::Vm;
1310 /// # use luna_core::version::LuaVersion;
1311 /// let mut vm = Vm::sandbox(LuaVersion::Lua55).open_base().build();
1312 /// vm.set_global("answer", 42).unwrap();
1313 /// vm.set_global("ratio", 0.5_f64).unwrap();
1314 /// vm.set_global("hello", "world").unwrap();
1315 /// let r = vm.eval("return answer, ratio, hello").unwrap();
1316 /// assert_eq!(r.len(), 3);
1317 /// ```
1318 pub fn set_global<V: crate::vm::IntoValue>(
1319 &mut self,
1320 name: &str,
1321 v: V,
1322 ) -> Result<(), LuaError> {
1323 let v = v.into_value(self);
1324 let k = Value::Str(self.heap.intern(name.as_bytes()));
1325 // 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).
1326 unsafe { self.globals.as_mut() }.set(&mut self.heap, k, v)?;
1327 self.heap
1328 .barrier_back(self.globals.as_ptr() as *mut crate::runtime::heap::GcHeader);
1329 Ok(())
1330 }
1331
1332 /// Backward write barrier shorthand for native lib code: demote `t` from
1333 /// BLACK back to gray so the next propagate step re-traces its fields.
1334 /// No-op outside Propagate (parent is never BLACK at mutation time).
1335 pub(crate) fn barrier_back_table(&mut self, t: Gc<Table>) {
1336 self.heap
1337 .barrier_back(t.as_ptr() as *mut crate::runtime::heap::GcHeader);
1338 }
1339
1340 /// Forward write barrier shorthand: a closed upvalue is a single-slot
1341 /// container — `barrier_forward` is cheaper than `barrier_back` here.
1342 /// No-op outside Propagate.
1343 pub(crate) fn barrier_forward_upvalue(&mut self, uv: Gc<Upvalue>, child: Value) {
1344 self.heap
1345 .barrier_forward(uv.as_ptr() as *mut crate::runtime::heap::GcHeader, child);
1346 }
1347
1348 /// Register a MacroLua macro under `name`. Inert
1349 /// under non-MacroLua dialects (the macro is stored but the load
1350 /// path only consults the registry when
1351 /// `self.version == LuaVersion::MacroLua`).
1352 ///
1353 /// `name` is stored without the leading `@` — source code writes
1354 /// `@double(x)` to invoke a macro registered as `"double"`.
1355 pub fn define_macro(&mut self, name: &str, m: Box<dyn crate::frontend::macro_expander::Macro>) {
1356 self.macro_registry.register(name, m);
1357 }
1358
1359 /// Drop all MacroLua macros (built-in + custom).
1360 /// Mostly useful for tests.
1361 pub fn clear_macros(&mut self) {
1362 self.macro_registry.clear();
1363 }
1364
1365 /// PUC `luaL_loadfilex`: compile the file `name` (standard input when
1366 /// `None`, named `stdin`) into a function. A first line starting with
1367 /// `#` is skipped; `mode` (`"t"`, `"b"`, `"bt"`, `None` for both)
1368 /// limits the chunk to text and/or binary. The error is the message
1369 /// PUC's function leaves: `cannot open <name>: <reason>` when the file
1370 /// cannot be read, or the positioned syntax error.
1371 pub fn load_file(
1372 &mut self,
1373 name: Option<&[u8]>,
1374 mode: Option<&[u8]>,
1375 ) -> Result<Value, LuaError> {
1376 crate::vm::lib_os_io::load_path(self, name, mode).map_err(LuaError)
1377 }
1378
1379 /// PUC `luaL_loadbufferx`: compile `src` under `chunkname`, the chunk
1380 /// kind limited by `mode` as in [`Vm::load_file`]. A syntax error comes
1381 /// back as its positioned message (`<chunk id>:<line>: <message>`), the
1382 /// string `load` returns.
1383 pub fn load_buffer(
1384 &mut self,
1385 src: &[u8],
1386 chunkname: &[u8],
1387 mode: Option<&[u8]>,
1388 ) -> Result<Value, LuaError> {
1389 crate::vm::lib_os_io::load_chunk(self, src, chunkname, mode).map_err(LuaError)
1390 }
1391
1392 /// Parse + compile a chunk and close it over the globals table.
1393 pub fn load(&mut self, src: &[u8], chunkname: &[u8]) -> Result<Gc<LuaClosure>, SyntaxError> {
1394 // Reject oversize input *before* handing the parser/lexer a
1395 // potentially multi-GB slice. The PUC-shaped `not enough memory`
1396 // message keeps `heavy.lua::loadrep` compatibility: that test
1397 // accepts either `string length overflow` or `not enough memory`
1398 // as the failure mode for a feeder loop that outruns the host
1399 // allocator. See `set_loader_input_budget`.
1400 if src.len() > self.loader_input_budget {
1401 return Err(SyntaxError {
1402 line: 0,
1403 msg: b"not enough memory".to_vec(),
1404 });
1405 }
1406 // a precompiled (binary) chunk is undumped; source is parsed + compiled
1407 let is_bytecode = crate::vm::dump::is_binary_chunk(src);
1408 if is_bytecode && !self.bytecode_loading {
1409 return Err(SyntaxError {
1410 line: 0,
1411 msg: b"attempt to load a binary chunk (bytecode loading disabled)".to_vec(),
1412 });
1413 }
1414 let proto = if is_bytecode {
1415 let allow_puc = self.puc_bytecode_loading;
1416 crate::vm::dump::undump_named(src, &mut self.heap, self.version, allow_puc, chunkname)
1417 .map_err(SyntaxError::unpositioned)?
1418 } else if self.version.is_macro_lua() {
1419 // MacroLua dialect: drain the lexer into a
1420 // token vec, run the macro expander pre-pass against the
1421 // per-Vm registry, then hand the rewritten stream to
1422 // `parse_tokens`. The AST + compiler are dialect-agnostic
1423 // because by this point all `@`/quote tokens are gone.
1424 let mut lexer = crate::frontend::lexer::Lexer::new(src, self.version);
1425 let mut raw: Vec<crate::frontend::token::TokenInfo> = Vec::new();
1426 loop {
1427 let t = lexer.next_token()?;
1428 let eof = matches!(t.tok, crate::frontend::token::Token::Eof);
1429 raw.push(t);
1430 if eof {
1431 break;
1432 }
1433 }
1434 // Drop the trailing Eof — expander operates on the body and
1435 // `parse_tokens` reinserts Eof when it runs out of tokens.
1436 raw.pop();
1437 let expanded = self.macro_registry.expand(raw)?;
1438 let depth = self.c_depth + self.pcall_depth;
1439 let parsed =
1440 crate::frontend::parser::parse_tokens_at_depth(expanded, src, self.version, depth)?;
1441 crate::compiler::compile_parsed(
1442 &parsed.chunk,
1443 &parsed.end_lines,
1444 self.version,
1445 chunkname,
1446 &mut self.heap,
1447 )?
1448 } else {
1449 // PUC's `nCcalls` counts protected calls as well
1450 let depth = self.c_depth + self.pcall_depth;
1451 let parsed = crate::frontend::parser::parse_at_depth(src, self.version, depth)?;
1452 crate::compiler::compile_parsed(
1453 &parsed.chunk,
1454 &parsed.end_lines,
1455 self.version,
1456 chunkname,
1457 &mut self.heap,
1458 )?
1459 };
1460 // PUC `lua_load` (lapi.c) only seeds the loaded closure's first
1461 // upvalue with the globals table when the closure has *exactly* one
1462 // upvalue — that's the main-chunk `_ENV` case. A dumped non-main
1463 // function with two-or-more upvalues keeps every cell at nil; the
1464 // host must use `debug.setupvalue` to wire them up. 5.2 calls.lua
1465 // :293's `assert(x() == nil)` pins this contract.
1466 let n = proto.upvals.len();
1467 let mut ups: Vec<Gc<Upvalue>> = Vec::with_capacity(n.max(1));
1468 if n == 0 {
1469 // synthetic main chunk has no declared upvalues, but the engine
1470 // still expects at least one cell so the host can probe via
1471 // `debug.upvalueid` etc. Match the historical luna shape.
1472 ups.push(
1473 self.heap
1474 .new_upvalue(UpvalState::Closed(Value::Table(self.globals))),
1475 );
1476 } else if n == 1 {
1477 ups.push(
1478 self.heap
1479 .new_upvalue(UpvalState::Closed(Value::Table(self.globals))),
1480 );
1481 } else {
1482 for _ in 0..n {
1483 ups.push(self.heap.new_upvalue(UpvalState::Closed(Value::Nil)));
1484 }
1485 }
1486 Ok(self.heap.new_closure(proto, ups.into_boxed_slice()))
1487 }
1488
1489 /// Compile and run `src` as an anonymous chunk; return its results.
1490 /// Source name in the traceback is `"=eval"`. Syntax errors are
1491 /// surfaced as `LuaError` carrying the formatted PUC-style message
1492 /// (interned through the heap so the error value composes with
1493 /// `pcall` / `error_text` like any runtime error).
1494 pub fn eval(&mut self, src: &str) -> Result<Vec<Value>, LuaError> {
1495 self.eval_chunk(src, "=eval")
1496 }
1497
1498 /// Render an error value for messages/tests. Non-string errors —
1499 /// `error({code=…})`, `error(42)`, etc. — collapse to a type tag
1500 /// (`"(error object is a table value)"`); embedders that need
1501 /// structured payloads should inspect `e.0` directly. Errors whose
1502 /// text starts with `"native panic:"` indicate a Rust panic
1503 /// crossed `catch_unwind` — the Vm may be inconsistent and should
1504 /// be dropped (do not reuse).
1505 pub fn error_text(&self, e: &LuaError) -> String {
1506 match e.0 {
1507 Value::Str(s) => String::from_utf8_lossy(s.as_bytes()).into_owned(),
1508 v => format!("(error object is a {} value)", v.type_name()),
1509 }
1510 }
1511
1512 /// Render an error value the way PUC's standalone `msghandler`
1513 /// does (lua.c): strings pass through, numbers stringify, and any
1514 /// other object is given a chance at its `__tostring` metamethod
1515 /// (the result must be a string) before collapsing to the
1516 /// `"(error object is a … value)"` tag. Needs `&mut self` because
1517 /// `__tostring` runs arbitrary Lua — `error_text` remains the
1518 /// non-executing variant.
1519 pub fn error_display(&mut self, e: &LuaError) -> String {
1520 match e.0 {
1521 Value::Str(s) => String::from_utf8_lossy(s.as_bytes()).into_owned(),
1522 v @ (Value::Int(_) | Value::Float(_)) => {
1523 String::from_utf8_lossy(&self.tostring_basic(v)).into_owned()
1524 }
1525 v => {
1526 let mm = self.get_mm(v, Mm::ToString);
1527 if !mm.is_nil()
1528 && let Ok(r) = self.call_value(mm, &[v])
1529 && let Some(Value::Str(s)) = r.first()
1530 {
1531 return String::from_utf8_lossy(s.as_bytes()).into_owned();
1532 }
1533 format!("(error object is a {} value)", v.type_name())
1534 }
1535 }
1536 }
1537
1538 /// Call `f` with `args` in protected mode with the message handler
1539 /// `msgh`: PUC `lua_pcall(L, nargs, LUA_MULTRET, msgh)` made from a C
1540 /// function of the host's, as lua.c's `docall` does from `pmain`.
1541 ///
1542 /// `msgh` runs where the error was raised, before the stack unwinds, so
1543 /// it can take a traceback of the failing call ([`Vm::traceback`]); an
1544 /// error inside it calls it again with the new error, as in PUC. The
1545 /// returned error carries what the handler returned.
1546 ///
1547 /// The call counts as one C level on the stack, the host function
1548 /// making it: `debug.getinfo` finds it below `f`, and a traceback taken
1549 /// inside ends with `[C]: in ?` (`[C]: ?` in 5.1).
1550 pub fn call_value_with_handler(
1551 &mut self,
1552 f: Value,
1553 args: &[Value],
1554 msgh: Value,
1555 ) -> Result<Vec<Value>, LuaError> {
1556 let level = self.native(crate::vm::builtins::nat_host_xpcall);
1557 let mut call_args = Vec::with_capacity(args.len() + 2);
1558 call_args.push(f);
1559 call_args.push(msgh);
1560 call_args.extend_from_slice(args);
1561 let mut results = self.call_value(level, &call_args)?;
1562 // the protected call's `true, results...` or `false, handled error`
1563 if results.first().is_some_and(|ok| ok.truthy()) {
1564 results.remove(0);
1565 Ok(results)
1566 } else {
1567 Err(LuaError(results.get(1).copied().unwrap_or(Value::Nil)))
1568 }
1569 }
1570
1571 /// PUC `luaL_getmetafield`: the field `event` of `v`'s metatable, read
1572 /// raw; nil when `v` has no metatable or the field is absent.
1573 pub fn metafield(&mut self, v: Value, event: &str) -> Value {
1574 match self.metatable_of(v) {
1575 Some(mt) => {
1576 let key = Value::Str(self.heap.intern(event.as_bytes()));
1577 mt.get(key)
1578 }
1579 None => Value::Nil,
1580 }
1581 }
1582
1583 /// Call any callable value from the host (or from natives like pcall).
1584 pub fn call_value(&mut self, f: Value, args: &[Value]) -> Result<Vec<Value>, LuaError> {
1585 // host-level entry (no enclosing exec): drop any error state from a
1586 // prior call that propagated uncaught (`error_traceback` would
1587 // otherwise leak into the next debug.traceback call).
1588 if self.public_call_depth == 0 {
1589 self.error_traceback = None;
1590 }
1591 self.public_call_depth += 1;
1592 // JIT fast path. A host call with no args targeting a Lua
1593 // chunk whose body fits the int-arith whitelist short-circuits
1594 // the whole interpreter dispatch and runs straight through the
1595 // mmap'd native code. The lookup is one Cell::get + one match —
1596 // the slow path (compile attempt on first reach) is paid once per
1597 // Proto.
1598 if args.is_empty()
1599 && let Value::Closure(cl) = f
1600 && let Some(vs) = self.try_jit_call(cl)
1601 {
1602 self.public_call_depth -= 1;
1603 return Ok(vs);
1604 }
1605 let r = self.call_value_impl(f, args, true);
1606 self.public_call_depth -= 1;
1607 r
1608 }
1609
1610 /// Peek/populate the Proto's JIT cache slot, returning
1611 /// `Some(values)` when the cached native fn is callable for a
1612 /// zero-arg call. (Non-zero-arg dispatch is handled by
1613 /// `try_jit_call_op` from inside `begin_call`.)
1614 fn try_jit_call(&mut self, cl: Gc<LuaClosure>) -> Option<Vec<Value>> {
1615 use crate::runtime::function::JitProtoState;
1616 if !self.jit.enabled {
1617 return None;
1618 }
1619 let proto = cl.proto;
1620 if let JitProtoState::Untried = proto.jit.get() {
1621 self.populate_jit_cache(proto);
1622 }
1623 match proto.jit.get() {
1624 JitProtoState::Compiled {
1625 entry,
1626 num_args: 0,
1627 returns_one,
1628 arg_float_mask: _,
1629 arg_table_mask: _,
1630 ret_is_float,
1631 ret_is_table,
1632 } => {
1633 // 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.
1634 let f: crate::jit::IntChunkFn = unsafe { std::mem::transmute(entry) };
1635 // Install the active Vm + closure
1636 // for any Rust helper the JIT'd code may call (e.g.
1637 // `luna_jit_new_table`, `luna_jit_upval_get`) via
1638 // cranelift `Linkage::Import`. RAII clear on return.
1639 // Chunks with no upvalue reads don't touch the closure
1640 // slot, paying nothing.
1641 // Route through chunk_compiler so
1642 // the NullJitBackend path stays inert. Raw-ptr arg
1643 // avoids the &mut self borrow conflict against the
1644 // shared self.jit.chunk_compiler read.
1645 let vm_ptr: *mut Vm = self;
1646 let _jit_vm_guard = self.jit.chunk_compiler.enter(vm_ptr, Some(cl));
1647 // 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).
1648 let r = unsafe { f() };
1649 drop(_jit_vm_guard);
1650 // A JIT helper may have detected a metatable
1651 // on a table operand and parked a deopt request here.
1652 // Discard the sentinel value and return None so the caller
1653 // re-runs the call through the interpreter, which honours
1654 // __index/__newindex.
1655 if self.jit.pending_err.take().is_some() {
1656 return None;
1657 }
1658 Some(if returns_one {
1659 let v = if ret_is_float {
1660 Value::Float(f64::from_bits(r as u64))
1661 } else if ret_is_table {
1662 Value::Table(crate::runtime::Gc::from_ptr(
1663 r as *mut crate::runtime::Table,
1664 ))
1665 } else {
1666 Value::Int(r)
1667 };
1668 vec![v]
1669 } else {
1670 Vec::new()
1671 })
1672 }
1673 // Non-zero-arg Compiled state: call_value's empty-args
1674 // fast path can't drive it. Op::Call handles those.
1675 JitProtoState::Compiled { .. } | JitProtoState::Failed | JitProtoState::Untried => None,
1676 }
1677 }
1678
1679 /// Populate the cache slot. Flips `Untried` to either
1680 /// `Compiled { … }` or `Failed`; idempotent on already-populated
1681 /// states (call sites guard with a get before invoking).
1682 ///
1683 /// Consults a thread-local cross-`Vm` cache keyed by a hash of
1684 /// `proto.code`. Compiled artefacts live in the thread-local
1685 /// `JITModule` so their mmap pages outlive the `Vm`; subsequent
1686 /// `Vm`s loading the same source skip the cranelift compile step
1687 /// entirely.
1688 fn populate_jit_cache(&mut self, proto: Gc<crate::runtime::function::Proto>) {
1689 use crate::runtime::function::JitProtoState;
1690 let version = self.version();
1691 let pre53 = version <= crate::version::LuaVersion::Lua53;
1692 // 5.1 and 5.2 have no Int subtype (all numbers
1693 // are Float). The JIT's `GetUpval` ValueRead path uses this
1694 // to default-pin upvalue reads to Float without a tag check.
1695 let float_only = version <= crate::version::LuaVersion::Lua52;
1696 // Split-borrow JitState so the
1697 // trait method can take `&mut dyn JitStorage` without
1698 // double-borrowing self.jit.
1699 let jit = &mut self.jit;
1700 jit.storage.claim(self.jit_owner_id);
1701 let storage: &mut dyn crate::jit::JitStorage = jit.storage.as_mut();
1702 match jit
1703 .chunk_compiler
1704 .try_compile(storage, proto, pre53, float_only)
1705 {
1706 crate::jit::CompileResult::Compiled {
1707 entry,
1708 num_args,
1709 returns_one,
1710 arg_float_mask,
1711 arg_table_mask,
1712 ret_is_float,
1713 ret_is_table,
1714 } => {
1715 proto.jit.set(JitProtoState::Compiled {
1716 entry,
1717 num_args,
1718 returns_one,
1719 arg_float_mask,
1720 arg_table_mask,
1721 ret_is_float,
1722 ret_is_table,
1723 });
1724 }
1725 crate::jit::CompileResult::Skipped => {
1726 proto.jit.set(JitProtoState::Failed);
1727 }
1728 }
1729 }
1730
1731 /// `Op::Call` JIT fast path. Run inside `begin_call`
1732 /// before `push_frame`. Returns `true` when the call was handled
1733 /// in-place (no new Lua frame). Constraints: every arg slot must
1734 /// be `Value::Int`, the cached arity must match the call site's
1735 /// `nargs`, the host wanted-count `wanted` is honoured by
1736 /// `finish_results`. Also bails when a debug hook is armed —
1737 /// JIT'd code does not fire line / call / return hooks, so any
1738 /// active hook makes the interpreter the source of truth.
1739 fn try_jit_call_op(
1740 &mut self,
1741 cl: Gc<LuaClosure>,
1742 func_slot: u32,
1743 nargs: u32,
1744 wanted: i32,
1745 ) -> bool {
1746 use crate::runtime::function::JitProtoState;
1747 if !self.jit.enabled {
1748 return false;
1749 }
1750 // Any active debug hook means the interpreter has to run the
1751 // call so the hook gets the expected events.
1752 if self.hook.func.is_some() || self.hook.rust_func.is_some() {
1753 return false;
1754 }
1755 let proto = cl.proto;
1756 if let JitProtoState::Untried = proto.jit.get() {
1757 self.populate_jit_cache(proto);
1758 }
1759 let JitProtoState::Compiled {
1760 entry,
1761 num_args,
1762 returns_one,
1763 arg_float_mask,
1764 arg_table_mask,
1765 ret_is_float,
1766 ret_is_table,
1767 } = proto.jit.get()
1768 else {
1769 return false;
1770 };
1771 if num_args as u32 != nargs {
1772 return false;
1773 }
1774 // Pack args into i64 bit-patterns per the per-slot expected
1775 // kind. A Float-typed slot accepts Value::Float verbatim (and on
1776 // 5.1/5.2 promotes Value::Int(x) via i64 → f64); a Table-typed slot
1777 // accepts only Value::Table and passes the raw Gc ptr; an
1778 // Int-typed slot accepts only Value::Int. Any other shape
1779 // bails to the interpreter so the call's actual dynamics
1780 // (metamethod dispatch / type-coerce) take over.
1781 let mut args: [i64; crate::jit::MAX_JIT_ARITY as usize] =
1782 [0; crate::jit::MAX_JIT_ARITY as usize];
1783 // From 5.3 an integer is its own subtype: turned into a float for
1784 // a float-typed parameter, it would come back out (returned,
1785 // stored, printed) as a float. Only 5.1/5.2, where every number
1786 // is a float, may convert it.
1787 let int_as_float = self.version() <= crate::version::LuaVersion::Lua52;
1788 for i in 0..num_args as usize {
1789 let v = self.stack[(func_slot + 1) as usize + i];
1790 let want_float = (arg_float_mask >> i) & 1 == 1;
1791 let want_table = (arg_table_mask >> i) & 1 == 1;
1792 args[i] = match (want_table, want_float, v) {
1793 (true, _, Value::Table(t)) => t.as_ptr() as i64,
1794 (false, false, Value::Int(x)) => x,
1795 (false, true, Value::Float(f)) => f.to_bits() as i64,
1796 (false, true, Value::Int(x)) if int_as_float => (x as f64).to_bits() as i64,
1797 _ => return false,
1798 };
1799 }
1800 // Vm + closure pin for helpers, routed through chunk_compiler;
1801 // see the matching guard in `try_jit_call`.
1802 let vm_ptr: *mut Vm = self;
1803 let _jit_vm_guard = self.jit.chunk_compiler.enter(vm_ptr, Some(cl));
1804 // 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.
1805 let r = unsafe {
1806 match num_args {
1807 0 => (std::mem::transmute::<*const u8, crate::jit::IntChunkFn>(entry))(),
1808 1 => (std::mem::transmute::<*const u8, crate::jit::IntFn1>(entry))(args[0]),
1809 2 => {
1810 (std::mem::transmute::<*const u8, crate::jit::IntFn2>(entry))(args[0], args[1])
1811 }
1812 3 => (std::mem::transmute::<*const u8, crate::jit::IntFn3>(entry))(
1813 args[0], args[1], args[2],
1814 ),
1815 4 => (std::mem::transmute::<*const u8, crate::jit::IntFn4>(entry))(
1816 args[0], args[1], args[2], args[3],
1817 ),
1818 _ => unreachable!("MAX_JIT_ARITY enforces num_args <= 4"),
1819 }
1820 };
1821 drop(_jit_vm_guard);
1822 // See matching path in `try_jit_call`. A helper
1823 // flagged a metatable on a table operand; bail to the interpreter
1824 // so `push_frame` runs the call from scratch.
1825 if self.jit.pending_err.take().is_some() {
1826 return false;
1827 }
1828 // Write result at func_slot, replacing the closure value, then
1829 // hand to finish_results to pad/truncate per the call site's
1830 // `wanted` count.
1831 if returns_one {
1832 let v = if ret_is_float {
1833 Value::Float(f64::from_bits(r as u64))
1834 } else if ret_is_table {
1835 Value::Table(crate::runtime::Gc::from_ptr(
1836 r as *mut crate::runtime::Table,
1837 ))
1838 } else {
1839 Value::Int(r)
1840 };
1841 self.stack[func_slot as usize] = v;
1842 self.finish_results(func_slot, 1, wanted);
1843 } else {
1844 self.finish_results(func_slot, 0, wanted);
1845 }
1846 true
1847 }
1848
1849 /// `call_value` with control over the `from_c` debug boundary. A `__close`
1850 /// handler runs *within* the closing Lua frame's activation (PUC luaF_close
1851 /// invokes it inside that ci), so it is called with `from_c = false`: its
1852 /// debug parent is the closing function, not a synthetic C level.
1853 fn call_value_impl(
1854 &mut self,
1855 f: Value,
1856 args: &[Value],
1857 from_c: bool,
1858 ) -> Result<Vec<Value>, LuaError> {
1859 if self.c_depth >= MAX_C_DEPTH {
1860 // PUC `luaE_checkcstack`: at the limit the call fails; an xpcall
1861 // handler running on the error gets a tenth more room before its
1862 // own failure is "error in error handling"
1863 if self.msgh_depth == 0 {
1864 return Err(self.runerror("C stack overflow"));
1865 }
1866 if self.c_depth >= MAX_C_DEPTH / 10 * 11 {
1867 return Err(self.plain_err("error in error handling"));
1868 }
1869 }
1870 self.c_depth += 1;
1871 let func_slot = self.stack.len() as u32;
1872 self.stack.push(f);
1873 self.stack.extend_from_slice(args);
1874 self.top = self.stack.len() as u32;
1875 let r = self.call_at(func_slot, args.len() as u32, from_c);
1876 self.c_depth -= 1;
1877 if r.is_err()
1878 && self.yielding.is_none()
1879 && self.terminating.is_none()
1880 && !self.host_yield_pending
1881 && self.pending_async_native_fut.is_none()
1882 {
1883 // A `coroutine.yield` in flight raises a sentinel error to unwind the
1884 // Rust stack, but the suspended coroutine's frames/registers (which
1885 // sit at/above `func_slot`) must survive for the next resume — so we
1886 // only truncate on a real error. A self-close termination is in the
1887 // same boat: the dying thread's state is discarded wholesale.
1888 // A `host_yield_pending` cooperative yield is in
1889 // the same boat as `yielding`: the next `EvalFuture::poll`
1890 // resumes the same call, so the in-flight frames must
1891 // survive.
1892 self.stack.truncate(func_slot as usize);
1893 self.top = func_slot;
1894 }
1895 r
1896 }
1897
1898 /// Invoke `f` with the running thread marked non-yieldable for the duration
1899 /// (PUC `luaD_callnoyield`): a `coroutine.yield` inside `f` hits the C-call
1900 /// boundary and errors instead of suspending. Used by library callbacks
1901 /// (sort comparator, gsub replacement) that run via synchronous Rust
1902 /// recursion and so could not be re-entered after a yield.
1903 pub(crate) fn call_noyield(
1904 &mut self,
1905 f: Value,
1906 args: &[Value],
1907 ) -> Result<Vec<Value>, LuaError> {
1908 self.nny += 1;
1909 let r = self.call_value(f, args);
1910 self.nny -= 1;
1911 r
1912 }
1913
1914 // ---- coroutines ----
1915
1916 pub(crate) fn new_coro(&mut self, body: Value) -> Gc<Coro> {
1917 // The new coroutine inherits the creating thread's current globals
1918 // (PUC `lua_newthread`: the new state copies `g->mainthread`'s
1919 // `l_gt`). `Vm.globals` always reflects the live thread, so reading
1920 // it here picks the creator regardless of which coro is running.
1921 self.heap.new_coro(body, self.globals)
1922 }
1923
1924 /// Is `t` the thread whose context is currently live in the VM?
1925 pub(crate) fn is_current_thread(&self, t: Option<Gc<Coro>>) -> bool {
1926 match (self.current, t) {
1927 (None, None) => true,
1928 (Some(a), Some(b)) => a.ptr_eq(b),
1929 _ => false,
1930 }
1931 }
1932
1933 /// Read an open-upvalue slot from its owning thread's stack (the live VM
1934 /// stack if that thread is current, else its saved context).
1935 #[doc(hidden)]
1936 pub fn read_slot(&self, slot: u32, thread: Option<Gc<Coro>>) -> Value {
1937 let s = slot as usize;
1938 if self.is_current_thread(thread) {
1939 self.stack[s]
1940 } else {
1941 match thread {
1942 Some(co) => co.stack[s],
1943 None => self.main_ctx.as_ref().expect("main context").stack[s],
1944 }
1945 }
1946 }
1947
1948 fn write_slot(&mut self, slot: u32, thread: Option<Gc<Coro>>, v: Value) {
1949 let s = slot as usize;
1950 if self.is_current_thread(thread) {
1951 self.stack[s] = v;
1952 } else {
1953 match thread {
1954 Some(co) => {
1955 // 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).
1956 unsafe { co.as_mut() }.stack[s] = v;
1957 // co.stack is traced by Coro::trace; demote co back to
1958 // gray so propagate re-traces this slot if it was
1959 // already black.
1960 self.heap
1961 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
1962 }
1963 None => self.main_ctx.as_mut().expect("main context").stack[s] = v,
1964 }
1965 }
1966 }
1967
1968 /// Whether `co` is the main thread's identity object.
1969 pub(crate) fn is_main_coro(&self, co: Gc<Coro>) -> bool {
1970 self.main_coro.is_some_and(|m| m.ptr_eq(co))
1971 }
1972
1973 /// The status of `co` from the caller's view. The main thread's identity
1974 /// object has no stored status — it is "running" when nothing else runs,
1975 /// else "normal" (it resumed the active coroutine).
1976 pub(crate) fn effective_coro_status(&self, co: Gc<Coro>) -> CoroStatus {
1977 if self.is_main_coro(co) {
1978 if self.current.is_none() {
1979 CoroStatus::Running
1980 } else {
1981 CoroStatus::Normal
1982 }
1983 } else {
1984 co.status
1985 }
1986 }
1987
1988 /// `coroutine.close` (PUC `lua_closethread`): run the suspended coroutine's
1989 /// pending to-be-closed `__close` handlers, then mark it dead and drop its
1990 /// context. Handlers see the coroutine's death error (if it died by error)
1991 /// or nil; an error they raise propagates out. `Ok(Some(e))` means it died
1992 /// with error `e` and no handler overrode it; `Err` means a handler raised.
1993 pub(crate) fn close_coro(&mut self, co: Gc<Coro>) -> Result<Option<Value>, LuaError> {
1994 // re-entrant close: a __close handler closed its own coroutine while the
1995 // outer close is mid-flight (its context is live). Report success and let
1996 // the outer close finish — re-entering the swap would corrupt the stack.
1997 if self.current.is_some_and(|c| c.ptr_eq(co)) {
1998 return Ok(None);
1999 }
2000 // A chain of coroutines whose `__close` handlers each close the previous
2001 // one recurses on the C stack (PUC `luaD_callnoyield` in `lua_closethread`).
2002 // The calling handler's `call_value` has already pushed `c_depth` to the
2003 // cap, so here it reads as full first — report PUC's "C stack overflow"
2004 // before the next handler call would surface the plainer "stack overflow".
2005 if self.c_depth >= MAX_C_DEPTH {
2006 return Err(self.rt_err("C stack overflow"));
2007 }
2008 // 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).
2009 let death_err = unsafe { co.as_mut() }.error_value.take();
2010 // swap the caller's live context out (into a GC-rooted home) and the
2011 // coroutine's in, mirroring resume_coro, so the __close handlers run on
2012 // the coroutine's stack while everything stays rooted.
2013 let resumer = self.current;
2014 let rctx = self.take_ctx();
2015 match resumer {
2016 Some(r) => {
2017 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
2018 let m = unsafe { r.as_mut() };
2019 m.stack = rctx.stack;
2020 m.frames = rctx.frames;
2021 m.frame_ccmt = rctx.frame_ccmt;
2022 m.open_upvals = rctx.open_upvals;
2023 m.tbc = rctx.tbc;
2024 m.top = rctx.top;
2025 m.pcall_depth = rctx.pcall_depth;
2026 }
2027 None => self.main_ctx = Some(rctx),
2028 }
2029 self.load_coro_ctx(co);
2030 self.current = Some(co);
2031 // PUC `luaE_resetthread` closes with no message handler, whatever
2032 // xpcall the coroutine was suspended in
2033 let natives_base = std::mem::replace(&mut self.natives_base, self.running_natives.len());
2034 let msgh_floor = std::mem::replace(&mut self.msgh_floor, self.frames.len());
2035 let result = self.close_slots(0, death_err);
2036 self.natives_base = natives_base;
2037 self.msgh_floor = msgh_floor;
2038 // discard the (now-closed) coroutine context and restore the caller
2039 let _ = self.take_ctx();
2040 match resumer {
2041 Some(r) => {
2042 self.load_coro_ctx(r);
2043 self.current = Some(r);
2044 }
2045 None => {
2046 let m = self.main_ctx.take().expect("main context saved");
2047 self.put_ctx(m);
2048 self.current = None;
2049 }
2050 }
2051 {
2052 // 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).
2053 let m = unsafe { co.as_mut() };
2054 m.status = CoroStatus::Dead;
2055 m.stack = Vec::new();
2056 m.frames = Vec::new();
2057 m.frame_ccmt = Vec::new();
2058 m.open_upvals = Vec::new();
2059 m.tbc = Vec::new();
2060 m.top = 0;
2061 m.pcall_depth = 0;
2062 m.resume_at = None;
2063 m.error_value = None;
2064 m.error_traceback = None;
2065 m.error_levels = None;
2066 }
2067 result.map(|()| death_err)
2068 }
2069
2070 /// `coroutine.running`: the running thread plus whether it is the main one.
2071 pub(crate) fn running_thread(&self) -> (Value, bool) {
2072 match self.current {
2073 Some(co) => (Value::Coro(co), false),
2074 None => (Value::Coro(self.main_coro.expect("main coro")), true),
2075 }
2076 }
2077
2078 /// `coroutine.isyieldable([co])`: whether `co` (default: the running
2079 /// thread) can yield. The main thread never can; any other coroutine can
2080 /// unless it is dead.
2081 pub(crate) fn is_yieldable(&self, co: Option<Gc<Coro>>) -> bool {
2082 match co {
2083 Some(c) => !self.main_coro.is_some_and(|m| m.ptr_eq(c)) && c.status != CoroStatus::Dead,
2084 // the running thread can yield only outside any non-yieldable C call
2085 None => self.current.is_some() && self.nny == 0,
2086 }
2087 }
2088
2089 /// Why `coroutine.yield` may not suspend the running thread right now, as a
2090 /// PUC error message — `None` if it may. Distinguishes "not in a coroutine"
2091 /// from "inside an unyieldable C call" (sort/gsub callback).
2092 pub(crate) fn yield_barrier(&self) -> Option<&'static str> {
2093 // 5.1's pcall/xpcall are plain C calls (no continuations), so a yield
2094 // below one crosses the boundary like any other; 5.1 also has a single
2095 // wording for every case, the main thread included.
2096 // 5.1 also calls every metamethod and generic-for iterator through
2097 // `luaD_call`, which counts as a C level, so a yield from inside one
2098 // is refused as well.
2099 if self.version <= LuaVersion::Lua51 {
2100 let inside_call = self.frames.iter().enumerate().any(|(i, f)| match f {
2101 CallFrame::Cont(nc) => matches!(nc.kind, ContKind::Meta(_)),
2102 CallFrame::Lua(fr) => {
2103 fr.tm.is_some()
2104 || (i > 0
2105 && self.frames[i - 1].lua().is_some_and(|c| {
2106 let pc = (c.pc as usize).wrapping_sub(1);
2107 c.closure
2108 .proto
2109 .code
2110 .get(pc)
2111 .is_some_and(|ins| ins.op() == Op::TForCall)
2112 }))
2113 }
2114 });
2115 if self.current.is_none() || self.nny > 0 || self.pcall_depth > 0 || inside_call {
2116 return Some("attempt to yield across metamethod/C-call boundary");
2117 }
2118 return None;
2119 }
2120 if self.current.is_none() {
2121 Some("attempt to yield from outside a coroutine")
2122 } else if self.nny > 0 {
2123 Some("attempt to yield across a C-call boundary")
2124 } else {
2125 None
2126 }
2127 }
2128
2129 /// The coroutine whose context is currently live (`None` on the main thread).
2130 pub(crate) fn current_coro(&self) -> Option<Gc<Coro>> {
2131 self.current
2132 }
2133
2134 /// `coroutine.close()` on the *running* thread (PUC 5.5 close-self): run all
2135 /// its pending `__close` handlers, then signal termination. The handlers run
2136 /// here, in place, with the thread still non-yieldable (a yield in one hits
2137 /// the C-call boundary). The returned sentinel unwinds the Rust stack the
2138 /// way a yield does — `exec_with` propagates it past any protecting pcall
2139 /// rather than letting `unwind` catch it — and `resume_coro` turns it into a
2140 /// clean death (or, if a handler raised, the coroutine's error).
2141 pub(crate) fn close_running(&mut self) -> LuaError {
2142 let death = match self.close_slots(0, None) {
2143 Ok(()) => None,
2144 Err(e) => Some(e.0),
2145 };
2146 self.terminating = Some(death);
2147 LuaError(Value::Nil)
2148 }
2149
2150 /// `coroutine.status` as seen by the caller.
2151 pub(crate) fn coro_status_str(&self, co: Gc<Coro>) -> &'static str {
2152 match self.effective_coro_status(co) {
2153 CoroStatus::Suspended => "suspended",
2154 CoroStatus::Running => "running",
2155 CoroStatus::Normal => "normal",
2156 CoroStatus::Dead => "dead",
2157 }
2158 }
2159
2160 fn take_ctx(&mut self) -> SavedCtx {
2161 let saved = SavedCtx {
2162 stack: std::mem::take(&mut self.stack),
2163 frames: std::mem::take(&mut self.frames),
2164 frame_ccmt: std::mem::take(&mut self.frame_ccmt),
2165 open_upvals: std::mem::take(&mut self.open_upvals),
2166 tbc: std::mem::take(&mut self.tbc),
2167 top: self.top,
2168 pcall_depth: self.pcall_depth,
2169 hook: self.hook,
2170 globals: self.globals,
2171 };
2172 self.frames_resync(); // frames now empty
2173 saved
2174 }
2175
2176 fn put_ctx(&mut self, c: SavedCtx) {
2177 self.stack = c.stack;
2178 self.frames = c.frames;
2179 self.frame_ccmt = c.frame_ccmt;
2180 self.open_upvals = c.open_upvals;
2181 self.tbc = c.tbc;
2182 self.top = c.top;
2183 self.pcall_depth = c.pcall_depth;
2184 self.hook = c.hook;
2185 self.globals = c.globals;
2186 self.frames_resync(); // sync shadow to new Vec
2187 }
2188
2189 /// Move a coroutine's saved context into the live VM fields.
2190 fn load_coro_ctx(&mut self, co: Gc<Coro>) {
2191 // 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).
2192 let m = unsafe { co.as_mut() };
2193 self.stack = std::mem::take(&mut m.stack);
2194 self.frames = std::mem::take(&mut m.frames);
2195 self.frame_ccmt = std::mem::take(&mut m.frame_ccmt);
2196 self.open_upvals = std::mem::take(&mut m.open_upvals);
2197 self.tbc = std::mem::take(&mut m.tbc);
2198 self.top = m.top;
2199 self.frames_resync(); // sync shadow to coro's frames
2200 self.pcall_depth = m.pcall_depth;
2201 self.hook = m.hook;
2202 self.globals = m.globals;
2203 }
2204
2205 /// Save the live VM context back into a coroutine object.
2206 fn store_coro_ctx(&mut self, co: Gc<Coro>) {
2207 let c = self.take_ctx();
2208 // 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).
2209 let m = unsafe { co.as_mut() };
2210 m.stack = c.stack;
2211 m.frames = c.frames;
2212 m.frame_ccmt = c.frame_ccmt;
2213 m.open_upvals = c.open_upvals;
2214 m.tbc = c.tbc;
2215 m.top = c.top;
2216 m.pcall_depth = c.pcall_depth;
2217 m.hook = c.hook;
2218 m.globals = c.globals;
2219 // bulk-overwrite of every collectable field traced by Coro::trace:
2220 // demote the coro back to gray so propagate re-traces its new state.
2221 self.heap
2222 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
2223 }
2224
2225 /// `coroutine.resume` core: drive `co` with `args` until it yields, returns
2226 /// or errors. Ok(values) carries yielded or returned values; Err carries an
2227 /// error raised inside the coroutine (the coroutine becomes dead).
2228 pub(crate) fn resume_coro(
2229 &mut self,
2230 co: Gc<Coro>,
2231 args: Vec<Value>,
2232 ) -> Result<Vec<Value>, LuaError> {
2233 match co.status {
2234 CoroStatus::Suspended => {}
2235 CoroStatus::Dead => return Err(self.plain_err("cannot resume dead coroutine")),
2236 _ => return Err(self.plain_err("cannot resume non-suspended coroutine")),
2237 }
2238 if self.c_depth >= MAX_C_DEPTH {
2239 return Err(self.plain_err("C stack overflow"));
2240 }
2241 self.c_depth += 1;
2242 let resumer = self.current;
2243 // save the resumer's live context away
2244 let rctx = self.take_ctx();
2245 match resumer {
2246 Some(r) => {
2247 // 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).
2248 let m = unsafe { r.as_mut() };
2249 m.stack = rctx.stack;
2250 m.frames = rctx.frames;
2251 m.frame_ccmt = rctx.frame_ccmt;
2252 m.open_upvals = rctx.open_upvals;
2253 m.tbc = rctx.tbc;
2254 m.top = rctx.top;
2255 m.pcall_depth = rctx.pcall_depth;
2256 m.globals = rctx.globals;
2257 m.status = CoroStatus::Normal;
2258 m.natives = self.natives_base..self.running_natives.len();
2259 // bulk overwrite of every traced field on r — mirror
2260 // store_coro_ctx's barrier_back so propagate re-traces r.
2261 self.heap
2262 .barrier_back(r.as_ptr() as *mut crate::runtime::heap::GcHeader);
2263 }
2264 None => self.main_ctx = Some(rctx),
2265 }
2266 // swap the coroutine in
2267 self.load_coro_ctx(co);
2268 {
2269 // 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).
2270 let m = unsafe { co.as_mut() };
2271 m.status = CoroStatus::Running;
2272 m.resumer = resumer;
2273 }
2274 // co.resumer is a traced Gc field; barrier_back covers the new
2275 // resumer reference and any future field writes during this call.
2276 self.heap
2277 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
2278 self.current = Some(co);
2279 let resumer_natives_base = self.natives_base;
2280 self.natives_base = self.running_natives.len();
2281 // the coroutine's own frames start a fresh reach for xpcall handlers
2282 let resumer_msgh_floor = std::mem::replace(&mut self.msgh_floor, 0);
2283 let resumer_msgh_running = self.msgh_running.take();
2284 // a coroutine that dies keeps its traceback for `debug.traceback(co)`
2285 let resumer_keeps_traceback = std::mem::replace(&mut self.keep_error_traceback, true);
2286
2287 // drive it
2288 let drive = if co.started {
2289 self.coro_continue(&args)
2290 } else {
2291 // 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).
2292 unsafe { co.as_mut() }.started = true;
2293 self.coro_first(co.body, &args)
2294 };
2295
2296 // classify: a self-close termination or a pending yield each win over
2297 // the (sentinel) error they raised to unwind the Rust stack.
2298 let (outcome, status) = if let Some(death) = self.terminating.take() {
2299 // the coroutine closed itself: it dies now, cleanly or with the
2300 // error a `__close` handler raised.
2301 match death {
2302 Some(e) => {
2303 // 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).
2304 unsafe { co.as_mut() }.error_value = Some(e);
2305 self.heap
2306 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
2307 (Err(LuaError(e)), CoroStatus::Dead)
2308 }
2309 None => (Ok(Vec::new()), CoroStatus::Dead),
2310 }
2311 } else {
2312 match self.yielding.take() {
2313 Some((vals, fslot, nres)) => {
2314 // 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).
2315 unsafe { co.as_mut() }.resume_at = Some((fslot, nres));
2316 (Ok(vals), CoroStatus::Suspended)
2317 }
2318 None => {
2319 // died: a return is clean, an error is remembered so a later
2320 // `coroutine.close` can report it (PUC lua_closethread).
2321 // Keep the error-point traceback (taken by `unwind` before
2322 // popping the failing frames) so `debug.traceback(co)` on
2323 // the dead coroutine still shows the error site, as PUC's
2324 // untouched dead stack does (db.lua :848 family).
2325 if drive.is_err() {
2326 let levels = self.error_traceback.take().unwrap_or_default();
2327 let tb =
2328 crate::vm::callstack::traceback_from_lines(self.version, &levels, 0);
2329 // 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).
2330 let m = unsafe { co.as_mut() };
2331 m.error_traceback = Some(tb);
2332 m.error_levels = Some(levels);
2333 }
2334 if let Err(e) = drive {
2335 // 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).
2336 unsafe { co.as_mut() }.error_value = Some(e.0);
2337 self.heap
2338 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
2339 }
2340 (drive, CoroStatus::Dead)
2341 }
2342 }
2343 };
2344
2345 // save the coroutine's context back and restore the resumer
2346 self.natives_base = resumer_natives_base;
2347 self.msgh_floor = resumer_msgh_floor;
2348 self.msgh_running = resumer_msgh_running;
2349 self.keep_error_traceback = resumer_keeps_traceback;
2350 self.store_coro_ctx(co);
2351 // 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).
2352 unsafe { co.as_mut() }.status = status;
2353 match resumer {
2354 Some(r) => {
2355 self.load_coro_ctx(r);
2356 // 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).
2357 unsafe { r.as_mut() }.status = CoroStatus::Running;
2358 self.current = Some(r);
2359 }
2360 None => {
2361 let m = self.main_ctx.take().expect("main context saved");
2362 self.put_ctx(m);
2363 self.current = None;
2364 }
2365 }
2366 self.c_depth -= 1;
2367 outcome
2368 }
2369
2370 /// First resume: install the body function at slot 0 and run.
2371 fn coro_first(&mut self, body: Value, args: &[Value]) -> Result<Vec<Value>, LuaError> {
2372 self.stack.clear();
2373 self.stack.push(body);
2374 self.stack.extend_from_slice(args);
2375 self.top = self.stack.len() as u32;
2376 match self.begin_call(0, Some(args.len() as u32), -1, true) {
2377 Ok(true) => self.exec_with(1),
2378 Ok(false) => Ok(self.take_results(0)),
2379 Err(e) => Err(e),
2380 }
2381 }
2382
2383 /// Resume after a yield: deliver `args` as the results of the call that
2384 /// yielded, then continue the suspended thread.
2385 fn coro_continue(&mut self, args: &[Value]) -> Result<Vec<Value>, LuaError> {
2386 let (fslot, nres) = self.current.unwrap().resume_at.expect("resume point");
2387 let n = args.len() as u32;
2388 // Restore the full register window of the suspended top frame: a yield
2389 // that unwound through a native (call_value) may have left the stack
2390 // shorter than the frame needs. `base + max_stack` is what push_frame
2391 // allocates; `fslot + n` covers the delivered yield results.
2392 let frame_need = self
2393 .frames
2394 .last()
2395 .and_then(CallFrame::lua)
2396 .map(|f| (f.base + f.closure.proto.max_stack as u32) as usize)
2397 .unwrap_or(0);
2398 let need = frame_need.max((fslot + n) as usize);
2399 if self.stack.len() < need {
2400 self.stack.resize(need, Value::Nil);
2401 }
2402 for (i, &v) in args.iter().enumerate() {
2403 self.stack[fslot as usize + i] = v;
2404 }
2405 self.finish_results(fslot, n, nres);
2406 // the suspended `coroutine.yield` (a C call) now returns its resume
2407 // values: fire the matching "return" hook PUC defers until the resume.
2408 self.hook_return(true, 1, n)?;
2409 self.exec_with(1)
2410 }
2411
2412 /// `coroutine.yield`: suspend the running coroutine, recording where to
2413 /// resume. Errors if called outside a coroutine. Returns a sentinel error
2414 /// that `exec`/`resume_coro` recognise as a yield (never surfaced to Lua).
2415 pub(crate) fn do_yield(&mut self, func_slot: u32, vals: Vec<Value>) -> LuaError {
2416 let nres = self.native_nresults;
2417 self.yielding = Some((vals, func_slot, nres));
2418 // value is irrelevant: resume_coro consults `self.yielding`, not this
2419 LuaError(Value::Nil)
2420 }
2421
2422 /// Install or clear the debug hook on the running thread (`debug.sethook`
2423 /// without a thread argument). Arms the calling frame's `oldpc` to the
2424 /// sethook CALL's own pc (one less than the next-to-execute pc), mirroring
2425 /// PUC `rethook`'s `L->oldpc = pcRel(savedpc, p)` (= savedpc - code - 1) on
2426 /// native return: the very next traceexec compares against the sethook
2427 /// CALL's line. When the install statement and the following statement are
2428 /// on different source lines (db.lua :322), `changedline` fires for that
2429 /// first statement; when they share a line (db.lua :25 wrapper), they do
2430 /// not, so the wrapper line is not re-fired.
2431 pub(crate) fn install_hook(&mut self, hook: HookState) {
2432 self.hook = hook;
2433 if self.hook.line
2434 && let Some(f) = self.frames.last_mut().and_then(CallFrame::lua_mut)
2435 {
2436 f.hook_oldpc = f.pc.saturating_sub(1);
2437 }
2438 }
2439
2440 /// Install a hook on `target` (`None`/current thread → the live VM fields;
2441 /// another, suspended thread → its saved `Coro` state). PUC `debug.sethook`
2442 /// with an optional thread argument.
2443 ///
2444 /// `target == None` means "no explicit thread argument" — PUC binds that
2445 /// to `L` (the running thread). luna's live VM fields (`self.hook`,
2446 /// `self.frames`, `self.stack`) ARE the running thread's state, regardless
2447 /// of whether that's the main thread or a currently-resumed coroutine
2448 /// (save/restore happens at resume/yield boundaries via `load_coro_ctx`/
2449 /// `store_coro_ctx`). So a `None` target should always route to
2450 /// `install_hook` on the live fields. The pre-fix predicate gate
2451 /// `is_current_thread(target)` returned `false` when running inside a
2452 /// coroutine (`self.current = Some(co)`, `target = None` don't match)
2453 /// and silently dropped the hook on the floor — the install happened on
2454 /// no thread at all.
2455 pub(crate) fn set_hook(&mut self, target: Option<Gc<Coro>>, state: HookState) {
2456 if target.is_none() || self.is_current_thread(target) {
2457 self.install_hook(state);
2458 } else if let Some(co) = target {
2459 // 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).
2460 let m = unsafe { co.as_mut() };
2461 m.hook = state;
2462 if state.line
2463 && let Some(f) = m.frames.last_mut().and_then(CallFrame::lua_mut)
2464 {
2465 f.hook_oldpc = u32::MAX;
2466 }
2467 // co.hook.func is a traced Value (Coro::trace covers it); demote
2468 // co back to gray so propagate sees the new hook function.
2469 self.heap
2470 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
2471 }
2472 }
2473
2474 /// The hook state of `target` (`None`/current → the live VM state).
2475 pub(crate) fn get_hook(&self, target: Option<Gc<Coro>>) -> HookState {
2476 match target {
2477 t if self.is_current_thread(t) => self.hook,
2478 Some(co) => co.hook,
2479 None => self.hook,
2480 }
2481 }
2482
2483 /// Invoke the debug hook for `event` (PUC `luaD_hook`). The hook runs with
2484 /// hooks disabled (PUC clears the mask) and its results/stack growth are
2485 /// discarded so the interrupted frame's register window is untouched.
2486 /// `line` is the source line for a "line" event, `None` (→ nil) otherwise.
2487 fn run_hook(
2488 &mut self,
2489 event: &[u8],
2490 line: Option<i64>,
2491 from_native: bool,
2492 ) -> Result<(), LuaError> {
2493 // line and count events transfer no values (PUC `luaD_hook(L,
2494 // event, line, 0, 0)`); call and return hooks set theirs first
2495 if matches!(event, b"line" | b"count") {
2496 self.hook_ftransfer = 0;
2497 self.hook_ntransfer = 0;
2498 }
2499 // Rust hook fires first (no Vm reentrancy via call_value;
2500 // synchronous fn pointer call). Both Rust and Lua hooks may be
2501 // installed; both observe each event.
2502 if let Some(rh) = self.hook.rust_func {
2503 let evt = match event {
2504 b"call" => Some(RustHookEvent::Call),
2505 b"return" => Some(RustHookEvent::Return),
2506 b"tail call" | b"tail return" => Some(RustHookEvent::TailCall),
2507 b"line" => Some(RustHookEvent::Line(line.unwrap_or(0).max(0) as u32)),
2508 b"count" => Some(RustHookEvent::Count),
2509 _ => None,
2510 };
2511 if let Some(evt) = evt {
2512 let was_in_hook = self.in_hook;
2513 self.in_hook = true;
2514 rh(self, evt);
2515 self.in_hook = was_in_hook;
2516 }
2517 }
2518 let Some(hook) = self.hook.func else {
2519 return Ok(());
2520 };
2521 let saved_top = self.top;
2522 let saved_len = self.stack.len();
2523 let name = Value::Str(self.heap.intern(event));
2524 let lv = line.map_or(Value::Nil, Value::Int);
2525 self.in_hook = true;
2526 // PUC `db_sethook`'s C trampoline `hookf` sits between the engine and
2527 // the Lua hook — so `getinfo(2)` inside the hook resolves to whatever
2528 // ci sat below `hookf` (the function being hooked). When that hooked
2529 // function is native, no Lua frame for it exists in luna's `frames`;
2530 // model it as a synthetic C level by pushing the hook with
2531 // `from_c = true` (then `c_frame_name` reads the caller's call
2532 // instruction → e.g. `name = "sethook"`). When the hooked function is
2533 // Lua (its frame is still on the stack), push with `from_c = false`
2534 // so the level descent lands on it directly. The hook's own frame
2535 // carries `is_hook = true` so `getinfo(1).namewhat` reports "hook"
2536 // (PUC `CIST_HOOKED`).
2537 self.pending_is_hook = true;
2538 let r = self.call_value_impl(hook, &[name, lv], from_native);
2539 self.pending_is_hook = false;
2540 self.in_hook = false;
2541 self.stack.truncate(saved_len);
2542 self.top = saved_top;
2543 r.map(|_| ())
2544 }
2545
2546 /// Fire the "call" hook on entry to a function, if armed and not already in
2547 /// a hook (PUC clears the mask while a hook runs). PUC's transferinfo for
2548 /// a call hook is the param window: ftransfer = 1, ntransfer = nargs.
2549 /// `is_tail` selects the "tail call" event (PUC `LUA_HOOKTAILCALL`); a
2550 /// tail-call hook has no matching return hook (PUC luaD_pretailcall).
2551 fn hook_call_with(
2552 &mut self,
2553 from_native: bool,
2554 nargs: u32,
2555 is_tail: bool,
2556 ) -> Result<(), LuaError> {
2557 if self.hook.call
2558 && !self.in_hook
2559 && (self.hook.func.is_some() || self.hook.rust_func.is_some())
2560 {
2561 self.hook_ftransfer = 1;
2562 self.hook_ntransfer = nargs.min(u16::MAX as u32) as u16;
2563 // PUC 5.1 didn't distinguish tail-call events — every call,
2564 // including tail-calls, fired plain `"call"`. 5.2 introduced
2565 // the separate `"tail call"` event (mask `"c"` covers both).
2566 // 5.1 db.lua :366 pins this with `{"call","call","call","call",
2567 // "return","tail return","return","tail return"}`.
2568 let event: &[u8] = if is_tail && self.version >= LuaVersion::Lua52 {
2569 b"tail call"
2570 } else {
2571 b"call"
2572 };
2573 self.run_hook(event, None, from_native)?;
2574 }
2575 Ok(())
2576 }
2577
2578 pub(crate) fn hook_call(&mut self, from_native: bool, nargs: u32) -> Result<(), LuaError> {
2579 self.hook_call_with(from_native, nargs, false)
2580 }
2581
2582 /// Fire the "return" hook on exit from a function, if armed. ftransfer is
2583 /// the first result slot relative to the activation's func slot, ntransfer
2584 /// the number of results.
2585 pub(crate) fn hook_return(
2586 &mut self,
2587 from_native: bool,
2588 ftransfer: u32,
2589 nresults: u32,
2590 ) -> Result<(), LuaError> {
2591 if self.hook.ret
2592 && !self.in_hook
2593 && (self.hook.func.is_some() || self.hook.rust_func.is_some())
2594 {
2595 self.hook_ftransfer = ftransfer.min(u16::MAX as u32) as u16;
2596 self.hook_ntransfer = nresults.min(u16::MAX as u32) as u16;
2597 self.run_hook(b"return", None, from_native)?;
2598 }
2599 Ok(())
2600 }
2601
2602 /// PUC "tail return" event — fires once per tail call that collapsed
2603 /// into the activation now returning, *after* its own "return" event.
2604 /// 5.1 hook mask `"r"` covers both `return` and `tail return`.
2605 fn hook_tail_return(&mut self) -> Result<(), LuaError> {
2606 if self.hook.ret
2607 && !self.in_hook
2608 && (self.hook.func.is_some() || self.hook.rust_func.is_some())
2609 {
2610 self.run_hook(b"tail return", None, false)?;
2611 }
2612 Ok(())
2613 }
2614
2615 /// Call a metamethod with a single expected result.
2616 fn call_mm1(&mut self, f: Value, args: &[Value]) -> Result<Value, LuaError> {
2617 let mut r = self.call_value(f, args)?;
2618 Ok(if r.is_empty() {
2619 Value::Nil
2620 } else {
2621 r.swap_remove(0)
2622 })
2623 }
2624
2625 /// Begin a *yieldable* metamethod call from a VM instruction: `func(args…)`
2626 /// driven through the interpreter loop with a `Meta` continuation, so a
2627 /// `coroutine.yield` inside the metamethod suspends and resumes cleanly.
2628 /// On the metamethod's return the loop head runs `finish_meta(action, …)`.
2629 /// Returns to the caller with the call set up — the opcode arm must do no
2630 /// further work on the running frame and let the loop iterate. `tm` is
2631 /// the metamethod event name (e.g. "index", "add"); a Lua handler frame
2632 /// born from this call inherits it via `pending_tm`, so
2633 /// `debug.getinfo(1).namewhat == "metamethod"` and `.name == tm`
2634 /// (db.lua :878).
2635 fn begin_meta_call(
2636 &mut self,
2637 func: Value,
2638 args: &[Value],
2639 action: MetaAction,
2640 tm: &'static str,
2641 ) -> Result<(), LuaError> {
2642 let saved_top = self.top;
2643 let cont_slot = self.stack.len() as u32;
2644 self.stack.push(func);
2645 self.stack.extend_from_slice(args);
2646 self.top = self.stack.len() as u32;
2647 frames_push_sync(
2648 &mut self.frames,
2649 &mut self.frames_top,
2650 CallFrame::Cont(NativeCont {
2651 kind: ContKind::Meta(MetaCont { action, saved_top }),
2652 func_slot: cont_slot,
2653 nresults: 1,
2654 }),
2655 );
2656 let saved_tm = self.pending_tm.replace(tm);
2657 // begin_call drives a Lua metamethod through the loop (returns true) or
2658 // runs a native one inline (returns false, leaving results at cont_slot
2659 // for the loop head to pick up); either way the Meta cont resolves there.
2660 let r = self.begin_call(cont_slot, Some(args.len() as u32), 1, true);
2661 // Native callees never consumed pending_tm (push_frame is only hit on
2662 // a Lua callee); restore so it doesn't leak to a later push_frame.
2663 self.pending_tm = saved_tm;
2664 r?;
2665 Ok(())
2666 }
2667
2668 /// `R[dst] := t[key]` for a VM read opcode, resolving `__index` yieldably.
2669 fn op_index(&mut self, t: Value, key: Value, dst: u32) -> Result<(), LuaError> {
2670 // Read-time probe: a collectable key must be live at
2671 // the moment it is used. O(1) membership test against the
2672 // freed-pointer log — gc-verify diagnostic builds only; exact
2673 // under quarantining allocators (ASAN).
2674 #[cfg(feature = "gc-verify")]
2675 if matches!(key, Value::Str(_)) {
2676 let h = match key {
2677 Value::Str(s) => s.as_ptr() as usize,
2678 _ => unreachable!(),
2679 };
2680 if self.heap.recently_freed.contains(&h) {
2681 let (pc, reg_info) = match self.frames.last() {
2682 Some(CallFrame::Lua(f)) => {
2683 let pc = f.pc as usize;
2684 let inst = f.closure.proto.code.get(pc.wrapping_sub(1));
2685 (
2686 pc,
2687 inst.map(|i| {
2688 format!(
2689 "op[pc-1]={:?} a={} b={} c={} base={}",
2690 i.op(),
2691 i.a(),
2692 i.b(),
2693 i.c(),
2694 f.base
2695 )
2696 })
2697 .unwrap_or_default(),
2698 )
2699 }
2700 _ => (0, String::new()),
2701 };
2702 panic!(
2703 "[gc-verify] op_index READ of dead string key {h:#x} \
2704 (gc_top {}, top {}, pc {pc}, {reg_info})",
2705 self.gc_top, self.top,
2706 );
2707 }
2708 }
2709 match self.index_step(t, key)? {
2710 MmOut::Done(v) => self.stack[dst as usize] = v,
2711 MmOut::Mm { func, recv } => {
2712 self.begin_meta_call(func, &[recv, key], MetaAction::Store { dst }, "index")?;
2713 }
2714 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from index_step"),
2715 }
2716 Ok(())
2717 }
2718
2719 /// `t[key] := v` for a VM write opcode, resolving `__newindex` yieldably.
2720 fn op_newindex(&mut self, t: Value, key: Value, v: Value) -> Result<(), LuaError> {
2721 match self.newindex_step(t, key, v)? {
2722 MmOut::Done(_) => {}
2723 MmOut::Mm { func, recv } => {
2724 self.begin_meta_call(func, &[recv, key, v], MetaAction::Discard, "newindex")?;
2725 }
2726 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from newindex_step"),
2727 }
2728 Ok(())
2729 }
2730
2731 /// Apply a comparison opcode's outcome: a known boolean drives the
2732 /// conditional skip directly; a metamethod is called yieldably, its
2733 /// truthiness driving the skip on return.
2734 fn op_compare(
2735 &mut self,
2736 step: MmOut,
2737 l: Value,
2738 r: Value,
2739 k: bool,
2740 tm: &'static str,
2741 ) -> Result<(), LuaError> {
2742 match step {
2743 MmOut::Done(v) => self.cond_skip(v.truthy(), k),
2744 MmOut::Mm { func, .. } => {
2745 self.begin_meta_call(func, &[l, r], MetaAction::Compare { k, negate: false }, tm)?;
2746 }
2747 MmOut::CompareSynth { func } => {
2748 // ≤5.3 `__le` falls back to `not __lt(r, l)`; the swap and
2749 // negation are driven through `MetaAction::Compare` so the
2750 // metamethod call can yield like any other compare.
2751 self.begin_meta_call(func, &[r, l], MetaAction::Compare { k, negate: true }, "lt")?;
2752 }
2753 }
2754 Ok(())
2755 }
2756
2757 /// Complete a VM instruction whose metamethod just returned `result` (PUC
2758 /// `luaV_finishOp`). The running frame is already back on top.
2759 fn finish_meta(&mut self, action: MetaAction, result: Value) -> Result<(), LuaError> {
2760 match action {
2761 MetaAction::Store { dst } => self.stack[dst as usize] = result,
2762 MetaAction::Discard => {}
2763 MetaAction::Compare { k, negate } => {
2764 let t = if negate {
2765 !result.truthy()
2766 } else {
2767 result.truthy()
2768 };
2769 self.cond_skip(t, k);
2770 }
2771 MetaAction::Concat { dst, base_a } => {
2772 self.stack[dst as usize] = result;
2773 self.top = dst + 1;
2774 self.concat_run(base_a)?;
2775 }
2776 }
2777 Ok(())
2778 }
2779
2780 // ---- metatables ----
2781
2782 pub(crate) fn metatable_of(&self, v: Value) -> Option<Gc<Table>> {
2783 match v {
2784 Value::Table(t) => t.metatable(),
2785 Value::Userdata(u) => u.metatable(),
2786 v => type_mt_slot(v).and_then(|i| self.type_mt[i]),
2787 }
2788 }
2789
2790 /// Set the shared metatable for `v`'s basic type (debug.setmetatable on a
2791 /// non-table). No-op for tables (they carry their own).
2792 pub(crate) fn set_type_metatable(&mut self, v: Value, mt: Option<Gc<Table>>) {
2793 if let Some(i) = type_mt_slot(v) {
2794 self.type_mt[i] = mt;
2795 }
2796 }
2797
2798 /// The metamethod of `v` for `mm`, or nil.
2799 pub(crate) fn get_mm(&self, v: Value, mm: Mm) -> Value {
2800 match self.metatable_of(v) {
2801 Some(mt) => mt.get(Value::Str(self.mm_names[mm as usize])),
2802 None => Value::Nil,
2803 }
2804 }
2805
2806 /// PUC 5.1 `get_compTM`: a comparison metamethod (`__eq` / `__lt` / `__le`)
2807 /// only fires when both operands carry a metatable that exposes the same
2808 /// implementation. Returns the metamethod to call, or `Nil` when no
2809 /// compatible match exists. Used to honour events.lua 5.1 :262's rule
2810 /// that `c == d` (where `d` has no metatable) falls back to raw equality.
2811 pub(crate) fn get_comp_mm(&self, l: Value, r: Value, mm: Mm) -> Value {
2812 let mt1 = self.metatable_of(l);
2813 let Some(mt1) = mt1 else { return Value::Nil };
2814 let key = Value::Str(self.mm_names[mm as usize]);
2815 let tm1 = mt1.get(key);
2816 if tm1.is_nil() {
2817 return Value::Nil;
2818 }
2819 let mt2 = self.metatable_of(r);
2820 let Some(mt2) = mt2 else { return Value::Nil };
2821 if mt1.as_ptr() == mt2.as_ptr() {
2822 return tm1;
2823 }
2824 let tm2 = mt2.get(key);
2825 if tm2.is_nil() {
2826 return Value::Nil;
2827 }
2828 if tm1.raw_eq(tm2) {
2829 return tm1;
2830 }
2831 Value::Nil
2832 }
2833
2834 /// PUC `luaT_objtypename`: the type name shown in error messages. A table
2835 /// or full userdata whose metatable carries a string `__name` reports that
2836 /// (e.g. "FILE*", "My Type") instead of the bare "table"/"userdata".
2837 pub(crate) fn obj_typename(&self, v: Value) -> String {
2838 // `__name` (luaT_objtypename) arrived in 5.3
2839 if self.version >= LuaVersion::Lua53
2840 && matches!(v, Value::Table(_) | Value::Userdata(_))
2841 && let Value::Str(s) = self.get_mm(v, Mm::Name)
2842 {
2843 return String::from_utf8_lossy(s.as_bytes()).into_owned();
2844 }
2845 v.type_name().to_string()
2846 }
2847
2848 fn call_at(
2849 &mut self,
2850 func_slot: u32,
2851 nargs: u32,
2852 from_c: bool,
2853 ) -> Result<Vec<Value>, LuaError> {
2854 let depth = self.frames.len();
2855 match self.begin_call(func_slot, Some(nargs), -1, from_c) {
2856 // run until every frame the call pushed has returned: a pcall /
2857 // xpcall / __pairs continuation sits below the frame of the
2858 // function it called, and it is the continuation that produces
2859 // the call's results (`true, ...`, or `false, msg` on an error)
2860 Ok(true) => self.exec_with(depth + 1),
2861 // native completed inline; results at func_slot..top
2862 Ok(false) => Ok(self.take_results(func_slot)),
2863 // pcall / xpcall pushed their continuation and then failed to
2864 // call their function (`pcall("x")`): the continuation catches
2865 // that error, as it does in the dispatch loop
2866 Err(e)
2867 if self.frames.len() > depth
2868 && self.yielding.is_none()
2869 && self.terminating.is_none()
2870 && !self.host_yield_pending
2871 && self.pending_async_native_fut.is_none() =>
2872 {
2873 match self.unwind(e.0, depth + 1) {
2874 Unwound::Caught => self.exec_with(depth + 1),
2875 Unwound::CaughtReturn(vals) => Ok(vals),
2876 Unwound::Propagated(err) => Err(err),
2877 }
2878 }
2879 Err(e) => Err(e),
2880 }
2881 }
2882
2883 /// Switch the `collectgarbage` mode, returning the previous mode name.
2884 pub(crate) fn gc_switch_mode(&mut self, new: &'static str) -> &'static str {
2885 std::mem::replace(&mut self.gc_mode, new)
2886 }
2887
2888 /// Whether the current `collectgarbage` mode is "generational" (where a
2889 /// "step" is a minor collection — a full atomic pass — rather than a paced
2890 /// incremental sweep).
2891 pub(crate) fn gc_mode_is_generational(&self) -> bool {
2892 self.gc_mode == "generational"
2893 }
2894
2895 /// Current `stepsize` pacing parameter (PUC: 0 means an unbounded step that
2896 /// completes a whole cycle at once).
2897 pub(crate) fn gc_stepsize(&self) -> i64 {
2898 self.gc_stepsize
2899 }
2900
2901 /// Set luna's collector knobs: heap growth before a new cycle (%), sweep
2902 /// work per safe point, and the default step size (0 = a step completes
2903 /// the cycle).
2904 pub(crate) fn set_gc_pacing(&mut self, pause: i64, stepmul: i64, stepsize: i64) {
2905 self.gc_pause = pause;
2906 self.gc_stepmul = stepmul;
2907 self.gc_stepsize = stepsize;
2908 }
2909
2910 /// Interpreter safe-point auto-GC: FULL incremental Propagate + adaptive
2911 /// paced sweep via `Vm::gc_step`.
2912 ///
2913 /// Running Propagate from a safe-point relies on objects being **born
2914 /// black during Propagate**: a newly allocated object never becomes
2915 /// dead-white at the atomic flip.
2916 ///
2917 /// Adaptive budget scales with heap size: 100M-object heap (heavy.lua's
2918 /// `loadrep` stress) gets a 25M-object budget so a cycle completes in
2919 /// O(SWEEP_DIVISOR) safe-points regardless of size.
2920 #[inline(always)]
2921 pub(crate) fn maybe_collect_garbage(&mut self, live_top: u32) {
2922 if self.gc_finalizing {
2923 return;
2924 }
2925 if !self.heap.gc_due() {
2926 return;
2927 }
2928 // Bare `live_top`, no `max(self.top)` widening: every frame-pop
2929 // site (`finish_results`, the Op::TailCall collapse, pcall
2930 // unwind) clears the slots it vacates, mirroring PUC's L->top
2931 // discipline.
2932 self.gc_top = live_top;
2933 // PUC stepmul: % of allocation rate. Higher = more GC work per
2934 // safe-point (lower memory, more CPU). Default 100 = `live / 4` per
2935 // step (~4 safe-points per cycle). stepmul=200 → `live / 2`, etc.
2936 const SWEEP_BASE: usize = 400; // 400 / stepmul=100 = divisor 4
2937 const MIN_BUDGET: usize = 64_000;
2938 let stepmul = self.gc_stepmul.max(1) as usize;
2939 let divisor = (SWEEP_BASE / stepmul).max(1);
2940 let budget = (self.heap.live_objects() / divisor).max(MIN_BUDGET);
2941 if self.gc_step(budget) {
2942 self.heap.rearm_gc_pause(self.gc_pause);
2943 }
2944 }
2945
2946 /// Enumerate the GC roots: first-class `Value` roots plus bare-object
2947 /// roots (open upvalues, which are not first-class Values). Shared by the
2948 /// full collector and the incremental-sweep driver so both snapshot the
2949 /// exact same live set.
2950 fn gc_roots(&self) -> (Vec<Value>, Vec<*mut GcHeader>) {
2951 let mut roots: Vec<Value> = Vec::with_capacity(self.stack.len() + 32);
2952 roots.push(Value::Table(self.globals));
2953 for mt in self.type_mt.into_iter().flatten() {
2954 roots.push(Value::Table(mt));
2955 }
2956 for &n in &self.mm_names {
2957 roots.push(Value::Str(n));
2958 }
2959 // Root the running thread's live registers (PUC marks [stack, top)).
2960 // `gc_top` is the instruction-level cursor of the last GC
2961 // safe-point: allocation safe-points set it via
2962 // `maybe_collect_garbage(live_top)`, and `begin_call` raises it
2963 // to the callee's argument top when entering a native — PUC's
2964 // `L->top = func + 1 + nargs` C-call discipline. Without that
2965 // raise, an explicit `collectgarbage()` collected with a STALE
2966 // cursor from some earlier (lower) safe-point and freed its own
2967 // caller's register-held strings
2968 // (STATUS_ACCESS_VIOLATION on Windows / ASAN heap-use-after-free
2969 // on Linux). Values stranded above the cursor stay
2970 // excluded so weak-table entries are not spuriously pinned
2971 // (gc.lua:544 suspended-coroutine collection).
2972 let live = (self.gc_top as usize).min(self.stack.len());
2973 roots.extend_from_slice(&self.stack[..live]);
2974 for cf in &self.frames {
2975 match cf {
2976 CallFrame::Lua(f) => roots.push(Value::Closure(f.closure)),
2977 CallFrame::Cont(NativeCont {
2978 kind: ContKind::Xpcall { handler },
2979 ..
2980 }) => roots.push(*handler),
2981 CallFrame::Cont(NativeCont {
2982 kind: ContKind::Close(cc),
2983 ..
2984 }) => {
2985 // Root the error threaded through this close chain so a
2986 // `collectgarbage()` inside a sibling `__close` handler
2987 // does not free it before the next handler is invoked
2988 // (PUC L->ci->u.l.errfunc / the closing_err shadow).
2989 if let Some(e) = cc.pending {
2990 roots.push(e);
2991 }
2992 if let AfterClose::ResumeUnwind { err, .. } = cc.after {
2993 roots.push(err);
2994 }
2995 }
2996 CallFrame::Cont(_) => {}
2997 }
2998 }
2999 if let Some(e) = self.closing_err {
3000 roots.push(e);
3001 }
3002 // Host roots — Lua-facade handles keep their referenced
3003 // values alive across calls/yields. Trace the whole vector;
3004 // unused slots (post-`unpin_all`) carry Value::Nil which the
3005 // GC ignores.
3006 for slot in &self.host_roots {
3007 // free-list slots carry Value::Nil (GC no-op)
3008 roots.push(slot.value);
3009 }
3010 // `table.sort` and similar builtins stash their working
3011 // `Vec<Value>` here so a `collectgarbage()` invoked inside the
3012 // comparator callback doesn't free strings/tables snapshotted
3013 // off the live table (sort.lua's `load(..)(); collectgarbage()`
3014 // compare regression).
3015 for buf in &self.sort_scratch {
3016 roots.extend_from_slice(buf);
3017 }
3018 // The running-natives chain holds Gc<NativeClosure>s
3019 // mid-execution. Without rooting them here, a `collectgarbage()`
3020 // invoked inside the running native (sort.lua's `load(..)();
3021 // collectgarbage()` compare callback regression) sweeps the
3022 // closure that's actively executing, leaving `nc.upvals`
3023 // dangling and the Rust local `nc` pointing at recycled memory
3024 // — the SIGSEGV pops on the very next field access or pop.
3025 for &nc in &self.running_natives {
3026 roots.push(Value::Native(nc));
3027 }
3028 // the running thread's debug hook (suspended threads root theirs via
3029 // Coro::trace / the main_ctx sweep below)
3030 if let Some(h) = self.hook.func {
3031 roots.push(h);
3032 }
3033 // the running coroutine (its saved-context fields live in the VM, but
3034 // the object itself + its resumer chain must stay reachable)
3035 if let Some(co) = self.current {
3036 roots.push(Value::Coro(co));
3037 }
3038 if let Some(mc) = self.main_coro {
3039 roots.push(Value::Coro(mc));
3040 }
3041 // debug.getregistry() and io library state
3042 if let Some(r) = self.registry {
3043 roots.push(Value::Table(r));
3044 }
3045 if let Some(mt) = self.file_mt {
3046 roots.push(Value::Table(mt));
3047 }
3048 if let Some(f) = self.io_input {
3049 roots.push(Value::Userdata(f));
3050 }
3051 if let Some(f) = self.io_output {
3052 roots.push(Value::Userdata(f));
3053 }
3054 if let Some(f) = self.io_stdin {
3055 roots.push(Value::Userdata(f));
3056 }
3057 // the main thread's saved context while a coroutine runs
3058 if let Some(m) = &self.main_ctx {
3059 roots.extend_from_slice(&m.stack);
3060 if let Some(h) = m.hook.func {
3061 roots.push(h);
3062 }
3063 for cf in &m.frames {
3064 match cf {
3065 CallFrame::Lua(f) => roots.push(Value::Closure(f.closure)),
3066 CallFrame::Cont(NativeCont {
3067 kind: ContKind::Xpcall { handler },
3068 ..
3069 }) => roots.push(*handler),
3070 CallFrame::Cont(_) => {}
3071 }
3072 }
3073 }
3074 let mut extra: Vec<*mut GcHeader> = self
3075 .open_upvals
3076 .iter()
3077 .map(|&(_, uv)| uv.as_ptr() as *mut GcHeader)
3078 .collect();
3079 if let Some(m) = &self.main_ctx {
3080 extra.extend(
3081 m.open_upvals
3082 .iter()
3083 .map(|&(_, uv)| uv.as_ptr() as *mut GcHeader),
3084 );
3085 }
3086 (roots, extra)
3087 }
3088
3089 /// Run a full collection with the VM's roots, then run any `__gc`
3090 /// finalizers the collection scheduled. A no-op (returns 0) when already
3091 /// inside a finalizer — the collector is not reentrant (PUC).
3092 pub fn collect_garbage(&mut self) -> usize {
3093 if self.gc_finalizing {
3094 return 0;
3095 }
3096 let (roots, extra) = self.gc_roots();
3097 let freed = self.heap.collect_ex(&roots, &extra);
3098 #[cfg(feature = "gc-verify")]
3099 self.verify_frame_regs_live("collect_garbage");
3100 self.run_finalizers();
3101 freed
3102 }
3103
3104 /// `gc-verify`: after a collect, every register slot the
3105 /// collector just rooted (`[0, max(gc_top, top))` — the same bound
3106 /// `gc_roots` uses) must hold a live value. A dead value inside the
3107 /// rooted range means the root snapshot and the sweep disagreed —
3108 /// a use-after-free waiting to happen. (Slots ABOVE the bound may hold
3109 /// stale dead values legitimately; the interpreter's contract is
3110 /// that it writes them before reading.)
3111 #[cfg(feature = "gc-verify")]
3112 pub(crate) fn verify_frame_regs_live(&self, ctx: &str) {
3113 let live = self.heap.debug_live_set();
3114 let header = |v: Value| -> Option<usize> {
3115 match v {
3116 Value::Str(s) => Some(s.as_ptr() as usize),
3117 Value::Table(t) => Some(t.as_ptr() as usize),
3118 Value::Closure(c) => Some(c.as_ptr() as usize),
3119 Value::Native(n) => Some(n.as_ptr() as usize),
3120 Value::Coro(c) => Some(c.as_ptr() as usize),
3121 Value::Userdata(u) => Some(u.as_ptr() as usize),
3122 _ => None,
3123 }
3124 };
3125 let bound = (self.gc_top as usize).min(self.stack.len());
3126 for i in 0..bound {
3127 if let Some(h) = header(self.stack[i])
3128 && !live.contains(&h)
3129 {
3130 panic!(
3131 "[gc-verify] {ctx}: rooted stack slot {i} (gc_top {}, top {}) \
3132 holds a dead value {h:#x} after collect",
3133 self.gc_top, self.top,
3134 );
3135 }
3136 }
3137 // Diagnostic tier: a dead value ABOVE the cursor is only a bug if
3138 // that register is a named local still in scope (the interpreter
3139 // WILL read it). Cross-check against the proto's LocVar table.
3140 for (fi, cf) in self.frames.iter().enumerate() {
3141 if let CallFrame::Lua(f) = cf {
3142 let base = f.base as usize;
3143 let maxs = f.closure.proto.max_stack as usize;
3144 let hi = (base + maxs).min(self.stack.len());
3145 let pc = f.pc;
3146 for i in bound.max(base)..hi {
3147 if let Some(h) = header(self.stack[i])
3148 && !live.contains(&h)
3149 {
3150 let reg = (i - base) as u32;
3151 if let Some(lv) = f
3152 .closure
3153 .proto
3154 .locvars
3155 .iter()
3156 .find(|lv| lv.reg == reg && lv.start_pc <= pc && pc < lv.end_pc)
3157 {
3158 panic!(
3159 "[gc-verify] {ctx}: frame {fi} IN-SCOPE LOCAL '{}' \
3160 (reg {reg}, abs {i}, pc {pc}, gc_top {}) holds a \
3161 dead value {h:#x} — live_top cursor excluded a \
3162 live named local",
3163 lv.name, self.gc_top,
3164 );
3165 }
3166 }
3167 }
3168 }
3169 }
3170 }
3171
3172 /// PUC 5.1 `collectgarbage` re-raised the first error a `__gc` finalizer
3173 /// threw; gc.lua's "errors during collection" probe relies on it. This
3174 /// variant runs the same cycle but propagates the captured finalizer
3175 /// error to the explicit caller.
3176 pub(crate) fn collect_garbage_propagating(&mut self) -> Result<usize, LuaError> {
3177 if self.gc_finalizing {
3178 return Ok(0);
3179 }
3180 let (roots, extra) = self.gc_roots();
3181 let freed = self.heap.collect_ex(&roots, &extra);
3182 #[cfg(feature = "gc-verify")]
3183 self.verify_frame_regs_live("collect_garbage_propagating");
3184 self.run_finalizers_or_err()?;
3185 Ok(freed)
3186 }
3187
3188 /// Whether a `__gc` finalizer is currently running (so `collectgarbage`
3189 /// should report fail rather than collect).
3190 pub(crate) fn gc_is_finalizing(&self) -> bool {
3191 self.gc_finalizing
3192 }
3193
3194 /// PUC 5.4+ default warnf: emit one piece of a warning message. `to_cont`
3195 /// = true indicates more pieces follow (concatenated until the first
3196 /// `to_cont = false` call flushes the whole line). Mirrors
3197 /// `lauxlib.c::warnfon` + `warnfcont` + `checkcontrol`:
3198 /// * If the buffer is fresh, `to_cont` is false, and the message is
3199 /// `@<word>`, treat as a control message — only `@on` / `@off` are
3200 /// recognised; any other `@…` is silently ignored.
3201 /// * Otherwise, while the state is `Off`, drop the piece; while `On`,
3202 /// accumulate, and flush to stderr + `warn_log` on the
3203 /// non-continuation call.
3204 pub(crate) fn emit_warn(&mut self, msg: &[u8], to_cont: bool) {
3205 if self.warn_buf.is_empty()
3206 && !to_cont
3207 && let Some(b'@') = msg.first().copied()
3208 {
3209 match &msg[1..] {
3210 b"on" => self.warn_state = WarnState::On,
3211 b"off" => self.warn_state = WarnState::Off,
3212 _ => {} // unknown control — silently ignored (PUC checkcontrol)
3213 }
3214 return;
3215 }
3216 if self.warn_state == WarnState::Off {
3217 // drop continuation pieces too — PUC `warnfoff` is the trampoline
3218 return;
3219 }
3220 self.warn_buf.extend_from_slice(msg);
3221 if !to_cont {
3222 let line = std::mem::take(&mut self.warn_buf);
3223 eprintln!("Lua warning: {}", String::from_utf8_lossy(&line));
3224 self.warn_log.push(line);
3225 }
3226 }
3227
3228 /// Drain the in-process warning log (one entry per emitted message, sans
3229 /// `"Lua warning: "` prefix and newline). For test harnesses that want to
3230 /// assert on warn output without scraping stderr.
3231 pub fn warn_log_take(&mut self) -> Vec<Vec<u8>> {
3232 std::mem::take(&mut self.warn_log)
3233 }
3234
3235 /// Arm the cooperative instruction budget. The run loop
3236 /// decrements this once per dispatch turn; on zero it raises a catchable
3237 /// `"instruction budget exceeded"` error and disarms itself so the host
3238 /// can resume with a fresh budget on the next call. `None` removes the
3239 /// cap. Pass `Some(n)` before `eval`/`call_value` for the embedder's
3240 /// short-script semantics.
3241 pub fn set_instr_budget(&mut self, budget: Option<i64>) {
3242 self.instr_budget = budget;
3243 }
3244
3245 /// Remaining instruction budget (None when unbounded).
3246 pub fn instr_budget_remaining(&self) -> Option<i64> {
3247 self.instr_budget
3248 }
3249
3250 /// Toggle the cranelift JIT. Default `true`. Sandbox embedders
3251 /// **must** disable JIT when relying on `instr_budget` — see the
3252 /// `jit_enabled` field doc for the rationale.
3253 pub fn set_jit_enabled(&mut self, enabled: bool) {
3254 self.jit.enabled = enabled;
3255 }
3256
3257 /// Current JIT enable state.
3258 pub fn jit_enabled(&self) -> bool {
3259 self.jit.enabled
3260 }
3261
3262 /// Toggle the trace JIT. Off by default. When enabled, hot
3263 /// back-edges are counted on `Proto.trace_hot_count`; once the
3264 /// counter passes `TRACE_HOT_THRESHOLD`, the dispatch loop enters
3265 /// recording mode at the back-edge target.
3266 pub fn set_trace_jit_enabled(&mut self, enabled: bool) {
3267 self.jit.trace_enabled = enabled;
3268 }
3269
3270 /// Opt-in flag for the self-link cycle catch. See field
3271 /// docs for the correctness blocker. Default `false`.
3272 pub fn set_self_link_enabled(&mut self, enabled: bool) {
3273 self.jit.self_link_enabled = enabled;
3274 }
3275
3276 /// Current state of the self-link cycle catch.
3277 pub fn self_link_enabled(&self) -> bool {
3278 self.jit.self_link_enabled
3279 }
3280
3281 #[doc(hidden)]
3282 #[deprecated(since = "3.2.0", note = "renamed to `set_self_link_enabled`")]
3283 pub fn set_p16_self_link_enabled(&mut self, enabled: bool) {
3284 self.set_self_link_enabled(enabled);
3285 }
3286
3287 #[doc(hidden)]
3288 #[deprecated(since = "3.2.0", note = "renamed to `self_link_enabled`")]
3289 pub fn p16_self_link_enabled(&self) -> bool {
3290 self.self_link_enabled()
3291 }
3292
3293 /// Current trace-JIT enable state.
3294 pub fn trace_jit_enabled(&self) -> bool {
3295 self.jit.trace_enabled
3296 }
3297
3298 /// Number of traces that have closed cleanly (looped back to the
3299 /// head PC) since this Vm was constructed. Cumulative; used by
3300 /// tests + tuning.
3301 pub fn trace_closed_count(&self) -> u64 {
3302 self.jit.counters.closed
3303 }
3304
3305 /// Number of traces that have aborted (exceeded MAX_TRACE_LEN or
3306 /// hit an un-recordable op).
3307 pub fn trace_aborted_count(&self) -> u64 {
3308 self.jit.counters.aborted
3309 }
3310
3311 /// Number of compiled traces whose close shape
3312 /// is `TraceEnd::InlineAbort` (depth>0 boundary). Such traces
3313 /// pin `dispatchable=false` because the dispatcher can't
3314 /// resume at a depth>0 PC without the matching CallFrames.
3315 /// The frame-materialisation helper could synthesise those, but
3316 /// the InlineAbort emit path isn't wired up to it.
3317 pub fn trace_inline_abort_count(&self) -> u64 {
3318 self.jit.counters.inline_abort
3319 }
3320
3321 /// See `JitCounters::dispatch_off_reasons`.
3322 pub fn trace_dispatch_off_reasons(&self) -> &[&'static str] {
3323 &self.jit.counters.dispatch_off_reasons
3324 }
3325
3326 /// See `JitCounters::compile_failed_reasons`.
3327 pub fn trace_compile_failed_reasons(&self) -> &[&'static str] {
3328 &self.jit.counters.compile_failed_reasons
3329 }
3330
3331 /// See `JitCounters::closed_lens`. Returns
3332 /// `(is_call_triggered, ops_len)` for every trace that closed.
3333 pub fn trace_closed_lens(&self) -> &[(bool, usize)] {
3334 &self.jit.counters.closed_lens
3335 }
3336
3337 /// See [`crate::vm::jit_state::JitCounters::close_cause_counts`].
3338 /// Per-reason close-cause counts (recorder-side abort/discard +
3339 /// lowerer-side dispatch_off labels) keyed by `&'static str`.
3340 pub fn trace_close_cause_counts(&self) -> &std::collections::HashMap<&'static str, u64> {
3341 &self.jit.counters.close_cause_counts
3342 }
3343
3344 /// Number of compiled traces whose
3345 /// `CompiledTrace.downrec_link` is `Some(_)` (lowerer's
3346 /// `downrec_idx_opt` arm emitted the stitch sentinel + caller-pc
3347 /// guard scaffold).
3348 pub fn trace_downrec_link_compiled_count(&self) -> u64 {
3349 self.jit.counters.downrec_link_compiled
3350 }
3351
3352 /// See
3353 /// [`crate::vm::jit_state::JitCounters::downrec_dispatched`]. Number
3354 /// of times the dispatcher's `is_downrec_sentinel` arm fired and
3355 /// classified the return as a caller-pc-guard HIT.
3356 pub fn trace_downrec_dispatched_count(&self) -> u64 {
3357 self.jit.counters.downrec_dispatched
3358 }
3359
3360 /// See
3361 /// [`crate::vm::jit_state::JitCounters::downrec_deopt`]. Number of
3362 /// times the dispatcher entered a `downrec_link`-bearing trace and
3363 /// the trace returned via the lowerer's deopt block (caller-pc
3364 /// guard MISS), or the dispatcher itself force-deopted via the
3365 /// stitch-cycle checkpoint.
3366 pub fn trace_downrec_deopt_count(&self) -> u64 {
3367 self.jit.counters.downrec_deopt
3368 }
3369
3370 /// See
3371 /// [`crate::vm::jit_state::JitCounters::multi_way_guard_emitted`].
3372 /// Number of compiled traces whose lowerer emitted a multi-way
3373 /// caller-pc guard chain (>= 2 distinct `caller_pc` candidates)
3374 /// at the `TraceEnd::DownRec` close + lifted `dispatchable = true`.
3375 pub fn trace_multi_way_guard_emitted_count(&self) -> u64 {
3376 self.jit.counters.multi_way_guard_emitted
3377 }
3378
3379 /// Number of closed traces the lowerer compiled and
3380 /// parked on `Proto.traces`. Re-records of the same head_pc are
3381 /// deduped (the second close finds the head_pc already cached
3382 /// and skips compile), so this never exceeds `trace_closed_count`.
3383 pub fn trace_compiled_count(&self) -> u64 {
3384 self.jit.counters.compiled
3385 }
3386
3387 /// Number of times the recorder captured a
3388 /// [`crate::jit::trace_types::FieldIcSnapshot`] under
3389 /// `LUNA_JIT_FIELD_IC=1`. Stays 0 on the env-default path. Used
3390 /// by the opt-in fire test to verify the env gate
3391 /// wiring round-trips end-to-end (env -> recorder -> snapshot
3392 /// -> counter -> getter -> assertion).
3393 pub fn trace_field_ic_snapshot_count(&self) -> u64 {
3394 self.jit.counters.field_ic_snapshot_captured
3395 }
3396
3397 /// Number of closed traces the lowerer rejected
3398 /// (any of the bail conditions in
3399 /// `crate::jit::trace::try_compile_trace`).
3400 pub fn trace_compile_failed_count(&self) -> u64 {
3401 self.jit.counters.compile_failed
3402 }
3403
3404 /// Number of times the dispatcher jumped into a
3405 /// compiled trace. Bumps on every entry; `trace_deopt_count`
3406 /// counts the subset where the trace returned with a parked
3407 /// `jit_pending_err`.
3408 pub fn trace_dispatched_count(&self) -> u64 {
3409 self.jit.counters.dispatched
3410 }
3411
3412 /// Number of trace entries that came back with
3413 /// `jit_pending_err` set (typically a metatable shadowed an
3414 /// index inside a helper, forcing the dispatcher to fall back
3415 /// to the interpreter without committing the trace's result).
3416 pub fn trace_deopt_count(&self) -> u64 {
3417 self.jit.counters.deopt
3418 }
3419
3420 /// Number of times the dispatcher started a side
3421 /// trace recording (an `exit_hit_counts` slot crossed
3422 /// [`crate::jit::trace::HOTEXIT_THRESHOLD`] while `active_trace`
3423 /// was None and trace JIT was enabled). Each unit is exactly one
3424 /// `start_side_trace` call; the actual compile success counts
3425 /// under [`Self::trace_compiled_count`] like any other trace.
3426 /// Probe use: distinguishes the "side-trace pipeline fired"
3427 /// signal from the "primary back-edge / call-trigger fired"
3428 /// signal without reading per-counter histograms.
3429 pub fn trace_side_trace_started_count(&self) -> u64 {
3430 self.jit.counters.side_trace_started
3431 }
3432
3433 /// Number of side-trace recordings that closed,
3434 /// compiled successfully, AND patched their parent's
3435 /// `exit_side_trace_ptrs[exit_idx]`.
3436 pub fn trace_side_trace_compiled_count(&self) -> u64 {
3437 self.jit.counters.side_trace_compiled
3438 }
3439
3440 /// Number of side traces that compiled
3441 /// successfully but were SHEDDED by the close-handler shape-
3442 /// match gate (`exit_tags_match_entry_tags`). High ratios
3443 /// vs. `trace_side_trace_compiled_count` indicate the
3444 /// architecture is shedding lots of would-be side traces;
3445 /// useful as a tuning probe for future relaxation of the
3446 /// gate or for child-IR re-specialisation against parent's
3447 /// exit shape.
3448 pub fn trace_side_trace_shape_mismatch_count(&self) -> u64 {
3449 self.jit.counters.side_trace_shape_mismatch
3450 }
3451
3452 /// Sum of NewTable sites the pre-emit escape sweep
3453 /// classified as `crate::jit::trace::EscapeState::Sinkable`
3454 /// across every successfully compiled trace on this Vm. The
3455 /// count is post-demotion: sites pre-emit drops back to Escaped
3456 /// for not meeting the sunk-emit criteria are NOT counted.
3457 /// `trace_sunk_alloc_count` matches one-for-one today (every
3458 /// surviving Sinkable site goes through sunk emit).
3459 pub fn trace_sinkable_seen_count(&self) -> u64 {
3460 self.jit.counters.sinkable_seen
3461 }
3462
3463 /// See `JitCounters::accum_bufferable_seen`.
3464 pub fn trace_accum_bufferable_seen_count(&self) -> u64 {
3465 self.jit.counters.accum_bufferable_seen
3466 }
3467
3468 /// Total dispatch hits across all known traces,
3469 /// broken into hot-exit telemetry (max single-exit count,
3470 /// total dispatches, exit count). Used by probes to identify
3471 /// hot side-exits as side-trace candidates.
3472 ///
3473 /// Walks `cl.proto` AND all nested protos in `cl.proto.protos`
3474 /// recursively, so inner functions' traces are reported.
3475 pub fn trace_exit_hit_summary(
3476 &self,
3477 cl: crate::runtime::heap::Gc<crate::runtime::function::LuaClosure>,
3478 ) -> Vec<(u32, Vec<u32>)> {
3479 fn walk(
3480 proto: crate::runtime::heap::Gc<crate::runtime::function::Proto>,
3481 out: &mut Vec<(u32, Vec<u32>)>,
3482 ) {
3483 for ct in proto.traces.borrow().iter() {
3484 let counts: Vec<u32> = ct.exit_hit_counts.iter().map(|c| c.get()).collect();
3485 out.push((ct.head_pc, counts));
3486 }
3487 for inner in proto.protos.iter() {
3488 walk(*inner, out);
3489 }
3490 }
3491 let mut out: Vec<(u32, Vec<u32>)> = Vec::new();
3492 walk(cl.proto, &mut out);
3493 out
3494 }
3495
3496 /// Surface every side-exit slot whose hit count is
3497 /// `>= HOTEXIT_THRESHOLD` across every trace reachable from
3498 /// `cl.proto` (recursively walking `proto.protos`). Returned
3499 /// entries are side-trace candidates: each carries the parent
3500 /// trace's `(head_proto, head_pc)`, the exit's index in the
3501 /// parent's `exit_hit_counts`, and the side trace's natural
3502 /// entry shape (`cont_pc` + `exit_tags`).
3503 ///
3504 /// Layout of `exit_hit_counts` (mirrored by the iter):
3505 /// - `[0..per_exit_inline.len())` → `InlineSideExit` (cont_pc +
3506 /// window-sized exit_tags).
3507 /// - `[per_exit_inline.len()..inline.len() + per_exit_tags.len())`
3508 /// → `per_exit_tags[i]` (per-cont_pc caller-window tags).
3509 /// - Last slot → global clean-tail (cont_pc = `head_pc`,
3510 /// exit_tags = `ct.exit_tags`).
3511 pub fn hot_exit_iter(
3512 &self,
3513 cl: crate::runtime::heap::Gc<crate::runtime::function::LuaClosure>,
3514 ) -> Vec<crate::jit::trace::HotExitInfo> {
3515 use crate::jit::trace::{HOTEXIT_THRESHOLD, HotExitInfo};
3516 fn walk(
3517 proto: crate::runtime::heap::Gc<crate::runtime::function::Proto>,
3518 out: &mut Vec<HotExitInfo>,
3519 ) {
3520 for ct in proto.traces.borrow().iter() {
3521 let inline_n = ct.per_exit_inline.len();
3522 let tags_n = ct.per_exit_tags.len();
3523 debug_assert_eq!(
3524 ct.exit_hit_counts.len(),
3525 inline_n + tags_n + 1,
3526 "exit_hit_counts layout invariant violated"
3527 );
3528 for (idx, cell) in ct.exit_hit_counts.iter().enumerate() {
3529 let hits = cell.get();
3530 if hits < HOTEXIT_THRESHOLD {
3531 continue;
3532 }
3533 let (cont_pc, exit_tags) = if idx < inline_n {
3534 let ent = &ct.per_exit_inline[idx];
3535 (ent.cont_pc, ent.exit_tags.clone())
3536 } else if idx < inline_n + tags_n {
3537 let (pc, tags) = &ct.per_exit_tags[idx - inline_n];
3538 (*pc, tags.clone())
3539 } else {
3540 (ct.head_pc, ct.exit_tags.clone())
3541 };
3542 out.push(HotExitInfo {
3543 head_proto: proto,
3544 head_pc: ct.head_pc,
3545 exit_idx: idx,
3546 hits,
3547 cont_pc,
3548 exit_tags,
3549 });
3550 }
3551 }
3552 for inner in proto.protos.iter() {
3553 walk(*inner, out);
3554 }
3555 }
3556 let mut out: Vec<HotExitInfo> = Vec::new();
3557 walk(cl.proto, &mut out);
3558 out
3559 }
3560
3561 /// Sum of NewTable sites that actually took the
3562 /// sunk-emit path across every successfully compiled trace on
3563 /// this Vm. Each counted site skips its heap `Gc<Table>`
3564 /// allocation per dispatch; the array part lives as Cranelift
3565 /// `Variable`s for the duration of the trace.
3566 pub fn trace_sunk_alloc_count(&self) -> u64 {
3567 self.jit.counters.sunk_alloc
3568 }
3569
3570 /// Sum of materialise-helper emit sites across every
3571 /// successfully compiled trace on this Vm. Each unit is a
3572 /// (site × cmp side-exit) pair whose IR reconstructs a heap
3573 /// `Gc<Table>` from the virt slots on deopt.
3574 pub fn trace_materialize_emit_count(&self) -> u64 {
3575 self.jit.counters.materialize_emit
3576 }
3577
3578 /// Diagnostic: total `Op::Closure` ops the trace JIT
3579 /// lowered to the `luna_jit_op_closure` helper. Each emitted op
3580 /// replaces a `Heap::new_closure_inline` call on the dispatch
3581 /// path; the count is static (one per matching op per compiled
3582 /// trace), summed at compile success.
3583 pub fn trace_closure_emit_count(&self) -> u64 {
3584 self.jit.counters.closure_emit
3585 }
3586
3587 /// See
3588 /// [`crate::vm::jit_state::JitCounters::per_exit_inline_compiled`].
3589 /// Number of compiled traces whose `per_exit_inline.len() > 0`
3590 /// (depth>0 inlined cmp side-exits emitted).
3591 pub fn trace_per_exit_inline_compiled_count(&self) -> u64 {
3592 self.jit.counters.per_exit_inline_compiled
3593 }
3594
3595 /// See
3596 /// [`crate::vm::jit_state::JitCounters::per_exit_inline_dispatchable`].
3597 /// Number of compiled traces with `per_exit_inline.len() > 0` AND
3598 /// `dispatchable == true` — i.e. the count of compiled traces
3599 /// that would actually exercise the AOT chain-reloc +
3600 /// deploy-resolver path.
3601 pub fn trace_per_exit_inline_dispatchable_count(&self) -> u64 {
3602 self.jit.counters.per_exit_inline_dispatchable
3603 }
3604
3605 /// Diagnostic: max `inline_depth` ever seen on any
3606 /// `RecordedOp` pushed by the recorder. Tells tests + tuning
3607 /// whether a self-recursive function actually walked the depth
3608 /// tracker past 0. Saturates at `MAX_INLINE_DEPTH`. Persists
3609 /// across traces and Vm activations; reset only on `Vm::new`.
3610 pub fn trace_max_depth_seen(&self) -> u8 {
3611 self.jit.max_depth_seen
3612 }
3613
3614 /// Last live Lua frame (the trace head's frame at
3615 /// dispatch time). The frame-materialization helper reads `.base`
3616 /// to compute offsets for each inlined frame's window.
3617 #[doc(hidden)]
3618 pub fn jit_last_lua_frame(&self) -> Option<Frame> {
3619 match self.frames.last() {
3620 Some(CallFrame::Lua(f)) => Some(*f),
3621 _ => None,
3622 }
3623 }
3624
3625 /// Read-only borrow of the current call
3626 /// stack, for the [`crate::vm::inspect`] pure-read accessors used
3627 /// by `luna-tools` (`luna-profile`'s sampler walks this from
3628 /// inside a `Count` hook). Sibling-module scope: not part of the
3629 /// public embedder surface, but `inspect::frames_for_profile` is.
3630 #[doc(hidden)]
3631 pub(super) fn inspect_frames(&self) -> &[CallFrame] {
3632 &self.frames
3633 }
3634
3635 /// Ensure the value stack covers indices
3636 /// `[0..need)`. Extends with Nil if shorter. Called by the
3637 /// frame-materialization helper before pushing an inlined frame
3638 /// whose register window may exceed the current stack length.
3639 #[doc(hidden)]
3640 pub fn jit_ensure_stack(&mut self, need: usize) {
3641 if self.stack.len() < need {
3642 self.stack.resize(need, Value::Nil);
3643 }
3644 }
3645
3646 /// Trace JIT path for `Op::Close A`. Predicts whether
3647 /// `__close` handlers would run (any active tbc slot ≥ from
3648 /// holding a non-nil/false Value); if so, returns 1 without doing
3649 /// anything and the trace side-exits at the op, so the interpreter
3650 /// runs the handlers. Otherwise performs the safe part of close —
3651 /// `close_from(from)` to close open upvals + drop any drained tbc
3652 /// entries ≥ from — and returns 0.
3653 ///
3654 /// Returns are i64-shaped so the cranelift import sig stays
3655 /// trivial (i64 → i64 mapping).
3656 #[doc(hidden)]
3657 pub fn jit_op_close(&mut self, start_offset: u32) -> i64 {
3658 let Some(f) = self.jit_last_lua_frame() else {
3659 return 1;
3660 };
3661 let from = f.base + start_offset;
3662 let has_handler = self.tbc.iter().any(|&s| {
3663 s >= from && {
3664 let v = self.stack[s as usize];
3665 !matches!(v, Value::Nil | Value::Bool(false))
3666 }
3667 });
3668 if has_handler {
3669 self.jit.counters.deopt += 1;
3670 return 1;
3671 }
3672 self.close_from(from);
3673 // Drain any tbc entries ≥ from (they're nil/false stubs the
3674 // interpreter's drive_close would have skipped silently).
3675 while let Some(&s) = self.tbc.last() {
3676 if s < from {
3677 break;
3678 }
3679 self.tbc.pop();
3680 }
3681 0
3682 }
3683
3684 /// Spill the trace's current value for a register to
3685 /// the underlying `vm.stack[base + slot_offset]`. Required before
3686 /// an `Op::Closure` whose inner proto has an `in_stack: true`
3687 /// upval at `slot_offset` — the helper's `find_or_create_upval`
3688 /// captures a live pointer to `vm.stack[base + slot_offset]`,
3689 /// which must hold the right value at call time (trace IR's
3690 /// Variable hasn't yet been written back).
3691 ///
3692 /// Parameters arrive as i64 from the IR: `slot_offset` is the
3693 /// caller-frame register index (`u32` in practice, depth=0
3694 /// only — depth>0 Closure is not supported); `tag` is the
3695 /// `crate::runtime::value::raw` byte for the slot's RegKind;
3696 /// `raw_bits` is the trace Variable's `use_var` payload
3697 /// (i64-shaped — Float is its bit-pattern, Table/Closure is the
3698 /// raw `Gc::as_ptr` cast).
3699 #[doc(hidden)]
3700 pub fn jit_spill_stack(&mut self, slot_offset: u32, tag: u8, raw_bits: u64) {
3701 let Some(f) = self.jit_last_lua_frame() else {
3702 self.jit.pending_err =
3703 Some(self.rt_err("JIT spill: no Lua frame on jit_last_lua_frame()"));
3704 return;
3705 };
3706 let idx = (f.base as usize) + (slot_offset as usize);
3707 if self.stack.len() <= idx {
3708 self.stack.resize(idx + 1, Value::Nil);
3709 }
3710 // SAFETY: caller (trace JIT IR emit) provides matching
3711 // `(tag, raw_bits)` — same shape produced by Value::unpack.
3712 let v = unsafe {
3713 crate::runtime::Value::pack(tag, crate::runtime::value::RawVal { zero: raw_bits })
3714 };
3715 self.stack[idx] = v;
3716 }
3717
3718 /// Refresh only the raw payload of
3719 /// `vm.stack[base + slot_offset]`, preserving its existing
3720 /// `Value` tag. The caller (trace JIT Op::Concat body emit)
3721 /// uses this when the slot's `RegKind` is `Unset` (no compile-
3722 /// time tag info; commonly `Str` slots which the trace doesn't
3723 /// model). The interp's previous execution of the same op
3724 /// already populated the slot with the right tag — the trace
3725 /// only needs to swap in its current raw value.
3726 #[doc(hidden)]
3727 pub fn jit_stack_update_raw(&mut self, slot_offset: u32, raw_bits: u64) {
3728 let Some(f) = self.jit_last_lua_frame() else {
3729 return;
3730 };
3731 let idx = (f.base as usize) + (slot_offset as usize);
3732 if idx >= self.stack.len() {
3733 return;
3734 }
3735 let (tag, _) = self.stack[idx].unpack();
3736 // 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).
3737 self.stack[idx] = unsafe {
3738 crate::runtime::Value::pack(tag, crate::runtime::value::RawVal { zero: raw_bits })
3739 };
3740 }
3741
3742 /// Trace JIT path for `Op::Concat A B`.
3743 ///
3744 /// Mirrors the interp arm (this file ~L5112): `self.top =
3745 /// base + a + n; concat_run(base + a)`. Result lands at
3746 /// `vm.stack[base + a]`. Returns `0` on success, `-1` when the
3747 /// interpreter must do it (any error from `concat_run` OR
3748 /// detection that the metamethod path was taken — `concat_run`
3749 /// returns `Ok(())` after `begin_meta_call` which has pushed a Lua
3750 /// frame the trace can't safely continue past); the trace then
3751 /// side-exits at the op and the interpreter redoes it, raising the
3752 /// error or calling `__concat` itself.
3753 ///
3754 /// The frame-push detection uses `pre/post frames.len()` and
3755 /// unwinds any pushed frames first, so the exit sees a clean stack.
3756 #[doc(hidden)]
3757 pub fn jit_op_concat(&mut self, slot_offset: u32, n: i32) -> i64 {
3758 let Some(f) = self.jit_last_lua_frame() else {
3759 return -1;
3760 };
3761 let abs_a = f.base + slot_offset;
3762 self.top = abs_a + n as u32;
3763 let pre_frames = self.frames.len();
3764 let result = self.concat_run(abs_a);
3765 let post_frames = self.frames.len();
3766 // Frame-push = metamethod path taken (begin_meta_call pushed
3767 // a Lua frame). The trace can't continue past it; unwind +
3768 // deopt so interp redoes Op::Concat in the slow path.
3769 while self.frames.len() > pre_frames {
3770 frames_pop_sync(&mut self.frames, &mut self.frames_top);
3771 }
3772 if result.is_err() || post_frames > pre_frames {
3773 self.jit.counters.deopt += 1;
3774 return -1;
3775 }
3776 0
3777 }
3778
3779 /// Pop a reusable `Vec<u8>` from the JIT
3780 /// accumulator buffer pool, returning a raw pointer. The trace
3781 /// fn's IR holds this pointer in a stack slot through the loop
3782 /// and calls `jit_str_buf_extend` per iter. If the pool is
3783 /// empty, allocate fresh.
3784 ///
3785 /// Safety: the returned pointer is valid until
3786 /// `jit_str_buf_release` is called or the Vm is dropped. The
3787 /// caller MUST not retain it across `enter_jit` boundaries.
3788 #[doc(hidden)]
3789 pub fn jit_str_buf_acquire(&mut self) -> *mut Vec<u8> {
3790 let buf = self.jit.str_buf_pool.pop().unwrap_or_default();
3791 // Move into a Box so the pointer is stable until release.
3792 Box::into_raw(Box::new(buf))
3793 }
3794
3795 /// Return a previously-acquired buffer to the
3796 /// pool, dropping any excess past `jit_str_buf_pool_cap`. The
3797 /// buffer is `clear`ed (capacity retained) so the next acquire
3798 /// gets a ready-to-extend Vec.
3799 ///
3800 /// Safety: `buf` must have been returned by a prior
3801 /// `jit_str_buf_acquire` on the same Vm.
3802 #[doc(hidden)]
3803 #[allow(clippy::not_unsafe_ptr_arg_deref)] // JIT helper: `buf` round-trips through `Box::into_raw`; SAFETY documented below.
3804 pub fn jit_str_buf_release(&mut self, buf: *mut Vec<u8>) {
3805 if buf.is_null() {
3806 return;
3807 }
3808 // 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.
3809 let mut owned = unsafe { Box::from_raw(buf) };
3810 owned.clear();
3811 if self.jit.str_buf_pool.len() < self.jit.str_buf_pool_cap {
3812 self.jit.str_buf_pool.push(*owned);
3813 }
3814 // Else: drop the buffer.
3815 }
3816
3817 /// Append a LuaStr's bytes to the accumulator
3818 /// buffer. The trace IR computes the `str_ptr` (= raw bits of
3819 /// the piece slot) and passes it through; we treat it as a
3820 /// `*mut LuaStr` and append its bytes.
3821 ///
3822 /// Returns 0 on success, -1 if the piece isn't a Str (would
3823 /// trip __concat metamethod path → deopt to interp).
3824 ///
3825 /// Safety: `buf` from prior `acquire`; `str_ptr` from the
3826 /// trace's piece slot raw bits.
3827 #[doc(hidden)]
3828 #[allow(clippy::not_unsafe_ptr_arg_deref)] // JIT helper: `buf` from prior `acquire`; `str_ptr` from trace piece slot; SAFETY documented below.
3829 pub fn jit_str_buf_extend(&mut self, buf: *mut Vec<u8>, str_ptr: i64) -> i64 {
3830 if buf.is_null() || str_ptr == 0 {
3831 return -1;
3832 }
3833 // 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).
3834 let buf = unsafe { &mut *buf };
3835 let lua_str_ptr = str_ptr as *const crate::runtime::string::LuaStr;
3836 // 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).
3837 let bytes = unsafe { crate::runtime::string::bytes_of(lua_str_ptr) };
3838 buf.extend_from_slice(bytes);
3839 0
3840 }
3841
3842 /// Drain the accumulator buffer into a fresh
3843 /// `LuaStr` via `heap.intern`, returning the raw ptr bits for
3844 /// the trace to write into the accumulator slot.
3845 ///
3846 /// Returns the LuaStr ptr as i64 on success, 0 on overflow
3847 /// (the hard cap; the trace deopts).
3848 ///
3849 /// Safety: `buf` from prior `acquire`. The buffer is left
3850 /// CLEAR (drained) ready for `release`.
3851 #[doc(hidden)]
3852 #[allow(clippy::not_unsafe_ptr_arg_deref)] // JIT helper: `buf` from prior `acquire`; SAFETY documented below.
3853 pub fn jit_str_buf_intern(&mut self, buf: *mut Vec<u8>) -> i64 {
3854 if buf.is_null() {
3855 return 0;
3856 }
3857 // 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).
3858 let buf = unsafe { &mut *buf };
3859 let bytes = std::mem::take(buf);
3860 // hard cap at 256KB
3861 if bytes.len() > 256 * 1024 {
3862 return 0;
3863 }
3864 let gc = self.heap.intern(&bytes);
3865 gc.as_ptr() as i64
3866 }
3867
3868 /// Trace JIT helper for `Op::TForCall A 0 C`.
3869 ///
3870 /// Base path: copy R[A..=A+2] → R[A+4..=A+6] + `begin_call`.
3871 /// ipairs `inext` fast path at the top — skip begin_call
3872 /// when R[A]=Native(ipairs_iter), R[A+1]=Table no-mt,
3873 /// R[A+2]=Int.
3874 /// Batched out-ptr writeback — fill ctrl/key/val raws into
3875 /// caller-provided buffers + return R[A+4]'s tag byte. Lets
3876 /// emit skip 3 separate `luna_jit_stack_load` calls and 1
3877 /// `luna_jit_stack_tag` call by reading the buffer via
3878 /// cranelift `stack_load` IR instead. Returns -1 on deopt,
3879 /// else R[A+4]'s tag byte | R[A+5]'s tag byte << 8 (the value's
3880 /// tag only when `nvars >= 2`, 0 otherwise).
3881 #[doc(hidden)]
3882 #[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.
3883 pub fn jit_op_tforcall(
3884 &mut self,
3885 slot_offset: u32,
3886 nvars: i32,
3887 ctrl_out: *mut i64,
3888 key_out: *mut i64,
3889 val_out: *mut i64,
3890 ) -> i64 {
3891 let Some(f) = self.jit_last_lua_frame() else {
3892 return -1;
3893 };
3894 let abs = f.base + slot_offset;
3895 let need = (abs + 7) as usize;
3896 if self.stack.len() < need {
3897 self.stack.resize(need, Value::Nil);
3898 }
3899 // ipairs fast path
3900 let took_fast_path = if let Value::Native(n) = self.stack[abs as usize]
3901 && std::ptr::fn_addr_eq(
3902 n.f,
3903 crate::vm::builtins::ipairs_iter as crate::runtime::value::NativeFn,
3904 )
3905 && let Value::Table(t) = self.stack[(abs + 1) as usize]
3906 && t.metatable().is_none()
3907 && let Value::Int(i) = self.stack[(abs + 2) as usize]
3908 {
3909 let next_i = i.wrapping_add(1);
3910 let v = t.get_int(next_i);
3911 if v.is_nil() {
3912 self.stack[(abs + 4) as usize] = Value::Nil;
3913 } else {
3914 self.stack[(abs + 4) as usize] = Value::Int(next_i);
3915 if (nvars as usize) >= 2 {
3916 self.stack[(abs + 5) as usize] = v;
3917 }
3918 for j in 2..nvars as usize {
3919 let slot = abs + 4 + j as u32;
3920 if (slot as usize) < self.stack.len() {
3921 self.stack[slot as usize] = Value::Nil;
3922 }
3923 }
3924 }
3925 true
3926 } else {
3927 false
3928 };
3929 if !took_fast_path {
3930 // slow path: copy R[A..=A+2] → R[A+4..=A+6], then
3931 // route through begin_call. Lua-closure iters would push
3932 // a Lua frame mid-trace → deopt.
3933 self.stack[(abs + 4) as usize] = self.stack[abs as usize];
3934 self.stack[(abs + 5) as usize] = self.stack[(abs + 1) as usize];
3935 self.stack[(abs + 6) as usize] = self.stack[(abs + 2) as usize];
3936 // the interpreter raises the call's error itself; and a native
3937 // that `begin_call` hands to the interpreter loop (pcall, xpcall,
3938 // pairs, an async native) pushes frames or parks a future
3939 // instead of returning its results here
3940 let runs_to_completion = match self.stack[abs as usize] {
3941 Value::Native(nc) => {
3942 use crate::runtime::value::NativeFn;
3943 !nc.is_async
3944 && ![
3945 nat_pcall as NativeFn,
3946 nat_xpcall as NativeFn,
3947 nat_host_xpcall as NativeFn,
3948 nat_pairs as NativeFn,
3949 ]
3950 .iter()
3951 .any(|&g| std::ptr::fn_addr_eq(nc.f, g))
3952 }
3953 _ => false,
3954 };
3955 if !runs_to_completion || self.begin_call(abs + 4, Some(2), nvars, false).is_err() {
3956 self.jit.counters.deopt += 1;
3957 return -1;
3958 }
3959 }
3960 // Batched writeback — fill the caller's buffers with the
3961 // raw bits of R[A+2] / R[A+4] / R[A+5] so the trace IR can
3962 // reload via cranelift `stack_load` instead of separate
3963 // `luna_jit_stack_load` helper calls.
3964 // 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).
3965 let ctrl_raw = unsafe { self.stack[(abs + 2) as usize].unpack().1.zero };
3966 let (key_tag, key_rv) = self.stack[(abs + 4) as usize].unpack();
3967 // 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).
3968 let key_raw = unsafe { key_rv.zero };
3969 let (val_tag, val_raw) = if (nvars as usize) >= 2 {
3970 let (tag, rv) = self.stack[(abs + 5) as usize].unpack();
3971 // 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).
3972 (tag, unsafe { rv.zero })
3973 } else {
3974 (0, 0u64)
3975 };
3976 // 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).
3977 unsafe {
3978 ctrl_out.write(ctrl_raw as i64);
3979 key_out.write(key_raw as i64);
3980 val_out.write(val_raw as i64);
3981 }
3982 i64::from(key_tag) | i64::from(val_tag) << 8
3983 }
3984
3985 /// Load the raw `i64` payload of
3986 /// `vm.stack[base + slot_offset]` for the active trace's head
3987 /// Lua frame. Used to reload trace IR `Variable`s after a
3988 /// helper has written to `vm.stack` directly (e.g. TForCall's
3989 /// iter results land at `R[A+4..A+4+nvars]`).
3990 #[doc(hidden)]
3991 pub fn jit_stack_load(&mut self, slot_offset: u32) -> i64 {
3992 let Some(f) = self.jit_last_lua_frame() else {
3993 return 0;
3994 };
3995 let idx = (f.base as usize) + (slot_offset as usize);
3996 if idx >= self.stack.len() {
3997 return 0;
3998 }
3999 let v = self.stack[idx];
4000 let (_, raw) = v.unpack();
4001 // 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).
4002 unsafe { raw.zero as i64 }
4003 }
4004
4005 /// Read the tag byte of
4006 /// `vm.stack[base + slot_offset]`. Used by `Op::TForLoop` emit
4007 /// to dispatch on the iterator's return-key tag at runtime
4008 /// (`raw::NIL` → loop end exit, `raw::INT` → continue, other →
4009 /// deopt).
4010 #[doc(hidden)]
4011 pub fn jit_stack_tag(&mut self, slot_offset: u32) -> u8 {
4012 let Some(f) = self.jit_last_lua_frame() else {
4013 return crate::runtime::value::raw::NIL;
4014 };
4015 let idx = (f.base as usize) + (slot_offset as usize);
4016 if idx >= self.stack.len() {
4017 return crate::runtime::value::raw::NIL;
4018 }
4019 self.stack[idx].unpack().0
4020 }
4021
4022 /// Push a Lua frame onto the call stack with
4023 /// JIT-known metadata. Used by `luna_jit_trace_materialize_frames`
4024 /// at trace side-exits to recreate the inlined call activations
4025 /// the lowerer compiled past. The contract (enforced by the
4026 /// lowerer's pre-emit pass): `cl.proto` is non-vararg,
4027 /// `nresults` is the caller's expected count (today always 1
4028 /// because the lowerer bails Op::Call C != 2), and the caller
4029 /// has already called `jit_ensure_stack` to cover
4030 /// `[0..base + cl.proto.max_stack)`.
4031 #[doc(hidden)]
4032 pub fn jit_push_inlined_frame(
4033 &mut self,
4034 cl: Gc<LuaClosure>,
4035 base: u32,
4036 pc: u32,
4037 nresults: i32,
4038 ) {
4039 self.set_frame_ccmt(0);
4040 frames_push_sync(
4041 &mut self.frames,
4042 &mut self.frames_top,
4043 CallFrame::Lua(Frame {
4044 closure: cl,
4045 base,
4046 pc,
4047 // Lua call ABI: callee R[0] sits at caller R[A+1], so
4048 // callee.base = caller.base + A + 1; func_slot is
4049 // caller.base + A = callee.base - 1.
4050 func_slot: base - 1,
4051 n_varargs: 0,
4052 nresults,
4053 hook_oldpc: u32::MAX,
4054 from_c: false,
4055 tm: None,
4056 is_hook: false,
4057 tailcalls: 0,
4058 }),
4059 );
4060 }
4061
4062 /// Toggle precompiled-chunk loading. Default `true`. Sandbox embedders
4063 /// should set to `false` so `load`/`loadstring` reject bytecode input
4064 /// (which bypasses parser limits and could exploit verifier gaps).
4065 pub fn set_bytecode_loading(&mut self, enabled: bool) {
4066 self.bytecode_loading = enabled;
4067 }
4068
4069 /// Current bytecode-loading gate state.
4070 pub fn bytecode_loading(&self) -> bool {
4071 self.bytecode_loading
4072 }
4073
4074 /// Toggle PUC `.luac` bytecode loading. Default `false` — PUC
4075 /// bytecode is a strictly larger trust surface than luna's own dump
4076 /// format (third-party toolchain bugs, malformed chunks, unknown
4077 /// opcode shapes). Enable only for trusted PUC chunks. Per-dialect
4078 /// translators live in `crate::vm::dump::puc`.
4079 pub fn set_puc_bytecode_loading(&mut self, enabled: bool) {
4080 self.puc_bytecode_loading = enabled;
4081 }
4082
4083 /// Current PUC bytecode-loading gate state.
4084 pub fn puc_bytecode_loading(&self) -> bool {
4085 self.puc_bytecode_loading
4086 }
4087
4088 /// Default loader input budget — 256 MiB.
4089 ///
4090 /// `Vm::load` and the Lua-level `load(reader, ...)` both refuse
4091 /// sources whose byte length crosses this cap, returning the
4092 /// PUC-shaped `not enough memory` error rather than letting the
4093 /// host allocator try (and crash) to hold the next chunk.
4094 pub const DEFAULT_LOADER_INPUT_BUDGET: usize = 256 * 1024 * 1024;
4095
4096 /// Set the loader input byte budget (see
4097 /// [`Vm::DEFAULT_LOADER_INPUT_BUDGET`]). Pass `usize::MAX` to
4098 /// effectively disable. Smaller caps are honored verbatim — a 0
4099 /// cap rejects every non-empty source.
4100 pub fn set_loader_input_budget(&mut self, bytes: usize) {
4101 self.loader_input_budget = bytes;
4102 }
4103
4104 /// Current loader input byte budget.
4105 pub fn loader_input_budget(&self) -> usize {
4106 self.loader_input_budget
4107 }
4108
4109 /// Take the error traceback captured at the latest error point and
4110 /// reset it. Embedders should call this immediately after a failed
4111 /// `call_value`/`eval`/`call`/etc. — the next public `call_value`
4112 /// entry clears it. Returns `None` if no error was in flight.
4113 pub fn take_error_traceback(&mut self) -> Option<String> {
4114 let levels = self.error_traceback.take()?;
4115 let tb = crate::vm::callstack::traceback_from_lines(self.version, &levels, 0);
4116 Some(String::from_utf8_lossy(&tb).into_owned())
4117 }
4118
4119 /// PUC `luaL_traceback(L, L, msg, level)` on the running thread: `msg`
4120 /// (when given) and a newline, then `stack traceback:` and one line per
4121 /// stack level from `level` on, level 0 being the running function (the
4122 /// native calling this, when a native does).
4123 pub fn traceback(&mut self, msg: Option<&[u8]>, level: i64) -> Vec<u8> {
4124 let mut out = match msg {
4125 Some(m) => {
4126 let mut out = m.to_vec();
4127 out.push(b'\n');
4128 out
4129 }
4130 None => Vec::new(),
4131 };
4132 out.extend_from_slice(b"stack traceback:");
4133 out.extend(self.traceback_lines(None, level));
4134 out
4135 }
4136
4137 /// Arm the soft memory cap. The run loop checks the
4138 /// heap's tracked byte usage between dispatch turns; on overshoot it
4139 /// first runs a full collect, and if `bytes` still exceeds the cap it
4140 /// raises a catchable `"memory cap exceeded"` Lua error and disarms
4141 /// itself (fire-once: re-arm before the next `call_value` if reusing
4142 /// the Vm across requests). `None` removes the cap. The accounting is
4143 /// approximate — internal Vec/Box capacity overhead is not tracked,
4144 /// so embedders should size the cap with ~2× margin over the desired
4145 /// hard limit and additionally bound the Vm's lifetime (drop after
4146 /// each request).
4147 pub fn set_memory_cap(&mut self, cap: Option<usize>) {
4148 self.heap.mem_cap = cap;
4149 }
4150
4151 /// Approximate bytes the heap is currently holding. Object shells plus
4152 /// every table's internal array/hash boxes (tracked via
4153 /// `Heap::apply_bytes_delta` in `set`/`rehash`/`ensure_*`). Proto
4154 /// bytecode and closure upvalue slices still go uncounted — this is a
4155 /// lower bound, not a precise `malloc_stats`-style total.
4156 pub fn memory_used(&self) -> usize {
4157 self.heap.bytes()
4158 }
4159
4160 /// Read upvalue slot `i` of the native function currently on top of the
4161 /// dispatch chain (the one whose body is executing). Returns `Value::Nil`
4162 /// when no native is running. Public so the C ABI trampoline can fetch
4163 /// the host C function pointer it stashed there at registration time.
4164 pub fn running_native_upvalue(&self, i: usize) -> Value {
4165 match self.running_natives.last() {
4166 // 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).
4167 Some(nc) => unsafe {
4168 let upvals = &(*nc.as_ptr()).upvals;
4169 upvals.get(i).copied().unwrap_or(Value::Nil)
4170 },
4171 None => Value::Nil,
4172 }
4173 }
4174
4175 /// Register a table for finalization if its (just-set) metatable carries a
4176 /// `__gc` metamethod (PUC luaC_checkfinalizer at setmetatable time — adding
4177 /// `__gc` to the metatable afterwards does not retroactively register).
4178 pub(crate) fn check_finalizer(&mut self, t: Gc<Table>) {
4179 // Tables gained finalizers in 5.2; PUC 5.1 runs `__gc` for userdata only.
4180 if self.version == crate::version::LuaVersion::Lua51 {
4181 return;
4182 }
4183 if !self.get_mm(Value::Table(t), Mm::Gc).is_nil() {
4184 self.heap.register_finalizable(t);
4185 }
4186 }
4187
4188 /// Same as [`Self::check_finalizer`] for a userdata. PUC 5.1 attaches the
4189 /// finalizer to the proxy produced by `newproxy(true)` once its metatable
4190 /// gains `__gc`. gc.lua's "testing userdata" section sets `__gc` on the
4191 /// metatable that `newproxy` returned, which then needs to flow through.
4192 /// Kept available for the future 5.2+ `lua_setmetatable` path (which
4193 /// would re-check at metatable-set time); luna's only userdata
4194 /// finalizables today come via `newproxy`, which registers itself.
4195 #[allow(dead_code)]
4196 pub(crate) fn check_finalizer_userdata(&mut self, u: Gc<crate::runtime::Userdata>) {
4197 if !self.get_mm(Value::Userdata(u), Mm::Gc).is_nil() {
4198 self.heap.register_finalizable_userdata(u);
4199 }
4200 }
4201
4202 /// Run pending `__gc` finalizers (objects the collector resurrected for
4203 /// finalization). Finalizer errors are swallowed — PUC turns them into a
4204 /// warning; they must never propagate to the mutator. Reentrancy-guarded.
4205 fn run_finalizers(&mut self) {
4206 let _ = self.run_finalizers_or_err();
4207 }
4208
4209 fn run_finalizers_or_err(&mut self) -> Result<(), LuaError> {
4210 if self.gc_finalizing {
4211 return Ok(());
4212 }
4213 let pending = self.heap.take_tobefnz();
4214 if pending.is_empty() {
4215 return Ok(());
4216 }
4217 self.gc_finalizing = true;
4218 let mut first_err: Option<LuaError> = None;
4219 for obj in pending {
4220 let gc = self.get_mm(obj, Mm::Gc);
4221 // PUC 5.2+ accepts any non-nil `__gc` at setmetatable time to
4222 // schedule the object for finalization (`__gc = true` is the
4223 // canonical placeholder); only call it at finalize time when it
4224 // is actually a function. gc.lua 5.2 :412 wires up exactly this
4225 // sentinel and then expects no call.
4226 let callable = matches!(gc, Value::Closure(_) | Value::Native(_));
4227 if callable {
4228 // PUC `GCTM` sets `CIST_FIN` on the new ci so
4229 // `funcnamefromfinalizer` reports `namewhat = "metamethod"`,
4230 // `name = "__gc"`. luna threads the same outcome through the
4231 // generic `pending_tm` slot: the Lua frame born from this
4232 // call consumes it in `push_frame`. Saved/restored around the
4233 // call in case the handler is a native (which never pops it).
4234 // Bare event name; `frame_name` / `c_frame_name` add the
4235 // `"__"` debug prefix for 5.2/5.3, drop it for 5.4+. Matches
4236 // the convention used by `__close`, `__index`, …
4237 let saved_tm = self.pending_tm.replace("gc");
4238 // PUC `GCTM` runs the finalizer with `luaD_pcall` and no
4239 // message handler
4240 if let Err(e) = self.call_protected(gc, &[obj]) {
4241 // PUC 5.1 GCTM raised the finalizer's error to the
4242 // explicit `collectgarbage()` caller (`gc.lua 5.1 :255`
4243 // baselines on `not pcall(collectgarbage)`). 5.2/5.3
4244 // wrapped it in `error in __gc metamethod (msg)` first
4245 // (`callGCTM` → `luaG_runerror`) but still raised. 5.4
4246 // introduced the warning system and switched to "warn
4247 // then continue" — never re-raise, just route the
4248 // wrapped message through `warn`. gc.lua 5.5 :378 wires
4249 // up `_WARN` capture under the `if T then …` block to
4250 // baseline on the same wrapped string.
4251 if self.version >= LuaVersion::Lua54 {
4252 let inner = self.error_text(&e);
4253 let msg = format!("error in __gc metamethod ({inner})");
4254 self.emit_warn(msg.as_bytes(), false);
4255 } else if first_err.is_none() {
4256 let wrapped = if self.version >= LuaVersion::Lua52 {
4257 let inner = self.error_text(&e);
4258 let msg = format!("error in __gc metamethod ({inner})");
4259 let s = Value::Str(self.heap.intern(msg.as_bytes()));
4260 LuaError(s)
4261 } else {
4262 e
4263 };
4264 first_err = Some(wrapped);
4265 }
4266 }
4267 self.pending_tm = saved_tm;
4268 }
4269 }
4270 self.gc_finalizing = false;
4271 match first_err {
4272 Some(e) => Err(e),
4273 None => Ok(()),
4274 }
4275 }
4276
4277 /// Drive one incremental GC step (PUC `collectgarbage("step", n)`).
4278 /// Crosses up to three phases per call:
4279 /// 1. Pause → seed Propagate (`gc_start_propagate`)
4280 /// 2. Propagate → drain gray up to `budget`; on exhaustion run atomic
4281 /// (`gc_finish_atomic` → tobefnz populated; finalizers
4282 /// run via `run_finalizers`) and enter Sweep
4283 /// 3. Sweep → `gc_sweep_step` up to (residual) `budget`
4284 /// Returns true when this call completed the cycle's sweep (back to
4285 /// Pause). The budget is spent generously across phases — a large `n`
4286 /// can finish a whole cycle in one call (PUC stop-the-world step).
4287 pub(crate) fn gc_step(&mut self, budget: usize) -> bool {
4288 // Re-entry guard: never recurse — `run_finalizers` calls Lua code
4289 // that may hit a safe point and try to step again. Re-entry was OK
4290 // under STW (collect_garbage had its own guard) but here the
4291 // intermediate phase state would corrupt.
4292 if self.gc_finalizing {
4293 return false;
4294 }
4295 if self.heap.gc_phase_is_pause() {
4296 let (roots, extra) = self.gc_roots();
4297 self.heap.gc_start_propagate(&roots, &extra);
4298 }
4299 if self.heap.gc_phase_is_propagate() {
4300 if !self.heap.gc_step_propagate(budget) {
4301 return false;
4302 }
4303 self.heap.gc_finish_atomic();
4304 // any __gc scheduled by atomic — run before sweep so a finalizer
4305 // re-registering `self` re-enters the next cycle, not this sweep
4306 self.run_finalizers();
4307 }
4308 // either we just transitioned, or we entered already in Sweep, or
4309 // a finalizer started a new cycle (gc_sweep_step is a no-op then)
4310 self.heap.gc_sweep_step(budget)
4311 }
4312
4313 // ---- frames & calls ----
4314
4315 /// Begin calling stack[func_slot] with `nargs` (None: up to self.top).
4316 /// Returns true if a Lua frame was pushed (the dispatch loop continues
4317 /// there), false if a native completed inline.
4318 fn begin_call(
4319 &mut self,
4320 func_slot: u32,
4321 nargs: Option<u32>,
4322 nresults: i32,
4323 from_c: bool,
4324 ) -> Result<bool, LuaError> {
4325 let mut nargs = match nargs {
4326 Some(n) => n,
4327 None => self.top - (func_slot + 1),
4328 };
4329 // Consume `pending_is_tail` at the boundary: a tail-call op sets it
4330 // only for the immediately-following Lua activation. Native dispatch
4331 // (or `__call` resolution) below must not let it leak to the next
4332 // begin_call's frame; restore it just before push_frame for the Lua
4333 // arm so its meaning is preserved across __call chaining.
4334 let tailcalls = std::mem::take(&mut self.pending_tailcalls);
4335 let tail_ccmt = std::mem::take(&mut self.pending_ccmt);
4336 // resolve __call handlers iteratively (PUC tryfuncTM loop): each handler
4337 // is inserted before the value so it becomes the first argument, and a
4338 // chain of `__call` tables resolves down to a real function.
4339 let mut chain = 0u32;
4340 loop {
4341 match self.stack[func_slot as usize] {
4342 Value::Closure(cl) => {
4343 // JIT fast path: if the Proto's body fits
4344 // the int-arith whitelist, every arg is `Value::Int`,
4345 // and the cached arity matches, skip frame setup and
4346 // run the cached native fn in-place.
4347 if self.try_jit_call_op(cl, func_slot, nargs, nresults) {
4348 self.pending_tailcalls = tailcalls;
4349 return Ok(false);
4350 }
4351 self.pending_tailcalls = tailcalls;
4352 self.pending_ccmt = if tailcalls > 0 {
4353 tail_ccmt
4354 } else {
4355 chain as u8
4356 };
4357 self.push_frame(cl, func_slot, nargs, nresults, from_c)?;
4358 // Trace-on-call trigger. The frame
4359 // we just pushed is the callee whose body the
4360 // recorder will trace. Bump the per-Proto call
4361 // counter; once it crosses `CALL_HOT_THRESHOLD`
4362 // and no other trace is in flight, snapshot the
4363 // callee's register window (R[0..max_stack]) and
4364 // begin recording at `pc=0`. This is what unlocks
4365 // tracing for functions whose body has no negative
4366 // `Op::Jmp` back-edge (`fib`, recursive helpers).
4367 //
4368 // Gated on `trace_jit_enabled`, so the default
4369 // dispatch pays a single not-taken branch.
4370 if self.jit.trace_enabled {
4371 let proto = cl.proto;
4372 let c = proto.call_hot_count.get();
4373 if c < u32::MAX / 2 {
4374 proto.call_hot_count.set(c + 1);
4375 }
4376 // Relaxed call-trigger:
4377 // `c >= THRESHOLD` (not `c == THRESHOLD`) +
4378 // `!already_cached` short-circuit. Lets a
4379 // discarded short call-trigger close retry
4380 // on the next call (fib(10/15/20/25)
4381 // pathology — first capture is base-case
4382 // [Lt,Jmp,Return1]; coverage-heuristic
4383 // discards; next call gets to record at a
4384 // potentially deeper recursion point).
4385 // Without `already_cached`, the relaxed
4386 // condition would re-record over a cached
4387 // trace every call.
4388 //
4389 // Additionally short-circuit on
4390 // `proto.trace_gave_up`. The per-Proto discard
4391 // cap force-compiles a partial trace and
4392 // flips this flag; subsequent calls into
4393 // this Proto skip the RefCell borrow + Vec
4394 // scan entirely.
4395 if proto.trace_gave_up.get() {
4396 return Ok(true);
4397 }
4398 let call_already_cached =
4399 proto.traces.borrow().iter().any(|t| t.head_pc == 0)
4400 || trace_head_abandoned(proto, 0);
4401 if c >= self.jit.call_hot_threshold
4402 && self.jit.active_trace.is_none()
4403 && !call_already_cached
4404 {
4405 // The new frame is on top: index in
4406 // `self.frames` is `len() - 1`.
4407 let frame_idx = self.frames.len() - 1;
4408 // Snapshot R[0..max_stack] at the callee's
4409 // base. `push_frame` resized `self.stack`
4410 // to `base + max_stack`, so this window is
4411 // guaranteed in-bounds.
4412 let f = match &self.frames[frame_idx] {
4413 CallFrame::Lua(f) => f,
4414 _ => unreachable!("push_frame just pushed a Lua frame"),
4415 };
4416 let max_stack = cl.proto.max_stack as usize;
4417 let base_us = f.base as usize;
4418 let mut entry_tags = Vec::with_capacity(max_stack);
4419 for i in 0..max_stack {
4420 let (tag, _) = self.stack[base_us + i].unpack();
4421 entry_tags.push(tag);
4422 }
4423 self.jit.active_trace =
4424 Some(Box::new(crate::jit::trace::TraceRecord::start(
4425 cl.proto, 0, entry_tags, true,
4426 )));
4427 self.jit.recording_frame_base = frame_idx;
4428 }
4429 }
4430 return Ok(true);
4431 }
4432 Value::Native(nc) => {
4433 // Async-marked NativeClosure.
4434 // Route through the cooperative-yield mechanism
4435 // when async_mode is on; reject when called from
4436 // a sync `eval`/`call_value` path (would have no
4437 // executor to drive the returned future).
4438 if nc.is_async {
4439 if !self.async_mode {
4440 let s = Value::Str(
4441 self.heap.intern(b"async native called in sync context"),
4442 );
4443 self.last_error_kind = crate::vm::error::LuaErrorKind::Runtime;
4444 return Err(LuaError(s));
4445 }
4446 // Same root-up bookkeeping as the sync path:
4447 // pin args + result-count expectation so a
4448 // collection across the suspend boundary
4449 // keeps the arg window live.
4450 self.native_nresults = nresults;
4451 self.gc_top = func_slot + nargs + 1;
4452 // Fire the "call" hook BEFORE
4453 // building the future. Mirrors the sync native
4454 // path's `hook_call(true, nargs)` site
4455 // (`exec.rs` further down) so embedders with a
4456 // Rust debug hook installed see a Call event
4457 // for async natives identical to the sync
4458 // path. The matching "return" hook fires from
4459 // `commit_async_native_result` in
4460 // `async_drive.rs` after the future resolves.
4461 // Placement: after the `native_nresults` / `gc_top`
4462 // pin, before the future is constructed, so a
4463 // hook body that triggers GC observes the
4464 // correct pinned window. On hook error the
4465 // sentinel never returns and
4466 // `pending_async_native_*` remain `None` —
4467 // the executor sees `DispatchOutcome::Error`.
4468 self.hook_call(true, nargs)?;
4469 // Transmute the stored NativeFn back to its
4470 // real AsyncNativeFn shape. Sound because
4471 // `set_async_native` / `create_async_native`
4472 // installed an AsyncNativeFn through the
4473 // identically-sized fn-pointer slot, and the
4474 // `is_async` marker bit is what records that
4475 // fact.
4476 let async_fn: crate::vm::async_drive::AsyncNativeFn =
4477 // SAFETY: same-size fn pointers; provenance
4478 // preserved through `mem::transmute`. The
4479 // `is_async` marker is the only safe-to-call
4480 // gate, set exclusively by
4481 // `Vm::create_async_native`.
4482 unsafe { std::mem::transmute(nc.f) };
4483 let vm_ptr: *mut Vm = self;
4484 let fut = async_fn(vm_ptr, func_slot, nargs);
4485 // Stash the future + post-call context for
4486 // `drive_one` to surface to `EvalFuture::poll`.
4487 self.pending_async_native_fut = Some(fut);
4488 self.pending_async_native_ctx = Some(AsyncNativeCallCtx {
4489 func_slot,
4490 nargs,
4491 nresults,
4492 gc_top: self.gc_top,
4493 });
4494 // Sentinel Err walked up to `drive_one` (same
4495 // shape as `host_yield_pending`'s budget yield).
4496 // Value::Nil — never seen by user code.
4497 return Err(LuaError(Value::Nil));
4498 }
4499 // pcall/xpcall are yieldable: rather than calling the
4500 // protected function through the Rust stack (which cannot be
4501 // suspended), push a continuation frame and drive the call
4502 // through the interpreter loop (PUC lua_pcallk). A yield
4503 // inside it is preserved with the thread's saved frames.
4504 use crate::runtime::value::NativeFn;
4505 if std::ptr::fn_addr_eq(nc.f, nat_pcall as NativeFn) {
4506 return self.begin_pcall(func_slot, nargs, nresults);
4507 }
4508 if std::ptr::fn_addr_eq(nc.f, nat_xpcall as NativeFn) {
4509 // 5.1 `xpcall(f, err)` calls `f` with no arguments
4510 let forward = self.version > LuaVersion::Lua51;
4511 return self.begin_xpcall(func_slot, nargs, nresults, forward);
4512 }
4513 if std::ptr::fn_addr_eq(nc.f, nat_host_xpcall as NativeFn) {
4514 return self.begin_xpcall(func_slot, nargs, nresults, true);
4515 }
4516 // From 5.4 on, pairs(t) calls a __pairs metamethod yieldably
4517 // (PUC luaB_pairs uses lua_callk). 5.2/5.3 use a plain
4518 // lua_call, and 5.1 has no `__pairs`: the native handles those.
4519 if std::ptr::fn_addr_eq(nc.f, nat_pairs as NativeFn)
4520 && nargs >= 1
4521 && self.version >= LuaVersion::Lua54
4522 {
4523 let arg = self.stack[(func_slot + 1) as usize];
4524 if !self.get_mm(arg, Mm::Pairs).is_nil() {
4525 return self.begin_pairs(func_slot, nresults);
4526 }
4527 }
4528 // a native that collects (e.g. `collectgarbage`) roots up to
4529 // its own arguments — the caller's live registers all sit
4530 // below `func_slot` and stay rooted.
4531 self.native_nresults = nresults;
4532 self.gc_top = func_slot + nargs + 1;
4533 // Push the native onto the running-natives chain BEFORE
4534 // firing the call hook so that `debug.getinfo(level)` and
4535 // `arg_error` from inside the hook see this native as the
4536 // currently-running C function (db.lua :344 reads
4537 // `getinfo(2, "f").func` for the just-entered callee).
4538 // Popped after the matching return hook fires — even on
4539 // error, the pop must happen, so the body is bracketed
4540 // through a scope guard.
4541 self.running_natives.push(nc);
4542 self.running_native_acts
4543 .push(crate::vm::callstack::NativeAct {
4544 func_slot,
4545 nargs,
4546 depth: self.frames.len() as u32,
4547 // a tail call resolved its `__call` chain before
4548 // calling here and passed the count in tail_ccmt
4549 ccmt: tail_ccmt + chain as u8,
4550 });
4551 // PUC C-call discipline: entering a C function sets
4552 // L->top to func + 1 + nargs, so a collect triggered
4553 // INSIDE the native (explicit `collectgarbage()`, or
4554 // an allocation crossing the GC threshold) roots the
4555 // whole caller window up to and including the
4556 // arguments. Without this raise the cursor is stale —
4557 // parked at some earlier, possibly much lower
4558 // safe-point — and the collect frees register-held
4559 // values of the native's own caller (use-after-free).
4560 // Never lower it: a re-entrant chain
4561 // (native → Lua → native) must keep the outermost
4562 // window rooted.
4563 self.gc_top = self.gc_top.max(func_slot + 1 + nargs);
4564 // PUC luaD_precall fires the "call" hook for C functions too.
4565 // A yield inside the native (coroutine.yield) propagates an
4566 // Err and the matching "return" hook fires on resume instead.
4567 if let Err(e) = self.hook_call(true, nargs) {
4568 self.running_natives.pop();
4569 self.running_native_acts.pop();
4570 return Err(e);
4571 }
4572 // Trap a Rust panic in the native and surface it as
4573 // a Lua error rather than letting it unwind through the
4574 // VM into the embedder. The VM's internal state may still
4575 // be inconsistent after a panic (half-pushed args,
4576 // dangling GC references), so embedders that catch this
4577 // class of error should drop and re-create the Vm — but
4578 // it's still better than tearing the host process down.
4579 // `AssertUnwindSafe` is sound because the caller is the
4580 // dispatch loop and any half-done state is fenced behind
4581 // the immediate Err return below.
4582 use std::panic::{AssertUnwindSafe, catch_unwind};
4583 let result =
4584 match catch_unwind(AssertUnwindSafe(|| (nc.f)(self, func_slot, nargs))) {
4585 Ok(r) => r,
4586 Err(payload) => {
4587 let msg = panic_payload_str(&payload);
4588 let s = Value::Str(
4589 self.heap.intern(format!("native panic: {msg}").as_bytes()),
4590 );
4591 Err(LuaError(s))
4592 }
4593 };
4594 let nret = match result {
4595 Ok(n) => n,
4596 Err(e) => {
4597 // PUC raises with the native still on the stack;
4598 // remember it for the handler and traceback of the
4599 // error (see `raise_to_handler`)
4600 let act = self.running_native_acts.pop().expect("pushed above");
4601 self.running_natives.pop();
4602 self.note_errored_native(nc, act, e.0);
4603 return Err(e);
4604 }
4605 };
4606 // PUC `luaD_poscall` fires the return hook BEFORE moving
4607 // results into the function's slot — at that point args
4608 // sit at `[func_slot + 1, func_slot + 1 + nargs)` and
4609 // results above them at `[func_slot + 1 + nargs, …)`.
4610 // luna's `nat_return` has already written the results
4611 // into `[func_slot, func_slot + nret)`, so we replay PUC's
4612 // layout by copying the results up past the preserved
4613 // args, firing the hook (with ftransfer = nargs + 1, so
4614 // `getlocal(2, ftransfer..)` reads results), and then
4615 // copying back for `finish_results`. db.lua :541 reads
4616 // `getinfo("r").ftransfer` + `getlocal` to inspect a
4617 // returning native's results this way.
4618 if self.hook.ret
4619 && !self.in_hook
4620 && (self.hook.func.is_some() || self.hook.rust_func.is_some())
4621 {
4622 let res_dst = func_slot + nargs + 1;
4623 let need = (res_dst + nret) as usize;
4624 if self.stack.len() < need {
4625 self.stack.resize(need, Value::Nil);
4626 }
4627 for i in (0..nret).rev() {
4628 self.stack[(res_dst + i) as usize] =
4629 self.stack[(func_slot + i) as usize];
4630 }
4631 // widen the C-frame's argument window for getlocal
4632 if let Some(act) = self.running_native_acts.last_mut() {
4633 act.nargs = nargs + nret;
4634 }
4635 let hr = self.hook_return(true, nargs + 1, nret);
4636 if let Some(act) = self.running_native_acts.last_mut() {
4637 act.nargs = nargs;
4638 }
4639 // restore results into the slot finish_results expects
4640 for i in 0..nret {
4641 self.stack[(func_slot + i) as usize] =
4642 self.stack[(res_dst + i) as usize];
4643 }
4644 self.running_natives.pop();
4645 self.running_native_acts.pop();
4646 hr?;
4647 } else {
4648 self.running_natives.pop();
4649 self.running_native_acts.pop();
4650 }
4651 self.finish_results(func_slot, nret, nresults);
4652 // the native may have allocated; collect with the results as
4653 // the live boundary (PUC checks GC after a call returns).
4654 self.maybe_collect_garbage(self.top);
4655 return Ok(false);
4656 }
4657 v => {
4658 let mm = self.get_mm(v, Mm::Call);
4659 if mm.is_nil() || self.call_mm_unusable(mm) {
4660 return Err(self.call_err(v));
4661 }
4662 chain += 1;
4663 // PUC 5.5 dropped the chain cap from `MAXTAGRECUR = 200`
4664 // (the value 5.4's `lvm.c` uses) down to `MAXCCMT = 16`,
4665 // and the 5.5 test exercises the new tight bound directly
4666 // (calls.lua :225 builds a 16-deep chain and expects the
4667 // 16th to error). 5.4 calls.lua :194 instead builds a 20-
4668 // deep chain and expects it to succeed.
4669 let cap = if self.version >= crate::version::LuaVersion::Lua55 {
4670 15
4671 } else {
4672 MAX_CCMT
4673 };
4674 if chain > cap {
4675 return Err(self.rt_err("'__call' chain too long"));
4676 }
4677 // slots above shift by one; at a call site those are dead
4678 // temps of the current frame
4679 self.stack.insert(func_slot as usize, mm);
4680 if self.top > func_slot {
4681 self.top += 1;
4682 }
4683 nargs += 1;
4684 }
4685 }
4686 }
4687 }
4688
4689 /// Up to 5.3 `tryfuncTM` takes one `__call` hop and needs a function
4690 /// there; anything else is a call error on the original object. 5.4
4691 /// retries the call with whatever `__call` holds, so chains resolve.
4692 fn call_mm_unusable(&self, mm: Value) -> bool {
4693 self.version <= LuaVersion::Lua53 && !matches!(mm, Value::Closure(_) | Value::Native(_))
4694 }
4695
4696 fn push_frame(
4697 &mut self,
4698 cl: Gc<LuaClosure>,
4699 func_slot: u32,
4700 nargs: u32,
4701 nresults: i32,
4702 from_c: bool,
4703 ) -> Result<(), LuaError> {
4704 if func_slot + 256 > MAX_LUA_STACK {
4705 // PUC `luaD_growstack`: the overflow raises "stack overflow" and
4706 // leaves ERRORSTACKSIZE's extra slots for the xpcall handler that
4707 // runs on it; overflowing those is LUA_ERRERR, "error in error
4708 // handling" (errors.lua :606, cstack.lua :29).
4709 if self.msgh_depth == 0 {
4710 return Err(self.rt_err("stack overflow"));
4711 }
4712 if func_slot + 256 > MAX_LUA_STACK + ERROR_STACK_EXTRA {
4713 return Err(self.plain_err("error in error handling"));
4714 }
4715 }
4716 let proto = cl.proto;
4717 let nparams = proto.num_params as u32;
4718 // 5.5 vararg layout (PUC luaT_adjustvarargs): the extra args stay on the
4719 // stack just below the new `base`, so a named vararg can be indexed
4720 // virtually without allocating a table. Rotate `[p1..pn][e1..em]` to
4721 // `[e1..em][p1..pn]` so the fixed params land at the new base.
4722 let n_varargs = if proto.is_vararg {
4723 nargs.saturating_sub(nparams)
4724 } else {
4725 0
4726 };
4727 if n_varargs > 0 {
4728 let s = (func_slot + 1) as usize;
4729 self.stack[s..s + nargs as usize].rotate_left(nparams as usize);
4730 }
4731 let base = func_slot + 1 + n_varargs;
4732 let need = (base + proto.max_stack as u32) as usize;
4733 if self.stack.len() < need {
4734 self.stack.resize(need, Value::Nil);
4735 }
4736 // wipe the register window beyond the kept parameters (stale values —
4737 // required for GC-safety and codegen). The varargs below `base` survive.
4738 let kept = nargs.saturating_sub(n_varargs).min(nparams);
4739 // SAFETY: just resized above so `need <= stack.len()`; `base + kept <=
4740 // need` since `base + nparams <= base + max_stack = need` and `kept <=
4741 // nparams`. `slice::fill` lowers to a single memset on Copy types.
4742 unsafe {
4743 self.stack
4744 .get_unchecked_mut((base + kept) as usize..need)
4745 .fill(Value::Nil);
4746 }
4747 let ccmt = std::mem::take(&mut self.pending_ccmt);
4748 self.set_frame_ccmt(ccmt);
4749 frames_push_sync(
4750 &mut self.frames,
4751 &mut self.frames_top,
4752 CallFrame::Lua(Frame {
4753 closure: cl,
4754 base,
4755 pc: 0,
4756 func_slot,
4757 nresults,
4758 hook_oldpc: u32::MAX,
4759 from_c,
4760 n_varargs,
4761 // single-shot consume: `close_slots` sets pending_tm before each
4762 // handler call; the next Lua frame born is that handler's.
4763 tm: self.pending_tm.take(),
4764 // `run_hook` sets `pending_is_hook` before dispatching the user
4765 // hook so its frame reports `namewhat = "hook"` via getinfo.
4766 is_hook: std::mem::take(&mut self.pending_is_hook),
4767 tailcalls: std::mem::take(&mut self.pending_tailcalls),
4768 }),
4769 );
4770 // PUC 5.1 `LUAI_COMPAT_VARARG`: populate the hidden `arg` local with
4771 // `{ n = n_varargs, [1] = e1, [2] = e2, … }`. The compiler reserved
4772 // the slot at `base + nparams`; the extras sit just below `base` from
4773 // the vararg rotate above. 5.1 db.lua :279 reads `arg.n` from a line
4774 // hook; vararg.lua's contradictory expectations were already going to
4775 // fail either way (some asserts want `arg == nil`).
4776 if proto.has_compat_vararg_arg {
4777 let arg_slot = (base + nparams) as usize;
4778 let t = self.heap.new_table();
4779 {
4780 // 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).
4781 let tm = unsafe { t.as_mut() };
4782 for i in 0..n_varargs {
4783 let v = self.stack[(base - n_varargs + i) as usize];
4784 // bounded by `n_varargs` (≤ MAXUPVAL territory), well
4785 // below `MAX_ASIZE`
4786 let _ = tm.set_int(&mut self.heap, (i + 1) as i64, v);
4787 }
4788 let nk = Value::Str(self.heap.intern(b"n"));
4789 tm.set(&mut self.heap, nk, Value::Int(n_varargs as i64))
4790 .expect("'n' key");
4791 }
4792 // once-per-table barrier mirrors SETLIST: t is born BLACK during
4793 // Propagate and the bulk `set_int`/`set` calls above don't barrier
4794 self.heap
4795 .barrier_back(t.as_ptr() as *mut crate::runtime::heap::GcHeader);
4796 self.stack[arg_slot] = Value::Table(t);
4797 }
4798 // PUC luaD_precall fires the "call" hook with the new frame current, so
4799 // a hook calling debug.getinfo(2) sees the entered function. For a Lua
4800 // callee, PUC `luaD_hookcall` passes `p->numparams` as ntransfer (only
4801 // fixed params count — extras already live below `base`).
4802 // A frame born via OP_TailCall fires "tail call" instead (PUC
4803 // luaD_pretailcall) and skips the matching "return" hook on exit.
4804 let is_tail = self
4805 .frames
4806 .last()
4807 .and_then(|f| f.lua())
4808 .is_some_and(|f| f.tailcalls > 0);
4809 self.hook_call_with(false, nparams, is_tail)?;
4810 Ok(())
4811 }
4812
4813 /// `pcall(f, ...)` (PUC luaB_pcall): push a continuation frame, then drive
4814 /// the protected call `f` through the interpreter loop. The protected
4815 /// function and its arguments already sit at `func_slot+1..`, so calling `f`
4816 /// at `func_slot+1` lets its results land one slot above the continuation —
4817 /// the loop head then writes `true` at `func_slot` to form `true, results…`.
4818 /// Always returns `Ok(true)`: a continuation is now on the stack to be
4819 /// resolved by the loop (even when `f` is a native that already ran inline).
4820 fn begin_pcall(&mut self, func_slot: u32, nargs: u32, nresults: i32) -> Result<bool, LuaError> {
4821 if nargs == 0 {
4822 // `luaL_checkany` fails here: there is no function to call.
4823 self.with_native_running(func_slot, nargs, |vm| {
4824 let a = crate::vm::argcheck::Args::new(func_slot, nargs);
4825 crate::vm::argcheck::check_any(vm, a, 0).map(drop)
4826 })?;
4827 }
4828 if self.pcall_depth >= MAX_C_DEPTH {
4829 // raised inside pcall, a C function: no position
4830 return Err(self.plain_err("C stack overflow"));
4831 }
4832 self.pcall_depth += 1;
4833 frames_push_sync(
4834 &mut self.frames,
4835 &mut self.frames_top,
4836 CallFrame::Cont(NativeCont {
4837 kind: ContKind::Pcall,
4838 func_slot,
4839 nresults,
4840 }),
4841 );
4842 // call f (slot func_slot+1) with the remaining args, asking for all
4843 // results; a yield or error inside propagates with the continuation kept
4844 // on the stack (caught by `unwind` / preserved across a yield).
4845 self.begin_call(func_slot + 1, Some(nargs - 1), -1, true)?;
4846 Ok(true)
4847 }
4848
4849 /// `xpcall(f, msgh, ...)` (PUC luaB_xpcall): like `begin_pcall`, but the
4850 /// message handler is stashed in the continuation and the arguments are
4851 /// shifted down over the handler's slot so `f`'s args are contiguous.
4852 /// `forward` is false for 5.1's `xpcall`, which passes `f` none of them.
4853 fn begin_xpcall(
4854 &mut self,
4855 func_slot: u32,
4856 nargs: u32,
4857 nresults: i32,
4858 forward: bool,
4859 ) -> Result<bool, LuaError> {
4860 self.with_native_running(func_slot, nargs, |vm| {
4861 let a = crate::vm::argcheck::Args::new(func_slot, nargs);
4862 crate::vm::builtins::xpcall_handler(vm, a).map(drop)
4863 })?;
4864 if self.pcall_depth >= MAX_C_DEPTH {
4865 // raised inside pcall, a C function: no position
4866 return Err(self.plain_err("C stack overflow"));
4867 }
4868 self.pcall_depth += 1;
4869 // layout: [xpcall@func_slot, f@+1, msgh@+2, a1@+3, ...]. Stash msgh and
4870 // close its gap so f's args become [f@+1, a1@+2, ...].
4871 let handler = self.stack[(func_slot + 2) as usize];
4872 // 5.1: `xpcall (f, err)` takes exactly two parameters — extra
4873 // arguments are NOT forwarded to `f` (5.2 added forwarding;
4874 // 5.1 calls f with zero args).
4875 let nfargs = if forward { nargs - 2 } else { 0 };
4876 for i in 0..nfargs {
4877 self.stack[(func_slot + 2 + i) as usize] = self.stack[(func_slot + 3 + i) as usize];
4878 }
4879 self.top = func_slot + 2 + nfargs;
4880 frames_push_sync(
4881 &mut self.frames,
4882 &mut self.frames_top,
4883 CallFrame::Cont(NativeCont {
4884 kind: ContKind::Xpcall { handler },
4885 func_slot,
4886 nresults,
4887 }),
4888 );
4889 self.begin_call(func_slot + 1, Some(nfargs), -1, true)?;
4890 Ok(true)
4891 }
4892
4893 /// `pairs(t)` where `t` has a `__pairs` metamethod (PUC luaB_pairs's
4894 /// lua_callk path): drive `__pairs(t)` through the loop with a `Pairs`
4895 /// continuation so a `coroutine.yield` inside it suspends cleanly. The
4896 /// metamethod is called in `pairs`'s own slot, so its (≤4, nil-padded)
4897 /// results land exactly where `pairs`'s results belong.
4898 /// Run a check of the native at `func_slot` while it counts as the running
4899 /// C function, so an argument error names it the way PUC does. pcall and
4900 /// xpcall check their arguments in the dispatcher, before the native
4901 /// would otherwise be entered.
4902 fn with_native_running(
4903 &mut self,
4904 func_slot: u32,
4905 nargs: u32,
4906 check: impl FnOnce(&mut Vm) -> Result<(), LuaError>,
4907 ) -> Result<(), LuaError> {
4908 let Value::Native(nc) = self.stack[func_slot as usize] else {
4909 unreachable!("pcall/xpcall dispatch sits on a native")
4910 };
4911 self.running_natives.push(nc);
4912 self.running_native_acts
4913 .push(crate::vm::callstack::NativeAct {
4914 func_slot,
4915 nargs,
4916 depth: self.frames.len() as u32,
4917 ccmt: 0,
4918 });
4919 let r = check(self);
4920 self.running_natives.pop();
4921 self.running_native_acts.pop();
4922 r
4923 }
4924
4925 fn begin_pairs(&mut self, func_slot: u32, nresults: i32) -> Result<bool, LuaError> {
4926 let arg = self.stack[(func_slot + 1) as usize];
4927 let mm = self.get_mm(arg, Mm::Pairs);
4928 // layout becomes [pairs@func_slot, mm@func_slot+1, t@func_slot+2]:
4929 // `pairs` keeps its slot so the debug interface can report it as the
4930 // C function running below the metamethod. Call mm(t) wanting 4.
4931 let need = (func_slot + 3) as usize;
4932 if self.stack.len() < need {
4933 self.stack.resize(need, Value::Nil);
4934 }
4935 self.stack[(func_slot + 2) as usize] = arg;
4936 self.stack[(func_slot + 1) as usize] = mm;
4937 self.top = func_slot + 3;
4938 frames_push_sync(
4939 &mut self.frames,
4940 &mut self.frames_top,
4941 CallFrame::Cont(NativeCont {
4942 kind: ContKind::Pairs,
4943 func_slot,
4944 nresults,
4945 }),
4946 );
4947 let want = crate::vm::builtins::pairs_mm_results(self) as i32;
4948 self.begin_call(func_slot + 1, Some(1), want, true)?;
4949 Ok(true)
4950 }
4951
4952 /// The running (top) Lua frame. The interpreter only reads this while a Lua
4953 /// frame is on top — a continuation frame is never the running frame (it is
4954 /// consumed the instant the call it protects unwinds onto it).
4955 #[inline]
4956 /// Records the `__call` count of the Lua frame about to be pushed.
4957 fn set_frame_ccmt(&mut self, ccmt: u8) {
4958 let i = self.frames.len();
4959 if self.frame_ccmt.len() <= i {
4960 self.frame_ccmt.resize(i + 1, 0);
4961 }
4962 self.frame_ccmt[i] = ccmt;
4963 }
4964
4965 fn top_frame(&self) -> &Frame {
4966 self.frames
4967 .last()
4968 .and_then(CallFrame::lua)
4969 .expect("running Lua frame")
4970 }
4971
4972 #[inline]
4973 fn top_frame_mut(&mut self) -> &mut Frame {
4974 self.frames
4975 .last_mut()
4976 .and_then(CallFrame::lua_mut)
4977 .expect("running Lua frame")
4978 }
4979
4980 /// Pad/announce results sitting at func_slot.
4981 pub(crate) fn finish_results(&mut self, func_slot: u32, nret: u32, wanted: i32) {
4982 // Capture the call's high-water-mark before
4983 // setting the new top so we can Nil-clear slots that the
4984 // call temporarily wrote but no longer holds — matching
4985 // PUC's `L->top` discipline (slots past L->top are "free"
4986 // and the next push overwrites them). Without this clear,
4987 // a stale `Value::Closure` (e.g. the called function
4988 // itself, when wanted = 0) sits at `func_slot` and a
4989 // later GC with wider `gc_top` traces it after the
4990 // closure has been freed by a previous narrow safe-point
4991 // GC → heap-buffer-overflow in `Marker::header` (sort.lua
4992 // comparator case).
4993 let prev_top = self.top as usize;
4994 if wanted < 0 {
4995 self.top = func_slot + nret;
4996 } else {
4997 let wanted = wanted as u32;
4998 let need = (func_slot + wanted) as usize;
4999 if self.stack.len() < need {
5000 self.stack.resize(need, Value::Nil);
5001 }
5002 for i in nret..wanted {
5003 self.stack[(func_slot + i) as usize] = Value::Nil;
5004 }
5005 self.top = func_slot + wanted;
5006 }
5007 let new_top = self.top as usize;
5008 let clear_end = prev_top.min(self.stack.len());
5009 if new_top < clear_end {
5010 for slot in &mut self.stack[new_top..clear_end] {
5011 *slot = Value::Nil;
5012 }
5013 }
5014 }
5015
5016 /// Current Lua call-frame depth (read-only).
5017 /// Used by `EvalFuture` on the bootstrap poll to compute the
5018 /// `entry_depth` it will pass to subsequent resume slices.
5019 pub(crate) fn frame_count(&self) -> usize {
5020 self.frames.len()
5021 }
5022
5023 fn take_results(&mut self, func_slot: u32) -> Vec<Value> {
5024 let nret = self.top - func_slot;
5025 let out = self.stack[func_slot as usize..(func_slot + nret) as usize].to_vec();
5026 self.stack.truncate(func_slot as usize);
5027 self.top = func_slot;
5028 out
5029 }
5030
5031 // ---- open upvalues ----
5032
5033 #[doc(hidden)]
5034 pub fn find_or_create_upval(&mut self, slot: u32) -> Gc<Upvalue> {
5035 match self.open_upvals.binary_search_by_key(&slot, |&(s, _)| s) {
5036 Ok(i) => self.open_upvals[i].1,
5037 Err(i) => {
5038 let uv = self.heap.new_upvalue(UpvalState::Open {
5039 slot,
5040 thread: self.current,
5041 });
5042 self.open_upvals.insert(i, (slot, uv));
5043 uv
5044 }
5045 }
5046 }
5047
5048 pub(crate) fn close_from(&mut self, slot: u32) {
5049 while let Some(&(s, uv)) = self.open_upvals.last() {
5050 if s < slot {
5051 break;
5052 }
5053 let v = self.stack[s as usize];
5054 // 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).
5055 unsafe { uv.as_mut() }.set_closed(v);
5056 self.heap
5057 .barrier_forward(uv.as_ptr() as *mut crate::runtime::heap::GcHeader, v);
5058 self.open_upvals.pop();
5059 }
5060 }
5061
5062 /// Register a to-be-closed slot (TBC op / generic-for closing value).
5063 fn register_tbc(&mut self, slot: u32) -> Result<(), LuaError> {
5064 let v = self.stack[slot as usize];
5065 if matches!(v, Value::Nil | Value::Bool(false)) {
5066 return Ok(()); // nil and false are silently ignored
5067 }
5068 if self.get_mm(v, Mm::Close).is_nil() {
5069 // PUC `checkclosemth`: "variable '<name>' got a non-closable
5070 // value", the name as `luaG_findlocal` gives it — the frame's
5071 // locvars at this pc, else "(temporary)".
5072 let f = self.top_frame();
5073 let reg = slot - f.base;
5074 let pc = (f.pc as usize).saturating_sub(1);
5075 let name = crate::vm::objname::getlocalname(&f.closure.proto, reg, pc)
5076 .unwrap_or("(temporary)");
5077 return Err(self.rt_err(&format!("variable '{name}' got a non-closable value")));
5078 }
5079 // compiled code registers in register order and closes before it
5080 // registers a slot again; only a crafted chunk (`TBC R0; TBC R0`)
5081 // breaks that, which PUC's list cannot represent either
5082 if self.tbc.last().is_some_and(|&s| s >= slot) {
5083 return Err(self.rt_err("'<close>' state corrupted"));
5084 }
5085 self.tbc.push(slot);
5086 Ok(())
5087 }
5088
5089 /// Close upvalues and run `__close` handlers for slots ≥ `from`
5090 /// (handlers in reverse registration order; PUC luaF_close).
5091 fn close_slots(&mut self, from: u32, err: Option<Value>) -> Result<(), LuaError> {
5092 self.close_from(from);
5093 // PUC: handlers run in reverse declaration order; an error raised by a
5094 // handler becomes the error object passed to the remaining ones, and
5095 // the rest are still closed. The last raised error propagates.
5096 let mut pending = err;
5097 let mut result = Ok(());
5098 let saved_err = self.closing_err;
5099 // On a normal close the handler runs within the closing function's
5100 // activation (debug parent = that function); during error unwinding the
5101 // function's frame is already gone, so the handler sits at the C
5102 // boundary instead (PUC: luaF_close runs after the ci is restored).
5103 let error_close = err.is_some();
5104 while let Some(&s) = self.tbc.last() {
5105 if s < from {
5106 break;
5107 }
5108 self.tbc.pop();
5109 let v = self.stack[s as usize];
5110 if matches!(v, Value::Nil | Value::Bool(false)) {
5111 continue;
5112 }
5113 let mm = self.get_mm(v, Mm::Close);
5114 if mm.is_nil() {
5115 // PUC `prepclosingmethod`: the __close metamethod was present
5116 // at OP_TBC (else we would have errored there) but has since
5117 // been removed/replaced. Treat as a non-callable target.
5118 let tn = self.obj_typename(v);
5119 let e = self.rt_err(&format!(
5120 "attempt to call a {tn} value (metamethod 'close')"
5121 ));
5122 pending = Some(e.0);
5123 result = Err(e);
5124 continue;
5125 }
5126 // root the pending error: a handler may trigger a collection
5127 self.closing_err = pending;
5128 // PUC `luaF_close` sets `ci->u.l.tm = TM_CLOSE` so traceback /
5129 // getinfo report the handler as "in metamethod 'close'". Saved/
5130 // restored around the call to cover the path where `mm` is a
5131 // native (`push_frame` never consumes it) or it raises before
5132 // reaching push_frame.
5133 let saved_tm = self.pending_tm.replace("close");
5134 // PUC 5.4 `prepclosingmethod` always pushed (obj, errobj) — errobj
5135 // is nil on a normal close (5.4 locals.lua :875's
5136 // `func2close(coroutine.yield)` wrap pins `(self, nil)` back
5137 // through the yield). PUC 5.5 dropped the trailing nil: a clean
5138 // close passes only `obj`, the error case still passes both
5139 // (5.5 locals.lua :314 `select("#", ...) == n` with n=1 for the
5140 // normal-close arms, n=2 for the error arm).
5141 let call = match pending {
5142 Some(e) => self.call_value_impl(mm, &[v, e], error_close),
5143 None => {
5144 if self.version >= LuaVersion::Lua55 {
5145 self.call_value_impl(mm, &[v], error_close)
5146 } else {
5147 self.call_value_impl(mm, &[v, Value::Nil], error_close)
5148 }
5149 }
5150 };
5151 self.pending_tm = saved_tm;
5152 if let Err(e) = call {
5153 pending = Some(e.0);
5154 result = Err(e);
5155 }
5156 }
5157 self.closing_err = saved_err;
5158 result
5159 }
5160
5161 /// Yieldable variant of `close_slots`: drive the chain of `__close`
5162 /// handlers for slots ≥ `from` through the interpreter loop with a
5163 /// `Cont::Close` continuation, so a `coroutine.yield()` inside any handler
5164 /// suspends cleanly (the close iteration's state rides on the thread's
5165 /// frame/stack like any other suspended call) — PUC's `lua_callk` pattern
5166 /// applied to `luaF_close`. `after` runs when every slot is closed; if
5167 /// `after` is `Return` and we've returned past `entry_depth`,
5168 /// `Ok(Some(vals))` carries the result up to the host caller.
5169 fn begin_close(
5170 &mut self,
5171 from: u32,
5172 err: Option<Value>,
5173 after: AfterClose,
5174 entry_depth: usize,
5175 ) -> Result<Option<Vec<Value>>, LuaError> {
5176 self.close_from(from);
5177 self.drive_close(from, err, after, entry_depth)
5178 }
5179
5180 /// Pop tbc slots ≥ `from`, skipping nil/false and synthesising a
5181 /// non-callable-mm error for an `__close` that was reset to a bad value
5182 /// between OP_TBC and now (PUC `prepclosingmethod`). The first real
5183 /// handler pushes a `Cont::Close` + `begin_call` and returns `Ok(None)`;
5184 /// the interpreter then drives the handler and re-enters this driver via
5185 /// the `Cont::Close` consumer in `run()`. When the chain is exhausted,
5186 /// the threaded error (if any) propagates or `after` fires.
5187 fn drive_close(
5188 &mut self,
5189 from: u32,
5190 mut pending: Option<Value>,
5191 after: AfterClose,
5192 entry_depth: usize,
5193 ) -> Result<Option<Vec<Value>>, LuaError> {
5194 loop {
5195 let drained = match self.tbc.last() {
5196 None => true,
5197 Some(&s) => s < from,
5198 };
5199 if drained {
5200 return self.finish_close_after(after, pending, entry_depth);
5201 }
5202 let s = self.tbc.pop().expect("tbc non-empty");
5203 let v = self.stack[s as usize];
5204 if matches!(v, Value::Nil | Value::Bool(false)) {
5205 continue;
5206 }
5207 let mm = self.get_mm(v, Mm::Close);
5208 if mm.is_nil() {
5209 let tn = self.obj_typename(v);
5210 let e = self.rt_err(&format!(
5211 "attempt to call a {tn} value (metamethod 'close')"
5212 ));
5213 pending = Some(e.0);
5214 continue;
5215 }
5216 // A real handler: stage [mm, v, (err?)] above the current top,
5217 // record the close iteration state in a Cont::Close, and let the
5218 // interpreter dispatch the handler. On return the run() head
5219 // re-enters this driver via the Cont::Close consumer.
5220 let func_slot = self.top;
5221 let error_close = pending.is_some();
5222 let need = (func_slot + 3) as usize;
5223 if self.stack.len() < need {
5224 self.stack.resize(need, Value::Nil);
5225 }
5226 self.stack[func_slot as usize] = mm;
5227 self.stack[func_slot as usize + 1] = v;
5228 // PUC 5.4 always passes (obj, errobj=nil) on a normal close;
5229 // 5.5 drops the trailing nil. 5.4 locals.lua :875 vs 5.5 :314.
5230 let nargs = match pending {
5231 Some(e) => {
5232 self.stack[func_slot as usize + 2] = e;
5233 2u32
5234 }
5235 None => {
5236 if self.version >= LuaVersion::Lua55 {
5237 1u32
5238 } else {
5239 self.stack[func_slot as usize + 2] = Value::Nil;
5240 2u32
5241 }
5242 }
5243 };
5244 self.top = func_slot + 1 + nargs;
5245 // Root the pending error during the call (a handler may collect).
5246 let saved_err = self.closing_err;
5247 self.closing_err = pending;
5248 // PUC `luaF_close` flags the handler frame as "metamethod 'close'"
5249 // for traceback / getinfo.
5250 let saved_tm = self.pending_tm.replace("close");
5251 frames_push_sync(
5252 &mut self.frames,
5253 &mut self.frames_top,
5254 CallFrame::Cont(NativeCont {
5255 kind: ContKind::Close(CloseCont {
5256 from,
5257 pending,
5258 after,
5259 }),
5260 func_slot,
5261 nresults: 0,
5262 }),
5263 );
5264 // PUC luaF_close runs a normal close *within* the closing
5265 // function's activation (debug parent = that function); during an
5266 // error unwind the function's frame is already gone and the
5267 // handler sits at the C boundary instead.
5268 let r = self.begin_call(func_slot, Some(nargs), 0, error_close);
5269 self.pending_tm = saved_tm;
5270 self.closing_err = saved_err;
5271 r?;
5272 return Ok(None);
5273 }
5274 }
5275
5276 /// Fire `after` once every `__close` handler has run. `Block` propagates
5277 /// any remaining error or simply continues; `Return` performs OP_Return's
5278 /// tail (hook + frame pop + result delivery) and may surface results to
5279 /// the host when the function whose return triggered the close was the
5280 /// entry activation, but only on a clean drain — a pending error skips
5281 /// the return tail and propagates instead. `ResumeUnwind` pops the
5282 /// deferred Lua frame and re-raises, letting a handler's own error win
5283 /// over the original propagating one (PUC luaF_close).
5284 fn finish_close_after(
5285 &mut self,
5286 after: AfterClose,
5287 pending: Option<Value>,
5288 entry_depth: usize,
5289 ) -> Result<Option<Vec<Value>>, LuaError> {
5290 match after {
5291 AfterClose::Block => match pending {
5292 Some(e) => Err(LuaError(e)),
5293 None => Ok(None),
5294 },
5295 AfterClose::Return {
5296 abs_a,
5297 nret,
5298 from_native,
5299 } => match pending {
5300 Some(e) => Err(LuaError(e)),
5301 None => self.complete_return(abs_a, nret, from_native, entry_depth),
5302 },
5303 AfterClose::ResumeUnwind { func_slot, err } => {
5304 // The aborting Lua frame was popped before `begin_close`;
5305 // restore the catcher's stack window down to `func_slot` and
5306 // re-raise — preferring a handler-raised error over the
5307 // original (PUC luaF_close).
5308 self.stack.truncate(func_slot as usize);
5309 self.top = func_slot;
5310 self.tbc.retain(|&s| s < func_slot);
5311 Err(LuaError(pending.unwrap_or(err)))
5312 }
5313 }
5314 }
5315
5316 /// OP_Return's post-close tail: fire the "return" hook (frame still
5317 /// current), pop the Lua frame, slide results into `func_slot`, then
5318 /// either hand them to the host (`Ok(Some(vals))` when we've returned
5319 /// past `entry_depth`), leave them contiguous for an exposed
5320 /// pcall/xpcall continuation, or finish into the caller's expected
5321 /// result slot. Mirrors the synchronous OP_Return tail so both paths
5322 /// share semantics — the `from_native` flag selects the right "return"
5323 /// hook context for `hook_return`.
5324 fn complete_return(
5325 &mut self,
5326 abs_a: u32,
5327 nret: u32,
5328 from_native: bool,
5329 entry_depth: usize,
5330 ) -> Result<Option<Vec<Value>>, LuaError> {
5331 // ftransfer is the local index (1-based) of the first result, as
5332 // `getinfo("r").ftransfer + getlocal(level, k)` consumes it. luna
5333 // exposes locals starting at `frame.base` (= func_slot + 1 +
5334 // n_varargs for a vararg call), so the conversion is the absolute
5335 // result slot minus base, plus one to make it 1-based. db.lua 5.4
5336 // :542 (`foo1(); on=false; eqseq(out, {10, 0})`) pins the vararg
5337 // shape end-to-end.
5338 let ftransfer = self
5339 .frames
5340 .last()
5341 .and_then(CallFrame::lua)
5342 .map(|fr| {
5343 let raw = abs_a.saturating_sub(fr.base) + 1;
5344 // 5.5 anonymous-vararg functions get a `(vararg table)` pseudo
5345 // local injected at index `numparams + 1`, so getlocal
5346 // numbering shifts results past it (5.5 db.lua :539
5347 // `eqseq(out, {10, 0})`). 5.4 and earlier have no such pseudo.
5348 if fr.closure.proto.has_vararg_table_pseudo {
5349 raw + 1
5350 } else {
5351 raw
5352 }
5353 })
5354 .unwrap_or(1);
5355 // PUC 5.1 `luaD_poscall`: fire one extra "tail return" hook event
5356 // per tail call that collapsed into this activation, *after* its
5357 // own "return". `tailcalls` tracks that count exactly (PUC
5358 // `ci->u.l.tailcalls`). 5.2+ retired LUA_HOOKTAILRET, so the
5359 // "return" hook fires once even when the activation absorbed
5360 // multiple tail calls — only `istailcall` on getinfo surfaces the
5361 // collapse. 5.1 db.lua :366 pins the event ordering.
5362 let tailcalls = if self.version <= LuaVersion::Lua51 {
5363 self.frames
5364 .last()
5365 .and_then(|f| f.lua())
5366 .map(|f| f.tailcalls)
5367 .unwrap_or(0)
5368 } else {
5369 0
5370 };
5371 self.hook_return(from_native, ftransfer, nret)?;
5372 for _ in 0..tailcalls {
5373 self.hook_tail_return()?;
5374 }
5375 let CallFrame::Lua(fr) =
5376 frames_pop_sync(&mut self.frames, &mut self.frames_top).expect("no frame")
5377 else {
5378 unreachable!("returning from a non-Lua frame")
5379 };
5380 for i in 0..nret {
5381 self.stack[(fr.func_slot + i) as usize] = self.stack[(abs_a + i) as usize];
5382 }
5383 if self.frames.len() < entry_depth {
5384 self.top = fr.func_slot + nret;
5385 return Ok(Some(self.take_results(fr.func_slot)));
5386 } else if matches!(self.frames.last(), Some(CallFrame::Cont(_))) {
5387 self.top = fr.func_slot + nret;
5388 } else {
5389 self.finish_results(fr.func_slot, nret, fr.nresults);
5390 }
5391 Ok(None)
5392 }
5393
5394 #[doc(hidden)]
5395 pub fn upval_get(&self, cl: Gc<LuaClosure>, idx: u32) -> Value {
5396 match cl.upvals()[idx as usize].state() {
5397 UpvalState::Open { slot, thread } => self.read_slot(slot, thread),
5398 UpvalState::Closed(v) => v,
5399 }
5400 }
5401
5402 fn upval_set(&mut self, cl: Gc<LuaClosure>, idx: u32, v: Value) {
5403 let uv = cl.upvals()[idx as usize];
5404 match uv.state() {
5405 UpvalState::Open { slot, thread } => self.write_slot(slot, thread, v),
5406 UpvalState::Closed(_) => {
5407 // 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).
5408 unsafe { uv.as_mut() }.set_closed(v);
5409 // forward barrier: a closed upvalue is single-slot, so the
5410 // forward variant is cheaper than barrier_back (PUC uses
5411 // `luaC_barrier_` for upvalues; `luaC_barrierback_` for
5412 // tables / threads).
5413 self.heap
5414 .barrier_forward(uv.as_ptr() as *mut crate::runtime::heap::GcHeader, v);
5415 }
5416 }
5417 }
5418
5419 // ---- register / error helpers ----
5420
5421 #[inline(always)]
5422 fn r(&self, base: u32, i: u32) -> Value {
5423 // SAFETY: the compiler reserves `proto.max_stack` slots above `base`
5424 // at frame entry (`push_frame` sizes the stack up to base + max_stack),
5425 // and every bytecode-generated reference falls within `[0, max_stack)`.
5426 // PUC's vmfetch uses raw `R(A)` (`s2v(L->base + A)`) for the same
5427 // reason. The bounds check would re-validate this invariant on every
5428 // op — the dispatch hot path can't afford it.
5429 // 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).
5430 unsafe { *self.stack.get_unchecked((base + i) as usize) }
5431 }
5432
5433 #[inline(always)]
5434 fn set_r(&mut self, base: u32, i: u32, v: Value) {
5435 // SAFETY: see `r` — `base + i < base + max_stack <= stack.len()` by
5436 // frame-entry contract.
5437 unsafe {
5438 *self.stack.get_unchecked_mut((base + i) as usize) = v;
5439 }
5440 }
5441
5442 #[doc(hidden)]
5443 pub fn rt_err(&mut self, msg: &str) -> LuaError {
5444 let text = match self.position_prefix() {
5445 Some(p) => format!("{p}{msg}"),
5446 None => msg.to_string(),
5447 };
5448 LuaError(Value::Str(self.heap.intern(text.as_bytes())))
5449 }
5450
5451 /// Error without the `chunk:line:` position prefix. PUC's
5452 /// `resume_error` (ldo.c) pushes its message as a bare literal,
5453 /// so `cannot resume dead coroutine` etc. must not be prefixed.
5454 pub(crate) fn plain_err(&mut self, msg: &str) -> LuaError {
5455 LuaError(Value::Str(self.heap.intern(msg.as_bytes())))
5456 }
5457
5458 /// A string a library built from pieces of any size: one longer than a
5459 /// string can hold raises, as the concatenation operator does.
5460 pub(crate) fn built_str(&mut self, bytes: &[u8]) -> Result<Value, LuaError> {
5461 if bytes.len() > crate::runtime::string::MAX_LEN {
5462 return Err(self.rt_err("string length overflow"));
5463 }
5464 Ok(Value::Str(self.heap.intern(bytes)))
5465 }
5466
5467 pub(crate) fn type_err(&mut self, what: &str, v: Value) -> LuaError {
5468 let extra = self.subject_varinfo(v);
5469 let tn = self.obj_typename(v);
5470 let msg = self.compose_type_err(what, &tn, &extra);
5471 self.runerror(&msg)
5472 }
5473
5474 /// Assemble a `luaG_typeerror` / `luaG_callerror` message in the dialect's
5475 /// word order.
5476 ///
5477 /// PUC ≤5.2 names the operand first — `attempt to call field 'f' (a nil
5478 /// value)`. 5.3 flipped it to type-first — `attempt to call a nil value
5479 /// (field 'f')`. luna emitted the 5.3+ form on every dialect, so every
5480 /// such error was worded wrong under 5.1/5.2.
5481 ///
5482 /// Two shapes carry no operand name on ≤5.2 and must collapse to the bare
5483 /// message: an absent varinfo (identical across dialects), and a
5484 /// metamethod target — ≤5.2's `luaG_typeerror` only names locals, globals,
5485 /// fields, upvalues and methods, so `(metamethod 'add')` has no ≤5.2
5486 /// counterpart and is dropped rather than reworded. All four shapes were
5487 /// measured against stock 5.1.5 / 5.2.4 / 5.5.1 before this was written.
5488 fn compose_type_err(&self, what: &str, tn: &str, extra: &str) -> String {
5489 if self.version() > crate::version::LuaVersion::Lua52 {
5490 return format!("attempt to {what} a {tn} value{extra}");
5491 }
5492 // `extra` is "" or " (kind 'name')" — unwrap to "kind 'name'".
5493 let inner = extra
5494 .trim_start()
5495 .trim_start_matches('(')
5496 .trim_end_matches(')');
5497 if inner.is_empty() || inner.starts_with("metamethod") {
5498 format!("attempt to {what} a {tn} value")
5499 } else {
5500 format!("attempt to {what} {inner} (a {tn} value)")
5501 }
5502 }
5503
5504 /// Name the offending operand of the current instruction (PUC varinfo) for
5505 /// a type error, e.g. " (global 'x')". The faulting value `bad` is matched
5506 /// to the instruction's subject register(s); a native-raised error whose
5507 /// current instruction doesn't hold `bad` simply yields "".
5508 fn subject_varinfo(&self, bad: Value) -> String {
5509 use crate::vm::isa::Op;
5510 // PUC `varinfo` names a variable only for a Lua activation
5511 if self.native_on_top() {
5512 return String::new();
5513 }
5514 let Some(f) = self.frames.last().and_then(CallFrame::lua) else {
5515 return String::new();
5516 };
5517 let proto = f.closure.proto;
5518 let p: &crate::runtime::Proto = &proto;
5519 let pc = f.pc as usize;
5520 if pc == 0 || pc > p.code.len() {
5521 return String::new();
5522 }
5523 let instr = p.code[pc - 1];
5524 let mut cands: Vec<u32> = Vec::new();
5525 match instr.op() {
5526 // indexed reads / length / method: the table/object is in B
5527 Op::GetField | Op::GetI | Op::GetTable | Op::SelfOp | Op::Len => {
5528 cands.push(instr.b());
5529 }
5530 // indexed writes / calls: the table/function is in A
5531 Op::SetField | Op::SetI | Op::SetTable | Op::Call | Op::TailCall => {
5532 cands.push(instr.a());
5533 }
5534 // arithmetic/bitwise: a register operand (B, and C unless constant)
5535 Op::Add
5536 | Op::Sub
5537 | Op::Mul
5538 | Op::Div
5539 | Op::Mod
5540 | Op::Pow
5541 | Op::IDiv
5542 | Op::BAnd
5543 | Op::BOr
5544 | Op::BXor
5545 | Op::Shl
5546 | Op::Shr => {
5547 cands.push(instr.b());
5548 if !instr.k() {
5549 cands.push(instr.c());
5550 }
5551 }
5552 Op::Unm | Op::BNot => cands.push(instr.b()),
5553 // indexing an upvalue table (`_ENV` for a global): PUC
5554 // `getupvalname` finds the value among the closure's upvalues
5555 Op::GetTabUp | Op::SetTabUp => {
5556 let u = if instr.op() == Op::GetTabUp {
5557 instr.b()
5558 } else {
5559 instr.a()
5560 };
5561 if self.upval_get(f.closure, u).raw_eq(bad)
5562 && let Some(d) = p.upvals.get(u as usize)
5563 {
5564 return format!(" (upvalue '{}')", d.name);
5565 }
5566 }
5567 Op::Concat => {
5568 let a = instr.a();
5569 for r in a..a + instr.b() {
5570 cands.push(r);
5571 }
5572 }
5573 _ => {}
5574 }
5575 // Up to 5.3 a binary operator takes a constant operand straight
5576 // from the constant table (RK), where `varinfo` cannot see it, so a
5577 // string constant is not named there; unary operators load it into
5578 // a register first and do name it.
5579 let rk_operands = self.version <= LuaVersion::Lua53
5580 && matches!(
5581 instr.op(),
5582 Op::Add
5583 | Op::Sub
5584 | Op::Mul
5585 | Op::Div
5586 | Op::Mod
5587 | Op::Pow
5588 | Op::IDiv
5589 | Op::BAnd
5590 | Op::BOr
5591 | Op::BXor
5592 | Op::Shl
5593 | Op::Shr
5594 );
5595 for reg in cands {
5596 if self.r(f.base, reg).raw_eq(bad) {
5597 return match crate::vm::objname::getobjname_in(p, pc - 1, reg, self.version) {
5598 Some(("constant", _)) if rk_operands => String::new(),
5599 Some((kind, name)) => format!(" ({kind} '{name}')"),
5600 None => String::new(),
5601 };
5602 }
5603 }
5604 String::new()
5605 }
5606
5607 /// "attempt to call a X value", enriched (PUC luaG_callerror) with a name
5608 /// for the call target: "(global 'f')" for a direct call, or "(metamethod
5609 /// 'add')" when the call is a metamethod dispatched by the current opcode.
5610 fn call_err(&mut self, v: Value) -> LuaError {
5611 let extra = self.call_target_varinfo(v);
5612 let tn = self.obj_typename(v);
5613 let msg = self.compose_type_err("call", &tn, &extra);
5614 self.runerror(&msg)
5615 }
5616
5617 /// Name the offending call target. A metamethod dispatch pushes a `Cont`
5618 /// frame before the call, so the opcode that triggered it lives in the
5619 /// nearest *Lua* frame — read that instruction: OP_CALL names the function
5620 /// register, any metamethod-bearing opcode yields "(metamethod 'event')".
5621 fn call_target_varinfo(&self, bad: Value) -> String {
5622 use crate::vm::isa::Op;
5623 if self.native_on_top() {
5624 return String::new();
5625 }
5626 let Some(f) = self.frames.iter().rev().find_map(CallFrame::lua) else {
5627 return String::new();
5628 };
5629 let proto = f.closure.proto;
5630 let p: &crate::runtime::Proto = &proto;
5631 let pc = f.pc as usize;
5632 if pc == 0 || pc > p.code.len() {
5633 return String::new();
5634 }
5635 let instr = p.code[pc - 1];
5636 match instr.source_op() {
5637 Op::Call | Op::TailCall => {
5638 let reg = instr.a();
5639 if self.r(f.base, reg).raw_eq(bad) {
5640 match crate::vm::objname::getobjname_in(p, pc - 1, reg, self.version) {
5641 Some((kind, name)) => format!(" ({kind} '{name}')"),
5642 None => String::new(),
5643 }
5644 } else {
5645 String::new()
5646 }
5647 }
5648 // 5.4 `funcnamefromcode` names the generic-for iterator call
5649 // (5.3 had the entry but raised through plain `luaG_typeerror`)
5650 Op::TForCall if self.version >= LuaVersion::Lua54 => {
5651 " (for iterator 'for iterator')".to_string()
5652 }
5653 // 5.4 `funcnamefromcall` names the metamethod; up to 5.3 the
5654 // call raised through `luaG_typeerror`, whose `varinfo` does not
5655 op if self.version >= LuaVersion::Lua54 => match mm_event_name(op) {
5656 Some(ev) => format!(" (metamethod '{ev}')"),
5657 None => String::new(),
5658 },
5659 _ => String::new(),
5660 }
5661 }
5662
5663 /// "number has no integer representation", enriched (PUC luaG_tointerror)
5664 /// with a "(field 'x')"-style suffix naming the offending operand of the
5665 /// current arithmetic instruction when it can be recovered from bytecode.
5666 fn no_int_rep_err(&mut self) -> LuaError {
5667 let extra = self.bad_operand_varinfo();
5668 self.runerror(&format!("number{extra} has no integer representation"))
5669 }
5670
5671 /// Inspect the current frame's faulting instruction: find the register
5672 /// operand holding a float with no integer representation and name it.
5673 fn bad_operand_varinfo(&self) -> String {
5674 if self.native_on_top() {
5675 return String::new();
5676 }
5677 let Some(f) = self.frames.last().and_then(CallFrame::lua) else {
5678 return String::new();
5679 };
5680 let proto = f.closure.proto;
5681 let p: &crate::runtime::Proto = &proto;
5682 let pc = f.pc as usize;
5683 if pc == 0 || pc > p.code.len() {
5684 return String::new();
5685 }
5686 let instr = p.code[pc - 1];
5687 let mut regs = vec![instr.b()];
5688 if !instr.k() {
5689 regs.push(instr.c());
5690 }
5691 let no_int = |n: Option<Num>| matches!(n, Some(Num::Float(x)) if crate::runtime::value::f2i_exact(x).is_none());
5692 for reg in regs {
5693 let v = self.r(f.base, reg);
5694 // before 5.4 a numeric string is converted first, so "2.5" is
5695 // the operand without an integer value
5696 let n = self.arith_operand()(v);
5697 if no_int(n) {
5698 return match crate::vm::objname::getobjname_in(p, pc - 1, reg, self.version) {
5699 Some((kind, name)) => format!(" ({kind} '{name}')"),
5700 None => String::new(),
5701 };
5702 }
5703 }
5704 String::new()
5705 }
5706
5707 /// Position prefix of the currently executing Lua frame. PUC `luaL_error`
5708 /// calls `luaL_where(L, 1)` which reads `L->ci->previous`. When the prior
5709 /// frame is a C function (e.g. a pcall Cont parked above `require`'s
5710 /// native call), PUC pushes no prefix — match that by looking only at the
5711 /// topmost frame directly and bailing if it is anything but a Lua frame.
5712 pub(crate) fn position_prefix(&self) -> Option<String> {
5713 let f = match self.frames.last()? {
5714 CallFrame::Lua(f) => f,
5715 // a native metamethod runs above the Meta continuation of the
5716 // instruction that triggered it: that Lua function is its caller
5717 CallFrame::Cont(NativeCont {
5718 kind: ContKind::Meta(_),
5719 ..
5720 }) => self.frames.iter().rev().nth(1)?.lua()?,
5721 CallFrame::Cont(_) => return None,
5722 };
5723 let proto = f.closure.proto;
5724 // a stripped chunk: no source in luna's own format, no line info in
5725 // PUC's (whose loader names the missing source "=?")
5726 if proto.source.as_bytes().is_empty() || proto.lines.is_empty() {
5727 return Some(self.stripped_prefix());
5728 }
5729 let line = proto.lines[(f.pc as usize).saturating_sub(1).min(proto.lines.len() - 1)];
5730 // 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).
5731 let raw = unsafe { crate::runtime::string::bytes_of(proto.source.as_ptr()) };
5732 let display = crate::vm::lib_debug::chunk_id(self.version, raw);
5733 let src = String::from_utf8_lossy(&display).into_owned();
5734 Some(format!("{src}:{line}: "))
5735 }
5736
5737 /// PUC `luaG_addinfo` prefix for a stripped chunk. 5.5 substitutes "=?"
5738 /// for the source and renders the line as "?" (so the prefix reads
5739 /// `?:?: `). 5.4 and below leave the source NULL ("?") and use the raw
5740 /// `getfuncline = -1`, so the prefix reads `?:-1: ` (5.4 errors.lua :282
5741 /// matches `^%?:%-1:`).
5742 fn stripped_prefix(&self) -> String {
5743 if self.version >= crate::version::LuaVersion::Lua55 {
5744 "?:?: ".to_string()
5745 } else {
5746 "?:-1: ".to_string()
5747 }
5748 }
5749
5750 /// PUC `luaL_where(L, level)`: `"short_src:line: "` for the function at
5751 /// `level` (0 = the running native), or `None` when that level does not
5752 /// exist or has no line information (a C function, a stripped chunk).
5753 pub(crate) fn position_prefix_at_level(&self, level: i64) -> Option<String> {
5754 let ts = self.thread_stack(None);
5755 let i = usize::try_from(level).ok()?;
5756 if i >= ts.levels.len() {
5757 return None;
5758 }
5759 let line = ts.currentline(i);
5760 if line <= 0 {
5761 return None;
5762 }
5763 let DbgKind::Lua(fi) = ts.levels[i] else {
5764 return None;
5765 };
5766 let ar = self.closure_ar(ts.lua(fi).closure);
5767 Some(format!(
5768 "{}:{line}: ",
5769 String::from_utf8_lossy(&ar.short_src)
5770 ))
5771 }
5772
5773 // ---- the interpreter ----
5774
5775 /// Run from the current top frame down to (but not past) `entry_depth`
5776 /// frames. Coroutine driving passes `entry_depth = 1` so the whole thread
5777 /// runs to completion or a yield.
5778 /// Resume the dispatcher from the saved
5779 /// `entry_depth` (captured pre-yield by `drive_one`). Called by
5780 /// `EvalFuture::poll` on every poll after the first to walk the
5781 /// existing call frames until the next `BudgetExhausted` or
5782 /// terminal `Ok`/`Err`. Not a public-API surface; the
5783 /// embedder reaches it through `Vm::eval_async`.
5784 pub(crate) fn exec_with_async(&mut self, entry_depth: usize) -> Result<Vec<Value>, LuaError> {
5785 self.exec_with(entry_depth)
5786 }
5787
5788 fn exec_with(&mut self, entry_depth: usize) -> Result<Vec<Value>, LuaError> {
5789 loop {
5790 let r = self.run(entry_depth);
5791 if r.is_err()
5792 && (self.yielding.is_some()
5793 || self.terminating.is_some()
5794 || self.host_yield_pending
5795 || self.pending_async_native_fut.is_some())
5796 {
5797 // a `coroutine.yield` is in flight: keep the frames intact (they
5798 // are the suspended coroutine's saved state) and propagate to
5799 // resume. A self-close termination propagates the same way, so a
5800 // protecting pcall on the way out cannot catch (unwind) it.
5801 // `host_yield_pending` is the async-mode
5802 // analogue: the sentinel must reach `drive_one` without
5803 // a protecting `pcall` swallowing it.
5804 return r;
5805 }
5806 match r {
5807 Ok(vals) => return Ok(vals),
5808 // unwind toward `entry_depth`. A protecting pcall/xpcall
5809 // continuation caught along the way turns the error into
5810 // `false, msg` and the loop resumes running its caller; an
5811 // uncaught error propagates out.
5812 Err(e) => match self.unwind(e.0, entry_depth) {
5813 Unwound::Caught => continue,
5814 Unwound::CaughtReturn(vals) => return Ok(vals),
5815 Unwound::Propagated(err) => return Err(err),
5816 },
5817 }
5818 }
5819 }
5820
5821 /// Unwind the call stack from the error point toward `entry_depth`, running
5822 /// `__close` handlers on each Lua frame. Stops at the first pcall/xpcall
5823 /// continuation frame at/above `entry_depth` (the error is *caught*: its
5824 /// slot receives `false, msg`); if none is reached, the error propagates.
5825 fn unwind(&mut self, mut err: Value, entry_depth: usize) -> Unwound {
5826 // The protected call runs in-place among the caller frames' registers,
5827 // so truncating the failed frames here cuts into caller windows below
5828 // the catcher. Snapshot the live length: at the error point the stack
5829 // already spans every surviving frame's window, so restoring it after a
5830 // catch reinstates them all (the reclaimed slots above are dead temps).
5831 // PUC handles overflow recovery via a separate EXTRA_STACK reserve;
5832 // we instead clamp the restore to the catcher's caller window when the
5833 // error point was at the stack limit (cause: the next `call_value_impl`
5834 // picks `func_slot = stack.len()` which would otherwise re-overflow).
5835 let saved_len = self.stack.len();
5836 err = self.raise_to_handler(err);
5837 // An error that no protected call inside the running coroutine will
5838 // catch kills it without unwinding: PUC's `lua_resume` leaves the
5839 // dead thread's stack as it was, so its pending to-be-closed
5840 // variables run only when it is closed (`coroutine.close`, or
5841 // `coroutine.wrap` closing it before re-raising). Scoped to the
5842 // coroutine's own run (`entry_depth == 1`); a run nested under a
5843 // native unwinds as before.
5844 if entry_depth == 1
5845 && self.version >= LuaVersion::Lua54
5846 && self
5847 .current
5848 .is_some_and(|c| c.status == crate::runtime::CoroStatus::Running)
5849 && !self.frames.iter().any(|f| {
5850 matches!(
5851 f,
5852 CallFrame::Cont(NativeCont {
5853 kind: ContKind::Pcall | ContKind::Xpcall { .. } | ContKind::Close(_),
5854 ..
5855 })
5856 )
5857 })
5858 {
5859 while self.frames.len() >= entry_depth {
5860 frames_pop_sync(&mut self.frames, &mut self.frames_top);
5861 }
5862 return Unwound::Propagated(LuaError(err));
5863 }
5864 while self.frames.len() >= entry_depth {
5865 match *self.frames.last().expect("frame") {
5866 // a yieldable-metamethod continuation does not catch: discard the
5867 // abandoned instruction and keep unwinding (PUC drops the partial
5868 // op on error).
5869 CallFrame::Cont(NativeCont {
5870 kind: ContKind::Meta(mc),
5871 func_slot,
5872 ..
5873 }) => {
5874 frames_pop_sync(&mut self.frames, &mut self.frames_top);
5875 self.stack.truncate(func_slot as usize);
5876 self.top = mc.saved_top.min(func_slot);
5877 self.tbc.retain(|&s| s < func_slot);
5878 }
5879 // a __pairs continuation does not catch either: an error inside
5880 // the metamethod propagates past `pairs`.
5881 CallFrame::Cont(NativeCont {
5882 kind: ContKind::Pairs,
5883 func_slot,
5884 ..
5885 }) => {
5886 frames_pop_sync(&mut self.frames, &mut self.frames_top);
5887 self.stack.truncate(func_slot as usize);
5888 self.top = func_slot;
5889 self.tbc.retain(|&s| s < func_slot);
5890 }
5891 // a __close continuation does not catch: drop the half-run
5892 // handler's window, then continue the close yieldably with
5893 // the new error threaded as `pending`. Preserve `cc.after`
5894 // verbatim — `Return`/`Block` originating from an aborting
5895 // OP_Return/OP_Close will be short-circuited by
5896 // `finish_close_after` (pending propagates as Err); a
5897 // `ResumeUnwind` originated by our own Lua-frame handler
5898 // must keep its deferred frame-pop semantics so that frame
5899 // is not orphaned. If a fresh handler yields, `drive_close`
5900 // pushes another `Cont::Close` and we return `Caught` so
5901 // `exec_with` re-enters the run loop.
5902 CallFrame::Cont(NativeCont {
5903 kind: ContKind::Close(cc),
5904 func_slot,
5905 ..
5906 }) => {
5907 frames_pop_sync(&mut self.frames, &mut self.frames_top);
5908 self.stack.truncate(func_slot as usize);
5909 self.top = func_slot;
5910 self.tbc.retain(|&s| s < func_slot);
5911 match self.drive_close(cc.from, Some(err), cc.after, entry_depth) {
5912 Ok(Some(_)) => {
5913 unreachable!(
5914 "Block / Return / ResumeUnwind never return host values mid-unwind"
5915 )
5916 }
5917 Ok(None) => return Unwound::Caught,
5918 Err(e) => {
5919 // the drained close re-raises `err`; only an
5920 // error a handler raised is new
5921 if !e.0.raw_eq(err) {
5922 err = self.raise_to_handler(e.0);
5923 }
5924 continue;
5925 }
5926 }
5927 }
5928 CallFrame::Cont(nc) => {
5929 frames_pop_sync(&mut self.frames, &mut self.frames_top);
5930 self.pcall_depth -= 1;
5931 let result = match nc.kind {
5932 ContKind::Pcall => {
5933 self.msgh_applied = None;
5934 err
5935 }
5936 // the handler ran where the error was raised (see
5937 // `raise_to_handler`); one raised past the handler's
5938 // reach (by the unwind itself) meets it here
5939 ContKind::Xpcall { handler } => {
5940 if self.msgh_applied.take().is_some_and(|v| v.raw_eq(err)) {
5941 err
5942 } else {
5943 self.call_msgh(handler, err)
5944 }
5945 }
5946 ContKind::Meta(_) | ContKind::Pairs | ContKind::Close(_) => {
5947 unreachable!("Meta/Pairs/Close cont handled above")
5948 }
5949 };
5950 // PUC 5.5 `luaG_errormsg` substitutes "<no error object>"
5951 // for nil AFTER the message handler ran (ldebug.c:849) —
5952 // so it applies to the pcall-caught object and to an
5953 // xpcall HANDLER'S return value, while the handler itself
5954 // (and a top-level propagation into the host, whose
5955 // `error_display` plays msghandler) still sees the raw
5956 // nil. 5.4- keep nil everywhere (errors.lua :49 asserts
5957 // `doit("error()") == nil`).
5958 let result = if matches!(result, Value::Nil)
5959 && self.version >= crate::version::LuaVersion::Lua55
5960 {
5961 Value::Str(self.heap.intern(b"<no error object>"))
5962 } else {
5963 result
5964 };
5965 // the error has been caught (pcall/xpcall): the captured
5966 // traceback was for that error and is no longer in flight.
5967 self.error_traceback = None;
5968 let fs = nc.func_slot as usize;
5969 if self.stack.len() < fs + 2 {
5970 self.stack.resize(fs + 2, Value::Nil);
5971 }
5972 self.stack[fs] = Value::Bool(false);
5973 self.stack[fs + 1] = result;
5974 self.top = nc.func_slot + 2;
5975 self.tbc.retain(|&s| s < nc.func_slot);
5976 if self.frames.len() < entry_depth {
5977 return Unwound::CaughtReturn(self.take_results(nc.func_slot));
5978 }
5979 self.finish_results(nc.func_slot, 2, nc.nresults);
5980 // reinstate the caller windows the unwind truncated into,
5981 // clamped to the catcher's caller window + a `MIN_STACK`
5982 // reserve. The clamp is a no-op for normal pcall catches
5983 // (saved_len lies within the caller's max_stack window),
5984 // and prevents the stack from staying near `MAX_LUA_STACK`
5985 // after an overflow-recovery catch — which would make the
5986 // next `call_value_impl` (e.g. a `__close` in the catcher's
5987 // errorh, locals.lua:659) pick `func_slot = stack.len()`
5988 // above the limit and re-overflow.
5989 // Restore the caller's full register window: opcodes
5990 // index it directly. The cap covers caller's base +
5991 // `max_stack` + a small reserve. We always resize to
5992 // exactly this window — previously this clamped
5993 // `saved_len` from above to prevent staying near
5994 // `MAX_LUA_STACK` after an overflow-recovery catch, and
5995 // a yieldable-unwind re-entry adds the dual case where
5996 // `saved_len` is *below* the window (a prior
5997 // `ResumeUnwind` truncated). Using the window directly
5998 // covers both.
5999 let restore = self
6000 .frames
6001 .iter()
6002 .rev()
6003 .find_map(CallFrame::lua)
6004 .map(|c| (c.base + c.closure.proto.max_stack as u32) as usize + 256)
6005 .unwrap_or(saved_len);
6006 if self.stack.len() < restore {
6007 self.stack.resize(restore, Value::Nil);
6008 } else if self.stack.len() > restore {
6009 self.stack.truncate(restore);
6010 }
6011 // Clear slots vacated by the popped
6012 // frames the unwind walked over. finish_results
6013 // above clears `[nc.func_slot + nresults ..
6014 // nc.func_slot + 2)`, which only covers the
6015 // pcall's own result region — the unwind-popped
6016 // frames' locals in `[nc.func_slot + 2 .. restore)`
6017 // are still in place with whatever Gc-bearing
6018 // Values they last held. Without this clear, a
6019 // later GC marks the stale pointers (same hazard as
6020 // the Op::Return finish_results path). PUC's `luaD_pcall` similarly truncates
6021 // L->top to the catcher's level — luna's
6022 // truncate above resizes the Vec but doesn't
6023 // touch slots [func_slot+2..restore) that were
6024 // already present.
6025 let clear_lo = (nc.func_slot as usize + 2).min(self.stack.len());
6026 let clear_hi = restore.min(self.stack.len());
6027 if clear_lo < clear_hi {
6028 for slot in &mut self.stack[clear_lo..clear_hi] {
6029 *slot = Value::Nil;
6030 }
6031 }
6032 return Unwound::Caught;
6033 }
6034 CallFrame::Lua(f) => {
6035 // Yieldable error-unwind close, PUC luaG_errormsg shape:
6036 // (1) pop the Lua frame immediately so each `__close`
6037 // handler runs at the C boundary above — `debug.getinfo`
6038 // sees the next outer Lua frame's call site (typically
6039 // `pcall`), not this aborting function (locals.lua:480).
6040 // (2) drive the close yieldably with
6041 // `AfterClose::ResumeUnwind { func_slot, err }`; on drain
6042 // it truncates to `func_slot` and re-raises (letting a
6043 // handler-raised error win over `err`). If a handler
6044 // yields, `drive_close` pushes `Cont::Close` and we
6045 // return `Caught` so `exec_with` re-enters the run loop;
6046 // a synchronous drain returns Err exactly as the old
6047 // path did.
6048 frames_pop_sync(&mut self.frames, &mut self.frames_top);
6049 let after = AfterClose::ResumeUnwind {
6050 func_slot: f.func_slot,
6051 err,
6052 };
6053 match self.begin_close(f.base, Some(err), after, entry_depth) {
6054 Ok(Some(_)) => {
6055 unreachable!("ResumeUnwind never returns host values")
6056 }
6057 Ok(None) => return Unwound::Caught,
6058 Err(e) => {
6059 // the drained close re-raises `err`; only an
6060 // error a handler raised is new
6061 if !e.0.raw_eq(err) {
6062 err = self.raise_to_handler(e.0);
6063 }
6064 continue;
6065 }
6066 }
6067 }
6068 }
6069 }
6070 Unwound::Propagated(LuaError(err))
6071 }
6072
6073 fn run(&mut self, entry_depth: usize) -> Result<Vec<Value>, LuaError> {
6074 loop {
6075 // Fast-path slow-check gate: most embedders run with both
6076 // `instr_budget` and `mem_cap` as None, so a single combined
6077 // is_some test lets the hot loop skip both branches with one
6078 // load + branch instead of two.
6079 if self.instr_budget.is_some() || self.heap.mem_cap.is_some() {
6080 if let Some(b) = self.instr_budget.as_mut() {
6081 *b -= 1;
6082 if *b <= 0 {
6083 self.instr_budget = None;
6084 // Async-mode cooperative
6085 // yield. Set a sentinel flag so `exec_with`
6086 // propagates the Err without `unwind` running
6087 // (mirroring the `yielding.is_some()` path),
6088 // and `call_value_impl` preserves the call
6089 // frames for the next `poll`. Translation back
6090 // to `DispatchOutcome::BudgetExhausted` happens
6091 // in `drive_one`. The Err value itself is
6092 // `Value::Nil` — a pure sentinel, never seen by
6093 // user code.
6094 if self.async_mode {
6095 self.host_yield_pending = true;
6096 return Err(LuaError(Value::Nil));
6097 }
6098 // Classify the trip so embedders can
6099 // distinguish budget exhaustion from a
6100 // generic Runtime error and retry / give up
6101 // accordingly.
6102 self.last_error_kind = crate::vm::error::LuaErrorKind::InstrBudget;
6103 let s = Value::Str(self.heap.intern(b"instruction budget exceeded"));
6104 return Err(LuaError(s));
6105 }
6106 }
6107 if let Some(cap) = self.heap.mem_cap
6108 && self.heap.bytes() > cap
6109 {
6110 // First try a full collect — embedders set tight caps
6111 // and the overshoot may be reclaimable (closures kept
6112 // by short-lived frames, intermediate strings). Only
6113 // disarm + raise if the cap is still breached after
6114 // collection. PUC's `LUA_GCEMERGENCY` path matches.
6115 //
6116 // Root up to the deepest Lua frame's
6117 // `base + max_stack` window rather than the entire
6118 // `self.stack.len()`
6119 // (covers register operands the current opcode
6120 // might reference). The cap fires during table
6121 // mutation in a tight `a[i] = i` loop where `a`
6122 // lives at a frame-register slot past `self.top`
6123 // (OP_NEWINDEX doesn't advance top); the deepest
6124 // frame's max_stack window provably covers it
6125 // since `a` is a register of the executing proto.
6126 //
6127 // Still over-roots caller frames' dead regs
6128 // (slots between caller.base and the callee
6129 // func_slot are live; slots past callee
6130 // func_slot in caller's frame are dead until
6131 // caller resumes). For fire-once cap path this
6132 // residual over-root is acceptable; there is no
6133 // full per-frame walk because a strong/weak pass
6134 // split is semantically impossible — the weak pass
6135 // depends on strong-pass marks.
6136 let cap_root_top = self
6137 .frames
6138 .iter()
6139 .rev()
6140 .find_map(CallFrame::lua)
6141 .map(|f| f.base + f.closure.proto.max_stack as u32)
6142 .unwrap_or(self.top);
6143 self.gc_top = cap_root_top.max(self.top);
6144 self.collect_garbage();
6145 if self.heap.bytes() > cap {
6146 self.heap.mem_cap = None;
6147 let s = Value::Str(self.heap.intern(b"memory cap exceeded"));
6148 return Err(LuaError(s));
6149 }
6150 }
6151 }
6152 // Single combined frame fetch: continuation arm OR Lua arm. Saves
6153 // a second `self.frames.last()` slice access vs the prior split
6154 // form (LLVM doesn't always CSE these across the cont branch).
6155 // A continuation frame on top means the call it protected just
6156 // delivered its results — wrap as `true, results…` and hand to
6157 // the pcall/xpcall caller. The error path is handled by `unwind`;
6158 // this branch is only reached on success/resume completion.
6159 // 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).
6160 let frame_peek = unsafe { self.frames.last().unwrap_unchecked() };
6161 if let &CallFrame::Cont(nc) = frame_peek {
6162 // a yieldable metamethod returned: complete the interrupted
6163 // instruction (PUC luaV_finishOp) and resume the running frame.
6164 if let ContKind::Meta(mc) = nc.kind {
6165 frames_pop_sync(&mut self.frames, &mut self.frames_top);
6166 let result = if self.top > nc.func_slot {
6167 self.stack[nc.func_slot as usize]
6168 } else {
6169 Value::Nil
6170 };
6171 self.stack.truncate(nc.func_slot as usize);
6172 self.top = mc.saved_top;
6173 self.finish_meta(mc.action, result)?;
6174 continue;
6175 }
6176 // a __close handler returned successfully: discard its
6177 // results, restore `top` to the slot the handler was called
6178 // at (the surrounding frame's register window above this slot
6179 // must stay alloc'd — never truncate the underlying stack),
6180 // then continue the close chain (next slot, or fire
6181 // AfterClose). When the close ends an entry activation,
6182 // drive_close hands the results up to exec_with directly.
6183 if let ContKind::Close(cc) = nc.kind {
6184 frames_pop_sync(&mut self.frames, &mut self.frames_top);
6185 self.top = nc.func_slot;
6186 if let Some(vals) =
6187 self.drive_close(cc.from, cc.pending, cc.after, entry_depth)?
6188 {
6189 return Ok(vals);
6190 }
6191 continue;
6192 }
6193 // __pairs returned: normalize its results to exactly the
6194 // dialect's count (iterator, state, control, and on 5.5 the
6195 // closing value) at pairs's slot, where the metamethod was
6196 // called, and hand them to pairs's caller.
6197 if let ContKind::Pairs = nc.kind {
6198 frames_pop_sync(&mut self.frames, &mut self.frames_top);
6199 let total = crate::vm::builtins::pairs_mm_results(self) as u32;
6200 let need = (nc.func_slot + total) as usize;
6201 if self.stack.len() < need {
6202 self.stack.resize(need, Value::Nil);
6203 }
6204 // the metamethod ran one slot above pairs's own
6205 let first = nc.func_slot + 1;
6206 let n = (self.top - first).min(total);
6207 for i in 0..n {
6208 self.stack[(nc.func_slot + i) as usize] = self.stack[(first + i) as usize];
6209 }
6210 for s in (nc.func_slot + n)..(nc.func_slot + total) {
6211 self.stack[s as usize] = Value::Nil;
6212 }
6213 self.top = nc.func_slot + total;
6214 if self.frames.len() < entry_depth {
6215 return Ok(self.take_results(nc.func_slot));
6216 }
6217 self.finish_results(nc.func_slot, total, nc.nresults);
6218 continue;
6219 }
6220 frames_pop_sync(&mut self.frames, &mut self.frames_top);
6221 self.pcall_depth -= 1;
6222 // f's results sit at nc.func_slot+1.. (f was called one slot
6223 // above the continuation), so writing `true` at the slot makes
6224 // `true, results…` already contiguous.
6225 let nret = self.top - (nc.func_slot + 1);
6226 self.stack[nc.func_slot as usize] = Value::Bool(true);
6227 let total = 1 + nret;
6228 self.top = nc.func_slot + total;
6229 if self.frames.len() < entry_depth {
6230 return Ok(self.take_results(nc.func_slot));
6231 }
6232 self.finish_results(nc.func_slot, total, nc.nresults);
6233 continue;
6234 }
6235 // GC runs only at the allocation safe points below (PUC's
6236 // `luaC_checkGC` sites), each with a precise `gc_top`; the loop head
6237 // no longer collects, so a stale full-window `gc_top` cannot leak in.
6238 //
6239 // Hot-path frame fetch: the Cont arm above continues the loop,
6240 // so reaching here means `frame_peek` is the Lua frame. Reuse it
6241 // rather than re-fetching `self.frames.last()`.
6242 let f = match frame_peek {
6243 CallFrame::Lua(f) => f,
6244 _ => unreachable!("Cont frame survived the dispatch loop head"),
6245 };
6246 let cl = f.closure;
6247 let base = f.base;
6248 let func_slot = f.func_slot;
6249 let n_varargs = f.n_varargs;
6250 let pc = f.pc;
6251 let oldpc = f.hook_oldpc;
6252
6253 // SAFETY: `pc` is bounded by the compiler against `proto.code.len()`
6254 // — every branch / call op only sets `pc` to a valid index, and
6255 // function entry initialises pc=0 with a non-empty body. PUC's
6256 // `vmfetch` uses the equivalent unchecked load.
6257 let inst = unsafe { *cl.proto.code.get_unchecked(pc as usize) };
6258
6259 // Trace recording append + close detection.
6260 // Gated on `trace_jit_enabled` + `active_trace.is_some()`
6261 // so default dispatch keeps a single not-taken branch.
6262 //
6263 // - At the head PC with a non-empty record, the trace has
6264 // looped back to its start: mark `closed = true` and
6265 // take the record for compile + cache.
6266 // - Otherwise, capture the op. If the record overflows
6267 // MAX_TRACE_LEN, abort by dropping it.
6268 if self.jit.trace_enabled
6269 && let Some(_rec) = self.jit.active_trace.as_mut()
6270 {
6271 // Depth tracking. The trace head's frame is
6272 // at index `recording_frame_base`; every Op::Call that
6273 // pushes a new frame bumps the live depth, every
6274 // Op::Return that pops one decrements it.
6275 //
6276 // **Three clean-close conditions**:
6277 // - `at_head`: cur_depth == 0 AND about-to-execute the
6278 // trace's head_pc on its head_proto (loop closed back
6279 // to start). Same for loop-triggered and call-triggered
6280 // traces, so a call-triggered trace does not close on
6281 // the first re-entry (that would leave fib's body at 7
6282 // depth=0 ops); it inlines up to MAX_INLINE_DEPTH
6283 // levels before any close.
6284 // - `returned_past_head`: trace head's frame is gone
6285 // (callee returned past it, or the call-trigger
6286 // started a recording inside a callee that has now
6287 // returned). Whatever ops were recorded form the
6288 // trace body; the lowerer treats the partial trace
6289 // the same as InlineAbort.
6290 // - `depth_cap_hit`: cur_depth > MAX_INLINE_DEPTH.
6291 // Recording any deeper would just bloat the IR; close
6292 // with the body we have. Lowerer's existing length
6293 // gate + InlineAbort path handles short bodies.
6294 let returned_past_head = self.frames.len() <= self.jit.recording_frame_base;
6295 let cur_depth = if returned_past_head {
6296 0
6297 } else {
6298 self.frames.len() - 1 - self.jit.recording_frame_base
6299 };
6300 let depth_cap_hit = cur_depth > crate::jit::trace::MAX_INLINE_DEPTH as usize;
6301 let rec = self.jit.active_trace.as_mut().expect("just checked Some");
6302 let at_head_loop = cur_depth == 0
6303 && !rec.ops.is_empty()
6304 && !returned_past_head
6305 && std::ptr::eq(cl.proto.as_ptr(), rec.head_proto.as_ptr())
6306 && pc == rec.head_pc;
6307 // Self-link cycle catch (mirrors LuaJIT's
6308 // `check_call_unroll` at `lj_record.c:1869`). Trips when:
6309 // 1. We're about to execute the head_pc on head_proto
6310 // at depth > 0 (we're re-entering the trace head
6311 // from inside an inlined recursion level — UpRec).
6312 // 2. The count of ancestor frames in the recording
6313 // window that share `head_proto` exceeds
6314 // [`RECUNROLL_THRESHOLD`] (default 2).
6315 // For fib(N): head_pc=0, head_proto=fib. After 2 inline
6316 // recursion levels are captured, the recorder enters
6317 // the 3rd nested fib frame, sees cur_depth=3 > 2, and
6318 // trips this catch — closing with `SelfRecKind::UpRec`.
6319 // The lowerer's `TraceEnd::SelfLink` tail emits the
6320 // bump-base + branch-to-self loop body.
6321 //
6322 // TailRec vs UpRec: LJ distinguishes via
6323 // `framedepth + retdepth == 0`. luna doesn't track
6324 // retdepth separately; cur_depth == 0 with a non-empty
6325 // call chain in tail position is rare (would require
6326 // explicit Lua TCO). We use cur_depth > 0 as the UpRec
6327 // condition (fib's case); cur_depth == 0 with positive
6328 // ancestor count would route to TailRec, but luna's
6329 // recorder doesn't currently produce that shape because
6330 // tail-call elision pops the caller frame and we'd
6331 // hit `at_head_loop` instead.
6332 let self_link_trip: Option<crate::jit::trace::SelfRecKind> = {
6333 if self.jit.self_link_enabled
6334 && !returned_past_head
6335 && std::ptr::eq(cl.proto.as_ptr(), rec.head_proto.as_ptr())
6336 && pc == rec.head_pc
6337 && cur_depth > 0
6338 {
6339 // Count ancestor frames sharing head_proto.
6340 // self.frames[recording_frame_base..] currently
6341 // includes the just-pushed frame at the top
6342 // (the one about to execute head_pc). Ancestors
6343 // = the slice excluding the top frame.
6344 let head_proto_ptr = rec.head_proto.as_ptr();
6345 let last_idx = self.frames.len() - 1;
6346 let mut count = 0usize;
6347 for i in self.jit.recording_frame_base..last_idx {
6348 if let CallFrame::Lua(f) = &self.frames[i]
6349 && std::ptr::eq(f.closure.proto.as_ptr(), head_proto_ptr)
6350 {
6351 count += 1;
6352 }
6353 }
6354 if count > crate::jit::trace::RECUNROLL_THRESHOLD {
6355 // cur_depth > 0 → UpRec (fib pattern).
6356 // cur_depth == 0 wouldn't reach this arm.
6357 Some(crate::jit::trace::SelfRecKind::UpRec)
6358 } else {
6359 None
6360 }
6361 } else {
6362 None
6363 }
6364 };
6365 if let Some(kind) = self_link_trip {
6366 // SelfLink relax for self-recursive patterns at frame
6367 // depth >= 2.
6368 //
6369 // Stamping `self_link_kind` unconditionally at the
6370 // head_pc re-entry would never dispatch: the
6371 // `downrec_close` marker can only fire from the
6372 // depth>0 Op::Return path (`rec.retfs` chain),
6373 // which never reaches the recorder for fib(28)-like
6374 // shapes that hit the SelfLink cycle catch BEFORE
6375 // any base-case Return — leaving `downrec_close`
6376 // None and routing the trace through the safe
6377 // `dispatchable=false` `"self-link-retf-r1"` path.
6378 //
6379 // So when the SelfLink trip fires AND
6380 // `cur_depth >= 2` (the count > RECUNROLL_THRESHOLD
6381 // gate already requires this — kept explicit as a
6382 // safety floor), route the close through `downrec_
6383 // close` INSTEAD of `self_link_kind`. The recorder
6384 // synthesises the close marker from the most
6385 // recent Op::Call at depth `cur_depth - 1`:
6386 // - `return_pc` = `call.pc + 1` (caller's resume
6387 // PC after the recursive call returns; mirror
6388 // of the `caller_pc` derivation at the
6389 // depth>0 Op::Return capture path below).
6390 // - `target_proto` = `call.proto` (caller's
6391 // proto; equals `rec.head_proto` for self-
6392 // recursion).
6393 // - `depth_delta` = `1` (the recorder always
6394 // unrolls one level; the Op::Return path uses
6395 // the same constant).
6396 //
6397 // The lowerer's `end_idx` picker routes through
6398 // `TraceEnd::DownRec` ahead of the `self_link_kind`
6399 // arm and emits the stitch-sentinel +
6400 // caller-pc-guard scaffold. A single-candidate guard
6401 // chain (this path produces 1 caller_pc candidate
6402 // because `rec.retfs` is empty) keeps
6403 // `dispatchable=false` + `"downrec-stitch-pending"`
6404 // label (the lowerer requires
6405 // `multi_way_candidate_count >= 2`). Net behaviour:
6406 // trace compiles under DownRec routing; interp runs
6407 // the recursion naturally.
6408 //
6409 // The `cur_depth >= 2` gate is automatically
6410 // satisfied by the count > RECUNROLL_THRESHOLD=2
6411 // trip condition (3 ancestor frames sharing
6412 // head_proto implies cur_depth >= 3), kept
6413 // explicit so a future RECUNROLL_THRESHOLD tweak
6414 // doesn't silently flip shallow-recursion
6415 // shapes (cur_depth == 1) onto the DownRec arm.
6416 //
6417 // The recorded body still uses depth-baked
6418 // op_offsets[] addressing, so this is routing
6419 // scaffolding and gives no speedup by itself.
6420 let _ = kind;
6421 let relaxed_to_downrec = cur_depth >= 2 && rec.downrec_close.is_none() && {
6422 let caller_depth_u8 = (cur_depth - 1) as u8;
6423 if let Some(call_op) = rec.ops.iter().rev().find(|r| {
6424 r.inline_depth == caller_depth_u8
6425 && matches!(r.inst.op(), crate::vm::isa::Op::Call)
6426 }) {
6427 rec.downrec_close = Some(crate::jit::trace::DownRecClose {
6428 return_pc: call_op.pc + 1,
6429 target_proto: call_op.proto,
6430 depth_delta: 1,
6431 });
6432 true
6433 } else {
6434 false
6435 }
6436 };
6437 if relaxed_to_downrec {
6438 // Close-cause taxonomy: tag the lift so
6439 // probes can tally the fire rate. Mirrors
6440 // the `"downrec-restart"` bump for the
6441 // depth>0 Op::Return path (different trip
6442 // origin, same downstream routing). The
6443 // existing `"self-link-retf-r1"` label still
6444 // fires for trips that DON'T relax (no
6445 // candidate Op::Call ancestor in rec.ops, or
6446 // cur_depth < 2) via the lowerer's
6447 // dispatch_off_reason mirror at the close
6448 // handler — kept as a regression safety net.
6449 self.jit
6450 .counters
6451 .bump_close_cause("selflink-yields-to-downrec");
6452 } else {
6453 rec.self_link_kind = Some(kind);
6454 }
6455 }
6456 let should_close =
6457 at_head_loop || returned_past_head || depth_cap_hit || self_link_trip.is_some();
6458 if should_close {
6459 // Long-trace bias: a call-triggered
6460 // recording that closed with a very short body
6461 // (fib base case: `Lt`/`Jmp`/`Return1` = 3 ops,
6462 // binary_trees `make(0)`: 4 ops) is pathological.
6463 // Compiling + caching it pins `Proto.traces` to a
6464 // trace that the length gate will refuse to
6465 // dispatch (per `MIN_DISPATCHABLE_TRUNC_BODY_FLOOR
6466 // = 40`), AND blocks the back-edge / longer-call
6467 // path from re-recording the same head_pc (the
6468 // dedup `already_cached` check below short-
6469 // circuits). The fix: discard the short call-
6470 // triggered recording WITHOUT caching, and bias
6471 // the proto's `call_hot_count` back to
6472 // `THRESHOLD - HOT_RETRY_WINDOW` so the next
6473 // sequence of calls retries the trigger at a
6474 // different (hopefully deeper) recursion point.
6475 //
6476 // Back-edge triggered traces are exempt — a
6477 // tight numeric-for loop's body is legitimately
6478 // 3 ops (`Add`, ForLoop) and DOES dispatch
6479 // usefully when re-entered many times.
6480 // Coverage heuristic to detect
6481 // pathologically partial call-triggered traces:
6482 // for self-recursive / branchy protos like
6483 // `fib` (~17 bytecode ops) or
6484 // `binary_trees.make` (~26 ops), the recorder
6485 // can fire at a BASE-case entry (`fib(0)` or
6486 // `make(0)`) producing a 3–4 op trace that
6487 // covers a tiny fraction of the proto's code.
6488 // That trace is doomed by the length gate
6489 // post-compile AND blocks any longer follow-up
6490 // (the dedup `already_cached` check below). The
6491 // fix: discard call-triggered closes where
6492 // `rec.ops.len() * 2 < head_proto.code.len()`
6493 // (less than half the proto's bytecode), so the
6494 // back-edge / longer call path can take over.
6495 //
6496 // Why coverage > raw length:protos with
6497 // intrinsically short bodies (closure
6498 // factories: `Closure + Return1` = 2 ops,
6499 // simple wrappers: `LoadI + Return1` = 2 ops)
6500 // record 100% coverage even at length 2 — those
6501 // ARE legitimately short and the closure /
6502 // sunk-emit lowering paths make
6503 // them worth compiling. The heuristic admits
6504 // them. fib's `[Lt, Jmp, Return1]` (3 of ~17)
6505 // and make's `[Lt, Jmp, LoadI, Return1]` (4 of
6506 // ~26) get discarded.
6507 //
6508 // Back-edge triggered traces are unaffected —
6509 // a tight numeric-for body legitimately covers
6510 // 3 of ~3 proto ops it can dispatch from
6511 // (`Add + ForLoop`) and the recorder fires on
6512 // the back-edge, not call entry.
6513 //
6514 // `call_hot_count` is intentionally NOT reset
6515 // (an earlier draft tried `THRESHOLD - 32` but
6516 // caused active_trace contention with the
6517 // outer back-edge trigger — see
6518 // setlist_b_zero_with_call_c_zero_sunk_emits).
6519 // We give up on dispatching the pathological
6520 // shape on the same proto; the back-edge or a
6521 // longer call path on a deeper recursion point
6522 // can still record + cache a real trace.
6523 let proto_code_len = rec.head_proto.code.len();
6524 let is_partial_coverage = rec.ops.len() * 2 < proto_code_len;
6525 // Per-Proto discard cap. The relaxed
6526 // trigger condition (`c >= THRESHOLD &&
6527 // !already_cached`) means a Proto whose every
6528 // recording is partial-coverage will re-fire the
6529 // trigger every call indefinitely (1500+ in
6530 // `binary_trees`-pattern test). The cap stops
6531 // discarding after `MAX_DISCARDS_PER_PROTO` —
6532 // the next close falls through to compile (even
6533 // if partial), caches the trace, and the
6534 // `already_cached` short-circuit kills the
6535 // storm. Dispatch may still be refused
6536 // post-compile (length gate), but the recorder
6537 // stops churning.
6538 const MAX_DISCARDS_PER_PROTO: u32 = 5;
6539 let prior_discards = rec.head_proto.trace_discard_count.get();
6540 let cap_reached = prior_discards >= MAX_DISCARDS_PER_PROTO;
6541 // Flip the `gave_up` flag the
6542 // moment cap is reached (BEFORE the close-
6543 // dispatching branch below). The trigger gates
6544 // short-circuit on this flag, skipping the
6545 // RefCell + linear `already_cached` scan on
6546 // every subsequent call to this Proto. Useful
6547 // for `binary_trees_pattern`-class loads where
6548 // a single Proto sees ~20k calls post-cap.
6549 if cap_reached
6550 && rec.is_call_triggered
6551 && is_partial_coverage
6552 && !rec.head_proto.trace_gave_up.get()
6553 {
6554 rec.head_proto.trace_gave_up.set(true);
6555 }
6556 if rec.is_call_triggered && is_partial_coverage && !cap_reached {
6557 // Tally as closed (for visibility) but DROP
6558 // without compile/cache. Use the existing
6559 // closed-lens accumulator so probes can
6560 // observe the discarded shape.
6561 // Bump discard count BEFORE
6562 // dropping the recording so the next
6563 // close sees the updated counter.
6564 rec.head_proto.trace_discard_count.set(prior_discards + 1);
6565 self.jit.counters.closed += 1;
6566 self.jit
6567 .counters
6568 .closed_lens
6569 .push((rec.is_call_triggered, rec.ops.len()));
6570 // Partial-coverage discard close path.
6571 // `closed` + `closed_lens` alone can't separate
6572 // a real successful close from a discard tally,
6573 // so tag explicitly to keep the recorder-side
6574 // close-cause taxonomy single-source.
6575 self.jit
6576 .counters
6577 .bump_close_cause("partial-coverage-discard");
6578 self.jit.active_trace = None;
6579 // Continue with interp loop — don't
6580 // fall through to compile path.
6581 // The op at `pc` hasn't dispatched yet;
6582 // the outer loop iteration handles it.
6583 } else {
6584 rec.closed = true;
6585 // Detach the closed record, then try
6586 // to compile it. Dedup by `head_pc`: a Proto
6587 // already carrying a CompiledTrace for this PC
6588 // skips recompile (the hot counter caps
6589 // re-recording at `u32::MAX / 2` anyway, but
6590 // explicit dedup keeps `Proto.traces` short
6591 // for the dispatcher's linear scan).
6592 //
6593 // On failure we just bump the failed counter
6594 // and drop the record.
6595 let head_pc_val = rec.head_pc;
6596 let closed_record = self
6597 .jit
6598 .active_trace
6599 .take()
6600 .expect("active_trace was Some this branch");
6601 self.jit.counters.closed += 1;
6602 self.jit
6603 .counters
6604 .closed_lens
6605 .push((closed_record.is_call_triggered, closed_record.ops.len()));
6606 // Cache the trace on the
6607 // recorder's *head proto*, not the current
6608 // closure's proto. For non-recursive
6609 // call-triggered traces, close fires after
6610 // `Return1` pops the callee frame — `cl` at
6611 // that point is the CALLER's closure, while
6612 // `closed_record.head_proto` is the CALLEE's
6613 // proto (the one we actually want the trace
6614 // to be discoverable from on the next call).
6615 // Self-recursive fib closed via depth-cap
6616 // mid-recursion, so `cl.proto == head_proto`
6617 // there, but only by coincidence.
6618 let head_proto = closed_record.head_proto;
6619 let already_cached = head_proto
6620 .traces
6621 .borrow()
6622 .iter()
6623 .any(|t| t.head_pc == head_pc_val);
6624 if !already_cached {
6625 // Internal-loop = true: the trace runs in
6626 // a native loop until a cmp side-exits, so
6627 // the dispatcher's per-entry marshal cost
6628 // amortizes across the whole run of
6629 // iterations the loop's recorded direction
6630 // stays valid. The lowerer auto-downgrades
6631 // to one-shot for cmp-less or Call-truncating
6632 // traces.
6633 // Side traces MUST NOT
6634 // internal-loop. The parent's recorded prefix
6635 // (ops at PCs < side trace's head_pc) defines
6636 // values for registers the child's body reads
6637 // without re-writing each iter — e.g. for
6638 // s12_step_b, parent's `pc=19 Add R[12] = R[1]
6639 // + R[11]` sets R[12], and the child trace
6640 // (head_pc=24) re-runs `pc=20 Move R[1] =
6641 // R[12]` each iter via its outer ForLoop
6642 // internal-loop, ALWAYS reading the stale
6643 // entry-time R[12]. The parent's Add never
6644 // re-runs during child's loop, so R[1] gets
6645 // pinned to one stale value. Force one-shot
6646 // for side traces: each parent-exit round-
6647 // trips through dispatcher → parent's Add
6648 // runs → side trace runs ONE iter → return.
6649 let opts = crate::jit::trace::CompileOptions {
6650 internal_loop: closed_record.side_trace_parent.is_none(),
6651 pre53: self.version() <= LuaVersion::Lua53,
6652 aot: false,
6653 };
6654 // Route through trace_compiler; split-borrow JitState
6655 // so the trait method can take `&mut dyn JitStorage`.
6656 let result = {
6657 let version = self.version();
6658 let jit = &mut self.jit;
6659 jit.storage.claim(self.jit_owner_id);
6660 let storage: &mut dyn crate::jit::JitStorage = jit.storage.as_mut();
6661 jit.trace_compiler.try_compile_trace_for(
6662 storage,
6663 &closed_record,
6664 opts,
6665 version,
6666 )
6667 };
6668 match result {
6669 Some(mut ct) => {
6670 // Tally Sinkable sites
6671 // + actually-sunk-emit sites + materialise
6672 // emit sites before moving `ct` into
6673 // Proto.traces.
6674 self.jit.counters.sinkable_seen +=
6675 ct.sinkable_sites_seen as u64;
6676 self.jit.counters.accum_bufferable_seen +=
6677 ct.accum_bufferable_seen as u64;
6678 self.jit.counters.sunk_alloc += ct.sunk_alloc_seen as u64;
6679 self.jit.counters.materialize_emit +=
6680 ct.materialize_emit_count as u64;
6681 self.jit.counters.closure_emit += ct.closure_seen as u64;
6682 if ct.is_inline_abort_close {
6683 self.jit.counters.inline_abort += 1;
6684 }
6685 // Split tally so a
6686 // probe can answer the AOT
6687 // `accepted_with_per_exit_inline`
6688 // gate's question at the JIT
6689 // surface too: how many compiled
6690 // traces emitted depth>0 cmp
6691 // side-exits, and how many of
6692 // those survived all the
6693 // `dispatchable = false` pins
6694 // (`InlineAbort-gate`,
6695 // `self-link-retf-r1`,
6696 // `downrec-stitch-pending`, etc.).
6697 if !ct.per_exit_inline.is_empty() {
6698 self.jit.counters.per_exit_inline_compiled += 1;
6699 if ct.dispatchable {
6700 self.jit.counters.per_exit_inline_dispatchable += 1;
6701 }
6702 }
6703 if let Some(reason) = ct.dispatch_off_reason {
6704 self.jit.counters.dispatch_off_reasons.push(reason);
6705 // Mirror
6706 // the ordered Vec push into
6707 // the per-reason HashMap so
6708 // probes can answer "how many
6709 // of each dispatch_off label
6710 // fired" in O(1) without
6711 // walking the Vec. Same
6712 // bucket as the recorder-side
6713 // abort/discard tags above.
6714 self.jit.counters.bump_close_cause(reason);
6715 }
6716 // Count compiled traces that
6717 // carry a down-recursion stitch
6718 // link. Bumped here (not at the
6719 // lowerer emit site) because the
6720 // Vm's JitCounters live on the Vm,
6721 // and the lowerer doesn't have a
6722 // Vm handle. Read via
6723 // `Vm::trace_downrec_link_compiled_count`.
6724 if ct.downrec_link.is_some() {
6725 self.jit.counters.downrec_link_compiled += 1;
6726 }
6727 // Multi-way guard emit counter.
6728 // Bumped when the lowerer collected
6729 // >= 2 distinct caller_pc candidates
6730 // and lifted `dispatchable=true`.
6731 // The single-CMP shape stores
6732 // `1` here without bumping; non-
6733 // DownRec closes store `0`.
6734 if ct.downrec_multi_way_count >= 2 {
6735 self.jit.counters.multi_way_guard_emitted += 1;
6736 }
6737 // Side-trace finalisation.
6738 // Pin `dispatchable=false` so the
6739 // primary lookup `traces.find(|t|
6740 // t.head_pc == pc && t.dispatchable)`
6741 // never matches this entry — the
6742 // side trace is meant to be entered
6743 // ONLY through the parent's exit
6744 // indirection, not the
6745 // back-edge / call-trigger paths.
6746 // Then write the entry fn ptr into
6747 // the parent's `exit_side_trace_ptrs`
6748 // slot so the parent's IR can read it.
6749 if let Some((parent_proto, parent_head_pc, parent_exit_idx)) =
6750 closed_record.side_trace_parent
6751 {
6752 // The lowerer's own verdict: a trace it
6753 // compiled but would not dispatch (an
6754 // untyped table read, an inline abort)
6755 // is not safe to enter from the parent's
6756 // exit either — unless the only reason
6757 // was the length gate, which weighs
6758 // dispatch overhead, not soundness (the
6759 // lowerer records the other reasons
6760 // first).
6761 let runnable = ct.dispatchable
6762 || ct.dispatch_off_reason == Some("length-gate");
6763 ct.dispatchable = false;
6764 let entry_ptr = ct.entry as *const () as *const u8;
6765 let _side_trace_head_pc = closed_record.head_pc;
6766 let parent_traces = parent_proto.traces.borrow();
6767 if let Some(parent_ct) = parent_traces
6768 .iter()
6769 .find(|t| t.head_pc == parent_head_pc)
6770 {
6771 // Shape-match
6772 // gate. Find the parent's per-exit
6773 // tag snapshot at the wired exit
6774 // (inline / tag / global) and
6775 // check the child's entry_tags
6776 // match. If not, leave the cell
6777 // null + skip cache populate so
6778 // the parent IR's
6779 // `call_indirect` stays inert at
6780 // this exit (the child's
6781 // shape-specialised IR would
6782 // mis-interpret raw bits the
6783 // parent writes to reg_state).
6784 let inline_n = parent_ct.per_exit_inline.len();
6785 let tags_n = parent_ct.per_exit_tags.len();
6786 let parent_exit_tags_slice: &[
6787 crate::jit::trace::ExitTag
6788 ] = if parent_exit_idx < inline_n {
6789 &parent_ct.per_exit_inline
6790 [parent_exit_idx]
6791 .exit_tags
6792 } else if parent_exit_idx
6793 < inline_n + tags_n
6794 {
6795 &parent_ct.per_exit_tags
6796 [parent_exit_idx - inline_n]
6797 .1
6798 } else {
6799 &parent_ct.exit_tags
6800 };
6801 let shape_matches =
6802 crate::jit::trace::exit_tags_match_entry_tags(
6803 &ct.entry_tags,
6804 parent_exit_tags_slice,
6805 &parent_ct.entry_tags,
6806 );
6807 if !shape_matches {
6808 self.jit.counters.side_trace_shape_mismatch += 1;
6809 }
6810 let shape_ok = runnable && shape_matches;
6811 // Write the child's
6812 // entry fn ptr to BOTH the legacy
6813 // `exit_side_trace_ptrs[idx]`
6814 // cell (read by the
6815 // walk_any_side_ptr_non_null tests)
6816 // AND the per-kind cell
6817 // whose heap address the parent's
6818 // IR baked. The IR-baked
6819 // cell is what the call_indirect
6820 // gate actually reads. Only write
6821 // when the shape gate passes.
6822 if shape_ok {
6823 if let Some(cell) = parent_ct
6824 .exit_side_trace_ptrs
6825 .get(parent_exit_idx)
6826 {
6827 cell.set(entry_ptr);
6828 }
6829 // Compute (kind, local) for the
6830 // IR-baked cell. Layout follows
6831 // exit_hit_counts: inline first,
6832 // then per_exit_tags, then the
6833 // global tail slot.
6834 let (sent_kind, sent_local) = if parent_exit_idx
6835 < inline_n
6836 {
6837 parent_ct.per_exit_inline[parent_exit_idx]
6838 .side_trace_ptr
6839 .set(entry_ptr);
6840 (
6841 crate::jit::trace::SIDE_SENT_KIND_INLINE,
6842 parent_exit_idx as u32,
6843 )
6844 } else if parent_exit_idx < inline_n + tags_n {
6845 let local = parent_exit_idx - inline_n;
6846 if let Some(b) =
6847 parent_ct.tags_side_trace_ptrs.get(local)
6848 {
6849 b.set(entry_ptr);
6850 }
6851 (
6852 crate::jit::trace::SIDE_SENT_KIND_TAG,
6853 local as u32,
6854 )
6855 } else {
6856 parent_ct.global_side_trace_ptr.set(entry_ptr);
6857 (crate::jit::trace::SIDE_SENT_KIND_GLOBAL, 0)
6858 };
6859 self.jit.counters.side_trace_compiled += 1;
6860 // Flip the
6861 // parent's fast-path hint so
6862 // the dispatcher knows to do
6863 // the tentative decode + cell
6864 // check on subsequent
6865 // dispatches. Set once and
6866 // stays true (we never unwire
6867 // a side trace today).
6868 parent_ct.has_any_side_wired.set(true);
6869
6870 // Populate
6871 // the O(1) lookup cache the
6872 // dispatcher consults on
6873 // sentinel-bit-set returns.
6874 // Key is the encoded sentinel
6875 // (same encoding the IR ORs
6876 // into bits 56..=62 of the
6877 // child's i64 return).
6878 let sentinel =
6879 crate::jit::trace::encode_side_sentinel(
6880 sent_kind, sent_local,
6881 );
6882 let predicted_idx = if std::ptr::eq(
6883 parent_proto.as_ptr(),
6884 head_proto.as_ptr(),
6885 ) {
6886 parent_traces.len() as u32
6887 } else {
6888 head_proto.traces.borrow().len() as u32
6889 };
6890 parent_ct
6891 .side_trace_cache
6892 .borrow_mut()
6893 .insert(sentinel, predicted_idx);
6894 }
6895 }
6896 drop(parent_traces);
6897 }
6898 head_proto.traces.borrow_mut().push(TArc::new(ct));
6899 self.jit.counters.compiled += 1;
6900 }
6901 None => {
6902 self.jit.counters.compile_failed += 1;
6903 note_trace_compile_failure(head_proto, closed_record.head_pc);
6904 self.jit
6905 .counters
6906 .compile_failed_reasons
6907 .push(self.jit.trace_compiler.last_compile_checkpoint());
6908 }
6909 }
6910 }
6911 } // close the long-trace-bias else branch
6912 } else {
6913 // Depth-aware push at the
6914 // current `cur_depth`. The `depth_cap_hit` /
6915 // `returned_past_head` early-exit is handled by
6916 // the `should_close` branch above; reaching here
6917 // means `cur_depth <= MAX_INLINE_DEPTH` and the
6918 // trace head's frame is still live.
6919 let depth_u8 = cur_depth as u8;
6920 if depth_u8 > self.jit.max_depth_seen {
6921 self.jit.max_depth_seen = depth_u8;
6922 }
6923 // Fix up a prior `Op::Call C=0` (multi-
6924 // return / variable return count). Recorder pushed
6925 // it with var_count=None before the call dispatched;
6926 // now that the call has returned and we're about to
6927 // push the next op, top reflects the actual return
6928 // count. Snapshot top - (caller.base + call.a).
6929 if let Some(last) = rec.ops.last_mut()
6930 && matches!(last.inst.op(), crate::vm::isa::Op::Call)
6931 && last.inst.c() == 0
6932 && last.var_count.is_none()
6933 && let Some(f) = self.frames.last().and_then(CallFrame::lua)
6934 {
6935 let from = f.base + last.inst.a();
6936 if self.top >= from {
6937 last.var_count = Some(self.top - from);
6938 }
6939 }
6940 // For SetList B=0, snapshot the source
6941 // count = top - A - 1 (mirrors Lua's `n = top - ra
6942 // - 1` from lvm.c OP_SETLIST). Sources are
6943 // R[A+1..top), exclusive top. For Call C=0's
6944 // var_count (the return count = top - A inclusive),
6945 // see the prior-op fix-up above; here we
6946 // initialise the current Call op to None and let
6947 // the fix-up on the next op's push populate it.
6948 let var_count = if matches!(inst.op(), crate::vm::isa::Op::SetList)
6949 && inst.b() == 0
6950 && let Some(f) = self.frames.last().and_then(CallFrame::lua)
6951 {
6952 let from = f.base + inst.a();
6953 if self.top > from {
6954 Some(self.top - from - 1)
6955 } else {
6956 None
6957 }
6958 } else {
6959 None
6960 };
6961 let op = crate::jit::trace::RecordedOp {
6962 proto: cl.proto,
6963 pc,
6964 inst,
6965 inline_depth: depth_u8,
6966 var_count,
6967 };
6968 // Depth>0 Return0/Return1 mirrors
6969 // LuaJIT's `IR_RETF` (lj_record.c:922+ lj_record_ret).
6970 // Captured as a side-channel `RetfRecord` parallel to
6971 // `ops` when `self_link_enabled` is on. The
6972 // down-rec stitch consumes these to guard side-trace
6973 // inlined-frame topology against the recorded shape.
6974 // Gated on the same flag as the cycle catch so the
6975 // ship-default path (p16 off) sees zero behavior
6976 // change. `caller_pc` is the recorded enclosing Call's
6977 // pc + 1 — interp's resume point after the inlined
6978 // frame pops.
6979 if self.jit.self_link_enabled
6980 && depth_u8 > 0
6981 && matches!(
6982 inst.op(),
6983 crate::vm::isa::Op::Return0 | crate::vm::isa::Op::Return1
6984 )
6985 {
6986 let results: u8 = match inst.op() {
6987 crate::vm::isa::Op::Return0 => 0,
6988 crate::vm::isa::Op::Return1 => 1,
6989 _ => 0,
6990 };
6991 // Most recent Op::Call recorded at the caller's
6992 // depth (`depth_u8 - 1`) is the frame this Return
6993 // is unwinding from. Reverse scan stops at the
6994 // first match.
6995 let caller_depth = depth_u8 - 1;
6996 let caller_call = rec.ops.iter().rev().find(|r| {
6997 r.inline_depth == caller_depth
6998 && matches!(r.inst.op(), crate::vm::isa::Op::Call)
6999 });
7000 let caller_pc = caller_call.map(|r| r.pc + 1).unwrap_or(pc);
7001 // Capture the caller's proto
7002 // for the RetfRecord. LuaJIT `IR_RETF.op1`
7003 // equivalent. For fib(28) the caller's proto
7004 // equals the trace head; for future mutual
7005 // recursion the recorded Op::Call's proto is the
7006 // right target. Fallback to head_proto when no
7007 // enclosing Call op was captured (mirrors
7008 // `caller_pc`'s fallback to the Return's own pc).
7009 let caller_proto = caller_call.map(|r| r.proto).unwrap_or(rec.head_proto);
7010 rec.retfs.push(crate::jit::trace::RetfRecord {
7011 from_depth: depth_u8,
7012 to_depth: caller_depth,
7013 results,
7014 caller_pc,
7015 proto: caller_proto,
7016 });
7017 // DownRec close trigger:
7018 // count RetfRecords on this recording whose
7019 // `proto` matches `caller_proto` (LuaJIT
7020 // `check_downrec_unroll` chain filter
7021 // `op1 == ptref`). Threshold mirrors
7022 // RECUNROLL_THRESHOLD; first trip stamps the
7023 // `downrec_close` marker, subsequent retfs
7024 // keep the marker without overwrite. The
7025 // lowerer's end_idx picker routes through
7026 // TraceEnd::DownRec when the marker is set.
7027 if rec.downrec_close.is_none() {
7028 let caller_proto_ptr = caller_proto.as_ptr();
7029 let prior_match_count = rec
7030 .retfs
7031 .iter()
7032 .filter(|r| r.proto.as_ptr() == caller_proto_ptr)
7033 .count();
7034 // Strictly-greater-than threshold matches
7035 // LuaJIT `count + J->tailcalled > recunroll`.
7036 // The newly-pushed retf is already counted.
7037 if prior_match_count > crate::jit::trace::RECUNROLL_THRESHOLD {
7038 rec.downrec_close = Some(crate::jit::trace::DownRecClose {
7039 return_pc: caller_pc,
7040 target_proto: caller_proto,
7041 depth_delta: 1,
7042 });
7043 // Close-cause taxonomy: tag the
7044 // restart with `"downrec-restart"`. The
7045 // lowerer adds `"downrec-stitch-failed"` when
7046 // the lifted back-edge falls back to
7047 // deopt.
7048 self.jit.counters.bump_close_cause("downrec-restart");
7049 }
7050 }
7051 }
7052 // Capture FieldIcSnapshot for the
7053 // FIRST eligible Op::GetField site under env-gate
7054 // LUNA_JIT_FIELD_IC=1. "Eligible" means:
7055 // - R[B] is Value::Table with metatable.is_none()
7056 // - K[C] is Value::Str
7057 // - The string key actually occupies a hash slot
7058 // (so the IC's slot_idx is a real index, not
7059 // a probe sentinel).
7060 // Once captured, subsequent GetFields skip this
7061 // logic (rec.field_ic_snapshot.is_some() short-
7062 // circuits). Env-OFF short-circuits on the cached
7063 // atomic check inside field_ic_enabled().
7064 if rec.field_ic_snapshot.is_none()
7065 && matches!(inst.op(), crate::vm::isa::Op::GetField)
7066 && crate::jit::trace_types::field_ic_enabled()
7067 {
7068 let b = inst.b();
7069 let c_idx = inst.c() as usize;
7070 let r_b = self.stack[(base + b) as usize];
7071 if let Value::Table(g) = r_b
7072 && g.metatable().is_none()
7073 && c_idx < cl.proto.consts.len()
7074 && let Value::Str(s) = cl.proto.consts[c_idx]
7075 {
7076 let key = Value::Str(s);
7077 let tbl_ref = &*g;
7078 if let Some(slot_idx) = tbl_ref.find_node_idx(key)
7079 && let Some(val) = tbl_ref.node_val_at(slot_idx)
7080 {
7081 let op_idx = rec.ops.len() as u32;
7082 rec.field_ic_snapshot =
7083 Some(crate::jit::trace_types::FieldIcSnapshot {
7084 op_idx,
7085 nodes_len: tbl_ref.nodes_capacity() as u64,
7086 slot_idx: slot_idx as u64,
7087 key_ptr_bits: s.as_ptr() as u64,
7088 cached_val_tag: val.tag_byte(),
7089 });
7090 self.jit.counters.field_ic_snapshot_captured += 1;
7091 }
7092 }
7093 }
7094 if !rec.push(op) {
7095 // Recorder overflow (MAX_TRACE_LEN). Tag it
7096 // explicitly under the close-cause bucket so
7097 // probes can tally overflow vs other abort
7098 // causes in O(1).
7099 self.jit.active_trace = None;
7100 self.jit.counters.aborted += 1;
7101 self.jit.counters.bump_close_cause("trace-overflow");
7102 }
7103 }
7104 }
7105
7106 // Trace JIT dispatcher.
7107 //
7108 // When the dispatch loop is about to execute the op at
7109 // `pc` and there's a `numeric_only` CompiledTrace cached
7110 // for that `head_pc`, marshal the live regs into an
7111 // i64 buffer, jump into the trace, and resume the
7112 // interpreter at the returned continuation PC.
7113 //
7114 // Skipped (zero overhead) when `trace_jit_enabled` is
7115 // false; the lookup is a borrow + scan over
7116 // `cl.proto.traces`, which is a `Vec` whose size is at
7117 // most one entry per back-edge per Proto in practice.
7118 //
7119 // Marshalling contract — only Int slots survive the
7120 // round-trip cleanly (the reg_state ABI is `*mut i64`
7121 // with no tag info). Any non-Int slot in the affected
7122 // window forces a skip; interp takes over for one op
7123 // and the back-edge brings us back to try again next
7124 // pass (slots that were Nil/Float at one moment can
7125 // settle to Int by the time the next back-edge fires).
7126 //
7127 // A trace that comes back with `vm.jit.pending_err`
7128 // parked is treated as a deopt: clear the err, leave
7129 // the stack as the trace wrote it, and let the
7130 // interpreter run from the same `pc`. The trace itself
7131 // is left cached — a future entry might find no
7132 // metatable in the way and succeed.
7133 // Single Rc<CompiledTrace> clone instead of per-field Rc
7134 // clones: proto.traces is Vec<Rc<CompiledTrace>>; the
7135 // dispatcher clones ONE Rc and reads fields via auto-deref.
7136 // One-shot consume of the
7137 // `suppress_downrec_admit_once` flag. Set by the
7138 // downrec post-invoke arm below when it force-deopts the
7139 // trace (caller-pc guard miss OR cycle-budget exhausted)
7140 // so the NEXT interpreter loop iteration skips the
7141 // downrec admit, lets interp run the op at `head_pc`,
7142 // advances `pc` past `head_pc`, and breaks the otherwise-
7143 // infinite admit loop. Reading + clearing here means a
7144 // single dispatch tick consumes the suppression — the
7145 // following tick re-admits naturally (with the budget
7146 // also reset by the deopt site).
7147 let downrec_admit_blocked = self.jit.suppress_downrec_admit_once;
7148 self.jit.suppress_downrec_admit_once = false;
7149 if self.jit.trace_enabled
7150 && let Some(ct) = {
7151 let traces = cl.proto.traces.borrow();
7152 traces
7153 .iter()
7154 .find(|t| {
7155 if t.head_pc != pc {
7156 return false;
7157 }
7158 let is_downrec = t.downrec_link.is_some();
7159 // The one-shot suppress
7160 // flag blocks any admit (primary or fallback)
7161 // for `downrec_link`-bearing traces so the
7162 // next interp iter can run the natural op
7163 // at `head_pc` and advance past it. The multi-way
7164 // `dispatchable=true` lift means the suppress
7165 // must also cover the primary `t.dispatchable`
7166 // arm — otherwise the lifted lookup would
7167 // immediately re-admit after a force-deopt
7168 // and the infinite loop returns.
7169 if downrec_admit_blocked {
7170 return false;
7171 }
7172 // Primary arm: `dispatchable=true` traces
7173 // (lifted multi-way DownRec or normal traces).
7174 // Fallback arm: single-CMP `dispatchable=false`
7175 // DownRec traces (single-CMP guard kept
7176 // pinned because the 90% miss-rate would
7177 // make blind admit perf-negative).
7178 t.dispatchable || is_downrec
7179 })
7180 .cloned()
7181 }
7182 {
7183 // Borrow Rc<[T]> fields as &Rc<[T]> instead
7184 // of cloning. The outer `ct: Rc<CompiledTrace>` is held
7185 // across the entire dispatch block so the fields outlive
7186 // all consumers.
7187 let entry_fn = ct.entry;
7188 let head_pc_val = ct.head_pc;
7189 let window_size = ct.window_size;
7190 let exit_tags = &ct.exit_tags;
7191 let per_exit_tags = &ct.per_exit_tags;
7192 let per_exit_inline = &ct.per_exit_inline;
7193 let compile_entry_tags = &ct.entry_tags;
7194 let global_tag_res_kind = ct.global_tag_res_kind;
7195 let exit_hit_counts = &ct.exit_hit_counts;
7196 let max_stack = cl.proto.max_stack as usize;
7197 let window_size_us = window_size as usize;
7198 let base_us = base as usize;
7199 // `reg_state` sized to the trace's `window_size`, which
7200 // may exceed max_stack.
7201 // Marshal-in still only writes [0..max_stack); slots
7202 // [max_stack..window_size) are zero-initialised and
7203 // filled by the trace's own GetUpval / arith.
7204 // Reuse the Vm's amortised buffers
7205 // instead of allocating fresh Vecs each dispatch.
7206 // mem::take leaves an empty placeholder we restore
7207 // at the end of the dispatch block (success +
7208 // deopt paths both fall through to the restore).
7209 let mut entry_tags: Vec<u8> = std::mem::take(&mut self.jit.entry_tags_buf);
7210 entry_tags.clear();
7211 entry_tags.reserve(max_stack);
7212 // This trace was admitted via the
7213 // `downrec_link.is_some()` arm rather than the normal
7214 // `dispatchable=true` arm. The pre-invoke path
7215 // populates a reserved saved-PC slot just past the
7216 // normal register window so the lowerer's guard load
7217 // (`reg_state[window_size]`) compares the runtime
7218 // saved caller PC against the recorded `dr_return_pc`.
7219 //
7220 // No `!ct.dispatchable` gate: when the lowerer lifts
7221 // `dispatchable = true` for multi-way guards, the
7222 // trace's body still emits the downrec sentinel shape
7223 // on return — the saved-PC slot
7224 // and post-invoke classifier must keep firing.
7225 // `downrec_link.is_some()` is the unique structural
7226 // signal that the trace closes via DownRec.
7227 let is_downrec_entry = ct.downrec_link.is_some();
7228 let mut reg_state: Vec<i64> = std::mem::take(&mut self.jit.reg_state_buf);
7229 reg_state.clear();
7230 // When admitting a downrec trace,
7231 // size the buffer to `window_size + 1` so the lowerer
7232 // can `load(I64, ..., reg_state, window_size * 8)`
7233 // for the saved caller PC guard input. The extra slot
7234 // is the LAST element so cranelift's existing
7235 // `0..window_size` accesses are unaffected.
7236 let reg_state_len = if is_downrec_entry {
7237 window_size_us + 1
7238 } else {
7239 window_size_us
7240 };
7241 reg_state.resize(reg_state_len, 0i64);
7242 let mut dispatch_ok = true;
7243 for i in 0..max_stack {
7244 let v = self.stack[base_us + i];
7245 let (tag, raw) = v.unpack();
7246 entry_tags.push(tag);
7247 // Entry tag guard. The trace's IR
7248 // is specialised to the compile-time entry tags
7249 // (via current_kinds propagation from
7250 // from_entry_tag). A runtime tag mismatch means
7251 // body ops would mis-interpret raw bits (e.g.
7252 // treat a Str pointer as Int payload → garbage).
7253 // Skip dispatch on mismatch so interp handles
7254 // this entry shape; the trace stays cached for
7255 // future entries that match.
7256 if i < compile_entry_tags.len() && tag != compile_entry_tags[i] {
7257 dispatch_ok = false;
7258 break;
7259 }
7260 match tag {
7261 // Int / Float / Table / Nil all marshal
7262 // to raw payload cleanly; the trace's IR
7263 // treats the 8-byte slot as an i64 (with
7264 // f64 ops bitcasting around the boundary).
7265 crate::runtime::value::raw::INT
7266 | crate::runtime::value::raw::FLOAT
7267 | crate::runtime::value::raw::TABLE
7268 | crate::runtime::value::raw::CLOSURE
7269 // Native iter slots (e.g.
7270 // R[A] = ipairs_iter) are present in
7271 // generic-for traces; the raw bits are a
7272 // valid `*mut NativeClosure` and round-trip
7273 // cleanly.
7274 | crate::runtime::value::raw::NATIVE
7275 // Str slots show up in
7276 // string-concat traces; raw bits = `*mut
7277 // LuaStr` (interned, GC-managed). Round-
7278 // trips cleanly as a heap pointer.
7279 | crate::runtime::value::raw::STR
7280 | crate::runtime::value::raw::NIL => {
7281 // 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).
7282 reg_state[i] = unsafe { raw.zero as i64 };
7283 }
7284 _ => {
7285 dispatch_ok = false;
7286 break;
7287 }
7288 }
7289 }
7290
7291 if dispatch_ok {
7292 debug_assert_eq!(head_pc_val, pc, "trace cache hit's head_pc != pc");
7293 // A recording in progress cannot see what the trace runs
7294 // natively: it would resume after the trace with ops
7295 // missing, and could close as a loop that never ran (a
7296 // side trace of two ops returning its own head, which
7297 // the dispatcher then entered forever). Drop it.
7298 if self.jit.active_trace.take().is_some() {
7299 self.jit.counters.aborted += 1;
7300 self.jit.counters.bump_close_cause("reached-compiled-trace");
7301 }
7302 self.jit.pending_err = None;
7303 // Snapshot the pre-entry frame
7304 // count. A cmp@d>0 side-exit calls the materialize
7305 // helper which pushes inlined frames onto
7306 // `vm.frames`; on deopt those frames must be popped
7307 // before falling through to the interpreter, else
7308 // the stack grows unboundedly per deopted dispatch.
7309 let pre_frames = self.frames.len();
7310 // Saved-PC slot population. The
7311 // recorded `dr_return_pc` on the closing trace is
7312 // the caller's resume PC captured at a depth>0
7313 // Return push (recorder push site). The natural runtime analogue for self-
7314 // stitch is the dispatching frame's PARENT frame's
7315 // PC: the trace's head_pc sits inside a Lua frame,
7316 // and the parent (caller) frame's `pc` is what
7317 // luna would observe as `[base-8]` in the LJ
7318 // `asm_retf` shape (`lj_asm_arm64.h:565`). When
7319 // the parent isn't a Lua frame (top-level dispatch
7320 // — first invocation through `call_value`), no
7321 // saved PC exists; we write 0, which always
7322 // mismatches the recorded `dr_return_pc != 0`
7323 // invariant (debug-asserted in the luna-jit trace
7324 // lowerer).
7325 if is_downrec_entry {
7326 let saved_pc: i64 = if pre_frames >= 2 {
7327 match &self.frames[pre_frames - 2] {
7328 CallFrame::Lua(parent) => parent.pc as i64,
7329 CallFrame::Cont(_) => 0,
7330 }
7331 } else {
7332 0
7333 };
7334 reg_state[window_size_us] = saved_pc;
7335 }
7336 // `LUNA_AOT_PROBE`
7337 // diagnostic hook. The probe fires once per trace dispatch
7338 // (regardless of JIT vs AOT origin — both go through this
7339 // arm), letting the AOT smoke test verify mcode actually
7340 // executed. Guarded behind `OnceLock` so the env read is
7341 // a one-time cost per process; not gated on a particular
7342 // counter so the smoke test gets a deterministic single-
7343 // line `aot_trace_fired pc=N` per first dispatch.
7344 if jit_probe_enabled() && self.jit.counters.dispatched == 0 {
7345 eprintln!("luna-runtime-helpers: aot_trace_fired pc={head_pc_val}");
7346 }
7347 let continuation_pc = {
7348 // chunk_compiler.enter
7349 // (CraneliftBackend delegates to enter_jit;
7350 // NullJitBackend returns an inert guard).
7351 let vm_ptr: *mut Vm = self;
7352 let _guard = self.jit.chunk_compiler.enter(vm_ptr, Some(cl));
7353 // 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).
7354 unsafe { entry_fn(reg_state.as_mut_ptr()) }
7355 };
7356 self.jit.counters.dispatched += 1;
7357
7358 if self.jit.pending_err.is_some() {
7359 self.jit.pending_err = None;
7360 self.jit.counters.deopt += 1;
7361 // Unwind any helper-pushed
7362 // inlined frames before the interpreter resumes.
7363 // Don't restore reg_state — the trace's partial
7364 // writes are discarded; interp re-executes from
7365 // the original `pc`.
7366 while self.frames.len() > pre_frames {
7367 frames_pop_sync(&mut self.frames, &mut self.frames_top);
7368 }
7369 if is_downrec_entry {
7370 // pending_err observed
7371 // mid-trace inside a downrec admit. Treat
7372 // it as a guard miss: bump `downrec_deopt`
7373 // and suppress the next downrec admit so
7374 // interp can advance past `head_pc` and
7375 // the same trace doesn't immediately re-
7376 // fire on the next loop iteration.
7377 self.jit.counters.downrec_deopt += 1;
7378 self.jit.suppress_downrec_admit_once = true;
7379 }
7380 } else if is_downrec_entry && {
7381 // Only enter the
7382 // downrec classifier for returns whose shape
7383 // matches the lowerer's `downrec_idx_opt` tail
7384 // emit: either the stitch_blk DOWNREC sentinel
7385 // (HIT) or the deopt_blk GLOBAL-sentinel-with-
7386 // body==head_pc (MISS via guard fail). Any
7387 // other return from a downrec trace (intermediate
7388 // body cmp side-exit, GetField inference fail,
7389 // etc.) carries a different sentinel/body shape
7390 // and means the body exited BEFORE reaching the
7391 // downrec close — classify those through the
7392 // normal decode path (else branch below) so
7393 // reg_state restores + pc advances correctly.
7394 // Classifying them all as MISS would skip the
7395 // normal restore, inflating `downrec_deopt` with
7396 // non-downrec events and losing the trace's
7397 // mid-flight writes.
7398 let raw_ret = continuation_pc as u64;
7399 let from_side_trace = (raw_ret >> 63) & 1 == 1;
7400 let sentinel_code = if from_side_trace {
7401 ((raw_ret >> 56) & 0x7F) as u32
7402 } else {
7403 0
7404 };
7405 let raw_body = raw_ret & 0x00FF_FFFF_FFFF_FFFFu64;
7406 let global_deopt_code = crate::jit::trace_types::encode_side_sentinel(
7407 crate::jit::trace_types::SIDE_SENT_KIND_GLOBAL,
7408 0,
7409 );
7410 from_side_trace
7411 && (crate::jit::trace_types::is_downrec_sentinel(sentinel_code)
7412 || (sentinel_code == global_deopt_code
7413 && raw_body == head_pc_val as u64))
7414 } {
7415 // Downrec event classifier.
7416 let raw_ret = continuation_pc as u64;
7417 let sentinel_code = ((raw_ret >> 56) & 0x7F) as u32;
7418 if crate::jit::trace_types::is_downrec_sentinel(sentinel_code) {
7419 // Guard HIT — saved_pc matched one of the
7420 // baked candidates and the trace's
7421 // `stitch_blk` arm returned the DOWNREC
7422 // sentinel. Cycle-safety checkpoint:
7423 // decrement budget; on underflow,
7424 // reclassify as deopt + reset budget.
7425 // `STITCH_DEPTH_DEFAULT = 32` lets
7426 // ~all natural HITs in a hot loop fire
7427 // before reset pressure.
7428 if self.jit.stitch_depth_remaining > 0 {
7429 self.jit.stitch_depth_remaining -= 1;
7430 self.jit.counters.downrec_dispatched += 1;
7431 } else {
7432 self.jit.counters.downrec_deopt += 1;
7433 self.jit.stitch_depth_remaining =
7434 crate::vm::jit_state::JitState::STITCH_DEPTH_DEFAULT;
7435 }
7436 } else {
7437 // Guard MISS via the lowerer's deopt_blk
7438 // arm (GLOBAL sentinel + body == head_pc).
7439 // The deopt_blk emit performs the
7440 // store-back via `emit_store_back_and_return_pc`,
7441 // so the live stack already reflects the
7442 // body's writes; no extra restore needed
7443 // from the dispatcher side.
7444 self.jit.counters.downrec_deopt += 1;
7445 }
7446 self.jit.suppress_downrec_admit_once = true;
7447 // Pop helper-pushed inlined frames (defensive —
7448 // the downrec emit shape doesn't push frames in the
7449 // tail, but a body side-exit before reaching
7450 // the tail may have via the materialize helper).
7451 while self.frames.len() > pre_frames {
7452 frames_pop_sync(&mut self.frames, &mut self.frames_top);
7453 }
7454 self.jit.reg_state_buf = reg_state;
7455 self.jit.entry_tags_buf = entry_tags;
7456 continue;
7457 } else {
7458 // Restore each slot using the trace's
7459 // exit-tag analysis (see ExitTag docs).
7460 // Decode the IR's
7461 // side-exit shape. Upper 32 bits = (site_idx
7462 // + 1) for inline cmp side-exits, 0 for
7463 // legacy clean-tail / non-inline exits.
7464 // The decode lives in
7465 // `crate::jit::trace::decode_exit_shape` so
7466 // side-trace returns can reuse it with the SIDE
7467 // TRACE's shape inputs when the sentinel bit
7468 // is set on `raw_ret`.
7469 let raw_ret = continuation_pc as u64;
7470 // Side-trace return decode.
7471 // Bit 63 of `raw_ret` is the side-trace
7472 // marker the parent's IR OR'd in when it
7473 // tail-called into a wired child trace.
7474 // Bits 56..=62 carry the sentinel code (the
7475 // cache key into the parent's
7476 // `side_trace_cache`); bits 0..=55 are the
7477 // child's own return value (encoded site or
7478 // plain cont_pc) which we MUST decode using
7479 // the CHILD's per_exit_inline / per_exit_tags
7480 // / exit_tags / exit_hit_counts — not the
7481 // parent's. The dispatcher snapshot read
7482 // above holds the parent's shapes; when bit
7483 // 63 is set we re-fetch the child's via the
7484 // sentinel-keyed cache.
7485 let from_side_trace = (raw_ret >> 63) & 1 == 1;
7486 let (
7487 decode_inline,
7488 decode_tags,
7489 decode_exit_tags,
7490 decode_hit_counts,
7491 decode_body,
7492 child_ran,
7493 ) = if from_side_trace {
7494 let sentinel_code = ((raw_ret >> 56) & 0x7F) as u32;
7495 let body = raw_ret & 0x00FF_FFFF_FFFF_FFFFu64;
7496 let traces = cl.proto.traces.borrow();
7497 let child_idx = traces
7498 .iter()
7499 .find(|t| t.head_pc == head_pc_val)
7500 .and_then(|pct| {
7501 pct.side_trace_cache.borrow().get(&sentinel_code).copied()
7502 });
7503 if let Some(idx) = child_idx
7504 && let Some(child) = traces.get(idx as usize)
7505 {
7506 if crate::jit::trace::v2c_probe_enabled() {
7507 eprintln!(
7508 "[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={}",
7509 sentinel_code,
7510 body,
7511 idx,
7512 child.n_ops,
7513 child.head_pc,
7514 child.window_size,
7515 pc,
7516 window_size,
7517 child.dispatchable,
7518 child.is_inline_abort_close,
7519 );
7520 }
7521 (
7522 child.per_exit_inline.clone(),
7523 child.per_exit_tags.clone(),
7524 child.exit_tags.clone(),
7525 child.exit_hit_counts.clone(),
7526 body,
7527 true,
7528 )
7529 } else {
7530 if crate::jit::trace::v2c_probe_enabled() {
7531 eprintln!(
7532 "[v2c-A3-decode] sentinel={:#04x} body={:#018x} child MISS (fallback parent shapes)",
7533 sentinel_code, body,
7534 );
7535 }
7536 // Cache miss — fall back to parent
7537 // shapes with the body bits. Best-
7538 // effort; the trace_side_trace_
7539 // shape_mismatch_count records this
7540 // path indirectly (close-handler
7541 // skips wiring on mismatch so we
7542 // shouldn't reach here when shape
7543 // gate held).
7544 (
7545 per_exit_inline.clone(),
7546 per_exit_tags.clone(),
7547 exit_tags.clone(),
7548 exit_hit_counts.clone(),
7549 body,
7550 true,
7551 )
7552 }
7553 } else {
7554 // Dispatcher-level side-trace invocation,
7555 // rather than an IR gate (`load + icmp +
7556 // brif`) at every emit_store_back callsite,
7557 // which measured as a net slowdown. The
7558 // tentative decode + cell load always runs:
7559 // short-circuiting it on a
7560 // `parent_has_side` hint measured slower on
7561 // btrees_d8 and no faster on fib_10.
7562 {
7563 let tentative = crate::jit::trace::decode_exit_shape(
7564 raw_ret,
7565 per_exit_inline,
7566 per_exit_tags,
7567 exit_tags,
7568 );
7569 let tentative_exit_idx = tentative.exit_hit_idx;
7570 let child_invoke = {
7571 let traces = cl.proto.traces.borrow();
7572 traces.iter().find(|t| t.head_pc == head_pc_val).and_then(
7573 |pct| {
7574 let cell =
7575 pct.exit_side_trace_ptrs.get(tentative_exit_idx)?;
7576 let fn_ptr = cell.get();
7577 if fn_ptr.is_null() {
7578 return None;
7579 }
7580 traces
7581 .iter()
7582 .find(|t| {
7583 t.entry as *const () as *const u8 == fn_ptr
7584 })
7585 .map(|child| {
7586 (
7587 child.entry,
7588 child.per_exit_inline.clone(),
7589 child.per_exit_tags.clone(),
7590 child.exit_tags.clone(),
7591 child.exit_hit_counts.clone(),
7592 )
7593 })
7594 },
7595 )
7596 };
7597 if let Some((cent, cpi, cpt, cet, chc)) = child_invoke {
7598 let child_raw_ret = {
7599 // chunk_compiler.enter
7600 // (side-trace entry).
7601 let vm_ptr: *mut Vm = self;
7602 let _guard =
7603 self.jit.chunk_compiler.enter(vm_ptr, Some(cl));
7604 // 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).
7605 unsafe { cent(reg_state.as_mut_ptr()) }
7606 };
7607 (cpi, cpt, cet, chc, child_raw_ret as u64, true)
7608 } else {
7609 (
7610 per_exit_inline.clone(),
7611 per_exit_tags.clone(),
7612 exit_tags.clone(),
7613 exit_hit_counts.clone(),
7614 raw_ret,
7615 false,
7616 )
7617 }
7618 }
7619 };
7620 let decoded = crate::jit::trace::decode_exit_shape(
7621 decode_body,
7622 &decode_inline,
7623 &decode_tags,
7624 &decode_exit_tags,
7625 );
7626 let site_id = decoded.site_id;
7627 let cont_pc = decoded.cont_pc;
7628 let exit_hit_idx = decoded.exit_hit_idx;
7629 let exit_tags_for_pc = decoded.exit_tags_for_pc;
7630 // When a side trace ran (tail-called by the
7631 // parent's code or invoked here), force
7632 // using_global_exit_tags=false so the restore
7633 // loop takes the per-tag slow path:
7634 // `global_tag_res_kind` classifies the parent's
7635 // exit tags, not the child's.
7636 let using_global_exit_tags = if child_ran {
7637 false
7638 } else {
7639 decoded.using_global_exit_tags
7640 };
7641 // Increment the counter (saturate
7642 // at u32::MAX to avoid wrap on long runs).
7643 // Track whether this increment is
7644 // the one that crossed `HOTEXIT_THRESHOLD`
7645 // (transition: previous v < threshold, new v
7646 // == threshold). The side-trace start is
7647 // deferred to just before `continue;` so
7648 // vm.stack and frame.pc are fully restored
7649 // (the snapshot reads post-restore values).
7650 let mut side_trace_should_start = false;
7651 // For side-trace returns the
7652 // counter to bump is the CHILD's (decoded
7653 // shape lookup) — `exit_hit_idx` is into the
7654 // decoded layout, so use the matching
7655 // `decode_hit_counts`. For parent decode
7656 // they're aliased (clone of the parent's
7657 // own Rc).
7658 if let Some(c) = decode_hit_counts.get(exit_hit_idx) {
7659 let v = c.get();
7660 if v < u32::MAX {
7661 c.set(v + 1);
7662 }
7663 // After a side trace ran, `exit_hit_idx` is an
7664 // exit of that child, but a side trace is
7665 // recorded and wired as one of `head_pc_val`'s
7666 // exits: it would replace the side trace on the
7667 // parent's exit of that number, which resumes
7668 // elsewhere.
7669 if v + 1 == crate::jit::trace::HOTEXIT_THRESHOLD
7670 && !child_ran
7671 && self.jit.active_trace.is_none()
7672 && self.jit.trace_enabled
7673 {
7674 side_trace_should_start = true;
7675 }
7676 }
7677 // At an inline cmp@d>0
7678 // side-exit, the helper has pushed N frames on
7679 // top of the trace head's frame and
7680 // `exit_tags_for_pc.len()` covers the full
7681 // window (caller + each inlined frame's
7682 // window). Slots beyond `max_stack` belong to
7683 // an inlined frame: their `Untouched` entries
7684 // default to Nil (no entry-tag fallback —
7685 // marshal-in only captured caller slots) and
7686 // we write to interp stack at `base + i` which
7687 // mirrors `op_offsets`-derived layout.
7688 let slot_count = exit_tags_for_pc.len();
7689 // The helper only extends
7690 // vm.stack up to the deepest pushed frame's
7691 // window, but the exit_tags snapshot covers
7692 // the trace's full `window_size` (which
7693 // includes depth-N+1 scratch slots that the
7694 // trace's IR may have written without a
7695 // matching pushed frame). Extend with Nil so
7696 // the write at the tail doesn't panic; these
7697 // slots get overwritten by the writeback loop
7698 // and won't leak meaningful data past the
7699 // pushed frames' R[0..max_stack) windows.
7700 if self.stack.len() < base_us + slot_count {
7701 self.stack
7702 .resize(base_us + slot_count, crate::runtime::Value::Nil);
7703 }
7704 // Fast-path restore loop. When
7705 // we landed on the global `exit_tags`,
7706 // dispatch on the compile-time
7707 // classification: skip the loop entirely
7708 // for `AllUntouched`, do a tag-free
7709 // `Value::Int(...)` write per slot for
7710 // `AllInt`, otherwise fall through to the
7711 // general match-arm loop. site_id > 0
7712 // (inline frame mat) and per_exit_tags
7713 // hits always take the general path —
7714 // their per-side-exit shapes aren't
7715 // pre-classified yet.
7716 // A generic-for exit whose TForCall wrote the loop
7717 // variables to the stack with tags the trace did not
7718 // compile for: leave those slots as they are.
7719 let keep_tfor =
7720 if decode_body & crate::jit::trace_types::EXIT_KEEP_TFOR_VARS != 0 {
7721 let call = cl.proto.code[cont_pc as usize - 1];
7722 debug_assert!(matches!(call.op(), crate::vm::isa::Op::TForCall));
7723 let first = call.a() as usize + 4;
7724 first..first + call.c() as usize
7725 } else {
7726 0..0
7727 };
7728 let fast_path_taken = if using_global_exit_tags && keep_tfor.is_empty() {
7729 match global_tag_res_kind {
7730 crate::jit::trace::TagResKind::AllUntouched => {
7731 // No-op: vm.stack already
7732 // matches the trace's post-
7733 // entry state for these
7734 // slots (entry values not
7735 // overridden, or already
7736 // spilled by helpers).
7737 true
7738 }
7739 crate::jit::trace::TagResKind::AllInt => {
7740 for i in 0..slot_count {
7741 self.stack[base_us + i] =
7742 crate::runtime::Value::Int(reg_state[i]);
7743 }
7744 true
7745 }
7746 crate::jit::trace::TagResKind::Mixed => false,
7747 }
7748 } else {
7749 false
7750 };
7751 if !fast_path_taken {
7752 for i in 0..slot_count {
7753 if keep_tfor.contains(&i) {
7754 continue;
7755 }
7756 let tag = match exit_tags_for_pc[i] {
7757 crate::jit::trace::ExitTag::Untouched => {
7758 if i < max_stack {
7759 entry_tags[i]
7760 } else {
7761 crate::runtime::value::raw::NIL
7762 }
7763 }
7764 crate::jit::trace::ExitTag::Int => {
7765 crate::runtime::value::raw::INT
7766 }
7767 crate::jit::trace::ExitTag::Float => {
7768 crate::runtime::value::raw::FLOAT
7769 }
7770 crate::jit::trace::ExitTag::Table => {
7771 crate::runtime::value::raw::TABLE
7772 }
7773 crate::jit::trace::ExitTag::Closure => {
7774 crate::runtime::value::raw::CLOSURE
7775 }
7776 // Trace actively wrote Nil
7777 // to this slot (e.g. via Op::LoadNil).
7778 // Restore as Nil regardless of the entry
7779 // tag, since the i64 payload is 0 and
7780 // packing as the entry tag (e.g. INT)
7781 // would mis-type the slot.
7782 crate::jit::trace::ExitTag::Nil => {
7783 crate::runtime::value::raw::NIL
7784 }
7785 // Trace wrote a Str ptr
7786 // to this slot (LoadK Str / Move from
7787 // Str / Concat result). Restore as
7788 // Value::Str with raw bits round-
7789 // tripped.
7790 crate::jit::trace::ExitTag::Str => {
7791 crate::runtime::value::raw::STR
7792 }
7793 };
7794 // SAFETY: tag is from a verified slot
7795 // (entry validated above) or pinned by
7796 // the exit-tag analysis to INT/TABLE.
7797 // The raw payload sits in reg_state[i].
7798 // Stack was extended by the materialize
7799 // helper for inline frames.
7800 // 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).
7801 self.stack[base_us + i] = unsafe {
7802 Value::pack(
7803 tag,
7804 crate::runtime::value::RawVal {
7805 zero: reg_state[i] as u64,
7806 },
7807 )
7808 };
7809 }
7810 }
7811 // For non-inline exits the
7812 // helper was never called (no metas chain for
7813 // this cont_pc), so `frames.last()` is the
7814 // trace head's frame and we set its pc to
7815 // cont_pc as before. For inline exits the
7816 // helper baked the side-exit PC into the
7817 // innermost frame's `pc` at push time
7818 // (chain.last().pc was overridden at emit),
7819 // so this assignment to `frames.last_mut().pc
7820 // = cont_pc` is a redundant-but-correct
7821 // confirmation.
7822 let _ = &per_exit_inline; // hold the Rc alive across dispatch
7823 // For inline side-exits the
7824 // helper has pushed N frames on top. The trace
7825 // head frame is at `pre_frames - 1`; set its
7826 // pc to `head_resume_pc` so when the chain
7827 // eventually pops back to it, interp resumes
7828 // PAST the trace's depth-0 Op::Call instead of
7829 // restarting from `head_pc` and re-triggering
7830 // dispatch (infinite loop). The innermost
7831 // (helper-pushed) frame already has its pc
7832 // baked in at compile time, but we still
7833 // assign `cont_pc` below for parity with the
7834 // non-inline path (no-op).
7835 if site_id > 0 {
7836 let idx = (site_id - 1) as usize;
7837 let head_resume_pc = decode_inline[idx].head_resume_pc;
7838 if pre_frames > 0
7839 && let CallFrame::Lua(f) = &mut self.frames[pre_frames - 1]
7840 {
7841 f.pc = head_resume_pc;
7842 }
7843 }
7844 let frames_len_now = self.frames.len();
7845 // 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).
7846 match unsafe { self.frames.last_mut().unwrap_unchecked() } {
7847 CallFrame::Lua(fmut) => {
7848 if crate::jit::trace::v2c_probe_enabled() {
7849 eprintln!(
7850 "[v2c-set-pc] from_side={} sentinel_or_raw={:#018x} prev_pc={} new_cont_pc={} site_id={} frames.len={} pre_frames={} max_stack={}",
7851 from_side_trace,
7852 raw_ret,
7853 fmut.pc,
7854 cont_pc,
7855 site_id,
7856 frames_len_now,
7857 pre_frames,
7858 max_stack,
7859 );
7860 }
7861 fmut.pc = cont_pc;
7862 }
7863 _ => unreachable!("Cont frame at trace dispatch"),
7864 }
7865 // Deferred side-trace start. The
7866 // increment block above flagged this exit's
7867 // hit count crossing HOTEXIT_THRESHOLD; now
7868 // that vm.stack is restored and frame.pc is
7869 // settled, snapshot entry_tags from the
7870 // resume frame's window and create the
7871 // recorder. The recorder's first push fires
7872 // on the next interp iteration at cont_pc.
7873 //
7874 // `head_proto` for the side trace = cl.proto
7875 // (trace JIT only inlines self-recursive
7876 // calls today, so cont_pc always lands in
7877 // the same proto as the parent). Frame base
7878 // is the resume frame (top of `self.frames`
7879 // — inline-pushed frames moved this).
7880 if side_trace_should_start {
7881 let (resume_base, resume_proto) = match self.frames.last() {
7882 Some(CallFrame::Lua(f)) => (f.base as usize, f.closure.proto),
7883 _ => (base_us, cl.proto),
7884 };
7885 let resume_max_stack = resume_proto.max_stack as usize;
7886 let mut side_entry_tags: Vec<u8> = Vec::with_capacity(resume_max_stack);
7887 // Extend stack if cont_pc's frame window
7888 // overhangs the current stack len (rare,
7889 // but inline-pushed frame stack writes
7890 // only covered the trace's writeback).
7891 if self.stack.len() < resume_base + resume_max_stack {
7892 self.stack.resize(
7893 resume_base + resume_max_stack,
7894 crate::runtime::Value::Nil,
7895 );
7896 }
7897 for i in 0..resume_max_stack {
7898 let (tag, _) = self.stack[resume_base + i].unpack();
7899 side_entry_tags.push(tag);
7900 }
7901 self.jit.active_trace =
7902 Some(Box::new(crate::jit::trace::TraceRecord::start_side_trace(
7903 resume_proto,
7904 cont_pc,
7905 side_entry_tags,
7906 cl.proto,
7907 head_pc_val,
7908 exit_hit_idx,
7909 )));
7910 self.jit.recording_frame_base = self.frames.len() - 1;
7911 self.jit.counters.side_trace_started += 1;
7912 }
7913 // Put the dispatch buffers back
7914 // before the `continue;` so the next
7915 // dispatch picks up the same allocation.
7916 self.jit.reg_state_buf = reg_state;
7917 self.jit.entry_tags_buf = entry_tags;
7918 continue;
7919 }
7920 }
7921 // !dispatch_ok / deopt path / non-cont
7922 // exit also restore the buffers before falling
7923 // through to the interp.
7924 self.jit.reg_state_buf = reg_state;
7925 self.jit.entry_tags_buf = entry_tags;
7926 }
7927
7928 // PUC `vmfetch` increments savedpc BEFORE firing traceexec, so
7929 // hook code that consults `currentpc = savedpc - 1` lands on the
7930 // instruction now executing. luna mirrors that by advancing
7931 // `f.pc` to `pc + 1` before the hook block — local_at /
7932 // getinfo / line attribution all read f.pc, and the existing
7933 // `pc - 1` convention in those helpers then yields the current
7934 // instruction's pc (db.lua :696: local `A` visible at the
7935 // chunk's return line once OP_CLOSURE has advanced pc).
7936 //
7937 // Inline `top_frame_mut` for the hot path: top is guaranteed Lua
7938 // (cont frames drained above) so the and_then/Option layers are
7939 // dead weight.
7940 // 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).
7941 match unsafe { self.frames.last_mut().unwrap_unchecked() } {
7942 CallFrame::Lua(fmut) => fmut.pc = pc + 1,
7943 _ => unreachable!("Cont frame at pc bump"),
7944 }
7945
7946 // count + line hooks (PUC traceexec): before executing the
7947 // instruction. Skipped while the hook itself runs.
7948 // (Parens here are load-bearing — without them `&&` binds tighter
7949 // than `||` and the `!in_hook` guard only gates the rust-hook arm,
7950 // letting a Lua line hook recurse into itself → stack overflow
7951 // on db.lua line-hook assertions. Matches the `hook_call_with` /
7952 // `hook_return` predicate shape at lines 2245 / 2279 / 2294 / 4023.)
7953 if !self.in_hook && (self.hook.func.is_some() || self.hook.rust_func.is_some()) {
7954 let lines = &cl.proto.lines;
7955 let cur_line = if lines.is_empty() {
7956 None
7957 } else {
7958 Some(lines[(pc as usize).min(lines.len() - 1)] as i64)
7959 };
7960 // count hook: fire every `count_base` instructions
7961 if self.hook.count {
7962 self.hook.count_left -= 1;
7963 if self.hook.count_left <= 0 {
7964 self.hook.count_left = self.hook.count_base;
7965 // hooked function is the running Lua frame: its frame
7966 // is on the stack, so no synthetic C level is needed.
7967 self.run_hook(b"count", cur_line, false)?;
7968 }
7969 }
7970 // line hook: fire on a fresh frame, a backward jump (loop), or a
7971 // change of source line.
7972 if self.hook.line {
7973 if lines.is_empty() {
7974 // PUC: a stripped chunk has no line info, so
7975 // `getfuncline` returns -1. The line hook still fires
7976 // on the first instruction of the new frame (where
7977 // `npci <= oldpc` holds at oldpc=0), with the line
7978 // pushed as `nil` instead of an integer (db.lua :1030
7979 // "hook called without debug info for 1st instruction").
7980 if oldpc == u32::MAX {
7981 self.run_hook(b"line", None, false)?;
7982 self.top_frame_mut().hook_oldpc = pc;
7983 }
7984 } else {
7985 let newline = lines[(pc as usize).min(lines.len() - 1)];
7986 // PUC `traceexec`: fire on frame entry (`oldpc == MAX`),
7987 // on a backward jump (`pc < oldpc` — strict; an equal pc
7988 // would re-fire the install-site after `oldpc = pc`),
7989 // or when the source line changes.
7990 let fire = oldpc == u32::MAX
7991 || pc < oldpc
7992 || newline != lines[(oldpc as usize).min(lines.len() - 1)];
7993 if fire {
7994 self.run_hook(b"line", Some(newline as i64), false)?;
7995 }
7996 self.top_frame_mut().hook_oldpc = pc;
7997 }
7998 }
7999 }
8000
8001 match inst.op() {
8002 Op::Move => {
8003 let v = self.r(base, inst.b());
8004 self.set_r(base, inst.a(), v);
8005 }
8006 Op::LoadI => self.set_r(base, inst.a(), Value::Int(inst.sbx() as i64)),
8007 Op::LoadF => self.set_r(base, inst.a(), Value::Float(inst.sbx() as f64)),
8008 Op::LoadK => {
8009 let v = cl.proto.consts[inst.bx() as usize];
8010 self.set_r(base, inst.a(), v);
8011 }
8012 Op::LoadKx => {
8013 let extra = cl.proto.code[self.pc_of_top() as usize];
8014 self.bump_pc();
8015 let v = cl.proto.consts[extra.ax() as usize];
8016 self.set_r(base, inst.a(), v);
8017 }
8018 Op::LoadFalse => self.set_r(base, inst.a(), Value::Bool(false)),
8019 Op::LFalseSkip => {
8020 self.set_r(base, inst.a(), Value::Bool(false));
8021 self.bump_pc();
8022 }
8023 Op::LoadTrue => self.set_r(base, inst.a(), Value::Bool(true)),
8024 Op::LoadNil => {
8025 let a = inst.a();
8026 for i in 0..=inst.b() {
8027 self.set_r(base, a + i, Value::Nil);
8028 }
8029 }
8030 Op::GetUpval => {
8031 let v = self.upval_get(cl, inst.b());
8032 self.set_r(base, inst.a(), v);
8033 }
8034 Op::SetUpval => {
8035 let v = self.r(base, inst.a());
8036 self.upval_set(cl, inst.b(), v);
8037 }
8038 Op::GetTabUp => {
8039 let t = self.upval_get(cl, inst.b());
8040 let key = cl.proto.consts[inst.c() as usize];
8041 self.op_index(t, key, base + inst.a())?;
8042 }
8043 Op::GetTable => {
8044 let t = self.r(base, inst.b());
8045 let key = self.r(base, inst.c());
8046 self.op_index(t, key, base + inst.a())?;
8047 }
8048 Op::GetI => {
8049 let t = self.r(base, inst.b());
8050 self.op_index(t, Value::Int(inst.c() as i64), base + inst.a())?;
8051 }
8052 Op::GetField => {
8053 let t = self.r(base, inst.b());
8054 let key = cl.proto.consts[inst.c() as usize];
8055 // Fast path: known-Str const key + no
8056 // metatable on the table → skip `op_index` /
8057 // `index_step`'s MAX_TAG_LOOP setup and the outer
8058 // `Value` match. Falls through to the slow path
8059 // when either invariant breaks (`__index`
8060 // metamethods, non-Table receivers, non-Str keys).
8061 if let Value::Table(tb) = t
8062 && tb.metatable().is_none()
8063 && let Value::Str(s) = key
8064 {
8065 let v = tb.get_str(s);
8066 self.stack[(base + inst.a()) as usize] = v;
8067 } else {
8068 self.op_index(t, key, base + inst.a())?;
8069 }
8070 }
8071 Op::SetTabUp => {
8072 let t = self.upval_get(cl, inst.a());
8073 let key = cl.proto.consts[inst.b() as usize];
8074 let v = self.r(base, inst.c());
8075 self.op_newindex(t, key, v)?;
8076 }
8077 Op::SetTable => {
8078 let t = self.r(base, inst.a());
8079 let key = self.r(base, inst.b());
8080 let v = self.r(base, inst.c());
8081 self.op_newindex(t, key, v)?;
8082 }
8083 Op::SetI => {
8084 let t = self.r(base, inst.a());
8085 let v = self.r(base, inst.c());
8086 self.op_newindex(t, Value::Int(inst.b() as i64), v)?;
8087 }
8088 Op::SetField => {
8089 let t = self.r(base, inst.a());
8090 let key = cl.proto.consts[inst.b() as usize];
8091 let v = self.r(base, inst.c());
8092 self.op_newindex(t, key, v)?;
8093 }
8094 Op::NewTable => {
8095 let t = self.heap.new_table();
8096 self.set_r(base, inst.a(), Value::Table(t));
8097 self.maybe_collect_garbage(base + inst.a() + 1);
8098 }
8099 Op::SetList => {
8100 let a = inst.a();
8101 let abs_a = base + a;
8102 // only `debug.setlocal` or crafted bytecode can put a
8103 // non-table here; PUC crashes, luna raises
8104 let t = match self.r(base, a) {
8105 Value::Table(t) => t,
8106 v => return Err(self.type_err("index", v)),
8107 };
8108 let n = if inst.b() == 0 {
8109 self.top - (abs_a + 1)
8110 } else {
8111 inst.b()
8112 };
8113 let offset = if inst.k() {
8114 let extra = cl.proto.code[self.pc_of_top() as usize];
8115 self.bump_pc();
8116 extra.ax() as i64
8117 } else {
8118 inst.c() as i64
8119 };
8120 for i in 1..=n {
8121 let v = self.r(base, a + i);
8122 // 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).
8123 if let Err(TableError::Overflow) =
8124 unsafe { t.as_mut() }.set_int(&mut self.heap, offset + i as i64, v)
8125 {
8126 return Err(self.rt_err("table overflow"));
8127 }
8128 }
8129 // one barrier_back covers every store this op did — PUC's
8130 // `luaC_barrierback_` once-per-table optimisation
8131 self.heap
8132 .barrier_back(t.as_ptr() as *mut crate::runtime::heap::GcHeader);
8133 // the element temps above the table are now consumed
8134 self.maybe_collect_garbage(base + a + 1);
8135 }
8136 Op::SelfOp => {
8137 let o = self.r(base, inst.b());
8138 self.set_r(base, inst.a() + 1, o);
8139 // PUC OP_SELF's C is a constant index when the k-flag is
8140 // set; otherwise it points to a register that holds the
8141 // (constant-loaded) key. luna's compiler falls back to the
8142 // register form when the constant index exceeds OP_SELF's
8143 // 8-bit C field (5.1 big.lua's `a:findfield(...)` against
8144 // a table with 250+ string keys, where "findfield" lands
8145 // past const #255). The exec must honour the same split.
8146 let key = if inst.k() {
8147 cl.proto.consts[inst.c() as usize]
8148 } else {
8149 self.r(base, inst.c())
8150 };
8151 self.op_index(o, key, base + inst.a())?;
8152 }
8153 Op::Add => self.arith_rr(inst, base, ArithOp::Add)?,
8154 Op::Sub => self.arith_rr(inst, base, ArithOp::Sub)?,
8155 Op::Mul => self.arith_rr(inst, base, ArithOp::Mul)?,
8156 Op::Mod => self.arith_rr(inst, base, ArithOp::Mod)?,
8157 Op::Pow => self.arith_rr(inst, base, ArithOp::Pow)?,
8158 Op::Div => self.arith_rr(inst, base, ArithOp::Div)?,
8159 Op::IDiv => self.arith_rr(inst, base, ArithOp::IDiv)?,
8160 Op::BAnd => self.arith_rr(inst, base, ArithOp::BAnd)?,
8161 Op::BOr => self.arith_rr(inst, base, ArithOp::BOr)?,
8162 Op::BXor => self.arith_rr(inst, base, ArithOp::BXor)?,
8163 Op::Shl => self.arith_rr(inst, base, ArithOp::Shl)?,
8164 Op::Shr => self.arith_rr(inst, base, ArithOp::Shr)?,
8165 Op::Unm => {
8166 let v = self.r(base, inst.b());
8167 match self.unary_operand(v) {
8168 Some(Num::Int(i)) => {
8169 self.set_r(base, inst.a(), Value::Int(i.wrapping_neg()))
8170 }
8171 Some(Num::Float(f)) => self.set_r(base, inst.a(), Value::Float(-f)),
8172 None => {
8173 let mm = self.get_mm(v, Mm::Unm);
8174 if mm.is_nil() {
8175 return Err(self.type_err("perform arithmetic on", v));
8176 }
8177 let dst = base + inst.a();
8178 self.begin_meta_call(mm, &[v, v], MetaAction::Store { dst }, "unm")?;
8179 }
8180 }
8181 }
8182 Op::BNot => {
8183 let v = self.r(base, inst.b());
8184 match self.arith_operand()(v) {
8185 Some(n) => {
8186 let Some(i) = int_of(n) else {
8187 return Err(self.no_int_rep_err());
8188 };
8189 self.set_r(base, inst.a(), Value::Int(!i));
8190 }
8191 None => {
8192 let mm = self.get_mm(v, Mm::BNot);
8193 if mm.is_nil() {
8194 return Err(self.type_err("perform bitwise operation on", v));
8195 }
8196 let dst = base + inst.a();
8197 self.begin_meta_call(mm, &[v, v], MetaAction::Store { dst }, "bnot")?;
8198 }
8199 }
8200 }
8201 Op::Not => {
8202 let v = self.r(base, inst.b());
8203 self.set_r(base, inst.a(), Value::Bool(!v.truthy()));
8204 }
8205 Op::Len => {
8206 let v = self.r(base, inst.b());
8207 match self.len_step(v)? {
8208 MmOut::Done(r) => self.set_r(base, inst.a(), r),
8209 MmOut::Mm { func, recv } => {
8210 let dst = base + inst.a();
8211 self.begin_meta_call(
8212 func,
8213 &[recv, recv],
8214 MetaAction::Store { dst },
8215 "len",
8216 )?;
8217 }
8218 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from len_step"),
8219 }
8220 }
8221 Op::Concat => {
8222 // right-associative fold over operands at base+a .. base+a+n,
8223 // in place on the stack so a yielding __concat can suspend.
8224 let a = inst.a();
8225 let n = inst.b();
8226 self.top = base + a + n;
8227 self.concat_run(base + a)?;
8228 }
8229 Op::Close => {
8230 // Yieldable: drive __close handlers through the
8231 // interpreter loop so a coroutine.yield() inside a
8232 // handler suspends cleanly (locals.lua block-end yield).
8233 // `drive_close` parks the handler call at `self.top`, so
8234 // raise `top` past this frame's full register window
8235 // first — a goto out of a nested for-loop can fire
8236 // OP_Close while `self.top` still sits at the inner
8237 // body's working top, which would let `push_frame`'s
8238 // wipe clobber the outer tbc slot before it could be
8239 // closed (locals.lua:1219 nested-for goto regression).
8240 self.top = self.top.max(base + cl.proto.max_stack as u32);
8241 let _ =
8242 self.begin_close(base + inst.a(), None, AfterClose::Block, entry_depth)?;
8243 }
8244 Op::Tbc => {
8245 self.register_tbc(base + inst.a())?;
8246 }
8247 Op::Jmp => {
8248 let off = inst.sj();
8249 // Trace JIT back-edge counter. A negative
8250 // jump offset is a loop back-edge (the only canonical
8251 // backward jumps the compiler emits — `while`, `for`,
8252 // `repeat`). Tick the per-Proto counter and, once it
8253 // exceeds the threshold, start a trace recording. The
8254 // whole block is gated on `trace_jit_enabled` so
8255 // existing benches see one branch-not-taken and no
8256 // counter writes.
8257 if self.jit.trace_enabled && off < 0 {
8258 let proto = cl.proto;
8259 let c = proto.trace_hot_count.get();
8260 if c < u32::MAX / 2 {
8261 proto.trace_hot_count.set(c + 1);
8262 }
8263 // Relaxed back-edge trigger:
8264 // `c >= THRESHOLD` (not `c == THRESHOLD`) so
8265 // a missed crossing (active_trace busy with
8266 // a call-trigger, or the recorder slot
8267 // happened to be in use) doesn't permanently
8268 // lock this back-edge target out. The
8269 // `already_cached` short-circuit prevents
8270 // duplicate recordings: once a trace is
8271 // cached for this target, subsequent
8272 // crossings skip the start. This pairs with
8273 // the discard-on-partial-coverage close
8274 // handling — when a short call-trigger is
8275 // discarded, the back-edge can still find an
8276 // open slot at the next iteration.
8277 let target_pc = (pc as i32 + 1 + off as i32).max(0) as u32;
8278 // Gave-up short-circuit. Skip
8279 // the RefCell borrow + scan when the
8280 // discard cap force-compiled a partial
8281 // trace on this Proto.
8282 let back_edge_already_cached = if proto.trace_gave_up.get() {
8283 true
8284 } else {
8285 proto.traces.borrow().iter().any(|t| t.head_pc == target_pc)
8286 || trace_head_abandoned(proto, target_pc)
8287 };
8288 if c >= self.jit.trace_hot_threshold
8289 && self.jit.active_trace.is_none()
8290 && !back_edge_already_cached
8291 {
8292 // Back-edge target = pc after `add_pc(off)`,
8293 // i.e. current `pc + 1 + off` (the dispatch
8294 // loop has already advanced f.pc to pc+1).
8295 let target = (pc as i32 + 1 + off as i32).max(0) as u32;
8296 // Snapshot per-slot Value tag at trace
8297 // entry so the lowerer's kind tracker
8298 // knows which arith path to lower
8299 // (iadd vs fadd, etc.).
8300 let max_stack = cl.proto.max_stack as usize;
8301 let base_us = base as usize;
8302 let mut entry_tags = Vec::with_capacity(max_stack);
8303 for i in 0..max_stack {
8304 let (tag, _) = self.stack[base_us + i].unpack();
8305 entry_tags.push(tag);
8306 }
8307 self.jit.active_trace =
8308 Some(Box::new(crate::jit::trace::TraceRecord::start(
8309 cl.proto, target, entry_tags, false,
8310 )));
8311 // Record the frame the trace
8312 // started in. `self.frames.len() - 1`
8313 // since we're inside the currently-running
8314 // Lua frame's dispatch.
8315 self.jit.recording_frame_base = self.frames.len() - 1;
8316 }
8317 }
8318 self.add_pc(off);
8319 }
8320 Op::Eq => {
8321 let l = self.r(base, inst.a());
8322 let r = self.r(base, inst.b());
8323 if let (Value::Int(a), Value::Int(b)) = (l, r) {
8324 if (a == b) != inst.k() {
8325 self.bump_pc();
8326 }
8327 } else {
8328 let step = self.eq_step(l, r);
8329 self.op_compare(step, l, r, inst.k(), "eq")?;
8330 }
8331 }
8332 Op::EqK => {
8333 let l = self.r(base, inst.a());
8334 let r = cl.proto.consts[inst.b() as usize];
8335 if let (Value::Int(a), Value::Int(b)) = (l, r) {
8336 if (a == b) != inst.k() {
8337 self.bump_pc();
8338 }
8339 } else {
8340 let step = self.eq_step(l, r);
8341 self.op_compare(step, l, r, inst.k(), "eq")?;
8342 }
8343 }
8344 Op::Lt => {
8345 let l = self.r(base, inst.a());
8346 let r = self.r(base, inst.b());
8347 // hot path: Int < Int — drops the MmOut + op_compare match
8348 if let (Value::Int(a), Value::Int(b)) = (l, r) {
8349 if (a < b) != inst.k() {
8350 self.bump_pc();
8351 }
8352 } else {
8353 let step = self.less_step(l, r, false)?;
8354 self.op_compare(step, l, r, inst.k(), "lt")?;
8355 }
8356 }
8357 Op::Le => {
8358 let l = self.r(base, inst.a());
8359 let r = self.r(base, inst.b());
8360 if let (Value::Int(a), Value::Int(b)) = (l, r) {
8361 if (a <= b) != inst.k() {
8362 self.bump_pc();
8363 }
8364 } else {
8365 let step = self.less_step(l, r, true)?;
8366 self.op_compare(step, l, r, inst.k(), "le")?;
8367 }
8368 }
8369 Op::Test => {
8370 let cond = self.r(base, inst.a()).truthy();
8371 self.cond_skip(cond, inst.k());
8372 }
8373 Op::TestSet => {
8374 let v = self.r(base, inst.b());
8375 if v.truthy() == inst.k() {
8376 self.set_r(base, inst.a(), v);
8377 } else {
8378 self.bump_pc();
8379 }
8380 }
8381 Op::Call => {
8382 let abs = base + inst.a();
8383 let nargs = if inst.b() == 0 {
8384 None
8385 } else {
8386 Some(inst.b() - 1)
8387 };
8388 let wanted = inst.c() as i32 - 1;
8389 self.begin_call(abs, nargs, wanted, false)?;
8390 }
8391 Op::TailCall => {
8392 let fr = *self.top_frame();
8393 let abs = base + inst.a();
8394 let mut nargs = if inst.b() == 0 {
8395 self.top - (abs + 1)
8396 } else {
8397 inst.b() - 1
8398 };
8399 // A tail call pops this frame before begin_call, so a
8400 // non-callable target would lose its name/position. Report
8401 // it now (PUC reads funcname from the still-current ci),
8402 // while the frame is intact, for "(field 'x')"-style info.
8403 let mut func = self.stack[abs as usize];
8404 if !matches!(func, Value::Closure(_) | Value::Native(_))
8405 && self.get_mm(func, Mm::Call).is_nil()
8406 {
8407 return Err(self.call_err(func));
8408 }
8409 // PUC `luaD_pretailcall` resolves a chain of `__call`
8410 // metamethods *in place* before deciding whether to
8411 // collapse this frame. Without that, each __call hop
8412 // would push a fresh Lua frame and a 10000-deep
8413 // tail-recursion through a 100-deep __call chain
8414 // (5.4 calls.lua :172) blows up. Mirror the PUC loop:
8415 // shift args right, install the handler at `abs`, retry.
8416 // Chain depth limit matches the call-site `begin_call`
8417 // version cap (5.5 calls.lua :223 — 15 max, then "too
8418 // long"; 16th wrap fails the call). An infinite
8419 // self-referential `__call` would otherwise spin.
8420 let chain_cap = if self.version >= LuaVersion::Lua55 {
8421 15
8422 } else {
8423 MAX_CCMT
8424 };
8425 let mut chain = 0u32;
8426 while !matches!(func, Value::Closure(_) | Value::Native(_)) {
8427 let mm = self.get_mm(func, Mm::Call);
8428 if mm.is_nil() || self.call_mm_unusable(mm) {
8429 return Err(self.call_err(func));
8430 }
8431 chain += 1;
8432 if chain > chain_cap {
8433 return Err(self.rt_err("'__call' chain too long"));
8434 }
8435 let end = (abs + 1 + nargs) as usize;
8436 if self.stack.len() < end + 1 {
8437 self.stack.resize(end + 1, Value::Nil);
8438 }
8439 for i in (0..=nargs).rev() {
8440 self.stack[(abs + 1 + i) as usize] = self.stack[(abs + i) as usize];
8441 }
8442 self.stack[abs as usize] = mm;
8443 nargs += 1;
8444 self.top = abs + 1 + nargs;
8445 func = mm;
8446 }
8447 // PUC's tail-call collapse is Lua→Lua only. A tail call to
8448 // a C function runs the C function under the *current* Lua
8449 // activation (no frame fold — a C frame has nothing to
8450 // collapse into); after the C function returns, the
8451 // calling Lua function returns those results normally.
8452 // Mirror that: keep our Lua frame on the stack, call the
8453 // target through `begin_call(abs, …)` as a regular call,
8454 // and let the fallback `Op::Return` that the compiler
8455 // emits right after `Op::TailCall` forward the results.
8456 // 5.1 closure.lua :177's `return getfenv()` from inside
8457 // foo needs level 1 to resolve to foo, not to the
8458 // thread's globals fallback that happens when no Lua
8459 // frame is on the stack.
8460 let lua_target = matches!(func, Value::Closure(_));
8461 if lua_target {
8462 self.close_slots(fr.base, None)?;
8463 for i in 0..=nargs {
8464 self.stack[(fr.func_slot + i) as usize] =
8465 self.stack[(abs + i) as usize];
8466 }
8467 // Clear the slot range that's now
8468 // stranded by the tail-call collapse. The args
8469 // were copied to `[fr.func_slot..fr.func_slot+
8470 // nargs+1)`; the source slots `[abs..abs+
8471 // nargs+1)` still hold the same `Value::Closure
8472 // / Value::Str / ...` entries, but they're past
8473 // the new call's window. Without this clear, a
8474 // later GC with wider gc_top would mark stale
8475 // pointers there (same hazard the
8476 // finish_results slot-clear closes for the
8477 // Op::Return path).
8478 let new_top_lower_bound = fr.func_slot + nargs + 1;
8479 let prev_top = (self.top as usize).min(self.stack.len());
8480 if (new_top_lower_bound as usize) < prev_top {
8481 for slot in &mut self.stack[new_top_lower_bound as usize..prev_top] {
8482 *slot = Value::Nil;
8483 }
8484 }
8485 // PUC `CIST_TAIL`: the new Lua activation inherits
8486 // the popped frame's tailcalls count plus one for
8487 // this collapse. 5.1 db.lua :372 hammers 30000
8488 // recursive tail calls and expects to see the
8489 // synthetic tail level for every one of them.
8490 self.pending_tailcalls = fr.tailcalls.saturating_add(1);
8491 self.pending_ccmt = self.frame_ccmt[self.frames.len() - 1];
8492 frames_pop_sync(&mut self.frames, &mut self.frames_top);
8493 if !self.begin_call(fr.func_slot, Some(nargs), fr.nresults, false)?
8494 && self.frames.len() < entry_depth
8495 {
8496 // a native completed what was this function's result
8497 return Ok(self.take_results(fr.func_slot));
8498 }
8499 } else {
8500 // Native (or __call-bearing) target: regular call. The
8501 // results land at `abs..self.top` and the next op (the
8502 // fallback `Op::Return`) forwards them. `wanted = -1`
8503 // because the caller will multret them through Return.
8504 // PUC's precallC gives the C call this tail call's own
8505 // `__call` count (5.5 extraargs); the chain was already
8506 // resolved above, so hand it over.
8507 self.pending_ccmt = chain as u8;
8508 self.begin_call(abs, Some(nargs), -1, false)?;
8509 }
8510 }
8511 Op::Return | Op::Return0 | Op::Return1 => {
8512 let (abs_a, nret) = match inst.op() {
8513 Op::Return0 => (base, 0),
8514 Op::Return1 => (base + inst.a(), 1),
8515 _ => {
8516 let abs_a = base + inst.a();
8517 let nret = if inst.b() == 0 {
8518 self.top - abs_a
8519 } else {
8520 inst.b() - 1
8521 };
8522 (abs_a, nret)
8523 }
8524 };
8525 // close before moving results: __close handlers run above
8526 // the stack top, so the result region [abs_a..abs_a+nret)
8527 // stays intact across any yields the close performs.
8528 // Fixed-count returns may leave `self.top` below the last
8529 // result slot (the compiler does not always re-bump it);
8530 // raise it past the result region so `drive_close` parks
8531 // the handler call *above* — landing at `self.top` would
8532 // otherwise clobber a result with the handler closure.
8533 self.top = self.top.max(abs_a + nret);
8534 if let Some(vals) = self.begin_close(
8535 base,
8536 None,
8537 AfterClose::Return {
8538 abs_a,
8539 nret,
8540 from_native: false,
8541 },
8542 entry_depth,
8543 )? {
8544 return Ok(vals);
8545 }
8546 }
8547 Op::ForPrep => self.for_prep(inst, base)?,
8548 Op::ForLoop => {
8549 // Trace JIT back-edge counter on the
8550 // numeric-for back-edge. ForLoop is always at
8551 // a back-edge position (when it continues);
8552 // for the trace recorder we treat it as the
8553 // close-detection equivalent of `Op::Jmp` with
8554 // negative offset. Counter only ticks when the
8555 // back-edge will actually fire (count > 0 in
8556 // the 5.4+ Int form, comparable predicates in
8557 // pre-5.3 / Float). The cheap check up front
8558 // matches the for_loop helper's branch.
8559 if self.jit.trace_enabled {
8560 let a = inst.a();
8561 let pre53 = self.version() <= LuaVersion::Lua53;
8562 let take_back_edge =
8563 match (self.r(base, a), self.r(base, a + 1), self.r(base, a + 2)) {
8564 (Value::Int(_), Value::Int(count), Value::Int(_)) if !pre53 => {
8565 count != 0
8566 }
8567 (Value::Int(cur), Value::Int(lim), Value::Int(st)) if pre53 => {
8568 let next = cur.wrapping_add(st);
8569 if st > 0 { next <= lim } else { next >= lim }
8570 }
8571 (Value::Float(cur), Value::Float(lim), Value::Float(st)) => {
8572 let next = cur + st;
8573 if st > 0.0 { next <= lim } else { next >= lim }
8574 }
8575 _ => false,
8576 };
8577 if take_back_edge {
8578 let proto = cl.proto;
8579 let c = proto.trace_hot_count.get();
8580 if c < u32::MAX / 2 {
8581 proto.trace_hot_count.set(c + 1);
8582 }
8583 if c == self.jit.trace_hot_threshold && self.jit.active_trace.is_none()
8584 {
8585 // ForLoop's back-edge target = pc
8586 // after `add_pc(-bx)` runs from the
8587 // already-bumped f.pc (= pc + 1).
8588 // So target = (pc + 1) - bx.
8589 let target = (pc as i32 + 1 - inst.bx() as i32).max(0) as u32;
8590 let max_stack = cl.proto.max_stack as usize;
8591 let base_us = base as usize;
8592 let mut entry_tags = Vec::with_capacity(max_stack);
8593 for i in 0..max_stack {
8594 let (tag, _) = self.stack[base_us + i].unpack();
8595 entry_tags.push(tag);
8596 }
8597 self.jit.active_trace =
8598 Some(Box::new(crate::jit::trace::TraceRecord::start(
8599 cl.proto, target, entry_tags, false,
8600 )));
8601 // Record the frame the trace
8602 // started in. The currently-running
8603 // Lua frame is at len() - 1.
8604 self.jit.recording_frame_base = self.frames.len() - 1;
8605 }
8606 }
8607 }
8608 self.for_loop(inst, base)?;
8609 }
8610 Op::TForPrep => {
8611 // the 4th control slot is the iterator's closing value
8612 self.register_tbc(base + inst.a() + 3)?;
8613 self.add_pc(inst.bx() as i32);
8614 }
8615 Op::TForCall => {
8616 let abs = base + inst.a();
8617 let need = (abs + 7) as usize;
8618 if self.stack.len() < need {
8619 self.stack.resize(need, Value::Nil);
8620 }
8621 self.stack[(abs + 4) as usize] = self.stack[abs as usize];
8622 self.stack[(abs + 5) as usize] = self.stack[(abs + 1) as usize];
8623 self.stack[(abs + 6) as usize] = self.stack[(abs + 2) as usize];
8624 let nvars = inst.c() as i32;
8625 self.begin_call(abs + 4, Some(2), nvars, false)?;
8626 }
8627 Op::TForLoop => {
8628 let a = inst.a();
8629 let ctrl = self.r(base, a + 4);
8630 if !ctrl.is_nil() {
8631 // Trace JIT back-edge counter on
8632 // generic-for back-edge. TForLoop sits at the
8633 // tail of `for k,v in expr do ... end`; recorder
8634 // treats it as the close-detection equivalent of
8635 // a negative Op::Jmp. Gate on `take_back_edge`
8636 // (= `ctrl != nil`) so empty-iter loops don't
8637 // pollute hot_count.
8638 if self.jit.trace_enabled {
8639 let proto = cl.proto;
8640 let c = proto.trace_hot_count.get();
8641 if c < u32::MAX / 2 {
8642 proto.trace_hot_count.set(c + 1);
8643 }
8644 if c == self.jit.trace_hot_threshold && self.jit.active_trace.is_none()
8645 {
8646 // TForLoop back-edge target = pc after
8647 // `add_pc(-bx)` runs from the already-
8648 // bumped f.pc (= pc + 1). So target =
8649 // (pc + 1) - bx, normally landing on
8650 // body_top (the op right after TForPrep).
8651 let target = (pc as i32 + 1 - inst.bx() as i32).max(0) as u32;
8652 let max_stack = cl.proto.max_stack as usize;
8653 let base_us = base as usize;
8654 let mut entry_tags = Vec::with_capacity(max_stack);
8655 for i in 0..max_stack {
8656 let (tag, _) = self.stack[base_us + i].unpack();
8657 entry_tags.push(tag);
8658 }
8659 // Snapshot the iter
8660 // fn's address if Native, so the
8661 // lowerer can specialise ipairs into
8662 // inline Table aget IR.
8663 let iter_ptr =
8664 if let Value::Native(n) = self.stack[base_us + a as usize] {
8665 Some(n.f as usize)
8666 } else {
8667 None
8668 };
8669 // Snapshot R[A+5]'s
8670 // tag (= current iter's val from
8671 // the just-fired TForCall). The
8672 // inline aget fast_blk emits a
8673 // runtime guard against this tag;
8674 // mixed-tag arrays deopt rather
8675 // than producing garbage pointers
8676 // through the spill path.
8677 let val_slot = base_us + (a as usize) + 5;
8678 let val_tag = if val_slot < self.stack.len() {
8679 Some(self.stack[val_slot].unpack().0)
8680 } else {
8681 None
8682 };
8683 let mut rec = crate::jit::trace::TraceRecord::start(
8684 cl.proto, target, entry_tags, false,
8685 );
8686 rec.tfor_iter_ptr = iter_ptr;
8687 rec.tfor_val_tag = val_tag;
8688 self.jit.active_trace = Some(Box::new(rec));
8689 self.jit.recording_frame_base = self.frames.len() - 1;
8690 }
8691 }
8692 self.set_r(base, a + 2, ctrl);
8693 self.add_pc(-(inst.bx() as i32));
8694 }
8695 }
8696 Op::Closure => {
8697 let proto = cl.proto.protos[inst.bx() as usize];
8698 let n_ups = proto.upvals.len();
8699 // Build upvals on the stack for small
8700 // closures, skipping the per-call Vec/Box alloc
8701 // that closure_alloc's 10k iters pay. INLINE_UPVALS_N
8702 // = 2 covers most Lua source (1 captured local, or
8703 // _ENV + a single capture). Beyond that, fall back
8704 // to a heap Vec.
8705 use crate::runtime::function::INLINE_UPVALS_N;
8706 let mut stack_buf: [std::mem::MaybeUninit<
8707 Gc<crate::runtime::function::Upvalue>,
8708 >; INLINE_UPVALS_N] = [std::mem::MaybeUninit::uninit(); INLINE_UPVALS_N];
8709 let mut heap_buf: Vec<Gc<crate::runtime::function::Upvalue>> = Vec::new();
8710 let use_inline = n_ups <= INLINE_UPVALS_N;
8711 if !use_inline {
8712 heap_buf.reserve_exact(n_ups);
8713 }
8714 for (i, d) in proto.upvals.iter().enumerate() {
8715 let uv = if d.in_stack {
8716 self.find_or_create_upval(base + d.index as u32)
8717 } else {
8718 cl.upvals()[d.index as usize]
8719 };
8720 if use_inline {
8721 stack_buf[i] = std::mem::MaybeUninit::new(uv);
8722 } else {
8723 heap_buf.push(uv);
8724 }
8725 }
8726 // Tiny shim around the two paths so the 5.1 _ENV
8727 // clone + cache check below see one uniform
8728 // `&mut [Gc<Upvalue>]`. The stack_buf slice points
8729 // into the local frame (still valid through the
8730 // rest of this Op::Closure handler).
8731 let ups: &mut [Gc<crate::runtime::function::Upvalue>] = if use_inline {
8732 // SAFETY: the first n_ups slots of stack_buf
8733 // were initialised above; we hand out a slice
8734 // covering exactly them.
8735 unsafe {
8736 std::slice::from_raw_parts_mut(
8737 stack_buf.as_mut_ptr()
8738 as *mut Gc<crate::runtime::function::Upvalue>,
8739 n_ups,
8740 )
8741 }
8742 } else {
8743 &mut heap_buf[..]
8744 };
8745 // PUC 5.1 had per-function environments: every Lua
8746 // function carried its own `env` slot, snapshotted from
8747 // the creating function's env at closure time, so a
8748 // `setfenv` on one closure never bled into a sibling.
8749 // luna models that by giving the 5.1 closure a *fresh*
8750 // closed upvalue for whichever cell holds `_ENV`, seeded
8751 // from the parent's current env value. Only that cell is
8752 // cloned — every other upvalue keeps its open/shared
8753 // identity (so e.g. `local function range(...) ...
8754 // range(...) ... end` still sees its self-reference). 5.2+
8755 // keeps the shared-upval model (and the proto cache that
8756 // depends on it).
8757 let v51 = self.version() <= LuaVersion::Lua51;
8758 if v51 && proto.env_upval_idx != u8::MAX {
8759 let i = proto.env_upval_idx as usize;
8760 let cur = match ups[i].state() {
8761 UpvalState::Open { slot, thread } => self.read_slot(slot, thread),
8762 UpvalState::Closed(v) => v,
8763 };
8764 ups[i] = self.heap.new_upvalue(UpvalState::Closed(cur));
8765 }
8766 let ups_slice: &[Gc<crate::runtime::function::Upvalue>] = ups;
8767 // PUC 5.2+ `getcached`: a Proto remembers its last LClosure
8768 // and reuses it when every fresh-upvalue binding still
8769 // points to the same Upvalue object as the cached one.
8770 // That keeps `function() return outer end` repeated in a
8771 // loop comparing equal across iterations (the captured
8772 // outer is a shared open upvalue), while `function()
8773 // return loop_var end` gets a fresh closure each round
8774 // because the loop var is re-created per iteration. PUC
8775 // 5.1 predated the cache, and the per-closure `_ENV`
8776 // clone above would defeat it anyway, so skip it.
8777 let nc = if v51 {
8778 self.heap.new_closure_inline(proto, ups_slice)
8779 } else {
8780 let cached = proto.cache.get().filter(|c| {
8781 c.upvals().len() == ups_slice.len()
8782 && c.upvals()
8783 .iter()
8784 .zip(ups_slice.iter())
8785 .all(|(a, b)| std::ptr::eq(a.as_ptr(), b.as_ptr()))
8786 });
8787 match cached {
8788 Some(c) => c,
8789 None => {
8790 let n = self.heap.new_closure_inline(proto, ups_slice);
8791 proto.cache.set(Some(n));
8792 n
8793 }
8794 }
8795 };
8796 self.set_r(base, inst.a(), Value::Closure(nc));
8797 self.maybe_collect_garbage(base + inst.a() + 1);
8798 }
8799 Op::Vararg => {
8800 let abs_a = base + inst.a();
8801 let wanted = inst.c() as i32 - 1;
8802 // A materialized named vararg lives in func_slot (its writes
8803 // must be visible to `...`); otherwise spread the extra args
8804 // straight off the stack at func_slot+1 .. +n_varargs.
8805 let vt = match self.stack[func_slot as usize] {
8806 Value::Table(t) => Some(t),
8807 _ => None,
8808 };
8809 let n = match vt {
8810 Some(t) => {
8811 let n_key = Value::Str(self.heap.intern(b"n"));
8812 // PUC getnumargs: a named vararg `t.n` set out of the
8813 // integer range [0, INT_MAX/2] is rejected here
8814 match t.get(n_key) {
8815 Value::Int(n) if (n as u64) <= (i32::MAX as u64 / 2) => n as u32,
8816 _ => return Err(self.rt_err("vararg table has no proper 'n'")),
8817 }
8818 }
8819 None => n_varargs,
8820 };
8821 let count = if wanted < 0 { n } else { wanted as u32 };
8822 // a named vararg's `n` can be set to anything up to
8823 // INT_MAX/2; PUC's `luaD_checkstack` refuses what the
8824 // stack cannot hold
8825 if abs_a + count > MAX_LUA_STACK {
8826 return Err(self.rt_err("stack overflow"));
8827 }
8828 let need = (abs_a + count) as usize;
8829 if self.stack.len() < need {
8830 self.stack.resize(need, Value::Nil);
8831 }
8832 for i in 0..count {
8833 let v = if i >= n {
8834 Value::Nil
8835 } else if let Some(t) = vt {
8836 t.get_int(i as i64 + 1)
8837 } else {
8838 self.stack[(func_slot + 1 + i) as usize]
8839 };
8840 self.stack[(abs_a + i) as usize] = v;
8841 }
8842 if wanted < 0 {
8843 self.top = abs_a + count;
8844 }
8845 }
8846 Op::GetVarg => {
8847 // materialize the vararg table (PUC table.pack shape) from the
8848 // stack varargs — used when the named vararg is written /
8849 // escapes / is `_ENV`. It is kept BOTH in func_slot (so `...`
8850 // sees later writes) and in the local register R[A].
8851 let n = n_varargs;
8852 let t = self.heap.new_table();
8853 {
8854 // 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).
8855 let tm = unsafe { t.as_mut() };
8856 for i in 0..n {
8857 let _ = tm.set_int(
8858 &mut self.heap,
8859 i as i64 + 1,
8860 self.stack[(func_slot + 1 + i) as usize],
8861 );
8862 }
8863 }
8864 let n_key = Value::Str(self.heap.intern(b"n"));
8865 // 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).
8866 unsafe { t.as_mut() }
8867 .set(&mut self.heap, n_key, Value::Int(n as i64))
8868 .expect("'n' is a valid key");
8869 // once-per-table barrier (mirror SETLIST): t is born BLACK
8870 // during Propagate; the bulk inserts above don't barrier.
8871 self.heap
8872 .barrier_back(t.as_ptr() as *mut crate::runtime::heap::GcHeader);
8873 self.stack[func_slot as usize] = Value::Table(t);
8874 self.set_r(base, inst.a(), Value::Table(t));
8875 }
8876 Op::VargIdx => {
8877 // R[A] := vararg[R[C]] without allocating: integer key in
8878 // [1,n] → that vararg, "n" → the count, else nil.
8879 let key = self.r(base, inst.c());
8880 let n = n_varargs;
8881 let v = match key {
8882 Value::Int(k) if k >= 1 && (k as u64) <= n as u64 => {
8883 self.stack[(func_slot + k as u32) as usize]
8884 }
8885 Value::Float(f) if f.fract() == 0.0 && f >= 1.0 && f <= n as f64 => {
8886 self.stack[(func_slot + f as u32) as usize]
8887 }
8888 Value::Str(s) if s.as_bytes() == b"n" => Value::Int(n as i64),
8889 _ => Value::Nil,
8890 };
8891 self.set_r(base, inst.a(), v);
8892 }
8893 Op::ErrNNil => {
8894 let v = self.r(base, inst.a());
8895 if !matches!(v, Value::Nil) {
8896 let bx = inst.bx();
8897 let name = if bx == 0 {
8898 "?".to_string()
8899 } else {
8900 match cl.proto.consts[(bx - 1) as usize] {
8901 Value::Str(s) => String::from_utf8_lossy(s.as_bytes()).into_owned(),
8902 _ => "?".to_string(),
8903 }
8904 };
8905 return Err(self.rt_err(&format!("global '{name}' already defined")));
8906 }
8907 }
8908 Op::ExtraArg => unreachable!("EXTRAARG executed directly"),
8909 }
8910 }
8911 }
8912
8913 #[inline(always)]
8914 fn pc_of_top(&self) -> u32 {
8915 self.top_frame().pc
8916 }
8917
8918 #[inline(always)]
8919 fn bump_pc(&mut self) {
8920 // Inline `top_frame_mut`: top is guaranteed Lua (continuation frames
8921 // drained at dispatch loop head). Avoids the and_then/lua_mut Option
8922 // layers — bump_pc fires per Jmp / cond_skip miss, so the savings add
8923 // up over `fib_28`'s ~500k jumps.
8924 // 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).
8925 match unsafe { self.frames.last_mut().unwrap_unchecked() } {
8926 CallFrame::Lua(f) => f.pc += 1,
8927 _ => unreachable!("Cont frame at bump_pc"),
8928 }
8929 }
8930
8931 #[inline(always)]
8932 fn add_pc(&mut self, d: i32) {
8933 // 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).
8934 match unsafe { self.frames.last_mut().unwrap_unchecked() } {
8935 CallFrame::Lua(f) => f.pc = (f.pc as i64 + d as i64) as u32,
8936 _ => unreachable!("Cont frame at add_pc"),
8937 }
8938 }
8939
8940 /// PUC conditional-skip convention: the JMP that follows is executed when
8941 /// `cond == k`; otherwise it is skipped.
8942 #[inline(always)]
8943 fn cond_skip(&mut self, cond: bool, k: bool) {
8944 if cond != k {
8945 self.bump_pc();
8946 }
8947 }
8948
8949 // ---- indexing (with __index/__newindex chains) ----
8950
8951 /// The `#` length operation: string byte length, `__len` if present, else
8952 /// the raw table border. Returns the raw length value (may be non-integer
8953 /// when `__len` is exotic).
8954 pub(crate) fn len_value(&mut self, v: Value) -> Result<Value, LuaError> {
8955 match self.len_step(v)? {
8956 MmOut::Done(n) => Ok(n),
8957 // PUC calls unary metamethods with the operand twice
8958 MmOut::Mm { func, recv } => self.call_mm1(func, &[recv, recv]),
8959 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from len_step"),
8960 }
8961 }
8962
8963 /// Length fast path: a string's byte count or a table's raw border when no
8964 /// `__len` is present (`Done`); otherwise the `__len` metamethod (`Mm`),
8965 /// called with the operand twice. Errors for a non-table with no `__len`.
8966 fn len_step(&mut self, v: Value) -> Result<MmOut, LuaError> {
8967 match v {
8968 Value::Str(s) => Ok(MmOut::Done(Value::Int(s.len() as i64))),
8969 Value::Table(t) => {
8970 // PUC 5.1's `__len` applies to userdata only — `luaV_objlen`
8971 // there takes the raw border for a table without consulting
8972 // the metatable, so `#setmetatable({}, {__len = f})` is 0 on
8973 // 5.1 and 7 on 5.2+. Verified against stock 5.1.5 / 5.2.4.
8974 if self.version() == crate::version::LuaVersion::Lua51 {
8975 return Ok(MmOut::Done(Value::Int(t.len())));
8976 }
8977 let mm = self.get_mm(v, Mm::Len);
8978 if mm.is_nil() {
8979 Ok(MmOut::Done(Value::Int(t.len())))
8980 } else {
8981 Ok(MmOut::Mm { func: mm, recv: v })
8982 }
8983 }
8984 _ => {
8985 let mm = self.get_mm(v, Mm::Len);
8986 if mm.is_nil() {
8987 Err(self.type_err("get length of", v))
8988 } else {
8989 Ok(MmOut::Mm { func: mm, recv: v })
8990 }
8991 }
8992 }
8993 }
8994
8995 pub(crate) fn index_value(&mut self, t: Value, key: Value) -> Result<Value, LuaError> {
8996 match self.index_step(t, key)? {
8997 MmOut::Done(v) => Ok(v),
8998 MmOut::Mm { func, recv } => self.call_mm1(func, &[recv, key]),
8999 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from index_step"),
9000 }
9001 }
9002
9003 /// PUC `MAXTAGLOOP`: 100 links of `__index`/`__newindex` in 5.1/5.2,
9004 /// 2000 from 5.3.
9005 fn tag_loop_limit(&self) -> u32 {
9006 if self.version <= LuaVersion::Lua52 {
9007 100
9008 } else {
9009 MAX_TAG_LOOP
9010 }
9011 }
9012
9013 /// Resolve `t[key]` through the `__index` chain, stopping at the first raw
9014 /// hit (`Done`) or function metamethod (`Mm`). Table-valued `__index` links
9015 /// are followed inline (no yield possible); only a function link can yield.
9016 fn index_step(&mut self, t: Value, key: Value) -> Result<MmOut, LuaError> {
9017 let mut cur = t;
9018 for _ in 0..self.tag_loop_limit() {
9019 let mm = match cur {
9020 Value::Table(tb) => {
9021 let v = tb.get(key);
9022 if !v.is_nil() {
9023 return Ok(MmOut::Done(v));
9024 }
9025 let mm = self.get_mm(cur, Mm::Index);
9026 if mm.is_nil() {
9027 return Ok(MmOut::Done(Value::Nil));
9028 }
9029 mm
9030 }
9031 v => {
9032 let mm = self.get_mm(v, Mm::Index);
9033 if mm.is_nil() {
9034 return Err(self.type_err("index", v));
9035 }
9036 mm
9037 }
9038 };
9039 match mm {
9040 Value::Closure(_) | Value::Native(_) => {
9041 return Ok(MmOut::Mm {
9042 func: mm,
9043 recv: cur,
9044 });
9045 }
9046 next => cur = next,
9047 }
9048 }
9049 Err(self.runerror(if self.version <= LuaVersion::Lua52 {
9050 "loop in gettable"
9051 } else {
9052 "'__index' chain too long; possible loop"
9053 }))
9054 }
9055
9056 pub(crate) fn newindex_value(
9057 &mut self,
9058 t: Value,
9059 key: Value,
9060 v: Value,
9061 ) -> Result<(), LuaError> {
9062 match self.newindex_step(t, key, v)? {
9063 MmOut::Done(_) => Ok(()),
9064 MmOut::Mm { func, recv } => {
9065 self.call_value(func, &[recv, key, v])?;
9066 Ok(())
9067 }
9068 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from newindex_step"),
9069 }
9070 }
9071
9072 /// Resolve `t[key] = v` through the `__newindex` chain. A raw assignment is
9073 /// performed inline (returning `Done`); only a function metamethod (`Mm`)
9074 /// needs an actual call — which the caller may run yieldably.
9075 fn newindex_step(&mut self, t: Value, key: Value, v: Value) -> Result<MmOut, LuaError> {
9076 // Read-time probe (gc-verify): a dead query key at a
9077 // WRITE site, attributed to the instruction that produced it.
9078 #[cfg(feature = "gc-verify")]
9079 if let Some(p) = match key {
9080 Value::Str(s) => Some(s.as_ptr() as usize),
9081 Value::Table(t2) => Some(t2.as_ptr() as usize),
9082 _ => None,
9083 } && crate::runtime::gc_verify_probe::is_freed(p)
9084 {
9085 let detail = match self.frames.last() {
9086 Some(CallFrame::Lua(f)) => {
9087 let pc = f.pc as usize;
9088 let mut w = String::new();
9089 for q in pc.saturating_sub(6)..(pc + 2) {
9090 if let Some(inst) = f.closure.proto.code.get(q) {
9091 w.push_str(&format!(
9092 "\n [{q}] {:?} a={} b={} c={} k={}",
9093 inst.op(),
9094 inst.a(),
9095 inst.b(),
9096 inst.c(),
9097 inst.k()
9098 ));
9099 }
9100 }
9101 format!("pc={pc} base={} gc_top={} window:{w}", f.base, self.gc_top)
9102 }
9103 _ => "non-Lua frame".into(),
9104 };
9105 panic!("[gc-verify] newindex_step QUERY key {p:#x} freed. {detail}");
9106 }
9107 let mut cur = t;
9108 for _ in 0..self.tag_loop_limit() {
9109 let mm = match cur {
9110 Value::Table(tb) => {
9111 // Single-walk collapse — Table::try_set_existing
9112 // fuses the prior `tb.get(key).is_nil()` gate and
9113 // `raw_set` walk into one chain traversal when the
9114 // key is already present with a non-nil value. The
9115 // __newindex chain semantics are preserved by the
9116 // identity (slot_nil ⇔ fire_newindex).
9117 //
9118 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the
9119 // heap is single-threaded and the pointer is live as
9120 // long as it is reachable from active roots (see
9121 // heap.rs:5-7). Mirrors the raw_set wrapper below.
9122 if unsafe { tb.as_mut() }.try_set_existing(key, v) {
9123 self.heap
9124 .barrier_back(tb.as_ptr() as *mut crate::runtime::heap::GcHeader);
9125 return Ok(MmOut::Done(Value::Nil));
9126 }
9127 let mm = self.get_mm(cur, Mm::NewIndex);
9128 if mm.is_nil() {
9129 self.raw_set(tb, key, v)?;
9130 return Ok(MmOut::Done(Value::Nil));
9131 }
9132 mm
9133 }
9134 bad => {
9135 let mm = self.get_mm(bad, Mm::NewIndex);
9136 if mm.is_nil() {
9137 return Err(self.type_err("index", bad));
9138 }
9139 mm
9140 }
9141 };
9142 match mm {
9143 Value::Closure(_) | Value::Native(_) => {
9144 return Ok(MmOut::Mm {
9145 func: mm,
9146 recv: cur,
9147 });
9148 }
9149 next => cur = next,
9150 }
9151 }
9152 Err(self.runerror(if self.version <= LuaVersion::Lua52 {
9153 "loop in settable"
9154 } else {
9155 "'__newindex' chain too long; possible loop"
9156 }))
9157 }
9158
9159 pub(crate) fn raw_set(&mut self, t: Gc<Table>, key: Value, v: Value) -> Result<(), LuaError> {
9160 // 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).
9161 match unsafe { t.as_mut() }.set(&mut self.heap, key, v) {
9162 Ok(()) => {
9163 self.heap
9164 .barrier_back(t.as_ptr() as *mut crate::runtime::heap::GcHeader);
9165 Ok(())
9166 }
9167 Err(TableError::NilIndex) => Err(self.runerror("table index is nil")),
9168 Err(TableError::NanIndex) => Err(self.runerror("table index is NaN")),
9169 Err(TableError::Overflow) => Err(self.runerror("table overflow")),
9170 Err(TableError::InvalidNext) => unreachable!(),
9171 }
9172 }
9173
9174 /// Decide equality, or surface the `__eq` metamethod to call. `Done` carries
9175 /// the boolean result; `Mm` (when raw equality fails and both are tables
9176 /// with an `__eq`) carries the metamethod — called with `(l, r)`.
9177 fn eq_step(&mut self, l: Value, r: Value) -> MmOut {
9178 if l.raw_eq(r) {
9179 return MmOut::Done(Value::Bool(true));
9180 }
9181 if let (Value::Table(_), Value::Table(_)) | (Value::Userdata(_), Value::Userdata(_)) =
9182 (l, r)
9183 {
9184 // PUC 5.3+ accepts any `__eq` reachable from either operand; 5.1
9185 // and 5.2 require the two operands' metatables to expose the same
9186 // `__eq` (`get_compTM` / `get_equalTM`) — `c == d` where `d` has
9187 // no metatable falls straight back to raw inequality. events.lua
9188 // 5.1 :262 bakes this in.
9189 let mm = if self.version() <= LuaVersion::Lua52 {
9190 self.get_comp_mm(l, r, Mm::Eq)
9191 } else {
9192 let mut m = self.get_mm(l, Mm::Eq);
9193 if m.is_nil() {
9194 m = self.get_mm(r, Mm::Eq);
9195 }
9196 m
9197 };
9198 if !mm.is_nil() {
9199 return MmOut::Mm { func: mm, recv: l };
9200 }
9201 }
9202 MmOut::Done(Value::Bool(false))
9203 }
9204
9205 // ---- arithmetic ----
9206
9207 #[inline(always)]
9208 fn arith_rr(&mut self, inst: Inst, base: u32, op: ArithOp) -> Result<(), LuaError> {
9209 let l = self.r(base, inst.b());
9210 let r = self.r(base, inst.c());
9211 // hot path: Int + Int for Add / Sub / Mul — fib_28, loop_int_1m,
9212 // binary_trees all hammer these. Skipping coerce_num + the big
9213 // arith_fast match shaves several conditional moves per op.
9214 if let (Value::Int(a), Value::Int(b)) = (l, r) {
9215 let fast = match op {
9216 ArithOp::Add => Some(Value::Int(a.wrapping_add(b))),
9217 ArithOp::Sub => Some(Value::Int(a.wrapping_sub(b))),
9218 ArithOp::Mul => Some(Value::Int(a.wrapping_mul(b))),
9219 _ => None,
9220 };
9221 if let Some(v) = fast {
9222 self.set_r(base, inst.a(), v);
9223 return Ok(());
9224 }
9225 }
9226 // hot path: Float + Float for Add / Sub / Mul / Div — math_loop_100k
9227 // and any numeric workload with non-integer accumulators benefits.
9228 if let (Value::Float(a), Value::Float(b)) = (l, r) {
9229 let fast = match op {
9230 ArithOp::Add => Some(Value::Float(a + b)),
9231 ArithOp::Sub => Some(Value::Float(a - b)),
9232 ArithOp::Mul => Some(Value::Float(a * b)),
9233 ArithOp::Div => Some(Value::Float(a / b)),
9234 _ => None,
9235 };
9236 if let Some(v) = fast {
9237 self.set_r(base, inst.a(), v);
9238 return Ok(());
9239 }
9240 }
9241 // An `Add` with k set is a 5.4+ `x - 0` (see `Op::Add`): the right
9242 // operand is the integer 0, so it adds only when the left is a number.
9243 let op = if inst.k() && op == ArithOp::Add && !matches!(l, Value::Int(_) | Value::Float(_))
9244 {
9245 ArithOp::Sub
9246 } else {
9247 op
9248 };
9249 match self.arith_fast(op, l, r)? {
9250 Some(v) => self.set_r(base, inst.a(), v),
9251 None => {
9252 let mm = self.arith_mm_func(op, l, r)?;
9253 let dst = base + inst.a();
9254 self.begin_meta_call(mm, &[l, r], MetaAction::Store { dst }, op.mm_name())?;
9255 }
9256 }
9257 Ok(())
9258 }
9259
9260 /// The number a unary `-` operand stands for: 5.4+ leaves strings to
9261 /// the string metatable, 5.3 converts them to floats (PUC `tonumber`).
9262 fn unary_operand(&self, v: Value) -> Option<Num> {
9263 let n = self.arith_operand()(v);
9264 if self.version == LuaVersion::Lua53 && matches!(v, Value::Str(_)) {
9265 n.map(|n| Num::Float(n.as_f64()))
9266 } else {
9267 n
9268 }
9269 }
9270
9271 /// How an arithmetic operand becomes a number: 5.4+ takes numbers only
9272 /// (strings go to their metatable), 5.3 converts numeric strings, and
9273 /// 5.1/5.2, which have only floats, convert them to floats (5.1 with C
9274 /// `strtod`, so `"inf"` and `"0x1p4"` count).
9275 fn arith_operand(&self) -> fn(Value) -> Option<Num> {
9276 if self.version >= LuaVersion::Lua54 {
9277 as_number
9278 } else if self.version == LuaVersion::Lua53 {
9279 coerce_num
9280 } else if self.version == LuaVersion::Lua52 {
9281 coerce_num_float
9282 } else {
9283 coerce_num_51
9284 }
9285 }
9286
9287 /// Fast path for an arithmetic/bitwise op: `Ok(Some(v))` when computed
9288 /// directly, `Ok(None)` when a metamethod is required (the caller decides
9289 /// whether to call it synchronously or yieldably).
9290 fn arith_fast(&mut self, op: ArithOp, l: Value, r: Value) -> Result<Option<Value>, LuaError> {
9291 use ArithOp::*;
9292 // 5.4 moved string->number coercion out of the VM: a string operand
9293 // goes to the string metatable's `__add` etc., and bitwise operators
9294 // have no string metamethods at all.
9295 let num = self.arith_operand();
9296 if let BAnd | BOr | BXor | Shl | Shr = op {
9297 let (Some(a), Some(b)) = (num(l), num(r)) else {
9298 return Ok(None);
9299 };
9300 let (Some(a), Some(b)) = (int_of(a), int_of(b)) else {
9301 // PUC luaG_tointerror: name the offending operand
9302 return Err(self.no_int_rep_err());
9303 };
9304 let v = match op {
9305 BAnd => a & b,
9306 BOr => a | b,
9307 BXor => a ^ b,
9308 Shl => shift_left(a, b),
9309 Shr => shift_left(a, b.wrapping_neg()),
9310 _ => unreachable!(),
9311 };
9312 return Ok(Some(Value::Int(v)));
9313 }
9314 let (Some(mut ln), Some(mut rn)) = (num(l), num(r)) else {
9315 return Ok(None);
9316 };
9317 // PUC 5.3 takes the integer path only when both operands are
9318 // integers (`ttisinteger`); a converted string goes through
9319 // `tonumber`, which yields a float.
9320 if self.version == LuaVersion::Lua53
9321 && (matches!(l, Value::Str(_)) || matches!(r, Value::Str(_)))
9322 {
9323 ln = Num::Float(ln.as_f64());
9324 rn = Num::Float(rn.as_f64());
9325 }
9326 match arith_num(self.version, op, ln, rn) {
9327 Ok(v) => Ok(Some(v)),
9328 Err(msg) => Err(self.runerror(msg)),
9329 }
9330 }
9331
9332 /// Find the arithmetic/bitwise metamethod (left operand first), or raise the
9333 /// PUC type error when neither operand provides one.
9334 fn arith_mm_func(&mut self, op: ArithOp, l: Value, r: Value) -> Result<Value, LuaError> {
9335 use ArithOp::*;
9336 let event = match op {
9337 Add => Mm::Add,
9338 Sub => Mm::Sub,
9339 Mul => Mm::Mul,
9340 Div => Mm::Div,
9341 Mod => Mm::Mod,
9342 Pow => Mm::Pow,
9343 IDiv => Mm::IDiv,
9344 BAnd => Mm::BAnd,
9345 BOr => Mm::BOr,
9346 BXor => Mm::BXor,
9347 Shl => Mm::Shl,
9348 Shr => Mm::Shr,
9349 };
9350 let mut mm = self.get_mm(l, event);
9351 if mm.is_nil() {
9352 mm = self.get_mm(r, event);
9353 }
9354 if mm.is_nil() {
9355 let what = if matches!(op, BAnd | BOr | BXor | Shl | Shr) {
9356 "perform bitwise operation on"
9357 } else {
9358 "perform arithmetic on"
9359 };
9360 // luaG_opinterror blames the first operand that is not a number;
9361 // before 5.4 a numeric string counts as one.
9362 let bad = if self.arith_operand()(l).is_none() {
9363 l
9364 } else {
9365 r
9366 };
9367 return Err(self.type_err(what, bad));
9368 }
9369 Ok(mm)
9370 }
9371
9372 // ---- comparison ----
9373
9374 /// `lua_compare(L, a, b, LUA_OPEQ)`: equality including `__eq`.
9375 pub(crate) fn equal(&mut self, l: Value, r: Value) -> Result<bool, LuaError> {
9376 match self.eq_step(l, r) {
9377 MmOut::Done(v) => Ok(v.truthy()),
9378 MmOut::Mm { func, .. } => Ok(self.call_mm1(func, &[l, r])?.truthy()),
9379 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from eq_step"),
9380 }
9381 }
9382
9383 pub(crate) fn less_than(&mut self, l: Value, r: Value, or_eq: bool) -> Result<bool, LuaError> {
9384 match self.less_step(l, r, or_eq)? {
9385 MmOut::Done(v) => Ok(v.truthy()),
9386 MmOut::Mm { func, .. } => Ok(self.call_mm1(func, &[l, r])?.truthy()),
9387 MmOut::CompareSynth { func } => {
9388 // ≤5.3 `__le` via `not __lt(r, l)`. Synchronous helper used
9389 // by library code (sort comparator etc.) — no yield expected
9390 // here (a yield would have hit `call_noyield`'s C boundary).
9391 Ok(!self.call_mm1(func, &[r, l])?.truthy())
9392 }
9393 }
9394 }
9395
9396 /// Decide `l < r` / `l <= r`, or surface the `__lt`/`__le` metamethod. `Done`
9397 /// carries the boolean result; `Mm` (for non-number/string operands) carries
9398 /// the metamethod — called with `(l, r)`; raises the PUC compare error when
9399 /// neither operand provides one.
9400 fn less_step(&mut self, l: Value, r: Value, or_eq: bool) -> Result<MmOut, LuaError> {
9401 let b = match (l, r) {
9402 (Value::Int(a), Value::Int(b)) => {
9403 if or_eq {
9404 a <= b
9405 } else {
9406 a < b
9407 }
9408 }
9409 (Value::Float(a), Value::Float(b)) => {
9410 if or_eq {
9411 a <= b
9412 } else {
9413 a < b
9414 }
9415 }
9416 (Value::Int(a), Value::Float(b)) => {
9417 if or_eq {
9418 int_le_float(a, b)
9419 } else {
9420 int_lt_float(a, b)
9421 }
9422 }
9423 (Value::Float(a), Value::Int(b)) => {
9424 if a.is_nan() {
9425 false
9426 } else if or_eq {
9427 !int_lt_float(b, a)
9428 } else {
9429 !int_le_float(b, a)
9430 }
9431 }
9432 (Value::Str(a), Value::Str(b)) => {
9433 let (a, b) = (a.as_bytes(), b.as_bytes());
9434 if or_eq { a <= b } else { a < b }
9435 }
9436 (l, r) => {
9437 let event = if or_eq { Mm::Le } else { Mm::Lt };
9438 // PUC 5.1's `get_compTM` rule applies to ordered comparisons
9439 // too: both operands' metatables must expose the same
9440 // implementation for `__lt` / `__le` to fire. events.lua 5.1
9441 // :262 expects `c < d` (where `d` has no metatable) to error
9442 // with the default "attempt to compare two table values"
9443 // rather than running c's `__lt` blindly.
9444 let mm = if self.version() <= LuaVersion::Lua51 {
9445 self.get_comp_mm(l, r, event)
9446 } else {
9447 let mut m = self.get_mm(l, event);
9448 if m.is_nil() {
9449 m = self.get_mm(r, event);
9450 }
9451 m
9452 };
9453 // PUC ≤5.4: `a <= b` falls back to `not (b < a)` when neither
9454 // operand carries `__le` (5.4 through its default build's
9455 // LUA_COMPAT_LT_LE); 5.5 requires an explicit `__le`.
9456 // events.lua 5.2/5.3 :172 relies on the synthesis — its
9457 // metatable defines only `__lt`. The `__lt` is looked up as
9458 // for `b < a`: on `b` first (5.1: the same one on both). The
9459 // fallback calls `__lt(r, l)` synchronously (the suite's
9460 // `__lt` doesn't yield) and negates the result; the yieldable
9461 // `__lt` path stays reserved for the explicit `<` operator.
9462 if mm.is_nil() && or_eq && self.version < LuaVersion::Lua55 {
9463 let mm_lt = if self.version <= LuaVersion::Lua51 {
9464 self.get_comp_mm(r, l, Mm::Lt)
9465 } else {
9466 let m = self.get_mm(r, Mm::Lt);
9467 if m.is_nil() {
9468 self.get_mm(l, Mm::Lt)
9469 } else {
9470 m
9471 }
9472 };
9473 if !mm_lt.is_nil() {
9474 return Ok(MmOut::CompareSynth { func: mm_lt });
9475 }
9476 }
9477 if mm.is_nil() {
9478 // PUC luaG_ordererror: "two X values" when the operand
9479 // types match, "X with Y" otherwise (objtypename-aware).
9480 let (t1, t2) = (self.obj_typename(l), self.obj_typename(r));
9481 return Err(self.runerror(&if t1 == t2 {
9482 format!("attempt to compare two {t1} values")
9483 } else {
9484 format!("attempt to compare {t1} with {t2}")
9485 }));
9486 }
9487 return Ok(MmOut::Mm { func: mm, recv: l });
9488 }
9489 };
9490 Ok(MmOut::Done(Value::Bool(b)))
9491 }
9492
9493 // ---- numeric for ----
9494
9495 /// Check and convert a numeric for's control values the way the
9496 /// dialect's `OP_FORPREP` does. The integer loop is chosen by the
9497 /// values' tags (a numeric string makes it a float loop, 5.3+); the
9498 /// check order, wording and the zero-step error differ per version:
9499 /// 5.1/5.2 test initial value, limit, step; 5.3+ limit, step, initial
9500 /// value; only 5.4+ reject a zero step, and an integer loop does that
9501 /// before looking at the limit.
9502 fn for_operands(&mut self, base: u32, a: u32) -> Result<(Num, Num, Num), LuaError> {
9503 let (init, limit, step) = (self.r(base, a), self.r(base, a + 1), self.r(base, a + 2));
9504 let v = self.version();
9505 let order = if v <= LuaVersion::Lua52 {
9506 [("initial value", init), ("limit", limit), ("step", step)]
9507 } else {
9508 [("limit", limit), ("step", step), ("initial value", init)]
9509 };
9510 if v >= LuaVersion::Lua54 && matches!((init, step), (Value::Int(_), Value::Int(0))) {
9511 return Err(self.rt_err("'for' step is zero"));
9512 }
9513 for (what, val) in order {
9514 if as_num(val, v).is_none() {
9515 return Err(self.rt_err(&if v >= LuaVersion::Lua54 {
9516 format!(
9517 "bad 'for' {what} (number expected, got {})",
9518 self.obj_typename(val)
9519 )
9520 } else {
9521 format!("'for' {what} must be a number")
9522 }));
9523 }
9524 }
9525 let n = |val| as_num(val, v).expect("checked above");
9526 let int_loop =
9527 v <= LuaVersion::Lua52 || matches!((init, step), (Value::Int(_), Value::Int(_)));
9528 if int_loop {
9529 return Ok((n(init), n(limit), n(step)));
9530 }
9531 let (i, l, st) = (n(init).as_f64(), n(limit).as_f64(), n(step).as_f64());
9532 if v >= LuaVersion::Lua54 && st == 0.0 {
9533 return Err(self.rt_err("'for' step is zero"));
9534 }
9535 Ok((Num::Float(i), Num::Float(l), Num::Float(st)))
9536 }
9537
9538 fn for_prep(&mut self, inst: Inst, base: u32) -> Result<(), LuaError> {
9539 let a = inst.a();
9540 let (init_n, limit_n, step_n) = self.for_operands(base, a)?;
9541 // PUC 5.1–5.3 `OP_FORPREP` stores `i = init - step` and *unconditionally*
9542 // jumps to the matching `OP_FORLOOP` — the body never runs ahead of the
9543 // first test, so each successful iteration emits a backward `OP_FORLOOP`
9544 // jump (db.lua's `for i=1,4 do a=1 end` ↦ 5 line-hook events instead of
9545 // 5.4's 4). 5.4+ collapsed that to a count-based fall-through. The skip
9546 // distance in luna's encoding is `loop_pc - prep_pc`; firing
9547 // `add_pc(bx - 1)` lands the running pc on OP_FORLOOP itself.
9548 let pre53 = self.version() <= LuaVersion::Lua53;
9549 match (init_n, step_n) {
9550 (Num::Int(i0), Num::Int(st)) => {
9551 if pre53 {
9552 // PUC 5.3 `forlimit`: int limit passes through; float limit
9553 // gets clamped to MIN/MAX with a `stopnow` flag set only
9554 // when the clamp is unreachable (positive float with a
9555 // negative step → limit=MAX, stopnow; negative float with
9556 // step>=0 → limit=MIN, stopnow). On `stopnow` PUC rewrites
9557 // `init = 0` so OP_FORLOOP's first test against the
9558 // unreachable clamp fails cleanly. An ordinary in-range
9559 // empty loop (e.g. `for i = 1, 0`) is *not* `stopnow` — it
9560 // lets OP_FORLOOP's natural test reject the first step.
9561 let (lim, stopnow) = match limit_n {
9562 Num::Int(l) => (l, false),
9563 Num::Float(f) => {
9564 // `luaV_tointeger` floors (ceils for a negative
9565 // step); a float it cannot fit is clamped on
9566 // the side of its sign, NaN counting as
9567 // negative (`0 < n` is false).
9568 let conv = if st < 0 { f.ceil() } else { f.floor() };
9569 if (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0)
9570 .contains(&conv)
9571 {
9572 (conv as i64, false)
9573 } else if f > 0.0 {
9574 (i64::MAX, st < 0)
9575 } else {
9576 (i64::MIN, st > 0)
9577 }
9578 }
9579 };
9580 let initv = if stopnow { 0 } else { i0 };
9581 let pre = initv.wrapping_sub(st);
9582 self.set_r(base, a, Value::Int(pre));
9583 self.set_r(base, a + 1, Value::Int(lim));
9584 self.set_r(base, a + 2, Value::Int(st));
9585 self.add_pc(inst.bx() as i32 - 1);
9586 return Ok(());
9587 }
9588 let (lim, empty) = int_for_limit(limit_n, i0, st);
9589 if empty {
9590 self.add_pc(inst.bx() as i32);
9591 return Ok(());
9592 }
9593 let count = if st > 0 {
9594 (lim as u64).wrapping_sub(i0 as u64) / (st as u64)
9595 } else {
9596 (i0 as u64).wrapping_sub(lim as u64) / (st as i128).unsigned_abs() as u64
9597 };
9598 self.set_r(base, a, Value::Int(i0));
9599 self.set_r(base, a + 1, Value::Int(count as i64));
9600 self.set_r(base, a + 2, Value::Int(st));
9601 self.set_r(base, a + 3, Value::Int(i0));
9602 }
9603 _ => {
9604 let (x0, lim, st) = (init_n.as_f64(), limit_n.as_f64(), step_n.as_f64());
9605 if pre53 {
9606 let pre = x0 - st;
9607 self.set_r(base, a, Value::Float(pre));
9608 self.set_r(base, a + 1, Value::Float(lim));
9609 self.set_r(base, a + 2, Value::Float(st));
9610 self.add_pc(inst.bx() as i32 - 1);
9611 return Ok(());
9612 }
9613 // lvm.c `forprep`: skip only when `0 < step ? limit < init :
9614 // init < limit`; a NaN makes both false, so the body runs
9615 // once (with a NaN step, on the second test's side)
9616 let skip = if 0.0 < st { lim < x0 } else { x0 < lim };
9617 let runs = !skip;
9618 if !runs {
9619 self.add_pc(inst.bx() as i32);
9620 return Ok(());
9621 }
9622 self.set_r(base, a, Value::Float(x0));
9623 self.set_r(base, a + 1, Value::Float(lim));
9624 self.set_r(base, a + 2, Value::Float(st));
9625 self.set_r(base, a + 3, Value::Float(x0));
9626 }
9627 }
9628 Ok(())
9629 }
9630
9631 #[inline(always)]
9632 fn for_loop(&mut self, inst: Inst, base: u32) -> Result<(), LuaError> {
9633 let a = inst.a();
9634 // PUC 5.1–5.3 `OP_FORLOOP` compares the post-step `i` to `limit`
9635 // directly (R[a+1] holds the limit, *not* a remaining-count) so the
9636 // first iteration's test fires through the same backward-jump path as
9637 // every later iteration. 5.4+ switched to the count-based form luna
9638 // already uses for `Int`; the float branch was already PUC-3.x-style.
9639 let v = self.version();
9640 let pre53 = v <= LuaVersion::Lua53;
9641 // `for_prep` leaves the three slots all Int or all Float; anything
9642 // else was written by `debug.setlocal` or by crafted bytecode. PUC
9643 // reads such slots unchecked (garbage or a crash); luna raises.
9644 match (self.r(base, a), self.r(base, a + 1), self.r(base, a + 2)) {
9645 (Value::Int(cur), Value::Int(lim), Value::Int(st)) if pre53 => {
9646 let next = cur.wrapping_add(st);
9647 let cont = if st > 0 { next <= lim } else { next >= lim };
9648 if cont {
9649 self.set_r(base, a, Value::Int(next));
9650 self.set_r(base, a + 3, Value::Int(next));
9651 self.add_pc(-(inst.bx() as i32));
9652 }
9653 }
9654 // the count is unsigned (PUC `lua_Unsigned`): a loop over
9655 // more than 2^63 values stores a "negative" one
9656 (Value::Int(cur), Value::Int(count), Value::Int(st)) => {
9657 if count != 0 {
9658 let next = cur.wrapping_add(st);
9659 self.set_r(base, a, Value::Int(next));
9660 self.set_r(base, a + 1, Value::Int(count.wrapping_sub(1)));
9661 self.set_r(base, a + 3, Value::Int(next));
9662 self.add_pc(-(inst.bx() as i32));
9663 }
9664 }
9665 (Value::Float(cur), Value::Float(lim), Value::Float(st)) => {
9666 self.float_for_step(inst, base, cur, lim, st);
9667 }
9668 // 5.1/5.2 have one number type, so a number of the other
9669 // representation stored into a slot is still a valid state
9670 (x, l, s) if v <= LuaVersion::Lua52 => {
9671 match (as_number(x), as_number(l), as_number(s)) {
9672 (Some(cur), Some(lim), Some(st)) => {
9673 self.float_for_step(inst, base, cur.as_f64(), lim.as_f64(), st.as_f64())
9674 }
9675 _ => return Err(self.rt_err("'for' state corrupted")),
9676 }
9677 }
9678 _ => return Err(self.rt_err("'for' state corrupted")),
9679 }
9680 Ok(())
9681 }
9682
9683 #[inline(always)]
9684 fn float_for_step(&mut self, inst: Inst, base: u32, cur: f64, lim: f64, st: f64) {
9685 let a = inst.a();
9686 let next = cur + st;
9687 let cont = if st > 0.0 { next <= lim } else { next >= lim };
9688 if cont {
9689 self.set_r(base, a, Value::Float(next));
9690 self.set_r(base, a + 3, Value::Float(next));
9691 self.add_pc(-(inst.bx() as i32));
9692 }
9693 }
9694
9695 // ---- native helpers (used by builtins) ----
9696
9697 /// A native function's own captured upvalue (self lives at func_slot).
9698 ///
9699 /// Public so `native_typed` trampolines and embedders authoring
9700 /// stateful natives via `native_with(...)` can read their upvals.
9701 pub fn nat_upval(&self, func_slot: u32, i: usize) -> Value {
9702 let Value::Native(nc) = self.stack[func_slot as usize] else {
9703 unreachable!("native frame without native closure");
9704 };
9705 nc.upvals[i]
9706 }
9707
9708 /// Number of upvalues captured by the native at `func_slot` (variadic
9709 /// captures such as the `io.lines` format list).
9710 pub(crate) fn nat_upcount(&self, func_slot: u32) -> usize {
9711 let Value::Native(nc) = self.stack[func_slot as usize] else {
9712 unreachable!("native frame without native closure");
9713 };
9714 nc.upvals.len()
9715 }
9716
9717 /// Write a native function's own upvalue (stateful iterators).
9718 pub(crate) fn nat_set_upval(&mut self, func_slot: u32, i: usize, v: Value) {
9719 let Value::Native(nc) = self.stack[func_slot as usize] else {
9720 unreachable!("native frame without native closure");
9721 };
9722 // 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).
9723 unsafe { nc.as_mut() }.upvals[i] = v;
9724 // NativeClosure.upvals is traced as part of its Trace; a long-lived
9725 // stateful iterator closure (e.g. string.gmatch) sees many writes —
9726 // barrier_back once-and-done is cheaper than per-child forward.
9727 self.heap
9728 .barrier_back(nc.as_ptr() as *mut crate::runtime::heap::GcHeader);
9729 }
9730
9731 /// Read the i-th positional argument inside a `NativeFn` body
9732 /// (analogous to `lua_tovalue(L, i + 1)`). `i >= nargs` yields `Nil`,
9733 /// matching PUC's "missing arg is nil" contract. Public so embedders
9734 /// can author their own natives.
9735 pub fn nat_arg(&self, func_slot: u32, nargs: u32, i: u32) -> Value {
9736 if i < nargs {
9737 self.stack[(func_slot + 1 + i) as usize]
9738 } else {
9739 Value::Nil
9740 }
9741 }
9742
9743 /// Overwrite the i-th argument slot of the running native (the in-place
9744 /// conversion `lua_tolstring` performs on a number argument).
9745 pub(crate) fn nat_set_arg(&mut self, func_slot: u32, i: u32, v: Value) {
9746 self.stack[(func_slot + 1 + i) as usize] = v;
9747 }
9748
9749 /// Push the return values of a `NativeFn` and return their count
9750 /// (analogous to pushing N values then `return N` from a C function).
9751 /// Public so embedders can author their own natives.
9752 pub fn nat_return(&mut self, func_slot: u32, vals: &[Value]) -> u32 {
9753 let need = func_slot as usize + vals.len();
9754 if self.stack.len() < need {
9755 self.stack.resize(need, Value::Nil);
9756 }
9757 for (i, &v) in vals.iter().enumerate() {
9758 self.stack[func_slot as usize + i] = v;
9759 }
9760 vals.len() as u32
9761 }
9762
9763 /// Fast string concatenation of an adjacent pair, or `None` when a
9764 /// `__concat` metamethod is required.
9765 fn concat_pair(&mut self, l: Value, r: Value) -> Result<Option<Value>, LuaError> {
9766 let legacy = self.float_fmt();
9767 // Length-check fast paths for both string operands BEFORE the
9768 // (expensive) copy in `concat_piece`, so a runaway `a..a..a..…`
9769 // chain (5.1 big.lua / 5.5 heavy.lua's `teststring`) raises the
9770 // overflow on the first pair that would exceed `INT_MAX` instead
9771 // of allocating multi-GB intermediates first.
9772 let max_str = i32::MAX as usize;
9773 if let (Value::Str(ls), Value::Str(rs)) = (l, r) {
9774 let a_len = ls.as_bytes().len();
9775 let b_len = rs.as_bytes().len();
9776 let new_len = a_len.checked_add(b_len);
9777 if new_len.is_none() || new_len.unwrap() > max_str {
9778 return Err(self.rt_err("string length overflow"));
9779 }
9780 }
9781 match (concat_piece(l, legacy), concat_piece(r, legacy)) {
9782 (Some(a), Some(b)) => {
9783 // PUC `MAX_SIZE` for Lua strings is `INT_MAX`; an attempt to
9784 // concat past it raises "string length overflow"
9785 // (5.5 heavy.lua `teststring` doubles `a..a..…` until it hits
9786 // exactly this wall).
9787 let new_len = a.len().checked_add(b.len());
9788 if new_len.is_none() || new_len.unwrap() > max_str {
9789 return Err(self.rt_err("string length overflow"));
9790 }
9791 let mut combined = a;
9792 combined.extend_from_slice(&b);
9793 Ok(Some(Value::Str(self.heap.intern(&combined))))
9794 }
9795 _ => Ok(None),
9796 }
9797 }
9798
9799 /// Fold the concat operands occupying `[base_a .. self.top)` right-to-left
9800 /// into a single result at `base_a` (PUC `luaV_concat`). Returns after
9801 /// either finishing (result at `base_a`) or arming a yieldable `__concat`
9802 /// call — its `Meta` continuation re-enters here on the metamethod's return.
9803 fn concat_run(&mut self, base_a: u32) -> Result<(), LuaError> {
9804 // Sum the lengths of all all-Str operands BEFORE starting the
9805 // right-associative fold so a 129-operand `a..a..…` chain
9806 // (5.1 big.lua's `rep129(longs)`) raises overflow immediately,
9807 // not after dozens of multi-GB intermediate intern+hash rounds.
9808 // A non-Str operand falls through to the per-pair check.
9809 let max_str = i32::MAX as usize;
9810 let mut total: usize = 0;
9811 let mut all_str = true;
9812 for slot in base_a..self.top {
9813 match self.stack[slot as usize] {
9814 Value::Str(s) => match total.checked_add(s.as_bytes().len()) {
9815 Some(t) if t <= max_str => total = t,
9816 _ => return Err(self.rt_err("string length overflow")),
9817 },
9818 _ => {
9819 all_str = false;
9820 break;
9821 }
9822 }
9823 }
9824 let _ = all_str; // discrimination already captured by early returns above
9825 while self.top.saturating_sub(base_a) >= 2 {
9826 let i = self.top - 1; // rightmost operand
9827 let x = self.stack[(i - 1) as usize];
9828 let y = self.stack[i as usize];
9829 match self.concat_pair(x, y)? {
9830 Some(s) => {
9831 self.stack[(i - 1) as usize] = s;
9832 self.top = i; // consumed y
9833 }
9834 None => {
9835 let mut mm = self.get_mm(x, Mm::Concat);
9836 if mm.is_nil() {
9837 mm = self.get_mm(y, Mm::Concat);
9838 }
9839 if mm.is_nil() {
9840 let legacy = self.float_fmt();
9841 let bad = if concat_piece(x, legacy).is_none() {
9842 x
9843 } else {
9844 y
9845 };
9846 return Err(self.type_err("concatenate", bad));
9847 }
9848 // result lands at i-1, dropping y (top→i); resume continues.
9849 let dst = i - 1;
9850 self.begin_meta_call(
9851 mm,
9852 &[x, y],
9853 MetaAction::Concat { dst, base_a },
9854 "concat",
9855 )?;
9856 return Ok(());
9857 }
9858 }
9859 }
9860 self.maybe_collect_garbage(base_a + 1);
9861 Ok(())
9862 }
9863
9864 /// `luaL_tolstring`: `__tostring` (whose result must be a string or a
9865 /// number, rendered), else the basic rendering, where 5.3+ names a value
9866 /// by a string `__name` metafield.
9867 pub fn tostring_value(&mut self, v: Value) -> Result<Vec<u8>, LuaError> {
9868 let mm = self.get_mm(v, Mm::ToString);
9869 if !mm.is_nil() {
9870 // `luaL_callmeta` is a plain `lua_call`: `__tostring` cannot yield.
9871 let r = self.call_noyield(mm, &[v])?;
9872 return match r.first().copied().unwrap_or(Value::Nil) {
9873 Value::Str(s) => Ok(s.as_bytes().to_vec()),
9874 r @ (Value::Int(_) | Value::Float(_)) => Ok(self.tostring_basic(r)),
9875 // luaL_error: positioned at whatever called the library function
9876 _ => Err(crate::vm::builtins::raise_str(
9877 self,
9878 "'__tostring' must return a string",
9879 )),
9880 };
9881 }
9882 if self.version >= LuaVersion::Lua53
9883 && !matches!(
9884 v,
9885 Value::Nil | Value::Bool(_) | Value::Int(_) | Value::Float(_) | Value::Str(_)
9886 )
9887 && let Value::Str(name) = self.get_mm(v, Mm::Name)
9888 {
9889 let basic = self.tostring_basic(v);
9890 let at = basic
9891 .iter()
9892 .position(|&c| c == b':')
9893 .expect("an object renders as `kind: address`");
9894 let mut out = name.as_bytes().to_vec();
9895 out.extend_from_slice(&basic[at..]);
9896 return Ok(out);
9897 }
9898 Ok(self.tostring_basic(v))
9899 }
9900
9901 /// The dialect's float-rendering flavor: ≤5.2 %.14g
9902 /// bare, 5.3/5.4 %.14g + ".0", 5.5 two-stage %.15g/%.17g + ".0".
9903 pub(crate) fn float_fmt(&self) -> numeric::FloatFmt {
9904 use crate::version::LuaVersion::*;
9905 match self.version {
9906 Lua51 | Lua52 => numeric::FloatFmt::Legacy14,
9907 Lua53 | Lua54 => numeric::FloatFmt::G14,
9908 _ => numeric::FloatFmt::TwoStage55,
9909 }
9910 }
9911
9912 /// Basic tostring (no metamethods).
9913 pub(crate) fn tostring_basic(&mut self, v: Value) -> Vec<u8> {
9914 match v {
9915 Value::Nil => b"nil".to_vec(),
9916 Value::Bool(true) => b"true".to_vec(),
9917 Value::Bool(false) => b"false".to_vec(),
9918 Value::Int(i) => numeric::num_to_string(Num::Int(i)).into_bytes(),
9919 // PUC ≤5.2 has no integer subtype — `tostring(2.0)` is `"2"`, not
9920 // `"2.0"`. The 5.3+ split needs the suffix so `print(2.0)` is
9921 // distinguishable from `print(2)`. pm.lua :13 builds patterns by
9922 // concatenating these renderings.
9923 Value::Float(f) => {
9924 numeric::num_to_string_for(Num::Float(f), self.float_fmt()).into_bytes()
9925 }
9926 Value::Str(s) => s.as_bytes().to_vec(),
9927 Value::Table(t) => format!("table: {:p}", t.as_ptr()).into_bytes(),
9928 Value::Closure(c) => format!("function: {:p}", c.as_ptr()).into_bytes(),
9929 Value::Native(n) => format!("function: {:p}", n.as_ptr()).into_bytes(),
9930 Value::Coro(co) => format!("thread: {:p}", co.as_ptr()).into_bytes(),
9931 // PUC names file handles `file (0x…)`; a bare userdata is
9932 // `userdata: 0x…`. The io library overrides this via __tostring.
9933 Value::Userdata(u) => format!("userdata: {:p}", u.as_ptr()).into_bytes(),
9934 // PUC `lua_topointer`/tostring on light udata: "userdata: 0x…"
9935 // (the "light" qualifier only appears in `luaL_typeerror`).
9936 Value::LightUserdata(p) => format!("userdata: {p:p}").into_bytes(),
9937 }
9938 }
9939}
9940
9941impl Vm {
9942 /// PUC's debug-API placeholder for an unnamed vararg slot returned by
9943 /// `debug.getlocal(_, -n)`. 5.2/5.3 spelled it `"(*vararg)"`; 5.4
9944 /// dropped the asterisk in favour of `"(vararg)"`. db.lua 5.2 :189 /
9945 /// 5.3 :195 / 5.4 :286 baseline on their respective form.
9946 pub(crate) fn vararg_locvar_name(&self) -> &'static str {
9947 if matches!(self.version, LuaVersion::Lua52 | LuaVersion::Lua53) {
9948 "(*vararg)"
9949 } else {
9950 "(vararg)"
9951 }
9952 }
9953
9954 /// PUC's debug-API placeholder for an unnamed temporary on a C
9955 /// activation. 5.2/5.3 reported `"(*temporary)"`; 5.4 switched to
9956 /// `"(C temporary)"`. db.lua 5.2 :288, 5.3 :312, 5.4 :404 each pin
9957 /// their spelling.
9958 pub(crate) fn temporary_locvar_name(&self) -> &'static str {
9959 if matches!(
9960 self.version,
9961 LuaVersion::Lua51 | LuaVersion::Lua52 | LuaVersion::Lua53
9962 ) {
9963 // PUC 5.1's `findlocal` C-frame branch reported `(*temporary)`
9964 // (db.lua :228 pins it). 5.2/5.3 kept the spelling, 5.4 changed
9965 // to `(C temporary)`.
9966 "(*temporary)"
9967 } else {
9968 "(C temporary)"
9969 }
9970 }
9971
9972 /// PUC's debug-API placeholder for an unnamed Lua-frame temporary
9973 /// (an arithmetic intermediate sitting past the last named local on a
9974 /// live register slot). 5.2/5.3 reported `"(*temporary)"`; 5.4 dropped
9975 /// the asterisk to `"(temporary)"`. db.lua 5.3 :786, 5.4 :966 pin the
9976 /// spelling.
9977 pub(crate) fn lua_temporary_locvar_name(&self) -> &'static str {
9978 if matches!(
9979 self.version,
9980 LuaVersion::Lua51 | LuaVersion::Lua52 | LuaVersion::Lua53
9981 ) {
9982 "(*temporary)"
9983 } else {
9984 "(temporary)"
9985 }
9986 }
9987
9988 /// PUC `pushglobalfuncname`: walk `package.loaded` to depth 2 looking for a
9989 /// native whose function pointer matches `target`, and return its qualified
9990 /// name (e.g. `"table.sort"`). A `_G.X` match is stripped to `"X"`. Returns
9991 /// `None` if no match is found. Used by `arg_error` when the running native
9992 /// was invoked from another native (PUC `ar.name == NULL` at level 0).
9993 pub(crate) fn pushglobalfuncname(
9994 &mut self,
9995 target: crate::runtime::value::NativeFn,
9996 ) -> Option<String> {
9997 let pkg_k = Value::Str(self.heap.intern(b"package"));
9998 let pkg = match self.globals().get(pkg_k) {
9999 Value::Table(t) => t,
10000 _ => return None,
10001 };
10002 let loaded_k = Value::Str(self.heap.intern(b"loaded"));
10003 let loaded = match pkg.get(loaded_k) {
10004 Value::Table(t) => t,
10005 _ => return None,
10006 };
10007 let matches = |v: Value| -> bool {
10008 matches!(v, Value::Native(nc) if std::ptr::fn_addr_eq(nc.f, target))
10009 };
10010 let mut k = Value::Nil;
10011 while let Ok(Some((nk, nv))) = loaded.next(k) {
10012 k = nk;
10013 let Value::Str(outer) = nk else { continue };
10014 let outer = String::from_utf8_lossy(outer.as_bytes()).into_owned();
10015 if matches(nv) {
10016 return Some(if outer == "_G" { String::new() } else { outer });
10017 }
10018 if let Value::Table(inner_t) = nv {
10019 let mut k2 = Value::Nil;
10020 while let Ok(Some((nk2, nv2))) = inner_t.next(k2) {
10021 k2 = nk2;
10022 if matches(nv2)
10023 && let Value::Str(inner) = nk2
10024 {
10025 let inner = String::from_utf8_lossy(inner.as_bytes()).into_owned();
10026 return Some(if outer == "_G" {
10027 inner
10028 } else {
10029 format!("{outer}.{inner}")
10030 });
10031 }
10032 }
10033 }
10034 }
10035 None
10036 }
10037
10038 /// How the caller named the running native (PUC `lua_getinfo("n")` at
10039 /// level 0): `None` when it gives no name, as when the caller is C.
10040 pub(crate) fn running_call_name(&self) -> Option<(&'static str, String)> {
10041 let ts = self.thread_stack(None);
10042 if ts.levels.is_empty() {
10043 return None;
10044 }
10045 self.level_name(&ts, 0)
10046 }
10047
10048 /// Read an upvalue cell of a closure (debug.getupvalue).
10049 pub(crate) fn upvalue_value(&self, cl: Gc<LuaClosure>, idx: usize) -> Value {
10050 match cl.upvals()[idx].state() {
10051 UpvalState::Open { slot, thread } => self.read_slot(slot, thread),
10052 UpvalState::Closed(v) => v,
10053 }
10054 }
10055
10056 /// Write an upvalue cell of a closure (debug.setupvalue).
10057 pub(crate) fn upvalue_set_value(&mut self, cl: Gc<LuaClosure>, idx: usize, v: Value) {
10058 let uv = cl.upvals()[idx];
10059 match uv.state() {
10060 UpvalState::Open { slot, thread } => self.write_slot(slot, thread, v),
10061 UpvalState::Closed(_) => {
10062 // 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).
10063 unsafe { uv.as_mut() }.set_closed(v);
10064 self.heap
10065 .barrier_forward(uv.as_ptr() as *mut crate::runtime::heap::GcHeader, v);
10066 }
10067 }
10068 }
10069}
10070
10071// ────────────────────────────────────────────────────────────────────
10072// AOT trace dispatch install.
10073//
10074// The deploy-side resolver in `luna-runtime-helpers` walks the binary's
10075// trace-meta section after `vm.load`, resolves each entry's
10076// `(proto_hash, head_pc, fn_ptr)` triple against the loaded chunk's
10077// proto tree, and pushes a `CompiledTrace` onto the matching Proto's
10078// `traces` Vec via [`Vm::install_aot_trace`] below. The existing
10079// trace-dispatch loop (this file's `cl.proto.traces.borrow().iter()
10080// .find(|t| t.head_pc == pc && t.dispatchable)`) then fires the AOT
10081// mcode without further plumbing — same code path the runtime JIT
10082// uses.
10083//
10084// Why a separate impl block: keeps the AOT API surface (one fn) easy
10085// to locate when grep'ing for `install_aot_trace`, without dragging
10086// the 8500-line `impl Vm` block above.
10087// ────────────────────────────────────────────────────────────────────
10088
10089impl Vm {
10090 /// Install a precompiled
10091 /// `CompiledTrace` onto `proto.traces` so the interp dispatcher
10092 /// fires it at the trace's `head_pc`. This is the runtime install
10093 /// API the deploy-side `luna-runtime-helpers` resolver calls once
10094 /// per AOT-emitted trace meta entry, after looking up `proto` by
10095 /// stable hash (see `crate::runtime::function::Proto::stable_hash`).
10096 ///
10097 /// # What this does
10098 ///
10099 /// Pushes `trace` onto `proto.traces` via the existing `RefCell`.
10100 /// The trace's `entry` fn ptr must already point at runnable
10101 /// machine code (the AOT linker resolved the symbol at link time;
10102 /// the deploy resolver passes the address verbatim).
10103 ///
10104 /// # What this does NOT do
10105 ///
10106 /// - **No deduplication.** Calling twice with the same `head_pc`
10107 /// pushes two entries; the dispatcher's `find` will pick the
10108 /// first match. The deploy resolver is responsible for not
10109 /// double-installing.
10110 /// - **No invalidation of the runtime JIT cache.** If the runtime
10111 /// JIT later records + compiles a trace for the same
10112 /// `(proto, head_pc)`, both coexist on `proto.traces` and the
10113 /// dispatcher's `find` picks whichever appears first. AOT
10114 /// traces install before any runtime recording is possible
10115 /// (resolver runs before `vm.load` returns its first closure),
10116 /// so AOT traces win the race for the same site.
10117 /// - **No coverage gating.** AOT traces are trusted by
10118 /// construction — they were validated at compile time. Setting
10119 /// `dispatchable: false` on the input would silently disable
10120 /// dispatch; the caller controls that flag.
10121 ///
10122 /// # Safety / soundness
10123 ///
10124 /// `trace.entry` is an `unsafe extern "C" fn` (mmap'd or linked
10125 /// machine code). Soundness contract:
10126 ///
10127 /// - The fn pointer must remain valid for the `Vm`'s lifetime.
10128 /// In the AOT-binary deploy shape this is trivially satisfied —
10129 /// the fn lives in the binary's `.text`.
10130 /// - `trace.entry_tags` / `exit_tags` / `window_size` must match
10131 /// what the trace's IR actually compiled against; the dispatcher
10132 /// uses them to marshal `reg_state` in and out without further
10133 /// validation. A mismatch corrupts vm.stack.
10134 ///
10135 /// The AOT pipeline (`luna-aot`) is responsible for ensuring these
10136 /// invariants hold; this fn is a plain push — no validation that
10137 /// would slow the dispatcher's hot path either.
10138 pub fn install_aot_trace(
10139 &mut self,
10140 proto: crate::runtime::Gc<crate::runtime::function::Proto>,
10141 trace: crate::jit::trace::CompiledTrace,
10142 ) {
10143 let _ = self; // resolver passes &mut Vm for symmetry with future
10144 // pending-install + hash-walk variants; nothing on `self` to
10145 // mutate today because the install target lives on the Proto.
10146 proto.traces.borrow_mut().push(TArc::new(trace));
10147 }
10148
10149 /// Walk the proto tree
10150 /// reachable from `root` and return `(proto, stable_hash)` pairs
10151 /// for every Proto found. Used by the deploy-side resolver to
10152 /// match AOT-emitted `proto_hash` keys against the freshly
10153 /// `undump`'d chunk's protos.
10154 ///
10155 /// The walk is BFS over `Proto.protos`. Same-Proto deduplication
10156 /// is done via `Gc::as_ptr` identity — a Proto re-referenced from
10157 /// multiple nested closures (rare; the cache field would catch
10158 /// the closure-side dedup, not the Proto side) is reported once.
10159 ///
10160 /// # Why on `&Vm` and not a free fn
10161 ///
10162 /// Keeps the AOT install API discoverable on the Vm surface —
10163 /// `vm.collect_proto_hashes(root)` reads naturally next to
10164 /// `vm.install_aot_trace(proto, trace)`. Doesn't actually touch
10165 /// any Vm field, so `&self` (read-only) is enough.
10166 pub fn collect_proto_hashes(
10167 &self,
10168 root: crate::runtime::Gc<crate::runtime::function::Proto>,
10169 ) -> Vec<(
10170 crate::runtime::Gc<crate::runtime::function::Proto>,
10171 [u8; 16],
10172 )> {
10173 let _ = self;
10174 let mut out = Vec::new();
10175 let mut seen: std::collections::HashSet<*const crate::runtime::function::Proto> =
10176 std::collections::HashSet::new();
10177 let mut queue: std::collections::VecDeque<
10178 crate::runtime::Gc<crate::runtime::function::Proto>,
10179 > = std::collections::VecDeque::new();
10180 queue.push_back(root);
10181 while let Some(p) = queue.pop_front() {
10182 let key = p.as_ptr() as *const _;
10183 if !seen.insert(key) {
10184 continue;
10185 }
10186 out.push((p, p.stable_hash()));
10187 for &child in p.protos.iter() {
10188 queue.push_back(child);
10189 }
10190 }
10191 out
10192 }
10193}
10194
10195/// Recordings of one trace head that may fail to compile before the head
10196/// is no longer recorded (LuaJIT likewise blacklists a trace start after
10197/// repeated failures). A few tries, since a later recording can see
10198/// different register kinds.
10199const MAX_TRACE_COMPILE_FAILURES: u8 = 3;
10200
10201fn note_trace_compile_failure(proto: Gc<crate::runtime::function::Proto>, head_pc: u32) {
10202 let mut failures = proto.trace_compile_failures.borrow_mut();
10203 match failures.iter_mut().find(|(pc, _)| *pc == head_pc) {
10204 Some((_, n)) => *n = n.saturating_add(1),
10205 None => failures.push((head_pc, 1)),
10206 }
10207}
10208
10209fn trace_head_abandoned(proto: Gc<crate::runtime::function::Proto>, head_pc: u32) -> bool {
10210 proto
10211 .trace_compile_failures
10212 .borrow()
10213 .iter()
10214 .any(|&(pc, n)| pc == head_pc && n >= MAX_TRACE_COMPILE_FAILURES)
10215}