neyuki 0.1.1

A small Lua-like scripting language implemented in Rust
// Debug standard library for Neyuki VM.

use num_bigint::BigInt;
use num_traits::ToPrimitive;
use std::cell::RefCell;
use std::rc::Rc;

use crate::vm::machine::VM;
use crate::vm::value::{NativeDef, Value, VmTable};

pub fn create_debug_lib() -> Value {
    let t = Rc::new(RefCell::new(VmTable::new()));
    let mut b = t.borrow_mut();

    b.set_str(
        "traceback",
        crate::native!("debug.traceback", debug_traceback),
    );
    b.set_str("getinfo", crate::native!("debug.getinfo", debug_getinfo));
    b.set_str("getlocal", crate::native!("debug.getlocal", debug_getlocal));
    b.set_str("setlocal", crate::native!("debug.setlocal", debug_setlocal));
    b.set_str(
        "getupvalue",
        crate::native!("debug.getupvalue", debug_getupvalue),
    );
    b.set_str(
        "setupvalue",
        crate::native!("debug.setupvalue", debug_setupvalue),
    );

    Value::Table(t.clone())
}

fn debug_traceback(vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
    let mut out = String::new();
    if let Some(msg) = args.first()
        && !matches!(msg, Value::Nil)
    {
        out.push_str(&msg.to_string());
        out.push('\n');
    }

    out.push_str("stack traceback:\n");
    let level_offset = match args.get(1) {
        Some(Value::Int(i)) => i.to_usize().unwrap_or(1),
        Some(Value::Float(f)) => *f as usize,
        _ => 1,
    };

    let total_frames = vm.frames.len();
    for (i, frame) in vm.frames.iter().rev().enumerate() {
        if i < level_offset.saturating_sub(1) {
            continue;
        }

        let proto = &frame.closure.proto;
        let name = proto.name.as_deref().unwrap_or("<anonymous>");
        let line = if frame.ip < proto.lines.len() {
            proto.lines[frame.ip]
        } else {
            proto.lines.last().copied().unwrap_or(1)
        };

        out.push_str(&format!(
            "  [frame {}] function '{}' at line {}\n",
            total_frames.saturating_sub(i),
            name,
            line
        ));
    }

    Ok(vec![Value::string(out)])
}

fn debug_getinfo(vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
    let arg0 = match args.first() {
        Some(a) => a,
        None => return Ok(vec![Value::Nil]),
    };

    match arg0 {
        Value::Closure(c) => {
            let tbl = Rc::new(RefCell::new(VmTable::new()));
            let mut b = tbl.borrow_mut();
            let name = c
                .proto
                .name
                .clone()
                .unwrap_or_else(|| "<anonymous>".to_string());
            b.set_str("name", Value::String((name.clone()).into()));
            b.set_str("what", Value::str("Lua"));
            b.set_str("source", Value::string(name));
            b.set_str(
                "numparams",
                Value::from_bigint(BigInt::from(c.proto.num_params)),
            );
            b.set_str("isvararg", Value::Bool(c.proto.is_vararg));
            b.set_str("currentline", Value::from_bigint(BigInt::from(-1)));
            b.set_str("func", Value::Closure(c.clone()));
            drop(b);
            Ok(vec![Value::Table(tbl)])
        }
        Value::Native(NativeDef { name, .. }) => {
            let tbl = Rc::new(RefCell::new(VmTable::new()));
            let mut b = tbl.borrow_mut();
            b.set_str("name", Value::string(name.to_string()));
            b.set_str("what", Value::str("C"));
            b.set_str("source", Value::str("=[C]"));
            b.set_str("numparams", Value::from_bigint(BigInt::from(0)));
            b.set_str("isvararg", Value::Bool(true));
            b.set_str("currentline", Value::from_bigint(BigInt::from(-1)));
            b.set_str("func", arg0.clone());
            drop(b);
            Ok(vec![Value::Table(tbl)])
        }
        Value::Int(i) => {
            let level = i.to_usize().unwrap_or(0);
            get_frame_info(vm, level)
        }
        Value::Float(f) => {
            let level = *f as usize;
            get_frame_info(vm, level)
        }
        _ => Ok(vec![Value::Nil]),
    }
}

fn get_frame_info(vm: &VM, level: usize) -> Result<Vec<Value>, String> {
    if level == 0 {
        let tbl = Rc::new(RefCell::new(VmTable::new()));
        let mut b = tbl.borrow_mut();
        b.set_str("name", Value::str("debug.getinfo"));
        b.set_str("what", Value::str("C"));
        b.set_str("source", Value::str("=[C]"));
        b.set_str("numparams", Value::from_bigint(BigInt::from(0)));
        b.set_str("isvararg", Value::Bool(true));
        b.set_str("currentline", Value::from_bigint(BigInt::from(-1)));
        drop(b);
        return Ok(vec![Value::Table(tbl)]);
    }

    if level > vm.frames.len() {
        return Ok(vec![Value::Nil]);
    }

    let frame = &vm.frames[vm.frames.len() - level];
    let proto = &frame.closure.proto;

    let tbl = Rc::new(RefCell::new(VmTable::new()));
    let mut b = tbl.borrow_mut();

    let name = proto
        .name
        .clone()
        .unwrap_or_else(|| "<anonymous>".to_string());
    b.set_str("name", Value::String((name.clone()).into()));
    b.set_str("what", Value::str("Lua"));
    b.set_str("source", Value::string(name));
    b.set_str(
        "numparams",
        Value::from_bigint(BigInt::from(proto.num_params)),
    );
    b.set_str("isvararg", Value::Bool(proto.is_vararg));

    let line = if frame.ip < proto.lines.len() {
        proto.lines[frame.ip]
    } else {
        proto.lines.last().copied().unwrap_or(1)
    };
    b.set_str("currentline", Value::from_bigint(BigInt::from(line)));
    b.set_str("func", Value::Closure(frame.closure.clone()));
    drop(b);

    Ok(vec![Value::Table(tbl)])
}

fn debug_getlocal(vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
    let level = match args.first() {
        Some(Value::Int(i)) => i.to_usize().unwrap_or(1),
        Some(Value::Float(f)) => *f as usize,
        _ => 1,
    };

    let idx = match args.get(1) {
        Some(Value::Int(i)) => i.to_usize().unwrap_or(1),
        Some(Value::Float(f)) => *f as usize,
        _ => 1,
    };

    if level == 0 || level > vm.frames.len() {
        return Ok(vec![Value::Nil]);
    }

    let frame = &vm.frames[vm.frames.len() - level];
    let proto = &frame.closure.proto;

    let current_ip = frame.ip as u32;
    let mut matching_locals: Vec<&crate::bytecode::proto::LocalVarInfo> = Vec::new();

    for info in &proto.local_names {
        if current_ip >= info.from_pc && current_ip <= info.to_pc {
            matching_locals.push(info);
        }
    }

    if idx == 0 || idx > matching_locals.len() {
        return Ok(vec![Value::Nil]);
    }

    let local_info = matching_locals[idx - 1];
    let reg_idx = local_info.reg;
    let stack_slot = frame.base + reg_idx as usize;
    let val = vm.stack.get(stack_slot).cloned().unwrap_or(Value::Nil);

    Ok(vec![Value::String((local_info.name.clone()).into()), val])
}

fn debug_setlocal(vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
    let level = match args.first() {
        Some(Value::Int(i)) => i.to_usize().unwrap_or(1),
        Some(Value::Float(f)) => *f as usize,
        _ => 1,
    };

    let idx = match args.get(1) {
        Some(Value::Int(i)) => i.to_usize().unwrap_or(1),
        Some(Value::Float(f)) => *f as usize,
        _ => 1,
    };

    let new_val = args.get(2).cloned().unwrap_or(Value::Nil);

    if level == 0 || level > vm.frames.len() {
        return Ok(vec![Value::Nil]);
    }

    let frame = &vm.frames[vm.frames.len() - level];
    let proto = &frame.closure.proto;

    let current_ip = frame.ip as u32;
    let mut matching_locals: Vec<&crate::bytecode::proto::LocalVarInfo> = Vec::new();

    for info in &proto.local_names {
        if current_ip >= info.from_pc && current_ip <= info.to_pc {
            matching_locals.push(info);
        }
    }

    if idx == 0 || idx > matching_locals.len() {
        return Ok(vec![Value::Nil]);
    }

    let local_info = matching_locals[idx - 1];
    let reg_idx = local_info.reg;
    let stack_slot = frame.base + reg_idx as usize;
    if stack_slot >= vm.stack.len() {
        vm.stack.resize(stack_slot + 1, Value::Nil);
    }
    vm.stack[stack_slot] = new_val;

    Ok(vec![Value::String((local_info.name.clone()).into())])
}

fn debug_getupvalue(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
    let Some(Value::Closure(c)) = args.first() else {
        return Err("bad argument #1 to 'debug.getupvalue' (function expected)".to_string());
    };
    let idx = match args.get(1) {
        Some(Value::Int(i)) => i.to_usize().unwrap_or(1),
        Some(Value::Float(f)) => *f as usize,
        _ => 1,
    };
    if idx == 0 || idx > c.upvalues.len() {
        return Ok(vec![Value::Nil]);
    }
    let val = _vm.upvalue_get(&c.upvalues[idx - 1]);
    Ok(vec![Value::String((format!("upval_{}", idx)).into()), val])
}

fn debug_setupvalue(_vm: &mut VM, args: &[Value]) -> Result<Vec<Value>, String> {
    let Some(Value::Closure(c)) = args.first() else {
        return Err("bad argument #1 to 'debug.setupvalue' (function expected)".to_string());
    };
    let idx = match args.get(1) {
        Some(Value::Int(i)) => i.to_usize().unwrap_or(1),
        Some(Value::Float(f)) => *f as usize,
        _ => 1,
    };
    let new_val = args.get(2).cloned().unwrap_or(Value::Nil);
    if idx == 0 || idx > c.upvalues.len() {
        return Ok(vec![Value::Nil]);
    }
    let uv = c.upvalues[idx - 1].clone();
    _vm.upvalue_set(&uv, new_val);
    Ok(vec![Value::String((format!("upval_{}", idx)).into())])
}