Promotes Lunar Pool

This commit is contained in:
tmerritt committed 2026-10-18 16:47:03 -04:00
1 parent 837007843d
commit 62866563ba
53 files changed
+115746 -4100

No files matched your search

+498
View File
@@ -0,0 +1,498 @@
#!/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}')
# ---- 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()
+4 -1
View File
@@ -51,9 +51,12 @@ ROOT = Path(__file__).resolve().parent.parent
# Comment spellings actually found in the repo, most-preferred first.
AUDIT_FORMS = {
# re.M matters: the trailing $ anchors the byte list to end of *line*. Without
# it the pattern only ever matches once, at end of file, so every project was
# reported as a non-canonical spelling.
"canonical CPU:$x PRG:xxxx AA BB":
re.compile(r"CPU:\$?([0-9A-Fa-f]{4})\s+PRG[: ]\$?([0-9A-Fa-f]{4})\s+"
r"((?:[0-9A-Fa-f]{2}[ ]?)+?)\s*(?:--.*)?$"),
r"((?:[0-9A-Fa-f]{2}[ ]?)+?)\s*(?:--.*)?$", re.M),
"pipe PRG $xxxx | CPU $xxxx | AA BB":
re.compile(r"PRG \$([0-9A-Fa-f]{4})\s*\|\s*CPU \$([0-9A-Fa-f]{4})\s*\|\s*"
r"((?:[0-9A-Fa-f]{2} )+)"),