Files
BFM-decomp/cookbook/C0055.md
T

9.8 KiB
Raw Blame History

§52 — The WALKER-FAMILY skeleton: 6 regalloc-order levers + a deeply-characterized intrinsic wall (func_80178004, 165 ins ×134; Phase 26, Fable5, 2026-07-15)

func_80178004 is the exemplar of the regalloc-order residual class (12 reach-134 siblings share it: the 9 close=0 + 3 close-21+ cores). A ~93-min / 477k-token Fable5 pass drove a PIN-FREE draft to structure-exact (163 vs 165 ins); residual = pure register identity — byte-verified (match_one 126/165 masked; the 126 is dominated by one $s0↔$s2 swap rippling ~90 lines). It did not byte-match: the wall reduces to three compiler-internal integers and is (probably) intrinsic to vanilla gcc-2.7.2. Correction (R14): the historic "pinned MATCH" was a myth — the pinned seed was NEVER a match (best historic permuter score 5, pinned); this is the deepest state the function has reached. Artifacts + gdb oracles under .run/fable_80178004/ (repro runorc.sh); Fable5-derived, headline byte-verified.

The 6 levers (the "walker-family skeleton" — apply to the 12 siblings; levers 1-5 retire ~half the residual)

  1. Mutated-parameter pointer. Declare the walking pointer AS the mutated parameter (u32 *p=arg0, then p += …). Any p = (u32*)a0 COPY leaves the initial value live in the param pseudo → cse1 rebases every store block onto constant offsets and DELETES the pointer-walk. The single biggest lever; transfers to every walker-style function.
  2. No derived-base variable. Never declare the second pointer — write p[1..3] directly and let loop.c mint a combined DEST_ADDR giv of the biv (anchor = last-recorded giv → the -8/-4/0 offsets; preheader init reads the biv reg). A source-level q biv makes loop.c reduce the (q-4,q-8) pair into an EXTRA pointer (loop.c:3824 worthwhile test).
  3. In-loop constant remat. A constant the target recomputes in-loop must be a user variable assigned in BOTH if/else arms (n_times_set==2 fails scan_loop's movable gate, loop.c:698-712 → stays in-loop). A bare literal is hoisted.
  4. Split the OR across two statements. t = …|0x4000; p[3] = t|0x6d160000; beats tree-level constant reassociation → in-loop ori + a hoisted lui-only temp.
  5. Break biv-recognition. Interpose one statement between n = i+1 and i = n (basic_induction_var follows a copy only to the immediately-previous insn, loop.c:4862) → the target's literal counter survives instead of a <<16 giv.
  6. Diagnose allocation walls with gdb-on-cc1 FIRST. peek2.gdb (allocno_order/find_reg) + a 3-integer oracle (reg_n_deaths/reg_n_refs) tells you in minutes whether a register identity is even reachable — before grinding C.

Why the wall is (probably) intrinsic

The walker is a block-local 1-death qty → local-alloc runs first and hands it the first free callee-saved $s0 (local-alloc.c:472/2103; nothing can pre-occupy s0/s1 for a call-crossing qty). Force it global (deaths≥2) and its priority floor_log2(44)*44/103 ≈ 21000 dominates → allocated first → pass-1 first-fit $s0 again (global.c:924, callee prefs stripped for call-crossers). The target needs pri < 2400 ⇒ effective refs ≤ ~10-12, but REG_N_REFS counts RTL mentions, fixed at 44 by the bytes, and every legal construct only pushes it UP (each ruled out with file:line). One untested lever: instrument qty_n_refs (local-alloc SUMS at tying, local-alloc.c:1869) vs flow's per-reg REG_N_REFS with peek2.gdb on tied-copy chains — if a tie shape yields a low-ref global view of an s2-window value, the wall falls; else it's the ×1-pinned + whole-binary-gate route.

Flywheel note (R16): even walled, this pass paid off — a cheap-Opus wave applying levers 1-5 should crack the siblings that are NOT at the intrinsic wall. Fable5 DISCOVERS the skeleton; cheap-Opus APPLIES it.

§52a — The regalloc sibling wave: new levers + two new wall classes (cheap-Opus applying §52, 2026-07-15)

Ran §52 as a 6-agent Opus wave over the regalloc-order reach-134 cluster: 2/6 banked ×134 (func_80171FFC, func_801775E0 → 268 instances, R22 136/136), the other 4 = precisely-characterized walls. Banks and walls each yielded a byte-verified, reusable lever:

New banking levers (each verified by a whole-binary bank):

  • Pass the callee its real arguments — f((s32)a0,(s32)a1), NOT the void-cast no-arg trick ((T(*)(void))f)(). When params are saved to callee-saved regs before the first call, the void-cast form STRIPS the param pseudos' arg refs → shuffles the $s0/$s1/$s2 order (RC-10 density). Passing the args naturally BOTH suppresses arg-setup moves (params already in place) AND preserves the density order. (func_80171FFC.)
  • Recompute a store-base in BOTH if/else arms (the §52-lever-3 analogue for a base pointer): keeps the recomputed base live across the branch-merge so subsequent p[k] address off it, not gcc address-CSE onto a still-live keeper base. (func_801775E0.)
  • Copy-chain direction: use the incoming param DIRECTLY as the persistent keeper (keep = param+off) with a working copy w = param; gcc saves param→$sK, chains $sJ=$sK. An explicit keeper var reverses it. (func_801775E0.)
  • pp-declaration position drives the prologue schedule (RC-1/K1): declare a param-copy pseudo AFTER the one you want saved first — sched1 emits in pseudo-creation order. (func_801775E0.)
  • A const/RTX_UNCHANGING_P pre-call load frees a sched2 prologue save-order tie (RC-3): f(*(const s32*)(a0+off)) — sched.c:true_dependence drops the load↔store deps so sw $s0/sw $ra stop readying simultaneously → the descending-regno save tie staggers right. BOUNDARY: it can OVER-free (the load then hoists above the saves), so it fixes save-order but not always the whole function. (func_80131A34.)

Two NEW intrinsic-wall classes (byte-characterized, P9 — distinct from §52's flagship $s0 local-alloc wall):

  • Caller-saved priority-first-fit wall. A long-lived block-local value ($v0/$v1/$a0 scratch) stored many times has LOW qty_compare priority (floor_log2(refs)·refs·size/(death−birth)) → loses the low-reg first-fit to its SHORT-lived competitor. §31-B in-out-asm and §47 live-length split TIES only, never a priority GAP, and the competitor can't be lengthened without deleting an instruction the target keeps. Needs pins → ×1-only. (func_80169228, bounded to 3 register identities.)
  • Non-coalescing delay-slot copy wall. A value computed in $vX, tested, then copied to $vY to survive a clobber in the branch delay slot needs two non-coalescing equal-valued pseudos. Every pure-C y=x is destroyed by cse.c canon_reg/make_regs_eqv head-promotion or global-alloc coalescing; the only preserving forms are #APP asm (blocks reorg's delay-slot fill) or a $0-add (SIGABRTs cc1 in sibling TUs, §42e). Intrinsic pin-free/sweep-safe wall. (func_80177AD4, 1 instruction.)

Wave economics: ~⅓ of a fully-walled cluster cracks pin-free by applying the idiom; the rest wall on a small set of distinct, now-named mechanisms. Cracks bank ×134; walls become permuter seeds or documented dead-ends.

§52b — Sibling wave 2: more de-pin levers, a third wall class, and the match_one→whole-binary gap at scale (2026-07-15)

A second 6-agent Opus wave (armed with §52a) over the close=0 regalloc cluster: 3/6 banked ×134 (func_801379FC, func_801497A8, func_801495C4), 2 whole-binary-near, 1 new wall. Additional VERIFIED levers (each proven by a whole-binary ×1 bank):

  • Per-loop pseudos for a register role-swap — when the target uses pointer=$s0/index=$s2 in one loop and the swapped roles in another, a single shared C var can't (one hard reg each). Declare SEPARATE per-loop locals (s32 p; s32 idx; inside each block); the swap falls out of K2 density (the ref-heavier value wins the low callee-saved per block). (func_801379FC.)
  • The RC-7 "second-set" dial — u8 *s = SYM; __asm__("" : "=r"(s) : "0"(s)); makes reg_n_sets(s)==2, failing update_equiv_regs' single-set gate → NO REG_EQUIV → the value is NOT rematerialized at its use → it must hold a callee-saved reg across calls (matching a target that keeps a base in $s1). Zero bytes, sweep-safe (generic "=r"/"0", no $N — distinct from the §42e $0-add). (func_801497A8.)
  • Value-barriers dissolve the CSE-stack-address-common wall — when correct stack-slot order (RC-1 decl order) forces a &buf(sp+off) to be CSE-commoned across two calls into a call-crossing pseudo that steals a callee-saved reg, wrap each &buf use in __asm__("" : "=r"(m) : "0"(&buf)) so cse can't fold them → each rematerialized fresh at its call. STEERABLE (banked), not intrinsic. (func_801495C4; its func_8014964C template used a register pin + 5 barriers — the pin was superfluous.)

A third intrinsic wall class (byte-characterized, P9): the symbol-address-base "wins-low-needs-high" wall (dual of §52a's caller-saved-priority wall). A 2-instruction symbol-address base feeding its own N loads is the densest block-local pseudo → first-fits the LOW reg ($v0), but the target needs it HIGH ($a1); making it low-priority is structurally impossible (a base can't out-rank the loads it feeds), and the movstri form that would place it high triggers the §52-flagship (plus $fp const) local-alloc theft (update_equiv_regs can't rematerialize a non-CONSTANT_P source). Only a register pin resolves both → ×1-only. (func_8012B4B8, close=15.)

Process finding — the match_one→whole-binary gap at wave scale: ~half of the agents' match_one close=0 drafts do NOT bank whole-binary (isolated reloc-masked compile overstates; A10). The whole-binary gate is the sole arbiter — a match_one MATCH is a CANDIDATE, not a bank; budget the gate cycles.