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, 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. When target is a different thread, use xmove.
181///
182fn treat_stack_option(
183    state: &mut LuaState,
184    target_is_self: bool,
185    fname: &[u8],
186) -> Result<(), LuaError> {
187    if target_is_self {
188        state.rotate(-2, 1)?;
189    } else {
190        // Moving a value from another thread's stack (`lua_xmove`) needs a
191        // simultaneous `&mut LuaState` for both threads, which safe Rust
192        // cannot express without interior mutability, so this pushes Nil
193        // as a placeholder instead of the real cross-thread value.
194        state.push(LuaValue::Nil);
195    }
196    state.set_field(-2, fname)
197}
198
199fn move_stack_option_from_target(
200    state: &mut LuaState,
201    target: &mut LuaState,
202    fname: &[u8],
203) -> Result<(), LuaError> {
204    let val = target.get_at(target.top_idx() - 1);
205    target.pop_n(1);
206    state.push(val);
207    state.set_field(-2, fname)
208}
209
210// ── Library functions ──────────────────────────────────────────────────────
211
212/// `debug.getregistry()` — return the Lua registry table.
213///
214pub(crate) fn get_registry(state: &mut LuaState) -> Result<usize, LuaError> {
215    state.push_registry()?;
216    Ok(1)
217}
218
219/// `debug.getmetatable(obj)` — return the metatable of `obj`, or nil if none.
220///
221pub(crate) fn get_metatable(state: &mut LuaState) -> Result<usize, LuaError> {
222    state.check_arg_any(1)?;
223    if !state.get_metatable(1)? {
224        state.push(LuaValue::Nil);
225    }
226    Ok(1)
227}
228
229/// `debug.setmetatable(obj, table)` — set `table` (or nil) as `obj`'s metatable.
230/// Returns the first argument `obj`.
231///
232pub(crate) fn set_metatable(state: &mut LuaState) -> Result<usize, LuaError> {
233    let t = state.type_at(2);
234    if !(t == LuaType::Nil || t == LuaType::Table) {
235        let got = state.arg(2);
236        return Err(LuaError::type_arg_error(2, "nil or table", &got));
237    }
238    lua_vm::api::set_top(state, 2)?;
239    state.set_metatable(1)?;
240    Ok(1)
241}
242
243/// `debug.getuservalue(obj [, n])` — return the n-th user value of userdata
244/// `obj` plus `true`, or the fail value if `obj` is not userdata or `n` is out
245/// of range.
246///
247pub(crate) fn get_uservalue(state: &mut LuaState) -> Result<usize, LuaError> {
248    let n = state.opt_arg_integer(2, 1)? as i32;
249    if state.type_at(1) != LuaType::UserData {
250        state.push_fail()?;
251        return Ok(1);
252    }
253    let ty = state.get_iuservalue(1, n)?;
254    if ty != LuaType::None {
255        state.push(LuaValue::Bool(true));
256        return Ok(2);
257    }
258    Ok(1)
259}
260
261/// `debug.setuservalue(obj, value [, n])` — set the n-th user value of userdata
262/// `obj` to `value`. Returns `obj`, or the fail value on failure.
263///
264pub(crate) fn set_uservalue(state: &mut LuaState) -> Result<usize, LuaError> {
265    let n = state.opt_arg_integer(3, 1)? as i32;
266    state.check_arg_type(1, LuaType::UserData)?;
267    state.check_arg_any(2)?;
268    lua_vm::api::set_top(state, 2)?;
269    if !state.set_iuservalue(1, n)? {
270        state.push_fail()?;
271    }
272    Ok(1)
273}
274
275/// `debug.getinfo([thread,] f|level [, what])` — collect debug information
276/// about function `f` or stack level `level` into a new table. The `what`
277/// string selects which fields to populate (default `"flnSrtu"`).
278///
279pub(crate) fn get_info(state: &mut LuaState) -> Result<usize, LuaError> {
280    let mut ar = DebugInfo::default();
281
282    let (arg, other_thread) = getthread(state);
283    let target_is_self = other_thread.is_none();
284    let target_state = resolve_debug_thread_target(state, &other_thread);
285
286    // to_vec() immediately to avoid borrow-checker conflict with subsequent &mut state ops.
287    let raw_opts: Vec<u8> = state.opt_arg_string(arg + 2, b"flnSrtu")?.to_vec();
288
289    check_cross_thread_stack(state, target_is_self, 3)?;
290
291    if raw_opts.first() == Some(&b'>') {
292        return Err(lua_vm::debug::arg_error_impl(
293            state,
294            arg + 2,
295            b"invalid option '>'",
296        ));
297    }
298
299    // Build the effective options string, prepending '>' when the subject is a function.
300    let options: Vec<u8>;
301    let info_target_owner: Option<Rc<RefCell<LuaState>>>;
302    let mut info_target: Option<crate::coro_lib::RootedThreadBorrow<'_>> = None;
303    let mut info_target_is_self = target_is_self;
304
305    if state.type_at(arg + 1) == LuaType::Function {
306        let mut prefixed = Vec::with_capacity(raw_opts.len() + 1);
307        prefixed.push(b'>');
308        prefixed.extend_from_slice(&raw_opts);
309        options = prefixed;
310
311        if target_is_self {
312            state.push_value_at(arg + 1)?;
313        } else {
314            // Moving the function value to another thread's stack (`lua_xmove`)
315            // needs a simultaneous `&mut LuaState` for both threads, which safe
316            // Rust cannot express; cross-thread getinfo with a function argument
317            // is incomplete — this branch is a no-op rather than pushing the value.
318        }
319
320        // With '>' prefix, get_debug_info consumes the function from the top of stack.
321        if state.get_debug_info(&options, &mut ar).is_err() {
322            return Err(lua_vm::debug::arg_error_impl(
323                state,
324                arg + 2,
325                b"invalid option",
326            ));
327        }
328    } else {
329        options = raw_opts;
330
331        let level = state.check_arg_integer(arg + 1)? as i32;
332        match target_state {
333            DebugThreadTarget::Current | DebugThreadTarget::Unavailable => {
334                info_target_is_self = true;
335                if !state.get_stack_level(level, &mut ar) {
336                    state.push_fail()?;
337                    return Ok(1);
338                }
339
340                if state.get_debug_info(&options, &mut ar).is_err() {
341                    return Err(lua_vm::debug::arg_error_impl(
342                        state,
343                        arg + 2,
344                        b"invalid option",
345                    ));
346                }
347            }
348            DebugThreadTarget::Other(target_state) => {
349                info_target_owner = Some(target_state);
350                let mut target = crate::coro_lib::borrow_thread_rooted(
351                    state,
352                    info_target_owner
353                        .as_ref()
354                        .expect("target owner just stored"),
355                );
356                if !target.get_stack_level(level, &mut ar) {
357                    state.push_fail()?;
358                    return Ok(1);
359                }
360                if target.get_debug_info(&options, &mut ar).is_err() {
361                    return Err(lua_vm::debug::arg_error_impl(
362                        state,
363                        arg + 2,
364                        b"invalid option",
365                    ));
366                }
367                target.resnapshot();
368                info_target = Some(target);
369            }
370        }
371    }
372
373    let result_tbl = state.new_table();
374    state.push(LuaValue::Table(result_tbl));
375
376    if options.contains(&b'S') {
377        let src = state.intern_str(ar.source_bytes())?;
378        state.push(LuaValue::Str(src));
379        state.set_field(-2, b"source")?;
380
381        settabss(state, b"short_src", Some(ar.short_src_bytes()))?;
382        settabsi(state, b"linedefined", ar.linedefined)?;
383        settabsi(state, b"lastlinedefined", ar.lastlinedefined)?;
384        settabss(state, b"what", Some(ar.what_bytes()))?;
385    }
386    if options.contains(&b'l') {
387        settabsi(state, b"currentline", ar.currentline)?;
388    }
389    if options.contains(&b'u') {
390        settabsi(state, b"nups", ar.nups as i32)?;
391        if !matches!(state.global().lua_version, LuaVersion::V51) {
392            settabsi(state, b"nparams", ar.nparams as i32)?;
393            settabsb(state, b"isvararg", ar.isvararg)?;
394        }
395    }
396    if options.contains(&b'n') {
397        let name_opt: Option<&[u8]> = ar.name.as_deref();
398        settabss(state, b"name", name_opt)?;
399        settabss(state, b"namewhat", Some(ar.namewhat_bytes()))?;
400    }
401    if options.contains(&b'r') {
402        settabsi(state, b"ftransfer", ar.ftransfer as i32)?;
403        settabsi(state, b"ntransfer", ar.ntransfer as i32)?;
404    }
405    if options.contains(&b't') {
406        settabsb(state, b"istailcall", ar.istailcall)?;
407        if matches!(state.global().lua_version, LuaVersion::V55) {
408            settabsi(state, b"extraargs", ar.extraargs as i32)?;
409        }
410    }
411    // The 'f' (function) and 'L' (active-lines table) results were pushed by
412    // get_debug_info in that order — function first, line-table on top — so they
413    // must be moved into the result table top-first: 'L' here, then 'f'. This
414    // ordering is load-bearing regardless of the option-string order.
415    if options.contains(&b'L') {
416        if info_target_is_self {
417            treat_stack_option(state, true, b"activelines")?;
418        } else if let Some(target) = info_target.as_mut() {
419            move_stack_option_from_target(state, &mut **target, b"activelines")?;
420        } else {
421            state.push(LuaValue::Nil);
422            state.set_field(-2, b"activelines")?;
423        }
424    }
425    if options.contains(&b'f') {
426        if info_target_is_self {
427            treat_stack_option(state, true, b"func")?;
428        } else if let Some(target) = info_target.as_mut() {
429            move_stack_option_from_target(state, &mut **target, b"func")?;
430        } else {
431            state.push(LuaValue::Nil);
432            state.set_field(-2, b"func")?;
433        }
434    }
435
436    Ok(1)
437}
438
439/// Whether `debug.getlocal` accepts a function as its first argument (the
440/// parameter-name introspection form).
441///
442/// This form is a 5.2 addition (the `lua_isfunction(L, arg+1)` branch in
443/// `ldblib.c` `db_getlocal`). On 5.1 there is no such branch: a function
444/// argument is fed straight to `luaL_checkint`, which raises
445/// `number expected, got function`. Returning `false` here lets the function
446/// argument fall through to the integer-level path so 5.1 reproduces that error.
447/// (`db_setlocal` has no function form on any version, so this gate is
448/// `getlocal`-only.)
449fn function_arg_form_supported(state: &LuaState) -> bool {
450    !matches!(state.global().lua_version, LuaVersion::V51)
451}
452
453/// `debug.getlocal([thread,] level, local)` — return the name and value of
454/// local variable `local` at stack level `level`.
455///
456/// On 5.2+ the first argument may be a function, in which case only the
457/// parameter name at position `local` is returned (no value); see
458/// [`function_arg_form_supported`].
459///
460pub(crate) fn get_local(state: &mut LuaState) -> Result<usize, LuaError> {
461    let (arg, other_thread) = getthread(state);
462    let target_state = resolve_debug_thread_target(state, &other_thread);
463
464    let nvar = state.check_arg_integer(arg + 2)? as i32;
465
466    if function_arg_form_supported(state) && state.type_at(arg + 1) == LuaType::Function {
467        state.push_value_at(arg + 1)?;
468        let name = state.get_param_name(0, nvar)?;
469        match name {
470            Some(n) => {
471                let ls = state.intern_str(&n)?;
472                state.push(LuaValue::Str(ls));
473            }
474            None => {
475                state.push(LuaValue::Nil);
476            }
477        }
478        return Ok(1);
479    }
480
481    // Stack-level path.
482    let level = state.check_arg_integer(arg + 1)? as i32;
483    let mut ar = DebugInfo::default();
484
485    let name = match target_state {
486        DebugThreadTarget::Current | DebugThreadTarget::Unavailable => {
487            if !state.get_stack_level(level, &mut ar) {
488                return Err(lua_vm::debug::arg_error_impl(
489                    state,
490                    arg + 1,
491                    b"level out of range",
492                ));
493            }
494            check_cross_thread_stack(state, true, 1)?;
495            // Pushes the local's value onto L1's stack and returns its name.
496            state.get_local_at(&ar, nvar)?
497        }
498        DebugThreadTarget::Other(target_state) => {
499            let mut target = crate::coro_lib::borrow_thread_rooted(state, &target_state);
500            if !target.get_stack_level(level, &mut ar) {
501                return Err(lua_vm::debug::arg_error_impl(
502                    state,
503                    arg + 1,
504                    b"level out of range",
505                ));
506            }
507            check_cross_thread_stack(state, false, 1)?;
508            let name = target.get_local_at(&ar, nvar)?;
509            if name.is_some() {
510                let val = target.get_at(target.top_idx() - 1);
511                target.pop_n(1);
512                state.push(val);
513            }
514            name
515        }
516    };
517
518    if let Some(n) = name {
519        let ls = state.intern_str(&n)?;
520        state.push(LuaValue::Str(ls));
521        state.rotate(-2, 1)?;
522        Ok(2)
523    } else {
524        state.push_fail()?;
525        Ok(1)
526    }
527}
528
529/// `debug.setlocal([thread,] level, local, value)` — set local variable
530/// `local` at stack level `level` to `value`. Returns the variable name, or
531/// nil on failure.
532///
533pub(crate) fn set_local(state: &mut LuaState) -> Result<usize, LuaError> {
534    let (arg, other_thread) = getthread(state);
535    let target_state = resolve_debug_thread_target(state, &other_thread);
536
537    let level = state.check_arg_integer(arg + 1)? as i32;
538    let nvar = state.check_arg_integer(arg + 2)? as i32;
539
540    let mut ar = DebugInfo::default();
541
542    state.check_arg_any(arg + 3)?;
543    lua_vm::api::set_top(state, arg + 3)?;
544
545    let name = match target_state {
546        DebugThreadTarget::Current | DebugThreadTarget::Unavailable => {
547            if !state.get_stack_level(level, &mut ar) {
548                return Err(lua_vm::debug::arg_error_impl(
549                    state,
550                    arg + 1,
551                    b"level out of range",
552                ));
553            }
554            check_cross_thread_stack(state, true, 1)?;
555            let name = state.set_local_at(&ar, nvar)?;
556            if name.is_none() {
557                state.pop_n(1);
558            }
559            name
560        }
561        DebugThreadTarget::Other(target_state) => {
562            let new_val = state.get_at(state.top_idx() - 1);
563            let mut target = crate::coro_lib::borrow_thread_rooted(state, &target_state);
564            if !target.get_stack_level(level, &mut ar) {
565                return Err(lua_vm::debug::arg_error_impl(
566                    state,
567                    arg + 1,
568                    b"level out of range",
569                ));
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    match name {
583        Some(n) => {
584            let ls = state.intern_str(&n)?;
585            state.push(LuaValue::Str(ls));
586        }
587        None => {
588            state.push(LuaValue::Nil);
589        }
590    }
591    Ok(1)
592}
593
594/// Shared implementation for `get_upvalue` and `set_upvalue`.
595///
596/// When `get` is `true`, retrieves upvalue `n` of the function at stack index 1,
597/// pushes its value, and returns `(name, value)` — 2 results.
598///
599/// When `get` is `false`, pops the top stack value and installs it as upvalue
600/// `n`, returning `(name,)` — 1 result.
601///
602/// Returns 0 results when the upvalue index is out of range.
603///
604fn aux_upvalue(state: &mut LuaState, get: bool) -> Result<usize, LuaError> {
605    let n = state.check_arg_integer(2)? as i32;
606    state.check_arg_type(1, LuaType::Function)?;
607
608    let name: Option<Vec<u8>> = if get {
609        // lua_getupvalue pushes the upvalue value and returns the name.
610        state.get_upvalue(1, n)?
611    } else {
612        // lua_setupvalue pops the top-of-stack value, sets upvalue n, returns name.
613        state.set_upvalue(1, n)?
614    };
615
616    let name_ref = match name {
617        Some(n) => n,
618        None => return Ok(0),
619    };
620
621    let ls = state.intern_str(&name_ref)?;
622    state.push(LuaValue::Str(ls));
623
624    // When get=true: stack is [..., value, name]; insert at -2 → [..., name, value].
625    // When get=false: insert at -1 is a no-op; stack is [..., name].
626    if get {
627        state.insert(-2)?;
628    }
629
630    Ok(if get { 2 } else { 1 })
631}
632
633/// `debug.getupvalue(f, up)` — return the name and value of upvalue `up` of `f`.
634///
635pub(crate) fn get_upvalue(state: &mut LuaState) -> Result<usize, LuaError> {
636    aux_upvalue(state, true)
637}
638
639/// `debug.setupvalue(f, up, value)` — set upvalue `up` of `f` to `value`.
640/// Returns the upvalue name.
641///
642pub(crate) fn set_upvalue(state: &mut LuaState) -> Result<usize, LuaError> {
643    state.check_arg_any(3)?;
644    aux_upvalue(state, false)
645}
646
647/// Verify that upvalue `argnup` of function at stack index `argf` exists.
648/// Returns the opaque identity handle and the upvalue index.
649/// If `require_valid` is true, raises an arg error when the upvalue is absent.
650///
651fn check_upval(
652    state: &mut LuaState,
653    argf: i32,
654    argnup: i32,
655    require_valid: bool,
656) -> Result<(Option<UpvalId>, i32), LuaError> {
657    let nup = state.check_arg_integer(argnup)? as i32;
658    state.check_arg_type(argf, LuaType::Function)?;
659    let id: Option<UpvalId> = match state.upvalue_id(argf, nup) {
660        Ok(p) if p.is_null() => None,
661        Ok(p) => Some(p as usize),
662        Err(_) => None,
663    };
664    if require_valid && id.is_none() {
665        return Err(lua_vm::debug::arg_error_impl(
666            state,
667            argnup,
668            b"invalid upvalue index",
669        ));
670    }
671    Ok((id, nup))
672}
673
674/// `debug.upvalueid(f, n)` — return a unique identifier for upvalue `n` of
675/// function `f` as a light userdata.
676///
677/// On 5.1/5.2/5.3 an out-of-range upvalue index raises
678/// `bad argument #2 ... (invalid upvalue index)` because those versions feed
679/// the index straight to `lua_upvalueid`, which asserts the index is in range.
680/// On 5.4/5.5 the index is validated and an out-of-range index returns the fail
681/// value instead, so the validity check is gated to the legacy/transitional
682/// versions.
683pub(crate) fn upvalue_id(state: &mut LuaState) -> Result<usize, LuaError> {
684    let require_valid = matches!(
685        state.global().lua_version,
686        LuaVersion::V51 | LuaVersion::V52 | LuaVersion::V53
687    );
688    let (id, _nup) = check_upval(state, 1, 2, require_valid)?;
689    match id {
690        Some(uid) => {
691            lua_vm::api::push_light_userdata(state, uid as *mut core::ffi::c_void);
692        }
693        None => {
694            state.push_fail()?;
695        }
696    }
697    Ok(1)
698}
699
700/// `debug.upvaluejoin(f1, n1, f2, n2)` — make upvalue `n1` of function `f1`
701/// refer to the same storage as upvalue `n2` of function `f2`.
702///
703pub(crate) fn upvalue_join(state: &mut LuaState) -> Result<usize, LuaError> {
704    let (_id1, n1) = check_upval(state, 1, 2, true)?;
705    let (_id2, n2) = check_upval(state, 3, 4, true)?;
706    if state.is_c_function_at(1) {
707        return Err(lua_vm::debug::arg_error_impl(
708            state,
709            1,
710            b"Lua function expected",
711        ));
712    }
713    if state.is_c_function_at(3) {
714        return Err(lua_vm::debug::arg_error_impl(
715            state,
716            3,
717            b"Lua function expected",
718        ));
719    }
720    state.join_upvalues(1, n1, 3, n2)?;
721    Ok(0)
722}
723
724/// Internal debug hook registered with the VM via `lua_sethook`. When
725/// invoked, it looks up the Lua-side hook function stored in
726/// `registry[HOOKKEY][current_thread]` and calls it with the event name
727/// and current line number.
728///
729pub(crate) fn hookf(state: &mut LuaState, event: i32, currentline: i32) -> Result<(), LuaError> {
730    state.get_registry_field(HOOKKEY)?;
731    state.push_thread()?;
732    if state.raw_get(-2)? == LuaType::Function {
733        let event_idx = event.clamp(0, HOOKNAMES.len() as i32 - 1) as usize;
734        let name = if event_idx == 4
735            && state.global().lua_version == lua_types::LuaVersion::V51
736        {
737            HOOKNAME_TAILRET_51
738        } else {
739            HOOKNAMES[event_idx]
740        };
741        let event_str = state.intern_str(name)?;
742        state.push(LuaValue::Str(event_str));
743
744        if currentline >= 0 {
745            state.push(LuaValue::Int(currentline as i64));
746        } else {
747            state.push(LuaValue::Nil);
748        }
749
750        state.call(2, 0)?;
751    }
752    // The caller (do_::hook) saves/restores the stack top, so any residual
753    // entries (hook table, non-function lookup result) are cleaned up there.
754    Ok(())
755}
756
757/// Convert the string hook-mask (`'c'`/`'r'`/`'l'` characters) and a count
758/// to the integer bitmask used by the VM's `sethook` API.
759///
760fn make_mask(smask: &[u8], count: i32) -> u32 {
761    let mut mask: u32 = 0;
762    if smask.contains(&b'c') {
763        mask |= MASK_CALL;
764    }
765    if smask.contains(&b'r') {
766        mask |= MASK_RET;
767    }
768    if smask.contains(&b'l') {
769        mask |= MASK_LINE;
770    }
771    if count > 0 {
772        mask |= MASK_COUNT;
773    }
774    mask
775}
776
777/// Convert the integer hook bitmask back to the string representation used in
778/// Lua (`'c'`/`'r'`/`'l'` characters).
779///
780fn unmake_mask(mask: u32) -> Vec<u8> {
781    let mut smask = Vec::with_capacity(3);
782    if mask & MASK_CALL != 0 {
783        smask.push(b'c');
784    }
785    if mask & MASK_RET != 0 {
786        smask.push(b'r');
787    }
788    if mask & MASK_LINE != 0 {
789        smask.push(b'l');
790    }
791    smask
792}
793
794/// `debug.sethook([thread,] hook, mask [, count])` — install a debug hook.
795/// Passing nil as `hook` removes the current hook.
796///
797pub(crate) fn set_hook(state: &mut LuaState) -> Result<usize, LuaError> {
798    let (arg, other_thread) = getthread(state);
799    let target_is_self = other_thread.is_none();
800
801    let hook_active: bool;
802    let mask: u32;
803    let count: i32;
804
805    if matches!(state.type_at(arg + 1), LuaType::None | LuaType::Nil) {
806        lua_vm::api::set_top(state, arg + 1)?;
807        hook_active = false;
808        mask = 0;
809        count = 0;
810    } else {
811        let smask: Vec<u8> = state.check_arg_string(arg + 2)?.to_vec();
812        state.check_arg_type(arg + 1, LuaType::Function)?;
813        count = state.opt_arg_integer(arg + 3, 0)? as i32;
814        hook_active = true;
815        mask = make_mask(&smask, count);
816    }
817
818    if !state.get_or_create_registry_subtable(HOOKKEY)? {
819        // Table was just created. Set it up as a weak-keyed table so that
820        // thread keys do not prevent GC of finished threads.
821        let k = state.intern_str(b"k")?;
822        state.push(LuaValue::Str(k));
823        state.set_field(-2, b"__mode")?;
824        state.push_value_at(-1)?;
825        state.set_metatable(-2)?;
826    }
827
828    check_cross_thread_stack(state, target_is_self, 1)?;
829    let target_state = resolve_debug_thread_target(state, &other_thread);
830    match &target_state {
831        DebugThreadTarget::Other(st) => {
832            st.borrow_mut().ensure_stack(1, "stack overflow")?;
833        }
834        DebugThreadTarget::Current => {}
835        DebugThreadTarget::Unavailable => {}
836    }
837
838    if target_is_self {
839        state.push_thread()?;
840    } else {
841        // Push the target thread (captured via getthread) as the key. The C
842        // `lua_pushthread(L1); lua_xmove(L1, L, 1)` dance is necessary because
843        // C uses two distinct lua_State pointers; in our impl the GcRef is
844        // already a global reference so we can push it directly on the parent
845        // stack as a Thread value. Without this push, raw_set below operates
846        // on a stack that's missing its key slot and panics in get_table_value.
847        let thr = other_thread
848            .clone()
849            .expect("other_thread is Some when target_is_self is false");
850        state.push(lua_types::value::LuaValue::Thread(thr));
851    }
852    state.push_value_at(arg + 1)?;
853    state.raw_set(-3)?;
854
855    let hook_box: Option<Box<dyn FnMut(&mut LuaState, &lua_vm::debug::LuaDebug)>> = if hook_active {
856        Some(Box::new(|st, ar| {
857            let _ = hookf(st, ar.event, ar.currentline);
858        }))
859    } else {
860        None
861    };
862    match target_state {
863        DebugThreadTarget::Current => {
864            lua_vm::debug::set_hook(state, hook_box, mask as i32, count);
865        }
866        DebugThreadTarget::Other(target_state) => {
867            lua_vm::debug::set_hook(&mut target_state.borrow_mut(), hook_box, mask as i32, count);
868        }
869        DebugThreadTarget::Unavailable => {
870            // Main-thread cross-thread targeting from a non-main state is not
871            // yet reachable in this build; record the function in the shared
872            // registry and leave execution on the current thread untouched.
873            return Ok(0);
874        }
875    }
876
877    Ok(0)
878}
879
880/// `debug.gethook([thread])` — return the current hook function, mask string,
881/// and count. Returns the fail value if no hook is installed.
882///
883pub(crate) fn get_hook(state: &mut LuaState) -> Result<usize, LuaError> {
884    let (_arg, other_thread) = getthread(state);
885    let target_is_self = other_thread.is_none();
886    let target_state = resolve_debug_thread_target(state, &other_thread);
887
888    let (mask, hook_is_set, hook_is_internal, hook_count) = match target_state {
889        DebugThreadTarget::Current => (
890            state.get_hook_mask(),
891            state.hook_is_set(),
892            state.hook_is_internal_lua_hook(),
893            state.get_hook_count(),
894        ),
895        DebugThreadTarget::Other(target_state) => {
896            let mut target_state = target_state.borrow_mut();
897            (
898                target_state.get_hook_mask(),
899                target_state.hook_is_set(),
900                target_state.hook_is_internal_lua_hook(),
901                target_state.get_hook_count(),
902            )
903        }
904        DebugThreadTarget::Unavailable => (0u32, false, false, 0i32),
905    };
906
907    if !hook_is_set {
908        state.push_fail()?;
909        return Ok(1);
910    }
911
912    if !hook_is_internal {
913        let s = state.intern_str(b"external hook")?;
914        state.push(LuaValue::Str(s));
915    } else {
916        state.get_registry_field(HOOKKEY)?;
917        check_cross_thread_stack(state, target_is_self, 1)?;
918        if target_is_self {
919            state.push_thread()?;
920        } else {
921            let key_thread = other_thread
922                .expect("other_thread is Some when target_is_self is false")
923                .clone();
924            state.push(lua_types::value::LuaValue::Thread(key_thread));
925        }
926        state.raw_get(-2)?;
927        state.remove(-2)?;
928    }
929
930    let smask = unmake_mask(mask);
931    let ls = state.intern_str(&smask)?;
932    state.push(LuaValue::Str(ls));
933
934    state.push(LuaValue::Int(hook_count as i64));
935
936    Ok(3)
937}
938
939/// `debug.debug()` — enter an interactive debug REPL.
940///
941/// Reads Lua source lines from stdin, compiles and runs each one. On EOF or
942/// when the user types `cont`, returns control to the caller. Errors in
943/// commands are printed to stderr and the loop continues.
944///
945pub(crate) fn debug_interactive(state: &mut LuaState) -> Result<usize, LuaError> {
946    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
947    {
948        let _ = state;
949        return Err(LuaError::runtime(format_args!(
950            "debug.debug interactive stdin not available in this host"
951        )));
952    }
953
954    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
955    {
956        let stdin = io::stdin();
957        loop {
958            eprint!("lua_debug> ");
959            let _ = io::stderr().flush();
960
961            // The `String` line buffer is Rust I/O infrastructure, not Lua data:
962            // its bytes are handed to the Lua API as `&[u8]` immediately below.
963            let mut line = String::new();
964            let n = stdin
965                .lock()
966                .read_line(&mut line)
967                .map_err(|e| LuaError::runtime(format_args!("stdin read error: {}", e)))?;
968
969            if n == 0 || line == "cont\n" {
970                return Ok(0);
971            }
972
973            let bytes: &[u8] = line.as_bytes();
974
975            let result = state
976                .load_buffer(bytes, b"=(debug command)", None)
977                .and_then(|_| state.protected_call(0, 0, 0));
978
979            if result.is_err() {
980                // Prints a generic message rather than the actual error text.
981                // `crate::auxlib::to_lua_string` (the `luaL_tolstring`
982                // equivalent) could render the real error object here.
983                eprintln!("(error in debug command)");
984                state.pop_n(1);
985            }
986
987            lua_vm::api::set_top(state, 0)?;
988        }
989    }
990}
991
992/// `debug.traceback([thread,] [message [, level]])` — return a traceback string.
993///
994/// If `message` is present but is not a string, it is returned unchanged.
995/// Otherwise a stack traceback is generated and optionally prepended with
996/// `message`.
997///
998pub(crate) fn traceback(state: &mut LuaState) -> Result<usize, LuaError> {
999    let (arg, other_thread) = getthread(state);
1000    let target_is_self = other_thread.is_none();
1001
1002    // Immediately clone to Vec<u8> to free the borrow on `state`.
1003    let msg_owned: Option<Vec<u8>> = state
1004        .to_lua_string(arg + 1)
1005        .map(|s: GcRef<LuaString>| s.as_bytes().to_vec());
1006
1007    let arg1_ty = state.type_at(arg + 1);
1008    if msg_owned.is_none() && !matches!(arg1_ty, LuaType::None | LuaType::Nil) {
1009        state.push_value_at(arg + 1)?;
1010    } else {
1011        let default_level: i64 = if target_is_self { 1 } else { 0 };
1012        let level = state.opt_arg_integer(arg + 2, default_level)? as i32;
1013
1014        match resolve_debug_thread_target(state, &other_thread) {
1015            DebugThreadTarget::Current => {
1016                crate::auxlib::traceback(state, None, msg_owned.as_deref(), level)?;
1017            }
1018            DebugThreadTarget::Other(target_state) => {
1019                let mut target_state = crate::coro_lib::borrow_thread_rooted(state, &target_state);
1020                crate::auxlib::traceback(
1021                    state,
1022                    Some(&mut *target_state),
1023                    msg_owned.as_deref(),
1024                    level,
1025                )?;
1026            }
1027            DebugThreadTarget::Unavailable => {
1028                crate::auxlib::traceback(state, None, msg_owned.as_deref(), level)?;
1029            }
1030        }
1031    }
1032    Ok(1)
1033}
1034
1035/// `debug.setcstacklimit(limit)` — set the C-stack depth limit. Returns the
1036/// old limit, or a platform-specific sentinel when not supported.
1037///
1038pub(crate) fn set_c_stack_limit(state: &mut LuaState) -> Result<usize, LuaError> {
1039    let limit = state.check_arg_integer(1)? as i32;
1040    let res = state.set_c_stack_limit(limit)?;
1041    state.push(LuaValue::Int(res as i64));
1042    Ok(1)
1043}
1044
1045// ── Library registration ───────────────────────────────────────────────────
1046
1047/// Function registration table for the `debug` library.
1048///
1049pub(crate) const DBLIB: &[(&[u8], LibFn)] = &[
1050    (b"debug", debug_interactive as LibFn),
1051    (b"getuservalue", get_uservalue as LibFn),
1052    (b"gethook", get_hook as LibFn),
1053    (b"getinfo", get_info as LibFn),
1054    (b"getlocal", get_local as LibFn),
1055    (b"getregistry", get_registry as LibFn),
1056    (b"getmetatable", get_metatable as LibFn),
1057    (b"getupvalue", get_upvalue as LibFn),
1058    (b"upvaluejoin", upvalue_join as LibFn),
1059    (b"upvalueid", upvalue_id as LibFn),
1060    (b"setuservalue", set_uservalue as LibFn),
1061    (b"sethook", set_hook as LibFn),
1062    (b"setlocal", set_local as LibFn),
1063    (b"setmetatable", set_metatable as LibFn),
1064    (b"setupvalue", set_upvalue as LibFn),
1065    (b"traceback", traceback as LibFn),
1066    (b"setcstacklimit", set_c_stack_limit as LibFn),
1067];
1068
1069/// Names withheld from the `debug` roster on the 5.1 backend.
1070///
1071/// 5.1's `ldblib.c` predates userdata user-values (`getuservalue`/
1072/// `setuservalue`), upvalue identity (`upvalueid`/`upvaluejoin`), and the 5.4
1073/// `setcstacklimit`. It instead carries the fenv accessors `getfenv`/`setfenv`,
1074/// which are layered on by [`open_debug`]. Verified against lua5.1.5.
1075const DBLIB_DROP_V51: &[&[u8]] = &[
1076    b"getuservalue",
1077    b"setuservalue",
1078    b"upvalueid",
1079    b"upvaluejoin",
1080    b"setcstacklimit",
1081];
1082
1083/// Open the `debug` library and push the module table onto the stack.
1084/// Returns 1 (the table).
1085///
1086/// The roster is version-gated: `setcstacklimit` is a 5.4-only addition
1087/// (removed again in 5.5), and the 5.1 backend swaps the modern upvalue/
1088/// uservalue accessors for the fenv accessors `getfenv`/`setfenv`. Every delta
1089/// is verified against that version's reference binary.
1090pub fn open_debug(state: &mut LuaState) -> Result<usize, LuaError> {
1091    use lua_types::LuaVersion;
1092    let version = state.global().lua_version;
1093    let is_v51 = matches!(version, LuaVersion::V51);
1094    let has_setcstacklimit = matches!(version, LuaVersion::V54);
1095
1096    let filtered: Vec<(&[u8], LibFn)> = DBLIB
1097        .iter()
1098        .filter(|(name, _)| {
1099            if !has_setcstacklimit && *name == b"setcstacklimit".as_slice() {
1100                return false;
1101            }
1102            if is_v51 && DBLIB_DROP_V51.contains(name) {
1103                return false;
1104            }
1105            true
1106        })
1107        .copied()
1108        .collect();
1109    state.new_lib(&filtered)?;
1110
1111    if is_v51 {
1112        // `debug.getfenv`/`debug.setfenv` are the object-form fenv accessors
1113        // (`db_getfenv`/`db_setfenv`), distinct from the level-aware globals
1114        // `getfenv`/`setfenv`: their first argument is the object itself, not a
1115        // stack level. Verified against lua5.1.5: `debug.getfenv ~= getfenv`.
1116        state.push_c_function(crate::base::debug_getfenv_fn)?;
1117        state.set_field(-2, b"getfenv")?;
1118        state.push_c_function(crate::base::debug_setfenv_fn)?;
1119        state.set_field(-2, b"setfenv")?;
1120    }
1121
1122    Ok(1)
1123}