1.1. Why Verify Software Formally?
Software controls aircraft, medical devices, financial systems, and communication networks. Errors in such systems cost money and lives. The standard way to find errors is testing, and testing examines finitely many executions of a program that admits infinitely many. Dijkstra stated the limitation precisely.E. W. Dijkstra, Notes on Structured Programming, EWD249, Technological University Eindhoven, 1970.
Program testing can be used to show the presence of bugs, but never to show their absence!
Formal verification takes the complementary route. We state a property of a program as a mathematical proposition and prove that every execution satisfies it. The proof covers all inputs at once, which no finite test suite achieves.
Proofs about real programs grow large, so we delegate their checking to a machine. A proof assistant is a program that checks every step of a proof with respect to the rules of a formal logic, and that helps the user construct the proof interactively. Lean, Rocq (formerly Coq), Isabelle/HOL, and Agda are proof assistants in current use. Landmark results include the verification of the seL4 operating-system microkernelG. Klein et al., seL4: Formal Verification of an OS Kernel, Proceedings of SOSP 2009, pp. 207–220. and of the CompCert optimizing C compiler.X. Leroy, Formal Verification of a Realistic Compiler, Communications of the ACM 52(7), 2009, pp. 107–115.
Language models now write a growing share of code. A model produces plausible text, and plausible is not the same as correct. Generated code can invoke functions that do not exist, handle only the cases that its prompt suggests, or drift from the stated requirement in ways that survive code review. The literature calls this failure mode hallucination.
Formal verification, in particular when automated, changes how we can trust such code.L. de Moura, The Lean Programming Language and Theorem Prover, ETAPS 2026. When generated code arrives with a machine-checked proof that it satisfies its specification, the proof assistant checks the proof independently of how the code came to be, so hallucinated or simply wrong code cannot pass. The burden of correctness moves from reading the code to writing the right specification. The techniques of this course apply unchanged to generated code, and the automation of the final lectures, with the mvcgen tactic, points toward verification at the pace of code generation.
In this course we use Lean. Lean is at once a programming language and a proof assistant, so we can write a program and prove its properties in the same system. Lectures 1 and 2 review classical logic while introducing Lean's proof language, following HTPIwL. Lectures 3 to 8 follow LoVe1 through interactive proving, functional programming, and inductive predicates. The final block treats the semantics of an imperative language, Hoare logic, and practical verification with the mvcgen tactic.
Figure 1.1 shows the components of Lean that the course exercises. The parser reads the text of a .lean file into syntax trees, and the macro expander unfolds the notations that libraries and user code define. The elaborator turns those trees into terms of the core language, and it does the work that the surface syntax leaves implicit, inferring omitted arguments, resolving type class instances, and running tactics. Tactics are themselves Lean programs, and they build terms rather than certificates of their own correctness. The kernel rechecks the finished term with respect to the rules of dependent type theory, so a tactic that produces a wrong term fails here, and only the kernel belongs to the trusted base. The compiler takes the same terms to native code, which is what #eval runs. The libraries supply notations, instances and lemmas to every stage above the kernel.
Figure 1.1. The main components of Lean.
These components serve any Lean development, and the course uses them for one specific end. The imperative language of the final lectures, its semantics and its Hoare logic are ordinary Lean definitions, the verification conditions are goals2 that tactics discharge, and the kernel checks the result as it checks any other proof. The objective of this course is to show how to use Lean to verify imperative programs formally, and Figure 1.2 describes an architecture for it.
A program and its specification form a Hoare triple. The big-step operational semantics gives the triple its meaning. The mvcgen tactic generates the verification conditions, which are purely logical goals. Tactic proofs discharge them, and the Lean kernel checks every proof.
Figure 1.2. Architecture of a program verifier in Lean.
1. LoVe collects the Lean files that accompany the Hitchhiker's Guide to Logical Verification, 2026 edition. Its support library LoVelib is not published as a Lake package, so these notes keep a copy of it under Lectures/LoVe/, together with its BSD 3-clause licence. The copy is verbatim except for the attribute @[reducible] on Set.PartialOrder, which the definition linter of Lean v4.32.0 requires and the original, written for Lean v4.24.0, does not carry.
2. A goal is what remains to be proved at a point in a proof. Lean displays it as the hypotheses in scope, one per line, followed by the symbol ⊢ and the proposition to prove. Every tactic either closes a goal or replaces it with simpler ones, and the proof ends when none remain. Proving Q ∧ P from a hypothesis h : P ∧ Q, for instance, starts from the goal
P Q : Prop h : P ∧ Q ⊢ Q ∧ P
which the tactic exact ⟨h.right, h.left⟩ closes. Section 1.8 returns to the subject in detail.