Skip to main content

lua_stdlib/
debug_lib.rs

1//! Debug library — Rust port of `ldblib.c`.
2//!
3//! Provides the `debug` Lua standard library module. Exposes debug
4//! introspection APIs: stack inspection (`getinfo`, `getlocal`), upvalue
5//! access (`getupvalue`, `setupvalue`, `upvaluejoin`), hook management
6//! (`sethook`, `gethook`), metatable overrides (`getmetatable`,
7//! `setmetatable`), userdata values (`getuservalue`, `setuservalue`),
8//! and utility functions (`traceback`, `debug`, `setcstacklimit`).
9//!
10//! C source: `reference/lua-5.4.7/src/ldblib.c` (484 lines, 20 functions)
11
12use std::cell::RefCell;
13use std::io::{self, BufRead, Write};
14use std::rc::Rc;
15
16use lua_types::{GcRef, LuaError, LuaString, LuaType, LuaValue, LuaStatus};
17use crate::state_stub::{LuaState, LuaStateStubExt as _, lua_CFunction, upvalue_index, CompareOp, LuaDebug as DebugInfo};
18
19// ── Constants ──────────────────────────────────────────────────────────────
20
21/// Registry key for the hook table that maps threads to their hook functions.
22///
23/// C: `static const char *const HOOKKEY = "_HOOKKEY";`
24const HOOKKEY: &[u8] = b"_HOOKKEY";
25
26/// Hook event names indexed by the raw event code stored in [`DebugInfo::event`].
27/// Order must match the `LUA_HOOK*` constants: Call=0, Return=1, Line=2, Count=3, TailCall=4.
28///
29/// C: `static const char *const hooknames[] = {"call","return","line","count","tail call"};`
30const HOOKNAMES: &[&[u8]; 5] = &[b"call", b"return", b"line", b"count", b"tail call"];
31
32/// Bitmask constants for hook event selection.
33/// C: `LUA_MASKCALL`, `LUA_MASKRET`, `LUA_MASKLINE`, `LUA_MASKCOUNT`
34const MASK_CALL: u32 = 1 << 0;
35const MASK_RET: u32 = 1 << 1;
36const MASK_LINE: u32 = 1 << 2;
37const MASK_COUNT: u32 = 1 << 3;
38
39// ── Local type aliases ─────────────────────────────────────────────────────
40
41/// Entry-point signature for a Lua stdlib function in Rust.
42pub(crate) type LibFn = fn(&mut LuaState) -> Result<usize, LuaError>;
43
44/// A Rust hook callback registered with the Lua VM's hook mechanism.
45///
46/// C: `lua_Hook` = `void (*)(lua_State *, lua_Debug *)`
47/// PORT NOTE: The Rust hook receives the event code and current line directly
48/// rather than a lua_Debug pointer, since the lua-stdlib `DebugInfo` and the
49/// canonical `lua_vm::debug::LuaDebug` are distinct types during Phase B.
50pub(crate) type HookFn = fn(&mut LuaState, i32, i32) -> Result<(), LuaError>;
51
52/// Opaque identity handle for an upvalue.
53///
54/// C: `void *` returned by `lua_upvalueid`. Lua uses pointer equality to
55/// check whether two upvalues share the same storage cell.
56///
57/// TODO(port): In C this is a raw pointer into the upvalue's storage cell.
58/// Safe Rust cannot expose a raw pointer outside `lua-gc`. A stable u64 ID
59/// or a GcRef-based comparison should be designed in Phase D. Using `usize`
60/// (pointer-sized) as a placeholder so the call sites compile.
61type UpvalId = usize;
62
63#[derive(Clone)]
64enum DebugThreadTarget {
65    Current,
66    Other(Rc<RefCell<LuaState>>),
67    Unavailable,
68}
69
70fn resolve_debug_thread_target(
71    state: &LuaState,
72    target_thread: &Option<GcRef<lua_types::value::LuaThread>>,
73) -> DebugThreadTarget {
74    let Some(thread) = target_thread else {
75        return DebugThreadTarget::Current;
76    };
77
78    if thread.id == state.cached_thread_id {
79        return DebugThreadTarget::Current;
80    }
81
82    let g = state.global();
83    if thread.id == g.main_thread_id {
84        DebugThreadTarget::Unavailable
85    } else {
86        g.threads
87            .get(&thread.id)
88            .map(|entry| DebugThreadTarget::Other(entry.state.clone()))
89            .unwrap_or(DebugThreadTarget::Unavailable)
90    }
91}
92
93// ── Internal helpers ───────────────────────────────────────────────────────
94
95/// Ensure the cross-thread target has room for `n` more stack slots.
96///
97/// When the target is the current thread this is a no-op because the current
98/// thread's stack is managed by the caller. When it is another thread we
99/// must verify its stack, but that requires a simultaneous `&mut LuaState`
100/// for both threads.
101///
102/// C: `static void checkstack(lua_State *L, lua_State *L1, int n)`
103fn check_cross_thread_stack(
104    state: &mut LuaState,
105    target_is_self: bool,
106    n: i32,
107) -> Result<(), LuaError> {
108    // C: if (l_unlikely(L != L1 && !lua_checkstack(L1, n)))
109    //        luaL_error(L, "stack overflow");
110    if !target_is_self {
111        // TODO(port): checking a different thread's stack requires simultaneous
112        // `&mut LuaState` for both threads, which is not expressible in safe Rust
113        // without interior mutability. Conservatively checks the current state only.
114        state.ensure_stack(n, "stack overflow")?;
115    }
116    Ok(())
117}
118
119/// Inspect argument 1: if it is a thread value, return `(1, Some(thread_ref))`;
120/// otherwise return `(0, None)` meaning "operate on the current state".
121///
122/// C: `static lua_State *getthread(lua_State *L, int *arg)`
123fn getthread(state: &mut LuaState) -> (i32, Option<GcRef<lua_types::value::LuaThread>>) {
124    // C: if (lua_isthread(L, 1)) { *arg = 1; return lua_tothread(L, 1); }
125    if state.type_at(1) == LuaType::Thread {
126        let thread = state.to_thread_at(1);
127        return (1, thread);
128    }
129    // C: *arg = 0; return L;
130    (0, None)
131}
132
133/// Push byte string `v` (or Nil when `v` is `None`) and store it under key
134/// `k` in the table that sits at stack position -2.
135///
136/// C: `static void settabss(lua_State *L, const char *k, const char *v)`
137/// PORT NOTE: The C version passes NULL to signal "no value" (lua_pushstring
138/// with NULL pushes nil). Rust uses Option<&[u8]> for the same semantics.
139fn settabss(state: &mut LuaState, k: &[u8], v: Option<&[u8]>) -> Result<(), LuaError> {
140    // C: lua_pushstring(L, v);  /* NULL -> nil */
141    match v {
142        Some(s) => {
143            let ls = state.intern_str(s)?;
144            state.push(LuaValue::Str(ls));
145        }
146        None => { state.push(LuaValue::Nil); }
147    }
148    // C: lua_setfield(L, -2, k);
149    state.set_field(-2, k)
150}
151
152/// Push integer `v` and store it under key `k` in the table at -2.
153///
154/// C: `static void settabsi(lua_State *L, const char *k, int v)`
155fn settabsi(state: &mut LuaState, k: &[u8], v: i32) -> Result<(), LuaError> {
156    // C: lua_pushinteger(L, v); lua_setfield(L, -2, k);
157    state.push(LuaValue::Int(v as i64));
158    state.set_field(-2, k)
159}
160
161/// Push boolean `v` and store it under key `k` in the table at -2.
162///
163/// C: `static void settabsb(lua_State *L, const char *k, int v)`
164fn settabsb(state: &mut LuaState, k: &[u8], v: bool) -> Result<(), LuaError> {
165    // C: lua_pushboolean(L, v); lua_setfield(L, -2, k);
166    state.push(LuaValue::Bool(v));
167    state.set_field(-2, k)
168}
169
170/// After `lua_getinfo` has pushed a result ('f' function or 'L' line table)
171/// onto L1's stack, move it into the result table on L as field `fname`.
172///
173/// When target is self, the value is already on our stack; rotate to bring
174/// it above the result table. When target is a different thread, use xmove.
175///
176/// C: `static void treatstackoption(lua_State *L, lua_State *L1, const char *fname)`
177fn treat_stack_option(
178    state: &mut LuaState,
179    target_is_self: bool,
180    fname: &[u8],
181) -> Result<(), LuaError> {
182    if target_is_self {
183        // C: lua_rotate(L, -2, 1);  /* exchange object and table */
184        state.rotate(-2, 1);
185    } else {
186        // C: lua_xmove(L1, L, 1);  /* move object to the "main" stack */
187        // TODO(port): moving a value from another thread's stack (lua_xmove)
188        // requires simultaneous `&mut LuaState` for both threads. Not expressible
189        // in safe Rust without interior mutability. Pushes Nil as placeholder.
190        state.push(LuaValue::Nil);
191    }
192    // C: lua_setfield(L, -2, fname);
193    state.set_field(-2, fname)
194}
195
196fn move_stack_option_from_target(
197    state: &mut LuaState,
198    target: &mut LuaState,
199    fname: &[u8],
200) -> Result<(), LuaError> {
201    let val = target.get_at(target.top_idx() - 1);
202    target.pop_n(1);
203    state.push(val);
204    state.set_field(-2, fname)
205}
206
207// ── Library functions ──────────────────────────────────────────────────────
208
209/// `debug.getregistry()` — return the Lua registry table.
210///
211/// C: `static int db_getregistry(lua_State *L)`
212pub(crate) fn get_registry(state: &mut LuaState) -> Result<usize, LuaError> {
213    // C: lua_pushvalue(L, LUA_REGISTRYINDEX); return 1;
214    state.push_registry();
215    Ok(1)
216}
217
218/// `debug.getmetatable(obj)` — return the metatable of `obj`, or nil if none.
219///
220/// C: `static int db_getmetatable(lua_State *L)`
221pub(crate) fn get_metatable(state: &mut LuaState) -> Result<usize, LuaError> {
222    // C: luaL_checkany(L, 1);
223    state.check_arg_any(1)?;
224    // C: if (!lua_getmetatable(L, 1)) lua_pushnil(L);
225    if !state.get_metatable(1)? {
226        state.push(LuaValue::Nil);
227    }
228    Ok(1)
229}
230
231/// `debug.setmetatable(obj, table)` — set `table` (or nil) as `obj`'s metatable.
232/// Returns the first argument `obj`.
233///
234/// C: `static int db_setmetatable(lua_State *L)`
235pub(crate) fn set_metatable(state: &mut LuaState) -> Result<usize, LuaError> {
236    // C: int t = lua_type(L, 2);
237    let t = state.type_at(2);
238    // C: luaL_argexpected(L, t == LUA_TNIL || t == LUA_TTABLE, 2, "nil or table");
239    if !(t == LuaType::Nil || t == LuaType::Table) {
240        let got = state.arg(2);
241        return Err(LuaError::type_arg_error(2, "nil or table", &got));
242    }
243    // C: lua_settop(L, 2); lua_setmetatable(L, 1); return 1;
244    lua_vm::api::set_top(state, 2)?;
245    state.set_metatable(1)?;
246    Ok(1)
247}
248
249/// `debug.getuservalue(obj [, n])` — return the n-th user value of userdata
250/// `obj` plus `true`, or the fail value if `obj` is not userdata or `n` is out
251/// of range.
252///
253/// C: `static int db_getuservalue(lua_State *L)`
254pub(crate) fn get_uservalue(state: &mut LuaState) -> Result<usize, LuaError> {
255    // C: int n = (int)luaL_optinteger(L, 2, 1);
256    let n = state.opt_arg_integer(2, 1)? as i32;
257    // C: if (lua_type(L, 1) != LUA_TUSERDATA) luaL_pushfail(L);
258    if state.type_at(1) != LuaType::UserData {
259        state.push_fail();
260        return Ok(1);
261    }
262    // C: else if (lua_getiuservalue(L, 1, n) != LUA_TNONE) { lua_pushboolean(L, 1); return 2; }
263    let ty = state.get_iuservalue(1, n)?;
264    if ty != LuaType::None {
265        state.push(LuaValue::Bool(true));
266        return Ok(2);
267    }
268    Ok(1)
269}
270
271/// `debug.setuservalue(obj, value [, n])` — set the n-th user value of userdata
272/// `obj` to `value`. Returns `obj`, or the fail value on failure.
273///
274/// C: `static int db_setuservalue(lua_State *L)`
275pub(crate) fn set_uservalue(state: &mut LuaState) -> Result<usize, LuaError> {
276    // C: int n = (int)luaL_optinteger(L, 3, 1);
277    let n = state.opt_arg_integer(3, 1)? as i32;
278    // C: luaL_checktype(L, 1, LUA_TUSERDATA);
279    state.check_arg_type(1, LuaType::UserData)?;
280    // C: luaL_checkany(L, 2);
281    state.check_arg_any(2)?;
282    // C: lua_settop(L, 2);
283    lua_vm::api::set_top(state, 2)?;
284    // C: if (!lua_setiuservalue(L, 1, n)) luaL_pushfail(L);
285    if !state.set_iuservalue(1, n)? {
286        state.push_fail();
287    }
288    Ok(1)
289}
290
291/// `debug.getinfo([thread,] f|level [, what])` — collect debug information
292/// about function `f` or stack level `level` into a new table. The `what`
293/// string selects which fields to populate (default `"flnSrtu"`).
294///
295/// C: `static int db_getinfo(lua_State *L)`
296pub(crate) fn get_info(state: &mut LuaState) -> Result<usize, LuaError> {
297    let mut ar = DebugInfo::default();
298
299    // C: int arg; lua_State *L1 = getthread(L, &arg);
300    let (arg, other_thread) = getthread(state);
301    let target_is_self = other_thread.is_none();
302    let target_state = resolve_debug_thread_target(state, &other_thread);
303
304    // C: const char *options = luaL_optstring(L, arg+2, "flnSrtu");
305    // to_vec() immediately to avoid borrow-checker conflict with subsequent &mut state ops.
306    let raw_opts: Vec<u8> = state.opt_arg_string(arg + 2, b"flnSrtu")?.to_vec();
307
308    // C: checkstack(L, L1, 3);
309    check_cross_thread_stack(state, target_is_self, 3)?;
310
311    // C: luaL_argcheck(L, options[0] != '>', arg + 2, "invalid option '>'");
312    if raw_opts.first() == Some(&b'>') {
313        return Err(LuaError::arg_error(arg + 2, "invalid option '>'"));
314    }
315
316    // Build the effective options string, prepending '>' when the subject is a function.
317    let options: Vec<u8>;
318    let mut info_target_owner: Option<Rc<RefCell<LuaState>>> = None;
319    let mut info_target: Option<std::cell::RefMut<'_, LuaState>> = None;
320    let mut info_target_is_self = target_is_self;
321
322    if state.type_at(arg + 1) == LuaType::Function {
323        // C: options = lua_pushfstring(L, ">%s", options);  /* add '>' to options */
324        // In C this also pushes the string onto the stack; in Rust we just build a Vec.
325        let mut prefixed = Vec::with_capacity(raw_opts.len() + 1);
326        prefixed.push(b'>');
327        prefixed.extend_from_slice(&raw_opts);
328        options = prefixed;
329
330        // C: lua_pushvalue(L, arg + 1);  /* move function to L1 stack */
331        // C: lua_xmove(L, L1, 1);
332        if target_is_self {
333            state.push_value_at(arg + 1)?;
334        } else {
335            // TODO(port): lua_xmove to another thread's stack requires simultaneous
336            // `&mut LuaState` for both threads. Cross-thread getinfo with a function
337            // argument is left incomplete for Phase A.
338        }
339
340        // C: if (!lua_getinfo(L1, options, &ar)) return luaL_argerror(...);
341        // With '>' prefix, get_debug_info consumes the function from the top of stack.
342        if state.get_debug_info(&options, &mut ar).is_err() {
343            return Err(LuaError::arg_error(arg + 2, "invalid option"));
344        }
345    } else {
346        options = raw_opts;
347
348        // C: if (!lua_getstack(L1, (int)luaL_checkinteger(L, arg+1), &ar)) { fail }
349        let level = state.check_arg_integer(arg + 1)? as i32;
350        match target_state {
351            DebugThreadTarget::Current | DebugThreadTarget::Unavailable => {
352                info_target_is_self = true;
353                if !state.get_stack_level(level, &mut ar) {
354                    // C: luaL_pushfail(L); return 1;
355                    state.push_fail()?;
356                    return Ok(1);
357                }
358
359                // C: if (!lua_getinfo(L1, options, &ar)) return luaL_argerror(...);
360                if state.get_debug_info(&options, &mut ar).is_err() {
361                    return Err(LuaError::arg_error(arg + 2, "invalid option"));
362                }
363            }
364            DebugThreadTarget::Other(target_state) => {
365                info_target_owner = Some(target_state);
366                let mut target = info_target_owner
367                    .as_ref()
368                    .expect("target owner just stored")
369                    .borrow_mut();
370                if !target.get_stack_level(level, &mut ar) {
371                    state.push_fail()?;
372                    return Ok(1);
373                }
374                if target.get_debug_info(&options, &mut ar).is_err() {
375                    return Err(LuaError::arg_error(arg + 2, "invalid option"));
376                }
377                info_target = Some(target);
378            }
379        }
380    }
381
382    // C: lua_newtable(L);  /* table to collect results */
383    let result_tbl = state.new_table();
384    state.push(LuaValue::Table(result_tbl));
385
386    // C: if (strchr(options, 'S')) { ... }
387    if options.contains(&b'S') {
388        // C: lua_pushlstring(L, ar.source, ar.srclen); lua_setfield(L, -2, "source");
389        let src = state.intern_str(ar.source_bytes())?;
390        state.push(LuaValue::Str(src));
391        state.set_field(-2, b"source")?;
392
393        settabss(state, b"short_src", Some(ar.short_src_bytes()))?;
394        settabsi(state, b"linedefined", ar.linedefined)?;
395        settabsi(state, b"lastlinedefined", ar.lastlinedefined)?;
396        settabss(state, b"what", Some(ar.what_bytes()))?;
397    }
398    if options.contains(&b'l') {
399        settabsi(state, b"currentline", ar.currentline)?;
400    }
401    if options.contains(&b'u') {
402        settabsi(state, b"nups", ar.nups as i32)?;
403        settabsi(state, b"nparams", ar.nparams as i32)?;
404        settabsb(state, b"isvararg", ar.isvararg)?;
405    }
406    if options.contains(&b'n') {
407        let name_opt: Option<&[u8]> = ar.name.as_deref();
408        settabss(state, b"name", name_opt)?;
409        settabss(state, b"namewhat", Some(ar.namewhat_bytes()))?;
410    }
411    if options.contains(&b'r') {
412        settabsi(state, b"ftransfer", ar.ftransfer as i32)?;
413        settabsi(state, b"ntransfer", ar.ntransfer as i32)?;
414    }
415    if options.contains(&b't') {
416        settabsb(state, b"istailcall", ar.istailcall)?;
417    }
418    // 'L' and 'f' options: lua_getinfo pushed line-table then function onto L1's stack.
419    // treat_stack_option moves each into the result table.
420    // PORT NOTE: C's lua_getinfo always pushes 'f' result before 'L' result (regardless
421    // of option-string order), so the treatstackoption calls below are intentionally
422    // ordered 'L' first then 'f' — matching the C db_getinfo exactly.
423    if options.contains(&b'L') {
424        if info_target_is_self {
425            treat_stack_option(state, true, b"activelines")?;
426        } else if let Some(target) = info_target.as_mut() {
427            move_stack_option_from_target(state, &mut **target, b"activelines")?;
428        } else {
429            state.push(LuaValue::Nil);
430            state.set_field(-2, b"activelines")?;
431        }
432    }
433    if options.contains(&b'f') {
434        if info_target_is_self {
435            treat_stack_option(state, true, b"func")?;
436        } else if let Some(target) = info_target.as_mut() {
437            move_stack_option_from_target(state, &mut **target, b"func")?;
438        } else {
439            state.push(LuaValue::Nil);
440            state.set_field(-2, b"func")?;
441        }
442    }
443
444    Ok(1)
445}
446
447/// `debug.getlocal([thread,] level, local)` — return the name and value of
448/// local variable `local` at stack level `level`.
449///
450/// When the first argument is a function, returns only the parameter name at
451/// position `local` (no value).
452///
453/// C: `static int db_getlocal(lua_State *L)`
454pub(crate) fn get_local(state: &mut LuaState) -> Result<usize, LuaError> {
455    // C: int arg; lua_State *L1 = getthread(L, &arg);
456    let (arg, other_thread) = getthread(state);
457    let target_state = resolve_debug_thread_target(state, &other_thread);
458
459    // C: int nvar = (int)luaL_checkinteger(L, arg + 2);
460    let nvar = state.check_arg_integer(arg + 2)? as i32;
461
462    // C: if (lua_isfunction(L, arg + 1)) { ... }
463    if state.type_at(arg + 1) == LuaType::Function {
464        // C: lua_pushvalue(L, arg + 1);  /* push function */
465        state.push_value_at(arg + 1)?;
466        // C: lua_pushstring(L, lua_getlocal(L, NULL, nvar));
467        // lua_getlocal with NULL ar reads parameter names from the function at the
468        // top of the stack; it does NOT push a value.
469        let name = state.get_param_name(0, nvar)?;
470        match name {
471            Some(n) => {
472                let ls = state.intern_str(&n)?;
473                state.push(LuaValue::Str(ls));
474            }
475            None => { state.push(LuaValue::Nil); }
476        }
477        // C: return 1;  /* return only name (there is no value) */
478        // The pushed function below name is discarded by the VM when it collects
479        // exactly 1 return value from the top of the stack.
480        return Ok(1);
481    }
482
483    // Stack-level path.
484    // C: int level = (int)luaL_checkinteger(L, arg + 1);
485    let level = state.check_arg_integer(arg + 1)? as i32;
486    let mut ar = DebugInfo::default();
487
488    let name = match target_state {
489        DebugThreadTarget::Current | DebugThreadTarget::Unavailable => {
490            // C: if (l_unlikely(!lua_getstack(L1, level, &ar))) return luaL_argerror(...);
491            if !state.get_stack_level(level, &mut ar) {
492                return Err(LuaError::arg_error(arg + 1, "level out of range"));
493            }
494            check_cross_thread_stack(state, true, 1)?;
495            // C: name = lua_getlocal(L1, &ar, nvar);
496            // Pushes the local's value onto L1's stack and returns its name.
497            state.get_local_at(&ar, nvar)?
498        }
499        DebugThreadTarget::Other(target_state) => {
500            let mut target = target_state.borrow_mut();
501            if !target.get_stack_level(level, &mut ar) {
502                return Err(LuaError::arg_error(arg + 1, "level out of range"));
503            }
504            check_cross_thread_stack(state, false, 1)?;
505            let name = target.get_local_at(&ar, nvar)?;
506            if name.is_some() {
507                let val = target.get_at(target.top_idx() - 1);
508                target.pop_n(1);
509                state.push(val);
510            }
511            name
512        }
513    };
514
515    if let Some(n) = name {
516        // C: lua_pushstring(L, name); lua_rotate(L, -2, 1); return 2;
517        let ls = state.intern_str(&n)?;
518        state.push(LuaValue::Str(ls));
519        state.rotate(-2, 1)?;
520        Ok(2)
521    } else {
522        // C: luaL_pushfail(L); return 1;
523        state.push_fail()?;
524        Ok(1)
525    }
526}
527
528/// `debug.setlocal([thread,] level, local, value)` — set local variable
529/// `local` at stack level `level` to `value`. Returns the variable name, or
530/// nil on failure.
531///
532/// C: `static int db_setlocal(lua_State *L)`
533pub(crate) fn set_local(state: &mut LuaState) -> Result<usize, LuaError> {
534    // C: int arg; lua_State *L1 = getthread(L, &arg);
535    let (arg, other_thread) = getthread(state);
536    let target_state = resolve_debug_thread_target(state, &other_thread);
537
538    // C: int level = (int)luaL_checkinteger(L, arg + 1);
539    let level = state.check_arg_integer(arg + 1)? as i32;
540    // C: int nvar = (int)luaL_checkinteger(L, arg + 2);
541    let nvar = state.check_arg_integer(arg + 2)? as i32;
542
543    let mut ar = DebugInfo::default();
544
545    // C: luaL_checkany(L, arg+3);
546    state.check_arg_any(arg + 3)?;
547    // C: lua_settop(L, arg+3);
548    lua_vm::api::set_top(state, arg + 3)?;
549
550    let name = match target_state {
551        DebugThreadTarget::Current | DebugThreadTarget::Unavailable => {
552            // C: if (l_unlikely(!lua_getstack(L1, level, &ar))) return luaL_argerror(...);
553            if !state.get_stack_level(level, &mut ar) {
554                return Err(LuaError::arg_error(arg + 1, "level out of range"));
555            }
556            check_cross_thread_stack(state, true, 1)?;
557            // C: name = lua_setlocal(L1, &ar, nvar);  /* pops value from L1 */
558            let name = state.set_local_at(&ar, nvar)?;
559            // C: if (name == NULL) lua_pop(L1, 1);  /* pop value if not consumed */
560            if name.is_none() {
561                state.pop_n(1);
562            }
563            name
564        }
565        DebugThreadTarget::Other(target_state) => {
566            let new_val = state.get_at(state.top_idx() - 1);
567            let mut target = target_state.borrow_mut();
568            if !target.get_stack_level(level, &mut ar) {
569                return Err(LuaError::arg_error(arg + 1, "level out of range"));
570            }
571            check_cross_thread_stack(state, false, 1)?;
572            target.push(new_val);
573            let name = target.set_local_at(&ar, nvar)?;
574            if name.is_none() {
575                target.pop_n(1);
576            }
577            state.pop_n(1);
578            name
579        }
580    };
581
582    // C: lua_pushstring(L, name);
583    match name {
584        Some(n) => {
585            let ls = state.intern_str(&n)?;
586            state.push(LuaValue::Str(ls));
587        }
588        None => { state.push(LuaValue::Nil); }
589    }
590    Ok(1)
591}
592
593/// Shared implementation for `get_upvalue` and `set_upvalue`.
594///
595/// When `get` is `true`, retrieves upvalue `n` of the function at stack index 1,
596/// pushes its value, and returns `(name, value)` — 2 results.
597///
598/// When `get` is `false`, pops the top stack value and installs it as upvalue
599/// `n`, returning `(name,)` — 1 result.
600///
601/// Returns 0 results when the upvalue index is out of range.
602///
603/// C: `static int auxupvalue(lua_State *L, int get)`
604fn aux_upvalue(state: &mut LuaState, get: bool) -> Result<usize, LuaError> {
605    // C: int n = (int)luaL_checkinteger(L, 2);
606    let n = state.check_arg_integer(2)? as i32;
607    // C: luaL_checktype(L, 1, LUA_TFUNCTION);
608    state.check_arg_type(1, LuaType::Function)?;
609
610    // C: name = get ? lua_getupvalue(L, 1, n) : lua_setupvalue(L, 1, n);
611    let name: Option<Vec<u8>> = if get {
612        // lua_getupvalue pushes the upvalue value and returns the name.
613        state.get_upvalue(1, n)?
614    } else {
615        // lua_setupvalue pops the top-of-stack value, sets upvalue n, returns name.
616        state.set_upvalue(1, n)?
617    };
618
619    // C: if (name == NULL) return 0;
620    let name_ref = match name {
621        Some(n) => n,
622        None => return Ok(0),
623    };
624
625    // C: lua_pushstring(L, name);
626    let ls = state.intern_str(&name_ref)?;
627    state.push(LuaValue::Str(ls));
628
629    // C: lua_insert(L, -(get+1));  /* no-op if get is false */
630    // When get=true: stack is [..., value, name]; insert at -2 → [..., name, value].
631    // When get=false: insert at -1 is a no-op; stack is [..., name].
632    if get {
633        state.insert(-2)?;
634    }
635
636    // C: return get + 1;
637    Ok(if get { 2 } else { 1 })
638}
639
640/// `debug.getupvalue(f, up)` — return the name and value of upvalue `up` of `f`.
641///
642/// C: `static int db_getupvalue(lua_State *L)`
643pub(crate) fn get_upvalue(state: &mut LuaState) -> Result<usize, LuaError> {
644    // C: return auxupvalue(L, 1);
645    aux_upvalue(state, true)
646}
647
648/// `debug.setupvalue(f, up, value)` — set upvalue `up` of `f` to `value`.
649/// Returns the upvalue name.
650///
651/// C: `static int db_setupvalue(lua_State *L)`
652pub(crate) fn set_upvalue(state: &mut LuaState) -> Result<usize, LuaError> {
653    // C: luaL_checkany(L, 3);
654    state.check_arg_any(3)?;
655    // C: return auxupvalue(L, 0);
656    aux_upvalue(state, false)
657}
658
659/// Verify that upvalue `argnup` of function at stack index `argf` exists.
660/// Returns the opaque identity handle and the upvalue index.
661/// If `require_valid` is true, raises an arg error when the upvalue is absent.
662///
663/// C: `static void *checkupval(lua_State *L, int argf, int argnup, int *pnup)`
664fn check_upval(
665    state: &mut LuaState,
666    argf: i32,
667    argnup: i32,
668    require_valid: bool,
669) -> Result<(Option<UpvalId>, i32), LuaError> {
670    // C: int nup = (int)luaL_checkinteger(L, argnup);
671    let nup = state.check_arg_integer(argnup)? as i32;
672    // C: luaL_checktype(L, argf, LUA_TFUNCTION);
673    state.check_arg_type(argf, LuaType::Function)?;
674    // C: id = lua_upvalueid(L, argf, nup);
675    // TODO(port): lua_upvalueid returns a raw void* that uniquely identifies
676    // an upvalue's storage cell. A safe equivalent (e.g., GcRef<UpVal> pointer
677    // comparison, or a stable u64 ID from the GC layer) must be defined in
678    // Phase D. Using Option<usize> as placeholder.
679    let id: Option<UpvalId> = match state.upvalue_id(argf, nup) {
680        Ok(p) if p.is_null() => None,
681        Ok(p) => Some(p as usize),
682        Err(_) => None,
683    };
684    // C: if (pnup) { luaL_argcheck(L, id != NULL, argnup, "invalid upvalue index"); *pnup = nup; }
685    if require_valid && id.is_none() {
686        return Err(LuaError::arg_error(argnup, "invalid upvalue index"));
687    }
688    Ok((id, nup))
689}
690
691/// `debug.upvalueid(f, n)` — return a unique identifier for upvalue `n` of
692/// function `f` as a light userdata. Returns fail on out-of-range index.
693///
694/// C: `static int db_upvalueid(lua_State *L)`
695pub(crate) fn upvalue_id(state: &mut LuaState) -> Result<usize, LuaError> {
696    // C: void *id = checkupval(L, 1, 2, NULL);
697    let (id, _nup) = check_upval(state, 1, 2, false)?;
698    match id {
699        Some(uid) => {
700            // C: lua_pushlightuserdata(L, id);
701            lua_vm::api::push_light_userdata(state, uid as *mut core::ffi::c_void);
702        }
703        None => {
704            // C: luaL_pushfail(L);
705            state.push_fail()?;
706        }
707    }
708    Ok(1)
709}
710
711/// `debug.upvaluejoin(f1, n1, f2, n2)` — make upvalue `n1` of function `f1`
712/// refer to the same storage as upvalue `n2` of function `f2`.
713///
714/// C: `static int db_upvaluejoin(lua_State *L)`
715pub(crate) fn upvalue_join(state: &mut LuaState) -> Result<usize, LuaError> {
716    // C: int n1, n2;
717    // C: checkupval(L, 1, 2, &n1);
718    let (_id1, n1) = check_upval(state, 1, 2, true)?;
719    // C: checkupval(L, 3, 4, &n2);
720    let (_id2, n2) = check_upval(state, 3, 4, true)?;
721    // C: luaL_argcheck(L, !lua_iscfunction(L, 1), 1, "Lua function expected");
722    if state.is_c_function_at(1) {
723        return Err(LuaError::arg_error(1, "Lua function expected"));
724    }
725    // C: luaL_argcheck(L, !lua_iscfunction(L, 3), 3, "Lua function expected");
726    if state.is_c_function_at(3) {
727        return Err(LuaError::arg_error(3, "Lua function expected"));
728    }
729    // C: lua_upvaluejoin(L, 1, n1, 3, n2);
730    state.join_upvalues(1, n1, 3, n2)?;
731    Ok(0)
732}
733
734/// Internal debug hook registered with the VM via `lua_sethook`. When
735/// invoked, it looks up the Lua-side hook function stored in
736/// `registry[HOOKKEY][current_thread]` and calls it with the event name
737/// and current line number.
738///
739/// C: `static void hookf(lua_State *L, lua_Debug *ar)`
740pub(crate) fn hookf(state: &mut LuaState, event: i32, currentline: i32) -> Result<(), LuaError> {
741    // C: lua_getfield(L, LUA_REGISTRYINDEX, HOOKKEY);
742    state.get_registry_field(HOOKKEY)?;
743    // C: lua_pushthread(L);
744    state.push_thread()?;
745    // C: if (lua_rawget(L, -2) == LUA_TFUNCTION) { ... }
746    if state.raw_get(-2)? == LuaType::Function {
747        // C: lua_pushstring(L, hooknames[(int)ar->event]);
748        let event_idx = event.clamp(0, HOOKNAMES.len() as i32 - 1) as usize;
749        let event_str = state.intern_str(HOOKNAMES[event_idx])?;
750        state.push(LuaValue::Str(event_str));
751
752        // C: if (ar->currentline >= 0) lua_pushinteger(L, ar->currentline); else lua_pushnil(L);
753        if currentline >= 0 {
754            state.push(LuaValue::Int(currentline as i64));
755        } else {
756            state.push(LuaValue::Nil);
757        }
758
759        // C: lua_call(L, 2, 0);
760        state.call(2, 0)?;
761    }
762    // The caller (do_::hook) saves/restores the stack top, so any residual
763    // entries (hook table, non-function lookup result) are cleaned up there.
764    Ok(())
765}
766
767/// Convert the string hook-mask (`'c'`/`'r'`/`'l'` characters) and a count
768/// to the integer bitmask used by the VM's `sethook` API.
769///
770/// C: `static int makemask(const char *smask, int count)`
771fn make_mask(smask: &[u8], count: i32) -> u32 {
772    let mut mask: u32 = 0;
773    // C: if (strchr(smask, 'c')) mask |= LUA_MASKCALL;
774    if smask.contains(&b'c') {
775        mask |= MASK_CALL;
776    }
777    // C: if (strchr(smask, 'r')) mask |= LUA_MASKRET;
778    if smask.contains(&b'r') {
779        mask |= MASK_RET;
780    }
781    // C: if (strchr(smask, 'l')) mask |= LUA_MASKLINE;
782    if smask.contains(&b'l') {
783        mask |= MASK_LINE;
784    }
785    // C: if (count > 0) mask |= LUA_MASKCOUNT;
786    if count > 0 {
787        mask |= MASK_COUNT;
788    }
789    mask
790}
791
792/// Convert the integer hook bitmask back to the string representation used in
793/// Lua (`'c'`/`'r'`/`'l'` characters).
794///
795/// C: `static char *unmakemask(int mask, char *smask)`
796fn unmake_mask(mask: u32) -> Vec<u8> {
797    let mut smask = Vec::with_capacity(3);
798    // C: if (mask & LUA_MASKCALL) smask[i++] = 'c'; ...
799    if mask & MASK_CALL != 0 {
800        smask.push(b'c');
801    }
802    if mask & MASK_RET != 0 {
803        smask.push(b'r');
804    }
805    if mask & MASK_LINE != 0 {
806        smask.push(b'l');
807    }
808    smask
809}
810
811/// `debug.sethook([thread,] hook, mask [, count])` — install a debug hook.
812/// Passing nil as `hook` removes the current hook.
813///
814/// C: `static int db_sethook(lua_State *L)`
815pub(crate) fn set_hook(state: &mut LuaState) -> Result<usize, LuaError> {
816    // C: int arg, mask, count; lua_Hook func;
817    // C: lua_State *L1 = getthread(L, &arg);
818    let (arg, other_thread) = getthread(state);
819    let target_is_self = other_thread.is_none();
820
821    let hook_active: bool;
822    let mask: u32;
823    let count: i32;
824
825    // C: if (lua_isnoneornil(L, arg+1)) { lua_settop(L, arg+1); func=NULL; mask=0; count=0; }
826    if matches!(state.type_at(arg + 1), LuaType::None | LuaType::Nil) {
827        lua_vm::api::set_top(state, arg + 1)?;
828        hook_active = false;
829        mask = 0;
830        count = 0;
831    } else {
832        // C: const char *smask = luaL_checkstring(L, arg+2);
833        let smask: Vec<u8> = state.check_arg_string(arg + 2)?.to_vec();
834        // C: luaL_checktype(L, arg+1, LUA_TFUNCTION);
835        state.check_arg_type(arg + 1, LuaType::Function)?;
836        // C: count = (int)luaL_optinteger(L, arg + 3, 0);
837        count = state.opt_arg_integer(arg + 3, 0)? as i32;
838        // C: func = hookf; mask = makemask(smask, count);
839        hook_active = true;
840        mask = make_mask(&smask, count);
841    }
842
843    // C: if (!luaL_getsubtable(L, LUA_REGISTRYINDEX, HOOKKEY)) { /* newly created */ }
844    if !state.get_or_create_registry_subtable(HOOKKEY)? {
845        // Table was just created. Set it up as a weak-keyed table so that
846        // thread keys do not prevent GC of finished threads.
847        // C: lua_pushliteral(L, "k"); lua_setfield(L, -2, "__mode");
848        let k = state.intern_str(b"k")?;
849        state.push(LuaValue::Str(k));
850        state.set_field(-2, b"__mode")?;
851        // C: lua_pushvalue(L, -1); lua_setmetatable(L, -2);
852        state.push_value_at(-1)?;
853        state.set_metatable(-2)?;
854    }
855
856    check_cross_thread_stack(state, target_is_self, 1)?;
857    let target_state = resolve_debug_thread_target(state, &other_thread);
858    match &target_state {
859        DebugThreadTarget::Other(st) => {
860            st.borrow_mut().ensure_stack(1, "stack overflow")?;
861        }
862        DebugThreadTarget::Current => {}
863        DebugThreadTarget::Unavailable => {}
864    }
865
866    // C: lua_pushthread(L1); lua_xmove(L1, L, 1);  /* key = target thread */
867    if target_is_self {
868        state.push_thread()?;
869    } else {
870        // Push the target thread (captured via getthread) as the key. The C
871        // `lua_pushthread(L1); lua_xmove(L1, L, 1)` dance is necessary because
872        // C uses two distinct lua_State pointers; in our impl the GcRef is
873        // already a global reference so we can push it directly on the parent
874        // stack as a Thread value. Without this push, raw_set below operates
875        // on a stack that's missing its key slot and panics in get_table_value.
876        let thr = other_thread.clone().expect("other_thread is Some when target_is_self is false");
877        state.push(lua_types::value::LuaValue::Thread(thr));
878    }
879    // C: lua_pushvalue(L, arg + 1);  /* value = hook function (or nil) */
880    state.push_value_at(arg + 1)?;
881    // C: lua_rawset(L, -3);  /* hooktable[L1] = hook */
882    state.raw_set(-3)?;
883
884    // C: lua_sethook(L1, func, mask, count);
885    let hook_box: Option<Box<dyn FnMut(&mut LuaState, &lua_vm::debug::LuaDebug)>> = if hook_active {
886        Some(Box::new(|st, ar| {
887            let _ = hookf(st, ar.event, ar.currentline);
888        }))
889    } else {
890        None
891    };
892    match target_state {
893        DebugThreadTarget::Current => {
894            lua_vm::debug::set_hook(state, hook_box, mask as i32, count);
895        }
896        DebugThreadTarget::Other(target_state) => {
897            lua_vm::debug::set_hook(&mut target_state.borrow_mut(), hook_box, mask as i32, count);
898        }
899        DebugThreadTarget::Unavailable => {
900            // Main-thread cross-thread targeting from a non-main state is not
901            // yet reachable in this build; record the function in the shared
902            // registry and leave execution on the current thread untouched.
903            return Ok(0);
904        }
905    }
906
907    Ok(0)
908}
909
910/// `debug.gethook([thread])` — return the current hook function, mask string,
911/// and count. Returns the fail value if no hook is installed.
912///
913/// C: `static int db_gethook(lua_State *L)`
914pub(crate) fn get_hook(state: &mut LuaState) -> Result<usize, LuaError> {
915    // C: int arg; lua_State *L1 = getthread(L, &arg);
916    let (_arg, other_thread) = getthread(state);
917    let target_is_self = other_thread.is_none();
918    let target_state = resolve_debug_thread_target(state, &other_thread);
919
920    let (mask, hook_is_set, hook_is_internal, hook_count) = match target_state {
921        DebugThreadTarget::Current => {
922            (
923                state.get_hook_mask(),
924                state.hook_is_set(),
925                state.hook_is_internal_lua_hook(),
926                state.get_hook_count(),
927            )
928        }
929        DebugThreadTarget::Other(target_state) => {
930            let mut target_state = target_state.borrow_mut();
931            (
932                target_state.get_hook_mask(),
933                target_state.hook_is_set(),
934                target_state.hook_is_internal_lua_hook(),
935                target_state.get_hook_count(),
936            )
937        }
938        DebugThreadTarget::Unavailable => (0u32, false, false, 0i32),
939    };
940
941    // C: if (hook == NULL) { luaL_pushfail(L); return 1; }
942    if !hook_is_set {
943        state.push_fail();
944        return Ok(1);
945    }
946
947    // C: else if (hook != hookf)  /* external hook? */
948    //      lua_pushliteral(L, "external hook");
949    if !hook_is_internal {
950        // C: lua_pushliteral(L, "external hook");
951        let s = state.intern_str(b"external hook")?;
952        state.push(LuaValue::Str(s));
953    } else {
954        // C: else { /* hook table must exist */
955        // C:   lua_getfield(L, LUA_REGISTRYINDEX, HOOKKEY);
956        state.get_registry_field(HOOKKEY)?;
957        check_cross_thread_stack(state, target_is_self, 1)?;
958        // C:   lua_pushthread(L1); lua_xmove(L1, L, 1);
959        if target_is_self {
960            state.push_thread();
961        } else {
962            let key_thread = other_thread
963                .expect("other_thread is Some when target_is_self is false")
964                .clone();
965            state.push(lua_types::value::LuaValue::Thread(key_thread));
966        }
967        // C:   lua_rawget(L, -2);  /* 1st result = hooktable[L1] */
968        state.raw_get(-2);
969        // C:   lua_remove(L, -2);  /* remove hook table */
970        state.remove(-2);
971    }
972
973    // C: lua_pushstring(L, unmakemask(mask, buff));  /* 2nd result = mask string */
974    let smask = unmake_mask(mask);
975    let ls = state.intern_str(&smask)?;
976    state.push(LuaValue::Str(ls));
977
978    // C: lua_pushinteger(L, lua_gethookcount(L1));  /* 3rd result = count */
979    state.push(LuaValue::Int(hook_count as i64));
980
981    Ok(3)
982}
983
984/// `debug.debug()` — enter an interactive debug REPL.
985///
986/// Reads Lua source lines from stdin, compiles and runs each one. On EOF or
987/// when the user types `cont`, returns control to the caller. Errors in
988/// commands are printed to stderr and the loop continues.
989///
990/// C: `static int db_debug(lua_State *L)`
991pub(crate) fn debug_interactive(state: &mut LuaState) -> Result<usize, LuaError> {
992    let stdin = io::stdin();
993    loop {
994        // C: lua_writestringerror("%s", "lua_debug> ");
995        eprint!("lua_debug> ");
996        let _ = io::stderr().flush();
997
998        // C: if (fgets(buffer, sizeof(buffer), stdin) == NULL || strcmp(buffer, "cont\n") == 0)
999        //        return 0;
1000        // PORT NOTE: using String for the line buffer is Rust I/O infrastructure,
1001        // not Lua data. The bytes are immediately converted to &[u8] before being
1002        // passed into the Lua API.
1003        let mut line = String::new();
1004        let n = stdin
1005            .lock()
1006            .read_line(&mut line)
1007            .map_err(|e| LuaError::runtime(format_args!("stdin read error: {}", e)))?;
1008
1009        if n == 0 || line == "cont\n" {
1010            return Ok(0);
1011        }
1012
1013        let bytes: &[u8] = line.as_bytes();
1014
1015        // C: if (luaL_loadbuffer(L, buffer, strlen(buffer), "=(debug command)") ||
1016        //        lua_pcall(L, 0, 0, 0))
1017        //      lua_writestringerror("%s\n", luaL_tolstring(L, -1, NULL));
1018        let result = state
1019            .load_buffer(bytes, b"=(debug command)", None)
1020            .and_then(|_| state.protected_call(0, 0, 0));
1021
1022        if let Err(_) = result {
1023            // TODO(port): display the error via state.coerce_to_string(-1) which
1024            // maps to luaL_tolstring. The exact method name for the coercing
1025            // to-string operation and the stderr-write helper need to be established
1026            // in Phase B (lua-vm/src/api.rs).
1027            eprintln!("(error in debug command)");
1028            state.pop_n(1);
1029        }
1030
1031        // C: lua_settop(L, 0);  /* remove eventual returns */
1032        lua_vm::api::set_top(state, 0)?;
1033    }
1034}
1035
1036/// `debug.traceback([thread,] [message [, level]])` — return a traceback string.
1037///
1038/// If `message` is present but is not a string, it is returned unchanged.
1039/// Otherwise a stack traceback is generated and optionally prepended with
1040/// `message`.
1041///
1042/// C: `static int db_traceback(lua_State *L)`
1043pub(crate) fn traceback(state: &mut LuaState) -> Result<usize, LuaError> {
1044    // C: int arg; lua_State *L1 = getthread(L, &arg);
1045    let (arg, other_thread) = getthread(state);
1046    let target_is_self = other_thread.is_none();
1047
1048    // C: const char *msg = lua_tostring(L, arg + 1);
1049    // Immediately clone to Vec<u8> to free the borrow on `state`.
1050    let msg_owned: Option<Vec<u8>> = state
1051        .to_lua_string(arg + 1)
1052        .map(|s: GcRef<LuaString>| s.as_bytes().to_vec());
1053
1054    // C: if (msg == NULL && !lua_isnoneornil(L, arg + 1))
1055    let arg1_ty = state.type_at(arg + 1);
1056    if msg_owned.is_none() && !matches!(arg1_ty, LuaType::None | LuaType::Nil) {
1057        // C: lua_pushvalue(L, arg + 1);  /* return it untouched */
1058        state.push_value_at(arg + 1)?;
1059    } else {
1060        // C: int level = (int)luaL_optinteger(L, arg+2, (L == L1) ? 1 : 0);
1061        let default_level: i64 = if target_is_self { 1 } else { 0 };
1062        let level = state.opt_arg_integer(arg + 2, default_level)? as i32;
1063
1064        // C: luaL_traceback(L, L1, msg, level);
1065        match resolve_debug_thread_target(state, &other_thread) {
1066            DebugThreadTarget::Current => {
1067                crate::auxlib::traceback(state, None, msg_owned.as_deref(), level)?;
1068            }
1069            DebugThreadTarget::Other(target_state) => {
1070                let mut target_state = target_state.borrow_mut();
1071                crate::auxlib::traceback(
1072                    state,
1073                    Some(&mut *target_state),
1074                    msg_owned.as_deref(),
1075                    level,
1076                )?;
1077            }
1078            DebugThreadTarget::Unavailable => {
1079                crate::auxlib::traceback(state, None, msg_owned.as_deref(), level)?;
1080            }
1081        }
1082    }
1083    Ok(1)
1084}
1085
1086/// `debug.setcstacklimit(limit)` — set the C-stack depth limit. Returns the
1087/// old limit, or a platform-specific sentinel when not supported.
1088///
1089/// C: `static int db_setcstacklimit(lua_State *L)`
1090pub(crate) fn set_c_stack_limit(state: &mut LuaState) -> Result<usize, LuaError> {
1091    // C: int limit = (int)luaL_checkinteger(L, 1);
1092    let limit = state.check_arg_integer(1)? as i32;
1093    // C: int res = lua_setcstacklimit(L, limit); lua_pushinteger(L, res); return 1;
1094    let res = state.set_c_stack_limit(limit)?;
1095    state.push(LuaValue::Int(res as i64));
1096    Ok(1)
1097}
1098
1099// ── Library registration ───────────────────────────────────────────────────
1100
1101/// Function registration table for the `debug` library.
1102///
1103/// C: `static const luaL_Reg dblib[]`
1104pub(crate) const DBLIB: &[(&[u8], LibFn)] = &[
1105    (b"debug",          debug_interactive as LibFn),
1106    (b"getuservalue",   get_uservalue     as LibFn),
1107    (b"gethook",        get_hook          as LibFn),
1108    (b"getinfo",        get_info          as LibFn),
1109    (b"getlocal",       get_local         as LibFn),
1110    (b"getregistry",    get_registry      as LibFn),
1111    (b"getmetatable",   get_metatable     as LibFn),
1112    (b"getupvalue",     get_upvalue       as LibFn),
1113    (b"upvaluejoin",    upvalue_join      as LibFn),
1114    (b"upvalueid",      upvalue_id        as LibFn),
1115    (b"setuservalue",   set_uservalue     as LibFn),
1116    (b"sethook",        set_hook          as LibFn),
1117    (b"setlocal",       set_local         as LibFn),
1118    (b"setmetatable",   set_metatable     as LibFn),
1119    (b"setupvalue",     set_upvalue       as LibFn),
1120    (b"traceback",      traceback         as LibFn),
1121    (b"setcstacklimit", set_c_stack_limit as LibFn),
1122];
1123
1124/// Open the `debug` library and push the module table onto the stack.
1125/// Returns 1 (the table).
1126///
1127/// C: `LUAMOD_API int luaopen_debug(lua_State *L)`
1128pub fn open_debug(state: &mut LuaState) -> Result<usize, LuaError> {
1129    // C: luaL_newlib(L, dblib); return 1;
1130    state.new_lib(DBLIB)?;
1131    Ok(1)
1132}
1133
1134// ──────────────────────────────────────────────────────────────────────────
1135// PORT STATUS
1136//   source:        src/ldblib.c  (484 lines, 20 functions)
1137//   target_crate:  lua-stdlib
1138//   confidence:    medium
1139//   todos:         16
1140//   port_notes:    3
1141//   unsafe_blocks: 0
1142//   notes:         Cross-thread ops (lua_xmove / simultaneous &mut LuaState)
1143//                  are the main blockers; all 16 TODOs are in that cluster or
1144//                  in UpvalId (raw-pointer identity). Single-thread paths are
1145//                  faithfully translated. Phase B must define: DebugInfo field
1146//                  accessor methods (source_bytes, short_src_bytes, what_bytes,
1147//                  name_bytes, namewhat_bytes), hook-kind predicates
1148//                  (hook_is_set, hook_is_internal_lua_hook, get_hook_mask,
1149//                  get_hook_count), get_or_create_registry_subtable,
1150//                  get_param_name, get_local_at, set_local_at,
1151//                  upvalue_id (UpvalId type), join_upvalues, lua_traceback,
1152//                  load_buffer, push_registry, get_registry_field, push_fail,
1153//                  push_thread, new_lib, set_hook(Option<HookFn>, u32, i32).
1154// ──────────────────────────────────────────────────────────────────────────