Files
BFM-decomp/cookbook/C0244.md
T

3.5 KiB

§223 — READING A jal DELAY SLOT: the value in it was produced BEFORE the call, so it is NEVER that call's return (P31 S58)

§194-M owns the direction "a store in a CONDITIONAL branch's slot ⇒ its statement dominates the branch", and §194-M BOUND 1 explicitly excludes jal slots. §176-A/L17616 and L17636 own the placement lever ("move the store above the call"). Nothing owned the READING rule, and four cards in this harvest lost a draft to it.

THE RULE. A delay slot executes BEFORE its jal transfers control. Therefore whatever the slot reads was live at the SLOT, not at the call: it is the PRECEDING call's $v0, or a constant materialised before the call, or the completion of a preceding statement. It is never the return of the call whose slot it occupies. Ask "which value is live at the slot", not "which call is on the line above".

Instance 1 — the preceding call's return. (✔ byte-checked: asm/ov_SC06_029/.../func_80186DA0.s idx 6-13.)

jal   func_8012B744
 addiu $a0, $a0, 0x4
...
jal   func_8012B2CC
 sh   $v0, 0x12($v1)      <- $v0 is func_8012B744's return

The naive reading ("store func_8012B2CC's return through *(state+0x20)+0x12") drifted by exactly one instruction everywhere after the slot. The correct C nests func_8012B744 directly as the stored value and leaves func_8012B2CC as a separate void statement.

Instance 2 — a pre-call CONSTANT. (✔ byte-checked: asm/ov_SC06_000/.../func_80184524.s idx 3-6.)

addiu $v0, $zero, 0x4      <- the constant
sw    $ra, 0x14($sp)
jal   func_8012B200
 sw   $v0, 0x1C($s0)       <- stores 4, NOT the call's return

Drafting x = func_8012B200(...) mis-schedules the whole tail. func_801871EC (ov_SC03_028) is the same shape twice over — both sw $v0,k($s0) sit in the FOLLOWING jal's slot with $v0 pre-loaded by li (0xF and 0x23) — and both callees' results are discarded. Tell: a li/addiu …,$zero,K above the jal and a sw $v0 in its slot ⇒ a constant store, written before the call.

Instance 3 — the completion of a preceding statement. func_80188C18 (ov_SC03_006): target line 28 sh $v0, 0x1A($sp) sits in the jal func_80133784 slot and is the scheduler-hoisted completion of out[1] += 8, not a store of the call result. (✔ byte-checked above in §210 — lhu 0x1A(sp) / addiu $v0,8 / jal / sh $v0,0x1A(sp).)

THE COROLLARY THAT SAVES A DRAFT — an EMPTY slot with an untouched $a0 means the incoming argument passes straight through. func_80184524 again: lw $a0,0xD0($s0) / beqz $a0 / jal func_80184AD4 with a nop slot ⇒ the callee's argument is the loaded pointer, which survived the branch, not the entity. func_801887CC (ov_SC02_005) reports the same thing as a missed call: a jal was invisible on first read because its slot held an sh and there was no arg-setup insn at all — $a0 still carried the incoming argument. func_8018BF88 (ov_SC06_029), func_80184B18 (ov_SC03_029) and func_801884C8 (ov_SC02_005) are three more of the same family; on the last one, the absent a1/a2 setup means the call passes garbage in those registers, which is expressible only through a casted function pointer.

BOUNDARY. This is a reading rule and it is one-way. It tells you what the source must have said; it does not promise that writing the statement there will refill the slot — that is fill_eager_delay_slots' problem and §199-F BOUND 2 / §194-M BOUND 3 own its failure modes.