38 Why prove
What to know first
Looking back
In chapter 31, what did checks like the back-end cross-check show about defects, and what could they not show?
A. They show that defects exist but not that they do not, because two implementations giving the same answer are not thereby both right. And absence, it said, is the job of proofs. Part X covers those proofs.
The need for this chapter, and its context
mod, the borrowing rules, the determinism of parallelism. This part honestly gathers what each of those claims means, what model it is about, and where it ends. Its first chapter sets out why proofs are needed, and how three layers — proof, exhaustive checking and cross-checking — overlap.By the end of this chapter
The questions this chapter answers
- How do proofs connect to compiler code? Does a theorem change the compiler?
38.1 Tests show existence, proofs show absence#
examples/ch38/average.low
module average .
rem run: mean2 10 20
rem trap: mean2 4294967295 1
rem test
fn mean2 input a u32 . input b u32 . output u32 .
do
return div (add a b) 2 .
end
test small_numbers
do
expect eq (mean2 10 20) 15 .
expect eq (mean2 0 0) 0 .
expect eq (mean2 1000000 3000000) 2000000 .
end
Output
$ lowentc --run mean2 average.low 10 20
mean2(10, 20) = 15
$ lowentc --run mean2 average.low 4294967295 1
== ir diagnostics (1) ==
0:0 E-VM-OVERFLOW: integer overflow at the declared width (use wrap_*/sat_*, or prove the range)
$ lowentc --test average.low
[PASS] small_numbers
== tests: 1 run, 1 passed, 0 FAILED ==
mean2, which computes the mean of two numbers, passes all three tests. But given 4294967295 and 1, add a b overflows u32 and stops. Write a million more tests, and if the author does not pick large numbers, the defect does not show.
This is the nature of testing. A test can show “it is wrong on this input (∃)” but not “it is wrong on no input (∀)”. Checking a million natural numbers is not checking all natural numbers. No amount of ∃ adds up to ∀. This gap is the one reason proofs are needed.
inputs of mean2: two u32 values = about 1.8 × 10^19 pairs
tests (10,20) (0,0) (10^6,3·10^6) three points tried → all right
counterexample (4294967295, 1) a point never tried → overflows, stops
proof ─── every pair ─── speaks about all of them at onceIn Lowent this defect is at least not silent. In C the overflowing sum would wrap and give a wrong mean. Here it stops (chapter 4). And the fixed version is written in a shape that cannot overflow.
examples/ch38/average_fixed.low
module average_fixed .
rem run: mean2 4294967295 1
rem ir
fn mean2 input a u32 . input b u32 . output u32 .
do
let lo u32 be min a b .
let hi u32 be max a b .
return add lo (div (sub hi lo) 2) .
end
Output
$ lowentc --run mean2 average_fixed.low 4294967295 1
mean2(4294967295, 1) = 2147483648
$ lowentc --ir average_fixed.low
-- runtime checks (interval analysis: overflow · division · narrowing) --
1 / 3 removed (33%)
Adding half the difference of the two numbers to the smaller cannot overflow. --ir says it removed one of three checks. The remaining two stay even though they really cannot overflow, because the interval analysis does not know the relation between min and max. The analysis is wrong only in the safe direction — when it cannot prove, it keeps the check.
38.2 Overlapping three layers#
| Layer | What it does | How strong | What it misses |
|---|---|---|---|
| Proof | Every case, by logic | No counterexample can exist within the model | Powerless if the model is wrong |
| Exhaustive checking | Every case of a fixed size, actually run | Certain within that size | Cannot see larger inputs |
| Cross-checking | Two implementations on the same inputs | Catches disagreements | Misses when both are wrong the same way |
Table 38.1 — Three layers of verification
They are overlapped because the three catch different kinds of mistakes. Drawn as who checks what against what:
rules of the spec ── modelled ──▶ Coq model ── proof ──▶ every case in the model
▲
│ exhaustive checking: model vs implementation, small sizes
▼
compiler ─┬─ run on the VM ───┐
└─ emitted as C ────┴─▶ cross-check: different answers = compiler defectProofs look at the model, cross-checks look at the implementation, and exhaustive checking ties the two together. In this repository cross-checks caught defects the proofs missed, and proofs caught defects cross-checks missed. And this part does not mix three words. “Proven” means Coq checked every case, “exhaustively checked” means every case of a fixed size was run, and “sketch” means a person argued it but no machine checked.
A common misconception. A program written in a proven language is correct
mean2 above has no borrowing violations and its overflow is not silent, but it was still a wrong design. Whether a program is correct is answered by contracts, tests and review.38.3 What is proven#
The proof files are all in docs/proofs/coq/. This is the scale counted for this edition.
| Item | Count |
|---|---|
| Coq files | 31 (including 1 checker extraction file) |
| Theorem · Corollary · Lemma | 179 · 15 · 250 |
| Example | 33 |
Qed | 481 |
Admitted · Axiom | 0 |
| Lines of proof script | 10,350 |
| Checkers | Rocq 9.2 (all) · Coq 8.20.1 (all but the two weak-memory files) |
Table 38.2 — Scale of machine-checked proofs
In Coq, Admitted means “assume this is true and move on”, and Axiom means “this is assumed”. If even one such place exists, every theorem in the file relies on that assumption. Both are 0. What is proven and what is not yet is written in docs/proofs/LEDGER.md.
Grouped, it looks like this.
| Group | In one sentence | Chapter |
|---|---|---|
| Tools | The six pieces of mathematics later chapters use — partial orders, lattices, fixed points, induction, abstract interpretation | chapter 39 |
| Numbers | No widening silently changes a value, and the cases where division fails are known exactly | chapter 40 |
| Bounds | Places where checks were removed are proven to be in range | chapter 41 |
| Ownership and borrowing | Programs passing translation have no borrowing violations at run time | chapter 42 |
| Loops | A loop that reaches a fixed point has no borrowing violations however many times it runs | chapter 43 |
| Effects | Effect declarations cover the effects that actually happen, and optimisations are legitimate on top of that | chapter 44 |
| Races and parallelism | Safe code has no data races, and parallel results are bit-for-bit identical to sequential | chapter 45 |
| Weak memory | With the default memory ordering you may think sequentially, and weaker orderings add behaviours | chapter 46 |
| Locks | A CAS spinlock gives mutual exclusion, and ascending lock order does not deadlock | chapter 47 |
| Syntax | One statement is a one-statement block, and whichever closer closes it gives the same tree | chapter 48 |
| Hashes | Comments do not change hashes, and equal encodings mean equal interfaces | chapter 49 |
Table 38.3 — Groups of what is proven, and the chapters of this part
The last chapter (chapter 50) gathers what all of these rely on and what they do not cover.
38.4 Three ways of proving#
You do not need to read proof scripts. But knowing how something was proven gives a sense of how much to trust the theorem.
Invariant preservation. Show that “a property is kept every time the program takes a step”. If it is true at the start (base) and a step carries truth to truth (preservation), it is true however many steps are taken. It is the program version of mathematical induction. The theorems for borrowing rules and loops take this approach.
Reduction to an order structure. Turn the problem into one about an order and answer it with properties of the order. Making “u8 fits safely into u32” into a partial order turns “which type to pick when joining two types” into the well-known notion of a least upper bound. Numeric widening and the weak-memory model take this approach.
Exhaustive checking. Properties not yet proven for all sizes are run for every case within a fixed size. This is not proof, and this part says so.
38.5 Promises this part keeps#
This part keeps three promises.
- It does not mix words. “Proven”, “exhaustively checked” and “sketch” are different strengths. “This language is proven” is often really “a few short cases were run”.
- Every theorem comes with the defect it stops. Theorems whose defect could not be named were left out. A theorem with no value to the reader has no reason to be here.
- This part is not normative. The language is defined by the specification’s clause canon. If this part disagrees with it, this part is wrong.
And the ideas this language rests on, reduced to six lines, are these. All six are either backed by mathematics or written down as not backed — that distinction is the purpose of this part.
- Make wrong programs impossible to write. Making them inexpressible is cheaper than catching them (chapter 42).
- Answer “I don’t know” when you don’t know. Failure is a value —
optionorresult— not a hidden exception. - Costs must be visible. Allocation, side effects and copies are all written in the source (chapter 44).
- When in doubt, keep the check. Checks are removed only when there is a proof (chapter 41).
- Two implementations must give the same answer. If the VM and native code differ, it is a compiler defect (chapter 31).
- Write down what was not done. Assumptions, limits and what is unproven are recorded (chapter 50).
Q. How do proofs connect to compiler code? Does a theorem change the compiler?
A. Theorems decide what the compiler must reject. That the condition for uN ⊑ iM in the widening table is N < M (strict), and that blocking only one shape — a write borrow living across a loop meeting an owner write in the body — is enough, came from theorems. The other direction exists too. Turning a defect the implementation hit (“laundering a borrow through a function”) into a theorem means the proof breaks the moment anyone removes that check. Stories are forgotten, but theorems remain (chapter 42).
theorem ────────────────▶ decides which shapes the compiler must reject
e.g. the condition for uN ⊑ iM is N < M (strict)
a defect the compiler hit ▶ turned into a theorem
e.g. laundering a borrow through a function
remove that check and the proof breaksRecap
Admitted or Axiom. Proofs are built by invariant preservation, reduction to order structures and exhaustive checking, and they decide what the compiler must reject. Proofs are about the language’s rules, not a guarantee that your program is correct.