#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DisasmWindowConfig {
pub before: usize,
pub after: usize,
pub top_margin: usize,
pub bottom_margin: usize,
}
impl Default for DisasmWindowConfig {
fn default() -> Self {
Self {
before: 10,
after: 9,
top_margin: 3,
bottom_margin: 3,
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct DisasmWindowState {
pub(crate) start: Option<u16>,
}
impl DisasmWindowState {
pub fn new() -> Self {
Self::default()
}
pub fn reset(&mut self) {
self.start = None;
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DisasmLine {
pub addr: u16,
pub bytes: Vec<u8>,
pub text: String,
pub is_current: bool,
}
impl DisasmLine {
pub fn new(addr: u16, bytes: Vec<u8>, text: String, is_current: bool) -> Self {
Self {
addr,
bytes,
text,
is_current,
}
}
}
pub trait InstructionDecoder {
fn instruction_length(opcode: u8) -> usize;
fn disassemble_one<F: Fn(u16) -> u8>(read: &F, addr: u16) -> (usize, String);
}
pub fn disassemble_window<F, P, D>(
read: F,
pc: u16,
config: DisasmWindowConfig,
prev_start: P,
disasm_one: D,
) -> Vec<DisasmLine>
where
F: Fn(u16) -> u8,
P: Fn(&dyn Fn(u16) -> u8, u16) -> Option<u16>,
D: Fn(&dyn Fn(u16) -> u8, u16, u16) -> DisasmLine,
{
let mut start = pc;
for _ in 0..config.before {
let Some(prev) = prev_start(&read, start) else {
break;
};
if prev > start {
break;
}
start = prev;
}
let target_lines = config.before + 1 + config.after;
disassemble_from_start(&read, start, pc, target_lines, &disasm_one)
}
pub fn disassemble_window_with_state<F, P, D>(
read: F,
pc: u16,
state: &mut DisasmWindowState,
config: DisasmWindowConfig,
prev_start: P,
disasm_one: D,
) -> Vec<DisasmLine>
where
F: Fn(u16) -> u8,
P: Fn(&dyn Fn(u16) -> u8, u16) -> Option<u16>,
D: Fn(&dyn Fn(u16) -> u8, u16, u16) -> DisasmLine,
{
let target_lines = config.before + 1 + config.after;
let mut lines = if let Some(start) = state.start {
disassemble_from_start(&read, start, pc, target_lines, &disasm_one)
} else {
disassemble_window(&read, pc, config, &prev_start, &disasm_one)
};
let current_index = lines.iter().position(|l| l.is_current);
if let Some(idx) = current_index {
let last_two_start = lines.len().saturating_sub(2);
if idx >= last_two_start {
lines = disassemble_window(&read, pc, config, &prev_start, &disasm_one);
state.start = lines.first().map(|l| l.addr);
return lines;
}
state.start = lines.first().map(|l| l.addr);
return lines;
}
lines = disassemble_window(&read, pc, config, &prev_start, &disasm_one);
state.start = lines.first().map(|l| l.addr);
lines
}
fn disassemble_from_start<F, D>(
read: &F,
start: u16,
pc: u16,
target_lines: usize,
disasm_one: &D,
) -> Vec<DisasmLine>
where
F: Fn(u16) -> u8,
D: Fn(&dyn Fn(u16) -> u8, u16, u16) -> DisasmLine,
{
let mut lines = Vec::with_capacity(target_lines);
let mut addr = start;
for _ in 0..target_lines {
let line = disasm_one(read, addr, pc);
let step = (line.bytes.len() as u16).max(1);
addr = addr.wrapping_add(step);
lines.push(line);
if addr == 0 {
break;
}
}
lines
}
pub fn format_disasm_bytes(bytes: &[u8]) -> String {
match bytes.len() {
0 => String::from(" "),
1 => format!("{:02X} ", bytes[0]),
2 => format!("{:02X} {:02X} ", bytes[0], bytes[1]),
_ => format!("{:02X} {:02X} {:02X} ", bytes[0], bytes[1], bytes[2]),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_disasm_bytes_one_byte() {
let bytes = vec![0x3E];
assert_eq!(format_disasm_bytes(&bytes), "3E ");
}
#[test]
fn test_format_disasm_bytes_two_bytes() {
let bytes = vec![0x3E, 0x50];
assert_eq!(format_disasm_bytes(&bytes), "3E 50 ");
}
#[test]
fn test_format_disasm_bytes_three_bytes() {
let bytes = vec![0xC3, 0x00, 0x01];
assert_eq!(format_disasm_bytes(&bytes), "C3 00 01 ");
}
#[test]
fn test_disasm_window_state_reset() {
let mut state = DisasmWindowState {
start: Some(0x8000),
};
state.reset();
assert_eq!(state.start, None);
}
fn mock_disasm_one(_read: &dyn Fn(u16) -> u8, addr: u16, pc: u16) -> DisasmLine {
DisasmLine {
addr,
bytes: vec![0xEA], text: "NOP".to_string(),
is_current: addr == pc,
}
}
fn mock_prev_start(_read: &dyn Fn(u16) -> u8, pc: u16) -> Option<u16> {
if pc == 0 { None } else { Some(pc - 1) }
}
#[test]
fn test_disassemble_window_centers_on_pc() {
let read = |_addr: u16| 0xEA;
let config = DisasmWindowConfig {
before: 4,
after: 4,
top_margin: 3,
bottom_margin: 3,
};
let lines = disassemble_window(read, 0xC004, config, mock_prev_start, mock_disasm_one);
assert_eq!(lines.len(), 9);
let current_idx = lines.iter().position(|l| l.is_current).unwrap();
assert_eq!(current_idx, 4); assert_eq!(lines[current_idx].addr, 0xC004);
}
#[test]
fn test_disassemble_window_with_state_recenters_when_pc_at_bottom() {
let read = |_addr: u16| 0xEA;
let config = DisasmWindowConfig {
before: 4,
after: 4,
top_margin: 3,
bottom_margin: 3,
};
let mut state = DisasmWindowState::default();
let lines0 = disassemble_window_with_state(
read,
0xC004,
&mut state,
config,
mock_prev_start,
mock_disasm_one,
);
let idx0 = lines0.iter().position(|l| l.is_current).unwrap();
assert_eq!(idx0, config.before);
let lines1 = disassemble_window_with_state(
read,
0xC00B,
&mut state,
config,
mock_prev_start,
mock_disasm_one,
);
let idx1 = lines1.iter().position(|l| l.is_current).unwrap();
assert_eq!(idx1, config.before); }
}