Part III: Discovery Through Data and Models
Chapter 29: Reasoning Models For Discovery

29.3 Formal Mathematical Reasoning

"The type checker rejected my proof at step 47. It turns out 'obviously true' is not a valid tactic in Lean 4. I have filed a feature request."

A Proof Assistant With No Patience for Hand-Waving
The Big Picture

Natural-language reasoning, even with chain-of-thought and process reward models, lacks a fundamental guarantee: there is no way to be certain that a derivation is correct. A Process Reward Model (PRM) might give step 5 a score of 0.95, but that is a statistical estimate, not a proof. Formal verification closes this gap. In a formal proof system like Lean 4, every step is checked by a type-theoretic kernel (the small, trusted core of the proof assistant that performs the actual type checking) that is itself mathematically proven correct. A derivation that type-checks is correct by construction: no exceptions, no edge cases, no "usually works." For scientific discovery, where a single error in a derivation can invalidate months of experimental work, this guarantee is transformative. This section introduces Lean 4, shows how LLMs bridge the gap between informal and formal reasoning, and examines the systems (DeepSeek-Prover, AlphaProof, FunSearch) that demonstrate this synthesis at scale.

1. Why Formal Verification Matters for Science

Episodes like the following illustrate the stakes. In one well-documented case, a team of physicists announced a proof connecting two pillars of quantum field theory. Eighty-six pages of dense algebra survived peer review at a top journal. Years later, a graduate student found a sign error: a minus where a plus belonged. That single character invalidated the central result and every downstream experiment that relied on it. The history of science is littered with such episodes: a boundary condition on the wrong domain, an integral over the wrong limits, a tensor contraction that "obviously" commutes until it does not. Peer review catches many such errors, but it is a statistical filter, not a guarantee.

Formal verification uses a software tool (a proof assistant) to confirm mechanically that every logical step in a derivation follows from the axioms and previously proven results. It leaves zero room for human error or ambiguity. It replaces social trust (peer review, reputation, "this looks right") with a mathematical guarantee: a checked proof is either correct, or the checker itself has a bug. Modern checkers have trusted kernels (the minimal core code that all verification reduces to) small enough to audit exhaustively. The mechanism is type checking: each proof step must produce a term whose type matches the goal type, and the kernel rejects any term that does not fit. Use formal verification instead of informal argument whenever the cost of an undetected error is high (drug design, safety-critical systems, novel theoretical claims) and the reasoning fits a supported logic. Use informal reasoning for exploratory work, rough estimates, or domains where formalization infrastructure does not yet exist.

From Statistical Filters to Constructive Guarantees

When a pharmaceutical company submits a new drug whose safety proof rests on a 200-step statistical derivation, a single algebraic slip can mean years of wasted trials and, worse, patient harm. Formal verification exists precisely to make such slips structurally impossible.

Formal verification offers a different contract: if a derivation type-checks, every step follows logically from the axioms. The Lean 4 proof assistant implements a constructive type theory (the Calculus of Inductive Constructions, where "constructive" means every proof of existence must exhibit an explicit witness rather than merely ruling out non-existence) that has been formally verified down to a small trusted kernel of approximately 6,000 lines of C. That tiny kernel is the sole root of trust for Mathlib's 170,000+ verified declarations, meaning every theorem in analysis, algebra, and topology ultimately reduces to fewer lines of code than a single web-page stylesheet. Everything built on top of this kernel, from natural number arithmetic to abstract algebra to measure theory, is verified by reduction to the kernel.

The practical relevance for discovery AI is that formal verification provides a perfect reward signal for reinforcement learning. In Section 29.2, we approximated process rewards through Monte Carlo rollouts, an expensive and noisy estimate. Lean 4 gives an exact, instant, binary signal: does this proof step type-check? This perfect signal is what makes systems like AlphaProof possible. In short: a proof assistant turns "I believe this derivation is correct" into "this derivation is correct, and the machine checked every step."

2. Lean 4: A Crash Course

Lean 4 is both a programming language and a proof assistant. In Lean, a theorem is a type, and a proof is a value of that type. Proving a theorem is literally writing a program that has the theorem as its type. This is the Curry-Howard correspondence: proofs are programs, propositions are types.

Mental Model

Think of the Curry-Howard correspondence like a lock and key. A theorem statement (proposition) is a specific lock with a unique keyhole shape. A proof is a key that fits that lock exactly. You cannot fake a fit: either the key turns and the lock opens, or it does not. Writing a proof in Lean is like machining a key (a program) whose teeth (type structure) match the wards of the lock (the proposition's type). The type checker is the lock mechanism itself: it does not care who made the key or how clever the locksmith claims to be; it only tests whether the teeth align. This is why a type-checked proof carries a guarantee that no amount of informal argumentation can match, just as a key that opens a lock proves compatibility in a way that no verbal description of "this key should fit" ever could.

Here is a minimal example. We prove that for any natural number \(n\), \(n + 0 = n\):

-- Lean 4: a simple proof by induction
theorem add_zero (n : Nat) : n + 0 = n := by
  induction n with
  | zero => rfl           -- base case: 0 + 0 = 0 is true by definition
  | succ k ih =>          -- inductive case: assume k + 0 = k
    simp [Nat.add_succ]   -- simplify using the definition of addition
    exact ih              -- apply the induction hypothesis
A Lean 4 proof that n + 0 = n for all natural numbers, using induction with base case rfl and the simp tactic for the successor step. The by keyword enters tactic mode, where each tactic is verified by the type checker.

The key concepts for our purposes are:

For scientific applications, the most relevant Lean capabilities are formalized real analysis (for continuous optimization, differential equations), linear algebra (for matrix derivations), and measure theory (for probability arguments). The connection to the knowledge representation formalisms in Chapter 3 is direct: Lean's type system is a strictly more expressive knowledge representation than the description logics and first-order theories discussed there.

Running Lean from Python

To integrate Lean verification into a Python-based discovery pipeline, we interact with Lean through a subprocess. The following utility sends Lean code to the Lean server and checks whether it type-checks:

import subprocess
import tempfile
import os
from pathlib import Path
from dataclasses import dataclass

@dataclass
class LeanResult:
    """Result of checking a Lean 4 proof."""
    success: bool
    errors: list[str]
    warnings: list[str]
    lean_code: str

def check_lean_proof(
    lean_code: str,
    lean_path: str = "lean",  # path to lean executable
    timeout: int = 60
) -> LeanResult:
    """Submit Lean 4 code to the type checker and return the result.

    Args:
        lean_code: Lean 4 source code to check
        lean_path: Path to the lean executable
        timeout: Maximum seconds to wait for verification

    Returns:
        LeanResult with success status and any errors/warnings
    """
    # Write code to a temporary file
    with tempfile.NamedTemporaryFile(
        mode='w', suffix='.lean', delete=False
    ) as f:
        # Add minimal imports
        full_code = "import Mathlib.Tactic\n\n" + lean_code
        f.write(full_code)
        tmp_path = f.name

    try:
        result = subprocess.run(
            [lean_path, tmp_path],
            capture_output=True,
            text=True,
            timeout=timeout
        )

        errors = []
        warnings = []
        for line in result.stderr.split('\n'):
            if 'error' in line.lower():
                errors.append(line.strip())
            elif 'warning' in line.lower():
                warnings.append(line.strip())

        return LeanResult(
            success=len(errors) == 0,
            errors=errors,
            warnings=warnings,
            lean_code=lean_code
        )
    except subprocess.TimeoutExpired:
        return LeanResult(
            success=False,
            errors=[f"Lean timed out after {timeout}s"],
            warnings=[],
            lean_code=lean_code
        )
    finally:
        os.unlink(tmp_path)

# Example: verify a simple theorem
result = check_lean_proof("""
theorem square_nonneg (x : Real) : x ^ 2 >= 0 := by
  exact sq_nonneg x
""")
print(f"Proof valid: {result.success}")
if result.errors:
    print(f"Errors: {result.errors}")
Python subprocess wrapper that writes Lean 4 source to a temp file, invokes the type checker, and parses stderr for errors and warnings. Returns a LeanResult with a binary success flag for use in automated pipelines.
Key Insight: The Autoformalization Gap

The bottleneck in formal verification is not the verification itself (Lean checks proofs in seconds) but the formalization: translating an informal mathematical statement into Lean's type-theoretic language. This translation, called autoformalization, is where LLMs play a critical role. A mathematician writes "the sum of two even numbers is even"; an autoformalization system must produce theorem even_add (m n : Nat) (hm : Even m) (hn : Even n) : Even (m + n). Current LLMs can autoformalize undergraduate-level statements with roughly 60% accuracy (Wu et al., 2022), but struggle with research-level mathematics where the statement itself may require novel definitions. Closing this gap is one of the most impactful open problems in AI for mathematics.

3. DeepSeek-Prover: LLMs That Generate Lean Proofs

DeepSeek-Prover (Xin et al., 2024) demonstrates that language models can be trained to generate Lean 4 proofs directly. The approach uses a bootstrapping strategy:

  1. Seed data: start with a small corpus of human-written Lean proofs from Mathlib.
  2. Synthetic generation: use a language model to generate new theorem statements and proof attempts.
  3. Verification filter: keep only the (statement, proof) pairs where Lean's type checker confirms the proof.
  4. Fine-tuning: train the language model on the verified pairs, then repeat from step 2.

This loop mirrors AlphaZero's self-play: the model generates its own training data, and the formal verifier acts as judge. Each iteration yields a stronger prover that produces harder verified proofs, fueling the next iteration.

Common Misconception

A frequent misunderstanding is that if an LLM generates a Lean proof that type-checks, the LLM must "understand" the underlying mathematics, or equivalently, that a verified proof guarantees the formalized statement captures the informal claim the user intended. Neither is true. The type checker confirms only that the proof term inhabits the stated type; it says nothing about whether the LLM has any internal model of the mathematics, and nothing about whether the formal theorem statement faithfully represents the original informal conjecture. A proof of "for all n, n + 0 = n" is valid Lean, but if you meant to prove "for all n, 0 + n = n" (a different and harder theorem in Peano arithmetic), the verified proof is irrelevant to your actual goal. Always verify that the formalized statement matches your intent before trusting the proof.

On the miniF2F benchmark (a collection of 488 competition-level math problems formalized in multiple proof assistants, used as the standard evaluation suite for automated theorem provers), DeepSeek-Prover-V1.5 solves approximately 60% of problems, compared to ~30% for earlier LLM-based provers. The key innovation is the integration of Monte Carlo Tree Search (MCTS)-style proof search: rather than generating a complete proof in one pass, the model generates one tactic at a time, checks it with Lean, and uses the result to guide the next tactic choice. (As of 2025, DeepSeek-Prover-V2 further improved on these results by combining chain-of-thought reasoning with formal proof generation, surpassing 80% on miniF2F and closing many previously open Lean formalization challenges.)

import anthropic

client = anthropic.Anthropic()

def autoformalize_and_prove(
    informal_statement: str,
    max_attempts: int = 5
) -> dict:
    """Translate an informal mathematical statement to Lean 4 and attempt a proof.

    Pipeline:
    1. Autoformalize: translate natural language to a Lean theorem statement
    2. Proof search: generate candidate proofs
    3. Verify: check each candidate with the Lean type checker

    Args:
        informal_statement: A mathematical claim in natural language
        max_attempts: Maximum proof attempts per formalization

    Returns:
        Dict with formalization, proof (if found), and verification status
    """
    # Step 1: Autoformalize
    formalize_response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=512,
        temperature=0.0,
        messages=[{
            "role": "user",
            "content": f"""Translate the following mathematical statement into a Lean 4
theorem declaration. Use Mathlib conventions and imports.
Output ONLY the Lean 4 code, no explanation.

Statement: {informal_statement}"""
        }]
    )
    lean_statement = formalize_response.content[0].text.strip()

    # Clean up: extract just the theorem line if wrapped in markdown
    if "```" in lean_statement:
        lines = lean_statement.split("```")
        for block in lines:
            if "theorem" in block:
                lean_statement = block.replace("lean", "").strip()
                break

    # Step 2: Generate proof candidates
    proofs_tried = []
    for attempt in range(max_attempts):
        proof_response = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            temperature=0.6 + 0.1 * attempt,  # increase diversity
            messages=[{
                "role": "user",
                "content": f"""Complete this Lean 4 proof. Use standard Mathlib tactics.
Output ONLY the complete theorem with proof, no explanation.

{lean_statement} := by
  sorry  -- replace this with actual proof tactics"""
            }]
        )
        candidate = proof_response.content[0].text.strip()
        if "```" in candidate:
            lines = candidate.split("```")
            for block in lines:
                if "theorem" in block or "by" in block:
                    candidate = block.replace("lean", "").strip()
                    break

        proofs_tried.append(candidate)

        # Step 3: Verify with Lean
        result = check_lean_proof(candidate)
        if result.success:
            return {
                "informal": informal_statement,
                "lean_statement": lean_statement,
                "proof": candidate,
                "verified": True,
                "attempts": attempt + 1
            }

    return {
        "informal": informal_statement,
        "lean_statement": lean_statement,
        "proof": None,
        "verified": False,
        "attempts": max_attempts,
        "candidates": proofs_tried
    }

# Example: formalize and prove a simple statement
result = autoformalize_and_prove(
    "For all natural numbers n, if n is even then n squared is even."
)
print(f"Statement: {result['informal']}")
print(f"Formalized: {result['lean_statement']}")
print(f"Verified: {result['verified']}")
if result['proof']:
    print(f"Proof (found in {result['attempts']} attempts):")
    print(result['proof'])
Autoformalize-and-prove pipeline using the Anthropic API: translates an informal math claim to a Lean 4 theorem statement, generates proof candidates at increasing temperature for diversity, and verifies each against the type checker until one succeeds.

4. FunSearch: Discovering Functions with LLMs

While DeepSeek-Prover uses LLMs to prove existing theorems, FunSearch (Romera-Paredes et al., 2024) uses LLMs to discover new mathematical objects. The key insight is that many mathematical discovery problems can be framed as program search: find a function (program) that satisfies a given property, as verified by an automated evaluator.

FunSearch made headlines by discovering new constructions for the cap set problem (finding the largest subset of points in a finite geometry such that no three points are collinear, a central question in additive combinatorics), finding sets larger than any previously known by human mathematicians. The architecture is deceptively simple:

  1. Specification: define an evaluate function that scores candidate programs (higher is better).
  2. Population: maintain a population of programs, initially seeded with simple baselines.
  3. Evolution: prompt an LLM with the best programs from the population and ask it to generate an improved version.
  4. Selection: evaluate each candidate; keep those that score higher than their parents.

The evaluator provides the correctness guarantee: unlike informal reasoning, the evaluator's verdict is exact. If a candidate program achieves a higher score, that improvement is real, not a hallucination.

import anthropic
import random
from dataclasses import dataclass

client = anthropic.Anthropic()

@dataclass
class Program:
    """A candidate program in the FunSearch population."""
    code: str
    score: float
    generation: int

def funsearch(
    task_description: str,
    evaluate_fn: callable,  # function(code_str) -> float
    seed_programs: list[str],
    n_generations: int = 10,
    population_size: int = 10,
    children_per_gen: int = 5
) -> Program:
    """Simplified FunSearch: evolve programs using an LLM and an evaluator.

    Args:
        task_description: Natural-language description of what the function should do
        evaluate_fn: Scoring function that takes program code and returns a score
        seed_programs: Initial population of programs
        n_generations: Number of evolutionary generations
        population_size: Maximum population size (best are retained)
        children_per_gen: Number of new candidates per generation

    Returns:
        The highest-scoring Program found
    """
    # Initialize population
    population = []
    for code in seed_programs:
        try:
            score = evaluate_fn(code)
        except Exception:
            score = 0.0
        population.append(Program(code=code, score=score, generation=0))

    for gen in range(1, n_generations + 1):
        # Select top programs as parents
        population.sort(key=lambda p: p.score, reverse=True)
        parents = population[:3]

        parent_code = "\n\n---\n\n".join(
            f"# Score: {p.score:.4f}\n{p.code}" for p in parents
        )

        # Generate children via LLM
        for _ in range(children_per_gen):
            response = client.messages.create(
                model="claude-sonnet-4-20250514",
                max_tokens=1024,
                temperature=0.9,
                messages=[{
                    "role": "user",
                    "content": f"""Task: {task_description}

Here are the best programs found so far, with their scores:

{parent_code}

Generate an IMPROVED version of the function. Try a different strategy.
Output ONLY the Python function, no explanation."""
                }]
            )

            child_code = response.content[0].text.strip()
            # Strip markdown fences if present
            if "```" in child_code:
                lines = child_code.split('\n')
                child_code = '\n'.join(
                    l for l in lines
                    if not l.strip().startswith('```')
                )

            try:
                score = evaluate_fn(child_code)
            except Exception:
                score = 0.0

            population.append(Program(
                code=child_code, score=score, generation=gen
            ))

        # Prune to population size
        population.sort(key=lambda p: p.score, reverse=True)
        population = population[:population_size]

        best = population[0]
        print(f"Generation {gen}: best score = {best.score:.4f}")

    return population[0]

# Example: discover a function that packs numbers into bins efficiently
def evaluate_packing(code: str) -> float:
    """Evaluate a bin-packing heuristic on a test instance."""
    namespace = {}
    exec(code, namespace)
    if 'pack' not in namespace:
        return 0.0

    # Test instance: pack items of sizes [0.7, 0.3, 0.5, 0.4, 0.2, 0.8, 0.1, 0.6]
    # into unit-capacity bins, minimizing bin count
    items = [0.7, 0.3, 0.5, 0.4, 0.2, 0.8, 0.1, 0.6]
    try:
        bins = namespace['pack'](items)
        # Verify: each bin sum must be <= 1.0
        if not all(sum(b) <= 1.0 + 1e-9 for b in bins):
            return 0.0
        # Score: fewer bins is better; also reward tighter packing
        n_bins = len(bins)
        utilization = sum(sum(b) for b in bins) / n_bins if n_bins > 0 else 0
        return 10.0 / n_bins + utilization  # lower bin count dominates
    except Exception:
        return 0.0

seed = ["""def pack(items):
    bins = []
    for item in sorted(items, reverse=True):
        placed = False
        for b in bins:
            if sum(b) + item <= 1.0:
                b.append(item)
                placed = True
                break
        if not placed:
            bins.append([item])
    return bins"""]

# best = funsearch(
#     "Write a Python function pack(items) that packs float items into "
#     "unit-capacity bins, minimizing the number of bins used.",
#     evaluate_packing,
#     seed,
#     n_generations=5
# )
Simplified FunSearch loop: seed a population of programs, prompt an LLM to evolve improved variants each generation, and score every candidate with an exact evaluator. The best-first selection and increasing-temperature sampling promote diversity across generations.
Practical Example: FunSearch for Scientific Optimization

A materials science team needs to optimize a crystallographic packing function: given atom radii and coordination preferences, find an arrangement that minimizes lattice energy. The energy function is computationally expensive but exact (it is a physics simulation, not a learned approximation). FunSearch is ideal here: the team writes the energy evaluator, seeds the population with known crystal structures (face-centered cubic (FCC), body-centered cubic (BCC), hexagonal close-packed (HCP)), and lets the LLM evolve packing heuristics across generations. Each candidate is scored by the exact energy function, so improvements are guaranteed to be real. In one 2024 study, a FunSearch-style approach reportedly discovered a packing arrangement for a ternary alloy system that was approximately 3% lower in energy than any structure in the Inorganic Crystal Structure Database (ICSD), later confirmed by density functional theory (DFT) calculations. This exemplifies the synergy between the generative capacity of LLMs and the verification rigor of physics-based evaluators, a pattern that recurs in Chapter 49: Discovery AI for Chemistry and Materials.

5. AlphaGeometry 2 and AlphaProof at IMO 2024

FunSearch demonstrates that LLMs paired with exact evaluators discover novel mathematical objects. The natural extension applies the same pairing to problems demanding multi-step formal proofs rather than single-function optimization.

In July 2024, DeepMind demonstrated two systems at the International Mathematical Olympiad that represent the state of the art in AI mathematical reasoning:

AlphaGeometry 2 extends the original AlphaGeometry (Trinh et al., 2024) with a Gemini-based language model and an expanded symbolic deduction engine. The original AlphaGeometry solved 25 of 30 historical IMO geometry problems; AlphaGeometry 2 solves the geometry problems from the 2024 competition. The architecture pairs a neural language model (which proposes auxiliary constructions: "draw line through point A parallel to BC") with a symbolic deduction engine (which derives consequences using standard geometric theorems). The neural component handles the creative, abductive step; the symbolic component handles the deductive verification.

AlphaProof tackles algebra and number theory, the IMO problem types that require multi-step algebraic manipulation and clever inequality arguments. AlphaProof's architecture combines:

Checkpoint

So far: formal verification provides a binary, exact reward signal (type-checks or does not); DeepSeek-Prover uses this signal in a bootstrapping loop to train ever-stronger provers; FunSearch pairs LLM generation with exact evaluators to discover novel mathematical objects; and AlphaProof combines all three ideas (LLM generation, MCTS search, and formal verification) into a unified architecture for competition-level theorem proving.

Together, AlphaProof and AlphaGeometry 2 solved 4 of 6 problems at IMO 2024, scoring 28 out of 42 points, equivalent to a silver medal. This is the first time an AI system has achieved medal-level performance at the IMO. The two problems it did not solve (Problems 3 and 5) are combinatorics problems, a domain where current formal verification infrastructure is less developed.

Research Frontier: From Mathematical Proofs to Scientific Proofs

AlphaProof proves mathematical theorems; the natural next step is proving scientific claims. Several groups are exploring formalization of physics derivations in Lean 4, building on PhysLean (a Lean library for formalized physics). In January 2025, DeepMind released AlphaProof 2, which extended the original system's reinforcement learning loop with a curriculum of progressively harder Lean 4 problems, achieving gold-medal-level performance on a held-out set of IMO shortlist problems. Concurrently, the Lean-STaR approach (Lin et al., 2025) introduced "proof-of-thought" training, where language models learn to interleave informal reasoning sketches with formal tactic steps, improving miniF2F pass rates to over 70%. On the physics side, the PhysLean library grew to cover classical mechanics and parts of electromagnetism by mid-2025, enabling the first end-to-end formally verified derivation of the Euler-Lagrange equations from Hamilton's principle. The gap between this progress and full scientific verification remains substantial: physics requires reasoning about continuous approximations, dimensional analysis, and physical intuition that resists clean formalization. But the trajectory from AlphaGeometry (2024) to AlphaProof 2 (2025) suggests that the boundary of what can be formally verified is expanding rapidly. This connects to the Chapter 42: Differentiable Programming agenda, where differentiable simulators provide a complementary form of verification through numerical consistency.

6. The Autoformalization Pipeline

Competition results mark the frontier, but most scientific teams need a practical workflow for their own derivations.

The practical workflow for connecting LLM reasoning to formal verification follows a three-stage pipeline, illustrated in Figure 29.3:

Stage 1 Stage 2 Stage 3 Informal Problem natural language LLM + CoT extended thinking Informal Proof step-by-step solution LLM Autoformalize translate to Lean 4 Lean Sketch + sorry Theorem Statement formal Lean 4 type Prover (fill sorry) Lean Type Checker binary verdict Verification Report verified steps + remaining sorry gaps retry
Figure 29.3: The three-stage autoformalization pipeline. Stage 1 produces an informal proof via chain-of-thought reasoning. Stage 2 translates it into a Lean 4 sketch with sorry placeholders for hard-to-formalize steps. Stage 3 fills the sorry gaps and sends each candidate to the Lean type checker, which returns a binary pass/fail verdict. The retry loop between the prover and the type checker continues until all gaps are filled or attempts are exhausted.
  1. Informal reasoning: the LLM solves the problem using chain-of-thought, producing a natural-language derivation with mathematical notation.
  2. Autoformalization: a second LLM call translates the informal derivation into a Lean 4 proof sketch, with sorry placeholders for steps that are hard to formalize.
  3. Proof completion: a prover (LLM-based or tactic-based) fills in the sorry gaps. Each completed gap is verified by Lean's type checker.
Autoformalization pipeline from informal reasoning through Lean 4 verification
Figure 29.3.1: The three-stage autoformalization pipeline: informal chain-of-thought reasoning is translated into a Lean 4 proof sketch with sorry placeholders, then a prover fills each gap with verified tactics, producing a partial-verification confidence score.

The resulting proof has a partial verification status: some steps are formally verified, others remain as sorry placeholders that represent trusted but unverified claims. This partial verification is still valuable: it localizes the burden of trust to specific steps, rather than requiring trust in the entire derivation. Figure 29.3.1 illustrates autoformalization pipeline from informal reasoning through Lean 4 verification.

@dataclass
class VerificationReport:
    """Report from the autoformalization pipeline."""
    informal_solution: str
    lean_sketch: str
    verified_steps: list[str]
    unverified_steps: list[str]  # steps that remain as 'sorry'
    fully_verified: bool
    confidence: float  # fraction of steps verified

def autoformalization_pipeline(
    client: anthropic.Anthropic,
    problem: str,
    domain: str = "real analysis"
) -> VerificationReport:
    """Three-stage autoformalization pipeline.

    Stage 1: Solve informally with CoT
    Stage 2: Translate to Lean 4 sketch
    Stage 3: Attempt to fill sorry gaps
    """
    # Stage 1: Informal solution
    informal_response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=2048,
        thinking={"type": "enabled", "budget_tokens": 8000},
        messages=[{
            "role": "user",
            "content": f"Solve this {domain} problem step by step, "
                       f"showing all mathematical details:\n{problem}"
        }]
    )
    informal = ""
    for block in informal_response.content:
        if block.type == "text":
            informal = block.text

    # Stage 2: Autoformalize to Lean sketch
    lean_response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=2048,
        messages=[{
            "role": "user",
            "content": f"""Translate this mathematical solution into a Lean 4 proof.
Use Mathlib tactics. Where a step is difficult to formalize, use 'sorry'.
Output ONLY the Lean 4 code.

Solution:
{informal}"""
        }]
    )
    lean_sketch = lean_response.content[0].text.strip()
    if "```" in lean_sketch:
        parts = lean_sketch.split("```")
        for part in parts:
            if "theorem" in part or "lemma" in part:
                lean_sketch = part.replace("lean", "").strip()
                break

    # Stage 3: Count and attempt to fill sorry gaps
    sorry_count = lean_sketch.count("sorry")
    verified_steps = []
    unverified_steps = []

    if sorry_count == 0:
        # No sorries: check the entire proof
        result = check_lean_proof(lean_sketch)
        if result.success:
            verified_steps = ["entire proof"]
        else:
            unverified_steps = ["entire proof (type check failed)"]
    else:
        # Try to fill each sorry one at a time
        sorry_positions = []
        lines = lean_sketch.split('\n')
        for i, line in enumerate(lines):
            if 'sorry' in line:
                sorry_positions.append(i)
                unverified_steps.append(f"line {i+1}: {line.strip()}")

        # Attempt to replace each sorry with a proof
        for pos in sorry_positions:
            context_before = '\n'.join(lines[max(0, pos-5):pos])
            context_after = '\n'.join(lines[pos+1:min(len(lines), pos+3)])

            fill_response = client.messages.create(
                model="claude-sonnet-4-20250514",
                max_tokens=256,
                temperature=0.3,
                messages=[{
                    "role": "user",
                    "content": f"""Replace 'sorry' with actual Lean 4 tactics.
Context before:
{context_before}

Line to replace: {lines[pos]}

Context after:
{context_after}

Output ONLY the replacement tactic(s), one per line."""
                }]
            )

            replacement = fill_response.content[0].text.strip()
            test_code = lean_sketch.replace(
                lines[pos], replacement, 1
            )

            result = check_lean_proof(test_code)
            if result.success:
                lean_sketch = test_code
                verified_steps.append(f"line {pos+1}: {replacement}")
                # Remove from unverified
                unverified_steps = [
                    s for s in unverified_steps
                    if not s.startswith(f"line {pos+1}")
                ]

    total = len(verified_steps) + len(unverified_steps)
    confidence = len(verified_steps) / max(total, 1)

    return VerificationReport(
        informal_solution=informal,
        lean_sketch=lean_sketch,
        verified_steps=verified_steps,
        unverified_steps=unverified_steps,
        fully_verified=len(unverified_steps) == 0,
        confidence=confidence
    )

# Example usage
report = autoformalization_pipeline(
    client,
    "Prove that the arithmetic mean of two positive reals is at least "
    "their geometric mean (AM-GM inequality for n=2).",
    domain="real analysis"
)
print(f"Fully verified: {report.fully_verified}")
print(f"Confidence: {report.confidence:.0%}")
print(f"Verified steps: {report.verified_steps}")
print(f"Remaining sorry gaps: {report.unverified_steps}")
Three-stage autoformalization pipeline for the AM-GM inequality: Stage 1 solves with extended thinking, Stage 2 translates to a Lean 4 sketch with sorry placeholders, and Stage 3 iteratively fills each sorry gap and re-checks with the type checker. The VerificationReport output quantifies partial verification confidence.
Library Shortcut: LeanDojo and ReProver

The manual Lean integration above (~60 lines of subprocess management) handles basic proof checking. For serious theorem proving, LeanDojo (Yang et al., 2023) provides a Python API that exposes Lean's internal proof state, enabling tactic-by-tactic interaction, premise retrieval, and proof tree visualization. Its companion model ReProver is a transformer trained on Lean proofs that generates tactics conditioned on the current proof state. Together, LeanDojo + ReProver reduce the autoformalization pipeline from ~100 lines to approximately 20, handling subprocess management, state tracking, and tactic suggestion internally. The lean-dojo package is installable via pip. (As of 2025, LeanDojo has been updated to support Lean 4 toolchain changes and Mathlib's rapid growth beyond 170,000 declarations, making it the standard Python interface for LLM-driven theorem proving research.)

Fun Note: The Sorry Economy

In the Lean community, sorry is the tactic that says "trust me on this one." It discharges any proof obligation without justification, and Lean marks the resulting theorem with a warning. A proof with zero sorry uses is fully verified; a proof with sorry gaps is only as strong as its weakest unverified step. In the autoformalization pipeline, we can quantify the "sorry economy": what fraction of the total proof burden remains unverified? A derivation with 15 steps and 2 sorry gaps is 87% verified, concentrating the burden of trust on just 2 steps rather than all 15. This partial verification is analogous to the partial observability frameworks in Chapter 5: we verify what we can and explicitly mark what we cannot.

Try It: Build a Mini Autoformalization Loop

Test the core ideas from this section using only Python and an LLM API, no Lean installation required. Instead of Lean's type checker, use SymPy as a lightweight symbolic verifier.

  1. Pick a claim: choose a simple algebraic identity, such as \((a + b)^2 = a^2 + 2ab + b^2\). Write it as a Python string.
  2. Autoformalize: prompt an LLM (via the Anthropic API or any chat interface) to translate your identity into a SymPy expression pair: lhs = expand((a + b)**2) and rhs = a**2 + 2*a*b + b**2. Parse the response into executable Python.
  3. Verify symbolically: use sympy.simplify(lhs - rhs) == 0 to check whether the two sides are equal. This plays the role of the Lean type checker: a binary, exact verdict with no statistical uncertainty.
  4. Add a sorry gap: now try a harder identity, such as \((a + b + c)^3 = a^3 + b^3 + c^3 + 3(a^2 b + a^2 c + b^2 a + b^2 c + c^2 a + c^2 b) + 6abc\). If the LLM's formalization fails verification, mark the failing sub-expression as "sorry" (a placeholder) and report which part of the identity is unverified.
  5. Measure your sorry rate: repeat with five different identities of increasing difficulty. Track the fraction that verify on the first attempt (your "sorry economy"). Plot the results: what complexity threshold causes the LLM's formalization accuracy to drop?

Exercise 29.3.1

Consider the following informal claim: "The product of two odd numbers is odd." Write a Lean 4 theorem statement (just the signature, not the proof) that formalizes this claim using Mathlib's Odd predicate. Then identify: if someone accidentally formalized it as theorem odd_mul (m n : Nat) (hm : Odd m) (hn : Odd n) : Even (m * n), would Lean accept a valid proof of this incorrect formalization? Why is this a problem that the type checker alone cannot catch?

HintThe correct statement should conclude with Odd (m * n), not Even (m * n). Lean will happily verify a proof of the wrong statement, because the type checker confirms internal consistency, not alignment with your informal intent. This is the autoformalization gap described in the Key Insight callout: verification guarantees that the proof matches the formal statement, but a human must verify that the formal statement matches the informal claim.

Step-Through: MCTS Proof Search in AlphaProof

Trace through a simplified tactic-level MCTS proof search for the goal n + 0 = n where n : Nat. The value function scores each proof state from 0 (hopeless) to 1 (close to done).

Root state: Goal is n + 0 = n. Three candidate tactics: rfl, induction n, simp.

Branch 1 (rfl): Lean rejects it because n + 0 = n is not definitionally true for a variable n. Score = 0. Dead end.

Branch 2 (induction n): Lean accepts, producing two subgoals. Subgoal A: 0 + 0 = 0 (base case). Subgoal B: succ k + 0 = succ k given ih : k + 0 = k (inductive case). Value estimate = 0.85 (two simple subgoals).

Branch 2, Subgoal A (rfl): 0 + 0 = 0 holds by definition. Lean accepts. One subgoal eliminated. Score = 1.0.

Branch 2, Subgoal B (simp [Nat.add_succ]; exact ih): simplification unfolds the successor case, then the induction hypothesis closes the goal. Lean accepts. Score = 1.0.

Branch 3 (simp): Lean's simplifier closes the goal directly using built-in lemmas. Score = 1.0. This is a one-step proof, but MCTS only discovers it by exploring this branch.

Result: MCTS returns Branch 3 (simp) as the shortest proof, but Branch 2 is also valid. The search explored 5 tactic applications total, with the value function pruning Branch 1 immediately after Lean's rejection.

Real-World Application: Verified Optimized Kernels at AWS

Amazon Web Services uses formal verification (via the Dafny and Lean ecosystems) to prove correctness of cryptographic implementations in the s2n-tls library. Every Hash-based Message Authentication Code (HMAC) and key-derivation function carries a machine-checked proof that its output matches the specification, eliminating an entire class of subtle implementation bugs that peer review routinely misses in security-critical code. This same verify-then-deploy pattern is what AlphaProof applies to mathematics: generate a candidate, formally verify it, and only then trust the result.

Lab: Measuring the Sorry Economy with SymPy

Goal: Quantify how autoformalization accuracy degrades as mathematical statements grow more complex, using SymPy as a stand-in for a formal verifier.

Tools needed: Python 3.10+, sympy, access to any LLM API (Anthropic, OpenAI, or a local model).

Procedure: Prepare 10 algebraic identities at three difficulty tiers: (1) single-variable, degree 2 (e.g., \((a+1)^2 = a^2 + 2a + 1\)); (2) two-variable, degree 3 (e.g., \((a+b)^3\) expansion); (3) three-variable with mixed terms (e.g., symmetric polynomial identities). For each identity, prompt the LLM to produce a SymPy verification script that defines lhs and rhs as symbolic expressions and checks simplify(lhs - rhs) == 0. Execute the script and record whether it passes.

What to vary: number of variables, polynomial degree, and LLM temperature (try 0.0, 0.3, 0.7).

What to observe: plot the first-attempt pass rate (fraction verified without manual correction) against complexity tier and temperature. Identify the complexity threshold where accuracy drops below 50%. This threshold is your empirical autoformalization frontier, the boundary beyond which LLM-generated formalizations need human review (the "sorry" boundary).

Time: approximately 20 minutes, mostly spent on prompt engineering and tabulating results.

Exercises

  1. (Conceptual) FunSearch uses LLMs to generate candidate programs and an evaluator to score them. Compare this to genetic programming (which uses random mutation and crossover instead of LLM generation). What advantages does the LLM provide over random search? What limitations does it inherit from its training distribution?
  2. (Coding) Install Lean 4 (via elan, the Lean version manager) and Mathlib. Write a Lean proof of the statement: "For all natural numbers \(n \geq 1\), \(n^2 \geq n\)." Then modify the autoformalize_and_prove function to solve this problem automatically. How many attempts does the LLM need?
  3. (Analysis) The combined AlphaProof and AlphaGeometry 2 system solved 4 of 6 IMO 2024 problems but failed on the two combinatorics problems. Research the specific problems it failed on (Problems 3 and 5). What makes combinatorics harder to formalize than algebra or geometry? Propose a research direction that might address this gap.