Functions and loops¶
Functions¶
int f(int x)
pre(x > 0 && x < 1000)
post(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)
pre(value > 0)
post(result > value);
int f(int x) { return x + 1; }
A contracted declaration with no definition is an explicit trusted interface.
Calls are verified against its contract, and cpp-verify emits a warning that
the external contract is being assumed rather than reporting it as verified.
An uncontracted constexpr definition may be lifted for use in contract
expressions. Once a constexpr function has executable pre/post
clauses, it remains a modular executable function: calls must satisfy its
preconditions and cannot be used as pure contract expressions.
At a modular call, preconditions and old(parameter) use the argument’s
entry value. If a by-value parameter is reassigned inside the callee, an
unwrapped occurrence of that parameter in a postcondition denotes its final
local value, not the caller’s unchanged argument. The call summary therefore
uses a fresh final value for each syntactically modified parameter.
Loops¶
while (c)
invariant(I)
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)
invariant(value >= 1 && value <= n + 1)
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 invariant clauses are conjoined. 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 old(...); use an ordinary snapshot local directly instead.
Returns inside loop bodies remain unsupported; an early return before a loop
continues to guard whether that loop is reached.
The verifier checks a loop modularly (no unrolling), discharging three obligations:
Obligation |
Meaning |
|---|---|
Establishment |
|
Preservation |
From an arbitrary state satisfying |
Termination |
|
Ghost-block and proof-function loops are erased at runtime and therefore must
include decreases; partial-correctness nontermination cannot be used as a
proof step.
After the loop the verifier knows exactly I && !c — anything needed
downstream must be captured by the invariant.
Note
Invariants are checked under honest machine integers. An unbounded
accumulator invariant like s >= 0 is not inductive (from s ==
INT_MAX, s + 1 overflows negative); 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 decreases expression must be integer-typed. A comma-separated tuple
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), with all components non-negative. This proves
termination of nested counters and Ackermann-style recursion.
Recursive spec, proof, and executable functions use the same
well-founded, lexicographic discipline. Each recursive call must occur on a
path where its 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.