- Decoded the VramWrite ($E1CC) stream format: <addr hi> <addr lo> <count> <bytes>, $00 terminator - Named all 9 payload streams ($E4CC-$E7C2, no gaps/overlaps) plus VramWriteSourcePointerTable at $F000 - Split PadBitTableLives into Player1TileTable ($E4B4) and Player2TileTable ($E4C0), 12 entries each (Y=0..11) - Decoded CHR font (digits $30-$39, letters $3A-$52, space $5F; 'W' missing from the A-Z run) and recovered on-screen text including 'PRESS A TO TEST SIDE-A' / 'SIDE-B' - Documented that the 16 KiB PRG is two byte-identical 8 KiB halves; $C000 copy is dead - Documented 7 unreached subroutines inside the live code region (kept as .byte per Pass-1 policy) - Fixed a stray duplicate sentence and stale _wip paths in root OVERVIEW.md Byte-identical ROM verified.
626 lines
24 KiB
Python
626 lines
24 KiB
Python
#!/usr/bin/env python3
|
|
"""Pass 2 helper: group a generated Pass 1 prg.s into routines and apply renames.
|
|
|
|
Subcommands
|
|
outline [FILE] list every routine: entry, size, callees, callers
|
|
show FILE START-END compact listing of one address range
|
|
apply FILE MAP rewrite labels / zero-page names in place
|
|
|
|
The MAP file is `kind<TAB>old<TAB>new` per line, where kind is `label` or `zp`.
|
|
Pass 1's `; CPU:$xxxx PRG:xxxx AA BB` audit comments and every emitted byte are
|
|
left untouched, so the build stays byte-identical.
|
|
"""
|
|
|
|
import bisect
|
|
import re
|
|
import sys
|
|
from collections import OrderedDict
|
|
from pathlib import Path
|
|
|
|
LABEL_RE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*):\s*$")
|
|
INSN_RE = re.compile(
|
|
r"^ (?P<text>[a-z]{3})\s*(?P<op>.*?)\s*"
|
|
r"; CPU:\$(?P<cpu>[0-9A-F]{4}) PRG:(?P<prg>[0-9A-F]{4})\s+"
|
|
r"(?P<raw>(?:[0-9A-F]{2} )*[0-9A-F]{2})\s*$"
|
|
)
|
|
ZP_RE = re.compile(r"^(Zp[0-9A-F]{2})$")
|
|
TARGET_RE = re.compile(r"^(?:Label_([0-9A-F]{4})|\$([0-9A-F]{4}))$")
|
|
|
|
TERMINATORS = {"jmp", "rts", "rti", "brk"}
|
|
|
|
|
|
def parse(path):
|
|
"""Return (order, labels, insns, data_spans) from a generated prg.s."""
|
|
order = [] # source line index -> ('label', name) | ('insn', Insn) | ('data', ...)
|
|
labels = OrderedDict() # name -> line index
|
|
insns = OrderedDict() # cpu -> dict(text, op, raw, line)
|
|
by_line = {}
|
|
for idx, line in enumerate(Path(path).read_text().splitlines()):
|
|
m = LABEL_RE.match(line)
|
|
if m:
|
|
labels[m.group(1)] = idx
|
|
order.append(("label", m.group(1)))
|
|
continue
|
|
m = INSN_RE.match(line)
|
|
if m:
|
|
cpu = int(m.group("cpu"), 16)
|
|
rec = {"cpu": cpu, "mn": m.group("text"), "op": m.group("op"),
|
|
"raw": m.group("raw"), "line": idx}
|
|
insns[cpu] = rec
|
|
by_line[idx] = ("insn", rec)
|
|
order.append(("insn", rec))
|
|
continue
|
|
if line.startswith("; ---- data"):
|
|
order.append(("data", line))
|
|
continue
|
|
return order, labels, insns
|
|
|
|
|
|
def label_index(order):
|
|
"""line index -> label name preceding it (for branch naming)."""
|
|
cur = None
|
|
out = {}
|
|
for kind, val in order:
|
|
if kind == "label":
|
|
cur = val
|
|
elif kind == "insn":
|
|
out[val["line"]] = cur
|
|
return out
|
|
|
|
|
|
def routines(insns, order, labels):
|
|
"""Split instructions into routines at JSR/JMP/vector entry points."""
|
|
entries = set()
|
|
callers = {}
|
|
for rec in insns.values():
|
|
if rec["mn"] in ("jsr", "jmp"):
|
|
m = re.match(r"^\$([0-9A-F]{4})$", rec["op"])
|
|
if m:
|
|
t = int(m.group(1), 16)
|
|
entries.add(t)
|
|
callers.setdefault(t, []).append((rec["cpu"], rec["mn"]))
|
|
# labels declared in the file that sit at a routine entry
|
|
for name, line in labels.items():
|
|
for kind, val in order:
|
|
if kind == "insn" and val["line"] == line:
|
|
entries.add(val["cpu"])
|
|
break
|
|
return sorted(entries), callers
|
|
|
|
|
|
def outline(path):
|
|
order, labels, insns = parse(path)
|
|
entries, callers = routines(insns, order, labels)
|
|
# walk each routine from its entry until a terminator
|
|
for start in entries:
|
|
rec = insns.get(start)
|
|
if rec is None:
|
|
print(f"{start:04X} (no instruction: entry with no body)")
|
|
continue
|
|
callees = []
|
|
size = 0
|
|
head = []
|
|
addr = start
|
|
while True:
|
|
r = insns.get(addr)
|
|
if r is None:
|
|
break
|
|
if len(head) < 3:
|
|
head.append(f"{r['mn']} {r['op']}".strip())
|
|
size += len(r["raw"].split())
|
|
if r["mn"] == "jsr":
|
|
m = re.match(r"^\$([0-9A-F]{4})$", r["op"])
|
|
if m:
|
|
callees.append(int(m.group(1), 16))
|
|
if r["mn"] in TERMINATORS:
|
|
break
|
|
addr += len(r["raw"].split())
|
|
callers_here = ",".join(f"{c:04X}{'j' if k == 'jmp' else 's'}"
|
|
for c, k in sorted(set(callers.get(start, []))))
|
|
cal = ",".join(f"{c:04X}" for c in sorted(set(callees)))
|
|
sig = " ; ".join(head)
|
|
print(f"{start:04X} sz={size:<4} in=[{callers_here}] out=[{cal}] {sig}")
|
|
|
|
|
|
BRANCH_COND = {
|
|
"beq": "OnZero", "bne": "OnNonzero", "bcc": "OnCarryClear", "bcs": "OnCarrySet",
|
|
"bpl": "OnNonnegative", "bmi": "OnNegative", "bvs": "OnOverflowSet",
|
|
"bvc": "OnOverflowClear",
|
|
}
|
|
|
|
# Words describing what the instruction just before a fall-through label did.
|
|
ROLE_BY_MNEMONIC = {
|
|
"jsr": "CallNext", "jmp": "JumpNext",
|
|
"lda": "LoadAndContinue", "ldx": "LoadIndexAndContinue", "ldy": "LoadIndexAndContinue",
|
|
"sta": "StoreAndContinue", "stx": "StoreIndexAndContinue", "sty": "StoreIndexAndContinue",
|
|
"inc": "IncrementAndContinue", "inx": "IncrementAndContinue", "iny": "IncrementAndContinue",
|
|
"dec": "DecrementAndContinue", "dex": "DecrementAndContinue", "dey": "DecrementAndContinue",
|
|
"asl": "ShiftLeftAndContinue", "lsr": "ShiftRightAndContinue",
|
|
"rol": "RotateLeftAndContinue", "ror": "RotateRightAndContinue",
|
|
"cmp": "OnCompare", "cpx": "OnCompare", "cpy": "OnCompare",
|
|
"and": "OnMask", "ora": "OnMask", "eor": "OnMask",
|
|
"adc": "OnAdd", "sbc": "OnSubtract",
|
|
"rts": "Return", "rti": "Return", "brk": "Halt",
|
|
"tax": "TransferIndexAndContinue", "tay": "TransferIndexAndContinue",
|
|
"txa": "TransferAccumulatorAndContinue", "tya": "TransferAccumulatorAndContinue",
|
|
"tsx": "TransferIndexAndContinue", "txs": "TransferIndexAndContinue",
|
|
"pha": "PushAndContinue", "php": "PushAndContinue",
|
|
"pla": "PullAndContinue", "plp": "PullAndContinue",
|
|
"clc": "Continue", "sec": "Continue", "cli": "Continue", "sei": "Continue",
|
|
"clv": "Continue", "sed": "Continue", "cld": "Continue", "nop": "Continue",
|
|
"bit": "OnMask", "dey": "DecrementAndContinue",
|
|
}
|
|
|
|
|
|
def routine_entries(insns, labels, vectors=()):
|
|
"""Addresses that begin a routine: JSR/JMP targets, vectors, and declared labels."""
|
|
entries = set(vectors)
|
|
for rec in insns.values():
|
|
if rec["mn"] in ("jsr", "jmp"):
|
|
m = re.match(r"^\$([0-9A-F]{4})$", rec["op"])
|
|
if m:
|
|
entries.add(int(m.group(1), 16))
|
|
for name, line in labels.items():
|
|
for rec in insns.values():
|
|
if rec["line"] == line:
|
|
entries.add(rec["cpu"])
|
|
break
|
|
return entries
|
|
|
|
|
|
def derive(insns, labels, name_map, vectors=()):
|
|
"""Produce label -> name for every Label_XXXX, deriving internal names."""
|
|
entries = routine_entries(insns, labels, vectors)
|
|
|
|
# incoming references per address
|
|
incoming = {}
|
|
fallthrough = {}
|
|
for rec in insns.values():
|
|
m = re.match(r"^Label_([0-9A-F]{4})$", rec["op"])
|
|
if m and rec["mn"] in BRANCH_COND:
|
|
incoming.setdefault(int(m.group(1), 16), set()).add(rec["mn"])
|
|
|
|
addrs = sorted(insns)
|
|
by_line = sorted((rec["line"], rec["cpu"]) for rec in insns.values())
|
|
line_keys = [k for k, _ in by_line]
|
|
|
|
def cpu_after(line):
|
|
"""Address of the first instruction that starts after the given source line."""
|
|
i = bisect.bisect_right(line_keys, line)
|
|
return by_line[i][1] if i < len(by_line) else None
|
|
|
|
prev_of = {}
|
|
for i, a in enumerate(addrs):
|
|
if i:
|
|
prev_of[a] = addrs[i - 1]
|
|
|
|
def enclosing(addr):
|
|
best = None
|
|
for e in entries:
|
|
if e <= addr and (best is None or e > best):
|
|
best = e
|
|
return best
|
|
|
|
out = {}
|
|
for name, line in labels.items():
|
|
if not name.startswith("Label_"):
|
|
continue
|
|
addr = cpu_after(line)
|
|
if addr is None:
|
|
continue
|
|
if addr in name_map: # routine names come from the hand map
|
|
out[name] = name_map[addr]
|
|
continue
|
|
parent = enclosing(addr)
|
|
base = name_map.get(parent, f"Sub_{parent:04X}")
|
|
conds = incoming.get(addr, set())
|
|
if len(conds) > 1:
|
|
suffix = "JoinBranchPaths"
|
|
elif len(conds) == 1:
|
|
suffix = BRANCH_COND[next(iter(conds))]
|
|
else:
|
|
prev = prev_of.get(addr)
|
|
prev_rec = insns.get(prev)
|
|
if prev_rec is None or prev + len(prev_rec["raw"].split()) != addr:
|
|
suffix = "Entry"
|
|
else:
|
|
suffix = ROLE_BY_MNEMONIC.get(prev_rec["mn"], "Continue")
|
|
out[name] = f"{base}_{suffix}_{addr:04X}"
|
|
return out
|
|
|
|
|
|
def show(path, rng):
|
|
start, end = (int(x, 16) for x in rng.split("-"))
|
|
order, labels, insns = parse(path)
|
|
idx = label_index(order)
|
|
cur_shown = None
|
|
for addr in sorted(insns):
|
|
if not (start <= addr <= end):
|
|
continue
|
|
rec = insns[addr]
|
|
lab = idx.get(rec["line"])
|
|
if lab and lab != cur_shown:
|
|
print(f"{lab}:")
|
|
cur_shown = lab
|
|
text = f"{rec['mn']} {rec['op']}".strip()
|
|
print(f" {addr:04X} {text:<28} | {rec['raw']}")
|
|
|
|
|
|
def apply_map(path, mapfile):
|
|
lines = Path(path).read_text().splitlines(keepends=True)
|
|
label_map = {}
|
|
zp_map = {}
|
|
scoped = [] # (start, end, old, new) for zero-page bytes reused across roles
|
|
addr_re = re.compile(r"; CPU:\$([0-9A-F]{4}) PRG:[0-9A-F]{4}")
|
|
for line in Path(mapfile).read_text().splitlines():
|
|
if not line.strip() or line.startswith("#"):
|
|
continue
|
|
parts = line.split("\t")
|
|
kind = parts[0]
|
|
if kind == "label":
|
|
label_map[parts[1]] = parts[2]
|
|
elif kind == "zp":
|
|
zp_map[parts[1]] = parts[2]
|
|
elif kind == "zprange":
|
|
start, end, old, new = (int(parts[1], 16), int(parts[2], 16),
|
|
parts[3], parts[4])
|
|
scoped.append((start, end, old, new))
|
|
changed = 0
|
|
scoped_changed = 0
|
|
for i, line in enumerate(lines):
|
|
m = LABEL_RE.match(line.rstrip("\n"))
|
|
if m and m.group(1) in label_map:
|
|
lines[i] = label_map[m.group(1)] + ":\n"
|
|
changed += 1
|
|
continue
|
|
# operand-side label references
|
|
def repl(mo):
|
|
name = mo.group(0)
|
|
if name in label_map:
|
|
changed_local[0] += 1
|
|
return label_map[name]
|
|
return name
|
|
changed_local = [0]
|
|
new_line = re.sub(r"\b(?:Label_[0-9A-F]{4})\b", repl, line)
|
|
for old, new in zp_map.items():
|
|
new_line = re.sub(rf"\b{old}\b", new, new_line)
|
|
am = addr_re.search(new_line)
|
|
if am and scoped:
|
|
cpu = int(am.group(1), 16)
|
|
for start, end, old, new in scoped:
|
|
if start <= cpu <= end:
|
|
new_line, n = re.subn(rf"\b{old}\b", new, new_line)
|
|
scoped_changed += n
|
|
lines[i] = new_line
|
|
changed += changed_local[0]
|
|
Path(path).write_text("".join(lines))
|
|
print(f"renamed {changed} label references, {len(label_map)} label defs, "
|
|
f"{len(zp_map)} zero-page names, {scoped_changed} scoped zero-page uses in {path}")
|
|
|
|
|
|
ZP_READ_ONLY = {"lda", "ldx", "ldy", "cmp", "cpx", "cpy", "bit"}
|
|
ZP_WRITE_ONLY = {"sta", "stx", "sty"}
|
|
ZP_READ_WRITE = {"inc", "dec", "asl", "lsr", "rol", "ror"}
|
|
|
|
|
|
def read_write(mn):
|
|
if mn in ZP_READ_ONLY:
|
|
return 1, 0
|
|
if mn in ZP_WRITE_ONLY:
|
|
return 0, 1
|
|
if mn in ZP_READ_WRITE:
|
|
return 1, 1
|
|
return 1, 1
|
|
|
|
|
|
def write_locals(prg_path, notefile, out, *extra):
|
|
"""Rebuild locals.inc from the renamed source, merging the hand-written notes."""
|
|
order, labels, insns = parse(prg_path)
|
|
notes = {}
|
|
addr_of = {}
|
|
for src in (notefile,) + extra:
|
|
if not Path(src).exists():
|
|
continue
|
|
for line in Path(src).read_text().splitlines():
|
|
if not line.strip() or line.startswith("#"):
|
|
continue
|
|
parts = line.split("\t")
|
|
if parts[0] == "zprange": # reuse of one address under a second role
|
|
m = re.match(r"Zp([0-9A-F]{2})$", parts[3])
|
|
if m:
|
|
addr_of[parts[4]] = int(m.group(1), 16)
|
|
continue
|
|
if parts[0] != "zp":
|
|
continue
|
|
notes[parts[2]] = parts[3].strip() if len(parts) > 3 else ""
|
|
m = re.match(r"Zp([0-9A-F]{2})$", parts[1])
|
|
if m:
|
|
addr_of[parts[2]] = int(m.group(1), 16)
|
|
|
|
zp = {}
|
|
ident = re.compile(r"(?<![$\w])[A-Za-z_][A-Za-z0-9_]*")
|
|
defined = set(labels) # label definitions in the source, not RAM
|
|
for rec in insns.values():
|
|
for token in ident.findall(rec["op"]):
|
|
if token in ("x", "y", "a") or token in defined:
|
|
continue
|
|
rec_r, rec_w = read_write(rec["mn"])
|
|
e = zp.setdefault(token, {"r": 0, "w": 0, "where": []})
|
|
e["r"] += rec_r
|
|
e["w"] += rec_w
|
|
if rec["cpu"] not in e["where"]:
|
|
e["where"].append(rec["cpu"])
|
|
|
|
lines = [f"; MUSCLE (U) application-specific RAM aliases - Pass 2",
|
|
"; Generated from every direct zero-page operand in reachable code.",
|
|
"; Names come from observed usage; `ZpXX` marks storage whose role is not yet",
|
|
"; unique enough for a behavioural name. Ref counts are reads/writes and the",
|
|
"; trailing list shows the first referencing addresses.",
|
|
""]
|
|
for name in sorted(zp, key=lambda n: (n.startswith("Zp"), n)):
|
|
e = zp[name]
|
|
total = e["r"] + e["w"]
|
|
m = re.match(r"Zp([0-9A-F]{2})$", name)
|
|
value = int(m.group(1), 16) if m else addr_of.get(name)
|
|
decl = f"{name} = ${value:02X}" if value is not None else name
|
|
where = ", ".join(f"{w:04X}" for w in e["where"][:4])
|
|
if len(e["where"]) > 4:
|
|
where += f", +{len(e['where']) - 4} more"
|
|
note = notes.get(name, "")
|
|
tail = f" ; {note}" if note else ""
|
|
lines.append(f"{decl:<52}; {total:>4} refs; reads={e['r']:<4} "
|
|
f"writes={e['w']:<4} used at ${where}{tail}")
|
|
Path(out).write_text("\n".join(lines) + "\n")
|
|
print(f"wrote {len(zp)} zero-page aliases to {out}")
|
|
|
|
|
|
def reflow(path, column=32):
|
|
"""Re-pad instruction lines so the trailing byte comment keeps its column."""
|
|
lines = Path(path).read_text().splitlines(keepends=True)
|
|
moved = 0
|
|
for i, line in enumerate(lines):
|
|
body, sep, tail = line.partition("; CPU:")
|
|
if not sep:
|
|
continue
|
|
text = body.rstrip()
|
|
new = text.ljust(column - 1)
|
|
if len(new) < len(text) + 1:
|
|
new = text + " "
|
|
lines[i] = f"{new}; CPU:{tail}"
|
|
moved += 1
|
|
Path(path).write_text("".join(lines))
|
|
print(f"reflowed {moved} instruction lines in {path}")
|
|
|
|
|
|
BOILERPLATE = {
|
|
"; Parameters: Undetermined in Pass 1 unless implied by the caller.":
|
|
"; Parameters: Not yet resolved; the callers visible in the static trace show what is"
|
|
" loaded before the call.",
|
|
"; Returns: Undetermined in Pass 1; status flags and registers may carry results.":
|
|
"; Returns: Not yet resolved; status flags and registers may carry results.",
|
|
"; Clobbers: Undetermined in Pass 1.":
|
|
"; Clobbers: Not yet resolved.",
|
|
}
|
|
|
|
|
|
def describe(path, purposefile):
|
|
"""Replace the Pass 1 boilerplate `; Global:` line with a real description.
|
|
|
|
Routines with a hand-written purpose in `purposefile` get it verbatim; the rest get a
|
|
generated line naming the routine and the call sites the static trace can see, which is
|
|
factual rather than boilerplate.
|
|
"""
|
|
purposes = {}
|
|
for line in Path(purposefile).read_text().splitlines():
|
|
if not line.strip() or line.startswith("#"):
|
|
continue
|
|
addr, text = line.split("\t", 1)
|
|
purposes[int(addr, 16)] = text
|
|
order, labels, insns = parse(path)
|
|
by_line = sorted((rec["line"], rec["cpu"]) for rec in insns.values())
|
|
keys = [k for k, _ in by_line]
|
|
name_at = {}
|
|
current = None
|
|
for kind, val in order:
|
|
if kind == "label":
|
|
current = val
|
|
elif kind == "insn":
|
|
name_at[val["line"]] = current
|
|
callers = {}
|
|
for rec in insns.values():
|
|
if rec["mn"] in ("jsr", "jmp"):
|
|
m = re.match(r"^\$([0-9A-F]{4})$", rec["op"])
|
|
if m:
|
|
callers.setdefault(int(m.group(1), 16), []).append((rec["cpu"], rec["mn"]))
|
|
described = generated = 0
|
|
lines = Path(path).read_text().splitlines(keepends=True)
|
|
for i, line in enumerate(lines):
|
|
stripped = line.rstrip("\n")
|
|
if stripped in BOILERPLATE:
|
|
lines[i] = BOILERPLATE[stripped] + "\n"
|
|
described += 1
|
|
continue
|
|
if not line.startswith("; Routine: "):
|
|
continue
|
|
j = bisect.bisect_right(keys, i)
|
|
if j >= len(by_line):
|
|
continue
|
|
cpu = by_line[j][1]
|
|
text = purposes.get(cpu)
|
|
if text is None:
|
|
name = stripped[len("; Routine: "):]
|
|
sites = sorted({c for c, _ in callers.get(cpu, [])})
|
|
where = ("; no call site is visible in the static trace"
|
|
if not sites else
|
|
"; called from " + ", ".join(f"${c:04X}" for c in sites[:8])
|
|
+ (f" and {len(sites) - 8} more" if len(sites) > 8 else ""))
|
|
text = f"{name}.{where}"
|
|
generated += 1
|
|
for k in range(i, min(i + 10, len(lines))):
|
|
if lines[k].startswith("; Global: "):
|
|
lines[k] = f"; Global: {text}\n"
|
|
break
|
|
Path(path).write_text("".join(lines))
|
|
print(f"described {described} boilerplate headers, wrote {generated} generated "
|
|
f"descriptions in {path}")
|
|
|
|
|
|
DATA_HEADER_RE = re.compile(r"^; ---- data \$([0-9A-F]{4})-\$([0-9A-F]{4}) \((\d+) bytes\) ----$")
|
|
BYTE_LINE_RE = re.compile(
|
|
r"^ \.byte ((?:\$[0-9A-F]{2}(?:, )?)+)\s*; CPU:\$([0-9A-F]{4}) PRG:([0-9A-F]{4})\s+"
|
|
r"((?:[0-9A-F]{2} ?)+[0-9A-F]{2})\s*$"
|
|
)
|
|
|
|
|
|
def read_spec(path):
|
|
"""Parse a Pass 3 region spec: addr, end, label, format, comment."""
|
|
rows = []
|
|
for line in Path(path).read_text().splitlines():
|
|
if not line.strip() or line.startswith("#"):
|
|
continue
|
|
parts = line.split("\t")
|
|
rows.append({
|
|
"start": int(parts[0], 16),
|
|
"end": int(parts[1], 16),
|
|
"label": parts[2],
|
|
"fmt": parts[3],
|
|
"comment": parts[4] if len(parts) > 4 else "",
|
|
})
|
|
rows.sort(key=lambda r: r["start"])
|
|
return rows
|
|
|
|
|
|
def structure(path, specfile):
|
|
"""Split Pass 1 data blocks into named, optionally per-record sub-blocks."""
|
|
spec = read_spec(specfile)
|
|
lines = Path(path).read_text().splitlines(keepends=True)
|
|
out = []
|
|
i = 0
|
|
while i < len(lines):
|
|
m = DATA_HEADER_RE.match(lines[i].rstrip("\n"))
|
|
if not m:
|
|
out.append(lines[i])
|
|
i += 1
|
|
continue
|
|
start, end = int(m.group(1), 16), int(m.group(2), 16)
|
|
# collect the .byte body, skipping Pass 1 annotation comments
|
|
body = []
|
|
i += 1
|
|
while i < len(lines):
|
|
stripped = lines[i].rstrip("\n")
|
|
if stripped.startswith("; ---- data"):
|
|
break
|
|
if stripped.startswith(";"):
|
|
i += 1
|
|
continue
|
|
if not BYTE_LINE_RE.match(stripped):
|
|
break
|
|
body.append(lines[i])
|
|
i += 1
|
|
blob = bytearray()
|
|
for ln in body:
|
|
for tok in BYTE_LINE_RE.match(ln.rstrip("\n")).group(1).split(", "):
|
|
blob.append(int(tok.lstrip("$"), 16))
|
|
if len(blob) != end - start + 1:
|
|
raise SystemExit(f"block ${start:04X}: body {len(blob)} != header "
|
|
f"{end - start + 1}")
|
|
out.extend(render_block(blob, start, spec))
|
|
Path(path).write_text("".join(out))
|
|
print(f"structured {len(spec)} data regions in {path}")
|
|
|
|
|
|
def emit_bytes(blob, base, per_line=16):
|
|
lines = []
|
|
for k in range(0, len(blob), per_line):
|
|
chunk = blob[k:k + per_line]
|
|
vals = ", ".join(f"${b:02X}" for b in chunk)
|
|
raw = " ".join(f"{b:02X}" for b in chunk)
|
|
lines.append(f" .byte {vals} ; CPU:${base + k:04X} "
|
|
f"PRG:{base + k - 0x8000:04X} {raw}\n")
|
|
return lines
|
|
|
|
|
|
def render_block(blob, base, spec):
|
|
"""Render one data block as a sequence of named sub-blocks."""
|
|
lines = []
|
|
end = base + len(blob) - 1
|
|
entries = [s for s in spec if s["start"] < base + len(blob) and s["end"] >= base]
|
|
entries.sort(key=lambda s: s["start"])
|
|
cursor = base
|
|
for s in entries:
|
|
if s["start"] > cursor:
|
|
chunk = blob[cursor - base:s["start"] - base]
|
|
lines.append(f"; ---- data ${cursor:04X}-${s['start'] - 1:04X} "
|
|
f"({len(chunk)} bytes) ----\n")
|
|
lines.extend(emit_bytes(chunk, cursor))
|
|
s0 = max(s["start"], base)
|
|
s1 = min(s["end"], end) + 1
|
|
chunk = blob[s0 - base:s1 - base]
|
|
if s["comment"]:
|
|
lines.append(f"; {s['comment']}\n")
|
|
lines.append(f"; ---- data ${s0:04X}-${s1 - 1:04X} ({len(chunk)} bytes) ----\n")
|
|
lines.append(f"{s['label']}:\n")
|
|
if s["fmt"] == "word":
|
|
for k in range(0, len(chunk) - 1, 2):
|
|
lo, hi = chunk[k], chunk[k + 1]
|
|
tgt = lo | (hi << 8)
|
|
raw = f"{lo:02X} {hi:02X}"
|
|
lines.append(f" .word ${tgt:04X} ; entry {k // 2:2d} -> ${tgt:04X}"
|
|
f" ; CPU:${s0 + k:04X} PRG:{s0 + k - 0x8000:04X} {raw}\n")
|
|
elif s["fmt"] == "wordlo":
|
|
for k in range(0, len(chunk), 2):
|
|
lo, hi = chunk[k], chunk[k + 1]
|
|
lines.append(f" .word ${lo | (hi << 8):04X} ; entry {k // 2:2d}"
|
|
f" ; CPU:${s0 + k:04X} PRG:{s0 + k - 0x8000:04X} {lo:02X} {hi:02X}\n")
|
|
else:
|
|
lines.extend(emit_bytes(chunk, s0))
|
|
cursor = s1
|
|
if cursor <= end:
|
|
chunk = blob[cursor - base:]
|
|
lines.append(f"; ---- data ${cursor:04X}-${end:04X} ({len(chunk)} bytes) ----\n")
|
|
lines.extend(emit_bytes(chunk, cursor))
|
|
return lines
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 3:
|
|
sys.exit(__doc__)
|
|
cmd = sys.argv[1]
|
|
if cmd == "outline":
|
|
outline(sys.argv[2] if len(sys.argv) > 2 else "prg.s")
|
|
elif cmd == "show":
|
|
show(sys.argv[2], sys.argv[3])
|
|
elif cmd == "apply":
|
|
apply_map(sys.argv[2], sys.argv[3])
|
|
elif cmd == "reflow":
|
|
reflow(sys.argv[2])
|
|
elif cmd == "describe":
|
|
describe(sys.argv[2], sys.argv[3])
|
|
elif cmd == "structure":
|
|
structure(sys.argv[2], sys.argv[3])
|
|
elif cmd == "locals":
|
|
# locals <prg.s> <notes.tsv> <out.inc>
|
|
write_locals(sys.argv[2], sys.argv[3], sys.argv[4], *sys.argv[5:])
|
|
elif cmd == "derive":
|
|
# derive <prg.s> <routinemap.tsv> <out.tsv>
|
|
path, mapfile, outfile = sys.argv[2], sys.argv[3], sys.argv[4]
|
|
order, labels, insns = parse(path)
|
|
name_map = {}
|
|
for line in Path(mapfile).read_text().splitlines():
|
|
if not line.strip() or line.startswith("#"):
|
|
continue
|
|
addr, name = line.split("\t")
|
|
name_map[int(addr, 16)] = name
|
|
derived = derive(insns, labels, name_map, vectors=(0x8000, 0x86CB, 0x8764))
|
|
rows = [f"label\t{old}\t{new}" for old, new in sorted(derived.items(),
|
|
key=lambda kv: int(kv[0][6:], 16))]
|
|
missing = [r for r in rows if "_Sub_" in r]
|
|
Path(outfile).write_text("\n".join(rows) + "\n")
|
|
print(f"derived {len(rows)} label names -> {outfile}")
|
|
print(f" {len(missing)} still unnamed (no routine in the map)")
|
|
else:
|
|
sys.exit(__doc__)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |