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