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.