Research-Stack/4-Infrastructure/shim/alphaproof_loop.py
Devin AI 0c9efac330 chore(consolidation): integrate E8Sidon stack (PRs #79 #80 #81 #89) into one PR
Squash the four overlapping feature branches into a single change set against
main, eliminating cross-PR merge conflicts and the duplicated CI-fix scripts.

What this brings in (merge order #79 -> #80 -> #81 -> #89):
- #79 refactor(infra): shared utilities (4-Infrastructure/lib/*: q16, hashing,
  jsonl, fraction_utils) + the scripts/math-first/* validators that the
  math-check CI requires.
- #80 feat(lean): Semantics.E8Sidon (1025 lines) -- Eisenstein coefficient
  identity E4^2 = E8 and the Sidon framework. E4_sq_eq_E8_coeff is fully proved
  (all Fourier-coefficient extraction machine-checked); the single residual gap
  is pinned to E4_sq_eq_E8_qExpansion (Mathlib lacks the valence formula /
  dim M8 = 1). 4 sorries + 1 axiom (e8_additive_completeness), all TODO(lean-port).
- #81 refactor(lean): Float-free FixedPoint core (integer-only sqrt/log2/expNeg).
  E8Sidon.lean kept at #80's final 1025-line version (the #81 intermediate
  438-line copy was overridden by merge order).
- #89 feat(lean): Semantics.RRC.PolyFactorIdentity -- short-sleeve polynomial
  detection at the zerocopy limb boundary; now imports Semantics.E8Sidon for
  sigma3/sigma7/convolutionLHS (single source of truth) instead of inlining them.

Conflict resolution:
- flake.nix -> canonical rs-surface removal (Garnix shutdown).
- scripts/math-first/* -> byte-identical across branches, clean.
- .cursorrules / AGENTS.md -> unified; baselines + sorry inventory refreshed.

Verification:
- lake build (default aggregator): 3573 jobs, 0 errors.
- lake build Semantics.RRC.PolyFactorIdentity (E8Sidon + FixedPoint + PolyFactor):
  3655 jobs, 0 errors. Witnesses verified (sigma7 4 = 16513, convolutionLHS 6 = 2350).
- Python tests: 68/68 pass.

Note: the "Workers Builds: researchstack" check is a preexisting external
Cloudflare build unrelated to this change (no branch touches 4-Infrastructure/cloudflare/).

Build: 3573 jobs (default), 3655 jobs (narrow), 0 errors
Co-Authored-By: Allaun Silverfox <bigdataiscoming+9i37y6j2@protonmail.com>
2026-06-16 02:01:31 +00:00

547 lines
19 KiB
Python

"""
AlphaProof-inspired proof search loop.
Generates Lean proof candidates via a local LLM (Ollama), verifies them
through lake build, and iterates with error feedback until a valid proof
is found or max iterations exhausted.
Logs all iterations to JSONL for replay analysis.
"""
import json
import os
import re
import subprocess
import sys
import tempfile
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional, List, Dict
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "4-Infrastructure"))
from lib.q16 import Q16_SCALE, q16_mul, to_q16
try:
import requests
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
# Import pre-filter for copy-if pattern
try:
from lean_proof_prefilter import analyze_file, classify_theorem, extract_theorems
HAS_PREFILTER = True
except ImportError:
HAS_PREFILTER = False
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
DEFAULT_MODEL = os.environ.get("ALPHAPROOF_MODEL", "deepseek-coder-v2:16b")
LOG_DIR = Path(__file__).parent / "alphaproof_logs"
# ---------------------------------------------------------------------------
# Ollama LLM interface
# ---------------------------------------------------------------------------
def generate_candidate(prompt: str, model: str = DEFAULT_MODEL,
temperature: float = 0.7) -> str:
"""Call Ollama API to generate a candidate Lean proof.
Args:
prompt: The problem description + error feedback.
model: Ollama model name.
temperature: Sampling temperature.
Returns:
Generated Lean code string.
"""
if not HAS_REQUESTS:
raise ImportError("requests library required. pip install requests")
system_prompt = (
"You are a Lean 4 theorem prover. Given a mathematical problem, "
"generate a complete, compilable Lean 4 module with all necessary "
"imports. Use `import Mathlib` for standard library. "
"Output ONLY the Lean code, no markdown fences or explanations. "
"The module must compile with `lake build`."
)
payload = {
"model": model,
"system": system_prompt,
"prompt": prompt,
"stream": False,
"options": {
"temperature": temperature,
"num_predict": 4096,
}
}
try:
resp = requests.post(f"{OLLAMA_URL}/api/generate", json=payload,
timeout=120)
resp.raise_for_status()
result = resp.json()
code = result.get("response", "")
# Strip markdown fences if present
code = re.sub(r'^```(?:lean|lean4)?\s*\n?', '', code, flags=re.MULTILINE)
code = re.sub(r'\n?```\s*$', '', code, flags=re.MULTILINE)
return code.strip()
except requests.RequestException as e:
return f"-- ERROR: Ollama request failed: {e}"
# ---------------------------------------------------------------------------
# Lean verification
# ---------------------------------------------------------------------------
def verify_proof(lean_code: str, module_name: str = "Candidate",
lake_project: Optional[str] = None,
timeout: int = 120) -> dict:
"""Write Lean code to a temp file and verify via lake build.
Args:
lean_code: Complete Lean 4 module source.
module_name: Name for the temp module file.
lake_project: Path to existing lake project. If None, creates a temp one.
timeout: Build timeout in seconds.
Returns:
{'success': bool, 'errors': list[str], 'warnings': list[str]}
"""
errors = []
warnings = []
with tempfile.TemporaryDirectory(prefix="alphaproof_") as tmpdir:
tmpdir = Path(tmpdir)
if lake_project is None:
# Create a minimal lake project
lakefile = tmpdir / "lakefile.lean"
lakefile.write_text(
'import Lake\n'
'open Lake DSL in\n'
'package «candidate» where\n'
' leanOptions := #[\n'
' ⟨`pp.unicode.fun, true⟩\n'
' ]\n\n'
'require mathlib from git\n'
' "https://github.com/leanprover-community/mathlib4" @ "master"\n'
)
project_dir = tmpdir
else:
project_dir = Path(lake_project)
# Write candidate file
candidate_dir = project_dir / "Candidate"
candidate_dir.mkdir(exist_ok=True)
candidate_file = candidate_dir / f"{module_name}.lean"
candidate_file.write_text(lean_code)
# Run lake build
try:
result = subprocess.run(
["lake", "build", f"Candidate.{module_name}"],
cwd=str(project_dir),
capture_output=True,
text=True,
timeout=timeout,
env={**os.environ, "LAKE_PATH": str(project_dir)}
)
if result.returncode == 0:
return {'success': True, 'errors': [], 'warnings': []}
# Parse errors from stderr
stderr = result.stderr
error_lines = []
for line in stderr.split('\n'):
line = line.strip()
if not line:
continue
if 'error' in line.lower():
error_lines.append(line)
elif 'warning' in line.lower():
warnings.append(line)
elif line.startswith('error:') or 'Error' in line:
error_lines.append(line)
if not error_lines and stderr:
error_lines = [stderr[:500]]
return {
'success': False,
'errors': error_lines,
'warnings': warnings
}
except subprocess.TimeoutExpired:
return {
'success': False,
'errors': [f'Build timed out after {timeout}s'],
'warnings': []
}
except FileNotFoundError:
return {
'success': False,
'errors': ['lake command not found. Is Lean/Lake installed?'],
'warnings': []
}
# ---------------------------------------------------------------------------
# FPGA Q16 acceleration (Python placeholder)
# ---------------------------------------------------------------------------
def q16_to_float(q: int) -> float:
"""Convert Q16.16 to float."""
return q / Q16_SCALE
def fpga_accelerate(candidates: list[dict]) -> list[dict]:
"""Rank candidates using Q16.16 fixed-point scoring.
This is a Python placeholder for FPGA Q16 LUT acceleration.
In production, this would route through UART to the Tang Nano 9K
or direct memory-mapped LUT access.
Scoring heuristic:
- Shorter proofs score higher (fewer tokens)
- Proofs with fewer sorry/axiom/admit score higher
- Previous success patterns boost score
Args:
candidates: list of {'code': str, 'iteration': int, ...}
Returns:
Candidates sorted by Q16 score (descending), with 'q16_score' added.
"""
scored = []
for c in candidates:
code = c.get('code', '')
# Length penalty (shorter is better)
length_score = to_q16(1.0 / (1.0 + len(code) / 1000.0))
# Penalty for incomplete proofs
penalty = 0
for bad_word in ['sorry', 'admit', 'axiom', 'by omega']:
count = code.count(bad_word)
penalty += to_q16(count * 0.1)
# Base score from length
base = length_score
# Bonus for having structure (def, theorem, proof)
if 'theorem' in code or 'lemma' in code:
base = q16_mul(base, to_q16(1.2))
final_score = max(0, base - penalty)
c['q16_score'] = final_score
c['q16_score_float'] = q16_to_float(final_score)
scored.append(c)
scored.sort(key=lambda x: x.get('q16_score', 0), reverse=True)
return scored
# ---------------------------------------------------------------------------
# Proof prioritization (copy-if pre-filter)
# ---------------------------------------------------------------------------
def prioritize_problems(lean_dir: str, max_problems: int = 20) -> List[Dict]:
"""Pre-filter and prioritize proof problems using the copy-if pattern.
Scans the Lean codebase, classifies theorems as trivial/non-trivial,
and returns the highest-priority non-trivial problems for the LLM.
This is the Lean equivalent of the vectorized copy_if:
- Trivial theorems (zero deltas) = skip
- Non-trivial theorems (non-zero deltas) = feed to solver
Args:
lean_dir: Path to Lean source directory.
max_problems: Maximum number of problems to return.
Returns:
List of prioritized problem dicts with file, theorem, and priority.
"""
if not HAS_PREFILTER:
print("[alphaproof] pre-filter not available, using default order")
return []
problems = []
lean_path = Path(lean_dir)
for lean_file in sorted(lean_path.rglob('*.lean')):
if '.lake' in str(lean_file):
continue
try:
analysis = analyze_file(str(lean_file))
for thm in analysis['theorems']:
if thm['classification'] == 'non_trivial':
# Priority: more tactics = harder = higher priority
tactic_count = len(thm.get('category', '').split(','))
has_sorry = 'sorry' in thm.get('category', '')
priority = tactic_count + (10 if has_sorry else 0)
problems.append({
'file': str(lean_file),
'theorem': thm['name'],
'kind': thm['kind'],
'line': thm['line'],
'category': thm['category'],
'priority': priority,
'block': thm['block'],
})
except Exception:
continue
# Sort by priority (highest first = hardest problems first)
problems.sort(key=lambda p: p['priority'], reverse=True)
# Deduplicate by theorem name
seen = set()
unique = []
for p in problems:
if p['theorem'] not in seen:
seen.add(p['theorem'])
unique.append(p)
return unique[:max_problems]
def batch_proof_search(lean_dir: str, max_iterations: int = 10,
model: str = DEFAULT_MODEL,
lake_project: Optional[str] = None) -> Dict:
"""Run AlphaProof on prioritized non-trivial theorems.
Uses the copy-if pre-filter to skip trivial theorems and focus
the LLM solver on the hardest problems first.
"""
problems = prioritize_problems(lean_dir)
if not problems:
print("[alphaproof] no non-trivial problems found")
return {'problems': [], 'results': []}
print(f"[alphaproof] found {len(problems)} non-trivial problems")
print(f"[alphaproof] top 5: {[p['theorem'] for p in problems[:5]]}")
results = []
for i, problem in enumerate(problems):
print(f"\n[alphaproof] [{i+1}/{len(problems)}] {problem['theorem']}"
f" (priority={problem['priority']}, category={problem['category']})")
# Build prompt from the theorem block
prompt = (
f"Prove this Lean 4 theorem. The file is {problem['file']}, "
f"line {problem['line']}.\n\n"
f"```lean\n{problem['block']}\n```\n\n"
f"Replace the sorry with a complete proof. "
f"Use omega, simp, decide, norm_num, ring, or linarith as appropriate."
)
result = search_loop(
problem=prompt,
max_iterations=max_iterations,
model=model,
lake_project=lake_project,
)
results.append({
'theorem': problem['theorem'],
'file': problem['file'],
'priority': problem['priority'],
'success': result['success'],
'iterations': result['iterations'],
'solution': result.get('solution', ''),
})
if result['success']:
print(f"[alphaproof] SOLVED {problem['theorem']} "
f"in {result['iterations']} iterations")
else:
print(f"[alphaproof] FAILED {problem['theorem']} "
f"after {result['iterations']} iterations")
# Summary
solved = sum(1 for r in results if r['success'])
print(f"\n[alphaproof] summary: {solved}/{len(results)} solved")
return {
'problems': problems,
'results': results,
'solved': solved,
'total': len(results),
}
# Main search loop
# ---------------------------------------------------------------------------
def search_loop(problem: str, max_iterations: int = 50,
model: str = DEFAULT_MODEL,
lake_project: Optional[str] = None,
temperature: float = 0.7,
build_timeout: int = 120) -> dict:
"""Main AlphaProof search loop: generate → verify → feedback → retry.
Args:
problem: Natural language description of the problem to prove.
max_iterations: Maximum number of generate-verify cycles.
model: Ollama model to use.
lake_project: Optional path to existing lake project.
temperature: LLM temperature.
build_timeout: Lean build timeout per iteration.
Returns:
{'solution': str, 'iterations': int, 'success': bool,
'candidates': list[dict], 'log_path': str}
"""
LOG_DIR.mkdir(parents=True, exist_ok=True)
log_path = LOG_DIR / f"search_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}.jsonl"
candidates = []
best_solution = ""
feedback = ""
with open(log_path, 'w') as log_f:
for iteration in range(1, max_iterations + 1):
t0 = time.time()
# Build prompt with feedback from previous iterations
if feedback:
prompt = (
f"Problem:\n{problem}\n\n"
f"Previous attempt failed with these errors:\n{feedback}\n\n"
f"Please fix the Lean code and try again. "
f"Iteration {iteration}/{max_iterations}."
)
else:
prompt = f"Problem:\n{problem}\n\nPlease write a Lean 4 proof."
# Generate candidate
code = generate_candidate(prompt, model=model,
temperature=temperature)
gen_time = time.time() - t0
# Verify
t1 = time.time()
result = verify_proof(code, module_name=f"Iter{iteration}",
lake_project=lake_project,
timeout=build_timeout)
ver_time = time.time() - t1
candidate = {
'iteration': iteration,
'code': code,
'success': result['success'],
'errors': result['errors'],
'warnings': result['warnings'],
'gen_time': round(gen_time, 2),
'verify_time': round(ver_time, 2),
'timestamp': datetime.now(timezone.utc).isoformat(),
}
candidates.append(candidate)
# Log to JSONL
log_entry = {**candidate, 'code_length': len(code)}
log_f.write(json.dumps(log_entry) + '\n')
log_f.flush()
if result['success']:
best_solution = code
# Rank remaining candidates via FPGA Q16 path
candidates = fpga_accelerate(candidates)
return {
'solution': best_solution,
'iterations': iteration,
'success': True,
'candidates': candidates,
'log_path': str(log_path),
}
# Build feedback for next iteration
feedback = '\n'.join(result['errors'][:5]) # Top 5 errors
print(f"[alphaproof] iter {iteration}/{max_iterations}: "
f"FAIL ({len(result['errors'])} errors, "
f"gen={gen_time:.1f}s, ver={ver_time:.1f}s)")
# Exhausted iterations
candidates = fpga_accelerate(candidates)
return {
'solution': best_solution,
'iterations': max_iterations,
'success': False,
'candidates': candidates,
'log_path': str(log_path),
}
# ---------------------------------------------------------------------------
# CLI entry point
# ---------------------------------------------------------------------------
if __name__ == '__main__':
import sys
if len(sys.argv) < 2:
print("Usage: python alphaproof_loop.py <problem_file_or_text> "
"[--max-iter N] [--model MODEL]")
print(" python alphaproof_loop.py --batch <lean_dir> "
"[--max-iter N] [--model MODEL]")
sys.exit(1)
problem_arg = sys.argv[1]
# Batch mode: run pre-filter on Lean codebase
if problem_arg == '--batch':
lean_dir = sys.argv[2] if len(sys.argv) > 2 else '.'
max_iter = 10
model_name = DEFAULT_MODEL
for i, arg in enumerate(sys.argv[3:], 3):
if arg == '--max-iter' and i + 1 < len(sys.argv):
max_iter = int(sys.argv[i + 1])
elif arg == '--model' and i + 1 < len(sys.argv):
model_name = sys.argv[i + 1]
result = batch_proof_search(lean_dir, max_iterations=max_iter,
model=model_name)
print(f"\n{'='*60}")
print(f"Solved: {result['solved']}/{result['total']}")
print(f"{'='*60}")
sys.exit(0 if result['solved'] > 0 else 1)
# Single problem mode
if os.path.isfile(problem_arg):
problem_text = Path(problem_arg).read_text()
else:
problem_text = problem_arg
max_iter = 50
model_name = DEFAULT_MODEL
for i, arg in enumerate(sys.argv[2:], 2):
if arg == '--max-iter' and i + 1 < len(sys.argv):
max_iter = int(sys.argv[i + 1])
elif arg == '--model' and i + 1 < len(sys.argv):
model_name = sys.argv[i + 1]
result = search_loop(problem_text, max_iterations=max_iter,
model=model_name)
print(f"\n{'='*60}")
print(f"Success: {result['success']}")
print(f"Iterations: {result['iterations']}")
print(f"Log: {result['log_path']}")
if result['success']:
print(f"\nSolution:\n{result['solution']}")