Files
BFM-decomp/tools/wave_targets.py
T
Drew T 91430e9309 feat(phase-21): _a wave is turnkey — gate_stage --src-file + wave_targets region-aware asm
- gate_stage.py --src-file: cast canonicalizes vs the SPLIT .c's decls + sig_unify SKIPPED (it reads
  main .c only -> drops _a/_o0 drafts). Validated end-to-end: gate_stage _a-mode banked func_8012C098.
- wave_targets.py region-aware asm paths (REGION_SUB/asm_for): --region a serves the 68 fresh _a
  reach-134 fns with correct ..._a .s paths (pool + --class modes).
- the _a wave is now fully wired (wave_targets --region a -> worker_wave -> gate_stage --src-file ->
  dedup_propagate split-aware). Turnkey runbook in CURRENT_PHASE.md (cont.3) for a fresh session.
- no net banks; fleet 62.31%; ov_SC01_077 d19c9580 (R22).
2026-06-24 19:35:17 -06:00

226 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""wave_targets.py — select a worker-wave target batch from the fuel manifest.
Emits a JSON array of {name, addr, nins, class, asm, ghidra_c} for tools/workflows/worker_wave.js
(passed as args.targets). Filters to still-OPEN (INCLUDE_ASM) cached targets in the chosen pool,
ranked by leverage (reach*nins), skipping known walls already logged 'failed'/'stub' in the backlog.
Pools (ROI rotation): tractable (reach-134 WAVE/PINS/STRUCT <=150 ins, main region) | giants |
o0 | capped | any-reach134 | reach1 (overlay-unique reach-1 fns, region main, sorted SMALLEST-FIRST
— the Phase-21 idiom-mining harvest: easy wins + distill the gcc quirk each reveals, feed forward).
Usage: tools/wave_targets.py --pool tractable --n 24 [--region main|a|any] [--out -]
"""
import argparse, glob, json, os, re, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import backlog
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
STUB_RE = re.compile(r"INCLUDE_ASM\([^,]+,\s*(\w+)\)")
ASM_SUBDIR = "asm/ov_SC01_077/nonmatchings/ov_SC01_077"
# region (fuel_manifest) -> asm subdir. An _a/_o0 fn's .s lives under its Phase-19 SPLIT subdir, not
# the main one — the batch must point drafters at the right .s, else they draft against the wrong asm.
REGION_SUB = {"main": "ov_SC01_077", "a": "ov_SC01_077_a", "o0": "ov_SC01_077_o0"}
def asm_for(region, name):
return f"asm/ov_SC01_077/nonmatchings/{REGION_SUB.get(region, 'ov_SC01_077')}/{name}.s"
# Canonical gcc-quirk residual classes (the cookbook §17–20 taxonomy). A wave studies ONE of these
# at a time (the Phase-18 learning model): bank what the class's idiom reaches, distill the quirk,
# feed it forward. Keyword-matched from the worker's self-reported klass + its where_stuck note.
CLASS_KEYWORDS = [
("REGALLOC", ("regalloc", "register", "$s", " pin", "reg-order", "reg order", "swap")),
("SCHEDULE", ("schedule", "sched", "store-vs-load", "store vs load", "delay slot", "reorder", "operand-order", "operand order")),
("REMAT", ("hoist", "remat", "rematerial", "array-decay", "array decay")),
("STRUCT", ("struct", "field", " type", "layout", "%lo", "array-of-struct", "array of struct", "union")),
("IV", ("iv-combine", "iv combine", "induction", "biv", "halfword rmw")),
("LOOPGUARD", ("loop-guard", "loop guard", "get_condition", "strength-reduc")),
("LOOSE", ("loose-typing", "loose typing", "arity", "conflicting types", "narrow-param")),
("PLUMBING", ("plumbing", "declaration", "extern", "call-site cast", "callee", "no-proto", "sibling decl")),
]
def canon_class(rec):
"""Normalize a backlog record (klass + where_stuck) to a canonical residual class."""
blob = ((rec.get("klass") or "") + " " + (rec.get("where_stuck") or "")).lower()
for name, kws in CLASS_KEYWORDS:
if any(k in blob for k in kws):
return name
return "OTHER"
def live_stubs():
s = set()
for p in glob.glob(os.path.join(REPO, "src/ov_SC01_077/ov_SC01_077*.c")):
s |= set(STUB_RE.findall(open(p).read()))
return s
def backlog_walls():
"""names already logged as 'failed'/'stub' — skip (don't waste agents re-drafting known walls)."""
walls = set()
p = os.path.join(REPO, ".run/backlog.jsonl")
if os.path.exists(p):
for line in open(p):
line = line.strip()
if not line:
continue
r = json.loads(line)
if r.get("status") in ("failed", "stub") and r.get("name"):
walls.add(r["name"])
return walls
def plumbing_blocked():
"""SELF-MATCH-but-gate-REJECTED near-misses (DEF-side / TU loose-typing plumbing). Re-drafting just
reproduces the same byte-correct body the gate rejects again -> NEVER banks via a worker wave
(Phase-21 finding). Skip in POOL selection so agents draft FRESH targets; these stay in the backlog
as recovery-tooling / hand-finish fuel (an improved sig_unify / recovery gate banks them out-of-band).
Signal: closeness==0 (post-recovery match_one MATCH) OR a drafter self-MATCH verdict in where_stuck
('none — MATCH …'). The latter is needed because sig_unify can REGRESS a self-MATCH draft to close>0
post-transform (Phase-19), so it dodges the ==0 filter though the body is byte-correct & gate-blocked."""
blocked = set()
p = os.path.join(REPO, ".run/backlog.jsonl")
if not os.path.exists(p):
return blocked
# Scan ALL raw records, not backlog.load_best(): load_best returns one record/fn and can
# return an early closeness>0 near-miss that MASKS a later 'none — MATCH' self-match-gate-reject
# record (both can share a closeness, e.g. a sig_unify-regressed self-match logged at close 14).
# A fn that EVER self-matched-but-gate-rejected is plumbing-blocked: re-drafting reproduces the
# same gate-rejected body -> skip it from waves (it's recovery-tooling / hand-finish fuel).
for line in open(p):
line = line.strip()
if not line:
continue
r = json.loads(line)
if r.get("status") != "near" or not r.get("name"):
continue
ws = (r.get("where_stuck") or "").strip().lower()
if r.get("closeness") == 0 or ws.startswith("none —") or ws.startswith("none -") or "— match" in ws or "match_one match" in ws:
blocked.add(r["name"])
return blocked
def reserved_walls(min_attempts=2):
"""Near-misses re-drafted >= min_attempts times WITHOUT banking are walls: two independent
toolkit-aware drafters (the initial wave + a smallest-first re-serve with the richer cookbook)
both failed to byte-match. Crackable near-misses crack on attempt 2 (observed: func_801651B8 /
func_801549F8 / func_80153D7C); the survivors are permuter-class schedule/regalloc/iv walls
(func_80140E6C: close=4 on BOTH wave 24 & 25). Smallest-first keeps re-serving them at the front
of every wave (fixed small nins) -> a slow leak that compounds as the band climbs. Skip them from
blind waves; they stay in the backlog for the grinder (permuter) or a TARGETED `--class` re-attempt
(which feeds the drafter the prior stuck-point — a far better shot than another blind draft).
A banked fn isn't a live stub, so it's already excluded by the stubs gate (it never reaches here)."""
import collections
p = os.path.join(REPO, ".run/backlog.jsonl")
if not os.path.exists(p):
return set()
c = collections.Counter()
for line in open(p):
line = line.strip()
if not line:
continue
r = json.loads(line)
if r.get("status") == "near" and r.get("name"):
c[r["name"]] += 1
return {n for n, k in c.items() if k >= min_attempts}
def emit(batch, out):
s = json.dumps(batch, indent=0)
if out == "-":
sys.stdout.write(s + "\n")
else:
open(os.path.join(REPO, out), "w").write(s)
print(f"{len(batch)} targets -> {out}", file=sys.stderr)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--pool", default="tractable",
choices=["tractable", "giants", "o0", "capped", "any-reach134", "reach1"])
ap.add_argument("--class", dest="rclass", default=None,
help="CLASS-GROUPED wave: select backlog near-misses of this residual class "
"(REGALLOC/SCHEDULE/REMAT/STRUCT/IV/LOOPGUARD/LOOSE/PLUMBING/OTHER) to re-attempt")
ap.add_argument("--list-classes", action="store_true",
help="print the backlog residual-class histogram (ranked by leverage) and exit")
ap.add_argument("--n", type=int, default=24)
ap.add_argument("--region", default="main", choices=["main", "a", "any"])
ap.add_argument("--max-nins", type=int, default=150)
ap.add_argument("--include-walls", action="store_true", help="don't skip backlog failed/stub")
ap.add_argument("--out", default="-")
a = ap.parse_args()
# --- CLASS-GROUPED modes (Phase-21 flywheel): operate on the backlog's classified near-misses ---
if a.list_classes:
import collections
recs = [r for r in backlog.load_best() if r.get("status") == "near"]
cnt = collections.Counter(canon_class(r) for r in recs)
lev = collections.Counter()
for r in recs:
lev[canon_class(r)] += (r.get("reach") or 1)
for cls, _ in lev.most_common():
print(f"{cls:10} n={cnt[cls]:3} reach-weight={lev[cls]}")
return
if a.rclass:
rc = a.rclass.upper()
mreg = {t["name"]: t.get("region", "main") # region -> correct split-file asm subdir
for t in json.load(open(os.path.join(REPO, ".run/fuel_manifest.json")))["targets"]}
stubs = live_stubs() # still-unbanked only (a banked fn left the INCLUDE_ASM stub set)
recs = [r for r in backlog.load_best()
if r.get("status") == "near" and canon_class(r) == rc and r.get("name")
and r["name"] in stubs # skip already-banked (stale near records)
and isinstance(r.get("closeness"), int) and r["closeness"] > 0] # genuine near-miss; close==0 = plumbing-blocked (re-draft can't bank)
recs.sort(key=lambda r: (-(r.get("reach") or 1), r.get("closeness") if isinstance(r.get("closeness"), int) else 999))
batch = [{"name": r["name"], "addr": r.get("addr") or ("0x" + r["name"][5:].lower()),
"nins": r.get("nins"), "class": rc, "asm": asm_for(mreg.get(r["name"], "main"), r["name"]),
"ghidra_c": f".run/ghidra_c/{r['name']}.c", "prior_stuck": r.get("where_stuck"),
"prior_closeness": r.get("closeness")}
for r in recs[:a.n]]
emit(batch, a.out)
return
m = json.load(open(os.path.join(REPO, ".run/fuel_manifest.json")))
stubs = live_stubs()
walls = set() if a.include_walls else (backlog_walls() | plumbing_blocked() | reserved_walls())
def ok(t):
if t["name"] not in stubs or not t["cached"]:
return False
if t["name"] in walls:
return False
if a.region != "any" and t["region"] != a.region:
return False
if a.pool == "tractable":
return t["reach_134"] and t["class"] in ("WAVE", "PINS", "STRUCT") and (t["nins"] or 999) <= a.max_nins
if a.pool == "giants":
return t["class"] == "GIANT"
if a.pool == "o0":
return t["class"] == "O0"
if a.pool == "any-reach134":
return t["reach_134"]
if a.pool == "reach1": # overlay-unique fuel (Phase 21 idiom-mining), smallest-first sorted below
return (not t["reach_134"]) and t["class"] in ("WAVE", "PINS", "STRUCT", "STUB") and (t["nins"] or 999) <= a.max_nins
return False
pool = [t for t in m["targets"] if ok(t)]
if a.pool == "capped": # the matched-but-local recovery set (not stubs)
pool = [{"name": n, "addr": "0x" + n[5:].lower(), "nins": None, "class": "CAPPED",
"reach": 134, "leverage": 0} for n in m.get("capped_recovery", [])]
if a.pool == "reach1":
pool.sort(key=lambda t: t.get("nins") or 0) # SMALLEST-FIRST (idiom-mining order, Drew)
else:
pool.sort(key=lambda t: t.get("leverage") or 0, reverse=True)
pool = pool[:a.n]
batch = [{"name": t["name"], "addr": t["addr"], "nins": t["nins"], "class": t["class"],
"asm": asm_for(t.get("region", "main"), t["name"]), "ghidra_c": f".run/ghidra_c/{t['name']}.c"}
for t in pool]
emit(batch, a.out)
if __name__ == "__main__":
main()