Chapter 13 — Spec and proof functions¶
Spec functions (mathematical)¶
cv::spec int fibo(int n)
cv::decreases(n)
{
if (n <= 1) return n;
return fibo(n - 1) + fibo(n - 2);
}
Use in contracts:
int client(int n)
cv::pre(n >= 0 && n <= 40)
cv::post(cv::result == fibo(n));
fibo is not compiled. Integers are mathematical (unbounded) inside cppverify::spec.
Because it has no compiled form, a spec can be called only from contracts, ghost
code, and other spec or proof functions; return fibo(n); in an executable
body is a compile error. In ghost or proof code, int f = fibo(n); converts
the unbounded value to int, and the verifier checks that it fits.
Proof functions (lemmas)¶
cv::proof void fibo_nonneg(int n)
cv::pre(n >= 0)
cv::post(fibo(n) >= 0)
cv::decreases(n)
{
if (n >= 2) {
fibo_nonneg(n - 1);
fibo_nonneg(n - 2);
}
}
cv::proof void lemma(int i, int j)
cv::pre(i <= j)
cv::post(fibo(i) <= fibo(j))
cv::decreases(j - i)
{
if (i < j) {
lemma(i, j - 1); // fibo(i) <= fibo(j - 1)
if (j >= 2)
fibo_nonneg(j - 2); // so fibo(j - 1) <= fibo(j)
}
}
A lemma’s body is its proof: each call is the induction hypothesis at a
smaller measure, or another lemma. Call lemmas from cppverify::ghost { ... } blocks
in executable functions.
Proof functions are isolated from runtime state: they may update local proof
variables, but cannot write pointees/globals or call executable functions.
Every proof-only loop needs cppverify::decreases. The same erased-state rules apply to
inline cppverify::ghost blocks, which also cannot return from their executable
enclosing function.
Soft preconditions (cppverify::recommends)¶
A cppverify::spec function stays total, but cppverify::recommends flags likely misuse. It generates no
call-site obligation; when a function that calls the spec fails verification,
the verifier warns if one of its calls may violate it:
cv::spec int safe_div(int a, int b)
cv::recommends(b != 0)
{ return a / b; }
Opacity: fuel, cppverify::reveal, cppverify::hide¶
A recursive cppverify::spec is finitely transparent by default (fuel 1), so its
defining equation cannot send Z3 into a matching loop.
cppverify::reveal_with_fuel(f, n) raises the depth when a proof needs more.
cppverify::reveal / cppverify::hide make a spec transparent / opaque in the
whole function, wherever the statement appears:
cv::spec int triple(int x) { return 3 * x; }
int f(int x)
cv::pre(x >= 0 && x <= 10)
cv::post(cv::result == triple(x))
{
cv::ghost { cv::reveal(triple); } // cv::reveal_with_fuel(f, n) for recursive specs
return x + x + x;
}
int g(int x)
cv::pre(x >= 0 && x <= 10)
cv::post(cv::result == x + x + x)
{
cv::ghost { cv::hide(triple); } // keep triple opaque; prove without unfolding it
return x + x + x;
}
Use the smallest fuel that exposes the recurrence needed at the current proof
site. If a proof function has already supplied a finite table or step lemma,
cppverify::hide(f) can suppress irrelevant recursive equations while the imported
postconditions remain available. Hiding a spec keeps its definition out of
proofs, never out of the program’s meaning: a counterexample must still fail
under the real definition, and an obligation that only the hidden definition
would settle is reported as spec.hidden. The full-range factorial and Fibonacci
acceptance tests use this pattern to retain exact mathematical specifications
without making solver time depend on unnecessary unfolding.
See also Chapter 17 — Backends, modular calls, and debugging.
Termination of recursive specs¶
A recursive cppverify::spec needs cppverify::decreases, and every recursive call must lower
the measure: a single measure must stay nonnegative and strictly decrease; for
a tuple, the first component that changes must stay nonnegative and decrease.
A spec’s definition becomes a fact only once the spec is known to terminate,
so the termination proof cannot use it. Each recursive call must decrease the
measure whatever the spec’s other calls return:
cv::spec int ack(int m, int n)
cv::decreases(m, n) // verified: m decides the outer call
{
return m <= 0 ? n + 1
: n <= 0 ? ack(m - 1, 1)
: ack(m - 1, ack(m, n - 1));
}
cv::spec int nested(int n)
cv::decreases(n) // unresolved: needs nested(n - 1) < n,
{ // a fact about nested itself
return n <= 0 ? 0 : 1 + nested(nested(n - 1));
}
A recursive call inside cppverify::forall, cppverify::exists, or cppverify::choose must
decrease the measure for every value of the bound variable. A call in the body
of a cppverify::choose is a call of the function the choose is written in, so it
is checked like any other:
cv::spec int stuck(int n)
cv::decreases(n)
{
return n <= 0 ? 0 : cv::choose(k, k == stuck(n) + 1);
}
int forty_two(int x)
cv::post(cv::result == 42)
{
cv::check(stuck(x) >= 0 || stuck(x) < 0);
if (x == 1)
return 0;
return 42;
}
stuck.cpp:4:42: error: spec decreases failed: stuck [backend=z3] [reason=counterexample] (n [type=math-i32] = 1)
Unresolved: forty_two [backend=z3] [reason=spec.termination] (relies on the definition of stuck, whose termination is not established)
At n = 1 the definition says that stuck(1) equals stuck(1) + 1, a
contradiction, and a proof that used it could prove anything. Before 0.2 the
call inside the choose was not seen as a recursive call: stuck was
accepted, and forty_two verified although it returns 0 at x = 1.
A choice’s body counts where the choice is written in a spec’s other checks too: the applications inside a choose get the same facts as the others, so a postcondition proved by induction may use the hypothesis at a call written inside a choose:
cv::spec int bit(int n)
cv::decreases(n)
cv::post(n < 0 || cv::result == cv::choose(k, 0, 2, k == n % 2))
{
return n <= 1 ? n : cv::choose(j, 0, 2, j == bit(n - 2));
}
Verified: spec decreases and post: bit
For n > 1, the hypothesis at bit(n - 2) says that it is the parity of
n - 2, 0 or 1. So the outer choice has a witness and is that
value, which is also the parity of n. A cppverify::reads check
follows the loads of a choose’s body in the same way
(Ghost, spec, and proofs).
Spec functions may call each other in a cycle when they share a measure of the same length and every call within the cycle lowers it; the termination proof treats all of them as opaque:
cv::spec bool is_odd(int n);
cv::spec bool is_even(int n) cv::decreases(n) { return n <= 0 ? n == 0 : is_odd(n - 1); }
cv::spec bool is_odd(int n) cv::decreases(n) { return n <= 0 ? false : is_even(n - 1); }
A spec whose termination is not established has no definition, so a proof
that relies on it is reported as not verified with reason
spec.termination rather than as verified.
Proof functions may also recurse through each other, and so may executable functions, under the same rule, with calls inside the cycle reasoned about through the callees’ contracts. A proof function cannot call an executable one.
A spec takes no cppverify::pre, cppverify::modifies, or cppverify::aliases: it is defined for every
argument, and cppverify::recommends states its intended domain.
A spec’s cppverify::post is a property of its value at every argument, proved
together with its termination by well-founded induction on the measure: a
recursive call may assume the post only where its measure is lower than the
caller’s. That is what lets a nested call terminate:
cv::spec int g(int n)
cv::decreases(n)
cv::post(cv::result >= 0 && cv::result <= (n < 0 ? 0 : n))
{
return n <= 0 ? 0 : g(g(n - 1)); // g(n - 1) < n by the post
}
cv::spec int m91(int n) // McCarthy's 91 function
cv::decreases(n > 100 ? 0 : 101 - n)
cv::post(n > 100 ? cv::result == n - 10 : cv::result == 91)
{
return n > 100 ? n - 10 : m91(m91(n + 11));
}
Every application of the spec then carries its post, so m91(n) == 91
for n <= 100 needs no unfolding. A post that fails is reported; a proof
that relied on it is reported with reason spec.post, or
spec.termination for a recursive spec, whose termination may have used
it. A non-recursive spec is unfolded at its calls, so its post matters where
it is hidden.
A post holds at the applications a proof names and also at those inside the
unfoldings it receives, as Dafny’s function postconditions do. With the post
on the spec, the monotonicity lemma above needs no separate lemma for
nonnegativity: fib(j - 1) <= fib(j) needs fib(j - 2) >= 0, and only
the unfolding of fib(j) names fib(j - 2).
cv::spec int fib(int n)
cv::decreases(n)
cv::post(cv::result >= 0)
{
return n <= 0 ? 0 : n == 1 ? 1 : fib(n - 2) + fib(n - 1);
}
cv::proof void fib_monotone(int i, int j)
cv::pre(i <= j)
cv::post(fib(i) <= fib(j))
cv::decreases(j - i)
{
if (i < j)
fib_monotone(i, j - 1);
}
cppverify::when(c) restricts a spec’s definition to the domain c. Termination is
checked under c only, and outside it the spec’s value is unspecified:
nothing about it can be proved, and a counterexample that depends on it is
reported as counterexample.unchecked. A post is required, and assumed,
only within the domain:
cv::spec int log2(int n)
cv::when(n >= 1)
cv::decreases(n)
cv::post(cv::result >= 0 && cv::result < n)
{
return n == 1 ? 0 : 1 + log2(n / 2); // no base case needed for n <= 0
}
Proving properties of recursive specs¶
The verifier gives the solver each spec’s defining equation at the call sites in a function, unfolded as deep as the fuel allows. Beyond that, it settles five kinds of goal without help:
Closed applications such as
sum(15000) == 112507500orfib(90) == ...are computed: the solver receives the defining equation at every argument the evaluation reaches, up to 20000 of them.Bounded domains. For
cppverify::pre(n >= 0 && n <= 200), the solver first proves the domain bounded, then receives the definitions across it and checks every value. Domains of up to about a thousand values are settled this way.Counterexamples. A failure is reported only after the counterexample is checked against the true definitions, so it never relies on a value the solver invented for an unfolded call.
Inductions. When no finite unfolding settles a goal, the verifier tries to prove it by induction on its own (below).
Counterexamples too large to compute. When a counterexample needs a spec value too large to compute, the verifier tries to confirm it by a proof at that input (below).
Automatic induction¶
Unfolding a recursive spec a few levels settles claims about a few values.
A claim about every n, such as fibo(n) >= n - 1 for all n >= 3,
needs induction: show it at n assuming it at smaller values. When
unfolding leaves a claim unresolved (spec.fuel), the verifier tries
inductions by itself before reporting it:
Following a recursive spec that the claim applies (shown as
following fibo): the claim is assumed at every value smaller in the spec’scppverify::decreasesmeasure, and in particular at the values the spec’s own recursion visits.fibo(n)callsfibo(n - 1)andfibo(n - 2), so the claim is assumed atn - 1andn - 2. The values are found by walking the definition: through the other specs of a mutual recursion, and for a call insidecppverify::forallorcppverify::exists, at every value the quantifier ranges over.On an integer variable (shown as
on n): strong induction, the claim assumed at every smaller nonnegative value ofn.
Everything else stays fixed, and the assumption is the claim exactly as the
function states it, so these never prove a false claim: the measure cannot
decrease forever, which is what the cppverify::decreases check of each spec shows.
Only proofs are taken from these attempts. None of the lemmas below needs a
body:
cv::spec int fibo(int n)
cv::decreases(n)
cv::post(cv::result >= 0)
{
if (n <= 0) return 0;
if (n == 1) return 1;
return fibo(n - 2) + fibo(n - 1);
}
// The claim at n - 1 and n - 2, as fibo recurses.
cv::proof void grows(int n)
cv::pre(n >= 3)
cv::post(fibo(n) >= n - 1)
{
}
cv::spec int total(seq s)
cv::decreases(s.len())
{
return s.len() <= 0 ? 0 : total(s.subrange(0, s.len() - 1)) + s[s.len() - 1];
}
// The claim at the shorter sequence total recurses on.
cv::proof void total_nonneg(seq s)
cv::pre(cv::forall(k, 0, s.len(), s[k] >= 0))
cv::post(total(s) >= 0)
{
}
// Mutual recursion: even reaches even(n - 2) through odd.
cv::spec bool odd(int n);
cv::spec bool even(int n) cv::decreases(n) { return n <= 0 ? true : odd(n - 1); }
cv::spec bool odd(int n) cv::decreases(n) { return n <= 0 ? false : even(n - 1); }
cv::proof void even_mod(int n)
cv::pre(n >= 0)
cv::post(even(n) == (n % 2 == 0))
{
}
// A call under forall: the claim at every k the forall ranges over.
cv::spec bool good(int n) cv::decreases(n) { return n <= 0 || cv::forall(k, 0, n, good(k)); }
cv::proof void all_good(int n)
cv::post(good(n))
{
}
Verified: grows [backend=z3] [by induction following fibo]
Verified: total_nonneg [backend=z3] [by induction following total]
Verified: even_mod [backend=z3] [by induction following even]
Verified: all_good [backend=z3] [by induction following good]
The suffix [by induction following fibo] says which induction proved
the claim (JSON "induction": "following fibo"). Assignments in a body are
substituted first, so return n * (n + 1); with
cppverify::post(cppverify::result == 2 * sum(n)) is proved the same way.
Inductions cost time only where a claim would otherwise stay unresolved:
verified and failed claims never try one. Each attempt gets a sixth of the
query’s timeout (--timeout, or --collection-timeout for a query over
collections), but at least two seconds (all of the timeout when that is
shorter, and five seconds when there is no timeout), and the attempts for
one claim stop after two such shares. With the default 30-second timeout an
unresolved claim takes at most 10 seconds more.
When no induction finds the proof. The automatic inductions assume the claim itself. When the recursion changes another argument, the claim at the smaller value is not the statement the step needs:
cv::spec int accumulate(int n, int acc)
cv::decreases(n)
{
return n <= 0 ? acc : accumulate(n - 1, acc + n);
}
cv::spec int sum(int n) cv::decreases(n) { return n <= 0 ? 0 : n + sum(n - 1); }
cv::proof void accumulated(int n)
cv::pre(n >= 0 && n <= 25000)
cv::post(accumulate(n, 0) == sum(n))
{
}
Unresolved: accumulated [backend=z3] [reason=spec.fuel] (proof obligation ...: every counterexample found needs accumulate, sum unfolded at ever larger arguments (the last one needed 21887 unfoldings), so no finite unfolding settles it; prove it by induction in a proof function whose decreases clause shrinks on each recursive call, and call that lemma here; induction following accumulate and induction following sum and induction on n did not prove it; a proof by induction following sum could start from the body 'if (n > 0 && n - 1 >= 0 && n - 1 <= 25000) { accumulated(n - 1); }', with cppverify::decreases(n))
The message says, in order:
what the solver tried: every counterexample it proposed needed the specs at ever larger arguments, so no amount of unfolding settles the claim;
which inductions the verifier tried, none of which proved it;
a body to start a proof by induction from: a call of the function itself at each value the recursion reaches, under the condition that reaches it and the function’s precondition there, with the
cppverify::decreasesclause to add.
Here the suggested body does not work as it stands: accumulate(n, 0)
continues as accumulate(n - 1, n), so the claim at n - 1, which is
about accumulate(n - 1, 0), does not help. The step needs a stronger
statement, about every acc. Prove that as a lemma whose cppverify::decreases
clause shrinks on each recursive call, and call it:
cv::spec int accumulate(int n, int acc)
cv::decreases(n)
{
return n <= 0 ? acc : accumulate(n - 1, acc + n);
}
cv::spec int sum(int n) cv::decreases(n) { return n <= 0 ? 0 : n + sum(n - 1); }
cv::proof void accumulate_adds(int n, long long acc)
cv::pre(n >= 0 && acc >= 0 && acc + n * n <= 1000000000000)
cv::post(accumulate(n, acc) == acc + sum(n))
cv::decreases(n)
{
if (n > 0)
accumulate_adds(n - 1, acc + n); // the claim at n - 1, for another acc
}
cv::proof void accumulated(int n)
cv::pre(n >= 0 && n <= 25000)
cv::post(accumulate(n, 0) == sum(n))
{
accumulate_adds(n, 0);
}
Verified: accumulate_adds [backend=z3]
Verified: accumulated [backend=z3]
The recursive call is legal only at a smaller measure, and its postcondition
is the induction hypothesis. A proof function computes with machine
integers, so acc + n is checked for overflow; the precondition bounds it.
Executable code calls a lemma from a cppverify::ghost block, and a
cppverify::check is proved where it stands and assumed afterwards, so a
chain of assertions in a ghost block works as a step-by-step proof:
void check(int n)
cv::pre(n >= 0 && n <= 25000)
{
cv::ghost {
accumulate_adds(n, 0);
cv::check(accumulate(n, 0) == sum(n));
}
}
Counterexamples too large to compute¶
A failure is reported only after its counterexample is checked against the
true definitions of the specs, so that it never rests on a value the solver
made up. Checking means computing the specs at the counterexample’s input,
and some values are too large for that: pow2(1000000000) has a billion
bits, and fibo(1000000000) takes a billion steps. The verifier then tries
to prove, at that input, that the claim fails. With the input fixed, it may
use only facts that are proved: the postconditions of specs, and the
contracts of cppverify::proof functions whose own verification succeeded. If the
solver proves it, the counterexample is confirmed:
cv::spec int pow2(int n)
cv::decreases(n)
cv::post(cv::result >= n + 1)
{
return n <= 0 ? 1 : 2 * pow2(n - 1);
}
// pow2(n) >= n + 1 > 1000, at inputs of a billion and more.
cv::proof void small_power(int n)
cv::pre(n >= 1000000000)
cv::post(pow2(n) < 1000)
{
}
error: verification failed: small_power [...::postcondition@...] (counterexample: n [ssa=n_0] [type=i32] = 1000023594; confirmed by a proof at this input, since its check could not compute pow2(1000023594)) [backend=z3] [reason=counterexample]
The failure rests on the facts the proof used (here pow2’s
postcondition); if one of them is not established, the verdict is not a
failure. When no proved fact settles the claim, the verifier cannot tell a
false claim from a true one that needs a proof, and says both:
cv::spec int fibo(int n)
cv::decreases(n)
cv::post(cv::result >= 0)
{
if (n <= 0) return 0;
if (n == 1) return 1;
return fibo(n - 2) + fibo(n - 1);
}
cv::proof void fibo_small(int n)
cv::pre(n >= 1000000000)
cv::post(fibo(n) < 5)
{
}
Unresolved: fibo_small [backend=z3] [reason=spec.fuel] (proof obligation ...: Z3 proposed n = 1000000000 as a counterexample, but checking it needs fibo(1000000000) (the evaluation nesting limit was reached while evaluating fibo), and no proved fact settles it: either the claim is false there, or it is true and needs a proof by induction; induction following fibo and induction on n did not prove it; a proof by induction following fibo could start from the body 'if (n > 0 && n != 1 && n - 2 >= 1000000000 && n - 1 >= 1000000000) { fibo_small(n - 2); fibo_small(n - 1); }', with cppverify::decreases(n))
Which to do depends on what you believe:
the claim is true: prove it by induction, starting from the body the message shows;
the claim is false: state the fact that refutes it as a lemma. Here
fibo(n) >= 5forn >= 5, proved by induction, makes the counterexample confirmed:
cv::spec int fibo(int n)
cv::decreases(n)
cv::post(cv::result >= 0)
{
if (n <= 0) return 0;
if (n == 1) return 1;
return fibo(n - 2) + fibo(n - 1);
}
cv::proof void fibo_at_least_5(int n)
cv::pre(n >= 5)
cv::post(fibo(n) >= 5)
cv::decreases(n)
{
if (n >= 6)
fibo_at_least_5(n - 1);
}
cv::proof void fibo_small(int n)
cv::pre(n >= 1000000000)
cv::post(fibo(n) < 5)
{
}
Verified: fibo_at_least_5 [backend=z3]
error: verification failed: fibo_small [...::postcondition@...] (counterexample: n [ssa=n_0] [type=i32] = 1000000000; confirmed by a proof at this input that uses the contract of fibo_at_least_5, since its check could not compute fibo(1000000000)) [backend=z3] [reason=counterexample]
The proof instantiates a lemma where its postcondition speaks of the same
spec application as the claim, here fibo_at_least_5 at
n = 1000000000. What it instantiates is what the lemma’s verification
proved: its postconditions wherever every assumption its proof started from
holds. These are its preconditions and the ones the verifier adds for its
parameters: a pointer is null or points to valid storage, valid(p, n)
means n >= 0 and n valid objects, mutable pointers address distinct
objects, and a record parameter satisfies its type invariant. A lemma is
therefore never used where an assumption its proof relied on fails. A
parameter the match leaves open, such as a pointer the conclusion does not
mention, can take any value, so the lemma applies wherever some value meets
those assumptions. A lemma over memory is used the same way:
cv::spec int total(const int *a, int n)
cv::reads(a, n)
cv::decreases(n)
{
return n <= 0 ? 0 : total(a, n - 1) + a[n - 1];
}
cv::proof void total_nonneg(const int *a, int n)
cv::pre(valid(a, n) && cv::forall(k, 0, n, a[k] >= 0))
cv::post(total(a, n) >= 0)
cv::decreases(n)
{
if (n > 0)
total_nonneg(a, n - 1);
}
cv::proof void total_negative(const int *a, int n)
cv::pre(valid(a, n) && n >= 1000000000)
cv::pre(cv::forall(k, 0, n, a[k] >= 0))
cv::post(total(a, n) < 0)
{
}
Verified: total_nonneg [backend=z3]
error: verification failed: total_negative [...::postcondition@...] (counterexample: n [ssa=n_0] [type=i32] = 1000000000, a [ssa=a_0] [type=pointer] = 1; confirmed by a proof at this input that uses the contract of total_nonneg, since its check could not compute total(1, 1000000000)) [backend=z3] [reason=counterexample]
Only contracts that are established count: a confirmation runs after every
function is verified, and a lemma that failed, or that rests on a fact that
is not established, is not used. Each confirmation gets one attempt’s share
of the time, and only verdicts that would otherwise stay unresolved try one.
In JSON, an unresolved verdict gives the proposed input and the value it
could not compute as unchecked_counterexample, and a confirmed failure
lists the lemmas it used as confirmed_with.
A spec’s own cppverify::post, cppverify::decreases, and cppverify::reads clauses are checked
without a function body to put a lemma call in. A proof block after the
clause gives them one. mul below is multiplication by repeated
addition; its commutativity needs mul(a, b) == a * b at both orders,
an induction the verifier does not find by itself:
cv::spec int mul(int a, int b) cv::decreases(b) { return b <= 0 ? 0 : mul(a, b - 1) + a; }
cv::proof void mul_is(int a, int b)
cv::post(b < 0 || mul(a, b) == a * b)
cv::decreases(b)
{
if (b > 0)
mul_is(a, b - 1);
}
cv::spec int area(int a, int b)
cv::post(a < 0 || b < 0 || cv::result == mul(b, a)) by { mul_is(a, b); mul_is(b, a); }
{
return mul(a, b);
}
Without the block, spec post: area is unresolved (spec.fuel); with
it, it is verified. In the block, cppverify::result is the value the body returns,
and naming the spec at a smaller measure uses the induction hypothesis
there. cppverify::decreases(...) by { ... } helps a termination proof the same
way, and cppverify::reads(p, n) by { ... } a frame. The block of an inductive
predicate’s postcondition is its induction step.
A block may also call a lemma about the spec itself. The two proofs then
rest on each other, which is sound only as an induction: they form a
cluster, as in Dafny, and must share cppverify::decreases clauses of one length.
Every call between them lowers the measure, and the lemma sees the spec’s
definition and postcondition only where the spec’s measure is below its
own. Lexicographic measures put the spec just below the lemma at the same
argument:
cv::proof void total_nonneg(int n);
cv::spec int total(int n)
cv::decreases(n, 0)
cv::post(cv::result >= 0) by { if (n > 0) total_nonneg(n - 1); }
{
return n <= 0 ? 0 : total(n - 1) + n;
}
cv::proof void total_nonneg(int n)
cv::decreases(n, 1)
cv::post(total(n) >= 0)
{
if (n > 0)
total_nonneg(n - 1);
}
Verified: spec decreases and post: total
Verified: total_nonneg [backend=z3]
The lemma unfolds total(n), since (n, 0) is below (n, 1); the
block calls the lemma at n - 1, since (n - 1, 1) is below
(n, 0). A lemma that would use the spec at the same measure gets
nothing from it, and a block call that does not lower the measure fails
like a non-terminating call. Without shared measures, two proofs that rest
on each other are reported with reason proof.cycle.
Use Z3, the default backend, for recursive specs. cvc5 checks its counterexamples against the same definitions but has no recursive definitions, does not cover bounded domains, computes deep closed applications much more slowly, and settles fewer lemmas and inductions with nonlinear machine arithmetic, so more such goals stay unresolved there. The strict portfolio is unresolved whenever cvc5 is.
Frames of heap-reading specs¶
A spec that reads through a pointer is evaluated in the heap state of each
call. After a write, the solver knows the value of the written cell but not
that a recursive spec over other cells is unchanged: that is an induction over
the spec. cppverify::reads(p, n) declares the cells p[0..n) the spec depends on,
and every write outside them then leaves the spec unchanged:
cv::spec int sum(const int *p, int n)
cv::reads(p, n)
cv::decreases(n)
{
return n <= 0 ? 0 : sum(p, n - 1) + p[n - 1];
}
void append(int *p, int n)
cv::pre(n >= 0 && n <= 100 && valid(p, n + 1))
cv::modifies(p[n])
cv::post(sum(p, n) == cv::old(sum(p, n))) // verified: p[n] is outside p[0..n)
{
p[n] = 7;
}
The clause is checked against the body: every load, under the conditions
that reach it, and every range that a heap-reading callee reads must lie in
the declared cells, so a spec may call another heap-reading spec only if that
spec has a cppverify::reads clause too. A wrong clause fails with a counterexample,
and a proof that used its frames is reported with reason spec.reads. The
range is a pointer and an element count fixed by the arguments; it cannot
depend on the heap. Several cppverify::reads clauses declare the union of their
ranges.
Structured proofs: by and cppverify::calc¶
A lemma call in a ghost block adds its postcondition to everything after it.
cppverify::check(c) by { ... } keeps a proof local instead: the block may
call lemmas, assert intermediate facts, and declare locals, and only c
holds afterwards, as with Verus’s and Dafny’s assert ... by:
cv::spec int sq(int x) { return x * x; }
cv::proof void sq_monotone(int a, int b)
cv::pre(0 <= a && a <= b)
cv::post(sq(a) <= sq(b))
{
}
int bigger(int a, int b)
cv::pre(0 <= a && a <= b && b <= 1000)
cv::post(cv::result == 1)
{
cv::check(sq(a) <= sq(b)) by {
sq_monotone(a, b);
}
return 1;
}
A lemma’s precondition is checked where the proof calls it. When the claim
is a cppverify::forall, the block proves it for one arbitrary value of the bound
variable, which it can name (Verus’s assert forall ... by):
cv::proof void pair_ordered(const int *a, int n, int i, int j)
cv::pre(valid(a, n) && n <= 1000 && 0 <= i && i <= j && j < n)
cv::pre(cv::forall(k, 0, n - 1, a[k] <= a[k + 1]))
cv::post(a[i] <= a[j])
cv::decreases(j - i)
{
if (i < j)
pair_ordered(a, n, i, j - 1);
}
cv::proof void below_last(const int *a, int n)
cv::pre(valid(a, n) && n >= 1 && n <= 1000)
cv::pre(cv::forall(k, 0, n - 1, a[k] <= a[k + 1]))
cv::post(cv::forall(k, 0, n, a[k] <= a[n - 1]))
{
cv::check(cv::forall(k, 0, n, a[k] <= a[n - 1])) by {
pair_ordered(a, n, k, n - 1);
}
}
Inside the block k lies in [0, n) and cannot be assigned; a lemma
call there is an instance for that k, and since k is arbitrary the
whole cppverify::forall follows. cppverify::calc chains
such steps, each with an optional proof block, and concludes the relation
between its first and last terms: == when every step is ==, < (or
>) when some step is strict, otherwise <= (or >=):
void chain(int a, int b, int c)
cv::pre(0 <= a && a <= b && b <= c && c <= 1000)
{
cv::calc {
sq(a);
<= { sq_monotone(a, b); }
sq(b);
<= { sq_monotone(b, c); }
sq(c);
== c * c;
}
cv::check(sq(a) <= c * c);
}
Quantifiers over all integers¶
cppverify::forall(k, body) and cppverify::exists(k, body) range over all mathematical
integers. They state lemmas without an artificial range and let a caller
instantiate them anywhere:
cv::proof void sq_nonnegative_all()
cv::post(cv::forall(k, sq(k) >= 0))
{
}
void uses_lemma(int a)
{
cv::ghost { sq_nonnegative_all(); }
cv::check(sq(a + 7) >= 0);
}
A counterexample to an unbounded quantifier is certified when its body
depends on its binders through linear arithmetic, comparisons, memory and
collection reads, and nested quantifiers, which the checker decides as
Presburger arithmetic once the model fixes everything else, or when a witness
among the values where the body’s comparisons change decides it. Otherwise (a
product or quotient of binders, say) the result is
counterexample.unchecked.
Triggers¶
The solver uses a quantified fact by instantiating it at terms that match a
pattern. Usually it picks the patterns itself; cppverify::trigger(term) inside the
body chooses one, as Verus’s #[trigger] does:
void positive(const int *a, int n)
cv::pre(valid(a, n) && n >= 1 && n <= 1000)
cv::pre(cv::forall(k, 0, n, cv::trigger(a[k]) > 0))
{
cv::check(a[n - 1] > 0);
}
A trigger must be a memory read, a collection read, or a call of a recursive
spec, and must mention a quantified variable; any other mark draws a warning
and is ignored. A trigger whose instances create new matching terms (for
example cppverify::trigger(g(k)) with a fact about g(k + 1)) makes a matching
loop, and the query times out. --profile-quantifiers reports how often
each quantifier of an unresolved query was instantiated, which points at the
culprit (see Chapter 16 — When verification fails).
cppverify::choose¶
cppverify::choose(k, body) is an integer for which body holds, when there is one,
and otherwise some unspecified integer (Hilbert’s ε). cppverify::choose(k, lo, hi,
body) chooses in [lo, hi):
cv::spec int index_of(const int *a, int n, int x)
{
return cv::choose(k, 0, n, a[k] == x);
}
void found(const int *a, int n, int x)
cv::pre(valid(a, n) && n >= 1 && n <= 1000)
cv::pre(cv::exists(k, 0, n, a[k] == x))
{
cv::check(0 <= index_of(a, n, x) && index_of(a, n, x) < n);
cv::check(a[index_of(a, n, x)] == x);
}
A choice is a function of the values its body mentions, so it is the same for the same values, and that is all the verifier knows about it: a claim that holds for some choices but not others fails with a checked counterexample. A checked counterexample uses only choices that satisfy their bodies wherever some value does, including the chooses inside the definitions and postconditions a proof unfolds.
“The same” is meant exactly. Two chooses whose conditions are written the same way, up to the names of their variables, are one choice, as Verus shares one function between them. The values a condition mentions include the cells it reads, so a choice over memory keeps its value across a write that leaves those cells as they were:
cv::proof void written_alike(int n)
cv::post(cv::choose(k, k > n) == cv::choose(j, j > n))
{
}
void write_other(const int *a, int n, int *b, int x)
cv::pre(valid(a, n) && n >= 1 && n <= 100 && valid(b, 1))
cv::modifies(*b)
cv::post(cv::choose(k, 0, n, a[k] == x) ==
cv::old(cv::choose(k, 0, n, a[k] == x)))
{
*b = 5;
}
Verified: written_alike [backend=z3]
Verified: write_other [backend=z3]
The condition of write_other’s choice reads a[0] to a[n - 1],
and b is another object. A write to one of those cells may change the
choice, and a condition written another way is another choice, even when
it means the same:
void write_inside(int *a, int n, int x)
cv::pre(valid(a, n) && n >= 1 && n <= 100)
cv::modifies(a[0])
cv::post(cv::choose(k, 0, n, a[k] == x) ==
cv::old(cv::choose(k, 0, n, a[k] == x)))
{
a[0] = x == 0 ? 1 : 0;
}
cv::proof void written_differently(int n)
cv::post(cv::choose(k, k > n) == cv::choose(k, n < k))
{
}
choose.cpp:5:43: error: verification failed: write_inside [...] (counterexample: a [ssa=a_0] [type=pointer] = ..., n [ssa=n_0] [type=i32] = 1, ...)
choose.cpp:12:33: error: verification failed: written_differently [...] (counterexample: n [ssa=n_0] [type=i32] = <unknown>)
In every counterexample to write_inside the write changes a[0], a
cell the condition reads, so the choice may move. When a proof needs equivalent conditions to choose alike, write
them the same way, or put the choice in one spec function and apply it in
both places. A counterexample is checked against this too: it may give one
choice two values only in memories where its condition reads some
different value.
Spec collections¶
<cppverify.h> provides cppverify::seq, set, multiset, and
map of mathematical integers, the counterparts of Verus’s Seq,
Set, Multiset, and Map. They exist only for verification: use
them in contracts, ghost code, and spec and proof functions. A ghost
sequence can record what a loop has seen:
using cppverify::seq;
int count_positive(const int *a, int n)
cv::pre(valid(a, n) && n >= 0 && n <= 1000)
cv::post(0 <= cv::result && cv::result <= n)
{
cv::ghost seq seen = cppverify::seq_empty();
int c = 0;
for (int i = 0; i < n; i = i + 1)
cv::invariant(0 <= i && i <= n && 0 <= c && c <= i)
cv::invariant(seen.len() == i)
cv::invariant(cv::forall(k, 0, i, seen[k] == a[k]))
cv::decreases(n - i)
{
if (a[i] > 0)
c = c + 1;
cv::ghost { seen = seen.push(a[i]); }
}
return c;
}
Spec functions over collections recurse on their size:
cv::spec int sum(seq s)
cv::decreases(s.len())
{
return s.len() <= 0 ? 0 : sum(s.subrange(0, s.len() - 1)) + s[s.len() - 1];
}
cv::proof void sum_push(seq s, int x)
cv::post(sum(s.push(x)) == sum(s) + x)
{
}
A sequence has len(), s[i], push(x), update(i, x),
reverse(), subrange(lo, hi), s + t, and contains(x).
update and reverse are themselves spec functions of
<cppverify.h>, defined by the other operations; reverse comes with
its length, s.reverse().len() == s.len(), and other facts about it are
proved by induction (see Chapter 20 — From mathematics to verified code).
A set has insert, remove, contains, unite, intersect,
difference, and subset_of; a multiset counts its elements; a map
has insert(k, v), remove(k), contains(k), and m[k]. Values
compare with == and !=. Facts about them are proved like any other:
cv::proof void set_facts(set s, int x, int y)
cv::pre(x != y)
cv::post(s.insert(x).contains(x))
cv::post(s.insert(x).remove(y).contains(x))
cv::post(s.subset_of(s.unite(set_empty().insert(y))))
cv::post(!s.difference(s).contains(x))
{
}
cv::proof void multiset_facts(multiset m, int x, int y)
cv::pre(x != y)
cv::post(m.insert(x).count(x) == m.count(x) + 1)
cv::post(m.insert(x).count(y) == m.count(y))
cv::post(m.insert(x).remove(x) == m)
{
}
cv::proof void map_facts(map m, int k, int v, int j)
cv::pre(j != k)
cv::post(m.insert(k, v)[k] == v && m.insert(k, v).contains(k))
cv::post(m.insert(k, v)[j] == m[j])
cv::post(!m.remove(k).contains(k))
{
}
A ghost multiset can stand for the elements a loop has passed, so an invariant can say what a counter counts:
int count_of(const int *a, int n, int x)
cv::pre(valid(a, n) && n >= 0 && n <= 1000)
cv::post(0 <= cv::result && cv::result <= n)
{
cv::ghost multiset bag = multiset_empty();
int c = 0;
for (int i = 0; i < n; i = i + 1)
cv::invariant(0 <= i && i <= n && 0 <= c && c <= i)
cv::invariant(c == bag.count(x))
cv::decreases(n - i)
{
if (a[i] == x)
c = c + 1;
cv::ghost { bag = bag.insert(a[i]); }
}
return c;
}
Every operation is total: an index outside [0, len()) reads 0, an update
there changes nothing, subrange clamps its bounds, and a key outside a
map’s domain maps to 0. Sequences are finite; sets, multisets, and maps range
over all integers and may be infinite. Counterexamples show their values, such
as s = [4, 3] or m = {1 -> 7, 4.. -> 2}. Z3 and cvc5 decide all
four; Lean support for them is planned for a future release.
Inductive predicates¶
Some predicates have no measure: whether b can be reached from a by
steps x -> x + 1 and x -> 2 * x is a question about paths, not about a
smaller argument. cppverify::inductive makes a cppverify::spec returning bool the
least predicate its body defines (Dafny’s least predicate): true exactly
where a finite derivation shows it.
cv::spec bool edge(int a, int b) { return b == a + 1 || b == 2 * a; }
cv::spec bool reach(int a, int b)
cv::inductive
cv::post(!cv::result || a < 0 || a <= b)
{
return a == b || cv::exists(c, edge(a, c) && reach(c, b));
}
cv::proof void three_reaches_twelve()
cv::post(reach(3, 12))
{
cv::check(reach(12, 12));
cv::check(reach(6, 12));
}
cv::proof void no_way_back(int a, int b)
cv::pre(a >= 0 && b < a)
cv::post(!reach(a, b))
{
}
cv::proof void wrong_way(int a, int b)
cv::pre(a >= 0 && reach(a, b))
cv::post(a < b)
{
}
Verified: spec axiom: edge
Verified: inductive predicate: reach
Verified: spec post: reach
Verified: three_reaches_twelve [backend=z3]
Verified: no_way_back [backend=z3]
book.cpp:25:14: error: verification failed: wrong_way [...::postcondition@25:14]
(counterexample: a [ssa=a_0] [type=i32] = 38, b [ssa=b_0] [type=i32] = 38)
[backend=z3] [reason=counterexample]
A proof sees reach through its body, unfolded once at each application it
names: reach(6, 12) holds by the edge 6 -> 12 and reach(12, 12),
and reach(3, 12) by the edge 3 -> 6. The postcondition says what every
derivation satisfies; the verifier proves it by induction on derivations,
and no_way_back follows from it. wrong_way fails: reach(38, 38)
holds with no step at all.
How deep a proof sees reach depends on what it needs, and the verifier
works most of it out itself. With constant arguments the counterexample
check computes the predicate: reach(3, 12) holds by the derivation
3 -> 6 -> 12, and the solver receives the unfoldings along it, so the
proof needs no body. When a verdict still depends on the predicate, the
verifier unfolds the applications inside the unfoldings too, one level more
at a time, up to four levels; cppverify::reveal_with_fuel(reach, n) asks for n
levels from the start.
cv::proof void three_reaches_twelve_again() cv::post(reach(3, 12)) {}
cv::proof void doubles(int b)
cv::pre(b >= 1)
cv::post(reach(b, 2 * b + 1))
{
}
cv::proof void far(int b)
cv::pre(b == 64)
cv::post(reach(1, b))
{
cv::ghost { cv::reveal_with_fuel(reach, 7); }
}
Verified: three_reaches_twelve_again [backend=z3]
Verified: doubles [backend=z3]
Verified: far [backend=z3]
Each level is an unfolding the verifier proved, so a proof found this way
stands, and a false claim still fails with a checked counterexample. Some
claims no unfolding settles. Where the derivations from an argument go
round a cycle, the predicate is false there, but every unfolding holds just
as well of a predicate that is true on the cycle. The verdict says so
(spec.fuel: every counterexample found needs ... to hold, but no
derivation shows it), the verifier does not unfold further, and a
postcondition or a lemma proved by induction supplies the missing step.
A counterexample is checked against what the predicate really means, true
or false. Where its derivations from an argument reach finitely many
arguments, the checker computes it over all of them: every value starts
false and becomes true once the body holds, until nothing changes. Where
they reach infinitely many, reach(a, b) true is shown by a derivation,
and false by the postcondition: reach(5, 3) cannot hold because
5 <= 3 does not.
The predicate may occur in its body only positively (as a conjunct, a
disjunct, a branch, or under cppverify::exists or a bounded cppverify::forall), which makes
the least predicate exist and equal its body. That equality is what the
proofs above use, and it is proved, not assumed: for each predicate the
verifier generates proofs of three rules, by induction on the height of
derivations where needed, and Verified: inductive predicate: reach says
they hold. Monotonicity says that a derivation of some height is one of
every greater height; case analysis, that reach(a, b) implies its body;
introduction, that the body implies reach(a, b). If one fails, the
predicate and every proof that uses it are Unresolved with reason
spec.inductive. A postcondition that does not hold of every derivation
fails with spec post by induction failed and a counterexample giving the
height of a derivation.
Predicates that apply each other are defined together, and a predicate may read memory:
cv::spec bool ev(int n);
cv::spec bool od(int n) cv::inductive { return n == 1 || ev(n - 1); }
cv::spec bool ev(int n) cv::inductive { return n == 0 || od(n - 1); }
struct node { int value; node *next; };
// q is reached from p by following next.
cv::spec bool segment(const node *p, const node *q) cv::inductive {
return p == q || (p != nullptr && segment(p->next, q));
}
A postcondition may mention the predicate itself. Transitivity is one: a
derivation of reach(a, b) extends every path from b:
cv::spec bool edge(int a, int b) { return b == a + 1 || b == 2 * a; }
cv::spec bool reach(int a, int b);
cv::spec bool reach(int a, int b)
cv::inductive
cv::post(!cv::result || cv::forall(c, !reach(b, c) || reach(a, c)))
{
return a == b || cv::exists(c, edge(a, c) && reach(c, b));
}
cv::proof void chain(int a, int b, int c)
cv::pre(reach(a, b) && reach(b, c))
cv::post(reach(a, c))
{
}
In the induction, the premise reach(c, b) already extends every path
from b (the induction hypothesis), and reach(a, c2) follows from
edge(a, c) and reach(c, c2) by the predicate’s proved unfolding. The
postcondition itself is never assumed while it is being proved, so a false
one such as cppverify::post(!cppverify::result || !reach(a, b)) is never proved: it fails, with
a derivation of height 1 where a == b. (The forward declaration lets the
postcondition name the predicate.)
constexpr as spec¶
Any non-recursive, loop-free constexpr function without a contract of its own is usable in
cppverify::pre/cppverify::post directly, with no separate cppverify::spec
re-declaration:
constexpr int square(int x) { return x * x; }
int area(int side)
cv::pre(side >= 0 && side <= 1000)
cv::post(cv::result == square(side))
{ return side * side; }
One body does double duty: the same constexpr runs at execution time and defines the spec, so
the two can never silently diverge (Verus requires a separate spec fn). A lifted constexpr
keeps machine integer semantics — honest overflow — unlike an explicit cppverify::spec (mathematical
Int); see Integers. It is lifted when a verified function uses it, so the
constexpr functions of a header such as <vector> cost nothing until
called, and a call with constant arguments is evaluated by Clang. A
constexpr function with its own contract stays an executable function,
which a contract cannot call.