Well-founded Relations and Termination
In an earlier chapter on proofs of termination, we learned about how F* checks that recursive functions terminate. In this chapter, we see how the termination check arises from inductive types and structural recursion. Just as with equality, termination checking, a core feature of F*’s logic and proof system, finds its foundation in inductive types.
For more technical background on this topic, the following resources may be useful:
Thanks to Aseem Rastogi, Chantal Keller, and Catalin Hritcu, for providing some of the F* libraries presented in this chapter.
Well-founded Relations and Accessibility Predicates
A binary relation \(R\) on elements of type \(T\) is well-founded if there is no infinite sequence \(x_0, x_1, x_2, ...\), such that \(x_i~R~x_{i + 1}\), for all \(i\).
As explained earlier, when typechecking a
recursive function f
, F* requires the user to provide a measure,
some function of the arguments of f, and checks that on a recursive
call, the measure of the arguments is related to the measure of the
formal parameters a built-in well-founded relation on F* terms. Since
well-founded relations have no infinite descending chains, every chain
of recursive calls related by such a relation must eventually
terminate. However, this built-in well-founded relation, written
<<
or precedes
, is a derived notion.
In its most primitive form, the well-foundedness of a relation can be
expressed in terms of an inductive type acc
(short for
“accessible”) shown below.
let binrel a = a -> a -> Type
noeq
type acc (#a:Type) (r:binrel a) (x0:a) : Type =
| AccIntro : access_smaller:(x1:a -> r x1 x0 -> acc r x1) -> acc r x0
The type acc
is parameterized by a type a
; a binary relation
r: a -> a -> Type
on a
; and an element of the type
x:a
. Informally, the relation r y x
is provable when y
is “smaller” than x
.
The acc
type has just one constructor AccIntro
, whose only
argument is a function of type y:a -> r y x -> acc r
y
. Intuitively, this says that in order to build an instance of
acc r x0
, you have to provide a function which can build a proof
of acc r x1
for all x1:a
smaller than x0
. The only way to
build such a function is one can avoid infinite regress, is if
the chain x0 r x1 r x2 r ...
, eventually terminates in some xn
such that there are no elements smaller than it according to r
.
In other words, if one can prove acc r x
for all x:a
, then
this precisely captures the condition that there are no infinite
descending r
-related chains in a
, or that r
is
well-founded. This is exactly what the definition below says, where
is_well_founded
is a classical (SMT-automatable) variant of
well_founded
.
let well_founded (#a:Type) (r:binrel a) = x:a -> acc r x
let is_well_founded (#a:Type) (r:binrel a) = forall x. squash (acc r x)
Well-founded Recursion
Given a relation r
and proof of p:acc r x
, one can define a
recursive function on x
whose termination can be established
purely in terms of structural recursion on the proof p
, even
though the function may not itself be structurally recursive on x
.
The combinator fix_F
shown below illustrates this at work:
let rec fix_F (#aa:Type)
(#r:binrel aa)
(#p:(aa -> Type))
(f: (x:aa -> (y:aa -> r y x -> p y) -> p x))
(x0:aa)
(accessible_x0:acc r x0)
: Tot (p x0) (decreases accessible_x0)
= f x0 (fun y r_yx -> fix_F f y (accessible_x0.access_smaller y r_yx))
If f
is a function such that every recursive call in the
definition of f x
is on an argument y
, such that that y
is
smaller than x
according to some relation r
; and if starting
from some argument x0
, we have a proof of accessibility acc r
x0
(i.e., no infinite descending r
-chains starting at x0
),
then the fixpoint of f
can be defined by structural recursion on
the proof of accessible_x0
.
fix_F
is structurally recursive onaccessible_x0
since the recursive call is on an elementh1 y r_yx
, i.e., a child node of the (possibly infinitely branching) tree rooted atAccIntro h1
.
A slightly simpler version of fix_F
is derivable if r
is
well-founded, i.e., r
is accessible for all elements x:a
.
let fix (#aa:Type) (#r:binrel aa) (rwf:well_founded r)
(p:aa -> Type) (f:(x:aa -> (y:aa -> r y x -> p y) -> p x))
(x:aa)
: p x
= fix_F f x (rwf x)
Some Well-founded Relations
We show how to buid some basic well-founded relations here. For
starters, since F* already internalizes that the <
ordering on
natural numbers as part of its termination check, it is easy to prove
that <
is well-founded.
let lt_nat (x y:nat) : Type = x < y == true
let rec wf_lt_nat (x:nat)
: acc lt_nat x
= AccIntro (fun y _ -> wf_lt_nat y)
We can also define combinators to derive well-founded relations from
other well-founded relations. For example, if a relation sub_r
is
a sub-relation of a well-founded relation r
(meaning we have r
x y
whenever we have sub_r x y
), then sub_r
is well-founded
too.
let subrel_wf (#a:Type) (#r #sub_r:binrel a)
(sub_w:(x:a -> y:a -> sub_r x y -> r x y))
(r_wf:well_founded r)
: well_founded sub_r
= let rec aux (x:a)
(acc_r:acc r x)
: Tot (acc sub_r x) (decreases acc_r)
= AccIntro
(fun y sub_r_y_x ->
aux y (acc_r.access_smaller y (sub_w y x sub_r_y_x)))
in
fun x -> aux x (r_wf x)
Another useful combinator derives the well-foundedness of a relation
r: binrel b
if it can be defined as the inverse image under some
function f: a -> b
of some other well-founded relation r:
binrel
.
let inverse_image (#a #b:Type) (r_b:binrel b) (f:a -> b) : binrel a =
fun x y -> r_b (f x) (f y)
let inverse_image_wf (#a #b:Type) (#r_b:binrel b)
(f:a -> b)
(r_b_wf:well_founded r_b)
: well_founded (inverse_image r_b f)
= let rec aux (x:a)
(acc_r_b:acc r_b (f x))
: Tot (acc (inverse_image r_b f) x)
(decreases acc_r_b)
= AccIntro (fun y p -> aux y (acc_r_b.access_smaller (f y) p)) in
fun x -> aux x (r_b_wf (f x))
For example, the >
ordering on negative numbers can be proven
well-founded by defining it as the inverse image of the <
ordering
on natural numbers.
let neg = x:int { x <= 0 }
let negate (x:neg) : nat = -x
let gt_neg : binrel neg = inverse_image lt_nat negate
let wf_gt_neg = inverse_image_wf negate wf_lt_nat
Termination Checking with Custom Well-founded Relations
In the F* library, FStar.LexicographicOrdering
, several other
relations are proven to be well-founded, including the lexicographic
ordering on dependent pairs.
noeq
type lexicographic_order (#a:Type)
(#b:a -> Type)
(r_a:binrel a)
(r_b:(x:a -> binrel (b x)))
: (x:a & b x) -> (x:a & b x) -> Type =
| Left_lex:
x1:a -> x2:a ->
y1:b x1 -> y2:b x2 ->
r_a x1 x2 ->
lexicographic_order r_a r_b (| x1, y1 |) (| x2, y2 |)
| Right_lex:
x:a ->
y1:b x -> y2:b x ->
r_b x y1 y2 ->
lexicographic_order r_a r_b (| x, y1 |) (| x, y2 |)
This order, defined as a binrel (x:a & b x)
, and is paramterized
by a binary relation (r_a
) on a
and a family of relations
(r_b
) on b x
, one for each x:a
. It has two cases:
Left_lex
: The first component of the pair decreases byr_a
, and the second component is irrelevant.
Right_lex
: The first component of the pair is invariant, but the second component decreases byr_b
.
The proof is a little involved (see
FStar.LexicographicOrdering.fst
), but one can prove that it is
well-founded when r_a
and r_b
are themselves well-founded,
i.e.,
val lexicographic_order_wf (#a:Type) (#b:a -> Type)
(#r_a:binrel a)
(#r_b:(x:a -> binrel (b x)))
(wf_a:well_founded r_a)
(wf_b:(x:a -> well_founded (r_b x)))
: well_founded (lexicographic_order r_a r_b)
But, with this well-foundedness proof in hand, we can define recursive functions with our own well-founded orders.
To illustrate, let’s define the ackermann
function again (we saw
it first here), this time using
accessibilty and well-founded relations, rather than the built-in
precedes
relation.
//A type abbreviation for a pair of nats
// we're using dependent pairs, even though there's no real dependence here
let nat_pair = (x:nat & nat)
//Making a lexicographic ordering from a pair of nat ordering
let lex_order_nat_pair
: binrel nat_pair
= lexicographic_order lt_nat (fun _ -> lt_nat)
// The lex order on nat pairs is well-founded, using our general proof
// of lexicographic composition of well-founded orders
let lex_order_nat_pair_wf
: well_founded lex_order_nat_pair
= lexicographic_order_wf wf_lt_nat (fun _ -> wf_lt_nat)
// A utility to introduce lt_nat
let mk_lt_nat (x:nat) (y:nat { x < y })
: lt_nat x y
= let _ : equals (x < y) true = Refl in
()
// A utility to make a lex ordering of nat pairs
let mk_lex_order_nat_pair (xy0:nat_pair)
(xy1:nat_pair {
let (|x0, y0|) = xy0 in
let (|x1, y1|) = xy1 in
x0 < x1 \/ (x0 == x1 /\ y0 < y1)
})
: lex_order_nat_pair xy0 xy1
= let (|x0, y0|) = xy0 in
let (|x1, y1|) = xy1 in
if x0 < x1 then Left_lex x0 x1 y0 y1 (mk_lt_nat x0 x1)
else Right_lex x0 y0 y1 (mk_lt_nat y0 y1)
// Defining ackermann, where `arec` is called for recursive calls
// on pairs that precede xy, lexicographically
let ackermann' (xy: nat_pair)
(arec: (xy':nat_pair -> lex_order_nat_pair xy' xy -> nat))
: nat
= let (| x, y |) = xy in
if x = 0 then y + 1
else if y = 0 then arec (| x - 1, 1 |) (mk_lex_order_nat_pair _ _)
else arec (| x - 1, arec (| x, y - 1|) (mk_lex_order_nat_pair _ _) |)
(mk_lex_order_nat_pair _ _)
// Tie the knot with `fix`
let ackermann : nat_pair -> nat = fix lex_order_nat_pair_wf (fun _ -> nat) ackermann'
This version is way more verbose than the ackermann we saw earlier—but this version demonstrates that F*’s built-in support for the lexicographic orders over the precedes relation is semantically justified by a more primitive model of well-founded relations
To make user-defined well-founded orderings easier to work with, F*
actually provides a variant of the decreases
clause to work with
well-founded relations. For example, one can use the following syntax
to gain from F*’s built-in from SMT automation and termination
checking, with the expressiveness of using ones own well-founded relation.
module L = FStar.LexicographicOrdering
let rec ackermann_wf (m n:nat)
: Tot nat
(decreases
{:well-founded
L.lex (coerce_wf wf_lt_nat) (fun _ -> (coerce_wf wf_lt_nat)) (| m, n |)
})
= if m = 0 then n + 1
else if n = 0 then ackermann_wf (m - 1) 1
else ackermann_wf (m - 1) (ackermann_wf m (n - 1))
To explain the syntax:
decreases {:well-founded p x}
: Here,p
is meant to be an instance forFStar.LexicographicOrdering.well_founded_relation
, applied to some termx
built from the formal parameters in scope.In this case, we use the combinator
L.lex
to build a lexicographic ordering fromwf_lt_nat
(coercing it using a utilitycoerce_wf
to turn the definitions used in our tutorial chapter here to the types expected by theFStar.LexicographicOrdering
library).
We show the coercions below for completeness, though one would not necessarily need them outside the context of this tutorial.
module W = FStar.WellFounded
let rec coerce #a #r #x (p:acc #a r x)
: Tot (W.acc r x) (decreases p)
= W.AccIntro (fun y r -> coerce (p.access_smaller y r))
let coerce_wf #a #r (p: (x:a -> acc r x))
: x:a -> W.acc r x
= fun x -> coerce (p x)