use std::io::Write;
use luau_common::{BStr, BString, ByteSlice};
use crate::model::{ClosureIndex, Instruction, InstructionAux};
use crate::opcodes::Opcode;
use crate::wire::{BytecodeFunctionWire, ClosureNameLookup};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct BytecodeDebugRemark<'a> {
pub pc: usize,
pub text: &'a BStr,
}
pub(super) fn dump_function(
function: &dyn BytecodeFunctionWire,
source_lines: Option<&[&BStr]>,
dump_lines: bool,
remarks: &[BytecodeDebugRemark<'_>],
closure_names: &dyn ClosureNameLookup,
) -> (BString, Vec<i32>) {
let code = function.code();
if code.is_empty() {
return (BString::default(), Vec::new());
}
let mut labels: Vec<Option<usize>> = vec![None; code.len()];
let mut pc = 0usize;
while pc < code.len() {
let inst = code[pc];
if let Some(target) = BytecodeDumpWriter::jump_target(inst, pc) {
debug_assert!(
target < labels.len(),
"bytecode dump jump target must point inside the function"
);
labels[target] = Some(0);
}
pc += unsafe { inst.opcode_unchecked() }.length();
}
let mut next_label = 0usize;
for label in &mut labels {
if label.is_some() {
*label = Some(next_label);
next_label += 1;
}
}
let mut writer = BytecodeDumpWriter::new(function, closure_names);
let mut offsets = vec![-1; code.len() + 1];
let mut last_line = -1i32;
let mut i = 0usize;
let mut next_remark = 0usize;
while i < code.len() {
let inst = code[i];
let opcode = unsafe { inst.opcode_unchecked() };
offsets[i] = i32::try_from(writer.len()).unwrap_or(i32::MAX);
if opcode == Opcode::PrepVarargs {
i += 1;
continue;
}
while next_remark < remarks.len() && remarks[next_remark].pc == i {
writer.write_bstr(b"REMARK ".as_bstr());
writer.write_bstr(remarks[next_remark].text);
writer.write_bstr(b"\n".as_bstr());
next_remark += 1;
}
if let Some(src_lines) = source_lines {
let line = function.line_for_pc(i).unwrap_or(0);
if line > 0 && line != last_line {
let line_idx = line as usize - 1;
if line_idx < src_lines.len() {
write!(writer, "{line:>5}: ").unwrap();
writer.write_bstr(src_lines[line_idx]);
writer.write_bstr(b"\n".as_bstr());
}
last_line = line;
}
}
if dump_lines {
let line = function.line_for_pc(i).unwrap_or(0);
write!(writer, "{line}: ").unwrap();
}
if let Some(Some(label_id)) = labels.get(i) {
write!(writer, "L{}: ", label_id).unwrap();
}
let aux = (opcode.length() == 2)
.then(|| code.get(i + 1).copied().map(Instruction::word))
.flatten()
.map(InstructionAux::new);
let target_label = BytecodeDumpWriter::jump_target(inst, i)
.and_then(|idx| labels.get(idx).copied().flatten());
writer.dump_instruction(opcode, inst, aux, target_label);
i += opcode.length();
}
offsets[code.len()] = i32::try_from(writer.len()).unwrap_or(i32::MAX);
(writer.into_output(), offsets)
}
pub(super) fn dump_constants(
function: &dyn BytecodeFunctionWire,
closure_names: &dyn ClosureNameLookup,
) -> BString {
let mut writer = BytecodeDumpWriter::new(function, closure_names);
for index in 0..function.constant_count() {
write!(writer, "K{index}: ").unwrap();
writer.dump_constant_detailed(index);
writer.write_bstr(b"\n".as_bstr());
}
writer.into_output()
}
struct BytecodeDumpWriter<'a> {
bytes: Vec<u8>,
function: &'a dyn BytecodeFunctionWire,
closure_names: &'a dyn ClosureNameLookup,
}
impl<'a> BytecodeDumpWriter<'a> {
fn jump_target(inst: Instruction, pc: usize) -> Option<usize> {
unsafe { inst.jump_target_unchecked(pc as u32) }
.filter(|target| *target >= 0)
.map(|target| target as usize)
}
fn new(
function: &'a dyn BytecodeFunctionWire,
closure_names: &'a dyn ClosureNameLookup,
) -> Self {
Self {
bytes: Vec::new(),
function,
closure_names,
}
}
fn len(&self) -> usize {
self.bytes.len()
}
fn into_output(self) -> BString {
BString::new(self.bytes)
}
fn write_bstr(&mut self, bytes: &BStr) {
self.bytes.extend_from_slice(bytes);
}
fn dump_constant_detailed(&mut self, index: usize) {
self.dump_constant_with_detail(index, true);
}
fn dump_constant_with_detail(&mut self, index: usize, detailed: bool) {
self.function
.append_constant(&mut self.bytes, index, self.closure_names, detailed);
}
fn dump_instruction(
&mut self,
opcode: Opcode,
inst: Instruction,
aux: Option<InstructionAux>,
target_label: Option<usize>,
) {
let constants = self.function;
let closure_names = self.closure_names;
let result = &mut self.bytes;
let a = inst.a();
let b = inst.b();
let c = inst.c();
let d = inst.d();
let aux_kv = aux.map(InstructionAux::kv).unwrap_or(0);
let aux_kv16 = aux.map(InstructionAux::kv16).unwrap_or(0);
let _aux_slot = aux.map(InstructionAux::slot).unwrap_or(0);
let aux_word = aux.map(InstructionAux::word).unwrap_or(0);
let aux = aux_word;
match opcode {
Opcode::Nop => writeln!(result, "NOP").unwrap(),
Opcode::Break => writeln!(result, "BREAK").unwrap(),
Opcode::LoadNil => writeln!(result, "LOADNIL R{a}").unwrap(),
Opcode::LoadB => {
if c != 0 {
writeln!(result, "LOADB R{a} {b} +{c}").unwrap();
} else {
writeln!(result, "LOADB R{a} {b}").unwrap();
}
}
Opcode::LoadN => {
writeln!(result, "LOADN R{a} {d}").unwrap();
}
Opcode::LoadK => {
let k = d as u16 as usize;
write_constant_instruction(
result,
format_args!("LOADK R{a} K{k} ["),
constants,
k,
closure_names,
);
}
Opcode::Move => writeln!(result, "MOVE R{a} R{b}").unwrap(),
Opcode::GetGlobal => {
let k = aux_word as usize;
write_constant_instruction(
result,
format_args!("GETGLOBAL R{a} K{k} ["),
constants,
k,
closure_names,
);
}
Opcode::SetGlobal => {
let k = aux_word as usize;
write_constant_instruction(
result,
format_args!("SETGLOBAL R{a} K{k} ["),
constants,
k,
closure_names,
);
}
Opcode::GetUpval => writeln!(result, "GETUPVAL R{a} {b}").unwrap(),
Opcode::SetUpval => writeln!(result, "SETUPVAL R{a} {b}").unwrap(),
Opcode::CloseUpvals => writeln!(result, "CLOSEUPVALS R{a}").unwrap(),
Opcode::GetImport => {
let id = d as u16 as usize;
write_constant_instruction(
result,
format_args!("GETIMPORT R{a} {id} ["),
constants,
id,
closure_names,
);
}
Opcode::GetTable => writeln!(result, "GETTABLE R{a} R{b} R{c}").unwrap(),
Opcode::SetTable => writeln!(result, "SETTABLE R{a} R{b} R{c}").unwrap(),
Opcode::GetTableKs => {
let k = aux_word as usize;
write_constant_instruction(
result,
format_args!("GETTABLEKS R{a} R{b} K{k} ["),
constants,
k,
closure_names,
);
}
Opcode::SetTableKs => {
let k = aux_word as usize;
write_constant_instruction(
result,
format_args!("SETTABLEKS R{a} R{b} K{k} ["),
constants,
k,
closure_names,
);
}
Opcode::GetTableN => {
writeln!(result, "GETTABLEN R{a} R{b} {}", u16::from(c) + 1).unwrap();
}
Opcode::SetTableN => {
writeln!(result, "SETTABLEN R{a} R{b} {}", u16::from(c) + 1).unwrap();
}
Opcode::NewClosure => {
let p = d as u16 as usize;
writeln!(result, "NEWCLOSURE R{a} P{p}").unwrap();
}
Opcode::NameCall => {
let k = aux_word as usize;
write_constant_instruction(
result,
format_args!("NAMECALL R{a} R{b} K{k} ["),
constants,
k,
closure_names,
);
}
Opcode::Call => {
writeln!(
result,
"CALL R{a} {} {}",
i32::from(b) - 1,
i32::from(c) - 1,
)
.unwrap();
}
Opcode::CallFb => {
writeln!(
result,
"CALLFB R{a} {} {} [{aux}]",
i32::from(b) - 1,
i32::from(c) - 1,
)
.unwrap();
}
Opcode::Return => {
writeln!(result, "RETURN R{a} {}", i32::from(b) - 1).unwrap();
}
Opcode::Jump => {
let lbl = target_label.expect("bytecode dump jump target must have a label");
writeln!(result, "JUMP L{lbl}").unwrap();
}
Opcode::JumpBack => {
let lbl = target_label.expect("bytecode dump jump target must have a label");
writeln!(result, "JUMPBACK L{lbl}").unwrap();
}
Opcode::JumpX => {
let lbl = target_label.expect("bytecode dump jump target must have a label");
writeln!(result, "JUMPX L{lbl}").unwrap();
}
Opcode::JumpIf => {
let lbl = target_label.expect("bytecode dump jump target must have a label");
writeln!(result, "JUMPIF R{a} L{lbl}").unwrap();
}
Opcode::JumpIfNot => {
let lbl = target_label.expect("bytecode dump jump target must have a label");
writeln!(result, "JUMPIFNOT R{a} L{lbl}").unwrap();
}
Opcode::JumpIfEq => {
let lbl = target_label.expect("bytecode dump jump target must have a label");
writeln!(result, "JUMPIFEQ R{a} R{aux} L{lbl}").unwrap();
}
Opcode::JumpIfLe => {
let lbl = target_label.expect("bytecode dump jump target must have a label");
writeln!(result, "JUMPIFLE R{a} R{aux} L{lbl}").unwrap();
}
Opcode::JumpIfLt => {
let lbl = target_label.expect("bytecode dump jump target must have a label");
writeln!(result, "JUMPIFLT R{a} R{aux} L{lbl}").unwrap();
}
Opcode::JumpIfNotEq => {
let lbl = target_label.expect("bytecode dump jump target must have a label");
writeln!(result, "JUMPIFNOTEQ R{a} R{aux} L{lbl}").unwrap();
}
Opcode::JumpIfNotLe => {
let lbl = target_label.expect("bytecode dump jump target must have a label");
writeln!(result, "JUMPIFNOTLE R{a} R{aux} L{lbl}").unwrap();
}
Opcode::JumpIfNotLt => {
let lbl = target_label.expect("bytecode dump jump target must have a label");
writeln!(result, "JUMPIFNOTLT R{a} R{aux} L{lbl}").unwrap();
}
Opcode::Add => writeln!(result, "ADD R{a} R{b} R{c}").unwrap(),
Opcode::Sub => writeln!(result, "SUB R{a} R{b} R{c}").unwrap(),
Opcode::Mul => writeln!(result, "MUL R{a} R{b} R{c}").unwrap(),
Opcode::Div => writeln!(result, "DIV R{a} R{b} R{c}").unwrap(),
Opcode::Mod => writeln!(result, "MOD R{a} R{b} R{c}").unwrap(),
Opcode::Pow => writeln!(result, "POW R{a} R{b} R{c}").unwrap(),
Opcode::IDiv => writeln!(result, "IDIV R{a} R{b} R{c}").unwrap(),
Opcode::AddK => {
let k = c as usize;
write_constant_instruction(
result,
format_args!("ADDK R{a} R{b} K{k} ["),
constants,
k,
closure_names,
);
}
Opcode::SubK => {
let k = c as usize;
write_constant_instruction(
result,
format_args!("SUBK R{a} R{b} K{k} ["),
constants,
k,
closure_names,
);
}
Opcode::MulK => {
let k = c as usize;
write_constant_instruction(
result,
format_args!("MULK R{a} R{b} K{k} ["),
constants,
k,
closure_names,
);
}
Opcode::DivK => {
let k = c as usize;
write_constant_instruction(
result,
format_args!("DIVK R{a} R{b} K{k} ["),
constants,
k,
closure_names,
);
}
Opcode::ModK => {
let k = c as usize;
write_constant_instruction(
result,
format_args!("MODK R{a} R{b} K{k} ["),
constants,
k,
closure_names,
);
}
Opcode::PowK => {
let k = c as usize;
write_constant_instruction(
result,
format_args!("POWK R{a} R{b} K{k} ["),
constants,
k,
closure_names,
);
}
Opcode::IDivK => {
let k = c as usize;
write_constant_instruction(
result,
format_args!("IDIVK R{a} R{b} K{k} ["),
constants,
k,
closure_names,
);
}
Opcode::SubRK => {
let k = b as usize;
write!(result, "SUBRK R{a} K{k} [").unwrap();
constants.append_constant(result, k, closure_names, false);
writeln!(result, "] R{c}").unwrap();
}
Opcode::DivRK => {
let k = b as usize;
write!(result, "DIVRK R{a} K{k} [").unwrap();
constants.append_constant(result, k, closure_names, false);
writeln!(result, "] R{c}").unwrap();
}
Opcode::And => writeln!(result, "AND R{a} R{b} R{c}").unwrap(),
Opcode::Or => writeln!(result, "OR R{a} R{b} R{c}").unwrap(),
Opcode::AndK => {
let k = c as usize;
write_constant_instruction(
result,
format_args!("ANDK R{a} R{b} K{k} ["),
constants,
k,
closure_names,
);
}
Opcode::OrK => {
let k = c as usize;
write_constant_instruction(
result,
format_args!("ORK R{a} R{b} K{k} ["),
constants,
k,
closure_names,
);
}
Opcode::Concat => writeln!(result, "CONCAT R{a} R{b} R{c}").unwrap(),
Opcode::Not => writeln!(result, "NOT R{a} R{b}").unwrap(),
Opcode::Minus => writeln!(result, "MINUS R{a} R{b}").unwrap(),
Opcode::Length => writeln!(result, "LENGTH R{a} R{b}").unwrap(),
Opcode::NewTable => {
let hash = if b == 0 { 0 } else { 1u32 << (b - 1) };
writeln!(result, "NEWTABLE R{a} {hash} {aux}").unwrap();
}
Opcode::DupTable => {
let k = d as u16 as usize;
writeln!(result, "DUPTABLE R{a} {k}").unwrap();
}
Opcode::SetList => {
let count = i32::from(c) - 1;
writeln!(result, "SETLIST R{a} R{b} {count} [{aux}]").unwrap();
}
Opcode::ForNPrep => {
let lbl = target_label.expect("bytecode dump jump target must have a label");
writeln!(result, "FORNPREP R{a} L{lbl}").unwrap();
}
Opcode::ForNLoop => {
let lbl = target_label.expect("bytecode dump jump target must have a label");
writeln!(result, "FORNLOOP R{a} L{lbl}").unwrap();
}
Opcode::ForGPrep => {
let lbl = target_label.expect("bytecode dump jump target must have a label");
writeln!(result, "FORGPREP R{a} L{lbl}").unwrap();
}
Opcode::ForGLoop => {
let extra = (aux & 0xff) as u8;
let inext = if (aux as i32) < 0 { " [inext]" } else { "" };
let lbl = target_label.expect("bytecode dump jump target must have a label");
writeln!(result, "FORGLOOP R{a} L{lbl} {extra}{inext}").unwrap();
}
Opcode::ForGPrepInext => {
let lbl = target_label.expect("bytecode dump jump target must have a label");
writeln!(result, "FORGPREP_INEXT R{a} L{lbl}").unwrap();
}
Opcode::ForGPrepNext => {
let lbl = target_label.expect("bytecode dump jump target must have a label");
writeln!(result, "FORGPREP_NEXT R{a} L{lbl}").unwrap();
}
Opcode::GetVarargs => {
writeln!(result, "GETVARARGS R{a} {}", i32::from(b) - 1).unwrap();
}
Opcode::DupClosure => {
let k = d as u16 as usize;
write_constant_instruction(
result,
format_args!("DUPCLOSURE R{a} K{k} ["),
constants,
k,
closure_names,
);
}
Opcode::LoadKx => {
let k = aux_word as usize;
write_constant_instruction(
result,
format_args!("LOADKX R{a} K{k} ["),
constants,
k,
closure_names,
);
}
Opcode::FastCall => {
let lbl = target_label.expect("bytecode dump jump target must have a label");
writeln!(result, "FASTCALL {a} L{lbl}").unwrap();
}
Opcode::FastCall1 => {
let lbl = target_label.expect("bytecode dump jump target must have a label");
writeln!(result, "FASTCALL1 {a} R{b} L{lbl}").unwrap();
}
Opcode::FastCall2 => {
let lbl = target_label.expect("bytecode dump jump target must have a label");
writeln!(result, "FASTCALL2 {a} R{b} R{aux} L{lbl}").unwrap();
}
Opcode::FastCall2K => {
let k = aux_word as usize;
let lbl = target_label.expect("bytecode dump jump target must have a label");
write!(result, "FASTCALL2K {a} R{b} K{k} L{lbl} [").unwrap();
constants.append_constant(result, k, closure_names, false);
result.extend_from_slice(b"]\n");
}
Opcode::FastCall3 => {
let lbl = target_label.expect("bytecode dump jump target must have a label");
writeln!(
result,
"FASTCALL3 {a} R{b} R{} R{} L{lbl}",
aux & 0xff,
(aux >> 8) & 0xff,
)
.unwrap();
}
Opcode::Coverage => {
writeln!(result, "COVERAGE").unwrap();
}
Opcode::Capture => {
let cap_type = match a {
2 => "UPVAL",
1 => "REF",
_ => "VAL",
};
let reg_prefix = if a == 2 { 'U' } else { 'R' };
writeln!(result, "CAPTURE {cap_type} {reg_prefix}{b}").unwrap();
}
Opcode::NativeCall => writeln!(result, "NATIVECALL").unwrap(),
Opcode::JumpXEqKNil => {
let not = if aux >> 31 != 0 { " NOT" } else { "" };
let lbl = target_label.expect("bytecode dump jump target must have a label");
writeln!(result, "JUMPXEQKNIL R{a} L{lbl}{not}").unwrap();
}
Opcode::JumpXEqKB => {
let bit = aux & 1;
let not = if aux >> 31 != 0 { " NOT" } else { "" };
let lbl = target_label.expect("bytecode dump jump target must have a label");
writeln!(result, "JUMPXEQKB R{a} {bit} L{lbl}{not}").unwrap();
}
Opcode::JumpXEqKN => {
let k = aux_kv as usize;
let not = if aux >> 31 != 0 { " NOT" } else { "" };
let lbl = target_label.expect("bytecode dump jump target must have a label");
write!(result, "JUMPXEQKN R{a} K{k} L{lbl}{not} [").unwrap();
constants.append_constant(result, k, closure_names, false);
result.extend_from_slice(b"]\n");
}
Opcode::JumpXEqKS => {
let k = aux_kv as usize;
let not = if aux >> 31 != 0 { " NOT" } else { "" };
let lbl = target_label.expect("bytecode dump jump target must have a label");
write!(result, "JUMPXEQKS R{a} K{k} L{lbl}{not} [").unwrap();
constants.append_constant(result, k, closure_names, false);
result.extend_from_slice(b"]\n");
}
Opcode::GetUDataKs => {
let k = aux_kv16 as usize;
write_constant_instruction(
result,
format_args!("GETUDATAKS R{a} R{b} K{k} ["),
constants,
k,
closure_names,
);
}
Opcode::SetUDataKs => {
let k = aux_kv16 as usize;
write_constant_instruction(
result,
format_args!("SETUDATAKS R{a} R{b} K{k} ["),
constants,
k,
closure_names,
);
}
Opcode::NameCallUData => {
let k = aux_kv16 as usize;
write_constant_instruction(
result,
format_args!("NAMECALLUDATA R{a} R{b} K{k} ["),
constants,
k,
closure_names,
);
}
Opcode::NewClassMember => {
let k = aux_word as usize;
write_constant_instruction(
result,
format_args!("NEWCLASSMEMBER R{a} R{c} ["),
constants,
k,
closure_names,
);
}
Opcode::CmpProto => {
let lbl = target_label.expect("bytecode dump jump target must have a label");
writeln!(result, "CMPPROTO R{a} #{aux} L{lbl}").unwrap();
}
Opcode::NewClass => {
let k = aux_word as usize;
write_constant_instruction(
result,
format_args!("NEWCLASS R{a} R{b} K{k} ["),
constants,
k,
closure_names,
);
}
Opcode::PrepVarargs => {
}
}
}
}
fn write_constant_instruction(
result: &mut Vec<u8>,
prefix: std::fmt::Arguments<'_>,
function: &dyn BytecodeFunctionWire,
index: usize,
closure_names: &dyn ClosureNameLookup,
) {
write!(result, "{prefix}").unwrap();
function.append_constant(result, index, closure_names, false);
result.extend_from_slice(b"]\n");
}
pub(crate) fn append_closure_name(
result: &mut Vec<u8>,
id: ClosureIndex,
closure_names: &dyn ClosureNameLookup,
) {
if let Some(name) = closure_names
.closure_name(id)
.filter(|name| !name.is_empty())
{
result.push(b'\'');
result.extend_from_slice(name);
result.push(b'\'');
}
}
pub(crate) fn append_string_constant(result: &mut Vec<u8>, bytes: &BStr) {
result.push(b'\'');
let printable = bytes.iter().all(|byte| *byte >= b' ');
for byte in bytes.iter().take(32) {
if !printable && *byte < b' ' {
write!(result, "\\x{byte:02X}").unwrap();
} else {
result.push(*byte);
}
}
if bytes.len() >= 32 {
result.extend_from_slice(b"'...");
} else {
result.push(b'\'');
}
}
impl Write for BytecodeDumpWriter<'_> {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
self.bytes.extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}