use luau_bytecode::builder::BytecodeBuilder;
use luau_bytecode::function::{BytecodeFunction, BytecodeFunctionConstant, BytecodeStringTable};
use luau_bytecode::graph::{
BytecodeBlock, BytecodeBlockId, BytecodeEdgeKind, BytecodeImmediate, BytecodeInstruction,
BytecodeInstructionId, BytecodeOperand, count_uses, has_use, verify_use_consistency,
};
use luau_bytecode::model::Instruction;
use luau_bytecode::opcodes::{BytecodeConstantTag, Opcode};
use luau_common::bytecode_wire::read_varint;
use luau_common::flags;
use luau_compiler::{CompileOptions, Compiler};
use luau_syntax::{
allocator::AstArena, ast_names::AstNameTable, parser::ParseOptions, parser::parse_bytes,
};
use std::borrow::Cow;
struct CompiledFunctionBytecode {
data: Vec<u8>,
strings: BytecodeStringTable<'static>,
}
impl CompiledFunctionBytecode {
fn function(&self) -> BytecodeFunction<'_> {
BytecodeFunction::from_function_bytecode(&self.data, &self.strings)
.expect("function bytecode should import")
}
}
fn build_bytecode(source: &[u8], optimization_level: u8) -> CompiledFunctionBytecode {
let options = CompileOptions {
optimization_level,
..CompileOptions::default()
};
build_bytecode_with_options(source, ParseOptions::default(), options)
}
fn function_bytecode_data(source: &[u8], optimization_level: u8) -> CompiledFunctionBytecode {
let options = CompileOptions {
optimization_level,
..CompileOptions::default()
};
function_bytecode_data_with_options(source, ParseOptions::default(), options)
}
fn build_bytecode_with_call_feedback(source: &[u8]) -> CompiledFunctionBytecode {
let _feedback = flags::LuauEmitCallFeedback.scoped(true);
let options = CompileOptions {
optimization_level: 0,
..CompileOptions::default()
};
build_bytecode_with_options(source, ParseOptions::default(), options)
}
fn build_bytecode_with_options(
source: &[u8],
parse_options: ParseOptions,
options: CompileOptions,
) -> CompiledFunctionBytecode {
let _string_interp = flags::LuauCompileStringInterpTargetTop.scoped(true);
let arena = AstArena::new();
let mut names = AstNameTable::new(&arena);
let result =
parse_bytes(source, &arena, &mut names, parse_options).expect("source should parse");
let mut builder = BytecodeBuilder::new();
Compiler::new(options)
.compile_into(&result, &mut names, &mut builder)
.expect("source should compile");
CompiledFunctionBytecode {
data: builder.get_function_data(0),
strings: extract_string_table(&builder),
}
}
fn function_bytecode_data_with_options(
source: &[u8],
parse_options: ParseOptions,
options: CompileOptions,
) -> CompiledFunctionBytecode {
let _string_interp = flags::LuauCompileStringInterpTargetTop.scoped(true);
let arena = AstArena::new();
let mut names = AstNameTable::new(&arena);
let result =
parse_bytes(source, &arena, &mut names, parse_options).expect("source should parse");
let mut builder = BytecodeBuilder::new();
Compiler::new(options)
.compile_into(&result, &mut names, &mut builder)
.expect("source should compile");
CompiledFunctionBytecode {
data: builder.get_function_data(0),
strings: extract_string_table(&builder),
}
}
fn function(source: &[u8]) -> CompiledFunctionBytecode {
build_bytecode(source, 0)
}
fn optimized_function(source: &[u8]) -> CompiledFunctionBytecode {
build_bytecode(source, 1)
}
fn roundtrip(source: &[u8]) {
for optimization_level in 0..=2 {
let compiled = function_bytecode_data(source, optimization_level);
let mut function = compiled.function();
let dumped = function
.to_function_bytecode()
.expect("roundtripped function bytecode should export");
BytecodeFunction::from_function_bytecode(&dumped, &compiled.strings)
.expect("roundtripped function bytecode should import");
assert_eq!(function_code(&compiled.data), function_code(&dumped));
}
}
fn roundtrip_with_options(
source: &[u8],
parse_options: ParseOptions,
compile_options: CompileOptions,
) {
let compiled = function_bytecode_data_with_options(source, parse_options, compile_options);
let mut function = compiled.function();
let dumped = function
.to_function_bytecode()
.expect("roundtripped function bytecode should export");
BytecodeFunction::from_function_bytecode(&dumped, &compiled.strings)
.expect("roundtripped function bytecode should import");
assert_eq!(function_code(&compiled.data), function_code(&dumped));
}
fn extract_string_table(builder: &BytecodeBuilder<'_>) -> BytecodeStringTable<'static> {
let bytecode = builder.get_bytecode();
let mut offset = 2;
let strings_count =
read_varint(bytecode, &mut offset).expect("string table count should decode") as usize;
let mut strings = Vec::with_capacity(strings_count);
for _ in 0..strings_count {
let string_len =
read_varint(bytecode, &mut offset).expect("string length should decode") as usize;
strings.push(bytecode[offset..offset + string_len].to_vec());
offset += string_len;
}
BytecodeStringTable::new(strings.into_iter().map(Cow::Owned).collect::<Vec<_>>())
}
fn function_code(bytecode: &[u8]) -> &[u8] {
let mut offset = 5;
let type_info_size =
read_varint(bytecode, &mut offset).expect("type info size should decode") as usize;
offset += type_info_size;
let code_words =
read_varint(bytecode, &mut offset).expect("code word count should decode") as usize;
&bytecode[offset..offset + code_words * std::mem::size_of::<Instruction>()]
}
fn edge_kinds(block: &BytecodeBlock) -> Vec<BytecodeEdgeKind> {
block.successors().iter().map(|edge| edge.kind).collect()
}
fn predecessor_kinds(block: &BytecodeBlock) -> Vec<BytecodeEdgeKind> {
block.predecessors().iter().map(|edge| edge.kind).collect()
}
#[test]
fn lbc_constant_regression_test() {
assert_eq!(BytecodeConstantTag::Nil as u8, 0);
assert_eq!(BytecodeConstantTag::Boolean as u8, 1);
assert_eq!(BytecodeConstantTag::Number as u8, 2);
assert_eq!(BytecodeConstantTag::String as u8, 3);
assert_eq!(BytecodeConstantTag::Import as u8, 4);
assert_eq!(BytecodeConstantTag::Table as u8, 5);
assert_eq!(BytecodeConstantTag::Closure as u8, 6);
assert_eq!(BytecodeConstantTag::Vector as u8, 7);
assert_eq!(BytecodeConstantTag::TableWithConstants as u8, 8);
assert_eq!(BytecodeConstantTag::Integer as u8, 9);
assert_eq!(BytecodeConstantTag::ClassShape as u8, 10);
assert_eq!(BytecodeConstantTag::VectorDouble as u8, 11);
assert_eq!(BytecodeConstantTag::Count as u8, 12);
}
fn opcodes(function: &BytecodeFunction<'_>, block: &BytecodeBlock) -> Vec<Opcode> {
block
.graph_instructions()
.iter()
.map(|id| function.graph_instruction(*id).opcode())
.collect()
}
fn instruction_id(block: &BytecodeBlock, index: usize) -> BytecodeInstructionId {
block.graph_instructions()[index]
}
fn instruction<'a>(
function: &'a BytecodeFunction<'_>,
block: &BytecodeBlock,
index: usize,
) -> &'a BytecodeInstruction {
function.graph_instruction(instruction_id(block, index))
}
fn fallthrough_block<'a>(
function: &'a BytecodeFunction<'_>,
block: &BytecodeBlock,
) -> &'a BytecodeBlock {
let target = block
.successors()
.iter()
.find(|edge| edge.kind == BytecodeEdgeKind::Fallthrough)
.expect("fallthrough edge should exist")
.target;
function.block(target)
}
fn branch_block<'a>(
function: &'a BytecodeFunction<'_>,
block: &BytecodeBlock,
) -> &'a BytecodeBlock {
let target = block
.successors()
.iter()
.find(|edge| edge.kind == BytecodeEdgeKind::Branch)
.expect("branch edge should exist")
.target;
function.block(target)
}
fn loop_target(block: &BytecodeBlock) -> BytecodeBlockId {
block
.successors()
.iter()
.find(|edge| edge.kind == BytecodeEdgeKind::Loop)
.expect("loop edge should exist")
.target
}
fn is_phi_of(
function: &BytecodeFunction<'_>,
operand: BytecodeOperand,
left: BytecodeInstructionId,
right: BytecodeInstructionId,
) -> bool {
let BytecodeOperand::Phi(id) = operand else {
return false;
};
function.phi(id).operands()
== [
BytecodeOperand::Instruction(left),
BytecodeOperand::Instruction(right),
]
}
#[test]
fn from_function_bytecode() {
let compiled = function(
br#"
function fn(a, b)
local extra = 0
if a > b then extra = 1 end
return extra + a + b
end
"#,
);
let function = compiled.function();
assert_eq!(function.upvalue_count, 0);
assert_eq!(function.num_params, 2);
assert_eq!(function.constants.len(), 2);
assert_eq!(function.blocks().len(), 4);
assert!(verify_use_consistency(&function));
let entry = function.block(function.entry());
assert_eq!(
edge_kinds(entry),
[BytecodeEdgeKind::Branch, BytecodeEdgeKind::Fallthrough]
);
let cond_false_target = entry.successors()[0].target;
let cond_true = fallthrough_block(&function, entry);
assert_eq!(edge_kinds(cond_true), [BytecodeEdgeKind::Fallthrough]);
let cond_false = branch_block(&function, entry);
assert_eq!(edge_kinds(cond_false), [BytecodeEdgeKind::Fallthrough]);
assert_eq!(cond_true.successors()[0].target, cond_false_target);
assert_eq!(cond_false.successors()[0].target, function.exit());
assert_eq!(function.block(function.exit()).graph_instructions(), []);
assert_eq!(entry.graph_instructions().len(), 2);
assert_eq!(
opcodes(&function, entry),
[Opcode::LoadK, Opcode::JumpIfNotLt]
);
let load_k_id = instruction_id(entry, 0);
let load_k = function.graph_instruction(load_k_id);
assert_eq!(load_k.operands(), [BytecodeOperand::VmConstant(0)]);
assert_eq!(function.constants[0], BytecodeFunctionConstant::Number(0.0));
assert_eq!(load_k.users().len(), 1);
let BytecodeOperand::Phi(extra_phi_id) = load_k.users()[0] else {
panic!("entry value should feed the merge phi");
};
let extra_phi = function.phi(extra_phi_id);
let cond_true_load = BytecodeOperand::Instruction(instruction_id(cond_true, 0));
assert_eq!(extra_phi.operands().len(), 2);
assert_eq!(
extra_phi
.operands()
.iter()
.filter(|operand| **operand == BytecodeOperand::Instruction(load_k_id))
.count(),
1
);
assert_eq!(
extra_phi
.operands()
.iter()
.filter(|operand| **operand == cond_true_load)
.count(),
1
);
let first_add = BytecodeOperand::Instruction(instruction_id(cond_false, 0));
assert_eq!(extra_phi.users(), [first_add]);
assert_eq!(
instruction(&function, cond_false, 0).operands()[0],
BytecodeOperand::Phi(extra_phi_id)
);
let jump_if_not_lt = instruction(&function, entry, 1);
assert_eq!(jump_if_not_lt.operands().len(), 3);
assert_eq!(opcodes(&function, cond_true), [Opcode::LoadK]);
assert_eq!(
opcodes(&function, cond_false),
[Opcode::Add, Opcode::Add, Opcode::Return]
);
}
#[test]
fn repeat_until_loop() {
let compiled = function(
br#"
function fn()
local var = 0
repeat var += 1 until var < 10
end
"#,
);
let function = compiled.function();
assert!(verify_use_consistency(&function));
assert_eq!(function.blocks().len(), 5);
let entry = function.block(function.entry());
assert_eq!(edge_kinds(entry), [BytecodeEdgeKind::Fallthrough]);
let loop_body = fallthrough_block(&function, entry);
assert_eq!(
predecessor_kinds(loop_body),
[BytecodeEdgeKind::Fallthrough, BytecodeEdgeKind::Loop]
);
assert_eq!(
edge_kinds(loop_body),
[BytecodeEdgeKind::Branch, BytecodeEdgeKind::Fallthrough]
);
let loop_jump_back = fallthrough_block(&function, loop_body);
assert_eq!(edge_kinds(loop_jump_back), [BytecodeEdgeKind::Loop]);
let ret = branch_block(&function, loop_body);
assert_eq!(opcodes(&function, entry), [Opcode::LoadK]);
assert_eq!(
opcodes(&function, loop_body),
[Opcode::LoadK, Opcode::Add, Opcode::LoadK, Opcode::JumpIfLt]
);
assert_eq!(opcodes(&function, loop_jump_back), [Opcode::JumpBack]);
assert_eq!(opcodes(&function, ret), [Opcode::Return]);
let var_init = instruction_id(entry, 0);
let load_one = instruction_id(loop_body, 0);
let add_var = instruction_id(loop_body, 1);
let add = instruction(&function, loop_body, 1);
assert_eq!(add.operands().len(), 2);
assert!(is_phi_of(&function, add.operands()[0], var_init, add_var));
assert_eq!(add.operands()[1], BytecodeOperand::Instruction(load_one));
let phi = add.operands()[0];
let jump = BytecodeOperand::Instruction(instruction_id(loop_body, 3));
assert_eq!(
function
.phi(match phi {
BytecodeOperand::Phi(id) => id,
_ => unreachable!(),
})
.users(),
[BytecodeOperand::Instruction(add_var)]
);
assert!(has_use(
&function,
phi,
BytecodeOperand::Instruction(add_var)
));
assert!(has_use(
&function,
BytecodeOperand::Instruction(add_var),
phi
));
assert!(has_use(
&function,
BytecodeOperand::Instruction(add_var),
jump
));
assert!(has_use(
&function,
BytecodeOperand::Instruction(var_init),
phi
));
assert!(has_use(
&function,
BytecodeOperand::Instruction(load_one),
BytecodeOperand::Instruction(add_var)
));
}
#[test]
fn def_use_chains() {
let compiled = function(
br#"
local function fn(a, b, c)
local s = a + b
local x = s + c
local y = s + a
return x + y
end
"#,
);
let function = compiled.function();
assert!(verify_use_consistency(&function));
assert_eq!(function.blocks().len(), 2);
let entry = function.block(function.entry());
assert_eq!(
opcodes(&function, entry),
[
Opcode::Add,
Opcode::Add,
Opcode::Add,
Opcode::Add,
Opcode::Return
]
);
let s = instruction_id(entry, 0);
let x = instruction_id(entry, 1);
let y = instruction_id(entry, 2);
let result = instruction_id(entry, 3);
let ret = instruction_id(entry, 4);
let s_operand = BytecodeOperand::Instruction(s);
let x_operand = BytecodeOperand::Instruction(x);
let y_operand = BytecodeOperand::Instruction(y);
let result_operand = BytecodeOperand::Instruction(result);
assert_eq!(function.graph_instruction(s).users().len(), 2);
assert_eq!(count_uses(&function, s_operand, x_operand), 1);
assert_eq!(count_uses(&function, s_operand, y_operand), 1);
assert_eq!(function.graph_instruction(x).operands()[0], s_operand);
assert_eq!(function.graph_instruction(y).operands()[0], s_operand);
assert_eq!(
function.graph_instruction(result).operands(),
[x_operand, y_operand]
);
assert_eq!(count_uses(&function, x_operand, result_operand), 1);
assert_eq!(count_uses(&function, y_operand, result_operand), 1);
assert_eq!(function.graph_instruction(x).users(), [result_operand]);
assert_eq!(function.graph_instruction(y).users(), [result_operand]);
assert_eq!(
function.graph_instruction(ret).operands().last(),
Some(&result_operand)
);
assert_eq!(
function.graph_instruction(result).users(),
[BytecodeOperand::Instruction(ret)]
);
}
#[test]
fn loop_invariant_inst_phi_collapse() {
let compiled = function(
br#"
local function fn(a, b)
local s = a + b
local acc = 0
repeat acc += s until acc < 100
return acc
end
"#,
);
let function = compiled.function();
assert!(verify_use_consistency(&function));
let entry = function.block(function.entry());
let s = BytecodeOperand::Instruction(instruction_id(entry, 0));
assert_eq!(
function
.graph_instruction(instruction_id(entry, 0))
.opcode(),
Opcode::Add
);
let mut consumers = 0;
for block in function.blocks().iter().filter(|block| !block.is_dead()) {
for id in block.graph_instructions() {
for operand in function.graph_instruction(*id).operands() {
if *operand == s {
assert!(has_use(&function, s, BytecodeOperand::Instruction(*id)));
consumers += 1;
}
}
}
}
assert_ne!(consumers, 0);
}
#[test]
fn for_loop_and_backward_input() {
let compiled = build_bytecode_with_call_feedback(
br#"
function fn()
local var = 3
for i = 1, 10 do
if var > 0 then print(i) end
var -= 1;
end
end
"#,
);
let function = compiled.function();
assert_eq!(function.blocks().len(), 6);
let entry = function.block(function.entry());
assert_eq!(
edge_kinds(entry),
[BytecodeEdgeKind::Branch, BytecodeEdgeKind::Fallthrough]
);
let loop_enter = fallthrough_block(&function, entry);
assert_eq!(
predecessor_kinds(loop_enter),
[BytecodeEdgeKind::Fallthrough, BytecodeEdgeKind::Loop]
);
assert_eq!(
edge_kinds(loop_enter),
[BytecodeEdgeKind::Branch, BytecodeEdgeKind::Fallthrough]
);
let loop_cond = fallthrough_block(&function, loop_enter);
assert_eq!(edge_kinds(loop_cond), [BytecodeEdgeKind::Fallthrough]);
let loop_epilog = branch_block(&function, loop_enter);
assert_eq!(
loop_cond.successors()[0].target,
loop_enter.successors()[0].target
);
assert_eq!(
edge_kinds(loop_epilog),
[BytecodeEdgeKind::Loop, BytecodeEdgeKind::Fallthrough]
);
assert_eq!(loop_target(loop_epilog), entry.successors()[1].target);
let ret = fallthrough_block(&function, loop_epilog);
assert_eq!(
opcodes(&function, entry),
[
Opcode::LoadK,
Opcode::LoadK,
Opcode::LoadK,
Opcode::LoadN,
Opcode::ForNPrep,
]
);
assert_eq!(
opcodes(&function, loop_enter),
[Opcode::LoadK, Opcode::JumpIfNotLt]
);
assert_eq!(
opcodes(&function, loop_cond),
[Opcode::GetGlobal, Opcode::Move, Opcode::CallFb]
);
assert_eq!(
opcodes(&function, loop_epilog),
[Opcode::LoadK, Opcode::Sub, Opcode::ForNLoop]
);
assert_eq!(opcodes(&function, ret), [Opcode::Return]);
let var_init = instruction_id(entry, 0);
let sub_var = instruction_id(loop_epilog, 1);
let jump_if_not_lt = instruction(&function, loop_enter, 1);
assert_eq!(jump_if_not_lt.operands().len(), 3);
assert_eq!(
jump_if_not_lt.operands()[0],
BytecodeOperand::Instruction(instruction_id(loop_enter, 0))
);
assert!(is_phi_of(
&function,
jump_if_not_lt.operands()[1],
var_init,
sub_var
));
assert_eq!(
jump_if_not_lt.operands()[2],
BytecodeOperand::Block(loop_enter.successors()[0].target)
);
let sub = instruction(&function, loop_epilog, 1);
assert_eq!(sub.operands().len(), 2);
assert!(is_phi_of(&function, sub.operands()[0], var_init, sub_var));
assert_eq!(
sub.operands()[1],
BytecodeOperand::Instruction(instruction_id(loop_epilog, 0))
);
}
#[test]
fn nested_loops() {
let compiled = function(
br#"
function fn()
local res = 0
local var = 0
repeat
local i = 0
repeat
res += i * var
i += 1
until i < 5
var += 1
until var < 10
end
"#,
);
let function = compiled.function();
assert_eq!(function.blocks().len(), 8);
let entry = function.block(function.entry());
let outer_entry = fallthrough_block(&function, entry);
let inner_entry = fallthrough_block(&function, outer_entry);
let inner_back_loop = fallthrough_block(&function, inner_entry);
let outer_epilog = branch_block(&function, inner_entry);
let outer_back_loop = fallthrough_block(&function, outer_epilog);
let ret = branch_block(&function, outer_epilog);
assert_eq!(edge_kinds(entry), [BytecodeEdgeKind::Fallthrough]);
assert_eq!(
predecessor_kinds(outer_entry),
[BytecodeEdgeKind::Fallthrough, BytecodeEdgeKind::Loop]
);
assert_eq!(edge_kinds(outer_entry), [BytecodeEdgeKind::Fallthrough]);
assert_eq!(
predecessor_kinds(inner_entry),
[BytecodeEdgeKind::Fallthrough, BytecodeEdgeKind::Loop]
);
assert_eq!(
edge_kinds(inner_entry),
[BytecodeEdgeKind::Branch, BytecodeEdgeKind::Fallthrough]
);
assert_eq!(edge_kinds(inner_back_loop), [BytecodeEdgeKind::Loop]);
assert_eq!(
loop_target(inner_back_loop),
outer_entry.successors()[0].target
);
assert_eq!(
edge_kinds(outer_epilog),
[BytecodeEdgeKind::Branch, BytecodeEdgeKind::Fallthrough]
);
assert_eq!(edge_kinds(outer_back_loop), [BytecodeEdgeKind::Loop]);
assert_eq!(loop_target(outer_back_loop), entry.successors()[0].target);
assert_eq!(opcodes(&function, entry), [Opcode::LoadK, Opcode::LoadK]);
assert_eq!(opcodes(&function, outer_entry), [Opcode::LoadK]);
assert_eq!(
opcodes(&function, inner_entry),
[
Opcode::Mul,
Opcode::Add,
Opcode::LoadK,
Opcode::Add,
Opcode::LoadK,
Opcode::JumpIfLt,
]
);
assert_eq!(opcodes(&function, inner_back_loop), [Opcode::JumpBack]);
assert_eq!(
opcodes(&function, outer_epilog),
[Opcode::LoadK, Opcode::Add, Opcode::LoadK, Opcode::JumpIfLt]
);
assert_eq!(opcodes(&function, outer_back_loop), [Opcode::JumpBack]);
assert_eq!(opcodes(&function, ret), [Opcode::Return]);
let var_init = instruction_id(entry, 1);
let var_inc = instruction_id(outer_epilog, 1);
let i_init = instruction_id(outer_entry, 0);
let i_inc = instruction_id(inner_entry, 3);
let i_times_var = instruction(&function, inner_entry, 0);
assert_eq!(i_times_var.operands().len(), 2);
assert!(is_phi_of(
&function,
i_times_var.operands()[0],
i_init,
i_inc
));
assert!(is_phi_of(
&function,
i_times_var.operands()[1],
var_init,
var_inc
));
}
#[test]
fn multi_call_fixed() {
let compiled = build_bytecode_with_call_feedback(
br#"
local function x()
local a, b = f()
return b, a
end
"#,
);
let function = compiled.function();
let entry = function.block(function.entry());
assert_eq!(
opcodes(&function, entry),
[
Opcode::GetGlobal,
Opcode::CallFb,
Opcode::Move,
Opcode::Move,
Opcode::Return,
]
);
let call = instruction_id(entry, 1);
let move_b = instruction(&function, entry, 2);
assert_eq!(move_b.operands().len(), 1);
let BytecodeOperand::Projection(projection) = move_b.operands()[0] else {
panic!("first move should read projected call result");
};
assert_eq!(
function.projection(projection).source,
BytecodeOperand::Instruction(call)
);
assert_eq!(function.projection(projection).index, 1);
let move_a = instruction(&function, entry, 3);
assert_eq!(move_a.operands().len(), 1);
let BytecodeOperand::Projection(projection) = move_a.operands()[0] else {
panic!("second move should read projected call result");
};
assert_eq!(
function.projection(projection).source,
BytecodeOperand::Instruction(call)
);
assert_eq!(function.projection(projection).index, 0);
let ret = instruction(&function, entry, 4);
assert_eq!(ret.operands().len(), 3);
let BytecodeOperand::Immediate(ret_count) = ret.operands()[0] else {
panic!("return count should be immediate");
};
assert_eq!(*function.immediate(ret_count), BytecodeImmediate::Int(2));
assert_eq!(
ret.operands()[1],
BytecodeOperand::Instruction(instruction_id(entry, 2))
);
assert_eq!(
ret.operands()[2],
BytecodeOperand::Instruction(instruction_id(entry, 3))
);
}
#[test]
fn multi_call_variadic() {
let compiled = build_bytecode_with_call_feedback(
br#"
local function fn(n)
if n > 0 then
return 0, 1
else
local a, b = fn(n - 1)
return a + b, fn(n)
end
end
"#,
);
let function = compiled.function();
assert_eq!(function.blocks().len(), 4);
let entry = function.block(function.entry());
let if_true = fallthrough_block(&function, entry);
let if_false = branch_block(&function, entry);
assert_eq!(
edge_kinds(entry),
[BytecodeEdgeKind::Branch, BytecodeEdgeKind::Fallthrough]
);
assert_eq!(edge_kinds(if_true), [BytecodeEdgeKind::Fallthrough]);
assert_eq!(
opcodes(&function, entry),
[Opcode::LoadK, Opcode::JumpIfNotLt]
);
assert_eq!(
opcodes(&function, if_true),
[Opcode::LoadK, Opcode::LoadK, Opcode::Return]
);
assert_eq!(
opcodes(&function, if_false),
[
Opcode::GetUpval,
Opcode::LoadK,
Opcode::Sub,
Opcode::CallFb,
Opcode::Add,
Opcode::GetUpval,
Opcode::Move,
Opcode::Call,
Opcode::Return,
]
);
let ret = instruction(&function, if_false, 8);
assert_eq!(ret.operands().len(), 3);
let BytecodeOperand::Immediate(ret_count) = ret.operands()[0] else {
panic!("return count should be immediate");
};
assert_eq!(*function.immediate(ret_count), BytecodeImmediate::Int(-1));
assert_eq!(
ret.operands()[1],
BytecodeOperand::Instruction(instruction_id(if_false, 4))
);
assert_eq!(
ret.operands()[2],
BytecodeOperand::Instruction(instruction_id(if_false, 7))
);
}
#[test]
fn variadic_function() {
let compiled = function(
br#"
local function fn(a, ...)
local b, c = ...
local l = {...}
return a + b + c + l[1], ...
end
"#,
);
let function = compiled.function();
assert_eq!(function.blocks().len(), 2);
let entry = function.block(function.entry());
assert_eq!(
opcodes(&function, entry),
[
Opcode::PrepVarargs,
Opcode::GetVarargs,
Opcode::NewTable,
Opcode::GetVarargs,
Opcode::SetList,
Opcode::Add,
Opcode::Add,
Opcode::LoadK,
Opcode::GetTable,
Opcode::Add,
Opcode::GetVarargs,
Opcode::Return,
]
);
let get_varargs_fixed = instruction(&function, entry, 1);
assert_eq!(get_varargs_fixed.operands().len(), 2);
assert_eq!(
get_varargs_fixed.operands()[0],
BytecodeOperand::VmRegister(1)
);
let BytecodeOperand::Immediate(count) = get_varargs_fixed.operands()[1] else {
panic!("fixed vararg count should be immediate");
};
assert_eq!(*function.immediate(count), BytecodeImmediate::Int(2));
let get_varargs_multret = instruction_id(entry, 3);
let get_varargs = instruction(&function, entry, 3);
assert_eq!(get_varargs.operands().len(), 2);
assert_eq!(get_varargs.operands()[0], BytecodeOperand::VmRegister(4));
let BytecodeOperand::Immediate(count) = get_varargs.operands()[1] else {
panic!("multret vararg count should be immediate");
};
assert_eq!(*function.immediate(count), BytecodeImmediate::Int(-1));
let set_list = instruction(&function, entry, 4);
assert_eq!(set_list.operands().len(), 4);
let BytecodeOperand::Immediate(start_index) = set_list.operands()[0] else {
panic!("setlist start index should be immediate");
};
assert_eq!(*function.immediate(start_index), BytecodeImmediate::Int(1));
let BytecodeOperand::Immediate(count) = set_list.operands()[1] else {
panic!("setlist count should be immediate");
};
assert_eq!(*function.immediate(count), BytecodeImmediate::Int(-1));
assert_eq!(
set_list.operands()[2],
BytecodeOperand::Instruction(instruction_id(entry, 2))
);
assert_eq!(
set_list.operands()[3],
BytecodeOperand::Instruction(get_varargs_multret)
);
}
#[test]
fn tables_strings_and_fastcall() {
let compiled = optimized_function(
br#"
local tt = {}
local function fn(x)
local t = { a = x, b = x .. 42 }
return table.insert({t}, tt)
end
"#,
);
let function = compiled.function();
assert_eq!(function.blocks().len(), 2);
let entry = function.block(function.entry());
assert_eq!(
opcodes(&function, entry),
[
Opcode::DupTable,
Opcode::SetTableKs,
Opcode::Move,
Opcode::LoadN,
Opcode::Concat,
Opcode::SetTableKs,
Opcode::NewTable,
Opcode::Move,
Opcode::SetList,
Opcode::GetUpval,
Opcode::FastCall2,
Opcode::GetImport,
Opcode::Call,
Opcode::Return,
]
);
}
#[test]
fn bytecode_roundtrip() {
for snippet in [
br#"
function fn(a, b)
local extra = 0
if a > b then extra = 1 end
return extra + a + b
end
"# as &[u8],
br#"
function fn()
local var = 0
repeat var += 1 until var < 10
end
"# as &[u8],
br#"
function fn()
local var = 3
for i = 1, 10 do
if var > 0 then print(i) end
var -= 1;
end
end
"# as &[u8],
br#"
function fn()
local res = 0
local var = 0
repeat
local i = 0
repeat
res += i * var
i += 1
until i < 5
var += 1
until var < 10
end
"# as &[u8],
br#"
local function x()
local a, b = f()
return b, a
end
"# as &[u8],
br#"
local function fn(n)
if n > 0 then
return 0, 1
else
local a, b = fn(n - 1)
return a + b, fn(n)
end
end
"# as &[u8],
br#"
local function fn(a, ...)
local b, c = ...
local l = {...}
return a + b + c + l[1], ...
end
"# as &[u8],
br#"
local function fn(x)
local f = function (a, b) return a .. " and " .. b .. " and agian " .. b end
return f(x, "eleven")
end
"# as &[u8],
br#"
local tt = {}
local function fn(x)
local t = { a = x, b = x .. 42 }
return table.insert({t}, tt)
end
"# as &[u8],
] {
roundtrip(snippet);
}
}
#[test]
fn classes_bytecode_roundtrips() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
roundtrip_with_options(
br#"
class Point
public x
public y
function magnitude(self)
return math.sqrt(self.x * self.x + self.y * self.y)
end
function __mul(self, other)
return Point { x = self.x * other.x, y = self.y * other.y }
end
function __add(self, other)
return Point { x = self.x + other.x, y = self.y + other.y }
end
function __eq(self, other)
return self.x == other.x and self.y == other.y
end
function zero()
return Point { x = 0, y = 0 }
end
function asserttriple(self)
local mag = self:magnitude()
assert(mag == math.ceil(mag), "Not a pythagorean triple!")
end
function __tostring(self)
return `Point(x={self.x}, y={self.y})`
end
end
print(Point)
return { Point = Point }
"#,
ParseOptions::default(),
CompileOptions::default(),
);
}
#[test]
fn inheriting_classes_bytecode_roundtrips() {
let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
roundtrip_with_options(
br#"
class Animal
public species: string
function __tostring(self)
return "I am an animal."
end
function live(self)
return "I am alive"
end
end
class Cat extends Animal
public breed: string
function __tostring(self): string
return `{Animal.__tostring(self)} I am a {self.breed} cat.`
end
end
print(Cat)
return { Animal = Animal, Cat = Cat }
"#,
ParseOptions::default(),
CompileOptions::default(),
);
}