Putting It All Together

To show how spec, proof, and exec code work together, consider the example below of computing the n-th triangular number. We’ll cover this example and the features it uses in more detail in Chapter 4, so for now, let’s focus on the high-level structure of the code.

We use a spec function triangle to mathematically define our specification using natural numbers (nat) and a recursive description. We then write a more efficient iterative implementation as the exec function loop_triangle (recall that exec is the default mode for functions). We connect the correctness of loop_triangle’s return value to our mathematical specification in loop_triangle’s ensures clause.

However, to successfully verify loop_triangle, we need a few more things. First, in executable code, we have to worry about the possibility of arithmetic overflow. To keep things simple here, we add a precondition to loop_triangle saying that the result needs to be less than one million, which means it will certainly fit into a u32.

We also need to translate the knowledge that the final triangle result fits in a u32 into the knowledge that each individual step of computing the result won’t overflow, i.e., that computing sum = sum + idx won’t overflow. We can do this by showing that triangle is monotonic; i.e., if you increase the argument to triangle, the result increases too. Showing this property requires an inductive proof. We cover inductive proofs later; the important thing here is that we can do this proof using a proof function (triangle_is_monotonic). To invoke the results of our proof in our exec implementation, we assert that the new sum fits, and as justification, we an invoke our proof with the relevant arguments. At the call site, Verus will check that the preconditions for triangle_is_monotonic hold and then assume that the postconditions hold.

Finally, our implementation uses a while loop, which means it requires some loop invariants, which we cover later.

spec fn triangle(n: nat) -> nat decreases n, { if n == 0 { 0 } else { n + triangle((n - 1) as nat) } }
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. } }
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 }