Urban Champion (JU): Pass 1 complete (CDL verified), Pass 2 complete - 519 labels renamed to semantic names via pass2.py apply - 10 zero-page semantic aliases added to locals.inc - Branch table at CB70-CBAC restructured per CDL (CB70-CB90=data, CB91-CBAC=code) - E07F-E09D region fixed (misaligned decode at E08D) - BYTE-IDENTICAL at PRG level verified Tools: - cdl_reclass.py: overrides now win over CDL conflicts - nesdis.py: fixed a: prefix parsing for JSR/JMP operands; IND mode emits no a: prefix - verify_pass1.py: all 17 top-level projects pass, no crashes
500 lines
23 KiB
Python
500 lines
23 KiB
Python
#!/usr/bin/env python3
|
|
"""Reclassify a NES disassembly's code/data split using a Mesen Code Data Logger file.
|
|
|
|
Why this exists
|
|
---------------
|
|
A static trace cannot see code reached through computed dispatch, and it cannot tell
|
|
inline string data from an instruction that happens to decode from the same bytes. Both
|
|
failure modes are common (see TODO.md "Gotchas that will bite you"), and both leave the
|
|
`.byte`/instruction split wrong even though the rebuild is still byte-identical.
|
|
|
|
Mesen's Code Data Logger is the oracle: it marks every PRG byte the CPU *fetched as an
|
|
opcode* (bit 0 = Code) and every byte it *read as data* (bit 1 = Data), plus bit 2
|
|
JumpTarget and bit 3 SubEntryPoint. This tool rewrites prg.s so its split matches the CDL
|
|
wherever the CDL has an opinion, and leaves bytes the CDL never observed exactly as they
|
|
were. Only the *representation* changes (instructions <-> .byte/.word); every ROM byte is
|
|
preserved, so `make` must still print BYTE-IDENTICAL afterwards -- run it.
|
|
|
|
Capture a CDL from the Mesen MCP:
|
|
mesen_cdl_file(action='save', memoryType='NesPrgRom', filepath='game.cdl')
|
|
|
|
You can also drive the routines the CDL never reached by setting the PC in the debugger --
|
|
but note that a routine that returns through a popped address (a `jsr` + `pla`/`rts`
|
|
dispatcher, very common) will run away when entered that way, marking garbage as Code.
|
|
Check the newly-marked ranges before trusting such a run; treat a PC-forced capture as
|
|
unverified and discard it if the CPU ends up somewhere nonsensical.
|
|
|
|
Inputs and defaults
|
|
-------------------
|
|
--dir DIR project copilot directory (default: cwd)
|
|
--cdl FILE the CDL file (required)
|
|
--prg FILE PRG image, default <dir>/prg.bin (byte-identity oracle, read only)
|
|
--src FILE assembly, default <dir>/prg.s (or main.s where used)
|
|
--locals FILE zero-page alias file, default <dir>/locals.inc; optional
|
|
--base 0xC000 CPU address the PRG is mapped at
|
|
--label-template 'Label_{addr:04X}'
|
|
name given to a *new* label the tool has to define. Labels already
|
|
defined in the source keep their exact spelling, whatever it is
|
|
(`SubCxxxx`, semantic names, ...).
|
|
--override LO-HI:C|D
|
|
force a range's classification. Use for structure the CDL is silent
|
|
about but that is provable another way; record the justification in
|
|
the project's OVERVIEW.md. Repeatable.
|
|
--apply write the result; without it the tool only reports
|
|
|
|
What it needs from the source format
|
|
------------------------------------
|
|
* per-emission audit comments `; CPU:$AAAA PRG:AAAA BB BB ...` (the addressing key)
|
|
* data emitted as `.byte` / `.word` (`.incbin` blocks are left untouched, since they
|
|
carry no per-byte provenance)
|
|
* the data-block header `; ---- data $AAAA-$BBBB (N bytes) ----` -- if absent, block
|
|
expansion is quietly skipped
|
|
* optionally the routine header `; Routine: <label>` followed by the label, so a new
|
|
label can be emitted with the file's own header template
|
|
|
|
Safety
|
|
------
|
|
Refuses to write if it finds a label it must define but cannot place on an instruction
|
|
boundary, or a code path that would branch to an address it cannot resolve. Branch
|
|
targets it cannot label are emitted as explicit PC-relative offsets (`beq *+$05`), the
|
|
convention already used elsewhere in this repo, because ca65 cannot resolve a bare
|
|
absolute address in a relocatable segment.
|
|
"""
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
|
|
# ------------------------------------------------------------------ opcode table
|
|
# 151 official + 105 undocumented. Third element is True for undocumented ones
|
|
# (ca65 V2.19 cannot encode those, so they are emitted as .byte with the disassembly
|
|
# in the audit comment -- keeping them inside the code region).
|
|
T = {}
|
|
|
|
|
|
def _op(op, mn, mode, undoc=False):
|
|
T[op] = (mn, mode, undoc)
|
|
|
|
|
|
for op, mn, mode in [
|
|
(0x00, 'brk', 'imp'), (0x01, 'ora', 'indx'), (0x05, 'ora', 'zp'), (0x06, 'asl', 'zp'), (0x08, 'php', 'imp'),
|
|
(0x09, 'ora', 'imm'), (0x0A, 'asl', 'acc'), (0x0D, 'ora', 'abs'), (0x0E, 'asl', 'abs'), (0x10, 'bpl', 'rel'),
|
|
(0x11, 'ora', 'indy'), (0x15, 'ora', 'zpx'), (0x16, 'asl', 'zpx'), (0x18, 'clc', 'imp'), (0x19, 'ora', 'absy'),
|
|
(0x1D, 'ora', 'absx'), (0x1E, 'asl', 'absx'), (0x20, 'jsr', 'abs'), (0x21, 'and', 'indx'), (0x24, 'bit', 'zp'),
|
|
(0x25, 'and', 'zp'), (0x26, 'rol', 'zp'), (0x28, 'plp', 'imp'), (0x29, 'and', 'imm'), (0x2A, 'rol', 'acc'),
|
|
(0x2C, 'bit', 'abs'), (0x2D, 'and', 'abs'), (0x2E, 'rol', 'abs'), (0x30, 'bmi', 'rel'), (0x31, 'and', 'indy'),
|
|
(0x35, 'and', 'zpx'), (0x36, 'rol', 'zpx'), (0x38, 'sec', 'imp'), (0x39, 'and', 'absy'), (0x3D, 'and', 'absx'),
|
|
(0x3E, 'rol', 'absx'), (0x40, 'rti', 'imp'), (0x41, 'eor', 'indx'), (0x45, 'eor', 'zp'), (0x46, 'lsr', 'zp'),
|
|
(0x48, 'pha', 'imp'), (0x49, 'eor', 'imm'), (0x4A, 'lsr', 'acc'), (0x4C, 'jmp', 'abs'), (0x4D, 'eor', 'abs'),
|
|
(0x4E, 'lsr', 'abs'), (0x50, 'bvc', 'rel'), (0x51, 'eor', 'indy'), (0x55, 'eor', 'zpx'), (0x56, 'lsr', 'zpx'),
|
|
(0x58, 'cli', 'imp'), (0x59, 'eor', 'absy'), (0x5D, 'eor', 'absx'), (0x5E, 'lsr', 'absx'), (0x60, 'rts', 'imp'),
|
|
(0x61, 'adc', 'indx'), (0x65, 'adc', 'zp'), (0x66, 'ror', 'zp'), (0x68, 'pla', 'imp'), (0x69, 'adc', 'imm'),
|
|
(0x6A, 'ror', 'acc'), (0x6C, 'jmp', 'ind'), (0x6D, 'adc', 'abs'), (0x6E, 'ror', 'abs'), (0x70, 'bvs', 'rel'),
|
|
(0x71, 'adc', 'indy'), (0x75, 'adc', 'zpx'), (0x76, 'ror', 'zpx'), (0x78, 'sei', 'imp'), (0x79, 'adc', 'absy'),
|
|
(0x7D, 'adc', 'absx'), (0x7E, 'ror', 'absx'), (0x81, 'sta', 'indx'), (0x84, 'sty', 'zp'), (0x85, 'sta', 'zp'),
|
|
(0x86, 'stx', 'zp'), (0x88, 'dey', 'imp'), (0x8A, 'txa', 'imp'), (0x8C, 'sty', 'abs'), (0x8D, 'sta', 'abs'),
|
|
(0x8E, 'stx', 'abs'), (0x90, 'bcc', 'rel'), (0x91, 'sta', 'indy'), (0x94, 'sty', 'zpx'), (0x95, 'sta', 'zpx'),
|
|
(0x96, 'stx', 'zpy'), (0x98, 'tya', 'imp'), (0x99, 'sta', 'absy'), (0x9A, 'txs', 'imp'), (0x9D, 'sta', 'absx'),
|
|
(0xA0, 'ldy', 'imm'), (0xA1, 'lda', 'indx'), (0xA2, 'ldx', 'imm'), (0xA4, 'ldy', 'zp'), (0xA5, 'lda', 'zp'),
|
|
(0xA6, 'ldx', 'zp'), (0xA8, 'tay', 'imp'), (0xA9, 'lda', 'imm'), (0xAA, 'tax', 'imp'), (0xAC, 'ldy', 'abs'),
|
|
(0xAD, 'lda', 'abs'), (0xAE, 'ldx', 'abs'), (0xB0, 'bcs', 'rel'), (0xB1, 'lda', 'indy'), (0xB4, 'ldy', 'zpx'),
|
|
(0xB5, 'lda', 'zpx'), (0xB6, 'ldx', 'zpy'), (0xB8, 'clv', 'imp'), (0xB9, 'lda', 'absy'), (0xBA, 'tsx', 'imp'),
|
|
(0xBC, 'ldy', 'absx'), (0xBD, 'lda', 'absx'), (0xBE, 'ldx', 'absy'), (0xC0, 'cpy', 'imm'), (0xC1, 'cmp', 'indx'),
|
|
(0xC4, 'cpy', 'zp'), (0xC5, 'cmp', 'zp'), (0xC6, 'dec', 'zp'), (0xC8, 'iny', 'imp'), (0xC9, 'cmp', 'imm'),
|
|
(0xCA, 'dex', 'imp'), (0xCC, 'cpy', 'abs'), (0xCD, 'cmp', 'abs'), (0xCE, 'dec', 'abs'), (0xD0, 'bne', 'rel'),
|
|
(0xD1, 'cmp', 'indy'), (0xD5, 'cmp', 'zpx'), (0xD6, 'dec', 'zpx'), (0xD8, 'cld', 'imp'), (0xD9, 'cmp', 'absy'),
|
|
(0xDD, 'cmp', 'absx'), (0xDE, 'dec', 'absx'), (0xE0, 'cpx', 'imm'), (0xE1, 'sbc', 'indx'), (0xE4, 'cpx', 'zp'),
|
|
(0xE5, 'sbc', 'zp'), (0xE6, 'inc', 'zp'), (0xE8, 'inx', 'imp'), (0xE9, 'sbc', 'imm'), (0xEA, 'nop', 'imp'),
|
|
(0xEC, 'cpx', 'abs'), (0xED, 'sbc', 'abs'), (0xEE, 'inc', 'abs'), (0xF0, 'beq', 'rel'), (0xF1, 'sbc', 'indy'),
|
|
(0xF5, 'sbc', 'zpx'), (0xF6, 'inc', 'zpx'), (0xF8, 'sed', 'imp'), (0xF9, 'sbc', 'absy'), (0xFD, 'sbc', 'absx'),
|
|
(0xFE, 'inc', 'absx')]:
|
|
_op(op, mn, mode)
|
|
for op, mn, mode in [
|
|
(0x03, 'slo', 'indx'), (0x07, 'slo', 'zp'), (0x0B, 'anc', 'imm'), (0x0C, 'nop', 'abs'), (0x0F, 'slo', 'abs'),
|
|
(0x13, 'slo', 'indy'), (0x17, 'slo', 'zpx'), (0x1A, 'nop', 'imp'), (0x1B, 'slo', 'absy'), (0x1C, 'nop', 'absx'),
|
|
(0x1F, 'slo', 'absx'), (0x23, 'rla', 'indx'), (0x27, 'rla', 'zp'), (0x2B, 'anc', 'imm'), (0x2F, 'rla', 'abs'),
|
|
(0x33, 'rla', 'indy'), (0x37, 'rla', 'zpx'), (0x3A, 'nop', 'imp'), (0x3B, 'rla', 'absy'), (0x3C, 'nop', 'absx'),
|
|
(0x3F, 'rla', 'absx'), (0x43, 'sre', 'indx'), (0x47, 'sre', 'zp'), (0x4B, 'alr', 'imm'), (0x4F, 'sre', 'abs'),
|
|
(0x53, 'sre', 'indy'), (0x57, 'sre', 'zpx'), (0x5A, 'nop', 'imp'), (0x5B, 'sre', 'absy'), (0x5C, 'nop', 'absx'),
|
|
(0x5F, 'sre', 'absx'), (0x63, 'rra', 'indx'), (0x67, 'rra', 'zp'), (0x6B, 'arr', 'imm'), (0x6F, 'rra', 'abs'),
|
|
(0x73, 'rra', 'indy'), (0x77, 'rra', 'zpx'), (0x7A, 'nop', 'imp'), (0x7B, 'rra', 'absy'), (0x7C, 'nop', 'absx'),
|
|
(0x7F, 'rra', 'absx'), (0x80, 'nop', 'imm'), (0x82, 'nop', 'imm'), (0x83, 'sax', 'indx'), (0x87, 'sax', 'zp'),
|
|
(0x89, 'nop', 'imm'), (0x8B, 'xaa', 'imm'), (0x8F, 'sax', 'abs'), (0x93, 'ahx', 'indy'), (0x97, 'sax', 'zpy'),
|
|
(0x9B, 'tas', 'absy'), (0x9C, 'shy', 'absx'), (0x9E, 'shx', 'absy'), (0x9F, 'ahx', 'absy'), (0xA3, 'lax', 'indx'),
|
|
(0xA7, 'lax', 'zp'), (0xAB, 'lax', 'imm'), (0xAF, 'lax', 'abs'), (0xB3, 'lax', 'indy'), (0xB7, 'lax', 'zpy'),
|
|
(0xBB, 'las', 'absy'), (0xBF, 'lax', 'absy'), (0xC2, 'nop', 'imm'), (0xC3, 'dcp', 'indx'), (0xC7, 'dcp', 'zp'),
|
|
(0xCB, 'axs', 'imm'), (0xCF, 'dcp', 'abs'), (0xD3, 'dcp', 'indy'), (0xD7, 'dcp', 'zpx'), (0xDA, 'nop', 'imp'),
|
|
(0xDB, 'dcp', 'absy'), (0xDC, 'nop', 'absx'), (0xDF, 'dcp', 'absx'), (0xE2, 'nop', 'imm'), (0xE3, 'isc', 'indx'),
|
|
(0xE7, 'isc', 'zp'), (0xEB, 'sbc', 'imm'), (0xEF, 'isc', 'abs'), (0xF3, 'isc', 'indy'), (0xF7, 'isc', 'zpx'),
|
|
(0xFA, 'nop', 'imp'), (0xFB, 'isc', 'absy'), (0xFC, 'nop', 'absx'), (0xFF, 'isc', 'absx')]:
|
|
_op(op, mn, mode, True)
|
|
for op in (0x02, 0x12, 0x22, 0x32, 0x42, 0x52, 0x62, 0x72, 0x92, 0xB2, 0xD2, 0xF2):
|
|
_op(op, 'nop', 'imm', True)
|
|
for op in (0x04, 0x44, 0x64):
|
|
_op(op, 'nop', 'zp', True)
|
|
for op in (0x14, 0x34, 0x54, 0x74, 0xD4, 0xF4):
|
|
_op(op, 'nop', 'zpx', True)
|
|
|
|
assert len(T) == 256 and sum(1 for v in T.values() if not v[2]) == 151
|
|
|
|
SIZE = {'imp': 1, 'acc': 1, 'imm': 2, 'zp': 2, 'zpx': 2, 'zpy': 2, 'abs': 3, 'absx': 3,
|
|
'absy': 3, 'ind': 3, 'indx': 2, 'indy': 2, 'rel': 2}
|
|
|
|
EMIT = re.compile(r'; CPU:\$([0-9A-F]{4}) PRG:([0-9A-F]{4}) ((?:[0-9A-F]{2} ?)+)')
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument('--dir', default='.')
|
|
ap.add_argument('--cdl', required=True)
|
|
ap.add_argument('--prg')
|
|
ap.add_argument('--src')
|
|
ap.add_argument('--locals')
|
|
ap.add_argument('--base', default='0xC000')
|
|
ap.add_argument('--label-template', default='Label_{addr:04X}')
|
|
ap.add_argument('--override', action='append', default=[],
|
|
help='LO-HI:C|D, e.g. 0xC0C0-0xC0E5:D')
|
|
ap.add_argument('--apply', action='store_true')
|
|
a = ap.parse_args()
|
|
|
|
d = a.dir.rstrip('/')
|
|
BASE = int(a.base, 0)
|
|
prg_path = a.prg or f'{d}/prg.bin'
|
|
src_path = a.src or f'{d}/prg.s'
|
|
loc_path = a.locals or f'{d}/locals.inc'
|
|
|
|
PRG = open(prg_path, 'rb').read()
|
|
N = len(PRG)
|
|
lo_ok = BASE <= 0x10000 - N
|
|
if not lo_ok:
|
|
sys.exit(f'{prg_path}: {N} bytes does not fit at ${BASE:04X}')
|
|
|
|
# ---- zero-page names (optional) ----
|
|
ZP = {}
|
|
try:
|
|
for ln in open(loc_path):
|
|
m = re.match(r'^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*\$([0-9A-Fa-f]{2})\b', ln)
|
|
if not m:
|
|
continue
|
|
name, val = m.group(1), int(m.group(2), 16)
|
|
if val not in ZP or (re.match(r'^Zp[0-9A-F]{2}$', ZP[val]) and
|
|
not re.match(r'^Zp[0-9A-F]{2}$', name)):
|
|
ZP[val] = name
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
def zpn(v):
|
|
return ZP.get(v, '$%02X' % v)
|
|
|
|
# ---- parse the source ----
|
|
LINES = open(src_path).read().split('\n')
|
|
SRC_ALL = '\n'.join(LINES)
|
|
units = []
|
|
for i, ln in enumerate(LINES):
|
|
m = EMIT.search(ln)
|
|
if not m:
|
|
continue
|
|
off = int(m.group(2), 16)
|
|
n = len(re.findall(r'[0-9A-F]{2}', m.group(3)))
|
|
kind = 'D' if ln.lstrip().startswith(('.byte', '.word', '.incbin')) else 'C'
|
|
units.append((i, off, n, kind))
|
|
cur = [None] * N
|
|
unit_of = [None] * N
|
|
for i, off, n, kind in units:
|
|
for k in range(n):
|
|
if off + k < N:
|
|
cur[off + k] = kind
|
|
unit_of[off + k] = (i, off, n)
|
|
|
|
# label definitions: `Name:` on the line(s) immediately above an emission.
|
|
# Consecutive labels are aliases of the same address, so they all map to it.
|
|
unit_line = {u[0]: u[1] for u in units}
|
|
label_at = {}
|
|
pending = []
|
|
for i, ln in enumerate(LINES):
|
|
s = ln.strip()
|
|
if re.match(r'^[A-Za-z_][A-Za-z0-9_]*:$', s):
|
|
pending.append(s[:-1])
|
|
continue
|
|
if s and not s.startswith(';'):
|
|
if pending and i in unit_line:
|
|
off = unit_line[i]
|
|
for name in pending:
|
|
label_at.setdefault(off, name)
|
|
pending = []
|
|
defined = set(re.findall(r'^([A-Za-z_][A-Za-z0-9_]*):', SRC_ALL, re.M))
|
|
assert set(label_at.values()) <= defined, 'label mapping disagrees with the file'
|
|
|
|
def is_ref(nm):
|
|
"""True if nm appears in operand position on an instruction line."""
|
|
return bool(re.search(r'^\s+\w[^\n]*\b' + re.escape(nm) + r'\b', SRC_ALL, re.M))
|
|
|
|
def name_at(off):
|
|
return label_at.get(off) or a.label_template.format(addr=BASE + off)
|
|
|
|
blocks = []
|
|
for ln in LINES:
|
|
m = re.match(r'^; ---- data \$([0-9A-F]{4})-\$([0-9A-F]{4}) ', ln)
|
|
if m:
|
|
blocks.append((int(m.group(1), 16) - BASE, int(m.group(2), 16) - BASE))
|
|
|
|
# ---- CDL -> target classification ----
|
|
raw = open(a.cdl, 'rb').read()
|
|
if raw[:5] == b'CDLv2':
|
|
# "CDLv2" + 4 header bytes, then one flags byte per PRG byte
|
|
body = raw[9:9 + N]
|
|
else:
|
|
body = raw[-N:]
|
|
if len(body) < N:
|
|
sys.exit(f'{a.cdl}: expected at least {N} CDL bytes, got {len(body)}')
|
|
tgt = [('C' if (body[i] & 1) else ('D' if (body[i] & 2) else cur[i])) for i in range(N)]
|
|
|
|
conflicts = []
|
|
for spec in a.override:
|
|
m = re.match(r'^\$?([0-9A-Fa-fx]+)-\$?([0-9A-Fa-fx]+):([CD])$', spec)
|
|
if not m:
|
|
sys.exit(f'--override {spec!r}: expected LO-HI:C|D')
|
|
lo, hi, k = int(m.group(1), 0) - BASE, int(m.group(2), 0) - BASE, m.group(3)
|
|
for i in range(max(lo, 0), min(hi, N - 1) + 1):
|
|
if not (body[i] & 3) or tgt[i] == k:
|
|
tgt[i] = k
|
|
else:
|
|
conflicts.append(f'${BASE+i:04X}: CDL says {tgt[i]}, override says {k} (override wins)')
|
|
tgt[i] = k # override always wins when explicitly specified
|
|
|
|
# ---- region derivation ----
|
|
def merge(iv):
|
|
out = []
|
|
for r in sorted(iv):
|
|
if out and r[0] <= out[-1][1] + 1:
|
|
out[-1][1] = max(out[-1][1], r[1])
|
|
else:
|
|
out.append([r[0], r[1]])
|
|
return out
|
|
|
|
def walk(s, e):
|
|
"""Decode [s,e]; return (branch targets, overrun end or None)."""
|
|
p, labels = s, set()
|
|
while p <= e:
|
|
if tgt[p] == 'C':
|
|
n = SIZE[T[PRG[p]][1]]
|
|
if p + n - 1 > e:
|
|
return labels, p + n - 1
|
|
if T[PRG[p]][1] == 'rel':
|
|
dd = PRG[p + 1]
|
|
dd = dd - 256 if dd > 127 else dd
|
|
labels.add(BASE + ((p + 2 + dd) % N))
|
|
p += n
|
|
else:
|
|
p += 1
|
|
return labels, None
|
|
|
|
affected = [i for i in range(N) if cur[i] and cur[i] != tgt[i]]
|
|
iv = []
|
|
for o in affected:
|
|
if iv and o <= iv[-1][1] + 1:
|
|
iv[-1][1] = o
|
|
else:
|
|
iv.append([o, o])
|
|
for r in iv: # to whole emission units
|
|
_, o0, n0 = unit_of[r[0]]
|
|
_, o1, n1 = unit_of[r[1]]
|
|
r[0], r[1] = o0, o1 + n1 - 1
|
|
iv = merge(iv)
|
|
_iv = [] # across small gaps, so stray
|
|
for r in iv: # mis-decode comments get cleaned
|
|
if _iv and r[0] - _iv[-1][1] <= 5:
|
|
_iv[-1][1] = r[1]
|
|
else:
|
|
_iv.append(r)
|
|
iv = _iv
|
|
for r in iv: # to whole data blocks
|
|
for bs, be in blocks:
|
|
if r[0] <= be and r[1] >= bs:
|
|
r[0] = min(r[0], bs)
|
|
r[1] = max(r[1], be)
|
|
regions = merge(iv)
|
|
for _ in range(8): # fix decodes that overrun a region
|
|
ext = []
|
|
for s, e in regions:
|
|
_, over = walk(s, e)
|
|
ext.append([s, over if over is not None else e])
|
|
m = merge(ext)
|
|
if m == regions:
|
|
break
|
|
regions = m
|
|
|
|
# ---- which labels must be (re)defined ----
|
|
must_branch, must_ref, nice = set(), set(), set()
|
|
for s, e in regions:
|
|
must_branch |= walk(s, e)[0]
|
|
for s, e in regions:
|
|
for t in range(s, e + 1):
|
|
if t in label_at:
|
|
nice.add(BASE + t)
|
|
if is_ref(label_at[t]):
|
|
must_ref.add(BASE + t)
|
|
|
|
region_of = [None] * N
|
|
for idx, (s, e) in enumerate(regions):
|
|
for t in range(s, e + 1):
|
|
region_of[t] = idx
|
|
starts = set()
|
|
for s, e in regions:
|
|
p = s
|
|
while p <= e:
|
|
starts.add(BASE + p)
|
|
p += SIZE[T[PRG[p]][1]] if tgt[p] == 'C' else 1
|
|
|
|
need = {c: name_at(c - BASE) for c in (must_branch | must_ref | nice)
|
|
if 0 <= c - BASE < N and region_of[c - BASE] is not None
|
|
and tgt[c - BASE] == 'C' and c in starts}
|
|
|
|
problems, warnings = [], []
|
|
for c in sorted(must_ref):
|
|
if c not in need:
|
|
problems.append(f'${c:04X} is referenced by surviving code but cannot be labelled')
|
|
for c in sorted(must_branch):
|
|
if c not in need and (c - BASE) not in label_at:
|
|
warnings.append(f'branch target ${c:04X} is not an instruction boundary; '
|
|
f'emitted as a raw relative offset')
|
|
dropped = sorted(label_at[t] for s, e in regions for t in range(s, e + 1)
|
|
if t in label_at and BASE + t not in need)
|
|
|
|
# ---- generation ----
|
|
TEMPLATE = None
|
|
for i, ln in enumerate(LINES):
|
|
m = re.match(r'^; Routine: (\S+)$', ln)
|
|
if m and i + 8 < len(LINES) and LINES[i + 8] == m.group(1) + ':':
|
|
TEMPLATE = LINES[i - 1:i + 9]
|
|
break
|
|
|
|
def hdr(name):
|
|
if not TEMPLATE:
|
|
return [name + ':']
|
|
b = list(TEMPLATE)
|
|
b[1] = '; Routine: ' + name
|
|
b[-1] = name + ':'
|
|
return b
|
|
|
|
unresolved = []
|
|
|
|
def fmt(mn, mode, ob, p):
|
|
if mode == 'imp':
|
|
return mn
|
|
if mode == 'acc':
|
|
return mn + ' a'
|
|
if mode == 'imm':
|
|
return f'{mn} #${ob[0]:02X}'
|
|
if mode == 'zp':
|
|
return f'{mn} {zpn(ob[0])}'
|
|
if mode == 'zpx':
|
|
return f'{mn} {zpn(ob[0])},x'
|
|
if mode == 'zpy':
|
|
return f'{mn} {zpn(ob[0])},y'
|
|
if mode == 'abs':
|
|
return f'{mn} ${ob[0] | (ob[1] << 8):04X}'
|
|
if mode == 'absx':
|
|
return f'{mn} ${ob[0] | (ob[1] << 8):04X},x'
|
|
if mode == 'absy':
|
|
return f'{mn} ${ob[0] | (ob[1] << 8):04X},y'
|
|
if mode == 'ind':
|
|
return f'{mn} (${ob[0] | (ob[1] << 8):04X})'
|
|
if mode == 'indx':
|
|
return f'{mn} ({zpn(ob[0])},x)'
|
|
if mode == 'indy':
|
|
return f'{mn} ({zpn(ob[0])}),y'
|
|
if mode == 'rel':
|
|
dd = ob[0] - 256 if ob[0] > 127 else ob[0]
|
|
cpu = BASE + ((p + 2 + dd) % N)
|
|
if cpu in need or (cpu - BASE) in label_at:
|
|
return f'{mn} {need.get(cpu) or name_at(cpu - BASE)}'
|
|
unresolved.append(cpu)
|
|
k = cpu - (BASE + p)
|
|
return f'{mn} *{"+" if k >= 0 else "-"}${abs(k):02X}'
|
|
|
|
def gen(s, e):
|
|
out, p = [], s
|
|
while p <= e:
|
|
if tgt[p] == 'D':
|
|
q = p
|
|
while q <= e and tgt[q] == 'D':
|
|
q += 1
|
|
out.append(f"; ---- data ${BASE+p:04X}-${BASE+q-1:04X} ({q-p} bytes) ----")
|
|
for i in range(p, q, 16):
|
|
ch = PRG[i:min(q, i + 16)]
|
|
out.append(" .byte %s ; CPU:$%04X PRG:%04X %s"
|
|
% (', '.join('$%02X' % b for b in ch), BASE + i, i,
|
|
' '.join('%02X' % b for b in ch)))
|
|
p = q
|
|
else:
|
|
mn, mode, undoc = T[PRG[p]]
|
|
n = SIZE[mode]
|
|
cpu = BASE + p
|
|
if cpu in need:
|
|
out.extend(hdr(need[cpu]))
|
|
text = fmt(mn, mode, PRG[p + 1:p + n], p)
|
|
bstr = ' '.join('%02X' % b for b in PRG[p:p + n])
|
|
if undoc:
|
|
out.append(" .byte %s ; CPU:$%04X PRG:%04X %s -- %s "
|
|
"(undocumented; ca65 cannot encode)"
|
|
% (', '.join('$%02X' % b for b in PRG[p:p + n]),
|
|
cpu, p, bstr, text))
|
|
else:
|
|
out.append(f" {text:<31}; CPU:${cpu:04X} PRG:{p:04X} {bstr}")
|
|
p += n
|
|
return out
|
|
|
|
# ---- splice ----
|
|
new_lines, cursor, report = [], 0, []
|
|
for s, e in regions:
|
|
first = next(i for i, off, n, k in units if off <= s < off + n)
|
|
last = next(i for i, off, n, k in units if off <= e < off + n)
|
|
lo, hi = first, last
|
|
if re.match(r'^[A-Za-z_][A-Za-z0-9_]*:$', LINES[lo - 1]):
|
|
lo -= 1
|
|
while lo > 0 and LINES[lo - 1].startswith('; ') \
|
|
and not LINES[lo - 1].startswith('; ---- data'):
|
|
lo -= 1
|
|
elif LINES[lo - 1].startswith('; ---- data '):
|
|
lo -= 1
|
|
new_lines.extend(LINES[cursor:lo])
|
|
genl = gen(s, e)
|
|
new_lines.extend(genl)
|
|
cursor = hi + 1
|
|
report.append(f" ${BASE+s:04X}-${BASE+e:04X} {e-s+1:5d} B "
|
|
f"lines {lo+1}-{hi+1} -> {len(genl)}")
|
|
new_lines.extend(LINES[cursor:])
|
|
print(f"{src_path}: {N} bytes, {len(units)} emissions, {len(regions)} region(s)")
|
|
for r in report:
|
|
print(r)
|
|
print(f"data->code {sum(1 for i in range(N) if cur[i]=='D' and tgt[i]=='C')} "
|
|
f"code->data {sum(1 for i in range(N) if cur[i]=='C' and tgt[i]=='D')} "
|
|
f"labels emitted {len(need)} dropped {len(dropped)}")
|
|
if dropped:
|
|
print(" dropped:", ' '.join(dropped))
|
|
if unresolved:
|
|
print(" raw relative branches:", ' '.join(f'${c:04X}' for c in sorted(set(unresolved))))
|
|
for c in conflicts:
|
|
print(" CONFLICT " + c)
|
|
if problems:
|
|
print("PROBLEMS:")
|
|
for p in problems:
|
|
print(' ' + p)
|
|
for w in warnings:
|
|
print(" warning: " + w)
|
|
|
|
if not a.apply:
|
|
print("\ndry run: nothing written (pass --apply)")
|
|
return
|
|
if problems:
|
|
print("\nrefusing to apply while problems remain")
|
|
sys.exit(1)
|
|
open(src_path, 'w').write('\n'.join(new_lines) + '\n')
|
|
print(f"\nAPPLIED to {src_path} -- now rebuild and check for BYTE-IDENTICAL")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|