use crate::support::{ConformanceOptions, run_fixture, with_state};
use luau_common::{ByteSlice, flags};
use luau_compiler::CompileOptions;
use luau_vm::internal::state::{RawLuaState, ThreadState};
use luau_vm::lua::Lua;
use luau_vm::state::ProtectedErrorAction;
use luau_vm::thread::{LUA_GLOBALS_INDEX, LUA_MIN_STACK, Thread};
use luau_vm::types::LUA_TNIL;
use luau_vm::{LuaDebug, NativeCallContext, NativeCallResult};
use luau_vm::{VmControl, VmError, VmErrorResult, VmExit, VmResult};
use std::ptr::{self, NonNull};
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
static INTERRUPT_INSPECTION_SKIP_BREAK: AtomicBool = AtomicBool::new(false);
static INTERRUPT_ERROR_INSPECTION_TARGET: AtomicUsize = AtomicUsize::new(0);
static INTERRUPT_ERROR_INSPECTION_STEP: AtomicUsize = AtomicUsize::new(0);
static INTERRUPT_INDEX: AtomicUsize = AtomicUsize::new(0);
static TAG_METHOD_ERROR_INDEX: AtomicUsize = AtomicUsize::new(0);
static TAG_METHOD_ERROR_BREAK: AtomicBool = AtomicBool::new(false);
static DEBUGGER_BREAK_HITS: AtomicUsize = AtomicUsize::new(0);
struct InterruptedThread(Thread);
unsafe impl Send for InterruptedThread {}
static DEBUGGER_INTERRUPT_THREAD: Mutex<Option<InterruptedThread>> = Mutex::new(None);
static DEBUGGER_SINGLE_STEP: AtomicBool = AtomicBool::new(false);
static DEBUGGER_STEP_HITS: AtomicUsize = AtomicUsize::new(0);
const TAG_METHOD_ERROR_EXPECTED_HITS: [i32; 3] = [37, 54, 73];
const INTERRUPT_EXPECTED_HITS: [i32; 22] = [
11, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 20, 15, 15, 15, 15, 18, 25, 23, 26,
];
unsafe fn error_str(thread: &Thread, message: &str) -> VmExit {
unsafe { thread.lua_error::<()>(message, []) }
.unwrap_err()
.into()
}
fn get_first_luau_frame_debug_info(thread: &Thread) -> Option<LuaDebug> {
let mut level = 0;
loop {
let mut ar = LuaDebug::default();
if unsafe { thread.get_info(level, "sl", &mut ar) }.expect("debug lookup should run") == 0 {
return None;
}
if ar.what.as_bytes() == b"Lua" {
return Some(ar);
}
level += 1;
}
}
fn interrupt_inspection_interrupt(thread: &Thread) -> VmResult {
unsafe {
if thread.is_yieldable() == 0 {
return Ok(());
}
let skip_break = INTERRUPT_INSPECTION_SKIP_BREAK.load(Ordering::Relaxed);
let result = if skip_break {
Ok(())
} else {
thread.break_current().map(|_| ())
};
INTERRUPT_INSPECTION_SKIP_BREAK.store(!skip_break, Ordering::Relaxed);
result
}
}
fn interrupt_inspection_hook(thread: &Thread, ar: &mut LuaDebug) -> VmResult {
unsafe {
assert_ne!(
{ thread.get_info(0, "nsl", ar) }.expect("debug lookup should run"),
0
);
}
Ok(())
}
fn interrupt_inspection_yield(thread: &Thread) -> bool {
let mut ar = LuaDebug::default();
assert_ne!(
unsafe { thread.get_info(0, "nsl", &mut ar) }.expect("debug lookup should run"),
0
);
unsafe { thread.call_hook(interrupt_inspection_hook, ptr::null_mut()) }
.expect("interrupt inspection hook should run");
false
}
fn interrupt_error_inspection_interrupt(thread: &Thread) -> VmResult {
unsafe {
let step = INTERRUPT_ERROR_INSPECTION_STEP.load(Ordering::Relaxed);
let target = INTERRUPT_ERROR_INSPECTION_TARGET.load(Ordering::Relaxed);
if step == target {
return Err(error_str(thread, "test"));
}
INTERRUPT_ERROR_INSPECTION_STEP.store(step + 1, Ordering::Relaxed);
Ok(())
}
}
fn interrupt_error_inspection_hook(thread: &Thread, ar: &mut LuaDebug) -> VmResult {
unsafe {
assert_ne!(
{ thread.get_info(0, "nsl", ar) }.expect("debug lookup should run"),
0
);
}
Ok(())
}
fn tag_method_error_protected_error(thread: &Thread) -> ProtectedErrorAction {
unsafe {
let ar = get_first_luau_frame_debug_info(thread);
assert_ne!(thread.is_yieldable(), 0);
let ar = ar.expect("expected Luau frame debug info");
let index = TAG_METHOD_ERROR_INDEX.fetch_add(1, Ordering::Relaxed);
assert!(index < TAG_METHOD_ERROR_EXPECTED_HITS.len());
assert_eq!(ar.currentline, TAG_METHOD_ERROR_EXPECTED_HITS[index]);
if TAG_METHOD_ERROR_BREAK.load(Ordering::Relaxed) {
ProtectedErrorAction::Break
} else {
ProtectedErrorAction::Continue
}
}
}
fn tag_method_error_yield(_: &Thread) -> bool {
true
}
fn coverage_helper(ctx: NativeCallContext) -> NativeCallResult {
let thread = ctx.raw_thread();
unsafe {
if thread.is_lua_function(1) == 0 {
return thread.lua_arg_error(1, "function").map_err(Into::into);
}
thread.create_table(0, 0)?;
thread.get_coverage(
1,
core::ptr::from_ref(thread).cast_mut().cast(),
|context, function, linedefined, depth, hits| {
let thread = &*context.cast::<Thread>();
thread
.create_table(0, 3)
.expect("coverage entry table should be created");
thread
.push_optional_string(function)
.expect("coverage function name should push");
thread
.set_field(-2, "name")
.expect("coverage function name should set");
thread
.push_integer(linedefined)
.expect("coverage line should push");
thread
.set_field(-2, "linedefined")
.expect("coverage line should set");
thread
.push_integer(depth)
.expect("coverage depth should push");
thread
.set_field(-2, "depth")
.expect("coverage depth should set");
for (index, hit_count) in hits.iter().copied().enumerate() {
if hit_count != -1 {
thread
.push_integer(hit_count)
.expect("coverage hit count should push");
thread
.raw_seti(-2, index as i32)
.expect("coverage hit count should set");
}
}
thread
.raw_seti(-2, thread.obj_len(-2) + 1)
.expect("coverage entry should append");
},
)?;
}
Ok(1)
}
unsafe fn setup_coverage(thread: &Thread) {
unsafe {
thread
.push_native_closure_k(coverage_helper, Some("getcoverage"), 0, None)
.expect("coverage helper should push");
thread
.set_field(LUA_GLOBALS_INDEX, "getcoverage")
.expect("coverage helper should set");
}
}
fn debugger_breakpoint(ctx: NativeCallContext) -> NativeCallResult {
let thread = ctx.raw_thread();
unsafe {
let line = thread.check_integer(1)?;
let enabled = thread.opt_boolean(2, 1)?;
let mut ar = LuaDebug::default();
assert_ne!(
thread
.get_info(thread.stack_depth() - 1, "f", &mut ar)
.expect("debug lookup should run"),
0
);
let _ = thread.breakpoint(-1, line, enabled)?;
thread.pop(1);
Ok(0)
}
}
fn debugger_step(_: &Thread, _: &mut LuaDebug) -> VmResult {
DEBUGGER_STEP_HITS.fetch_add(1, Ordering::Relaxed);
Ok(())
}
fn debugger_break(thread: &Thread, _: &mut LuaDebug) -> VmResult {
unsafe {
let break_hits = DEBUGGER_BREAK_HITS.fetch_add(1, Ordering::Relaxed) + 1;
let _ = thread.debug_trace().expect("debug trace should build");
if break_hits % 2 == 1 {
return thread.break_current().map(|_| ());
}
}
Ok(())
}
fn debugger_interrupt(_thread: &Thread, ar: &mut LuaDebug) -> VmErrorResult {
let mut interrupted_thread = DEBUGGER_INTERRUPT_THREAD.lock().unwrap();
assert!(interrupted_thread.is_none());
assert!(
!ar.userdata.is_null(),
"debug interrupt should carry thread userdata"
);
let interrupted_raw = NonNull::new(ar.userdata.cast::<RawLuaState>())
.expect("debug interrupt should carry thread userdata");
*interrupted_thread = Some(InterruptedThread(unsafe {
Thread::from_raw(interrupted_raw)
}));
Ok(())
}
unsafe fn setup_debugger(thread: &Thread) {
unsafe {
let global = thread.global();
let callbacks = &mut *global.callbacks();
thread.single_step(i32::from(DEBUGGER_SINGLE_STEP.load(Ordering::Relaxed)));
callbacks.debug_step = Some(debugger_step);
callbacks.debug_break = Some(debugger_break);
callbacks.debug_interrupt = Some(debugger_interrupt);
thread
.push_native_closure_k(debugger_breakpoint, Some("breakpoint"), 0, None)
.expect("debugger breakpoint should push");
thread
.set_field(LUA_GLOBALS_INDEX, "breakpoint")
.expect("debugger breakpoint should set");
}
}
fn debugger_yield(thread: &Thread) -> bool {
let break_hits = DEBUGGER_BREAK_HITS.load(Ordering::Relaxed);
assert_eq!(break_hits % 2, 1);
if !flags::LuauAutoStack.get() {
assert_ne!(unsafe { thread.check_stack(LUA_MIN_STACK as i32) }, 0);
}
match break_hits {
1 => {
let argument =
unsafe { thread.get_argument(0, 1) }.expect("debug argument lookup should run");
assert_ne!(argument, 0);
assert_eq!(unsafe { thread.to_integer(-1) }, Some(50));
unsafe { thread.pop(1) };
let vararg =
unsafe { thread.get_argument(0, 2) }.expect("debug argument lookup should run");
assert_ne!(vararg, 0);
assert_eq!(unsafe { thread.to_integer(-1) }, Some(42));
unsafe { thread.pop(1) };
let local = unsafe { thread.get_local(0, 1) }.expect("debug local lookup should run");
assert!(local.is_some());
assert_eq!(local.as_ref().unwrap().as_bytes(), b"b");
assert_eq!(unsafe { thread.to_integer(-1) }, Some(50));
unsafe { thread.pop(1) };
let mut ar = LuaDebug::default();
assert_ne!(
unsafe { thread.get_info(0, "f", &mut ar) }.expect("debug lookup should run"),
0
);
let upvalue =
unsafe { thread.get_upvalue(-1, 1) }.expect("debugger upvalue lookup should run");
assert!(upvalue.is_some());
assert_eq!(upvalue.as_ref().unwrap().as_bytes(), b"a");
assert_eq!(unsafe { thread.to_integer(-1) }, Some(5));
unsafe { thread.pop(2) };
}
3 => {
let local = unsafe { thread.get_local(0, 1) }.expect("debug local lookup should run");
assert!(local.is_some());
assert_eq!(local.as_ref().unwrap().as_bytes(), b"a");
assert_eq!(unsafe { thread.to_integer(-1) }, Some(6));
unsafe { thread.pop(1) };
}
5 | 7 | 9 => {
let expected = match break_hits {
5 => 7,
7 => 8,
_ => 9,
};
let local = unsafe { thread.get_local(1, 1) }.expect("debug local lookup should run");
assert!(local.is_some());
assert_eq!(local.as_ref().unwrap().as_bytes(), b"a");
assert_eq!(unsafe { thread.to_integer(-1) }, Some(expected));
unsafe { thread.pop(1) };
}
13 => {
let local = unsafe { thread.get_local(0, 1) }.expect("debug local lookup should run");
assert!(local.is_some());
assert_eq!(local.as_ref().unwrap().as_bytes(), b"a");
assert_eq!(unsafe { thread.type_of(-1) }, LUA_TNIL);
unsafe { thread.pop(1) };
}
15 => {
let local = unsafe { thread.get_local(2, 1) }.expect("debug local lookup should run");
assert!(local.is_some());
assert_eq!(local.as_ref().unwrap().as_bytes(), b"x");
unsafe { thread.pop(1) };
let missing = unsafe { thread.get_local(2, 2) }.expect("debug local lookup should run");
assert!(missing.is_none());
}
_ => {}
}
let interrupted_thread = DEBUGGER_INTERRUPT_THREAD.lock().unwrap().take();
if let Some(interrupted_thread) = interrupted_thread {
let _ = unsafe { interrupted_thread.0.resume(None, 0) };
}
false
}
fn run_debugger(single_step: bool) {
DEBUGGER_BREAK_HITS.store(0, Ordering::Relaxed);
*DEBUGGER_INTERRUPT_THREAD.lock().unwrap() = None;
DEBUGGER_SINGLE_STEP.store(single_step, Ordering::Relaxed);
DEBUGGER_STEP_HITS.store(0, Ordering::Relaxed);
let compile = CompileOptions {
debug_level: 2,
..CompileOptions::default()
};
let options = ConformanceOptions {
compile,
setup: Some(setup_debugger),
yield_callback: Some(debugger_yield),
..ConformanceOptions::default()
};
run_fixture("debugger.luau", &options);
assert_eq!(DEBUGGER_BREAK_HITS.load(Ordering::Relaxed), 16);
if single_step {
assert!(DEBUGGER_STEP_HITS.load(Ordering::Relaxed) > 100);
}
}
fn interrupt_validate_hits(thread: &Thread) -> VmResult {
unsafe {
let index = INTERRUPT_INDEX.load(Ordering::Relaxed);
assert!(index < INTERRUPT_EXPECTED_HITS.len());
let mut ar = LuaDebug::default();
assert_ne!(
{ thread.get_info(0, "l", &mut ar) }.expect("debug lookup should run"),
0
);
assert_eq!(ar.currentline, INTERRUPT_EXPECTED_HITS[index]);
let next = index + 1;
INTERRUPT_INDEX.store(next, Ordering::Relaxed);
if next == 4 {
return thread.yield_current(0).map(|_| ());
}
Ok(())
}
}
fn interrupt_break_infinite_loops(thread: &Thread) -> VmResult {
unsafe {
let next = INTERRUPT_INDEX.fetch_add(1, Ordering::Relaxed) + 1;
assert!(next <= 11);
if next == 11 {
return thread.yield_current(0).map(|_| ());
}
Ok(())
}
}
fn interrupt_timeout_pattern(thread: &Thread) -> VmErrorResult {
unsafe {
let next = INTERRUPT_INDEX.fetch_add(1, Ordering::Relaxed) + 1;
if next == 1_000 {
INTERRUPT_INDEX.store(0, Ordering::Relaxed);
return luau_vm::error!(thread, "timeout");
}
Ok(())
}
}
fn interrupt_timeout(thread: &Thread) -> VmResult {
interrupt_timeout_pattern(thread).map_err(Into::into)
}
#[test]
fn interrupt_inspection() {
INTERRUPT_INSPECTION_SKIP_BREAK.store(false, Ordering::Relaxed);
let options = ConformanceOptions {
setup: Some(|thread| unsafe {
(*thread.global().callbacks()).execution_interrupt =
Some(interrupt_inspection_interrupt);
}),
yield_callback: Some(interrupt_inspection_yield),
..ConformanceOptions::default()
};
run_fixture("basic.luau", &options);
}
#[test]
fn interrupt_error_inspection() {
let source = br#"
function fib(n)
return n < 2 and 1 or fib(n - 1) + fib(n - 2)
end
fib(5)
"#;
for target in 0..20 {
INTERRUPT_ERROR_INSPECTION_TARGET.store(target, Ordering::Relaxed);
INTERRUPT_ERROR_INSPECTION_STEP.store(0, Ordering::Relaxed);
let lua = Lua::new().expect("Lua::new should succeed");
let thread = lua.main_thread();
unsafe { thread.open_libs() }.expect("libraries should open");
unsafe { thread.sandbox() }.expect("thread should sandbox");
unsafe { thread.sandbox_thread() }.expect("thread should sandbox");
let bytecode = luau_compiler::compile_bytes(source, CompileOptions::default());
assert_eq!(
unsafe { thread.load("=InterruptErrorInspection", &bytecode, 0) },
Ok(())
);
unsafe {
(*thread.global().callbacks()).execution_interrupt =
Some(interrupt_error_inspection_interrupt);
}
let _ = unsafe { thread.resume(None, 0) };
let mut ar = LuaDebug::default();
assert_ne!(
unsafe { thread.get_info(0, "nsl", &mut ar) }.expect("debug lookup should run"),
0
);
unsafe { thread.call_hook(interrupt_error_inspection_hook, ptr::null_mut()) }
.expect("interrupt error inspection hook should run");
}
}
#[test]
fn ndebug_get_upvalue() {
let compile = CompileOptions {
debug_level: 0,
optimization_level: 0,
..CompileOptions::default()
};
let options = ConformanceOptions {
compile,
yield_callback: Some(|thread| {
if !flags::LuauAutoStack.get() {
assert_ne!(unsafe { thread.check_stack(LUA_MIN_STACK as i32) }, 0);
}
let mut ar = LuaDebug::default();
assert_ne!(
unsafe { thread.get_info(1, "f", &mut ar) }.expect("debug lookup should run"),
0
);
let upvalue =
unsafe { thread.get_upvalue(-1, 1) }.expect("debugger upvalue lookup should run");
assert!(upvalue.is_some());
assert_eq!(upvalue.as_ref().unwrap().as_bytes(), b"");
assert_eq!(unsafe { thread.to_integer(-1) }, Some(5));
unsafe { thread.pop(2) };
false
}),
..ConformanceOptions::default()
};
run_fixture("ndebug_upvalues.luau", &options);
}
#[test]
fn tag_method_error() {
for do_lua_break in [false, true] {
TAG_METHOD_ERROR_INDEX.store(0, Ordering::Relaxed);
TAG_METHOD_ERROR_BREAK.store(do_lua_break, Ordering::Relaxed);
let options = ConformanceOptions {
setup: Some(|thread| unsafe {
(*thread.global().callbacks()).debug_protected_error =
Some(tag_method_error_protected_error);
}),
yield_callback: Some(tag_method_error_yield),
..ConformanceOptions::default()
};
run_fixture("tmerror.luau", &options);
assert_eq!(
TAG_METHOD_ERROR_INDEX.load(Ordering::Relaxed),
TAG_METHOD_ERROR_EXPECTED_HITS.len()
);
}
}
#[test]
fn coverage() {
let compile = CompileOptions {
optimization_level: 1,
coverage_level: 2,
..CompileOptions::default()
};
let options = ConformanceOptions {
compile,
setup: Some(setup_coverage),
..ConformanceOptions::default()
};
run_fixture("coverage.luau", &options);
}
#[test]
fn debugger() {
run_debugger(false);
run_debugger(true);
}
#[test]
fn interrupt() {
let compile = CompileOptions {
optimization_level: 1,
..CompileOptions::default()
};
let options = ConformanceOptions {
compile,
..ConformanceOptions::default()
};
with_state("interrupt.luau", &options, |lua| {
let main_thread = lua.main_thread();
let global = unsafe { main_thread.global() };
unsafe {
(*global.callbacks()).execution_interrupt = Some(interrupt_validate_hits);
}
{
let thread =
unsafe { main_thread.new_thread() }.expect("interrupt test thread should be made");
unsafe {
thread
.get_field(LUA_GLOBALS_INDEX, "test")
.expect("interrupt test function should load")
};
INTERRUPT_INDEX.store(0, Ordering::Relaxed);
let status = unsafe { thread.resume(None, 0) };
assert_eq!(status, Err(VmExit::Control(VmControl::Yield)));
assert_eq!(INTERRUPT_INDEX.load(Ordering::Relaxed), 4);
let status = unsafe { thread.resume(None, 0) };
assert_eq!(status, Ok(()));
assert_eq!(
INTERRUPT_INDEX.load(Ordering::Relaxed),
INTERRUPT_EXPECTED_HITS.len()
);
unsafe { main_thread.pop(1) };
}
unsafe {
(*global.callbacks()).execution_interrupt = Some(interrupt_break_infinite_loops);
}
for test in 1..=10 {
let thread =
unsafe { main_thread.new_thread() }.expect("interrupt loop thread should be made");
unsafe {
thread
.get_field(LUA_GLOBALS_INDEX, format!("infloop{test}"))
.expect("interrupt loop function should load")
};
INTERRUPT_INDEX.store(0, Ordering::Relaxed);
let status = unsafe { thread.resume(None, 0) };
assert_eq!(status, Err(VmExit::Control(VmControl::Yield)));
assert_eq!(INTERRUPT_INDEX.load(Ordering::Relaxed), 11);
unsafe { main_thread.pop(1) };
}
unsafe {
(*global.callbacks()).execution_interrupt = Some(interrupt_timeout);
(*global.callbacks()).pattern_interrupt = Some(interrupt_timeout_pattern);
}
for test in 1..=6 {
let thread =
unsafe { main_thread.new_thread() }.expect("interrupt hang thread should be made");
unsafe {
thread
.get_field(LUA_GLOBALS_INDEX, format!("hang{test}"))
.expect("interrupt hang function should load")
};
INTERRUPT_INDEX.store(0, Ordering::Relaxed);
let status = unsafe { thread.resume(None, 0) };
assert_eq!(status, Err(VmExit::Error(VmError::Runtime)));
assert!(
unsafe { thread.to_string(-1) }
.expect("interrupt error should convert to string")
.unwrap()
.contains_str("timeout")
);
unsafe { main_thread.pop(1) };
}
{
let thread =
unsafe { main_thread.new_thread() }.expect("interrupt pcall thread should be made");
unsafe {
thread
.get_field(LUA_GLOBALS_INDEX, "hangpcall")
.expect("interrupt pcall function should load")
};
INTERRUPT_INDEX.store(0, Ordering::Relaxed);
let status = unsafe { thread.resume(None, 0) };
assert_eq!(status, Ok(()));
unsafe { main_thread.pop(1) };
}
});
}