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