Verus overview

Verus is a tool for verifying the correctness of code written in Rust. The main goal is to verify full functional correctness of low-level systems code, building on ideas from existing verification frameworks like Dafny, Boogie, F*, VCC, Prusti, Creusot, Aeneas, Cogent, Coq, and Isabelle/HOL. Verification is static: Verus adds no run-time checks, but instead uses computer-aided theorem proving to statically verify that executable Rust code will always satisfy some user-provided specifications for all possible executions of the code.

In more detail, Verus aims to:

  • provide a pure mathematical language for expressing specifications (like Dafny, Creusot, F*, Coq, Isabelle/HOL)
  • provide a mathematical language for expressing proofs (like Dafny, F*, Coq, Isabelle/HOL) based exclusively on classical logic (like Dafny)
  • provide a low-level, imperative language for expressing executable code (like VCC), based on Rust (like Prusti, Creusot, and Aeneas)
  • generate small, simple verification conditions that an SMT solver like Z3 can solve efficiently, based on the following principles:
    • keep the mathematical specification language close to the SMT solver’s mathematical language (like Boogie)
    • use lightweight linear type checking, rather than SMT solving, to reason about memory and aliasing (like Cogent, Creusot, Aeneas, and linear Dafny)

We believe that Rust is a good language for achieving these goals. Rust combines low-level data manipulation, including manual memory management, with an advanced, high-level, safe type system. The type system includes features commonly found in higher-level verification languages, including algebraic datatypes (with pattern matching), type classes, and first-class functions. This makes it easy to express specifications and proofs in a natural way. More importantly, Rust’s type system includes sophisticated support for linear types and borrowing, which takes care of much of the reasoning about memory and aliasing. As a result, the remaining reasoning can ignore most memory and aliasing issues, and treat the Rust code as if it were code written in a purely functional language, which makes verification easier.

This guide

This guide assumes that you’re already somewhat familiar with the basics of Rust programming. (If you’re not, we recommend spending a couple hours on the Learn Rust page.) Familiarity with Rust is useful for Verus, because Verus builds on Rust’s syntax and Rust’s type system to express specifications, proofs, and executable code. In fact, there is no separate language for specifications and proofs; instead, specifications and proofs are written in Rust syntax and type-checked with Rust’s type checker. So if you already know Rust, you’ll have an easier time getting started with Verus.

Nevertheless, verifying the correctness of Rust code requires concepts and techniques beyond just writing ordinary executable Rust code. For example, Verus extends Rust’s syntax (via macros) with new concepts for writing specifications and proofs, such as forall, exists, requires, and ensures, as well as introducing new types, like the mathematical integer types int and nat. It can be challenging to prove that a Rust function satisfies its postconditions (its ensures clauses) or that a call to a function satisfies the function’s preconditions (its requires clauses). Therefore, this guide’s tutorial will walk you through the various concepts and techniques, starting with relatively simple concepts (basic proofs about integers), moving on to more moderately difficult challenges (inductive proofs about data structures), and then on to more advanced topics such as proofs about arrays using forall and exists and proofs about concurrent code.

All of these proofs are aided by an automated theorem prover (specifically, Z3, a satisfiability-modulo-theories solver, or “SMT solver” for short). The SMT solver will often be able to prove simple properties, such as basic properties about booleans or integer arithmetic, with no additional help from the programmer. However, more complex proofs often require effort from both the programmer and the SMT solver. Therefore, this guide will also help you understand the strengths and limitations of SMT solving, and give advice on how to fill in the parts of proofs that SMT solvers cannot handle automatically. (For example, SMT solvers usually cannot automatically perform proofs by induction, but you can write a proof by induction simply by writing a recursive Rust function whose ensures clause expresses the induction hypothesis.)

Getting Started

To get started with Verus, use git clone to fetch the Verus source code from the Verus GitHub page, and then follow the directions on the Verus GitHub page to build Verus.

Let’s try running Verus on the following sample Rust program, found at getting_started.rs:

use vstd::prelude::*;

verus! {

spec fn min(x: int, y: int) -> int {
    if x <= y {
        x
    } else {
        y
    }
}

fn main() {
    assert(min(10, 20) == 10);
    assert(min(-10, -20) == -20);
    assert(forall|i: int, j: int| min(i, j) <= i && min(i, j) <= j);
    assert(forall|i: int, j: int| min(i, j) == i || min(i, j) == j);
    assert(forall|i: int, j: int| min(i, j) == min(j, i));
}

} // verus!

To run Verus on this code, change to the source directory and type the following in Unix:

./target-verus/release/verus rust_verify/example/guide/getting_started.rs

or the following in Windows:

.\target-verus\release\verus.exe rust_verify\example\guide\getting_started.rs

You should see the following output:

note: verifying root module

verification results:: 1 verified, 0 errors

This indicates that Verus successfully verified 1 function (the main function). If you want, you can try editing the rust_verify/example/guide/getting_started.rs file to see a verification failure. For example, if you add the following line to main:

    assert(forall|i: int, j: int| min(i, j) == min(i, i));

you will see an error message:

note: verifying root module

error: assertion failed
  --> example/guide/getting_started.rs:19:12
   |
19 |     assert(forall|i: int, j: int| min(i, j) == min(i, i));
   |            ^^^^^^ assertion failed

error: aborting due to previous error

verification results:: 0 verified, 1 errors

Using Verus in Rust files

Verus uses a macro named verus! to extend Rust’s syntax with verification-related features such as preconditions, postconditions, assertions, forall, exists, etc. Therefore, each file in a crate will typically contain the following declarations:

use vstd::prelude::*;

verus! {

In the remainder of this guide, we will omit these declarations from the examples to avoid clutter. However, remember that any example code should be placed inside the verus! { ... } block, and that the file should use vstd::prelude::*;.

Compilation

The instructions above verify a Rust file without compiling it. To both verify and compile a Rust file, add the --compile command-line option. For example:

./target-verus/release/verus --compile rust_verify/example/guide/getting_started.rs

This will generate an executable for getting_started.rs. (However, in this example, the executable won’t do anything interesting, because the main function contains no executable code — it contains only statically-checked assertions, which are erased before compilation.)

Basic specifications

Verus programs contain specifications to describe the intended behavior of the code. These specifications include preconditions, postconditions, assertions, and loop invariants. Specifications are one form of ghost code — code that appears in the Rust source code for verification’s sake, but does not appear in the compiled executable.

This chapter will walk through some basic examples of preconditions, postconditions, and assertions, showing the syntax for writing these specifications and discussing integer arithmetic and equality in specifications.

Preconditions (requires clauses)

Let’s start with a small example. Suppose we want to verify a function octuple that multiplies a number by 8:

fn octuple(x1: i8) -> i8 {
    let x2 = x1 + x1;
    let x4 = x2 + x2;
    x4 + x4
}

If we ask Verus to verify this code, Verus immediately reports errors about octuple:

error: possible arithmetic underflow/overflow
   |
   |     let x2 = x1 + x1;
   |              ^^^^^^^

Here, Verus cannot prove that the result of x1 + x1 fits in an 8-bit i8 value, which allows values in the range -128127. If x1 were 100, for example, x1 + x1 would be 200, which is larger than 127. We need to make sure that when octuple is called, the argument x1 is not too large. We can do this by adding preconditions (also known as “requires clauses”) to octuple specifying which values for x1 are allowed. In Verus, preconditions are written with a requires followed by zero or more boolean expressions separated by commas:

fn octuple(x1: i8) -> i8
    requires
        -64 <= x1,
        x1 < 64,
{
    let x2 = x1 + x1;
    let x4 = x2 + x2;
    x4 + x4
}

The two preconditions above say that x1 must be at least -64 and less than 64, so that x1 + x1 will fit in the range -128127. This fixes the error about x1 + x1, but we still get an error about x2 + x2:

error: possible arithmetic underflow/overflow
   |
   |     let x4 = x2 + x2;
   |              ^^^^^^^

If we want x1 + x1, x2 + x2, and x4 + x4 to all succeed, we need a tighter bound on x1:

fn octuple(x1: i8) -> i8
    requires
        -16 <= x1,
        x1 < 16,
{
    let x2 = x1 + x1;
    let x4 = x2 + x2;
    x4 + x4
}

This time, verification is successful.

Now suppose we try to call octuple with a value that does not satisfy octuple’s precondition:

fn main() {
    let n = octuple(20);
}

For this call, Verus reports an error, since 20 is not less than 16:

error: precondition not satisfied
   |
   |         x1 < 16,
   |         ------- failed precondition
...
   |     let n = octuple(20);
   |             ^^^^^^^^^^^

If we pass 10 instead of 20, verification succeeds:

fn main() {
    let n = octuple(10);
}

Postconditions (ensures clauses)

Suppose we want to verify properties about the value returned from octuple. For example, we might want to assert that the value returned from octuple is 8 times as large as the argument passed to octuple. Let’s try putting an assertion in main that the result of calling octuple(10) is 80:

fn main() {
    let n = octuple(10);
    assert(n == 80);
}

Although octuple(10) really does return 80, Verus nevertheless reports an error:

error: assertion failed
   |
   |     assert(n == 80);
   |            ^^^^^^^ assertion failed

The error occurs because, even though octuple multiplies its argument by 8, octuple doesn’t publicize this fact to the other functions in the program. To do this, we can add postconditions (ensures clauses) to octuple specifying some properties of octuple’s return value:

fn octuple(x1: i8) -> (x8: i8)
    requires
        -16 <= x1,
        x1 < 16,
    ensures
        x8 == 8 * x1,
{
    let x2 = x1 + x1;
    let x4 = x2 + x2;
    x4 + x4
}

To write a property about the return value, we need to give a name to the return value. The Verus syntax for this is -> (name: return_type). In the example above, saying -> (x8: i8) allows the postcondition x8 == 8 * x1 to use the name x8 for octuple’s return value.

Preconditions and postconditions establish a modular verification protocol between functions. When main calls octuple, Verus checks that the arguments in the call satisfy octuple’s preconditions. When Verus verifies the body of the octuple function, it can assume that the preconditions are satisfied, without having to know anything about the exact arguments passed in by main. Likewise, when Verus verifies the body of the main function, it can assume that octuple satisfies its postconditions, without having to know anything about the body of octuple. In this way, Verus can verify each function independently. This modular verification approach breaks verification into small, manageable pieces, which makes verification more efficient than if Verus tried to verify all of a program’s functions together simultaneously. Nevertheless, writing preconditions and postconditions requires significant programmer effort — if you want to verify a large program with a lot of functions, you’ll probably spend substantial time writing preconditions and postconditions for the functions.

assert and assume

While requires and ensures connect functions together, assert makes a local, private request to the SMT solver to prove a certain fact. (Note: assert(...) should not be confused with the Rust assert!(...) macro — the former is statically checked using the SMT solver, while the latter is checked at run-time.)

assert has an evil twin named assume, which asks the SMT solver to simply accept some boolean expression as a fact without proof. While assert is harmless and won’t cause any unsoundness in a proof, assume can easily enable a “proof” of a fact that isn’t true. In fact, by writing assume(false), you can prove anything you want:

assume(false);
assert(2 + 2 == 5); // succeeds

Verus programmers often use assert and assume to help develop and debug proofs. They may add temporary asserts to determine which facts the SMT solver can prove and which it can’t, and they may add temporary assumes to see which additional assumptions are necessary for the SMT solver to complete a proof, or as a placeholder for parts of the proof that haven’t yet been written. As the proof evolves, the programmer replaces assumes with asserts, and may eventually remove the asserts. A complete proof may contain asserts, but should not contain any assumes.

(In some situations, assert can help the SMT solver complete a proof, by giving the SMT hints about how to manipulate forall and exists expressions. There are also special forms of assert, such as assert(...) by(bit_vector), to help prove properties about bit vectors, nonlinear integer arithmetic, forall expressions, etc.)

Executable code and ghost code

Let’s put everything from this section together into a final version of our example program:

use vstd::prelude::*;

verus! {

#[verifier::external_body]
fn print_two_digit_number(i: i8)
    requires
        -99 <= i < 100,
{
    println!("The answer is {}", i);
}

fn octuple(x1: i8) -> (x8: i8)
    requires
        -16 <= x1 < 16,
    ensures
        x8 == 8 * x1,
{
    let x2 = x1 + x1;
    let x4 = x2 + x2;
    x4 + x4
}

fn main() {
    let n = octuple(10);
    assert(n == 80);
    print_two_digit_number(n);
}

} // verus!

Here, we’ve made a few final adjustments.

First, we’ve combined the two preconditions -16 <= x1 and x1 < 16 into a single preconditon -16 <= x1 < 16, since Verus lets us chain multiple inequalities together in a single expression (equivalently, we could have also written -16 <= x1 && x1 < 16).

Second, we’ve added a function print_two_digit_number to print the result of octuple. Unlike main and octuple, we ask Verus not to verify print_two_digit_number. We do this by marking it #[verifier::external_body], so that Verus pays attention to the function’s preconditions and postconditions but ignores the function’s body. This is common in projects using Verus: you may want to verify some of it (perhaps the program’s core algorithms), but leave other aspects, such as input-output operations, unverified. More generally, since verifying all the software in the world is still infeasible, there will be some boundary between verified code and unverified code, and #[verifier::external_body] can be used to mark this boundary.

We can now compile the program above using the --compile option to Verus:

./tools/rust-verify.sh --compile rust_verify/example/guide/requires_ensures.rs

This will produce an executable that prints a message when run:

The answer is 80

Note that the generated executable does not contain the requires, ensures, and assert code, since these are only needed during static verification, not during run-time execution. We refer to requires, ensures, assert, and assume as ghost code, in contast to the executable code that actually gets compiled. Verus erases all ghost code before compilation so that it imposes no run-time overhead.

Expressions and operators for specifications

Verus extends Rust’s syntax with additional operators and expressions useful for writing specifications. For example:

forall|i: int, j: int| 0 <= i <= j < len ==> f(i, j)

This snippet illustrates:

  • the forall quantifier, which we will cover later
  • chained operators
  • implication operators

Here, we’ll discuss the last two, along with Verus notation for conjunction, disjunction, and field access.

Chained inequalities

Specifications can chain together multiple <=, <, >=, and > operations. For example, 0 <= i <= j < len has the same meaning as 0 <= i && i <= j && j < len.

Logical implication

To make specifications more readable, Verus supports an implication operator ==>. The expression a ==> b (pronounced “a implies b”) is logically equivalent to !a || b. As an example, the expression

forall|i: int, j: int| 0 <= i <= j < len ==> f(i, j)

means that for every pair i and j such that 0 <= i <= j < len, f(i, j) is true.

Note that ==> has lower precedence that most other boolean operations. For example, a ==> b && c means a ==> (b && c). Verus also supports two-way implication for booleans (<==>) with even lower precedence, so that a <==> b && c is equivalent to a == (b && c). See the reference for a full description of precedence in Verus.

Conjunction and disjunction

Because &&, ||, and ==> are so common in Verus specifications, it is often desirable to have low precedence versions of && and ||. Verus also supports “triple-and” (&&&) and “triple-or” (|||) which are equivalent to && and || except for their precedence. Implication ==> and equivalence <==> bind more tightly than either &&& or |||. &&& and ||| are also convenient for the “bulleted list” form:

&&& a ==> b
&&& c
&&& d <==> e && f

This has the same meaning as (a ==> b) && c && (d <==> (e && f)).

Accessing fields of a struct or enum

Verus has ->, is, and matches syntax for accessing fields of structs and matching variants of enums.

Integer types

Rust supports various fixed-bit-width integer types:

  • i8, i16, i32, i64, i128, isize
  • u8, u16, u32, u64, u128, usize

To these, Verus adds two more integer types to represent arbitrarily large integers in specifications:

  • int
  • nat

The type int is the most fundamental type for reasoning about integer arithmetic in Verus. It represents all mathematical integers, both positive and negative. The SMT solver contains direct support for reasoning about values of type int.

Internally, Verus uses int to represent the other integer types, adding mathematical constraints to limit the range of the integers. For example, a value of the type nat of natural numbers is a mathematical integer constrained to be greater than or equal to 0. Rust’s fixed-bit-width integer types have both a lower and upper bound; a u8 value is an integer constrained to be greater than or equal to 0 and less than 256:

fn test_u8(u: u8) {
    assert(0 <= u < 256);
}

The bounds of usize and isize are platform dependent. By default, Verus assumes that these types may be either 32 bits or 64 bits wide, but this assumption may be configured. Verus recognizes the constants usize::BITS, usize::MAX, isize::MAX, and isize::MIN, which are useful for reasoning symbolically about the usize integer range.

Using integer types in specifications

Since there are 14 different integer types (counting int, nat, u8usize, and i8isize), it’s not always obvious which type to use when writing a specification. Our advice is to be as general as possible by default:

  • Use int by default, since this is the most general type and is supported most efficiently by the SMT solver.
    • Example: the Verus sequence library uses int for most operations, such as indexing into a sequence.
    • Note: as discussed below, most arithmetic operations in specifications produce values of type int, so it is usually most convenient to write specifications in terms of int.
  • Use nat for return values and datatype fields where the 0 lower bound is likely to provide useful information, such as lengths of sequences.
    • Example: the Verus Seq::len() function returns a nat to represent the length of a sequence.
    • The type nat is also handy for proving that recursive definitions terminate; you might to define a recursive factorial function to take a parameter of type nat, if you don’t want to provide a definition of factorial for negative integers.
  • Use fixed-width integer types for fixed-with values such as bytes.
    • Example: the bytes of a network packet can be represented with type Seq<u8>, an arbitrary-length sequence of 8-bit values.

Note that int and nat are usable only in ghost code; they cannot be compiled to executable code. For example, the following will not work:

fn main() {
    let i: int = 5; // FAILS: executable variable `i` cannot have type `int`, which is ghost-only
}

Integer constants

As in ordinary Rust, integer constants in Verus can include their type as a suffix (e.g. 7u8 or 7u32 or 7int) to precisely specify the type of the constant:

fn test_consts() {
    let u: u8 = 1u8;
    assert({
        let i: int = 2int;
        let n: nat = 3nat;
        0int <= u < i < n < 4int
    });
}

Usually, but not always, Verus and Rust will be able to infer types for integer constants, so that you can omit the suffixes unless the Rust type checker complains about not being able to infer the type:

fn test_consts_infer() {
    let u: u8 = 1;
    assert({
        let i: int = 2;
        let n: nat = 3;
        0 <= u < i < n < 4
    });
}

Note that the values 0, u, i, n, and 4 in the expression 0 <= u < i < n < 4 are allowed to all have different types — you can use <=, <, >=, >, ==, and != to compare values of different integer types inside ghost code (e.g. comparing a u8 to an int in u < i).

Constants with the suffix int and nat can be arbitrarily large:

fn test_consts_large() {
    assert({
        let i: int = 0x10000000000000000000000000000000000000000000000000000000000000000int;
        let j: int = i + i;
        j == 2 * i
    });
}

Integer coercions using “as”

As in ordinary rust, the as operator coerces one integer type to another. In ghost code, you can use as int or as nat to coerce to int or nat:

fn test_coerce() {
    let u: u8 = 1;
    assert({
        let i: int = u as int;
        let n: nat = u as nat;
        u == i && u == n
    });
}

You can use as to coerce a value v to a type t even if v is too small or too large to fit in t. However, if the value v is outside the bounds of type t, then the expression v as t will produce some arbitrary value of type t:

fn test_coerce_fail() {
    let v: u16 = 257;
    let u: u8 = v as u8;
    assert(u == v); // FAILS, because u has type u8 and therefore cannot be equal to 257
}

This produces an error for the assertion, along with a hint that the value in the as coercion might have been out of range:

error: assertion failed
   |
   |     assert(u == v); // FAILS, because u has type u8 and therefore cannot be equal to 257
   |            ^^^^^^ assertion failed

note: recommendation not met: value may be out of range of the target type (use `#[verifier::truncate]` on the cast to silence this warning)
   |
   |     let u: u8 = v as u8;
   |                 ^

See the reference for more on how Verus defines as-truncation and how to reason about it.

Integer arithmetic

Integer arithmetic behaves a bit differently in ghost code than in executable code.

In executable code, we frequently have to reason about integer overflow, and in fact, Verus requires us to prove the absence of overflow. The following operation fails because the arithmetic might produce an operation greater than 255:

fn test_sum(x: u8, y: u8) {
    let sum1: u8 = x + y; // FAILS: possible overflow
}
error: possible arithmetic underflow/overflow
   |
   |     let sum1: u8 = x + y; // FAILS: possible overflow
   |                    ^^^^^

In ghost code, however, common arithmetic operations (+, -, *, /, %) never overflow or wrap. To make this possible, Verus widens the results of many operations; for example, adding two u8 values is widened to type int.

fn test_sum2(x: u8, y: u8) {
    assert({
        let sum2: int = x + y;  // in ghost code, + returns int and does not overflow
        0 <= sum2 < 511
    });
}

Since + does not overflow in ghost code, we can easily write specifications about overflow. For example, to make sure that the executable x + y doesn’t overflow, we simply write requires x + y < 256, relying on the fact that x + y is widened to type int in the requires clause:

fn test_sum3(x: u8, y: u8)
    requires
        x + y < 256,  // make sure "let sum1: u8 = x + y" can't overflow
{
    let sum1: u8 = x + y;  // succeeds
}

Also note that the inputs need not have the same type; you can add, subtract, or multiply one integer type with another:

fn test_sum_mixed(x: u8, y: u16) {
    assert(x + y >= y);  // x + y has type int, so the assertion succeeds
    assert(x - y <= x);  // x - y has type int, so the assertion succeeds
}

In general in ghost code, Verus widens native Rust integer types to int for operators like +, -, and * that might overflow; the reference page describes the widening rules in more detail.

Here are some more tips to keep in mind:

  • In ghost code, / and % compute Euclidean division and remainder, rather than Rust’s truncating division and remainder, when operating on negative left-hand sides or negative right-hand sides.
  • Division-by-0 and mod-by-0 are errors in executable code and are unspecified in ghost code (see Ghost code vs. exec code for more detail).
  • The named arithmetic functions, add(x, y), sub(x, y), and mul(x, y), do not perform widening, and thus have truncating behavior, even in ghost code. Verus also recognizes some Rust functions like wrapped_add and checked_add, which may be used in either executable or ghost code.

Equality

Equality behaves differently in ghost code than in executable code. In executable code, Rust defines == to mean a call to the eq function of the PartialEq trait:

fn equal1(x: u8, y: u8) {
    let eq1 = x == y;  // means x.eq(y) in Rust
    let eq2 = y == x;  // means y.eq(x) in Rust
    assert(eq1 ==> eq2);  // succeeds
}

For built-in integer types like u8, the x.eq(y) function is defined as we’d expect, returning true if x and y hold the same integers. For user-defined types, though, eq could have other behaviors: it might have side effects, behave nondeterministically, or fail to fulfill its promise to implement an equivalence relation, even if the type implements the Rust Eq trait:

fn equal2<A: Eq>(x: A, y: A) {
    let eq1 = x == y; // means x.eq(y) in Rust
    let eq2 = y == x; // means y.eq(x) in Rust
    assert(eq1 ==> eq2); // won't work; we can't be sure that A is an equivalence relation
}

In ghost code, by contrast, the == operator is always an equivalence relation (i.e. it is reflexive, symmetric, and transitive):

fn equal3(x: u8, y: u8) {
    assert({
        let eq1 = x == y;
        let eq2 = y == x;
        eq1 ==> eq2
    });
}

Verus defines == in ghost code to be true when:

  • for two integers or booleans, the values are equal
  • for two structs or enums, the types are the same and the fields are equal
  • for two & references, two Box values, two Rc values, or two Arc values, the pointed-to values are the same
  • for two RefCell values or two Cell values, the pointers to the interior data are equal (not the interior contents)

In addition, collection dataypes such as Seq<T>, Set<T>, and Map<Key, Value> have their own definitions of ==, where two sequences, two sets, or two maps are equal if their elements are equal. As explained more in specification libraries and extensional equality, these sometimes require the “extensional equality” operator =~= to help prove equality between two sequences, two sets, or two maps.

Specification code, proof code, executable code

Verus classifies code into three modes: spec, proof, and exec, where:

  • spec code describes properties about programs
  • proof code proves that programs satisfy properties
  • exec code is ordinary Rust code that can be compiled and run

Both spec code and proof code are forms of ghost code, so we can organize the three modes in a hierarchy:

  • code
    • ghost code
      • spec code
      • proof code
    • exec code

Every function in Verus is either a spec function, a proof function, or an exec function:

spec fn f1(x: int) -> int {
    x / 2
}

proof fn f2(x: int) -> int {
    x / 2
}

// "exec" is optional, and is usually omitted
exec fn f3(x: u64) -> u64 {
    x / 2
}

exec is the default function annotation, so it is usually omitted:

fn f3(x: u64) -> u64 { x / 2 } // exec function

The rest of this chapter will discuss these three modes in more detail. As you read, you can keep in mind the following relationships between the three modes:

spec codeproof codeexec code
can contain spec code, call spec functionsyesyesyes
can contain proof code, call proof functionsnoyesyes
can contain exec code, call exec functionsnonoyes

spec functions

Let’s start with a simple spec function that computes the minimum of two integers:

spec fn min(x: int, y: int) -> int {
    if x <= y {
        x
    } else {
        y
    }
}

fn test() {
    assert(min(10, 20) == 10); // succeeds
    assert(min(100, 200) == 100); // succeeds
}

Unlike exec functions, the bodies of spec functions are visible to other functions in the same module, so the test function can see inside the min function, which allows the assertions in test to succeed.

Across modules, the bodies of spec functions can be made public to other modules or kept private to the current module. The body is public if the function is marked open, allowing assertions about the function’s body to succeed in other modules:

mod M1 {
    use builtin::*;

    pub open spec fn min(x: int, y: int) -> int {
        if x <= y {
            x
        } else {
            y
        }
    }
}

mod M2 {
    use builtin::*;
    use crate::M1::*;

    fn test() {
        assert(min(10, 20) == 10); // succeeds
    }
}

By contrast, if the function is marked closed, then other modules cannot see the function’s body, even if they can see the function’s declaration. However, functions within the same module can view a closed spec fn’s body. In other words, pub makes the declaration public, while open and closed make the body public or private. All pub spec functions must be marked either open or closed; Verus will complain if the function lacks this annotation.

mod M1 {
    use builtin::*;

    pub closed spec fn min(x: int, y: int) -> int {
        if x <= y {
            x
        } else {
            y
        }
    }

    pub proof fn lemma_min(x: int, y: int)
        ensures
            min(x,y) <= x && min(x,y) <= y,
    {}
}

mod M2 {
    use builtin::*;
    use crate::M1::*;

    fn test() {
        assert(min(10, 20) == min(10, 20)); // succeeds
        assert(min(10, 20) == 10); // FAILS
        proof {
            lemma_min(10,20);
        }
        assert(min(10, 20) <= 10); // succeeds
    }
}

In the example above with min being closed, the module M2 can still talk about the function min, proving, for example, that min(10, 20) equals itself (because everything equals itself, regardless of what’s in it’s body). On the other hand, the assertion that min(10, 20) == 10 fails, because M2 cannot see min’s body and therefore doesn’t know that min computes the minimum of two numbers:

error: assertion failed
   |
   |         assert(min(10, 20) == 10); // FAILS
   |                ^^^^^^^^^^^^^^^^^ assertion failed

After the call to lemma_min, the assertion that min(10, 20) <= 10 succeeds because lemma_min exposes min(x,y) <= x as a post-condition. lemma_min can prove because this postcondition because it can see the body of min despite min being closed, as lemma_min and min are in the same module.

You can think of pub open spec functions as defining abbreviations and pub closed spec functions as defining abstractions. Both can be useful, depending on the situation.

spec functions may be called from other spec functions and from specifications inside exec functions, such as preconditions and postconditions. For example, we can define the minimum of three numbers, min3, in terms of the mininum of two numbers. We can then define an exec function, compute_min3, that uses imperative code with mutable updates to compute the minimum of 3 numbers, and defines its postcondition in terms of the spec function min3:

spec fn min(x: int, y: int) -> int {
    if x <= y {
        x
    } else {
        y
    }
}

spec fn min3(x: int, y: int, z: int) -> int {
    min(x, min(y, z))
}

fn compute_min3(x: u64, y: u64, z: u64) -> (m: u64)
    ensures
        m == min3(x as int, y as int, z as int),
{
    let mut m = x;
    if y < m {
        m = y;
    }
    if z < m {
        m = z;
    }
    m
}

fn test() {
    let m = compute_min3(10, 20, 30);
    assert(m == 10);
}

The difference between min3 and compute_min3 highlights some differences between spec code and exec code. While exec code may use imperative language features like mutation, spec code is restricted to purely functional mathematical code. On the other hand, spec code is allowed to use int and nat, while exec code is restricted to compilable types like u64.

proof functions

Consider the pub closed spec min function from the previous section. This defined an abstract min function without revealing the internal definition of min to other modules. However, an abstract function definition is useless unless we can say something about the function. For this, we can use a proof function. In general, proof functions will reveal or prove properties about specifications. In this example, we’ll define a proof function named lemma_min that reveals properties about min without revealing the exact definition of min. Specifically, lemma_min reveals that min(x, y) equals either x or y and is no larger than x and y:

mod M1 {
    use builtin::*;

    pub closed spec fn min(x: int, y: int) -> int {
        if x <= y {
            x
        } else {
            y
        }
    }

    pub proof fn lemma_min(x: int, y: int)
        ensures
            min(x, y) <= x,
            min(x, y) <= y,
            min(x, y) == x || min(x, y) == y,
    {
    }
}

mod M2 {
    use builtin::*;
    use crate::M1::*;

    proof fn test() {
        lemma_min(10, 20);
        assert(min(10, 20) == 10); // succeeds
        assert(min(100, 200) == 100); // FAILS
    }
}

Like exec functions, proof functions may have requires and ensures clauses. Unlike exec functions, proof functions are ghost and are not compiled to executable code. In the example above, the lemma_min(10, 20) function is used to help the function test in module M2 prove an assertion about min(10, 20), even when M2 cannot see the internal definition of min because min is closed. On the other hand, the assertion about min(100, 200) still fails unless test also calls lemma_min(100, 200).

proof blocks

Ultimately, the purpose of spec functions and proof functions is to help prove properties about executable code in exec functions. In fact, exec functions can contain pieces of proof code in proof blocks, written with proof { ... }. Just like a proof function contains proof code, a proof block in an exec function contains proof code and can use all of the ghost code features that proof functions can use, such as the int and nat types.

Consider an earlier example that introduced variables inside an assertion:

fn test_consts_infer() {
    let u: u8 = 1;
    assert({
        let i: int = 2;
        let n: nat = 3;
        0 <= u < i < n < 4
    });
}

We can write this in a more natural style using a proof block:

fn test_consts_infer() {
    let u: u8 = 1;
    proof {
        let i: int = 2;
        let n: nat = 3;
        assert(0 <= u < i < n < 4);
    }
}

Here, the proof code inside the proof block can create local variables of type int and nat, which can then be used in a subsequent assertion. The entire proof block is ghost code, so all of it, including its local variables, will be erased before compilation to executable code.

Proof blocks can call proof functions. In fact, any calls from an exec function to a proof function must appear inside proof code such as a proof block, rather than being called directly from the exec function’s exec code. This helps clarify which code is executable and which code is ghost, both for the compiler and for programmers reading the code. In the exec function test shown below, a proof block is used to call lemma_min, allowing subsequent assertions about min to succeed.

mod M1 {
    use builtin::*;

    pub closed spec fn min(x: int, y: int) -> int {
        if x <= y {
            x
        } else {
            y
        }
    }

    pub proof fn lemma_min(x: int, y: int)
        ensures
            min(x, y) <= x,
            min(x, y) <= y,
            min(x, y) == x || min(x, y) == y,
    {
    }
}

mod M2 {
    use builtin::*;
    use crate::M1::*;

    fn test() {
        proof {
            lemma_min(10, 20);
            lemma_min(100, 200);
        }
        assert(min(10, 20) == 10);  // succeeds
        assert(min(100, 200) == 100);  // succeeds
    }
}

assert-by

Notice that in the previous example, the information that test gains about min is not confined to the proof block, but instead propagates past the end of the proof block to help prove the subsequent assertions. This is often useful, particularly when the proof block helps prove preconditions to subsequent calls to exec functions, which must appear outside the proof block.

However, sometimes we only need to prove information for a specific purpose, and it clarifies the structure of the code if we limit the scope of the information gained. For this reason, Verus supports assert(...) by { ... } expressions, which allows proof code inside the by { ... } block whose sole purpose is to prove the asserted expression in the assert(...). Any additional information gained in the proof code is limited to the scope of the block and does not propagate outside the assert(...) by { ... } expression.

In the example below, the proof code in the block calls both lemma_min(10, 20) and lemma_min(100, 200). The first call is used to prove min(10, 20) == 10 in the assert(...) by { ... } expression. Once this is proven, the subsequent assertion assert(min(10, 20) == 10); succeeds. However, the assertion assert(min(100, 200) == 100); fails, because the information gained by the lemma_min(100, 200) call does not propagate outside the block that contains the call.

mod M1 {
    use builtin::*;

    pub closed spec fn min(x: int, y: int) -> int {
        if x <= y {
            x
        } else {
            y
        }
    }

    pub proof fn lemma_min(x: int, y: int)
        ensures
            min(x, y) <= x,
            min(x, y) <= y,
            min(x, y) == x || min(x, y) == y,
    {
    }
}

mod M2 {
    use builtin::*;
    use crate::M1::*;

    fn test() {
        assert(min(10, 20) == 10) by {
            lemma_min(10, 20);
            lemma_min(100, 200);
        }
        assert(min(10, 20) == 10); // succeeds
        assert(min(100, 200) == 100); // FAILS
    }
}

spec functions vs. proof functions

Now that we’ve seen both spec functions and proof functions, let’s take a longer look at the differences between them. We can summarize the differences in the following table (including exec functions in the table for reference):

spec functionproof functionexec function
compiled or ghostghostghostcompiled
code stylepurely functionalmutation allowedmutation allowed
can call spec functionsyesyesyes
can call proof functionsnoyesyes
can call exec functionsnonoyes
body visibilitymay be visiblenever visiblenever visible
bodybody optionalbody mandatorybody mandatory
determinismdeterministicnondeterministicnondeterministic
preconditions/postconditionsrecommendsrequires/ensuresrequires/ensures

As described in the spec functions section, spec functions make their bodies visible to other functions in their module and may optionally make their bodies visible to other modules as well. spec functions can also omit their bodies entirely:

spec fn f(i: int) -> int;

Such an uninterpreted function can be useful in libraries that define an abstract, uninterpreted function along with trusted axioms about the function.

Determinism

spec functions are deterministic: given the same arguments, they always return the same result. Code can take advantage of this determinism even when a function’s body is not visible. For example, the assertion x1 == x2 succeeds in the code below, because both x1 and x2 equal s(10), and s(10) always produces the same result, because s is a spec function:

mod M1 {
    use builtin::*;

    pub closed spec fn s(i: int) -> int {
        i + 1
    }

    pub proof fn p(i: int) -> int {
        i + 1
    }
}

mod M2 {
    use builtin::*;
    use crate::M1::*;

    proof fn test_determinism() {
        let s1 = s(10);
        let s2 = s(10);
        assert(s1 == s2); // succeeds

        let p1 = p(10);
        let p2 = p(10);
        assert(p1 == p2); // FAILS
    }
}

By contrast, the proof function p is, in principle, allowed to return different results each time it is called, so the assertion p1 == p2 fails. (Nondeterminism is common for exec functions that perform input-output operations or work with random numbers. In practice, it would be unusual for a proof function to behave nondeterministically, but it is allowed.)

recommends

exec functions and proof functions can have requires and ensures clauses. By contrast, spec functions cannot have requires and ensures clauses. This is similar to the way Boogie works, but differs from other systems like Dafny and F*. The reason for disallowing requires and ensures is to keep Verus’s specification language close to the SMT solver’s mathematical language in order to use the SMT solver as efficiently as possible (see the Verus Overview).

Nevertheless, it’s sometimes useful to have some sort of preconditions on spec functions to help catch mistakes in specifications early or to catch accidental misuses of spec functions. Therefore, spec functions may contain recommends clauses that are similar to requires clauses, but represent just lightweight recommendations rather than hard requirements. For example, for the following function, callers are under no obligation to obey the i > 0 recommendation:

spec fn f(i: nat) -> nat
    recommends
        i > 0,
{
    (i - 1) as nat
}

proof fn test1() {
    assert(f(0) == f(0));  // succeeds
}

It’s perfectly legal for test1 to call f(0), and no error or warning will be generated for g (in fact, Verus will not check the recommendation at all). However, if there’s a verification error in a function, Verus will automatically rerun the verification with recommendation checking turned on, in hopes that any recommendation failures will help diagnose the verification failure. For example, in the following:

proof fn test2() {
    assert(f(0) <= f(1)); // FAILS
}

Verus print the failed assertion as an error and then prints the failed recommendation as a note:

error: assertion failed
    |
    |     assert(f(0) <= f(1)); // FAILS
    |            ^^^^^^^^^^^^ assertion failed

note: recommendation not met
    |
    |     recommends i > 0
    |                ----- recommendation not met
...
    |     assert(f(0) <= f(1)); // FAILS
    |            ^^^^^^^^^^^^

If the note isn’t helpful, programmers are free to ignore it.

By default, Verus does not perform recommends checking on calls from spec functions:

spec fn caller1() -> nat {
    f(0)  // no note, warning, or error generated

}

However, you can write spec(checked) to request recommends checking, which will cause Verus to generate warnings for recommends violations:

spec(checked) fn caller2() -> nat {
    f(0)  // generates a warning because of "(checked)"

}

This is particularly useful for specifications that are part of the “trusted computing base” that describes the interface to external, unverified components.

Ghost code vs. exec code

The purpose of exec code is to manipulate physically real values — values that exist in physical electronic circuits when a program runs. The purpose of ghost code, on the other hand, is merely to talk about the values that exec code manipulates. In a sense, this gives ghost code supernatural abilities: ghost code can talk about things that could not be physically implemented at run-time. We’ve already seen one example of this with the types int and nat, which can only be used in ghost code. As another example, ghost code can talk about the result of division by zero:

fn divide_by_zero() {
    let x: u8 = 1;
    assert(x / 0 == x / 0); // succeeds in ghost code
    let y = x / 0; // FAILS in exec code
}

This simply reflects the SMT solver’s willingness to reason about the result of division by zero as an unspecified integer value. By contrast, Verus reports a verification failure if exec code attempts to divide by zero:

error: possible division by zero
    |
    |     let y = x / 0; // FAILS
    |             ^^^^^

Two particular abilities of ghost code1 are worth keeping in mind:

  • Ghost code can copy values of any type, even if the type doesn’t implement the Rust Copy trait.
  • Ghost code can create a value of any type2, even if the type has no public constructors (e.g. even if the type is struct whose fields are all private to another module).

For example, the following spec functions create and duplicate values of type S, defined in another module with private fields and without the Copy trait:

mod MA {
    // does not implement Copy
    // does not allow construction by other modules
    pub struct S {
        private_field: u8,
    }

}

mod MB {
    use builtin::*;
    use crate::MA::*;

    // construct a ghost S
    spec fn make_S() -> S;

    // duplicate an S
    spec fn duplicate_S(s: S) -> (S, S) {
        (s, s)
    }
}

These operations are not allowed in exec code. Furthermore, values from ghost code are not allowed to leak into exec code — what happens in ghost code stays in ghost code. Any attempt to use a value from ghost code in exec code will result in a compile-time error:

fn test(s: S) {
    let pair = duplicate_S(s); // FAILS
}
error: cannot call function with mode spec
    |
    |         let pair = duplicate_S(s); // FAILS
    |                    ^^^^^^^^^^^^^^

As an example of ghost code that uses these abilities, a call to the Verus Seq::index(...) function can duplicate a value from the sequence, if the index i is within bounds, and create a value out of thin air if i is out of bounds:

impl<A> Seq<A> {
...
    /// Gets the value at the given index `i`.
    ///
    /// If `i` is not in the range `[0, self.len())`, then the resulting value
    /// is meaningless and arbitrary.

    pub spec fn index(self, i: int) -> A
        recommends 0 <= i < self.len();
...
}

1

Variables in proof code can opt out of these special abilities using the tracked annotation, but this is an advanced feature that can be ignored for now.

2

This is true even if the type has no values in exec code, like the Rust ! “never” type (see the “bottom” value in this technical discussion).

const declarations

const declarations can either be marked spec or left without a mode:

spec const SPEC_ONE: int = 1;

spec fn spec_add_one(x: int) -> int {
    x + SPEC_ONE
}

const ONE: u8 = 1;

fn add_one(x: u8) -> (ret: u8)
    requires
        x < 0xff,
    ensures
        ret == x + ONE,  // use "ONE" in spec code
{
    x + ONE  // use "ONE" in exec code
}

A spec const is like spec function with no arguments. It is always ghost and cannot be used as an exec value.

By contrast, a const without a mode is dual-use: it is usable as both an exec value and a spec value. Therefore, the const definition is restricted to obey the rules for both exec code and spec code. For example, as with exec code, its type must be compilable (e.g. u8, not int), and, as with spec code, it cannot call any exec or proof functions.

Recursion and loops

Suppose we want to compute the nth triangular number:

triangle(n) = 0 + 1 + 2 + ... + (n - 1) + n

We can express this as a simple recursive funciton:

spec fn triangle(n: nat) -> nat
    decreases n,
{
    if n == 0 {
        0
    } else {
        n + triangle((n - 1) as nat)
    }
}

This chapter discusses how to define and use recursive functions, including writing decreases clauses and using fuel. It then explores a series of verified implementations of triangle, starting with a basic recursive implementation and ending with a while loop.

Recursive functions, decreases, fuel

Recursive functions are functions that call themselves. In order to ensure soundness, a recursive spec function must terminate on all inputs — infinite recursive calls aren’t allowed. To see why termination is important, consider the following nonterminating function definition:

spec fn bogus(i: int) -> int {
    bogus(i) + 1 // FAILS, error due to nontermination
}

Verus rejects this definition because the recursive call loops infinitely, never terminating. If Verus accepted the definion, then you could very easily prove false, because, for example, the definition insists that bogus(3) == bogus(3) + 1, which implies that 0 == 1, which is false:

proof fn exploit_bogus()
    ensures
        false,
{
    assert(bogus(3) == bogus(3) + 1);
}

To help prove termination, Verus requires that each recursive spec function definition contain a decreases clause:

spec fn triangle(n: nat) -> nat
    decreases n,
{
    if n == 0 {
        0
    } else {
        n + triangle((n - 1) as nat)
    }
}

Each recursive call must decrease the expression in the decreases clause by at least 1. Furthermore, the call cannot cause the expression to decrease below 0. With these restrictions, the expression in the decreases clause serves as an upper bound on the depth of calls that triangle can make to itself, ensuring termination.

Fuel and reasoning about recursive functions

Given the definition of triangle above, we can make some assertions about it:

fn test_triangle_fail() {
    assert(triangle(0) == 0); // succeeds
    assert(triangle(10) == 55); // FAILS
}

The first assertion, about triangle(0), succeeds. But somewhat surprisingly, the assertion assert(triangle(10) == 55) fails, despite the fact that triangle(10) really is equal to 55. We’ve just encountered a limitation of automated reasoning: SMT solvers cannot automatically prove all true facts about all recursive functions.

For nonrecursive functions, an SMT solver can reason about the functions simply by inlining them. For example, if we have a call min(a + 1, 5) to the min function:

spec fn min(x: int, y: int) -> int {
    if x <= y {
        x
    } else {
        y
    }
}

the SMT solver can replace min(a + 1, 5) with:

    if a + 1 <= 5 {
        a + 1
    } else {
        5
    }

which eliminates the call. However, this strategy doesn’t completely work with recursive functions, because inlining the function produces another expression with a call to the same function:

triangle(x) = if x == 0 { 0 } else { x + triangle(x - 1) }

Naively, the solver could keep inlining again and again, producing more and more expressions, and this strategy would never terminate:

triangle(x) = if x == 0 { 0 } else { x + triangle(x - 1) }
triangle(x) = if x == 0 { 0 } else { x + (if x - 1 == 0 { 0 } else { x - 1 + triangle(x - 2) }) }
triangle(x) = if x == 0 { 0 } else { x + (if x - 1 == 0 { 0 } else { x - 1 + (if x - 2 == 0 { 0 } else { x - 2 + triangle(x - 3) }) }) }

To avoid this infinite inlining, Verus limits the number of recursive calls that any given call can spawn in the SMT solver. This limit is called the fuel; each nested recursive inlining consumes one unit of fuel. By default, the fuel is 1, which is just enough for assert(triangle(0) == 0) to succeed but not enough for assert(triangle(10) == 55) to succeed. To increase the fuel to a larger amount, we can use the reveal_with_fuel directive:

fn test_triangle_reveal() {
    proof {
        reveal_with_fuel(triangle, 11);
    }
    assert(triangle(10) == 55);
}

Here, 11 units of fuel is enough to inline the 11 calls triangle(0), …, triangle(10). Note that even if we only wanted to supply 1 unit of fuel, we could still prove assert(triangle(10) == 55) through a long series of assertions:

fn test_triangle_step_by_step() {
    assert(triangle(0) == 0);
    assert(triangle(1) == 1);
    assert(triangle(2) == 3);
    assert(triangle(3) == 6);
    assert(triangle(4) == 10);
    assert(triangle(5) == 15);
    assert(triangle(6) == 21);
    assert(triangle(7) == 28);
    assert(triangle(8) == 36);
    assert(triangle(9) == 45);
    assert(triangle(10) == 55);  // succeeds
}

This works because 1 unit of fuel is enough to prove assert(triangle(0) == 0), and then once we know that triangle(0) == 0, we only need to inline triangle(1) once to get:

triangle(1) = if 1 == 0 { 0 } else { 1 + triangle(0) }

Now the SMT solver can use the previously computed triangle(0) to simplify this to:

triangle(1) = if 1 == 0 { 0 } else { 1 + 0 }

and then produce triangle(1) == 1. Likewise, the SMT solver can then use 1 unit of fuel to rewrite triangle(2) in terms of triangle(1), proving triangle(2) == 3, and so on. However, it’s probably best to avoid long series of assertions if you can, and instead write a proof that makes it clear why the SMT proof fails by default (not enough fuel) and fixes exactly that problem:

fn test_triangle_assert_by() {
    assert(triangle(10) == 55) by {
        reveal_with_fuel(triangle, 11);
    }
}

Recursive exec and proof functions, proofs by induction

The previous section introduced a specification for triangle numbers. Given that, let’s try a series of executable implementations of triangle numbers, starting with a simple recursive implementation:

fn rec_triangle(n: u32) -> (sum: u32)
    ensures
        sum == triangle(n as nat),
{
    if n == 0 {
        0
    } else {
        n + rec_triangle(n - 1) // FAILS: possible overflow
    }
}

We immediately run into one small practical difficulty: the implementation needs to use a finite-width integer to hold the result, and this integer may overflow:

error: possible arithmetic underflow/overflow
   |
   |         n + rec_triangle(n - 1) // FAILS: possible overflow
   |         ^^^^^^^^^^^^^^^^^^^^^^^

Indeed, we can’t expect the implementation to work if the result won’t fit in the finite-width integer type, so it makes sense to add a precondition saying that the result must fit, which for a u32 result means triangle(n) < 0x1_0000_0000:

fn rec_triangle(n: u32) -> (sum: u32)
    requires
        triangle(n as nat) < 0x1_0000_0000,
    ensures
        sum == triangle(n as nat),
{
    if n == 0 {
        0
    } else {
        n + rec_triangle(n - 1)
    }
}

This time, verification succeeds. It’s worth pausing for a few minutes, though, to understand why the verification succeeds. For example, an execution of rec_triangle(10) performs 10 separate additions, each of which could potentially overflow. How does Verus know that none of these 10 additions will overflow, given just the initial precondition triangle(10) < 0x1_0000_0000?

The answer is that each instance of triangle(n) for n != 0 makes a recursive call to triangle(n - 1), and this recursive call must satisfy the precondition triangle(n - 1) < 0x1_0000_0000. Let’s look at how this is proved. If we know triangle(n) < 0x1_0000_0000 from rec_triangle’s precondition and we use 1 unit of fuel to inline the definition of triangle once, we get:

triangle(n) < 0x1_0000_0000
triangle(n) = if n == 0 { 0 } else { n + triangle(n - 1) }

In the case where n != 0, this simplifies to:

triangle(n) < 0x1_0000_0000
triangle(n) = n + triangle(n - 1)

From this, we conclude n + triangle(n - 1) < 0x1_0000_0000, which means that triangle(n - 1) < 0x1_0000_0000, since 0 <= n, since n has type u32, which is nonnegative.

Intuitively, you can imagine that as rec_triangle executes, proofs about triangle(n) < 0x1_0000_0000 gets passed down the stack to the recursive calls, proving triangle(10) < 0x1_0000_0000 in the first call, then triangle(9) < 0x1_0000_0000 in the second call, triangle(8) < 0x1_0000_0000 in the third call, and so on. (Of course, the proofs don’t actually exist at run-time — they are purely static and are erased before compilation — but this is still a reasonable way to think about it.)

Towards an imperative implementation: mutation and tail recursion

The recursive implementation presented above is easy to write and verify, but it’s not very efficient, since it requires a lot of stack space for the recursion. Let’s take a couple small steps towards a more efficient, imperative implementation based on while loops. First, to prepare for the mutable variables that we’ll use in while loops, let’s switch sum from being a return value to being a mutably updated variable:

fn mut_triangle(n: u32, sum: &mut u32)
    requires
        triangle(n as nat) < 0x1_0000_0000,
    ensures
        *sum == triangle(n as nat),
{
    if n == 0 {
        *sum = 0;
    } else {
        mut_triangle(n - 1, sum);
        *sum = *sum + n;
    }
}

From the verification’s point of view, this doesn’t change anything significant. Internally, when performing verification, Verus simply represents the final value of *sum as a return value, making the verification of mut_triangle essentially the same as the verification of rec_triangle.

Next, let’s try to eliminate the excessive stack usage by making the function tail recursive. We do this by introducing and index variable idx that counts up from 0 to n, just as a while loop would do:

fn tail_triangle(n: u32, idx: u32, sum: &mut u32)
    requires
        idx <= n,
        *old(sum) == triangle(idx as nat),
        triangle(n as nat) < 0x1_0000_0000,
    ensures
        *sum == triangle(n as nat),
{
    if idx < n {
        let idx = idx + 1;
        *sum = *sum + idx;
        tail_triangle(n, idx, sum);
    }
}

In the preconditions and postconditions, the expression *old(sum) refers to the initial value of *sum, at the entry to the function, while *sum refers to the final value, at the exit from the function. The precondition *old(sum) == triangle(idx as nat) specifies that as tail_triangle executes more and more recursive calls, sum accumulates the sum 0 + 1 + ... + idx. Each recursive call increases idx by 1 until idx reaches n, at which point sum equals 0 + 1 + ... + n and the function simply returns sum unmodified.

When we try to verify tail_triangle, though, Verus reports an error about possible overflow:

error: possible arithmetic underflow/overflow
    |
    |         *sum = *sum + idx;
    |                ^^^^^^^^^^

This may seem perplexing at first: why doesn’t the precondition triangle(n as nat) < 0x1_0000_0000 automatically take care of the overflow, as it did for rec_triangle and mut_triangle?

The problem is that we’ve reversed the order of the addition and the recursive call. rec_triangle and mut_triangle made the recursive call first, and then performed the addition. This allowed them to prove all the necessary facts about overflow first in the series of recursive calls (e.g. proving triangle(10) < 0x1_0000_0000, triangle(9) < 0x1_0000_0000, …, triangle(0) < 0x1_0000_0000.) before doing the arithmetic that depends on these facts. But tail_triangle tries to perform the arithmetic first, before the recursion, so it never has a chance to develop these facts from the original triangle(n) < 0x1_0000_0000 assumption.

Proofs by induction

In the example of computing triangle(10), we need to know triangle(0) < 0x1_0000_0000, then triangle(1) < 0x1_0000_0000, and so on, but we only know triangle(10) < 0x1_0000_0000 to start with. If we somehow knew that triangle(0) <= triangle(10), triangle(1) <= triangle(10), and so on, then we could derive what we want from triangle(10) < 0x1_0000_0000. What we need is a lemma that proves the if i <= j, then triangle(i) <= triangle(j). In other words, we need to prove that triangle is monotonic.

We can use a proof function to implement this lemma:

proof fn triangle_is_monotonic(i: nat, j: nat)
    ensures
        i <= j ==> triangle(i) <= triangle(j),
    decreases j,
{
    // We prove the statement `i <= j ==> triangle(i) <= triangle(j)`
    // by induction on `j`.

    if j == 0 {
        // The base case (`j == 0`) is trivial since it's only
        // necessary to reason about when `i` and `j` are both 0.
        // So no proof lines are needed for this case.
    }
    else {
        // In the induction step, we can assume the statement is true
        // for `j - 1`. In Verus, we can get that fact into scope with
        // a recursive call substituting `j - 1` for `j`.

        triangle_is_monotonic(i, (j - 1) as nat);

        // Once we know it's true for `j - 1`, the rest of the proof
        // is trivial.
    }
}

The proof is by induction on j, where the base case of the induction is i == j and the induction step relates j - 1 to j. In Verus, the induction step is implemented as a recursive call from the proof to itself (in this example, this recursive call is line triangle_is_monotonic(i, (j - 1) as nat)).

As with recursive spec functions, recursive proof functions must terminate and need a decreases clause. Otherwise, it would be easy to prove false, as in the following non-terminating “proof”:

proof fn circular_reasoning()
    ensures
        false,
{
    circular_reasoning(); // FAILS, does not terminate
}

We can use the triangle_is_monotonic lemma to complete the verification of tail_triangle:

fn tail_triangle(n: u32, idx: u32, sum: &mut u32)
    requires
        idx <= n,
        *old(sum) == triangle(idx as nat),
        triangle(n as nat) < 0x1_0000_0000,
    ensures
        *sum == triangle(n as nat),
{
    if idx < n {
        let idx = idx + 1;
        assert(*sum + idx < 0x1_0000_0000) by {
            triangle_is_monotonic(idx as nat, n as nat);
        }
        *sum = *sum + idx;
        tail_triangle(n, idx, sum);
    }
}

Intuitively, we can think of the call from tail_triangle to triangle_is_monotonic as performing a similar recursive proof that rec_triangle and mut_triangle performed as they proved their triangle(n) < 0x1_0000_0000 preconditions in their recursive calls. In going from rec_triangle and mut_triangle to tail_triangle, we’ve just shifted this recursive reasoning from the executable code into a separate recursive lemma.

Loops and invariants

The previous section developed a tail-recursive implementation of triangle:

fn tail_triangle(n: u32, idx: u32, sum: &mut u32)
    requires
        idx <= n,
        *old(sum) == triangle(idx as nat),
        triangle(n as nat) < 0x1_0000_0000,
    ensures
        *sum == triangle(n as nat),
{
    if idx < n {
        let idx = idx + 1;
        assert(*sum + idx < 0x1_0000_0000) by {
            triangle_is_monotonic(idx as nat, n as nat);
        }
        *sum = *sum + idx;
        tail_triangle(n, idx, sum);
    }
}

We can rewrite this as a while loop as follows:

fn loop_triangle(n: u32) -> (sum: u32)
    requires
        triangle(n as nat) < 0x1_0000_0000,
    ensures
        sum == triangle(n as nat),
{
    let mut sum: u32 = 0;
    let mut idx: u32 = 0;
    while idx < n
        invariant
            idx <= n,
            sum == triangle(idx as nat),
            triangle(n as nat) < 0x1_0000_0000,
    {
        idx = idx + 1;
        assert(sum + idx < 0x1_0000_0000) by {
            triangle_is_monotonic(idx as nat, n as nat);
        }
        sum = sum + idx;
    }
    sum
}

The loop is quite similar to the tail-recursive implementation. (In fact, internally, Verus verifies the loop as if it were its own function, separate from the enclosing loop_triangle function.) Where the tail-recursive function had preconditions, the loop has loop invariants that describe what must be true before and after each iteration of the loop. For example, if n = 10, then the loop invariant must be true 11 times: before each of the 10 iterations, and after the final iteration.

Notice that the invariant idx <= n allows for the possibility that idx == n, since this will be the case after the final iteration. If we tried to write the invariant as idx < n, then Verus would fail to prove that the invariant is maintained after the final iteration.

After the loop exits, Verus knows that idx <= n (because of the loop invariant) and it knows that the loop condition idx < n must have been false (otherwise, the loop would have continued). Putting these together allows Verus to prove that idx == n after exiting the loop. Since we also have the invariant sum == triangle(idx as nat), Verus can then substitute n for idx to conclude sum == triangle(n as nat), which proves the postcondition of loop_triangle.

Just as verifying functions requires some programmer effort to write appropriate preconditions and postconditions, verifying loops requires programmer effort to write loop invariants. The loop invariants have to be neither too weak (invariant true is usually too weak) nor too strong (invariant false is too strong), so that:

  • the invariants hold upon the initial entry to the loop (e.g. idx <= n holds for the initial value idx = 0, since 0 <= n)
  • the invariant still holds at the end of the loop body, so that the invariant is maintained across loop iterations
  • the invariant is strong enough to prove the properties we want to know after the loop exits (e.g. to prove loop_triangle’s postcondition)

As mentioned above, Verus verifies the loop separately from the function that contains the loop (e.g. separately from loop_triangle). This means that the loop does not automatically inherit preconditions like triangle(n as nat) < 0x1_0000_0000 from the surrounding function — if the loop relies on these preconditions, they must be listed explicitly in the loop invariants. (The reason for this is to improve the efficiency of the SMT solving for large functions with large while loops; verification runs faster if Verus breaks the surrounding function and the loops into separate pieces and verifies them modularly.)

Verus does allow you to opt-out of this behavior, meaning that your loops will inherit information from the surrounding context. This will simplify your loop invariants, but verification time may increase for medium-to-large functions. To opt-out for a single function or while loop, you can add the attribute #[verifier::loop_isolation(false)]. You can also opt-out at the module or crate level, by adding the #![verifier::loop_isolation(false)] attribute to the module or the root of the crate. You can then override the global setting locally by adding #[verifier::loop_isolation(true)] on individual functions or loops.

Loops with break

Loops can exit early using return or break. Suppose, for example, we want to remove the requirement triangle(n as nat) < 0x1_0000_0000 from the loop_triangle function, and instead check for overflow at run-time. The following version of the function uses return to return the special value 0xffff_ffff in case overflow is detected at run-time:

fn loop_triangle_return(n: u32) -> (sum: u32)
    ensures
        sum == triangle(n as nat) || (sum == 0xffff_ffff && triangle(n as nat) >= 0x1_0000_0000),
{
    let mut sum: u32 = 0;
    let mut idx: u32 = 0;
    while idx < n
        invariant
            idx <= n,
            sum == triangle(idx as nat),
    {
        idx = idx + 1;
        if sum as u64 + idx as u64 >= 0x1_0000_0000 {
            proof {
                triangle_is_monotonic(idx as nat, n as nat);
            }
            return 0xffff_ffff;
        }
        sum = sum + idx;
    }
    sum
}

Another way to exit early from a loop is with a break inside the loop body. However, break complicates the specification of a loop slightly. For simple while loops without a break, Verus knows that the loop condition (e.g. idx < n) must be false after exiting the loop. If there is a break, though, the loop condition is not necessarily false after the loop, because the break might cause the loop to exit even when the loop condition is true. To deal with this, while loops with a break, as well as Rust loop expressions (loops with no condition), must explicitly specify what is true after the loop exit using ensures clauses, as shown in the following code. Furthermore, invariants that don’t hold after a break must be marked as invariant_except_break rather than invariant:

fn loop_triangle_break(n: u32) -> (sum: u32)
    ensures
        sum == triangle(n as nat) || (sum == 0xffff_ffff && triangle(n as nat) >= 0x1_0000_0000),
{
    let mut sum: u32 = 0;
    let mut idx: u32 = 0;
    while idx < n
        invariant_except_break
            idx <= n,
            sum == triangle(idx as nat),
        ensures
            sum == triangle(n as nat) || (sum == 0xffff_ffff && triangle(n as nat) >= 0x1_0000_0000),
    {
        idx = idx + 1;
        if sum as u64 + idx as u64 >= 0x1_0000_0000 {
            proof {
                triangle_is_monotonic(idx as nat, n as nat);
            }
            sum = 0xffff_ffff;
            break;
        }
        sum = sum + idx;
    }
    sum
}

Lexicographic decreases clauses

For some recursive functions, it’s difficult to specify a single value that decreases in each recursive call. For example, the Ackermann function has two parameters m and n, and neither m nor n decrease in all 3 of the recursive calls:

spec fn ackermann(m: nat, n: nat) -> nat
    decreases m, n,
{
    if m == 0 {
        n + 1
    } else if n == 0 {
        ackermann((m - 1) as nat, 1)
    } else {
        ackermann((m - 1) as nat, ackermann(m, (n - 1) as nat))
    }
}

proof fn test_ackermann() {
    reveal_with_fuel(ackermann, 12);
    assert(ackermann(3, 2) == 29);
}

For this situation, Verus allows the decreases clause to contain multiple expressions, and it treats these expressions as lexicographically ordered. For example, decreases m, n means that one of the following must be true:

  • m stays the same, and n decreases, which happens in the call ackermann(m, (n - 1) as nat)
  • m decreases and n may increase or decrease arbitrarily, which happens in the two calls of the form ackermann((m - 1) as nat, ...)

Mutual recursion

Functions may be mutually recursive, as in the following example where is_even calls is_odd recursively and is_odd calls is_even recursively:

spec fn abs(i: int) -> int {
    if i < 0 {
        -i
    } else {
        i
    }
}

spec fn is_even(i: int) -> bool
    decreases abs(i),
{
    if i == 0 {
        true
    } else if i > 0 {
        is_odd(i - 1)
    } else {
        is_odd(i + 1)
    }
}

spec fn is_odd(i: int) -> bool
    decreases abs(i),
{
    if i == 0 {
        false
    } else if i > 0 {
        is_even(i - 1)
    } else {
        is_even(i + 1)
    }
}

proof fn even_odd_mod2(i: int)
    ensures
        is_even(i) <==> i % 2 == 0,
        is_odd(i) <==> i % 2 == 1,
    decreases abs(i),
{
    if i < 0 {
        even_odd_mod2(i + 1);
    }
    if i > 0 {
        even_odd_mod2(i - 1);
    }
}

fn test_even() {
    proof {
        reveal_with_fuel(is_even, 11);
    }
    assert(is_even(10));
}

fn test_odd() {
    proof {
        reveal_with_fuel(is_odd, 11);
    }
    assert(!is_odd(10));
}

The recursion here works for both positive and negative i; in both cases, the recursion decreases abs(i), the absolute value of i.

An alternate way to write this mutual recursion is:

spec fn is_even(i: int) -> bool
    decreases abs(i), 0int,
{
    if i == 0 {
        true
    } else if i > 0 {
        is_odd(i - 1)
    } else {
        is_odd(i + 1)
    }
}

spec fn is_odd(i: int) -> bool
    decreases abs(i), 1int,
{
    !is_even(i)
}

proof fn even_odd_mod2(i: int)
    ensures
        is_even(i) <==> i % 2 == 0,
        is_odd(i) <==> i % 2 == 1,
    decreases abs(i),
{
    reveal_with_fuel(is_odd, 2);
    if i < 0 {
        even_odd_mod2(i + 1);
    }
    if i > 0 {
        even_odd_mod2(i - 1);
    }
}

fn test_even() {
    proof {
        reveal_with_fuel(is_even, 21);
    }
    assert(is_even(10));
}

fn test_odd() {
    proof {
        reveal_with_fuel(is_odd, 22);
    }
    assert(!is_odd(10));
}

In this alternate version, the recursive call !is_even(i) doesn’t decrease abs(i), so we can’t just use abs(i) as the decreases clause by itself. However, we can employ a trick with lexicographic ordering. If we write decreases abs(i), 1, then the call to !is_even(i) keeps the first expression abs(i) the same, but decreases the second expression from 1 to 0, which satisfies the lexicographic requirements for decreasing. The call is_odd(i - 1) also obeys lexicographic ordering, since it decreases the first expression abs(i), which allows the second expression to increase from 0 to 1.

Datatypes: Structs and Enums

Datatypes, in both executable code and specifications, are defined via Rust’s struct and enum.

Struct

In Verus, just as in Rust, you can use struct to define a datatype that collects a set of fields together:

struct Point {
    x: int,
    y: int,
}

Spec and exec code can refer to struct fields:

impl Point {
    spec fn len2(&self) -> int {
        self.x * self.x + self.y * self.y
    }
}

fn rotate_90(p: Point) -> (o: Point)
    ensures o.len2() == p.len2()
{
    let o = Point { x: -p.y, y: p.x };
    assert((-p.y) * (-p.y) == p.y * p.y) by(nonlinear_arith);
    o
}

Enum

In Verus, just as in Rust, you can use enum to define a datatype that is any one of the defined variants:

enum Beverage {
    Coffee { creamers: nat, sugar: bool },
    Soda { flavor: Syrup },
    Water { ice: bool },
}

An enum is often used just for its tags, without member fields:

enum Syrup {
    Cola,
    RootBeer,
    Orange,
    LemonLime,
}

Identifying a variant with the is operator

In spec contexts, the is operator lets you query which variant of an enum a variable contains.

fn make_float(bev: Beverage) -> Dessert
    requires bev is Soda
{
    Dessert::new(/*...*/)
}

Accessing fields with the arrow operator

If all the fields have distinct names, as in the Beverage example, you can refer to fields with the arrow -> operator:

proof fn sufficiently_creamy(bev: Beverage) -> bool
    requires bev is Coffee
{
   bev->creamers >= 2
}

If an enum field reuses a name, you can qualify the field access:

enum Life {
    Mammal { legs: int, has_pocket: bool },
    Arthropod { legs: int, wings: int },
    Plant { leaves: int },
}

spec fn is_insect(l: Life) -> bool
{
    l is Arthropod && l->Arthropod_legs == 6
}

match works as in Rust.

enum Shape {
    Circle(int),
    Rect(int, int),
}

spec fn area_2(s: Shape) -> int {
    match s {
        Shape::Circle(radius) => { radius * radius * 3 },
        Shape::Rect(width, height) => { width * height }
    }
}

For variants like Shape declared with round parentheses (), you can use Verus’ `->’ tuple-like syntax to access a single field without a match destruction:

spec fn rect_height(s: Shape) -> int
    recommends s is Rect
{
    s->1
}

matches with &&, ==>, and &&&

match is natural for examining every variant of an enum. If you’d like to bind the fields while only considering one or two of the variants, you can use Verus’ matches syntax:

use Life::*;
spec fn cuddly(l: Life) -> bool {
    ||| l matches Mammal { legs, .. } && legs == 4
    ||| l matches Arthropod { legs, wings } && legs == 8 && wings == 0
}

Because the matches syntax binds names in patterns, it has no trouble with field names reused across variants, so it may be preferable to the (qualified) arrow syntax.

Notice that l matches Mammal{legs} && legs == 4 is a boolean expression, with the special property that legs is bound in the remainder of the expression after &&. That helpful binding also works with ==> and &&&:

spec fn is_kangaroo(l: Life) -> bool {
    &&& l matches Life::Mammal { legs, has_pocket }
    &&& legs == 2
    &&& has_pocket
}

spec fn walks_upright(l: Life) -> bool {
    l matches Life::Mammal { legs, .. } ==> legs == 2
}

Libraries

Specification libraries: Seq, Set, Map

The Verus libraries contain types Seq<T>, Set<T>, and Map<Key, Value> for representing sequences, sets, and maps in specifications. In contrast to executable Rust collection datatypes in std::collections, the Seq, Set and Map types represent collections of arbitrary size. For example, while the len() method of std::collections::HashSet returns a length of type usize, which is bounded, the len() methods of Seq and Set return lengths of type nat, which is unbounded. Furthermore, Set and Map can represent infinite sets and maps. (Sequences, on the other hand, are always finite.) This allows specifications to talk about collections that are larger than could be contained in the physical memory of a computer.

Constructing and using Seq, Set, Map

The seq!, set!, and map! macros construct values of type Seq, Set, and Map with particular contents:

proof fn test_seq1() {
    let s: Seq<int> = seq![0, 10, 20, 30, 40];
    assert(s.len() == 5);
    assert(s[2] == 20);
    assert(s[3] == 30);
}

proof fn test_set1() {
    let s: Set<int> = set![0, 10, 20, 30, 40];
    assert(s.finite());
    assert(s.contains(20));
    assert(s.contains(30));
    assert(!s.contains(60));
}

proof fn test_map1() {
    let m: Map<int, int> = map![0 => 0, 10 => 100, 20 => 200, 30 => 300, 40 => 400];
    assert(m.dom().contains(20));
    assert(m.dom().contains(30));
    assert(!m.dom().contains(60));
    assert(m[20] == 200);
    assert(m[30] == 300);
}

The macros above can only construct finite sequences, sets, and maps. There are also functions Seq::new, Set::new, and Map::new, which can allocate both finite values and (for sets and maps) infinite values:

proof fn test_seq2() {
    let s: Seq<int> = Seq::new(5, |i: int| 10 * i);
    assert(s.len() == 5);
    assert(s[2] == 20);
    assert(s[3] == 30);
}

proof fn test_set2() {
    let s: Set<int> = Set::new(|i: int| 0 <= i <= 40 && i % 10 == 0);
    assert(s.contains(20));
    assert(s.contains(30));
    assert(!s.contains(60));

    let s_infinite: Set<int> = Set::new(|i: int| i % 10 == 0);
    assert(s_infinite.contains(20));
    assert(s_infinite.contains(30));
    assert(!s_infinite.contains(35));
}

proof fn test_map2() {
    let m: Map<int, int> = Map::new(|i: int| 0 <= i <= 40 && i % 10 == 0, |i: int| 10 * i);
    assert(m[20] == 200);
    assert(m[30] == 300);

    let m_infinite: Map<int, int> = Map::new(|i: int| i % 10 == 0, |i: int| 10 * i);
    assert(m_infinite[20] == 200);
    assert(m_infinite[30] == 300);
    assert(m_infinite[90] == 900);
}

Each Map<Key, Value> value has a domain of type Set<Key> given by .dom(). In the test_map2 example above, m’s domain is the finite set {0, 10, 20, 30, 40}, while m_infinite’s domain is the infinite set {..., -20, 10, 0, 10, 20, ...}.

For more operations, including sequence contenation (.add or +), sequence update, sequence subrange, set union (.union or +), set intersection (.intersect), etc., see:

See also the API documentation.

Proving properties of Seq, Set, Map

The SMT solver will prove some properties about Seq, Set, and Map automatically, as shown in the examples above. However, some other properties may require calling lemmas in the library or may require proofs by induction.

If two collections (Seq, Set, or Map) have the same elements, Verus considers them to be equal. This is known as equality via extensionality. However, the SMT solver will in general not automatically recognize that the two collections are equal if the collections were constructed in different ways. For example, the following 3 sequences are equal, but asserting equality fails:

proof fn test_eq_fail() {
    let s1: Seq<int> = seq![0, 10, 20, 30, 40];
    let s2: Seq<int> = seq![0, 10] + seq![20] + seq![30, 40];
    let s3: Seq<int> = Seq::new(5, |i: int| 10 * i);
    assert(s1 === s2); // FAILS, even though it's true
    assert(s1 === s3); // FAILS, even though it's true
}

To convince the SMT solver that s1, s2, and s3 are equal, we have to explicitly assert the equality via the extensional equality operator =~=, rather than just the ordinary equality operator ==. Using =~= forces the SMT solver to check that all the elements of the collections are equal, which it would not ordinarily do. Once we’ve explicitly proven equality via extensionality, we can then successfully assert ==:

proof fn test_eq() {
    let s1: Seq<int> = seq![0, 10, 20, 30, 40];
    let s2: Seq<int> = seq![0, 10] + seq![20] + seq![30, 40];
    let s3: Seq<int> = Seq::new(5, |i: int| 10 * i);
    assert(s1 =~= s2);
    assert(s1 =~= s3);
    assert(s1 === s2);  // succeeds
    assert(s1 === s3);  // succeeds
}

(See the Equality via extensionality section for more details.)

Proofs about set cardinality (Set::len) and set finiteness (Set::finite) often require inductive proofs. For example, the exact cardinality of the intersection of two sets depends on which elements the two sets have in common. If the two sets are disjoint, the intersection’s cardinality will be 0, but otherwise, the intersections’s cardinality will be some non-zero value. Let’s try to prove that the intersection’s cardinality is no larger than either of the two sets’ cardinalities. Without loss of generality, we can just prove that the intersection’s cardinality is no larger than the first set’s cardinality: s1.intersect(s2).len() <= s1.len().

The proof (which is found in set_lib.rs) is by induction on the size of the set s1. In the induction step, we need to make s1 smaller, which means we need to remove an element from it. The two methods .choose and .remove allow us to choose an arbitrary element from s1 and remove it:

let a = s1.choose();
... s1.remove(a) ...

Based on this, we expect an inductive proof to look something like the following, where the inductive step removes s1.choose():

pub proof fn lemma_len_intersect<A>(s1: Set<A>, s2: Set<A>)
    requires
        s1.finite(),
    ensures
        s1.intersect(s2).len() <= s1.len(),
    decreases
        s1.len(),
{
    if s1.is_empty() {

    } else {
        let a = s1.choose();

        lemma_len_intersect(s1.remove(a), s2);
    }
}

Unfortunately, Verus fails to verify this proof. Therefore, we’ll need to fill in the base case and induction case with some more detail. Before adding this detail to the code, let’s think about what a fully explicit proof might look like if we wrote it out by hand:

pub proof fn lemma_len_intersect<A>(s1: Set<A>, s2: Set<A>)
    requires
        s1.finite(),
    ensures
        s1.intersect(s2).len() <= s1.len(),
    decreases
        s1.len(),
{
    if s1.is_empty() {
        // s1 is the empty set.
        // Therefore, s1.intersect(s2) is also empty.
        // So both s1.len() and s1.intersect(s2).len() are 0,
        // and 0 <= 0.
    } else {
        // s1 is not empty, so it has at least one element.
        // Let a be an element from s1.
        // Let s1' be the set s1 with the element a removed (i.e. s1' == s1 - {a}).
        // Removing an element decreases the cardinality by 1, so s1'.len() == s1.len() - 1.
        // By induction, s1'.intersect(s2).len() <= s1'.len(), so:
        //   (s1 - {a}).intersect(s2).len() <= s1'.len()
        //   (s1.intersect(s2) - {a}).len() <= s1'.len()
        //   (s1.intersect(s2) - {a}).len() <= s1.len() - 1
        // case a in s1.intersect(s2):
        //   (s1.intersect(s2) - {a}).len() == s1.intersect(s2).len() - 1
        // case a not in s1.intersect(s2):
        //   (s1.intersect(s2) - {a}).len() == s1.intersect(s2).len()
        // In either case:
        //   s1.intersect(s2).len() <= (s1.intersect(s2) - {a}).len() + 1
        // Putting all the inequalities together:
        //   s1.intersect(s2).len() <= (s1.intersect(s2) - {a}).len() + 1 <= (s1.len() - 1) + 1
        // So:
        //   s1.intersect(s2).len() <= (s1.len() - 1) + 1
        //   s1.intersect(s2).len() <= s1.len()
    }
}

For such a simple property, this is a surprisingly long proof! Fortunately, the SMT solver can automatically prove most of the steps written above. What it will not automatically prove, though, is any step requiring equality via extensionality, as discussed earlier. The two crucial steps requiring equality via extensionality are:

  • “Therefore, s1.intersect(s2) is also empty.”
  • Replacing (s1 - {a}).intersect(s2) with s1.intersect(s2) - {a}

For these, we need to explicitly invoke =~=:

pub proof fn lemma_len_intersect<A>(s1: Set<A>, s2: Set<A>)
    requires
        s1.finite(),
    ensures
        s1.intersect(s2).len() <= s1.len(),
    decreases
        s1.len(),
{
    if s1.is_empty() {
        assert(s1.intersect(s2) =~= s1);
    } else {
        let a = s1.choose();
        assert(s1.intersect(s2).remove(a) =~= s1.remove(a).intersect(s2));
        lemma_len_intersect(s1.remove(a), s2);
    }
}

With this, Verus and the SMT solver successfully complete the proof. However, Verus and the SMT solver aren’t the only audience for this proof. Anyone maintaining this code might want to know why we invoked =~=, and we probably shouldn’t force them to work out the entire hand-written proof above to rediscover this. So although it’s not strictly necessary, it’s probably polite to wrap the assertions in assert...by to indicate the purpose of the =~=:

pub proof fn lemma_len_intersect<A>(s1: Set<A>, s2: Set<A>)
    requires
        s1.finite(),
    ensures
        s1.intersect(s2).len() <= s1.len(),
    decreases s1.len(),
{
    if s1.is_empty() {
        assert(s1.intersect(s2).len() == 0) by {
            assert(s1.intersect(s2) =~= s1);
        }
    } else {
        let a = s1.choose();
        lemma_len_intersect(s1.remove(a), s2);
        // by induction: s1.remove(a).intersect(s2).len() <= s1.remove(a).len()
        assert(s1.intersect(s2).remove(a).len() <= s1.remove(a).len()) by {
            assert(s1.intersect(s2).remove(a) =~= s1.remove(a).intersect(s2));
        }
        // simplifying ".remove(a).len()" yields s1.intersect(s2).len() <= s1.len())

    }
}

Executable libraries: Vec

The previous section discussed the mathematical collection types Seq, Set, and Map. This section will discuss Vec, an executable implementation of Seq. Verus supports some functionality of Rust’s std::vec::Vec type. To use Vec, include use std::vec::Vec; in your code.

You can allocate Vec using Vec::new and then push elements into it:

fn test_vec1() {
    let mut v: Vec<u32> = Vec::new();
    v.push(0);
    v.push(10);
    v.push(20);
    v.push(30);
    v.push(40);
    assert(v.len() == 5);
    assert(v[2] == 20);
    assert(v[3] == 30);
    v.set(2, 21);
    assert(v[2] == 21);
    assert(v[3] == 30);
}

The code above is able to make assertions directly about the Vec value v. You could also write more compilicated specifications and proofs about Vec values. In general, though, Verus encourages programmers to write spec functions and proof functions about mathematical types like Seq, Set, and Map instead of hard-wiring the specifications and proofs to particular concrete datatypes like Vec. This allows spec functions and proof functions to focus on the essential ideas, written in terms of mathematical types like Seq, Set, Map, int, and nat, rather than having to fiddle around with finite-width integers like usize, worry about arithmetic overflow, etc.

Of course, there needs to be a connection between the mathematical types and the concrete types, and specifications in exec functions will commonly have to move back and forth between mathematical abstractions and concrete reality. To make this easier, Verus supports the syntactic sugar @ for extracting a mathematical view from a concrete type. For example, v@ returns a Seq of all the elements in the vector v:

spec fn has_five_sorted_numbers(s: Seq<u32>) -> bool {
    s.len() == 5 && s[0] <= s[1] <= s[2] <= s[3] <= s[4]
}

fn test_vec2() {
    let mut v: Vec<u32> = Vec::new();
    v.push(0);
    v.push(10);
    v.push(20);
    v.push(30);
    v.push(40);
    v.set(2, 21);
    assert(v@ =~= seq![0, 10, 21, 30, 40]);
    assert(v@ =~= seq![0, 10] + seq![21] + seq![30, 40]);
    assert(v@[2] == 21);
    assert(v@[3] == 30);
    assert(v@.subrange(2, 4) =~= seq![21, 30]);
    assert(has_five_sorted_numbers(v@));
}

Using the Seq view of the Vec allows us to use the various features of Seq, such as concatenation and subsequences, when writing specifications about the Vec contents.

Verus support for std::vec::Vec is currently being expanded. For up-to-date documentation, visit this link. Note that these functions provide specifications for std::vec::Vec functions. Thus, for example, ex_vec_insert represents support for the Vec function insert. Code written in Verus should use insert rather than ex_vec_insert.

Documentation for std::vec::Vec functionality can be found here.

Spec Closures

Verus supports anonymous functions (known as “closures” in Rust) in ghost code. For example, the following code from earlier in this chapter uses an anonymous function |i: int| 10 * i to initialize a sequence with the values 0, 10, 20, 30, 40:

proof fn test_seq2() {
    let s: Seq<int> = Seq::new(5, |i: int| 10 * i);
    assert(s.len() == 5);
    assert(s[2] == 20);
    assert(s[3] == 30);
}

The anonymous function |i: int| 10 * i has type spec_fn(int) -> int and has mode spec. Because it has mode spec, the anonymous function is subject to the same restrictions as named spec functions. (For example, it can call other spec functions but not proof functions or exec functions.)

Note that in contrast to standard executable Rust closures, where Fn, FnOnce, and FnMut are traits, spec_fn(int) -> int is a type, not a trait. Therefore, ghost code can return a spec closure directly, using a return value of type spec_fn(t1, ..., tn) -> tret, without having to use dyn or impl, as with standard executable Rust closures. For example, the spec function adder, shown below, can return an anonymous function that adds x to y:

spec fn adder(x: int) -> spec_fn(int) -> int {
    |y: int| x + y
}

proof fn test_adder() {
    let f = adder(10);
    assert(f(20) == 30);
    assert(f(60) == 70);
}

Using assert and assume to develop proofs

In an earlier chapter, we started with an outline of a proof:

pub proof fn lemma_len_intersect<A>(s1: Set<A>, s2: Set<A>)
    requires
        s1.finite(),
    ensures
        s1.intersect(s2).len() <= s1.len(),
    decreases
        s1.len(),
{
    if s1.is_empty() {

    } else {
        let a = s1.choose();

        lemma_len_intersect(s1.remove(a), s2);
    }
}

and then filled in the crucial missing steps to complete the proof. It didn’t say, though, how you might go about discovering which crucial steps are missing. In practice, it takes some experimentation to fill in this kind of proof.

This section will walk through a typical process of developing a proof, using the proof outline above as a starting point. The process will consist of a series of queries to Verus and the SMT solver, using assert and assume to ask questions, and using the answers to narrow in on the cause of the verification failure.

If we run the proof above, Verus reports an error:

error: postcondition not satisfied
   |
   |           s1.intersect(s2).len() <= s1.len(),
   |           ---------------------------------- failed this postcondition

This raises a couple questions:

  • Why is this postcondition failing?
  • If this postcondition succeeded, would the verification of the whole function succeed?

Let’s check the second question first. We can simply assume the postcondition and see what happens:

pub proof fn lemma_len_intersect<A>(s1: Set<A>, s2: Set<A>)
    ...
{
    if s1.is_empty() {
    } else {
        let a = s1.choose();
        lemma_len_intersect::<A>(s1.remove(a), s2);
    }
    assume(s1.intersect(s2).len() <= s1.len());
}

In this case, verification succeeds:

verification results:: verified: 1 errors: 0

There are two paths through the code, one when s1.is_empty() and one when !s1.empty(). The failure could lie along either path, or both. Let’s prepare to work on each branch of the if/else separately by moving a separate copy the assume into each branch:

{
    if s1.is_empty() {
        assume(s1.intersect(s2).len() <= s1.len());
    } else {
        let a = s1.choose();
        lemma_len_intersect::<A>(s1.remove(a), s2);
        assume(s1.intersect(s2).len() <= s1.len());
    }
}
verification results:: verified: 1 errors: 0

Next, let’s change the first assume to an assert to see if it succeeds in the if branch:

{
    if s1.is_empty() {
        assert(s1.intersect(s2).len() <= s1.len());
    } else {
        let a = s1.choose();
        lemma_len_intersect::<A>(s1.remove(a), s2);
        assume(s1.intersect(s2).len() <= s1.len());
    }
}
error: assertion failed
   |
   |         assert(s1.intersect(s2).len() <= s1.len());
   |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ assertion failed

In the s1.is_empty() case, we expect s1.len() == 0 (an empty set has cardinality 0). We can double-check this with a quick assertion:

{
    if s1.is_empty() {
        assert(s1.len() == 0);
        assume(s1.intersect(s2).len() <= s1.len());
    } else {
        ...
    }
}
verification results:: verified: 1 errors: 0

So what we need is s1.intersect(s2).len() <= 0. If this were true, we’d satisfy the postcondition here:

{
    if s1.is_empty() {
        assume(s1.intersect(s2).len() <= 0);
        assert(s1.intersect(s2).len() <= s1.len());
    } else {
        ...
    }
}
verification results:: verified: 1 errors: 0

Since set cardinality is a nat, the only way it can be <= 0 is if it’s equal to 0:

{
    if s1.is_empty() {
        assume(s1.intersect(s2).len() == 0);
        assert(s1.intersect(s2).len() <= s1.len());
    } else {
        ...
    }
}
verification results:: verified: 1 errors: 0

and the only way it can be 0 is if the set is the empty set:

{
    if s1.is_empty() {
        assume(s1.intersect(s2) === Set::empty());
        assert(s1.intersect(s2).len() == 0);
        assert(s1.intersect(s2).len() <= s1.len());
    } else {
        ...
    }
}
verification results:: verified: 1 errors: 0

If we change the assume to an assert, the assertion fails:

{
    if s1.is_empty() {
        assert(s1.intersect(s2) === Set::empty());
        assert(s1.intersect(s2).len() == 0);
        assert(s1.intersect(s2).len() <= s1.len());
    } else {
        ...
    }
}
error: assertion failed
   |
   |         assert(s1.intersect(s2) === Set::empty());
   |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ assertion failed

So we’ve narrowed in on the problem: the intersection of the empty set s1 with another set should equal the empty set, but the verifier doesn’t see this automatically. And from the previous section’s discussion of equality, we can guess why: the SMT solver doesn’t always automatically prove equalities between collections, but instead requires us to assert the equality using extensionality. So we can add the extensionality assertion:

{
    if s1.is_empty() {
        assert(s1.intersect(s2) =~= Set::empty());
        assert(s1.intersect(s2) === Set::empty());
        assert(s1.intersect(s2).len() == 0);
        assert(s1.intersect(s2).len() <= s1.len());
    } else {
        ...
    }
}
verification results:: verified: 1 errors: 0

It works! We’ve now verified the s1.is_empty() case, and we can turn our attention to the !s1.is_empty() case:

{
    if s1.is_empty() {
        ...
    } else {
        let a = s1.choose();
        lemma_len_intersect::<A>(s1.remove(a), s2);
        assume(s1.intersect(s2).len() <= s1.len());
    }
}

Changing this assume to an assert fails, so we’ve got work to do in this case as well:

{
    if s1.is_empty() {
        ...
    } else {
        let a = s1.choose();
        lemma_len_intersect::<A>(s1.remove(a), s2);
        assert(s1.intersect(s2).len() <= s1.len());
    }
}
error: assertion failed
   |
   |         assert(s1.intersect(s2).len() <= s1.len());
   |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ assertion failed

Fortunately, the recursive call lemma_len_intersect::<A>(s1.remove(a), s2) succeeded, so we have some information from the postcondition of this call. Let’s write this out explictly so we can examine it more closely, substituting s1.remove(a) for s1:

{
    if s1.is_empty() {
        ...
    } else {
        let a = s1.choose();
        lemma_len_intersect::<A>(s1.remove(a), s2);
        assert(s1.remove(a).intersect(s2).len() <= s1.remove(a).len());

        assume(s1.intersect(s2).len() <= s1.len());
    }
}
verification results:: verified: 1 errors: 0

Let’s compare what we know above s1.remove(a) with what we’re trying to prove about s1:

        assert(s1.remove(a).intersect(s2).len() <= s1.remove(a).len()); // WE KNOW THIS

        assume(s1          .intersect(s2).len() <= s1          .len()); // WE WANT THIS

Is there any way we can make what we know look more like what we want? For example, how does s1.remove(a).len() relate to s1.len()? The value a is an element of s1, so if we remove it from s1, it should decrease the cardinality by 1:

{
    if s1.is_empty() {
        ...
    } else {
        let a = s1.choose();
        lemma_len_intersect::<A>(s1.remove(a), s2);
        assert(s1.remove(a).intersect(s2).len() <= s1.remove(a).len());
        assert(s1.remove(a).len() == s1.len() - 1);

        assume(s1.intersect(s2).len() <= s1.len());
    }
}
verification results:: verified: 1 errors: 0

So we can simplify a bit:

{
    if s1.is_empty() {
        ...
    } else {
        let a = s1.choose();
        lemma_len_intersect::<A>(s1.remove(a), s2);
        assert(s1.remove(a).intersect(s2).len() <= s1.remove(a).len());
        assert(s1.remove(a).intersect(s2).len() <= s1.len() - 1);
        assert(s1.remove(a).intersect(s2).len() + 1 <= s1.len());

        assume(s1.intersect(s2).len() <= s1.len());
    }
}
verification results:: verified: 1 errors: 0

Now the missing piece is the relation between s1.remove(a).intersect(s2).len() + 1 and s1.intersect(s2).len():

{
    if s1.is_empty() {
        ...
    } else {
        let a = s1.choose();
        lemma_len_intersect::<A>(s1.remove(a), s2);
        assert(s1.remove(a).intersect(s2).len() <= s1.remove(a).len());
        assert(s1.remove(a).intersect(s2).len() <= s1.len() - 1);
        assert(s1.remove(a).intersect(s2).len() + 1 <= s1.len());

        assume(s1.intersect(s2).len() <= s1.remove(a).intersect(s2).len() + 1);

        assert(s1.intersect(s2).len() <= s1.len());
    }
}
verification results:: verified: 1 errors: 0

If we can prove the assumption s1.intersect(s2).len() <= s1.remove(a).intersect(s2).len() + 1, we’ll be done:

        assume(s1          .intersect(s2).len()
            <= s1.remove(a).intersect(s2).len() + 1);

Is there anyway we can make s1.remove(a).intersect(s2) look more like s1.intersect(s2) so that it’s easier to prove this inequality? If we switched the order from s1.remove(a).intersect(s2) to s1.intersect(s2).remove(a), then the subexpression s1.intersect(s2) would match:

        assume(s1.intersect(s2)          .len()
            <= s1.intersect(s2).remove(a).len() + 1);

so let’s try that:

{
    if s1.is_empty() {
        ...
    } else {
        let a = s1.choose();
        lemma_len_intersect::<A>(s1.remove(a), s2);
        assert(s1.remove(a).intersect(s2).len() <= s1.remove(a).len());
        assert(s1.remove(a).intersect(s2).len() <= s1.len() - 1);
        assert(s1.remove(a).intersect(s2).len() + 1 <= s1.len());

        assert(s1.intersect(s2).len() <= s1.intersect(s2).remove(a).len() + 1);
        assert(s1.intersect(s2).len() <= s1.remove(a).intersect(s2).len() + 1);

        assert(s1.intersect(s2).len() <= s1.len());
    }
}
error: assertion failed
   |
   |         assert(s1.intersect(s2).len() <= s1.remove(a).intersect(s2).len() + 1);
   |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ assertion failed

One of these assertion succeeds and the other fails. The only difference between the successful assertion and the failing assertion is the order of intersect and remove in s1.intersect(s2).remove(a) and s1.remove(a).intersect(s2), so all we need to finish the proof is for s1.intersect(s2).remove(a) to be equal to s1.remove(a).intersect(s2):

{
    if s1.is_empty() {
        ...
    } else {
        let a = s1.choose();
        lemma_len_intersect::<A>(s1.remove(a), s2);
        assert(s1.remove(a).intersect(s2).len() <= s1.remove(a).len());
        assert(s1.remove(a).intersect(s2).len() <= s1.len() - 1);
        assert(s1.remove(a).intersect(s2).len() + 1 <= s1.len());

        assert(s1.intersect(s2).len() <= s1.intersect(s2).remove(a).len() + 1);
        assume(s1.intersect(s2).remove(a) === s1.remove(a).intersect(s2));
        assert(s1.intersect(s2).len() <= s1.remove(a).intersect(s2).len() + 1);

        assert(s1.intersect(s2).len() <= s1.len());
    }
}
verification results:: verified: 1 errors: 0

Again, we found ourselves needing to know the equality of two collections. And again, the first thing to try is to assert extensional equality:

{
    if s1.is_empty() {
        ...
    } else {
        let a = s1.choose();
        lemma_len_intersect::<A>(s1.remove(a), s2);
        assert(s1.remove(a).intersect(s2).len() <= s1.remove(a).len());
        assert(s1.remove(a).intersect(s2).len() <= s1.len() - 1);
        assert(s1.remove(a).intersect(s2).len() + 1 <= s1.len());

        assert(s1.intersect(s2).len() <= s1.intersect(s2).remove(a).len() + 1);
        assert(s1.intersect(s2).remove(a) =~= s1.remove(a).intersect(s2));
        assert(s1.intersect(s2).remove(a) === s1.remove(a).intersect(s2));
        assert(s1.intersect(s2).len() <= s1.remove(a).intersect(s2).len() + 1);

        assert(s1.intersect(s2).len() <= s1.len());
    }
}
verification results:: verified: 1 errors: 0

It works! Now we’ve eliminated all the assumes, so we’ve completed the verification:

pub proof fn lemma_len_intersect<A>(s1: Set<A>, s2: Set<A>)
    requires
        s1.finite(),
    ensures
        s1.intersect(s2).len() <= s1.len(),
    decreases
        s1.len(),
{
    if s1.is_empty() {
        assert(s1.intersect(s2) =~= Set::empty());
        assert(s1.intersect(s2) === Set::empty());
        assert(s1.intersect(s2).len() == 0);
        assert(s1.intersect(s2).len() <= s1.len());
    } else {
        let a = s1.choose();
        lemma_len_intersect::<A>(s1.remove(a), s2);
        assert(s1.remove(a).intersect(s2).len() <= s1.remove(a).len());
        assert(s1.remove(a).intersect(s2).len() <= s1.len() - 1);
        assert(s1.remove(a).intersect(s2).len() + 1 <= s1.len());

        assert(s1.intersect(s2).len() <= s1.intersect(s2).remove(a).len() + 1);
        assert(s1.intersect(s2).remove(a) =~= s1.remove(a).intersect(s2));
        assert(s1.intersect(s2).remove(a) === s1.remove(a).intersect(s2));
        assert(s1.intersect(s2).len() <= s1.remove(a).intersect(s2).len() + 1);

        assert(s1.intersect(s2).len() <= s1.len());
    }
}
verification results:: verified: 1 errors: 0

The code above contains a lot of unnecessary asserts, though, so it’s worth spending a few minutes cleaning the code up for sake of anyone who has to maintain the code in the future. We want to clear out unnecessary code so there’s less code to maintain, but keep enough information so someone maintaining the code can still understand the code. The right amount of information is a matter of taste, but we can try to strike a reasonable balance between conciseness and informativeness:

pub proof fn lemma_len_intersect<A>(s1: Set<A>, s2: Set<A>)
    requires
        s1.finite(),
    ensures
        s1.intersect(s2).len() <= s1.len(),
    decreases s1.len(),
{
    if s1.is_empty() {
        assert(s1.intersect(s2).len() == 0) by {
            assert(s1.intersect(s2) =~= s1);
        }
    } else {
        let a = s1.choose();
        lemma_len_intersect(s1.remove(a), s2);
        // by induction: s1.remove(a).intersect(s2).len() <= s1.remove(a).len()
        assert(s1.intersect(s2).remove(a).len() <= s1.remove(a).len()) by {
            assert(s1.intersect(s2).remove(a) =~= s1.remove(a).intersect(s2));
        }
        // simplifying ".remove(a).len()" yields s1.intersect(s2).len() <= s1.len())

    }
}

Quantifiers

Suppose that we want to specify that all the elements of a sequence are even. If the sequence has a small, fixed size, we could write a specification for every element separately:

spec fn is_even(i: int) -> bool {
    i % 2 == 0
}

proof fn test_seq_5_is_evens(s: Seq<int>)
    requires
        s.len() == 5,
        is_even(s[0]),
        is_even(s[1]),
        is_even(s[3]),
        is_even(s[3]),
        is_even(s[4]),
{
    assert(is_even(s[3]));
}

Clearly, though, this won’t scale well to larger sequences or sequences of unknown length.

We could write a recursive specification:

spec fn all_evens(s: Seq<int>) -> bool
    decreases s.len(),
{
    if s.len() == 0 {
        true
    } else {
        is_even(s.last()) && all_evens(s.drop_last())
    }
}

proof fn test_seq_recursive(s: Seq<int>)
    requires
        s.len() == 5,
        all_evens(s),
{
    assert(is_even(s[3])) by {
        reveal_with_fuel(all_evens, 2);
    }
}

However, using a recursive definition will lead to many proofs by induction, which can require a lot of programmer effort to write.

Fortunately, Verus and SMT solvers support the universal and existential quantifiers forall and exists, which we can think of as infinite conjunctions or disjunctions:

(forall|i: int| f(i)) = ... f(-2) && f(-1) && f(0) && f(1) && f(2) && ...
(exists|i: int| f(i)) = ... f(-2) || f(-1) || f(0) || f(1) || f(2) || ...

With this, it’s much more convenient to write a specification about all elements of a sequence:

proof fn test_use_forall(s: Seq<int>)
    requires
        5 <= s.len(),
        forall|i: int| 0 <= i < s.len() ==> #[trigger] is_even(s[i]),
{
    assert(is_even(s[3]));
}

Although quantifiers are very powerful, they require some care, because the SMT solver’s reasoning about quantifiers is incomplete. This isn’t a deficiency in the SMT solver’s implementation, but rather a deeper issue: it’s an undecidable problem to figure out whether a formula with quantifiers, functions, and arithmetic is valid or not, so there’s no complete algorithm that the SMT solver could implement. Instead, the SMT solver uses an incomplete strategy based on triggers, which instantiates quantifiers when expressions match trigger patterns.

This chapter will describe how to use forall and exists, how triggers work, and some related topics on choose expressions and closures.

forall and triggers

Let’s take a closer look at the following code, which uses a forall expression in a requires clause to prove an assertion:

proof fn test_use_forall(s: Seq<int>)
    requires
        5 <= s.len(),
        forall|i: int| 0 <= i < s.len() ==> #[trigger] is_even(s[i]),
{
    assert(is_even(s[3]));
}

The forall expression means that 0 <= i < s.len() ==> is_even(s[i]) for all possible integers i:

...
0 <= -3 < s.len() ==> is_even(s[-3])
0 <= -2 < s.len() ==> is_even(s[-2])
0 <= -1 < s.len() ==> is_even(s[-1])
0 <= 0 < s.len() ==> is_even(s[0])
0 <= 1 < s.len() ==> is_even(s[1])
0 <= 2 < s.len() ==> is_even(s[2])
0 <= 3 < s.len() ==> is_even(s[3])
...

There are infinitely many integers i, so the list shown above is infinitely long. We can’t expect the SMT solver to literally expand the forall into an infinite list of expressions. Furthermore, in this example, we only care about one of the expressions, the expression for i = 3, since this is all we need to prove assert(is_even(s[3])):

0 <= 3 < s.len() ==> is_even(s[3])

Ideally, the SMT solver will choose just the i that are likely to be relevant to verifying a particular program. The most common technique that SMT solvers use for choosing likely relevant i is based on triggers (also known as SMT patterns or just patterns).

A trigger is simply an expression or set of expressions that the SMT solver uses as a pattern to match with. In the example above, the #[trigger] attribute marks the expression is_even(s[i]) as the trigger for the forall expression. Based on this attribute, the SMT solver looks for expressions of the form is_even(s[...]). During the verification of the test_use_forall function shown above, there is one expression that has this form: is_even(s[3]). This matches the trigger is_even(s[i]) exactly for i = 3. Based on this pattern match, the SMT solver chooses i = 3 and introduces the following fact:

0 <= 3 < s.len() ==> is_even(s[3])

This fact allows the SMT solver to complete the proof about the assertion assert(is_even(s[3])).

Triggers are the way you program the instantiations of the forall expressions (and the way you program proofs of exists expressions, as discussed in a later section). By choosing different triggers, you can influence how the forall expressions get instantiated with different values, such as i = 3 in the example above. Suppose, for example, we change the assertion slightly so that we assert s[3] % 2 == 0 instead of is_even(s[3]). Mathematically, these are both equivalent. However, the assertion about s[3] % 2 == 0 fails:

spec fn is_even(i: int) -> bool {
    i % 2 == 0
}

proof fn test_use_forall_fail(s: Seq<int>)
    requires
        5 <= s.len(),
        forall|i: int| 0 <= i < s.len() ==> #[trigger] is_even(s[i]),
{
    assert(s[3] % 2 == 0); // FAILS: doesn't trigger is_even(s[i])
}

This fails because there are no expressions matching the pattern is_even(s[...]); the expression s[3] % 2 == 0 doesn’t mention is_even at all. In order to prove s[3] % 2 == 0, we’d first have to mention is_even(s[3]) explicitly:

proof fn test_use_forall_succeeds1(s: Seq<int>)
    requires
        5 <= s.len(),
        forall|i: int| 0 <= i < s.len() ==> #[trigger] is_even(s[i]),
{
    assert(is_even(s[3]));  // triggers is_even(s[3])
    assert(s[3] % 2 == 0);  // succeeds, because previous line already instantiated the forall
}

Once the expression is_even(s[3]) coaxes the SMT solver into instantiating the forall expression with i = 3, the SMT solver can use the resulting 0 <= 3 < s.len() ==> is_even(s[3]) to prove s[3] % 2 == 0.

Alternatively, we could just choose a trigger that is less picky. For example, the trigger s[i] matches any expression of the form s[...], which includes the s[3] inside s[3] % 2 == 0 and also includes the s[3] inside is_even(s[3]):

proof fn test_use_forall_succeeds2(s: Seq<int>)
    requires
        5 <= s.len(),
        forall|i: int| 0 <= i < s.len() ==> is_even(#[trigger] s[i]),
{
    assert(s[3] % 2 == 0);  // succeeds by triggering s[3]
}

In fact, if we omit the #[trigger] attribute entirely, Verus chooses the trigger s[i] automatically:

proof fn test_use_forall_succeeds3(s: Seq<int>)
    requires
        5 <= s.len(),
        forall|i: int| 0 <= i < s.len() ==> is_even(s[i]), // Verus chooses s[i] as the trigger and prints a note
{
    assert(s[3] % 2 == 0); // succeeds by triggering s[3]
}

In fact, Verus prints a note stating that it chose this trigger:

note: automatically chose triggers for this expression:
   |
   |         forall|i: int| 0 <= i < s.len() ==> is_even(s[i]), // Verus chooses s[i] as the trigger
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

note:   trigger 1 of 1:
   |
   |         forall|i: int| 0 <= i < s.len() ==> is_even(s[i]), // Verus chooses s[i] as the trigger
   |                                                     ^^^^

note: Verus printed one or more automatically chosen quantifier triggers
      because it had low confidence in the chosen triggers.

Verus isn’t sure, though, whether the programmer wants s[i] as the trigger or is_even(s[i]) as the trigger. It slightly prefers s[i] because s[i] is smaller than is_even(s[i]), so it chooses s[i], but it also prints out the note encouraging the programmer to review the decision. The programmer can accept this decision by writing #![auto] before the quantifier body, which suppresses the note:

proof fn test_use_forall_succeeds4(s: Seq<int>)
    requires
        5 <= s.len(),
        forall|i: int|
            #![auto]
            0 <= i < s.len() ==> is_even(s[i]),  // Verus chooses s[i] as the trigger
{
    assert(s[3] % 2 == 0);  // succeeds by triggering s[3]
}

Good triggers and bad triggers

So … which trigger is better, s[i] or is_even(s[i])? Unfortunately, there’s no one best answer to this kind of question. There are tradeoffs between the two different choices. The trigger s[i] leads to more pattern matches than is_even(s[i]). More matches means that the SMT solver is more likely to find relevant instantiations that help a proof succeed. However, more matches also means that the SMT solver is more likely to generate irrelevant instantiations that clog up the SMT solver with useless information, slowing down the proof.

In this case, s[i] is probably a good trigger to choose. It matches whenever the function test_use_forall_succeeds4 talks about an element of the sequence s, yielding a fact that is likely to be useful for reasoning about s. By contrast, suppose we chose the following bad trigger, 0 <= i:

proof fn test_use_forall_bad1(s: Seq<int>)
    requires
        5 <= s.len(),
        forall|i: int| (#[trigger](0 <= i)) && i < s.len() ==> is_even(s[i]),
{
    assert(s[3] % 2 == 0);
}

In principle, this would match any value that is greater than or equal to 0, which would include values that have nothing to do with s and are unlikely to be relevant to s. In practice, Verus doesn’t even let you do this: triggers cannot contain equality or disequality (==, ===, !=, or !==), any basic integer arithmetic operator (like <= or +), or any basic boolean operator (like &&):

error: trigger must be a function call, a field access, or a bitwise operator
    |
    |         forall|i: int| (#[trigger](0 <= i)) && i < s.len() ==> is_even(s[i]),
    |                        ^^^^^^^^^^^^^^^^^^^^

If we really wanted, we could work around this by introducing an extra function:

spec fn nonnegative(i: int) -> bool {
    0 <= i
}

proof fn test_use_forall_bad2(s: Seq<int>)
    requires
        5 <= s.len(),
        forall|i: int| #[trigger] nonnegative(i) && i < s.len() ==> is_even(s[i]),
{
    assert(is_even(s[3])); // FAILS: doesn't trigger nonnegative(i)
}

but this trigger fails to match, because the code doesn’t explicitly mention nonnegative(3) (you’d have to add an explicit assert(nonnegative(3)) to make the code work). This is probably just as well; s[i] is simply a better trigger than nonnegative(i), because s[i] mentions s, and the whole point of forall|i: int| 0 <= i < s.len() ==> is_even(s[i]) is to say something about the elements of s, not to say something about nonnegative numbers.

Multiple variables, multiple triggers, matching loops

Suppose we have a forall expression with more than one variable, i and j:

spec fn is_distinct(x: int, y: int) -> bool {
    x != y
}

proof fn test_distinct1(s: Seq<int>)
    requires
        5 <= s.len(),
        forall|i: int, j: int| 0 <= i < j < s.len() ==> #[trigger] is_distinct(s[i], s[j]),
{
    assert(is_distinct(s[2], s[4]));
}

The forall expression shown above says that every element of s is distinct. (Note: we could have written 0 <= i < s.len() && 0 <= j < s.len() && i != j instead of 0 <= i < j < s.len(), but the latter is more concise and is just as general: given any two distinct integers, we can let i be the smaller one and j be the larger one so that i < j.)

In the example above, the trigger is_distinct(s[i], s[j]) contains both the variables i and j, and the expression is_distinct(s[2], s[4]) matches the trigger with i = 2, j = 4:

0 <= 2 < 4 < s.len() ==> is_distinct(s[2], s[4])

Instead of using a function call is_distinct(s[i], s[j]), we could just write s[i] != s[j] directly. However, in this case, we cannot use the expression s[i] != s[j] as a trigger, because, as discussed in the previous section, triggers cannot contain equalities and disequalities like !=. However, a trigger does not need to be just a single expression. It can be split across multiple expressions, as in the following code, which defines the trigger to be the pair of expressions s[i], s[j]:

proof fn test_distinct2(s: Seq<int>)
    requires
        5 <= s.len(),
        forall|i: int, j: int| 0 <= i < j < s.len() ==> #[trigger] s[i] != #[trigger] s[j],
{
    assert(s[4] != s[2]);
}

Verus also supports an alternate, equivalent syntax #![trigger ...], where the #![trigger ...] immediately follows the forall|...|, in case we prefer to write the pair s[i], s[j] directly:

proof fn test_distinct3(s: Seq<int>)
    requires
        5 <= s.len(),
        forall|i: int, j: int| #![trigger s[i], s[j]] 0 <= i < j < s.len() ==> s[i] != s[j],
{
    assert(s[4] != s[2]);
}

When the trigger is the pair s[i], s[j], there are four matches: i = 2, j = 2 and i = 2, j = 4 and i = 4, j = 2 and i = 4, j = 4:

0 <= 2 < 2 < s.len() ==> s[2] != s[2]
0 <= 2 < 4 < s.len() ==> s[2] != s[4]
0 <= 4 < 2 < s.len() ==> s[4] != s[2]
0 <= 4 < 4 < s.len() ==> s[4] != s[4]

The i = 2, j = 4 instantiation proves s[2] != s[4], which is equivalent to s[4] != s[2]. The other instantiations are dead ends, since 2 < 2, 4 < 2, and 4 < 4 all fail.

A trigger must mention each of the quantifier variables i and j at least once. Otherwise, Verus will complain:

proof fn test_distinct_fail1(s: Seq<int>)
    requires
        5 <= s.len(),
        forall|i: int, j: int|
            0 <= i < j < s.len() ==> s[i] != #[trigger] s[j], // error: trigger fails to mention i
{
    assert(s[4] != s[2]);
}
error: trigger does not cover variable i
    |
    | / ...   forall|i: int, j: int|
    | | ...       0 <= i < j < s.len() ==> s[i] != #[trigger] s[j], // error: trigger fails to ment...
    | |__________________________________________________________^

In order to match a trigger with multiple expressions, the SMT solver has to find matches for all the expressions in the trigger. Therefore, you can always make a trigger more restrictive by adding more expressions to the trigger. For example, we could gratuitously add a third expression is_even(i) to the trigger, which would cause the match to fail, since no expression matches is_even(i):

proof fn test_distinct_fail2(s: Seq<int>)
    requires
        5 <= s.len(),
        forall|i: int, j: int| #![trigger s[i], s[j], is_even(i)]
            0 <= i < j < s.len() ==> s[i] != s[j],
{
    assert(s[4] != s[2]); // FAILS, because nothing matches is_even(i)
}

To make this example succeed, we’d have to mention is_even(2) explicitly:

    assert(is_even(2));
    assert(s[4] != s[2]); // succeeds; we've matched s[2], s[4], is_even(2)

Multiple triggers

In all the examples so far, each quantifier contained exactly one trigger (although the trigger sometimes contained more than one expression). It’s also possible, although rarer, to specify multiple triggers for a quantifier. The SMT solver will instantiate the quantifier if any of the triggers match. Thus, adding more triggers leads to more quantifier instantiations. (This stands in contrast to adding expressions to a trigger: adding more expressions to a trigger makes a trigger more restrictive and leads to fewer quantifier instantiations.)

The following example specifies both #![trigger a[i], b[j]] and #![trigger a[i], c[j]] as triggers, since neither is obviously better than the other:

proof fn test_multitriggers(a: Seq<int>, b: Seq<int>, c: Seq<int>)
    requires
        5 <= a.len(),
        a.len() == b.len(),
        a.len() == c.len(),
        forall|i: int, j: int|
            #![trigger a[i], b[j]]
            #![trigger a[i], c[j]]
            0 <= i < j < a.len() ==> a[i] != b[j] && a[i] != c[j],
{
    assert(a[2] != c[4]);  // succeeds, matches a[i], c[j]
}

(Note: to specify multiple triggers, you must use the #![trigger ...] syntax rather than the #[trigger] syntax.)

If the quantifier had only mentioned the single trigger #![trigger a[i], b[j]], then the assertion above would have failed, because a[2] != c[4] doesn’t mention b. A single trigger #![trigger a[i], b[j], c[j]] would be even more restrictive, requiring both b and c to appear, so the assertion would still fail.

In the example above, you can omit the explicit triggers and Verus will automatically infer exactly the two triggers #![trigger a[i], b[j]] and #![trigger a[i], c[j]]. However, in most cases, Verus deliberately avoids inferring more than one trigger, because multiple triggers lead to more quantifier instantiations, which potentially slows down the SMT solver. One trigger is usually enough.

As an example of where one trigger is safer than multiple triggers, consider an assertion that says that updating element j of sequence s leaves element i unaffected:

proof fn seq_update_different<A>(s: Seq<A>, i: int, j: int, a: A) {
    assert(forall|i: int, j: int|
        0 <= i < s.len() && 0 <= j < s.len() && i != j ==> s.update(j, a)[i] == s[i]);
}

There are actually two possible triggers for this:

#![trigger s.update(j, a)[i]]
#![trigger s.update(j, a), s[i]]

However, Verus selects only the first one and rejects the second, in order to avoid too many quantifier instantiations:

note: automatically chose triggers for this expression:
    |
    |       assert(forall|i: int, j: int|
    |  ____________^
    | |         0 <= i < s.len() && 0 <= j < s.len() && i != j ==> s.update(j, a)[i] === s[i]
    | |_____________________________________________________________________________________^

note:   trigger 1 of 1:
   --> .\rust_verify\example\guide\quants.rs:243:60
    |
    |         0 <= i < s.len() && 0 <= j < s.len() && i != j ==> s.update(j, a)[i] === s[i]
    |                                                            ^^^^^^^^^^^^^^^^^

(Note: you can use the --triggers command-line option to print the message above.)

Matching loops: what they are and to avoid them

Suppose we want to specify that a sequence is sorted. We can write this in a similar way to the earlier forall expression about sequence distinctness, writing s[i] <= s[j] in place of s[i] != s[j]:

proof fn test_sorted_good(s: Seq<int>)
    requires
        5 <= s.len(),
        forall|i: int, j: int| 0 <= i <= j < s.len() ==> s[i] <= s[j],
{
    assert(s[2] <= s[4]);
}

In Verus, this is the best way to express sortedness, because the trigger s[i], s[j] works very well. However, there is an alternate approach. Instead of quantifying over both i and j, we could try to quantify over just a single variable i, and then compare s[i] to s[i + 1]:

proof fn test_sorted_bad(s: Seq<int>)
    requires
        5 <= s.len(),
        forall|i: int|
            0 <= i < s.len() - 1 ==> s[i] <= s[i + 1],
{
    assert(s[2] <= s[4]);
}

However, Verus complains that it couldn’t find any good triggers:

error: Could not automatically infer triggers for this quantifer.  Use #[trigger] annotations to manually mark trigger terms instead.
    |
    | /         forall|i: int|
    | |             0 <= i < s.len() - 1 ==> s[i] <= s[i + 1],
    | |_____________________________________________________^

Verus considers the expressions 0 <= i, i < s.len() - 1, s[i], and s[i + 1] as candidates for a trigger. However, all of these except s[i] contain integer arithmetic, which is not allowed in triggers. The remaining candidate, s[i], looks reasonable at first glance. Verus nevertheless rejects it, though, because it potentially leads to an infinite matching loop. Triggers are the way to program the SMT solver’s quantifier instantiations, and if we’re not careful, we can program infinite loops. Let’s look at how this can happen. Suppose that we insist on using s[i] as a trigger:

forall|i: int|
    0 <= i < s.len() - 1 ==> #[trigger] s[i] <= s[i + 1],

(TODO: Verus should print a warning about a potential matching loop here.)

This would, in fact, succeed in verifying the assertion s[2] <= s[4], but not necessarily in a good way. The SMT solver would match on i = 2 and i = 4. For i = 2, we’d get:

0 <= 2 < s.len() - 1 ==> s[2] <= s[3]

This creates a new expression s[3], which the SMT can then match on with i = 3:

0 <= 3 < s.len() - 1 ==> s[3] <= s[4]

This tells us s[2] <= s[3] and s[3] <= s[4], which is sufficient to prove s[2] <= s[4]. The problem is that the instantiations don’t necessarily stop here. Given s[4], we can match with i = 4, which creates s[5], which leads to matching with i = 5, and so on:

0 <= 4 < s.len() - 1 ==> s[4] <= s[5]
0 <= 5 < s.len() - 1 ==> s[5] <= s[6]
0 <= 6 < s.len() - 1 ==> s[6] <= s[7]
...

In principle, the SMT solver could loop forever with i = 6, i = 7, and so on. In practice, the SMT solver imposes a cutoff on quantifier instantiations which often (but not always) halts the infinite loops. But even if the SMT solver halts the loop, this is still an inefficient process, and matching loops should be avoided. (For an example of a matching loop that causes the SMT solver to use an infinite amount of time and memory, see this section.)

exists and choose

exists expressions are the dual of forall expressions. While forall|i: int| f(i) means that f(i) is true for all i, exists|i: int| f(i) means that f(i) is true for at least one i. To prove exists|i: int| f(i), an SMT solver has to find one value for i such that f(i) is true. This value is called a witness for exists|i: int| f(i). As with forall expressions, proofs about exists expressions are based on triggers. Specifically, to prove an exists expression, the SMT solver uses the exists expression’s trigger to try to find a witness.

In the following example, the trigger is is_even(i):

proof fn test_exists_succeeds() {
    assert(is_even(4));
    assert(!is_even(5));
    assert(is_even(6));
    assert(exists|i: int| #[trigger] is_even(i));  // succeeds with witness i = 4 or i = 6
}

There are three expressions that match the trigger: is_even(4), is_even(5), and is_even(6). Two of them, is_even(4) and is_even(6) are possible witnesses for exists|i: int| #[trigger] is_even(i). Based on these, the assertion succeeds, using either i = 4 or i = 6 as a witness.

By contrast, the same assertion fails in the following code, since no expressions matching is_even(i) are around:

proof fn test_exists_fails() {
    assert(exists|i: int| #[trigger] is_even(i)); // FAILS, no match for trigger
}

choose

The proofs above try to prove that an exists expression is true. Suppose, though, that we already know that an exists expression is true, perhaps because we assume it as a function precondition. This means that some witness to the exists expression must exist. If we want to get the witness, we can use a choose expression.

A choose expression choose|i: int| f(i) implements the Hilbert choice operator (sometimes known as epsilon): it chooses some value i that satisfies f(i) if such a value exists. Otherwise, it picks an arbitrary value for i.

The following example assumes exists|i: int| f(i) as a precondition. Based on this, the SMT solver knows that there is at least one witness i that makes f(i) true, and choose picks one of these witnesses arbitrarily:

spec fn f(i: int) -> bool;

proof fn test_choose_succeeds()
    requires
        exists|i: int| f(i),
{
    let i_witness = choose|i: int| f(i);
    assert(f(i_witness));
}

If, on the other hand, we don’t know exists|i: int| f(i), then choose just returns an arbitrary value that might not satisfy f(i) (as discussed in ghost vs exec code, ghost code can create an arbitrary value of any type):

proof fn test_choose_fails() {
    let i_witness = choose|i: int| f(i);
    assert(i_witness < 0 || i_witness >= 0); // i_witness is some integer
    assert(f(i_witness)); // FAILS because we don't know exists|i: int| f(i)
}

Regardless of whether we know exists|i: int| f(i) or not, the choose|i: int| f(i) expression always returns the same value:

proof fn test_choose_same() {
    let x = choose|i: int| f(i);
    let y = choose|i: int| f(i);
    assert(x == y);
}

You can also choose multiple values together, collecting the values in a tuple:

spec fn less_than(x: int, y: int) -> bool {
    x < y
}

proof fn test_choose_succeeds2() {
    assert(less_than(3, 7));  // promote i = 3, i = 7 as a witness
    let (x, y) = choose|i: int, j: int| less_than(i, j);
    assert(x < y);
}

In this example, the SMT solver can prove exists|i: int, j: int| less_than(i, j) because the expression less_than(3, 7) matches the automatically chosen trigger less_than(i, j) when i = 3 and j = 7, so that i = 3, j = 7 serves as a witness.

Proofs about forall and exists

The previous sections emphasized the importance of triggers for forall and exists expressions. Specifically, if you know forall|i| f(i), then the SMT solver will instantiate i by looking at triggers, and if you want to prove exists|i| f(i), then the SMT solver will look at triggers to find a witness i such that f(i) is true. In other words, using a forall expression relies on triggers and proving an exists expression relies on triggers. We can write these cases in the following table:

provingusing
forallusually just works; otherwise assert-bytriggers
existstriggersusually just works; otherwise choose

What about the other two cases, proving a forall expression and using an exists expression? These cases are actually easier to automate and do not rely on triggers. In fact, they often just work automatically, as in the following examples:

spec fn is_distinct(x: int, y: int) -> bool {
    x != y
}

spec fn dummy(i: int) -> bool;

proof fn prove_forall()
    ensures
        forall|i: int, j: int|
            #![trigger dummy(i), dummy(j)]
            is_distinct(i, j) ==> is_distinct(j, i),
{
    // proving the forall just works; the trigger is irrelevant
}

proof fn use_exists(x: int)
    requires
        exists|i: int| #![trigger dummy(i)] x == i + 1 && is_distinct(i, 5),
{
    // using the exists just works; the trigger is irrelevant
    assert(x != 6);
}

In these examples, the triggers play no role. To emphasize this, we’ve used a dummy function for the trigger that doesn’t even appear anywhere else in the examples, and the SMT solver still verifies the functions with no difficulty. (Note, though, that if you called one of the functions above, then the caller would have to prove the exists expression or use the forall expression, and the caller would have to deal with triggers.)

If you want some intuition for why the SMT solver doesn’t rely on triggers to verify the code above, you can think of the verification as being similar to the verification of the following code, where the quantifiers are eliminated and the quantified variables are hoisted into the function parameters:

proof fn hoisted_forall(i: int, j: int)
    ensures
        is_distinct(i, j) ==> is_distinct(j, i),
{
}

proof fn hosted_exists(x: int, i: int)
    requires
        x == i + 1 && is_distinct(i, 5),
{
    assert(x != 6);
}

Proving forall with assert-by

Sometimes a proof doesn’t “just work” like it does in the simple examples above. For example, the proof might rely on a lemma that is proved by induction, which the SMT solver cannot prove completely automatically. Suppose we have a lemma that proves f(i) for any even i:

spec fn f(i: int) -> bool { ... }

proof fn lemma_even_f(i: int)
    requires
        is_even(i),
    ensures
        f(i),
{ ... }

Now suppose we want to prove that f(i) is true for all even i:

proof fn test_even_f()
    ensures
        forall|i: int| is_even(i) ==> f(i), // FAILS because we don't call the lemma
{
}

The proof above fails because it doesn’t call lemma_even_f. If we try to call lemma_even_f, though, we immediately run into a problem: we need to pass i as an argument to the lemma, but i isn’t in scope:

proof fn test_even_f()
    ensures
        forall|i: int| is_even(i) ==> f(i),
{
    lemma_even_f(i); // ERROR: i is not in scope here
}

To deal with this, Verus supports a special form of assert ... by for proving forall expressions:

proof fn test_even_f()
    ensures
        forall|i: int| is_even(i) ==> f(i),
{
    assert forall|i: int| is_even(i) implies f(i) by {
        // First, i is in scope here
        // Second, we assume is_even(i) here
        lemma_even_f(i);
        // Finally, we have to prove f(i) here
    }
}

Inside the body of the assert ... by, the variables of the forall are in scope and the left-hand side of the ==> is assumed. This allows the body to call lemma_even_f(i).

Using exists with choose

The example above needed to bring a forall quantifier variable into scope in order to call a lemma. A similar situation can arise for exists quantifier variables. Suppose we have the following lemma to prove f(i):

spec fn g(i: int, j: int) -> bool { ... }

proof fn lemma_g_proves_f(i: int, j: int)
    requires
        g(i, j),
    ensures
        f(i),
{ ... }

If we know that there exists some j such that g(i, j) is true, we should be able to call lemma_g_proves_f. However, we run into the problem that j isn’t in scope:

proof fn test_g_proves_f(i: int)
    requires
        exists|j: int| g(i, j),
    ensures
        f(i),
{
    lemma_g_proves_f(i, j); // ERROR: j is not in scope here
}

In this situation, we can use choose (discussed in the previous section) to extract the value j from the exists expression:

proof fn test_g_proves_f(i: int)
    requires
        exists|j: int| g(i, j),
    ensures
        f(i),
{
    lemma_g_proves_f(i, choose|j: int| g(i, j));
}

Example: binary search

Let’s see how forall and exists work in a larger example. The following code searches for a value k in a sorted sequence and returns the index r where k resides.

fn binary_search(v: &Vec<u64>, k: u64) -> (r: usize)
    requires
        forall|i: int, j: int| 0 <= i <= j < v.len() ==> v[i] <= v[j],
        exists|i: int| 0 <= i < v.len() && k == v[i],
    ensures
        r < v.len(),
        k == v[r as int],
{
    let mut i1: usize = 0;
    let mut i2: usize = v.len() - 1;
    while i1 != i2
        invariant
            i2 < v.len(),
            exists|i: int| i1 <= i <= i2 && k == v[i],
            forall|i: int, j: int| 0 <= i <= j < v.len() ==> v[i] <= v[j],
    {
        let ix = i1 + (i2 - i1) / 2;
        if v[ix] < k {
            i1 = ix + 1;
        } else {
            i2 = ix;
        }
    }
    i1
}

fn main() {
    let mut v: Vec<u64> = Vec::new();
    v.push(0);
    v.push(10);
    v.push(20);
    v.push(30);
    v.push(40);
    assert(v[3] == 30);  // needed to trigger exists|i: int| ... k == v[i]
    let r = binary_search(&v, 30);
    assert(r == 3);
}

The precondition exists|i: int| 0 <= i < v.len() && k == v[i] specifies that k is somewhere in the sequence, so that the search is guaranteed to find it. The automatically inferred trigger for this exists expression is v[i]. The main function satisfies this with the witness i = 3 so that 30 == v[3]:

assert(v[3] == 30); // needed to trigger exists|i: int| ... k == v[i]
let r = binary_search(&v, 30);

The search proceeds by keeping two indices i1 and i2 that narrow in on k from both sides, so that the index containing k remains between i1 and i2 throughout the search:

exists|i: int| i1 <= i <= i2 && k == v[i]

In order for the loop to exit, the loop condition i1 != i2 must be false, which means that i1 and i2 must be equal. In this case, the i in the exists expression above must be equal to i1 and i2, so we know k == v[i1], so that we can return the result i1.

Proving that the loop invariant is maintained

In each loop iteration, we can assume that the loop invariants hold before the iteration, and we have to prove that the loop invariants hold after the iteration. Let’s look in more detail at the proof of the invariant exists|i: int| i1 <= i <= i2 && k == v[i], focusing on how the SMT solver handles the forall and exists quantifiers.

The key steps are:

  • Knowing exists|i: int| ... k == v[i] gives us a witness i_witness such that k == v[i_witness].
  • The witness i_witness from the current iteration’s exists|i: int| ... serves as the witness for the next iteration’s exists|i: int| ....
  • The comparison *v.index(ix) < k tells us whether v[ix] < k or v[ix] >= k.
  • The expressions v[i_witness] and v[ix] match the trigger v[i], v[j] trigger in the expression forall|i: int, j: int| ... v[i] <= v[j].

We’ll now walk through these steps in more detail. (Feel free to skip ahead if this is too boring — as the next subsection discusses, the whole point is that the SMT solver takes care of the boring details automatically if we set things up right.)

There are two cases to consider, one where the if condition *v.index(ix) < k is true and one where *v.index(ix) < k is false. We’ll just look at the former, where v[ik] < k.

We assume the loop invariant at the beginning of the loop iteration:

exists|i: int| i1 <= i <= i2 && k == v[i]

This tells us that there is some witness i_witness such that:

i1 <= i_witness <= i2 && k == v[i_witness]

In the case where *v.index(ix) < k is true, we execute i1 = ix + 1:

let ix = i1 + (i2 - i1) / 2;
if *v.index(ix) < k {
    i1 = ix + 1;
} else {

Since the new value of i1 is ix + 1, we’ll need to prove the loop invariant with ix + 1 substituted for i1:

exists|i: int| ix + 1 <= i <= i2 && k == v[i]

To prove an exists expression, the SMT solver needs to match the expression’s trigger. The automatically chosen trigger for this expression is v[i], so the SMT solver looks for expressions of the form v[...]. It finds v[i_witness] from the previous loop invariant (shown above). It also finds v[ix] from the call v.index(ix) in the expression *v.index(ix) < k. Based on these, it attempts to prove ix + 1 <= i <= i2 && k == v[i] with i = i_witness or i = ix:

ix + 1 <= i_witness <= i2 && k == v[i_witness]
ix + 1 <= ix <= i2 && k == v[ix]

The i = ix case is a dead end, because ix + 1 <= ix is never true. The i = i_witness case is more promising. We already know i_witness <= i2 and k == v[i_witness] from our assumptions about i_witness at the beginning of the loop iteration. We just need to prove ix + 1 <= i_witness. We can simplify this to ix < i_witness.

Proving ix < i_witness

To prove ix < i_witness, we now turn to the forall loop invariant:

forall|i: int, j: int| 0 <= i <= j < v.len() ==> v[i] <= v[j],

In order to instantiate this, the SMT solver again relies on triggers. In this forall, expression, the trigger is v[i], v[j], so again the SMT solver looks for terms of the form v[...] and finds v[i_witness] and v[ix]. There are four different possible assignments of i_witness and ix to i and j.

0 <= i_witness <= i_witness < v.len() ==> v[i_witness] <= v[i_witness]
0 <= i_witness <= ix < v.len() ==> v[i_witness] <= v[ix]
0 <= ix <= i_witness < v.len() ==> v[ix] <= v[i_witness]
0 <= ix <= ix < v.len() ==> v[ix] <= v[ix]

Out of these, the second one is most useful:

0 <= i_witness <= ix < v.len() ==> v[i_witness] <= v[ix]

We already know k == v[i_witness], so this becomes:

0 <= i_witness <= ix < v.len() ==> k <= v[ix]

The right-hand side of the ==> says k <= v[ix], which contradicts our assumption that v[ik] < k in the case where *v.index(ix) < k. This means that the left-hand side of the ==> must be false:

!(0 <= i_witness <= ix < v.len())

The SMT solver knows that 0 <= i_witness and ix < v.len(), so it narrows this down to:

!(i_witness <= ix)

This tells us that ix < i_witness, which is what we want.

Helping the automation succeed

As seen in the previous section, proving the loop invariant requires a long chain of reasoning. Fortunately, the SMT solver performs all of these steps automatically. In fact, this is a particularly fortunate example, because Verus automatically chooses the triggers as well, and these triggers happen to be just what the SMT solver needs to complete the proof.

In general, though, how we express the preconditions, postconditions, and loop invariants has a big influence on whether Verus and the SMT solver succeed automatically. Suppose, for example, that we had written the sortedness condition (in the precondition and loop invariant) as:

forall|i: int| 0 <= i < v.len() - 1 ==> #[trigger] v[i] <= v[i + 1]

instead of:

forall|i: int, j: int| 0 <= i <= j < v.len() ==> v[i] <= v[j]

As discussed in a previous section, the trigger v[i] in combination with v[i] <= v[i + 1] leads to a matching loop, which can send the SMT solver into an infinite loop. This is, in fact, exactly what happens:

error: while loop: Resource limit (rlimit) exceeded; consider rerunning with --profile for more details
    |
    | /     while i1 != i2
    | |         invariant
    | |             i2 < v.len(),
    | |             exists|i: int| i1 <= i <= i2 && k == v[i],
      |
    | |         }
    | |     }
    | |_____^

Even if the SMT solver had avoided the infinite loop, though, it’s hard to see how it could have succeeded automatically. As discussed above, a crucial step involves instantiating i = i_witness and j = ix to learn something about v[i_witness] <= v[ix]. This simply isn’t a possible instantiation when there’s only one variable i in the forall expression. Learning something about v[i_witness] <= v[ix] would require chaining together an arbitrarily long sequence of v[i] <= v[i + 1] steps to get from i_witness to i_witness + 1 to i_witness + 2 all the way to ix. This would require a separate proof by induction. Intuitively, the expression v[i] <= v[j] is better suited than v[i] <= v[i + 1] to an algorithm like binary search that takes large steps from one index to another, because i and j can be arbitrarily far apart, whereas i and i + 1 are only one element apart.

When the SMT automation fails, it’s often tempting to immediately start adding asserts, lemmas, proofs by induction, etc., until the proof succeeds. Given enough manual effort, we could probably finish a proof of binary search with the problematic v[i] <= v[i + 1] definition of sortedness. But this would be a mistake; it’s better to structure the definitions in a way that helps the automation succeed without so much manual effort. If you find yourself writing a long manual proof, it’s worth stepping back and figuring out why the automation is failing; maybe a change of definitions can fix the failure in the automation.

After all, if your car breaks down, it’s usually better to fix the car than to push it.

Adding Ambient Facts to the Proof Environment with broadcast

In a typical Verus project, a developer might prove a fact (e.g., that reversing a sequence preserves its length) in a proof function, e.g.,

pub proof fn seq_reverse_len<A>(s: Seq<A>)
    ensures
        reverse(s).len() == s.len(), 
{
  ...
}

To make use of this fact, the developer must explicitly invoke the proof function, e.g.,

fn example(s: Seq<bool>) {
  let t = reverse(s);
  // assert(t.len() == s.len()); // FAILS
  seq_reverse(s);                // Adds the proof's fact to the proof environment
  assert(t.len() == s.len());    // SUCCEEDS
}

However, in some cases, a proof fact is so useful that a developer always wants it to be in scope, without manually invoking the corresponding proof. For example, the fact that an empty sequence’s length is zero is so “obvious” that most programmers will expect Verus to always know it. This feature should be used with caution, however, as every extra ambient fact slows the prover’s overall performance.

Suppose that after considering the impact on the solver’s performance, the programmer decides to make the above fact about reverse ambient. To do so, they can add the broadcast modifier in the definition of seq_reverse_len: pub broadcast proof fn seq_reverse_len<A>(s: Seq<A>). The effect is to introduce the following quantified fact to the proof environment:

forall |s| reverse(s).len() == s.len()

Because this introduces a quantifier, Verus will typically ask you to explicitly choose a trigger, e.g., by adding a #[trigger] annotation. Hence, the final version of our example might look like this:

pub broadcast proof fn seq_reverse_len<A>(s: Seq<A>)
    ensures
        #[trigger] reverse(s).len() == s.len(), 
{
  ...
}

To bring this ambient lemma into scope, for a specific proof, or for an entire module, you can use broadcast use seq_reverse_len;.

Some of these broadcast-ed lemmas are available in the verus standard library vstd, some as part of broadcast “groups”, which combine a number of properties into a single group name, which can be brought into scope with broadcast use broadcast_group_name;. We are working on extending the discoverablility of these groups in the standard library documentation: they currently appear as regular functions.

SMT solving and automation

Sometimes an assertion will fail even though it’s true. At a high level, Verus works by generating formulas called “verification conditions” from a program’s assertions and specifications (requires and ensures clauses); if these verification conditions are always true, then all of the assertions and specifications hold. The verification conditions are checked by an SMT solver (Z3), and therefore Verus is limited by Z3’s ability to prove generated verification conditions.

This section walks through the reasons why a proof might fail.

The first reason why a proof might fail is that the statement is wrong! If there is a bug in a specification or assertion, then we hope that Z3 will not manage to prove it. We won’t talk too much about this case in this document, but it’s important to keep this in mind when debugging proofs.

The core reason for verification failures is that proving the verification conditions from Verus is an undecidable task: there is no algorithm that can prove general formulas true. In practice Z3 is good at proving even complex formulas are true, but there are some features that lead to inconclusive verification results.

Quantifiers: Proving theorems with quantifiers (exists and forall) is in general undecidable. For Verus, we rely on Z3’s pattern-based instantiation of quantifiers (“triggers”) to use and prove formulas with quantifiers. See the section on forall and triggers for more details.

Opaque and closed functions: Verification conditions by default hide the bodies of opaque and closed functions; revealing those bodies might make verification succeed, but Verus intentionally leaves this to the user to improve performance and allow hiding where desired.

Inductive invariants: Reasoning about recursion (loops, recursive lemmas) requires an inductive invariant, which Z3 cannot in general come up with.

Extensional equality assertions: If a theorem requires extensional equality (eg, between sequences, maps, or spec functions), this typically requires additional assertions in the proof. The key challenge is that there are many possible sequence expressions (for example) in a program that Z3 could attempt to prove are equal. For performance reasons Z3 cannot attempt to prove all pairs of expressions equal, both because there are too many (including the infinitely many not in the program at all) and because each proof involves quantifiers and is reasonably expensive. The result is that a proof may start working if you add an equality assertion: the assertion explicitly asks Z3 to prove and use an equality. See extensional equality for how to use the extensional equality operators =~= and =~~=.

Incomplete axioms: The standard library includes datatypes like Map and Seq that are implemented using axioms that describe their expected properties. These axioms might be incomplete; there may be a property that you intuitively expect a map or sequence to satisfy but which isn’t implied by the axioms, or which is implied but requires a proof by induction. If you think this is the case, please open an issue or a pull request adding the missing axiom.

Slow proofs: Z3 may be able to find a proof but it would simply take too long. We limit how long Z3 runs (using its resource limit or “rlimit” feature so that this limit is independent of how fast the computer is), and consider it a failure if Z3 runs into this limit. The philosophy of Verus is that it’s better to improve solver performance than rely on a slow proof. Improving SMT performance talks more about what you can do to diagnose and fix poor verification performance.

Integers: Nonlinear Arithmetic

Generally speaking, Verus’s default solver (Z3) is excellent at handling linear integer arithmetic. Linear arithmetic captures equalities, inequalities, addition, subtraction, and multiplication and division by constants. This means it’s great at handling expressions like 4 * x + 3 * y - z <= 20. However, it is less capable when nonlinear expressions are involved, like x * y (when neither x nor y can be substituted for a constant) or x / y (when y cannot be substituted for a constant).

That means many common axioms are inaccessible in the default mode, including but not limited to:

  • x * y == y * x
  • x * (y * z) == (x * y) * z
  • x * (a + b) == x * a + x * b
  • 0 <= x <= y && 0 <= z <= w ==> x * z <= y * w

The reason for this limitation is that Verus intentionally disables theories of nonlinear arithmetic in its default prover mode.

However, it is possible to opt-in to nonlinear reasoning by invoking a specialized prover mode. There are two prover modes related to nonlinear arithmetic.

  • nonlinear_arith - Enable Z3’s nonlinear theory of arithmetic.
  • integer_ring - Enable a decidable, equational theory of rings.

The first is general purpose, but unfortunately somewhat unpredicable. (This is why it is turned off by default.) The second implements a decidable procedure for a specific class of problems. Invoking either prover mode requires an understanding of how to minimize prover context. We describe each of these modes in more detail below.

If neither mode works for your proof, you can also manually invoke a lemma from Verus’s arithmetic library, which supplies a large collection of verified facts about how nonlinear operations behave. For example, the inaccessible properties listed above can be proven by invoking

  • lemma_mul_is_commutative
  • lemma_mul_is_associative
  • lemma_mul_is_distributive_add
  • lemma_mul_upper_bound

respectively. If your proof involves using multiple such lemmas, you may want to use a structured proof to make the proof more readable and easier to maintain.

1. Invoking a specialized solver: nonlinear_arith

A specialized solver is invoked with the by keyword, which can be applied to either an assert statement or a proof fn.

Here, we’ll see how it works using the nonlinear_arith solver, which enables Z3’s theory of nonlinear arithmetic for integers.

Inline Proofs with assert(...) by(nonlinear_arith)

To prove a nonlinear property in the midst of a larger function, you can write assert(...) by(nonlinear_arith). This creates a separate Z3 query just to prove the asserted property, and for this query, Z3 runs with its nonlinear heuristics enabled. The query does NOT include ambient facts (e.g., knowledge that stems from the surrounding function’s requires clause or from preceding variable assignments) other than that which is:

  • inferred from a variable’s type (e.g., the allowed ranges of a u64 or nat), or
  • supplied explicitly.

To supply context explicitly, you can use a requires clause, a shown below:

proof fn bound_check(x: u32, y: u32, z: u32)
    requires
        x <= 8,
        y <= 8,
{
    assert(x * y <= 100) by (nonlinear_arith)
        requires
            x <= 10,
            y <= 10;

    assert(x * y <= 1000);
}

Let’s go through this example, one step at a time:

  • Verus uses its normal solver to prove that assert’s “requires” clause, that x <= 10 && y <= 10. This follows from the precondition of the function.
  • Verus uses Z3’s nonlinear solver to prove x <= 10 && y <= 10 ==> x * y <= 100. This would not be possible with the normal solver, but it is possible for the nonlinear solver.
  • The fact x * y <= 100 is now provided in the proof context for later asserts.
  • Verus uses its normal solver to prove that x * y <= 1000, which follows from x * y <= 100.

Furthermore, if you use a by clause, as in assert ... by(nonlinear_arith) by { ... }, then everything in the by clause will opt-in to the nonlinear solver.

Reusable proofs with proof fn ... by(nonlinear_arith)

You can also use by(nonlinear_arith) in a proof function’s signature. By including by(nonlinear_arith), the query for this function runs with nonlinear arithmetic reasoning enabled. For example:

proof fn bound_check2(x: u32, y: u32, z: u32) by (nonlinear_arith)
    requires
        x <= 8,
        y <= 8,
    ensures
        x * y <= 64
{ }

When a specialized solver is invoked on a proof fn like this, it is used to prove the lemma. When the lemma is then invoked from elsewhere, Verus (as usual) proves that the precondition is met; for this it uses its normal solver.

2. Proving Ring-based Properties: integer_ring

While general nonlinear formulas cannot be solved consistently, certain sub-classes of nonlinear formulas can be. For example, nonlinear formulas that consist of a series of congruence relations (i.e., equalities modulo some divisor n). As a simple example, we might like to show that a % n == b % n ==> (a * c) % n == (b * c) % n.

Verus offers a deterministic proof strategy to discharge such obligations. The strategy is called integer_ring.

[Note: at present, it is only possible to invoke integer_ring using the proof fn ... by(integer_ring) style; inline asserts are not supported.]

Verus will then discharge the proof obligation using a dedicated algebra solver called Singular. As hinted at by the annotation, this proof technique is only complete (i.e., guaranteed to succeed) for properties that are true for all rings. Formulas that rely specifically on properties of the integers may not be solved successfully.

Using this proof technique requires a bit of additional configuration of your Verus installation. See installing and setting up Singular.

Details/Limitations

  • This can be used only with int parameters.
  • Formulas that involve inequalities are not supported.
  • Division is not supported.
  • Function calls in the formulas are treated as uninterpreted functions. If a function definition is important for the proof, you should unfold the definition of the function in the proof function’s requires clause.
  • When using an integer_ring lemma, the divisor of a modulus operator (%) must not be zero. If a divisor can be zero in the ensures clause of the integer_ring lemma, the facts in the ensures clause will not be available in the callsite.

To understand what integer_ring can or cannot do, it is important to understand how it handles the modulus operator, %. Since integer_ring does not understand inequalities, it cannot perform reasoning that requires that 0 <= (a % b) < b. As a result, Singular’s results might be confusing if you think of % primarily as the operator you’re familiar with from programming.

For example, suppose you use a % b == x as a precondition. Encoded in Singular, this will become a % b == x % b, or in more traditional “mathematical” language, a ≡ x (mod b). This does not imply that x is in the range [0, b), it only implies that a and x are in the same equivalence class mod b. In other words, a % b == x implies a ≡ x (mod b), but not vice versa.

For the same reason, you cannot ask the integer_ring solver to prove a postcondition of the form a % b == x, unless x is 0. The integer_ring solver can prove that a ≡ x (mod b), equivalently (a - x) % b == 0, but this does not imply that a % b == x.

Let’s look at a specific example to understand the limitation.

proof fn foo(a: int, b: int, c: int, d: int, x: int, y: int) by(integer_ring)
    requires
        a == c,
        b == d,
        a % b == x,
        c % d == y
    ensures
        x == y,
{
}

This theorem statement appears to be trivial, and indeed, Verus would solve it easily using its default proof strategy. However, integer_ring will not solve it. We can inspect the Singular query to understand why: (See here for how to log these.)

ring ring_R=integer, (a, b, c, d, x, y, tmp_0, tmp_1, tmp_2), dp;
    ideal ideal_I =
      a - c,
      b - d,
      (a - (b * tmp_0)) - x,
      (c - (d * tmp_1)) - y;
    ideal ideal_G = groebner(ideal_I);
    reduce(x - y, ideal_G);
    quit;

We can see here that a % b is translated to a - b * tmp_0, while c % d is translated to c - d * tmp_1. Again, since there is no constraint that a - b * tmp_0 or c - d * tmp_1 is bounded, it is not possible to conclude that a - b * tmp_0 == c - d * tmp_1 after this simplification has taken place.

3. Combining integer_ring and nonlinear_arith.

As explained above, the integer_ring feature has several limitations, it is not possible to get an arbitary nonlinear property only with the integer_ring feature. Instead, it is a common pattern to have a by(nonlinear_arith) function as a main lemma for the desired property, and use integer_ring lemma as a helper lemma.

To work around the lack of support for inequalities and division, you can often write a helper proof discharged with integer_ring and use it to prove properties that are not directly supported by integer_ring. Furthermore, you can also add additional variables to the formulas. For example, to work around division, one can introduce c where b = a * c, instead of b/a.

Example 1: integer_ring as a helper lemma to provide facts on modular arithmetic

In the lemma_mod_difference_equal function below, we have four inequalities inside the requires clauses, which cannot be encoded into integer_ring. In the ensures clause, we want to prove y % d - x % d == y - x. The helper lemma lemma_mod_difference_equal_helper simply provides that y % d - x % d is equal to (y - x) modulo d. The rest of the proof is done by by(nonlinear_arith).

pub proof fn lemma_mod_difference_equal_helper(x: int, y:int, d:int, small_x:int, small_y:int, tmp1:int, tmp2:int) by(integer_ring)
    requires
        small_x == x % d,
        small_y == y % d,
        tmp1 == (small_y - small_x) % d,
        tmp2 == (y - x) % d,
    ensures
        (tmp1 - tmp2) % d == 0
{}
pub proof fn lemma_mod_difference_equal(x: int, y: int, d: int) by(nonlinear_arith)
    requires
        d > 0,
        x <= y,
        x % d <= y % d,
        y - x < d
    ensures
        y % d - x % d == y - x
{
    let small_x = x % d;
    let small_y = y % d;
    let tmp1 = (small_y - small_x) % d;
    let tmp2 = (y - x) % d;
    lemma_mod_difference_equal_helper(x,y,d, small_x, small_y, tmp1, tmp2);
}

In the lemma_mod_between function below, we want to prove that x % d <= z % d < y % d. However, integer_ring only supports equalities, so we cannot prove lemma_mod_between directly. Instead, we provide facts that can help assist the proof. The helper lemma provides 1) x % d - y % d == x - y (mod d) and 2) y % d - z % d == y - z (mod d). The rest of the proof is done via by(nonlinear_arith).

pub proof fn lemma_mod_between_helper(x: int, y: int, d: int, small_x:int, small_y:int, tmp1:int) by(integer_ring)
    requires
        small_x == x % d,
        small_y == y % d,
        tmp1 == (small_x - small_y) % d,
    ensures
        (tmp1 - (x-y)) % d == 0
{}

// note that below two facts are from the helper function, and the rest are done by `by(nonlinear_arith)`.
// x % d - y % d == x - y  (mod d)
// y % d - z % d == y - z  (mod d)
pub proof fn lemma_mod_between(d: int, x: int, y: int, z: int) by(nonlinear_arith)
    requires
        d > 0,
        x % d < y % d,
        y - x <= d,
        x <= z < y
    ensures
        x % d <= z % d < y % d
{
    let small_x = x % d;
    let small_y = y % d;
    let small_z = z % d;
    let tmp1 = (small_x - small_z) % d;
    lemma_mod_between_helper(x,z,d, small_x, small_z, tmp1);

    let tmp2 = (small_z - small_y) % d;
    lemma_mod_between_helper(z,y,d, small_z, small_y, tmp2);    
}

Example 2: Proving properties on bounded integers with the help of integer_ring

Since integer_ring proofs only support int, you need to include explicit bounds when you want to prove properties about bounded integers. For example, as shown below, in order to use the proof lemma_mod_after_mul on u32s, lemma_mod_after_mul_u32 must ensure that all arguments are within the proper bounds before passing them to lemma_mod_after_mul.

If a necessary bound (e.g., m > 0) is not included, Verus will fail to verify the proof.

proof fn lemma_mod_after_mul(x: int, y: int, z: int, m: int) by (integer_ring)
    requires (x-y) % m == 0
    ensures (x*z - y*z) % m == 0
{}

proof fn lemma_mod_after_mul_u32(x: u32, y: u32 , z: u32, m: u32)   
    requires
        m > 0,
        (x-y) % (m as int) == 0,
        x >= y,
        x <= 0xffff,
        y <= 0xffff,
        z <= 0xffff,
        m <= 0xffff,
    ensures (x*z - y*z) % (m as int) == 0
{ 
  lemma_mod_after_mul(x as int, y as int, z as int, m as int);
  // rest of proof body omitted for space
}

The desired property for nat can be proved similarly.

The next example is similar, but note that we introduce several additional variables(ab, bc, and abc) to help with the integer_ring proof.

pub proof fn multiple_offsed_mod_gt_0_helper(a: int, b: int, c: int, ac: int, bc: int, abc: int) by (integer_ring)
    requires
        ac == a % c,
        bc == b % c,
        abc == (a - b) % c,
    ensures (ac - bc - abc) % c == 0
{}

pub proof fn multiple_offsed_mod_gt_0(a: nat, b: nat, c: nat) by (nonlinear_arith) 
    requires
        a > b,
        c > 0,
        b % c == 0,
        a % c > 0,
    ensures (a - b) % (c as int) > 0
{
    multiple_offsed_mod_gt_0_helper(
      a as int, 
      b as int, 
      c as int, 
      (a % c) as int, 
      (b % c) as int, 
      ((a - b) % (c as int)) as int
    );
}

More integer_ring examples can be found in this folder, and this testcase file.

Examining the encoding

Singular queries will be logged to the directory specified with --log-dir (which defaults to .verus-log) in a the .air file for the module containing the file.

Bit vectors and bitwise operations

In its default prover mode, Verus treats bitwise operations like &, |, ^, << and >> as uninterpreted functions. Even basic facts like x & y == y & x are not exported by Verus’s default solver mode.

To handle these situations, Verus provides the specialized solver mode bit_vector. This solver is great for properties about bitwise operators, and it can also handle some bounded integer arithmetic, though for this, its efficacy varies.

Invoking the bit_vector prover mode.

The bit_vector prover mode can be invoked similarly to nonlinear_arith, with by(bit_vector) either on an assert or a proof fn.

For example, we can shorts and context-free bit-manipulation properties:

fn test_passes(b: u32) {
    assert(b & 7 == b % 8) by (bit_vector);
    assert(b & 0xff < 0x100) by (bit_vector);
}

Again, as with nonlinear_arith, assertions that use 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).

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
}

But context can be imported explicitly with a requires clause:

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
}

And by(bit_vector) is also supported on proof functions:

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,
{
}

Again, this will use the bit_vector solver to prove the lemma, but all calls to the lemma will use the normal solver to prove the precondition.

How the bit_vector solver works and what it’s good at

The bitvector solver uses a different SMT encoding, though one where all arithmetic operations have the same semantic meaning. Specifically, it encodes all integers into the Z3 bv type and encodes arithmetic via the built-in bit-vector operations. Internally, the SMT solver uses a technique called “bit blasting”.

In order to implement this encoding, Verus needs to choose an appropriate bit width to represent any given integer. For symbolic, fixed-width integer values (e.g., u64) it can just choose the appropriate bitwidth (e.g., 64 bits). For the results of arithmetic operations, Verus chooses an appropriate bitwidth automatically. However, for this reason, the bitvector solver cannot reason over symbolic integer values.

The bitvector solver is ideal for proofs about bitwise operations (&, |, ^, << and >>). However, it is also decent at arithmetic (+, -, *, /, %) over bounded integers.

Examples and tips

Functions vs macros

The bit-vector solver doesn’t allow arbitrary functions. However, you can use macros. This is useful when certain operations need a common shorthand, like “get the ith bit of an integer”.

macro_rules! get_bit_macro {
    ($a:expr, $b:expr) => {{
        (0x1u32 & ($a >> $b)) == 1
    }};
}

macro_rules! get_bit {
    ($($a:tt)*) => {
        verus_proof_macro_exprs!(get_bit_macro!($($a)*))
    }
}

Overflow checking

Though the bit_vector solver does not handle symbolic int values, it does support many arithmetic operations that return int values. This makes it possible to write conditions about overflow:

proof fn test_overflow_check(a: u8, b: u8) {
    // `a` and `b` are both `u8` integers, but we can test if their addition
    // overflows a `u8` by simply writing `a + b < 256`.
    assert((a & b) == 0 ==> (a | b) == (a + b) && (a + b) < 256) by(bit_vector);
}

Integer wrapping and truncation

The bit_vector solver is one of the easiest ways to reason about truncation, which can be naturally expressed through bit operations.

proof fn test_truncation(a: u64) {
    assert(a as u32 == a & 0xffff_ffff) by(bit_vector);

    // You can write an identity with modulus as well:
    assert(a as u32 == a % 0x1_0000_0000) by(bit_vector);
}

You may also find it convenient to use add, sub, and mul, which (unlike +, -, and *) automatically truncate.

proof fn test_truncating_add(a: u64, b: u64) {
    assert(add(a, b) == (a + b) as u64) by(bit_vector);
}

Working with usize and isize

If you use variables of type usize or isize, the bitvector solver (by default) assumes they might be either 32-bit or 64-bit, which affects the encoding. In that case, the solver will generate 2 different queries and verifies both.

However, the solver can also be configured to assume a particular platform size.

Bit-width dependence and independence

For many operations, their results are independent of the input bit-widths. This is true of &, |, ^, and >>. In fact, we don’t even need the bit-vector to prove this; the normal solver mode is “aware” of this fact as well.

proof fn test_xor_u32_vs_u64(x: u32, y: u32) {
    assert((x as u64) ^ (y as u64) == (x ^ y) as u64) by(bit_vector);

    // XOR operation is independent of bitwidth so we don't even
    // need the `bit_vector` solver to do this:
    assert((x as u64) ^ (y as u64) == (x ^ y) as u64);
}

However, this is not true of left shift, <<. With left shift, you always need to be careful of the bitwidth of the left operand.

proof fn test_left_shift_u32_vs_u64(y: u32) {
    assert(1u32 << y == 1u64 << y); // FAILS (in either mode) because it's not true
}

More examples

Some larger examples to browse:

Equality via extensionality

In the specification libraries section, we introduced the extensional equality operator =~= to check equivalence for Seq, Set, and Map.

Suppose that a struct or enum datatype has a field containing Seq, Set, and Map, and suppose that we’d like to prove that two values of the datatype are equal. We could do this by using =~= on each field individually:

    struct Foo {
        a: Seq<int>,
        b: Set<int>,
    }

    proof fn ext_equal_struct() {
        let f1 = Foo { a: seq![1, 2, 3], b: set!{4, 5, 6} };
        let f2 = Foo { a: seq![1, 2].push(3), b: set!{5, 6}.insert(4) };
        // assert(f1 == f2);    // FAILS -- need to use =~= first
        assert(f1.a =~= f2.a);  // succeeds
        assert(f1.b =~= f2.b);  // succeeds
        assert(f1 == f2);  // succeeds, now that we've used =~= on .a and .b
    }

However, it’s rather painful to use =~= on each field every time to check for equivalence. To help with this, Verus supports the #[verifier::ext_equal] attribute to mark datatypes that need extensionality on Seq, Set, Map, Multiset, spec_fn fields or fields of other #[verifier::ext_equal] datatypes. For example:

#[verifier::ext_equal]  // necessary for invoking =~= on the struct
struct Foo {
    a: Seq<int>,
    b: Set<int>,
}

proof fn ext_equal_struct() {
    let f1 = Foo { a: seq![1, 2, 3], b: set!{4, 5, 6} };
    let f2 = Foo { a: seq![1, 2].push(3), b: set!{5, 6}.insert(4) };
    assert(f1.a =~= f2.a);  // succeeds
    // assert(f1 == f2);    // FAILS
    assert(f1 =~= f2);  // succeeds
}

(Note: adding #[verifier::ext_equal] does not change the meaning of ==; it just makes it more convenient to use =~= to prove == on datatypes.)

Collection datatypes like sequences and sets can contain other collection datatypes as elements (for example, a sequence of sequences, or set of sequences). The =~= operator only applies extensionality to the top-level collection, not to the nested elements of the collection. To also apply extensionality to the elements, Verus provides a “deep” extensional equality operator =~~= that handles arbitrary nesting of collections, spec_fn, and datatypes. For example:

proof fn ext_equal_nested() {
    let inner: Set<int> = set!{1, 2, 3};
    let s1: Seq<Set<int>> = seq![inner];
    let s2 = s1.update(0, s1[0].insert(1));
    let s3 = s1.update(0, s1[0].insert(2).insert(3));
    // assert(s2 == s3);  // FAILS
    // assert(s2 =~= s3); // FAILS
    assert(s2 =~~= s3);  // succeeds
    let s4: Seq<Seq<Set<int>>> = seq![s1];
    let s5: Seq<Seq<Set<int>>> = seq![s2];
    assert(s4 =~~= s5);  // succeeds
}

The same applies to spec_fn, as in:

#[verifier::ext_equal]  // necessary for invoking =~= on the struct
struct Bar {
    a: spec_fn(int) -> int,
}

proof fn ext_equal_fnspec(n: int) {
    // basic case
    let f1 = (|i: int| i + 1);
    let f2 = (|i: int| 1 + i);
    // assert(f1 == f2); // FAILS
    assert(f1 =~= f2);  // succeeds
    // struct case
    let b1 = Bar { a: |i: int| if i == 1 { i } else { 1 } };
    let b2 = Bar { a: |i: int| 1int };
    // assert(b1 == b2); // FAILS
    assert(b1 =~= b2);  // succeeds
    // nested case
    let i1 = (|i: int| i + 2);
    let i2 = (|i: int| 2 + i);
    let n1: Seq<spec_fn(int) -> int> = seq![i1];
    let n2: Seq<spec_fn(int) -> int> = seq![i2];
    // assert(n1 =~= n2); // FAILS
    assert(n1 =~~= n2);  // succeeds
}

Managing proof performance and why it’s critical

Sometimes your proof succeeds, but it takes too long. It’s tempting to simply tolerate the longer verification time and move on. However, we urge you to take the time to improve the verification performance. Slow verification performance typically has an underlying cause. Diagnosing and fixing the cause is much easier to do as the problems arise; waiting until you have multiple performance problems compounds the challenges of diagnosis and repair. Plus, if the proof later breaks, you’ll appreciate having a short code-prove development cycle. Keeping verification times short also makes it easier to check for regressions.

This chapter describes various ways to measure the performance of your proofs and steps you can take to improve it.

Meausuring verification performance

To see a more detailed breakdown of where Verus is spending time, you can pass --time on the command line. For even more details, try --time-expanded. For a machine-readable output, add --output-json. These flags will also report on the SMT resources (rlimit) used. SMT resources are an advanced topic; they give a very rough estimate of how hard the SMT solver worked on the provided query (or queries).

See verus --help for more information about these options.

Quantifier Profiling

Sometimes the verification of a Verus function will time out, meaning that the solver couldn’t determine whether all of the proof obligations have been satisfied. Or verification might succeed but take longer than we would like. One common cause for both of these phenomena is quantifiers. If quantifiers (and their associated triggers) are written too liberally (i.e., they trigger too often), then the SMT solver may generate too many facts to sort through efficiently. To determine if this is the case for your Verus code, you can use the built-in quantifier profiler.

As a concrete example, suppose we have the following three functions defined:

spec fn f(x: nat, y: nat) -> bool;

spec fn g(x: nat) -> bool;

spec fn h(x: nat, y: nat) -> bool;

and we use them in the following proof code:

proof fn trigger_forever2()
    requires
        forall|x: nat| g(x),
        forall|x: nat, y: nat| h(x, y) == f(x, y),
        forall|x: nat, y: nat| f(x + 1, 2 * y) && f(2 * x, y + x) || f(y, x) ==> #[trigger] f(x, y),
    ensures
        forall|x: nat, y: nat| x > 2318 && y < 100 ==> h(x, y),
{
    assert(g(4));
}

Notice that we have three quantifiers in the requires clause; the first will trigger on g(x), which will be useful for proving the assertion about g(4). The second quantifier triggers on both f(x, y) and h(x, y) and says that they’re equal. The last quantifier is manually triggered on f(x, y), but it then introduces two more expressions that have a similar shape, namely f(x + 1, 2 * y) and f(2 * x, y + x). Each of these has new arguments to f, so this will cause quantifier 3 to trigger again, creating an infinite cycle of instantations. Notice that each such instantiation will also cause quantifier 2 to trigger as well.

If we run Verus on this example, it will quickly time out. When this happens, you can run Verus with the --profile option to launch the profiler. We strongly recommend combining that option with --rlimit 1, so that you don’t generate too much profiling data (the more you generate, the longer the analysis takes). With --profile, if verification times out, the profiler automatically launches. If you want to profile a function that is verifying successfully but slowly, you can use the --profile-all option. You may want to combine this with the --verify-function option to target the function you’re interested in.

If we run the profiler on the example above, we’ll see something along the lines of:

error: function body check: Resource limit (rlimit) exceeded
  --> rust_verify/example/trigger_loops.rs:64:1
   |
64 | fn trigger_forever2() {
   | ^^^^^^^^^^^^^^^^^^^^^

Analyzing prover log...
[00:00:39] ████████████████████████████████████████████████████████████████████████████████ 1153/1153K lines
... analysis complete

note: Observed 27,184 total instantiations of user-level quantifiers

note: Cost * Instantiations: 5391549700 (Instantiated 13,591 times - 49% of the total, cost 396700) top 1 of 3 user-level quantifiers.
  --> rust_verify/example/trigger_loops.rs:68:78
   |
68 |    forall|x: nat, y: nat| f(x + 1, 2 * y) && f(2 * x, y + x) || f(y, x) ==> #[trigger] f(x, y),
   |    -------------------------------------------------------------------------^^^^^^^^^^^^^^^^^^ Triggers selected for this quantifier

note: Cost * Instantiations: 1037237938 (Instantiated 13,591 times - 49% of the total, cost 76318) top 2 of 3 user-level quantifiers.
  --> rust_verify/example/trigger_loops.rs:67:28
   |
67 |    forall|x: nat, y: nat| h(x, y) == f(x, y),
   |    -----------------------^^^^^^^----^^^^^^^ Triggers selected for this quantifier

note: Cost * Instantiations: 16 (Instantiated 2 times - 0% of the total, cost 8) top 3 of 3 user-level quantifiers.
  --> rust_verify/example/trigger_loops.rs:66:20
   |
66 |    forall|x: nat| g(x),
   |    ---------------^^^^ Triggers selected for this quantifier

error: aborting due to previous error

The profiler measures two aspects of quantifier performance. First, it collects a basic count of how many times each quantifier is instantiated. Second, it attempts to calculate a “cost” for each quantifier. The cost of a quantifier is the sum of cost of its instantiations. The cost of an instantiation i is roughly 1 + sum_{(i, n) \in edges} cost(n) / in-degree(n) where each n is an instantiation caused by instantiation i. In other words, instantiation i produced a term that caused the solver to create another instantiation (of the same or a different quantifier) n. This heuristic attempts to place more weight on quantifiers whose instantiations themselves cause other expensive instantiations. By default, the profiler will sort by the product of these two metrics.

In the example above, we see that the top quantifier is quantifer 3 in the Verus code, which is indeed the troublemaker. The use of the cost metric elevates it above quantifier 2, which had the same number of instantiations but is really an “innocent bystander” in this scenario. And both of these quantifiers are instantiated vastly more than quantifier 3, indicating that quantifier 3 is not the source of the problem. If all of the quantifiers have a small number of instantiations, that may be a sign that quantifier instantiation is not the underlying source of the solver’s poor performance.

Hiding local proofs with assert(...) by { ... }

Motivation

Sometimes, in a long function, you need to establish a fact F that requires a modest-size proof P. Typically, you do this by ...; P; assert(F); .... But doing this comes with a risk: the facts P introduces can be used not only for proving F but also for proving the entire rest of the function. This gives the SMT solver much more to think about when proving things beyond assert(F), which is especially problematic when these additional facts are universally quantified. This can make the solver take longer, and even time out, on the rest of the function.

Enter assert(...) by { ... }

Saying assert(F) by {P} restricts the context that P affects, so that it’s used to establish F and nothing else. After the closing brace at the end of { P }, all facts that it established except for F are removed from the proof context.

Underlying mechanism

The way this works internally is as follows. The solver is given the facts following from P as a premise when proving F but isn’t given them for the rest of the proof. For instance, suppose lemma_A establishes fact A and lemma_B establishes fact B. Then

lemma_A();
assert(F) by { lemma_B(); };
assert(G);

is encoded to the solver as something like (A && B ==> F) && (A ==> G). If B is an expansive fact to think about, like forall|i: int| b(i), the solver won’t be able to think about it when trying to establish G.

Difference from auxiliary lemmas

Another way to isolate the proof of F from the local context is to put the proof P in a separate lemma and invoke that lemma. To do this, the proof writer has to think about what parts of the context (like fact A in the example above) are necessary to establish F, and put those as requires clauses in the lemma. The developer may then also need to pass other variables to the lemma that are mentioned in those required facts. This can be done, but can be a lot of work. Using assert(F) by { P } obviates all this work. It also makes the proof more compact by removing the need to have a separate lemma with its own signature.

Structured Proofs by Calculation

Motivation

Sometimes, you need to establish some relation R between two expressions, say, a_1 and a_n, where it might be easier to do this in a series of steps, a_1 to a_2, a_2 to a_3, … all the way to a_n. One might do this by just doing all the steps at once, but as mentioned in the section on assert-by, a better approach might be to split it into a collection of restricted contexts. This is better, but still might not be ideal, since you need to repeat each of the intermediate expressions at each point.

calc!ulations, to Reduce Redundant Redundancy

The calc! macro supports structured proofs through calculations.

In particular, one can show a_1 R a_n for some transitive relation R by performing a series of steps a_1 R a_2, a_2 R a_3, … a_{n-1} R a_n. The calc macro provides both convenient syntax sugar to perform such a proof conveniently, without repeating oneself too often, or exposing the internal steps to the outside context.

The expected usage looks like:

calc! {
  (R)
  a_1; { /* proof that a_1 R a_2 */ }
  a_2; { /* proof that a_2 R a_3 */ }
   ...
  a_n;
}

For example,

    let a: int = 2;
    calc! {
        (<=)
        a; {}
        a + 3; {}
        5;
    }

which is equivalent to proving a <= 5 using a <= b <= 5. In this case, each of the intermediate proofs are trivial, thus have an empty {} block, but in general, can have arbitrary proofs inside their blocks.

Notice that you mention a_1, a_2, … a_n only once each. Additionally, the proof for each of the steps is localized, and restricted to only its particular step, ensuring that proof-context is not polluted.

The body of the function where this calc statement is written only gets to see a_1 R a_n, and not any of the intermediate steps (or their proofs), further limiting proof-context pollution.

Currently, the calc! macro supports common transitive relations for R (such as == and <=). This set of relations may be extended in the future.

Relating Relations to Relations

While a relation like <= might be useful to use like above, it is possible that not every intermediate step needs a <=; sometimes one might be able to be more precise, and maintaining this (especially for documentation/readability reasons) might be useful. For example, one might want to say a_1 <= a_2 == a_3 <= a_4 < a_5 <= ....

This is supported by calc by specifying the extra intermediate relations inline (with the default being the high-level relation). These relations are checked to be consistent with the top-level relation, in order to maintain transitivity (so for example, using > in the above chain would be caught and reported with a helpful message).

A simple example of using intermediate relations looks like the following:

    let x: int = 2;
    let y: int = 5;
    calc! {
        (<=)
        x; (==) {}
        5 - 3; (<) {}
        5int; {}  // Notice that no intermediate relation
                  // is specified here, so `calc!` will
                  // consider the top-level relation
                  // `R`; here `<=`.
        y;
    }

This example is equivalent to saying x <= y using x == 5 - 3 < 5 <= y.

Proofs by Computation

Motivation

Some proofs should be “obvious” by simply computing on values. For example, given a function pow(base, exp) defining exponentiation, we would like it to be straightforward and deterministic to prove that pow(2, 8) == 256. However, in general, to keep recursive functions like pow from overwhelming the SMT solver with too many unrollings, Verus defaults to only unrolling such definitions once. Hence, to make the assertion above go through, the developer needs to carefully adjust the amount of “fuel” provided to unroll pow. Even with such adjustment, we have observed cases where Z3 does “the wrong thing”, e.g., it does not unroll the definitions enough, or it refuses to simplify non-linear operations on statically known constants. As a result, seemingly simple proofs like the one above don’t always go through as expected.

Enter Proof by Computation

Verus allows the developer to perform such proofs via computation, i.e., by running an internal interpreter over the asserted fact. The developer can specify the desired computation using assert(e) by (compute) (for some expression e). Continuing the example above, the developer could write:

// Naive definition of exponentiation
spec fn pow(base: nat, exp: nat) -> nat
    decreases exp,
{
    if exp == 0 {
        1
    } else {
        base * pow(base, (exp - 1) as nat)
    }
}

proof fn concrete_pow() {
    assert(pow(2, 8) == 256) by (compute);  // Assertion 1
    assert(pow(2, 9) == 512);  // Assertion 2
    assert(pow(2, 8) == 256) by (compute_only);  // Assertion 3
}

In Assertion 1, Verus will internally reduce the left-hand side to 256 by repeatedly evaluating pow and then simplify the entire expression to true.

When encoded to the SMT solver, the result will be (approximately):

assert(true);
assume(pow(2, 8) == 256);

In other words, in the encoding, we assert whatever remains after simplification and then assume the original expression. Hence, even if simplification only partially succeeds, Z3 may still be able to complete the proof. Furthermore, because we assume the original expression, it is still available to trigger other ambient knowledge or contribute to subsequent facts. Hence Assertion 2 will succeed, since Z3 will unfold the definition of pow once and then use the previously established fact that pow(2,8) == 256.

If you want to ensure that the entire proof completes through computation and leaves no additional work for Z3, then you can use assert(e) by (compute_only) as shown in Assertion 3. Such an assertion will fail unless the interpreter succeeds in reducing the expression completely down to true. This can be useful for ensuring the stability of your proof, since it does not rely on any Z3 heuristics.

Important note: An assertion using proof by computation does not inherit any context from its environment. Hence, this example:

let x = 2;
assert(pow(2, x) == 4) by (compute_only);

will fail, since x will be treated symbolically, and hence the assertion will not simplify all the way down to true. This can be remedied either by using assert(e) by (compute) and allowing Z3 to finish the proof, or by moving the let into the assertion, e.g., as:

proof fn let_passes() {
    assert({
        let x = 2;
        pow(2, x) == 4
    }) by (compute_only);
}

While proofs by computation are most useful for concrete values, the interpreter also supports symbolic values, and hence it can complete certain proofs symbolically. For example, given variables a, b, c, d, the following succeeds:

proof fn seq_example(a: Seq<int>, b: Seq<int>, c: Seq<int>, d: Seq<int>) {
    assert(seq![a, b, c, d] =~= seq![a, b].add(seq![c, d])) by (compute_only);
}

Many proofs by computation take place over a concrete range of integers. To reduce the boilerplate needed for such proofs, you can use all_spec. In the example below,

use vstd::compute::RangeAll;

spec fn p(u: usize) -> bool {
    u >> 8 == 0
}

proof fn range_property(u: usize)
    requires 25 <= u < 100,
    ensures p(u),
{
    assert((25..100int).all_spec(|x| p(x as usize))) by (compute_only);
    let prop = |x| p(x as usize);
    assert(prop(u));
}

we use all_spec to prove that p holds for all values between 25 and 100, and hence it must hold for a generic value u that we know is in that range. Note that all_spec currently expects to operate over ints, so you may need add casts as appropriate. Also, due to some techinical restrictions, at present, you can’t pass a top-level function like p to all_spec. Instead, you need to wrap it in a closure, as seen in this example. Finally, the lemmas in vstd will give you a quantified resulted about the outcome of all_spec, so you may need to add an additional assertion (in our example, assert(prop(u))) to trigger that quantifier. This guide provides more detail on quantifiers and triggers in another chapter.

To prevent infinite interpretation loops (which can arise even when the code is proven to terminate, since the termination proof only applies to concrete inputs, whereas the interpreter may encounter symbolic values), Verus limits the time it will spend interpreting any given proof by computation. Specifically, the time limit is the number of seconds specified via the --rlimit command-line option.

By default, the interpreter does not cache function call results based on the value of the arguments passed to the function. Experiments showed this typically hurts performance, since it entails traversing the (large) AST nodes representing the arguments. However, some examples need such caching to succceed (e.g., computing with the naive definition of Fibonacci). Such functions can be annotated with #[verifier::memoize], which will cause their results to be cached during computation.

Current Limitations

  1. As mentioned above, the expression given to a proof by computation is interpreted in isolation from any surrounding context.
  2. The expression passed to a proof-by-computation assertion must be in spec mode, which means it cannot be used on proof or exec mode functions.
  3. The interpreter is recursive, so a deeply nested expression (or series of function calls) may cause Verus to exceed the process’ stack space.

See Also

  1. The test suite has a variety of small examples.
  2. We also have several more complex examples.

Breaking proofs into smaller pieces

Motivation

If you write a long function with a lot of proof code, Verus will correspondingly give the SMT solver a long and difficult problem to solve. So one can improve solver performance by breaking that function down into smaller pieces. This performance improvement can be dramatic because solver response time typically increases nonlinearly as proof size increases. After all, having twice as many facts in scope gives the solver far more than twice as many possible paths to search for a proof. As a consequence, breaking functions down can even make the difference between the solver timing out and the solver succeeding quickly.

Moving a subproof to a lemma

If you have a long function, look for a modest-size piece P of it that functions as a proof of some locally useful set of facts S. Replace P with a call to a lemma whose postconditions are S, then make P the body of that lemma. Consider what parts of the original context of P are necessary to establish S, and put those as requires clauses in the lemma. Those requires clauses may involve local variables, in which case pass those variables to the lemma as parameters.

For instance:

fn my_long_function(x: u64, ...)
{
    let y: int = ...;
    ... // first part of proof, establishing fact f(x, y)
    P1; // modest-size proof...
    P2; //   establishing...
    P3; //   facts s1 and s2...
    P4; //   about x and y
    ... // second part of proof, using facts s1 and s2
}

might become

proof fn my_long_function_helper(x: u64, y: int)
    requires
        f(x, y)
    ensures
        s1(x),
        s2(x, y)
{
    P1; // modest-size proof...
    P2; //   establishing...
    P3; //   facts s1 and s2...
    P4; //   about x and y
}

fn my_long_function(x: u64, ...)
{
    ... // first part of proof, establishing fact f(x, y)
    my_long_function_helper(x, y);
    ... // second part of proof, using facts s1 and s2
}

You may find that, once you’ve moved P into the body of the lemma, you can not only remove P from the long function but also remove significant portions of P from the lemma where it was moved to. This is because a lemma dedicated solely to establishing S will have a smaller context for the solver to reason about. So less proof annotation may be necessary to get it to successfully and quickly establish S. For instance:

proof fn my_long_function_helper(x: u64, y: int)
    requires
        f(x, y)
    ensures
        s1(x),
        s2(x, y)
{
    P1; // It turns out that P2 and P3 aren't necessary when
    P4; //    the solver is focused on just f, s1, s2, x, and y.
}

Dividing a proof into parts 1, 2, …, n

Another approach is to divide your large function’s proof into n consecutive pieces and put each of those pieces into its own lemma. Make the first lemma’s requires clauses be the requires clauses for the function, and make its ensures clauses be a summary of what its proof establishes. Make the second lemma’s requires clauses match the ensures clauses of the first lemma, and make its ensures clauses be a summary of what it establishes. Keep going until lemma number n, whose ensures clauses should be the ensures clauses of the original function. Finally, replace the original function’s proof with a sequence of calls to those n lemmas in order.

For instance:

proof fn my_long_function(x: u64)
    requires r(x)
    ensures  e(x)
{
    P1;
    P2;
    P3;
}

might become

proof fn my_long_function_part1(x: u64) -> (y: int)
    requires
        r(x)
    ensures
        mid1(x, y)
{
    P1;
}

proof fn my_long_function_part2(x: u64, y: int)
    requires
        mid1(x, y)
    ensures
        mid2(x, y)
{
    P2;
}

proof fn my_long_function_part3(x: u64, y: int)
    requires
        mid2(x, y)
    ensures
        e(x)
{
    P3;
}

proof fn my_long_function(x: u64)
    requires r(x)
    ensures  e(x)
{
    let y = my_long_function_part1(x);
	my_long_function_part2(x, y);
	my_long_function_part3(x, y);
}

Since the expressions r(x), mid1(x, y), mid2(x, y), and e(x) are each repeated twice, it may be helpful to factor each out as a spec function and thereby avoid repetition.

Checklist: What to do when proofs go wrong

A proof is failing and I don’t expect it to. What’s going wrong?

  • Try running Verus with --expand-errors to get more specific information about what’s failing.
  • Check Verus’s output for recommends-failures and other notes.
  • Add more assert statements. This can either give you more information about what’s failing, or even just fix the proof. See this guide.
  • Are you using quantifiers? Make sure you understand how triggers work.
  • Are you using nonlinear arithmetic? Try one of the strategies for nonlinear arithmetic.
  • Are you using bitwise arithmetic or as-truncation? Try the bit_vector solver.
  • Are you relying on the equality of a container type (like Seq or Map)? Try extensional equality.
  • Are you using a recursive function? Make sure you understand how fuel works.

The verifier says “rlimit exceeded”. What can I do?

My proof is “flaky”: it sometimes works, but then I change something unrelated, and it breaks.

Higher-order executable functions

Here we discuss the use of higher order functions via closures and other function types in Rust.

Passing functions as values

In Rust, functions may be passed by value using the FnOnce, FnMut, and Fn traits. Just like for normal functions, Verus supports reasoning about the preconditions and postconditions of such functions.

Reasoning about preconditions and postconditions

Verus allows you to reason about the preconditions and postconditions of function values via two builtin spec functions: call_requires and call_ensures.

  • call_requires(f, args) represents the precondition. It takes two arguments: the function object and arguments as a tuple. If it returns true, then it is possible to call f with the given args.
  • call_ensures(f, args, output) represents the postcondition. It takes takes three arguments: the function object, arguments, and return vaue. It represents the valid input-output pairs for f.

The vstd library also provides aliases, f.requires(args) and f.ensures(args, output). These mean the same thing as call_requires and call_ensures.

As with any normal call, Verus demands that the precondition be satisfied when you call a function object. This is demonstrated by the following example:

    fn double(x: u8) -> (res: u8)
        requires
            0 <= x < 128,
        ensures
            res == 2 * x,
    {
        2 * x
    }

    fn higher_order_fn(f: impl Fn(u8) -> u8) -> (res: u8) {
        f(50)
    }

    fn test() {
        higher_order_fn(double);
    }

As we can see, test calls higher_order_fn, passing in double. The higher_order_fn then calls the argument with 50. This should be allowed, according to the requires clause of double; however, higher_order_fn does not have the information to know this is correct. Verus gives an error:

error: Call to non-static function fails to satisfy `callee.requires(args)`
  --> vec_map.rs:25:5
   |
25 |     f(50)
   |     ^^^^^

To fix this, we can add a precondition to higher_order_fn that gives information on the precondition of f:

    fn double(x: u8) -> (res: u8)
        requires
            0 <= x < 128,
        ensures
            res == 2 * x,
    {
        2 * x
    }

    fn higher_order_fn(f: impl Fn(u8) -> u8) -> (res: u8)
        requires
            call_requires(f, (50,)),
    {
        f(50)
    }

    fn test() {
        higher_order_fn(double);
    }

The (50,) looks a little funky. This is a 1-tuple. The call_requires and call_ensures always take tuple arguments for the “args”. If f takes 0 arguments, then call_requires takes a unit tuple; if f takes 2 arguments, then it takes a pair; etc. Here, f takes 1 argument, so it takes a 1-tuple, which can be constructed by using the trailing comma, as in (50,).

Verus now accepts this code, as the precondition of higher_order_fn now guarantees that f accepts the input of 50.

We can go further and allow higher_order_fn to reason about the output value of f:

    fn double(x: u8) -> (res: u8)
        requires
            0 <= x < 128,
        ensures
            res == 2 * x,
    {
        2 * x
    }

    fn higher_order_fn(f: impl Fn(u8) -> u8) -> (res: u8)
        requires
            call_requires(f, (50,)),
            forall|x, y| call_ensures(f, x, y) ==> y % 2 == 0,
        ensures
            res % 2 == 0,
    {
        let ret = f(50);
        return ret;
    }

    fn test() {
        higher_order_fn(double);
    }

Observe that the precondition of higher_order_fn places a constraint on the postcondition of f. As a result, higher_order_fn learns information about the return value of f(50). Specifically, it learns that call_ensures(f, (50,), ret) holds, which by higher_order_fn’s precondition, implies that ret % 2 == 0.

An important note

The above examples show the idiomatic way to constrain the preconditions and postconditions of a function argument. Observe that call_requires is used in a positive position, i.e., “call_requires holds for this value”. Meanwhile call_ensures is used in a negative position, i.e., on the left hand side of an implication: “if call_ensures holds for a given value, this is satisfies this particular constraint”.

It is very common to need a guarantee that f(args) will return one specific value, say expected_return_value. In this situation, it can be tempting to write,

requires call_ensures(f, args, expected_return_value),

as your constraint. However, this is almost never what you actually want, and in fact, Verus may not even let you prove it. The proposition call_ensures(f, args, expected_return_value) says that expected_return_value is a possible return value of f(args); however, it says nothing about other possible return values. In general, f may be nondeterministic! Just because expected_return_value is one possible return value does not mean it is only one.

When faced with this situation, what you really want is to write:

requires forall |ret| call_ensures(f, args, ret) ==> ret == expected_return_value

This is the proposition that you really want, i.e., “if f(args) returns a value ret, then that value is equal to expected_return_value”.

Of course, this is flipped around when you write a postcondition, as we’ll see in the next example.

Example: vec_map

Let’s take what we learned and write a simple function, vec_map, which applies a given function to each element of a vector and returns a new vector.

The key challenge is to determine the right specfication to use.

The signature we want is:

fn vec_map<T, U>(v: &Vec<T>, f: impl Fn(T) -> U) -> (result: Vec<U>) where
    T: Copy,

First, what do we need to require? We need to require that it’s okay to call f with any element of the vector as input.

    requires
        forall|i|
            0 <= i < v.len() ==> call_requires(
                f,
                (v[i],),
            ),

Next, what ought we to ensure? Naturally, we want the returned vector to have the same length as the input. Furthermore, we want to guarantee that any element in the output vector is a possible output when the provided function f is called on the corresponding element from the input vector.

    ensures
        result.len() == v.len(),
        forall|i|
            0 <= i < v.len() ==> call_ensures(
                f,
                (v[i],),
                #[trigger] result[i],
            )
        ,

Now that we have a specification, the implementation and loop invariant should fall into place:

fn vec_map<T, U>(v: &Vec<T>, f: impl Fn(T) -> U) -> (result: Vec<U>) where
    T: Copy,

    requires
        forall|i|
            0 <= i < v.len() ==> call_requires(
                f,
                (v[i],),
            ),
    ensures
        result.len() == v.len(),
        forall|i|
            0 <= i < v.len() ==> call_ensures(
                f,
                (v[i],),
                #[trigger] result[i],
            )
        ,
{
    let mut result = Vec::new();
    let mut j = 0;
    while j < v.len()
        invariant
            forall|i| 0 <= i < v.len() ==> call_requires(f, (v[i],)),
            0 <= j <= v.len(),
            j == result.len(),
            forall|i| 0 <= i < j ==> call_ensures(f, (v[i],), #[trigger] result[i]),
    {
        result.push(f(v[j]));
        j += 1;
    }
    result
}

Finally, we can try it out with an example:

fn double(x: u8) -> (res: u8)
    requires
        0 <= x < 128,
    ensures
        res == 2 * x,
{
    2 * x
}

fn test_vec_map() {
    let mut v = Vec::new();
    v.push(0);
    v.push(10);
    v.push(20);
    let w = vec_map(&v, double);
    assert(w[2] == 40);
}

Conclusion

In this chapter, we learned how to write higher-order functions with higher-order specifications, i.e., specifications that constrain the specifications of functions that are passed around as values.

All of the examples from this chapter passed functions by referring to them directly by name, e.g., passing the function double by writing double. In Rust, a more common way to work with higher-order functions is to pass closures. In the next chapter, we’ll learn how to use closures.

Closures

In the previous chapter, we saw how to pass functions as values, which we did by referencing function items by name. However, it is more common in Rust to creating functions using closures.

Preconditions and postconditions on a closure

Verus allows you to specify requires and ensures on a closure just like you can for any other function. Here’s an example, calling the vec_map function we defined in the previous chapter:

fn test_vec_map_with_closure() {
    let double = |x: u8| -> (res: u8)
        requires 0 <= x < 128
        ensures res == 2 * x
    {
        2 * x
    };

    assert(forall |x| 0 <= x < 128 ==> call_requires(double, (x,)));
    assert(forall |x, y| call_ensures(double, (x,), y) ==> y == 2 * x);

    let mut v = Vec::new();
    v.push(0);
    v.push(10);
    v.push(20);
    let w = vec_map(&v, double);
    assert(w[2] == 40);
}

Closure capturing

One of the most challenging aspects of closures, in general, is that closures can capture variables from the surrounding context. Rust resolves this challenge through its hierarcy of function traits: FnOnce, FnMut, and Fn. The declaration of the closure and the details of its context capture determine which traits it has. In turn, the traits determine what capabilities the caller has: Can they call it more than once? Can they call it in parallel?

See the Rust documentation for a more detailed introduction.

In brief, the traits provide the following capabilities to callers and restrictions on the context capture:

Caller capabilityCapturing
FnOnceMay call onceMay move variables from the context
FnMutMay call multiple times via &mut referenceMay borrow mutably from the context
FnMay call multiple times via & referenceMay borrow immutably from the context

Verus does not yet support borrowing mutably from the context, though it does handle moving and immutable borrows easily. Therefore, Verus has better support for Fn and FnOnce—it does not yet take advantage of the capturing capabilities supported by Rust’s FnMut.

Fortunately, both move-captures and immutable-reference-captures are easy to handle, as we can simply take their values inside the closure to be whatever they are at the program point of the closure expression.

Example:

fn example_closure_capture() {
    let x: u8 = 20;

    let f = || {
        // Inside the closure, we have seamless access to
        // variables defined outside the closure.
        assert(x == 20);
        x
    };
}

Unsafe code & complex ownership

Here we discuss the handling of more complex patterns relating to Rust ownership including:

  • Interior mutability, where Rust allows you to mutate data even through a shared reference &T
  • Raw pointers, which require proper ownership handling in order to uphold safety contracts
  • Concurrency, where objects owned across different threads may need to coordinate.

Interior Mutability

The Interior Mutability pattern is a particular Rust pattern wherein the user is able to manipulate the contents of a value accessed via a shared borrow &. (Though & is often referred to as “immutable borrow,” we will call it a “shared borrow” here, to avoid confusion.) Two common Rust types illustrating interior mutability are Cell and RefCell. Here, we will overview the equivalent concepts in Verus.

Mutating stuff that can’t mutate

To understand the key challenge in verifying these interior mutability patterns, recall an important fact of Verus’s SMT encoding. Verus assumes that any value of type &T, for any type T, can never change. However, we also know that the contents of a &Cell<V> might change. After all, that’s the whole point of the Cell<T> type!

The inescapable conclusion, then, is that the value taken by a Cell<T> in Verus’ SMT encoding must not depend on the cell’s contents. Instead, the SMT “value” of a Cell<T> is nothing more than a unique identifier for the Cell. In some regards, it may help to think of Cell<T> as similar to a pointer T*. The value of the Cell<T> is only its identifier (its “pointer address”) rather than its contents (“the thing pointed to be a pointer”). Of course, it’s not a pointer, but from the perspective of the encoding, it might as well be.

Note one immediate ramification of this property: Verus’ pure equality === on Cell types cannot possibly give the same results as Rust’s standard == (eq) on Cell types. Rust’s == function actually compares the contents of the cells. But pure equality, ===, which must depend on the SMT encoding values, cannot possibly depend on the contents! Instead, === compares two cells as equal only if they are the same cell.

So, with these challenges in mind, how do we handle interior mutability in Verus?

There are a few different approaches we can take.

  • When retrieving a value from the interior of a Cell-like data structure, we can model this as non-deterministically receiving a value of the given type. At first, this might seem like it gives us too little to work with for verifying correctness properties. However, we can impose additional structure by specifying data invariants to restrict the space of possible values.

  • Track the exact value using tracked ghost code.

More sophisticated data structures—especially concurrent ones—often require a careful balance of both approaches. We’ll introduce both here.

Data Invariants with InvCell.

Suppose we have an expensive computation and we want to memoize its value. The first time we need to compute the value, we perform the computation and store its value for whenever it’s needed later. To do this, we’ll use a Cell, whose interior is intialized to None to store the computed value. The memoized compute function will then:

  • Read the value in the Cell.
    • If it’s None, then the value hasn’t been computed yet. Compute the value, store it for later, then return it.
    • If it’s Some(x), then the value has already been computed, so return it immediately.

Crucially, the correctness of this approach doesn’t actually depend on being able to predict which of these cases any invocation will take. (It might be a different story if we were attempting to prove a bound on the time the program will take.) All we need to know is that it will take one of these cases. Therefore, we can verify this code by using a cell with a data invariant:

  • Invariant: the value stored in the interior of the cell is either None or Some(x), where x is the expected result of the computation.

Concretely, the above can be implemented in Verus using InvCell, provided by Verus’ standard library, which provides a data-invariant-based specification. When constructing a new InvCell<T>, the user specifies a data invariant: some boolean predicate over the type T which tells the cell what values are allowed to be stored. Then, the InvCell only has to impose the restriction that whenever the user writes to the cell, the value val being written has to satisfy the predicate, cell.inv(val). In exchange, though, whenever the user reads from the cell, they know the value they receive satisfies cell.inv(val).

Here’s an example using an InvCell to implement a memoized function:

spec fn result_of_computation() -> u64 {
    2
}

fn expensive_computation() -> (res: u64)
    ensures
        res == result_of_computation(),
{
    1 + 1
}

spec fn cell_is_valid(cell: &InvCell<Option<u64>>) -> bool {
    forall|v|
        (cell.inv(v) <==> match v {
            Option::Some(i) => i == result_of_computation(),
            Option::None => true,
        })
}

// Memoize the call to `expensive_computation()`.
// The argument here is an InvCell wrapping an Option<u64>,
// which is initially None, but then it is set to the correct
// answer once it's computed.
//
// The precondition here, given in the definition of `cell_is_valid` above,
// says that the InvCell has an invariant that the interior contents is either
// `None` or `Some(i)` where `i` is the desired value.
fn memoized_computation(cell: &InvCell<Option<u64>>) -> (res: u64)
    requires
        cell_is_valid(cell),
    ensures
        res == result_of_computation(),
{
    let c = cell.get();
    match c {
        Option::Some(i) => {
            // The value has already been computed; return the cached value
            i
        },
        Option::None => {
            // The value hasn't been computed yet. Compute it here
            let i = expensive_computation();
            // Store it for later
            cell.replace(Option::Some(i));
            // And return it now
            i
        },
    }
}

Tracked ghost state with PCell.

(TODO finish writing this chapter)

Pointers and cells

See the vstd documentation for more information on handling these features.

  • For cells, see PCell
  • For pointers to fixed-sized heap allocations, see PPtr.
  • For general support for *mut T and *const T, see vstd::raw_ptr

Concurrency

Verus provides the VerusSync framework for verifing programs that require a nontrivial ownership discipline. This includes multi-threaded concurrent code, and frequently it is also needed for nontrivial applications of unsafe features (such as pointers or unsafe cells).

The topic is sufficiently complex that we cover it in a separate tutorial and reference book.

Verifying a container library

In this section, we’ll learn how to verify a simple container library, specifically, via an example of a map data structure implemented using a binary search tree. In the case study, we’ll explore various considerations for writing a modular specification that encapsulates verification details as well as implementation details.

A simple binary search tree

In this section, we’re going to be implementing and verifying a Binary Search Tree (BST).

In the study of data structures, there are many known ways to balance a binary search tree. To keep things simple, we won’t be implementing any of them—instead, we’ll be implementing a straightforward, unbalanced binary search tree. Improving the design to be more efficient will be left as an exercise.

Furthermore, our first draft of an implementation is going to map keys of the fixed orderable type, u64, to values of type V. In a later chapter, we’ll change the keys to also be generic, thus mapping K to V for arbitrary types K and V.

The implementation

The structs

We’ll start by defining the tree shape itself, which contains one (key, value) pair at every node. We make no distinction between “leaf nodes” and “interior nodes”. Rather, every node has an optional left child and an optional right child. Furthermore, the tree might be entirely empty, in which case there is no root.

struct Node<V> {
    key: u64,
    value: V,
    left: Option<Box<Node<V>>>,
    right: Option<Box<Node<V>>>,
}

pub struct TreeMap<V> {
    root: Option<Box<Node<V>>>,
}

Note that only TreeMap is marked pub. Its field, root, as well as the Node type as a whole, are implementation details, and thus are private to the module.

The abstract view

When creating a new data structure, there are usually two important first steps:

  • Establish an interpretation of the data structure as some abstract datatype that will be used to write specifications.
  • Establish the well-formedness invariants of the data structure.

We’ll do the first one first (in part because it will actually help with the second one). In this case, we want to interpret the data structure as a Map<u64, V>. We can define such a function recursively.

impl<V> Node<V> {
    spec fn optional_as_map(node_opt: Option<Box<Node<V>>>) -> Map<u64, V>
        decreases node_opt,
    {
        match node_opt {
            None => Map::empty(),
            Some(node) => node.as_map(),
        }
    }

    spec fn as_map(self) -> Map<u64, V>
        decreases self,
    {
        Node::<V>::optional_as_map(self.left)
          .union_prefer_right(Node::<V>::optional_as_map(self.right))
          .insert(self.key, self.value)
    }
}

impl<V> TreeMap<V> {
    pub closed spec fn as_map(self) -> Map<u64, V> {
        Node::<V>::optional_as_map(self.root)
    }
}

Again note that only TreeMap::as_map is marked pub, and furthermore, that it’s marked closed. The definition of as_map is, again, an implementation detail.

It is customary to also implement the View trait as a convenience. This lets clients refer to the map implementation using the @ notation, e.g., tree_map@ as a shorthand for tree_map.view(). We’ll be writing our specifications in terms of tree_map.view().

impl<V> View for TreeMap<V> {
    type V = Map<u64, V>;

    open spec fn view(&self) -> Map<u64, V> {
        self.as_map()
    }
}

Establishing well-formedness

Next we establish well-formedness. This amounts to upholding the BST ordering property, namely, that for every node N, the nodes in N’s left subtree have keys less than N, while the nodes in N’s right subtree have keys greater than N. Again, this can be defined by a recursive spec function.

impl<V> Node<V> {
    spec fn well_formed(self) -> bool
        decreases self
    {
        &&& (forall |elem| Node::<V>::optional_as_map(self.left).dom().contains(elem) ==> elem < self.key)
        &&& (forall |elem| Node::<V>::optional_as_map(self.right).dom().contains(elem) ==> elem > self.key)
        &&& (match self.left {
            Some(left_node) => left_node.well_formed(),
            None => true,
        })
        &&& (match self.right {
            Some(right_node) => right_node.well_formed(),
            None => true,
        })
    }
}

impl<V> TreeMap<V> {
    pub closed spec fn well_formed(self) -> bool {
        match self.root {
            Some(node) => node.well_formed(),
            None => true, // empty tree always well-formed
        }
    }
}

Implementing a constructor: TreeMap::new()

Defining a constructor is simple; we create an empty tree with no root. The specification indicates that the returned object must represent the empty map.

impl<V> TreeMap<V> {
    pub fn new() -> (tree_map: Self)
        ensures
            tree_map.well_formed(),
            tree_map@ == Map::<u64, V>::empty()
    {
        TreeMap::<V> { root: None }
    }
}

Recall that tree_map@ is equivalent to tree_map.as_map(). An inspection of the definition of tree_map.as_map() and Node::optional_as_map() should make it apparent this will be the empty map when root is None.

Observe again that this specification does not refer to the tree internals at all, only that it is well-formed and that its abstract view is the empty map.

Implementing the insert operation

We can also implement insert using a recursive traversal. We search for the given node, using the well-formedness conditions to prove that we’re doing the right thing. During this traversal, we’ll either find a node with the right key, in which case we update the value, or we’ll reach a leaf without ever finding the desired node, in which case we create a new node.

(Aside: One slight snag has to do with a limitation of Verus’s handing of mutable references. Specifically, Verus doesn’t yet support an easy way to get a &mut T out of a &mut Option<T>. To get around this, we use std::mem::swap to get ownership of the node.)

impl<V> Node<V> {
    fn insert_into_optional(node: &mut Option<Box<Node<V>>>, key: u64, value: V)
        requires
            old(node).is_some() ==> old(node).unwrap().well_formed(),
        ensures
            node.is_some() ==> node.unwrap().well_formed(),
            Node::<V>::optional_as_map(*node) =~= Node::<V>::optional_as_map(*old(node)).insert(key, value)
    {
        if node.is_none() {
            *node = Some(Box::new(Node::<V> {
                key: key,
                value: value,
                left: None,
                right: None,
            }));
        } else {
            let mut tmp = None;
            std::mem::swap(&mut tmp, node);
            let mut boxed_node = tmp.unwrap();

            (&mut *boxed_node).insert(key, value);

            *node = Some(boxed_node);
        }
    }

    fn insert(&mut self, key: u64, value: V)
        requires
            old(self).well_formed(),
        ensures
            self.well_formed(),
            self.as_map() =~= old(self).as_map().insert(key, value),
    {
        if key == self.key {
            self.value = value;

            assert(!Node::<V>::optional_as_map(self.left).dom().contains(key));
            assert(!Node::<V>::optional_as_map(self.right).dom().contains(key));
        } else if key < self.key {
            Self::insert_into_optional(&mut self.left, key, value);

            assert(!Node::<V>::optional_as_map(self.right).dom().contains(key));
        } else {
            Self::insert_into_optional(&mut self.right, key, value);

            assert(!Node::<V>::optional_as_map(self.left).dom().contains(key));
        }
    }
}

impl<V> TreeMap<V> {
    pub fn insert(&mut self, key: u64, value: V)
        requires
            old(self).well_formed()
        ensures
            self.well_formed(),
            self@ == old(self)@.insert(key, value)
    {
        Node::<V>::insert_into_optional(&mut self.root, key, value);
    }
}

Observe that the specification of TreeMap::insert is given in terms of Map::insert.

Implementing the delete operation

Implementing delete is a little harder, because if we need to remove an interior node, we might have to reshape the tree a bit. However, since we aren’t trying to follow any particular balancing strategy, it’s still not that bad:

impl<V> Node<V> {
    fn delete_from_optional(node: &mut Option<Box<Node<V>>>, key: u64)
        requires
            old(node).is_some() ==> old(node).unwrap().well_formed(),
        ensures
            node.is_some() ==> node.unwrap().well_formed(),
            Node::<V>::optional_as_map(*node) =~= Node::<V>::optional_as_map(*old(node)).remove(key)
    {
        if node.is_some() {
            let mut tmp = None;
            std::mem::swap(&mut tmp, node);
            let mut boxed_node = tmp.unwrap();

            if key == boxed_node.key {
                assert(!Node::<V>::optional_as_map(boxed_node.left).dom().contains(key));
                assert(!Node::<V>::optional_as_map(boxed_node.right).dom().contains(key));

                if boxed_node.left.is_none() {
                    *node = boxed_node.right;
                } else {
                    if boxed_node.right.is_none() {
                        *node = boxed_node.left;
                    } else {
                        let (popped_key, popped_value) = Node::<V>::delete_rightmost(&mut boxed_node.left);
                        boxed_node.key = popped_key;
                        boxed_node.value = popped_value;
                        *node = Some(boxed_node);
                    }
                }
            } else if key < boxed_node.key {
                assert(!Node::<V>::optional_as_map(boxed_node.right).dom().contains(key));
                Node::<V>::delete_from_optional(&mut boxed_node.left, key);
                *node = Some(boxed_node);
            } else {
                assert(!Node::<V>::optional_as_map(boxed_node.left).dom().contains(key));
                Node::<V>::delete_from_optional(&mut boxed_node.right, key);
                *node = Some(boxed_node);
            }
        }
    }

    fn delete_rightmost(node: &mut Option<Box<Node<V>>>) -> (popped: (u64, V))
        requires
            old(node).is_some(),
            old(node).unwrap().well_formed(),
        ensures
            node.is_some() ==> node.unwrap().well_formed(),
            Node::<V>::optional_as_map(*node) =~= Node::<V>::optional_as_map(*old(node)).remove(popped.0),
            Node::<V>::optional_as_map(*old(node)).dom().contains(popped.0),
            Node::<V>::optional_as_map(*old(node))[popped.0] == popped.1,
            forall |elem| Node::<V>::optional_as_map(*old(node)).dom().contains(elem) ==> popped.0 >= elem,
    {
        let mut tmp = None;
        std::mem::swap(&mut tmp, node);
        let mut boxed_node = tmp.unwrap();

        if boxed_node.right.is_none() {
            *node = boxed_node.left;
            assert(Node::<V>::optional_as_map(boxed_node.right) =~= Map::empty());
            assert(!Node::<V>::optional_as_map(boxed_node.left).dom().contains(boxed_node.key));
            return (boxed_node.key, boxed_node.value);
        } else {
            let (popped_key, popped_value) = Node::<V>::delete_rightmost(&mut boxed_node.right);
            assert(!Node::<V>::optional_as_map(boxed_node.left).dom().contains(popped_key));
            *node = Some(boxed_node);
            return (popped_key, popped_value);
        }
    }
}

impl<V> TreeMap<V> {
    pub fn delete(&mut self, key: u64)
        requires old(self).well_formed()
        ensures
            self.well_formed(),
            self@ == old(self)@.remove(key)
    {
        Node::<V>::delete_from_optional(&mut self.root, key);
    }
}

Observe that the specification of TreeMap::delete is given in terms of Map::remove.

Implementing the get operation

Finally, we implement and verify TreeMap::get. This function looks up a key and returns an Option<&V> (None if the key isn’t in the TreeMap).

impl<V> Node<V> {
    fn get_from_optional(node: &Option<Box<Node<V>>>, key: u64) -> Option<&V>
        requires node.is_some() ==> node.unwrap().well_formed(),
        returns (match node {
            Some(node) => (if node.as_map().dom().contains(key) { Some(&node.as_map()[key]) } else { None }),
            None => None,
        }),
    {
        match node {
            None => None,
            Some(node) => {
                node.get(key)
            }
        }
    }

    fn get(&self, key: u64) -> Option<&V>
        requires self.well_formed(),
        returns (if self.as_map().dom().contains(key) { Some(&self.as_map()[key]) } else { None })
    {
        if key == self.key {
            Some(&self.value)
        } else if key < self.key {
            proof { assert(!Node::<V>::optional_as_map(self.right).dom().contains(key)); }
            Self::get_from_optional(&self.left, key)
        } else {
            proof { assert(!Node::<V>::optional_as_map(self.left).dom().contains(key)); }
            Self::get_from_optional(&self.right, key)
        }
    }
}

impl<V> TreeMap<V> {
    pub fn get(&self, key: u64) -> Option<&V>
        requires self.well_formed(),
        returns (if self@.dom().contains(key) { Some(&self@[key]) } else { None })
    {
        Node::<V>::get_from_optional(&self.root, key)
    }
}

Using the TreeMap as a client

A short client program illustrates how we can reason about the TreeMap as if it were a Map.

fn test() {
    let mut tree_map = TreeMap::<bool>::new();
    tree_map.insert(17, false);
    tree_map.insert(18, false);
    tree_map.insert(17, true);

    assert(tree_map@ == map![17u64 => true, 18u64 => false]);

    tree_map.delete(17);

    assert(tree_map@ == map![18u64 => false]);

    let elem17 = tree_map.get(17);
    let elem18 = tree_map.get(18);
    assert(elem17.is_none());
    assert(elem18 == Some(&false));
}

Full source

The full source for this example can be found here.

Encapsulating well-formedness with type invariants

Recall our specifications from the previous chapter:

impl<V> TreeMap<V> {
    pub fn new() -> (tree_map: Self)
        ensures
            tree_map.well_formed(),
            tree_map@ == Map::<u64, V>::empty()

    pub fn insert(&mut self, key: u64, value: V)
        requires
            old(self).well_formed()
        ensures
            self.well_formed(),
            self@ == old(self)@.insert(key, value)

    pub fn delete(&mut self, key: u64)
        requires old(self).well_formed()
        ensures
            self.well_formed(),
            self@ == old(self)@.remove(key)

    pub fn get(&self, key: u64) -> Option<&V>
        requires self.well_formed(),
        returns (if self@.dom().contains(key) { Some(&self@[key]) } else { None })
}

Observe the presenence of this tree_map.well_formed() predicate, especially in the requires clauses. As a result of this, the client needs to work with this tree_map.well_formed() predicate all throughout their own code. For example:

fn test2() {
    let mut tree_map = TreeMap::<bool>::new();
    test_callee(tree_map);
}

fn test_callee(tree_map: TreeMap<bool>)
    requires tree_map.well_formed(),
{
    let mut tree_map = tree_map;
    tree_map.insert(25, true);
    tree_map.insert(100, true);
}

Without the requires clause, the above snippet would fail to verify.

Intuitively, however, one might wonder why we have to carry this predicate around at all. After all, due to encapsulation, it isn’t ever possible for the client to create a tree_map where well_formed() doesn’t hold.

In this section, we’ll show how to use Verus’s type invariants feature to remedy this burden from the client.

Applying the type_invariant attribute.

In order to tell Verus that we want the well_formed() predicate to be inferred automatically, we can mark it with the #[verifier::type_invariant] attribute:

impl<V> TreeMap<V> {
    #[verifier::type_invariant]
    spec fn well_formed(self) -> bool {
        match self.root {
            Some(node) => node.well_formed(),
            None => true, // empty tree always well-formed
        }
    }
}

This has two effects:

  • It adds an extra constraint that all TreeMap objects satsify the well_formed() condition at all times. This constraint is checked by Verus whenever a TreeMap object is constructed or modified.
  • It allows the programmer to assume the well_formed() condition at all times, even when it isn’t present in a requires clause.

Note that in addition to adding the type_invariant attribute, we have also removed the pub specifier from well_formed. Now not only is the body invisible to the client, even the name is as well. After all, our intent is to prevent the client from needing to reason about it, at which point there is no reason to expose it through the public interface at all.

Of course, for this to be possible, we’ll need to update the specifications of TreeMap’s various pub methods.

Updating the code: new

Let’s start with an easy one: new.

impl<V> TreeMap<V> {
    pub fn new() -> (s: Self)
        ensures
            s@ == Map::<u64, V>::empty()
    {
        TreeMap::<V> { root: None }
    }
}

All we’ve done here is remove the s.well_formed() postcondition, which as discussed, is no longer necessary.

Crucially, Verus still requires us to prove that s.well_formed() holds. Specifically, since well_formed has been marked with #[verifier::type_invariant], Verus checks that well_formed() holds when the TreeMap constructor returns. As before, Verus can check this condition fairly trivially.

Updating the code: get

Now let’s take a look at get. The first thing to notice is that we remove the requires self.well_formed() clause.

impl<V> TreeMap<V> {
    pub fn get(&self, key: u64) -> Option<&V>
        returns (if self@.dom().contains(key) { Some(&self@[key]) } else { None })
    {
        proof { use_type_invariant(&*self); }
        Node::<V>::get_from_optional(&self.root, key)
    }
}

Given that we no longer have the precondition, how do we deduce self.well_formed() (which is needed to prove self.root is well-formed and call Node::get_from_optional)?

This can be done with the built-in pseudo-lemma use_type_invariant. When called on any object, this feature guarantees that the provided object satisfies its type invariants.

Updating the code: insert

Now let’s check TreeMap::insert, which if you recall, has to modify the tree.

impl<V> TreeMap<V> {
    pub fn insert(&mut self, key: u64, value: V)
        ensures
            self@ == old(self)@.insert(key, value)
    {
        proof { use_type_invariant(&*self); }
        let mut root = None;
        std::mem::swap(&mut root, &mut self.root);
        Node::<V>::insert_into_optional(&mut root, key, value);
        self.root = root;
    }
}

As before, we use use_type_invariant to establish that self.well_formed() holds at the beginning of the function, even without the requires clause.

One slight challenge that arises from the use of #[verifier::type_invariant] is that it enforces type invariants to hold at every program point. Sometimes, this can make intermediate computation a little tricky.

In this case, an easy way to get around this is to swap the root field with None, then swap back when we’re done. This works because the empty TreeMap trivially satisfies the well-formedness, so it’s allowed.

One might wonder why we can’t just do Node::<V>::insert_into_optional(&mut self.root, key, value) without swapping. The trouble with this is that it requires us to ensure the call to insert_into_optional is “unwind-safe”, i.e., that all type invariants would be preserved even if a panic occurs and insert_into_optional has to exit early. Right now, Verus only has one way to ensure unwind-safety, which is to bluntly ensure that no unwinding happens at all. Thus, the ideal solution would be to mark insert_into_optional as no_unwind. However, this is impossible in this case, because node insertion will call Box::new.

Between this problem, and Verus’s current limitations regarding unwind-safety, the swap approach becomes the easiest solution as a way of sidestepping it. Check the reference page for more information on the limitations of the type_invariant feature.

Updating the code: delete

This is pretty much the same as insert.

impl<V> TreeMap<V> {
    pub fn delete(&mut self, key: u64)
        ensures
            self@ == old(self)@.remove(key)
    {
        proof { use_type_invariant(&*self); }
        let mut root = None;
        std::mem::swap(&mut root, &mut self.root);
        Node::<V>::delete_from_optional(&mut root, key);
        self.root = root;
    }
}

The new signatures and specifications

Putting it all together, we end up with the following specifications for our public API:

impl<V> TreeMap<V> {
    pub fn new() -> (s: Self)
        ensures
            s@ == Map::<u64, V>::empty()

    pub fn insert(&mut self, key: u64, value: V)
        ensures
            self@ == old(self)@.insert(key, value)

    pub fn delete(&mut self, key: u64)
        ensures
            self@ == old(self)@.remove(key)

    pub fn get(&self, key: u64) -> Option<&V>
        returns (if self@.dom().contains(key) { Some(&self@[key]) } else { None })
}

These are almost the same as what we had before; the only difference is that all the well_formed() clauses have been removed.

Conveniently, there are no longer any requires clause at all, so it’s always possible to call any of these functions. This is also important if we want to prove the API “safe” in the Rust sense (see this page).

The new client code

As before, the client code gets to reason about the TreeMap as if it were just a Map. Now, however, it’s a bit simpler because we don’t have to reason about tree_map.well_formed().

fn test() {
    let mut tree_map = TreeMap::<bool>::new();
    tree_map.insert(17, false);
    tree_map.insert(18, false);
    tree_map.insert(17, true);

    assert(tree_map@ == map![17u64 => true, 18u64 => false]);

    tree_map.delete(17);

    assert(tree_map@ == map![18u64 => false]);

    let elem17 = tree_map.get(17);
    let elem18 = tree_map.get(18);
    assert(elem17.is_none());
    assert(elem18 == Some(&false));

    test2(tree_map);
}

fn test2(tree_map: TreeMap<bool>) {
    let mut tree_map = tree_map;
    tree_map.insert(25, true);
    tree_map.insert(100, true);
}

Full source

The full source for this example can be found here.

Making it generic

In the previous sections, we devised a TreeMap<V> which a used fixed key type (u64). In this section, we’ll show to make a TreeMap<K, V> which is generic over the key type K.

Defining a “total order”

The main reason this is challenging is that the BST requires a way of comparing values of K, both for equality, and to obtain an ordering. This comparison is used both in the implementation (to find the node for a given key, or to figure out where such a node should be inserted) and in the well-formedness invariants that enforce the BST ordering property.

We can define the concept of “total order” generically by creating a trait.

pub enum Cmp { Less, Equal, Greater }

pub trait TotalOrdered : Sized {
    spec fn le(self, other: Self) -> bool;

    proof fn reflexive(x: Self)
        ensures Self::le(x, x);

    proof fn transitive(x: Self, y: Self, z: Self)
        requires Self::le(x, y), Self::le(y, z),
        ensures Self::le(x, z);

    proof fn antisymmetric(x: Self, y: Self)
        requires Self::le(x, y), Self::le(y, x),
        ensures x == y;

    proof fn total(x: Self, y: Self)
        ensures Self::le(x, y) || Self::le(y, x);

    fn compare(&self, other: &Self) -> (c: Cmp)
        ensures (match c {
            Cmp::Less => self.le(*other) && self != other,
            Cmp::Equal => self == other,
            Cmp::Greater => other.le(*self) && self != other,
        });
}

This trait simultaneously:

  • Requires a binary relation le to exist
  • Requires it to satisfy the properties of a total order
  • Requires an executable three-way comparison function to exist

There’s one simplification we’ve made here: we’re assuming that “equality” in the comparison function is the same as spec equality. This isn’t always suitable; some datatypes may have more than one way to represent the same logical value. A more general specification would allow an ordering that respects some arbitrary equivalence relation. This is how vstd::hash_map::HashMapWithView works, for example. To keep things simple for this demonstration though, we’ll use a total ordering that respects spec equality.

Updating the struct and definitions

We’ll start by updating the structs to take a generic parameter K: TotalOrdered.

struct Node<K: TotalOrdered, V> {
    key: K,
    value: V,
    left: Option<Box<Node<K, V>>>,
    right: Option<Box<Node<K, V>>>,
}

pub struct TreeMap<K: TotalOrdered, V> {
    root: Option<Box<Node<K, V>>>,
}

We’ll also update the well-formedness condition to use the generic K::le instead of integer <=. Where the original definition used a < b, we now use a.le(b) && a != b.

impl<K: TotalOrdered, V> Node<K, V> {
    pub closed spec fn well_formed(self) -> bool
        decreases self
    {
        &&& (forall |elem| #[trigger] Node::<K, V>::optional_as_map(self.left).dom().contains(elem) ==> elem.le(self.key) && elem != self.key)
        &&& (forall |elem| #[trigger] Node::<K, V>::optional_as_map(self.right).dom().contains(elem) ==> self.key.le(elem) && elem != self.key)
        &&& (match self.left {
            Some(left_node) => left_node.well_formed(),
            None => true,
        })
        &&& (match self.right {
            Some(right_node) => right_node.well_formed(),
            None => true,
        })
    }
}

impl<K: TotalOrdered, V> TreeMap<K, V> {
    #[verifier::type_invariant]
    spec fn well_formed(self) -> bool {
        match self.root {
            Some(node) => node.well_formed(),
            None => true, // empty tree always well-formed
        }
    }
}

Meanwhile, the definition of as_map doesn’t rely on the ordering function, so it can be left alone, the same as before.

Updating the implementations and proofs

Updating the implementations take a bit more work, since we need more substantial proof code. Whereas Verus has good automation for integer inequalities (<), it has no such automation for our new, hand-made TotalOrdered trait. Thus, we need to add proof code to invoke its properties manually.

Let’s take a look at Node::get.

The meat of the proof roughly goes as follows:

Supoose we’re looking for the key key which compares less than self.key. Then we need to show that recursing into the left subtree gives the correct answer; for this, it suffices to show that key is not in the right subtree.

Suppose (for contradiction) that key were in the right subtree. Then (by the well-formedness invariant), we must have key > self.key. But we already established that key < self.key. Contradiction. (Formally, this contradiction can be obtained by invoking antisymmetry.)

impl<K: TotalOrdered, V> Node<K, V> {
    fn get_from_optional(node: &Option<Box<Node<K, V>>>, key: K) -> Option<&V>
        requires node.is_some() ==> node.unwrap().well_formed(),
        returns (match node {
            Some(node) => (if node.as_map().dom().contains(key) { Some(&node.as_map()[key]) } else { None }),
            None => None,
        }),
    {
        match node {
            None => None,
            Some(node) => {
                node.get(key)
            }
        }
    }

    fn get(&self, key: K) -> Option<&V>
        requires self.well_formed(),
        returns (if self.as_map().dom().contains(key) { Some(&self.as_map()[key]) } else { None })
    {
        match key.compare(&self.key) {
            Cmp::Equal => {
                Some(&self.value)
            }
            Cmp::Less => {
                proof {
                    if Node::<K, V>::optional_as_map(self.right).dom().contains(key) {
                        TotalOrdered::antisymmetric(self.key, key);
                        assert(false);
                    }
                    assert(key != self.key);
                    assert((match self.left {
                            Some(node) => (if node.as_map().dom().contains(key) { Some(&node.as_map()[key]) } else { None }),
                            None => None,
                        }) == (if self.as_map().dom().contains(key) { Some(&self.as_map()[key]) } else { None }));
                }
                Self::get_from_optional(&self.left, key)
            }
            Cmp::Greater => {
                proof {
                    if Node::<K, V>::optional_as_map(self.left).dom().contains(key) {
                        TotalOrdered::antisymmetric(self.key, key);
                        assert(false);
                    }
                    assert(key != self.key);
                    assert((match self.right {
                            Some(node) => (if node.as_map().dom().contains(key) { Some(&node.as_map()[key]) } else { None }),
                            None => None,
                        }) == (if self.as_map().dom().contains(key) { Some(&self.as_map()[key]) } else { None }));
                }
                Self::get_from_optional(&self.right, key)
            }
        }
    }
}

We can update insert and delete similarly, manually inserting lemma calls to invoke the total-ordering properties where necessary.

Full source

The full source for this example can be found here.

Implementing Clone

As a finishing touch, let’s implement Clone for TreeMap<K, V>. The main trick here will be in figuring out the correct specification for TreeMap::<K, V>::Clone.

Naturally, such an implementation will require both K: Clone and V: Clone. However, to write a sensible clone implementation for the tree, we have to consider what the implementations of K::clone and V::clone actually do.

Generally speaking, Verus imposes no constraints on the implementations of Clone, so it is not necessarily true that a clone() call will return a value that is spec-equal to its input.

With this in mind, to simplify this example, we’re going to prove the following signature for TreeMap<K, V>::clone:

impl<K: Copy + TotalOrdered, V: Clone> Clone for TreeMap<K, V> {
    fn clone(&self) -> (res: Self)
        ensures self@.dom() =~= res@.dom(),
            forall |key| #[trigger] res@.dom().contains(key) ==>
                call_ensures(V::clone, (&self@[key],), res@[key])
    {
        ...
    }
}

We explain the details of this signature below.

Dealing with K::clone

In order to clone all the keys, we need K::clone to respect the ordering of elements; otherwise during a clone operation, we’d need to re-sort all the keys so that the resulting tree would be valid. However, it’s unlikely that is desirable behavior. If K::clone doesn’t respect the TotalOrdered implementation, it’s likely a user bug.

A general way to handle this would be to require that Clone actually be compatible with the total-ordering in some sense. However, you’ll recall from the previous section that we’re already simplifying the “total ordered” specification a bit. Likewise, we’re going to continue to keep things simple here by also requiring that K: Copy.

As a result, we’ll be able to prove that our TreeMap clone implementation can preserve all keys exactly, even when compared via spec equality. That is, we’ll be able to ensure that self@.dom() =~= res@.dom().

Dealing with V::clone

So what about V? Again, we don’t know a priori what V::clone does. It might return a value unequal to the imput; it might even be nondeterminstic. Therefore, a cloned TreeMap may have different values than the original.

In order to specify TreeMap::<K, V>::clone as generically as possible, we choose to write its ensures clause in terms of the ensures clause for V::clone. This can be done using call_ensures. The predicate call_ensures(V::clone, (&self@[key],), res@[key]) effectively says “self@[key] and res@[key] are a possible input-output pair for V::clone”.

Understanding the implications of the signature

Let’s do a few examples.

First, consider cloning a TreeMap::<u64, u32>. The Verus standard library provides a specification for u32::clone; it’s the same as a copy, i.e., a cloned u32 always equals the input. As a result, we can deduce that cloning a TreeMap::<u64, u32> will preserve its view exactly. We can prove this using extensional equality.

fn test_clone_u32(tree_map: TreeMap<u64, u32>) {
    let tree_map2 = tree_map.clone();
    assert(tree_map2@ =~= tree_map@);
}

We can do the same for any type where clone guarantees spec-equality. Here’s another example with a user-defined type.

struct IntWrapper {
    pub int_value: u32,
}

impl Clone for IntWrapper {
    fn clone(&self) -> (s: Self)
        ensures s == *self
    {
        IntWrapper { int_value: self.int_value }
    }
}

fn test_clone_int_wrapper(tree_map: TreeMap<u64, IntWrapper>) {
    let tree_map2 = tree_map.clone();
    assert(tree_map2@ =~= tree_map@);
}

This works because of the postcondition on IntWrapper::clone, that is, ensures *s == self. If you’re new to this style, it might seem initially surprising that IntWrapper::clone has any effect on the verification of test_clone_int_wrapper, since it doesn’t directly call IntWrapper::clone. In this case, the postcondition is referenced indirectly via TreeMap<u64, IntWrapper>:clone.

Let’s do one more example, this time with a less precise clone function.

struct WeirdInt {
    pub int_value: u32,
    pub other: u32,
}

impl Clone for WeirdInt {
    fn clone(&self) -> (s: Self)
        ensures s.int_value == self.int_value
    {
        WeirdInt { int_value: self.int_value, other: 0 }
    }
}

fn test_clone_weird_int(tree_map: TreeMap<u64, WeirdInt>) {
    let tree_map2 = tree_map.clone();

    // assert(tree_map2@ =~= tree_map@); // this would fail

    assert(tree_map2@.dom() == tree_map@.dom());
    assert(forall |k| tree_map@.dom().contains(k) ==>
        tree_map2@[k].int_value == tree_map@[k].int_value);
}

This example is a bit pathological; our struct, WeirdInt, has an extra field that doesn’t get cloned. You could imagine real-life scenarios that have this property (for example, if every struct needs to have a unique identifier). Anyway, the postcondition of WeirdInt::clone doesn’t say both objects are equal, only that the int_value fields are equal. This postcondition can then be inferred for each value in the map, as shown.

Implementing TreeMap::<K, V>::Clone.

As usual, we write the implementation as a recursive function.

It’s not necessary to implement Node::Clone; one could instead just implement a normal recursive function as a helper for TreeMap::Clone; but it’s more Rust-idiomatic to do it this way. This lets us call Option<Node<K, V>>::Clone in the implementation of TreeMap::clone (the spec for Option::clone is provided by vstd). However, you can see that there are a few ‘gotchas’ that need to be worked around.

impl<K: Copy + TotalOrdered, V: Clone> Clone for Node<K, V> {
    fn clone(&self) -> (res: Self)
        ensures
            self.well_formed() ==> res.well_formed(),
            self.as_map().dom() =~= res.as_map().dom(),
            forall |key| #[trigger] res.as_map().dom().contains(key) ==>
                call_ensures(V::clone, (&self.as_map()[key],), res.as_map()[key])
    {
        // TODO(fixme): Assigning V::clone to a variable is a hack needed to work around
        // this issue: https://github.com/verus-lang/verus/issues/1348
        let v_clone = V::clone;

        let res = Node {
            key: self.key,
            value: v_clone(&self.value),
            // Ordinarily, we would use Option<Node>::clone rather than inlining
            // the case statement here; we write it this way to work around
            // this issue: https://github.com/verus-lang/verus/issues/1346
            left: (match &self.left {
                Some(node) => Some(Box::new((&**node).clone())),
                None => None,
            }),
            right: (match &self.right {
                Some(node) => Some(Box::new((&**node).clone())),
                None => None,
            }),
        };

        proof {
            assert(Node::optional_as_map(res.left).dom() =~= 
                Node::optional_as_map(self.left).dom());
            assert(Node::optional_as_map(res.right).dom() =~= 
                Node::optional_as_map(self.right).dom());
        }

        return res;
    }
}

impl<K: Copy + TotalOrdered, V: Clone> Clone for TreeMap<K, V> {
    fn clone(&self) -> (res: Self)
        ensures self@.dom() =~= res@.dom(),
            forall |key| #[trigger] res@.dom().contains(key) ==>
                call_ensures(V::clone, (&self@[key],), res@[key])
    {
        proof {
            use_type_invariant(self);
        }

        TreeMap {
            // This calls Option<Node<K, V>>::Clone
            root: self.root.clone(),
        }
    }
}

Full source

The full source for this example can be found here.

Full source for the examples

First draft

use vstd::prelude::*;

verus!{

struct Node<V> {
    key: u64,
    value: V,
    left: Option<Box<Node<V>>>,
    right: Option<Box<Node<V>>>,
}

pub struct TreeMap<V> {
    root: Option<Box<Node<V>>>,
}

impl<V> Node<V> {
    spec fn optional_as_map(node_opt: Option<Box<Node<V>>>) -> Map<u64, V>
        decreases node_opt,
    {
        match node_opt {
            None => Map::empty(),
            Some(node) => node.as_map(),
        }
    }

    spec fn as_map(self) -> Map<u64, V>
        decreases self,
    {
        Node::<V>::optional_as_map(self.left)
          .union_prefer_right(Node::<V>::optional_as_map(self.right))
          .insert(self.key, self.value)
    }
}

impl<V> TreeMap<V> {
    pub closed spec fn as_map(self) -> Map<u64, V> {
        Node::<V>::optional_as_map(self.root)
    }
}

impl<V> View for TreeMap<V> {
    type V = Map<u64, V>;

    open spec fn view(&self) -> Map<u64, V> {
        self.as_map()
    }
}

impl<V> Node<V> {
    spec fn well_formed(self) -> bool
        decreases self
    {
        &&& (forall |elem| Node::<V>::optional_as_map(self.left).dom().contains(elem) ==> elem < self.key)
        &&& (forall |elem| Node::<V>::optional_as_map(self.right).dom().contains(elem) ==> elem > self.key)
        &&& (match self.left {
            Some(left_node) => left_node.well_formed(),
            None => true,
        })
        &&& (match self.right {
            Some(right_node) => right_node.well_formed(),
            None => true,
        })
    }
}

impl<V> TreeMap<V> {
    pub closed spec fn well_formed(self) -> bool {
        match self.root {
            Some(node) => node.well_formed(),
            None => true, // empty tree always well-formed
        }
    }
}

impl<V> TreeMap<V> {
    pub fn new() -> (tree_map: Self)
        ensures
            tree_map.well_formed(),
            tree_map@ == Map::<u64, V>::empty()
    {
        TreeMap::<V> { root: None }
    }
}

impl<V> Node<V> {
    fn insert_into_optional(node: &mut Option<Box<Node<V>>>, key: u64, value: V)
        requires
            old(node).is_some() ==> old(node).unwrap().well_formed(),
        ensures
            node.is_some() ==> node.unwrap().well_formed(),
            Node::<V>::optional_as_map(*node) =~= Node::<V>::optional_as_map(*old(node)).insert(key, value)
    {
        if node.is_none() {
            *node = Some(Box::new(Node::<V> {
                key: key,
                value: value,
                left: None,
                right: None,
            }));
        } else {
            let mut tmp = None;
            std::mem::swap(&mut tmp, node);
            let mut boxed_node = tmp.unwrap();

            (&mut *boxed_node).insert(key, value);

            *node = Some(boxed_node);
        }
    }

    fn insert(&mut self, key: u64, value: V)
        requires
            old(self).well_formed(),
        ensures
            self.well_formed(),
            self.as_map() =~= old(self).as_map().insert(key, value),
    {
        if key == self.key {
            self.value = value;

            assert(!Node::<V>::optional_as_map(self.left).dom().contains(key));
            assert(!Node::<V>::optional_as_map(self.right).dom().contains(key));
        } else if key < self.key {
            Self::insert_into_optional(&mut self.left, key, value);

            assert(!Node::<V>::optional_as_map(self.right).dom().contains(key));
        } else {
            Self::insert_into_optional(&mut self.right, key, value);

            assert(!Node::<V>::optional_as_map(self.left).dom().contains(key));
        }
    }
}

impl<V> TreeMap<V> {
    pub fn insert(&mut self, key: u64, value: V)
        requires
            old(self).well_formed()
        ensures
            self.well_formed(),
            self@ == old(self)@.insert(key, value)
    {
        Node::<V>::insert_into_optional(&mut self.root, key, value);
    }
}

impl<V> Node<V> {
    fn delete_from_optional(node: &mut Option<Box<Node<V>>>, key: u64)
        requires
            old(node).is_some() ==> old(node).unwrap().well_formed(),
        ensures
            node.is_some() ==> node.unwrap().well_formed(),
            Node::<V>::optional_as_map(*node) =~= Node::<V>::optional_as_map(*old(node)).remove(key)
    {
        if node.is_some() {
            let mut tmp = None;
            std::mem::swap(&mut tmp, node);
            let mut boxed_node = tmp.unwrap();

            if key == boxed_node.key {
                assert(!Node::<V>::optional_as_map(boxed_node.left).dom().contains(key));
                assert(!Node::<V>::optional_as_map(boxed_node.right).dom().contains(key));

                if boxed_node.left.is_none() {
                    *node = boxed_node.right;
                } else {
                    if boxed_node.right.is_none() {
                        *node = boxed_node.left;
                    } else {
                        let (popped_key, popped_value) = Node::<V>::delete_rightmost(&mut boxed_node.left);
                        boxed_node.key = popped_key;
                        boxed_node.value = popped_value;
                        *node = Some(boxed_node);
                    }
                }
            } else if key < boxed_node.key {
                assert(!Node::<V>::optional_as_map(boxed_node.right).dom().contains(key));
                Node::<V>::delete_from_optional(&mut boxed_node.left, key);
                *node = Some(boxed_node);
            } else {
                assert(!Node::<V>::optional_as_map(boxed_node.left).dom().contains(key));
                Node::<V>::delete_from_optional(&mut boxed_node.right, key);
                *node = Some(boxed_node);
            }
        }
    }

    fn delete_rightmost(node: &mut Option<Box<Node<V>>>) -> (popped: (u64, V))
        requires
            old(node).is_some(),
            old(node).unwrap().well_formed(),
        ensures
            node.is_some() ==> node.unwrap().well_formed(),
            Node::<V>::optional_as_map(*node) =~= Node::<V>::optional_as_map(*old(node)).remove(popped.0),
            Node::<V>::optional_as_map(*old(node)).dom().contains(popped.0),
            Node::<V>::optional_as_map(*old(node))[popped.0] == popped.1,
            forall |elem| Node::<V>::optional_as_map(*old(node)).dom().contains(elem) ==> popped.0 >= elem,
    {
        let mut tmp = None;
        std::mem::swap(&mut tmp, node);
        let mut boxed_node = tmp.unwrap();

        if boxed_node.right.is_none() {
            *node = boxed_node.left;
            assert(Node::<V>::optional_as_map(boxed_node.right) =~= Map::empty());
            assert(!Node::<V>::optional_as_map(boxed_node.left).dom().contains(boxed_node.key));
            return (boxed_node.key, boxed_node.value);
        } else {
            let (popped_key, popped_value) = Node::<V>::delete_rightmost(&mut boxed_node.right);
            assert(!Node::<V>::optional_as_map(boxed_node.left).dom().contains(popped_key));
            *node = Some(boxed_node);
            return (popped_key, popped_value);
        }
    }
}

impl<V> TreeMap<V> {
    pub fn delete(&mut self, key: u64)
        requires old(self).well_formed()
        ensures
            self.well_formed(),
            self@ == old(self)@.remove(key)
    {
        Node::<V>::delete_from_optional(&mut self.root, key);
    }
}

impl<V> Node<V> {
    fn get_from_optional(node: &Option<Box<Node<V>>>, key: u64) -> Option<&V>
        requires node.is_some() ==> node.unwrap().well_formed(),
        returns (match node {
            Some(node) => (if node.as_map().dom().contains(key) { Some(&node.as_map()[key]) } else { None }),
            None => None,
        }),
    {
        match node {
            None => None,
            Some(node) => {
                node.get(key)
            }
        }
    }

    fn get(&self, key: u64) -> Option<&V>
        requires self.well_formed(),
        returns (if self.as_map().dom().contains(key) { Some(&self.as_map()[key]) } else { None })
    {
        if key == self.key {
            Some(&self.value)
        } else if key < self.key {
            proof { assert(!Node::<V>::optional_as_map(self.right).dom().contains(key)); }
            Self::get_from_optional(&self.left, key)
        } else {
            proof { assert(!Node::<V>::optional_as_map(self.left).dom().contains(key)); }
            Self::get_from_optional(&self.right, key)
        }
    }
}

impl<V> TreeMap<V> {
    pub fn get(&self, key: u64) -> Option<&V>
        requires self.well_formed(),
        returns (if self@.dom().contains(key) { Some(&self@[key]) } else { None })
    {
        Node::<V>::get_from_optional(&self.root, key)
    }
}

fn test() {
    let mut tree_map = TreeMap::<bool>::new();
    tree_map.insert(17, false);
    tree_map.insert(18, false);
    tree_map.insert(17, true);

    assert(tree_map@ == map![17u64 => true, 18u64 => false]);

    tree_map.delete(17);

    assert(tree_map@ == map![18u64 => false]);

    let elem17 = tree_map.get(17);
    let elem18 = tree_map.get(18);
    assert(elem17.is_none());
    assert(elem18 == Some(&false));
}

fn test2() {
    let mut tree_map = TreeMap::<bool>::new();
    test_callee(tree_map);
}

fn test_callee(tree_map: TreeMap<bool>)
    requires tree_map.well_formed(),
{
    let mut tree_map = tree_map;
    tree_map.insert(25, true);
    tree_map.insert(100, true);
}


}

Version with type invariants

use vstd::prelude::*;

verus!{

struct Node<V> {
    key: u64,
    value: V,
    left: Option<Box<Node<V>>>,
    right: Option<Box<Node<V>>>,
}

pub struct TreeMap<V> {
    root: Option<Box<Node<V>>>,
}

impl<V> Node<V> {
    spec fn optional_as_map(node_opt: Option<Box<Node<V>>>) -> Map<u64, V>
        decreases node_opt,
    {
        match node_opt {
            None => Map::empty(),
            Some(node) => node.as_map(),
        }
    }

    spec fn as_map(self) -> Map<u64, V>
        decreases self,
    {
        Node::<V>::optional_as_map(self.left)
          .union_prefer_right(Node::<V>::optional_as_map(self.right))
          .insert(self.key, self.value)
    }
}

impl<V> TreeMap<V> {
    pub closed spec fn as_map(self) -> Map<u64, V> {
        Node::<V>::optional_as_map(self.root)
    }
}

impl<V> View for TreeMap<V> {
    type V = Map<u64, V>;

    open spec fn view(&self) -> Map<u64, V> {
        self.as_map()
    }
}

impl<V> Node<V> {
    spec fn well_formed(self) -> bool
        decreases self
    {
        &&& (forall |elem| Node::<V>::optional_as_map(self.left).dom().contains(elem) ==> elem < self.key)
        &&& (forall |elem| Node::<V>::optional_as_map(self.right).dom().contains(elem) ==> elem > self.key)
        &&& (match self.left {
            Some(left_node) => left_node.well_formed(),
            None => true,
        })
        &&& (match self.right {
            Some(right_node) => right_node.well_formed(),
            None => true,
        })
    }
}

impl<V> TreeMap<V> {
    #[verifier::type_invariant]
    spec fn well_formed(self) -> bool {
        match self.root {
            Some(node) => node.well_formed(),
            None => true, // empty tree always well-formed
        }
    }
}

impl<V> TreeMap<V> {
    pub fn new() -> (s: Self)
        ensures
            s@ == Map::<u64, V>::empty()
    {
        TreeMap::<V> { root: None }
    }
}

impl<V> Node<V> {
    fn insert_into_optional(node: &mut Option<Box<Node<V>>>, key: u64, value: V)
        requires
            old(node).is_some() ==> old(node).unwrap().well_formed(),
        ensures
            node.is_some() ==> node.unwrap().well_formed(),
            Node::<V>::optional_as_map(*node) =~= Node::<V>::optional_as_map(*old(node)).insert(key, value)
    {
        if node.is_none() {
            *node = Some(Box::new(Node::<V> {
                key: key,
                value: value,
                left: None,
                right: None,
            }));
        } else {
            let mut tmp = None;
            std::mem::swap(&mut tmp, node);
            let mut boxed_node = tmp.unwrap();

            (&mut *boxed_node).insert(key, value);

            *node = Some(boxed_node);
        }
    }

    fn insert(&mut self, key: u64, value: V)
        requires
            old(self).well_formed(),
        ensures
            self.well_formed(),
            self.as_map() =~= old(self).as_map().insert(key, value),
    {
        if key == self.key {
            self.value = value;

            assert(!Node::<V>::optional_as_map(self.left).dom().contains(key));
            assert(!Node::<V>::optional_as_map(self.right).dom().contains(key));
        } else if key < self.key {
            Self::insert_into_optional(&mut self.left, key, value);

            assert(!Node::<V>::optional_as_map(self.right).dom().contains(key));
        } else {
            Self::insert_into_optional(&mut self.right, key, value);

            assert(!Node::<V>::optional_as_map(self.left).dom().contains(key));
        }
    }
}

impl<V> TreeMap<V> {
    pub fn insert(&mut self, key: u64, value: V)
        ensures
            self@ == old(self)@.insert(key, value)
    {
        proof { use_type_invariant(&*self); }
        let mut root = None;
        std::mem::swap(&mut root, &mut self.root);
        Node::<V>::insert_into_optional(&mut root, key, value);
        self.root = root;
    }
}

impl<V> Node<V> {
    fn delete_from_optional(node: &mut Option<Box<Node<V>>>, key: u64)
        requires
            old(node).is_some() ==> old(node).unwrap().well_formed(),
        ensures
            node.is_some() ==> node.unwrap().well_formed(),
            Node::<V>::optional_as_map(*node) =~= Node::<V>::optional_as_map(*old(node)).remove(key)
    {
        if node.is_some() {
            let mut tmp = None;
            std::mem::swap(&mut tmp, node);
            let mut boxed_node = tmp.unwrap();

            if key == boxed_node.key {
                assert(!Node::<V>::optional_as_map(boxed_node.left).dom().contains(key));
                assert(!Node::<V>::optional_as_map(boxed_node.right).dom().contains(key));

                if boxed_node.left.is_none() {
                    *node = boxed_node.right;
                } else {
                    if boxed_node.right.is_none() {
                        *node = boxed_node.left;
                    } else {
                        let (popped_key, popped_value) = Node::<V>::delete_rightmost(&mut boxed_node.left);
                        boxed_node.key = popped_key;
                        boxed_node.value = popped_value;
                        *node = Some(boxed_node);
                    }
                }
            } else if key < boxed_node.key {
                assert(!Node::<V>::optional_as_map(boxed_node.right).dom().contains(key));
                Node::<V>::delete_from_optional(&mut boxed_node.left, key);
                *node = Some(boxed_node);
            } else {
                assert(!Node::<V>::optional_as_map(boxed_node.left).dom().contains(key));
                Node::<V>::delete_from_optional(&mut boxed_node.right, key);
                *node = Some(boxed_node);
            }
        }
    }

    fn delete_rightmost(node: &mut Option<Box<Node<V>>>) -> (popped: (u64, V))
        requires
            old(node).is_some(),
            old(node).unwrap().well_formed(),
        ensures
            node.is_some() ==> node.unwrap().well_formed(),
            Node::<V>::optional_as_map(*node) =~= Node::<V>::optional_as_map(*old(node)).remove(popped.0),
            Node::<V>::optional_as_map(*old(node)).dom().contains(popped.0),
            Node::<V>::optional_as_map(*old(node))[popped.0] == popped.1,
            forall |elem| Node::<V>::optional_as_map(*old(node)).dom().contains(elem) ==> popped.0 >= elem,
    {
        let mut tmp = None;
        std::mem::swap(&mut tmp, node);
        let mut boxed_node = tmp.unwrap();

        if boxed_node.right.is_none() {
            *node = boxed_node.left;
            assert(Node::<V>::optional_as_map(boxed_node.right) =~= Map::empty());
            assert(!Node::<V>::optional_as_map(boxed_node.left).dom().contains(boxed_node.key));
            return (boxed_node.key, boxed_node.value);
        } else {
            let (popped_key, popped_value) = Node::<V>::delete_rightmost(&mut boxed_node.right);
            assert(!Node::<V>::optional_as_map(boxed_node.left).dom().contains(popped_key));
            *node = Some(boxed_node);
            return (popped_key, popped_value);
        }
    }
}

impl<V> TreeMap<V> {
    pub fn delete(&mut self, key: u64)
        ensures
            self@ == old(self)@.remove(key)
    {
        proof { use_type_invariant(&*self); }
        let mut root = None;
        std::mem::swap(&mut root, &mut self.root);
        Node::<V>::delete_from_optional(&mut root, key);
        self.root = root;
    }
}

impl<V> Node<V> {
    fn get_from_optional(node: &Option<Box<Node<V>>>, key: u64) -> Option<&V>
        requires node.is_some() ==> node.unwrap().well_formed(),
        returns (match node {
            Some(node) => (if node.as_map().dom().contains(key) { Some(&node.as_map()[key]) } else { None }),
            None => None,
        }),
    {
        match node {
            None => None,
            Some(node) => {
                node.get(key)
            }
        }
    }

    fn get(&self, key: u64) -> Option<&V>
        requires self.well_formed(),
        returns (if self.as_map().dom().contains(key) { Some(&self.as_map()[key]) } else { None })
    {
        if key == self.key {
            Some(&self.value)
        } else if key < self.key {
            proof { assert(!Node::<V>::optional_as_map(self.right).dom().contains(key)); }
            Self::get_from_optional(&self.left, key)
        } else {
            proof { assert(!Node::<V>::optional_as_map(self.left).dom().contains(key)); }
            Self::get_from_optional(&self.right, key)
        }
    }
}

impl<V> TreeMap<V> {
    pub fn get(&self, key: u64) -> Option<&V>
        returns (if self@.dom().contains(key) { Some(&self@[key]) } else { None })
    {
        proof { use_type_invariant(&*self); }
        Node::<V>::get_from_optional(&self.root, key)
    }
}

fn test() {
    let mut tree_map = TreeMap::<bool>::new();
    tree_map.insert(17, false);
    tree_map.insert(18, false);
    tree_map.insert(17, true);

    assert(tree_map@ == map![17u64 => true, 18u64 => false]);

    tree_map.delete(17);

    assert(tree_map@ == map![18u64 => false]);

    let elem17 = tree_map.get(17);
    let elem18 = tree_map.get(18);
    assert(elem17.is_none());
    assert(elem18 == Some(&false));

    test2(tree_map);
}

fn test2(tree_map: TreeMap<bool>) {
    let mut tree_map = tree_map;
    tree_map.insert(25, true);
    tree_map.insert(100, true);
}


}

Version with generic key type and Clone implementation

use vstd::prelude::*;

verus!{

pub enum Cmp { Less, Equal, Greater }

pub trait TotalOrdered : Sized {
    spec fn le(self, other: Self) -> bool;

    proof fn reflexive(x: Self)
        ensures Self::le(x, x);

    proof fn transitive(x: Self, y: Self, z: Self)
        requires Self::le(x, y), Self::le(y, z),
        ensures Self::le(x, z);

    proof fn antisymmetric(x: Self, y: Self)
        requires Self::le(x, y), Self::le(y, x),
        ensures x == y;

    proof fn total(x: Self, y: Self)
        ensures Self::le(x, y) || Self::le(y, x);

    fn compare(&self, other: &Self) -> (c: Cmp)
        ensures (match c {
            Cmp::Less => self.le(*other) && self != other,
            Cmp::Equal => self == other,
            Cmp::Greater => other.le(*self) && self != other,
        });
}

struct Node<K: TotalOrdered, V> {
    key: K,
    value: V,
    left: Option<Box<Node<K, V>>>,
    right: Option<Box<Node<K, V>>>,
}

pub struct TreeMap<K: TotalOrdered, V> {
    root: Option<Box<Node<K, V>>>,
}

impl<K: TotalOrdered, V> Node<K, V> {
    spec fn optional_as_map(node_opt: Option<Box<Node<K, V>>>) -> Map<K, V>
        decreases node_opt,
    {
        match node_opt {
            None => Map::empty(),
            Some(node) => node.as_map(),
        }
    }

    pub closed spec fn as_map(self) -> Map<K, V>
        decreases self,
    {
        Node::<K, V>::optional_as_map(self.left)
          .union_prefer_right(Node::<K, V>::optional_as_map(self.right))
          .insert(self.key, self.value)
    }
}

impl<K: TotalOrdered, V> TreeMap<K, V> {
    pub closed spec fn as_map(self) -> Map<K, V> {
        Node::<K, V>::optional_as_map(self.root)
    }
}

impl<K: TotalOrdered, V> View for TreeMap<K, V> {
    type V = Map<K, V>;

    open spec fn view(&self) -> Map<K, V> {
        self.as_map()
    }
}

impl<K: TotalOrdered, V> Node<K, V> {
    pub closed spec fn well_formed(self) -> bool
        decreases self
    {
        &&& (forall |elem| #[trigger] Node::<K, V>::optional_as_map(self.left).dom().contains(elem) ==> elem.le(self.key) && elem != self.key)
        &&& (forall |elem| #[trigger] Node::<K, V>::optional_as_map(self.right).dom().contains(elem) ==> self.key.le(elem) && elem != self.key)
        &&& (match self.left {
            Some(left_node) => left_node.well_formed(),
            None => true,
        })
        &&& (match self.right {
            Some(right_node) => right_node.well_formed(),
            None => true,
        })
    }
}

impl<K: TotalOrdered, V> TreeMap<K, V> {
    #[verifier::type_invariant]
    spec fn well_formed(self) -> bool {
        match self.root {
            Some(node) => node.well_formed(),
            None => true, // empty tree always well-formed
        }
    }
}

impl<K: TotalOrdered, V> TreeMap<K, V> {
    pub fn new() -> (s: Self)
        ensures
            s@ == Map::<K, V>::empty(),
    {
        TreeMap::<K, V> { root: None }
    }
}

impl<K: TotalOrdered, V> Node<K, V> {
    fn insert_into_optional(node: &mut Option<Box<Node<K, V>>>, key: K, value: V)
        requires
            old(node).is_some() ==> old(node).unwrap().well_formed(),
        ensures
            node.is_some() ==> node.unwrap().well_formed(),
            Node::<K, V>::optional_as_map(*node) =~= Node::<K, V>::optional_as_map(*old(node)).insert(key, value)
    {
        if node.is_none() {
            *node = Some(Box::new(Node::<K, V> {
                key: key,
                value: value,
                left: None,
                right: None,
            }));
        } else {
            let mut tmp = None;
            std::mem::swap(&mut tmp, node);
            let mut boxed_node = tmp.unwrap();

            (&mut *boxed_node).insert(key, value);

            *node = Some(boxed_node);
        }
    }

    fn insert(&mut self, key: K, value: V)
        requires
            old(self).well_formed(),
        ensures
            self.well_formed(),
            self.as_map() =~= old(self).as_map().insert(key, value),
    {
        match key.compare(&self.key) {
            Cmp::Equal => {
                self.value = value;

                assert(!Node::<K, V>::optional_as_map(self.left).dom().contains(key));
                assert(!Node::<K, V>::optional_as_map(self.right).dom().contains(key));
            }
            Cmp::Less => {
                Self::insert_into_optional(&mut self.left, key, value);

                proof {
                    if self.key.le(key) {
                        TotalOrdered::antisymmetric(self.key, key);
                    }
                    assert(!Node::<K, V>::optional_as_map(self.right).dom().contains(key));
                }
            }
            Cmp::Greater => {
                Self::insert_into_optional(&mut self.right, key, value);

                proof {
                    if key.le(self.key) {
                        TotalOrdered::antisymmetric(self.key, key);
                    }
                    assert(!Node::<K, V>::optional_as_map(self.left).dom().contains(key));
                }
            }
        }
    }
}

impl<K: TotalOrdered, V> TreeMap<K, V> {
    pub fn insert(&mut self, key: K, value: V)
        ensures
            self@ == old(self)@.insert(key, value)
    {
        proof { use_type_invariant(&*self); }
        let mut root = None;
        std::mem::swap(&mut root, &mut self.root);
        Node::<K, V>::insert_into_optional(&mut root, key, value);
        self.root = root;
    }
}

impl<K: TotalOrdered, V> Node<K, V> {
    fn delete_from_optional(node: &mut Option<Box<Node<K, V>>>, key: K)
        requires
            old(node).is_some() ==> old(node).unwrap().well_formed(),
        ensures
            node.is_some() ==> node.unwrap().well_formed(),
            Node::<K, V>::optional_as_map(*node) =~= Node::<K, V>::optional_as_map(*old(node)).remove(key)
    {
        if node.is_some() {
            let mut tmp = None;
            std::mem::swap(&mut tmp, node);
            let mut boxed_node = tmp.unwrap();

            match key.compare(&boxed_node.key) {
                Cmp::Equal => {
                    assert(!Node::<K, V>::optional_as_map(boxed_node.left).dom().contains(key));
                    assert(!Node::<K, V>::optional_as_map(boxed_node.right).dom().contains(key));
                    assert(boxed_node.right.is_some() ==> boxed_node.right.unwrap().well_formed());
                    assert(boxed_node.left.is_some() ==> boxed_node.left.unwrap().well_formed());

                    if boxed_node.left.is_none() {
                        *node = boxed_node.right;
                    } else {
                        if boxed_node.right.is_none() {
                            *node = boxed_node.left;
                        } else {
                            let (popped_key, popped_value) = Node::<K, V>::delete_rightmost(&mut boxed_node.left);
                            boxed_node.key = popped_key;
                            boxed_node.value = popped_value;
                            *node = Some(boxed_node);

                            proof {
                                assert forall |elem| #[trigger] Node::<K, V>::optional_as_map(node.unwrap().right).dom().contains(elem) implies node.unwrap().key.le(elem) && elem != node.unwrap().key
                                by {
                                    TotalOrdered::transitive(node.unwrap().key, key, elem);
                                    if elem == node.unwrap().key {
                                        TotalOrdered::antisymmetric(elem, key);
                                    }
                                }
                            }
                        }
                    }
                }
                Cmp::Less => {
                    proof {
                        if Node::<K, V>::optional_as_map(boxed_node.right).dom().contains(key) {
                            TotalOrdered::antisymmetric(boxed_node.key, key);
                            assert(false);
                        }
                    }
                    Node::<K, V>::delete_from_optional(&mut boxed_node.left, key);
                    *node = Some(boxed_node);
                }
                Cmp::Greater => {
                    proof {
                        if Node::<K, V>::optional_as_map(boxed_node.left).dom().contains(key) {
                            TotalOrdered::antisymmetric(boxed_node.key, key);
                            assert(false);
                        }
                    }
                    Node::<K, V>::delete_from_optional(&mut boxed_node.right, key);
                    *node = Some(boxed_node);
                }
            }
        }
    }

    fn delete_rightmost(node: &mut Option<Box<Node<K, V>>>) -> (popped: (K, V))
        requires
            old(node).is_some(),
            old(node).unwrap().well_formed(),
        ensures
            node.is_some() ==> node.unwrap().well_formed(),
            Node::<K, V>::optional_as_map(*node) =~= Node::<K, V>::optional_as_map(*old(node)).remove(popped.0),
            Node::<K, V>::optional_as_map(*old(node)).dom().contains(popped.0),
            Node::<K, V>::optional_as_map(*old(node))[popped.0] == popped.1,
            forall |elem| #[trigger] Node::<K, V>::optional_as_map(*old(node)).dom().contains(elem) ==> elem.le(popped.0),
    {
        let mut tmp = None;
        std::mem::swap(&mut tmp, node);
        let mut boxed_node = tmp.unwrap();

        if boxed_node.right.is_none() {
            *node = boxed_node.left;
            proof {
                assert(Node::<K, V>::optional_as_map(boxed_node.right) =~= Map::empty());
                assert(!Node::<K, V>::optional_as_map(boxed_node.left).dom().contains(boxed_node.key));
                TotalOrdered::reflexive(boxed_node.key);
            }
            return (boxed_node.key, boxed_node.value);
        } else {
            let (popped_key, popped_value) = Node::<K, V>::delete_rightmost(&mut boxed_node.right);
            proof {
                if Node::<K, V>::optional_as_map(boxed_node.left).dom().contains(popped_key) {
                    TotalOrdered::antisymmetric(boxed_node.key, popped_key);
                    assert(false);
                }
                assert forall |elem| #[trigger] Node::<K, V>::optional_as_map(*old(node)).dom().contains(elem) implies elem.le(popped_key)
                by {
                    if elem.le(boxed_node.key) {
                        TotalOrdered::transitive(elem, boxed_node.key, popped_key);
                    }
                }
            }
            *node = Some(boxed_node);
            return (popped_key, popped_value);
        }
    }
}

impl<K: TotalOrdered, V> TreeMap<K, V> {
    pub fn delete(&mut self, key: K)
        ensures
            self@ == old(self)@.remove(key),
    {
        proof { use_type_invariant(&*self); }
        let mut root = None;
        std::mem::swap(&mut root, &mut self.root);
        Node::<K, V>::delete_from_optional(&mut root, key);
        self.root = root;
    }
}

impl<K: TotalOrdered, V> Node<K, V> {
    fn get_from_optional(node: &Option<Box<Node<K, V>>>, key: K) -> Option<&V>
        requires node.is_some() ==> node.unwrap().well_formed(),
        returns (match node {
            Some(node) => (if node.as_map().dom().contains(key) { Some(&node.as_map()[key]) } else { None }),
            None => None,
        }),
    {
        match node {
            None => None,
            Some(node) => {
                node.get(key)
            }
        }
    }

    fn get(&self, key: K) -> Option<&V>
        requires self.well_formed(),
        returns (if self.as_map().dom().contains(key) { Some(&self.as_map()[key]) } else { None })
    {
        match key.compare(&self.key) {
            Cmp::Equal => {
                Some(&self.value)
            }
            Cmp::Less => {
                proof {
                    if Node::<K, V>::optional_as_map(self.right).dom().contains(key) {
                        TotalOrdered::antisymmetric(self.key, key);
                        assert(false);
                    }
                    assert(key != self.key);
                    assert((match self.left {
                            Some(node) => (if node.as_map().dom().contains(key) { Some(&node.as_map()[key]) } else { None }),
                            None => None,
                        }) == (if self.as_map().dom().contains(key) { Some(&self.as_map()[key]) } else { None }));
                }
                Self::get_from_optional(&self.left, key)
            }
            Cmp::Greater => {
                proof {
                    if Node::<K, V>::optional_as_map(self.left).dom().contains(key) {
                        TotalOrdered::antisymmetric(self.key, key);
                        assert(false);
                    }
                    assert(key != self.key);
                    assert((match self.right {
                            Some(node) => (if node.as_map().dom().contains(key) { Some(&node.as_map()[key]) } else { None }),
                            None => None,
                        }) == (if self.as_map().dom().contains(key) { Some(&self.as_map()[key]) } else { None }));
                }
                Self::get_from_optional(&self.right, key)
            }
        }
    }
}

impl<K: TotalOrdered, V> TreeMap<K, V> {
    pub fn get(&self, key: K) -> Option<&V>
        returns (if self@.dom().contains(key) { Some(&self@[key]) } else { None })
    {
        proof { use_type_invariant(&*self); }
        Node::<K, V>::get_from_optional(&self.root, key)
    }
}

impl<K: Copy + TotalOrdered, V: Clone> Clone for Node<K, V> {
    fn clone(&self) -> (res: Self)
        ensures
            self.well_formed() ==> res.well_formed(),
            self.as_map().dom() =~= res.as_map().dom(),
            forall |key| #[trigger] res.as_map().dom().contains(key) ==>
                call_ensures(V::clone, (&self.as_map()[key],), res.as_map()[key])
    {
        // TODO(fixme): Assigning V::clone to a variable is a hack needed to work around
        // this issue: https://github.com/verus-lang/verus/issues/1348
        let v_clone = V::clone;

        let res = Node {
            key: self.key,
            value: v_clone(&self.value),
            // Ordinarily, we would use Option<Node>::clone rather than inlining
            // the case statement here; we write it this way to work around
            // this issue: https://github.com/verus-lang/verus/issues/1346
            left: (match &self.left {
                Some(node) => Some(Box::new((&**node).clone())),
                None => None,
            }),
            right: (match &self.right {
                Some(node) => Some(Box::new((&**node).clone())),
                None => None,
            }),
        };

        proof {
            assert(Node::optional_as_map(res.left).dom() =~= 
                Node::optional_as_map(self.left).dom());
            assert(Node::optional_as_map(res.right).dom() =~= 
                Node::optional_as_map(self.right).dom());
        }

        return res;
    }
}

impl<K: Copy + TotalOrdered, V: Clone> Clone for TreeMap<K, V> {
    fn clone(&self) -> (res: Self)
        ensures self@.dom() =~= res@.dom(),
            forall |key| #[trigger] res@.dom().contains(key) ==>
                call_ensures(V::clone, (&self@[key],), res@[key])
    {
        proof {
            use_type_invariant(self);
        }

        TreeMap {
            // This calls Option<Node<K, V>>::Clone
            root: self.root.clone(),
        }
    }
}

impl TotalOrdered for u64 {
    open spec fn le(self, other: Self) -> bool { self <= other }

    proof fn reflexive(x: Self) { }
    proof fn transitive(x: Self, y: Self, z: Self) { }
    proof fn antisymmetric(x: Self, y: Self) { }
    proof fn total(x: Self, y: Self) { }

    fn compare(&self, other: &Self) -> (c: Cmp) {
        if *self == *other {
            Cmp::Equal
        } else if *self < *other {
            Cmp::Less
        } else {
            Cmp::Greater
        }
    }
}

fn test() {
    let mut tree_map = TreeMap::<u64, bool>::new();
    tree_map.insert(17, false);
    tree_map.insert(18, false);
    tree_map.insert(17, true);

    assert(tree_map@ == map![17u64 => true, 18u64 => false]);

    tree_map.delete(17);

    assert(tree_map@ == map![18u64 => false]);

    let elem17 = tree_map.get(17);
    let elem18 = tree_map.get(18);
    assert(elem17.is_none());
    assert(elem18 == Some(&false));

    test2(tree_map);
}

fn test2(tree_map: TreeMap<u64, bool>) {
    let mut tree_map = tree_map;
    tree_map.insert(25, true);
    tree_map.insert(100, true);
}

fn test_clone_u32(tree_map: TreeMap<u64, u32>) {
    let tree_map2 = tree_map.clone();
    assert(tree_map2@ =~= tree_map@);
}

struct IntWrapper {
    pub int_value: u32,
}

impl Clone for IntWrapper {
    fn clone(&self) -> (s: Self)
        ensures s == *self
    {
        IntWrapper { int_value: self.int_value }
    }
}

fn test_clone_int_wrapper(tree_map: TreeMap<u64, IntWrapper>) {
    let tree_map2 = tree_map.clone();
    assert(tree_map2@ =~= tree_map@);
}

struct WeirdInt {
    pub int_value: u32,
    pub other: u32,
}

impl Clone for WeirdInt {
    fn clone(&self) -> (s: Self)
        ensures s.int_value == self.int_value
    {
        WeirdInt { int_value: self.int_value, other: 0 }
    }
}

fn test_clone_weird_int(tree_map: TreeMap<u64, WeirdInt>) {
    let tree_map2 = tree_map.clone();

    // assert(tree_map2@ =~= tree_map@); // this would fail

    assert(tree_map2@.dom() == tree_map@.dom());
    assert(forall |k| tree_map@.dom().contains(k) ==>
        tree_map2@[k].int_value == tree_map@[k].int_value);
}


}

Understanding the guarantees of a verified program

A persistent challenge with verified software is understanding what, exactly, is being verified and what guarantees are being given. Verified code doesn’t run in a vacuum; verified code ofter interacts with unverified code, possibly in either direction. This chapter documents technical information need to properly understand how verified code might interact with unverified code.

Assumptions and trusted components

Often times, it’s not possible to verify every line of code and some things need to be assumed. In such cases, the ultimate correctness of the code is dependent not just on verification but on the assumptions being made.

Assumptions can be introduced through the following mechanisms:

  • As assume statement
  • An axiom - any proof function introduced with #[verifier::external_body]
  • An axiomatic specification - any exec function introduced with #[verifier::external_body] or #[verifier::external_fn_specification]
  • #[verifier::external] (See below.)

Types (structs and enums) can also be marked as #[verifier::external_body], though to be pedantic, this does not introduce a new assumption per se. In practice, though, such types are usually associated with additional assumptions to make them useful.

The #[verifier::external] attribute

The #[verifier::external] annotation tells Verus to ignore an item entirely. It can be applied to any item - a function, trait, trait implementation, type, etc.

For many items (functions, types, trait declarations), this does not, on its own, introduce any new “assumptions” about that item. Attempting to call an external function from a verified function, for example, will result in an error from Verus. In practice, a developer will often call an external function (say f) from an external_body function (say g), in which case, the external_body attribute introduces assumptions about g, thus indirectly introducing assumptions about f.

Furthermore, adding #[verifier::external] to a trait implementation requires even more careful consideration, as Verus relies on rustc’s trait-checking for some things, so trait implementations can sometimes affect what code gets accepted or rejected.

For example:

#[verifier::external]
unsafe impl Send for X { }

Memory safety is conditional on verification

Let’s briefly compare and contrast the philosophies of Rust and Verus with regards to memory safety. Memory safety, here, refers to a program being free of any undefined behavior (UB) in its memory access. Both Rust and Verus rely on memory safety being upheld; in turn, they both do a great deal to enforce it. However, they enforce it in different ways.

Rust’s enforcement of memory safety is built around a contract between “safe” and “unsafe” code. The first chapter of the Rustonomicon summarizes the philosophy. In short: any “safe” code (i.e., code free of the unsafe keyword) must be memory safe, enforced by Rust itself via its type-checker and borrow-checker, regardless of user error. However, if any code uses unsafe, it is the responsibility of the programmer to ensure that the program is memory safe—and if the programmer fails to do so, then the behavior of the program is undefined (by definition).

In practice, of course, most code does use unsafe, albeit only indirectly. Most code relies on low-level utilities that can only be implemented with unsafe code, including many from the standard library (e.g., Arc, RefCell, and so on), but also from user-provided crates. In any case, the Rust philosophy is that the providers of these low-level utilities should meet a standard of “unsafe encapsulation.” A programmer interacting using the library only through its safe API (and also not using unsafe code anywhere else) should not be able to exhibit undefined behavior, not even by writing buggy code or using the API is an unintended way. As such, the library implementors need to code defensively against all possible ways the client might use the safe API. When they are successful in this, the clients once again gain the guarantee that they cannot invoke UB without unsafe code.

By contrast, Verus does not have an “unsafe/safe” distinction, nor does it have a notion of unsafe encapsulation. This is because it verifies both memory safety and other forms of correctness through Verus specifications.

Example

Consider, for example, the index operation in Rust’s standard Vec container. If the client calls this function with an index that is not in-range for the vector’s length, then it is likely a bug on the part of the client. However, the index operation is part of the safe API, and therefore it must be robust to such things, and it can never attempt to read out-of-bounds memory. Therefore, the implementation of this operation has to do a bounds check (panicking if the bounds check fails).

On the other hand, consider this (possible) implementation of index for Verus’s Vec collection:

impl<A> Vec<A> {
    #[verifier::external_body]
    pub fn index(&self, i: usize) -> (r: &A)
        requires
            i < self.len(),
        ensures
            *r === self[i as int],
    {
        unsafe { self.vec.get_unchecked(i) }
    }
}

Unlike Rust’s index, this implementation has no bounds checks, and it exhibits UB if called for a value of i that is out-of-bounds. Therefore, as ordinary Rust, it would not meet the standards of unsafe encapsulation.

However, due to its requires clause, Verus enforces that any call to this function will satisfy the contract and be in-bounds. Therefore, UB cannot occur in a verified Verus program, but type-checking alone is not sufficient to ensure this.

Conclusion

Rust’s concept of unsafe encapsulation means that programmers writing in safe Rust can be sure that their programs will be memory safe as long as they type-check and pass the borrow-checker, even if their code otherwise has bugs.

In Verus, there is no staggered notion of correctness. If the program verifies, then it is memory safe, and it will execute according to all its specifications. If the program fails to verify, then all bets are off.

Calling verified code from unverified code

Of course, the correctness of Verus code depends on meeting the assumptions as provided in its specification. If you call verified code from unverified code, Verus won’t be able to check that these contracts are upheld at each call-site, so the responsibility is on the developer to meet them.

The developer needs to meet these assumptions:

  • Any requires clauses on the function being called
  • Any trait implementation used to meet the function’s trait bounds are implemented according to the trait specifications.

Let me give an example of the latter. Suppose V is the verified source code, which declares a trait Trait and a function with a trait bound, f<T: Trait>. Also suppose Trait has a function trait_fn with an ensures clause.

Now suppose we have unverified source U, which defines a type X and a trait impl impl Trait for X.

Then, in order for U to safely call f, the developer needs to make sure that X::trait_fn correctly meets the ensures specification that V demands.

Requirements on the Drop trait

Note: We hope to simplify or remove this requirement in the future.

Note that the Drop trait has some special considerations. Specifically, Verus treats drop as if it has the following signature:

fn drop(&mut self)
    opens_invariants none
    no_unwind

(See opens_invariants and no_unwind.)

For any verified implementation of Drop, Verus checks that it meets this criterion. For unverified implementations of drop, this onus is on the user to meet this criterion.

Warning

As discussed in the last chapter, the memory safety of a verified program is conditional on verification. Therefore, calling verified code from unverified code could be non-memory-safe if the unverified code fails to uphold these contracts.

IDE Support for Verus

Verus currently has IDE support for VS Code and Emacs.

For VS Code, we require verus-analyzer, our Verus-specific fork of rust-analyzer. To use Verus with VS Code, follow the instructions in the README for verus-analyzer.

For Emacs, we have stand-alone support for Verus. The steps to get started with Emacs are below.

Quickstart Emacs

We support for Verus programming in Emacs through verus-mode.el, a major mode that supports syntax highlighting, verification-on-save, jump-to-definition, and more.

To use verus-mode, the setup can be as simple as configuring .emacs to (i) set verus-home to the path to Verus, and then (ii) load up verus-mode.

For example, if you use use-package, you can clone verus-mode.el into a location that Emacs can load from, and add the following snippet:

(use-package verus-mode
  :init
  (setq verus-home "PATH_TO_VERUS_DIR"))   ; Path to where you've cloned https://github.com/verus-lang/verus

Depending on your specific Emacs setup, your exact installation process for verus-mode.el could vary. Detailed installation steps for various Emacs setups are documented in the Install section on verus-mode.el’s README.

For more details on latest features, key-bindings, and troubleshooting tips, do check out the README for verus-mode.el.

Installing and configuring Singular

Singular must be installed in order to use the integer_ring solver mode.

Steps:

  1. Install Singular

    • To use Singular’s standard library, you need more than just the Singular executable binary. Hence, when possible, we strongly recommend using your system’s package manager. Regardless of the method you select, please install Singular version 4.3.2: other versions are untested, and 4.4.0 is known to be incompatible with Verus. Here are some suggested steps for different platforms.
      • Mac: brew install Singular and set the VERUS_SINGULAR_PATH environment variable when running Verus. (e.g. VERUS_SINGULAR_PATH=/usr/local/bin/Singular). For more options, see Singular’s OS X installation guide.

      • Debian-based Linux: apt-get install singular and set the VERUS_SINGULAR_PATH environment variable when running Verus. (e.g. VERUS_SINGULAR_PATH=/usr/bin/Singular). For more options, see Singular’s Linux installation guide.

      • Windows: See Singular’s Windows installation guide.

  2. Compiling Verus with Singular Support

    • The integer_ring functionality is conditionally compiled when the singular feature is set. To add this feature, add the --features singular flag when you invoke vargo build to compile Verus.

Documentation with Rustdoc

Verus provides a tool to help make Verus specification look nice in rustdoc. To do this, you first run rustdoc on a crate and then run an HTML postprocessor called Verusdoc.

First, make sure verusdoc is built by running vargo build -p verusdoc in the verus/source directory.

Unfortunately, we currently don’t have helpful tooling for running rustdoc with the appropriate dependencies and flags, so you’ll need to set that up manually. Here is an example:

VERUS=/path/to/verus/source

if [ `uname` == "Darwin" ]; then
    DYN_LIB_EXT=dylib
elif [ `uname` == "Linux" ]; then
    DYN_LIB_EXT=so
fi

# Run rustdoc.
# Note the VERUSDOC environment variable.

RUSTC_BOOTSTRAP=1 VERUSDOC=1 rustdoc \
  --extern builtin=$VERUS/target-verus/debug/libbuiltin.rlib \
  --extern builtin_macros=$VERUS/target-verus/debug/libbuiltin_macros.$DYN_LIB_EXT \
  --extern state_machines_macros=$VERUS/target-verus/debug/libstate_machines_macros.$DYN_LIB_EXT \
  --extern vstd=$VERUS/target-verus/debug/libvstd.rlib \
  --edition=2021 \
  --cfg verus_keep_ghost \
  --cfg verus_keep_ghost_body \
  --cfg 'feature="std"' \
  --cfg 'feature="alloc"' \
  '-Zcrate-attr=feature(register_tool)' \
  '-Zcrate-attr=register_tool(verus)' \
  '-Zcrate-attr=register_tool(verifier)' \
  '-Zcrate-attr=register_tool(verusfmt)' \
  --crate-type=lib \
  ./lib.rs

# Run the post-processor.

$VERUS/target/debug/verusdoc

If you run it with a file lib.rs like this:

#![allow(unused_imports)]

use builtin::*;
use builtin_macros::*;
use vstd::prelude::*;

verus!{

/// Computes the max
pub fn compute_max(x: u32, y: u32) -> (max: u32)
    ensures max == (if x > y { x } else { y }),
{
    if x < y {
        y
    } else {
        x
    }
}

}

It will generate rustdoc that looks like this:

Screenshot of a verusdoc example illustrating the inclusion of an ensures clauses

Supported Rust Features

Quick reference for supported Rust features. Note that this list does not include all Verus features, and Verus has many spec/proof features without any standard Rust equivalent—this list only concerns Rust features. See the guide for more information about Verus’ distinction between executable Rust code, specification code, and proof code.

Note that Verus is in active development. If a feature is unsupported, it might be genuinely hard, or it might just be low priority. See the github issues or discussions for information on planned features.

Last Updated: 2024-06-26

Items
Functions, methods, associated functions Supported
Associated constants Not supported
Structs Supported
Enums Supported
Const functions Partially supported
Async functions Not supported
Macros Supported
Type aliases Supported
Const items Partially supported
Static items Partially supported
Struct/enum definitions
Type parameters Supported
Where clauses Supported
Lifetime parameters Supported
Const generics Partially Supported
Custom discriminants Not supported
public / private fields Partially supported
Expressions and Statements
Variables, assignment, mut variables Supported
If, else Supported
patterns, match, if-let, match guards Supported
Block expressions Supported
Items Not supported
loop, while Supported
for Partially supported
? Supported
Async blocks Not supported
await Not supported
Unsafe blocks Supported
& Supported
&mut, place expressions Partially supported
==, != Supported, for certain types
Type cast (as) Partially supported
Compound assigments (+=, etc.) Supported
Array expressions Partially supported (no fill expressions with `const` arguments)
Range expressions Supported
Index expressions Partially supported
Tuple expressions Supported
Struct/enum constructors Supported
Field access Supported
Function and method calls Supported
Closures Supported
Labels, break, continue Supported
Return statements Supported
Integer arithmetic
Arithmetic for unsigned Supported
Arithmetic for signed (+, -, *) Supported
Arithmetic for signed (/, %) Not supported
Bitwise operations (&, |, !, >>, <<) Supported
Arch-dependent types (usize, isize) Supported
Types and standard library functionality
Integer types Supported
bool Supported
Strings Supported
Vec Supported
Option / Result Supported
Floating point Not supported
Slices Supported
Arrays Supported
Pointers Partially supported
References (&) Supported
Mutable references (&mut) Partially supported
Never type Not supported
Function pointer types Not supported
Closure types Supported
Trait objects (dyn) Not supported
impl types Partially supported
Cell, RefCell Not supported (see vstd alternatives)
Iterators Not supported
HashMap Not supported
Smart pointers (Box, Rc, Arc) Supported
Pin Not supported
Hardware intrinsics Not supported
Printing, I/O Not supported
Panic-unwinding Not supported
Traits
User-defined traits Supported
Default implementations Supported
Trait bounds on trait declarations Supported
Traits with type arguments Partially supported
Associated types Partially supported
Generic associated types Partially supported (only lifetimes are supported)
Higher-ranked trait bounds Supported
Clone Supported
Marker traits (Copy, Send, Sync) Supported
Standard traits (Hash, Debug) Not supported
User-defined destructors (Drop) Not supported
Sized (size_of, align_of) Supported
Deref, DerefMut Not supported
Multi-threading
Mutex, RwLock (from standard library) Not supported
Verified lock implementations Supported
Atomics Supported (vstd equivalent)
spawn and join Supported
Interior mutability Supported
Unsafe
Raw pointers Supported (only pointers from global allocator)
Transmute Not supported
Unions Supported
UnsafeCell Supported (vstd equivalent)
Crates and code organization
Multi-crate projects Partially supported
Verified crate + unverified crates Partially supported
Modules Supported
rustdoc Supported

Verus Syntax

The code below illustrates a large swath of Verus’ syntax.

#![allow(unused_imports)]

use builtin::*;
use builtin_macros::*;
use vstd::{modes::*, prelude::*, seq::*, *};

#[verifier::external]
fn main() {}

verus! {

/// functions may be declared exec (default), proof, or spec, which contain
/// exec code, proof code, and spec code, respectively.
///   - exec code: compiled, may have requires/ensures
///   - proof code: erased before compilation, may have requires/ensures
///   - spec code: erased before compilation, no requires/ensures, but may have recommends
/// exec and proof functions may name their return values inside parentheses, before the return type
fn my_exec_fun(x: u32, y: u32) -> (sum: u32)
    requires
        x < 100,
        y < 100,
    ensures
        sum < 200,
{
    x + y
}

proof fn my_proof_fun(x: int, y: int) -> (sum: int)
    requires
        x < 100,
        y < 100,
    ensures
        sum < 200,
{
    x + y
}

spec fn my_spec_fun(x: int, y: int) -> int
    recommends
        x < 100,
        y < 100,
{
    x + y
}

/// exec code cannot directly call proof functions or spec functions.
/// However, exec code can contain proof blocks (proof { ... }),
/// which contain proof code.
/// This proof code can call proof functions and spec functions.
fn test_my_funs(x: u32, y: u32)
    requires
        x < 100,
        y < 100,
{
    // my_proof_fun(x, y); // not allowed in exec code
    // let u = my_spec_fun(x, y); // not allowed exec code
    proof {
        let u = my_spec_fun(x as int, y as int);  // allowed in proof code
        my_proof_fun(u / 2, y as int);  // allowed in proof code
    }
}

/// spec functions with pub or pub(...) must specify whether the body of the function
/// should also be made publicly visible (open function) or not visible (closed function).
pub open spec fn my_pub_spec_fun1(x: int, y: int) -> int {
    // function and body visible to all
    x / 2 + y / 2
}

/* TODO
pub open(crate) spec fn my_pub_spec_fun2(x: u32, y: u32) -> u32 {
    // function visible to all, body visible to crate
    x / 2 + y / 2
}
*/

// TODO(main_new) pub(crate) is not being handled correctly
// pub(crate) open spec fn my_pub_spec_fun3(x: int, y: int) -> int {
//     // function and body visible to crate
//     x / 2 + y / 2
// }
pub closed spec fn my_pub_spec_fun4(x: int, y: int) -> int {
    // function visible to all, body visible to module
    x / 2 + y / 2
}

pub(crate) closed spec fn my_pub_spec_fun5(x: int, y: int) -> int {
    // function visible to crate, body visible to module
    x / 2 + y / 2
}

/// Recursive functions must have decreases clauses so that Verus can verify that the functions
/// terminate.
fn test_rec(x: u64, y: u64)
    requires
        0 < x < 100,
        y < 100 - x,
    decreases x,
{
    if x > 1 {
        test_rec(x - 1, y + 1);
    }
}

/// Multiple decreases clauses are ordered lexicographically, so that later clauses may
/// increase when earlier clauses decrease.
spec fn test_rec2(x: int, y: int) -> int
    decreases x, y,
{
    if y > 0 {
        1 + test_rec2(x, y - 1)
    } else if x > 0 {
        2 + test_rec2(x - 1, 100)
    } else {
        3
    }
}

/// Decreases and recommends may specify additional clauses:
///   - decreases .. "when" restricts the function definition to a condition
///     that makes the function terminate
///   - decreases .. "via" specifies a proof function that proves the termination
///   - recommends .. "when" specifies a proof function that proves the
///     recommendations of the functions invoked in the body
spec fn add0(a: nat, b: nat) -> nat
    recommends
        a > 0,
    via add0_recommends
{
    a + b
}

spec fn dec0(a: int) -> int
    decreases a,
    when a > 0
    via dec0_decreases
{
    if a > 0 {
        dec0(a - 1)
    } else {
        0
    }
}

#[via_fn]
proof fn add0_recommends(a: nat, b: nat) {
    // proof
}

#[via_fn]
proof fn dec0_decreases(a: int) {
    // proof
}

/// variables may be exec, tracked, or ghost
///   - exec: compiled
///   - tracked: erased before compilation, checked for lifetimes (advanced feature, discussed later)
///   - ghost: erased before compilation, no lifetime checking, can create default value of any type
/// Different variable modes may be used in different code modes:
///   - variables in exec code are always exec
///   - variables in proof code are ghost by default (tracked variables must be marked "tracked")
///   - variables in spec code are always ghost
/// For example:
fn test_my_funs2(
    a: u32,  // exec variable
    b: u32,  // exec variable
)
    requires
        a < 100,
        b < 100,
{
    let s = a + b;  // s is an exec variable
    proof {
        let u = a + b;  // u is a ghost variable
        my_proof_fun(u / 2, b as int);  // my_proof_fun(x, y) takes ghost parameters x and y
    }
}

/// assume and assert are treated as proof code even outside of proof blocks.
/// "assert by" may be used to provide proof code that proves the assertion.
#[verifier::opaque]
spec fn f1(i: int) -> int {
    i + 1
}

fn assert_by_test() {
    assert(f1(3) > 3) by {
        reveal(f1);  // reveal f1's definition just inside this block
    }
    assert(f1(3) > 3);
}

/// "assert by" can also invoke specialized provers for bit-vector reasoning or nonlinear arithmetic.
fn assert_by_provers(x: u32) {
    assert(x ^ x == 0u32) by (bit_vector);
    assert(2 <= x && x < 10 ==> x * x > x) by (nonlinear_arith);
}

/// "assert by" provers can also appear on function signatures to select a specific prover
/// for the function body.
proof fn lemma_mul_upper_bound(x: int, x_bound: int, y: int, y_bound: int)
    by (nonlinear_arith)
    requires
        x <= x_bound,
        y <= y_bound,
        0 <= x,
        0 <= y,
    ensures
        x * y <= x_bound * y_bound,
{
}

/// "assert by" can use nonlinear_arith with proof code,
/// where "requires" clauses selectively make facts available to the proof code.
proof fn test5_bound_checking(x: u32, y: u32, z: u32)
    requires
        x <= 0xffff,
        y <= 0xffff,
        z <= 0xffff,
{
    assert(x * z == mul(x, z)) by (nonlinear_arith)
        requires
            x <= 0xffff,
            z <= 0xffff,
    {
        assert(0 <= x * z);
        assert(x * z <= 0xffff * 0xffff);
    }
}

/// The syntax for forall and exists quantifiers is based on closures:
fn test_quantifier() {
    assert(forall|x: int, y: int| 0 <= x < 100 && 0 <= y < 100 ==> my_spec_fun(x, y) >= x);
    assert(my_spec_fun(10, 20) == 30);
    assert(exists|x: int, y: int| my_spec_fun(x, y) == 30);
}

/// "assert forall by" may be used to prove foralls:
fn test_assert_forall_by() {
    assert forall|x: int, y: int| f1(x) + f1(y) == x + y + 2 by {
        reveal(f1);
    }
    assert(f1(1) + f1(2) == 5);
    assert(f1(3) + f1(4) == 9);
    // to prove forall|...| P ==> Q, write assert forall|...| P implies Q by {...}
    assert forall|x: int| x < 10 implies f1(x) < 11 by {
        assert(x < 10);
        reveal(f1);
        assert(f1(x) < 11);
    }
    assert(f1(3) < 11);
}

/// To extract ghost witness values from exists, use choose:
fn test_choose() {
    assume(exists|x: int| f1(x) == 10);
    proof {
        let x_witness = choose|x: int| f1(x) == 10;
        assert(f1(x_witness) == 10);
    }
    assume(exists|x: int, y: int| f1(x) + f1(y) == 30);
    proof {
        let (x_witness, y_witness): (int, int) = choose|x: int, y: int| f1(x) + f1(y) == 30;
        assert(f1(x_witness) + f1(y_witness) == 30);
    }
}

/// To manually specify a trigger to use for the SMT solver to match on when instantiating a forall
/// or proving an exists, use #[trigger]:
fn test_single_trigger1() {
    // Use [my_spec_fun(x, y)] as the trigger
    assume(forall|x: int, y: int| f1(x) < 100 && f1(y) < 100 ==> #[trigger] my_spec_fun(x, y) >= x);
}

fn test_single_trigger2() {
    // Use [f1(x), f1(y)] as the trigger
    assume(forall|x: int, y: int| #[trigger]
        f1(x) < 100 && #[trigger] f1(y) < 100 ==> my_spec_fun(x, y) >= x);
}

/// To manually specify multiple triggers, use #![trigger]:
fn test_multiple_triggers() {
    // Use both [my_spec_fun(x, y)] and [f1(x), f1(y)] as triggers
    assume(forall|x: int, y: int|
        #![trigger my_spec_fun(x, y)]
        #![trigger f1(x), f1(y)]
        f1(x) < 100 && f1(y) < 100 ==> my_spec_fun(x, y) >= x);
}

/// Verus can often automatically choose a trigger if no manual trigger is given.
/// Use the command-line option --triggers to print the chosen triggers.
fn test_auto_trigger1() {
    // Verus automatically chose [my_spec_fun(x, y)] as the trigger.
    // (It considers this safer, i.e. likely to match less often, than the trigger [f1(x), f1(y)].)
    assume(forall|x: int, y: int| f1(x) < 100 && f1(y) < 100 ==> my_spec_fun(x, y) >= x);
}

/// If Verus prints a note saying that it automatically chose a trigger with low confidence,
/// you can supply manual triggers or use #![auto] to accept the automatically chosen trigger.
fn test_auto_trigger2() {
    // Verus chose [f1(x), f1(y)] as the trigger; go ahead and accept that
    assume(forall|x: int, y: int| #![auto] f1(x) < 100 && f1(y) < 100 ==> my_spec_fun(3, y) >= 3);
}

/// &&& and ||| are like && and ||, but have low precedence (lower than all other binary operators,
/// and lower than forall/exists/choose).
/// &&& must appear before each conjunct, rather than between the conjuncts (similarly for |||).
/// &&& must appear directly inside a block or at the end of a block.
spec fn simple_conjuncts(x: int, y: int) -> bool {
    &&& 1 < x
    &&& y > 9 ==> x + y < 50
    &&& x < 100
    &&& y < 100
}

spec fn complex_conjuncts(x: int, y: int) -> bool {
    let b = x < y;
    &&& b
    &&& if false {
        &&& b ==> b
        &&& !b ==> !b
    } else {
        ||| b ==> b
        ||| !b
    }
    &&& false ==> true
}

/// ==> associates to the right, while <== associates to the left.
/// <==> is nonassociative.
/// == is SMT equality.
/// != is SMT disequality.
pub(crate) proof fn binary_ops<A>(a: A, x: int) {
    assert(false ==> true);
    assert(true && false ==> false && false);
    assert(!(true && (false ==> false) && false));
    assert(false ==> false ==> false);
    assert(false ==> (false ==> false));
    assert(!((false ==> false) ==> false));
    assert(false <== false <== false);
    assert(!(false <== (false <== false)));
    assert((false <== false) <== false);
    assert(2 + 2 !== 3);
    assert(a == a);
    assert(false <==> true && false);
}

/// In specs, <=, <, >=, and > may be chained together so that, for example, a <= b < c means
/// a <= b && b < c.  (Note on efficiency: if b is a complex expression,
/// Verus will automatically introduce a temporary variable under the hood so that
/// the expression doesn't duplicate b: {let x_b = b; a <= x_b && x_b < c}.)
proof fn chained_comparisons(i: int, j: int, k: int)
    requires
        0 <= i + 1 <= j + 10 < k + 7,
    ensures
        j < k,
{
}

/// In specs, e@ is an abbreviation for e.view()
/// Many types implement a view() method to get an abstract ghost view of a concrete type.
fn test_views() {
    let mut v: Vec<u8> = Vec::new();
    v.push(10);
    v.push(20);
    proof {
        let s: Seq<u8> = v@;  // v@ is equivalent to v.view()
        assert(s[0] == 10);
        assert(s[1] == 20);
    }
}

/// struct and enum declarations may be declared exec (default), tracked, or ghost,
/// and fields may be declared exec (default), tracked or ghost.
tracked struct TrackedAndGhost<T, G>(tracked T, ghost G);

/// Proof code may manipulate tracked variables directly.
/// Declarations of tracked variables must be explicitly marked as "tracked".
proof fn consume(tracked x: int) {
}

proof fn test_tracked(
    tracked w: int,
    tracked x: int,
    tracked y: int,
    z: int,
) -> tracked TrackedAndGhost<(int, int), int> {
    consume(w);
    let tracked tag: TrackedAndGhost<(int, int), int> = TrackedAndGhost((x, y), z);
    let tracked TrackedAndGhost((a, b), c) = tag;
    TrackedAndGhost((a, b), c)
}

/// Variables in exec code may be exec, ghost, or tracked.
fn test_ghost(x: u32, y: u32)
    requires
        x < 100,
        y < 100,
{
    let ghost u: int = my_spec_fun(x as int, y as int);
    let ghost mut v = u + 1;
    assert(v == x + y + 1);
    proof {
        v = v + 1;  // proof code may assign to ghost mut variables
    }
    let ghost w = {
        let temp = v + 1;
        temp + 1
    };
    assert(w == x + y + 4);
}

/// Variables in exec code may be exec, ghost, or tracked.
/// However, exec function parameters and return values are always exec.
/// In these places, the library types Ghost and Tracked are used
/// to wrap ghost values and tracked values.
/// Ghost and tracked expressions Ghost(expr) and Tracked(expr) create values of type Ghost<T>
/// and Tracked<T>, where expr is treated as proof code whose value is wrapped inside Ghost or Tracked.
/// The view x@ of a Ghost or Tracked x is the ghost or tracked value inside the Ghost or Tracked.
fn test_ghost_wrappers(x: u32, y: Ghost<u32>)
    requires
        x < 100,
        y@ < 100,
{
    // Ghost(...) expressions can create values of type Ghost<...>:
    let u: Ghost<int> = Ghost(my_spec_fun(x as int, y@ as int));
    let mut v: Ghost<int> = Ghost(u@ + 1);
    assert(v@ == x + y@ + 1);
    proof {
        v@ = v@ + 1;  // proof code may assign to the view of exec variables of type Ghost/Tracked
    }
    let w: Ghost<int> = Ghost(
        {
            // proof block that returns a ghost value
            let temp = v@ + 1;
            temp + 1
        },
    );
    assert(w@ == x + y@ + 4);
}

fn test_consume(t: Tracked<int>)
    requires
        t@ <= 7,
{
    proof {
        let tracked x = t.get();
        assert(x <= 7);
        consume(x);
    }
}

/// Ghost(...) and Tracked(...) patterns can unwrap Ghost<...> and Tracked<...> values:
fn test_ghost_unwrap(
    x: u32,
    Ghost(y): Ghost<u32>,
)  // unwrap so that y has typ u32, not Ghost<u32>
    requires
        x < 100,
        y < 100,
{
    // Ghost(u) pattern unwraps Ghost<...> values and gives u and v type int:
    let Ghost(u): Ghost<int> = Ghost(my_spec_fun(x as int, y as int));
    let Ghost(mut v): Ghost<int> = Ghost(u + 1);
    assert(v == x + y + 1);
    proof {
        v = v + 1;  // assign directly to ghost mut v
    }
    let Ghost(w): Ghost<int> = Ghost(
        {
            // proof block that returns a ghost value
            let temp = v + 1;
            temp + 1
        },
    );
    assert(w == x + y + 4);
}

struct S {}

/// Exec code can use "let ghost" and "let tracked" to create local ghost and tracked variables.
/// Exec code can extract individual ghost and tracked values from Ghost and Tracked wrappers
/// with "let ...Ghost(x)..." and "let ...Tracked(x)...".
fn test_ghost_tuple_match(t: (Tracked<S>, Tracked<S>, Ghost<int>, Ghost<int>)) -> Tracked<S> {
    let ghost g: (int, int) = (10, 20);
    assert(g.0 + g.1 == 30);
    let ghost (g1, g2) = g;
    assert(g1 + g2 == 30);
    // b1, b2: Tracked<S> and g3, g4: Ghost<int>
    let (Tracked(b1), Tracked(b2), Ghost(g3), Ghost(g4)) = t;
    Tracked(b2)
}

/// Exec code can Ghost(...) or Tracked(...) unwrapped parameter
/// to create a mutable ghost or tracked parameter:
fn test_ghost_mut(Ghost(g): Ghost<&mut int>)
    ensures
        *g == *old(g) + 1,
{
    proof {
        *g = *g + 1;
    }
}

fn test_call_ghost_mut() {
    let ghost mut g = 10int;
    test_ghost_mut(Ghost(&mut g));
    assert(g == 11);
}

/// Spec functions are not checked for correctness (although they are checked for termination).
/// However, marking a spec function as "spec(checked)" enables lightweight "recommends checking"
/// inside the spec function.
spec(checked) fn my_spec_fun2(x: int, y: int) -> int
    recommends
        x < 100,
        y < 100,
{
    // Because of spec(checked), Verus checks that my_spec_fun's recommends clauses are satisfied here:
    my_spec_fun(x, y)
}

/// Spec functions may omit their body, in which case they are considered
/// uninterpreted (returning an arbitrary value of the return type depending on the input values).
/// This is safe, since spec functions (unlike proof and exec functions) may always
/// return arbitrary values of any type,
/// where the value may be special "bottom" value for otherwise uninhabited types.
spec fn my_uninterpreted_fun1(i: int, j: int) -> int;

spec fn my_uninterpreted_fun2(i: int, j: int) -> int
    recommends
        0 <= i < 10,
        0 <= j < 10,
;

/// Trait functions may have specifications
trait T {
    proof fn my_uninterpreted_fun2(&self, i: int, j: int) -> (r: int)
        requires
            0 <= i < 10,
            0 <= j < 10,
        ensures
            i <= r,
            j <= r,
    ;
}

enum ThisOrThat {
    This(nat),
    That { v: int },
}

proof fn uses_is(t: ThisOrThat) {
    match t {
        ThisOrThat::This(..) => assert(t is This),
        ThisOrThat::That { .. } => assert(t is That),
    }
}

proof fn uses_arrow_matches_1(t: ThisOrThat)
    requires
        t is That ==> t->v == 3,
        t is This ==> t->0 == 4,
{
    assert(t matches ThisOrThat::This(k) ==> k == 4);
    assert(t matches ThisOrThat::That { v } ==> v == 3);
}

proof fn uses_arrow_matches_2(t: ThisOrThat)
    requires
        t matches ThisOrThat::That { v: a } && a == 3,
{
    assert(t is That && t->v == 3);
}

proof fn uses_spec_has(s: Set<int>, ms: vstd::multiset::Multiset<int>)
    requires
        s has 3,
        ms has 4,
{
    assert(s has 3);
    assert(s has 3 == s has 3);
    assert(ms has 4);
    assert(ms has 4 == ms has 4);
}

} // verus!

Variable modes

In addition to having three function modes, Verus has three variable modes: exec, tracked, and ghost. Only exec variables exist in the compiled code, while ghost and tracked variables are “erased” from the compiled code.

See this tutorial page for an introduction to the concept of modes. The tracked mode is an advanced feature, and is discussed more in the concurrency guide.

Variable modes and function modes

Which variables are allowed depends on the expression mode, according to the following table:

Default variable modeghost variablestracked variablesexec variables
spec codeghostyes
proof codeghostyesyes
exec codeexecyesyesyes

Although exec code allows variables of any mode, there are some restrictions; see below.

Using tracked and ghost variables from a proof function.

By default, any variable in a proof function has ghost mode. Parameters, variables, and return values may be marked tracked. For example:

fn some_proof_fn(tracked param: Foo) -> (tracked ret: RetType) {
    let tracked x = ...;
}

For return values, the tracked keyword can only apply to the entire return type. It is not possible to selectively apply tracked to individual elements of a tuple, for example.

To mix-and-match tracked and ghost data, there are a few possibilities. First, you can create a struct marked tracked, which individual fields either marked ghost or tracked.

Secondly, you can use the Tracked and Ghost types from Verus’s builtin library to create tuples like (Tracked<X>, Ghost<Y>). These support pattern matching:

proof fn some_call() -> (tracked ret: (Tracked<X>, Ghost<Y>)) { ... }

proof fn example() {
    // The lower-case `tracked` keyword is used to indicate the right-hand side
    // has `proof` mode, in order to allow the `tracked` call.
    // The upper-case `Tracked` and `Ghost` are used in the pattern matching to unwrap
    // the `X` and `Y` objects.
    let tracked (Tracked(x), Ghost(y)) = some_call();
}

Using tracked and ghost variables from an exec function.

Variables in exec code may be marked tracked or ghost. These variables will be erased when the code is compiled. However, there are some restrictions. In particular, variables marked tracked or ghost may be declared anywhere in an exec block. However, such variables may only be assigned to from inside a proof { ... } block.

fn some_exec_fn() {
    let ghost mut x = 5; // this is allowed

    proof {
        x = 7; // this is allowed
    }

    x = 9; // this is not allowed
}

Futhermore:

  • Arguments and return values for an exec function must be exec mode.

  • Struct fields of an exec struct must be exec mode.

To work around these, programs can use the Tracked and Ghost types. Like in proof code, Verus supports pattern-matching for these types.

exec fn example() {
    // Because of the keyword `tracked`, Verus interprets the right-hand side
    // as if it were in a `proof` block.
    let tracked (Tracked(x), Ghost(y)) = some_call();
}

To handle parameters that must be passed via Tracked or Ghost types, you can unwrap them via pattern matching:

exec fn example(Tracked(x): Tracked<X>, Ghost(y): Ghost<Y>) {
    // Use `x` as if it were declared `let tracked x`
    // Use `y` as if it were declared `let tracked y`
}

Cheat sheet

Proof function, take tracked or ghost param

proof fn example(tracked x: X, ghost y: Y)

To call this function from proof code:

proof fn test(tracked x: X, ghost y: Y) {
    example(x, y);
}

To call this function from exec code:

fn test() {
    let tracked x = ...;
    let ghost y = ...;

    // From a proof block:
    proof { example(x, y); }
}

Proof function, return ghost param

proof fn example() -> (ret: Y)

To call this function from proof code:

proof fn test() {
    let y = example();
}

To call this function from exec code:

fn test() {
    let ghost y = example();
}

Proof function, return tracked param

proof fn example() -> (tracked ret: X)

To call this function from proof code:

proof fn test() {
    let tracked y = example();
}

To call this function from exec code:

fn test() {
    // In a proof block:
    proof { let tracked y = example(); }

    // Or outside a proof block:
    let tracked y = example();
}

Proof function, return both a ghost param and tracked param

proof fn example() -> (tracked ret: (Tracked<X>, Ghost<Y>))

To call this function from proof code:

proof fn test() {
    let tracked (Tracked(x), Ghost(y)) = example();
}

To call this function from exec code:

fn test() {
    // In a proof block:
    proof { let tracked (Tracked(x), Ghost(y)) = example(); }

    // or outside a proof block:
    let tracked (Tracked(x), Ghost(y)) = example();
}

Exec function, take a tracked and ghost parameter:

fn example(Tracked(x): Tracked<X>, Ghost(y): Ghost<Y>)

To call this function from exec code:

fn test() {
    let tracked x = ...;
    let ghost y = ...;

    example(Tracked(x), Ghost(y));
}

Exec function, return a tracked and ghost value:

fn example() -> (Tracked<X>, Ghost<Y>)

To call this function from exec code:

fn test() {
    let (Tracked(x), Ghost(y)) = example();
}

Exec function, take a tracked parameter that is a mutable reference:

fn example(Tracked(x): Tracked<&mut X>)

To call this function from exec code:

fn test() {
    let tracked mut x = ...;

    example(Tracked(&mut x));
}

Spec expressions

Many built-in operators are in spec mode, i.e., they can be used in specification expressions. This section discusses those operators.

Rust subset

Much of the spec language looks like a subset of the Rust language, though there are some subtle differences.

Function calls

Only pure function calls are allowed (i.e., calls to other spec functions or functions marked with the when_used_as_spec directive).

Let-assignment

Spec expressions support let-bindings, but not let mut-bindings.

if / if let / match statements

These work as normal.

&& and ||

These work as normal, though as all spec expressions are pure and effectless, there is no notion of “short-circuiting”.

Equality (==)

This is not the same thing as == in exec-mode; see more on ==.

Arithmetic

Arithmetic works a little differently in order to operate with Verus’s int and nat types. See more on arithmetic.

References (&T)

Verus attempts to ignore Box and references as much as possible in spec mode. However, you still needs to satisfy the Rust type-checker, so you may need to insert references (&) or dereferences (*) to satisfy the checker. Verus will ignore these operations however.

Box

Verus special-cases Box along with box operations like Box::new(x) or *box so they may be used in spec mode. Like with references, these operations are ignored, however they are often useful. For example, to create a recursive type you need to satisfy Rust’s sanity checks, which often involves using a Box.

Operator Precedence

OperatorAssociativity
Binds tighter
. ->left
is matchesleft
* / %left
+ -left
<< >>left
&left
^left
|left
!== == != <= < >= >requires parentheses
&&left
||left
==>right
<==left
<==>requires parentheses
..left
=right
closures; forall, exists; chooseright
&&&left
|||left
Binds looser

All operators that are from ordinary Rust have the same precedence-ordering as in ordinary Rust. See also the Rust operator precedence.

Arithmetic in spec code

Note: This reference page is about arithmetic in Verus specification code. This page is does not apply to arithmetic is executable Rust code.

For an introduction to Verus arithmetic, see Integers and arithmetic.

Type widening

In spec code, the results of arithmetic are automatically widened to avoid overflow or wrapping. The types of various operators, given as functions of the input types, are summarized in the below table. Note that in most cases, the types of the inputs are not required to be the same.

operationLHS typeRHS typeresult typenotes
<= < >= >t1t2bool
== !=t1t2bool
+t1t2intexcept for nat + nat
+natnatnat
-t1t2int
*t1t2intexcept for nat * nat
*natnatnat
/ttintfor i8…isize, int
/tttfor u8…usize, nat
%ttt
add(_, _)ttt
sub(_, _)ttt
mul(_, _)ttt
& | ^ttt
<< >>t1t2t1

Definitions: Quotient and remainder

In Verus specifications, / and % are defined by Euclidean division. Euclidean division may differ from the usual Rust / and % operators when operands are negative.

For b != 0, the quotient a / b and remainder a % b are defined as the unique integers q and r such that:

  • b * q + r == a
  • 0 <= r < |b|.

Note that:

  • The remainder a % b is always nonnegative
  • The quotient is “floor division” when b is positive
  • The quotient is “ceiling division” when b is negative

Also note that a / b and a % b are unspecified when b == 0. However, because all spec functions are total, division-by-0 or mod-by-0 are not hard errors.

More advanced arithmetic

The Verus standard library includes the following additional arithmetic functions usable in spec expressions:

Bitwise ops

See bitwise operators.

Bit operators

Definitions

&, |, and ^

These have the usual meaning: bitwise-OR, bitwise-AND, and bitwise-XOR. Verus, like Rust, requires the input operands to be the same type, even in specification code. However, as binary operators defined over the integers, ℤ x ℤ → ℤ, these operations are independent of bitwidth. This is true even for negative operands, as a result of the way two’s complement sign-extension works.

>> and <<

Verus specifications, like Rust, does not require the left and right sides of a shift operator to be the same type. Shift is unspecified when the right-hand side is negative. Unlike in executable code, however, there is no upper bound on the right-hand side.

a << b and and a >> b both have the same type as a.

Right shifts can be defined over the integers ℤ x ℤ → ℤ independently of the input bitwidth.

For <<, however, the result does depend on the input type because a left shift may involve truncation if some bits get shifted “off to the left”. There is no widening to an int (unlike, say, Verus specification +).

Reasoning about bit operators

In Verus’s default prover mode, the definitions of these bitwise operators are not exported. To prove nontrivial facts about bitwise operators, use the bit-vector solver or the compute solver.

Coercion with as

In spec code, any “integer type” may be coerced to any other integer type via as. For the sake of this page, “integer type” means any of the following:

  • i8, i16, i32, i64, i128, isize
  • u8, u16, u32, u64, u128, usize
  • int
  • nat
  • char

Note that this is more permissive than as in Rust exec code. For example, Rust does not permit using as to cast from a u16 to a char, but this is allowed in Verus spec code.

Definition

Verus defines as-casting as follows:

  • Casting to an int is always defined and does not require truncation.
  • Casting to a nat is unspecified if the input value is negative.
  • Casting to a char is unspecified if the input value is outside the allowed char values.
  • Casting to any other finite-size integer type is defined as truncation — taking the lower N bits for the appropriate N, then interpreting the result as a signed or unsigned integer.

Reasoning about truncation

The definition of truncation is not exported in Verus’s default prover mode (i.e., it behaves as if it is unspecified). To reason about truncation, use the bit-vector solver or the compute solver.

Also note that the value of N for usize and isize may be configured with the global directive.

Spec equality (==)

The spec equality operator == is explained in Equality.

Extensional equality (=~= and =~~=)

The extensional equality operators =~= and =~~= are explained in Extensional equality.

Prefix and/or (&&& and |||)

The prefix and/or operators (&&& and |||) are explained in Expressions and operators for specifications.

Chained operators

In spec code, equality and inequality operators can be chained. For example, a <= b < c is equivalent to a <= b && b < c.

Chained inequalities support <, <=, >, >=, and ==, and support sequences of chained operators of arbitrary length.

Implication (==>, <==, and <==>)

The operator P ==> Q, read P implies Q, is equivalent to !P || Q.

This can also be written backwards: Q <== P is equivalent to P ==> Q.

Finally, P <==> Q is equivalent to P == Q. It is sometimes useful for readability, and because <==> has the same syntactic precedence as ==> rather than the precedence of ==.

Spec quantifiers (forall, exists)

Quantifiers are explained in the Quantifiers part of the tutorial. Specifically, forall is explained in forall and triggers and exists is explained in exists and choose.

Such that (choose)

The such-that operator (choose) is explained in exists and choose.

Trigger annotations

To every quantifier expression (forall, exists, choose) in the program, including “implicit” quantifiers such as in broadcast lemmas.

There are many implications of triggers on proof automation that Verus developers should be aware of. See the relevant chapter of the guide, particulary the section on multiple triggers and matching loops.

This page explains the procedure Verus uses to determine these triggers from Verus source code.

Terminology: trigger groups and trigger expressions

Every quantifier has a number of quantifier variables. To control how the solver instantiates these variables, trigger groups are used.

  • To every quantifier, Verus determines a collection of trigger groups.
  • Every trigger group is a collection of trigger expressions.

By necessity, any trigger group is only well-formed if every quantifier variable is used by at least one trigger expression in the group.

Note that:

  • The SMT solver will instantiate any quantifier whenever any trigger group fires.
  • However, a trigger group will only fire if every expression in the group matches.

Therefore:

  • Having more trigger groups makes the quantifier be instantiated more often.
  • A trigger group with more trigger expressions will fire less often.

Selecting trigger groups

Verus determines the collection of trigger groups as follows:

  • Verus finds all applicable #[trigger] and #[trigger(n)] annotations in the body of the quantifier.
    • In the case of nested quantifiers, every #[trigger] or #[trigger(n)] annotation is applicable to exactly one quantifier expression: the innermost quantifier which binds a variable used by the trigger expression.
  • All applicable expressions marked by #[trigger] become a trigger group.
  • All applicable expressions marked by #[trigger(n)] for the same n become a trigger group.
  • Every annotation #![trigger EXPR1, …, EXPRk] at the root of the quantifier expression becomes a trigger group.
  • If, after all of the above, no trigger groups have been identified, Verus may use heuristics to determine the trigger group(s) based on the body of the quantifier expression.
    • If #![all_triggers] is provided, Verus uses an “aggressive” strategy, all trigger groups that can reasonably be inferred as applicable from the body.
    • If #![auto] is provided, Verus uses a “conservative” strategy that selects only on trigger group.
    • If neither #![all_triggers] nor #![auto] are provided, Verus uses the same “conservative” strategy as it does for #![auto].
  • If, after all of the above, Verus is unable to find any trigger groups, it produces an error.

Trigger logging

By default, Verus often prints verbose information about selected triggers in cases where Verus’s heuristics are “un-confident” in the selected trigger groups. You can silence this information on a case-by-case basis using the #![auto] attribute. When #![auto] is applied to a quantifier, this tells Verus that you want the automatically selected triggers even when Verus is un-confident, in which case this logging will be silenced.

The behavior can be configured through the command line:

OptionBehavior
--triggers-silentDo not show automatically chosen triggers
--triggers-selectiveDefault. Show automatically chosen triggers only when heuristics are un-confident, and when #![auto] has not been supplied
--triggersShow all automatically chosen triggers for verified modules
--triggers-verboseShow all automatically chosen triggers for verified modules and imported definitions from other module

The view function @

The expression expr@ is a shorthand for expr.view(). The view() function is a Verus convention for the abstraction of an exec-mode object, usually defined by the View trait. However, the expansion of the @ syntax is purely syntactic, so it does not necessarily correspond to the trait function.

Spec index operator []

In spec expressions, the index operator is treated differently than in exec expressions, where it corresponds to the usual Rust index operator.

Specifically, in a spec expression, the expression expr[i] is a shorthand for expr.spec_index(i). This is a purely syntactic transformation, and there is no particular trait.

For example:

decreases_to!

The expression decreases_to!(e1, e2, …, en => f1, f2, …, fn) is a bool indicating if the left-hand sequence e1, e2, …, en lexicographically-decreases-to the right-hand sequence f1, f2, …, fn

The lexicographic-decreases-to is used to check the decreases measure for spec functions.

See this tutorial chapter for an introductory discussion of lexicographic-decreases.

Definition

We say that e1, e2, …, en lexicographically-decreases-to f1, f2, …, fn if there exists a k where 1 <= k <= n such that:

  • ek decreases-to fk.
  • For each i where 1 <= i < k, ei == fi.

The decreases-to relation is a partial order on all values; values of different types are comparable. The relation permits, but is not necessarily limited to:

  • If x and y are integers, where x > y >= 0, then x decreases-to y.
  • If a is a datatype (struct, tuple, or enum) and f is one of its “potentially recursive” fields, then a decreases-to a.f.
    • For a datatype X, a field is considered “potentially recursive” if it either mentions X or a generic type parameter of X.
  • If f is a spec_fn, then f decreases-to f(i).
  • If s is a Seq, then s decreases-to s[i].
  • If s is a Seq, then s decreases-to s.subrange(i, j) if the given range is strictly smaller than 0 .. s.len().
  • If v is a Vec, then v decreases-to v@.

These axioms are triggered when the relevant expression (e.g., x.f, x->f, s[i], v@) is used as part of a decreases-to expression.

Notes

  1. Tuples are not compared lexicographically; tuples are datatypes, which are compared as explained above, e.g., a decreases_to a.0. Only the “top level” sequences in a decreases_to! expression are compared lexicographically.

  2. Sequences are not compared on len() alone. However, you can always use s.len() as a decreases-measure instead of s.

Examples

proof fn example_decreases_to(s: Seq<int>)
    requires s.len() == 5
{
    assert(decreases_to!(8int => 4int));

    // fails: can't decrease to negative number
    // assert(decreases_to!(8 => -2));

    // Comma-separated elements are treated lexicographically:
    assert(decreases_to!(12int, 8int, 1int => 12int, 4int, 50000int));

    // Datatypes decrease-to their fields:
    let x = Some(8int);
    assert(decreases_to!(x => x->0));

    let y = (true, false);
    assert(decreases_to!(y => y.0));

    // fails: tuples are not treated lexicographically
    // assert(decreases_to!((20, 9) => (11, 15)));

    // sequence decreases-to an element of the sequence
    assert(decreases_to!(s => s[2]));

    // sequence decreases-to a subrange of the sequence
    assert(decreases_to!(s => s.subrange(1, 3)));
}

assert … by

The assert ... by statement is used to encapsulate a proof. For a boolean spec expression, P, one writes:

assert(P) by {
    // ... proof here
}
// ... remainder

Verus will validate the proof and then attempt to use it to prove the P. The contents of the proof, however, will not be included in the context used to prove the remainder. Only P will be introduced into the context for the remainder.

assert forall … by

The assert forall ... by statement is used to write a proof of a forall expression while introducing the quantified variables into the context.

assert forall |idents| P by {
    // ... proof here
}
// ... remainder

Much like an ordinary assert ... by statement, the proof inside the body does not enter the context for the remainder of the proof. Only the forall |idents| P expression enters the context. Furthermore, within the proof body, the variables in the idents may be

Note that the parentheses must be left off, in contrast to other kinds of assert statements.

For convenience, you can use implies to introduce a hypothesis automatically into the proof block:

assert forall |idents| H implies P by {
    // ... proof here
}
// ... remainder

This will make H available in the proof block, so you only have to prove P. In the end, the predicate forall |idents| H ==> P will be proved.

assert … by(bit_vector)

Invoke Verus’s bitvector solver to prove the given predicate. This is particularly useful for bitwise operators and integer arithmetic on finite-width integers. Internally, the solver uses a technique called bit-blasting, which represents each numeric variable by its binary representation as a bit vector, and every operation as a boolean circuit.

assert(P) by(bit_vector);
assert(P) by(bit_vector)
  requires Q;

The prover does not have access to any prior context except that which is given in the requires clause, if provided. If the requires clause is provided, then the bit vector solver attempts to prove Q ==> P. Verus will also check (using its normal solver) that Q holds from the prior proof context.

The expressions P and Q may only contain expressions that the bit solver understands. This includes:

  • Variables of type bool or finite-width integer types (u64, i64, usize, etc.)
    • All free variables are treated symbolically. Even if a variable is defined via a let statement declared outside the bitvector assertion, this definition is not visible to the solver.
  • Integer and boolean literals
  • Non-truncating arithmetic (+, -, *, /, and %)
  • Truncating arithmetic (add, sub, mul functions)
  • Bit operations (&, |, ^, !, <<, >>)
  • Equality and inequality (==, !=, <, >, <=, >=)
  • Boolean operators (&&, ||, ^) and conditional expressions
  • The usize::BITS constant

Internal operation

Verus’s bitvector solver encodes the expression by representing all integers using an SMT “bitvector” type. Most of the above constraints arise because of the fact that Verus has to choose a fixed bitwidth for any given expression.

Note that, although the bitvector solver cannot handle free variables of type int or nat, it can handle other kinds of expressions that are typed int or nat. For example, if x and y have type u64, then x + y has type int, but the Verus bitvector solver knows that x + y is representable with 65 bits.

Handling usize and isize

If the expression uses any symbolic values whose width is architecture-dependent, and the architecture bitwidth has not been specified via a global directive, Verus will generate multiple queries, one for each possible bitwidth (32 bits or 64 bits).

assert … by(nonlinear_arith)

Invoke Z3’s nonlinear solver to prove the given predicate.

assert(P) by(bit_vector);
assert(P) by(bit_vector)
  requires Q;

The solver uses Z3’s theory of nonlinear arithmetic. This can often solve problems that involve multiplication or division of symbolic values. For example, commutativity axioms like a * b == b * a are accessible in this mode.

The prover does not have access to any prior context except that which is given in the requires clause, if provided. If the requires clause is provided, then the bit vector solver attempts to prove Q ==> P. Verus will also check (using its normal solver) that Q holds from the prior proof context.

assert … by(compute) / by(compute_only)

See this section of the tutorial for motivation and an example.

A statement of the form:

assert(P) by(compute_only);

Will evaluate the expression P as far a possible, and Verus accepts the result if it evaluates to the boolean expression true. It unfolds function definitions and evaluates arithmetic expressions. It is capable of some symbolic manipulation, but it does not handle algebraic laws like a + b == b + a, and it works best when evaluating constant expressions.

Note that it will not substitute local variables, instead treating them as symbolic values.

This statement:

assert(P) by(compute);

Will first run the interpreter as above, but if it doesn’t succeed, it will then attempt to finish the problem through the normal solver. So for example, if after expansion P results in a trivial expression like a+b == b+a, then it should be solved with by(compute).

Memoization

The #[verifier::memoize] attribute can be used to mark certain functions for memoizing. This will direct Verus’s internal interpreter to only evaluate the function once for any given combination of arguments. This is useful for functions that would be impractical to evaluate naively, as in this example:

#[verifier::memoize]
spec fn fibonacci(n: nat) -> nat
    decreases n
{
    if n == 0 {
        0
    } else if n == 1 {
        1
    } else {
        fibonacci((n - 2) as nat) + fibonacci((n - 1) as nat)
    }
}

proof fn test_fibonacci() {
    assert(fibonacci(63) == 6557470319842) by(compute_only);
}

reveal, reveal_with_fuel, hide

These attributes control whether and how Verus will unfold the definition of a spec function while solving. For a spec function f:

  • reveal(f) directs Verus to unfold the definition of f when it encounters a use of f.
  • hide(f) directs Verus to treat f as an uninterpreted function without reasoning about its definition.

Technically speaking, Verus handles “function unfolding” by creating axioms of the form forall |x| f(x) == (definition of f(x)). Thus, reveal(f) makes this axiom accessible to the solver, while hide(f) makes this axiom inaccessible.

By default, functions are always revealed when they are in scope. This can be changed by marking the function with the #[verifier::opaque] attribute.

The reveal_with_fuel(f, n) directive is used for recursive functions. The integer n indicates how many times Verus should unfold a recursive function. Limiting the fuel to a finite amount is necessary to avoid trigger loops. The default fuel (absent any reveal_with_fuel directive) is 1.

opens_invariants

The opens_invariants clause may be applied to any proof or exec function.

This indicates the set of names of tracked invariants that may be opened by the function. At this time, it has three forms. See the documentation for open_local_invariant for more information about why Verus enforces these restrictions.

fn example()
    opens_invariants any
{
    // Any invariant may be opened here
}

or:

fn example()
    opens_invariants none
{
    // No invariant may be opened here
}

or:

fn example()
    opens_invariants [ $EXPR1, $EXPR2, ... ]
{
    // Only invariants with names in [ $EXPR1, $EXPR2, ... ] may be opened.
}

Defaults

For exec functions, the default is opens_invariants any.

For proof functions, the default is opens_invariants none.

Unwinding signature

For any exec-mode function, it is possible to specify whether that function may unwind. The allowed forms of the signature are:

  • No signature (default) - This means the function may unwind.
  • no_unwind - This means the function may not unwind.
  • no_unwind when {boolean expression in the input arguments} - If the given condition holds, then the call is guaranteed to not unwind.
    • no_unwind when true is equivalent to no_unwind
    • no_unwind when false is equivalent to the default behavior

By default, a function is allowed to unwind. (Note, though, that Verus does rule out common sources of unwinding, such as integer overflow, even when the function signature technically allows unwinding.)

Example

Suppose you want to write a function which takes an index, and that you want to specify:

  • The function will execute normally if the index is in-bounds
  • The function will unwind otherwise

You might write it like this:

fn get(&self, i: usize) -> (res: T)
    ensures i < self.len() && res == self[i]
    no_unwind when i < self.len()

This effectively says:

  • If i < self.len(), then the function will not unwind.
  • If the function returns normally, then i < self.len() (equivalently, if i >= self.len(), then the function must unwind).

Restrictions with invariants

You cannot unwind when an invariant is open. This restriction is necessary because an unwinding operation does not necessarily abort a program. Rust allows a program to “catch” an unwind, for example, or there might be other threads to continue execution. As a result, Verus cannot permit the program to exit an invariant-block early without restoring the invariant, not even for unwinding.

This is restriction is what enables Verus to rule out exception safety violations.

Drops

If you implement Drop for a type, you are required to give it a signature of no_unwind.

Signature inheritance

Usually, the developer does not write a signature for methods in a trait implementation, as the signatures are inherited from the trait declaration. However, the signature can be modified in limited ways. To ensure soundness of the trait system, Verus has to make sure that the signature on any function must be at least as strong as the corresponding signature on the trait declaration.

  • All requires clauses in a trait declaration are inherited in the trait implementation. The user cannot add additional requires clauses in a trait implementation.
  • All ensures clauses in a trait declaration are inherited in the trait implementation. Furthermore, the user can add additional ensures clauses in the trait implementation.
  • The opens_invariants signature is inherited in the trait implementation and cannot be modified.
  • The unwinding signature is inherited in the trait implementation and cannot be modified.

When a trait function is called, Verus will attempt to statically resolve the function to a particular trait implementation. If this is possible, it uses the possibly-stronger specification from the trait implementation; in all other cases, it uses the generic specification from the trait declaration.

Specifications on FnOnce

For any function object, i.e., a value of any type that implements FnOnce (for example, a named function, or a closure) the signature can be reasoned about generically via the Verus built-in functions call_requires and call_ensures.

  • call_requires(f, args) is a predicate indicating if f is safe to call with the given args. For any non-static call, Verus requires the developer to prove that call_requires(f, args) is satisfied at the call-site.
  • call_ensures(f, args, output) is a predicate indicating if it is possible for f to return the given output when called with args. For any non-static call, Verus will assume that call_ensures(f, args, output) holds after the call-site.
  • At this time, the opens_invariants aspect of the signature is not treated generically. Verus conservatively treats any non-static call as if it might open any invariant.

The args is always given as a tuple (possibly a 0-tuple or 1-tuple).

See the tutorial chapter for examples and more tips.

For any function with a Verus signature (whether a named function or a closure), Verus generates axioms resembling the following:

(user-declared requires clause) ==> call_requires(f, args)
call_ensures(f, args, output) ==> (user-declared ensures clauses)

Using implication (==>) rather than a strict equivalence (<==>) in part to allow flexible signatures in traits. However, our axioms use this form for all functions, not just trait functions. This form reflects the proper way to write specifications for higher-order functions.

decreases … when … via …

The decreases clause is necessary for ensuring termination of recursive and mutually-recursive functions. See this tutorial page for an introduction.

Overview

A collection of functions is mutually recursive if their call graph is strongly connected (i.e., every function in the collection depends, directly or indirectly, on every function in the collection). (A single function that calls itself forms a mutually recursive collection of size 1.) A function is recursive if it is in some mutually recursive collection.

A recursive spec function is required to supply a decreases clause, which takes the form:

decreases EXPR_1, ...
    [ when BOOL_EXPR ]?
    [ via FUNCTION_NAME ]?

The sequence of expressions in the decreases clause is the decreases-measure. The expressions in the decreases-measure and the expression in the when-clause may reference the function’s arguments.

Verus requires that, for any two mutually recursive functions, the number of elements in their decreases-measure must be the same.

The decreases-measure

Verus checks that, when a recursive function calls itself or any other function in its mutually recursive collection, the decreases-measure of the caller decreases-to the decreases-measure of the callee. See the formal definition of decreases-to.

The when clause

If the when clause is supplied, then the given condition may be assumed when proving the decreases properties. However, the function will only be defined when the when clause is true. In other words, something like this:

fn f(...) -> _
    decreases ...
        when condition
{
    body
}

Will be equivalent to this:

fn f(args...) -> _
{
    if condition {
        body
    } else {
        some_unknown_function(args...)
    }
}

The via clause

Sometimes, it may be true that the decreases-measure decreases, but Verus cannot prove it automatically. In this case, the user can supply a lemma to prove the decreases property.

If the via clause is supplied, the FUNCTION_NAME must be the name of a proof function defined in the same module. The number of expressions in the decreases clause must be the same for each function in a mutually recursive collection. This proof function must also be annotated with the #[via_fn] attribute.

It is the job of the proof function to prove the relevant decreases property for each call site.

Type invariants

Structs and enums may be augmented with a type invariant, a boolean predicate indicating well-formedness of a value. The type invariant applies to any exec object or tracked-mode ghost object and does not apply to spec objects.

Type invariants are primarily intended for encapsulating and hiding invariants.

Declaring a type invariant

A type invariant may be declared with the #[verifier::type_invariant] attribute. It can be declared either as a top-level item or in an impl block.

#[verifier::type_invariant]
spec fn type_inv(x: X) -> bool { ... }
impl X {
    #[verifier::type_invariant]
    spec fn type_inv(self) -> bool { ... }
}

It can be inside an impl block and take self, of it can be declared as a top-level item. It can have any name.

The invariant predicate must:

  • Be a spec function of type (X) -> bool or (&X) -> bool, where X is the type the invariant is applied to.
  • Be applied to a datatype (struct or enum) that:
    • Is declared in the same crate
    • Has no fields public outside of the crate

There is no restriction that the type invariant function have the same visibility as the type it is declared for, only that it is visible whenever the type invariant needs to be asserted or assumed (as described below). Since type invariants are intended for encapsulation, it is recommended that it be as private as possible.

Enforcing that the type invariant holds

For any type X with a type invariant, Verus enforces that the predicate always hold for any exec object or tracked-mode ghost object of type X. Therefore, Verus add a proof obligation that the predicate holds:

  • For any constructor expression of X
  • After any assignment to a field of X
  • After any function call that takes a mutable borrow to X

Currently, there is no support for “temporarily breaking” a type invariant, though this capability may be added in the future. This can often be worked around by taking mutable borrows to the fields.

Applying the type invariant

Though the type invariant is enforced automatically, it is not provided to the user automatically. For any object x: X with a type invariant, you can call the builtin pseudo-lemma use_type_invariant to learn that the type invariant holds on x.

use_type_invariant(&x);

The value x must be a tracked or exec variable. This statement is a proof feature, and if it appears in an exec function, it must be in a proof block.

Example

struct X {
    i: u8,
    j: u8,
}

impl X {
    #[verifier::type_invariant]
    spec fn type_inv(self) -> bool {
        self.i <= self.j
    }
}

fn example(x: X) {
    proof {
        use_type_invariant(&x);
    }

    assert(x.i <= x.j); // succeeds
}

fn example_caller() {
    let x = X { i: 20, j: 30 }; // succeeds
    example(x);
}

fn example_caller2() {
    let x = X { i: 30, j: 20 }; // fails
    example(x);
}

Attributes

#![all_triggers]

Applied to a quantifier, and instructs Verus to aggressively select trigger groups for the quantifier. See the trigger specification procedure for more information.

Unlike most Verus attributes, this does not require the verifier:: prefix.

#[verifier::atomic]

The attribute #[verifier::atomic] can be applied to any exec-mode function to indicate that it is “atomic” for the purposes of the atomicity check by open_atomic_invariant!.

Verus checks that the body is indeed atomic, unless the function is also marked external_body, in which case this feature is assumed together with the rest of the function signature.

This attribute is used by vstd’s trusted atomic types.

#![auto]

Applied to a quantifier, and indicates intent for Verus to use heuristics to automatically infer Technically has no effect on verification, but may impact verbose trigger logging. See the trigger specification procedure for more information.

Unlike most Verus attributes, this does not require the verifier:: prefix.

#[verifier::external]

Tells Verus to ignore the given item. Verus will error if any verified code attempts to reference the given item.

This can have nontrivial implications for the TCB of a verified crate; see here.

#[verifier::inline]

The attribute #[verifier::inline] can be applied to any spec-mode function to indicate that that Verus should automatically expand its definition in the STM-LIB encoding.

This has no effect on the semantics of the function but may impact triggering.

#[verifier::loop_isolation]

The attributes #[verifier::loop_isolation(false)] and #[verifier::loop_isolation(true)] can be applied to modules, functions, or individual loops. For any loop, the most specific applicable attribute will take precedence. This attribute impacts the deductions that Verus can make automatically inside the loop body (absent any loop invariants).

  • When set to true: Verus does not automatically infer anything inside the loop body, not even function preconditions.
  • When set the false: Verus automatically makes some facts from outside the loop body available in the loop body. In particular, any assertion outside the loop body that depends only on variables not mutated by the loop body will also be available inside the loop.

#[verifier::memoize]

The attribute #[verifier::memoize] can be applied to any spec-mode function to indicate that the by(compute) and by(compute_only) prover-modes should “memoize” the results of this function.

#[verifier::opaque]

Directs the solver to not automatically reveal the definition of this function. The definition can then be revealed locally via the reveal and reveal_with_fuel directives.

#[verifier::rlimit(n)] and #[verifier::rlimit(infinity)]

The rlimit option can be applied to any function to configure the computation limit applied to the solver for that function.

The default rlimit is 10. The rlimit is roughly proportional to the amount of time taken by the solver before it gives up. The default, 10, is meant to be around 2 seconds.

The rlmit may be set to infinity to remove the limit.

The rlimit can also be configured with the --rlimit command line option.

#[trigger]

Used to manually specify trigger groups for a quantifier. See the trigger specification procedure for more information.

Unlike most Verus attributes, this does not require the verifier:: prefix.

#[verifier::truncate]

The #[verifier::truncate] attribute can be added to expressions to silence recommends-checking regarding out-of-range as-casts.

When casting from one integer type to another, Verus usually inserts recommends-checks that the source value fits into the target type. For example, if x is a u32 and we cast it via x as u8, Verus will add a recommends-check that 0 <= x < 256. However, sometimes truncation is the desired behavior, so #[verifier::truncate] can be used to signal this intent, suppressing the recommends-check.

Note that the attribute is optional, even when truncation behavior is intended. The only effect of the attribute is to silence the recommends-check, which is already elided if the enclosing function body has no legitimate verification errors.

Aside. When truncation is intended, the bit-vector solver mode is often useful for writing proofs about truncation.

#[verifier::type_invariant]

Declares that a spec function is a type invariant for some datatype. See type invariants.

The “global” directive

By default, Verus has no access to layout information, such as the size (std::mem::size_of::<T>()) or alignment (std::mem::align_of::<T>()) of a struct. Such information is often unstable (i.e., it may vary between versions of Rust) or may be platform-dependent (such as the size of usize).

This information can be provided to Verus as needed using the global directive.

For a type T, and integer literals n or m, the global directive is a Verus item that takes the form:

global layout T is size == n, align == m;

Either size or align may be omitted. The global directive both:

  • Exports the axioms size_of::<T>() == n and align_of::<T> == m for use in Verus proofs
  • Creates a “static” check ensuring the given values are actually correct when compiled.

Note that the second check only happens when codegen is run; an “ordinary” verification pass will not perform this check. This ensures that the check is always performed on the correct platform, but it may cause surprises if you spend time on verification without running codegen.

In order to keep the layout stable, it is recommended using Rust attributes like #[repr(C)]. Keep in mind that the Verus verifier gets no information from these attributes. Layout information can only be provided to Verus via the global directive.

With usize and isize

For the integer types usize and isize, the global directive has additional behavior. Specifically, it influences the integer range used in encoding usize and isize types.

For an integer literal n, the directive,

global layout usize is size == n;

Tells Verus that:

  • usize::BITS == 8 * n
  • isize::BITS == 8 * n
  • The integer range for usize (usize::MIN ..= usize::MAX) is 0 ..= 28*n - 1
  • The integer range for isize (isize::MIN ..= isize::MAX) is -28*n-1 ..= 28*n-1 - 1

By default (i.e., in the absence of a global directive regarding usize or isize), Verus assumes that the size is either 4 or 8, i.e., that the integer range is either 32 bits or 64 bits.

Example

global layout usize is size == 4;

fn test(x: usize) {
    // This passes because Verus assumes x is a 32-bit integer:
    assert(x <= 0xffffffff);
    assert(usize::BITS == 32);
}

Static items

Verus supports static items, similar to const items. Unlike const items, though, static items are only usable in exec mode. Note that this requires them to be explicitly marked as exec:

exec static x: u64 = 0;

The reason for this is consistency with const; for const items, the default mode for an unmarked const item is the dual spec-exec mode. However, this mode is not supported for static items; therefore, static items need to be explicitly marked exec.

Note there are some limitations to the current support for static items. Currently, a static item cannot be referenced from a spec expression. This means, for example, that you can’t prove that two uses of the same static item give the same value if those uses are in different functions. We expect this limitation will be lifted in the future.

The char primitive

Citing the Rust documentation on char:

A char is a ‘Unicode scalar value’, which is any ‘Unicode code point’ other than a surrogate code point. This has a fixed numerical definition: code points are in the range 0 to 0x10FFFF, inclusive. Surrogate code points, used by UTF-16, are in the range 0xD800 to 0xDFFF.

Verus treats char similarly to bounded integer primitives like u64 or u32: We represent char as an integer. A char always carries an invariant that it is in the prescribed set of allowed values:

[0, 0xD7ff] ∪ [0xE000, 0x10FFFF]

In spec code, chars can be cast to an from other integer types using as. This is more permissive than exec code, which disallows many of these coercions. As with other coercions, the result may be undefined if the integer being coerced does not fit in the target range.

Unions

Verus supports Rust unions.

Internally, Verus represents unions a lot like enums. However, Rust syntax for accessing unions is different than enums. In Rust, a field of a union is accessed with field access: u.x. Verus allows this operation in exec-mode, and Verus always checks it is well-formed, i.e., it checks that u is the correct “variant”.

In spec-mode, you can use the built-in spec operators is_variant and get_union_field to reason about a union. Both operators refer to the field name via string literals.

  • is_variant(u, "field_name") returns true if u is in the "field_name" variant.
  • get_union_field::<U, T>(u, "field_name") returns a value of type T, where T is the type of "field_name". (Verus will error if T does not match between the union and the generic parameter T of the operator.)

Example

union U {
    x: u8,
    y: bool,
}

fn union_example() {
    let u = U { x: 3 };

    assert(is_variant(u, "x"));
    assert(get_union_field::<U, u8>(u, "x") == 3); 

    unsafe {
        let j = u.x; // this operation is well-formed
        assert(j == 3); 

        let j = u.y; // Verus rejects this operation
    }   
}   

Note on unsafe

The unsafe keyword is needed to satisfy Rust, because Rust treats union field access as an unsafe operation. However, the operation is safe in Verus because Verus is able to check its precondition. See more on how Verus handles memory safety.

Pointers and cells

See the vstd documentation for more information on handling these features.

  • For cells, see PCell
  • For pointers to fixed-sized heap allocations, see PPtr.
  • For general support for *mut T and *const T, see vstd::raw_ptr

Record flag

Sometimes, you might wish to record an execution trace of Verus to share, along with all the necessary dependencies to reproduce an execution. This might be useful for either packaging up your verified project, or to report a Verus bug to the issue tracker.

The --record flag will do precisely this. In particular, to record an execution of Verus (say, verus foo --bar --baz), simply add the --record flag (for example, verus foo --bar --baz --record). This will re-run Verus, and package all the relevant source files, along with the execution output and version information into a zip file (yyyy-mm-dd-hh-mm-ss.zip) in your current directory.