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