Files
BFM-decomp/tools/idiom_hunt.py
T
Drew T d71305fa5f feat(phase-23): T10.8 idiom-hunt harness + calibration — GLM confirms our idioms, no NEW ones
- tools/idiom_hunt.py: group backlog near-misses by residual class -> GLM names the reusable idiom +
  emits corrected C -> byte-gate to validate; captures reasoning; HARD --budget cap
- CALIBRATION ($0.51 total, 2 classes): struct + regalloc-order -> 0 banks. GLM re-derives our OWN
  idioms (register-pin §17, array-of-struct §18, type-width §25) and confirms walls, but banks nothing
  new — the backlog near-misses are the residual our idioms already failed on (irreducible/def-side wall).
- VERDICT: the new-idiom well is DRY (Fable5 review confirmed empirically for $0.51, not $300 overnight).
  GLM's value stays: direct drafter for FRESH def-conflict-free hard fns (~22%), not an idiom generator.
2026-07-01 23:13:29 -06:00

238 lines
11 KiB
Python

#!/usr/bin/env python3
"""idiom_hunt.py — Phase 23 T10.7: use a REASONING model (GLM5.2) to DISCOVER new gcc-2.7.2 idioms from
grouped near-misses, not to bank random functions (Drew's reframe: idioms compound, fleet% is linear; R16).
The backlog holds ~3100 near-misses (median closeness 1 — often ONE instruction off byte-match), classified
by residual class (regalloc-order / schedule / iv-combine / remat / struct / ... + a large unclassified pool).
Grouping near-misses of the SAME class and asking a reasoning model "what reusable C transformation makes
gcc-2.7.2 emit these bytes?" turns GLM from a drafter into an IDIOM RESEARCHER: one discovered idiom banks a
whole class (now + forever + for free v3). Some classes are intrinsic walls (def-side, narrow-param) — GLM
CONFIRMS those; that is also progress.
Per batch (same-class near-misses): assemble exemplars (asm + best-draft + the match_one diff) -> GLM ->
save REASONING (.run/idiom_hunt/, the idiom deliverable) -> parse the named idiom + per-fn corrected C ->
byte-gate the corrected C in each fn's binary (gate_stage, sole arbiter G3/P9). Reports idioms found, banks
(validation), and $ spent. **HARD --budget cap** (stops launching batches once spend hits it) — never a
surprise bill; calibrate cheap, scale only if the idiom yield-per-dollar pays.
Env: API_BASE, MODEL, API_KEY (OpenRouter), MAXTOK (default 24000). Usage:
API_BASE=https://openrouter.ai/api/v1 MODEL=z-ai/glm-5.2 API_KEY=... \
tools/idiom_hunt.py --max-close 3 --min-nins 16 --count 30 --batch 6 --budget 8 --workers 3
"""
import argparse, glob, json, os, re, subprocess, sys, threading, urllib.request
from concurrent.futures import ThreadPoolExecutor
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import backlog
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PY = '.venv/bin/python'
API_BASE = os.environ.get('API_BASE', '').rstrip('/')
API_KEY = os.environ.get('API_KEY', 'none')
MODEL = os.environ.get('MODEL', '')
MAXTOK = int(os.environ.get('MAXTOK', '24000'))
OUT = os.path.join(REPO, '.run/idiom_hunt')
COST = [0.0]
_LK = threading.Lock()
SYS = ("You are a PS1 matching-decompilation expert who discovers REUSABLE compiler idioms. The compiler is "
"gcc-2.7.2 (-O2 -G0 -mips1, C89/K&R). Given several functions that are ALMOST byte-identical to the "
"target MIPS (a few instructions off, same residual class), you find the ONE reusable C transformation "
"that makes gcc emit the exact target bytes, name it, and apply it. Bytes are truth; never hand-wave.")
def sh(cmd, timeout=None):
return subprocess.run(cmd, capture_output=True, text=True, cwd=REPO, timeout=timeout)
def cookbook_known():
"""The idiom § headers we ALREADY have — so GLM finds what's NEEDED (new, or a known one applied), not
re-derive the obvious. Condensed from docs/matching-cookbook.md."""
try:
heads = [l.strip('# ').strip() for l in open(os.path.join(REPO, 'docs/matching-cookbook.md'))
if re.match(r'#+\s*§', l)]
return '\n'.join('- ' + h[:110] for h in heads)
except Exception:
return '(cookbook unavailable)'
def call(messages, retries=2):
body = json.dumps({'model': MODEL, 'messages': messages, 'max_tokens': MAXTOK, 'temperature': 0.2}).encode()
for attempt in range(retries + 1):
try:
req = urllib.request.Request(API_BASE + '/chat/completions', data=body,
headers={'Content-Type': 'application/json',
'Authorization': 'Bearer ' + API_KEY})
with urllib.request.urlopen(req, timeout=1800) as r:
d = json.loads(r.read())
m = d['choices'][0]['message']
with _LK:
COST[0] += (d.get('usage') or {}).get('cost', 0) or 0
return (m.get('content') or ''), (m.get('reasoning') or '')
except Exception as e:
if attempt < retries:
continue
return '', f'[call error: {e}]'
def asm_of(binary, fn):
hits = glob.glob(os.path.join(REPO, f'asm/{binary}/nonmatchings/*/{fn}.s'))
if not hits:
return '(asm not found)', None
return open(hits[0]).read(), os.path.relpath(os.path.dirname(hits[0]), REPO)
def diff_of(fn, draft_path, asm_subdir):
if not (draft_path and os.path.exists(os.path.join(REPO, draft_path)) and asm_subdir):
return '(no diff)'
r = sh([PY, 'tools/match_one.py', fn, '--c', draft_path, '--asm-subdir', asm_subdir], timeout=180)
return '\n'.join((r.stdout + r.stderr).splitlines()[:40])
def build_user(klass, items):
ex = []
for it in items:
draft = ''
p = it.get('best_draft')
if p and os.path.exists(os.path.join(REPO, p)):
draft = open(os.path.join(REPO, p)).read()
ex.append(f"""----- {it['name']} (binary {it['binary']}, {it.get('nins')} ins, closeness "
{it.get('closeness')}) -----
--- TARGET MIPS ---
{it['asm']}
--- CLOSEST C SO FAR ---
{draft or '(none)'}
--- match_one DIFF (idx | MINE | TARGET) ---
{it['diff']}
""")
return f"""These {len(items)} functions share the residual class **{klass}** — each is a few instructions
off byte-identical (see the diffs). Find the ONE reusable gcc-2.7.2 C idiom that makes the compiler emit the
exact target bytes for this class, then apply it.
Idioms we ALREADY have (find what's NEEDED — a NEW idiom, or one of these applied; if the class is an
intrinsic wall no C can steer, say so and explain why):
{cookbook_known()}
{chr(10).join(ex)}
OUTPUT:
1) `## IDIOM: <short name>` then 2-5 sentences: the gcc-2.7.2 behavior, the reusable C transformation that
triggers the wanted bytes, and when it applies. If the class is an intrinsic wall, write `## WALL:` + why.
2) For EACH function, a fenced block `// FN: func_XXXX` then the corrected full C:
```c
// FN: func_XXXXXXXX
<corrected C>
```
Only change what the idiom dictates; keep every other instruction identical.
"""
def parse_fns(content):
"""map func_XXXX -> corrected C, from ```c blocks tagged `// FN: func_XXXX`."""
out = {}
for blk in re.findall(r'```c?\s*\n(.*?)```', content, re.S):
m = re.search(r'//\s*FN:\s*(func_[0-9A-Fa-f]+)', blk)
if m:
out[m.group(1)] = re.sub(r'//\s*FN:\s*func_[0-9A-Fa-f]+\s*\n', '', blk, count=1)
return out
def idiom_name(content):
m = re.search(r'##\s*(IDIOM|WALL):\s*(.+)', content)
return (m.group(1) + ': ' + m.group(2).strip()[:80]) if m else '(none stated)'
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('--max-close', type=int, default=3, help='only near-misses within N mismatches')
ap.add_argument('--min-nins', type=int, default=16, help='hard band only')
ap.add_argument('--max-nins', type=int, default=150)
ap.add_argument('--class', dest='klass', default=None, help='restrict to one residual class')
ap.add_argument('--count', type=int, default=30, help='near-misses to pull')
ap.add_argument('--batch', type=int, default=6, help='near-misses per GLM call (same class)')
ap.add_argument('--budget', type=float, default=8.0, help='HARD $ cap — stop launching batches at this spend')
ap.add_argument('--workers', type=int, default=3)
a = ap.parse_args()
if not (API_BASE and MODEL):
sys.exit('set API_BASE, MODEL, API_KEY')
os.makedirs(OUT, exist_ok=True)
recs = [r for r in backlog.load_best()
if r.get('status') == 'near' and isinstance(r.get('closeness'), int)
and r['closeness'] <= a.max_close and r.get('best_draft')
and a.min_nins <= (r.get('nins') or 0) <= a.max_nins
and (not a.klass or (r.get('klass') or '').split()[0] == a.klass)]
for r in recs:
r['binary'] = r.get('binary') or 'ov_SC01_077' # legacy records default to the canonical binary
# group by class, then chunk each class into batches
byc = {}
for r in sorted(recs, key=lambda x: (x.get('klass') or 'z', x['closeness'])):
byc.setdefault((r.get('klass') or 'unclassified').split()[0], []).append(r)
batches = []
taken = 0
for kl, lst in sorted(byc.items(), key=lambda kv: -len(kv[1])):
for i in range(0, len(lst), a.batch):
if taken >= a.count:
break
grp = lst[i:i + a.batch]
batches.append((kl, grp)); taken += len(grp)
print(f'idiom_hunt: {len(recs)} eligible near-misses -> {len(batches)} batches '
f'(classes: {sorted(byc)[:8]}), budget ${a.budget}')
# enrich each item with asm + diff
for _, grp in batches:
for it in grp:
it['asm'], sub = asm_of(it['binary'], it['name'])
it['diff'] = diff_of(it['name'], it.get('best_draft'), sub)
results = []
def run_batch(idx_kl_grp):
idx, (kl, grp) = idx_kl_grp
if COST[0] >= a.budget:
return None
content, reasoning = call([{'role': 'system', 'content': SYS},
{'role': 'user', 'content': build_user(kl, grp)}])
open(os.path.join(OUT, f'batch{idx:02d}_{kl}.txt'), 'w').write(
f'=== class {kl} ({len(grp)} fns) ===\nIDIOM: {idiom_name(content)}\n\n=== reasoning ===\n{reasoning}\n\n=== content ===\n{content}\n')
return (idx, kl, grp, content)
with ThreadPoolExecutor(max_workers=a.workers) as ex:
for res in ex.map(run_batch, list(enumerate(batches))):
if res:
results.append(res)
# validate: gate the corrected C per binary (gate_stage recovery + whole-binary byte-gate)
import gate_stage
banked_all, idioms = [], []
bydir = {}
for idx, kl, grp, content in results:
idioms.append((kl, idiom_name(content)))
fns = parse_fns(content)
for it in grp:
c = fns.get(it['name'])
if not c:
continue
d = os.path.join(OUT, 'drafts', it['binary'])
os.makedirs(d, exist_ok=True)
open(os.path.join(d, it['name'] + '.c'), 'w').write(c)
bydir.setdefault(it['binary'], d)
for b, d in sorted(bydir.items()):
try:
r = gate_stage.run_gate(os.path.relpath(d, REPO), binary=b, propagate=False, commit=False,
compute_fleet=False, lock_path=f'.run/auto/gate.{b}.lock')
banked_all += r.get('verified', [])
except Exception as e:
print(f' [gate {b}: {e}]')
print(f'\n=== idiom_hunt summary === cost ${COST[0]:.3f}')
print(f'idioms/verdicts proposed ({len(idioms)}):')
for kl, nm in idioms:
print(f' [{kl}] {nm}')
print(f'validation banks (idioms that byte-gated): {len(banked_all)} — {" ".join(banked_all) or "(none)"}')
print(f'reasoning + idioms saved -> {OUT}/ (review + distill the winners into cookbook §)')
if __name__ == '__main__':
main()