Files
BFM-decomp/cookbook/C0313.md
T

27 KiB

From ck + cl + cm



ADDENDUM — §NNN — sharpens §55a / §164-37 / §165-28 (switch-vs-tree cluster): A SHAPE THAT LOOKS LIKE A SWITCH DISPATCH CAN BE PLAIN NESTED ifs WHOSE SHARED BODY WAS TRIPLICATED BY THE SOURCE AND THEN CROSS-JUMP-MERGED BACK DOWN

Function. func_801F06B0 (md_SC03_076, 33/33 ins). Reported independently in wave ck (worked out from first principles) and wave cm (confirmed by finding the actual cross-binary twin, md_SC04_027:func_801E8D70, already banked).

Symptom. The Ghidra seed reads as switch (a1) { case 0: ...; case 5: ...; default: ...; } — a plausible 2-case dispatch. §55a/§163c/§165-28 already state that gcc-2.7.2 compiles anything below CASE_VALUES_THRESHOLD (5) as a flat equality-test tree, not a jump table — but a naive C switch with exactly this case set still compiles to a SHORTER, flatter tree (measured 26 ins: beqz/li 5/beq) than the target's 33 ins. The extra length is not a scheduling residual; it is a shape error.

Mechanism. The target's 33 ins decode as three copies of ONE shared compare-and-load body, spread across a slti/bgtz root and an inner a1 != 0 test, with gcc's cross-jump pass (§5a family) merging two of the three copies back into shared code. The real source is closer to if ((a1 > 0 && a1 < 5) || (a1 != 0 && a1 != 5)) { ...shared body... } than to any switch/case construct — the "switch" reading was a plausible-looking misdirection from the seed, not evidence of a dispatch at all. The cm instance found the actual mechanism by locating the real twin: md_SC04_027's already- banked func_801E8D70 at the same shape (33 ins, same D_80115158 table, same caller pattern), findable only by grepping asm/ for the shared symbol + call shape (D_80115158[ + * 2), not by name — which is exactly §238's documented "positive use" (grep asm/ by encoding/shape, not src/ by name) applied cross-binary to a same-family md_* module rather than an overlay.

Why ADDENDUM, not NEW. The cross-binary-twin-discovery half is already §238's positive-use case, just with an md_* module standing in for an overlay — not a new mechanism. What is worth adding as a sharpening of the §55a/§164-37/§165-28 cluster is the negative caution the cluster doesn't currently carry: a case-labeled shape under CASE_VALUES_THRESHOLD that is LONGER than the flat tree its own apparent case set would produce is a structural tell (wrong C construct entirely, not a missing transform) — check the instruction count against the naive flat-tree prediction before trusting a switch reading at all, the same discipline §162a3/§164-37 already apply to jump-table dispatch shapes but do not currently state for the below-threshold tree route.



ADDENDUM — §NNN — sharpens §167-13's boundary: CHAINING TWO IDENTICAL SIDE-BY-SIDE STORES INTO ONE C ASSIGNMENT STATEMENT IS A MID-BLOCK SCHEDULING-PRIORITY DIAL, NOT ONLY A STORE-ORDER SPELLING

Function. func_80181BE0 (ov_SC03_112, 51/51 ins). Reproduced across all three waves (ck 8 oracle calls, cl 2, cm 1) with a consistent finding each time; cl/cm's later passes re-confirm rather than add.

Symptom. A mid-block argument-setup instruction (move $a0,$s1 feeding a call to func_8012AD80) schedules one sra/addiu pair earlier than the target places it — a SCHEDULE-REORDER residual that plain statement reordering, named temps, an asm barrier on the parameter, and a decl-order swap all failed to fix (measured and rejected in the ck note).

Mechanism / fix, byte-verified against the target's own relocation lines (7 sites): the target stores two adjacent halfwords with the SAME value (*(s16*)(b+0x18) and *(s16*)(b+0x1C)). Writing them as two separate C statements leaves the mid-block arg-setup instruction scheduled where it was; writing them as ONE chained assignment — *(s16*)(b+0x18) = *(s16*)(b+0x1C) = expr; — shifts that store pair's priority/LUID enough that the arg-setup instruction sinks below it, matching the target exactly.

Why ADDENDUM, not NEW. §167-13 already documents a related but distinct fact — "a leading p1 = a1; terminates rather than rides the bb0 parameter pin, and the direction/pass/dose were previously misstated" — but its own scope is explicitly the FUNCTION-HEAD parameter-pin weave, not an ARBITRARY mid-block pair of stores. This candidate's own self-citation ("not in cookbook under 'head-crack'; §167-13 covers prologue-pair weaving, not mid-block") is accurate: the corpus has no entry for chained-assignment-as-a-mid-block-priority-dial specifically. Recorded as ADDENDUM rather than NEW because it is best read as extending §167-13's family (LUID/priority manipulation via statement chaining) into a region §167-13 explicitly disclaims covering, and because the underlying compiler mechanism (why chaining changes LUID assignment) was not independently re-derived from source here — only the C-level lever and its effect were confirmed, three times, against the bytes.



ADDENDUM — §NNN — sharpens §162a3/§60b: A sll $v0,16 / sltiu $v0,1 ZERO-TEST OF A JUST-DECREMENTED HALFWORD IS A SIGNED-TEMP WIDTH TELL OUTSIDE ANY SWITCH/RANGE-TEST CONTEXT

Function. func_80186C3C (ov_SC06_029, 19/19 ins). Reproduced identically in waves cl and cm.

Byte evidence (asm/ov_SC06_029/nonmatchings/ov_SC06_029_jr_8017C954/func_80186C3C.s):

lhu   $v0, 0x10A($s0)
addiu $v0, $v0, -1
sh    $v0, 0x10A($s0)
sll   $v0, $v0, 16      ; NO following sra
sltiu $v0, $v0, 1

The field is loaded/stored u16 (lhu/sh — it must stay unsigned for the store), but the post-decrement zero-test uses sll $v0,16 immediately followed by sltiu $v0,1 (true only when the shifted 32-bit value is exactly 0, i.e. when the low 16 bits were 0) rather than the more obvious andi $v0,0xffff / sltu $zero,$v0 masking idiom. This spelling is produced by declaring the DECREMENT TEMPORARY s16 (signed) even though the memory access itself must stay u16/lhu/sh to match — an u16 temp instead emits the andi-based zero test.

Why ADDENDUM, not COVERED. §162a3/§164-37/§165-28's sll 16 / sra 16 family is specifically about a SWITCH DISPATCH index (a value about to feed a sltiu-bounded jump table or beq chain against table edges) — none of their text or examples cover a plain post-decrement ==0 test with no dispatch anywhere nearby, and — noted as a discriminator, not a coincidence — this instance has NO trailing sra, unlike the switch-index family's sll 16 ; sra 16 pair (this is a one-sided shift used purely as a cheap "are the low 16 bits zero" test via sltiu, not a sign-extension). §60b's "sltiu==!x" observation is the nearest neighbor but does not connect it to the signed/unsigned temp-declaration choice. Grepped the corpus for this exact combination outside switch-dispatch contexts — no hits.



ADDENDUM — §NNN — sharpens §37/§124 (unspecified-parameter-list family): DERIVE A CALLEE'S ARITY LOWER BOUND FROM ITS OWN .s STACK-ARGUMENT READS, NOT FROM ANY SINGLE CALL SITE

Function. func_801A8DCC (md_SC07_004, 26/26 ins). Reproduced in waves cl and cm; the callee in question, func_801A8E34, is itself still INCLUDE_ASM (no C source or documented signature exists anywhere in the tree).

Mechanism, byte-verified against asm/md_SC07_004/nonmatchings/md_SC07_004/func_801A8E34.s: the callee's own prologue subtracts 0x68 from $sp (addiu $sp,$sp,-0x68), and its body later executes lw $s6, 0x78($sp) — a read at an offset ABOVE its own frame (0x78 > 0x68), which can only resolve to a slot the CALLER set up before the call (an incoming stack argument, per §217/§217-CROSS- CONFIRMED's "the incoming home slot is the mirror" pattern, already documented for the symmetric case of reading a caller-relative offset before sw $ra). Since func_801A8DCC itself only ever passes 5 arguments to this callee, and the true callee reads at least one argument beyond the four register-passed slots, the only C spelling that is safe against every caller (this one included, which under-supplies the true arity) is the K&R unspecified parameter list — extern void func_801A8E34(); — never a guessed fixed-arity prototype.

Why ADDENDUM, not NEW. The unspecified-parameter-list rule itself ("? is compatible with any prototype and never a conflict") is already well established and repeatedly cited by name across this corpus (§37, §124, and the drafting notes' own "card decl law" references) — this is not new. What was previously undocumented is the SPECIFIC TECHNIQUE for deciding, from evidence, that a plain guess is unsafe: read the callee's OWN .s for a stack-argument reference beyond its incoming-register budget, using its declared frame size as the zero point. Could not independently verify the exact argument COUNT the note claims (it says "sixth argument"; by the standard MIPS o32-style layout used elsewhere in this corpus — where the first stack argument lands at the caller's $sp+0x10 — the offset measured here reads as the callee's fifth argument, not sixth). The count is not load-bearing for the law (unspecified-list is safe regardless of the exact number), so this is flagged in the "could not verify" section rather than asserted either way.



ADDENDUM to §5a (func_80181F74, ov_SC03_112, wave cn)

A CROSS-JUMP TWIN BLOCK GUARDED BY ITS OWN if NEEDS THE BARRIER IN THE ELSE-CONTINUATION, NOT AT THE BLOCK HEAD AND NOT INSIDE THE GUARD'S THEN-ARM

§5a's own recipe places the zero-byte volatile-asm barrier "after the last store, before the return" — written for a flat twin block (a sequence of stores ending in return c;). func_80181F74 (ov_SC03_112, MATCH 196/196, banked at src/ov_SC03_112/ov_SC03_112_jr_80181F74.c) shows a twin block that is itself an if-guarded counter (*(s32*)(p+0x1C) += 1; if (counter >= 0x10) goto C78;), and §5a's placement instruction does not say where inside a guarded twin the barrier goes. Three placements were probed and only one works:

  • Barrier above the shared suffix (top of the block): find_cross_jump's backward scan starts at the fall-through edge below the barrier and merges the whole suffix anyway — the barrier never gets compared because the scan never walks up into it.
  • Barrier inside the THEN-arm of if (>= 0x10): kills the merge, but jump.c's block layout then re-lays the branch as bnez-into-the-guarded-arm, flipping the target's beqz-into-C78 polarity — a BRANCH-POLARITY residual traded for the LENGTH-DRIFT it fixed.
  • Barrier in the ELSE-continuation, immediately after the guard, before the shared tail goto — the only spelling that reproduces the target exactly:
case 3:
    *(s32 *)(param_1 + 0x1c) += 1;
    if (*(s32 *)(param_1 + 0x1c) >= 0x10) {
        goto C78;
    }
    __asm__ __volatile__("");
    goto TAIL;

The sibling twin block (case 1) has the byte-identical 7-instruction shape but carries no barrier — adding one to only ONE of a pair of would-be-identical tails is what defeats rtx_renumbered_equal_p's suffix comparison; both blocks do not need the barrier, only one.

Evidence. asm/ov_SC03_112/nonmatchings/ov_SC03_112_jr_80181F74/func_80181F74.s; banked source src/ov_SC03_112/ov_SC03_112_jr_80181F74.c:2748-2864, case 3: at line ~2825 carries the __asm__ __volatile__(""); exactly as shown above; case 1: (line ~2789) has the same += 1; if (>= 0x10) goto C78; goto TAIL; shape with no barrier.

When it applies. Two candidate cross-jump twins are each an if-guarded tail (not a flat store-sequence) sharing the SAME guard shape and SAME post-guard fall-through. Place the barrier as the LAST statement of the fall-through continuation on exactly one of the two candidates, after the guard's own branch and before the tail jump — not at the block's head (the scan walks past it) and not inside the guard's taken-arm (it changes which arm is fall-through and flips branch polarity instead of just blocking the merge).



ADDENDUM to §265 (func_8017E26C, ov_SC04_016, wave cn)

THE void f() { __asm__(...); } WRAPPER FORM MUST END THE STRING AT .set reorder — A TRANSCRIBED jr $ra/nop DUPLICATES cc1's OWN AUTO-GENERATED FUNCTION EPILOGUE

§265's C-definition-wrapper recipe (form 2) already documents transcribing a handwritten .s verbatim inside a void f() { __asm__ __volatile__(...); } body. What it does not say: for THIS wrapper form, cc1 still owns the function's own epilogue (the jr $ra/nop pair that closes out the C function), because from cc1's point of view the function body is a single asm statement followed by an implicit fall-off-the-end return. If the transcribed string ALSO includes its own trailing jr $ra/nop (copied straight from the target .s's tail, which is exactly what "transcribe verbatim" invites), the two epilogues stack — the target's return sequence appears twice. The fix is to transcribe everything EXCEPT the final jr $ra/nop, ending the string at .set reorder and letting cc1 emit its own closing return.

Byte evidence. func_8017E26C (ov_SC04_016, banked verbatim at src/ov_SC04_016/ov_SC04_016_jr_8017BEBC.c:3534-3813). The asm string's own hand-transcribed prologue/epilogue supplies sw $ra,80($sp) / addiu $sp,$sp,-88 at the head and lw $ra,80($sp) / addiu $sp,$sp,88 at the tail (both present, both load-bearing — this function saves/restores its own frame inside the string), but the string ends at ".set\treorder\n" with no explicit jr $ra/nop — those two instructions are left for cc1's own function-closing codegen to emit. Confirmed by reading the source directly (lines 3808-3813).

Toolchain facts confirmed alongside this (§265 already states the first; the rest are additions):

  • MASPSX rejects hex immediates inside the string — decimal only (already in §265).
  • %hi/%lo use a single %; a doubled %%hi is rejected by the assembler.
  • A GTE control-register operand must be written by its numeric form ($31), not the pseudo-name form ($12) — cfc2 $12,31 is rejected, cfc2 $12,$31 (destination GPR, numeric CP2 control reg) is accepted.

When it applies. Any §265 lane-2 (void f() { asm(...); }) transcription. Read the target .s's own trailing jr $ra/nop as belonging to cc1's auto-generated epilogue, not to the payload to transcribe — leave it out and end the string at .set reorder, exactly as func_800CBA44's existing precedent already does (cited by §265, but the WHY was not stated).



ADDENDUM to §42b (func_8018247C, ov_SC07_002, waves cu + cw)

A "DID THE SPLICE ADD A NEW COMPILER DIAGNOSTIC" DIFF IS A CHEAP PRE-GATE SANITY CHECK FOR A DECLARATION-LAYER CONFLICT — DOCUMENTED ONLY AS ONE FUNCTION'S OWN BANKED COMMENT, NOWHERE IN THE COOKBOOK OR INDEX

§42b's mandatory gate shape (git checkout → splice → rm the split .o → make build with an asserted exit code → sha1sum vs config/check.<binary>.sha) is the real, sole arbiter — that does not change. What is missing is a pre-gate diagnostic for narrowing down WHY a byte-exact, match_one-MATCH draft keeps failing that real gate: diff the whole-TU compiler stderr with and without the splice. If the splice adds zero new diagnostic lines over the untouched-TU baseline, the declaration-layer classes §236/§61a/§138 name are very likely closed (necessary evidence, not by itself sufficient — the gate is still the sha1 check) — narrower framing than the candidate note's own "diagnostic parity IS the bank test," which overstates a diagnostic heuristic as if it were the bank criterion itself.

Evidence this technique is real and already practiced in-tree (not merely proposed): src/ov_SC07_002/ov_SC07_002_jr_8017C8D0.c:6737-6845, the banked comment on func_801849AC, states verbatim: "Verified by splicing this body over the INCLUDE_ASM at TU L3917 and compiling the whole TU: same 7 stderr lines as the untouched TU, and func_801849AC is byte-identical." This is a real, on-disk precedent of a prior session doing exactly this diff before trusting a splice — confirmed by reading the comment directly, not inferred.

When it applies. A card that is match_one-MATCH and byte-stable across many resubmissions but keeps failing the whole-binary gate, where every checkable §236/§61a/§138 declaration-layer class comes back clean by static reading. Before spending another session re-deriving the C: build the untouched TU once, build the spliced TU once, and diff cc1's stderr line-for-line. New lines pinpoint the exact conflicting declaration (or its absence rules out the declaration layer as the cause, redirecting suspicion to sibling-card / batch-gating mechanics per §176b).



ADDENDUM to §199-F family (func_8017EFB0, ov_SC02_021, wave cu)

RESTRUCTURING THE ENCLOSING GUARD FROM "WRAP THE BODY IN if" TO "EARLY RETURN" CAN FILL AN UNRELATED, DOWNSTREAM CONDITIONAL BRANCH'S DELAY SLOT — A CFG-SHAPE LEVER, NOT A FENCE

§199-F's target-head fence (a bare __asm__ __volatile__("") at the head of one arm) is the documented way to steer which thread's instruction fills a conditional branch's delay slot. func_8017EFB0 (ov_SC02_021, MATCH 25/25) shows a DIFFERENT lever reaching the same class of outcome: the outer guard of the whole function was written as an early return (if (s0 == 0) return 1;) rather than a wrapping if (s0 != 0) { …body… }. Both spellings are semantically identical and both pass match_one on their own local shape, but only the early-return form reproduces the SECOND, unrelated branch's delay-slot fill deeper in the function (beqz $v0,.L8017EFF8 picking up lui $v0,(0x80520>>16) — a load literal belonging to the fall-through arm, not the branch's own condition).

Mechanism (as far as observable from the outside). fill_eager_delay_slots' scan of a branch's candidate threads is gated by own_thread_p/LABEL_NUSES reachability analysis over the whole function's CFG, not just the branch's immediate arms. Wrapping the guarded body in an if block creates a different block/edge structure around the SAME later branch than an early-return does (the early-return form removes one CFG edge — the "guard false" path never reaches a block that also flows into the later branch's fall-through — changing which insns downstream own_fallthrough analysis considers eligible). The practical result matches §199-F's own consequence (a delay slot gets filled from a specific thread) without touching that branch's own arms or adding a fence anywhere near it.

Byte evidence. src/ov_SC02_021/ov_SC02_021_jr_8017C294.c:3818-3828:

s32 func_8017EFB0(void) {
    s32 s0 = func_80029204(0);
    s32 v1;
    if (s0 == 0)
        return 1;
    func_80162760();
    v1 = *(s32 *)D_80193528;
    if (v1 < s0)
        v1 += 0x80520;
    return (v1 - s0) < 0x2D0;
}

asm/ov_SC02_021/nonmatchings/ov_SC02_021_jr_8017C294/func_8017EFB0.s: beqz $s0,.L8017F000 / addiu $v0,$zero,0x1 (outer guard's OWN delay slot, unremarkable — the early-return constant lands there per ordinary jump-inversion, §3-T4) — but ALSO, further down, beqz $v0,.L8017EFF8 / lui $v0,(0x80520>>16) for the SECOND, independent test, which the note reports ~19 if/else-spelled drafts of the SAME body left as a bare nop.

When it applies. A near-miss whose only diff is an unfilled (nop) conditional-branch delay slot DOWNSTREAM of an unrelated guard, where every fence placement at that branch itself (§199-F) has already been tried and failed or is inapplicable. Try restructuring the EARLIER, unrelated guard from a wrapping if to an early return before concluding the slot is unfillable — the CFG shape of a guard several statements earlier can gate a later branch's own eligibility.

(Declaration-layer note, not promoted: the function's sole data symbol D_80193528 is typed u8[] per its access width, matching the card's own atlas row — ordinary practice, not a new fact.)



ADDENDUM to §195-E (func_800CFC1C, md_MAIN_003, wave cv)

A GUARD'S OWN 0/1 TRUTH VALUE CAN BE THE FUNCTION'S RETURN VALUE, NOT JUST THE BRANCH'S CONDITION — SPELL IT AS return (T)gate; USING THE SAME NAMED BOOLEAN THE if TESTS

§195-E already establishes that a 0/1 value materialised at a join immediately before a controlling beqz/bnez proves the source named the condition. func_800CFC1C (md_MAIN_003, MATCH 121/121, banked at src/md_MAIN_003/md_MAIN_003.c:241) sharpens this for the specific case where the guard's failure path RETURNS EXACTLY THE GUARD'S OWN TRUTH VALUE: writing gate = cond; if (gate) return (T)gate; — reusing the SAME named boolean both as the branch's condition and, unmodified, as the return expression — keeps the compare's own destination register ($v0, from slti) as the return register, with no separate li 0/li 1 materialisation on that path. A semantically equivalent but differently-spelled version (if (cond) return 1;, or a fresh return cond ? 1 : 0;) would introduce a second definition and is NOT proven byte-neutral here — the target's exact byte pattern requires the compare's result itself, untouched, to reach $v0.

Byte evidence. src/md_MAIN_003/md_MAIN_003.c:265-270:

D_800EC69C = D_800EC69C + 1;
gate = D_800EC69C < 2;
if (gate) {
    return (u16 *)gate;
}

asm/md_MAIN_003/nonmatchings/md_MAIN_003/func_800CFC1C.s:E4C-E5C:

addiu   $v0, $v0, 0x1
lui     $at, %hi(D_800EC69C)
sw      $v0, %lo(D_800EC69C)($at)
slti    $v0, $v0, 0x2
bnez    $v0, .L800CFDDC
 addu   $s3, $zero, $zero

slti writes its 0/1 result directly into $v0; the bnez on that same register branches to the shared epilogue with $v0 already holding the function's return value — no intervening li/move.

When it applies. A guard whose ENTIRE failure-path body is return <the guard's own boolean> (not some other constant, and not a value derived from the guard by more than a bare cast). Name the compare result once, test the SAME named local in the if, and return that same local (cast to the return type) — do not re-derive the return value as a fresh literal or a second expression, even one that is mathematically identical.



ADDENDUM to §215 addendum (func_800CB2C8, md_MAIN_033, waves cv + cw)

A TIED WRITEBACK BETWEEN TWO ALREADY-PINNED VARIABLES FORCES A GENUINE CROSS-REGISTER MOVE FOR A MULTI-STEP COPY CHAIN — x = y; asm("":"=r"(x):"0"(x)); y = x; — WHERE PLAIN REASSIGNMENT WOULD COALESCE OR GET VALUE-FORWARDED AWAY

§215 addendum item 5 already establishes that two pins of the SAME hard register coexist when their live ranges are disjoint BLOCKS. func_800CB2C8 (md_MAIN_033, banked at src/md_MAIN_033/md_MAIN_033.c:146) shows a related but distinct case: TWO DIFFERENT pinned variables (register s32 raw __asm__("$3") and register s32 t __asm__("$2")), where the target's asm shows a genuine register-to-register MOVE from one physical register to the other partway through the function (a move/addu d,s,$zero encoding — confirmed NOT a zero-register add). A plain C reassignment between two pinned locals (t = raw;) is not guaranteed to survive as a real move — cse/value-forwarding can fold it away, especially when both sides are already hard-reg pinned and the "assignment" looks like a no-op to the optimizer. The working idiom names the SAME variable pair through an explicit tied input/output asm with no operands changed, forcing gcc to treat it as a live use that must be materialised:

raw = t;
__asm__("" : "=r"(raw) : "0"(raw));
t = raw;

Because raw and t are each pinned to exactly one hard register apiece (no duplicate-pin conflict, §257 item — pins on the same register in the same scope are refused by this cc1), and the tied asm gives raw a real def/use pair that cannot be coalesced with t's, the subsequent t = raw; is forced to emit as an actual cross-register move rather than being value-forwarded or dropped.

Byte evidence. src/md_MAIN_033/md_MAIN_033.c:146-165:

register void *p __asm__("$4");
register s32 raw __asm__("$3");
register s32 t __asm__("$2");
...
raw = *(u8 *)((u8 *)s1 + 0x24);
t = raw - 0x10;
if (t < 0) { ... }
raw = t;
__asm__("" : "=r"(raw) : "0"(raw));
t = raw;
*(u8 *)((u8 *)s1 + 0x24) = *(u8 *)((u8 *)s1 + 0x25) = *(u8 *)((u8 *)s1 + 0x26) = t;

The note's own residual history (near-33 → near-7 (pins) → near-4 (zero-reg-add misreading) → near-2 → MATCH) records introducing FRESH variables for the same chain as the failed intermediate step — confirming the specific requirement is REUSING the two already-pinned variables, not adding new pinned locals.

When it applies. A target shows a plain register-to-register move/addu d,s,$zero mid-function between two hard registers that are both independently already forced by pins elsewhere in the same function, and a naive reassignment between the two pinned C variables does not reproduce it. Route the value through BOTH pinned variables with a tied, unmodified "0"-constrained asm between the two assignments, rather than adding a third, freshly pinned local.



ADDENDUM to §236 item 4 (func_8017D268, ov_SC04_006, wave cw)

A SHARED Blk8/Rec8/S8-STYLE STRUCT TYPEDEF'S NAME IS KEYED TO (THE GLOBAL'S ADDRESS, THE DEFINING FUNCTION'S OWN ADDRESS) — GREP FOR YOUR FUNCTION'S ADDRESS, NOT A TWIN'S OR A NEIGHBOUR'S

§236 item 4 lists Blk8_80126940_8017D6D0_8017F6D4 among the shared-header names a draft must not re-typedef, but does not state the naming convention itself. src/shared/engine_types.h disambiguates per-function VIEWS of one shared raw buffer by compounding the global's own address with the address(es) of the function(s) that use that particular field layout — because the same 8-byte (or other small) global is legitimately interpreted with DIFFERENT field layouts by different functions, one bare Blk8_<addr> name is not always enough. func_8017D268 (ov_SC04_006) was drafted against a donor struct name borrowed from a NEIGHBOUR in the same TU (Blk8_80126940_8017D344) and separately from a banked TWIN in another overlay (Blk8_80126940_8017D6D0) — both wrong; the correct spelling, Blk8_80126940_8017D268, is keyed to func_8017D268's OWN address and exists in the header specifically for it.

Evidence. grep -n Blk8_80126940_8017D268 src/shared/engine_types.h → line 1465, } Blk8_80126940_8017D268; — confirmed present, keyed exactly as described. The banked same-address twin src/ov_SC04_003/ov_SC04_003_jr_8017BEBC.c:3342 independently uses this same spelling for the same global, corroborating that the key is the (global, defining-function) pair, not a per-overlay or per-neighbour choice.

When it applies. Any card whose seed or a plausible twin/neighbour offers a compound-named shared struct typedef (Blk8_, Blk20_, Rec8_, S8_, …) for a global your OWN function also touches. Before adopting a borrowed spelling, grep src/shared/engine_types.h for a name that ends in YOUR function's own address; a hit there is the authoritative donor, not a twin's or neighbour's compound name (which encodes a DIFFERENT function's field-layout view of the same raw bytes and may not match yours).