Bit vectors and bitwise operations
Verus offers two dedicated mechanisms for reasoning about bit manipulation
(e.g., to prove that xor(w, w) == 0
). Small, one-off proofs can be done
via assert(...) by(bit_vector)
. Larger proofs, or proofs that will be needed in more than one place, can be done by writing a proof function and adding the annotation
by(bit_vector)
.
Both mechanisms export facts expressed over integers (e.g., in terms of u32
), but internally, they translate the proof obligations into assertions about bit vectors and use a dedicated solver to discharge those assertions.
assert(...) by(bit_vector)
This style can be used to prove a short and context-free bit-manipulation property. Here are two example use cases:
fn test_passes(b: u32) {
assert(b & 7 == b % 8) by (bit_vector);
assert(b & 0xff < 0x100) by (bit_vector);
}
Currently, assertions expressed via assert(...) by(bit_vector)
do not include any ambient facts from the surrounding context (e.g., from the surrounding function’s requires
clause or from previous variable assignments). For example, the following example will fail.
fn test_fails(x: u32, y: u32)
requires x == y
{
assert(x & 3 == y & 3) by(bit_vector); // Fails
}
To make ambient facts available, add a requires
clause to “import” these facts into the bit-vector assertion. For example, the following example will now succeed.
fn test_success(x: u32, y: u32)
requires
x == y,
{
assert(x & 3 == y & 3) by (bit_vector)
requires
x == y,
; // now x == y is available for the bit_vector proof
}
proof fn ... by(bit_vector)
This mechanism should be used when proving more complex facts about bit manipulation or when a proof will be used more than once. To use this mechanism, you should write a function in proof
mode.
The function should not have a body. Context can be provided via a requires
clause.
For example:
proof fn de_morgan_auto()
by (bit_vector)
ensures
forall|a: u32, b: u32| #[trigger] (!(a & b)) == !a | !b,
forall|a: u32, b: u32| #[trigger] (!(a | b)) == !a & !b,
{
}
Limitations
Supported Expressions
The bit-manipulation reasoning mechanism supports only a subset of the full set of expressions Verus offers. Currently, it supports:
- Unsigned integer types (as well as the
as
keyword between unsigned integers) - Built-in operators
let
binding- Quantifiers
if-then-else
Note that function calls are not supported. As a workaround, you may consider using a macro instead of a function.
Bitwise Operators As Uninterpreted Functions
Outside of by(bit_vector)
, bitwise operators are translated into uninterpreted functions in Z3, meaning Z3 knows nothing about them when used in other contexts.
As a consequence, basic properties such as the commutativity and associativity of bitwise-AND will not be applied automatically. To make use of these properties, please refer to this example file, which contains basic properties for bitwise operators.
Naming Arithmetic Operators: add/sub/mul
Inside a bit-vector assertion, please use add
, sub
, and mul
for fixed-width operators instead of +
-
*
, as the latter operators widen the result to a mathematical integer.
Bit-Manipulation Examples Using the get_bit!
and set_bit!
Macros
You can use two macros, get_bit!
and set_bit!
, to access and modify a single bit of an integer variable. Please refer our garbage collection example and our bitvector equivalence example.