modemwars/tools/m6502.py
2026-08-23 02:09:40 -05:00

166 lines
6.5 KiB
Python

#!/usr/bin/env python3
# m6502.py - minimal 6502 opcode table, instruction decoder and recursive-descent code tracer.
from dataclasses import dataclass
# addressing modes
IMP, ACC, IMM, ZP, ZPX, ZPY, ABS, ABX, ABY, IND, IZX, IZY, REL = range(13)
MODE_LEN = {IMP:1, ACC:1, IMM:2, ZP:2, ZPX:2, ZPY:2, ABS:3, ABX:3, ABY:3, IND:3, IZX:2, IZY:2, REL:2}
OPCODES = {}
def _def(mnemonic, table):
for opcode, mode in table.items():
OPCODES[opcode] = (mnemonic, mode)
_def("adc", {0x69:IMM,0x65:ZP,0x75:ZPX,0x6D:ABS,0x7D:ABX,0x79:ABY,0x61:IZX,0x71:IZY})
_def("and", {0x29:IMM,0x25:ZP,0x35:ZPX,0x2D:ABS,0x3D:ABX,0x39:ABY,0x21:IZX,0x31:IZY})
_def("asl", {0x0A:ACC,0x06:ZP,0x16:ZPX,0x0E:ABS,0x1E:ABX})
_def("bcc", {0x90:REL}); _def("bcs", {0xB0:REL}); _def("beq", {0xF0:REL}); _def("bmi", {0x30:REL})
_def("bne", {0xD0:REL}); _def("bpl", {0x10:REL}); _def("bvc", {0x50:REL}); _def("bvs", {0x70:REL})
_def("bit", {0x24:ZP,0x2C:ABS})
_def("brk", {0x00:IMP})
_def("clc", {0x18:IMP}); _def("cld", {0xD8:IMP}); _def("cli", {0x58:IMP}); _def("clv", {0xB8:IMP})
_def("cmp", {0xC9:IMM,0xC5:ZP,0xD5:ZPX,0xCD:ABS,0xDD:ABX,0xD9:ABY,0xC1:IZX,0xD1:IZY})
_def("cpx", {0xE0:IMM,0xE4:ZP,0xEC:ABS}); _def("cpy", {0xC0:IMM,0xC4:ZP,0xCC:ABS})
_def("dec", {0xC6:ZP,0xD6:ZPX,0xCE:ABS,0xDE:ABX}); _def("dex", {0xCA:IMP}); _def("dey", {0x88:IMP})
_def("eor", {0x49:IMM,0x45:ZP,0x55:ZPX,0x4D:ABS,0x5D:ABX,0x59:ABY,0x41:IZX,0x51:IZY})
_def("inc", {0xE6:ZP,0xF6:ZPX,0xEE:ABS,0xFE:ABX}); _def("inx", {0xE8:IMP}); _def("iny", {0xC8:IMP})
_def("jmp", {0x4C:ABS,0x6C:IND}); _def("jsr", {0x20:ABS})
_def("lda", {0xA9:IMM,0xA5:ZP,0xB5:ZPX,0xAD:ABS,0xBD:ABX,0xB9:ABY,0xA1:IZX,0xB1:IZY})
_def("ldx", {0xA2:IMM,0xA6:ZP,0xB6:ZPY,0xAE:ABS,0xBE:ABY})
_def("ldy", {0xA0:IMM,0xA4:ZP,0xB4:ZPX,0xAC:ABS,0xBC:ABX})
_def("lsr", {0x4A:ACC,0x46:ZP,0x56:ZPX,0x4E:ABS,0x5E:ABX})
_def("nop", {0xEA:IMP})
_def("ora", {0x09:IMM,0x05:ZP,0x15:ZPX,0x0D:ABS,0x1D:ABX,0x19:ABY,0x01:IZX,0x11:IZY})
_def("pha", {0x48:IMP}); _def("php", {0x08:IMP}); _def("pla", {0x68:IMP}); _def("plp", {0x28:IMP})
_def("rol", {0x2A:ACC,0x26:ZP,0x36:ZPX,0x2E:ABS,0x3E:ABX})
_def("ror", {0x6A:ACC,0x66:ZP,0x76:ZPX,0x6E:ABS,0x7E:ABX})
_def("rti", {0x40:IMP}); _def("rts", {0x60:IMP})
_def("sbc", {0xE9:IMM,0xE5:ZP,0xF5:ZPX,0xED:ABS,0xFD:ABX,0xF9:ABY,0xE1:IZX,0xF1:IZY})
_def("sec", {0x38:IMP}); _def("sed", {0xF8:IMP}); _def("sei", {0x78:IMP})
_def("sta", {0x85:ZP,0x95:ZPX,0x8D:ABS,0x9D:ABX,0x99:ABY,0x81:IZX,0x91:IZY})
_def("stx", {0x86:ZP,0x96:ZPY,0x8E:ABS}); _def("sty", {0x84:ZP,0x94:ZPX,0x8C:ABS})
_def("tax", {0xAA:IMP}); _def("tay", {0xA8:IMP}); _def("tsx", {0xBA:IMP})
_def("txa", {0x8A:IMP}); _def("txs", {0x9A:IMP}); _def("tya", {0x98:IMP})
BRANCHES = {"bcc","bcs","beq","bmi","bne","bpl","bvc","bvs"}
FLOW_END = {"rts","rti","brk"}
@dataclass
class InsnT:
addr: int
opcode: int
mnemonic: str
mode: int
length: int
operand: int # raw operand value (or 0)
target: int # effective address for ABS/ZP/REL/etc (None for IMP/ACC/IMM)
def isLegal(self):
return self.mnemonic != "???"
def decode(mem, addr):
opcode = mem[addr]
if opcode not in OPCODES:
return InsnT(addr, opcode, "???", IMP, 1, 0, None)
mnemonic, mode = OPCODES[opcode]
length = MODE_LEN[mode]
operand = 0
target = None
if length == 2:
operand = mem[(addr+1) & 0xFFFF]
elif length == 3:
operand = mem[(addr+1) & 0xFFFF] | (mem[(addr+2) & 0xFFFF] << 8)
if mode == REL:
target = (addr + 2 + ((operand ^ 0x80) - 0x80)) & 0xFFFF
elif mode in (ZP, ZPX, ZPY, ABS, ABX, ABY, IND, IZX, IZY):
target = operand
return InsnT(addr, opcode, mnemonic, mode, length, operand, target)
def formatOperand(insn, labelFor=None):
m = insn.mode
def lab(a, width):
if labelFor:
l = labelFor(a)
if l:
return l
return f"${a:0{width}X}"
if m in (IMP,):
return ""
if m == ACC:
return "a"
if m == IMM:
return f"#${insn.operand:02X}"
if m == ZP:
return lab(insn.operand, 2)
if m == ZPX:
return lab(insn.operand, 2) + ",x"
if m == ZPY:
return lab(insn.operand, 2) + ",y"
if m == ABS:
return lab(insn.operand, 4)
if m == ABX:
return lab(insn.operand, 4) + ",x"
if m == ABY:
return lab(insn.operand, 4) + ",y"
if m == IND:
return "(" + lab(insn.operand, 4) + ")"
if m == IZX:
return "(" + lab(insn.operand, 2) + ",x)"
if m == IZY:
return "(" + lab(insn.operand, 2) + "),y"
if m == REL:
return lab(insn.target, 4)
return "?"
def trace(mem, entries, isValid=lambda a: True, noReturn=frozenset(), inlineArgs=None, known=frozenset()):
"""Recursive-descent: returns (codeSet, callTargets, jumpTargets, indirectJumps, illegalHits).
inlineArgs: dict subroutineAddr -> number of inline bytes that follow a JSR to it."""
code = set()
calls = {}
jumps = {}
indirects = []
illegal = []
work = list(entries)
seen = set(entries)
while work:
pc = work.pop()
while True:
if pc in code or pc in known or not isValid(pc):
break
insn = decode(mem, pc)
if not insn.isLegal():
illegal.append(pc)
break
for i in range(insn.length):
code.add((pc + i) & 0xFFFF)
nxt = (pc + insn.length) & 0xFFFF
if insn.mnemonic in BRANCHES:
if insn.target not in seen:
seen.add(insn.target); work.append(insn.target)
pc = nxt
elif insn.mnemonic == "jsr":
calls.setdefault(insn.target, []).append(pc)
if insn.target not in seen:
seen.add(insn.target); work.append(insn.target)
if inlineArgs and insn.target in inlineArgs:
nxt = (nxt + inlineArgs[insn.target]) & 0xFFFF
if insn.target in noReturn:
break
pc = nxt
elif insn.mnemonic == "jmp":
if insn.mode == IND:
indirects.append(pc)
break
jumps.setdefault(insn.target, []).append(pc)
if insn.target not in seen:
seen.add(insn.target); work.append(insn.target)
break
elif insn.mnemonic in FLOW_END:
break
else:
pc = nxt
return code, calls, jumps, indirects, illegal