fix(verbatim_to_stub): refuse a §179-C epilogue-less fragment

A function with no `jr $ra` of its own falls into a sibling's shared
epilogue. gcc-2.7.2 has no sibcall/tail-merge pass and appends an epilogue to
every C function it compiles, so no C spelling can ever match — converting one
to an INCLUDE_ASM stub just puts an unbankable target into the drawable
frontier.

I did exactly that to six functions in src/800c.c, on a `rows == 1` filter
that meant "the manifest listed one row", not "this is an independent
function" — ignoring the DECOMPILE-AS-PARENT disposition whose entire meaning
is "this row is a FRAGMENT". Three drafting agents then rediscovered §179-C
from scratch, one citing the cookbook line that names its own target.

The symptom is one grep, so nobody should pay an agent to find it again.

TWO THINGS THIS COST, both caught only by testing a known-true case:
  * the first version read the function's .s — but splat stops emitting <fn>.s
    for a verbatim body, so it had nothing to read and returned False: inert
    for precisely the case it guards. It now reads the verbatim block itself.
  * my first negative control was CloseEvent, a libapi trampoline that
    genuinely has no `jr $ra` — a "false positive" that was the correct
    answer. Re-controlled on VectorNormal (verbatim, has jr $ra, guard stays
    silent) vs func_80047E58 (verbatim, no jr $ra, guard fires).

Census of main's verbatim blocks: 37 have jr $ra, 100 do not.
This commit is contained in:
Drew T
2026-09-03 15:02:50 -06:00
parent 37beb6420b
commit dc4412b1de
2 changed files with 49 additions and 0 deletions
+35
View File
@@ -154,3 +154,38 @@ def is_verbatim_asm_draft(text, fn):
return False
c_def = re.search(rf'^[A-Za-z_][\w \*]*\b{f}\s*\([^;]*\)\s*(?:/\*.*?\*/\s*)?\{{', text, re.M | re.S)
return not c_def
def is_epilogueless_fragment(asm_path=None, asm_text=None):
"""True if this function has NO `jr $ra` of its own — cookbook §179-C (P31 S76).
THE CLASS. A §179-C function ends mid-basic-block, or every exit is a raw `j` into a SIBLING's
label, and the shared `lw $ra / addiu $sp / jr $ra` tail lives in the next symbol. gcc-2.7.2 has
no sibcall/cross-function tail-merge pass and `expand_function_end` appends an epilogue to every
C function it compiles, so NO C spelling exists: every attempt comes out +2 instructions
(a phantom `jr $ra` / `nop`). The only correct form is a file-scope `__asm__`.
WHY IT IS A PRECHECK AND NOT A LESSON (S76). The manifest already marks these DECOMPILE-AS-PARENT
("this row is a FRAGMENT — decompile the unit_entry, NEVER the fragment") and the cookbook §179-C
entry NAMES several of them. I converted six of them to INCLUDE_ASM stubs anyway, on a `rows == 1`
filter that meant "the manifest listed one row", not "this is an independent function" — putting
six unbankable targets into the drawable frontier. Three separate drafting agents then rediscovered
§179-C from scratch, one of them citing the very cookbook line that names its target.
The symptom is one grep over the target's own `.s`, so no one should ever pay an agent to find it
again: a function with no `jr $ra` is not drawable and not convertible, whatever any disposition
field says. Verified on the S76 set: 6 of 6 main `src/800c.c` conversions flagged, 5 of 5 overlay
conversions cleared.
READ THE RIGHT SOURCE. Pass `asm_text` when the function is still a VERBATIM body: splat stops
emitting `<fn>.s` for a function that is not a stub, so a path-only check has nothing to read and
returns False — inert for exactly the case it exists to prevent. Measured S76: the first version
of this guard passed a §179-C fragment straight through for that reason, caught only by testing
it on a case whose answer was already known. The verbatim block IS the assembly; use it."""
if asm_text is None:
try:
with open(asm_path, errors='replace') as fh:
asm_text = fh.read()
except (OSError, TypeError):
return False # cannot read -> not our call to make; the caller's oracle decides
return not any('jr' in ln and '$ra' in ln for ln in asm_text.splitlines())
+14
View File
@@ -28,6 +28,7 @@ purpose is to enable a byte gate, so it refuses to be the one link that goes unc
tools/verbatim_to_stub.py --restore <path.bak> # undo
"""
import argparse
import glob
import os
import re
import shutil
@@ -125,6 +126,19 @@ def main():
f'— is it already a stub, or a class-B asm-bodied C function? (R43: refusing to guess)')
path, text, s, e, blk = hit
# REFUSE A §179-C FRAGMENT (P31 S76). A function with no `jr $ra` of its own falls into a
# sibling's shared epilogue; gcc-2.7.2 appends an epilogue to every C function it compiles, so
# no C spelling can ever match and converting it to a stub only puts an unbankable target into
# the drawable frontier. Measured: six of these were converted on a filter that read the
# manifest's row COUNT instead of its DECOMPILE-AS-PARENT disposition, and three agents then
# rediscovered §179-C independently. The symptom is one grep over the target's own .s.
import draft_prechecks as _DP
if _DP.is_epilogueless_fragment(asm_text=blk):
sys.exit('verbatim_to_stub: REFUSED — %s has NO `jr $ra` of its own (cookbook §179-C): it '
'falls into a sibling\'s shared epilogue, so gcc-2.7.2 can never emit it from C '
'and a stub here is an unbankable target. It belongs in the file-scope __asm__ '
'block it already is.' % a.fn)
# The asm subdir INCLUDE_ASM must name: take it from a sibling stub in the SAME FILE, because
# that is the only spelling guaranteed to resolve for this TU (subsegs are per-file, and a
# neighbouring file's spelling is a different subseg — using it would produce a stub that