module Hybrid
open FStar.Tactics
open FStar.Mul
(*
Meta-F* supports a "hybrid" style of proof, where we can just
simplify/tweak goals before sending them to the SMT solver instead of
completely solving them via tactics. This is useful, since sometimes
writing an automated proof procedure can be a lot of work. Hence,
reusing the SMT solver's logic saves us some work.
Previously, providing "hints" to the SMT solver involved modifying the
program/proof by introducing lemma calls, as so:
*)
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; (* a hint *)
FStar.Math.Lemmas.modulo_distributivity a c p; (* a hint *)
assert ((a + b) % p == (a + c) % p)
(* Now, instead of doing that, we can tackle the assertion via a tactic. In
this case we simply "pose" the lemmas to get their hypotheses in the context,
so not much work is saved, but we're not making use of automation (yet!). *)
let modulo_add_tactic (p:pos) (a b c : int)
: Lemma (requires (b % p == c % p)) (ensures ((a + b) % p == (a + c) % p))
= assert ((a + b) % p == (a + c) % p)
by (ignore (pose_lemma (quote (FStar.Math.Lemmas.modulo_distributivity a b p)));
ignore (pose_lemma (quote (FStar.Math.Lemmas.modulo_distributivity a c p))))
(*
Exercise: print and look at the proofstates before and after the
`pose_lemma` calls. There are some SMT goals introduced by the calls,
what do they represent?
Notice that, for this assertion, F* generates a proofstate with the
assertion as a goal, and with a context including all variables in scope
*plus* the logical facts (such as the requires clause).
*)
(*
The inner workings:
As we have seen, assertions essentially introduce a cut
in the verification condition that F* computes: the assertion needs to
be proven at that program point, and is assumed in the continuation.
What's happening here is that F* is computing a verification condition
with the assertion, which basically introduces a "cut". When the assertion
has a tactic `t`, the proposition is actually wrapped with a marker `with_tactic`.
val with_tactic : (unit -> Tac unit) -> Type -> Type
Semantically, this marker just returns its second argument, ignoring
the tactic. However, when F* finds it before discharging a VC, it gets
special treatment. If a part of a VC is wrapped with this marker, it is
split out of the VC and turned into a proofstate. This is done only when
it appears in a "positive" position (i.e. a proof obligation) and not
for "negative" ones (i.e. hypotheses).
So, for instance, if we have `assert phy by tau ()` in a term, our VC
will look something like:
.... ==> with_tactic tau phi /\ (with_tactic tau phi ==> ....
Here F* will turn this into
.... ==> True /\ (phi ==> ....
trivializing the assertion within the VC, and creating a goal for
`squash phi` in a proper context (including all hypotheses in the left
"....").
The modified VC, called the "skeleton", is discharged as usual by the
SMT solver. The created proofstate will be fed to the `tau` tactic,
which can either solve it or finish with some open goals, which will be
fed to SMT.
*)
(*
Posing lemmas is a sort of "forward style" of proof: we conclude new
facts from previous facts in order to prove our goal. We can also do
proofs in backwards style, where we reduce our goal to simpler, usually
smaller, goals (note: implies_intro, split, left, and etc, are all
backwards-style).
To use a lemma in backwards style, there is `apply_lemma`. This
primitive unifies a Lemma's postcondition with the current goal and,
if that succeeds, creates goals for the missing arguments of the lemma
and (possibly) for the precondition of it.
Let's use them to prove a simple lemma (triang2), after some auxiliary
definitions.
*)
let rec triang (n : nat) : Tot nat =
if n = 0 then 0 else n + triang (n - 1)
(* Z3 is actually not so great at doing this proof when the context
includes many facts, so we delete everything but the most primitive
module and the current one. *)
#push-options "--using_facts_from '-* +Prims +Hybrid'"
let rec gauss (n : int) : Lemma (requires (n >= 0))
(ensures (triang n == (n * (n + 1)) / 2))
=
if n <> 0 then gauss (n - 1)
#pop-options
let prod_even (n : int) : Lemma ((n * (n + 1)) % 2 == 0) =
(* z3 needs some help *)
FStar.Math.Lemmas.lemma_mod_mul_distr_l n (n+1) 2
let triang2 (a : nat) : Lemma (triang a + triang a == a * (a + 1)) =
assert ((a * (a + 1)) % 2 == 0)
by (apply_lemma (`prod_even); qed ());
assert (triang a == (a * (a + 1) / 2))
by (apply_lemma (`gauss); smt (); qed ());
()
(* Exercise: step through the proof and look at the proof state
evolution on both assertions, what goals does `apply_lemma` create in
each case? *)
(*
In the calls above, there was only one valid choice for the arguments
to `gauss` and `prod_even` (namely `a` in both cases), but this is not
always the case. When this happens, apply_lemma will create a new goal
for the missing term/proof. For terms, we can use `exact` to provide the
solution manually.
*)
let silly_lemma (a b : nat) : Lemma (requires (a > b))
(ensures (a * a > 0)) = ()
let test_need_arg (a : nat{a > 2}) =
assert (a * a > 0)
by (apply_lemma (`silly_lemma);
exact (`2))
(* Exercise: dump the intermediate proof states and identify what each
goal represents. *)
(* Exercise: make this work without SMT by using the canonicalizer from
FStar.Tactics.Canon, and print the resulting simplified goal. Equalities
between syntactically equal terms can be solved by the `trefl` tactic.
*)
open FStar.Tactics.Canon
(* let arith_test (a b c d e : int) = *)
(* assert *)
(* ((a+b+c+d + e%2)*(b+c+d+a) == *)
(* a * a + b * b *)
(* + c * c + d * d *)
(* + e%2 * a + e%2 * b *)
(* + e%2 * c + e%2 * d *)
(* + a * b + a * b *)
(* + a * c + a * c *)
(* + a * d + a * d *)
(* + b * c + b * c *)
(* + b * d + b * d *)
(* + c * d + c * d) *)