diff --git a/tools/backlog.py b/tools/backlog.py index c3c9dd5eb..f7ed62c88 100644 --- a/tools/backlog.py +++ b/tools/backlog.py @@ -37,7 +37,14 @@ DRAFTS = os.path.join(REPO, ".run/backlog_drafts") SRC_GLOB = os.path.join(REPO, "src/ov_SC01_077/ov_SC01_077*.c") STUB_RE = re.compile(r"INCLUDE_ASM\([^,]+,\s*(\w+)\)") FIELDS = ("ts", "addr", "name", "reach", "klass", "nins", "status", - "closeness", "where_stuck", "best_draft", "binary", "source") + "closeness", "where_stuck", "best_draft", "binary", "source", + # Phase-29 Task-12: structured failure telemetry for the permuter-autopsy loop. + # residual = masked_diff.structured_diff()[:cap] — [(idx, mine, target), ...]; the WHAT/WHERE + # of the residual (reg-alloc swap / scheduled-order flip / extra-absent insn). passes_tried = + # the permuter perm_* passes exercised (from permuter_weights) before the plateau — so the + # autopsy can tell "the right lever was tried and failed" (wall) from "the lever was never + # tried" (missing-transform → extend the ILS). Both optional; absent on legacy records. + "residual", "passes_tried") def append_record(rec): diff --git a/tools/gate_stage.py b/tools/gate_stage.py index 571a0f3db..0a1775b7a 100644 --- a/tools/gate_stage.py +++ b/tools/gate_stage.py @@ -112,16 +112,25 @@ def match_one_closeness(fn, cpath, asm, binary=None): if hit is not None: asm = hit.asm_dir if not asm: - return ("fail", None) + return ("fail", None, None) try: - r = sh([PY, "tools/match_one.py", fn, "--c", cpath, "--asm-subdir", asm], timeout=180) + r = sh([PY, "tools/match_one.py", fn, "--c", cpath, "--asm-subdir", asm, "--json"], timeout=180) except subprocess.TimeoutExpired: - return ("fail", None) - first = (r.stdout.strip().splitlines() or ["?"])[0] - if first.startswith("MATCH"): - return ("match", 0) - m = re.search(r"(\d+) mismatch", first) - return ("near", int(m.group(1))) if m else ("fail", None) + return ("fail", None, None) + # Task-12: parse the JSON result line (structured residual for the autopsy); text-fallback on any issue. + try: + j = json.loads([l for l in r.stdout.strip().splitlines() if l.strip()][-1]) + if j.get("status") == "match": + return ("match", 0, []) + if j.get("status") == "near": + return ("near", int(j["closeness"]), j.get("residual")) + return ("fail", None, None) + except Exception: + first = (r.stdout.strip().splitlines() or ["?"])[0] + if first.startswith("MATCH"): + return ("match", 0, []) + m = re.search(r"(\d+) mismatch", first) + return ("near", int(m.group(1)), None) if m else ("fail", None, None) def run_gate(drafts, binary=OV, src=None, asm=None, out=None, good_sha=None, @@ -251,7 +260,7 @@ def _run_gate_locked(drafts, binary, src, asm, out, good_sha, propagate, source_ for fn in [f for f in draft_fns if f not in verified]: cpath = os.path.join(REPO, d, fn + ".c") body = open(cpath).read() if os.path.exists(cpath) else "" - kind, close = match_one_closeness(fn, cpath, asm, binary) if body else ("fail", None) + kind, close, resid = match_one_closeness(fn, cpath, asm, binary) if body else ("fail", None, None) meta = _manifest_class(fn) cm = re.search(r"//\s*@class:\s*(.+)", body) sm = re.search(r"//\s*@stuck:\s*(.+)", body) @@ -272,6 +281,7 @@ def _run_gate_locked(drafts, binary, src, asm, out, good_sha, propagate, source_ backlog.append_record({"addr": meta.get("addr"), "name": fn, "reach": meta.get("reach"), "klass": rclass or meta.get("class"), "nins": meta.get("nins"), "status": status, "closeness": close, "where_stuck": where, "best_draft": draft_path, + "residual": (resid[:24] if resid else None), # Task-12 structured residual telemetry "binary": binary, "source": source_tag}) # binary: lets the grinder gate non-077 near-misses backlog.render() diff --git a/tools/grinder.py b/tools/grinder.py index f7175c83a..6be6916a4 100644 --- a/tools/grinder.py +++ b/tools/grinder.py @@ -187,8 +187,14 @@ def main(): continue win = p16_permute.run_permuter(pd, a.permute_secs, a.j) if win: - open(os.path.join(REPO, DRAFTS, fn + ".c"), "w").write( - p16_permute.winner_to_draft(open(win).read())) + _wtext = p16_permute.winner_to_draft(open(win).read()) + open(os.path.join(REPO, DRAFTS, fn + ".c"), "w").write(_wtext) + # Task-12: durably persist the winning C BEFORE gate_stage banks/propagates/commits, so a + # mid-flight interrupt (kill or a revert of the uncommitted bank) never loses a hard-won + # permuter crack (this session's lesson: 3 wins lost to exactly that). permuter-winners/ is + # the recovery source — winner_to_draft(...) form, re-gateable any time. + _wdir = os.path.join(REPO, ".run", "permuter-winners"); os.makedirs(_wdir, exist_ok=True) + open(os.path.join(_wdir, fn + ".c"), "w").write(_wtext) won.append((fn, binary)); log(f"permuter WON {fn} @ {binary} (close was {r.get('closeness')})") except Exception as e: log(f"{fn}: {e}") diff --git a/tools/masked_diff.py b/tools/masked_diff.py index ab2d5fb95..6a97ca45d 100644 --- a/tools/masked_diff.py +++ b/tools/masked_diff.py @@ -166,6 +166,28 @@ def insns_from_s(s_path): return insns +def structured_diff(mine, tgt): + """The per-instruction relocation-masked mismatch list: [(idx, mine_str, tgt_str), ...] over + two insns_from_* streams. Each entry is a mismatching instruction position; mine_str/tgt_str are + ' ' (or '--' past the end of the shorter stream). len() == the masked mismatch + count (the `closeness`). This is the STRUCTURED RESIDUAL telemetry the permuter-autopsy classifier + reads (Phase-29 Task-12): idx localizes the residual, the word+mnem pair says what codegen differs + (a reg-alloc swap, a scheduled-order flip, an extra/absent instruction). Shared by match_one + + gate_stage's near-record so the same residual an agent reads is the one the autopsy mines.""" + n = max(len(mine), len(tgt)) + diffs = [] + for i in range(n): + mw = mine[i]['word'] if i < len(mine) else None + mask = mask_for(mine[i]['word'], mine[i]['reloc_kind']) if i < len(mine) else 0xFFFFFFFF + me = (mw & mask) if mw is not None else None + tg = (tgt[i]['word'] & mask) if i < len(tgt) else None + if me != tg: + diffs.append((i, + ('%08x %s' % (mine[i]['word'], mine[i]['mnem'])) if i < len(mine) else '--', + ('%08x %s' % (tgt[i]['word'], tgt[i]['mnem'])) if i < len(tgt) else '--')) + return diffs + + def diff_object_object(cand, tgt): """masked mismatch count between two objdump'd objects (permuter scorer). Mask driven by the TARGET's relocs; at masked reloc/jal positions also require reloc-operand (symbol+addend) equality. diff --git a/tools/match_one.py b/tools/match_one.py index 6727f34e0..0c82c33fd 100644 --- a/tools/match_one.py +++ b/tools/match_one.py @@ -18,6 +18,7 @@ longer under-counted (func_80132784 now reads its true 400 ins, not 384). """ import subprocess, re, sys, os, argparse sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import json import masked_diff ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) @@ -31,6 +32,9 @@ ap.add_argument('--work', default=None, ap.add_argument('--o0', action='store_true', help='compile at -O0 (for the _o0 split subsegments: ov_SC01_077_o0.c, whale _o0b — ' 'their target bytes are -O0; an -O2 compile can never match them, Makefile:445)') +ap.add_argument('--json', action='store_true', + help='emit one JSON result line {status,closeness,nins,residual} (Task-12 structured ' + 'residual telemetry for the permuter-autopsy). Still exits 0 on MATCH, 1 otherwise.') a = ap.parse_args() # PRIVATE SCRATCH BY DEFAULT (Phase-28 T5). This tool's own docstring promises "Fully isolated (own @@ -91,21 +95,19 @@ tgt = masked_diff.insns_from_s('%s/%s.s' % (a.asm_subdir, a.fn)) if not mine: print('FAIL: my object has no function', a.fn, '(compile produced nothing?)'); sys.exit(1) -n = max(len(mine), len(tgt)) -diffs = [] -for i in range(n): - mw = mine[i]['word'] if i < len(mine) else None - mask = masked_diff.mask_for(mine[i]['word'], mine[i]['reloc_kind']) if i < len(mine) else 0xFFFFFFFF - me = (mw & mask) if mw is not None else None - tg = (tgt[i]['word'] & mask) if i < len(tgt) else None - if me != tg: - diffs.append((i, - ('%08x %s' % (mine[i]['word'], mine[i]['mnem'])) if i < len(mine) else '--', - ('%08x %s' % (tgt[i]['word'], tgt[i]['mnem'])) if i < len(tgt) else '--')) +# structured residual (shared with gate_stage's near-record + the Task-12 autopsy telemetry) +diffs = masked_diff.structured_diff(mine, tgt) if not diffs and len(mine) == len(tgt): - print('MATCH (%d ins) %s' % (len(mine), a.fn)) + if a.json: + print(json.dumps({"status": "match", "closeness": 0, "nins": len(mine), "residual": []})) + else: + print('MATCH (%d ins) %s' % (len(mine), a.fn)) sys.exit(0) +if a.json: + print(json.dumps({"status": "near" if diffs else "fail", "closeness": len(diffs), + "nins": len(mine), "residual": [[i, me, tg] for i, me, tg in diffs[:48]]})) + sys.exit(1) print('DIFF %s mine=%d ins, target=%d ins, %d mismatched' % (a.fn, len(mine), len(tgt), len(diffs))) print(' idx | MINE | TARGET') for i, me, tg in diffs[:40]: