module Metaprogramming
open FStar.Tactics
(*
Now, we shall turn to the automated generation of programs. Meta-F*
enables this in much the same way that it allows handling proofs: it
provides the user with a proofstate, a set of hypotheses and goals, and
one uses primitives to solve them. The way to call Meta-F* for metaprogramming
is the `_ by tau` syntax, as in the following:
*)
let my_int : int = _ by (exact (quote 42))
(*
In the fragment above, F* infers that the `_` must have type `int` (from
the annotation of `my_int`), and generates a proof state with a single
`int` goal. If you dump the initial proof state, you'll see something
like this:
Goal 1/1
--------------------------------------------------------------------------------
int
(*?u35*) _
Then, we solve this goal with the `exact` primitive
val exact : term -> Tac unit
`exact` takes a term and attempts to solve the current goal with it. In
this case, it succeeds and the original `_` is solved to `42`. If you
print the definition of `my_int` there will be no mention of the tactic,
it will have been run already.
Exercise: try to solve the goal with a type-incorrect solution, for
example `true`.
Here there is a key difference the kind of goals that appeared before.
For proving, we start with an *irrelevant* (i.e. squashed) goal, where a
proof term is not strictly needed. Indeed, we can simply send irrelevant
goals to the SMT solver. On the other hand, in metaprogramming, the goal
is usually relevant and a fully-defined term, in this case an integer,
is needed.
Exercise: instead of using `exact`, try sending this goal to the SMT
solver, or not solving it somehow (it should fail!).
*)
(*
Instead of solving a goal outright with a solution, other primitives
allow to build terms incrementally. For instance, `apply f` solves the
goal to an application of `f` to some arguments (as many as needed), and
presents new goals for each argument. *)
let metasum : int = _ by (apply (quote (+));
// dump ""; // You should see two new goals here!
exact (`13);
exact (`29))
(* Exercise: print the definition of metasum *)
(* We can also construct functions via the `intro` primitive. Calling
`intro` when the current goal is of type `a -> b` will solve it with
`fun x -> ?`, while presenting a new goal for the `?`, of at type `b`.
A `binder` for `x` is returned by intro, so it can be used to construct
the new solution. *)
let fancy_id : x:int -> int = _ by (let x = intro () in
exact (binder_to_term x))
(* Exercise: look at the proof states and final solution of `fancy_id` *)
(* Exercise: write a `zero` tactic that solves any goal of type `a -> b
-> ... -> z -> int` with the constant zero function. It should pass the
following tests. *)
//let const () : Tac unit = ???
//
//let f1 : int = _ by (const ())
//let f2 : nat -> int = _ by (const ())
//let f3 : bool -> list int -> unit -> int = _ by (const ())
//let f4 : int -> int -> int -> int -> int = _ by (const ())
//
//let _ = assert (f1 == 0)
//let _ = assert (forall x. f2 x == 0)
//let _ = assert (forall x y z. f3 x y z == 0)
//let _ = assert (forall x y z w . f4 x y z w == 0)
(* We can also perform case analysis on inductive types by the `destruct` primitive`.
Essentially, the `destruct` tactic takes a term `t` that has some inductive type
and performs a `match` on it. Then, it returns goals for each of the possible
cases. In each case, we also get an hypothesis for the fact that
the term we destructed is equal to the given branch. We ignore those
hypotheses here by just `intro`ducing them. *)
type enum = | A | B | C
let matchenum : enum -> int =
_ by (let x = intro () in
destruct (binder_to_term x);
ignore (intro ()); exact (`1);
ignore (intro ()); exact (`2);
ignore (intro ()); exact (`3))
(* Exercise: look at the proof states and final solution for matchenum *)
(* Calling `destruct` also allows to get the arguments of each constructor. *)
type either (a b : Type) = | Left of a | Right of b
let matcheither : either int int -> int =
_ by (let x = intro () in
destruct (binder_to_term x);
let x = intro() in ignore (intro ()); exact (binder_to_term x);
let y = intro() in ignore (intro ()); exact (binder_to_term y))
let _ = assert (matcheither (Left 10) == 10)
let _ = assert (matcheither (Right 20) == 20)
(* For a fancier example of metaprogramming, you can check the
`examples/tactics/Printers.fst` file in the F* repo, which derives
printers for inductive types automatically. Though it is a bit beyond
the scope of this course. *)