Files
lean-bench/tests/test_build_database.py
T
claude-bot fbcb0c3383 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
2026-09-18 19:43:54 +00:00

86 lines
4.3 KiB
Python

import unittest
from textbook.build_database import build, decl_name, split_prelude, statement_identifiers
ENTRIES = [
{"id": "goodman-1-6-1", "source_label": "Proposition 1.6.1", "kind": "proposition",
"statement": "\u2200 (a : \u2124), a + 0 = a", "prelude": "import Mathlib",
"natural_language": "Zero is an additive identity. [fix note: irrelevant]",
"requires": ["Int.add_zero"], "elaborates": True},
{"id": "goodman-1-6-24", "source_label": "Proposition 1.6.24", "kind": "proposition",
"statement": "\u2200 (as : List \u2124), IsGCDOfList 1 as", "prelude": "import Mathlib\ndef IsGCDOfList (d : \u2115) (as : List \u2124) : Prop := True",
"natural_language": "", "requires": [], "elaborates": True},
{"id": "goodman-1-6-99", "source_label": "Proposition 1.6.99", "kind": "proposition",
"statement": "\u2200 (as : List \u2124), IsGCDOfList 2 as", "prelude": "import Mathlib\ndef IsGCDOfList (d : \u2115) (as : List \u2124) : Prop := True",
"natural_language": "", "requires": [], "elaborates": True},
]
class BuildDatabaseTests(unittest.TestCase):
def test_decl_name_is_a_legal_lean_identifier(self):
self.assertEqual(decl_name('goodman-1-6-2-a'), 'goodman_1_6_2_a')
def test_split_prelude_separates_imports_from_defs(self):
imports, other = split_prelude('import Mathlib\ndef Foo := True')
self.assertEqual(imports, ['import Mathlib'])
self.assertEqual(other, 'def Foo := True')
def test_unproven_entries_become_axioms(self):
text, index, emitted, skipped = build(ENTRIES)
self.assertIn('axiom goodman_1_6_1 :', text)
self.assertEqual(len(emitted), 3)
self.assertEqual(skipped, [])
self.assertTrue(all(r['status'] == 'asserted' for r in index))
def test_proven_entries_become_real_theorems_with_their_proof(self):
results = {"goodman-1-6-1": {"id": "goodman-1-6-1", "success": True,
"attempts": [{"proof": "simp"}]}}
text, index, _emitted, _skipped = build(ENTRIES, results)
self.assertIn('theorem goodman_1_6_1 :', text)
self.assertNotIn('axiom goodman_1_6_1 :', text)
self.assertIn(' simp', text)
statuses = {r['id']: r['status'] for r in index}
self.assertEqual(statuses['goodman-1-6-1'], 'proved')
self.assertEqual(statuses['goodman-1-6-24'], 'asserted')
def test_shared_def_in_prelude_is_emitted_once_not_per_entry(self):
# Two entries share the same `def`; declaring it twice in one Lean file is an
# "already declared" error, so it must be hoisted and deduplicated.
text, _index, _emitted, _skipped = build(ENTRIES)
self.assertEqual(text.count('def IsGCDOfList'), 1)
self.assertEqual(text.count('import Mathlib'), 1)
def test_require_checked_skips_unverified_entries(self):
entries = [dict(ENTRIES[0]), dict(ENTRIES[1], elaborates=False)]
_text, index, emitted, skipped = build(entries, require_checked=True)
self.assertEqual(emitted, ['goodman-1-6-1'])
self.assertEqual(skipped, ['goodman-1-6-24'])
self.assertEqual(len(index), 1)
def test_recorded_proof_counts_only_once_checked(self):
# An unverified proof must NOT be emitted as a theorem -- the library may
# never claim more than Lean has confirmed.
pending = [dict(ENTRIES[0], proof='ring', proof_checked=False)]
text, index, _e, _s = build(pending)
self.assertIn('axiom goodman_1_6_1 :', text)
self.assertNotIn('theorem goodman_1_6_1 :', text)
self.assertEqual(index[0]['status'], 'proof_pending')
self.assertEqual(index[0]['proof'], 'ring')
checked = [dict(ENTRIES[0], proof='ring', proof_checked=True)]
text, index, _e, _s = build(checked)
self.assertIn('theorem goodman_1_6_1 :', text)
self.assertIn(' ring', text)
self.assertEqual(index[0]['status'], 'proved')
def test_index_carries_identifiers_and_strips_fix_notes(self):
_text, index, _emitted, _skipped = build(ENTRIES)
first = index[0]
self.assertIn('Int.add_zero', first['requires'])
self.assertNotIn('fix note', first['natural_language'])
self.assertNotIn('let', statement_identifiers('let x := 1'))
if __name__ == '__main__':
unittest.main()