Record and gate proofs, so "proven" means Lean verified it
The index had no proof field and every entry was an axiom, so the database asserted its premises rather than proving them. Adds the proof half, with the verification gate that makes the status trustworthy. - Database records now carry `proof` and `proof_checked`, and the search index exposes the proof, with status one of proved / proof_pending / asserted. build_database.py emits a real `theorem` only when a proof has been verified (by check_proofs.py, or by a lean-bench run via --results); a recorded but unverified proof stays an `axiom` and is reported as proof_pending, so the library never claims more than Lean has confirmed. A test pins that gating in both directions. - textbook/check_proofs.py verifies proofs by compiling `theorem ... := by <proof>`. Stricter than check_statements.py in three ways: it rejects sorry/admit/native_decide outright, and it reads `#print axioms` to reject any proof depending on more than Lean's own three axioms (propext, Classical.choice, Quot.sound) -- which in particular catches a proof leaning on one of this database's own book-asserted axioms, since that is not an independent proof. With --update it writes proof_checked back, promoting entries on the next build. - Ten candidate proofs recorded, all currently proof_checked=false pending a Lean run: goodman-1-6-1 (the eight algebraic identities are `ring`, plus a short argument for the nonzero conjunct), the four parts of Proposition 1.6.2 (Int.mul_eq_one_iff_eq_one_or_neg_one, associated_of_dvd_dvd with Int.associated_iff, dvd_trans, dvd_add with mul_left), 1-6-4 (Nat.exists_prime_and_dvd), 1-6-15 (Int.gcd_eq_one_iff_coprime, where IsCoprime unfolds to exactly the target existential), 1-6-17 (IsCoprime.mul_dvd via Nat.isCoprime_iff_coprime), 1-6-19 (Int.Prime.dvd_mul') and 1-6-20 (Prime.dvd_prod_iff). These are citations of existing Mathlib results, which is the realistic way to populate a textbook premise library -- the remaining 11 (1-6-6, -7, -9, -10, -11, -13, -18, -21, -23, -24, -26) are the ones needing actual work and carry no proof yet. Written without Lean available here, so expect a real failure rate on first compile -- run check_proofs.py --update and send back the FAILED ids with diagnostics, same loop as the statement fixes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017vbLFFJiHSZqGZHEkjvG5x
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
"""Verify the database's proofs by compiling them, and record the verdict.
|
||||
|
||||
check_statements.py asks "is this statement well-formed Lean"; this asks the much
|
||||
stronger question "does this proof actually prove it". A database entry is only
|
||||
allowed to claim proven status after passing here, so nothing enters the citable
|
||||
premise library on an author's confidence alone.
|
||||
|
||||
What counts as passing:
|
||||
- `theorem <name> : <statement> := by <proof>` compiles with no error, and
|
||||
- the proof text contains no `sorry`/`admit`/`native_decide`, and
|
||||
- `#print axioms <name>` reports no axiom dependency beyond Lean's own three
|
||||
(propext, Classical.choice, Quot.sound) -- in particular it must not lean on
|
||||
`sorryAx`, nor on any of the book-asserted `axiom` declarations, since a
|
||||
premise proven *through* an asserted axiom is not independently proven.
|
||||
|
||||
With --update, entries that pass get `proof_checked: true` written back into the
|
||||
database JSONL, which is what makes build_database.py emit them as real theorems
|
||||
instead of axioms. Needs Lean/Lake and a built Mathlib, same as check_statements.py.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
BANNED = re.compile(r'\b(sorry|admit|native_decide)\b')
|
||||
# The three axioms Lean's own standard library is built on; anything else is ours.
|
||||
ALLOWED_AXIOMS = {'propext', 'Classical.choice', 'Quot.sound'}
|
||||
AXIOM_LINE = re.compile(r"^'[^']+' depends on axioms: \[(.*)\]$", re.M)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProofResult:
|
||||
ok: bool
|
||||
diagnostics: str
|
||||
|
||||
|
||||
def check_proof(name, statement, proof, prelude='', command=None, cwd=None, timeout=120):
|
||||
command = command or ['lean']
|
||||
if not proof or not proof.strip():
|
||||
return ProofResult(False, 'no proof recorded')
|
||||
if BANNED.search(proof):
|
||||
return ProofResult(False, f'proof uses a banned escape hatch: {BANNED.search(proof).group(1)}')
|
||||
|
||||
body = '\n'.join(' ' + line for line in proof.strip().splitlines())
|
||||
source = ((prelude + '\n' if prelude else '')
|
||||
+ f'theorem {name} : {statement} := by\n{body}\n'
|
||||
+ f'#print axioms {name}\n')
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix='lean-proof-', dir=cwd) as directory:
|
||||
path = Path(directory) / 'ProofCheck.lean'
|
||||
path.write_text(source, encoding='utf-8')
|
||||
try:
|
||||
result = subprocess.run([*command, str(path)], cwd=cwd, capture_output=True,
|
||||
text=True, timeout=timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
return ProofResult(False, 'Lean timeout')
|
||||
|
||||
diagnostics = result.stdout + result.stderr
|
||||
if result.returncode != 0 or 'error:' in diagnostics:
|
||||
return ProofResult(False, diagnostics[-8000:])
|
||||
|
||||
match = AXIOM_LINE.search(diagnostics)
|
||||
if match:
|
||||
used = {a.strip() for a in match.group(1).split(',') if a.strip()}
|
||||
extra = used - ALLOWED_AXIOMS
|
||||
if extra:
|
||||
return ProofResult(False, f'proof depends on non-standard axioms: {sorted(extra)}\n'
|
||||
+ diagnostics[-2000:])
|
||||
return ProofResult(True, diagnostics[-2000:])
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Compile and verify the proofs recorded in a database JSONL')
|
||||
parser.add_argument('database', type=Path, help='e.g. database/goodman-1.6.jsonl')
|
||||
parser.add_argument('--lean-command', default='["lean"]', help='JSON argv array; e.g. ["lake","env","lean"]')
|
||||
parser.add_argument('--lean-project', help='Lean/Lake project working directory (needs Mathlib built)')
|
||||
parser.add_argument('--timeout', type=float, default=120)
|
||||
parser.add_argument('--report', type=Path, help='write JSONL of {id, ok, diagnostics} here')
|
||||
parser.add_argument('--update', action='store_true',
|
||||
help='write proof_checked back into the database for entries that pass')
|
||||
parser.add_argument('--only', nargs='*', help='check only these ids')
|
||||
args = parser.parse_args()
|
||||
|
||||
command = json.loads(args.lean_command)
|
||||
records = [json.loads(line) for line in args.database.read_text(encoding='utf-8').splitlines() if line.strip()]
|
||||
|
||||
reports, checked, failed, skipped = [], 0, 0, 0
|
||||
for record in records:
|
||||
if args.only and record['id'] not in args.only:
|
||||
continue
|
||||
if not record.get('proof'):
|
||||
skipped += 1
|
||||
continue
|
||||
name = record['id'].replace('-', '_')
|
||||
result = check_proof(name, record['statement'], record['proof'], record.get('prelude', ''),
|
||||
command, args.lean_project, args.timeout)
|
||||
reports.append({"id": record['id'], "ok": result.ok, "diagnostics": result.diagnostics})
|
||||
print(('PROVED ' if result.ok else 'FAILED ') + record['id'])
|
||||
if result.ok:
|
||||
checked += 1
|
||||
if args.update:
|
||||
record['proof_checked'] = True
|
||||
else:
|
||||
failed += 1
|
||||
if args.update:
|
||||
record['proof_checked'] = False
|
||||
|
||||
if args.update:
|
||||
with args.database.open('w', encoding='utf-8') as stream:
|
||||
for record in records:
|
||||
stream.write(json.dumps(record, ensure_ascii=False) + '\n')
|
||||
print(f'updated proof_checked in {args.database}')
|
||||
|
||||
if args.report:
|
||||
with args.report.open('w', encoding='utf-8') as stream:
|
||||
for report in reports:
|
||||
stream.write(json.dumps(report, ensure_ascii=False) + '\n')
|
||||
|
||||
print(f'{checked} proved, {failed} failed, {skipped} entries have no proof recorded yet')
|
||||
raise SystemExit(1 if failed else 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user