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