use luau_common::{BStr, ByteSlice};
use luau_vm::Thread as VmThread;
use luau_vm::debug::LuaDebug;
use luau_vm::state::ProtectedErrorAction;
use luau_vm::{VmErrorResult, VmResult};
use crate::callback::{raise_callback_error, raise_callback_vm_error};
use crate::error::Result;
use crate::lua::runtime::RuntimeData;
use crate::lua::{Lua, LuaRef};
use crate::thread::Thread;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum DebugAction {
#[default]
Continue,
Break,
}
#[derive(Clone, Copy)]
pub struct DebugContext<'callback> {
lua: LuaRef<'callback>,
debug: &'callback LuaDebug,
}
#[derive(Clone, Copy)]
pub struct ProtectedErrorContext<'callback> {
lua: LuaRef<'callback>,
}
pub trait DebugHandler: 'static {
fn step(&self, _context: DebugContext<'_>) -> Result<DebugAction> {
Ok(DebugAction::Continue)
}
fn breakpoint(&self, _context: DebugContext<'_>) -> Result<DebugAction> {
Ok(DebugAction::Continue)
}
fn interrupt(&self, _context: DebugContext<'_>) -> Result<()> {
Ok(())
}
fn protected_error(&self, _context: ProtectedErrorContext<'_>) -> DebugAction {
DebugAction::Continue
}
}
type DebugCallback = Box<dyn for<'callback> Fn(DebugContext<'callback>) -> Result<DebugAction>>;
type DebugInterruptCallback = Box<dyn for<'callback> Fn(DebugContext<'callback>) -> Result<()>>;
type ProtectedErrorCallback =
Box<dyn for<'callback> Fn(ProtectedErrorContext<'callback>) -> DebugAction>;
#[derive(Default)]
pub struct DebugHooks {
step: Option<DebugCallback>,
breakpoint: Option<DebugCallback>,
interrupt: Option<DebugInterruptCallback>,
protected_error: Option<ProtectedErrorCallback>,
}
impl DebugHooks {
pub const fn new() -> Self {
Self {
step: None,
breakpoint: None,
interrupt: None,
protected_error: None,
}
}
#[must_use]
pub fn on_step<F>(mut self, callback: F) -> Self
where
F: for<'callback> Fn(DebugContext<'callback>) -> Result<DebugAction> + 'static,
{
self.step = Some(Box::new(callback));
self
}
#[must_use]
pub fn on_breakpoint<F>(mut self, callback: F) -> Self
where
F: for<'callback> Fn(DebugContext<'callback>) -> Result<DebugAction> + 'static,
{
self.breakpoint = Some(Box::new(callback));
self
}
#[must_use]
pub fn on_interrupt<F>(mut self, callback: F) -> Self
where
F: for<'callback> Fn(DebugContext<'callback>) -> Result<()> + 'static,
{
self.interrupt = Some(Box::new(callback));
self
}
#[must_use]
pub fn on_protected_error<F>(mut self, callback: F) -> Self
where
F: for<'callback> Fn(ProtectedErrorContext<'callback>) -> DebugAction + 'static,
{
self.protected_error = Some(Box::new(callback));
self
}
}
impl DebugHandler for DebugHooks {
fn step(&self, context: DebugContext<'_>) -> Result<DebugAction> {
self.step
.as_ref()
.map(|callback| callback(context))
.unwrap_or(Ok(DebugAction::Continue))
}
fn breakpoint(&self, context: DebugContext<'_>) -> Result<DebugAction> {
self.breakpoint
.as_ref()
.map(|callback| callback(context))
.unwrap_or(Ok(DebugAction::Continue))
}
fn interrupt(&self, context: DebugContext<'_>) -> Result<()> {
self.interrupt
.as_ref()
.map(|callback| callback(context))
.unwrap_or(Ok(()))
}
fn protected_error(&self, context: ProtectedErrorContext<'_>) -> DebugAction {
self.protected_error
.as_ref()
.map(|callback| callback(context))
.unwrap_or(DebugAction::Continue)
}
}
impl Lua {
pub fn set_single_step(&mut self, enabled: bool) {
self.current_thread().set_single_step(enabled);
}
pub fn set_debug_handler(&mut self, handler: impl DebugHandler) {
self.runtime
.callbacks_mut()
.set_debug_handler(Box::new(handler));
self.install_callbacks();
}
pub fn remove_debug_handler(&mut self) {
self.runtime.callbacks_mut().remove_debug_handler();
self.install_callbacks();
}
}
impl Thread<'_> {
pub fn set_single_step(&self, enabled: bool) {
unsafe {
self.as_vm().single_step(i32::from(enabled));
}
}
}
impl<'callback> DebugContext<'callback> {
pub(in crate::hooks) const fn new(lua: LuaRef<'callback>, debug: &'callback LuaDebug) -> Self {
Self { lua, debug }
}
pub const fn lua(&self) -> LuaRef<'callback> {
self.lua
}
pub fn current_line(&self) -> Option<usize> {
usize::try_from(self.debug.currentline).ok()
}
pub fn name(&self) -> Option<&'callback BStr> {
self.debug.name.as_ref().map(|name| name.as_bstr())
}
pub fn source(&self) -> &'callback BStr {
self.debug.source.as_bstr()
}
pub fn short_source(&self) -> &'callback BStr {
self.debug.short_src().as_bstr()
}
pub fn what(&self) -> &'callback BStr {
self.debug.what.as_bstr()
}
pub fn line_defined(&self) -> Option<usize> {
usize::try_from(self.debug.linedefined).ok()
}
pub fn parameter_count(&self) -> u8 {
self.debug.nparams
}
pub fn upvalue_count(&self) -> u8 {
self.debug.nupvals
}
pub fn is_vararg(&self) -> bool {
self.debug.is_vararg
}
}
impl<'callback> ProtectedErrorContext<'callback> {
pub(in crate::hooks) const fn new(lua: LuaRef<'callback>) -> Self {
Self { lua }
}
pub const fn lua(&self) -> LuaRef<'callback> {
self.lua
}
}
pub(in crate::hooks) fn debug_step(thread: &VmThread, debug: &mut LuaDebug) -> VmResult {
let runtime = RuntimeData::from_thread(thread);
let Some(handler) = runtime.callbacks().debug_handler() else {
return Ok(());
};
runtime.with_thread(thread, || {
let context = DebugContext::new(LuaRef::new(thread, runtime), debug);
match handler.step(context) {
Ok(action) => apply_debug_action(thread, action),
Err(error) => raise_callback_error(thread, runtime, error),
}
})
}
pub(in crate::hooks) fn debug_break(thread: &VmThread, debug: &mut LuaDebug) -> VmResult {
let runtime = RuntimeData::from_thread(thread);
let Some(handler) = runtime.callbacks().debug_handler() else {
return Ok(());
};
runtime.with_thread(thread, || {
let context = DebugContext::new(LuaRef::new(thread, runtime), debug);
match handler.breakpoint(context) {
Ok(action) => apply_debug_action(thread, action),
Err(error) => raise_callback_error(thread, runtime, error),
}
})
}
pub(in crate::hooks) fn debug_interrupt(thread: &VmThread, debug: &mut LuaDebug) -> VmErrorResult {
let runtime = RuntimeData::from_thread(thread);
let Some(handler) = runtime.callbacks().debug_handler() else {
return Ok(());
};
runtime.with_thread(thread, || {
let context = DebugContext::new(LuaRef::new(thread, runtime), debug);
handler
.interrupt(context)
.or_else(|error| raise_callback_vm_error(thread, runtime, error))
})
}
pub(in crate::hooks) fn debug_protected_error(thread: &VmThread) -> ProtectedErrorAction {
let runtime = RuntimeData::from_thread(thread);
runtime.with_thread(thread, || {
let action = runtime
.callbacks()
.debug_handler()
.map(|handler| {
handler.protected_error(ProtectedErrorContext::new(LuaRef::new(thread, runtime)))
})
.unwrap_or(DebugAction::Continue);
match action {
DebugAction::Continue => ProtectedErrorAction::Continue,
DebugAction::Break => ProtectedErrorAction::Break,
}
})
}
fn apply_debug_action(thread: &VmThread, action: DebugAction) -> VmResult {
match action {
DebugAction::Continue => Ok(()),
DebugAction::Break => unsafe { thread.break_current().map(|_| ()) },
}
}