#!/usr/bin/env python3 """Turn a false decode back into data. A `jsr $XXXX` whose target carries no instruction is the tracer having decoded a data byte as a call. The instruction is not code, so it is emitted as the explicit `.byte` row it always was, with the same bytes and the same audit comment - the build cannot change - and the raw operand disappears. The tool only touches a call when the target really has no instruction somewhere in the file; a call into a routine the tracer simply never reached is left alone, because that is a coverage problem and not a data problem. Usage: python3 _tools/fix_false_decodes.py [--write] """ import argparse import re from pathlib import Path INSN = re.compile( r"^ (?P[A-Za-z@][A-Za-z0-9_]*)\s+(?P\S+)\s*; " r"CPU:\$(?P[0-9A-F]{4}) PRG:(?P[0-9A-F]{4})\s+" r"(?P(?:[0-9A-F]{2} ?)+)\s*$") LABEL = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*):\s*$") RAW = re.compile(r"^\$(?:jsr|jmp)\s") CALL = re.compile(r"^\$([0-9A-F]{4})$") def main(): ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) ap.add_argument("project") ap.add_argument("--write", action="store_true") args = ap.parse_args() path = Path(args.project) / "prg.s" lines = path.read_text().split("\n") # every address that carries an instruction have = set() for line in lines: m = INSN.match(line) if m: have.add(int(m.group("cpu"), 16)) out = [] changed = 0 for line in lines: m = INSN.match(line) if m: op = m.group("op") c = CALL.match(op) if m.group("mn") in ("jsr", "jmp") and c and \ int(c.group(1), 16) not in have: # a call into nowhere: the bytes are data raw = m.group("bytes").split() vals = ", ".join(f"${b}" for b in raw) out.append(f" .byte {vals} ; CPU:${m.group('cpu')} " f"PRG:{m.group('prg')} {m.group('bytes').strip()}" f" -- data, not a call") changed += 1 continue out.append(line) print(f"{changed} false decode(s) turned back into data") if args.write and changed: path.write_text("\n".join(out)) print("written") if __name__ == "__main__": main()