Checkpoint #2: Match nestest

Last updated:

The 6502 has a lot of opcodes, and writing them all before running anything is a long stretch with no feedback. nestest removes that. You make your emulator print its state in the same format, run it alongside the log, and stop at the first line that doesn't match — that line points straight at the bug.

So you build the CPU incrementally: implement an opcode, see how much further you get before the next mismatch, implement the one it tripped on, repeat. You're never debugging more than one instruction at a time.

The trap with a CPU is reading the whole instruction set before running anything. Don't. The point of nestest is that the failing log is the to-do list: it tells you which opcode to write next, one at a time. So we build the smallest CPU that runs, point it at nestest, and let it tell us what's missing.

The loop itself is small. Every opcode is unimplemented to start with:

loop {
    log_upcoming_instruction_and_state()
    opcode = read(PC); PC++
    switch (opcode) {
        // nothing here yet
        default: panic("unimplemented opcode {opcode:02X} at {PC-1:04X}")
    }
}

First rung: match line 1, with zero opcodes

nestest runs in automated mode: you force PC = $C000 at startup instead of using the reset vector, and the rest of the state is the standard reset — A=X=Y=00, P=24, SP=FD, and the cycle counter already at 7 (the reset sequence burns seven cycles). So the first line of the log is:

C000  4C F5 C5  JMP $C5F5                       A:00 X:00 Y:00 P:24 SP:FD CYC:7

(The 4C F5 C5 bytes and the JMP $C5F5 disassembly are a separate concern: printing them means knowing each instruction's length, which only falls out once you decode opcodes. Add those columns as you go, or leave them off the diff at first — they're for humans, not for matching.)

The development loop

Run it against nestest.log and diff. With the skeleton, line 1's state matches, then the CPU fetches 4C and panics: unimplemented opcode 4C. That's your instruction:

  1. Look up 4C in the instruction reference — it's JMP absolute. Implement just that one.
  2. Rerun. The CPU now executes the jump, logs line 2, and panics on its opcode.
  3. Repeat. Implement the opcode it tripped on, see how much further you get.

You're never holding more than one instruction in your head. When the log stops mismatching, you've matched nestest — and the CPU is the part you can now trust.