use luau_bytecode::model::Instruction;
use luau_bytecode::opcodes::Opcode;
use luau_common::{BStr, ByteSlice};
use luau_printf::Arg;
use crate::function::Proto;
use crate::handle::RawHandle;
use crate::handle::sealed::Sealed;
use crate::memory::MemoryRuntime;
use crate::metamethod::{MetamethodRuntime, TmEvent};
use crate::state::ThreadState;
use crate::state::{CallInfo, ExecutionDisable};
use crate::string::StringFormatting;
use crate::string::TString;
use crate::string::{LuaString, printf_error_message};
use crate::thread::Thread;
use crate::value::TValue;
use crate::{VmError, VmErrorResult, VmResult};
#[allow(
clippy::missing_safety_doc,
reason = "all methods share the capability-level safety contract"
)]
pub trait DebugRuntime: Sealed {
unsafe fn type_error<T>(&self, object: TValue, operation: &str) -> VmErrorResult<T>;
unsafe fn for_error<T>(&self, object: TValue, what: &str) -> VmErrorResult<T>;
unsafe fn concat_error<T>(&self, left: TValue, right: TValue) -> VmErrorResult<T>;
unsafe fn arith_error<T>(
&self,
left: TValue,
right: TValue,
operation: TmEvent,
) -> VmErrorResult<T>;
unsafe fn order_error<T>(
&self,
left: TValue,
right: TValue,
operation: TmEvent,
) -> VmErrorResult<T>;
unsafe fn index_error<T>(&self, object: TValue, key: TValue) -> VmErrorResult<T>;
unsafe fn method_error<T>(&self, object: TValue, key: TString) -> VmErrorResult<T>;
unsafe fn missing_member_error<T>(&self, object: TValue, key: TValue) -> VmErrorResult<T>;
unsafe fn readonly_error<T>(&self) -> VmErrorResult<T>;
unsafe fn run_error<'a, T, F, A>(&self, format: F, args: A) -> VmErrorResult<T>
where
F: AsRef<[u8]>,
A: AsMut<[luau_printf::Arg<'a>]>;
unsafe fn push_error(&self, error: &BStr) -> VmErrorResult;
unsafe fn breakpoint_internal(&self, proto: Proto, line: i32, enable: bool) -> VmErrorResult;
unsafe fn on_break(&self) -> bool;
unsafe fn is_native(&self, level: i32) -> i32;
unsafe fn has_native(&self, level: i32) -> i32;
}
pub const LUA_ID_SIZE: usize = 256;
pub struct LuaDebug {
pub name: Option<LuaString>,
pub what: LuaString,
pub source: LuaString,
pub short_src: [u8; LUA_ID_SIZE],
pub linedefined: i32,
pub currentline: i32,
pub nupvals: u8,
pub nparams: u8,
pub is_vararg: bool,
pub userdata: *mut (),
}
impl Default for LuaDebug {
fn default() -> Self {
Self {
name: None,
what: LuaString::from_static(b"".as_bstr()),
source: LuaString::from_static(b"".as_bstr()),
short_src: [0; LUA_ID_SIZE],
linedefined: 0,
currentline: 0,
nupvals: 0,
nparams: 0,
is_vararg: false,
userdata: core::ptr::null_mut(),
}
}
}
impl LuaDebug {
pub fn short_src(&self) -> &[u8] {
let len = self
.short_src
.iter()
.position(|byte| *byte == 0)
.unwrap_or(self.short_src.len());
&self.short_src[..len]
}
pub(crate) fn set_short_src(&mut self, source: &[u8]) {
self.short_src.fill(0);
let len = source.len().min(self.short_src.len());
self.short_src[..len].copy_from_slice(&source[..len]);
}
}
pub type LuaHook = fn(&Thread, &mut LuaDebug) -> VmResult;
pub type LuaDebugInterruptHook = fn(&Thread, &mut LuaDebug) -> VmErrorResult;
pub type LuaCoverage = fn(*mut (), Option<&BStr>, i32, i32, &[i32]);
pub type LuaCounterFunction = fn(*mut (), Option<&BStr>, i32);
pub type LuaCounterValue = fn(*mut (), i32, i32, u64);
pub(crate) fn chunk_id<'a>(buffer: &'a mut [u8], source: &'a [u8]) -> &'a [u8] {
if source.first() == Some(&b'=') {
if source.len() <= buffer.len() {
return &source[1..];
}
let copy_len = buffer.len().saturating_sub(1);
buffer[..copy_len].copy_from_slice(&source[1..1 + copy_len]);
return &buffer[..copy_len];
}
if source.first() == Some(&b'@') {
if source.len() <= buffer.len() {
return &source[1..];
}
buffer[..3].copy_from_slice(b"...");
let tail_len = buffer.len() - 4;
let start = source.len() - tail_len;
buffer[3..3 + tail_len].copy_from_slice(&source[start..start + tail_len]);
return &buffer[..buffer.len() - 1];
}
let len = source
.iter()
.position(|byte| *byte == b'\n' || *byte == b'\r')
.unwrap_or(source.len());
let source = &source[..len];
let mut room = buffer.len() - b"[string \"...\"]".len() - 1;
if len < room {
room = len;
}
let mut write = 0;
buffer[write..write + 9].copy_from_slice(b"[string \"");
write += 9;
buffer[write..write + room].copy_from_slice(&source[..room]);
write += room;
if room < source.len() {
buffer[write..write + 3].copy_from_slice(b"...");
write += 3;
}
buffer[write..write + 2].copy_from_slice(b"\"]");
write += 2;
&buffer[..write]
}
pub(crate) fn current_pc(call_info: CallInfo, proto: Proto) -> i32 {
unsafe { proto.pc_rel(call_info.saved_pc()) }
}
pub(crate) fn lua_proto(call_info: CallInfo) -> Option<Proto> {
if unsafe { call_info.is_lua() } {
unsafe { call_info.function_closure().proto() }
} else {
None
}
}
fn patch_breakpoint(
thread: &Thread,
proto: Proto,
line: i32,
enable: bool,
disable: Option<ExecutionDisable>,
) -> VmErrorResult {
let proto_ref = unsafe { proto.as_ptr().as_ref().unwrap_unchecked() };
if proto_ref.line_info.is_null() || (disable.is_none() && !proto_ref.exec_data.is_null()) {
for index in 0..proto_ref.size_p as usize {
let child = unsafe { proto.child_proto(index).unwrap_unchecked() };
patch_breakpoint(thread, child, line, enable, disable)?;
}
return Ok(());
}
for index in 0..proto_ref.size_code as usize {
let instruction = Instruction::new(unsafe { *proto_ref.code.add(index) });
if unsafe { instruction.opcode_unchecked() } == Opcode::PrepVarargs {
continue;
}
if unsafe { proto.get_line(index as i32) } != line {
continue;
}
if proto_ref.debug_insn.is_null() {
let size = proto_ref.size_code as usize;
let debug_insn = unsafe { thread.new_array::<u8>(size, proto_ref.memcat)? };
for debug_index in 0..size {
unsafe {
*debug_insn.add(debug_index) =
Instruction::new(*proto_ref.code.add(debug_index)).opcode_unchecked() as u8;
}
}
unsafe {
proto.as_ptr().as_mut().unwrap_unchecked().debug_insn = debug_insn;
}
}
let opcode = if enable {
Opcode::Break
} else {
let original = unsafe { *proto_ref.debug_insn.add(index) };
Opcode::from_byte(original)
.expect("breakpoint debug opcode must decode to a valid Luau opcode")
};
unsafe {
let code = proto_ref.code.add(index);
*code = (Instruction::new(*code).word() & !0xff) | u32::from(opcode as u8);
}
if enable
&& !proto_ref.exec_data.is_null()
&& let Some(disable) = disable
{
unsafe { disable(thread, proto) };
}
break;
}
for index in 0..proto_ref.size_p as usize {
let child = unsafe { proto.child_proto(index).unwrap_unchecked() };
patch_breakpoint(thread, child, line, enable, disable)?;
}
Ok(())
}
impl DebugRuntime for Thread {
unsafe fn type_error<T>(&self, object: TValue, operation: &str) -> VmErrorResult<T> {
unsafe {
let type_name = self.obj_type_name(object);
crate::run_error!(self, "attempt to %s a %s value", operation, &type_name)
}
}
unsafe fn for_error<T>(&self, object: TValue, what: &str) -> VmErrorResult<T> {
unsafe {
let type_name = self.obj_type_name(object);
crate::run_error!(
self,
"invalid 'for' %s (number expected, got %s)",
what,
&type_name,
)
}
}
unsafe fn concat_error<T>(&self, left: TValue, right: TValue) -> VmErrorResult<T> {
unsafe {
let left_type = self.obj_type_name(left);
let right_type = self.obj_type_name(right);
crate::run_error!(
self,
"attempt to concatenate %s with %s",
&left_type,
&right_type,
)
}
}
unsafe fn arith_error<T>(
&self,
left: TValue,
right: TValue,
operation: TmEvent,
) -> VmErrorResult<T> {
unsafe {
let left_type = self.obj_type_name(left);
let right_type = self.obj_type_name(right);
let operation_name = self.global().tm_name(operation as usize);
let op_name = &operation_name.as_bytes()[2..];
let left_bytes = left_type.as_bytes();
let right_bytes = right_type.as_bytes();
if left_bytes == right_bytes {
crate::run_error!(
self,
"attempt to perform arithmetic (%s) on %s",
op_name,
left_bytes,
)
} else {
crate::run_error!(
self,
"attempt to perform arithmetic (%s) on %s and %s",
op_name,
left_bytes,
right_bytes,
)
}
}
}
unsafe fn order_error<T>(
&self,
left: TValue,
right: TValue,
operation: TmEvent,
) -> VmErrorResult<T> {
unsafe {
let left_type = self.obj_type_name(left);
let right_type = self.obj_type_name(right);
let op_name = match operation {
TmEvent::Lt => b"<".as_bstr(),
TmEvent::Le => b"<=".as_bstr(),
_ => b"==".as_bstr(),
};
crate::run_error!(
self,
"attempt to compare %s %s %s",
&left_type,
op_name,
&right_type,
)
}
}
unsafe fn index_error<T>(&self, object: TValue, key: TValue) -> VmErrorResult<T> {
unsafe {
let object_type = self.obj_type_name(object);
let key_type = self.obj_type_name(key);
let object_bytes = object_type.as_bytes();
let key_type_bytes = key_type.as_bytes();
if key.is_string() {
let string = key.string_value();
let key_bytes = string.as_bytes();
if string.as_ptr().as_ref().unwrap_unchecked().len <= 64 {
return crate::run_error!(
self,
"attempt to index %s with '%s'",
object_bytes,
key_bytes,
);
}
}
crate::run_error!(
self,
"attempt to index %s with %s",
object_bytes,
key_type_bytes,
)
}
}
unsafe fn method_error<T>(&self, object: TValue, key: TString) -> VmErrorResult<T> {
unsafe {
let object_type = self.obj_type_name(object);
crate::run_error!(
self,
"attempt to call missing method '%s' of %s",
key.as_bytes(),
&object_type,
)
}
}
unsafe fn missing_member_error<T>(&self, object: TValue, key: TValue) -> VmErrorResult<T> {
unsafe {
let object_type = self.obj_type_name(object);
let object_bytes = object_type.as_bytes();
if !key.is_string() {
let key_type = self.obj_type_name(key);
crate::run_error!(self, "cannot index %s with a %s", object_bytes, &key_type)
} else {
let key_string = key.string_value();
crate::run_error!(
self,
"this %s does not have a key named '%s'",
object_bytes,
key_string.as_bytes(),
)
}
}
}
unsafe fn readonly_error<T>(&self) -> VmErrorResult<T> {
unsafe { crate::run_error!(self, "attempt to modify a readonly table") }
}
unsafe fn run_error<'a, T, F, A>(&self, format: F, mut args: A) -> VmErrorResult<T>
where
F: AsRef<[u8]>,
A: AsMut<[Arg<'a>]>,
{
let format = format.as_ref();
let args = args.as_mut();
let mut formatted = Vec::new();
let message = if args.is_empty() {
format
} else {
formatted.reserve(format.len());
if let Err(error) =
luau_printf::printf_c_locale(&mut formatted, luau_printf::BStr::new(format), args)
{
unsafe { self.push_error(printf_error_message(&error).as_bstr())? };
return Err(VmError::Runtime);
}
formatted.as_slice()
};
let message_len = message
.iter()
.position(|byte| *byte == 0)
.unwrap_or(message.len())
.min(crate::thread::LUA_BUFFER_SIZE - 1);
unsafe { self.push_error(message[..message_len].as_bstr()) }?;
Err(VmError::Runtime)
}
unsafe fn push_error(&self, error: &BStr) -> VmErrorResult {
unsafe {
self.raw_check_stack(1)?;
let call_info = self.current_call_info();
if call_info.is_lua() {
let proto = lua_proto(call_info).unwrap_unchecked();
let source = proto.source().unwrap_unchecked();
let mut chunk_buffer = [0u8; LUA_ID_SIZE];
let chunk_id = chunk_id(&mut chunk_buffer, source.as_bytes());
let line = proto.get_line(current_pc(call_info, proto));
let args = [
Arg::string(chunk_id.as_bstr()),
Arg::int(line),
Arg::string(error),
];
self.push_fstring_internal("%s:%d: %s", args)?;
} else {
self.push_string(error)?;
}
}
Ok(())
}
unsafe fn breakpoint_internal(&self, proto: Proto, line: i32, enable: bool) -> VmErrorResult {
let disable = unsafe { self.global() }.execution_disable();
patch_breakpoint(self, proto, line, enable, disable)
}
unsafe fn on_break(&self) -> bool {
unsafe {
let call_info = self.current_call_info();
if call_info == self.base_call_info() {
return false;
}
if !call_info.is_lua() {
return false;
}
Instruction::new(call_info.saved_pc().read()).opcode_unchecked() == Opcode::Break
}
}
unsafe fn is_native(&self, level: i32) -> i32 {
let depth = unsafe { self.stack_depth() };
if level < 0 || level >= depth {
return 0;
}
let call_info = unsafe {
self.current_call_info_cursor()
.sub(level as usize)
.call_info_unchecked()
};
i32::from(
unsafe { call_info.as_ptr().as_ref().unwrap_unchecked().flags }
& crate::state::LUA_CALLINFO_NATIVE
!= 0,
)
}
unsafe fn has_native(&self, level: i32) -> i32 {
unsafe {
let depth = self.stack_depth();
if level < 0 || level >= depth {
return 0;
}
let call_info = self
.current_call_info_cursor()
.sub(level as usize)
.call_info_unchecked();
let Some(proto) = lua_proto(call_info) else {
return 0;
};
i32::from(
!proto
.as_ptr()
.as_ref()
.unwrap_unchecked()
.exec_data
.is_null(),
)
}
}
}