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