Files
BFM-decomp/cookbook/C0419.md
T

2.9 KiB

§378 ★★★ — THE SELF-CALLER CAST: LET A TU KEEP CALLING THE FUNCTION IT IS ABOUT TO DEFINE (P31 S69; byte-proven ov_SC04_010/func_8017D6CC)

The mirror image of §20. cast_call_sites fixes the DRAFT calling a conflicting CALLEE. Nothing handled the opposite direction — the TU's own already-banked code calling the function the draft is about to DEFINE — and that is the terminal blocker of the entire §376 "it already matches standalone" pile.

The three-step failure, and why fixing step 1 only reveals step 2:

/* step 1 — the TU carries a forward decl written for its own call site            */
extern void func_8017D6CC(void);          /* draft defines void func_8017D6CC(void *a0) */
        => conflicting types for `func_8017D6CC'

/* step 2 — fix_arity_callers --any-proto no-protos it … and now the DRAFT'S
   DEFINITION is the prototype in scope, so the SAME call site fails differently  */
extern void func_8017D6CC();
    void func_8017DB20(void) { func_8017D6CC(); }
        => too few arguments to function `func_8017D6CC'

/* step 3 — cast the call site to a NO-PROTO function pointer: byte-neutral       */
    void func_8017DB20(void) { ((void (*)())func_8017D6CC)(); }
        => VERIFIED, final SHA BYTE-IDENTICAL

Why a no-proto cast and not a typed one. The call site's ARGUMENT LIST must survive; its argument TYPES are not knowable from the call text. A no-proto pointer accepts any list and applies the default promotions — exactly what the site did before the draft arrived, when it called through a no-proto extern. Inventing types is how every cheaper §20 lever produced a byte MISS instead of a compile error. gcc-2.7.2 folds a cast of a KNOWN function symbol back to a direct jal, so the caller's bytes do not move.

The standing recipe for the whole class (each step reveals the next; do not stop at the first):

tools/fix_arity_callers.py  --binary B --funcs FN --any-proto --apply --journal …   # conflicting types
tools/cast_self_callers.py  --binary B --funcs FN --drafts D   --apply --journal …   # too few arguments
<gate>                                                                               # the sole arbiter

cast_self_callers.py reads the return type off the DRAFT (never guesses it — a guessed return type changes the call's value category, so it REFUSES instead, R43), skips declarations, definitions, INCLUDE_ASM lines and already-cast sites, and journals every edit for an exact --undo-journal --keep <banked> revert of whatever did not buy a match.

The generalizable law. When you move a function's body INTO a TU, you change what every existing call site in that TU is checked against. A draft is not just a body — it is a new prototype imposed on code that already compiled. Expect to fix the callers, and expect the fix to be a cast, because a cast is the only edit that changes typing without changing codegen.