mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-10-02 16:00:27 -04:00
2e61220bde
- closes, each --try 0 then apply-body IDENTICAL + propagate N/N 0 refused: func_80133AB0 (u16 width moves), func_80130D48
(one call per goto-tail site), func_80135168 (reused temps split + H16 member store), func_80134A74 (widths + join
statement in both arms), func_80148AFC (implicit handler argument + later operand), func_8015D738 (jump threading:
re-read + a do-while on precedent, the class raised with Drew), func_80135004 (temp split + argument from its global)
- func_8017EEC0: the uninitialised a0v T4 tus10 left is the parameter (8/8, IDENTICAL)
- CI red since cb2fb5e6d: verbatim_check --strict saw the DECOMPILE-NOW row func_8017EEC0 converted; row removed (one
row), --update keeps order + UTF-8 (proven equal to the hand fix), verbatim_check --strict now in make tools-health
- delever.split_reused_locals = family R23 (selftest + two refusals; known-true: joint split = the agents' measured 12/26)
- check-all 218 passed 0 failed (twice); lever_census 20,206 marked 0 UNMARKED; Drew: at most five concurrent agents
260 lines
13 KiB
Python
260 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""verbatim_check.py — the REGRESSION GUARD for §265 verbatim-`__asm__` bodies. Replaces asm_in_c.py.
|
|
|
|
WHY THIS IS A GUARD AND NOT A CENSUS (P31 S75, R33: "the best outcome is a DELETED SCANNER").
|
|
|
|
`tools/asm_in_c.py` existed to DISCOVER this class by parsing `__asm__` blocks. That job is done, and
|
|
regex was the wrong instrument for it — five successive censuses returned **116 → 112 → 108 → 178 →
|
|
199**, and the classification was worse than the count: it called **154 rows "game code"** when the
|
|
authoritative answer is **24**. The real answers came from evidence regex cannot see:
|
|
|
|
* the `<OBJ>_OBJ_<hex>` naming key — every one is `placed_PsyQ_object.text_start + hex`, so those
|
|
symbols are OFFSETS INTO LIBRARY OBJECTS, not functions;
|
|
* the PsyQ archive symbol tables in `.run/obj40/`, which keep statics as `W` symbols, so for a
|
|
byte-identical object the archive IS the function map (`checkRECT` = SYS.o+0x52C = func_80059760,
|
|
and NONE of the 44 `SYS_OBJ_*` symbols is a function).
|
|
|
|
So the taxonomy is now DATA — `config/verbatim_manifest.json`, derived once and committed — and this
|
|
tool only answers the one question that recurs: **has the tree drifted from it?** It detects verbatim
|
|
bodies (the cheap part, and the only part regex is good at), diffs the names against the manifest,
|
|
and fails on:
|
|
|
|
NEW a verbatim body not in the manifest -> someone banked assembly; classify it before
|
|
it becomes invisible work, and NEVER let it inherit a disposition by default
|
|
GONE a manifest row no longer present -> decompiled or converted; update the manifest
|
|
MOVED same name, different file
|
|
|
|
It deliberately does NOT classify, count units, or judge SDK-vs-game. Those were the wrong answers
|
|
last time and they live in the manifest now.
|
|
|
|
tools/verbatim_check.py # diff the tree against the manifest
|
|
tools/verbatim_check.py --strict # exit 1 on any drift (for tools-health)
|
|
tools/verbatim_check.py --update # rewrite the manifest's row set from the tree
|
|
"""
|
|
import argparse
|
|
import collections
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
MANIFEST = os.path.join(REPO, 'config/verbatim_manifest.json')
|
|
|
|
LINKED_SEGS = set(
|
|
'apicard1 apicard2 apicard3 apicard4 libc2_1 libc2_2 libcd1 libcd2 libetc libgpu libgs1 libgs2 '
|
|
'libgs3 libgs4 libgs5 libgs6 libgte1 libgte10 libgte11 libgte12 libgte13 libgte14 libgte15 '
|
|
'libgte16 libgte17 libgte18 libgte19 libgte2 libgte20 libgte21 libgte22 libgte3 libgte4 libgte5 '
|
|
'libgte6 libgte7 libgte8 libgte9 libmcrd1 libmcrd2 snd1 snd2 snd3 snd4 snd5 snd6 snd7 snd8 '
|
|
'snd9'.split())
|
|
|
|
# BOTH spellings, and the `\n` terminator is what stops a bare `".ent\t"` fragment from yielding a
|
|
# phantom function literally named `t` (it did, six times). A `.globl`+label pair proves EXPORT, not
|
|
# CODE — jump tables are exported too — so it is only trusted for a name that is not data-shaped.
|
|
RE_ENT = re.compile(r'\.ent(?:\\t|[ \t])+(\w+)\\n')
|
|
RE_TYPE = re.compile(r'\.type(?:\\t|[ \t])+(\w+)\s*,\s*@function')
|
|
RE_GLOBL = re.compile(r'\.globl(?:\\t|[ \t])+(\w+)\\n')
|
|
RE_LABEL = re.compile(r'"(\w+):\\n"')
|
|
RE_DATA_NAME = re.compile(r'^(?:jtbl_|D_|_?LC?\d|\$L)')
|
|
|
|
|
|
def defined_in(block):
|
|
"""Function names DEFINED by one `__asm__` block."""
|
|
names = set(RE_ENT.findall(block)) | set(RE_TYPE.findall(block))
|
|
both = set(RE_GLOBL.findall(block)) & set(RE_LABEL.findall(block))
|
|
return names | {n for n in both if not RE_DATA_NAME.match(n)}
|
|
|
|
|
|
|
|
def sources(binary=None):
|
|
"""[(path, binary)] for every non-LINKED .c in src/. The ONE enumerator (R33)."""
|
|
out = []
|
|
for root, _d, files in os.walk(os.path.join(REPO, 'src')):
|
|
for f in files:
|
|
if not f.endswith('.c') or f[:-2] in LINKED_SEGS:
|
|
continue
|
|
rel = os.path.relpath(root, os.path.join(REPO, 'src'))
|
|
b = 'main' if rel == '.' else rel.split(os.sep)[0]
|
|
if binary and b != binary:
|
|
continue
|
|
out.append((os.path.join(root, f), b))
|
|
return sorted(out)
|
|
|
|
|
|
def asm_blocks(text):
|
|
"""[(start_line, end_line, block_text, is_file_scope)] for every __asm__/asm statement.
|
|
|
|
Brace-depth tracking decides file scope, so a block INSIDE a function (inline asm, e.g. the §3a
|
|
zero-byte barrier that appears in 3,182 of 4,224 sources) is never mistaken for a definition."""
|
|
lines = text.split('\n')
|
|
out, depth, i, n = [], 0, 0, len(lines)
|
|
while i < n:
|
|
code = re.sub(r'//.*$', '', lines[i])
|
|
if re.search(r'\b(__asm__|asm)\s*\(', code):
|
|
start, d2, opened = i, 0, False
|
|
while i < n:
|
|
c = re.sub(r'//.*$', '', lines[i])
|
|
d2 += c.count('(') - c.count(')')
|
|
if '(' in c:
|
|
opened = True
|
|
i += 1
|
|
if opened and d2 <= 0:
|
|
break
|
|
blk = '\n'.join(lines[start:i])
|
|
out.append((start + 1, i, blk, depth == 0))
|
|
depth += blk.count('{') - blk.count('}')
|
|
continue
|
|
depth += code.count('{') - code.count('}')
|
|
i += 1
|
|
return out
|
|
|
|
|
|
DETECTORS = (('defined_in', defined_in),)
|
|
|
|
|
|
def scan_in_function():
|
|
"""{(binary, fn): path} for every function whose body is ONE asm statement (declarations aside) — the IN-FUNCTION form of the
|
|
lane (P36 T1b, 2026-09-09): a C shell around a whole routine written in assembly. Detected STRUCTURALLY through
|
|
tools/lever_census.whole_body_asm_functions (the one detector, R33), in every non-LINKED .c and in src/shared/**/*.h (binary
|
|
'shared' — a shared header's routine is instantiated in every includer)."""
|
|
sys.path.insert(0, os.path.join(REPO, 'tools'))
|
|
import lever_census
|
|
found = {}
|
|
files = list(sources())
|
|
shared = os.path.join(REPO, 'src', 'shared')
|
|
for root, _d, fs in os.walk(shared):
|
|
for f in fs:
|
|
if f.endswith('.h'):
|
|
files.append((os.path.join(root, f), 'shared'))
|
|
for path, binary in files:
|
|
text = open(path, errors='ignore').read()
|
|
if '__asm__' not in text and 'asm(' not in text and '__asm(' not in text:
|
|
continue
|
|
rel = os.path.relpath(path, REPO)
|
|
for fn, _line, _blk in lever_census.whole_body_asm_functions(text, rel):
|
|
found[(binary, fn)] = rel
|
|
return found
|
|
|
|
|
|
def scan():
|
|
"""{(binary, fn): path} for every file-scope __asm__ body defining a function."""
|
|
found = {}
|
|
for root, _d, files in os.walk(os.path.join(REPO, 'src')):
|
|
for f in files:
|
|
if not f.endswith('.c') or f[:-2] in LINKED_SEGS:
|
|
continue
|
|
path = os.path.join(root, f)
|
|
text = open(path, errors='ignore').read()
|
|
if '__asm__' not in text and 'asm(' not in text:
|
|
continue
|
|
rel = os.path.relpath(root, os.path.join(REPO, 'src'))
|
|
binary = 'main' if rel == '.' else rel.split(os.sep)[0]
|
|
# file scope = brace depth 0; a block inside a function is inline asm, not a definition
|
|
lines, depth, i, n = text.split('\n'), 0, 0, len(text.split('\n'))
|
|
while i < n:
|
|
code = re.sub(r'//.*$', '', lines[i])
|
|
if re.search(r'\b(__asm__|asm)\s*\(', code):
|
|
start, d2, opened = i, 0, False
|
|
while i < n:
|
|
c = re.sub(r'//.*$', '', lines[i])
|
|
d2 += c.count('(') - c.count(')')
|
|
if '(' in c:
|
|
opened = True
|
|
i += 1
|
|
if opened and d2 <= 0:
|
|
break
|
|
blk = '\n'.join(lines[start:i])
|
|
if depth == 0:
|
|
for fn in defined_in(blk):
|
|
found[(binary, fn)] = os.path.relpath(path, REPO)
|
|
depth += blk.count('{') - blk.count('}')
|
|
continue
|
|
depth += code.count('{') - code.count('}')
|
|
i += 1
|
|
return found
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument('--strict', action='store_true', help='exit 1 on any drift')
|
|
ap.add_argument('--update', action='store_true', help='rewrite the manifest row set from the tree')
|
|
a = ap.parse_args()
|
|
|
|
if not os.path.exists(MANIFEST):
|
|
sys.exit(f'verbatim_check: {os.path.relpath(MANIFEST, REPO)} is missing — it is the '
|
|
f'authoritative census and this tool does not recompute one (R33).')
|
|
man = json.load(open(MANIFEST))
|
|
rows = man['rows']
|
|
known = {(r['binary'], r['fn']): r for r in rows}
|
|
# the manifest carries names as they were spelled at derivation; an address is a NUMBER, so
|
|
# compare case-insensitively on the hex (R48 in its case-sensitivity form, measured this session)
|
|
def key(b, fn):
|
|
m = re.match(r'(func_|D_)([0-9A-Fa-f]{8})$', fn)
|
|
return (b, m.group(1) + m.group(2).upper()) if m else (b, fn)
|
|
known_k = {key(*k): v for k, v in known.items()}
|
|
# a row with binary "ov_*" (P36 T1b) covers that routine in EVERY overlay and in the shared headers
|
|
wild = {key('ov_*', r['fn'])[1]: r for r in rows if r['binary'] == 'ov_*'}
|
|
|
|
found_file = scan()
|
|
found_in = scan_in_function()
|
|
found = dict(found_file)
|
|
found.update(found_in)
|
|
found_k = {key(*k): v for k, v in found.items()}
|
|
form_of = {key(*k): 'file-scope' for k in found_file}
|
|
form_of.update({key(*k): 'in-function' for k in found_in})
|
|
|
|
def covered(k):
|
|
return k in known_k or (k[1] in wild and (k[0].startswith('ov_') or k[0] == 'shared'))
|
|
|
|
new = sorted(k for k in found_k if not covered(k))
|
|
gone = sorted(k for k in known_k if k[0] != 'ov_*' and k not in found_k)
|
|
gone += sorted(('ov_*', fn) for fn in wild if not any(fk[1] == fn and (fk[0].startswith('ov_') or fk[0] == 'shared') for fk in found_k))
|
|
moved = sorted(k for k in set(found_k) & set(known_k) if found_k[k] != known_k[k].get('path'))
|
|
|
|
n_in = sum(1 for k in found_k if form_of[k] == 'in-function')
|
|
print(f'verbatim bodies in tree: {len(found_k)} ({len(found_k) - n_in} file-scope, {n_in} in-function sites across '
|
|
f'{len({k[1] for k in found_k if form_of[k] == "in-function"})} routines) manifest rows: {len(known_k)} '
|
|
f'({len(wild)} of them fleet-wide "ov_*" in-function rows)')
|
|
d = collections.Counter((r['disposition'], r.get('form', 'file-scope')) for r in rows)
|
|
for (k, f), v in d.most_common():
|
|
print(f' {k:22s} {f:12s} {v:4d}')
|
|
print()
|
|
if new:
|
|
print(f'!! {len(new)} NEW verbatim body(ies) NOT in the manifest — assembly was banked and is '
|
|
f'now invisible work. Classify each before it inherits a disposition by default:')
|
|
for b, fn in new[:20]:
|
|
print(f' {b:14s} {fn:24s} {found_k[(b, fn)]}')
|
|
if gone:
|
|
print(f'-- {len(gone)} manifest row(s) no longer verbatim (decompiled or converted — good; '
|
|
f'update the manifest with --update):')
|
|
for b, fn in gone[:20]:
|
|
print(f' {b:14s} {fn}')
|
|
if moved:
|
|
print(f'~~ {len(moved)} moved file:')
|
|
for b, fn in moved[:10]:
|
|
print(f' {b:14s} {fn}: {known_k[(b, fn)].get("path")} -> {found_k[(b, fn)]}')
|
|
if not (new or gone or moved):
|
|
print('no drift — the tree matches the manifest.')
|
|
|
|
if a.update:
|
|
# the fleet-wide in-function rows are kept as they are (their dispositions are ratified, not derived)
|
|
keep = [r for r in rows if r['binary'] == 'ov_*' or key(r['binary'], r['fn']) in found_k]
|
|
for b, fn in new:
|
|
keep.append(dict(binary=b, fn=fn, addr=None, nins=None, cls='UNCLASSIFIED',
|
|
disposition='UNKNOWN', form=form_of.get((b, fn), 'file-scope'), unit_entry=fn, unit_nins=None,
|
|
path=found_k[(b, fn)], why='added by --update; NEEDS CLASSIFICATION'))
|
|
# the manifest's row ORDER and its non-ASCII text (§, —, ·) are part of the file: the first --update of P36 S103
|
|
# re-sorted every row and escaped every character, a 588-line diff for one removed row. Keep the order, append
|
|
# the new rows, write UTF-8 as it was written (R57 — the instrument's write path is part of the instrument).
|
|
man['rows'] = keep
|
|
with open(MANIFEST, 'w') as fh:
|
|
fh.write(json.dumps(man, indent=1, ensure_ascii=False) + '\n')
|
|
print(f'\nmanifest updated: {len(rows)} -> {len(man["rows"])} rows '
|
|
f'({len(new)} added as UNCLASSIFIED — classify them)')
|
|
if a.strict and (new or gone or moved):
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|