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$v0you must consume (else "void value not ignored"); - a canonical 0-arg prototype where the target sets up
$a0in the delay slot — the cast restores the arg copy (func_80182428,func_80184F68,func_80178D18across 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.