Case study: simply-typed lambda-calculus¶
module STLC
We now look at a larger case study: proving the soundness of a type-checker for the simply-typed \(\lambda\)-calculus (STLC). If you’re not familiar with STLC, you can have a look at the Software Foundations book for a gentle introduction given by the textual explanations (you can ignore the Coq parts there). The formalization and proof here closely follows the one in Software Foundations. Our proofs are, however, shorter and much more readable than Coq proofs.
Syntax¶
We represent STLC types by the F* inductive datatype ty.
type ty = | TBool : ty | TArrow : tin:ty -> tout:ty -> ty type var = int
We consider Booleans as the only base type (TBool). Function types
are represented by the TArrow constructor taking two type arguments.
For instance we write TArrow TBool TBool for the type of functions
taking a Boolean argument and returning a Boolean result. This would be
written as bool -> bool in F* syntax, and
\(\mathsf{bool} \to \mathsf{bool}\) in paper notation.
We represent the expressions of STLC by the datatype exp.
type exp = | EVar : v:var -> exp | EApp : fn:exp -> arg:exp -> exp | EAbs : v:var -> vty:ty -> body:exp -> exp | ETrue : exp | EFalse : exp | EIf : test:exp -> btrue:exp -> bfalse:exp -> exp
Variables are represented as integer “names” decorated by the
constructor EVar. Variables are “bound” by lambda abstractions
(EAbs). For instance the identity function on Booleans is written
EAbs 0 TBool (EVar 0). In paper notation one would write this
function as \((\lambda x:\mathsf{bool}.~x)\). The type annotation on
the argument (TBool) allows for very simple type-checking. We are
not considering type inference here, to keep things simple. The
expression that applies the identity function to the ETrue constant
is written
let stlc_app_id_to_true = EApp (EAbs 0 TBool (EVar 0)) ETrue
(in paper notation \((\lambda x:\mathsf{bool}.~x)~\mathsf{true}\)).
The language also has a conditional construct (if-then-else). For instance, the Boolean “not” function can be written as
let stlc_not = EAbs 0 TBool (EIf (EVar 0) EFalse ETrue)
(in paper notation \(\lambda x:\mathsf{bool}.~\mathsf{if }~x~\mathsf{ then~false~else~true}\)).
Operational semantics¶
We define a standard small-step call-by-value interpreter for STLC. The
final result of successfully evaluating an expression is called a
value. We postulate that functions and the Boolean constants are
values by defining is_value, a boolean predicate on expressions (a
total function):
val is_value : exp -> Tot bool let is_value e = match e with | EAbs _ _ _ | ETrue | EFalse -> true | _ -> false
The EAbs, ETrue, and EFalse cases share the same
right-hand-side (true), which is a way to prevent duplication in
definitions.
In order to give a semantics to function applications we define a
function subst x e e' that substitutes x with e in e':
val subst : int -> exp -> exp -> Tot exp
let rec subst x e e' =
match e' with
| EVar x' -> if x = x' then e else e'
| EAbs x' t e1 ->
EAbs x' t (if x = x' then e1 else (subst x e e1))
| EApp e1 e2 -> EApp (subst x e e1) (subst x e e2)
| ETrue -> ETrue
| EFalse -> EFalse
| EIf e1 e2 e3 -> EIf (subst x e e1) (subst x e e2) (subst x e e3)
We traverse the expression and when we reach a variable (EVar x') we
check whether this is the variable x we want to substitute, and if
it is we replace it by e. For lambda abstractions (EAbs x' t e1)
we only substitute inside the body e1 if x and x' are
different; if they are the same we leave the body unchanged. The reason
for this is that the x in e1 is bound by the abstraction: it is
a new, local name that just happens to be spelled the same as some
global name x. The global x is no longer accessible in this
scope, since it is shadowed by the local x. The other cases are
straightforward.
Note for experts: Because we will only reduce closed expressions, where all variables are bound by previous lambdas, we will only ever substitute closed expressionse, and this naive definition of substitution is good enough. Substitution would become trickier to define or the representation of variables would have to change if we were considering the case wheree, the expression replacing a variable in some other expression, may itself contain free variables.
Given the definition of values and of substitution we can now define a
small-step interpreter, as a function step that takes an expression
e and it either returns the expression to which e reduces in a
single step, or it returns None in case of an error (all errors in
this language are typing errors, and will be prevented statically by the
type system).
val step : exp -> Tot (option exp)
let rec step e =
match e with
| EApp e1 e2 ->
if is_value e1 then
if is_value e2 then
match e1 with
| EAbs x t e' -> Some (subst x e2 e')
| _ -> None
else
match (step e2) with
| Some e2' -> Some (EApp e1 e2')
| None -> None
else
(match (step e1) with
| Some e1' -> Some (EApp e1' e2)
| None -> None)
| EIf e1 e2 e3 ->
if is_value e1 then
match e1 with
| ETrue -> Some e2
| EFalse -> Some e3
| _ -> None
else
(match (step e1) with
| Some e1' -> Some (EIf e1' e2 e3)
| None -> None)
| _ -> None
We execute an application expression EApp e1 e2 in multiple steps by
first reducing e1 to a value, then reducing e2 to a value
(following a call-by-value evaluation order), and if additionally
e1 is an abstraction EAbs x t e'we continue by substituting
the formal argument x by the actual argument e2. If not we
signal a dynamic typing error (a non-functional value is applied to
arguments) by returning None. For EIf e1 e2 e3 we first reduce
the guard e1: if the guard reduces to true then we continue with
e2, if the guard reduces to false we continue with e3, and
if the guard reduces to something else (e.g. a function) we report a
dynamic typing error. The None -> None cases simply propagate errors
to the top level.
let _ = assert (step (EApp (EAbs 0 TBool (EVar 0)) ETrue) = Some ETrue) let _ = assert (step (EApp ETrue ETrue) = None)
Type-checker¶
In order to assign a type to a term, we need to know what assumptions we should make about the types of its free variables. So typing happens with respect to a typing environment—a mapping from the variables in scope to their types. We represent such partial maps as functions taking an integer variable name and returning an optional type:
type env = int -> Tot (option ty)
We start type-checking closed terms in the empty environment, i.e. initially no variables are in scope.
val empty : env let empty = fun _ -> None
When we move under a binder we extend the typing environment.
val extend : env -> int -> ty -> Tot env let extend g x t = fun x' -> if x = x' then Some t else g x'
For instance we type-check EAbs x t e in typing environment g by
first type-checking the body e in the environment extend g x t.
The type-checker is a total function taking an environment g and an
expression e and producing either the type of e or None if
e is not well-typed.
val typing : env -> exp -> Tot (option ty)
let rec typing g e =
match e with
| EVar x -> g x
| EAbs x t e1 ->
(match typing (extend g x t) e1 with
| Some t' -> Some (TArrow t t')
| None -> None)
| EApp e1 e2 ->
(match typing g e1, typing g e2 with
| Some (TArrow t11 t12), Some t2 -> if t11 = t2 then Some t12 else None
| _ , _ -> None)
| ETrue -> Some TBool
| EFalse -> Some TBool
| EIf e1 e2 e3 ->
(match typing g e1, typing g e2, typing g e3 with
| Some TBool, Some t2, Some t3 -> if t2 = t3 then Some t2 else None
| _ , _ , _ -> None)
Variables are simply looked up in the environment. For abstractions
EAbs x t e1 we type-check the body e1 under the environment
extend g x t, as explained above. If that succeeds and produces a
type t', then the whole abstraction is given type TArrow t t'.
For applications EApp e1 e2 we type-check e1 and e2
separately, and if e1 has a function type TArrow t11 t12 and
e2 has type t11, then the whole application has type t12.
ETrue and EFalse have type TBool. For conditionals, we
require that the guard has type TBool and the two branches have the
same type, which is also the type of the conditional.
Soundness proof¶
We prove progress and preservation for STLC. The progress theorem tells us that closed, well-typed terms do not produce (immediate) dynamic typing errors: either a well-typed term is a value, or it can take an evaluation step. The proof is a relatively straightforward induction.
val progress : e:exp -> Lemma
(requires (Some? (typing empty e)))
(ensures (is_value e \/ (Some? (step e))))
let rec progress e =
match e with
| EApp e1 e2 -> progress e1; progress e2
| EIf e1 e2 e3 -> progress e1; progress e2; progress e3
| _ -> ()
Variables are not well-typed in the empty environment, so the theorem holds vacuously for variables. Boolean constants and abstractions are values, so the theorem holds trivially for these. All these simple cases are proved automatically by F*. For the remaining cases we need to use the induction hypothesis, but otherwise the proofs are fully automated. Under the hood F* and Z3 are doing quite a bit of work though.
In case e = (EApp e1 e2) F* and Z3 automate the following intuitive
argument: We case split on the first instance of the induction
hypothesis (is_value e1 \/ (Some? (step e1))).
- If
e1steps toe1'then, by the definition ofstep,(EApp e1 e2)steps to(EApp e1' e2). - If
e1is a value, we case split on the second induction hypothesis instance,(is_value e2 \/ (Some? (step e2))). - If
e2steps toe2'then(EApp e1 e2)steps to(EApp e1 e2'), sincee1is a value. - If
e2is also a value, then we need to obtain thate1has a function type and from this that it is an abstraction. Expressione1has a function type because, by the definition oftyping, an application is well-typed only when the first expression is a function. The remaining step is usually done as a separate “canonical forms” lemma, stating that any value that has a function type is actually an abstraction. Z3 can prove this fact automatically from the definitions oftypingandis_value.
The intuitive proof of the EIf case is similar.
The preservation theorem (sometimes also called “subject reduction”) states that when a well-typed expression takes a step, the result is a well-typed expression of the same type. In order to show preservation we need to prove a couple of auxiliary results for reasoning about variables and substitution.
The case for function application has to reason about “beta reduction”
steps, i.e. substituting the formal argument of a function with an
actual value. To see that this step preserves typing, we need to know
that the substitution itself does. So we prove a substitution lemma,
stating that substituting a (closed) term v for a variable x in
an expression e preserves the type of e. The tricky cases in the
substitution proof are the ones for variables and for function
abstractions. In both cases, we discover that we need to take an
expression e that has been shown to be well-typed in some context
g and consider the same expression e in a slightly different
context g'. For this we prove a context invariance lemma,
showing that typing is preserved under “inessential changes” to the
context g—in particular, changes that do not affect any of the free
variables of the expression. For this, we need a definition of the free
variables of an expression—i.e., the variables occurring in the
expression that are not bound by an abstraction.
A variable x appears free in e if e contains some occurrence
of x that is not under an abstraction labeled x:
val appears_free_in : x:int -> e:exp -> Tot bool
let rec appears_free_in x e =
match e with
| EVar y -> x = y
| EApp e1 e2 -> appears_free_in x e1 || appears_free_in x e2
| EAbs y _ e1 -> x <> y && appears_free_in x e1
| EIf e1 e2 e3 ->
appears_free_in x e1 || appears_free_in x e2 || appears_free_in x e3
| ETrue
| EFalse -> false
We also need a technical lemma connecting free variables and typing
contexts. If a variable x appears free in an expression e, and
if we know that e is well typed in context g, then it must be
the case that g assigns some type to x.
val free_in_context : x:int -> e:exp -> g:env -> Lemma
(requires (Some? (typing g e)))
(ensures (appears_free_in x e ==> Some? (g x)))
let rec free_in_context x e g =
match e with
| EVar _
| ETrue
| EFalse -> ()
| EAbs y t e1 -> free_in_context x e1 (extend g y t)
| EApp e1 e2 -> free_in_context x e1 g; free_in_context x e2 g
| EIf e1 e2 e3 -> free_in_context x e1 g;
free_in_context x e2 g; free_in_context x e3 g
The proof is a straightforward induction. As a corollary for
g == empty we obtain that expressions typable in the empty
environment have no free variables.
val typable_empty_closed : x:int -> e:exp -> Lemma
(requires (Some? (typing empty e)))
(ensures (not(appears_free_in x e)))
[SMTPat (appears_free_in x e)]
let typable_empty_closed x e = free_in_context x e empty
The quantifier pattern [SMTPat (appears_free_in x e)] signals to Z3
that it should consider applying this lemma when its context contains a
term of the form appears_free_in
Sometimes, we know that typing g e = Some t, and we will need to
replace g by an “equivalent” context g'. We still need to define
formally when two environments are equivalent. A natural definition is
extensional equivalence of functions:
logic type equal (g1:env) (g2:env) = forall (x:int). g1 x = g2 x
According to this definition two environments are equivalent if have the
same domain and they map all variables in the domain to the same type.
We remark equal in particular and logical formulas in general are
types in F*, thus the different syntax for this definition.
The context invariance lemma uses in fact a weaker variant of this
equivalence in which the two environments only need to agree on the
variables that appear free in an expression e:
logic type equalE (e:exp) (g1:env) (g2:env) = forall (x:int). appears_free_in x e ==> g1 x = g2 x
The context invariance lemma is then easily proved by induction:
val context_invariance : e:exp -> g:env -> g':env -> Lemma
(requires (equalE e g g'))
(ensures (typing g e == typing g' e))
let rec context_invariance e g g' =
match e with
| EAbs x t e1 ->
context_invariance e1 (extend g x t) (extend g' x t)
| EApp e1 e2 ->
context_invariance e1 g g';
context_invariance e2 g g'
| EIf e1 e2 e3 ->
context_invariance e1 g g';
context_invariance e2 g g';
context_invariance e3 g g'
| _ -> ()
Because equal is a stronger relation than equalE we obtain the
same property for equal:
val typing_extensional : g:env -> g':env -> e:exp -> Lemma (requires (equal g g')) (ensures (typing g e == typing g' e)) let typing_extensional g g' e = context_invariance e g g'
We can use these results to show the following substitution lemma:
val substitution_preserves_typing : x:int -> e:exp -> v:exp -> g:env -> Lemma
(requires (Some? (typing empty v) /\
Some? (typing (extend g x (Some?.v (typing empty v))) e)))
(ensures (Some? (typing empty v) /\
typing g (subst x v e) ==
typing (extend g x (Some?.v (typing empty v))) e))
let rec substitution_preserves_typing x e v g =
let Some t_x = typing empty v in
let gx = extend g x t_x in
match e with
| ETrue -> ()
| EFalse -> ()
| EVar y ->
if x=y
then context_invariance v empty g (* uses lemma typable_empty_closed *)
else context_invariance e gx g
| EApp e1 e2 ->
substitution_preserves_typing x e1 v g;
substitution_preserves_typing x e2 v g
| EIf e1 e2 e3 ->
substitution_preserves_typing x e1 v g;
substitution_preserves_typing x e2 v g;
substitution_preserves_typing x e3 v g
| EAbs y t_y e1 ->
let gxy = extend gx y t_y in
let gy = extend g y t_y in
if x=y
then typing_extensional gxy gy e1
else
(let gyx = extend gy x t_x in
typing_extensional gxy gyx e1;
substitution_preserves_typing x e1 v gy)
The proof proceeds by induction on the expression e; we give the
intuition of the two most interesting cases:
Case
e = EVar yCase
x = y: We havesubst x v e = vand we already know thattyping empty v == Some t_x. However, what we need to show istyping g v == Some t_xfor some arbitrary environmentg. From thetypable_empty_closedlemma we obtain thatvcontains no free variables, so we haveequalE v empty g. This allows us to apply thecontext_invariancelemma to obtain thattyping empty v = typing g vand complete the proof of this case.Case
x <> yWe havesubst x v e = eandtyping gx e = Some t_eand need to show thattyping g e == Some t_e
We have that
EquivE (EVar y) gx gsincex <> yso we can apply thecontext_invariancelemma to conclude.Case
e = EAbs y t_y e1: We have thattyping gxy e1 = Some t_e1, and need to show thattyping gy e1 == Some t_e1Case
x = yWe havesubst x v e = EAbs y t_y e1. Sincex = ythexbinder ingxyis spurious (we haveequal gy gxy) and can apply thetyping_extensionallemma.Case
x <> yWe havesubst x v e = EAbs y t_y (subst x v e1). Sincex <> y(and since we are in a simply typed calculus, not a dependently typed one) we can swap thexandybinding to show thatequal gxy xyx. By thetyping_extensionallemma we obtain thattyping gxy e1 == typing gyx e1. By the induction hypothesis we thus obtain thattyping gy (subst x v e1) == Some t_e1
and by the definition of
typingwe conclude thattyping g (EAbs y t_y (subst x v e)) = t_e.
We now have the tools we need to prove preservation: if a closed
expression e has type t and takes an evaluation step to e',
then e' is also a closed expression with type t. In other words,
the small-step evaluation relation preserves types.
val preservation : e:exp -> Lemma
(requires (Some? (typing empty e) /\ Some? (step e) ))
(ensures (Some? (step e) /\
typing empty (Some?.v (step e)) == typing empty e))
let rec preservation e =
match e with
| EApp e1 e2 ->
if is_value e1
then (if is_value e2
then let EAbs x _ ebody = e1 in
substitution_preserves_typing x ebody e2 empty
else preservation e2)
else preservation e1
| EIf e1 _ _ ->
if not (is_value e1) then preservation e1
We only have two cases to consider, since only applications and
conditionals can take successful execution steps. The case for
e = EIf e1 e2 e3 is simple: either e1 is a value and thus the
conditional reduces to e2 or e3 which by the typing hypothesis
also have type t, or e1 takes a successful step and we can apply
the induction hypothesis. We use the Some?.v projector, which
requires F* to prove that indeed e1 can take a step; this is
immediate since we know that the whole conditional takes a step and we
know that e1 is not a value.
The case for e = EApp e1 e2 is a bit more complex. If e1 steps
or if e1 is a value and e2 steps then we just apply the
induction hypothesis. If both e1 and e2 are values it needs to
be the case that e1 is an abstraction EAbs x targ ebody and
step e = subst x e2 ebody. From the typing assumption we have
typing (extend empty x tags) ebody = Some t and
typing empty e2 = Some targ, so we can use the substitution lemma to
obtain that typing empty (subst x e2 ebody) = Some t, which
concludes the proof.
Exercises for STLC¶
Define a typed_step step function that takes a well-typed expression
e that is not a value and produces the expression to which e
steps. Give typed_step the following strong type (basically this
type captures both progress and preservation):
val typed_step : e:exp{Some? (typing empty e) /\ not(is_value e)} ->
Tot (e':exp{typing empty e' = typing empty e})
(Hint: the most direct solution to this exercise fits on one line)
let typed_step e = progress e; preservation e; Some?.v (step e)
To add pairs to this formal development we add the following to the definition of types, expressions, values, substitution, and step:
type tyP =
(* {{{ 2 unchanged cases *)
| TBoolP : tyP
| TArrowP : tin:tyP -> tout:tyP -> tyP
(* }}} *)
| TPairP : tyP -> tyP -> tyP
type expP =
(* {{{ 6 unchanged cases *)
| EVarP : v:var -> expP
| EAppP : fn:expP -> arg:expP -> expP
| EAbsP : v:var -> vty:tyP -> body:expP -> expP
| ETrueP : expP
| EFalseP : expP
| EIfP : test:expP -> btrue:expP -> bfalse:expP -> expP
(* }}} *)
| EPairP : expP -> expP -> expP
| EFstP : expP -> expP
| ESndP : expP -> expP
let rec is_valueP e = (* Note the extra let rec! *)
match e with
(* {{{ 3 unchanged cases *)
| EAbsP _ _ _
| ETrueP
| EFalseP -> true
(* }}} *)
| EPairP e1 e2 -> is_valueP e1 && is_valueP e2
| _ -> false
let rec substP x e e' =
match e' with
(* {{{ 6 unchanged cases *)
| EVarP x' -> if x = x' then e else e'
| EAbsP x' t e1 ->
EAbsP x' t (if x = x' then e1 else (substP x e e1))
| EAppP e1 e2 -> EAppP (substP x e e1) (substP x e e2)
| ETrueP -> ETrueP
| EFalseP -> EFalseP
| EIfP e1 e2 e3 -> EIfP (substP x e e1) (substP x e e2) (substP x e e3)
(* }}} *)
| EPairP e1 e2 -> EPairP (substP x e e1) (substP x e e2)
| EFstP e1 -> EFstP (substP x e e1)
| ESndP e1 -> ESndP (substP x e e1)
let rec stepP e =
match e with
(* {{{ 2 unchanged cases *)
| EAppP e1 e2 ->
if is_valueP e1 then
if is_valueP e2 then
match e1 with
| EAbsP x t e' -> Some (substP x e2 e')
| _ -> None
else
match (stepP e2) with
| Some e2' -> Some (EAppP e1 e2')
| None -> None
else
(match (stepP e1) with
| Some e1' -> Some (EAppP e1' e2)
| None -> None)
| EIfP e1 e2 e3 ->
if is_valueP e1 then
match e1 with
| ETrueP -> Some e2
| EFalseP -> Some e3
| _ -> None
else
(match (stepP e1) with
| Some e1' -> Some (EIfP e1' e2 e3)
| None -> None)
(* }}} *)
| EPairP e1 e2 ->
if is_valueP e1 then
if is_valueP e2 then None
else
(match (stepP e2) with
| Some e2' -> Some (EPairP e1 e2')
| None -> None)
else
(match (stepP e1) with
| Some e1' -> Some (EPairP e1' e2)
| None -> None)
| EFstP e1 ->
if is_valueP e1 then
(match e1 with
| EPairP v1 v2 -> Some v1
| _ -> None)
else
(match (stepP e1) with
| Some e1' -> Some (EFstP e1')
| None -> None)
| ESndP e1 ->
if is_valueP e1 then
(match e1 with
| EPairP v1 v2 -> Some v2
| _ -> None)
else
(match (stepP e1) with
| Some e1' -> Some (ESndP e1')
| None -> None)
| _ -> None
Add cases to typingP for the new constructs and fix all the proofs.
We extend the typingP and appears_free_inP functions with cases
for EPairP, EFstP, and ESndP:
type envP = int -> Tot (option tyP) val emptyP : envP let emptyP = fun _ -> None
When we move under a binder we extend the typing environment.
val extendP : envP -> int -> tyP -> Tot envP
let extendP g x t = fun x' -> if x = x' then Some t else g x'
val typingP : envP -> expP -> Tot (option tyP)
let rec typingP g e =
match e with
(* {{{ 6 unchanged cases *)
| EVarP x -> g x
| EAbsP x t e1 ->
(match typingP (extendP g x t) e1 with
| Some t' -> Some (TArrowP t t')
| None -> None)
| EAppP e1 e2 ->
(match typingP g e1, typingP g e2 with
| Some (TArrowP t11 t12), Some t2 -> if t11 = t2 then Some t12 else None
| _ , _ -> None)
| ETrueP -> Some TBoolP
| EFalseP -> Some TBoolP
| EIfP e1 e2 e3 ->
(match typingP g e1, typingP g e2, typingP g e3 with
| Some TBoolP, Some t2, Some t3 -> if t2 = t3 then Some t2 else None
| _ , _ , _ -> None)
(* }}} *)
| EPairP e1 e2 ->
(match typingP g e1, typingP g e2 with
| Some t1, Some t2 -> Some (TPairP t1 t2)
| _ , _ -> None)
| EFstP e1 ->
(match typingP g e1 with
| Some (TPairP t1 t2) -> Some t1
| _ -> None)
| ESndP e1 ->
(match typingP g e1 with
| Some (TPairP t1 t2) -> Some t2
| _ -> None)
val appears_free_inP : x:int -> e:expP -> Tot bool
let rec appears_free_inP x e =
match e with
(* {{{ 6 unchanged cases *)
| EVarP y -> x = y
| EAppP e1 e2 -> appears_free_inP x e1 || appears_free_inP x e2
| EAbsP y _ e1 -> x <> y && appears_free_inP x e1
| EIfP e1 e2 e3 ->
appears_free_inP x e1 || appears_free_inP x e2 || appears_free_inP x e3
| ETrueP
| EFalseP -> false
(* }}} *)
| EPairP e1 e2 -> appears_free_inP x e1 || appears_free_inP x e2
| EFstP e1 -> appears_free_inP x e1
| ESndP e1 -> appears_free_inP x e1
The proofs of the lemmas are also easy to extend by just calling the induction hypothesis:
val free_in_contextP : x:int -> e:expP -> g:envP -> Lemma
(requires (Some? (typingP g e)))
(ensures (appears_free_inP x e ==> Some? (g x)))
let rec free_in_contextP x e g =
match e with
(* {{{ 6 unchanged cases *)
| EVarP _
| ETrueP
| EFalseP -> ()
| EAbsP y t e1 -> free_in_contextP x e1 (extendP g y t)
| EAppP e1 e2 -> free_in_contextP x e1 g; free_in_contextP x e2 g
| EIfP e1 e2 e3 -> free_in_contextP x e1 g;
free_in_contextP x e2 g; free_in_contextP x e3 g
(* }}} *)
| EPairP e1 e2 -> free_in_contextP x e1 g; free_in_contextP x e2 g
| EFstP e1
| ESndP e1 -> free_in_contextP x e1 g
logic type equalEP (e:expP) (g1:envP) (g2:envP) =
forall (x:int). appears_free_inP x e ==> g1 x = g2 x
val context_invarianceP : e:expP -> g:envP -> g':envP -> Lemma
(requires (equalEP e g g'))
(ensures (typingP g e == typingP g' e))
let rec context_invarianceP e g g' =
match e with
(* {{{ 3 unchanged cases *)
| EAbsP x t e1 ->
context_invarianceP e1 (extendP g x t) (extendP g' x t)
| EAppP e1 e2 ->
context_invarianceP e1 g g';
context_invarianceP e2 g g'
| EIfP e1 e2 e3 ->
context_invarianceP e1 g g';
context_invarianceP e2 g g';
context_invarianceP e3 g g'
(* }}} *)
| EPairP e1 e2 ->
context_invarianceP e1 g g';
context_invarianceP e2 g g'
| EFstP e1
| ESndP e1 -> context_invarianceP e1 g g'
| _ -> ()
(* {{{ similar definitions skipped *)
logic type equalP (g1:envP) (g2:envP) = forall (x:int). g1 x = g2 x
val typing_extensionalP : g:envP -> g':envP -> e:expP -> Lemma
(requires (equalP g g'))
(ensures (typingP g e == typingP g' e))
let typing_extensionalP g g' e = context_invarianceP e g g'
val typable_empty_closedP : x:int -> e:expP -> Lemma
(requires (Some? (typingP emptyP e)))
(ensures (not(appears_free_inP x e)))
[SMTPat (appears_free_inP x e)]
let typable_empty_closedP x e = free_in_contextP x e emptyP
(* }}} *)
val substitution_preserves_typingP : x:int -> e:expP -> v:expP -> g:envP -> Lemma
(requires (Some? (typingP emptyP v) /\
Some? (typingP (extendP g x (Some?.v (typingP emptyP v))) e)))
(ensures (Some? (typingP emptyP v) /\
typingP g (substP x v e) ==
typingP (extendP g x (Some?.v (typingP emptyP v))) e))
let rec substitution_preserves_typingP x e v g =
(* {{{ unchanged preamble skipped *)
let Some t_x = typingP emptyP v in
let gx = extendP g x t_x in
match e with
| ETrueP -> ()
| EFalseP -> ()
| EVarP y ->
if x=y
then context_invarianceP v emptyP g (* uses lemma typable_empty_closed *)
else context_invarianceP e gx g
| EAppP e1 e2 ->
substitution_preserves_typingP x e1 v g;
substitution_preserves_typingP x e2 v g
| EIfP e1 e2 e3 ->
substitution_preserves_typingP x e1 v g;
substitution_preserves_typingP x e2 v g;
substitution_preserves_typingP x e3 v g
| EAbsP y t_y e1 ->
let gxy = extendP gx y t_y in
let gy = extendP g y t_y in
if x=y
then typing_extensionalP gxy gy e1
else
(let gyx = extendP gy x t_x in
typing_extensionalP gxy gyx e1;
substitution_preserves_typingP x e1 v gy)
(* }}} *)
| EPairP e1 e2 ->
(substitution_preserves_typingP x e1 v g;
substitution_preserves_typingP x e2 v g)
| EFstP e1
| ESndP e1 ->
substitution_preserves_typingP x e1 v g
| _ -> ()
As for the other cases, the preservation proof when e = EPair e1 e2
follows the structure of the step function. If e1 is not a value
then it further evaluates, so we apply the induction hypothesis to
e1. If e1 is a value, then since we know that the pair
evaluates, it must be the case that e2 is not a value and further
evaluates, so we apply the induction hypothesis to it. The cases for
EFst and ESnd are similar.
val preservationP : e:expP{Some? (typingP emptyP e) /\ Some? (stepP e)} ->
Tot (u:unit{typingP emptyP (Some?.v (stepP e)) == typingP emptyP e})
let rec preservationP e =
match e with
(* {{{ two unchanged cases *)
| EAppP e1 e2 ->
if is_valueP e1
then (if is_valueP e2
then let EAbsP x _ ebody = e1 in
substitution_preserves_typingP x ebody e2 emptyP
else preservationP e2)
else preservationP e1
| EIfP e1 _ _ ->
if not (is_valueP e1) then preservationP e1
(* }}} *)
| EPairP e1 e2 ->
(match is_valueP e1, is_valueP e2 with
| false, _ -> preservationP e1
| true , false -> preservationP e2)
| EFstP e1
| ESndP e1 ->
if not (is_valueP e1) then preservationP e1
We want to add a let construct to this formal development. We add the following to the definition of expressions:
type exp =
...
| ELet : int -> exp -> exp -> exp
Add cases for ELet to all definitions and proofs.
Define a big-step interpreter for STLC as a recursive function eval
that given a well-typed expression e either produces the well-typed
value v to which e evaluates or it diverges if the evaluation of
e loops. Give eval the following strong type ensuring that v
has the same type as e (basically this type captures both progress
and preservation):
val eval : e:exp{Some? (typing empty e)} ->
Dv (v:exp{is_value v && typing empty v = typing empty e})
The Dv effect is that of potentially divergent computations. We
cannot mark this as Tot since a priori STLC computations could
loop, and it is hard to prove that well-typed ones don’t.
Here is a solution that only uses typed_step (suggested by Santiago
Zanella):
let rec eval e = if is_value e then e else eval (typed_step e)
or using the progress and preservation lemmas instead of
typed_step (suggested by Guido Martinez):
val eval' : e:exp{Some? (typing empty e)} ->
Dv (v:exp{is_value v && typing empty v = typing empty e})
let rec eval' e = match step e with
| None -> progress e; e
| Some e' -> preservation e; eval' e'
Here is another solution that only uses the substitution lemma:
val eval'' : e:exp{Some? (typing empty e)} ->
Dv (v:exp{is_value v && typing empty v = typing empty e})
let rec eval'' e =
let Some t = typing empty e in
match e with
| EApp e1 e2 ->
(let EAbs x _ e' = eval' e1 in
let v = eval' e2 in
substitution_preserves_typing x e' v empty;
eval'' (subst x v e'))
| EAbs _ _ _
| ETrue
| EFalse -> e
| EIf e1 e2 e3 ->
(match eval'' e1 with
| ETrue -> eval'' e2
| EFalse -> eval'' e3)