verifying-crypto-with-lean/chapters/ch04-tactics.tex
mrwulf 1f11b8aa4d print-quality pass: the book gets looked at, and the looking becomes a gate
The operator caught what no check had ever tested: nobody had LOOKED at
the rendered pages. A ten-inspector visual audit of all 129 pages (every
page opened as an image) found 40 defects, including didactic
correctness bugs invisible to the text layer:

BAD, fixed:
- ch03 printed WRONG Lean operators: \lean{P /\ Q} lost its backslash
  ('P / Q') and \lean{P \/ Q} lost the operator entirely ('P  Q') —
  TeX ate them inside the non-verbatim macro. Now the unicode ∧/∨ the
  book uses everywhere else.
- ch12: the doubling display overflowed its box border, slicing the
  math; stacked on two lines.
- toolkit Card 6: the headroom-audit display was clipped by the page
  edge; now an align* stack.

UGLY, fixed:
- title page: the 'pyramid motif' at 5% white opacity on near-black
  rendered as smudge artifacts, plus a clipped ∀ glyph in the corner
  — redrawn with solid mixed colors (no transparency), glyph removed;
  the footer's mid-word paragraph gap was a \vspace landing inside
  horizontal mode; fixed with \par
- ch06: both clock diagrams' wrap-around arrows ran counterclockwise,
  retracing over earlier arcs — target position expressed as 12 (one
  revolution) so the arc continues clockwise, landing on 0 (mod 12)
  and 1 (mod 11) correctly
- ch04: two_mul'' printed as two_mul" (quote ligature)
- one-line orphaned box fragments and stranded solution headings
  throughout: bigidea/tryit/pitfall/aha/checkpoint are now unbreakable
  (none exceeds half a page), worked boxes announce '(continued)' after
  a break, \solhead keeps four lines with \Needspace
- --all/--receipt flags printed as one merged dash: \ddash macro
- inline code no longer hyphen-breaks at underscores (codeguards)
- ch09's 2^{...} smudge, glossary margin overflow, ch08 orphaned
  listing line, ch13 command-line layout, three >10pt overfulls

THE STRUCTURAL LESSON, encoded: the two worst clipping bugs had been
announced as 80pt/73pt overfull warnings in every build log and ignored.
check-book.sh now FAILS on any overfull box past 10pt — the machine was
telling us; now it is allowed to stop us.

132 pages; publication-history and README counts synced; every fixed
page re-rendered and verified by eye. Button: ALL GREEN (96 checks).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-08 19:11:09 +02:00

410 lines
19 KiB
TeX
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

\chapter{Tactics: Proving as a Dialogue}
\label{ch:tactics}
\section{From programs to conversations}
Here is the problem with everything you learned in the last chapter: a real
correctness proof for field multiplication --- the theorem this book is
climbing toward --- would, written as a raw proof program, be a program the
size of a small compiler. Nobody writes those by hand. (Chapter~\ref{ch:pat}'s
handcrafted terms were honest work; honest work does not scale.)
Instead, Lean offers \emph{tactic mode}: an interactive dialogue where you
issue commands and Lean builds the proof program for you, step by step,
showing you the remaining work after each move.
You enter the dialogue with the keyword \lean{by}:
\begin{lstlisting}[language=Lean]
theorem and_swap (P Q : Prop) : P ∧ Q → Q ∧ P := by
intro h
constructor
· exact h.2
· exact h.1
\end{lstlisting}
Place your cursor after \lean{by} in the editor and Lean shows the
\textbf{goal state} --- the exact logical situation at that point:
\begin{lstlisting}
P Q : Prop
⊢ P ∧ Q → Q ∧ P
\end{lstlisting}
Everything above the turnstile \(\vdash\) is what you \emph{have} (the
context); the line after it is what you \emph{owe} (the goal). Every tactic
transforms this picture. After \lean{intro h}, the hypothesis moves above the
line; after \lean{constructor}, the goal splits in two. Proving becomes a
game whose board you can always see.
\begin{bigidea}
A tactic proof is a \textbf{recorded conversation with the goal state}. The
skill of proving is not memorizing tactic names --- it is learning to
\emph{read the goal state} and recognize which of a handful of moves makes it
simpler. Below is the core vocabulary; it covers the vast majority of every
proof in the real Ed25519 development.
\end{bigidea}
\begin{center}
\begin{tabular}{@{}lll@{}}
\toprule
\textbf{Tactic} & \textbf{When the goal looks like...} & \textbf{Effect} \\
\midrule
\lean{intro h} & \lean{P → Q}, \ \lean{∀ x, P x} & assume it; name the evidence \\
\lean{exact e} & anything & finish: \lean{e} is a proof of the goal \\
\lean{apply f} & \lean{Q}, given \lean{f : P → Q} & reduce the goal to \lean{P} \\
\lean{constructor} & \lean{P ∧ Q}, \lean{P ↔ Q}, ... & split into pieces \\
\lean{cases h} & have \lean{h : P Q} (or \(\wedge\), \lean{}) & case analysis on \lean{h} \\
\lean{rw [eq]} & contains a rewritable subterm & replace using equation \lean{eq} \\
\lean{simp} & simplifiable clutter & rewrite with a lemma database \\
\lean{induction n} & \lean{∀ n : Nat, ...} & base case + inductive step \\
\lean{rfl} & \lean{a = a} after computation & close by computation \\
\bottomrule
\end{tabular}
\end{center}
\section{Rewriting: equality as a tool}
The workhorse tactic of equational reasoning is \lean{rw} (rewrite). Given a
proven equation, it replaces one side by the other inside your goal:
\begin{lstlisting}[language=Lean]
example (a b : Nat) (h : a = b) : a + a = b + b := by
rw [h] -- goal becomes: b + b = b + b, closed by rfl automatically
\end{lstlisting}
Chains of rewrites read like the two-column proofs of school geometry,
except a machine checks every line. Here is commutativity-and-associativity
shuffling, Mathlib lemmas by name:
\begin{lstlisting}[language=Lean]
example (a b c : Nat) : a + b + c = c + b + a := by
rw [Nat.add_comm a b] -- b + a + c = c + b + a
rw [Nat.add_assoc] -- b + (a + c) = c + b + a
rw [Nat.add_comm a c] -- b + (c + a) = c + b + a
rw [← Nat.add_assoc] -- b + c + a = c + b + a
rw [Nat.add_comm b c] -- done
\end{lstlisting}
The arrow \(\leftarrow\) rewrites right-to-left. Nobody enjoys writing five-line
shuffles like this, which is exactly why Chapter~\ref{ch:automation}
introduces \lean{ring} --- but you must \emph{once} feel the manual version to
understand what the automation is doing on your behalf.
\section{Induction: the tactic that conquers infinity}
Remember the embarrassment of Chapter~\ref{ch:pat}: \lean{0 + n = n} does not
hold by computation. Now we can prove it --- by induction, the proof
technique that inductive types were born for:
\begin{lstlisting}[language=Lean]
theorem zero_add (n : Nat) : 0 + n = n := by
induction n with
| zero => rfl -- 0 + 0 = 0: computes
| succ k ih => rw [Nat.add_succ, ih]
\end{lstlisting}
The \lean{induction} tactic converts a statement about \emph{all} naturals
into two finite obligations: the statement for \lean{zero}, and the statement
for \lean{succ k} \emph{assuming it for} \lean{k} (the induction hypothesis
\lean{ih}). Because every natural number is built from those two
constructors, the two cases cover infinity.
\begin{worked}{the full board of \lean{zero\_add} --- every goal state, by hand}
Reading a finished tactic proof is like reading a chess score without a
board. Here is the same proof \emph{with} the board: every goal state
written out, exactly as the editor would show it. Reproduce this trace on
paper until the transitions feel inevitable --- it is the core skill of
this chapter. We prove \lean{0 + n = n}, where \lean{+} recurses on its
second argument (Chapter~\ref{ch:lean}).
\smallskip
\noindent After \lean{induction n with}, two boards appear.
\emph{Case} \lean{zero}:
\begin{lstlisting}
⊢ 0 + 0 = 0
\end{lstlisting}
The second argument is the literal \lean{0}, so the first defining
equation fires: \lean{0 + 0} \emph{computes} to \lean{0}. Both sides are
now the same term; \lean{rfl} closes the board. (Compare the worked
example in Chapter~\ref{ch:pat}: this is the ``computation sees it'' half.)
\smallskip
\noindent\emph{Case} \lean{succ}: the board now carries an assumption ---
the induction hypothesis:
\begin{lstlisting}
k : Nat
ih : 0 + k = k
⊢ 0 + (k + 1) = k + 1
\end{lstlisting}
The left side is \lean{add 0 (succ k)}; the \emph{second} defining
equation fires and rewrites it to \lean{succ (0 + k)}. That step is the
tactic \lean{rw [Nat.add_succ]} (or one layer of
\lean{simp only [Nat.add]}), and the board becomes:
\begin{lstlisting}
k : Nat
ih : 0 + k = k
⊢ (0 + k) + 1 = k + 1
\end{lstlisting}
Now the subterm \lean{0 + k} is \emph{exactly} the left side of \lean{ih}.
Rewriting with \lean{rw [ih]} replaces it:
\begin{lstlisting}
k : Nat
ih : 0 + k = k
⊢ k + 1 = k + 1
\end{lstlisting}
Identical sides --- \lean{rw} auto-closes with \lean{rfl}. Done.
\smallskip
Two habits to take from this trace. First, at every step ask ``which
defining equation \emph{can} fire, and on which subterm?'' --- that
question, not tactic names, is what drives the proof. Second, notice
where the induction hypothesis earned its keep: computation alone got
stuck at \lean{0 + k} (opaque variable --- Chapter~\ref{ch:pat}'s blocked
reduction), and \lean{ih} is precisely the permission to cross that gap.
An induction proof is computation, plus permission slips at the stuck
points.
\end{worked}
\begin{aha}
Induction is not a new axiom to swallow --- it falls out of the inductive
definition of \lean{Nat} itself. ``Every \lean{Nat} is \lean{zero} or a
\lean{succ}'' \emph{is} the license to do case analysis; recursion on the
structure \emph{is} the induction. Data and proof principle are two views of
the same declaration. This is Curry--Howard paying rent again.
\end{aha}
\section{When you get stuck --- and you will}
No chapter on tactics is honest without this section. The steady march of
examples above is what proving looks like \emph{afterwards}, cleaned up for
print. What it looks like \emph{during} is the next box --- and the
discipline it teaches is worth more than any tactic in the table.
\begin{worked}{a debugging session, reconstructed honestly}
Here is a stuck proof, exactly as it happens to everyone, worked
through with the discipline this chapter preaches. Goal: every number
is at most its double --- \lean{∀ n : Nat, n ≤ 2 * n}. Attempt one:
\begin{lstlisting}[language=Lean]
theorem le_double (n : Nat) : n ≤ 2 * n := by
induction n with
| zero => rfl -- fine: 0 ≤ 0 checks
| succ k ih => rw [ih] -- ERROR
\end{lstlisting}
The error says \lean{rw} failed: \lean{ih} is
\lean{k ≤ 2 * k} --- an \emph{inequality}, and \lean{rw} rewrites with
\emph{equations} only. First lesson banked: the tactic wasn't wrong,
the \emph{move category} was --- inequalities compose by transitivity
and monotonicity, not substitution. Attempt two, reading the goal
state first this time:
\begin{lstlisting}
k : Nat
ih : k ≤ 2 * k
⊢ k + 1 ≤ 2 * (k + 1)
\end{lstlisting}
Plan on paper before touching the keyboard: $2(k+1) = 2k + 2$, and from
\lean{ih}, $k + 1 \le 2k + 1 \le 2k + 2$ ✓ --- a two-step transitivity
chain. That \emph{plan} is provable many ways
(\lean{calc}, \lean{Nat.succ_le_succ} plus lemmas)\dots{} at which
point the honest practitioner pauses: hypotheses and goal are all
\emph{linear arithmetic}. The entire induction was unnecessary:
\begin{lstlisting}[language=Lean]
theorem le_double (n : Nat) : n ≤ 2 * n := by omega
\end{lstlisting}
Third lesson, the big one: getting stuck is often the discovery that
you are proving something in the wrong \emph{fragment} --- and the next
chapter is precisely the field guide to fragments. Keep the session's
order of operations, though, because it generalizes: (1) read what the
error says about the move category; (2) write the goal state and plan
on paper; (3) only then ask whether a decision procedure owns the whole
goal. Ten seconds of (3) before an hour of (2) is the professional's
cheat --- but (2) is what makes you able to survive when (3) says no.
\end{worked}
\begin{pitfall}
When a proof gets stuck, resist the urge to try random tactics --- the
formal-methods equivalent of mashing buttons. The goal state is telling you
something. Three honest questions unstick most situations: (1)~Is the
statement actually true as written --- check a small example with
\lean{\#eval}! (2)~Am I missing a hypothesis --- is there an unstated bound or
nonzero condition? (3)~Is my induction on the right variable? In the real
projects behind this book, ``the proof is stuck'' was, more often than not,
the \emph{statement} being subtly wrong --- a truncated subtraction, a missing
bound. The proof assistant was the messenger.
\end{pitfall}
\section{Structuring real proofs: \texttt{have} and \texttt{calc}}
Big proofs are not flat lists of tactics; they are structured arguments with
named intermediate results. The \lean{have} tactic states and proves a
stepping stone; \lean{calc} lays out a chain of equalities or inequalities
the way you would on a whiteboard:
\begin{lstlisting}[language=Lean]
example (a b : Nat) (h : a = 2 * b) : a + a = 4 * b := by
have h2 : a + a = 2 * a := by rw [Nat.two_mul]
calc a + a = 2 * a := h2
_ = 2 * (2 * b) := by rw [h]
_ = 4 * b := by rw [← Nat.mul_assoc]
\end{lstlisting}
\begin{worked}{planning a \lean{calc} on paper --- backwards from the target}
Professionals do not type a \lean{calc} top-to-bottom and hope; they plan
it on paper, usually \emph{backwards}. Watch the method on the real
reduction identity from the Ed25519 code (Chapter~\ref{ch:modular} proves
the ingredients): given a 256-bit value split as
$x = x_{\mathrm{lo}} + 2^{255} x_{\mathrm{hi}}$, the code returns
$x_{\mathrm{lo}} + 19 x_{\mathrm{hi}}$, and we owe
\[
x_{\mathrm{lo}} + 19\, x_{\mathrm{hi}}
\;\equiv\;
x \pmod{p}.
\]
Plan backwards: the target's right side is $x$, which we only know through
its \emph{definition}, so the last line of the chain will be
``$\ldots = x_{\mathrm{lo}} + 2^{255} x_{\mathrm{hi}} = x$ by the split.''
What could precede it? Something that turns $19$ into $2^{255}$: the fact
$19 \equiv 2^{255}$, i.e.\ the constant identity $2^{255} - 19 = p
\equiv 0$. So the middle step is a congruence rewrite, and the paper plan
reads, bottom to top:
\[
\begin{array}{lll}
\text{(3)} & x_{\mathrm{lo}} + 2^{255} x_{\mathrm{hi}} = x & \text{definition of the split}\\
\text{(2)} & x_{\mathrm{lo}} + 19\, x_{\mathrm{hi}} \equiv x_{\mathrm{lo}} + 2^{255} x_{\mathrm{hi}} & \text{since } 19 \equiv 2^{255} \pmod p\\
\text{(1)} & \text{start: } x_{\mathrm{lo}} + 19\, x_{\mathrm{hi}} & \\
\end{array}
\]
Reverse it, and the \lean{calc} types itself --- each line's justification
was decided \emph{before} any Lean was written. The habit scales: the real
multiplication proof in \code{dalek-ed25519-verified} was planned exactly
this way, as a page of paper algebra whose lines became \lean{have}s. When
you cannot plan the chain on paper, you are not ready to type it; the
proof assistant checks reasoning, it does not supply it.
\end{worked}
This style is not cosmetic. In the verified field arithmetic you will read
later, a single multiplication correctness proof is a \lean{calc} chain
tracking limb products through carries --- dozens of steps, each trivial,
whose \emph{composition} is the theorem. \lean{have} and \lean{calc} are how
proofs stay readable at that scale; they are also how they stay
\emph{maintainable}, because a broken step localizes the damage to one line.
There is one more structuring fact worth knowing early, because it saved the
real project from a crash-course (literally --- see
Chapter~\ref{ch:honesty}): breaking a proof into small named \lean{have}
steps also controls the proof assistant's \emph{memory appetite}. A monolithic
``figure it all out at once'' tactic call over a huge context can consume
gigabytes; ten targeted steps, pennies each, prove the same thing. Structure
is not just style --- it is engineering.
\begin{tryit}
Open \code{exercises/Ch04.lean}. It sets up each theorem with the goal state
drawn in a comment, then asks you to: prove \lean{and_swap} in tactic mode;
prove \lean{zero_add} \emph{without} peeking above; and repair a broken
\lean{calc} chain in which exactly one step is wrong. The third
exercise is secretly the most realistic job training in this book.
\end{tryit}
\section*{Exercises}
\exercise{Prove by induction: \lean{∀ n : Nat, n + 0 = n} and
\lean{∀ n m : Nat, n + succ m = succ (n + m)}. (These are the mirror images
of the definitional equations --- the ones computation gives you for free ---
and together they yield commutativity.)}
\exercise{Using the previous exercise, prove
\lean{∀ n m : Nat, n + m = m + n} by induction on \lean{m}. Write out, in
one prose sentence per case, what each branch of your proof says.}
\exercise{Prove \lean{∀ n : Nat, 2 * n = n + n} twice: once with
\lean{induction}, once with a single \lean{rw} using a Mathlib lemma you find
yourself (search hint: \lean{exact?} asks Lean to search for you).}
\exercise{(Reading) In the goal state
\lean{h : a < 2\textasciicircum{}51 ⊢ a * 19 < 2\textasciicircum{}56}, no induction is needed --- this is pure
arithmetic. Which tactic from the table would you \emph{guess} handles it?
(Answer next chapter; your guess is the point.)}
\section*{Solutions and pathways}
\solutionsintro
\solhead{4.1}
\pathway Before proving anything, predict which facts are \emph{free}:
addition recurses on its second argument, so any statement whose second
argument exhibits a constructor should compute. Both requested statements
--- \lean{n + 0 = n} and \lean{n + succ m = succ (n + m)} --- have
constructor-shaped second arguments (\lean{0} and \lean{succ m}). The
honest content of this exercise is noticing that.
\answer \lean{theorem a : ∀ n : Nat, n + 0 = n := fun n => rfl} --- the
first defining equation. \lean{theorem b : ∀ n m, n + succ m = succ (n+m)
:= fun n m => rfl} --- the second. Neither needs induction; both are the
definitional mirror images of the two facts that \emph{do}
(\lean{0 + n = n}, \lean{succ n + m = succ (n + m)}), which is exactly the
asymmetry the worked example traced. If you reached for \lean{induction}
here, nothing is wrong --- it succeeds --- but recognizing a free fact
saves you thirty seconds a hundred times a day.
\solhead{4.2}
\pathway Induct on \lean{m} (the variable the recursion consumes on the
\emph{right} of \lean{n + m}), so the definitional equations fire on one
side and 4.1's lemmas patch the other. In each case, before touching
tactics, write the goal and ask: which side steps by definition, which
needs a lemma?
\answer
\begin{lstlisting}[language=Lean]
theorem add_comm' (n m : Nat) : n + m = m + n := by
induction m with
| zero => rw [Nat.add_zero, Nat.zero_add]
| succ k ih => rw [Nat.add_succ, Nat.succ_add, ih]
\end{lstlisting}
Prose, one sentence per case, as requested. \emph{Zero case:} ``$n + 0$ is
$n$ by definition, and $0 + n$ is $n$ by the mirror lemma, so both sides
are $n$.'' \emph{Successor case:} ``both sides compute to a successor ---
the left by the defining equation, the right by the mirror lemma --- and
under the \lean{succ} the two sides are the induction hypothesis.'' Note
the architecture: two definitional equations, two mirror lemmas, one
induction --- commutativity is not one fact but a small \emph{ecosystem},
and you have now built all of it from bare constructors.
\solhead{4.3}
\pathway For the induction route, the \lean{succ} case needs
$2(k+1) = (k+1) + (k+1)$ reorganized into the induction hypothesis's shape
plus successors --- expect two rewrites. For the library route, the skill
is \emph{search}: place the cursor on the goal and run \lean{exact?}.
\answer Induction:
\begin{lstlisting}[language=Lean]
theorem two_mul' (n : Nat) : 2 * n = n + n := by
induction n with
| zero => rfl
| succ k ih => rw [Nat.mul_succ, ih, Nat.succ_add, Nat.add_succ]
\end{lstlisting}
Library: \lean{exact?} finds \lean{Nat.two_mul}, giving
\lean{theorem two_mul\textquotesingle\textquotesingle\ (n : Nat) : 2 * n = n + n}
proved by \lean{Nat.two_mul n}. Both
are legitimate craft: the first when you are building the ecosystem, the
second when you are using it. Mathlib has over 200{,}000 lemmas ---
searching \emph{is} a proof technique, and \lean{exact?} is its tactic.
\solhead{4.4}
\pathway The goal mixes a hypothesis bound, multiplication by a
\emph{constant}, and powers of two --- inspect the table: nothing fits
perfectly, which is the setup's point. What you want is a decision
procedure for linear arithmetic.
\answer The intended guess is \lean{simp} or \lean{rw}-with-lemmas ---
and the intended discovery is that neither is pleasant. The actual answer,
one chapter ahead: \lean{omega}, which decides such goals instantly
because $a \cdot 19$ is \emph{linear} in $a$ (19 is a constant). If you
guessed \lean{omega} from the table's absence, better still: you have
started classifying goals by \emph{logical fragment}, which is precisely
the professional habit Chapter~\ref{ch:automation} installs.
\begin{checkpoint}
You should now be able to: read a goal state (context, turnstile, goal);
drive the core tactics \lean{intro}, \lean{exact}, \lean{apply},
\lean{cases}, \lean{rw}, \lean{induction}; structure a multi-step argument
with \lean{have} and \lean{calc}; and --- most importantly --- when stuck,
interrogate the \emph{statement} before blaming the proof. One goal from the
exercises is deliberately still open: the bound
$a \cdot 19 < 2^{56}$, which no tactic in this chapter's table owns. Carry it
with you --- the next chapter opens by handing you the tactic that eats it in
one line.
\end{checkpoint}