Verifying a program
cerastes verify checks a C or C++ source file against the contracts
written in it. It proves, for every function with a body, that the
postconditions hold, that every call meets the callee’s precondition, that
every CERASTES_CHECK and loop invariant holds, that no integer
operation overflows, divides by zero or shifts out of range, and that every
loop and every recursive function terminates. When it cannot prove one of
these, it reports the line. This page describes the command,
what it verifies, what it prints and the assumptions a result rests on.
The annotations are described in Annotations. The meaning of the integer operations is in Integer semantics, and every message the tool can print is in Error messages.
Running it
build/cerastes verify midpoint.cpp
The file is the one source file of the run. It is this function:
#include <cerastes.h>
int midpoint(int lo, int hi)
CERASTES_REQUIRES(0 <= lo && lo <= hi)
CERASTES_ENSURES(r, lo <= r && r <= hi)
{
return lo + (hi - lo) / 2;
}
midpoint.cpp: verified (1 function; one translation unit: assumes that functions defined elsewhere satisfy the contracts declared here)
The command takes these options and no others. An option that is not in this table is refused with exit status 2, even if the front end would accept it: verification must not depend on a diagnostic that was switched off or a dialect that was changed behind its back.
| Option | Meaning |
|---|---|
|
Add |
|
Define or undefine a macro. |
|
The language mode. The default is |
|
Keep the generated Viper program and its position map. See Keeping the Viper program. |
|
The wall-clock limit for the proof search. The default is 300. Reaching it is a failure of the tool, not a verdict on your program (exit status 3). |
|
Print usage, or the version. |
The tools the verifier runs (the Java runtime, Silicon, Z3 and the system
include directories) are fixed when cerastes is built. Environment variables
such as SILICON_JAR or Z3_EXE do not change a result.
What is verified
cerastes verifies the primary file and every user header it includes: each function body in them. A header is verified again in every run that includes it.
A system file is not verified. It is a file under one of the system include
directories the tool was built with (the SDK, the C++ library, the
compiler’s own headers), or exactly the include/cerastes.h of the cerastes
checkout. The test is made on the file’s real path (symbolic links and ..
are resolved), never on a flag in the source, so a #pragma or a #line in
your code cannot turn the checks off. A #line directive in a verified file is
rejected for the same reason. A function declared in a system file is used only
through a contract; its body is never read.
A directory you give with -I is a user directory, even if it holds
library code.
The bodies of functions marked CERASTES_PREDICATE and CERASTES_PURE are
specification and are not verified. In this version a call of one of them in
executable code is rejected, and in a contract only cerastes::old may be
called.
The output
A verification error is one line on standard output, followed by notes that say which clause is involved:
midpoint_overflow.cpp:10: error: overflow: signed `int` addition may overflow
The file is written as you gave it on the command line (or as the include resolved, for a header), the line is the line of the C or C++ source, and the category is one of eleven names that Error messages explains. Errors are sorted by file, then line, then category, and each is printed once. A program with errors exits with status 1.
#include <cerastes.h>
int midpoint(int lo, int hi)
CERASTES_REQUIRES(0 <= lo && lo <= hi)
CERASTES_ENSURES(r, lo <= r && r <= hi)
{
return (lo + hi) / 2;
}
A program that cerastes does not accept, because EDG finds a compile error or because it uses a construct that cerastes does not verify, is reported on standard error in EDG’s format, with the diagnostic’s name, and the exit status is 2. Standard output is then empty:
"rejected_double.cpp", line 6: error #cerastes-unsupported_type: type "double" is not supported by "cerastes verify": only integer types, bool and the enumerations of the verified files are (decisions 0012 and 0018)
double half(double x)
^
1 error detected in the compilation of "rejected_double.cpp".
Nothing else is printed. In particular a warning from the front end in a verified file is shown on standard error like any diagnostic, so a program that is meant to verify cleanly should compile without warnings.
Exit status
| Status | Meaning |
|---|---|
0 |
Every function verified. |
1 |
Verification errors, printed on standard output. |
2 |
The program is not accepted: a front-end error, a construct cerastes does not verify, a refused option or a missing file. |
3 |
The tool failed: the prover crashed, timed out or gave no verdict, an error could not be traced to a source line, or the front end aborted with an internal error. This is never a statement about your program. |
Loops and termination
A loop needs an invariant to be verified (the verifier does not unroll it) and
a measure, CERASTES_DECREASES(…), to show that it ends. The invariant
must hold when the loop is first reached and after every iteration, and what
follows the loop is proved from the invariant and the negation of the
condition. The measure is one or more integer expressions that get smaller
with every iteration and never go below zero: in n - i, the loop condition
i < n makes it positive, and i += 1 makes it smaller. The arithmetic in the
loop is checked on every iteration, and so is the condition each time it is
evaluated, and so is the arithmetic of the measure.
#include <cerastes.h>
int double_count(int n)
CERASTES_REQUIRES(0 <= n && n <= 1000)
CERASTES_ENSURES(r, r == 2 * n)
{
int i = 0;
int s = 0;
CERASTES_INVARIANT(0 <= i && i <= n && s == 2 * i)
CERASTES_DECREASES(n - i)
while (i < n) {
s += 2;
i += 1;
}
return s;
}
Here the invariant s == 2 * i and the bound i ⇐ n give s == 2 * n when
the loop ends, and the measure n - i shows that it ends. A loop with no
CERASTES_INVARIANT is verified with only the facts the verifier derives
automatically (that each variable the loop assigns stays in the range of its
type), which is rarely enough to prove anything about the result.
A recursive function needs a measure too, written after its parameter list. It is compared from the entry of the function to every call of a function in the same recursion cycle, over the arguments of the call:
#include <cerastes.h>
int twice(int n)
CERASTES_REQUIRES(0 <= n && n <= 1000)
CERASTES_DECREASES(n)
CERASTES_ENSURES(r, r == 2 * n)
{
if (n == 0) {
return 0;
}
return 2 + twice(n - 1);
}
A measure with several expressions is compared lexicographically: at the first one that changes, the new value must be smaller than the old one, which must not be negative, and the later ones may be anything. A function that is not in a recursion cycle needs no measure. A loop, or a function in a cycle (direct recursion, or mutual recursion through other functions of the file), without one is rejected, because a program whose termination is not shown cannot be called verified:
#include <cerastes.h>
int count_to(int n)
CERASTES_REQUIRES(n >= 0)
CERASTES_ENSURES(r, r == n)
{
int i = 0;
CERASTES_INVARIANT(0 <= i && i <= n)
while (i < n) {
i += 1;
}
return i;
}
The errors a wrong invariant or measure can cause are invariant-entry,
invariant-preserved and termination, described in
Error messages.
Leaving a loop early: break, continue, do and switch
break leaves a loop, or a switch, at once, without evaluating the loop’s
condition, so the invariant is not checked there. What follows the loop is
proved from the way it was left: the invariant and the negated condition, or
the state at one of the break statements. continue ends the iteration: in a
for loop the increment runs, then the condition is evaluated again, so the
invariant and the measure are checked there as at the end of the body.
#include <cerastes.h>
// The smallest divisor of n above 1 (n itself when n is prime).
int smallest_divisor(int n)
CERASTES_REQUIRES(2 <= n && n <= 10000)
CERASTES_ENSURES(r, 2 <= r && r <= n && n % r == 0)
{
int d = 2;
CERASTES_INVARIANT(2 <= d && d <= n)
CERASTES_DECREASES(n - d)
while (d < n) {
if (n % d == 0) {
break;
}
d += 1;
}
return d;
}
// The days in a month of a year that is not a leap year.
int days_in_month(int month)
CERASTES_REQUIRES(1 <= month && month <= 12)
CERASTES_ENSURES(r, 28 <= r && r <= 31)
{
int days = 31;
switch (month) {
case 2:
days = 28;
break;
case 4:
case 6:
case 9:
case 11:
days = 30;
break;
default:
break;
}
return days;
}
In smallest_divisor the loop ends either at the break, where n % d == 0,
or when d < n is false, where the invariant gives d == n; both give the
postcondition.
A do loop runs its body before its first test, so its invariant holds at the
start of every iteration (on entry, and each time the test is true), and the
arithmetic of its condition is checked after the body, where the condition is
evaluated. CERASTES_INVARIANT and CERASTES_DECREASES are written before
do. A do { … } while (0), whose body runs once, needs no measure; a
condition that names a variable, even a constant equal to zero, makes it an
ordinary loop that needs one.
A switch evaluates its controlling expression once, with the usual checks,
and goes to the matching case label, to default, or past the switch.
Execution falls through from one label’s statements to the next until a
break; [[fallthrough]]; (or [[gnu::fallthrough]];) marks an intended
fallthrough for compilers that warn about it. Each case value is checked as it
is written, like any constant: case 1 << 40: is a shift error at the label,
even where the compiler only warns. The case values are checked at the start
of the function, whether or not the switch is reached, and in C++ they
may name constants (Named constants). A switch on an enumeration must also pass
the compilers' -Wswitch rules (Enumerations).
A label may jump past a declaration without an initializer: the variable has no
value there until it is assigned (Locals without an initializer). cerastes rejects a case or default label that is not directly
inside the braces of its switch (as in Duff’s device), a GNU case range
(case 1 … 3:), a statement other than a declaration without an initializer
before the first label, a declaration with an initializer that a later label
jumps over, and a switch with an init-statement or a declaration in its
condition.
Jumping forward with goto
A goto is verified when it jumps outward and forward: its label is a later
statement of a block that encloses the goto, and no declaration of that block
lies between them. That covers the usual uses: an early exit to a common label
at the end of a function, a cleanup label, and leaving nested loops or a
switch at once. As with break, the loop’s condition is not evaluated on the
way out, so its invariant is not checked there; after the label, what follows is
proved from each way of getting there.
#include <cerastes.h>
// x - y, or -1 when y is negative or greater than x.
int checked_difference(int x, int y)
CERASTES_ENSURES(r, ((y < 0 || x < y) && r == -1) || (0 <= y && y <= x && r == x - y))
{
int result = -1;
if (y < 0) {
goto done;
}
if (x < y) {
goto done;
}
result = x - y;
done:
return result;
}
After done:, the postcondition is proved for each way of getting there: from
either goto, where result is still -1, and from the subtraction, which
cannot overflow because 0 ⇐ y ⇐ x holds there.
A goto may jump past a declaration without an initializer; the variable has
no value at the label until it is assigned (Locals without an initializer). cerastes rejects
a goto that jumps backward (write the loop as while, for or do), one that
jumps into a block, an if branch, a loop or the body of a switch, one that
jumps past a declaration with an initializer that is still in scope at the
label (the initialization would be skipped), and the GNU computed goto.
Locals without an initializer
A local may be declared without an initializer and given its value later. Until
it is assigned it has no value: reading it is undefined behaviour in C and in
C++ (erroneous behaviour in C++26), and the first rule of the
C++ initialization profile (P4222) forbids it. cerastes checks every
read. A read that may come before the variable has a value is an
uninitialized error at the read
(Error messages). The check follows the
paths of the program, so a variable assigned on every path that reaches the
read verifies, even when the assignment is in another branch than the read:
#include <cerastes.h>
int pick(int c, int a)
CERASTES_ENSURES(r, (c != 0 && r == a) || (c == 0 && r == 0))
{
int x;
if (c) {
x = a;
}
if (c) {
return x;
}
return 0;
}
int sign(int a)
CERASTES_ENSURES(r, (a > 0 && r == 1) || (a == 0 && r == 0) || (a < 0 && r == -1))
{
int s;
if (a > 0) {
s = 1;
} else if (a < 0) {
s = -1;
} else {
s = 0;
}
return s;
}
In pick, x is read only when c is non-zero, and then it was assigned.
A read is any use of the value: in an expression, a condition, a return, an
argument, the old value in x += 1 or x++, and an annotation such as
CERASTES_CHECK or a loop invariant. Each time its declaration is reached, in
the next iteration of a loop for example, the variable has no value again. A
goto or a case label may jump past a declaration without an initializer, as
C and C++ allow; the variable then has no value at the label until it
is assigned. A const local without an initializer is rejected, since it can
never be given a value. cerastes is stricter than C++ in two cases:
copying an indeterminate unsigned char is allowed there and an error here,
and an expression statement that only names the variable (x; or
(void)x;) does not read it in C++, while cerastes counts it as a read,
as C does. An operand of sizeof is never a read.
Side effects in expressions
An assignment, a compound assignment and ++ or -- may be used as a
value, and the comma operator may join expressions, in both modes:
#include <cerastes.h>
int store_and_use(int a)
CERASTES_REQUIRES(-1073741824 <= a && a <= 1073741823)
CERASTES_ENSURES(r, r == 2 * a + 1)
{
int y;
int z = (y = a) + 1;
return z + y;
}
int both(int x)
CERASTES_REQUIRES(-1073741824 <= x && x <= 1073741822)
CERASTES_ENSURES(r, r == 2 * x + 2)
{
int a = x++;
int b = ++x;
return a + b;
}
int meet(int n)
CERASTES_REQUIRES(0 <= n && n < 2147483647)
CERASTES_ENSURES(r, r == (n + 1) / 2)
{
int i, j;
CERASTES_INVARIANT(i <= j + 1 && i + j == n)
CERASTES_DECREASES(j - i)
for (i = 0, j = n; i < j; i++, j--) {
}
return i;
}
int count_up(int n)
CERASTES_REQUIRES(0 <= n && n <= 715827882)
CERASTES_ENSURES(r, r == 3 * n + 1)
{
int i = 0;
int total = 0;
CERASTES_INVARIANT(i <= n && total == 2 * i)
CERASTES_DECREASES(n - i)
while (i++ < n) {
total += 2;
}
return total + i;
}
The value of x = e is the value stored in x: e converted to the type of
x, which is checked like every conversion (Integer
semantics). The value of x += e and of ++x is the new value of x,
the value of x++ the old one. In a, b the left operand is evaluated
completely, then the right one, whose value is the value of the whole
expression. A side effect in the right operand of && or ||, or in an arm of
?:, happens only when that operand is evaluated, and one in a loop condition
happens each time the condition is evaluated: the loop invariant holds before
each evaluation. A variable assigned inside an expression has its value from
that point on (Locals without an initializer).
The last evaluation of a loop condition counts too. In count_up the condition’s
i++ runs once more than the body, so after the loop i is n + 1;
returning total + i - 1 instead gives 3 * n, and the postcondition fails at
the return:
#include <cerastes.h>
int count_up(int n)
CERASTES_REQUIRES(0 <= n && n <= 715827882)
CERASTES_ENSURES(r, r == 3 * n + 1)
{
int i = 0;
int total = 0;
CERASTES_INVARIANT(i <= n && total == 2 * i)
CERASTES_DECREASES(n - i)
while (i++ < n) {
total += 2;
}
return total + i - 1;
}
C leaves the order in which most operands are evaluated unspecified, and an
expression that modifies a variable and also reads or modifies it in an operand
that is not sequenced with the modification is undefined behaviour. cerastes
rejects such an expression at the modification, by C’s rules in both modes,
even where C++17 defines it (x = x++):
#include <cerastes.h>
int bad(int i)
{
// i++ and i are unsequenced
return i++ + i;
}
The operators that sequence their operands are &&, ||, , and the condition
of ?:; both arms of ?: count as modifying what either arm modifies. The
operands of the other operators, and the arguments of a call, are not
sequenced. As with -Wswitch, a few shapes that the compilers' -Wall warns
about are rejected too: an assignment used as a condition without extra
parentheses (write if x = e) != 0) or if ((x = e, not if (x = e)), a
comma operator one of whose operands has no effect, a discarded value computed
around a side effect (x++ + 1;), and a side effect inside sizeof,
which is never evaluated.
Functions that do not return
A function declared [[noreturn]] (or _Noreturn in C, or with the GNU
attribute noreturn) must never return: cerastes checks that no return and no
closing brace of its body can be reached, and reports one that can as a
noreturn error. A call of such a function ends the path of its caller, so what
would follow it is not checked.
abort, exit and _Exit from <stdlib.h> are known not to return. They are
the only functions of the C library that cerastes knows so far; quick_exit,
__builtin_trap, __builtin_unreachable (and C23’s unreachable()) and
std::terminate are rejected.
#include <limits.h>
#include <stdlib.h>
#include <cerastes.h>
[[noreturn]] void fail(void)
{
abort();
}
// a / b; the program ends where the division would be undefined.
int checked_div(int a, int b)
{
if (b == 0 || (a == INT_MIN && b == -1)) {
fail();
}
return a / b;
}
Without the call of fail, a / b would be a division-by-zero and an
overflow error. A function declared not to return cannot have
CERASTES_ENSURES (it would never be checked), and main cannot be declared not
to return. A function that never returns by looping forever is rejected, since
every loop needs a measure.
assert
assert(e) from <assert.h> (or <cassert>) is verified as a check of e: a
failure is a check error at the assert. Unlike CERASTES_CHECK, it is code:
e is evaluated at run time, so its arithmetic is checked like any expression’s,
and it may call functions. When NDEBUG is defined, assert does nothing, and
cerastes checks nothing, as the program will run. A result holds for the
configuration that was verified: with asserts on, the code after assert(e) may
rely on e, which a build with NDEBUG no longer checks, so verify the
configuration you ship.
#include <assert.h>
#include <cerastes.h>
int average(int a, int b)
CERASTES_REQUIRES(0 <= a && a <= 100 && 0 <= b && b <= 100)
CERASTES_ENSURES(r, r == (a + b) / 2)
{
int sum = a + b;
assert(sum <= 200);
return sum / 2;
}
Here the requires keeps sum small, the assert states a bound that the requires guarantees, and the contract pins the result exactly: r == (a + b) / 2.
Named constants
A const variable at file scope (in C++ also constexpr, and at
namespace scope) whose initializer is a constant can be used anywhere: in code,
in CERASTES_REQUIRES, CERASTES_ENSURES, CERASTES_CHECK, invariants and
measures. In C++ it can also be named in the initializer of another
constant and be a case value, and so can a local const or constexpr
variable initialized with a constant (case LIMIT:, case LIMIT + 1:,
case k:).
#include <cerastes.h>
const int MAX_LEVEL = 3;
int level_cost(int level)
CERASTES_REQUIRES(0 <= level && level <= MAX_LEVEL)
CERASTES_ENSURES(r, r == 10 * level)
{
switch (level) {
case 0:
return 0;
case 1:
return 10;
case 1 << 1:
return 20;
default:
return 30;
}
}
int extremes(void)
CERASTES_ENSURES(r, r == 30)
{
int cheapest = level_cost(0);
int dearest = level_cost(MAX_LEVEL);
return cheapest + dearest;
}
A constant’s initializer is checked as you wrote it, like any constant (Integer semantics), at the start of every function in the file, whether or not the function uses it, so the error is reported at the constant even when nothing reads it:
#include <cerastes.h>
const int BIG = 1 << 40;
int scaled(int x)
CERASTES_REQUIRES(0 <= x && x <= 1)
CERASTES_ENSURES(r, r == x * BIG)
{
return x * BIG;
}
named_constants_error.c:7: error: shift: the shift count may be negative or at least the width (32 bits) of `int`
Until it is fixed, such an error hides the other errors of the file. The case values of a function
are checked the same way, at its start, so a bad case value is reported even
where the switch is never reached.
Rejected: a global variable that is not const; a const declared extern
and not defined in the file; in C, const int z; without an initializer (another
file may define z, and the linker would use its value: write = 0, or make it
static); a weak constant (__attribute__((weak)), #pragma weak), whose
value another file may replace; a constant initialized by running code (in
C++, an initializer that is not a constant expression, such as
const int n = f();); volatile and thread_local constants; a global
declared in a system header. In C, a constant cannot be named where the language
requires a constant expression (a case value, _Static_assert, the
initializer of another file-scope constant): a const object is not a constant
expression in C, although some compilers accept it, so the front end refuses it.
C23’s constexpr objects are not accepted by the front end yet.
Keeping the Viper program
cerastes translates the program to Viper, the verification language of the
Silicon prover, and Silicon asks Z3. --keep-vpr writes the Viper text and
the position map that links each checked line of it to your source:
build/cerastes verify midpoint.cpp --keep-vpr
This writes midpoint.vpr and midpoint.vpr.map into the current directory.
--keep-vpr=<path> writes <path> and <path>.map. The files are written for
every run that reaches the prover (exit status 0, 1 or 3), whatever the
verdict. They are for people who want to see what was checked; the reports
already point at your source, never at the Viper file.
The single-unit assumption
A result is for one translation unit. A contract you declare for a function that is defined elsewhere, in another source file or in a library, is trusted: the callers in this unit are verified against it, and nothing here checks that the definition satisfies it. The success line says so. The definition is checked when the file that contains it is verified; a library function that is never verified is trusted for good.
#include <cerastes.h>
int next_id(int last)
CERASTES_REQUIRES(0 <= last && last < 1000000)
CERASTES_ENSURES(r, r == last + 1);
int two_ids(int last)
CERASTES_REQUIRES(0 <= last && last < 1000)
CERASTES_ENSURES(r, r == last + 2)
{
return next_id(next_id(last));
}
Here two_ids is verified assuming next_id meets its contract. A function
that has neither a contract nor a body in a verified file cannot be called from
verified code: the call is rejected, because there is nothing to verify it
against.
The same holds for constants: a constant’s value is the one in this file. In
C++ a namespace-scope const has internal linkage, so the file that
defines a function whose contract you trust may give a constant of the same
name another value; keep such constants in one header that both files include.
And for enumerations: a value of an enumeration is assumed to be one of its values (Enumerations) when it comes from outside the verified code, as a parameter or as the result of a function whose contract you trust. Every conversion into an enumeration in verified code is checked, but C lets code in another file store any value of the integer type the compiler picks for the enumeration, and such a value is outside what the result covers.
What a result means
A verified function terminates on every input that satisfies its requires,
has no undefined behaviour on the way, and returns a value that satisfies its
ensures, or ends the program by calling a function that does not return (such
as abort or exit): the result is total correctness. A call of exit in
main ends the program without checking the CERASTES_ENSURES of main. (In these programs, which have no
input or output, a loop or recursion that never ends is undefined behaviour in
C++ anyway, which is why cerastes does not leave it unchecked.)
What a result rests on is the single-unit assumption above, and it includes termination. A contract you declare for a function that is defined elsewhere is trusted to hold and to terminate. This is most visible with a contract that cannot be met:
#include <cerastes.h>
int never_returns(int x)
CERASTES_REQUIRES(x >= 0)
CERASTES_ENSURES(r, false);
int absurd(int x)
CERASTES_REQUIRES(x >= 0)
CERASTES_ENSURES(r, r == 42)
{
return never_returns(x);
}
absurd verifies, because its only path calls a function that, by its
contract, never returns. If never_returns can in fact return, the
verification says nothing about absurd. A contract you declare for code you
have not verified is something you promise, not something the tool checks.
The termination assumption has a limit that the check cannot see. If a function of this file calls a function that is defined in another file, which in turn calls back a function of this file, the cycle goes through code that was verified in another run: each file verifies, and the program does not terminate. A recursion cycle that passes through a function defined in another translation unit is not detected.
A parameter in a postcondition denotes the value it had on entry, as the
Annotations page describes. CERASTES_CHECK and a loop
invariant see its current value.
Both modes mean the same
The same source means the same in C23 and in C++20. Where the two
standards differ (conversions to a signed type, left shift of a negative
value), cerastes uses the stricter rule in both, so a .c file verifies, or
fails, identically in either mode:
#include <cerastes.h>
int clamp(int x, int lo, int hi)
CERASTES_REQUIRES(lo <= hi)
CERASTES_ENSURES(r, lo <= r && r <= hi)
{
if (x < lo) {
return lo;
}
if (x > hi) {
return hi;
}
return x;
}
What is not supported yet
cerastes rejects what it cannot verify, with a diagnostic at the line, and never approximates it:
#include <cerastes.h>
double half(double x)
{
return x / 2;
}
The constructs that are rejected in this version are:
-
Statements: a
gotothat is not outward and forward (Jumping forward with goto), the GNU computedgoto,tryandthrow,asm, aniforswitchwith an init-statement or with a declaration in its condition, and the forms ofswitchlisted in Leaving a loop early: break, continue, do and switch. -
Declarations: a
constlocal with no initializer, a local initialized from itself (int x = x;),static,thread_localandexternlocals, global variables other than the constants of Named constants, arrays, classes, unions, an enumeration declared in a function, a class or a template, or with aboolunderlying type or an enumerator outside the range ofint, an enumeration declared without its enumerators and without an underlying type, lambdas, templates,constexprandconstevalfunctions, variadic functions, function pointers,CERASTES_REQUIRESonmain, amaindeclared not to return, andCERASTES_ENSURESon a function declared not to return. -
Calls: of a function with no body in the file and no contract, among them the functions of the C library other than
abort,exitand_Exit(for examplequick_exit,__builtin_trap,__builtin_unreachableandstd::terminate). -
Types: pointers, references, floating point,
volatile,_BitInt, vector types, enumerations declared in system headers. -
Expressions: the bitwise operators (
&,|,^,~), two calls in one expression that are neither nested in each other nor ordered by the comma operator (f(x) + g(y)), a call in the second operand of&&,||or?:, or in a loop condition, the shapes of Side effects in expressions (an unsequenced modification, an assignment as a condition without extra parentheses, a comma operand without effect, a discarded value around a side effect, a side effect insizeof), and, in C, a variable named where a constant expression is required (acasevalue,_Static_assert, an enumerator’s value), and an enumeration value that the compiler converts to an integer for arithmetic, a comparison of order, a unary operator, a shift or an arm of?:(comparing the order of two values of the same enumeration is allowed), a?:whose result is an enumeration, aswitchon an enumeration that the compilers'-Wswitchrejects (acasevalue that is not an enumerator, or a missing enumerator withoutdefault), in C an enumerator’s value that names an earlier enumerator whose initializer has a type other thanint(the front end gives it the C++ type) or that usessizeofor_Alignofof an expression ortypeof, and compound assignment,++and--on an enumeration (Enumerations):#include <cerastes.h> enum color { RED, GREEN, BLUE }; int plus_one(enum color c) { return c + 1; } -
Attributes: any attribute other than the cerastes ones and
maybe_unused,nodiscard,deprecated,likely,unlikely,fallthroughandnoreturn(some attributes, such asassume, would let you state an unchecked fact). -
Pragmas: a
#pragmathe front end does not recognize (among them#pragma clang attribute, which applies attributes the verifier would not see, and#pragma clang diagnosticor#pragma GCC diagnostic), and the front end’s own#pragma diag_suppressand its relatives, which could turn the errors cerastes relies on into warnings.
A function with a body but no contract is verified against the default contract: its parameters and its result are in the range of their types.