mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 21:36:06 -04:00
9ab0d9eb67
I converted 9 main SDK functions from §265 verbatim bodies to INCLUDE_ASM stubs so they could be decompiled, then 'banked' all 9 from stored drafts that were those same verbatim asm blocks. match_one printed closeness 0 nine times and the whole-binary gate went BYTE-IDENTICAL — both truthfully, since a raw asm blob assembles to the bytes it was copied from. Nothing was decompiled. progress.py caught it by not moving: REAL 882, VERBATIM 164, INCLUDE_ASM 37, identical before and after. The banks are reverted. The cookbook's closing paragraph, written last session, describes this exact trap. I read it and hit it anyway ~4 hours later, because the rule was addressed to 'any burst over this class' and I was hand-picking stored drafts, and because 'no byte gate can catch it' reads as unpreventable. The byte CHECK cannot; a slate-load refusal can. draft_prechecks.is_verbatim_asm_draft: a file-scope __asm__ naming the fn via .ent/.globl/label AND no C definition of it. Both spellings of .ent handled (inside a C string it is a backslash-t, not a tab — five censuses of this class disagreed until that was fixed). gate_main refuses such a slate beside its existing INCLUDE_ASM no-op refusal (R43). Census of the draft store: 1,099 of 704,375 .c files are verbatim-asm drafts under ordinary <fn>.c names. Negative control: 0 false positives across 45,898 drafts carrying both a C definition and an inline __asm__ (R39).
157 lines
7.3 KiB
Python
157 lines
7.3 KiB
Python
"""Static pre-checks that turn a BUILD-time draft failure into a GENERATION-time skip.
|
|
|
|
Every class here was measured as a real failure in the S50 autodraft runs, and every one is
|
|
decidable without compiling anything. That is the whole point: a build spent to learn something a
|
|
regex knows is a build wasted, and at ~1 min/group over ~100 groups the waste is the run.
|
|
|
|
1. ARITY DISAGREEMENT — `too few arguments to function 'func_80146C3C'`.
|
|
The destination TU already declares a callee with a different parameter count than the seed's
|
|
body passes. Neither emitting the seed's decl (conflicting types) nor relying on the
|
|
destination's (too few arguments) can work; the member needs a real edit.
|
|
|
|
2. UNDEFINED DATA — `undefined reference to 'D_801E9C44'`.
|
|
The draft references a symbol nothing in the DESTINATION binary defines. The seed's own data
|
|
symbol survived into the draft because it sits at no relocation slot symbol_map covers.
|
|
"""
|
|
import glob
|
|
import os
|
|
import re
|
|
|
|
|
|
def _decl_arity(text, sym):
|
|
"""Parameter count of the first declaration/definition of `sym` in `text`, or None."""
|
|
m = re.search(rf'^[ \t]*(?:extern\s+)?[A-Za-z_][\w \*]*\b{re.escape(sym)}\s*\(([^;{{)]*)\)',
|
|
text, re.M)
|
|
if not m:
|
|
return None
|
|
args = m.group(1).strip()
|
|
if args == '':
|
|
return None # C89: `f()` declares UNSPECIFIED parameters, not zero — never a conflict
|
|
if args == 'void':
|
|
return 0
|
|
return len([a for a in args.split(',') if a.strip()])
|
|
|
|
|
|
def _call_arity(body, sym):
|
|
"""Argument count at the first CALL of `sym` in `body`, or None. Depth-aware so a nested call
|
|
or a comma inside a cast does not miscount."""
|
|
m = re.search(rf'\b{re.escape(sym)}\s*\(', body)
|
|
if not m:
|
|
return None
|
|
depth, args, cur = 0, [], ''
|
|
for ch in body[m.end() - 1:]:
|
|
if ch == '(':
|
|
depth += 1
|
|
if depth == 1:
|
|
continue
|
|
elif ch == ')':
|
|
depth -= 1
|
|
if depth == 0:
|
|
args.append(cur)
|
|
break
|
|
if depth == 1 and ch == ',':
|
|
args.append(cur)
|
|
cur = ''
|
|
continue
|
|
if depth >= 1:
|
|
cur += ch
|
|
return len([a for a in args if a.strip()])
|
|
|
|
|
|
def arity_conflicts(body, dest_text, syms, self_name=None):
|
|
"""Symbols the body CALLS with an argument count the destination's own declaration refuses."""
|
|
bad = []
|
|
for s in syms:
|
|
if not s.startswith('func_') or (self_name and s.lower() == self_name.lower()):
|
|
continue # the member's own definition is not a call to itself
|
|
d, c = _decl_arity(dest_text, s), _call_arity(body, s)
|
|
# CONSERVATIVE by measurement: flag only when the destination declares NAMED parameters and
|
|
# the body passes fewer. Every true positive had that shape (`dest decl 1 vs call 0`) and
|
|
# every false positive the other one (`dest decl 0 vs call 1`) — where the `0` comes from a
|
|
# `(void)` my parser reads out of a declaration it should not have matched. A pre-check that
|
|
# throws away good drafts is worse than one that lets a few builds fail.
|
|
if d is not None and c is not None and d > c:
|
|
bad.append(f"{s}(dest decl {d} vs call {c})")
|
|
return bad
|
|
|
|
|
|
_DEF_CACHE = {}
|
|
|
|
|
|
def defined_in_binary(binary, sym):
|
|
"""True if `sym` is defined anywhere in the destination binary — its asm (a dlabel/glabel, or
|
|
a symbol file entry) or its C. Cached per binary: the asm scan is the expensive part."""
|
|
key = binary
|
|
if key not in _DEF_CACHE:
|
|
defs = set()
|
|
for p in glob.glob(f"asm/{binary}/**/*.s", recursive=True):
|
|
for m in re.finditer(r'^(?:dlabel|glabel|jlabel)\s+([A-Za-z_]\w*)', open(p).read(), re.M):
|
|
defs.add(m.group(1))
|
|
for p in glob.glob(f"config/symbols.{binary}*.txt"):
|
|
for m in re.finditer(r'^([A-Za-z_]\w*)\s*=', open(p).read(), re.M):
|
|
defs.add(m.group(1))
|
|
_DEF_CACHE[key] = defs
|
|
return sym in _DEF_CACHE[key]
|
|
|
|
|
|
def undefined_data(binary, dest_text, syms, self_name=None, body=""):
|
|
"""Referenced symbols nothing in the destination binary defines, and the destination TU does
|
|
not define locally either.
|
|
|
|
A symbol the DRAFT ITSELF defines is not undefined — its own function name above all, which
|
|
has no `glabel` left in asm precisely BECAUSE it is now C. Skipping that was 38 of 38 false
|
|
positives in the negative control."""
|
|
out = []
|
|
for s in syms:
|
|
if self_name and s.lower() == self_name.lower():
|
|
continue
|
|
if re.search(rf'^[^\n=;]*\b{re.escape(s)}\b[^\n;]*\)\s*\{{', body, re.M) or \
|
|
re.search(rf'^[ \t]*(?:const\s+|static\s+)*[A-Za-z_][\w \*]*\b{re.escape(s)}\b'
|
|
rf'(?:\s*\[[^\]]*\])?\s*=', body, re.M):
|
|
continue
|
|
if defined_in_binary(binary, s):
|
|
continue
|
|
if re.search(rf'^[ \t]*(?:const\s+|static\s+)*[A-Za-z_][\w \*]*\b{re.escape(s)}\b[^\n;]*[=;]',
|
|
dest_text, re.M):
|
|
continue
|
|
out.append(s)
|
|
return out
|
|
|
|
|
|
def is_verbatim_asm_draft(text, fn):
|
|
"""True if `text` is the target's own assembly, not a decompile of it (P31 S76).
|
|
|
|
THE DEFECT THIS CATCHES, and why no byte gate can. `tools/asm_verbatim.py` emits a §265
|
|
verbatim body -- the function's own .s pasted into a file-scope `__asm__` string. Those files
|
|
sit in the same draft directories as real C drafts and are named `<fn>.c` like everything else.
|
|
Score one with match_one and it prints MATCH, truthfully: a raw asm blob assembles to the bytes
|
|
it was copied from. Substitute one and the whole-binary gate goes GREEN, also truthfully. The
|
|
function is then reported BANKED while nothing whatsoever has been decompiled.
|
|
|
|
S75 hit this once (an agent submitted the verbatim block as its own "decompile"; only an
|
|
adversarial reader caught it) and recorded "No byte gate can" catch it. That is true of the
|
|
BYTE check and false of a slate-load refusal, which is why this lives here: the verbatim form
|
|
is trivially decidable from the text (R43 -- refuse the input, never mishandle it).
|
|
|
|
S76 hit it again, from the other direction and at scale: nine functions were converted from
|
|
verbatim bodies to stubs precisely so they could be decompiled, then "banked" from stored
|
|
drafts that were the same verbatim blocks -- a round trip through the gate that moved
|
|
progress.py by exactly zero and read as 9 of 9 MATCH. The instrument was right and the claim
|
|
was wrong.
|
|
|
|
A draft is verbatim iff a file-scope `__asm__` names this function as a .ent/.globl/label AND
|
|
no C definition of it exists. Both halves matter: a real draft may legitimately carry a small
|
|
inline `__asm__`, and a verbatim body may omit .globl for a static."""
|
|
if '__asm__' not in text:
|
|
return False
|
|
f = re.escape(fn)
|
|
# The escaped forms are what the emitters write INTO a C string: `.ent\tfoo\n` is backslash-t,
|
|
# not a tab. Match both the escaped and the literal spellings -- five S76 censuses of this
|
|
# class disagreed with each other until both were handled.
|
|
asm_names = re.search(rf'\.(?:ent|globl)(?:\\t|[ \t])+{f}\b', text) or \
|
|
re.search(rf'"\s*{f}:\s*(?:\\n|$)', text)
|
|
if not asm_names:
|
|
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
|