mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 13:33:34 -04:00
168 lines
6.8 KiB
Python
168 lines
6.8 KiB
Python
#!/usr/bin/env python3
|
||
"""P31 T9 — the permuter/grinder FEEDER: warm-start drafts from banked exemplars + routed fuel.
|
||
|
||
WHY: the grinder's own audit (Phase-22) showed 92% of its CPU went where the permuter provably
|
||
cannot win; its targeting fix admits autopsy `bucket=="permuter"` and — deliberately — KEEPS
|
||
unclassified records ("unknown is not a reason to skip", R32). So a feeder that enqueues ONLY
|
||
pre-verified permuter-shaped work needs NO grinder patch: it appends backlog near-records
|
||
(source=warmstart) whose drafts are remapped PROVEN sibling bodies, and the daemon picks them up.
|
||
|
||
Modes:
|
||
--from-banked fn[,fn…] --binary B for each newly-banked exemplar: walk its h_seq family's
|
||
still-open members (the atlas group's mechanical remainder); for each, build the remapped
|
||
seed draft (family_remap.symbol_map — refuses on reloc-count mismatch) and STREAM-CLASSIFY
|
||
member-vs-seed with ZERO compiles (both are ground-truth bytes); enqueue only
|
||
bucket==permuter or LENGTH-DRIFT |Δ|<=2.
|
||
--lenmiss ingest .run/lenmiss/permuter.json (T8's |Δ|<=2 clean-drift route, drafts already
|
||
on disk) as warmstart backlog records.
|
||
|
||
The whole-binary byte-gate remains the arbiter; a wrong enqueue costs grinder CPU, never a byte.
|
||
"""
|
||
import argparse, json, os, sys
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
import backlog
|
||
import corpus
|
||
import family_remap as FR
|
||
import family_cousins as FC
|
||
import aprop_autodraft as APD
|
||
import residual_class
|
||
import sig_image
|
||
|
||
|
||
def stream_dicts(words, vram=0x80100000):
|
||
"""words -> masked_diff-shaped [{word, mnem, reloc_kind, reloc_op}] for classify_streams
|
||
(zero compiles: both sides are ground-truth bytes)."""
|
||
rel = FR.reloc_indices(words)
|
||
out = []
|
||
pending_hi = set()
|
||
for k, w in enumerate(words):
|
||
ins = sig_image.make_insn(w, vram + 4 * k)
|
||
mnem = ins.getOpcodeName().lower() if ins.isValid() else "?"
|
||
kind = None
|
||
if k in rel:
|
||
op = w >> 26
|
||
if op in (2, 3):
|
||
kind = "R_MIPS_26"
|
||
elif op == 0x0F:
|
||
kind = "R_MIPS_HI16"
|
||
pending_hi.add((w >> 16) & 31)
|
||
else:
|
||
kind = "R_MIPS_LO16"
|
||
out.append({"word": w, "mnem": mnem, "reloc_kind": kind, "reloc_op": None})
|
||
return out
|
||
|
||
|
||
def classify_pair(seed_words, member_words):
|
||
"""-> verdict dict from residual_class over the two byte-truth streams."""
|
||
return residual_class.classify_streams(stream_dicts(seed_words), stream_dicts(member_words))
|
||
|
||
|
||
def enqueue(rec, dry):
|
||
if dry:
|
||
print(" would enqueue:", rec["binary"], rec["name"], rec.get("klass"))
|
||
return
|
||
backlog.append_record(rec)
|
||
|
||
|
||
def from_banked(fns, binary, outdir, dry):
|
||
fam = json.load(open(".run/family_hseq.json"))["families"]
|
||
by_hs = {}
|
||
sig = corpus.sig(binary)
|
||
n_enq = n_ref = 0
|
||
for fn in fns:
|
||
addr = int(fn.split("_")[1], 16)
|
||
row = sig.get(addr)
|
||
if row is None:
|
||
print(f" {fn}: not in {binary}'s sig — skipped")
|
||
continue
|
||
hs = row["h_seq"]
|
||
f = next((x for x in fam if x["h_seq"] == hs), None)
|
||
if f is None:
|
||
print(f" {fn}: no family for its h_seq — ×1 bank, nothing to seed")
|
||
continue
|
||
seed_words = FR.stream_words(binary, addr, row["nins"])
|
||
body_ref = FC.seed_body_ref(binary, addr)
|
||
if not body_ref or body_ref.get("kind") == "unresolved":
|
||
print(f" {fn}: seed body unresolved — skipped")
|
||
continue
|
||
body, seed_text, _kind = APD.seed_body(body_ref, fn)
|
||
if body is None:
|
||
print(f" {fn}: seed body unreadable — skipped")
|
||
continue
|
||
for m in f["members"]:
|
||
mb, ma = m[0], (m[1] if isinstance(m[1], int) else int(str(m[1]), 16))
|
||
if mb == binary and ma == addr:
|
||
continue
|
||
st = corpus.stubs(mb)
|
||
if ma not in st:
|
||
continue # already banked
|
||
member_words = FR.stream_words(mb, ma, f["nins"])
|
||
if member_words is None:
|
||
continue
|
||
v = classify_pair(seed_words, member_words)
|
||
klass = v.get("klass")
|
||
delta = len(member_words) - len(seed_words)
|
||
ok = (v.get("bucket") == "permuter") or (klass == "LENGTH-DRIFT" and abs(delta) <= 2)
|
||
if not ok:
|
||
n_ref += 1
|
||
continue
|
||
mfn = st[ma].symbol
|
||
try:
|
||
renames, sm_err = FR.symbol_map(addr, binary, mb, to_addr=ma)
|
||
except Exception as e:
|
||
renames, sm_err = None, repr(e)[:60]
|
||
if renames is None:
|
||
print(f" {mfn}: symbol_map refused ({sm_err})")
|
||
n_ref += 1
|
||
continue
|
||
draft, skip = APD.build_draft(body, fn, mfn, renames, seed_text, "")
|
||
if draft is None:
|
||
print(f" {mfn}: build_draft skipped ({skip})")
|
||
n_ref += 1
|
||
continue
|
||
os.makedirs(f"{outdir}/{mb}", exist_ok=True)
|
||
p = f"{outdir}/{mb}/{mfn}.c"
|
||
open(p, "w").write(draft)
|
||
enqueue({"name": mfn, "binary": mb, "status": "near", "source": "warmstart",
|
||
"klass": v.get("profile") or klass, "closeness": v.get("closeness"),
|
||
"best_draft": p, "where_stuck": f"warmstart from {binary}:{fn}"}, dry)
|
||
n_enq += 1
|
||
print(f"warmstart --from-banked: enqueued {n_enq}, refused {n_ref}")
|
||
|
||
|
||
def from_lenmiss(dry):
|
||
rows = json.load(open(".run/lenmiss/permuter.json"))
|
||
n = 0
|
||
for r in rows:
|
||
enqueue({"name": r["fn"], "binary": r["binary"], "status": "near",
|
||
"source": "warmstart", "klass": "LENGTH-DRIFT",
|
||
"closeness": abs(r.get("delta") or 0),
|
||
"best_draft": r["draft"],
|
||
"where_stuck": "lenmiss permuter-length route"}, dry)
|
||
n += 1
|
||
print(f"warmstart --lenmiss: enqueued {n}")
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
||
ap.add_argument("--from-banked", default=None, help="comma-separated banked fn names")
|
||
ap.add_argument("--binary", default=None)
|
||
ap.add_argument("--lenmiss", action="store_true")
|
||
ap.add_argument("--outdir", default=".run/warmstart")
|
||
ap.add_argument("--dry-run", action="store_true")
|
||
a = ap.parse_args()
|
||
if a.lenmiss:
|
||
from_lenmiss(a.dry_run)
|
||
if a.from_banked:
|
||
if not a.binary:
|
||
sys.exit("--from-banked needs --binary")
|
||
from_banked([x.strip() for x in a.from_banked.split(",") if x.strip()],
|
||
a.binary, a.outdir, a.dry_run)
|
||
if not a.lenmiss and not a.from_banked:
|
||
sys.exit("nothing to do: pass --lenmiss and/or --from-banked")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|