Skip to main content

lua_stdlib/
debug_lib.rs

1//! Debug library — the `debug` Lua standard library module.
2//!
3//! Exposes debug introspection APIs: stack inspection (`getinfo`, `getlocal`,
4//! `setlocal`), upvalue access (`getupvalue`, `setupvalue`, `upvalueid`,
5//! `upvaluejoin`), hook management (`sethook`, `gethook`), metatable overrides
6//! (`getmetatable`, `setmetatable`), userdata values (`getuservalue`,
7//! `setuservalue`), the registry (`getregistry`), and utilities (`traceback`,
8//! `debug`, `setcstacklimit`).
9//!
10//! # Graduation (Idiomatization Sprint 2, Phase 2 — P2-debug, 2026-06-14)
11//!
12//! Most of this module is **VM-introspection plumbing**: `getinfo`/`getlocal`/
13//! `setlocal`/`getupvalue`/`setupvalue`/`upvalueid`/`upvaluejoin`/`sethook`/
14//! `gethook`/`traceback`/`getregistry` reach into `lua-vm`'s call stack,
15//! activation records, upvalue cells, and registry. That cross-crate plumbing
16//! is **load-bearing** — it is idiomatized AROUND (the cold arg-checking, the
17//! `getinfo` result-table assembly, traceback formatting), never refactored in
18//! how it reaches into the VM. The cross-thread `lua_xmove` TODOs and the
19//! `UpvalId` pointer-identity TODO are genuine deferred behavior, kept verbatim.
20//!
21//! Behavioral net (the only oracle — there is no structural one): the official
22//! `db.lua` suite (5.4), `multiversion_oracle`, the version batteries
23//! (`specs/oracle/check.sh 5.1`..`5.5`), and this crate's reference-pinned
24//! `tests/debug_strengthen.rs`. Strengthening that net FIRST caught two real
25//! 5.1 divergences (the 5.2+ `getinfo 'u'` `nparams`/`isvararg` fields and the
26//! 5.2+ function-argument `getlocal` form leaked onto 5.1); both fixed here in
27//! the cold arg-handling surface. See `crates/lua-stdlib/GRADUATED.md` "debug".
28
29use std::cell::RefCell;
30#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
31use std::io::{self, BufRead, Write};
32use std::rc::Rc;
33
34use crate::state_stub::{LuaDebug as DebugInfo, LuaState, LuaStateStubExt as _};
35use lua_types::{GcRef, LuaError, LuaStatus, LuaString, LuaType, LuaValue, LuaVersion};
36
37// ── Constants ──────────────────────────────────────────────────────────────
38
39/// Registry key for the hook table that maps threads to their hook functions.
40///
41const HOOKKEY: &[u8] = b"_HOOKKEY";
42
43/// Hook event names indexed by the raw event code stored in [`DebugInfo::event`].
44/// Order must match the `LUA_HOOK*` constants: Call=0, Return=1, Line=2, Count=3, TailCall=4.
45///
46/// Event code 4 names a tail event whose wording is version-specific: Lua 5.1's
47/// `LUA_HOOKTAILRET` reports `"tail return"` on the return side, while 5.2+'s
48/// `LUA_HOOKTAILCALL` reports `"tail call"` on the call side. This table holds
49/// the 5.2+ name; [`hookf`] substitutes the 5.1 name for code 4.
50const HOOKNAMES: &[&[u8]; 5] = &[b"call", b"return", b"line", b"count", b"tail call"];
51
52/// Lua 5.1's name for hook event code 4 (`LUA_HOOKTAILRET`).
53const HOOKNAME_TAILRET_51: &[u8] = b"tail return";
54
55/// Bitmask constants for hook event selection.
56const MASK_CALL: u32 = 1 << 0;
57const MASK_RET: u32 = 1 << 1;
58const MASK_LINE: u32 = 1 << 2;
59const MASK_COUNT: u32 = 1 << 3;
60
61// ── Local type aliases ─────────────────────────────────────────────────────
62
63/// Entry-point signature for a Lua stdlib function in Rust.
64pub(crate) type LibFn = fn(&mut LuaState) -> Result<usize, LuaError>;
65
66/// A Rust hook callback registered with the Lua VM's hook mechanism.
67///
68/// The hook receives the event code and current line directly (not a debug
69/// record), because the lua-stdlib `DebugInfo` and the canonical
70/// `lua_vm::debug::LuaDebug` are distinct types.
71#[expect(
72    dead_code,
73    reason = "ported stdlib helper; not yet wired into the runtime"
74)]
75pub(crate) type HookFn = fn(&mut LuaState, i32, i32) -> Result<(), LuaError>;
76
77/// Opaque identity handle for an upvalue, used to check whether two
78/// upvalues share the same storage cell.
79///
80/// In C this is a raw pointer into the upvalue's storage cell. Safe Rust
81/// cannot expose a raw pointer outside `lua-gc`, so this uses `usize`
82/// (pointer-sized) as a placeholder so call sites compile; a stable u64 ID
83/// or a `GcRef`-based comparison would be the eventual real design.
84type UpvalId = usize;
85
86#[derive(Clone)]
87enum DebugThreadTarget {
88    Current,
89    Other(Rc<RefCell<LuaState>>),
90    Unavailable,
91}
92
93fn resolve_debug_thread_target(
94    state: &LuaState,
95    target_thread: &Option<GcRef<lua_types::value::LuaThread>>,
96) -> DebugThreadTarget {
97    let Some(thread) = target_thread else {
98        return DebugThreadTarget::Current;
99    };
100
101    if thread.id == state.cached_thread_id {
102        return DebugThreadTarget::Current;
103    }
104
105    let g = state.global();
106    if thread.id == g.main_thread_id {
107        DebugThreadTarget::Unavailable
108    } else {
109        g.threads
110            .get(&thread.id)
111            .map(|entry| DebugThreadTarget::Other(entry.state.clone()))
112            .unwrap_or(DebugThreadTarget::Unavailable)
113    }
114}
115
116// ── Internal helpers ───────────────────────────────────────────────────────
117
118/// Ensure the cross-thread target has room for `n` more stack slots.
119///
120/// When the target is the current thread this is a no-op because the current
121/// thread's stack is managed by the caller. When it is another thread we
122/// must verify its stack, but that requires a simultaneous `&mut LuaState`
123/// for both threads.
124///
125fn check_cross_thread_stack(
126    state: &mut LuaState,
127    target_is_self: bool,
128    n: i32,
129) -> Result<(), LuaError> {
130    if !target_is_self {
131        state.ensure_stack(n, "stack overflow")?;
132    }
133    Ok(())
134}
135
136/// Inspect argument 1: if it is a thread value, return `(1, Some(thread_ref))`;
137/// otherwise return `(0, None)` meaning "operate on the current state".
138///
139fn getthread(state: &mut LuaState) -> (i32, Option<GcRef<lua_types::value::LuaThread>>) {
140    if state.type_at(1) == LuaType::Thread {
141        let thread = state.to_thread_at(1);
142        return (1, thread);
143    }
144    (0, None)
145}
146
147/// Push byte string `v` (or Nil when `v` is `None`) and store it under key
148/// `k` in the table that sits at stack position -2.
149fn settabss(state: &mut LuaState, k: &[u8], v: Option<&[u8]>) -> Result<(), LuaError> {
150    match v {
151        Some(s) => {
152            let ls = state.intern_str(s)?;
153            state.push(LuaValue::Str(ls));
154        }
155        None => {
156            state.push(LuaValue::Nil);
157        }
158    }
159    state.set_field(-2, k)
160}
161
162/// Push integer `v` and store it under key `k` in the table at -2.
163///
164fn settabsi(state: &mut LuaState, k: &[u8], v: i32) -> Result<(), LuaError> {
165    state.push(LuaValue::Int(v as i64));
166    state.set_field(-2, k)
167}
168
169/// Push boolean `v` and store it under key `k` in the table at -2.
170///
171fn settabsb(state: &mut LuaState, k: &[u8], v: bool) -> Result<(), LuaError> {
172    state.push(LuaValue::Bool(v));
173    state.set_field(-2, k)
174}
175
176/// After `lua_getinfo` has pushed a result ('f' function or 'L' line table)
177/// onto L1's stack, move it into the result table on L as field `fname`.
178///
179/// When target is self, the value is already on our stack; rotate to bring
180/// it above the result table.
181///
182/// `target_is_self` is always `true` at every current call site: the real
183/// cross-thread case (target is a different, reachable thread) is handled by
184/// [`move_stack_option_from_target`] instead, which xmoves the value off the
185/// target's own stack. The `false` arm here is unreachable dead code kept
186/// only because a `target_is_self` caller could in principle exist; it must
187/// never be exercised by a genuine cross-thread borrow (that always goes
188/// through `move_stack_option_from_target`), so it pushes `Nil` rather than
189/// silently fabricating a value.
190fn treat_stack_option(
191    state: &mut LuaState,
192    target_is_self: bool,
193    fname: &[u8],
194) -> Result<(), LuaError> {
195    if target_is_self {
196        state.rotate(-2, 1)?;
197    } else {
198        state.push(LuaValue::Nil);
199    }
200    state.set_field(-2, fname)
201}
202
203fn move_stack_option_from_target(
204    state: &mut LuaState,
205    target: &mut LuaState,
206    fname: &[u8],
207) -> Result<(), LuaError> {
208    let val = target.get_at(target.top_idx() - 1);
209    target.pop_n(1);
210    state.push(val);
211    state.set_field(-2, fname)
212}
213
214// ── Library functions ──────────────────────────────────────────────────────
215
216/// `debug.getregistry()` — return the Lua registry table.
217///
218pub(crate) fn get_registry(state: &mut LuaState) -> Result<usize, LuaError> {
219    state.push_registry()?;
220    Ok(1)
221}
222
223/// `debug.getmetatable(obj)` — return the metatable of `obj`, or nil if none.
224///
225pub(crate) fn get_metatable(state: &mut LuaState) -> Result<usize, LuaError> {
226    state.check_arg_any(1)?;
227    if !state.get_metatable(1)? {
228        state.push(LuaValue::Nil);
229    }
230    Ok(1)
231}
232
233/// `debug.setmetatable(obj, table)` — set `table` (or nil) as `obj`'s metatable.
234/// Returns the first argument `obj`.
235///
236pub(crate) fn set_metatable(state: &mut LuaState) -> Result<usize, LuaError> {
237    let t = state.type_at(2);
238    if !(t == LuaType::Nil || t == LuaType::Table) {
239        let got = state.arg(2);
240        return Err(LuaError::type_arg_error(2, "nil or table", &got));
241    }
242    lua_vm::api::set_top(state, 2)?;
243    state.set_metatable(1)?;
244    Ok(1)
245}
246
247/// `debug.getuservalue(obj [, n])` — return the n-th user value of userdata
248/// `obj` plus `true`, or the fail value if `obj` is not userdata or `n` is out
249/// of range.
250///
251pub(crate) fn get_uservalue(state: &mut LuaState) -> Result<usize, LuaError> {
252    let n = state.opt_arg_integer(2, 1)? as i32;
253    if state.type_at(1) != LuaType::UserData {
254        state.push_fail()?;
255        return Ok(1);
256    }
257    let ty = state.get_iuservalue(1, n)?;
258    if ty != LuaType::None {
259        state.push(LuaValue::Bool(true));
260        return Ok(2);
261    }
262    Ok(1)
263}
264
265/// `debug.setuservalue(obj, value [, n])` — set the n-th user value of userdata
266/// `obj` to `value`. Returns `obj`, or the fail value on failure.
267///
268pub(crate) fn set_uservalue(state: &mut LuaState) -> Result<usize, LuaError> {
269    let n = state.opt_arg_integer(3, 1)? as i32;
270    state.check_arg_type(1, LuaType::UserData)?;
271    state.check_arg_any(2)?;
272    lua_vm::api::set_top(state, 2)?;
273    if !state.set_iuservalue(1, n)? {
274        state.push_fail()?;
275    }
276    Ok(1)
277}
278
279/// `debug.getinfo([thread,] f|level [, what])` — collect debug information
280/// about function `f` or stack level `level` into a new table. The `what`
281/// string selects which fields to populate (default `"flnSrtu"`).
282///
283pub(crate) fn get_info(state: &mut LuaState) -> Result<usize, LuaError> {
284    let mut ar = DebugInfo::default();
285
286    let (arg, other_thread) = getthread(state);
287    let target_is_self = other_thread.is_none();
288    let target_state = resolve_debug_thread_target(state, &other_thread);
289
290    // The default option string is version-gated, mirroring `db_getinfo`'s
291    // `luaL_optstring(L, arg+2, ...)` default: 5.1 has no tail-call ('t') or
292    // transfer ('r') info (`flnSu`); 5.2/5.3 add 't' (`flnStu`); 5.4/5.5 add 'r'
293    // (`flnSrtu`). The per-version accepted-option set is enforced separately in
294    // the VM's `aux_get_info`.
295    let default_opts: &[u8] = match state.global().lua_version {
296        LuaVersion::V51 => b"flnSu",
297        LuaVersion::V52 | LuaVersion::V53 => b"flnStu",
298        _ => b"flnSrtu",
299    };
300    // to_vec() immediately to avoid borrow-checker conflict with subsequent &mut state ops.
301    let raw_opts: Vec<u8> = state.opt_arg_string(arg + 2, default_opts)?.to_vec();
302
303    check_cross_thread_stack(state, target_is_self, 3)?;
304
305    if raw_opts.first() == Some(&b'>') {
306        // 5.4/5.5 reject a leading '>' explicitly with `luaL_argcheck(...,
307        // "invalid option '>'")`. Pre-5.4 has no such early check: a leading '>'
308        // instead flows into the natural path where `aux_get_info` reports the
309        // bare `invalid option`. The explicit guard is kept on every version
310        // (the function value it would otherwise consume off the stack is not
311        // present here), but the message matches the target era.
312        let msg: &[u8] = match state.global().lua_version {
313            LuaVersion::V54 | LuaVersion::V55 => b"invalid option '>'",
314            _ => b"invalid option",
315        };
316        return Err(lua_vm::debug::arg_error_impl(state, arg + 2, msg));
317    }
318
319    // Build the effective options string, prepending '>' when the subject is a function.
320    let options: Vec<u8>;
321    let info_target_owner: Option<Rc<RefCell<LuaState>>>;
322    let mut info_target: Option<crate::coro_lib::RootedThreadBorrow<'_>> = None;
323    let mut info_target_is_self = target_is_self;
324
325    if state.type_at(arg + 1) == LuaType::Function {
326        let mut prefixed = Vec::with_capacity(raw_opts.len() + 1);
327        prefixed.push(b'>');
328        prefixed.extend_from_slice(&raw_opts);
329        options = prefixed;
330
331        // Function-form getinfo is thread-INDEPENDENT: it inspects the closure's
332        // proto (source, line-defined, nparams, upvalue count, the function value
333        // itself, its active-lines table), none of which depends on which thread
334        // is nominated. C runs `lua_getinfo(L1, ">...")` only because it xmoved
335        // the function onto L1, but the result is identical on any thread. So we
336        // always push the function onto the CURRENT `state` and inspect it there,
337        // for every DebugThreadTarget — never borrowing the nominated thread. That
338        // both avoids a redundant borrow and sidesteps a "RefCell already
339        // borrowed" panic when the nominated thread is an actively-suspended
340        // ancestor mid-resume (its cell is already mutably borrowed up the call
341        // stack). Target-thread borrowing is reserved for the level-based form,
342        // which genuinely reads that thread's live call stack.
343        info_target_is_self = true;
344        state.push_value_at(arg + 1)?;
345        if state.get_debug_info(&options, &mut ar).is_err() {
346            return Err(lua_vm::debug::arg_error_impl(
347                state,
348                arg + 2,
349                b"invalid option",
350            ));
351        }
352    } else {
353        options = raw_opts;
354
355        let level = state.check_arg_integer(arg + 1)? as i32;
356        match target_state {
357            DebugThreadTarget::Current | DebugThreadTarget::Unavailable => {
358                info_target_is_self = true;
359                if !state.get_stack_level(level, &mut ar) {
360                    state.push_fail()?;
361                    return Ok(1);
362                }
363
364                if state.get_debug_info(&options, &mut ar).is_err() {
365                    return Err(lua_vm::debug::arg_error_impl(
366                        state,
367                        arg + 2,
368                        b"invalid option",
369                    ));
370                }
371            }
372            DebugThreadTarget::Other(target_state) => {
373                info_target_owner = Some(target_state);
374                let mut target = crate::coro_lib::borrow_thread_rooted(
375                    state,
376                    info_target_owner
377                        .as_ref()
378                        .expect("target owner just stored"),
379                );
380                if !target.get_stack_level(level, &mut ar) {
381                    state.push_fail()?;
382                    return Ok(1);
383                }
384                if target.get_debug_info(&options, &mut ar).is_err() {
385                    return Err(lua_vm::debug::arg_error_impl(
386                        state,
387                        arg + 2,
388                        b"invalid option",
389                    ));
390                }
391                target.resnapshot();
392                info_target = Some(target);
393            }
394        }
395    }
396
397    let result_tbl = state.new_table();
398    state.push(LuaValue::Table(result_tbl));
399
400    if options.contains(&b'S') {
401        let src = state.intern_str(ar.source_bytes())?;
402        state.push(LuaValue::Str(src));
403        state.set_field(-2, b"source")?;
404
405        settabss(state, b"short_src", Some(ar.short_src_bytes()))?;
406        settabsi(state, b"linedefined", ar.linedefined)?;
407        settabsi(state, b"lastlinedefined", ar.lastlinedefined)?;
408        settabss(state, b"what", Some(ar.what_bytes()))?;
409    }
410    if options.contains(&b'l') {
411        settabsi(state, b"currentline", ar.currentline)?;
412    }
413    if options.contains(&b'u') {
414        settabsi(state, b"nups", ar.nups as i32)?;
415        if !matches!(state.global().lua_version, LuaVersion::V51) {
416            settabsi(state, b"nparams", ar.nparams as i32)?;
417            settabsb(state, b"isvararg", ar.isvararg)?;
418        }
419    }
420    if options.contains(&b'n') {
421        let name_opt: Option<&[u8]> = ar.name.as_deref();
422        settabss(state, b"name", name_opt)?;
423        settabss(state, b"namewhat", Some(ar.namewhat_bytes()))?;
424    }
425    if options.contains(&b'r') {
426        settabsi(state, b"ftransfer", ar.ftransfer as i32)?;
427        settabsi(state, b"ntransfer", ar.ntransfer as i32)?;
428    }
429    if options.contains(&b't') {
430        settabsb(state, b"istailcall", ar.istailcall)?;
431        if matches!(state.global().lua_version, LuaVersion::V55) {
432            settabsi(state, b"extraargs", ar.extraargs as i32)?;
433        }
434    }
435    // The 'f' (function) and 'L' (active-lines table) results were pushed by
436    // get_debug_info in that order — function first, line-table on top — so they
437    // must be moved into the result table top-first: 'L' here, then 'f'. This
438    // ordering is load-bearing regardless of the option-string order.
439    if options.contains(&b'L') {
440        if info_target_is_self {
441            treat_stack_option(state, true, b"activelines")?;
442        } else if let Some(target) = info_target.as_mut() {
443            move_stack_option_from_target(state, &mut **target, b"activelines")?;
444        } else {
445            state.push(LuaValue::Nil);
446            state.set_field(-2, b"activelines")?;
447        }
448    }
449    if options.contains(&b'f') {
450        if info_target_is_self {
451            treat_stack_option(state, true, b"func")?;
452        } else if let Some(target) = info_target.as_mut() {
453            move_stack_option_from_target(state, &mut **target, b"func")?;
454        } else {
455            state.push(LuaValue::Nil);
456            state.set_field(-2, b"func")?;
457        }
458    }
459
460    Ok(1)
461}
462
463/// Whether `debug.getlocal` accepts a function as its first argument (the
464/// parameter-name introspection form).
465///
466/// This form is a 5.2 addition (the `lua_isfunction(L, arg+1)` branch in
467/// `ldblib.c` `db_getlocal`). On 5.1 there is no such branch: a function
468/// argument is fed straight to `luaL_checkint`, which raises
469/// `number expected, got function`. Returning `false` here lets the function
470/// argument fall through to the integer-level path so 5.1 reproduces that error.
471/// (`db_setlocal` has no function form on any version, so this gate is
472/// `getlocal`-only.)
473fn function_arg_form_supported(state: &LuaState) -> bool {
474    !matches!(state.global().lua_version, LuaVersion::V51)
475}
476
477/// `debug.getlocal([thread,] level, local)` — return the name and value of
478/// local variable `local` at stack level `level`.
479///
480/// On 5.2+ the first argument may be a function, in which case only the
481/// parameter name at position `local` is returned (no value); see
482/// [`function_arg_form_supported`].
483///
484pub(crate) fn get_local(state: &mut LuaState) -> Result<usize, LuaError> {
485    let (arg, other_thread) = getthread(state);
486    let target_state = resolve_debug_thread_target(state, &other_thread);
487
488    let nvar = state.check_arg_integer(arg + 2)? as i32;
489
490    if function_arg_form_supported(state) && state.type_at(arg + 1) == LuaType::Function {
491        state.push_value_at(arg + 1)?;
492        let name = state.get_param_name(0, nvar)?;
493        match name {
494            Some(n) => {
495                let ls = state.intern_str(&n)?;
496                state.push(LuaValue::Str(ls));
497            }
498            None => {
499                state.push(LuaValue::Nil);
500            }
501        }
502        return Ok(1);
503    }
504
505    // Stack-level path.
506    let level = state.check_arg_integer(arg + 1)? as i32;
507    let mut ar = DebugInfo::default();
508
509    let name = match target_state {
510        DebugThreadTarget::Current | DebugThreadTarget::Unavailable => {
511            if !state.get_stack_level(level, &mut ar) {
512                return Err(lua_vm::debug::arg_error_impl(
513                    state,
514                    arg + 1,
515                    b"level out of range",
516                ));
517            }
518            check_cross_thread_stack(state, true, 1)?;
519            // Pushes the local's value onto L1's stack and returns its name.
520            state.get_local_at(&ar, nvar)?
521        }
522        DebugThreadTarget::Other(target_state) => {
523            let mut target = crate::coro_lib::borrow_thread_rooted(state, &target_state);
524            if !target.get_stack_level(level, &mut ar) {
525                return Err(lua_vm::debug::arg_error_impl(
526                    state,
527                    arg + 1,
528                    b"level out of range",
529                ));
530            }
531            check_cross_thread_stack(state, false, 1)?;
532            let name = target.get_local_at(&ar, nvar)?;
533            if name.is_some() {
534                let val = target.get_at(target.top_idx() - 1);
535                target.pop_n(1);
536                state.push(val);
537            }
538            name
539        }
540    };
541
542    if let Some(n) = name {
543        let ls = state.intern_str(&n)?;
544        state.push(LuaValue::Str(ls));
545        state.rotate(-2, 1)?;
546        Ok(2)
547    } else {
548        state.push_fail()?;
549        Ok(1)
550    }
551}
552
553/// `debug.setlocal([thread,] level, local, value)` — set local variable
554/// `local` at stack level `level` to `value`. Returns the variable name, or
555/// nil on failure.
556///
557pub(crate) fn set_local(state: &mut LuaState) -> Result<usize, LuaError> {
558    let (arg, other_thread) = getthread(state);
559    let target_state = resolve_debug_thread_target(state, &other_thread);
560
561    let level = state.check_arg_integer(arg + 1)? as i32;
562    let nvar = state.check_arg_integer(arg + 2)? as i32;
563
564    let mut ar = DebugInfo::default();
565
566    state.check_arg_any(arg + 3)?;
567    lua_vm::api::set_top(state, arg + 3)?;
568
569    let name = match target_state {
570        DebugThreadTarget::Current | DebugThreadTarget::Unavailable => {
571            if !state.get_stack_level(level, &mut ar) {
572                return Err(lua_vm::debug::arg_error_impl(
573                    state,
574                    arg + 1,
575                    b"level out of range",
576                ));
577            }
578            check_cross_thread_stack(state, true, 1)?;
579            let name = state.set_local_at(&ar, nvar)?;
580            if name.is_none() {
581                state.pop_n(1);
582            }
583            name
584        }
585        DebugThreadTarget::Other(target_state) => {
586            let new_val = state.get_at(state.top_idx() - 1);
587            let mut target = crate::coro_lib::borrow_thread_rooted(state, &target_state);
588            if !target.get_stack_level(level, &mut ar) {
589                return Err(lua_vm::debug::arg_error_impl(
590                    state,
591                    arg + 1,
592                    b"level out of range",
593                ));
594            }
595            check_cross_thread_stack(state, false, 1)?;
596            target.push(new_val);
597            let name = target.set_local_at(&ar, nvar)?;
598            if name.is_none() {
599                target.pop_n(1);
600            }
601            state.pop_n(1);
602            name
603        }
604    };
605
606    match name {
607        Some(n) => {
608            let ls = state.intern_str(&n)?;
609            state.push(LuaValue::Str(ls));
610        }
611        None => {
612            state.push(LuaValue::Nil);
613        }
614    }
615    Ok(1)
616}
617
618/// Shared implementation for `get_upvalue` and `set_upvalue`.
619///
620/// When `get` is `true`, retrieves upvalue `n` of the function at stack index 1,
621/// pushes its value, and returns `(name, value)` — 2 results.
622///
623/// When `get` is `false`, pops the top stack value and installs it as upvalue
624/// `n`, returning `(name,)` — 1 result.
625///
626/// Returns 0 results when the upvalue index is out of range.
627///
628fn aux_upvalue(state: &mut LuaState, get: bool) -> Result<usize, LuaError> {
629    let n = state.check_arg_integer(2)? as i32;
630    state.check_arg_type(1, LuaType::Function)?;
631
632    let name: Option<Vec<u8>> = if get {
633        // lua_getupvalue pushes the upvalue value and returns the name.
634        state.get_upvalue(1, n)?
635    } else {
636        // lua_setupvalue pops the top-of-stack value, sets upvalue n, returns name.
637        state.set_upvalue(1, n)?
638    };
639
640    let name_ref = match name {
641        Some(n) => n,
642        None => return Ok(0),
643    };
644
645    let ls = state.intern_str(&name_ref)?;
646    state.push(LuaValue::Str(ls));
647
648    // When get=true: stack is [..., value, name]; insert at -2 → [..., name, value].
649    // When get=false: insert at -1 is a no-op; stack is [..., name].
650    if get {
651        state.insert(-2)?;
652    }
653
654    Ok(if get { 2 } else { 1 })
655}
656
657/// `debug.getupvalue(f, up)` — return the name and value of upvalue `up` of `f`.
658///
659pub(crate) fn get_upvalue(state: &mut LuaState) -> Result<usize, LuaError> {
660    aux_upvalue(state, true)
661}
662
663/// `debug.setupvalue(f, up, value)` — set upvalue `up` of `f` to `value`.
664/// Returns the upvalue name.
665///
666pub(crate) fn set_upvalue(state: &mut LuaState) -> Result<usize, LuaError> {
667    state.check_arg_any(3)?;
668    aux_upvalue(state, false)
669}
670
671/// Verify that upvalue `argnup` of function at stack index `argf` exists.
672/// Returns the opaque identity handle and the upvalue index.
673/// If `require_valid` is true, raises an arg error when the upvalue is absent.
674///
675fn check_upval(
676    state: &mut LuaState,
677    argf: i32,
678    argnup: i32,
679    require_valid: bool,
680) -> Result<(Option<UpvalId>, i32), LuaError> {
681    let nup = state.check_arg_integer(argnup)? as i32;
682    state.check_arg_type(argf, LuaType::Function)?;
683    let id: Option<UpvalId> = match state.upvalue_id(argf, nup) {
684        Ok(p) if p.is_null() => None,
685        Ok(p) => Some(p as usize),
686        Err(_) => None,
687    };
688    if require_valid && id.is_none() {
689        return Err(lua_vm::debug::arg_error_impl(
690            state,
691            argnup,
692            b"invalid upvalue index",
693        ));
694    }
695    Ok((id, nup))
696}
697
698/// `debug.upvalueid(f, n)` — return a unique identifier for upvalue `n` of
699/// function `f` as a light userdata.
700///
701/// On 5.1/5.2/5.3 an out-of-range upvalue index raises
702/// `bad argument #2 ... (invalid upvalue index)` because those versions feed
703/// the index straight to `lua_upvalueid`, which asserts the index is in range.
704/// On 5.4/5.5 the index is validated and an out-of-range index returns the fail
705/// value instead, so the validity check is gated to the legacy/transitional
706/// versions.
707pub(crate) fn upvalue_id(state: &mut LuaState) -> Result<usize, LuaError> {
708    let require_valid = matches!(
709        state.global().lua_version,
710        LuaVersion::V51 | LuaVersion::V52 | LuaVersion::V53
711    );
712    let (id, _nup) = check_upval(state, 1, 2, require_valid)?;
713    match id {
714        Some(uid) => {
715            lua_vm::api::push_light_userdata(state, uid as *mut core::ffi::c_void);
716        }
717        None => {
718            state.push_fail()?;
719        }
720    }
721    Ok(1)
722}
723
724/// `debug.upvaluejoin(f1, n1, f2, n2)` — make upvalue `n1` of function `f1`
725/// refer to the same storage as upvalue `n2` of function `f2`.
726///
727pub(crate) fn upvalue_join(state: &mut LuaState) -> Result<usize, LuaError> {
728    let (_id1, n1) = check_upval(state, 1, 2, true)?;
729    let (_id2, n2) = check_upval(state, 3, 4, true)?;
730    if state.is_c_function_at(1) {
731        return Err(lua_vm::debug::arg_error_impl(
732            state,
733            1,
734            b"Lua function expected",
735        ));
736    }
737    if state.is_c_function_at(3) {
738        return Err(lua_vm::debug::arg_error_impl(
739            state,
740            3,
741            b"Lua function expected",
742        ));
743    }
744    state.join_upvalues(1, n1, 3, n2)?;
745    Ok(0)
746}
747
748/// Internal debug hook registered with the VM via `lua_sethook`. When
749/// invoked, it looks up the Lua-side hook function stored in
750/// `registry[HOOKKEY][current_thread]` and calls it with the event name
751/// and current line number.
752///
753pub(crate) fn hookf(state: &mut LuaState, event: i32, currentline: i32) -> Result<(), LuaError> {
754    state.get_registry_field(HOOKKEY)?;
755    state.push_thread()?;
756    if state.raw_get(-2)? == LuaType::Function {
757        let event_idx = event.clamp(0, HOOKNAMES.len() as i32 - 1) as usize;
758        let name = if event_idx == 4
759            && state.global().lua_version == lua_types::LuaVersion::V51
760        {
761            HOOKNAME_TAILRET_51
762        } else {
763            HOOKNAMES[event_idx]
764        };
765        let event_str = state.intern_str(name)?;
766        state.push(LuaValue::Str(event_str));
767
768        if currentline >= 0 {
769            state.push(LuaValue::Int(currentline as i64));
770        } else {
771            state.push(LuaValue::Nil);
772        }
773
774        state.call(2, 0)?;
775    }
776    // The caller (do_::hook) saves/restores the stack top, so any residual
777    // entries (hook table, non-function lookup result) are cleaned up there.
778    Ok(())
779}
780
781/// Convert the string hook-mask (`'c'`/`'r'`/`'l'` characters) and a count
782/// to the integer bitmask used by the VM's `sethook` API.
783///
784fn make_mask(smask: &[u8], count: i32) -> u32 {
785    let mut mask: u32 = 0;
786    if smask.contains(&b'c') {
787        mask |= MASK_CALL;
788    }
789    if smask.contains(&b'r') {
790        mask |= MASK_RET;
791    }
792    if smask.contains(&b'l') {
793        mask |= MASK_LINE;
794    }
795    if count > 0 {
796        mask |= MASK_COUNT;
797    }
798    mask
799}
800
801/// Convert the integer hook bitmask back to the string representation used in
802/// Lua (`'c'`/`'r'`/`'l'` characters).
803///
804fn unmake_mask(mask: u32) -> Vec<u8> {
805    let mut smask = Vec::with_capacity(3);
806    if mask & MASK_CALL != 0 {
807        smask.push(b'c');
808    }
809    if mask & MASK_RET != 0 {
810        smask.push(b'r');
811    }
812    if mask & MASK_LINE != 0 {
813        smask.push(b'l');
814    }
815    smask
816}
817
818/// `debug.sethook([thread,] hook, mask [, count])` — install a debug hook.
819/// Passing nil as `hook` removes the current hook.
820///
821pub(crate) fn set_hook(state: &mut LuaState) -> Result<usize, LuaError> {
822    let (arg, other_thread) = getthread(state);
823    let target_is_self = other_thread.is_none();
824
825    let hook_active: bool;
826    let mask: u32;
827    let count: i32;
828
829    if matches!(state.type_at(arg + 1), LuaType::None | LuaType::Nil) {
830        lua_vm::api::set_top(state, arg + 1)?;
831        hook_active = false;
832        mask = 0;
833        count = 0;
834    } else {
835        let smask: Vec<u8> = state.check_arg_string(arg + 2)?.to_vec();
836        state.check_arg_type(arg + 1, LuaType::Function)?;
837        count = state.opt_arg_integer(arg + 3, 0)? as i32;
838        hook_active = true;
839        mask = make_mask(&smask, count);
840    }
841
842    if !state.get_or_create_registry_subtable(HOOKKEY)? {
843        // Table was just created. Set it up as a weak-keyed table so that
844        // thread keys do not prevent GC of finished threads.
845        let k = state.intern_str(b"k")?;
846        state.push(LuaValue::Str(k));
847        state.set_field(-2, b"__mode")?;
848        state.push_value_at(-1)?;
849        state.set_metatable(-2)?;
850    }
851
852    check_cross_thread_stack(state, target_is_self, 1)?;
853    let target_state = resolve_debug_thread_target(state, &other_thread);
854    match &target_state {
855        DebugThreadTarget::Other(st) => {
856            st.borrow_mut().ensure_stack(1, "stack overflow")?;
857        }
858        DebugThreadTarget::Current => {}
859        DebugThreadTarget::Unavailable => {}
860    }
861
862    if target_is_self {
863        state.push_thread()?;
864    } else {
865        // Push the target thread (captured via getthread) as the key. The C
866        // `lua_pushthread(L1); lua_xmove(L1, L, 1)` dance is necessary because
867        // C uses two distinct lua_State pointers; in our impl the GcRef is
868        // already a global reference so we can push it directly on the parent
869        // stack as a Thread value. Without this push, raw_set below operates
870        // on a stack that's missing its key slot and panics in get_table_value.
871        let thr = other_thread
872            .clone()
873            .expect("other_thread is Some when target_is_self is false");
874        state.push(lua_types::value::LuaValue::Thread(thr));
875    }
876    state.push_value_at(arg + 1)?;
877    state.raw_set(-3)?;
878
879    let hook_box: Option<Box<dyn FnMut(&mut LuaState, &lua_vm::debug::LuaDebug)>> = if hook_active {
880        Some(Box::new(|st, ar| {
881            let _ = hookf(st, ar.event, ar.currentline);
882        }))
883    } else {
884        None
885    };
886    match target_state {
887        DebugThreadTarget::Current => {
888            lua_vm::debug::set_hook(state, hook_box, mask as i32, count);
889        }
890        DebugThreadTarget::Other(target_state) => {
891            lua_vm::debug::set_hook(&mut target_state.borrow_mut(), hook_box, mask as i32, count);
892        }
893        DebugThreadTarget::Unavailable => {
894            // Main-thread cross-thread targeting from a non-main state is not
895            // yet reachable in this build; record the function in the shared
896            // registry and leave execution on the current thread untouched.
897            return Ok(0);
898        }
899    }
900
901    Ok(0)
902}
903
904/// `debug.gethook([thread])` — return the current hook function, mask string,
905/// and count. Returns the fail value if no hook is installed.
906///
907pub(crate) fn get_hook(state: &mut LuaState) -> Result<usize, LuaError> {
908    let (_arg, other_thread) = getthread(state);
909    let target_is_self = other_thread.is_none();
910    let target_state = resolve_debug_thread_target(state, &other_thread);
911
912    let (mask, hook_is_set, hook_is_internal, hook_count) = match target_state {
913        DebugThreadTarget::Current => (
914            state.get_hook_mask(),
915            state.hook_is_set(),
916            state.hook_is_internal_lua_hook(),
917            state.get_hook_count(),
918        ),
919        DebugThreadTarget::Other(target_state) => {
920            let mut target_state = target_state.borrow_mut();
921            (
922                target_state.get_hook_mask(),
923                target_state.hook_is_set(),
924                target_state.hook_is_internal_lua_hook(),
925                target_state.get_hook_count(),
926            )
927        }
928        DebugThreadTarget::Unavailable => (0u32, false, false, 0i32),
929    };
930
931    if !hook_is_set {
932        state.push_fail()?;
933        return Ok(1);
934    }
935
936    if !hook_is_internal {
937        let s = state.intern_str(b"external hook")?;
938        state.push(LuaValue::Str(s));
939    } else {
940        state.get_registry_field(HOOKKEY)?;
941        check_cross_thread_stack(state, target_is_self, 1)?;
942        if target_is_self {
943            state.push_thread()?;
944        } else {
945            let key_thread = other_thread
946                .expect("other_thread is Some when target_is_self is false")
947                .clone();
948            state.push(lua_types::value::LuaValue::Thread(key_thread));
949        }
950        state.raw_get(-2)?;
951        state.remove(-2)?;
952    }
953
954    let smask = unmake_mask(mask);
955    let ls = state.intern_str(&smask)?;
956    state.push(LuaValue::Str(ls));
957
958    state.push(LuaValue::Int(hook_count as i64));
959
960    Ok(3)
961}
962
963/// `debug.debug()` — enter an interactive debug REPL.
964///
965/// Reads Lua source lines from stdin, compiles and runs each one. On EOF or
966/// when the user types `cont`, returns control to the caller. Errors in
967/// commands are printed to stderr and the loop continues.
968///
969pub(crate) fn debug_interactive(state: &mut LuaState) -> Result<usize, LuaError> {
970    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
971    {
972        let _ = state;
973        return Err(LuaError::runtime(format_args!(
974            "debug.debug interactive stdin not available in this host"
975        )));
976    }
977
978    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
979    {
980        let stdin = io::stdin();
981        loop {
982            eprint!("lua_debug> ");
983            let _ = io::stderr().flush();
984
985            // The `String` line buffer is Rust I/O infrastructure, not Lua data:
986            // its bytes are handed to the Lua API as `&[u8]` immediately below.
987            let mut line = String::new();
988            let n = stdin
989                .lock()
990                .read_line(&mut line)
991                .map_err(|e| LuaError::runtime(format_args!("stdin read error: {}", e)))?;
992
993            if n == 0 || line == "cont\n" {
994                return Ok(0);
995            }
996
997            let bytes: &[u8] = line.as_bytes();
998
999            let load_status = state.load_buffer(bytes, b"=(debug command)", None)?;
1000            let result: Result<(), LuaError> = if load_status == LuaStatus::Ok {
1001                state.protected_call(0, 0, 0)
1002            } else {
1003                // Unlike `protected_call`'s failures (which the port's `pcall`
1004                // machinery embeds in the `Err` itself, popping the Lua-stack
1005                // copy before returning), a syntax error from `load_buffer`
1006                // leaves the message on top of the stack, matching
1007                // `luaL_loadbuffer`'s convention. Without this branch the code
1008                // fell through to `protected_call` unconditionally, calling
1009                // whatever the failed load left behind (the error message
1010                // string) and reporting "attempt to call a string value"
1011                // instead of the syntax error.
1012                let top = state.top_idx();
1013                let msg = state.get_at(top - 1);
1014                state.pop_n(1);
1015                Err(LuaError::Syntax(msg))
1016            };
1017
1018            if let Err(e) = result {
1019                // The error value must be back on the stack (rather than read
1020                // from `e` directly) before stringifying it the way the
1021                // reference `db_debug` does.
1022                let val = e.into_value();
1023                state.push(val);
1024                // The stringification is era-split, mirroring `db_debug`:
1025                // 5.4/5.5 use `luaL_tolstring` (honors `__tostring`, and renders
1026                // other non-string objects as `type: 0x…`); 5.1-5.3 use plain
1027                // `lua_tostring`, which yields the value only for a string or a
1028                // number and otherwise NULL — printed via `lua_writestringerror`
1029                // as the literal `(null)`.
1030                let msg: Vec<u8> = if matches!(
1031                    state.global().lua_version,
1032                    LuaVersion::V54 | LuaVersion::V55
1033                ) {
1034                    crate::auxlib::to_lua_string(state, -1)?
1035                } else {
1036                    // `lua_tostring` returning `None` is the meaningful
1037                    // "not a string or number" signal (C's NULL), which
1038                    // `lua_writestringerror("%s\n", NULL)` renders as `(null)`.
1039                    match state.to_lua_string_bytes(-1) {
1040                        Some(bytes) => bytes,
1041                        None => b"(null)".to_vec(),
1042                    }
1043                };
1044                let mut err = io::stderr();
1045                let _ = err.write_all(&msg);
1046                let _ = err.write_all(b"\n");
1047                let _ = err.flush();
1048            }
1049
1050            lua_vm::api::set_top(state, 0)?;
1051        }
1052    }
1053}
1054
1055/// `debug.traceback([thread,] [message [, level]])` — return a traceback string.
1056///
1057/// If `message` is present but is not a string, it is returned unchanged.
1058/// Otherwise a stack traceback is generated and optionally prepended with
1059/// `message`.
1060///
1061pub(crate) fn traceback(state: &mut LuaState) -> Result<usize, LuaError> {
1062    let (arg, other_thread) = getthread(state);
1063    let target_is_self = other_thread.is_none();
1064
1065    // Immediately clone to Vec<u8> to free the borrow on `state`.
1066    let msg_owned: Option<Vec<u8>> = state
1067        .to_lua_string(arg + 1)
1068        .map(|s: GcRef<LuaString>| s.as_bytes().to_vec());
1069
1070    let arg1_ty = state.type_at(arg + 1);
1071    if msg_owned.is_none() && !matches!(arg1_ty, LuaType::None | LuaType::Nil) {
1072        state.push_value_at(arg + 1)?;
1073    } else {
1074        let default_level: i64 = if target_is_self { 1 } else { 0 };
1075        let level = state.opt_arg_integer(arg + 2, default_level)? as i32;
1076
1077        match resolve_debug_thread_target(state, &other_thread) {
1078            DebugThreadTarget::Current => {
1079                crate::auxlib::traceback(state, None, msg_owned.as_deref(), level)?;
1080            }
1081            DebugThreadTarget::Other(target_state) => {
1082                let mut target_state = crate::coro_lib::borrow_thread_rooted(state, &target_state);
1083                crate::auxlib::traceback(
1084                    state,
1085                    Some(&mut *target_state),
1086                    msg_owned.as_deref(),
1087                    level,
1088                )?;
1089            }
1090            DebugThreadTarget::Unavailable => {
1091                crate::auxlib::traceback(state, None, msg_owned.as_deref(), level)?;
1092            }
1093        }
1094    }
1095    Ok(1)
1096}
1097
1098/// `debug.setcstacklimit(limit)` — set the C-stack depth limit. Returns the
1099/// old limit, or a platform-specific sentinel when not supported.
1100///
1101pub(crate) fn set_c_stack_limit(state: &mut LuaState) -> Result<usize, LuaError> {
1102    let limit = state.check_arg_integer(1)? as i32;
1103    let res = state.set_c_stack_limit(limit)?;
1104    state.push(LuaValue::Int(res as i64));
1105    Ok(1)
1106}
1107
1108// ── Library registration ───────────────────────────────────────────────────
1109
1110/// Function registration table for the `debug` library.
1111///
1112pub(crate) const DBLIB: &[(&[u8], LibFn)] = &[
1113    (b"debug", debug_interactive as LibFn),
1114    (b"getuservalue", get_uservalue as LibFn),
1115    (b"gethook", get_hook as LibFn),
1116    (b"getinfo", get_info as LibFn),
1117    (b"getlocal", get_local as LibFn),
1118    (b"getregistry", get_registry as LibFn),
1119    (b"getmetatable", get_metatable as LibFn),
1120    (b"getupvalue", get_upvalue as LibFn),
1121    (b"upvaluejoin", upvalue_join as LibFn),
1122    (b"upvalueid", upvalue_id as LibFn),
1123    (b"setuservalue", set_uservalue as LibFn),
1124    (b"sethook", set_hook as LibFn),
1125    (b"setlocal", set_local as LibFn),
1126    (b"setmetatable", set_metatable as LibFn),
1127    (b"setupvalue", set_upvalue as LibFn),
1128    (b"traceback", traceback as LibFn),
1129    (b"setcstacklimit", set_c_stack_limit as LibFn),
1130];
1131
1132/// Names withheld from the `debug` roster on the 5.1 backend.
1133///
1134/// 5.1's `ldblib.c` predates userdata user-values (`getuservalue`/
1135/// `setuservalue`), upvalue identity (`upvalueid`/`upvaluejoin`), and the 5.4
1136/// `setcstacklimit`. It instead carries the fenv accessors `getfenv`/`setfenv`,
1137/// which are layered on by [`open_debug`]. Verified against lua5.1.5.
1138const DBLIB_DROP_V51: &[&[u8]] = &[
1139    b"getuservalue",
1140    b"setuservalue",
1141    b"upvalueid",
1142    b"upvaluejoin",
1143    b"setcstacklimit",
1144];
1145
1146/// Open the `debug` library and push the module table onto the stack.
1147/// Returns 1 (the table).
1148///
1149/// The roster is version-gated: `setcstacklimit` is a 5.4-only addition
1150/// (removed again in 5.5), and the 5.1 backend swaps the modern upvalue/
1151/// uservalue accessors for the fenv accessors `getfenv`/`setfenv`. Every delta
1152/// is verified against that version's reference binary.
1153pub fn open_debug(state: &mut LuaState) -> Result<usize, LuaError> {
1154    use lua_types::LuaVersion;
1155    let version = state.global().lua_version;
1156    let is_v51 = matches!(version, LuaVersion::V51);
1157    let has_setcstacklimit = matches!(version, LuaVersion::V54);
1158
1159    let filtered: Vec<(&[u8], LibFn)> = DBLIB
1160        .iter()
1161        .filter(|(name, _)| {
1162            if !has_setcstacklimit && *name == b"setcstacklimit".as_slice() {
1163                return false;
1164            }
1165            if is_v51 && DBLIB_DROP_V51.contains(name) {
1166                return false;
1167            }
1168            true
1169        })
1170        .copied()
1171        .collect();
1172    state.new_lib(&filtered)?;
1173
1174    if is_v51 {
1175        // `debug.getfenv`/`debug.setfenv` are the object-form fenv accessors
1176        // (`db_getfenv`/`db_setfenv`), distinct from the level-aware globals
1177        // `getfenv`/`setfenv`: their first argument is the object itself, not a
1178        // stack level. Verified against lua5.1.5: `debug.getfenv ~= getfenv`.
1179        state.push_c_function(crate::base::debug_getfenv_fn)?;
1180        state.set_field(-2, b"getfenv")?;
1181        state.push_c_function(crate::base::debug_setfenv_fn)?;
1182        state.set_field(-2, b"setfenv")?;
1183    }
1184
1185    Ok(1)
1186}