254 lines
8.2 KiB
Python
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()
|