Skip to main content

luaur_rt/
debug.rs

1//! Stack inspection. Mirrors the Luau-feasible subset of `mlua::Lua::inspect_stack`
2//! and `mlua::debug::Debug`.
3//!
4//! ## What Luau can back
5//!
6//! Luau's debug model is **not** the Lua 5.x line/count hook. It exposes
7//! `lua_getinfo(L, level, what, ar)` for activation records and `lua_singlestep`
8//! + the interrupt callback for stepping. We surface the *informational* part —
9//! resolving a stack level into a [`Debug`] record (current line, source, name,
10//! what kind of function) — which maps cleanly onto `lua_getinfo`.
11//!
12//! ## What is deferred (and why)
13//!
14//! The full `mlua::Lua::set_hook(HookTriggers, ...)` API (per-line / per-N-
15//! instruction / on-call / on-return hooks with a `Debug` event) is a Lua 5.x
16//! construct. Luau has no equivalent multiplexed hook: it has a *single* global
17//! interrupt callback (see [`Lua::set_interrupt`](crate::Lua::set_interrupt))
18//! and `lua_singlestep`. mlua itself gates `tests/hooks.rs` and `tests/debug.rs`
19//! behind `#![cfg(not(feature = "luau"))]` for exactly this reason. We therefore
20//! do **not** fake a 5.x hook surface; the interrupt API is the Luau-native
21//! analog and is implemented separately.
22
23use std::ffi::CStr;
24
25use crate::state::Lua;
26use crate::sys::*;
27
28/// What kind of function an activation record refers to. Mirrors the relevant
29/// part of `mlua::debug::DebugSource::what`.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum DebugWhat {
32    /// A Lua function.
33    Lua,
34    /// The main chunk.
35    Main,
36    /// A C / Rust (native) function.
37    C,
38    /// Unknown / unavailable.
39    Unknown,
40}
41
42/// A snapshot of one activation record, resolved from a stack level via
43/// `lua_getinfo`. Mirrors the informational subset of `mlua::debug::Debug`.
44#[derive(Debug, Clone)]
45pub struct Debug {
46    name: Option<String>,
47    what: DebugWhat,
48    source: Option<String>,
49    short_src: Option<String>,
50    current_line: Option<i64>,
51    line_defined: Option<i64>,
52}
53
54impl Debug {
55    /// The function's name, if known (`(n)`).
56    pub fn name(&self) -> Option<&str> {
57        self.name.as_deref()
58    }
59
60    /// What kind of function this record refers to (`(s)`).
61    pub fn what(&self) -> DebugWhat {
62        self.what
63    }
64
65    /// The chunk source (`(s)`).
66    pub fn source(&self) -> Option<&str> {
67        self.source.as_deref()
68    }
69
70    /// A short, human-readable source description (`(s)`).
71    pub fn short_src(&self) -> Option<&str> {
72        self.short_src.as_deref()
73    }
74
75    /// The currently executing line (`(l)`), if available.
76    pub fn current_line(&self) -> Option<i64> {
77        self.current_line
78    }
79
80    /// The line where the function was defined (`(s)`).
81    pub fn line_defined(&self) -> Option<i64> {
82        self.line_defined
83    }
84}
85
86impl Lua {
87    /// Inspect the activation record `level` frames up the call stack (0 = the
88    /// currently running function). Returns `None` if there is no function at
89    /// that level. Mirrors the Luau-feasible part of `mlua::Lua::inspect_stack`.
90    pub fn inspect_stack(&self, level: usize) -> Option<Debug> {
91        let state = self.state();
92        unsafe {
93            let mut ar: LuaDebug = core::mem::zeroed();
94            // `lua_getinfo` with a non-negative level walks call-info depth.
95            let opt = c"nsl";
96            let ok = lua_getinfo(
97                state,
98                level as c_int,
99                opt.as_ptr() as *const c_char,
100                &mut ar,
101            );
102            if ok == 0 {
103                return None;
104            }
105            let cstr = |p: *const c_char| -> Option<String> {
106                if p.is_null() {
107                    None
108                } else {
109                    Some(CStr::from_ptr(p).to_string_lossy().into_owned())
110                }
111            };
112            let what_str = cstr(ar.what).unwrap_or_default();
113            let what = match what_str.as_str() {
114                "Lua" => DebugWhat::Lua,
115                "main" => DebugWhat::Main,
116                "C" => DebugWhat::C,
117                _ => DebugWhat::Unknown,
118            };
119            let current_line = if ar.currentline >= 0 {
120                Some(ar.currentline as i64)
121            } else {
122                None
123            };
124            let line_defined = if ar.linedefined > 0 {
125                Some(ar.linedefined as i64)
126            } else {
127                None
128            };
129            Some(Debug {
130                name: cstr(ar.name),
131                what,
132                source: cstr(ar.source),
133                short_src: cstr(ar.short_src),
134                current_line,
135                line_defined,
136            })
137        }
138    }
139}