Skip to main content

lua_vm/
debug.rs

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