fbcb0c3383
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
181 lines
8.2 KiB
Python
181 lines
8.2 KiB
Python
"""Generate the citable premise database from a book section's main-content statements.
|
|
|
|
This is the "third party math library" half of the pipeline: the book's own
|
|
Theorem/Proposition/Lemma/Corollary claims, turned into real Lean declarations that
|
|
lean-agent can cite by name while proving something else (the bench targets, which
|
|
come from the exercises instead -- see formalize_exercises.py).
|
|
|
|
Two outputs:
|
|
|
|
1. An importable Lean file where every database entry is a named declaration. An
|
|
entry is emitted as a real `theorem ... := by <proof>` when a lean-bench run has
|
|
actually proven it (status "proved"), and otherwise as an `axiom` -- asserted from
|
|
the textbook, which is what the book itself does for its own body results before
|
|
an exercise asks you to use them. This is deliberate and has a real cost: anything
|
|
proven *through* an axiom inherits it, which is exactly why the verifier's
|
|
`#print axioms` check matters. A statement that only elaborates is not the same as
|
|
a statement that is true, and a garbled extraction asserted as an axiom can make
|
|
downstream "proofs" worthless -- so every axiom here needs to have been read by a
|
|
human against the book, and `--require-checked` refuses to emit any entry not
|
|
marked as having passed check_statements.py.
|
|
|
|
2. A search index (JSONL) carrying each entry's declaration name, statement, the
|
|
Mathlib identifiers it mentions, and its status -- what search_database.py ranks
|
|
against the current goals at each step of a proof.
|
|
|
|
Pass a lean-bench run's results.jsonl with --results to upgrade the entries it
|
|
proved from axiom to a real theorem carrying the exact proof that run verified.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
|
|
IDENT_RE = re.compile(r'[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)+|[A-Za-z_][A-Za-z0-9_]*')
|
|
# Tokens that say nothing about which premise is relevant.
|
|
STOPWORDS = {'let', 'in', 'fun', 'if', 'then', 'else', 'true', 'false', 'Prop', 'Type'}
|
|
|
|
|
|
def load_jsonl(path):
|
|
return [json.loads(line) for line in path.read_text(encoding='utf-8').splitlines() if line.strip()]
|
|
|
|
|
|
def decl_name(entry_id):
|
|
"""goodman-1-6-2-a -> goodman_1_6_2_a (a legal Lean identifier)."""
|
|
return entry_id.replace('-', '_')
|
|
|
|
|
|
def split_prelude(prelude):
|
|
"""Return (import lines, other prelude lines) for one entry's prelude."""
|
|
imports, other = [], []
|
|
for line in (prelude or '').splitlines():
|
|
(imports if line.strip().startswith('import ') else other).append(line)
|
|
return imports, '\n'.join(other).strip()
|
|
|
|
|
|
def statement_identifiers(statement):
|
|
"""Identifiers mentioned in a statement, for the search index."""
|
|
found = []
|
|
for match in IDENT_RE.findall(statement or ''):
|
|
if match in STOPWORDS or match in found or len(match) < 2:
|
|
continue
|
|
found.append(match)
|
|
return found
|
|
|
|
|
|
def build(entries, results=None, require_checked=False):
|
|
results = results or {}
|
|
|
|
lines = ['-- Generated by textbook/build_database.py -- do not hand-edit.',
|
|
'-- Citable premise database from the textbook\'s main content.',
|
|
'-- `axiom` entries are asserted from the book, NOT proven here: anything',
|
|
'-- proven through one inherits it, so check `#print axioms` downstream.',
|
|
'']
|
|
|
|
shared_imports, shared_defs = [], []
|
|
for entry in entries:
|
|
imports, other = split_prelude(entry.get('prelude'))
|
|
for line in imports:
|
|
if line.strip() not in shared_imports:
|
|
shared_imports.append(line.strip())
|
|
# A def can only be declared once per file, so dedupe rather than attaching
|
|
# it per-theorem the way build_library.py does.
|
|
if other and other not in shared_defs:
|
|
shared_defs.append(other)
|
|
|
|
lines.extend(sorted(shared_imports))
|
|
lines.append('')
|
|
for block in shared_defs:
|
|
lines.append(block)
|
|
lines.append('')
|
|
|
|
index, emitted, skipped = [], [], []
|
|
for entry in entries:
|
|
entry_id = entry['id']
|
|
if require_checked and not entry.get('elaborates'):
|
|
skipped.append(entry_id)
|
|
continue
|
|
|
|
name = decl_name(entry_id)
|
|
result = results.get(entry_id)
|
|
# A proof can come from a lean-bench run, or be recorded on the entry itself
|
|
# (hand-written, or authored by an agent outside a bench run). Either way it
|
|
# only counts once check_proofs.py has actually compiled it: an unverified
|
|
# proof stays an axiom, so the library never claims more than Lean confirmed.
|
|
run_proof = result['attempts'][-1]['proof'] if (
|
|
result and result.get('success') and result.get('attempts')) else None
|
|
own_proof = entry.get('proof') if entry.get('proof_checked') else None
|
|
proof = run_proof or own_proof
|
|
proved = bool(proof)
|
|
proof_pending = bool(not proved and entry.get('proof'))
|
|
provenance = entry.get('source_label', entry_id)
|
|
|
|
doc = f'/-- {provenance} (Goodman, Algebra: Abstract and Concrete).'
|
|
if entry.get('natural_language'):
|
|
doc += f' {entry["natural_language"].split("[fix note:")[0].strip()}'
|
|
if proved:
|
|
doc += ' Proof verified by Lean.'
|
|
elif proof_pending:
|
|
doc += ' ASSERTED from the textbook; a proof is recorded but not yet verified.'
|
|
else:
|
|
doc += ' ASSERTED from the textbook, not proven here.'
|
|
doc += ' -/'
|
|
lines.append(doc)
|
|
|
|
if proved:
|
|
lines.append(f'theorem {name} : {entry["statement"]} := by')
|
|
lines.extend(' ' + line for line in proof.strip().splitlines())
|
|
else:
|
|
lines.append(f'axiom {name} : {entry["statement"]}')
|
|
lines.append('')
|
|
|
|
index.append({
|
|
"name": name,
|
|
"id": entry_id,
|
|
"source_label": entry.get('source_label'),
|
|
"kind": entry.get('kind'),
|
|
"status": "proved" if proved else ("proof_pending" if proof_pending else "asserted"),
|
|
"proof": proof if proved else entry.get('proof'),
|
|
"statement": entry['statement'],
|
|
"natural_language": entry.get('natural_language', '').split('[fix note:')[0].strip(),
|
|
"identifiers": statement_identifiers(entry['statement']),
|
|
"requires": entry.get('requires', []),
|
|
})
|
|
emitted.append(entry_id)
|
|
|
|
return '\n'.join(lines), index, emitted, skipped
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='Generate the citable premise database from main-content statements')
|
|
parser.add_argument('database', type=Path, help='JSONL of main-content statements, e.g. database/goodman-1.6.jsonl')
|
|
parser.add_argument('--results', type=Path, help="a lean-bench run's results.jsonl; proven entries become real theorems")
|
|
parser.add_argument('--out-lean', type=Path, required=True, help='importable Lean file of citable premises')
|
|
parser.add_argument('--out-index', type=Path, required=True, help='JSONL search index for search_database.py')
|
|
parser.add_argument('--require-checked', action='store_true',
|
|
help='refuse entries not marked "elaborates": true (i.e. not yet verified by check_statements.py)')
|
|
args = parser.parse_args()
|
|
|
|
entries = load_jsonl(args.database)
|
|
results = {r['id']: r for r in load_jsonl(args.results)} if args.results else None
|
|
text, index, emitted, skipped = build(entries, results, args.require_checked)
|
|
|
|
args.out_lean.parent.mkdir(parents=True, exist_ok=True)
|
|
args.out_lean.write_text(text + '\n', encoding='utf-8')
|
|
args.out_index.parent.mkdir(parents=True, exist_ok=True)
|
|
with args.out_index.open('w', encoding='utf-8') as stream:
|
|
for record in index:
|
|
stream.write(json.dumps(record, ensure_ascii=False) + '\n')
|
|
|
|
proved = sum(1 for r in index if r['status'] == 'proved')
|
|
pending = sum(1 for r in index if r['status'] == 'proof_pending')
|
|
print(f'{len(emitted)} premises -> {args.out_lean} ({proved} proven, '
|
|
f'{len(emitted) - proved} asserted as axioms, of which {pending} have an unverified proof)')
|
|
print(f'search index -> {args.out_index}')
|
|
if skipped:
|
|
print(f'{len(skipped)} skipped for not being elaboration-checked: {", ".join(skipped)}')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|