Files
BFM-decomp/cookbook/C0499.md
T

3.1 KiB

§450 ★★★ — REGENERATING A TARGET .s FOR A FUNCTION THAT IS NO LONGER A STUB (P31 S75; 146 functions unblocked, two silent defects caught by ONE cross-check)

§448 found 147 game functions carried as verbatim __asm__ bodies. Every one was unworkable, and not for want of information: splat emits asm/nonmatchings/<subseg>/<fn>.s only for functions that are still INCLUDE_ASM stubs, so once a function stops being a stub its target disappears — while match_one and rtu_match BOTH consume a .s. Measured: 1 of 147 had a target on disk. The information was in the wrong FORM.

tools/verbatim_target_s.py regenerates it from the extracted ROM image. That source is not an implementation detail — it is the whole point. Never regenerate a target from the __asm__ block in our own source: the block is the thing under test. A target derived from it agrees with the candidate by construction, and a "decompile" verified against it proves only that we transcribed our own transcription (R34: the oracle must be able to DISAGREE).

TWO DEFECTS, BOTH SILENT, BOTH CAUGHT BY ONE KNOWN-TRUE CROSS-CHECK — regenerating a target for a function that still had a splat .s, and comparing word for word:

  • BYTE ORDER. splat writes the four bytes AS THEY SIT IN THE IMAGE (C8FFBD27 for the instruction 0x27BDFFC8) and masked_diff.insns_from_s reads that column with struct.unpack("<I", bytes.fromhex(...)). objdump prints the VALUE. Reversing to "fix" it double-swaps: 91 of 1139 words agreed. The LENGTH was perfect, so nothing but a word-level comparison could have caught it — a length check would have passed it straight through.
  • objdump ELIDES RUNS OF ZERO BYTES as ..., and a MIPS nop IS 0x00000000. So every nop and every nop-padded tail silently vanished: func_80049610 (three nops) disassembled to ZERO instructions, func_80047D3C to 31 of 36. -z / --disassemble-zeroes is load-bearing. Here the length assertion DID catch it — which is why the tool refuses on a count mismatch instead of emitting a short target (R32/R43): ~30 quietly-truncated targets would otherwise have shipped.

Final state: 1139/1139 words identical to splat's own .s, 146 of 147 emitted, the single refusal reported by name.

And the companion move, tools/verbatim_to_stub.py: to GATE one of these, do not write a parallel gate (R33). Convert the verbatim body back to INCLUDE_ASM and it becomes a first-class citizen of every existing tool — splat re-emits its .s, gate_main/gate_stage/harvest_verify all splice it normally. INCLUDE_ASM pastes the same assembly the block transcribes, so the bytes are unchanged; what changes is the ACCOUNTING, and in the honest direction — a stub counts as outstanding work while a verbatim body counted as banked. Two refusals guard it: the block is located by brace/paren MATCHING (never regex-sliced — these blocks are full of braces and parens inside string literals), and the new stub's asm subdir is copied from a sibling stub IN THE SAME FILE, because subsegs are per-file and a neighbour's spelling silently includes ANOTHER FUNCTION'S ASSEMBLY.