Files
BFM-decomp/cookbook/C0229.md
T

3.5 KiB

§208 — TWO NAMED LOCALS FOR ONE RELOADED EXPRESSION BUY TWO ALLOCNOS — the naming granularity owns the REGISTER SPLIT, not just the load count (P31 S58)

Bounds §193-E / §193-H, which own how MANY lws a pointer-derived base emits (one per CSE-live interval / per store-separated run) and name the C local as the only lever over that count. Neither says what happens when you declare two locals for the same expression. Refutes §137's "source-level levers are a dead end for REGALLOC-PERM" for micro-functions.

THE TELL. The target reloads the same expression twice into two different registers:

lw   $v1, 0x20($s0)      <- reload #1
sh   $v0, 0x12($v1)
lw   $v0, 0x20($s0)      <- reload #2, DIFFERENT register
sh   $zero, 0x14($v0)

(✔ byte-checked: asm/ov_SC03_028/.../func_80185028.s idx 5/9 — $v1 then $v0, same 0x20($s0).)

THE LAW. One named local for both reads gives gcc one pseudo; local-alloc coalesces the two reload sites onto one hard register and you get an unfixable clean swap. Two separate locals mint two pseudos ⇒ two allocnos ⇒ two hard registers, restoring the split. The edit is one extra declaration and it changes the pseudo set, not the schedule — which is precisely why §137's two-compile priority arithmetic cannot see it.

function binary residual before edit result
func_80185028 ov_SC03_028 single pointer CSE'd → lost the $v1/$v0 split and the two zero-stores' delay-slot pairing two distinct locals for the two reloads of *(a0+0x20) MATCH 29
func_801801AC ov_SC04_004 REGALLOC-PERM $v1>$a0>$v1 p1/p2 as separate locals (target keeps $a0 for the first — pointer and saved halfword — and $v1 for the reload) MATCH 34
func_801852C8 ov_SC03_006 4 tail diffs; one reassigned local let gcc merge both lw 0x20($a0) into one long-lived pseudo a second pointer local born only after the halfword dies MATCH 15
func_801A7F84 md_SC07_004 $v1/$a0 split between two identical inline reload expressions pin ONE side (register s32 v1 __asm__("$3")) and leave the |= block's temp unpinned MATCH 52

THE READ-BACK RULE. A target that reloads one expression into two different registers is telling you the source had TWO locals, not one reassigned local. func_801852C8's note puts it best: the second lw landing in the halfword's freed register is the diagnostic, and it is in the target bytes, not in your diff.

BOUND 1 — it only fires when the two reads are separated by a store or a call. With nothing between them cse merges the two locals back into one and the extra declaration is inert. BOUND 2 — keeping the FIRST value live across the separator undoes it. func_801852C8 explicitly: a second pin on the first pointer re-merged the loads and cancelled the whole lever.

THE OPPOSITE DIRECTION, for completeness. Where the residual is a value staying live too long rather than a register split, sink the load INTO the if condition: func_80182960 (ov_SC04_002) hoisted *(p+0xC4) into a local first and gcc kept it live across the store, used li instead of ori for 0x8A10, reordered sh 0x52 past the andi and duplicated the epilogue — 31 ins. Written inline in the condition: MATCH.

PROVENANCE. Four independent wave-ac/ad/ae/af cards, four different overlays. func_80185028's two-register reload shape was re-read out of the .s this session.