Files
BFM-decomp/cookbook/C0477.md
T

3.6 KiB

§428a ★★★ — TWO RESIDUALS THAT MOVE IN OPPOSITE DIRECTIONS UNDER EVERY LEVER USUALLY SHARE ONE CAUSE (P31 S72; main/func_8001B0D4, NEAR/53 -> MATCH; my first answer here was WRONG and is kept below as the refutation)

The observation (sonnet, attempt 3, closeness 53, length EXACT 86/86, jtbl byte-verified). The residual was a gcc-2.7.2 regalloc double-hop ($2 -> $a0 -> $v1) in two nested switch dispatchers. Every lever that reliably killed the double-hop — scoping, inline-assign, goto-shared-tail, and §5a volatile __asm__ cross-jump barriers — re-enabled a 2-instruction cross-jump OVER-merge elsewhere. ~20 variants across two attempts, all landing near the same closeness. The two residuals looked like they were in direct tension.

WHAT I PREDICTED, AND IT WAS WRONG. I wrote that the resolution would be §428's UID-based barrier, on the reasoning that it changes no liveness and would therefore move the over-merge WITHOUT touching allocation — "when two residuals move in opposite directions, find the lever that changes only ONE of them." The escalation that tested it did not use §428 at all.

WHAT ACTUALLY UNLOCKED IT — §3-B. Seven in-block return 0; statements were replaced with goto L_ret0; to a single shared tail. Those seven returns were priority-1 hard-$v0 sets; removing them freed $v0 for the D_800747E4 reload and $v1 for CdQueueBusy's result — a single addu, no register pin, no double-hop — and fired all three cross-jumps at once (92 -> 86). One edit, both residuals, opposite directions.

THE REAL LAW. Two residuals that move in opposite directions under every lever are usually not in tension at all: they are two symptoms of one starved resource, and every lever tried so far was paying for one with the other because none of them released the resource. The question to ask is not "which lever moves only one of these?" — it is "what are they both competing for?" Here it was $v0, held hostage by seven hard sets that the C source spelled as an innocuous return 0;.

The diagnostic habit that follows. When A/B search stalls with two coupled residuals, stop generating variants and inventory the hard register sets the source forces — returns, division results, jal return values, anything with a fixed ABI register. A repeated return <const>; inside switch arms is the commonest source-level way to pin $v0 many times over, and folding them to one shared tail is free.

SHARPENED S73 — it is not just a diagnosis, it is a PER-ARM DIAL. main/StreamLoadStateMachine (MATCH 459/459) settled the general form: break vs return 0 is a REGALLOC control you set arm by arm. return 0 keeps the hard-$v0 set live inside that arm, which EXCLUDES $v0 from the allocator's choices there; break to a shared post-switch return 0 removes it and frees the register. On that function case 11 needs return 0 and every other zero-returning arm needs break — one dial, eleven positions, and the correct setting is per-arm rather than global.

So the ladder is: §3-B (fold returns to a shared tail) is the DEFAULT because it frees the register; this section is why (the freed resource resolves coupled residuals); and the dial above is how you put the pin BACK where a single arm needs it. main/func_80035C4C is the same pattern from the other side — case 3 must break while case 4 keeps its explicit return 0, and that one hard set is what blocks the cross-jump over-merge.

Verification (§405-A): all 3 jump tables and 20 relocs byte-verified past match_one's masking before the MATCH was reported.