Files
BFM-decomp/tools/residual_rules_b.py
Drew T 9fc27960e9 feat(integration): teach the rest of the toolkit about §378 (self-caller cast)
The lever existed but nothing downstream applied it. Proof it mattered: a wave
agent this session diagnosed its own blocker as "§378 THE SELF-CALLER CAST, a
TU-level fix (cast_self_callers.py) that requires editing src/, which I'm not
permitted to touch" — the knowledge propagated, the automation did not.

* recover_integration.py: NEW "self-cast" stage (tier=binary), so the driver can
  run the whole chain as --stages arity,self-cast. The docstring states WHY the
  order is not arbitrary: self-cast answers the error that "arity" CREATES.
* residual_rules_b.py: both decl-conflict tiers now prescribe the full chain
  instead of "route to integration / budget for banking", and
  NOCOMPILE-UNDECLARED-FIXED now says outright NOT to gate the autodecl arm (it
  is a second conflicting declaration in the real TU).
* wave-playbook §4b: replaced the stale two-step recipe with the three-step
  chain, the one-driver form, the callee variant, and the MANDATORY
  --undo-journal.
* SETUP.md: full inventory row (R21) — it had zero mentions.

Not wired, deliberately: gate_stage's ladder rewrites DRAFTS via _xform, while
this edits the TU; a src-side edit inside the automatic gate needs
revert-on-failure, which recover_integration already owns.

Still open: a draft_prechecks rule to catch the self-decl conflict statically,
before a build is spent. The new stage's plumbing is verified (CLI + candidate
selection); its functional end-to-end run is NOT — gate12 held the tree.
2026-09-01 11:03:06 -06:00

1064 lines
55 KiB
Python

#!/usr/bin/env python3
"""residual_rules_b.py — residual-SHAPE -> cookbook-rule classifier (head-to-head implementation B).
Given a candidate C draft that does NOT byte-match, say WHY and prescribe the fix, by recognising
the residual's mechanical shape. Sits ON TOP of tools/match_one.py (the measurement instrument) and
tools/residual_class.py (the deterministic klass) — this module adds the cookbook-rule layer.
DESIGN CHOICES THAT DIFFER FROM THE OBVIOUS APPROACH
1. NO DISASSEMBLY TEXT IS EVER PARSED. The two sides of a match_one residual are disassembled by
different tools (objdump vs the splat .s comments) and disagree on `$` prefixes, operand order
display, and constant base (`30` vs `0x1E`). Every equality decision here is made on the raw
32-bit WORD (each residual row's text begins with 8 hex digits; --emit-streams carries the full
word streams), decoded with residual_class.decode(). Formatting cannot lie about a word.
2. TARGET-SIDE ORACLES FIRE FIRST. A §332 wall is a property of the TARGET bytes, not of any
draft — wall_sweep.scan() (reused, not rewritten) decides it before the draft is even read.
Likewise the §188 epilogue wall is decided by a REAL second compile (cc1 raw + `as -O2`
bypass, oracle_reorder semantics): bypass == 0 diffs while the pinned triple > 0 is PROOF the
C is right and the pinned assembler can never emit it. Certainty comes from oracles, not
pattern confidence.
3. FULL STREAMS, NOT THE CAPPED RESIDUAL. match_one caps `residual` at 48 rows — exactly enough
to hide a drift signature. --emit-streams provides the uncapped word streams; the aligned diff
is recomputed here with masked_diff.mask_for (match_one semantics, mask from MY side).
THE RULES, each with its TELL, FIX, CONFIDENCE and — required — WHAT WOULD REFUTE IT
W332 maspsx %lo-in-delay-slot wall [certain]
TELL wall_sweep.scan(target .s) > 0 hits (a delay slot holding a macro tail `%lo(...)`).
FIX none from C: toolchain wall. Ledger it, never draft, never escalate (§332/§332a).
REFUTED BY a function with a hit that the whole-binary gate nevertheless banks from C.
INTEG standalone MATCH [certain]
TELL match_one status == "match" (closeness 0).
FIX the C is byte-correct; the blocker is TU plumbing — reconcile ladder (§58/§59, §367).
REFUTED BY a standalone MATCH whose whole-TU compile fails for a CODEGEN (not decl) reason.
W188 epilogue/assembler-reorder wall, oracle-proven [certain]
TELL the SAME draft, cc1 raw output (move->addu) + `as -O2` bypass, diffs == 0 while the
pinned maspsx + `as -O1` path diffs > 0.
FIX none from C under the pinned triple (§188, tools/oracle_reorder.py). Stop grinding.
REFUTED BY a bypass-0 function later banked by a pure C change under the pinned triple.
R368 reload-remat constant [certain / high]
TELL a wrong-register diff row on a COMMUTATIVE op whose shared operand crosses slots —
mine (x, c) vs target (c', x) — i.e. the constant's register is operand-2 in mine
and operand-1 in the target (§368: cse forces constants to op-2; reload-remat's
rematerialised constant lands op-1). CERTAIN when a companion diff row loads the SAME
immediate into c/c' from $zero (the constant's materialisation moved registers too).
FIX function-scope single-set local for the constant, assigned once at entry, used
constant-first (§368). Pins measured WORSE — do not pin.
REFUTED BY a swap-row residual that §368's lever does not close and RTL dumps attribute
elsewhere (cf. §372, whose rows have NO commutative operands — that absence is the border).
R372 copy-capture pair [high]
TELL a wrong-register diff row where MINE reads the DESTINATION of a nearby MATCHING copy
insn (addu $dst,$src,$zero == `move`) whose source is what the TARGET reads. cse
make_regs_eqv / optimize_reg_copy_1 re-based my use onto the copy dest (§372).
FIX spell the copy as `P = X + zr` with `register s32 zr __asm__("$0")` — byte-identical
encoding, kills both passes (§372).
REFUTED BY the mirror direction dominating (target reads the copy dest): then the ORIGINAL
kept the capture and the +zr edit is exactly wrong (reported as R372-INV, medium).
R370 reorg slot-steal not reproduced [high]
TELL LENGTH-DRIFT +1 whose extra MINE row is a `nop` in a branch delay slot, and the
TARGET's slot word occurs in MINE within the 8 insns before the branch — reorg stole
the first eligible insn scanning back; my accumulator was ineligible (§370/§373).
FIX spell the value SPLIT so it accumulates outside the register-heavy tail (§370); or
the §373.2 pin that deletes the sched2 anti-dep.
REFUTED BY the "stolen" insn being a %lo macro tail (that is W332 — checked first).
R373 target fills a delay slot where mine holds nop (aligned) [high]
TELL >=half of the aligned diff rows are mine-nop vs target-insn immediately after a
branch/jump (the DELAY-SLOT klass, target-filled direction).
FIX §373 (dead-reset cse-breaker; anti-dep pin) / §370 (eligibility) / §176-A statement
order around the call.
REFUTED BY wall_sweep hits (then it is W332) — checked first.
R5A gcc stole a slot the target leaves as nop (reverse direction) [high]
TELL aligned diff rows mine-insn vs target-NOP in a delay slot.
FIX zero-byte `__asm__("")` fence before the branch — reorg stop_search_p halts the
filler (§5a + §34; §336: at the BOTTOM of a duplicated block).
REFUTED BY the row not sitting in any delay slot (plain reorder — not this rule).
R366 group_case_nodes stacked-label merge [high]
TELL mine SHORTER (length drift < 0, |d| 2..40) AND the draft C stacks consecutive case
labels (`case 5: case 6:`) — gcc merged them into one range node (§366).
FIX give every case its own duplicated body + break; let cross_jump re-fold (§366/§298).
REFUTED BY unstacking not recovering the drift (then the node count difference is real
— e.g. R339's missing node).
R339 missing third case node [high]
TELL mine SHORTER by exactly 2 and the target's extra rows at the drift point decode to
slti/sltiu + branch — the low-bound range test only >=3 case nodes emit (§339).
FIX add the missing case arm (e.g. `case 0: return;`).
REFUTED BY the extra pair not being compare+branch.
R333 frame-size drift [high]
TELL every imm-diff row is $sp-relative (mem base $sp, or `addiu $sp,$sp,K`), same ops
and regs, ONE constant delta across all rows (§333/§334/§335/§193-I).
FIX declare the missing bytes as an unreferenced TRAILING aggregate local (§333); check
§334 (a reload spill slot rounds to 8B) and §335 (extern u16[] dead temps).
REFUTED BY the delta not being constant, or rows off non-$sp bases (struct layout, not frame).
R35 slti/sltiu signedness flip [high]
TELL diff rows differing ONLY slti<->sltiu with equal regs+imm (§35).
FIX keep a separate SIGNED copy of the unsigned load; spell each bound as its own if (§21).
REFUTED BY regs/imm also differing (then it is not the pure signedness class).
RPOL branch polarity [high]
TELL diff rows differing ONLY beq<->bne (same regs) (§3-T4, BRANCH-POLARITY klass).
FIX invert the source condition / swap arms (§3-T4, §32.2).
RJINT internal `j` to the wrong label [high]
TELL a diff row where BOTH words are `j` (op 2) — same instruction, different target:
break-vs-return / goto to the wrong label (§195-D, §162).
FIX re-check which arm falls through vs jumps: the surviving cross-jump copy is the LATER
one; a BACKWARD j into a sibling arm is a source `goto` (§162).
R31 clean register permutation [medium]
TELL klass REGALLOC-PERM: one injective map explains every diff (cycles named).
FIX §31 decl/stmt-order levers, §17 register pins, permuter `regalloc` profile.
(medium on purpose: the CLASS is certain, the specific lever is not.)
NOCOMPILE [high for decl conflicts]
TELL cc1/cpp/as failure; stderr names a decl conflict (§337/§367) or other blocker.
FIX adopt the TU's banked spelling / §37 asm-label alias; finish on the whole-TU compile.
REDRAFT (pass-through of residual_class SIZE-MISMATCH) [medium — rebadged]
The draft is a different-sized function: re-draft, never seed-tweak. Marked medium and
flagged `rebadged` because the knowledge predates this tool (residual_class.py).
SCORING HONESTY. `certain`/`high` are claimed ONLY for oracle-backed or double-confirmed tells.
The eval summary separates compile-fails and rebadged verdicts so the money metric cannot be
inflated by them. A sweep that returns 0 must prove it can return non-zero: --selftest runs the two
development cases (proven §368 / proven §332) and refuses to pass unless both classify correctly.
USAGE
python3 tools/residual_rules_b.py --binary ov_SC03_105 --fn func_80187A30 \
--draft .run/S68o1/opus/func_80187A30.c # one function, human report
python3 tools/residual_rules_b.py --selftest # the two dev cases (must pass)
python3 tools/residual_rules_b.py --eval .run/S68_rules_eval_set.json --jobs 8
# held-out sweep + score table
"""
import argparse
import collections
import json
import os
import re
import subprocess
import sys
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(REPO, "tools"))
import masked_diff # noqa: E402 — mask_for / insns_from_s / strip_scalar_typedefs
import residual_class # noqa: E402 — decode() and the deterministic klass
import wall_sweep # noqa: E402 — the §332 detector, REUSED not rewritten
WORK_ROOT = os.path.join(REPO, ".run", "rules_b")
# ---------------------------------------------------------------------------------------------
# word-level helpers (no disassembly text is ever consulted)
# ---------------------------------------------------------------------------------------------
_COMMUTATIVE = {0x18, 0x19, 0x20, 0x21, 0x24, 0x25, 0x26} # mult multu add addu and or xor
_CMP_FN = {0x2A, 0x2B} # slt sltu (R-type)
def _op(w):
return w >> 26
def _fields(w):
return (w >> 21) & 31, (w >> 16) & 31, (w >> 11) & 31, w & 0xFFFF # rs rt rd imm
def is_branch(w):
op = _op(w)
if op in (2, 3, 4, 5, 6, 7): # j jal beq bne blez bgtz
return True
if op == 1: # REGIMM bltz/bgez/…
return True
if op == 0 and (w & 63) in (8, 9): # jr jalr
return True
if op in (16, 17, 18, 19) and ((w >> 21) & 31) == 8: # BCzT/BCzF
return True
return False
def _is_move(w):
"""addu/or $rd,$src,$zero (or $rd,$zero,$src) -> (dst, src) or None. gcc's `move`."""
if _op(w) != 0 or (w & 63) not in (0x21, 0x25):
return None
rs, rt, rd, _ = _fields(w)
if rd == 0:
return None
if rt == 0 and rs != 0:
return rd, rs
if rs == 0 and rt != 0:
return rd, rt
return None
def _const_load(w):
"""addiu/ori/li $rt,$zero,K -> (rt, K) or None."""
if _op(w) in (9, 13): # addiu / ori
rs, rt, _rd, imm = _fields(w)
if rs == 0:
return rt, imm
return None
class Streams:
"""Aligned view over the two full word streams, match_one mask semantics."""
def __init__(self, mine_words, tgt_words, mine_relocs):
self.mine = mine_words
self.tgt = tgt_words
self.masks = []
for i, w in enumerate(mine_words):
rk = mine_relocs.get(str(i)) or mine_relocs.get(i)
self.masks.append(masked_diff.mask_for(w, rk["kind"] if rk else None))
def eqm(self, i, j):
if i >= len(self.mine) or j >= len(self.tgt):
return False
return (self.mine[i] & self.masks[i]) == (self.tgt[j] & self.masks[i])
def diffs(self):
return [i for i in range(max(len(self.mine), len(self.tgt))) if not self.eqm(i, i)]
# ---------------------------------------------------------------------------------------------
# the measurement step — match_one as a subprocess (the sanctioned instrument)
# ---------------------------------------------------------------------------------------------
class Banked(Exception):
"""The function is no longer an open stub — corpus.matched (sig MINUS stubs, the invariant-
derived oracle) says it already builds from C byte-exactly. The 28-of-113 eval-set entries in
this state are what a naive `[s for s in stubs if s.symbol==fn][0]` resolver CRASHES on: the
functions banked after the set was drawn (S68 closed 23), so their INCLUDE_ASM — and usually
their .s — is gone. 'Already banked, the draft is obsolete' is a real classification, not an
error."""
def resolve_stub(binary, fn):
"""(asm_dir, s_path) via corpus.stubs; raises Banked for a since-matched function; falls back
to a tree glob so a carve-dirty binary still yields the target .s when it exists."""
import corpus
try:
for s in corpus.stubs(binary).values():
if s.symbol == fn:
return s.asm_dir, os.path.join(REPO, s.asm_dir, fn + ".s")
except Exception as e:
err = repr(e)
else:
err = "no open stub named %s in %s" % (fn, binary)
try:
m = corpus.matched(binary)
addr = None
if re.fullmatch(r"func_[0-9A-Fa-f]{8}", fn):
addr = int(fn[5:], 16)
if (addr in m) or any((r.get("name") or "").lower() == fn.lower() for r in m.values()):
raise Banked("%s/%s builds from C already (corpus.matched, sig-minus-stubs)"
% (binary, fn))
except Banked:
raise
except Exception:
pass
root = os.path.join(REPO, "asm") if binary == "main" else os.path.join(REPO, "asm", binary)
hits = subprocess.run(["find", root, "-name", fn + ".s", "-path", "*nonmatchings*"],
capture_output=True, text=True).stdout.split()
if binary == "main": # main's tree root also holds every overlay
hits = [h for h in hits
if os.path.relpath(h, os.path.join(REPO, "asm")).split(os.sep)[0] == "nonmatchings"]
if len(hits) == 1:
return os.path.relpath(os.path.dirname(hits[0]), REPO), hits[0]
raise LookupError("%s/%s: %s; glob found %d candidates" % (binary, fn, err, len(hits)))
def run_match_one(fn, draft, asm_dir, tag):
wd = os.path.join(WORK_ROOT, tag)
os.makedirs(wd, exist_ok=True)
streams_path = os.path.join(wd, "streams.json")
p = subprocess.run([sys.executable, "tools/match_one.py", fn, "--c", draft,
"--asm-subdir", asm_dir, "--json", "--work", wd,
"--emit-streams", streams_path],
capture_output=True, text=True, cwd=REPO, timeout=300)
last = p.stdout.strip().splitlines()[-1] if p.stdout.strip() else ""
try:
res = json.loads(last)
except (json.JSONDecodeError, ValueError):
raise RuntimeError("match_one emitted no JSON (rc=%d): stdout=%r stderr=%r"
% (p.returncode, p.stdout[-400:], p.stderr[-400:]))
streams = None
if os.path.exists(streams_path):
with open(streams_path) as f:
sj = json.load(f)
streams = Streams(sj["mine"], sj["tgt"], sj.get("mine_relocs", {}))
return res, streams
def bypass_as_o2(fn, draft, s_path, tag):
"""The §188 oracle (oracle_reorder semantics): cc1 raw, move->addu, `as -O2`, NO maspsx.
Returns the masked diff count, or None when the bypass path itself fails (no verdict)."""
wd = os.path.join(WORK_ROOT, tag, "bypass")
os.makedirs(wd, exist_ok=True)
src = masked_diff.strip_scalar_typedefs(open(draft).read())
if '#include "common.h"' not in src:
src = '#include "common.h"\n' + src
open(os.path.join(wd, "t.c"), "w").write(src)
cpp = ("mipsel-linux-gnu-cpp -lang-c -Iinclude -undef -Wall -fno-builtin -Dmips -D__GNUC__=2 "
"-D__OPTIMIZE__ -Dpsx -D_PSYQ -D_MIPSEL -D_LANGUAGE_C").split() + [os.path.join(wd, "t.c")]
cc1 = ["tools/bin/gcc-2.7.2-psx/cc1"] + "-quiet -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker".split()
p = subprocess.run(cpp, capture_output=True, cwd=REPO)
if p.returncode:
return None
q = subprocess.run(cc1, input=p.stdout, capture_output=True, cwd=REPO)
if q.returncode:
return None
asm = re.sub(r"\tmove\t(\$\w+),(\$\w+)",
lambda m: "\taddu\t%s,%s,$0" % (m.group(1), m.group(2)), q.stdout.decode())
r = subprocess.run(["mipsel-linux-gnu-as"] + "-Iinclude -march=r3000 -mtune=r3000 -no-pad-sections -O2 -G0".split()
+ ["-o", os.path.join(wd, "t.o")], input=asm.encode(), capture_output=True, cwd=REPO)
if r.returncode:
return None
try:
mine = masked_diff.insns_from_object(os.path.join(wd, "t.o"), fn)
tgt = masked_diff.insns_from_s(s_path)
if not mine:
return None
return len(masked_diff.structured_diff(mine, tgt))
except Exception:
return None
# ---------------------------------------------------------------------------------------------
# the rules
# ---------------------------------------------------------------------------------------------
def _fire(rules, rule, conf, section, evidence, fix, **extra):
rules.append(dict(rule=rule, confidence=conf, section=section,
evidence=evidence, fix=fix, **extra))
def rule_368(st, diffs, rules):
"""Reload-remat constant: commutative-swap wrong-register rows + moved constant load."""
swaps, consts = [], []
for i in diffs:
if i >= len(st.mine) or i >= len(st.tgt):
continue
mw, tw = st.mine[i], st.tgt[i]
cl_m, cl_t = _const_load(mw), _const_load(tw)
if cl_m and cl_t and cl_m[1] == cl_t[1] and cl_m[0] != cl_t[0]:
consts.append((i, cl_m[0], cl_t[0], cl_m[1]))
continue
if _op(mw) == 0 and _op(tw) == 0 and (mw & 63) == (tw & 63) and (mw & 63) in _COMMUTATIVE:
m_rs, m_rt, m_rd, _ = _fields(mw)
t_rs, t_rt, t_rd, _ = _fields(tw)
if m_rd != t_rd:
continue
# ANTI-REORDER GUARD (held-out audit, func_80020DA4): in a schedule-reordered stream
# of many mults, an aligned-position compare can pair two DIFFERENT mults and read
# them as an operand swap. A genuine §368 swap row's words exist NOWHERE else in the
# residual; a reorder's words re-occur verbatim at another diff position.
reorder = (any(j != i and j < len(st.tgt) and st.tgt[j] == mw for j in diffs)
or any(j != i and j < len(st.mine) and st.mine[j] == tw for j in diffs))
if reorder:
continue
# shared operand crossed slots: mine (x, c) vs target (c', x)
if m_rs == t_rt and m_rt != t_rs and (m_rs, m_rt) != (t_rs, t_rt):
swaps.append((i, m_rt, t_rs)) # (row, c_mine, c_target)
if not swaps:
return
# constant confirmation: a const row whose register pair matches a swap row's pair
confirmed = [s for s in swaps for c in consts if (s[1], s[2]) == (c[1], c[2])]
conf = "certain" if confirmed else "high"
_fire(rules, "R368-RELOAD-REMAT", conf, "§368",
"commutative-swap wrong-register row(s) at %s (constant reg op-2 in mine, op-1 in "
"target)%s" % ([s[0] for s in swaps],
"; SAME immediate 0x%X loads into the swapped registers at %s"
% (consts[0][3], [c[0] for c in consts]) if confirmed else ""),
"function-scope single-set local for the constant, assigned once at entry, used "
"constant-first (§368). Do NOT pin — pins measured worse (18/14 vs 8).")
def _src_regs(w):
"""The GPR numbers this word READS (source roles only — a dest-register difference is an
allocation choice, not a captured use; counting it fabricated a §372 hit on func_80186020)."""
op = _op(w)
rs, rt, rd, _ = _fields(w)
if op == 0:
fn = w & 63
if fn in (0, 2, 3): # sll/srl/sra: rt is the source
return [rt]
if fn in (4, 6, 7): # sllv/srlv/srav: rs=amount, rt=value
return [rs, rt]
if fn in (8, 9): # jr/jalr
return [rs]
if fn in (16, 18): # mfhi/mflo
return []
if fn in (17, 19): # mthi/mtlo
return [rs]
return [rs, rt] # alu 3-reg, mult/div, syscall-ish
if op == 1 or op in (6, 7): # regimm / blez / bgtz
return [rs]
if op in (4, 5): # beq/bne
return [rs, rt]
if 8 <= op <= 14: # addi..xori: rs source, rt dest
return [rs]
if op == 15: # lui
return []
if 32 <= op <= 38: # loads: base only
return [rs]
if 40 <= op <= 46: # stores: base AND value
return [rs, rt]
if 48 <= op <= 51 or 56 <= op <= 59: # lwc/swc: base
return [rs]
return [rs, rt]
def rule_372(st, diffs, rules, klass=None):
"""Copy-capture: a wrong-register row READS the dest of a nearby MATCHING copy. §372's own
wording scopes the tell to REGALLOC residuals; on a structural klass (WIDTH/OPCODE-MIXED/…)
the same register coincidence rides on a different instruction shape, so confidence drops."""
diffset = set(diffs)
fwd_hits, inv_hits = [], []
for i in diffs:
if i >= len(st.mine) or i >= len(st.tgt):
continue
mw, tw = st.mine[i], st.tgt[i]
# same operation, register fields differ, immediate agrees under the mask
try:
sm, rm, im = residual_class.decode(mw)
stg, rt_, it = residual_class.decode(tw)
except residual_class.UnknownOpcode:
continue
if sm != stg or len(rm) != len(rt_):
continue
if (im != it) and (st.masks[i] & 0xFFFF):
continue
pairs = [(a, b) for a, b in zip(_src_regs(mw), _src_regs(tw)) if a != b]
if not pairs:
continue
lo, hi = max(0, i - 10), min(len(st.mine), i + 11)
for j in range(lo, hi):
if j in diffset or j >= len(st.tgt):
continue # the copy insn itself must MATCH
mv = _is_move(st.mine[j])
if not mv:
continue
dst, src = mv
for a, b in pairs:
if (a, b) == (dst, src):
fwd_hits.append((i, j, dst, src)) # mine reads dest, target reads src
elif (a, b) == (src, dst):
inv_hits.append((i, j, dst, src))
reg_klass = klass in (None, "REGALLOC-PERM", "REGALLOC-LOCAL")
if fwd_hits:
_fire(rules, "R372-COPY-CAPTURE", "high" if reg_klass else "medium", "§372",
"wrong-register row(s) %s read the DEST of matching copy insn(s) at %s (cse "
"make_regs_eqv / optimize_reg_copy_1 re-based the use onto the copy dest)"
% (sorted({h[0] for h in fwd_hits}), sorted({h[1] for h in fwd_hits})),
"spell the copy as `P = X + zr` with `register s32 zr __asm__(\"$0\")` — "
"byte-identical addu encoding, defeats both capture passes (§372).")
elif inv_hits:
_fire(rules, "R372-INV-COPY-KEPT", "medium", "§372",
"TARGET reads the copy DEST where mine reads the src (rows %s, copies at %s): the "
"original source kept the capture alive"
% (sorted({h[0] for h in inv_hits}), sorted({h[1] for h in inv_hits})),
"let the copy capture: order the uses AFTER the copy in source order, or keep a "
"live copy of the incoming argument (§372 entry-copy flavor, WITHOUT +zr).")
def _rediff_without(st, drop):
"""Masked diff count + positions of (mine minus the index `drop`) vs tgt."""
m = [(w, k) for i, (w, k) in enumerate(zip(st.mine, st.masks)) if i != drop]
out = []
for i in range(max(len(m), len(st.tgt))):
if i >= len(m) or i >= len(st.tgt):
out.append(i)
continue
w, k = m[i]
if (w & k) != (st.tgt[i] & k):
out.append(i)
return m, out
def rule_slot(st, verdict, diffs, rules):
"""§370 slot-steal / §373 slot-fill / §5a over-eager fill.
THE +1 SHAPE, derived by construction, not assumed: a slot-steal the draft fails to reproduce
displaces the stolen insn (mine has it BEFORE the branch, the target has it IN the slot) AND
inserts a nop — so the residual is never a clean one-point tail drift. The operational test is
REMOVE THE SLOT NOP AND RE-DIFF: if what remains is equal, the target filled the slot from the
fall-through; if what remains is a small same-multiset window, the filler was stolen from
upstream (reorg fill_simple_delay_slots scanning back)."""
d = len(st.mine) - len(st.tgt)
if d == 1 and diffs:
cands = [s for s in range(1, len(st.mine))
if st.mine[s] == 0 and is_branch(st.mine[s - 1])
and diffs[0] - 12 <= s <= diffs[-1] + 1]
for s in cands:
m2, d2 = _rediff_without(st, s)
if not d2:
_fire(rules, "R373-SLOT-FILL", "high", "§373/§370/§176-A",
"removing MY slot nop at %d makes the streams EQUAL — the target fills that "
"branch's delay slot from the fall-through where mine emits nop" % s,
"statement order around the branch/call decides the filler (§176-A); §373 "
"dead-reset / §373.2 anti-dep pin / §370 eligibility.")
return
if len(d2) <= 12 and d2[-1] - d2[0] <= 16:
win = range(d2[0], min(d2[-1] + 1, len(m2), len(st.tgt)))
ms = collections.Counter(m2[i][0] & m2[i][1] for i in win)
ts = collections.Counter(st.tgt[i] & m2[i][1] for i in win)
if ms == ts:
_fire(rules, "R370-SLOT-STEAL", "high", "§370/§373",
"mine +1 with a nop in the delay slot at %d; removing it leaves a "
"same-multiset window %s — the target's slot holds an insn reorg STOLE "
"from upstream; my spelling made it ineligible" % (s, [d2[0], d2[-1]]),
"spell the accumulating value SPLIT (early/mid/late statements) so it "
"lives outside the tail's registers and becomes slot-eligible (§370); "
"or the §373.2 pin that deletes the sched2 anti-dep.")
return
# aligned nop-vs-insn rows
tgt_filled, mine_filled = [], []
for i in diffs:
if i == 0 or i >= len(st.mine) or i >= len(st.tgt):
continue
after_branch = is_branch(st.mine[i - 1]) or is_branch(st.tgt[i - 1])
if not after_branch:
continue
if st.mine[i] == 0 and st.tgt[i] != 0:
tgt_filled.append(i)
elif st.tgt[i] == 0 and st.mine[i] != 0:
mine_filled.append(i)
if tgt_filled and len(tgt_filled) * 2 >= len(diffs):
_fire(rules, "R373-SLOT-FILL", "high", "§373/§370",
"target fills delay slot(s) at %s where mine emits nop" % tgt_filled,
"§373 dead-reset cse-breaker / §373.2 anti-dep pin / §370 split-accumulator "
"eligibility; check §176-A statement order first on a call slot.")
elif mine_filled and len(mine_filled) * 2 >= len(diffs):
_fire(rules, "R5A-FENCE", "high", "§5a/§34/§336",
"MINE fills delay slot(s) at %s the target leaves as nop — gcc's filler was more "
"eager than the original's" % mine_filled,
"zero-byte `__asm__(\"\")` fence: reorg stop_search_p halts the filler (§5a, §34); "
"place it at the BOTTOM of a duplicated block (§336).")
_CASE_RE = re.compile(r"case\s+(0[xX][0-9a-fA-F]+|\d+)\s*:\s*case\s+(0[xX][0-9a-fA-F]+|\d+)\s*:")
def rule_366(draft_text, verdict, rules):
if verdict.get("klass") not in ("LENGTH-DRIFT", "SIZE-MISMATCH"):
return
d = verdict.get("detail", {}).get("delta", 0)
if not (-40 <= d <= -2):
return
stacked = [(int(a, 0), int(b, 0)) for a, b in _CASE_RE.findall(draft_text)]
consec = [p for p in stacked if p[1] == p[0] + 1 or p[0] == p[1] + 1]
if consec:
_fire(rules, "R366-STACKED-CASES", "high", "§366",
"mine SHORTER by %d and the draft stacks consecutive case labels %s — "
"group_case_nodes merged them into range nodes" % (-d, consec[:4]),
"give EVERY case its own duplicated body + break; order arms so each family's "
"cross_jump fold lands after its last contributing arm (§366, §298).")
def rule_339(st, verdict, rules):
if verdict.get("klass") != "LENGTH-DRIFT" or verdict.get("detail", {}).get("delta") != -2:
return
k = verdict["detail"].get("at", 0)
if k + 1 >= len(st.tgt):
return
w0, w1 = st.tgt[k], st.tgt[k + 1]
is_cmp = _op(w0) in (10, 11) or (_op(w0) == 0 and (w0 & 63) in _CMP_FN) # slti/sltiu/slt/sltu
if is_cmp and is_branch(w1):
_fire(rules, "R339-MISSING-CASE-NODE", "high", "§339",
"mine shorter by exactly 2 and the target's extra pair at %d is compare+branch "
"(%08x/%08x) — the low-bound range test only >=3 case nodes emit" % (k, w0, w1),
"a case node is missing from the draft: add the missing arm (e.g. `case 0: "
"return;`) — read the case count off the range test (§339).")
def rule_333(st, diffs, rules):
if not diffs or len(st.mine) != len(st.tgt):
return
deltas, n_sp = set(), 0
for i in diffs:
if i >= len(st.mine) or i >= len(st.tgt):
return
mw, tw = st.mine[i], st.tgt[i]
try:
sm, rm, im = residual_class.decode(mw)
stg, rt_, it = residual_class.decode(tw)
except residual_class.UnknownOpcode:
return
if sm != stg or rm != rt_ or im is None or it is None:
return
base_sp = any(role == "g" and slot == 0 and v == 29 for role, slot, v in rm)
is_mem = sm[0] == "m"
is_sp_add = sm == ("i", 9) and base_sp
if not ((is_mem and base_sp) or is_sp_add):
return
n_sp += 1
dv = (it - im) & 0xFFFF
dv = dv if dv < 0x8000 else dv - 0x10000
# the sp-adjust rows (`addiu $sp,$sp,-F`) move OPPOSITE to the offset rows when the frame
# grows: normalise to "frame delta" sign so one frame change reads as ONE delta.
deltas.add(-dv if is_sp_add else dv)
if n_sp and len(deltas) == 1:
dv = deltas.pop()
_fire(rules, "R333-FRAME-SIZE", "high", "§333/§334/§335/§193-I",
"all %d diff rows are $sp-relative with ONE constant delta %+d — the frame differs, "
"the body does not" % (n_sp, dv),
"declare the missing bytes as an UNREFERENCED trailing aggregate local (§333); if "
"+8/+16, suspect a reload spill slot rounded to 8B (§334) or extern-array dead "
"temps (§335); N arrays vs one struct shifts every displacement (§193-I).")
def rule_333_component(st, diffs, rules):
"""A CONSISTENT frame-size delta visible in the prologue/epilogue rows of an OTHERWISE noisy
residual (held-out audit, func_80020DA4: `addiu $sp,-8` vs `-0x18` + all four save/restore
offsets shifted +0x10 amid a mult-chain reorder). A component verdict, deliberately medium:
it explains PART of the residual, not all of it."""
if any(r["rule"] == "R333-FRAME-SIZE" for r in rules) or len(st.mine) != len(st.tgt):
return
deltas, rows = set(), []
for i in diffs:
if i >= len(st.mine) or i >= len(st.tgt):
continue
mw, tw = st.mine[i], st.tgt[i]
om, ot = _op(mw), _op(tw)
m_rs, m_rt, _rd, im = _fields(mw)
t_rs, t_rt, _trd, it = _fields(tw)
dv = (it - im) & 0xFFFF
dv = dv if dv < 0x8000 else dv - 0x10000
if om == ot == 9 and m_rs == t_rs == 29 and m_rt == t_rt == 29: # addiu $sp,$sp,K
deltas.add(-dv)
rows.append(i)
elif (om == ot and (32 <= om <= 46) and m_rs == t_rs == 29 and m_rt == t_rt):
deltas.add(dv) # $sp-relative mem
rows.append(i)
if len(rows) >= 3 and len(deltas) == 1:
dv = deltas.pop()
if dv:
_fire(rules, "R333-FRAME-COMPONENT", "medium", "§333/§334/§335",
"%d prologue/epilogue rows (%s) show ONE consistent frame delta %+#x amid other "
"residual noise" % (len(rows), rows[:6], dv),
"part of this residual is a frame-size delta: an unreferenced trailing aggregate "
"(§333), a reload spill slot rounded to 8B (§334), or extern-array dead temps "
"(§335) — fix the frame first, then re-measure the rest.")
def rule_small_flips(st, diffs, rules):
"""slti<->sltiu (§35) and beq<->bne (§3-T4) pure flips, decided on words."""
if not diffs:
return
sign, pol = [], []
for i in diffs:
if i >= len(st.mine) or i >= len(st.tgt):
return
mw, tw = st.mine[i], st.tgt[i]
om, ot = _op(mw), _op(tw)
if {om, ot} == {10, 11} and (mw & 0x03FFFFFF) == (tw & 0x03FFFFFF):
sign.append(i)
elif {om, ot} == {4, 5} and (mw & 0x03FFFFFF & st.masks[i]) == (tw & 0x03FFFFFF & st.masks[i]):
pol.append(i)
else:
return # not a PURE flip residual
if sign and not pol:
_fire(rules, "R35-SIGNEDNESS", "high", "§35/§21",
"diff rows %s are slti<->sltiu with identical regs+imm" % sign,
"keep a separate SIGNED int copy of the unsigned load (§35); write each bound as "
"its own if/goto so the range does not fold (§21).")
elif pol and not sign:
_fire(rules, "RPOL-BRANCH-POLARITY", "high", "§3-T4/§32",
"diff rows %s are beq<->bne with identical operands" % pol,
"invert the source condition / swap the arms (§3-T4); see §32.2 for operand order.")
def rule_jint(st, diffs, rules):
rows = [i for i in diffs if i < len(st.mine) and i < len(st.tgt)
and _op(st.mine[i]) == 2 and _op(st.tgt[i]) == 2]
if rows and len(rows) == len(diffs):
_fire(rules, "RJINT-WRONG-J-TARGET", "high", "§195-D/§162",
"every diff row (%s) is a `j` on both sides — same instruction, different label: "
"break-vs-return or a goto to the wrong arm" % rows,
"check which arm falls through vs jumps: the surviving cross-jump copy is the "
"LATER one; a backward `j` into a sibling arm is a source `goto` (§162, §195-D).")
def _shape_rules(st, verdict, diffs, draft, rules):
"""The word-level shape rules + klass-driven advisories, shared by the direct path and the
post-autodecl path."""
rule_368(st, diffs, rules)
rule_372(st, diffs, rules, klass=verdict.get("klass"))
rule_slot(st, verdict, diffs, rules)
try:
rule_366(open(os.path.join(REPO, draft)).read(), verdict, rules)
except OSError:
pass
rule_339(st, verdict, rules)
rule_333(st, diffs, rules)
rule_333_component(st, diffs, rules)
rule_small_flips(st, diffs, rules)
rule_jint(st, diffs, rules)
# klass-driven ADVISORIES (medium: the klass is residual_class's, the section mapping is the
# cookbook-index's symptom table — useful routing, but not this tool's own discovery)
if verdict.get("klass") == "BRANCH-POLARITY" and not any(
r["confidence"] in ("certain", "high") for r in rules):
_fire(rules, "RPOL-KLASS", "medium", "§3-T4/§32",
"residual_class: every opcode diff is a branch-family flip",
"invert the source condition / swap the arms (§3-T4, §32.2); trailing return-0 "
"note at cookbook L1344.")
if verdict.get("klass") == "DELAY-SLOT" and not any(
r["confidence"] in ("certain", "high") for r in rules):
_fire(rules, "RSLOT-KLASS", "medium", "§373/§370/§176-A/§5a",
"residual_class: nop-vs-insn dominates the diff rows",
"delay-slot fill difference: §176-A statement order first, then §373 dead-reset / "
"§370 eligibility (target-filled) or the §5a/§34 fence (mine-filled).")
def _autodecl_retry(binary, fn, draft, s_path, undecl, tag):
"""Compute the missing externs FROM THE TARGET .s BYTE EVIDENCE (decl_from_use — access
widths/signs read off real opcodes, nothing invented), patch the draft, re-measure. Up to two
rounds (a first patch can surface further undeclared symbols).
Returns (match_one_result, Streams, note) or (None, None, why_not_str). `note["defaulted"]`
marks decls that carried a sign/width DEFAULT — the caller caps rule confidence for those,
because a defaulted width can itself be the residual it then classifies."""
try:
import decl_from_use as dfu
except Exception as e:
return None, None, "decl_from_use unavailable: %r" % e
try:
text = open(os.path.join(REPO, draft)).read()
except OSError as e:
return None, None, "draft unreadable: %r" % e
decls, defaulted = {}, False
try:
ev, _seen_section = dfu.collect(s_path, binary)
except Exception as e:
return None, None, "collect(%s) failed: %r" % (os.path.basename(s_path), e)
todo = list(undecl)
for _round in (1, 2):
try:
masked = dfu.blank_decl_statements(text)
except Exception:
masked = text
for sym in todo:
if sym in decls:
continue
try:
forms = dfu.usage_forms(masked, sym)
r = dfu.infer(sym, ev, forms)
decls[sym] = r["decl"]
if any("default" in f for f in r.get("flags", [])):
defaulted = True
except dfu.Refuse as e:
return None, None, "infer(%s) refused: %s" % (sym, e)
except Exception as e:
return None, None, "infer(%s) failed: %r" % (sym, e)
try:
patched = dfu.insert_decls(text, decls)
except Exception as e:
return None, None, "insert_decls failed: %r" % e
pdir = os.path.join(WORK_ROOT, tag)
os.makedirs(pdir, exist_ok=True)
ppath = os.path.join(pdir, "autodecl.c")
open(ppath, "w").write(patched)
try:
res, st = run_match_one(fn, os.path.relpath(ppath, REPO),
os.path.relpath(os.path.dirname(s_path), REPO),
tag + ".ad%d" % _round)
except Exception as e:
return None, None, "re-measure failed: %r" % e
if res.get("status") in ("match", "near", "fail"):
return res, st, {"decls": decls, "defaulted": defaulted, "rounds": _round}
more = sorted(set(re.findall(r"`(\w+)' undeclared", res.get("error", ""))) - set(decls))
if not more:
return None, None, ("patched compile still fails (%s): %s"
% (res.get("status"), res.get("error", "")[:120]))
todo = more
return None, None, "undeclared symbols kept surfacing after 2 rounds"
# ---------------------------------------------------------------------------------------------
# per-case driver
# ---------------------------------------------------------------------------------------------
def classify(binary, fn, draft, tag=None):
"""One case -> a verdict dict with the fired rules, best-first."""
tag = tag or ("%s.%s" % (binary, fn))
out = dict(binary=binary, fn=fn, draft=draft, rules=[], status=None, klass=None)
rules = out["rules"]
try:
asm_dir, s_path = resolve_stub(binary, fn)
except Banked as e:
out["status"] = "banked"
_fire(rules, "ALREADY-BANKED", "certain", "corpus.matched",
str(e), "the function already byte-matches in the tree — this draft is obsolete; "
"spend zero tokens (a stale card, not a matching problem).")
return _finish(out)
except Exception as e:
out["status"] = "error"
out["error"] = "resolve: %s" % e
return out
out["asm_dir"] = asm_dir
# W332 — target-side, draft-independent, checked FIRST
hits = wall_sweep.scan(s_path)
if hits:
_fire(rules, "W332-MASPSX-WALL", "certain", "§332",
"%d delay slot(s) in the TARGET hold a %%lo macro tail (first: line %d, `%s` / "
"slot `%s`) — unreachable from C under the pinned triple, whatever the draft says"
% (len(hits), hits[0][0], hits[0][1][-40:], hits[0][2][-40:]),
"toolchain wall: ledger it, never draft, never escalate (§332/§332a); needs the "
"narrow maspsx macro-hop patch, not C work.")
if not os.path.exists(os.path.join(REPO, draft)):
out["status"] = "error"
out["error"] = "draft file missing: %s" % draft
return out
try:
res, st = run_match_one(fn, draft, asm_dir, tag)
except Exception as e:
out["status"] = "error"
out["error"] = "match_one: %s" % e
return out
out["status"] = res.get("status")
out["closeness"] = res.get("closeness")
verdict = res.get("verdict") or {}
out["klass"] = verdict.get("klass")
if out["status"] not in ("match", "near", "fail"): # a toolchain-stage failure
err = res.get("error", "")
if re.search(r"conflicting types|redeclar|redefin", err):
_fire(rules, "NOCOMPILE-DECL-CONFLICT", "high", "§337/§367/§37",
"cc1 rejects a declaration conflict: %s" % err.strip().splitlines()[-1][:160],
"adopt the TU's banked spelling and adapt the use site (§367), or the §37/§183 "
"asm-label alias; the static blocker oracle is blind here (§337) — finish on "
"the real whole-TU compile.")
return _finish(out)
undecl = sorted(set(re.findall(r"`(\w+)' undeclared", err)))
if undecl:
res2, st2, note = _autodecl_retry(binary, fn, draft, s_path, undecl, tag)
if res2 is not None:
out["autodecl"] = note
if res2.get("status") == "match":
out["status"] = "match"
out["closeness"] = 0
_fire(rules, "NOCOMPILE-UNDECLARED-FIXED", "certain", "decl_from_use/§58",
"the ONLY defect was %d missing extern(s) (%s); with decls COMPUTED "
"from the target .s access widths the body byte-matches standalone"
% (len(undecl), ", ".join(note["decls"].values())),
"GATE-FIRST, NOT A BANK (§376, measured 0/28 raw in P31 S69). Do NOT "
"gate the autodecl arm: the extern that satisfies the STANDALONE probe is "
"a SECOND conflicting declaration in the real TU. Gate the RAW draft after "
"the §378 chain, in this order: fix_arity_callers --any-proto --binary "
"<B> --funcs <FN> -> cast_self_callers --binary <B> --funcs <FN> "
"--drafts <D> [--sync-decls] -> gate. Each step only reveals the next.")
return _finish(out)
if res2.get("status") in ("near", "fail") and st2 is not None:
out["status"] = res2["status"]
out["closeness"] = res2.get("closeness")
verdict = res2.get("verdict") or {}
out["klass"] = verdict.get("klass")
_fire(rules, "NOCOMPILE-UNDECLARED-MEASURED", "medium", "decl_from_use",
"%d missing extern(s) auto-computed from the .s (%s); the patched "
"draft measures closeness %s %s"
% (len(undecl), ", ".join(note["decls"].values()),
res2.get("closeness"), verdict.get("klass")),
"add the computed extern(s); then treat the residual below.")
res, st = res2, st2
diffs = st.diffs()
out["ndiffs"] = len(diffs)
_shape_rules(st, verdict, diffs, draft, rules)
if note.get("defaulted"):
for r in rules: # a defaulted sign/width can BE the residual
if r["confidence"] in ("certain", "high") and r["rule"] not in (
"W332-MASPSX-WALL",):
r["confidence"] = "medium"
r["capped"] = "autodecl sign/width default may cause this residual"
return _finish(out)
_fire(rules, "NOCOMPILE-UNDECLARED", "high", "§337/decl_from_use",
"undeclared symbol(s) %s%s" % (undecl, "; " + note if isinstance(note, str) else ""),
"supply the externs (tools/decl_from_use.py computes them from the target .s "
"access widths) or gate in-TU where the TU already declares them.")
return _finish(out)
_fire(rules, "NOCOMPILE", "medium", "§337",
"%s: %s" % (out["status"], err.strip().splitlines()[-1][:160] if err else "?"),
"fix the compile blocker first; the residual cannot be measured until it builds.")
return _finish(out)
if out["status"] == "match":
_fire(rules, "INTEG-STANDALONE-MATCH", "certain", "§58/§59/§367",
"closeness 0: the draft is byte-correct standalone",
"matching is done; BANKING IS NOT. A standalone closeness of 0 proves the BODY and "
"says nothing about the TU accepting the SIGNATURE (§376 — this class gated 0/28 raw "
"in P31 S69, then 8 banked once the chain existed). Route: fix_arity_callers "
"--any-proto -> cast_self_callers [--sync-decls] (§378/§378a) -> gate; or "
"recover_integration --stages arity,self-cast. If the diagnostic names a DIFFERENT "
"symbol, run the same chain on THAT symbol.")
return _finish(out)
if st is None:
out["error"] = "no streams emitted"
return _finish(out)
diffs = st.diffs()
out["ndiffs"] = len(diffs)
# W188 — the as -O2 bypass oracle (skip for -O0 targets: no scheduling there)
o0 = False
try:
from o0_detect import detect_o0
o0 = detect_o0(s_path)
except Exception:
pass
if not o0 and not any(r["rule"] == "W332-MASPSX-WALL" for r in rules):
bd = bypass_as_o2(fn, draft, s_path, tag)
out["bypass_diffs"] = bd
if bd == 0:
_fire(rules, "W188-EPILOGUE-WALL", "certain", "§188",
"the SAME C, maspsx bypassed + `as -O2`, gives 0 diffs — the draft is "
"byte-correct and the pinned `as -O1` can never emit it",
"file IMMOVABLE (§188): no C-level work can close it; candidate for "
"psyq_integrate if it is an SDK object (§332a).")
# the shape rules, word-level
_shape_rules(st, verdict, diffs, draft, rules)
# pass-throughs (existing knowledge, deliberately capped at medium)
if verdict.get("klass") == "SIZE-MISMATCH" and not rules:
_fire(rules, "REDRAFT-SIZE-MISMATCH", "medium", "residual_class",
"the draft is a different-sized function (ratio %s)"
% verdict.get("detail", {}).get("ratio"),
"re-draft from the target .s; never seed-tweak this (rebadged residual_class verdict).",
rebadged=True)
if verdict.get("klass") == "REGALLOC-PERM" and not any(
r["confidence"] in ("certain", "high") for r in rules):
_fire(rules, "R31-REGALLOC-PERM", "medium", "§31/§17/§76",
"one injective register map explains every diff: %s"
% verdict.get("detail", {}).get("cycles"),
"§31 decl/statement-order levers; §17 register pins; §76 condition-local reuse; "
"permuter `regalloc` profile.")
return _finish(out)
_CONF_ORD = {"certain": 0, "high": 1, "medium": 2, "low": 3}
def _finish(out):
out["rules"].sort(key=lambda r: _CONF_ORD.get(r["confidence"], 9))
out["top"] = out["rules"][0]["rule"] if out["rules"] else None
out["top_confidence"] = out["rules"][0]["confidence"] if out["rules"] else None
return out
# ---------------------------------------------------------------------------------------------
# eval sweep + selftest
# ---------------------------------------------------------------------------------------------
def _one(e, idx):
try:
return classify(e["binary"], e["fn"], e["draft"], tag="%03d.%s.%s" % (idx, e["binary"], e["fn"]))
except Exception as ex: # loud in the data, never silent (R32)
return dict(binary=e["binary"], fn=e["fn"], draft=e["draft"],
status="error", error="classify crashed: %r" % ex, rules=[],
top=None, top_confidence=None)
def run_eval(path, jobs):
import concurrent.futures as cf
cases = json.load(open(path))
print("eval set: %d cases (the denominator)" % len(cases))
results = [None] * len(cases)
with cf.ThreadPoolExecutor(max_workers=jobs) as ex:
futs = {ex.submit(_one, e, i): i for i, e in enumerate(cases)}
done = 0
for f in cf.as_completed(futs):
i = futs[f]
results[i] = f.result()
done += 1
r = results[i]
print(" [%3d/%3d] %-14s %-16s %-7s top=%s (%s)"
% (done, len(cases), r["binary"], r["fn"], r.get("status"),
r.get("top"), r.get("top_confidence")), flush=True)
os.makedirs(WORK_ROOT, exist_ok=True)
with open(os.path.join(WORK_ROOT, "eval_results.jsonl"), "w") as f:
for r in results:
f.write(json.dumps(r) + "\n")
summarize(results)
return results
def summarize(results):
n = len(results)
errored = [r for r in results if r.get("status") == "error"]
banked = [r for r in results if r.get("status") == "banked"]
classified = [r for r in results if r.get("status") != "error"]
nocompile = [r for r in classified
if r.get("status") not in ("match", "near", "fail", "banked")]
fired = [r for r in classified if r.get("top")]
certhigh = [r for r in fired if r.get("top_confidence") in ("certain", "high")]
ch_resid = [r for r in certhigh
if r["top"] not in ("ALREADY-BANKED",) and not r["top"].startswith("NOCOMPILE")
and not r["rules"][0].get("rebadged")]
print("\n================ SCORE (denominator: %d cases) ================" % n)
print("processed without error : %d (%.0f%%)" % (len(classified), 100.0 * len(classified) / n))
print("ERRORED : %d" % len(errored))
for r in errored:
print(" %s/%s: %s" % (r["binary"], r["fn"], r.get("error", "?")[:120]))
print("ALREADY-BANKED (stale entries — the rival's 28-error class): %d" % len(banked))
print("any rule fired : %d (%.0f%%)" % (len(fired), 100.0 * len(fired) / n))
print("certain/high (ALL) : %d (%.0f%%)" % (len(certhigh), 100.0 * len(certhigh) / n))
print("certain/high RESIDUAL-SHAPE only (excl. ALREADY-BANKED + NOCOMPILE + rebadged): "
"%d (%.0f%%)" % (len(ch_resid), 100.0 * len(ch_resid) / n))
print(" (compile-fail drafts in the set: %d)" % len(nocompile))
tab = collections.Counter()
for r in classified:
for ru in r["rules"]:
tab[(ru["rule"], ru["confidence"])] += 1
print("\nper-rule firing table (every fired rule, not only top):")
for (rule, conf), c in sorted(tab.items(), key=lambda kv: (-kv[1], kv[0])):
print(" %-26s %-8s %3d" % (rule, conf, c))
klasses = collections.Counter(r.get("klass") for r in classified)
print("\nresidual_class klass distribution: %s" % dict(klasses))
def selftest():
"""The two development cases with KNOWN, PROVEN causes. A detector that cannot reproduce a
case whose answer is known is not evidence — this must pass before any sweep is trusted."""
ok = True
r1 = classify("ov_SC03_105", "func_80187A30", ".run/S68o1/opus/func_80187A30.c", tag="self.368")
top1 = [ru for ru in r1["rules"] if ru["rule"] == "R368-RELOAD-REMAT"]
print("dev case §368 ov_SC03_105/func_80187A30: top=%s conf=%s"
% (r1.get("top"), r1.get("top_confidence")))
if not top1 or top1[0]["confidence"] not in ("certain", "high") or r1["top"] != "R368-RELOAD-REMAT":
print(" FAIL: expected R368-RELOAD-REMAT as top rule; got %s" % r1["rules"])
ok = False
else:
print(" OK evidence: %s" % top1[0]["evidence"][:140])
r2 = classify("main", "func_8005D734", ".run/S68m1/sonnet/func_8005D734.c", tag="self.332")
print("dev case §332 main/func_8005D734: top=%s conf=%s"
% (r2.get("top"), r2.get("top_confidence")))
if r2.get("top") != "W332-MASPSX-WALL" or r2.get("top_confidence") != "certain":
print(" FAIL: expected W332-MASPSX-WALL certain; got %s" % r2["rules"])
ok = False
else:
print(" OK evidence: %s" % r2["rules"][0]["evidence"][:140])
print("SELFTEST %s" % ("PASS" if ok else "FAIL"))
return 0 if ok else 1
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--binary")
ap.add_argument("--fn")
ap.add_argument("--draft")
ap.add_argument("--eval", dest="eval_path")
ap.add_argument("--jobs", type=int, default=8)
ap.add_argument("--selftest", action="store_true")
a = ap.parse_args()
os.makedirs(WORK_ROOT, exist_ok=True)
if a.selftest:
return selftest()
if a.eval_path:
run_eval(a.eval_path, a.jobs)
return 0
if not (a.binary and a.fn and a.draft):
ap.error("need --binary --fn --draft (or --eval / --selftest)")
r = classify(a.binary, a.fn, a.draft)
print(json.dumps(r, indent=1))
return 0
if __name__ == "__main__":
sys.exit(main())