Back in Part 1 of this series, I mentioned that there is a design choice between eager and lazy cast checking. Recall the following example. letf=(λx:Int.incx):Int→Int⇒ℓ0⋆⇒ℓ1Bool→Boolinftrue With eager cast checking, the cast labeled ℓ1 fails at the moment when it is applied to a value. Whereas with lazy cast checking, the ℓ1 cast initially succeeds, but then later, when the function is applied at ftrue, the cast fails. I like eager cast checking because it tells the programmer as soon as possible that something is amiss. Further, it turns out that when using the space-efficient implementations, eager and lazy checking are about the same regarding run-time overhead. (Lazy can be faster if you don't care about space efficiency.)
We saw the specification for lazy cast checking in Part 1, but the specification for eager checking was postponed. The reason for the postponement was that specifying the semantics of eager cast checking requires more machinery than for lazy cast checking. (I'll expand on this claim in the next paragraph.) Thankfully, in the meantime we've acquired the necessary machinery: the Coercion Calculus. The Eager Coercion Calculus was first discussed in the paper Space-Efficient Gradual Typing and was extended to include blame labels in Exploring the Design Space of Higher-Order Casts. Here we'll discuss the version with blame labels and flesh out more of the theory, such as characterizing the coercion normal forms and defining an efficient method of composing coercions in normal form. This is based on an ongoing collaboration with Ronald Garcia.
Before getting into the eager coercion calculus, let me take some time to explain why the eager coercion calculus is needed for the semantics, not just for an efficient implementation. After all, there was no mention of coercions in the semantics of the lazy variants of the Gradually-Typed Lambda Calculus. Instead, those semantics just talked about casts, which consisted of a pair of types (source and target) and a blame label. The heart of those semantics was a cast function that applies a cast to a value.
It's instructive to see where naive definitions of a cast function for eager checking break down. The most obvious thing to try is to modify the cast function to check for (deep) type consistency instead of only looking at the head of the type. So we change the first line of the cast function from cast(v,T1,ℓ,T2)=blameℓif hd(T1)≁hd(T2) to cast(v,T1,ℓ,T2)=blameℓif T1≁T2 Let's see what happens on an example just a tad different from the previous example. In this example we go through ⋆→⋆ instead of ⋆. letf=(λx:Int.incx):Int→Int⇒ℓ0⋆→⋆⇒ℓ1Bool→Boolinftrue With the naive cast function, both casts initially succeed, producing the value (λx:Int.incx):Int→Int⇒ℓ0⋆→⋆⇒ℓ1Bool→Bool However, that's not what an eager cast checking should do. The above should not be a value, it should have already failed and blamed ℓ0.
So the cast function not only needs to check whether the target type is consistent with the source type, it also needs to check whether the target type is consistent with all of the casts that are wrapping the value. One way we could try to do this is to compute the greatest lower bound (with respect to naive subtyping) of all the types in the casts on the value, and then compare the target type to the greatest lower bound. The meet operator on types is defined as follows: B⊓B=B⋆⊓T=TT⊓⋆=T(T1→T2)⊓(T3→T4)=(T1⊓T3)→(T2⊓T4)if (T1→T2)∼(T3→T4)T1⊓T2=⊥if T1≁T2 We introduce the bottom type ⊥ so that the meet operator can be a total function. Next we define a function that computes the meet of all the casts wrapping a value. ⊓(s:T1⇒ℓT2)=T1⊓T2⊓(v:T1⇒ℓ1T2⇒ℓ1T3)=(⊓(v:T1⇒ℓ1T2))⊓T3 Now we can replace the first line of the cast function to use this meet operator. cast(v,T1,ℓ,T2)=blameℓif (⊓v)≁T2 How does this version fare on our example? An error is now triggered when the value flows into the cast labeled ℓ1, so that's good, but the blame goes to ℓ1. Unfortunately, the prior work on eager checking based on coercions says that ℓ0 should be blamed instead! The problem with this version of cast is that the ⊓ operator forgets about all the blame labels that are in the casts wrapping the value. In this example, it's dropping the label ℓ0 which really ought to be blamed.
The Eager Coercion Calculus
In the context of the Coercion Calculus, one needs to add the following two reduction rules to obtain eager cast checking. What these rules do is make sure that failure coercions immediately bubble up to the top of the coercion where they can trigger a cast failure. (Failℓ→c)⟶Failℓ(ˆc→Failℓ)⟶Failℓ In the second rule, we require that the domain coercion be in normal form, thereby imposing a left-to-right ordering for coercion failures.
To ensure confluence, we also need to make two changes to existing reduction rules. In the rule for composing function coercions, we need to require that the two coercions be in normal form. (The notation ˜c is new and will be explained shortly.) (˜c11→˜c12);(˜c21→˜c22)⟶(˜c21;˜c11)→(˜c12;˜c22) Here's the counter-example to confluence, thanks to Ron, if the above restriction is not made. (Failℓ1→c1);(Failℓ2→c2)⟶Failℓ1;(Failℓ2→c2)⟶Failℓ1(Failℓ1→c1);(Failℓ2→c2)⟶(Failℓ2;Failℓ1)→(c1;c2)⟶Failℓ2→(c1;c2)⟶Failℓ2 There is also a confluence problem regarding the following rule. ¯c;Failℓ⟶Failℓ The counter-example, again thanks to Ron, is (ι→Bool!);(ι→Int?ℓ2);Failℓ1⟶(ι;ι)→(Bool!;Int?ℓ2);Failℓ1⟶∗ι→(Failℓ2);Failℓ1⟶∗Failℓ2(ι→Bool!);(ι→Int?ℓ2);Failℓ1⟶(ι→Bool!);Failℓ1⟶Failℓ1 We fix this problem by making the reduction rule more specific, by only allowing injections to be consumed on the left of a failure. I!;Failℓ⟶Failℓ
Here's the complete set of reduction rules for the Eager Coercion Calculus. I1!;I2?ℓ⟶C(I1⇒ℓI2)(˜c11→˜c12);(˜c21→˜c22)⟶(˜c21;˜c11)→(˜c12;˜c22)Failℓ;c⟶FailℓI!;Failℓ⟶Failℓ(Failℓ→c)⟶Failℓ(˜c→Failℓ)⟶Failℓ
These additions and changes to the reduction rules cause changes in
the normal forms for coercions. First, Failℓ cannot
appear under a function coercion We therefore introduce another
category, called ``normal parts'' and written ˜c, that
excludes Failℓ (but still includes I?ℓ1;Failℓ2 because the ℓ1 projection could still
fail and take precedence over ℓ2). Also,
(˜c1→˜c2);Failℓ is now a normal
form. Further, to regularize the form that coercions can take,
we always write them as having three parts.
The following grammar defines the normal coercions for eager
cast checking.
optional injectionsi::=ι∣I!i⊥::=i∣Failℓoptional functionsf::=ι∣˜c→˜cf⊥::=f∣Failℓoptional projectionsj::=ι∣I?ℓwrapper coercions¯c::=ι;f;i†normal parts˜c::=j;f;i⊥‡normal coercionsˆc::=˜c∣ι;ι;Failℓ
† The coercion (ι;ι;ι) is not a wrapper coercion.
‡ The coercion (ι;ι;Failℓ) is not a normal part.
The Eager Gradually-Typed Lambda Calculus
Taking a step back, recall that we gave the semantics of the Lazy Gradually-Typed Lambda Calculus in terms of a denotational semantics, based on an evaluation function E. We can do the same for the Eager variant but using coercions to give the meaning of casts. The following is the definition of values and results for the Eager variant. F∈V→cRvaluesv∈V::=k∣F∣v:¯cresultsr∈R::=v∣blameℓ
Most of the action in the E function is in the cast auxiliary function. We will give an alternative version of cast for eager checking. To make cast more succinct we make use of the following helper function regarding cast failure. isfail(c,ℓ)≡(c=Failℓ or c=Failℓ∘(˜c1→˜c2) for some ˜c1 and ˜c2) Here's the updated definition of cast for eager checking. cast(˜v,ˆc)={˜vif ˆc=ιblameℓif isfail(ˆc,ℓ)˜v:ˆcotherwisecast(˜v:¯c1,ˆc2)={˜vif (¯c1;ˆc2)=ιblameℓif (¯c1;ˆc2)⟶∗ˆc3 and isfail(ˆc3,ℓ)˜v:¯c3if (¯c1;ˆc2)⟶∗¯c3
We can now give the definition of E, making use of the above cast function as well as a function C for compiling casts to coercions. (Use CD or CUD for C to obtain the D or UD blame tracking strategy.) E(k,ρ)=returnkE(x,ρ)=returnρ(x)E(λx:T.e,ρ)=return(λv.E(e,ρ[x↦v]))E(op(e))=letBX=E(e,ρ)inδ(op,X)E(e:T1⇒ℓT2)=letBX=E(e,ρ)incast(X,C(T1⇒ℓT2))E(e1e2)=letBX1=E(e1,ρ)inletBX2=E(e2,ρ)inapply(X1,X2)
The semantics for the Eager Gradually-Typed Lambda Calculus is defined by the following eval partial function. eval(e)={observe(r)if ∅⊢e⇝e′:T and E(e′,∅)=r⊥otherwise where observe(k)=kobserve(F)=functionobserve(v:ι∘(ˆc1→ˆc2)∘ι)=functionobserve(v:I!∘ι∘ι)=dynamicobserve(blameℓ)=blameℓ
An Eager Space-Efficient Machine
To obtain a space efficient machine for the Eager variant, we just plug the eager version of cast into the lazy space-efficient machine.
An Eager Time-Efficient Machine
Recall that the lazy time-efficient machine used threesomes instead of coercions because we could define an efficient function for composing threesomes, whereas reducing coercions is a complex process. The natural thing to do here is to try and come up with an eager variant of threesomes and the composition function. The lazy threesomes were isomorphic to lazy coercions in normal form, and we already have the normal forms for eager coercions, so it should be straightforward to come up with eager threesomes. It is straightforward, but in this case nothing is gained; we just end up with a slightly different notation. The reason is that the normal forms for eager coercions are more complex. So we might as well stick with using the eager coercions.
However, the essential lesson from the threesomes is that we don't need to implement reduction on coercions, instead we just need to define a composition function that takes coercions in normal form. After thinking about this for a long time, trying lots of variants, we've come up with the definition shown below. (Here we use ⊳ for composition. I'd prefer to use the fatsemi latex symbol, but it seems that is not available in MathJax.)
Composition of Normal Coercions: ˆc⊳ˆc
(j;f;i⊥)⊳(j′;f′;i′⊥)=casei⊥⊳j′ofI!⇒j;f;(I!⊳i′⊥)∣I?ℓ⇒I?ℓ;f′;i′⊥∣Failℓ⇒j;f;Failℓ∣c⇒case(f⊳c)⊳f′ofFailℓ⇒j;ι;Failℓ∣c′⇒j;c′;i′⊥
ι⊳c=cc⊳ι=cI1!⊳I2?ℓ=C(I1⇒ℓI2)(˜c1→˜c2)⊳(˜c3→˜c4)=(~c3⊳~c1)∙→(˜c2⊳˜c4)Failℓ⊳c=FailℓI!⊳Failℓ=Failℓ˜c1∙→˜c2=˜c1→˜c2Failℓ∙→ˆc2=Failℓ˜c1∙→Failℓ=Failℓ
To obtain an eager, time-efficient machine, we just replace coercion reduction with coercion composition. cast(˜v,ˆc)={˜vif ˆc=ιblameℓif isfail(ˆc,ℓ)˜v:ˆcotherwisecast(˜v:¯c1,ˆc2)={˜vif (¯c1;ˆc2)=ιblameℓif (¯c1⊳ˆc2)=ˆc3 and isfail(ˆc3,ℓ)˜v:¯c3if (¯c1⊳ˆc2)=¯c3
No comments:
Post a Comment