Files
BFM-decomp/cookbook/C0258.md
T

4.1 KiB

§237 — THE CAST-AT-CALL-SITE DECISION TABLE: WHAT §17a-1 FIXES, WHAT IT CANNOT, AND THE FOUR ESCAPES (P31 S58b)

§17a-1's ((s32 (*)(s32))func_X)(a) is the workhorse of this project — it appears in over a hundred banked cards. This harvest bounds it.

IT FIXES (byte-neutral, decl untouched):

  • a void-declared callee whose $v0 you must consume (else "void value not ignored");
  • a canonical 0-arg prototype where the target sets up $a0 in the delay slot — the cast restores the arg copy (func_80182428, func_80184F68, func_80178D18 across a dozen TUs);
  • an s16-returning prototype whose sign-extension pair the target does not have (func_80183258);
  • an extra argument past a (void) prototype (func_801855CC — and there the cast beat an asm-label alias, which compile-failed on conflicting types and changed nothing anyway).

IT DOES NOT FIX — the boundary, byte-measured. func_80180480 (ov_SC07_010): a cast-call does not bypass a conflicting file-scope prototype; gcc-2.7.2 still errors "too many arguments". The working idiom there is a block-scope extern s32 func_8013D13C(s32 a0); shadowing the TU's authoritative (void)-arity decl. Cast fixes types at the call; only a shadowing declaration fixes arity against a hostile prototype in scope.

THE INVERSE ESCAPE — SUPPRESS AN ARGUMENT. func_80187008 (ov_SC04_011): the target's jal func_801805F8 has a bare nop delay slot even though $s0 is live, but the TU's only visible prototype is void func_801805F8(s32), so a plain call emits addu $a0,$s0,$zero into the slot. Fix: a block-scope UNPROTOTYPED extern void func_801805F8();, composite-compatible with the prototyped definition under C89, called with no arguments. Same lever on func_80182EB0's func_80185764 and func_8017F470's func_8017F534 (whose fleet row claims three params but whose own .s sets only $a0; a prototyped decl cost +2 instructions of $a1/$a2 setup).

ESCAPE 2 — THE DEF-SIDE ALIAS (§202), NOW ALSO FOR A RETURN-TYPE CLASH. func_8017EBB8 (ov_SC07_010): the TU declares extern void func_8017EBB8(void*, void*) above the splice point while the body must be s32 f(s32,s32) — a conflict on both axes. Define under an alias identifier with __asm__("func_8017EBB8"); the C identifiers never collide and the emitted symbol is unchanged. Also func_80185C6C (s16-vs-s32 params), func_80184358 (address-taken as a callback under a (void) decl).

ESCAPE 3 — THE IMPLICIT-INT DEFINITION WITH NO return. func_801830F4 (ov_SC02_005): the TU declares extern s32 func_801830F4(s32,s32); at three call sites, but the target never writes $v0 before jr $ra. A void definition is a conflict; s32 … { …; return 0; } emits move $v0,$zero and mismatches. The form that satisfies both is an implicit-int definition with no return statement — C89-compatible with the s32 externs, and gcc-2.7.2 emits no epilogue $v0 write.

ESCAPE 4 — (void)-PARAMETERISED DEFINITION + register __asm__("$4") CAPTURE. Where the TU declares extern void f(void) at live call sites you may not touch, define f(void) and recover the incoming argument with register void *a0r __asm__("$4");. This escape is two-sided: it was byte-identical on func_80182EB0 (ov_SC02_005) and func_80181FA8, but on func_801A1E94 (md_SC07_004) it coalesced the body to 36 instructions — with no declared parameter gcc treated $a0 as scratch, re-homed $s0, and lost both addu $s0,$a0,$zero copies. Probe it; do not assume it. When it fails, the answer is the §236-8 TU edit.

AND THE ARITY EVIDENCE ITSELF. The atlas def row can contradict an observable $v0 consumer (func_8012CBCC prints void(s32) fleet-wide, n≈1653, yet dozens of targets branch on its result; func_8012D624's canonical ('void',('s32',)) drops two argument setups the target has). The asm tell — delay-slot argument setup, a pre-branch load feeding $a2, a bnez on $v0 — outranks the fleet vote at a given call site. The canonical void spelling is a fleet-wide convention, not evidence the engine function returns nothing.