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 definition will only be concretely specified 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...)
}
}
Helping Verus prove termination
Sometimes, it may be true that the decreases-measure decreases, but Verus cannot prove it automatically. When this happens, the user can either supply a proof inside the body of the recursive function, or use a separate lemma to prove the decreases property. In the vast majority of cases, writing a proof inside the function body is simpler and easier to read. The main reason to use a separate lemma is if you want to keep your trusted specifications as minimal as possible.
Example.
On its own, Verus cannot see that n
decreases at each recursive call.
We’ll use this example to illustrate the two methods of helping Verus prove termination.
spec fn floor_log2(n: u64) -> int
decreases n
{
if n <= 1 {
0
} else {
floor_log2(n >> 1) + 1
}
}
In order to check that the recursion in floor_log2
terminates, Verus generates a proof obligation
that n > 1 ==> decreases_to!(n => n >> 1)
. (The n > 1
hypothesis stems from the fact that
the recursive call is in the else-block.) Thus we need to show:
n > 1 ==> (n >> 1) < n
Verus cannot prove this automatically.
Writing an proof of termination inside your recursive function
For the purposes of termination checking, you can include a proof {}
block
in the body of your spec function. The proof needs to demonstrate that your
decreases measure really does decrease at each recursive call site.
Note that at present, this proof block can only assist with termination. You cannot use it, for example, to prove additional properties about your function for use elsewhere.
Example Here we supply the fact Verus was missing using Verus’s specialized bit-vector reasoning mode.
spec fn floor_log2(n: u64) -> int
decreases n
{
if n <= 1 {
0
} else {
proof {
assert(n > 1 ==> (n >> 1) < n) by(bit_vector);
}
floor_log2(n >> 1) + 1
}
}
Writing a separate proof of termination with the via
clause
To avoid cluttering your recursive spec function with proof material,
you can add a via PROOF_FUNCTION_NAME
clause to the spec function.
FUNCTION_NAME
must be the name of a proof
function defined in the same module
that takes the same arguments as the recursive function.
This proof function must also be annotated with the #[via_fn]
attribute.
The proof function’s job is to prove the relevant decreases property for each call site. In other words, it needs to show that the decreases measure actually decreases at each recursive call in the spec function’s body.
Example.
In the following definition, we use a via
clause to prove that the decreases-measure
decreases.
spec fn floor_log2_via(n: u64) -> int
decreases n
via floor_log2_decreases_proof
{
if n <= 1 {
0
} else {
floor_log2_via(n >> 1) + 1
}
}
#[via_fn]
proof fn floor_log2_decreases_proof(n: u64) {
assert(n > 1 ==> (n >> 1) < n) by(bit_vector);
}
The proof function floor_log2_decreases_proof
is defined as a via_fn
and is referenced from the via
clause. The body of the proof function contains a proof that n > 1 ==> (n >> 1) < n
(the same proof we used inline above).