use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt;
use super::cp0;
use super::isa::{self, Fmt, Op, REG_NAMES};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Disassembled {
pub addr: u64,
pub len: u64,
pub encoding: u32,
pub text: String,
pub delay_slot: bool,
}
impl fmt::Display for Disassembled {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:08x}: {:08x} {}", self.addr, self.encoding, self.text)
}
}
fn r(i: u32) -> &'static str {
REG_NAMES[(i & 31) as usize]
}
fn simm(word: u32) -> String {
let v = (word & 0xffff) as u16 as i16;
if v.unsigned_abs() < 10 {
format!("{v}")
} else if v < 0 {
format!("-0x{:x}", v.unsigned_abs())
} else {
format!("0x{v:x}")
}
}
fn cp0_reg(n: u32) -> String {
match cp0::reg::name(n) {
Some(name) => format!("${name}"),
None => format!("$c0_{n}"),
}
}
#[must_use]
pub fn format_word(word: u32, pc: u32) -> String {
if word == 0 {
return "nop".to_string();
}
let Some(insn) = isa::decode(word) else {
return format!(".word 0x{word:08x}");
};
let m = insn.op.mnemonic();
let rs = isa::rs(word);
let rt = isa::rt(word);
let rd = isa::rd(word);
let delay_pc = pc.wrapping_add(4);
match insn.fmt {
Fmt::R => format!("{m} {}, {}, {}", r(rd), r(rs), r(rt)),
Fmt::Shift => format!("{m} {}, {}, {}", r(rd), r(rt), isa::sa(word)),
Fmt::ShiftV => format!("{m} {}, {}, {}", r(rd), r(rt), r(rs)),
Fmt::I => {
match insn.op {
Op::Andi | Op::Ori | Op::Xori => {
format!("{m} {}, {}, 0x{:x}", r(rt), r(rs), isa::imm(word))
}
_ => format!("{m} {}, {}, {}", r(rt), r(rs), simm(word)),
}
}
Fmt::Mem => format!("{m} {}, {}({})", r(rt), simm(word), r(rs)),
Fmt::Lui => format!("{m} {}, 0x{:x}", r(rt), isa::imm(word)),
Fmt::Branch => format!(
"{m} {}, {}, 0x{:08x}",
r(rs),
r(rt),
isa::branch_target(delay_pc, word)
),
Fmt::BranchZ => format!(
"{m} {}, 0x{:08x}",
r(rs),
isa::branch_target(delay_pc, word)
),
Fmt::Jump => format!("{m} 0x{:08x}", isa::jump_target(delay_pc, word)),
Fmt::Rs | Fmt::MoveTo => format!("{m} {}", r(rs)),
Fmt::JumpLink => {
if rd == 31 {
format!("{m} {}", r(rs))
} else {
format!("{m} {}, {}", r(rd), r(rs))
}
}
Fmt::Rd => format!("{m} {}", r(rd)),
Fmt::HiLo => format!("{m} {}, {}", r(rs), r(rt)),
Fmt::Code => {
let code = isa::code(word);
if code == 0 {
m.to_string()
} else {
format!("{m} 0x{code:x}")
}
}
Fmt::Cop0Move => format!("{m} {}, {}", r(rt), cp0_reg(rd)),
Fmt::None => m.to_string(),
Fmt::CopFun => format!("{m} 0x{:07x}", isa::cofun(word)),
Fmt::CopMem => format!("{m} $c{rt}, {}({})", simm(word), r(rs)),
}
}
#[must_use]
pub fn disassemble_one(pc: u32, read: &mut impl FnMut(u64) -> Option<u32>) -> Option<Disassembled> {
let word = read(u64::from(pc))?;
Some(Disassembled {
addr: u64::from(pc),
len: 4,
encoding: word,
text: format_word(word, pc),
delay_slot: isa::decode(word).is_some_and(|i| i.is_branch()),
})
}
#[must_use]
pub fn disassemble_run(
pc: u32,
count: usize,
mut read: impl FnMut(u64) -> Option<u32>,
) -> Vec<Disassembled> {
let mut out = Vec::with_capacity(count);
let mut at = pc;
for _ in 0..count {
let Some(one) = disassemble_one(at, &mut read) else {
break;
};
at = at.wrapping_add(4);
out.push(one);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_zero_word_is_a_nop() {
assert_eq!(format_word(0, 0x1000), "nop");
}
#[test]
fn the_common_forms_read_the_way_an_assembler_writes_them() {
assert_eq!(
format_word(0x0085_1021, 0),
"addu v0, a0, a1",
"{:08x}",
0x0085_1021u32
);
assert_eq!(format_word(0x27bd_ffe0, 0), "addiu sp, sp, -0x20");
assert_eq!(format_word(0x8fbf_001c, 0), "lw ra, 0x1c(sp)");
assert_eq!(format_word(0xafb0_0010, 0), "sw s0, 0x10(sp)");
assert_eq!(format_word(0x3c01_8000, 0), "lui at, 0x8000");
assert_eq!(format_word(0x3404_1234, 0), "ori a0, zero, 0x1234");
assert_eq!(format_word(0x0009_40c0, 0), "sll t0, t1, 3");
}
#[test]
fn a_branch_target_is_relative_to_the_delay_slot() {
assert_eq!(
format_word(0x1000_ffff, 0x1000),
"beq zero, zero, 0x00001000"
);
assert_eq!(
format_word(0x1000_0002, 0x2000),
"beq zero, zero, 0x0000200c"
);
}
#[test]
fn a_jump_target_takes_its_high_bits_from_the_delay_slot() {
assert_eq!(
format_word(0x0800_0100, 0x0fff_fffc),
"j 0x10000400",
"the region comes from the delay slot"
);
}
#[test]
fn a_branch_marks_the_line_after_it_as_a_delay_slot() {
let program = [0x1000_ffffu32, 0x0000_0000, 0x2402_0001];
let out = disassemble_run(0x1000, 3, |addr| {
let i = ((addr - 0x1000) / 4) as usize;
program.get(i).copied()
});
assert_eq!(out.len(), 3);
assert!(out[0].delay_slot, "beq has a delay slot");
assert!(!out[1].delay_slot, "nop does not");
assert!(!out[2].delay_slot);
assert_eq!(out[1].addr, 0x1004);
}
#[test]
fn cop0_registers_print_by_name() {
let word = 0x4000_0000 | (26 << 16) | (12 << 11);
assert_eq!(format_word(word, 0), "mfc0 k0, $sr");
let word = 0x4080_0000 | (26 << 16) | (20 << 11);
assert_eq!(format_word(word, 0), "mtc0 k0, $c0_20");
}
#[test]
fn the_processor_control_instructions_print_bare() {
assert_eq!(format_word(0x4200_0010, 0), "rfe");
assert_eq!(format_word(0x4200_0002, 0), "tlbwi");
assert_eq!(format_word(0x0000_000c, 0), "syscall");
assert_eq!(format_word(0x0004_100d, 0), "break 0x1040");
}
#[test]
fn an_unknown_encoding_prints_as_a_word_rather_than_a_guess() {
assert_eq!(format_word(0x7c00_0000, 0), ".word 0x7c000000");
}
#[test]
fn every_table_entry_disassembles_to_something_starting_with_its_mnemonic() {
for insn in isa::TABLE {
let text = format_word(insn.bits, 0x1000);
if insn.bits == 0 {
assert_eq!(text, "nop");
continue;
}
assert!(
text.starts_with(insn.op.mnemonic()),
"{} printed as `{text}`",
insn.op.mnemonic()
);
}
}
}