For readers interested in mechanized metatheory, here's the mechanized version of this post in Isabelle.
Syntax
This time we do not need to put the language in A-normal form. To review, we study a simple but Turing-complete language with integers, Booleans, and first-class functions that may be recursive. typesT::=Int∣Bool∣T→Tvariablesx,y,zintegersnoperatorso::=+∣−∣=Booleansb::=true∣falseconstantsc::=n∣bexpressionse::=c∣o(e,e)∣x∣funf(x:T)e∣e(e)Dynamic Semantics
As before, we use the notation ϵ for the empty list. Given a list L, the notation a⋅L is a larger list with a as the first element and the rest of the elements are the same as L. We use lists of key-value pairs (association lists) to represent mapping from variables to types (type environments, written Γ) and variables to values (environments, written ρ). The following lookup function finds the thing associated with a given key in an association list. The return type of lookup and the use of stuck and return deserves a little explanation, which I give below. lookup:α×(α×β)list→βresultlookup(K1,ϵ)=stucklookup(K1,(K2,V)⋅L)={returnVif K1=K2lookup(K1,L)otherwise
One might normally consider lookup as a partial function,
because it might fail to find a matching key in the association list.
However, here we're going to make the partiality explicit by returning
a special result called stuck. We'll also include
timeout in the kinds of results, which we explain shortly.
datatypeαresult=Resultα∣Stuck∣TimeOut
We define the following auxiliary notation for dealing with the
result datatype. The most import definition is the last
line, which helps us avoid cluttering our code with lots
of case's.
returna≡Resultastuck≡Stucktimeout≡TimeOutX←M1;M2≡caseM1ofStuck⇒Stuck∣TimeOut⇒TimeOut∣ResultR⇒[X:=R]M2
The values of this language (the results of evaluation) are constants
and closures. A closure pairs a function with an environment ρ.
valuesv::=c∣⟨f(x:T)e,ρ⟩
In many places within the interpreter we're going to extract an
integer, Boolean, or closure from a value. The extraction might fail
because, for example, even though we may want to extract an integer
the value may instead be a Boolean.
toInt(n)=returnntoInt(b)=stucktoInt(⟨f(x:T)e,ρ⟩)=stucktoBool(n)=stucktoBool(b)=returnbtoBool(⟨f(x:T)e,ρ⟩)=stucktoClosure(n)=stucktoClosure(b)=stucktoClosure(⟨f(x:T)e,ρ⟩)=return(f,x,e,ρ)
Next we define the δ function, which gives meaning to the
primitive operators.
δ(+,v1,v2)=n1←toInt(v1);n2←toInt(v2);return(n1+n2)δ(−,v1,v2)=n1←toInt(v1);n2←toInt(v2);return(n1−n2)δ(=,n1,n2)=n1←toInt(v1);n2←toInt(v2);return(n1=n2)
The evaluation function V is a mostly-standard
closure-based interpreter. The one thing that is a bit unusual is
that we make sure that the interpreter is a total function by ensuring
termination. The interpreter includes a third parameter that is a
counter. If the counter gets to zero, the result is
timeout. (This counter technique was described in the
earlier blog post.)
V(e,ρ,0)=timeoutV(x,ρ,1+k)=lookup(x,ρ)V(c,ρ,1+k)=returncV(funf(x:T)e,ρ,1+k)=return⟨f(x:T)e,ρ⟩V(o(e1,e2),ρ,1+k)=v1←V(e1,ρ,k);v2←V(e2,ρ,k);δ(o,v1,v2)V(e1e2,ρ,1+k)=v1←V(e1,ρ,k);v2←V(e2,ρ,k);(f,x,e,ρ′)←toClosure(v1);V(e,(x,v2)⋅(f,v1)⋅ρ′,k)
To finish off the dynamic semantics we must define eval
which specifies the behavior of whole programs. The behavior of a
program is to either return a constant, return diverge
which indicates that the program runs forever, or the behavior is
undefined.
eval(e)={cif V(e,ϵ,n)=Resultc for some ndivergeif ∀n.V(e,ϵ,n)=TimeOut
Type System
The types for the constants is given by the typeof partial function. typeof(n)=Inttypeof(true)=Booltypeof(false)=Bool
The Δ partial function maps a primitive operator and argument types to
the return type.
Δ(+,Int,Int)=IntΔ(−,Int,Int)=IntΔ(=,Int,Int)=Bool
The following presents the type rules for expressions.
Γ⊢e:T
lookup(x,Γ)=ResultTΓ⊢x:Ttypeof(c)=TΓ⊢c:T(x,T1)⋅(f,T1→T2)⋅Γ⊢e:T2Γ⊢funf(x:T1)e:T1→T2Γ⊢e1:T1→T2Γ⊢e2:T1Γ⊢e1(e2):T2Δ(o,T1,T2)=T3Γ⊢e1:T1Γ⊢e2:T2Γ⊢o(e1,e2):T3
Our proof of type safety will require that we define notions of
well-typed values, results, and environments.
⊢v:T typeof(c)=T⊢c:TΓ⊢ρ(x,T1)⋅(f,T1→T2)⋅Γ⊢e:T2⊢⟨f(x:T1)e,ρ⟩:T1→T2
⊢r:T
⊢v:T⊢Resultv:T⊢TimeOut:T
Γ⊢ρ
ϵ⊢ϵ⊢v:TΓ⊢ρ(x,T)⋅Γ⊢(x,v)⋅ρ
Type Safety
The proof of type safety, as promised, includes just three easy lemmas. The first two lemmas are essentially identical to the corresponding lemmas in ``Type Safety in Five''. They establish that the primitive operators and lookup on environments are sound with respect to Δ and lookup on type environments, respectively. The third lemma is the main event of the proof, showing that V is sound with respect to the type system. Because we know that V terminates, we can do the proof by induction on the definition of V, which helps to streamline the proof. We cap off the proof with the Type Safety theorem, which follows almost immediately from the soundness of V.Lemma (δ is safe)
If Δ(o,¯T)=T and ⊢vi:Ti for i∈{1,…,n}, then δ(o,¯v)=Resultv and ⊢v:T, for some v.
Proof. We proceed by cases on the operator o.
- If the operator o is +, then we have T1=T2=Int and T=Int. Then because ⊢vi:Int, we know that vi=ni for i∈{1,2}. Then δ(+,n1,n2)=Result(n1+n2) and we have ⊢(n1+n2):Int.
- If the operator o is −, then we have T1=T2=Int and T=Int. Then because ⊢vi:Int, we know that vi=ni for i∈{1,2}. Then δ(−,n1,n2)=Result(n1−n2) and we have ⊢(n1−n2):Int.
- If the operator o is =, then we have T1=T2=Int and T=Bool. Then because ⊢vi:Int, we know that vi=ni for i∈{1,2}. Then δ(=,n1,n2)=Result(n1=n2) and we have ⊢(n1=n2):Bool.
Lemma (lookup is safe)
If Γ⊢ρ and lookup(x,Γ)=ResultT, then lookup(x,ρ)=Resultv and ⊢v:T for some v.
Proof. We proceed by induction on Γ⊢ρ.
- Case ϵ⊢ϵ:(Γ=ϵ,ρ=ϵ)
But then we have a contradition with the premise lookup(x,Γ)=ResultT, so this case is vacuously true. - Case ⊢v:T′Γ′⊢ρ′(x′,T′)⋅Γ′⊢(x′,v)⋅ρ′:
Next we consider two cases, whether x=x′ or not.- Case x=x′: Then lookup(x,ρ)=Resultv and T=T′, so we conclude that ⊢v:T.
- Case x≠x′: Then lookup(x,ρ)=lookup(x,ρ′) and lookup(x,Γ)=lookup(x,Γ′)=ResultT. By the induction hypothesis, we have lookup(x,ρ′)=Resultv and ⊢v:T for some v, which completes this case.
Lemma (V is safe)
If Γ⊢e:T and Γ⊢ρ, then ⊢V(e,ρ,k):T.
Proof. The proof is by induction on the definition of V.
- Case V(e,ρ,0): We have V(e,ρ,0)=TimeOut and ⊢TimeOut:T.
- Case V(x,ρ,1+k): We have V(x,ρ,1+k)=lookup(x,ρ). From Γ⊢x:T we have lookup(x,Γ)=ResultT. Then by the lookup is safe lemma, we have lookup(x,ρ)=Resultv and ⊢v:T for some v. Thus we have ⊢lookup(x,ρ):T and conclude ⊢V(x,ρ,1+k):T.
- Case V(c,ρ,1+k): From Γ⊢c:T we have typeof(c)=T and therefore ⊢c:T. We have V(c,ρ)=Resultc and therefore ⊢V(c,ρ,1+k):T.
- Case V(funf(x:T1)e1,ρ,1+k):
We have V(funf(x:T1)e1,ρ,1+k)=Result⟨f(x:T1)e1,ρ⟩. From Γ⊢funf(x:T1)e1:T we have (x,T1)⋅(f,T1→T2)⋅Γ⊢e1:T2, with T=T1→T2. Together with Γ⊢ρ, we have ⊢⟨f(x:T1)e1,ρ⟩:T and therefore ⊢V(funf(x:T1)e1,ρ,1+k):T. - Case V(o(e1,e2),ρ,1+k)):
We have Δ(o,T1,T2)=T and Γ⊢e1:T1 and Γ⊢e2:T2. By the induction hypothesis, we have V(e1,ρ,k)=Resultv1 and ⊢v1:T1 and V(e2,ρ,k)=Resultv2 and ⊢v2:T2. Because δ is safe, we have δ(o,v1,v2)=Resultv3 and ⊢v3:T for some v3. We have ⊢δ(o,v1,v2):T and therefore ⊢V(o(e1,e2),ρ,1+k)):T. - Case V(e1e2,ρ,1+k): We have Γ⊢e1:T2→T and Γ⊢e2:T2. By the induction hypothesis, we have V(e1,ρ,k)=Resultv1 and ⊢v1:T2→T and V(e2,ρ,k)=Resultv2 and ⊢v2:T2. By cases on ⊢v1:T2→T, we have that v1=⟨f(x:T2)e3,ρ′⟩ and Γ′⊢ρ′ and (x,T2)⋅(f,T2→T)⋅Γ′⊢e3:T for some f,x,e3,Γ′,ρ′. So we have V(e1e2,ρ,1+k)=V(e3,(x,v2)⋅(f,v1)⋅ρ′,k). Applying the induction hypothesis for a third time, we have ⊢V(e3,(x,v2)⋅(f,v1)⋅ρ′,k):T. Therefore ⊢V(e1e2,ρ,1+k):T.
Theorem (Type Safety)
If ϵ⊢e:T and T∈{Int,Bool}, then either eval(e)=c and ⊢c:T for some c or eval(e)=diverge .
Proof. We consider two cases: whether the program diverges or not.
- Suppose that V(e,ϵ,k)=TimeOut for all k. Then eval(e)=diverge.
- Suppose it is not the case that V(e,ϵ,k)=TimeOut for all k. So V(e,ϵ,k′)≠TimeOut for some k′. Then because V is safe, we have V(e,ϵ,k′)=Resultv and ⊢v:T. Then from T∈{Int,Bool} and by cases on ⊢v:T, we have v=c for some c.
I was curious to try how this proof would go through in Coq, and the result was - surprisingly easy. (The current Coq source is here: https://gist.github.com/dkrustev/5890291) As I used Coq's functional induction (for the first time) for the 3rd lemma, it required considering 16 cases, not 6 (including cases when some intermediate computation times out or gets stuck), but most of these cases were easy to deal with. One thing I realized only during the Coq formalization - though, in retrospect, it should have been obvious from the beginning - was that while all lemmas have constructive proofs, the main theorem does not. It appears this non-constructiveness follows from the way \mathit{eval} is defined. I still wonder if another definition of \mathit{eval} would be possible, giving both fully constructive proofs and a similar level of simplicity ... Another minor point: the \mathit{typeof} function is actually total in the Coq formalization, but it probably depends on the exact encoding of the language syntax.
ReplyDeleteGreat! Thanks for sharing!
DeleteThat would be interesting if you could find a constructive way to define eval. (Though I wouldn't hold my breadth.)
I forget why I said typeof is partial... in my Isabelle version it is total. I've now linked to my Isabelle development at the beginning of the post.
Hi Jeremy, awesome post thanks! The link to the Isabelle development is not working anymore. Is it still available somewhere? Thanks! Ranjit.
DeleteYes, darn Dropbox change a while back regarding Public directory access. I've fixed the link. Thanks for pointing this out!
DeleteJust a quick typo: Turing-complete is misspelled as Turning-complete.
ReplyDeleteThanks, fixed!
Delete