Skip to main content

lua_stdlib/
debug_lib.rs

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