Files
BFM-decomp/tools/test_o0_detect.py
Drew T 05a4aeaf41 feat(o0): md_MAIN_011 compiles -O0 — 21 functions / 4,321 ins unlocked, byte-proven
Its entire code subseg is the -O0 run and the .c is stub-only, so this is the boot
precedent: a whole-object CC1FLAGS override, no splat change, no carve, and none of
the 18-P29 re-disassembly risk. Proven byte-neutral by a CLEAN per-binary rebuild —
build dirs deleted, re-extract, rebuild:
  sha1 80731bac0ddd6b3e354f43b2c179582b12590752 == config/check.md_MAIN_011.sha

Landed with the coupling fix it requires, or the 21 would have stayed invisible.
Three tools decided -O0-ness from the subseg NAME ('_o0' in it, or 'boot'); this
object keeps its plain name, so match_one would have warned 'cannot bank' about
functions that now bank and the wave draw would have kept refusing to draw them. All
three now ask corpus.o0_subseg(), which derives the answer from the Makefile itself
(R33: a name is a convention, the Makefile is ground truth).

Verified end to end: match_one compiles md_MAIN_011 targets at -O0 with the
cannot-bank warning correctly gone, the wave draw emits cards for them, and
test_o0_detect still passes 167/167 coverage with 0 false positives.
2026-08-24 12:43:40 -06:00

105 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""test_o0_detect.py — negative control for match_one's -O0 auto-detection (P31 S59).
WHY IT EXISTS. `match_one --o0` has always existed, and nothing in the drafting path ever passed
it: `api_draft.match_one()` — the oracle every wave agent iterates against — builds a fixed argv
without it. An agent handed an -O0 target was shown an -O2 compile of its own C and a mismatch on
every instruction, feedback that cannot converge for a reason invisible in the diff. S59 made the
opt level auto-detected. This is the control on that detector.
THE DETECTION LAW (cookbook §6/§18, byte-proven): gcc-2.7.2 keeps a frame pointer at -O0 and omits
it at -O2, so an -O0 function opens with `sw $fp, N($sp)` + `addu $fp, $sp, $zero` (word 21F0A003).
THREE CHECKS:
NC-1 COVERAGE — every .s under asm/ that carries the -O0 signature anywhere is detected as -O0,
either by its prologue or by its subseg being an -O0 object. A miss compiles an -O0 target
at -O2, which is the whole defect.
NC-2 NO FALSE POSITIVES OUTSIDE THE -O0 OBJECTS — a file the signature grep does NOT find may
only be called -O0 if it lives in a subseg the Makefile compiles -O0 (`boot`, `*_o0*`).
Calling an -O2 function -O0 is the same defect with the sign flipped.
NC-3 THE PROLOGUE RULE IS ANCHORED AT THE FUNCTION — the two md_MAIN_011 files whose .s opens
with a migrated jump table / .asciz blob must still be detected. They are the reason the
scan starts at `glabel`, not at the top of the file.
Run: .venv/bin/python tools/test_o0_detect.py
"""
import glob
import os
import re
import subprocess
import sys
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Import detect_o0 without executing match_one's CLI (it parses argv at import time).
_SRC = open(os.path.join(REPO, "tools/match_one.py")).read()
_FN = _SRC[_SRC.index("def detect_o0"):_SRC.index("_sub = os.path.basename")]
_NS = {"re": re, "os": os}
exec(_FN, _NS)
detect_o0 = _NS["detect_o0"]
sys.path.insert(0, os.path.join(REPO, "tools"))
import corpus # noqa: E402
FAIL = []
def sub_is_o0(path):
"""Does the BUILD compile this subseg -O0? Asked of the Makefile, never of the name (R33) —
md_MAIN_011 keeps its plain name and is compiled -O0 by an explicit whole-object rule."""
sub = os.path.basename(os.path.dirname(path))
parts = os.path.normpath(path).split(os.sep)
binary = "main" if (len(parts) > 1 and parts[1] == "nonmatchings") else parts[1]
return corpus.o0_subseg(binary, sub)
def main():
os.chdir(REPO)
files = glob.glob("asm/**/*.s", recursive=True)
grep = set(subprocess.run(
["bash", "-c", "grep -rl 'addu .*\\$fp, *\\$sp\\|21F0A003' asm/ --include=*.s"],
capture_output=True, text=True).stdout.split())
print(f"R32 denominator: {len(files)} .s files under asm/")
print(f"carrying the -O0 signature anywhere in the file: {len(grep)}")
detected = {p for p in files if detect_o0(p) or sub_is_o0(p)}
missed = sorted(grep - detected)
print(f"detected as -O0 (prologue OR -O0 subseg): {len(detected)}")
# NC-1
if missed:
FAIL.append(f"NC-1 COVERAGE: {len(missed)} signature-carrying files not detected: {missed[:5]}")
print(f"NC-1 coverage : {len(grep) - len(missed)}/{len(grep)} signature files detected")
# NC-2
fp = sorted(p for p in detected - grep if not sub_is_o0(p))
if fp:
FAIL.append(f"NC-2 FALSE POSITIVES: {len(fp)} files called -O0 with no signature and no -O0 subseg: {fp[:5]}")
extra_in_o0 = len(detected - grep) - len(fp)
print(f"NC-2 false positives : 0 outside -O0 objects ({extra_in_o0} inside them, which the "
f"Makefile does compile -O0)")
# NC-3
anchored = ["asm/md_MAIN_011/nonmatchings/md_MAIN_011/func_800CF28C.s",
"asm/md_MAIN_011/nonmatchings/md_MAIN_011/func_800D04F4.s"]
for p in anchored:
if not os.path.exists(p):
print(f"NC-3 skipped ({p} absent — asm/ not extracted?)")
break
if not detect_o0(p):
FAIL.append(f"NC-3 {p}: rodata-first .s not detected — the scan is not anchored at glabel")
else:
print(f"NC-3 rodata-first : {len(anchored)}/{len(anchored)} detected (scan anchored at glabel)")
if FAIL:
print("\nFAILED:")
for f in FAIL:
print(" -", f)
return 1
print("\nALL CHECKS PASS")
return 0
if __name__ == "__main__":
sys.exit(main())