From dc4412b1de0811d408e1abff3bbf7f5b5001adfa Mon Sep 17 00:00:00 2001 From: Drew T <50529377+Druthulu@users.noreply.github.com> Date: Thu, 3 Sep 2026 15:02:50 -0600 Subject: [PATCH] =?UTF-8?q?fix(verbatim=5Fto=5Fstub):=20refuse=20a=20?= =?UTF-8?q?=C2=A7179-C=20epilogue-less=20fragment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 .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. --- tools/draft_prechecks.py | 35 +++++++++++++++++++++++++++++++++++ tools/verbatim_to_stub.py | 14 ++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/tools/draft_prechecks.py b/tools/draft_prechecks.py index 70728d086..b13189312 100644 --- a/tools/draft_prechecks.py +++ b/tools/draft_prechecks.py @@ -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 `.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()) diff --git a/tools/verbatim_to_stub.py b/tools/verbatim_to_stub.py index bd4c7d082..6e2c23dc8 100644 --- a/tools/verbatim_to_stub.py +++ b/tools/verbatim_to_stub.py @@ -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 # 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