Files
disasm/_tools/derive_branch_names.py
T

254 lines
8.2 KiB
Python

#!/usr/bin/env python3
"""Derive the internal branch-target names for a Pass 2 map.
A routine entry gets a semantic name written by hand (see routinemap.tsv).
The internal branch targets inside it are named mechanically from the branch
that reaches them: the role says what has to be true for the branch to be
taken, and the owner is the routine the target belongs to.
beq -> OnZero bne -> OnNonzero
bcc -> OnCarryClear bcs -> OnCarrySet
bmi -> OnNegative bpl -> OnNonnegative
bvc -> OnOverflowClear bvs -> OnOverflowSet
a backwards branch (the target is below the branch) -> Loop
Names are made unique inside their owner by appending 2, 3, ... in address
order, so the map is deterministic.
A banked cartridge (MMC3 and friends) has one copy of every label per bank,
so the map may name a bank's label with a `<prefix>:<addr>` key and
`--bank-prefix` says which prefix belongs to which part of the file.
Usage:
python3 _tools/derive_branch_names.py <project dir> <routinemap.tsv>
"""
import re
import sys
from collections import defaultdict
from pathlib import Path
ROLE = {
"beq": "OnZero", "bne": "OnNonzero",
"bcc": "OnCarryClear", "bcs": "OnCarrySet",
"bmi": "OnNegative", "bpl": "OnNonnegative",
"bvc": "OnOverflowClear", "bvs": "OnOverflowSet",
}
LABEL_RE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*):\s*$")
CPU_RE = re.compile(r"; CPU:\$([0-9A-F]{4}) PRG:([0-9A-F]{4})(?: BANK:([0-9A-F]+))?")
BANKED_LABEL_RE = re.compile(r"^(?:(?P<pre>[A-Za-z][A-Za-z0-9]*)_)?Label_(?P<addr>[0-9A-F]{4})$")
def read_map(lines):
"""{(prefix, address): name}"""
names = {}
for raw in lines:
line = raw.split("#", 1)[0].strip()
if not line:
continue
parts = line.split("\t") if "\t" in line else line.split(None, 1)
key = parts[0].strip()
prefix = ""
if ":" in key:
prefix, _, key = key.partition(":")
prefix = prefix.strip()
names[(prefix, int(key.strip().lstrip("$"), 16))] = parts[1].strip()
return names
def read_bank_prefixes(spec):
out = []
for tok in spec.split(","):
tok = tok.strip()
if not tok:
continue
off, _, pre = tok.partition(":")
out.append((int(off, 16), pre.strip()))
return sorted(out)
def prefix_at(bank_prefixes, file_offset):
pre = ""
for off, p in bank_prefixes:
if file_offset >= off:
pre = p
else:
break
return pre
def file_offset_of(match):
"""File offset of an audit comment.
A banked image larger than 64 KB cannot fit its file offset in the
four-digit PRG field, so banked_dis.py records the bank base in a
BANK: field and leaves PRG as the offset within the bank.
"""
if match.lastindex and match.lastindex >= 3 and match.group(3):
return int(match.group(3), 16) + int(match.group(2), 16)
return int(match.group(2), 16)
def main():
proj = Path(sys.argv[1])
mapfile = Path(sys.argv[2])
bank_prefixes = read_bank_prefixes(sys.argv[3] if len(sys.argv) > 3 else "")
outfile = Path(sys.argv[4]) if len(sys.argv) > 4 else mapfile
# The file holds the hand-written routine names and, below a marker, the
# names this tool derived. Only the hand-written part is input: reading
# the derived part back would make the tool accumulate names over
# repeated runs.
raw_lines = mapfile.read_text().splitlines()
marker = "# ---- derived internal branch-target names ----"
hand = []
for line in raw_lines:
if line.startswith(marker):
break
hand.append(line)
# the hand-written routine names
names = read_map(hand)
lines = (proj / "prg.s").read_text().split("\n")
# every label and its address
labels = {}
addr_of = {}
cur = None
for i, line in enumerate(lines):
m = LABEL_RE.match(line)
if m:
cur = m.group(1)
continue
m2 = CPU_RE.search(line)
if m2 and cur:
lm = BANKED_LABEL_RE.match(cur)
if lm:
key = (lm.group("pre") or "", int(lm.group("addr"), 16))
else:
key = ("", int(m2.group(1), 16))
labels[key] = int(m2.group(1), 16)
addr_of[key] = cur
cur = None
# the branches that reach each label
incoming = defaultdict(list)
# the calls that reach each label: a `jsr`/`jmp` target that no hand
# written name covers keeps the address-derived form the repo accepts
# (`Sub_XXXX`).
called = set()
for i, line in enumerate(lines):
m = re.match(r"^\s*(b[a-z]{2})\s+(\S+)\s*;", line)
if m:
op, tgt = m.group(1), m.group(2)
lm = BANKED_LABEL_RE.match(tgt)
if not lm:
continue
am = CPU_RE.search(line)
if not am:
continue
prefix = prefix_at(bank_prefixes, file_offset_of(am))
incoming[(prefix, int(lm.group("addr"), 16))].append(
(op, int(am.group(1), 16)))
continue
m = re.match(r"^\s*(jsr|jmp)\s+(\S+)\s*;", line)
if m:
lm = BANKED_LABEL_RE.match(m.group(2))
if lm:
called.add((lm.group("pre") or "", int(lm.group("addr"), 16)))
continue
am = CPU_RE.search(line)
rm = re.match(r"^\$([0-9A-F]{4})$", m.group(2))
if rm and am:
prefix = prefix_at(bank_prefixes, file_offset_of(am))
addr = int(rm.group(1), 16)
# A call into $E000-$FFFF always lands in the fixed bank,
# whichever bank the caller runs in.
if 0xE000 <= addr <= 0xFFFF:
prefix = ""
called.add((prefix, addr))
# the owner of each address: the nearest routine entry below it
entries = defaultdict(list)
for (prefix, a) in names:
entries[prefix].append(a)
for prefix in entries:
entries[prefix].sort()
def owner_of(prefix, addr):
best = None
for e in entries.get(prefix, []):
if e <= addr:
best = e
else:
break
return best
# name the internal targets
internal = defaultdict(list)
for key, brs in incoming.items():
if key in names:
continue
internal[key] = brs
used = defaultdict(set)
out = {}
for key in sorted(internal):
prefix, a = key
brs = internal[key]
roles = []
for op, baddr in brs:
if a <= baddr:
roles.append(ROLE.get(op, "Branch"))
else:
roles.append("Loop")
role = sorted(roles, key=lambda r: (roles.count(r), r))[-1]
own = owner_of(prefix, a)
base = f"{prefix + '_' if prefix else ''}{names.get((prefix, own), 'Sub')}_{role}"
n = 2
cand = base
while cand in used[prefix]:
cand = f"{base}{n}"
n += 1
used[prefix].add(cand)
out[key] = cand
# A `jsr`/`jmp` target that no hand-written name covers keeps the
# address-derived form: the repo accepts `Sub_XXXX` for an entry point
# whose role the dump does not reveal.
for key in sorted(called):
if key in names or key in out:
continue
prefix, a = key
name = f"{prefix + '_' if prefix else ''}Sub_{a:04X}"
if name in used[prefix]:
continue
used[prefix].add(name)
out[key] = name
# merge: the hand-written routine names plus the derived ones
merged = dict(names)
merged.update(out)
# keep the header the hand-written map already carries, so the file
# documents itself in the project's own terms
header = [l for l in hand if l.startswith("#")]
with outfile.open("w") as f:
for line in hand:
if not line.startswith("#"):
f.write(line + "\n")
f.write("\n")
f.write(marker + "\n")
for key in sorted(merged):
prefix, a = key
f.write(f"{prefix + ':' if prefix else ''}{a:04X}\t{merged[key]}\n")
print(f"{len(names)} routine names + {len(out)} derived branch names "
f"= {len(merged)} entries -> {outfile}")
if __name__ == "__main__":
main()