-
Notifications
You must be signed in to change notification settings - Fork 236
_tc_example
In this example, we will try to type check the program
module Simple
val simple: x:int{x>=0} -> Tot int
let rec simple n =
if n = 0 then 1 else n + (simple (n - 1))
In order to be able to use the ocaml debugger, we need a lighter version of prims.fst (especially without recursive definitions). The version I have been using is prims.fst.
This experiment was started on 7/3/2017 by @shaolintl
Running with --debug_level Extreme --debug simple gives the following output
- Allow @shaolintl to understand what is going on in the type checker, how SMT obligations are generation and how the communication between the type checker and the solver is happening.
- Have an easy to read example that everybody wanting to understand the tc algorithm (As of the date above) can read
- Use it to try out @aseemr experiment of breaking the type checking into two phases (see this page).
- Have one place where all data structures accumulated during the process can be analyzed.
- FStar.Main: we call batch_mode_tc on the file containing the program
- FStar.Universal: After type checking the modified prims using tc_prims, we get prims_mod dsenv and env
- FStar.Universal: we now proceed to the type checking of Simple
- FStar.Universal: We have one file only so we jump to tc_one_file using env and dsenv
- FStar.Universal: We parse the file using dsenv and the filename
- FStar.Universal: We can Tc.check_module on the module and env
- FStar.TypeChecker.Tc: We override lax definition in env according to if we should verify the specific module
- FStar.TypeChecker.Tc: We override is_iface (interface) and admit (not verify) in env
- FStar.TypeChecker.Tc: We are now type checking the module declarations (tc_decls)
- FStar.TypeChecker.Tc: We start type checking and elaborating the declarations
We process declaration by declaration using tc_decl. tc_decl does also elaboration and therefore, it might return further declarations which should be parsed recursively. We will display for each rule the input and output environments, type checked and elaborated returned declarations as well as calls to further type checking.
For using the debugger, we skip tc_prims and jump to the declarations of Simple using the following debugger commands (shaolintl_two_phase_experiment 2c9771a7a3eb63c7d52ec6bcf3430f8c1c37adc0):
set print_length 999999999999999999
set print_depth 10000
break @ FStar_Universal 362
run
break @ FStar_TypeChecker_Tc 2840
run
The first env and se correspond to the type declaration
val simple: x:int{x>=0} -> Tot int
- Current uvs is []
- We use check_and_gen with the env from above and the type t to get t (uvs stays the same)
- The last step used the env in order to populate the type with information such as the sort of
x
changed from Unknown to U_zero, positions are set of the different elements and in general types from Prims are assigned. - We modify the declaration to contain the new t, push_sigelt it in the environment and return it
The second env and se correspond to the program
let rec simple n =
if n = 0 then 1 else n + (simple (n - 1))
- The first part is to annotate all lbs in the program using types from the declaration, if possible.
- On the first lb
- We are searching the env for the declared type of the label name simple
- We get the following type annotation for the 'simple' program.
- We annotate the label with the above type to get an annotated version
- There are no more labels
- We now compute qualifiers
- Since there are no qualifiers over this let program, we add the Visible_default qualifier
- We type check the let declaraion
- We transform the (top level) let declaration into a let term with an empty body
- We change the environment to mention that it is top level and should be generalized
- We call TcTerm.tc_maybe_toplevel_term on the let expression (see below) with the above environment
- We translate back the term into a top level declaraion
- We add the returned type checked top level let declaration to the environment (Env.push_sigelt)
- We add the declaration to the exported values
- We go to finish finish_partial_modul
- We set the previously computed exports into the module
- Since we are lax, we are not using Z3 to encode the module and try it out
- We need to look deeper into type checking the let term and encoding the module for
- We first compress the expression from before into this term
- We now call check_top_level_let_rec on the expression, containng this lbs
- We set Env.instantiate_imp=true
- Calling Subst.open_let_rec does nothing on top level letrecs
- We clear the expected_typ field in env
- We update the labels and the env (we have only one label in this example)
- We extract the let rec annotations by calling (in our case) Subst.open_univ_vars
- This function takes the empty univ_vars we have right now and the term and is supposed (it seems) to replace the variable names in the term with new names
- In our case, there are no univ_vars so the function does nothing
- We push the (empty list of) univ_vars to the env
- We call Syntax.Subst.unascribe on the [label.lbdef] (https://gist.github.com/shaolintl/6e38004d8f2f36c88c18da7d1cb76a22) which, since the term is not Tm_ascribed, does nothing
- The type remains the same as extract_let_rec_annotation told us not to check the type
- We now set the new env
- First we check if there is a termination check
- We check the type, it is an arrow type to int
- What does we do to it? and to the rest of the type? It is handled by the function arrow_formals_comp (check Syntax.Subst 854)
- Anyway, there is a termination check since the function's type is Tot and also Env.should_verify env is true (we are trying now with lax=false)
- We add to the env.letrecs the lb with type t
- We set lb to contain lbtyp=t, lbunivs=univ_vars=[] and lbdef=e
- We return the constructed lb and env computed in the previous step
- We extract the let rec annotations by calling (in our case) Subst.open_univ_vars
- So far,we have taken care of the top_level part by building the correct environment, etc., we now go to actually check the let_recs by calling check_let_recs on the lb
Here starts the interesting part of type checking the term: (fun n -> (match (op_Equality n@0 0) with 12 | true -> 1 13 |_ -> (op_Addition n@1 (simple (op_Subtraction n@1 1)))))
against the type: (x:(x#6:int{(b2t (op_GreaterThanOrEqual x@0 0))}) -> Tot int)
- Type checking an abstraction with binders and body which should follow the rule (from EMF*) S; G |- t : Type_i S; G, x:t |- e : C ------------------------------------- [T-Abs] S; G |- fun (x:t) -> e : (x:t -> C)
- We use the expected type in the environment (and then reset it) with the body of the abstraction to compute the expected function type
- Matching on the expected type, we get that it is an arrow with bs_expected and c_expected
- We check the binders (bs against bs_expected) using check_binders. Both lists contain only one element.
- We first compute the expected_t based on the sort of the expected binder and (the empty) subst.
- The meta data of the bs of the program is Unknown and therefore we just set it to the expected type
- We push the new binder into the environment. Since we have only one binder, we dont care about extending the substs.
- Where do we check the first obligation above? S; G |- t : Type_i Is it implicit since we created t in the function signature?
- The number of binders of expected and actual types is the same and we skip the handle_more function
- We didnt create any other guard except the trivial_guard
This process continues to compute the expected type of the let statement, binders, letrecs, computation, body and guard.