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