use std::collections::BTreeMap;
use std::fmt;
use std::io;
use crate::bytecode::error::StreamError;
use crate::bytecode::{Instruction, InstructionStream, Program, Register};
use crate::opcodes;
#[expect(
clippy::cast_possible_truncation,
reason = "bytes.len() <= 8 per debug_assert; 8 * 8 = 64 always fits in u32"
)]
fn sign_extend_be(bytes: &[u8]) -> i64 {
debug_assert!(!bytes.is_empty() && bytes.len() <= 8);
let mut v = 0i64;
for &b in bytes {
v = (v << 8) | i64::from(b);
}
let shift = 64u32 - (bytes.len() * 8) as u32;
(v << shift) >> shift
}
trait FmtOperand {
fn fmt_operand(
&self,
f: &mut dyn io::Write,
labels: &BTreeMap<usize, String>,
instr_offset: usize,
) -> io::Result<()>;
}
impl FmtOperand for Register {
fn fmt_operand(
&self,
f: &mut dyn io::Write,
_labels: &BTreeMap<usize, String>,
_instr_offset: usize,
) -> io::Result<()> {
write!(f, "r{}", self.0)
}
}
impl FmtOperand for i64 {
fn fmt_operand(
&self,
f: &mut dyn io::Write,
_labels: &BTreeMap<usize, String>,
_instr_offset: usize,
) -> io::Result<()> {
write!(f, "{self}")
}
}
macro_rules! impl_fmt_operand_byte_array {
($($n:literal),+) => {
$(
impl FmtOperand for [u8; $n] {
fn fmt_operand(
&self,
f: &mut dyn io::Write,
_labels: &BTreeMap<usize, String>,
_instr_offset: usize,
) -> io::Result<()> {
write!(f, "{}", sign_extend_be(self))
}
}
)+
};
}
impl_fmt_operand_byte_array!(1, 2, 3, 4, 5, 6, 7, 8);
impl FmtOperand for u16 {
fn fmt_operand(
&self,
f: &mut dyn io::Write,
_labels: &BTreeMap<usize, String>,
_instr_offset: usize,
) -> io::Result<()> {
write!(f, ".{self}")
}
}
impl FmtOperand for u8 {
fn fmt_operand(
&self,
f: &mut dyn io::Write,
_labels: &BTreeMap<usize, String>,
_instr_offset: usize,
) -> io::Result<()> {
write!(f, ".{self}")
}
}
macro_rules! impl_fmt_instruction {
( $( ($code:literal, $variant:ident, $mnem:literal, $doc:literal,
$_delta:expr, {$($fname:ident: $ftype:ty),*}) ),* $(,)? ) => {
fn fmt_instruction(
instr: &Instruction,
instr_offset: usize,
labels: &BTreeMap<usize, String>,
f: &mut dyn io::Write,
) -> io::Result<()> {
match instr {
$(
Instruction::$variant { $($fname,)* } => {
write!(f, "{:<8}", $mnem)?;
let mut _first = true;
$(
if !_first { write!(f, ", ")?; }
$fname.fmt_operand(f, labels, instr_offset)?;
_first = false;
)*
let _ = _first;
Ok(())
}
)*
}
}
};
}
opcodes!(impl_fmt_instruction);
#[derive(Debug, Clone)]
pub struct Disassembly<'a> {
stream: InstructionStream<'a>,
}
impl<'a> Disassembly<'a> {
pub fn new(bytes: &'a [u8]) -> Self {
Self {
stream: InstructionStream::new(bytes),
}
}
pub fn from_program(program: &'a Program) -> Self {
Self {
stream: InstructionStream::from_program(program),
}
}
pub fn write_to(&self, out: &mut impl io::Write) -> io::Result<()> {
let label_col = self
.stream
.labels()
.values()
.map(|s| s.len() + 1)
.max()
.unwrap_or(0);
let labels = self.stream.labels().clone();
for item in self.stream.clone() {
match item {
Ok((offset, label, instr)) => {
let label_str = label.map(|s| format!("{s}:")).unwrap_or_default();
write!(out, " 0x{offset:04X}: ")?;
if label_col > 0 {
write!(out, "{label_str:<label_col$} ")?;
}
fmt_instruction(&instr, offset, &labels, out)?;
writeln!(out)?;
}
Err(ref e) => {
let (offset, byte) = match e {
StreamError::UnknownOpcode { offset, byte } => (*offset, *byte),
StreamError::TruncatedInstruction { offset } => {
let byte = *self.stream.bytes().get(*offset).unwrap_or_else(|| {
unreachable!("TruncatedInstruction offset within buffer")
});
(*offset, byte)
}
StreamError::SeekOutOfBounds { .. } => {
unreachable!("stream never seeks internally")
}
};
let label_str = labels
.get(&offset)
.map(|s| format!("{s}:"))
.unwrap_or_default();
write!(out, " 0x{offset:04X}: ")?;
if label_col > 0 {
write!(out, "{label_str:<label_col$} ")?;
}
writeln!(out, ".byte 0x{byte:02X}")?;
}
}
}
Ok(())
}
}
impl fmt::Display for Disassembly<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut buf = Vec::new();
self.write_to(&mut buf).map_err(|_| fmt::Error)?;
f.write_str(std::str::from_utf8(&buf).map_err(|_| fmt::Error)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bytecode::{Instruction, JumpTable, Program, Register, codec};
fn assemble(program: &[Instruction]) -> Vec<u8> {
program.iter().flat_map(codec::encode).collect()
}
#[test]
fn basic_program_contains_mnemonics() {
let buf = assemble(&[Instruction::Push1 { val: [42] }, Instruction::Halt {}]);
let text = Disassembly::new(&buf).to_string();
assert!(text.contains("PUSH1"), "missing PUSH1 in:\n{text}");
assert!(text.contains("42"), "missing immediate 42 in:\n{text}");
assert!(text.contains("HALT"), "missing HALT in:\n{text}");
}
#[test]
fn register_operand_displays_as_r_slot() {
let buf = assemble(&[Instruction::Load { reg: Register(3) }]);
let text = Disassembly::new(&buf).to_string();
assert!(text.contains("r3"), "expected r3 in:\n{text}");
}
#[test]
fn byte_offsets_are_correct() {
let buf = assemble(&[Instruction::Pop {}, Instruction::Halt {}]);
let text = Disassembly::new(&buf).to_string();
assert!(text.contains("0x0000"), "missing 0x0000 in:\n{text}");
assert!(text.contains("0x0001"), "missing 0x0001 in:\n{text}");
}
#[test]
fn backward_jump_gets_label() {
let code = assemble(&[
Instruction::Target {},
Instruction::Push1 { val: [5] },
Instruction::Gt {},
Instruction::JumpI1 { label: 0u8 },
Instruction::Halt {},
]);
let program = Program::new(code);
let text = Disassembly::from_program(&program).to_string();
assert!(text.contains(".0:"), "missing label .0 in:\n{text}");
assert!(
text.contains("JUMPI1 .0"),
"expected 'JUMPI1 .0' in:\n{text}"
);
}
#[test]
fn forward_jump_gets_label() {
let code = assemble(&[
Instruction::Jump1 { label: 0u8 },
Instruction::Nop {},
Instruction::Target {},
Instruction::Halt {},
]);
let program = Program::new(code);
let text = Disassembly::from_program(&program).to_string();
assert!(text.contains(".0:"), "missing label in:\n{text}");
assert!(
text.contains("JUMP1 .0"),
"expected 'JUMP1 .0' in:\n{text}"
);
}
#[test]
fn two_distinct_labels_appear() {
let code = assemble(&[
Instruction::Target {},
Instruction::JumpI1 { label: 0u8 },
Instruction::Jump1 { label: 1u8 },
Instruction::Target {},
Instruction::Halt {},
]);
let program = Program::new(code);
assert_eq!(program.jump_table().len(), 2);
let _ = JumpTable::default(); let text = Disassembly::from_program(&program).to_string();
assert!(text.contains(".0:"), "missing .0 in:\n{text}");
assert!(text.contains(".1:"), "missing .1 in:\n{text}");
}
#[test]
fn unknown_byte_displays_as_dot_byte() {
let buf = [0xFEu8]; let text = Disassembly::new(&buf).to_string();
assert!(text.contains(".byte"), "expected .byte in:\n{text}");
assert!(text.contains("0xFE"), "expected 0xFE in:\n{text}");
}
#[test]
fn energy_instruction_shows_two_registers() {
let buf = assemble(&[Instruction::Energy {
model: Register(1),
sample: Register(2),
}]);
let text = Disassembly::new(&buf).to_string();
assert!(text.contains("ENERGY"), "missing ENERGY in:\n{text}");
assert!(text.contains("r1"), "missing r1 in:\n{text}");
assert!(text.contains("r2"), "missing r2 in:\n{text}");
}
#[test]
fn negative_immediate_displays_correctly() {
let buf = assemble(&[Instruction::Push1 { val: [0xF9] }]); let text = Disassembly::new(&buf).to_string();
assert!(text.contains("-7"), "expected -7 in:\n{text}");
}
#[test]
fn empty_bytecode_produces_empty_output() {
assert_eq!(Disassembly::new(&[]).to_string(), "");
}
#[test]
fn push2_displays_value() {
let buf = assemble(&[
Instruction::Push2 { val: [0x00, 0x07] },
Instruction::Halt {},
]);
let text = Disassembly::new(&buf).to_string();
assert!(text.contains("PUSH2"), "missing PUSH2 in:\n{text}");
assert!(text.contains('7'), "missing value 7 in:\n{text}");
}
#[test]
fn push3_displays_large_value() {
let buf = assemble(&[
Instruction::Push3 {
val: [0x01, 0x86, 0xA0],
},
Instruction::Halt {},
]);
let text = Disassembly::new(&buf).to_string();
assert!(text.contains("100000"), "missing 100000 in:\n{text}");
}
}