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