-
Notifications
You must be signed in to change notification settings - Fork 232
Meta F*, (anti) quotations, term views and `inspect` versus `inspect_ln`
In F*, one can quote a term, transforming an expression into its representation. The infix operator `
takes an expression of arbitrary type and produces a term
. For instance, the expression `(20 + 22)
is of type term
and correspond to F*'s internal representation of this addition.
Of course, one cannot manipulate directly F*'s internal representation of terms directly within F*. Instead, one should inspect a term to get a view of it, of type term_view
.
The backtick operator is total, i.e. one can write the top-level t1
below.
let t1: term = `(30 + 12)
Such a backtick quotation is purely syntactic: for instance, the top-level t2
below will not capture the value of the local binding x
. t2
is a representation of an addition between the literal 40
and some free variable x
. Of course one can write t3
as below to fix the problem.
let t2: term =
let x = 2 in
`(40 + x)
let t3: term = `(let x = 2 in 40 + x)
However, while meta-programming, it is very useful to capture values from the current scope while quoting an expression.
The quote
operator is effectful but allows for such a behavior, i.e.:
let t4 (x: int): Tac term =
let y = x + 1 in
quote (y + 2)
The backtick quotation operator `
can also be used in conjunction with (effectful) antiquotation operators `@
and `#
. The former inserts an expression of arbitrary type in a quotation, while the latter inserts a term
in a quotation. Example:
let t4 (x: int) (y: term): Tac term =
`(`@x + `#y)
The reverse operation, unquoting, is the effectful operation unquote
. For example, below unquoting t4 12 (`30)
results in the integer 42
as expected. Note the syntax assert goal by tac
invokes the tactic tac
against the goal goal
; here the goal is trivial, we just use this construction to print a result.
let _ = assert True by (
print (string_of_int (unquote (t4 12 (`30))))
)
Others ways for invoking Meta F* programs are documented here.
The module FStar.Reflection.Builtins
exposes the total funtion inspect_ln
of type term -> term_view
, while FStar.Tactics.Builtins
exposes the effectful inspect
of type term -> Tac term_view
.
inspect
is almost always preferable to inspect_ln
.
In F*, there is two kinds of bounded variables:
- named bounded variables (
Tv_Var
), and -
De Bruijn bounded variables (
Tv_BVar
).
inspect
produces term_view
s with only named variables, while inspect_tv
produces term_view
s with only De Bruijn variables.
To disambiguate potential shadowing and for efficiency, F* gives each named variable an index that uniquely refers to it.
A named variable is represented by the term_view
constructor Tv_Var
, of type bv -> term_view
: it takes a bv
(bounded variable). A bv
can be inspected (with inspect_bv
) as a record which has the attribute bv_index
.
In the following module, we let the top-level example
be a term. Inspecting (with inspect
) and then observing the index of the variable x
(with get_let_bv_index
) multiple times yields different result.
module InspectEffectful
open FStar.Tactics
let example = (`(let x = 1 in x))
let get_let_bv_index (tv: term_view): string
= match tv with
| Tv_Let _ _ bv _ _ -> string_of_int (inspect_bv bv).bv_index
| _ -> "Not a Tv_Let!"
let _ = run_tactic (fun _ ->
let with_inspect () = get_let_bv_index (inspect example) in
let with_inspect_ln () = get_let_bv_index (inspect_ln example) in
print ( "Inspecting indexes with [inspect] is not pure : 1st run gives "
^ with_inspect () ^ ", while 2nd run gives " ^ with_inspect () ^ ". "
^ "In opposition, with [inspect_ln], the results are constants: "
^ with_inspect_ln () ^ " is equal to " ^ with_inspect_ln ())
)
This prints (in my F* session, you will get different numbers running this code):
Inspecting indexes with [inspect] is not pure: 1st run gives 205589, while 2nd run gives 205603. In opposition, with [inspect_ln], the results are consistent: 180975 is equal to 180975.
De Bruijn indexes are not practical to use by hand: instead of referring to a variable by its name, De Bruijn indexes consist in counting the number of abstractions (lambdas) you need to traverse to reach the binder you want to target.
The following module demonstrates this:
module DeBruijn
open FStar.Tactics
// Let [abs] a term that consists in nested abstractions. Note [fun x y z -> ...] is syntactic sugar for [fun x -> fun y -> fun z -> ...]
let abs = (`(fun x -> fun y -> fun z -> (x, y, z)))
let get_let_bv_index (tv: term_view): string
= match tv with
| Tv_Let _ _ bv _ _ -> string_of_int (inspect_bv bv).bv_index
| _ -> "Not a Tv_Let!"
// [string_of_bv] prints a [bv] with its index
let string_of_bv (bv: bv): string =
let bvv = inspect_bv bv in
"name="^bvv.bv_ppname ^"; index="^string_of_int (bvv.bv_index)^""
// Given a term (expected to be [Var] or a [BVar]), [bv_string_of_term] produces a string saying wether it is a named variable or a De Bruijn one, and specifying it's name and index
let bv_string_of_term (bv: term): Tac string =
match inspect bv with
| Tv_BVar b -> "DeBruijn{"^string_of_bv b^"}"
| Tv_Var b -> "Named{"^string_of_bv b^"}"
| _ -> "{[bv_string_of_term] ?}"
let observe abs_description = match abs_description with
| [x_binder;y_binder;z_binder], body ->
( match collect_app body with
| _Mktuple3, [x_var,_; y_var,_; z_var,_] ->
print ( "x_var: " ^ bv_string_of_term x_var ^ "\n"
^ "y_var: " ^ bv_string_of_term y_var ^ "\n"
^ "z_var: " ^ bv_string_of_term z_var ^ "\n"
)
| _ -> fail "Expected tuple3"
)
| _ -> fail "Expected 3 abstractions"
let _ = run_tactic (fun _ -> observe (collect_abs_ln abs))
// Inspecting with `inspect_ln` yields the message:
// [F*] x_var: DeBruijn{name=x; index=2}
// [F*] y_var: DeBruijn{name=y; index=1}
// [F*] z_var: DeBruijn{name=z; index=0}
let _ = run_tactic (fun _ -> observe (collect_abs abs))
// Inspecting with (effectful) `inspect` yields the message:
// [F*] x_var: Named{name=x; index=256152}
// [F*] y_var: Named{name=y; index=256158}
// [F*] z_var: Named{name=z; index=256163}
If one inspects the term fun x -> fun y -> fun z -> (x, y, z)
with inspect_ln
, the representation of the variable x
in the tuple (x, y, z)
is a Tv_BVar
, that is a De Bruijn represented variable with index 2.
The integer 2 is a suffisient representation for x
: x
is the binder attached to the thrid last abstraction (the first one being z
, and the second one being y
) in the stack of abstractions.
Basically, here, the term fun x -> fun y -> fun z -> (x, y, z)
is encoded as fun _ -> fun _ -> fun _ -> (<2>, <1>, <0>)
.
This is very impractical while meta-programming: if you move this <2>
somewhere else, this might make no sense any longer: fun _ -> fun _ -> (<2>, fun _ -> (<1>, <0>))
makes no sense for instance. The function mk_abs
crafts such a term naively: invoking it with inspect
yields the value fun x y -> (x, (fun z -> (y, z)))
while invoking it with inspect_ln
yields (as expected) an error.
let mk_abs tv: Tac term =
match tv with
| [x_binder;y_binder;z_binder], body ->
( match collect_app body with
| _Mktuple3, [x_var,_; y_var,_; z_var,_] ->
pack (Tv_Abs x_binder ( // λ x →
pack (Tv_Abs y_binder // λ y →
(mk_e_app (`Mktuple2) // (
[ x_var // x,
; pack (
Tv_Abs z_binder // λ z →
(mk_e_app
(`Mktuple2) // (
[ y_var // y,
; z_var])) // z
])) // )
) // )
)
| _ -> fail "Expected tuple3"
)
| _ -> fail "Expected 3 abstractions"
let _ = assert True by (
let t: int -> int -> (int * (int -> (int * int))) = unquote (mk_abs (collect_abs abs)) in
print (term_to_string (quote t))
)
// prints "fun x y -> x, (fun z -> y, z)"
let _ = assert True by (
let t: int -> int -> (int * (int -> (int * int))) = unquote (mk_abs (collect_abs_ln abs)) in
print (term_to_string (quote t))
)
// (Error 228) user tactic failed: `unquote: Cannot type fun _ y -> x, (fun z -> y, z) in context (). Error = (Violation of locally nameless convention: x)`
With inspect
, you end up with term views without any De Bruijn-represented variables (i.e. no Tv_BVar
, only Tv_Var
), and thus, you can move them freely without worrying about their positions relatively to abstractions.