#!/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
