use core::alloc::Layout;
use core::ptr::NonNull;
use luau_common::{BStr, BString, ByteSlice};
use luau_compiler::CompileOptions;
use luau_vm::internal::gc::GcRuntime;
use luau_vm::lua::Lua;
use luau_vm::state::{LuaAllocator, SystemLuaAllocator};
use luau_vm::thread::{LUA_ENVIRON_INDEX, LUA_GLOBALS_INDEX, LUA_TSTRING};
use luau_vm::{
LUA_GC_COLLECT, LUA_GC_COUNT, LUA_GC_IS_RUNNING, LUA_GC_RESTART, LUA_GC_SET_GOAL,
LUA_GC_SET_STEP_MUL, LUA_GC_SET_STEP_SIZE, LUA_GC_STEP, LUA_GC_STOP, NativeCallContext,
NativeCallResult, NativeFunction, Thread,
};
use luau_vm::{VmControl, VmError, VmExit};
use std::io::{self, Write};
use std::panic;
use std::path::PathBuf;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
const VERBOSE: bool = false;
const CONFORMANCE_FIXTURE_DIR: &str = "../luau-vm/tests/fixtures/conformance";
const CONFORMANCE_STACK_SIZE: usize = 16 * 1024 * 1024;
const GC_OPTIONS: &[&str] = &[
"stop",
"restart",
"collect",
"count",
"isrunning",
"step",
"setgoal",
"setstepmul",
"setstepsize",
];
static FIXTURE_LOCK: Mutex<()> = Mutex::new(());
static BLOCKABLE_ALLOCATIONS_ALLOWED: AtomicBool = AtomicBool::new(true);
const CONFORMANCE_HELPERS: &[NativeFunction] = &[
NativeFunction {
name: "collectgarbage",
function: collect_garbage,
},
NativeFunction {
name: "loadstring",
function: load_string,
},
];
const SILENT_PRINT_HELPER: &[NativeFunction] = &[NativeFunction {
name: "print",
function: silence,
}];
#[derive(Clone)]
pub(crate) struct ConformanceOptions {
pub(crate) allocator: ConformanceAllocator,
pub(crate) compile: CompileOptions,
pub(crate) setup: Option<unsafe fn(&Thread)>,
pub(crate) yield_callback: Option<fn(&Thread) -> bool>,
}
#[derive(Clone, Copy, Default)]
pub(crate) enum ConformanceAllocator {
#[default]
System,
Limited,
Blockable,
}
impl Default for ConformanceOptions {
fn default() -> Self {
let compile = CompileOptions {
debug_level: 1,
optimization_level: 1,
type_info_level: 1,
..CompileOptions::default()
};
Self {
allocator: ConformanceAllocator::System,
compile,
setup: None,
yield_callback: None,
}
}
}
impl ConformanceAllocator {
fn new_lua(self) -> Option<Lua> {
match self {
ConformanceAllocator::System => Lua::new(),
ConformanceAllocator::Limited => Lua::new_with_allocator(LimitedAllocator),
ConformanceAllocator::Blockable => Lua::new_with_allocator(BlockableAllocator),
}
}
}
#[derive(Clone, Copy)]
struct LimitedAllocator;
unsafe impl LuaAllocator for LimitedAllocator {
unsafe fn allocate(&self, layout: Layout) -> Option<NonNull<u8>> {
if layout.size() > 8 * 1024 * 1024 {
None
} else {
unsafe { SystemLuaAllocator.allocate(layout) }
}
}
unsafe fn reallocate(
&self,
ptr: NonNull<u8>,
old_layout: Layout,
new_layout: Layout,
) -> Option<NonNull<u8>> {
if new_layout.size() > 8 * 1024 * 1024 {
None
} else {
unsafe { SystemLuaAllocator.reallocate(ptr, old_layout, new_layout) }
}
}
unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
unsafe { SystemLuaAllocator.deallocate(ptr, layout) };
}
}
#[derive(Clone, Copy)]
struct BlockableAllocator;
unsafe impl LuaAllocator for BlockableAllocator {
unsafe fn allocate(&self, layout: Layout) -> Option<NonNull<u8>> {
if BLOCKABLE_ALLOCATIONS_ALLOWED.load(Ordering::Relaxed) {
unsafe { SystemLuaAllocator.allocate(layout) }
} else {
None
}
}
unsafe fn reallocate(
&self,
ptr: NonNull<u8>,
old_layout: Layout,
new_layout: Layout,
) -> Option<NonNull<u8>> {
if new_layout.size() <= old_layout.size()
|| BLOCKABLE_ALLOCATIONS_ALLOWED.load(Ordering::Relaxed)
{
unsafe { SystemLuaAllocator.reallocate(ptr, old_layout, new_layout) }
} else {
None
}
}
unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
unsafe { SystemLuaAllocator.deallocate(ptr, layout) };
}
}
pub(crate) fn set_blockable_allocations_allowed(allowed: bool) {
BLOCKABLE_ALLOCATIONS_ALLOWED.store(allowed, Ordering::Relaxed);
}
fn collect_garbage(ctx: NativeCallContext) -> NativeCallResult {
let thread = ctx.raw_thread();
let (what, result) = unsafe {
let option = thread.check_option(1, Some("collect"), GC_OPTIONS)?;
let extra = thread.opt_integer(2, 0)?;
let what = match option {
0 => LUA_GC_STOP,
1 => LUA_GC_RESTART,
2 => LUA_GC_COLLECT,
3 => LUA_GC_COUNT,
4 => LUA_GC_IS_RUNNING,
5 => LUA_GC_STEP,
6 => LUA_GC_SET_GOAL,
7 => LUA_GC_SET_STEP_MUL,
8 => LUA_GC_SET_STEP_SIZE,
_ => unreachable!(),
};
(what, thread.gc(what, extra)?)
};
unsafe {
match what {
LUA_GC_STEP | LUA_GC_IS_RUNNING => thread.push_boolean(result)?,
_ => thread.push_number(result as f64)?,
}
}
Ok(1)
}
fn load_string(ctx: NativeCallContext) -> NativeCallResult {
let thread = ctx.raw_thread();
unsafe {
let source = thread.check_string(1)?;
let chunk_name = thread.opt_string(2)?.unwrap_or(source);
thread.set_safe_env(LUA_ENVIRON_INDEX, 0);
let bytecode = luau_compiler::compile_bytes(source, CompileOptions::default());
let result = thread.load(chunk_name, &bytecode, 0);
if result.is_ok() {
return Ok(1);
}
thread.push_nil()?;
thread.insert(-2);
Ok(2)
}
}
fn silence(ctx: NativeCallContext) -> NativeCallResult {
let thread = ctx.raw_thread();
unsafe {
let count = thread.get_top();
let mut sink = io::sink();
for index in 1..=count {
let string = thread.lua_to_string(index)?;
if index > 1 {
let _ = sink.write_all(b"\t");
}
let _ = sink.write_all(string.as_bytes());
thread.pop(1);
}
let _ = sink.write_all(b"\n");
Ok(0)
}
}
fn fixture_path(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join(CONFORMANCE_FIXTURE_DIR)
.join(name)
}
fn read_fixture(name: &str) -> Vec<u8> {
std::fs::read(fixture_path(name)).expect("conformance fixture should exist")
}
fn thread_bstr(thread: &Thread, index: i32) -> Option<&BStr> {
unsafe { thread.to_string(index) }.expect("test string conversion should not fail")
}
fn register_helpers(thread: &Thread) {
unsafe { thread.push_value(LUA_GLOBALS_INDEX) }.expect("globals table should push");
unsafe { thread.register(None, CONFORMANCE_HELPERS) }
.expect("conformance helpers should register");
if !VERBOSE {
unsafe { thread.register(None, SILENT_PRINT_HELPER) }
.expect("silent print helper should register");
}
unsafe { thread.pop(1) };
}
fn thread_message(thread: &Thread, index: i32) -> String {
thread_bstr(thread, index)
.map(|bytes| String::from_utf8_lossy(bytes.as_bytes()).into_owned())
.unwrap_or_else(|| "<non-string error>".to_owned())
}
pub(crate) fn run_source_chunk(
chunk_name: &str,
source: &[u8],
options: &ConformanceOptions,
) -> Lua {
let bytecode = luau_compiler::compile_bytes(source, options.compile.clone());
run_bytecode_chunk(chunk_name, &bytecode, options)
}
pub(crate) fn run_bytecode_chunk(
chunk_name: &str,
bytecode: &[u8],
options: &ConformanceOptions,
) -> Lua {
let lua = options
.allocator
.new_lua()
.expect("lua_newstate should succeed");
let thread = lua.main_thread();
unsafe { thread.open_libs() }.expect("conformance libraries should open");
register_helpers(thread);
if cfg!(debug_assertions) {
unsafe {
thread
.push_boolean(1)
.expect("limitedstack flag should push");
thread
.set_field(LUA_GLOBALS_INDEX, "limitedstack")
.expect("limitedstack flag should set");
}
}
if let Some(setup) = options.setup {
unsafe { setup(thread) };
}
unsafe {
thread.sandbox().expect("conformance state should sandbox");
thread
.sandbox_thread()
.expect("conformance thread should sandbox");
}
unsafe {
thread
.push_value(LUA_GLOBALS_INDEX)
.expect("globals table should push");
thread.set_field(-1, "_G").expect("_G should set");
}
let load_result = unsafe { thread.load(chunk_name, bytecode, 0) };
let mut status = if load_result.is_ok() {
unsafe { thread.resume(None, 0) }
} else {
Err(VmExit::Error(VmError::Syntax))
};
while let Some(yield_callback) = options.yield_callback {
if !matches!(
status,
Err(VmExit::Control(VmControl::Yield | VmControl::Break))
) {
break;
}
status = if yield_callback(thread) {
unsafe { thread.resume_error(None) }
} else {
unsafe { thread.resume(None, 0) }
};
}
unsafe { thread.validate() };
if status != Ok(()) {
let mut message = if matches!(status, Err(VmExit::Control(VmControl::Yield))) {
BString::from("thread yielded unexpectedly")
} else {
BString::from(thread_message(thread, -1))
};
message.extend_from_slice(b"\nstacktrace:\n");
if unsafe { thread.check_stack(1) } != 0 {
unsafe { thread.traceback(Some(thread), None, 0) }.expect("traceback should build");
message.extend_from_slice(thread_message(thread, -1).as_bytes());
} else {
message.extend_from_slice(b"<unavailable>");
}
panic!("{}", String::from_utf8_lossy(message.as_slice()));
}
lua
}
pub(crate) fn run_state(name: &str, options: &ConformanceOptions) -> Lua {
let source = read_fixture(name);
let state = run_source_chunk(&format!("={name}"), &source, options);
let thread = state.main_thread();
assert_eq!(unsafe { thread.type_of(-1) }, LUA_TSTRING);
assert_eq!(thread_bstr(thread, -1).unwrap().as_bytes(), b"OK");
unsafe { thread.pop(1) };
state
}
pub(crate) fn with_state<F>(name: &str, options: &ConformanceOptions, f: F)
where
F: FnOnce(Lua) + Send + 'static,
{
let _guard = FIXTURE_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let name = name.to_owned();
let options = options.to_owned();
let result = std::thread::Builder::new()
.stack_size(CONFORMANCE_STACK_SIZE)
.spawn(move || {
let state = run_state(&name, &options);
f(state);
})
.expect("conformance thread should spawn")
.join();
if let Err(payload) = result {
panic::resume_unwind(payload);
}
}
pub(crate) fn run_fixture(name: &str, options: &ConformanceOptions) {
let _guard = FIXTURE_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let name = name.to_owned();
let options = options.to_owned();
let result = std::thread::Builder::new()
.stack_size(CONFORMANCE_STACK_SIZE)
.spawn(move || {
let _state = run_state(&name, &options);
})
.expect("conformance thread should spawn")
.join();
if let Err(payload) = result {
panic::resume_unwind(payload);
}
}