luna_core/vm/exec.rs
1//! The interpreter. Dispatch is a plain match over opcodes. Lua→Lua calls
2//! share one loop and never recurse the Rust stack; only native↔Lua boundaries do (e.g. pcall).
3//!
4//! Varargs follow 5.5 semantics: a vararg call materializes a vararg table
5//! (fields 1..n plus "n") kept in the function's own stack slot; `...`
6//! expands from it and `...name` binds it. 5.1 LUAI_COMPAT_VARARG also
7//! materializes a local `arg` table (see `proto.has_compat_vararg_arg`).
8
9use crate::frontend::SyntaxError;
10use crate::jit::send_compat::TArc;
11use crate::numeric::{self, Num};
12use crate::runtime::heap::GcHeader;
13use crate::runtime::{
14 AfterClose, CallFrame, CloseCont, ContKind, Coro, CoroStatus, Frame, Gc, Heap, LuaClosure,
15 MetaAction, MetaCont, NativeClosure, NativeCont, Table, TableError, UpvalState, Upvalue, Value,
16};
17use crate::version::LuaVersion;
18use crate::vm::builtins::{nat_host_xpcall, nat_pairs, nat_pcall, nat_xpcall};
19use crate::vm::callstack::DbgKind;
20use crate::vm::error::LuaError;
21use crate::vm::isa::{Inst, Op};
22
23/// A Lua virtual machine: one OS thread's worth of Lua state.
24///
25/// # Threading model
26///
27/// `Vm` is **`!Send + !Sync`**. The GC uses `Gc<T> = NonNull<T>` over
28/// an intrusive mark-sweep heap (not `Rc<RefCell<T>>`), and the trace
29/// JIT side-table uses `Rc<CompiledTrace>` — both single-threaded by
30/// design. Embedders that want concurrency spawn one `Vm` per OS
31/// thread (or per single-thread Tokio worker) and exchange data via
32/// channels. See [`docs/threading.md`](../../docs/threading.md) for
33/// canonical embedding patterns including Tokio `current_thread`,
34/// `LocalSet` on multi-thread, and `Vm`-per-OS-thread + channels.
35///
36/// The constraint is enforced at compile time:
37///
38/// ```compile_fail
39/// fn must_be_send<T: Send>() {}
40/// must_be_send::<luna_core::Vm>(); // error[E0277]: `Vm` cannot be sent between threads safely
41/// ```
42///
43/// A future `feature = "send"` will gate an
44/// opt-in `Arc<RwLock<T>>` mode with a hard ≤8% perf regression
45/// budget.
46pub struct Vm {
47 /// The GC heap owned by this VM. Embedders normally interact via the
48 /// `Vm` methods (`load` / `call_value` / `set_global` / …) rather than
49 /// the heap directly.
50 pub heap: Heap,
51 pub(crate) stack: Vec<Value>,
52 pub(crate) frames: Vec<CallFrame>,
53 /// How many `__call` metamethods were resolved to reach each Lua frame,
54 /// by index into `frames`; each adds one argument (PUC 5.5 `CIST_CCMT`
55 /// bits, reported as `getinfo("t").extraargs`). Written whenever a Lua
56 /// frame is pushed, so the entry for a live frame is always its own;
57 /// entries past `frames.len()` are stale and never read. Kept beside
58 /// `frames` rather than in `Frame` because `Frame` is public with all
59 /// fields public, where a new field is a breaking change.
60 pub(crate) frame_ccmt: Vec<u8>,
61 /// Shadow of `self.frames.len()`. Synced on every push/pop in the
62 /// `frames_push_sync`/`frames_pop_sync` helpers (debug-asserted on
63 /// use). Not consumed by readers yet; it is scaffolding for replacing
64 /// `frames: Vec<CallFrame>` with a flat `[CallFrame; MAX_FRAMES]`
65 /// indexed by frames_top.
66 frames_top: u32,
67 /// open upvalues, sorted ascending by stack slot
68 open_upvals: Vec<(u32, Gc<Upvalue>)>,
69 /// to-be-closed slots, ascending
70 tbc: Vec<u32>,
71 /// logical stack top for multi-result sequences
72 pub(crate) top: u32,
73 globals: Gc<Table>,
74 /// shared metatable for all strings (populated by the string lib)
75 /// per-basic-type metatables (PUC luaT): indexed by `type_mt_slot`
76 /// (0 nil, 1 boolean, 2 number, 3 string, 4 function); tables carry their
77 /// own. Settable via debug.setmetatable.
78 type_mt: [Option<Gc<Table>>; 5],
79 /// pre-interned metamethod event names, indexed by `Mm`
80 mm_names: Vec<Gc<crate::runtime::LuaStr>>,
81 /// native↔Lua nesting depth (PUC C-stack guard analogue)
82 c_depth: u32,
83 /// number of live pcall/xpcall continuation frames on the running thread
84 /// (PUC counts these against nCcalls). Bounds protected-call recursion the
85 /// way `c_depth` bounds call_value recursion. Per-thread: saved/restored
86 /// with the coroutine context, since continuations survive a yield.
87 pcall_depth: u32,
88 /// number of non-yieldable C calls in flight on the running thread (PUC's
89 /// `L->nny`). A library callback that runs via synchronous Rust recursion
90 /// (sort comparator, gsub replacement) cannot be continued across a yield,
91 /// so it bumps this for its duration; `coroutine.yield` inside hits the
92 /// C-call boundary and errors. Always 0 at a suspend point (a yield can
93 /// never cross such a call), so it needs no per-thread save/restore.
94 nny: u32,
95 /// Nonzero while an xpcall message handler is on the Rust stack. Used so a
96 /// stack-overflow that surfaces *inside* the handler is reported as PUC's
97 /// "error in error handling" (LUA_ERRERR + `luaD_seterrorobj`), not the
98 /// plain "stack overflow" — errors.lua :606's `checkerr("error handling",
99 /// loop)` then matches. PUC tracks this via the soft-cap window
100 /// `nCcalls >= MAXCCALLS/10*11`; luna's c_depth is strict, so we mark the
101 /// scope explicitly.
102 pub(crate) msgh_depth: u32,
103 /// set by a coroutine closing itself (`coroutine.close()` on the running
104 /// thread): the to-be-closed handlers have already run; the thread must now
105 /// terminate. `Some(None)` is a clean close, `Some(Some(e))` a handler
106 /// raised `e`. Checked by `exec_with`/`resume_coro` to propagate (not
107 /// unwind, so a protecting pcall cannot catch it) the termination.
108 terminating: Option<Option<Value>>,
109 /// xoshiro256** state (math.random)
110 rng: [u64; 4],
111 /// VM creation time (os.clock)
112 started: std::time::Instant,
113 version: LuaVersion,
114 /// error object being threaded through a chain of __close handlers; a GC
115 /// root for the duration (a handler may trigger collection)
116 closing_err: Option<Value>,
117 /// the coroutine whose context is currently live in the fields above;
118 /// `None` while the main thread runs
119 pub(crate) current: Option<Gc<crate::runtime::Coro>>,
120 /// the main thread's saved execution context while a coroutine runs
121 main_ctx: Option<SavedCtx>,
122 /// set by `coroutine.yield` to suspend the running coroutine: the yielded
123 /// values plus the slot/result-count needed to finish the yielding call on
124 /// the next resume. Checked by `exec` to propagate (not unwind) on yield.
125 yielding: Option<(Vec<Value>, u32, i32)>,
126 /// results expected by the in-flight native call (so `yield` knows how many
127 /// values its call site wants when it suspends)
128 native_nresults: i32,
129 /// identity object for the main thread, returned by `coroutine.running`
130 /// (the main thread's context lives in the VM fields / `main_ctx`, not here)
131 main_coro: Option<Gc<Coro>>,
132 /// `collectgarbage` mode name ("incremental"/"generational"). The collector
133 /// itself is still stop-the-world mark-sweep; this tracks the mode so mode
134 /// switches report the previous one, as PUC does.
135 gc_mode: &'static str,
136 /// the live-register boundary of the running thread for GC rooting (PUC's
137 /// `L->top`): set precisely at each GC safe point so freed temporary
138 /// registers above it are not rooted. Without this the collector roots the
139 /// whole stack window, pinning weak-table values stranded in stale temps
140 /// (e.g. closure.lua's `while x[1]` GC-detection loop).
141 pub(crate) gc_top: u32,
142 /// `collectgarbage("param", name [,value])` pacing parameters. The collector
143 /// is still stop-the-world, so these are stored/returned for API fidelity
144 /// (PUC round-trips them via `setparam`/`getparam`). Defaults mirror PUC's
145 /// `LUAI_GC*` knobs: pause=200, stepmul=100, stepsize=13.
146 gc_pause: i64,
147 gc_stepmul: i64,
148 gc_stepsize: i64,
149 /// `collectgarbage`'s parameters as the dialect stores them; they set
150 /// the three knobs above through `set_gc_pacing`.
151 pub(crate) gc_params: crate::vm::lib_gc::GcParams,
152 /// true while `__gc` finalizers are being run, so a finalizer that calls
153 /// `collectgarbage` gets a no-op (PUC's non-reentrancy: lua_gc returns -1 →
154 /// `collectgarbage` yields fail).
155 gc_finalizing: bool,
156 /// C ABI scratch (`capi` module): the host-visible value stack that C
157 /// callers operate on via `lua_pushinteger` / `lua_tostring` / etc.
158 /// Kept here (instead of in a separate `LuaState` wrapper) so the
159 /// trampoline that bridges to a `LuaCFunction` can safely cast the
160 /// Vm pointer it already holds to the public `*mut LuaState` type
161 /// without any aliasing of `&mut Vm` against `&mut LuaState.vm`.
162 pub capi_stack: Vec<crate::runtime::Value>,
163 /// Pinned CString backing the pointer last returned by `lua_tostring`;
164 /// valid until the next `lua_tostring` on the same Vm.
165 pub capi_cstr_pin: Option<std::ffi::CString>,
166 /// PUC 5.4+ warning system. Lua manual §6.1 `warn`: emitted messages
167 /// concatenate across continuation calls until a non-`tocont` call
168 /// flushes; the default warnf recognises `@on`/`@off` control messages
169 /// and starts disabled. luna's `emit_warn` mirrors the default warnf
170 /// behaviour and 5.4+ `__gc` errors are routed through it (5.1–5.3
171 /// keep the older raise semantics).
172 pub(crate) warn_state: WarnState,
173 pub(crate) warn_buf: Vec<u8>,
174 /// Embedding cooperative budget: a per-Vm tick counter that the run
175 /// loop decrements once per dispatch turn. When it hits zero the loop
176 /// raises a catchable "instruction budget exceeded" error so the embedder
177 /// can yield control back to its caller (short-script eval, game
178 /// frame budgets). `None` = unbounded; reset on each call via
179 /// `set_instr_budget`.
180 pub(crate) instr_budget: Option<i64>,
181 // JIT-specific state lives in the `JitState` sidecar; see `self.jit`
182 // below and `crate::vm::jit_state` for field docs.
183 /// Bytecode-loading gate. Default `true`. Sandbox embedders should
184 /// call `set_bytecode_loading(false)` so `load`/`loadstring` reject
185 /// precompiled chunks (which bypass the parser's depth / opcode
186 /// limits). When `false`, the loader rejects any source whose first
187 /// byte is the bytecode signature `\27` ("`\27Lua`").
188 pub(crate) bytecode_loading: bool,
189 /// PUC bytecode-loading gate. Default `false` — PUC `.luac` files are
190 /// a strictly larger trust surface than luna's own dump format
191 /// (third-party toolchain bugs, malformed chunks, unknown opcode
192 /// shapes). When `true`, the loader routes `\x1bLua\x{51..55}` inputs
193 /// through the per-dialect PUC translators in `crate::vm::dump::puc`.
194 /// Embedder toggles via `set_puc_bytecode_loading`.
195 pub(crate) puc_bytecode_loading: bool,
196 /// Byte budget for source fed into `load` / `loadstring` / `Vm::load`.
197 /// Default [`Vm::DEFAULT_LOADER_INPUT_BUDGET`] (256 MiB). When the
198 /// accumulated reader output (`load(f, ...)`) or a one-shot `&[u8]`
199 /// source exceeds this, the loader returns the PUC-shaped
200 /// `not enough memory` error before the host allocator is asked to
201 /// hold the next chunk. Defends against `heavy.lua::loadrep`-style
202 /// 7 GB+ feeder loops that would otherwise SIGSEGV when `Vec::push`
203 /// crosses `isize::MAX` or the host runs out of RAM.
204 /// Embedders that genuinely need to load > 256 MiB sources widen the
205 /// cap via [`Vm::set_loader_input_budget`].
206 pub(crate) loader_input_budget: usize,
207 /// In-process log of fully-emitted warnings (each entry = one flushed
208 /// message, sans the "Lua warning: " prefix and trailing newline). Lets
209 /// tests assert what was warned without scraping stderr.
210 pub(crate) warn_log: Vec<Vec<u8>>,
211 /// PUC's `LUA_REGISTRYINDEX` table — a single Lua table the debug library
212 /// exposes via `debug.getregistry`. Used to hold `_HOOKKEY` (the weak-key
213 /// table PUC's `db_sethook` keys per-thread hooks under). luna stores hook
214 /// state directly in `Vm.hook`/`Coro.hook`, so the entry is largely a
215 /// shape stub for db.lua :328; if other registry-keyed APIs land later
216 /// they can share this table.
217 pub(crate) registry: Option<Gc<Table>>,
218 /// the shared `FILE*` metatable for io file handles (PUC's LUA_FILEHANDLE
219 /// registry entry); attached to every file userdata the io library makes
220 pub(crate) file_mt: Option<Gc<Table>>,
221 /// io library default input/output streams (PUC registry IO_INPUT/IO_OUTPUT)
222 pub(crate) io_input: Option<Gc<crate::runtime::Userdata>>,
223 pub(crate) io_output: Option<Gc<crate::runtime::Userdata>>,
224 /// `io.stdin` as the io library made it, whatever the script later does
225 /// to the `io` table: the stream a host's line reads share
226 pub(crate) io_stdin: Option<Gc<crate::runtime::Userdata>>,
227 /// lua.c's `-E`: libraries opened from now on ignore the environment
228 pub(crate) ignore_env: bool,
229 /// the running thread's debug hook state (`debug.sethook`); per-thread,
230 /// swapped with the execution context on a coroutine resume/yield
231 pub(crate) hook: HookState,
232 /// true while the hook itself runs, so its own execution fires no events
233 /// (PUC clears the mask for the duration)
234 pub(crate) in_hook: bool,
235 /// arms the next Lua frame's `tailcalls` count (PUC `ci->u.l.tailcalls`),
236 /// consumed by `push_frame`. `OP_TailCall` sets it to the caller's
237 /// own tailcalls + 1 before begin_call so deeply tail-recursive chains
238 /// accumulate the count instead of capping at 1.
239 pub(crate) pending_tailcalls: u32,
240 /// arms the next Lua frame's `ccmt` (its `__call` chain length), consumed
241 /// by `push_frame`. `OP_TailCall` sets it to the reused activation's
242 /// count; `begin_call` otherwise sets the chain it just resolved.
243 pending_ccmt: u8,
244 /// Name of the C native that just propagated an error (captured before
245 /// the native is popped from `running_natives`). Lets a dying coroutine
246 /// preserve `[C]: in function '<name>'` at the top of its traceback
247 /// snapshot — PUC walks `luaG_funcnamefrompc` over a still-live ci, but
248 /// luna's native frames are off-stack so we stash the name explicitly.
249 pub(crate) errored_natives: Vec<crate::vm::callstack::ErroredNative>,
250 /// Frames below this index are out of reach of the error handler of
251 /// an `xpcall` (PUC `L->errfunc`): a protected call made from Rust — a
252 /// finalizer, the handler itself — starts a fresh `errfunc` scope.
253 pub(crate) msgh_floor: usize,
254 /// The message handler that is running, if any. PUC's `luaG_errormsg`
255 /// calls the handler with `L->errfunc` still set, so an error inside
256 /// the handler (and not caught within it) calls the handler again, at
257 /// the point of that error. Per thread, like `L->errfunc`.
258 pub(crate) msgh_running: Option<Value>,
259 /// How many message-handler runs have started; lets a run tell whether
260 /// the error it got back was already handled by a nested run.
261 pub(crate) msgh_runs: u64,
262 /// The value the last `xpcall` handler produced for the error in
263 /// flight, so the unwind that carries it to the `xpcall` does not
264 /// run the handler again.
265 pub(crate) msgh_applied: Option<Value>,
266 /// Whether an error nothing catches should keep its traceback: not
267 /// inside a protected call made from Rust, which discards it.
268 pub(crate) keep_error_traceback: bool,
269 /// PUC `CallInfo.u2.transferinfo`: index of the first transferred value
270 /// (relative to the activation's func slot) and the number transferred.
271 /// Set just before firing a call/return hook, read by `getinfo("r")`.
272 pub(crate) hook_ftransfer: u16,
273 pub(crate) hook_ntransfer: u16,
274 /// metamethod event tag (e.g. "close") to attach to the next Lua frame
275 /// pushed by `push_frame`; `close_slots` sets this before calling a
276 /// `__close` handler so `debug.traceback` names it "metamethod 'close'"
277 /// (PUC `CallInfo.u.l.tm`). Single-shot: `push_frame` consumes it.
278 pending_tm: Option<&'static str>,
279 /// `true` when the next `push_frame` is the user hook function itself,
280 /// so `debug.getinfo(1).namewhat` resolves to `"hook"` (PUC
281 /// `CIST_HOOKED`). `run_hook` arms it before dispatching the hook.
282 pending_is_hook: bool,
283 /// traceback of an error nothing in its thread catches, one line per
284 /// stack level, taken where it was raised (see `raise_to_handler`): what
285 /// the host gets from `take_error_traceback`, and what `debug.traceback`
286 /// shows of the coroutine it kills. Cleared on a catch and at host-level
287 /// `call_value` entry (`public_call_depth == 0`).
288 pub(crate) error_traceback: Option<Vec<Vec<u8>>>,
289 /// nesting depth of public `call_value` entries (host vs. internal). The
290 /// outermost entry (depth 0) resets per-error state (`error_traceback`);
291 /// internal calls (e.g. xpcall msgh, sort callback) preserve it.
292 public_call_depth: u32,
293 /// stack of native (`Value::Native`) closures currently running on the
294 /// Rust call stack. `begin_call` pushes the closure before invoking
295 /// `nc.f` and pops on return. Used by `arg_error` to detect a *nested*
296 /// native call (PUC `ar.name == NULL` at level 0 because the level-0
297 /// caller is C, not Lua) and qualify the running function's name via
298 /// `pushglobalfuncname` (e.g. `'sort'` → `'table.sort'`).
299 pub(crate) running_natives: Vec<Gc<NativeClosure>>,
300 /// Parallel to `running_natives`: where each native sits on the value
301 /// and frame stacks, so the debug interface can place it among the Lua
302 /// activations as PUC's CallInfo chain would (see `callstack`).
303 pub(crate) running_native_acts: Vec<crate::vm::callstack::NativeAct>,
304 /// Index into `running_natives` where the running thread's own natives
305 /// begin; the ones below belong to the threads that resumed it.
306 pub(crate) natives_base: usize,
307 /// JIT sidecar. Always present (never `Option`); inert
308 /// when `chunk_compiler` / `trace_compiler` are
309 /// [`crate::jit::NullJitBackend`]. See [`crate::vm::jit_state`].
310 ///
311 /// `#[doc(hidden)] pub` so the `luna` crate's
312 /// `extern "C"` JIT helpers can write `vm.jit.pending_err`
313 /// directly. Not part of the embedder-facing API surface.
314 #[doc(hidden)]
315 pub jit: crate::vm::jit_state::JitState,
316
317 /// Host roots — a `Vec<Value>` traced as an extra GC root set.
318 /// `Lua` facade handles (`LuaFunction`, `LuaTable`, `LuaRoot`) hold
319 /// indices into this vector so the underlying `Gc<T>` stays alive
320 /// across `eval` calls / yield boundaries. Freed slots are recycled
321 /// through `host_roots_free`.
322 pub(crate) host_roots: Vec<crate::vm::host_roots::HostRootSlot>,
323 /// Recycled-slot index pool. `pin_host` pops the
324 /// back if non-empty, else extends `host_roots`. Generation
325 /// overflow at `u32::MAX` retires the slot (NOT pushed here).
326 pub(crate) host_roots_free: Vec<u32>,
327
328 /// GC-rooted scratch stack for `table.sort` (and any other
329 /// builtin that needs a Rust-side `Vec<Value>` to outlive a user
330 /// callback). Each entry is one in-flight working buffer; `gc_roots`
331 /// extends with every contained `Value` so a `collectgarbage()`
332 /// inside the comparator cannot free strings/tables snapshotted
333 /// here. Nested sorts push a new buffer on entry, pop on exit
334 /// (sort.lua's `load(..)(); collectgarbage()` compare callback
335 /// regression).
336 pub(crate) sort_scratch: Vec<Vec<Value>>,
337
338 /// MacroLua compile-time macro registry.
339 /// Pre-populated with built-in macros (`@quote` / `@unquote` /
340 /// `@if` / `@gensym`) at construction time when `version ==
341 /// LuaVersion::MacroLua`; embedders register custom macros via
342 /// [`Vm::define_macro`]. The expander runs once per `load()` call
343 /// between lexing and parsing (only when `is_macro_lua()`).
344 pub(crate) macro_registry: crate::frontend::macro_expander::MacroRegistry,
345
346 /// Per-Vm cache of `Gc<Table>` metatables keyed
347 /// by `TypeId::of::<T>()` for embedder types implementing
348 /// [`crate::vm::userdata_trait::LuaUserdata`]. Populated lazily by
349 /// [`Vm::register_userdata`]; metatables are pinned via
350 /// [`Vm::pin_host`] at registration time so the entry's
351 /// `Gc<Table>` stays live for the rest of the Vm's lifetime.
352 pub(crate) userdata_metatables:
353 std::collections::HashMap<std::any::TypeId, Gc<crate::runtime::table::Table>>,
354
355 /// Classification of the most recent error raised on this Vm.
356 /// Embedders read via [`Vm::error_kind`]; the dispatcher sets it
357 /// at well-known sites (syntax errors, instr-budget trips, native
358 /// callback errors, type errors).
359 pub(crate) last_error_kind: crate::vm::error::LuaErrorKind,
360
361 /// `(source_name, line)` of the most recent error. Set by the
362 /// dispatcher / lexer / parser; cleared when a new call_value
363 /// enters cleanly.
364 pub(crate) last_error_source: Option<(String, u32)>,
365
366 /// When `true`, `instr_budget` exhaustion in
367 /// the dispatcher hot loop yields cooperatively (sets
368 /// [`Vm::host_yield_pending`] + returns a sentinel `Err` walked up
369 /// to `EvalFuture::poll`) instead of returning a real
370 /// "instruction budget exceeded" error. Set by [`Vm::eval_async`]
371 /// for the duration of the future; restored to `false` on
372 /// `Poll::Ready`. The sync `Vm::eval` / `Vm::call_value` paths
373 /// leave it `false` so budget exhaustion stays a real error there.
374 pub(crate) async_mode: bool,
375
376 /// Host waker cloned by `EvalFuture::poll` before driving a slice.
377 /// The dispatcher itself does not call it (the future's poll loop
378 /// does `wake_by_ref` after observing `BudgetExhausted`); it is kept
379 /// so async natives can wake the host directly from a helper future.
380 pub(crate) async_waker: Option<std::task::Waker>,
381
382 /// Per-poll opcode quota loaded into
383 /// `instr_budget` at the start of each `EvalFuture::poll` slice.
384 /// Default 10_000. Tunable via
385 /// [`Vm::set_async_slice`].
386 pub(crate) async_slice_size: i64,
387
388 /// Set by the dispatcher when an async-mode
389 /// budget exhaustion fires; checked by `exec_with` (so the
390 /// sentinel propagates without `unwind` running, mirroring
391 /// `yielding.is_some()`) and by `call_value_impl` (so the call
392 /// frames survive for the next poll). Cleared by `drive_one`
393 /// after translating it to `DispatchOutcome::BudgetExhausted`.
394 pub(crate) host_yield_pending: bool,
395
396 /// Set by the dispatcher's native-call path
397 /// when an async-marked [`NativeClosure`] is invoked under
398 /// `async_mode`. The Vm pauses the dispatcher (same sentinel-Err
399 /// mechanism as `host_yield_pending` — see `exec_with` +
400 /// `call_value_impl`), stashes the in-flight future +
401 /// post-completion context here, and surfaces them to
402 /// `EvalFuture::poll` via `drive_one`. Cleared by `drive_one`
403 /// once the future is moved out into a
404 /// `DispatchOutcome::AsyncNativeAwaiting`.
405 pub(crate) pending_async_native_fut:
406 Option<std::pin::Pin<Box<dyn std::future::Future<Output = Result<u32, LuaError>>>>>,
407
408 /// Companion to `pending_async_native_fut`:
409 /// the `(func_slot, nargs, nresults, gc_top)` quad needed to
410 /// commit the future's eventual `Ok(nret)` back into the calling
411 /// frame's expected result slots. Recorded by the dispatcher;
412 /// consumed by [`Vm::commit_async_native_result`] after the
413 /// future resolves.
414 pub(crate) pending_async_native_ctx: Option<AsyncNativeCallCtx>,
415
416 /// Identifies this Vm to the JIT storages it compiles through
417 /// ([`crate::jit::JitStorage::claim`]).
418 jit_owner_id: u64,
419 /// Storages [`Vm::install_jit_storage`] replaced: code compiled into
420 /// them may still be referenced by this Vm's functions, so they live
421 /// as long as the Vm.
422 retired_jit_storage: Vec<Box<dyn crate::jit::JitStorage>>,
423}
424
425/// Call-site context an in-flight async native
426/// needs preserved across the cooperative-yield boundary.
427///
428/// The dispatcher records this when it routes a `NativeClosure` with
429/// `is_async == true` through the cooperative path; `EvalFuture::poll`
430/// hands it back to [`Vm::commit_async_native_result`] once the
431/// awaited future resolves so `finish_results` (and the post-call GC
432/// checkpoint) can run as if the native had completed synchronously.
433#[derive(Clone, Copy)]
434pub(crate) struct AsyncNativeCallCtx {
435 pub func_slot: u32,
436 /// Recorded for parity with the sync native-call path's
437 /// `native_nresults`/`gc_top` bookkeeping; reserved for hook
438 /// firing + traceback shaping. Not read yet.
439 #[allow(dead_code)]
440 pub nargs: u32,
441 pub nresults: i32,
442 /// Recorded for traceback + GC-root-window checks. The resume path
443 /// reads `Vm.gc_top` directly, so this is unread today; carried so a
444 /// check can confirm the pre-suspend root window matches the
445 /// post-resume one.
446 #[allow(dead_code)]
447 pub gc_top: u32,
448}
449
450/// Per-thread debug hook state (PUC `lua_State` hook/hookmask/basehookcount/
451/// hookcount). `func` is the Lua hook; the booleans are the PUC mask bits.
452#[derive(Clone, Copy, Default)]
453pub struct HookState {
454 /// the hook function (`None` when no hook is installed)
455 pub func: Option<Value>,
456 /// Rust-side debug hook. Fires alongside the Lua hook
457 /// (Rust first); both can be installed simultaneously, but most
458 /// embedders pick one.
459 pub rust_func: Option<RustDebugHook>,
460 /// LUA_MASKCALL — fire on function entry
461 pub call: bool,
462 /// LUA_MASKRET — fire on function return
463 pub ret: bool,
464 /// LUA_MASKLINE — fire on source-line change
465 pub line: bool,
466 /// LUA_MASKCOUNT — fire every `count_base` instructions
467 pub count: bool,
468 /// instruction count between count events (PUC basehookcount)
469 pub count_base: i64,
470 /// instructions left until the next count event (PUC hookcount)
471 pub count_left: i64,
472}
473
474/// Rust-side debug hook callback. Receives the `Vm` plus a
475/// classified event. The callback runs synchronously in the
476/// dispatcher; the hook flag (`in_hook`) is set for its duration so
477/// hook recursion is suppressed.
478pub type RustDebugHook = fn(&mut Vm, RustHookEvent);
479
480/// Classified debug event delivered to a [`RustDebugHook`].
481#[derive(Clone, Copy, Debug, PartialEq, Eq)]
482pub enum RustHookEvent {
483 /// Function entry (`hook_call` analogue).
484 Call,
485 /// Function return (`hook_return` analogue).
486 Return,
487 /// Tail call entry (PUC 5.2+ separates this from a plain Call).
488 TailCall,
489 /// Source-line change (the `u32` is the 1-based line number).
490 Line(u32),
491 /// Instruction count event (fires every `count_base` instructions).
492 Count,
493}
494
495/// Mask flags for [`Vm::set_rust_debug_hook`]. OR these to subscribe
496/// to multiple event categories with a single hook installation.
497pub const HOOK_MASK_CALL: u32 = 1;
498/// Subscribe to function-return events.
499pub const HOOK_MASK_RETURN: u32 = 2;
500/// Subscribe to line-change events.
501pub const HOOK_MASK_LINE: u32 = 4;
502/// Subscribe to instruction-count events.
503pub const HOOK_MASK_COUNT: u32 = 8;
504
505/// A thread's swapped-out execution context (PUC per-thread stack state).
506struct SavedCtx {
507 stack: Vec<Value>,
508 frames: Vec<CallFrame>,
509 frame_ccmt: Vec<u8>,
510 open_upvals: Vec<(u32, Gc<Upvalue>)>,
511 tbc: Vec<u32>,
512 top: u32,
513 pcall_depth: u32,
514 hook: HookState,
515 /// PUC `L->l_gt` — the thread's own globals table. Carried alongside
516 /// the rest of the suspended state so each thread can keep its own
517 /// `setfenv(0, env)` rewire without the swap leaking into another
518 /// thread (5.1 closure.lua :177).
519 globals: Gc<Table>,
520}
521
522/// Outcome of unwinding the call stack on an error (see `Vm::unwind`).
523enum Unwound {
524 /// caught by a pcall/xpcall continuation; resume running its caller
525 Caught,
526 /// caught by a continuation that was the entry-level activation; these are
527 /// the call's (wrapped) results
528 CaughtReturn(Vec<Value>),
529 /// no protecting continuation up to `entry_depth`; propagate the error
530 Propagated(LuaError),
531}
532
533/// Outcome of an index/newindex/comparison fast path: either a directly
534/// computed result, or a metamethod (with the receiver it resolved against) the
535/// caller must invoke — synchronously (C context) or yieldably (VM opcode).
536enum MmOut {
537 /// index → the looked-up value; newindex → done (raw set performed);
538 /// comparison → the boolean result already known
539 Done(Value),
540 /// a metamethod to call; `recv` is the chain element it was found on (the
541 /// extra args — key / value — are supplied by the caller)
542 Mm { func: Value, recv: Value },
543 /// ≤5.3 `a <= b` synthesised via `not __lt(b, a)` when neither operand
544 /// carries `__le` — `op_compare` swaps the args and negates the result.
545 /// Lives separate from `Mm` so the synth path can stay yieldable without
546 /// every other Mm caller learning a swap flag they would never set.
547 CompareSynth { func: Value },
548}
549
550/// Metamethod events; discriminants index `Vm::mm_names`.
551#[derive(Clone, Copy, PartialEq, Eq)]
552#[repr(usize)]
553pub(crate) enum Mm {
554 Index,
555 NewIndex,
556 Call,
557 ToString,
558 Metatable,
559 Name,
560 Eq,
561 Lt,
562 Le,
563 Concat,
564 Len,
565 Add,
566 Sub,
567 Mul,
568 Div,
569 Mod,
570 Pow,
571 IDiv,
572 BAnd,
573 BOr,
574 BXor,
575 Shl,
576 Shr,
577 Unm,
578 BNot,
579 Close,
580 Gc,
581 Pairs,
582}
583
584const MM_NAMES: [&str; 28] = [
585 "__index",
586 "__newindex",
587 "__call",
588 "__tostring",
589 "__metatable",
590 "__name",
591 "__eq",
592 "__lt",
593 "__le",
594 "__concat",
595 "__len",
596 "__add",
597 "__sub",
598 "__mul",
599 "__div",
600 "__mod",
601 "__pow",
602 "__idiv",
603 "__band",
604 "__bor",
605 "__bxor",
606 "__shl",
607 "__shr",
608 "__unm",
609 "__bnot",
610 "__close",
611 "__gc",
612 "__pairs",
613];
614
615/// The metamethod event an opcode dispatches, without the `__` prefix (PUC
616/// funcnamefromcode), for "(metamethod 'event')" call-error suffixes.
617fn mm_event_name(op: crate::vm::isa::Op) -> Option<&'static str> {
618 use crate::vm::isa::Op;
619 Some(match op {
620 Op::Add => "add",
621 Op::Sub => "sub",
622 Op::Mul => "mul",
623 Op::Div => "div",
624 Op::Mod => "mod",
625 Op::Pow => "pow",
626 Op::IDiv => "idiv",
627 Op::BAnd => "band",
628 Op::BOr => "bor",
629 Op::BXor => "bxor",
630 Op::Shl => "shl",
631 Op::Shr => "shr",
632 Op::Unm => "unm",
633 Op::BNot => "bnot",
634 Op::Concat => "concat",
635 Op::Len => "len",
636 Op::GetField | Op::GetTable | Op::GetI | Op::SelfOp => "index",
637 Op::SetField | Op::SetTable | Op::SetI => "newindex",
638 Op::Eq | Op::EqK => "eq",
639 Op::Lt => "lt",
640 Op::Le => "le",
641 _ => return None,
642 })
643}
644
645/// PUC MAXTAGLOOP (5.3+): bound on `__index`/`__newindex` chains.
646const MAX_TAG_LOOP: u32 = 2000;
647/// PUC `MAXCCMT`: bound on a `__call` metamethod chain (lvm.c). 200 chains
648/// is more than any reasonable program needs and matches PUC 5.4/5.5; a
649/// bound of `15` is tight enough to fire on calls.lua :194 (N=20).
650const MAX_CCMT: u32 = 200;
651/// PUC LUAI_MAXCCALLS analogue: native↔Lua nesting bound.
652pub(crate) const MAX_C_DEPTH: u32 = 200;
653/// Stack an xpcall handler may use past `MAX_LUA_STACK` while handling a
654/// stack overflow: PUC's 200 extra `ERRORSTACKSIZE` slots, plus the frame
655/// reserve (256) the overflowing call was refused, so the handler's first
656/// frame fits where that one did not.
657const ERROR_STACK_EXTRA: u32 = 200 + 256;
658/// luna's engine-level VM stack cap (used by call-site overflow checks).
659/// Slightly larger than PUC's `LUAI_MAXSTACK` so engine internals have a
660/// little headroom above any single library push.
661const MAX_LUA_STACK: u32 = 1 << 20;
662/// PUC `LUAI_MAXSTACK` (`luaconf.h`): the cap library code consults via
663/// `lua_checkstack` to refuse multi-value pushes (`table.unpack` returning
664/// N values, `string.pack` results, etc.). 5.3 coroutine.lua :530 pins
665/// this at one million — `for j in {lim-10, …}` expects every j ≥ lim-10
666/// to fail because the few slots already consumed in the coroutine push
667/// the effective cap below lim-10.
668const PUC_MAXSTACK: i64 = 1_000_000;
669
670/// PUC 5.4+ default warnf state. The base library's `warn` function flips
671/// between `Off` and `On` via the `@on` / `@off` control messages; any other
672/// `@<word>` control is silently ignored, mirroring `lauxlib.c::checkcontrol`.
673#[derive(Clone, Copy, PartialEq, Eq, Debug)]
674pub enum WarnState {
675 /// `warn` calls are silently dropped (default after `warn("@off")`).
676 Off,
677 /// `warn` calls are delivered to stderr (after `warn("@on")`).
678 On,
679}
680
681/// Best-effort extraction of a textual message from a `catch_unwind` payload.
682/// `panic!("msg")` arrives as `String`, `panic!(static)` as `&str`; anything
683/// else degrades to `"<non-string panic>"`. Used by the native-call
684/// catch_unwind to fold the panic into a Lua error.
685fn panic_payload_str(payload: &Box<dyn std::any::Any + Send>) -> String {
686 if let Some(s) = payload.downcast_ref::<String>() {
687 return s.clone();
688 }
689 if let Some(s) = payload.downcast_ref::<&'static str>() {
690 return (*s).to_string();
691 }
692 "<non-string panic>".to_string()
693}
694
695/// Combined error type returned by [`Vm::eval`] and friends — either the
696/// chunk failed to parse / compile, or it raised at runtime.
697#[derive(Debug)]
698pub enum Error {
699 /// Parse or compile failure.
700 Syntax(SyntaxError),
701 /// Runtime error raised during execution.
702 Runtime(LuaError),
703}
704
705impl From<SyntaxError> for Error {
706 fn from(e: SyntaxError) -> Error {
707 Error::Syntax(e)
708 }
709}
710
711impl From<LuaError> for Error {
712 fn from(e: LuaError) -> Error {
713 Error::Runtime(e)
714 }
715}
716
717impl Vm {
718 /// `lua_close` from inside a running script (`os.exit(code, true)`):
719 /// close the main thread's pending to-be-closed variables, then run every
720 /// finalizer. Both run protected, so their errors are dropped as PUC's
721 /// `close_state` drops them. Inside a coroutine the main thread's stack is
722 /// parked, and only the finalizers run.
723 pub(crate) fn close_state(&mut self) {
724 if self.current.is_none() {
725 let _ = self.close_slots(0, None);
726 }
727 self.heap.queue_all_finalizers();
728 self.run_finalizers();
729 }
730}
731
732impl Drop for Vm {
733 fn drop(&mut self) {
734 // state close: run `__gc` for every still-registered finalizable before
735 // the heap frees them (PUC separatetobefnz(g,1) + callallpending). A
736 // single pass — objects created by a closing finalizer are not
737 // re-finalized (they go to the heap's free list directly).
738 self.heap.queue_all_finalizers();
739 self.run_finalizers();
740 let id = self.jit_owner_id;
741 // SAFETY: the finalizers were the last Lua code this Vm runs, and
742 // its functions (the only holders of entry points compiled for it)
743 // go away with its heap.
744 unsafe {
745 self.jit.storage.release_code(id);
746 for s in &mut self.retired_jit_storage {
747 s.release_code(id);
748 }
749 }
750 }
751}
752
753// Split-borrow free fn helpers for frames push/pop with shadow counter
754// `frames_top: u32`. Free fns (not Vm methods) so callers can pass
755// `&mut self.frames` + `&mut self.frames_top` as split borrows, allowing
756// other `&mut self.field` reads inside the CallFrame construction (e.g.
757// `std::mem::take(&mut self.pending_tm)`).
758//
759// The shadow has no readers yet; it just stays in sync + asserts.
760#[inline(always)]
761fn frames_push_sync(frames: &mut Vec<CallFrame>, frames_top: &mut u32, cf: CallFrame) {
762 frames.push(cf);
763 // Shadow maintenance is debug-only: release builds skip the
764 // increment + assertion entirely. While nothing reads the shadow,
765 // its purpose is to VERIFY the assumed invariant
766 // (frames_top == frames.len()) across all push/pop sites; once readers
767 // consume it, release must run the increment unconditionally.
768 #[cfg(debug_assertions)]
769 {
770 *frames_top += 1;
771 debug_assert_eq!(
772 *frames_top as usize,
773 frames.len(),
774 "P17-D frames_top out of sync after push",
775 );
776 }
777 #[cfg(not(debug_assertions))]
778 let _ = frames_top;
779}
780
781#[inline(always)]
782fn frames_pop_sync(frames: &mut Vec<CallFrame>, frames_top: &mut u32) -> Option<CallFrame> {
783 let r = frames.pop();
784 #[cfg(debug_assertions)]
785 {
786 if r.is_some() {
787 *frames_top = frames_top.saturating_sub(1);
788 }
789 debug_assert_eq!(
790 *frames_top as usize,
791 frames.len(),
792 "P17-D frames_top out of sync after pop",
793 );
794 }
795 #[cfg(not(debug_assertions))]
796 let _ = frames_top;
797 r
798}
799
800/// One-time env-var read for
801/// `LUNA_AOT_PROBE`. Returns `true` iff the env var is set to any
802/// non-empty value. The result is cached in a `OnceLock` so the
803/// dispatcher's hot path pays a single atomic load per process. Off
804/// by default — production deploys don't bleed diagnostic prints.
805fn jit_probe_enabled() -> bool {
806 static PROBE_ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
807 *PROBE_ON.get_or_init(|| {
808 std::env::var("LUNA_AOT_PROBE")
809 .ok()
810 .filter(|v| !v.is_empty())
811 .is_some()
812 })
813}
814
815impl Vm {
816 /// Re-sync `frames_top` after a bulk `frames: Vec`
817 /// swap (take_ctx, put_ctx, load_coro_ctx). Must be called after
818 /// the Vec replacement to keep the shadow valid.
819 #[inline(always)]
820 fn frames_resync(&mut self) {
821 // Debug-only — see `frames_push_sync` comment.
822 #[cfg(debug_assertions)]
823 {
824 self.frames_top = self.frames.len() as u32;
825 }
826 }
827
828 // ====================================================================
829 // Stack-inline frame metadata accessors (unused).
830 //
831 // These methods read/write the LJ_FR2 marker slots at `stack[base-2]`
832 // (closure GCRef) and `stack[base-1]` (FrameMarker as i64). No call
833 // site uses them yet.
834 //
835 // Preconditions (debug-asserted):
836 // - base >= 2 (slots base-2 and base-1 must exist below the frame)
837 // - self.stack.len() > base + max_stack (caller has grown stack)
838 // - For Lua frames, stack[base-2] holds Value::Closure(cl)
839 // - For Lua frames, stack[base-1] holds Value::Int(marker.to_raw())
840 //
841 // No release-build cost when unused (LTO strips dead methods).
842 // ====================================================================
843
844 /// Write a Lua frame's closure pointer into `stack[base-2]`.
845 /// The caller must ensure `base >= 2` and the slot is within the
846 /// stack's allocated range.
847 #[inline]
848 #[allow(dead_code)] // no consumer yet
849 fn write_frame_closure(&mut self, base: u32, cl: crate::runtime::Gc<LuaClosure>) {
850 debug_assert!(
851 base >= 2,
852 "frame closure slot needs base >= 2; got {}",
853 base
854 );
855 let idx = (base - 2) as usize;
856 debug_assert!(idx < self.stack.len(), "stack[base-2] out of range");
857 self.stack[idx] = Value::Closure(cl);
858 }
859
860 /// Read a Lua frame's closure pointer from `stack[base-2]`.
861 /// Returns `None` if the slot doesn't hold a closure (caller is
862 /// expected to treat that as a corrupt frame).
863 ///
864 /// Uses the [`Value::tag_byte`] fast-path
865 /// to avoid the enum-match cost on the hot path. Tag check via
866 /// 1-byte load + branch + `as_closure_unchecked` payload load.
867 #[inline]
868 #[allow(dead_code)]
869 fn read_frame_closure(&self, base: u32) -> Option<crate::runtime::Gc<LuaClosure>> {
870 debug_assert!(base >= 2);
871 let v = self.stack.get((base - 2) as usize)?;
872 if v.tag_byte() == crate::runtime::value::tag::CLOSURE {
873 // SAFETY: tag byte just verified == CLOSURE.
874 Some(unsafe { v.as_closure_unchecked() })
875 } else {
876 None
877 }
878 }
879
880 /// Write a packed [`FrameMarker`] into `stack[base-1]`. The marker
881 /// encodes the frame kind (Lua / Cont) + PC-or-delta payload.
882 /// Stored as `Value::Int(marker.to_raw())` so it round-trips
883 /// cleanly through the value stack without losing bits.
884 #[inline]
885 #[allow(dead_code)]
886 fn write_frame_marker(&mut self, base: u32, marker: crate::runtime::frame_marker::FrameMarker) {
887 debug_assert!(base >= 1, "frame marker slot needs base >= 1; got {}", base);
888 let idx = (base - 1) as usize;
889 debug_assert!(idx < self.stack.len(), "stack[base-1] out of range");
890 self.stack[idx] = Value::Int(marker.to_raw());
891 }
892
893 /// Read a packed [`FrameMarker`] from `stack[base-1]`. Returns
894 /// `None` if the slot isn't a `Value::Int` (caller treats as a
895 /// corrupt frame); the kind tag itself may still be invalid, in
896 /// which case [`FrameMarker::kind`] returns `None` on the result.
897 ///
898 /// Uses the [`Value::tag_byte`] fast-path
899 /// for the tag check + `as_int_unchecked` for the payload load.
900 #[inline]
901 #[allow(dead_code)]
902 fn read_frame_marker(&self, base: u32) -> Option<crate::runtime::frame_marker::FrameMarker> {
903 debug_assert!(base >= 1);
904 let v = self.stack.get((base - 1) as usize)?;
905 if v.tag_byte() == crate::runtime::value::tag::INT {
906 // SAFETY: tag byte just verified == INT.
907 Some(crate::runtime::frame_marker::FrameMarker::from_raw(
908 unsafe { v.as_int_unchecked() },
909 ))
910 } else {
911 None
912 }
913 }
914
915 /// Build the raw `Vm` struct without main coroutine / RNG seed / library
916 /// setup. Private helper shared by `Vm::new` and `Vm::new_minimal`; the
917 /// caller is responsible for the rest of the bring-up.
918 fn new_inner(version: LuaVersion) -> Vm {
919 let mut heap = Heap::new();
920 // PUC 5.1 had no ephemeron pass — `__mode='k'` tables marked their
921 // values strongly. gc.lua's "weak tables" section relies on that.
922 heap.no_ephemeron = version <= LuaVersion::Lua51;
923 // PUC 5.3 needs two GC cycles to finalize a table caught in a
924 // coroutine reference cycle (gc.lua :502); 5.4+ rewrote the GC and
925 // finalize in a single cycle (5.4/5.5 gc.lua :544 assert exactly one).
926 heap.defer_thread_cycle_finalize = version == LuaVersion::Lua53;
927 let globals = heap.new_table();
928 let mm_names = MM_NAMES.iter().map(|n| heap.intern(n.as_bytes())).collect();
929
930 Vm {
931 heap,
932 stack: Vec::new(),
933 frames: Vec::new(),
934 frame_ccmt: Vec::new(),
935 frames_top: 0,
936 open_upvals: Vec::new(),
937 tbc: Vec::new(),
938 top: 0,
939 globals,
940 type_mt: [None; 5],
941 mm_names,
942 c_depth: 0,
943 pcall_depth: 0,
944 nny: 0,
945 msgh_depth: 0,
946 terminating: None,
947 rng: [0; 4],
948 started: std::time::Instant::now(),
949 version,
950 closing_err: None,
951 current: None,
952 main_ctx: None,
953 yielding: None,
954 native_nresults: -1,
955 main_coro: None,
956 // PUC 5.4+ boots in GENERATIONAL mode (the first
957 // `collectgarbage("generational")` reports "generational"
958 // as the previous mode on stock lua5.4;
959 // 5.5 behaves the same, probed against lua5.5). luna's
960 // collector is a single incremental engine either way;
961 // this field is the MODE REPORT the stdlib exposes.
962 gc_mode: if version >= crate::version::LuaVersion::Lua54 {
963 "generational"
964 } else {
965 "incremental"
966 },
967 gc_top: 0,
968 gc_pause: 200,
969 gc_stepmul: 100,
970 gc_stepsize: 13,
971 gc_params: crate::vm::lib_gc::GcParams::new(version),
972 gc_finalizing: false,
973 capi_stack: Vec::new(),
974 capi_cstr_pin: None,
975 warn_state: WarnState::Off,
976 warn_buf: Vec::new(),
977 warn_log: Vec::new(),
978 instr_budget: None,
979 bytecode_loading: true,
980 puc_bytecode_loading: false,
981 loader_input_budget: Vm::DEFAULT_LOADER_INPUT_BUDGET,
982 registry: None,
983 file_mt: None,
984 io_input: None,
985 io_output: None,
986 io_stdin: None,
987 ignore_env: false,
988 hook: HookState::default(),
989 in_hook: false,
990 pending_tailcalls: 0,
991 pending_ccmt: 0,
992 errored_natives: Vec::new(),
993 msgh_floor: 0,
994 msgh_running: None,
995 msgh_runs: 0,
996 msgh_applied: None,
997 keep_error_traceback: true,
998 hook_ftransfer: 0,
999 hook_ntransfer: 0,
1000 pending_tm: None,
1001 pending_is_hook: false,
1002 error_traceback: None,
1003 public_call_depth: 0,
1004 running_natives: Vec::new(),
1005 running_native_acts: Vec::new(),
1006 natives_base: 0,
1007 // JIT-specific state lives in the `JitState`
1008 // sidecar. The `luna` crate's `Vm::new_minimal_with_jit` /
1009 // `install_jit_backend` / `luaL_newstate` swap in
1010 // `CraneliftBackend` for callers that want JIT acceleration.
1011 jit: crate::vm::jit_state::JitState::with_null_backend(),
1012 // host roots ticket pool for the `Lua` facade
1013 host_roots: Vec::new(),
1014 // MacroLua registry. Pre-populated with
1015 // built-ins (`@quote` / `@unquote` / `@if` / `@gensym`)
1016 // when this Vm is constructed under `LuaVersion::MacroLua`.
1017 macro_registry: if version == LuaVersion::MacroLua {
1018 crate::frontend::macro_expander::MacroRegistry::with_builtins()
1019 } else {
1020 crate::frontend::macro_expander::MacroRegistry::new()
1021 },
1022 host_roots_free: Vec::new(),
1023 sort_scratch: Vec::new(),
1024 // LuaUserdata trait sugar's per-Vm
1025 // metatable cache. Populated lazily by register_userdata.
1026 userdata_metatables: std::collections::HashMap::new(),
1027 // Error classification metadata. Defaults to
1028 // Runtime; set at known sites (syntax / budget trip /
1029 // native error / type error).
1030 last_error_kind: crate::vm::error::LuaErrorKind::default(),
1031 last_error_source: None,
1032 // Async embedder fields. Defaults preserve sync behavior
1033 // bit-for-bit (`async_mode = false` means the budget hot loop
1034 // errors out instead of yielding).
1035 async_mode: false,
1036 async_waker: None,
1037 async_slice_size: 10_000,
1038 host_yield_pending: false,
1039 // Pending async-native state. Empty by
1040 // default; populated only by the dispatcher when an
1041 // async-marked NativeClosure is invoked under async_mode.
1042 pending_async_native_fut: None,
1043 pending_async_native_ctx: None,
1044 jit_owner_id: {
1045 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
1046 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
1047 },
1048 retired_jit_storage: Vec::new(),
1049 }
1050 }
1051
1052 /// Build a fully-loaded Vm — the default for embedders that want PUC's
1053 /// standard library surface. Equivalent to `Vm::new_minimal(version)`
1054 /// followed by `vm.open_all_libs()`.
1055 pub fn new(version: LuaVersion) -> Vm {
1056 let mut vm = Vm::new_minimal(version);
1057 vm.open_all_libs();
1058 vm
1059 }
1060
1061 /// Build a Vm with no standard libraries loaded. Embedders
1062 /// that want a sandbox (Redis-style scripts, in-game scripting with
1063 /// a curated API) call this and then `open_base` / `open_math` / etc.
1064 /// selectively. The Vm is otherwise fully initialized (main coroutine,
1065 /// RNG seed, GC) so `eval` and `call_value` are immediately usable.
1066 pub fn new_minimal(version: LuaVersion) -> Vm {
1067 let mut vm = Vm::new_inner(version);
1068 let mc = vm.heap.new_coro(Value::Nil, vm.globals);
1069 // 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).
1070 unsafe { mc.as_mut() }.status = CoroStatus::Running;
1071 vm.main_coro = Some(mc);
1072 let (a, b) = vm.rng_auto_seed();
1073 vm.rng_seed(a as u64, b as u64);
1074 vm
1075 }
1076
1077 /// Install a caller-supplied JIT backend. The
1078 /// `luna` crate uses this to swap in its `CraneliftBackend`; tests
1079 /// or third-party backends pass their own [`crate::jit::IntChunkCompiler`] /
1080 /// [`crate::jit::TraceCompiler`] implementations. Re-installing on a Vm whose
1081 /// closures already populated `Proto.jit: JitProtoState::Compiled`
1082 /// does NOT evict those cached entries — call right after
1083 /// construction for a clean swap.
1084 ///
1085 /// Naming: `install_jit_backend` (not `install_default_jit`)
1086 /// because the "default" in luna-core is `NullJitBackend`; the
1087 /// "default JIT" lives in the `luna` crate.
1088 pub fn install_jit_backend<C, T>(&mut self, chunk: C, trace: T)
1089 where
1090 C: crate::jit::IntChunkCompiler + 'static,
1091 T: crate::jit::TraceCompiler + 'static,
1092 {
1093 self.jit.chunk_compiler = Box::new(chunk);
1094 self.jit.trace_compiler = Box::new(trace);
1095 }
1096
1097 /// Install a caller-supplied JIT
1098 /// storage holder. Default is [`crate::jit::NullJitStorage`];
1099 /// the `luna_jit` crate's `install_default_jit` pairs this with
1100 /// `install_jit_backend(CraneliftBackend, CraneliftBackend)` to
1101 /// also install a fresh `CraneliftJitStorage`. Storage holds
1102 /// the per-`Vm` JIT cache + handle collections.
1103 ///
1104 /// The storage it replaces is kept until the Vm drops: functions
1105 /// compiled through it may still be called.
1106 pub fn install_jit_storage<S>(&mut self, storage: S)
1107 where
1108 S: crate::jit::JitStorage + 'static,
1109 {
1110 let old = std::mem::replace(&mut self.jit.storage, Box::new(storage));
1111 self.retired_jit_storage.push(old);
1112 }
1113
1114 /// Install the no-op JIT backend. `try_compile`
1115 /// reports "skipped" so every closure stays on the interpreter
1116 /// path, and the trace recorder's compile attempt always returns
1117 /// `None`. Intended for tests that want to verify the trait
1118 /// boundary works in a JIT-free configuration.
1119 ///
1120 /// Calling this on a Vm whose closures already populated
1121 /// `Proto.jit: JitProtoState::Compiled` does NOT evict those
1122 /// cached entries — the dispatcher will still call into them. For
1123 /// a truly JIT-free run, call this immediately after construction.
1124 pub fn install_null_jit(&mut self) {
1125 self.jit.chunk_compiler = Box::new(crate::jit::NullJitBackend);
1126 self.jit.trace_compiler = Box::new(crate::jit::NullJitBackend);
1127 }
1128
1129 /// Open the entire 5.5 standard library on a `new_minimal`-built Vm.
1130 /// `Vm::new` calls this; sandboxed embedders open libraries one at a
1131 /// time instead (`open_base`, `open_math`, `open_table`, …).
1132 pub fn open_all_libs(&mut self) {
1133 self.open_base();
1134 self.open_math();
1135 self.open_table();
1136 self.open_string();
1137 self.open_utf8();
1138 self.open_os_io();
1139 self.open_debug();
1140 self.open_coroutine();
1141 // PUC 5.2 introduced `bit32`; 5.3 retired it in the manual BUT
1142 // the stock 5.3 build ships -DLUA_COMPAT_5_2, which keeps the
1143 // library loaded. The diff ground truth is the default build
1144 // (stock lua5.3), so expose it under 5.2 AND
1145 // 5.3; 5.4 dropped the compat default for real.
1146 if matches!(self.version, LuaVersion::Lua52 | LuaVersion::Lua53) {
1147 self.open_bit32();
1148 }
1149 // last, so `package.loaded` lists every library opened before it
1150 self.open_package();
1151 }
1152
1153 /// Install the base library (`print`, `type`, `pairs`, `tostring`,
1154 /// `pcall`, `error`, `assert`, `select`, `setmetatable`, `getmetatable`,
1155 /// `rawequal`, `rawget`, `rawset`, `rawlen`, `next`, `tonumber`,
1156 /// `collectgarbage`, `warn` on 5.4+, `_VERSION`, `_G`, plus 5.1's
1157 /// retired globals `unpack`, `loadstring`, `setfenv`, `getfenv`,
1158 /// `newproxy`, `gcinfo` when version == 5.1). Safe to call at most
1159 /// once per Vm.
1160 pub fn open_base(&mut self) {
1161 crate::vm::builtins::open_base(self);
1162 }
1163 /// Install the `math` standard library.
1164 pub fn open_math(&mut self) {
1165 crate::vm::lib_math::open_math(self);
1166 }
1167 /// Install the `table` standard library.
1168 pub fn open_table(&mut self) {
1169 crate::vm::lib_table::open_table(self);
1170 }
1171 /// Install the `string` standard library (and the shared string metatable).
1172 pub fn open_string(&mut self) {
1173 crate::vm::lib_string::open_string(self);
1174 }
1175 /// Install the `utf8` standard library (5.3+).
1176 pub fn open_utf8(&mut self) {
1177 crate::vm::lib_utf8::open_utf8(self);
1178 }
1179 /// `os` and `io` are merged because file userdata shares state with both
1180 /// (`io.tmpname` and `os.tmpname` are the same function, `io.popen`
1181 /// wraps `os.execute`'s shell).
1182 pub fn open_os_io(&mut self) {
1183 crate::vm::lib_os_io::open_os_io(self);
1184 }
1185 /// Install the `debug` standard library (introspection / hooks). Off by
1186 /// default for sandbox embedders.
1187 pub fn open_debug(&mut self) {
1188 crate::vm::lib_debug::open_debug(self);
1189 }
1190 /// Install the `coroutine` standard library.
1191 pub fn open_coroutine(&mut self) {
1192 crate::vm::lib_coroutine::open_coroutine(self);
1193 }
1194 /// `package` plus the 5.1-only `module` and `package.seeall` aliases.
1195 pub fn open_package(&mut self) {
1196 crate::vm::lib_package::open_package(self);
1197 }
1198 /// 5.2-only `bit32` library (5.3+ retired in favour of native bitwise
1199 /// ops on 64-bit integers).
1200 pub fn open_bit32(&mut self) {
1201 crate::vm::lib_bit32::open_bit32(self);
1202 }
1203
1204 /// xoshiro256** next.
1205 pub(crate) fn rng_next(&mut self) -> u64 {
1206 let s = &mut self.rng;
1207 let result = s[1].wrapping_mul(5).rotate_left(7).wrapping_mul(9);
1208 let t = s[1] << 17;
1209 s[2] ^= s[0];
1210 s[3] ^= s[1];
1211 s[1] ^= s[2];
1212 s[0] ^= s[3];
1213 s[2] ^= t;
1214 s[3] = s[3].rotate_left(45);
1215 result
1216 }
1217
1218 /// Seed the RNG via splitmix64 expansion (PUC randseed shape).
1219 pub(crate) fn rng_seed(&mut self, a: u64, b: u64) {
1220 // PUC setseed: state = [n1, 0xff, n2, 0] (0xff avoids an all-zero
1221 // state), then 16 discards to spread the seed. Matches PUC's exact
1222 // sequence so the low-level conformance test passes.
1223 self.rng = [a, 0xff, b, 0];
1224 for _ in 0..16 {
1225 self.rng_next();
1226 }
1227 }
1228
1229 /// Wall-clock since VM creation (os.clock approximation).
1230 pub(crate) fn uptime(&self) -> std::time::Duration {
1231 self.started.elapsed()
1232 }
1233
1234 /// Entropy for math.randomseed() with no arguments.
1235 pub(crate) fn rng_auto_seed(&mut self) -> (i64, i64) {
1236 let t = std::time::SystemTime::now()
1237 .duration_since(std::time::UNIX_EPOCH)
1238 .map(|d| d.as_nanos() as u64)
1239 .unwrap_or(0);
1240 let addr = &self.rng as *const _ as u64;
1241 (t as i64, addr as i64)
1242 }
1243
1244 /// Allocate a native function object (no upvalues): builtin registration.
1245 pub fn native(&mut self, f: crate::runtime::value::NativeFn) -> Value {
1246 Value::Native(self.heap.new_native(f, Box::new([])))
1247 }
1248
1249 /// Allocate a native function object with captured upvalues.
1250 pub fn native_with(
1251 &mut self,
1252 f: crate::runtime::value::NativeFn,
1253 upvals: Box<[Value]>,
1254 ) -> Value {
1255 Value::Native(self.heap.new_native(f, upvals))
1256 }
1257
1258 /// Install the shared string metatable (string library).
1259 pub fn set_string_metatable(&mut self, mt: Option<Gc<Table>>) {
1260 self.type_mt[3] = mt;
1261 }
1262
1263 /// The current globals table (`_G` / `_ENV` source for new chunks).
1264 pub fn globals(&self) -> Gc<Table> {
1265 self.globals
1266 }
1267
1268 /// Remaining VM stack slots (PUC `L->stack_last - L->top` analogue).
1269 /// Library code that pushes a known number of fresh slots — e.g.
1270 /// `table.unpack` returning N values — consults this to refuse when
1271 /// the push would blow past `LUAI_MAXSTACK`. 5.3 coroutine.lua :530's
1272 /// `for j in {lim-10, lim-5, …}` series pins this contract: the
1273 /// coroutine's already-built table eats a few slots, so an unpack of
1274 /// ~lim values can't fit.
1275 pub(crate) fn stack_room(&self) -> i64 {
1276 PUC_MAXSTACK - (self.stack.len() as i64)
1277 }
1278
1279 /// Repoint the thread's "global table" used by *future* `Vm::load` calls
1280 /// for the chunk's `_ENV` upvalue (PUC 5.1 `setfenv(0, env)` rewrites
1281 /// `L->l_gt`). Already-loaded chunks keep their own snapshot via the
1282 /// per-closure cell-0 clone in `Op::Closure`, so they are unaffected.
1283 pub(crate) fn set_globals(&mut self, env: Gc<Table>) {
1284 self.globals = env;
1285 }
1286
1287 /// The Lua dialect this VM was constructed for (5.1 / 5.2 / 5.3 / 5.4 /
1288 /// 5.5). Determines numeric semantics, available standard libraries, and
1289 /// metamethod behavior.
1290 pub fn version(&self) -> LuaVersion {
1291 self.version
1292 }
1293
1294 /// Set a global by name. `v` may be any `IntoValue`: a primitive
1295 /// (`i64`, `f64`, `bool`, `&str`, `String`, `Vec<u8>`), a `Value`
1296 /// directly, an `Option<T>`, or a `Gc<Table>` / `Gc<LuaClosure>` /
1297 /// `Gc<NativeClosure>` handle.
1298 ///
1299 /// Returns `Err(LuaError)` only if the globals table overflows
1300 /// (extremely unlikely in practice — `MAX_ASIZE = 1 << 27`).
1301 /// String interning + key construction cannot fail.
1302 ///
1303 /// ```
1304 /// # use luna_core::vm::Vm;
1305 /// # use luna_core::version::LuaVersion;
1306 /// let mut vm = Vm::sandbox(LuaVersion::Lua55).open_base().build();
1307 /// vm.set_global("answer", 42).unwrap();
1308 /// vm.set_global("ratio", 0.5_f64).unwrap();
1309 /// vm.set_global("hello", "world").unwrap();
1310 /// let r = vm.eval("return answer, ratio, hello").unwrap();
1311 /// assert_eq!(r.len(), 3);
1312 /// ```
1313 pub fn set_global<V: crate::vm::IntoValue>(
1314 &mut self,
1315 name: &str,
1316 v: V,
1317 ) -> Result<(), LuaError> {
1318 let v = v.into_value(self);
1319 let k = Value::Str(self.heap.intern(name.as_bytes()));
1320 // 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).
1321 unsafe { self.globals.as_mut() }.set(&mut self.heap, k, v)?;
1322 self.heap
1323 .barrier_back(self.globals.as_ptr() as *mut crate::runtime::heap::GcHeader);
1324 Ok(())
1325 }
1326
1327 /// Backward write barrier shorthand for native lib code: demote `t` from
1328 /// BLACK back to gray so the next propagate step re-traces its fields.
1329 /// No-op outside Propagate (parent is never BLACK at mutation time).
1330 pub(crate) fn barrier_back_table(&mut self, t: Gc<Table>) {
1331 self.heap
1332 .barrier_back(t.as_ptr() as *mut crate::runtime::heap::GcHeader);
1333 }
1334
1335 /// Forward write barrier shorthand: a closed upvalue is a single-slot
1336 /// container — `barrier_forward` is cheaper than `barrier_back` here.
1337 /// No-op outside Propagate.
1338 pub(crate) fn barrier_forward_upvalue(&mut self, uv: Gc<Upvalue>, child: Value) {
1339 self.heap
1340 .barrier_forward(uv.as_ptr() as *mut crate::runtime::heap::GcHeader, child);
1341 }
1342
1343 /// Register a MacroLua macro under `name`. Inert
1344 /// under non-MacroLua dialects (the macro is stored but the load
1345 /// path only consults the registry when
1346 /// `self.version == LuaVersion::MacroLua`).
1347 ///
1348 /// `name` is stored without the leading `@` — source code writes
1349 /// `@double(x)` to invoke a macro registered as `"double"`.
1350 pub fn define_macro(&mut self, name: &str, m: Box<dyn crate::frontend::macro_expander::Macro>) {
1351 self.macro_registry.register(name, m);
1352 }
1353
1354 /// Drop all MacroLua macros (built-in + custom).
1355 /// Mostly useful for tests.
1356 pub fn clear_macros(&mut self) {
1357 self.macro_registry.clear();
1358 }
1359
1360 /// PUC `luaL_loadfilex`: compile the file `name` (standard input when
1361 /// `None`, named `stdin`) into a function. A first line starting with
1362 /// `#` is skipped; `mode` (`"t"`, `"b"`, `"bt"`, `None` for both)
1363 /// limits the chunk to text and/or binary. The error is the message
1364 /// PUC's function leaves: `cannot open <name>: <reason>` when the file
1365 /// cannot be read, or the positioned syntax error.
1366 pub fn load_file(
1367 &mut self,
1368 name: Option<&[u8]>,
1369 mode: Option<&[u8]>,
1370 ) -> Result<Value, LuaError> {
1371 crate::vm::lib_os_io::load_path(self, name, mode).map_err(LuaError)
1372 }
1373
1374 /// PUC `luaL_loadbufferx`: compile `src` under `chunkname`, the chunk
1375 /// kind limited by `mode` as in [`Vm::load_file`]. A syntax error comes
1376 /// back as its positioned message (`<chunk id>:<line>: <message>`), the
1377 /// string `load` returns.
1378 pub fn load_buffer(
1379 &mut self,
1380 src: &[u8],
1381 chunkname: &[u8],
1382 mode: Option<&[u8]>,
1383 ) -> Result<Value, LuaError> {
1384 crate::vm::lib_os_io::load_chunk(self, src, chunkname, mode).map_err(LuaError)
1385 }
1386
1387 /// Parse + compile a chunk and close it over the globals table.
1388 pub fn load(&mut self, src: &[u8], chunkname: &[u8]) -> Result<Gc<LuaClosure>, SyntaxError> {
1389 // Reject oversize input *before* handing the parser/lexer a
1390 // potentially multi-GB slice. The PUC-shaped `not enough memory`
1391 // message keeps `heavy.lua::loadrep` compatibility: that test
1392 // accepts either `string length overflow` or `not enough memory`
1393 // as the failure mode for a feeder loop that outruns the host
1394 // allocator. See `set_loader_input_budget`.
1395 if src.len() > self.loader_input_budget {
1396 return Err(SyntaxError {
1397 line: 0,
1398 msg: b"not enough memory".to_vec(),
1399 });
1400 }
1401 // a precompiled (binary) chunk is undumped; source is parsed + compiled
1402 let is_bytecode = crate::vm::dump::is_binary_chunk(src);
1403 if is_bytecode && !self.bytecode_loading {
1404 return Err(SyntaxError {
1405 line: 0,
1406 msg: b"attempt to load a binary chunk (bytecode loading disabled)".to_vec(),
1407 });
1408 }
1409 let proto = if is_bytecode {
1410 let allow_puc = self.puc_bytecode_loading;
1411 crate::vm::dump::undump_named(src, &mut self.heap, self.version, allow_puc, chunkname)
1412 .map_err(SyntaxError::unpositioned)?
1413 } else if self.version.is_macro_lua() {
1414 // MacroLua dialect: drain the lexer into a
1415 // token vec, run the macro expander pre-pass against the
1416 // per-Vm registry, then hand the rewritten stream to
1417 // `parse_tokens`. The AST + compiler are dialect-agnostic
1418 // because by this point all `@`/quote tokens are gone.
1419 let mut lexer = crate::frontend::lexer::Lexer::new(src, self.version);
1420 let mut raw: Vec<crate::frontend::token::TokenInfo> = Vec::new();
1421 loop {
1422 let t = lexer.next_token()?;
1423 let eof = matches!(t.tok, crate::frontend::token::Token::Eof);
1424 raw.push(t);
1425 if eof {
1426 break;
1427 }
1428 }
1429 // Drop the trailing Eof — expander operates on the body and
1430 // `parse_tokens` reinserts Eof when it runs out of tokens.
1431 raw.pop();
1432 let expanded = self.macro_registry.expand(raw)?;
1433 let depth = self.c_depth + self.pcall_depth;
1434 let parsed =
1435 crate::frontend::parser::parse_tokens_at_depth(expanded, src, self.version, depth)?;
1436 crate::compiler::compile_parsed(
1437 &parsed.chunk,
1438 &parsed.end_lines,
1439 self.version,
1440 chunkname,
1441 &mut self.heap,
1442 )?
1443 } else {
1444 // PUC's `nCcalls` counts protected calls as well
1445 let depth = self.c_depth + self.pcall_depth;
1446 let parsed = crate::frontend::parser::parse_at_depth(src, self.version, depth)?;
1447 crate::compiler::compile_parsed(
1448 &parsed.chunk,
1449 &parsed.end_lines,
1450 self.version,
1451 chunkname,
1452 &mut self.heap,
1453 )?
1454 };
1455 // PUC `lua_load` (lapi.c) only seeds the loaded closure's first
1456 // upvalue with the globals table when the closure has *exactly* one
1457 // upvalue — that's the main-chunk `_ENV` case. A dumped non-main
1458 // function with two-or-more upvalues keeps every cell at nil; the
1459 // host must use `debug.setupvalue` to wire them up. 5.2 calls.lua
1460 // :293's `assert(x() == nil)` pins this contract.
1461 let n = proto.upvals.len();
1462 let mut ups: Vec<Gc<Upvalue>> = Vec::with_capacity(n.max(1));
1463 if n == 0 {
1464 // synthetic main chunk has no declared upvalues, but the engine
1465 // still expects at least one cell so the host can probe via
1466 // `debug.upvalueid` etc. Match the historical luna shape.
1467 ups.push(
1468 self.heap
1469 .new_upvalue(UpvalState::Closed(Value::Table(self.globals))),
1470 );
1471 } else if n == 1 {
1472 ups.push(
1473 self.heap
1474 .new_upvalue(UpvalState::Closed(Value::Table(self.globals))),
1475 );
1476 } else {
1477 for _ in 0..n {
1478 ups.push(self.heap.new_upvalue(UpvalState::Closed(Value::Nil)));
1479 }
1480 }
1481 Ok(self.heap.new_closure(proto, ups.into_boxed_slice()))
1482 }
1483
1484 /// Compile and run `src` as an anonymous chunk; return its results.
1485 /// Source name in the traceback is `"=eval"`. Syntax errors are
1486 /// surfaced as `LuaError` carrying the formatted PUC-style message
1487 /// (interned through the heap so the error value composes with
1488 /// `pcall` / `error_text` like any runtime error).
1489 pub fn eval(&mut self, src: &str) -> Result<Vec<Value>, LuaError> {
1490 self.eval_chunk(src, "=eval")
1491 }
1492
1493 /// Render an error value for messages/tests. Non-string errors —
1494 /// `error({code=…})`, `error(42)`, etc. — collapse to a type tag
1495 /// (`"(error object is a table value)"`); embedders that need
1496 /// structured payloads should inspect `e.0` directly. Errors whose
1497 /// text starts with `"native panic:"` indicate a Rust panic
1498 /// crossed `catch_unwind` — the Vm may be inconsistent and should
1499 /// be dropped (do not reuse).
1500 pub fn error_text(&self, e: &LuaError) -> String {
1501 match e.0 {
1502 Value::Str(s) => String::from_utf8_lossy(s.as_bytes()).into_owned(),
1503 v => format!("(error object is a {} value)", v.type_name()),
1504 }
1505 }
1506
1507 /// Render an error value the way PUC's standalone `msghandler`
1508 /// does (lua.c): strings pass through, numbers stringify, and any
1509 /// other object is given a chance at its `__tostring` metamethod
1510 /// (the result must be a string) before collapsing to the
1511 /// `"(error object is a … value)"` tag. Needs `&mut self` because
1512 /// `__tostring` runs arbitrary Lua — `error_text` remains the
1513 /// non-executing variant.
1514 pub fn error_display(&mut self, e: &LuaError) -> String {
1515 match e.0 {
1516 Value::Str(s) => String::from_utf8_lossy(s.as_bytes()).into_owned(),
1517 v @ (Value::Int(_) | Value::Float(_)) => {
1518 String::from_utf8_lossy(&self.tostring_basic(v)).into_owned()
1519 }
1520 v => {
1521 let mm = self.get_mm(v, Mm::ToString);
1522 if !mm.is_nil()
1523 && let Ok(r) = self.call_value(mm, &[v])
1524 && let Some(Value::Str(s)) = r.first()
1525 {
1526 return String::from_utf8_lossy(s.as_bytes()).into_owned();
1527 }
1528 format!("(error object is a {} value)", v.type_name())
1529 }
1530 }
1531 }
1532
1533 /// Call `f` with `args` in protected mode with the message handler
1534 /// `msgh`: PUC `lua_pcall(L, nargs, LUA_MULTRET, msgh)` made from a C
1535 /// function of the host's, as lua.c's `docall` does from `pmain`.
1536 ///
1537 /// `msgh` runs where the error was raised, before the stack unwinds, so
1538 /// it can take a traceback of the failing call ([`Vm::traceback`]); an
1539 /// error inside it calls it again with the new error, as in PUC. The
1540 /// returned error carries what the handler returned.
1541 ///
1542 /// The call counts as one C level on the stack, the host function
1543 /// making it: `debug.getinfo` finds it below `f`, and a traceback taken
1544 /// inside ends with `[C]: in ?` (`[C]: ?` in 5.1).
1545 pub fn call_value_with_handler(
1546 &mut self,
1547 f: Value,
1548 args: &[Value],
1549 msgh: Value,
1550 ) -> Result<Vec<Value>, LuaError> {
1551 let level = self.native(crate::vm::builtins::nat_host_xpcall);
1552 let mut call_args = Vec::with_capacity(args.len() + 2);
1553 call_args.push(f);
1554 call_args.push(msgh);
1555 call_args.extend_from_slice(args);
1556 let mut results = self.call_value(level, &call_args)?;
1557 // the protected call's `true, results...` or `false, handled error`
1558 if results.first().is_some_and(|ok| ok.truthy()) {
1559 results.remove(0);
1560 Ok(results)
1561 } else {
1562 Err(LuaError(results.get(1).copied().unwrap_or(Value::Nil)))
1563 }
1564 }
1565
1566 /// PUC `luaL_getmetafield`: the field `event` of `v`'s metatable, read
1567 /// raw; nil when `v` has no metatable or the field is absent.
1568 pub fn metafield(&mut self, v: Value, event: &str) -> Value {
1569 match self.metatable_of(v) {
1570 Some(mt) => {
1571 let key = Value::Str(self.heap.intern(event.as_bytes()));
1572 mt.get(key)
1573 }
1574 None => Value::Nil,
1575 }
1576 }
1577
1578 /// Call any callable value from the host (or from natives like pcall).
1579 pub fn call_value(&mut self, f: Value, args: &[Value]) -> Result<Vec<Value>, LuaError> {
1580 // host-level entry (no enclosing exec): drop any error state from a
1581 // prior call that propagated uncaught (`error_traceback` would
1582 // otherwise leak into the next debug.traceback call).
1583 if self.public_call_depth == 0 {
1584 self.error_traceback = None;
1585 }
1586 self.public_call_depth += 1;
1587 // JIT fast path. A host call with no args targeting a Lua
1588 // chunk whose body fits the int-arith whitelist short-circuits
1589 // the whole interpreter dispatch and runs straight through the
1590 // mmap'd native code. The lookup is one Cell::get + one match —
1591 // the slow path (compile attempt on first reach) is paid once per
1592 // Proto.
1593 if args.is_empty()
1594 && let Value::Closure(cl) = f
1595 && let Some(vs) = self.try_jit_call(cl)
1596 {
1597 self.public_call_depth -= 1;
1598 return Ok(vs);
1599 }
1600 let r = self.call_value_impl(f, args, true);
1601 self.public_call_depth -= 1;
1602 r
1603 }
1604
1605 /// Peek/populate the Proto's JIT cache slot, returning
1606 /// `Some(values)` when the cached native fn is callable for a
1607 /// zero-arg call. (Non-zero-arg dispatch is handled by
1608 /// `try_jit_call_op` from inside `begin_call`.)
1609 fn try_jit_call(&mut self, cl: Gc<LuaClosure>) -> Option<Vec<Value>> {
1610 use crate::runtime::function::JitProtoState;
1611 if !self.jit.enabled {
1612 return None;
1613 }
1614 let proto = cl.proto;
1615 if let JitProtoState::Untried = proto.jit.get() {
1616 self.populate_jit_cache(proto);
1617 }
1618 match proto.jit.get() {
1619 JitProtoState::Compiled {
1620 entry,
1621 num_args: 0,
1622 returns_one,
1623 arg_float_mask: _,
1624 arg_table_mask: _,
1625 ret_is_float,
1626 ret_is_table,
1627 } => {
1628 // 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.
1629 let f: crate::jit::IntChunkFn = unsafe { std::mem::transmute(entry) };
1630 // Install the active Vm + closure
1631 // for any Rust helper the JIT'd code may call (e.g.
1632 // `luna_jit_new_table`, `luna_jit_upval_get`) via
1633 // cranelift `Linkage::Import`. RAII clear on return.
1634 // Chunks with no upvalue reads don't touch the closure
1635 // slot, paying nothing.
1636 // Route through chunk_compiler so
1637 // the NullJitBackend path stays inert. Raw-ptr arg
1638 // avoids the &mut self borrow conflict against the
1639 // shared self.jit.chunk_compiler read.
1640 let vm_ptr: *mut Vm = self;
1641 let _jit_vm_guard = self.jit.chunk_compiler.enter(vm_ptr, Some(cl));
1642 // 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).
1643 let r = unsafe { f() };
1644 drop(_jit_vm_guard);
1645 // A JIT helper may have detected a metatable
1646 // on a table operand and parked a deopt request here.
1647 // Discard the sentinel value and return None so the caller
1648 // re-runs the call through the interpreter, which honours
1649 // __index/__newindex.
1650 if self.jit.pending_err.take().is_some() {
1651 return None;
1652 }
1653 Some(if returns_one {
1654 let v = if ret_is_float {
1655 Value::Float(f64::from_bits(r as u64))
1656 } else if ret_is_table {
1657 Value::Table(crate::runtime::Gc::from_ptr(
1658 r as *mut crate::runtime::Table,
1659 ))
1660 } else {
1661 Value::Int(r)
1662 };
1663 vec![v]
1664 } else {
1665 Vec::new()
1666 })
1667 }
1668 // Non-zero-arg Compiled state: call_value's empty-args
1669 // fast path can't drive it. Op::Call handles those.
1670 JitProtoState::Compiled { .. } | JitProtoState::Failed | JitProtoState::Untried => None,
1671 }
1672 }
1673
1674 /// Populate the cache slot. Flips `Untried` to either
1675 /// `Compiled { … }` or `Failed`; idempotent on already-populated
1676 /// states (call sites guard with a get before invoking).
1677 ///
1678 /// Consults a thread-local cross-`Vm` cache keyed by a hash of
1679 /// `proto.code`. Compiled artefacts live in the thread-local
1680 /// `JITModule` so their mmap pages outlive the `Vm`; subsequent
1681 /// `Vm`s loading the same source skip the cranelift compile step
1682 /// entirely.
1683 fn populate_jit_cache(&mut self, proto: Gc<crate::runtime::function::Proto>) {
1684 use crate::runtime::function::JitProtoState;
1685 let version = self.version();
1686 let pre53 = version <= crate::version::LuaVersion::Lua53;
1687 // 5.1 and 5.2 have no Int subtype (all numbers
1688 // are Float). The JIT's `GetUpval` ValueRead path uses this
1689 // to default-pin upvalue reads to Float without a tag check.
1690 let float_only = version <= crate::version::LuaVersion::Lua52;
1691 // Split-borrow JitState so the
1692 // trait method can take `&mut dyn JitStorage` without
1693 // double-borrowing self.jit.
1694 let jit = &mut self.jit;
1695 jit.storage.claim(self.jit_owner_id);
1696 let storage: &mut dyn crate::jit::JitStorage = jit.storage.as_mut();
1697 match jit
1698 .chunk_compiler
1699 .try_compile(storage, proto, pre53, float_only)
1700 {
1701 crate::jit::CompileResult::Compiled {
1702 entry,
1703 num_args,
1704 returns_one,
1705 arg_float_mask,
1706 arg_table_mask,
1707 ret_is_float,
1708 ret_is_table,
1709 } => {
1710 proto.jit.set(JitProtoState::Compiled {
1711 entry,
1712 num_args,
1713 returns_one,
1714 arg_float_mask,
1715 arg_table_mask,
1716 ret_is_float,
1717 ret_is_table,
1718 });
1719 }
1720 crate::jit::CompileResult::Skipped => {
1721 proto.jit.set(JitProtoState::Failed);
1722 }
1723 }
1724 }
1725
1726 /// `Op::Call` JIT fast path. Run inside `begin_call`
1727 /// before `push_frame`. Returns `true` when the call was handled
1728 /// in-place (no new Lua frame). Constraints: every arg slot must
1729 /// be `Value::Int`, the cached arity must match the call site's
1730 /// `nargs`, the host wanted-count `wanted` is honoured by
1731 /// `finish_results`. Also bails when a debug hook is armed —
1732 /// JIT'd code does not fire line / call / return hooks, so any
1733 /// active hook makes the interpreter the source of truth.
1734 fn try_jit_call_op(
1735 &mut self,
1736 cl: Gc<LuaClosure>,
1737 func_slot: u32,
1738 nargs: u32,
1739 wanted: i32,
1740 ) -> bool {
1741 use crate::runtime::function::JitProtoState;
1742 if !self.jit.enabled {
1743 return false;
1744 }
1745 // Any active debug hook means the interpreter has to run the
1746 // call so the hook gets the expected events.
1747 if self.hook.func.is_some() || self.hook.rust_func.is_some() {
1748 return false;
1749 }
1750 let proto = cl.proto;
1751 if let JitProtoState::Untried = proto.jit.get() {
1752 self.populate_jit_cache(proto);
1753 }
1754 let JitProtoState::Compiled {
1755 entry,
1756 num_args,
1757 returns_one,
1758 arg_float_mask,
1759 arg_table_mask,
1760 ret_is_float,
1761 ret_is_table,
1762 } = proto.jit.get()
1763 else {
1764 return false;
1765 };
1766 if num_args as u32 != nargs {
1767 return false;
1768 }
1769 // Pack args into i64 bit-patterns per the per-slot expected
1770 // kind. A Float-typed slot accepts Value::Float verbatim (and on
1771 // 5.1/5.2 promotes Value::Int(x) via i64 → f64); a Table-typed slot
1772 // accepts only Value::Table and passes the raw Gc ptr; an
1773 // Int-typed slot accepts only Value::Int. Any other shape
1774 // bails to the interpreter so the call's actual dynamics
1775 // (metamethod dispatch / type-coerce) take over.
1776 let mut args: [i64; crate::jit::MAX_JIT_ARITY as usize] =
1777 [0; crate::jit::MAX_JIT_ARITY as usize];
1778 // From 5.3 an integer is its own subtype: turned into a float for
1779 // a float-typed parameter, it would come back out (returned,
1780 // stored, printed) as a float. Only 5.1/5.2, where every number
1781 // is a float, may convert it.
1782 let int_as_float = self.version() <= crate::version::LuaVersion::Lua52;
1783 for i in 0..num_args as usize {
1784 let v = self.stack[(func_slot + 1) as usize + i];
1785 let want_float = (arg_float_mask >> i) & 1 == 1;
1786 let want_table = (arg_table_mask >> i) & 1 == 1;
1787 args[i] = match (want_table, want_float, v) {
1788 (true, _, Value::Table(t)) => t.as_ptr() as i64,
1789 (false, false, Value::Int(x)) => x,
1790 (false, true, Value::Float(f)) => f.to_bits() as i64,
1791 (false, true, Value::Int(x)) if int_as_float => (x as f64).to_bits() as i64,
1792 _ => return false,
1793 };
1794 }
1795 // Vm + closure pin for helpers, routed through chunk_compiler;
1796 // see the matching guard in `try_jit_call`.
1797 let vm_ptr: *mut Vm = self;
1798 let _jit_vm_guard = self.jit.chunk_compiler.enter(vm_ptr, Some(cl));
1799 // 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.
1800 let r = unsafe {
1801 match num_args {
1802 0 => (std::mem::transmute::<*const u8, crate::jit::IntChunkFn>(entry))(),
1803 1 => (std::mem::transmute::<*const u8, crate::jit::IntFn1>(entry))(args[0]),
1804 2 => {
1805 (std::mem::transmute::<*const u8, crate::jit::IntFn2>(entry))(args[0], args[1])
1806 }
1807 3 => (std::mem::transmute::<*const u8, crate::jit::IntFn3>(entry))(
1808 args[0], args[1], args[2],
1809 ),
1810 4 => (std::mem::transmute::<*const u8, crate::jit::IntFn4>(entry))(
1811 args[0], args[1], args[2], args[3],
1812 ),
1813 _ => unreachable!("MAX_JIT_ARITY enforces num_args <= 4"),
1814 }
1815 };
1816 drop(_jit_vm_guard);
1817 // See matching path in `try_jit_call`. A helper
1818 // flagged a metatable on a table operand; bail to the interpreter
1819 // so `push_frame` runs the call from scratch.
1820 if self.jit.pending_err.take().is_some() {
1821 return false;
1822 }
1823 // Write result at func_slot, replacing the closure value, then
1824 // hand to finish_results to pad/truncate per the call site's
1825 // `wanted` count.
1826 if returns_one {
1827 let v = if ret_is_float {
1828 Value::Float(f64::from_bits(r as u64))
1829 } else if ret_is_table {
1830 Value::Table(crate::runtime::Gc::from_ptr(
1831 r as *mut crate::runtime::Table,
1832 ))
1833 } else {
1834 Value::Int(r)
1835 };
1836 self.stack[func_slot as usize] = v;
1837 self.finish_results(func_slot, 1, wanted);
1838 } else {
1839 self.finish_results(func_slot, 0, wanted);
1840 }
1841 true
1842 }
1843
1844 /// `call_value` with control over the `from_c` debug boundary. A `__close`
1845 /// handler runs *within* the closing Lua frame's activation (PUC luaF_close
1846 /// invokes it inside that ci), so it is called with `from_c = false`: its
1847 /// debug parent is the closing function, not a synthetic C level.
1848 fn call_value_impl(
1849 &mut self,
1850 f: Value,
1851 args: &[Value],
1852 from_c: bool,
1853 ) -> Result<Vec<Value>, LuaError> {
1854 if self.c_depth >= MAX_C_DEPTH {
1855 // PUC `luaE_checkcstack`: at the limit the call fails; an xpcall
1856 // handler running on the error gets a tenth more room before its
1857 // own failure is "error in error handling"
1858 if self.msgh_depth == 0 {
1859 return Err(self.runerror("C stack overflow"));
1860 }
1861 if self.c_depth >= MAX_C_DEPTH / 10 * 11 {
1862 return Err(self.plain_err("error in error handling"));
1863 }
1864 }
1865 self.c_depth += 1;
1866 let func_slot = self.stack.len() as u32;
1867 self.stack.push(f);
1868 self.stack.extend_from_slice(args);
1869 self.top = self.stack.len() as u32;
1870 let r = self.call_at(func_slot, args.len() as u32, from_c);
1871 self.c_depth -= 1;
1872 if r.is_err()
1873 && self.yielding.is_none()
1874 && self.terminating.is_none()
1875 && !self.host_yield_pending
1876 && self.pending_async_native_fut.is_none()
1877 {
1878 // A `coroutine.yield` in flight raises a sentinel error to unwind the
1879 // Rust stack, but the suspended coroutine's frames/registers (which
1880 // sit at/above `func_slot`) must survive for the next resume — so we
1881 // only truncate on a real error. A self-close termination is in the
1882 // same boat: the dying thread's state is discarded wholesale.
1883 // A `host_yield_pending` cooperative yield is in
1884 // the same boat as `yielding`: the next `EvalFuture::poll`
1885 // resumes the same call, so the in-flight frames must
1886 // survive.
1887 self.stack.truncate(func_slot as usize);
1888 self.top = func_slot;
1889 }
1890 r
1891 }
1892
1893 /// Invoke `f` with the running thread marked non-yieldable for the duration
1894 /// (PUC `luaD_callnoyield`): a `coroutine.yield` inside `f` hits the C-call
1895 /// boundary and errors instead of suspending. Used by library callbacks
1896 /// (sort comparator, gsub replacement) that run via synchronous Rust
1897 /// recursion and so could not be re-entered after a yield.
1898 pub(crate) fn call_noyield(
1899 &mut self,
1900 f: Value,
1901 args: &[Value],
1902 ) -> Result<Vec<Value>, LuaError> {
1903 self.nny += 1;
1904 let r = self.call_value(f, args);
1905 self.nny -= 1;
1906 r
1907 }
1908
1909 // ---- coroutines ----
1910
1911 pub(crate) fn new_coro(&mut self, body: Value) -> Gc<Coro> {
1912 // The new coroutine inherits the creating thread's current globals
1913 // (PUC `lua_newthread`: the new state copies `g->mainthread`'s
1914 // `l_gt`). `Vm.globals` always reflects the live thread, so reading
1915 // it here picks the creator regardless of which coro is running.
1916 self.heap.new_coro(body, self.globals)
1917 }
1918
1919 /// Is `t` the thread whose context is currently live in the VM?
1920 pub(crate) fn is_current_thread(&self, t: Option<Gc<Coro>>) -> bool {
1921 match (self.current, t) {
1922 (None, None) => true,
1923 (Some(a), Some(b)) => a.ptr_eq(b),
1924 _ => false,
1925 }
1926 }
1927
1928 /// Read an open-upvalue slot from its owning thread's stack (the live VM
1929 /// stack if that thread is current, else its saved context).
1930 #[doc(hidden)]
1931 pub fn read_slot(&self, slot: u32, thread: Option<Gc<Coro>>) -> Value {
1932 let s = slot as usize;
1933 if self.is_current_thread(thread) {
1934 self.stack[s]
1935 } else {
1936 match thread {
1937 Some(co) => co.stack[s],
1938 None => self.main_ctx.as_ref().expect("main context").stack[s],
1939 }
1940 }
1941 }
1942
1943 fn write_slot(&mut self, slot: u32, thread: Option<Gc<Coro>>, v: Value) {
1944 let s = slot as usize;
1945 if self.is_current_thread(thread) {
1946 self.stack[s] = v;
1947 } else {
1948 match thread {
1949 Some(co) => {
1950 // 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).
1951 unsafe { co.as_mut() }.stack[s] = v;
1952 // co.stack is traced by Coro::trace; demote co back to
1953 // gray so propagate re-traces this slot if it was
1954 // already black.
1955 self.heap
1956 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
1957 }
1958 None => self.main_ctx.as_mut().expect("main context").stack[s] = v,
1959 }
1960 }
1961 }
1962
1963 /// Whether `co` is the main thread's identity object.
1964 pub(crate) fn is_main_coro(&self, co: Gc<Coro>) -> bool {
1965 self.main_coro.is_some_and(|m| m.ptr_eq(co))
1966 }
1967
1968 /// The status of `co` from the caller's view. The main thread's identity
1969 /// object has no stored status — it is "running" when nothing else runs,
1970 /// else "normal" (it resumed the active coroutine).
1971 pub(crate) fn effective_coro_status(&self, co: Gc<Coro>) -> CoroStatus {
1972 if self.is_main_coro(co) {
1973 if self.current.is_none() {
1974 CoroStatus::Running
1975 } else {
1976 CoroStatus::Normal
1977 }
1978 } else {
1979 co.status
1980 }
1981 }
1982
1983 /// `coroutine.close` (PUC `lua_closethread`): run the suspended coroutine's
1984 /// pending to-be-closed `__close` handlers, then mark it dead and drop its
1985 /// context. Handlers see the coroutine's death error (if it died by error)
1986 /// or nil; an error they raise propagates out. `Ok(Some(e))` means it died
1987 /// with error `e` and no handler overrode it; `Err` means a handler raised.
1988 pub(crate) fn close_coro(&mut self, co: Gc<Coro>) -> Result<Option<Value>, LuaError> {
1989 // re-entrant close: a __close handler closed its own coroutine while the
1990 // outer close is mid-flight (its context is live). Report success and let
1991 // the outer close finish — re-entering the swap would corrupt the stack.
1992 if self.current.is_some_and(|c| c.ptr_eq(co)) {
1993 return Ok(None);
1994 }
1995 // A chain of coroutines whose `__close` handlers each close the previous
1996 // one recurses on the C stack (PUC `luaD_callnoyield` in `lua_closethread`).
1997 // The calling handler's `call_value` has already pushed `c_depth` to the
1998 // cap, so here it reads as full first — report PUC's "C stack overflow"
1999 // before the next handler call would surface the plainer "stack overflow".
2000 if self.c_depth >= MAX_C_DEPTH {
2001 return Err(self.rt_err("C stack overflow"));
2002 }
2003 // 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).
2004 let death_err = unsafe { co.as_mut() }.error_value.take();
2005 // swap the caller's live context out (into a GC-rooted home) and the
2006 // coroutine's in, mirroring resume_coro, so the __close handlers run on
2007 // the coroutine's stack while everything stays rooted.
2008 let resumer = self.current;
2009 let rctx = self.take_ctx();
2010 match resumer {
2011 Some(r) => {
2012 // 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).
2013 let m = unsafe { r.as_mut() };
2014 m.stack = rctx.stack;
2015 m.frames = rctx.frames;
2016 m.frame_ccmt = rctx.frame_ccmt;
2017 m.open_upvals = rctx.open_upvals;
2018 m.tbc = rctx.tbc;
2019 m.top = rctx.top;
2020 m.pcall_depth = rctx.pcall_depth;
2021 }
2022 None => self.main_ctx = Some(rctx),
2023 }
2024 self.load_coro_ctx(co);
2025 self.current = Some(co);
2026 // PUC `luaE_resetthread` closes with no message handler, whatever
2027 // xpcall the coroutine was suspended in
2028 let natives_base = std::mem::replace(&mut self.natives_base, self.running_natives.len());
2029 let msgh_floor = std::mem::replace(&mut self.msgh_floor, self.frames.len());
2030 let result = self.close_slots(0, death_err);
2031 self.natives_base = natives_base;
2032 self.msgh_floor = msgh_floor;
2033 // discard the (now-closed) coroutine context and restore the caller
2034 let _ = self.take_ctx();
2035 match resumer {
2036 Some(r) => {
2037 self.load_coro_ctx(r);
2038 self.current = Some(r);
2039 }
2040 None => {
2041 let m = self.main_ctx.take().expect("main context saved");
2042 self.put_ctx(m);
2043 self.current = None;
2044 }
2045 }
2046 {
2047 // 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).
2048 let m = unsafe { co.as_mut() };
2049 m.status = CoroStatus::Dead;
2050 m.stack = Vec::new();
2051 m.frames = Vec::new();
2052 m.frame_ccmt = Vec::new();
2053 m.open_upvals = Vec::new();
2054 m.tbc = Vec::new();
2055 m.top = 0;
2056 m.pcall_depth = 0;
2057 m.resume_at = None;
2058 m.error_value = None;
2059 m.error_traceback = None;
2060 m.error_levels = None;
2061 }
2062 result.map(|()| death_err)
2063 }
2064
2065 /// `coroutine.running`: the running thread plus whether it is the main one.
2066 pub(crate) fn running_thread(&self) -> (Value, bool) {
2067 match self.current {
2068 Some(co) => (Value::Coro(co), false),
2069 None => (Value::Coro(self.main_coro.expect("main coro")), true),
2070 }
2071 }
2072
2073 /// `coroutine.isyieldable([co])`: whether `co` (default: the running
2074 /// thread) can yield. The main thread never can; any other coroutine can
2075 /// unless it is dead.
2076 pub(crate) fn is_yieldable(&self, co: Option<Gc<Coro>>) -> bool {
2077 match co {
2078 Some(c) => !self.main_coro.is_some_and(|m| m.ptr_eq(c)) && c.status != CoroStatus::Dead,
2079 // the running thread can yield only outside any non-yieldable C call
2080 None => self.current.is_some() && self.nny == 0,
2081 }
2082 }
2083
2084 /// Why `coroutine.yield` may not suspend the running thread right now, as a
2085 /// PUC error message — `None` if it may. Distinguishes "not in a coroutine"
2086 /// from "inside an unyieldable C call" (sort/gsub callback).
2087 pub(crate) fn yield_barrier(&self) -> Option<&'static str> {
2088 // 5.1's pcall/xpcall are plain C calls (no continuations), so a yield
2089 // below one crosses the boundary like any other; 5.1 also has a single
2090 // wording for every case, the main thread included.
2091 // 5.1 also calls every metamethod and generic-for iterator through
2092 // `luaD_call`, which counts as a C level, so a yield from inside one
2093 // is refused as well.
2094 if self.version <= LuaVersion::Lua51 {
2095 let inside_call = self.frames.iter().enumerate().any(|(i, f)| match f {
2096 CallFrame::Cont(nc) => matches!(nc.kind, ContKind::Meta(_)),
2097 CallFrame::Lua(fr) => {
2098 fr.tm.is_some()
2099 || (i > 0
2100 && self.frames[i - 1].lua().is_some_and(|c| {
2101 let pc = (c.pc as usize).wrapping_sub(1);
2102 c.closure
2103 .proto
2104 .code
2105 .get(pc)
2106 .is_some_and(|ins| ins.op() == Op::TForCall)
2107 }))
2108 }
2109 });
2110 if self.current.is_none() || self.nny > 0 || self.pcall_depth > 0 || inside_call {
2111 return Some("attempt to yield across metamethod/C-call boundary");
2112 }
2113 return None;
2114 }
2115 if self.current.is_none() {
2116 Some("attempt to yield from outside a coroutine")
2117 } else if self.nny > 0 {
2118 Some("attempt to yield across a C-call boundary")
2119 } else {
2120 None
2121 }
2122 }
2123
2124 /// The coroutine whose context is currently live (`None` on the main thread).
2125 pub(crate) fn current_coro(&self) -> Option<Gc<Coro>> {
2126 self.current
2127 }
2128
2129 /// `coroutine.close()` on the *running* thread (PUC 5.5 close-self): run all
2130 /// its pending `__close` handlers, then signal termination. The handlers run
2131 /// here, in place, with the thread still non-yieldable (a yield in one hits
2132 /// the C-call boundary). The returned sentinel unwinds the Rust stack the
2133 /// way a yield does — `exec_with` propagates it past any protecting pcall
2134 /// rather than letting `unwind` catch it — and `resume_coro` turns it into a
2135 /// clean death (or, if a handler raised, the coroutine's error).
2136 pub(crate) fn close_running(&mut self) -> LuaError {
2137 let death = match self.close_slots(0, None) {
2138 Ok(()) => None,
2139 Err(e) => Some(e.0),
2140 };
2141 self.terminating = Some(death);
2142 LuaError(Value::Nil)
2143 }
2144
2145 /// `coroutine.status` as seen by the caller.
2146 pub(crate) fn coro_status_str(&self, co: Gc<Coro>) -> &'static str {
2147 match self.effective_coro_status(co) {
2148 CoroStatus::Suspended => "suspended",
2149 CoroStatus::Running => "running",
2150 CoroStatus::Normal => "normal",
2151 CoroStatus::Dead => "dead",
2152 }
2153 }
2154
2155 fn take_ctx(&mut self) -> SavedCtx {
2156 let saved = SavedCtx {
2157 stack: std::mem::take(&mut self.stack),
2158 frames: std::mem::take(&mut self.frames),
2159 frame_ccmt: std::mem::take(&mut self.frame_ccmt),
2160 open_upvals: std::mem::take(&mut self.open_upvals),
2161 tbc: std::mem::take(&mut self.tbc),
2162 top: self.top,
2163 pcall_depth: self.pcall_depth,
2164 hook: self.hook,
2165 globals: self.globals,
2166 };
2167 self.frames_resync(); // frames now empty
2168 saved
2169 }
2170
2171 fn put_ctx(&mut self, c: SavedCtx) {
2172 self.stack = c.stack;
2173 self.frames = c.frames;
2174 self.frame_ccmt = c.frame_ccmt;
2175 self.open_upvals = c.open_upvals;
2176 self.tbc = c.tbc;
2177 self.top = c.top;
2178 self.pcall_depth = c.pcall_depth;
2179 self.hook = c.hook;
2180 self.globals = c.globals;
2181 self.frames_resync(); // sync shadow to new Vec
2182 }
2183
2184 /// Move a coroutine's saved context into the live VM fields.
2185 fn load_coro_ctx(&mut self, co: Gc<Coro>) {
2186 // 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).
2187 let m = unsafe { co.as_mut() };
2188 self.stack = std::mem::take(&mut m.stack);
2189 self.frames = std::mem::take(&mut m.frames);
2190 self.frame_ccmt = std::mem::take(&mut m.frame_ccmt);
2191 self.open_upvals = std::mem::take(&mut m.open_upvals);
2192 self.tbc = std::mem::take(&mut m.tbc);
2193 self.top = m.top;
2194 self.frames_resync(); // sync shadow to coro's frames
2195 self.pcall_depth = m.pcall_depth;
2196 self.hook = m.hook;
2197 self.globals = m.globals;
2198 }
2199
2200 /// Save the live VM context back into a coroutine object.
2201 fn store_coro_ctx(&mut self, co: Gc<Coro>) {
2202 let c = self.take_ctx();
2203 // 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).
2204 let m = unsafe { co.as_mut() };
2205 m.stack = c.stack;
2206 m.frames = c.frames;
2207 m.frame_ccmt = c.frame_ccmt;
2208 m.open_upvals = c.open_upvals;
2209 m.tbc = c.tbc;
2210 m.top = c.top;
2211 m.pcall_depth = c.pcall_depth;
2212 m.hook = c.hook;
2213 m.globals = c.globals;
2214 // bulk-overwrite of every collectable field traced by Coro::trace:
2215 // demote the coro back to gray so propagate re-traces its new state.
2216 self.heap
2217 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
2218 }
2219
2220 /// `coroutine.resume` core: drive `co` with `args` until it yields, returns
2221 /// or errors. Ok(values) carries yielded or returned values; Err carries an
2222 /// error raised inside the coroutine (the coroutine becomes dead).
2223 pub(crate) fn resume_coro(
2224 &mut self,
2225 co: Gc<Coro>,
2226 args: Vec<Value>,
2227 ) -> Result<Vec<Value>, LuaError> {
2228 match co.status {
2229 CoroStatus::Suspended => {}
2230 CoroStatus::Dead => return Err(self.plain_err("cannot resume dead coroutine")),
2231 _ => return Err(self.plain_err("cannot resume non-suspended coroutine")),
2232 }
2233 if self.c_depth >= MAX_C_DEPTH {
2234 return Err(self.plain_err("C stack overflow"));
2235 }
2236 self.c_depth += 1;
2237 let resumer = self.current;
2238 // save the resumer's live context away
2239 let rctx = self.take_ctx();
2240 match resumer {
2241 Some(r) => {
2242 // 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).
2243 let m = unsafe { r.as_mut() };
2244 m.stack = rctx.stack;
2245 m.frames = rctx.frames;
2246 m.frame_ccmt = rctx.frame_ccmt;
2247 m.open_upvals = rctx.open_upvals;
2248 m.tbc = rctx.tbc;
2249 m.top = rctx.top;
2250 m.pcall_depth = rctx.pcall_depth;
2251 m.globals = rctx.globals;
2252 m.status = CoroStatus::Normal;
2253 m.natives = self.natives_base..self.running_natives.len();
2254 // bulk overwrite of every traced field on r — mirror
2255 // store_coro_ctx's barrier_back so propagate re-traces r.
2256 self.heap
2257 .barrier_back(r.as_ptr() as *mut crate::runtime::heap::GcHeader);
2258 }
2259 None => self.main_ctx = Some(rctx),
2260 }
2261 // swap the coroutine in
2262 self.load_coro_ctx(co);
2263 {
2264 // 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).
2265 let m = unsafe { co.as_mut() };
2266 m.status = CoroStatus::Running;
2267 m.resumer = resumer;
2268 }
2269 // co.resumer is a traced Gc field; barrier_back covers the new
2270 // resumer reference and any future field writes during this call.
2271 self.heap
2272 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
2273 self.current = Some(co);
2274 let resumer_natives_base = self.natives_base;
2275 self.natives_base = self.running_natives.len();
2276 // the coroutine's own frames start a fresh reach for xpcall handlers
2277 let resumer_msgh_floor = std::mem::replace(&mut self.msgh_floor, 0);
2278 let resumer_msgh_running = self.msgh_running.take();
2279 // a coroutine that dies keeps its traceback for `debug.traceback(co)`
2280 let resumer_keeps_traceback = std::mem::replace(&mut self.keep_error_traceback, true);
2281
2282 // drive it
2283 let drive = if co.started {
2284 self.coro_continue(&args)
2285 } else {
2286 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
2287 unsafe { co.as_mut() }.started = true;
2288 self.coro_first(co.body, &args)
2289 };
2290
2291 // classify: a self-close termination or a pending yield each win over
2292 // the (sentinel) error they raised to unwind the Rust stack.
2293 let (outcome, status) = if let Some(death) = self.terminating.take() {
2294 // the coroutine closed itself: it dies now, cleanly or with the
2295 // error a `__close` handler raised.
2296 match death {
2297 Some(e) => {
2298 // 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).
2299 unsafe { co.as_mut() }.error_value = Some(e);
2300 self.heap
2301 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
2302 (Err(LuaError(e)), CoroStatus::Dead)
2303 }
2304 None => (Ok(Vec::new()), CoroStatus::Dead),
2305 }
2306 } else {
2307 match self.yielding.take() {
2308 Some((vals, fslot, nres)) => {
2309 // 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).
2310 unsafe { co.as_mut() }.resume_at = Some((fslot, nres));
2311 (Ok(vals), CoroStatus::Suspended)
2312 }
2313 None => {
2314 // died: a return is clean, an error is remembered so a later
2315 // `coroutine.close` can report it (PUC lua_closethread).
2316 // Keep the error-point traceback (taken by `unwind` before
2317 // popping the failing frames) so `debug.traceback(co)` on
2318 // the dead coroutine still shows the error site, as PUC's
2319 // untouched dead stack does (db.lua :848 family).
2320 if drive.is_err() {
2321 let levels = self.error_traceback.take().unwrap_or_default();
2322 let tb =
2323 crate::vm::callstack::traceback_from_lines(self.version, &levels, 0);
2324 // 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).
2325 let m = unsafe { co.as_mut() };
2326 m.error_traceback = Some(tb);
2327 m.error_levels = Some(levels);
2328 }
2329 if let Err(e) = drive {
2330 // 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).
2331 unsafe { co.as_mut() }.error_value = Some(e.0);
2332 self.heap
2333 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
2334 }
2335 (drive, CoroStatus::Dead)
2336 }
2337 }
2338 };
2339
2340 // save the coroutine's context back and restore the resumer
2341 self.natives_base = resumer_natives_base;
2342 self.msgh_floor = resumer_msgh_floor;
2343 self.msgh_running = resumer_msgh_running;
2344 self.keep_error_traceback = resumer_keeps_traceback;
2345 self.store_coro_ctx(co);
2346 // 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).
2347 unsafe { co.as_mut() }.status = status;
2348 match resumer {
2349 Some(r) => {
2350 self.load_coro_ctx(r);
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 { r.as_mut() }.status = CoroStatus::Running;
2353 self.current = Some(r);
2354 }
2355 None => {
2356 let m = self.main_ctx.take().expect("main context saved");
2357 self.put_ctx(m);
2358 self.current = None;
2359 }
2360 }
2361 self.c_depth -= 1;
2362 outcome
2363 }
2364
2365 /// First resume: install the body function at slot 0 and run.
2366 fn coro_first(&mut self, body: Value, args: &[Value]) -> Result<Vec<Value>, LuaError> {
2367 self.stack.clear();
2368 self.stack.push(body);
2369 self.stack.extend_from_slice(args);
2370 self.top = self.stack.len() as u32;
2371 match self.begin_call(0, Some(args.len() as u32), -1, true) {
2372 Ok(true) => self.exec_with(1),
2373 Ok(false) => Ok(self.take_results(0)),
2374 Err(e) => Err(e),
2375 }
2376 }
2377
2378 /// Resume after a yield: deliver `args` as the results of the call that
2379 /// yielded, then continue the suspended thread.
2380 fn coro_continue(&mut self, args: &[Value]) -> Result<Vec<Value>, LuaError> {
2381 let (fslot, nres) = self.current.unwrap().resume_at.expect("resume point");
2382 let n = args.len() as u32;
2383 // Restore the full register window of the suspended top frame: a yield
2384 // that unwound through a native (call_value) may have left the stack
2385 // shorter than the frame needs. `base + max_stack` is what push_frame
2386 // allocates; `fslot + n` covers the delivered yield results.
2387 let frame_need = self
2388 .frames
2389 .last()
2390 .and_then(CallFrame::lua)
2391 .map(|f| (f.base + f.closure.proto.max_stack as u32) as usize)
2392 .unwrap_or(0);
2393 let need = frame_need.max((fslot + n) as usize);
2394 if self.stack.len() < need {
2395 self.stack.resize(need, Value::Nil);
2396 }
2397 for (i, &v) in args.iter().enumerate() {
2398 self.stack[fslot as usize + i] = v;
2399 }
2400 self.finish_results(fslot, n, nres);
2401 // the suspended `coroutine.yield` (a C call) now returns its resume
2402 // values: fire the matching "return" hook PUC defers until the resume.
2403 self.hook_return(true, 1, n)?;
2404 self.exec_with(1)
2405 }
2406
2407 /// `coroutine.yield`: suspend the running coroutine, recording where to
2408 /// resume. Errors if called outside a coroutine. Returns a sentinel error
2409 /// that `exec`/`resume_coro` recognise as a yield (never surfaced to Lua).
2410 pub(crate) fn do_yield(&mut self, func_slot: u32, vals: Vec<Value>) -> LuaError {
2411 let nres = self.native_nresults;
2412 self.yielding = Some((vals, func_slot, nres));
2413 // value is irrelevant: resume_coro consults `self.yielding`, not this
2414 LuaError(Value::Nil)
2415 }
2416
2417 /// Install or clear the debug hook on the running thread (`debug.sethook`
2418 /// without a thread argument). Arms the calling frame's `oldpc` to the
2419 /// sethook CALL's own pc (one less than the next-to-execute pc), mirroring
2420 /// PUC `rethook`'s `L->oldpc = pcRel(savedpc, p)` (= savedpc - code - 1) on
2421 /// native return: the very next traceexec compares against the sethook
2422 /// CALL's line. When the install statement and the following statement are
2423 /// on different source lines (db.lua :322), `changedline` fires for that
2424 /// first statement; when they share a line (db.lua :25 wrapper), they do
2425 /// not, so the wrapper line is not re-fired.
2426 pub(crate) fn install_hook(&mut self, hook: HookState) {
2427 self.hook = hook;
2428 if self.hook.line
2429 && let Some(f) = self.frames.last_mut().and_then(CallFrame::lua_mut)
2430 {
2431 f.hook_oldpc = f.pc.saturating_sub(1);
2432 }
2433 }
2434
2435 /// Install a hook on `target` (`None`/current thread → the live VM fields;
2436 /// another, suspended thread → its saved `Coro` state). PUC `debug.sethook`
2437 /// with an optional thread argument.
2438 ///
2439 /// `target == None` means "no explicit thread argument" — PUC binds that
2440 /// to `L` (the running thread). luna's live VM fields (`self.hook`,
2441 /// `self.frames`, `self.stack`) ARE the running thread's state, regardless
2442 /// of whether that's the main thread or a currently-resumed coroutine
2443 /// (save/restore happens at resume/yield boundaries via `load_coro_ctx`/
2444 /// `store_coro_ctx`). So a `None` target should always route to
2445 /// `install_hook` on the live fields. The pre-fix predicate gate
2446 /// `is_current_thread(target)` returned `false` when running inside a
2447 /// coroutine (`self.current = Some(co)`, `target = None` don't match)
2448 /// and silently dropped the hook on the floor — the install happened on
2449 /// no thread at all.
2450 pub(crate) fn set_hook(&mut self, target: Option<Gc<Coro>>, state: HookState) {
2451 if target.is_none() || self.is_current_thread(target) {
2452 self.install_hook(state);
2453 } else if let Some(co) = target {
2454 // 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).
2455 let m = unsafe { co.as_mut() };
2456 m.hook = state;
2457 if state.line
2458 && let Some(f) = m.frames.last_mut().and_then(CallFrame::lua_mut)
2459 {
2460 f.hook_oldpc = u32::MAX;
2461 }
2462 // co.hook.func is a traced Value (Coro::trace covers it); demote
2463 // co back to gray so propagate sees the new hook function.
2464 self.heap
2465 .barrier_back(co.as_ptr() as *mut crate::runtime::heap::GcHeader);
2466 }
2467 }
2468
2469 /// The hook state of `target` (`None`/current → the live VM state).
2470 pub(crate) fn get_hook(&self, target: Option<Gc<Coro>>) -> HookState {
2471 match target {
2472 t if self.is_current_thread(t) => self.hook,
2473 Some(co) => co.hook,
2474 None => self.hook,
2475 }
2476 }
2477
2478 /// Invoke the debug hook for `event` (PUC `luaD_hook`). The hook runs with
2479 /// hooks disabled (PUC clears the mask) and its results/stack growth are
2480 /// discarded so the interrupted frame's register window is untouched.
2481 /// `line` is the source line for a "line" event, `None` (→ nil) otherwise.
2482 fn run_hook(
2483 &mut self,
2484 event: &[u8],
2485 line: Option<i64>,
2486 from_native: bool,
2487 ) -> Result<(), LuaError> {
2488 // line and count events transfer no values (PUC `luaD_hook(L,
2489 // event, line, 0, 0)`); call and return hooks set theirs first
2490 if matches!(event, b"line" | b"count") {
2491 self.hook_ftransfer = 0;
2492 self.hook_ntransfer = 0;
2493 }
2494 // Rust hook fires first (no Vm reentrancy via call_value;
2495 // synchronous fn pointer call). Both Rust and Lua hooks may be
2496 // installed; both observe each event.
2497 if let Some(rh) = self.hook.rust_func {
2498 let evt = match event {
2499 b"call" => Some(RustHookEvent::Call),
2500 b"return" => Some(RustHookEvent::Return),
2501 b"tail call" | b"tail return" => Some(RustHookEvent::TailCall),
2502 b"line" => Some(RustHookEvent::Line(line.unwrap_or(0).max(0) as u32)),
2503 b"count" => Some(RustHookEvent::Count),
2504 _ => None,
2505 };
2506 if let Some(evt) = evt {
2507 let was_in_hook = self.in_hook;
2508 self.in_hook = true;
2509 rh(self, evt);
2510 self.in_hook = was_in_hook;
2511 }
2512 }
2513 let Some(hook) = self.hook.func else {
2514 return Ok(());
2515 };
2516 let saved_top = self.top;
2517 let saved_len = self.stack.len();
2518 let name = Value::Str(self.heap.intern(event));
2519 let lv = line.map_or(Value::Nil, Value::Int);
2520 self.in_hook = true;
2521 // PUC `db_sethook`'s C trampoline `hookf` sits between the engine and
2522 // the Lua hook — so `getinfo(2)` inside the hook resolves to whatever
2523 // ci sat below `hookf` (the function being hooked). When that hooked
2524 // function is native, no Lua frame for it exists in luna's `frames`;
2525 // model it as a synthetic C level by pushing the hook with
2526 // `from_c = true` (then `c_frame_name` reads the caller's call
2527 // instruction → e.g. `name = "sethook"`). When the hooked function is
2528 // Lua (its frame is still on the stack), push with `from_c = false`
2529 // so the level descent lands on it directly. The hook's own frame
2530 // carries `is_hook = true` so `getinfo(1).namewhat` reports "hook"
2531 // (PUC `CIST_HOOKED`).
2532 self.pending_is_hook = true;
2533 let r = self.call_value_impl(hook, &[name, lv], from_native);
2534 self.pending_is_hook = false;
2535 self.in_hook = false;
2536 self.stack.truncate(saved_len);
2537 self.top = saved_top;
2538 r.map(|_| ())
2539 }
2540
2541 /// Fire the "call" hook on entry to a function, if armed and not already in
2542 /// a hook (PUC clears the mask while a hook runs). PUC's transferinfo for
2543 /// a call hook is the param window: ftransfer = 1, ntransfer = nargs.
2544 /// `is_tail` selects the "tail call" event (PUC `LUA_HOOKTAILCALL`); a
2545 /// tail-call hook has no matching return hook (PUC luaD_pretailcall).
2546 fn hook_call_with(
2547 &mut self,
2548 from_native: bool,
2549 nargs: u32,
2550 is_tail: bool,
2551 ) -> Result<(), LuaError> {
2552 if self.hook.call
2553 && !self.in_hook
2554 && (self.hook.func.is_some() || self.hook.rust_func.is_some())
2555 {
2556 self.hook_ftransfer = 1;
2557 self.hook_ntransfer = nargs.min(u16::MAX as u32) as u16;
2558 // PUC 5.1 didn't distinguish tail-call events — every call,
2559 // including tail-calls, fired plain `"call"`. 5.2 introduced
2560 // the separate `"tail call"` event (mask `"c"` covers both).
2561 // 5.1 db.lua :366 pins this with `{"call","call","call","call",
2562 // "return","tail return","return","tail return"}`.
2563 let event: &[u8] = if is_tail && self.version >= LuaVersion::Lua52 {
2564 b"tail call"
2565 } else {
2566 b"call"
2567 };
2568 self.run_hook(event, None, from_native)?;
2569 }
2570 Ok(())
2571 }
2572
2573 pub(crate) fn hook_call(&mut self, from_native: bool, nargs: u32) -> Result<(), LuaError> {
2574 self.hook_call_with(from_native, nargs, false)
2575 }
2576
2577 /// Fire the "return" hook on exit from a function, if armed. ftransfer is
2578 /// the first result slot relative to the activation's func slot, ntransfer
2579 /// the number of results.
2580 pub(crate) fn hook_return(
2581 &mut self,
2582 from_native: bool,
2583 ftransfer: u32,
2584 nresults: u32,
2585 ) -> Result<(), LuaError> {
2586 if self.hook.ret
2587 && !self.in_hook
2588 && (self.hook.func.is_some() || self.hook.rust_func.is_some())
2589 {
2590 self.hook_ftransfer = ftransfer.min(u16::MAX as u32) as u16;
2591 self.hook_ntransfer = nresults.min(u16::MAX as u32) as u16;
2592 self.run_hook(b"return", None, from_native)?;
2593 }
2594 Ok(())
2595 }
2596
2597 /// PUC "tail return" event — fires once per tail call that collapsed
2598 /// into the activation now returning, *after* its own "return" event.
2599 /// 5.1 hook mask `"r"` covers both `return` and `tail return`.
2600 fn hook_tail_return(&mut self) -> Result<(), LuaError> {
2601 if self.hook.ret
2602 && !self.in_hook
2603 && (self.hook.func.is_some() || self.hook.rust_func.is_some())
2604 {
2605 self.run_hook(b"tail return", None, false)?;
2606 }
2607 Ok(())
2608 }
2609
2610 /// Call a metamethod with a single expected result.
2611 fn call_mm1(&mut self, f: Value, args: &[Value]) -> Result<Value, LuaError> {
2612 let mut r = self.call_value(f, args)?;
2613 Ok(if r.is_empty() {
2614 Value::Nil
2615 } else {
2616 r.swap_remove(0)
2617 })
2618 }
2619
2620 /// Begin a *yieldable* metamethod call from a VM instruction: `func(args…)`
2621 /// driven through the interpreter loop with a `Meta` continuation, so a
2622 /// `coroutine.yield` inside the metamethod suspends and resumes cleanly.
2623 /// On the metamethod's return the loop head runs `finish_meta(action, …)`.
2624 /// Returns to the caller with the call set up — the opcode arm must do no
2625 /// further work on the running frame and let the loop iterate. `tm` is
2626 /// the metamethod event name (e.g. "index", "add"); a Lua handler frame
2627 /// born from this call inherits it via `pending_tm`, so
2628 /// `debug.getinfo(1).namewhat == "metamethod"` and `.name == tm`
2629 /// (db.lua :878).
2630 fn begin_meta_call(
2631 &mut self,
2632 func: Value,
2633 args: &[Value],
2634 action: MetaAction,
2635 tm: &'static str,
2636 ) -> Result<(), LuaError> {
2637 let saved_top = self.top;
2638 let cont_slot = self.stack.len() as u32;
2639 self.stack.push(func);
2640 self.stack.extend_from_slice(args);
2641 self.top = self.stack.len() as u32;
2642 frames_push_sync(
2643 &mut self.frames,
2644 &mut self.frames_top,
2645 CallFrame::Cont(NativeCont {
2646 kind: ContKind::Meta(MetaCont { action, saved_top }),
2647 func_slot: cont_slot,
2648 nresults: 1,
2649 }),
2650 );
2651 let saved_tm = self.pending_tm.replace(tm);
2652 // begin_call drives a Lua metamethod through the loop (returns true) or
2653 // runs a native one inline (returns false, leaving results at cont_slot
2654 // for the loop head to pick up); either way the Meta cont resolves there.
2655 let r = self.begin_call(cont_slot, Some(args.len() as u32), 1, true);
2656 // Native callees never consumed pending_tm (push_frame is only hit on
2657 // a Lua callee); restore so it doesn't leak to a later push_frame.
2658 self.pending_tm = saved_tm;
2659 r?;
2660 Ok(())
2661 }
2662
2663 /// `R[dst] := t[key]` for a VM read opcode, resolving `__index` yieldably.
2664 fn op_index(&mut self, t: Value, key: Value, dst: u32) -> Result<(), LuaError> {
2665 // Read-time probe: a collectable key must be live at
2666 // the moment it is used. O(1) membership test against the
2667 // freed-pointer log — gc-verify diagnostic builds only; exact
2668 // under quarantining allocators (ASAN).
2669 #[cfg(feature = "gc-verify")]
2670 if matches!(key, Value::Str(_)) {
2671 let h = match key {
2672 Value::Str(s) => s.as_ptr() as usize,
2673 _ => unreachable!(),
2674 };
2675 if self.heap.recently_freed.contains(&h) {
2676 let (pc, reg_info) = match self.frames.last() {
2677 Some(CallFrame::Lua(f)) => {
2678 let pc = f.pc as usize;
2679 let inst = f.closure.proto.code.get(pc.wrapping_sub(1));
2680 (
2681 pc,
2682 inst.map(|i| {
2683 format!(
2684 "op[pc-1]={:?} a={} b={} c={} base={}",
2685 i.op(),
2686 i.a(),
2687 i.b(),
2688 i.c(),
2689 f.base
2690 )
2691 })
2692 .unwrap_or_default(),
2693 )
2694 }
2695 _ => (0, String::new()),
2696 };
2697 panic!(
2698 "[gc-verify] op_index READ of dead string key {h:#x} \
2699 (gc_top {}, top {}, pc {pc}, {reg_info})",
2700 self.gc_top, self.top,
2701 );
2702 }
2703 }
2704 match self.index_step(t, key)? {
2705 MmOut::Done(v) => self.stack[dst as usize] = v,
2706 MmOut::Mm { func, recv } => {
2707 self.begin_meta_call(func, &[recv, key], MetaAction::Store { dst }, "index")?;
2708 }
2709 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from index_step"),
2710 }
2711 Ok(())
2712 }
2713
2714 /// `t[key] := v` for a VM write opcode, resolving `__newindex` yieldably.
2715 fn op_newindex(&mut self, t: Value, key: Value, v: Value) -> Result<(), LuaError> {
2716 match self.newindex_step(t, key, v)? {
2717 MmOut::Done(_) => {}
2718 MmOut::Mm { func, recv } => {
2719 self.begin_meta_call(func, &[recv, key, v], MetaAction::Discard, "newindex")?;
2720 }
2721 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from newindex_step"),
2722 }
2723 Ok(())
2724 }
2725
2726 /// Apply a comparison opcode's outcome: a known boolean drives the
2727 /// conditional skip directly; a metamethod is called yieldably, its
2728 /// truthiness driving the skip on return.
2729 fn op_compare(
2730 &mut self,
2731 step: MmOut,
2732 l: Value,
2733 r: Value,
2734 k: bool,
2735 tm: &'static str,
2736 ) -> Result<(), LuaError> {
2737 match step {
2738 MmOut::Done(v) => self.cond_skip(v.truthy(), k),
2739 MmOut::Mm { func, .. } => {
2740 self.begin_meta_call(func, &[l, r], MetaAction::Compare { k, negate: false }, tm)?;
2741 }
2742 MmOut::CompareSynth { func } => {
2743 // ≤5.3 `__le` falls back to `not __lt(r, l)`; the swap and
2744 // negation are driven through `MetaAction::Compare` so the
2745 // metamethod call can yield like any other compare.
2746 self.begin_meta_call(func, &[r, l], MetaAction::Compare { k, negate: true }, "lt")?;
2747 }
2748 }
2749 Ok(())
2750 }
2751
2752 /// Complete a VM instruction whose metamethod just returned `result` (PUC
2753 /// `luaV_finishOp`). The running frame is already back on top.
2754 fn finish_meta(&mut self, action: MetaAction, result: Value) -> Result<(), LuaError> {
2755 match action {
2756 MetaAction::Store { dst } => self.stack[dst as usize] = result,
2757 MetaAction::Discard => {}
2758 MetaAction::Compare { k, negate } => {
2759 let t = if negate {
2760 !result.truthy()
2761 } else {
2762 result.truthy()
2763 };
2764 self.cond_skip(t, k);
2765 }
2766 MetaAction::Concat { dst, base_a } => {
2767 self.stack[dst as usize] = result;
2768 self.top = dst + 1;
2769 self.concat_run(base_a)?;
2770 }
2771 }
2772 Ok(())
2773 }
2774
2775 // ---- metatables ----
2776
2777 pub(crate) fn metatable_of(&self, v: Value) -> Option<Gc<Table>> {
2778 match v {
2779 Value::Table(t) => t.metatable(),
2780 Value::Userdata(u) => u.metatable(),
2781 v => type_mt_slot(v).and_then(|i| self.type_mt[i]),
2782 }
2783 }
2784
2785 /// Set the shared metatable for `v`'s basic type (debug.setmetatable on a
2786 /// non-table). No-op for tables (they carry their own).
2787 pub(crate) fn set_type_metatable(&mut self, v: Value, mt: Option<Gc<Table>>) {
2788 if let Some(i) = type_mt_slot(v) {
2789 self.type_mt[i] = mt;
2790 }
2791 }
2792
2793 /// The metamethod of `v` for `mm`, or nil.
2794 pub(crate) fn get_mm(&self, v: Value, mm: Mm) -> Value {
2795 match self.metatable_of(v) {
2796 Some(mt) => mt.get(Value::Str(self.mm_names[mm as usize])),
2797 None => Value::Nil,
2798 }
2799 }
2800
2801 /// PUC 5.1 `get_compTM`: a comparison metamethod (`__eq` / `__lt` / `__le`)
2802 /// only fires when both operands carry a metatable that exposes the same
2803 /// implementation. Returns the metamethod to call, or `Nil` when no
2804 /// compatible match exists. Used to honour events.lua 5.1 :262's rule
2805 /// that `c == d` (where `d` has no metatable) falls back to raw equality.
2806 pub(crate) fn get_comp_mm(&self, l: Value, r: Value, mm: Mm) -> Value {
2807 let mt1 = self.metatable_of(l);
2808 let Some(mt1) = mt1 else { return Value::Nil };
2809 let key = Value::Str(self.mm_names[mm as usize]);
2810 let tm1 = mt1.get(key);
2811 if tm1.is_nil() {
2812 return Value::Nil;
2813 }
2814 let mt2 = self.metatable_of(r);
2815 let Some(mt2) = mt2 else { return Value::Nil };
2816 if mt1.as_ptr() == mt2.as_ptr() {
2817 return tm1;
2818 }
2819 let tm2 = mt2.get(key);
2820 if tm2.is_nil() {
2821 return Value::Nil;
2822 }
2823 if tm1.raw_eq(tm2) {
2824 return tm1;
2825 }
2826 Value::Nil
2827 }
2828
2829 /// PUC `luaT_objtypename`: the type name shown in error messages. A table
2830 /// or full userdata whose metatable carries a string `__name` reports that
2831 /// (e.g. "FILE*", "My Type") instead of the bare "table"/"userdata".
2832 pub(crate) fn obj_typename(&self, v: Value) -> String {
2833 // `__name` (luaT_objtypename) arrived in 5.3
2834 if self.version >= LuaVersion::Lua53
2835 && matches!(v, Value::Table(_) | Value::Userdata(_))
2836 && let Value::Str(s) = self.get_mm(v, Mm::Name)
2837 {
2838 return String::from_utf8_lossy(s.as_bytes()).into_owned();
2839 }
2840 v.type_name().to_string()
2841 }
2842
2843 fn call_at(
2844 &mut self,
2845 func_slot: u32,
2846 nargs: u32,
2847 from_c: bool,
2848 ) -> Result<Vec<Value>, LuaError> {
2849 let depth = self.frames.len();
2850 match self.begin_call(func_slot, Some(nargs), -1, from_c) {
2851 // run until every frame the call pushed has returned: a pcall /
2852 // xpcall / __pairs continuation sits below the frame of the
2853 // function it called, and it is the continuation that produces
2854 // the call's results (`true, ...`, or `false, msg` on an error)
2855 Ok(true) => self.exec_with(depth + 1),
2856 // native completed inline; results at func_slot..top
2857 Ok(false) => Ok(self.take_results(func_slot)),
2858 // pcall / xpcall pushed their continuation and then failed to
2859 // call their function (`pcall("x")`): the continuation catches
2860 // that error, as it does in the dispatch loop
2861 Err(e)
2862 if self.frames.len() > depth
2863 && self.yielding.is_none()
2864 && self.terminating.is_none()
2865 && !self.host_yield_pending
2866 && self.pending_async_native_fut.is_none() =>
2867 {
2868 match self.unwind(e.0, depth + 1) {
2869 Unwound::Caught => self.exec_with(depth + 1),
2870 Unwound::CaughtReturn(vals) => Ok(vals),
2871 Unwound::Propagated(err) => Err(err),
2872 }
2873 }
2874 Err(e) => Err(e),
2875 }
2876 }
2877
2878 /// Switch the `collectgarbage` mode, returning the previous mode name.
2879 pub(crate) fn gc_switch_mode(&mut self, new: &'static str) -> &'static str {
2880 std::mem::replace(&mut self.gc_mode, new)
2881 }
2882
2883 /// Whether the current `collectgarbage` mode is "generational" (where a
2884 /// "step" is a minor collection — a full atomic pass — rather than a paced
2885 /// incremental sweep).
2886 pub(crate) fn gc_mode_is_generational(&self) -> bool {
2887 self.gc_mode == "generational"
2888 }
2889
2890 /// Current `stepsize` pacing parameter (PUC: 0 means an unbounded step that
2891 /// completes a whole cycle at once).
2892 pub(crate) fn gc_stepsize(&self) -> i64 {
2893 self.gc_stepsize
2894 }
2895
2896 /// Set luna's collector knobs: heap growth before a new cycle (%), sweep
2897 /// work per safe point, and the default step size (0 = a step completes
2898 /// the cycle).
2899 pub(crate) fn set_gc_pacing(&mut self, pause: i64, stepmul: i64, stepsize: i64) {
2900 self.gc_pause = pause;
2901 self.gc_stepmul = stepmul;
2902 self.gc_stepsize = stepsize;
2903 }
2904
2905 /// Interpreter safe-point auto-GC: FULL incremental Propagate + adaptive
2906 /// paced sweep via `Vm::gc_step`.
2907 ///
2908 /// Running Propagate from a safe-point relies on objects being **born
2909 /// black during Propagate**: a newly allocated object never becomes
2910 /// dead-white at the atomic flip.
2911 ///
2912 /// Adaptive budget scales with heap size: 100M-object heap (heavy.lua's
2913 /// `loadrep` stress) gets a 25M-object budget so a cycle completes in
2914 /// O(SWEEP_DIVISOR) safe-points regardless of size.
2915 #[inline(always)]
2916 pub(crate) fn maybe_collect_garbage(&mut self, live_top: u32) {
2917 if self.gc_finalizing {
2918 return;
2919 }
2920 if !self.heap.gc_due() {
2921 return;
2922 }
2923 // Bare `live_top`, no `max(self.top)` widening: every frame-pop
2924 // site (`finish_results`, the Op::TailCall collapse, pcall
2925 // unwind) clears the slots it vacates, mirroring PUC's L->top
2926 // discipline.
2927 self.gc_top = live_top;
2928 // PUC stepmul: % of allocation rate. Higher = more GC work per
2929 // safe-point (lower memory, more CPU). Default 100 = `live / 4` per
2930 // step (~4 safe-points per cycle). stepmul=200 → `live / 2`, etc.
2931 const SWEEP_BASE: usize = 400; // 400 / stepmul=100 = divisor 4
2932 const MIN_BUDGET: usize = 64_000;
2933 let stepmul = self.gc_stepmul.max(1) as usize;
2934 let divisor = (SWEEP_BASE / stepmul).max(1);
2935 let budget = (self.heap.live_objects() / divisor).max(MIN_BUDGET);
2936 if self.gc_step(budget) {
2937 self.heap.rearm_gc_pause(self.gc_pause);
2938 }
2939 }
2940
2941 /// Enumerate the GC roots: first-class `Value` roots plus bare-object
2942 /// roots (open upvalues, which are not first-class Values). Shared by the
2943 /// full collector and the incremental-sweep driver so both snapshot the
2944 /// exact same live set.
2945 fn gc_roots(&self) -> (Vec<Value>, Vec<*mut GcHeader>) {
2946 let mut roots: Vec<Value> = Vec::with_capacity(self.stack.len() + 32);
2947 roots.push(Value::Table(self.globals));
2948 for mt in self.type_mt.into_iter().flatten() {
2949 roots.push(Value::Table(mt));
2950 }
2951 for &n in &self.mm_names {
2952 roots.push(Value::Str(n));
2953 }
2954 // Root the running thread's live registers (PUC marks [stack, top)).
2955 // `gc_top` is the instruction-level cursor of the last GC
2956 // safe-point: allocation safe-points set it via
2957 // `maybe_collect_garbage(live_top)`, and `begin_call` raises it
2958 // to the callee's argument top when entering a native — PUC's
2959 // `L->top = func + 1 + nargs` C-call discipline. Without that
2960 // raise, an explicit `collectgarbage()` collected with a STALE
2961 // cursor from some earlier (lower) safe-point and freed its own
2962 // caller's register-held strings
2963 // (STATUS_ACCESS_VIOLATION on Windows / ASAN heap-use-after-free
2964 // on Linux). Values stranded above the cursor stay
2965 // excluded so weak-table entries are not spuriously pinned
2966 // (gc.lua:544 suspended-coroutine collection).
2967 let live = (self.gc_top as usize).min(self.stack.len());
2968 roots.extend_from_slice(&self.stack[..live]);
2969 for cf in &self.frames {
2970 match cf {
2971 CallFrame::Lua(f) => roots.push(Value::Closure(f.closure)),
2972 CallFrame::Cont(NativeCont {
2973 kind: ContKind::Xpcall { handler },
2974 ..
2975 }) => roots.push(*handler),
2976 CallFrame::Cont(NativeCont {
2977 kind: ContKind::Close(cc),
2978 ..
2979 }) => {
2980 // Root the error threaded through this close chain so a
2981 // `collectgarbage()` inside a sibling `__close` handler
2982 // does not free it before the next handler is invoked
2983 // (PUC L->ci->u.l.errfunc / the closing_err shadow).
2984 if let Some(e) = cc.pending {
2985 roots.push(e);
2986 }
2987 if let AfterClose::ResumeUnwind { err, .. } = cc.after {
2988 roots.push(err);
2989 }
2990 }
2991 CallFrame::Cont(_) => {}
2992 }
2993 }
2994 if let Some(e) = self.closing_err {
2995 roots.push(e);
2996 }
2997 // Host roots — Lua-facade handles keep their referenced
2998 // values alive across calls/yields. Trace the whole vector;
2999 // unused slots (post-`unpin_all`) carry Value::Nil which the
3000 // GC ignores.
3001 for slot in &self.host_roots {
3002 // free-list slots carry Value::Nil (GC no-op)
3003 roots.push(slot.value);
3004 }
3005 // `table.sort` and similar builtins stash their working
3006 // `Vec<Value>` here so a `collectgarbage()` invoked inside the
3007 // comparator callback doesn't free strings/tables snapshotted
3008 // off the live table (sort.lua's `load(..)(); collectgarbage()`
3009 // compare regression).
3010 for buf in &self.sort_scratch {
3011 roots.extend_from_slice(buf);
3012 }
3013 // The running-natives chain holds Gc<NativeClosure>s
3014 // mid-execution. Without rooting them here, a `collectgarbage()`
3015 // invoked inside the running native (sort.lua's `load(..)();
3016 // collectgarbage()` compare callback regression) sweeps the
3017 // closure that's actively executing, leaving `nc.upvals`
3018 // dangling and the Rust local `nc` pointing at recycled memory
3019 // — the SIGSEGV pops on the very next field access or pop.
3020 for &nc in &self.running_natives {
3021 roots.push(Value::Native(nc));
3022 }
3023 // the running thread's debug hook (suspended threads root theirs via
3024 // Coro::trace / the main_ctx sweep below)
3025 if let Some(h) = self.hook.func {
3026 roots.push(h);
3027 }
3028 // the running coroutine (its saved-context fields live in the VM, but
3029 // the object itself + its resumer chain must stay reachable)
3030 if let Some(co) = self.current {
3031 roots.push(Value::Coro(co));
3032 }
3033 if let Some(mc) = self.main_coro {
3034 roots.push(Value::Coro(mc));
3035 }
3036 // debug.getregistry() and io library state
3037 if let Some(r) = self.registry {
3038 roots.push(Value::Table(r));
3039 }
3040 if let Some(mt) = self.file_mt {
3041 roots.push(Value::Table(mt));
3042 }
3043 if let Some(f) = self.io_input {
3044 roots.push(Value::Userdata(f));
3045 }
3046 if let Some(f) = self.io_output {
3047 roots.push(Value::Userdata(f));
3048 }
3049 if let Some(f) = self.io_stdin {
3050 roots.push(Value::Userdata(f));
3051 }
3052 // the main thread's saved context while a coroutine runs
3053 if let Some(m) = &self.main_ctx {
3054 roots.extend_from_slice(&m.stack);
3055 if let Some(h) = m.hook.func {
3056 roots.push(h);
3057 }
3058 for cf in &m.frames {
3059 match cf {
3060 CallFrame::Lua(f) => roots.push(Value::Closure(f.closure)),
3061 CallFrame::Cont(NativeCont {
3062 kind: ContKind::Xpcall { handler },
3063 ..
3064 }) => roots.push(*handler),
3065 CallFrame::Cont(_) => {}
3066 }
3067 }
3068 }
3069 let mut extra: Vec<*mut GcHeader> = self
3070 .open_upvals
3071 .iter()
3072 .map(|&(_, uv)| uv.as_ptr() as *mut GcHeader)
3073 .collect();
3074 if let Some(m) = &self.main_ctx {
3075 extra.extend(
3076 m.open_upvals
3077 .iter()
3078 .map(|&(_, uv)| uv.as_ptr() as *mut GcHeader),
3079 );
3080 }
3081 (roots, extra)
3082 }
3083
3084 /// Run a full collection with the VM's roots, then run any `__gc`
3085 /// finalizers the collection scheduled. A no-op (returns 0) when already
3086 /// inside a finalizer — the collector is not reentrant (PUC).
3087 pub fn collect_garbage(&mut self) -> usize {
3088 if self.gc_finalizing {
3089 return 0;
3090 }
3091 let (roots, extra) = self.gc_roots();
3092 let freed = self.heap.collect_ex(&roots, &extra);
3093 #[cfg(feature = "gc-verify")]
3094 self.verify_frame_regs_live("collect_garbage");
3095 self.run_finalizers();
3096 freed
3097 }
3098
3099 /// `gc-verify`: after a collect, every register slot the
3100 /// collector just rooted (`[0, max(gc_top, top))` — the same bound
3101 /// `gc_roots` uses) must hold a live value. A dead value inside the
3102 /// rooted range means the root snapshot and the sweep disagreed —
3103 /// a use-after-free waiting to happen. (Slots ABOVE the bound may hold
3104 /// stale dead values legitimately; the interpreter's contract is
3105 /// that it writes them before reading.)
3106 #[cfg(feature = "gc-verify")]
3107 pub(crate) fn verify_frame_regs_live(&self, ctx: &str) {
3108 let live = self.heap.debug_live_set();
3109 let header = |v: Value| -> Option<usize> {
3110 match v {
3111 Value::Str(s) => Some(s.as_ptr() as usize),
3112 Value::Table(t) => Some(t.as_ptr() as usize),
3113 Value::Closure(c) => Some(c.as_ptr() as usize),
3114 Value::Native(n) => Some(n.as_ptr() as usize),
3115 Value::Coro(c) => Some(c.as_ptr() as usize),
3116 Value::Userdata(u) => Some(u.as_ptr() as usize),
3117 _ => None,
3118 }
3119 };
3120 let bound = (self.gc_top as usize).min(self.stack.len());
3121 for i in 0..bound {
3122 if let Some(h) = header(self.stack[i])
3123 && !live.contains(&h)
3124 {
3125 panic!(
3126 "[gc-verify] {ctx}: rooted stack slot {i} (gc_top {}, top {}) \
3127 holds a dead value {h:#x} after collect",
3128 self.gc_top, self.top,
3129 );
3130 }
3131 }
3132 // Diagnostic tier: a dead value ABOVE the cursor is only a bug if
3133 // that register is a named local still in scope (the interpreter
3134 // WILL read it). Cross-check against the proto's LocVar table.
3135 for (fi, cf) in self.frames.iter().enumerate() {
3136 if let CallFrame::Lua(f) = cf {
3137 let base = f.base as usize;
3138 let maxs = f.closure.proto.max_stack as usize;
3139 let hi = (base + maxs).min(self.stack.len());
3140 let pc = f.pc;
3141 for i in bound.max(base)..hi {
3142 if let Some(h) = header(self.stack[i])
3143 && !live.contains(&h)
3144 {
3145 let reg = (i - base) as u32;
3146 if let Some(lv) = f
3147 .closure
3148 .proto
3149 .locvars
3150 .iter()
3151 .find(|lv| lv.reg == reg && lv.start_pc <= pc && pc < lv.end_pc)
3152 {
3153 panic!(
3154 "[gc-verify] {ctx}: frame {fi} IN-SCOPE LOCAL '{}' \
3155 (reg {reg}, abs {i}, pc {pc}, gc_top {}) holds a \
3156 dead value {h:#x} — live_top cursor excluded a \
3157 live named local",
3158 lv.name, self.gc_top,
3159 );
3160 }
3161 }
3162 }
3163 }
3164 }
3165 }
3166
3167 /// PUC 5.1 `collectgarbage` re-raised the first error a `__gc` finalizer
3168 /// threw; gc.lua's "errors during collection" probe relies on it. This
3169 /// variant runs the same cycle but propagates the captured finalizer
3170 /// error to the explicit caller.
3171 pub(crate) fn collect_garbage_propagating(&mut self) -> Result<usize, LuaError> {
3172 if self.gc_finalizing {
3173 return Ok(0);
3174 }
3175 let (roots, extra) = self.gc_roots();
3176 let freed = self.heap.collect_ex(&roots, &extra);
3177 #[cfg(feature = "gc-verify")]
3178 self.verify_frame_regs_live("collect_garbage_propagating");
3179 self.run_finalizers_or_err()?;
3180 Ok(freed)
3181 }
3182
3183 /// Whether a `__gc` finalizer is currently running (so `collectgarbage`
3184 /// should report fail rather than collect).
3185 pub(crate) fn gc_is_finalizing(&self) -> bool {
3186 self.gc_finalizing
3187 }
3188
3189 /// PUC 5.4+ default warnf: emit one piece of a warning message. `to_cont`
3190 /// = true indicates more pieces follow (concatenated until the first
3191 /// `to_cont = false` call flushes the whole line). Mirrors
3192 /// `lauxlib.c::warnfon` + `warnfcont` + `checkcontrol`:
3193 /// * If the buffer is fresh, `to_cont` is false, and the message is
3194 /// `@<word>`, treat as a control message — only `@on` / `@off` are
3195 /// recognised; any other `@…` is silently ignored.
3196 /// * Otherwise, while the state is `Off`, drop the piece; while `On`,
3197 /// accumulate, and flush to stderr + `warn_log` on the
3198 /// non-continuation call.
3199 pub(crate) fn emit_warn(&mut self, msg: &[u8], to_cont: bool) {
3200 if self.warn_buf.is_empty()
3201 && !to_cont
3202 && let Some(b'@') = msg.first().copied()
3203 {
3204 match &msg[1..] {
3205 b"on" => self.warn_state = WarnState::On,
3206 b"off" => self.warn_state = WarnState::Off,
3207 _ => {} // unknown control — silently ignored (PUC checkcontrol)
3208 }
3209 return;
3210 }
3211 if self.warn_state == WarnState::Off {
3212 // drop continuation pieces too — PUC `warnfoff` is the trampoline
3213 return;
3214 }
3215 self.warn_buf.extend_from_slice(msg);
3216 if !to_cont {
3217 let line = std::mem::take(&mut self.warn_buf);
3218 eprintln!("Lua warning: {}", String::from_utf8_lossy(&line));
3219 self.warn_log.push(line);
3220 }
3221 }
3222
3223 /// Drain the in-process warning log (one entry per emitted message, sans
3224 /// `"Lua warning: "` prefix and newline). For test harnesses that want to
3225 /// assert on warn output without scraping stderr.
3226 pub fn warn_log_take(&mut self) -> Vec<Vec<u8>> {
3227 std::mem::take(&mut self.warn_log)
3228 }
3229
3230 /// Arm the cooperative instruction budget. The run loop
3231 /// decrements this once per dispatch turn; on zero it raises a catchable
3232 /// `"instruction budget exceeded"` error and disarms itself so the host
3233 /// can resume with a fresh budget on the next call. `None` removes the
3234 /// cap. Pass `Some(n)` before `eval`/`call_value` for the embedder's
3235 /// short-script semantics.
3236 pub fn set_instr_budget(&mut self, budget: Option<i64>) {
3237 self.instr_budget = budget;
3238 }
3239
3240 /// Remaining instruction budget (None when unbounded).
3241 pub fn instr_budget_remaining(&self) -> Option<i64> {
3242 self.instr_budget
3243 }
3244
3245 /// Toggle the cranelift JIT. Default `true`. Sandbox embedders
3246 /// **must** disable JIT when relying on `instr_budget` — see the
3247 /// `jit_enabled` field doc for the rationale.
3248 pub fn set_jit_enabled(&mut self, enabled: bool) {
3249 self.jit.enabled = enabled;
3250 }
3251
3252 /// Current JIT enable state.
3253 pub fn jit_enabled(&self) -> bool {
3254 self.jit.enabled
3255 }
3256
3257 /// Toggle the trace JIT. Off by default. When enabled, hot
3258 /// back-edges are counted on `Proto.trace_hot_count`; once the
3259 /// counter passes `TRACE_HOT_THRESHOLD`, the dispatch loop enters
3260 /// recording mode at the back-edge target.
3261 pub fn set_trace_jit_enabled(&mut self, enabled: bool) {
3262 self.jit.trace_enabled = enabled;
3263 }
3264
3265 /// Opt-in flag for the self-link cycle catch. See field
3266 /// docs for the correctness blocker. Default `false`.
3267 pub fn set_self_link_enabled(&mut self, enabled: bool) {
3268 self.jit.self_link_enabled = enabled;
3269 }
3270
3271 /// Current state of the self-link cycle catch.
3272 pub fn self_link_enabled(&self) -> bool {
3273 self.jit.self_link_enabled
3274 }
3275
3276 #[doc(hidden)]
3277 #[deprecated(since = "3.2.0", note = "renamed to `set_self_link_enabled`")]
3278 pub fn set_p16_self_link_enabled(&mut self, enabled: bool) {
3279 self.set_self_link_enabled(enabled);
3280 }
3281
3282 #[doc(hidden)]
3283 #[deprecated(since = "3.2.0", note = "renamed to `self_link_enabled`")]
3284 pub fn p16_self_link_enabled(&self) -> bool {
3285 self.self_link_enabled()
3286 }
3287
3288 /// Current trace-JIT enable state.
3289 pub fn trace_jit_enabled(&self) -> bool {
3290 self.jit.trace_enabled
3291 }
3292
3293 /// Number of traces that have closed cleanly (looped back to the
3294 /// head PC) since this Vm was constructed. Cumulative; used by
3295 /// tests + tuning.
3296 pub fn trace_closed_count(&self) -> u64 {
3297 self.jit.counters.closed
3298 }
3299
3300 /// Number of traces that have aborted (exceeded MAX_TRACE_LEN or
3301 /// hit an un-recordable op).
3302 pub fn trace_aborted_count(&self) -> u64 {
3303 self.jit.counters.aborted
3304 }
3305
3306 /// Number of compiled traces whose close shape
3307 /// is `TraceEnd::InlineAbort` (depth>0 boundary). Such traces
3308 /// pin `dispatchable=false` because the dispatcher can't
3309 /// resume at a depth>0 PC without the matching CallFrames.
3310 /// The frame-materialisation helper could synthesise those, but
3311 /// the InlineAbort emit path isn't wired up to it.
3312 pub fn trace_inline_abort_count(&self) -> u64 {
3313 self.jit.counters.inline_abort
3314 }
3315
3316 /// See `JitCounters::dispatch_off_reasons`.
3317 pub fn trace_dispatch_off_reasons(&self) -> &[&'static str] {
3318 &self.jit.counters.dispatch_off_reasons
3319 }
3320
3321 /// See `JitCounters::compile_failed_reasons`.
3322 pub fn trace_compile_failed_reasons(&self) -> &[&'static str] {
3323 &self.jit.counters.compile_failed_reasons
3324 }
3325
3326 /// See `JitCounters::closed_lens`. Returns
3327 /// `(is_call_triggered, ops_len)` for every trace that closed.
3328 pub fn trace_closed_lens(&self) -> &[(bool, usize)] {
3329 &self.jit.counters.closed_lens
3330 }
3331
3332 /// See [`crate::vm::jit_state::JitCounters::close_cause_counts`].
3333 /// Per-reason close-cause counts (recorder-side abort/discard +
3334 /// lowerer-side dispatch_off labels) keyed by `&'static str`.
3335 pub fn trace_close_cause_counts(&self) -> &std::collections::HashMap<&'static str, u64> {
3336 &self.jit.counters.close_cause_counts
3337 }
3338
3339 /// Number of compiled traces whose
3340 /// `CompiledTrace.downrec_link` is `Some(_)` (lowerer's
3341 /// `downrec_idx_opt` arm emitted the stitch sentinel + caller-pc
3342 /// guard scaffold).
3343 pub fn trace_downrec_link_compiled_count(&self) -> u64 {
3344 self.jit.counters.downrec_link_compiled
3345 }
3346
3347 /// See
3348 /// [`crate::vm::jit_state::JitCounters::downrec_dispatched`]. Number
3349 /// of times the dispatcher's `is_downrec_sentinel` arm fired and
3350 /// classified the return as a caller-pc-guard HIT.
3351 pub fn trace_downrec_dispatched_count(&self) -> u64 {
3352 self.jit.counters.downrec_dispatched
3353 }
3354
3355 /// See
3356 /// [`crate::vm::jit_state::JitCounters::downrec_deopt`]. Number of
3357 /// times the dispatcher entered a `downrec_link`-bearing trace and
3358 /// the trace returned via the lowerer's deopt block (caller-pc
3359 /// guard MISS), or the dispatcher itself force-deopted via the
3360 /// stitch-cycle checkpoint.
3361 pub fn trace_downrec_deopt_count(&self) -> u64 {
3362 self.jit.counters.downrec_deopt
3363 }
3364
3365 /// See
3366 /// [`crate::vm::jit_state::JitCounters::multi_way_guard_emitted`].
3367 /// Number of compiled traces whose lowerer emitted a multi-way
3368 /// caller-pc guard chain (>= 2 distinct `caller_pc` candidates)
3369 /// at the `TraceEnd::DownRec` close + lifted `dispatchable = true`.
3370 pub fn trace_multi_way_guard_emitted_count(&self) -> u64 {
3371 self.jit.counters.multi_way_guard_emitted
3372 }
3373
3374 /// Number of closed traces the lowerer compiled and
3375 /// parked on `Proto.traces`. Re-records of the same head_pc are
3376 /// deduped (the second close finds the head_pc already cached
3377 /// and skips compile), so this never exceeds `trace_closed_count`.
3378 pub fn trace_compiled_count(&self) -> u64 {
3379 self.jit.counters.compiled
3380 }
3381
3382 /// Number of times the recorder captured a
3383 /// [`crate::jit::trace_types::FieldIcSnapshot`] under
3384 /// `LUNA_JIT_FIELD_IC=1`. Stays 0 on the env-default path. Used
3385 /// by the opt-in fire test to verify the env gate
3386 /// wiring round-trips end-to-end (env -> recorder -> snapshot
3387 /// -> counter -> getter -> assertion).
3388 pub fn trace_field_ic_snapshot_count(&self) -> u64 {
3389 self.jit.counters.field_ic_snapshot_captured
3390 }
3391
3392 /// Number of closed traces the lowerer rejected
3393 /// (any of the bail conditions in
3394 /// `crate::jit::trace::try_compile_trace`).
3395 pub fn trace_compile_failed_count(&self) -> u64 {
3396 self.jit.counters.compile_failed
3397 }
3398
3399 /// Number of times the dispatcher jumped into a
3400 /// compiled trace. Bumps on every entry; `trace_deopt_count`
3401 /// counts the subset where the trace returned with a parked
3402 /// `jit_pending_err`.
3403 pub fn trace_dispatched_count(&self) -> u64 {
3404 self.jit.counters.dispatched
3405 }
3406
3407 /// Number of trace entries that came back with
3408 /// `jit_pending_err` set (typically a metatable shadowed an
3409 /// index inside a helper, forcing the dispatcher to fall back
3410 /// to the interpreter without committing the trace's result).
3411 pub fn trace_deopt_count(&self) -> u64 {
3412 self.jit.counters.deopt
3413 }
3414
3415 /// Number of times the dispatcher started a side
3416 /// trace recording (an `exit_hit_counts` slot crossed
3417 /// [`crate::jit::trace::HOTEXIT_THRESHOLD`] while `active_trace`
3418 /// was None and trace JIT was enabled). Each unit is exactly one
3419 /// `start_side_trace` call; the actual compile success counts
3420 /// under [`Self::trace_compiled_count`] like any other trace.
3421 /// Probe use: distinguishes the "side-trace pipeline fired"
3422 /// signal from the "primary back-edge / call-trigger fired"
3423 /// signal without reading per-counter histograms.
3424 pub fn trace_side_trace_started_count(&self) -> u64 {
3425 self.jit.counters.side_trace_started
3426 }
3427
3428 /// Number of side-trace recordings that closed,
3429 /// compiled successfully, AND patched their parent's
3430 /// `exit_side_trace_ptrs[exit_idx]`.
3431 pub fn trace_side_trace_compiled_count(&self) -> u64 {
3432 self.jit.counters.side_trace_compiled
3433 }
3434
3435 /// Number of side traces that compiled
3436 /// successfully but were SHEDDED by the close-handler shape-
3437 /// match gate (`exit_tags_match_entry_tags`). High ratios
3438 /// vs. `trace_side_trace_compiled_count` indicate the
3439 /// architecture is shedding lots of would-be side traces;
3440 /// useful as a tuning probe for future relaxation of the
3441 /// gate or for child-IR re-specialisation against parent's
3442 /// exit shape.
3443 pub fn trace_side_trace_shape_mismatch_count(&self) -> u64 {
3444 self.jit.counters.side_trace_shape_mismatch
3445 }
3446
3447 /// Sum of NewTable sites the pre-emit escape sweep
3448 /// classified as `crate::jit::trace::EscapeState::Sinkable`
3449 /// across every successfully compiled trace on this Vm. The
3450 /// count is post-demotion: sites pre-emit drops back to Escaped
3451 /// for not meeting the sunk-emit criteria are NOT counted.
3452 /// `trace_sunk_alloc_count` matches one-for-one today (every
3453 /// surviving Sinkable site goes through sunk emit).
3454 pub fn trace_sinkable_seen_count(&self) -> u64 {
3455 self.jit.counters.sinkable_seen
3456 }
3457
3458 /// See `JitCounters::accum_bufferable_seen`.
3459 pub fn trace_accum_bufferable_seen_count(&self) -> u64 {
3460 self.jit.counters.accum_bufferable_seen
3461 }
3462
3463 /// Total dispatch hits across all known traces,
3464 /// broken into hot-exit telemetry (max single-exit count,
3465 /// total dispatches, exit count). Used by probes to identify
3466 /// hot side-exits as side-trace candidates.
3467 ///
3468 /// Walks `cl.proto` AND all nested protos in `cl.proto.protos`
3469 /// recursively, so inner functions' traces are reported.
3470 pub fn trace_exit_hit_summary(
3471 &self,
3472 cl: crate::runtime::heap::Gc<crate::runtime::function::LuaClosure>,
3473 ) -> Vec<(u32, Vec<u32>)> {
3474 fn walk(
3475 proto: crate::runtime::heap::Gc<crate::runtime::function::Proto>,
3476 out: &mut Vec<(u32, Vec<u32>)>,
3477 ) {
3478 for ct in proto.traces.borrow().iter() {
3479 let counts: Vec<u32> = ct.exit_hit_counts.iter().map(|c| c.get()).collect();
3480 out.push((ct.head_pc, counts));
3481 }
3482 for inner in proto.protos.iter() {
3483 walk(*inner, out);
3484 }
3485 }
3486 let mut out: Vec<(u32, Vec<u32>)> = Vec::new();
3487 walk(cl.proto, &mut out);
3488 out
3489 }
3490
3491 /// Surface every side-exit slot whose hit count is
3492 /// `>= HOTEXIT_THRESHOLD` across every trace reachable from
3493 /// `cl.proto` (recursively walking `proto.protos`). Returned
3494 /// entries are side-trace candidates: each carries the parent
3495 /// trace's `(head_proto, head_pc)`, the exit's index in the
3496 /// parent's `exit_hit_counts`, and the side trace's natural
3497 /// entry shape (`cont_pc` + `exit_tags`).
3498 ///
3499 /// Layout of `exit_hit_counts` (mirrored by the iter):
3500 /// - `[0..per_exit_inline.len())` → `InlineSideExit` (cont_pc +
3501 /// window-sized exit_tags).
3502 /// - `[per_exit_inline.len()..inline.len() + per_exit_tags.len())`
3503 /// → `per_exit_tags[i]` (per-cont_pc caller-window tags).
3504 /// - Last slot → global clean-tail (cont_pc = `head_pc`,
3505 /// exit_tags = `ct.exit_tags`).
3506 pub fn hot_exit_iter(
3507 &self,
3508 cl: crate::runtime::heap::Gc<crate::runtime::function::LuaClosure>,
3509 ) -> Vec<crate::jit::trace::HotExitInfo> {
3510 use crate::jit::trace::{HOTEXIT_THRESHOLD, HotExitInfo};
3511 fn walk(
3512 proto: crate::runtime::heap::Gc<crate::runtime::function::Proto>,
3513 out: &mut Vec<HotExitInfo>,
3514 ) {
3515 for ct in proto.traces.borrow().iter() {
3516 let inline_n = ct.per_exit_inline.len();
3517 let tags_n = ct.per_exit_tags.len();
3518 debug_assert_eq!(
3519 ct.exit_hit_counts.len(),
3520 inline_n + tags_n + 1,
3521 "exit_hit_counts layout invariant violated"
3522 );
3523 for (idx, cell) in ct.exit_hit_counts.iter().enumerate() {
3524 let hits = cell.get();
3525 if hits < HOTEXIT_THRESHOLD {
3526 continue;
3527 }
3528 let (cont_pc, exit_tags) = if idx < inline_n {
3529 let ent = &ct.per_exit_inline[idx];
3530 (ent.cont_pc, ent.exit_tags.clone())
3531 } else if idx < inline_n + tags_n {
3532 let (pc, tags) = &ct.per_exit_tags[idx - inline_n];
3533 (*pc, tags.clone())
3534 } else {
3535 (ct.head_pc, ct.exit_tags.clone())
3536 };
3537 out.push(HotExitInfo {
3538 head_proto: proto,
3539 head_pc: ct.head_pc,
3540 exit_idx: idx,
3541 hits,
3542 cont_pc,
3543 exit_tags,
3544 });
3545 }
3546 }
3547 for inner in proto.protos.iter() {
3548 walk(*inner, out);
3549 }
3550 }
3551 let mut out: Vec<HotExitInfo> = Vec::new();
3552 walk(cl.proto, &mut out);
3553 out
3554 }
3555
3556 /// Sum of NewTable sites that actually took the
3557 /// sunk-emit path across every successfully compiled trace on
3558 /// this Vm. Each counted site skips its heap `Gc<Table>`
3559 /// allocation per dispatch; the array part lives as Cranelift
3560 /// `Variable`s for the duration of the trace.
3561 pub fn trace_sunk_alloc_count(&self) -> u64 {
3562 self.jit.counters.sunk_alloc
3563 }
3564
3565 /// Sum of materialise-helper emit sites across every
3566 /// successfully compiled trace on this Vm. Each unit is a
3567 /// (site × cmp side-exit) pair whose IR reconstructs a heap
3568 /// `Gc<Table>` from the virt slots on deopt.
3569 pub fn trace_materialize_emit_count(&self) -> u64 {
3570 self.jit.counters.materialize_emit
3571 }
3572
3573 /// Diagnostic: total `Op::Closure` ops the trace JIT
3574 /// lowered to the `luna_jit_op_closure` helper. Each emitted op
3575 /// replaces a `Heap::new_closure_inline` call on the dispatch
3576 /// path; the count is static (one per matching op per compiled
3577 /// trace), summed at compile success.
3578 pub fn trace_closure_emit_count(&self) -> u64 {
3579 self.jit.counters.closure_emit
3580 }
3581
3582 /// See
3583 /// [`crate::vm::jit_state::JitCounters::per_exit_inline_compiled`].
3584 /// Number of compiled traces whose `per_exit_inline.len() > 0`
3585 /// (depth>0 inlined cmp side-exits emitted).
3586 pub fn trace_per_exit_inline_compiled_count(&self) -> u64 {
3587 self.jit.counters.per_exit_inline_compiled
3588 }
3589
3590 /// See
3591 /// [`crate::vm::jit_state::JitCounters::per_exit_inline_dispatchable`].
3592 /// Number of compiled traces with `per_exit_inline.len() > 0` AND
3593 /// `dispatchable == true` — i.e. the count of compiled traces
3594 /// that would actually exercise the AOT chain-reloc +
3595 /// deploy-resolver path.
3596 pub fn trace_per_exit_inline_dispatchable_count(&self) -> u64 {
3597 self.jit.counters.per_exit_inline_dispatchable
3598 }
3599
3600 /// Diagnostic: max `inline_depth` ever seen on any
3601 /// `RecordedOp` pushed by the recorder. Tells tests + tuning
3602 /// whether a self-recursive function actually walked the depth
3603 /// tracker past 0. Saturates at `MAX_INLINE_DEPTH`. Persists
3604 /// across traces and Vm activations; reset only on `Vm::new`.
3605 pub fn trace_max_depth_seen(&self) -> u8 {
3606 self.jit.max_depth_seen
3607 }
3608
3609 /// Last live Lua frame (the trace head's frame at
3610 /// dispatch time). The frame-materialization helper reads `.base`
3611 /// to compute offsets for each inlined frame's window.
3612 #[doc(hidden)]
3613 pub fn jit_last_lua_frame(&self) -> Option<Frame> {
3614 match self.frames.last() {
3615 Some(CallFrame::Lua(f)) => Some(*f),
3616 _ => None,
3617 }
3618 }
3619
3620 /// Read-only borrow of the current call
3621 /// stack, for the [`crate::vm::inspect`] pure-read accessors used
3622 /// by `luna-tools` (`luna-profile`'s sampler walks this from
3623 /// inside a `Count` hook). Sibling-module scope: not part of the
3624 /// public embedder surface, but `inspect::frames_for_profile` is.
3625 #[doc(hidden)]
3626 pub(super) fn inspect_frames(&self) -> &[CallFrame] {
3627 &self.frames
3628 }
3629
3630 /// Ensure the value stack covers indices
3631 /// `[0..need)`. Extends with Nil if shorter. Called by the
3632 /// frame-materialization helper before pushing an inlined frame
3633 /// whose register window may exceed the current stack length.
3634 #[doc(hidden)]
3635 pub fn jit_ensure_stack(&mut self, need: usize) {
3636 if self.stack.len() < need {
3637 self.stack.resize(need, Value::Nil);
3638 }
3639 }
3640
3641 /// Trace JIT path for `Op::Close A`. Predicts whether
3642 /// `__close` handlers would run (any active tbc slot ≥ from
3643 /// holding a non-nil/false Value); if so, returns 1 without doing
3644 /// anything and the trace side-exits at the op, so the interpreter
3645 /// runs the handlers. Otherwise performs the safe part of close —
3646 /// `close_from(from)` to close open upvals + drop any drained tbc
3647 /// entries ≥ from — and returns 0.
3648 ///
3649 /// Returns are i64-shaped so the cranelift import sig stays
3650 /// trivial (i64 → i64 mapping).
3651 #[doc(hidden)]
3652 pub fn jit_op_close(&mut self, start_offset: u32) -> i64 {
3653 let Some(f) = self.jit_last_lua_frame() else {
3654 return 1;
3655 };
3656 let from = f.base + start_offset;
3657 let has_handler = self.tbc.iter().any(|&s| {
3658 s >= from && {
3659 let v = self.stack[s as usize];
3660 !matches!(v, Value::Nil | Value::Bool(false))
3661 }
3662 });
3663 if has_handler {
3664 self.jit.counters.deopt += 1;
3665 return 1;
3666 }
3667 self.close_from(from);
3668 // Drain any tbc entries ≥ from (they're nil/false stubs the
3669 // interpreter's drive_close would have skipped silently).
3670 while let Some(&s) = self.tbc.last() {
3671 if s < from {
3672 break;
3673 }
3674 self.tbc.pop();
3675 }
3676 0
3677 }
3678
3679 /// Spill the trace's current value for a register to
3680 /// the underlying `vm.stack[base + slot_offset]`. Required before
3681 /// an `Op::Closure` whose inner proto has an `in_stack: true`
3682 /// upval at `slot_offset` — the helper's `find_or_create_upval`
3683 /// captures a live pointer to `vm.stack[base + slot_offset]`,
3684 /// which must hold the right value at call time (trace IR's
3685 /// Variable hasn't yet been written back).
3686 ///
3687 /// Parameters arrive as i64 from the IR: `slot_offset` is the
3688 /// caller-frame register index (`u32` in practice, depth=0
3689 /// only — depth>0 Closure is not supported); `tag` is the
3690 /// `crate::runtime::value::raw` byte for the slot's RegKind;
3691 /// `raw_bits` is the trace Variable's `use_var` payload
3692 /// (i64-shaped — Float is its bit-pattern, Table/Closure is the
3693 /// raw `Gc::as_ptr` cast).
3694 #[doc(hidden)]
3695 pub fn jit_spill_stack(&mut self, slot_offset: u32, tag: u8, raw_bits: u64) {
3696 let Some(f) = self.jit_last_lua_frame() else {
3697 self.jit.pending_err =
3698 Some(self.rt_err("JIT spill: no Lua frame on jit_last_lua_frame()"));
3699 return;
3700 };
3701 let idx = (f.base as usize) + (slot_offset as usize);
3702 if self.stack.len() <= idx {
3703 self.stack.resize(idx + 1, Value::Nil);
3704 }
3705 // SAFETY: caller (trace JIT IR emit) provides matching
3706 // `(tag, raw_bits)` — same shape produced by Value::unpack.
3707 let v = unsafe {
3708 crate::runtime::Value::pack(tag, crate::runtime::value::RawVal { zero: raw_bits })
3709 };
3710 self.stack[idx] = v;
3711 }
3712
3713 /// Refresh only the raw payload of
3714 /// `vm.stack[base + slot_offset]`, preserving its existing
3715 /// `Value` tag. The caller (trace JIT Op::Concat body emit)
3716 /// uses this when the slot's `RegKind` is `Unset` (no compile-
3717 /// time tag info; commonly `Str` slots which the trace doesn't
3718 /// model). The interp's previous execution of the same op
3719 /// already populated the slot with the right tag — the trace
3720 /// only needs to swap in its current raw value.
3721 #[doc(hidden)]
3722 pub fn jit_stack_update_raw(&mut self, slot_offset: u32, raw_bits: u64) {
3723 let Some(f) = self.jit_last_lua_frame() else {
3724 return;
3725 };
3726 let idx = (f.base as usize) + (slot_offset as usize);
3727 if idx >= self.stack.len() {
3728 return;
3729 }
3730 let (tag, _) = self.stack[idx].unpack();
3731 // 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).
3732 self.stack[idx] = unsafe {
3733 crate::runtime::Value::pack(tag, crate::runtime::value::RawVal { zero: raw_bits })
3734 };
3735 }
3736
3737 /// Trace JIT path for `Op::Concat A B`.
3738 ///
3739 /// Mirrors the interp arm (this file ~L5112): `self.top =
3740 /// base + a + n; concat_run(base + a)`. Result lands at
3741 /// `vm.stack[base + a]`. Returns `0` on success, `-1` when the
3742 /// interpreter must do it (any error from `concat_run` OR
3743 /// detection that the metamethod path was taken — `concat_run`
3744 /// returns `Ok(())` after `begin_meta_call` which has pushed a Lua
3745 /// frame the trace can't safely continue past); the trace then
3746 /// side-exits at the op and the interpreter redoes it, raising the
3747 /// error or calling `__concat` itself.
3748 ///
3749 /// The frame-push detection uses `pre/post frames.len()` and
3750 /// unwinds any pushed frames first, so the exit sees a clean stack.
3751 #[doc(hidden)]
3752 pub fn jit_op_concat(&mut self, slot_offset: u32, n: i32) -> i64 {
3753 let Some(f) = self.jit_last_lua_frame() else {
3754 return -1;
3755 };
3756 let abs_a = f.base + slot_offset;
3757 self.top = abs_a + n as u32;
3758 let pre_frames = self.frames.len();
3759 let result = self.concat_run(abs_a);
3760 let post_frames = self.frames.len();
3761 // Frame-push = metamethod path taken (begin_meta_call pushed
3762 // a Lua frame). The trace can't continue past it; unwind +
3763 // deopt so interp redoes Op::Concat in the slow path.
3764 while self.frames.len() > pre_frames {
3765 frames_pop_sync(&mut self.frames, &mut self.frames_top);
3766 }
3767 if result.is_err() || post_frames > pre_frames {
3768 self.jit.counters.deopt += 1;
3769 return -1;
3770 }
3771 0
3772 }
3773
3774 /// Pop a reusable `Vec<u8>` from the JIT
3775 /// accumulator buffer pool, returning a raw pointer. The trace
3776 /// fn's IR holds this pointer in a stack slot through the loop
3777 /// and calls `jit_str_buf_extend` per iter. If the pool is
3778 /// empty, allocate fresh.
3779 ///
3780 /// Safety: the returned pointer is valid until
3781 /// `jit_str_buf_release` is called or the Vm is dropped. The
3782 /// caller MUST not retain it across `enter_jit` boundaries.
3783 #[doc(hidden)]
3784 pub fn jit_str_buf_acquire(&mut self) -> *mut Vec<u8> {
3785 let buf = self.jit.str_buf_pool.pop().unwrap_or_default();
3786 // Move into a Box so the pointer is stable until release.
3787 Box::into_raw(Box::new(buf))
3788 }
3789
3790 /// Return a previously-acquired buffer to the
3791 /// pool, dropping any excess past `jit_str_buf_pool_cap`. The
3792 /// buffer is `clear`ed (capacity retained) so the next acquire
3793 /// gets a ready-to-extend Vec.
3794 ///
3795 /// Safety: `buf` must have been returned by a prior
3796 /// `jit_str_buf_acquire` on the same Vm.
3797 #[doc(hidden)]
3798 #[allow(clippy::not_unsafe_ptr_arg_deref)] // JIT helper: `buf` round-trips through `Box::into_raw`; SAFETY documented below.
3799 pub fn jit_str_buf_release(&mut self, buf: *mut Vec<u8>) {
3800 if buf.is_null() {
3801 return;
3802 }
3803 // 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.
3804 let mut owned = unsafe { Box::from_raw(buf) };
3805 owned.clear();
3806 if self.jit.str_buf_pool.len() < self.jit.str_buf_pool_cap {
3807 self.jit.str_buf_pool.push(*owned);
3808 }
3809 // Else: drop the buffer.
3810 }
3811
3812 /// Append a LuaStr's bytes to the accumulator
3813 /// buffer. The trace IR computes the `str_ptr` (= raw bits of
3814 /// the piece slot) and passes it through; we treat it as a
3815 /// `*mut LuaStr` and append its bytes.
3816 ///
3817 /// Returns 0 on success, -1 if the piece isn't a Str (would
3818 /// trip __concat metamethod path → deopt to interp).
3819 ///
3820 /// Safety: `buf` from prior `acquire`; `str_ptr` from the
3821 /// trace's piece slot raw bits.
3822 #[doc(hidden)]
3823 #[allow(clippy::not_unsafe_ptr_arg_deref)] // JIT helper: `buf` from prior `acquire`; `str_ptr` from trace piece slot; SAFETY documented below.
3824 pub fn jit_str_buf_extend(&mut self, buf: *mut Vec<u8>, str_ptr: i64) -> i64 {
3825 if buf.is_null() || str_ptr == 0 {
3826 return -1;
3827 }
3828 // 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).
3829 let buf = unsafe { &mut *buf };
3830 let lua_str_ptr = str_ptr as *const crate::runtime::string::LuaStr;
3831 // 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).
3832 let bytes = unsafe { crate::runtime::string::bytes_of(lua_str_ptr) };
3833 buf.extend_from_slice(bytes);
3834 0
3835 }
3836
3837 /// Drain the accumulator buffer into a fresh
3838 /// `LuaStr` via `heap.intern`, returning the raw ptr bits for
3839 /// the trace to write into the accumulator slot.
3840 ///
3841 /// Returns the LuaStr ptr as i64 on success, 0 on overflow
3842 /// (the hard cap; the trace deopts).
3843 ///
3844 /// Safety: `buf` from prior `acquire`. The buffer is left
3845 /// CLEAR (drained) ready for `release`.
3846 #[doc(hidden)]
3847 #[allow(clippy::not_unsafe_ptr_arg_deref)] // JIT helper: `buf` from prior `acquire`; SAFETY documented below.
3848 pub fn jit_str_buf_intern(&mut self, buf: *mut Vec<u8>) -> i64 {
3849 if buf.is_null() {
3850 return 0;
3851 }
3852 // 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).
3853 let buf = unsafe { &mut *buf };
3854 let bytes = std::mem::take(buf);
3855 // hard cap at 256KB
3856 if bytes.len() > 256 * 1024 {
3857 return 0;
3858 }
3859 let gc = self.heap.intern(&bytes);
3860 gc.as_ptr() as i64
3861 }
3862
3863 /// Trace JIT helper for `Op::TForCall A 0 C`.
3864 ///
3865 /// Base path: copy R[A..=A+2] → R[A+4..=A+6] + `begin_call`.
3866 /// ipairs `inext` fast path at the top — skip begin_call
3867 /// when R[A]=Native(ipairs_iter), R[A+1]=Table no-mt,
3868 /// R[A+2]=Int.
3869 /// Batched out-ptr writeback — fill ctrl/key/val raws into
3870 /// caller-provided buffers + return R[A+4]'s tag byte. Lets
3871 /// emit skip 3 separate `luna_jit_stack_load` calls and 1
3872 /// `luna_jit_stack_tag` call by reading the buffer via
3873 /// cranelift `stack_load` IR instead. Returns -1 on deopt,
3874 /// else R[A+4]'s tag byte | R[A+5]'s tag byte << 8 (the value's
3875 /// tag only when `nvars >= 2`, 0 otherwise).
3876 #[doc(hidden)]
3877 #[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.
3878 pub fn jit_op_tforcall(
3879 &mut self,
3880 slot_offset: u32,
3881 nvars: i32,
3882 ctrl_out: *mut i64,
3883 key_out: *mut i64,
3884 val_out: *mut i64,
3885 ) -> i64 {
3886 let Some(f) = self.jit_last_lua_frame() else {
3887 return -1;
3888 };
3889 let abs = f.base + slot_offset;
3890 let need = (abs + 7) as usize;
3891 if self.stack.len() < need {
3892 self.stack.resize(need, Value::Nil);
3893 }
3894 // ipairs fast path
3895 let took_fast_path = if let Value::Native(n) = self.stack[abs as usize]
3896 && std::ptr::fn_addr_eq(
3897 n.f,
3898 crate::vm::builtins::ipairs_iter as crate::runtime::value::NativeFn,
3899 )
3900 && let Value::Table(t) = self.stack[(abs + 1) as usize]
3901 && t.metatable().is_none()
3902 && let Value::Int(i) = self.stack[(abs + 2) as usize]
3903 {
3904 let next_i = i.wrapping_add(1);
3905 let v = t.get_int(next_i);
3906 if v.is_nil() {
3907 self.stack[(abs + 4) as usize] = Value::Nil;
3908 } else {
3909 self.stack[(abs + 4) as usize] = Value::Int(next_i);
3910 if (nvars as usize) >= 2 {
3911 self.stack[(abs + 5) as usize] = v;
3912 }
3913 for j in 2..nvars as usize {
3914 let slot = abs + 4 + j as u32;
3915 if (slot as usize) < self.stack.len() {
3916 self.stack[slot as usize] = Value::Nil;
3917 }
3918 }
3919 }
3920 true
3921 } else {
3922 false
3923 };
3924 if !took_fast_path {
3925 // slow path: copy R[A..=A+2] → R[A+4..=A+6], then
3926 // route through begin_call. Lua-closure iters would push
3927 // a Lua frame mid-trace → deopt.
3928 self.stack[(abs + 4) as usize] = self.stack[abs as usize];
3929 self.stack[(abs + 5) as usize] = self.stack[(abs + 1) as usize];
3930 self.stack[(abs + 6) as usize] = self.stack[(abs + 2) as usize];
3931 // the interpreter raises the call's error itself; and a native
3932 // that `begin_call` hands to the interpreter loop (pcall, xpcall,
3933 // pairs, an async native) pushes frames or parks a future
3934 // instead of returning its results here
3935 let runs_to_completion = match self.stack[abs as usize] {
3936 Value::Native(nc) => {
3937 use crate::runtime::value::NativeFn;
3938 !nc.is_async
3939 && ![
3940 nat_pcall as NativeFn,
3941 nat_xpcall as NativeFn,
3942 nat_host_xpcall as NativeFn,
3943 nat_pairs as NativeFn,
3944 ]
3945 .iter()
3946 .any(|&g| std::ptr::fn_addr_eq(nc.f, g))
3947 }
3948 _ => false,
3949 };
3950 if !runs_to_completion || self.begin_call(abs + 4, Some(2), nvars, false).is_err() {
3951 self.jit.counters.deopt += 1;
3952 return -1;
3953 }
3954 }
3955 // Batched writeback — fill the caller's buffers with the
3956 // raw bits of R[A+2] / R[A+4] / R[A+5] so the trace IR can
3957 // reload via cranelift `stack_load` instead of separate
3958 // `luna_jit_stack_load` helper calls.
3959 // 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).
3960 let ctrl_raw = unsafe { self.stack[(abs + 2) as usize].unpack().1.zero };
3961 let (key_tag, key_rv) = self.stack[(abs + 4) as usize].unpack();
3962 // 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).
3963 let key_raw = unsafe { key_rv.zero };
3964 let (val_tag, val_raw) = if (nvars as usize) >= 2 {
3965 let (tag, rv) = self.stack[(abs + 5) as usize].unpack();
3966 // 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).
3967 (tag, unsafe { rv.zero })
3968 } else {
3969 (0, 0u64)
3970 };
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 unsafe {
3973 ctrl_out.write(ctrl_raw as i64);
3974 key_out.write(key_raw as i64);
3975 val_out.write(val_raw as i64);
3976 }
3977 i64::from(key_tag) | i64::from(val_tag) << 8
3978 }
3979
3980 /// Load the raw `i64` payload of
3981 /// `vm.stack[base + slot_offset]` for the active trace's head
3982 /// Lua frame. Used to reload trace IR `Variable`s after a
3983 /// helper has written to `vm.stack` directly (e.g. TForCall's
3984 /// iter results land at `R[A+4..A+4+nvars]`).
3985 #[doc(hidden)]
3986 pub fn jit_stack_load(&mut self, slot_offset: u32) -> i64 {
3987 let Some(f) = self.jit_last_lua_frame() else {
3988 return 0;
3989 };
3990 let idx = (f.base as usize) + (slot_offset as usize);
3991 if idx >= self.stack.len() {
3992 return 0;
3993 }
3994 let v = self.stack[idx];
3995 let (_, raw) = v.unpack();
3996 // 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).
3997 unsafe { raw.zero as i64 }
3998 }
3999
4000 /// Read the tag byte of
4001 /// `vm.stack[base + slot_offset]`. Used by `Op::TForLoop` emit
4002 /// to dispatch on the iterator's return-key tag at runtime
4003 /// (`raw::NIL` → loop end exit, `raw::INT` → continue, other →
4004 /// deopt).
4005 #[doc(hidden)]
4006 pub fn jit_stack_tag(&mut self, slot_offset: u32) -> u8 {
4007 let Some(f) = self.jit_last_lua_frame() else {
4008 return crate::runtime::value::raw::NIL;
4009 };
4010 let idx = (f.base as usize) + (slot_offset as usize);
4011 if idx >= self.stack.len() {
4012 return crate::runtime::value::raw::NIL;
4013 }
4014 self.stack[idx].unpack().0
4015 }
4016
4017 /// Push a Lua frame onto the call stack with
4018 /// JIT-known metadata. Used by `luna_jit_trace_materialize_frames`
4019 /// at trace side-exits to recreate the inlined call activations
4020 /// the lowerer compiled past. The contract (enforced by the
4021 /// lowerer's pre-emit pass): `cl.proto` is non-vararg,
4022 /// `nresults` is the caller's expected count (today always 1
4023 /// because the lowerer bails Op::Call C != 2), and the caller
4024 /// has already called `jit_ensure_stack` to cover
4025 /// `[0..base + cl.proto.max_stack)`.
4026 #[doc(hidden)]
4027 pub fn jit_push_inlined_frame(
4028 &mut self,
4029 cl: Gc<LuaClosure>,
4030 base: u32,
4031 pc: u32,
4032 nresults: i32,
4033 ) {
4034 self.set_frame_ccmt(0);
4035 frames_push_sync(
4036 &mut self.frames,
4037 &mut self.frames_top,
4038 CallFrame::Lua(Frame {
4039 closure: cl,
4040 base,
4041 pc,
4042 // Lua call ABI: callee R[0] sits at caller R[A+1], so
4043 // callee.base = caller.base + A + 1; func_slot is
4044 // caller.base + A = callee.base - 1.
4045 func_slot: base - 1,
4046 n_varargs: 0,
4047 nresults,
4048 hook_oldpc: u32::MAX,
4049 from_c: false,
4050 tm: None,
4051 is_hook: false,
4052 tailcalls: 0,
4053 }),
4054 );
4055 }
4056
4057 /// Toggle precompiled-chunk loading. Default `true`. Sandbox embedders
4058 /// should set to `false` so `load`/`loadstring` reject bytecode input
4059 /// (which bypasses parser limits and could exploit verifier gaps).
4060 pub fn set_bytecode_loading(&mut self, enabled: bool) {
4061 self.bytecode_loading = enabled;
4062 }
4063
4064 /// Current bytecode-loading gate state.
4065 pub fn bytecode_loading(&self) -> bool {
4066 self.bytecode_loading
4067 }
4068
4069 /// Toggle PUC `.luac` bytecode loading. Default `false` — PUC
4070 /// bytecode is a strictly larger trust surface than luna's own dump
4071 /// format (third-party toolchain bugs, malformed chunks, unknown
4072 /// opcode shapes). Enable only for trusted PUC chunks. Per-dialect
4073 /// translators live in `crate::vm::dump::puc`.
4074 pub fn set_puc_bytecode_loading(&mut self, enabled: bool) {
4075 self.puc_bytecode_loading = enabled;
4076 }
4077
4078 /// Current PUC bytecode-loading gate state.
4079 pub fn puc_bytecode_loading(&self) -> bool {
4080 self.puc_bytecode_loading
4081 }
4082
4083 /// Default loader input budget — 256 MiB.
4084 ///
4085 /// `Vm::load` and the Lua-level `load(reader, ...)` both refuse
4086 /// sources whose byte length crosses this cap, returning the
4087 /// PUC-shaped `not enough memory` error rather than letting the
4088 /// host allocator try (and crash) to hold the next chunk.
4089 pub const DEFAULT_LOADER_INPUT_BUDGET: usize = 256 * 1024 * 1024;
4090
4091 /// Set the loader input byte budget (see
4092 /// [`Vm::DEFAULT_LOADER_INPUT_BUDGET`]). Pass `usize::MAX` to
4093 /// effectively disable. Smaller caps are honored verbatim — a 0
4094 /// cap rejects every non-empty source.
4095 pub fn set_loader_input_budget(&mut self, bytes: usize) {
4096 self.loader_input_budget = bytes;
4097 }
4098
4099 /// Current loader input byte budget.
4100 pub fn loader_input_budget(&self) -> usize {
4101 self.loader_input_budget
4102 }
4103
4104 /// Take the error traceback captured at the latest error point and
4105 /// reset it. Embedders should call this immediately after a failed
4106 /// `call_value`/`eval`/`call`/etc. — the next public `call_value`
4107 /// entry clears it. Returns `None` if no error was in flight.
4108 pub fn take_error_traceback(&mut self) -> Option<String> {
4109 let levels = self.error_traceback.take()?;
4110 let tb = crate::vm::callstack::traceback_from_lines(self.version, &levels, 0);
4111 Some(String::from_utf8_lossy(&tb).into_owned())
4112 }
4113
4114 /// PUC `luaL_traceback(L, L, msg, level)` on the running thread: `msg`
4115 /// (when given) and a newline, then `stack traceback:` and one line per
4116 /// stack level from `level` on, level 0 being the running function (the
4117 /// native calling this, when a native does).
4118 pub fn traceback(&mut self, msg: Option<&[u8]>, level: i64) -> Vec<u8> {
4119 let mut out = match msg {
4120 Some(m) => {
4121 let mut out = m.to_vec();
4122 out.push(b'\n');
4123 out
4124 }
4125 None => Vec::new(),
4126 };
4127 out.extend_from_slice(b"stack traceback:");
4128 out.extend(self.traceback_lines(None, level));
4129 out
4130 }
4131
4132 /// Arm the soft memory cap. The run loop checks the
4133 /// heap's tracked byte usage between dispatch turns; on overshoot it
4134 /// first runs a full collect, and if `bytes` still exceeds the cap it
4135 /// raises a catchable `"memory cap exceeded"` Lua error and disarms
4136 /// itself (fire-once: re-arm before the next `call_value` if reusing
4137 /// the Vm across requests). `None` removes the cap. The accounting is
4138 /// approximate — internal Vec/Box capacity overhead is not tracked,
4139 /// so embedders should size the cap with ~2× margin over the desired
4140 /// hard limit and additionally bound the Vm's lifetime (drop after
4141 /// each request).
4142 pub fn set_memory_cap(&mut self, cap: Option<usize>) {
4143 self.heap.mem_cap = cap;
4144 }
4145
4146 /// Approximate bytes the heap is currently holding. Object shells plus
4147 /// every table's internal array/hash boxes (tracked via
4148 /// `Heap::apply_bytes_delta` in `set`/`rehash`/`ensure_*`). Proto
4149 /// bytecode and closure upvalue slices still go uncounted — this is a
4150 /// lower bound, not a precise `malloc_stats`-style total.
4151 pub fn memory_used(&self) -> usize {
4152 self.heap.bytes()
4153 }
4154
4155 /// Read upvalue slot `i` of the native function currently on top of the
4156 /// dispatch chain (the one whose body is executing). Returns `Value::Nil`
4157 /// when no native is running. Public so the C ABI trampoline can fetch
4158 /// the host C function pointer it stashed there at registration time.
4159 pub fn running_native_upvalue(&self, i: usize) -> Value {
4160 match self.running_natives.last() {
4161 // 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).
4162 Some(nc) => unsafe {
4163 let upvals = &(*nc.as_ptr()).upvals;
4164 upvals.get(i).copied().unwrap_or(Value::Nil)
4165 },
4166 None => Value::Nil,
4167 }
4168 }
4169
4170 /// Register a table for finalization if its (just-set) metatable carries a
4171 /// `__gc` metamethod (PUC luaC_checkfinalizer at setmetatable time — adding
4172 /// `__gc` to the metatable afterwards does not retroactively register).
4173 pub(crate) fn check_finalizer(&mut self, t: Gc<Table>) {
4174 // Tables gained finalizers in 5.2; PUC 5.1 runs `__gc` for userdata only.
4175 if self.version == crate::version::LuaVersion::Lua51 {
4176 return;
4177 }
4178 if !self.get_mm(Value::Table(t), Mm::Gc).is_nil() {
4179 self.heap.register_finalizable(t);
4180 }
4181 }
4182
4183 /// Same as [`Self::check_finalizer`] for a userdata. PUC 5.1 attaches the
4184 /// finalizer to the proxy produced by `newproxy(true)` once its metatable
4185 /// gains `__gc`. gc.lua's "testing userdata" section sets `__gc` on the
4186 /// metatable that `newproxy` returned, which then needs to flow through.
4187 /// Kept available for the future 5.2+ `lua_setmetatable` path (which
4188 /// would re-check at metatable-set time); luna's only userdata
4189 /// finalizables today come via `newproxy`, which registers itself.
4190 #[allow(dead_code)]
4191 pub(crate) fn check_finalizer_userdata(&mut self, u: Gc<crate::runtime::Userdata>) {
4192 if !self.get_mm(Value::Userdata(u), Mm::Gc).is_nil() {
4193 self.heap.register_finalizable_userdata(u);
4194 }
4195 }
4196
4197 /// Run pending `__gc` finalizers (objects the collector resurrected for
4198 /// finalization). Finalizer errors are swallowed — PUC turns them into a
4199 /// warning; they must never propagate to the mutator. Reentrancy-guarded.
4200 fn run_finalizers(&mut self) {
4201 let _ = self.run_finalizers_or_err();
4202 }
4203
4204 fn run_finalizers_or_err(&mut self) -> Result<(), LuaError> {
4205 if self.gc_finalizing {
4206 return Ok(());
4207 }
4208 let pending = self.heap.take_tobefnz();
4209 if pending.is_empty() {
4210 return Ok(());
4211 }
4212 self.gc_finalizing = true;
4213 let mut first_err: Option<LuaError> = None;
4214 for obj in pending {
4215 let gc = self.get_mm(obj, Mm::Gc);
4216 // PUC 5.2+ accepts any non-nil `__gc` at setmetatable time to
4217 // schedule the object for finalization (`__gc = true` is the
4218 // canonical placeholder); only call it at finalize time when it
4219 // is actually a function. gc.lua 5.2 :412 wires up exactly this
4220 // sentinel and then expects no call.
4221 let callable = matches!(gc, Value::Closure(_) | Value::Native(_));
4222 if callable {
4223 // PUC `GCTM` sets `CIST_FIN` on the new ci so
4224 // `funcnamefromfinalizer` reports `namewhat = "metamethod"`,
4225 // `name = "__gc"`. luna threads the same outcome through the
4226 // generic `pending_tm` slot: the Lua frame born from this
4227 // call consumes it in `push_frame`. Saved/restored around the
4228 // call in case the handler is a native (which never pops it).
4229 // Bare event name; `frame_name` / `c_frame_name` add the
4230 // `"__"` debug prefix for 5.2/5.3, drop it for 5.4+. Matches
4231 // the convention used by `__close`, `__index`, …
4232 let saved_tm = self.pending_tm.replace("gc");
4233 // PUC `GCTM` runs the finalizer with `luaD_pcall` and no
4234 // message handler
4235 if let Err(e) = self.call_protected(gc, &[obj]) {
4236 // PUC 5.1 GCTM raised the finalizer's error to the
4237 // explicit `collectgarbage()` caller (`gc.lua 5.1 :255`
4238 // baselines on `not pcall(collectgarbage)`). 5.2/5.3
4239 // wrapped it in `error in __gc metamethod (msg)` first
4240 // (`callGCTM` → `luaG_runerror`) but still raised. 5.4
4241 // introduced the warning system and switched to "warn
4242 // then continue" — never re-raise, just route the
4243 // wrapped message through `warn`. gc.lua 5.5 :378 wires
4244 // up `_WARN` capture under the `if T then …` block to
4245 // baseline on the same wrapped string.
4246 if self.version >= LuaVersion::Lua54 {
4247 let inner = self.error_text(&e);
4248 let msg = format!("error in __gc metamethod ({inner})");
4249 self.emit_warn(msg.as_bytes(), false);
4250 } else if first_err.is_none() {
4251 let wrapped = if self.version >= LuaVersion::Lua52 {
4252 let inner = self.error_text(&e);
4253 let msg = format!("error in __gc metamethod ({inner})");
4254 let s = Value::Str(self.heap.intern(msg.as_bytes()));
4255 LuaError(s)
4256 } else {
4257 e
4258 };
4259 first_err = Some(wrapped);
4260 }
4261 }
4262 self.pending_tm = saved_tm;
4263 }
4264 }
4265 self.gc_finalizing = false;
4266 match first_err {
4267 Some(e) => Err(e),
4268 None => Ok(()),
4269 }
4270 }
4271
4272 /// Drive one incremental GC step (PUC `collectgarbage("step", n)`).
4273 /// Crosses up to three phases per call:
4274 /// 1. Pause → seed Propagate (`gc_start_propagate`)
4275 /// 2. Propagate → drain gray up to `budget`; on exhaustion run atomic
4276 /// (`gc_finish_atomic` → tobefnz populated; finalizers
4277 /// run via `run_finalizers`) and enter Sweep
4278 /// 3. Sweep → `gc_sweep_step` up to (residual) `budget`
4279 /// Returns true when this call completed the cycle's sweep (back to
4280 /// Pause). The budget is spent generously across phases — a large `n`
4281 /// can finish a whole cycle in one call (PUC stop-the-world step).
4282 pub(crate) fn gc_step(&mut self, budget: usize) -> bool {
4283 // Re-entry guard: never recurse — `run_finalizers` calls Lua code
4284 // that may hit a safe point and try to step again. Re-entry was OK
4285 // under STW (collect_garbage had its own guard) but here the
4286 // intermediate phase state would corrupt.
4287 if self.gc_finalizing {
4288 return false;
4289 }
4290 if self.heap.gc_phase_is_pause() {
4291 let (roots, extra) = self.gc_roots();
4292 self.heap.gc_start_propagate(&roots, &extra);
4293 }
4294 if self.heap.gc_phase_is_propagate() {
4295 if !self.heap.gc_step_propagate(budget) {
4296 return false;
4297 }
4298 self.heap.gc_finish_atomic();
4299 // any __gc scheduled by atomic — run before sweep so a finalizer
4300 // re-registering `self` re-enters the next cycle, not this sweep
4301 self.run_finalizers();
4302 }
4303 // either we just transitioned, or we entered already in Sweep, or
4304 // a finalizer started a new cycle (gc_sweep_step is a no-op then)
4305 self.heap.gc_sweep_step(budget)
4306 }
4307
4308 // ---- frames & calls ----
4309
4310 /// Begin calling stack[func_slot] with `nargs` (None: up to self.top).
4311 /// Returns true if a Lua frame was pushed (the dispatch loop continues
4312 /// there), false if a native completed inline.
4313 fn begin_call(
4314 &mut self,
4315 func_slot: u32,
4316 nargs: Option<u32>,
4317 nresults: i32,
4318 from_c: bool,
4319 ) -> Result<bool, LuaError> {
4320 let mut nargs = match nargs {
4321 Some(n) => n,
4322 None => self.top - (func_slot + 1),
4323 };
4324 // Consume `pending_is_tail` at the boundary: a tail-call op sets it
4325 // only for the immediately-following Lua activation. Native dispatch
4326 // (or `__call` resolution) below must not let it leak to the next
4327 // begin_call's frame; restore it just before push_frame for the Lua
4328 // arm so its meaning is preserved across __call chaining.
4329 let tailcalls = std::mem::take(&mut self.pending_tailcalls);
4330 let tail_ccmt = std::mem::take(&mut self.pending_ccmt);
4331 // resolve __call handlers iteratively (PUC tryfuncTM loop): each handler
4332 // is inserted before the value so it becomes the first argument, and a
4333 // chain of `__call` tables resolves down to a real function.
4334 let mut chain = 0u32;
4335 loop {
4336 match self.stack[func_slot as usize] {
4337 Value::Closure(cl) => {
4338 // JIT fast path: if the Proto's body fits
4339 // the int-arith whitelist, every arg is `Value::Int`,
4340 // and the cached arity matches, skip frame setup and
4341 // run the cached native fn in-place.
4342 if self.try_jit_call_op(cl, func_slot, nargs, nresults) {
4343 self.pending_tailcalls = tailcalls;
4344 return Ok(false);
4345 }
4346 self.pending_tailcalls = tailcalls;
4347 self.pending_ccmt = if tailcalls > 0 {
4348 tail_ccmt
4349 } else {
4350 chain as u8
4351 };
4352 self.push_frame(cl, func_slot, nargs, nresults, from_c)?;
4353 // Trace-on-call trigger. The frame
4354 // we just pushed is the callee whose body the
4355 // recorder will trace. Bump the per-Proto call
4356 // counter; once it crosses `CALL_HOT_THRESHOLD`
4357 // and no other trace is in flight, snapshot the
4358 // callee's register window (R[0..max_stack]) and
4359 // begin recording at `pc=0`. This is what unlocks
4360 // tracing for functions whose body has no negative
4361 // `Op::Jmp` back-edge (`fib`, recursive helpers).
4362 //
4363 // Gated on `trace_jit_enabled`, so the default
4364 // dispatch pays a single not-taken branch.
4365 if self.jit.trace_enabled {
4366 let proto = cl.proto;
4367 let c = proto.call_hot_count.get();
4368 if c < u32::MAX / 2 {
4369 proto.call_hot_count.set(c + 1);
4370 }
4371 // Relaxed call-trigger:
4372 // `c >= THRESHOLD` (not `c == THRESHOLD`) +
4373 // `!already_cached` short-circuit. Lets a
4374 // discarded short call-trigger close retry
4375 // on the next call (fib(10/15/20/25)
4376 // pathology — first capture is base-case
4377 // [Lt,Jmp,Return1]; coverage-heuristic
4378 // discards; next call gets to record at a
4379 // potentially deeper recursion point).
4380 // Without `already_cached`, the relaxed
4381 // condition would re-record over a cached
4382 // trace every call.
4383 //
4384 // Additionally short-circuit on
4385 // `proto.trace_gave_up`. The per-Proto discard
4386 // cap force-compiles a partial trace and
4387 // flips this flag; subsequent calls into
4388 // this Proto skip the RefCell borrow + Vec
4389 // scan entirely.
4390 if proto.trace_gave_up.get() {
4391 return Ok(true);
4392 }
4393 let call_already_cached =
4394 proto.traces.borrow().iter().any(|t| t.head_pc == 0)
4395 || trace_head_abandoned(proto, 0);
4396 if c >= self.jit.call_hot_threshold
4397 && self.jit.active_trace.is_none()
4398 && !call_already_cached
4399 {
4400 // The new frame is on top: index in
4401 // `self.frames` is `len() - 1`.
4402 let frame_idx = self.frames.len() - 1;
4403 // Snapshot R[0..max_stack] at the callee's
4404 // base. `push_frame` resized `self.stack`
4405 // to `base + max_stack`, so this window is
4406 // guaranteed in-bounds.
4407 let f = match &self.frames[frame_idx] {
4408 CallFrame::Lua(f) => f,
4409 _ => unreachable!("push_frame just pushed a Lua frame"),
4410 };
4411 let max_stack = cl.proto.max_stack as usize;
4412 let base_us = f.base as usize;
4413 let mut entry_tags = Vec::with_capacity(max_stack);
4414 for i in 0..max_stack {
4415 let (tag, _) = self.stack[base_us + i].unpack();
4416 entry_tags.push(tag);
4417 }
4418 self.jit.active_trace =
4419 Some(Box::new(crate::jit::trace::TraceRecord::start(
4420 cl.proto, 0, entry_tags, true,
4421 )));
4422 self.jit.recording_frame_base = frame_idx;
4423 }
4424 }
4425 return Ok(true);
4426 }
4427 Value::Native(nc) => {
4428 // Async-marked NativeClosure.
4429 // Route through the cooperative-yield mechanism
4430 // when async_mode is on; reject when called from
4431 // a sync `eval`/`call_value` path (would have no
4432 // executor to drive the returned future).
4433 if nc.is_async {
4434 if !self.async_mode {
4435 let s = Value::Str(
4436 self.heap.intern(b"async native called in sync context"),
4437 );
4438 self.last_error_kind = crate::vm::error::LuaErrorKind::Runtime;
4439 return Err(LuaError(s));
4440 }
4441 // Same root-up bookkeeping as the sync path:
4442 // pin args + result-count expectation so a
4443 // collection across the suspend boundary
4444 // keeps the arg window live.
4445 self.native_nresults = nresults;
4446 self.gc_top = func_slot + nargs + 1;
4447 // Fire the "call" hook BEFORE
4448 // building the future. Mirrors the sync native
4449 // path's `hook_call(true, nargs)` site
4450 // (`exec.rs` further down) so embedders with a
4451 // Rust debug hook installed see a Call event
4452 // for async natives identical to the sync
4453 // path. The matching "return" hook fires from
4454 // `commit_async_native_result` in
4455 // `async_drive.rs` after the future resolves.
4456 // Placement: after the `native_nresults` / `gc_top`
4457 // pin, before the future is constructed, so a
4458 // hook body that triggers GC observes the
4459 // correct pinned window. On hook error the
4460 // sentinel never returns and
4461 // `pending_async_native_*` remain `None` —
4462 // the executor sees `DispatchOutcome::Error`.
4463 self.hook_call(true, nargs)?;
4464 // Transmute the stored NativeFn back to its
4465 // real AsyncNativeFn shape. Sound because
4466 // `set_async_native` / `create_async_native`
4467 // installed an AsyncNativeFn through the
4468 // identically-sized fn-pointer slot, and the
4469 // `is_async` marker bit is what records that
4470 // fact.
4471 let async_fn: crate::vm::async_drive::AsyncNativeFn =
4472 // SAFETY: same-size fn pointers; provenance
4473 // preserved through `mem::transmute`. The
4474 // `is_async` marker is the only safe-to-call
4475 // gate, set exclusively by
4476 // `Vm::create_async_native`.
4477 unsafe { std::mem::transmute(nc.f) };
4478 let vm_ptr: *mut Vm = self;
4479 let fut = async_fn(vm_ptr, func_slot, nargs);
4480 // Stash the future + post-call context for
4481 // `drive_one` to surface to `EvalFuture::poll`.
4482 self.pending_async_native_fut = Some(fut);
4483 self.pending_async_native_ctx = Some(AsyncNativeCallCtx {
4484 func_slot,
4485 nargs,
4486 nresults,
4487 gc_top: self.gc_top,
4488 });
4489 // Sentinel Err walked up to `drive_one` (same
4490 // shape as `host_yield_pending`'s budget yield).
4491 // Value::Nil — never seen by user code.
4492 return Err(LuaError(Value::Nil));
4493 }
4494 // pcall/xpcall are yieldable: rather than calling the
4495 // protected function through the Rust stack (which cannot be
4496 // suspended), push a continuation frame and drive the call
4497 // through the interpreter loop (PUC lua_pcallk). A yield
4498 // inside it is preserved with the thread's saved frames.
4499 use crate::runtime::value::NativeFn;
4500 if std::ptr::fn_addr_eq(nc.f, nat_pcall as NativeFn) {
4501 return self.begin_pcall(func_slot, nargs, nresults);
4502 }
4503 if std::ptr::fn_addr_eq(nc.f, nat_xpcall as NativeFn) {
4504 // 5.1 `xpcall(f, err)` calls `f` with no arguments
4505 let forward = self.version > LuaVersion::Lua51;
4506 return self.begin_xpcall(func_slot, nargs, nresults, forward);
4507 }
4508 if std::ptr::fn_addr_eq(nc.f, nat_host_xpcall as NativeFn) {
4509 return self.begin_xpcall(func_slot, nargs, nresults, true);
4510 }
4511 // From 5.4 on, pairs(t) calls a __pairs metamethod yieldably
4512 // (PUC luaB_pairs uses lua_callk). 5.2/5.3 use a plain
4513 // lua_call, and 5.1 has no `__pairs`: the native handles those.
4514 if std::ptr::fn_addr_eq(nc.f, nat_pairs as NativeFn)
4515 && nargs >= 1
4516 && self.version >= LuaVersion::Lua54
4517 {
4518 let arg = self.stack[(func_slot + 1) as usize];
4519 if !self.get_mm(arg, Mm::Pairs).is_nil() {
4520 return self.begin_pairs(func_slot, nresults);
4521 }
4522 }
4523 // a native that collects (e.g. `collectgarbage`) roots up to
4524 // its own arguments — the caller's live registers all sit
4525 // below `func_slot` and stay rooted.
4526 self.native_nresults = nresults;
4527 self.gc_top = func_slot + nargs + 1;
4528 // Push the native onto the running-natives chain BEFORE
4529 // firing the call hook so that `debug.getinfo(level)` and
4530 // `arg_error` from inside the hook see this native as the
4531 // currently-running C function (db.lua :344 reads
4532 // `getinfo(2, "f").func` for the just-entered callee).
4533 // Popped after the matching return hook fires — even on
4534 // error, the pop must happen, so the body is bracketed
4535 // through a scope guard.
4536 self.running_natives.push(nc);
4537 self.running_native_acts
4538 .push(crate::vm::callstack::NativeAct {
4539 func_slot,
4540 nargs,
4541 depth: self.frames.len() as u32,
4542 // a tail call resolved its `__call` chain before
4543 // calling here and passed the count in tail_ccmt
4544 ccmt: tail_ccmt + chain as u8,
4545 });
4546 // PUC C-call discipline: entering a C function sets
4547 // L->top to func + 1 + nargs, so a collect triggered
4548 // INSIDE the native (explicit `collectgarbage()`, or
4549 // an allocation crossing the GC threshold) roots the
4550 // whole caller window up to and including the
4551 // arguments. Without this raise the cursor is stale —
4552 // parked at some earlier, possibly much lower
4553 // safe-point — and the collect frees register-held
4554 // values of the native's own caller (use-after-free).
4555 // Never lower it: a re-entrant chain
4556 // (native → Lua → native) must keep the outermost
4557 // window rooted.
4558 self.gc_top = self.gc_top.max(func_slot + 1 + nargs);
4559 // PUC luaD_precall fires the "call" hook for C functions too.
4560 // A yield inside the native (coroutine.yield) propagates an
4561 // Err and the matching "return" hook fires on resume instead.
4562 if let Err(e) = self.hook_call(true, nargs) {
4563 self.running_natives.pop();
4564 self.running_native_acts.pop();
4565 return Err(e);
4566 }
4567 // Trap a Rust panic in the native and surface it as
4568 // a Lua error rather than letting it unwind through the
4569 // VM into the embedder. The VM's internal state may still
4570 // be inconsistent after a panic (half-pushed args,
4571 // dangling GC references), so embedders that catch this
4572 // class of error should drop and re-create the Vm — but
4573 // it's still better than tearing the host process down.
4574 // `AssertUnwindSafe` is sound because the caller is the
4575 // dispatch loop and any half-done state is fenced behind
4576 // the immediate Err return below.
4577 use std::panic::{AssertUnwindSafe, catch_unwind};
4578 let result =
4579 match catch_unwind(AssertUnwindSafe(|| (nc.f)(self, func_slot, nargs))) {
4580 Ok(r) => r,
4581 Err(payload) => {
4582 let msg = panic_payload_str(&payload);
4583 let s = Value::Str(
4584 self.heap.intern(format!("native panic: {msg}").as_bytes()),
4585 );
4586 Err(LuaError(s))
4587 }
4588 };
4589 let nret = match result {
4590 Ok(n) => n,
4591 Err(e) => {
4592 // PUC raises with the native still on the stack;
4593 // remember it for the handler and traceback of the
4594 // error (see `raise_to_handler`)
4595 let act = self.running_native_acts.pop().expect("pushed above");
4596 self.running_natives.pop();
4597 self.note_errored_native(nc, act, e.0);
4598 return Err(e);
4599 }
4600 };
4601 // PUC `luaD_poscall` fires the return hook BEFORE moving
4602 // results into the function's slot — at that point args
4603 // sit at `[func_slot + 1, func_slot + 1 + nargs)` and
4604 // results above them at `[func_slot + 1 + nargs, …)`.
4605 // luna's `nat_return` has already written the results
4606 // into `[func_slot, func_slot + nret)`, so we replay PUC's
4607 // layout by copying the results up past the preserved
4608 // args, firing the hook (with ftransfer = nargs + 1, so
4609 // `getlocal(2, ftransfer..)` reads results), and then
4610 // copying back for `finish_results`. db.lua :541 reads
4611 // `getinfo("r").ftransfer` + `getlocal` to inspect a
4612 // returning native's results this way.
4613 if self.hook.ret
4614 && !self.in_hook
4615 && (self.hook.func.is_some() || self.hook.rust_func.is_some())
4616 {
4617 let res_dst = func_slot + nargs + 1;
4618 let need = (res_dst + nret) as usize;
4619 if self.stack.len() < need {
4620 self.stack.resize(need, Value::Nil);
4621 }
4622 for i in (0..nret).rev() {
4623 self.stack[(res_dst + i) as usize] =
4624 self.stack[(func_slot + i) as usize];
4625 }
4626 // widen the C-frame's argument window for getlocal
4627 if let Some(act) = self.running_native_acts.last_mut() {
4628 act.nargs = nargs + nret;
4629 }
4630 let hr = self.hook_return(true, nargs + 1, nret);
4631 if let Some(act) = self.running_native_acts.last_mut() {
4632 act.nargs = nargs;
4633 }
4634 // restore results into the slot finish_results expects
4635 for i in 0..nret {
4636 self.stack[(func_slot + i) as usize] =
4637 self.stack[(res_dst + i) as usize];
4638 }
4639 self.running_natives.pop();
4640 self.running_native_acts.pop();
4641 hr?;
4642 } else {
4643 self.running_natives.pop();
4644 self.running_native_acts.pop();
4645 }
4646 self.finish_results(func_slot, nret, nresults);
4647 // the native may have allocated; collect with the results as
4648 // the live boundary (PUC checks GC after a call returns).
4649 self.maybe_collect_garbage(self.top);
4650 return Ok(false);
4651 }
4652 v => {
4653 let mm = self.get_mm(v, Mm::Call);
4654 if mm.is_nil() || self.call_mm_unusable(mm) {
4655 return Err(self.call_err(v));
4656 }
4657 chain += 1;
4658 // PUC 5.5 dropped the chain cap from `MAXTAGRECUR = 200`
4659 // (the value 5.4's `lvm.c` uses) down to `MAXCCMT = 16`,
4660 // and the 5.5 test exercises the new tight bound directly
4661 // (calls.lua :225 builds a 16-deep chain and expects the
4662 // 16th to error). 5.4 calls.lua :194 instead builds a 20-
4663 // deep chain and expects it to succeed.
4664 let cap = if self.version >= crate::version::LuaVersion::Lua55 {
4665 15
4666 } else {
4667 MAX_CCMT
4668 };
4669 if chain > cap {
4670 return Err(self.rt_err("'__call' chain too long"));
4671 }
4672 // slots above shift by one; at a call site those are dead
4673 // temps of the current frame
4674 self.stack.insert(func_slot as usize, mm);
4675 if self.top > func_slot {
4676 self.top += 1;
4677 }
4678 nargs += 1;
4679 }
4680 }
4681 }
4682 }
4683
4684 /// Up to 5.3 `tryfuncTM` takes one `__call` hop and needs a function
4685 /// there; anything else is a call error on the original object. 5.4
4686 /// retries the call with whatever `__call` holds, so chains resolve.
4687 fn call_mm_unusable(&self, mm: Value) -> bool {
4688 self.version <= LuaVersion::Lua53 && !matches!(mm, Value::Closure(_) | Value::Native(_))
4689 }
4690
4691 fn push_frame(
4692 &mut self,
4693 cl: Gc<LuaClosure>,
4694 func_slot: u32,
4695 nargs: u32,
4696 nresults: i32,
4697 from_c: bool,
4698 ) -> Result<(), LuaError> {
4699 if func_slot + 256 > MAX_LUA_STACK {
4700 // PUC `luaD_growstack`: the overflow raises "stack overflow" and
4701 // leaves ERRORSTACKSIZE's extra slots for the xpcall handler that
4702 // runs on it; overflowing those is LUA_ERRERR, "error in error
4703 // handling" (errors.lua :606, cstack.lua :29).
4704 if self.msgh_depth == 0 {
4705 return Err(self.rt_err("stack overflow"));
4706 }
4707 if func_slot + 256 > MAX_LUA_STACK + ERROR_STACK_EXTRA {
4708 return Err(self.plain_err("error in error handling"));
4709 }
4710 }
4711 let proto = cl.proto;
4712 let nparams = proto.num_params as u32;
4713 // 5.5 vararg layout (PUC luaT_adjustvarargs): the extra args stay on the
4714 // stack just below the new `base`, so a named vararg can be indexed
4715 // virtually without allocating a table. Rotate `[p1..pn][e1..em]` to
4716 // `[e1..em][p1..pn]` so the fixed params land at the new base.
4717 let n_varargs = if proto.is_vararg {
4718 nargs.saturating_sub(nparams)
4719 } else {
4720 0
4721 };
4722 if n_varargs > 0 {
4723 let s = (func_slot + 1) as usize;
4724 self.stack[s..s + nargs as usize].rotate_left(nparams as usize);
4725 }
4726 let base = func_slot + 1 + n_varargs;
4727 let need = (base + proto.max_stack as u32) as usize;
4728 if self.stack.len() < need {
4729 self.stack.resize(need, Value::Nil);
4730 }
4731 // wipe the register window beyond the kept parameters (stale values —
4732 // required for GC-safety and codegen). The varargs below `base` survive.
4733 let kept = nargs.saturating_sub(n_varargs).min(nparams);
4734 // SAFETY: just resized above so `need <= stack.len()`; `base + kept <=
4735 // need` since `base + nparams <= base + max_stack = need` and `kept <=
4736 // nparams`. `slice::fill` lowers to a single memset on Copy types.
4737 unsafe {
4738 self.stack
4739 .get_unchecked_mut((base + kept) as usize..need)
4740 .fill(Value::Nil);
4741 }
4742 let ccmt = std::mem::take(&mut self.pending_ccmt);
4743 self.set_frame_ccmt(ccmt);
4744 frames_push_sync(
4745 &mut self.frames,
4746 &mut self.frames_top,
4747 CallFrame::Lua(Frame {
4748 closure: cl,
4749 base,
4750 pc: 0,
4751 func_slot,
4752 nresults,
4753 hook_oldpc: u32::MAX,
4754 from_c,
4755 n_varargs,
4756 // single-shot consume: `close_slots` sets pending_tm before each
4757 // handler call; the next Lua frame born is that handler's.
4758 tm: self.pending_tm.take(),
4759 // `run_hook` sets `pending_is_hook` before dispatching the user
4760 // hook so its frame reports `namewhat = "hook"` via getinfo.
4761 is_hook: std::mem::take(&mut self.pending_is_hook),
4762 tailcalls: std::mem::take(&mut self.pending_tailcalls),
4763 }),
4764 );
4765 // PUC 5.1 `LUAI_COMPAT_VARARG`: populate the hidden `arg` local with
4766 // `{ n = n_varargs, [1] = e1, [2] = e2, … }`. The compiler reserved
4767 // the slot at `base + nparams`; the extras sit just below `base` from
4768 // the vararg rotate above. 5.1 db.lua :279 reads `arg.n` from a line
4769 // hook; vararg.lua's contradictory expectations were already going to
4770 // fail either way (some asserts want `arg == nil`).
4771 if proto.has_compat_vararg_arg {
4772 let arg_slot = (base + nparams) as usize;
4773 let t = self.heap.new_table();
4774 {
4775 // 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).
4776 let tm = unsafe { t.as_mut() };
4777 for i in 0..n_varargs {
4778 let v = self.stack[(base - n_varargs + i) as usize];
4779 // bounded by `n_varargs` (≤ MAXUPVAL territory), well
4780 // below `MAX_ASIZE`
4781 let _ = tm.set_int(&mut self.heap, (i + 1) as i64, v);
4782 }
4783 let nk = Value::Str(self.heap.intern(b"n"));
4784 tm.set(&mut self.heap, nk, Value::Int(n_varargs as i64))
4785 .expect("'n' key");
4786 }
4787 // once-per-table barrier mirrors SETLIST: t is born BLACK during
4788 // Propagate and the bulk `set_int`/`set` calls above don't barrier
4789 self.heap
4790 .barrier_back(t.as_ptr() as *mut crate::runtime::heap::GcHeader);
4791 self.stack[arg_slot] = Value::Table(t);
4792 }
4793 // PUC luaD_precall fires the "call" hook with the new frame current, so
4794 // a hook calling debug.getinfo(2) sees the entered function. For a Lua
4795 // callee, PUC `luaD_hookcall` passes `p->numparams` as ntransfer (only
4796 // fixed params count — extras already live below `base`).
4797 // A frame born via OP_TailCall fires "tail call" instead (PUC
4798 // luaD_pretailcall) and skips the matching "return" hook on exit.
4799 let is_tail = self
4800 .frames
4801 .last()
4802 .and_then(|f| f.lua())
4803 .is_some_and(|f| f.tailcalls > 0);
4804 self.hook_call_with(false, nparams, is_tail)?;
4805 Ok(())
4806 }
4807
4808 /// `pcall(f, ...)` (PUC luaB_pcall): push a continuation frame, then drive
4809 /// the protected call `f` through the interpreter loop. The protected
4810 /// function and its arguments already sit at `func_slot+1..`, so calling `f`
4811 /// at `func_slot+1` lets its results land one slot above the continuation —
4812 /// the loop head then writes `true` at `func_slot` to form `true, results…`.
4813 /// Always returns `Ok(true)`: a continuation is now on the stack to be
4814 /// resolved by the loop (even when `f` is a native that already ran inline).
4815 fn begin_pcall(&mut self, func_slot: u32, nargs: u32, nresults: i32) -> Result<bool, LuaError> {
4816 if nargs == 0 {
4817 // `luaL_checkany` fails here: there is no function to call.
4818 self.with_native_running(func_slot, nargs, |vm| {
4819 let a = crate::vm::argcheck::Args::new(func_slot, nargs);
4820 crate::vm::argcheck::check_any(vm, a, 0).map(drop)
4821 })?;
4822 }
4823 if self.pcall_depth >= MAX_C_DEPTH {
4824 // raised inside pcall, a C function: no position
4825 return Err(self.plain_err("C stack overflow"));
4826 }
4827 self.pcall_depth += 1;
4828 frames_push_sync(
4829 &mut self.frames,
4830 &mut self.frames_top,
4831 CallFrame::Cont(NativeCont {
4832 kind: ContKind::Pcall,
4833 func_slot,
4834 nresults,
4835 }),
4836 );
4837 // call f (slot func_slot+1) with the remaining args, asking for all
4838 // results; a yield or error inside propagates with the continuation kept
4839 // on the stack (caught by `unwind` / preserved across a yield).
4840 self.begin_call(func_slot + 1, Some(nargs - 1), -1, true)?;
4841 Ok(true)
4842 }
4843
4844 /// `xpcall(f, msgh, ...)` (PUC luaB_xpcall): like `begin_pcall`, but the
4845 /// message handler is stashed in the continuation and the arguments are
4846 /// shifted down over the handler's slot so `f`'s args are contiguous.
4847 /// `forward` is false for 5.1's `xpcall`, which passes `f` none of them.
4848 fn begin_xpcall(
4849 &mut self,
4850 func_slot: u32,
4851 nargs: u32,
4852 nresults: i32,
4853 forward: bool,
4854 ) -> Result<bool, LuaError> {
4855 self.with_native_running(func_slot, nargs, |vm| {
4856 let a = crate::vm::argcheck::Args::new(func_slot, nargs);
4857 crate::vm::builtins::xpcall_handler(vm, a).map(drop)
4858 })?;
4859 if self.pcall_depth >= MAX_C_DEPTH {
4860 // raised inside pcall, a C function: no position
4861 return Err(self.plain_err("C stack overflow"));
4862 }
4863 self.pcall_depth += 1;
4864 // layout: [xpcall@func_slot, f@+1, msgh@+2, a1@+3, ...]. Stash msgh and
4865 // close its gap so f's args become [f@+1, a1@+2, ...].
4866 let handler = self.stack[(func_slot + 2) as usize];
4867 // 5.1: `xpcall (f, err)` takes exactly two parameters — extra
4868 // arguments are NOT forwarded to `f` (5.2 added forwarding;
4869 // 5.1 calls f with zero args).
4870 let nfargs = if forward { nargs - 2 } else { 0 };
4871 for i in 0..nfargs {
4872 self.stack[(func_slot + 2 + i) as usize] = self.stack[(func_slot + 3 + i) as usize];
4873 }
4874 self.top = func_slot + 2 + nfargs;
4875 frames_push_sync(
4876 &mut self.frames,
4877 &mut self.frames_top,
4878 CallFrame::Cont(NativeCont {
4879 kind: ContKind::Xpcall { handler },
4880 func_slot,
4881 nresults,
4882 }),
4883 );
4884 self.begin_call(func_slot + 1, Some(nfargs), -1, true)?;
4885 Ok(true)
4886 }
4887
4888 /// `pairs(t)` where `t` has a `__pairs` metamethod (PUC luaB_pairs's
4889 /// lua_callk path): drive `__pairs(t)` through the loop with a `Pairs`
4890 /// continuation so a `coroutine.yield` inside it suspends cleanly. The
4891 /// metamethod is called in `pairs`'s own slot, so its (≤4, nil-padded)
4892 /// results land exactly where `pairs`'s results belong.
4893 /// Run a check of the native at `func_slot` while it counts as the running
4894 /// C function, so an argument error names it the way PUC does. pcall and
4895 /// xpcall check their arguments in the dispatcher, before the native
4896 /// would otherwise be entered.
4897 fn with_native_running(
4898 &mut self,
4899 func_slot: u32,
4900 nargs: u32,
4901 check: impl FnOnce(&mut Vm) -> Result<(), LuaError>,
4902 ) -> Result<(), LuaError> {
4903 let Value::Native(nc) = self.stack[func_slot as usize] else {
4904 unreachable!("pcall/xpcall dispatch sits on a native")
4905 };
4906 self.running_natives.push(nc);
4907 self.running_native_acts
4908 .push(crate::vm::callstack::NativeAct {
4909 func_slot,
4910 nargs,
4911 depth: self.frames.len() as u32,
4912 ccmt: 0,
4913 });
4914 let r = check(self);
4915 self.running_natives.pop();
4916 self.running_native_acts.pop();
4917 r
4918 }
4919
4920 fn begin_pairs(&mut self, func_slot: u32, nresults: i32) -> Result<bool, LuaError> {
4921 let arg = self.stack[(func_slot + 1) as usize];
4922 let mm = self.get_mm(arg, Mm::Pairs);
4923 // layout becomes [pairs@func_slot, mm@func_slot+1, t@func_slot+2]:
4924 // `pairs` keeps its slot so the debug interface can report it as the
4925 // C function running below the metamethod. Call mm(t) wanting 4.
4926 let need = (func_slot + 3) as usize;
4927 if self.stack.len() < need {
4928 self.stack.resize(need, Value::Nil);
4929 }
4930 self.stack[(func_slot + 2) as usize] = arg;
4931 self.stack[(func_slot + 1) as usize] = mm;
4932 self.top = func_slot + 3;
4933 frames_push_sync(
4934 &mut self.frames,
4935 &mut self.frames_top,
4936 CallFrame::Cont(NativeCont {
4937 kind: ContKind::Pairs,
4938 func_slot,
4939 nresults,
4940 }),
4941 );
4942 let want = crate::vm::builtins::pairs_mm_results(self) as i32;
4943 self.begin_call(func_slot + 1, Some(1), want, true)?;
4944 Ok(true)
4945 }
4946
4947 /// The running (top) Lua frame. The interpreter only reads this while a Lua
4948 /// frame is on top — a continuation frame is never the running frame (it is
4949 /// consumed the instant the call it protects unwinds onto it).
4950 #[inline]
4951 /// Records the `__call` count of the Lua frame about to be pushed.
4952 fn set_frame_ccmt(&mut self, ccmt: u8) {
4953 let i = self.frames.len();
4954 if self.frame_ccmt.len() <= i {
4955 self.frame_ccmt.resize(i + 1, 0);
4956 }
4957 self.frame_ccmt[i] = ccmt;
4958 }
4959
4960 fn top_frame(&self) -> &Frame {
4961 self.frames
4962 .last()
4963 .and_then(CallFrame::lua)
4964 .expect("running Lua frame")
4965 }
4966
4967 #[inline]
4968 fn top_frame_mut(&mut self) -> &mut Frame {
4969 self.frames
4970 .last_mut()
4971 .and_then(CallFrame::lua_mut)
4972 .expect("running Lua frame")
4973 }
4974
4975 /// Pad/announce results sitting at func_slot.
4976 pub(crate) fn finish_results(&mut self, func_slot: u32, nret: u32, wanted: i32) {
4977 // Capture the call's high-water-mark before
4978 // setting the new top so we can Nil-clear slots that the
4979 // call temporarily wrote but no longer holds — matching
4980 // PUC's `L->top` discipline (slots past L->top are "free"
4981 // and the next push overwrites them). Without this clear,
4982 // a stale `Value::Closure` (e.g. the called function
4983 // itself, when wanted = 0) sits at `func_slot` and a
4984 // later GC with wider `gc_top` traces it after the
4985 // closure has been freed by a previous narrow safe-point
4986 // GC → heap-buffer-overflow in `Marker::header` (sort.lua
4987 // comparator case).
4988 let prev_top = self.top as usize;
4989 if wanted < 0 {
4990 self.top = func_slot + nret;
4991 } else {
4992 let wanted = wanted as u32;
4993 let need = (func_slot + wanted) as usize;
4994 if self.stack.len() < need {
4995 self.stack.resize(need, Value::Nil);
4996 }
4997 for i in nret..wanted {
4998 self.stack[(func_slot + i) as usize] = Value::Nil;
4999 }
5000 self.top = func_slot + wanted;
5001 }
5002 let new_top = self.top as usize;
5003 let clear_end = prev_top.min(self.stack.len());
5004 if new_top < clear_end {
5005 for slot in &mut self.stack[new_top..clear_end] {
5006 *slot = Value::Nil;
5007 }
5008 }
5009 }
5010
5011 /// Current Lua call-frame depth (read-only).
5012 /// Used by `EvalFuture` on the bootstrap poll to compute the
5013 /// `entry_depth` it will pass to subsequent resume slices.
5014 pub(crate) fn frame_count(&self) -> usize {
5015 self.frames.len()
5016 }
5017
5018 fn take_results(&mut self, func_slot: u32) -> Vec<Value> {
5019 let nret = self.top - func_slot;
5020 let out = self.stack[func_slot as usize..(func_slot + nret) as usize].to_vec();
5021 self.stack.truncate(func_slot as usize);
5022 self.top = func_slot;
5023 out
5024 }
5025
5026 // ---- open upvalues ----
5027
5028 #[doc(hidden)]
5029 pub fn find_or_create_upval(&mut self, slot: u32) -> Gc<Upvalue> {
5030 match self.open_upvals.binary_search_by_key(&slot, |&(s, _)| s) {
5031 Ok(i) => self.open_upvals[i].1,
5032 Err(i) => {
5033 let uv = self.heap.new_upvalue(UpvalState::Open {
5034 slot,
5035 thread: self.current,
5036 });
5037 self.open_upvals.insert(i, (slot, uv));
5038 uv
5039 }
5040 }
5041 }
5042
5043 pub(crate) fn close_from(&mut self, slot: u32) {
5044 while let Some(&(s, uv)) = self.open_upvals.last() {
5045 if s < slot {
5046 break;
5047 }
5048 let v = self.stack[s as usize];
5049 // 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).
5050 unsafe { uv.as_mut() }.set_closed(v);
5051 self.heap
5052 .barrier_forward(uv.as_ptr() as *mut crate::runtime::heap::GcHeader, v);
5053 self.open_upvals.pop();
5054 }
5055 }
5056
5057 /// Register a to-be-closed slot (TBC op / generic-for closing value).
5058 fn register_tbc(&mut self, slot: u32) -> Result<(), LuaError> {
5059 let v = self.stack[slot as usize];
5060 if matches!(v, Value::Nil | Value::Bool(false)) {
5061 return Ok(()); // nil and false are silently ignored
5062 }
5063 if self.get_mm(v, Mm::Close).is_nil() {
5064 // PUC `checkclosemth`: "variable '<name>' got a non-closable
5065 // value", the name as `luaG_findlocal` gives it — the frame's
5066 // locvars at this pc, else "(temporary)".
5067 let f = self.top_frame();
5068 let reg = slot - f.base;
5069 let pc = (f.pc as usize).saturating_sub(1);
5070 let name = crate::vm::objname::getlocalname(&f.closure.proto, reg, pc)
5071 .unwrap_or("(temporary)");
5072 return Err(self.rt_err(&format!("variable '{name}' got a non-closable value")));
5073 }
5074 // compiled code registers in register order and closes before it
5075 // registers a slot again; only a crafted chunk (`TBC R0; TBC R0`)
5076 // breaks that, which PUC's list cannot represent either
5077 if self.tbc.last().is_some_and(|&s| s >= slot) {
5078 return Err(self.rt_err("'<close>' state corrupted"));
5079 }
5080 self.tbc.push(slot);
5081 Ok(())
5082 }
5083
5084 /// Close upvalues and run `__close` handlers for slots ≥ `from`
5085 /// (handlers in reverse registration order; PUC luaF_close).
5086 fn close_slots(&mut self, from: u32, err: Option<Value>) -> Result<(), LuaError> {
5087 self.close_from(from);
5088 // PUC: handlers run in reverse declaration order; an error raised by a
5089 // handler becomes the error object passed to the remaining ones, and
5090 // the rest are still closed. The last raised error propagates.
5091 let mut pending = err;
5092 let mut result = Ok(());
5093 let saved_err = self.closing_err;
5094 // On a normal close the handler runs within the closing function's
5095 // activation (debug parent = that function); during error unwinding the
5096 // function's frame is already gone, so the handler sits at the C
5097 // boundary instead (PUC: luaF_close runs after the ci is restored).
5098 let error_close = err.is_some();
5099 while let Some(&s) = self.tbc.last() {
5100 if s < from {
5101 break;
5102 }
5103 self.tbc.pop();
5104 let v = self.stack[s as usize];
5105 if matches!(v, Value::Nil | Value::Bool(false)) {
5106 continue;
5107 }
5108 let mm = self.get_mm(v, Mm::Close);
5109 if mm.is_nil() {
5110 // PUC `prepclosingmethod`: the __close metamethod was present
5111 // at OP_TBC (else we would have errored there) but has since
5112 // been removed/replaced. Treat as a non-callable target.
5113 let tn = self.obj_typename(v);
5114 let e = self.rt_err(&format!(
5115 "attempt to call a {tn} value (metamethod 'close')"
5116 ));
5117 pending = Some(e.0);
5118 result = Err(e);
5119 continue;
5120 }
5121 // root the pending error: a handler may trigger a collection
5122 self.closing_err = pending;
5123 // PUC `luaF_close` sets `ci->u.l.tm = TM_CLOSE` so traceback /
5124 // getinfo report the handler as "in metamethod 'close'". Saved/
5125 // restored around the call to cover the path where `mm` is a
5126 // native (`push_frame` never consumes it) or it raises before
5127 // reaching push_frame.
5128 let saved_tm = self.pending_tm.replace("close");
5129 // PUC 5.4 `prepclosingmethod` always pushed (obj, errobj) — errobj
5130 // is nil on a normal close (5.4 locals.lua :875's
5131 // `func2close(coroutine.yield)` wrap pins `(self, nil)` back
5132 // through the yield). PUC 5.5 dropped the trailing nil: a clean
5133 // close passes only `obj`, the error case still passes both
5134 // (5.5 locals.lua :314 `select("#", ...) == n` with n=1 for the
5135 // normal-close arms, n=2 for the error arm).
5136 let call = match pending {
5137 Some(e) => self.call_value_impl(mm, &[v, e], error_close),
5138 None => {
5139 if self.version >= LuaVersion::Lua55 {
5140 self.call_value_impl(mm, &[v], error_close)
5141 } else {
5142 self.call_value_impl(mm, &[v, Value::Nil], error_close)
5143 }
5144 }
5145 };
5146 self.pending_tm = saved_tm;
5147 if let Err(e) = call {
5148 pending = Some(e.0);
5149 result = Err(e);
5150 }
5151 }
5152 self.closing_err = saved_err;
5153 result
5154 }
5155
5156 /// Yieldable variant of `close_slots`: drive the chain of `__close`
5157 /// handlers for slots ≥ `from` through the interpreter loop with a
5158 /// `Cont::Close` continuation, so a `coroutine.yield()` inside any handler
5159 /// suspends cleanly (the close iteration's state rides on the thread's
5160 /// frame/stack like any other suspended call) — PUC's `lua_callk` pattern
5161 /// applied to `luaF_close`. `after` runs when every slot is closed; if
5162 /// `after` is `Return` and we've returned past `entry_depth`,
5163 /// `Ok(Some(vals))` carries the result up to the host caller.
5164 fn begin_close(
5165 &mut self,
5166 from: u32,
5167 err: Option<Value>,
5168 after: AfterClose,
5169 entry_depth: usize,
5170 ) -> Result<Option<Vec<Value>>, LuaError> {
5171 self.close_from(from);
5172 self.drive_close(from, err, after, entry_depth)
5173 }
5174
5175 /// Pop tbc slots ≥ `from`, skipping nil/false and synthesising a
5176 /// non-callable-mm error for an `__close` that was reset to a bad value
5177 /// between OP_TBC and now (PUC `prepclosingmethod`). The first real
5178 /// handler pushes a `Cont::Close` + `begin_call` and returns `Ok(None)`;
5179 /// the interpreter then drives the handler and re-enters this driver via
5180 /// the `Cont::Close` consumer in `run()`. When the chain is exhausted,
5181 /// the threaded error (if any) propagates or `after` fires.
5182 fn drive_close(
5183 &mut self,
5184 from: u32,
5185 mut pending: Option<Value>,
5186 after: AfterClose,
5187 entry_depth: usize,
5188 ) -> Result<Option<Vec<Value>>, LuaError> {
5189 loop {
5190 let drained = match self.tbc.last() {
5191 None => true,
5192 Some(&s) => s < from,
5193 };
5194 if drained {
5195 return self.finish_close_after(after, pending, entry_depth);
5196 }
5197 let s = self.tbc.pop().expect("tbc non-empty");
5198 let v = self.stack[s as usize];
5199 if matches!(v, Value::Nil | Value::Bool(false)) {
5200 continue;
5201 }
5202 let mm = self.get_mm(v, Mm::Close);
5203 if mm.is_nil() {
5204 let tn = self.obj_typename(v);
5205 let e = self.rt_err(&format!(
5206 "attempt to call a {tn} value (metamethod 'close')"
5207 ));
5208 pending = Some(e.0);
5209 continue;
5210 }
5211 // A real handler: stage [mm, v, (err?)] above the current top,
5212 // record the close iteration state in a Cont::Close, and let the
5213 // interpreter dispatch the handler. On return the run() head
5214 // re-enters this driver via the Cont::Close consumer.
5215 let func_slot = self.top;
5216 let error_close = pending.is_some();
5217 let need = (func_slot + 3) as usize;
5218 if self.stack.len() < need {
5219 self.stack.resize(need, Value::Nil);
5220 }
5221 self.stack[func_slot as usize] = mm;
5222 self.stack[func_slot as usize + 1] = v;
5223 // PUC 5.4 always passes (obj, errobj=nil) on a normal close;
5224 // 5.5 drops the trailing nil. 5.4 locals.lua :875 vs 5.5 :314.
5225 let nargs = match pending {
5226 Some(e) => {
5227 self.stack[func_slot as usize + 2] = e;
5228 2u32
5229 }
5230 None => {
5231 if self.version >= LuaVersion::Lua55 {
5232 1u32
5233 } else {
5234 self.stack[func_slot as usize + 2] = Value::Nil;
5235 2u32
5236 }
5237 }
5238 };
5239 self.top = func_slot + 1 + nargs;
5240 // Root the pending error during the call (a handler may collect).
5241 let saved_err = self.closing_err;
5242 self.closing_err = pending;
5243 // PUC `luaF_close` flags the handler frame as "metamethod 'close'"
5244 // for traceback / getinfo.
5245 let saved_tm = self.pending_tm.replace("close");
5246 frames_push_sync(
5247 &mut self.frames,
5248 &mut self.frames_top,
5249 CallFrame::Cont(NativeCont {
5250 kind: ContKind::Close(CloseCont {
5251 from,
5252 pending,
5253 after,
5254 }),
5255 func_slot,
5256 nresults: 0,
5257 }),
5258 );
5259 // PUC luaF_close runs a normal close *within* the closing
5260 // function's activation (debug parent = that function); during an
5261 // error unwind the function's frame is already gone and the
5262 // handler sits at the C boundary instead.
5263 let r = self.begin_call(func_slot, Some(nargs), 0, error_close);
5264 self.pending_tm = saved_tm;
5265 self.closing_err = saved_err;
5266 r?;
5267 return Ok(None);
5268 }
5269 }
5270
5271 /// Fire `after` once every `__close` handler has run. `Block` propagates
5272 /// any remaining error or simply continues; `Return` performs OP_Return's
5273 /// tail (hook + frame pop + result delivery) and may surface results to
5274 /// the host when the function whose return triggered the close was the
5275 /// entry activation, but only on a clean drain — a pending error skips
5276 /// the return tail and propagates instead. `ResumeUnwind` pops the
5277 /// deferred Lua frame and re-raises, letting a handler's own error win
5278 /// over the original propagating one (PUC luaF_close).
5279 fn finish_close_after(
5280 &mut self,
5281 after: AfterClose,
5282 pending: Option<Value>,
5283 entry_depth: usize,
5284 ) -> Result<Option<Vec<Value>>, LuaError> {
5285 match after {
5286 AfterClose::Block => match pending {
5287 Some(e) => Err(LuaError(e)),
5288 None => Ok(None),
5289 },
5290 AfterClose::Return {
5291 abs_a,
5292 nret,
5293 from_native,
5294 } => match pending {
5295 Some(e) => Err(LuaError(e)),
5296 None => self.complete_return(abs_a, nret, from_native, entry_depth),
5297 },
5298 AfterClose::ResumeUnwind { func_slot, err } => {
5299 // The aborting Lua frame was popped before `begin_close`;
5300 // restore the catcher's stack window down to `func_slot` and
5301 // re-raise — preferring a handler-raised error over the
5302 // original (PUC luaF_close).
5303 self.stack.truncate(func_slot as usize);
5304 self.top = func_slot;
5305 self.tbc.retain(|&s| s < func_slot);
5306 Err(LuaError(pending.unwrap_or(err)))
5307 }
5308 }
5309 }
5310
5311 /// OP_Return's post-close tail: fire the "return" hook (frame still
5312 /// current), pop the Lua frame, slide results into `func_slot`, then
5313 /// either hand them to the host (`Ok(Some(vals))` when we've returned
5314 /// past `entry_depth`), leave them contiguous for an exposed
5315 /// pcall/xpcall continuation, or finish into the caller's expected
5316 /// result slot. Mirrors the synchronous OP_Return tail so both paths
5317 /// share semantics — the `from_native` flag selects the right "return"
5318 /// hook context for `hook_return`.
5319 fn complete_return(
5320 &mut self,
5321 abs_a: u32,
5322 nret: u32,
5323 from_native: bool,
5324 entry_depth: usize,
5325 ) -> Result<Option<Vec<Value>>, LuaError> {
5326 // ftransfer is the local index (1-based) of the first result, as
5327 // `getinfo("r").ftransfer + getlocal(level, k)` consumes it. luna
5328 // exposes locals starting at `frame.base` (= func_slot + 1 +
5329 // n_varargs for a vararg call), so the conversion is the absolute
5330 // result slot minus base, plus one to make it 1-based. db.lua 5.4
5331 // :542 (`foo1(); on=false; eqseq(out, {10, 0})`) pins the vararg
5332 // shape end-to-end.
5333 let ftransfer = self
5334 .frames
5335 .last()
5336 .and_then(CallFrame::lua)
5337 .map(|fr| {
5338 let raw = abs_a.saturating_sub(fr.base) + 1;
5339 // 5.5 anonymous-vararg functions get a `(vararg table)` pseudo
5340 // local injected at index `numparams + 1`, so getlocal
5341 // numbering shifts results past it (5.5 db.lua :539
5342 // `eqseq(out, {10, 0})`). 5.4 and earlier have no such pseudo.
5343 if fr.closure.proto.has_vararg_table_pseudo {
5344 raw + 1
5345 } else {
5346 raw
5347 }
5348 })
5349 .unwrap_or(1);
5350 // PUC 5.1 `luaD_poscall`: fire one extra "tail return" hook event
5351 // per tail call that collapsed into this activation, *after* its
5352 // own "return". `tailcalls` tracks that count exactly (PUC
5353 // `ci->u.l.tailcalls`). 5.2+ retired LUA_HOOKTAILRET, so the
5354 // "return" hook fires once even when the activation absorbed
5355 // multiple tail calls — only `istailcall` on getinfo surfaces the
5356 // collapse. 5.1 db.lua :366 pins the event ordering.
5357 let tailcalls = if self.version <= LuaVersion::Lua51 {
5358 self.frames
5359 .last()
5360 .and_then(|f| f.lua())
5361 .map(|f| f.tailcalls)
5362 .unwrap_or(0)
5363 } else {
5364 0
5365 };
5366 self.hook_return(from_native, ftransfer, nret)?;
5367 for _ in 0..tailcalls {
5368 self.hook_tail_return()?;
5369 }
5370 let CallFrame::Lua(fr) =
5371 frames_pop_sync(&mut self.frames, &mut self.frames_top).expect("no frame")
5372 else {
5373 unreachable!("returning from a non-Lua frame")
5374 };
5375 for i in 0..nret {
5376 self.stack[(fr.func_slot + i) as usize] = self.stack[(abs_a + i) as usize];
5377 }
5378 if self.frames.len() < entry_depth {
5379 self.top = fr.func_slot + nret;
5380 return Ok(Some(self.take_results(fr.func_slot)));
5381 } else if matches!(self.frames.last(), Some(CallFrame::Cont(_))) {
5382 self.top = fr.func_slot + nret;
5383 } else {
5384 self.finish_results(fr.func_slot, nret, fr.nresults);
5385 }
5386 Ok(None)
5387 }
5388
5389 #[doc(hidden)]
5390 pub fn upval_get(&self, cl: Gc<LuaClosure>, idx: u32) -> Value {
5391 match cl.upvals()[idx as usize].state() {
5392 UpvalState::Open { slot, thread } => self.read_slot(slot, thread),
5393 UpvalState::Closed(v) => v,
5394 }
5395 }
5396
5397 fn upval_set(&mut self, cl: Gc<LuaClosure>, idx: u32, v: Value) {
5398 let uv = cl.upvals()[idx as usize];
5399 match uv.state() {
5400 UpvalState::Open { slot, thread } => self.write_slot(slot, thread, v),
5401 UpvalState::Closed(_) => {
5402 // 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).
5403 unsafe { uv.as_mut() }.set_closed(v);
5404 // forward barrier: a closed upvalue is single-slot, so the
5405 // forward variant is cheaper than barrier_back (PUC uses
5406 // `luaC_barrier_` for upvalues; `luaC_barrierback_` for
5407 // tables / threads).
5408 self.heap
5409 .barrier_forward(uv.as_ptr() as *mut crate::runtime::heap::GcHeader, v);
5410 }
5411 }
5412 }
5413
5414 // ---- register / error helpers ----
5415
5416 #[inline(always)]
5417 fn r(&self, base: u32, i: u32) -> Value {
5418 // SAFETY: the compiler reserves `proto.max_stack` slots above `base`
5419 // at frame entry (`push_frame` sizes the stack up to base + max_stack),
5420 // and every bytecode-generated reference falls within `[0, max_stack)`.
5421 // PUC's vmfetch uses raw `R(A)` (`s2v(L->base + A)`) for the same
5422 // reason. The bounds check would re-validate this invariant on every
5423 // op — the dispatch hot path can't afford it.
5424 // 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).
5425 unsafe { *self.stack.get_unchecked((base + i) as usize) }
5426 }
5427
5428 #[inline(always)]
5429 fn set_r(&mut self, base: u32, i: u32, v: Value) {
5430 // SAFETY: see `r` — `base + i < base + max_stack <= stack.len()` by
5431 // frame-entry contract.
5432 unsafe {
5433 *self.stack.get_unchecked_mut((base + i) as usize) = v;
5434 }
5435 }
5436
5437 #[doc(hidden)]
5438 pub fn rt_err(&mut self, msg: &str) -> LuaError {
5439 let text = match self.position_prefix() {
5440 Some(p) => format!("{p}{msg}"),
5441 None => msg.to_string(),
5442 };
5443 LuaError(Value::Str(self.heap.intern(text.as_bytes())))
5444 }
5445
5446 /// Error without the `chunk:line:` position prefix. PUC's
5447 /// `resume_error` (ldo.c) pushes its message as a bare literal,
5448 /// so `cannot resume dead coroutine` etc. must not be prefixed.
5449 pub(crate) fn plain_err(&mut self, msg: &str) -> LuaError {
5450 LuaError(Value::Str(self.heap.intern(msg.as_bytes())))
5451 }
5452
5453 /// A string a library built from pieces of any size: one longer than a
5454 /// string can hold raises, as the concatenation operator does.
5455 pub(crate) fn built_str(&mut self, bytes: &[u8]) -> Result<Value, LuaError> {
5456 if bytes.len() > crate::runtime::string::MAX_LEN {
5457 return Err(self.rt_err("string length overflow"));
5458 }
5459 Ok(Value::Str(self.heap.intern(bytes)))
5460 }
5461
5462 pub(crate) fn type_err(&mut self, what: &str, v: Value) -> LuaError {
5463 let extra = self.subject_varinfo(v);
5464 let tn = self.obj_typename(v);
5465 let msg = self.compose_type_err(what, &tn, &extra);
5466 self.runerror(&msg)
5467 }
5468
5469 /// Assemble a `luaG_typeerror` / `luaG_callerror` message in the dialect's
5470 /// word order.
5471 ///
5472 /// PUC ≤5.2 names the operand first — `attempt to call field 'f' (a nil
5473 /// value)`. 5.3 flipped it to type-first — `attempt to call a nil value
5474 /// (field 'f')`. luna emitted the 5.3+ form on every dialect, so every
5475 /// such error was worded wrong under 5.1/5.2.
5476 ///
5477 /// Two shapes carry no operand name on ≤5.2 and must collapse to the bare
5478 /// message: an absent varinfo (identical across dialects), and a
5479 /// metamethod target — ≤5.2's `luaG_typeerror` only names locals, globals,
5480 /// fields, upvalues and methods, so `(metamethod 'add')` has no ≤5.2
5481 /// counterpart and is dropped rather than reworded. All four shapes were
5482 /// measured against stock 5.1.5 / 5.2.4 / 5.5.1 before this was written.
5483 fn compose_type_err(&self, what: &str, tn: &str, extra: &str) -> String {
5484 if self.version() > crate::version::LuaVersion::Lua52 {
5485 return format!("attempt to {what} a {tn} value{extra}");
5486 }
5487 // `extra` is "" or " (kind 'name')" — unwrap to "kind 'name'".
5488 let inner = extra
5489 .trim_start()
5490 .trim_start_matches('(')
5491 .trim_end_matches(')');
5492 if inner.is_empty() || inner.starts_with("metamethod") {
5493 format!("attempt to {what} a {tn} value")
5494 } else {
5495 format!("attempt to {what} {inner} (a {tn} value)")
5496 }
5497 }
5498
5499 /// Name the offending operand of the current instruction (PUC varinfo) for
5500 /// a type error, e.g. " (global 'x')". The faulting value `bad` is matched
5501 /// to the instruction's subject register(s); a native-raised error whose
5502 /// current instruction doesn't hold `bad` simply yields "".
5503 fn subject_varinfo(&self, bad: Value) -> String {
5504 use crate::vm::isa::Op;
5505 // PUC `varinfo` names a variable only for a Lua activation
5506 if self.native_on_top() {
5507 return String::new();
5508 }
5509 let Some(f) = self.frames.last().and_then(CallFrame::lua) else {
5510 return String::new();
5511 };
5512 let proto = f.closure.proto;
5513 let p: &crate::runtime::Proto = &proto;
5514 let pc = f.pc as usize;
5515 if pc == 0 || pc > p.code.len() {
5516 return String::new();
5517 }
5518 let instr = p.code[pc - 1];
5519 let mut cands: Vec<u32> = Vec::new();
5520 match instr.op() {
5521 // indexed reads / length / method: the table/object is in B
5522 Op::GetField | Op::GetI | Op::GetTable | Op::SelfOp | Op::Len => {
5523 cands.push(instr.b());
5524 }
5525 // indexed writes / calls: the table/function is in A
5526 Op::SetField | Op::SetI | Op::SetTable | Op::Call | Op::TailCall => {
5527 cands.push(instr.a());
5528 }
5529 // arithmetic/bitwise: a register operand (B, and C unless constant)
5530 Op::Add
5531 | Op::Sub
5532 | Op::Mul
5533 | Op::Div
5534 | Op::Mod
5535 | Op::Pow
5536 | Op::IDiv
5537 | Op::BAnd
5538 | Op::BOr
5539 | Op::BXor
5540 | Op::Shl
5541 | Op::Shr => {
5542 cands.push(instr.b());
5543 if !instr.k() {
5544 cands.push(instr.c());
5545 }
5546 }
5547 Op::Unm | Op::BNot => cands.push(instr.b()),
5548 // indexing an upvalue table (`_ENV` for a global): PUC
5549 // `getupvalname` finds the value among the closure's upvalues
5550 Op::GetTabUp | Op::SetTabUp => {
5551 let u = if instr.op() == Op::GetTabUp {
5552 instr.b()
5553 } else {
5554 instr.a()
5555 };
5556 if self.upval_get(f.closure, u).raw_eq(bad)
5557 && let Some(d) = p.upvals.get(u as usize)
5558 {
5559 return format!(" (upvalue '{}')", d.name);
5560 }
5561 }
5562 Op::Concat => {
5563 let a = instr.a();
5564 for r in a..a + instr.b() {
5565 cands.push(r);
5566 }
5567 }
5568 _ => {}
5569 }
5570 // Up to 5.3 a binary operator takes a constant operand straight
5571 // from the constant table (RK), where `varinfo` cannot see it, so a
5572 // string constant is not named there; unary operators load it into
5573 // a register first and do name it.
5574 let rk_operands = self.version <= LuaVersion::Lua53
5575 && matches!(
5576 instr.op(),
5577 Op::Add
5578 | Op::Sub
5579 | Op::Mul
5580 | Op::Div
5581 | Op::Mod
5582 | Op::Pow
5583 | Op::IDiv
5584 | Op::BAnd
5585 | Op::BOr
5586 | Op::BXor
5587 | Op::Shl
5588 | Op::Shr
5589 );
5590 for reg in cands {
5591 if self.r(f.base, reg).raw_eq(bad) {
5592 return match crate::vm::objname::getobjname_in(p, pc - 1, reg, self.version) {
5593 Some(("constant", _)) if rk_operands => String::new(),
5594 Some((kind, name)) => format!(" ({kind} '{name}')"),
5595 None => String::new(),
5596 };
5597 }
5598 }
5599 String::new()
5600 }
5601
5602 /// "attempt to call a X value", enriched (PUC luaG_callerror) with a name
5603 /// for the call target: "(global 'f')" for a direct call, or "(metamethod
5604 /// 'add')" when the call is a metamethod dispatched by the current opcode.
5605 fn call_err(&mut self, v: Value) -> LuaError {
5606 let extra = self.call_target_varinfo(v);
5607 let tn = self.obj_typename(v);
5608 let msg = self.compose_type_err("call", &tn, &extra);
5609 self.runerror(&msg)
5610 }
5611
5612 /// Name the offending call target. A metamethod dispatch pushes a `Cont`
5613 /// frame before the call, so the opcode that triggered it lives in the
5614 /// nearest *Lua* frame — read that instruction: OP_CALL names the function
5615 /// register, any metamethod-bearing opcode yields "(metamethod 'event')".
5616 fn call_target_varinfo(&self, bad: Value) -> String {
5617 use crate::vm::isa::Op;
5618 if self.native_on_top() {
5619 return String::new();
5620 }
5621 let Some(f) = self.frames.iter().rev().find_map(CallFrame::lua) else {
5622 return String::new();
5623 };
5624 let proto = f.closure.proto;
5625 let p: &crate::runtime::Proto = &proto;
5626 let pc = f.pc as usize;
5627 if pc == 0 || pc > p.code.len() {
5628 return String::new();
5629 }
5630 let instr = p.code[pc - 1];
5631 match instr.source_op() {
5632 Op::Call | Op::TailCall => {
5633 let reg = instr.a();
5634 if self.r(f.base, reg).raw_eq(bad) {
5635 match crate::vm::objname::getobjname_in(p, pc - 1, reg, self.version) {
5636 Some((kind, name)) => format!(" ({kind} '{name}')"),
5637 None => String::new(),
5638 }
5639 } else {
5640 String::new()
5641 }
5642 }
5643 // 5.4 `funcnamefromcode` names the generic-for iterator call
5644 // (5.3 had the entry but raised through plain `luaG_typeerror`)
5645 Op::TForCall if self.version >= LuaVersion::Lua54 => {
5646 " (for iterator 'for iterator')".to_string()
5647 }
5648 // 5.4 `funcnamefromcall` names the metamethod; up to 5.3 the
5649 // call raised through `luaG_typeerror`, whose `varinfo` does not
5650 op if self.version >= LuaVersion::Lua54 => match mm_event_name(op) {
5651 Some(ev) => format!(" (metamethod '{ev}')"),
5652 None => String::new(),
5653 },
5654 _ => String::new(),
5655 }
5656 }
5657
5658 /// "number has no integer representation", enriched (PUC luaG_tointerror)
5659 /// with a "(field 'x')"-style suffix naming the offending operand of the
5660 /// current arithmetic instruction when it can be recovered from bytecode.
5661 fn no_int_rep_err(&mut self) -> LuaError {
5662 let extra = self.bad_operand_varinfo();
5663 self.runerror(&format!("number{extra} has no integer representation"))
5664 }
5665
5666 /// Inspect the current frame's faulting instruction: find the register
5667 /// operand holding a float with no integer representation and name it.
5668 fn bad_operand_varinfo(&self) -> String {
5669 if self.native_on_top() {
5670 return String::new();
5671 }
5672 let Some(f) = self.frames.last().and_then(CallFrame::lua) else {
5673 return String::new();
5674 };
5675 let proto = f.closure.proto;
5676 let p: &crate::runtime::Proto = &proto;
5677 let pc = f.pc as usize;
5678 if pc == 0 || pc > p.code.len() {
5679 return String::new();
5680 }
5681 let instr = p.code[pc - 1];
5682 let mut regs = vec![instr.b()];
5683 if !instr.k() {
5684 regs.push(instr.c());
5685 }
5686 let no_int = |n: Option<Num>| matches!(n, Some(Num::Float(x)) if crate::runtime::value::f2i_exact(x).is_none());
5687 for reg in regs {
5688 let v = self.r(f.base, reg);
5689 // before 5.4 a numeric string is converted first, so "2.5" is
5690 // the operand without an integer value
5691 let n = self.arith_operand()(v);
5692 if no_int(n) {
5693 return match crate::vm::objname::getobjname_in(p, pc - 1, reg, self.version) {
5694 Some((kind, name)) => format!(" ({kind} '{name}')"),
5695 None => String::new(),
5696 };
5697 }
5698 }
5699 String::new()
5700 }
5701
5702 /// Position prefix of the currently executing Lua frame. PUC `luaL_error`
5703 /// calls `luaL_where(L, 1)` which reads `L->ci->previous`. When the prior
5704 /// frame is a C function (e.g. a pcall Cont parked above `require`'s
5705 /// native call), PUC pushes no prefix — match that by looking only at the
5706 /// topmost frame directly and bailing if it is anything but a Lua frame.
5707 pub(crate) fn position_prefix(&self) -> Option<String> {
5708 let f = match self.frames.last()? {
5709 CallFrame::Lua(f) => f,
5710 // a native metamethod runs above the Meta continuation of the
5711 // instruction that triggered it: that Lua function is its caller
5712 CallFrame::Cont(NativeCont {
5713 kind: ContKind::Meta(_),
5714 ..
5715 }) => self.frames.iter().rev().nth(1)?.lua()?,
5716 CallFrame::Cont(_) => return None,
5717 };
5718 let proto = f.closure.proto;
5719 // a stripped chunk: no source in luna's own format, no line info in
5720 // PUC's (whose loader names the missing source "=?")
5721 if proto.source.as_bytes().is_empty() || proto.lines.is_empty() {
5722 return Some(self.stripped_prefix());
5723 }
5724 let line = proto.lines[(f.pc as usize).saturating_sub(1).min(proto.lines.len() - 1)];
5725 // 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).
5726 let raw = unsafe { crate::runtime::string::bytes_of(proto.source.as_ptr()) };
5727 let display = crate::vm::lib_debug::chunk_id(self.version, raw);
5728 let src = String::from_utf8_lossy(&display).into_owned();
5729 Some(format!("{src}:{line}: "))
5730 }
5731
5732 /// PUC `luaG_addinfo` prefix for a stripped chunk. 5.5 substitutes "=?"
5733 /// for the source and renders the line as "?" (so the prefix reads
5734 /// `?:?: `). 5.4 and below leave the source NULL ("?") and use the raw
5735 /// `getfuncline = -1`, so the prefix reads `?:-1: ` (5.4 errors.lua :282
5736 /// matches `^%?:%-1:`).
5737 fn stripped_prefix(&self) -> String {
5738 if self.version >= crate::version::LuaVersion::Lua55 {
5739 "?:?: ".to_string()
5740 } else {
5741 "?:-1: ".to_string()
5742 }
5743 }
5744
5745 /// PUC `luaL_where(L, level)`: `"short_src:line: "` for the function at
5746 /// `level` (0 = the running native), or `None` when that level does not
5747 /// exist or has no line information (a C function, a stripped chunk).
5748 pub(crate) fn position_prefix_at_level(&self, level: i64) -> Option<String> {
5749 let ts = self.thread_stack(None);
5750 let i = usize::try_from(level).ok()?;
5751 if i >= ts.levels.len() {
5752 return None;
5753 }
5754 let line = ts.currentline(i);
5755 if line <= 0 {
5756 return None;
5757 }
5758 let DbgKind::Lua(fi) = ts.levels[i] else {
5759 return None;
5760 };
5761 let ar = self.closure_ar(ts.lua(fi).closure);
5762 Some(format!(
5763 "{}:{line}: ",
5764 String::from_utf8_lossy(&ar.short_src)
5765 ))
5766 }
5767
5768 // ---- the interpreter ----
5769
5770 /// Run from the current top frame down to (but not past) `entry_depth`
5771 /// frames. Coroutine driving passes `entry_depth = 1` so the whole thread
5772 /// runs to completion or a yield.
5773 /// Resume the dispatcher from the saved
5774 /// `entry_depth` (captured pre-yield by `drive_one`). Called by
5775 /// `EvalFuture::poll` on every poll after the first to walk the
5776 /// existing call frames until the next `BudgetExhausted` or
5777 /// terminal `Ok`/`Err`. Not a public-API surface; the
5778 /// embedder reaches it through `Vm::eval_async`.
5779 pub(crate) fn exec_with_async(&mut self, entry_depth: usize) -> Result<Vec<Value>, LuaError> {
5780 self.exec_with(entry_depth)
5781 }
5782
5783 fn exec_with(&mut self, entry_depth: usize) -> Result<Vec<Value>, LuaError> {
5784 loop {
5785 let r = self.run(entry_depth);
5786 if r.is_err()
5787 && (self.yielding.is_some()
5788 || self.terminating.is_some()
5789 || self.host_yield_pending
5790 || self.pending_async_native_fut.is_some())
5791 {
5792 // a `coroutine.yield` is in flight: keep the frames intact (they
5793 // are the suspended coroutine's saved state) and propagate to
5794 // resume. A self-close termination propagates the same way, so a
5795 // protecting pcall on the way out cannot catch (unwind) it.
5796 // `host_yield_pending` is the async-mode
5797 // analogue: the sentinel must reach `drive_one` without
5798 // a protecting `pcall` swallowing it.
5799 return r;
5800 }
5801 match r {
5802 Ok(vals) => return Ok(vals),
5803 // unwind toward `entry_depth`. A protecting pcall/xpcall
5804 // continuation caught along the way turns the error into
5805 // `false, msg` and the loop resumes running its caller; an
5806 // uncaught error propagates out.
5807 Err(e) => match self.unwind(e.0, entry_depth) {
5808 Unwound::Caught => continue,
5809 Unwound::CaughtReturn(vals) => return Ok(vals),
5810 Unwound::Propagated(err) => return Err(err),
5811 },
5812 }
5813 }
5814 }
5815
5816 /// Unwind the call stack from the error point toward `entry_depth`, running
5817 /// `__close` handlers on each Lua frame. Stops at the first pcall/xpcall
5818 /// continuation frame at/above `entry_depth` (the error is *caught*: its
5819 /// slot receives `false, msg`); if none is reached, the error propagates.
5820 fn unwind(&mut self, mut err: Value, entry_depth: usize) -> Unwound {
5821 // The protected call runs in-place among the caller frames' registers,
5822 // so truncating the failed frames here cuts into caller windows below
5823 // the catcher. Snapshot the live length: at the error point the stack
5824 // already spans every surviving frame's window, so restoring it after a
5825 // catch reinstates them all (the reclaimed slots above are dead temps).
5826 // PUC handles overflow recovery via a separate EXTRA_STACK reserve;
5827 // we instead clamp the restore to the catcher's caller window when the
5828 // error point was at the stack limit (cause: the next `call_value_impl`
5829 // picks `func_slot = stack.len()` which would otherwise re-overflow).
5830 let saved_len = self.stack.len();
5831 err = self.raise_to_handler(err);
5832 // An error that no protected call inside the running coroutine will
5833 // catch kills it without unwinding: PUC's `lua_resume` leaves the
5834 // dead thread's stack as it was, so its pending to-be-closed
5835 // variables run only when it is closed (`coroutine.close`, or
5836 // `coroutine.wrap` closing it before re-raising). Scoped to the
5837 // coroutine's own run (`entry_depth == 1`); a run nested under a
5838 // native unwinds as before.
5839 if entry_depth == 1
5840 && self.version >= LuaVersion::Lua54
5841 && self
5842 .current
5843 .is_some_and(|c| c.status == crate::runtime::CoroStatus::Running)
5844 && !self.frames.iter().any(|f| {
5845 matches!(
5846 f,
5847 CallFrame::Cont(NativeCont {
5848 kind: ContKind::Pcall | ContKind::Xpcall { .. } | ContKind::Close(_),
5849 ..
5850 })
5851 )
5852 })
5853 {
5854 while self.frames.len() >= entry_depth {
5855 frames_pop_sync(&mut self.frames, &mut self.frames_top);
5856 }
5857 return Unwound::Propagated(LuaError(err));
5858 }
5859 while self.frames.len() >= entry_depth {
5860 match *self.frames.last().expect("frame") {
5861 // a yieldable-metamethod continuation does not catch: discard the
5862 // abandoned instruction and keep unwinding (PUC drops the partial
5863 // op on error).
5864 CallFrame::Cont(NativeCont {
5865 kind: ContKind::Meta(mc),
5866 func_slot,
5867 ..
5868 }) => {
5869 frames_pop_sync(&mut self.frames, &mut self.frames_top);
5870 self.stack.truncate(func_slot as usize);
5871 self.top = mc.saved_top.min(func_slot);
5872 self.tbc.retain(|&s| s < func_slot);
5873 }
5874 // a __pairs continuation does not catch either: an error inside
5875 // the metamethod propagates past `pairs`.
5876 CallFrame::Cont(NativeCont {
5877 kind: ContKind::Pairs,
5878 func_slot,
5879 ..
5880 }) => {
5881 frames_pop_sync(&mut self.frames, &mut self.frames_top);
5882 self.stack.truncate(func_slot as usize);
5883 self.top = func_slot;
5884 self.tbc.retain(|&s| s < func_slot);
5885 }
5886 // a __close continuation does not catch: drop the half-run
5887 // handler's window, then continue the close yieldably with
5888 // the new error threaded as `pending`. Preserve `cc.after`
5889 // verbatim — `Return`/`Block` originating from an aborting
5890 // OP_Return/OP_Close will be short-circuited by
5891 // `finish_close_after` (pending propagates as Err); a
5892 // `ResumeUnwind` originated by our own Lua-frame handler
5893 // must keep its deferred frame-pop semantics so that frame
5894 // is not orphaned. If a fresh handler yields, `drive_close`
5895 // pushes another `Cont::Close` and we return `Caught` so
5896 // `exec_with` re-enters the run loop.
5897 CallFrame::Cont(NativeCont {
5898 kind: ContKind::Close(cc),
5899 func_slot,
5900 ..
5901 }) => {
5902 frames_pop_sync(&mut self.frames, &mut self.frames_top);
5903 self.stack.truncate(func_slot as usize);
5904 self.top = func_slot;
5905 self.tbc.retain(|&s| s < func_slot);
5906 match self.drive_close(cc.from, Some(err), cc.after, entry_depth) {
5907 Ok(Some(_)) => {
5908 unreachable!(
5909 "Block / Return / ResumeUnwind never return host values mid-unwind"
5910 )
5911 }
5912 Ok(None) => return Unwound::Caught,
5913 Err(e) => {
5914 // the drained close re-raises `err`; only an
5915 // error a handler raised is new
5916 if !e.0.raw_eq(err) {
5917 err = self.raise_to_handler(e.0);
5918 }
5919 continue;
5920 }
5921 }
5922 }
5923 CallFrame::Cont(nc) => {
5924 frames_pop_sync(&mut self.frames, &mut self.frames_top);
5925 self.pcall_depth -= 1;
5926 let result = match nc.kind {
5927 ContKind::Pcall => {
5928 self.msgh_applied = None;
5929 err
5930 }
5931 // the handler ran where the error was raised (see
5932 // `raise_to_handler`); one raised past the handler's
5933 // reach (by the unwind itself) meets it here
5934 ContKind::Xpcall { handler } => {
5935 if self.msgh_applied.take().is_some_and(|v| v.raw_eq(err)) {
5936 err
5937 } else {
5938 self.call_msgh(handler, err)
5939 }
5940 }
5941 ContKind::Meta(_) | ContKind::Pairs | ContKind::Close(_) => {
5942 unreachable!("Meta/Pairs/Close cont handled above")
5943 }
5944 };
5945 // PUC 5.5 `luaG_errormsg` substitutes "<no error object>"
5946 // for nil AFTER the message handler ran (ldebug.c:849) —
5947 // so it applies to the pcall-caught object and to an
5948 // xpcall HANDLER'S return value, while the handler itself
5949 // (and a top-level propagation into the host, whose
5950 // `error_display` plays msghandler) still sees the raw
5951 // nil. 5.4- keep nil everywhere (errors.lua :49 asserts
5952 // `doit("error()") == nil`).
5953 let result = if matches!(result, Value::Nil)
5954 && self.version >= crate::version::LuaVersion::Lua55
5955 {
5956 Value::Str(self.heap.intern(b"<no error object>"))
5957 } else {
5958 result
5959 };
5960 // the error has been caught (pcall/xpcall): the captured
5961 // traceback was for that error and is no longer in flight.
5962 self.error_traceback = None;
5963 let fs = nc.func_slot as usize;
5964 if self.stack.len() < fs + 2 {
5965 self.stack.resize(fs + 2, Value::Nil);
5966 }
5967 self.stack[fs] = Value::Bool(false);
5968 self.stack[fs + 1] = result;
5969 self.top = nc.func_slot + 2;
5970 self.tbc.retain(|&s| s < nc.func_slot);
5971 if self.frames.len() < entry_depth {
5972 return Unwound::CaughtReturn(self.take_results(nc.func_slot));
5973 }
5974 self.finish_results(nc.func_slot, 2, nc.nresults);
5975 // reinstate the caller windows the unwind truncated into,
5976 // clamped to the catcher's caller window + a `MIN_STACK`
5977 // reserve. The clamp is a no-op for normal pcall catches
5978 // (saved_len lies within the caller's max_stack window),
5979 // and prevents the stack from staying near `MAX_LUA_STACK`
5980 // after an overflow-recovery catch — which would make the
5981 // next `call_value_impl` (e.g. a `__close` in the catcher's
5982 // errorh, locals.lua:659) pick `func_slot = stack.len()`
5983 // above the limit and re-overflow.
5984 // Restore the caller's full register window: opcodes
5985 // index it directly. The cap covers caller's base +
5986 // `max_stack` + a small reserve. We always resize to
5987 // exactly this window — previously this clamped
5988 // `saved_len` from above to prevent staying near
5989 // `MAX_LUA_STACK` after an overflow-recovery catch, and
5990 // a yieldable-unwind re-entry adds the dual case where
5991 // `saved_len` is *below* the window (a prior
5992 // `ResumeUnwind` truncated). Using the window directly
5993 // covers both.
5994 let restore = self
5995 .frames
5996 .iter()
5997 .rev()
5998 .find_map(CallFrame::lua)
5999 .map(|c| (c.base + c.closure.proto.max_stack as u32) as usize + 256)
6000 .unwrap_or(saved_len);
6001 if self.stack.len() < restore {
6002 self.stack.resize(restore, Value::Nil);
6003 } else if self.stack.len() > restore {
6004 self.stack.truncate(restore);
6005 }
6006 // Clear slots vacated by the popped
6007 // frames the unwind walked over. finish_results
6008 // above clears `[nc.func_slot + nresults ..
6009 // nc.func_slot + 2)`, which only covers the
6010 // pcall's own result region — the unwind-popped
6011 // frames' locals in `[nc.func_slot + 2 .. restore)`
6012 // are still in place with whatever Gc-bearing
6013 // Values they last held. Without this clear, a
6014 // later GC marks the stale pointers (same hazard as
6015 // the Op::Return finish_results path). PUC's `luaD_pcall` similarly truncates
6016 // L->top to the catcher's level — luna's
6017 // truncate above resizes the Vec but doesn't
6018 // touch slots [func_slot+2..restore) that were
6019 // already present.
6020 let clear_lo = (nc.func_slot as usize + 2).min(self.stack.len());
6021 let clear_hi = restore.min(self.stack.len());
6022 if clear_lo < clear_hi {
6023 for slot in &mut self.stack[clear_lo..clear_hi] {
6024 *slot = Value::Nil;
6025 }
6026 }
6027 return Unwound::Caught;
6028 }
6029 CallFrame::Lua(f) => {
6030 // Yieldable error-unwind close, PUC luaG_errormsg shape:
6031 // (1) pop the Lua frame immediately so each `__close`
6032 // handler runs at the C boundary above — `debug.getinfo`
6033 // sees the next outer Lua frame's call site (typically
6034 // `pcall`), not this aborting function (locals.lua:480).
6035 // (2) drive the close yieldably with
6036 // `AfterClose::ResumeUnwind { func_slot, err }`; on drain
6037 // it truncates to `func_slot` and re-raises (letting a
6038 // handler-raised error win over `err`). If a handler
6039 // yields, `drive_close` pushes `Cont::Close` and we
6040 // return `Caught` so `exec_with` re-enters the run loop;
6041 // a synchronous drain returns Err exactly as the old
6042 // path did.
6043 frames_pop_sync(&mut self.frames, &mut self.frames_top);
6044 let after = AfterClose::ResumeUnwind {
6045 func_slot: f.func_slot,
6046 err,
6047 };
6048 match self.begin_close(f.base, Some(err), after, entry_depth) {
6049 Ok(Some(_)) => {
6050 unreachable!("ResumeUnwind never returns host values")
6051 }
6052 Ok(None) => return Unwound::Caught,
6053 Err(e) => {
6054 // the drained close re-raises `err`; only an
6055 // error a handler raised is new
6056 if !e.0.raw_eq(err) {
6057 err = self.raise_to_handler(e.0);
6058 }
6059 continue;
6060 }
6061 }
6062 }
6063 }
6064 }
6065 Unwound::Propagated(LuaError(err))
6066 }
6067
6068 fn run(&mut self, entry_depth: usize) -> Result<Vec<Value>, LuaError> {
6069 loop {
6070 // Fast-path slow-check gate: most embedders run with both
6071 // `instr_budget` and `mem_cap` as None, so a single combined
6072 // is_some test lets the hot loop skip both branches with one
6073 // load + branch instead of two.
6074 if self.instr_budget.is_some() || self.heap.mem_cap.is_some() {
6075 if let Some(b) = self.instr_budget.as_mut() {
6076 *b -= 1;
6077 if *b <= 0 {
6078 self.instr_budget = None;
6079 // Async-mode cooperative
6080 // yield. Set a sentinel flag so `exec_with`
6081 // propagates the Err without `unwind` running
6082 // (mirroring the `yielding.is_some()` path),
6083 // and `call_value_impl` preserves the call
6084 // frames for the next `poll`. Translation back
6085 // to `DispatchOutcome::BudgetExhausted` happens
6086 // in `drive_one`. The Err value itself is
6087 // `Value::Nil` — a pure sentinel, never seen by
6088 // user code.
6089 if self.async_mode {
6090 self.host_yield_pending = true;
6091 return Err(LuaError(Value::Nil));
6092 }
6093 // Classify the trip so embedders can
6094 // distinguish budget exhaustion from a
6095 // generic Runtime error and retry / give up
6096 // accordingly.
6097 self.last_error_kind = crate::vm::error::LuaErrorKind::InstrBudget;
6098 let s = Value::Str(self.heap.intern(b"instruction budget exceeded"));
6099 return Err(LuaError(s));
6100 }
6101 }
6102 if let Some(cap) = self.heap.mem_cap
6103 && self.heap.bytes() > cap
6104 {
6105 // First try a full collect — embedders set tight caps
6106 // and the overshoot may be reclaimable (closures kept
6107 // by short-lived frames, intermediate strings). Only
6108 // disarm + raise if the cap is still breached after
6109 // collection. PUC's `LUA_GCEMERGENCY` path matches.
6110 //
6111 // Root up to the deepest Lua frame's
6112 // `base + max_stack` window rather than the entire
6113 // `self.stack.len()`
6114 // (covers register operands the current opcode
6115 // might reference). The cap fires during table
6116 // mutation in a tight `a[i] = i` loop where `a`
6117 // lives at a frame-register slot past `self.top`
6118 // (OP_NEWINDEX doesn't advance top); the deepest
6119 // frame's max_stack window provably covers it
6120 // since `a` is a register of the executing proto.
6121 //
6122 // Still over-roots caller frames' dead regs
6123 // (slots between caller.base and the callee
6124 // func_slot are live; slots past callee
6125 // func_slot in caller's frame are dead until
6126 // caller resumes). For fire-once cap path this
6127 // residual over-root is acceptable; there is no
6128 // full per-frame walk because a strong/weak pass
6129 // split is semantically impossible — the weak pass
6130 // depends on strong-pass marks.
6131 let cap_root_top = self
6132 .frames
6133 .iter()
6134 .rev()
6135 .find_map(CallFrame::lua)
6136 .map(|f| f.base + f.closure.proto.max_stack as u32)
6137 .unwrap_or(self.top);
6138 self.gc_top = cap_root_top.max(self.top);
6139 self.collect_garbage();
6140 if self.heap.bytes() > cap {
6141 self.heap.mem_cap = None;
6142 let s = Value::Str(self.heap.intern(b"memory cap exceeded"));
6143 return Err(LuaError(s));
6144 }
6145 }
6146 }
6147 // Single combined frame fetch: continuation arm OR Lua arm. Saves
6148 // a second `self.frames.last()` slice access vs the prior split
6149 // form (LLVM doesn't always CSE these across the cont branch).
6150 // A continuation frame on top means the call it protected just
6151 // delivered its results — wrap as `true, results…` and hand to
6152 // the pcall/xpcall caller. The error path is handled by `unwind`;
6153 // this branch is only reached on success/resume completion.
6154 // 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).
6155 let frame_peek = unsafe { self.frames.last().unwrap_unchecked() };
6156 if let &CallFrame::Cont(nc) = frame_peek {
6157 // a yieldable metamethod returned: complete the interrupted
6158 // instruction (PUC luaV_finishOp) and resume the running frame.
6159 if let ContKind::Meta(mc) = nc.kind {
6160 frames_pop_sync(&mut self.frames, &mut self.frames_top);
6161 let result = if self.top > nc.func_slot {
6162 self.stack[nc.func_slot as usize]
6163 } else {
6164 Value::Nil
6165 };
6166 self.stack.truncate(nc.func_slot as usize);
6167 self.top = mc.saved_top;
6168 self.finish_meta(mc.action, result)?;
6169 continue;
6170 }
6171 // a __close handler returned successfully: discard its
6172 // results, restore `top` to the slot the handler was called
6173 // at (the surrounding frame's register window above this slot
6174 // must stay alloc'd — never truncate the underlying stack),
6175 // then continue the close chain (next slot, or fire
6176 // AfterClose). When the close ends an entry activation,
6177 // drive_close hands the results up to exec_with directly.
6178 if let ContKind::Close(cc) = nc.kind {
6179 frames_pop_sync(&mut self.frames, &mut self.frames_top);
6180 self.top = nc.func_slot;
6181 if let Some(vals) =
6182 self.drive_close(cc.from, cc.pending, cc.after, entry_depth)?
6183 {
6184 return Ok(vals);
6185 }
6186 continue;
6187 }
6188 // __pairs returned: normalize its results to exactly the
6189 // dialect's count (iterator, state, control, and on 5.5 the
6190 // closing value) at pairs's slot, where the metamethod was
6191 // called, and hand them to pairs's caller.
6192 if let ContKind::Pairs = nc.kind {
6193 frames_pop_sync(&mut self.frames, &mut self.frames_top);
6194 let total = crate::vm::builtins::pairs_mm_results(self) as u32;
6195 let need = (nc.func_slot + total) as usize;
6196 if self.stack.len() < need {
6197 self.stack.resize(need, Value::Nil);
6198 }
6199 // the metamethod ran one slot above pairs's own
6200 let first = nc.func_slot + 1;
6201 let n = (self.top - first).min(total);
6202 for i in 0..n {
6203 self.stack[(nc.func_slot + i) as usize] = self.stack[(first + i) as usize];
6204 }
6205 for s in (nc.func_slot + n)..(nc.func_slot + total) {
6206 self.stack[s as usize] = Value::Nil;
6207 }
6208 self.top = nc.func_slot + total;
6209 if self.frames.len() < entry_depth {
6210 return Ok(self.take_results(nc.func_slot));
6211 }
6212 self.finish_results(nc.func_slot, total, nc.nresults);
6213 continue;
6214 }
6215 frames_pop_sync(&mut self.frames, &mut self.frames_top);
6216 self.pcall_depth -= 1;
6217 // f's results sit at nc.func_slot+1.. (f was called one slot
6218 // above the continuation), so writing `true` at the slot makes
6219 // `true, results…` already contiguous.
6220 let nret = self.top - (nc.func_slot + 1);
6221 self.stack[nc.func_slot as usize] = Value::Bool(true);
6222 let total = 1 + nret;
6223 self.top = nc.func_slot + total;
6224 if self.frames.len() < entry_depth {
6225 return Ok(self.take_results(nc.func_slot));
6226 }
6227 self.finish_results(nc.func_slot, total, nc.nresults);
6228 continue;
6229 }
6230 // GC runs only at the allocation safe points below (PUC's
6231 // `luaC_checkGC` sites), each with a precise `gc_top`; the loop head
6232 // no longer collects, so a stale full-window `gc_top` cannot leak in.
6233 //
6234 // Hot-path frame fetch: the Cont arm above continues the loop,
6235 // so reaching here means `frame_peek` is the Lua frame. Reuse it
6236 // rather than re-fetching `self.frames.last()`.
6237 let f = match frame_peek {
6238 CallFrame::Lua(f) => f,
6239 _ => unreachable!("Cont frame survived the dispatch loop head"),
6240 };
6241 let cl = f.closure;
6242 let base = f.base;
6243 let func_slot = f.func_slot;
6244 let n_varargs = f.n_varargs;
6245 let pc = f.pc;
6246 let oldpc = f.hook_oldpc;
6247
6248 // SAFETY: `pc` is bounded by the compiler against `proto.code.len()`
6249 // — every branch / call op only sets `pc` to a valid index, and
6250 // function entry initialises pc=0 with a non-empty body. PUC's
6251 // `vmfetch` uses the equivalent unchecked load.
6252 let inst = unsafe { *cl.proto.code.get_unchecked(pc as usize) };
6253
6254 // Trace recording append + close detection.
6255 // Gated on `trace_jit_enabled` + `active_trace.is_some()`
6256 // so default dispatch keeps a single not-taken branch.
6257 //
6258 // - At the head PC with a non-empty record, the trace has
6259 // looped back to its start: mark `closed = true` and
6260 // take the record for compile + cache.
6261 // - Otherwise, capture the op. If the record overflows
6262 // MAX_TRACE_LEN, abort by dropping it.
6263 if self.jit.trace_enabled
6264 && let Some(_rec) = self.jit.active_trace.as_mut()
6265 {
6266 // Depth tracking. The trace head's frame is
6267 // at index `recording_frame_base`; every Op::Call that
6268 // pushes a new frame bumps the live depth, every
6269 // Op::Return that pops one decrements it.
6270 //
6271 // **Three clean-close conditions**:
6272 // - `at_head`: cur_depth == 0 AND about-to-execute the
6273 // trace's head_pc on its head_proto (loop closed back
6274 // to start). Same for loop-triggered and call-triggered
6275 // traces, so a call-triggered trace does not close on
6276 // the first re-entry (that would leave fib's body at 7
6277 // depth=0 ops); it inlines up to MAX_INLINE_DEPTH
6278 // levels before any close.
6279 // - `returned_past_head`: trace head's frame is gone
6280 // (callee returned past it, or the call-trigger
6281 // started a recording inside a callee that has now
6282 // returned). Whatever ops were recorded form the
6283 // trace body; the lowerer treats the partial trace
6284 // the same as InlineAbort.
6285 // - `depth_cap_hit`: cur_depth > MAX_INLINE_DEPTH.
6286 // Recording any deeper would just bloat the IR; close
6287 // with the body we have. Lowerer's existing length
6288 // gate + InlineAbort path handles short bodies.
6289 let returned_past_head = self.frames.len() <= self.jit.recording_frame_base;
6290 let cur_depth = if returned_past_head {
6291 0
6292 } else {
6293 self.frames.len() - 1 - self.jit.recording_frame_base
6294 };
6295 let depth_cap_hit = cur_depth > crate::jit::trace::MAX_INLINE_DEPTH as usize;
6296 let rec = self.jit.active_trace.as_mut().expect("just checked Some");
6297 let at_head_loop = cur_depth == 0
6298 && !rec.ops.is_empty()
6299 && !returned_past_head
6300 && std::ptr::eq(cl.proto.as_ptr(), rec.head_proto.as_ptr())
6301 && pc == rec.head_pc;
6302 // Self-link cycle catch (mirrors LuaJIT's
6303 // `check_call_unroll` at `lj_record.c:1869`). Trips when:
6304 // 1. We're about to execute the head_pc on head_proto
6305 // at depth > 0 (we're re-entering the trace head
6306 // from inside an inlined recursion level — UpRec).
6307 // 2. The count of ancestor frames in the recording
6308 // window that share `head_proto` exceeds
6309 // [`RECUNROLL_THRESHOLD`] (default 2).
6310 // For fib(N): head_pc=0, head_proto=fib. After 2 inline
6311 // recursion levels are captured, the recorder enters
6312 // the 3rd nested fib frame, sees cur_depth=3 > 2, and
6313 // trips this catch — closing with `SelfRecKind::UpRec`.
6314 // The lowerer's `TraceEnd::SelfLink` tail emits the
6315 // bump-base + branch-to-self loop body.
6316 //
6317 // TailRec vs UpRec: LJ distinguishes via
6318 // `framedepth + retdepth == 0`. luna doesn't track
6319 // retdepth separately; cur_depth == 0 with a non-empty
6320 // call chain in tail position is rare (would require
6321 // explicit Lua TCO). We use cur_depth > 0 as the UpRec
6322 // condition (fib's case); cur_depth == 0 with positive
6323 // ancestor count would route to TailRec, but luna's
6324 // recorder doesn't currently produce that shape because
6325 // tail-call elision pops the caller frame and we'd
6326 // hit `at_head_loop` instead.
6327 let self_link_trip: Option<crate::jit::trace::SelfRecKind> = {
6328 if self.jit.self_link_enabled
6329 && !returned_past_head
6330 && std::ptr::eq(cl.proto.as_ptr(), rec.head_proto.as_ptr())
6331 && pc == rec.head_pc
6332 && cur_depth > 0
6333 {
6334 // Count ancestor frames sharing head_proto.
6335 // self.frames[recording_frame_base..] currently
6336 // includes the just-pushed frame at the top
6337 // (the one about to execute head_pc). Ancestors
6338 // = the slice excluding the top frame.
6339 let head_proto_ptr = rec.head_proto.as_ptr();
6340 let last_idx = self.frames.len() - 1;
6341 let mut count = 0usize;
6342 for i in self.jit.recording_frame_base..last_idx {
6343 if let CallFrame::Lua(f) = &self.frames[i]
6344 && std::ptr::eq(f.closure.proto.as_ptr(), head_proto_ptr)
6345 {
6346 count += 1;
6347 }
6348 }
6349 if count > crate::jit::trace::RECUNROLL_THRESHOLD {
6350 // cur_depth > 0 → UpRec (fib pattern).
6351 // cur_depth == 0 wouldn't reach this arm.
6352 Some(crate::jit::trace::SelfRecKind::UpRec)
6353 } else {
6354 None
6355 }
6356 } else {
6357 None
6358 }
6359 };
6360 if let Some(kind) = self_link_trip {
6361 // SelfLink relax for self-recursive patterns at frame
6362 // depth >= 2.
6363 //
6364 // Stamping `self_link_kind` unconditionally at the
6365 // head_pc re-entry would never dispatch: the
6366 // `downrec_close` marker can only fire from the
6367 // depth>0 Op::Return path (`rec.retfs` chain),
6368 // which never reaches the recorder for fib(28)-like
6369 // shapes that hit the SelfLink cycle catch BEFORE
6370 // any base-case Return — leaving `downrec_close`
6371 // None and routing the trace through the safe
6372 // `dispatchable=false` `"self-link-retf-r1"` path.
6373 //
6374 // So when the SelfLink trip fires AND
6375 // `cur_depth >= 2` (the count > RECUNROLL_THRESHOLD
6376 // gate already requires this — kept explicit as a
6377 // safety floor), route the close through `downrec_
6378 // close` INSTEAD of `self_link_kind`. The recorder
6379 // synthesises the close marker from the most
6380 // recent Op::Call at depth `cur_depth - 1`:
6381 // - `return_pc` = `call.pc + 1` (caller's resume
6382 // PC after the recursive call returns; mirror
6383 // of the `caller_pc` derivation at the
6384 // depth>0 Op::Return capture path below).
6385 // - `target_proto` = `call.proto` (caller's
6386 // proto; equals `rec.head_proto` for self-
6387 // recursion).
6388 // - `depth_delta` = `1` (the recorder always
6389 // unrolls one level; the Op::Return path uses
6390 // the same constant).
6391 //
6392 // The lowerer's `end_idx` picker routes through
6393 // `TraceEnd::DownRec` ahead of the `self_link_kind`
6394 // arm and emits the stitch-sentinel +
6395 // caller-pc-guard scaffold. A single-candidate guard
6396 // chain (this path produces 1 caller_pc candidate
6397 // because `rec.retfs` is empty) keeps
6398 // `dispatchable=false` + `"downrec-stitch-pending"`
6399 // label (the lowerer requires
6400 // `multi_way_candidate_count >= 2`). Net behaviour:
6401 // trace compiles under DownRec routing; interp runs
6402 // the recursion naturally.
6403 //
6404 // The `cur_depth >= 2` gate is automatically
6405 // satisfied by the count > RECUNROLL_THRESHOLD=2
6406 // trip condition (3 ancestor frames sharing
6407 // head_proto implies cur_depth >= 3), kept
6408 // explicit so a future RECUNROLL_THRESHOLD tweak
6409 // doesn't silently flip shallow-recursion
6410 // shapes (cur_depth == 1) onto the DownRec arm.
6411 //
6412 // The recorded body still uses depth-baked
6413 // op_offsets[] addressing, so this is routing
6414 // scaffolding and gives no speedup by itself.
6415 let _ = kind;
6416 let relaxed_to_downrec = cur_depth >= 2 && rec.downrec_close.is_none() && {
6417 let caller_depth_u8 = (cur_depth - 1) as u8;
6418 if let Some(call_op) = rec.ops.iter().rev().find(|r| {
6419 r.inline_depth == caller_depth_u8
6420 && matches!(r.inst.op(), crate::vm::isa::Op::Call)
6421 }) {
6422 rec.downrec_close = Some(crate::jit::trace::DownRecClose {
6423 return_pc: call_op.pc + 1,
6424 target_proto: call_op.proto,
6425 depth_delta: 1,
6426 });
6427 true
6428 } else {
6429 false
6430 }
6431 };
6432 if relaxed_to_downrec {
6433 // Close-cause taxonomy: tag the lift so
6434 // probes can tally the fire rate. Mirrors
6435 // the `"downrec-restart"` bump for the
6436 // depth>0 Op::Return path (different trip
6437 // origin, same downstream routing). The
6438 // existing `"self-link-retf-r1"` label still
6439 // fires for trips that DON'T relax (no
6440 // candidate Op::Call ancestor in rec.ops, or
6441 // cur_depth < 2) via the lowerer's
6442 // dispatch_off_reason mirror at the close
6443 // handler — kept as a regression safety net.
6444 self.jit
6445 .counters
6446 .bump_close_cause("selflink-yields-to-downrec");
6447 } else {
6448 rec.self_link_kind = Some(kind);
6449 }
6450 }
6451 let should_close =
6452 at_head_loop || returned_past_head || depth_cap_hit || self_link_trip.is_some();
6453 if should_close {
6454 // Long-trace bias: a call-triggered
6455 // recording that closed with a very short body
6456 // (fib base case: `Lt`/`Jmp`/`Return1` = 3 ops,
6457 // binary_trees `make(0)`: 4 ops) is pathological.
6458 // Compiling + caching it pins `Proto.traces` to a
6459 // trace that the length gate will refuse to
6460 // dispatch (per `MIN_DISPATCHABLE_TRUNC_BODY_FLOOR
6461 // = 40`), AND blocks the back-edge / longer-call
6462 // path from re-recording the same head_pc (the
6463 // dedup `already_cached` check below short-
6464 // circuits). The fix: discard the short call-
6465 // triggered recording WITHOUT caching, and bias
6466 // the proto's `call_hot_count` back to
6467 // `THRESHOLD - HOT_RETRY_WINDOW` so the next
6468 // sequence of calls retries the trigger at a
6469 // different (hopefully deeper) recursion point.
6470 //
6471 // Back-edge triggered traces are exempt — a
6472 // tight numeric-for loop's body is legitimately
6473 // 3 ops (`Add`, ForLoop) and DOES dispatch
6474 // usefully when re-entered many times.
6475 // Coverage heuristic to detect
6476 // pathologically partial call-triggered traces:
6477 // for self-recursive / branchy protos like
6478 // `fib` (~17 bytecode ops) or
6479 // `binary_trees.make` (~26 ops), the recorder
6480 // can fire at a BASE-case entry (`fib(0)` or
6481 // `make(0)`) producing a 3–4 op trace that
6482 // covers a tiny fraction of the proto's code.
6483 // That trace is doomed by the length gate
6484 // post-compile AND blocks any longer follow-up
6485 // (the dedup `already_cached` check below). The
6486 // fix: discard call-triggered closes where
6487 // `rec.ops.len() * 2 < head_proto.code.len()`
6488 // (less than half the proto's bytecode), so the
6489 // back-edge / longer call path can take over.
6490 //
6491 // Why coverage > raw length:protos with
6492 // intrinsically short bodies (closure
6493 // factories: `Closure + Return1` = 2 ops,
6494 // simple wrappers: `LoadI + Return1` = 2 ops)
6495 // record 100% coverage even at length 2 — those
6496 // ARE legitimately short and the closure /
6497 // sunk-emit lowering paths make
6498 // them worth compiling. The heuristic admits
6499 // them. fib's `[Lt, Jmp, Return1]` (3 of ~17)
6500 // and make's `[Lt, Jmp, LoadI, Return1]` (4 of
6501 // ~26) get discarded.
6502 //
6503 // Back-edge triggered traces are unaffected —
6504 // a tight numeric-for body legitimately covers
6505 // 3 of ~3 proto ops it can dispatch from
6506 // (`Add + ForLoop`) and the recorder fires on
6507 // the back-edge, not call entry.
6508 //
6509 // `call_hot_count` is intentionally NOT reset
6510 // (an earlier draft tried `THRESHOLD - 32` but
6511 // caused active_trace contention with the
6512 // outer back-edge trigger — see
6513 // setlist_b_zero_with_call_c_zero_sunk_emits).
6514 // We give up on dispatching the pathological
6515 // shape on the same proto; the back-edge or a
6516 // longer call path on a deeper recursion point
6517 // can still record + cache a real trace.
6518 let proto_code_len = rec.head_proto.code.len();
6519 let is_partial_coverage = rec.ops.len() * 2 < proto_code_len;
6520 // Per-Proto discard cap. The relaxed
6521 // trigger condition (`c >= THRESHOLD &&
6522 // !already_cached`) means a Proto whose every
6523 // recording is partial-coverage will re-fire the
6524 // trigger every call indefinitely (1500+ in
6525 // `binary_trees`-pattern test). The cap stops
6526 // discarding after `MAX_DISCARDS_PER_PROTO` —
6527 // the next close falls through to compile (even
6528 // if partial), caches the trace, and the
6529 // `already_cached` short-circuit kills the
6530 // storm. Dispatch may still be refused
6531 // post-compile (length gate), but the recorder
6532 // stops churning.
6533 const MAX_DISCARDS_PER_PROTO: u32 = 5;
6534 let prior_discards = rec.head_proto.trace_discard_count.get();
6535 let cap_reached = prior_discards >= MAX_DISCARDS_PER_PROTO;
6536 // Flip the `gave_up` flag the
6537 // moment cap is reached (BEFORE the close-
6538 // dispatching branch below). The trigger gates
6539 // short-circuit on this flag, skipping the
6540 // RefCell + linear `already_cached` scan on
6541 // every subsequent call to this Proto. Useful
6542 // for `binary_trees_pattern`-class loads where
6543 // a single Proto sees ~20k calls post-cap.
6544 if cap_reached
6545 && rec.is_call_triggered
6546 && is_partial_coverage
6547 && !rec.head_proto.trace_gave_up.get()
6548 {
6549 rec.head_proto.trace_gave_up.set(true);
6550 }
6551 if rec.is_call_triggered && is_partial_coverage && !cap_reached {
6552 // Tally as closed (for visibility) but DROP
6553 // without compile/cache. Use the existing
6554 // closed-lens accumulator so probes can
6555 // observe the discarded shape.
6556 // Bump discard count BEFORE
6557 // dropping the recording so the next
6558 // close sees the updated counter.
6559 rec.head_proto.trace_discard_count.set(prior_discards + 1);
6560 self.jit.counters.closed += 1;
6561 self.jit
6562 .counters
6563 .closed_lens
6564 .push((rec.is_call_triggered, rec.ops.len()));
6565 // Partial-coverage discard close path.
6566 // `closed` + `closed_lens` alone can't separate
6567 // a real successful close from a discard tally,
6568 // so tag explicitly to keep the recorder-side
6569 // close-cause taxonomy single-source.
6570 self.jit
6571 .counters
6572 .bump_close_cause("partial-coverage-discard");
6573 self.jit.active_trace = None;
6574 // Continue with interp loop — don't
6575 // fall through to compile path.
6576 // The op at `pc` hasn't dispatched yet;
6577 // the outer loop iteration handles it.
6578 } else {
6579 rec.closed = true;
6580 // Detach the closed record, then try
6581 // to compile it. Dedup by `head_pc`: a Proto
6582 // already carrying a CompiledTrace for this PC
6583 // skips recompile (the hot counter caps
6584 // re-recording at `u32::MAX / 2` anyway, but
6585 // explicit dedup keeps `Proto.traces` short
6586 // for the dispatcher's linear scan).
6587 //
6588 // On failure we just bump the failed counter
6589 // and drop the record.
6590 let head_pc_val = rec.head_pc;
6591 let closed_record = self
6592 .jit
6593 .active_trace
6594 .take()
6595 .expect("active_trace was Some this branch");
6596 self.jit.counters.closed += 1;
6597 self.jit
6598 .counters
6599 .closed_lens
6600 .push((closed_record.is_call_triggered, closed_record.ops.len()));
6601 // Cache the trace on the
6602 // recorder's *head proto*, not the current
6603 // closure's proto. For non-recursive
6604 // call-triggered traces, close fires after
6605 // `Return1` pops the callee frame — `cl` at
6606 // that point is the CALLER's closure, while
6607 // `closed_record.head_proto` is the CALLEE's
6608 // proto (the one we actually want the trace
6609 // to be discoverable from on the next call).
6610 // Self-recursive fib closed via depth-cap
6611 // mid-recursion, so `cl.proto == head_proto`
6612 // there, but only by coincidence.
6613 let head_proto = closed_record.head_proto;
6614 let already_cached = head_proto
6615 .traces
6616 .borrow()
6617 .iter()
6618 .any(|t| t.head_pc == head_pc_val);
6619 if !already_cached {
6620 // Internal-loop = true: the trace runs in
6621 // a native loop until a cmp side-exits, so
6622 // the dispatcher's per-entry marshal cost
6623 // amortizes across the whole run of
6624 // iterations the loop's recorded direction
6625 // stays valid. The lowerer auto-downgrades
6626 // to one-shot for cmp-less or Call-truncating
6627 // traces.
6628 // Side traces MUST NOT
6629 // internal-loop. The parent's recorded prefix
6630 // (ops at PCs < side trace's head_pc) defines
6631 // values for registers the child's body reads
6632 // without re-writing each iter — e.g. for
6633 // s12_step_b, parent's `pc=19 Add R[12] = R[1]
6634 // + R[11]` sets R[12], and the child trace
6635 // (head_pc=24) re-runs `pc=20 Move R[1] =
6636 // R[12]` each iter via its outer ForLoop
6637 // internal-loop, ALWAYS reading the stale
6638 // entry-time R[12]. The parent's Add never
6639 // re-runs during child's loop, so R[1] gets
6640 // pinned to one stale value. Force one-shot
6641 // for side traces: each parent-exit round-
6642 // trips through dispatcher → parent's Add
6643 // runs → side trace runs ONE iter → return.
6644 let opts = crate::jit::trace::CompileOptions {
6645 internal_loop: closed_record.side_trace_parent.is_none(),
6646 pre53: self.version() <= LuaVersion::Lua53,
6647 aot: false,
6648 };
6649 // Route through trace_compiler; split-borrow JitState
6650 // so the trait method can take `&mut dyn JitStorage`.
6651 let result = {
6652 let jit = &mut self.jit;
6653 jit.storage.claim(self.jit_owner_id);
6654 let storage: &mut dyn crate::jit::JitStorage = jit.storage.as_mut();
6655 jit.trace_compiler
6656 .try_compile_trace(storage, &closed_record, opts)
6657 };
6658 match result {
6659 Some(mut ct) => {
6660 // Tally Sinkable sites
6661 // + actually-sunk-emit sites + materialise
6662 // emit sites before moving `ct` into
6663 // Proto.traces.
6664 self.jit.counters.sinkable_seen +=
6665 ct.sinkable_sites_seen as u64;
6666 self.jit.counters.accum_bufferable_seen +=
6667 ct.accum_bufferable_seen as u64;
6668 self.jit.counters.sunk_alloc += ct.sunk_alloc_seen as u64;
6669 self.jit.counters.materialize_emit +=
6670 ct.materialize_emit_count as u64;
6671 self.jit.counters.closure_emit += ct.closure_seen as u64;
6672 if ct.is_inline_abort_close {
6673 self.jit.counters.inline_abort += 1;
6674 }
6675 // Split tally so a
6676 // probe can answer the AOT
6677 // `accepted_with_per_exit_inline`
6678 // gate's question at the JIT
6679 // surface too: how many compiled
6680 // traces emitted depth>0 cmp
6681 // side-exits, and how many of
6682 // those survived all the
6683 // `dispatchable = false` pins
6684 // (`InlineAbort-gate`,
6685 // `self-link-retf-r1`,
6686 // `downrec-stitch-pending`, etc.).
6687 if !ct.per_exit_inline.is_empty() {
6688 self.jit.counters.per_exit_inline_compiled += 1;
6689 if ct.dispatchable {
6690 self.jit.counters.per_exit_inline_dispatchable += 1;
6691 }
6692 }
6693 if let Some(reason) = ct.dispatch_off_reason {
6694 self.jit.counters.dispatch_off_reasons.push(reason);
6695 // Mirror
6696 // the ordered Vec push into
6697 // the per-reason HashMap so
6698 // probes can answer "how many
6699 // of each dispatch_off label
6700 // fired" in O(1) without
6701 // walking the Vec. Same
6702 // bucket as the recorder-side
6703 // abort/discard tags above.
6704 self.jit.counters.bump_close_cause(reason);
6705 }
6706 // Count compiled traces that
6707 // carry a down-recursion stitch
6708 // link. Bumped here (not at the
6709 // lowerer emit site) because the
6710 // Vm's JitCounters live on the Vm,
6711 // and the lowerer doesn't have a
6712 // Vm handle. Read via
6713 // `Vm::trace_downrec_link_compiled_count`.
6714 if ct.downrec_link.is_some() {
6715 self.jit.counters.downrec_link_compiled += 1;
6716 }
6717 // Multi-way guard emit counter.
6718 // Bumped when the lowerer collected
6719 // >= 2 distinct caller_pc candidates
6720 // and lifted `dispatchable=true`.
6721 // The single-CMP shape stores
6722 // `1` here without bumping; non-
6723 // DownRec closes store `0`.
6724 if ct.downrec_multi_way_count >= 2 {
6725 self.jit.counters.multi_way_guard_emitted += 1;
6726 }
6727 // Side-trace finalisation.
6728 // Pin `dispatchable=false` so the
6729 // primary lookup `traces.find(|t|
6730 // t.head_pc == pc && t.dispatchable)`
6731 // never matches this entry — the
6732 // side trace is meant to be entered
6733 // ONLY through the parent's exit
6734 // indirection, not the
6735 // back-edge / call-trigger paths.
6736 // Then write the entry fn ptr into
6737 // the parent's `exit_side_trace_ptrs`
6738 // slot so the parent's IR can read it.
6739 if let Some((parent_proto, parent_head_pc, parent_exit_idx)) =
6740 closed_record.side_trace_parent
6741 {
6742 // The lowerer's own verdict: a trace it
6743 // compiled but would not dispatch (an
6744 // untyped table read, an inline abort)
6745 // is not safe to enter from the parent's
6746 // exit either — unless the only reason
6747 // was the length gate, which weighs
6748 // dispatch overhead, not soundness (the
6749 // lowerer records the other reasons
6750 // first).
6751 let runnable = ct.dispatchable
6752 || ct.dispatch_off_reason == Some("length-gate");
6753 ct.dispatchable = false;
6754 let entry_ptr = ct.entry as *const () as *const u8;
6755 let _side_trace_head_pc = closed_record.head_pc;
6756 let parent_traces = parent_proto.traces.borrow();
6757 if let Some(parent_ct) = parent_traces
6758 .iter()
6759 .find(|t| t.head_pc == parent_head_pc)
6760 {
6761 // Shape-match
6762 // gate. Find the parent's per-exit
6763 // tag snapshot at the wired exit
6764 // (inline / tag / global) and
6765 // check the child's entry_tags
6766 // match. If not, leave the cell
6767 // null + skip cache populate so
6768 // the parent IR's
6769 // `call_indirect` stays inert at
6770 // this exit (the child's
6771 // shape-specialised IR would
6772 // mis-interpret raw bits the
6773 // parent writes to reg_state).
6774 let inline_n = parent_ct.per_exit_inline.len();
6775 let tags_n = parent_ct.per_exit_tags.len();
6776 let parent_exit_tags_slice: &[
6777 crate::jit::trace::ExitTag
6778 ] = if parent_exit_idx < inline_n {
6779 &parent_ct.per_exit_inline
6780 [parent_exit_idx]
6781 .exit_tags
6782 } else if parent_exit_idx
6783 < inline_n + tags_n
6784 {
6785 &parent_ct.per_exit_tags
6786 [parent_exit_idx - inline_n]
6787 .1
6788 } else {
6789 &parent_ct.exit_tags
6790 };
6791 let shape_matches =
6792 crate::jit::trace::exit_tags_match_entry_tags(
6793 &ct.entry_tags,
6794 parent_exit_tags_slice,
6795 &parent_ct.entry_tags,
6796 );
6797 if !shape_matches {
6798 self.jit.counters.side_trace_shape_mismatch += 1;
6799 }
6800 let shape_ok = runnable && shape_matches;
6801 // Write the child's
6802 // entry fn ptr to BOTH the legacy
6803 // `exit_side_trace_ptrs[idx]`
6804 // cell (read by the
6805 // walk_any_side_ptr_non_null tests)
6806 // AND the per-kind cell
6807 // whose heap address the parent's
6808 // IR baked. The IR-baked
6809 // cell is what the call_indirect
6810 // gate actually reads. Only write
6811 // when the shape gate passes.
6812 if shape_ok {
6813 if let Some(cell) = parent_ct
6814 .exit_side_trace_ptrs
6815 .get(parent_exit_idx)
6816 {
6817 cell.set(entry_ptr);
6818 }
6819 // Compute (kind, local) for the
6820 // IR-baked cell. Layout follows
6821 // exit_hit_counts: inline first,
6822 // then per_exit_tags, then the
6823 // global tail slot.
6824 let (sent_kind, sent_local) = if parent_exit_idx
6825 < inline_n
6826 {
6827 parent_ct.per_exit_inline[parent_exit_idx]
6828 .side_trace_ptr
6829 .set(entry_ptr);
6830 (
6831 crate::jit::trace::SIDE_SENT_KIND_INLINE,
6832 parent_exit_idx as u32,
6833 )
6834 } else if parent_exit_idx < inline_n + tags_n {
6835 let local = parent_exit_idx - inline_n;
6836 if let Some(b) =
6837 parent_ct.tags_side_trace_ptrs.get(local)
6838 {
6839 b.set(entry_ptr);
6840 }
6841 (
6842 crate::jit::trace::SIDE_SENT_KIND_TAG,
6843 local as u32,
6844 )
6845 } else {
6846 parent_ct.global_side_trace_ptr.set(entry_ptr);
6847 (crate::jit::trace::SIDE_SENT_KIND_GLOBAL, 0)
6848 };
6849 self.jit.counters.side_trace_compiled += 1;
6850 // Flip the
6851 // parent's fast-path hint so
6852 // the dispatcher knows to do
6853 // the tentative decode + cell
6854 // check on subsequent
6855 // dispatches. Set once and
6856 // stays true (we never unwire
6857 // a side trace today).
6858 parent_ct.has_any_side_wired.set(true);
6859
6860 // Populate
6861 // the O(1) lookup cache the
6862 // dispatcher consults on
6863 // sentinel-bit-set returns.
6864 // Key is the encoded sentinel
6865 // (same encoding the IR ORs
6866 // into bits 56..=62 of the
6867 // child's i64 return).
6868 let sentinel =
6869 crate::jit::trace::encode_side_sentinel(
6870 sent_kind, sent_local,
6871 );
6872 let predicted_idx = if std::ptr::eq(
6873 parent_proto.as_ptr(),
6874 head_proto.as_ptr(),
6875 ) {
6876 parent_traces.len() as u32
6877 } else {
6878 head_proto.traces.borrow().len() as u32
6879 };
6880 parent_ct
6881 .side_trace_cache
6882 .borrow_mut()
6883 .insert(sentinel, predicted_idx);
6884 }
6885 }
6886 drop(parent_traces);
6887 }
6888 head_proto.traces.borrow_mut().push(TArc::new(ct));
6889 self.jit.counters.compiled += 1;
6890 }
6891 None => {
6892 self.jit.counters.compile_failed += 1;
6893 note_trace_compile_failure(head_proto, closed_record.head_pc);
6894 self.jit
6895 .counters
6896 .compile_failed_reasons
6897 .push(self.jit.trace_compiler.last_compile_checkpoint());
6898 }
6899 }
6900 }
6901 } // close the long-trace-bias else branch
6902 } else {
6903 // Depth-aware push at the
6904 // current `cur_depth`. The `depth_cap_hit` /
6905 // `returned_past_head` early-exit is handled by
6906 // the `should_close` branch above; reaching here
6907 // means `cur_depth <= MAX_INLINE_DEPTH` and the
6908 // trace head's frame is still live.
6909 let depth_u8 = cur_depth as u8;
6910 if depth_u8 > self.jit.max_depth_seen {
6911 self.jit.max_depth_seen = depth_u8;
6912 }
6913 // Fix up a prior `Op::Call C=0` (multi-
6914 // return / variable return count). Recorder pushed
6915 // it with var_count=None before the call dispatched;
6916 // now that the call has returned and we're about to
6917 // push the next op, top reflects the actual return
6918 // count. Snapshot top - (caller.base + call.a).
6919 if let Some(last) = rec.ops.last_mut()
6920 && matches!(last.inst.op(), crate::vm::isa::Op::Call)
6921 && last.inst.c() == 0
6922 && last.var_count.is_none()
6923 && let Some(f) = self.frames.last().and_then(CallFrame::lua)
6924 {
6925 let from = f.base + last.inst.a();
6926 if self.top >= from {
6927 last.var_count = Some(self.top - from);
6928 }
6929 }
6930 // For SetList B=0, snapshot the source
6931 // count = top - A - 1 (mirrors Lua's `n = top - ra
6932 // - 1` from lvm.c OP_SETLIST). Sources are
6933 // R[A+1..top), exclusive top. For Call C=0's
6934 // var_count (the return count = top - A inclusive),
6935 // see the prior-op fix-up above; here we
6936 // initialise the current Call op to None and let
6937 // the fix-up on the next op's push populate it.
6938 let var_count = if matches!(inst.op(), crate::vm::isa::Op::SetList)
6939 && inst.b() == 0
6940 && let Some(f) = self.frames.last().and_then(CallFrame::lua)
6941 {
6942 let from = f.base + inst.a();
6943 if self.top > from {
6944 Some(self.top - from - 1)
6945 } else {
6946 None
6947 }
6948 } else {
6949 None
6950 };
6951 let op = crate::jit::trace::RecordedOp {
6952 proto: cl.proto,
6953 pc,
6954 inst,
6955 inline_depth: depth_u8,
6956 var_count,
6957 };
6958 // Depth>0 Return0/Return1 mirrors
6959 // LuaJIT's `IR_RETF` (lj_record.c:922+ lj_record_ret).
6960 // Captured as a side-channel `RetfRecord` parallel to
6961 // `ops` when `self_link_enabled` is on. The
6962 // down-rec stitch consumes these to guard side-trace
6963 // inlined-frame topology against the recorded shape.
6964 // Gated on the same flag as the cycle catch so the
6965 // ship-default path (p16 off) sees zero behavior
6966 // change. `caller_pc` is the recorded enclosing Call's
6967 // pc + 1 — interp's resume point after the inlined
6968 // frame pops.
6969 if self.jit.self_link_enabled
6970 && depth_u8 > 0
6971 && matches!(
6972 inst.op(),
6973 crate::vm::isa::Op::Return0 | crate::vm::isa::Op::Return1
6974 )
6975 {
6976 let results: u8 = match inst.op() {
6977 crate::vm::isa::Op::Return0 => 0,
6978 crate::vm::isa::Op::Return1 => 1,
6979 _ => 0,
6980 };
6981 // Most recent Op::Call recorded at the caller's
6982 // depth (`depth_u8 - 1`) is the frame this Return
6983 // is unwinding from. Reverse scan stops at the
6984 // first match.
6985 let caller_depth = depth_u8 - 1;
6986 let caller_call = rec.ops.iter().rev().find(|r| {
6987 r.inline_depth == caller_depth
6988 && matches!(r.inst.op(), crate::vm::isa::Op::Call)
6989 });
6990 let caller_pc = caller_call.map(|r| r.pc + 1).unwrap_or(pc);
6991 // Capture the caller's proto
6992 // for the RetfRecord. LuaJIT `IR_RETF.op1`
6993 // equivalent. For fib(28) the caller's proto
6994 // equals the trace head; for future mutual
6995 // recursion the recorded Op::Call's proto is the
6996 // right target. Fallback to head_proto when no
6997 // enclosing Call op was captured (mirrors
6998 // `caller_pc`'s fallback to the Return's own pc).
6999 let caller_proto = caller_call.map(|r| r.proto).unwrap_or(rec.head_proto);
7000 rec.retfs.push(crate::jit::trace::RetfRecord {
7001 from_depth: depth_u8,
7002 to_depth: caller_depth,
7003 results,
7004 caller_pc,
7005 proto: caller_proto,
7006 });
7007 // DownRec close trigger:
7008 // count RetfRecords on this recording whose
7009 // `proto` matches `caller_proto` (LuaJIT
7010 // `check_downrec_unroll` chain filter
7011 // `op1 == ptref`). Threshold mirrors
7012 // RECUNROLL_THRESHOLD; first trip stamps the
7013 // `downrec_close` marker, subsequent retfs
7014 // keep the marker without overwrite. The
7015 // lowerer's end_idx picker routes through
7016 // TraceEnd::DownRec when the marker is set.
7017 if rec.downrec_close.is_none() {
7018 let caller_proto_ptr = caller_proto.as_ptr();
7019 let prior_match_count = rec
7020 .retfs
7021 .iter()
7022 .filter(|r| r.proto.as_ptr() == caller_proto_ptr)
7023 .count();
7024 // Strictly-greater-than threshold matches
7025 // LuaJIT `count + J->tailcalled > recunroll`.
7026 // The newly-pushed retf is already counted.
7027 if prior_match_count > crate::jit::trace::RECUNROLL_THRESHOLD {
7028 rec.downrec_close = Some(crate::jit::trace::DownRecClose {
7029 return_pc: caller_pc,
7030 target_proto: caller_proto,
7031 depth_delta: 1,
7032 });
7033 // Close-cause taxonomy: tag the
7034 // restart with `"downrec-restart"`. The
7035 // lowerer adds `"downrec-stitch-failed"` when
7036 // the lifted back-edge falls back to
7037 // deopt.
7038 self.jit.counters.bump_close_cause("downrec-restart");
7039 }
7040 }
7041 }
7042 // Capture FieldIcSnapshot for the
7043 // FIRST eligible Op::GetField site under env-gate
7044 // LUNA_JIT_FIELD_IC=1. "Eligible" means:
7045 // - R[B] is Value::Table with metatable.is_none()
7046 // - K[C] is Value::Str
7047 // - The string key actually occupies a hash slot
7048 // (so the IC's slot_idx is a real index, not
7049 // a probe sentinel).
7050 // Once captured, subsequent GetFields skip this
7051 // logic (rec.field_ic_snapshot.is_some() short-
7052 // circuits). Env-OFF short-circuits on the cached
7053 // atomic check inside field_ic_enabled().
7054 if rec.field_ic_snapshot.is_none()
7055 && matches!(inst.op(), crate::vm::isa::Op::GetField)
7056 && crate::jit::trace_types::field_ic_enabled()
7057 {
7058 let b = inst.b();
7059 let c_idx = inst.c() as usize;
7060 let r_b = self.stack[(base + b) as usize];
7061 if let Value::Table(g) = r_b
7062 && g.metatable().is_none()
7063 && c_idx < cl.proto.consts.len()
7064 && let Value::Str(s) = cl.proto.consts[c_idx]
7065 {
7066 let key = Value::Str(s);
7067 let tbl_ref = &*g;
7068 if let Some(slot_idx) = tbl_ref.find_node_idx(key)
7069 && let Some(val) = tbl_ref.node_val_at(slot_idx)
7070 {
7071 let op_idx = rec.ops.len() as u32;
7072 rec.field_ic_snapshot =
7073 Some(crate::jit::trace_types::FieldIcSnapshot {
7074 op_idx,
7075 nodes_len: tbl_ref.nodes_capacity() as u64,
7076 slot_idx: slot_idx as u64,
7077 key_ptr_bits: s.as_ptr() as u64,
7078 cached_val_tag: val.tag_byte(),
7079 });
7080 self.jit.counters.field_ic_snapshot_captured += 1;
7081 }
7082 }
7083 }
7084 if !rec.push(op) {
7085 // Recorder overflow (MAX_TRACE_LEN). Tag it
7086 // explicitly under the close-cause bucket so
7087 // probes can tally overflow vs other abort
7088 // causes in O(1).
7089 self.jit.active_trace = None;
7090 self.jit.counters.aborted += 1;
7091 self.jit.counters.bump_close_cause("trace-overflow");
7092 }
7093 }
7094 }
7095
7096 // Trace JIT dispatcher.
7097 //
7098 // When the dispatch loop is about to execute the op at
7099 // `pc` and there's a `numeric_only` CompiledTrace cached
7100 // for that `head_pc`, marshal the live regs into an
7101 // i64 buffer, jump into the trace, and resume the
7102 // interpreter at the returned continuation PC.
7103 //
7104 // Skipped (zero overhead) when `trace_jit_enabled` is
7105 // false; the lookup is a borrow + scan over
7106 // `cl.proto.traces`, which is a `Vec` whose size is at
7107 // most one entry per back-edge per Proto in practice.
7108 //
7109 // Marshalling contract — only Int slots survive the
7110 // round-trip cleanly (the reg_state ABI is `*mut i64`
7111 // with no tag info). Any non-Int slot in the affected
7112 // window forces a skip; interp takes over for one op
7113 // and the back-edge brings us back to try again next
7114 // pass (slots that were Nil/Float at one moment can
7115 // settle to Int by the time the next back-edge fires).
7116 //
7117 // A trace that comes back with `vm.jit.pending_err`
7118 // parked is treated as a deopt: clear the err, leave
7119 // the stack as the trace wrote it, and let the
7120 // interpreter run from the same `pc`. The trace itself
7121 // is left cached — a future entry might find no
7122 // metatable in the way and succeed.
7123 // Single Rc<CompiledTrace> clone instead of per-field Rc
7124 // clones: proto.traces is Vec<Rc<CompiledTrace>>; the
7125 // dispatcher clones ONE Rc and reads fields via auto-deref.
7126 // One-shot consume of the
7127 // `suppress_downrec_admit_once` flag. Set by the
7128 // downrec post-invoke arm below when it force-deopts the
7129 // trace (caller-pc guard miss OR cycle-budget exhausted)
7130 // so the NEXT interpreter loop iteration skips the
7131 // downrec admit, lets interp run the op at `head_pc`,
7132 // advances `pc` past `head_pc`, and breaks the otherwise-
7133 // infinite admit loop. Reading + clearing here means a
7134 // single dispatch tick consumes the suppression — the
7135 // following tick re-admits naturally (with the budget
7136 // also reset by the deopt site).
7137 let downrec_admit_blocked = self.jit.suppress_downrec_admit_once;
7138 self.jit.suppress_downrec_admit_once = false;
7139 if self.jit.trace_enabled
7140 && let Some(ct) = {
7141 let traces = cl.proto.traces.borrow();
7142 traces
7143 .iter()
7144 .find(|t| {
7145 if t.head_pc != pc {
7146 return false;
7147 }
7148 let is_downrec = t.downrec_link.is_some();
7149 // The one-shot suppress
7150 // flag blocks any admit (primary or fallback)
7151 // for `downrec_link`-bearing traces so the
7152 // next interp iter can run the natural op
7153 // at `head_pc` and advance past it. The multi-way
7154 // `dispatchable=true` lift means the suppress
7155 // must also cover the primary `t.dispatchable`
7156 // arm — otherwise the lifted lookup would
7157 // immediately re-admit after a force-deopt
7158 // and the infinite loop returns.
7159 if downrec_admit_blocked {
7160 return false;
7161 }
7162 // Primary arm: `dispatchable=true` traces
7163 // (lifted multi-way DownRec or normal traces).
7164 // Fallback arm: single-CMP `dispatchable=false`
7165 // DownRec traces (single-CMP guard kept
7166 // pinned because the 90% miss-rate would
7167 // make blind admit perf-negative).
7168 t.dispatchable || is_downrec
7169 })
7170 .cloned()
7171 }
7172 {
7173 // Borrow Rc<[T]> fields as &Rc<[T]> instead
7174 // of cloning. The outer `ct: Rc<CompiledTrace>` is held
7175 // across the entire dispatch block so the fields outlive
7176 // all consumers.
7177 let entry_fn = ct.entry;
7178 let head_pc_val = ct.head_pc;
7179 let window_size = ct.window_size;
7180 let exit_tags = &ct.exit_tags;
7181 let per_exit_tags = &ct.per_exit_tags;
7182 let per_exit_inline = &ct.per_exit_inline;
7183 let compile_entry_tags = &ct.entry_tags;
7184 let global_tag_res_kind = ct.global_tag_res_kind;
7185 let exit_hit_counts = &ct.exit_hit_counts;
7186 let max_stack = cl.proto.max_stack as usize;
7187 let window_size_us = window_size as usize;
7188 let base_us = base as usize;
7189 // `reg_state` sized to the trace's `window_size`, which
7190 // may exceed max_stack.
7191 // Marshal-in still only writes [0..max_stack); slots
7192 // [max_stack..window_size) are zero-initialised and
7193 // filled by the trace's own GetUpval / arith.
7194 // Reuse the Vm's amortised buffers
7195 // instead of allocating fresh Vecs each dispatch.
7196 // mem::take leaves an empty placeholder we restore
7197 // at the end of the dispatch block (success +
7198 // deopt paths both fall through to the restore).
7199 let mut entry_tags: Vec<u8> = std::mem::take(&mut self.jit.entry_tags_buf);
7200 entry_tags.clear();
7201 entry_tags.reserve(max_stack);
7202 // This trace was admitted via the
7203 // `downrec_link.is_some()` arm rather than the normal
7204 // `dispatchable=true` arm. The pre-invoke path
7205 // populates a reserved saved-PC slot just past the
7206 // normal register window so the lowerer's guard load
7207 // (`reg_state[window_size]`) compares the runtime
7208 // saved caller PC against the recorded `dr_return_pc`.
7209 //
7210 // No `!ct.dispatchable` gate: when the lowerer lifts
7211 // `dispatchable = true` for multi-way guards, the
7212 // trace's body still emits the downrec sentinel shape
7213 // on return — the saved-PC slot
7214 // and post-invoke classifier must keep firing.
7215 // `downrec_link.is_some()` is the unique structural
7216 // signal that the trace closes via DownRec.
7217 let is_downrec_entry = ct.downrec_link.is_some();
7218 let mut reg_state: Vec<i64> = std::mem::take(&mut self.jit.reg_state_buf);
7219 reg_state.clear();
7220 // When admitting a downrec trace,
7221 // size the buffer to `window_size + 1` so the lowerer
7222 // can `load(I64, ..., reg_state, window_size * 8)`
7223 // for the saved caller PC guard input. The extra slot
7224 // is the LAST element so cranelift's existing
7225 // `0..window_size` accesses are unaffected.
7226 let reg_state_len = if is_downrec_entry {
7227 window_size_us + 1
7228 } else {
7229 window_size_us
7230 };
7231 reg_state.resize(reg_state_len, 0i64);
7232 let mut dispatch_ok = true;
7233 for i in 0..max_stack {
7234 let v = self.stack[base_us + i];
7235 let (tag, raw) = v.unpack();
7236 entry_tags.push(tag);
7237 // Entry tag guard. The trace's IR
7238 // is specialised to the compile-time entry tags
7239 // (via current_kinds propagation from
7240 // from_entry_tag). A runtime tag mismatch means
7241 // body ops would mis-interpret raw bits (e.g.
7242 // treat a Str pointer as Int payload → garbage).
7243 // Skip dispatch on mismatch so interp handles
7244 // this entry shape; the trace stays cached for
7245 // future entries that match.
7246 if i < compile_entry_tags.len() && tag != compile_entry_tags[i] {
7247 dispatch_ok = false;
7248 break;
7249 }
7250 match tag {
7251 // Int / Float / Table / Nil all marshal
7252 // to raw payload cleanly; the trace's IR
7253 // treats the 8-byte slot as an i64 (with
7254 // f64 ops bitcasting around the boundary).
7255 crate::runtime::value::raw::INT
7256 | crate::runtime::value::raw::FLOAT
7257 | crate::runtime::value::raw::TABLE
7258 | crate::runtime::value::raw::CLOSURE
7259 // Native iter slots (e.g.
7260 // R[A] = ipairs_iter) are present in
7261 // generic-for traces; the raw bits are a
7262 // valid `*mut NativeClosure` and round-trip
7263 // cleanly.
7264 | crate::runtime::value::raw::NATIVE
7265 // Str slots show up in
7266 // string-concat traces; raw bits = `*mut
7267 // LuaStr` (interned, GC-managed). Round-
7268 // trips cleanly as a heap pointer.
7269 | crate::runtime::value::raw::STR
7270 | crate::runtime::value::raw::NIL => {
7271 // 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).
7272 reg_state[i] = unsafe { raw.zero as i64 };
7273 }
7274 _ => {
7275 dispatch_ok = false;
7276 break;
7277 }
7278 }
7279 }
7280
7281 if dispatch_ok {
7282 debug_assert_eq!(head_pc_val, pc, "trace cache hit's head_pc != pc");
7283 // A recording in progress cannot see what the trace runs
7284 // natively: it would resume after the trace with ops
7285 // missing, and could close as a loop that never ran (a
7286 // side trace of two ops returning its own head, which
7287 // the dispatcher then entered forever). Drop it.
7288 if self.jit.active_trace.take().is_some() {
7289 self.jit.counters.aborted += 1;
7290 self.jit.counters.bump_close_cause("reached-compiled-trace");
7291 }
7292 self.jit.pending_err = None;
7293 // Snapshot the pre-entry frame
7294 // count. A cmp@d>0 side-exit calls the materialize
7295 // helper which pushes inlined frames onto
7296 // `vm.frames`; on deopt those frames must be popped
7297 // before falling through to the interpreter, else
7298 // the stack grows unboundedly per deopted dispatch.
7299 let pre_frames = self.frames.len();
7300 // Saved-PC slot population. The
7301 // recorded `dr_return_pc` on the closing trace is
7302 // the caller's resume PC captured at a depth>0
7303 // Return push (recorder push site). The natural runtime analogue for self-
7304 // stitch is the dispatching frame's PARENT frame's
7305 // PC: the trace's head_pc sits inside a Lua frame,
7306 // and the parent (caller) frame's `pc` is what
7307 // luna would observe as `[base-8]` in the LJ
7308 // `asm_retf` shape (`lj_asm_arm64.h:565`). When
7309 // the parent isn't a Lua frame (top-level dispatch
7310 // — first invocation through `call_value`), no
7311 // saved PC exists; we write 0, which always
7312 // mismatches the recorded `dr_return_pc != 0`
7313 // invariant (debug-asserted in the luna-jit trace
7314 // lowerer).
7315 if is_downrec_entry {
7316 let saved_pc: i64 = if pre_frames >= 2 {
7317 match &self.frames[pre_frames - 2] {
7318 CallFrame::Lua(parent) => parent.pc as i64,
7319 CallFrame::Cont(_) => 0,
7320 }
7321 } else {
7322 0
7323 };
7324 reg_state[window_size_us] = saved_pc;
7325 }
7326 // `LUNA_AOT_PROBE`
7327 // diagnostic hook. The probe fires once per trace dispatch
7328 // (regardless of JIT vs AOT origin — both go through this
7329 // arm), letting the AOT smoke test verify mcode actually
7330 // executed. Guarded behind `OnceLock` so the env read is
7331 // a one-time cost per process; not gated on a particular
7332 // counter so the smoke test gets a deterministic single-
7333 // line `aot_trace_fired pc=N` per first dispatch.
7334 if jit_probe_enabled() && self.jit.counters.dispatched == 0 {
7335 eprintln!("luna-runtime-helpers: aot_trace_fired pc={head_pc_val}");
7336 }
7337 let continuation_pc = {
7338 // chunk_compiler.enter
7339 // (CraneliftBackend delegates to enter_jit;
7340 // NullJitBackend returns an inert guard).
7341 let vm_ptr: *mut Vm = self;
7342 let _guard = self.jit.chunk_compiler.enter(vm_ptr, Some(cl));
7343 // 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).
7344 unsafe { entry_fn(reg_state.as_mut_ptr()) }
7345 };
7346 self.jit.counters.dispatched += 1;
7347
7348 if self.jit.pending_err.is_some() {
7349 self.jit.pending_err = None;
7350 self.jit.counters.deopt += 1;
7351 // Unwind any helper-pushed
7352 // inlined frames before the interpreter resumes.
7353 // Don't restore reg_state — the trace's partial
7354 // writes are discarded; interp re-executes from
7355 // the original `pc`.
7356 while self.frames.len() > pre_frames {
7357 frames_pop_sync(&mut self.frames, &mut self.frames_top);
7358 }
7359 if is_downrec_entry {
7360 // pending_err observed
7361 // mid-trace inside a downrec admit. Treat
7362 // it as a guard miss: bump `downrec_deopt`
7363 // and suppress the next downrec admit so
7364 // interp can advance past `head_pc` and
7365 // the same trace doesn't immediately re-
7366 // fire on the next loop iteration.
7367 self.jit.counters.downrec_deopt += 1;
7368 self.jit.suppress_downrec_admit_once = true;
7369 }
7370 } else if is_downrec_entry && {
7371 // Only enter the
7372 // downrec classifier for returns whose shape
7373 // matches the lowerer's `downrec_idx_opt` tail
7374 // emit: either the stitch_blk DOWNREC sentinel
7375 // (HIT) or the deopt_blk GLOBAL-sentinel-with-
7376 // body==head_pc (MISS via guard fail). Any
7377 // other return from a downrec trace (intermediate
7378 // body cmp side-exit, GetField inference fail,
7379 // etc.) carries a different sentinel/body shape
7380 // and means the body exited BEFORE reaching the
7381 // downrec close — classify those through the
7382 // normal decode path (else branch below) so
7383 // reg_state restores + pc advances correctly.
7384 // Classifying them all as MISS would skip the
7385 // normal restore, inflating `downrec_deopt` with
7386 // non-downrec events and losing the trace's
7387 // mid-flight writes.
7388 let raw_ret = continuation_pc as u64;
7389 let from_side_trace = (raw_ret >> 63) & 1 == 1;
7390 let sentinel_code = if from_side_trace {
7391 ((raw_ret >> 56) & 0x7F) as u32
7392 } else {
7393 0
7394 };
7395 let raw_body = raw_ret & 0x00FF_FFFF_FFFF_FFFFu64;
7396 let global_deopt_code = crate::jit::trace_types::encode_side_sentinel(
7397 crate::jit::trace_types::SIDE_SENT_KIND_GLOBAL,
7398 0,
7399 );
7400 from_side_trace
7401 && (crate::jit::trace_types::is_downrec_sentinel(sentinel_code)
7402 || (sentinel_code == global_deopt_code
7403 && raw_body == head_pc_val as u64))
7404 } {
7405 // Downrec event classifier.
7406 let raw_ret = continuation_pc as u64;
7407 let sentinel_code = ((raw_ret >> 56) & 0x7F) as u32;
7408 if crate::jit::trace_types::is_downrec_sentinel(sentinel_code) {
7409 // Guard HIT — saved_pc matched one of the
7410 // baked candidates and the trace's
7411 // `stitch_blk` arm returned the DOWNREC
7412 // sentinel. Cycle-safety checkpoint:
7413 // decrement budget; on underflow,
7414 // reclassify as deopt + reset budget.
7415 // `STITCH_DEPTH_DEFAULT = 32` lets
7416 // ~all natural HITs in a hot loop fire
7417 // before reset pressure.
7418 if self.jit.stitch_depth_remaining > 0 {
7419 self.jit.stitch_depth_remaining -= 1;
7420 self.jit.counters.downrec_dispatched += 1;
7421 } else {
7422 self.jit.counters.downrec_deopt += 1;
7423 self.jit.stitch_depth_remaining =
7424 crate::vm::jit_state::JitState::STITCH_DEPTH_DEFAULT;
7425 }
7426 } else {
7427 // Guard MISS via the lowerer's deopt_blk
7428 // arm (GLOBAL sentinel + body == head_pc).
7429 // The deopt_blk emit performs the
7430 // store-back via `emit_store_back_and_return_pc`,
7431 // so the live stack already reflects the
7432 // body's writes; no extra restore needed
7433 // from the dispatcher side.
7434 self.jit.counters.downrec_deopt += 1;
7435 }
7436 self.jit.suppress_downrec_admit_once = true;
7437 // Pop helper-pushed inlined frames (defensive —
7438 // the downrec emit shape doesn't push frames in the
7439 // tail, but a body side-exit before reaching
7440 // the tail may have via the materialize helper).
7441 while self.frames.len() > pre_frames {
7442 frames_pop_sync(&mut self.frames, &mut self.frames_top);
7443 }
7444 self.jit.reg_state_buf = reg_state;
7445 self.jit.entry_tags_buf = entry_tags;
7446 continue;
7447 } else {
7448 // Restore each slot using the trace's
7449 // exit-tag analysis (see ExitTag docs).
7450 // Decode the IR's
7451 // side-exit shape. Upper 32 bits = (site_idx
7452 // + 1) for inline cmp side-exits, 0 for
7453 // legacy clean-tail / non-inline exits.
7454 // The decode lives in
7455 // `crate::jit::trace::decode_exit_shape` so
7456 // side-trace returns can reuse it with the SIDE
7457 // TRACE's shape inputs when the sentinel bit
7458 // is set on `raw_ret`.
7459 let raw_ret = continuation_pc as u64;
7460 // Side-trace return decode.
7461 // Bit 63 of `raw_ret` is the side-trace
7462 // marker the parent's IR OR'd in when it
7463 // tail-called into a wired child trace.
7464 // Bits 56..=62 carry the sentinel code (the
7465 // cache key into the parent's
7466 // `side_trace_cache`); bits 0..=55 are the
7467 // child's own return value (encoded site or
7468 // plain cont_pc) which we MUST decode using
7469 // the CHILD's per_exit_inline / per_exit_tags
7470 // / exit_tags / exit_hit_counts — not the
7471 // parent's. The dispatcher snapshot read
7472 // above holds the parent's shapes; when bit
7473 // 63 is set we re-fetch the child's via the
7474 // sentinel-keyed cache.
7475 let from_side_trace = (raw_ret >> 63) & 1 == 1;
7476 let (
7477 decode_inline,
7478 decode_tags,
7479 decode_exit_tags,
7480 decode_hit_counts,
7481 decode_body,
7482 child_ran,
7483 ) = if from_side_trace {
7484 let sentinel_code = ((raw_ret >> 56) & 0x7F) as u32;
7485 let body = raw_ret & 0x00FF_FFFF_FFFF_FFFFu64;
7486 let traces = cl.proto.traces.borrow();
7487 let child_idx = traces
7488 .iter()
7489 .find(|t| t.head_pc == head_pc_val)
7490 .and_then(|pct| {
7491 pct.side_trace_cache.borrow().get(&sentinel_code).copied()
7492 });
7493 if let Some(idx) = child_idx
7494 && let Some(child) = traces.get(idx as usize)
7495 {
7496 if crate::jit::trace::v2c_probe_enabled() {
7497 eprintln!(
7498 "[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={}",
7499 sentinel_code,
7500 body,
7501 idx,
7502 child.n_ops,
7503 child.head_pc,
7504 child.window_size,
7505 pc,
7506 window_size,
7507 child.dispatchable,
7508 child.is_inline_abort_close,
7509 );
7510 }
7511 (
7512 child.per_exit_inline.clone(),
7513 child.per_exit_tags.clone(),
7514 child.exit_tags.clone(),
7515 child.exit_hit_counts.clone(),
7516 body,
7517 true,
7518 )
7519 } else {
7520 if crate::jit::trace::v2c_probe_enabled() {
7521 eprintln!(
7522 "[v2c-A3-decode] sentinel={:#04x} body={:#018x} child MISS (fallback parent shapes)",
7523 sentinel_code, body,
7524 );
7525 }
7526 // Cache miss — fall back to parent
7527 // shapes with the body bits. Best-
7528 // effort; the trace_side_trace_
7529 // shape_mismatch_count records this
7530 // path indirectly (close-handler
7531 // skips wiring on mismatch so we
7532 // shouldn't reach here when shape
7533 // gate held).
7534 (
7535 per_exit_inline.clone(),
7536 per_exit_tags.clone(),
7537 exit_tags.clone(),
7538 exit_hit_counts.clone(),
7539 body,
7540 true,
7541 )
7542 }
7543 } else {
7544 // Dispatcher-level side-trace invocation,
7545 // rather than an IR gate (`load + icmp +
7546 // brif`) at every emit_store_back callsite,
7547 // which measured as a net slowdown. The
7548 // tentative decode + cell load always runs:
7549 // short-circuiting it on a
7550 // `parent_has_side` hint measured slower on
7551 // btrees_d8 and no faster on fib_10.
7552 {
7553 let tentative = crate::jit::trace::decode_exit_shape(
7554 raw_ret,
7555 per_exit_inline,
7556 per_exit_tags,
7557 exit_tags,
7558 );
7559 let tentative_exit_idx = tentative.exit_hit_idx;
7560 let child_invoke = {
7561 let traces = cl.proto.traces.borrow();
7562 traces.iter().find(|t| t.head_pc == head_pc_val).and_then(
7563 |pct| {
7564 let cell =
7565 pct.exit_side_trace_ptrs.get(tentative_exit_idx)?;
7566 let fn_ptr = cell.get();
7567 if fn_ptr.is_null() {
7568 return None;
7569 }
7570 traces
7571 .iter()
7572 .find(|t| {
7573 t.entry as *const () as *const u8 == fn_ptr
7574 })
7575 .map(|child| {
7576 (
7577 child.entry,
7578 child.per_exit_inline.clone(),
7579 child.per_exit_tags.clone(),
7580 child.exit_tags.clone(),
7581 child.exit_hit_counts.clone(),
7582 )
7583 })
7584 },
7585 )
7586 };
7587 if let Some((cent, cpi, cpt, cet, chc)) = child_invoke {
7588 let child_raw_ret = {
7589 // chunk_compiler.enter
7590 // (side-trace entry).
7591 let vm_ptr: *mut Vm = self;
7592 let _guard =
7593 self.jit.chunk_compiler.enter(vm_ptr, Some(cl));
7594 // 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).
7595 unsafe { cent(reg_state.as_mut_ptr()) }
7596 };
7597 (cpi, cpt, cet, chc, child_raw_ret as u64, true)
7598 } else {
7599 (
7600 per_exit_inline.clone(),
7601 per_exit_tags.clone(),
7602 exit_tags.clone(),
7603 exit_hit_counts.clone(),
7604 raw_ret,
7605 false,
7606 )
7607 }
7608 }
7609 };
7610 let decoded = crate::jit::trace::decode_exit_shape(
7611 decode_body,
7612 &decode_inline,
7613 &decode_tags,
7614 &decode_exit_tags,
7615 );
7616 let site_id = decoded.site_id;
7617 let cont_pc = decoded.cont_pc;
7618 let exit_hit_idx = decoded.exit_hit_idx;
7619 let exit_tags_for_pc = decoded.exit_tags_for_pc;
7620 // When a side trace ran (tail-called by the
7621 // parent's code or invoked here), force
7622 // using_global_exit_tags=false so the restore
7623 // loop takes the per-tag slow path:
7624 // `global_tag_res_kind` classifies the parent's
7625 // exit tags, not the child's.
7626 let using_global_exit_tags = if child_ran {
7627 false
7628 } else {
7629 decoded.using_global_exit_tags
7630 };
7631 // Increment the counter (saturate
7632 // at u32::MAX to avoid wrap on long runs).
7633 // Track whether this increment is
7634 // the one that crossed `HOTEXIT_THRESHOLD`
7635 // (transition: previous v < threshold, new v
7636 // == threshold). The side-trace start is
7637 // deferred to just before `continue;` so
7638 // vm.stack and frame.pc are fully restored
7639 // (the snapshot reads post-restore values).
7640 let mut side_trace_should_start = false;
7641 // For side-trace returns the
7642 // counter to bump is the CHILD's (decoded
7643 // shape lookup) — `exit_hit_idx` is into the
7644 // decoded layout, so use the matching
7645 // `decode_hit_counts`. For parent decode
7646 // they're aliased (clone of the parent's
7647 // own Rc).
7648 if let Some(c) = decode_hit_counts.get(exit_hit_idx) {
7649 let v = c.get();
7650 if v < u32::MAX {
7651 c.set(v + 1);
7652 }
7653 // After a side trace ran, `exit_hit_idx` is an
7654 // exit of that child, but a side trace is
7655 // recorded and wired as one of `head_pc_val`'s
7656 // exits: it would replace the side trace on the
7657 // parent's exit of that number, which resumes
7658 // elsewhere.
7659 if v + 1 == crate::jit::trace::HOTEXIT_THRESHOLD
7660 && !child_ran
7661 && self.jit.active_trace.is_none()
7662 && self.jit.trace_enabled
7663 {
7664 side_trace_should_start = true;
7665 }
7666 }
7667 // At an inline cmp@d>0
7668 // side-exit, the helper has pushed N frames on
7669 // top of the trace head's frame and
7670 // `exit_tags_for_pc.len()` covers the full
7671 // window (caller + each inlined frame's
7672 // window). Slots beyond `max_stack` belong to
7673 // an inlined frame: their `Untouched` entries
7674 // default to Nil (no entry-tag fallback —
7675 // marshal-in only captured caller slots) and
7676 // we write to interp stack at `base + i` which
7677 // mirrors `op_offsets`-derived layout.
7678 let slot_count = exit_tags_for_pc.len();
7679 // The helper only extends
7680 // vm.stack up to the deepest pushed frame's
7681 // window, but the exit_tags snapshot covers
7682 // the trace's full `window_size` (which
7683 // includes depth-N+1 scratch slots that the
7684 // trace's IR may have written without a
7685 // matching pushed frame). Extend with Nil so
7686 // the write at the tail doesn't panic; these
7687 // slots get overwritten by the writeback loop
7688 // and won't leak meaningful data past the
7689 // pushed frames' R[0..max_stack) windows.
7690 if self.stack.len() < base_us + slot_count {
7691 self.stack
7692 .resize(base_us + slot_count, crate::runtime::Value::Nil);
7693 }
7694 // Fast-path restore loop. When
7695 // we landed on the global `exit_tags`,
7696 // dispatch on the compile-time
7697 // classification: skip the loop entirely
7698 // for `AllUntouched`, do a tag-free
7699 // `Value::Int(...)` write per slot for
7700 // `AllInt`, otherwise fall through to the
7701 // general match-arm loop. site_id > 0
7702 // (inline frame mat) and per_exit_tags
7703 // hits always take the general path —
7704 // their per-side-exit shapes aren't
7705 // pre-classified yet.
7706 // A generic-for exit whose TForCall wrote the loop
7707 // variables to the stack with tags the trace did not
7708 // compile for: leave those slots as they are.
7709 let keep_tfor =
7710 if decode_body & crate::jit::trace_types::EXIT_KEEP_TFOR_VARS != 0 {
7711 let call = cl.proto.code[cont_pc as usize - 1];
7712 debug_assert!(matches!(call.op(), crate::vm::isa::Op::TForCall));
7713 let first = call.a() as usize + 4;
7714 first..first + call.c() as usize
7715 } else {
7716 0..0
7717 };
7718 let fast_path_taken = if using_global_exit_tags && keep_tfor.is_empty() {
7719 match global_tag_res_kind {
7720 crate::jit::trace::TagResKind::AllUntouched => {
7721 // No-op: vm.stack already
7722 // matches the trace's post-
7723 // entry state for these
7724 // slots (entry values not
7725 // overridden, or already
7726 // spilled by helpers).
7727 true
7728 }
7729 crate::jit::trace::TagResKind::AllInt => {
7730 for i in 0..slot_count {
7731 self.stack[base_us + i] =
7732 crate::runtime::Value::Int(reg_state[i]);
7733 }
7734 true
7735 }
7736 crate::jit::trace::TagResKind::Mixed => false,
7737 }
7738 } else {
7739 false
7740 };
7741 if !fast_path_taken {
7742 for i in 0..slot_count {
7743 if keep_tfor.contains(&i) {
7744 continue;
7745 }
7746 let tag = match exit_tags_for_pc[i] {
7747 crate::jit::trace::ExitTag::Untouched => {
7748 if i < max_stack {
7749 entry_tags[i]
7750 } else {
7751 crate::runtime::value::raw::NIL
7752 }
7753 }
7754 crate::jit::trace::ExitTag::Int => {
7755 crate::runtime::value::raw::INT
7756 }
7757 crate::jit::trace::ExitTag::Float => {
7758 crate::runtime::value::raw::FLOAT
7759 }
7760 crate::jit::trace::ExitTag::Table => {
7761 crate::runtime::value::raw::TABLE
7762 }
7763 crate::jit::trace::ExitTag::Closure => {
7764 crate::runtime::value::raw::CLOSURE
7765 }
7766 // Trace actively wrote Nil
7767 // to this slot (e.g. via Op::LoadNil).
7768 // Restore as Nil regardless of the entry
7769 // tag, since the i64 payload is 0 and
7770 // packing as the entry tag (e.g. INT)
7771 // would mis-type the slot.
7772 crate::jit::trace::ExitTag::Nil => {
7773 crate::runtime::value::raw::NIL
7774 }
7775 // Trace wrote a Str ptr
7776 // to this slot (LoadK Str / Move from
7777 // Str / Concat result). Restore as
7778 // Value::Str with raw bits round-
7779 // tripped.
7780 crate::jit::trace::ExitTag::Str => {
7781 crate::runtime::value::raw::STR
7782 }
7783 };
7784 // SAFETY: tag is from a verified slot
7785 // (entry validated above) or pinned by
7786 // the exit-tag analysis to INT/TABLE.
7787 // The raw payload sits in reg_state[i].
7788 // Stack was extended by the materialize
7789 // helper for inline frames.
7790 // 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).
7791 self.stack[base_us + i] = unsafe {
7792 Value::pack(
7793 tag,
7794 crate::runtime::value::RawVal {
7795 zero: reg_state[i] as u64,
7796 },
7797 )
7798 };
7799 }
7800 }
7801 // For non-inline exits the
7802 // helper was never called (no metas chain for
7803 // this cont_pc), so `frames.last()` is the
7804 // trace head's frame and we set its pc to
7805 // cont_pc as before. For inline exits the
7806 // helper baked the side-exit PC into the
7807 // innermost frame's `pc` at push time
7808 // (chain.last().pc was overridden at emit),
7809 // so this assignment to `frames.last_mut().pc
7810 // = cont_pc` is a redundant-but-correct
7811 // confirmation.
7812 let _ = &per_exit_inline; // hold the Rc alive across dispatch
7813 // For inline side-exits the
7814 // helper has pushed N frames on top. The trace
7815 // head frame is at `pre_frames - 1`; set its
7816 // pc to `head_resume_pc` so when the chain
7817 // eventually pops back to it, interp resumes
7818 // PAST the trace's depth-0 Op::Call instead of
7819 // restarting from `head_pc` and re-triggering
7820 // dispatch (infinite loop). The innermost
7821 // (helper-pushed) frame already has its pc
7822 // baked in at compile time, but we still
7823 // assign `cont_pc` below for parity with the
7824 // non-inline path (no-op).
7825 if site_id > 0 {
7826 let idx = (site_id - 1) as usize;
7827 let head_resume_pc = decode_inline[idx].head_resume_pc;
7828 if pre_frames > 0
7829 && let CallFrame::Lua(f) = &mut self.frames[pre_frames - 1]
7830 {
7831 f.pc = head_resume_pc;
7832 }
7833 }
7834 let frames_len_now = self.frames.len();
7835 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
7836 match unsafe { self.frames.last_mut().unwrap_unchecked() } {
7837 CallFrame::Lua(fmut) => {
7838 if crate::jit::trace::v2c_probe_enabled() {
7839 eprintln!(
7840 "[v2c-set-pc] from_side={} sentinel_or_raw={:#018x} prev_pc={} new_cont_pc={} site_id={} frames.len={} pre_frames={} max_stack={}",
7841 from_side_trace,
7842 raw_ret,
7843 fmut.pc,
7844 cont_pc,
7845 site_id,
7846 frames_len_now,
7847 pre_frames,
7848 max_stack,
7849 );
7850 }
7851 fmut.pc = cont_pc;
7852 }
7853 _ => unreachable!("Cont frame at trace dispatch"),
7854 }
7855 // Deferred side-trace start. The
7856 // increment block above flagged this exit's
7857 // hit count crossing HOTEXIT_THRESHOLD; now
7858 // that vm.stack is restored and frame.pc is
7859 // settled, snapshot entry_tags from the
7860 // resume frame's window and create the
7861 // recorder. The recorder's first push fires
7862 // on the next interp iteration at cont_pc.
7863 //
7864 // `head_proto` for the side trace = cl.proto
7865 // (trace JIT only inlines self-recursive
7866 // calls today, so cont_pc always lands in
7867 // the same proto as the parent). Frame base
7868 // is the resume frame (top of `self.frames`
7869 // — inline-pushed frames moved this).
7870 if side_trace_should_start {
7871 let (resume_base, resume_proto) = match self.frames.last() {
7872 Some(CallFrame::Lua(f)) => (f.base as usize, f.closure.proto),
7873 _ => (base_us, cl.proto),
7874 };
7875 let resume_max_stack = resume_proto.max_stack as usize;
7876 let mut side_entry_tags: Vec<u8> = Vec::with_capacity(resume_max_stack);
7877 // Extend stack if cont_pc's frame window
7878 // overhangs the current stack len (rare,
7879 // but inline-pushed frame stack writes
7880 // only covered the trace's writeback).
7881 if self.stack.len() < resume_base + resume_max_stack {
7882 self.stack.resize(
7883 resume_base + resume_max_stack,
7884 crate::runtime::Value::Nil,
7885 );
7886 }
7887 for i in 0..resume_max_stack {
7888 let (tag, _) = self.stack[resume_base + i].unpack();
7889 side_entry_tags.push(tag);
7890 }
7891 self.jit.active_trace =
7892 Some(Box::new(crate::jit::trace::TraceRecord::start_side_trace(
7893 resume_proto,
7894 cont_pc,
7895 side_entry_tags,
7896 cl.proto,
7897 head_pc_val,
7898 exit_hit_idx,
7899 )));
7900 self.jit.recording_frame_base = self.frames.len() - 1;
7901 self.jit.counters.side_trace_started += 1;
7902 }
7903 // Put the dispatch buffers back
7904 // before the `continue;` so the next
7905 // dispatch picks up the same allocation.
7906 self.jit.reg_state_buf = reg_state;
7907 self.jit.entry_tags_buf = entry_tags;
7908 continue;
7909 }
7910 }
7911 // !dispatch_ok / deopt path / non-cont
7912 // exit also restore the buffers before falling
7913 // through to the interp.
7914 self.jit.reg_state_buf = reg_state;
7915 self.jit.entry_tags_buf = entry_tags;
7916 }
7917
7918 // PUC `vmfetch` increments savedpc BEFORE firing traceexec, so
7919 // hook code that consults `currentpc = savedpc - 1` lands on the
7920 // instruction now executing. luna mirrors that by advancing
7921 // `f.pc` to `pc + 1` before the hook block — local_at /
7922 // getinfo / line attribution all read f.pc, and the existing
7923 // `pc - 1` convention in those helpers then yields the current
7924 // instruction's pc (db.lua :696: local `A` visible at the
7925 // chunk's return line once OP_CLOSURE has advanced pc).
7926 //
7927 // Inline `top_frame_mut` for the hot path: top is guaranteed Lua
7928 // (cont frames drained above) so the and_then/Option layers are
7929 // dead weight.
7930 // 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).
7931 match unsafe { self.frames.last_mut().unwrap_unchecked() } {
7932 CallFrame::Lua(fmut) => fmut.pc = pc + 1,
7933 _ => unreachable!("Cont frame at pc bump"),
7934 }
7935
7936 // count + line hooks (PUC traceexec): before executing the
7937 // instruction. Skipped while the hook itself runs.
7938 // (Parens here are load-bearing — without them `&&` binds tighter
7939 // than `||` and the `!in_hook` guard only gates the rust-hook arm,
7940 // letting a Lua line hook recurse into itself → stack overflow
7941 // on db.lua line-hook assertions. Matches the `hook_call_with` /
7942 // `hook_return` predicate shape at lines 2245 / 2279 / 2294 / 4023.)
7943 if !self.in_hook && (self.hook.func.is_some() || self.hook.rust_func.is_some()) {
7944 let lines = &cl.proto.lines;
7945 let cur_line = if lines.is_empty() {
7946 None
7947 } else {
7948 Some(lines[(pc as usize).min(lines.len() - 1)] as i64)
7949 };
7950 // count hook: fire every `count_base` instructions
7951 if self.hook.count {
7952 self.hook.count_left -= 1;
7953 if self.hook.count_left <= 0 {
7954 self.hook.count_left = self.hook.count_base;
7955 // hooked function is the running Lua frame: its frame
7956 // is on the stack, so no synthetic C level is needed.
7957 self.run_hook(b"count", cur_line, false)?;
7958 }
7959 }
7960 // line hook: fire on a fresh frame, a backward jump (loop), or a
7961 // change of source line.
7962 if self.hook.line {
7963 if lines.is_empty() {
7964 // PUC: a stripped chunk has no line info, so
7965 // `getfuncline` returns -1. The line hook still fires
7966 // on the first instruction of the new frame (where
7967 // `npci <= oldpc` holds at oldpc=0), with the line
7968 // pushed as `nil` instead of an integer (db.lua :1030
7969 // "hook called without debug info for 1st instruction").
7970 if oldpc == u32::MAX {
7971 self.run_hook(b"line", None, false)?;
7972 self.top_frame_mut().hook_oldpc = pc;
7973 }
7974 } else {
7975 let newline = lines[(pc as usize).min(lines.len() - 1)];
7976 // PUC `traceexec`: fire on frame entry (`oldpc == MAX`),
7977 // on a backward jump (`pc < oldpc` — strict; an equal pc
7978 // would re-fire the install-site after `oldpc = pc`),
7979 // or when the source line changes.
7980 let fire = oldpc == u32::MAX
7981 || pc < oldpc
7982 || newline != lines[(oldpc as usize).min(lines.len() - 1)];
7983 if fire {
7984 self.run_hook(b"line", Some(newline as i64), false)?;
7985 }
7986 self.top_frame_mut().hook_oldpc = pc;
7987 }
7988 }
7989 }
7990
7991 match inst.op() {
7992 Op::Move => {
7993 let v = self.r(base, inst.b());
7994 self.set_r(base, inst.a(), v);
7995 }
7996 Op::LoadI => self.set_r(base, inst.a(), Value::Int(inst.sbx() as i64)),
7997 Op::LoadF => self.set_r(base, inst.a(), Value::Float(inst.sbx() as f64)),
7998 Op::LoadK => {
7999 let v = cl.proto.consts[inst.bx() as usize];
8000 self.set_r(base, inst.a(), v);
8001 }
8002 Op::LoadKx => {
8003 let extra = cl.proto.code[self.pc_of_top() as usize];
8004 self.bump_pc();
8005 let v = cl.proto.consts[extra.ax() as usize];
8006 self.set_r(base, inst.a(), v);
8007 }
8008 Op::LoadFalse => self.set_r(base, inst.a(), Value::Bool(false)),
8009 Op::LFalseSkip => {
8010 self.set_r(base, inst.a(), Value::Bool(false));
8011 self.bump_pc();
8012 }
8013 Op::LoadTrue => self.set_r(base, inst.a(), Value::Bool(true)),
8014 Op::LoadNil => {
8015 let a = inst.a();
8016 for i in 0..=inst.b() {
8017 self.set_r(base, a + i, Value::Nil);
8018 }
8019 }
8020 Op::GetUpval => {
8021 let v = self.upval_get(cl, inst.b());
8022 self.set_r(base, inst.a(), v);
8023 }
8024 Op::SetUpval => {
8025 let v = self.r(base, inst.a());
8026 self.upval_set(cl, inst.b(), v);
8027 }
8028 Op::GetTabUp => {
8029 let t = self.upval_get(cl, inst.b());
8030 let key = cl.proto.consts[inst.c() as usize];
8031 self.op_index(t, key, base + inst.a())?;
8032 }
8033 Op::GetTable => {
8034 let t = self.r(base, inst.b());
8035 let key = self.r(base, inst.c());
8036 self.op_index(t, key, base + inst.a())?;
8037 }
8038 Op::GetI => {
8039 let t = self.r(base, inst.b());
8040 self.op_index(t, Value::Int(inst.c() as i64), base + inst.a())?;
8041 }
8042 Op::GetField => {
8043 let t = self.r(base, inst.b());
8044 let key = cl.proto.consts[inst.c() as usize];
8045 // Fast path: known-Str const key + no
8046 // metatable on the table → skip `op_index` /
8047 // `index_step`'s MAX_TAG_LOOP setup and the outer
8048 // `Value` match. Falls through to the slow path
8049 // when either invariant breaks (`__index`
8050 // metamethods, non-Table receivers, non-Str keys).
8051 if let Value::Table(tb) = t
8052 && tb.metatable().is_none()
8053 && let Value::Str(s) = key
8054 {
8055 let v = tb.get_str(s);
8056 self.stack[(base + inst.a()) as usize] = v;
8057 } else {
8058 self.op_index(t, key, base + inst.a())?;
8059 }
8060 }
8061 Op::SetTabUp => {
8062 let t = self.upval_get(cl, inst.a());
8063 let key = cl.proto.consts[inst.b() as usize];
8064 let v = self.r(base, inst.c());
8065 self.op_newindex(t, key, v)?;
8066 }
8067 Op::SetTable => {
8068 let t = self.r(base, inst.a());
8069 let key = self.r(base, inst.b());
8070 let v = self.r(base, inst.c());
8071 self.op_newindex(t, key, v)?;
8072 }
8073 Op::SetI => {
8074 let t = self.r(base, inst.a());
8075 let v = self.r(base, inst.c());
8076 self.op_newindex(t, Value::Int(inst.b() as i64), v)?;
8077 }
8078 Op::SetField => {
8079 let t = self.r(base, inst.a());
8080 let key = cl.proto.consts[inst.b() as usize];
8081 let v = self.r(base, inst.c());
8082 self.op_newindex(t, key, v)?;
8083 }
8084 Op::NewTable => {
8085 let t = self.heap.new_table();
8086 self.set_r(base, inst.a(), Value::Table(t));
8087 self.maybe_collect_garbage(base + inst.a() + 1);
8088 }
8089 Op::SetList => {
8090 let a = inst.a();
8091 let abs_a = base + a;
8092 // only `debug.setlocal` or crafted bytecode can put a
8093 // non-table here; PUC crashes, luna raises
8094 let t = match self.r(base, a) {
8095 Value::Table(t) => t,
8096 v => return Err(self.type_err("index", v)),
8097 };
8098 let n = if inst.b() == 0 {
8099 self.top - (abs_a + 1)
8100 } else {
8101 inst.b()
8102 };
8103 let offset = if inst.k() {
8104 let extra = cl.proto.code[self.pc_of_top() as usize];
8105 self.bump_pc();
8106 extra.ax() as i64
8107 } else {
8108 inst.c() as i64
8109 };
8110 for i in 1..=n {
8111 let v = self.r(base, a + i);
8112 // 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).
8113 if let Err(TableError::Overflow) =
8114 unsafe { t.as_mut() }.set_int(&mut self.heap, offset + i as i64, v)
8115 {
8116 return Err(self.rt_err("table overflow"));
8117 }
8118 }
8119 // one barrier_back covers every store this op did — PUC's
8120 // `luaC_barrierback_` once-per-table optimisation
8121 self.heap
8122 .barrier_back(t.as_ptr() as *mut crate::runtime::heap::GcHeader);
8123 // the element temps above the table are now consumed
8124 self.maybe_collect_garbage(base + a + 1);
8125 }
8126 Op::SelfOp => {
8127 let o = self.r(base, inst.b());
8128 self.set_r(base, inst.a() + 1, o);
8129 // PUC OP_SELF's C is a constant index when the k-flag is
8130 // set; otherwise it points to a register that holds the
8131 // (constant-loaded) key. luna's compiler falls back to the
8132 // register form when the constant index exceeds OP_SELF's
8133 // 8-bit C field (5.1 big.lua's `a:findfield(...)` against
8134 // a table with 250+ string keys, where "findfield" lands
8135 // past const #255). The exec must honour the same split.
8136 let key = if inst.k() {
8137 cl.proto.consts[inst.c() as usize]
8138 } else {
8139 self.r(base, inst.c())
8140 };
8141 self.op_index(o, key, base + inst.a())?;
8142 }
8143 Op::Add => self.arith_rr(inst, base, ArithOp::Add)?,
8144 Op::Sub => self.arith_rr(inst, base, ArithOp::Sub)?,
8145 Op::Mul => self.arith_rr(inst, base, ArithOp::Mul)?,
8146 Op::Mod => self.arith_rr(inst, base, ArithOp::Mod)?,
8147 Op::Pow => self.arith_rr(inst, base, ArithOp::Pow)?,
8148 Op::Div => self.arith_rr(inst, base, ArithOp::Div)?,
8149 Op::IDiv => self.arith_rr(inst, base, ArithOp::IDiv)?,
8150 Op::BAnd => self.arith_rr(inst, base, ArithOp::BAnd)?,
8151 Op::BOr => self.arith_rr(inst, base, ArithOp::BOr)?,
8152 Op::BXor => self.arith_rr(inst, base, ArithOp::BXor)?,
8153 Op::Shl => self.arith_rr(inst, base, ArithOp::Shl)?,
8154 Op::Shr => self.arith_rr(inst, base, ArithOp::Shr)?,
8155 Op::Unm => {
8156 let v = self.r(base, inst.b());
8157 match self.unary_operand(v) {
8158 Some(Num::Int(i)) => {
8159 self.set_r(base, inst.a(), Value::Int(i.wrapping_neg()))
8160 }
8161 Some(Num::Float(f)) => self.set_r(base, inst.a(), Value::Float(-f)),
8162 None => {
8163 let mm = self.get_mm(v, Mm::Unm);
8164 if mm.is_nil() {
8165 return Err(self.type_err("perform arithmetic on", v));
8166 }
8167 let dst = base + inst.a();
8168 self.begin_meta_call(mm, &[v, v], MetaAction::Store { dst }, "unm")?;
8169 }
8170 }
8171 }
8172 Op::BNot => {
8173 let v = self.r(base, inst.b());
8174 match self.arith_operand()(v) {
8175 Some(n) => {
8176 let Some(i) = int_of(n) else {
8177 return Err(self.no_int_rep_err());
8178 };
8179 self.set_r(base, inst.a(), Value::Int(!i));
8180 }
8181 None => {
8182 let mm = self.get_mm(v, Mm::BNot);
8183 if mm.is_nil() {
8184 return Err(self.type_err("perform bitwise operation on", v));
8185 }
8186 let dst = base + inst.a();
8187 self.begin_meta_call(mm, &[v, v], MetaAction::Store { dst }, "bnot")?;
8188 }
8189 }
8190 }
8191 Op::Not => {
8192 let v = self.r(base, inst.b());
8193 self.set_r(base, inst.a(), Value::Bool(!v.truthy()));
8194 }
8195 Op::Len => {
8196 let v = self.r(base, inst.b());
8197 match self.len_step(v)? {
8198 MmOut::Done(r) => self.set_r(base, inst.a(), r),
8199 MmOut::Mm { func, recv } => {
8200 let dst = base + inst.a();
8201 self.begin_meta_call(
8202 func,
8203 &[recv, recv],
8204 MetaAction::Store { dst },
8205 "len",
8206 )?;
8207 }
8208 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from len_step"),
8209 }
8210 }
8211 Op::Concat => {
8212 // right-associative fold over operands at base+a .. base+a+n,
8213 // in place on the stack so a yielding __concat can suspend.
8214 let a = inst.a();
8215 let n = inst.b();
8216 self.top = base + a + n;
8217 self.concat_run(base + a)?;
8218 }
8219 Op::Close => {
8220 // Yieldable: drive __close handlers through the
8221 // interpreter loop so a coroutine.yield() inside a
8222 // handler suspends cleanly (locals.lua block-end yield).
8223 // `drive_close` parks the handler call at `self.top`, so
8224 // raise `top` past this frame's full register window
8225 // first — a goto out of a nested for-loop can fire
8226 // OP_Close while `self.top` still sits at the inner
8227 // body's working top, which would let `push_frame`'s
8228 // wipe clobber the outer tbc slot before it could be
8229 // closed (locals.lua:1219 nested-for goto regression).
8230 self.top = self.top.max(base + cl.proto.max_stack as u32);
8231 let _ =
8232 self.begin_close(base + inst.a(), None, AfterClose::Block, entry_depth)?;
8233 }
8234 Op::Tbc => {
8235 self.register_tbc(base + inst.a())?;
8236 }
8237 Op::Jmp => {
8238 let off = inst.sj();
8239 // Trace JIT back-edge counter. A negative
8240 // jump offset is a loop back-edge (the only canonical
8241 // backward jumps the compiler emits — `while`, `for`,
8242 // `repeat`). Tick the per-Proto counter and, once it
8243 // exceeds the threshold, start a trace recording. The
8244 // whole block is gated on `trace_jit_enabled` so
8245 // existing benches see one branch-not-taken and no
8246 // counter writes.
8247 if self.jit.trace_enabled && off < 0 {
8248 let proto = cl.proto;
8249 let c = proto.trace_hot_count.get();
8250 if c < u32::MAX / 2 {
8251 proto.trace_hot_count.set(c + 1);
8252 }
8253 // Relaxed back-edge trigger:
8254 // `c >= THRESHOLD` (not `c == THRESHOLD`) so
8255 // a missed crossing (active_trace busy with
8256 // a call-trigger, or the recorder slot
8257 // happened to be in use) doesn't permanently
8258 // lock this back-edge target out. The
8259 // `already_cached` short-circuit prevents
8260 // duplicate recordings: once a trace is
8261 // cached for this target, subsequent
8262 // crossings skip the start. This pairs with
8263 // the discard-on-partial-coverage close
8264 // handling — when a short call-trigger is
8265 // discarded, the back-edge can still find an
8266 // open slot at the next iteration.
8267 let target_pc = (pc as i32 + 1 + off as i32).max(0) as u32;
8268 // Gave-up short-circuit. Skip
8269 // the RefCell borrow + scan when the
8270 // discard cap force-compiled a partial
8271 // trace on this Proto.
8272 let back_edge_already_cached = if proto.trace_gave_up.get() {
8273 true
8274 } else {
8275 proto.traces.borrow().iter().any(|t| t.head_pc == target_pc)
8276 || trace_head_abandoned(proto, target_pc)
8277 };
8278 if c >= self.jit.trace_hot_threshold
8279 && self.jit.active_trace.is_none()
8280 && !back_edge_already_cached
8281 {
8282 // Back-edge target = pc after `add_pc(off)`,
8283 // i.e. current `pc + 1 + off` (the dispatch
8284 // loop has already advanced f.pc to pc+1).
8285 let target = (pc as i32 + 1 + off as i32).max(0) as u32;
8286 // Snapshot per-slot Value tag at trace
8287 // entry so the lowerer's kind tracker
8288 // knows which arith path to lower
8289 // (iadd vs fadd, etc.).
8290 let max_stack = cl.proto.max_stack as usize;
8291 let base_us = base as usize;
8292 let mut entry_tags = Vec::with_capacity(max_stack);
8293 for i in 0..max_stack {
8294 let (tag, _) = self.stack[base_us + i].unpack();
8295 entry_tags.push(tag);
8296 }
8297 self.jit.active_trace =
8298 Some(Box::new(crate::jit::trace::TraceRecord::start(
8299 cl.proto, target, entry_tags, false,
8300 )));
8301 // Record the frame the trace
8302 // started in. `self.frames.len() - 1`
8303 // since we're inside the currently-running
8304 // Lua frame's dispatch.
8305 self.jit.recording_frame_base = self.frames.len() - 1;
8306 }
8307 }
8308 self.add_pc(off);
8309 }
8310 Op::Eq => {
8311 let l = self.r(base, inst.a());
8312 let r = self.r(base, inst.b());
8313 if let (Value::Int(a), Value::Int(b)) = (l, r) {
8314 if (a == b) != inst.k() {
8315 self.bump_pc();
8316 }
8317 } else {
8318 let step = self.eq_step(l, r);
8319 self.op_compare(step, l, r, inst.k(), "eq")?;
8320 }
8321 }
8322 Op::EqK => {
8323 let l = self.r(base, inst.a());
8324 let r = cl.proto.consts[inst.b() as usize];
8325 if let (Value::Int(a), Value::Int(b)) = (l, r) {
8326 if (a == b) != inst.k() {
8327 self.bump_pc();
8328 }
8329 } else {
8330 let step = self.eq_step(l, r);
8331 self.op_compare(step, l, r, inst.k(), "eq")?;
8332 }
8333 }
8334 Op::Lt => {
8335 let l = self.r(base, inst.a());
8336 let r = self.r(base, inst.b());
8337 // hot path: Int < Int — drops the MmOut + op_compare match
8338 if let (Value::Int(a), Value::Int(b)) = (l, r) {
8339 if (a < b) != inst.k() {
8340 self.bump_pc();
8341 }
8342 } else {
8343 let step = self.less_step(l, r, false)?;
8344 self.op_compare(step, l, r, inst.k(), "lt")?;
8345 }
8346 }
8347 Op::Le => {
8348 let l = self.r(base, inst.a());
8349 let r = self.r(base, inst.b());
8350 if let (Value::Int(a), Value::Int(b)) = (l, r) {
8351 if (a <= b) != inst.k() {
8352 self.bump_pc();
8353 }
8354 } else {
8355 let step = self.less_step(l, r, true)?;
8356 self.op_compare(step, l, r, inst.k(), "le")?;
8357 }
8358 }
8359 Op::Test => {
8360 let cond = self.r(base, inst.a()).truthy();
8361 self.cond_skip(cond, inst.k());
8362 }
8363 Op::TestSet => {
8364 let v = self.r(base, inst.b());
8365 if v.truthy() == inst.k() {
8366 self.set_r(base, inst.a(), v);
8367 } else {
8368 self.bump_pc();
8369 }
8370 }
8371 Op::Call => {
8372 let abs = base + inst.a();
8373 let nargs = if inst.b() == 0 {
8374 None
8375 } else {
8376 Some(inst.b() - 1)
8377 };
8378 let wanted = inst.c() as i32 - 1;
8379 self.begin_call(abs, nargs, wanted, false)?;
8380 }
8381 Op::TailCall => {
8382 let fr = *self.top_frame();
8383 let abs = base + inst.a();
8384 let mut nargs = if inst.b() == 0 {
8385 self.top - (abs + 1)
8386 } else {
8387 inst.b() - 1
8388 };
8389 // A tail call pops this frame before begin_call, so a
8390 // non-callable target would lose its name/position. Report
8391 // it now (PUC reads funcname from the still-current ci),
8392 // while the frame is intact, for "(field 'x')"-style info.
8393 let mut func = self.stack[abs as usize];
8394 if !matches!(func, Value::Closure(_) | Value::Native(_))
8395 && self.get_mm(func, Mm::Call).is_nil()
8396 {
8397 return Err(self.call_err(func));
8398 }
8399 // PUC `luaD_pretailcall` resolves a chain of `__call`
8400 // metamethods *in place* before deciding whether to
8401 // collapse this frame. Without that, each __call hop
8402 // would push a fresh Lua frame and a 10000-deep
8403 // tail-recursion through a 100-deep __call chain
8404 // (5.4 calls.lua :172) blows up. Mirror the PUC loop:
8405 // shift args right, install the handler at `abs`, retry.
8406 // Chain depth limit matches the call-site `begin_call`
8407 // version cap (5.5 calls.lua :223 — 15 max, then "too
8408 // long"; 16th wrap fails the call). An infinite
8409 // self-referential `__call` would otherwise spin.
8410 let chain_cap = if self.version >= LuaVersion::Lua55 {
8411 15
8412 } else {
8413 MAX_CCMT
8414 };
8415 let mut chain = 0u32;
8416 while !matches!(func, Value::Closure(_) | Value::Native(_)) {
8417 let mm = self.get_mm(func, Mm::Call);
8418 if mm.is_nil() || self.call_mm_unusable(mm) {
8419 return Err(self.call_err(func));
8420 }
8421 chain += 1;
8422 if chain > chain_cap {
8423 return Err(self.rt_err("'__call' chain too long"));
8424 }
8425 let end = (abs + 1 + nargs) as usize;
8426 if self.stack.len() < end + 1 {
8427 self.stack.resize(end + 1, Value::Nil);
8428 }
8429 for i in (0..=nargs).rev() {
8430 self.stack[(abs + 1 + i) as usize] = self.stack[(abs + i) as usize];
8431 }
8432 self.stack[abs as usize] = mm;
8433 nargs += 1;
8434 self.top = abs + 1 + nargs;
8435 func = mm;
8436 }
8437 // PUC's tail-call collapse is Lua→Lua only. A tail call to
8438 // a C function runs the C function under the *current* Lua
8439 // activation (no frame fold — a C frame has nothing to
8440 // collapse into); after the C function returns, the
8441 // calling Lua function returns those results normally.
8442 // Mirror that: keep our Lua frame on the stack, call the
8443 // target through `begin_call(abs, …)` as a regular call,
8444 // and let the fallback `Op::Return` that the compiler
8445 // emits right after `Op::TailCall` forward the results.
8446 // 5.1 closure.lua :177's `return getfenv()` from inside
8447 // foo needs level 1 to resolve to foo, not to the
8448 // thread's globals fallback that happens when no Lua
8449 // frame is on the stack.
8450 let lua_target = matches!(func, Value::Closure(_));
8451 if lua_target {
8452 self.close_slots(fr.base, None)?;
8453 for i in 0..=nargs {
8454 self.stack[(fr.func_slot + i) as usize] =
8455 self.stack[(abs + i) as usize];
8456 }
8457 // Clear the slot range that's now
8458 // stranded by the tail-call collapse. The args
8459 // were copied to `[fr.func_slot..fr.func_slot+
8460 // nargs+1)`; the source slots `[abs..abs+
8461 // nargs+1)` still hold the same `Value::Closure
8462 // / Value::Str / ...` entries, but they're past
8463 // the new call's window. Without this clear, a
8464 // later GC with wider gc_top would mark stale
8465 // pointers there (same hazard the
8466 // finish_results slot-clear closes for the
8467 // Op::Return path).
8468 let new_top_lower_bound = fr.func_slot + nargs + 1;
8469 let prev_top = (self.top as usize).min(self.stack.len());
8470 if (new_top_lower_bound as usize) < prev_top {
8471 for slot in &mut self.stack[new_top_lower_bound as usize..prev_top] {
8472 *slot = Value::Nil;
8473 }
8474 }
8475 // PUC `CIST_TAIL`: the new Lua activation inherits
8476 // the popped frame's tailcalls count plus one for
8477 // this collapse. 5.1 db.lua :372 hammers 30000
8478 // recursive tail calls and expects to see the
8479 // synthetic tail level for every one of them.
8480 self.pending_tailcalls = fr.tailcalls.saturating_add(1);
8481 self.pending_ccmt = self.frame_ccmt[self.frames.len() - 1];
8482 frames_pop_sync(&mut self.frames, &mut self.frames_top);
8483 if !self.begin_call(fr.func_slot, Some(nargs), fr.nresults, false)?
8484 && self.frames.len() < entry_depth
8485 {
8486 // a native completed what was this function's result
8487 return Ok(self.take_results(fr.func_slot));
8488 }
8489 } else {
8490 // Native (or __call-bearing) target: regular call. The
8491 // results land at `abs..self.top` and the next op (the
8492 // fallback `Op::Return`) forwards them. `wanted = -1`
8493 // because the caller will multret them through Return.
8494 // PUC's precallC gives the C call this tail call's own
8495 // `__call` count (5.5 extraargs); the chain was already
8496 // resolved above, so hand it over.
8497 self.pending_ccmt = chain as u8;
8498 self.begin_call(abs, Some(nargs), -1, false)?;
8499 }
8500 }
8501 Op::Return | Op::Return0 | Op::Return1 => {
8502 let (abs_a, nret) = match inst.op() {
8503 Op::Return0 => (base, 0),
8504 Op::Return1 => (base + inst.a(), 1),
8505 _ => {
8506 let abs_a = base + inst.a();
8507 let nret = if inst.b() == 0 {
8508 self.top - abs_a
8509 } else {
8510 inst.b() - 1
8511 };
8512 (abs_a, nret)
8513 }
8514 };
8515 // close before moving results: __close handlers run above
8516 // the stack top, so the result region [abs_a..abs_a+nret)
8517 // stays intact across any yields the close performs.
8518 // Fixed-count returns may leave `self.top` below the last
8519 // result slot (the compiler does not always re-bump it);
8520 // raise it past the result region so `drive_close` parks
8521 // the handler call *above* — landing at `self.top` would
8522 // otherwise clobber a result with the handler closure.
8523 self.top = self.top.max(abs_a + nret);
8524 if let Some(vals) = self.begin_close(
8525 base,
8526 None,
8527 AfterClose::Return {
8528 abs_a,
8529 nret,
8530 from_native: false,
8531 },
8532 entry_depth,
8533 )? {
8534 return Ok(vals);
8535 }
8536 }
8537 Op::ForPrep => self.for_prep(inst, base)?,
8538 Op::ForLoop => {
8539 // Trace JIT back-edge counter on the
8540 // numeric-for back-edge. ForLoop is always at
8541 // a back-edge position (when it continues);
8542 // for the trace recorder we treat it as the
8543 // close-detection equivalent of `Op::Jmp` with
8544 // negative offset. Counter only ticks when the
8545 // back-edge will actually fire (count > 0 in
8546 // the 5.4+ Int form, comparable predicates in
8547 // pre-5.3 / Float). The cheap check up front
8548 // matches the for_loop helper's branch.
8549 if self.jit.trace_enabled {
8550 let a = inst.a();
8551 let pre53 = self.version() <= LuaVersion::Lua53;
8552 let take_back_edge =
8553 match (self.r(base, a), self.r(base, a + 1), self.r(base, a + 2)) {
8554 (Value::Int(_), Value::Int(count), Value::Int(_)) if !pre53 => {
8555 count != 0
8556 }
8557 (Value::Int(cur), Value::Int(lim), Value::Int(st)) if pre53 => {
8558 let next = cur.wrapping_add(st);
8559 if st > 0 { next <= lim } else { next >= lim }
8560 }
8561 (Value::Float(cur), Value::Float(lim), Value::Float(st)) => {
8562 let next = cur + st;
8563 if st > 0.0 { next <= lim } else { next >= lim }
8564 }
8565 _ => false,
8566 };
8567 if take_back_edge {
8568 let proto = cl.proto;
8569 let c = proto.trace_hot_count.get();
8570 if c < u32::MAX / 2 {
8571 proto.trace_hot_count.set(c + 1);
8572 }
8573 if c == self.jit.trace_hot_threshold && self.jit.active_trace.is_none()
8574 {
8575 // ForLoop's back-edge target = pc
8576 // after `add_pc(-bx)` runs from the
8577 // already-bumped f.pc (= pc + 1).
8578 // So target = (pc + 1) - bx.
8579 let target = (pc as i32 + 1 - inst.bx() as i32).max(0) as u32;
8580 let max_stack = cl.proto.max_stack as usize;
8581 let base_us = base as usize;
8582 let mut entry_tags = Vec::with_capacity(max_stack);
8583 for i in 0..max_stack {
8584 let (tag, _) = self.stack[base_us + i].unpack();
8585 entry_tags.push(tag);
8586 }
8587 self.jit.active_trace =
8588 Some(Box::new(crate::jit::trace::TraceRecord::start(
8589 cl.proto, target, entry_tags, false,
8590 )));
8591 // Record the frame the trace
8592 // started in. The currently-running
8593 // Lua frame is at len() - 1.
8594 self.jit.recording_frame_base = self.frames.len() - 1;
8595 }
8596 }
8597 }
8598 self.for_loop(inst, base)?;
8599 }
8600 Op::TForPrep => {
8601 // the 4th control slot is the iterator's closing value
8602 self.register_tbc(base + inst.a() + 3)?;
8603 self.add_pc(inst.bx() as i32);
8604 }
8605 Op::TForCall => {
8606 let abs = base + inst.a();
8607 let need = (abs + 7) as usize;
8608 if self.stack.len() < need {
8609 self.stack.resize(need, Value::Nil);
8610 }
8611 self.stack[(abs + 4) as usize] = self.stack[abs as usize];
8612 self.stack[(abs + 5) as usize] = self.stack[(abs + 1) as usize];
8613 self.stack[(abs + 6) as usize] = self.stack[(abs + 2) as usize];
8614 let nvars = inst.c() as i32;
8615 self.begin_call(abs + 4, Some(2), nvars, false)?;
8616 }
8617 Op::TForLoop => {
8618 let a = inst.a();
8619 let ctrl = self.r(base, a + 4);
8620 if !ctrl.is_nil() {
8621 // Trace JIT back-edge counter on
8622 // generic-for back-edge. TForLoop sits at the
8623 // tail of `for k,v in expr do ... end`; recorder
8624 // treats it as the close-detection equivalent of
8625 // a negative Op::Jmp. Gate on `take_back_edge`
8626 // (= `ctrl != nil`) so empty-iter loops don't
8627 // pollute hot_count.
8628 if self.jit.trace_enabled {
8629 let proto = cl.proto;
8630 let c = proto.trace_hot_count.get();
8631 if c < u32::MAX / 2 {
8632 proto.trace_hot_count.set(c + 1);
8633 }
8634 if c == self.jit.trace_hot_threshold && self.jit.active_trace.is_none()
8635 {
8636 // TForLoop back-edge target = pc after
8637 // `add_pc(-bx)` runs from the already-
8638 // bumped f.pc (= pc + 1). So target =
8639 // (pc + 1) - bx, normally landing on
8640 // body_top (the op right after TForPrep).
8641 let target = (pc as i32 + 1 - inst.bx() as i32).max(0) as u32;
8642 let max_stack = cl.proto.max_stack as usize;
8643 let base_us = base as usize;
8644 let mut entry_tags = Vec::with_capacity(max_stack);
8645 for i in 0..max_stack {
8646 let (tag, _) = self.stack[base_us + i].unpack();
8647 entry_tags.push(tag);
8648 }
8649 // Snapshot the iter
8650 // fn's address if Native, so the
8651 // lowerer can specialise ipairs into
8652 // inline Table aget IR.
8653 let iter_ptr =
8654 if let Value::Native(n) = self.stack[base_us + a as usize] {
8655 Some(n.f as usize)
8656 } else {
8657 None
8658 };
8659 // Snapshot R[A+5]'s
8660 // tag (= current iter's val from
8661 // the just-fired TForCall). The
8662 // inline aget fast_blk emits a
8663 // runtime guard against this tag;
8664 // mixed-tag arrays deopt rather
8665 // than producing garbage pointers
8666 // through the spill path.
8667 let val_slot = base_us + (a as usize) + 5;
8668 let val_tag = if val_slot < self.stack.len() {
8669 Some(self.stack[val_slot].unpack().0)
8670 } else {
8671 None
8672 };
8673 let mut rec = crate::jit::trace::TraceRecord::start(
8674 cl.proto, target, entry_tags, false,
8675 );
8676 rec.tfor_iter_ptr = iter_ptr;
8677 rec.tfor_val_tag = val_tag;
8678 self.jit.active_trace = Some(Box::new(rec));
8679 self.jit.recording_frame_base = self.frames.len() - 1;
8680 }
8681 }
8682 self.set_r(base, a + 2, ctrl);
8683 self.add_pc(-(inst.bx() as i32));
8684 }
8685 }
8686 Op::Closure => {
8687 let proto = cl.proto.protos[inst.bx() as usize];
8688 let n_ups = proto.upvals.len();
8689 // Build upvals on the stack for small
8690 // closures, skipping the per-call Vec/Box alloc
8691 // that closure_alloc's 10k iters pay. INLINE_UPVALS_N
8692 // = 2 covers most Lua source (1 captured local, or
8693 // _ENV + a single capture). Beyond that, fall back
8694 // to a heap Vec.
8695 use crate::runtime::function::INLINE_UPVALS_N;
8696 let mut stack_buf: [std::mem::MaybeUninit<
8697 Gc<crate::runtime::function::Upvalue>,
8698 >; INLINE_UPVALS_N] = [std::mem::MaybeUninit::uninit(); INLINE_UPVALS_N];
8699 let mut heap_buf: Vec<Gc<crate::runtime::function::Upvalue>> = Vec::new();
8700 let use_inline = n_ups <= INLINE_UPVALS_N;
8701 if !use_inline {
8702 heap_buf.reserve_exact(n_ups);
8703 }
8704 for (i, d) in proto.upvals.iter().enumerate() {
8705 let uv = if d.in_stack {
8706 self.find_or_create_upval(base + d.index as u32)
8707 } else {
8708 cl.upvals()[d.index as usize]
8709 };
8710 if use_inline {
8711 stack_buf[i] = std::mem::MaybeUninit::new(uv);
8712 } else {
8713 heap_buf.push(uv);
8714 }
8715 }
8716 // Tiny shim around the two paths so the 5.1 _ENV
8717 // clone + cache check below see one uniform
8718 // `&mut [Gc<Upvalue>]`. The stack_buf slice points
8719 // into the local frame (still valid through the
8720 // rest of this Op::Closure handler).
8721 let ups: &mut [Gc<crate::runtime::function::Upvalue>] = if use_inline {
8722 // SAFETY: the first n_ups slots of stack_buf
8723 // were initialised above; we hand out a slice
8724 // covering exactly them.
8725 unsafe {
8726 std::slice::from_raw_parts_mut(
8727 stack_buf.as_mut_ptr()
8728 as *mut Gc<crate::runtime::function::Upvalue>,
8729 n_ups,
8730 )
8731 }
8732 } else {
8733 &mut heap_buf[..]
8734 };
8735 // PUC 5.1 had per-function environments: every Lua
8736 // function carried its own `env` slot, snapshotted from
8737 // the creating function's env at closure time, so a
8738 // `setfenv` on one closure never bled into a sibling.
8739 // luna models that by giving the 5.1 closure a *fresh*
8740 // closed upvalue for whichever cell holds `_ENV`, seeded
8741 // from the parent's current env value. Only that cell is
8742 // cloned — every other upvalue keeps its open/shared
8743 // identity (so e.g. `local function range(...) ...
8744 // range(...) ... end` still sees its self-reference). 5.2+
8745 // keeps the shared-upval model (and the proto cache that
8746 // depends on it).
8747 let v51 = self.version() <= LuaVersion::Lua51;
8748 if v51 && proto.env_upval_idx != u8::MAX {
8749 let i = proto.env_upval_idx as usize;
8750 let cur = match ups[i].state() {
8751 UpvalState::Open { slot, thread } => self.read_slot(slot, thread),
8752 UpvalState::Closed(v) => v,
8753 };
8754 ups[i] = self.heap.new_upvalue(UpvalState::Closed(cur));
8755 }
8756 let ups_slice: &[Gc<crate::runtime::function::Upvalue>] = ups;
8757 // PUC 5.2+ `getcached`: a Proto remembers its last LClosure
8758 // and reuses it when every fresh-upvalue binding still
8759 // points to the same Upvalue object as the cached one.
8760 // That keeps `function() return outer end` repeated in a
8761 // loop comparing equal across iterations (the captured
8762 // outer is a shared open upvalue), while `function()
8763 // return loop_var end` gets a fresh closure each round
8764 // because the loop var is re-created per iteration. PUC
8765 // 5.1 predated the cache, and the per-closure `_ENV`
8766 // clone above would defeat it anyway, so skip it.
8767 let nc = if v51 {
8768 self.heap.new_closure_inline(proto, ups_slice)
8769 } else {
8770 let cached = proto.cache.get().filter(|c| {
8771 c.upvals().len() == ups_slice.len()
8772 && c.upvals()
8773 .iter()
8774 .zip(ups_slice.iter())
8775 .all(|(a, b)| std::ptr::eq(a.as_ptr(), b.as_ptr()))
8776 });
8777 match cached {
8778 Some(c) => c,
8779 None => {
8780 let n = self.heap.new_closure_inline(proto, ups_slice);
8781 proto.cache.set(Some(n));
8782 n
8783 }
8784 }
8785 };
8786 self.set_r(base, inst.a(), Value::Closure(nc));
8787 self.maybe_collect_garbage(base + inst.a() + 1);
8788 }
8789 Op::Vararg => {
8790 let abs_a = base + inst.a();
8791 let wanted = inst.c() as i32 - 1;
8792 // A materialized named vararg lives in func_slot (its writes
8793 // must be visible to `...`); otherwise spread the extra args
8794 // straight off the stack at func_slot+1 .. +n_varargs.
8795 let vt = match self.stack[func_slot as usize] {
8796 Value::Table(t) => Some(t),
8797 _ => None,
8798 };
8799 let n = match vt {
8800 Some(t) => {
8801 let n_key = Value::Str(self.heap.intern(b"n"));
8802 // PUC getnumargs: a named vararg `t.n` set out of the
8803 // integer range [0, INT_MAX/2] is rejected here
8804 match t.get(n_key) {
8805 Value::Int(n) if (n as u64) <= (i32::MAX as u64 / 2) => n as u32,
8806 _ => return Err(self.rt_err("vararg table has no proper 'n'")),
8807 }
8808 }
8809 None => n_varargs,
8810 };
8811 let count = if wanted < 0 { n } else { wanted as u32 };
8812 // a named vararg's `n` can be set to anything up to
8813 // INT_MAX/2; PUC's `luaD_checkstack` refuses what the
8814 // stack cannot hold
8815 if abs_a + count > MAX_LUA_STACK {
8816 return Err(self.rt_err("stack overflow"));
8817 }
8818 let need = (abs_a + count) as usize;
8819 if self.stack.len() < need {
8820 self.stack.resize(need, Value::Nil);
8821 }
8822 for i in 0..count {
8823 let v = if i >= n {
8824 Value::Nil
8825 } else if let Some(t) = vt {
8826 t.get_int(i as i64 + 1)
8827 } else {
8828 self.stack[(func_slot + 1 + i) as usize]
8829 };
8830 self.stack[(abs_a + i) as usize] = v;
8831 }
8832 if wanted < 0 {
8833 self.top = abs_a + count;
8834 }
8835 }
8836 Op::GetVarg => {
8837 // materialize the vararg table (PUC table.pack shape) from the
8838 // stack varargs — used when the named vararg is written /
8839 // escapes / is `_ENV`. It is kept BOTH in func_slot (so `...`
8840 // sees later writes) and in the local register R[A].
8841 let n = n_varargs;
8842 let t = self.heap.new_table();
8843 {
8844 // 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).
8845 let tm = unsafe { t.as_mut() };
8846 for i in 0..n {
8847 let _ = tm.set_int(
8848 &mut self.heap,
8849 i as i64 + 1,
8850 self.stack[(func_slot + 1 + i) as usize],
8851 );
8852 }
8853 }
8854 let n_key = Value::Str(self.heap.intern(b"n"));
8855 // 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).
8856 unsafe { t.as_mut() }
8857 .set(&mut self.heap, n_key, Value::Int(n as i64))
8858 .expect("'n' is a valid key");
8859 // once-per-table barrier (mirror SETLIST): t is born BLACK
8860 // during Propagate; the bulk inserts above don't barrier.
8861 self.heap
8862 .barrier_back(t.as_ptr() as *mut crate::runtime::heap::GcHeader);
8863 self.stack[func_slot as usize] = Value::Table(t);
8864 self.set_r(base, inst.a(), Value::Table(t));
8865 }
8866 Op::VargIdx => {
8867 // R[A] := vararg[R[C]] without allocating: integer key in
8868 // [1,n] → that vararg, "n" → the count, else nil.
8869 let key = self.r(base, inst.c());
8870 let n = n_varargs;
8871 let v = match key {
8872 Value::Int(k) if k >= 1 && (k as u64) <= n as u64 => {
8873 self.stack[(func_slot + k as u32) as usize]
8874 }
8875 Value::Float(f) if f.fract() == 0.0 && f >= 1.0 && f <= n as f64 => {
8876 self.stack[(func_slot + f as u32) as usize]
8877 }
8878 Value::Str(s) if s.as_bytes() == b"n" => Value::Int(n as i64),
8879 _ => Value::Nil,
8880 };
8881 self.set_r(base, inst.a(), v);
8882 }
8883 Op::ErrNNil => {
8884 let v = self.r(base, inst.a());
8885 if !matches!(v, Value::Nil) {
8886 let bx = inst.bx();
8887 let name = if bx == 0 {
8888 "?".to_string()
8889 } else {
8890 match cl.proto.consts[(bx - 1) as usize] {
8891 Value::Str(s) => String::from_utf8_lossy(s.as_bytes()).into_owned(),
8892 _ => "?".to_string(),
8893 }
8894 };
8895 return Err(self.rt_err(&format!("global '{name}' already defined")));
8896 }
8897 }
8898 Op::ExtraArg => unreachable!("EXTRAARG executed directly"),
8899 }
8900 }
8901 }
8902
8903 #[inline(always)]
8904 fn pc_of_top(&self) -> u32 {
8905 self.top_frame().pc
8906 }
8907
8908 #[inline(always)]
8909 fn bump_pc(&mut self) {
8910 // Inline `top_frame_mut`: top is guaranteed Lua (continuation frames
8911 // drained at dispatch loop head). Avoids the and_then/lua_mut Option
8912 // layers — bump_pc fires per Jmp / cond_skip miss, so the savings add
8913 // up over `fib_28`'s ~500k jumps.
8914 // 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).
8915 match unsafe { self.frames.last_mut().unwrap_unchecked() } {
8916 CallFrame::Lua(f) => f.pc += 1,
8917 _ => unreachable!("Cont frame at bump_pc"),
8918 }
8919 }
8920
8921 #[inline(always)]
8922 fn add_pc(&mut self, d: i32) {
8923 // 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).
8924 match unsafe { self.frames.last_mut().unwrap_unchecked() } {
8925 CallFrame::Lua(f) => f.pc = (f.pc as i64 + d as i64) as u32,
8926 _ => unreachable!("Cont frame at add_pc"),
8927 }
8928 }
8929
8930 /// PUC conditional-skip convention: the JMP that follows is executed when
8931 /// `cond == k`; otherwise it is skipped.
8932 #[inline(always)]
8933 fn cond_skip(&mut self, cond: bool, k: bool) {
8934 if cond != k {
8935 self.bump_pc();
8936 }
8937 }
8938
8939 // ---- indexing (with __index/__newindex chains) ----
8940
8941 /// The `#` length operation: string byte length, `__len` if present, else
8942 /// the raw table border. Returns the raw length value (may be non-integer
8943 /// when `__len` is exotic).
8944 pub(crate) fn len_value(&mut self, v: Value) -> Result<Value, LuaError> {
8945 match self.len_step(v)? {
8946 MmOut::Done(n) => Ok(n),
8947 // PUC calls unary metamethods with the operand twice
8948 MmOut::Mm { func, recv } => self.call_mm1(func, &[recv, recv]),
8949 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from len_step"),
8950 }
8951 }
8952
8953 /// Length fast path: a string's byte count or a table's raw border when no
8954 /// `__len` is present (`Done`); otherwise the `__len` metamethod (`Mm`),
8955 /// called with the operand twice. Errors for a non-table with no `__len`.
8956 fn len_step(&mut self, v: Value) -> Result<MmOut, LuaError> {
8957 match v {
8958 Value::Str(s) => Ok(MmOut::Done(Value::Int(s.len() as i64))),
8959 Value::Table(t) => {
8960 // PUC 5.1's `__len` applies to userdata only — `luaV_objlen`
8961 // there takes the raw border for a table without consulting
8962 // the metatable, so `#setmetatable({}, {__len = f})` is 0 on
8963 // 5.1 and 7 on 5.2+. Verified against stock 5.1.5 / 5.2.4.
8964 if self.version() == crate::version::LuaVersion::Lua51 {
8965 return Ok(MmOut::Done(Value::Int(t.len())));
8966 }
8967 let mm = self.get_mm(v, Mm::Len);
8968 if mm.is_nil() {
8969 Ok(MmOut::Done(Value::Int(t.len())))
8970 } else {
8971 Ok(MmOut::Mm { func: mm, recv: v })
8972 }
8973 }
8974 _ => {
8975 let mm = self.get_mm(v, Mm::Len);
8976 if mm.is_nil() {
8977 Err(self.type_err("get length of", v))
8978 } else {
8979 Ok(MmOut::Mm { func: mm, recv: v })
8980 }
8981 }
8982 }
8983 }
8984
8985 pub(crate) fn index_value(&mut self, t: Value, key: Value) -> Result<Value, LuaError> {
8986 match self.index_step(t, key)? {
8987 MmOut::Done(v) => Ok(v),
8988 MmOut::Mm { func, recv } => self.call_mm1(func, &[recv, key]),
8989 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from index_step"),
8990 }
8991 }
8992
8993 /// PUC `MAXTAGLOOP`: 100 links of `__index`/`__newindex` in 5.1/5.2,
8994 /// 2000 from 5.3.
8995 fn tag_loop_limit(&self) -> u32 {
8996 if self.version <= LuaVersion::Lua52 {
8997 100
8998 } else {
8999 MAX_TAG_LOOP
9000 }
9001 }
9002
9003 /// Resolve `t[key]` through the `__index` chain, stopping at the first raw
9004 /// hit (`Done`) or function metamethod (`Mm`). Table-valued `__index` links
9005 /// are followed inline (no yield possible); only a function link can yield.
9006 fn index_step(&mut self, t: Value, key: Value) -> Result<MmOut, LuaError> {
9007 let mut cur = t;
9008 for _ in 0..self.tag_loop_limit() {
9009 let mm = match cur {
9010 Value::Table(tb) => {
9011 let v = tb.get(key);
9012 if !v.is_nil() {
9013 return Ok(MmOut::Done(v));
9014 }
9015 let mm = self.get_mm(cur, Mm::Index);
9016 if mm.is_nil() {
9017 return Ok(MmOut::Done(Value::Nil));
9018 }
9019 mm
9020 }
9021 v => {
9022 let mm = self.get_mm(v, Mm::Index);
9023 if mm.is_nil() {
9024 return Err(self.type_err("index", v));
9025 }
9026 mm
9027 }
9028 };
9029 match mm {
9030 Value::Closure(_) | Value::Native(_) => {
9031 return Ok(MmOut::Mm {
9032 func: mm,
9033 recv: cur,
9034 });
9035 }
9036 next => cur = next,
9037 }
9038 }
9039 Err(self.runerror(if self.version <= LuaVersion::Lua52 {
9040 "loop in gettable"
9041 } else {
9042 "'__index' chain too long; possible loop"
9043 }))
9044 }
9045
9046 pub(crate) fn newindex_value(
9047 &mut self,
9048 t: Value,
9049 key: Value,
9050 v: Value,
9051 ) -> Result<(), LuaError> {
9052 match self.newindex_step(t, key, v)? {
9053 MmOut::Done(_) => Ok(()),
9054 MmOut::Mm { func, recv } => {
9055 self.call_value(func, &[recv, key, v])?;
9056 Ok(())
9057 }
9058 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from newindex_step"),
9059 }
9060 }
9061
9062 /// Resolve `t[key] = v` through the `__newindex` chain. A raw assignment is
9063 /// performed inline (returning `Done`); only a function metamethod (`Mm`)
9064 /// needs an actual call — which the caller may run yieldably.
9065 fn newindex_step(&mut self, t: Value, key: Value, v: Value) -> Result<MmOut, LuaError> {
9066 // Read-time probe (gc-verify): a dead query key at a
9067 // WRITE site, attributed to the instruction that produced it.
9068 #[cfg(feature = "gc-verify")]
9069 if let Some(p) = match key {
9070 Value::Str(s) => Some(s.as_ptr() as usize),
9071 Value::Table(t2) => Some(t2.as_ptr() as usize),
9072 _ => None,
9073 } && crate::runtime::gc_verify_probe::is_freed(p)
9074 {
9075 let detail = match self.frames.last() {
9076 Some(CallFrame::Lua(f)) => {
9077 let pc = f.pc as usize;
9078 let mut w = String::new();
9079 for q in pc.saturating_sub(6)..(pc + 2) {
9080 if let Some(inst) = f.closure.proto.code.get(q) {
9081 w.push_str(&format!(
9082 "\n [{q}] {:?} a={} b={} c={} k={}",
9083 inst.op(),
9084 inst.a(),
9085 inst.b(),
9086 inst.c(),
9087 inst.k()
9088 ));
9089 }
9090 }
9091 format!("pc={pc} base={} gc_top={} window:{w}", f.base, self.gc_top)
9092 }
9093 _ => "non-Lua frame".into(),
9094 };
9095 panic!("[gc-verify] newindex_step QUERY key {p:#x} freed. {detail}");
9096 }
9097 let mut cur = t;
9098 for _ in 0..self.tag_loop_limit() {
9099 let mm = match cur {
9100 Value::Table(tb) => {
9101 // Single-walk collapse — Table::try_set_existing
9102 // fuses the prior `tb.get(key).is_nil()` gate and
9103 // `raw_set` walk into one chain traversal when the
9104 // key is already present with a non-nil value. The
9105 // __newindex chain semantics are preserved by the
9106 // identity (slot_nil ⇔ fire_newindex).
9107 //
9108 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the
9109 // heap is single-threaded and the pointer is live as
9110 // long as it is reachable from active roots (see
9111 // heap.rs:5-7). Mirrors the raw_set wrapper below.
9112 if unsafe { tb.as_mut() }.try_set_existing(key, v) {
9113 self.heap
9114 .barrier_back(tb.as_ptr() as *mut crate::runtime::heap::GcHeader);
9115 return Ok(MmOut::Done(Value::Nil));
9116 }
9117 let mm = self.get_mm(cur, Mm::NewIndex);
9118 if mm.is_nil() {
9119 self.raw_set(tb, key, v)?;
9120 return Ok(MmOut::Done(Value::Nil));
9121 }
9122 mm
9123 }
9124 bad => {
9125 let mm = self.get_mm(bad, Mm::NewIndex);
9126 if mm.is_nil() {
9127 return Err(self.type_err("index", bad));
9128 }
9129 mm
9130 }
9131 };
9132 match mm {
9133 Value::Closure(_) | Value::Native(_) => {
9134 return Ok(MmOut::Mm {
9135 func: mm,
9136 recv: cur,
9137 });
9138 }
9139 next => cur = next,
9140 }
9141 }
9142 Err(self.runerror(if self.version <= LuaVersion::Lua52 {
9143 "loop in settable"
9144 } else {
9145 "'__newindex' chain too long; possible loop"
9146 }))
9147 }
9148
9149 pub(crate) fn raw_set(&mut self, t: Gc<Table>, key: Value, v: Value) -> Result<(), LuaError> {
9150 // 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).
9151 match unsafe { t.as_mut() }.set(&mut self.heap, key, v) {
9152 Ok(()) => {
9153 self.heap
9154 .barrier_back(t.as_ptr() as *mut crate::runtime::heap::GcHeader);
9155 Ok(())
9156 }
9157 Err(TableError::NilIndex) => Err(self.runerror("table index is nil")),
9158 Err(TableError::NanIndex) => Err(self.runerror("table index is NaN")),
9159 Err(TableError::Overflow) => Err(self.runerror("table overflow")),
9160 Err(TableError::InvalidNext) => unreachable!(),
9161 }
9162 }
9163
9164 /// Decide equality, or surface the `__eq` metamethod to call. `Done` carries
9165 /// the boolean result; `Mm` (when raw equality fails and both are tables
9166 /// with an `__eq`) carries the metamethod — called with `(l, r)`.
9167 fn eq_step(&mut self, l: Value, r: Value) -> MmOut {
9168 if l.raw_eq(r) {
9169 return MmOut::Done(Value::Bool(true));
9170 }
9171 if let (Value::Table(_), Value::Table(_)) | (Value::Userdata(_), Value::Userdata(_)) =
9172 (l, r)
9173 {
9174 // PUC 5.3+ accepts any `__eq` reachable from either operand; 5.1
9175 // and 5.2 require the two operands' metatables to expose the same
9176 // `__eq` (`get_compTM` / `get_equalTM`) — `c == d` where `d` has
9177 // no metatable falls straight back to raw inequality. events.lua
9178 // 5.1 :262 bakes this in.
9179 let mm = if self.version() <= LuaVersion::Lua52 {
9180 self.get_comp_mm(l, r, Mm::Eq)
9181 } else {
9182 let mut m = self.get_mm(l, Mm::Eq);
9183 if m.is_nil() {
9184 m = self.get_mm(r, Mm::Eq);
9185 }
9186 m
9187 };
9188 if !mm.is_nil() {
9189 return MmOut::Mm { func: mm, recv: l };
9190 }
9191 }
9192 MmOut::Done(Value::Bool(false))
9193 }
9194
9195 // ---- arithmetic ----
9196
9197 #[inline(always)]
9198 fn arith_rr(&mut self, inst: Inst, base: u32, op: ArithOp) -> Result<(), LuaError> {
9199 let l = self.r(base, inst.b());
9200 let r = self.r(base, inst.c());
9201 // hot path: Int + Int for Add / Sub / Mul — fib_28, loop_int_1m,
9202 // binary_trees all hammer these. Skipping coerce_num + the big
9203 // arith_fast match shaves several conditional moves per op.
9204 if let (Value::Int(a), Value::Int(b)) = (l, r) {
9205 let fast = match op {
9206 ArithOp::Add => Some(Value::Int(a.wrapping_add(b))),
9207 ArithOp::Sub => Some(Value::Int(a.wrapping_sub(b))),
9208 ArithOp::Mul => Some(Value::Int(a.wrapping_mul(b))),
9209 _ => None,
9210 };
9211 if let Some(v) = fast {
9212 self.set_r(base, inst.a(), v);
9213 return Ok(());
9214 }
9215 }
9216 // hot path: Float + Float for Add / Sub / Mul / Div — math_loop_100k
9217 // and any numeric workload with non-integer accumulators benefits.
9218 if let (Value::Float(a), Value::Float(b)) = (l, r) {
9219 let fast = match op {
9220 ArithOp::Add => Some(Value::Float(a + b)),
9221 ArithOp::Sub => Some(Value::Float(a - b)),
9222 ArithOp::Mul => Some(Value::Float(a * b)),
9223 ArithOp::Div => Some(Value::Float(a / b)),
9224 _ => None,
9225 };
9226 if let Some(v) = fast {
9227 self.set_r(base, inst.a(), v);
9228 return Ok(());
9229 }
9230 }
9231 // An `Add` with k set is a 5.4+ `x - 0` (see `Op::Add`): the right
9232 // operand is the integer 0, so it adds only when the left is a number.
9233 let op = if inst.k() && op == ArithOp::Add && !matches!(l, Value::Int(_) | Value::Float(_))
9234 {
9235 ArithOp::Sub
9236 } else {
9237 op
9238 };
9239 match self.arith_fast(op, l, r)? {
9240 Some(v) => self.set_r(base, inst.a(), v),
9241 None => {
9242 let mm = self.arith_mm_func(op, l, r)?;
9243 let dst = base + inst.a();
9244 self.begin_meta_call(mm, &[l, r], MetaAction::Store { dst }, op.mm_name())?;
9245 }
9246 }
9247 Ok(())
9248 }
9249
9250 /// The number a unary `-` operand stands for: 5.4+ leaves strings to
9251 /// the string metatable, 5.3 converts them to floats (PUC `tonumber`).
9252 fn unary_operand(&self, v: Value) -> Option<Num> {
9253 let n = self.arith_operand()(v);
9254 if self.version == LuaVersion::Lua53 && matches!(v, Value::Str(_)) {
9255 n.map(|n| Num::Float(n.as_f64()))
9256 } else {
9257 n
9258 }
9259 }
9260
9261 /// How an arithmetic operand becomes a number: 5.4+ takes numbers only
9262 /// (strings go to their metatable), 5.3 converts numeric strings, and
9263 /// 5.1/5.2, which have only floats, convert them to floats (5.1 with C
9264 /// `strtod`, so `"inf"` and `"0x1p4"` count).
9265 fn arith_operand(&self) -> fn(Value) -> Option<Num> {
9266 if self.version >= LuaVersion::Lua54 {
9267 as_number
9268 } else if self.version == LuaVersion::Lua53 {
9269 coerce_num
9270 } else if self.version == LuaVersion::Lua52 {
9271 coerce_num_float
9272 } else {
9273 coerce_num_51
9274 }
9275 }
9276
9277 /// Fast path for an arithmetic/bitwise op: `Ok(Some(v))` when computed
9278 /// directly, `Ok(None)` when a metamethod is required (the caller decides
9279 /// whether to call it synchronously or yieldably).
9280 fn arith_fast(&mut self, op: ArithOp, l: Value, r: Value) -> Result<Option<Value>, LuaError> {
9281 use ArithOp::*;
9282 // 5.4 moved string->number coercion out of the VM: a string operand
9283 // goes to the string metatable's `__add` etc., and bitwise operators
9284 // have no string metamethods at all.
9285 let num = self.arith_operand();
9286 if let BAnd | BOr | BXor | Shl | Shr = op {
9287 let (Some(a), Some(b)) = (num(l), num(r)) else {
9288 return Ok(None);
9289 };
9290 let (Some(a), Some(b)) = (int_of(a), int_of(b)) else {
9291 // PUC luaG_tointerror: name the offending operand
9292 return Err(self.no_int_rep_err());
9293 };
9294 let v = match op {
9295 BAnd => a & b,
9296 BOr => a | b,
9297 BXor => a ^ b,
9298 Shl => shift_left(a, b),
9299 Shr => shift_left(a, b.wrapping_neg()),
9300 _ => unreachable!(),
9301 };
9302 return Ok(Some(Value::Int(v)));
9303 }
9304 let (Some(mut ln), Some(mut rn)) = (num(l), num(r)) else {
9305 return Ok(None);
9306 };
9307 // PUC 5.3 takes the integer path only when both operands are
9308 // integers (`ttisinteger`); a converted string goes through
9309 // `tonumber`, which yields a float.
9310 if self.version == LuaVersion::Lua53
9311 && (matches!(l, Value::Str(_)) || matches!(r, Value::Str(_)))
9312 {
9313 ln = Num::Float(ln.as_f64());
9314 rn = Num::Float(rn.as_f64());
9315 }
9316 match arith_num(self.version, op, ln, rn) {
9317 Ok(v) => Ok(Some(v)),
9318 Err(msg) => Err(self.runerror(msg)),
9319 }
9320 }
9321
9322 /// Find the arithmetic/bitwise metamethod (left operand first), or raise the
9323 /// PUC type error when neither operand provides one.
9324 fn arith_mm_func(&mut self, op: ArithOp, l: Value, r: Value) -> Result<Value, LuaError> {
9325 use ArithOp::*;
9326 let event = match op {
9327 Add => Mm::Add,
9328 Sub => Mm::Sub,
9329 Mul => Mm::Mul,
9330 Div => Mm::Div,
9331 Mod => Mm::Mod,
9332 Pow => Mm::Pow,
9333 IDiv => Mm::IDiv,
9334 BAnd => Mm::BAnd,
9335 BOr => Mm::BOr,
9336 BXor => Mm::BXor,
9337 Shl => Mm::Shl,
9338 Shr => Mm::Shr,
9339 };
9340 let mut mm = self.get_mm(l, event);
9341 if mm.is_nil() {
9342 mm = self.get_mm(r, event);
9343 }
9344 if mm.is_nil() {
9345 let what = if matches!(op, BAnd | BOr | BXor | Shl | Shr) {
9346 "perform bitwise operation on"
9347 } else {
9348 "perform arithmetic on"
9349 };
9350 // luaG_opinterror blames the first operand that is not a number;
9351 // before 5.4 a numeric string counts as one.
9352 let bad = if self.arith_operand()(l).is_none() {
9353 l
9354 } else {
9355 r
9356 };
9357 return Err(self.type_err(what, bad));
9358 }
9359 Ok(mm)
9360 }
9361
9362 // ---- comparison ----
9363
9364 /// `lua_compare(L, a, b, LUA_OPEQ)`: equality including `__eq`.
9365 pub(crate) fn equal(&mut self, l: Value, r: Value) -> Result<bool, LuaError> {
9366 match self.eq_step(l, r) {
9367 MmOut::Done(v) => Ok(v.truthy()),
9368 MmOut::Mm { func, .. } => Ok(self.call_mm1(func, &[l, r])?.truthy()),
9369 MmOut::CompareSynth { .. } => unreachable!("CompareSynth from eq_step"),
9370 }
9371 }
9372
9373 pub(crate) fn less_than(&mut self, l: Value, r: Value, or_eq: bool) -> Result<bool, LuaError> {
9374 match self.less_step(l, r, or_eq)? {
9375 MmOut::Done(v) => Ok(v.truthy()),
9376 MmOut::Mm { func, .. } => Ok(self.call_mm1(func, &[l, r])?.truthy()),
9377 MmOut::CompareSynth { func } => {
9378 // ≤5.3 `__le` via `not __lt(r, l)`. Synchronous helper used
9379 // by library code (sort comparator etc.) — no yield expected
9380 // here (a yield would have hit `call_noyield`'s C boundary).
9381 Ok(!self.call_mm1(func, &[r, l])?.truthy())
9382 }
9383 }
9384 }
9385
9386 /// Decide `l < r` / `l <= r`, or surface the `__lt`/`__le` metamethod. `Done`
9387 /// carries the boolean result; `Mm` (for non-number/string operands) carries
9388 /// the metamethod — called with `(l, r)`; raises the PUC compare error when
9389 /// neither operand provides one.
9390 fn less_step(&mut self, l: Value, r: Value, or_eq: bool) -> Result<MmOut, LuaError> {
9391 let b = match (l, r) {
9392 (Value::Int(a), Value::Int(b)) => {
9393 if or_eq {
9394 a <= b
9395 } else {
9396 a < b
9397 }
9398 }
9399 (Value::Float(a), Value::Float(b)) => {
9400 if or_eq {
9401 a <= b
9402 } else {
9403 a < b
9404 }
9405 }
9406 (Value::Int(a), Value::Float(b)) => {
9407 if or_eq {
9408 int_le_float(a, b)
9409 } else {
9410 int_lt_float(a, b)
9411 }
9412 }
9413 (Value::Float(a), Value::Int(b)) => {
9414 if a.is_nan() {
9415 false
9416 } else if or_eq {
9417 !int_lt_float(b, a)
9418 } else {
9419 !int_le_float(b, a)
9420 }
9421 }
9422 (Value::Str(a), Value::Str(b)) => {
9423 let (a, b) = (a.as_bytes(), b.as_bytes());
9424 if or_eq { a <= b } else { a < b }
9425 }
9426 (l, r) => {
9427 let event = if or_eq { Mm::Le } else { Mm::Lt };
9428 // PUC 5.1's `get_compTM` rule applies to ordered comparisons
9429 // too: both operands' metatables must expose the same
9430 // implementation for `__lt` / `__le` to fire. events.lua 5.1
9431 // :262 expects `c < d` (where `d` has no metatable) to error
9432 // with the default "attempt to compare two table values"
9433 // rather than running c's `__lt` blindly.
9434 let mm = if self.version() <= LuaVersion::Lua51 {
9435 self.get_comp_mm(l, r, event)
9436 } else {
9437 let mut m = self.get_mm(l, event);
9438 if m.is_nil() {
9439 m = self.get_mm(r, event);
9440 }
9441 m
9442 };
9443 // PUC ≤5.4: `a <= b` falls back to `not (b < a)` when neither
9444 // operand carries `__le` (5.4 through its default build's
9445 // LUA_COMPAT_LT_LE); 5.5 requires an explicit `__le`.
9446 // events.lua 5.2/5.3 :172 relies on the synthesis — its
9447 // metatable defines only `__lt`. The `__lt` is looked up as
9448 // for `b < a`: on `b` first (5.1: the same one on both). The
9449 // fallback calls `__lt(r, l)` synchronously (the suite's
9450 // `__lt` doesn't yield) and negates the result; the yieldable
9451 // `__lt` path stays reserved for the explicit `<` operator.
9452 if mm.is_nil() && or_eq && self.version < LuaVersion::Lua55 {
9453 let mm_lt = if self.version <= LuaVersion::Lua51 {
9454 self.get_comp_mm(r, l, Mm::Lt)
9455 } else {
9456 let m = self.get_mm(r, Mm::Lt);
9457 if m.is_nil() {
9458 self.get_mm(l, Mm::Lt)
9459 } else {
9460 m
9461 }
9462 };
9463 if !mm_lt.is_nil() {
9464 return Ok(MmOut::CompareSynth { func: mm_lt });
9465 }
9466 }
9467 if mm.is_nil() {
9468 // PUC luaG_ordererror: "two X values" when the operand
9469 // types match, "X with Y" otherwise (objtypename-aware).
9470 let (t1, t2) = (self.obj_typename(l), self.obj_typename(r));
9471 return Err(self.runerror(&if t1 == t2 {
9472 format!("attempt to compare two {t1} values")
9473 } else {
9474 format!("attempt to compare {t1} with {t2}")
9475 }));
9476 }
9477 return Ok(MmOut::Mm { func: mm, recv: l });
9478 }
9479 };
9480 Ok(MmOut::Done(Value::Bool(b)))
9481 }
9482
9483 // ---- numeric for ----
9484
9485 /// Check and convert a numeric for's control values the way the
9486 /// dialect's `OP_FORPREP` does. The integer loop is chosen by the
9487 /// values' tags (a numeric string makes it a float loop, 5.3+); the
9488 /// check order, wording and the zero-step error differ per version:
9489 /// 5.1/5.2 test initial value, limit, step; 5.3+ limit, step, initial
9490 /// value; only 5.4+ reject a zero step, and an integer loop does that
9491 /// before looking at the limit.
9492 fn for_operands(&mut self, base: u32, a: u32) -> Result<(Num, Num, Num), LuaError> {
9493 let (init, limit, step) = (self.r(base, a), self.r(base, a + 1), self.r(base, a + 2));
9494 let v = self.version();
9495 let order = if v <= LuaVersion::Lua52 {
9496 [("initial value", init), ("limit", limit), ("step", step)]
9497 } else {
9498 [("limit", limit), ("step", step), ("initial value", init)]
9499 };
9500 if v >= LuaVersion::Lua54 && matches!((init, step), (Value::Int(_), Value::Int(0))) {
9501 return Err(self.rt_err("'for' step is zero"));
9502 }
9503 for (what, val) in order {
9504 if as_num(val, v).is_none() {
9505 return Err(self.rt_err(&if v >= LuaVersion::Lua54 {
9506 format!(
9507 "bad 'for' {what} (number expected, got {})",
9508 self.obj_typename(val)
9509 )
9510 } else {
9511 format!("'for' {what} must be a number")
9512 }));
9513 }
9514 }
9515 let n = |val| as_num(val, v).expect("checked above");
9516 let int_loop =
9517 v <= LuaVersion::Lua52 || matches!((init, step), (Value::Int(_), Value::Int(_)));
9518 if int_loop {
9519 return Ok((n(init), n(limit), n(step)));
9520 }
9521 let (i, l, st) = (n(init).as_f64(), n(limit).as_f64(), n(step).as_f64());
9522 if v >= LuaVersion::Lua54 && st == 0.0 {
9523 return Err(self.rt_err("'for' step is zero"));
9524 }
9525 Ok((Num::Float(i), Num::Float(l), Num::Float(st)))
9526 }
9527
9528 fn for_prep(&mut self, inst: Inst, base: u32) -> Result<(), LuaError> {
9529 let a = inst.a();
9530 let (init_n, limit_n, step_n) = self.for_operands(base, a)?;
9531 // PUC 5.1–5.3 `OP_FORPREP` stores `i = init - step` and *unconditionally*
9532 // jumps to the matching `OP_FORLOOP` — the body never runs ahead of the
9533 // first test, so each successful iteration emits a backward `OP_FORLOOP`
9534 // jump (db.lua's `for i=1,4 do a=1 end` ↦ 5 line-hook events instead of
9535 // 5.4's 4). 5.4+ collapsed that to a count-based fall-through. The skip
9536 // distance in luna's encoding is `loop_pc - prep_pc`; firing
9537 // `add_pc(bx - 1)` lands the running pc on OP_FORLOOP itself.
9538 let pre53 = self.version() <= LuaVersion::Lua53;
9539 match (init_n, step_n) {
9540 (Num::Int(i0), Num::Int(st)) => {
9541 if pre53 {
9542 // PUC 5.3 `forlimit`: int limit passes through; float limit
9543 // gets clamped to MIN/MAX with a `stopnow` flag set only
9544 // when the clamp is unreachable (positive float with a
9545 // negative step → limit=MAX, stopnow; negative float with
9546 // step>=0 → limit=MIN, stopnow). On `stopnow` PUC rewrites
9547 // `init = 0` so OP_FORLOOP's first test against the
9548 // unreachable clamp fails cleanly. An ordinary in-range
9549 // empty loop (e.g. `for i = 1, 0`) is *not* `stopnow` — it
9550 // lets OP_FORLOOP's natural test reject the first step.
9551 let (lim, stopnow) = match limit_n {
9552 Num::Int(l) => (l, false),
9553 Num::Float(f) => {
9554 // `luaV_tointeger` floors (ceils for a negative
9555 // step); a float it cannot fit is clamped on
9556 // the side of its sign, NaN counting as
9557 // negative (`0 < n` is false).
9558 let conv = if st < 0 { f.ceil() } else { f.floor() };
9559 if (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0)
9560 .contains(&conv)
9561 {
9562 (conv as i64, false)
9563 } else if f > 0.0 {
9564 (i64::MAX, st < 0)
9565 } else {
9566 (i64::MIN, st > 0)
9567 }
9568 }
9569 };
9570 let initv = if stopnow { 0 } else { i0 };
9571 let pre = initv.wrapping_sub(st);
9572 self.set_r(base, a, Value::Int(pre));
9573 self.set_r(base, a + 1, Value::Int(lim));
9574 self.set_r(base, a + 2, Value::Int(st));
9575 self.add_pc(inst.bx() as i32 - 1);
9576 return Ok(());
9577 }
9578 let (lim, empty) = int_for_limit(limit_n, i0, st);
9579 if empty {
9580 self.add_pc(inst.bx() as i32);
9581 return Ok(());
9582 }
9583 let count = if st > 0 {
9584 (lim as u64).wrapping_sub(i0 as u64) / (st as u64)
9585 } else {
9586 (i0 as u64).wrapping_sub(lim as u64) / (st as i128).unsigned_abs() as u64
9587 };
9588 self.set_r(base, a, Value::Int(i0));
9589 self.set_r(base, a + 1, Value::Int(count as i64));
9590 self.set_r(base, a + 2, Value::Int(st));
9591 self.set_r(base, a + 3, Value::Int(i0));
9592 }
9593 _ => {
9594 let (x0, lim, st) = (init_n.as_f64(), limit_n.as_f64(), step_n.as_f64());
9595 if pre53 {
9596 let pre = x0 - st;
9597 self.set_r(base, a, Value::Float(pre));
9598 self.set_r(base, a + 1, Value::Float(lim));
9599 self.set_r(base, a + 2, Value::Float(st));
9600 self.add_pc(inst.bx() as i32 - 1);
9601 return Ok(());
9602 }
9603 // lvm.c `forprep`: skip only when `0 < step ? limit < init :
9604 // init < limit`; a NaN makes both false, so the body runs
9605 // once (with a NaN step, on the second test's side)
9606 let skip = if 0.0 < st { lim < x0 } else { x0 < lim };
9607 let runs = !skip;
9608 if !runs {
9609 self.add_pc(inst.bx() as i32);
9610 return Ok(());
9611 }
9612 self.set_r(base, a, Value::Float(x0));
9613 self.set_r(base, a + 1, Value::Float(lim));
9614 self.set_r(base, a + 2, Value::Float(st));
9615 self.set_r(base, a + 3, Value::Float(x0));
9616 }
9617 }
9618 Ok(())
9619 }
9620
9621 #[inline(always)]
9622 fn for_loop(&mut self, inst: Inst, base: u32) -> Result<(), LuaError> {
9623 let a = inst.a();
9624 // PUC 5.1–5.3 `OP_FORLOOP` compares the post-step `i` to `limit`
9625 // directly (R[a+1] holds the limit, *not* a remaining-count) so the
9626 // first iteration's test fires through the same backward-jump path as
9627 // every later iteration. 5.4+ switched to the count-based form luna
9628 // already uses for `Int`; the float branch was already PUC-3.x-style.
9629 let v = self.version();
9630 let pre53 = v <= LuaVersion::Lua53;
9631 // `for_prep` leaves the three slots all Int or all Float; anything
9632 // else was written by `debug.setlocal` or by crafted bytecode. PUC
9633 // reads such slots unchecked (garbage or a crash); luna raises.
9634 match (self.r(base, a), self.r(base, a + 1), self.r(base, a + 2)) {
9635 (Value::Int(cur), Value::Int(lim), Value::Int(st)) if pre53 => {
9636 let next = cur.wrapping_add(st);
9637 let cont = if st > 0 { next <= lim } else { next >= lim };
9638 if cont {
9639 self.set_r(base, a, Value::Int(next));
9640 self.set_r(base, a + 3, Value::Int(next));
9641 self.add_pc(-(inst.bx() as i32));
9642 }
9643 }
9644 // the count is unsigned (PUC `lua_Unsigned`): a loop over
9645 // more than 2^63 values stores a "negative" one
9646 (Value::Int(cur), Value::Int(count), Value::Int(st)) => {
9647 if count != 0 {
9648 let next = cur.wrapping_add(st);
9649 self.set_r(base, a, Value::Int(next));
9650 self.set_r(base, a + 1, Value::Int(count.wrapping_sub(1)));
9651 self.set_r(base, a + 3, Value::Int(next));
9652 self.add_pc(-(inst.bx() as i32));
9653 }
9654 }
9655 (Value::Float(cur), Value::Float(lim), Value::Float(st)) => {
9656 self.float_for_step(inst, base, cur, lim, st);
9657 }
9658 // 5.1/5.2 have one number type, so a number of the other
9659 // representation stored into a slot is still a valid state
9660 (x, l, s) if v <= LuaVersion::Lua52 => {
9661 match (as_number(x), as_number(l), as_number(s)) {
9662 (Some(cur), Some(lim), Some(st)) => {
9663 self.float_for_step(inst, base, cur.as_f64(), lim.as_f64(), st.as_f64())
9664 }
9665 _ => return Err(self.rt_err("'for' state corrupted")),
9666 }
9667 }
9668 _ => return Err(self.rt_err("'for' state corrupted")),
9669 }
9670 Ok(())
9671 }
9672
9673 #[inline(always)]
9674 fn float_for_step(&mut self, inst: Inst, base: u32, cur: f64, lim: f64, st: f64) {
9675 let a = inst.a();
9676 let next = cur + st;
9677 let cont = if st > 0.0 { next <= lim } else { next >= lim };
9678 if cont {
9679 self.set_r(base, a, Value::Float(next));
9680 self.set_r(base, a + 3, Value::Float(next));
9681 self.add_pc(-(inst.bx() as i32));
9682 }
9683 }
9684
9685 // ---- native helpers (used by builtins) ----
9686
9687 /// A native function's own captured upvalue (self lives at func_slot).
9688 ///
9689 /// Public so `native_typed` trampolines and embedders authoring
9690 /// stateful natives via `native_with(...)` can read their upvals.
9691 pub fn nat_upval(&self, func_slot: u32, i: usize) -> Value {
9692 let Value::Native(nc) = self.stack[func_slot as usize] else {
9693 unreachable!("native frame without native closure");
9694 };
9695 nc.upvals[i]
9696 }
9697
9698 /// Number of upvalues captured by the native at `func_slot` (variadic
9699 /// captures such as the `io.lines` format list).
9700 pub(crate) fn nat_upcount(&self, func_slot: u32) -> usize {
9701 let Value::Native(nc) = self.stack[func_slot as usize] else {
9702 unreachable!("native frame without native closure");
9703 };
9704 nc.upvals.len()
9705 }
9706
9707 /// Write a native function's own upvalue (stateful iterators).
9708 pub(crate) fn nat_set_upval(&mut self, func_slot: u32, i: usize, v: Value) {
9709 let Value::Native(nc) = self.stack[func_slot as usize] else {
9710 unreachable!("native frame without native closure");
9711 };
9712 // 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).
9713 unsafe { nc.as_mut() }.upvals[i] = v;
9714 // NativeClosure.upvals is traced as part of its Trace; a long-lived
9715 // stateful iterator closure (e.g. string.gmatch) sees many writes —
9716 // barrier_back once-and-done is cheaper than per-child forward.
9717 self.heap
9718 .barrier_back(nc.as_ptr() as *mut crate::runtime::heap::GcHeader);
9719 }
9720
9721 /// Read the i-th positional argument inside a `NativeFn` body
9722 /// (analogous to `lua_tovalue(L, i + 1)`). `i >= nargs` yields `Nil`,
9723 /// matching PUC's "missing arg is nil" contract. Public so embedders
9724 /// can author their own natives.
9725 pub fn nat_arg(&self, func_slot: u32, nargs: u32, i: u32) -> Value {
9726 if i < nargs {
9727 self.stack[(func_slot + 1 + i) as usize]
9728 } else {
9729 Value::Nil
9730 }
9731 }
9732
9733 /// Overwrite the i-th argument slot of the running native (the in-place
9734 /// conversion `lua_tolstring` performs on a number argument).
9735 pub(crate) fn nat_set_arg(&mut self, func_slot: u32, i: u32, v: Value) {
9736 self.stack[(func_slot + 1 + i) as usize] = v;
9737 }
9738
9739 /// Push the return values of a `NativeFn` and return their count
9740 /// (analogous to pushing N values then `return N` from a C function).
9741 /// Public so embedders can author their own natives.
9742 pub fn nat_return(&mut self, func_slot: u32, vals: &[Value]) -> u32 {
9743 let need = func_slot as usize + vals.len();
9744 if self.stack.len() < need {
9745 self.stack.resize(need, Value::Nil);
9746 }
9747 for (i, &v) in vals.iter().enumerate() {
9748 self.stack[func_slot as usize + i] = v;
9749 }
9750 vals.len() as u32
9751 }
9752
9753 /// Fast string concatenation of an adjacent pair, or `None` when a
9754 /// `__concat` metamethod is required.
9755 fn concat_pair(&mut self, l: Value, r: Value) -> Result<Option<Value>, LuaError> {
9756 let legacy = self.float_fmt();
9757 // Length-check fast paths for both string operands BEFORE the
9758 // (expensive) copy in `concat_piece`, so a runaway `a..a..a..…`
9759 // chain (5.1 big.lua / 5.5 heavy.lua's `teststring`) raises the
9760 // overflow on the first pair that would exceed `INT_MAX` instead
9761 // of allocating multi-GB intermediates first.
9762 let max_str = i32::MAX as usize;
9763 if let (Value::Str(ls), Value::Str(rs)) = (l, r) {
9764 let a_len = ls.as_bytes().len();
9765 let b_len = rs.as_bytes().len();
9766 let new_len = a_len.checked_add(b_len);
9767 if new_len.is_none() || new_len.unwrap() > max_str {
9768 return Err(self.rt_err("string length overflow"));
9769 }
9770 }
9771 match (concat_piece(l, legacy), concat_piece(r, legacy)) {
9772 (Some(a), Some(b)) => {
9773 // PUC `MAX_SIZE` for Lua strings is `INT_MAX`; an attempt to
9774 // concat past it raises "string length overflow"
9775 // (5.5 heavy.lua `teststring` doubles `a..a..…` until it hits
9776 // exactly this wall).
9777 let new_len = a.len().checked_add(b.len());
9778 if new_len.is_none() || new_len.unwrap() > max_str {
9779 return Err(self.rt_err("string length overflow"));
9780 }
9781 let mut combined = a;
9782 combined.extend_from_slice(&b);
9783 Ok(Some(Value::Str(self.heap.intern(&combined))))
9784 }
9785 _ => Ok(None),
9786 }
9787 }
9788
9789 /// Fold the concat operands occupying `[base_a .. self.top)` right-to-left
9790 /// into a single result at `base_a` (PUC `luaV_concat`). Returns after
9791 /// either finishing (result at `base_a`) or arming a yieldable `__concat`
9792 /// call — its `Meta` continuation re-enters here on the metamethod's return.
9793 fn concat_run(&mut self, base_a: u32) -> Result<(), LuaError> {
9794 // Sum the lengths of all all-Str operands BEFORE starting the
9795 // right-associative fold so a 129-operand `a..a..…` chain
9796 // (5.1 big.lua's `rep129(longs)`) raises overflow immediately,
9797 // not after dozens of multi-GB intermediate intern+hash rounds.
9798 // A non-Str operand falls through to the per-pair check.
9799 let max_str = i32::MAX as usize;
9800 let mut total: usize = 0;
9801 let mut all_str = true;
9802 for slot in base_a..self.top {
9803 match self.stack[slot as usize] {
9804 Value::Str(s) => match total.checked_add(s.as_bytes().len()) {
9805 Some(t) if t <= max_str => total = t,
9806 _ => return Err(self.rt_err("string length overflow")),
9807 },
9808 _ => {
9809 all_str = false;
9810 break;
9811 }
9812 }
9813 }
9814 let _ = all_str; // discrimination already captured by early returns above
9815 while self.top.saturating_sub(base_a) >= 2 {
9816 let i = self.top - 1; // rightmost operand
9817 let x = self.stack[(i - 1) as usize];
9818 let y = self.stack[i as usize];
9819 match self.concat_pair(x, y)? {
9820 Some(s) => {
9821 self.stack[(i - 1) as usize] = s;
9822 self.top = i; // consumed y
9823 }
9824 None => {
9825 let mut mm = self.get_mm(x, Mm::Concat);
9826 if mm.is_nil() {
9827 mm = self.get_mm(y, Mm::Concat);
9828 }
9829 if mm.is_nil() {
9830 let legacy = self.float_fmt();
9831 let bad = if concat_piece(x, legacy).is_none() {
9832 x
9833 } else {
9834 y
9835 };
9836 return Err(self.type_err("concatenate", bad));
9837 }
9838 // result lands at i-1, dropping y (top→i); resume continues.
9839 let dst = i - 1;
9840 self.begin_meta_call(
9841 mm,
9842 &[x, y],
9843 MetaAction::Concat { dst, base_a },
9844 "concat",
9845 )?;
9846 return Ok(());
9847 }
9848 }
9849 }
9850 self.maybe_collect_garbage(base_a + 1);
9851 Ok(())
9852 }
9853
9854 /// `luaL_tolstring`: `__tostring` (whose result must be a string or a
9855 /// number, rendered), else the basic rendering, where 5.3+ names a value
9856 /// by a string `__name` metafield.
9857 pub fn tostring_value(&mut self, v: Value) -> Result<Vec<u8>, LuaError> {
9858 let mm = self.get_mm(v, Mm::ToString);
9859 if !mm.is_nil() {
9860 // `luaL_callmeta` is a plain `lua_call`: `__tostring` cannot yield.
9861 let r = self.call_noyield(mm, &[v])?;
9862 return match r.first().copied().unwrap_or(Value::Nil) {
9863 Value::Str(s) => Ok(s.as_bytes().to_vec()),
9864 r @ (Value::Int(_) | Value::Float(_)) => Ok(self.tostring_basic(r)),
9865 // luaL_error: positioned at whatever called the library function
9866 _ => Err(crate::vm::builtins::raise_str(
9867 self,
9868 "'__tostring' must return a string",
9869 )),
9870 };
9871 }
9872 if self.version >= LuaVersion::Lua53
9873 && !matches!(
9874 v,
9875 Value::Nil | Value::Bool(_) | Value::Int(_) | Value::Float(_) | Value::Str(_)
9876 )
9877 && let Value::Str(name) = self.get_mm(v, Mm::Name)
9878 {
9879 let basic = self.tostring_basic(v);
9880 let at = basic
9881 .iter()
9882 .position(|&c| c == b':')
9883 .expect("an object renders as `kind: address`");
9884 let mut out = name.as_bytes().to_vec();
9885 out.extend_from_slice(&basic[at..]);
9886 return Ok(out);
9887 }
9888 Ok(self.tostring_basic(v))
9889 }
9890
9891 /// The dialect's float-rendering flavor: ≤5.2 %.14g
9892 /// bare, 5.3/5.4 %.14g + ".0", 5.5 two-stage %.15g/%.17g + ".0".
9893 pub(crate) fn float_fmt(&self) -> numeric::FloatFmt {
9894 use crate::version::LuaVersion::*;
9895 match self.version {
9896 Lua51 | Lua52 => numeric::FloatFmt::Legacy14,
9897 Lua53 | Lua54 => numeric::FloatFmt::G14,
9898 _ => numeric::FloatFmt::TwoStage55,
9899 }
9900 }
9901
9902 /// Basic tostring (no metamethods).
9903 pub(crate) fn tostring_basic(&mut self, v: Value) -> Vec<u8> {
9904 match v {
9905 Value::Nil => b"nil".to_vec(),
9906 Value::Bool(true) => b"true".to_vec(),
9907 Value::Bool(false) => b"false".to_vec(),
9908 Value::Int(i) => numeric::num_to_string(Num::Int(i)).into_bytes(),
9909 // PUC ≤5.2 has no integer subtype — `tostring(2.0)` is `"2"`, not
9910 // `"2.0"`. The 5.3+ split needs the suffix so `print(2.0)` is
9911 // distinguishable from `print(2)`. pm.lua :13 builds patterns by
9912 // concatenating these renderings.
9913 Value::Float(f) => {
9914 numeric::num_to_string_for(Num::Float(f), self.float_fmt()).into_bytes()
9915 }
9916 Value::Str(s) => s.as_bytes().to_vec(),
9917 Value::Table(t) => format!("table: {:p}", t.as_ptr()).into_bytes(),
9918 Value::Closure(c) => format!("function: {:p}", c.as_ptr()).into_bytes(),
9919 Value::Native(n) => format!("function: {:p}", n.as_ptr()).into_bytes(),
9920 Value::Coro(co) => format!("thread: {:p}", co.as_ptr()).into_bytes(),
9921 // PUC names file handles `file (0x…)`; a bare userdata is
9922 // `userdata: 0x…`. The io library overrides this via __tostring.
9923 Value::Userdata(u) => format!("userdata: {:p}", u.as_ptr()).into_bytes(),
9924 // PUC `lua_topointer`/tostring on light udata: "userdata: 0x…"
9925 // (the "light" qualifier only appears in `luaL_typeerror`).
9926 Value::LightUserdata(p) => format!("userdata: {p:p}").into_bytes(),
9927 }
9928 }
9929}
9930
9931#[derive(Clone, Copy, PartialEq, Eq)]
9932pub(crate) enum ArithOp {
9933 Add,
9934 Sub,
9935 Mul,
9936 Mod,
9937 Pow,
9938 Div,
9939 IDiv,
9940 BAnd,
9941 BOr,
9942 BXor,
9943 Shl,
9944 Shr,
9945}
9946
9947impl ArithOp {
9948 /// PUC metamethod event name (`__add` → "add" etc.) used by
9949 /// `debug.getinfo(level, "n")` inside a metamethod handler.
9950 fn mm_name(self) -> &'static str {
9951 match self {
9952 ArithOp::Add => "add",
9953 ArithOp::Sub => "sub",
9954 ArithOp::Mul => "mul",
9955 ArithOp::Mod => "mod",
9956 ArithOp::Pow => "pow",
9957 ArithOp::Div => "div",
9958 ArithOp::IDiv => "idiv",
9959 ArithOp::BAnd => "band",
9960 ArithOp::BOr => "bor",
9961 ArithOp::BXor => "bxor",
9962 ArithOp::Shl => "shl",
9963 ArithOp::Shr => "shr",
9964 }
9965 }
9966}
9967
9968fn as_num(v: Value, version: LuaVersion) -> Option<Num> {
9969 match v {
9970 Value::Int(i) => Some(Num::Int(i)),
9971 Value::Float(f) => Some(Num::Float(f)),
9972 // PUC forprep coerces numeric strings (`for i = "10", "1", "-2"`).
9973 Value::Str(s) => str_to_num(s.as_bytes(), version),
9974 _ => None,
9975 }
9976}
9977
9978/// Float `%` as each dialect's `luai_nummod` defines it. 5.1/5.2 compute
9979/// `a - floor(a/b)*b` (so `5 % math.huge` is nan); 5.3 fixes `fmod`'s sign
9980/// when `m*b < 0`; 5.4 compares signs instead, because that product
9981/// underflows to zero for tiny operands.
9982fn float_mod(version: LuaVersion, a: f64, b: f64) -> f64 {
9983 if version <= LuaVersion::Lua52 {
9984 return a - (a / b).floor() * b;
9985 }
9986 let m = a % b;
9987 let fix = if version == LuaVersion::Lua53 {
9988 m * b < 0.0
9989 } else {
9990 (m > 0.0 && b < 0.0) || (m < 0.0 && b > 0.0)
9991 };
9992 if fix { m + b } else { m }
9993}
9994
9995/// A concatenable operand's byte form (string, or a number coerced to its
9996/// string), or `None` when only a `__concat` metamethod can handle it.
9997/// `legacy_float = true` follows PUC ≤5.2's `%.14g` rendering (no `.0`
9998/// suffix on integer-valued floats) — see `num_to_string_for`.
9999fn concat_piece(v: Value, float_fmt: numeric::FloatFmt) -> Option<Vec<u8>> {
10000 match v {
10001 Value::Str(s) => Some(s.as_bytes().to_vec()),
10002 Value::Int(x) => Some(numeric::num_to_string(Num::Int(x)).into_bytes()),
10003 Value::Float(x) => Some(numeric::num_to_string_for(Num::Float(x), float_fmt).into_bytes()),
10004 _ => None,
10005 }
10006}
10007
10008/// Index into the per-basic-type metatable table for a non-table value
10009/// (None for tables, which carry their own metatable).
10010fn type_mt_slot(v: Value) -> Option<usize> {
10011 match v {
10012 Value::Nil => Some(0),
10013 Value::Bool(_) => Some(1),
10014 Value::Int(_) | Value::Float(_) => Some(2),
10015 Value::Str(_) => Some(3),
10016 Value::Closure(_) | Value::Native(_) => Some(4),
10017 // tables and full userdata carry their own metatable; threads and
10018 // light userdata have none (PUC keeps a shared per-type mt slot for
10019 // light, but luna doesn't expose it — no test gates on it yet).
10020 Value::Table(_) | Value::Coro(_) | Value::Userdata(_) | Value::LightUserdata(_) => None,
10021 }
10022}
10023
10024/// A number operand as-is; strings stay non-numbers (5.4+ `tonumberns`).
10025fn as_number(v: Value) -> Option<Num> {
10026 match v {
10027 Value::Int(i) => Some(Num::Int(i)),
10028 Value::Float(f) => Some(Num::Float(f)),
10029 _ => None,
10030 }
10031}
10032
10033/// Arithmetic (not bitwise) on two numbers, PUC `luaO_rawarith`: integer
10034/// results for integer operands except `/` and `^`. The error is the
10035/// message of a zero integer divisor.
10036pub(crate) fn arith_num(
10037 version: LuaVersion,
10038 op: ArithOp,
10039 ln: Num,
10040 rn: Num,
10041) -> Result<Value, &'static str> {
10042 use ArithOp::*;
10043 Ok(match (op, ln, rn) {
10044 (Add, Num::Int(a), Num::Int(b)) => Value::Int(a.wrapping_add(b)),
10045 (Sub, Num::Int(a), Num::Int(b)) => Value::Int(a.wrapping_sub(b)),
10046 (Mul, Num::Int(a), Num::Int(b)) => Value::Int(a.wrapping_mul(b)),
10047 (IDiv, Num::Int(a), Num::Int(b)) => {
10048 if b == 0 {
10049 return Err("attempt to divide by zero");
10050 }
10051 let mut q = a.wrapping_div(b);
10052 if (a ^ b) < 0 && q.wrapping_mul(b) != a {
10053 q -= 1;
10054 }
10055 Value::Int(q)
10056 }
10057 (Mod, Num::Int(a), Num::Int(b)) => {
10058 if b == 0 {
10059 return Err("attempt to perform 'n%0'");
10060 }
10061 let mut m = a.wrapping_rem(b);
10062 if m != 0 && (m ^ b) < 0 {
10063 m += b;
10064 }
10065 Value::Int(m)
10066 }
10067 (Add, a, b) => Value::Float(a.as_f64() + b.as_f64()),
10068 (Sub, a, b) => Value::Float(a.as_f64() - b.as_f64()),
10069 (Mul, a, b) => Value::Float(a.as_f64() * b.as_f64()),
10070 (Div, a, b) => Value::Float(a.as_f64() / b.as_f64()),
10071 // 5.4+ `luai_numpow` squares by multiplying, which can differ from
10072 // the C library's `pow` in the last bit
10073 (Pow, a, b) => {
10074 let (a, b) = (a.as_f64(), b.as_f64());
10075 Value::Float(if b == 2.0 && version >= LuaVersion::Lua54 {
10076 a * a
10077 } else {
10078 a.powf(b)
10079 })
10080 }
10081 (IDiv, a, b) => Value::Float((a.as_f64() / b.as_f64()).floor()),
10082 (Mod, a, b) => Value::Float(float_mod(version, a.as_f64(), b.as_f64())),
10083 (BAnd | BOr | BXor | Shl | Shr, ..) => unreachable!("bitwise op in arith_num"),
10084 })
10085}
10086
10087/// A number's integer value, if it has one.
10088fn int_of(n: Num) -> Option<i64> {
10089 match n {
10090 Num::Int(i) => Some(i),
10091 Num::Float(f) => crate::runtime::value::f2i_exact(f),
10092 }
10093}
10094
10095/// A number, or a numeric string read as a float (5.2).
10096fn coerce_num_float(v: Value) -> Option<Num> {
10097 match v {
10098 Value::Str(s) => numeric::str2num(s.as_bytes(), false, true),
10099 v => as_number(v),
10100 }
10101}
10102
10103/// Number, or string coerced to number (5.3 string-arith coercion).
10104fn coerce_num(v: Value) -> Option<Num> {
10105 match v {
10106 Value::Str(s) => numeric::str2num(s.as_bytes(), true, true),
10107 v => as_number(v),
10108 }
10109}
10110
10111/// A number, or a numeric string read by C `strtod` (5.1 `luaO_str2d`).
10112fn coerce_num_51(v: Value) -> Option<Num> {
10113 match v {
10114 Value::Str(s) => numeric::strtod_str(s.as_bytes()).map(Num::Float),
10115 v => as_number(v),
10116 }
10117}
10118
10119/// A string's numeric value as the dialect converts it: 5.1 runs C
10120/// `strtod` over it (so `inf`/`nan` count and a NUL ends it), 5.2 has its
10121/// own reader but still only floats, 5.3+ `luaO_str2num` with integers.
10122pub(crate) fn str_to_num(s: &[u8], version: LuaVersion) -> Option<Num> {
10123 match version {
10124 LuaVersion::Lua51 => numeric::strtod_str(s).map(Num::Float),
10125 LuaVersion::Lua52 => numeric::str2num(s, false, true),
10126 _ => numeric::str2num(s, true, true),
10127 }
10128}
10129
10130/// Lua shifts: logical on 64 bits; |shift| ≥ 64 yields 0; negative shifts
10131/// reverse direction.
10132fn shift_left(a: i64, b: i64) -> i64 {
10133 if b < 0 {
10134 if b <= -64 {
10135 0
10136 } else {
10137 ((a as u64) >> (-b as u32)) as i64
10138 }
10139 } else if b >= 64 {
10140 0
10141 } else {
10142 ((a as u64) << (b as u32)) as i64
10143 }
10144}
10145
10146/// i < f, exactly (PUC LTintfloat shape).
10147fn int_lt_float(i: i64, f: f64) -> bool {
10148 if f.is_nan() {
10149 return false;
10150 }
10151 if f >= 9_223_372_036_854_775_808.0 {
10152 return true;
10153 }
10154 if f < -9_223_372_036_854_775_808.0 {
10155 return false;
10156 }
10157 let ff = f.floor();
10158 let fi = ff as i64;
10159 if f == ff { i < fi } else { i <= fi }
10160}
10161
10162/// i <= f, exactly.
10163fn int_le_float(i: i64, f: f64) -> bool {
10164 if f.is_nan() {
10165 return false;
10166 }
10167 if f >= 9_223_372_036_854_775_808.0 {
10168 return true;
10169 }
10170 if f < -9_223_372_036_854_775_808.0 {
10171 return false;
10172 }
10173 i <= f.floor() as i64
10174}
10175
10176/// Clip a numeric `for` limit to the integer range (PUC forlimit). Returns
10177/// (clipped limit, loop-is-empty).
10178fn int_for_limit(limit: Num, init: i64, step: i64) -> (i64, bool) {
10179 match limit {
10180 Num::Int(l) => {
10181 let empty = if step > 0 { init > l } else { init < l };
10182 (l, empty)
10183 }
10184 Num::Float(f) => {
10185 // PUC `forlimit` treats NaN like a limit below the integer
10186 // range (`0 < flim` is false): no run upward, down to minint
10187 // otherwise.
10188 if f.is_nan() {
10189 return if step > 0 {
10190 (0, true)
10191 } else {
10192 (i64::MIN, false)
10193 };
10194 }
10195 if step > 0 {
10196 if f >= 9_223_372_036_854_775_808.0 {
10197 (i64::MAX, false)
10198 } else {
10199 let l = f.floor();
10200 if l < -9_223_372_036_854_775_808.0 {
10201 (i64::MIN, true)
10202 } else {
10203 let li = l as i64;
10204 (li, init > li)
10205 }
10206 }
10207 } else if f <= -9_223_372_036_854_775_808.0 {
10208 (i64::MIN, false)
10209 } else {
10210 let l = f.ceil();
10211 if l >= 9_223_372_036_854_775_808.0 {
10212 // PUC forlimit: a positive limit beyond the integer range
10213 // is unreachable for a decreasing loop — empty.
10214 (i64::MAX, true)
10215 } else {
10216 let li = l as i64;
10217 (li, init < li)
10218 }
10219 }
10220 }
10221 }
10222}
10223
10224impl Vm {
10225 /// PUC's debug-API placeholder for an unnamed vararg slot returned by
10226 /// `debug.getlocal(_, -n)`. 5.2/5.3 spelled it `"(*vararg)"`; 5.4
10227 /// dropped the asterisk in favour of `"(vararg)"`. db.lua 5.2 :189 /
10228 /// 5.3 :195 / 5.4 :286 baseline on their respective form.
10229 pub(crate) fn vararg_locvar_name(&self) -> &'static str {
10230 if matches!(self.version, LuaVersion::Lua52 | LuaVersion::Lua53) {
10231 "(*vararg)"
10232 } else {
10233 "(vararg)"
10234 }
10235 }
10236
10237 /// PUC's debug-API placeholder for an unnamed temporary on a C
10238 /// activation. 5.2/5.3 reported `"(*temporary)"`; 5.4 switched to
10239 /// `"(C temporary)"`. db.lua 5.2 :288, 5.3 :312, 5.4 :404 each pin
10240 /// their spelling.
10241 pub(crate) fn temporary_locvar_name(&self) -> &'static str {
10242 if matches!(
10243 self.version,
10244 LuaVersion::Lua51 | LuaVersion::Lua52 | LuaVersion::Lua53
10245 ) {
10246 // PUC 5.1's `findlocal` C-frame branch reported `(*temporary)`
10247 // (db.lua :228 pins it). 5.2/5.3 kept the spelling, 5.4 changed
10248 // to `(C temporary)`.
10249 "(*temporary)"
10250 } else {
10251 "(C temporary)"
10252 }
10253 }
10254
10255 /// PUC's debug-API placeholder for an unnamed Lua-frame temporary
10256 /// (an arithmetic intermediate sitting past the last named local on a
10257 /// live register slot). 5.2/5.3 reported `"(*temporary)"`; 5.4 dropped
10258 /// the asterisk to `"(temporary)"`. db.lua 5.3 :786, 5.4 :966 pin the
10259 /// spelling.
10260 pub(crate) fn lua_temporary_locvar_name(&self) -> &'static str {
10261 if matches!(
10262 self.version,
10263 LuaVersion::Lua51 | LuaVersion::Lua52 | LuaVersion::Lua53
10264 ) {
10265 "(*temporary)"
10266 } else {
10267 "(temporary)"
10268 }
10269 }
10270
10271 /// PUC `pushglobalfuncname`: walk `package.loaded` to depth 2 looking for a
10272 /// native whose function pointer matches `target`, and return its qualified
10273 /// name (e.g. `"table.sort"`). A `_G.X` match is stripped to `"X"`. Returns
10274 /// `None` if no match is found. Used by `arg_error` when the running native
10275 /// was invoked from another native (PUC `ar.name == NULL` at level 0).
10276 pub(crate) fn pushglobalfuncname(
10277 &mut self,
10278 target: crate::runtime::value::NativeFn,
10279 ) -> Option<String> {
10280 let pkg_k = Value::Str(self.heap.intern(b"package"));
10281 let pkg = match self.globals().get(pkg_k) {
10282 Value::Table(t) => t,
10283 _ => return None,
10284 };
10285 let loaded_k = Value::Str(self.heap.intern(b"loaded"));
10286 let loaded = match pkg.get(loaded_k) {
10287 Value::Table(t) => t,
10288 _ => return None,
10289 };
10290 let matches = |v: Value| -> bool {
10291 matches!(v, Value::Native(nc) if std::ptr::fn_addr_eq(nc.f, target))
10292 };
10293 let mut k = Value::Nil;
10294 while let Ok(Some((nk, nv))) = loaded.next(k) {
10295 k = nk;
10296 let Value::Str(outer) = nk else { continue };
10297 let outer = String::from_utf8_lossy(outer.as_bytes()).into_owned();
10298 if matches(nv) {
10299 return Some(if outer == "_G" { String::new() } else { outer });
10300 }
10301 if let Value::Table(inner_t) = nv {
10302 let mut k2 = Value::Nil;
10303 while let Ok(Some((nk2, nv2))) = inner_t.next(k2) {
10304 k2 = nk2;
10305 if matches(nv2)
10306 && let Value::Str(inner) = nk2
10307 {
10308 let inner = String::from_utf8_lossy(inner.as_bytes()).into_owned();
10309 return Some(if outer == "_G" {
10310 inner
10311 } else {
10312 format!("{outer}.{inner}")
10313 });
10314 }
10315 }
10316 }
10317 }
10318 None
10319 }
10320
10321 /// How the caller named the running native (PUC `lua_getinfo("n")` at
10322 /// level 0): `None` when it gives no name, as when the caller is C.
10323 pub(crate) fn running_call_name(&self) -> Option<(&'static str, String)> {
10324 let ts = self.thread_stack(None);
10325 if ts.levels.is_empty() {
10326 return None;
10327 }
10328 self.level_name(&ts, 0)
10329 }
10330
10331 /// Read an upvalue cell of a closure (debug.getupvalue).
10332 pub(crate) fn upvalue_value(&self, cl: Gc<LuaClosure>, idx: usize) -> Value {
10333 match cl.upvals()[idx].state() {
10334 UpvalState::Open { slot, thread } => self.read_slot(slot, thread),
10335 UpvalState::Closed(v) => v,
10336 }
10337 }
10338
10339 /// Write an upvalue cell of a closure (debug.setupvalue).
10340 pub(crate) fn upvalue_set_value(&mut self, cl: Gc<LuaClosure>, idx: usize, v: Value) {
10341 let uv = cl.upvals()[idx];
10342 match uv.state() {
10343 UpvalState::Open { slot, thread } => self.write_slot(slot, thread, v),
10344 UpvalState::Closed(_) => {
10345 // 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).
10346 unsafe { uv.as_mut() }.set_closed(v);
10347 self.heap
10348 .barrier_forward(uv.as_ptr() as *mut crate::runtime::heap::GcHeader, v);
10349 }
10350 }
10351 }
10352}
10353
10354// ────────────────────────────────────────────────────────────────────
10355// AOT trace dispatch install.
10356//
10357// The deploy-side resolver in `luna-runtime-helpers` walks the binary's
10358// trace-meta section after `vm.load`, resolves each entry's
10359// `(proto_hash, head_pc, fn_ptr)` triple against the loaded chunk's
10360// proto tree, and pushes a `CompiledTrace` onto the matching Proto's
10361// `traces` Vec via [`Vm::install_aot_trace`] below. The existing
10362// trace-dispatch loop (this file's `cl.proto.traces.borrow().iter()
10363// .find(|t| t.head_pc == pc && t.dispatchable)`) then fires the AOT
10364// mcode without further plumbing — same code path the runtime JIT
10365// uses.
10366//
10367// Why a separate impl block: keeps the AOT API surface (one fn) easy
10368// to locate when grep'ing for `install_aot_trace`, without dragging
10369// the 8500-line `impl Vm` block above.
10370// ────────────────────────────────────────────────────────────────────
10371
10372impl Vm {
10373 /// Install a precompiled
10374 /// `CompiledTrace` onto `proto.traces` so the interp dispatcher
10375 /// fires it at the trace's `head_pc`. This is the runtime install
10376 /// API the deploy-side `luna-runtime-helpers` resolver calls once
10377 /// per AOT-emitted trace meta entry, after looking up `proto` by
10378 /// stable hash (see `crate::runtime::function::Proto::stable_hash`).
10379 ///
10380 /// # What this does
10381 ///
10382 /// Pushes `trace` onto `proto.traces` via the existing `RefCell`.
10383 /// The trace's `entry` fn ptr must already point at runnable
10384 /// machine code (the AOT linker resolved the symbol at link time;
10385 /// the deploy resolver passes the address verbatim).
10386 ///
10387 /// # What this does NOT do
10388 ///
10389 /// - **No deduplication.** Calling twice with the same `head_pc`
10390 /// pushes two entries; the dispatcher's `find` will pick the
10391 /// first match. The deploy resolver is responsible for not
10392 /// double-installing.
10393 /// - **No invalidation of the runtime JIT cache.** If the runtime
10394 /// JIT later records + compiles a trace for the same
10395 /// `(proto, head_pc)`, both coexist on `proto.traces` and the
10396 /// dispatcher's `find` picks whichever appears first. AOT
10397 /// traces install before any runtime recording is possible
10398 /// (resolver runs before `vm.load` returns its first closure),
10399 /// so AOT traces win the race for the same site.
10400 /// - **No coverage gating.** AOT traces are trusted by
10401 /// construction — they were validated at compile time. Setting
10402 /// `dispatchable: false` on the input would silently disable
10403 /// dispatch; the caller controls that flag.
10404 ///
10405 /// # Safety / soundness
10406 ///
10407 /// `trace.entry` is an `unsafe extern "C" fn` (mmap'd or linked
10408 /// machine code). Soundness contract:
10409 ///
10410 /// - The fn pointer must remain valid for the `Vm`'s lifetime.
10411 /// In the AOT-binary deploy shape this is trivially satisfied —
10412 /// the fn lives in the binary's `.text`.
10413 /// - `trace.entry_tags` / `exit_tags` / `window_size` must match
10414 /// what the trace's IR actually compiled against; the dispatcher
10415 /// uses them to marshal `reg_state` in and out without further
10416 /// validation. A mismatch corrupts vm.stack.
10417 ///
10418 /// The AOT pipeline (`luna-aot`) is responsible for ensuring these
10419 /// invariants hold; this fn is a plain push — no validation that
10420 /// would slow the dispatcher's hot path either.
10421 pub fn install_aot_trace(
10422 &mut self,
10423 proto: crate::runtime::Gc<crate::runtime::function::Proto>,
10424 trace: crate::jit::trace::CompiledTrace,
10425 ) {
10426 let _ = self; // resolver passes &mut Vm for symmetry with future
10427 // pending-install + hash-walk variants; nothing on `self` to
10428 // mutate today because the install target lives on the Proto.
10429 proto.traces.borrow_mut().push(TArc::new(trace));
10430 }
10431
10432 /// Walk the proto tree
10433 /// reachable from `root` and return `(proto, stable_hash)` pairs
10434 /// for every Proto found. Used by the deploy-side resolver to
10435 /// match AOT-emitted `proto_hash` keys against the freshly
10436 /// `undump`'d chunk's protos.
10437 ///
10438 /// The walk is BFS over `Proto.protos`. Same-Proto deduplication
10439 /// is done via `Gc::as_ptr` identity — a Proto re-referenced from
10440 /// multiple nested closures (rare; the cache field would catch
10441 /// the closure-side dedup, not the Proto side) is reported once.
10442 ///
10443 /// # Why on `&Vm` and not a free fn
10444 ///
10445 /// Keeps the AOT install API discoverable on the Vm surface —
10446 /// `vm.collect_proto_hashes(root)` reads naturally next to
10447 /// `vm.install_aot_trace(proto, trace)`. Doesn't actually touch
10448 /// any Vm field, so `&self` (read-only) is enough.
10449 pub fn collect_proto_hashes(
10450 &self,
10451 root: crate::runtime::Gc<crate::runtime::function::Proto>,
10452 ) -> Vec<(
10453 crate::runtime::Gc<crate::runtime::function::Proto>,
10454 [u8; 16],
10455 )> {
10456 let _ = self;
10457 let mut out = Vec::new();
10458 let mut seen: std::collections::HashSet<*const crate::runtime::function::Proto> =
10459 std::collections::HashSet::new();
10460 let mut queue: std::collections::VecDeque<
10461 crate::runtime::Gc<crate::runtime::function::Proto>,
10462 > = std::collections::VecDeque::new();
10463 queue.push_back(root);
10464 while let Some(p) = queue.pop_front() {
10465 let key = p.as_ptr() as *const _;
10466 if !seen.insert(key) {
10467 continue;
10468 }
10469 out.push((p, p.stable_hash()));
10470 for &child in p.protos.iter() {
10471 queue.push_back(child);
10472 }
10473 }
10474 out
10475 }
10476}
10477
10478/// Recordings of one trace head that may fail to compile before the head
10479/// is no longer recorded (LuaJIT likewise blacklists a trace start after
10480/// repeated failures). A few tries, since a later recording can see
10481/// different register kinds.
10482const MAX_TRACE_COMPILE_FAILURES: u8 = 3;
10483
10484fn note_trace_compile_failure(proto: Gc<crate::runtime::function::Proto>, head_pc: u32) {
10485 let mut failures = proto.trace_compile_failures.borrow_mut();
10486 match failures.iter_mut().find(|(pc, _)| *pc == head_pc) {
10487 Some((_, n)) => *n = n.saturating_add(1),
10488 None => failures.push((head_pc, 1)),
10489 }
10490}
10491
10492fn trace_head_abandoned(proto: Gc<crate::runtime::function::Proto>, head_pc: u32) -> bool {
10493 proto
10494 .trace_compile_failures
10495 .borrow()
10496 .iter()
10497 .any(|&(pc, n)| pc == head_pc && n >= MAX_TRACE_COMPILE_FAILURES)
10498}