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
.