#!/usr/bin/env python3 """ prove.py — Lean proof-filling loop with 5-check verification pipeline. Adapted from Peng et al. (2026) pipeline-math verify.sh: 1. SHA pin check — frozen theorem stubs haven't drifted 2. Banned keywords — no sorry/native_decide/admit/axiom in proof files 3. lake build clean — 0 errors, 0 unexpected warnings 4. #print axioms — proof depends only on {propext, Classical.choice, Quot.sound} 5. Discharge gate — @Frozen = @Proof := rfl (proof matches theorem type) Metric: build_errors * 1000 + sorries (lower = better, 0 = perfect). """ import os, sys, time, json, subprocess, re, hashlib, shutil from pathlib import Path import urllib.request # ── Config ────────────────────────────────────────────────────────────── SILVERSIGHT_DIR = os.path.expanduser("~/SilverSight") LEAN_FILE = os.environ.get("TARGET_FILE", "formal/CoreFormalism/Bind.lean") LLM_MODEL = os.environ.get("LLM_MODEL", "phi4:14b") LLM_BASE_URL = os.environ.get("LLM_BASE_URL", "http://100.92.88.64:11434/v1") LLM_API_KEY = os.environ.get("LLM_API_KEY", "sk-local") TIME_BUDGET_S = int(os.environ.get("TIME_BUDGET_S", "600")) BANNED_KEYWORDS = ["sorryAx", "native_decide", "admit", "unsafe", "implemented_by", "ofReduceBool"] ALLOWED_AXIOMS = {"propext", "Classical.choice", "Quot.sound"} # ── LLM client ───────────────────────────────────────────────────────── def llm_complete(prompt, system="You are a Lean 4 proof engineer."): body = json.dumps({ "model": LLM_MODEL, "messages": [ {"role": "system", "content": system}, {"role": "user", "content": prompt}, ], "temperature": 0.3, "max_tokens": 4096, }).encode() req = urllib.request.Request( f"{LLM_BASE_URL}/chat/completions", data=body, headers={"Content-Type": "application/json", "Authorization": f"Bearer {LLM_API_KEY}"}, method="POST", ) try: resp = urllib.request.urlopen(req, timeout=120) data = json.loads(resp.read()) return data["choices"][0]["message"]["content"] except Exception as e: print(f"LLM error: {e}") return None # ─── Check 1: SHA pins ──────────────────────────────────────────────── def check_sha_pins(lean_file): """Check SHA-256 of file against pinned hash in scripts/frozen.sha256.""" pins_path = Path(SILVERSIGHT_DIR) / "scripts" / "frozen.sha256" if not pins_path.exists(): return True, "no pins file (SKIP)" rel_path = os.path.relpath( os.path.join(SILVERSIGHT_DIR, lean_file), SILVERSIGHT_DIR ) content = pins_path.read_text() for line in content.splitlines(): line = line.strip() if not line or line.startswith("#"): continue parts = line.split(None, 1) if len(parts) == 2 and parts[1] == rel_path: pinned = parts[0] actual = sha256_of(os.path.join(SILVERSIGHT_DIR, lean_file)) if pinned == actual: return True, f"SHA pin match: {rel_path}" else: return False, f"SHA pin MISMATCH {rel_path}: pinned={pinned[:16]} actual={actual[:16]}" return True, "no pin for this file (SKIP)" def sha256_of(path): h = hashlib.sha256() with open(path, "rb") as f: h.update(f.read()) return h.hexdigest() # ─── Check 2: Banned keywords ───────────────────────────────────────── def check_banned_keywords(text, allow_sorry=False): """Check for banned keywords in Lean code. Returns (pass, message).""" cleaned = strip_comments(text) for kw in BANNED_KEYWORDS: if re.search(r'\b' + re.escape(kw) + r'\b', cleaned): return False, f"banned keyword: {kw}" if not allow_sorry: if re.search(r'\bsorry\b', cleaned): return False, "banned keyword: sorry (not allowed in proof files)" return True, "no banned keywords" def strip_comments(s): """Strip Lean comments (-- line, /- ... -/ block).""" out = [] i, n = 0, len(s) depth = 0 while i < n: two = s[i:i+2] if depth == 0 and two == "--": j = s.find("\n", i) i = j if j != -1 else n elif two == "/-": depth += 1 i += 2 elif depth > 0 and two == "-/": depth -= 1 i += 2 elif depth > 0: i += 1 else: out.append(s[i]) i += 1 return "".join(out) # ─── Check 3: lake build ────────────────────────────────────────────── def lake_build(target=None, timeout=120): """Run `lake build` and return (error_count, output).""" cmd = ["lake", "build"] if target: cmd.append(target) try: result = subprocess.run( cmd, cwd=SILVERSIGHT_DIR, capture_output=True, text=True, timeout=timeout, ) output = result.stdout + result.stderr errors = output.count("error:") warnings = output.count("warning:") - output.count("declaration uses .sorry.") return errors, warnings, output except subprocess.TimeoutExpired: return 999, 0, "TIMEOUT" except Exception as e: return 999, 0, str(e) # ─── Check 4: #print axioms ─────────────────────────────────────────── def check_axioms(module_name): """Check that a module's axioms are within the allowed set.""" ax_file = os.path.join(SILVERSIGHT_DIR, ".lake", "tmp_ax_check.lean") os.makedirs(os.path.dirname(ax_file), exist_ok=True) with open(ax_file, "w") as f: f.write(f"import {module_name}\n") f.write(f"#print axioms {module_name}\n") try: result = subprocess.run( ["lake", "env", "lean", ax_file], cwd=SILVERSIGHT_DIR, capture_output=True, text=True, timeout=60, ) output = result.stdout + result.stderr os.remove(ax_file) # Parse axiom line m = re.search(r"'([^']+)' depends on axioms:\s*\[([^\]]+)\]", output) if m: axioms = set(a.strip() for a in m.group(2).split(",") if a.strip()) bad = axioms - ALLOWED_AXIOMS if bad: return False, f"unexpected axioms: {bad}" return True, f"axioms OK: {axioms}" return True, "no axiom line (module might be empty)" except Exception as e: return False, f"axiom check error: {e}" # ─── Check 5: Discharge gate ────────────────────────────────────────── def check_discharge_gate(lean_file): """Check that Discharge.lean compiles — proof ↔ theorem type gate.""" discharge_path = os.path.join( os.path.dirname(os.path.join(SILVERSIGHT_DIR, lean_file)), "Discharge.lean" ) if not os.path.exists(discharge_path): return True, "no Discharge.lean (SKIP)" discharge_module = "Discharge" err, warn, out = lake_build(target=discharge_module, timeout=60) if err == 0: return True, "Discharge gate PASS" return False, f"Discharge gate FAIL ({err} errors)" # ── Proof filling ────────────────────────────────────────────────────── def count_sorries(text): cleaned = strip_comments(text) return len(re.findall(r'\bsorry\b', cleaned)) def get_sorry_context(filepath): path = Path(os.path.join(SILVERSIGHT_DIR, filepath)) if not path.exists(): return None, None, 0, "file not found" text = path.read_text() n = count_sorries(text) if n == 0: return text, None, 0, "no sorries found" # Find the first theorem/lemma that contains a sorry blocks = list(re.finditer( r'(theorem\s+\w+|lemma\s+\w+|def\s+\w+)[\s\S]*?(?=\b(theorem|lemma|def)\b|\Z)', text )) for m in blocks: block = m.group(0) if 'sorry' in block: return text, block, n, None return text, None, n, "could not find sorry block" def fill_sorry(filepath, proof_text): path = Path(os.path.join(SILVERSIGHT_DIR, filepath)) text = path.read_text() new_text = text.replace("sorry", proof_text.strip(), 1) path.write_text(new_text) return new_text def restore_file(filepath, original): path = Path(os.path.join(SILVERSIGHT_DIR, filepath)) path.write_text(original) # ── Main experiment loop ──────────────────────────────────────────────── def experiment(): print("=" * 60) print(" AutoResearch: Lean Proof Filling (5-check pipeline)") print(f" Model: {LLM_MODEL}") print(f" File: {LEAN_FILE}") print(f" Budget: {TIME_BUDGET_S}s") print("=" * 60) t_start = time.time() t_end = t_start + TIME_BUDGET_S results = [] best_score = 999999 initial_sorries = count_sorries(Path(os.path.join(SILVERSIGHT_DIR, LEAN_FILE)).read_text()) print(f" Initial sorries: {initial_sorries}") print() while time.time() < t_end: iteration = len(results) + 1 t_iter = time.time() text, context, n, err = get_sorry_context(LEAN_FILE) if err: current = count_sorries(Path(os.path.join(SILVERSIGHT_DIR, LEAN_FILE)).read_text()) if current == 0: print(f"\n*** All {initial_sorries} sorries filled! ***") break print(f"\n[{iteration}] {err}") break # Ask LLM to fill the sorry sys_prompt = "You are a Lean 4 proof engineer. Write complete, compilable proofs." prompt = f"""Fill the `sorry` in this Lean 4 theorem. Write ONLY the proof body — the text that replaces `sorry` after `:= by`. Do NOT repeat the theorem header. Use `by` block syntax, not `begin`/`end`. Output raw Lean code only, no markdown. {context} Proof body to replace `sorry`:""" proof = llm_complete(prompt, sys_prompt) if not proof: print(f" [{iteration}] LLM returned nothing, skipping") results.append({"iteration": iteration, "error": "no response"}) continue # Post-process LLM output proof = proof.strip() if proof.startswith("```"): proof = proof.split("```")[1] if "```" in proof[3:] else proof if proof.startswith("lean"): proof = proof[4:] proof = proof.strip().rstrip("`").strip() if proof.startswith("theorem") or proof.startswith("lemma") or proof.startswith("def"): lines = proof.split("\n") for i, line in enumerate(lines): if line.strip().startswith(":= by"): proof = "\n".join(lines[i+1:]) break else: proof = lines[-1] # Apply proof original = Path(os.path.join(SILVERSIGHT_DIR, LEAN_FILE)).read_text() fill_sorry(LEAN_FILE, proof) # ── 5-check pipeline ──────────────────────────────────────── checks_passed = True new_text = Path(os.path.join(SILVERSIGHT_DIR, LEAN_FILE)).read_text() # Check 1: SHA pins pin_ok, pin_msg = check_sha_pins(LEAN_FILE) if not pin_ok: print(f" [{iteration}] CHECK 1 FAIL: {pin_msg}") checks_passed = False # Check 2: Banned keywords kw_ok, kw_msg = check_banned_keywords(new_text, allow_sorry=False) if not kw_ok: print(f" [{iteration}] CHECK 2 FAIL: {kw_msg}") checks_passed = False # Check 3: lake build build_errors, build_warnings, build_out = lake_build(timeout=120) current_sorries = count_sorries(new_text) elapsed = time.time() - t_iter score = build_errors * 1000 + current_sorries status = "KEEP" if checks_passed and score < best_score else "DISCARD" if status == "KEEP": # Check 4: #print axioms (only if build passed) if build_errors == 0: module_name = LEAN_FILE.replace("/", ".").replace(".lean", "") ax_ok, ax_msg = check_axioms(module_name) if not ax_ok: print(f" [{iteration}] CHECK 4 FAIL: {ax_msg}") checks_passed = False status = "DISCARD" # Check 5: Discharge gate if checks_passed: dg_ok, dg_msg = check_discharge_gate(LEAN_FILE) if not dg_ok: print(f" [{iteration}] CHECK 5 FAIL: {dg_msg}") checks_passed = False status = "DISCARD" if status == "KEEP": best_score = score subprocess.run( ["git", "add", "-f", LEAN_FILE], cwd=SILVERSIGHT_DIR, capture_output=True ) subprocess.run( ["git", "commit", "-m", f"autoresearch: {build_errors} err/{current_sorries} sorries (iter {iteration})"], cwd=SILVERSIGHT_DIR, capture_output=True ) if status == "DISCARD": restore_file(LEAN_FILE, original) checks_str = "" if not checks_passed: checks_str = " CHECKS FAILED" print(f" [{iteration:3d}] build_err={build_errors} sorries={current_sorries:3d}" f" elapsed={elapsed:.1f}s [{status}]{checks_str}") results.append({ "iteration": iteration, "sorries": current_sorries, "build_errors": build_errors, "status": status, "checks_passed": checks_passed, "elapsed_s": round(elapsed, 1), }) if current_sorries == 0 and build_errors == 0: print(f"\n*** ALL PROOFS COMPLETE! ***") break total = time.time() - t_start print(f"\n{'='*60}") print(f" Summary: {len(results)} iterations, {total:.0f}s") print(f" Best score (errors*1000+sorries): {best_score}") print(f" Model: {LLM_MODEL}") print(f"{'='*60}") if __name__ == "__main__": experiment()