mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 13:33:34 -04:00
2e84b53b2d
Phase-29 opens by running the Phase-28 disambiguating probe before scaling any "(cores)x(reach)" arithmetic on the legacy-PURE-non-jr "~3% as-tooled" swing number. - NEW tools/diff_regions.py (the deferred roadmap tool): remap the exemplar exactly as family_sweep --hseq stages it, compile at the EXEMPLAR's real opt level (auto-detected from the Makefile -O0 rules), masked-diff vs target, classify each member O0-FLAG / TEMPLATES / PLUMBING-ISO / REGALLOC / NO-TARGET. Composes family_remap + match_one + masked_diff (R33). - VERDICT (byte-proven): the 274 DIFF is dominated (~272) by two -O0 families (0x8013c964, 0x8013c938) that family_sweep compiled -O2 (member stub files are -O2); an -O2 compile of an -O0 target can never match. Compiled -O0 they masked-MATCH (func_8013C964->MATCH(10), func_8013C938->MATCH(11)). 106-member sample across nins 2..133: O0-FLAG 45 / already-banked 29 / TEMPLATES 17 / type-lift-plumbing 15 / REGALLOC 0. ZERO codegen walls. - The "~3% ceiling" is RETIRED (a tooling artifact — the 3rd structural wall to resolve to tooling after B2 and SC07). The member track is a mechanical -O0 split rollout + type-lift sweep (Task 2a), NOT a per-member grind; member_adapt.py not needed here. - Honest scope (§52b, R14/R35): masked-MATCH is a CANDIDATE; whole-binary banking is Task 2a's gate. Verdict + byte evidence -> docs/calibration.md + docs/decision-log.md (R31).
229 lines
11 KiB
Python
229 lines
11 KiB
Python
#!/usr/bin/env python3
|
||
"""diff_regions.py — classify WHERE a family-remapped member's compiled bytes diverge from its target.
|
||
|
||
The T3b swing-number probe (Phase-29 Task 1) + the P29–P31 agent-iteration aid (the roadmap's
|
||
deferred P28 tool). Given a MATCHED exemplar and a same-family stub MEMBER, remap the exemplar C to
|
||
the member (`family_remap.remap_hseq` — EXACTLY what `family_sweep --hseq` stages), compile it at the
|
||
EXEMPLAR's real optimization level (a per-location member's target was compiled at the same level as
|
||
its exemplar — same engine function, same per-file opt), and diff the compiled object against the
|
||
member target (`match_one`'s pinned triple + `masked_diff`'s masking oracle), classifying each member:
|
||
|
||
* O0-FLAG — the draft masked-MATCHes at -O0 but not -O2 (or the auto level differs from -O2):
|
||
a COMPILE-FLAG artifact, NOT a codegen wall. Fix = an -O0 split file (the
|
||
`ov_SC01_077_o0` / whale `_o0b` precedent + the Makefile -O0 rule). This is the class
|
||
that made the Phase-28 T3b "~3%" (`family_sweep` compiled -O0 members at -O2).
|
||
* MASKED-MATCH — masked-MATCH at the exemplar's real opt level: the codegen is reproduced. Any
|
||
remaining whole-binary failure can only be a RESOLVED-RELOC (symbol) issue (case a,
|
||
a `family_remap.symbol_map` gap) — confirm on the whole-binary gate.
|
||
* REGALLOC — masked-DIFF at the correct opt level with mismatches in opcode/register bits away
|
||
from reloc slots: genuine TU-context codegen drift (a real per-member wall — §52 work).
|
||
* LEN — instruction-count delta (structural / wrong body).
|
||
* REMAP-REFUSED — `remap_hseq` refuses (STRUCT / unresolved-immediate / symbol-map): not this
|
||
tool's class (family_remap's own pre-filter).
|
||
|
||
The whole-binary byte-gate stays the SOLE arbiter (G3/P9): this classifies a CANDIDATE's residual to
|
||
ROUTE the fix; it never banks. `match_one` is masked/isolated — a MASKED-MATCH is a candidate, not a
|
||
bank (§52b). Reuses masked_diff + match_one + family_remap (composed, not re-implemented — R33).
|
||
|
||
tools/diff_regions.py --family 0x8013c964 [--exemplar-ov ov_SC01_077] [--members 8] [--opt both]
|
||
tools/diff_regions.py --addr 0x8013c964 --member-ov ov_SC01_000 [--member-addr 0x8013c964]
|
||
"""
|
||
import argparse
|
||
import glob
|
||
import json
|
||
import os
|
||
import re
|
||
import subprocess
|
||
import sys
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
import family_remap as FR
|
||
import masked_diff
|
||
|
||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
PY = ".venv/bin/python"
|
||
WORKROOT = ".run/diff_regions"
|
||
|
||
_O0_SET = None
|
||
|
||
|
||
def o0_srcfiles():
|
||
"""The set of src/*.c files the build compiles at -O0, DERIVED from the Makefile's own
|
||
`-O0` object rules (R33 — never a hand-kept list, which rots as splits are added). Two shapes:
|
||
explicit `build/src/X.o: CC1FLAGS := … -O0 …` targets, and a wildcard over `src/ov_*/ov_*_o0b.c`
|
||
(the whale rollout). Returns (explicit_paths:set, matches_o0b:callable)."""
|
||
global _O0_SET
|
||
if _O0_SET is None:
|
||
mk = open(os.path.join(REPO, "Makefile")).read()
|
||
explicit = set()
|
||
for m in re.finditer(r"^build/(src/\S+?\.o):\s*CC1FLAGS\s*:=[^\n]*-O0\b", mk, re.M):
|
||
explicit.add(m.group(1)[:-2] + ".c") # build/src/X.o -> src/X.c
|
||
o0b = bool(re.search(r"WHALE_O0B_OBJS\s*:=.*ov_\*_o0b\.c", mk))
|
||
_O0_SET = (explicit, o0b)
|
||
return _O0_SET
|
||
|
||
|
||
def is_o0(src_rel):
|
||
"""True iff the build compiles this src/*.c at -O0 (per the Makefile)."""
|
||
if not src_rel:
|
||
return False
|
||
explicit, o0b = o0_srcfiles()
|
||
base = src_rel.replace("\\", "/")
|
||
if base in explicit:
|
||
return True
|
||
return o0b and re.search(r"/ov_[^/]*_o0b\.c$", "/" + base) is not None
|
||
|
||
|
||
def exemplar_srcfile(ex_ov, addr):
|
||
"""the src file where the exemplar's def lives (via extract_unit's own resolution). Returns the
|
||
.c path (or a src/shared/*.h for a macro-lifted body). Used only to detect the -O0 opt level."""
|
||
_unit, cf = FR.extract_unit(ex_ov, addr)
|
||
return cf
|
||
|
||
|
||
def member_subdir(mem_ov, addr):
|
||
"""the asm subdir holding this member's target .s (globbed). None if the member has no .s."""
|
||
hits = glob.glob(os.path.join(REPO, f"asm/{mem_ov}/nonmatchings/*/func_{addr:08X}.s"))
|
||
if not hits:
|
||
return None
|
||
# asm/<ov>/nonmatchings/<subdir>/func_X.s -> asm/<ov>/nonmatchings/<subdir>
|
||
return os.path.dirname(os.path.relpath(hits[0], REPO))
|
||
|
||
|
||
def _run_match_one(fn, cfile, subdir, o0, wd):
|
||
"""compile the draft standalone (match_one's pinned triple) at the given opt, masked-compare to the
|
||
target .s. Returns (status, ndiff, nmine, ntgt, reason). status in {MATCH, DIFF, FAIL}. On FAIL,
|
||
`reason` is the first cc1/cpp error line (to distinguish an ISOLATION plumbing fail — an undeclared
|
||
shared type the real TU carries — from a hard fail; this tool compiles ISOLATED so it cannot see
|
||
src/shared/engine_types.h, exactly the blind spot family_sweep --no-preclassify + the real-TU gate
|
||
resolve)."""
|
||
cmd = [PY, "tools/match_one.py", fn, "--c", cfile, "--asm-subdir", subdir, "--work", wd]
|
||
if o0:
|
||
cmd.append("--o0")
|
||
r = subprocess.run(cmd, capture_output=True, text=True, cwd=REPO)
|
||
out = r.stdout.strip()
|
||
m = re.search(r"MATCH \((\d+) ins\)", out)
|
||
if m:
|
||
return "MATCH", 0, int(m.group(1)), int(m.group(1)), ""
|
||
m = re.search(r"DIFF\s+\S+\s+mine=(\d+) ins, target=(\d+) ins, (\d+) mismatched", out)
|
||
if m:
|
||
return "DIFF", int(m.group(3)), int(m.group(1)), int(m.group(2)), ""
|
||
# CPP/CC1/AS fail — surface the first real error line (skip the "FAIL\n<stage> FAIL" header)
|
||
err = next((ln for ln in out.splitlines() if re.search(r"error|undeclared|parse", ln)), out[:120])
|
||
return "FAIL", -1, 0, 0, err
|
||
|
||
|
||
def probe_member(ex_ov, ex_addr, mem_ov, mem_addr, opt_mode="both"):
|
||
"""Remap the exemplar to the member and classify. opt_mode: 'auto' (exemplar's level only),
|
||
'both' (compile -O0 AND -O2 to detect the O0-FLAG class). Returns a result dict."""
|
||
fn = f"func_{mem_addr:08X}"
|
||
draft, info = FR.remap_hseq(ex_addr, ex_ov, mem_ov, mem_addr)
|
||
if draft is None:
|
||
return {"member": f"{mem_ov}@{mem_addr:08X}", "class": "REMAP-REFUSED", "why": info}
|
||
subdir = member_subdir(mem_ov, mem_addr)
|
||
if subdir is None:
|
||
return {"member": f"{mem_ov}@{mem_addr:08X}", "class": "NO-TARGET",
|
||
"why": "no target .s (member already matched?)"}
|
||
wd_base = os.path.join(WORKROOT, f"{mem_ov}_{mem_addr:08X}")
|
||
cfile = os.path.join(REPO, wd_base + ".c")
|
||
os.makedirs(os.path.dirname(cfile), exist_ok=True)
|
||
open(cfile, "w").write(draft + "\n")
|
||
|
||
ex_o0 = is_o0(exemplar_srcfile(ex_ov, ex_addr))
|
||
res = {"member": f"{mem_ov}@{mem_addr:08X}", "exemplar_opt": "O0" if ex_o0 else "O2",
|
||
"symbol_map": info.get("symbol_map"), "subdir": subdir}
|
||
|
||
levels = {}
|
||
want = [ex_o0] if opt_mode == "auto" else [True, False] # both: -O0 and -O2
|
||
for o0 in want:
|
||
st, nd, nmine, ntgt, why = _run_match_one(fn, cfile, subdir, o0, wd_base + ("_o0" if o0 else "_o2"))
|
||
levels["O0" if o0 else "O2"] = {"status": st, "ndiff": nd, "mine": nmine, "tgt": ntgt, "why": why}
|
||
res["levels"] = levels
|
||
|
||
auto = levels["O0" if ex_o0 else "O2"]
|
||
other = levels.get("O2" if ex_o0 else "O0")
|
||
if auto["status"] == "MATCH":
|
||
# matches at the exemplar's real opt. If it needs the OTHER (wrong-for-the-sweep) opt, the whole
|
||
# class was the -O0 compile-flag artifact (the sweep stages into the member's -O2 stub file).
|
||
res["class"] = ("O0-FLAG" if ex_o0 else "TEMPLATES") if (other and other["status"] != "MATCH") \
|
||
else "TEMPLATES"
|
||
elif auto["status"] == "FAIL":
|
||
# This tool compiles ISOLATED, so an undeclared shared type / carried-extern parse error is a
|
||
# match_one blind spot the real TU (which #includes engine_types.h) does not have — a recoverable
|
||
# §40a/§8d plumbing class, NOT a codegen wall. Label it so, honestly (R35).
|
||
w = auto.get("why", "")
|
||
res["why"] = w
|
||
res["class"] = "PLUMBING-ISO" if re.search(r"undeclared|parse error before", w) else "COMPILE-FAIL"
|
||
elif auto["mine"] != auto["tgt"]:
|
||
res["class"] = "LEN"
|
||
else:
|
||
res["class"] = "REGALLOC" # masked-diff, non-reloc bits differ = a real wall
|
||
return res
|
||
|
||
|
||
def load_family(addr, exemplar_ov=None):
|
||
"""the family_hseq record for exemplar addr (optionally pinned to an overlay). Returns the dict."""
|
||
d = json.load(open(os.path.join(REPO, ".run/family_hseq.json")))
|
||
fams = d if isinstance(d, list) else d.get("families", d)
|
||
for f in fams:
|
||
ex = f.get("exemplar", {})
|
||
if int(ex.get("addr", "0x0"), 16) == addr and (exemplar_ov is None or ex.get("ov") == exemplar_ov):
|
||
return f
|
||
return None
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||
ap.add_argument("--family", help="exemplar addr (hex) — pull members from .run/family_hseq.json")
|
||
ap.add_argument("--exemplar-ov", default=None)
|
||
ap.add_argument("--members", type=int, default=8, help="how many stub members to probe (--family)")
|
||
ap.add_argument("--addr", help="exemplar addr (hex) for a single explicit --member-ov probe")
|
||
ap.add_argument("--member-ov")
|
||
ap.add_argument("--member-addr", default=None)
|
||
ap.add_argument("--opt", choices=["auto", "both"], default="both")
|
||
ap.add_argument("--json", action="store_true", help="emit the raw result list as JSON")
|
||
a = ap.parse_args()
|
||
os.chdir(REPO)
|
||
|
||
results = []
|
||
if a.addr and a.member_ov:
|
||
ex_addr = int(a.addr, 16)
|
||
mem_addr = int(a.member_addr, 16) if a.member_addr else ex_addr
|
||
ex_ov = a.exemplar_ov or "ov_SC01_077"
|
||
results.append(probe_member(ex_ov, ex_addr, a.member_ov, mem_addr, a.opt))
|
||
elif a.family:
|
||
ex_addr = int(a.family, 16)
|
||
fam = load_family(ex_addr, a.exemplar_ov)
|
||
if not fam:
|
||
print(f"no family_hseq record for {a.family}"); sys.exit(2)
|
||
ex_ov = fam["exemplar"]["ov"]
|
||
print(f"family func_{ex_addr:08X} @ {ex_ov}: nins={fam.get('nins')} band={fam.get('band')} "
|
||
f"diff_class={fam.get('diff_class')} has_mid_jr={fam.get('has_mid_jr')} "
|
||
f"n_members={fam.get('n_members')} exemplar_opt="
|
||
f"{'O0' if is_o0(exemplar_srcfile(ex_ov, ex_addr)) else 'O2'}")
|
||
n = 0
|
||
for mov, addr_s in fam["members"]:
|
||
if n >= a.members:
|
||
break
|
||
maddr = int(addr_s, 16)
|
||
if (mov, maddr) == (ex_ov, ex_addr):
|
||
continue
|
||
r = probe_member(ex_ov, ex_addr, mov, maddr, a.opt)
|
||
results.append(r)
|
||
print(f" {r['member']:24s} {r['class']:14s} "
|
||
+ " ".join(f"{k}:{v['status']}({v['ndiff']})" for k, v in r.get("levels", {}).items())
|
||
+ (f" [{r.get('why','')}]" if r["class"] in ("REMAP-REFUSED", "NO-TARGET") else ""))
|
||
n += 1
|
||
else:
|
||
ap.error("give --family 0xADDR or --addr 0xADDR --member-ov ov_X")
|
||
|
||
from collections import Counter
|
||
dist = Counter(r["class"] for r in results)
|
||
print(f"\nDISTRIBUTION ({len(results)} members): {dict(dist)}")
|
||
if a.json:
|
||
print(json.dumps(results, indent=2))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|