#!/usr/bin/env python3 """ Verify Pass 1 (code/data split) across every top-level project. The repo's hard contract is a byte-identical rebuild, and that is what proves the sources are *faithful*: nothing lost, nothing reformatted. It says nothing about whether Pass 1 is *complete* — a project can reproduce all 16,384 bytes perfectly while calling 95% of them data because the tracer gave up early. So there are two independent questions: 1. **Faithful?** Does the source still rebuild byte-identical? 2. **Complete?** Does the committed source claim as much code as a fresh trace can actually reach? Completeness is checked by re-running the tracer and comparing instruction counts. Comparing *lines* rather than bytes keeps this independent of comment style, which matters because the audit-comment format is not uniform across the repo (see CONVENTIONS below). Each project is traced twice: with the 151 official opcodes, and with the 105 undocumented NMOS opcodes added. The gap between the two separates "the tracer was too weak" from "genuinely unreachable". Verdicts -------- OK committed split agrees with the tracer SEEDED tracer finds materially LESS than committed - the project used auto-seed / dispatch-table recovery, so a plain vector trace cannot judge it. Not a defect. TRUNCATED tracer finds materially MORE than committed - Pass 1 stopped early and real code was written out as data. A defect. CONVENTIONS (reported, not enforced) ----------------------------------- AGENTS.md requires every instruction to carry `; CPU:$xxxx PRG:xxxx AA BB`. Several projects predate or ignore that. This tool reports which spelling each project uses so the drift is visible. """ import os import re import subprocess import sys from pathlib import Path sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import nesdis # noqa: E402 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*(?:--.*)?$", 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} )+)"), "colon PRG:$xxxx AA BB": re.compile(r"PRG:\$?([0-9A-Fa-f]{4})\s+((?:[0-9A-Fa-f]{2} )+)"), "slash PRG $xxxx / CPU $xxxx: AA BB": re.compile(r"PRG \$([0-9A-Fa-f]{4})\s*/\s*CPU \$[0-9A-Fa-f]{4}:\s*" r"((?:[0-9A-Fa-f]{2} )+)"), "short $xxxx: AA BB": re.compile(r"\$\$?([0-9A-Fa-f]{4}):\s*((?:[0-9A-Fa-f]{2} )+)"), "bytes PRG:$xxxx Bytes:AA BB": re.compile(r"PRG:\$?[0-9A-Fa-f]{4}\s+Bytes:\s*(?:[0-9A-Fa-f]{2} ?)+"), "rom ROM $xxxxxx: AA BB": re.compile(r"ROM \$([0-9A-Fa-f]{6}):\s*((?:[0-9A-Fa-f]{2} )+)"), } # Every mnemonic the tracer can emit, official and undocumented. ALL_MNEMONICS = (set(m for m, _ in nesdis.OPCODES.values()) | set(m for m, _ in nesdis.UNDOC_OPCODES.values())) def find_source(copilot): for name in ("prg.s", "main.s"): if (copilot / name).is_file(): return copilot / name return None def prg_bytes(copilot): """PRG image: prefer the split copy, else carve it out of rom.nes.""" p = copilot / "prg.bin" if p.is_file(): return p.read_bytes() rom = copilot / "rom.nes" if rom.is_file(): r = rom.read_bytes() return r[16 + (512 if (r[6] & 4) else 0):16 + r[4] * 16384] return None def prg_base(copilot): """CPU address the PRG is mapped at. Most projects link the PRG segment at its real CPU base ($C000, or $8000 for MUSCLE). Bomberman instead links one flat file with the iNES header at $0000 and PRG at $0010, so its linker start is a *file* offset, not a CPU address; fall back to $C000 in that case. """ m = re.search(r"PRG:\s*start\s*=\s*\$([0-9A-Fa-f]+)", (copilot / "linker.cfg").read_text()) if not m: return 0xC000 start = int(m.group(1), 16) return start if start >= 0x8000 else 0xC000 def count_instructions(src): """Instruction lines in the committed source. Comment/directive agnostic.""" n = 0 for raw in src.read_text().splitlines(): s = raw.strip() if not s or s.startswith((";", ".")): continue # Macro invocations and symbol assignments are not instructions. if s.split()[0].lower() in ALL_MNEMONICS: n += 1 return n def audit_form(src): text = src.read_text() hits = {} for label, rx in AUDIT_FORMS.items(): n = len(rx.findall(text)) if n: hits[label] = n if not hits: return "NONE" return max(hits, key=hits.get).split()[0] def builds_byte_identical(copilot): """The faithfulness check: does the project still rebuild its ROM exactly?""" try: r = subprocess.run(["make"], cwd=copilot, capture_output=True, text=True, timeout=600) except subprocess.TimeoutExpired: return "TIMEOUT" out = (r.stdout + r.stderr) if "Byte-identical ROM verified." in out: return "yes" return "NO" def trace_split(prg, base, undoc): """Re-run the Pass 1 tracer. Returns (insn_count, code_bytes, invalid).""" nesdis.set_undocumented(undoc) vo = len(prg) - 6 rst = prg[vo + 2] | (prg[vo + 3] << 8) nmi = prg[vo] | (prg[vo + 1] << 8) irq = prg[vo + 4] | (prg[vo + 5] << 8) insns, invalid, _i, _t, _o = nesdis.trace(prg, base, [rst, nmi, irq]) nesdis.set_undocumented(False) return len(insns), sum(i.length for i in insns.values()), invalid def main(): wanted = sys.argv[1:] projects = sorted( d for d in os.listdir(ROOT) if (ROOT / d / "copilot" / "Makefile").is_file() and (ROOT / d / "copilot" / "prg.bin" or (ROOT / d / "copilot" / "rom.nes").is_file()) and (wanted == [] or d in wanted) ) print(f"{'project':30s} {'build':>6} {'cmt':>6} {'committed':>10} " f"{'official':>10} {'+undoc':>10} verdict") print("-" * 96) flagged, odd_comments = [], [] for name in projects: copilot = ROOT / name / "copilot" src = find_source(copilot) prg = prg_bytes(copilot) base = prg_base(copilot) if src is None or prg is None: print(f"{name:30s} SKIP") continue build = builds_byte_identical(copilot) for junk in copilot.glob("*.o"): junk.unlink() form = audit_form(src) if form != "canonical": odd_comments.append((name, form)) committed = count_instructions(src) off_n, off_b, _ = trace_split(prg, base, False) und_n, und_b, _ = trace_split(prg, base, True) if build != "yes": verdict = f"BUILD-{build}" elif und_n > committed * 1.05 + 16: verdict = "TRUNCATED" elif off_n < committed * 0.95: verdict = "SEEDED" else: verdict = "OK" if verdict not in ("OK", "SEEDED"): flagged.append((name, verdict)) print(f"{name:30s} {build:>6} {form[:6]:>6} {committed:>10} " f"{off_n:>6}/{off_b:<3} {und_n:>5}/{und_b:<4} {verdict}") print() if flagged: print("Needs attention:") for n, v in flagged: print(f" {v:14s} {n}") else: print("No truncated or unfaithful Pass 1 found.") print() print("Comment conventions (AGENTS.md wants `canonical`):") for n, f in odd_comments: print(f" {f:8s} {n}") if not odd_comments: print(" all canonical") return 1 if flagged else 0 if __name__ == "__main__": sys.exit(main())