-
Notifications
You must be signed in to change notification settings - Fork 235
Sugar for manipulating connectives in classical logic
- Introduction
-
forall
: introduce, eliminate -
exists
: introduce, eliminate -
==>
: introduce, eliminate -
\/
: introduce, eliminate -
/\
: introduce, eliminate - Derived forms
Working directly with FStar.Squash and FStar.Classical is tedious. Instead, F* provides syntactic sugar to manipulate the five main connectives used in the classical logic fragment of F*. These connectives are:
- forall
- exists
- implies
- and
- or
The syntactic sugar encapsulates the main proof rules for these connectives. As is traditional, the proof rules for each connective come in two flavors.
- Introduction rules: These are rules that allow you to prove a formula beginning with a given connective. Sugar for the introduction rules begin with the
introduce
keyword. The rough syntactic form of an introduction rule is
introduce <some formula>
with <some arguments>
and <some more arguments or proof terms>
The shape of the rule varies depending on the head constructor of the introduced formula.
- Elimination rules: These are rules that allow you to make use of an already proven formula of a given connective, and to derive another formula from it. Sugar for the elimination rules begin with the
eliminate
keyword.
The rough syntactic form of an elimination rule is
eliminate <some formula>
returns <some other formula>
with <some arguments>
and <some more arguments or proof terms>
Again, the shape of the rule varies depending on the head constructor of the eliminated formula.
See this file for examples.
Universally quantified formulas have the shape forall x1...xn. p
.
This formula is itself sugar for a more basic type defined in prims.fst
.
forall (x:t). p = squash (x:t -> GTot (p x))
That is, a forall x. p
is a squashed ghost function, returning a proof of p x
for every x
.
However, proving forall x. t
by directly building a term of this type, while possible, is very cumbersome. This is particularly true when quantifying over multiple arguments. For instance, forall x y. p
would be squash (x:t -> GTot (squash (y:t' -> GTot (p x y)))
.
Likewise, given a proof of forall x. p x
, apply it to a particular v
and obtaining p v
is also cumbersome.
Instead of building these proof terms directly, a typical usage in F* is to rely on the SMT solver to reason with this connective. However, one often needs more control. For the most precise control one can use a tactic, though that requires a different level of expertise. The sugar described below is a middle ground---it allows one to more carefully control how these connectives are manipulated, while still staying within F* rather than stepping to the meta level with a tactic.
Given a lemma proving x:a -> y:b -> z:c -> squash (p x y z)
, one can turn it into a squash (forall x y z. p x y z)
by doing:
let test_forall_intro_1 #a #b #c (p: a -> b -> c -> Type)
(f:(x:a -> y:b -> z:c -> squash (p x y z)))
: squash (forall x y z. p x y z)
= introduce forall x y z. p x y z
with f x y z
Alternatively, working only with Lemma
rather than squash
(which is often more idiomatic), one can write:
let test_forall_intro_2 #a #b #c (p: a -> b -> c -> Type)
(f:(x:a -> y:b -> z:c -> Lemma (p x y z)))
: Lemma (forall x y z. p x y z)
= introduce forall x y z. p x y z
with f x y z
In the syntax
introduce forall x1 .. xn. p
with proof
The binders x1...xn
are in scope for both p
and proof
, where proof : squash p
or, equivalently, proof : Lemma p
.
You can mostly ignore this, but in case you're wondering how this sugar is implemented, in fstar-mode.el, the emacs mode, you can inspect a term after it has been desugared (using C-c C-s C-p). For test_forall_intro_2
the desugaring is:
let test_forall_intro_2 :
p: (_: a -> _: b -> _: c -> Prims.Tot Type0) ->
f: (x: a -> y: b -> z: c -> FStar.Pervasives.Lemma (ensures p x y z))
-> FStar.Pervasives.Lemma (ensures forall (x: a) (y: b) (z: c). p x y z) = fun p f ->
FStar.Classical.Sugar.forall_intro a
(fun x -> forall (y: b) (z: c). p x y z)
(fun x ->
FStar.Classical.Sugar.forall_intro b
(fun y -> forall (z: c). p x y z)
(fun y ->
FStar.Classical.Sugar.forall_intro c
(fun z -> p x y z)
(fun z -> f x y z <: Prims.squash (p x y z))))
Eliminating a universal quantifier amounts to applying it to some specific arguments. Using SMT alone, the default way to instantiate such quantifiers is by relying on SMT patterns and triggers. To directly instantiate a quantifier, one can use the syntax below.
let test_elim_forall_2 (p: nat -> nat -> Type)
: Lemma
(requires forall x y. p x y)
(ensures p 0 1)
= eliminate forall x y. p x y
with 0 1
The general form is
eliminate forall x1...xn. p
with v1...vn
The x1..xn
are in scope for p
only. The instantiations v1..vn
must have the same length as the binders x1..xn
. In a context where forall x1...xn.p
is provable, this piece of syntax proves p[v1..vn/x1..xn]
.
The existential quantifier exists (x:t). p
is defined in prims.fst
as squash (x:t & p x)
, i.e., a squashed dependent pair of a witness x:t
and proof of p x
. As with forall
, working with this directly, especially the n
-ary version is painful---one can rely on the SMT solver or tactics to work with exists
, but again, the sugar below provides a lighter option.
If you have a proof of p va vb vc
, you can turn it into exists x y z. p x y z
using this syntax:
let test_exists_intro_1 #a #b #c (p: a -> b -> c -> Type) va vb vc
(f:squash (p va vb vc))
: squash (exists x y z. p x y z)
= introduce exists x y z. p x y z
with va vb vc
and f
The general form is
introduce exists x1 .. xn. p
with v1 .. vn
and proof
- where the
x1..xn
are in scope forp
only -
v1..vn
are instantiations for all thex1..xn
- and
proof
establishesp [v1..vn/x1..xn]
, i.e., it is a proof ofp
with thex1..xn
instantiated withv1..vn
.
To eliminate an existential formula and prove something else from it, you often need to "get your hands on" the witness. However, for soundness, one must also ensure that the witness does not escape its scope. Consider the following:
Suppose you have a binary predicate p
with a lemma proving that it is transitive. And suppose you know that exists y. p x y /\ p y z
. To apply the lemma and prove p x z
, one needs access to the witness y
. The syntax below helps:
let test_elim_exists_1 (p:nat -> nat -> Type)
(trans: (x:nat -> y:nat -> z:nat ->
Lemma (requires p x y /\ p y z)
(ensures p x z)))
(x z:nat)
: Lemma
(requires
(exists y. p x y /\ p y z))
(ensures
p x z)
= eliminate exists y. p x y /\ p y z
returns p x z
with _. trans x y z
The general form of eliminate
for existential formulas is as follows:
eliminate exists x1 ... xn. p
returns q
with p_pf. proof
Here, x1..xn
are in scope of p
and proof
, only.
Additional p_pf
is a binder whose type is squash p
and is also in scope of proof
.
And proof
is expected to prove q
, i.e., proof : Lemma q
or, equivalently, proof : squash q
Importantly, the binders x1..xn
are not in scope for q
.
You may wonder why we have the additional binder p_pf
. Sometimes, it can be convenient to have access to the proof term of p
, as shown in this variant below that works with squash
terms rather than Lemma
. If you don't need the proof of p
, you can always just use _
as its name, as shown in the first example above.
let test_elim_exists_2 p x z
(_:squash (exists y. p x y /\ p y z))
(trans : (#x:_ -> #y:_ -> #z:_ -> squash (p x y /\ p y z) -> squash (p x z)))
: squash (p x z)
= eliminate exists (y:nat).
p x y /\ p y z
returns p x z
with pf. trans pf
In prims.fst
, the formula p ==> q
is defined as squash (p -> GTot q)
.
To turn a Lemma
into an implication, one can write:
let test_implies_intro_2 p q (f: unit -> Lemma (requires p) (ensures q))
: Lemma (p ==> q)
= introduce p ==> q
with _. f ()
The same syntax also works with squashed proofs.
let test_implies_intro_1 p q (f: squash p -> squash q)
: squash (p ==> q)
= introduce p ==> q
with x. f x
The general form is
introduce p ==> q
with x. proof
where x:squash p
and is in scope of proof : squash q
.
Conversely, eliminating an implication is the classic modus ponens rule of propositional logic. This rule is usually very well automated by the SMT solver, so it is unlikely that you would need to use the syntax described below very much. However, it is included for completeness.
You can write:
let test_elim_implies_1 p q (_:squash (p ==> q)) (x:squash p)
: Tot (squash q)
= eliminate p ==> q
with x
let test_elim_implies_2 p q (f: unit -> Lemma p)
: Lemma (requires (p ==> q))
(ensures q)
= eliminate p ==> q
with f()
The general form is
eliminate p ==> q
with proof
where proof : squash p
and the entire term proves squash q
.
In prims.fst
, p \/ q
is defined as squash (c_or p q)
where
type c_or p q =
| Left : p -> c_or p q
| Right : q -> c_or p q
That is, p \/ q
is a squashed version of a constructor proof c_or p q
.
Given a proof of p \/ q
it is often convenient to "case analyze" it, to prove some goal
conditionally on whether p
holds or q
holds. That is, the elimination form of p \/ q
is quite useful. The introduction form is included for completeness, but is likely less useful, since it is well-automated by SMT.
let test_or_intro_left_1 p q (f: squash p)
: squash (p \/ q)
= introduce p \/ q
with Left f
let test_or_intro_right_2 p q (f: unit -> Lemma q)
: squash (p \/ q)
= introduce p \/ q
with Right (f())
The general form is
introduce p \/ q
with [Left | Right] proof
where when used with Left
we check that proof : squash p
, otherwise proof : squash q
.
Given p \/ q
one can derive r
if both p ==> r
and q ==> r
. The following syntax illustrates this:
let test_elim_or_2 p q r
(f: unit -> Lemma (requires p) (ensures r))
(g: unit -> Lemma (requires q) (ensures r))
: Lemma (requires p \/ q)
(ensures r)
= eliminate p \/ q
returns r
with _p. f ()
and _q. g ()
The general form is
eliminate p \/ q
returns r
with pf_p. proof1
and pf_q. proof2
where
-
pf_p : squash p
is in scope ofproof1 : squash r
-
pf_q: squash q
is in scope ofproof2 : squash r
As in the case of eliminate exists
, the proof terms can be bound to _
if not needed.
In prims.fst
, p /\ q
is defined as squash (c_and p q)
where type c_and p q = And : p -> q -> c_and p q
. I.e., it is squashed pair of proofs of p
and q
respectively.
let test_and_intro_1 p q (f:squash p) (g:squash q)
: squash (p /\ q)
= introduce p /\ q
with f
and g
The general form is
introduce p /\ q
with f
and g
where f : squash p
and g : squash q
.
To eliminate a conjunction p /\ q
, one derives a formula r
from separate proofs of p
and q
.
let test_elim_and_2 p q r (f: squash p -> squash q -> Lemma r)
: Lemma
(requires p /\ q)
(ensures r)
= eliminate p /\ q
returns r
with pf_p pf_q. f pf_p pf_q
The general form is
eliminate p /\ q
returns r
with pf_p pf_q. proof
where pf_p: squash p
and pf_q: squash q
and proof : squash r
The disjunction squash (p \/ ~p)
is directly provable in SMT for any p
. To make use of this in a proof (e.g., a proof by contradiction), one can eliminate as shown below, using the elimination form for disjunctions. This provides a way to simulate a "case analysis" of any proposition.
let test_excluded_middle p r
(f: unit -> Lemma (requires p) (ensures r))
(g: unit -> Lemma (requires ~p) (ensures r))
: Lemma r
= eliminate p \/ ~p
returns r
with _. f ()
and _. g ()
It's quite common to want to turn a lemma of the form x:t -> Lemma (requires p) (ensures q)
into a quantified implication forall x. p ==> q
. To do that, one can combine the introduction rule for universal quantifiers and implications, as shown below.
let test_forall_implies a (p:a -> Type) (q:a -> Type) (f: (x:a -> squash (p x) -> squash (q x)))
: squash (forall x. p x ==> q x)
= introduce forall x. p x ==> q x
with introduce p x ==> q x
with px. (
f x px
)
Note, however, that the sugar is integrated with F*'s type inference, so you can leave holes like _
and let F* infer the instantiations.
So, you could also write
let test_forall_implies_2_1 a (p:a -> Type) (q:a -> Type) (f: (x:a -> Lemma (requires p x) (ensures q x)))
: Lemma (forall x. p x ==> q x)
= introduce forall x. p x ==> q x
with introduce _ ==> _
with _. (
assert (p x); //the asserts are optional, just here to show that any kind of proof term is legal here
f x;
assert (q x)
)