Chapter 6 — Ghost reasoning and lemmas

Some facts are true but not part of the executable algorithm. Ghost (or proof-only) code exists only to help the verifier — lemmas, case splits, and internal assertions.

Proof obligations inside a function

While proving a function, you may need an intermediate fact:

// We know a <= b and b <= c from earlier lines
// We need a <= c before the return

A proof step (cppverify::check in CppVerify) asks the verifier to show the formula at that point. It is not a runtime check.

Lemmas as separate functions

Large proofs factor into lemma functions:

lemma monotonic(i, j):
  requires i <= j
  ensures f(i) <= f(j)

The lemma is verified once; call sites use its contract without repeating the induction.

Ghost variables and code

Ghost variables track proof-only state (counters in a proof, snapshots). Ghost blocks group steps that must not appear in compiled binaries.

Zero runtime cost is non-negotiable for production verification: CppVerify strips ghost constructs in CodeGen (Part II).

Erasure also imposes an isolation rule: ghost code may update only ghost-local variables and their direct fields. It cannot mutate executable locals, global state, or pointees; call executable functions; or return from the enclosing function. Ghost loops require a termination measure, so an erased infinite loop cannot suppress runtime code only in the verifier.

Axioms vs executable definitions

Spec (mathematical) functions define symbols used in contracts — like fibo(n) as a recurrence. Their bodies are definitions for the verifier, not instructions the CPU runs.

Proof functions are ghost programs whose only job is to establish pre ==> post. They can mutate local proof state and call other proof functions, but cannot write executable memory or invoke executable functions.

Opacity and fuel

A recursive spec’s defining axiom — ∀n. fibo(n) = fibo(n-1) + fibo(n-2) — is dangerous to hand an SMT solver unrestricted: matching the right-hand side reintroduces the symbol and the instantiation never stops (a matching loop). The remedy is to unfold a recursive spec only a bounded number of times: once by default (fuel 1), and deeper only where a proof asks for it:

  • fuel n — unfold the definition n times along this obligation;

  • reveal / hide — turn a spec transparent or opaque in a whole function.

This is a knob on automation, not on truth: no setting makes a false claim verify or a true one fail. It does change what a proof can reach: with a definition hidden, or unfolded too few times, a claim may stay unresolved, and more fuel can settle it. Part II maps these to cppverify::reveal_with_fuel, cppverify::reveal, and cppverify::hide.