#!/usr/bin/env python3 """auto_driver.py — Phase 16 autonomous struct-harvest driver (LLM-free, set-and-go). Loops the non-giant shared-stub worklist of ov_SC01_077 and, for each function: m2c --valid-syntax -> draft (byte-faithful M2C_FIELD; common.h defines the macro) match_one -> fast relocation-masked prefilter (rank: direct-match / near-miss / structural) decomp-permuter -> (time-boxed) close a near-miss by regalloc/schedule search [optional] harvest_verify -> WHOLE-BINARY byte-gate (the SOLE arbiter, G3/P9) on the batch dedup_propagate -> propagate each banked match fleet-wide (up to 134 overlays) git commit -> checkpoint (explicit pathspec; NEVER git-checkout the overlay, §14c) Runs pass after pass until the worklist is dry OR a safe-exit is requested. The byte-gate guarantees nothing wrong is ever banked, no matter how long it runs unattended. SAFE EXIT (Drew): `touch .run/auto/STOP` (or tools/auto_stop.sh) -> the driver finishes the current function's gate+propagate+commit, writes a final heartbeat, and exits 0 cleanly; the supervisor sees STOP and does not relaunch. Checked at every function/batch boundary. HEARTBEAT: .run/auto/heartbeat.json — {ts, pass, current, banked_total, fleet_pct, last_commit, state}. Usage: python3 tools/auto_driver.py [--batch 40] [--permute-secs 0] [--max-nins 150] [--once] """ import argparse, json, os, re, subprocess, sys, time, glob, shutil, signal sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import p16_permute # reuse the validated permuter setup/run (M2C_FIELD->cast expander, scratch, run) REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) OV = "ov_SC01_077" SRC = f"src/{OV}/{OV}.c" ASM = f"asm/{OV}/nonmatchings/{OV}" GOOD_SHA = "d19c9580a02dc63ba1f0e7e0c770f3b10de35635" OUT = f"build/{OV}/{OV}" AUTODIR = ".run/auto" STOP = f"{AUTODIR}/STOP" HEARTBEAT = f"{AUTODIR}/heartbeat.json" DRAFTS = f"{AUTODIR}/drafts" PY = ".venv/bin/python" INS_RE = re.compile(r"/\* [0-9A-Fa-f]+ [0-9A-Fa-f]{8} [0-9A-Fa-f]{8} \*/") NONFAITHFUL = re.compile(r"M2C_ERROR|M2C_BREAK|MULT_HI|MULTU_HI|\bCLZ\b|M2C_TRAP|GLUE_F64|BSWAP|REVERSE_BITS|ROTATE_RIGHT") def sh(cmd, timeout=None): return subprocess.run(cmd, capture_output=True, text=True, cwd=REPO, timeout=timeout) def log(msg): print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True) def stop_requested(): return os.path.exists(os.path.join(REPO, STOP)) def nins_of(fn): p = os.path.join(REPO, ASM, fn + ".s") if not os.path.exists(p): return 0 return len(INS_RE.findall(open(p, errors="replace").read())) def worklist(max_nins): """Current INCLUDE_ASM stubs in the overlay .c, non-giant (nins<=max_nins), leaf/small-first.""" src = open(os.path.join(REPO, SRC)).read() stubs = re.findall(r"INCLUDE_ASM\([^,]+,\s*(\w+)\)", src) sized = [(fn, nins_of(fn)) for fn in stubs] sized = [(fn, n) for fn, n in sized if 0 < n <= max_nins] sized.sort(key=lambda x: x[1]) # smallest first (highest yield, cheapest gate) return [fn for fn, n in sized] def m2c_draft(fn): s = os.path.join(REPO, ASM, fn + ".s") if not os.path.exists(s): return None try: out = sh([PY, "tools/m2c/m2c.py", "-t", "mipsel-gcc-c", "--valid-syntax", "-f", fn, s], timeout=90).stdout except subprocess.TimeoutExpired: return None if not out.strip() or "OSError" in out or NONFAITHFUL.search(out): return None # untyped/GTE/special -> defer return out def match_one_score(fn, cpath): """Return ('match',0) | ('near', n_mismatch) | ('fail', -1).""" r = sh([PY, "tools/match_one.py", fn, "--c", cpath, "--asm-subdir", ASM], timeout=120) first = (r.stdout.strip().splitlines() or ["?"])[0] if first.startswith("MATCH"): return ("match", 0) m = re.search(r"(\d+) mismatched", first) if m: return ("near", int(m.group(1))) return ("fail", -1) def write_heartbeat(state, pass_n, current, banked, fleet_pct, last_commit): os.makedirs(os.path.join(REPO, AUTODIR), exist_ok=True) json.dump({"ts": time.strftime("%Y-%m-%d %H:%M:%S"), "pass": pass_n, "current": current, "banked_total": banked, "fleet_pct": fleet_pct, "last_commit": last_commit, "state": state}, open(os.path.join(REPO, HEARTBEAT), "w"), indent=1) def fleet_pct(): try: r = sh([PY, "tools/progress.py", "--fleet"], timeout=120) m = re.search(r"byte-identical\s+:\s+\d+\s*/\s*\d+\s*=\s*([\d.]+)%", r.stdout) return float(m.group(1)) if m else None except Exception: return None def git_commit(msg): sh(["git", "add", SRC, "src/shared/engine_core.h", "config/dedup.us.yaml"]) sh(["git", "commit", "-q", "-m", msg]) return sh(["git", "rev-parse", "--short", "HEAD"]).stdout.strip() def sig_unify(draft_dir): """Canonicalize each draft's callee externs + own def-signature to the banked set. ESSENTIAL (Phase-15 §14b/§14c + Phase-16 finding): without this the whole-binary gate fails on extern/data type conflicts even when match_one (relocation-masked) says MATCH.""" uni = draft_dir + "-uni" r = sh([PY, "tools/sig_unify.py", "--overlay", OV, "--in", draft_dir, "--out", uni], timeout=600) return uni if os.path.isdir(os.path.join(REPO, uni)) else draft_dir def byte_gate(draft_dir): """sig_unify then harvest_verify the drafts; returns list of verified fn names (the SOLE arbiter).""" gated = sig_unify(draft_dir) r = sh([PY, "tools/harvest_verify.py", "--binary", OV, "--src", SRC, "--asm-subdir", ASM, "--out", OUT, "--good-sha", GOOD_SHA, "--drafts", gated, "--chunk", "8"], timeout=7200) m = re.search(r"VERIFIED:\s*(.*)", r.stdout) if not m or "(none)" in m.group(1): return [] return [w for w in m.group(1).split() if w.startswith("func_")] def propagate(): sh([PY, "tools/dedup_propagate.py", "--auto-from", OV, "--min-reach", "2"], timeout=3600) def main(): ap = argparse.ArgumentParser() ap.add_argument("--batch", type=int, default=40, help="functions per gate batch") ap.add_argument("--permute-secs", type=int, default=0, help="permuter budget per near-miss (0=skip)") ap.add_argument("--permute-j", type=int, default=8, help="permuter -j (RAM-bound on the 15GiB box)") ap.add_argument("--max-nins", type=int, default=150, help="exclude giants (>this)") ap.add_argument("--once", action="store_true", help="one pass then exit (for trials)") a = ap.parse_args() os.chdir(REPO) os.makedirs(AUTODIR, exist_ok=True) if stop_requested(): log("STOP sentinel present at startup; remove it to run. Exiting.") return banked = 0 last_commit = sh(["git", "rev-parse", "--short", "HEAD"]).stdout.strip() pass_n = 0 log(f"auto_driver start (batch={a.batch}, permute={a.permute_secs}s, max_nins={a.max_nins})") while True: if stop_requested(): log("STOP requested — exiting cleanly."); write_heartbeat("stopped", pass_n, None, banked, fleet_pct(), last_commit); return pass_n += 1 wl = worklist(a.max_nins) log(f"pass {pass_n}: {len(wl)} non-giant shared stubs remain") if not wl: log("worklist DRY — all gettable non-giant work done."); write_heartbeat("dry", pass_n, None, banked, fleet_pct(), last_commit); return pass_banked = 0 for i in range(0, len(wl), a.batch): if stop_requested(): log("STOP requested at batch boundary — exiting cleanly."); write_heartbeat("stopped", pass_n, None, banked, fleet_pct(), last_commit); return batch = wl[i:i + a.batch] # build candidate drafts for this batch if os.path.exists(os.path.join(REPO, DRAFTS)): shutil.rmtree(os.path.join(REPO, DRAFTS)) os.makedirs(os.path.join(REPO, DRAFTS), exist_ok=True) cand = 0 for fn in batch: write_heartbeat("drafting", pass_n, fn, banked, None, last_commit) draft = m2c_draft(fn) if draft is None: continue cpath = os.path.join(REPO, DRAFTS, fn + ".c") open(cpath, "w").write(draft) # prefilter; permuter (time-boxed) on near-misses — the key yield lever kind, score = match_one_score(fn, cpath) if kind == "fail": os.remove(cpath); continue # won't compile standalone -> drop (gate would revert anyway) if kind == "near" and a.permute_secs > 0: write_heartbeat("permuting", pass_n, fn, banked, None, last_commit) pd = p16_permute.setup(fn, draft) if pd: win = p16_permute.run_permuter(pd, a.permute_secs, a.permute_j) if win: # draft = externs + permuted body (common.h provides scalars/macros in the TU) open(cpath, "w").write(p16_permute.winner_to_draft(open(win).read())) cand += 1 if cand == 0: continue write_heartbeat("gating", pass_n, f"batch@{i}", banked, None, last_commit) verified = byte_gate(DRAFTS) if verified: propagate() banked += len(verified); pass_banked += len(verified) fp = fleet_pct() last_commit = git_commit( f"feat(phase-16): auto-harvest pass {pass_n} batch@{i} — +{len(verified)} fns (fleet {fp}%)") log(f" batch@{i}: banked {len(verified)} (total {banked}); fleet {fp}%; commit {last_commit}") write_heartbeat("running", pass_n, f"batch@{i}", banked, fp, last_commit) log(f"pass {pass_n} complete: banked {pass_banked}") if a.once or pass_banked == 0: log("once/no-progress -> exiting."); write_heartbeat("done", pass_n, None, banked, fleet_pct(), last_commit); return if __name__ == "__main__": main()