Files
BFM-decomp/cookbook/C0358.md
T

3.6 KiB

§320 — THE §43 "RETURN-TYPE FLIP PAIR" IS NOT TU-EDIT-REQUIRED: THREE DRAFT-ONLY ESCAPES (P31 S66; byte-proven func_800CCBC0, func_800D30D0, func_800D2A24)

(BOUNDS §43 and §183 item 4, which both record this shape as an immovable needing a TU edit. It is not. Three independent agents broke it three different ways in one lane, each byte-proven.)

THE SHAPE. The destination TU declares extern void f(...) — often through a DEFINE_ macro in engine_core.h, often with the sole caller discarding the value — while the target's own asm plainly materialises a result in $v0 on every exit (addu $v0,$zero,$zero in a branch-delay slot, addiu $v0,$zero,1 before the epilogue). Compile the byte-true s32 body and cc1 rejects the TU with conflicting types. Compile it as a true void and gcc-2.7.2 dead-codes exactly those two materialisations — you lose 1-2 instructions and the diff is all downstream of them. §43 called this a pair with no draft-side move. It has three.

ESCAPE 1 — the §202 DEF-SIDE-RETURN alias (cleanest; prefer this). Bind the byte-correct s32 definition to the real link symbol and leave every TU declaration untouched:

s32 aF800CCBC0(void) __asm__("func_800CCBC0");
s32 aF800CCBC0(void) { ... }          /* the byte-true body, unchanged */

Byte-proven twice: func_800CCBC0 (md_MAIN_047, MATCH 138/138 first try) and func_800D30D0 (md_MAIN_003, MATCH 76/76 — which ALSO needed the TU's old-style extern void func_8005C604(); adopted verbatim, because a varargs prototype is incompatible with a no-prototype declaration under C89 6.5.4.3).

ESCAPE 2 — hold $v0 live across a bare return; (when the alias is unavailable). Keep the void signature the TU demands, and materialise the return value into a pinned $v0 that an input-only asm barrier keeps alive:

void func_800D2A24(...) {
    register s32 v0 __asm__("$2");
    ...
    if (early) { v0 = 1; goto ret; }      /* NOT inline in the if body — see the bound */
    v0 = 0;
ret:
    __asm__ __volatile__("" :: "r"(v0));  /* zero bytes; stops the dead-code elimination */
    return;
}

BOUND, measured: the early-exit's v0 = must sit behind a goto to a SHARED exit label. Setting it inline inside the if body bloats that block and flips the branch polarity — NEAR closeness 10 inline vs MATCH closeness 0 via the shared exit.

ESCAPE 3 — adopt the TU's old-style declaration. Where the TU's own forward decl is K&R (extern void func_800CB428();), match THAT rather than writing a full prototype: MIPS passes word-sized args in $a0-$a2 identically, so no cast-at-use is needed (func_800CB1CC, MATCH 47/47; sibling callers in the same TU already call it with 3 args under that same decl).

WHEN A TU EDIT IS STILL THE RIGHT ANSWER — and how to prove it safe. If every call site discards the result, widening the declaration void -> s32 is byte-neutral, and you must MEASURE that, not argue it: apply the edit with NO draft substituted and rebuild — main must still produce 143dbb89f34491258bbc27810d0a12ec8b43a8dd. Done twice for func_8002A544/func_8002A2D4/ func_8002A7B4 in src/800.c; 2 of the 3 then banked. Prefer escapes 1-3 where the TU is shared — the alias has the smaller blast radius.

PROCESS TRAP THAT COST A PASS. gate_main SNAPSHOTS AND RESTORES the destination TU between passes, so an UNCOMMITTED TU edit is reverted before the gate ever sees it. Measured: the first attempt banked 4 of 7 and left all three return-type targets as stubs; committing the (proven neutral) decl change first and re-gating banked 2 of 3.