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.