Skip to main content

lua_vm/
debug.rs

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