Functions and loops¶
Functions¶
int f(int x)
cv::pre(x > 0 && x < 1000)
cv::post(cv::result > x)
{ return x + 1; }
Contracts may instead appear on a forward declaration. A later definition inherits that declaration’s contract even when its parameter names differ:
int f(int value)
cv::pre(value > 0 && value < 1000)
cv::post(cv::result > value);
int f(int x) { return x + 1; }
A caller’s proof uses its callees’ contracts, so a verdict is only as good as
those contracts. A caller whose proof relies on a callee contract that the
callee’s own verification did not establish is Unresolved with reason
callee.contract, naming the callee.
Trusted contracts¶
Some contracts cannot be proved in the program being verified: a function
from a library compiled elsewhere, a system call, a routine whose body is
outside the verified subset, or a lemma you decide to assume.
[[cppverify::trusted]] marks such a contract as assumed, as Verus’s
#[verifier::external_body] does:
// Defined in another library.
[[cppverify::trusted]] int clamp_byte(int v)
cv::post(0 <= cv::result && cv::result <= 255);
// Compiled and run, but its body is not verified.
[[cppverify::trusted]] int read_sensor(int channel)
cv::pre(channel >= 0 && channel < 4)
cv::post(cv::result >= 0 && cv::result <= 1023)
{
return channel * 300;
}
// An axiom: a proof function without a proof.
cv::spec int sq(int x) { return x * x; }
[[cppverify::trusted]] cv::proof void sq_nonnegative(int x)
cv::post(sq(x) >= 0);
int sample(int c)
cv::pre(c >= 0 && c < 4)
cv::post(0 <= cv::result && cv::result <= 255)
{
int r = read_sensor(c);
cv::ghost { sq_nonnegative(r); }
return clamp_byte(r);
}
trusted.cpp:2:28: Trusted: clamp_byte (contract assumed, not verified)
trusted.cpp:6:28: Trusted: read_sensor (contract assumed, not verified)
Verified: spec axiom: sq
trusted.cpp:16:39: Trusted: sq_nonnegative (contract assumed, not verified)
Verified: sample [backend=z3] [trusts=clamp_byte,read_sensor,sq_nonnegative]
On a declaration, the contract is assumed at every call; the preconditions are still checked there.
On a definition, the body is compiled but not verified.
On a proof function, the postcondition is an axiom.
A spec function cannot be trusted: its definition is its meaning, so there is nothing to assume. The attribute is an error there.
Each trusted contract is marked on its own function:
#pragma clang attributecannot apply the attribute to a region.Every verdict that relies on a trusted contract lists it in
[trusts=...](JSON"trusts"), transitively through verified callees: a caller ofsamplecarries the same three names.
The mark may appear on any declaration of the function. Without it, a contract with no definition is neither proved nor vouched for, so it proves nothing:
int helper(int v)
cv::post(cv::result == v);
int uses_helper(int v)
cv::post(cv::result == v)
{
return helper(v);
}
warning: helper has a contract but no definition, so its callers are not
verified; mark the declaration [[cppverify::trusted]] to assume the contract
Unresolved: uses_helper [backend=z3] [reason=callee.contract] (relies on the
contract of helper (no definition; mark it [[cppverify::trusted]] to
assume it), which is not established)
A trusted contract is an assumption, and a wrong one makes proofs wrong. One
that contradicts the state of a call (no result can satisfy it) would make
everything after the call hold vacuously; CppVerify reports that case with
[vacuous] and a warning at the call (see Commands and flags).
An uncontracted, non-recursive, loop-free constexpr definition may be
lifted for use in contract expressions; a recursive one used with symbolic
arguments is rejected. It is lifted where verification uses it: in a contract, a
verified body, a type invariant, or another lifted function, so the
constexpr functions of a header that nothing verified uses are never
examined. Once a constexpr function has executable cppverify::pre/cppverify::post
clauses, it remains a modular executable function: calls must satisfy its
preconditions and cannot be used as pure contract expressions.
Preconditions, cppverify::old(parameter), and a parameter named in a postcondition
all denote the argument’s entry value, as in ACSL, even when the callee
reassigns its by-value parameter; a caller can therefore use the
postcondition directly.
Behaviors¶
A contract can be split into cases, as ACSL behaviors do.
cppverify::behavior(name, assumes) starts a case; the cppverify::pre and
cppverify::post clauses after it apply where its assumption holds
(assumes at entry):
int abs_value(int x)
cv::behavior(nonnegative, x >= 0)
cv::post(cv::result == x)
cv::behavior(negative, x < 0)
cv::pre(x > -2147483647 - 1)
cv::post(cv::result == -x)
cv::complete_behaviors
cv::disjoint_behaviors
{
return x < 0 ? -x : x;
}
cppverify::complete_behaviors requires that some behavior applies to every input the
preconditions admit, and cppverify::disjoint_behaviors that no two do; either may
list the behaviors it relates, as in cppverify::disjoint_behaviors(low, high).
Loops¶
while (c)
cv::invariant(I)
cv::decreases(D)
{ ... }
Clauses go after the loop header’s closing ); for loops use the same
placement. For do loops, clauses go after the trailing while (c) and
before its semicolon:
do {
value = value + 1;
} while (value <= n)
cv::invariant(value >= 1 && value <= n + 1)
cv::decreases(n + 1 - value);
The mandatory first body execution is checked from the concrete incoming state.
It must establish the invariant; subsequent iterations use the ordinary
modular while rule. The invariant therefore need not hold before entering
the first body.
Multiple cppverify::invariant clauses are conjoined. cppverify::old(expr) is permitted in an
invariant and always denotes the enclosing function’s entry state, not the
previous iteration. Function locals do not exist at function entry and are
rejected inside cppverify::old(...); use an ordinary snapshot local directly instead.
return, break, and continue may leave a while or for loop
from any iteration. A return checks the postcondition in its own state and
a break continues after the loop in its own state. A continue ends the
iteration: the invariant must hold again and the cppverify::decreases measure must
drop, after a for increment has run. break and continue in a
do loop are not supported yet, and cppverify::ghost code cannot leave an
executable loop.
The verifier checks a loop modularly (no unrolling), discharging four obligations:
Obligation |
Meaning |
|---|---|
Establishment |
|
Preservation |
From an arbitrary state satisfying |
Termination |
|
Frame |
With a loop |
Verification is total correctness, as in Verus: an executable loop without
cppverify::decreases leaves its function Unresolved with reason
decreases.missing. cppverify::decreases(*) allows a loop (or an executable
function) to diverge; its function and every caller are then reported
Verified ... [partial], proved only for the executions that terminate.
Ghost-block and proof-function loops are erased at runtime and must have a
real measure; cppverify::decreases(*) is rejected there.
Nobody knows whether this loop ends for every start, so it has no measure:
unsigned collatz_steps(unsigned n)
cv::post(n != 1 || cv::result == 0)
{
unsigned steps = 0;
while (n != 1)
cv::invariant(cv::old(n) != 1 || (n == 1 && steps == 0))
cv::decreases(*)
{
n = n % 2 == 0 ? n / 2 : 3 * n + 1;
steps = steps + 1;
}
return steps;
}
unsigned none()
cv::post(cv::result == 0)
{
return collatz_steps(1);
}
Verified: collatz_steps [backend=z3] [partial]
Verified: none [backend=z3] [partial]
warning: collatz_steps: proved only for executions that terminate: cppverify::decreases(*) at 7:19 allows it to diverge
warning: none: proved only for executions that terminate: it calls collatz_steps, which may diverge
A loop writes only the objects its stores and calls reach, so other memory
keeps its value across it without an invariant. cppverify::modifies(...) after the
invariants narrows that further, like ACSL’s loop assigns; its footprints
are read in each iteration’s state, so cppverify::modifies(a[0 : i]) describes the
prefix written so far.
void clear_prefix(int *a, int n, int k)
cv::pre(valid(a, n) && 0 <= k && k <= n && n <= 1000)
cv::modifies(a[0 : k])
cv::post(cv::forall(j, 0, k, a[j] == 0))
cv::post(cv::forall(j, k, n, a[j] == cv::old(a[j])))
{
for (int i = 0; i < k; i = i + 1)
cv::invariant(0 <= i && i <= k)
cv::invariant(cv::forall(j, 0, i, a[j] == 0))
cv::modifies(a[0 : i])
cv::decreases(k - i)
{
a[i] = 0;
}
}
The loop’s frame is checked at the end of each iteration, after the for
increment, so a[0 : i] then covers the cell just written; the function’s
cppverify::modifies(a[0 : k]) is the range [0, k), and the cells from k on
keep their values without an invariant saying so.
After a normal exit the verifier knows I && !c about what the loop may
change; variables the loop does not assign and memory outside its write set
keep their values. A break or return leaves in its own state instead.
Note
Contracts are mathematical, but the loop body runs on machine integers.
An unbounded accumulator invariant like s >= 0 does not prove that the
body’s s + 1 cannot overflow (s may be INT_MAX); bound the
accumulator instead (e.g. s == i). A loop placed after an early return is checked only on
the path that reaches it.
Each cppverify::decreases expression must be integer-typed. A comma-separated tuple
cppverify::decreases(a, b) is a lexicographic measure: each iteration the tuple must
strictly decrease in lexicographic order (some component drops while every
earlier component stays equal), and the component that drops must be
non-negative before the step, as in ACSL. This proves termination of nested
counters and Ackermann-style recursion.
Recursive cppverify::spec, cppverify::proof, and executable functions use the same
well-founded, lexicographic discipline. Each recursive call must occur on a
path where its cppverify::decreases measure is nonnegative and strictly smaller than
the caller’s. Calls hidden after assignment-only or fallthrough branches are
checked as well.
The end-to-end acceptance programs include recursive and iterative factorial
and Fibonacci implementations, each proved against a mathematical recursive
specification at the exact signed-int boundary.