Chapter 12 — Loops in practice¶
Map Part I’s invariant story to CppVerify syntax.
Example skeleton¶
while (i < n)
cv::invariant(0 <= i && i <= n)
cv::decreases(n - i)
{
// ...
i++;
}
Place cppverify::invariant and cppverify::decreases after the loop header’s closing ).
What the verifier actually checks¶
A loop is verified modularly: the verifier does not unroll it (bounded
model checking, --backend=bmc, does; see Chapter 17). Instead it
discharges three obligations from the invariant I and measure D:
Establishment.
Iholds when the loop is first reached (from the concrete state just before the loop).Preservation. Starting from an arbitrary state that satisfies
Iand the loop condition, one iteration of the body re-establishesI. The state is arbitrary — the verifier forgets (“havocs”) every variable the loop writes and assumes onlyI. This is what makes the proof cover all iterations at once.Termination. Under the same arbitrary state, the measure is non-negative before the iteration and strictly smaller after it:
0 <= D_oldandD_new < D_old.
After a normal exit the verifier knows I && !cond, plus the unchanged
values of everything the loop does not write. A break or return leaves
from its own state. A fact about what the loop writes has to be in the
invariant.
do loops¶
A contracted do loop places its clauses after the trailing condition and
before the semicolon:
do {
i = i + 1;
} while (i < n)
cv::invariant(i >= 1 && i <= n)
cv::decreases(n - i);
The first body execution is mandatory. CppVerify checks it from the concrete
incoming state and establishes the invariant after that execution. It then
uses the same modular preservation and termination rule as while for all
later iterations. cppverify::old(expr) in a loop invariant always reads the enclosing
function’s entry state; it is not a previous-iteration operator. A function
local has no entry-state value and is rejected inside cppverify::old(...).
A return in a loop body checks the postcondition in its own state, in
every kind of loop. break and continue in a do loop, and ghost code
that leaves a loop, are rejected.
Inductive invariants and machine integers¶
Because preservation starts from an arbitrary I-state, the invariant must
be inductive: strong enough to re-prove itself. The classic trap is an
unbounded accumulator under honest machine integers:
// REJECTED: s >= 0 is not inductive.
int sum(int n) cv::pre(n >= 0 && n <= 1000) cv::post(cv::result >= 0) {
int s = 0, i = 0;
while (i < n)
cv::invariant(i >= 0 && i <= n && s >= 0) // too weak
cv::decreases(n - i)
{ s = s + 1; i = i + 1; }
return s;
}
From an arbitrary s satisfying s >= 0 the verifier may pick
s == INT_MAX; then s + 1 overflows int, which is undefined
behavior, and the verifier reports the overflow obligation at s + 1.
CppVerify reasons about int as a real 32-bit type, so this is not a false
alarm. The fix is to bound the accumulator
so it provably cannot overflow:
int sum_bounded(int n) cv::pre(n >= 0 && n <= 1000) cv::post(cv::result >= 0) {
int s = 0, i = 0;
while (i < n)
cv::invariant(i >= 0 && i <= n && s == i) // s tracks i, bounded by n
cv::decreases(n - i)
{ s = s + 1; i = i + 1; }
return s;
}
Every loop needs cppverify::decreases¶
Verification is total correctness, as in Verus. A loop without cppverify::decreases
leaves its function Unresolved with reason decreases.missing, unless
its condition is the constant false (the do { ... } while (0) of a macro):
such a loop never repeats. When a
loop really may run forever (an event loop, a server), say so with
cppverify::decreases(*): the function is then proved for the executions that
terminate and reported Verified ... [partial], and so is every caller.
int wait_for(const int *flag)
cv::pre(valid(flag, 1))
cv::post(cv::result == 1)
{
while (*flag != 1)
cv::decreases(*)
{
}
return *flag;
}
What a loop does not write¶
A loop writes only the objects its stores and calls reach, so every other
object keeps its value with no invariant saying so. To narrow the frame inside
one object, list what the loop writes with cppverify::modifies after its invariants
(ACSL’s loop assigns). The footprints are read in each iteration’s state:
void zero_prefix(int *a, int n)
cv::pre(valid(a, n + 1) && n >= 1 && n <= 1000)
cv::modifies(*a)
cv::post(a[n] == cv::old(a[n]))
{
for (int i = 0; i < n; i = i + 1)
cv::invariant(0 <= i && i <= n)
cv::modifies(a[0 : n])
cv::decreases(n - i)
{
a[i] = 0;
}
}
Ghost variables¶
cppverify::ghost T x = e; declares a variable that exists only for verification and
lives in the function’s scope, so loop invariants may name it. It can snapshot
a value from before the loop:
void add_all(int *a, int n, int d)
cv::pre(valid(a, n) && n >= 0 && n <= 1000 && d >= 0 && d <= 1000)
cv::pre(cv::forall(k, 0, n, 0 <= a[k] && a[k] <= 1000))
cv::modifies(*a)
cv::post(cv::forall(k, 0, n, a[k] == cv::old(a[k]) + d))
{
cv::ghost int total = n;
for (int i = 0; i < n; i = i + 1)
cv::invariant(0 <= i && i <= n && total == n)
cv::invariant(cv::forall(k, 0, i, a[k] == cv::old(a[k]) + d))
cv::invariant(cv::forall(k, i, n, a[k] == cv::old(a[k])))
cv::decreases(total - i)
{
a[i] = a[i] + d;
}
}
Lexicographic measures¶
When no single quantity falls every iteration, pass a comma-separated tuple —
cppverify::decreases(a, b) is ordered lexicographically. Each iteration the tuple must
strictly decrease in lex order: some component drops while every earlier
component is unchanged. The component that drops must be non-negative before
the step; the others may be anything. This is what
proves nested counters and Ackermann-style recursion terminate:
while (i > 0 || j > 0)
cv::invariant(i >= 0 && j >= 0)
cv::decreases(i, j) // j falls while i is fixed; when j resets, i drops
{
if (j > 0) { j = j - 1; }
else { i = i - 1; j = b; }
}
The same tuple syntax works on a function’s cppverify::decreases clause, for recursive
cppverify::spec/cppverify::proof functions whose arguments shrink lexicographically.
Loops after an early return¶
A loop that follows an early return verifies normally — its obligations are
only checked on the path that actually reaches it:
int f(int n) cv::pre(n >= 0 && n <= 50) cv::post(cv::result >= 0) {
if (n == 0) return 0; // early exit
int i = 0;
while (i < n)
cv::invariant(0 <= i && i <= n) // not checked on the n == 0 path
cv::decreases(n - i)
{ i = i + 1; }
return i;
}
Common failure modes¶
Symptom |
Likely fix |
|---|---|
Establishment fails |
Weaken invariant or strengthen pre before loop |
Preservation fails |
Strengthen invariant to include facts needed after body |
Termination fails |
Fix |
for loops use the same clause placement after the for (...) part.
Quantified properties¶
A loop usually establishes a property over a range. Express that with the bounded quantifiers
cppverify::forall(i, lo, hi, e) and cppverify::exists(i, lo, hi, e) — i ranges over [lo, hi), and the
solver chooses the terms that instantiate the quantifier unless a cppverify::trigger
names one (Chapter 13 — Spec and proof functions):
bool nonneg_prefix(int n)
cv::pre(n >= 0 && n <= 8)
cv::post(cv::result == cv::forall(i, 0, n, i >= 0))
{ return true; }
Quantifiers are valid anywhere a contract expression is — cppverify::pre, cppverify::post, cppverify::invariant — which
is how a loop invariant talks about “everything processed so far”. Quantifiers
without bounds, cppverify::forall(i, e), range over all integers (Chapter 13).
Recursive spec functions in an invariant¶
A common pattern relates the accumulator to a recursive cppverify::spec function — the
loop computes what the spec defines. The invariant acc == count(i - 1)
below says “after processing i - 1 elements, acc equals the spec’s
value”:
cv::spec int count(int n)
cv::decreases(n)
{ if (n <= 0) return 0; return 1 + count(n - 1); }
int compute_count(int n)
cv::pre(n >= 0 && n <= 10)
cv::post(cv::result == count(n))
{
int acc = 0, i = 1;
while (i <= n)
cv::invariant(i >= 1 && i <= n + 1 && acc == count(i - 1))
cv::decreases(n - i + 1)
{
acc = acc + 1;
i = i + 1;
}
return acc;
}
The solver unfolds count once at each application the proof names, so
preservation sees count(i) == 1 + count(i - 1) at the symbolic index i
without help. A spec that multiplies works the same way:
cv::spec int factorial(int n)
cv::decreases(n)
{ return n <= 0 ? 1 : n * factorial(n - 1); }
int fact_loop(int n)
cv::pre(n >= 0 && n <= 12)
cv::post(cv::result == factorial(n))
{
int acc = 1, i = 1;
while (i <= n)
cv::invariant(i >= 1 && i <= n + 1 && acc == factorial(i - 1))
cv::decreases(n - i + 1)
{
acc = acc * i;
i = i + 1;
}
return acc;
}
When a step needs more than one unfolding, raise the depth with
cppverify::reveal_with_fuel in a ghost block, or state the step as a lemma (a
cppverify::proof function whose postcondition is the fact) and call it from the
loop body. Chapter 13 shows both.