use std::cell::RefCell;
use std::rc::Rc;
use crate::bytecode::proto::{LocalVarInfo, Proto};
use crate::vm::frame::CallFrame;
use crate::vm::machine::VM;
use crate::vm::value::{Value, VmTable};
pub fn create_debug_lib() -> Value {
let mut table = VmTable::new();
macro_rules! reg {
($($name:literal => $func:ident),* $(,)?) => {
$(table.set_str($name, crate::native!(concat!("debug.", $name), $func));)*
};
}
reg! {
"getinfo" => debug_getinfo,
"getlocal" => debug_getlocal,
"getmetatable" => debug_getmetatable,
"gethook" => debug_gethook,
"getupvalue" => debug_getupvalue,
"setlocal" => debug_setlocal,
"setmetatable" => debug_setmetatable,
"sethook" => debug_sethook,
"setupvalue" => debug_setupvalue,
"traceback" => debug_traceback,
"upvalueid" => debug_upvalueid,
}
crate::vm::libs::args::frozen(table)
}
fn count_arg(args: &[Value], index: usize, name: &str, default: i64) -> Result<i64, String> {
match args.get(index) {
None | Some(Value::Nil) => Ok(default),
Some(Value::Int(i)) => Ok(*i),
Some(Value::Float(f)) if f.fract() == 0.0 => Ok(*f as i64),
Some(other) => Err(format!(
"{} must be an integer, got {}",
name,
other.type_name()
)),
}
}
fn frame_at(vm: &VM, level: i64) -> Option<&CallFrame> {
if level < 1 {
return None;
}
let level = level as usize;
vm.frames.len().checked_sub(level).map(|i| &vm.frames[i])
}
fn current_pc(frame: &CallFrame) -> usize {
frame.ip.saturating_sub(1)
}
fn current_line(frame: &CallFrame) -> i64 {
let proto = &frame.closure.proto;
proto
.lines
.get(current_pc(frame))
.map(|l| *l as i64)
.unwrap_or(-1)
}
fn debug_traceback(vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let message = match args.first() {
None | Some(Value::Nil) => None,
Some(Value::String(s)) => Some(s.to_string()),
Some(other) => return Ok(vec![other.clone()]),
};
let level = count_arg(args, 1, "level", 1)?.max(1) as usize;
let mut out = message.map(|m| format!("{}\n", m)).unwrap_or_default();
out.push_str("stack traceback:");
for frame in vm.capture_trace().into_iter().skip(level - 1) {
out.push_str("\n ");
if frame.source.is_none() && frame.line == 0 {
out.push_str(frame.function.as_deref().unwrap_or(""));
} else {
out.push_str(&frame.to_string());
}
}
Ok(vec![Value::string(out)])
}
fn proto_info(table: &mut VmTable, proto: &Proto) {
let name = proto.name.as_deref();
table.set_str(
"name",
match name {
Some("main") | None => Value::Nil,
Some(n) => Value::str(n),
},
);
table.set_str(
"what",
Value::str(if name == Some("main") {
"main"
} else {
"Neyuki"
}),
);
let source = proto.source.as_deref().unwrap_or("?");
table.set_str("source", Value::str(source));
table.set_str(
"short_src",
Value::str(source.rsplit(['/', '\\']).next().unwrap_or(source)),
);
let first = proto.lines.iter().copied().filter(|l| *l > 0).min();
let last = proto.lines.iter().copied().max();
table.set_str("linedefined", Value::Int(first.unwrap_or(0) as i64));
table.set_str("lastlinedefined", Value::Int(last.unwrap_or(0) as i64));
table.set_str("numparams", Value::Int(proto.num_params as i64));
table.set_str("isvararg", Value::Bool(proto.is_vararg));
table.set_str("nups", Value::Int(proto.upvalues.len() as i64));
}
fn native_info(table: &mut VmTable, name: &str) {
table.set_str("name", Value::str(name));
table.set_str("what", Value::str("native"));
table.set_str("source", Value::str("=[native]"));
table.set_str("short_src", Value::str("[native]"));
table.set_str("linedefined", Value::Int(-1));
table.set_str("lastlinedefined", Value::Int(-1));
table.set_str("currentline", Value::Int(-1));
table.set_str("numparams", Value::Int(0));
table.set_str("isvararg", Value::Bool(true));
table.set_str("nups", Value::Int(0));
}
fn debug_getinfo(vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let mut table = VmTable::new();
match args.first() {
Some(Value::Closure(c)) => {
proto_info(&mut table, &c.proto);
table.set_str("currentline", Value::Int(-1));
}
Some(Value::Native(def)) => native_info(&mut table, def.name),
Some(level @ (Value::Int(_) | Value::Float(_))) => {
let level = count_arg(std::slice::from_ref(level), 0, "level", 1)?;
if level == 0 {
native_info(&mut table, "debug.getinfo");
} else {
let Some(frame) = frame_at(vm, level) else {
return Ok(vec![Value::Nil]);
};
proto_info(&mut table, &frame.closure.proto);
table.set_str("currentline", Value::Int(current_line(frame)));
table.set_str("func", Value::Closure(frame.closure.clone()));
}
}
_ => return Err("getinfo expects a level or a function".to_string()),
}
if let Some(f @ (Value::Closure(_) | Value::Native(_))) = args.first() {
table.set_str("func", f.clone());
}
Ok(vec![Value::Table(Rc::new(RefCell::new(table)))])
}
fn active_locals(frame: &CallFrame) -> Vec<&LocalVarInfo> {
let pc = current_pc(frame) as u32;
frame
.closure
.proto
.local_names
.iter()
.filter(|info| info.from_pc <= pc && pc < info.to_pc)
.collect()
}
fn debug_getlocal(vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let n = count_arg(args, 1, "n", 1)?;
if let Some(Value::Closure(c)) = args.first() {
let name = c
.proto
.local_names
.iter()
.filter(|info| info.from_pc == 0 && (info.reg as i64) < c.proto.num_params as i64)
.nth((n - 1).max(0) as usize)
.filter(|_| n >= 1)
.map(|info| Value::str(&info.name));
return Ok(vec![name.unwrap_or(Value::Nil)]);
}
let level = count_arg(args, 0, "level", 1)?;
let Some(frame) = frame_at(vm, level) else {
return Err("level out of range".to_string());
};
if n < 0 {
let index = (-n - 1) as usize;
return Ok(match frame.varargs.get(index) {
Some(v) => vec![Value::str("(vararg)"), v.clone()],
None => vec![Value::Nil],
});
}
let locals = active_locals(frame);
let Some(info) = (n >= 1).then(|| locals.get(n as usize - 1)).flatten() else {
return Ok(vec![Value::Nil]);
};
let value = vm
.stack
.get(frame.base + info.reg as usize)
.cloned()
.unwrap_or(Value::Nil);
Ok(vec![Value::str(&info.name), value])
}
fn debug_setlocal(vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let level = count_arg(args, 0, "level", 1)?;
let n = count_arg(args, 1, "n", 1)?;
let value = args.get(2).cloned().unwrap_or(Value::Nil);
let Some(frame) = frame_at(vm, level) else {
return Err("level out of range".to_string());
};
if n < 0 {
let index = (-n - 1) as usize;
let at = vm.frames.len() - level as usize;
return Ok(match vm.frames[at].varargs.get_mut(index) {
Some(slot) => {
*slot = value;
vec![Value::str("(vararg)")]
}
None => vec![Value::Nil],
});
}
let target = active_locals(frame)
.get((n - 1).max(0) as usize)
.filter(|_| n >= 1)
.map(|info| (info.name.clone(), frame.base + info.reg as usize));
let Some((name, slot)) = target else {
return Ok(vec![Value::Nil]);
};
if slot >= vm.stack.len() {
vm.stack.resize(slot + 1, Value::Nil);
}
vm.stack[slot] = value;
Ok(vec![Value::string(name)])
}
fn upvalue_name(c: &crate::vm::value::VmClosure, index: usize) -> Value {
match c.proto.upvalue_names.get(index) {
Some(name) if !name.is_empty() => Value::str(name),
_ => Value::str("?"),
}
}
fn debug_getupvalue(vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let Some(Value::Closure(c)) = args.first() else {
return Ok(vec![Value::Nil]);
};
let n = count_arg(args, 1, "n", 1)?;
if n < 1 || n as usize > c.upvalues.len() {
return Ok(vec![Value::Nil]);
}
let value = vm.upvalue_get(&c.upvalues[n as usize - 1]);
Ok(vec![upvalue_name(c, n as usize - 1), value])
}
fn debug_setupvalue(vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let Some(Value::Closure(c)) = args.first() else {
return Ok(vec![Value::Nil]);
};
let n = count_arg(args, 1, "n", 1)?;
if n < 1 || n as usize > c.upvalues.len() {
return Ok(vec![Value::Nil]);
}
let value = args.get(2).cloned().unwrap_or(Value::Nil);
let uv = c.upvalues[n as usize - 1].clone();
vm.upvalue_set(&uv, value);
Ok(vec![upvalue_name(c, n as usize - 1)])
}
fn debug_upvalueid(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let Some(Value::Closure(c)) = args.first() else {
return Err("upvalueid expects a function".to_string());
};
let n = count_arg(args, 1, "n", 1)?;
match (n >= 1).then(|| c.upvalues.get(n as usize - 1)).flatten() {
Some(uv) => Ok(vec![Value::Int(Rc::as_ptr(uv) as i64)]),
None => Ok(vec![Value::Nil]),
}
}
fn debug_getmetatable(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
Ok(vec![match args.first() {
Some(Value::Table(t)) => t
.borrow()
.metatable
.clone()
.map(Value::Table)
.unwrap_or(Value::Nil),
_ => Value::Nil,
}])
}
fn debug_setmetatable(vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let Some(Value::Table(t)) = args.first() else {
return Err("setmetatable expects a table".to_string());
};
let mt = match args.get(1) {
None | Some(Value::Nil) => None,
Some(Value::Table(mt)) => Some(mt.clone()),
Some(other) => {
return Err(format!(
"metatable must be a table or nil, got {}",
other.type_name()
));
}
};
let gc_handler = mt.as_ref().is_some_and(|mt| {
matches!(
mt.borrow().get_str("__gc"),
Value::Closure(_) | Value::Native(_)
)
});
t.borrow_mut().metatable = mt;
if gc_handler {
vm.gc.pin_table(t);
} else {
vm.gc.unpin_table(t);
}
if crate::vm::gc::table_weak_values(t) {
vm.gc.has_weak_tables = true;
}
Ok(vec![args[0].clone()])
}
fn debug_sethook(vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
let func = args.first().cloned().unwrap_or(Value::Nil);
if matches!(func, Value::Nil) {
vm.hook = None;
vm.update_trap();
return Ok(Vec::new());
}
if !matches!(func, Value::Closure(_) | Value::Native(_)) {
return Err(format!("hook must be a function, got {}", func.type_name()));
}
let mask = match args.get(1) {
None | Some(Value::Nil) if args.get(2).is_none() => "cr",
None | Some(Value::Nil) => "",
Some(Value::String(s)) => s,
Some(other) => {
return Err(format!("mask must be a string, got {}", other.type_name()));
}
};
let (mut on_call, mut on_return, mut on_line) = (false, false, false);
for ch in mask.chars() {
match ch {
'c' => on_call = true,
'r' => on_return = true,
'l' => on_line = true,
_ => return Err(format!("invalid hook mask character '{}'", ch)),
}
}
let count = count_arg(args, 2, "count", 0)?.clamp(0, u32::MAX as i64) as u32;
vm.hook = Some(crate::vm::machine::DebugHook {
func,
on_call,
on_return,
on_line,
count,
});
vm.hook_countdown = count;
if let Some(frame) = vm.frames.last_mut() {
let pc = current_pc(frame);
frame.hook_line = frame.closure.proto.lines.get(pc).copied().unwrap_or(0);
frame.hook_ip = pc;
}
vm.update_trap();
Ok(Vec::new())
}
fn debug_gethook(vm: &mut VM, _args: &[Value]) -> Result<Vec<Value>, String> {
let Some(hook) = &vm.hook else {
return Ok(vec![Value::Nil]);
};
let mut mask = String::new();
if hook.on_call {
mask.push('c');
}
if hook.on_return {
mask.push('r');
}
if hook.on_line {
mask.push('l');
}
Ok(vec![
hook.func.clone(),
Value::string(mask),
Value::Int(hook.count as i64),
])
}