Skip to main content

lua_vm/
debug.rs

1//! Debug interface — ported from `ldebug.c`.
2//!
3//! Provides the Lua debug API: stack inspection, source info, variable lookup,
4//! hook management, and runtime error formatting.
5
6#[allow(unused_imports)]
7use crate::prelude::*;
8use crate::state::{
9    CallInfo, GcRef, LuaClosure, LuaClosureLua, LuaProto, LuaState, LuaTable, LuaValue, CIST_FIN,
10    CIST_HOOKED, CIST_HOOKYIELD, CIST_TAIL, CIST_TRAN,
11};
12use crate::vm::InstructionExt;
13use lua_types::error::LuaError;
14use lua_types::opcode::Instruction;
15use lua_types::{CallInfoIdx, LuaString, StackIdx};
16
17// ─── Constants from ldebug.h ──────────────────────────────────────────────────
18
19const ABS_LINE_INFO: i8 = -0x80_i8;
20
21const MAX_IWTH_ABS: i32 = 128;
22
23/// Matches `LUA_IDSIZE` in upstream `luaconf.h`.
24const LUA_IDSIZE: usize = 60;
25
26const LUA_MASKLINE: u8 = 1 << 2;
27const LUA_MASKCOUNT: u8 = 1 << 3;
28
29const LUA_HOOKLINE: i32 = 2;
30const LUA_HOOKCOUNT: i32 = 3;
31
32const LUA_ENV: &[u8] = b"_ENV";
33
34// ─── Local error constructors (not yet in lua-types) ─────────────────────────
35
36/// Build a `LuaError::Runtime` from a raw byte-string message.
37///
38fn runtime_bytes(msg: Vec<u8>) -> LuaError {
39    LuaError::Runtime(lua_types::LuaValue::Str(lua_types::GcRef::new(
40        lua_types::LuaString::from_bytes(msg),
41    )))
42}
43
44/// Prepend `[source]:line:` to `msg` when the current call frame is a Lua
45/// function. Mirrors what `luaG_addinfo` does for messages routed through
46/// `luaG_runerror`; the typed error constructors below build their own
47/// message and skip that path, so we add the same prefix here.
48/// Public wrapper for `prefixed_runtime` so other VM modules can re-prefix
49/// bare runtime errors raised from typed-arith helpers with the current call
50/// frame's `source:line:`.
51pub(crate) fn prefixed_runtime_pub(state: &LuaState, msg: Vec<u8>) -> LuaError {
52    prefixed_runtime(state, msg)
53}
54
55fn prefixed_runtime(state: &LuaState, msg: Vec<u8>) -> LuaError {
56    let ci_idx = state.current_ci_idx();
57    let ci = state.get_ci(ci_idx).clone();
58    if !ci.is_lua() {
59        return runtime_bytes(msg);
60    }
61    let proto = ci_lua_proto(&ci, state);
62    let src = proto.source_string();
63    let line = get_current_line(&ci, state);
64    let unknown_line_as_question =
65        src.is_none() && state.global().lua_version == lua_types::LuaVersion::V55;
66    let prefixed = add_info(
67        None,
68        &msg,
69        src.map(|s| &**s),
70        line,
71        unknown_line_as_question,
72    );
73    runtime_bytes(prefixed)
74}
75
76pub fn c_api_runtime(state: &LuaState, msg: Vec<u8>) -> LuaError {
77    let ci_idx = state.current_ci_idx();
78    if let Some(parent_idx) = state.prev_ci(ci_idx) {
79        let parent_ci = state.get_ci(parent_idx).clone();
80        if parent_ci.is_lua() {
81            let proto = ci_lua_proto(&parent_ci, state);
82            let src = proto.source_string();
83            let line = get_current_line(&parent_ci, state);
84            let unknown_line_as_question =
85                src.is_none() && state.global().lua_version == lua_types::LuaVersion::V55;
86            let prefixed = add_info(
87                None,
88                &msg,
89                src.map(|s| &**s),
90                line,
91                unknown_line_as_question,
92            );
93            return runtime_bytes(prefixed);
94        }
95    }
96    runtime_bytes(msg)
97}
98
99/// Walk a table's entries looking for `target` function (by identity).
100/// At `depth == 1`, also recurses one level into table-valued entries so that
101/// e.g. `_G.table.sort` can be found as `"table.sort"`.
102/// Returns the dotted path on success, `None` otherwise.
103/// Mirrors `ldblib.c:findfield` from reference C-Lua 5.4.
104///
105/// Not called from `arg_error_impl` (that path was removed to prevent stack
106/// overflow via re-entrant error generation). Reserved for a future
107/// `debug.findfield` Lua binding.
108#[allow(dead_code)]
109fn find_func_in_table(
110    table: &LuaTable,
111    target: &LuaValue,
112    prefix: &[u8],
113    depth: u8,
114) -> Option<Vec<u8>> {
115    let mut key = LuaValue::Nil;
116    loop {
117        let (k, v) = match table.next_pair(&key) {
118            Some(pair) => pair,
119            None => break,
120        };
121        if !matches!(v, LuaValue::Nil) {
122            let key_bytes: Option<Vec<u8>> = match &k {
123                LuaValue::Str(s) => Some(s.as_bytes().to_vec()),
124                _ => None,
125            };
126            if let Some(kb) = key_bytes {
127                if &v == target {
128                    if prefix.is_empty() {
129                        return Some(kb);
130                    }
131                    let mut result = prefix.to_vec();
132                    result.push(b'.');
133                    result.extend_from_slice(&kb);
134                    return Some(result);
135                }
136                if depth > 0 {
137                    if let LuaValue::Table(sub) = &v {
138                        let new_prefix = if prefix.is_empty() {
139                            kb.clone()
140                        } else {
141                            let mut p = prefix.to_vec();
142                            p.push(b'.');
143                            p.extend_from_slice(&kb);
144                            p
145                        };
146                        if let Some(name) =
147                            find_func_in_table(&**sub, target, &new_prefix, depth - 1)
148                        {
149                            return Some(name);
150                        }
151                    }
152                }
153            }
154        }
155        key = k;
156    }
157    None
158}
159
160/// When `get_info` cannot resolve a function name (e.g. the function was called
161/// as a value from C code), walk `_G` to find its dotted path by identity.
162/// Returns `None` if not found; caller falls back to `"?"`.
163///
164/// Not called from `arg_error_impl` (that path was removed to prevent stack
165/// overflow via re-entrant error generation). Reserved for a future
166/// `debug.findfield` Lua binding.
167#[allow(dead_code)]
168fn find_func_name_in_globals(state: &LuaState, func_val: &LuaValue) -> Option<Vec<u8>> {
169    let globals = state.global().globals.clone();
170    if let LuaValue::Table(globals_table) = globals {
171        find_func_in_table(&*globals_table, func_val, b"", 1)
172    } else {
173        None
174    }
175}
176
177/// Mirrors C `pushglobalfuncname` (lauxlib.c): search `package.loaded` (the
178/// `_LOADED` registry entry) for `func_val` by identity.  Only descends one
179/// level into each loaded module, so `table.sort` is found as `"table.sort"`.
180///
181/// Uses only raw table lookups (`get_str_bytes`, `next_pair`) — no VM calls,
182/// no metamethods, no GC.  Safe to call from error-formatting paths.
183fn find_func_name_in_loaded(state: &LuaState, func_val: &LuaValue) -> Option<Vec<u8>> {
184    let registry = state.global().l_registry.clone();
185    let loaded = match registry {
186        LuaValue::Table(ref reg_table) => reg_table.get_str_bytes(b"_LOADED"),
187        _ => return None,
188    };
189    let loaded_table = match loaded {
190        LuaValue::Table(t) => t,
191        _ => return None,
192    };
193    find_func_in_table(&*loaded_table, func_val, b"", 1)
194}
195
196/// Per-version `pushglobalfuncname` (C `lauxlib.c`): resolve the C function at
197/// the current call frame to a name by searching `package.loaded` by identity.
198///
199/// The version seam (the F1 funcname resolver):
200/// - **5.1** recorded no names for C functions — PUC-Rio 5.1 has no
201///   `pushglobalfuncname`, so `luaL_argerror` falls straight through to `'?'`.
202///   We return `None` here so the caller emits `'?'`.
203/// - **5.2** searches the *global table* (`lua_pushglobaltable`) and does **not**
204///   strip the `_G.` prefix (PUC-Rio 5.2's `pushglobalfuncname` has no strip).
205///   A bare global resolved through the `_G` module therefore renders
206///   `'_G.<name>'`; a module member (`coroutine.resume`) carries its own dotted
207///   name and is unaffected. We keep the `_G.` prefix for V52.
208/// - **5.3+** searches `package.loaded` and explicitly strips a leading `_G.`
209///   (C: `strncmp(name, LUA_GNAME ".", 3)`), reporting the bare `<name>`.
210///
211/// PUC-Rio 5.2's exact `_G.`-vs-bare choice is *also*
212/// hash-iteration-order-dependent and non-deterministic across runs of the
213/// reference binary itself: the global table contains `_G._G` (a self-reference),
214/// so `findfield` reaches e.g. `next` either directly under `_G` (→ `'next'`) or
215/// one level deeper through the self-reference (→ `'_G.next'`), and which it hits
216/// first depends on hash-iteration order. The same global can print `'next'` on
217/// one run and `'_G.next'` on the next. We pin the deterministic `'_G.<name>'`
218/// form for V52 globals (always reachable via the `_G` module), which is one of
219/// the two valid reference outputs; the `error_wording_kit` doc-comment records
220/// this for the entries it pins.
221fn arg_error_global_name(
222    state: &LuaState,
223    ar: &LuaDebug,
224    version: lua_types::LuaVersion,
225) -> Option<Vec<u8>> {
226    if version == lua_types::LuaVersion::V51 {
227        return None;
228    }
229    let keeps_global_prefix = version == lua_types::LuaVersion::V52;
230    let ci_idx = ar.i_ci?;
231    let func_slot = state.get_ci(ci_idx).func;
232    let func_val = state.get_at(func_slot).clone();
233    let found = find_func_name_in_loaded(state, &func_val)?;
234    if !keeps_global_prefix && found.starts_with(b"_G.") {
235        Some(found[3..].to_vec())
236    } else {
237        Some(found)
238    }
239}
240
241/// Equivalent of C `luaL_argerror`: build an arg-type error with function name
242/// (from debug info) and caller source location. Handles method calls by
243/// producing "calling 'f' on bad self ..." when arg==1 and namewhat=="method".
244pub fn arg_error_impl(state: &mut LuaState, mut arg: i32, extramsg: &[u8]) -> LuaError {
245    let mut ar = LuaDebug::default();
246    if !get_stack(state, 0, &mut ar) {
247        let msg = format!(
248            "bad argument #{} ({})",
249            arg,
250            String::from_utf8_lossy(extramsg)
251        );
252        return c_api_runtime(state, msg.into_bytes());
253    }
254    get_info(state, b"n", &mut ar);
255    if ar.namewhat.as_deref() == Some(b"method") {
256        arg -= 1;
257        if arg == 0 {
258            let name = ar.name.clone().unwrap_or_else(|| b"?".to_vec());
259            let msg = format!(
260                "calling '{}' on bad self ({})",
261                String::from_utf8_lossy(&name),
262                String::from_utf8_lossy(extramsg)
263            );
264            return c_api_runtime(state, msg.into_bytes());
265        }
266    }
267    let version = state.global().lua_version;
268    let fname = ar
269        .name
270        .clone()
271        .or_else(|| arg_error_global_name(state, &ar, version))
272        .unwrap_or_else(|| b"?".to_vec());
273    let msg = format!(
274        "bad argument #{} to '{}' ({})",
275        arg,
276        String::from_utf8_lossy(&fname),
277        String::from_utf8_lossy(extramsg)
278    );
279    c_api_runtime(state, msg.into_bytes())
280}
281
282// ─── Debug info structures ────────────────────────────────────────────────────
283
284/// Debug introspection record.
285///
286/// Holds only the fields that `ldebug.c` writes/reads. `name` and `namewhat`
287/// are optional byte strings because in C they can be NULL. `source` is owned
288/// here, built from `Proto.source` (a `GcRef`). `short_src` matches the C
289/// layout as a fixed array.
290pub struct LuaDebug {
291    pub event: i32,
292    pub name: Option<Vec<u8>>,
293    pub namewhat: Option<&'static [u8]>,
294    pub what: Option<&'static [u8]>,
295    pub source: Option<Vec<u8>>,
296    pub srclen: usize,
297    pub currentline: i32,
298    pub linedefined: i32,
299    pub lastlinedefined: i32,
300    pub nups: u8,
301    pub nparams: u8,
302    pub isvararg: bool,
303    pub istailcall: bool,
304    pub extraargs: u8,
305    pub ftransfer: u16,
306    pub ntransfer: u16,
307    pub short_src: [u8; LUA_IDSIZE],
308    /// C stores a raw pointer here; this stores an index into
309    /// `LuaState.call_stack` instead.
310    pub i_ci: Option<CallInfoIdx>,
311}
312
313impl Default for LuaDebug {
314    fn default() -> Self {
315        LuaDebug {
316            event: 0,
317            name: None,
318            namewhat: None,
319            what: None,
320            source: None,
321            srclen: 0,
322            currentline: -1,
323            linedefined: -1,
324            lastlinedefined: -1,
325            nups: 0,
326            nparams: 0,
327            isvararg: false,
328            istailcall: false,
329            extraargs: 0,
330            ftransfer: 0,
331            ntransfer: 0,
332            short_src: [0u8; LUA_IDSIZE],
333            i_ci: None,
334        }
335    }
336}
337
338// ─── File-local helper: is this a Lua (non-C) closure? ───────────────────────
339
340#[inline]
341fn is_lua_closure(cl: Option<&LuaClosure>) -> bool {
342    matches!(cl, Some(LuaClosure::Lua(_)))
343}
344
345// ─── Current-PC helpers ───────────────────────────────────────────────────────
346
347/// Returns the program counter (0-based instruction index) for the current
348/// instruction in call frame `ci`.
349///
350/// C's `savedpc` is a pointer to the *next* instruction; `pcRel` subtracts the
351/// code base and then subtracts 1 more to get the *current* instruction.
352/// Here `saved_pc()` stores the 0-based index of the next instruction, so the
353/// current instruction index is `saved_pc() - 1`.
354fn current_pc(ci: &CallInfo) -> i32 {
355    debug_assert!(ci.is_lua());
356    ci.saved_pc().saturating_sub(1) as i32
357}
358
359// ─── Line-info lookup ─────────────────────────────────────────────────────────
360
361/// Finds the "base line" entry in `f.abslineinfo` for instruction `pc`.
362///
363/// Sets `*basepc` to the pc of the base entry (or -1 if starting from the
364/// function's first line), and returns the line number at that base.
365///
366fn get_baseline(f: &LuaProto, pc: i32, basepc: &mut i32) -> i32 {
367    if f.abslineinfo.is_empty() || pc < f.abslineinfo[0].pc {
368        *basepc = -1;
369        return f.linedefined;
370    }
371    let mut i = (pc as u32 / MAX_IWTH_ABS as u32).saturating_sub(1) as usize;
372    debug_assert!(
373        i < f.abslineinfo.len() && f.abslineinfo[i].pc <= pc,
374        "getbaseline: estimate is not a lower bound"
375    );
376    while i + 1 < f.abslineinfo.len() && pc >= f.abslineinfo[i + 1].pc {
377        i += 1;
378    }
379    *basepc = f.abslineinfo[i].pc;
380    f.abslineinfo[i].line
381}
382
383/// Returns the source line number corresponding to instruction `pc` in proto `f`.
384/// Returns -1 if the proto has no debug line information.
385///
386pub(crate) fn get_func_line(f: &LuaProto, pc: i32) -> i32 {
387    if f.lineinfo.is_empty() {
388        return -1;
389    }
390    let mut basepc: i32 = 0;
391    let mut baseline = get_baseline(f, pc, &mut basepc);
392    // C uses post-increment `basepc++` in the loop condition and the body then
393    // uses the already-incremented value; this loop pre-increments instead to
394    // get the same sequence of values.
395    while basepc < pc {
396        basepc += 1;
397        debug_assert!(
398            f.lineinfo[basepc as usize] != ABS_LINE_INFO,
399            "get_func_line: hit ABSLINEINFO in incremental walk"
400        );
401        baseline += f.lineinfo[basepc as usize] as i32;
402    }
403    baseline
404}
405
406/// Returns the source line for the current instruction in call frame `ci`.
407///
408fn get_current_line(ci: &CallInfo, state: &LuaState) -> i32 {
409    let proto = ci_lua_proto(ci, state);
410    get_func_line(&proto, current_pc(ci))
411}
412
413// ─── Hook support ─────────────────────────────────────────────────────────────
414
415/// Sets the `trap` flag on every active Lua call frame so that the VM checks
416/// debug hooks before each instruction.
417///
418/// C walks an intrusive doubly-linked list of call frames; here
419/// `LuaState.call_stack` is a `Vec<CallInfo>`, so this iterates the slice
420/// instead. Marks every Lua call-frame on `state` as trapped so the dispatch
421/// loop re-reads the hook mask on its next iteration. Exposed for the
422/// sandbox, which arms the count-hook mask directly rather than through
423/// [`set_hook`].
424pub(crate) fn arm_traps(state: &mut LuaState) {
425    set_traps(state);
426}
427
428fn set_traps(state: &mut LuaState) {
429    for ci in state.call_stack_mut().iter_mut() {
430        if ci.is_lua() {
431            ci.set_trap(true);
432        }
433    }
434}
435
436/// Installs a debug hook on thread `state`.
437///
438pub fn set_hook(
439    state: &mut LuaState,
440    func: Option<Box<dyn FnMut(&mut LuaState, &LuaDebug)>>,
441    mask: i32,
442    count: i32,
443) {
444    let (func, mask) = if func.is_none() || mask == 0 {
445        (None, 0i32)
446    } else {
447        (func, mask)
448    };
449    state.set_hook(func);
450    state.set_base_hook_count(count);
451    state.reset_hook_count();
452    state.set_hook_mask(mask as u8);
453    if mask != 0 {
454        set_traps(state);
455    }
456}
457
458/// Returns whether a debug hook function is currently installed.
459///
460/// C's `lua_gethook` returns the `lua_Hook` function pointer itself; a
461/// `Box<dyn FnMut>` cannot be returned by reference the same way, so this
462/// reports only presence.
463pub fn get_hook_installed(state: &LuaState) -> bool {
464    state.hook().is_some()
465}
466
467/// Returns the current hook event mask.
468///
469pub fn get_hook_mask(state: &LuaState) -> i32 {
470    state.hook_mask() as i32
471}
472
473/// Returns the current hook call count.
474///
475pub fn get_hook_count(state: &LuaState) -> i32 {
476    state.base_hook_count()
477}
478
479// ─── Stack introspection ──────────────────────────────────────────────────────
480
481/// Fills `ar` with information about the call frame at depth `level`.
482/// Level 0 is the current running function, level 1 is the caller, etc.
483/// Returns `true` on success, `false` if the level is out of range.
484///
485pub fn get_stack(state: &LuaState, level: i32, ar: &mut LuaDebug) -> bool {
486    if level < 0 {
487        return false;
488    }
489    if state.global().lua_version == lua_types::LuaVersion::V51 {
490        return get_stack_51(state, level, ar);
491    }
492    let mut remaining = level;
493    let mut ci_idx = state.current_ci_idx();
494    loop {
495        if remaining == 0 {
496            break;
497        }
498        match state.prev_ci(ci_idx) {
499            Some(prev) => {
500                ci_idx = prev;
501                remaining -= 1;
502            }
503            None => {
504                return false;
505            }
506        }
507    }
508    if !state.is_base_ci(ci_idx) {
509        ar.i_ci = Some(ci_idx);
510        true
511    } else {
512        false
513    }
514}
515
516/// Lua 5.1 `lua_getstack`: the level walk that accounts for "lost" tail calls.
517///
518/// 5.1 reuses a frame on a tail call (like every later version) but exposes the
519/// lost frames to the debug API as synthetic `(tail call)` levels. Each Lua
520/// frame contributes its own level plus one extra per accumulated tail call
521/// (`ci.tailcalls`). When `level` lands inside that synthetic span the C code
522/// sets `ar->i_ci = 0` (the base-CI index) as a sentinel; we mirror that with
523/// `Some(CallInfoIdx(0))`, which `get_info` reads as "emit a tail frame". The
524/// base CI is never a real `getinfo` target, so that index is free to overload
525/// exactly as C overloads it.
526fn get_stack_51(state: &LuaState, level: i32, ar: &mut LuaDebug) -> bool {
527    let mut remaining = level;
528    let mut ci_idx = state.current_ci_idx();
529    loop {
530        if remaining <= 0 || state.is_base_ci(ci_idx) {
531            break;
532        }
533        remaining -= 1;
534        let ci = state.get_ci(ci_idx);
535        if ci.is_lua() {
536            remaining -= ci.tailcalls as i32;
537        }
538        match state.prev_ci(ci_idx) {
539            Some(prev) => ci_idx = prev,
540            None => break,
541        }
542    }
543    if remaining == 0 && !state.is_base_ci(ci_idx) {
544        ar.i_ci = Some(ci_idx);
545        true
546    } else if remaining < 0 {
547        ar.i_ci = Some(CallInfoIdx(0));
548        true
549    } else {
550        false
551    }
552}
553
554// ─── Upvalue and local variable name lookup ───────────────────────────────────
555
556/// Counts the user-visible upvalues of a Lua function under Lua 5.1 semantics.
557///
558/// Lua 5.1 has no `_ENV`: globals compile to `GETGLOBAL`/`SETGLOBAL`, so a
559/// function that only touches globals reports `nups == 0`. Our core uses the
560/// Option-B fenv model and carries a synthetic `_ENV` upvalue regardless. Since
561/// 5.1 has no `_ENV` syntax, any upvalue named `_ENV` on a 5.1 instance is that
562/// synthetic cell, so excluding it reproduces the reference count
563/// (`debug.getinfo(g).nups` in db.lua:184).
564fn visible_upvalue_count_51(p: &LuaProto) -> usize {
565    p.upvalues
566        .iter()
567        .filter(|uv| uv.name.as_ref().map_or(true, |s| s.as_bytes() != LUA_ENV))
568        .count()
569}
570
571/// Returns the name of upvalue `uv` in proto `p` (as a byte slice), or `b"?"`.
572///
573fn upval_name(p: &LuaProto, uv: usize) -> &[u8] {
574    debug_assert!(uv < p.upvalues.len(), "upval_name: index out of range");
575    p.upvalues[uv]
576        .name
577        .as_ref()
578        .map_or(b"?" as &[u8], |s| s.as_bytes())
579}
580
581/// Generic name reported by `debug.getlocal` for an unnamed-but-valid stack
582/// slot (a "temporary").
583///
584/// The wording is version-gated. Lua 5.1–5.3 report a single `(*temporary)`
585/// for every valid slot, with no distinction between Lua and C frames
586/// (`getfuncname`/`luaG_findlocal` in their `ldebug.c`). Lua 5.4 split this
587/// into `(temporary)` for a Lua frame and `(C temporary)` for a C frame
588/// (`isLua(ci) ? "(temporary)" : "(C temporary)"`), and 5.5 kept that split.
589fn temporary_local_name(state: &LuaState, ci_is_lua: bool) -> &'static [u8] {
590    match state.global().lua_version {
591        lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52 | lua_types::LuaVersion::V53 => {
592            b"(*temporary)"
593        }
594        _ => {
595            if ci_is_lua {
596                b"(temporary)"
597            } else {
598                b"(C temporary)"
599            }
600        }
601    }
602}
603
604/// Finds the stack slot for vararg value number `n` (n is negative) in `ci`.
605/// Returns `Some(pos)` and the generic vararg name if found, else `None`.
606///
607/// The generic name is version-gated: Lua 5.2 and 5.3 report `(*vararg)`
608/// (`findvararg` in their `ldebug.c`), while 5.4 and 5.5 dropped the asterisk
609/// to `(vararg)`. 5.1 has no `findvararg` (it exposes varargs through the `arg`
610/// table, not `debug.getlocal`), so it never reaches this path.
611///
612/// C sets `*pos` as an out-parameter; this returns an `Option` of the stack
613/// index alongside the name instead.
614fn find_vararg(state: &LuaState, ci: &CallInfo, n: i32) -> Option<(StackIdx, &'static [u8])> {
615    let proto = ci_lua_proto(ci, state);
616    if proto.is_vararg {
617        let nextra = ci.nextra_args();
618        if n >= -(nextra as i32) {
619            // ci.func is the function slot; varargs are at func - nextra - 1 .. func - 1.
620            let pos = ci.func - (nextra + n + 1);
621            let name: &'static [u8] = match state.global().lua_version {
622                lua_types::LuaVersion::V52 | lua_types::LuaVersion::V53 => b"(*vararg)",
623                _ => b"(vararg)",
624            };
625            return Some((pos, name));
626        }
627    }
628    None
629}
630
631/// Finds the name and stack position for local variable `n` in call frame `ci`.
632///
633/// - If `n > 0`, looks up as a numbered local (1-based).
634/// - If `n < 0`, looks up as a vararg slot.
635/// - Returns `None` if no such variable exists.
636/// - If `pos` is `Some`, sets it to the variable's stack index.
637///
638/// Returns an owned `Vec<u8>` rather than `&[u8]`: the Lua-function case calls
639/// `get_local_name`, which returns a slice borrowed from a `GcRef<LuaProto>`
640/// that drops at function end, so there is no caller lifetime the slice could
641/// be tied to. Cloning the name is cheap (a handful of bytes).
642pub(crate) fn find_local(
643    state: &LuaState,
644    ci_idx: CallInfoIdx,
645    n: i32,
646    pos: Option<&mut StackIdx>,
647) -> Option<Vec<u8>> {
648    let ci = state.get_ci(ci_idx);
649    let base = ci.func + 1;
650    let mut name: Option<Vec<u8>> = None;
651
652    if ci.is_lua() {
653        if n < 0 {
654            if let Some((vpos, vname)) = find_vararg(state, ci, n) {
655                if let Some(out_pos) = pos {
656                    *out_pos = vpos;
657                }
658                return Some(vname.to_vec());
659            }
660            return None;
661        } else {
662            let proto = ci_lua_proto(ci, state);
663            let pc = current_pc(ci);
664            name = crate::func::get_local_name(&proto, n, pc).map(|s| s.to_vec());
665        }
666    }
667
668    if name.is_none() {
669        let limit: u32 = if ci_idx == state.current_ci_idx() {
670            state.top_idx().0
671        } else {
672            ci.next
673                .map(|next| state.get_ci(next).func.0)
674                .unwrap_or_else(|| state.top_idx().0)
675        };
676        if n > 0 && limit.saturating_sub(base.0) >= n as u32 {
677            name = Some(temporary_local_name(state, ci.is_lua()).to_vec());
678        } else {
679            return None;
680        }
681    }
682
683    if let Some(out_pos) = pos {
684        *out_pos = base + (n - 1);
685    }
686    name
687}
688
689/// Gets the name and value of local variable `n` in call frame `ar->i_ci`
690/// (or in the function at the top of the stack if `ar` is NULL).
691/// Pushes the value on the stack and returns its name, or returns `None`.
692///
693pub fn get_local(state: &mut LuaState, ar: Option<&LuaDebug>, n: i32) -> Option<Vec<u8>> {
694    if ar.is_none() {
695        let top_val = state.peek_top();
696        if !matches!(top_val, LuaValue::Function(LuaClosure::Lua(_))) {
697            return None;
698        }
699        // Convert to an owned Vec<u8> inside the block so `cl` (and the
700        // borrow through it) drop before we return.
701        let name_owned: Option<Vec<u8>> = {
702            let cl = match top_val {
703                LuaValue::Function(LuaClosure::Lua(ref cl)) => cl.clone(),
704                _ => unreachable!(),
705            };
706            get_local_name_from_closure(&cl, n, 0).map(|s| s.to_vec())
707        };
708        return name_owned;
709    }
710
711    let ar = ar.unwrap();
712    let ci_idx = ar.i_ci?;
713    let mut pos = StackIdx(0);
714    // Clone name to an owned Vec<u8> so the immutable borrow of `state` ends
715    // before the mutable push below.
716    let name_owned: Option<Vec<u8>> = find_local(state, ci_idx, n, Some(&mut pos));
717
718    if name_owned.is_some() {
719        let val = state.get_at(pos).clone();
720        state.push(val);
721    }
722    name_owned
723}
724
725/// Sets local variable `n` in call frame `ar->i_ci` to the value on top of the
726/// stack. Pops the value and returns the variable name, or returns `None`.
727///
728pub fn set_local(state: &mut LuaState, ar: &LuaDebug, n: i32) -> Option<Vec<u8>> {
729    let ci_idx = ar.i_ci?;
730    let mut pos = StackIdx(0);
731    let name_owned: Option<Vec<u8>> = find_local(state, ci_idx, n, Some(&mut pos));
732    if name_owned.is_some() {
733        let val = state.get_at(state.top_idx() - 1).clone();
734        state.set_at(pos, val);
735        state.pop_n(1);
736    }
737    name_owned
738}
739
740// ─── Function info helpers ────────────────────────────────────────────────────
741
742/// Fills the source/line fields of `ar` from closure `cl`.
743///
744fn func_info(ar: &mut LuaDebug, cl: Option<&LuaClosure>) {
745    if !is_lua_closure(cl) {
746        ar.source = Some(b"=[C]".to_vec());
747        ar.srclen = b"=[C]".len();
748        ar.linedefined = -1;
749        ar.lastlinedefined = -1;
750        ar.what = Some(b"C");
751    } else {
752        let lua_cl = match cl {
753            Some(LuaClosure::Lua(cl)) => cl,
754            _ => unreachable!(),
755        };
756        let proto: &LuaProto = &lua_cl.proto;
757        // renders as "?". Stripped binary chunks commonly have no source.
758        if let Some(src) = proto.source_string() {
759            ar.source = Some(src.as_bytes().to_vec());
760            ar.srclen = src.as_bytes().len();
761        } else {
762            ar.source = Some(b"=?".to_vec());
763            ar.srclen = b"=?".len();
764        }
765        ar.linedefined = proto.linedefined;
766        ar.lastlinedefined = proto.lastlinedefined;
767        ar.what = Some(if ar.linedefined == 0 { b"main" } else { b"Lua" });
768    }
769    chunk_id(
770        &mut ar.short_src,
771        ar.source.as_deref().unwrap_or(b"?"),
772        ar.srclen,
773    );
774}
775
776/// Returns the line number after advancing by one instruction from `currentline`.
777/// Handles the ABSLINEINFO sentinel by falling through to `get_func_line`.
778///
779fn next_line(p: &LuaProto, currentline: i32, pc: usize) -> i32 {
780    //    else return luaG_getfuncline(p, pc);
781    if p.lineinfo.get(pc).copied() != Some(ABS_LINE_INFO) {
782        currentline + p.lineinfo[pc] as i32
783    } else {
784        get_func_line(p, pc as i32)
785    }
786}
787
788/// Collects all source lines that are covered by instructions in closure `f`
789/// into a new table and pushes it on the stack (or pushes `nil` for C functions).
790///
791fn collect_valid_lines(state: &mut LuaState, cl: Option<&LuaClosure>) -> Result<(), LuaError> {
792    if !is_lua_closure(cl) {
793        state.push(LuaValue::Nil);
794        return Ok(());
795    }
796    let lua_cl = match cl {
797        Some(LuaClosure::Lua(cl)) => cl.clone(),
798        _ => unreachable!(),
799    };
800    let proto: GcRef<LuaProto> = lua_cl.proto.clone();
801    let p: &LuaProto = &proto;
802
803    let mut currentline = p.linedefined;
804
805    let t = state.new_table();
806    state.push(LuaValue::Table(t.clone()));
807
808    if !p.lineinfo.is_empty() {
809        let v = LuaValue::Bool(true);
810
811        let start_i = if !p.is_vararg {
812            0usize
813        } else {
814            debug_assert!(
815                p.code.first().map(|i| i.is_vararg_prep()).unwrap_or(false),
816                "collect_valid_lines: first instruction of vararg should be OP_VARARGPREP"
817            );
818            currentline = next_line(p, currentline, 0);
819            1usize
820        };
821
822        // C iterates up to sizelineinfo, which is the same as lineinfo.len() here.
823        for i in start_i..p.lineinfo.len() {
824            currentline = next_line(p, currentline, i);
825            t.raw_set_int(state, currentline as i64, v.clone())?;
826        }
827    }
828    Ok(())
829}
830
831// ─── Function naming (symbolic execution) ────────────────────────────────────
832
833/// Resolves the `name`/`namewhat` pair for the inspected frame `ci`, mirroring
834/// each reference version's `getfuncname` (and pre-5.3 `case 'n'`) verbatim.
835///
836/// The finalizer-naming seam diverges sharply across versions and is
837/// load-bearing for `db.lua`. C 5.3 reports `CIST_FIN` on the frame that
838/// *carries* the flag (the C frame that invoked the finalizer), so the
839/// metamethod surfaces one level *above* the finalizer itself. C 5.4/5.5 moved
840/// the check to `funcnamefromcall(ci->previous)`, so the finalizer's own frame
841/// is named `__gc`. C 5.1/5.2 have no `CIST_FIN` naming case at all, so the
842/// finalizer-invoking frame keeps whatever name its own caller implies.
843///
844/// This runs only on `getinfo`'s cold `'n'` path, so the per-version branch is
845/// outside the hot dispatch loop.
846fn get_func_name<'a>(
847    state: &'a LuaState,
848    ci: Option<&CallInfo>,
849    name: &mut Option<Vec<u8>>,
850) -> Option<&'static [u8]> {
851    let ci = ci?;
852    match state.global().lua_version {
853        lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52 => {
854            if ci.callstatus & CIST_TAIL != 0 {
855                return None;
856            }
857            funcname_from_caller_code(state, ci, false, name)
858        }
859        lua_types::LuaVersion::V53 => {
860            if ci.callstatus & CIST_FIN != 0 {
861                *name = Some(b"__gc".to_vec());
862                return Some(b"metamethod");
863            }
864            if ci.callstatus & CIST_TAIL != 0 {
865                return None;
866            }
867            funcname_from_caller_code(state, ci, true, name)
868        }
869        lua_types::LuaVersion::V54 | lua_types::LuaVersion::V55 | _ => {
870            if ci.callstatus & CIST_TAIL != 0 {
871                return None;
872            }
873            let prev_ci = state.get_ci(ci.previous?).clone();
874            funcname_from_call(state, &prev_ci, name)
875        }
876    }
877}
878
879/// Resolves `ci`'s name from its caller's calling instruction, the
880/// pre-5.4 `getfuncname` tail: only when the caller (`ci->previous`) is a Lua
881/// frame is there a calling opcode to read; a C caller yields no name.
882///
883/// `check_hooked` mirrors that C 5.3 moved the `CIST_HOOKED` test *inside*
884/// `funcnamefromcode` (so it reads the caller's flag, after the `isLua`
885/// guard), whereas C 5.1/5.2 have no hook-naming case at all.
886fn funcname_from_caller_code<'a>(
887    state: &'a LuaState,
888    ci: &CallInfo,
889    check_hooked: bool,
890    name: &mut Option<Vec<u8>>,
891) -> Option<&'static [u8]> {
892    let prev_ci = state.get_ci(ci.previous?).clone();
893    if !prev_ci.is_lua() {
894        return None;
895    }
896    if check_hooked && prev_ci.callstatus & CIST_HOOKED != 0 {
897        *name = Some(b"?".to_vec());
898        return Some(b"hook");
899    }
900    let proto = ci_lua_proto(&prev_ci, state);
901    funcname_from_code(state, &proto, current_pc(&prev_ci), name)
902}
903
904/// Fills `ar` with the requested debug information about closure `f` / frame `ci`.
905///
906fn aux_get_info(
907    state: &LuaState,
908    what: &[u8],
909    ar: &mut LuaDebug,
910    cl: Option<&LuaClosure>,
911    ci: Option<&CallInfo>,
912) -> bool {
913    let mut status = true;
914    for &ch in what {
915        match ch {
916            b'S' => {
917                func_info(ar, cl);
918            }
919            b'l' => {
920                ar.currentline = match ci {
921                    Some(ci) if ci.is_lua() => get_current_line(ci, state),
922                    _ => -1,
923                };
924            }
925            b'u' => {
926                ar.nups = cl.map_or(0, |c| c.nupvalues() as u8);
927                match cl {
928                    Some(LuaClosure::Lua(lua_cl)) => {
929                        ar.isvararg = lua_cl.proto.is_vararg;
930                        ar.nparams = lua_cl.proto.numparams;
931                        if state.global().lua_version == lua_types::LuaVersion::V51 {
932                            ar.nups = visible_upvalue_count_51(&lua_cl.proto) as u8;
933                        }
934                    }
935                    _ => {
936                        ar.isvararg = true;
937                        ar.nparams = 0;
938                    }
939                }
940            }
941            b't' => {
942                if state.global().lua_version == lua_types::LuaVersion::V51 {
943                    status = false;
944                } else if let Some(ci) = ci {
945                    ar.istailcall = ci.callstatus & CIST_TAIL != 0;
946                    ar.extraargs = ci.call_metamethods;
947                } else {
948                    ar.istailcall = false;
949                    ar.extraargs = 0;
950                }
951            }
952            b'n' => {
953                let mut name: Option<Vec<u8>> = None;
954                ar.namewhat = get_func_name(state, ci, &mut name);
955                if ar.namewhat.is_none() {
956                    ar.namewhat = Some(b"");
957                    ar.name = None;
958                } else {
959                    ar.name = name;
960                }
961            }
962            b'r' => {
963                if matches!(
964                    state.global().lua_version,
965                    lua_types::LuaVersion::V51
966                        | lua_types::LuaVersion::V52
967                        | lua_types::LuaVersion::V53
968                ) {
969                    status = false;
970                } else {
971                    match ci {
972                        Some(ci) if ci.callstatus & CIST_TRAN != 0 => {
973                            ar.ftransfer = ci.transfer_ftransfer();
974                            ar.ntransfer = ci.transfer_ntransfer();
975                        }
976                        _ => {
977                            ar.ftransfer = 0;
978                            ar.ntransfer = 0;
979                        }
980                    }
981                }
982            }
983            b'L' | b'f' => {}
984            _ => {
985                status = false;
986            }
987        }
988    }
989    status
990}
991
992/// Returns debug information about a function or active call frame.
993///
994pub fn get_info(state: &mut LuaState, what: &[u8], ar: &mut LuaDebug) -> bool {
995    let (cl, ci_idx, func_val, what) = if what.first() == Some(&b'>') {
996        let func_val = state.peek_at(state.top_idx() - 1).clone();
997        state.pop_n(1);
998        debug_assert!(
999            matches!(func_val, LuaValue::Function(_)),
1000            "get_info: function expected"
1001        );
1002        let cl = match &func_val {
1003            LuaValue::Function(LuaClosure::Lua(_) | LuaClosure::C(_)) => Some(match &func_val {
1004                LuaValue::Function(c) => c.clone(),
1005                _ => unreachable!(),
1006            }),
1007            _ => None,
1008        };
1009        (cl, None, func_val, &what[1..])
1010    } else {
1011        let ci_idx = match ar.i_ci {
1012            Some(i) => i,
1013            None => return false,
1014        };
1015        if state.global().lua_version == lua_types::LuaVersion::V51
1016            && state.is_base_ci(ci_idx)
1017        {
1018            return get_info_tailcall_51(state, what, ar);
1019        }
1020        let func_val = state.get_at(state.get_ci(ci_idx).func).clone();
1021        debug_assert!(
1022            matches!(func_val, LuaValue::Function(_)),
1023            "get_info: non-function at ci->func"
1024        );
1025        let cl = match &func_val {
1026            LuaValue::Function(LuaClosure::Lua(_) | LuaClosure::C(_)) => Some(match &func_val {
1027                LuaValue::Function(c) => c.clone(),
1028                _ => unreachable!(),
1029            }),
1030            _ => None,
1031        };
1032        (cl, Some(ci_idx), func_val, what)
1033    };
1034
1035    let ci = ci_idx.and_then(|idx| Some(state.get_ci(idx).clone()));
1036    let status = aux_get_info(state, what, ar, cl.as_ref(), ci.as_ref());
1037
1038    if what.contains(&b'f') {
1039        state.push(func_val);
1040    }
1041    if what.contains(&b'L') {
1042        let _ = collect_valid_lines(state, cl.as_ref());
1043    }
1044    status
1045}
1046
1047/// Fills `ar` for a Lua 5.1 synthetic `(tail call)` frame, mirroring C's
1048/// `info_tailcall`. The frame has no associated closure, so every option that
1049/// would inspect one yields the tail defaults: `what == "tail"`,
1050/// `source == "=(tail call)"` (rendered `(tail call)`), all lines `-1`, empty
1051/// name/namewhat, zero upvalues, and a `nil` function pushed for the `'f'`
1052/// option / `nil` valid-lines table for `'L'`.
1053fn get_info_tailcall_51(state: &mut LuaState, what: &[u8], ar: &mut LuaDebug) -> bool {
1054    let what = if what.first() == Some(&b'>') {
1055        &what[1..]
1056    } else {
1057        what
1058    };
1059    info_tailcall(ar);
1060    let mut status = true;
1061    for &ch in what {
1062        if !matches!(ch, b'S' | b'l' | b'u' | b'n' | b't' | b'r' | b'L' | b'f') {
1063            status = false;
1064        }
1065    }
1066    if what.contains(&b'f') {
1067        state.push(LuaValue::Nil);
1068    }
1069    if what.contains(&b'L') {
1070        state.push(LuaValue::Nil);
1071    }
1072    status
1073}
1074
1075/// Sets the tail-frame fields on `ar`. See `get_info_tailcall_51`.
1076fn info_tailcall(ar: &mut LuaDebug) {
1077    ar.name = Some(Vec::new());
1078    ar.namewhat = Some(b"");
1079    ar.what = Some(b"tail");
1080    ar.linedefined = -1;
1081    ar.lastlinedefined = -1;
1082    ar.currentline = -1;
1083    ar.source = Some(b"=(tail call)".to_vec());
1084    ar.srclen = b"=(tail call)".len();
1085    chunk_id(&mut ar.short_src, b"=(tail call)", b"=(tail call)".len());
1086    ar.nups = 0;
1087    ar.istailcall = false;
1088}
1089
1090// ─── Symbolic execution — finding which instruction set a register ────────────
1091
1092/// Filters a pc: if `pc` is inside a conditional branch (before `jmptarget`),
1093/// returns -1 (unknown); otherwise returns `pc`.
1094///
1095#[inline]
1096fn filter_pc(pc: i32, jmptarget: i32) -> i32 {
1097    if pc < jmptarget {
1098        -1
1099    } else {
1100        pc
1101    }
1102}
1103
1104/// Finds the last instruction before `lastpc` that wrote to register `reg`.
1105/// Returns the pc of that instruction, or -1 if not found.
1106///
1107fn find_set_reg(p: &LuaProto, lastpc: i32, reg: i32) -> i32 {
1108    let mut setreg: i32 = -1;
1109    let mut jmptarget: i32 = 0;
1110
1111    let effective_lastpc = if p
1112        .code
1113        .get(lastpc as usize)
1114        .map_or(false, |i| i.is_mm_mode())
1115    {
1116        lastpc - 1
1117    } else {
1118        lastpc
1119    };
1120
1121    for pc in 0..effective_lastpc {
1122        let instr = p.code[pc as usize];
1123        let op = instr.opcode();
1124        let a = instr.arg_a() as i32;
1125
1126        let change = match op {
1127            OpCode::LoadNil => {
1128                let b = instr.arg_b() as i32;
1129                a <= reg && reg <= a + b
1130            }
1131            OpCode::TForCall => reg >= a + 2,
1132            OpCode::Call | OpCode::TailCall => reg >= a,
1133            OpCode::Jmp => {
1134                let b = instr.arg_s_j();
1135                let dest = pc + 1 + b;
1136                if dest <= effective_lastpc && dest > jmptarget {
1137                    jmptarget = dest;
1138                }
1139                false
1140            }
1141            _ => {
1142                instr.test_a_mode() && reg == a
1143            }
1144        };
1145
1146        if change {
1147            setreg = filter_pc(pc, jmptarget);
1148        }
1149    }
1150    setreg
1151}
1152
1153/// Finds a "name" for the constant at `index` in proto `p`.
1154/// Returns `Some("constant")` and sets `*name` to the string content,
1155/// or returns `None` and sets `*name` to `"?"`.
1156///
1157fn kname<'a>(p: &'a LuaProto, index: usize, name: &mut &'a [u8]) -> Option<&'static [u8]> {
1158    match p.k.get(index) {
1159        Some(LuaValue::Str(s)) => {
1160            *name = s.as_bytes();
1161            Some(b"constant")
1162        }
1163        _ => {
1164            *name = b"?";
1165            None
1166        }
1167    }
1168}
1169
1170/// Tries to find a basic name for register `reg` in proto `p` at instruction `ppc`.
1171/// Returns the "kind" of the name (e.g. "local", "upvalue", "constant"), or `None`.
1172///
1173fn basic_get_obj_name<'a>(
1174    p: &'a LuaProto,
1175    ppc: &mut i32,
1176    reg: i32,
1177    name: &mut &'a [u8],
1178) -> Option<&'static [u8]> {
1179    let pc = *ppc;
1180    //    if (*name) return "local";
1181    if let Some(local_name) = get_local_name(p, reg + 1, pc) {
1182        *name = local_name;
1183        return Some(b"local");
1184    }
1185
1186    *ppc = find_set_reg(p, pc, reg);
1187    let pc = *ppc;
1188
1189    if pc == -1 {
1190        return None;
1191    }
1192
1193    let instr = p.code[pc as usize];
1194    let op = instr.opcode();
1195    match op {
1196        OpCode::Move => {
1197            let b = instr.arg_b() as i32;
1198            if b < instr.arg_a() as i32 {
1199                return basic_get_obj_name(p, ppc, b, name);
1200            }
1201        }
1202        OpCode::GetUpVal => {
1203            *name = upval_name(p, instr.arg_b() as usize);
1204            return Some(b"upvalue");
1205        }
1206        OpCode::LoadK => {
1207            return kname(p, instr.arg_bx() as usize, name);
1208        }
1209        OpCode::LoadKx => {
1210            let next = p.code[(pc + 1) as usize];
1211            return kname(p, next.arg_ax() as usize, name);
1212        }
1213        _ => {}
1214    }
1215    None
1216}
1217
1218/// Finds a name for a register-or-K instruction's `C` field (the key side).
1219/// Stores a "constant name" if possible, otherwise `"?"`.
1220///
1221fn rname<'a>(p: &'a LuaProto, pc: i32, c: i32, name: &mut &'a [u8]) {
1222    let mut pc = pc;
1223    let what = basic_get_obj_name(p, &mut pc, c, name);
1224    if !matches!(what, Some(kind) if kind.first() == Some(&b'c')) {
1225        *name = b"?";
1226    }
1227}
1228
1229/// Finds the name for an RK-encoded `C` operand (either a constant or a register).
1230///
1231fn rkname<'a>(p: &'a LuaProto, pc: i32, instr: Instruction, name: &mut &'a [u8]) {
1232    let c = instr.arg_c() as i32;
1233    if instr.arg_k() != 0 {
1234        kname(p, c as usize, name);
1235    } else {
1236        rname(p, pc, c, name);
1237    }
1238}
1239
1240/// Determines whether the table indexed by instruction `i` is `_ENV`.
1241/// Returns `"global"` if so, `"field"` otherwise.
1242///
1243fn is_env<'a>(p: &'a LuaProto, pc: i32, instr: Instruction, isup: bool) -> &'static [u8] {
1244    let t = instr.arg_b() as usize;
1245    let mut name: &[u8] = b"?";
1246    if isup {
1247        name = upval_name(p, t);
1248    } else {
1249        let mut pc = pc;
1250        let what = basic_get_obj_name(p, &mut pc, t as i32, &mut name);
1251        if !matches!(what, Some(kind) if kind == b"local" || kind == b"upvalue") {
1252            name = b"?";
1253        }
1254    }
1255    if name == LUA_ENV {
1256        b"global"
1257    } else {
1258        b"field"
1259    }
1260}
1261
1262/// Extended version of `basic_get_obj_name` that also handles table accesses.
1263/// Returns the "kind" of name, or `None`.
1264///
1265fn get_obj_name<'a>(
1266    p: &'a LuaProto,
1267    lastpc: i32,
1268    reg: i32,
1269    name: &mut &'a [u8],
1270) -> Option<&'static [u8]> {
1271    let mut lastpc = lastpc;
1272    let kind = basic_get_obj_name(p, &mut lastpc, reg, name);
1273    if kind.is_some() {
1274        return kind;
1275    }
1276
1277    if lastpc == -1 {
1278        return None;
1279    }
1280
1281    let instr = p.code[lastpc as usize];
1282    let op = instr.opcode();
1283    match op {
1284        OpCode::GetTabUp => {
1285            let k = instr.arg_c() as usize;
1286            kname(p, k, name);
1287            Some(is_env(p, lastpc, instr, true))
1288        }
1289        OpCode::GetTable => {
1290            let k = instr.arg_c() as i32;
1291            rname(p, lastpc, k, name);
1292            Some(is_env(p, lastpc, instr, false))
1293        }
1294        OpCode::GetI => {
1295            *name = b"integer index";
1296            Some(b"field")
1297        }
1298        OpCode::GetField => {
1299            let k = instr.arg_c() as usize;
1300            kname(p, k, name);
1301            Some(is_env(p, lastpc, instr, false))
1302        }
1303        OpCode::Self_ => {
1304            rkname(p, lastpc, instr, name);
1305            Some(b"method")
1306        }
1307        _ => None,
1308    }
1309}
1310
1311// ─── Function naming ──────────────────────────────────────────────────────────
1312
1313/// Tries to derive a name for a function from the bytecode instruction that
1314/// called it. Returns the "kind" of call (e.g. "for iterator", "metamethod"),
1315/// or `None`.
1316///
1317fn funcname_from_code<'a>(
1318    state: &LuaState,
1319    p: &'a LuaProto,
1320    pc: i32,
1321    name: &mut Option<Vec<u8>>,
1322) -> Option<&'static [u8]> {
1323    let instr = p.code[pc as usize];
1324    let op = instr.opcode();
1325
1326    match op {
1327        OpCode::Call | OpCode::TailCall => {
1328            let mut name_bytes: &[u8] = b"?";
1329            let kind = get_obj_name(p, pc, instr.arg_a() as i32, &mut name_bytes);
1330            *name = Some(name_bytes.to_vec());
1331            kind
1332        }
1333        OpCode::TForCall => {
1334            *name = Some(b"for iterator".to_vec());
1335            Some(b"for iterator")
1336        }
1337        // Metamethod dispatch cases — look up tm name from GlobalState
1338        OpCode::Self_ | OpCode::GetTabUp | OpCode::GetTable | OpCode::GetI | OpCode::GetField => {
1339            get_tm_name(state, TagMethod::Index, name)
1340        }
1341        OpCode::SetTabUp | OpCode::SetTable | OpCode::SetI | OpCode::SetField => {
1342            get_tm_name(state, TagMethod::NewIndex, name)
1343        }
1344        OpCode::MmBin | OpCode::MmBinI | OpCode::MmBinK => {
1345            let tm_idx = instr.arg_c() as u8;
1346            let tm = TagMethod::from_u8(tm_idx);
1347            get_tm_name(state, tm, name)
1348        }
1349        OpCode::Unm => get_tm_name(state, TagMethod::Unm, name),
1350        OpCode::BNot => get_tm_name(state, TagMethod::BNot, name),
1351        OpCode::Len => get_tm_name(state, TagMethod::Len, name),
1352        OpCode::Concat => get_tm_name(state, TagMethod::Concat, name),
1353        OpCode::Eq => get_tm_name(state, TagMethod::Eq, name),
1354        OpCode::Lt | OpCode::LtI | OpCode::GtI => get_tm_name(state, TagMethod::Lt, name),
1355        OpCode::Le | OpCode::LeI | OpCode::GeI => get_tm_name(state, TagMethod::Le, name),
1356        OpCode::Close | OpCode::Return => get_tm_name(state, TagMethod::Close, name),
1357        _ => None,
1358    }
1359}
1360
1361/// Looks up the name for tag method `tm` from GlobalState and stores it in `*name`.
1362/// Returns `Some("metamethod")`, or `None` on Lua 5.1.
1363///
1364/// 5.1's `getfuncname` only recognises `OP_CALL`/`OP_TAILCALL`/
1365/// `OP_TFORLOOP`; it never names a metamethod-dispatched call, so a 5.1
1366/// metamethod handler reports `namewhat == "" , name == nil`. 5.2/5.3 added the
1367/// metamethod cases and report the raw event name (`__index`). 5.4's
1368/// `funcnamefromcode` advances the event name by `+2` to drop the leading `__`
1369/// (`__index` -> `index`). db.lua (5.2) asserts `info.name == "__index"`.
1370fn get_tm_name(
1371    state: &LuaState,
1372    tm: TagMethod,
1373    name: &mut Option<Vec<u8>>,
1374) -> Option<&'static [u8]> {
1375    if state.global().lua_version == lua_types::LuaVersion::V51 {
1376        return None;
1377    }
1378    // tm_name returns Option<GcRef<LuaString>>; materialise the bytes before
1379    // stripping so there is no borrow of a temporary.
1380    let raw_bytes: Vec<u8> = state
1381        .global()
1382        .tm_name(tm)
1383        .map(|s| s.as_bytes().to_vec())
1384        .unwrap_or_default();
1385    let keeps_prefix = matches!(
1386        state.global().lua_version,
1387        lua_types::LuaVersion::V52 | lua_types::LuaVersion::V53
1388    );
1389    let resolved = if keeps_prefix {
1390        raw_bytes
1391    } else {
1392        raw_bytes.strip_prefix(b"__").unwrap_or(&raw_bytes).to_vec()
1393    };
1394    *name = Some(resolved);
1395    Some(b"metamethod")
1396}
1397
1398/// Tries to derive a name for a function from how it was called (`ci`).
1399///
1400fn funcname_from_call<'a>(
1401    state: &'a LuaState,
1402    ci: &CallInfo,
1403    name: &mut Option<Vec<u8>>,
1404) -> Option<&'static [u8]> {
1405    if ci.callstatus & CIST_HOOKED != 0 {
1406        *name = Some(b"?".to_vec());
1407        return Some(b"hook");
1408    }
1409    if ci.callstatus & CIST_FIN != 0 {
1410        *name = Some(b"__gc".to_vec());
1411        return Some(b"metamethod");
1412    }
1413    if ci.is_lua() {
1414        let proto = ci_lua_proto(ci, state);
1415        return funcname_from_code(state, &proto, current_pc(ci), name);
1416    }
1417    None
1418}
1419
1420// ─── Pointer-to-value tracking (varinfo for error messages) ──────────────────
1421
1422/// Checks whether value at stack index `val_idx` is in the call frame `ci`'s
1423/// register window, and if so returns the register index (0-based).
1424/// Returns -1 if not found.
1425///
1426/// C compares raw pointers here; this compares `StackIdx` values instead,
1427/// taking the value's `StackIdx` directly rather than a `*o` pointer.
1428fn in_stack(ci: &CallInfo, val_idx: StackIdx) -> i32 {
1429    let base = StackIdx(ci.func.0 + 1);
1430    let ci_top = ci.top;
1431    let mut pos = 0i32;
1432    let mut cur = base;
1433    while cur.0 < ci_top.0 {
1434        if cur == val_idx {
1435            return pos;
1436        }
1437        cur = StackIdx(cur.0 + 1);
1438        pos += 1;
1439    }
1440    -1
1441}
1442
1443/// Checks whether `val_idx` is the current value of one of the upvalues in the
1444/// Lua closure at `ci`. If so, sets `*name` and returns `Some("upvalue")`.
1445///
1446/// C compares `c->upvals[i]->v.p == o` (pointer identity on open upvalues or
1447/// the closed slot); each thread's stack is a separate allocation, so that
1448/// comparison can never straddle two threads. Here, open upvalues hold a
1449/// `(thread_id, StackIdx)` pair, and `val_idx` is only ever a slot on the
1450/// currently running thread (`state.cached_thread_id`), so both the thread id
1451/// and the index must match — comparing the index alone would let an upvalue
1452/// homed on a different, unrelated thread falsely match a same-numbered
1453/// register here. Closed upvalues cannot be identified by stack position, so
1454/// they are not matched here.
1455///
1456/// `name` is an owned `Vec<u8>` rather than `&[u8]`, matching `find_local`:
1457/// the name is borrowed from `ci_lua_proto`'s owned `GcRef<LuaProto>`, which
1458/// drops at function end, so there is no caller lifetime the slice could be
1459/// tied to.
1460fn get_upval_name(
1461    ci: &CallInfo,
1462    val_idx: StackIdx,
1463    name: &mut Vec<u8>,
1464    state: &LuaState,
1465) -> Option<&'static [u8]> {
1466    let proto = ci_lua_proto(ci, state);
1467    let lua_cl = match state.get_at(ci.func) {
1468        LuaValue::Function(LuaClosure::Lua(cl)) => cl.clone(),
1469        _ => return None,
1470    };
1471    let current_thread = state.cached_thread_id;
1472    for (i, upval_slot) in lua_cl.upvals.iter().enumerate() {
1473        let upval = upval_slot.get();
1474        if let Some((thread_id, idx)) = upval.try_open_payload() {
1475            if thread_id == current_thread && idx == val_idx {
1476                *name = upval_name(&proto, i).to_vec();
1477                return Some(b"upvalue");
1478            }
1479        }
1480    }
1481    None
1482}
1483
1484/// Builds a human-readable "variable info" string like ` (local 'x')` or
1485/// ` (upvalue 'y')` to append to error messages. Returns an empty `Vec<u8>`
1486/// if no information is available.
1487///
1488fn format_var_info(kind: Option<&[u8]>, name: Option<&[u8]>) -> Vec<u8> {
1489    match (kind, name) {
1490        (Some(k), Some(n)) => {
1491            let mut out = Vec::with_capacity(4 + k.len() + n.len());
1492            out.extend_from_slice(b" (");
1493            out.extend_from_slice(k);
1494            out.extend_from_slice(b" '");
1495            out.extend_from_slice(n);
1496            out.extend_from_slice(b"')");
1497            out
1498        }
1499        _ => Vec::new(),
1500    }
1501}
1502
1503/// Returns a description string for the value at `val_idx` in the current call
1504/// frame, e.g. `" (local 'x')"` or `" (upvalue 'y')"`. Used in error messages.
1505///
1506fn var_info(state: &LuaState, val_idx: StackIdx) -> Vec<u8> {
1507    let (kind, name) = var_info_parts(state, val_idx);
1508    format_var_info(kind.as_deref(), name.as_deref())
1509}
1510
1511/// Resolves the `(kind, name)` description for the value at `val_idx` in the
1512/// current call frame (e.g. `(b"local", b"x")`), returning owned bytes so the
1513/// caller can choose the message ordering. Returns `(None, None)` when no
1514/// information is available. Splits the lookup out of `var_info` so the
1515/// type-error constructors can build the 5.1/5.2 `<kind> '<name>' (a <type>
1516/// value)` ordering as well as the 5.3+ `a <type> value (<kind> '<name>')` one.
1517fn var_info_parts(state: &LuaState, val_idx: StackIdx) -> (Option<Vec<u8>>, Option<Vec<u8>>) {
1518    let ci_idx = state.current_ci_idx();
1519    let ci = state.get_ci(ci_idx).clone();
1520    let mut kind: Option<&[u8]> = None;
1521    let mut name_owned: Vec<u8> = b"?".to_vec();
1522
1523    if ci.is_lua() {
1524        let mut up_name: Vec<u8> = b"?".to_vec();
1525        kind = get_upval_name(&ci, val_idx, &mut up_name, state);
1526        if kind.is_some() {
1527            name_owned = up_name;
1528        } else {
1529            let reg = in_stack(&ci, val_idx);
1530            if reg >= 0 {
1531                let proto = ci_lua_proto(&ci, state);
1532                let mut nref: &[u8] = b"?";
1533                let pc = current_pc(&ci);
1534                let k = get_obj_name(&proto, pc, reg, &mut nref);
1535                kind = k;
1536                if kind.is_some() {
1537                    name_owned = nref.to_vec();
1538                }
1539            }
1540        }
1541    }
1542    match kind {
1543        Some(k) => (Some(k.to_vec()), Some(name_owned)),
1544        None => (None, None),
1545    }
1546}
1547
1548// ─── Error-raising functions ──────────────────────────────────────────────────
1549
1550/// Internal helper: raises a type error attributing the failure to the value
1551/// `val` (operation `op`) with optional `(kind, name)` variable info.
1552///
1553/// The attribution ordering is version-gated, mirroring `luaG_typeerror`:
1554/// 5.1/5.2 put the variable clause first — `attempt to <op> <kind> '<name>'
1555/// (a <type> value)` — while 5.3+ trail it — `attempt to <op> a <type> value
1556/// (<kind> '<name>')`. With no variable info both collapse to `attempt to <op>
1557/// a <type> value`.
1558fn typeerror_inner_parts(
1559    state: &LuaState,
1560    val: &LuaValue,
1561    op: &[u8],
1562    kind: Option<&[u8]>,
1563    name: Option<&[u8]>,
1564) -> LuaError {
1565    let t = state.obj_type_name(val);
1566    let legacy_order = matches!(
1567        state.global().lua_version,
1568        lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52
1569    );
1570    let mut msg = Vec::new();
1571    msg.extend_from_slice(b"attempt to ");
1572    msg.extend_from_slice(op);
1573    if let (true, Some(k), Some(n)) = (legacy_order, kind, name) {
1574        msg.extend_from_slice(b" ");
1575        msg.extend_from_slice(k);
1576        msg.extend_from_slice(b" '");
1577        msg.extend_from_slice(n);
1578        msg.extend_from_slice(b"' (a ");
1579        msg.extend_from_slice(&t);
1580        msg.extend_from_slice(b" value)");
1581    } else {
1582        msg.extend_from_slice(b" a ");
1583        msg.extend_from_slice(&t);
1584        msg.extend_from_slice(b" value");
1585        msg.extend_from_slice(&format_var_info(kind, name));
1586    }
1587    prefixed_runtime(state, msg)
1588}
1589
1590/// Raises a type error for performing operation `op` on value `val`.
1591/// Includes variable-info context (e.g. "local 'x'") if available.
1592///
1593pub(crate) fn type_error(
1594    state: &LuaState,
1595    val: &LuaValue,
1596    val_idx: StackIdx,
1597    op: &[u8],
1598) -> LuaError {
1599    let (kind, name) = var_info_parts(state, val_idx);
1600    typeerror_inner_parts(state, val, op, kind.as_deref(), name.as_deref())
1601}
1602
1603/// Raises an arithmetic-coercion type error (the `<=5.3` core path that owns
1604/// string coercion via `luaG_opinterror`/`luaG_aritherror`). Identical to
1605/// `type_error` except for when a `constant` operand is reported:
1606///
1607/// - **5.1** never attributes a `constant` for arithmetic — its `getobjname`
1608///   has no `OP_LOADK` case, so `-"abc"` and `"abc"+1` both give a bare
1609///   `... a string value`.
1610/// - **5.2/5.3** attribute a `constant` only for unary minus (the operand is a
1611///   live register the bytecode can trace back); a binary operand passed to
1612///   `luaG_typeerror` from `luaO_arith` points into the constant table, so
1613///   `varinfo` reports nothing.
1614///
1615/// The `constant` kind was wired into 5.4 arithmetic wording differently and
1616/// 5.4/5.5 never reach this path.
1617pub(crate) fn arith_type_error(
1618    state: &LuaState,
1619    val: &LuaValue,
1620    val_idx: StackIdx,
1621    op: &[u8],
1622    binary: bool,
1623) -> LuaError {
1624    let (kind, name) = var_info_parts(state, val_idx);
1625    let is_constant = matches!(kind.as_deref(), Some(b"constant"));
1626    let suppress_constant = is_constant
1627        && match state.global().lua_version {
1628            lua_types::LuaVersion::V51 => true,
1629            lua_types::LuaVersion::V52 | lua_types::LuaVersion::V53 => binary,
1630            _ => false,
1631        };
1632    let (kind, name) = if suppress_constant {
1633        (None, None)
1634    } else {
1635        (kind, name)
1636    };
1637    typeerror_inner_parts(state, val, op, kind.as_deref(), name.as_deref())
1638}
1639
1640/// Variant of `type_error` for bytecode paths where the target isn't on the
1641/// active stack — OP_SETTABUP / OP_GETTABUP read directly from the closure's
1642/// upvalue cells, so `var_info`'s in-stack heuristic can't recover the name.
1643/// The caller passes a pre-formatted `(kind, name)` pair (e.g.
1644/// `(b"upvalue", b"a")`) used verbatim in the trailing `(kind 'name')`.
1645pub(crate) fn type_error_with_hint(
1646    state: &LuaState,
1647    val: &LuaValue,
1648    op: &[u8],
1649    kind: &[u8],
1650    name: &[u8],
1651) -> LuaError {
1652    let t = obj_type_name_static(val);
1653    let legacy_order = matches!(
1654        state.global().lua_version,
1655        lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52
1656    );
1657    let mut msg = Vec::new();
1658    msg.extend_from_slice(b"attempt to ");
1659    msg.extend_from_slice(op);
1660    if legacy_order {
1661        msg.extend_from_slice(b" ");
1662        msg.extend_from_slice(kind);
1663        msg.extend_from_slice(b" '");
1664        msg.extend_from_slice(name);
1665        msg.extend_from_slice(b"' (a ");
1666        msg.extend_from_slice(t);
1667        msg.extend_from_slice(b" value)");
1668    } else {
1669        msg.extend_from_slice(b" a ");
1670        msg.extend_from_slice(t);
1671        msg.extend_from_slice(b" value");
1672        msg.extend_from_slice(&format_var_info(Some(kind), Some(name)));
1673    }
1674    prefixed_runtime(state, msg)
1675}
1676
1677/// Standalone type-name accessor that does not require `&LuaState`. Used by
1678/// `type_error_with_hint` since callers there cannot easily thread `state`.
1679fn obj_type_name_static(val: &LuaValue) -> &'static [u8] {
1680    match val {
1681        LuaValue::Nil => b"nil",
1682        LuaValue::Bool(_) => b"boolean",
1683        LuaValue::Int(_) | LuaValue::Float(_) => b"number",
1684        LuaValue::Str(_) => b"string",
1685        LuaValue::Table(_) => b"table",
1686        LuaValue::Function(_) => b"function",
1687        LuaValue::UserData(_) => b"userdata",
1688        LuaValue::LightUserData(_) => b"light userdata",
1689        LuaValue::Thread(_) => b"thread",
1690    }
1691}
1692
1693/// Raises a "call" type error for a non-callable `val`.
1694///
1695/// Lua 5.4 introduced `luaG_callerror`, which attributes the failed call via
1696/// `funcnamefromcall`/`funcnamefromcode` on the calling instruction. That is how
1697/// 5.4/5.5 name a generic-for iterator failure `(for iterator 'for iterator')`.
1698/// Lua 5.1/5.2/5.3 had no such path: a non-callable value raised a plain
1699/// `luaG_typeerror` whose `varinfo` only names the value's register or upvalue,
1700/// so `for k,v in 3 do` reports the bare `attempt to call a number value`.
1701pub(crate) fn call_error(state: &LuaState, val: &LuaValue, val_idx: StackIdx) -> LuaError {
1702    let uses_callerror = matches!(
1703        state.global().lua_version,
1704        lua_types::LuaVersion::V54 | lua_types::LuaVersion::V55
1705    );
1706    let (kind, name) = if uses_callerror {
1707        let ci_idx = state.current_ci_idx();
1708        let ci = state.get_ci(ci_idx).clone();
1709        let mut name: Option<Vec<u8>> = None;
1710        let kind = funcname_from_call(state, &ci, &mut name);
1711        if kind.is_some() {
1712            (kind.map(|k| k.to_vec()), name)
1713        } else {
1714            var_info_parts(state, val_idx)
1715        }
1716    } else {
1717        var_info_parts(state, val_idx)
1718    };
1719    typeerror_inner_parts(state, val, b"call", kind.as_deref(), name.as_deref())
1720}
1721
1722/// Raises a "bad 'for' <what>" error.
1723///
1724pub(crate) fn for_error(state: &mut LuaState, val: &LuaValue, what: &[u8]) -> LuaError {
1725    // Lua 5.3 (and 5.1/5.2) use the older wording `'for' <what> must be a
1726    // number`; 5.4 reworded it to `bad 'for' <what> (number expected, got
1727    // <type>)` (`forerror` / `luaG_forerror`). Match each version's reference.
1728    if matches!(
1729        state.global().lua_version,
1730        lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52 | lua_types::LuaVersion::V53
1731    ) {
1732        let mut msg = Vec::new();
1733        msg.extend_from_slice(b"'for' ");
1734        msg.extend_from_slice(what);
1735        msg.extend_from_slice(b" must be a number");
1736        return prefixed_runtime(state, msg);
1737    }
1738    let t = crate::tagmethods::obj_type_name(state, val)
1739        .unwrap_or_else(|_| crate::tagmethods::type_name(val.base_type()).to_vec());
1740    let mut msg = Vec::new();
1741    msg.extend_from_slice(b"bad 'for' ");
1742    msg.extend_from_slice(what);
1743    msg.extend_from_slice(b" (number expected, got ");
1744    msg.extend_from_slice(&t);
1745    msg.push(b')');
1746    prefixed_runtime(state, msg)
1747}
1748
1749/// Raises an arithmetic type error. If `p1` is not a number, blames `p1`;
1750/// otherwise blames `p2`.
1751///
1752pub(crate) fn op_int_error(
1753    state: &LuaState,
1754    p1: &LuaValue,
1755    p1_idx: StackIdx,
1756    p2: &LuaValue,
1757    p2_idx: StackIdx,
1758    msg: &[u8],
1759) -> LuaError {
1760    let (bad_val, bad_idx) = if !matches!(p1, LuaValue::Int(_) | LuaValue::Float(_)) {
1761        (p1, p1_idx)
1762    } else {
1763        (p2, p2_idx)
1764    };
1765    type_error(state, bad_val, bad_idx, msg)
1766}
1767
1768/// Raises an "no integer representation" error for float→int conversion failure.
1769///
1770///
1771/// Stack indices are optional: when an operand is from a constant table or
1772/// an immediate, no register backs it and `var_info` has nothing to report.
1773pub(crate) fn to_int_error(
1774    state: &LuaState,
1775    p1: &LuaValue,
1776    p1_idx: Option<StackIdx>,
1777    _p2: &LuaValue,
1778    p2_idx: Option<StackIdx>,
1779) -> LuaError {
1780    let bad_idx = if p1.to_integer_no_strconv().is_none() {
1781        p1_idx
1782    } else {
1783        p2_idx
1784    };
1785    let extra = match bad_idx {
1786        Some(idx) => var_info(state, idx),
1787        None => Vec::new(),
1788    };
1789    let mut msg = Vec::new();
1790    msg.extend_from_slice(b"number");
1791    msg.extend_from_slice(&extra);
1792    msg.extend_from_slice(b" has no integer representation");
1793    prefixed_runtime(state, msg)
1794}
1795
1796/// Raises an order-comparison type error for incompatible types.
1797///
1798pub(crate) fn order_error(state: &LuaState, p1: &LuaValue, p2: &LuaValue) -> LuaError {
1799    let t1 = state.obj_type_name(p1);
1800    let t2 = state.obj_type_name(p2);
1801    let msg = if t1 == t2 {
1802        let mut m = Vec::new();
1803        m.extend_from_slice(b"attempt to compare two ");
1804        m.extend_from_slice(&t1);
1805        m.extend_from_slice(b" values");
1806        m
1807    } else {
1808        let mut m = Vec::new();
1809        m.extend_from_slice(b"attempt to compare ");
1810        m.extend_from_slice(&t1);
1811        m.extend_from_slice(b" with ");
1812        m.extend_from_slice(&t2);
1813        m
1814    };
1815    prefixed_runtime(state, msg)
1816}
1817
1818/// Prepends `src:line: ` to `msg` (as a new Lua string on the stack) and
1819/// returns the formatted string.
1820///
1821///
1822/// The C signature takes `lua_State *L` because the result is pushed onto the
1823/// Lua stack via `luaO_pushfstring`. Our port returns `Vec<u8>` instead, so
1824/// the state parameter is unused — keep an optional reference for callers
1825/// that still pass one, but the function works without it.
1826pub(crate) fn add_info(
1827    _state: Option<&mut LuaState>,
1828    msg: &[u8],
1829    src: Option<&LuaString>,
1830    line: i32,
1831    unknown_line_as_question: bool,
1832) -> Vec<u8> {
1833    let mut buff = [0u8; LUA_IDSIZE];
1834    if let Some(src) = src {
1835        chunk_id(&mut buff, src.as_bytes(), src.len());
1836    } else if unknown_line_as_question {
1837        let mut out = Vec::with_capacity(5 + msg.len());
1838        out.extend_from_slice(b"?:?: ");
1839        out.extend_from_slice(msg);
1840        return out;
1841    } else {
1842        buff[0] = b'?';
1843    }
1844    // Returns the formatted Vec<u8> instead of pushing on the stack; callers
1845    // that need the result on the stack push it themselves.
1846    let src_part = buff
1847        .iter()
1848        .position(|&b| b == 0)
1849        .map_or(&buff[..], |n| &buff[..n]);
1850    let mut out = Vec::with_capacity(src_part.len() + 12 + msg.len());
1851    out.extend_from_slice(src_part);
1852    out.push(b':');
1853    // Write line number as decimal bytes
1854    let line_str = line.to_string();
1855    out.extend_from_slice(line_str.as_bytes());
1856    out.extend_from_slice(b": ");
1857    out.extend_from_slice(msg);
1858    out
1859}
1860
1861// ─── Line change detection ────────────────────────────────────────────────────
1862
1863/// Checks whether instruction `newpc` is on a different source line than `oldpc`.
1864///
1865fn changed_line(p: &LuaProto, oldpc: i32, newpc: i32) -> bool {
1866    if p.lineinfo.is_empty() {
1867        return false;
1868    }
1869
1870    if newpc - oldpc < MAX_IWTH_ABS / 2 {
1871        let mut delta: i32 = 0;
1872        let mut pc = oldpc;
1873        loop {
1874            pc += 1;
1875            if pc as usize >= p.lineinfo.len() {
1876                break;
1877            }
1878            let lineinfo = p.lineinfo[pc as usize];
1879            if lineinfo == ABS_LINE_INFO {
1880                break;
1881            }
1882            delta += lineinfo as i32;
1883            if pc == newpc {
1884                return delta != 0;
1885            }
1886        }
1887    }
1888    get_func_line(p, oldpc) != get_func_line(p, newpc)
1889}
1890
1891// ─── Trace execution hooks ────────────────────────────────────────────────────
1892
1893/// Called at the start of a Lua function. Fires the call hook if appropriate.
1894/// Returns 1 to keep the trap on, 0 to turn it off.
1895///
1896pub(crate) fn trace_call(state: &mut LuaState) -> Result<i32, LuaError> {
1897    let ci_idx = state.current_ci_idx();
1898    let ci = state.get_ci(ci_idx).clone();
1899    state.get_ci_mut(ci_idx).set_trap(true);
1900    let proto = ci_lua_proto(&ci, state);
1901
1902    if ci.saved_pc() == 0 {
1903        if proto.is_vararg {
1904            return Ok(0);
1905        } else if ci.callstatus & CIST_HOOKYIELD == 0 {
1906            state.hook_call(ci_idx)?;
1907        }
1908    }
1909    Ok(1)
1910}
1911
1912/// Called before each VM instruction when debugging is active.
1913/// Fires line and count hooks as appropriate.
1914/// Returns 1 to keep trap on, 0 to turn it off.
1915///
1916/// C's `pc` parameter is a pointer to the instruction array. Here, `pc` is
1917/// the 0-based index of the NEXT instruction (same semantic as `savedpc`);
1918/// after incrementing for reference (`pc++` in C), it equals the
1919/// next-instruction index.
1920pub(crate) fn trace_exec(state: &mut LuaState, pc: u32) -> Result<i32, LuaError> {
1921    let ci_idx = state.current_ci_idx();
1922    let ci = state.get_ci(ci_idx).clone();
1923
1924    let mask = state.hook_mask();
1925
1926    if !state.allowhook {
1927        return Ok(1);
1928    }
1929
1930    if mask & (LUA_MASKLINE | LUA_MASKCOUNT) == 0 {
1931        state.get_ci_mut(ci_idx).set_trap(false);
1932        return Ok(0);
1933    }
1934
1935    let next_pc = pc + 1;
1936    state.get_ci_mut(ci_idx).set_saved_pc(next_pc);
1937
1938    let counthook = if mask & LUA_MASKCOUNT != 0 {
1939        let hc = state.hook_count() - 1;
1940        state.set_hook_count(hc);
1941        hc == 0
1942    } else {
1943        false
1944    };
1945
1946    if counthook {
1947        state.reset_hook_count();
1948    } else if mask & LUA_MASKLINE == 0 {
1949        return Ok(1);
1950    }
1951
1952    // Sandbox enforcement: charge the runtime-wide budget once per count-hook
1953    // interval, on every thread. Native (returns `Err` directly) and
1954    // independent of any user `debug.sethook` closure — the count mask may be
1955    // armed purely for the sandbox with no user hook installed.
1956    if counthook {
1957        if let Some(err) = state.sandbox_charge_interval() {
1958            return Err(err);
1959        }
1960    }
1961
1962    if ci.callstatus & CIST_HOOKYIELD != 0 {
1963        state.get_ci_mut(ci_idx).callstatus &= !CIST_HOOKYIELD;
1964        return Ok(1);
1965    }
1966
1967    if state.ci_lua_closure(ci_idx).is_none() {
1968        return Ok(1);
1969    }
1970
1971    let cur_instr = state.get_proto_instr(ci_idx, pc as u32);
1972    if !cur_instr.is_in_top() {
1973        let ci_top = state.get_ci(ci_idx).top;
1974        state.set_top(ci_top);
1975    }
1976
1977    if counthook {
1978        state.call_hook_event(LUA_HOOKCOUNT, -1)?;
1979    }
1980
1981    if mask & LUA_MASKLINE != 0 {
1982        let proto = ci_lua_proto(&ci, state);
1983        let oldpc = if state.old_pc() < proto.code.len() as u32 {
1984            state.old_pc() as i32
1985        } else {
1986            0
1987        };
1988        // current instruction is pc (0-based); pcRel gives current = next - 1
1989        let npci = next_pc as i32 - 1;
1990
1991        if npci <= oldpc || changed_line(&proto, oldpc, npci) {
1992            let newline = get_func_line(&proto, npci);
1993            state.call_hook_event(LUA_HOOKLINE, newline)?;
1994        }
1995        state.set_old_pc(npci as u32);
1996    }
1997
1998    if state.status() == lua_types::status::LuaStatus::Yield {
1999        if counthook {
2000            state.set_hook_count(1);
2001        }
2002        state.get_ci_mut(ci_idx).callstatus |= CIST_HOOKYIELD;
2003        return Err(LuaError::Yield);
2004    }
2005
2006    Ok(1)
2007}
2008
2009// ─── File-local helpers referenced above but not directly translated ──────────
2010
2011/// Gets the source line name (short, truncated) for error messages.
2012///
2013/// to the real impl in `crate::object`. Handles `=name`, `@filename`, and
2014/// `[string "..."]` formatting so error prefixes are concise rather than dumping
2015/// the entire source verbatim.
2016fn chunk_id(out: &mut [u8; LUA_IDSIZE], source: &[u8], _srclen: usize) {
2017    out.fill(0);
2018    let n = crate::object::chunk_id(&mut out[..], source);
2019    if n < out.len() {
2020        out[n] = 0;
2021    }
2022}
2023
2024/// Gets the local variable name for register `reg+1` at instruction `pc` in `p`.
2025/// Returns `None` if not found (variable is not live at `pc`).
2026///
2027fn get_local_name(p: &LuaProto, n: i32, pc: i32) -> Option<&[u8]> {
2028    crate::func::get_local_name(p, n, pc)
2029}
2030
2031/// Gets the n-th local name from a Lua closure (for non-active function query).
2032fn get_local_name_from_closure(cl: &LuaClosureLua, n: i32, pc: i32) -> Option<&[u8]> {
2033    get_local_name(&cl.proto, n, pc)
2034}
2035
2036/// Retrieves the LuaProto for the Lua closure at `ci.func` from the stack.
2037///
2038/// C's version returns a raw pointer via a macro. This returns an owned
2039/// `GcRef<LuaProto>` (an Rc clone) rather than a borrowed reference, since a
2040/// reference into `get_at`'s result would point at a temporary `LuaValue`.
2041/// Callers deref through `GcRef<T>: Deref<Target=T>`.
2042fn ci_lua_proto(ci: &CallInfo, state: &LuaState) -> GcRef<LuaProto> {
2043    match state.get_at(ci.func) {
2044        LuaValue::Function(LuaClosure::Lua(cl)) => cl.proto.clone(),
2045        _ => panic!("ci_lua_proto: call frame does not hold a Lua closure"),
2046    }
2047}