mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-28 06:49:47 -04:00
9a3e438ed9
Measured on the 489-draft run: inline seeds banked 145/213 (68%), macro seeds 77/276 (28%),
and the macro failures were decl collisions to a fault — 'parse error before *', 'too few
arguments to function'. Cause: a de-macroized DEFINE_ block is decls + definition, and
pasting it wholesale reintroduces the exact decl-agreement failure the minimal-preamble
design exists to avoid. func_8016AB6C's block is 1,891 lines of which 108 are the function.
- seed_body() now returns the DEFINITION as the body and keeps the macro block as the DECL
SOURCE, so only the externs the definition actually references are emitted.
- aprop_symfix.body_in_text(): text-based sibling of body_text (R33 — one implementation).
Indented-definition fallback for macro blocks, confirmed by a '{' with no ';' before it,
so an indented CALL can never be mistaken for a definition.
215 lines
9.4 KiB
Python
215 lines
9.4 KiB
Python
"""Rebase stale SEED symbols in adapt/A-prop drafts onto the TARGET's own symbols.
|
|
|
|
WHY THIS EXISTS (S50, cookbook §171). The adapt/A-prop lanes hand an agent a *proven seed body*
|
|
from one location plus a small token-diff, and the agent adapts the code — but a per-location data
|
|
symbol (`D_8018xxxx`, a function-pointer table, a jump table) is part of the seed's ENVIRONMENT,
|
|
not its logic. Carry it over unrebased and:
|
|
|
|
- `match_one` still scores MATCH. It compiles standalone and compares instruction encodings;
|
|
a `%hi(D_seed)` and a `%hi(D_target)` are the same instruction with a different relocation
|
|
TARGET NAME, and the scorer is blind to the name.
|
|
- the whole-binary gate then fails at LINK: `undefined reference to 'D_seed'`.
|
|
|
|
That gap — standalone-MATCH, link-undefined — was the entire A-prop 91%-agent-vs-57%-gate delta
|
|
measured in S49 (24 of 24 concentrated failures; §170's TU-collision hypothesis was refuted by
|
|
the same data: 5-draft groups banked 5/5).
|
|
|
|
This is the SECOND, DISAGREEING ORACLE (R34) for that class: match_one says MATCH, the symbol
|
|
audit says the draft references a symbol the target's own .s never mentions. It is deterministic
|
|
and costs no build, so it belongs BEFORE the gate, never after.
|
|
|
|
audit: tools/aprop_symfix.py <slate.json> (report only)
|
|
fix: tools/aprop_symfix.py <slate.json> --fix -> .run/aprop_symfix/<fn>/<fn>.c + slate
|
|
|
|
A slate record is {fn|name, binary, draft}; the emitted slate points `draft` at the fixed copy so
|
|
`tools/gate_lane.py` consumes it unchanged.
|
|
"""
|
|
import argparse, collections, json, os, re, sys
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
|
|
import corpus
|
|
|
|
# vram-suffixed symbols: D_80185A28, func_801727D0, jtbl_8018xxxx, ...
|
|
SYM = re.compile(r'\b[A-Za-z_][A-Za-z0-9_]*_?8[0-9A-Fa-f]{7}\b')
|
|
ASM_OPS = re.compile(r'%hi\(([^)]+)\)|%lo\(([^)]+)\)|\bjal\s+([A-Za-z_]\w*)|\.word\s+([A-Za-z_]\w*)')
|
|
|
|
|
|
def _strip_comments(txt):
|
|
return re.sub(r'//[^\n]*', '', re.sub(r'/\*.*?\*/', '', txt, flags=re.S))
|
|
|
|
|
|
def syms_in_text(txt):
|
|
return set(SYM.findall(_strip_comments(txt)))
|
|
|
|
|
|
def draft_syms(path):
|
|
return syms_in_text(open(path).read())
|
|
|
|
|
|
def body_text(path, name):
|
|
"""The C body of ONE function out of a FILE (thin wrapper over body_in_text)."""
|
|
if not path or not os.path.isfile(path):
|
|
return None
|
|
return body_in_text(open(path).read(), name)
|
|
|
|
|
|
def body_in_text(txt, name):
|
|
"""The C body of ONE function out of a block of text (a draft is one function; a seed's file,
|
|
or a de-macroized DEFINE_ block, is not).
|
|
|
|
Matches an `DEFINE_<name>(` engine_core macro or a plain definition, then brace-matches. A
|
|
whole-file scan would drag every OTHER function's symbols into the comparison and drown the
|
|
signal, so callers annotating a SEED must pass through here."""
|
|
txt = _strip_comments(txt)
|
|
# Column-0 first (a file's definitions start there). The INDENTED fallback exists for a
|
|
# de-macroized DEFINE_ block, whose lines are all indented — but an indented `func_X(` is far
|
|
# more often a CALL, so that form must be confirmed by a `{` with no `;` before it.
|
|
m = (re.search(rf'\bDEFINE_{re.escape(name)}\s*\(', txt) or
|
|
re.search(rf'^[A-Za-z_][^\n=;]*\b{re.escape(name)}\s*\(', txt, re.M))
|
|
if not m:
|
|
for cand in re.finditer(rf'^[ \t]*[A-Za-z_][^\n=;]*\b{re.escape(name)}\s*\(', txt, re.M):
|
|
i = txt.find('{', cand.start())
|
|
if i >= 0 and ';' not in txt[cand.end():i]:
|
|
m = cand
|
|
break
|
|
if not m:
|
|
return None
|
|
i = txt.find('{', m.start())
|
|
if i < 0:
|
|
return None
|
|
depth = 0
|
|
for j in range(i, len(txt)):
|
|
if txt[j] == '{':
|
|
depth += 1
|
|
elif txt[j] == '}':
|
|
depth -= 1
|
|
if depth == 0:
|
|
return txt[m.start():j + 1]
|
|
return None
|
|
|
|
|
|
def asm_syms_ordered(path):
|
|
"""Relocated-symbol operands in EMISSION order, deduped. Order is the useful part when the
|
|
rename is not 1:1 — an agent can align an n:m case against the seed body's own reference order,
|
|
which a sorted set destroys."""
|
|
out = []
|
|
for m in ASM_OPS.finditer(open(path).read()):
|
|
for g in m.groups():
|
|
if g:
|
|
s = g.split('+')[0].strip()
|
|
if s not in out:
|
|
out.append(s)
|
|
return out
|
|
|
|
|
|
def asm_syms(path):
|
|
return set(asm_syms_ordered(path))
|
|
|
|
|
|
def diff_syms(c_path, asm_path, self_name):
|
|
"""Compare a C body's symbols against the symbols its target .s actually relocates.
|
|
|
|
Shared primitive: `aprop_symfix` uses it on a DRAFT (post-hoc guard), `family_cousins.sym_map`
|
|
uses it on the SEED BODY (pre-hoc card annotation). A matched seed has no `.s` of its own —
|
|
it is compiled from C — so the seed side must always be read from the C text (R33: one
|
|
implementation, two callers, rather than two drifting re-parsers).
|
|
|
|
-> (status, stale[], asm_only[]). status: clean | STALE | AMBIGUOUS | NO_ASM | NO_DRAFT.
|
|
"""
|
|
if not c_path or not os.path.isfile(c_path):
|
|
return 'NO_DRAFT', [], []
|
|
return diff_syms_text(open(c_path).read(), asm_path, self_name)
|
|
|
|
|
|
def diff_syms_text(c_text, asm_path, self_name, ignore=()):
|
|
"""`ignore` names FUNCTION IDENTITIES to exclude from both sides — the caller's own name plus,
|
|
for a seed body, the SEED's name. Compared case-insensitively (sig_image spells hex lowercase,
|
|
splat uppercase) and with the `DEFINE_` macro prefix stripped, or a macro-bodied seed reports
|
|
its own name as a stale symbol and every 1:1 rename degrades to AMBIGUOUS."""
|
|
if c_text is None:
|
|
return 'NO_DRAFT', [], []
|
|
if not asm_path or not os.path.exists(asm_path):
|
|
return 'NO_ASM', [], []
|
|
ign = {n.lower() for n in (self_name, *ignore) if n}
|
|
def own(s):
|
|
return re.sub(r'^DEFINE_', '', s).lower() in ign
|
|
d, a = syms_in_text(c_text), asm_syms(asm_path)
|
|
stale = sorted(s for s in d - a if not own(s))
|
|
asm_only = sorted(s for s in a - d if not own(s) and not s.startswith('.'))
|
|
if not stale:
|
|
return 'clean', [], asm_only
|
|
if not asm_only:
|
|
# Draft-DEFINED identifiers that merely carry a vram-looking suffix (`Blk8_80126940_…`,
|
|
# `S8_80172780`, `L_call_…`). Nothing in the target's .s is missing, so there is nothing to
|
|
# rebase — measured across the whole wave-7a/7b residue, this is every non-clean case there.
|
|
return 'local-only', stale, []
|
|
# 1:1 is the mechanically-safe case (one seed symbol, one target symbol to take its place).
|
|
return ('STALE' if len(stale) == 1 and len(asm_only) == 1 else 'AMBIGUOUS'), stale, asm_only
|
|
|
|
|
|
def audit_one(fn, binary, draft):
|
|
return diff_syms(draft, corpus.asm_path(binary, fn), fn)
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument('slate')
|
|
ap.add_argument('--fix', action='store_true', help='write rebased copies + a gateable slate')
|
|
ap.add_argument('--outdir', default='.run/aprop_symfix')
|
|
ap.add_argument('--out-slate', default='.run/aprop_symfix_slate.json')
|
|
ap.add_argument('--skip', help='json list of already-banked fn names to exclude')
|
|
a = ap.parse_args()
|
|
|
|
recs = [r for r in json.load(open(a.slate)) if isinstance(r, dict) and (r.get('fn') or r.get('name'))]
|
|
skip = set(json.load(open(a.skip))) if a.skip else set()
|
|
seen, rows = set(), []
|
|
for r in recs:
|
|
fn = r.get('fn') or r['name']
|
|
if fn in skip or fn in seen:
|
|
continue
|
|
seen.add(fn)
|
|
draft = r.get('draft') or f".run/aprop1/{fn}/{fn}.c"
|
|
st, stale, asm_only = audit_one(fn, r['binary'], draft)
|
|
rows.append(dict(fn=fn, binary=r['binary'], draft=draft, status=st,
|
|
stale=stale, asm_only=asm_only, sub=r.get('sub')))
|
|
|
|
cls = collections.Counter(x['status'] for x in rows)
|
|
if not rows: # R32: a silent skip is a DEFECT
|
|
sys.exit(f"R32: {a.slate} yielded 0 auditable records — wrong shape or everything skipped?")
|
|
print(f"{len(rows)} drafts audited: {dict(cls)}")
|
|
for x in rows:
|
|
if x['status'] in ('STALE', 'AMBIGUOUS'):
|
|
print(f" {x['status']:9} {x['fn']:16} {x['binary']:12} "
|
|
f"draft-only={x['stale']} asm-only={x['asm_only']}")
|
|
|
|
if not a.fix:
|
|
json.dump(rows, open('.run/aprop_symcheck.json', 'w'), indent=1)
|
|
print("audit only — wrote .run/aprop_symcheck.json")
|
|
return
|
|
|
|
slate, deltas = [], collections.Counter()
|
|
for x in rows:
|
|
if x['status'] != 'STALE':
|
|
continue
|
|
old, new = x['stale'][0], x['asm_only'][0]
|
|
txt = open(x['draft']).read()
|
|
fixed, n = re.subn(rf'\b{re.escape(old)}\b', new, txt)
|
|
if n == 0: # R32: prove the edit ran (R37) rather than assuming it did
|
|
print(f" !! {x['fn']}: 0 substitutions of {old} — skipped")
|
|
continue
|
|
d = os.path.join(a.outdir, x['fn'])
|
|
os.makedirs(d, exist_ok=True)
|
|
p = os.path.join(d, f"{x['fn']}.c")
|
|
open(p, 'w').write(fixed)
|
|
deltas[int(new[2:], 16) - int(old[2:], 16)] += 1
|
|
slate.append(dict(fn=x['fn'], binary=x['binary'], sub=x['sub'], draft=p,
|
|
rebased=f"{old}->{new}", subs=n))
|
|
json.dump(slate, open(a.out_slate, 'w'), indent=1)
|
|
print(f"\nrebased {len(slate)}/{cls['STALE']} STALE drafts -> {a.outdir}")
|
|
print(f"seed->target vram deltas: {[(hex(k), v) for k, v in deltas.items()]}")
|
|
print(f"gateable slate: {a.out_slate}")
|
|
|
|
|
|
if __name__ == '__main__': # importable: family_cousins reuses the primitives above
|
|
main()
|