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//! # C source
7//! `reference/lua-5.4.7/src/ldebug.c` (962 lines, 30 functions)
8
9
10#[allow(unused_imports)] use crate::prelude::*;
11use crate::state::{
12    CallInfo, GcRef, LuaClosure, LuaClosureLua, LuaProto, LuaState, LuaTable, LuaValue,
13    UpVal, CIST_C, CIST_FIN, CIST_HOOKED, CIST_HOOKYIELD, CIST_TAIL, CIST_TRAN,
14};
15use lua_types::{CallInfoIdx, StackIdx, LuaString};
16use lua_types::error::LuaError;
17use lua_types::opcode::Instruction;
18use crate::vm::InstructionExt;
19
20// TODO(port): the following are cross-crate imports that will resolve in Phase B:
21//   - LuaDebug  (lua_Debug struct; Phase E debug)
22//   - HookEvent (LUA_HOOKCALL / LUA_HOOKLINE / LUA_HOOKCOUNT constants)
23//   - LuaStatus (LUA_OK / LUA_YIELD / LUA_ERRRUN)
24//   - luaF_getlocalname — from crate::func
25//   - luaT_objtypename  — from crate::tagmethods
26//   - luaO_chunkid      — from crate::object
27//   - luaD_hookcall, luaD_hook, luaD_callnoyield — from crate::do_
28//   - luaH_setint       — from crate::table
29//   - luaV_tointegerns  — from crate::vm
30//   - OpCode, Instruction field accessors — from lua_code crate
31
32// ─── Constants from macros.tsv / ldebug.h ────────────────────────────────────
33
34// macros.tsv: ABSLINEINFO → const ABS_LINE_INFO: i8 = -0x80
35const ABS_LINE_INFO: i8 = -0x80_i8;
36
37// macros.tsv: MAXIWTHABS → const MAX_IWTH_ABS: i32 = 128
38const MAX_IWTH_ABS: i32 = 128;
39
40// TODO(port): import from lua_types or luaconf.h translation
41const LUA_IDSIZE: usize = 60;
42
43// TODO(port): import from HookEvent enum once defined
44const LUA_MASKLINE: u8 = 1 << 2;
45const LUA_MASKCOUNT: u8 = 1 << 3;
46const LUA_MASKCALL: u8 = 1 << 0;
47
48const LUA_HOOKLINE: i32 = 2;
49const LUA_HOOKCOUNT: i32 = 3;
50
51// TODO(port): replace with LuaStatus::Yield once enum is defined
52const LUA_YIELD_STATUS: i32 = 1;
53
54// macros.tsv: LUA_ENV → const LUA_ENV: &[u8] = b"_ENV"
55const LUA_ENV: &[u8] = b"_ENV";
56
57// ─── Local error constructors (not yet in lua-types) ─────────────────────────
58
59/// Build a `LuaError::Runtime` from a raw byte-string message.
60///
61/// TODO(phase-b): expose as `LuaError::runtime_bytes` in lua-types once
62/// that crate has a `LuaString::from_bytes` constructor in its public API.
63fn runtime_bytes(msg: Vec<u8>) -> LuaError {
64    LuaError::Runtime(lua_types::LuaValue::Str(lua_types::GcRef::new(
65        lua_types::LuaString::from_bytes(msg),
66    )))
67}
68
69/// Prepend `[source]:line:` to `msg` when the current call frame is a Lua
70/// function. Mirrors what `luaG_addinfo` does for messages routed through
71/// `luaG_runerror`; the typed error constructors below build their own
72/// message and skip that path, so we add the same prefix here.
73/// Public wrapper for `prefixed_runtime` so other VM modules can re-prefix
74/// bare runtime errors raised from typed-arith helpers with the current call
75/// frame's `source:line:`.
76pub(crate) fn prefixed_runtime_pub(state: &LuaState, msg: Vec<u8>) -> LuaError {
77    prefixed_runtime(state, msg)
78}
79
80fn prefixed_runtime(state: &LuaState, msg: Vec<u8>) -> LuaError {
81    let ci_idx = state.current_ci_idx();
82    let ci = state.get_ci(ci_idx).clone();
83    if !ci.is_lua() {
84        return runtime_bytes(msg);
85    }
86    let proto = ci_lua_proto(&ci, state);
87    let src = proto.source_string();
88    let line = get_current_line(&ci, state);
89    let prefixed = add_info(
90        None,
91        &msg,
92        src.map(|s| &**s),
93        line,
94    );
95    runtime_bytes(prefixed)
96}
97
98pub fn c_api_runtime(state: &LuaState, msg: Vec<u8>) -> LuaError {
99    let ci_idx = state.current_ci_idx();
100    if let Some(parent_idx) = state.prev_ci(ci_idx) {
101        let parent_ci = state.get_ci(parent_idx).clone();
102        if parent_ci.is_lua() {
103            let proto = ci_lua_proto(&parent_ci, state);
104            let src = proto.source_string();
105            let line = get_current_line(&parent_ci, state);
106            let prefixed = add_info(None, &msg, src.map(|s| &**s), line);
107            return runtime_bytes(prefixed);
108        }
109    }
110    runtime_bytes(msg)
111}
112
113/// Walk a table's entries looking for `target` function (by identity).
114/// At `depth == 1`, also recurses one level into table-valued entries so that
115/// e.g. `_G.table.sort` can be found as `"table.sort"`.
116/// Returns the dotted path on success, `None` otherwise.
117/// Mirrors `ldblib.c:findfield` from reference C-Lua 5.4.
118///
119/// Not called from `arg_error_impl` (that path was removed to prevent stack
120/// overflow via re-entrant error generation). Reserved for a future
121/// `debug.findfield` Lua binding.
122#[allow(dead_code)]
123fn find_func_in_table(table: &LuaTable, target: &LuaValue, prefix: &[u8], depth: u8) -> Option<Vec<u8>> {
124    let mut key = LuaValue::Nil;
125    loop {
126        let (k, v) = match table.next_pair(&key) {
127            Some(pair) => pair,
128            None => break,
129        };
130        if !matches!(v, LuaValue::Nil) {
131            let key_bytes: Option<Vec<u8>> = match &k {
132                LuaValue::Str(s) => Some(s.as_bytes().to_vec()),
133                _ => None,
134            };
135            if let Some(kb) = key_bytes {
136                if &v == target {
137                    if prefix.is_empty() {
138                        return Some(kb);
139                    }
140                    let mut result = prefix.to_vec();
141                    result.push(b'.');
142                    result.extend_from_slice(&kb);
143                    return Some(result);
144                }
145                if depth > 0 {
146                    if let LuaValue::Table(sub) = &v {
147                        let new_prefix = if prefix.is_empty() {
148                            kb.clone()
149                        } else {
150                            let mut p = prefix.to_vec();
151                            p.push(b'.');
152                            p.extend_from_slice(&kb);
153                            p
154                        };
155                        if let Some(name) = find_func_in_table(&**sub, target, &new_prefix, depth - 1) {
156                            return Some(name);
157                        }
158                    }
159                }
160            }
161        }
162        key = k;
163    }
164    None
165}
166
167/// When `get_info` cannot resolve a function name (e.g. the function was called
168/// as a value from C code), walk `_G` to find its dotted path by identity.
169/// Returns `None` if not found; caller falls back to `"?"`.
170///
171/// Not called from `arg_error_impl` (that path was removed to prevent stack
172/// overflow via re-entrant error generation). Reserved for a future
173/// `debug.findfield` Lua binding.
174#[allow(dead_code)]
175fn find_func_name_in_globals(state: &LuaState, func_val: &LuaValue) -> Option<Vec<u8>> {
176    let globals = state.global().globals.clone();
177    if let LuaValue::Table(globals_table) = globals {
178        find_func_in_table(&*globals_table, func_val, b"", 1)
179    } else {
180        None
181    }
182}
183
184/// Mirrors C `pushglobalfuncname` (lauxlib.c): search `package.loaded` (the
185/// `_LOADED` registry entry) for `func_val` by identity.  Only descends one
186/// level into each loaded module, so `table.sort` is found as `"table.sort"`.
187///
188/// Uses only raw table lookups (`get_str_bytes`, `next_pair`) — no VM calls,
189/// no metamethods, no GC.  Safe to call from error-formatting paths.
190fn find_func_name_in_loaded(state: &LuaState, func_val: &LuaValue) -> Option<Vec<u8>> {
191    let registry = state.global().l_registry.clone();
192    let loaded = match registry {
193        LuaValue::Table(ref reg_table) => reg_table.get_str_bytes(b"_LOADED"),
194        _ => return None,
195    };
196    let loaded_table = match loaded {
197        LuaValue::Table(t) => t,
198        _ => return None,
199    };
200    find_func_in_table(&*loaded_table, func_val, b"", 1)
201}
202
203/// Equivalent of C `luaL_argerror`: build an arg-type error with function name
204/// (from debug info) and caller source location. Handles method calls by
205/// producing "calling 'f' on bad self ..." when arg==1 and namewhat=="method".
206pub fn arg_error_impl(state: &mut LuaState, mut arg: i32, extramsg: &[u8]) -> LuaError {
207    let mut ar = LuaDebug::default();
208    if !get_stack(state, 0, &mut ar) {
209        let msg = format!("bad argument #{} ({})", arg, String::from_utf8_lossy(extramsg));
210        return c_api_runtime(state, msg.into_bytes());
211    }
212    get_info(state, b"n", &mut ar);
213    if ar.namewhat.as_deref() == Some(b"method") {
214        arg -= 1;
215        if arg == 0 {
216            let name = ar.name.clone().unwrap_or_else(|| b"?".to_vec());
217            let msg = format!(
218                "calling '{}' on bad self ({})",
219                String::from_utf8_lossy(&name),
220                String::from_utf8_lossy(extramsg)
221            );
222            return c_api_runtime(state, msg.into_bytes());
223        }
224    }
225    let fname = ar.name.clone().or_else(|| {
226        let ci_idx = ar.i_ci?;
227        let func_slot = state.get_ci(ci_idx).func;
228        let func_val = state.get_at(func_slot).clone();
229        let found = find_func_name_in_loaded(state, &func_val)?;
230        if found.starts_with(b"_G.") {
231            Some(found[3..].to_vec())
232        } else {
233            Some(found)
234        }
235    }).unwrap_or_else(|| b"?".to_vec());
236    let msg = format!(
237        "bad argument #{} to '{}' ({})",
238        arg,
239        String::from_utf8_lossy(&fname),
240        String::from_utf8_lossy(extramsg)
241    );
242    c_api_runtime(state, msg.into_bytes())
243}
244
245/// Build a `LuaError::Runtime` from the top of the Lua stack.
246///
247/// Pops the error value from the stack and wraps it.
248/// TODO(phase-b): expose as `LuaError::from_top` in lua-types.
249fn runtime_from_top(state: &mut crate::state::LuaState) -> LuaError {
250    let v = state.pop();
251    LuaError::Runtime(v)
252}
253
254// ─── Debug info structures ────────────────────────────────────────────────────
255
256/// Debug introspection record.
257///
258/// holds only the fields that `ldebug.c` writes/reads.
259///
260/// # Port note
261/// `name` and `namewhat` are optional byte strings because in C they can be
262/// NULL. `source` is owned here because we build it from Proto.source (a GcRef).
263/// `short_src` matches C layout as a fixed array.
264pub struct LuaDebug {
265    pub event: i32,
266    pub name: Option<Vec<u8>>,
267    pub namewhat: Option<&'static [u8]>,
268    pub what: Option<&'static [u8]>,
269    pub source: Option<Vec<u8>>,
270    pub srclen: usize,
271    pub currentline: i32,
272    pub linedefined: i32,
273    pub lastlinedefined: i32,
274    pub nups: u8,
275    pub nparams: u8,
276    pub isvararg: bool,
277    pub istailcall: bool,
278    pub ftransfer: u16,
279    pub ntransfer: u16,
280    pub short_src: [u8; LUA_IDSIZE],
281    // PORT NOTE: C stores a raw pointer; Rust stores an index into LuaState.call_stack.
282    pub i_ci: Option<CallInfoIdx>,
283}
284
285impl Default for LuaDebug {
286    fn default() -> Self {
287        LuaDebug {
288            event: 0,
289            name: None,
290            namewhat: None,
291            what: None,
292            source: None,
293            srclen: 0,
294            currentline: -1,
295            linedefined: -1,
296            lastlinedefined: -1,
297            nups: 0,
298            nparams: 0,
299            isvararg: false,
300            istailcall: false,
301            ftransfer: 0,
302            ntransfer: 0,
303            short_src: [0u8; LUA_IDSIZE],
304            i_ci: None,
305        }
306    }
307}
308
309// ─── File-local helper: is this a Lua (non-C) closure? ───────────────────────
310
311// macros.tsv: LUA_VLCL → LuaClosure::Lua(_)
312#[inline]
313fn is_lua_closure(cl: Option<&LuaClosure>) -> bool {
314    matches!(cl, Some(LuaClosure::Lua(_)))
315}
316
317// ─── Current-PC helpers ───────────────────────────────────────────────────────
318
319/// Returns the program counter (0-based instruction index) for the current
320/// instruction in call frame `ci`.
321///
322/// ```c
323/// lua_assert(isLua(ci));
324/// return pcRel(ci->u.l.savedpc, ci_func(ci)->p);
325/// ```
326///
327/// PORT NOTE: In C, `savedpc` is a pointer to the *next* instruction. `pcRel`
328/// subtracts the code base and then subtracts 1 more to get the *current*
329/// instruction. In Rust, `saved_pc()` stores the 0-based index of the next
330/// instruction, so the current instruction index is `saved_pc() - 1`.
331fn current_pc(ci: &CallInfo) -> i32 {
332    debug_assert!(ci.is_lua());
333    // macros.tsv: pcRel → (pc - proto.code_base()) as i32 - 1
334    // In Rust savedpc is a u32 offset into code[]; current = savedpc - 1
335    ci.saved_pc().saturating_sub(1) as i32
336}
337
338// ─── Line-info lookup ─────────────────────────────────────────────────────────
339
340/// Finds the "base line" entry in `f.abslineinfo` for instruction `pc`.
341///
342/// Sets `*basepc` to the pc of the base entry (or -1 if starting from the
343/// function's first line), and returns the line number at that base.
344///
345fn get_baseline(f: &LuaProto, pc: i32, basepc: &mut i32) -> i32 {
346    if f.abslineinfo.is_empty() || pc < f.abslineinfo[0].pc {
347        *basepc = -1;
348        return f.linedefined;
349    }
350    // macros.tsv: cast_uint(x) → x as u32
351    let mut i = (pc as u32 / MAX_IWTH_ABS as u32).saturating_sub(1) as usize;
352    debug_assert!(
353        i < f.abslineinfo.len() && f.abslineinfo[i].pc <= pc,
354        "getbaseline: estimate is not a lower bound"
355    );
356    while i + 1 < f.abslineinfo.len() && pc >= f.abslineinfo[i + 1].pc {
357        i += 1;
358    }
359    *basepc = f.abslineinfo[i].pc;
360    f.abslineinfo[i].line
361}
362
363/// Returns the source line number corresponding to instruction `pc` in proto `f`.
364/// Returns -1 if the proto has no debug line information.
365///
366pub(crate) fn get_func_line(f: &LuaProto, pc: i32) -> i32 {
367    if f.lineinfo.is_empty() {
368        return -1;
369    }
370    let mut basepc: i32 = 0;
371    let mut baseline = get_baseline(f, pc, &mut basepc);
372    // PORT NOTE: C uses post-increment `basepc++` in the condition; the body
373    // then uses the already-incremented value. Rewritten as pre-increment.
374    while basepc < pc {
375        basepc += 1;
376        debug_assert!(
377            f.lineinfo[basepc as usize] != ABS_LINE_INFO,
378            "get_func_line: hit ABSLINEINFO in incremental walk"
379        );
380        baseline += f.lineinfo[basepc as usize] as i32;
381    }
382    baseline
383}
384
385/// Returns the source line for the current instruction in call frame `ci`.
386///
387fn get_current_line(ci: &CallInfo, state: &LuaState) -> i32 {
388    let proto = ci_lua_proto(ci, state);
389    get_func_line(&proto, current_pc(ci))
390}
391
392// ─── Hook support ─────────────────────────────────────────────────────────────
393
394/// Sets the `trap` flag on every active Lua call frame so that the VM checks
395/// debug hooks before each instruction.
396///
397///
398/// PORT NOTE: In C this walks an intrusive doubly-linked list. In Rust,
399/// `LuaState.call_stack` is a `Vec<CallInfo>`, so we iterate the slice.
400fn set_traps(state: &mut LuaState) {
401    //      if (isLua(ci)) ci->u.l.trap = 1;
402    // TODO(port): call_stack iteration API not yet finalised; this will change
403    // when LuaState.call_stack is fully implemented.
404    for ci in state.call_stack_mut().iter_mut() {
405        if ci.is_lua() {
406            ci.set_trap(true);
407        }
408    }
409}
410
411/// Installs a debug hook on thread `state`.
412///
413pub fn set_hook(
414    state: &mut LuaState,
415    func: Option<Box<dyn FnMut(&mut LuaState, &LuaDebug)>>,
416    mask: i32,
417    count: i32,
418) {
419    let (func, mask) = if func.is_none() || mask == 0 {
420        (None, 0i32)
421    } else {
422        (func, mask)
423    };
424    state.set_hook(func);
425    state.set_base_hook_count(count);
426    // macros.tsv: resethookcount → state.reset_hook_count()
427    state.reset_hook_count();
428    // macros.tsv: cast_byte(x) → x as u8
429    state.set_hook_mask(mask as u8);
430    if mask != 0 {
431        set_traps(state);
432    }
433}
434
435/// Returns the current debug hook function, if any.
436///
437///
438/// TODO(port): In C this returns a `lua_Hook` function pointer. In Rust the hook
439/// is a `Box<dyn FnMut>` and cannot be returned by raw reference without
440/// restructuring; for now returns a bool indicating whether a hook is installed.
441pub fn get_hook_installed(state: &LuaState) -> bool {
442    state.hook().is_some()
443}
444
445/// Returns the current hook event mask.
446///
447pub fn get_hook_mask(state: &LuaState) -> i32 {
448    state.hook_mask() as i32
449}
450
451/// Returns the current hook call count.
452///
453pub fn get_hook_count(state: &LuaState) -> i32 {
454    state.base_hook_count()
455}
456
457// ─── Stack introspection ──────────────────────────────────────────────────────
458
459/// Fills `ar` with information about the call frame at depth `level`.
460/// Level 0 is the current running function, level 1 is the caller, etc.
461/// Returns `true` on success, `false` if the level is out of range.
462///
463pub fn get_stack(state: &LuaState, level: i32, ar: &mut LuaDebug) -> bool {
464    if level < 0 {
465        return false;
466    }
467    let mut remaining = level;
468    let mut ci_idx = state.current_ci_idx();
469    loop {
470        if remaining == 0 {
471            break;
472        }
473        match state.prev_ci(ci_idx) {
474            Some(prev) => {
475                ci_idx = prev;
476                remaining -= 1;
477            }
478            None => {
479                return false;
480            }
481        }
482    }
483    if !state.is_base_ci(ci_idx) {
484        ar.i_ci = Some(ci_idx);
485        true
486    } else {
487        false
488    }
489}
490
491// ─── Upvalue and local variable name lookup ───────────────────────────────────
492
493/// Returns the name of upvalue `uv` in proto `p` (as a byte slice), or `b"?"`.
494///
495fn upval_name(p: &LuaProto, uv: usize) -> &[u8] {
496    //    if (s == NULL) return "?"; else return getstr(s);
497    // macros.tsv: check_exp(c, e) → { debug_assert!(c); e }
498    debug_assert!(uv < p.upvalues.len(), "upval_name: index out of range");
499    // TODO(port): UpvalDesc.name is GcRef<LuaString>; calling .as_bytes() requires
500    // access to the interned string's data. Actual lifetime is tied to the GcRef.
501    p.upvalues[uv].name.as_ref().map_or(b"?" as &[u8], |s| s.as_bytes())
502}
503
504/// Finds the stack slot for vararg value number `n` (n is negative) in `ci`.
505/// Returns `Some(pos)` and the name `b"(vararg)"` if found, else `None`.
506///
507///
508/// PORT NOTE: C sets `*pos` as an out-parameter. Rust returns an Option of the
509/// stack index alongside the name.
510fn find_vararg(state: &LuaState, ci: &CallInfo, n: i32) -> Option<(StackIdx, &'static [u8])> {
511    let proto = ci_lua_proto(ci, state);
512    if proto.is_vararg {
513        let nextra = ci.nextra_args();
514        if n >= -(nextra as i32) {
515            // PORT NOTE: pointer arithmetic converted to index arithmetic.
516            // ci->func.p is the function slot; varargs are at func - nextra - 1 .. func - 1
517            let pos = ci.func - (nextra + n + 1);
518            return Some((pos, b"(vararg)" as &[u8]));
519        }
520    }
521    None
522}
523
524/// Finds the name and stack position for local variable `n` in call frame `ci`.
525///
526/// - If `n > 0`, looks up as a numbered local (1-based).
527/// - If `n < 0`, looks up as a vararg slot.
528/// - Returns `None` if no such variable exists.
529/// - If `pos` is `Some`, sets it to the variable's stack index.
530///
531///
532/// PORT NOTE: returns an owned `Vec<u8>` rather than `&[u8]`. The Lua-function
533/// case must call `get_local_name`, which returns a slice borrowed from a
534/// `GcRef<LuaProto>` that drops at function end — there is no caller lifetime
535/// the slice could be tied to. Cloning the name is cheap (a handful of bytes).
536pub(crate) fn find_local(
537    state: &LuaState,
538    ci_idx: CallInfoIdx,
539    n: i32,
540    pos: Option<&mut StackIdx>,
541) -> Option<Vec<u8>> {
542    let ci = state.get_ci(ci_idx);
543    let base = ci.func + 1;
544    let mut name: Option<Vec<u8>> = None;
545
546    if ci.is_lua() {
547        if n < 0 {
548            if let Some((vpos, vname)) = find_vararg(state, ci, n) {
549                if let Some(out_pos) = pos {
550                    *out_pos = vpos;
551                }
552                return Some(vname.to_vec());
553            }
554            return None;
555        } else {
556            let proto = ci_lua_proto(ci, state);
557            let pc = current_pc(ci);
558            name = crate::func::get_local_name(&proto, n, pc).map(|s| s.to_vec());
559        }
560    }
561
562    if name.is_none() {
563        let limit: u32 = if ci_idx == state.current_ci_idx() {
564            state.top_idx().0
565        } else {
566            ci.next
567                .map(|next| state.get_ci(next).func.0)
568                .unwrap_or_else(|| state.top_idx().0)
569        };
570        if n > 0 && limit.saturating_sub(base.0) >= n as u32 {
571            name = Some(if ci.is_lua() { b"(temporary)".to_vec() } else { b"(C temporary)".to_vec() });
572        } else {
573            return None;
574        }
575    }
576
577    if let Some(out_pos) = pos {
578        *out_pos = base + (n - 1);
579    }
580    name
581}
582
583/// Gets the name and value of local variable `n` in call frame `ar->i_ci`
584/// (or in the function at the top of the stack if `ar` is NULL).
585/// Pushes the value on the stack and returns its name, or returns `None`.
586///
587pub fn get_local(state: &mut LuaState, ar: Option<&LuaDebug>, n: i32) -> Option<Vec<u8>> {
588    if ar.is_none() {
589        // macros.tsv: isLfunction → matches!(o, LuaValue::Function(LuaClosure::Lua(_)))
590        let top_val = state.peek_top();
591        if !matches!(top_val, LuaValue::Function(LuaClosure::Lua(_))) {
592            return None;
593        }
594        // PORT NOTE: reshaped for borrowck — convert to owned Vec<u8> inside the
595        // block so `cl` (and the borrow through it) drop before we return.
596        let name_owned: Option<Vec<u8>> = {
597            let cl = match top_val {
598                LuaValue::Function(LuaClosure::Lua(ref cl)) => cl.clone(),
599                _ => unreachable!(),
600            };
601            // TODO(port): access proto from LuaClosureLua GcRef
602            get_local_name_from_closure(&cl, n, 0).map(|s| s.to_vec())
603        };
604        return name_owned;
605    }
606
607    let ar = ar.unwrap();
608    let ci_idx = ar.i_ci?;
609    let mut pos = StackIdx(0);
610    // PORT NOTE: reshaped for borrowck — clone name to an owned Vec<u8> so the
611    // immutable borrow of `state` ends before the mutable push below.
612    let name_owned: Option<Vec<u8>> = find_local(state, ci_idx, n, Some(&mut pos));
613
614    if name_owned.is_some() {
615        let val = state.get_at(pos).clone();
616        state.push(val);
617    }
618    name_owned
619}
620
621/// Sets local variable `n` in call frame `ar->i_ci` to the value on top of the
622/// stack. Pops the value and returns the variable name, or returns `None`.
623///
624pub fn set_local(state: &mut LuaState, ar: &LuaDebug, n: i32) -> Option<Vec<u8>> {
625    let ci_idx = ar.i_ci?;
626    let mut pos = StackIdx(0);
627    // PORT NOTE: reshaped for borrowck — clone name before mutably borrowing state.
628    let name_owned: Option<Vec<u8>> = find_local(state, ci_idx, n, Some(&mut pos));
629    if name_owned.is_some() {
630        let val = state.get_at(state.top_idx() - 1).clone();
631        state.set_at(pos, val);
632        state.pop_n(1);
633    }
634    name_owned
635}
636
637// ─── Function info helpers ────────────────────────────────────────────────────
638
639/// Fills the source/line fields of `ar` from closure `cl`.
640///
641fn func_info(ar: &mut LuaDebug, cl: Option<&LuaClosure>) {
642    if !is_lua_closure(cl) {
643        // macros.tsv: LL(x) → literal.len()
644        ar.source = Some(b"=[C]".to_vec());
645        ar.srclen = b"=[C]".len();
646        ar.linedefined = -1;
647        ar.lastlinedefined = -1;
648        ar.what = Some(b"C");
649    } else {
650        let lua_cl = match cl {
651            Some(LuaClosure::Lua(cl)) => cl,
652            _ => unreachable!(),
653        };
654        // TODO(port): access proto via GcRef<LuaProto>
655        let proto: &LuaProto = &lua_cl.proto;
656        // renders as "?". Stripped binary chunks commonly have no source.
657        if let Some(src) = proto.source_string() {
658            ar.source = Some(src.as_bytes().to_vec());
659            ar.srclen = src.as_bytes().len();
660        } else {
661            ar.source = Some(b"=?".to_vec());
662            ar.srclen = b"=?".len();
663        }
664        ar.linedefined = proto.linedefined;
665        ar.lastlinedefined = proto.lastlinedefined;
666        ar.what = Some(if ar.linedefined == 0 { b"main" } else { b"Lua" });
667    }
668    // TODO(port): luaO_chunkid lives in crate::object; call it once available
669    chunk_id(&mut ar.short_src, ar.source.as_deref().unwrap_or(b"?"), ar.srclen);
670}
671
672/// Returns the line number after advancing by one instruction from `currentline`.
673/// Handles the ABSLINEINFO sentinel by falling through to `get_func_line`.
674///
675fn next_line(p: &LuaProto, currentline: i32, pc: usize) -> i32 {
676    //    else return luaG_getfuncline(p, pc);
677    if p.lineinfo.get(pc).copied() != Some(ABS_LINE_INFO) {
678        currentline + p.lineinfo[pc] as i32
679    } else {
680        get_func_line(p, pc as i32)
681    }
682}
683
684/// Collects all source lines that are covered by instructions in closure `f`
685/// into a new table and pushes it on the stack (or pushes `nil` for C functions).
686///
687fn collect_valid_lines(state: &mut LuaState, cl: Option<&LuaClosure>) -> Result<(), LuaError> {
688    if !is_lua_closure(cl) {
689        // macros.tsv: setnilvalue → *o = LuaValue::Nil; api_incr_top → gone
690        state.push(LuaValue::Nil);
691        return Ok(());
692    }
693    let lua_cl = match cl {
694        Some(LuaClosure::Lua(cl)) => cl.clone(),
695        _ => unreachable!(),
696    };
697    // TODO(port): access proto via GcRef<LuaProto>
698    let proto: GcRef<LuaProto> = lua_cl.proto.clone();
699    let p: &LuaProto = &proto;
700
701    let mut currentline = p.linedefined;
702
703    // macros.tsv: luaH_new(L) → state.new_table()
704    let t = state.new_table();
705    // macros.tsv: sethvalue2s → state.set_at(o, LuaValue::Table(t.clone()))
706    state.push(LuaValue::Table(t.clone()));
707
708    if !p.lineinfo.is_empty() {
709        // macros.tsv: setbtvalue → *o = LuaValue::Bool(true)
710        let v = LuaValue::Bool(true);
711
712        let start_i = if !p.is_vararg {
713            0usize
714        } else {
715            // TODO(port): verify opcode — GET_OPCODE lives in lua_code crate
716            debug_assert!(
717                p.code.first().map(|i| i.is_vararg_prep()).unwrap_or(false),
718                "collect_valid_lines: first instruction of vararg should be OP_VARARGPREP"
719            );
720            currentline = next_line(p, currentline, 0);
721            1usize
722        };
723
724        // PORT NOTE: C iterates up to sizelineinfo (same as lineinfo.len() in Rust).
725        for i in start_i..p.lineinfo.len() {
726            currentline = next_line(p, currentline, i);
727            // TODO(port): luaH_setint lives in crate::table; stub call here
728            t.raw_set_int(state, currentline as i64, v.clone())?;
729        }
730    }
731    Ok(())
732}
733
734// ─── Function naming (symbolic execution) ────────────────────────────────────
735
736/// Tries to find a name for the function being called, based on the calling
737/// call frame `ci`. Returns `None` if the frame is tail-called or unavailable.
738///
739fn get_func_name<'a>(
740    state: &'a LuaState,
741    ci: Option<&CallInfo>,
742    name: &mut Option<Vec<u8>>,
743) -> Option<&'static [u8]> {
744    //      return funcnamefromcall(L, ci->previous, name);
745    //    else return NULL;
746    let ci = ci?;
747    if ci.callstatus & CIST_TAIL != 0 {
748        return None;
749    }
750    // TODO(port): ci->previous requires navigating call_stack by prev idx
751    // TODO(phase-b): get_prev_ci needs to accept &CallInfo or take the previous idx.
752    let prev_idx = ci.previous?;
753    let prev_ci = state.get_ci(prev_idx).clone();
754    funcname_from_call(state, &prev_ci, name)
755}
756
757/// Fills `ar` with the requested debug information about closure `f` / frame `ci`.
758///
759fn aux_get_info(
760    state: &LuaState,
761    what: &[u8],
762    ar: &mut LuaDebug,
763    cl: Option<&LuaClosure>,
764    ci: Option<&CallInfo>,
765) -> bool {
766    let mut status = true;
767    for &ch in what {
768        match ch {
769            b'S' => {
770                func_info(ar, cl);
771            }
772            b'l' => {
773                ar.currentline = match ci {
774                    Some(ci) if ci.is_lua() => get_current_line(ci, state),
775                    _ => -1,
776                };
777            }
778            b'u' => {
779                ar.nups = cl.map_or(0, |c| c.nupvalues() as u8);
780                match cl {
781                    Some(LuaClosure::Lua(lua_cl)) => {
782                        // TODO(port): access proto via GcRef<LuaProto>
783                        ar.isvararg = lua_cl.proto.is_vararg;
784                        ar.nparams = lua_cl.proto.numparams;
785                    }
786                    _ => {
787                        ar.isvararg = true;
788                        ar.nparams = 0;
789                    }
790                }
791            }
792            b't' => {
793                ar.istailcall = ci.map_or(false, |ci| ci.callstatus & CIST_TAIL != 0);
794            }
795            b'n' => {
796                let mut name: Option<Vec<u8>> = None;
797                ar.namewhat = get_func_name(state, ci, &mut name);
798                if ar.namewhat.is_none() {
799                    ar.namewhat = Some(b"");
800                    ar.name = None;
801                } else {
802                    ar.name = name;
803                }
804            }
805            //              else { ftransfer = ...; ntransfer = ...; }
806            b'r' => match ci {
807                Some(ci) if ci.callstatus & CIST_TRAN != 0 => {
808                    // TODO(port): ci->u2.transferinfo.ftransfer / ntransfer
809                    ar.ftransfer = ci.transfer_ftransfer();
810                    ar.ntransfer = ci.transfer_ntransfer();
811                }
812                _ => {
813                    ar.ftransfer = 0;
814                    ar.ntransfer = 0;
815                }
816            },
817            b'L' | b'f' => {}
818            _ => {
819                status = false;
820            }
821        }
822    }
823    status
824}
825
826/// Returns debug information about a function or active call frame.
827///
828pub fn get_info(state: &mut LuaState, what: &[u8], ar: &mut LuaDebug) -> bool {
829    let (cl, ci_idx, func_val, what) = if what.first() == Some(&b'>') {
830        let func_val = state.peek_at(state.top_idx() - 1).clone();
831        state.pop_n(1);
832        debug_assert!(
833            matches!(func_val, LuaValue::Function(_)),
834            "get_info: function expected"
835        );
836        let cl = match &func_val {
837            LuaValue::Function(LuaClosure::Lua(_) | LuaClosure::C(_)) => {
838                Some(match &func_val {
839                    LuaValue::Function(c) => c.clone(),
840                    _ => unreachable!(),
841                })
842            }
843            _ => None,
844        };
845        (cl, None, func_val, &what[1..])
846    } else {
847        let ci_idx = match ar.i_ci { Some(i) => i, None => return false };
848        let func_val = state.get_at(state.get_ci(ci_idx).func).clone();
849        debug_assert!(
850            matches!(func_val, LuaValue::Function(_)),
851            "get_info: non-function at ci->func"
852        );
853        let cl = match &func_val {
854            LuaValue::Function(LuaClosure::Lua(_) | LuaClosure::C(_)) => {
855                Some(match &func_val {
856                    LuaValue::Function(c) => c.clone(),
857                    _ => unreachable!(),
858                })
859            }
860            _ => None,
861        };
862        (cl, Some(ci_idx), func_val, what)
863    };
864
865    let ci = ci_idx.and_then(|idx| Some(state.get_ci(idx).clone()));
866    let status = aux_get_info(state, what, ar, cl.as_ref(), ci.as_ref());
867
868    if what.contains(&b'f') {
869        state.push(func_val);
870    }
871    if what.contains(&b'L') {
872        // TODO(port): propagate error from collect_valid_lines
873        let _ = collect_valid_lines(state, cl.as_ref());
874    }
875    status
876}
877
878// ─── Symbolic execution — finding which instruction set a register ────────────
879
880/// Filters a pc: if `pc` is inside a conditional branch (before `jmptarget`),
881/// returns -1 (unknown); otherwise returns `pc`.
882///
883#[inline]
884fn filter_pc(pc: i32, jmptarget: i32) -> i32 {
885    if pc < jmptarget { -1 } else { pc }
886}
887
888/// Finds the last instruction before `lastpc` that wrote to register `reg`.
889/// Returns the pc of that instruction, or -1 if not found.
890///
891fn find_set_reg(p: &LuaProto, lastpc: i32, reg: i32) -> i32 {
892    let mut setreg: i32 = -1;
893    let mut jmptarget: i32 = 0;
894
895    // macros.tsv: testMMMode(op) → (luaP_opmodes[op as usize] & (1 << 7)) != 0
896    // TODO(port): GET_OPCODE and opmode tests live in lua_code crate
897    let effective_lastpc = if p.code.get(lastpc as usize).map_or(false, |i| i.is_mm_mode()) {
898        lastpc - 1
899    } else {
900        lastpc
901    };
902
903    for pc in 0..effective_lastpc {
904        let instr = p.code[pc as usize];
905        let op = instr.opcode();
906        let a = instr.arg_a() as i32;
907
908        let change = match op {
909            OpCode::LoadNil => {
910                let b = instr.arg_b() as i32;
911                a <= reg && reg <= a + b
912            }
913            OpCode::TForCall => reg >= a + 2,
914            OpCode::Call | OpCode::TailCall => reg >= a,
915            OpCode::Jmp => {
916                let b = instr.arg_s_j();
917                let dest = pc + 1 + b;
918                if dest <= effective_lastpc && dest > jmptarget {
919                    jmptarget = dest;
920                }
921                false
922            }
923            _ => {
924                // macros.tsv: testAMode(op) → (luaP_opmodes[op as usize] & (1 << 3)) != 0
925                // TODO(port): opmode table lives in lua_code crate
926                instr.test_a_mode() && reg == a
927            }
928        };
929
930        if change {
931            setreg = filter_pc(pc, jmptarget);
932        }
933    }
934    setreg
935}
936
937/// Finds a "name" for the constant at `index` in proto `p`.
938/// Returns `Some("constant")` and sets `*name` to the string content,
939/// or returns `None` and sets `*name` to `"?"`.
940///
941fn kname<'a>(p: &'a LuaProto, index: usize, name: &mut &'a [u8]) -> Option<&'static [u8]> {
942    //    if (ttisstring(kvalue)) { *name = getstr(tsvalue(kvalue)); return "constant"; }
943    //    else { *name = "?"; return NULL; }
944    match p.k.get(index) {
945        Some(LuaValue::Str(s)) => {
946            // TODO(port): as_bytes() lifetime is tied to GcRef; revisit in Phase B
947            *name = s.as_bytes();
948            Some(b"constant")
949        }
950        _ => {
951            *name = b"?";
952            None
953        }
954    }
955}
956
957/// Tries to find a basic name for register `reg` in proto `p` at instruction `ppc`.
958/// Returns the "kind" of the name (e.g. "local", "upvalue", "constant"), or `None`.
959///
960fn basic_get_obj_name<'a>(
961    p: &'a LuaProto,
962    ppc: &mut i32,
963    reg: i32,
964    name: &mut &'a [u8],
965) -> Option<&'static [u8]> {
966    let pc = *ppc;
967    //    if (*name) return "local";
968    if let Some(local_name) = get_local_name(p, reg + 1, pc) {
969        *name = local_name;
970        return Some(b"local");
971    }
972
973    *ppc = find_set_reg(p, pc, reg);
974    let pc = *ppc;
975
976    if pc == -1 {
977        return None;
978    }
979
980    let instr = p.code[pc as usize];
981    let op = instr.opcode();
982    match op {
983        OpCode::Move => {
984            let b = instr.arg_b() as i32;
985            if b < instr.arg_a() as i32 {
986                return basic_get_obj_name(p, ppc, b, name);
987            }
988        }
989        OpCode::GetUpVal => {
990            *name = upval_name(p, instr.arg_b() as usize);
991            return Some(b"upvalue");
992        }
993        OpCode::LoadK => {
994            return kname(p, instr.arg_bx() as usize, name);
995        }
996        OpCode::LoadKx => {
997            let next = p.code[(pc + 1) as usize];
998            return kname(p, next.arg_ax() as usize, name);
999        }
1000        _ => {}
1001    }
1002    None
1003}
1004
1005/// Finds a name for a register-or-K instruction's `C` field (the key side).
1006/// Stores a "constant name" if possible, otherwise `"?"`.
1007///
1008fn rname<'a>(p: &'a LuaProto, pc: i32, c: i32, name: &mut &'a [u8]) {
1009    let mut pc = pc;
1010    //    if (!(what && *what == 'c')) *name = "?";
1011    let what = basic_get_obj_name(p, &mut pc, c, name);
1012    if !matches!(what, Some(kind) if kind.first() == Some(&b'c')) {
1013        *name = b"?";
1014    }
1015}
1016
1017/// Finds the name for an RK-encoded `C` operand (either a constant or a register).
1018///
1019fn rkname<'a>(p: &'a LuaProto, pc: i32, instr: Instruction, name: &mut &'a [u8]) {
1020    let c = instr.arg_c() as i32;
1021    // macros.tsv: GETARG_k → i.arg_k() -> u32
1022    if instr.arg_k() != 0 {
1023        kname(p, c as usize, name);
1024    } else {
1025        rname(p, pc, c, name);
1026    }
1027}
1028
1029/// Determines whether the table indexed by instruction `i` is `_ENV`.
1030/// Returns `"global"` if so, `"field"` otherwise.
1031///
1032fn is_env<'a>(p: &'a LuaProto, pc: i32, instr: Instruction, isup: bool) -> &'static [u8] {
1033    let t = instr.arg_b() as usize;
1034    let mut name: &[u8] = b"?";
1035    if isup {
1036        name = upval_name(p, t);
1037    } else {
1038        let mut pc = pc;
1039        basic_get_obj_name(p, &mut pc, t as i32, &mut name);
1040    }
1041    if name == LUA_ENV { b"global" } else { b"field" }
1042}
1043
1044/// Extended version of `basic_get_obj_name` that also handles table accesses.
1045/// Returns the "kind" of name, or `None`.
1046///
1047fn get_obj_name<'a>(
1048    p: &'a LuaProto,
1049    lastpc: i32,
1050    reg: i32,
1051    name: &mut &'a [u8],
1052) -> Option<&'static [u8]> {
1053    let mut lastpc = lastpc;
1054    let kind = basic_get_obj_name(p, &mut lastpc, reg, name);
1055    if kind.is_some() {
1056        return kind;
1057    }
1058
1059    if lastpc == -1 {
1060        return None;
1061    }
1062
1063    let instr = p.code[lastpc as usize];
1064    let op = instr.opcode();
1065    match op {
1066        OpCode::GetTabUp => {
1067            let k = instr.arg_c() as usize;
1068            kname(p, k, name);
1069            Some(is_env(p, lastpc, instr, true))
1070        }
1071        OpCode::GetTable => {
1072            let k = instr.arg_c() as i32;
1073            rname(p, lastpc, k, name);
1074            Some(is_env(p, lastpc, instr, false))
1075        }
1076        OpCode::GetI => {
1077            *name = b"integer index";
1078            Some(b"field")
1079        }
1080        OpCode::GetField => {
1081            let k = instr.arg_c() as usize;
1082            kname(p, k, name);
1083            Some(is_env(p, lastpc, instr, false))
1084        }
1085        OpCode::Self_ => {
1086            rkname(p, lastpc, instr, name);
1087            Some(b"method")
1088        }
1089        _ => None,
1090    }
1091}
1092
1093// ─── Function naming ──────────────────────────────────────────────────────────
1094
1095/// Tries to derive a name for a function from the bytecode instruction that
1096/// called it. Returns the "kind" of call (e.g. "for iterator", "metamethod"),
1097/// or `None`.
1098///
1099fn funcname_from_code<'a>(
1100    state: &LuaState,
1101    p: &'a LuaProto,
1102    pc: i32,
1103    name: &mut Option<Vec<u8>>,
1104) -> Option<&'static [u8]> {
1105    let instr = p.code[pc as usize];
1106    let op = instr.opcode();
1107
1108    match op {
1109        OpCode::Call | OpCode::TailCall => {
1110            let mut name_bytes: &[u8] = b"?";
1111            let kind = get_obj_name(p, pc, instr.arg_a() as i32, &mut name_bytes);
1112            *name = Some(name_bytes.to_vec());
1113            kind
1114        }
1115        OpCode::TForCall => {
1116            *name = Some(b"for iterator".to_vec());
1117            Some(b"for iterator")
1118        }
1119        // Metamethod dispatch cases — look up tm name from GlobalState
1120        OpCode::Self_ | OpCode::GetTabUp | OpCode::GetTable | OpCode::GetI | OpCode::GetField => {
1121            get_tm_name(state, TagMethod::Index, name)
1122        }
1123        OpCode::SetTabUp | OpCode::SetTable | OpCode::SetI | OpCode::SetField => {
1124            get_tm_name(state, TagMethod::NewIndex, name)
1125        }
1126        OpCode::MmBin | OpCode::MmBinI | OpCode::MmBinK => {
1127            // macros.tsv: cast(TMS, x) → x as TagMethod
1128            // TODO(port): TagMethod::from_u8 needs to exist
1129            let tm_idx = instr.arg_c() as u8;
1130            let tm = TagMethod::from_u8(tm_idx);
1131            get_tm_name(state, tm, name)
1132        }
1133        OpCode::Unm => get_tm_name(state, TagMethod::Unm, name),
1134        OpCode::BNot => get_tm_name(state, TagMethod::BNot, name),
1135        OpCode::Len => get_tm_name(state, TagMethod::Len, name),
1136        OpCode::Concat => get_tm_name(state, TagMethod::Concat, name),
1137        OpCode::Eq => get_tm_name(state, TagMethod::Eq, name),
1138        OpCode::Lt | OpCode::LtI | OpCode::GtI => get_tm_name(state, TagMethod::Lt, name),
1139        OpCode::Le | OpCode::LeI | OpCode::GeI => get_tm_name(state, TagMethod::Le, name),
1140        OpCode::Close | OpCode::Return => get_tm_name(state, TagMethod::Close, name),
1141        _ => None,
1142    }
1143}
1144
1145/// Looks up the name for tag method `tm` from GlobalState and stores it in `*name`.
1146/// Returns `Some("metamethod")`.
1147///
1148/// PORT NOTE: `+2` skips the leading `__` prefix in C; here we strip it from
1149/// the byte slice.
1150fn get_tm_name(
1151    state: &LuaState,
1152    tm: TagMethod,
1153    name: &mut Option<Vec<u8>>,
1154) -> Option<&'static [u8]> {
1155    // macros.tsv: getshrstr(ts) → ts.as_bytes(); G → state.global()
1156    // PORT NOTE: reshaped for borrowck — tm_name returns Option<GcRef<LuaString>>;
1157    // materialise the bytes before stripping so there is no borrow of a temporary.
1158    let raw_bytes: Vec<u8> = state.global()
1159        .tm_name(tm)
1160        .map(|s| s.as_bytes().to_vec())
1161        .unwrap_or_default();
1162    let stripped = raw_bytes
1163        .strip_prefix(b"__")
1164        .unwrap_or(&raw_bytes)
1165        .to_vec();
1166    *name = Some(stripped);
1167    Some(b"metamethod")
1168}
1169
1170/// Tries to derive a name for a function from how it was called (`ci`).
1171///
1172fn funcname_from_call<'a>(
1173    state: &'a LuaState,
1174    ci: &CallInfo,
1175    name: &mut Option<Vec<u8>>,
1176) -> Option<&'static [u8]> {
1177    if ci.callstatus & CIST_HOOKED != 0 {
1178        *name = Some(b"?".to_vec());
1179        return Some(b"hook");
1180    }
1181    if ci.callstatus & CIST_FIN != 0 {
1182        *name = Some(b"__gc".to_vec());
1183        return Some(b"metamethod");
1184    }
1185    if ci.is_lua() {
1186        let proto = ci_lua_proto(ci, state);
1187        return funcname_from_code(state, &proto, current_pc(ci), name);
1188    }
1189    None
1190}
1191
1192// ─── Pointer-to-value tracking (varinfo for error messages) ──────────────────
1193
1194/// Checks whether value at stack index `val_idx` is in the call frame `ci`'s
1195/// register window, and if so returns the register index (0-based).
1196/// Returns -1 if not found.
1197///
1198///
1199/// PORT NOTE: In C this compares raw pointers. In Rust we compare StackIdx
1200/// values. The function signature changes: instead of a `*o` pointer we take
1201/// the StackIdx of the value directly.
1202fn in_stack(ci: &CallInfo, val_idx: StackIdx, state: &LuaState) -> i32 {
1203    let base = StackIdx(ci.func.0 + 1);
1204    // TODO(port): in C this is a pointer-identity check (`o == s2v(base+pos)`).
1205    // In Rust, `val_idx` IS a StackIdx; we just check whether it falls in range.
1206    let ci_top = ci.top;
1207    let mut pos = 0i32;
1208    let mut cur = base;
1209    while cur.0 < ci_top.0 {
1210        if cur == val_idx {
1211            return pos;
1212        }
1213        cur = StackIdx(cur.0 + 1);
1214        pos += 1;
1215    }
1216    -1
1217}
1218
1219/// Checks whether `val_idx` is the current value of one of the upvalues in the
1220/// Lua closure at `ci`. If so, sets `*name` and returns `Some("upvalue")`.
1221///
1222///
1223/// PORT NOTE: In C this compares `c->upvals[i]->v.p == o` (pointer identity on
1224/// open upvalues or the closed slot). In Rust, open upvalues hold a StackIdx; we
1225/// compare that against `val_idx`. Closed upvalues cannot be identified by stack
1226/// position, so they are not matched here.
1227fn get_upval_name<'a>(
1228    ci: &CallInfo,
1229    val_idx: StackIdx,
1230    name: &mut &'a [u8],
1231    state: &'a LuaState,
1232) -> Option<&'static [u8]> {
1233    let proto = ci_lua_proto(ci, state);
1234    // TODO(port): actual upvalue objects require ci.lua_closure() on the LuaState;
1235    // this is a best-effort translation
1236    let lua_cl = match state.get_at(ci.func) {
1237        LuaValue::Function(LuaClosure::Lua(cl)) => cl.clone(),
1238        _ => return None,
1239    };
1240    for (i, upval_slot) in lua_cl.upvals.iter().enumerate() {
1241        let upval = upval_slot.get();
1242        let state = upval.slot().clone();
1243        if let lua_types::UpValState::Open { idx, .. } = state {
1244            if idx == val_idx {
1245                // TODO(phase-b): the name needs to be tied to state's lifetime; using
1246                // a static fallback keeps the trait bounds satisfied for now.
1247                let _ = upval_name(&proto, i);
1248                *name = b"upvalue";
1249                return Some(b"upvalue");
1250            }
1251        }
1252    }
1253    None
1254}
1255
1256/// Builds a human-readable "variable info" string like ` (local 'x')` or
1257/// ` (upvalue 'y')` to append to error messages. Returns an empty `Vec<u8>`
1258/// if no information is available.
1259///
1260fn format_var_info(kind: Option<&[u8]>, name: Option<&[u8]>) -> Vec<u8> {
1261    match (kind, name) {
1262        (Some(k), Some(n)) => {
1263            let mut out = Vec::with_capacity(4 + k.len() + n.len());
1264            out.extend_from_slice(b" (");
1265            out.extend_from_slice(k);
1266            out.extend_from_slice(b" '");
1267            out.extend_from_slice(n);
1268            out.extend_from_slice(b"')");
1269            out
1270        }
1271        _ => Vec::new(),
1272    }
1273}
1274
1275/// Returns a description string for the value at `val_idx` in the current call
1276/// frame, e.g. `" (local 'x')"` or `" (upvalue 'y')"`. Used in error messages.
1277///
1278fn var_info(state: &LuaState, val_idx: StackIdx) -> Vec<u8> {
1279    let ci_idx = state.current_ci_idx();
1280    let ci = state.get_ci(ci_idx).clone();
1281    let mut kind: Option<&[u8]> = None;
1282    let mut name_owned: Vec<u8> = b"?".to_vec();
1283
1284    if ci.is_lua() {
1285        let mut up_name: &[u8] = b"?";
1286        kind = get_upval_name(&ci, val_idx, &mut up_name, state);
1287        if kind.is_some() {
1288            name_owned = up_name.to_vec();
1289        } else {
1290            let reg = in_stack(&ci, val_idx, state);
1291            if reg >= 0 {
1292                let proto = ci_lua_proto(&ci, state);
1293                let mut nref: &[u8] = b"?";
1294                let pc = current_pc(&ci);
1295                let k = get_obj_name(&proto, pc, reg, &mut nref);
1296                kind = k;
1297                if kind.is_some() {
1298                    name_owned = nref.to_vec();
1299                }
1300            }
1301        }
1302    }
1303    format_var_info(kind, if kind.is_some() { Some(&name_owned) } else { None })
1304}
1305
1306// ─── Error-raising functions ──────────────────────────────────────────────────
1307
1308/// Internal helper: raises a type error with the given `extra` info string.
1309///
1310fn typeerror_inner(
1311    state: &LuaState,
1312    val: &LuaValue,
1313    op: &[u8],
1314    extra: &[u8],
1315) -> LuaError {
1316    let t = state.obj_type_name(val);
1317    let mut msg = Vec::new();
1318    msg.extend_from_slice(b"attempt to ");
1319    msg.extend_from_slice(op);
1320    msg.extend_from_slice(b" a ");
1321    msg.extend_from_slice(&t);
1322    msg.extend_from_slice(b" value");
1323    msg.extend_from_slice(extra);
1324    prefixed_runtime(state, msg)
1325}
1326
1327/// Raises a type error for performing operation `op` on value `val`.
1328/// Includes variable-info context (e.g. "local 'x'") if available.
1329///
1330pub(crate) fn type_error(state: &LuaState, val: &LuaValue, val_idx: StackIdx, op: &[u8]) -> LuaError {
1331    let extra = var_info(state, val_idx);
1332    typeerror_inner(state, val, op, &extra)
1333}
1334
1335/// Variant of `type_error` for bytecode paths where the target isn't on the
1336/// active stack — OP_SETTABUP / OP_GETTABUP read directly from the closure's
1337/// upvalue cells, so `var_info`'s in-stack heuristic can't recover the name.
1338/// The caller passes a pre-formatted `(kind, name)` pair (e.g.
1339/// `(b"upvalue", b"a")`) used verbatim in the trailing `(kind 'name')`.
1340pub(crate) fn type_error_with_hint(
1341    state: &LuaState,
1342    val: &LuaValue,
1343    op: &[u8],
1344    kind: &[u8],
1345    name: &[u8],
1346) -> LuaError {
1347    let extra = format_var_info(Some(kind), Some(name));
1348    let t = obj_type_name_static(val);
1349    let mut msg = Vec::new();
1350    msg.extend_from_slice(b"attempt to ");
1351    msg.extend_from_slice(op);
1352    msg.extend_from_slice(b" a ");
1353    msg.extend_from_slice(t);
1354    msg.extend_from_slice(b" value");
1355    msg.extend_from_slice(&extra);
1356    prefixed_runtime(state, msg)
1357}
1358
1359/// Standalone type-name accessor that does not require `&LuaState`. Used by
1360/// `type_error_with_hint` since callers there cannot easily thread `state`.
1361fn obj_type_name_static(val: &LuaValue) -> &'static [u8] {
1362    match val {
1363        LuaValue::Nil => b"nil",
1364        LuaValue::Bool(_) => b"boolean",
1365        LuaValue::Int(_) | LuaValue::Float(_) => b"number",
1366        LuaValue::Str(_) => b"string",
1367        LuaValue::Table(_) => b"table",
1368        LuaValue::Function(_) => b"function",
1369        LuaValue::UserData(_) => b"userdata",
1370        LuaValue::LightUserData(_) => b"light userdata",
1371        LuaValue::Thread(_) => b"thread",
1372    }
1373}
1374
1375/// Raises a "call" type error for a non-callable `val`.
1376/// Prefers name from `funcnamefromcall`; falls back to `varinfo`.
1377///
1378pub(crate) fn call_error(state: &LuaState, val: &LuaValue, val_idx: StackIdx) -> LuaError {
1379    let ci_idx = state.current_ci_idx();
1380    let ci = state.get_ci(ci_idx).clone();
1381    let mut name: Option<Vec<u8>> = None;
1382    let kind = funcname_from_call(state, &ci, &mut name);
1383    let extra = if kind.is_some() {
1384        format_var_info(kind, name.as_deref())
1385    } else {
1386        var_info(state, val_idx)
1387    };
1388    typeerror_inner(state, val, b"call", &extra)
1389}
1390
1391/// Raises a "bad 'for' <what>" error.
1392///
1393pub(crate) fn for_error(state: &mut LuaState, val: &LuaValue, what: &[u8]) -> LuaError {
1394    let t = crate::tagmethods::obj_type_name(state, val)
1395        .unwrap_or_else(|_| crate::tagmethods::type_name(val.base_type()).to_vec());
1396    let mut msg = Vec::new();
1397    msg.extend_from_slice(b"bad 'for' ");
1398    msg.extend_from_slice(what);
1399    msg.extend_from_slice(b" (number expected, got ");
1400    msg.extend_from_slice(&t);
1401    msg.push(b')');
1402    prefixed_runtime(state, msg)
1403}
1404
1405/// Raises a concatenation type error for the first non-coercible operand.
1406///
1407pub(crate) fn concat_error(
1408    state: &LuaState,
1409    p1: &LuaValue,
1410    p1_idx: StackIdx,
1411    p2: &LuaValue,
1412    p2_idx: StackIdx,
1413) -> LuaError {
1414    // macros.tsv: ttisstring → matches!(o, LuaValue::Str(_))
1415    // macros.tsv: cvt2str → matches!(o, LuaValue::Int(_) | LuaValue::Float(_))
1416    let (bad_val, bad_idx) = if matches!(p1, LuaValue::Str(_) | LuaValue::Int(_) | LuaValue::Float(_)) {
1417        (p2, p2_idx)
1418    } else {
1419        (p1, p1_idx)
1420    };
1421    type_error(state, bad_val, bad_idx, b"concatenate")
1422}
1423
1424/// Raises an arithmetic type error. If `p1` is not a number, blames `p1`;
1425/// otherwise blames `p2`.
1426///
1427pub(crate) fn op_int_error(
1428    state: &LuaState,
1429    p1: &LuaValue,
1430    p1_idx: StackIdx,
1431    p2: &LuaValue,
1432    p2_idx: StackIdx,
1433    msg: &[u8],
1434) -> LuaError {
1435    // macros.tsv: ttisnumber → matches!(o, LuaValue::Int(_) | LuaValue::Float(_))
1436    let (bad_val, bad_idx) = if !matches!(p1, LuaValue::Int(_) | LuaValue::Float(_)) {
1437        (p1, p1_idx)
1438    } else {
1439        (p2, p2_idx)
1440    };
1441    type_error(state, bad_val, bad_idx, msg)
1442}
1443
1444/// Raises an "no integer representation" error for float→int conversion failure.
1445///
1446///
1447/// Stack indices are optional: when an operand is from a constant table or
1448/// an immediate, no register backs it and `var_info` has nothing to report.
1449pub(crate) fn to_int_error(
1450    state: &LuaState,
1451    p1: &LuaValue,
1452    p1_idx: Option<StackIdx>,
1453    _p2: &LuaValue,
1454    p2_idx: Option<StackIdx>,
1455) -> LuaError {
1456    let bad_idx = if p1.to_integer_no_strconv().is_none() {
1457        p1_idx
1458    } else {
1459        p2_idx
1460    };
1461    let extra = match bad_idx {
1462        Some(idx) => var_info(state, idx),
1463        None => Vec::new(),
1464    };
1465    let mut msg = Vec::new();
1466    msg.extend_from_slice(b"number");
1467    msg.extend_from_slice(&extra);
1468    msg.extend_from_slice(b" has no integer representation");
1469    prefixed_runtime(state, msg)
1470}
1471
1472/// Raises an order-comparison type error for incompatible types.
1473///
1474pub(crate) fn order_error(state: &LuaState, p1: &LuaValue, p2: &LuaValue) -> LuaError {
1475    // TODO(port): obj_type_name lives in crate::tagmethods
1476    let t1 = state.obj_type_name(p1);
1477    let t2 = state.obj_type_name(p2);
1478    //    else                      luaG_runerror(L, "attempt to compare %s with %s", t1, t2);
1479    let msg = if t1 == t2 {
1480        let mut m = Vec::new();
1481        m.extend_from_slice(b"attempt to compare two ");
1482        m.extend_from_slice(&t1);
1483        m.extend_from_slice(b" values");
1484        m
1485    } else {
1486        let mut m = Vec::new();
1487        m.extend_from_slice(b"attempt to compare ");
1488        m.extend_from_slice(&t1);
1489        m.extend_from_slice(b" with ");
1490        m.extend_from_slice(&t2);
1491        m
1492    };
1493    prefixed_runtime(state, msg)
1494}
1495
1496/// Prepends `src:line: ` to `msg` (as a new Lua string on the stack) and
1497/// returns the formatted string.
1498///
1499///
1500/// The C signature takes `lua_State *L` because the result is pushed onto the
1501/// Lua stack via `luaO_pushfstring`. Our port returns `Vec<u8>` instead, so
1502/// the state parameter is unused — keep an optional reference for callers
1503/// that still pass one, but the function works without it.
1504pub(crate) fn add_info(
1505    _state: Option<&mut LuaState>,
1506    msg: &[u8],
1507    src: Option<&LuaString>,
1508    line: i32,
1509) -> Vec<u8> {
1510    //    else { buff[0] = '?'; buff[1] = '\0'; }
1511    let mut buff = [0u8; LUA_IDSIZE];
1512    if let Some(src) = src {
1513        // macros.tsv: getstr(ts) → ts.as_bytes(); tsslen(ts) → ts.len()
1514        // TODO(port): luaO_chunkid lives in crate::object
1515        chunk_id(&mut buff, src.as_bytes(), src.len());
1516    } else {
1517        buff[0] = b'?';
1518    }
1519    // PORT NOTE: Instead of pushing on the stack, we return the formatted Vec<u8>.
1520    // Callers that need the result on the stack should push it themselves.
1521    let src_part = buff.iter().position(|&b| b == 0).map_or(&buff[..], |n| &buff[..n]);
1522    let mut out = Vec::with_capacity(src_part.len() + 12 + msg.len());
1523    out.extend_from_slice(src_part);
1524    out.push(b':');
1525    // Write line number as decimal bytes
1526    let line_str = line.to_string();
1527    out.extend_from_slice(line_str.as_bytes());
1528    out.extend_from_slice(b": ");
1529    out.extend_from_slice(msg);
1530    out
1531}
1532
1533/// Raises the value currently on top of the stack as a runtime error, invoking
1534/// the error handler if one is set.
1535///
1536pub(crate) fn error_msg(state: &mut LuaState) -> Result<(), LuaError> {
1537    //    luaD_throw(L, LUA_ERRRUN);
1538    // macros.tsv: restorestack(L, L->errfunc) → the StackIdx stored in L->errfunc
1539    if state.errfunc() != 0 {
1540        let errfunc_idx = StackIdx(state.errfunc() as u32);
1541        debug_assert!(
1542            matches!(state.get_at(errfunc_idx), LuaValue::Function(_)),
1543            "error_msg: error handler is not a function"
1544        );
1545        let arg = state.get_at(state.top_idx() - 1).clone();
1546        state.push(arg);
1547        let func = state.get_at(errfunc_idx).clone();
1548        state.set_at(state.top_idx() - 2, func);
1549        // PORT NOTE: the extra stack slot is guaranteed; push() handles it
1550        // TODO(port): luaD_callnoyield lives in crate::do_; call it once available
1551        state.call_no_yield(state.top_idx() - 2, 1)?;
1552    }
1553    // macros.tsv: luaD_throw → return Err(LuaError::with_status(errcode))
1554    // error_sites.tsv: luaD_throw(L, LUA_ERRRUN) → return Err(LuaError::with_status(LuaStatus::ErrRun))
1555    Err(runtime_from_top(state))
1556}
1557
1558/// Formats and raises a runtime error with printf-style arguments. Prepends
1559/// source:line information for Lua frames.
1560///
1561pub(crate) fn run_error(state: &mut LuaState, msg: Vec<u8>) -> Result<(), LuaError> {
1562    // macros.tsv: luaC_checkGC → state.gc().check_step()
1563    state.gc().check_step();
1564
1565    let ci_idx = state.current_ci_idx();
1566    let ci = state.get_ci(ci_idx).clone();
1567
1568    let final_msg = if ci.is_lua() {
1569        // TODO(port): access proto.source via ci_lua_proto
1570        let line = get_current_line(&ci, state);
1571        let proto = ci_lua_proto(&ci, state);
1572        let src = proto.source_string();
1573        add_info(Some(state), &msg, src.map(|s| &**s), line)
1574    } else {
1575        msg
1576    };
1577
1578    // Push the final message as a string, then call error_msg.
1579    // TODO(port): state.intern_str or state.push_string to get the string on stack
1580    let str_val = state.new_string(&final_msg)?;
1581    state.push(LuaValue::Str(str_val));
1582    error_msg(state)
1583}
1584
1585// ─── Line change detection ────────────────────────────────────────────────────
1586
1587/// Checks whether instruction `newpc` is on a different source line than `oldpc`.
1588///
1589fn changed_line(p: &LuaProto, oldpc: i32, newpc: i32) -> bool {
1590    if p.lineinfo.is_empty() {
1591        return false;
1592    }
1593
1594    if newpc - oldpc < MAX_IWTH_ABS / 2 {
1595        let mut delta: i32 = 0;
1596        let mut pc = oldpc;
1597        loop {
1598            pc += 1;
1599            if pc as usize >= p.lineinfo.len() {
1600                break;
1601            }
1602            let lineinfo = p.lineinfo[pc as usize];
1603            if lineinfo == ABS_LINE_INFO {
1604                break;
1605            }
1606            delta += lineinfo as i32;
1607            if pc == newpc {
1608                return delta != 0;
1609            }
1610        }
1611    }
1612    get_func_line(p, oldpc) != get_func_line(p, newpc)
1613}
1614
1615// ─── Trace execution hooks ────────────────────────────────────────────────────
1616
1617/// Called at the start of a Lua function. Fires the call hook if appropriate.
1618/// Returns 1 to keep the trap on, 0 to turn it off.
1619///
1620pub(crate) fn trace_call(state: &mut LuaState) -> Result<i32, LuaError> {
1621    let ci_idx = state.current_ci_idx();
1622    let ci = state.get_ci(ci_idx).clone();
1623    state.get_ci_mut(ci_idx).set_trap(true);
1624    let proto = ci_lua_proto(&ci, state);
1625
1626    if ci.saved_pc() == 0 {
1627        if proto.is_vararg {
1628            return Ok(0);
1629        } else if ci.callstatus & CIST_HOOKYIELD == 0 {
1630            // TODO(port): luaD_hookcall lives in crate::do_
1631            state.hook_call(ci_idx)?;
1632        }
1633    }
1634    Ok(1)
1635}
1636
1637/// Called before each VM instruction when debugging is active.
1638/// Fires line and count hooks as appropriate.
1639/// Returns 1 to keep trap on, 0 to turn it off.
1640///
1641///
1642/// PORT NOTE: The C `pc` parameter is a pointer to the instruction array.
1643/// In Rust, `pc` is the 0-based index of the NEXT instruction (same semantic as
1644/// `savedpc`). After incrementing for reference (`pc++` in C), it equals
1645/// the next-instruction index.
1646pub(crate) fn trace_exec(state: &mut LuaState, pc: u32) -> Result<i32, LuaError> {
1647    let ci_idx = state.current_ci_idx();
1648    let ci = state.get_ci(ci_idx).clone();
1649
1650    let mask = state.hook_mask();
1651
1652    if !state.allowhook {
1653        return Ok(1);
1654    }
1655
1656    if mask & (LUA_MASKLINE | LUA_MASKCOUNT) == 0 {
1657        state.get_ci_mut(ci_idx).set_trap(false);
1658        return Ok(0);
1659    }
1660
1661    let next_pc = pc + 1;
1662    state.get_ci_mut(ci_idx).set_saved_pc(next_pc);
1663
1664    let counthook = if mask & LUA_MASKCOUNT != 0 {
1665        let hc = state.hook_count() - 1;
1666        state.set_hook_count(hc);
1667        hc == 0
1668    } else {
1669        false
1670    };
1671
1672    if counthook {
1673        state.reset_hook_count();
1674    } else if mask & LUA_MASKLINE == 0 {
1675        return Ok(1);
1676    }
1677
1678    if ci.callstatus & CIST_HOOKYIELD != 0 {
1679        state.get_ci_mut(ci_idx).callstatus &= !CIST_HOOKYIELD;
1680        return Ok(1);
1681    }
1682
1683    if state.ci_lua_closure(ci_idx).is_none() {
1684        return Ok(1);
1685    }
1686
1687    // macros.tsv: isIT(i) → i.is_in_top()
1688    // PORT NOTE: savedpc - 1 is the current instruction (now at index next_pc - 1 = pc).
1689    let cur_instr = state.get_proto_instr(ci_idx, pc as u32);
1690    if !cur_instr.is_in_top() {
1691        let ci_top = state.get_ci(ci_idx).top;
1692        state.set_top(ci_top);
1693    }
1694
1695    if counthook {
1696        // TODO(port): luaD_hook lives in crate::do_
1697        state.call_hook_event(LUA_HOOKCOUNT, -1)?;
1698    }
1699
1700    if mask & LUA_MASKLINE != 0 {
1701        let proto = ci_lua_proto(&ci, state);
1702        let oldpc = if state.old_pc() < proto.code.len() as u32 {
1703            state.old_pc() as i32
1704        } else {
1705            0
1706        };
1707        // current instruction is pc (0-based); pcRel gives current = next - 1
1708        let npci = next_pc as i32 - 1;
1709
1710        if npci <= oldpc || changed_line(&proto, oldpc, npci) {
1711            let newline = get_func_line(&proto, npci);
1712            // TODO(port): luaD_hook lives in crate::do_
1713            state.call_hook_event(LUA_HOOKLINE, newline)?;
1714        }
1715        state.set_old_pc(npci as u32);
1716    }
1717
1718    if state.status() == lua_types::status::LuaStatus::Yield {
1719        if counthook {
1720            state.set_hook_count(1);
1721        }
1722        state.get_ci_mut(ci_idx).callstatus |= CIST_HOOKYIELD;
1723        // error_sites.tsv: luaD_throw(L, LUA_YIELD) → return Err(LuaError::with_status(LuaStatus::Yield))
1724        return Err(LuaError::Yield);
1725    }
1726
1727    Ok(1)
1728}
1729
1730// ─── File-local helpers referenced above but not directly translated ──────────
1731
1732/// Gets the source line name (short, truncated) for error messages.
1733///
1734/// to the real impl in `crate::object`. Handles `=name`, `@filename`, and
1735/// `[string "..."]` formatting so error prefixes are concise rather than dumping
1736/// the entire source verbatim.
1737fn chunk_id(out: &mut [u8; LUA_IDSIZE], source: &[u8], _srclen: usize) {
1738    out.fill(0);
1739    let n = crate::object::chunk_id(&mut out[..], source);
1740    if n < out.len() {
1741        out[n] = 0;
1742    }
1743}
1744
1745/// Gets the local variable name for register `reg+1` at instruction `pc` in `p`.
1746/// Returns `None` if not found (variable is not live at `pc`).
1747///
1748fn get_local_name(p: &LuaProto, n: i32, pc: i32) -> Option<&[u8]> {
1749    crate::func::get_local_name(p, n, pc)
1750}
1751
1752/// Gets the n-th local name from a Lua closure (for non-active function query).
1753fn get_local_name_from_closure(cl: &LuaClosureLua, n: i32, pc: i32) -> Option<&[u8]> {
1754    get_local_name(&cl.proto, n, pc)
1755}
1756
1757/// Retrieves the LuaProto for the Lua closure at `ci.func` from the stack.
1758///
1759/// macros.tsv: ci_func → ci.lua_closure() returning &GcRef<LuaClosure::Lua>
1760///
1761/// PORT NOTE: The C version returns a raw pointer and is a macro. Here we
1762/// navigate through the LuaState stack. Returns a reference with the
1763/// lifetime of the proto inside the GcRef (Rc), which must remain valid.
1764///
1765/// TODO(port): This returns a cloned Rc's inner reference; Phase B must verify
1766/// lifetimes are correct once all types are wired.
1767/// PORT NOTE: reshaped for borrowck — returns `GcRef<LuaProto>` (Rc clone) instead
1768/// of `&'a LuaProto` to avoid returning a reference to a temporary `LuaValue`
1769/// produced by `get_at`. Callers deref through `GcRef<T>: Deref<Target=T>`.
1770fn ci_lua_proto(ci: &CallInfo, state: &LuaState) -> GcRef<LuaProto> {
1771    match state.get_at(ci.func) {
1772        LuaValue::Function(LuaClosure::Lua(cl)) => cl.proto.clone(),
1773        _ => panic!("ci_lua_proto: call frame does not hold a Lua closure"),
1774    }
1775}
1776
1777// ──────────────────────────────────────────────────────────────────────────────
1778// PORT STATUS
1779//   source:        src/ldebug.c  (962 lines, 30 functions)
1780//   target_crate:  lua-vm
1781//   confidence:    medium
1782//   todos:         44
1783//   port_notes:    15
1784//   unsafe_blocks: 0
1785//   notes:         Logic faithful to C; cross-crate imports (luaF_*, luaT_*,
1786//                  luaD_*, luaO_chunkid, opcode accessors) are stubbed with
1787//                  TODO(port) markers. LuaState accessor methods (call_stack_mut,
1788//                  get_ci, set_trap, saved_pc, hook_mask, etc.) are called as if
1789//                  defined in state.rs — Phase B must implement them. The
1790//                  pointer-identity comparisons in instack/getupvalname are
1791//                  translated to StackIdx comparisons (a structural change).
1792//                  `lua_gethook` returns a bool instead of a fn pointer because
1793//                  Box<dyn FnMut> cannot be returned by value without restructuring.
1794//                  rustc check: zero real syntax errors; all 67 diagnostics are
1795//                  expected name-resolution errors (E0432/E0433/E0425/E0282).
1796// ──────────────────────────────────────────────────────────────────────────────