016. Opcode table refactor
Replace CPU.step branches with an opcode table.
Lesson 16 of 356 · tests/chapter_01_cpu/test_016_opcode_table_refactor.py
File to create
emulator/cpu/opcodes.pyFile to update
emulator/cpu/cpu.pyLocations
opcodes.lda_immediate
opcodes.lda_zero_page
opcodes.lda_absolute
opcodes.OPCODE_TABLE
CPU.stepWhy this step exists
Direct branches work for three opcodes but scale poorly across the 6502 instruction set. A table makes byte-to-handler dispatch data-driven while retaining the existing addressing and instruction boundaries.
Complete example implementation
# emulator/cpu/opcodes.py
from emulator.cpu.addressing_modes import absolute, immediate, zero_page
from emulator.cpu.instructions import lda
def lda_immediate(cpu) -> None:
lda(cpu, immediate(cpu))
def lda_zero_page(cpu) -> None:
address = zero_page(cpu)
lda(cpu, cpu.bus.read(address))
def lda_absolute(cpu) -> None:
address = absolute(cpu)
lda(cpu, cpu.bus.read(address))
OPCODE_TABLE = {
0xA9: lda_immediate,
0xA5: lda_zero_page,
0xAD: lda_absolute,
}
# emulator/cpu/cpu.py
from emulator.cpu.opcodes import OPCODE_TABLE
class CPU:
def step(self) -> None:
opcode = self.fetch_byte()
handler = OPCODE_TABLE.get(opcode)
if handler is None:
raise NotImplementedError(
f"Opcode {opcode:02X} not implemented"
)
return handler(self)Important boundary
CPU.step dispatches; an opcode handler coordinates addressing, memory access, and the instruction; lda performs the register-and-flags state transition.
Common misconception
An opcode handler is not the same as an instruction. Three LDA opcodes share one lda instruction but use different operand acquisition mechanisms.
Out of scope
- indexed LDA handlers
- cycle-count return values
- a generic instruction decoder object
Run this lesson
uv run pytest tests/chapter_01_cpu/test_016_opcode_table_refactor.py -v