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