mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 21:36:06 -04:00
8562aa89c1
- tools/glm_reconcile.py (NEW): aim GLM's reasoning at the DEF-side loose-typing wall (body + conflicting TU decls + reconciliation toolkit -> consistent buildable byte-identical decls); captures reasoning (.run/glm_reason/, idiom source R16); relax-in-any-TU-file + crash-robust call - api_draft: REASON=1 saves the reasoning trace per draft (idiom mining on any GLM run) - fix_arity_callers: --any-proto (relax any prototype, not just (void)) - RESULT: GLM's reasoning is expert-level (store-width/sh-vs-sw awareness, K&R promotion, independently derives the cast idiom) but banks only 1/7 reconciliations; mechanical relaxation 0/7. The def-side wall is INTRINSIC (narrow-param + byte-level addressing defeat reconciliation) — Fable5 §3c re-test CONFIRMS the wall holds even vs a frontier reasoning model aimed directly at it. func_80175184 banked, check-all 136/136
221 lines
11 KiB
Python
221 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""glm_reconcile.py — Option-3 (Phase 23 T10.7): aim a REASONING model at the DEF-side loose-typing WALL,
|
|
and CAPTURE its reasoning for the idiom flywheel (R16).
|
|
|
|
The def-side wall (Phase 16/20, re-confirmed by the T10.7 A/B): a byte-correct function body cannot be
|
|
banked because the overlay translation unit already declares the function (or its callees/data) with
|
|
signatures/types that conflict with the body's true form -> gcc-2.7.2 C89 `conflicting types`. This caps
|
|
ANY drafter -- it is a DECLARATION-reconciliation problem, per-function and situation-specific (mechanical
|
|
`fix_arity_callers --any-proto` recovers ~0). "Loose-typing reconciliation is a reasoning problem" (Fable5
|
|
review Sec 4.1): give GLM5.2 the body + the actual conflicting declarations + the legal byte-neutral moves,
|
|
and let it reason out a consistent, buildable, byte-identical set.
|
|
|
|
Per function: assemble context -> GLM -> save REASONING (.run/glm_reason/<fn>.txt, the idiom source) ->
|
|
parse a ```c block (reconciled C) + a ```relax block (forward-decls to K&R no-proto, in ANY TU file) ->
|
|
snapshot the TU, apply, whole-binary byte-gate (harvest_verify, sole arbiter G3/P9) -> keep iff
|
|
byte-identical, else restore the snapshot. Final check-all proves the shared-header relaxations are fleet-safe.
|
|
|
|
Env: API_BASE, MODEL, API_KEY (OpenRouter), MAXTOK (default 32000). Usage:
|
|
API_BASE=https://openrouter.ai/api/v1 MODEL=z-ai/glm-5.2 API_KEY=... \
|
|
tools/glm_reconcile.py --binary ov_SC01_077 --funcs func_A,func_B --bodies .run/ab_glm
|
|
"""
|
|
import argparse, glob, json, os, re, subprocess, sys, urllib.request
|
|
|
|
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', '32000'))
|
|
EC = os.path.join(REPO, 'src/shared/engine_core.h')
|
|
REASON_DIR = os.path.join(REPO, '.run/glm_reason')
|
|
DRAFTS = os.path.join(REPO, '.run/glm_recon')
|
|
COST = [0.0]
|
|
|
|
TOOLKIT = """LEGAL byte-neutral reconciliation moves under gcc-2.7.2 -O2 -G0 -mips1 (C89/K&R):
|
|
- Relax a conflicting file-scope forward-decl to K&R no-prototype: `extern R func_X(void);` OR
|
|
`extern R func_X(T,...);` -> `extern R func_X();`. Byte-neutral for callers ONLY IF the real def's
|
|
params are default-promotion-safe (int/s32/u32/long/pointer). char/short/s8/s16/u8/u16/float BY VALUE
|
|
are NOT safe (K&R default-promotes them) -> do NOT no-proto those; that is the intrinsic narrow-param wall.
|
|
- Match a DATA extern's type to what the TU already declares: same-width int/s32/u32 (and their pointers)
|
|
emit identical MIPS, so declare D_x with the type already present in the TU to avoid `conflicting types`.
|
|
- Cast a call site to the intended fn-ptr type: `((R(*)(A))func_X)(args)` -- gcc folds the cast of a known
|
|
symbol to a direct `jal`, byte-neutral. Use when a callee is a DEFINITION (can't be re-declared) or its
|
|
canonical decl differs from how THIS body must call it.
|
|
- NEVER change the function BODY's computation or control flow (it is already byte-correct); reconcile ONLY
|
|
declarations, extern types, and call-site casts.
|
|
The whole-binary SHA1 byte-gate is the sole arbiter (G3/P9): a wrong reconciliation just fails."""
|
|
|
|
SYS = ("You are a PS1 matching-decompilation expert. The original compiler is gcc-2.7.2 (-O2 -G0 -mips1, "
|
|
"C89/K&R). You reconcile C DECLARATIONS so a byte-correct function compiles into its translation "
|
|
"unit while every function stays byte-for-byte identical. You never change verified computation.")
|
|
|
|
|
|
def sh(cmd, timeout=None):
|
|
return subprocess.run(cmd, capture_output=True, text=True, cwd=REPO, timeout=timeout)
|
|
|
|
|
|
def good_sha(b):
|
|
return open(os.path.join(REPO, f'config/check.{b}.sha')).read().split()[0]
|
|
|
|
|
|
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']
|
|
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
|
|
print(f' [call error after {retries+1} tries: {e}]')
|
|
return '', ''
|
|
|
|
|
|
def tu_files(binary):
|
|
return [EC] + sorted(glob.glob(os.path.join(REPO, f'src/{binary}/{binary}*.c')))
|
|
|
|
|
|
def snapshot(binary):
|
|
return {f: open(f).read() for f in tu_files(binary)}
|
|
|
|
|
|
def restore(snap):
|
|
for f, t in snap.items():
|
|
open(f, 'w').write(t)
|
|
|
|
|
|
def relax_in_tu(binary, fns):
|
|
"""Relax `extern <ret> func_X(<params>);` -> `extern <ret> func_X();` in EVERY TU file (engine_core.h
|
|
AND the overlay .c) — the conflicting forward-decl can live in either (fix_arity_callers only did EC)."""
|
|
for f in tu_files(binary):
|
|
txt = orig = open(f).read()
|
|
for fn in fns:
|
|
m = re.search(r'func_([0-9A-Fa-f]+)', fn)
|
|
if not m:
|
|
continue
|
|
ad = m.group(1)
|
|
txt = re.sub(rf'(extern\s+[A-Za-z_][\w \t\*]*?\bfunc_{ad}\s*\()\s*[^;)]+?\s*(\)\s*;)', r'\1\2', txt, flags=re.I)
|
|
if txt != orig:
|
|
open(f, 'w').write(txt)
|
|
|
|
|
|
def find_asm(binary, fn):
|
|
hits = glob.glob(os.path.join(REPO, f'asm/{binary}/nonmatchings/*/{fn}.s'))
|
|
return open(hits[0]).read() if hits else '(asm not found)'
|
|
|
|
|
|
def decl_context(binary, fn, body):
|
|
syms = set(re.findall(r'\b(?:func_[0-9A-Fa-f]+|D_[0-9A-Fa-f]+)\b', body)) | {fn}
|
|
pat = re.compile(r'\b(' + '|'.join(re.escape(s) for s in syms) + r')\b')
|
|
seen, out = set(), []
|
|
for f in tu_files(binary):
|
|
tag = os.path.basename(f)
|
|
for ln in open(f):
|
|
s = ln.rstrip('\n').strip(' \\\t')
|
|
if not pat.search(s) or 'INCLUDE_ASM' in s:
|
|
continue
|
|
if 'extern' in s or 'DEFINE_func_' in s or re.search(r'\b(?:void|s32|u32|s16|u16|s8|u8|int|char|short|float|struct)\b[^;{]*\(', s):
|
|
key = s[:160]
|
|
if key not in seen:
|
|
seen.add(key); out.append(f'[{tag}] {key}')
|
|
return '\n'.join(out[:140]) or '(no existing decls found — symbols may be undeclared in the TU)'
|
|
|
|
|
|
def build_user(fn, binary, asm, body, decls):
|
|
return f"""FUNCTION {fn} in overlay {binary}. Its body below is ALREADY byte-correct (matches the
|
|
target MIPS), but the overlay translation unit does NOT compile because its declarations conflict with
|
|
declarations already present in the TU (from other banked functions). Produce a version that COMPILES in
|
|
the whole TU and stays byte-for-byte identical, changing ONLY declarations / extern types / call-site casts.
|
|
|
|
=== TARGET MIPS (ground truth) ===
|
|
{asm}
|
|
|
|
=== BYTE-CORRECT BODY (keep its computation; reconcile its declarations) ===
|
|
{body}
|
|
|
|
=== EXISTING TU DECLARATIONS that conflict / are relevant ===
|
|
{decls}
|
|
|
|
=== {TOOLKIT} ===
|
|
|
|
Reason first: name each conflicting declaration and the exact move that resolves it. Then output EXACTLY:
|
|
1) a ```c block — the final self-contained C for {fn}: the definition (computation unchanged) plus the
|
|
externs it needs, with types/prototypes CONSISTENT with the TU decls above (reuse their types).
|
|
2) a ```relax block — zero or more lines, each a func_XXXX whose EXISTING forward-declaration (in
|
|
engine_core.h OR the overlay .c) must be relaxed to K&R no-prototype `()` (only promotion-safe defs).
|
|
"""
|
|
|
|
|
|
def gate_fn(binary):
|
|
entries = [(f'src/{binary}/{binary}.c', f'asm/{binary}/nonmatchings/{binary}')]
|
|
for p in sorted(glob.glob(os.path.join(REPO, f'src/{binary}/{binary}_*.c'))):
|
|
n = os.path.basename(p)[:-2]
|
|
sub = f'asm/{binary}/nonmatchings/{n}'
|
|
if os.path.isdir(os.path.join(REPO, sub)):
|
|
entries.append((f'src/{binary}/{n}.c', sub))
|
|
banked = []
|
|
for src, asm in entries:
|
|
sh([PY, 'tools/harvest_verify.py', '--binary', binary, '--src', src, '--asm-subdir', asm,
|
|
'--out', f'build/{binary}/{binary}', '--good-sha', good_sha(binary),
|
|
'--drafts', os.path.relpath(DRAFTS, REPO), '--chunk', '1',
|
|
'--verified-out', '.run/glm_recon_v.txt', '--failed-out', '.run/glm_recon_f.txt'], timeout=1800)
|
|
vp = os.path.join(REPO, '.run/glm_recon_v.txt')
|
|
banked += [w for w in (open(vp).read().split() if os.path.exists(vp) else []) if w.startswith('func_')]
|
|
return banked
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument('--binary', default='ov_SC01_077')
|
|
ap.add_argument('--funcs', required=True)
|
|
ap.add_argument('--bodies', default='.run/ab_glm')
|
|
a = ap.parse_args()
|
|
if not (API_BASE and MODEL):
|
|
sys.exit('set API_BASE, MODEL, API_KEY')
|
|
os.makedirs(REASON_DIR, exist_ok=True); os.makedirs(DRAFTS, exist_ok=True)
|
|
funcs = [x.strip() for x in a.funcs.split(',') if x.strip()]
|
|
banked_all, relaxed_all = [], []
|
|
|
|
for fn in funcs:
|
|
bpath = os.path.join(REPO, a.bodies, fn + '.c')
|
|
body = open(bpath).read() if os.path.exists(bpath) else '(no prior body — draft it byte-correct from the asm)'
|
|
content, reasoning = call([{'role': 'system', 'content': SYS},
|
|
{'role': 'user', 'content': build_user(fn, a.binary, find_asm(a.binary, fn), body,
|
|
decl_context(a.binary, fn, body))}])
|
|
open(os.path.join(REASON_DIR, fn + '.txt'), 'w').write(
|
|
f'=== {fn} reasoning ({len(reasoning)} chars) ===\n{reasoning}\n\n=== content ===\n{content}\n')
|
|
cblocks = re.findall(r'```c?\s*\n(.*?)```', content, re.S)
|
|
relax_funcs = re.findall(r'func_[0-9A-Fa-f]+', '\n'.join(re.findall(r'```relax\s*\n(.*?)```', content, re.S)))
|
|
if not cblocks:
|
|
print(f' {fn}: no C block returned'); continue
|
|
snap = snapshot(a.binary)
|
|
if relax_funcs:
|
|
relax_in_tu(a.binary, set(relax_funcs))
|
|
for old in glob.glob(os.path.join(DRAFTS, '*.c')):
|
|
os.remove(old)
|
|
open(os.path.join(DRAFTS, fn + '.c'), 'w').write(max(cblocks, key=len))
|
|
if fn in gate_fn(a.binary):
|
|
banked_all.append(fn); relaxed_all += relax_funcs
|
|
print(f' {fn}: BANKED (relax={relax_funcs or "none"})')
|
|
else:
|
|
restore(snap)
|
|
print(f' {fn}: fail (reconciliation did not byte-gate)')
|
|
|
|
print(f'\nglm_reconcile: BANKED {len(banked_all)}/{len(funcs)}: {" ".join(banked_all) or "(none)"}')
|
|
print(f' relaxed: {sorted(set(relaxed_all)) or "(none)"} cost ${COST[0]:.4f} reasoning -> {REASON_DIR}/')
|
|
if banked_all:
|
|
print(' fleet-safety check-all (engine_core.h relaxations are shared)...')
|
|
print(' ' + (sh(['make', 'check-all'], timeout=3600).stdout.strip().splitlines() or ['?'])[-1])
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|