use core::fmt;
use super::isa::{Class, Insn, Mode, decode};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Disassembled {
pub pc: u16,
pub opcode: u8,
pub insn: Insn,
pub operand: [u8; 2],
pub len: u8,
pub truncated: bool,
}
impl Disassembled {
#[must_use]
pub const fn word(&self) -> u16 {
(self.operand[0] as u16) | ((self.operand[1] as u16) << 8)
}
#[must_use]
pub const fn branch_target(&self) -> Option<u16> {
match self.insn.mode {
Mode::Relative => Some(
self.pc
.wrapping_add(2)
.wrapping_add(self.operand[0] as i8 as u16),
),
_ => None,
}
}
#[must_use]
pub const fn static_target(&self) -> Option<u16> {
match self.insn.mode {
Mode::ZeroPage => Some(self.operand[0] as u16),
Mode::Absolute => Some(self.word()),
Mode::Relative => self.branch_target(),
_ => None,
}
}
#[must_use]
pub const fn is_undocumented(&self) -> bool {
!matches!(self.insn.class, Class::Documented)
}
}
impl fmt::Display for Disassembled {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.insn.op.mnemonic())?;
let lo = self.operand[0];
match self.insn.mode {
Mode::Implied | Mode::Break => Ok(()),
Mode::Accumulator => f.write_str(" A"),
Mode::Immediate => write!(f, " #${lo:02x}"),
Mode::ZeroPage => write!(f, " ${lo:02x}"),
Mode::ZeroPageX => write!(f, " ${lo:02x},X"),
Mode::ZeroPageY => write!(f, " ${lo:02x},Y"),
Mode::Absolute => write!(f, " ${:04x}", self.word()),
Mode::AbsoluteX => write!(f, " ${:04x},X", self.word()),
Mode::AbsoluteY => write!(f, " ${:04x},Y", self.word()),
Mode::Indirect => write!(f, " (${:04x})", self.word()),
Mode::IndirectX => write!(f, " (${lo:02x},X)"),
Mode::IndirectY => write!(f, " (${lo:02x}),Y"),
Mode::Relative => write!(
f,
" ${:04x}",
self.branch_target().expect("relative mode has a target")
),
}
}
}
#[must_use]
pub fn disassemble(pc: u16, bytes: &[u8]) -> Disassembled {
let opcode = bytes.first().copied().unwrap_or(0);
let insn = decode(opcode);
let len = insn.bytes() as usize;
let mut operand = [0u8; 2];
let mut truncated = bytes.len() < len;
for (i, slot) in operand.iter_mut().enumerate().take(len.saturating_sub(1)) {
match bytes.get(i + 1) {
Some(b) => *slot = *b,
None => truncated = true,
}
}
Disassembled {
pc,
opcode,
insn,
operand,
len: len as u8,
truncated,
}
}
pub fn disassemble_run(
pc: u16,
count: usize,
mut fetch: impl FnMut(u16) -> Option<u8>,
) -> alloc::vec::Vec<Disassembled> {
let mut out = alloc::vec::Vec::with_capacity(count);
let mut at = pc;
for _ in 0..count {
let mut window = [0u8; 3];
let mut got = 0usize;
for (i, slot) in window.iter_mut().enumerate() {
match fetch(at.wrapping_add(i as u16)) {
Some(b) => {
*slot = b;
got += 1;
}
None => break,
}
}
if got == 0 {
break;
}
let d = disassemble(at, &window[..got]);
at = at.wrapping_add(u16::from(d.len));
out.push(d);
if got < 3 && d.truncated {
break;
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
fn reassemble(text: &str) -> Option<(u8, Vec<u8>)> {
let (mnemonic, operand) = match text.split_once(' ') {
Some((m, o)) => (m, o.trim()),
None => (text, ""),
};
let hex = |s: &str| u16::from_str_radix(s, 16).ok();
let (mode, value): (Mode, u16) = if operand.is_empty() {
(Mode::Implied, 0)
} else if operand == "A" {
(Mode::Accumulator, 0)
} else if let Some(v) = operand.strip_prefix("#$") {
(Mode::Immediate, hex(v)?)
} else if let Some(v) = operand
.strip_prefix("($")
.and_then(|v| v.strip_suffix(",X)"))
{
(Mode::IndirectX, hex(v)?)
} else if let Some(v) = operand
.strip_prefix("($")
.and_then(|v| v.strip_suffix("),Y"))
{
(Mode::IndirectY, hex(v)?)
} else if let Some(v) = operand.strip_prefix("($").and_then(|v| v.strip_suffix(')')) {
(Mode::Indirect, hex(v)?)
} else if let Some(v) = operand.strip_prefix('$').and_then(|v| v.strip_suffix(",X")) {
let m = if v.len() == 2 {
Mode::ZeroPageX
} else {
Mode::AbsoluteX
};
(m, hex(v)?)
} else if let Some(v) = operand.strip_prefix('$').and_then(|v| v.strip_suffix(",Y")) {
let m = if v.len() == 2 {
Mode::ZeroPageY
} else {
Mode::AbsoluteY
};
(m, hex(v)?)
} else {
let v = operand.strip_prefix('$')?;
let m = if v.len() == 2 {
Mode::ZeroPage
} else {
Mode::Absolute
};
(m, hex(v)?)
};
for opcode in 0..=255u8 {
let insn = decode(opcode);
if insn.op.mnemonic() == mnemonic && insn.mode == mode {
let mut bytes = Vec::new();
match insn.bytes() {
2 => bytes.push(value as u8),
3 => {
bytes.push(value as u8);
bytes.push((value >> 8) as u8);
}
_ => {}
}
return Some((opcode, bytes));
}
}
None
}
#[test]
fn operand_syntax_matches_the_assembler_convention() {
let cases: &[(&[u8], &str)] = &[
(&[0xea], "NOP"),
(&[0x0a], "ASL A"),
(&[0xa9, 0x42], "LDA #$42"),
(&[0xa5, 0x42], "LDA $42"),
(&[0xb5, 0x42], "LDA $42,X"),
(&[0xb6, 0x42], "LDX $42,Y"),
(&[0xad, 0x34, 0x12], "LDA $1234"),
(&[0xbd, 0x34, 0x12], "LDA $1234,X"),
(&[0xb9, 0x34, 0x12], "LDA $1234,Y"),
(&[0x6c, 0x34, 0x12], "JMP ($1234)"),
(&[0xa1, 0x42], "LDA ($42,X)"),
(&[0xb1, 0x42], "LDA ($42),Y"),
(&[0x00, 0x00], "BRK"),
(&[0x03, 0x42], "SLO ($42,X)"),
(&[0xeb, 0x42], "USBC #$42"),
];
for (bytes, want) in cases {
assert_eq!(format!("{}", disassemble(0xc000, bytes)), *want);
}
}
#[test]
fn a_branch_prints_its_target_not_its_displacement() {
assert_eq!(
format!("{}", disassemble(0xc000, &[0xd0, 0x05])),
"BNE $c007"
);
assert_eq!(
format!("{}", disassemble(0xc000, &[0xd0, 0xfe])),
"BNE $c000"
);
assert_eq!(
format!("{}", disassemble(0xc0f0, &[0x10, 0x40])),
"BPL $c132"
);
assert_eq!(
format!("{}", disassemble(0xfffe, &[0xd0, 0x10])),
"BNE $0010"
);
}
#[test]
fn every_opcode_round_trips_through_its_text() {
for opcode in 0..=255u8 {
let insn = decode(opcode);
let bytes = [opcode, 0x34, 0x12];
let d = disassemble(0xc000, &bytes[..insn.bytes() as usize]);
let text: String = format!("{d}");
if insn.mode == Mode::Relative || insn.mode == Mode::Break {
continue;
}
let (back, operand) =
reassemble(&text).unwrap_or_else(|| panic!("{opcode:02x}: cannot parse {text:?}"));
let round = decode(back);
assert_eq!(round.op, insn.op, "{opcode:02x} {text}");
assert_eq!(round.mode, insn.mode, "{opcode:02x} {text}");
assert_eq!(operand, bytes[1..insn.bytes() as usize], "{opcode:02x}");
}
}
#[test]
fn a_short_buffer_decodes_as_truncated_rather_than_panicking() {
let d = disassemble(0xc000, &[0xad, 0x34]);
assert!(d.truncated);
assert_eq!(d.len, 3);
assert_eq!(d.word(), 0x0034);
let empty = disassemble(0xc000, &[]);
assert!(empty.truncated);
assert_eq!(empty.opcode, 0x00);
}
#[test]
fn a_run_walks_instruction_by_instruction() {
let program: [u8; 8] = [0xa9, 0x01, 0x8d, 0x00, 0x02, 0xd0, 0xf9, 0xea];
let run = disassemble_run(0xc000, 4, |a| {
program.get(a.wrapping_sub(0xc000) as usize).copied()
});
let text: Vec<String> = run.iter().map(|d| format!("{d}")).collect();
assert_eq!(text, ["LDA #$01", "STA $0200", "BNE $c000", "NOP"]);
assert_eq!(run[2].branch_target(), Some(0xc000));
assert_eq!(run[1].static_target(), Some(0x0200));
}
#[test]
fn undocumented_encodings_are_flagged() {
assert!(disassemble(0, &[0x03, 0]).is_undocumented());
assert!(!disassemble(0, &[0xa9, 0]).is_undocumented());
}
}