Files
BFM-decomp/tools/rtu_shadow.py
T
Drew T 197470ff5f fix(gate/tools): baseline-RED skip in the gate, jtbl_pads_fix un-broken three ways, mk_write parse guard, shadow join for 0-bank waves (P31 S61 T10+/S61-10)
gate_stage: a binary on .run/baseline_red.txt (+ fleet_red.txt) refuses its drafts with class
BASELINE-RED before any build — a RED binary rejects every draft gated against it, and 174 of the
resolver's 245 doubly-verified drafts were refused exactly that way (negative-controlled both
directions: RED refused without a build, GREEN still reaches the ladder).
jtbl_pads_fix: (1) OBJ_ERR required the make bracket to start with build/src but make prints
[Makefile:687: build/src/...] — find_drift returned None over failing builds ('no pad-count drift',
six binaries); (2) only the 'consumed N but M' phrasing was handled — the 'more rodata .align
directives than pad specs' direction (a NEW table from a banked switch) now searches declared+1/+2;
(3) write_pads split its line on ':' but ':=' contains a colon, appending a second ': JTBL_PADS'
per write, and wrote via a raw truncating open() — the fifth un-converted mk write site; now
path-split-once + mk_write.
mk_write: refuses a parse-poisoned registry line (R43) — one such line kills EVERY build of EVERY
binary at make parse time, strictly worse than the wipes the line-count floor guards.
rtu_shadow: a 0-bank wave has no commit — gatedness now comes from the ox ledger; baseline-RED
binaries are excluded from the prediction metrics with their count reported (R41).
integration_resolver: stub_removals() nets out carve moves (72 gross -> 63 real in the first pass).
2026-08-26 00:29:05 -06:00

226 lines
10 KiB
Python

#!/usr/bin/env python3
"""rtu_shadow.py — shadow-mode rtu_match beside the wave gate, to decide whether to INVERT the gate.
WHY (P31 S61, frontier-analysis-s60 §3). The gate ladder spends ~3 whole-binary builds per FAILING
draft (stage 0 raw, stage 1 after transforms, stage 2 after sig_unify) and ~1 per banking draft, so
at 1-6% conversion almost every build is spent confirming a failure. `rtu_match` compiles the real
split TU with the draft spliced in — no build tree, no locks, ~2 s, parallel-safe — and could be the
gate's stage 0, so that whole-binary builds are spent only on rtu-MATCH drafts (builds proportional
to BANKS, not drafts). That is a ~20x reduction at current conversion — IF rtu-MATCH predicts
banking and rtu-non-MATCH does not throw away banks. This tool measures exactly that, with zero
behavioural change to the gate (G3/P9 untouched: the SHA gate stays the sole arbiter).
PROSPECTIVE (run BEFORE the wave is gated — no peeking; and NOT while a gate is in flight, because
a gate transiently splices bodies into the very src/ TUs rtu reads):
tools/rtu_shadow.py --wave fa --wave fb -j 32
-> .run/rtu_shadow/<wave>.jsonl one row per draft: rtu verdict, ndiff, reloc pre-filter status
JOIN (run AFTER the gate committed):
tools/rtu_shadow.py --join fa --join fb
-> confusion matrix rtu-verdict x outcome, the false-negative rate (banks rtu would have refused),
and the build accounting (current ladder vs inverted), every figure with its denominator (R41).
Drafts are mapped to their binary through the SHARD'S targets file, never by bare function name — a
wave drafts siblings under the same name in several binaries (54 of wave fa's 470 cards).
"""
import argparse
import collections
import glob
import json
import os
import re
import sys
import time
from concurrent.futures import ThreadPoolExecutor
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import corpus # noqa: E402
import integration_resolver as ir # noqa: E402
REPO = ir.REPO
OUT = ".run/rtu_shadow"
def wave_drafts(w):
"""[(shard, card, draft_path)] with the card taken from that shard's own targets file."""
rows, unmapped, ambiguous = [], 0, 0
for tp in sorted(glob.glob(f".run/wave_{w}_targets.*.json"),
key=lambda p: int(p.rsplit(".", 2)[1])):
i = int(tp.rsplit(".", 2)[1])
try:
targets = json.load(open(tp))
except Exception:
continue
by_name = collections.defaultdict(list)
for c in targets:
by_name[c.get("name") or c.get("fn")].append(c)
for d in glob.glob(f".run/wave_{w}/shard{i}/*.c"):
fn = os.path.basename(d)[:-2]
cs = by_name.get(fn) or []
if not cs:
unmapped += 1
continue
if len(cs) > 1:
ambiguous += 1
continue
rows.append((i, cs[0], d))
return rows, unmapped, ambiguous
def reloc_status(w):
out = {}
if not os.path.exists(ir.REJECTS):
return out
for line in open(ir.REJECTS, errors="replace"):
try:
r = json.loads(line)
except Exception:
continue
if r.get("wave") == w and r.get("fn") and r.get("binary"):
out[(r["binary"], r["fn"])] = (r.get("status"), r.get("shape"))
return out
def shadow(w, jobs):
rows, unmapped, ambiguous = wave_drafts(w)
rl = reloc_status(w)
head = ir.sh(["git", "rev-parse", "--short", "HEAD"], 30).stdout.strip()
ir.say(f"wave {w}: {len(rows)} drafts mapped via shard targets (unmapped {unmapped}, ambiguous-name {ambiguous}); "
f"reloc pre-filter rows for this wave: {len(rl)}; HEAD {head}")
def one(item):
i, c, d = item
b, fn = c["binary"], c.get("name") or c.get("fn")
row = {"wave": w, "shard": i, "binary": b, "fn": fn, "draft": d, "draft_sha": ir.sha(d),
"nins_card": c.get("nins"), "reloc": rl.get((b, fn), ("AGREE?", None))[0],
"reloc_shape": rl.get((b, fn), (None, None))[1], "head": head,
"ts": time.strftime("%Y-%m-%d %H:%M:%S")}
rb = ir.real_binary(b)
if rb is None or rb == "main":
row.update(rtu="UNSUPPORTED", ndiff=None, nins=None, note="main / unknown binary")
return row
idx, err = ir.stub_index(rb)
if err:
row.update(rtu="HARNESS-CORPUS", ndiff=None, nins=None, note=err)
return row
stub = idx.get(fn)
if stub is None:
row.update(rtu="NOT-A-STUB", ndiff=None, nins=None, note="already banked")
return row
work = os.path.join(OUT, "work", w, f"{b}__{fn}")
v = ir.rtu(rb, fn, stub, d, work, corpus.is_o0(stub.path))
row.update(rtu=v["verdict"], ndiff=v.get("ndiff"), nins=v.get("nins"), note=v.get("note", "")[:160])
return row
t0 = time.time()
with ThreadPoolExecutor(max_workers=jobs) as ex:
res = list(ex.map(one, rows))
os.makedirs(OUT, exist_ok=True)
with open(os.path.join(OUT, f"{w}.jsonl"), "w") as f:
for r in res:
f.write(json.dumps(r) + "\n")
c = collections.Counter(r["rtu"] for r in res)
ir.say(f"wave {w}: shadow verdicts in {time.time() - t0:.0f}s: " + " ".join(f"{k}={v}" for k, v in sorted(c.items())))
cg = collections.Counter(r["rtu"] for r in res if r["reloc"] == "AGREE?")
ir.say(f"wave {w}: among drafts the pre-filter will pass (no reject row): " + " ".join(f"{k}={v}" for k, v in sorted(cg.items())))
return res
def wave_commits(w):
out = []
for line in ir.sh(["git", "log", "--format=%h %s", "-600"], 60).stdout.splitlines():
h, _, s = line.partition(" ")
if re.search(r"ox wave %s(\b|$)" % re.escape(w), s):
out.append(h)
return out
def join(w):
p = os.path.join(OUT, f"{w}.jsonl")
if not os.path.exists(p):
ir.say(f"wave {w}: no shadow ledger at {p}")
return
rows = [json.loads(l) for l in open(p) if l.strip()]
# the ox ledger is the authority on whether the wave was GATED: a 0-bank wave has NO commit
# (ox_campaign.commit() returns None on a clean tree), so "no commit" must not read as "not gated"
led = [json.loads(l) for l in open(".run/ox_campaign_ledger.jsonl", errors="replace") if l.strip()]
gated_rows = [r for r in led if r.get("wave") == w and r.get("gated") is not None]
if not gated_rows:
ir.say(f"wave {w}: no gate row in the ox ledger — not gated yet")
return
commits = [r["commit"] for r in gated_rows if r.get("commit")] or wave_commits(w)
banked = set()
pat = re.compile(r'^-.*INCLUDE_ASM\("(asm/[^"]+)",\s*(\w+)\)')
for h in commits:
for line in ir.sh(["git", "show", "--unified=0", h, "--", "src/"], 120).stdout.splitlines():
m = pat.match(line)
if m:
parts = m.group(1).split("/")
banked.add((parts[1] if parts[1] != "nonmatchings" else "main", m.group(2)))
# RED binaries make every verdict about the BINARY, not the draft (S61 fleet audit): report them
# separately and exclude them from the prediction metrics (R41: say what the denominator is).
red = set()
try:
red = {l.strip() for l in open(".run/baseline_red.txt") if l.strip()}
except OSError:
pass
n_red = sum(1 for r in rows if r["binary"] in red)
if n_red:
ir.say(f"wave {w}: {n_red} of {len(rows)} shadowed drafts sit in {len(red & {r['binary'] for r in rows})} "
f"baseline-RED binaries — excluded from the prediction metrics below")
rows = [r for r in rows if r["binary"] not in red]
outcome = collections.Counter()
table = collections.Counter()
for r in rows:
k = (r["binary"], r["fn"])
if r["rtu"] == "NOT-A-STUB":
o = "ALREADY-BANKED"
elif k in banked:
o = "BANKED"
elif r["reloc"] != "AGREE?":
o = "PREFILTER-DROPPED"
else:
idx, err = ir.stub_index(r["binary"])
o = "GATE-REJECTED" if (idx and r["fn"] in idx) else "BANKED-ELSEWHERE"
outcome[o] += 1
table[(r["rtu"], o)] += 1
ir.say(f"wave {w}: gate commit(s) {commits}; outcomes: " + " ".join(f"{k}={v}" for k, v in sorted(outcome.items())))
print(" %-16s %-8s %-14s %-18s %-16s %s" % ("rtu verdict", "BANKED", "GATE-REJECTED", "PREFILTER-DROPPED", "BANKED-ELSEWHERE", "ALREADY"))
for v in sorted({r["rtu"] for r in rows}):
print(" %-16s %-8d %-14d %-18d %-16d %d" % (v, table[(v, "BANKED")], table[(v, "GATE-REJECTED")],
table[(v, "PREFILTER-DROPPED")], table[(v, "BANKED-ELSEWHERE")],
table[(v, "ALREADY-BANKED")]))
gated = [r for r in rows if r["reloc"] == "AGREE?" and r["rtu"] not in ("NOT-A-STUB", "UNSUPPORTED", "HARNESS-CORPUS")]
g_b = [r for r in gated if (r["binary"], r["fn"]) in banked]
g_r = [r for r in gated if (r["binary"], r["fn"]) not in banked]
m_b = sum(1 for r in g_b if r["rtu"] == "MATCH")
m_r = sum(1 for r in g_r if r["rtu"] == "MATCH")
n_m = m_b + m_r
print(f" gated drafts {len(gated)}: banked {len(g_b)}, rejected {len(g_r)}")
print(f" P(bank | rtu MATCH) = {m_b}/{n_m}" + (f" = {100.0 * m_b / n_m:.1f}%" if n_m else ""))
print(f" false-negative (banks rtu would refuse) = {len(g_b) - m_b}/{len(g_b)}"
+ (f" = {100.0 * (len(g_b) - m_b) / len(g_b):.1f}%" if g_b else ""))
cur = len(g_b) * 1 + len(g_r) * 3
inv = m_b * 1 + m_r * 3
print(f" whole-binary builds, current ladder ~ {cur} (1/bank + 3/reject) vs inverted ~ {inv} (rtu-MATCH only)"
+ (f" -> {cur / max(1, inv):.1f}x fewer" if inv else ""))
dropped_match = sum(1 for r in rows if r["reloc"] != "AGREE?" and r["rtu"] == "MATCH")
print(f" pre-filter-dropped drafts that rtu-MATCH the real TU (never gated): {dropped_match}")
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--wave", action="append", default=[], help="prospective shadow over this wave's drafts")
ap.add_argument("--join", action="append", default=[], help="join a shadowed wave with its gate outcome")
ap.add_argument("-j", "--jobs", type=int, default=32)
a = ap.parse_args()
os.chdir(REPO)
for w in a.wave:
shadow(w, a.jobs)
for w in a.join:
join(w)
return 0
if __name__ == "__main__":
sys.exit(main())