Files
BFM-decomp/tools/delever_search.py

1077 lines
62 KiB
Python
Raw Permalink 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
"""delever_search.py — rung G of the lever ladder: the GUIDED search for a lever-free body (Phase 36, S101).
tools/delever_search.py --plan [--limit N] [--only X ...] [--score] # the residue's exemplars; --score = their starting distance
tools/delever_search.py --run [--limit K] [--only X ...] [-j W] [--beam B] [--depth D] [--cap C] [--budget M] [--label gN]
tools/delever_search.py --positive-control TU FN [--moves 1|2] # perturb a MATCHING body, require the search to return to 0
tools/delever_search.py --try TU FN FILE [--body] # score a candidate WITHOUT writing the tree (an agent's loop)
tools/delever_search.py --status
tools/delever_search.py --selftest
WHY. Rung R (`delever --recipes`) is BLIND: it generates ~57 one-move candidates per body, asks the oracle "IDENTICAL?" and throws
everything else away — 134 of 134 on a class whose shape rung D had just found, 0 of 300 where no shape was known (S100, cookbook
§454a): a replication engine, not a discovery engine. Rung D (decomp-permuter) discovers, but as a random walk that reprints the
source. This engine sits between them: the SAME candidate generators, the SAME oracle, but every candidate is SCORED by its masked
mismatch count against the target, the residual is CLASSIFIED to choose the move families, and the search hill-climbs — two- and
three-move compositions guided by the gradient — instead of guessing once.
THE SCORER IS THE ORACLE'S OWN OBJECT. A candidate TU text is compiled through the recipe of ONE object (a header: its first
includer's), the function's instructions are read out of that scratch object (`objdump -drz`, 35 ms on the largest overlay object)
and compared, relocation-masked with reloc-operand equality (`masked_diff.diff_object_object`), with the same function in the fleet
run's baseline object under `build/`. That target is the tree's own bytes — proven by the SHA1 check, carrying the candidates'
relocations by construction — so no listing is assembled, no TU isolated, no cpp expanded (the two instrument classes that cost rung D
two campaigns, §454). Whole-object equality on EVERY recipe of the TU stays the bank verdict (`delever --apply-body`), and the clean
fleet run (R22) gates the batch as it gates every batch of this phase.
THE SEARCH. Beam `B` (3), depth `D` (3), per-node cap `C` (48), budget `M` compiles (400). Node = a TU text with its score and the
move path that made it. Children are generated by the families the residual's class selects (REG on the caller-saved bank → the
commutative swap, the temp inlined/introduced, the initializer split; REG on the callee-saved bank → declaration order/move first;
COUNT → the temp moves; ORDER → the block wrap and the adjacent swap), ranked by score; a child worse than its parent is dropped, a
child with the parent's score and the parent's diff signature (a no-effect move) goes to the tail. The first score-0 child is
verified on every recipe and banked; siblings of its text class are then given the same shape by `delever --propagate`, serially,
after the parallel phase (a sibling lives in a TU another worker may own).
THE CONTROLS (R39/R40/R56), before a yield is believed: every body starts by scoring the tree's OWN text — it must score 0 and be
byte-identical, or the harness is not measuring this function (UNCALIBRATED, recorded); `--positive-control` perturbs a lever-free
MATCHING body by one or two generator moves, requires the perturbed text to score > 0, and requires the search to find its way back to
0 without ever writing the tree; `--selftest` runs the classifier on synthetic instruction streams and the beam on a stub scorer whose
answer needs three composed moves. The evidence of every attempt is `.run/P36/engine/outcomes.jsonl` and a per-body trace under
`.run/P36/engine/trace/` (every scored candidate: the move, the parent's score, its score, its class) — the byte record lane B's
compiler-source hypotheses are checked against.
NEVER RUN A LONG CAMPAIGN AS A HARNESS BACKGROUND TASK (the low-memory guard kills it): `setsid nohup nice -n 10 … &` + a Monitor.
A worker owns a whole translation unit (the oracle writes candidates into the real source path); headers run serially after the TUs.
`pkill -f` never with a literal your own command line contains (R79).
"""
import argparse
import collections
import difflib
import json
import os
import pathlib
import random
import re
import subprocess
import sys
import threading
import time
from concurrent.futures import ThreadPoolExecutor
REPO = pathlib.Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "tools"))
import delever as dl # noqa: E402
import delever_oracle as oracle # noqa: E402
import lever_census as lc # noqa: E402
import masked_diff as md # noqa: E402
import share_census as sc # noqa: E402
RUN = REPO / ".run" / "P36" / "engine"
OUTCOMES = RUN / "outcomes.jsonl"
TRACE = RUN / "trace"
OBJ = RUN / "obj"
PY = str(REPO / ".venv" / "bin" / "python")
_LOCK = threading.Lock()
REGNAME = {0: "zero", 1: "at", 2: "v0", 3: "v1", 4: "a0", 5: "a1", 6: "a2", 7: "a3", 8: "t0", 9: "t1", 10: "t2", 11: "t3",
12: "t4", 13: "t5", 14: "t6", 15: "t7", 16: "s0", 17: "s1", 18: "s2", 19: "s3", 20: "s4", 21: "s5", 22: "s6",
23: "s7", 24: "t8", 25: "t9", 26: "k0", 27: "k1", 28: "gp", 29: "sp", 30: "fp", 31: "ra"}
CALLEE = set(range(16, 24)) | {30}
CALLER = set(range(2, 16)) | {24, 25}
# the move families per residual class, in trial order (the first is the one the class's byte-proven exemplar closed on).
# R19 leads EVERY class since S102: it is the only family that can change a call's arity, it emits a handful of
# candidates and only for calls whose declaration provably disagrees with the callee's definition, and six independent
# agent cracks say it is the largest single class in the residue. It costs nothing when it does not apply.
FAMILIES = {
# lane B's map (`.run/P36/engine/residual_moves.md`, S101, gcc 2.7.2 source): the caller-saved swap is decided in
# local-alloc's block_alloc/combine_regs by which dying pseudo the operand ties to — the temp inlined/introduced first; a
# constant-operand commutative swap is undone by fold (fold-const.c) and only a var/var swap can reach the allocator.
# R15 (the sink) is the arm-scoped form of the same tie: a value set in every arm of an if/else chain is a CROSS-BLOCK
# pseudo local-alloc never gives a quantity, so the arm holds two quantities and takes block_alloc's unrolled case 2;
# sinking makes it three, and case 3 falls through into case 2 and undoes its own exchange (T7 agent a1, func_80156044).
"REG-caller": ("R19", "R25", "R26", "R23", "R24", "R21", "R6", "R20", "R16", "R8", "R15", "R17", "R5", "R18", "R10", "R12", "R14", "R13", "R3", "R7", "R9", "R2", "R4", "R22", "R44", "R45", "R46"),
# the s-bank order is global.c's allocno_compare (ref weight x live length), declaration order only on an exact tie
"REG-callee": ("R19", "R25", "R26", "R23", "R24", "R21", "R20", "R2", "R4", "R3", "R6", "R16", "R8", "R15", "R18", "R12", "R7", "R9", "R17", "R10", "R14", "R13", "R5", "R22", "R44", "R45", "R46"),
"REG-mixed": ("R19", "R25", "R26", "R23", "R24", "R21", "R6", "R20", "R16", "R2", "R15", "R17", "R18", "R5", "R10", "R4", "R3", "R8", "R12", "R13", "R14", "R7", "R9", "R22", "R44", "R45", "R46"),
# a copy dies to cse's canon_reg or the local-alloc tie unless its destination changes MODE (the width); an address
# pseudo lives when a pointer local is used twice; a value named once is computed once; a short PARAMETER is extended in place
"COUNT": ("R19", "R25", "R26", "R22", "R44", "R45", "R21", "R20", "R12", "R16", "R14", "R15", "R6", "R8", "R3", "R18", "R7", "R17", "R5", "R13", "R9", "R10", "R2", "R4", "R23", "R24", "R46"),
# statement order IS the schedule among equal-priority insns (rank_for_schedule's LUID tie-break); do-while is a barrier
# R17 is the DIRECTED form of the run-split R9 reaches only by luck: agent a2 measured 2,271 compiles for R9 to find it
# in func_80168828 and R16+R17 reproduce the same close in ten.
"ORDER": ("R19", "R25", "R46", "R17", "R18", "R20", "R21", "R9", "R7", "R13", "R3", "R16", "R6", "R8", "R5", "R12", "R14", "R10", "R15", "R2", "R4", "R22", "R23", "R24", "R26", "R44", "R45"),
"MIXED": dl.ALL_FAMILIES,
"OTHER": dl.ALL_FAMILIES,
}
class Unstrippable(Exception):
pass
# ----------------------------------------------------------------------------------------------------------------------
# the residual's class, from two instruction streams
# ----------------------------------------------------------------------------------------------------------------------
def shape(i):
"""the instruction with its register fields zeroed (a reloc-masked immediate too) — two words of one shape differ only
in which registers they name."""
w, kind = i["word"], i["reloc_kind"]
op = w >> 26
if op == 0:
return ("R", w & 63, (w >> 6) & 31)
if op in (2, 3):
return ("J", op, i.get("reloc_op") or "")
if op == 18: # cop2: rs = the sub-op, rt = the CPU register, the rest cop fields
return ("C", w & ~(31 << 16))
return ("I", op, (w & 0xFFFF) if kind is None else ("rel", i.get("reloc_op") or ""))
def regs_of(i):
w = i["word"]
op = w >> 26
if op == 0:
return ((w >> 21) & 31, (w >> 16) & 31, (w >> 11) & 31)
if op in (2, 3):
return ()
if op == 18:
return ((w >> 16) & 31,)
return ((w >> 21) & 31, (w >> 16) & 31)
def masked_word(i):
"""the comparable form of one instruction: the reloc-masked word, the reloc operand (symbol+addend) and an internal `j`'s
target — equality of two streams of these ⟺ masked_diff.diff_object_object == 0 (the same rule, R33)."""
return (i["word"] & md.mask_for(i["word"], i["reloc_kind"]), i.get("reloc_op") or "", i.get("jrel"))
def classify(mine, tgt):
"""dict(kind, bank, score, n_mine, n_tgt, positions, pairs) — kind in REG | COUNT | ORDER | MIXED | OTHER | IDENTICAL;
bank (REG only) in caller | callee | mixed, from the register pairs that differ.
THE SCORE IS AN EDIT DISTANCE, NOT A POSITIONAL COUNT. The positional mismatch count (`match_one`'s closeness) reads a
one-instruction shift as "everything after it" — the two-move positive control at S101 read 43 for one inlined temp — so a
hill-climb on it cannot see that a candidate moved the residual from 43 shifted words to 2 real ones. difflib's opcodes over
the masked words give the real delta (insertions, deletions, replaced blocks); a replace block whose words keep their shape
(opcode, immediate, function code) and change only their register fields is the REG class."""
a, b = [masked_word(i) for i in mine], [masked_word(i) for i in tgt]
if a == b:
return dict(kind="IDENTICAL", bank=None, score=0, n_mine=len(mine), n_tgt=len(tgt), positions=[], pairs=[])
ops = [op for op in difflib.SequenceMatcher(None, a, b, autojunk=False).get_opcodes() if op[0] != "equal"]
score = sum(max(i2 - i1, j2 - j1) for _, i1, i2, j1, j2 in ops)
pos = [i for _, i1, i2, _, _ in ops for i in range(i1, max(i2, i1 + 1))]
pairs = collections.Counter()
count = any(t in ("insert", "delete") or (i2 - i1) != (j2 - j1) for t, i1, i2, j1, j2 in ops)
n_rep = n_reg = 0
for t, i1, i2, j1, j2 in ops:
if t != "replace" or (i2 - i1) != (j2 - j1):
continue
for k in range(i2 - i1):
n_rep += 1
if shape(mine[i1 + k]) == shape(tgt[j1 + k]):
n_reg += 1
for x, y in zip(regs_of(mine[i1 + k]), regs_of(tgt[j1 + k])):
if x != y:
pairs[(x, y)] += 1
if len(a) == len(b) and sorted(a) == sorted(b):
kind = "ORDER" # a pure reorder reads as delete+insert to difflib: test it first
elif count:
kind = "COUNT"
elif n_rep and n_reg == n_rep:
kind = "REG"
else:
kind = "MIXED" if n_reg else "OTHER"
regs = {x for x, _ in pairs} | {y for _, y in pairs}
bank = None
if kind == "REG":
callee, caller = bool(regs & CALLEE), bool(regs & CALLER)
bank = "mixed" if callee and caller else "callee" if callee else "caller"
return dict(kind=kind, bank=bank, score=score, n_mine=len(mine), n_tgt=len(tgt), positions=pos[:24],
pairs=[(REGNAME.get(x, x), REGNAME.get(y, y), n) for (x, y), n in pairs.most_common(8)])
def family_key(cls):
if cls["kind"] == "REG":
return f"REG-{cls['bank']}"
return cls["kind"] if cls["kind"] in FAMILIES else "OTHER"
def signature(cls):
"""what the diff looks like, independent of the text that produced it — two candidates with one signature moved nothing
relative to each other."""
return (cls["kind"], cls["n_mine"], tuple(cls["positions"]), tuple(cls["pairs"]))
# ----------------------------------------------------------------------------------------------------------------------
# the scorer: one compile through the oracle's recipe, the function's instructions against the baseline object's
# ----------------------------------------------------------------------------------------------------------------------
class Scorer:
def __init__(self, tu, fn, recs, tag, inflight):
if not recs:
raise Unstrippable([("<recipe>", 0, f"no recipe compiles {tu}")])
self.tu, self.fn, self.recs, self.tag = tu, fn, recs, tag
self.primary = recs[0]
self.header = tu.endswith(".h")
self.path = REPO / tu
self.inflight = inflight
base = oracle.baseline_path(self.primary["obj"]) # the snapshot when one exists: `make clean` must not wipe a live score
if not base.exists():
raise Unstrippable([("<baseline>", 0, f"{self.primary['obj']} missing — run the fleet build first (R56)")])
self.base_bytes = base.read_bytes()
dump = subprocess.run([md.OBJDUMP, "-drz", "-j", ".text", str(base)], capture_output=True, text=True).stdout
if f"<{fn}>:" not in dump:
raise Unstrippable([("<baseline>", 0, f"{fn} is not a label of {self.primary['obj']}")])
self.tgt = md.insns_from_object(str(base), fn)
self.compiles = 0
self.seconds = 0.0
OBJ.mkdir(parents=True, exist_ok=True)
# PER TU, not per tag (S101 run g4s): the tag was the TU's index modulo the worker count, not a worker id, so two
# TUs could share `g0.o` at the same moment and one worker objdumped the other's object — a byte-identical body read
# as 14,871 mismatches (UNCALIBRATED), and any candidate scored in that window was noise. A worker owns a whole TU,
# so a name keyed by the TU cannot collide. The bank was never at risk: it re-verifies whole-object equality itself.
self.scratch = OBJ / f"{tag}__{tu.replace('/', '_')}.o"
def score(self, text):
"""dict(score, ident, cls, seconds, err) — score None on a compile failure (R61: not judged is not a distance)."""
raw, st = self.path.read_text(errors="surrogateescape"), self.path.stat()
with _LOCK:
self.inflight[self.tu] = raw
dl.INFLIGHT.write_text(json.dumps(self.inflight))
try:
data, dt, err = oracle.compile_obj(self.primary, text, tag=self.tag, write_path=(self.tu if self.header else None))
finally:
dl.restore_file(self.path, raw, st)
with _LOCK:
self.inflight.pop(self.tu, None)
if self.inflight:
dl.INFLIGHT.write_text(json.dumps(self.inflight))
else:
dl.INFLIGHT.unlink(missing_ok=True)
self.compiles += 1
self.seconds += dt
if data is None:
return dict(score=None, ident=False, cls=None, seconds=dt,
err=("COMPILE-CRASH" if err.startswith("CRASH:") else "COMPILE-ERROR") + ": " + err[:120])
p = self.scratch
p.write_bytes(data)
mine = md.insns_from_object(str(p), self.fn)
cls = classify(mine, self.tgt)
return dict(score=cls["score"], ident=(data == self.base_bytes), cls=cls, seconds=dt, err="")
def verify_all(self, text):
"""the bank verdict: IDENTICAL on every recipe of the file (a twin's object; every includer of a header)."""
raw, st = self.path.read_text(errors="surrogateescape"), self.path.stat()
try:
return oracle.judge_all(self.recs, text, tag=self.tag + "v", write_path=(self.tu if self.header else None))
finally:
dl.restore_file(self.path, raw, st)
# ----------------------------------------------------------------------------------------------------------------------
# the population and the seed
# ----------------------------------------------------------------------------------------------------------------------
_sites = None
def sites_by_body():
global _sites
with _LOCK:
if _sites is None:
built = collections.defaultdict(list)
for s in dl.load_sites():
if s.get("fn"):
built[(s["tu"], s["fn"])].append(s)
_sites = built
return _sites
AB_KINDS = {"pin", "barrier", "launder", "keepalive", "instruction", "gte-lever", "asm-body"}
def lever_free_body(tu, raw, fn, sites):
"""the TU with every class A/B site of THIS body rewritten away (rung A's edits, nothing else — the file-scope asm and every
other body untouched, since the scorer compares whole objects too). A class C/D site the tree still carries (a byte-needed
`volatile` cast or bare `register`) STAYS: decision 3 keeps it as ordinary C, unmarked and ledgered, so the search starts
from the spelling the original plausibly had rather than paying to remove what the phase does not ask removed (S101: the
engine had been stripping a needed `volatile` cast and searching for a reload the cast already produced)."""
m, ls = dl.same_len_mask(raw), dl.line_starts(raw)
edits = []
for s in sites:
if s["cls"] not in "AB":
continue
if (s["cls"], s["kind"]) not in dl.REMOVABLE:
if s["kind"] in dl.DEFERRED_KINDS:
raise Unstrippable([(s["kind"], s["line"], "asm-body: T7's work")])
continue
try:
edits += dl.site_edits(raw, m, ls, s)
except dl.Refuse as ex:
raise Unstrippable([(s["kind"], s["line"], str(ex)[:120])])
if not edits:
return raw
try:
out = dl.apply_edits(raw, edits)
except dl.Refuse as ex:
raise Unstrippable([("<combination>", 0, str(ex)[:120])])
# R43 (S104 e24, func_80180E24): a deleted lever line that OPENED a multi-line comment left the comment's tail as code — the
# start text did not compile, and every sweep read the class as UNSCORED, never as refused. Refuse instead.
if out.count("/*") - out.count("*/") != raw.count("/*") - raw.count("*/"):
raise Unstrippable([("<comment>", 0, "a stripped lever line opened or closed a block comment")])
return out
def load_outcomes():
rows = []
if OUTCOMES.exists():
for l in OUTCOMES.read_text().splitlines():
if l.strip():
rows.append(json.loads(l))
return rows
def outcome_append(row):
RUN.mkdir(parents=True, exist_ok=True)
with _LOCK, open(OUTCOMES, "a") as f:
f.write(json.dumps(row) + "\n")
def matches(ex, only):
if not only:
return True
return any(o == ex["fn"] or o == ex["tu"] or o in ex["tu"] or (ex["nhash"] or "").startswith(o) or o == ex["alias"] for o in only)
def exemplars(only=(), limit=None, include_done=False):
"""one exemplar per RESIDUE text class of the delever ledger, largest class first, then fewest NEEDED sites."""
cur, order = {}, []
for r in dl.load_ledger():
k = (r.get("tu"), r.get("fn"))
if not k[0] or not k[1]:
continue
if k not in cur:
order.append(k)
cur[k] = r
groups = collections.OrderedDict()
nohash = 0
for k in order:
r = cur[k]
if r.get("verdict") != "RESIDUE" or r["fn"] == dl.FILE_SCOPE_FN:
continue
# THE PHASE'S NUMBER is pins + asm statements: a body whose only NEEDED sites are class C/D (a byte-needed `volatile`, a bare
# `register`) is done (decision 3) and is not drawn — the T4 close counted 498 such bodies, and g3 spent searches on them
if not any(s.get("verdict") == "NEEDED" and s.get("kind") in AB_KINDS for s in r.get("sites", [])):
continue
nh = r.get("nhash_after") or r.get("nhash_before")
if not nh:
nohash += 1
continue
groups.setdefault(nh, []).append((k, r))
if nohash:
sys.exit(f"delever_search: {nohash} RESIDUE row(s) carry no text hash — run `tools/delever.py --repair-nhash` first (R43)")
done = set() if include_done else {o["nhash"] for o in load_outcomes() if o.get("kind") == "attempt"}
out = []
for nh, members in groups.items():
if nh in done:
continue
(tu, fn), r = members[0]
needed = [s for s in r.get("sites", []) if s.get("verdict") == "NEEDED"]
out.append(dict(nhash=nh, tu=tu, fn=fn, alias=(r.get("aliases") or [None])[0], copies=len(members), needed=len(needed),
kinds=sorted({s["kind"] for s in needed}), regs=sorted({s.get("detail", "") for s in needed if s["kind"] == "pin"}),
members=[dict(tu=t, fn=f) for (t, f), _ in members], row=r))
out.sort(key=lambda e: (-e["copies"], e["needed"], e["tu"], e["fn"]))
out = [e for e in out if matches(e, only)]
return out[:limit] if limit else out
def recs_for(tu, by_src, inc):
return [r for t_ in inc.get(tu, []) for r in by_src.get(t_, [])] if tu.endswith(".h") else by_src.get(tu, [])
# ----------------------------------------------------------------------------------------------------------------------
# the search
# ----------------------------------------------------------------------------------------------------------------------
def body_span(text, tu, fn):
d_ = next((r for r in sc.scan_text(text, tu, shared_defs=None) if r["form"] == "def" and r["name"] == fn), None)
return d_
def rank(cands, fam_order, cls, d_):
"""ROUND-ROBIN across the families in the class's order, locality within a family. A strict family-first order starved
every later family under the cap: the S101 one-move control was perturbed by one R9 swap and its inverse sat behind 64
block-wrap candidates, so depth 1 never saw it and the search paid 142 compiles for a two-move detour. Within a family
the candidates nearest the residual go first (the changed block's index mapped onto the body's lines by fraction — a
tie-break, never the decider)."""
est = []
if cls and cls.get("positions") and d_ and cls["n_mine"]:
span = d_["end"] - d_["line"]
est = [d_["line"] + p / cls["n_mine"] * span for p in cls["positions"]]
def dist(item):
m = re.search(r"@(\d+)$", item[1])
return min((abs(int(m.group(1)) - e) for e in est), default=0) if m else 0
groups = collections.OrderedDict((f, []) for f in fam_order)
for c in cands:
groups.setdefault(c[0], []).append(c)
for g in groups.values():
g.sort(key=dist)
out = []
while any(groups.values()):
for g in groups.values():
if g:
out.append(g.pop(0))
return out
def search(seed, scorer_fn, gen_fn, beam=3, depth=3, cap=48, budget=400, trace=None, log=None):
"""the beam. scorer_fn(text) -> dict(score, ident, cls, err); gen_fn(text, cls) -> [(rec, desc, text)] ranked.
Returns dict(verdict, best, start, compiles, path, text, node_cls) — verdict MATCH | NO-MATCH | BUDGET | UNSCORED."""
s0 = scorer_fn(seed)
compiles = 1
if s0["score"] is None:
return dict(verdict="UNSCORED", best=None, start=None, compiles=compiles, path=[], text=seed, err=s0.get("err", ""))
if s0["score"] == 0 and s0["ident"]:
return dict(verdict="MATCH", best=0, start=0, compiles=compiles, path=[], text=seed, node_cls=s0["cls"])
frontier = [dict(text=seed, score=s0["score"], cls=s0["cls"], path=[])]
best = frontier[0]
seen = {seed}
for d in range(depth):
children = []
for parent in frontier:
psig = signature(parent["cls"])
cands = gen_fn(parent["text"], parent["cls"])[:cap]
for rec, desc, ctext in cands:
if ctext in seen:
continue
seen.add(ctext)
if compiles >= budget:
break
r = scorer_fn(ctext)
compiles += 1
row = dict(depth=d + 1, move=f"{rec} {desc}", parent=parent["score"], score=r["score"],
kind=(r["cls"]["kind"] if r["cls"] else r.get("err", "")[:14]), ident=r["ident"])
if trace is not None:
trace.append(row)
if r["score"] is None:
continue
child = dict(text=ctext, score=r["score"], cls=r["cls"], path=parent["path"] + [f"{rec} {desc}"],
noop=(r["score"] == parent["score"] and signature(r["cls"]) == psig))
if r["score"] == 0 and r["ident"]:
if log:
log(f" depth {d + 1}: score 0 by {' + '.join(child['path'])}")
return dict(verdict="MATCH", best=0, start=s0["score"], compiles=compiles, path=child["path"],
text=ctext, node_cls=r["cls"])
if r["score"] <= parent["score"]:
children.append(child)
if compiles >= budget:
break
if not children:
break
children.sort(key=lambda c: (c["score"], c["noop"]))
if children[0]["score"] < best["score"]:
best = children[0]
if log:
log(f" depth {d + 1}: {len(children)} kept of the scored, best {children[0]['score']} "
f"({' + '.join(children[0]['path'])}) · {compiles} compiles")
frontier = children[:beam]
if compiles >= budget:
return dict(verdict="BUDGET", best=best["score"], start=s0["score"], compiles=compiles, path=best["path"],
text=best["text"], node_cls=best["cls"])
return dict(verdict="NO-MATCH", best=best["score"], start=s0["score"], compiles=compiles, path=best["path"],
text=best["text"], node_cls=best["cls"])
def make_gen(tu, fn, names):
def gen(text, cls):
fam = FAMILIES[family_key(cls)] if cls else dl.ALL_FAMILIES
cands = dl.recipe_candidates(text, tu, fn, names, cap=None, families=fam)
return rank(cands, fam, cls, body_span(text, tu, fn))
return gen
# ----------------------------------------------------------------------------------------------------------------------
# one exemplar, end to end
# ----------------------------------------------------------------------------------------------------------------------
def run_body(ex, a, tag, by_src, inc, inflight, log):
tu, fn = ex["tu"], ex["fn"]
t0 = time.time()
row = dict(kind="attempt", ts=time.strftime("%Y-%m-%d %H:%M:%S"), label=a.label, nhash=ex["nhash"], tu=tu, fn=fn,
alias=ex["alias"], copies=ex["copies"], needed=ex["needed"], kinds=ex["kinds"], regs=ex.get("regs", []),
beam=a.beam, depth=a.depth, cap=a.cap, budget=a.budget)
sites = sites_by_body().get((tu, fn), [])
raw = (REPO / tu).read_text(errors="surrogateescape")
try:
if not sites:
raise Unstrippable([("<census>", 0, "no site in the census for this body (stale? rerun lever_census --sites)")])
free = lever_free_body(tu, raw, fn, sites)
scorer = Scorer(tu, fn, recs_for(tu, by_src, inc), tag, inflight)
except Unstrippable as u:
row.update(verdict="UNSTRIPPABLE", why=u.args[0][:4], seconds=round(time.time() - t0, 1))
outcome_append(row)
log(f" {ex['alias']}__{fn}: UNSTRIPPABLE — {u.args[0][:2]}")
return row
# THE CONTROL (R56): the tree's own text must score 0 AND be byte-identical, or this harness is not measuring this function
ctl = scorer.score(raw)
if ctl["score"] != 0 or not ctl["ident"]:
row.update(verdict="UNCALIBRATED", control=dict(score=ctl["score"], ident=ctl["ident"], err=ctl.get("err", "")),
seconds=round(time.time() - t0, 1))
outcome_append(row)
log(f" {ex['alias']}__{fn}: UNCALIBRATED — the tree's own text scores {ctl['score']} ident={ctl['ident']} {ctl.get('err', '')[:80]}")
return row
names = dl.pin_names(sites)
trace = []
res = search(free, scorer.score, make_gen(tu, fn, names), beam=a.beam, depth=a.depth, cap=a.cap, budget=a.budget,
trace=trace, log=log)
if res["verdict"] == "MATCH":
# the bank verdict: every recipe of the file, then the proven apply path (the ledger row with before/after text)
v, dt, err = scorer.verify_all(res["text"])
body = None
if v != "IDENTICAL":
res["verdict"] = "ONE-OBJECT-ONLY" # R34: the one-object score said 0, the file's other objects disagree
row["verify"] = f"{v} {err[:120]}"
else:
d_ = body_span(res["text"], tu, fn)
ls = dl.line_starts(res["text"])
body = res["text"][ls[d_["line"] - 1]:ls[d_["end"]]]
# THE BANK IS BODY-ONLY (apply_body_core splices the definition; --propagate remaps that body to siblings), so
# a candidate that also edited lines outside the definition verifies at 0 and can never be banked. Say so in
# its own words instead of letting the bank fail on `conflicting types` (S102 run s4, func_80136824: 288
# compiles to a real score 0, then a BANK-REFUSED that read like a bad body). R14 no longer generates these.
d0 = body_span(free, tu, fn)
l0 = dl.line_starts(free)
outside_now = res["text"][:ls[d_["line"] - 1]] + res["text"][ls[d_["end"]]:]
outside_before = free[:l0[d0["line"] - 1]] + free[l0[d0["end"]]:]
if outside_now != outside_before:
res["verdict"] = "OUT-OF-BODY"
row["verify"] = "the candidate edits lines outside the definition; the bank is body-only"
body = None
if res["verdict"] == "MATCH" and body is not None:
bf = RUN / "bodies" / f"{(ex['alias'] or 'x')}__{fn}.c"
bf.parent.mkdir(parents=True, exist_ok=True)
bf.write_text(body, errors="surrogateescape") # the record of what was handed to the bank
# IN PROCESS (S101): the bank used to be a subprocess that re-imported delever.py (an edit mid-run broke a bank)
# and reloaded the recipes/includers each time
with _LOCK: # the ledger append and the source write, one bank at a time
ok_, line = dl.apply_body_core(tu, fn, body, a.label, "G", source=bf.relative_to(REPO).as_posix())
row["banked"] = ok_
row["bank_line"] = line[:220]
if not ok_:
res["verdict"] = "BANK-REFUSED"
row.update(verdict=res["verdict"], start=res.get("start"), best=res.get("best"), compiles=res["compiles"],
path=res.get("path", []), start_cls=(trace and None) or None, seconds=round(time.time() - t0, 1),
oracle_seconds=round(scorer.seconds, 1))
if res.get("node_cls"):
row["best_cls"] = {k: v for k, v in res["node_cls"].items() if k != "positions"}
if trace:
TRACE.mkdir(parents=True, exist_ok=True)
with open(TRACE / f"{(ex['alias'] or 'x')}__{fn}.jsonl", "w") as f:
f.write(json.dumps(dict(kind="seed", start=res.get("start"), tu=tu, fn=fn, label=a.label)) + "\n")
for t in trace:
f.write(json.dumps(t) + "\n")
outcome_append(row)
log(f" {ex['alias']}__{fn}: {row['verdict']} start={row['start']} best={row['best']} compiles={row['compiles']} "
f"({row['seconds']}s, {ex['copies']} copies, {ex['needed']} needed"
+ (f", {' + '.join(row['path'])}" if row['path'] else "") + ")")
return row
def cmd_run(a):
dl.ensure_census(a.jobs)
ok, why = oracle.calibration_current()
if not ok:
sys.exit(f"delever_search --run: calibration not current ({why})")
clean, dirty = dl.src_clean()
if not clean and not a.dirty_ok:
sys.exit(f"delever_search --run: src/ is dirty (commit or --restore first):\n{dirty[:400]}")
by_src = oracle.recipes_by_src(oracle.load_recipes()["recipes"])
inc = dl.includers()
ex = exemplars(a.only, a.limit, a.include_done)
if not ex:
print("delever_search --run: no drawable exemplar (every class attempted, or --only matched none) (R68)")
return 1
inflight = {}
RUN.mkdir(parents=True, exist_ok=True)
print(f"delever_search --run: {len(ex)} exemplars ({sum(e['copies'] for e in ex):,} bodies behind them), {a.jobs} worker(s), "
f"beam {a.beam} × depth {a.depth} × cap {a.cap}, budget {a.budget} compiles per body, label {a.label}", flush=True)
t0 = time.time()
def log(msg):
print(msg, flush=True)
# a worker owns a whole TU: exemplars grouped by TU; headers serial after the TUs
by_tu = collections.OrderedDict()
for e in ex:
by_tu.setdefault(e["tu"], []).append(e)
# BOTTOM-UP WITHIN A FILE (rung R's rule): a bank shifts the lines of every body below it, and the census positions the
# lever-free rewrite starts from were taken before the run — run g1 lost func_80142EC0 to a `token mismatch` after
# func_801424E4 was banked above it in the same TU
sb = sites_by_body()
for tu in by_tu:
by_tu[tu].sort(key=lambda e: -min((s["line"] for s in sb.get((e["tu"], e["fn"]), [])), default=0))
tus = [t for t in by_tu if not t.endswith(".h")]
hdrs = [t for t in by_tu if t.endswith(".h")]
rows = []
def work(item):
n, tu = item
return [run_body(e, a, f"g{n % max(1, a.jobs)}", by_src, inc, inflight, log) for e in by_tu[tu]]
with ThreadPoolExecutor(max_workers=max(1, a.jobs)) as pool:
for res in pool.map(work, list(enumerate(tus))):
rows += res
for tu in hdrs:
rows += work((0, tu))
won = [r for r in rows if r["verdict"] == "MATCH" and r.get("banked")]
# propagation, serially after the parallel phase: a sibling lives in a TU another worker may have owned
if a.propagate and won:
for r in won:
ns = argparse.Namespace(propagate=[r["tu"], r["fn"]], label=a.label + "p", only=[], limit=None)
try:
okn, n, bad = dl.propagate(ns) # in process (S101): no subprocess per sibling
line = f"delever --propagate: {okn} of {n} sibling(s) banked, {bad} refused"
rc = 0 if okn else 1
except SystemExit as e: # "no banked reshape" — the refusal is the line
line, rc = str(e), 2
r["propagate_line"] = line[:200]
outcome_append(dict(kind="propagate", ts=time.strftime("%Y-%m-%d %H:%M:%S"), label=a.label, tu=r["tu"], fn=r["fn"],
nhash=r["nhash"], line=line[:300], rc=rc))
log(f" propagate {r['alias']}__{r['fn']}: {line[:160]}")
v = collections.Counter(r["verdict"] for r in rows)
print(f"\nsearch: {len(won)} of {len(rows)} exemplars matched lever-free in {(time.time() - t0) / 3600:.2f} h "
f"({sum(r['copies'] for r in won):,} of {sum(r['copies'] for r in rows):,} bodies behind them; "
f"{sum(r.get('compiles', 0) for r in rows):,} compiles) — " + " · ".join(f"{k} {n}" for k, n in v.most_common()))
return 0
# ----------------------------------------------------------------------------------------------------------------------
# --plan, --status, --positive-control
# ----------------------------------------------------------------------------------------------------------------------
def cmd_plan(a):
ex = exemplars(a.only, a.limit, a.include_done)
tot = exemplars(include_done=True)
print(f"delever_search --plan: {len(ex)} exemplars drawable of {len(tot)} residue classes "
f"({sum(e['copies'] for e in ex):,} bodies behind them)")
if a.score:
by_src = oracle.recipes_by_src(oracle.load_recipes()["recipes"])
inc = dl.includers()
for e in ex[:a.show]:
line = (f" {e['copies']:4d} copies · {e['needed']:2d} needed {','.join(e['kinds']):<26} {','.join(e.get('regs', [])) or '-':<12} "
f"{e['alias']} {e['tu']}:{e['fn']}")
if a.score:
try:
sites = sites_by_body().get((e["tu"], e["fn"]), [])
raw = (REPO / e["tu"]).read_text(errors="surrogateescape")
free = lever_free_body(e["tu"], raw, e["fn"], sites)
sc_ = Scorer(e["tu"], e["fn"], recs_for(e["tu"], by_src, inc), "plan", {})
ctl = sc_.score(raw)
r = sc_.score(free)
line += (f" · control {ctl['score']}/{'id' if ctl['ident'] else 'DIFF'} · start {r['score']} "
f"{family_key(r['cls']) if r['cls'] else r.get('err', '')[:30]} {r['cls']['pairs'][:3] if r['cls'] else ''}")
except Unstrippable as u:
line += f" · UNSTRIPPABLE {u.args[0][:1]}"
print(line, flush=True)
if len(ex) > a.show:
print(f" … {len(ex) - a.show} more")
return 0
def explain(a):
"""--explain TU FN: the lever-free body's residual, read — its class, every differing block as mnemonics (mine vs the
target), the NEEDED sites with their lines — and, with --path "move1|move2", the residual after those moves are applied in
order (a move is named exactly as the trace names it). No writes; one compile per text scored."""
tu, fn = a.explain
by_src = oracle.recipes_by_src(oracle.load_recipes()["recipes"])
inc = dl.includers()
raw = (REPO / tu).read_text(errors="surrogateescape")
sites = sites_by_body().get((tu, fn), [])
scorer = Scorer(tu, fn, recs_for(tu, by_src, inc), "ex", {})
names = dl.pin_names(sites)
text = lever_free_body(tu, raw, fn, sites)
for mv in [m.strip() for m in (a.path or "").split("|") if m.strip()]:
cands = dl.recipe_candidates(text, tu, fn, names, cap=None, families=dl.ALL_FAMILIES)
hit = next((c for c in cands if f"{c[0]} {c[1]}" == mv), None)
if hit is None:
sys.exit(f"delever_search --explain: no candidate named `{mv}` on the current text (have: {[f'{c[0]} {c[1]}' for c in cands[:12]]} …)")
text = hit[2]
r = scorer.score(text)
if r["score"] is None:
print(f"{tu}:{fn}: {r['err']}")
return 1
cls = r["cls"]
print(f"{tu}:{fn}: score {r['score']} ({family_key(cls)}; mine {cls['n_mine']} ins, target {cls['n_tgt']}) ident={r['ident']}"
+ (f" after {a.path}" if a.path else ""))
if cls["pairs"]:
print(" register pairs (mine -> target, count):", ", ".join(f"{x}->{y} x{n}" for x, y, n in cls["pairs"]))
row = latest_row(tu, fn) or {}
for s in sites:
v = next((d for d in row.get("sites", []) if d.get("line") == s["line"]), None)
tag = (v or {}).get("verdict", "?")
print(f" site {s['kind']:11s} {s.get('detail', ''):5s} line {s['line']:5d} {tag:8s} {s.get('text', '')[:70]}")
mine = md.insns_from_object(str(scorer.scratch), fn)
aa, bb = [masked_word(i) for i in mine], [masked_word(i) for i in scorer.tgt]
for t, i1, i2, j1, j2 in difflib.SequenceMatcher(None, aa, bb, autojunk=False).get_opcodes():
if t == "equal":
continue
print(f" {t:7s} mine[{i1}:{i2}] target[{j1}:{j2}]")
for k in range(max(i2 - i1, j2 - j1)):
m_ = mine[i1 + k]["mnem"] if i1 + k < i2 else "--"
t_ = scorer.tgt[j1 + k]["mnem"] if j1 + k < j2 else "--"
print(f" {i1 + k:4d} {m_:34s} | {t_}")
return 0
def score_file(a):
"""--try: `_score_file` with its per-call scratch removed afterwards (unless --keep, which prints where the candidate
object is — agents asked for both: parallel same-function scoring, and the object to dump)."""
try:
return _score_file(a)
finally:
tu, fn, _ = a.try_
base = RUN / "score"
pid = f"p{os.getpid()}"
dirs = list(base.glob(f"*__{fn}/{pid}"))
if getattr(a, "keep", False):
for d in dirs:
print(f" --keep: candidate object {d / 'cand.o'}")
else:
import shutil
for d in dirs:
shutil.rmtree(d, ignore_errors=True)
for o in oracle.SCRATCH.glob(f"*.score_*_{fn}_{pid}.o"):
try:
o.unlink()
except OSError:
pass
def _score_file(a):
"""--try TU FN FILE: FILE is a candidate text of the WHOLE translation unit (or, with --body, of the function's definition
alone, spliced into the tree's TU); it is compiled through the TU's own recipe with the source path swapped for a scratch copy
(`-I<the TU's directory>` added so its relative includes resolve) and the function's instructions are compared with the fleet
run's baseline object — the same score, class and mnemonic diff as --explain, WITHOUT writing the tree. This is the loop an
agent runs on its own candidate (T7, one agent at a time): zero tree writes, so any number may run at once; the bank itself
stays the coordinator's (`delever.apply_body_core` on the real recipe)."""
tu, fn, path = a.try_
by_src = oracle.recipes_by_src(oracle.load_recipes()["recipes"])
inc = dl.includers()
recs = recs_for(tu, by_src, inc)
if not recs:
sys.exit(f"delever_search --try: no recipe compiles {tu}")
text = pathlib.Path(path).read_text(errors="surrogateescape")
raw = (REPO / tu).read_text(errors="surrogateescape")
if a.body:
d_ = body_span(raw, tu, fn)
if d_ is None:
sys.exit(f"delever_search --try: {fn} is not defined in {tu}")
ls = dl.line_starts(raw)
text = raw[:ls[d_["line"] - 1]] + text.rstrip("\n") + "\n" + raw[ls[d_["end"]]:]
rec = recs[0]
src_dir = os.path.dirname(rec["src"] if not tu.endswith(".h") else tu)
# PER CALL, not per function (S103): two `--try` runs of the SAME function shared this directory and one of them read
# the other's object — agents c19, c21 and c22 each wrote a private parallel scorer to get around it. The directory is
# removed afterwards unless --keep (which prints the object's path, the other thing agents asked for).
scratch_dir = RUN / "score" / f"{rec['alias']}__{fn}" / f"p{os.getpid()}"
scratch_dir.mkdir(parents=True, exist_ok=True)
scratch_src = scratch_dir / os.path.basename(rec["src"])
# a header candidate: the includer compiles with the header swapped in through a scratch include dir that shadows it
if tu.endswith(".h"):
shadow = scratch_dir / "inc"
(shadow / os.path.dirname(os.path.relpath(tu, os.path.dirname(rec["src"])))).mkdir(parents=True, exist_ok=True)
(shadow / os.path.relpath(tu, os.path.dirname(rec["src"]))).write_text(text, errors="surrogateescape")
scratch_src.write_text(raw if False else (REPO / rec["src"]).read_text(errors="surrogateescape"), errors="surrogateescape")
# the INCLUDER's directory too, after the shadow: its own `#include "../shared/engine_prelude.h"` resolves only
# from there (the scratch copy sits in scratch_dir, whose `../shared` does not exist). Without it every header-TU
# candidate was a COMPILE-ERROR — S103's regen pass found 24 of 24 such classes unscorable, none ever judged.
extra = f"-I{shadow.as_posix()} -I{os.path.dirname(rec['src'])} -I{src_dir}"
else:
scratch_src.write_text(text, errors="surrogateescape")
extra = f"-I{src_dir}"
pipeline = rec["pipeline"].replace(" " + rec["src"], " " + scratch_src.as_posix(), 1).replace("-Iinclude", f"-Iinclude {extra}", 1)
mod = dict(rec, pipeline=pipeline, src=scratch_src.as_posix())
# THE TAG IS PER FUNCTION, not per file. compile_obj names its scratch object `<obj>.<tag>.o`, so with a constant tag
# two agents scoring different functions of the SAME translation unit write and read one object — T7's burst of 20
# (S102) put nine agents in one file and two of them reported it independently: spurious COMPILE-ERRORs, and one agent
# scored four candidates against another agent's function before the echoed TU/FN line gave it away.
tag = "score_" + re.sub(r"\W+", "_", f"{rec['alias']}_{fn}") + f"_p{os.getpid()}"
data, dt, err = oracle.compile_obj(mod, None, tag=tag)
if data is None:
print(f"{tu}:{fn}: {'COMPILE-CRASH' if err.startswith('CRASH:') else 'COMPILE-ERROR'} — {err[:300]}")
return 2
base = oracle.baseline_path(rec["obj"]) # ditto: an agent scoring during an R22 gate must still see a baseline
tgt = md.insns_from_object(str(base), fn)
p = scratch_dir / "cand.o"
p.write_bytes(data)
mine = md.insns_from_object(str(p), fn)
cls = classify(mine, tgt)
print(f"{tu}:{fn}: score {cls['score']} ({family_key(cls)}; mine {cls['n_mine']} ins, target {cls['n_tgt']}) — "
f"{'MATCH (the function is byte-identical; the bank judges the whole object)' if cls['score'] == 0 else 'not yet'}")
if cls["pairs"]:
print(" register pairs (mine -> target, count):", ", ".join(f"{x}->{y} x{n}" for x, y, n in cls["pairs"]))
aa, bb = [masked_word(i) for i in mine], [masked_word(i) for i in tgt]
for t, i1, i2, j1, j2 in difflib.SequenceMatcher(None, aa, bb, autojunk=False).get_opcodes():
if t == "equal":
continue
print(f" {t:7s} mine[{i1}:{i2}] target[{j1}:{j2}]")
for k in range(max(i2 - i1, j2 - j1)):
m_ = mine[i1 + k]["mnem"] if i1 + k < i2 else "--"
t_ = tgt[j1 + k]["mnem"] if j1 + k < j2 else "--"
print(f" {i1 + k:4d} {m_:34s} | {t_}")
return 0 if cls["score"] == 0 else 1
def latest_row(tu, fn):
row = None
for r in dl.load_ledger():
if r.get("tu") == tu and r.get("fn") == fn:
row = r
return row
def cmd_status(a):
outs = load_outcomes()
att = [o for o in outs if o.get("kind") == "attempt"]
v = collections.Counter(o.get("verdict") for o in att)
left = exemplars()
print(f"delever_search --status: {len(att)} attempts {dict(v)} · {sum(1 for o in att if o.get('banked'))} banked · "
f"{len(left)} residue classes not yet attempted ({sum(e['copies'] for e in left):,} bodies)")
for o in att:
if o.get("verdict") == "MATCH":
print(f" MATCH {o['alias']}__{o['fn']} ({o['copies']} copies, start {o.get('start')}, {o.get('compiles')} compiles, "
f"{' + '.join(o.get('path', []))})")
return 0
def positive_control(a):
"""perturb a body the tree already matches by one or two generator moves; the search must find its way back to 0.
Never writes the tree (the scorer restores after every compile; nothing is banked)."""
tu, fn = a.positive_control
raw = (REPO / tu).read_text(errors="surrogateescape")
d_ = body_span(raw, tu, fn)
if d_ is None:
sys.exit(f"delever_search --positive-control: {fn} is not defined in {tu}")
by_src = oracle.recipes_by_src(oracle.load_recipes()["recipes"])
inc = dl.includers()
scorer = Scorer(tu, fn, recs_for(tu, by_src, inc), "pc", {})
ctl = scorer.score(raw)
print(f"positive control {tu}:{fn}: the tree's own text scores {ctl['score']} ident={ctl['ident']}", flush=True)
if ctl["score"] != 0 or not ctl["ident"]:
sys.exit("delever_search --positive-control: the UNPERTURBED body does not score 0 — calibrate/build first (R56)")
names = dl.pin_names(sites_by_body().get((tu, fn), []))
gen = make_gen(tu, fn, names)
text, moves = raw, []
rng = random.Random(a.seed)
for k in range(a.moves):
cands = dl.recipe_candidates(text, tu, fn, names, cap=None, families=dl.ALL_FAMILIES)
rng.shuffle(cands)
picked = None
for rec, desc, ctext in cands[:40]:
r = scorer.score(ctext)
if r["score"] is not None and r["score"] > 0 and (k == 0 or r["score"] != scorer.score(text)["score"]):
picked = (rec, desc, ctext, r["score"])
break
if picked is None:
sys.exit(f"delever_search --positive-control: no generator move perturbs {fn} at step {k + 1} — pick another body")
moves.append(f"{picked[0]} {picked[1]}")
text = picked[2]
print(f" perturbed by {moves[-1]} -> score {picked[3]}", flush=True)
trace = []
res = search(text, scorer.score, gen, beam=a.beam, depth=a.depth, cap=a.cap, budget=a.budget, trace=trace,
log=lambda m: print(m, flush=True))
ok = res["verdict"] == "MATCH"
print(f"positive control: {'PASS' if ok else 'FAIL'} — perturbed by {' + '.join(moves)} (start {res.get('start')}), "
f"{res['verdict']} after {res['compiles']} compiles" + (f" by {' + '.join(res['path'])}" if ok else f" (best {res.get('best')})"))
outcome_append(dict(kind="positive-control", ts=time.strftime("%Y-%m-%d %H:%M:%S"), tu=tu, fn=fn, moves=moves,
verdict=res["verdict"], start=res.get("start"), compiles=res["compiles"], path=res.get("path", []),
seconds=round(scorer.seconds, 1)))
return 0 if ok else 1
# ----------------------------------------------------------------------------------------------------------------------
# --selftest: the classifier on synthetic streams, the beam on a stub scorer whose answer needs three composed moves
# ----------------------------------------------------------------------------------------------------------------------
def _ins(word, reloc_kind=None, reloc_op=None):
return dict(off=0, word=word, mnem="", reloc_kind=reloc_kind, reloc_op=reloc_op, jrel=None)
def selftest():
ok = True
def fail(msg):
nonlocal ok
ok = False
print("selftest:", msg)
# and v0,v1,v0 (00621024) vs and v0,v0,v1 (00431024): one REG position, caller-saved pairs
tgt = [_ins(0x8E230044), _ins(0x2402FFDF), _ins(0x00431024)]
mine = [_ins(0x8E220044), _ins(0x2403FFDF), _ins(0x00621024)]
c = classify(mine, tgt)
if c["kind"] != "REG" or c["bank"] != "caller" or c["score"] != 3:
fail(f"REG-caller classification wrong: {c}")
# The invariant, not a fixed window (widening the window once per new generator hid what it was for): the temp move
# leads, and every TARGETED caller-saved lever — R5 the commutative swap, R15 the sink, R16 the constant holder —
# is drawn before the BLIND families that permute declarations or statements wholesale (R9, R2, R4).
caller = FAMILIES["REG-caller"]
# R19 leads (the only family that can change a call's arity, and free when it does not apply), R6 the temp move is
# still near the front, and every TARGETED lever precedes every BLIND family. The window is deliberately loose: it
# was widened once per new generator until it stopped expressing anything, so it now states the ORDERING RULE only.
# and every TARGETED lever before every BLIND family that permutes declarations or statements wholesale.
targeted, blind = {"R5", "R15", "R16", "R19"}, {"R9", "R2", "R4"}
if family_key(c) != "REG-caller" or caller[0] != "R19" or "R6" not in caller[:4] or not targeted <= set(caller) \
or max(caller.index(t) for t in targeted) > min(caller.index(b) for b in blind):
fail(f"REG-caller family wrong: {family_key(c)} {caller}")
# a callee-saved swap: addu s0,a0,zero vs addu s1,a0,zero
c = classify([_ins(0x00808021)], [_ins(0x00808821)])
if c["kind"] != "REG" or c["bank"] != "callee":
fail(f"REG-callee classification wrong: {c}")
# COUNT: one extra move
c = classify([_ins(0x00808021), _ins(0x8E220044)], [_ins(0x8E220044)])
if c["kind"] != "COUNT":
fail(f"COUNT classification wrong: {c}")
# ORDER: the same two words swapped
c = classify([_ins(0x8E220044), _ins(0x2403FFDF)], [_ins(0x2403FFDF), _ins(0x8E220044)])
if c["kind"] != "ORDER":
fail(f"ORDER classification wrong: {c}")
# a reloc-masked pair with the same symbol is not a mismatch; a different symbol is
c = classify([_ins(0x3C040000, "HI16", "D_800A0000")], [_ins(0x3C040000, "HI16", "D_800A0000")])
if c["kind"] != "IDENTICAL":
fail(f"masked reloc read as a mismatch: {c}")
c = classify([_ins(0x3C040000, "HI16", "D_800A0000")], [_ins(0x3C040000, "HI16", "D_800A0004")])
if c["score"] != 1:
fail(f"a different reloc symbol not counted: {c}")
# the beam on a stub: a text is "a,b,c"; the target (2,1,3); score = L1 distance; moves = ±1 on one coordinate, tagged
# by family so the ranking has something to order; the start (1,0,2) is THREE moves away -> depth 3 must find it
target = (2, 1, 3)
def parse(t):
return tuple(int(x) for x in t.split(","))
def stub_score(t):
v = parse(t)
s = sum(abs(x - y) for x, y in zip(v, target))
return dict(score=s, ident=(s == 0), cls=dict(kind=("IDENTICAL" if s == 0 else "OTHER"), bank=None, score=s, n_mine=3,
n_tgt=3, positions=[i for i in range(3) if v[i] != target[i]], pairs=[]),
seconds=0.0, err="")
def stub_gen(t, cls):
v = parse(t)
out = []
for i in range(3):
for dlt in (1, -1):
w = list(v)
w[i] += dlt
out.append((("R5", "R7", "R2")[i], f"coord{i}{'+' if dlt > 0 else '-'} @{i + 1}", ",".join(map(str, w))))
return out
calls = dict(n=0)
def counting(t):
calls["n"] += 1
return stub_score(t)
res = search("1,0,2", counting, stub_gen, beam=2, depth=3, cap=6, budget=200)
if res["verdict"] != "MATCH" or len(res["path"]) != 3 or res["compiles"] != calls["n"]:
fail(f"the beam did not compose three moves: {res}")
res = search("1,0,2", stub_score, stub_gen, beam=2, depth=2, cap=6, budget=200)
if res["verdict"] != "NO-MATCH" or res["best"] != 1:
fail(f"depth 2 should end NO-MATCH at distance 1: {res}")
res = search("1,0,2", stub_score, stub_gen, beam=2, depth=3, cap=6, budget=4)
if res["verdict"] != "BUDGET" or res["compiles"] > 4:
fail(f"the budget did not stop the search: {res}")
res = search("2,1,3", stub_score, stub_gen)
if res["verdict"] != "MATCH" or res["compiles"] != 1:
fail(f"a seed at distance 0 must be MATCH in one score: {res}")
# the generators on a fixture body: R8 introduces a typed temp, R9 swaps adjacent statements, both through the real parser
fix = ("s32 f(s32 *p, s32 q)\n{\n s32 a;\n s32 b;\n a = *(s32 *)(p + 4) & q;\n b = q + 1;\n return a + b;\n}\n")
cands = dl.recipe_candidates(fix, "src/x.c", "f", [], cap=None, families=("R8", "R9"))
r8 = [c for c in cands if c[0] == "R8"]
r9 = [c for c in cands if c[0] == "R9"]
temp = [c for c in r8 if c[1].startswith("temp")]
base = [c for c in r8 if c[1].startswith("base")]
if len(temp) != 1 or "s32 tmp0;" not in temp[0][2] or "tmp0 = *(s32 *)(p + 4);" not in temp[0][2] or "a = tmp0 & q;" not in temp[0][2]:
fail(f"R8 did not hoist the dereference into a typed temp: {temp[:1]}")
if len(base) != 1 or "tmp0 = p + 4;" not in base[0][2] or "a = *(s32 *)tmp0 & q;" not in base[0][2]:
fail(f"R8 did not hoist the dereference's base into an address local: {base[:1]}")
if len(r9) != 1 or " b = q + 1;\n a = *(s32 *)(p + 4) & q;\n" not in r9[0][2]:
fail(f"R9 did not swap the two adjacent statements: {r9[:1]}")
default = dl.recipe_candidates(fix, "src/x.c", "f", [], cap=None)
if any(c[0] in ("R8", "R9", "R10", "R12", "R13") for c in default):
fail("rung R's default family set grew — R8..R13 must stay the engine's until measured")
# R10 on a void-parameter function must yield nothing; on (s32 *p, s32 q) one copy per parameter, pointer spelled `s32 *p2`
fix2 = ("s32 g(void)\n{\n s32 d;\n d = 1;\n return d;\n}\n")
if dl.recipe_candidates(fix2, "src/x.c", "g", [], cap=None, families=("R10",)):
fail("R10 generated a parameter copy for a (void) function")
r10 = dl.recipe_candidates(fix, "src/x.c", "f", [], cap=None, families=("R10",))
if len(r10) != 2 or "s32 *p2;" not in r10[0][2] or "p2 = p;" not in r10[0][2] or "*(s32 *)(p2 + 4)" not in r10[0][2]:
fail(f"R10 parameter copies wrong: {[(c[0], c[1]) for c in r10]}")
# R12 widens/narrows a local; R13 reassociates a +/- chain; R8 names a repeated RHS once
fix3 = ("s32 h(s32 *p, s32 q)\n{\n s32 a;\n u16 w;\n a = q + *(s32 *)(p + 4) - w;\n"
" *(s16 *)(p + 28) = -a;\n *(s16 *)(p + 24) = -a;\n return a;\n}\n")
r12 = dl.recipe_candidates(fix3, "src/x.c", "h", [], cap=None, families=("R12",))
if [c[1] for c in r12] != ["width a s32->u16 @3", "width a s32->s16 @3", "width a s32->u8 @3",
"width w u16->s32 @4", "width w u16->s16 @4", "width w u16->u8 @4"]:
fail(f"R12 widths wrong: {[c[1] for c in r12]}")
# R14 changes a PARAMETER's width in the header; R8's shared base names one address local for two dereferences of one base
r14 = dl.recipe_candidates(fix3, "src/x.c", "h", [], cap=None, families=("R14",))
if [c[1] for c in r14] != ["param-width q s32->s16 @1", "param-width q s32->u16 @1", "param-width q s32->u8 @1"] \
or "s32 h(s32 *p, s16 q)" not in r14[0][2]:
fail(f"R14 parameter widths wrong: {[c[1] for c in r14]}")
fix4 = ("s32 m(s32 *p, s32 q)\n{\n s32 a;\n a = *(s32 *)(p + 4) & q;\n *(u16 *)(p + 4) = q;\n return a;\n}\n")
sb = [c for c in dl.recipe_candidates(fix4, "src/x.c", "m", [], cap=None, families=("R8",)) if c[1].startswith("base-shared")]
if len(sb) != 1 or "tmp0 = p + 4;" not in sb[0][2] or "a = *(s32 *)tmp0 & q;" not in sb[0][2] or "*(u16 *)tmp0 = q;" not in sb[0][2]:
fail(f"R8 shared base wrong: {[(c[1], c[2]) for c in sb][:1]}")
fix5 = ("s32 k(s32 param_2)\n{\n u8 *src;\n src = (u8 *)((u32)param_2);\n return src[3];\n}\n")
al = [c for c in dl.recipe_candidates(fix5, "src/x.c", "k", [], cap=None, families=("R10",)) if c[1].startswith("param-alias")]
if len(al) != 1 or "return param_2[3];" not in al[0][2] or "src = " in al[0][2]:
fail(f"R10 alias through a cast wrong: {[(c[1], c[2]) for c in al][:1]}")
r13 = dl.recipe_candidates(fix3, "src/x.c", "h", [], cap=None, families=("R13",))
if len(r13) != 1 or "a = q - w + *(s32 *)(p + 4);" not in r13[0][2]:
fail(f"R13 reassociation wrong: {[(c[1], c[2].split(chr(10))[4]) for c in r13]}")
cse = [c for c in dl.recipe_candidates(fix3, "src/x.c", "h", [], cap=None, families=("R8",)) if c[1].startswith("cse")]
if len(cse) != 1 or "tmp0 = -a;" not in cse[0][2] or cse[0][2].count("= tmp0;") != 2:
fail(f"R8 common subexpression wrong: {[(c[1]) for c in cse]}")
print(f"delever_search --selftest: {'OK' if ok else 'FAILED'} — classifier 6 cases, beam 4 cases, generators R8/R9")
return ok
def main():
ap = argparse.ArgumentParser()
g = ap.add_mutually_exclusive_group(required=True)
g.add_argument("--plan", action="store_true")
g.add_argument("--run", action="store_true")
g.add_argument("--positive-control", nargs=2, metavar=("TU", "FN"))
g.add_argument("--status", action="store_true")
g.add_argument("--explain", nargs=2, metavar=("TU", "FN"), help="read one body's residual (no writes)")
g.add_argument("--try", dest="try_", nargs=3, metavar=("TU", "FN", "FILE"), help="score a candidate TU text (or --body: a body) WITHOUT writing the tree")
g.add_argument("--selftest", action="store_true")
ap.add_argument("--path", help="--explain: moves to apply first, `|`-separated, named as the trace names them")
ap.add_argument("--body", action="store_true", help="--try: FILE holds the function's definition only")
ap.add_argument("--keep", action="store_true", help="--try: keep the per-call scratch and print the candidate object path")
ap.add_argument("--only", nargs="*", default=[], help="fn, TU (substring), alias or nhash prefix")
ap.add_argument("--limit", type=int)
ap.add_argument("--show", type=int, default=25)
ap.add_argument("--score", action="store_true", help="--plan: score each listed exemplar's lever-free text (one compile each + the control)")
ap.add_argument("--beam", type=int, default=3)
ap.add_argument("--depth", type=int, default=3)
ap.add_argument("--cap", type=int, default=48, help="candidates scored per node")
ap.add_argument("--budget", type=int, default=400, help="compiles per body")
ap.add_argument("--moves", type=int, default=1, help="--positive-control: how many perturbing moves")
ap.add_argument("--seed", type=int, default=1)
ap.add_argument("--label", default="g1")
ap.add_argument("--propagate", action=argparse.BooleanOptionalAction, default=True, help="spread each bank to its class (serial, after the run)")
ap.add_argument("--dirty-ok", action="store_true")
ap.add_argument("--include-done", action="store_true", help="draw classes that already have an outcome row")
ap.add_argument("-j", "--jobs", type=int, default=8)
a = ap.parse_args()
os.chdir(REPO)
RUN.mkdir(parents=True, exist_ok=True)
if a.selftest:
return 0 if selftest() else 1
if a.plan:
return cmd_plan(a)
if a.status:
return cmd_status(a)
if a.explain:
return explain(a)
if a.try_:
return score_file(a)
if a.positive_control:
return positive_control(a)
if a.run:
return cmd_run(a)
if __name__ == "__main__":
sys.exit(main())