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<std::cell::RefMut<'_, LuaState>> = 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 = info_target_owner
334                    .as_ref()
335                    .expect("target owner just stored")
336                    .borrow_mut();
337                if !target.get_stack_level(level, &mut ar) {
338                    state.push_fail()?;
339                    return Ok(1);
340                }
341                if target.get_debug_info(&options, &mut ar).is_err() {
342                    return Err(lua_vm::debug::arg_error_impl(
343                        state,
344                        arg + 2,
345                        b"invalid option",
346                    ));
347                }
348                info_target = Some(target);
349            }
350        }
351    }
352
353    let result_tbl = state.new_table();
354    state.push(LuaValue::Table(result_tbl));
355
356    if options.contains(&b'S') {
357        let src = state.intern_str(ar.source_bytes())?;
358        state.push(LuaValue::Str(src));
359        state.set_field(-2, b"source")?;
360
361        settabss(state, b"short_src", Some(ar.short_src_bytes()))?;
362        settabsi(state, b"linedefined", ar.linedefined)?;
363        settabsi(state, b"lastlinedefined", ar.lastlinedefined)?;
364        settabss(state, b"what", Some(ar.what_bytes()))?;
365    }
366    if options.contains(&b'l') {
367        settabsi(state, b"currentline", ar.currentline)?;
368    }
369    if options.contains(&b'u') {
370        settabsi(state, b"nups", ar.nups as i32)?;
371        settabsi(state, b"nparams", ar.nparams as i32)?;
372        settabsb(state, b"isvararg", ar.isvararg)?;
373    }
374    if options.contains(&b'n') {
375        let name_opt: Option<&[u8]> = ar.name.as_deref();
376        settabss(state, b"name", name_opt)?;
377        settabss(state, b"namewhat", Some(ar.namewhat_bytes()))?;
378    }
379    if options.contains(&b'r') {
380        settabsi(state, b"ftransfer", ar.ftransfer as i32)?;
381        settabsi(state, b"ntransfer", ar.ntransfer as i32)?;
382    }
383    if options.contains(&b't') {
384        settabsb(state, b"istailcall", ar.istailcall)?;
385        if matches!(state.global().lua_version, LuaVersion::V55) {
386            settabsi(state, b"extraargs", ar.extraargs as i32)?;
387        }
388    }
389    // 'L' and 'f' options: lua_getinfo pushed line-table then function onto L1's stack.
390    // treat_stack_option moves each into the result table.
391    // PORT NOTE: C's lua_getinfo always pushes 'f' result before 'L' result (regardless
392    // of option-string order), so the treatstackoption calls below are intentionally
393    // ordered 'L' first then 'f' — matching the C db_getinfo exactly.
394    if options.contains(&b'L') {
395        if info_target_is_self {
396            treat_stack_option(state, true, b"activelines")?;
397        } else if let Some(target) = info_target.as_mut() {
398            move_stack_option_from_target(state, &mut **target, b"activelines")?;
399        } else {
400            state.push(LuaValue::Nil);
401            state.set_field(-2, b"activelines")?;
402        }
403    }
404    if options.contains(&b'f') {
405        if info_target_is_self {
406            treat_stack_option(state, true, b"func")?;
407        } else if let Some(target) = info_target.as_mut() {
408            move_stack_option_from_target(state, &mut **target, b"func")?;
409        } else {
410            state.push(LuaValue::Nil);
411            state.set_field(-2, b"func")?;
412        }
413    }
414
415    Ok(1)
416}
417
418/// `debug.getlocal([thread,] level, local)` — return the name and value of
419/// local variable `local` at stack level `level`.
420///
421/// When the first argument is a function, returns only the parameter name at
422/// position `local` (no value).
423///
424pub(crate) fn get_local(state: &mut LuaState) -> Result<usize, LuaError> {
425    let (arg, other_thread) = getthread(state);
426    let target_state = resolve_debug_thread_target(state, &other_thread);
427
428    let nvar = state.check_arg_integer(arg + 2)? as i32;
429
430    if state.type_at(arg + 1) == LuaType::Function {
431        state.push_value_at(arg + 1)?;
432        // lua_getlocal with NULL ar reads parameter names from the function at the
433        // top of the stack; it does NOT push a value.
434        let name = state.get_param_name(0, nvar)?;
435        match name {
436            Some(n) => {
437                let ls = state.intern_str(&n)?;
438                state.push(LuaValue::Str(ls));
439            }
440            None => {
441                state.push(LuaValue::Nil);
442            }
443        }
444        // The pushed function below name is discarded by the VM when it collects
445        // exactly 1 return value from the top of the stack.
446        return Ok(1);
447    }
448
449    // Stack-level path.
450    let level = state.check_arg_integer(arg + 1)? as i32;
451    let mut ar = DebugInfo::default();
452
453    let name = match target_state {
454        DebugThreadTarget::Current | DebugThreadTarget::Unavailable => {
455            if !state.get_stack_level(level, &mut ar) {
456                return Err(lua_vm::debug::arg_error_impl(
457                    state,
458                    arg + 1,
459                    b"level out of range",
460                ));
461            }
462            check_cross_thread_stack(state, true, 1)?;
463            // Pushes the local's value onto L1's stack and returns its name.
464            state.get_local_at(&ar, nvar)?
465        }
466        DebugThreadTarget::Other(target_state) => {
467            let mut target = target_state.borrow_mut();
468            if !target.get_stack_level(level, &mut ar) {
469                return Err(lua_vm::debug::arg_error_impl(
470                    state,
471                    arg + 1,
472                    b"level out of range",
473                ));
474            }
475            check_cross_thread_stack(state, false, 1)?;
476            let name = target.get_local_at(&ar, nvar)?;
477            if name.is_some() {
478                let val = target.get_at(target.top_idx() - 1);
479                target.pop_n(1);
480                state.push(val);
481            }
482            name
483        }
484    };
485
486    if let Some(n) = name {
487        let ls = state.intern_str(&n)?;
488        state.push(LuaValue::Str(ls));
489        state.rotate(-2, 1)?;
490        Ok(2)
491    } else {
492        state.push_fail()?;
493        Ok(1)
494    }
495}
496
497/// `debug.setlocal([thread,] level, local, value)` — set local variable
498/// `local` at stack level `level` to `value`. Returns the variable name, or
499/// nil on failure.
500///
501pub(crate) fn set_local(state: &mut LuaState) -> Result<usize, LuaError> {
502    let (arg, other_thread) = getthread(state);
503    let target_state = resolve_debug_thread_target(state, &other_thread);
504
505    let level = state.check_arg_integer(arg + 1)? as i32;
506    let nvar = state.check_arg_integer(arg + 2)? as i32;
507
508    let mut ar = DebugInfo::default();
509
510    state.check_arg_any(arg + 3)?;
511    lua_vm::api::set_top(state, arg + 3)?;
512
513    let name = match target_state {
514        DebugThreadTarget::Current | DebugThreadTarget::Unavailable => {
515            if !state.get_stack_level(level, &mut ar) {
516                return Err(lua_vm::debug::arg_error_impl(
517                    state,
518                    arg + 1,
519                    b"level out of range",
520                ));
521            }
522            check_cross_thread_stack(state, true, 1)?;
523            let name = state.set_local_at(&ar, nvar)?;
524            if name.is_none() {
525                state.pop_n(1);
526            }
527            name
528        }
529        DebugThreadTarget::Other(target_state) => {
530            let new_val = state.get_at(state.top_idx() - 1);
531            let mut target = target_state.borrow_mut();
532            if !target.get_stack_level(level, &mut ar) {
533                return Err(lua_vm::debug::arg_error_impl(
534                    state,
535                    arg + 1,
536                    b"level out of range",
537                ));
538            }
539            check_cross_thread_stack(state, false, 1)?;
540            target.push(new_val);
541            let name = target.set_local_at(&ar, nvar)?;
542            if name.is_none() {
543                target.pop_n(1);
544            }
545            state.pop_n(1);
546            name
547        }
548    };
549
550    match name {
551        Some(n) => {
552            let ls = state.intern_str(&n)?;
553            state.push(LuaValue::Str(ls));
554        }
555        None => {
556            state.push(LuaValue::Nil);
557        }
558    }
559    Ok(1)
560}
561
562/// Shared implementation for `get_upvalue` and `set_upvalue`.
563///
564/// When `get` is `true`, retrieves upvalue `n` of the function at stack index 1,
565/// pushes its value, and returns `(name, value)` — 2 results.
566///
567/// When `get` is `false`, pops the top stack value and installs it as upvalue
568/// `n`, returning `(name,)` — 1 result.
569///
570/// Returns 0 results when the upvalue index is out of range.
571///
572fn aux_upvalue(state: &mut LuaState, get: bool) -> Result<usize, LuaError> {
573    let n = state.check_arg_integer(2)? as i32;
574    state.check_arg_type(1, LuaType::Function)?;
575
576    let name: Option<Vec<u8>> = if get {
577        // lua_getupvalue pushes the upvalue value and returns the name.
578        state.get_upvalue(1, n)?
579    } else {
580        // lua_setupvalue pops the top-of-stack value, sets upvalue n, returns name.
581        state.set_upvalue(1, n)?
582    };
583
584    let name_ref = match name {
585        Some(n) => n,
586        None => return Ok(0),
587    };
588
589    let ls = state.intern_str(&name_ref)?;
590    state.push(LuaValue::Str(ls));
591
592    // When get=true: stack is [..., value, name]; insert at -2 → [..., name, value].
593    // When get=false: insert at -1 is a no-op; stack is [..., name].
594    if get {
595        state.insert(-2)?;
596    }
597
598    Ok(if get { 2 } else { 1 })
599}
600
601/// `debug.getupvalue(f, up)` — return the name and value of upvalue `up` of `f`.
602///
603pub(crate) fn get_upvalue(state: &mut LuaState) -> Result<usize, LuaError> {
604    aux_upvalue(state, true)
605}
606
607/// `debug.setupvalue(f, up, value)` — set upvalue `up` of `f` to `value`.
608/// Returns the upvalue name.
609///
610pub(crate) fn set_upvalue(state: &mut LuaState) -> Result<usize, LuaError> {
611    state.check_arg_any(3)?;
612    aux_upvalue(state, false)
613}
614
615/// Verify that upvalue `argnup` of function at stack index `argf` exists.
616/// Returns the opaque identity handle and the upvalue index.
617/// If `require_valid` is true, raises an arg error when the upvalue is absent.
618///
619fn check_upval(
620    state: &mut LuaState,
621    argf: i32,
622    argnup: i32,
623    require_valid: bool,
624) -> Result<(Option<UpvalId>, i32), LuaError> {
625    let nup = state.check_arg_integer(argnup)? as i32;
626    state.check_arg_type(argf, LuaType::Function)?;
627    // TODO(port): lua_upvalueid returns a raw void* that uniquely identifies
628    // an upvalue's storage cell. A safe equivalent (e.g., GcRef<UpVal> pointer
629    // comparison, or a stable u64 ID from the GC layer) must be defined in
630    // Phase D. Using Option<usize> as placeholder.
631    let id: Option<UpvalId> = match state.upvalue_id(argf, nup) {
632        Ok(p) if p.is_null() => None,
633        Ok(p) => Some(p as usize),
634        Err(_) => None,
635    };
636    if require_valid && id.is_none() {
637        return Err(lua_vm::debug::arg_error_impl(
638            state,
639            argnup,
640            b"invalid upvalue index",
641        ));
642    }
643    Ok((id, nup))
644}
645
646/// `debug.upvalueid(f, n)` — return a unique identifier for upvalue `n` of
647/// function `f` as a light userdata. Returns fail on out-of-range index.
648///
649pub(crate) fn upvalue_id(state: &mut LuaState) -> Result<usize, LuaError> {
650    let (id, _nup) = check_upval(state, 1, 2, false)?;
651    match id {
652        Some(uid) => {
653            lua_vm::api::push_light_userdata(state, uid as *mut core::ffi::c_void);
654        }
655        None => {
656            state.push_fail()?;
657        }
658    }
659    Ok(1)
660}
661
662/// `debug.upvaluejoin(f1, n1, f2, n2)` — make upvalue `n1` of function `f1`
663/// refer to the same storage as upvalue `n2` of function `f2`.
664///
665pub(crate) fn upvalue_join(state: &mut LuaState) -> Result<usize, LuaError> {
666    let (_id1, n1) = check_upval(state, 1, 2, true)?;
667    let (_id2, n2) = check_upval(state, 3, 4, true)?;
668    if state.is_c_function_at(1) {
669        return Err(lua_vm::debug::arg_error_impl(
670            state,
671            1,
672            b"Lua function expected",
673        ));
674    }
675    if state.is_c_function_at(3) {
676        return Err(lua_vm::debug::arg_error_impl(
677            state,
678            3,
679            b"Lua function expected",
680        ));
681    }
682    state.join_upvalues(1, n1, 3, n2)?;
683    Ok(0)
684}
685
686/// Internal debug hook registered with the VM via `lua_sethook`. When
687/// invoked, it looks up the Lua-side hook function stored in
688/// `registry[HOOKKEY][current_thread]` and calls it with the event name
689/// and current line number.
690///
691pub(crate) fn hookf(state: &mut LuaState, event: i32, currentline: i32) -> Result<(), LuaError> {
692    state.get_registry_field(HOOKKEY)?;
693    state.push_thread()?;
694    if state.raw_get(-2)? == LuaType::Function {
695        let event_idx = event.clamp(0, HOOKNAMES.len() as i32 - 1) as usize;
696        let event_str = state.intern_str(HOOKNAMES[event_idx])?;
697        state.push(LuaValue::Str(event_str));
698
699        if currentline >= 0 {
700            state.push(LuaValue::Int(currentline as i64));
701        } else {
702            state.push(LuaValue::Nil);
703        }
704
705        state.call(2, 0)?;
706    }
707    // The caller (do_::hook) saves/restores the stack top, so any residual
708    // entries (hook table, non-function lookup result) are cleaned up there.
709    Ok(())
710}
711
712/// Convert the string hook-mask (`'c'`/`'r'`/`'l'` characters) and a count
713/// to the integer bitmask used by the VM's `sethook` API.
714///
715fn make_mask(smask: &[u8], count: i32) -> u32 {
716    let mut mask: u32 = 0;
717    if smask.contains(&b'c') {
718        mask |= MASK_CALL;
719    }
720    if smask.contains(&b'r') {
721        mask |= MASK_RET;
722    }
723    if smask.contains(&b'l') {
724        mask |= MASK_LINE;
725    }
726    if count > 0 {
727        mask |= MASK_COUNT;
728    }
729    mask
730}
731
732/// Convert the integer hook bitmask back to the string representation used in
733/// Lua (`'c'`/`'r'`/`'l'` characters).
734///
735fn unmake_mask(mask: u32) -> Vec<u8> {
736    let mut smask = Vec::with_capacity(3);
737    if mask & MASK_CALL != 0 {
738        smask.push(b'c');
739    }
740    if mask & MASK_RET != 0 {
741        smask.push(b'r');
742    }
743    if mask & MASK_LINE != 0 {
744        smask.push(b'l');
745    }
746    smask
747}
748
749/// `debug.sethook([thread,] hook, mask [, count])` — install a debug hook.
750/// Passing nil as `hook` removes the current hook.
751///
752pub(crate) fn set_hook(state: &mut LuaState) -> Result<usize, LuaError> {
753    let (arg, other_thread) = getthread(state);
754    let target_is_self = other_thread.is_none();
755
756    let hook_active: bool;
757    let mask: u32;
758    let count: i32;
759
760    if matches!(state.type_at(arg + 1), LuaType::None | LuaType::Nil) {
761        lua_vm::api::set_top(state, arg + 1)?;
762        hook_active = false;
763        mask = 0;
764        count = 0;
765    } else {
766        let smask: Vec<u8> = state.check_arg_string(arg + 2)?.to_vec();
767        state.check_arg_type(arg + 1, LuaType::Function)?;
768        count = state.opt_arg_integer(arg + 3, 0)? as i32;
769        hook_active = true;
770        mask = make_mask(&smask, count);
771    }
772
773    if !state.get_or_create_registry_subtable(HOOKKEY)? {
774        // Table was just created. Set it up as a weak-keyed table so that
775        // thread keys do not prevent GC of finished threads.
776        let k = state.intern_str(b"k")?;
777        state.push(LuaValue::Str(k));
778        state.set_field(-2, b"__mode")?;
779        state.push_value_at(-1)?;
780        state.set_metatable(-2)?;
781    }
782
783    check_cross_thread_stack(state, target_is_self, 1)?;
784    let target_state = resolve_debug_thread_target(state, &other_thread);
785    match &target_state {
786        DebugThreadTarget::Other(st) => {
787            st.borrow_mut().ensure_stack(1, "stack overflow")?;
788        }
789        DebugThreadTarget::Current => {}
790        DebugThreadTarget::Unavailable => {}
791    }
792
793    if target_is_self {
794        state.push_thread()?;
795    } else {
796        // Push the target thread (captured via getthread) as the key. The C
797        // `lua_pushthread(L1); lua_xmove(L1, L, 1)` dance is necessary because
798        // C uses two distinct lua_State pointers; in our impl the GcRef is
799        // already a global reference so we can push it directly on the parent
800        // stack as a Thread value. Without this push, raw_set below operates
801        // on a stack that's missing its key slot and panics in get_table_value.
802        let thr = other_thread
803            .clone()
804            .expect("other_thread is Some when target_is_self is false");
805        state.push(lua_types::value::LuaValue::Thread(thr));
806    }
807    state.push_value_at(arg + 1)?;
808    state.raw_set(-3)?;
809
810    let hook_box: Option<Box<dyn FnMut(&mut LuaState, &lua_vm::debug::LuaDebug)>> = if hook_active {
811        Some(Box::new(|st, ar| {
812            let _ = hookf(st, ar.event, ar.currentline);
813        }))
814    } else {
815        None
816    };
817    match target_state {
818        DebugThreadTarget::Current => {
819            lua_vm::debug::set_hook(state, hook_box, mask as i32, count);
820        }
821        DebugThreadTarget::Other(target_state) => {
822            lua_vm::debug::set_hook(&mut target_state.borrow_mut(), hook_box, mask as i32, count);
823        }
824        DebugThreadTarget::Unavailable => {
825            // Main-thread cross-thread targeting from a non-main state is not
826            // yet reachable in this build; record the function in the shared
827            // registry and leave execution on the current thread untouched.
828            return Ok(0);
829        }
830    }
831
832    Ok(0)
833}
834
835/// `debug.gethook([thread])` — return the current hook function, mask string,
836/// and count. Returns the fail value if no hook is installed.
837///
838pub(crate) fn get_hook(state: &mut LuaState) -> Result<usize, LuaError> {
839    let (_arg, other_thread) = getthread(state);
840    let target_is_self = other_thread.is_none();
841    let target_state = resolve_debug_thread_target(state, &other_thread);
842
843    let (mask, hook_is_set, hook_is_internal, hook_count) = match target_state {
844        DebugThreadTarget::Current => (
845            state.get_hook_mask(),
846            state.hook_is_set(),
847            state.hook_is_internal_lua_hook(),
848            state.get_hook_count(),
849        ),
850        DebugThreadTarget::Other(target_state) => {
851            let mut target_state = target_state.borrow_mut();
852            (
853                target_state.get_hook_mask(),
854                target_state.hook_is_set(),
855                target_state.hook_is_internal_lua_hook(),
856                target_state.get_hook_count(),
857            )
858        }
859        DebugThreadTarget::Unavailable => (0u32, false, false, 0i32),
860    };
861
862    if !hook_is_set {
863        state.push_fail()?;
864        return Ok(1);
865    }
866
867    //      lua_pushliteral(L, "external hook");
868    if !hook_is_internal {
869        let s = state.intern_str(b"external hook")?;
870        state.push(LuaValue::Str(s));
871    } else {
872        state.get_registry_field(HOOKKEY)?;
873        check_cross_thread_stack(state, target_is_self, 1)?;
874        if target_is_self {
875            state.push_thread()?;
876        } else {
877            let key_thread = other_thread
878                .expect("other_thread is Some when target_is_self is false")
879                .clone();
880            state.push(lua_types::value::LuaValue::Thread(key_thread));
881        }
882        state.raw_get(-2)?;
883        state.remove(-2)?;
884    }
885
886    let smask = unmake_mask(mask);
887    let ls = state.intern_str(&smask)?;
888    state.push(LuaValue::Str(ls));
889
890    state.push(LuaValue::Int(hook_count as i64));
891
892    Ok(3)
893}
894
895/// `debug.debug()` — enter an interactive debug REPL.
896///
897/// Reads Lua source lines from stdin, compiles and runs each one. On EOF or
898/// when the user types `cont`, returns control to the caller. Errors in
899/// commands are printed to stderr and the loop continues.
900///
901pub(crate) fn debug_interactive(state: &mut LuaState) -> Result<usize, LuaError> {
902    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
903    {
904        let _ = state;
905        return Err(LuaError::runtime(format_args!(
906            "debug.debug interactive stdin not available in this host"
907        )));
908    }
909
910    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
911    {
912        let stdin = io::stdin();
913        loop {
914            eprint!("lua_debug> ");
915            let _ = io::stderr().flush();
916
917            //        return 0;
918            // PORT NOTE: using String for the line buffer is Rust I/O infrastructure,
919            // not Lua data. The bytes are immediately converted to &[u8] before being
920            // passed into the Lua API.
921            let mut line = String::new();
922            let n = stdin
923                .lock()
924                .read_line(&mut line)
925                .map_err(|e| LuaError::runtime(format_args!("stdin read error: {}", e)))?;
926
927            if n == 0 || line == "cont\n" {
928                return Ok(0);
929            }
930
931            let bytes: &[u8] = line.as_bytes();
932
933            //        lua_pcall(L, 0, 0, 0))
934            //      lua_writestringerror("%s\n", luaL_tolstring(L, -1, NULL));
935            let result = state
936                .load_buffer(bytes, b"=(debug command)", None)
937                .and_then(|_| state.protected_call(0, 0, 0));
938
939            if let Err(_) = result {
940                // TODO(port): display the error via state.coerce_to_string(-1) which
941                // maps to luaL_tolstring. The exact method name for the coercing
942                // to-string operation and the stderr-write helper need to be established
943                // in Phase B (lua-vm/src/api.rs).
944                eprintln!("(error in debug command)");
945                state.pop_n(1);
946            }
947
948            lua_vm::api::set_top(state, 0)?;
949        }
950    }
951}
952
953/// `debug.traceback([thread,] [message [, level]])` — return a traceback string.
954///
955/// If `message` is present but is not a string, it is returned unchanged.
956/// Otherwise a stack traceback is generated and optionally prepended with
957/// `message`.
958///
959pub(crate) fn traceback(state: &mut LuaState) -> Result<usize, LuaError> {
960    let (arg, other_thread) = getthread(state);
961    let target_is_self = other_thread.is_none();
962
963    // Immediately clone to Vec<u8> to free the borrow on `state`.
964    let msg_owned: Option<Vec<u8>> = state
965        .to_lua_string(arg + 1)
966        .map(|s: GcRef<LuaString>| s.as_bytes().to_vec());
967
968    let arg1_ty = state.type_at(arg + 1);
969    if msg_owned.is_none() && !matches!(arg1_ty, LuaType::None | LuaType::Nil) {
970        state.push_value_at(arg + 1)?;
971    } else {
972        let default_level: i64 = if target_is_self { 1 } else { 0 };
973        let level = state.opt_arg_integer(arg + 2, default_level)? as i32;
974
975        match resolve_debug_thread_target(state, &other_thread) {
976            DebugThreadTarget::Current => {
977                crate::auxlib::traceback(state, None, msg_owned.as_deref(), level)?;
978            }
979            DebugThreadTarget::Other(target_state) => {
980                let mut target_state = target_state.borrow_mut();
981                crate::auxlib::traceback(
982                    state,
983                    Some(&mut *target_state),
984                    msg_owned.as_deref(),
985                    level,
986                )?;
987            }
988            DebugThreadTarget::Unavailable => {
989                crate::auxlib::traceback(state, None, msg_owned.as_deref(), level)?;
990            }
991        }
992    }
993    Ok(1)
994}
995
996/// `debug.setcstacklimit(limit)` — set the C-stack depth limit. Returns the
997/// old limit, or a platform-specific sentinel when not supported.
998///
999pub(crate) fn set_c_stack_limit(state: &mut LuaState) -> Result<usize, LuaError> {
1000    let limit = state.check_arg_integer(1)? as i32;
1001    let res = state.set_c_stack_limit(limit)?;
1002    state.push(LuaValue::Int(res as i64));
1003    Ok(1)
1004}
1005
1006// ── Library registration ───────────────────────────────────────────────────
1007
1008/// Function registration table for the `debug` library.
1009///
1010pub(crate) const DBLIB: &[(&[u8], LibFn)] = &[
1011    (b"debug", debug_interactive as LibFn),
1012    (b"getuservalue", get_uservalue as LibFn),
1013    (b"gethook", get_hook as LibFn),
1014    (b"getinfo", get_info as LibFn),
1015    (b"getlocal", get_local as LibFn),
1016    (b"getregistry", get_registry as LibFn),
1017    (b"getmetatable", get_metatable as LibFn),
1018    (b"getupvalue", get_upvalue as LibFn),
1019    (b"upvaluejoin", upvalue_join as LibFn),
1020    (b"upvalueid", upvalue_id as LibFn),
1021    (b"setuservalue", set_uservalue as LibFn),
1022    (b"sethook", set_hook as LibFn),
1023    (b"setlocal", set_local as LibFn),
1024    (b"setmetatable", set_metatable as LibFn),
1025    (b"setupvalue", set_upvalue as LibFn),
1026    (b"traceback", traceback as LibFn),
1027    (b"setcstacklimit", set_c_stack_limit as LibFn),
1028];
1029
1030/// Open the `debug` library and push the module table onto the stack.
1031/// Returns 1 (the table).
1032///
1033pub fn open_debug(state: &mut LuaState) -> Result<usize, LuaError> {
1034    state.new_lib(DBLIB)?;
1035    Ok(1)
1036}
1037
1038// ──────────────────────────────────────────────────────────────────────────
1039// PORT STATUS
1040//   source:        src/ldblib.c  (484 lines, 20 functions)
1041//   target_crate:  lua-stdlib
1042//   confidence:    medium
1043//   todos:         16
1044//   port_notes:    3
1045//   unsafe_blocks: 0
1046//   notes:         Cross-thread ops (lua_xmove / simultaneous &mut LuaState)
1047//                  are the main blockers; all 16 TODOs are in that cluster or
1048//                  in UpvalId (raw-pointer identity). Single-thread paths are
1049//                  faithfully translated. Phase B must define: DebugInfo field
1050//                  accessor methods (source_bytes, short_src_bytes, what_bytes,
1051//                  name_bytes, namewhat_bytes), hook-kind predicates
1052//                  (hook_is_set, hook_is_internal_lua_hook, get_hook_mask,
1053//                  get_hook_count), get_or_create_registry_subtable,
1054//                  get_param_name, get_local_at, set_local_at,
1055//                  upvalue_id (UpvalId type), join_upvalues, lua_traceback,
1056//                  load_buffer, push_registry, get_registry_field, push_fail,
1057//                  push_thread, new_lib, set_hook(Option<HookFn>, u32, i32).
1058// ──────────────────────────────────────────────────────────────────────────