The syntax of FH is given in Figure 3.1. For unrefined types we have: base types
B, which must include Bool; type variables α; dependent function types x:T1 →T2
wherex is bound inT2; and universal types∀α.T, whereαis bound inT. Aside from
dependency in function types, these are just the types of the standard polymorphic lambda calculus. As usual, we writeT1 →T2 forx:T1 →T2 whenxdoes not appear
free in T2. We also have predicate contracts, or refinement types, written {x:T | e}.
Conceptually, {x:T |e}denotes values v of typeT for whiche[v/x] reduces totrue. For each B, I fix a set KB of the constants in that type; I require the typing rules for constants and the typing and evaluation rules for operations to respect this set. I also require that KBool={true,false}.
In the syntax of terms, the first line is standard for a call-by-value polymorphic language: variables, constants, several monomorphic first-order operations op (i.e., destructors of one or more base-type arguments), term and type abstractions, and term and type applications. The second line offers the standard constructs of a manifest contract calculus [28, 34, 44], with a few alterations, discussed below.
Casts are the distinguishing feature of manifest contract calculi. When applied to a value of type T1, the cast hT1 ⇒ T2il ensures that its argument behaves—and is
treated—like a value of type T2. When a cast detects a problem, it raises blame, a
label-indexed uncatchable exception written⇑l. The labell allows us to trace blame back to a specific cast. (While labels here are drawn from an arbitrary set, in practice
to support a small-step semantics for checking casts into refinement types. In an active check, {x:T |e1} is the refinement being checked, e2 is the current state of checking,
andv is the value being checked. The type in the first position of an active check isn’t necessary for the operational semantics, but I keep it around as a technical aid to type soundness. If checking succeeds, the check will return v; if checking fails, the check will blame its label, raising⇑l. Active checks and blame are not intended to occur in source programs—they are runtime devices. (In a real programming language based on this calculus, casts will probably not appear explicitly either, but will be inserted by an elaboration phase. The details of this process are beyond my present scope.)
The values in FH are constants, term and type abstractions, and casts. We also
define results, which are either values or blame. (Type soundness—a consequence of Theorems 3.2.25 and 3.2.27 below—will show that evaluation produces a result, but not necessarily a value.) In Chapter 2, casts between function types applied to values were themselves considered values. I make the other choice here: excluding appli- cations from the possible syntactic forms of values simplifies our inversion lemmas. Instead, casts between function types will η-expand their argument. This makes the notion of “function proxy” explicit: the cast semantics adds many new closures. I address this issue in Chapter 4, where I make manifest contracts space-efficient.
There are two notable features relative to existing manifest calculi: first, anytype (even a refinement type) can be refined, not just base types (as in [28, 34, 36, 44, 51]); second, the third part of the active check form h{x:T | e1},e2,vil can be any value,
not just a constant. Both of these changes are motivated by the introduction of polymorphism. In particular, to support refinement of type variables we must allow refinements of all types, since any type can be substituted for a variable.
Operational semantics
The call-by-value operational semantics in Figure 3.2 are given as a small-step rela- tion, split into two sub-relations: one for reductions ( ) and one for congruence and blame lifting (−→).
The latter relation is standard. The E Reducerule lifts reductions into−→;
the E Compat rule turns −→ into a congruence over evaluation contexts; and the E Blame rule lifts blame, treating it as an uncatchable exception. The reduction
relation is more interesting. There are four different kinds of reductions: the stan- dard lambda calculus reductions, structural cast reductions, cast staging reductions, and checking reductions.
The E Beta, and E TBeta rules should need no explanation—these are the
standard call-by-value polymorphic lambda calculus reductions. The E Oprule uses a denotation function [[−]] to give meaning to the first-order operations.
The E Refl, E Fun, and E Forall rules reduce casts structurally. E Refl
eliminates a cast from a type to itself; intuitively, such a cast should always succeed anyway. (I discuss this rule more in Section 3.3.) When a cast between function types is applied to a value v, the E Fun rule produces a new lambda, wrapping v with
Reduction rules e1 e2 op (v1, ... ,vn) [[op]] (v1, ... ,vn) E Op (λx:T1.e12)v2 e12[v2/x] E Beta (Λα.e)T e[T/α] E TBeta hT ⇒Tilv v E Refl hx:T11→T12⇒x:T21→T22ilv E Fun λx:T21.(hT12[hT21⇒T11ilx/x]⇒T22il(v(hT21⇒T11ilx))) whenx:T11→T126=x:T21→T22 h∀α.T1⇒ ∀α.T2ilv Λα.(hT1⇒T2il(vα)) E Forall when∀α.T16=∀α.T2 h{x:T1|e} ⇒T2ilv hT1⇒T2ilv E Forget whenT26={x:T1|e}and T26={y:{x:T1|e} |e2} hT1⇒ {x:T2|e}ilv hT2⇒ {x:T2|e}il(hT1⇒T2ilv) E PreCheck whenT16=T2and T16={x:T0|e0} hT⇒ {x:T |e}ilv h{x:T |e},e[v/x],vil E Check h{x:T |e},true,vil v E OK h{x:T |e},false,vil ⇑l E Fail Evaluation rules e1−→e2 e1 e2 e1−→e2 E Reduce e1−→e2 E[e1]−→E[e2] E Compat E[⇑l]−→ ⇑l E Blame
Figure 3.2: Operational semantics for FH
a contravariant cast on the domain and covariant cast on the codomain. The extra substitution in the left-hand side of the codomain cast may seem suspicious, but in fact the rule must be this way in order for type preservation to hold (see [34] for an explanation). TheE Forall rule is similar toE Fun, generating a type abstraction
with the necessary covariant cast. Side conditions on E Forall and E Fun ensure
that these rules apply only when E Refldoesn’t.
The E Forget,E PreCheck, andE Checkrules are cast-staging reductions,
breaking a complex cast down to a series of simpler casts and checks. All of these rules require that the left- and right-hand sides of the cast be different—if they are the same, then E Refl applies. The E Forget rule strips a layer of refinement
off the left-hand side; in addition to requiring that the left- and right-hand sides are different, the preconditions require that the right-hand side isn’t a refinement of the left-hand side. TheE PreCheck rule breaks a cast into two parts: one that checks
apply this rule when the two sides of the cast are different and when the left-hand side isn’t a refinement. The E Check rule applies when the right-hand side refines
the left-hand side; it takes the cast value and checks that it satisfies the right-hand side. (We don’t have to check the left-hand side, since that’s the type we’re casting
from.)
Before explaining how these rules interact in general, I offer a few examples. First, here is a reduction using E Check, E Compat, E Op, and E OK:
hInt⇒ {x:Int|x ≥0}il5 −→ h{x:Int|x ≥0},5≥0,5il
−→ h{x:Int|x ≥0},true,5il −→5
A failed check will work the same way until the last reduction, which will useE Fail
rather than E OK:
hInt⇒ {x:Int|x ≥0}il(−1) −→ h{x:Int|x ≥0},−1≥0,−1il
−→ h{x:Int|x ≥0},false,−1il −→ ⇑l
Notice that the blame label comes from the cast that failed. Here is a similar reduction that needs some staging, using E Forgetfollowed by the first reduction we gave:
h{x:Int|x = 5} ⇒ {x:Int|x ≥0}il 5 −→ hInt⇒ {x:Int|x ≥0}il5
−→ h{x:Int|x ≥0},5≥0,5il −→∗ 5
There are two cases where we need to use E PreCheck. First, when multiple
refinements are involved:
hInt⇒ {x:{y:Int|y ≥0} |x = 5}il5 −→
h{y:Int|y ≥0} ⇒ {x:{y:Int |y ≥0} |x = 5}il(hInt⇒ {y:Int|y ≥0}il 5) −→∗
h{y:Int|y ≥0} ⇒ {x:{y:Int|y ≥0} |x = 5}il5 −→
h{x:{y:Int|y ≥0} |x = 5},5 = 5,5il −→∗
5
Second, when casting a function or universal type into a refinement of a different
function or universal type.
hBool→ {x:Bool|x} ⇒ {f:Bool→Bool|f true =f false}ilv −→
hBool→Bool⇒ {f:Bool→Bool|f true =f false}il (hBool→ {x:Bool|x} ⇒Bool→Boolilv)
E Reflis necessary for simple cases, likehInt ⇒Intil5−→5. Hopefully, such a silly
cast would never be written, but it could arise as a result of E Fun or E Forall.
(We also need E Refl in our proof of parametricity; see Section 3.3.)
I offer two higher level ways to understand the interactions of these complicated cast rules. First, we can see the reduction rules as an unfolding of a recursive function,
choosing the first clause in case of ambiguity. That is, the operational semantics unfolds a cast hT1 ⇒T2ilv like Cl(T1,T2,v):
Cl(T,T,v) = v Cl({x:T 1 |e},T2,v) = Cl(T1,T2,v) Cl(T 1,{x:T2 |e},v) = let x =Cl(T1,T2,v) in h{x:T2 |e},e,xil Cl(∀α.T 1,∀α.T2,v) = Λα. Cl(T1,T2,v) Cl(x:T 11 →T12,x:T21 →T22,v) = λx:T21.Cl(T12[Cl(T21,T11,x)/x],T22,vCl(T21,T11,x))
Alternatively, the rules firing during the evaluation of a cast in the small-step seman- tics obeys the following regular schema:
Refl|(Forget∗ (Refl|(PreCheck∗ (Refl|Fun|Forall)? Check∗)))
Let’s consider the cast hT1 ⇒ T2ilv. To simplify the following discussion, I define
unref(T) asT without any outer refinements (though refinements on, e.g., the domain of a function would be unaffected); I write unrefn(T) when we remove only the n outermost refinements:
unref(T) = (
unref(T0) if T ={x:T0 |e}
T otherwise
First, if T1 =T2, we can apply E Refl and be done with it. If that doesn’t work,
we’ll reduce by E Forget until the left-hand side doesn’t have any refinements. (N.B. we may not have to make any of these reductions.) Either all of the refinements will be stripped away from the source type, or E Refl eventually applies and the
entire cast disappears. Assuming E Refl doesn’t apply, we now have hunref(T1)⇒
T2ilv. Next, we apply E PreCheck until the cast is completely decomposed into
one-step casts, once for each refinement inT2:
hunref1(T2)⇒T2il(hunref2(T2)⇒unref1(T2)il
(...(hunref(T1)⇒unref(T2)ilv) ...))
As our next step, we apply whichever structural cast rule applies to hunref(T1) ⇒
unref(T2)ilv, one of E Refl,E Fun, orE Forall. Now all that remains are some
number of refinement checks, which can be dispatched by the E Check rule (and
other rules, of course, during the predicate checks themselves).
TheE Reflrule merits some more discussion. On the face of it, a casthT ⇒Til
seems like it can’t do anything: any value it applies must have already had type T, so what could go wrong during any checks? One might worry that adding such a cast will cause a different label to be blamed. In fact, we will find that such casts have no effect in Section 3.4, using the parametricity logical relation. But this safety proof is for a system whereE Reflwas there in the first place! I haven’t been able to prove
parametricty for a system without E Refl. Including it in the system, however, is
a weaker assumption than including subsumption, as earlier systems did [34, 44]. It may be possible to further weaken the E Reflby restricting it to base types.
Static typing
The type system comprises three mutually recursive judgments: context well formed- ness, type well formedness, and term well typing. The rules for contexts and types are unsurprising. The rules for terms are mostly standard. First, the T App rule is
dependent, to account for dependent function types. We have no need for a value restriction because FH doesn’t have effects; any effectful extension, even nontermi-
nation, would force such a restriction. The T Cast rule is standard for manifest
calculi, allowing casts between compatibly structured well formed types. Compati- bility of type structures is defined in Figure 3.4; in short, compatible types erase to identical simple type skeletons. I discuss type compatibility and type conversion (as used in T Conv) below. Note that I assign casts a non-dependent function type.
The T Op rule uses the ty function to assign (possibly dependent) monomorphic
first-order types to operations; I require that ty(op) and [[op]] agree.
Some of the typing rules—T Check, T Blame, T Exact, T Forget, and T Conv—are “runtime only”. These rules aren’t needed to type check source pro-
grams, but we need them to guarantee preservation. T Check, T Exact, and
T Conv are excluded from source programs because I don’t want the typing of
source programs to rely on the evaluation relation; such an interaction is acceptable in this setting, but disrupts the phase distinction and is ultimately incompatible with nontermination and effects. I exclude T Blame because programs shouldn’t start
with failures. Finally, I exclude T Forget because I imagine that source programs
have all type changes explicitly managed by casts. In explicitly tagged calculus, as described in Section 3.5 and Chapter 4, we are able to abandon theT Forgetrule in
its entirety. Note that the conclusions of these rules use a context Γ, but their premises don’t use Γ at all. Even though runtime terms and their typing rules should only ever occur in an empty context, the T App rule substitutes terms into types—so a run-
time term could end up under a binder. I therefore allow the runtime typing rules to apply in any well formed context, so long as the terms they type check are closed. The
T Blamerule allows us to give any type to blame—this is necessary for preservation.
TheT Checkrule types an active check,h{x:T |e1},e2,vil. Such a term arises when
a term like hT ⇒ {x:T | e1}ilv reduces by E Check. The premises of the rule are
all intuitive except fore1[v/x]−→∗ e2, which is necessary to avoid nonsensical terms
likeh{x:T |x ≥0},true,−1il, where the wrong predicate gets checked. In Chapter 2, we have a similar but looser requirement: e2 −→∗ trueimpliese1[v/x]−→∗ true. The
proofs don’t change radically, but I use the more syntactic requirement here because I aim to keep the system as syntactic as possible. The T Exact rule allows us to
retype a closed value of typeT at{x:T |e}ife[v/x]−→∗ true. This typing rule guar-
antees type preservation for E OK: h{x:T | e1},true,vil −→ v. If the active check
was well typed, then we know that e1[v/x] −→∗ true, so T Exact applies. Earlier
systems used most-specific types and subtyping to show that the E OK rule pre-
serves typing. While the “most specific” requirement is abstract, a constant k ∈ KB is typically given the selfified type ty(k) ={x:B | x = k} [51]. But functions don’t
Context well formedness `Γ ` ∅ WF Empty `Γ Γ`T `Γ,x:T WF ExtendVar `Γ ` Γ, α WF ExtendTVar
Type well formedness Γ`T
` Γ Γ`B WF Base ` Γ α∈Γ Γ`α WF TVar Γ, α`T Γ` ∀α.T WF Forall Γ`T1 Γ,x:T1`T2 Γ`x:T1→T2 WF Fun Γ`T Γ,x:T`e:Bool Γ` {x:T |e} WF Refine Term typing Γ`e:T `Γ x:T ∈Γ Γ`x :T T Var `Γ Γ`k:ty(k) T Const ∅ `T `Γ Γ` ⇑l :T T Blame Γ`T1 Γ,x:T1`e12:T2 Γ`λx:T1.e12:x:T1→T2 T Abs Γ`e1: (x:T1→T2) Γ`e2:T1 Γ`e1e2:T2[e2/x] T App `Γ ty(op) =x1:T1→ ... →xn:Tn →T Γ`ei :Ti[e1/x1, ...,ei−1/xi−1] Γ`op (e1, ... ,en) :T[e1/x1, ...,en/xn] T Op Γ, α`e:T Γ`Λα.e:∀α.T T TAbs Γ`e1:∀α.T Γ`T2 Γ`e1T2:T[T2/α] T TApp Γ`T1 Γ`T2 T1kT2 Γ` hT1⇒T2il :T1→T2 T Cast `Γ ∅ ` {x:T |e1} ∅ `v :T ∅ `e2:Bool e1[v/x]−→∗e2 Γ` h{x:T |e1},e2,vil :{x:T |e1} T Check `Γ ∅ `e:T ∅ `T0 T ≡T0 Γ`e:T0 T Conv ∅ `v :{x:T |e} `Γ Γ`v :T T Forget ` Γ ∅ `v:T ∅ ` {x:T|e} e[v/x]−→∗true Γ`v:{x:T |e} T Exact
admit a decidable equality, so there isn’t an obvious way to assign them most-specific types. T Exact is a suitably extensional,syntactic, and subtyping-free replacement
for the earlier semantic requirement: constants and functions can be assigned less spe- cific types, but we can useT Exactin the preservation proof to remember successful
checks. Finally, theT Convrule allows us to retype expressions at convertible types:
if ∅ ` e :T and T ≡T0, then ∅ ` e : T0 (or in any well formed context Γ). I define
≡ as an explicitly substitutive but otherwise structural type conversion relation in Figure 3.4. The T Conv rule is necessary to prove preservation in the case where e1e2 −→e1e20. Why? The first term is typed atT2[e2/x] (byT App), but reapplying