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 𝑒fact=(𝖿𝗂𝗑𝑓(𝑛:𝗂𝗇𝗍):𝗂𝗇𝗍.𝗂𝖿𝟢(𝑛,1,𝑛×𝑓(𝑛−1)))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 𝑒fact 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. 𝑒fact has type 𝗂𝗇𝗍, 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.
𝜏::=𝛼∣𝗂𝗇𝗍∣𝜏1→𝜏2∣∀𝛼.𝜏∣⟨𝜏1,…,𝜏𝑛⟩,𝑢::=𝑥∣𝑖∣𝖿𝗂𝗑𝑥(𝑥1:𝜏1):𝜏2.𝑒∣𝑒1𝑒2∣Λ𝛼.𝑒∣𝑒[𝜏]∣⟨𝑒1,…,𝑒𝑛⟩∣𝜋𝑖(𝑒)∣𝑒1𝑝𝑒2∣𝗂𝖿𝟢(𝑒1,𝑒2,𝑒3),𝑒::=𝑢𝜏, where 𝑝 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 Δ;Γ⊢𝐹𝑒:𝜏 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 𝖿𝗂𝗑 rather than 𝜆 matters exactly once: 𝜆𝐹 has general recursion, so no translation below may appeal to normalization, and every theorem is about typing rather than termination.
Types, values, declarations and terms are 𝜏::=𝛼∣𝗂𝗇𝗍∣∀[𝛼1,…,𝛼𝑚].(𝜏1,…,𝜏𝑛)→𝗏𝗈𝗂𝖽∣⟨𝜏1,…,𝜏𝑛⟩,𝑣::=𝑥∣𝑖∣𝖿𝗂𝗑𝑥[⃗𝛼](𝑥1:𝜏1,…,𝑥𝑛:𝜏𝑛).𝑒∣⟨𝑣1,…,𝑣𝑛⟩∣𝑣[𝜏],𝑒::=𝗅𝖾𝗍𝑑𝗂𝗇𝑒∣𝑣[⃗𝜎](𝑣1,…,𝑣𝑛)∣𝗂𝖿𝟢(𝑣,𝑒1,𝑒2)∣𝗁𝖺𝗅𝗍[𝜏]𝑣. A function does not return: its result type is 𝗏𝗈𝗂𝖽, and a call is a jump. Only values have types; the judgment for terms is Δ;Γ⊢𝐾𝑒, with no type on the right.
On types, [[𝛼]]𝐾=𝛼,[[𝗂𝗇𝗍]]𝐾=𝗂𝗇𝗍,[[𝜏1→𝜏2]]𝐾=([[𝜏1]]𝐾,𝖢𝗈𝗇𝗍[[𝜏2]]𝐾)→𝗏𝗈𝗂𝖽,[[∀𝛼.𝜏]]𝐾=∀[𝛼].(𝖢𝗈𝗇𝗍[[𝜏]]𝐾)→𝗏𝗈𝗂𝖽, with 𝖢𝗈𝗇𝗍𝜏=(𝜏)→𝗏𝗈𝗂𝖽 and tuples translated componentwise. On terms, [[𝑒]]𝐾𝑘 computes 𝑒 and hands the result to the continuation 𝑘; the two clauses that determine the shape of the rest are [[(𝑢𝜏11𝑢𝜏22)𝜏]]𝐾𝑘=[[𝑢𝜏11]]𝐾(𝜆𝑥1:[[𝜏1]]𝐾.[[𝑢𝜏22]]𝐾(𝜆𝑥2:[[𝜏2]]𝐾.𝑥1(𝑥2,𝑘))),[[𝑢𝜏]]𝐾,prog=[[𝑢𝜏]]𝐾(𝜆𝑥:[[𝜏]]𝐾.𝗁𝖺𝗅𝗍[[[𝜏]]𝐾]𝑥). The variables 𝑥1, 𝑥2, 𝑐 and 𝑥 are chosen outside the free variables of the terms and continuations already fixed.
Proof. Strengthen the statement to the form the induction actually needs: if Δ;Γ⊢𝐹𝑒:𝜏 and Δ;[[Γ]]𝐾⊢𝐾𝑘:𝖢𝗈𝗇𝗍[[𝜏]]𝐾, then Δ;[[Γ]]𝐾⊢𝐾[[𝑒]]𝐾𝑘. 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 𝜆𝐹 typing derivation. Variable and integer.[[𝑥𝜏]]𝐾𝑘=𝑘(𝑥), and the call rule of 𝜆𝐾 applies because 𝑘 has type 𝖢𝗈𝗇𝗍[[𝜏]]𝐾 and 𝑥 has type [[𝜏]]𝐾 in [[Γ]]𝐾.
Application. Let Δ;Γ⊢𝐹𝑢1:𝜏1→𝜏2 and Δ;Γ⊢𝐹𝑢2:𝜏1. The inner continuation 𝜆𝑥2:[[𝜏1]]𝐾.𝑥1(𝑥2,𝑘) is well typed in [[Γ]]𝐾,𝑥1:[[𝜏1→𝜏2]]𝐾, because [[𝜏1→𝜏2]]𝐾=([[𝜏1]]𝐾,𝖢𝗈𝗇𝗍[[𝜏2]]𝐾)→𝗏𝗈𝗂𝖽 is exactly the type of a function taking the argument and the continuation. It therefore has type 𝖢𝗈𝗇𝗍[[𝜏1]]𝐾, which is what the induction hypothesis for 𝑢2 requires. The outer continuation is then well typed at 𝖢𝗈𝗇𝗍[[𝜏1→𝜏2]]𝐾, which is what the induction hypothesis for 𝑢1 requires.
Recursive function. Its translation is the call 𝑘(𝖿𝗂𝗑𝑥(𝑥1:[[𝜏1]]𝐾,𝑐:𝖢𝗈𝗇𝗍[[𝜏2]]𝐾).[[𝑒]]𝐾𝑐). Applying the induction hypothesis to the body with the continuation variable 𝑐 gives the body’s well-formedness; the 𝜆𝐾 function rule then types the value at [[𝜏1→𝜏2]]𝐾, and the call rule applies 𝑘 to it.
Type abstraction and application, tuples, projection, arithmetic and 𝗂𝖿𝟢 follow the same two moves: build the continuation demanded by the induction hypothesis of each subterm, and apply the 𝜆𝐾 rule whose premises the hypotheses supply. The 𝗂𝖿𝟢 case is the only one that uses 𝑘 twice; both uses are at the same type, so no duplication of typing obligations arises. Finally, 𝜆𝑥:[[𝜏]]𝐾.𝗁𝖺𝗅𝗍[[[𝜏]]𝐾]𝑥 has type 𝖢𝗈𝗇𝗍[[𝜏]]𝐾, which discharges the strengthened statement at the top level. ◻
★☆☆ Compute [[(1+2)𝗂𝗇𝗍]]𝐾,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.
A 𝜆𝐾 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.
Extend 𝜆𝐾 with existential types ∃𝛼.𝜏, the value form 𝗉𝖺𝖼𝗄[𝜏1,𝑣]𝖺𝗌𝜏2, the declaration [𝛼,𝑥]=𝗎𝗇𝗉𝖺𝖼𝗄𝑣, and partial type application 𝑣[𝜏] as a value. The function rule requires the body to be closed:
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 𝑣[𝜎] is a value, because at run time it is the same word as 𝑣. 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,…,𝜏𝑛)→𝗏𝗈𝗂𝖽]]𝐶=∃𝛽.⟨∀[⃗𝛼](𝛽,[[𝜏1]]𝐶,…,[[𝜏𝑛]]𝐶)→𝗏𝗈𝗂𝖽,𝛽⟩, 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.
Proof of Lemma 133.7 — Closure conversion type correctness
Proof. Induct on the 𝜆𝐾 derivation, with the invariant that a 𝜆𝐾 value of type 𝜏 becomes a 𝜆𝐶 value of type [[𝜏]]𝐶 and a well-formed term becomes a well-formed term.
The one case that is not a rewriting is 𝖿𝗂𝗑. Let the free term variables of the 𝜆𝐾 function be 𝑦1:𝜎1,…,𝑦𝑚:𝜎𝑚 and its free type variables be ⃗𝛾. Build the environment value 𝑣env=⟨𝑦1,…,𝑦𝑚⟩ of type 𝜎env=⟨[[𝜎1]]𝐶,…,[[𝜎𝑚]]𝐶⟩, and the code block 𝑣code=𝖿𝗂𝗑𝑥[⃗𝛾,⃗𝛼](𝑧:𝜎env,𝑥1:[[𝜏1]]𝐶,…).𝗅𝖾𝗍⟨𝑦1,…,𝑦𝑚⟩=𝑧𝗂𝗇[[𝑒]]𝐶, whose body has no free term variable other than those bound by the block, so C-Fix applies with the context ⃗𝛾,⃗𝛼. The closure is 𝗉𝖺𝖼𝗄[𝜎env,⟨𝑣code[⃗𝛾],𝑣env⟩]𝖺𝗌[[𝜏]]𝐶.C-TApp types 𝑣code[⃗𝛾] — 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 ∀[⃗𝛼](𝛽,…)→𝗏𝗈𝗂𝖽 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. ◻
Let 𝜆𝐻 be 𝜆𝐶 with 𝖿𝗂𝗑 removed from the value forms and code blocks collected in a top-level heap 𝗅𝖾𝗍𝗋𝖾𝖼. Replacing every 𝖿𝗂𝗑 by a fresh label bound in the heap gives [[⋅]]𝐻, and if ∅;∅⊢𝐶𝑒 then ⊢𝐻[[𝑒]]𝐻,prog.
Proof. After lemma 133.7 every code block is closed, so moving it to the top level changes no free variable. Formally, induct on 𝑒, 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 𝜆𝐻 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.
Tuple types carry an initialization flag per field, ⟨𝜏𝜑11,…,𝜏𝜑𝑛𝑛⟩ with 𝜑∈{0,1}. Projection requires 𝜑𝑖=1; allocation 𝑥=𝗆𝖺𝗅𝗅𝗈𝖼[⃗𝜏] produces all flags 0; and the update 𝑥=𝑣1[𝑖]←𝑣2 sets the 𝑖th flag to 1:
Building ⟨3,4⟩ produces 𝗅𝖾𝗍𝑥0=𝗆𝖺𝗅𝗅𝗈𝖼[𝗂𝗇𝗍,𝗂𝗇𝗍],𝑥1=𝑥0[1]←3,𝑥=𝑥1[2]←4𝗂𝗇… with 𝑥0:⟨𝗂𝗇𝗍0,𝗂𝗇𝗍0⟩, 𝑥1:⟨𝗂𝗇𝗍1,𝗂𝗇𝗍0⟩ and 𝑥:⟨𝗂𝗇𝗍1,𝗂𝗇𝗍1⟩. The three variables name the same machine address at three types. A projection 𝜋2(𝑥1) has no derivation, because A-Proj requires 𝜑2=1. The flags do not forbid writing a field twice: A-Init is derivable when 𝜑𝑖 is already 1, and the resulting type is unchanged.
Proof of Lemma 133.11 — Allocation type correctness
Proof. Induct on the 𝜆𝐻 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 𝑛th update the bound variable has the type ⟨[[𝜏1]]1𝐴,…,[[𝜏𝑛]]1𝐴⟩, which is [[⟨𝜏1,…,𝜏𝑛⟩]]𝐴. Every subsequent projection therefore satisfies the side condition of A-Proj. Because a 𝜆𝐻 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. ◻
𝜄::=𝖺𝖽𝖽𝑟𝑑,𝑟𝑠,𝑣∣𝖻𝗇𝗓𝑟,𝑣∣𝗅𝖽𝑟𝑑,𝑟𝑠[𝑖]∣𝗆𝖺𝗅𝗅𝗈𝖼𝑟𝑑[⃗𝜏]∣𝗆𝗈𝗏𝑟𝑑,𝑣∣𝗆𝗎𝗅𝑟𝑑,𝑟𝑠,𝑣∣𝗌𝗍𝑟𝑑[𝑖],𝑟𝑠∣𝗌𝗎𝖻𝑟𝑑,𝑟𝑠,𝑣∣𝗎𝗇𝗉𝖺𝖼𝗄[𝛼,𝑟𝑑],𝑣,𝐼::=𝜄;𝐼∣𝗃𝗆𝗉𝑣∣𝗁𝖺𝗅𝗍[𝜏],𝑃::=(𝐻,𝑅,𝐼), with 𝐻 a heap mapping labels to heap values, 𝑅 a register file, and code blocks 𝖼𝗈𝖽𝖾[⃗𝛼]Γ.𝐼 recording the register file type Γ they require. The machine relation 𝑃⟶𝑃′ is deterministic; for example 𝗃𝗆𝗉𝑣 with ̂𝑅(𝑣)=ℓ[⃗𝜏] and 𝐻(ℓ)=𝖼𝗈𝖽𝖾[⃗𝛼]Γ.𝐼′ steps to (𝐻,𝑅,𝐼′[⃗𝜏/⃗𝛼]).
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.
Proof of Theorem 133.13 — Subject reduction and progress
Proof. Both halves are structural inductions over the instruction at the head of 𝐼, and both rest on the same three auxiliary facts. Canonical word forms: if ⊢TAL𝐻:Ψ and Ψ;∅⊢TAL𝑤:𝜏𝗐𝗏𝖺𝗅, then the shape of 𝑤 is determined by the outermost constructor of 𝜏 — an integer for 𝗂𝗇𝗍, 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 ⊢TAL𝐻:Ψ. Register file update: writing a well-typed word into 𝑟𝑑 preserves Ψ⊢TAL𝑅:Γ at the updated Γ.
For progress at 𝗃𝗆𝗉𝑣: typing gives Ψ;Δ;Γ⊢TAL𝑣:∀[].Γ′ and Γ<:Γ′; canonical word forms make ̂𝑅(𝑣) 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 𝗅𝖽𝑟𝑑,𝑟𝑠[𝑖], canonical forms give 𝑅(𝑟𝑠)=ℓ with 𝐻(ℓ)=⟨𝑤0,…,𝑤𝑛−1⟩; the initialization flag carried into TAL from 𝜆𝐴 is what guarantees 0≤𝑖<𝑛 and that 𝑤𝑖 is a well-typed word rather than a junk value ?𝜏. At 𝗆𝖺𝗅𝗅𝗈𝖼𝑟𝑑[⃗𝜏] 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. ◻
If ⊢𝐴𝑃 then ⊢TAL[[𝑃]]𝑇, where the type translation assigns registers to the value arguments of a function type: [[∀[⃗𝛼](𝜏1,…,𝜏𝑛)→𝗏𝗈𝗂𝖽]]𝑇=∀[⃗𝛼].{𝑟1:[[𝜏1]]𝑇,…,𝑟𝑛:[[𝜏𝑛]]𝑇}.
Proof of Proposition 133.17 — Type preservation does not imply semantic preservation
Proof. Let Z(𝑒)=(∅,{𝑟1↦0},𝗁𝖺𝗅𝗍[𝗂𝗇𝗍]) for every 𝑒. The heap typing is empty, the register file typing is {𝑟1:𝗂𝗇𝗍}, and 𝗁𝖺𝗅𝗍[𝗂𝗇𝗍] requires exactly that, so ⊢TALZ(𝑒) for every 𝑒. Taking 𝑒=𝑒fact 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 𝜆𝐹 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.
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.
The CPS observation relation: two 𝜆𝐾 programs are related when both reduce to 𝗁𝖺𝗅𝗍[𝗂𝗇𝗍]𝑖 with the same 𝑖, or both diverge.
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.
The existential-closure calling interface of construction 133.6: a closure is a package ∃𝛽.⟨∀[⃗𝛼](𝛽,⃗𝜏)→𝗏𝗈𝗂𝖽,𝛽⟩, and a caller may only unpack, project and apply.
If ⊢TAL𝐻1:Ψ1, ⊢TAL𝐻2:Ψ2, the domains of 𝐻1 and 𝐻2 are disjoint, and each heap’s imported labels are exported by the other at the same types, then ⊢TAL𝐻1⊎𝐻2:Ψ1⊎Ψ2.
Proof. The heap typing rule checks each heap value against Ψ. Every such check in 𝐻1 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. ◻
★★☆ Give two well-typed TAL heaps whose union is not well typed because one imports a label at a register file type that the other exports at a different one. Then state the weakest condition on the two types under which lemma 133.19 still holds, using TAL’s subtyping on register file types.
★★☆ 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.
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.
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.
Run one nested 𝗆𝖺𝗉 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.
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.
★★☆ Let 𝖤𝗑𝗉𝗋 have constructors 𝖵𝖺𝗅𝑛 and 𝖠𝖽𝖽𝑥𝑦, with 𝖾𝗏𝖺𝗅(𝖵𝖺𝗅𝑛)=𝑛 and 𝖾𝗏𝖺𝗅(𝖠𝖽𝖽𝑥𝑦)=𝖾𝗏𝖺𝗅𝑥+𝖾𝗏𝖺𝗅𝑦. Posit a stack evaluator satisfying the specification 𝖾𝗏𝖺𝗅𝖲𝑥𝑠=𝖾𝗏𝖺𝗅𝑥::𝑠, and calculate its two defining equations from that specification by induction on 𝑥, annotating each step with the equation used. Then read a push instruction and an add instruction off the two calculated right-hand sides.
★★★ Continue exercise 133.4. Index the source expressions and the stack by their types, so that 𝖤𝗑𝗉𝗋𝐴 and a stack shape 𝖲𝗍𝖺𝖼𝗄⃗𝐴 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.
★★★ Closure-convert the polymorphic function 𝗍𝗐𝗂𝖼𝖾=Λ𝛼.𝜆𝑓:𝛼→𝛼.𝜆𝑥:𝛼.𝑓(𝑓𝑥), 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.
★★★Practical project.til-pipeline Complete project til-pipeline. Implement, for the fragment of 𝜆𝐹 containing integers, arithmetic, 𝗂𝖿𝟢, tuples, projection, recursive functions and application, (i) a type checker and a call-by-value evaluator for 𝜆𝐹, (ii) type checkers for 𝜆𝐾, for 𝜆𝐶 — which is the 𝜆𝐾 checker together with the closedness condition of C-Fix — and for 𝜆𝐴 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 𝜆𝐹 checker to an input accepted by the 𝜆𝐾 checker. The named cases print
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.