-
Notifications
You must be signed in to change notification settings - Fork 16
/
Copy pathClase10.Tac.Constructive.fst
203 lines (172 loc) · 6.71 KB
/
Clase10.Tac.Constructive.fst
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
module Clase10.Tac.Constructive
open FStar.Tactics
(*
As you probably know by now, verification in F* works by first computing
verification conditions for terms, and then discharging them via the SMT
solver (when they are not trivial).
When a program fails to verify, the user can take one of many paths.
Firstly, one can increase the time limit of the SMT solver, or change
its configuration in the hope that the proof will go through. More
commonly, however, the programmer needs to give the SMT solver some
help in the form of new facts that it can use. We've seen examples of
this when we call lemmas within a function body in order to make it
verify. For instance, compare the following two examples (the first of
which fails).
*)
[@expect_failure]
let modulo_add_fail (p:pos) (a b c : int)
: Lemma (requires (b % p == c % p)) (ensures ((a + b) % p == (a + c) % p))
= ()
let modulo_add (p:pos) (a b c : int)
: Lemma (requires (b % p == c % p)) (ensures ((a + b) % p == (a + c) % p))
= FStar.Math.Lemmas.modulo_distributivity a b p;
FStar.Math.Lemmas.modulo_distributivity a c p
(*
This style of proving has two serious disadvantages. First, the goal
and set of hypotheses and not visible to the user, so proving usually
involves some trial and error. Second, there is no automation: the user
is responsible for writing all intermediate assertions and lemma calls,
which also clutter the proof/program.
With tactics, we can do better. We can inspect the "proof state",
i.e. the hypotheses we have and goal we have to solve, and implement
automated proof procedures. We can use tactics to do non-trivial proofs
without the SMT solver, or to simply give it some help when that's more
convenient. In this file we will mostly focus on the first alternative.
To get our feet wet, we will start with some simple examples of logical
propositions. The SMT is very good at this, but we will not use it at
all in the following examples.
Let us prove that implication is reflexive. We will use some tactics
from the `FStar.Tactics.Logic` module, in order to introduce the
implication and use the hypothesis.
When the goal is an implication, `implies_intro` will introduce the
implication and return a `binder` which represents the variable that was
pushed into the context. In the example, we use this `binder` to solve
the goal via `hyp`.
*)
let ex1 (p : prop) =
assert (p ==> p)
by (let b = implies_intro () in
hyp b)
(*
The `qed` tactic checks that there are no more open goals when called,
or fails. We can use it here to check that we have fully solved the
assertion via tactics.
*)
(* This would fail with: (Error) user tactic failed: qed: not done! *)
//let _ =
// assert True
// by (qed ())
let ex1_qed (p : prop) =
assert (p ==> p)
by (let b = implies_intro () in
hyp b;
qed ())
(* Exercise: add intermediate `dump` calls to see how the proofstate evolves *)
(* The following examples are somewhat similar, try to follow them by looking
at the proof states. *)
let ex2 (p q : prop) =
assert (p ==> q ==> p)
by (let bp = implies_intro () in
let _ = implies_intro () in
hyp bp;
qed ())
(*
We need some more interesting tactics in order to handle more kinds
of formulae, we now go over a few of them.
*)
(*
The `split` tactic will turn a goal that is a conjunction into separate
goals for each conjunct, which we can then solve independently.
*)
let ex3 (p q : prop) =
assert (p ==> q ==> q /\ p)
by (let bp = implies_intro () in
let bq = implies_intro () in
split ();
(* Now we have two goals: q and p *)
hyp bq;
(* Only one goal left, p *)
hyp bp;
(* Done! *)
qed ())
(* `destruct_and` can be used to destruct a conjunction and get hypotheses
for each conjunct. *)
let ex4 (p q : prop) =
assert (p /\ q ==> p)
by (let h = implies_intro () in
let (bp, bq) = destruct_and (binder_to_term h) in
hyp bp;
qed ())
(* The `left` and `right` tactics solve a disjunction by reducing
it to the left or right disjunct accordingly. *)
let ex5 (p q : prop) =
assert (p ==> p \/ q)
by (let bp = implies_intro () in
left ();
hyp bp;
qed ())
let ex6 (p q : prop) =
assert (p ==> q \/ p)
by (let bp = implies_intro () in
right ();
hyp bp;
qed ())
(* `cases_or` instead destroys a disjunction `p \/ q`, and splits the
`phi` goal into `p ==> phi` and `q ==> phi`. *)
let ex7 (p q : prop) =
assert (p \/ q ==> q \/ p)
by (let bp_or_q = implies_intro () in
cases_or (binder_to_term bp_or_q);
(* first case *)
let bp = implies_intro () in
right ();
hyp bp;
(* second case *)
let bq = implies_intro () in
left ();
hyp bq;
qed ())
(* To use an implication, we can use the `mapply` tactic. This tactic
takes a `term` argument, so we need to cast the `binder` to a `term` via
`binder_to_term`. *)
let ex8 (p q : prop) =
assert ((p ==> q) ==> p ==> q)
by (let i = implies_intro () in
let h = implies_intro () in
mapply (binder_to_term i);
mapply (binder_to_term h);
qed ())
(* `forall_intro` will turn a goal of the shape `forall (x:t). phi` into
`phi`, while adding a variable `x` (of type `t`) to the context and
returning a binder for it. *)
let ex9 (p : prop) =
assert (p ==> (forall (_x:int). p))
by (let bp = implies_intro () in
let _bx = forall_intro () in
hyp bp;
qed ())
(* `instantiate` will instantiate a forall with some particular term
and add the new hypothesis to the context. The `witness` tactic solves
an existential goal with the explicit witness, but requires a proof of
the property. Here we use zero as the witness, via a static quotation
(`0) (more about quotations on a next chapter). *)
let ex10 (p : int -> prop) =
assert ((forall x. p x) ==> (exists x. p x))
by (let bf = implies_intro () in
witness (`0);
let bp0 = instantiate (binder_to_term bf) (`0) in
hyp bp0)
(* Exercises: do these proofs constructively, and make sure to end with
a `qed ()` to check that the SMT solver is not used. *)
let ex11 (p q : nat -> prop) =
assert ((forall x. p x) ==> (forall x. p x \/ q x))
by (smt ())
let ex12 (p q : nat -> prop) =
assert ((forall x. p x /\ q x) ==> (forall x. p x))
by (smt ())
let ex13 (p q : nat -> prop) =
assert ((forall x. p x /\ q x) ==> (exists x. p x \/ q x))
by (tadmit ()) // SMT is bad with existentials, just admit
let ex14 (a:Type) (p q : a -> prop) =
assert ((forall x. p x ==> q x) ==> (forall x. p x) ==> (forall x. q x))
by (smt ())