1use luau_bytecode::model::Instruction;
2use luau_bytecode::opcodes::Opcode;
3use luau_common::{BStr, ByteSlice};
4use luau_printf::Arg;
5
6use crate::function::Proto;
7use crate::handle::RawHandle;
8use crate::handle::sealed::Sealed;
9use crate::memory::MemoryRuntime;
10use crate::metamethod::{MetamethodRuntime, TmEvent};
11use crate::state::ThreadState;
12use crate::state::{CallInfo, ExecutionDisable};
13use crate::string::StringFormatting;
14use crate::string::TString;
15use crate::string::{LuaString, printf_error_message};
16use crate::thread::Thread;
17use crate::value::TValue;
18use crate::{VmError, VmErrorResult, VmResult};
19
20#[allow(
28 clippy::missing_safety_doc,
29 reason = "all methods share the capability-level safety contract"
30)]
31pub trait DebugRuntime: Sealed {
32 unsafe fn type_error<T>(&self, object: TValue, operation: &str) -> VmErrorResult<T>;
34
35 unsafe fn for_error<T>(&self, object: TValue, what: &str) -> VmErrorResult<T>;
37
38 unsafe fn concat_error<T>(&self, left: TValue, right: TValue) -> VmErrorResult<T>;
40
41 unsafe fn arith_error<T>(
43 &self,
44 left: TValue,
45 right: TValue,
46 operation: TmEvent,
47 ) -> VmErrorResult<T>;
48
49 unsafe fn order_error<T>(
51 &self,
52 left: TValue,
53 right: TValue,
54 operation: TmEvent,
55 ) -> VmErrorResult<T>;
56
57 unsafe fn index_error<T>(&self, object: TValue, key: TValue) -> VmErrorResult<T>;
59
60 unsafe fn method_error<T>(&self, object: TValue, key: TString) -> VmErrorResult<T>;
62
63 unsafe fn missing_member_error<T>(&self, object: TValue, key: TValue) -> VmErrorResult<T>;
65
66 unsafe fn readonly_error<T>(&self) -> VmErrorResult<T>;
68
69 unsafe fn run_error<'a, T, F, A>(&self, format: F, args: A) -> VmErrorResult<T>
71 where
72 F: AsRef<[u8]>,
73 A: AsMut<[luau_printf::Arg<'a>]>;
74
75 unsafe fn push_error(&self, error: &BStr) -> VmErrorResult;
77
78 unsafe fn breakpoint_internal(&self, proto: Proto, line: i32, enable: bool) -> VmErrorResult;
80
81 unsafe fn on_break(&self) -> bool;
83
84 unsafe fn is_native(&self, level: i32) -> i32;
86
87 unsafe fn has_native(&self, level: i32) -> i32;
89}
90
91pub const LUA_ID_SIZE: usize = 256;
93
94pub struct LuaDebug {
95 pub name: Option<LuaString>,
96 pub what: LuaString,
97 pub source: LuaString,
98 pub short_src: [u8; LUA_ID_SIZE],
99 pub linedefined: i32,
100 pub currentline: i32,
101 pub nupvals: u8,
102 pub nparams: u8,
103 pub is_vararg: bool,
104 pub userdata: *mut (),
105}
106
107impl Default for LuaDebug {
108 fn default() -> Self {
109 Self {
110 name: None,
111 what: LuaString::from_static(b"".as_bstr()),
112 source: LuaString::from_static(b"".as_bstr()),
113 short_src: [0; LUA_ID_SIZE],
114 linedefined: 0,
115 currentline: 0,
116 nupvals: 0,
117 nparams: 0,
118 is_vararg: false,
119 userdata: core::ptr::null_mut(),
120 }
121 }
122}
123
124impl LuaDebug {
125 pub fn short_src(&self) -> &[u8] {
126 let len = self
127 .short_src
128 .iter()
129 .position(|byte| *byte == 0)
130 .unwrap_or(self.short_src.len());
131 &self.short_src[..len]
132 }
133
134 pub(crate) fn set_short_src(&mut self, source: &[u8]) {
135 self.short_src.fill(0);
136 let len = source.len().min(self.short_src.len());
137 self.short_src[..len].copy_from_slice(&source[..len]);
138 }
139}
140
141pub type LuaHook = fn(&Thread, &mut LuaDebug) -> VmResult;
142
143pub type LuaDebugInterruptHook = fn(&Thread, &mut LuaDebug) -> VmErrorResult;
144
145pub type LuaCoverage = fn(*mut (), Option<&BStr>, i32, i32, &[i32]);
146
147pub type LuaCounterFunction = fn(*mut (), Option<&BStr>, i32);
148
149pub type LuaCounterValue = fn(*mut (), i32, i32, u64);
150
151pub(crate) fn chunk_id<'a>(buffer: &'a mut [u8], source: &'a [u8]) -> &'a [u8] {
153 if source.first() == Some(&b'=') {
154 if source.len() <= buffer.len() {
155 return &source[1..];
156 }
157
158 let copy_len = buffer.len().saturating_sub(1);
159 buffer[..copy_len].copy_from_slice(&source[1..1 + copy_len]);
160 return &buffer[..copy_len];
161 }
162
163 if source.first() == Some(&b'@') {
164 if source.len() <= buffer.len() {
165 return &source[1..];
166 }
167
168 buffer[..3].copy_from_slice(b"...");
169 let tail_len = buffer.len() - 4;
170 let start = source.len() - tail_len;
171 buffer[3..3 + tail_len].copy_from_slice(&source[start..start + tail_len]);
172 return &buffer[..buffer.len() - 1];
173 }
174
175 let len = source
176 .iter()
177 .position(|byte| *byte == b'\n' || *byte == b'\r')
178 .unwrap_or(source.len());
179 let source = &source[..len];
180 let mut room = buffer.len() - b"[string \"...\"]".len() - 1;
181 if len < room {
182 room = len;
183 }
184
185 let mut write = 0;
186 buffer[write..write + 9].copy_from_slice(b"[string \"");
187 write += 9;
188
189 buffer[write..write + room].copy_from_slice(&source[..room]);
190 write += room;
191
192 if room < source.len() {
193 buffer[write..write + 3].copy_from_slice(b"...");
194 write += 3;
195 }
196
197 buffer[write..write + 2].copy_from_slice(b"\"]");
198 write += 2;
199 &buffer[..write]
200}
201
202pub(crate) fn current_pc(call_info: CallInfo, proto: Proto) -> i32 {
204 unsafe { proto.pc_rel(call_info.saved_pc()) }
205}
206
207pub(crate) fn lua_proto(call_info: CallInfo) -> Option<Proto> {
209 if unsafe { call_info.is_lua() } {
210 unsafe { call_info.function_closure().proto() }
211 } else {
212 None
213 }
214}
215
216fn patch_breakpoint(
218 thread: &Thread,
219 proto: Proto,
220 line: i32,
221 enable: bool,
222 disable: Option<ExecutionDisable>,
223) -> VmErrorResult {
224 let proto_ref = unsafe { proto.as_ptr().as_ref().unwrap_unchecked() };
225 if proto_ref.line_info.is_null() || (disable.is_none() && !proto_ref.exec_data.is_null()) {
226 for index in 0..proto_ref.size_p as usize {
227 let child = unsafe { proto.child_proto(index).unwrap_unchecked() };
228 patch_breakpoint(thread, child, line, enable, disable)?;
229 }
230
231 return Ok(());
232 }
233
234 for index in 0..proto_ref.size_code as usize {
235 let instruction = Instruction::new(unsafe { *proto_ref.code.add(index) });
236 if unsafe { instruction.opcode_unchecked() } == Opcode::PrepVarargs {
237 continue;
238 }
239
240 if unsafe { proto.get_line(index as i32) } != line {
241 continue;
242 }
243
244 if proto_ref.debug_insn.is_null() {
245 let size = proto_ref.size_code as usize;
246 let debug_insn = unsafe { thread.new_array::<u8>(size, proto_ref.memcat)? };
247
248 for debug_index in 0..size {
249 unsafe {
250 *debug_insn.add(debug_index) =
251 Instruction::new(*proto_ref.code.add(debug_index)).opcode_unchecked() as u8;
252 }
253 }
254
255 unsafe {
256 proto.as_ptr().as_mut().unwrap_unchecked().debug_insn = debug_insn;
257 }
258 }
259
260 let opcode = if enable {
261 Opcode::Break
262 } else {
263 let original = unsafe { *proto_ref.debug_insn.add(index) };
264 Opcode::from_byte(original)
265 .expect("breakpoint debug opcode must decode to a valid Luau opcode")
266 };
267
268 unsafe {
269 let code = proto_ref.code.add(index);
270 *code = (Instruction::new(*code).word() & !0xff) | u32::from(opcode as u8);
271 }
272
273 if enable
274 && !proto_ref.exec_data.is_null()
275 && let Some(disable) = disable
276 {
277 unsafe { disable(thread, proto) };
278 }
279
280 break;
281 }
282
283 for index in 0..proto_ref.size_p as usize {
284 let child = unsafe { proto.child_proto(index).unwrap_unchecked() };
285 patch_breakpoint(thread, child, line, enable, disable)?;
286 }
287 Ok(())
288}
289
290impl DebugRuntime for Thread {
291 unsafe fn type_error<T>(&self, object: TValue, operation: &str) -> VmErrorResult<T> {
293 unsafe {
294 let type_name = self.obj_type_name(object);
295 crate::run_error!(self, "attempt to %s a %s value", operation, &type_name)
296 }
297 }
298
299 unsafe fn for_error<T>(&self, object: TValue, what: &str) -> VmErrorResult<T> {
301 unsafe {
302 let type_name = self.obj_type_name(object);
303 crate::run_error!(
304 self,
305 "invalid 'for' %s (number expected, got %s)",
306 what,
307 &type_name,
308 )
309 }
310 }
311
312 unsafe fn concat_error<T>(&self, left: TValue, right: TValue) -> VmErrorResult<T> {
314 unsafe {
315 let left_type = self.obj_type_name(left);
316 let right_type = self.obj_type_name(right);
317 crate::run_error!(
318 self,
319 "attempt to concatenate %s with %s",
320 &left_type,
321 &right_type,
322 )
323 }
324 }
325
326 unsafe fn arith_error<T>(
328 &self,
329 left: TValue,
330 right: TValue,
331 operation: TmEvent,
332 ) -> VmErrorResult<T> {
333 unsafe {
334 let left_type = self.obj_type_name(left);
335 let right_type = self.obj_type_name(right);
336 let operation_name = self.global().tm_name(operation as usize);
337 let op_name = &operation_name.as_bytes()[2..];
338 let left_bytes = left_type.as_bytes();
339 let right_bytes = right_type.as_bytes();
340
341 if left_bytes == right_bytes {
342 crate::run_error!(
343 self,
344 "attempt to perform arithmetic (%s) on %s",
345 op_name,
346 left_bytes,
347 )
348 } else {
349 crate::run_error!(
350 self,
351 "attempt to perform arithmetic (%s) on %s and %s",
352 op_name,
353 left_bytes,
354 right_bytes,
355 )
356 }
357 }
358 }
359
360 unsafe fn order_error<T>(
362 &self,
363 left: TValue,
364 right: TValue,
365 operation: TmEvent,
366 ) -> VmErrorResult<T> {
367 unsafe {
368 let left_type = self.obj_type_name(left);
369 let right_type = self.obj_type_name(right);
370 let op_name = match operation {
371 TmEvent::Lt => b"<".as_bstr(),
372 TmEvent::Le => b"<=".as_bstr(),
373 _ => b"==".as_bstr(),
374 };
375 crate::run_error!(
376 self,
377 "attempt to compare %s %s %s",
378 &left_type,
379 op_name,
380 &right_type,
381 )
382 }
383 }
384
385 unsafe fn index_error<T>(&self, object: TValue, key: TValue) -> VmErrorResult<T> {
387 unsafe {
388 let object_type = self.obj_type_name(object);
389 let key_type = self.obj_type_name(key);
390 let object_bytes = object_type.as_bytes();
391 let key_type_bytes = key_type.as_bytes();
392
393 if key.is_string() {
394 let string = key.string_value();
395 let key_bytes = string.as_bytes();
396 if string.as_ptr().as_ref().unwrap_unchecked().len <= 64 {
397 return crate::run_error!(
398 self,
399 "attempt to index %s with '%s'",
400 object_bytes,
401 key_bytes,
402 );
403 }
404 }
405
406 crate::run_error!(
407 self,
408 "attempt to index %s with %s",
409 object_bytes,
410 key_type_bytes,
411 )
412 }
413 }
414
415 unsafe fn method_error<T>(&self, object: TValue, key: TString) -> VmErrorResult<T> {
417 unsafe {
418 let object_type = self.obj_type_name(object);
419 crate::run_error!(
420 self,
421 "attempt to call missing method '%s' of %s",
422 key.as_bytes(),
423 &object_type,
424 )
425 }
426 }
427
428 unsafe fn missing_member_error<T>(&self, object: TValue, key: TValue) -> VmErrorResult<T> {
430 unsafe {
431 let object_type = self.obj_type_name(object);
432 let object_bytes = object_type.as_bytes();
433
434 if !key.is_string() {
435 let key_type = self.obj_type_name(key);
436 crate::run_error!(self, "cannot index %s with a %s", object_bytes, &key_type)
437 } else {
438 let key_string = key.string_value();
439 crate::run_error!(
440 self,
441 "this %s does not have a key named '%s'",
442 object_bytes,
443 key_string.as_bytes(),
444 )
445 }
446 }
447 }
448
449 unsafe fn readonly_error<T>(&self) -> VmErrorResult<T> {
451 unsafe { crate::run_error!(self, "attempt to modify a readonly table") }
452 }
453
454 unsafe fn run_error<'a, T, F, A>(&self, format: F, mut args: A) -> VmErrorResult<T>
456 where
457 F: AsRef<[u8]>,
458 A: AsMut<[Arg<'a>]>,
459 {
460 let format = format.as_ref();
461 let args = args.as_mut();
462 let mut formatted = Vec::new();
463 let message = if args.is_empty() {
464 format
465 } else {
466 formatted.reserve(format.len());
467 if let Err(error) =
468 luau_printf::printf_c_locale(&mut formatted, luau_printf::BStr::new(format), args)
469 {
470 unsafe { self.push_error(printf_error_message(&error).as_bstr())? };
471 return Err(VmError::Runtime);
472 }
473 formatted.as_slice()
474 };
475 let message_len = message
476 .iter()
477 .position(|byte| *byte == 0)
478 .unwrap_or(message.len())
479 .min(crate::thread::LUA_BUFFER_SIZE - 1);
480 unsafe { self.push_error(message[..message_len].as_bstr()) }?;
481 Err(VmError::Runtime)
482 }
483
484 unsafe fn push_error(&self, error: &BStr) -> VmErrorResult {
486 unsafe {
487 self.raw_check_stack(1)?;
488
489 let call_info = self.current_call_info();
490 if call_info.is_lua() {
491 let proto = lua_proto(call_info).unwrap_unchecked();
492 let source = proto.source().unwrap_unchecked();
493 let mut chunk_buffer = [0u8; LUA_ID_SIZE];
494 let chunk_id = chunk_id(&mut chunk_buffer, source.as_bytes());
495 let line = proto.get_line(current_pc(call_info, proto));
496 let args = [
497 Arg::string(chunk_id.as_bstr()),
498 Arg::int(line),
499 Arg::string(error),
500 ];
501 self.push_fstring_internal("%s:%d: %s", args)?;
502 } else {
503 self.push_string(error)?;
504 }
505 }
506 Ok(())
507 }
508
509 unsafe fn breakpoint_internal(&self, proto: Proto, line: i32, enable: bool) -> VmErrorResult {
511 let disable = unsafe { self.global() }.execution_disable();
512 patch_breakpoint(self, proto, line, enable, disable)
513 }
514
515 unsafe fn on_break(&self) -> bool {
517 unsafe {
518 let call_info = self.current_call_info();
519 if call_info == self.base_call_info() {
520 return false;
521 }
522
523 if !call_info.is_lua() {
524 return false;
525 }
526
527 Instruction::new(call_info.saved_pc().read()).opcode_unchecked() == Opcode::Break
528 }
529 }
530
531 unsafe fn is_native(&self, level: i32) -> i32 {
533 let depth = unsafe { self.stack_depth() };
534 if level < 0 || level >= depth {
535 return 0;
536 }
537
538 let call_info = unsafe {
539 self.current_call_info_cursor()
540 .sub(level as usize)
541 .call_info_unchecked()
542 };
543 i32::from(
544 unsafe { call_info.as_ptr().as_ref().unwrap_unchecked().flags }
545 & crate::state::LUA_CALLINFO_NATIVE
546 != 0,
547 )
548 }
549
550 unsafe fn has_native(&self, level: i32) -> i32 {
552 unsafe {
553 let depth = self.stack_depth();
554 if level < 0 || level >= depth {
555 return 0;
556 }
557
558 let call_info = self
559 .current_call_info_cursor()
560 .sub(level as usize)
561 .call_info_unchecked();
562 let Some(proto) = lua_proto(call_info) else {
563 return 0;
564 };
565
566 i32::from(
567 !proto
568 .as_ptr()
569 .as_ref()
570 .unwrap_unchecked()
571 .exec_data
572 .is_null(),
573 )
574 }
575 }
576}