use std::cell::Cell;
use std::rc::Rc;
use luau::{ChunkMode, Compiler, Error, Lua, LuaString, Table, Value};
use luau_bytecode::builder::BytecodeBuilder;
use luau_bytecode::model::Instruction;
use luau_bytecode::opcodes::{BytecodeConstantTag, Opcode};
#[cfg(feature = "macros")]
use luau::Function;
#[cfg(feature = "macros")]
#[test]
fn chunk_macro_captures_values_without_losing_nil_or_assignment_ownership() -> Result<(), Error> {
let lua = Lua::new()?;
lua.globals()?.set("missing", 99)?;
lua.globals()?.set("fallback", 40)?;
let name = String::from("captured");
let missing: Option<i32> = None;
let table = lua.create_table()?;
table.set("value", 2)?;
let (first, second, value, global): (String, String, i32, i32) = lua
.load(luau::chunk! {
assert($missing == nil)
$missing = 5
assert($missing == 5)
written = fallback + $table.value
return $name, $name, $missing, written
})
.call(())?;
assert_eq!((first.as_str(), second.as_str()), ("captured", "captured"));
assert_eq!((value, global), (5, 42));
assert_eq!(lua.globals()?.get::<i32>("missing")?, 99);
assert_eq!(lua.globals()?.get::<i32>("written")?, 42);
Ok(())
}
#[cfg(feature = "macros")]
#[test]
fn captured_environment_metamethod_rejects_direct_invalid_calls() -> Result<(), Error> {
let lua = Lua::new()?;
let captured = 1;
let chunk = lua.load(luau::chunk! { return $captured });
{
let environment = chunk.environment().expect("captured environment");
let metatable = environment.metatable()?.expect("capture metatable");
let new_index: Function<'_> = metatable.raw_get("__newindex")?;
assert!(new_index.call::<()>((42, "captured", 2)).is_err());
}
assert_eq!(chunk.call::<i32>(())?, 1);
Ok(())
}
#[cfg(feature = "macros")]
#[test]
fn chunk_macro_preserves_source_lines() -> Result<(), Error> {
let lua = Lua::new()?;
let error = lua
.load(luau::chunk! {
local value = 1
value += 1
error("boom")
})
.exec()
.expect_err("the chunk should raise an error");
let Error::RuntimeError(message) = error else {
panic!("expected a runtime error");
};
assert!(message.contains(":3:"), "{message}");
Ok(())
}
#[test]
fn load_mode() -> Result<(), Error> {
let lua = Lua::new()?;
assert_eq!(
lua.load("1 + 1").set_mode(ChunkMode::Text).eval::<i32>()?,
2
);
assert!(matches!(
lua.load("1 + 1").set_mode(ChunkMode::Binary).exec(),
Err(Error::SyntaxError { .. })
));
let bytecode = luau::Compiler::default().compile("return 1 + 1")?;
assert_eq!(lua.load(&bytecode).eval::<i32>()?, 2);
assert_eq!(
lua.load(&bytecode)
.set_mode(ChunkMode::Binary)
.eval::<i32>()?,
2
);
assert!(matches!(
lua.load(&bytecode).set_mode(ChunkMode::Text).exec(),
Err(Error::SyntaxError { .. })
));
Ok(())
}
#[test]
fn malformed_bytecode_opcode_is_rejected() -> Result<(), Error> {
let lua = Lua::new()?;
let mut bytecode = Compiler::default().compile("")?;
let first_instruction = Instruction::abc(Opcode::PrepVarargs, 0, 0, 0)
.word()
.to_le_bytes();
let opcode_offset = bytecode
.windows(first_instruction.len())
.position(|window| window == first_instruction)
.expect("empty chunks should begin with PREPVARARGS");
bytecode[opcode_offset] = u8::MAX;
assert!(matches!(
lua.load(&bytecode).set_mode(ChunkMode::Binary).exec(),
Err(Error::SyntaxError { .. })
));
Ok(())
}
#[test]
fn malformed_class_shape_member_is_rejected_without_leaking() -> Result<(), Error> {
let _classes = luau_common::flags::DebugLuauUserDefinedClasses.scoped(true);
let mut bytecode = Compiler::default().compile("class Poi end")?;
let empty_class_shape = [BytecodeConstantTag::ClassShape as u8, 0, 0, 0];
let class_shape = bytecode
.windows(empty_class_shape.len())
.position(|window| window == empty_class_shape)
.expect("the empty class should serialize an empty class shape");
bytecode[class_shape + 2] = 1;
bytecode[class_shape + 4] = 1;
let lua = Lua::new()?;
assert!(matches!(
lua.load(&bytecode).set_mode(ChunkMode::Binary).exec(),
Err(Error::SyntaxError { .. })
));
Ok(())
}
enum ForwardProtoReference {
ClosureConstant,
Child,
}
fn bytecode_with_forward_proto_reference(reference: ForwardProtoReference) -> Vec<u8> {
let mut builder = BytecodeBuilder::new();
let main = builder.begin_function(0, false);
match reference {
ForwardProtoReference::ClosureConstant => {
builder.add_constant_closure(1);
}
ForwardProtoReference::Child => {
builder
.add_child_function(1)
.expect("one child proto should fit the bytecode format");
}
}
builder.emit_abc(Opcode::Return, 0, 1, 0);
builder.end_function(1, 0, 0, 0);
builder.begin_function(0, false);
builder.emit_abc(Opcode::Return, 0, 1, 0);
builder.end_function(1, 0, 0, 0);
builder.set_main_function(main);
builder.finalize();
builder.get_bytecode().to_vec()
}
#[test]
fn malformed_forward_closure_proto_reference_is_rejected() -> Result<(), Error> {
let lua = Lua::new()?;
let bytecode = bytecode_with_forward_proto_reference(ForwardProtoReference::ClosureConstant);
assert!(matches!(
lua.load(&bytecode).set_mode(ChunkMode::Binary).exec(),
Err(Error::SyntaxError { .. })
));
Ok(())
}
#[test]
fn malformed_forward_child_proto_reference_is_rejected() -> Result<(), Error> {
let lua = Lua::new()?;
let bytecode = bytecode_with_forward_proto_reference(ForwardProtoReference::Child);
assert!(matches!(
lua.load(&bytecode).set_mode(ChunkMode::Binary).exec(),
Err(Error::SyntaxError { .. })
));
Ok(())
}
#[test]
fn chunk_compile_errors_report_the_first_located_diagnostic() -> Result<(), Error> {
let lua = Lua::new()?;
let error = lua
.load("local =")
.set_name("invalid_chunk")
.exec()
.expect_err("invalid source should not compile");
let Error::SyntaxError { message, .. } = error else {
panic!("expected a syntax error");
};
assert!(message.starts_with("invalid_chunk:1: "), "{message}");
assert!(message.contains("Expected identifier"), "{message}");
assert!(!message.contains("parse errors"), "{message}");
Ok(())
}
#[test]
fn sandbox_load_state_advances_only_after_a_successful_shared_load() -> Result<(), Error> {
let lua = Lua::new()?;
let calls = Rc::new(Cell::new(0));
let module = lua.create_table()?;
let metatable = lua.create_table()?;
let callback_calls = Rc::clone(&calls);
metatable.set(
"__index",
lua.create_function(luau::callback!(
move |_lua, _table: Table<'_>, _key: String| {
callback_calls.set(callback_calls.get() + 1);
Ok(1)
}
))?,
)?;
module.set_metatable(Some(&metatable))?;
lua.globals()?.set("m", module)?;
lua.sandbox(true)?;
{
let _unconsumed = lua.load("return 0");
}
assert!(lua.load("local =").into_function().is_err());
let _isolated = lua.load("return 0").into_sandboxed()?;
let first = lua.load("return m.value").into_function()?;
assert_eq!(calls.get(), 1);
assert_eq!(first.call::<i32>(())?, 1);
assert_eq!(calls.get(), 1);
let _second = lua.load("return 0").into_function()?;
assert_eq!(first.call::<i32>(())?, 1);
assert_eq!(calls.get(), 2);
Ok(())
}
#[test]
fn sandbox_reachable_table_mutation_invalidates_cached_imports() -> Result<(), Error> {
let lua = Lua::new()?;
let inner = lua.create_table()?;
inner.set("value", 1)?;
let module = lua.create_table()?;
module.set("inner", &inner)?;
lua.globals()?.set("module", module)?;
lua.sandbox(true)?;
let function = lua.load("return module.inner.value").into_function()?;
assert_eq!(function.call::<i32>(())?, 1);
inner.set("value", 2)?;
assert_eq!(function.call::<i32>(())?, 2);
Ok(())
}
#[test]
fn application_data_mutation_invalidates_cached_imports() -> Result<(), Error> {
let lua = Lua::new()?;
lua.set_app_data(1_i32);
let module = lua.create_table()?;
let metatable = lua.create_table()?;
metatable.set(
"__index",
lua.create_function(luau::callback!(|lua, _table: Table<'_>, _key: String| {
Ok(*lua
.app_data_ref::<i32>()
.expect("application data should be installed"))
}))?,
)?;
module.set_metatable(Some(&metatable))?;
lua.globals()?.set("module", module)?;
lua.sandbox(true)?;
let read = lua.load("return module.value").into_function()?;
assert_eq!(read.call::<i32>(())?, 1);
lua.set_app_data(2_i32);
assert_eq!(read.call::<i32>(())?, 2);
Ok(())
}
#[test]
fn disabling_sandbox_invalidates_loaded_main_environments() -> Result<(), Error> {
let lua = Lua::new()?;
lua.globals()?.set("value", 1)?;
lua.sandbox(true)?;
let function = lua.load("return value").into_function()?;
assert_eq!(function.call::<i32>(())?, 1);
lua.sandbox(false)?;
lua.globals()?.set("value", 2)?;
assert_eq!(function.call::<i32>(())?, 2);
Ok(())
}
#[test]
fn dynamic_callback_loads_deopt_before_import_resolution() -> Result<(), Error> {
let lua = Lua::new()?;
let host_value = Rc::new(Cell::new(1));
let calls = Rc::new(Cell::new(0));
let module = lua.create_table()?;
let metatable = lua.create_table()?;
let callback_value = Rc::clone(&host_value);
let callback_calls = Rc::clone(&calls);
metatable.set(
"__index",
lua.create_function(luau::callback!(
move |_lua, _table: Table<'_>, _key: String| {
callback_calls.set(callback_calls.get() + 1);
Ok(callback_value.get())
}
))?,
)?;
module.set_metatable(Some(&metatable))?;
lua.globals()?.set("m", module)?;
lua.sandbox(true)?;
let load_function = lua.create_function(luau::callback!(|lua| {
lua.load("return m.value").into_function()
}))?;
let function: luau::Function<'_> = load_function.call(())?;
assert_eq!(calls.get(), 0);
host_value.set(2);
assert_eq!(function.call::<i32>(())?, 2);
assert_eq!(calls.get(), 1);
Ok(())
}
#[test]
fn explicit_chunk_environment_owns_import_resolution() -> Result<(), Error> {
let lua = Lua::new()?;
let calls = Rc::new(Cell::new(0));
let global_module = lua.create_table()?;
let metatable = lua.create_table()?;
let callback_calls = Rc::clone(&calls);
metatable.set(
"__index",
lua.create_function(luau::callback!(
move |_lua, _table: Table<'_>, _key: String| {
callback_calls.set(callback_calls.get() + 1);
Ok(1)
}
))?,
)?;
global_module.set_metatable(Some(&metatable))?;
lua.globals()?.set("m", global_module)?;
let custom_module = lua.create_table()?;
custom_module.set("value", 2)?;
let environment = lua.create_table()?;
environment.set("m", custom_module)?;
environment.set_safe_env(true);
lua.sandbox(true)?;
let function = lua
.load("return m.value")
.set_environment(environment)
.into_function()?;
assert_eq!(calls.get(), 0);
assert_eq!(function.call::<i32>(())?, 2);
Ok(())
}
#[test]
fn explicit_current_globals_follow_managed_load_tracking() -> Result<(), Error> {
let lua = Lua::new()?;
lua.globals()?.set("x", 1)?;
lua.sandbox(true)?;
let globals = lua.globals()?;
let function = lua
.load("return x")
.set_environment(globals.try_clone()?)
.into_function()?;
lua.load("x = 2").set_environment(globals).exec()?;
assert_eq!(function.call::<i32>(())?, 2);
Ok(())
}
#[test]
fn replacing_a_type_metatable_invalidates_cached_imports() -> Result<(), Error> {
let lua = Lua::new()?;
let first_index = lua.create_table()?;
first_index.set("value", 1)?;
let first_metatable = lua.create_table()?;
first_metatable.set("__index", first_index)?;
let second_index = lua.create_table()?;
second_index.set("value", 2)?;
let second_metatable = lua.create_table()?;
second_metatable.set("__index", second_index)?;
lua.set_type_metatable::<LuaString<'_>>(Some(first_metatable))?;
lua.globals()?.set("text", "hello")?;
lua.sandbox(true)?;
let read = lua.load("return text.value").into_function()?;
assert_eq!(read.call::<i32>(())?, 1);
lua.set_type_metatable::<LuaString<'_>>(Some(second_metatable))?;
assert_eq!(read.call::<i32>(())?, 2);
Ok(())
}
#[test]
fn compile_constants_preserve_number_and_integer_domains() -> Result<(), Error> {
let lua = Lua::new()?;
lua.set_compiler(
Compiler::new()
.set_optimization_level(2)
.add_library_constant("constants.number", 42_i32)
.add_library_constant("constants.integer", 42_i64),
);
let (number, integer): (Value<'_>, Value<'_>) = lua
.load("return constants.number, constants.integer")
.call(())?;
assert!(matches!(number, Value::Number(42.0)));
assert!(matches!(integer, Value::Integer(42)));
Ok(())
}