Lectures onType Theory
Chapter 133
Chapter 133Optional

Typed Intermediate Languages and Certified Closure Conversion

Prerequisites. Direct starred prerequisites: none. System F and existential abstraction supply the prerequisites; no compiler experience is assumed. No later core chapter depends on this route.

Take the factorial program efact=(fix f(n:int):int.if0(n,1,n×f(n1)))6 and ask what survives compilation. After the program has been rewritten so that every control transfer is a jump, every function is closed and carries its environment explicitly, every tuple is allocated field by field, and every value lives in a machine register, the object that remains is a sequence of instructions over a heap and a register file. Nothing in the erased evaluator for efact says which registers a jump target expects to be live, whether a field of a freshly allocated tuple has been written before it is read, or which values a code block may treat as its own environment. Those are exactly the facts a consumer of the compiled code would have to trust.

Attempting to keep the source types is not enough. efact has type int, and so does the whole compiled program; that one type says nothing about the four intermediate representations through which the program passed. What is needed is a type system for each intermediate language, and a theorem for each pass saying that the pass maps well-typed input to well-typed output. This chapter constructs that sequence, following Morrisett, Walker, Crary and Glew (1999), whose Figures 2–21 supply the five languages, the five translations, and the assembly-level safety theorem.

The source language

Definition 133.1 — λ ^F

τ::=αintτ1τ2α.ττ1,,τn,u::=xifix x(x1:τ1):τ2.ee1e2Λα.ee[τ]e1,,enπi(e)e1pe2if0(e1,e2,e3),e::=uτ, where p ranges over +,,×. Every term is annotated with its type, so that each translation below is a function of the term rather than of a typing derivation. Typing is Δ;ΓFe:τ with Δ a set of type variables; the rules are the usual ones for System F with integers, tuples and a recursive function form.

The choice of fix rather than λ matters exactly once: λF has general recursion, so no translation below may appeal to normalization, and every theorem is about typing rather than termination.

Continuations

Definition 133.2 — λ ^K

Types, values, declarations and terms are τ::=αint[α1,,αm].(τ1,,τn)voidτ1,,τn,v::=xifix x[α](x1:τ1,,xn:τn).ev1,,vnv[τ],e::=let d in ev[σ](v1,,vn)if0(v,e1,e2)halt[τ]v. A function does not return: its result type is void, and a call is a jump. Only values have types; the judgment for terms is Δ;ΓKe, with no type on the right.

Definition 133.3 — CPS translation

On types, [[α]]K=α,[[int]]K=int,[[τ1τ2]]K=([[τ1]]K,Cont[[τ2]]K)void,[[α.τ]]K=[α].(Cont[[τ]]K)void, with Contτ=(τ)void and tuples translated componentwise. On terms, [[e]]Kk computes e and hands the result to the continuation k; the two clauses that determine the shape of the rest are [[(u1τ1u2τ2)τ]]Kk=[[u1τ1]]K(λx1:[[τ1]]K.[[u2τ2]]K(λx2:[[τ2]]K.x1(x2,k))),[[uτ]]K,prog=[[uτ]]K(λx:[[τ]]K.halt[[[τ]]K]x). The variables x1, x2, c and x are chosen outside the free variables of the terms and continuations already fixed.

Lemma 133.4 — CPS type correctness

If ;Fe:τ then ;K[[e]]K,prog.

Proof of Lemma 133.4 — CPS type correctness

Proof. Strengthen the statement to the form the induction actually needs: if Δ;ΓFe:τ and Δ;[[Γ]]KKk:Cont[[τ]]K, then Δ;[[Γ]]KK[[e]]Kk. The unstrengthened statement is not an induction hypothesis, because the translation of a subterm is applied to a continuation built by the translation of its context.

Induct on the λF typing derivation. Variable and integer. [[xτ]]Kk=k(x), and the call rule of λK applies because k has type Cont[[τ]]K and x has type [[τ]]K in [[Γ]]K.

Application. Let Δ;ΓFu1:τ1τ2 and Δ;ΓFu2:τ1. The inner continuation λx2:[[τ1]]K.x1(x2,k) is well typed in [[Γ]]K,x1:[[τ1τ2]]K, because [[τ1τ2]]K=([[τ1]]K,Cont[[τ2]]K)void is exactly the type of a function taking the argument and the continuation. It therefore has type Cont[[τ1]]K, which is what the induction hypothesis for u2 requires. The outer continuation is then well typed at Cont[[τ1τ2]]K, which is what the induction hypothesis for u1 requires.

Recursive function. Its translation is the call k(fix x(x1:[[τ1]]K,c:Cont[[τ2]]K).[[e]]Kc). Applying the induction hypothesis to the body with the continuation variable c gives the body’s well-formedness; the λK function rule then types the value at [[τ1τ2]]K, and the call rule applies k to it.

Type abstraction and application, tuples, projection, arithmetic and if0 follow the same two moves: build the continuation demanded by the induction hypothesis of each subterm, and apply the λK rule whose premises the hypotheses supply. The if0 case is the only one that uses k twice; both uses are at the same type, so no duplication of typing obligations arises. Finally, λx:[[τ]]K.halt[[[τ]]K]x has type Cont[[τ]]K, which discharges the strengthened statement at the top level. ◻

Exercise 133.1

★☆☆ Compute [[(1+2)int]]K,prog in full, and mark each redex whose contraction changes no observable behaviour. Say why lemma 133.4 still holds if those redexes are contracted, and which premise of the proof you had to recheck.

Closures

A λK function may have free variables; a code block on a machine may not. Closure conversion makes the free variables an explicit argument. The difficulty is polymorphism: if the function also has free type variables, a naive translation must store types in the environment, and the environment’s type then depends on the types it stores.

Definition 133.5 — λ ^C

Extend λK with existential types α.τ, the value form pack[τ1,v] as τ2, the declaration [α,x]=unpack v, and partial type application v[τ] as a value. The function rule requires the body to be closed:

αCτiα;x:[α](τ1,,τn)void,x1:τ1,,xn:τnCe
Δ;ΓCfix x[α](x1:τ1,,xn:τn).e:[α](τ1,,τn)void
C-Fix
ΔCτ1Δ;ΓCv:τ2[τ1/α]
Δ;ΓCpack[τ1,v] as α.τ2:α.τ2
C-Pack
ΔCσΔ;ΓCv:[α,β](τ)void
Δ;ΓCv[σ]:[β](τ[σ/α])void
C-TApp

Note the context in the premise of C-Fix: it is α, not Δ, and it lists exactly the code block’s own type parameters.

Construction 133.6 — Polymorphic closure conversion

The obstruction is the free type variables of a function. Storing them in the environment forces the environment type to mention them, and the existential that hides the environment then has to quantify over a type that occurs in its own witness. The move that removes the obstruction is to read polymorphism by type erasure: a partial type application v[σ] is a value, because at run time it is the same word as v. A function’s free type variables can then be substituted directly into its code block, and only its free term variables need an environment. The type translation is [[[α](τ1,,τn)void]]C=β.[α](β,[[τ1]]C,,[[τn]]C)void, β, a pair of a code pointer and its environment, with the environment type β hidden. A call unpacks the pair, projects the two components and applies the code to the environment and the original arguments.

Lemma 133.7 — Closure conversion type correctness

If ;Ke then ;C[[e]]C,prog.

Proof of Lemma 133.7 — Closure conversion type correctness

Proof. Induct on the λK derivation, with the invariant that a λK value of type τ becomes a λC value of type [[τ]]C and a well-formed term becomes a well-formed term.

The one case that is not a rewriting is fix. Let the free term variables of the λK function be y1:σ1,,ym:σm and its free type variables be γ. Build the environment value venv=y1,,ym of type σenv=[[σ1]]C,,[[σm]]C, and the code block vcode=fix x[γ,α](z:σenv,x1:[[τ1]]C,).let y1,,ym=z in [[e]]C, whose body has no free term variable other than those bound by the block, so C-Fix applies with the context γ,α. The closure is pack[σenv,vcode[γ],venv] as [[τ]]C. C-TApp types vcode[γ] — this is where partial type application being a value does the work — and C-Pack then hides σenv. At a call site, C-Unpack and two projections recover the code at type [α](β,)void and the environment at type β, for the freshly bound β; the call rule then applies.

The remaining cases replace each subterm by its translation and reapply the same rule; no type changes shape except through construction 133.6, and that shape is exactly what the call case consumes. ◻

Lemma 133.8 — Hoisting

Let λH be λC with fix removed from the value forms and code blocks collected in a top-level heap letrec. Replacing every fix by a fresh label bound in the heap gives [[]]H, and if ;Ce then H[[e]]H,prog.

Proof of Lemma 133.8 — Hoisting

Proof. After lemma 133.7 every code block is closed, so moving it to the top level changes no free variable. Formally, induct on e, carrying a heap typing Ψ that assigns to each fresh label the type its block had as a value; each C-Fix node contributes one binding, and the label is typed by the heap rule at exactly that type. ◻

Allocation and initialization

A tuple in λH is built in one step. A machine allocates a block and then writes its fields. Between those two events the block exists and its fields do not yet hold values, so the type system must forbid reading them.

Definition 133.9 — λ ^A

Tuple types carry an initialization flag per field, τ1φ1,,τnφn with φ{0,1}. Projection requires φi=1; allocation x=malloc[τ] produces all flags 0; and the update x=v1[i]v2 sets the ith flag to 1:

Δ;ΓAv:τ1φ1,,τnφnΔ;Γ,x:τiAeφi=1
Δ;ΓAlet x=πi(v) in e
A-Proj
ΔAτiΔ;Γ,x:τ10,,τn0Ae
Δ;ΓAlet x=malloc[τ1,,τn] in e
A-Malloc
Δ;ΓAv1:τ1φ1,,τnφnΔ;ΓAv2:τiΔ;Γ,x:τ1φ1,,τi1,,τnφnAe
Δ;ΓAlet x=v1[i]v2 in e
A-Init

The translation adds the flag 1 to every field of every λH tuple type and expands each tuple construction into a malloc followed by n updates.

Example 133.10 — Reading the flags

Building 3,4 produces let x0=malloc[int,int], x1=x0[1]3, x=x1[2]4 in  with x0:int0,int0, x1:int1,int0 and x:int1,int1. The three variables name the same machine address at three types. A projection π2(x1) has no derivation, because A-Proj requires φ2=1. The flags do not forbid writing a field twice: A-Init is derivable when φi is already 1, and the resulting type is unchanged.

Lemma 133.11 — Allocation type correctness

If HP then A[[P]]A.

Proof of Lemma 133.11 — Allocation type correctness

Proof. Induct on the λH derivation. The tuple case is the only one whose translation is not a renaming: it emits the sequence of example 133.10, and after the nth update the bound variable has the type [[τ1]]A1,,[[τn]]A1, which is [[τ1,,τn]]A. Every subsequent projection therefore satisfies the side condition of A-Proj. Because a λH value may itself contain a tuple, the value translation returns a sequence of declarations together with a value; the induction hypothesis is stated for that pair, and the declaration sequences are concatenated in the order the subterms are traversed. ◻

Typed assembly language

Definition 133.12 — TAL

ι::=add rd,rs,vbnz r,vld rd,rs[i]malloc rd[τ]mov rd,vmul rd,rs,vst rd[i],rssub rd,rs,vunpack[α,rd],v,I::=ι;Ijmp vhalt[τ],P::=(H,R,I), with H a heap mapping labels to heap values, R a register file, and code blocks code[α]Γ.I recording the register file type Γ they require. The machine relation PP is deterministic; for example jmp v with R^(v)=[τ] and H()=code[α]Γ.I steps to (H,R,I[τ/α]).

The type of a code block is [α].Γ: a jump is well typed exactly when the current register file satisfies the target’s assumptions. That is the invariant the opening asked for, and it is now part of the program text.

Theorem 133.13 — Subject reduction and progress

If TALP and PP then TALP. If TALP then either PP for some P, or P has the form (H,R{r1w},halt[τ]).

Proof of Theorem 133.13 — Subject reduction and progress

Proof. Both halves are structural inductions over the instruction at the head of I, and both rest on the same three auxiliary facts. Canonical word forms: if TALH:Ψ and Ψ;TALw:τ wval, then the shape of w is determined by the outermost constructor of τ — an integer for int, a label for a tuple or code type, a package for an existential. Heap extension and update: adding a label at a type not already in Ψ, or overwriting a field with a value of a subtype of its recorded type, preserves TALH:Ψ. Register file update: writing a well-typed word into rd preserves ΨTALR:Γ at the updated Γ.

For progress at jmp v: typing gives Ψ;Δ;ΓTALv:[].Γ and Γ<:Γ; canonical word forms make R^(v) a label instantiation [τ], the heap typing supplies a code block at , and the machine rule applies. For subject reduction at the same instruction, the substituted instruction sequence is well typed because code block typing is closed under type substitution and Γ<:Γ lets the register file be weakened to the block’s assumptions. At ld rd,rs[i], canonical forms give R(rs)= with H()=w0,,wn1; the initialization flag carried into TAL from λA is what guarantees 0i<n and that wi is a well-typed word rather than a junk value ?τ. At malloc rd[τ] the fresh label is added with all fields junk and all flags 0, which is exactly the case heap extension covers. The remaining instructions are register-to-register moves and arithmetic and are handled by register file update alone. ◻

Corollary 133.14 — Type safety

If TALP then no P with PP is stuck.

Lemma 133.15 — Code generation

If AP then TAL[[P]]T, where the type translation assigns registers to the value arguments of a function type: [[[α](τ1,,τn)void]]T=[α].{r1:[[τ1]]T,,rn:[[τn]]T}.

Corollary 133.16 — Compiler type correctness

Write C for the composite [[]]T[[]]A[[]]H[[]]C[[]]K. If Fe:τ then TALC(e).

Proof of Corollary 133.16 — Compiler type correctness

Proof. Compose lemma 133.4, lemma 133.7, lemma 133.8, lemma 133.11, lemma 133.15, each of whose conclusion is the hypothesis of the next. ◻

What the composite corollary does not say

Corollary 133.16 is a statement about typing. It is worth seeing precisely how far that is from a statement about behaviour.

Proposition 133.17 — Type preservation does not imply semantic preservation

There is a translation Z from λF to TAL satisfying “if Fe:int then TALZ(e)” for which some closed e evaluates to 6 while Z(e) halts with 0.

Proof of Proposition 133.17 — Type preservation does not imply semantic preservation

Proof. Let Z(e)=(,{r10},halt[int]) for every e. The heap typing is empty, the register file typing is {r1:int}, and halt[int] requires exactly that, so TALZ(e) for every e. Taking e=efact gives the stated behaviours. ◻

So a chapter that proves corollary 133.16 has proved that a consumer can check the compiled code for safety without trusting the compiler, and nothing more. The missing statement — that source and target agree on observable behaviour — needs a separate relation between the two languages and a separate theorem. Two further facts about that missing statement are worth recording, because both are easy to assume and both are false. A semantic-preservation theorem for whole programs is strictly stronger than corollary 133.16 and requires a relation between λF values and TAL word values, which no lemma above constructs. And a semantic-preservation theorem for whole programs does not by itself say anything about a compiled component linked with a target context that the source language could not have produced.

Definition 133.18 — The exported interfaces

Three objects constructed above are named here so that a later development needing one of them can cite this definition rather than rebuild the construction from the translations.

  1. The CPS observation relation: two λK programs are related when both reduce to halt[int]i with the same i, or both diverge.

  2. The component boundary: a component is a heap fragment with the heap type Ψ it exports and the heap type it imports. Linking is the disjoint union of two heaps whose exported and imported types agree.

  3. The existential-closure calling interface of construction 133.6: a closure is a package β.[α](β,τ)void,β, and a caller may only unpack, project and apply.

Lemma 133.19 — Linking preserves typing

If TALH1:Ψ1, TALH2:Ψ2, the domains of H1 and H2 are disjoint, and each heap’s imported labels are exported by the other at the same types, then TALH1H2:Ψ1Ψ2.

Proof of Lemma 133.19 — Linking preserves typing

Proof. The heap typing rule checks each heap value against Ψ. Every such check in H1 used only labels in Ψ1 or imported labels, and heap value typing is monotone in Ψ by the heap extension lemma of theorem 133.13; the imported labels are present in Ψ1Ψ2 at the assumed types. Disjointness of domains makes the union a function. ◻

Exercise 133.3

★★☆ Show that a caller holding a value of the closure type of construction 133.6 cannot form a term whose type mentions the environment type. (Two lines: appeal to the scope of the existential variable in C-Unpack.) Then exhibit a closure whose environment is a tuple and one whose environment is a machine word, and check that both have the same closure type.

Translation validation, and what a solver assumes

The pipeline above proves a property of the translation once and for all. An alternative is to check each run: compile, then verify that the particular output agrees with the particular input. The Alive2 tool does this for a production compiler by encoding a source function, a target function and a refinement claim as a satisfiability problem.

Remark 133.20 — System card: bounded translation validation

Alive2 checks refinement between two functions of one intermediate representation, under an encoding of that representation’s semantics. Three assumptions travel with every answer it gives. Undefined behaviour in the source is a licence for the target to do anything, so a report that one function refines another is relative to the encoding of undefined behaviour. Poison values propagate through the encoding, and a mismatch in their treatment changes the answer. The verdict is a solver’s, and holds relative to that solver and its timeout. Checking every function of every compilation is not a theorem about the compiler. It is evidence about the compilations that were run.

The contrast with corollary 133.16 is exact. The corollary quantifies over all source programs and gives typing; translation validation quantifies over the runs performed and gives refinement under an encoding. Neither implies the other.

Remark 133.21 — System card: a typed array intermediate representation

Run one nested map through a typed functional array compiler’s intermediate representation. The flattening pass rewrites nested parallel combinators into flat ones, and the compiler’s own type checker accepts the result: the pass preserves the intermediate representation’s typing invariant. That observation is of the same kind as lemma 133.11 and of no other kind. It is not a proof that the flattened program computes the same array, still less that the generated accelerator code does. A compiler’s own type checker accepting a pass’s output is evidence that the pass preserves that checker’s invariant on the inputs tried, and nothing more.

Limits and seminar

The frozen source is Morrisett, Walker, Crary and Glew (1999). Every language, translation and theorem statement above is theirs; lemma 133.4, lemma 133.7, lemma 133.8, lemma 133.11, lemma 133.15 and theorem 133.13 are their lemmas, proved here at their exact signatures. Proposition 133.17, Lemma 133.19 and definition 133.18 are local.

Four boundaries. First, and most importantly, proposition 133.17: none of the five lemmas relates the behaviour of a program to the behaviour of its translation, so the chapter proves no compiler-correctness theorem. Second, the pipeline performs no optimization; a realistic compiler integrates optimizations into these passes, and each such integration needs its own preservation argument. Third, TAL is a conventional RISC abstraction with an infinite heap and no memory deallocation; nothing here concerns garbage collection, calling conventions of a real machine, or code layout. Fourth, the certified-code architecture in which a consumer checks a producer’s code assumes that the consumer’s checker implements the rules of definition 133.12; that assumption is a trusted computing base and is not discharged here.

[4]

Suggested first pass.

None of these problems is a prerequisite for a later chapter. Begin with exercise 133.4, then complete exercise 133.7.

Exercise 133.4

★★☆ Let Expr have constructors Val n and Add xy, with eval(Val n)=n and eval(Add xy)=eval x+eval y. Posit a stack evaluator satisfying the specification evalS x s=eval x::s, and calculate its two defining equations from that specification by induction on x, annotating each step with the equation used. Then read a push instruction and an add instruction off the two calculated right-hand sides.

Exercise 133.5

★★★ Continue exercise 133.4. Index the source expressions and the stack by their types, so that Expr A and a stack shape Stack A make the specification typed. Recalculate the two equations, and say at which step the type index forces a choice that the untyped calculation left free. State the correctness theorem of the resulting compiler and check that it is a statement about behaviour, not about typing.

Exercise 133.6

★★★ Closure-convert the polymorphic function twice=Λα.λf:αα.λx:α.f(fx), including the two nested inner functions, and give the closure type of each. Then redo the conversion under the alternative design in which free type variables are stored in a type environment, and identify the point at which the environment’s type mentions a variable that the existential must bind.

Exercise 133.7 — Practical: type-preserving pipeline

★★★ Practical project.til-pipeline Complete project til-pipeline. Implement, for the fragment of λF containing integers, arithmetic, if0, tuples, projection, recursive functions and application, (i) a type checker and a call-by-value evaluator for λF, (ii) type checkers for λK, for λC — which is the λK checker together with the closedness condition of C-Fix — and for λA with its initialization flags, and (iii) the type-directed CPS translation of definition 133.3. The invariant the implementation must maintain is the one lemma 133.4 states: the translation must map an input accepted by the λF checker to an input accepted by the λK checker. The named cases print

fact6-source: 720
fact6-cps: accepted
fact6-closure: accepted
fact6-alloc: accepted
read-before-init: rejected
closure-escape: rejected

The checkers are independent evidence for the fragment; they do not prove corollary 133.16, do not implement hoisting, code generation or TAL, do not exercise polymorphic closure conversion — the fragment is monomorphic — and, by proposition 133.17, establish nothing about the behaviour of the translated programs beyond the one printed value.

Search the book

Type to search the local edition.