Integers¶
Integer semantics depend on where the value lives.
Kind |
Semantics |
|---|---|
|
Mathematical |
|
Machine integer (target width; unsigned wraps modulo |
|
Machine integer (target width; unsigned wraps modulo |
Contract arithmetic ( |
Mathematical |
Contracts are mathematical, as in ACSL and Verus. +, -, *,
/, %, and unary - in a contract are exact, so
cppverify::post(cppverify::result + 1 > cppverify::result) holds rather than overflowing. An implicit
conversion whose result C++ could change (a narrowing or a sign change) keeps
the value; a value-preserving one is an ordinary extension. Quantifier binders
are mathematical integers. A negative constant converted to an unsigned type
draws a warning, since the contract compares its exact value. An explicit
cast still converts and is checked; write wraparound explicitly as
% 4294967296.
Mathematical integers are unbounded, but / and % retain C++’s
truncate-toward-zero sign rules. Their total logical extension at a zero divisor
is quotient zero and remainder equal to the dividend. Executable machine
evaluation sites still emit a nonzero-divisor proof obligation.
Width and signedness¶
Each machine integer’s bit width comes from the target’s data model
(ASTContext::getTypeSize), so int is checked at 32 bits and long /
long long at 64 bits on an LP64 target. Mixed-width arithmetic
((long)a + b) sign-extends the narrower operand, exactly as C++ does.
Narrow integers and extensions such as __int128 retain their target widths;
the usual integral promotions are applied before arithmetic.
Signedness is tracked too, and it matters: signed overflow is undefined behavior in C++, while unsigned overflow is defined modular wraparound. The verifier treats them differently. Heap payloads are width-neutral mathematical integers; typed loads and stores perform the required target-width conversions.
Solver encoding¶
How a machine integer reaches the solver is independent of its meaning.
--int-encoding=bitvector uses solver bit-vectors. --int-encoding=integer
uses mathematical integers kept in the type’s range: every variable carries its
range, and every operation is reduced modulo 2^N, so wraparound, division by
zero, conversions, and overflow checks are exactly the bit-vector ones. Integers
combine far better with quantifiers and the heap, for example a 64-bit
cppverify::forall over a buffer. The default auto uses integers for a query unless
it needs the bits of a non-constant operand (a & b, x << s); masks such as
x & 0xff and constant shifts are arithmetic and keep integers. Every mode
gives the same verdicts and counterexamples, so the flag is purely a
performance choice.
Mandatory C++ definedness¶
Executable and cppverify::proof code must be well-defined C++. CppVerify therefore
generates path-sensitive safety obligations automatically; these checks are not
optional because a functional proof about an undefined execution would be
meaningless.
Core checks include:
Operation |
Obligation |
|---|---|
signed |
does not overflow (at the operand’s width) |
|
divisor |
signed |
not |
|
shift count is in range; signed left shift satisfies the C++17 rule |
pointer loads and stores |
base pointer is non-null and abstractly valid |
mathematical value converted to a machine type |
value is in the type’s range |
Unsigned arithmetic is never flagged — C++ defines it as modular wraparound,
so the machine operation wraps modulo 2^N. Contract arithmetic is
mathematical, though: a + b in a contract is the exact sum, so a
contract that means the wrapped value says so with % 2^N.
int abs_unguarded(int x) cv::post(cv::result >= 0)
{ return x < 0 ? -x : x; }
int abs_guarded(int x) cv::pre(x > -2147483648) cv::post(cv::result >= 0)
{ return x < 0 ? -x : x; }
unsigned mix(unsigned a, unsigned b) cv::post(cv::result == (a + b) % 4294967296)
{ return a + b; }
unsigned mix_wrong(unsigned a, unsigned b) cv::post(cv::result == a + b)
{ return a + b; }
abs.cpp:2:3: error: verification failed: abs_unguarded [...::overflow@2:3]
(counterexample: x [type=i32] = -2147483648)
Verified: abs_guarded [backend=z3]
Verified: mix [backend=z3]
abs.cpp:10:64: error: verification failed: mix_wrong [...::postcondition@10:64]
(counterexample: result [type=u32] = 0, a [type=u32] = 1, ...)
Negating INT_MIN overflows, so abs_unguarded fails at the overflow
check with that input, and the precondition the tool asks for makes
abs_guarded verify. mix_wrong claims the exact sum, which the
wrapped result is not when the sum reaches 2^32.
Memory bounds¶
Memory accesses are checked by default (--check-ub; --no-check-ub
turns it off). Write valid(p, n) in a precondition to declare an extent:
every p[i] or *(p + i) access rooted at p must then prove
0 <= i < n, and pointer arithmetic must stay in [0, n]. The marker
itself requires n >= 0 and, for a positive extent, a non-null abstractly
valid pointer; extent zero permits null. A pointer without a valid
declaration addresses a single object, so p[0] is fine and p[1] must
be proved inside that object, which fails. Typed pointer steps are converted
to target-byte offsets using sizeof(T), but bounds remain half-open
element bounds. The marker must be a positive top-level conjunction clause on
the bare pointer. See Pointers.
Concrete extent, lifetime, alignment, and initialization metadata is tracked
for the bounded local scalar new/delete subset and for promoted
automatic objects (address-taken scalar locals, fixed local arrays, and their
enclosing records). General buffer
provenance and parameter-pointer extents remain abstract; see
Dynamic storage and Current limitations and C++ readiness.
Lifted constexpr functions retain target machine widths. At each call the
verifier unfolds the body for C++ definedness checks, so signed overflow, invalid
shifts, and division undefined behavior cannot be justified by machine
wraparound.
Signed left shift follows the C++17 rule: the left operand must be nonnegative
and the shifted value must fit the corresponding unsigned type. This permits
constructing the sign bit (for example, 1 << 31 for a 32-bit int) while
still rejecting values beyond the unsigned range.
Calls crossing between mathematical cppverify::spec code and lifted machine
constexpr code convert at each parameter and return boundary. A machine
result converts exactly to an unbounded integer; an unsigned result has already
wrapped at its target width, as C++ defines. A mathematical argument must fit
the machine parameter type.
A mathematical value never wraps into a C++ type:
A spec result, and a length, element, or count of a spec collection, stays unbounded wherever it is used: in contracts and in
cppverify::ghostandcppverify::proofcode alike. Arithmetic, selections, and comparisons involving it are exact, and the implicit conversions of C++’s usual arithmetic conversions do not bound it, socppverify::post(cppverify::result == total(x) + n)compares exact values ands.subrange(0, s.len() - 1)in a proof is exact. A comparison between a machine value and a mathematical one is exact.Storing such a value in a
cppverify::ghostorcppverify::proofvariable, passing it to a machine parameter, returning it, an explicit cast such as(int)total(x), and a bitwise operator applied to it convert it to a machine type. The conversion is defined only when the value fits, and it carries anoverflowobligation instead of wrapping:
cv::spec int scaled(int x) { return x * 1000; }
cv::proof void lemma(int x) cv::pre(x >= 0 && x <= 1000)
{ int value = scaled(x); } // verifies: the value fits in int
cv::proof void too_big(int x) cv::pre(x == 3000000)
{ int value = scaled(x); } // FAILS (overflow): 3000000000 is not an int
cv::proof void element(cppverify::seq s) cv::pre(s.len() > 0)
{ int x = s[0]; } // FAILS (overflow): an element need not fit
Executable code cannot call a spec function at all; see Ghost, spec, and proofs.
Executable modular calls likewise apply Clang’s formal-parameter and destination conversions. Signedness, widening, and narrowing therefore occur before a callee contract is instantiated and before a returned value is used by the caller.
Exact int boundaries¶
CppVerify’s permanent acceptance suite proves recursive and iterative
implementations against unbounded mathematical specifications while retaining
32-bit signed int execution:
factorial is verified for
0 <= n <= 12; computing13!is rejected because6227020800is not representable;Fibonacci is verified for
0 <= n <= 46; computingF(47)is rejected because2971215073is not representable.
These are C++ representation limits, not arbitrary verifier cutoffs. The negative cases exercise the same path-sensitive signed-overflow obligations as ordinary executable code.