diff --git a/docs/matching-cookbook.md b/docs/matching-cookbook.md index f829c06426..050ea470af 100644 --- a/docs/matching-cookbook.md +++ b/docs/matching-cookbook.md @@ -2351,7 +2351,7 @@ Cracked **6 of 8** reach-134 §G giants — `func_801571C4` (permuter), `func_80 **BANKING PATTERNS (main + `_a` split):** caller-extern reconcile (`void`→canonical return, 571C4); **asm-label alias** for sibling-decl signedness/proto conflicts (`extern u16 X __asm__("D_x")`, 372B0) — beats reconcile's `*(u16*)&D_x` cast whose address-of PERTURBS regalloc; **canonical call-cast + anonymize the shared `/s` struct** for isomorphic-sibling ports (76D94). **×134 blockers (§A gaps → tool fixes):** register-asm pins + overlay-local *named* types fail `dedup_propagate.compiles_standalone` → bank ×1 (needs a pin/asm self-containment shim + a uniquely-renamed type-lift). -**TOOLING (flywheel):** fixed a silent permuter bug — `tools/p16_permute.py` header comments broke `cpp` → decomp-permuter no-op'd `(0s)` on EVERY commented draft (`strip_c_comments`); NEW `tools/permuter_ils.py` (warm-restart iterated-local-search — descends where a cold run plateaus: 48094 72→29). **Escalation: model to close=2 by hand, THEN directed-permuter the residual** (372B0's last lever fell at permuter iter 291; permuter is low-ROI at close=8). +**TOOLING (flywheel):** fixed a silent permuter bug — `tools/p16_permute.py` header comments broke `cpp` → decomp-permuter no-op'd `(0s)` on EVERY commented draft (`strip_c_comments`); **a SECOND silent no-op of the same class (Phase 26 session 6): a draft whose GTE ops are `#define`s CONTAINING `__asm__` (the PsyQ `inline_c.h` convention — i.e. most renderer code) got its macro DEFINITIONS chewed up by `hide_asm` (which is built for `__asm__` statements / `register` pins and scans to the nearest `;{}`), swallowing the function itself → pycparser `Function not found in base.c` → permuter no-op `(0s)`. Fix: `cpp_expand_macros()` pre-expands with `cpp -P` so each GTE op becomes an inline `__asm__` statement hide_asm can carry — applied ONLY when a `#define ... __asm__` is present, so macro-free drafts are byte-untouched. LESSON: the permuter reporting `no match (0s)` is a TOOLING failure signature, never a real search result — always confirm workers actually ran.** `p16_permute` also gained `--asm-subdir` (it was hardcoded to ov_SC01_077's main object, so no core in another overlay/split object could be permuted at all); NEW `tools/permuter_ils.py` (warm-restart iterated-local-search — descends where a cold run plateaus: 48094 72→29). **Escalation: model to close=2 by hand, THEN directed-permuter the residual** (372B0's last lever fell at permuter iter 291; permuter is low-ROI at close=8). ## §38 — The WHALE `func_80144B9C` (770 ins): the -O0 struct-assign memcpy idiom + the -O0 reach-134 ×134 rollout (Phase 24 T7 §G, cheap Opus — no Fable5, no calls.c) diff --git a/tools/p16_permute.py b/tools/p16_permute.py index a2232a5474..317b90857c 100644 --- a/tools/p16_permute.py +++ b/tools/p16_permute.py @@ -139,11 +139,36 @@ def strip_c_comments(c): return COMMENT_RE.sub("", c) +_DEFINE_ASM_RE = re.compile(r'^\s*#\s*define\b[^\n]*__asm__', re.M) + + +def cpp_expand_macros(c): + """Macro-expand a draft whose GTE ops are `#define`s CONTAINING `__asm__` (the PsyQ `inline_c.h` + convention this codebase uses for RTPT/RTPS/NCLIP/etc). + + `hide_asm` is built for `__asm__` STATEMENTS and `register ... __asm__("$sN")` pins inside a function + body: it scans back to the previous `;`/`{`/`}` and forward to the next top-level `;`. A multi-line + `#define gte_ldv0(r0) __asm__ volatile (...)` has neither boundary where hide_asm expects, so it chews + through the macro DEFINITIONS and swallows the function itself. pycparser then reports "Function + not found in base.c" and decomp-permuter **silently no-ops in 0s** — the same false-negative signature + as the §G comment bug, and it would disable the permuter on every GTE-using draft (i.e. most of the + renderer code). Pre-expanding with cpp turns each GTE op into an inline `__asm__` statement, which + hide_asm then hides correctly via the b64 pragma carrier. + + Applied ONLY when the collision is actually present, so macro-free drafts are byte-untouched.""" + if not _DEFINE_ASM_RE.search(c): + return c + p = subprocess.run(["mipsel-linux-gnu-cpp", "-P", "-nostdinc", "-"], + input=c, capture_output=True, text=True) + return p.stdout if p.returncode == 0 and p.stdout.strip() else c + + def make_base_c(draft_c): """permuter base.c = scalar typedefs + the draft's custom typedefs/#defines/externs + the M2C_FIELD-expanded, asm-hidden body. Keeping externs + custom types is essential: without them the permuter matches in a DIFFERENT context than the whole-binary build (-> winners don't byte-gate).""" body = strip_c_comments(draft_c) + body = cpp_expand_macros(body) # GTE `#define ... __asm__` would otherwise be eaten by hide_asm body = expand_m2c_field(body) body = hide_asm(body) body = drop_preproc_and_scalar_typedefs(body) @@ -226,6 +251,9 @@ def main(): ap.add_argument("--winners", default=".run/permuter-winners") ap.add_argument("--klass", default=None, help="§31 residual class to direct the mutation weights (default: auto-lookup from the backlog)") + ap.add_argument("--asm-subdir", default=ASM, + help="target asm dir (default: ov_SC01_077's main object). A core in another overlay " + "or split object needs its own, e.g. asm/ov_SC01_000/nonmatchings/ov_SC01_000_after") a = ap.parse_args() os.chdir(REPO) os.makedirs(a.winners, exist_ok=True) @@ -237,7 +265,7 @@ def main(): funcs = [] for cf in sorted(glob.glob(a.from_drafts + "/*.c")): fn = os.path.basename(cf)[:-2] - r = sh([PY, "tools/match_one.py", fn, "--c", cf, "--asm-subdir", ASM]) + r = sh([PY, "tools/match_one.py", fn, "--c", cf, "--asm-subdir", a.asm_subdir]) first = (r.stdout.strip().splitlines() or ["?"])[0] m = re.search(r"(\d+) mismatched", first) if m and 1 <= int(m.group(1)) <= a.near_max: @@ -251,7 +279,7 @@ def main(): if not os.path.exists(cf): print(f" [{k}/{len(funcs)}] {fn}: no draft"); continue klass, where = (a.klass, "") if a.klass else klass_for_fn(fn) - pd = setup(fn, open(cf).read(), klass=klass, where=where) + pd = setup(fn, open(cf).read(), asm_subdir=a.asm_subdir, klass=klass, where=where) if not pd: print(f" [{k}/{len(funcs)}] {fn}: setup failed (target.o)"); continue _prof = permuter_weights.classify(klass, where)