Skip to main content

rustynes_cpu/
disasm.rs

1//! 6502 disassembler — used by the debugger UI.
2//!
3//! Side-effect-free. Takes a `peek` closure that samples bytes on the CPU
4//! bus *without* advancing time. The output rows are intended for a
5//! scrollable listing; the addressing-mode decode is canonical for the
6//! 151 documented opcodes plus the handful of unofficial opcodes that
7//! ship games actually use. Unknown opcodes render as `.byte $XX`.
8//!
9//! This file is deliberately ~200 LOC: a single static table covers
10//! the entire 256-entry opcode space, and one match dispatches by
11//! addressing mode. There's no allocation per-instruction.
12
13#![allow(clippy::cast_lossless)]
14#![allow(clippy::cast_possible_truncation)]
15#![allow(clippy::cast_possible_wrap)]
16#![allow(clippy::cast_sign_loss)]
17#![allow(clippy::enum_glob_use)]
18#![allow(clippy::items_after_statements)]
19#![allow(clippy::too_many_lines)]
20
21use alloc::format;
22use alloc::{string::String, vec::Vec};
23
24/// One decoded instruction.
25#[derive(Debug, Clone)]
26pub struct DisasmLine {
27    /// PC at which the instruction starts.
28    pub addr: u16,
29    /// Raw opcode bytes (1-3 bytes).
30    pub bytes: Vec<u8>,
31    /// Mnemonic (`"LDA"`, `"BRK"`, ...).
32    pub mnemonic: &'static str,
33    /// Formatted operand, e.g. `"$1234,X"`, `"#$42"`, `""`.
34    pub operand: String,
35}
36
37/// 6502 addressing modes.
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39enum AddrMode {
40    /// Implied / no operand.
41    Implied,
42    /// Accumulator (e.g. `ASL A`).
43    Accumulator,
44    /// `#$nn`.
45    Immediate,
46    /// `$nn` zero page.
47    ZeroPage,
48    /// `$nn,X` zero page indexed.
49    ZeroPageX,
50    /// `$nn,Y` zero page indexed.
51    ZeroPageY,
52    /// `$nnnn` absolute.
53    Absolute,
54    /// `$nnnn,X` absolute indexed.
55    AbsoluteX,
56    /// `$nnnn,Y` absolute indexed.
57    AbsoluteY,
58    /// `($nnnn)` indirect (`JMP` only).
59    Indirect,
60    /// `($nn,X)` indexed indirect.
61    IndirectX,
62    /// `($nn),Y` indirect indexed.
63    IndirectY,
64    /// `$nn` relative branch target.
65    Relative,
66}
67
68const fn op_len(mode: AddrMode) -> u16 {
69    match mode {
70        AddrMode::Implied | AddrMode::Accumulator => 1,
71        AddrMode::Immediate
72        | AddrMode::ZeroPage
73        | AddrMode::ZeroPageX
74        | AddrMode::ZeroPageY
75        | AddrMode::IndirectX
76        | AddrMode::IndirectY
77        | AddrMode::Relative => 2,
78        AddrMode::Absolute | AddrMode::AbsoluteX | AddrMode::AbsoluteY | AddrMode::Indirect => 3,
79    }
80}
81
82/// Static (mnemonic, mode) table for all 256 opcodes.
83///
84/// Coverage: all 151 documented + the most common unofficial opcodes
85/// games rely on; everything else renders as `???` with `.byte`.
86static OPCODE_TABLE: [(&str, AddrMode); 256] = build_opcode_table();
87
88const fn build_opcode_table() -> [(&'static str, AddrMode); 256] {
89    let mut t = [("???", AddrMode::Implied); 256];
90    // Macro-free, all-const init — verbose but cheap.
91    use AddrMode::*;
92    macro_rules! set {
93        ($op:expr_2021, $m:expr_2021, $mode:expr_2021) => {
94            t[$op as usize] = ($m, $mode);
95        };
96    }
97    // Loads / stores / transfers.
98    set!(0xA9, "LDA", Immediate);
99    set!(0xA5, "LDA", ZeroPage);
100    set!(0xB5, "LDA", ZeroPageX);
101    set!(0xAD, "LDA", Absolute);
102    set!(0xBD, "LDA", AbsoluteX);
103    set!(0xB9, "LDA", AbsoluteY);
104    set!(0xA1, "LDA", IndirectX);
105    set!(0xB1, "LDA", IndirectY);
106    set!(0xA2, "LDX", Immediate);
107    set!(0xA6, "LDX", ZeroPage);
108    set!(0xB6, "LDX", ZeroPageY);
109    set!(0xAE, "LDX", Absolute);
110    set!(0xBE, "LDX", AbsoluteY);
111    set!(0xA0, "LDY", Immediate);
112    set!(0xA4, "LDY", ZeroPage);
113    set!(0xB4, "LDY", ZeroPageX);
114    set!(0xAC, "LDY", Absolute);
115    set!(0xBC, "LDY", AbsoluteX);
116    set!(0x85, "STA", ZeroPage);
117    set!(0x95, "STA", ZeroPageX);
118    set!(0x8D, "STA", Absolute);
119    set!(0x9D, "STA", AbsoluteX);
120    set!(0x99, "STA", AbsoluteY);
121    set!(0x81, "STA", IndirectX);
122    set!(0x91, "STA", IndirectY);
123    set!(0x86, "STX", ZeroPage);
124    set!(0x96, "STX", ZeroPageY);
125    set!(0x8E, "STX", Absolute);
126    set!(0x84, "STY", ZeroPage);
127    set!(0x94, "STY", ZeroPageX);
128    set!(0x8C, "STY", Absolute);
129    set!(0xAA, "TAX", Implied);
130    set!(0xA8, "TAY", Implied);
131    set!(0xBA, "TSX", Implied);
132    set!(0x8A, "TXA", Implied);
133    set!(0x9A, "TXS", Implied);
134    set!(0x98, "TYA", Implied);
135    // Stack.
136    set!(0x48, "PHA", Implied);
137    set!(0x08, "PHP", Implied);
138    set!(0x68, "PLA", Implied);
139    set!(0x28, "PLP", Implied);
140    // Logical.
141    set!(0x29, "AND", Immediate);
142    set!(0x25, "AND", ZeroPage);
143    set!(0x35, "AND", ZeroPageX);
144    set!(0x2D, "AND", Absolute);
145    set!(0x3D, "AND", AbsoluteX);
146    set!(0x39, "AND", AbsoluteY);
147    set!(0x21, "AND", IndirectX);
148    set!(0x31, "AND", IndirectY);
149    set!(0x49, "EOR", Immediate);
150    set!(0x45, "EOR", ZeroPage);
151    set!(0x55, "EOR", ZeroPageX);
152    set!(0x4D, "EOR", Absolute);
153    set!(0x5D, "EOR", AbsoluteX);
154    set!(0x59, "EOR", AbsoluteY);
155    set!(0x41, "EOR", IndirectX);
156    set!(0x51, "EOR", IndirectY);
157    set!(0x09, "ORA", Immediate);
158    set!(0x05, "ORA", ZeroPage);
159    set!(0x15, "ORA", ZeroPageX);
160    set!(0x0D, "ORA", Absolute);
161    set!(0x1D, "ORA", AbsoluteX);
162    set!(0x19, "ORA", AbsoluteY);
163    set!(0x01, "ORA", IndirectX);
164    set!(0x11, "ORA", IndirectY);
165    set!(0x24, "BIT", ZeroPage);
166    set!(0x2C, "BIT", Absolute);
167    // Arithmetic.
168    set!(0x69, "ADC", Immediate);
169    set!(0x65, "ADC", ZeroPage);
170    set!(0x75, "ADC", ZeroPageX);
171    set!(0x6D, "ADC", Absolute);
172    set!(0x7D, "ADC", AbsoluteX);
173    set!(0x79, "ADC", AbsoluteY);
174    set!(0x61, "ADC", IndirectX);
175    set!(0x71, "ADC", IndirectY);
176    set!(0xE9, "SBC", Immediate);
177    set!(0xE5, "SBC", ZeroPage);
178    set!(0xF5, "SBC", ZeroPageX);
179    set!(0xED, "SBC", Absolute);
180    set!(0xFD, "SBC", AbsoluteX);
181    set!(0xF9, "SBC", AbsoluteY);
182    set!(0xE1, "SBC", IndirectX);
183    set!(0xF1, "SBC", IndirectY);
184    set!(0xC9, "CMP", Immediate);
185    set!(0xC5, "CMP", ZeroPage);
186    set!(0xD5, "CMP", ZeroPageX);
187    set!(0xCD, "CMP", Absolute);
188    set!(0xDD, "CMP", AbsoluteX);
189    set!(0xD9, "CMP", AbsoluteY);
190    set!(0xC1, "CMP", IndirectX);
191    set!(0xD1, "CMP", IndirectY);
192    set!(0xE0, "CPX", Immediate);
193    set!(0xE4, "CPX", ZeroPage);
194    set!(0xEC, "CPX", Absolute);
195    set!(0xC0, "CPY", Immediate);
196    set!(0xC4, "CPY", ZeroPage);
197    set!(0xCC, "CPY", Absolute);
198    // Inc / dec.
199    set!(0xE6, "INC", ZeroPage);
200    set!(0xF6, "INC", ZeroPageX);
201    set!(0xEE, "INC", Absolute);
202    set!(0xFE, "INC", AbsoluteX);
203    set!(0xE8, "INX", Implied);
204    set!(0xC8, "INY", Implied);
205    set!(0xC6, "DEC", ZeroPage);
206    set!(0xD6, "DEC", ZeroPageX);
207    set!(0xCE, "DEC", Absolute);
208    set!(0xDE, "DEC", AbsoluteX);
209    set!(0xCA, "DEX", Implied);
210    set!(0x88, "DEY", Implied);
211    // Shifts.
212    set!(0x0A, "ASL", Accumulator);
213    set!(0x06, "ASL", ZeroPage);
214    set!(0x16, "ASL", ZeroPageX);
215    set!(0x0E, "ASL", Absolute);
216    set!(0x1E, "ASL", AbsoluteX);
217    set!(0x4A, "LSR", Accumulator);
218    set!(0x46, "LSR", ZeroPage);
219    set!(0x56, "LSR", ZeroPageX);
220    set!(0x4E, "LSR", Absolute);
221    set!(0x5E, "LSR", AbsoluteX);
222    set!(0x2A, "ROL", Accumulator);
223    set!(0x26, "ROL", ZeroPage);
224    set!(0x36, "ROL", ZeroPageX);
225    set!(0x2E, "ROL", Absolute);
226    set!(0x3E, "ROL", AbsoluteX);
227    set!(0x6A, "ROR", Accumulator);
228    set!(0x66, "ROR", ZeroPage);
229    set!(0x76, "ROR", ZeroPageX);
230    set!(0x6E, "ROR", Absolute);
231    set!(0x7E, "ROR", AbsoluteX);
232    // Jumps / flow.
233    set!(0x4C, "JMP", Absolute);
234    set!(0x6C, "JMP", Indirect);
235    set!(0x20, "JSR", Absolute);
236    set!(0x60, "RTS", Implied);
237    set!(0x40, "RTI", Implied);
238    set!(0x00, "BRK", Implied);
239    // Branches.
240    set!(0x10, "BPL", Relative);
241    set!(0x30, "BMI", Relative);
242    set!(0x50, "BVC", Relative);
243    set!(0x70, "BVS", Relative);
244    set!(0x90, "BCC", Relative);
245    set!(0xB0, "BCS", Relative);
246    set!(0xD0, "BNE", Relative);
247    set!(0xF0, "BEQ", Relative);
248    // Flag ops.
249    set!(0x18, "CLC", Implied);
250    set!(0x38, "SEC", Implied);
251    set!(0x58, "CLI", Implied);
252    set!(0x78, "SEI", Implied);
253    set!(0xB8, "CLV", Implied);
254    set!(0xD8, "CLD", Implied);
255    set!(0xF8, "SED", Implied);
256    set!(0xEA, "NOP", Implied);
257    t
258}
259
260/// Disassemble `count` instructions starting at `pc`. `peek` returns the
261/// byte at any CPU bus address without side effects.
262///
263/// Unknown opcodes are rendered as `.byte $XX` with length 1 so the
264/// listing can keep walking forward.
265pub fn disassemble_at<F: Fn(u16) -> u8>(peek: F, pc: u16, count: usize) -> Vec<DisasmLine> {
266    let mut out = Vec::with_capacity(count);
267    let mut cur = pc;
268    for _ in 0..count {
269        let op = peek(cur);
270        let (mnemonic, mode) = OPCODE_TABLE[op as usize];
271        let len = op_len(mode);
272        let mut bytes = Vec::with_capacity(len as usize);
273        for i in 0..len {
274            bytes.push(peek(cur.wrapping_add(i)));
275        }
276        let operand = if mnemonic == "???" {
277            format!(".byte ${op:02X}")
278        } else {
279            format_operand(mode, cur, &bytes)
280        };
281        out.push(DisasmLine {
282            addr: cur,
283            bytes,
284            mnemonic,
285            operand,
286        });
287        cur = cur.wrapping_add(len);
288    }
289    out
290}
291
292fn format_operand(mode: AddrMode, pc: u16, bytes: &[u8]) -> String {
293    let b1 = bytes.get(1).copied().unwrap_or(0);
294    let b2 = bytes.get(2).copied().unwrap_or(0);
295    let abs16 = u16::from(b1) | (u16::from(b2) << 8);
296    match mode {
297        AddrMode::Implied => String::new(),
298        AddrMode::Accumulator => "A".into(),
299        AddrMode::Immediate => format!("#${b1:02X}"),
300        AddrMode::ZeroPage => format!("${b1:02X}"),
301        AddrMode::ZeroPageX => format!("${b1:02X},X"),
302        AddrMode::ZeroPageY => format!("${b1:02X},Y"),
303        AddrMode::Absolute => format!("${abs16:04X}"),
304        AddrMode::AbsoluteX => format!("${abs16:04X},X"),
305        AddrMode::AbsoluteY => format!("${abs16:04X},Y"),
306        AddrMode::Indirect => format!("(${abs16:04X})"),
307        AddrMode::IndirectX => format!("(${b1:02X},X)"),
308        AddrMode::IndirectY => format!("(${b1:02X}),Y"),
309        AddrMode::Relative => {
310            let delta = b1 as i8;
311            let target = pc.wrapping_add(2).wrapping_add(delta as u16);
312            format!("${target:04X}")
313        }
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    fn peek_from(bytes: &[u8], base: u16) -> impl Fn(u16) -> u8 + '_ {
322        move |addr: u16| {
323            let off = addr.wrapping_sub(base) as usize;
324            bytes.get(off).copied().unwrap_or(0)
325        }
326    }
327
328    #[test]
329    fn disasm_lda_imm() {
330        // A9 42  =>  LDA #$42
331        let prog = [0xA9, 0x42];
332        let lines = disassemble_at(peek_from(&prog, 0xC000), 0xC000, 1);
333        assert_eq!(lines.len(), 1);
334        assert_eq!(lines[0].addr, 0xC000);
335        assert_eq!(lines[0].mnemonic, "LDA");
336        assert_eq!(lines[0].operand, "#$42");
337    }
338
339    #[test]
340    fn disasm_jmp_indirect() {
341        // 6C 34 12  =>  JMP ($1234)
342        let prog = [0x6C, 0x34, 0x12];
343        let lines = disassemble_at(peek_from(&prog, 0xC000), 0xC000, 1);
344        assert_eq!(lines[0].mnemonic, "JMP");
345        assert_eq!(lines[0].operand, "($1234)");
346    }
347
348    #[test]
349    fn disasm_walks_forward_past_unknown() {
350        // 02 (illegal/JAM) then EA NOP — the unknown shouldn't stall the walk.
351        let prog = [0x02, 0xEA];
352        let lines = disassemble_at(peek_from(&prog, 0xC000), 0xC000, 2);
353        assert_eq!(lines.len(), 2);
354        assert_eq!(lines[1].mnemonic, "NOP");
355    }
356
357    #[test]
358    fn disasm_branch_target_is_pc_plus_2_plus_delta() {
359        // 10 FE => BPL $C000 (branch to self after pc+=2)
360        let prog = [0x10, 0xFE];
361        let lines = disassemble_at(peek_from(&prog, 0xC000), 0xC000, 1);
362        assert_eq!(lines[0].mnemonic, "BPL");
363        assert_eq!(lines[0].operand, "$C000");
364    }
365}