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