module Intro
(* This file is meant to skim through the features of Meta-F*, and not
really to teach you how to use it. That's what the next files are for! *)
(* We always need this module to use Meta-F* *)
open FStar.Tactics
(*
Meta-F* has many purposes. It can be used to discharge proof obligations
(replacing the SMT solver), to simply preprocess obligations
(complementing the SMT solver), for metaprogramming (i.e. the automatic
generation of terms/programs), solving implicit arguments (which can be
seen as a variant of metaprogramming), and more.
We (somewhat loosely) refer to the setting of using Meta-F* for
proofs as "tactics", and to the generation of terms/programs as
"metaprogramming".
(For experts: If you are familiar with other dependent languages such
as Coq/Agda/Lean, you might think these are the same thing. This is not
exactly so in F*, read ahead for details!)
At its core, Meta-F* is a language for describing correct (i.e. sound)
proof state transformations. A proof state is a set of "goals" that need
to be solved by providing a solution or proof for them. For instance,
a goal can be to prove `forall x. x + 1 == 1 + x`, or to construct a
term of type `int -> int -> Tot int`. The language allows to break down,
tweak, and solve these goals incrementally, while ensuring that the
solutions are correct (valid proofs / well-typed terms).
Meta-F* is in fact a subset of F* itself, and not a separate language
(as Coq and Ltac are). Making good use of F*'s effect system, Meta-F* is
embedded via an effect, named `Tac`, which enjoys all good properties of
F* effects (specifications by WPs, call-by-value, direct-style syntax,
type checking/inference, etc). So, let us present the most simple
metaprogram:
*)
let idtac () : Tac unit = ()
(*
We can also make use of all existing code in the PURE and DIV effects,
since they can be lifted into Tac.
*)
let max_list (l1 l2 : list int) : Tac (list int) =
if FStar.List.length l1 > FStar.List.length l2 then l1 else l2
let rec recursive_tac (n:nat) : Tac unit =
if n = 0
then ()
else recursive_tac (n - 1)
let rec looping_tac () : Tac unit =
// Metaprograms need not terminate!
looping_tac ()
(*
Of course, these programs are not so interesting as they do not have
any effect on the (implicit) proof state that they will manipulate.
Interesting metaprograms can (and must!) be built from *primitives*
(found in FStar.Tactic.Builtins) which can inspect and transform the
proof state. For instance, the `dump` primitive will, when run, output
the proof state in a pretty-printed format to the user, along with a
user-defined string.
*)
(* Exercise: try `C-c` `C-s` `C-d` and query the type of `dump` *)
(*
We can use `run_tactic` to test it. The `run_tactic` "hook" takes a term
of type `unit -> Tac unit` and runs it on a trivial empty proof state.
*)
let _ = run_tactic (fun () -> dump "Hello world!")
(*
You should see a "Hello world!" in the *fstar: goals* buffer of Emacs.
*)
(*
Exercise: Try writing and running a metaprogram that will call `dump` to
count down from 10 to 0. The `string_of_int` function from `Prims` might
be useful. You can navigate the dumps in the *fstar: goals* buffer with
PgUp and PgDown.
*)
// let rec countdown (n:nat) : Tac unit = ???
//
// let _ = run_tactic (fun () -> countdown 10)
(*
Now that we have seen some trivial metaprograms, we can start writing
more interesting ones that actually manipulate the proof state. We will
begin by writing some tactics, i.e. metaprograms for proving, and using
them to solve assertions within code (as is common in F* ).
For instance, let us prove the following tautology with an explicit
proof:
*)
let constr (a b : prop) : Lemma (a ==> b ==> b /\ a) =
assert (a ==> b ==> b /\ a)
by (let ha = implies_intro () in
let hb = implies_intro () in
split ();
hyp hb;
hyp ha;
qed ())
(* Exercise: inspect the intermediate proof states to understand the
proof. *)
(*
In the previous example, our proof was completed internally. However, F*
allows for a mixed style of proof where we can leverage the SMT solver
and use it when it works well, without having to fully spell out proofs
or write complex tactics.
For instance, consider the following fragment:
*)
open FStar.Mul
// computes 1 + 2 + .. + n
let rec triang (n : nat) : Tot nat =
if n = 0 then 0 else n + triang (n - 1)
let rec gauss (n : nat) : Lemma (triang n == (n * (n + 1)) / 2) =
if n <> 0 then gauss (n - 1)
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
[@expect_failure] // this means the next definition is checked to *fail*!
let test (a : nat) : Lemma (triang a + triang a == a * (a + 1)) =
//assert (triang a == (a * (a + 1) / 2));
//assert ((a * (a + 1)) % 2 == 0);
()
(*
The test above fails since the SMT cannot find a proof for the .
assertions We did not call the lemmas, and there are no patterns either.
Exercise: check that both assertions indeed fail by changing them to
`assume`s one at a time. Also make the proof work by using explicit
lemma calls.
Instead of doing either of those things, we can use tactics to guide the
proof in a more local fashion, using each lemma where needed.
*)
let test_good (a : nat) : Lemma (triang a + triang a == a * (a + 1)) =
assert (triang a == (a * (a + 1) / 2))
by (apply_lemma (`gauss); qed ());
assert ((a * (a + 1)) % 2 == 0)
by (apply_lemma (`prod_even); qed ());
()
(*
In this case, the postcondition is being proved by the SMT solver,
while each assertion is completely proven by the tactics.
A couple of things are going here. When using the `assert..by` syntax,
the assertion is turned into a proof state with a single goal, and
the associated tactic is run over it. The backtick marks a "static
quotation"; which we won't explain just yet.
Exercise: print and inspect the proof state before and after applying
the lemmas on each assertion.
These proof states single out the chosen assertion from the full VC of
the term, so one can write tactics for specific subobligations instead
of dealing with an (usually unpleasant) VC. In this case, we use the
`apply_lemma` primitive to solve each one.
*)
(*
Metaprograms can also inspect the goals they are invoked on, which
allows a single metaprogram to be useful in many different scenarios.
We call this ability "reflection", and it is mostly provided by the
FStar.Reflection module and the `inspect` tactic primitive. The
FStar.Reflection.Formula library provides a convenient way to use it for
the case of logical formulae.
*)
let _ =
assert (True /\ True)
by (let f = cur_formula () in
match f with
| And _ _ -> dump "It's a conjunction!"
| _ -> fail "Uh-oh!")
let _ =
assert (False ==> True)
by (let f = cur_formula () in
match f with
| Implies _ _ -> dump "It's an implication!"
| _ -> fail "Uh-oh!")
(*
By using these inspection capabilities, we can construct more
automated tactics. For instance, the following one that will split all
conjunctions and introduce all implications:
*)
let rec blowup () : Tac unit =
match cur_formula () with
| And _ _ -> split (); iseq [blowup; blowup]
| Implies _ _ -> let _ = implies_intro () in blowup ()
| _ -> ()
let test_blowup () =
assert (True /\ (False ==> (True /\ False)))
by (blowup (); dump "Final state")
(* Note how the final state has 3 separate goals *)
(*
Besides using Meta-F* for proving, we can use it do metaprogramming.
Using Meta-F* to construct terms follows the same principles as for
proving: there is a set of goals that need to be solved, and primitives
can be used to do so. The most simple way to solve a goal is to provide
a term via `exact`. The way to invoke Meta-F* for term generation is `_
by` syntax shown below:
*)
let int42 : int = _ by (exact (`42))
(* Exercise: print the definition of `int42` *)
(*
Besides providing exact solutions, terms can be built algorithmically by
calling the primitives. For instance, the `apply` primitive takes a term
`f` and solves the current goal with an application of `f` to as many
arguments as needed.
*)
let suc (x:int) = x + 1
let rec repeatN (n:nat) (t : unit -> Tac unit) : Tac unit =
if n = 0
then ()
else (t (); repeatN (n-1) t)
let ten : int = _ by (repeatN 10 (fun () -> apply (`suc)); exact (`0))
(* Exercise: print the definition of `ten` (it's not 10!) *)
let rec sums (n:nat) : Tac unit =
if n = 0
then exact (`1)
else begin
apply (`(+));
sums (n-1);
sums (n-1)
end
let big : int = _ by (sums 4)
(* Exercise: print the definition of `big` *)
(*
We can also use the reflection capabilities of F* to inspect the
goals we need to solve and act accordingly. For instance, here is a
metaprogram that constructs an addition function for any amount of
arguments.
*)
let rec __run (bs:binders) : Tac unit =
match inspect (cur_goal ()) with
| Tv_Arrow _ _ ->
let b = intro () in
__run (b::bs)
| _ ->
iter (fun b -> apply (`(+));
exact (binder_to_term b)) bs;
exact (`0)
let make_add () : Tac unit =
__run []
let add_2 : a:int -> b:int -> Tot int = _ by (make_add ())
let add_3 : a:int -> b:int -> c:int -> Tot int = _ by (make_add ())
let add_4 : a:int -> b:int -> c:int -> d:int -> Tot int = _ by (make_add ())
let _ = assert (add_2 3 4 == 7)
let _ = assert (add_3 3 4 5 == 12)
let _ = assert (add_4 3 4 5 6 == 18)
(*
Another usage of metaprogramming is resolving implicit arguments.
Usually, in dependently typed languages, they are solved via
unification, which works fine when there is a single correct solution
implied by the well-typing of the term.
*)
let id (#a:Type) (x:a) = x
let call_id () = id 5
#push-options "--print_implicits"
(*
Run C-c C-s C-p call_id here, the implicit argument was resolved to
`int` since `5` has type `int`.
*)
#pop-options
(*
In other cases, there is perhaps no single correct solution via
unification, but there might however be some strategy to solve them.
Meta-F* provides a way to solve implicits via tactics, which allows
programmers to write their own strategies. As a simple example, let's
write a "diagonal" function whose second arguments is inferred to be the
same as the first one, unless explicitly provided.
*)
let diag (x : int) (#[exact (quote x)] y : int) : tuple2 int int = (x, y)
(* When the implicit is not given, this is simply a pair of an element
with itself. Note how in this case, there is no information that fully
defines the implicit from the types, and any integer is a correct
solution. *)
let _ = assert (diag 5 == (5, 5))
(* But one can override it *)
let _ = assert (diag 5 #23 == (5, 23))
(* As a more interesting example, this is exactly how typeclasses are
implemented in F*. See the type of add3 below , it contains an implicit
argument of type `add a`, which is to be resolved by the `tcresolve`
tactic. *)
open FStar.Tactics.Typeclasses
class add 'a = {
sum : 'a -> 'a -> 'a
}
(* [|add a|] is just sugar for `(#[tcresolve ()] _ : add a)` *)
let add3 (#a:Type) [|add a|] (x y z : a) : a = sum x (sum y z)
(* Instances are just values with a `tcinstance` attribute, which is
what `tcresolve` looks for. *)
instance add_int : add int = { sum = (+) }
(* Same as
[@tcinstance] let add_int : add int = ...
*)
(* When called over a concrete type with an instance, such as `int`, the
`tcresolve` tactic solves the implicit to the instance. *)
let test_add = assert (add3 1 2 3 == 6)
#push-options "--print_implicits"
(* Exercise: print the definition of `test_add` here and look at the
solved implicits. *)
#pop-options
(* Having seen a bit of everything, we can now move into more advanced
topics on each area. *)
// Constructive.fst: doing proofs via tactics instead of SMT solver
// Automation.fst: inspecting goals and automating proofs
// Hybrid.fst: mixed style of proofs, using SMT + tactics
// Term.fst: The term representation and view
// Metaprogramming.fst: generating terms automatically and from type definitions