Abstract Principled Compilation and Scavenging

Stefan Monnier

With the growth of the Internet and of the use of computers in general, the safety and reliability of computer systems is much more important. As the most popular methods to address these needs, such as discipline, authentication, or monitoring, are proving insufficient, there grows a need for formal methods that enable automatic verification of the safety of computer programs.

While approaches such as model checking or Hoare logic hold the promise of proving not just safety but even correctness, their applicability is too restricted for general purpose applications. At the other end of the scale, type systems are very popular but only check a very limited set of properties and impose sometimes severe restrictions which can prevent some forms of optimizations and even whole categories of code.

In order to use type systems to guarantee the security of computer systems, applications need to ship with type annotations, which requires type-preserving compilers, i.e. compilers that preserve the code’s type information through every stage of the compilation. As the program gets turned into lower level code, the restrictions imposed by the type system become more crippling, or alternatively, the type system needs to become more sophisticated.

My thesis defines and uses, within the context of type-preserving compilation, novel type systems for the purpose of proving more sophisticated properties of programs, thus trying to reduce the gap between program verification techniques and type systems. More specifically, I first present my experience porting the optimizer of the SML/NJ compiler to a type-preserving intermediate language. Then I extend this with a cross-module inliner and show a typed two-level language that allows me to formalize the inlining algorithm as a form of partial evaluation. In the second part, I develop techniques to type check a garbage collector and its interaction with the mutator. This leads me to develop a novel type system that allows me to reason about sophisticated invariants, much like Hoare logic would but without losing the modularity so crucial to the success of type systems.

Principled Compilation and Scavenging

A Dissertation

Presented to the Faculty of the Graduate School

of Yale University

in Candidacy for the Degree of Doctor of Philosophy

By

Stefan Monnier

Dissertation Director: Zhong Shao

December 2003 \begin{matrix}c \\ ◯ \end{matrix}2004 Stefan Monnier. All Rights Reserved.

Contents

Contents i

List of Figures vi

Acknowledgement viii

1 Introduction 1 1.1 The need for stronger type systems . . . . . . . . . . . . . . . . . . . . . . . 2 1.2 Reasoning about the state . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 1.3 Optimizing a typed intermediate language . . . . . . . . . . . . . . . . . . . 3 1.4 Formalizing a cross-module inlining algorithm . . . . . . . . . . . . . . . . . 4 1.5 Type-safe garbage collection . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 1.6 Typed regions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5

2 The FLINT optimizer 6 2.1 The old middle end . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 2.2 The FLINT intermediate language . . . . . . . . . . . . . . . . . . . . . . . 10 2.3 The FLINT optimizer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12

2.3.1 Known functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 2.3.2 General organization . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 2.3.3 Flattening . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 2.3.4 Contracting β-redexes . . . . . . . . . . . . . . . . . . . . . . . . . . 18

2.4 A-normal vs CPS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 2.4.1 The difference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 2.4.2 Advantages of using CPS . . . . . . . . . . . . . . . . . . . . . . . . 21 2.4.3 Advantages of A-normal . . . . . . . . . . . . . . . . . . . . . . . . . 23

2.5 Impact of types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 2.6 Related work . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 2.7 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27

3 Inlining as staged computation 29 3.1 The big picture . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 3.2 The source calculus SRC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 3.3 The target calculus TLC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 3.4 Translation from SRC to TLC . . . . . . . . . . . . . . . . . . . . . . . . . . 41

3.4.1 A quick overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 3.4.2 Staging the core . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43 3.4.3 Splitting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 3.4.4 The main algorithm . . . . . . . . . . . . . . . . . . . . . . . . . . . 52

3.5 Handling the top level . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 3.6 Extensions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59

3.6.1 Side effects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 3.6.2 Recursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 3.6.3 Optimizations as staged computation . . . . . . . . . . . . . . . . . 62 3.6.4 Staging refinement . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 3.6.5 Link-time optimizations . . . . . . . . . . . . . . . . . . . . . . . . . 63 3.6.6 Implementation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63

3.7 Related work . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64 3.8 Conclusions and future work . . . . . . . . . . . . . . . . . . . . . . . . . . 67

4 Principled scavenging 68 4.1 Motivation and approach . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 4.1.1 The problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 4.1.2 My solution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72

4.2 Source language λCLOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 4.3 Target language λGC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76

4.3.1 Regions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 4.3.2 Functions and code . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 4.3.3 Intensional type analysis . . . . . . . . . . . . . . . . . . . . . . . . . 78

4.4 Translating λCLOS to λGC . . . . . . . . . . . . . . . . . . . . . . . . . . 78

4.4.1 Closed CPS garbage collector . . . . . . . . . . . . . . . . . . . . . . 82 4.5 A closer look at λGC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82

4.5.1 Closure conversion and copy. . . . . . . . . . . . . . . . . . . . . . . 84 4.5.2 Functions and code . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 4.5.3 The type calculus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 4.5.4 The term calculus . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86 4.5.5 Formal properties of the language . . . . . . . . . . . . . . . . . . . 87

4.6 Forwarding pointers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91

4.6.1 How to widen safely . . . . . . . . . . . . . . . . . . . . . . . . . . . 94 4.7 Generational collection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97 4.8 Related work . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100 4.9 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102

5 Typed regions 103 5.1 Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 5.2 Background . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106

5.2.1 Cyclic structures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106 5.2.2 Regions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 5.2.3 Alias types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110 5.2.4 Calculus of inductive constructions . . . . . . . . . . . . . . . . . . . 112

5.3 Typed regions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 5.3.1 The core language . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114 5.3.2 Semantics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116 5.3.3 Properties of the core language . . . . . . . . . . . . . . . . . . . . . 120

5.4 Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121 5.4.1 Simple regions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121 5.4.2 Stacks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123 5.4.3 Adoption and focus . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124

5.5 The full language . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126 5.5.1 Semantics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128 5.5.2 Properties of the full language . . . . . . . . . . . . . . . . . . . . . 132

5.6 Generational collection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132 5.6.1 Simple type-preserving GC . . . . . . . . . . . . . . . . . . . . . . . 132 5.6.2 Generational GC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133 5.6.3 Solution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136

5.7 Related work . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138 5.8 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140

6 Future directions 141 6.1 Implementation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141 6.2 Advanced uses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142 6.3 Surface syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142

Bibliography 143

A Soundness proofs 150

A.1 Soundness of principled scavenging . . . . . . . . . . . . . . . . . . . . . . . 150 A.1.1 Soundness of λ_{GC} . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150 A.1.2 Soundness of λ_{GCforw} . . . . . . . . . . . . . . . . . . . . . . . . . . . 165 A.1.3 Soundness of λ_{GCgen} . . . . . . . . . . . . . . . . . . . . . . . . . . . 173

A.2 Soundness of typed regions . . . . . . . . . . . . . . . . . . . . . . . . . . . 184 A.2.1 Soundness of the core language . . . . . . . . . . . . . . . . . . . . . 184 A.2.2 Soundness of the full language . . . . . . . . . . . . . . . . . . . . . 194

List of Figures

2.1 The FLINT datatype . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 2.2 Flattening polymorphic code. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17

3.1 Structure of the compiler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 3.2 Syntax for the source calculus SRC . . . . . . . . . . . . . . . . . . . . . . . . . 34 3.3 Static semantics for SRC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 3.4 Syntax for the target calculus TLC . . . . . . . . . . . . . . . . . . . . . . . . . 37 3.5 Static semantics for TLC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 3.6 Primitive static reduction for TLC . . . . . . . . . . . . . . . . . . . . . . . . . 40 3.7 Single-step partial evaluation for TLC . . . . . . . . . . . . . . . . . . . . . . . 40 3.8 Primitive reduction relation for TLC . . . . . . . . . . . . . . . . . . . . . . . . 40 3.9 Single-step call-by-value standard evaluation for TLC . . . . . . . . . . . . . . 40 3.10 Core type translations | · | and coercions ↓ and ↑. . . . . . . . . . . . . . . . . . 45 3.11 Core code translation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 3.12 The λ-split algorithm. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 3.13 Type and environment translation. . . . . . . . . . . . . . . . . . . . . . . . . . 52 3.14 Binding-time coercions. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 3.15 Module code translation. id_{t}^{b} is a shorthand for λ^{b}x:t.x. . . . . . . . . . . . . . . . 54 3.16 New rules using existential types. . . . . . . . . . . . . . . . . . . . . . . . . . . 60

4.1 Stop-and-Copy from region F to region T. . . . . . . . . . . . . . . . . . . . . . 71 4.2 Syntax of λ_{GC} . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 4.3 Translation of λ_{CLOS} terms. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 4.4 The garbage collector proper. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 4.5 The basic GC code after CPS and closure conversion. . . . . . . . . . . . . . . 83 4.6 Operational semantics of λ_{GC}. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 4.7 Static semantics of λ_{GC}, part 1. . . . . . . . . . . . . . . . . . . . . . . . . . . . 88 4.8 Static semantics of λ_{GC}, part 2. . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 4.9 Environment formation rules. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 4.10 Static semantics for λ_{GCforw}. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 4.11 GC with forwarding pointers. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95 4.12 Static semantics of λ_{GCgen}. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98 4.13 Generational GC. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100

5.1 Syntax of a region-based language. . . . . . . . . . . . . . . . . . . . . . . . . . 108 5.2 Syntax of an alias-types language. . . . . . . . . . . . . . . . . . . . . . . . . . 110 5.3 Syntax of the core language. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114 5.4 Operational semantics of the core language. . . . . . . . . . . . . . . . . . . . . 116 5.5 Environment formation rules. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117 5.6 Static semantics of the core language. . . . . . . . . . . . . . . . . . . . . . . . 119 5.7 Syntax of the full language . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126 5.8 Operational semantics of the full language. . . . . . . . . . . . . . . . . . . . . 127 5.9 Static semantics of the full language: auxiliary judgments. . . . . . . . . . . . . 129 5.10 Static semantics of the full language: well-formed terms. . . . . . . . . . . . . . 130 5.11 Generational GC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137

Acknowledgement

My work was funded in part by Yale University fellowship, NSF grants CCR-9633390, CCR-9901011, CCR-0081590, and CCR-0208618, and a DARPA OASIS grant F30602-99-1-0519.

I owe a lot of thanks to too many people to list them all here.

Let me specifically mention my officemate Chris League who somehow managed to survive my constant rambling about Emacs; Bratin Saha who is really responsible for the bulk of the soundness proofs for Chap. 4; Valery Trifonov for his endless supply of explanations about any kind of formal system; and of course my advisor Shao Zhong for his enthusiasm, his unexplained trust in my abilities, and his constant support.

Of course, this work would never have seen the light of day without all the feedback and comments I got on all my submitted papers.

Thanks also to all my friends, among whom Dominik stands out as the most closely related to this work. Finally, I would like to thank Fernanda for not beating me over the head, even in the last few days before my defense, and without whom this effort seemed to lack meaning.

Chapter 1

Introduction

The goal of my thesis is to show that type systems can be used to guarantee more sophisticated properties than basic type safety. More specifically, I present in the first part a typed language in which I can prove some behavioral properties of a cross-module inliner and in the second part, I describe how to use type systems to show that a generational copying garbage collector (GC) is correct in the sense that it will only deallocate memory that the mutator will not access in the future.

Type systems are often viewed as ways to do sanity checking of source code, as a guide to help design and structure code, or as instructions to determine the implementation strategy used by the compiler. Type annotations can also be considered as specifications which are automatically checked by the compiler.

The limitations imposed by a type system manifest themselves either in that the type system rejects code that should be considered valid (i.e. it is too restrictive), or in that it accepts code which should be considered incorrect (i.e. it is too permissive). Very often those two situations, although apparently diametrically opposed, are just the two sides of the same coin: by being too permissive in one case, the system cannot verify the invariant necessary to accept the other.

Traditional type systems such as used in SML or Java impose some non-trivial restrictions, but are very popular nonetheless because a large class of programs does not suffer terribly from them whereas they provide a welcome safety cushion in return for a little bit of extra annotation. Yet there is a growing need for more flexible type systems that can capture more sophisticated invariants and thus impose fewer restrictions.

1.1 The need for stronger type systems

This need is coming mostly from growing security problems with the use of the Internet, but also from the fact that the field of computer systems is slowly maturing to the point where people consider bugs not just as something you have to learn to live with but as a problem that needs to be addressed.

With regards to the more pressing security problem, a popular solution is indeed to rely on type systems to ensure the basic safety of the system. The best known examples of this are Java and .NET. But in order to be able to type-check code before running it, you need the program to come in a form that lends itself to type-checking. Both of those systems solve this by keeping the “compiled” code at a fairly high level, leaving the rest of the work to a just-in-time compiler. The reason for it is that when trying to type-check lower-level code, the invariants necessary to show its safety become more complex. Or rather, as details of the implementation are made explicit in the lower-level code, details of the proof of safety need to be made explicit in the types.

The field of certifying compilation aims at solving those difficulties up to the point where compilers can generate an actual proof of safety along with the fully optimized raw binary code [Nec97, MWCG98, App01, HST^{+}02]. The proof is typically encoded in some Hoare logic, so that it is fully generic and independent from any type system the compiler might have chosen to use.

1.2 Reasoning about the state

One of the more challenging difficulties for type systems is to express properties of the state. Traditional type systems only reason about properties of values. The problem with reasoning about the state is that when a memory cell is modified, it can influence invariants at any number of places, due to aliasing: there might be any number of pointers to this memory cell. Solutions to this problem traditionally rely on keeping track of all the aliases, which is not possible in the general case.

One of the contributions of this thesis is to show another way to tackle this problem which relies on preserving invariants that do not depend on aliasing. I claim that this is needed for certifying compilers because the source language in general does not impose any constraint on the aliasing patterns, so the compiler cannot use any of the approaches that relies on static aliasing information if it needs to reason about global state.

The rest of this chapter gives an overview of the thesis.

1.3 Optimizing a typed intermediate language

The FLINT project [Sha97b] uses the SML/NJ compiler [AM91] as a testbed for its work on certifying compilation. For many years now the front-end generates strongly typed intermediate code, but the type information was mostly thrown out just before going through the main code optimizer.

Obviously, if we want to preserve types throughout the compilation, we need to preserve them through optimization, since a non-optimizing compiler would be even academically of dubious interest. So I ported all those optimizations from the untyped intermediate language (called CPS) to the typed language (called FLINT). These optimizations are fairly simple. They include inlining, η-contraction, uncurrying, argument flattening, constant propagation, and constant folding, and cross-module inlining.

Chapter 2 reports on the problems we have encountered and how we solved them. CPS and FLINT also differ in a few more points than types, so I also report on my experience with those aspects of the respective languages.

1.4 Formalizing a cross-module inlining algorithm

The cross-module inliner I implemented is based on the work of Matthias Blume [BA97] but with the difference that his work was done in an untyped setting and with a few subtle differences in the behavior.

While arguing about what the proper behavior of the inliner should be and how to guarantee it, we found a need for a formalism in which we could express the algorithm such that we can try to reason about some of its properties.

Chapter 3 develops such a formalism in the form of a language in which the type of each expression is annotated with the phase (either compile-time or run-time) in which it is evaluated. I then present how to translate a simple SML-like source language into this intermediate language. Finally I formalize the cross-module inlining algorithm and show some of its properties.

1.5 Type-safe garbage collection

The second part of my thesis, starting with Chap. 4 presents techniques to write a garbage collector in a type-safe language.

Certifying compilers typically take as input programs written in a language using a traditional type system, which relies on automatic memory reclamation. So the generated code needs to come with enough annotations to show that the memory management is done correctly. The first step in this direction was made by Daniel Wang who shows in [WA01] how to implement a very simple copying garbage collector (GC) on top of a type-safe language with region annotations.

In this chapter I extend this work to address some of its limitations. First, in order to keep the type system simple, his work requires whole program analysis, forces the garbage collector to be tailored to the application, and requires every type used by the application to come with its own set of garbage collection routines. I address these issues using a more sophisticated type system that allows intensional type analysis.

In this chapter I also show two extensions to the basic GC: first, using a very simple extension of the type system, I show how to encode in the types the age-barrier invariant of a simplistic generational garbage collector. Second, I develop a first, very primitive and ad-hoc, attempt at dealing with issues of global state, which I use to implement forwarding pointers.

1.6 Typed regions

The above work is based on well understood type systems, with minor extensions. But all the GCs one can implement in such systems suffer from very serious limitations. The main limitation they all share is that they are unable to deal with cycles in the heap.

It turns out that garbage collecting a heap that includes cycles is almost impossible with such simple type systems. It requires more advanced reasoning about the state. In short, creating a cycle requires a memory assignment and we need to be able to reason about properties of this assignment in order to show that the GC will only collect garbage.

Chapter 5 presents a novel type system that allows such reasoning about the state, even in the absence of any aliasing information. I first explain why such reasoning is required and why existing type systems are unable to solve the problem in its generality. Then I show how this type system can be used to implement stack allocation as well as a generational GC. Contrary to the generational GC presented in the anterior chapter, this collector does not suffer from any fundamental restriction.

The FLINT optimizer

Standard ML [MTHM97] is a modern functional programming language, with a flexible type system and a powerful module language. SML/NJ [App91] is a robust and mature implementation of the Standard ML language which is also used as a testbed for research in compiler technology.

Since version 109.24, the SML/NJ compiler includes a strongly typed intermediate language which is used to implement some type-directed optimizations, most importantly a flexible representation analysis [Sha97a].

But the lion’s share of the middle end of the compiler was still implemented in an untyped intermediate language called CPS. This includes most optimization phases as well as the closure conversion. The FLINT project aims to use strongly typed languages for all phases of the compiler, so we needed to port the untyped optimizations out of the CPS part of the code to the earlier strongly typed phase. This included a minor redesign of the typed intermediate language to make it more amenable to optimization.

The optimizer in SML/NJ is fairly unsophisticated and mostly aims at cleaning up the most glaring inefficiencies introduced by the compiler or the language. My reimplementation does not attempt to go any further. Its main goal was to reduce the amount of work spent on untyped code. Of course, in order for the new optimizer to be fit for integration in the official SML/NJ distribution, I had to ensure that it did not slow down compilation or execution. To this end, the new code mostly does the same as the old one.

In the rest of this chapter, I will present the structure of the old middle end in Sec. 2.1, the new FLINT intermediate language in Sec. 2.2, how the middle end was changed in Sec. 2.3, then in Sec. 2.4 I discuss our experience comparing the A-normal form used in FLINT to the CPS form used in the old optimizer. Traditional untyped optimizations should ideally be unaffected by the addition of types; I discuss my experience in this respect in Sec. 2.5. finally, I will conclude with summary in Sec 2.7.

2.1 The old middle end

The SML/NJ compiler is composed of a front end, a middle end, and a back end. The front end does the lexical analysis, syntactic analysis, type inference, pattern matching compilation, as well as module language compilation. The back end takes care of the code generation, including register allocation and instruction scheduling. The middle end, which is the part of interest in this chapter, includes the optimization phases, the choice of representation of data-structures, and the closure conversion. A somewhat dated but still relevant presentation of the whole compiler can be found in [App91].

Prior to my work, the middle end was structured as follows [SA95]:

• PLambda: the typed language generated by the front end. This is basically a variant of the F_{ω} λ-calculus, extended with tuples and tagged sums. Functions take a single argument and return a single value. Every variable name is distinct, so there is never any possibility of name capture. This phase does A-normalization [FSDF93], which decomposes expressions into a sequence of elementary operations, where each intermediate value is bound to a variable.

• Lambda: the main type intermediate language. This language uses A-normal form but is otherwise the same as PLambda. This phase implements the representation analysis algorithm [Sha97a]. It is composed of the following phases:

– lcontract: this is a simple cleanup phase that inlines called-once functions to their sole calling location and flattens the let bindings by applying the let-associativity rule: let x = let y = e_{1} in e_{2} in e_{3} ⇒ let y = e_{1} in let x = e_{2} in e_{3}

– specialize: the type inference algorithm for SML is designed to find the most general type of expressions. This is good for programming, but regarding efficiency this often introduces unnecessary generality, so this phase instead specializes expressions to their least general type.

– wrap: this phase implements the core of the representation analysis [Sha97a], deciding which value needs to be boxed, which one needs to use coercions [Ler92] and which one type passing [HM95]. It also introduces the coercions where necessary.

– reify: this phase compiles away the type passing where it is used. In other words, it turns types into runtime data wherever this is needed. The output of this phase is not strongly typed any more, although it still has type annotations.

– convert: the last phase compiles switch statements to table-based computed jumps, turns the tags of tagged sums into plain integers, and most importantly converts the code to continuation passing style.

• CPS: the untyped intermediate language. This language is continuation based: functions never return and every primitive operation binds its return value to a variable. This phase first passes the code through a series of optimization phases, then applies closure conversion [SA94] and finally passes the result to the back end. The optimization phases are the following:

– contract: dead code elimination, constant propagation, constant folding, inlining of functions only called from a single spot [AJ97]. This phase is executed after almost every other phase, as a form of cleanup and it can also be executed repeatedly because its output might still contain missed opportunities for simplification.

– etasplit: introduces η-redexes such that all functions fall into one of two categories: either all of their call sites are known or none of their call sites are known. I.e. if a function f is not in one of those two categories, a new function f′ = λx.f x is introduced and all places where f escapes (i.e. is passed as a higher function argument rather than being directly called) are redirected to use f′.

– eta: elimination of η-redexes: replaces all expressions of the form λx.f x with f. Because this tends to undo the work of etasplit and because it is rarely beneficial, this phase is used only at the very beginning to cleanup the output of the CPS conversion, and at the end when η-redexes are not beneficial any more.

– expand: general inlining, which decides whether or not to inline functions called more than once based on a budget and on estimates of code size and of optimization opportunities that inlining will create. This also does loop unrolling, and introduction of loop pre-headers [App94] which allows loops to be inlined.

– flatten: argument flattening. As mentioned earlier, the earlier phases of the compiler use languages where functions take and return single values: when multiple values are needed, they are bundled in a tuple. This phase optimizes the code so that such bundles are opened up and passed directly in registers as multiple arguments or multiple return values.

– uncurry: simple form of uncurrying which turns curried functions fun f x y = e into an uncurried function fun f′(x, y) = e and an uncurry wrapper fun f x y = f′(x, y). The actual uncurrying at the call sites is done later on in the expand phase by inlining the wrapper.

Additionally to the above, there had been some cross-module inlining code [BA97] implemented in a previous untyped version of Lambda and which was never integrated into the official distribution. My work involved reimplementing the optimizations performed in CPS as well as the cross-module inlining to the successor of Lambda, called FLINT.

     datatype exp
       = RET of value list
       | LET of var list * exp * exp
       | FIX of fundec list * exp
       | APP of value * value list
       | TFN of tfundec * exp
       | TAPP of value * ty list
       | CON of con * ty list * value * var * exp
       | SWITCH of value * (con * exp) list * exp option
       | RECORD of value list * var * exp
       | SELECT of value * int * var * exp
       | RAISE of value * ty list
       | HANDLE of exp * value
       | PRIMOP of primop * value list * var * exp
     withtype fundec = fkind * var * (var * ty) list * exp
          and tfundec = var * (tvar * kind) list * exp

Figure 2.1: The FLINT datatype

2.2 The FLINT intermediate language

The new typed intermediate language, called FLINT, is a minor update of the Lambda

intermediate language, intended to make it more appropriate for optimization. Other than

cosmetic changes, the only real difference is the fact that functions now take multiple

arguments and return multiple values.

It is a variant of the F_{ω} λ-calculus using an A-normal representation, which means that

every intermediate value is bound to a variable. More concretely, it means that instead of

providing a primitive expression like (v_{1} + v_{2}), the language always uses expressions of the

form let x = v_{1} + v_{2} in e, so (v_{1} + (v_{2} + v_{3})) becomes let x_{1} = v_{2} + v_{3} in let x_{2} = v_{1} + x_{1} in e.

Figure 2.1 shows a slightly simplified definition of the SML datatype used to represent

FLINT expressions. value can be a variable var or an immediate integer. We decided

to keep type annotations to a minimum, so they are only added where a simple type

propagation traversal cannot infer them: mostly, function arguments.

Expressions evaluate to a list of values, returned with RET v⃗. LET(x⃗, e_{1}, e_{2}) executes e_{1},

binds the results to x⃗, and then executes e_{2}.

FIX(\overrightarrow{fun}, e) defines a set of mutually recursive functions bound in the scope of e. Each function has a name (a variable to which it is bound), a list of arguments with their type and a body. The variable to which the function is bound has type σ⃗_{1} → σ⃗_{2} where σ⃗_{1} is the list of types of the function’s arguments and σ⃗_{2} is the list of types of the function’s return values. For non-recursive functions, σ⃗_{2} is inferred when needed by type reconstruction, whereas for recursive functions σ⃗_{2} is recorded in the fkind part of the fundec. APP(v, v⃗) applies a function v to a list of arguments v⃗.

TFN(tfun, e) defines a type function tfun bound in the scope of e. A type function has a name (a variable to which it is bound), a list of type parameters with their kind, and a body. The type of the variable to which the type function is bound is ∀\overrightarrow{t:κ}.σ⃗ where \overrightarrow{t:κ} is the list of type arguments with their kind and σ⃗ is the type of the return results. Type functions are used to implement polymorphism. TAPP(f, σ⃗) passes the type arguments σ⃗ to the type function f. Type functions are side-effect free, so that any TAPP can freely be moved, copied, or deleted.

CON(con, σ⃗, v, x, e) constructs a tagged value of constructor con and carrying the value v. The result is then bound to x while executing e. σ⃗ is the list of types used to instantiate the constructor in cases when it is polymorphic. CON is used when constructing datatype elements. For example, when building an option value SOME v, con is SOME and σ⃗ is the type of v. SWITCH(v, \overrightarrow{(con, e)}, e_{def}) jumps to the e whose con matches the tag of v, or executes e_{def} if none of the branches match. Additionally to jumping, SWITCH also extracts the value v carried by the constructor con and binds it to a free variable of e.

RECORD(v⃗, x, e) makes a tuple carrying values v⃗, binds it to x, and executes e. Just as in SML, tuples are not mutable. SELECT(v, i, x, e) extracts the i^{th} element of tuple v, binds it to x and executes e.

RAISE(v, σ⃗) raises an exception v. σ⃗ is the type of the expression: since the expression does not return, we cannot infer its type otherwise. HANDLE(e, v) executes e while the function v is temporarily used as the exception handler.

PRIMOP(po, v⃗, x, e) executes the primitive po with arguments v⃗, binds the single result to x and then executes e. Primops include, arithmetic operations, ref-cell operations, array operations, and a lot more.

In order to avoid problems of name capture, every variable name is unique and this invariant is preserved by α renaming whenever a piece of code is duplicated.

2.3 The FLINT optimizer

The first part of the work was to port the old Lambda phases to the new FLINT representation. I will not discuss this for two reason: first because it was straightforward, given the similarity between the two languages, and second because most of the work was done by other people.

As explained in the introduction, since the purpose was not to do better than the old optimizer but to do just as good, the new optimizer basically applies the same optimizations as the old one. Nevertheless, the two are structured fairly differently and because the languages on which they operate are different, the optimizations they perform are not always the same.

The main differences are the following:

• Flattening: instead of the untyped local flattening optimization performed by the old optimizer, the new optimizer uses a type-directed flattening that is built on top of the pre-existing representation analysis phase.

• known functions: the old optimizer restricted several optimizations to known functions only, i.e. functions whose call sites are all statically known. The new optimizer uses optimizations that work either way instead.

• General organization: the phases in which specific optimizations are done are changed in order to try to reduce the number of traversals.

These points are explained below.

2.3.1 Known functions

The old optimizer conditionalized several optimizations on whether or not the function that is a candidate for an optimization is known, i.e. all its call sites are statically known. It was done when the optimization needed to change not only the function but also all its calls. This was the case for example for flattening or for elimination of dead arguments. To counteract this, the etasplit phase was introducing η-redexes for every function whose call sites were only partly known, such that all the known call sites would call the original function while the other places where the function was used referred to the η-redex instead.

In the new optimizer, I simplified those optimizations so that they do not need to update the call sites at all. Instead, when an optimization such as removal or dead arguments is performed, it splits the function into a small inlinable wrapper that uses the same calling convention as before and the main body whose calling convention is optimized. This technique was already used for uncurrying and I simply applied it wherever possible. The advantage is that it eliminates the need for the distinction between known and unknown functions, allows the optimization to be done completely locally without having to pay attention to the way the function is used, and lets the generic inliner do the actual work of updating the callers.

2.3.2 General organization

The FLINT optimizer is made of the following phases:

• flintnm: translate PLambda into FLINT. Other than cosmetic changes between the two languages, this is really a combination of A-normalization and the first phase of the flattening.

• fcontract: elimination of dead code, constant propagation, constant folding, function inlining, η-reduction, flattening of let bindings using the let-associativity rule. This closely corresponds to the combination of the CPS contract and eta phases, except that it also does general inlining, which used to be done in expand.

• lcontract: this phase was carried over from Lambda and does a subset of what fcontract does. It does a much less thorough job, but is much faster and was kept to do the first cleanup after translation from PLambda.

• loopify: look for functions that call themselves, wrap them in a pre-header, eliminate arguments that stay constant throughout the loop, and check whether all the recursive calls are in tail position, in which case the loop is marked as being a while loop. The corresponding optimization in the old optimizer was done in expand.

• fixfix: decide which functions are candidates for inlining, rewrite curried function like uncurry did in the old optimizer, and break up groups of apparently mutually-recursive functions into smaller subgroups. Other than introducing loop pre-headers, done in loopify, and deciding which function calls to inline and to perform the inlining itself, done in fcontract, this phase corresponds to the expand phase of the old optimizer.

• split: split top-level functions corresponding to SML functors into a small inlinable component and a large component containing the rest. The inlinable component is then added to the compilation units that refer to the current one, for cross-module inlining. This phase does not correspond to any optimization performed by the CPS optimizer, but corresponds instead to the lsplit phase that had been implemented in an earlier untyped incarnation of Lambda [BA97].

Part of the reason for having placed the different optimizations in different phases is just simplicity or expediency, but an important aspect was compile time. There are several reasons why compile time tends to be higher with a FLINT-based optimizer than with a CPS-based one, but it seems that the most important ones are the fact that the CPS terms have had their TAPP and TFN terms removed, the fact that a CPS representation is a bit more uniform, so the code of the optimizer is a bit simpler (see the Sec. 2.4 for a discussion of these issues), and the fact that the FLINT optimizer operates earlier in the compiler, so we still need to perform a bit of optimization in CPS after the FLINT optimizer in order to clean up some inefficiencies introduced by later phases.

The specific reasons for the new structure are as follows:

• As explained before, the need for etasplit is avoided by using slightly different formulations of the relevant optimizations.

• It was easy and harmless to do uncurrying as part of fixfix, with the added benefit that it merged two tree traversals into one. This is particularly important since those phases are worth doing often and both traversals need to build a whole new FLINT expression, which is slower than read-only traversals.

• It seemed easy and harmless to move η-elimination into fcontract, and with similar benefits as above. In retrospect, it took a long time to debug, which, I later learned, was the main reason why it was a separate phase in CPS.

• The reason to move the actual inlining out of fixfix and into fcontract was so that cascading inlining could take place. It also simplified fixfix slightly. Another reason was that split requires the choice of inlining candidates to be separate from the inlining itself.

• loopify was moved out of fixfix because it does not need to be run as often, but it requires two passes (a first pass that collects information and a second that does the code transformation), whereas fixfix is implemented in a single pass.

2.3.3 Flattening

All SML functions take a single argument and return a single value. In order to pass multiple arguments to a function, you can either use currying or you can combine the arguments into a tuple or a record. The SML syntax makes tupling and untupling mostly transparent. In the example below, (a,b) is a pattern that matches a tuple and extracts its elements and (b,a) builds a new tuple:

   fun f (a,b) = g (b,a)

This use of tuples is very convenient for the programmer, but for performance reasons the compiler needs to recognize such uses and eliminate the construction/deconstruction of tuples by passing the multiple values in separate registers instead.

This flattening optimization used to be performed in the old optimizer by a phase that would look for functions taking a tuple as argument and if all the call sites were statically known, it would change the function and all its callers. In the new optimizer we decided instead to take advantage of the representation analysis to do a type-directed flattening. If a function’s argument is of tuple-type, the function is changed to take the various elements of the tuple as separate arguments. Function calls are also correspondingly updated.

The main advantage of such a type-directed approach is that it works even if some of the call sites are not statically known. More specifically, this means that flattening takes place even if the function is exported to or imported from another compilation unit, or even if it passed as an argument to another function. Incidentally, in CPS, continuation functions are always passed to other functions, which means that return values were not flattened, whereas our approach does flatten return values just like it flattens arguments.

The presence of polymorphism makes type-directed flattening more complex. Let us take the identity function:

    fun f x = x

clearly this function cannot be flattened. Its type is ∀t.t → t: it takes an argument of any type t and returns a value of the same type. If we instantiate t to int × int we can use this function anywhere where a function of type int × int → int × int is expected. But such calls necessarily have their arguments and return values flattened. For this reason, when we instantiate t to int × int, we need to add coercion wrappers that adapt the calling convention of the polymorphic code to the calling convention corresponding to its instantiation. Figure 2.2 shows an example where the flattening phase had to complement the TAPP with a coercion wrapper so that f_{3} gets the type expected by the rest of the code. Those wrappers of course get inlined wherever possible. The insertion of those coercion wrappers is taken

                            let fun f x = x
                            in ... f (x:int*int)

⇓ unflattened translation ⇓ TFN f_{1} [t:Ω] = FIX f_{2} [x_{1} :t] = RET[x_{1}] IN RET[f_{2}] LET [f_{3}] = TAPP(f_{1}, [int × int])

⋮

IN APP(f_{3}, [x_{15}]))))

⇓ flattening ⇓

TFN f_{1} [t:Ω] = FIX f_{2} [x_{1} :t] = RET[x_{1}] IN RET[f_{2}] LET [f_{4}] = TAPP(f_{1}, [int × int]) FIX f_{3} [x_{2} :int, x_{3} :int] = LET x_{4} = RECORD[x_{2}, x_{3}] LET [x_{5}] = APP(f_{4}, [x_{4}]) LET x_{6} = SELECT(x_{5}, 0) LET x_{7} = SELECT(x_{5}, 1) IN RET[x_{6}, x_{7}]

⋮

LET x_{16} = SELECT(x_{15}, 0) LET x_{17} = SELECT(x_{15}, 1) IN APP(f_{3}, [x_{16}, x_{17}])))) Figure 2.2: Flattening polymorphic code.

care of by the representation analysis code which was already doing the same kind of things in order to box/unbox data structures.

As can be seen from the example, flattening can introduce a lot of extra code around type applications, in the form of wrappers that then require a lot of cleanup. Furthermore, we cannot not always inline the wrappers at the call sites and we are often unable to “inline” the wrappers into the wrapped function, so wrappers are difficult to eliminate once introduced, which means that we should avoid introducing such wrappers before we know that they will be needed. In practice this means that we should not flatten before specialize since specialize will eliminate a large number of type applications.

But at the same time, flattening can enable further optimizations, so we wanted to perform flattening early. So we decided to have our cake and eat it too: flattening is done in the first phase (flintnm), but the coercion wrappers are only inserted later, after specialize in the wrap phase which was already taking care of other coercion wrappers. In other words, until we get to executed wrap, our type applications are implicitly doing the coercions, such that in the previous example, TAPP(f_{1}, [int × int] magically returns a flattened function even though f_{2} is not flattened.

This has two side effects: first in specialize, we have to flatten functions whose argument or return type we change from a single polymorphic argument to a tuple. This turns out to be very good since it means that we flatten polymorphic code as early as possible. Second, the type system becomes more complex: when we instantiate a type like ∀t.[t] → [t] to int × int, we need to get ∀t.[int, int] → [int, int] instead of ∀t.[int × int] → [int × int], which means that the function we use to do type instantiation cannot just substitute int × int for t, but needs to flatten the resulting type if necessary.

2.3.4 Contracting β-redexes

The fcontract phase is really the workhorse of the FLINT optimizer, even more so than contract was for the old optimizer. The reason is that most other optimizations limit themselves to detecting and enabling optimization opportunities while leaving the actual work to fcontract. So it needs to do a thorough job when performing those optimizations and experience shows that it is easy to write a contraction phase that leaves a lot of optimization opportunities in its output, requiring repeated execution to get a good result.

A contraction phase is generally structured as a first phase that collects usage info to determine liveness of variables as well as to figure out which functions are only called once, and a second phase that performs the contractions. The problem is that contractions tend to cascade such that after having performed one contraction, others become possible, but the counters might not reflect it. For example, when eliminating a dead function, some other function might become dead or might have its call-count reach one, but unless the counters are properly updated, the optimization will be missed.

To minimize this problem, fcontract uses the same approach as was used in contract [AJ97]: counters are updated as optimizations are performed. Actually, fcontract is a bit more aggressive in that the counters of variables referred to by a term are decremented as soon as the term becomes dead, whereas in contract the decrement was only taking place on the way up of the recursion.

An important difference between the old contract and the new fcontract is the fact that fcontract performs general inlining rather than only inlining called-once functions. As mentioned, this allows cascading inlining. A typical example of cascading inlining is when map is passed an inlinable function: only after inlining map can the function argument be inlined. But this also runs the risk of inlining indefinitely.

Preventing infinite inlining is a classical problem and it is solved very straightforwardly in fcontract by keeping track of the stack of functions we are currently inlining so as to detect and break inlining cycles. The first attempt at solving the problem was to decide that recursive functions could not be inlined, but it had two disadvantages: first, it is not enough because you can use a recursive datatype to create a loop without any recursive function, second it is too restrictive because many wrappers such as uncurry wrappers can be recursive and yet should be inlined.

2.4 A-normal vs CPS

Other than the fact that the new optimizer works on typed terms, it is also different from the old optimizer in that it works on A-normal terms rather than CPS terms. In [FSDF93], A-normalization was proposed as an alternative to CPS that provides the same benefits but while preserving more of the source-level structure. In this section, I will discuss the various places in which the difference between the two seemed relevant.

2.4.1 The difference

In general, a term is said to be in CPS form if all the function calls are tail calls. That means that non-tail function calls need to be modified by passing an explicit continuation (which is equivalent in some sense to a return address and an activation frame), which the called function will then call when it wants to return. Also, all primitive operations only take immediate values or variables as arguments, rather than expressions, and bind their result to a variable, so every intermediate value has a name and all operations are explicitly sequentialized. Since tail-calls are only a step away from an assembly jump instruction, the CPS representation of a program provides a nice mix of being very close to assembly code while still enjoying the high-level formalism provided by the λ-calculus.

A-normal form keeps the requirement that all primitives only take an immediate value or a variable as argument and that they bind their result to a variable, so that primitive operations are sequentialized and all intermediate values are named, but it allows non-tail function calls. The idea behind it is that, this way, the main structure of the code is kept and we do not need to introduce explicit continuations. One of the advantages of keeping continuations implicit, is that it constrains the control flow in such a way that activation frames can be allocated on a stack. This advantage is not really interesting in our case, since the rest of the SML/NJ system uses heap-allocated activation frames anyway.

In terms of the definition of allowed terms, the difference between CPS and A-normal form is fairly small, and to a first approximation, we could say that the main difference between the two is embodied in FLINT’s LET terms which CPS would disallow. For this reason, it is interesting to look at the kinds of codes in FLINT that use LET. Most uses of LET are removed by simple local rewriting, the rest falls into the following 4 cases:

                      LET x⃗ = APP(v, v⃗) IN e
                      LET x⃗ = TAPP(v, σ⃗) IN e
                      LET x⃗ = HANDLE(e_{1}, v) IN e

LET x⃗ = SWITCH(v, \overrightarrow{(con, e)}, e_{def}) IN e′

Strictly speaking, FLINT is not using A-normal form, because the A-normal form was defined such that the fourth case was disallowed as well. More specifically, it would have been turned into something of the form:

SWITCH(v, con_{1} ⟹ LET x⃗ = e_{1} IN e′ con_{2} ⟹ LET x⃗ = e_{2} IN e′ … con_{n} ⟹ LET x⃗ = e_{n} IN e′ def ⟹ LET x⃗ = e_{def} IN e′)

Of course, such a transformation is naive since it blindly duplicates e′ and will lead to uncontrolled code size explosion. An easy way to address this issue is to use the following instead:

FIX k \overrightarrow{x:σ} = e′
SWITCH(v, con_{1} ⟹ LET x⃗ = e_{1} IN APP(k, x⃗) con_{2} ⟹ LET x⃗ = e_{2} IN APP(k, x⃗) … con_{n} ⟹ LET x⃗ = e_{n} IN APP(k, x⃗) def ⟹ LET x⃗ = e_{def} IN APP(k, x⃗))

The MLJ compiler [BKR98] used something similar, except that they additionally constrained their SWITCH statement such that each branch is just a function call. We decided not to stick to a strict A-normal form because it is not closed under β-reduction. In other words, if we replace an APP term by inlining the function, the resulting code might not be in strict A-normal form, so unless we are careful to always re-normalize the code simultaneously to inlining, we have to allow at least temporarily a de-normalized representation. Neither CPS nor our loose A-normal representation suffer from such a problem.

2.4.2 Advantages of using CPS

When compared to the original optimizer, the FLINT optimizer had several new problems to deal with. Some of them come from the difference between CPS and A-normal form, while others come from the difference between FLINT and strict A-normal form. The most obvious advantage of CPS is that returning from a function is done by a function call, so all optimizations written for function applications magically apply to function returns as well. A lot of optimizations that came for free in CPS have to be explicitly handled in FLINT, most notably:

• Inlining called-once functions in CPS subsumes the let-associativity rules in FLINT. Let-associativity is used by fcontract and lcontract to normalize code by replacing the various forms of

                   let x1 = let x2 = e1 in e2 in e3

with

                   let x2 = e1 in let x1 = e2 in e3

• Elimination of η-redexes in CPS subsumes the elimination of LET x⃗ = e IN RET x⃗ in FLINT.

• Elimination of dead function arguments in CPS subsumes elimination of dead variables in LET bindings in FLINT. Incidentally, this optimization is not fully implemented in FLINT because it requires a lot of extra processing.

• Removal of invariant function arguments in CPS subsumes the special code that detects tail-recursive loops, since it is nothing more than hoisting the continuation argument out of the loop. Also, the lack of GOTO in FLINT means that tail-recursive loops need to be marked with an ad-hoc annotation, whereas the corresponding optimization in CPS is all done within the language with no need for special annotations.

• Exception handling can be compiled away in CPS into plain functions and function calls, at which point function inlining subsumes elimination of RAISE inside a HANDLE, dead function elimination subsumes dead handler elimination, and so on. The old CPS language does not compile exception handling away, so it does not take advantage of this.

• Function inlining in CPS subsumes path-splitting in FLINT. Path-splitting is an optimization that turns

                   let x = (if a then b else c) in e

into

           if a then (let x = b in e) else (let x = c in e)

The FLINT optimizer does not perform this optimization because it did not seem worth the effort.

There are other circumstances where a CPS representation is more beneficial. The most obvious one is for exception handling: the restricted control flow in A-normal form makes any kind of optimization of exception handling very difficult. Luckily the old optimizer performed only very limited optimization of exception handling, so the lack of any such optimization in the new optimizer does not penalize it too excessively.

Another case where a CPS representation is helpful is with tail recursive loop exits, as in the example below:

        fun loop x = if e1
                     then e2
                     else loop e3
        let y = loop v
        in e

⟹^{CPS}

                                           let k y = e
                                           let loop x = if e1
                                                        then k e2
                                                        else loop e3
                                           in loop v

The direct-style version on the left does not make the connection between e2 and e explicit, whereas the CPS transformation shows quite clearly that k can be inlined to move e into the exit of the loop. The FLINT optimizer does not attempt to recognize such cases.

2.4.3 Advantages of A-normal

Of course, an A-normal representation of code has its own advantages. One already mentioned is that it puts the necessary constraints on control flow to ensure that the activation frame obey a stack discipline.

Another one is that in CPS, the return continuation is just a higher-order function and there is no obvious indication of the fact that its use follows a special pattern: for example, if all the call sites of a function are known, then its continuation argument can only contain values within a particular set and those continuations will not be used anywhere else than in this function.

Such constraints ensure that we can easily change the calling convention between this call site and the corresponding functions. Optimizations that could take advantage of this are optimizations such as flattening of return values, or elimination of dead or constant return values. Indeed the flattening phase in CPS did not flatten return values and it would have been difficult to extend it to do that, whereas a translation of that phase into FLINT could have been naturally extended to also flatten return values. As it turns out, we used a type-directed flattening algorithm, which is oblivious to such issues and would work just as well in a CPS representation.

2.5 Impact of types

While porting those classical untyped optimizations from the untyped CPS language to the typed FLINT, most of the work was unaffected by the addition of types. But there were of course some exceptions. Types interact with the optimizer in several ways:

• Too many type annotations can make the compiler writer’s life more difficult by having to do a lot of work to maintain those types through the various phases. They can also lead to a slowdown of the compiler.

• Too few types annotations can make some optimizations difficult. This can happen when the optimization’s output requires more type annotations than the input, in which case some type inference is needed.

• Additional terms that manipulate types such as TAPP or TFN make the optimizer more complex and potentially slower, but more importantly they can hide or prevent optimizations.

• Types can provide additional information which can enable more optimizations. This is rather rare in the context of traditional untyped optimizations.

Here are in no particular order the concrete ways in which types influenced the FLINT optimizer:

• Manipulating types can be costly. The Lambda intermediate language used a very efficient type representation which was reused for the FLINT language [SLM98], so we had already addressed most of the performance trouble spots. But we did encounter cases where we used types naively in the flattening code, which lead to an exponential complexity in some cases. The problem was that we traversed our types as if they were trees, instead of being careful to traverse them as graphs to benefit from the large mount of sharing.

• LET [x] = RAISE(y, σ⃗) IN e: in code such as this, e is clearly dead and we should be able to replace the whole expression with RAISE(y, xσ′). The problem here is that σ⃗′ is the type of the values returned by e and this type is not readily available. Because of this problem, the FLINT optimizer does not perform dead code elimination for the above code. There are many ways to solve this problem, from removing the type annotation on RAISE to adding type annotations at other places, but my favorite solution is probably to use a CPS representation of the code, in which case this dead code elimination comes for free.

• LET x_{0} = SELECT(x, 0) ... LET x_{n} = SELECT(x, n) INRECORD(x_{0}, ..., x_{n}): to replace this record construction with just a reference to x we need to ensure that x is of size n. This only works if we have the corresponding type information. The FLINT optimizer does perform this optimization when the type information is easily accessible. It is not often useful, but it can help when flattening over-enthusiastically. This optimization might also be considered valid even if the size of x is not n (i.e. even with no type information), but it can lead to memory leaks.

• The original version of FLINT used DeBruijn indices in its representation of types. This meant that moving code into or out of a TFN term required adjusting all the embedded type annotations. This was sufficiently tricky that I decided at first to temporarily disallow function inlining into a TFN. Later Christopher League changed the type representation to use named variables for type variables bound by TFN, while still keeping DeBruijn indices for type variables bound inside the types themselves. Thanks to this, the FLINT optimizer now ignores DeBruijn indices and inlines into

TFNterms.

• Inlining TFN terms is difficult: there are several problems at play here. First is the fact that the classical inlining heuristics are designed for functions that take actual values as arguments and use them actively in their body. Those heuristics work poorly for functions of no arguments, which is how TFN would be treated. But additionally to this, in the case of FLINT, inlining a TFN is made more difficult by the fact that we may need to do flattening since we are instantiating the type variable. Because of this, fcontract never inlines TFN. On another hand, specialize does properly eliminate any TFN that is called only once.

• TFN hides its content, preventing other optimizations, such as in the following case: TFN f [t:Ω] = FIX g [x:σ_{x}] = … FIX h [y:σ_{y}] = … IN RET [g, h]

…
LET [g, h] = TAPP(f, [σ])
IN … APP(g, v⃗) …

In an untyped setting, both g and h would be defined at top level rather than inside f, so if g is a small inlinable function, it gets inlined as a matter of course. But the presence of TFN hides this and the FLINT optimizer will miss this opportunity. Maybe the λ-splitting code in split could be reused to solve those cases.

• Similarly, a polymorphic curried function will sometimes not be uncurried because the curry wrapper is hidden in the outside TFN used to represent polymorphism. The FLINT optimizer does not currently attempt to handle this case either and to uncurry the TFN together with the function.

2.6 Related work

Most functional language compilers rely on similar optimizations as presented here. But few of them use typed intermediate representations.

The TIL project [TMC^{+}96] is probably the closest. They also use an intermediate representation derived from A-normal form and they implement the same untyped optimizations, plus a few loop-based optimizations such as array bounds check elimination, common subexpression elimination, and loop invariant removal. Their flattening is not type-directed.

The MLJ compiler [BKR98] also uses a similar setup to ours. Its internal types include some effect annotations which allows it to do type-directed uncurrying and its backend is quite different because of the need to adapt to the idiosyncrasies of JVM, but the untyped optimizations are otherwise very similar to ours.

The Glasgow Haskell compiler [PMar, PS98] also uses a typed intermediate language and a very similar structure of the optimizer. It does not use type-directed optimizations for flattening, data-representation, or uncurrying but still carries types to implement Haskell’s type classes dictionaries.

2.7 Conclusion

The port of the traditional optimization from the untyped CPS intermediate language to the typed FLINT intermediate language did not suffer excessively from the fact that the target language was using A-normal form or that it was strongly typed, although both aspects turned out to be minor impediments.

In retrospect, a language based on a CPS representation of the code is better suited to those kinds of optimizations because it is more uniform and makes several things more explicit, most importantly control flow, which would help optimize tail-recursive loops exits, exception handling, and sequences of conditionals. Of course, a CPS representation might introduce other problems if we had to use a stack-discipline for our activation frames. It seems that a technique like the one presented in [DL92] could be used to recover the needed information, but I can only speculate.

Also, the trick of spreading flattening from flintnm to wrap through several other phases is probably not worth the effort. It seems like it would be better to do it once and for all in wrap so the other phases can manipulate the code more freely.

The new optimizer has been included in the official distribution of SML/NJ since version 110.26. Interestingly enough it was first integrated in version 110.10, then removed because of a bad interaction with some other change. It took a year of debugging and finger pointing to finally fix the bug which happened to be caused by generated code that did not obey the garbage collector’s expected interface. This would not have happened had SML/NJ used a formalized and automatically checked interface between its collector and mutator, like the ones I will show in the second half of this thesis.

Inlining as staged computation

Clear and maintainable code requires modularity and abstraction to enforce well-designed interfaces between software components. The module language of Standard ML [MTHM97] provides powerful tools for such high-level code structuring. But these constructs often incur a considerable performance penalty which forces the programmer to break abstraction boundaries or to think twice before using advanced features like parameterized modules (e.g., ML functors).

Efficient implementation of these high-level language constructs often rely crucially on function inlining. Inlining algorithms have been used for many years, but their “best-effort” behavior prevents me from knowing or making sure that a function will always be inlined (at least, wherever possible given the compilation model). For example, SML/NJ has several ad-hoc tricks sprinkled in the code to expand primitive operations. These tricks tend to muddy up the abstraction boundaries so it would be nice if they could be replaced by a general-purpose inlining algorithm.

But would the inliner perform as good a job inlining those primitive operations as with the ad-hoc approaches? For simple cases, it is straightforward to ensure that primitive operations are always inlined, but when higher-order functions or even higher-order modules (such as SML/NJ functors or Java generics) come into play, coupled with separate compilation, the question becomes more challenging. In the course of implementing the extension of Blume and Appel’s cross-module inlining algorithm [BA97], we tried to understand the relationship between inlining opportunities and separate compilation. We felt a need to formalize the solution to better understand its behavior.

This chapter is the result of my efforts to formalize the inlining algorithm presented in the previous section. More specifically, I borrow from the partial-evaluation community [JGS93] to model inlining as a staged computation. By using a two-level λcalculus [Mog97] as my intermediate language, I can assign each function (in the program) with a binding-time annotation of static or dynamic: a static function call is executed at compile time and thus is always inlined, while a dynamic call is executed at run time and thus is not inlined. The inlining optimization is then equivalent to the standard off-line partial evaluation: First, use the binding-time analysis to locate all the inlining candidates, then run the specialization phase to do the actual inlining. The binding-time attributes can also be exported to the source level (or the compiler front-end) to serve as inlining annotations and to allow programmers (or the compiler writer) to control various inlining decisions manually.

Apparently, all partial evaluators support some form of β-reductions as part of the specialization, however, these techniques do not immediately apply to the inlining optimization. Because of the different application domains, partial evaluators are generally much more aggressive than compiler optimizers. Even the binding time annotations can pose problems at the source level because they can clutter the module interface and interact badly with ML functors; for example, I would have to add abstraction over binding-time (commonly called “binding-time polymorphism”) in the type if I wanted to apply a functor to modules with different binding time (but with same signature otherwise).

The main objective of this chapter is to hammer out these details and to see what it would take to launch various partial-evaluation techniques into real compilers. This chapter builds upon previous work on cross-module inlining [BA97, Sha98, Ler95] and two-level λcalculus [Mog97, NN92, DP96, TS97] but makes the following new contributions:

• As far as I know, my work is the first comprehensive study on how to model inlining as staged computation. The formalism from the staging calculus allows me to explicitly reason about and manipulate the set of inlinable functions. Doing such reasoning is much harder with a traditional inlining algorithm, especially in the presence of ML-style parameterized modules.

• By careful engineering of binding-time coercions, and combined with proper staging and splitting, I show how to model inlining as staged computation without introducing unwanted code duplication (see Sec. 3.4).

• Adding inlining annotations to a surface language allows a programmer to mark a function as inlinable explicitly. Inlining annotations, however, could pollute the module interface with additional binding-time specifications. This makes the underlying module language more complex. I show how to support inlining annotations while still avoiding such pollution. In fact, my scheme is fully compatible with ML-style modules and requires no change to its module language.

• Using a two-level λ-calculus, I show how inlining annotations are compiled into the internal binding-time annotations and how they interact with the module code. This allows me to propagate inlining information across arbitrarily functorized code, even in the presence of separate compilation.

• I extend binding-time coercions to work with parametric polymorphism.

The rest of this chapter is organized as follows: Section 3.1 gives an overview of a compiler that supports cross-module inlining and shows how inlining annotations (at the source level) and two-level λ-calculus (as intermediate language) fit into the picture; Section 3.2 formally defines my source language SRC which supports inlining annotations and (indirectly) MLstyle modules; Section 3.3 formally defines my target language TLC which is a two-level λ-calculus supporting staged computation; Section 3.4 presents my detailed algorithm for compiling SRC programs into TLC; the algorithm involves staging, splitting, and careful insertion of binding-time coercions; Section 3.5 shows how to handle top-level issues for

Source Language

Typed SRC (Sec. 3.2) (Sec. 3.4) Staging+Split

[figure: TLC (Sec. 3.3); Import Summaries (Sec. 3.5); Partial Evaluation (Sec. 3.3)]

TLC

(no static redexes)

\overline{}Export Summary (Sec. 3.5)

BTR+Opts

(Sec. 3.4.3) λ-Split Residual Code

Machine Code

Figure 3.1: Structure of the compiler

inlining across multiple compilation units; Section 3.6 then presents several extensions over the basic algorithm; finally, Section 3.7 and 3.8 describe related work and then conclude.

3.1 The big picture

To model inlining as staged computation, we first give an overview of a compiler that supports cross-module inlining. We use our FLINT compiler [Sha97b, SLM98] as an example. Figure 3.1 shows various stages of compilation used in the compiler. The source code is first turned into a strongly typed intermediate language based on a predicative System-F calculus (we name it SRC and present its details in Sec. 3.2). The SRC calculus contains a module language and a core language. Each core-language function is annotated with inlining hints to indicate whether the function should be inlined or not. Those hints could be provided by the user or by the earlier phases of a compiler (using some inlining heuristics).

The inlining hints are then turned into staging annotations, mapping inlinable functions to static functions (functions executed at compile-time) and the rest to dynamic code (executed at run-time), by translating the code into a two-level intermediate language extended with polymorphism (we name it TLC and present its details in Sec. 3.3).

To minimize the performance cost of the module code, we want to mark it as static so as to expose as many inlining opportunities as possible. But this would imply that each functor application (SML’s equivalent to template instantiation) would create a duplicate copy of the full functor body. This approach, while sometimes acceptable, can lead to excessive code growth and compilation times for heavily functorized code, as any programmer who has worked with C++ templates knows.

We use a variant of the λ-splitting technique [BA97] to split each module function into a static part and a dynamic part. This splitting is done carefully to ensure that it does not obfuscate any inlining opportunity. Splitting is done together with staging in the main translation algorithm (see Sec. 3.4.4). The resulting code is completed by incorporating a copy of the summaries from all the import modules (see Sec. 3.5); a summary contains the code that should be inlined across compilation-units, similarly to OCaml’s approximations [Ler95].

The static part of the code is then reduced by a straightforward partial evaluation returning the same code but with no remaining static redexes. This code then goes through a binding-time refinement (BTR) or other optimization phases which could introduce new static code requiring a new pass through the partial evaluator.

Once these optimization steps are finished, we reuse the λ-splitting algorithm to split the compilation-unit itself into a summary containing all the remaining static code (i.e. inlinable code for future cross-compilation-unit inlining) and a fully dynamic residual code (encompassing the bulk of the program) which is then passed to the code generator.

Inlining across compilation units increases the coupling between those units. If a unit is modified, all units that import it will now need to be recompiled, even if the modification was only internal and did not change the interface. This is automatically handled in our case by a compilation manager [Blu95].

(ctypes) τ ::= int | t | τ_{1} → τ_{2}
(mtypes) σ ::= V(τ) | σ_{1} × ^{...} × σ_{n} | σ_{1} → σ_{2} | ∀t.σ
(inline) a ::= | i
(cterms) c ::= n | z | π_{v}x | λ^{a}z:τ.c | c_{1}c_{2}
(mterms) m ::={x | ι_{v}(c) | ⟨x_{1},..., x_{n}⟩ | π_{i}x | λx:σ.m | @x_{1}x_{2} | Λt.m | x[τ] \atop | let x = m_{1} in m_{2}}

Figure 3.2: Syntax for the source calculus SRC

3.2 The source calculus SRC

This section formally defines our source language SRC which is a variant of the polymorphic lambda calculus System-F [Gir72, Rey74]. SRC differs from System-F in that it has inlining annotations on the core functions and it has a stratified structure with a polymorphic module language layered on top of a monomorphic core language. Also the module language uses A-normal form [FSDF93] which means that all intermediate values need to be named via let-binding, thus making all sharing between expressions explicit.

The syntax of SRC is given in Fig. 3.2. Here, a SRC program is just a module term (m). Each module term can be either a variable (x), a structure (ι_{v}(c)) consisting of a single core term (c), a compound module consisting of a collection of other modules (⟨x_{1},..., x_{n}⟩), an i-th component from another module (π_{i}x), a parameterized module (over other modules: λx:σ.m or over types: Λt.m), a module application (over other modules: @x_{1}x_{2} or over types: x[τ]), or a let declaration.

Because the module language can already express polymorphic functions, we intentionally restrict the core language to be a simply typed lambda calculus. A core term can be either an integer constant (n), a variable (z), a value field of a module (π_{v}x), a function definition (λ^{a}z:τ.c) with inlining annotation (a), or a function application (c_{1}c_{2}).

A module type can either be a singleton-value type (V(τ) which refers to a module consisting of a core term of type τ), a compound module type (σ_{1} × ^{...} × σ_{n} with n submodules, each with type σ_{i} for i = 1, ..., n), or a parameterized module (over other modules: σ_{1} → σ_{2} or over types: ∀t.σ). A core type can be either the integer type (int), a type

kind environment Δ ::= · | Δ, t
type environment Γ ::= · | Γz, τ: | Γx, σ:
[figure]

Δ ⊢ τ and Δ ⊢ σ and Δ ⊢ Γ

Δ ⊢ int  {t ∈ Δ \atop Δ ⊢ t}  \begin{matrix}Δ ⊢ τ_{1} & Δ ⊢ τ_{2} \\ Δ ⊢ τ_{1} → τ_{2} \end{matrix}  {Δ ⊢ τ \atop Δ ⊢ V(τ)}  {Δ ⊢ σ_{i} (1≤i≤n) \atop Δ ⊢ σ_{1} × ^{...} × σ_{n}}

\begin{matrix}Δ ⊢ σ_{1} & Δ ⊢ σ_{2} \\ Δ ⊢ σ_{1} → σ_{2} \end{matrix}  {Δ, t ⊢ σ \atop Δ ⊢ ∀t.σ}  Δ ⊢ ·  \begin{matrix}Δ ⊢ Γ & Δ ⊢ τ \\ Δ ⊢ Γz, τ: \end{matrix}  \begin{matrix}Δ ⊢ Γ & Δ ⊢ σ \\ Δ ⊢ Γx, σ: \end{matrix}

[figure: Δ; Γ ⊢ m : σ and Δ; Γ ⊢ c : τ]

{Δ ⊢ Γ \atop Δ; Γ ⊢ n : int}  {Δ ⊢ Γ \atop Δ; Γ ⊢ z : Γ(z)}  {Δ; Γ ⊢ x : V(τ) \atop Δ; Γ ⊢ π_{v}x : τ}

{Δ; Γz, τ_{1} :⊢ c : τ_{2} \atop Δ; Γ ⊢ λ^{a}z:τ_{1}.c : τ_{1} → τ_{2}}  \begin{matrix}Δ; Γ ⊢ c_{1} : τ_{2} → τ_{1} & Δ; Γ ⊢ c_{2} : τ_{2} \\ Δ; Γ ⊢ c_{1}c_{2} : τ_{1} \end{matrix}

{Δ ⊢ Γ \atop Δ; Γ ⊢ x : Γ(x)}  {Δ; Γ ⊢ c : τ \atop Δ; Γ ⊢ ι_{v}(c) : V(τ)}  {Δ; Γ ⊢ x_{i} : σ_{i} (1≤i≤n) \atop Δ; Γ ⊢ ⟨x_{1},..., x_{n}⟩ : σ_{1} × ^{...} × σ_{n}}

\begin{matrix}Δ; Γ ⊢ x : σ_{1} × ^{...} × σ_{n} & 1≤i≤n \\ Δ; Γ ⊢ π_{i}x : σ_{i} \end{matrix}  {Δ; Γx, σ_{1} :⊢ m : σ_{2} \atop Δ; Γ ⊢ λx:σ_{1}.m : σ_{1} → σ_{2}}

\begin{matrix}Δ; Γ ⊢ x_{1} : σ_{2} → σ_{1} & Δ; Γ ⊢ x_{2} : σ_{2} \\ Δ; Γ ⊢ @x_{1} x_{2} : σ_{1} \end{matrix}  {Δ, t; Γ ⊢ m : σ \atop Δ; Γ ⊢ Λt.m : ∀t.σ}

{Δ; Γ ⊢ x : ∀t.σ \atop Δ; Γ ⊢ x[τ] : \{τ/t\}σ} Δ; Γ ⊢ m_{1} : σ_{1} Δ; Γx, σ_{1} :⊢ m_{2} : σ_{2}

Δ; Γ ⊢ let x = m_{1} in m_{2} : σ_{2}

Figure 3.3: Static semantics for SRC variable, or a function type (τ_{1} → τ_{2}). The singleton-value type V(τ) is used to distinguish between cases like V(int → int) and V(int) → V(int).

The SRC language was chosen to be expressive enough to exhibit the main difficulties that an optimizer based on staged computation might encounter. The language is split between the module and the core languages because the inliner needs to use two different compilation strategies. Of course, we could merge the two languages and annotate the terms to indicate whether or not to treat them like module code. Recent work [Sha98, Sha99, HMM90] has shown that Standard ML can be compiled into a SRC-like typed intermediate language.

Figure 3.3 gives the static semantics for SRC. The environment Δ is the list of bound type variables; the type environment Γ maps both core and module variables to their respective types. Both the type- and the term-formation rules are rather straight-forward. The language is predicative in that the module language supports polymorphism but type variables can only be instantiated to core types. SRC uses a call-by-value semantics (omitted since it is straightforward); it is easy to show that the typing system for SRC is sound with respect to the corresponding dynamic semantics.

The most interesting feature of SRC is the inlining annotation a. The annotation i means that the underlying λ-expression should be inlined if possible while the empty annotation means it should not. Notice that we do not track inlining annotations in the types; a core function λ^{a}z:τ.c has the same type whether the annotation a is i or empty.

This design choice is deliberate. We believe inlining annotations should be made as non-intrusive as possible. Tracking them in the types would significantly complicate the module language; for example, we would have to add binding-time polymorphism in the type if we want to apply a functor to modules with different inlining annotations (but with the same signature otherwise).

When compiling SML to a SRC-like language, the SML module language maps to the SRC module language as expected, but the SML core language does not map only to the SRC core language: polymorphic core SML functions translate to module-level type

(kind) b, k ::= s | d
(type) σ ::= int | t | σ_{1} ×^{b} ^{...} × σ_{n} | σ_{1} →^{b} σ_{2} | ∀^{b}t:k.σ
(term) e ::= v | π_{i}^{b}v | @^{b}v_{1} v_{2} | v[σ]^{b} | let x = e_{1} in e_{2}
(value) v ::= n | x | ⟨v_{1},..., v_{n}⟩^{b} | λ^{b}x:σ.e | Λ^{b}t:k.e

Figure 3.4: Syntax for the target calculus TLC

abstractions (together with a core-level function) in SRC. This does not introduce any problem, however; since polymorphic recursion is not available, type instantiations can be done statically, or hoisted to the top-level [SS98].

3.3 The target calculus TLC

This section formally defines our target language TLC. As a typed intermediate language, TLC is is essentially a hybrid of System-F in A-normal form [FSDF93] and the two-level lambda calculus λ2sd by Moggi [Mog97].

The syntax of TLC is given in Fig. 3.4. A TLC term (e) can be either a value (v), a record selection, a function application, a type application, or a let expression. A TLC value (v) is either an integer constant, a variable, an n-tuple, a function, or a type function. A TLC type is either the integer type, a type variable, a record type, a function type, or a polymorphic type. Many of these have a binding-time annotation (called “kind”) that can either be s for static code (evaluated at compile-time) or d for dynamic code (evaluated at run-time).

Compared to SRC, TLC replaces inlining hints on core functions with staging annotations on tuples, functions and type-abstractions and merges the module and the core languages since the distinction between the two is only needed to direct the translation from SRC. Notice also how ∀ types have two binding-time annotations, one for the type abstraction itself and another that constrains the possible types it can be instantiated to.

To simplify the presentation, we force the ground types (i.e., int) to be considered as dynamic. This is justified by the fact that we are only interested in function-level reductions.

We would lift this restriction if we wanted to model constant propagation.

In the rest of this paper, we will also use the following syntactic sugar:

π_{i}^{b}e ≡ let x = e in π_{i}^{b}x
e[σ]^{b} ≡ let x = e in x[σ]^{b}
@^{b}e_{1} e_{2} ≡ let x_{1} = e_{1} in let x_{2} = e_{2} in @^{b}x_{1} x_{2}
⟨e_{1},..., e_{n}⟩^{b} ≡ let x_{1} = e_{1} in … let x_{n} = e_{n} in ⟨x_{1},..., x_{n}⟩^{b}
λ^{b}⟨x_{1},..., x_{n}⟩^{b′}:σ.e ≡ λ^{b}x:σ.let x_{1} = π_{1}^{b′}x in … let x_{n} = π_{n}^{b′}x in e

Essentially, we will put an e term where only v is allowed, leaving the let transformation implicit and we will use a pattern-matching variant of let; we will also assume that alpharenaming is used so variables are never shadowed.

Figure 3.5 gives the typing rules for TLC. In addition to the usual type safety, these rules also ensure binding-time correctness. Here the kind environment Δ maps type variables to their binding time; the type environment Γ maps variables to their types. To enforce the usual restriction that no dynamic entity can contain or manipulate a static value, types are classified as being either of dynamic kind or static kind, with subkinding such that a type σ of dynamic kind can also be considered to have static kind but not vice versa.

Figures 3.6 to 3.9 give the dynamic semantics for TLC as a set of primitive reductions and single-step evaluation relations that determine where those reductions can be applied. Figure 3.6 defines the primitive static reduction e ⇝_{s} e′. Figure 3.7 defines the single-step partial evaluation e ↦_{s} e′ together with the corresponding v ↦_{s}^{v} v′ used for values. Note how partial evaluation in this language amounts to reducing all the static redexes of a term. Figures 3.8 and 3.9 show the corresponding reductions ⇝ and ↦ of a standard call-by-value evaluator. In contrast to the partial evaluation case, those reductions apply to both static and dynamic redexes but only to the outermost ones.

TLC is a variant of Moggi’s λ_{c} [Mog88] restricted to A-normal form; the primitive reduction relations in Fig. 3.6 and 3.8 are same as that for λ_{c} (except that we added type applications and removed η-reductions). We claim that it is easy to show that the type

kind environment Δ ::= · | Δ, t:κ
type environment Γ ::= · | Γx, σ:

[figure: b_{1} ≤ b_{2}]

d ≤ d d ≤ s s ≤ s

[figure: Δ ⊢ σ : κ and Δ ⊢ Γ]

Δ ⊢ int : d Δ ⊢ t : Δ(t) \begin{matrix}Δ ⊢ σ : b_{1} & b_{1} ≤ b_{2} \\ Δ ⊢ σ : b_{2} \end{matrix} {Δ ⊢ σ_{i} : b (1≤i≤n) \atop Δ ⊢ σ_{1} ×^{b} ^{...} × σ_{n} : b}

\begin{matrix}Δ ⊢ σ_{1} : b & Δ ⊢ σ_{2} : b \\ Δ ⊢ σ_{1} →^{b} σ_{2} : b \end{matrix}  \begin{matrix}Δ, t:k ⊢ σ : b & k ≤ b \\ Δ ⊢ ∀^{b}t:k.σ : b \end{matrix}  Δ ⊢ ·  \begin{matrix}Δ ⊢ Γ & Δ ⊢ σ : s \\ Δ ⊢ Γx, σ: \end{matrix}

[figure: Δ; Γ ⊢ e : σ and Δ; Γ ⊢ v : σ]

{Δ ⊢ Γ \atop Δ; Γ ⊢ n : int}  {Δ ⊢ Γ \atop Δ; Γ ⊢ x : Γ(x)}  \begin{matrix}Δ; Γ ⊢ v_{1} : σ_{2} →^{b} σ_{1} & Δ; Γ ⊢ v_{2} : σ_{2} \\ Δ; Γ ⊢ @^{b}v_{1} v_{2} : σ_{1} \end{matrix}
\begin{matrix}Δ; Γ ⊢ v_{i} : σ_{i} (1≤i≤n) & Δ ⊢ σ_{1} ×^{b} ^{...} × σ_{n} : b \\ Δ; Γ ⊢ ⟨v_{1},..., v_{n}⟩^{b} : σ_{1} ×^{b} ^{...} × σ_{n} \end{matrix}  {Δ; Γ ⊢ v : σ_{1} ×^{b} ^{...} × σ_{n} 1≤i≤n \atop Δ; Γ ⊢ π_{i}^{b}v : σ_{i}}

\begin{matrix}Δ; Γx, σ_{1} :⊢ e : σ_{2} & Δ ⊢ σ_{1} →^{b} σ_{2} : b \\ Δ; Γ ⊢ λ^{b}x:σ_{1}.e : σ_{1} →^{b} σ_{2} \end{matrix} Δ; Γ ⊢ e_{1} : σ_{1} Δ; Γx, σ_{1} :⊢ e_{2} : σ_{2}

Δ; Γ ⊢ let x = e_{1} in e_{2} : σ_{2}
\begin{matrix}Δ, t:k; Γ ⊢ e : σ & Δ ⊢ ∀^{b}t:k.σ : b \\ Δ; Γ ⊢ Λ^{b}t:k.e : ∀^{b}t:k.σ \end{matrix}  \begin{matrix}Δ; Γ ⊢ v : ∀^{b}t:k.σ_{2} & Δ ⊢ σ_{1} : k \\ Δ; Γ ⊢ v[σ_{1}]^{b} : \{σ_{1}/t\}σ_{2} \end{matrix}

Figure 3.5: Static semantics for TLC

system for TLC is sound and the static reduction ↦_{s} is strongly normalizing and confluent. We can thus define a partial evaluation function P_{e}(e) that returns the static normal form of e. Similarly it is easy to show that ↦ is confluent, so we can also define a partial function R_{e}(e) which does the standard evaluation of e. R_{e}(e) happens to be a total function, but we do not rely on it.

P_{e}(e) = e′such that e ↦_{s}^{∗} e′ and there is no e′′ for which e′ ↦_{s} e′′
R_{e}(e) = e′such that e ↦^{∗} e′ and there is no e′′ for which e′ ↦ e′′

(β_{λ}) @^{s}(λ^{s}x:σ.e) (v) ⇝_{s} {v/x}e
(β_{Λ}) (Λ^{s}t:k.e)[σ]^{s} ⇝_{s} {σ/t}e
(π) π_{i}^{s}⟨v_{1},..., v_{n}⟩^{s} ⇝_{s} v_{i}  if 1≤i≤n
(let) let x = v in e ⇝_{s} {v/x}e
(asc) let x_{2} = (let x_{1} = e_{1} in e_{2}) in e_{3}
⇝_{s} let x_{1} = e_{1} in let x_{2} = e_{2} in e_{3}

Figure 3.6: Primitive static reduction for TLC

e ⇝_{s} e′ ⇒  e ↦_{s} e′
e ↦_{s} e′ ⇒  let x = e in e_{2} ↦_{s} let x = e′ in e_{2}
e ↦_{s} e′ ⇒  let x = e_{2} in e ↦_{s} let x = e_{2} in e′
e ↦_{s} e′ ⇒  λ^{b}x:σ.e ↦_{s}^{v} λ^{b}x:σ.e′
e ↦_{s} e′ ⇒  Λ^{b}t:k.e ↦_{s}^{v} Λ^{b}t:k.e′
v ↦_{s}^{v} v′ ⇒ ⟨v_{1}, …, v, …, v_{n}⟩^{b} ↦_{s}^{v} ⟨v_{1}, …, v′, …, v_{n}⟩^{b}
v ↦_{s}^{v} v′ ⇒  @^{b}v v_{2} ↦_{s} @^{b}v′ v_{2}
v ↦_{s}^{v} v′ ⇒  @^{b}v_{2} v ↦_{s} @^{b}v_{2} v′
v ↦_{s}^{v} v′ ⇒  v[σ]^{b} ↦_{s} v′[σ]^{b}
v ↦_{s}^{v} v′ ⇒  π_{i}^{b}v ↦_{s} π_{i}^{b}v′

Figure 3.7: Single-step partial evaluation for TLC

(⇝_{s}) e  ⇝ e′  if e ⇝_{s} e′
(β_{λ}) @^{d}(λ^{d}x:τ.e) (v) ⇝ {v/x}e
(β_{Λ}) (Λ^{d}t:k.e)[τ]^{d} ⇝ {τ/t}e
(π) π_{i}^{d}⟨v_{1},..., v_{n}⟩^{d} ⇝ v_{i}  if 1≤i≤n

Figure 3.8: Primitive reduction relation for TLC

e ⇝ e′ ⇒  e ↦ e′
e ↦ e′ ⇒ let x = e in e_{2} ↦ let x = e′ in e_{2}

Figure 3.9: Single-step call-by-value standard evaluation for TLC where ↦^{∗} and ↦_{s}^{∗} are the reflexive transitive closures of ↦ and ↦_{s}. TLC satisfies the following important residualization property:

Theorem 3.3.1 (Residualization) If Δ; Γ ⊢ e : σ and Δ ⊢ σ : d and ∀x∈fv(e). Δ ⊢ Γ(x) : d, then P_{e}(e) is free of any static subterms.

In other words, given an expression e with dynamic type σ, partially evaluating e will inline all of its inlinable functions and result in an expression free of static subterms.

Next we show why inlining does not affect the semantics of the program. We first introduce a notion of semantic equivalence on well-typed TLC values:

Definition 3.3.2 (Equivalence) If ·; · ⊢ v : σ and ·; · ⊢ v′ : σ, we say that v ≃ v′ if one of the following holds:

(int) v ≡ v′.

(×) v = ⟨v_{1},..., v_{n}⟩^{b} and v′ = ⟨v′_{1},..., v′_{n}⟩^{b} and ∀i∈[1..n].v_{i} ≃ v′_{i}.

(→) σ = σ_{1} →^{b} σ_{2} and for any value w of type σ_{1} then R_{e}(@^{b}v w) ≃ R_{e}(@^{b}v′ w).

(∀) σ = ∀^{b}t:k.σ_{1} and for any well-formed type σ_{2}:k then R_{e}(v[σ_{2}]^{b}) ≃ R_{e}(v′[σ_{2}]^{b}).

The correctness theorem can then be proved by induction over the reduction steps of P_{e}(e).

Theorem 3.3.3 (Correctness) If ·; · ⊢ e : σ and · ⊢ σ : d then R_{e}(P_{e}(e)) ≃ R_{e}(e).

3.4 Translation from SRC to TLC

The translation from SRC to TLC involves both staging and splitting, executed in an interleaved manner. Staging translates inlining annotations in the core language into bindingtime annotations. It also calls the splitting algorithm to divide each module term into a static part and a dynamic part. The static part is used to propagate inlining information and implement cross-module inlining. In the rest of this section, we first give a quick overview of our approach; we then show how to stage core terms and split module terms; finally, we give the main translation algorithm that links all the parts together.

3.4.1 A quick overview

The translation from SRC to TLC mostly consists of adding staging annotations. This is usually known as binding-time analysis and has been extensively studied in the partial evaluation community.

One desirable goal is to make sure that binding-time annotations do not hide opportunities for static evaluation. For example, let’s take the inlinable compose function o defined as follows:

o = λ^{i}f:τ_{1} → τ_{2}.λ^{i}g:τ_{2} → τ_{3}.λx:τ_{1}.g(fx)

When translating it, we probably do not want to assign it the following type:

o : (τ_{1} →^{d} τ_{2}) →^{s} (τ_{2} →^{d} τ_{3}) →^{s} (τ_{1} →^{d} τ_{3})

(i.e. a static function that composes two dynamic functions) since it would force us to make sure that all the functions passed to it are not inlinable, which mostly defeats the purpose of inlining it in the first place. Now clearly, if we mark it as:

o : (τ_{1} →^{s} τ_{2}) →^{s} (τ_{2} →^{s} τ_{3}) →^{s} (τ_{1} →^{d} τ_{3})

that will make it impossible to call it with a non-inlinable function. We could work around this problem by using polymorphism at the binding-time level [HM94, GSSS01], but we decided to keep our calculus simple. With monomorphic staging annotations, we have two options: code duplication to provide a poor man’s polymorphic binding-time, or coercions in the form of binding-time improvements [DMP96, Dan96].

A compiler needs to be very careful about duplicating code so we decided to use coercions instead, especially since they provide us with a lot of flexibility. More specifically, we can completely avoid the need for a full-blown binding-time analysis and use a simple one-pass translation instead, by optimistically marking s any place that might need to accommodate a static value and inserting coercions when needed, just like the unboxing coercions [Ler92, Sha97a]. It also allows us to simplify our types: all types are either (completely) dynamic or completely static. In effect, we are using polymorphism but encoded using Leroy’s coercions.

3.4.2 Staging the core

Staging could be done via any kind of binding-time analysis [Con93, BW95], but this would be too costly for our application, so instead of performing global code analysis to add the annotations, we add them in a single traversal of the code using only local information. In order to maximize the amount of static computation, we make extensive use of binding-time improvements [DMP96].

Binding-time improvements are usually some form of η-redexes that coerce an object between its static and dynamic representations. They improve the binding-time annotations by allowing values to be used statically at one place and dynamically at another and even to make this choice “dynamically” during specialization.

Staging is then simple: based on the inlining annotations, SRC terms can either be translated to completely static or dynamic entities (except for int, which is always dynamic). Because inlining annotations are not typechecked in SRC, the resulting TLC terms may use dynamic subterms in a static context or vice versa. We insert coercions whenever there is such a mismatch.

We define two type-translation functions |·|^{s} and |·|^{d} that turn any SRC type into either its fully static or its fully dynamic TLC equivalent, and two coercion functions ↓^{τ}: |τ|^{s} →^{s} |τ|^{d} and ↑^{τ} : |τ|^{d} →^{s} |τ|^{s}. Those coercions (and corresponding type translations) could simply be:

↓^{int}x = x
↓^{τ_{1}→τ_{2}}x = λ^{d}x_{1}:|τ_{1}|^{d}. ↓^{τ_{2}}(@^{s}x (↑^{τ_{1}}x_{1}))
⋯
↑^{int}x = x
↑^{τ_{1}→τ_{2}}x = λ^{s}x_{1}:|τ_{1}|^{s}. ↑^{τ_{2}}(@^{d}x (↓^{τ_{1}}x_{1}))
⋯

|int|^{d} = int
|τ_{1} → τ_{2}|^{d} = |τ_{1}|^{d} →^{d} |τ_{2}|^{d}
⋯
|int|^{s} = int
|τ_{1} → τ_{2}|^{s} = |τ_{1}|^{s} →^{s} |τ_{2}|^{s}
⋯

But this would run the risk of introducing unexpected code duplication.

Spurious copies A naive coercion of a static function to its dynamic equivalent tends to introduce static redexes which cause the function to be inlined unnecessarily at the place where it escapes. Consider the following piece of SRC code:

let

id = λ^{i}x:int.x
big = λf:int → int. ...big body...

in ⟨id, @big id⟩

A simple-minded staging scheme would turn it into:

let

id = λ^{s}x:int.x
big = λ^{d}f:int →^{d} int. ...big body...

in ⟨ ↓^{int→int}id, @^{d}big (↓^{int→int}id)⟩^{d}

where the coercions get expanded to:

let

id = λ^{s}x:int.x
big = λ^{d}f:int →^{d} int. ...big body...

in ⟨λ^{d}x:int.@^{s}id x, @^{d}big (λ^{d}x:int.@^{s}id x)⟩^{d}

Note that the two escaping uses of id have been turned now into η-redexes where id is called directly. Thus specialization will happily inline two copies of id even though

|int|^{d} = int
|τ_{1} → τ_{2}|^{d} = |τ_{1}|^{d} →^{d} |τ_{2}|^{d}
↓^{τ}  : |τ|^{s} →^{s} |τ|^{d}
↓^{int}x  = x
↓^{τ_{1}→τ_{2}}x = π_{2}^{s}x

|int|^{s} = int
|τ_{1} → τ_{2}|^{s} = |τ_{1}|^{s} →^{s} |τ_{2}|^{s} ×^{s} |τ_{1} → τ_{2}|^{d}
↑^{τ}  : |τ|^{d} →^{s} |τ|^{s}
↑^{int}x  = x
↑^{τ_{1}→τ_{2}}x = ⟨λ^{s}x_{1}:|τ_{1}|^{s}. ↑^{τ_{2}}(@^{d}x (↓^{τ_{1}}x_{1})), x⟩^{s}
| Δ; Γ; Σ | = Δ; Γ′
where Γ′ = {x:|Γ(x)|^{s} | x ∈ dom(Γ)} ∪ {z:|τ|^{b} | τ = Γ(z) and b = Σ(z)}

and (binding-time environment) Σ ::= · | Σ, z:b

Figure 3.10: Core type translations | · | and coercions ↓ and ↑.

no optimization will be enabled since both uses are really escaping. We do not want to introduce such wasteful code duplication.

In other words, we want to ensure that there can be only one non-inlined copy of any function, shared among all its escaping uses. To this end, we must arrange for ↓^{σ} not to introduce spurious redexes. We could simply introduce a special coerce primitive operation with an ad-hoc treatment in the partial-evaluator, but depending on the semantics chosen (e.g. binding-time erasure) it can end up leaking static code to run-time, introducing unwanted run-time penalties and it does not easily solve the problem of ensuring a unique dynamic copy of a function, even in the presence of cross-module inlining.

So we decided to choose a more sophisticated representation for the static translation of a function, where each static function is now represented as a pair of the real static function and the already-coerced dynamic function. Now ↓^{σ_{1}→σ_{2}} becomes π_{2}^{s} and the only real coercion happens once, making it clear that only one instance of the dynamic version will exist. The definition of our type translations | · |^{s} and | · |^{d} and coercions ↓^{τ} and ↑^{τ} for the core calculus is shown in Fig. 3.10. The previous example is now staged as follows:

let

id = ⟨λ^{s}x:int.x, λ^{d}x:int.x⟩^{s}
big = λ^{d}f:int →^{d} int. ...big body...

in ⟨ ↓^{int→int}id, @^{d}big (↓^{int→int}id)⟩^{d}

[figure: Δ; Γ; Σ ⊢ ⟦c:τ⟧ ⇒^{b} e]such that | Δ; Γ; Σ | ⊢ e : |τ|^{b}

Δ ⊢ Γ

Δ; Γ; Σ ⊢ ⟦n:int⟧ ⇒^{b} n

Δ ⊢ Γ Σ(z) = b Δ; Γ; Σ ⊢ ⟦z:Γ(τ)⟧ ⇒^{b} z
Δ; Γ ⊢ x : V(τ)

Δ; Γ; Σ ⊢ ⟦π_{v}x:τ⟧ ⇒^{s} x

Δ; Γz, τ_{1} :; Σz, s:⊢ ⟦c:τ_{2}⟧ ⇒^{s} e

Δ; Γ; Σ ⊢ ⟦λ^{i}z:τ_{1}.c:τ_{1} → τ_{2}⟧ ⇒^{s}

let x_{s} = λ^{s}z:|τ_{1}|^{s}.e

x_{d} = λ^{d}z:|τ_{1}|^{d}. ↓^{τ_{2}}(@^{s}x_{s} (↑^{τ_{1}}z))

in ⟨x_{s}, x_{d}⟩^{s}

Δ; Γz, τ_{1} :; Σz, d:⊢ ⟦c:τ_{2}⟧ ⇒^{d} e

Δ; Γ; Σ ⊢ ⟦λz:τ_{1}.c:τ_{1} → τ_{2}⟧ ⇒^{d} λ^{d}z:|τ_{1}|^{d}.e

Δ; Γ; Σ ⊢ ⟦c_{1} :τ_{1} → τ_{2}⟧ ⇒^{d} e_{1} Δ; Γ; Σ ⊢ ⟦c_{2} :τ_{1}⟧ ⇒^{d} e_{2}

Δ; Γ; Σ ⊢ ⟦c_{1}c_{2} :τ_{2}⟧ ⇒^{d} @^{d}e_{1} e_{2}

Δ; Γ; Σ ⊢ ⟦c_{1} :τ_{1} → τ_{2}⟧ ⇒^{s} e_{1} Δ; Γ; Σ ⊢ ⟦c_{2} :τ_{1}⟧ ⇒^{s} e_{2}

Δ; Γ; Σ ⊢ ⟦c_{1}c_{2} :τ_{2}⟧ ⇒^{s} @^{s}(π_{1}^{s}e_{1}) e_{2}

Δ; Γ; Σ ⊢ ⟦c:τ⟧ ⇒^{d} e

Δ; Γ; Σ ⊢ ⟦c:τ⟧ ⇒^{s}↑^{τ}e

Δ; Γ; Σ ⊢ ⟦c:τ⟧ ⇒^{s} e

Δ; Γ; Σ ⊢ ⟦c:τ⟧ ⇒^{d}↓^{τ}e

Figure 3.11: Core code translation.

Since the coercion ↓^{int→int} is now π_{2}^{s}, it just selects the dynamic version of id, with no code duplication.

Partial evaluators have long used such paired representation in their specializer for similar reasons [Asa99], although our case is slightly different in that the pairs are explicit in the program being specialized rather than used internally by the specializer.

This pairing approach can also be seen as a poor man’s polymorphic binding-time where we only allow the two extreme cases (all dynamic or all static). Minamide and Garrigue [MG98] used the same pairing approach when attempting to avoid the problem of accumulative coercion wrappers that appears when unboxing coercions are used to reconcile polymorphism and specialized data representation.

The staging algorithm is shown in Fig. 3.11. The judgment Δ; Γ; Σ ⊢ ⟦c: τ⟧ ⇒^{b} e says that a core SRC term c of type τ (under contexts Δ and Γ) is translated into a TLC term e. The environment Σ maps core variables to the binding-time of the corresponding variable in e. The b on the arrow indicates whether a static or a dynamic term e is expected.

Most rules come in two forms, depending on whether the context expects a dynamic or

[figure: Δ; Γ ⊢^{split} ⟦e⟧ ⟹^{σ} E_{e} ; e_{i}]where Δ; Γ ⊢ e : |σ|^{s}

Δ; Γ ⊢^{split} ⟦let x_{1} = e_{1} in let x_{2} = e_{2} in e_{3}⟧ ⟹^{σ} E_{e} ; e_{i}
Δ; Γ ⊢^{split} ⟦let x_{2} = let x_{1} = e_{1} in e_{2} in e_{3}⟧ ⟹^{σ} E_{e} ; e_{i}

(sp-asc)

x ∉ fv(e_{i}) ∨ Δ ⊢ σ_{1} : d

Δ; Γ ⊢ e_{1} : σ_{1} Δ; Γx, σ_{1} :⊢^{split} ⟦e_{2}⟧ ⟹^{σ} E_{e} ; e_{i}

Δ; Γ ⊢^{split} ⟦let x = e_{1} in e_{2}⟧ ⟹^{σ} (let x = e_{1} in E_{e}) ; e_{i}

(sp-share)

Δ; Γ ⊢ e_{1} : σ_{1} Δ; Γx, σ_{1} :⊢^{split} ⟦e_{2}⟧ ⟹^{σ} E_{e} ; e_{i}

Δ; Γ ⊢^{split} ⟦let x = e_{1} in e_{2}⟧ ⟹^{σ}

(let x = e_{1} in E_{e}) ; (let x = e_{1} in e_{i})

(sp-dup)

E_{e} = (let x_{fv} = • in ⟨ ↓^{σ}x, x_{fv}⟩^{d}) Δ; Γ ⊢^{split} ⟦x⟧ ⟹^{σ} E_{e} ; x (sp-var) Δ; Γ ⊢^{split} ⟦let x = e in x⟧ ⟹^{σ} E_{e} ; e_{i} Δ; Γ ⊢^{split} ⟦e⟧ ⟹^{σ} E_{e} ; e_{i} (sp-exp)

Figure 3.12: The λ-split algorithm.

static term. The dynamic case is trivial (it corresponds to the no-inlining case so we do not need to do anything) while the static case needs to build the static/dynamic pair (in the λ case) or to extract the static half of the pair before applying it (in the @ case).

3.4.3 Splitting

Module-level functions are typically used differently from core-level functions. They also do not have any inlining annotations thus deserve special treatment during the translation. As noted earlier, it is desirable to mark all the module code as static to “compile it away” or at least, to allow inlining information to flow freely through module boundaries. But that would imply that every single module-level function application gets its own copy of the body, which leads to unnecessary code duplication.

To overcome this difficulty, we use a form of partial inlining inspired from Blume and Appel’s λ-splitting [BA97] that splits each function into a static and a dynamic part. It rewrites a TLC expression e into a list of let bindings and copies every inlinable (i.e. static) binding from e into e_{i}, and puts the rest into the expression e_{e} (the e subscript stands for “expansive”) in such a way that the two can be combined to get back an expression equivalent to e with e ≃ let ⟨fv⟩ = e_{e} in e_{i} where fv is the list of free variables of e_{i}. Since e_{i} is small, it can be copied wherever e was originally used, while the main part of the code is kept separate in e_{e}.

Basically, e_{i} is just like e but where all the non-inlinable code has been taken out (and the variables that refer to it are thus free), whereas e_{e} is a complete copy of e except that it returns all those values that have been taken out of e_{i}, so it can be used to close over the free variables of e_{i}. Take for example the following expression e where lookup is inlinable but balance is not (note that the algorithm assumes that the return value of e is completely static):

e = let balance = λ^{d}⟨t, x⟩^{d}:tree^{d} ×^{d} elem^{d}…. lookup = λ^{s}⟨t, p⟩^{s}:tree^{s} ×^{s} elem^{s} →^{s} bool. let x = @^{s}(@^{s}find p) t in @^{d}balance ⟨ ↓^{tree}t, ↓^{elem}x⟩^{d} in lookup

This expression e will be split into a dynamic e_{e} and a static e_{i} where balance has been taken out since it is not inlinable. e_{i} will look like:

e_{i} = let lookup = λ^{s}⟨t, p⟩^{s}:tree^{s} ×^{s} elem^{s} →^{s} bool. let x = @^{s}(@^{s}find p) t in @^{d}balance ⟨ ↓^{tree}t, ↓^{elem}x⟩^{d} in lookup

The free variables of e_{i} are provided by e_{e} which carries all the old code and returns all the missing bindings for e_{i} to use. In this example, balance is the only free variable, so the result looks like:

e_{e} = let balance = λ^{d}⟨t, x⟩^{d}:tree^{d} ×^{d} elem^{d}…. lookup = λ^{s}⟨t, p⟩^{s}:tree^{s} ×^{s} elem^{s} →^{s} bool. let x = @^{s}(@^{s}find p) t in @^{d}balance ⟨ ↓^{tree}t, ↓^{elem}x⟩^{d} in ⟨balance⟩^{d}

And we can combine e_{e} and e_{i} back together with e ≃ let ⟨fv⟩^{d} = e_{e} in e_{i} where fv is the list of free variables of e_{i}. We could of course remove lookup from e_{e}, but we might need it for something else (as will be shown in the next section) and it is easier to take care of it in a separate dead code elimination pass.

Because e_{e} has to return all the free variables of e_{i}, which are not known until e_{i} is complete, we cannot conveniently build e_{e} directly as we build e_{i}. Instead we build an expression with a hole E_{e} such that e_{e} ≡ E_{e}[⟨fv⟩^{d}]:

E_{e} = let balance = … lookup = … in ⟨•⟩^{d}

Here a TLC term with a hole E is formally defined as follows:

E ::= let x = • in e | let x = e in E

E[e] then fills the hole in E by textually substituting e for • without avoiding name capture:

(let x = • in e)[e_{1}] ≡ (let x = e_{1} in e)
(let x = e in E)[e_{1}] ≡ (let x = e in E[e_{1}])

The splitting rules are shown in Fig. 3.12. The judgment Δ; Γ ⊢^{split} ⟦e⟧ ⟹^{σ} E_{e} ; e_{i} states that E_{e} and e_{i} are a valid split of e in contexts Δ and Γ assuming that e: |σ|^{s}. The rules only guarantee correctness of the split, but do not specify a deterministic algorithm. In practice, whenever several rules can apply, the splitting algorithm chooses the first rule shown that applies: sp-asc is preferred over sp-share which is preferred over sp-dup, while sp-exp is only applied when there is no other choice (i.e. when e is neither a let binding nor a mere variable). This ensures that we return the smallest e_{i}.

The way the rules work is as follows: sp-asc together with sp-exp turn e into a list of let bindings that ends by returning a variable; sp-share copies dynamic bindings to E_{e} but omits them from e_{i} while sp-dup copies static bindings to both E_{e} and e_{i}; finally sp-var replaces the terminating variable with a hole in E_{e}.

Splitting functions

When splitting a function f, we could apply the above algorithm to the body, and then combine the two results into two functions f_{e} and f_{i}:

f = λ^{s}x:|σ|^{s}.e ⟹ f_{e} = λ^{d}x_{d}:|σ|^{d}.let x =↑^{σ}x_{d} in e_{e} f_{i} = λ^{s}x:|σ|^{s}.let ⟨fv⟩^{d} = @^{d}f_{e} (↓^{σ}x) in e_{i}

From then on f_{i} can be used in place of f (assuming that f_{e} is in scope), so that e_{i} will be inlined without having to ever duplicate e_{e}.

As we have seen before when staging inlinable core functions, the static representation of a function is a pair of the dynamic and the static version of that function f = ⟨f_{d}, f_{s}⟩^{s}. A similar representation needs to be used for module-level functions. One would be tempted to just use f_{i} for f_{s} and f_{e} for f_{d}, but a bit more work is required. First, we cannot use f_{e} directly because it only returns the free variables of f_{i} instead of the expected return value of f_{d}, but we can simply coerce f_{i} (which has f_{e} as a free variable) to a dynamic value:

f_{s} = f_{i}  f_{d} = λ^{d}x_{d}:|σ_{1}|^{d}. ↓^{σ_{2}}(@^{s}f_{i} (↑^{σ_{1}}x_{d}))

The problem with this approach is that there might be some code duplication between e_{i} and e_{e}, so f_{d} might contain unnecessary copies of code already existing in f_{e}. To work around this, we slightly change the way splitting is done, so that e_{e} returns not only the free variables of e_{i} but also the original output of e (see the sp-var rule):

E_{e} = let balance = … lookup = … in ⟨ ↓^{σ}lookup, •⟩^{d}

Of course, we need to adjust f_{i} so as to select the second component of f_{e}’s result to bind to its free variables. On the other hand, f_{e} is unchanged:

f = λ^{s}x:|σ|^{s}.e ⟹ f_{e} = λ^{d}x_{d}:|σ|^{d}.let x =↑^{σ}x_{d} in e_{e} f_{i} = λ^{s}x:|σ|^{s}.let ⟨fv⟩^{d} = π_{2}^{d}(@^{d}f_{e} (↓^{σ}x)) in e_{i}

Since f_{e} now returns the original result in its first field, we can use it directly almost as is to build f_{d} and we can of course still use f_{i} as f_{s}:

f_{s} = f_{i} f_{d} = π_{1} ∘ f_{e} = λ^{d}x_{d}:|σ_{1}|^{d}.π_{1}^{d}(@^{d}f_{e} x_{d})

Properties

To define and show correctness of the splitting algorithm, we need an extended notion of equivalence that applies to expressions rather than just values:

e ≃ e′ if and only if for any E such that ·; · ⊢ E[e] : σ then ·; · ⊢ E[e′] : σ and R_{e}(E[e]) ≃ R_{e}(E[e′])

Splitting turns an expression e into a dynamic part E_{e} and a static part e_{i}. The following theorems state that combining E_{e} and e_{i} yields a well-typed term that is semantically equivalent to e.

Theorem 3.4.1 (Type preservation) if Δ; Γ ⊢ e : |σ|^{s} and Δ; Γ ⊢^{split} ⟦e⟧ ⟹^{σ} E_{e} ; e_{i} and fv = fv(e_{i}) − dom(Γ) then Δ; Γ ⊢ let ⟨fv⟩^{d} = π_{2}^{d}(E_{e}[⟨fv⟩^{d}]) in e_{i} : |σ|^{s}.

Theorem 3.4.2 (Correctness) if Δ; Γ ⊢ e : |σ|^{s} and Δ; Γ ⊢^{split} ⟦e⟧ ⟹^{σ} E_{e} ; e_{i} and fv = fv(e_{i}) − dom(Γ) then e ≃ let ⟨fv⟩^{d} = π_{2}^{d}(E_{e}[⟨fv⟩^{d}]) in e_{i}.

Both theorems can be proved via induction on the splitting derivation with the help of an invariant. For correctness, the invariant is:

For any term e′ and set of variables xs such that (fv(e′) − fv(E_{e}[e′]) ⊆ xs ⊆ fv(e′) then E_{e}[e′] ≃ let ⟨xs⟩^{d} = π_{2}^{d}(E_{e}[⟨xs⟩^{d}]) in e′

The invariant for type preservation is similar.

t_{co} = t_{d} →^{s} t_{s} ×^{s} t_{s} →^{s} t_{d}
|int|^{s} = int
|t|^{s} = t_{s}
|τ_{1} → τ_{2}|^{s} = |τ_{1}|^{s} →^{s} |τ_{2}|^{s} ×^{s} |τ_{1} → τ_{2}|^{d}
|V(τ)|^{s} = |τ|^{s}
|σ_{1} × ^{...} × σ_{n}|^{s} = |σ_{1}|^{s} ×^{s} ^{...} × |σ_{n}|^{s} ×^{s} |σ_{1} × ^{...} × σ_{n}|^{d}
|σ_{1} → σ_{2}|^{s} = |σ_{1}|^{s} →^{s} |σ_{2}|^{s} ×^{s} |σ_{1} → σ_{2}|^{d}
|∀t.σ|^{s} = ∀^{s}t_{s}:s.∀^{s}t_{d}:d.t_{co} →^{s} |σ|^{s} ×^{s} |∀t.σ|^{d}

|int|^{d} = int
|t|^{d} = t_{d}
|τ_{1} → τ_{2}|^{d} = |τ_{1}|^{d} →^{d} |τ_{2}|^{d}
|V(τ)|^{d} = |τ|^{d}
|σ_{1} × ^{...} × σ_{n}|^{d} = |σ_{1}|^{d} ×^{d} ^{...} × |σ_{n}|^{d}
|σ_{1} → σ_{2}|^{d} = |σ_{1}|^{d} →^{d} |σ_{2}|^{d}
|∀t.σ|^{d} = ∀^{d}t:d.|σ|^{d}
| Δ; Γ; Σ | = Δ′; Γ′
where Δ′={t_{s}:s , t_{d}:d | t ∈ dom(Δ)}
and Γ′ ={x:|Γ(x)|^{s} | x ∈ dom(Γ)} ∪ {x_{t}:t_{co} | t ∈ dom(Δ)}
∪ {z:|τ|^{b} | τ = Γ(z) and b = Σ(z)}

Figure 3.13: Type and environment translation.

The splitting algorithm also satisfies the following property:

Theorem 3.4.3 (Static closure) if Δ; Γ ⊢ e : |σ|^{s} and Δ; Γ ⊢^{split} ⟦e⟧ ⟹^{σ} E_{e} ; e_{i} then all free variables in e_{i} are either bound in Γ or they have dynamic type (in the context of E_{e}).

We prove this property by inspection of the rules: if a variable is free in e_{i} but not in Γ it can only be because the sp-share rule was used, but that rule only applies to dynamic variables or variables which are not free in e_{i}.

Static closure implies that e_{i} contains all the inlinable sub-terms in e so splitting does not hide any inlining opportunities. In other words, when doing partial evaluation of a term containing e, we can substitute e_{i} for e without preventing any reduction (except reductions internal to the terms omitted in e_{i}, obviously). This in turn implies that compilation-unit boundaries have no influence on whether or not a core function gets inlined at a particular call site. We call it the completeness property.

3.4.4 The main algorithm

The main algorithm is the translation of the module language, which works similarly to (and uses) the core translation presented earlier, but is interleaved with the splitting algorithm.

[figure] ↓^{σ}: |σ|^{s} →^{s} |σ|^{d} and ↑^{σ}: |σ|^{d} →^{s} |σ|^{s}
↓^{int}x  = x
↓^{t}x  = @^{s}(π_{2}^{s}x_{t}) x
↓^{τ_{1}→τ_{2}}x = π_{2}^{s}x
↓^{V(τ)}x  = ↓^{τ}x
↓^{σ_{1}×^{...}×σ_{n}}x = π_{2}^{s}x
↓^{σ_{1}→σ_{2}}x = π_{2}^{s}x
↓^{∀t.σ}x  = π_{2}^{s}x

↑^{int}x  = x
↑^{t}x  = @^{s}(π_{1}^{s}x_{t}) x
↑^{τ_{1}→τ_{2}}x = ⟨λ^{s}x_{1}:|τ_{1}|^{s}. ↑^{τ_{2}}(@^{d}x (↓^{τ_{1}}x_{1})), x⟩^{s}
↑^{V(τ)}x  = ↑^{τ}x
↑^{σ_{1}×^{...}×σ_{n}}x = ⟨⟨ ↑^{σ_{1}}(π_{1}^{d}x),..., ↑^{σ_{n}}(π_{n}^{d}x)⟩^{s}, x⟩^{s}
↑^{σ_{1}→σ_{2}}x = ⟨λ^{s}x_{1}:|σ_{1}|^{s}. ↑^{σ_{2}}(@^{d}x (↓^{σ_{1}}x_{1})), x⟩^{s}
↑^{∀t.σ}x  = ⟨Λ^{s}t_{s}:s.Λ^{s}t_{d}:d.λ^{s}x_{t}:t_{co}. ↑^{σ}(x[t_{d}]^{d}), x⟩^{s}

Figure 3.14: Binding-time coercions.

It also relies on the use of pairs that keep both a dynamic and a static version of every module-level value to avoid unnecessary code duplication.

Figures 3.13 and 3.14 extend the type translations | · |^{s} and | · |^{d} and the coercions ↓ and ↑ to the module calculus. The main change is the case for the type abstraction which we will explain later.

The full staging algorithm is shown in Fig. 3.15. The judgment Δ; Γ ⊢ ⟦m : σ⟧ ⇒^{m} e means that under the environments Δ and Γ, the SRC module m of type σ is translated into the TLC term e. Most rules are straightforward. The translation of expressions ι_{v}(c) is delegated to the core translation.

The case for module-level function and type abstraction are most interesting. The translation of a module-level function λx:σ.m begins by recursively translating the body m, and then splitting it. This is done with the judgment Δ; Γ ⊢ ⟦m: σ⟧ ⟹ e_{e} ; E_{i}. We then build a pair f = ⟨f_{d}, f_{s}⟩^{s} as described in the previous section. The translation of a type abstraction Λt.m follows the same pattern, except for a subtle complication introduced by typing problems discussed below.

Since all module-level code is considered static, it is tempting to think that we do not need pairing at all and can simply represent module entities with the static counterpart. But the e_{e} component obtained from λ-split is dynamic and we thus need coercions to interact with it: both the sp-var rule of λ-split (see Fig. 3.12) and the construction of f_{i} out of e_{i} (see Sec. 3.4.3) introduce coercions. And since modules tend to be larger than core

[figure: Δ; Γ ⊢ ⟦m:σ⟧ ⇒^{m} e]such that | Δ; Γ; · | ⊢ e : |σ|^{s}

Δ ⊢ Γ Δ; Γ ⊢ ⟦x:Γ(x)⟧ ⇒^{m} x Δ; Γ; · ⊢ ⟦c:τ⟧ ⇒^{s} e

Δ; Γ ⊢ ⟦ι_{v}(c):V(τ)⟧ ⇒^{m} e

Δ; Γ ⊢ x_{i} : σ_{i} (1≤i≤n) σ = σ_{1} × ^{...} × σ_{n}

Δ; Γ ⊢ ⟦⟨x_{1},..., x_{n}⟩:σ⟧ ⇒^{m} let \begin{matrix}x_{s} = & ⟨x_{1},..., x_{n}⟩^{s} \\ x_{d} = & ⟨ ↓^{σ_{1}}x_{1},..., ↓^{σ_{n}}x_{n}⟩^{d} \end{matrix} in ⟨x_{s}, x_{d}⟩^{s}

Δ; Γ ⊢ x : σ_{1} × ^{...} × σ_{n} 1≤i≤n Δ; Γ ⊢ ⟦π_{i}x:σ_{i}⟧ ⇒^{m} π_{i}^{s}(π_{1}^{s}x) Δ; Γ ⊢ x_{1} : σ_{2} → σ_{1} Δ; Γ ⊢ x_{2} : σ_{2}

Δ; Γ ⊢ ⟦@x_{1} x_{2} :σ_{2}⟧ ⇒^{m} @^{s}(π_{1}^{s}x_{1}) x_{2}

Δ; Γx, σ_{1} :⊢ ⟦m:σ_{2}⟧ ⟹ e_{e} ; E_{i}

Δ; Γ ⊢ ⟦λx:σ_{1}.m:σ_{1} → σ_{2}⟧ ⇒^{m}

let

x_{e} = λ^{d}x_{d}:|σ_{1}|^{d}. let x =↑^{σ_{1}}x_{d} in e_{e}
x_{i} = λ^{s}x:|σ_{1}|^{s}. E_{i}[π_{2}^{d}(@^{d}x_{e} (↓^{σ_{1}}x))]

in ⟨x_{i}, λ^{d}x_{d}:|σ_{1}|^{d}.π_{1}^{d}(@^{d}x_{e} x_{d})⟩^{s} Δt,:; Γ ⊢ ⟦m:σ⟧ ⟹ e_{e} ; E_{i}

Δ; Γ ⊢ ⟦Λt.m:∀t.σ⟧ ⇒^{m}

let x_{e} = Λ^{d}t:d. {t/t_{d}, t/t_{s}, ⟨id_{t}^{s}, id_{t}^{s}⟩^{s}/x_{t}}e_{e}

x_{i} = Λ^{s}t_{s}:s.Λ^{s}t_{d}:d.λ^{s}x_{t}:t_{co}. E_{i}[π_{2}^{d}(x_{e}[t_{d}]^{d})]

in ⟨x_{i}, Λ^{d}t:d.π_{1}^{d}(x_{e}[t]^{d})⟩^{s}

Δ; Γ ⊢ x : ∀t.σ

Δ; Γ ⊢ ⟦x[τ] :{τ/t}σ⟧ ⇒^{m} @^{s}(π_{1}^{s}x)[|τ|^{s}]^{s}[|τ|^{d}]^{s} ⟨ ↓^{τ}, ↑^{τ}⟩

Δ; Γ ⊢ ⟦m_{1} :σ_{1}⟧ ⇒^{m} e_{1} Δ; Γx, σ_{1} :⊢ ⟦m_{2} :σ_{2}⟧ ⇒^{m} e_{2}

Δ; Γ ⊢ ⟦let x = m_{1} in m_{2} :σ_{2}⟧ ⇒^{m} let x = e_{1} in e_{2}

[figure: Δ; Γ ⊢ ⟦m:σ⟧ ⟹ e_{e} ; E_{i}]

such that | Δ; Γ; · | ⊢ E_{i}[π_{2}^{d}e_{e}] : |σ|^{s}

Δ; Γ ⊢ ⟦m:σ⟧ ⇒^{m} e | Δ; Γ; · | ⊢^{split} ⟦e⟧ ⟹^{σ} E_{e} ; e_{i}

fv = fv(e_{i}) − dom(Γ) E_{i} = (let ⟨fv⟩^{d} = • in e_{i})

Δ; Γ ⊢ ⟦m:σ⟧ ⟹ E_{e}[⟨fv⟩^{d}] ; E_{i}

Figure 3.15: Module code translation. id_{t}^{b} is a shorthand for λ^{b}x:t.x. functions, it is even more important to avoid spurious copies.

Type abstractions As mentioned above, type abstractions introduce some complications. The problem appears when we try to define coercion functions. The naive approach would look like:

↓^{∀t.σ}x=Λ^{d}t:d. ↓^{σ}(x[t]^{s})
↑^{∀t.σ}x=Λ^{s}t:s. ↑^{σ}(x[t]^{d})

but this is not type correct, since t in the second rule can be static and hence cannot be passed to the dynamic x. Obviously, we need here the same kind of (contra-variant) argument coercion as we use on functions, but our language does not provide us with any way to create a ↓ operator to apply to types.

Furthermore, the two inner coercions ↓^{σ} and ↑^{σ} are not very well defined since σ can have a free variable t. This begs the question: what should ↑^{t} do ?

There are several ways to solve these two problems:

• Give up on static type arguments and force any type-variable to be dynamic. This restriction is fairly minor in practice. It only manifests itself when a function is manipulated as data by polymorphic code, such as when a function is passed to the identity function id: the function returned by @id f cannot be inlined even if f is.

• Extend our language with a more powerful type-system that allows intensional typeanalysis [HM95]. This seems possible, but would complicate the type-system considerably and potentially the staging and the coercions as well.

• Use a dictionary-passing approach [WB89]: instead of trying to coerce our static t into a dynamic t, we can simply always provide both versions t_{s} and t_{d} along with both ↓^{t} and ↑^{t} so that the coercions are constructed at the type application site, where t is statically known.

The first solution is simple and effective, but we opted for the third alternative because it has fewer limitations. The static version of Λt.m (before pairing with its dynamic counterpart) looks like:

Λ^{s}t_{s}:s.Λ^{s}t_{d}:d.λ^{s}x_{t}:t_{co}.e

This means that for every t in the SRC Δ environment, we now have two corresponding type variables t_{s} and t_{d} plus one value variable x_{t} which holds the two coercion functions ↓^{t} and ↑^{t} As can be seen in Fig. 3.13 (which refines Fig. 3.10) where t_{co} is also defined. This notation is used for convenience in all the figures.

Such an encoding might look convoluted and cumbersome, but type abstractions only represent a small fraction of the total code size and the run-time code size is unaffected, so it is a small price to pay in exchange for the ability to inline code that had to pass through a function like id.

Theorem 3.4.4 (Type preservation) If Δ; Γ ⊢ m : σ and Δ; Γ ⊢ ⟦m:σ⟧ ⟹ e_{e} ; E_{i} then | Δ; Γ; · | ⊢ E_{i}[π_{2}^{d}e_{e}] : |σ|^{s}.

Together with the residualization theorem 3.3.1, this means that after specialization all the inlinable code has been inlined away.

3.5 Handling the top level

The above presentation only explains how to translate each SRC compilation unit into its TLC counterpart. This section describes in detail how to handle top-level issues to link multiple compilation units together.

Handling of compilation units is not difficult, but is worth looking at not only to get a better idea of how the code flows through the compiler, but also because the treatment of side-effects depends on the specifics of the evaluation of each compilation unit (see Sec. 3.6.1).

As can be seen in Fig. 3.1, we apply λ-split twice. This derives from the need to handle the top-level of the compilation unit in a special way where splitting internal module functions should be done early, while splitting the top-level should be done late. Here is a slightly more detailed diagram:

\begin{aligned}sum & 1..n \\ & ↘ \end{aligned}  \begin{matrix}sum \\ ↗ \end{matrix}
PRG_{S} ⟶^{stage+split} PRG_{1} ⟶^{⊕} PRG_{2} ⟶^{P_{e}} PRG_{3} ⟶^{split} PRG_{4}

Instead of spreading the split into two parts, we could of course do it once and for all at the very beginning, but then we would lose the opportunity to move into the export summary copies of wrapper functions (used e.g. for uncurrying, unboxing or flattening) introduced by the intermediate optimization phases.

Doing the split in two steps also forces us to apply the split to TLC terms (it would be silly to have two splitting algorithms). This also motivates our choice to interleave the staging and splitting since the splitting algorithm needs to know which functions are modules and which are not because splitting core functions is often detrimental to performance.

The top-level also gets a special treatment because of separate compilation. A compilation-unit can contain free variables, which are essentially the imports of the unit. Instead of considering such an open term, we close it by turning it into a function from its imports to its exports. More specifically, a compilation-unit in SRC will look like:

PRG_{S} = λ⟨imp_{1},..., imp_{n}⟩:σ_{1} × ^{...} × σ_{n}.m

The translation to TLC assumes that the function is static as well as the imports (these will be import summaries, which are by essence inlinable, after all) and simply translates the body using:

·; {imp_{i}:|σ_{i}|^{s} | 1 ≤ i ≤ n} ⊢ ⟦m:σ⟧ ⇒^{m} e

This recursively splits each and every internal module-level function (the recursion is done by the staging part of the translation which calls λ-split when needed, see the rules for λ and Λ in Fig. 3.15), but leaves the top-level function alone. These internal splits are necessary to allow inlining across module boundaries but still within a compilation unit.

The program now looks like:

PRG_{1} = λ^{s}⟨imp_{1},..., imp_{n}⟩^{s}:|σ_{1}|^{s} ×^{s} ^{...} × |σ_{n}|^{s}.e

The next step is to bring in copies of the import summaries. Every imp_{i} has a corresponding summary sum_{i} generated when that import was compiled. Summaries are the e_{i} half of a split and thus contain free dynamic variables. So we replace each imp_{i} argument with a copy of sum_{i}, and add the corresponding new free variables imp_{ij} as new arguments:

PRG_{2} = λ^{s}⟨imp_{11},..., imp_{nk}⟩^{d}:σ_{11} ×^{d} ^{...} × σ_{nk}. let imp_{1} = sum_{1} ; ⋯; imp_{n} = sum_{n} in e

After that comes the actual partial-evaluation and optimization which ends with a term PRG_{3} very much like PRG_{2} but with an optimized body e_{o} exempt of any static redex:

PRG_{3} = P_{e}(PRG_{2}) = λ^{s}⟨imp_{11},..., imp_{nk}⟩^{d}:σ_{11} ×^{d} ^{...} × σ_{nk}. e_{o}

We then pass it to the second λ-split, along with the SRC σ output type that we remembered from the staging phase:

Δ; Γ ⊢^{split} ⟦e_{o}⟧ ⟹^{σ} E_{e} ; e_{i}

This split gives us a residual program and an export summary sum that will be used as a sum_{i} next time around:

sum = λ^{s}⟨fv⟩^{d}:⟨σ_{fv}⟩^{d}.e_{i}  where fv = fv(e_{i})
PRG_{4} = λ^{d}⟨imp_{11},..., imp_{nk}⟩^{d}:σ_{11} ×^{d} ^{...} × σ_{nk}.E_{e}[⟨fv⟩^{d}]

The export summary sum will be stashed somewhere to be used when a compilation unit wants to import it. As for PRG_{4}, it continues through the remaining compilation stages down to machine code.

When the program is run, all the compilation units need to be instantiated in the proper order. Once all the imports imp_{ij} of our unit have been built, PRG_{4} is run as follows:

⟨exp, ⟨fv⟩^{d}⟩^{d} = R_{e}(@^{d}PRG_{4} ⟨imp_{11},..., imp_{nk}⟩^{d})

Here exp is the original exports of this compilation unit. They will be ignored for all but the main compilation unit (unless one of the dependent units was compiled without crossmodule inlining, in which case exp will be used by that unit). ⟨fv⟩^{d} is the set of exports generated by the λ-split and needed for all the compilation units that depend on the current unit and hence imported sum. When running those dependent units, the current ⟨fv⟩^{d} will then appear as the arguments imp_{i1} … imp_{ik}.

Note that PRG_{4} will only be run once and for all whereas sum will be evaluated as many times as it is imported by dependent compilation units. Also, PRG_{4} is not completely dynamic since the coercion ↓^{σ}x of the sp-var rule (in Fig. 3.12) introduces static redexes; we have to perform another round of partial evaluation on PRG_{4} before feeding it to the backend code generator.

3.6 Extensions

In order to model real world inliners faithfully, our translation still needs various additions which we have not explored in depth yet. We present some here along with other potential extensions.

3.6.1 Side effects

Introducing side-effects is mostly straightforward, with just one exception: The sp-dup rule in Fig. 3.12 cannot be applied to non-pure terms since it would duplicate their effects. But reverting to the sp-share rule (also in Fig. 3.12) for those side-effecting terms is not an

(type) σ ::= ... | ∃^{b}t:k.σ
(term) e ::= ... | open^{b} v as ⟨t, x⟩ in e
(value) v ::= ... | ⟨t=σ:κ, v:σ⟩^{b}
|σ_{1} → σ_{2}|^{s} = ∃^{s}t:d.|σ_{1}|^{s} →^{s} t →^{s} |σ_{2}|^{s} ×^{s} |σ_{1}|^{d} →^{d} (|σ_{2}|^{d} ×^{d} t)
↓^{σ_{1}→σ_{2}}x = λ^{d}x_{1}:|σ_{1}|^{d}.open^{s} x as ⟨t, x⟩ in π_{1}^{d}(@^{d}(π_{2}^{s}x) x_{1})
↑^{σ_{1}→σ_{2}}x = pack^{s}(t = ⟨⟩^{d}:d, let x_{s} = λ^{s}x_{1}:|σ_{1}|^{s}.λ^{s}x_{2}:t. ↑^{σ_{2}}(@^{d}x x_{1}) x_{d} = λ^{d}x_{3}:|σ_{1}|^{d}.⟨@^{d}x x_{3}, ⟨⟩^{d}⟩^{d} in ⟨x_{s}, x_{d}⟩^{s} )

Δ; Γ ⊢ x_{1} : σ_{2} → σ_{1} Δ; Γ ⊢ x_{2} : σ_{2}

Δ; Γ ⊢ ⟦@x_{1} x_{2} :σ_{2}⟧ ⇒^{m}

open^{s} x_{1} as ⟨t, x⟩ in

let x_{e} = @^{d}(π_{2}^{s}x) (↓^{σ_{1}}x_{2})

in @^{s}(@^{s}(π_{1}^{s}x) x_{2}) (π_{2}^{d}x_{e})

Δ; Γx, σ_{1} :⊢ ⟦m:σ_{2}⟧ ⟹ e_{e} ; E_{i}

Δ; Γ ⊢ ⟦λx:σ_{1}.m:σ_{1} → σ_{2}⟧ ⇒^{m}

pack^{s}(t = σ_{fv}:d, let x_{e} = λ^{d}x_{d}:|σ_{1}|^{d}. let x =↑^{σ_{1}}x_{d} in e_{e} x_{i} = λ^{s}x:|σ_{1}|^{s}.λ^{s}y:t. E_{i}[y] in ⟨x_{i}, x_{e}⟩^{s} )

Figure 3.16: New rules using existential types. The type, kind, and evaluation semantics should be extended correspondingly.

Furthermore, the last rule needs the type σ_{fv} of the free variables of e_{i} which can easily be propagated during splitting.

option either because we would then lose the completeness property that we are looking for. An alternative is to use the following sp-move rule that moves the binding to e_{i} instead of merely copying it:

\frac{Δ; Γ ⊢ e_{1} : σ_{1} Δ; Γx, σ_{1} :⊢^{split} ⟦e_{2}⟧ ⟹^{σ} E_{e} ; e_{i}}{Δ; Γ ⊢^{split} ⟦let x = e_{1} in e_{2}⟧ ⟹^{σ} let x_{1} = λ^{d}x:σ_{1}.E_{e}[⟨fv⟩^{d}] in • ; let x = e_{1} in let ⟨fv⟩^{d} = @^{d}x_{1} (↓^{σ_{1}}x) in e_{i}}

As presented, this rule is not quite correct because the coercion ↓^{σ_{1}} assumes σ_{1} is an SRC type but σ_{1} is really a TLC type. This can be easily resolved by passing all the SRC types during the translation. Alternatively, we could represent |int|^{s} as int×^{s}int, then the coercion ↓ simply becomes π_{2}^{s} for all types.

A potential issue is that, as can be seen in Sec. 3.5, the top-level E_{e} is evaluated once and for all in a global environment, while e_{i} will be evaluated each time it is imported into a client unit. This means that the top-level e_{i} must be free of side-effects. Luckily, we can show that this problem does not appear: the compilation unit does not contain any static free variables or static redexes; so the only static code to split into e_{i} is composed exclusively of values, which have no side effects.

This sp-move rule could also be used for other purposes, such as specializing a binding in the context of the client, as was suggested briefly in Blume and Appel’s paper (they did not have such a rule).

Another approach altogether to the handling of side-effects is to notice that since e_{i} has to be pure, we can turn @^{s}x_{1} x_{2} into @^{d}(@^{s}x_{i} x_{2}) (π_{2}^{d}(@^{d}x_{e} (↓x_{2}))) and then split out the remaining static application which is known to be pure. We do not even need to change the splitting algorithm itself, but just two rules in Fig. 3.15.

The key is that we can do this rewrite even if we do not know x_{1}. We simply need to represent x_{1} as a pair ⟨x_{i}, x_{e}⟩^{s}. But of course, this is already the case for other reasons, so the changes are very minor.

Of course, there is a catch: the type of the free variables of e_{i} suddenly leak into the type of |σ_{1} → σ_{2}|^{s} which becomes an existential type.

Figure 3.16 shows what the rules would look like. Some of the work is now shifted from the function definition to the function application, but overall, the complexity of terms is not seriously impacted. Apart from the introduction of existential types, this translation variant also requires an impredicative calculus. TLC was already impredicative, so no changes were required there.

3.6.2 Recursion

The TLC calculus lacks fixpoint. Adding recursive functions does not pose any conceptual problem, except for the risk of compilation not terminating and the fact that some of our proofs rely on the fact that the static computation is strongly normalizing. There are several reasonable solutions to these problems either from the inlining community or from the partial evaluation community. The most trivial solution is to allow fixpoint on dynamic terms only, which amounts to disallowing inlining of recursive functions, but since it can be important to allow inlining even in the presence of recursion, one can also do a little bit of analysis to find a conservative estimate of whether or not a risk of infinite recursion is present [PM99].

Recursion on types is more challenging since recursive data-structures cannot (or should not) be coerced. In our case, however, this restriction only applies to coercions from d to s, so we can always work around the problematic cases by forcing recursive data-structures to be dynamic.

3.6.3 Optimizations as staged computation

With a full λ-calculus available at compile-time, we can now provide facilities similar to macros, or rather to Lisp’s compiler macros. For example, a compiler macro for multiplication could test its arguments at compile time and replace the multiplication with some other operations depending on whether or not one of the value is statically known and what value it takes. Using such a facility we could move some of the optimizations built into the compiler into a simple library, making them easily extensible.

A more realistic use in the short term is to encode the predicate for conditional inlining directly into the language. The current inliner allows inlining hints more subtle than the ones present in SRC. They can express a set of conditions that should hold at the call site in order for the call to be inlined. For example, map will only be inlined if it is applied to a known function.

We could now strip out those ad-hoc annotations and simply write map as a compiletime function that intensionally analyzes its arguments and either returns a copy of its body if the function argument is a λ-expression or returns just a call to the common version if the argument is a variable (i.e. an escaping function).

3.6.4 Staging refinement

Partial evaluation as well as other optimizations will sufficiently change the shape of the code to justify or even require refining the binding-time annotations. This can happen because a function has been optimized down to just a handful of statements, or because it has been split into a wrapper and a main body or any other reason.

Turning a dynamic function into a static one is not very difficult to implement, but more work needs to be done to express it cleanly within our framework.

It seems to require among other things the ability to optimize away pairs of coercions that cancel each other out such as not only ↓^{σ}↑^{σ}x (which is trivially done by the partial evaluator) but also ↑^{σ}↓^{σ}x which appears to involve evaluation of dynamic code at compiletime.

3.6.5 Link-time optimizations

Another extension is to add multiple levels so that we can express compile-time execution, link-time execution, run-time code generation and more.

This will require extending a calculus such as λ^{◯} [Dav96] with at least some form of polymorphism, but should not pose any real problem, except that the kind of tricks we used to work around the lack of simple coercion for type abstraction might need to be generalized to n-levels. If n is unbounded, it might not be possible and even if it is bounded, it might be impractical.

Also, the use of pairs of fully-static and fully-dynamic representations of the same original expressions would not generalize to n-levels easily, but could still be kept for the benefit of the compile stage.

3.6.6 Implementation

As mentioned in the introduction, this paper was motivated by the need to better understand the behavior of our inliner in SML/NJ. Since our implementation handles the complete SML language in a production compiler, it has to deal with all the issues mentioned above. Here are the most important differences between the model presented in this paper and the actual code:

• For historical reasons, our intermediate language is predicative, which prevents us from using existential packages to solve the problem of side effects. Instead we simply revert to using the sp-share rule and lose the completeness property.

• Our language allows recursion both for dynamic and for static functions. Termination of P_{e}(e) is ensured by a simple conservative loop detection.

• The dynamic and static pairs we use to avoid spurious code duplication are represented in an ad-hoc way that eliminates the redundancy. This ad-hoc representation looked like a good idea at the time, but made it unnecessarily painful to add the refinement described in Sec. 3.4.3.

3.7 Related work

Functional-language compilers such as O’Caml [Ler95], SML/NJ [App91], GHC [PM99], and TIL [Tar96], all spend great efforts to provide better support to inlining. Although none of them models inlining as staged computation, the heuristics for detecting what functions should be inlined are still useful in our framework. In fact, our FLINT optimizer [MBS99] inherits most of the heuristics used in the original SML/NJ compiler.

Control-flow analysis (CFA) [Shi91, Ash97] is an alternative to λ-splitting to propagate inlining information across functions and functors. It tries to find, for example via abstract interpretation, the set of functions possibly invoked at each call site in the program. It offers the advantage of requiring less code duplication and may expose more opportunities for inlining inside a compilation unit. For example, in a code such as:

let f x y = ..y x.. and g x = ... in ⟨f 1 g, f 2 g⟩

CFA can inline the function g into f without inlining f whereas our inliner will only reach the same result if it can first inline the two calls to f. On the other hand, in a code such as f g x where f is a functor that ends up returning its argument unchanged, our inliner will be able to replace the code with g x, no matter how f is defined, whereas in the case of CFA, if the definition of f is sufficiently complex, a costly polyvariant analysis is needed to discover that the code can be replaced with g x.

Partial evaluation is a very active research area. Jones et al [JGS93] gives a good summary about some of the earlier results. Danvy’s paper [Dan96] on type-directed partial evaluation inspired us to look into sophisticated forms of binding-time coercions.

Tempo [CN96, MCB99] is a C compiler that makes extensive use of partial-evaluation technologies. Its main emphasis is however on efficient runtime code generation. Sperber and Thiemann [ST96, ST97] worked on combining compilation with partial evaluation, however they were not concerned with modeling the inlining optimization as done in a production compiler.

Nielson and Nielson [NN92] gave an introduction to a two-level λ-calculus. Davies and Pfenning [DP96] proposed to use modal logic to express staged computation. Moggi [Mog97] pointed out that both of these calculi are subtly different from the two-level calculus used in partial evaluation [JGS93]. Taha et al [Tah99, TS97, MTBS99] showed how to combine these different calculi into a single framework.

Our TLC calculus is an extension of Moggi’s two-level λ2sd calculus [Mog97] with the System-F-style polymorphism [Gir72, Rey74]. Davies [Dav96] used the temporal logic to model an n-level calculus which naturally extends λ2sd.

Foster et al. [FFA99] proposed to use qualified types to model source-level program directives. Their framework can be applied to binding time annotations but these annotations would have to become parts of the type specifications. Our inlining annotations on the other hand do not change the source-level type specifications.

Blume and Appel [BA97] suggested λ-splitting to support cross module inlining. Their algorithm is based on a weakly-typed λ-calculus and provides a convenient cross-module inlining algorithm. Our work extends theirs by porting their algorithm to a much more powerful language and formalizing it. By using the two-level λ-calculus we can express some of the inliner’s behavior in the types.

O’Caml [Ler95] collects the small inlinable functions of a module into its approximation and then reads in this extra info (if available) when compiling a client module. It works very well across modules and can even inline functions from within a functor to the client of the functor, but is unable to inline the argument of a functor. For example, passing a module through a trivial “adaptor” functor (which massages a module to adapt it to some other signature, e.g.) will lose the approximation, preventing inlining.

By encoding the equivalent of approximations directly into types, Shao [Sha98] presents an alternative approach which allows the full inlining information to be completely propagated across functor applications by propagating it along with the types. But this comes at the cost of a further complication of the module elaboration. Another problem is that some of the functions we might want to inline (such as uncurry wrappers) do not yet exist at the time of module elaboration.

Recently, Ganz et al [GST01] presented an expressive, typed language that supports generative macros. The language, MacroML, is defined by an interpretation into MetaML [TS97]. This is similar to our approach because inlinable functions can also be viewed as macros; the translation from MacroML to MetaML resembles our translation from the source calculus SRC to the target calculus TLC. There are, however, several major differences. First, in MacroML, macros and functions are different language constructs; macros never escape so they can be unconditionally marked as “static” and no coercions or polymorphic bindingtime annotations are ever needed; in our SRC calculus, however, functions that are marked as inlinable are still treated as regular functions so they can escape in any way they like. Second, it is unclear how MacroML can be extended to support ML-style modules; MacroML assigns different types to macros and functions so exporting macros would require adding new forms of specifications into ML signatures; in our SRC language, however, a function is always assigned the same type whether it is marked as inlinable or not, so we can just reuse the existing ML module language. More annoyingly, macro definitions might need to be included in module signatures, to allow separate compilation. Third, unlike MacroML, we presented various techniques to control code duplication—this is crucial for cross-module inlining since naively expanding every functor in ML would certainly cause code explosion. On the other hand MacroML simply shifts the burden onto the programmer, which has also the advantage of allowing recursive macro definitions, simply declaring infinite macro expansion to be the mistake of the programmer.

3.8 Conclusions and future work

Expressing inlining in terms of a staged computation allows us to better formalize the behavior of the inliner and provide strong guarantees of what gets inlined where.

We have shown how this can be done in the context of a realistic two-level polymorphic language and how it interacts with cross-module inlining. The formalism led us to a clean design in which we can easily show that code is only and always duplicated when useful.

The present design eliminates run-time penalties usually imposed by the powerful abstraction mechanism offered by parameterized modules, enabling a more natural programming style. More importantly, our algorithm provides such flexibility while still maintaining separate compilation.

An interesting question is whether or not using monomorphic staging annotations was a good choice. It seems that polymorphism could allow us to do away with the coercions, although it would at least require the use of continuation passing style in order to maintain precision of annotations.

Principled scavenging

The correctness of most type-safe systems relies critically on the correctness of an underlying garbage collector (GC). This also holds for Proof-Carrying Code (PCC) [Nec97] and Typed Assembly Languages (TAL) [MWCG98]—both of which aim to minimize the trusted computing base by directly certifying the actual machine code. Unfortunately, these systems cannot get rid of the dependency on a trusted garbage collector. Indeed, constructing a verifiably type-safe garbage collector is widely considered as one of the major open problems in the area of certifying compilation [Mor00, Cra00].

Recently, Wang and Appel [WA01] proposed to tackle the problem by building a tracing garbage collector on top of a region-based calculus. My work builds on theirs but makes the following new significant contributions:

• I show how to use intensional type analysis (ITA) [TSS00, HM95] to accurately describe the contract between the mutator and the collector and how the same framework can be applied to construct various different type-safe GCs.

• Using ITA to typecheck GC may seem to be an obvious idea to some people, however, none of the previous work [WA99, STS00, TSS00] have succeeded in getting it to work. Indeed, Wang and Appel [WA01] subsequently gave up on using ITA. I show why the problem is nontrivial (see Section 4.1.2) and how to modify the basic ITA framework to solve the problem.

• Wang and Appel’s collector [WA01] relies on whole-program analysis and code duplication to support higher-order and polymorphic languages; this breaks separate compilation and is impractical. I show how to use runtime type analysis to write my GC as a library (thus no code duplication) and how to directly support higher-order polymorphic functions.

• I expose in detail how to implement and certify efficient forwarding pointers. Making them type-safe is surprisingly subtle (see Section 4.6). Wang and Appel [WA01] also claim to support forwarding pointers but their scheme is less efficient and it is unclear whether it is sound.

• I also show how to handle generations with a simple extension of my base calculus.

• A garbage collector is type-safe only if it is written in a provably type-safe language. I have complete type-soundness proofs for all my calculi. Wang and Appel’s collectors [WA01, WA00], on the other hand, are not fully formalized.

Although this work is theoretical in nature, I believe it will be of great interests to the general audience, especially those who are looking to apply new language theory to solve important practical problems such as mobile-code safety and certifying compilation. The next chapter will show how to address the more problematic limitations.

4.1 Motivation and approach

Why do I want a type-safe garbage collector?

The explosive growth of the Internet has induced newfound interest in mobile computation as well as security. Increasingly, applications are being developed at remote sites and then downloaded for execution. A robust mobile code system must allow code from potentially untrusted sources to be executed. At the same time, the system must detect and prevent the execution of malicious code.

The safety of such a system depends not only on the properties of the code being downloaded, but also on the security of the host system itself, or more specifically, its trusted computing base (TCB).

Proof-carrying code and typed assembly languages have been proposed to reduce the size of this TCB by bundling the untrusted code with a mechanically checkable proof of safety, where the safety is usually defined as type-safety. Such systems only need to trust their verifier and runtime system rather than their whole compiler suite.

But all these certifying-compiler projects (e.g., PCC, TAL) still crucially rely on the correctness of a tracing garbage collector for their safety. Recently, both Crary [Cra00] and Morrisett [Mor00] have characterized type-safe garbage collection as one of the major open problems in the area of certifying compilation.

A type-safe GC is not only desirable for reducing the size of the TCB but also for making it possible to ship custom-tailored GC along with mobile code, or to choose between many more GC variants without risking the integrity of the system. Writing GC inside a type-safe language itself also makes it possible to achieve principled interoperation between garbage collection and other memory-management mechanisms (e.g., those based on malloc-free and regions). Indeed, the major software-engineering benefit is that a type-safe GC must make explicit the contract between the collector and the mutator and it must make sure that it is always respected. Without typechecking, such rules can prove difficult to implement correctly and bugs can be very difficult to find.

4.1.1 The problem

Recently, Wang and Appel [WA99] proposed to tackle the problem by layering a stop-and-copy tracing garbage collector [Wil92] on top of a region based calculus, thus providing both type safety and completely automatic memory management.

A region calculus [TT94] annotates the type of every heap object with the region in which it is allocated (such as σ_{1} ×^{ρ} σ_{2} where ρ is the region), thus allowing to safely reclaim memory by freeing any region that does not appear in any of the currently live types.

F ⋮ ×^{F}^{⋮ ×^{F} int int} int  ⇒  T ⋮ ×^{T}^{⋮ ×^{T} int int} int
copy : ∀F.∀T.∀α.(α → α[T/F])

GC = ΛF.Λα.λ(x:α, k:∀ρ.α[ρ/F] → 0)

let region T in

let y = copy[F][T][α](x) in only T in k[T](y)

Figure 4.1: Stop-and-Copy from region F to region T.

GC is written in continuation passing style (CPS). It takes the current region, the heap and a continuation and begins by allocating a new region T with “let region T in e”. It then copies the heap into this new region and frees the old region implicitly with “only T in e” which tells that all regions but T can be reclaimed. This way of freeing regions was introduced by Wang and Appel to circumvent problems linked to aliasing of regions.

The basic idea in building a type-safe GC is to concentrate on type-safety rather than correctness. Rather than try to prove that the copy function faithfully copies all the heap, I just need to show that it has a type looking somewhat like ∀α.(α → (α[T/F])) where (α[T/F]) stands for the type α where the region annotation T is substituted for F (see Fig. 4.1.1). Assuming I have such a function and I don’t keep any reference to the region F, the region calculus will allow to safely reclaim F.

Clearly, there is no correctness guarantee in sight since the copy function might return a completely different value or might not faithfully reproduce the original graph, but it ensures type-safe execution of the whole mutator-collector system and even offers a form of type-preservation guarantee.

The main problem is clearly to write this copy function which needs to trace through arbitrary heap structures at runtime. Therefore, the language needs to support some form of runtime type information in order to do the actual copy.

In their followup paper [WA01], Wang and Appel suggest to circumvent the problem of runtime type information using a mix of monomorphization and defunctionalization (a form of closure conversion due to Reynolds [Rey72, TO98] which reduces the language to a mono-morphic first-order calculus). However, this approach suffers from several major drawbacks:

• It can introduce a significant code size increase and forces the use of separate specialized GC and copy functions for each type appearing in the program. Instead of the promised flexibility to choose among various GC variants, this approach locks you into a single 100% tailor-made collector.

• Monomorphization is not applicable in the presence of recursive polymorphism or existential packages, so their type-safe GC cannot handle languages with polymorphic recursion or abstract types.

• Monomorphization and defunctionalization all rely on whole program analysis which are clearly imcompatible with separate compilation.

• Finally, although their type-safe GC does properly formalize the interaction between the mutator and the collector, the formalization is hidden inside the compiler and hence does not allow to bring out open the overly intimate relationship between the GC and the compiler.

They also try to preserve sharing using forwarding pointers. The rough sketch of the solution they propose is similar to the one I developed (which was done independently). It relies mostly on a form of cast which allows covariant subtyping of references. Making sure that this cast is sufficiently constrained to be safe is difficult. Their informal presentation is incomplete and possibly incorrect, and leaves many important questions unanswered.

4.1.2 My solution

I want to do away with any form of whole program analysis so as to make the mutator and the collector independent in order to reap the promised benefits of more flexibility and clearer interaction between mutator and GC.

In this chapter, I present a different approach for writing the copy function, relying on runtime type analysis. The substitution present in the return type of copy as well as the need to observe types at runtime leads one very naturally to use intensional type analysis (ITA) [TSS00, HM95]. In fact, an earlier paper of Wang and Appel [WA99] was titled “safe garbage collection = regions + intensional type analysis” but they subsequently gave up on using ITA and opted for the solution mentioned above [WA01]. Saha et al. [STS00, TSS00] also tried to use ITA to write the copy function, but their attempt is missing crucial details and didn’t really work either because they failed to properly separate the preserved part of the type from the part that is modified.

Intensional type analysis

A full description of ITA is outside the scope of this thesis, but in short, ITA provides a typecase term to examine at runtime the actual type with which a type variable was instantiated (using some form of runtime type descriptor) and a typerec type to write type functions that do the equivalent of a fold to map one type into another. You can thus define a type function that replaces all occurrences of Int with Bool. One important property is that typerec functions only analyse type constructors and linger when faced with a type variable until that variable is instantiated.

A case for symmetry

So what is the problem? It seems that ITA provides us with just the right tools. We can for example write a simple typerec function S_{T,F}(σ) which substitutes region annotations T for F by recursively analyzing the type σ. At the term level, in the body of copy, we can similarly use typecase to determine the actual type of the object we are copying.

But that means that the type grows each time we go through the GC, from σ to S_{T,F}(σ) to S_{ρ,T}(S_{T,F}(σ)) …. This may seem unimportant since S should be reduced away anyway. But S_{ρ,T}(α) cannot be reduced further until α is instantiated: ∃α.S_{T,F}(α) is a normal form. So the accumulation of S operators is a real problem, since S_{ρ,F}(α) is not equal to S_{ρ,T}(S_{T,F}(α)).

We could arrange for S_{ρ,T}(S_{T,F}(σ)) to reduce to S_{ρ,F}(σ). But then all types become S_{ρ,F}(σ) (where F is the “initial region”) except before the first collection. Also it is very ad-hoc and only works if S obeys this kind of idempotency.

A better approach is to ensure that the input and output types of copy are symmetric. We first redefine S_{ρ}(σ) which simply substitutes ρ for any region annotation (why bother with an initial region) and then redefine copy to have type ∀F.∀T.∀α.(S_{F}(α) → S_{T}(α)) which gets us rid of the special case before the first collection and does not require any special reduction rule for S since GC does not increase the size of the type any more.

A case for tags

The above solution looks good until we try to copy existential packages ∃α ∈ Θ.σ, used to encode closures. The Θ annotation is used to bound the set of regions that can appear in the witness type hidden under the type variable α.

Opening an existential package of type ∃α ∈ {F}.S_{F}(α), gives us the value σ of α (i.e. the witness type) and a value of type S_{F}(σ). Recursively applying copy to that value will return a new value of type S_{T}(σ), but how can we construct the new existential package of type ∃α∈{T}.S_{T}(α)? Reusing σ for the witness type will not do since σ is not constrained to {T} but to {F}. A witness of S_{T}(σ) cannot work either; the only correctly typed package we can produce is ⟨α=S_{T}(σ), v:α⟩ which has type ∃α∈{T}.α.

Clearly, we are again trying to push a new S onto the type rather than replacing an S with another. So we can again arrange for S_{T}(S_{F}(σ)) to reduce to S_{T}(σ), but we really do not want to tie our hands with such an ad-hoc and restrictive scheme.

Instead, we can pay a bit more attention to what we do and observe that S_{ρ}(σ) makes region annotations on σ completely useless, so instead of trying to get those annotations right only to see them substituted we can simply define a parallel set of non-annotated types τ (that we will call tags). Since tags have no region annotations, we can hide them in tag variables without any Θ constraint, which side-steps the problem when copying existentials conveniently. Note that contrary to common practice, our tags are not attached to their corresponding objects but are managed completely independently.

Such a split between types and tags is not a new concept since it was already used in the work on intensional type analysis where tags were called constructors [HM95, CW99]. But here, tags take on more significance since they correspond to a source-level notion of type and will be mapped to different actual types with different type functions M (formerly S) which are used to encapsulate all the constraints that mutator data has to satisfy in order for the collector to do its job. As you will see in sections 4.6 and 4.7 we will use a non-trivial M mapping to force the mutator to provide space for forwarding pointers and to enforce the invariant that references do not point from the old generation to the new.

4.2 Source language λ_{CLOS}

For simplicity of the presentation, the source language we propose to compile and garbage collect is the simply typed λ-calculus.

In order to be able to use our region calculus, we need to convert the source program into a continuation passing style form (CPS). And we also need to close our code to make all data manipulation explicit, so we turn all closures into existential packages.

We will not go into the details of how to do the CPS conversion [DF92] and the closure conversion using existentials [MMH96, MH97].

The language used after CPS conversion and closure conversion is the language λ_{CLOS} shown below.

(types)  τ ::= Int | t | τ_{1} × τ_{2} | τ → 0 | ∃t.τ
(values)  v ::= n | f | x | ⟨v_{1}, v_{2}⟩ | ⟨t=τ_{1}, v:τ_{2}⟩
(terms)  e ::= let x = v in e | let x = π_{i}v in e | v_{1}(v_{2}) | open v as ⟨t, x⟩ in e | halt v
(programs) p ::= letrec \overrightarrow{f = λ(x:τ).e} in e

Since functions are in CPS, they never return, which we represent with the arbitrary return type 0, often pronounced void. ⟨v_{1}, v_{2}⟩ builds a pair while π_{i}v selects its i^{th} element. To represent closures, the language includes existential packages of type ∃t.τ_{2} and constructed by ⟨t=τ_{1}, v:τ_{2}⟩. The construct

open v as ⟨t, x⟩ in e

takes an existential package v, binds the witness type to t and the value to x, and then executes e. The complete program

letrec \overrightarrow{f_{i} = λ(x_{i}:τ_{i}).e_{i}} in e

consists of a list of mutually recursive closed function declarations followed by the main term to be executed.

4.3 Target language λ_{GC}

We translate λ_{CLOS} programs into our target language λ_{GC}. The target language is also used to write the garbage collector. Figure 4.2 gives the syntax of λ_{GC}; the semantics are presented in Section 4.5). λ_{GC} extends λ_{CLOS} with regions [TT94] and (fully reflexive) intensional type analysis [TSS00]. Functions are also fully closed and use CPS but they can additionally be polymorphic over tags and regions.

4.3.1 Regions

Our region calculus uses reference values ν.ℓ of type (σ at ν). ν is the region in which the object is allocated and ℓ is the actual location within that region. Object allocation and memory accesses are made explicit with put and get. In order to trigger GC, ifgc allows us to check whether a region is full. Ensuring timely collection using heap limit checks or other mechanisms is outside the scope of this paper, so put never fails, even if the region is “full”.

(tenv)  Δ ::= · | Δ, t:κ
(venv)  Γ ::= · | Γ, x:σ
(renv)  Θ ::= · | Θ, ρ
(α env)  Φ ::= · | Φ, α:Θ
(region types) Υ ::= {ℓ_{1} :σ_{1}, …, ℓ_{n}:σ_{n}}
(mem types) Ψ ::= {cd:Υ_{cd}, ν_{1} :Υ_{1}, …, ν_{n}:Υ_{n}}
(regions)  R ::= {ℓ_{1} ↦ v_{1}, …, ℓ_{n} ↦ v_{n}}
(memories) M ::= {cd ↦ R_{cd}, ν_{1} ↦ R_{1}, …, ν_{n} ↦ R_{n}}
(states)  P ::= (M, e)
(regions)  ρ ::= ν | r
(kinds)  κ ::= Ω | Ω → Ω
(tags)  τ ::={t | Int | τ_{1} × τ_{2} | τ → 0 | ∃t.τ \atop | λt.τ | τ_{1}τ_{2}}
(types)  σ ::={int | σ_{1} × σ_{2} | ∀[t⃗:κ][r⃗](σ⃗) → 0 | ∃t:κ.σ | σ at ρ | M_{ρ}(τ) \atop | α | ∀⟦τ⃗⟧[r⃗](σ⃗) ⟶^{ρ} 0 | ∃α:Θ.σ}
(values)  v ::={n | x | ν.ℓ | ⟨v_{1}, v_{2}⟩ | ⟨t=τ, v:σ⟩ | v⟦τ⃗⟧ \atop | ⟨α:Θ=σ_{1}, v:σ_{2}⟩ | λ[t⃗:κ][r⃗](\overrightarrow{x:σ}).e}
(operations) op ::= v | π_{i}v | put[ρ]v | get v
(terms)  e ::= v[τ⃗][ρ⃗](v⃗) | let x = op in e | halt v | ifgc ρ e_{1} e_{2} | open v as ⟨t, x⟩ in e | open v as ⟨α, x⟩ in e | let region r in e | only Θ in e | tcase τ of (e_{i}; e_{→}; t_{1}t_{2}.e_{×}; t_{e}.e_{∃})

(tagnf)  τ′ ::= t| Int| τ′ → 0 | τ′_{1} × τ′_{2} | ∃t.τ′ | λt.τ′ | tτ′

Figure 4.2: Syntax of λ_{GC}

Region allocation and reclamation is done with letregion and only. Deallocation of a region is implicit since only lists the regions that should be kept. This neatly works around aliasing problems, at the cost of a more expensive deallocation operation (only needs to go through the list of all regions to find which ones need to be reclaimed). In our case, we have very few regions and deallocate them only occasionally, so it is a good tradeoff.

4.3.2 Functions and code

Since programs in λ_{GC} are completely closed, we can separate code from data. The memory configuration enforces this by having a separate dedicated region cd for all the code blocks. A value λ[t⃗][r⃗](\overrightarrow{x:σ}).e is only an array of instructions (which can contain references to other values in cd) and needs to be put into a region to get a function pointer before one can call it. In practice, functions are placed into the cd region when translating code from λ_{CLOS} and never directly appear in λ_{GC} code.

The indirection provided by memory references allows us to do away with letrec. For convenience, we will use fixf.e in examples, but in reality, e will be placed at the location ℓ in the cd region and all occurrences of f will be replaced by cd.ℓ.

4.3.3 Intensional type analysis

As explained earlier, we have split the notion of type into two. Tags represent the runtime type descriptors and map very directly to source-level types without any region annotations. The only different between λ_{CLOS} types and λ_{GC} tags is the addition of tag functions λt.τ and tag applications τ_{1}τ_{2}, which are needed for type analysis of existentials [TSS00]. To do the actual analysis of tags, terms include a refining typecase construct tcase, i.e. a more refined tag is substituted for τ in each arm of the tcase. Finally, instead of a full-blown typerec construct we only provide a hard-coded M, to keep the presentation simpler. M_{ρ}(τ) is the type corresponding to the tag τ complemented with region annotations ρ:

M_{ρ}(Int)  ⟹ int
M_{ρ}(τ_{1} × τ_{2}) ⟹ (M_{ρ}(τ_{1}) × M_{ρ}(τ_{2})) at ρ
M_{ρ}(∃t.τ) ⟹ (∃t:Ω.M_{ρ}(τ)) at ρ
M_{ρ}(τ → 0) ⟹ ∀[][r](M_{r}(τ)) → 0 at cd

This definition of M forces the mutator to maintain the invariant that all objects are allocated in the same region, which is all our garbage collector requires.

4.4 Translating λ_{CLOS} to λ_{GC}

The translation of terms from λ_{CLOS} to λ_{GC} shown in Fig. 4.3 is mostly directed by the type translation M_{ρ} presented earlier: each function takes the current region as an argument and begins by checking if a garbage collection is necessary. All operations on data are slightly [figure: F ⊢ λ_{CLOS} ⇒ λ_{GC}]

F ⊢_{v} n ⇒ n  F ⊢_{v} f ⇒ cd.F (f)  F ⊢_{v} x ⇒ x

\begin{matrix}F ⊢_{v} v_{1} ⇒ v′_{1} & F ⊢_{v} v_{2} ⇒ v′_{2} \\ F ⊢_{v} ⟨v_{1}, v_{2}⟩ ⇒ put[r]⟨v′_{1}, v′_{2}⟩ \end{matrix}

{F ⊢_{v} v ⇒ v′ \atop F ⊢_{v} ⟨t=τ_{1}, v:τ_{2}⟩ ⇒ put[r]⟨t=τ_{1}, v′:M_{r}(τ_{2})⟩}

\begin{matrix}F ⊢_{v} v_{1} ⇒ v′_{1} & F ⊢_{v} v_{2} ⇒ v′_{2} \\ F ⊢_{e} v_{1}(v_{2}) ⇒ v′_{1}[][r](v′_{2}) \end{matrix} F ⊢_{v} v ⇒ v′

F ⊢_{e} halt v ⇒ halt v′
F ⊢_{e} e ⇒ e′ F ⊢_{v} v ⇒ v′

F ⊢_{e} open v as ⟨t, x⟩ in e ⇒ open (get v′) as ⟨t, x⟩ in e′

F ⊢_{e} e ⇒ e′ F ⊢_{v} v ⇒ v′
F ⊢_{e} let x = v in e ⇒ let x = v′ in e′
F ⊢_{e} e ⇒ e′ F ⊢_{v} v ⇒ v′

F ⊢_{e} let x = π_{i}v in e ⇒ let x = π_{i}(get v′) in e′

\begin{matrix}F ⊢_{e} e ⇒ e′ & ℓ = F (f) \\ ⊢_{f} \begin{matrix}(f = λ(x:τ).e) \\ ⇒ λ[][r](x:M_{r}(τ)).ifgc r (gc[τ][r](cd.ℓ, x)) e′ \end{matrix} \end{matrix}

\begin{matrix}F = \{f_{1} ↦ ℓ_{1}, …, f_{n} ↦ ℓ_{n}\} \\ F ⊢_{f} f_{i} = λ(x_{i}:τ_{i}).e_{i} ⇒ f′_{i} F ⊢_{e} e ⇒ e′ \end{matrix}

⊢_{p} letrec \overrightarrow{f_{i} = λ(x_{i}:τ_{i}).e_{i}} in e ⇒ (\{cd ↦ \{ℓ_{1} ↦ f′_{1}, …\}\}, let region r in e′) Figure 4.3: Translation of λ_{CLOS} terms.

rewritten to account for the need to allocate them in the region or to fetch them from the region. For example a λ_{CLOS} function like:

fix swap(x:Int × Int).
let x1 = π_{1}x in let x2 = π_{2}x in let x′ = ⟨x2, x1⟩ in halt 0

will be turned into the following λ_{GC} function:

cd.ℓ = λ[][r](x:(int × int) at r). ifgc r (gc[Int × Int][r](cd.ℓ, x)) let x = get x in let x1 = π_{1}x in let x2 = π_{2}x in let x′ = put[r]⟨x2, x1⟩ in halt 0

The mapping between λ_{CLOS} identifiers like swap and λ_{GC} location like cd.ℓ is kept in F. The new argument r refers to the current region. It is initially created at the very beginning of the program and is changed after each garbage collection.

An important detail here is that the garbage collector receives the tag τ rather than the type σ of the argument. The GC receives the tags for analysis as they were in λ_{CLOS} rather than as they are translated in λ_{GC}. This maintains a clear distinction between the types the programmer thinks he manipulates and the underlying types they map to.

Another interesting detail is that if the region is full, the function calls the garbage collector with itself as the return function. I.e. when the collection is finished, the collector will jump back to the function which will then redo the check. We could instead call the garbage collector with another function as argument. That would save us from redoing the ifgc but would require many tiny functions which are just not worth bothering with.

The translation in Fig. 4.3 uses λ_{GC} in a somewhat loose way to keep the presentation concise. More specifically, it will generate terms such as let x = π_{i}(get v) in e instead of

let x′ = get v in let x = π_{i}x′ in e. fix gc[t:Ω][r_{1}](f:∀[][r](M_{r}(t)) → 0, x:M_{r_{1}}(t)).

let region r_{2} in

let y = copy[t][r_{1}, r_{2}](x) in only {r_{2}} in f[][r_{2}](y)

fix copy[t:Ω][r_{1}, r_{2}](x:M_{r_{1}}(t)) : M_{r_{2}}(t).

tcase t of

Int ⇒ x
→ ⇒ x

t_{1} ×^{r} t_{2} ⇒ let x_{1} = copy[t_{1}][r_{1}, r_{2}](π_{1}(get x)) in let x_{2} = copy[t_{2}][r_{1}, r_{2}](π_{2}(get x)) in

put[r_{2}]⟨x_{1}, x_{2}⟩^{r_{2}}

∃t_{e} ⇒ open (get x) as ⟨t, y⟩ in

let z = copy[t_{e}t][r_{1}, r_{2}](y) in
put[r_{2}]⟨t=t, z:M_{r_{2}}(t_{e}t)⟩

Figure 4.4: The garbage collector proper.

Turning such code back into the strict λ_{GC} is very straightforward.

On the other hand, the garbage-collection code in Fig. 4.4 uses not only some syntactic sugar but even resorts to using a direct-style presentation of the copy function. This is only for clarity of presentation, of course. As can be seen in figure 4.5, the code after CPS and closure conversion is a lot more difficult to read, partly because of the need to do a form of typed closure conversion [MMH96].

The garbage collector itself is very simple: it first allocates the to region, asks copy to move everything into it and then frees the from region before jumping to its continuation, using the new region.

The copy function is similarly straightforward, recursing over the whole heap and copying in a depth-first way. Clearly, the direct style here hides the stack. When the code is CPS converted and closed (as shown in Fig. 4.5) , we have to allocate that stack of continuations in an additional temporary region and unless our language is extended with some notion of stack, none of those continuations would be collected until the end of the whole garbage collection. The size of this temporary region can be bounded by the size of the to region since we can’t allocate more than one continuation per copied object, so it is still algorithmically efficient, although this memory overhead is a considerable shortcoming.

4.4.1 Closed CPS garbage collector

Figure 4.5 presents the code of the basic collector after CPS and closure conversion (the direct-style code is shown in Fig. 4.4). The presence of free tag variables in the continuations requires the use of a form of translucent types for the typed closure conversion [MMH96].

In the general case typed closure conversion also requires existential quantification over kinds, but in the present case, we can avoid it by using a superset of all possible kinds: since some continuations require t_{1}, t_{2} of kind Ω, Ω while others only need t_{1}, t_{e} or kind Ω, (Ω → Ω), we unify the two into t_{1}, t_{2}, t_{e} where some of the arguments are simply left unused.

The verbose type annotations make it look more scary than it really is. Four new functions were introduced because of CPS conversion. They are all used as continuations to calls to copy:

1. gcend is the code executed at the end of the toplevel call to copy and just finishes the garbage collection by freeing the from space and calling back the mutator.

2. copyexist1 is the continuation of the recursive call to copy an existential package.

3. Since copying a pair requires two recursive calls, we need both copypair1 for the continuation of the first call and copypair2 for the continuation of the second.

Since the code has to be closed, all the free variables need to be passed explicitly via the continuation object k whose type is abbreviated as t_{k}[t] but is really a big existential wrapper around the real data whose type is abbreviated as t_{c}[t].

4.5 A closer look at λ_{GC}

Programs in λ_{GC} use an allocation semantics which makes the allocation of data in memory explicit. The semantics, defined in Fig. 4.6, maps a machine state P to a new machine — Syntactic type shorthands for notational convenience: —

— Basic type of the continuations of copy —

t_{c}[t] ≡ ∀⟦t_{1}, t_{2}, t_{e}⟧[r_{1}, r_{2}, r_{3}](M_{r_{2}}(t), α_{c}) ⟶^{ρ} 0 × α_{c}

— Same as t_{c} but closed with existential packages — t_{k}[t] ≡ (∃t_{1} :Ω.∃t_{2} :Ω.∃t_{e}:Ω → Ω.∃α_{c}:{r_{1}, r_{2}, r_{3}}.t_{c}[t]) at r_{3}

— The main GC entry point — fix gc[t:Ω][r_{1}](f:∀[][r](M_{r}(t)) → 0, x:M_{r_{1}}(t)).

let region r_{2} in

let region r_{3} in

let c = ⟨gcend⟦t, int, λt.t⟧, f⟩ in

let k = put[r_{3}]⟨t_{1} =t, t_{2} =int, t_{e} =λt.t, α_{c}:{r_{1}, r_{2}, r_{3}} = ∀[][r](M_{r}(t)) → 0, c:t_{c}[t]⟩ in copy[t][r_{1}, r_{2}, r_{3}](x, k)

— The second half of GC, passed as a continuation to copy — fix gcend[t_{1} :Ω, t_{2} :Ω, t_{e}:Ω → Ω][r_{1}, r_{2}, r_{3}](y:M_{r_{2}}(t_{1}), f:∀[][r](M_{r}(t_{1})) → 0).

only {r_{2}} in f[][r_{2}](y)

— The main copy entry point — fix copy[t:Ω][r_{1}, r_{2}, r_{3}](x:M_{r_{1}}(t), k:t_{k}[t]).

tcase t of

Int ⇒ open (get k) as ⟨t_{1}, t_{2}, t_{e}, α_{c}, c⟩ in (π_{1}c)[t_{1}, t_{2}, t_{e}][r_{1}, r_{2}, r_{3}](x, π_{2}c)
→ ⇒ open (get k) as ⟨t_{1}, t_{2}, t_{e}, α_{c}, c⟩ in (π_{1}c)[t_{1}, t_{2}, t_{e}][r_{1}, r_{2}, r_{3}](x, π_{2}c)

t_{1} ×^{r} t_{2} ⇒ let c = ⟨copypair1⟦t_{1}, t_{2}, λt.t⟧, ⟨π_{2}(get x), k⟩⟩ in let k = put[r_{3}]⟨t_{1} =t_{1}, t_{2} =t_{2}, t_{e} =λt.t, α_{c}:{r_{1}, r_{2}, r_{3}} = M_{r_{1}}(t_{2}) × t_{k}[t], c:t_{c}[t_{1}]⟩

in copy[t_{1}][r_{1}, r_{2}, r_{3}](π_{1}(get x), k)

∃t_{e} ⇒ open (get x) as ⟨t_{x}, y⟩ in

let c = ⟨copyexist1⟦t_{x}, int, t_{e}⟧, k⟩ in
let k = put[r_{3}]⟨t_{1} =t_{x}, t_{2} =int, t_{e} =t_{e}, α_{c}:{r_{1}, r_{2}, r_{3}} = t_{k}[t], c:t_{c}[t_{e}t_{1}]⟩ in
copy[t_{e}t_{x}][r_{1}, r_{2}, r_{3}](y, k)

— First continuation when copying a pair — fix copypair1[t_{1} :Ω, t_{2} :Ω, t_{e}:Ω → Ω][r_{1}, r_{2}, r_{3}](x_{1} :M_{r_{2}}(t_{1}), c:M_{r_{1}}(t_{2}) × t_{k}[t_{1} × t_{2}]).

let c′ = ⟨copypair2⟦t_{2}, t_{1}, λt.t⟧, ⟨x_{1}, π_{2}c⟩⟩ in

let k = put[r_{3}]⟨t_{1} =t_{1}, t_{2} =t_{2}, t_{e} =λt.t, α_{c}:{r_{1}, r_{2}, r_{3}} = M_{r_{2}}(t_{1}) × t_{k}[t_{1} × t_{2}], c′:t_{c}[t_{2}]⟩ in copy[t_{2}][r_{1}, r_{2}, r_{3}](π_{1}c, k)

— Second continuation when copying a pair — fix copypair2[t_{1} :Ω, t_{2} :Ω, t_{e}:Ω → Ω][r_{1}, r_{2}, r_{3}](x_{2} :M_{r_{2}}(t_{2}), c:M_{r_{2}}(t_{1}) × t_{k}[t_{1} × t_{2}]).

open (get (π_{2}c)) as ⟨t_{1}, t_{2}, t_{e}, α_{c}, c′⟩ in (π_{1}c′)[t_{1}, t_{2}, t_{e}][r_{1}, r_{2}, r_{3}](put[r_{2}]⟨π_{1}c, x_{2}⟩, π_{2}c′)

— Continuation when copying an existential package — fix copyexist1[t_{1} :Ω, t_{2} :Ω, t_{e}:Ω → Ω][r_{1}, r_{2}, r_{3}](z:M_{r_{2}}(t_{e}t_{1}), c:t_{k}[t_{e}t_{1}]).

open (get c) as ⟨t′_{1}, t′_{2}, t′_{e}, α′_{c}, c′⟩ in (π_{1}c′)[t′_{1}, t′_{2}, t′_{e}][r_{1}, r_{2}, r_{3}](put[r_{2}]⟨t=t_{1}, z:M_{r_{2}}(t_{e}t)⟩, π_{2}c′)

Figure 4.5: The basic GC code after CPS and closure conversion.

(M, ν.ℓ[τ⃗][ρ⃗](v⃗))  ⟹ (M, ν.ℓ[τ⃗′][ρ⃗](v⃗))
\begin{matrix}(M, ν.ℓ[τ⃗′][ρ⃗](v⃗)) \\ where M(ν.ℓ) = (λ[t⃗:κ][r⃗](x⃗:σ⃗).e) \end{matrix}  ⟹ (M, e[ρ⃗, τ⃗′, v⃗/r⃗, t⃗, x⃗])
(M, (v⟦τ⃗⟧)[τ⃗][ρ⃗](v⃗))  ⟹ (M, v[τ⃗][ρ⃗](v⃗))
(M, let x = v in e)  ⟹ (M, e[v/x])
(M, let x = π_{i}⟨v_{1}, v_{2}⟩ in e)  ⟹ (M, e[v_{i}/x])
(M, let x = put[ν]v in e)  ⟹ (M{ν.ℓ ↦ v}, e[ν.ℓ/x]) where ℓ ∉ Dom(M(ν))
(M, let x = get ν.ℓ in e)  ⟹ (M, e[v/x]) where M(ν.ℓ) = v
(M, open ⟨t=τ, v:σ⟩ as ⟨t, x⟩ in e)  ⟹ (M, open ⟨t=τ′, v:σ⟩ as ⟨t, x⟩ in e)
(M, open ⟨t=τ′, v:σ⟩ as ⟨t, x⟩ in e)  ⟹ (M, e[τ′, v/t, x])
(M, open ⟨α:Θ=σ_{1}, v:σ_{2}⟩ as ⟨α, x⟩ in e) ⟹ (M, e[σ_{1}, v/α, x])
(M, ifgc ρ e_{1} e_{2})  ⟹ (M, e_{1}) if ρ is full
(M, ifgc ρ e_{1} e_{2})  ⟹ (M, e_{2}) if ρ is not full
(M, let region r in e)  ⟹ (M{ν ↦ {}}, e[ν/r]) where ν ∉ Dom(M)
(M, only Θ in e)  ⟹ (M|_{Θ}, e)
(M, tcase τ of (e_{i}; e_{→}; t_{1}t_{2}.e_{×}; t_{e}.e_{∃}))  ⟹ (M, tcase τ′ of (e_{i}; e_{→}; t_{1}t_{2}.e_{×}; t_{e}.e_{∃}))
(M, tcase Int of (e_{i}; e_{→}; t_{1}t_{2}.e_{×}; t_{e}.e_{∃}))  ⟹ (M, e_{i})
(M, tcase τ → 0 of (e_{i}; e_{→}; t_{1}t_{2}.e_{×}; t_{e}.e_{∃})) ⟹ (M, e_{→})
(M, tcase τ_{1} × τ_{2} of (e_{i}; e_{→}; t_{1}t_{2}.e_{×}; t_{e}.e_{∃})) ⟹ (M, e_{×}[τ^{1}, τ^{2}/t_{1}, t_{2}])
(M, tcase ∃t.τ of (e_{i}; e_{→}; t_{1}t_{2}.e_{×}; t_{e}.e_{∃})) ⟹ (M, e_{∃}[λt.τ/t_{e}])

Figure 4.6: Operational semantics of λ_{GC}.

state P′. A machine state is a pair (M, e) of a memory M and a term e being executed. A memory consists of a set of regions; hence, it is defined formally as a map between region names ν and regions R. A region, in turn, is a map from offsets ℓ to storable values v. Therefore, an address is given by a pair of a region and an offset ν.ℓ. We assign a type to every location allocated in a region with the memory environment Ψ. Fig. 4.8 presents the static semantics.

4.5.1 Closure conversion and copy.

Since the source language is monomorphic, closure conversion need only rely on existentials. This simplicity is however broken by the copy function in the garbage collector itself because this function is (recursively) polymorphic. For that reason, we also need a form of translucent type, namely ∀⟦τ⃗⟧[r⃗](σ⃗) ⟶^{ρ} 0. Closure conversion of the CPS form of copy was also the only reason for introducing ⟨α:Θ=σ_{1}, v:σ_{2}⟩.

4.5.2 Functions and code

Since function bodies can contain references to other functions in cd but we do not have an easy way for the garbage collector to analyze a function body to trace through those references, cd enjoys a special status. It cannot be freed and can only contain functions, no other kind of data.

An alternative would be to require all functions to be fully closed, but that would require the addition of recursive types for the environment containing pointers to all functions and passed around everywhere. It would save us from so many cd special cases, and would allow garbage collecting code, but on the other hand, it would be less realistic since it would amount to disallowing direct function calls.

4.5.3 The type calculus

The target language must be expressive enough to write a tracing garbage collector. Since the garbage collector needs to know the type of values at runtime, the language λ_{GC} must support the runtime analysis of types. Therefore, conceptually, types need to play a dual role in this language. As in the source language λ_{CLOS}, they are used at compile time to type-check well formed terms. However, they are also used at runtime, as tags, to be inspected by the garbage collector (and, in general, by any type analyzing function). To enforce this distinction, we split types into a tag language and a type language. The tags correspond to the runtime entity, while the types correspond to the compile time entity.

During the translation from λ_{CLOS} to λ_{GC}, the tag for a value must be constructed from its type, so the tags in λ_{GC} closely resemble the type language in λ_{CLOS}. To support tag analysis, we need to add tag-level functions (λt.τ) and tag-level applications (ττ_{1}) which in turn requires adding the function kind Ω → Ω.

Types are used to classify terms. The type language includes the existential type for typing closures and the code type ∀[t⃗][r⃗](σ⃗) → 0 for fully closed CPS functions. Moreover, types in the target language must include the region in which the corresponding value resides. Therefore, we use the notation σ at ρ for the type of a value of type σ in region ρ.

To reason about the safety of programs in this language, we will often need to assume that a value resides in a particular region only. For example, after the copy function is finished, we must be able to assume that all the data is contained only in the new region; so that the old region can be safely freed. Therefore, to ensure type safety, we must be able to enforce this invariant at the type level. For this, we use the built-in type operator M. The type M_{ρ}(τ) can only contain values that are in region ρ. This should be contrasted with values of type σ at ρ which can contain references to other regions than ρ.

4.5.4 The term calculus

The term language must support region based memory management and runtime type analysis. New regions are created through the let region r in e construct which allocates a new region ν at runtime and binds r to it. A term of the form put[ρ]v allocates a value v in the region ρ. Data is read from a region in two ways. Functions are read implicitly through a function call. Data may also be read through the get v construct.

Operationally, get takes a memory address ν.ℓ and dereferences it. Since our region calculus does not admit dangling references, and since each reference implicitly carries a region handle, get does not need a region argument, as opposed to put.

Deallocation is handled implicitly through the only Θ in e construct [WA99]. It asserts statically that the expression e can be evaluated using only the set of regions in Θ′ (i.e. Θ extended with the cd region), which is a subset of the regions currently in scope. At runtime, an implementation would treat the set of regions in Θ′ as live and reclaim all other regions.

Θ′ = Θ, cd Ψ|_{Θ′}; Θ′; Δ; Γ|_{Θ′} ⊢ e Θ′ ⊂ Θ′′

Ψ; Θ′′; Δ; Γ ⊢ only Θ in e

The construct |_{Θ′} restricts an environment to the set of regions in Θ′. I.e. Ψ|_{Θ′} is the subset of the heap restricted to the regions in Θ′. Similarly, Γ|_{Θ′} eliminates from Γ all variables whose type refers to regions not mentioned in Θ′.

The use of only was chosen for its simplicity. Other approaches either do not work with a CPS language or carry a significant added complexity to handle the problem of aliasing whereby the program requests the deletion of region r_{1} while r_{2} is still in use and might refer to the same region. only side steps this difficulty by making the actual deletion implicit: instead of requesting deletion of r_{1}, the program will request to keep r_{2} which will prevent r_{1} from being deleted if r_{1} and r_{2} happen to be aliases.

The runtime type analysis is handled through a tcase construct (it should arguably be called tagcase since it analyses tags rather than types). Depending on the head of the tag being analyzed, tcase chooses one of the branches for execution. When analyzing a tag variable t, we refine types containing t in each of the branches [CWM98].

Δ ⊢ t : Ω
Ψ; Θ; Δ; Φ; Γ[int/t] ⊢ e_{i}[int/t]
…
Ψ; Θ; Δ; Φ; Γ ⊢ tcase t of (e_{i}; e_{→}; t_{1}t_{2}.e_{×}; t_{e}.e_{∃})

In the e_{i} branch, we know that the tag variable t is bound to Int and can therefore substitute it away. A similar rule is applied to the other cases.

4.5.5 Formal properties of the language

In this section, we show that type checking in λ_{GC} is decidable and that the calculus is sound. The proofs are in Sec. A.1.

Type checking is decidable

Proposition 4.5.1 Reduction of well formed types is strongly normalizing.

Proof Since the tag language is a simply typed lambda calculus, reduction of well formed tags is strongly normalizing and confluent. The termination of M_{ρ}(τ) follows from a straight-

Δ ⊢ τ : κ  τ has kind κ in context Δ

\overline{· ⊢ Int : Ω}  \frac{Δ(t) = κ}{Δ ⊢ t : κ}  \frac{Δ ⊢ τ_{1} : Ω Δ ⊢ τ_{2} : Ω}{Δ ⊢ τ_{1} × τ_{2} : Ω}  \frac{Δ ⊢ τ_{i} : Ω}{Δ ⊢ τ⃗ → 0 : Ω}

\frac{Δ, t:Ω ⊢ τ : Ω}{Δ ⊢ ∃t.τ : Ω}  \frac{Δ, t:Ω ⊢ τ : Ω}{Δ ⊢ λt.τ : Ω → Ω}  \frac{Δ ⊢ τ_{1} : Ω → Ω  Δ ⊢ τ_{2} : Ω}{Δ ⊢ τ_{1}τ_{2} : Ω}

Θ; Δ; Φ ⊢ σ  σ is well-formed

\overline{Θ; Δ; Φ ⊢ int}  \frac{Θ; Δ; Φ ⊢ σ_{1}  Θ; Δ; Φ ⊢ σ_{2}}{Θ; Δ; Φ ⊢ σ_{1} × σ_{2}}  \frac{\{r⃗\}; t⃗:κ; · ⊢ σ_{i}}{Θ; Δ; Φ ⊢ ∀[t⃗:κ][r⃗](σ⃗) → 0}

\frac{Θ; Δ, t:κ; Φ ⊢ σ}{Θ; Δ; Φ ⊢ ∃t:κ.σ}  \frac{Θ; Δ; Φ ⊢ σ  ρ ∈ Θ}{Θ; Δ; Φ ⊢ σ at ρ}  \frac{Θ; Δ; Φ, α:Θ′ ⊢ σ  Θ′ ⊂ Θ}{Θ; Δ; Φ ⊢ ∃α:Θ′.σ}

\frac{Δ ⊢ τ : Ω  ρ ∈ Θ}{Θ; Δ; Φ ⊢ M_{ρ}(τ)}  \frac{Φ(α) = Θ′  Θ′ ⊂ Θ}{Θ; Δ; Φ ⊢ α}  \frac{\{r⃗\}; Δ; · ⊢ σ_{i} Δ ⊢ τ_{i} : κ_{i} ρ ∈ Θ}{Θ; Δ; Φ ⊢ ∀⟦τ⃗⟧[r⃗](σ⃗) ⟶^{ρ} 0}

Ψ; Θ; Δ; Φ; Γ ⊢ v : σ  v has type σ
Ψ; Θ; Δ; Φ; Γ ⊢ op : σ  op returns a value of type σ

Ψ; Θ; Δ; Φ; Γ ⊢ n : int

\frac{Γ(x) = σ}{Ψ; Θ; Δ; Φ; Γ ⊢ x : σ} \frac{Ψ(ν.ℓ) = σ Dom(Ψ); ·; · ⊢ σ at ν}{Ψ; Θ; Δ; Φ; Γ ⊢ ν.ℓ : σ at ν}

\frac{cd, r⃗; t⃗; · ⊢ σ_{i} Ψ|_{cd}; cd, r⃗; \overrightarrow{t:κ}; ·; \overrightarrow{x:σ} ⊢ e}{Ψ; Θ; Δ; Φ; Γ ⊢ λ[t⃗:κ][r⃗](\overrightarrow{x:σ}).e : ∀[t⃗:κ][r⃗](σ⃗) → 0}

\frac{Ψ; Θ; Δ; Φ; Γ ⊢ v : ∀[t⃗:κ][r⃗](σ⃗) → 0 at ρ Δ ⊢ τ_{i} : κ_{i}}{Ψ; Θ; Δ; Φ; Γ ⊢ v⟦τ⃗⟧ : ∀⟦τ⃗⟧[r⃗](σ⃗[τ⃗/t⃗]) ⟶^{ρ} 0}

\frac{Ψ; Θ; Δ; Φ; Γ ⊢ v_{1} : σ_{1}  Ψ; Θ; Δ; Φ; Γ ⊢ v_{2} : σ_{2}}{Ψ; Θ; Δ; Φ; Γ ⊢ ⟨v_{1}, v_{2}⟩ : σ_{1} × σ_{2}}  \frac{Ψ; Θ; Δ; Φ; Γ ⊢ v : σ_{1} × σ_{2}}{Ψ; Θ; Δ; Φ; Γ ⊢ π_{i}v : σ_{i}}

\frac{Ψ; Θ; Δ; Φ; Γ ⊢ v : σ at ρ}{Ψ; Θ; Δ; Φ; Γ ⊢ get v : σ}  \frac{Δ ⊢ τ : κ  Ψ; Θ; Δ; Φ; Γ ⊢ v : σ[τ/t]}{Ψ; Θ; Δ; Φ; Γ ⊢ ⟨t=τ, v:σ⟩ : ∃t:κ.σ}

\frac{Θ′; Δ; Φ|_{Θ′} ⊢ σ_{1}  Ψ; Θ; Δ; Φ; Γ ⊢ v : σ_{2}[σ^{1}/α]}{Ψ; Θ; Δ; Φ; Γ ⊢ ⟨α:Θ′ =σ_{1}, v:σ_{2}⟩ : ∃α:Θ′.σ_{2}}  \frac{Ψ; Θ; Δ; Φ; Γ ⊢ v : σ  ρ ∈ Θ}{Ψ; Θ; Δ; Φ; Γ ⊢ put[ρ]v : σ at ρ}

Figure 4.7: Static semantics of λ_{GC}, part 1.

CHAPTER 4. PRINCIPLED SCAVENGING  89
Ψ; Θ; Δ; Φ; Γ ⊢ e  e is well-formed
\frac{Ψ; Θ; Δ; Φ; Γ ⊢ v : ∀[t⃗:κ][r⃗](σ⃗) → 0 at ρ Ψ; Θ; Δ; Φ; Γ ⊢ v_{i} : σ_{i}[ρ⃗, τ⃗/r⃗, t⃗]  Δ ⊢ τ_{i} : κ_{i}  ρ_{i} ∈ Θ}{Ψ; Θ; Δ; Φ; Γ ⊢ v[τ⃗][ρ⃗](v⃗)}
\frac{Ψ; Θ; Δ; Φ; Γ ⊢ v : ∀⟦τ⃗⟧[r⃗](σ⃗) ⟶^{ρ} 0 Ψ; Θ; Δ; Φ; Γ ⊢ v_{i} : σ_{i}[ρ⃗/r⃗]  ρ_{i} ∈ Θ}{Ψ; Θ; Δ; Φ; Γ ⊢ v[τ⃗][ρ⃗](v⃗)}  \frac{Ψ; Θ; Δ; Φ; Γ ⊢ op : σ  Ψ; Θ; Δ; Φ; Γ, x:σ ⊢ e}{Ψ; Θ; Δ; Φ; Γ ⊢ let x = op in e}

\frac{Ψ; Θ; Δ; Φ; Γ ⊢ v : ∃t′:κ.σ Ψ; Θ; Δ, t:κ; Φ; Γ, x:σ[t/t_{′}] ⊢ e}{Ψ; Θ; Δ; Φ; Γ ⊢ open v as ⟨t, x⟩ in e}

\frac{Ψ; Θ; Δ; Φ; Γ ⊢ v : ∃α′:Θ′.σ Ψ; Θ; Δ; Φ, α:Θ′; Γ, x:σ[α/α_{′}] ⊢ e}{Ψ; Θ; Δ; Φ; Γ ⊢ open v as ⟨α, x⟩ in e}

\frac{Ψ; Θ; Δ; Φ; Γ ⊢ e_{1} Ψ; Θ; Δ; Φ; Γ ⊢ e_{2} ρ ∈ Θ}{Ψ; Θ; Δ; Φ; Γ ⊢ ifgc ρ e_{1} e_{2}}  \frac{Ψ; Θ, r; Δ; Φ; Γ ⊢ e}{Ψ; Θ; Δ; Φ; Γ ⊢ let region r in e}
\frac{Ψ; Θ; Δ; Φ; Γ ⊢ v : int}{Ψ; Θ; Δ; Φ; Γ ⊢ halt v} \frac{Ψ|_{Θ′}; Θ′, cd; Δ; Φ|_{Θ′}; Γ|_{Θ′} ⊢ e Θ′ ⊂ Θ}{Ψ; Θ; Δ; Φ; Γ ⊢ only Θ′ in e}

\frac{Ψ; Θ; Δ; Φ; Γ ⊢ e_{i}}{Ψ; Θ; Δ; Φ; Γ ⊢ tcase Int of (e_{i}; e_{→}; t_{1}t_{2}.e_{×}; t_{e}.e_{∃})}

\frac{Ψ; Θ; Δ; Φ; Γ ⊢ e_{→}}{Ψ; Θ; Δ; Φ; Γ ⊢ tcase τ⃗ → 0 of (e_{i}; e_{→}; t_{1}t_{2}.e_{×}; t_{e}.e_{∃})}

\frac{Ψ; Θ; Δ; Φ; Γ ⊢ e_{×}[τ^{1}, τ^{2}/t_{1}, t_{2}]}{Ψ; Θ; Δ; Φ; Γ ⊢ tcase (τ_{1} ×^{ρ} τ_{2}) of (e_{i}; e_{→}; t_{1}t_{2}.e_{×}; t_{e}.e_{∃})}

\frac{Ψ; Θ; Δ; Φ; Γ ⊢ e_{∃}[λt.τ/t_{e}]}{Ψ; Θ; Δ; Φ; Γ ⊢ tcase ∃t.τ of (e_{i}; e_{→}; t_{1}t_{2}.e_{×}; t_{e}.e_{∃})}

\frac{Δ ⊢ t : Ω Ψ; Θ; Δ; Φ; Γ[int/t] ⊢ e_{i}[int/t] Ψ; Θ; Δ; Φ; Γ ⊢ e_{→} Ψ; Θ; Δ, t_{1} :Ω, t_{2} :Ω; Φ; Γ[t^{1} ×^{r} t^{2}/t] ⊢ e_{×}[t^{1} ×^{r} t^{2}/t] Ψ; Θ; Δ, t_{e}:Ω → Ω; Φ; Γ[∃t.t^{e}t/t] ⊢ e_{∃}[∃t.t^{e}t/t]}{Ψ; Θ; Δ; Φ; Γ ⊢ tcase t of (e_{i}; e_{→}; t_{1}t_{2}.e_{×}; t_{e}.e_{∃})}

Figure 4.8: Static semantics of λ_{GC}, part 2.

Θ ⊢ Υ  Υ is well-formed in context Θ
⊢ Ψ  Ψ is well-formed
\frac{Θ; ·; · ⊢ σ_{i}}{Θ ⊢ \{ℓ_{1} :σ_{1}, …, ℓ_{n}:σ_{n}\}}  \frac{\{ν_{1}, …, ν_{n}\} ⊢ Υ_{i}}{⊢ \{ν_{1} :Υ_{1}, …, ν_{n}:Υ_{n}\}}

Υ_{cd} = \{ℓ_{1} :∀[τ⃗_{1}][r⃗_{1}](\overrightarrow{v_{1} :σ_{1}}) → 0, …, ℓ_{n}:∀[τ⃗_{n}][r⃗_{n}](\overrightarrow{v_{n}:σ_{n}}) → 0\}

Ψ ⊢ R : Υ  R has type Υ in context Ψ
⊢ M : Ψ  M has type Ψ

\frac{Ψ; Dom(Ψ); ·; ·; · ⊢ v_{i} : σ_{i}}{Ψ ⊢ \{ℓ_{1} ↦ v_{1}, …, ℓ_{n} ↦ v_{n}\} : \{ℓ_{1} :σ_{1}, …, ℓ_{n}:σ_{n}\}}

\frac{⊢ \{ν_{1} :Υ_{1}, …, ν_{n}:Υ_{n}\} \{ν_{1} :Υ_{1}, …, ν_{n}:Υ_{n}\} ⊢ R_{i} : Υ_{i}}{⊢ \{ν_{1} ↦ R_{1}, …, ν_{n} ↦ R_{n}\} : \{ν_{1} :Υ_{1}, …, ν_{n}:Υ_{n}\}}

Figure 4.9: Environment formation rules.

forward induction on the size of the tag τ. □

Proposition 4.5.2 Reduction of well formed types is confluent.

Proof Since the reduction of well formed types is strongly normalizing, confluence of the reduction follows from local confluence. This follows easily from a case analysis of the reduction of the M_{ρ}(τ) tag. □

Soundness

Definition 4.5.3 The judgment ⊢ (M, e) says that the machine state (M, e) is well-formed. It is defined by:

{⊢ M:Ψ Ψ; Dom(Ψ); ·; ·; · ⊢ e \atop ⊢ (M, e)}

Contrary to the other environments, Ψ is not explicitly constructed in any of the static rules, since it reflects dynamic information. Instead, the soundness proof, or more specifically the type preservation proof, needs to construct some witness Ψ′ for the new state (M′, e′) based on the Ψ of the initial state (M, e).

Proposition 4.5.4 (Type Preservation) If ⊢ (M, e) and (M, e) ⟹ (M′, e′) then ⊢ (M′, e′).

Proposition 4.5.5 (Progress) If ⊢ (M, e) then either e = halt v or there exists a (M′, e′) such that (M, e) ⟹ (M′, e′).

4.6 Forwarding pointers

The base algorithm presented before is unrealistic in a number of ways. The first is the fact that the copy function does not preserve sharing and thus turns any DAG into a tree.

We hence need to add some form of forwarding pointers. Wang and Appel [WA01] propose to pair up every object with its forwarding pointer, incurring a significant memory cost. This additional word is not unheard of, since replicating garbage collectors [NO93, BC99] incur a similar overhead, justified by the desire to provide concurrent collection while avoiding the cost of a read-barrier. We want instead to represent objects as a sum (σ + fwd σ), which requires a single bit per object and corresponds much more closely to traditional implementations. To this end, λ_{GCforw} extends λ_{GC} with new types and terms for tag bits and sum types as well as memory assignment. We do not need a new fwd or ref type since we can use the region calculus’ references for that purpose.

Another requirement for a realistic GC is that the mutator should not need to constantly check for the presence of forwarding pointers since such a read-barrier would only be justified for an incremental GC. In other words, the type as seen by the mutator should not be a sum, although it should still contain the single-bit tag that the GC will use to distinguish between forwarding pointers. Also there should be a way to switch from the mutator’s view of the type of an object to the one of the collector. So we also need to add a form of cast that we call widen which we will use at the beginning of a collection to give the collector access to the forwarding pointers:

Ψ; Θ; Δ; Φ; Γ ⊢ v : σ  v has type σ
Ψ; Θ; Δ; Φ; Γ ⊢ e  e is well-formed
\frac{Ψ; Θ; Δ; Φ; Γ ⊢ v : left σ}{Ψ; Θ; Δ; Φ; Γ ⊢ strip v : σ}  \frac{Ψ; Θ; Δ; Φ; Γ ⊢ v : right σ}{Ψ; Θ; Δ; Φ; Γ ⊢ strip v : σ}
\frac{Ψ; Θ; Δ; Φ; Γ ⊢ v : σ}{Ψ; Θ; Δ; Φ; Γ ⊢ inl v : left σ}  \frac{Ψ; Θ; Δ; Φ; Γ ⊢ v : σ}{Ψ; Θ; Δ; Φ; Γ ⊢ inr v : right σ}

\frac{Ψ; Θ; Δ; Φ; Γ ⊢ v : left σ_{1} Θ; Δ; Φ ⊢ right σ_{2}}{Ψ; Θ; Δ; Φ; Γ ⊢ v : left σ_{1} + right σ_{2}}

\frac{Ψ; Θ; Δ; Φ; Γ ⊢ v : right σ_{2} Θ; Δ; Φ ⊢ left σ_{1}}{Ψ; Θ; Δ; Φ; Γ ⊢ v : left σ_{1} + right σ_{2}}

\frac{Ψ; Θ; Δ; Φ; Γ ⊢ v : σ_{1} + σ_{2} Ψ; Θ; Δ; Φ; Γ, x:σ_{1} ⊢ e_{l} Ψ; Θ; Δ; Φ; Γ, x:σ_{2} ⊢ e_{r}}{Ψ; Θ; Δ; Φ; Γ ⊢ ifleft x = v e_{l} e_{r}}

\frac{Ψ; Θ; Δ; Φ; Γ ⊢ e Ψ; Θ; Δ; Φ; Γ ⊢ v_{1} : σ at ρ Ψ; Θ; Δ; Φ; Γ ⊢ v_{2} : σ}{Ψ; Θ; Δ; Φ; Γ ⊢ set v_{1} := v_{2} ; e}

\frac{Ψ; Θ; Δ; Φ; Γ ⊢ v : M_{ρ}(τ) Ψ|_{cd}; cd, ρ, ρ′; Δ; Φ|_{ρρ′}; x:C_{ρ,ρ′}(τ) ⊢ e}{Ψ; Θ; Δ; Φ; Γ ⊢ let x = widen[ρ′][τ](v) in e}

Figure 4.10: Static semantics for λ_{GCforw}.

σ ::= … | left σ | right σ | left σ_{1} + right σ_{2} | C_{ρ,ρ′}(τ)
v ::= … | inl v | inr v
op ::= … | strip v
e ::= … | ifleft x = v e_{l} e_{r} | set v_{1} := v_{2} ; e | let x = widen[ρ][τ](v) in e

Figure 4.10 shows the relevant new rules of the static semantics. Here, inl and inr (and their type-level counterparts left and right) can be thought of as adding a single tag bit to an object while strip gets back the untagged object and ifleft checks the tag bit. The idea is to represent objects as (left σ) to the mutator (and without the “right σ” alternative to avoid the need for checks) and cast them with the widen operator to (left σ + right(σ at to)) when entering the garbage collector (here “to” denotes the region for the to-space).

Since a single source-level type now maps to two different possible types, we need two type operators: M_{ρ}(τ) to map source types to the mutator’s view of the data and C_{ρ,ρ′}(τ) to map source types to the collector’s view (which adds forwarding pointers). M_{ρ}(τ) is the same as before for base types and for code types, but is changed for existentials and pairs by adding the left constructor that constrains the mutator to provide the tag bit needed to distinguish the forwarded pointer from the non-forwarded data.

M_{ρ}(Int)  ⟹ int
M_{ρ}(τ → 0) ⟹ ∀[][r](M_{r}(τ)) → 0 at cd
M_{ρ}(∃t.τ)  ⟹ (left(∃t.M_{ρ}(τ))) at ρ
M_{ρ}(τ_{1} × τ_{2}) ⟹ (left(M_{ρ}(τ_{1}) × M_{ρ}(τ_{2}))) at ρ
C_{ρ,ρ′}(Int)  ⟹ int
C_{ρ,ρ′}(τ → 0) ⟹ M_{ρ}(τ → 0)
C_{ρ,ρ′}(∃t.τ) ⟹ (left(∃t.C_{ρ,ρ′}(τ)) + right(M_{ρ′}(∃t.τ))) at ρ
C_{ρ,ρ′}(τ_{1} × τ_{2}) ⟹ (+\begin{matrix}left(C_{ρ,ρ′}(τ_{1}) × C_{ρ,ρ′}(τ_{2})) \\ right(M_{ρ′}(τ_{1} × τ_{2})) \end{matrix}) at ρ

It is worth noting again here how the M type operators cleanly encapsulate the invariants imposed on the mutator by the collector. In this case, it forces the mutator to provide the collector with free bit that the collector can then use to distinguish forwarding pointers from non-forwarded data. And we also see how the same mechanism can be used to express the difference between the restricted view offered to the mutator and the full blown access to internal data that the collector needs.

The operational semantics of the new operations is straightforward, especially since we can implement the assignment operator by reusing the indirection through the memory:

(M, let x = strip (inl v) in e) ⟹ (M, e[v/x])
(M, let x = strip (inr v) in e) ⟹ (M, e[v/x])
(M, ifleft x = (inl v) e_{l} e_{r})  ⟹ (M, e_{l}[inl v/x])
(M, ifleft x = (inr v) e_{l} e_{r})  ⟹ (M, e_{r}[inr v/x])
(M, set ν.ℓ := v ; e)  ⟹ (M{ν.ℓ ↦ v}, e)
(M, let x = widen[ρ][τ](v) in e) ⟹ (M, e[v/x])

The translation from λ_{CLOS} to this λ_{GCforw} is not shown since it is basically the same as before except for the insertion of all the inl and strip. The garbage collector can be seen in Fig. 4.11. Compared to the original algorithm, the only difference in the gc function itself is the widening of the heap from M_{r1} to C_{r1,r2} and the fact that we have to bundle the f and x arguments into a pair in order to pass it through the widen operator and unbundle it afterwards. The copy function also needs to be changed of course: when copying a heap object such as a pair, it now has to check with ifleft whether the object was forwarded, if so it just returns the forwarded object, otherwise it does the copy as before and has to overwrite (using set) the original object with the forwarding pointer before returning the copied object. Since the copy is still fundamentally depth-first, it will loop indefinitely in the presence of a cycle. Our source calculus cannot create cycles in the heap, but for more realistic languages, this is a significant restriction.

4.6.1 How to widen safely

The only non-trivial extension is widen which allows the garbage collector to have a different view of the existing memory, provided the two views are somehow compatible. It seems difficult to solve the problem of allowing two views on the same data without such a form of cast. At first, it seems we are just applying a form of subtyping, but this form of subtyping is very powerful since it allows covariant subtyping of references. This means that aliasing issues have to be handled with extreme care.

When faced with the same problem, Wang and Appel came up independently with a fix gc[t:Ω][r_{1}](f:M_{r_{1}}(t → 0), x:M_{r_{1}}(t)).

let region r_{2} in let w = widen[r_{2}][(t → 0 × t)](put[r_{1}](inl ⟨f, x⟩)) in ifleft w = get w then

let w = strip w in let y = copy[t][r_{1}, r_{2}](π_{2}w) in only {r_{2}} in (π_{1}w)[][r_{2}](y)

else halt 0

fix copy[t:Ω][r_{1}, r_{2}](x:C_{r_{1},r_{2}}(t)) : M_{r_{2}}(t).

tcase t of

Int ⇒ x
→ ⇒ x

t_{1} ×^{r} t_{2} ⇒ let y = get x in

ifleft y = y then let x_{1} = copy[t_{1}][r_{1}, r_{2}](π_{1}(strip y)) in let x_{2} = copy[t_{2}][r_{1}, r_{2}](π_{2}(strip y)) in let z = put[r_{2}](inl ⟨x_{1}, x_{2}⟩^{r_{2}}) in set x := inr z ; z

else

strip y

∃t_{e} ⇒ let y = get x in

ifleft y = y then open (strip y) as ⟨t, y⟩ in let y = copy[t_{e}t][r_{1}, r_{2}](y) in let z = put[r_{2}](inl ⟨t=t, y:M_{r_{2}}(t_{e}t)⟩) in set x := inr z ; z

else strip y

Figure 4.11: GC with forwarding pointers.

similar idea. But their suggested cast leaves many questions open and might need more

work to be made type-safe. Also its operational semantics actually does a complete copy

of the heap from one region to the other. This might make it easier to prove soundness but

makes it unclear whether it can really be implemented as a nop. In contrast, the operational

semantics of widen is a nop and we have a proof of its soundness.

In order to handle the problem of aliasing mentioned above, it might be possible to rely

on some form of linear typing or alias types [WM00], but given the inherent generality of a garbage collector, it seems difficult. Our approach is to rely on the consistent application of the same cast over the whole heap, so that aliases are guaranteed to be cast in the same way (or discarded, since all free variables are thrown away).

Rather than an ad-hoc widen we could provide a more general cast that consistently applies a given type transformation (as long as it obeys the notion of subtyping extended with covariant subtyping of references) to any particular set of regions. See the next chapter for an example.

In Figure 4.10, the typing rule for widen shows that the expression e is typed in an environment that only contains x. In essence x represents the entire heap. Further, x is obtained from the value v that has type M_{ρ}(τ). Looking at the definition of M, we can see that all values reachable from v will have a type of the form M_{ρ}(τ′). Since both M and C are iterators, we can now define a casting operation from one type to the other as an iterator. This iterator will traverse the entire heap and systematically convert from one type to the other; this systematic conversion is necessary to avoid ending up with a value that has a particular type along one path, but has a different type along another path.

The proof of soundness of widen is rather intricate. It starts by ignoring some dead objects from the heap, so that only objects of type M_{ρ}(τ) are left, which get cast to C_{ρ,ρ′}(τ). For that reason, we need to loosen our notion of a well formed machine state to allow restricting the considered memory M to just a well-typed sufficient subset \overline{M}, where “sufficient” means that no object outside of \overline{M} is needed to complete execution. This safely permits ill-typed garbage.

Definition 4.6.1 The machine state (M, e) is well formed iff

\begin{aligned}\overline{M} ⊂ M & ⊢ \overline{M}:Ψ & Ψ; Dom(Ψ); ·; ·; · ⊢ e \\ & ⊢ (M, e) \end{aligned}

Proposition 4.6.2 (Type Preservation) If ⊢ (M, e) and (M, e) ⟹ (M′, e′) then ⊢ (M′, e′). Proof See Sec. A.1.2.

Proposition 4.6.3 (Progress) If ⊢ (M, e) then either e = halt v or there exists a (M′, e′) such that (M, e) ⟹ (M′, e′). Proof See Sec. A.1.2.

4.7 Generational collection

Another important aspect of a modern GC is the support for generational garbage collection. If we first restrict ourselves to a side-effect free language, then we can collect a single generation at a time so long as we can express the fact that an object in the old generation cannot point to an object in the young generation.

To that end we need to extend λ_{GC} with existential quantification over regions, so that the mutator does not need to care whether an object is allocated in the young or the old region. We also need to add some way to check in which region an object is allocated so that GC can detect when an object is in the old generation (and hence does not need copying):

σ ::= … | ∃r ∈Θ.(σ at r)
v ::= … | ⟨r ∈ Θ = ρ, v:σ⟩
e ::= … | open v as ⟨r, x⟩ in e | ifreg (ρ_{1} = ρ_{2}) e_{1} e_{2}

Apart from those new constructs (whose static semantics is presented in Fig. 4.12), the M type operator also needs to be modified to reflect the new invariant imposed on the mutator. It is now indexed by two regions (the old and the new) and has to enforce the fact that objects in the old region cannot have references to the new region:

M_{ρ_{y},ρ_{o}}(Int)  ⟹ int
M_{ρ_{y},ρ_{o}}(τ → 0) ⟹ ∀[][r_{y}, r_{o}](M_{r_{y},r_{o}}(τ)) → 0 at cd
M_{ρ_{y},ρ_{o}}(∃t.τ) ⟹ ∃r ∈{ρ_{y}, ρ_{o}}.((∃t.M_{r,ρ_{o}}(τ)) at r)
M_{ρ_{y},ρ_{o}}(τ_{1} × τ_{2}) ⟹ ∃r ∈{ρ_{y}, ρ_{o}}.((M_{r,ρ_{o}}(τ_{1}) × M_{r,ρ_{o}}(τ_{2})) at r)

By using the set {r, ρ_{o}} we make sure that if r is the old generation, pointers reachable

Θ; Δ; Φ ⊢ σ  σ is well-formed
Ψ; Θ; Δ; Φ; Γ ⊢ v : σ  v has type σ
Ψ; Θ; Δ; Φ; Γ ⊢ e  e is well-formed
\frac{Θ′ ⊂ Θ  Θ, r; Δ; Φ ⊢ σ}{Θ; Δ; Φ ⊢ ∃r ∈Θ′.(σ at r)}  \frac{Δ ⊢ τ : Ω  ρ_{1} ∈ Θ  ρ_{2} ∈ Θ}{Θ; Δ; Φ ⊢ M_{ρ_{1},ρ_{2}}(τ)}
\frac{Ψ; Θ; Δ; Φ; Γ ⊢ v : σ[ρ/r] at ρ  ρ ∈ Θ′  Θ′ ⊂ Θ}{Ψ; Θ; Δ; Φ; Γ ⊢ ⟨r ∈ Θ′ = ρ, v:σ⟩ : ∃r ∈Θ′.(σ at r)}

\frac{Ψ; Θ; Δ; Φ; Γ ⊢ v : ∃r ∈Θ′.(σ at r) Ψ; Θ, r; Δ; Φ; Γ, x:σ at r ⊢ e}{Ψ; Θ; Δ; Φ; Γ ⊢ open v as ⟨r, x⟩ in e}

\frac{Ψ; Θ[r, r/r_{1}, r_{2}]; Δ; Φ[r, r/r_{1}, r_{2}]; Γ[r, r/r_{1}, r_{2}] ⊢ e_{1}[r, r/r_{1}, r_{2}] Ψ; Θ; Δ; Φ; Γ ⊢ e_{2} r ∉ Θ}{Ψ; Θ; Δ; Φ; Γ ⊢ ifreg (r_{1} = r_{2}) e_{1} e_{2}}

\frac{Ψ; Θ[ν/r]; Δ; Φ[ν/r]; Γ[ν/r] ⊢ e_{1}[ν/r] Ψ; Θ; Δ; Φ; Γ ⊢ e_{2}}{Ψ; Θ; Δ; Φ; Γ ⊢ ifreg (r = ν) e_{1} e_{2} Ψ; Θ; Δ; Φ; Γ ⊢ ifreg (ν = r) e_{1} e_{2}}

\frac{Ψ; Θ; Δ; Φ; Γ ⊢ e_{2}}{Ψ; Θ; Δ; Φ; Γ ⊢ ifreg (ν_{1} = ν_{2}) e_{1} e_{2}}  \frac{Ψ; Θ; Δ; Φ; Γ ⊢ e_{1}}{Ψ; Θ; Δ; Φ; Γ ⊢ ifreg (ν_{1} = ν_{1}) e_{1} e_{2}}

Figure 4.12: Static semantics of λ_{GCgen}.

from it cannot point back to the new generation.

The operational semantics are again rather simple:

(M, open ⟨r ∈ Θ = ν, v:σ⟩ as ⟨r, x⟩ in e) ⟹ (M, e[ν, v/r, x])
(M, ifreg (ν = ν) e_{1} e_{2})  ⟹ (M, e_{1})
(M, ifreg (ν_{1} = ν_{2}) e_{1} e_{2})  ⟹ (M, e_{2})

Although the operational semantics do not take advantage of it (in order to simplify the soundness proof), we defined the existentials over regions in such a way that they can be implemented as nop since the encapsulated reference usually already encodes the region in its bit-pattern (or in its ν.ℓ).

The new term translation is again not shown since it is so similar to the original one. The new type constraint is trivially always satisfied as long as the mutator only allocates from the younger generation and as long as the memory is immutable. If side-effects were to be necessary, it should be possible to extend this scheme with one mutable region (keeping all others immutable) which would be considered similarly to the older generation but scanned at each collection. Obviously, this would first require adding some way to scan a region, but should not present any serious difficulty.

The GC itself can be seen in figure 4.13. The main difference with the basic GC of figure 4.4 is that it does not copy to a new region but to an existing one and stops traversing the tree as soon as we encounter a reference to the old generation.

When hitting such an external reference, we have to repack it just to help the typesystem understand that this reference is of type M_{ρ_{y},ρ_{o}}(τ). But those operations are free anyway.

Note that another function needs to be written to garbage collect the old generation, but that one is the same as the non-generational one.

At first sight, the λ_{GCgen} language may seem unsound because we allow existentials over regions. However, these types are not existentials in a real sense since they do not hide a region within a type. Rather, in the type ∃r ∈ Θ.(σ at r), the set Θ is an upper bound on the regions that the variable r may range over. In this sense, our existential is closer to a bounded quantification.

Proposition 4.7.1 (Type Preservation) If ⊢ (M, e) and (M, e) ⟹ (M′, e′) then ⊢ (M′, e′). Proof See Sec. A.1.3.

Proposition 4.7.2 (Progress) If ⊢ (M, e) then either e = halt v or there exists a (M′, e′) such that (M, e) ⟹ (M′, e′). Proof See Sec. A.1.3. fix gc[t:Ω][r_{y}, r_{o}](f:M_{r_{y},r_{o}}(t → 0), x:M_{r_{y},r_{o}}(t)).

let y = copy[t][r_{y}, r_{o}](x) in
only {r_{o}} in let region r_{y} in f[][r_{y}, r_{o}](y)
fix copy[t:Ω][r_{y}, r_{o}](x:M_{r_{y},r_{o}}(t)) : M_{r_{o},r_{o}}(t).
tcase t of
Int ⇒ x
→ ⇒ x
t_{1} ×^{r} t_{2} ⇒ open x as ⟨r, x⟩ in

ifreg r = r_{o} then ⟨r ∈ {r_{o}} = r_{o}, x⟩ else let x_{1} = copy[t_{1}][r_{y}, r_{o}](π_{1}(get x)) in let x_{2} = copy[t_{2}][r_{y}, r_{o}](π_{2}(get x)) in ⟨r ∈ {r_{o}} = r_{o}, put[r]⟨x_{1}, x_{2}⟩^{r_{2}}⟩

∃t_{e} ⇒ open x as ⟨r, x⟩ in

ifreg r = r_{o} then ⟨r ∈ {r_{o}} = r_{o}, x⟩ else open (get x) as ⟨t, y⟩ in let z = copy[t_{e}t][r_{y}, r_{o}](y) in ⟨r ∈ {r_{o}} = r_{o}, put[r]⟨t=t, z:M_{r,r_{o}}(t_{e}t)⟩⟩

Figure 4.13: Generational GC.

4.8 Related work

Wang and Appel [WA99] proposed to build a tracing garbage collector on top of a regionbased calculus, thus providing both type safety and completely automatic memory management. The main weakness of their proposal is that it relies on a closure conversion algorithm due to Tolmach [TO98] that represents closures as datatypes. This makes closures transparent, making it easier for the copy function to analyze, but it requires whole program analysis and has major drawbacks in the presence of separate compilation. We believe it is more natural to represent closures as existentials [MMH96, MH97] and we show how to use intensional type analysis (on quantified types [TSS00]) to typecheck the GC-copy function.

Recently, Vanderwaart and Crary [VC03] have focused on type-checking the details of the interface between the mutator and the collector, without trying to type-check the GC itself. They show how to express in the types the constraints on the shape of the stack that are necessary for the GC to find all the roots. Also the GC function is not called explicitly, instead the typing rules enforce that every point where a GC can occur has a well-formed stack. Their modeling of the stack layout is very detailed and includes for example a table that maps return addresses to the description of the contents of the corresponding activation frames.

Intensional type analysis was first proposed by Harper and Morrisett [HM95]. They introduced the idea of having explicit type analysis operators which inductively traverse the structure of types. However, to retain decidability of type checking, they restrict the analysis to a predicative subset of the type language. Crary et al. [CW99] propose a very powerful type analysis framework. They define a rich kind calculus that includes sum kinds and inductive kinds. They also provide primitive recursion at the type level. Therefore, they can define new kinds within their calculus and directly encode type analysis operators within their language. They also include a novel refinement operation at the term level. Saha et al [TSS00] shows how to handle polymorphic functions that analyze the quantified type variable—this allows the type analysis to handle arbitrary quantified types. The typerec operators (e.g., M_{ρ}) used in this paper do not require the full power of what is provided in [TSS00] because our source language is only a simply typed lambda calculus.

Tofte and Talpin [TT94] proposed to use region calculus to type check memory management for higher-order functional languages. Crary et al [CWM99] presented a low-level typed intermediate language that can express explicit region allocation and deallocation. Our λ_{GC} language borrows the basic organization of memories and regions from Crary et al. The main difference is that we don’t require explicit capabilities—region deallocation is handled through the only primitive.

Necula and Lee [Nec97, NL98] proposed the idea of proof-carrying code and showed how to construct a certifying compiler for a type-safe subset of C. Morrisett et al. [MWCG98] showed that type-preserving compilation via typed assembly language is a good basis for building certifying compilers. This paper shows that low-level runtime services such as garbage collection can also be expressed in a type-safe language.

4.9 Conclusion

I have presented a type-safe intermediate language with regions and intensional type analysis and show how it can be used to provide a simple and provably type-safe stop-and-copy tracing garbage collector. My key idea is to use intensional type analysis on quantified types (i.e., existentials) to express the garbage-collection invariants on the mutator data objects. I show how this same idea can be used to express more realistic scavengers with efficient forwarding pointers and generations. Because intensional type analysis is also applicable to polymorphic lambda calculus [TSS00], we believe our type safe collector can be extended to handle polymorphic languages as well.

Typed regions

As the technology of certifying compilation and proof carrying code [Nec97, App01, HST^{+}02] progresses, the need to ensure the safety of the runtime system increases: if you go through the trouble of writing a foundational proof of safety of your code, you would rather not trust an unverified conservative garbage collector (GC) with your data. For this reason, it is important to be able to write a type-safe GC, but the state of the art in this matter is still completely impractical: it cannot even handle cyclic data-structures. This chapter’s main goals are thus:

• Argue that, in order to type-check a GC that can collect cyclic data-structures, the type system has to provide a form of assignment that can change the type of a location (i.e. a strong update) even if the set of aliases to this location is unknown.

• Present a type system that provides such a facility. This type system allows the programmer to choose any mix of linear or intuitionistic typing of references and to seamlessly change this choice over time to adapt it to the current needs.

• Implement a type-preserving GC that properly collects cycles and a generational GC that allows the mutator to use destructive assignment.

Traditional type systems are not well-suited to reason about type safety of low-level memory management such as explicit memory allocation, initialization, deallocation, or reuse. Existing solutions to these problems either have a very limited applicability or rely on some form of linearity constraint. Such constraints tend to be inconvenient and a lot of work has gone into relaxing them. For example, the alias-types system [WM00] is able to cleanly handle several of the points above, even in the presence of arbitrary aliasing, as long as the aliases can be statically tracked by the type system.

The reason why it is challenging to show type safety of low-level memory management is that for this kind of code, the line between type safety and correctness is blurred: we end up having to prove some non-trivial properties about the code just to show its type safety. For example, type safety of a generational GC depends on the correct processing of the remembered-set (a data-structure holding the set of pointers from the old generation to the new).

An alternative approach would be to use Hoare logic [Hoa69] to show the correctness of the low-level code and then provide a type-safe interface to it. But it is not clear how those two parts would interact: the low-level code might be spread in pieces over a lot of code and might need to propagate complex invariants to the various pieces through the type-safe interface, as is the case for the code that maintains the remembered-set in the mutator. Furthermore as we start to encode more properties than basic type safety into our type systems, the difficulties we are seeing here will start to appear for more mundane code as well. This tendency can already be seen in the Vault project which uses an approach taken from alias-types to prove other properties of their code than just type safety.

The present work is thus an attempt to provide a middle ground between Hoare logic and traditional type systems. Additionally to the above stated goals, we make the following contributions:

• A language that subsumes traditional region calculi as well as alias-types calculi to simultaneously combine the benefits of traditional intuitionistic references and linear references.

• We introduce type cast on memory locations and strong update operations that work in the absence of any static aliasing information.

• The language also offers the ability to iterate over the objects contained in a sequential area of memory.

• We show how to use the calculus of inductive constructions (CiC) to track properties of state. This extends the work of Shao et al. [SSTP02] where they used CiC as their type language to track arbitrary properties of values.

Section 5.1 gives a quick preview of the basic idea developed in this chapter. Section 5.2 introduces the problem of cyclic data-structures as well as two type systems on which our work is built. Section 5.3 describes the core part of the new language. Section 5.4 shows some examples of what the language can do. Section 5.5 extends the language with several features needed by Sec. 5.6 where we show how we use it to write a generational GC. We then discuss related work and conclude. Additionally Sec. A.2 shows the soundness proofs of both the core and the extended language.

5.1 Overview

The typed regions system presented in this chapter is basically a hybrid between traditional region systems and alias types systems. In a traditional region system, the type of a pointer completely determines the type of the object to which it points. In alias types systems on the other hand, the type of the pointer does not carry any information about the type of the object to which it points. Instead, the type of the pointer only indicates the location to which it points, and a separate environment is used to look up the type of that location. Typed regions combines those two such that the type of a location is partly determined by the type of the pointers and partly by a separate environment.

The key idea is to introduce the concept of the intended type of a memory location, which stays constant throughout the lifetime of that location and thus corresponds to a traditional (intuitionistic) type, supplemented with a non-constant map that translates the intended type of each location to its actual type.

The intended type of a location is typically a high-level view of the type of objects that it can hold, that abstracts away time-varying details such as the fact that some fields might be temporarily uninitialized, or that the object is currently replaced by a forwarding pointer.

Let’s say we have a pointer of type τ at ρ.n: that means that it points to an object of intended type τ at location n in region ρ. Each region has a type φ. This type is a function that takes two arguments, the location n of an object and its intended type τ, and returns the actual type of the object φ n τ.

Because the intended types are reflected in the types of pointers, they are kept immutable, since changing the intended type of a location would require updating the type of all the pointers to that location. On the other hand, assignments can change the actual type of a location by modifying the region’s type φ.

If φ is a simple identity function that ignores n, then intended types and actual types are the same and we have basically a traditional region system. On the other hand, if φ ignores τ and only uses n to determine the actual type of a location, we have a system reminiscent of alias types.

The difference in power between typed regions and alias types is similar to the difference between destructive update and a simulation of it using functional update: when functionally updating one element of a data-structure, one needs to rebuild all the spines that lead to this element which requires one to keep track of those spines, whereas with destructive updates, the operation can be done without any knowledge of where this object is currently referenced.

5.2 Background

5.2.1 Cyclic structures

In the course of writing the copy routine of a garbage collector, we discovered that although current type systems can handle the case where the graph is acyclic, generalizing the code to properly handle cycles proves difficult. After experimenting with various algorithms, it became clear that the problem is more fundamental: current type systems are unable to type-check some generic code that can build arbitrary cyclic data-structures. By generic, I mean that it can apply to objects of any type. In other contexts, it could be called polytypic, or intentionally polymorphic. The most obvious examples are gccopy and unpickle.

To see this, let us look at a classic example, a datatype for doubly-linked lists:

datatype α dlist = Node of α ∗ α dlist ref ∗ α dlist ref

The SML type system allows us to declare this datatype and write functions to manipulate it, but does not offer us any way to create such an object because there is no base case to start from. But even if we have a base case, the type system can get in the way. Let us take another example:

datatype α tree = Node of α | Branch of α tree ref ∗ α tree ref

This time, we can construct such trees since we do have a base case, but if we want to construct an “infinite” tree with no actual Node in it, we still first need to build a Node. More specifically, in order to create a cyclic data-structure, we always need a base case to start from, even if the data-structure we want to get in the end does not contain any node of this base case any more. But in the above example, we can only build a base case if we have an object of the proper type α. Which means that a generic routine such as an unpickler or a copying GC needs to be able to construct from scratch the base case of any type that could be involved in a cyclic data-structure. In the tree example above, that means creating a Node of α for any α. Clearly this is not possible.

OCaml provides special support to build cyclic data-structures, such as the dlist example above, with val rec n = Node(0, n, n). This helps for specific code, but is of no use for generic code since it only works for pre-determined cycles, whereas a copying GC simply does not even know when it is creating cycles.

(kinds)  κ ::= Ω | R
(regions) ρ ::= r | ν
(kind env) Δ ::= • | Δ, t:κ
(type env) Γ ::= • | Γ, x:σ
(heap type) Ψ ::= • | Ψ, ν.n ↦ σ
(region env) Θ ::= • | Θ, ρ
(types)  σ ::= t | int | σ × σ | σ at ρ | ∀[Δ]{Θ}(σ⃗) → 0
(values)  v ::= x | i | (v, v) | ν.n | λ[Δ]{Θ}(Γ).e
(operations) op ::= v | π_{i}v | put[ρ] v | get v
(terms)  e ::= v[σ⃗](v⃗) | halt v | let x = op in e | set v := v; e | let r = newrgn in e | freergn ρ; e

Figure 5.1: Syntax of a region-based language.

Type systems that can decouple allocation from initialization are the key to solving this problem, but none of the systems developed so far are sufficiently flexible to handle the case of a generic function such as unpickle. More specifically, none of them know how to handle the case where the pointer to the allocated object escapes (i.e. is passed around and stored at arbitrary locations) before the object is initialized: when we allocate a new object, we obviously know its one and only alias, but we cannot fully initialize yet it because some of the values might not exist yet, and by the time we are done unpickling the children such that initialization can complete, there can be any number of aliases and we do not statically know them because the function is generic.

In order to type-check a practical copying GC, we need a new type system that is able to update the type (e.g. from uninitialized to initialized) of all the aliases to a particular object even when those aliases are not statically known.

5.2.2 Regions

Region-based type systems [TT94, CWM99] are the most practical systems offering typesafe explicit memory management. They provide a solution to the problem of safe deallocation, with a minimum of added constraints. Even though they do not offer any help when trying to type-check low-level code such as object initialization, their practicality makes them very attractive as a starting point. The idea behind region calculi is to only provide bulk deallocation of a whole region (group of objects) at a time. This way the type system only needs to keep track of regions rather than individual objects: the type of every pointer is simply annotated with the region that it references.

Figure 5.1 an example of such a language. put[ρ] v allocates v in region ρ; get v dereferences v and returns the object it points to; newrgn creates a new region; freergn deallocates it; set v_{1} := v_{2} places v_{2} at the location to which v_{1} points; ν.n is a pointer to the n^{th} object created by put in region ν and has type σ at ν if the object referenced has type σ; λ[Δ]{Θ}(Γ).e is a function of type ∀[Δ]{Θ}(σ⃗) → 0; functions are fully closed and use continuation passing style, so they never return (hence the → 0 in the type); Δ is the list of type parameters; Θ lists the regions that need to be live at the time of the call; and σ⃗ and Γ lists the types of the value parameters. A function call v[σ⃗](v⃗) passes types σ⃗ and values v⃗ to function v. Type variables can have two kinds, depending on whether they range over the types σ of kind Ω of objects or over the types ρ of kind R of regions; Ψ does not appear in the terms but is used in the typing rules (not shown here) where it keeps track of the type of each memory location, such that for all reference value ν.n its type is Ψ(ν.n) at ν.

Region calculi do not all look like the above, of course. They do not all use fully closed functions and continuation passing style, for example, but a direct-style presentation would be more complex, as is the correct treatment of closure allocation. Also this language is quite different from the one used in the previous chapter where we had only instead of freergn. The reason is that a calculus based on only made the presentation simpler but it does not lend itself to the kind of extension that are presented in this chapter.

Here is a sample function that creates a cyclic node of the tree datatype presented

(kinds)  κ ::= Ω | Heap | Loc
(locations) ρ ::= r | ν
(type env) Γ ::= • | Γ, x:σ
(kind env) Δ ::= • | Δ, t:κ
(mem env) Θ ::= • | Θ, ρ↦(σ, ..., σ)
(types)  σ ::= t | int | ρ | ∀[Δ]{Θ}(σ⃗) → 0
(values)  v ::= x | i | ν | λ[Δ]{Θ}(Γ).e
(operations) op ::= v | π_{i}v
(terms)  e ::= v[σ⃗](v⃗) | halt v | let x = op in e | free ρ; e | let (r, x) = new n in e | set π_{i}v := v; e

Figure 5.2: Syntax of an alias-types language.

previously, assuming the language has been extended with support for datatypes:

mktree[r, t]{r}(x:t, k : ∀[r, t]{r}((tree t) at r) → 0)
= let y = put[r] (Node x) in set y := Branch y y; k[r, t](y)

The function expects two type arguments r and t, it expects the region r to be live, and expects an argument x of type t (which is only used temporarily to create the dummy Node) and a continuation argument k. The (omitted) kind of r is R and the kind of t is Ω. The put operation allocates memory and temporarily puts a dummy Node into it, while the set operation creates the actual cycle. The continuation k also expects two types r and t, it also expects region r to be live and expects a single value argument which is a pointer to a tree in region r. If k’s type had {} in place of {r}, it would force us to deallocate the region r before calling it and it would make n into a dangling pointer, which is allowed because liveness of the region is only needed and checked when dereferencing with get.

5.2.3 Alias types

The alias-types system [SWM00, WM00] was developed precisely to handle low-level code such as object initialization, memory reuse, and safe deallocation at the object level. To do that, the type of pointers is changed to carry no information about the type of the referenced object. Instead, the type of a pointer is just the location it is pointing to, so it does not need to change when the location’s type or liveness changes. While it provides a lot of power when dealing with low-level code, it relies on an amount of static information which is not always available and definitely not available in our copying GC: if we had this information, we could also statically decide when to deallocate, so we would not need a GC in the first place.

Figure 5.2 shows the syntax of a very simple alias-types language. It can be thought of as a region-based language where the pointers can only point to regions rather than to objects inside them, where regions have been turned into tuples, and where objects inside regions are instead just fields of those tuples. put has disappeared since you cannot add fields to a tuple; get is replaced by π_{i}; set now only mutates a field of a tuple; pointers ν now just have type ν. The environment Ψ mapping locations to their types has been merged into Θ. When dereferencing a pointer of type ρ, we thus have to check the liveness and the type of the corresponding location by looking up ρ in Θ. let (r, x) = new n in e allocates a new object of size n and returns the location as both a value x and a type r. We could also have done this for regions so as to distinguish between the region type and the region value passed to put at runtime, but we conflated the two for simplicity.

Here is a sample code that takes a value of type t and creates an infinite list of this element (a 1-element circular list):

mklist[ϵ, t]{ϵ}(x : t, k:∀[ϵ, t, r]{ϵ, r ↦ (t, r)}(r) → 0)
= let (r, n) = new 2 in set π_{0}n := x; set π_{1}n := n; k[ϵ, t, r](n)

The function expects two arguments ϵ and t where ϵ has kind Heap; it expects also that ϵ is live, and it expects to value arguments x of type t and k, the continuation. The type of the continuation shows that it expects three type arguments, where r is a location; it expects the heap ϵ to be live and extended with a pair at location r holding the infinite list; and it expects a single value argument which is the pointer to that list. Since new only knows about the size of the object, it can only do allocation and the type at location r is originally set to (int, int) and is then incrementally updated by each set operation to (t, int) and then (t, r).

The ability to update a location’s type is the key power of alias-types. But for that it relies crucially on the fact that the type system keeps track of pointer values. In particular, the types need to statically but precisely describe the shape of the heap. Witness the fact in the above example that the type of the circular list is not just list t but instead explicitly describes a 1-element cycle and thus disallows any other shape. The type language of [WM00] is of course much richer than what we show here, providing a lot more flexibility in the kind of heap shapes you can describe.

5.2.4 Calculus of inductive constructions

The calculus of inductive constructions (CiC) [Pau93] that we use as our type language is an extension of the calculus of constructions (CC) [CH88], which is a higher-order typed logic. CC can encode Church’s higher-order predicate logic via the Curry-Howard isomorphism [How80]. Understanding the details of this language is not necessary for this chapter, so I will only give a brief overview, starting with its syntax as a pure type system [Bar91]:

(sort) s ::= Kind | Kscm | Ext
(ptm) φ ::= s | x | λx:φ. φ | φ φ | Πx:φ. φ | Ind(x:φ){φ⃗} | Ctor (i, φ) | Elim[φ](φ){φ⃗}

CC is a typed λ-calculus. x is a variable; φ_{1} φ_{2} is a function application; λx:φ_{1}. φ_{2} is a function; Πx:φ_{1}. φ_{2} is the type of a function taking an argument of type φ_{1} and returning a value of type φ_{2}. This is called a dependent product type and subsumes both the usual function type φ_{1} → φ_{2} and the universal quantifier ∀x:φ_{1}.φ_{2}. When the bound variable x does not occur in φ_{2}, it can be abbreviated φ_{1} → φ_{2}.

CiC, extends CC with inductive definitions, which can be thought of as ML datatypes.

For example, the following introduces an inductive definition of polymorphic lists:

Ind(List:Kind → Kind) {nil : Πx.List x | cons : Πx.x → List x → List x}

Lists can then be built using the constructor Ctor (i, List) where i is either 0 for nil or 1 for cons. The elimination construct Elim combines case analysis with a fixpoint operation. Objects of an inductive type can thus be iterated over using these constructs. When applied to a list, it behaves like fold. For example, if x is a list, Elim[Nat](x){0; λy.λz.z+1} returns its length. CiC has been shown to be strongly normalizing [Wer94], hence the corresponding logic is consistent. It is supported by the Coq proof assistant [HPM^{+}00], which I used to experiment with a prototype system of the results presented in this chapter.

In the remainder of this chapter, I will often use more familiar mathematical notation rather than the strict definition of CiC syntax given in this section. For example, inductive definitions will mostly be presented in BNF format. We will, however, retain the Π notation, which can generally be read as a universal quantifier.

5.3 Typed regions

Our new system of typed regions can be thought of as a hybrid between alias-types and the calculus of capabilities [CWM99] supplemented with the calculus of inductive constructions (CiC), similarly to λ_{H} [SSTP02]. Where alias-types rely on a linear map of live locations’s types and the calculus of capabilities relies on a linear set of live regions, we rely on a linear map of regions’s types.

In a typical region calculus, the type of the object reachable from a pointer (its target) is entirely given by the type of the pointer. In contrast, in the alias-types system, the type of the pointer does not provide any direct information about the type of the target; instead, the target’s type is kept in a linearly managed type map indexed by the pointer’s type, which is the singleton type holding the object’s location. Our new type system mixes the

(sort)  s ::= Kind | Kscm | Ext
(ptm) φ, τ, κ, P ::= s | x | λx:φ. φ | φ φ | Πx:φ. φ | Ind(x:φ){φ⃗} | Ctor (i, φ) | Elim[φ](φ){φ⃗}
(memory) M ::= • | M, ν.n ↦ v
(heap type) Ψ ::= • | Ψ, ν.n ↦ τ
(type env) Γ ::= • | Γ, x:σ
(kind env) Δ ::= • | Δ, t:κ
(region env) Θ ::= • | Θ, ρ↦(φ, n)
(regions) ρ:R κ ::= r | ν
(types)  σ:Ω ::= t | int | σ × σ | τ at ρ.n | ∀[Δ]{Θ}(σ⃗) → 0
(values)  v ::= x | i | (v, v) | ν.n | λ[Δ]{Θ}(Γ).e
(operations) op ::= v | π_{i}v | get v
(terms)  e ::= v[φ⃗](v⃗) | halt v | let x = op in e | let x = put[ρ, τ] v in e | let r = newrgn φ in e | freergn ρ; e | set v :=^{φ} v; e | cast[P] ν ↦ φ; e

Figure 5.3: Syntax of the core language.

two, such that the pointer’s type holds both the location and some information (called the intended type) about the object to which it points, while the remaining information is kept in a map of regions’ types. The type of a region is a function that maps an object’s location and its intended type to its actual type.

5.3.1 The core language

The syntax of a trimmed down version of our language is shown in Fig. 5.3. The language uses continuation passing style and fully closed functions. M, Ψ, Γ, Δ, Θ are environments used in the typing rules and operational semantics. Values v can be integers, pairs ((v, v):σ × σ), references (ν.n:τ at ν.n), and functions. The terms do the following:

halt v  halt the machine, returning v as the result.
π_{i}v  select i^{th} from tuple v.
v[φ⃗](v⃗)  make a tail-call to function v.
newrgn φ  allocate a new region of type φ.
freergn ρ; e  free the region ρ.
put[ρ, τ] v  allocate an object v of intended type τ in region ρ.
get v  fetch the object pointed to by v.
set v_{1} :=^{φ} v_{2}; e update location v_{1} with value v_{2}. φ is the new type of the location.
cast[P] ρ ↦ φ; e Set the type of region ρ to φ. P is a proof that φ is a valid replacement.

The language is very much similar to the simple region calculus presented before, except for the following differences:

• The type language is CiC. The kinds such as Ω (the kind of types σ) and R (the kind of regions ρ) along with the basic type constructors of σ are now defined directly as inductive definitions in CiC.

• The region environment Θ now contains not only a list of live regions, but a map from live regions to their type φ and size n. The type φ is a CiC function that maps the index of a location and its intended type to the actual type of that location.

• Pointer types have the form τ at ρ.n rather than just σ at ρ, where n is the offset inside the region. Such a pointer points to an object of intended type τ but whose actual type can only be discovered by an indirection through the region’s type: the target’s type is φ n τ where Θ(ρ) = (φ, n).

• Just as before, a value ν.n has type (Ψ(ν.n)) at ν.n but Ψ(ν.n) is an intended type and can be of any kind rather than only Ω.

• The region kind R now takes a parameter κ specifying the kind of intended types in this region. If ρ:R κ, then the region’s type φ will be of kind Nat → κ → Ω and the term τ in τ at ρ.n will have to have kind κ.

(M; Θ; Ψ; let x = v in e)  ⟹ (M; Θ; Ψ; e[v/x])
(M; Θ; Ψ; let x = π_{i}(v_{1}, v_{2}) in e)  ⟹ (M; Θ; Ψ; e[v^{i}/x])
(M; Θ; Ψ; ν.n[φ⃗](v⃗)) where M(ν.n) = λ[\overrightarrow{t:κ}]\{Θ′\}(\overrightarrow{x:σ}).e ⟹ (M; Θ; Ψ; e[φ⃗, v⃗/t⃗, x⃗])
(M; Θ; Ψ; let x = get ν.n in e)  ⟹ (M; Θ; Ψ; e[M(ν.n)/x])
(M; Θ, ν ↦(φ, n); Ψ ; let x = put[ν, τ] v in e)  ⟹ \begin{matrix}(M, ν.n ↦ v; Θ, ν ↦(φ, n+1) \\ ; Ψ, ν.n ↦ τ; e[ν.n/x]) \end{matrix}
(M; Θ, ν ↦(φ, n); Ψ; freergn ν; e)  ⟹ (M\ν; Θ; Ψ\ν; e)

(M; Θ; Ψ; let r = newrgn φ in e) where • ⊢^{CiC} φ : Nat → κ → Ω ⟹

(M; Θ, ν ↦ (φ, 0); Ψ; e[ν/r])
where ν is fresh and • ⊢^{CiC} ν : R κ
(M; Θ, ν ↦(φ, n); Ψ; cast[P] ν ↦ φ′; e) ⟹ (M; Θ, ν ↦(φ′, n); Ψ; e)
(M; Θ, ν ↦(φ′, n′); Ψ; set ν.n :=^{φ} v; e) ⟹ (M, ν.n ↦ v; Θ, ν ↦(upd φ′ n φ, n′); Ψ; e)

Figure 5.4: Operational semantics of the core language.

• put takes an additional parameter τ and returns a pointer of type τ at ρ.n after checking that v:φ n τ.

• set is now a strong update: it changes the type of the location to φ which has kind κ → Ω. This type needs to be provided because there are many valid choices and they are not all equivalent.

• newrgn now takes a parameter φ which is the initial type of the region.

We have decided to manipulate whole objects rather than words and not to split allocation from initialization. It is easy to change the language to provide alloc, load, and store instead of put, get, and set, but the typing rules become more verbose and so does the code in the examples.

5.3.2 Semantics

Figure 5.4 shows the typed operational semantics. The machine state is defined as the 4-tuple (M; Θ; Ψ; e). The memory type Ψ can basically be ignored in those reduction steps, since it will only really play a role when we extend the language in Sec. 5.5. Also, the region

⊢ Ψ  memory type Ψ is well-formed
⊢ Δ  type environment Δ is well-formed
Δ ⊢ Θ  region type environment Θ is well-formed in context Δ
Δ ⊢ Γ  value environment Γ is well-formed in context Δ
Ψ; Θ ⊢ M  memory M is well formed in Ψ and Θ
⊢ (M; Θ; Ψ; e)  machine state is well-formed
\frac{• ⊢^{CiC} φ_{ij} : κ  • ⊢^{CiC} ν_{i} : R κ}{⊢ ν_{0}.0 ↦ φ_{00}, …, ν_{m}.n ↦ φ_{mn}}  \overline{⊢ •}  \frac{⊢ Δ  Δ ⊢^{CiC} κ : s}{⊢ Δ, t:κ}

\overline{Δ ⊢ •}  \frac{Δ ⊢ Θ  ρ ∉ Dom(Θ)  Δ ⊢^{CiC} ρ : R κ Δ ⊢^{CiC} φ : Nat → κ → Ω  Δ ⊢^{CiC} n : Nat}{Δ ⊢ Θ, ρ↦(φ, n)}  \frac{Δ ⊢^{CiC} σ_{i} : Ω}{Δ ⊢ x_{0} :σ_{0}, …, x_{n}:σ_{n}}

\frac{∀i:0..n . ∀j:Nat . if j ≥ n_{i} then ν_{i}.j ∉ Dom(Ψ) else Ψ; •; Θ; • ⊢ M(ν_{i}.j) : φ_{i} j (Ψ(ν_{i}.j))}{Ψ; Θ = ν_{0} ↦ (φ_{0}, n_{0}), …, ν_{n} ↦ (φ_{n}, n_{n}) ⊢ M}

\frac{Ψ; Θ ⊢ M  • ⊢ Θ  ⊢ Ψ  Ψ; •; Θ; • ⊢ e}{⊢ (M; Θ; Ψ; e)}

Figure 5.5: Environment formation rules.

environment Θ is only marginally used in put where we need to find the next free location in the region. After type erasure, Ψ would disappear and Θ would only keep track of the size of each region. The rules use the auxiliary type function upd which takes a function φ taking an argument of type Nat and returns a function equal to it, except at point n where it now returns φ′ instead:

upd φ n φ′ = λi.if (i = n) (φ′) (φ i)

The reduction rules are pretty unexceptional. Things to note are that cast is indeed a no-op which does not affect anything other than types. Also the type region environment after a function call is Θ rather than Θ′: the two should be equivalent and e has been type-checked using Θ′, but the use of Θ makes it more obvious that the operation is just a jump. Finally the additional type constraints on the reduction rule for newrgn are only present to simplify the presentation of the soundness proof and would disappear after type erasure.

The formation rules for environments are given in Fig. 5.5 together with the definition of a well-formed machine state ⊢ (M; Θ; Ψ; e). A few notes about the notation used in the rules: environments on the left-hand side of a ⊢ are treated as unordered even for Δ and Θ where order matters. I.e. Δ, t:κ ⊢ Γ is actually a shorthand for Δ′, t:κ, Δ′′ ⊢ Γ where Δ = Δ′, Δ′′. Also, when an i appears in the premises of a rule, it is universally quantified. Finally, a meta-variable x⃗ is completely separate from a meta-variable x and is equivalent to a list x_{0}..x_{n}.

The judgment Δ ⊢^{CiC} φ : κ used in those rules, taken directly from CiC and not shown here, states that φ has kind κ in environment Δ. The judgment ⊢ Δ is also taken directly from CiC. The judgment ⊢ Ψ checks that each intended types has a kind consistent with its region. The judgment Δ ⊢ Γ checks that each variable’s type has kind Ω. The judgment Δ ⊢ Θ checks that the type of each region is consistent with its kind and that each region has only one binding in Θ (a linearity constraint). This is important because in order to be able to free region ρ or to change its type, we need to be sure that the same physical region is not referred to somewhere else under some other name. The judgment Ψ; Θ ⊢ M is the one that expresses the invariant that needs to hold so that intended types, actual types and region types are all consistent with one another. It checks that the actual type of each object in M indeed matches the result of applying to its intended type (stored in Ψ) the corresponding region’s type (stored in Θ). In a simple region system, the well-formedness of M is sometimes written as ⊢ M : Ψ so in our case the judgment Ψ; Θ ⊢ M could be thought of as ⊢ M : Θ(Ψ) where Θ is taken as a function that interprets the intended memory type Ψ and returns an actual memory type. An important detail about the rule is that it verifies that Ψ has no binding for not-yet-allocated locations. This is needed because the intended type is immutable, so Ψ can only be extended but none of its existing bindings can be modified.

Figure 5.6 shows the formation rules for types and terms. The equivalence rule Θ ◁ Θ′ is used to formalize the fact that Θ is not ordered. In the rule for pointer values ν.n, dangling pointers to dead regions can have any type (because the rule for get prevents dereferencing

Δ ⊢ σ⃗ : \overrightarrow{t:κ}  actual type arguments σ⃗ match formal arguments \overrightarrow{t:κ}
Θ ∼ Θ′  Θ is equivalent to Θ′

\frac{Δ ⊢^{CiC} σ : κ  Δ ⊢ σ⃗ : \overrightarrow{t:κ}[σ/t]}{Δ ⊢ σ, σ⃗ : t:κ, \overrightarrow{t:κ}}  \overline{Θ ∼ Θ}  \frac{Θ ∼ Θ′}{Θ, ρ↦(φ, n) ∼ Θ′, ρ↦(φ, n)}

Ψ; Δ; Θ; Γ ⊢ v : σ  value v has type σ
Ψ; Δ; Θ; Γ ⊢ v ↦ σ  value v is a pointer to an object of type σ
Ψ; Δ; Θ; Γ ⊢ op : σ  operation op returns a value of type σ

\overline{Ψ; Δ; Θ; Γ ⊢ n : int}  \frac{Ψ; Δ; Θ; Γ ⊢ v_{i} : σ_{i}}{Ψ; Δ; Θ; Γ ⊢ (v_{1}, v_{2}) : σ_{1} × σ_{2}}  \overline{Ψ; Δ; Θ; Γ ⊢ x : Γ(x)}

\frac{ν ∈ Dom(Θ) ⇒ τ = Ψ(ν.n)}{Ψ; Δ; Θ; Γ ⊢ ν.n : τ at ν.n}  \frac{⊢ Δ  Δ ⊢ Θ  Δ ⊢ \overrightarrow{x:σ}  Ψ; Δ; Θ; \overrightarrow{x:σ} ⊢ e}{Ψ; Δ′; Θ′; Γ ⊢ λ[Δ]\{Θ\}(\overrightarrow{x:σ}).e : ∀[Δ]\{Θ\}(σ⃗) → 0}

Θ(ρ) = (φ, m) Ψ; Δ; Θ; Γ ⊢ v : τ at ρ.n

Ψ; Δ; Θ; Γ ⊢ v ↦ φ n τ

\frac{Ψ; Δ; Θ; Γ ⊢ v ↦ σ}{Ψ; Δ; Θ; Γ ⊢ get v : σ} \frac{Ψ; Δ; Θ; Γ ⊢ v : σ_{1} × σ_{2}}{Ψ; Δ; Θ; Γ ⊢ π_{i}v : σ_{i}}

Ψ; Δ; Θ; Γ ⊢ e  expression e is well-formed

\frac{Ψ; Δ; Θ; Γ ⊢ op : σ  Ψ; Δ; Θ; Γ, x:σ ⊢ e}{Ψ; Δ; Θ; Γ ⊢ let x = op in e}  \frac{Ψ; Δ; •; Γ ⊢ v : int}{Ψ; Δ; •; Γ ⊢ halt v}

\frac{Δ ⊢^{CiC} ρ : R κ  Δ ⊢^{CiC} τ : κ  Ψ; Δ; Θ; Γ ⊢ v : φ n τ Ψ; Δ; Θ, ρ↦(φ, n+1); Γ, x:τ at ρ.n ⊢ e}{Ψ; Δ; Θ, ρ↦(φ, n); Γ ⊢ let x = put[ρ, τ] v in e}

\frac{Ψ; Δ; Θ; Γ ⊢ v ↦ ∀[\overrightarrow{t:κ}]\{Θ′\}(σ⃗) → 0 Δ ⊢ φ⃗ : \overrightarrow{t:κ} Θ ∼ Θ′[φ⃗/t⃗] Ψ; Δ; Θ; Γ ⊢ v_{i} : σ_{i}[φ⃗/t⃗]}{Ψ; Δ; Θ; Γ ⊢ v[φ⃗](v⃗)}  \frac{Δ ⊢^{CiC} φ : Nat → κ → Ω Ψ; Δ, r:R κ; Θ, r ↦(φ, 0); Γ ⊢ e}{Ψ; Δ; Θ; Γ ⊢ let r = newrgn φ in e}

\frac{Δ ⊢^{CiC} P : Πi:0..n−1. Πt:κ. (φ i t) = (φ′ i t) Ψ; Δ; Θ, ρ↦(φ′, n); Γ ⊢ e  Δ ⊢^{CiC} ρ : R κ}{Ψ; Δ; Θ, ρ↦(φ, n); Γ ⊢ cast[P] ρ ↦ φ′; e}  \frac{Ψ; Δ; Θ; Γ ⊢ e}{Ψ; Δ; Θ, ρ↦(φ, n); Γ ⊢ freergn ρ; e}

\frac{Ψ; Δ; Θ, ρ↦(φ, n); Γ ⊢ v : τ at ρ.m Δ ⊢^{CiC} ρ : R κ  Δ ⊢^{CiC} φ′ : κ → Ω  Ψ; Δ; Θ, ρ↦(φ, n); Γ ⊢ v′ : φ′ τ Ψ; Δ; Θ, ρ↦(upd φ m φ′, n); Γ ⊢ e}{Ψ; Δ; Θ, ρ↦(φ, n); Γ ⊢ set v :=^{φ′} v′; e}

Figure 5.6: Static semantics of the core language. them), but pointers past the allocation line of a region are disallowed by checking that they have a binding in Ψ. This way, pointers are live iff their region is live. In the rule for functions, the function is forced to be fully closed by typing its body in an environment that does not include Γ or Δ′. The rule for the trivial operation where op is v is not shown but just delegates to the rule for values. The auxiliary rule for v ↦ σ is used by get and by function calls to make sure that the pointer is not dangling and also to find out the actual type at that location. The rule for put checks that the region is live, with appropriate type, and updates the size. The rule for function calls checks that the arguments have the proper kind and type and also that the current region environment is equivalent to the one expected by the function. The rule for cast checks that the region is live and that P indeed proves that the new type σ′ is equivalent to the old type for all the live locations and for all possible intended types. It does not pay attention to which intended types are actually used at those locations because those intended types are, in general, not known yet when type-checking. The rule for freergn checks that the region is live before and that the rest of the code does not use the region any more. The rule for set does not use the auxiliary rule v ↦ σ because it does not care about the actual type before assignment, since it will overwrite the location. Instead it just checks that the pointer is live and that the new value matches the new type.

5.3.3 Properties of the core language

We state here a few important properties of the language. The proofs can be found in Sec. A.2. Since our type language is CiC, we know it is strongly normalizing and confluent.

Lemma 5.3.1 (Type Preservation) If ⊢ (M; Θ; Ψ; e) and (M; Θ; Ψ; e) ⟹ (M′; Θ′; Ψ′; e′) then ⊢ (M′; Θ′; Ψ′; e′).

Lemma 5.3.2 (Progress) If ⊢ (M; Θ; Ψ; e), either e = halt v or (M; Θ; Ψ; e) ⟹ (M′; Θ′; Ψ′; e′).

Lemma 5.3.3 (Complete Collection)

If ⊢ (M; Θ; Ψ; e) and ∀ν.n∈Dom(M) . ν ∈Dom(Θ) and (M; Θ; Ψ; e) ⟹ (M′; Θ′; Ψ′; e′) then ∀ν.n∈Dom(M′) . ν ∈ Dom(Θ′). In particular, if e′ = halt v then M′ = •. Proof The proof follows trivially from inspection of the reduction rules. The particular case of halt v uses additionally the preservation lemma to verify that ⊢ (M′; Θ′; Ψ′; e′). □

5.4 Examples

To get an idea of how the language is used, here are some examples of how you can simulate the behavior of other systems in this language.

5.4.1 Simple regions

Given the differences between the simple region system presented before and the typed region system, here is what we have to do to translate a program written in the simple region system:

• Add region types and region sizes to Θ. Assuming that intended types are always the same as actual types, the regions’s types will always be λi.λt.t. On the other hand their size will keep changing, so it needs to be passed around as extra type argument of kind Nat.

• Add the kind of intended types to region kinds R. Since the intended types are the same as actual types, the region kind is R Ω.

• Annotate put with the type of the allocated object.

• Tell set that the region’s type is left unchanged. At first it seems like using λt.t for the type annotation φ is enough. But then the type of the region after set is:

λi.if (i = n) (λt.t) ((λi.λt.t) i)

⇓ which reduces to ⇓ λi.if (i = n) (λt.t) (λt.t) where n is the location referenced by the pointer. But this is not equal to λi.λt.t, so we need to add a cast to reshape the region’s type to what we need:

cast[P] ρ ↦ λi.λt.t; e

And P needs to show equivalence between the two types:

P : Πi:0..n−1. Πt:κ. ((λi.if (i = n) (λt.t) (λt.t)) i t) = ((λi.λt.t) i t)
⇓
P : Πi:0..n−1. Πt:κ. ((if (i = n) (λt.t) (λt.t)) t) = t
⇓
P = λi.λt.< λb.((if b (λt.t) (λt.t)) t) = t > if (i = n) (refl equal t) (refl equal t)

Where “refl equal φ” is the proof of φ = φ and where “<f>if b φ_{1} φ_{2}” is a proof of f b if φ_{1} is a proof of f true and φ_{2} is a proof of f false.

• Pointer types σ at ρ need to be turned into σ at ρ.n, but since we do not know n we have to hide it with an existential package: ∃n.σ at ρ.n. Existential packages will be added in the full language shown further.

For convenience, let’s define the two type functions id = λt.t and idr = λi.id. The example from the introduction becomes:

mktree[r, n, t]{r ↦(idr, n)}(x:t, k : ∀[r, n, t]{r ↦(idr, n)}((tree t) at r.(n−1)) → 0)

= let y = put[r, tree t] (Node x) in set y :=^{id} Branch y y; cast[P] r ↦ idr; k[r, n+1, t](y)

Here I avoided using existential packages by making the actual address of the new tree passed to the continuation k explicit. To make writing such code easier, one would define a new set that does a traditional weak update by coupling the set and the cast above. The burden of keeping track of the allocation limit n is not as bad as it seems since each function is actually a basic block with a fixed number of allocations in it.

5.4.2 Stacks

We can use a typed region to represent a contiguous stack of objects. More specifically, a stack is a region S of kind R Unit. The locations in a stack have no intended type, so we use the dummy Unit kind which has a single element denoted (). A function using such a stack will have the following shape:

λ[S:R Unit, st:ST, sp, ss:Nat, …] \{S ↦(st, ss), … \} (\begin{matrix}sp:() at S.sp \\ k:∀[st’:ST, …] \{S ↦(λn.if (n < sp)(st n)(st’ n), ss), …\} (…) → 0 \\ … \end{matrix}) ….

The kind of the stack type is defined as ST = Nat → Unit → Ω where the second argument, of kind Unit, is unused. The type function st, maps the locations in the stack to their current type. The top of the stack is kept in sp, both as a value and a type. The size of the stack is kept in the type variable ss. The type of the continuation k specifies that the stack S when we return to k has the same type as before for all elements below sp, while the rest can be changed at will and described by the type function st’.

When pushing a new element on the stack, we somehow need to find the address of the next consecutive element in region S. Let us extend the language with such a facility, called ifnext v (x, t . e_{1}) (e_{2}) that simply takes a pointer v, checks whether it is the last element in the region, if so continues with e_{2}, otherwise continues with e_{1} with x bound to a pointer to the next consecutive element in the region and t bound to its intended type. Now pushing an object on the stack is done by:

set sp :=^{λ .σ} v; ifnext sp (sp, . e) (halt 0)

where e is the rest of the computation, indicates that we do not care about this argument (it will always be () anyway), σ is the type of v, and where we could do something more clever in case of stack overflow.

Popping elements is simply done implicitly by reverting to a previously saved value of sp.

Before returning to the continuation k we need to find an appropriate st’, and show that the current type of S is indeed the one expected by the continuation. This proof will be passed to a cast operation just before jumping to k. The first part is trivial since the current stack type is exactly what we need to pass as st’. The second part requires proving that λn.if (n < sp)(st n)(st’ n) is equivalent to st’. The type of S at that point will have the following shape:

st’ = λn.if (n = sp) (λ .σ_{1}) (if \begin{matrix}(n = sp+1) (λ .σ_{2}) \\ (if … (st n))) \end{matrix}

The proof mainly entails showing that st and st’ (i.e. the old and the new stack types) are equal w.r.t. locations below sp, which is easy to show since pushing only modifies locations above sp.

5.4.3 Adoption and focus

The Vault language [DF01] uses a mix of alias types and traditional types to provide a powerful user-level language used to write device drivers, where the type system is used among other things to check the correct ordering of operations such as open, write, close. They do that by allowing some types to be tracked, meaning that they behave linearly like alias types. Operations on objects of tracked types can update their type, so the type system can keep track of their state.

In a subsequent paper [FD02], the authors extended the language with adoption which allows to “untrack” a tracked object so it behaves intuitionistically and focus which does the reverse. This allows them to use intuitionistic references while at the same time being able to temporarily strengthen them to a linear reference, using the focus construct:

let x = focus e_{1} in e_{2}

The expression e_{1} has an intuitionistic type, but while inside e_{2}, the variable x to which it is bound has a linear type and can thus be modified using strong update. To guarantee soundness, they impose the restriction that even though strong update can modify the type of the object referred to by x, its type should be the same at the end of e_{2} as it was at its beginning, so the type modification is only temporary. Also any other object in the same region as e_{1} is temporarily unavailable.

We can encode something similar. Let’s assume we have a region ρ like the ones used in the simple regions example above. Its type is idr, i.e. it does not depend on the location of a given object just as is the case in intuitionistic systems. So references will hide their location using an existential wrapper: ∃n.σ at ρ.n. A code equivalent to the focus construct will then look like:

let ⟨n, x⟩ = open e_{1} in e_{2}; cast[P] ρ ↦ idr ; ...

Where e_{1} has type ∃n.σ at ρ.n and x has thus type σ at ρ.n. Within e_{2}, strong update can be used at will on x, but before reaching cast the actual type at location n needs to be reset to σ. Remember that focus had the additional constraint that other references to ρ could temporarily not be used. In our case, we can still use them, although we will not be able to do anything useful with them unless we know their aliasing relationship with x.

(sort)  s ::= Kind | Kscm | Ext
(ptm) φ, τ, κ, P ::= s | x | λx:φ. φ | φ φ | Πx:φ. φ | Ind(x:φ){φ⃗} | Ctor (i, φ) | Elim[φ](φ){φ⃗}
(memory) M ::= • | M, ν.n ↦ v
(heap type) Ψ ::= • | Ψ, ν.n ↦ τ
(type env) Γ ::= • | Γ, x:σ
(kind env) Δ ::= • | Δ, t:κ
(region env) Θ ::= • | Θ, ρ↦θ
(region spec) θ ::= (φ, n) | ρ⃗
(regions) ρ:R κ ::= r | ν
(types)  σ:Ω ::= int | σ × σ | τ at ρ.n | ∃t:κ.σ | ∃r ∈ρ⃗.σ | ∀[Δ]{Θ}(σ⃗) → 0
(values)  v ::= x | i | (v, v) | ν.n | ⟨φ, v⟩ | ⟨ρ, v⟩ | λ[Δ]{Θ}(Γ).e
(operations) op ::= v | π_{i}v | get v | cast[P] v
(terms)  e ::= v[φ⃗](v⃗) | halt v | let x = op in e | let x = put[ρ, τ] v in e | let r = newrgn φ in e | freergn ρ; e | set v :=^{φ} v; e | cast[P] ρ ↦ φ; e | let ⟨t, x⟩ = open v in e | let ⟨r, x⟩ = openrgn v in e | iffirst ρ (x, t . e) (e) | ifnext v (x, t . e) (e)

Figure 5.7: Syntax of the full language

5.5 The full language

As mentioned, this system solves the three problems we have encountered when trying to code a generational GC: The before b test can simply compare b to the offset n of the pointer since it is now carried in its type; Since all objects in a region have a type of the form φ n t, setting the region’s type to something like C makes sure that first indeed returns a pointer to an object of type C(t); Finally, since Θ is managed linearly and since all memory accesses have to look up the target’s type in Θ, we can globally update the type of objects when we move the boundary by updating the corresponding type in Θ.

So here I extend the core calculus by adding various features necessary to write code such as a generational GC. The syntax is shown in Fig. 5.7.

In order to be able to scan regions sequentially, as seen before for stack and as done in GCs as well, we have the following two new primitives:

iffirst ρ (x, t . e) (e): test whether ρ is empty and selects the corresponding branch. If

(M; Θ; Ψ; let x = v in e)  ⟹ (M; Θ; Ψ; e[v/x])
(M; Θ; Ψ; let x = π_{i}(v_{1}, v_{2}) in e)  ⟹ (M; Θ; Ψ; e[v^{i}/x])
(M; Θ; Ψ; ν.n[\overrightarrow{φ}](\overrightarrow{v})) where M(ν.n) = λ[\overrightarrow{t:κ}]\{Θ′\}(\overrightarrow{x:σ}).e ⟹ (M; Θ; Ψ; e[φ⃗, v⃗/t⃗, x⃗])
(M; Θ; Ψ; let x = get ν.n in e)  ⟹ (M; Θ; Ψ; e[M(ν.n)/x])
(M; Θ, ν ↦(φ, n); Ψ ; let x = put[ν, τ] v in e)  ⟹ \begin{matrix}(M, ν.n ↦ v; Θ, ν ↦(φ, n+1) \\ ; Ψ, ν.n ↦ τ; e[ν.n/x]) \end{matrix}
(M; Θ, ν ↦(φ, n); Ψ; freergn ν; e)  ⟹ (M\ν; Θ; Ψ\ν; e)

(M; Θ; Ψ; let r = newrgn φ in e) where • ⊢^{CiC} φ : Nat → κ → Ω ⟹

(M; Θ, ν ↦ (φ, 0); Ψ; e[ν/r])
where ν is fresh and • ⊢^{CiC} ν : R κ
(M; Θ, ν ↦(φ′, n); Ψ; cast[P] ν ↦ φ; e) ⟹ (M; Θ, ν ↦(φ, n); Ψ; e)
(M; Θ, ν ↦(φ′, n′); Ψ; set ν.n :=^{φ} v; e)  ⟹ (M, ν.n ↦ v; Θ, ν ↦(upd φ′ n φ, n′); Ψ; e)
(M; Θ; Ψ; let x = cast[P] v in e)  ⟹ (M; Θ; Ψ; e[v/x])
(M; Θ; Ψ; let ⟨t, x⟩ = open ⟨φ, v⟩ in e) ⟹ (M; Θ; Ψ; e[φ, v/t, x])
(M; Θ; Ψ; let ⟨r, x⟩ = openrgn ⟨ρ, v⟩ in e) ⟹ (M; Θ; Ψ; e[ρ, v/r, x])
\begin{matrix}(M; Θ; Ψ \\ ; iffirst ν (x, t . e_{1}) (e_{2})) \end{matrix} ⟹ \begin{cases}(M; Θ; Ψ; e_{2}) & if Θ(ν) = (φ, 0) \\ (M; Θ; Ψ; e_{1}[τ, ν.0/t, x]) & where Ψ(ν.0) = τ \end{cases}
(M; Θ; Ψ ; ifnext ν.n (x, t . e_{1}) (e_{2})) ⟹ \begin{cases}(M; Θ; Ψ; e_{2}) & if Θ(ν) = (φ, n+1) \\ (M; Θ; Ψ; e_{1}[τ, ν.n+1/t, x]) & where Ψ(ν.n+1) = τ \end{cases}

Figure 5.8: Operational semantics of the full language. non-empty, x is bound to a pointer to the first object and t to its intended type.

ifnext v (x, t . e) (e): similarly, give a pointer to the next object past v, if any. We also have existential packages to hide all those extra type details that we do not always want to have to deal with, as seen before for simple regions:

⟨φ, v⟩ : ∃t:κ.σ: create an existential package of v hiding the witness φ under name t. let ⟨t, x⟩ = open v in e: unpack the existential v into t and x.

Also in some cases it can be difficult to keep track of the region in which lives an object, whereas it is easy to know which set of regions it lives in. So we have region aliases that allow us to give a new name to a region and indicate which set of real regions this can be an alias for. This is used extensively in our generational GC since the mutator generally does not know whether an object lives in the nursery or in the old space:

⟨ρ, v⟩ : ∃r ∈ ρ⃗.τ at r.n: create an existential region package which hides the region ρ under region alias r which can range over regions ρ⃗.

let ⟨r, x⟩ = openrgn v in e: unpack the existential region package v into variable x and region alias r.

To support such operations, we extend the definition of Θ slightly: the region spec stored in the region environment Θ is now either the type of the region together with its size (φ, n) or the set of regions for which this region can be an alias. The typing rules have to be updated also. The generational GC also wants to be able to extend the bound ρ⃗ of an existential region package, so we define a subsumption relation (defined as a CiC inductive type) σ ◁ σ shown later in the typing rules. And we introduce a new operation cast[P] v which returns value v but with its type changed to a supertype according to proof P.

5.5.1 Semantics

The operational semantics of the language are shown in Fig. 5.8. The semantics of the terms of the core language is left unchanged. The reduction rule for cast[P] v is a no-op as expected. The scanning operations iffirst and ifnext both use Ψ to get the intended type of the new location. This of course would disappear after type-erasure.

The environment formation rules stay the same as before except for the following additional rule which specifies that a region alias is valid in Θ as long as the regions’s kinds match:

\frac{Δ ⊢ Θ  Δ ⊢^{CiC} ρ : R κ  Δ ⊢^{CiC} ρ_{i} : R κ}{Δ ⊢ Θ, ρ↦ρ⃗}

Note that regions can be aliases to other region aliases. They can also alias to dead regions. This allows freergn to free a region without caring about possible aliases. Of course the typing rules make sure such dangling aliases are themselves considered dead.

Δ ⊢ σ⃗ : t⃗:κ  actual type arguments σ⃗ match formal arguments t⃗:κ
Θ ⊢ ρ ∈ ρ⃗  region ρ is a member of region set ρ⃗
Θ ◁ Θ′  region type environment Θ can be used where Θ′ is expected
Δ ⊢ σ ◁ σ′  type σ is a subtype of σ′

\frac{Δ ⊢^{CiC} σ : κ  Δ ⊢ σ⃗ : \overrightarrow{t:κ}[σ/t]}{Δ ⊢ σ, σ⃗ : t:κ, \overrightarrow{t:κ}}  \frac{ρ ∈ ρ⃗}{Θ ⊢ ρ ∈ ρ⃗}  \frac{Θ ⊢ ρ_{i} ∈ ρ⃗′}{Θ, ρ↦ρ⃗ ⊢ ρ ∈ ρ⃗′}

\overline{• ◁ •}  \frac{Θ ◁ Θ′}{Θ, ρ↦(φ, n) ◁ Θ′, ρ↦(φ, n)}  \frac{Θ ◁ Θ′}{Θ, ρ↦ρ⃗ ◁ Θ′}  \frac{Θ ◁ Θ′  Θ ⊢ ρ ∈ ρ⃗}{Θ ◁ Θ′, ρ↦ρ⃗}

\overline{Δ ⊢ σ ◁ σ}  \frac{Δ ⊢ σ_{i} ◁ σ′_{i}}{Δ ⊢ σ_{1} × σ_{2} ◁ σ′_{1} × σ′_{2}}  \frac{\begin{matrix}Δ ⊢^{CiC} ρ_{i} : R κ  Δ ⊢^{CiC} ρ′_{j} : R κ \\ ρ⃗ ⊂ ρ⃗′  Δ, r:R κ ⊢ σ ◁ σ′ \end{matrix}}{Δ ⊢ ∃r ∈ρ⃗.σ ◁ ∃r ∈ρ⃗′.σ′}

Ψ; Δ; Θ; Γ ⊢ v : σ  value v has type σ

\overline{Ψ; Δ; Θ; Γ ⊢ n : int}  \frac{Ψ; Δ; Θ; Γ ⊢ v_{i} : σ_{i}}{Ψ; Δ; Θ; Γ ⊢ (v_{1}, v_{2}) : σ_{1} × σ_{2}}  \overline{Ψ; Δ; Θ; Γ ⊢ x : Γ(x)}

\frac{Θ(ν) = (φ, m) ⇒ τ = Ψ(ν.n)}{Ψ; Δ; Θ; Γ ⊢ ν.n : τ at ν.n}  \frac{⊢ Δ  Δ ⊢ Θ  Δ ⊢ \overrightarrow{x:σ}  Ψ; Δ; Θ; \overrightarrow{x:σ} ⊢ e}{Ψ; Δ′; Θ′; Γ ⊢ λ[Δ]\{Θ\}(\overrightarrow{x:σ}).e : ∀[Δ]\{Θ\}(σ⃗) → 0}

\frac{Δ ⊢^{CiC} φ : κ  Ψ; Δ; Θ; Γ ⊢ v : σ[φ/t]}{Ψ; Δ; Θ; Γ ⊢ ⟨φ, v⟩ : ∃t:κ.σ}  \frac{Θ ⊢ ρ ∈ ρ⃗  Ψ; Δ; Θ; Γ ⊢ v : σ[ρ/r]}{Ψ; Δ; Θ; Γ ⊢ ⟨ρ, v⟩ : ∃r ∈ρ⃗.σ}

Θ ⊢ τ at ρ.n ↦ σ  pointer of type τ at ρ.n points to an object of type σ
Ψ; Δ; Θ; Γ ⊢ v ↦ σ  value v points to an object of type σ
Ψ; Δ; Θ; Γ ⊢ op : σ  operation op returns a value of type σ

\overline{Θ, ρ↦(φ, m) ⊢ τ at ρ.n ↦ φ n τ}  \frac{Θ ⊢ ρ ∈ ρ⃗  Θ ⊢ τ at ρ_{i}.n ↦ φ ρ_{i}}{Θ ⊢ τ at ρ.n ↦ φ ρ}

\frac{Ψ; Δ; Θ; Γ ⊢ v : τ at ρ.n  Θ ⊢ τ at ρ.n ↦ σ}{Ψ; Δ; Θ; Γ ⊢ v ↦ σ}  \frac{Ψ; Δ; Θ; Γ ⊢ v ↦ σ}{Ψ; Δ; Θ; Γ ⊢ get v : σ}

\frac{Ψ; Δ; Θ; Γ ⊢ v : σ_{1} × σ_{2}}{Ψ; Δ; Θ; Γ ⊢ π_{i}v : σ_{i}}  \frac{Ψ; Δ; Θ; Γ ⊢ v : σ′  Δ ⊢^{CiC} P : σ′ ◁ σ}{Ψ; Δ; Θ; Γ ⊢ cast[P] v : σ}

Figure 5.9: Static semantics of the full language: auxiliary judgments.

Ψ; Δ; Θ; Γ ⊢ e  expression e is well-formed

\frac{Ψ; Δ; Θ; Γ ⊢ op : σ  Ψ; Δ; Θ; Γ, x:σ ⊢ e}{Ψ; Δ; Θ; Γ ⊢ let x = op in e}  \frac{Ψ; Δ; •; Γ ⊢ v : int}{Ψ; Δ; •; Γ ⊢ halt v}

\frac{Δ ⊢^{CiC} ρ : R κ  Δ ⊢^{CiC} τ : κ  Ψ; Δ; Θ; Γ ⊢ v : φ n τ Ψ; Δ; Θ, ρ↦(φ, n+1); Γ, x:τ at ρ.n ⊢ e}{Ψ; Δ; Θ, ρ↦(φ, n); Γ ⊢ let x = put[ρ, τ] v in e}

\frac{Ψ; Δ; Θ; Γ ⊢ v ↦ ∀[\overrightarrow{t:κ}]\{Θ′\}(σ⃗) → 0 Δ ⊢ φ⃗ : \overrightarrow{t:κ} Θ ◁ Θ′[φ⃗/t⃗] Ψ; Δ; Θ; Γ ⊢ v_{i} : σ_{i}[φ⃗/t⃗]}{Ψ; Δ; Θ; Γ ⊢ v[φ⃗](v⃗)}  \frac{Δ ⊢^{CiC} φ : Nat → κ → Ω Ψ; Δ, r:R κ; Θ, r ↦(φ, 0); Γ ⊢ e}{Ψ; Δ; Θ; Γ ⊢ let r = newrgn φ in e}

\frac{Δ ⊢^{CiC} P : Πi:0..n−1. Πt:κ. (φ i t) ◁ (φ′ i t) Ψ; Δ; Θ, ρ↦(φ′, n); Γ ⊢ e  Δ ⊢^{CiC} ρ : R κ}{Ψ; Δ; Θ, ρ↦(φ, n); Γ ⊢ cast[P] ρ ↦ φ′; e}  \frac{Ψ; Δ; Θ; Γ ⊢ e}{Ψ; Δ; Θ, ρ↦(φ, n); Γ ⊢ freergn ρ; e}

Ψ; Δ; Θ, ρ↦(φ, n); Γ ⊢ v : τ at ρ.m Δ ⊢^{CiC} ρ : R κ Δ ⊢^{CiC} φ′ : κ → Ω Ψ; Δ; Θ, ρ↦(φ, n); Γ ⊢ v′ : φ′ τ Ψ; Δ; Θ, ρ↦(upd φ m φ′, n); Γ ⊢ e

Ψ; Δ; Θ, ρ↦(φ, n); Γ ⊢ set v :=^{φ′} v′; e

\frac{Ψ; Δ; Θ; Γ ⊢ v : ∃t′:κ.σ Ψ; Δ, t:κ; Θ; Γ, x:σ[t/t_{′}] ⊢ e}{Ψ; Δ; Θ; Γ ⊢ let ⟨t, x⟩ = open v in e}  \frac{Ψ; Δ; Θ; Γ ⊢ v : ∃r′ ∈ρ⃗.σ  Δ ⊢^{CiC} ρ_{i} : R κ Ψ; Δ, r:R κ; Θ, r ↦ρ⃗; Γ, x:σ[r/r_{′}] ⊢ e}{Ψ; Δ; Θ; Γ ⊢ let ⟨r, x⟩ = openrgn v in e}

\frac{Δ ⊢^{CiC} ρ : R κ Ψ; Δ; Θ, ρ↦(φ, 0); Γ ⊢ e_{2} Ψ; Δ, t:κ; Θ, ρ↦(φ, n); Γ, x:t at ρ.0 ⊢ e_{1}}{Ψ; Δ; Θ, ρ↦(φ, n); Γ ⊢ iffirst ρ (x, t . e_{1}) (e_{2})}

\frac{Ψ; Δ; Θ; Γ ⊢ v : τ at ρ.m  Ψ; Δ; Θ, ρ↦(φ, m+1); Γ ⊢ e_{2} Δ ⊢^{CiC} ρ : R κ  Ψ; Δ, t:κ; Θ, ρ↦(φ, n); Γ, x:t at ρ.m+1 ⊢ e_{1}}{Ψ; Δ; Θ, ρ↦(φ, n); Γ ⊢ ifnext v (x, t . e_{1}) (e_{2})}

Figure 5.10: Static semantics of the full language: well-formed terms.

Figure 5.9 shows the formation rules for values, operations and auxiliary judgments. Large parts are kept exactly as they were for the core language, so I will concentrate on the differences. The new judgment Θ ⊢ ρ ∈ ρ⃗ checks whether ρ is known to be one of ρ⃗, given the aliasing info provided in Θ. This is used to check the validity of new region aliases. The equivalence relation Θ ∼ Θ′ is replaced by Θ ◁ Θ′. This new relation checks that if the current environment is Θ, we can call a function that expects Θ′, which is true if the real regions are equal and if the call drops some region aliases, or if it creates some new region aliases whose validity we can infer from Θ. The formation rule for ν.n is slightly modified to check that ν is a real live region and not a dead alias.

The auxiliary rule v ↦ σ is extended, with the help of another auxiliary rule τ at ρ.n ↦ σ, to handle region aliases. If ρ is a region alias, we could simply require that the actual type be the same, independently from the region this is an alias for, but that would be too restrictive. Instead, we put no restriction of any sort and simply allow the type σ to depend on ρ. For example, if Θ ⊢ ρ ∈ ρ_{1}, ρ_{2} and the actual type in ρ_{1} is σ_{1} whereas the actual type in ρ_{2} is σ_{2}, the actual type in ρ can be “if (ρ = ρ_{1}) (σ_{1}) (σ_{2})”. More often, σ_{1} and σ_{2} share some structure, as in “int × τ at ρ_{1}.n” and “int × τ at ρ_{2}.n” where the actual type in ρ can naturally be “int × τ at ρ.n”. Inferring such types is not possible in general, so type-checking might need to impose additional constraints or to require additional type annotations in order to be decidable.

Figure 5.10 shows the formation rules for the main terms. In the terms taken from the core language, the only changes are that function calls now use Θ ◁ Θ′ instead of Θ ∼ Θ′ and that cast uses σ ◁ σ′ instead of equality. Most importantly, set is unchanged and does not allow assignment through a region alias, since it would not know which region’s type to update. We might thus need to extend the language again with a new set term that allows assignment through a region alias but requires the old and new actual types to match so that the region’s type does not need to be updated.

5.5.2 Properties of the full language

The full language enjoys the same type preservation and progress properties as the core language. These are proved in A.2.2. It also enjoys the complete collection property, whose proof is the same as it was for the core calculus.

5.6 Generational collection

The simple type-preserving GCs developed until now have a few important limitations. The most important one is that cyclic garbage is not properly collected. Another limitation is that generational collectors do not allow the mutator (i.e. the code that uses the GC) to create references from the old region to the young via destructive assignment. I will show in this section how to use typed regions to lift those restrictions that make existing type-preserving GCs impractical.

5.6.1 Simple type-preserving GC

As seen in the previous chapter, the basic idea of a type-preserving GC, proposed by Wang and Appel [WA01], is to layer a stop&copy collector on top of a region calculus, where the whole heap is placed in a single region and where the copy function copies the heap from the from region to a new to region and then frees the from region.

Because the type of the heap contains region annotations which will necessarily be different before copying than after, the copy function cannot just be of type t → t but instead has to be of the form M_{F} (t) → M_{T} (t) where t represents what should be preserved while the M type function annotates it with details that the mutator does not care about. Furthermore, in order to work correctly, the copy routine might require things like tag bits, mark bits or in case of generational GC it will need to make sure the mutator obeyed the generation barriers and provides a correct remembered set. All those added constraints will need to be somehow encoded in M since copy has obviously no control over t.

Note that M is really a function of two arguments. I write M_{F} (t) instead of M F t only to make the special role of t more visible and to parallel the notation used in the previous chapter.

A good way to look at it is that t is a high-level, GC-oblivious type of the heap, while M_{F} (t) is the low-level representation with all the added details necessary for the correct functioning of the GC. The two will generally not be of the same kind; t might for example correspond to the type used in the high-level source language of the mutator code.

The simplest case is a bare-bones stop&copy GC with a source language that only supports integers and pairs. M might then look like the following, where R is the current region holding the heap:

M_{R}(int)  ⇒ int
M_{R}(τ_{1} × τ_{2}) ⇒ (M_{R}(τ_{1}) × M_{R}(τ_{2})) at R

In the case of a 2-generation collector, the heap will be spread over 2 regions: Y for the nursery and O for the old generation. M will now have to enforce that there cannot be any pointer back from O to Y by only allowing (M_{O,Y} (τ_{1}) × M_{O,Y} (τ_{2})) at Y and (M_{O,O}(τ_{1}) × M_{O,O}(τ_{2})) at O:

M_{O,A}(int)  ⇒ int
M_{O,A}(τ_{1} × τ_{2}) ⇒ ∃r ∈{O, A}.(M_{O,r}(τ_{1}) × M_{O,r}(τ_{2})) at r

In this definition, A is expected to be either equal to O or Y depending on whether pointers to Y are allowed at this place.

5.6.2 Generational GC

We want to create a simple yet usable type-preserving generational GC. In order to be usable, it needs to be able to properly handle cycles in the heap and it needs to allow the mutator to use destructive assignment, even if that means creating pointers back from the old generation to the nursery. The traditional way to deal with such pointers is to keep track of all of them in a remembered-set, but for simplicity, we will restrict mutability to ref-cells, as is done in SML, and place all those ref-cells in a separate region which will thus play the role of a degenerate remembered-set.

Compared to the situations outlined in the previous section, the heap is now spread over three regions: Y for the nursery, O for the old generation and R for the ref-cells. So that the collection of Y can take place without having to scan O, we need to make sure that there is no pointer from O to Y. I.e. pointers from Y and R can point to any of Y, O or R but pointers from O can only point to O or R. Our M type function which maps high-level types to their low-level representation will again have to enforce this constraint. If our high-level types include integers, ref-cells and pairs, M could be defined as follows:

M_{Y,R,O,A}(int)  ⇒ int
M_{Y,R,O,A}(ref τ) ⇒ M_{Y,R,O,Y} (τ) at R
M_{Y,R,O,A}(τ_{1} × τ_{2}) ⇒ ∃r ∈{O, A}.(M_{Y,R,O,r}(τ_{1}) × M_{Y,R,O,r}(τ_{2})) at r

Here A is expected to be either equal to O or to Y and the type M_{Y,R,O,A}(τ_{1}×τ_{2}) is the type of a pair that can be allocated either in O or in A and whose children (and grand-children, etc...) might also refer additionally to R or to Y.

The GC code that copies objects from the nursery Y to the old space O takes the type of the heap t, the regions Y, O, and R, and the heap itself h : M_{Y,R,O,Y} (t) as well as a continuation k. It first copies everything reachable from the root h (but only within Y) to O. Then scans all the ref-cells, considered as extra roots, and copies anything reachable from them as well. Finally, it frees Y and creates a new nursery before returning to the continuation k. It could look like the following:

GC[t:Ω^{τ}]{Y, R, O}(h:M_{Y,R,O,Y} (t), k:…)
= let h = copy[t, Y, R, O](h) in let t′, x = first R in let = redirect[t′, Y, R, O](x) in freergn Y; let Y = newrgn in k[Y, R, O](h)

where the copy function should simply do a generic deep copy, but only within the bounds of Y and the redirect function should scan the R region, redirecting any pointer to the Y region to a copy of that object in O. The type of the new root pointer h returned by copy can be described as M_{Y,R,O,O}(t).

In order to be able to free Y at the end of the collection, we need to keep track somewhere at the type-level of the fact that pointers in R that have been redirected can only point to O or to R but not to Y any more. Indeed, as the redirection is taking place, the type of h keeps changing gradually until the end when it becomes equivalent to M_{O,R,O,O}(t) and thus independent from Y. If b is the boundary between what has been redirected and what has not, then we can write h:C_{Y,R,O,O,b}(t) where C is defined as:

C_{Y,R,O,A,b}(int)  ⇒ int
C_{Y,R,O,A,b}(ref τ) ⇒ let r = if (before b) (O) (Y) in C_{Y,R,O,r,b}(τ) at R
C_{Y,R,O,A,b}(τ_{1} × τ_{2}) ⇒ ∃r ∈{O, A}.(C_{Y,R,O,r,b}(τ_{1}) × C_{Y,R,O,r,b}(τ_{2})) at r

This is almost the same as the previous M, except for the added b parameter and the case of ref where the object reachable from the ref-cell will have either type C_{Y,R,O,O,b}(τ) or C_{Y,R,O,Y,b}(τ) depending on whether the cell has already been redirected or not.

The above presentation is sloppy and simplistic but already requires unusual features from the language:

• The before b test in the definition of C_{Y,R,O,A,b}(ref τ) obviously needs to know not just the region in which the object is located but its actual location, which implies

that object locations need to be reflected in the pointer type.

• The new operation first (and its companion ifnext which will be needed inside redirect), that allows the code to scan the region R, should return both a new pointer x and a new type t′ where x is expected to be of type C_{Y,R,O,Y,b}(t′). But this is only safe if we know that all objects in the relevant section have a type of the form C_{Y,R,O,Y,b}(τ).

• Since the type of h, indexed by the boundary b keeps changing as the boundary moves, we need to be able to update its type when the assignment x := copy[..](..) moves the boundary, even though we do not know anything about the aliasing relationship between x and h.

Those same three issues appear when trying to write a Cheney-style stop&copy GC where the to region is used as a queue and needs to be scanned and redirected in very similar ways.

5.6.3 Solution

Now, using typed regions, the above problems become very easy to solve. int, ref τ, and τ_{1} × τ_{2} are defined as a CiC inductive type of kind Ω^{τ}. Region R has kind R Ω^{τ}, regions Y and O have kind R (Ω^{τ} × Ω^{τ}). The type of our variables have the shape M_{O,R,Y} (τ) defined below:

M_{O,R,A}(int)  ⇒ int
M_{O,R,A}(ref τ) ⇒ ∃n:Nat.τ at R.n
M_{O,R,A}(τ_{1} × τ_{2}) ⇒ ∃r ∈{O, A}.∃n:Nat.(τ_{1}, τ_{2}) at r.n

where (τ_{1}, τ_{2}) is a pair of kind Ω^{τ} × Ω^{τ}. The regions’s types while the mutator is running are:

\begin{matrix}Y ↦ & λn.λ(t_{1}, t_{2}).M_{O,R,Y} (t_{1}) × M_{O,R,Y} (t_{2}) \\ O ↦ & λn.λ(t_{1}, t_{2}).M_{O,R,O}(t_{1}) × M_{O,R,O}(t_{2}) \\ R ↦ & λn.λt.M_{O,R,Y} (t) \end{matrix}

(* Definition of the source-level types *)

Inductive Ω^{τ} : Kind = int : Ω^{τ} | ref : Ω^{τ} → Ω^{τ} | pair : Ω^{τ} → Ω^{τ} → Ω^{τ}

(* Mapping types from source to low-level *)

typedef M_{o,r,a}(t)

= case t of int ⇒ int | ref t ⇒ ∃n.t at r.n | pair t_{1} t_{2} ⇒ ∃b∈{o, a}.∃n.(t_{1}, t_{2}) at b.n

(* Type of a region holding tuples *)

typedef TupRgn o r a = λn.λ(t_{1}, t_{2}).M_{o,r,a}(t_{1}) × M_{o,r,a}(t_{2})

(* Type of a region holding ref cells *)
(* b:  scan boundary during redirection *)

typedef RefRgn o r y b = λn.λt.let a = if (b > n) o y in M_{o,r,a}(t)

(* Type of the continuation passed to GC *)

typedef gccont = ∀[y:R (Ω^{τ} × Ω^{τ})]{…}(x:M_{o,r,y}(t)) → 0

(* A deep copy function not shown here *)

copy : ∀[t:Ω^{τ} , b:Nat, …]{…}(M_{o,r,y}(t)) → M_{o,r,o}(t)

(* Main entry point *)
(* h is the root pointer and t its type *)

GC [t:Ω^{τ} , y, o:R (Ω^{τ} × Ω^{τ}), r : R Ω^{τ} , …]

{y ↦ (TupRgn o r y, m_{y}), o ↦ (TupRgn o r o, m_{o}), r ↦ (λn.λt.M_{o,r,y}(t), m_{r})}
(h:M_{o,r,y}(t), k:gccont)

= let h = copy[t, 0, y, r, o](h) in iffirst r then x, t′ . redirect[t, 0, t′, y, r, o](x, h, k) else GCtail[t, y, r, o](h, k)

(* Finish up GC when redirection is done *)

GCtail

[t:Ω^{τ} , y, o:R (Ω^{τ} × Ω^{τ}), r : R Ω^{τ} , …]

{y ↦ (TupRgn o r y, m_{y}), o ↦ (TupRgn o r o, m_{o}), r ↦ (RefRgn o r y m_{r}, m_{r}), }

(h:M_{o,r,o}(t), k:gccont)

= freergn y; let y = newrgn (λ :(Ω^{τ} × Ω^{τ}). int) in cast[…] r ↦ (λn.λt.M_{o,r,y}(t)); cast[…] y ↦ (TupRgn o r y); k[y](cast[…: M_{o,r,o}(t) ◁ M_{o,r,y}(t)] h)

(* Scan r, redirecting ptrs from y to o *)
(* h: root pointer to pass back to GCtail *)
(* x: boundary pointer that sweeps r *)

redirect

[t:Ω^{τ} , b:Nat, t′:Ω^{τ} , y, o:R (Ω^{τ} × Ω^{τ}), r : R Ω^{τ} , …]

{y ↦ (TupRgn o r y, m_{y}), o ↦ (TupRgn o r o, m_{o}), r ↦ (RefRgn o r y b, m_{r})}

(x:t at r.b, h:M_{o,r,o}(t′), k:gccont)

= set x :=^{M_{o,r,o}} copy[t, b, y, r, o](⟨y, get x⟩); cast[…] r ↦ (RefRgn o r y (b+1)); ifnext x then x, t . redirect[t, b+1, t′, y, r, o](x, h, k) else GCtail[t, y, r, o](h)

Figure 5.11: Generational GC where λ(t_{1}, t_{2}) is just a pattern matching short hand. While a collection copying objects from Y to O is in progress and pointers in R are redirected from Y to O, the type of Y and O stay the same, and the same M function is still used. Only R needs to be changed to the following:

R ↦ λn.λt.let r = if (m > n) (O) (Y) in M_{O,R,r}(t)

The top-level code of the GC code that copies objects from the nursery Y to the old space O is shown in figure 5.11. We omit proof arguments as well as some type parameters and use direct-style rather than continuation passing style where convenient, to make the code more readable. The code of the copy function is not shown. A copy function similar to the one presented in last chapter could be used.

Since the types of regions R, O, and Y all refer to each other, there is a problem of circularity that prevents us from creating a region with its proper type. In GCtail we show how to work around such problems using cast: we first create Y with a dummy type, and then cast it to the desired type (the proof needs to show that the new type preserves the type of all objects in the region which is trivial since the region is empty).

5.7 Related work

The calculus of capabilities [CWM99] was the first calculus to provide safe explicit memory deallocation while allowing dangling pointers. The linear handling of our regions was strongly influenced by that work.

In the work on TAL [MWCG98], the authors showed a simple way to handle the problem of separating allocation from object initialization, without resorting to any form of linearity.

Alias types [SWM00, WM00] was also a major source of inspiration for our system. It’s a type system designed specifically to handle low-level code. In that work, strong update is the only form of update available. Separating object initialization from allocation is very easy, as is explicit deallocation and memory reuse.

The Vault language [DF01] took the work on alias types and both extended it and gave it a surface syntax (so as to enable the programmer to give that needed aliasing information). In the first paper, they mostly showed how to integrate classical intuitionistic references with alias-types-style statically tracked references. They also showed that tracking references to region objects allows their system to subsume a region type system. The main difference compared to our work is that you have to choose once and for all whether a reference should be intuitionistic or linear (i.e. tracked).

In a subsequent paper [FD02] they addressed that limitation by introducing the operators adoption, which allows the user to make a linear reference intuitionistic, and focus, which does the converse. This significantly increases the expressiveness of their user-level language. Sadly, these constructs are not applicable to our GC problem for two reasons: First, adoption is too high-level for us and seems difficult to adapt to a low-level language. Second, although focus does allow some of what we need (such as strong update in the presence of arbitrary aliasing), it requires the type change to be transient, limited to the scope of the construct.

Wang and Appel [WA99] built a tracing garbage collector on top of a region-based calculus, thus providing both type safety and completely automatic memory management.

Vanderwaart and Crary [VC03] design a type system that enforces that the programs correctly manipulate the details of the stack layout required by a particular GC which is kept implicit. The system is designed for a sophisticated GC which uses a static table indexed by the return address to describe the activation frames, much like stack-walking implementations of exception handling. The type system checks among other things that the table provided by the program is correct and used consistently.

Shao et al. [SSTP02] proposed to use CiC as the type calculus of a programming language. This allows sophisticated type manipulation and enables programs to express arbitrary properties of the values manipulated. I reuse their idea with the same purpose but by virtue of the rest of the type system I can additionally capture arbitrary properties of the state.

5.8 Conclusion

I have presented a novel type system that offers an unusual flexibility to play with the typing of memory locations. This type system offers the ability to choose any mix of linear or intuitionistic typing of references and to change this choice over time to adapt it to the current needs. It is able to handle strong update of memory locations even in the presence of unknown aliasing patterns. The reliance on CiC allows very sophisticated type manipulations.

We have shown how to use it to solve the difficult problem of type checking a generational garbage collector, including proper handling of cycles and while allowing the mutator to perform destructive assignment on reference cells. We have also shown how to use this same type system to encode a stack of activation records inside a region.

Future directions

I have shown that types can be used to formalize and reason about complex properties of code such as the behavior of a code inliner and the safety of a generational garbage collector.

The language I developed for the garbage collector is very powerful and opens up many new possibilities. It is also highly unconventional and represents an intriguing point on the design space of type systems. Many future directions seem possible.

6.1 Implementation

The most immediately needed development is an actual implementation of such a language, so as to get more experience with its power and limitations. Especially challenging will be the issue of managing the cost of the heavy type annotations and the extensive use of complex operations at the type level which will have to be interpreted throughout the compilation.

Another issue with an implementation is to make the language more realistic. This mostly involves replacing put and get with alloc, load, and store which is easy enough but will also require splitting the region handles into a run-time and a compile-time component. This should not present any particular theoretical difficulty.

Finally, any reasonable implementation will need to work within the scope of a compiler for a high-level language and compiling the intermediate language into a language such as my typed regions will require substantial work. An important difficulty will be encoding recursive type in the inductive definition of Ω, for example.

6.2 Advanced uses

Another venue is to go further in the direction of encoding properties in the types. It should be easy to extend the system with singleton integers and use them to explicitly encode GC tags, forwarding pointer bits, and remembered sets. Together with a Cheney-style copy routine, it should be possible to type-check a production quality garbage collector.

Also, compiling such a typed regions system to proof carrying code could present substantial difficulties because of the power the type system I use. It seems for example impossible to prove the soundness of the language using a deep embedding into Coq.

6.3 Surface syntax

As it stands the system of typed regions can only be used by a dedicated individual with a very good understanding of sophisticated type systems.

But the way the type system is structured makes it possible to move a significant part of the complex invariants to a few specific places such as in the regions’s types, leaving most of the code uncluttered as long as it sticks to the traditional usage patterns of a simple region system. With a bit of syntactic sugar and a few special shorthands, it should be possible to make it into a usable source language. How to keep the source language understandable while still making the full power of the underlying system available is probably going to be a problem.

As mentioned before, this system represents an unusual point in the design space of type systems. It seems very likely that yet different points in the design space will prove at least equally useful. There is a lot of room for experimentation in that area as well.

Bibliography

[AJ97] Andrew W. Appel and Trevor Jim. Shrinking lambda expressions in linear time. Journal of Functional Programming, 7(5):515–540, September 1997. Princeton TR-556-97.

[AM91] Andrew W. Appel and David B. MacQueen. Standard ML of New Jersey. In Martin Wirsing, editor, Third Int’l Symp. on Prog. Lang. Implementation and Logic Programming, pages 1–13, New York, August 1991. Springer-Verlag.

[App91] Andrew W. Appel. Compiling with Continuations. Cambridge University Press, 1991. ISBN 0-521-41695-7.

[App94] Andrew W. Appel. Loop headers in lambda-calculus or CPS. Lisp and Symbolic Computation, 7:337–343, June 1994. Princeton TR-460-94.

[App01] Andrew W. Appel. Foundational proof-carrying code. In Annual Symposium on Logic in Computer Science, pages 247–258, June 2001.

[Asa99] Kenichi Asai. Binding-time analysis for both static and dynamic expressions. In Static Analysis Symposium, pages 117–133, September 1999.

[Ash97] Michael J. Ashley. The effectiveness of flow analysis for inlining. In International Conference on Functional Programming, pages 99–111. ACM Press, June 1997.

[BA97] Matthias Blume and Andrew W. Appel. Lambda-splitting: A higher-order approach to cross-module optimizations. In International Conference on Functional Programming, pages 112–124. ACM Press, June 1997.

[Bar91] Henk P. Barendregt. Lambda calculi with types. In S. Abramsky, D. Gabbay, and T. Maibaum, editors, Handbook of Logic in Computer Science (volume 2). Oxford Univ. Press, 1991.

[BC99] Guy Blelloch and Perry Cheng. On bounding time and space for multiprocessor garbage collection. In Symposium on Programming Languages Design and Implementation, pages 104–107. ACM Press, May 1999.

[BKR98] Nick Benton, Andrew Kennedy, and George Russel. Compiling Standard ML to Java bytecodes. In International Conference on Functional Programming, pages 129–140. ACM Press, September 1998.

[Blu95] Matthias Blume. Standard ML of New Jersey Compilation Manager, 1995. Manual accompanying SML/NJ software.

[BW95] Lars Birkedal and Morten Welinder. Binding-time analysis for Standard ML. Lisp and Symbolic Computation, 8(3):191–208, September 1995.

[CH88] Thierry Coquand and Gérard P. Huet. The calculus of constructions. Information and Computation, 76:95–120, 1988.

[CN96] Charles Consel and Francois Noël. A general approach for run-time specialization, and its application to C. In Symposium on Principles of Programming Languages, pages 145–156. ACM Press, January 1996.

[Con93] Charles Consel. Polyvariant binding-time analysis for applicative languages. In Symposium on Partial Evaluation and Semantics-Based Program Manipulation, pages 145–154, 1993.

[Cra00] Karl Crary. Typed assembly language: Type theory for machine code. Talk presented at 2000 PCC Workshop, Santa Barbara, CA, June 2000.

[CW99] Karl Crary and Stephanie Weirich. Flexible type analysis. In International Conference on Functional Programming, pages 233–248. ACM Press, September 1999.

[CWM98] Karl Crary, Stephanie Weirich, and Greg Morrisett. Intensional polymorphism in type-erasure semantics. In International Conference on Functional Programming, pages 301–312. ACM Press, September 1998.

[CWM99] Karl Crary, David Walker, and Greg Morrisett. Typed memory management in a calculus of capabilities. In Symposium on Principles of Programming Languages, pages 262–275, San Antonio, TX, January 1999.

[Dan96] Olivier Danvy. Type-directed partial evaluation. In Symposium on Principles of Programming Languages, pages 242–257. ACM Press, January 1996.

[Dav96] Rowan Davies. A temporal-logic approach to binding-time analysis. In 11^{th} Annual Symposium on Logic in Computer Science, pages 184–195, July 1996.

[DF92] Olivier Danvy and Andrzej Filinski. Representing control, a study of the CPS transformation. Mathematical Structures in Computer Science, 2(4):361–391, 1992.

[DF01] Robert DeLine and Manuel Fähndrich. Enforcing high-level protocols in lowlevel software. In Symposium on Programming Languages Design and Implementation. ACM Press, May 2001.

[DL92] Olivier Danvy and Julia L. Lawall. Back to direct style II: First-class continuations. In Conference on Lisp and Functional Programming, pages 299–310, San Francisco, CA, 1992.

[DMP96] Olivier Danvy, Karoline Malmkjær, and Jens Palsberg. Eta-expansion does The Trick. Transactions on Programming Languages and Systems, 8(6):730– 751, 1996.

[DP96] Rowan Davies and Frank Pfenning. A modal analysis of staged computation. In Symposium on Principles of Programming Languages, pages 258–270. ACM Press, January 1996.

[FD02] Manuel Fähndrich and Robert DeLine. Adoption and focus: Practical linear types for imperative programming. In Symposium on Programming Languages Design and Implementation. ACM Press, May 2002.

[FFA99] Jeffrey S. Foster, Manuel Fähndrich, and Alexander Aiken. A theory of qualified types. In Symposium on Programming Languages Design and Implementation, pages 192–203. ACM Press, May 1999.

[FSDF93] Cormac Flanagan, Amr Sabry, Bruce F. Duba, and Matthias Felleisen. The essence of compiling with continuations. In Symposium on Programming Languages Design and Implementation, pages 237–247, New York, June 1993. ACM Press.

[Gir72] J. Y. Girard. Interprétation Fonctionnelle et Élimination des Coupures dans l’Arithmétique d’Ordre Supérieur. PhD thesis, University of Paris VII, 1972.

[GSSS01] Kevin Glynn, Peter J. Stuckey, Martin Sulzmann, and Harald Sndergaard. Boolean constraints for binding-time analysis. In Program as Data Objects, pages 39–62, 2001.

[GST01] Steve Ganz, Amr Sabry, and Walid Taha. Macros as multi-stage computations: Type-safe, generative, binding macros in macroml. In International Conference on Functional Programming. ACM Press, September 2001.

[HM94] Fritz Henglein and Christian Mossin. Polymorphic binding-time analysis. In European Symposium on Programming, pages 287–301, Edinburgh, UK, April 1994.

[HM95] Robert Harper and Greg Morrisett. Compiling polymorphism using intensional type analysis. In Symposium on Principles of Programming Languages, pages 130–141, January 1995.

[HMM90] Robert Harper, John C. Mitchell, and Eugenio Moggi. Higher-order modules and the phase distinction. In Symposium on Principles of Programming Languages, pages 341–344, New York, January 1990. ACM Press.

[Hoa69] C. A. R. Hoare. An axiomatic basis for computer programming. Communications of the ACM, 12(10):576–580, October 1969.

[How80] W. A. Howard. The formulae-as-types notion of constructions. In To H.B.Curry: Essays on Computational Logic, Lambda Calculus and Formalism. Academic Press, 1980.

[HPM^{+}00] Gerard Huet, Christine Paulin-Mohring, et al. The Coq proof assistant reference manual. Part of the Coq system version 6.3.1, May 2000.

[HST^{+}02] Nadeem Abdul Hamid, Zhong Shao, Valery Trifonov, Stefan Monnier, and Zhaozhong Ni. A syntactic approach to foundational proof-carrying code. In Annual Symposium on Logic in Computer Science, pages 89–100, Copenhagen, Denmark, July 2002.

[JGS93] Neil D. Jones, Carsten K. Gomard, and Peter Sestoft. Partial Evaluation and Automatic Program Generations. Prentice Hall International, 1993.

[Ler92] Xavier Leroy. Unboxed objects and polymorphic typing. In Symposium on Principles of Programming Languages, pages 177–188, January 1992.

[Ler95] Xavier Leroy. Le système Caml Special Light: Modules et compilation efficace en Caml. Technical Report 2721, Institut National de Recherche en Informatique et Automatique, nov 1995.

[MBS99] Stefan Monnier, Matthias Blume, and Zhong Shao. Cross-functor inlining in FLINT. Technical Report YALEU/DCS/TR-1189, Dept. of Computer Science, Yale University, New Haven, CT, March 1999.

[MCB99] Renaud Marlet, Charles Consel, and Philippe Boinot. Efficient incremental run-time specialization for free. In Symposium on Programming Languages Design and Implementation, pages 281–292. ACM Press, May 1999.

[MG98] Yasuhiko Minamide and Jacques Garrigue. On the runtime complexity of typedirected unboxing. In International Conference on Functional Programming, pages 1–12. ACM Press, September 1998.

[MH97] Greg Morrisett and Robert Harper. A comprehensive treatment of typed closure conversion. In Higher Order Operational Techniques in Semantics, Stanford, CA, December 1997.

[MMH96] Yasuhiko Minimide, Greg Morrisett, and Robert Harper. Typed closure conversion. In Symposium on Principles of Programming Languages, pages 271–283. ACM Press, January 1996.

[Mog88] Eugenio Moggi. Computational lambda-calculus and monads. Technical Report ECS-LFCS-88-86, University of Edinburgh, 1988.

[Mog97] Eugenio Moggi. A categorical account of two-level languages. In 13^{th} Conference on the Mathematical Foundations of Programming Semantics, March 1997.

[Mor00] Greg Morrisett. Open problems for certifying compilers. Talk presented at 2000 PCC Workshop, Santa Barbara, CA, June 2000.

[MTBS99] Eugenio Moggi, Walid Taha, Zine El-Abidine Benaissa, and Tim Sheard. An idealized MetaML: simpler, and more expressive. In European Symposium on Programming, pages 193–207, Amsterdam, The Netherlands, March 1999.

[MTHM97] Robin Milner, Mads Tofte, Robert Harper, and David B. MacQueen. The Definition of Standard ML Revised. MIT Press, Cambridge, Massachusetts, 1997.

[MWCG98] Greg Morrisett, David Walker, Karl Crary, and Neal Glew. From system F to typed assembly language. In Symposium on Principles of Programming Languages, pages 85–97, San Diego, CA, January 1998.

[Nec97] George C. Necula. Proof-carrying code. In Symposium on Principles of Programming Languages, January 1997.

[NL98] George Necula and Peter Lee. The design and implementation of a certifying compiler. In Symposium on Programming Languages Design and Implementation, pages 333–344, New York, NY, June 1998. ACM Press.

[NN92] Flemming Nielson and Hanne Riis Nielson. Two-level functional languages. Cambridge University Press, New York, 1992.

[NO93] Scott Nettles and James O’Toole. Real-time replication garbage collection. In Symposium on Programming Languages Design and Implementation, New York, June 1993. ACM Press.

[Pau93] C. Paulin-Mohring. Inductive definitions in the system Coq—rules and properties. In M. Bezem and J. Groote, editors, Proc. TLCA. LNCS 664, Springer-Verlag, 1993.

[PM99] Simon Peyton Jones and Simon Marlow. Secrets of the Glasgow Haskell Compiler inliner. In Proceedings of the International Workshop on Implementation of Declarative Languages, Paris, France, September 1999.

[PMar] Simon Peyton Jones and Simon Marlow. Secrets of the glasgow haskell compiler inliner. Journal of Functional Programming, to appear.

[PS98] Simon Peyton Jones and André L M Santos. A transformation-based optimiser for haskell. Science of Computer Programming, 32(1-3):3–47, September 1998.

[Rey72] John C. Reynolds. Definitional interpreters for higher-order programming languages. In Proceedings of the ACM annual conference, pages 717–740, 1972.

[Rey74] John C. Reynolds. Towards a theory of type structure. In Proceedings, Colloque sur la Programmation, Lecture Notes in Computer Science, volume 19, pages 408–425. Springer-Verlag, Berlin, 1974.

[SA94] Zhong Shao and Andrew W. Appel. Space-efficient closure representations. In Proc. 1994 ACM Conf. on Lisp and Functional Programming, pages 150–161, Orlando, FL, June 1994.

[SA95] Zhong Shao and Andrew W. Appel. A type-based compiler for Standard ML. In Symposium on Programming Languages Design and Implementation, pages 116–129, La Jolla, CA, June 1995. ACM Press.

[Sha97a] Zhong Shao. Flexible representation analysis. In International Conference on Functional Programming, pages 85–98. ACM Press, June 1997.

[Sha97b] Zhong Shao. An overview of the FLINT/ML compiler. In International Workshop on Types in Compilation, June 1997.

[Sha98] Zhong Shao. Typed cross-module compilation. In International Conference on Functional Programming, pages 141–152. ACM Press, September 1998.

[Sha99] Zhong Shao. Transparent modules with fully syntactic signatures. In International Conference on Functional Programming, pages 220–232, Paris, France, September 1999. ACM Press.

[Shi91] Olin Shivers. Control-Flow Analysis of Higher-Order Languages. PhD thesis, Carnegie Mellon University, May 1991. CMU-CS-91-145.

[SLM98] Zhong Shao, Christopher League, and Stefan Monnier. Implementing typed intermediate languages. In International Conference on Functional Programming, pages 313–323. ACM Press, September 1998.

[SS98] Bratin Saha and Zhong Shao. Optimal type lifting. In International Workshop on Types in Compilation, March 1998.

[SSTP02] Zhong Shao, Bratin Saha, Valery Trifonov, and Nikolaos Papaspyrou. A type system for certified binaries. In Symposium on Principles of Programming Languages, pages 217–232, Portland, OR, January 2002.

[ST96] Michael Sperber and Peter Thiemann. Realistic compilation by partial evaluation. In Symposium on Programming Languages Design and Implementation, pages 206–214, Philadelphia, PA, May 1996. ACM Press.

[ST97] Michael Sperber and Peter Thiemann. Two for the price of one: Composing partial evaluation and compilation. In Symposium on Programming Languages Design and Implementation, pages 215–225. ACM Press, June 1997.

[STS00] Bratin Saha, Valery Trifonov, and Zhong Shao. Fully reflexive intensional type analysis. Technical Report YALEU/DCS/TR-1194, Dept. of Computer Science, Yale University, New Haven, CT, March 2000.

[SWM00] Frederick Smith, David Walker, and Greg Morrisett. Alias types. In European Symposium on Programming, 2000.

[Tah99] Walid Taha. Multi-Stage Programming: Its Theory and Applications. PhD thesis, Oregon Graduate Institute, Beaverton, Oregon, November 1999.

[Tar96] David Tarditi. Design and Implementation of Code Optimizations for a Type-Directed Compiler for Standard ML. PhD thesis, Carnegie Mellon University, December 1996. CMU-CS-97-108.

[TMC^{+}96] David Tarditi, Greg Morrisett, Perry Cheng, Christopher Stone, Robert Harper, and Peter Lee. TIL: A type-directed optimizing compiler for ML. In Symposium on Programming Languages Design and Implementation, pages 181–192, Philadelphia, PA, May 1996. ACM Press.

[TO98] Andrew Tolmach and Dino P. Oliva. From ML to Ada: Strongly-typed language interoperability via source translation. Journal of Functional Programming, 8(4):367–412, July 1998.

[TS97] Walid Taha and Tim Sheard. Multi-stage programming with explicit annotations. In Symposium on Partial Evaluation and Semantics-Based Program Manipulation, pages 203–217, June 1997.

[TSS00] Valery Trifonov, Bratin Saha, and Zhong Shao. Fully reflexive intensional type analysis. In International Conference on Functional Programming, pages 82–93. ACM Press, September 2000.

[TT94] Mads Tofte and Jean-Pierre Talpin. Implementation of the typed call-by-value λ-calculus using a stack of regions. In Symposium on Principles of Programming Languages, pages 188–201, January 1994.

[VC03] Joseph C. Vanderwaart and Karl Crary. A typed interface for garbage collection. In Types in Language Design and Implementation, New Orleans, LA, January 2003.

[WA99] Daniel C. Wang and Andrew W. Appel. Safe garbage collection = regions + intensional type analysis. Technical Report TR-609-99, Princeton University, 1999.

[WA00] Daniel C. Wang and Andrew W. Appel. Type-preserving garbage collectors (extended version). Technical Report TR-624-00, Princeton University, 2000.

[WA01] Daniel C. Wang and Andrew W. Appel. Type-preserving garbage collectors. In Symposium on Principles of Programming Languages, January 2001.

[WB89] Philip Wadler and Stephen Blott. How to make ad-hoc polymorphism less ad hoc. In Symposium on Principles of Programming Languages, Austin, TX, January 1989.

[Wer94] Benjamin Werner. Une Théorie des Constructions Inductives. PhD thesis, A L’Université Paris 7, Paris, France, 1994.

[Wil92] Paul R. Wilson. Uniprocessor garbage collection techniques. In International Workshop on Memory Management, September 1992.

[WM00] David Walker and Greg Morrisett. Alias types for recursive data structures. In International Workshop on Types in Compilation, August 2000.

Appendix A

Soundness proofs

A.1 Soundness of principled scavenging

Throughout this section, I assume unique variable names. The environments are sets with no duplicate occurrences and no ordering. It is easy to show by induction over judgments that extending environments with additional bindings is safe. I will assume this in the rest of the section.

A.1.1 Soundness of λ_{GC}

The code region cd is always implicitly part of the environment. We treat it as a constant region. Even when the environment is restricted to a particular set, say Ψ|_{Θ}, the code region is included in the restricted set. Therefore Ψ|_{ν_{1},...ν_{k}} is equivalent to {cd:Υ_{cd}, ν_{1} :Υ_{ν_{1}}, … ν_{k}:Υ_{ν_{k}}}. And Ψ|_{cd} is equivalent to {cd:Υ_{cd}}.

Lemma A.1.1 If Θ′, r; Δ; Φ ⊢ σ, then Θ[ν/r]; Δ; Φ[ν/r] ⊢ σ[ν/r] where Θ′, r = Θ.

Proof The proof is a straightforward induction over the structure of σ. □

Lemma A.1.2 (Φ[ν/r])|_{Θ,ν} = (Φ|_{Θ,r})[ν/r] and (Γ[ν/r])|_{Θ,ν} = (Γ|_{Θ,r})[ν/r]

Proof The lemma is proved by considering the structure of Φ and Γ respectively, and comparing the results in the two cases. □

Lemma A.1.3 If Ψ; Θ′, r; Δ; Φ; Γ ⊢ op : σ, then Ψ; Θ[ν/r]; Δ; Φ[ν/r]; Γ[ν/r] ⊢ op[ν/r] : σ[ν/r] where Θ′, r = Θ.

Proof The proof is by induction over the structure of op. Most of the cases follow directly by induction. We will show only the case for type packages. case ⟨α:Θ_{1} =σ_{1}, v:σ_{2}⟩: We know that

Ψ; Θ′, r; Δ; Φ; Γ ⊢ ⟨α:Θ_{1} =σ_{1}, v:σ_{2}⟩ : ∃α:Θ_{1}.σ_{2}

This implies that Θ_{1}; Δ; Φ|_{Θ_{1}} ⊢ σ_{1} and

Ψ; Θ′, r; Δ; Φ; Γ ⊢ v : σ_{2}[σ^{1}/α]

Suppose r ∉ Θ_{1}. Then r does not occur free in σ_{1}. Then applying the inductive hypothesis to the derivation for v, we get that

Ψ; Θ[ν/r]; Δ; Φ[ν/r]; Γ[ν/r] ⊢ v[ν/r] : (σ_{2}[ν/r])[σ^{1}/α]

Suppose Φ_{1} = Φ|_{Θ_{1}}. Then we have that Φ[ν/r]|_{Θ_{1}} = Φ_{1}, Φ_{2} and Dom(Φ_{1}) ∩ Dom(Φ_{2}) = ∅. Therefore, we have that

Θ_{1}; Δ; Φ|_{Θ_{1}}, Φ_{2} ⊢ σ_{1}

This implies that Θ_{1}; Δ; Φ[ν/r]|_{Θ_{1}} ⊢ σ_{1}, which leads to the required result. Consider now that r ∈ Θ_{1}. Suppose Θ_{1} = Θ_{2}, r. Then

Θ_{2}, r; Δ; Φ|_{Θ_{2},r} ⊢ σ_{1}

Applying lemmas A.1.1 and A.1.2 we get that

Θ_{1}[ν/r]; Δ; (Φ[ν/r])|_{Θ_{2},ν} ⊢ σ_{1}[ν/r]

But Θ_{2}, ν = Θ_{1}[ν/r]. The second subderivation now becomes

Ψ; Θ′, r; Δ; Φ; Γ ⊢ v : σ_{2}[σ^{1}/α]

By applying the inductive hypothesis we get that

Ψ; Θ[ν/r]; Δ; Φ[ν/r]; Γ[ν/r] ⊢ v[ν/r] : (σ_{2}[ν/r])[σ^{1}[ν/r]/α]

This leads to the required result. □

Lemma A.1.4 If Ψ; Θ′, r; Δ; Φ; Γ ⊢ e, then Ψ; Θ[ν/r]; Δ; Φ[ν/r]; Γ[ν/r] ⊢ e[ν/r] where Θ′, r = Θ

Proof The proof is by induction over the derivation of e. Most of the cases follow directly from the inductive hypothesis. We will consider only one case here. case only Θ_{1} in e: We get that

Ψ; Θ, r; Δ; Φ; Γ ⊢ only Θ_{1} in e

This implies that

Ψ|_{Θ_{1}}; Θ_{1}, cd; Δ; Φ|_{Θ_{1}}; Γ|_{Θ_{1}} ⊢ e

and Θ_{1} ⊂ Θ, r. Suppose r ∉ Θ_{1}. Then r does not occur free in e. Also Θ_{1}[ν/r] = Θ_{1}. Suppose Γ|_{Θ_{1}} = Γ_{1}. Then we have that Γ[ν/r]|_{Θ_{1}} = Γ_{1}, Γ_{2} and Dom(Γ_{1}) ∩ Dom(Γ_{2}) = ∅. Since we can extend environments, we get that Ψ|_{Θ_{1}}; Θ_{1}, cd; Δ; Φ_{1}, Φ_{2}; Γ_{1}, Γ_{2} ⊢ e, where Φ_{1} and Φ_{2} are constructed similar to Γ_{1} and Γ_{2}. This implies that

Ψ|_{Θ_{1}}; Θ_{1}, cd; Δ; Φ_{1}, Φ_{2}; Γ_{1}, Γ_{2} ⊢ e

Also Θ_{1} ⊂ (Θ, r)[ν/r]. This leads to the required result.

APPENDIX A. SOUNDNESS PROOFS  153

Suppose now that r ∈ Θ_{1}. Suppose that Θ_{1} = Θ_{2}, r. Then Θ_{1}[ν/r] = Θ_{2}, ν. Then we have that

Ψ|_{Θ_{2},r}; Θ_{2}, r, cd; Δ; Φ|_{Θ_{2},r}; Γ|_{Θ_{2},r} ⊢ e

Applying the inductive hypothesis we get that

Ψ|_{Θ_{2},r}; Θ_{1}[ν/r], cd; Δ; Φ|_{Θ_{2},r}[ν/r]; Γ|_{Θ_{2},r}[ν/r] ⊢ e[ν/r]

Applying lemma A.1.2 we get that

Ψ|_{Θ_{2},r}; Θ_{1}[ν/r], cd; Δ; Φ[ν/r]|_{Θ_{2},ν}; Γ[ν/r]|_{Θ_{2},ν} ⊢ e[ν/r]

But we have that Ψ|_{Θ_{2},r} = Ψ|_{Θ_{2}}. Moreover, Ψ|_{Θ_{2},ν} = Ψ|_{Θ_{2}}, Ψ′. Therefore, we get that

Ψ|_{Θ_{2},ν}; Θ_{1}[ν/r], cd; Δ; Φ[ν/r]|_{Θ_{2},ν}; Γ[ν/r]|_{Θ_{2},ν} ⊢ e[ν/r]

We also have that Θ_{1}[ν/r] ⊂ Θ[ν/r]. This leads to the required result. □ Lemma A.1.5 If Δ, t:κ′ ⊢ τ : κ and Δ ⊢ τ′ : κ′, then Δ ⊢ τ[τ′/t] : κ Proof The proof is a straightforward induction over the structure of τ. □ Lemma A.1.6 If Θ; Δ, t:κ; Φ ⊢ σ and Δ ⊢ τ : κ, then Θ; Δ; Φ ⊢ σ[τ/t] Proof The proof is again a straighforward induction over the structure of σ. □ Lemma A.1.7 If Ψ; Θ; Δ, t:κ; Φ; Γ ⊢ op : σ and Δ ⊢ τ : κ then Ψ; Θ; Δ; Φ; Γ[τ/t] ⊢ op[τ/t] : σ[τ/t] Proof The proof is a straightforward induction over the structure of op. The only unusual case is when op = ν.ℓ. In this case, Ψ(ν.ℓ) = σ and Dom(Ψ); ·; · ⊢ σ. Therefore, the variable t does not occur free in σ at ν. □ Lemma A.1.8 If Ψ; Θ; Δ, t:κ; Φ; Γ ⊢ e and · ⊢ τ′ : κ then Ψ; Θ; Δ; Φ; Γ[τ′/t] ⊢ e[τ′/t] Proof The proof is a straightforward induction over the structure of e. The only interesting case is for a tcase when the substituted variable is being analyzed. case tcase t of (e_{i}; e_{→}; t_{1}t_{2}.e_{×}; t_{e}.e_{∃}): Suppose we substitute the type τ′ for the variable t. Then τ′ can only be one of Int, τ′′ → 0, τ′_{1} × τ′_{2}, or ∃t.τ′′. For a Int, we need to prove that

Ψ; Θ; Δ; Φ; Γ[Int/t] ⊢ (tcase t of (e_{i}; e_{→}; t_{1}t_{2}.e_{×}; t_{e}.e_{∃}))[Int/t]

This implies that we need to prove that

Ψ; Θ; Δ; Φ; Γ[Int/t] ⊢ e_{i}[Int/t]

By definition, we know that

Ψ; Θ; Δ, t:Ω; Φ; Γ[Int/t] ⊢ e_{i}[Int/t]

Since t is being substituted away, this leads to the required result. For a code type we need to prove that

Ψ; Θ; Δ; Φ; Γ[τ′ → 0/t] ⊢
(tcase t of (e_{i}; e_{→}; t_{1}t_{2}.e_{×}; t_{e}.e_{∃}))[τ′ → 0/t]

This implies that we need to prove that

Ψ; Θ; Δ; Φ; Γ[τ′ → 0/t] ⊢ e_{→}[τ′ → 0/t]

By definition, we get that Ψ; Θ; Δ, t:Ω; Φ; Γ ⊢ e_{→}. Substituting for t and applying the inductive hypothesis leads to the result. For the pair type we need to prove that

Ψ; Θ; Δ; Φ; Γ[(τ ′1 ×^{ρ} τ ′2)/t] ⊢
(tcase t of (e_{i}; e_{→}; t_{1}t_{2}.e_{×}; t_{e}.e_{∃}))[(τ ′1 ×^{ρ} τ ′2)/t]

This implies that we need to prove that

Ψ; Θ; Δ; Φ; Γ[(τ ′1 ×^{ρ} τ ′2)/t] ⊢ e_{×}[(τ ′1 ×^{ρ} τ ′2), τ ′1, τ ′2/t, t_{1}, t_{2}]

By definition, we know that

Ψ; Θ; Δ, t:Ω, t_{1} :Ω, t_{2} :Ω; Φ; Γ[t^{1} ×^{r} t^{2}/t] ⊢ e_{×}[t^{1} ×^{r} t^{2}/t]

Note that the variables t_{1} and t_{2} do not occur free separately in Γ. Substituting τ′_{1} for t_{1}, τ′_{2} for t_{2}, and τ′_{1} ×^{r} τ′_{2} for t_{1} ×^{r} t_{2} leads to the required result. For the existential type we need to prove that

Ψ; Θ; Δ; Φ; Γ[∃t^{1}.τ′/t] ⊢
(tcase t of (e_{i}; e_{→}; t_{1}t_{2}.e_{×}; t_{e}.e_{∃}))[∃t^{1}.τ′/t]

This implies that we need to prove that

Ψ; Θ; Δ; Φ; Γ[∃t^{1}.τ′/t] ⊢ e_{∃}[∃t^{1}.τ′, λt^{1}.τ′/t, t_{e}]

By definition we know that

Ψ; Θ; Δ, t:Ω, t_{e}:Ω → Ω; Φ; Γ[∃t^{1}.t^{e}t^{1}/t] ⊢ e_{∃}[∃t^{1}.t^{e}t^{1}/t]

Substituting (λt_{1}.τ′) for t_{e} and applying the inductive hypothesis we get that

Ψ; Θ; Δ, t:Ω; Φ; Γ[∃t^{1}.τ′/t] ⊢ e_{∃}[∃t^{1}.τ′, λt^{1}.τ′/t, t_{e}]

Since t is being substituted away, we can remove it from the type environment. This leads to the required result. □

Lemma A.1.9 If Θ; Δ; Φ, α:Θ′ ⊢ σ and Θ′; Δ; Φ ⊢ σ′, then Θ; Δ; Φ ⊢ σ[σ′/α] Proof The proof is a straighforward induction over the structure of σ. In the case of c‘ode types, we use the fact that the argument types σ⃗ are fully closed. □

Lemma A.1.10 If Ψ; Θ; Δ; Φ, α:Θ′; Γ ⊢ op : σ and Θ′; Δ; Φ ⊢ σ′ then Ψ; Θ; Δ; Φ; Γ[σ′/α] ⊢ op[σ′/α] : σ[σ′/α]

Proof The proof is again by induction over the typing derivation for op. We will consider only the case for packages. case ⟨β:Θ′ =σ_{1}, v:σ_{2}⟩: By definition,

Ψ; Θ; Δ; Φ, α:Θ_{1}; Γ ⊢ ⟨β:Θ′ =σ_{1}, v:σ_{2}⟩ : ∃β:Θ′.σ_{2}

There are two possible cases. If Θ_{1} ⊂ Θ′, then we get that Θ′; Δ; Φ|_{Θ′}, α:Θ_{1} ⊢ σ_{1} and Ψ; Θ; Δ; Φ, α:Θ_{1}; Γ ⊢ v : σ_{2}[σ^{1}/β] By lemma A.1.9, Θ′; Δ; Φ|_{Θ′} ⊢ σ_{1}[σ′/α] Applying the inductive hypothesis on the typing rule for v leads to the result. In the other case, we get that Θ′; Δ; Φ|_{Θ′} ⊢ σ_{1} and Ψ; Θ; Δ; Φ, α:Θ_{1}; Γ ⊢ v : σ_{2}[σ^{1}/β] This implies that α does not occur free in σ_{1}. Therefore, we need to prove that

\begin{matrix}Ψ; Θ; Δ; Φ; Γ[σ′/α] ⊢ \\ ⟨β:Θ′ =σ_{1}, v[σ′/α]:σ_{2}[σ′/α]⟩ : ∃β:Θ′.σ_{2}[σ′/α] \end{matrix}

This follows from applying the inductive hypothesis to the judgment for v. □

Lemma A.1.11 If Θ; Δ; Φ ⊢ σ[σ′/α] and α occurs free in σ, then Θ; Δ; Φ ⊢ σ′

Proof The proof is by induction over the structure of σ. □

Lemma A.1.12 If Ψ; Θ; Δ; Φ, α:Θ′; Γ ⊢ e and Θ′; Δ; Φ ⊢ σ′ then Ψ; Θ; Δ; Φ; Γ[σ′/α] ⊢ e[σ′/α]

Proof The proof is again by induction over the derivation. The only non-trivial case is the only construct. case only Θ_{1} in e: By definition, Ψ; Θ; Δ; Φ, α:Θ′; Γ ⊢ only Θ_{1} in e and Θ′; Δ; Φ ⊢ σ′. Suppose Θ′ ⊂ Θ_{1}. Then we get that

Ψ|_{Θ_{1}}; Θ_{1}, cd; Δ; Φ|_{Θ_{1}}, α:Θ′; Γ|_{Θ_{1}} ⊢ e

Applying the inductive hypothesis we get that

Ψ|_{Θ_{1}}; Θ_{1}, cd; Δ; Φ|_{Θ_{1}}; Γ|_{Θ_{1}}[σ′/α] ⊢ e[σ′/α]

But we also have that Γ|_{Θ_{1}}[σ′/α] = Γ[σ′/α]|_{Θ_{1}}. From here we can conclude that

Ψ; Θ; Δ; Φ; Γ[σ′/α] ⊢ only Θ_{1} in e[σ′/α]

If Θ_{1} ⊂ Θ′, then we get that

Ψ|_{Θ_{1}}; Θ_{1}, cd; Δ; Φ|_{Θ_{1}}; Γ|_{Θ_{1}} ⊢ e

This implies that α does not occur free in e. We also have that Θ′; Δ; Φ ⊢ σ′. Using lemma A.1.11, we can show that Γ[σ′/α]|_{Θ_{1}} = Γ|_{Θ_{1}}. Therefore, we get that

Ψ|_{Θ_{1}}; Θ_{1}, cd; Δ; Φ|_{Θ_{1}}; Γ[σ′/α]|_{Θ_{1}} ⊢ e

This implies that

Ψ; Θ; Δ; Φ; Γ[σ′/α] ⊢ only Θ_{1} in e
□

Lemma A.1.13 If Ψ; Θ; Δ; Φ; Γ, x:σ′ ⊢ op : σ and Ψ; Θ; Δ; Φ; Γ ⊢ v′ : σ′ then Ψ; Θ; Δ; Φ; Γ ⊢ op[v′/x] : σ

Proof The proof is a straightforward induction over the typing derivation for op. □

Lemma A.1.14 If Ψ; Θ; Δ; Φ; Γ ⊢ v : σ and Θ_{1}; Δ; Φ|_{Θ_{1}} ⊢ σ and Θ_{1} ⊂ Θ, then Ψ|_{Θ_{1}}; Θ_{1}; Δ; Φ|_{Θ_{1}}; Γ|_{Θ_{1}} ⊢ v : σ

Proof The proof is by induction over the derivation for v. Most of the cases follow directly from the inductive hypothesis. We will consider only one case here. case ν.ℓ: We have that Ψ; Θ; Δ; Φ; Γ ⊢ ν.ℓ : σ at ν. This implies that Ψ(ν.ℓ) = σ and Dom(Ψ); ·; · ⊢ σ at ν. However, by assumption we also know that Θ_{1}; Δ; Φ|_{Θ_{1}} ⊢ σ at ν. This implies that ν ∈ Θ_{1}. This implies that Ψ|_{Θ_{1}}(ν.ℓ) = σ. Moreover, we also get that Θ_{1}; ·; · ⊢ σ at ν. Therefore, we get that Dom(Ψ|_{Θ_{1}}); ·; · ⊢ σ at ν. From here we get that Ψ|_{Θ_{1}}; Θ_{1}; Δ; Φ|_{Θ_{1}}; Γ|_{Θ_{1}} ⊢ ν.ℓ : σ at ν. □

Lemma A.1.15 If Ψ; Θ; Δ; Φ; Γ, x:σ ⊢ e and Ψ; Θ; Δ; Φ; Γ ⊢ v : σ then Ψ; Θ; Δ; Φ; Γ ⊢ e[v/x]

Proof The proof is again a straightforward induction over the structure of e. We will only show the proof for a couple of cases, the rest of them follow similarly. case only Θ in e: We have that Ψ; Θ; Δ; Φ; Γ, x:σ ⊢ only Θ_{1} in e. This implies that Ψ|_{Θ_{1}}; Θ_{1}; Δ; Φ|_{Θ_{1}}; (Γ, x:σ)|_{Θ_{1}} ⊢ e. If we have that Θ_{1}; Δ; Φ|_{Θ_{1}} ⊢ σ, then we get that Ψ|_{Θ_{1}}; Θ_{1}; Δ; Φ|_{Θ_{1}}; Γ|_{Θ_{1}}, x:σ ⊢ e. By lemma A.1.14 we get that Ψ|_{Θ_{1}}; Θ_{1}; Δ; Φ|_{Θ_{1}}; Γ|_{Θ_{1}} ⊢ v : σ. Applying the inductive hypothesis gives us that Ψ|_{Θ_{1}}; Θ_{1}; Δ; Φ|_{Θ_{1}}; Γ|_{Θ_{1}} ⊢ e[v/x]. In the other case, we get that Ψ|_{Θ_{1}}; Θ_{1}; Δ; Φ|_{Θ_{1}}; Γ|_{Θ_{1}} ⊢ e. This implies that x does not occur free in e. The required result follows from here. case tcase t of (e_{i}; e_{→}; t_{1}t_{2}.e_{×}; t_{e}.e_{∃}): By assumption, we get that Δ ⊢ t : Ω Ψ; Θ; Δ; Φ; Γ[Int/t], x:σ[Int/t] ⊢ e_{i}[Int/t] Ψ; Θ; Δ; Φ; Γ, x:σ ⊢ e_{→}

\begin{matrix}Ψ; Θ; Δ, t_{1} :Ω, t_{2} :Ω; Φ; Γ[t^{1} ×^{r} t^{2}/t], x:σ[t^{1} ×^{r} t^{2}/t] ⊢ \\ e_{×}[t^{1} ×^{r} t^{2}/t] \end{matrix}

\begin{matrix}Ψ; Θ; Δ, t_{e}:Ω → Ω; Φ; Γ[∃t^{1}.t^{e}t^{1}/t], x:σ[∃t^{1}.t^{e}t^{1}/t] ⊢ \\ e_{∃}[∃t^{1}.t^{e}t^{1}/t] \end{matrix}

By lemma A.1.7, we know that if Ψ; Θ; Δ; Φ; Γ ⊢ v : σ, then Ψ; Θ; Δ; Φ; Γ[τ/t] ⊢ v : σ[τ/t]. Now substitute v[Int/t] in the e_{i} branch, substitute v in the e_{→} branch, substitute v[t^{1} ×^{r} t^{2}/t] in the e_{×} branch, and substitute v[∃t^{1}.t^{e}t^{1}/t] in the e_{∃} branch. The required result follows from the inductive hypothesis on each branch. □

Proposition A.1.16 (Type Preservation) If ⊢ (M, e) and (M, e) ⟹ (M′, e′) then ⊢ (M′, e′). Proof The proof is by induction over the evaluation relation. We will consider only the cases that do not follow directly from the inductive hypothesis and the substitution lemmas. case ν.ℓ[τ⃗][ν⃗](v⃗): The lemma follows from the fact that tag reduction is strongly normalizing and confluent, and that tag reduction preserves kind. case ν.ℓ[τ⃗′][ν⃗](v⃗): By definition,

Ψ; Dom(Ψ); ·; ·; · ⊢ ν.ℓ[τ⃗′][ν⃗](v⃗)

Since M(ν.ℓ) = (λ[t⃗:κ][r⃗](x⃗:σ⃗).e), we have that

Ψ; Dom(Ψ); ·; ·; · ⊢ ν.ℓ : ∀[t⃗:κ][r⃗](σ⃗) → 0 at ν

This implies that

Ψ|_{cd}; cd, r⃗; \overrightarrow{t:κ}; ·; \overrightarrow{x:σ} ⊢ e

By the typing rule, we get that

Ψ; Dom(Ψ); ·; ·; · ⊢ v_{i} : σ_{i}[ν⃗, τ⃗′/r⃗, t⃗]

and · ⊢ τ′_{i} : κ_{i}. From lemma A.1.4 we get that

Ψ|_{cd}; cd, ν⃗; Δ; ·; \overrightarrow{x:σ[ν⃗/r⃗]} ⊢ e[ν⃗/r⃗]

From lemma A.1.8 we get that

Ψ|_{cd}; cd, ν⃗; ·; ·; \overrightarrow{x:σ[ν⃗, τ⃗′/r⃗, t⃗]} ⊢ e[ν⃗, τ⃗′/r⃗, t⃗]

Since Ψ|_{cd} ⊂ Ψ and cd, ν⃗ ⊂ Dom(Ψ), we can extend the environment for deriving e. Applying lemma A.1.15 we get that

Ψ; Dom(Ψ); ·; ·; · ⊢ e[ν⃗, τ⃗′, v⃗/r⃗, t⃗, x⃗]

which leads to the result. case (v⟦τ⃗⟧)[τ⃗][ν⃗](v⃗): By definition,

Ψ; Dom(Ψ); ·; ·; · ⊢ (v⟦τ⃗⟧)[τ⃗][ν⃗](v⃗)

From the typing rules Ψ; Dom(Ψ); ·; ·; · ⊢ (v⟦τ⃗⟧) : ∀⟦τ⃗⟧[r⃗](σ⃗) ⟶^{ν} 0 for some ν and Ψ; Dom(Ψ); ·; ·; · ⊢ v_{i} : σ_{i}[ν⃗/r⃗]. Again from the typing rules we get that Ψ; Dom(Ψ); ·; ·; · ⊢ v : ∀[t⃗:κ][r⃗](σ⃗′) → 0 at ν where σ′_{i}[τ⃗/t⃗] = σ_{i} and · ⊢ τ_{i} : κ_{i}. We need to prove that

Ψ; Dom(Ψ); ·; ·; · ⊢ v[τ⃗][ν⃗](v⃗)

This is true if Ψ; Dom(Ψ); ·; ·; · ⊢ v_{i} : σ′_{i}[ν⃗, τ⃗/r⃗, t⃗]. But we already know that this holds. case let x = put[ν]v in e: By definition,

Ψ; Dom(Ψ); ·; ·; · ⊢ let x = put[ν]v in e

From the typing rules,

Ψ; Dom(Ψ); ·; ·; · ⊢ put[ν]v : σ at ν

for some type σ, and ν ∈ Dom(Ψ). This implies that

Ψ; Dom(Ψ); ·; ·; · ⊢ v : σ

Again, from the typing rules,

Ψ, ν.ℓ:σ; Dom(Ψ); ·; ·; · ⊢ ν.ℓ : σ at ν

The required result now follows from lemma A.1.15. case let x = get ν.ℓ in e: By definition,

Ψ; Dom(Ψ); ·; ·; · ⊢ let x = get ν.ℓ in e

From the typing rules we get that

Ψ; Dom(Ψ); ·; ·; · ⊢ ν.ℓ : σ at ν

for some type σ Again from the typing rules, we get that Ψ(ν.ℓ) = σ. This implies that if M(ν.ℓ) = v, then

Ψ; Dom(Ψ); ·; ·; · ⊢ v : σ

The required result follows from lemma A.1.15. case open ⟨α:Θ=σ_{1}, v:σ_{2}⟩ as ⟨α, x⟩ in e: The two open constructs are proved similarly. We will show the proof for only one of them. By definition,

Ψ; Dom(Ψ); ·; ·; · ⊢ open ⟨α:Θ=σ_{1}, v:σ_{2}⟩ as ⟨α, x⟩ in e

This implies that

Ψ; Dom(Ψ); ·; α:Θ; x:σ_{2} ⊢ e

The required result follows from lemmas A.1.15 and A.1.12. case let region r in e: By definition,

Ψ; Dom(Ψ); ·; ·; · ⊢ let region r in e

This implies that

Ψ; Dom(Ψ), r; ·; ·; · ⊢ e

By lemma A.1.4,

Ψ; Dom(Ψ), ν; ·; ·; · ⊢ e[ν/r]

Since ν is a newly introduced region, we can extend Ψ with it. This implies that

Ψ, ν ↦ {}; Dom(Ψ), ν; ·; ·; · ⊢ e[ν/r]

This is the required result. case only Θ in e: By definition,

Ψ; Dom(Ψ); ·; ·; · ⊢ only Θ in e

This implies that

Ψ|_{Θ}; cd, Θ; ·; ·; · ⊢ e

But cd, Θ = Dom(Ψ|_{Θ}). This implies that

Ψ|_{Θ}; Dom(Ψ|_{Θ}); ·; ·; · ⊢ e

which is the required result. For all of the tcase, the required result follows directly from the typing rules since the value environment is empty.

Lemma A.1.17 (Canonical forms)

1. If Ψ; Θ; ·; ·; · ⊢ v : int then v = n.

2. If Ψ; Θ; ·; ·; · ⊢ v : σ at ν then v = ν.ℓ.

3. If Ψ; Θ; ·; ·; · ⊢ v : σ_{1} × σ_{2} then v = ⟨v_{1}, v_{2}⟩.

4. If Ψ; Θ; ·; ·; · ⊢ v : ∃t:κ.σ then v = ⟨t=τ, v′:σ⟩.

5. If Ψ; Θ; ·; ·; · ⊢ v : ∃α:Θ′.σ then v = ⟨α:Θ′ =σ_{1}, v′:σ_{2}⟩.

6. If Ψ; Θ; ·; ·; · ⊢ v : ∀⟦τ⃗⟧[r⃗](σ⃗) ⟶^{ρ} 0 then v = v′⟦τ⃗⟧.

7. If Ψ; Θ; ·; ·; · ⊢ v : ∀[t⃗:κ][r⃗](σ⃗) → 0 then v = λ[t⃗:κ][r⃗](x⃗:σ).e. Proof The proof follows from the inspection of the typing rules for values. □

Proposition A.1.18 (Progress) If ⊢ (M, e) then either e = halt v or there exists a (M′, e′) such that (M, e) ⟹ (M′, e′). Proof The proof is again by induction over the structure of e. By definition, Ψ; Dom(Ψ); ·; ·; · ⊢ e. The proof for the individual cases start from this point. case v[τ⃗][ν⃗](v⃗): From the typing rules, either

v:∀[t⃗:κ][r⃗](σ⃗) → 0 at ν or v:∀⟦τ⃗⟧[r⃗](σ⃗) ⟶^{ν} 0

In the first case by lemma A.1.17, v = ν.ℓ. From the typing rules M(ν.ℓ) = λ[t⃗:κ][r⃗](x⃗:σ).e This implies that we have a reduction.

In the second case, by lemma A.1.17 v = v′⟦τ⟧. In this case also we have a reduction to v′[τ⃗][ν⃗](v⃗). case let x = op in e: If op = v, then we have a reduction. If op = π_{i}v, then from the typing rules, Ψ; Dom(Ψ); ·; ·; · ⊢ v : σ_{1} × σ_{2}. The required result follows from lemma A.1.17. In the case of put[ν]v, the result follows directly. The constraint ν ∈ Θ ensures that ν ∈ Dom(Ψ). In the case for get v, by the typing rules we know that v = ν.ℓ for some ν.ℓ. Again from the typing rule we know that Ψ(ν.ℓ) = σ. This implies that M(ν.ℓ) = v′ for some value v′. For the other cases of e, the proposition follows directly from the operational semantics. □

A.1.2 Soundness of λ_{GCforw}

As before, We use the usual exchange and weakening properties of environments without proving them. The code region cd is always implicitly part of the environment. Even when the environment is restricted to a particular set, say Ψ|_{Θ}, the code region is included in the restricted set. We will show the proofs only for the extensions. The only non-trivial extension is the widen construct. The proofs of the propositions for the other constructs follow in a straightforward way by induction over the derivations. Because of the widen operator, the notion of a well formed machine state has to be slightly adjusted to allow non-well-formed memory elements as long as they can be shown not to influence the execution:

Definition A.1.19 The machine state (M, e) is well formed iff

{\overline{M} ⊂ M  ⊢ \overline{M}:Ψ  Ψ; Dom(Ψ); ·; ·; · ⊢ e \atop ⊢ (M, e)}

Note that to show well-formedness we now need to find not only a witness Ψ but also a proper \overline{M} subset. The reason is that widen casts the whole live heap from one type to the other but cannot cast any arbitrary M because it might include existential packages of the form ⟨α:Θ=σ_{1}, v:σ_{2}⟩ where casting α would require an update of M, which goes against the idea that widen is a nop. This problem appears when we try to prove type preservation, which is the main obstacle to show soundness of the widen operator, whose proof uses two lemmas for the following two steps:

1. First, we construct a \overline{M} which only contains elements that we can cast (i.e. of type M_{ρ}(τ)) and we show that the state is still well-formed. This is the case because when we reach a widen, all the live data if of such a type.

2. Then we show that when everything has a type of the form M_{ρ}(τ), we can cast every single type consistently to its C_{ρ,ρ′}(τ) equivalent and the result is still properly typed. This relies on the subsumption rule on sum types and the fact that C types only differs from M types by adding branches to sum types.

Lemma A.1.20 (Region Substitution)

1. If Ψ; Θ, r; Δ; Φ; Γ ⊢ op : σ then Ψ; Θ, ν; Δ; Φ[ν/r]; Γ[ν/r] ⊢ op[ν/r] : σ[ν/r].

2. If Ψ; Θ, r; Δ; Φ; Γ ⊢ e then Ψ; Θ, ν; Δ; Φ[ν/r]; Γ[ν/r] ⊢ e[ν/r].

Proof Straightforward proof via induction over the derivation of the typing judgment. Note that ν ∈ Ψ is not required here. case let x = widen[ρ′][τ](v) in e: We have that Ψ; Θ, r; Δ; Φ; Γ ⊢ let x = widen[ρ′][τ](v) in e. This implies Ψ; Θ, r; Δ; Φ; Γ ⊢ v : M_{ρ}(τ) and Ψ|_{cd}; cd, ρ, ρ′; Δ; Φ|_{ρρ′}; x:C_{ρ,ρ′}(τ) ⊢ e. Applying lemma A.1.20.1 to the derivation for v we get that Ψ; Θ, ν; Δ; Φ[ν/r]; Γ[ν/r] ⊢ v[ν/r] : M_{ρ[ν/r]}(τ) since M_{ρ}(τ)[ν/r] = M_{ρ[ν/r]}(τ). If either ρ = r or ρ′ = r then the inductive hypothesis on the derivation of e and lemma A.1.2 and the fact that C_{ρ,ρ′}(τ)[ν/r] = C_{(ρ,ρ′)[ν/r]}(τ) leads to the required result. In the other case, Φ[ν/r]|_{ρρ′} = Φ|_{ρρ′}, Φ′ where Dom(Φ|_{ρρ′}) ∩ Dom(Φ′) is empty. Moreover, r does not occur free in e. Since we can extend environments with new bindings we get that Ψ|_{cd}; cd, ρ, ρ′; Δ; Φ|_{ρρ′}, Φ′; x:C_{ρ,ρ′}(τ) ⊢ e. This leads to the required result. □

Lemma A.1.21 (Type Substitution)

1. If Ψ; Θ; Δ, t:κ; Φ; Γ ⊢ op : σ and Δ ⊢ τ : κ then Ψ; Θ; Δ; Φ; Γ[τ/t] ⊢ op[τ/t] : σ[τ/t].

2. If Ψ; Θ; Δ, t:κ; Φ; Γ ⊢ e and Δ ⊢ τ : κ then Ψ; Θ; Δ; Φ; Γ[τ/t] ⊢ e[τ/t].

3. If Ψ; Θ; Δ; Φ, α:Θ′; Γ ⊢ op : σ and Θ′; Δ; Φ ⊢ σ′ then Ψ; Θ; Δ; Φ; Γ[σ′/α] ⊢ op[σ′/α] : σ[σ′/α]

4. If Ψ; Θ; Δ; Φ, α:Θ′; Γ ⊢ e and Θ′; Δ; Φ ⊢ σ′ then Ψ; Θ; Δ; Φ; Γ[σ′/α] ⊢ e[σ′/α]

Proof Same thing here, the induction hypothesis can always be applied directly. □

Lemma A.1.22 (Value Substitution)

1. If Ψ; Θ; Δ; Φ; Γ, x:σ ⊢ op : σ′ and Ψ; Θ; Δ; Φ; Γ ⊢ v : σ then Ψ; Θ; Δ; Φ; Γ ⊢ op[v/x] : σ′.

2. If Ψ; Θ; Δ; Φ; Γ, x:σ ⊢ e and Ψ; Θ; Δ; Φ; Γ ⊢ v : σ then Ψ; Θ; Δ; Φ; Γ ⊢ e[v/x].

Proof The proof is by induction over the derivation of the type judgment. Most of the cases follow directly from the induction hypothesis. Exceptions are:

widen We need to show that the substitution on e has no effect, which is easy since the variable we are substituting cannot occur freely in e.

□

Definition A.1.23 Let Ψ|_{ν,M} be the restriction of Ψ to pieces of code or elements of type M_{ν}(τ):

Ψ|_{ν′,M} = \{ν.ℓ:σ | Ψ(ν.ℓ) = σ ∧ (\begin{matrix}σ = ∀[τ⃗][r⃗](\overrightarrow{v:σ}) → 0 \\ ∨ ∃τ.(σ at ν = M_{ν′}(τ)) \end{matrix})\}

Note that since cd only contains functions, Ψ|_{ν,M} will preserve all its contents.

Lemma A.1.24 If Ψ; Dom(Ψ); ·; ·; · ⊢ v : M_{ν}(τ) then Ψ|_{ν,M}; Dom(Ψ|_{ν}); ·; ·; · ⊢ v : M_{ν}(τ)

Proof By the definition of the M type, v is either of type int or of type σ at ν. The lemma follows trivially if v is of type int. Consider the other cases. case M_{ν}(τ → 0): In this case v = cd.ℓ. Therefore Ψ(cd.ℓ) = ∀[][r](M_{r}(τ)) → 0. Therefore M(cd.ℓ) = λ[][r](\overrightarrow{x:σ}).e. The lemma now follows since the body e is typed only under the code region Ψ|_{cd} and the region environment cd, r. case M_{ν}(τ_{1} × τ_{2}): We again have that v = ν.ℓ. Therefore, Ψ(ν.ℓ) = left(M_{ν}(τ_{1}) × M_{ν}(τ_{2})). This implies that M(ν.ℓ) = inl v′. We know that

Ψ; Dom(Ψ); ·; ·; · ⊢ inl v′ : left(M_{ν}(τ_{1}) × M_{ν}(τ_{2}))

This implies that

Ψ; Dom(Ψ); ·; ·; · ⊢ v′ : (M_{ν}(τ_{1}) × M_{ν}(τ_{2}))

This implies that v′ = ⟨v′_{1}, v′_{2}⟩. From the typing rules we get that Ψ; Dom(Ψ); ·; ·; · ⊢ v′_{1} : M_{ν}(τ_{1}) and Ψ; Dom(Ψ); ·; ·; · ⊢ v′_{2} : M_{ν}(τ_{2}). Applying the inductive hypothesis to the derivations for v′_{1} and v′_{2}, we get that Ψ|_{ν,M}; Dom(Ψ|_{ν}); ·; ·; · ⊢ v′_{1} : M_{ν}(τ_{1}) and Ψ|_{ν,M}; Dom(Ψ|_{ν}); ·; ·; · ⊢ v′_{2} : M_{ν}(τ_{2}). The required result follows from this. case M_{ν}(∃t.τ): We again have that v = ν.ℓ. Therefore, Ψ(ν.ℓ) = left(∃t.M_{ν}(τ)). This implies that M(ν.ℓ) = inl v′. We know that

Ψ; Dom(Ψ); ·; ·; · ⊢ inl v′ : left(∃t.M_{ν}(τ))

This implies that

Ψ; Dom(Ψ); ·; ·; · ⊢ v′ : ∃t.M_{ν}(τ)

This implies that v′ = ⟨t=τ′, v′′:M_{ν}(τ)⟩. From here we get that Ψ; Dom(Ψ); ·; ·; · ⊢ v′′ : (M_{ν}(τ))[τ′/t]. But we have that (M_{ν}(τ))[τ′/t] = M_{ν}(τ[τ′/t]). Therefore, applying the inductive hypothesis to the derivation for v′′, we get that Ψ|_{ν,M}; Dom(Ψ|_{ν}); ·; ·; · ⊢ v′′ : M_{ν}(τ[τ′/t]). The required result follows from this. □

Definition A.1.25 Let T_{ρ,ρ′} be the type operator that turns a type of the form M_{ρ}(τ) into a type of the form C_{ρ,ρ′}(τ) and keeps code pointer types unchanged:

\begin{matrix}T_{ρ,ρ′}(M_{ρ}(τ)) = C_{ρ,ρ′}(τ) \\ T_{ρ,ρ′}(∀[τ⃗][r⃗](\overrightarrow{v:σ}) → 0 at ρ) = ∀[τ⃗][r⃗](\overrightarrow{v:σ}) → 0 at ρ \end{matrix}

We also use this operator on Ψ where it is defined as:

T_{ρ,ρ′}(Ψ) = {ν.ℓ:σ | Ψ(ν.ℓ) = σ′ ∧ T_{ρ,ρ′}(σ′ at ρ) = σ at ρ}

Note that the two parts of the definition of T_{ρ,ρ′}(σ) overlap but are consistent since M_{ρ}(τ) and C_{ρ,ρ′}(τ) are identical in the case when τ is an arrow type. Also, since cd only contains functions, T_{ρ,ρ′}(Ψ) does not change the type of cd. Finally since both M and C are iterators, the T type operator can be defined as an iterator.

Lemma A.1.26 If Ψ|_{ν,M}; Dom(Ψ|_{ν}); ·; ·; · ⊢ v : M_{ν}(τ), then T_{ν,ν′}(Ψ|_{ν,M}); Dom(Ψ|_{ν}), ν′; ·; ·; · ⊢ v : C_{νν′}(τ)

Proof When v is of type int, the lemma follows trivially. case M_{ν}(τ → 0): In this case v = cd.ℓ and M(cd.ℓ) = λ[][r](\overrightarrow{x:σ}).e. This implies that Ψ|_{cd}; cd, r; ·; ·; \overrightarrow{x:σ} ⊢ e. Since M_{ν}(τ′ → 0) = C_{νν′}(τ′ → 0) the type operator T is the identity for code types. Therefore, the cast will leave the type of the code region unchanged. Therefore Ψ|_{cd} = (T_{ν,ν′}(Ψ|_{ν,M}))|_{cd}. This leads to the required result. case M_{ν}(τ_{1} × τ_{2}): We again have that v = ν.ℓ. Therefore, Ψ(ν.ℓ) = left(M_{ν}(τ_{1}) × M_{ν}(τ_{2})). This implies that M(ν.ℓ) = inl v′. We know that

Ψ; Dom(Ψ); ·; ·; · ⊢ inl v′ : left(M_{ν}(τ_{1}) × M_{ν}(τ_{2}))

This implies that

Ψ; Dom(Ψ); ·; ·; · ⊢ v′ : (M_{ν}(τ_{1}) × M_{ν}(τ_{2}))

This implies that v′ = ⟨v′_{1}, v′_{2}⟩. From the typing rules we get that Ψ; Dom(Ψ); ·; ·; · ⊢ v′_{1} : M_{ν}(τ_{1}) and Ψ; Dom(Ψ); ·; ·; · ⊢ v′_{2} : M_{ν}(τ_{2}). Applying lemma A.1.24 to the derivations for v′_{1} and v′_{2}, we get that Ψ|_{ν,M}; Dom(Ψ|_{ν}); ·; ·; · ⊢ v′_{1} : M_{ν}(τ_{1}) and Ψ|_{ν,M}; Dom(Ψ|_{ν}); ·; ·; · ⊢ v′_{2} : M_{ν}(τ_{2}). Applying the inductive hypothesis to the derivations for v′_{1} and v′_{2} we get that T_{ν,ν′}(Ψ|_{ν,M}); Dom(Ψ|_{ν}), ν′; ·; ·; · ⊢ v′_{1} : C_{νν′}(τ_{1}) and T_{ν,ν′}(Ψ|_{ν,M}); Dom(Ψ|_{ν}), ν′; ·; ·; · ⊢ v′_{2} : C_{νν′}(τ_{2}). From here we can conclude that

T_{ν,ν′}(Ψ|_{ν,M}); Dom(Ψ|_{ν}), ν′; ·; ·; · ⊢
inl v′ : left(C_{νν′}(τ_{1}) × C_{νν′}(τ_{2}))

By the subtyping rule we can conclude that

T_{ν,ν′}(Ψ|_{ν,M}); Dom(Ψ|_{ν}), ν′; ·; ·; · ⊢

inl v′ : (+\begin{matrix}left(C_{νν′}(τ_{1}) × C_{νν′}(τ_{2})) \\ right(M_{ν′}(τ_{2} × τ_{2})) \end{matrix})

This is the required result. case M_{ν}(∃t.τ): The proof proceeds exactly as in the previous case. We only need to use the fact that (C_{ρ,ρ′}(τ))[τ′/t] = C_{ρ,ρ′}(τ[τ′/t])

□

Proposition A.1.27 (Type Preservation) If ⊢ (M, e) and (M, e) ⟹ (M′, e′) then ⊢ (M′, e′). Proof The proof is by cases on the structure of e. For each possible case, we use the typing derivation together with the evaluation step to get the derivation of the new typing judgment. This mostly relies on the substitution lemmas. We only show the more interesting cases.

• (M, let x = v in e) ⟹ (M, e[v/x])

From ⊢ (M, let x = v in e) we get that ⊢ \overline{M}:Ψ and Ψ; Dom(Ψ); ·; ·; · ⊢ let x = v in e. The derivation in turns tells us that Ψ; Dom(Ψ); ·; ·; · ⊢ v : σ and Ψ; Dom(Ψ); ·; ·; ·, x:σ:⊢ e. At this point, we can apply value substitution to get Ψ; Dom(Ψ); ·; ·; · ⊢ e[v/x].

• (M, only Θ in e) ⟹ (M|_{Θ}, e)

Here we get Ψ; Dom(Ψ); ·; ·; · ⊢ only Θ in e which gives us Ψ|_{Θ}; Θ; ·; ·; · ⊢ e. Since \overline{M} ⊂ M, we have that \overline{M}|_{Θ} ⊂ M|_{Θ}. Moreover, we know that Θ ⊂ Dom(Ψ). Therefore, Dom(Ψ|_{Θ}) = Θ.

• (M, let x = widen[ν′][τ](v) in e) ⟹ (M, e[v/x]) We know that if \overline{M}:Ψ, then we have that Ψ; Dom(Ψ); ·; ·; · ⊢ let x = widen[ν′][τ](v) in e. This implies that Ψ; Dom(Ψ); ·; ·; · ⊢ v : M_{ν}(τ) and Ψ|_{cd}; cd, ν, ν′; ·; ·; , x:C_{νν′}(τ) ⊢ e for some ν and ν′ belonging to Dom(Ψ). By lemma A.1.24 we have that Ψ|_{ν,M}; Dom(Ψ|_{ν}); ·; ·; · ⊢ v : M_{ν}(τ). By lemma A.1.26 we have that T_{ν,ν′}(Ψ|_{ν,M}); Dom(Ψ|_{ν}); ·; ·; · ⊢ v : C_{νν′}(τ). But we have that T_{ν,ν′}(Ψ|_{ν,M}) = Ψ|_{cd}, Ψ′ for some Ψ′ since the cast leaves the code type unchanged. Also Ψ′ = T_{νν′}(Ψ|_{ν,M}) Since we can extend the environment, we have that Ψ|_{cd}, Ψ′; cd, ν, ν′; ·; ·; , x:C_{νν′}(τ) ⊢ e. Applying lemma A.1.22.2 we get that Ψ|_{cd}, Ψ′; cd, ν, ν′; ·; ·; · ⊢ e[v/x]. This implies that Ψ|_{cd}, Ψ′, ν′ ↦ \{\}; cd, ν, ν′; ·; ·; · ⊢ e[v/x]. If \overline{M}:Ψ|_{cd}, Ψ′, ν′ ↦ \{\}, then clearly \overline{M} ⊂ M. This leads to the required result.

□

Lemma A.1.28 (Canonical forms)

1. If Ψ; Θ; ·; ·; · ⊢ v : int then v = n.

2. If Ψ; Θ; ·; ·; · ⊢ v : σ at ρ then v = ν.ℓ.

3. If Ψ; Θ; ·; ·; · ⊢ v : σ_{1} × σ_{2} then v = ⟨v_{1}, v_{2}⟩.

APPENDIX A. SOUNDNESS PROOFS  172

4. If Ψ; Θ; ·; ·; · ⊢ v : ∃t.σ then v = ⟨t=τ, v′:σ⟩.

5. If Ψ; Θ′; ·; ·; · ⊢ v : ∃α:Θ.σ then v = ⟨α:Θ=σ_{1}, v′:σ_{2}⟩.

6. If Ψ; Θ; ·; ·; · ⊢ v : ∀⟦τ⃗⟧[r⃗](σ⃗) ⟶^{ρ} 0 then v = v′⟦τ⃗⟧.

7. If Ψ; Θ; ·; ·; · ⊢ v : ∀[t⃗:κ][r⃗](σ⃗) → 0

then v = λ[t⃗:κ][r⃗](x⃗:σ).e. Proof The proof follows from the inspection of the typing rules for values. □ Proposition A.1.29 (Progress) If ⊢ (M, e) then either e = halt v or there exists a (M′, e′) such that (M, e) ⟹ (M′, e′). Proof The proof is again by cases on the structure of e. It uses lemma A.1.28. □

A.1.3 Soundness of λ_{GCgen}

In this section, we prove the soundness of λ_{GCgen}. We will prove soundness in the same way – with the subject reduction and progress lemmas. Since λ_{GCgen} is just an extension of λ_{GC}, we will prove the required lemmas only for the new cases. As usual we will use the exchange and widening lemmas for environments throughout the section.

Lemma A.1.30 If Θ; Δ; Φ ⊢ σ, then Θ[ν/r]; Δ; Φ[ν/r] ⊢ σ[ν/r].

Proof The proof is by induction over the derivation of σ. □

Lemma A.1.31 (Φ[ν/r])|_{Θ,ν} = (Φ|_{Θ,r})[ν/r] and (Γ[ν/r])|_{Θ,ν} = (Γ|_{Θ,r})[ν/r]

Proof Same as lemma A.1.2. □

Lemma A.1.32 (M_{ρ_{1},ρ_{2}}(τ))[τ′/t] = M_{ρ_{1},ρ_{2}}(τ[τ′/t]) and (M_{ρ_{1},ρ_{2}}(τ))[ν/r] = M_{ρ_{1}[ν/r],ρ_{2}[ν/r]}(τ)

Proof Proved by considering the different possible reductions of M_{ρ_{1},ρ_{2}}(τ). □

Lemma A.1.33 If Ψ; Θ, r; Δ; Φ; Γ ⊢ op : σ, then Ψ; Θ, ν; Δ; Φ[ν/r]; Γ[ν/r] ⊢ op[ν/r] : σ[ν/r]

Proof The proof is a straightforward induction over the derivation for op. Note that Θ, ν = (Θ, r)[ν/r]. It uses lemma A.1.32 for the case involving subtyping with the M_{ρ_{1},ρ_{2}}(τ) type. case ⟨r′ ∈ Θ′ = ρ, v⟩: We have that

Ψ; Θ, r; Δ; Φ; Γ ⊢ ⟨r′ ∈ Θ′ = ρ, v⟩ : ∃r′ ∈Θ′.(σ at r′)

This implies that Ψ; Θ, r; Δ; Φ; Γ ⊢ v : σ[ρ/r_{′}] at ρ and ρ ∈ Θ′ and Θ′ ⊂ Θ, r. Applying the inductive hypothesis we get that

Ψ; Θ, ν; Δ; Φ[ν/r]; Γ[ν/r] ⊢ v[ν/r] : (σ[ρ/r_{′}])[ν/r] at ρ[ν/r]

This implies that

\begin{matrix}Ψ; Θ, ν; Δ; Φ[ν/r]; Γ[ν/r] ⊢ \\ v[ν/r] : (σ[ν/r])[(ρ[ν/r])/r_{′}] at ρ[ν/r] \end{matrix}

Moreover, Θ′[ν/r] ⊂ Θ, ν and ρ[ν/r] ∈ Θ′[ν/r]. This implies that

Ψ; Θ, ν; Δ; Φ[ν/r]; Γ[ν/r] ⊢
⟨r′ ∈ Θ′[ν/r] = ρ[ν/r], v[ν/r]⟩ : ∃r′ ∈{Θ′[ν/r]}.(σ[ν/r] at r′)
□

Lemma A.1.34 If Ψ; Θ, r; Δ; Φ; Γ ⊢ e, then Ψ; Θ, ν; Δ; Φ[ν/r]; Γ[ν/r] ⊢ e[ν/r]

Proof The proof is by induction over the derivation of e. Note that Θ, ν = (Θ, r)[ν/r]. We will consider only the extra cases here. case open v as ⟨r′, x⟩ in e: We have that

Ψ; Θ, r; Δ; Φ; Γ ⊢ open v as ⟨r′, x⟩ in e

This implies that

Ψ; Θ, r, r′; Δ; Φ; Γ, x:σ at r′ ⊢ e

and Ψ; Θ, r; Δ; Φ; Γ ⊢ v : ∃r′ ∈Θ′.(σ at r′). Applying lemma A.1.33 to the derivation for v we get that

Ψ; Θ, ν; Δ; Φ[ν/r]; Γ[ν/r] ⊢ v[ν/r] : ∃r′ ∈Θ′[ν/r].(σ[ν/r] at r′)

Applying the inductive hypothesis to the derivation for e we get that

Ψ; Θ, ν, r′; Δ; Φ[ν/r]; Γ[ν/r], x:σ[ν/r] at r′ ⊢ e[ν/r]

From this we get that

Ψ; Θ, ν; Δ; Φ[ν/r]; Γ[ν/r] ⊢ open v[ν/r] as ⟨r′, x⟩ in e[ν/r]

case ifreg (r_{1} = r_{2}) e_{1} e_{2}: We have that

\begin{matrix}Ψ; (Θ, r)[r′, r′/r_{1}, r_{2}]; Δ; Φ[r′, r′/r_{1}, r_{2}]; Γ[r′, r′/r_{1}, r_{2}] ⊢ \\ e_{1}[r′, r′/r_{1}, r_{2}] \end{matrix}

and Ψ; Θ, r; Δ; Φ; Γ ⊢ e_{2} and r′ ∉ Θ, r. Applying the inductive hypothesis to the derivation for e_{2} leads to

Ψ; Θ, ν; Δ; Φ[ν/r]; Γ[ν/r] ⊢ e_{2}[ν/r]

Applying the inductive hypothesis to the derivation for e_{1} and using the fact that r is different from r_{1} and r_{2} we get that

Ψ; (Θ, ν)[r′, r′/r_{1}, r_{2}]; Δ;
(Φ[ν/r])[r′, r′/r_{1}, r_{2}]; (Γ[ν/r])[r′, r′/r_{1}, r_{2}] ⊢
(e_{1}[ν/r])[r′, r′/r_{1}, r_{2}]

This leads to the required result. case ifreg (r = r_{2}) e_{1} e_{2}: We have that

Ψ; Θ, r; Δ; Φ; Γ ⊢ ifreg (r = r_{2}) e_{1} e_{2}

This implies that

\begin{matrix}Ψ; (Θ, r)[r′, r′/r, r_{2}]; Δ; Φ[r′, r′/r, r_{2}]; Γ[r′, r′/r, r_{2}] ⊢ \\ e_{1}[r′, r′/r, r_{2}] \end{matrix}

and Ψ; Θ, r; Δ; Φ; Γ ⊢ e_{2}. We must prove that

Ψ; Θ, ν; Δ; Φ[ν/r]; Γ[ν/r] ⊢ ifreg (ν = r_{2}) e_{1}[ν/r] e_{2}[ν/r]

This implies that we must prove that Ψ; Θ, ν; Δ; Φ[ν/r]; Γ[ν/r] ⊢ e_{2}[ν/r] and

\begin{matrix}Ψ; (Θ, ν)[ν/r_{2}]; Δ; (Φ[ν/r])[ν/r_{2}]; (Γ[ν/r])[ν/r_{2}] ⊢ \\ (e_{1}[ν/r])[ν/r_{2}] \end{matrix}

This implies that we must prove that

\begin{matrix}Ψ; (Θ, r)[ν, ν/r, r_{2}]; Δ; Φ[ν, ν/r, r_{2}]; Γ[ν, ν/r, r_{2}] ⊢ \\ e_{1}[ν, ν/r, r_{2}] \end{matrix}

Applying the inductive hypothesis to the derivation for e_{2} leads to the required result for this derivation. In the case for e_{1}, we substitute for r′ and applying the inductive hypothesis and the fact that r′ ∉ Θ, r leads to the required result. case ifreg (ν′ = r_{2}) e_{1} e_{2}: This case follows directly from the inductive hypothesis on the derivations of e_{1} and e_{2} and using the fact that ν′ ≠ ν and r ≠ r_{2}. case ifreg (ν = r_{2}) e_{1} e_{2}: This case follows directly from the inductive hypothesis on the derivations of e_{1} and e_{2} and using the fact that r ≠ r_{2}. case ifreg (ν′ = r) e_{1} e_{2}: We have that

Ψ; Θ, r; Δ; Φ; Γ ⊢ ifreg (ν′ = r) e_{1} e_{2}

This means that we know that

Ψ; Θ, r; Δ; Φ; Γ ⊢ e_{2}

We have to prove that

Ψ; Θ, ν; Δ; Φ[ν/r]; Γ[ν/r] ⊢ ifreg (ν′ = ν) e_{1}[ν/r] e_{2}[ν/r]

APPENDIX A. SOUNDNESS PROOFS  177

This implies that we have to prove that

Ψ; Θ, ν; Δ; Φ[ν/r]; Γ[ν/r] ⊢ e_{2}[ν/r]

Applying the inductive hypothesis to the derivation for e_{2} leads to the result. case ifreg (ν = r) e_{1} e_{2}: We have that

Ψ; Θ, r; Δ; Φ; Γ ⊢ ifreg (ν = r) e_{1} e_{2}

This means that we know that

Ψ; Θ, ν; Δ; Φ[ν/r]; Γ[ν/r] ⊢ e_{1}[ν/r]

We have to prove that

Ψ; Θ, ν; Δ; Φ[ν/r]; Γ[ν/r] ⊢ ifreg (ν = ν) e_{1}[ν/r] e_{2}[ν/r]

This implies that we have to prove that

Ψ; Θ, ν; Δ; Φ[ν/r]; Γ[ν/r] ⊢ e_{1}[ν/r]

But we already know this. □ Lemma A.1.35 If Δ, t:κ′ ⊢ τ : κ and Δ ⊢ τ′ : κ′, then Δ ⊢ τ[τ′/t] : κ Proof The proof is a straightforward induction over the structure of τ. □ Lemma A.1.36 If Θ; Δ, t:κ; Φ ⊢ σ and Δ ⊢ τ : κ, then Θ; Δ; Φ ⊢ σ[τ/t] Proof The proof is again a straighforward induction over the structure of σ. □ Lemma A.1.37 If Ψ; Θ; Δ, t:κ; Φ; Γ ⊢ op : σ and Δ ⊢ τ : κ then Ψ; Θ; Δ; Φ; Γ[τ/t] ⊢ op[τ/t] : σ[τ/t]

APPENDIX A. SOUNDNESS PROOFS  178

Proof The proof is by induction over the derivation for op. □ Lemma A.1.38 If Ψ; Θ; Δ, t:κ; Φ; Γ ⊢ e and · ⊢ τ′ : κ then Ψ; Θ; Δ; Φ; Γ[τ′/t] ⊢ e[τ′/t] Proof The proof is again by induction over the derivation for e. □ Lemma A.1.39 If Θ; Δ; Φ, α:Θ′ ⊢ σ and Θ′; Δ; Φ ⊢ σ′, then Θ; Δ; Φ ⊢ σ[σ′/α] Proof The proof is a straighforward induction over the structure of σ. □ Lemma A.1.40 If Ψ; Θ; Δ; Φ, α:Θ′; Γ ⊢ op : σ and Θ′; Δ; Φ ⊢ σ′ then Ψ; Θ; Δ; Φ; Γ[σ′/α] ⊢ op[σ′/α] : σ[σ′/α] Proof The proof is again by induction over the typing derivation for op. We will consider only the case for region packages. case ⟨r ∈ Θ′ = ρ, v⟩: We have that

Ψ; Θ; Δ; Φ, α:Θ′; Γ ⊢ ⟨r ∈ Θ′ = ρ, v⟩ : ∃r ∈Θ′.(σ at r)

and Θ′; Δ; Φ ⊢ σ′. This implies that Ψ; Θ; Δ; Φ, α:Θ′; Γ ⊢ v : σ[ρ/r] at ρ and ρ ∈ Θ′ and Θ′ ⊂ Θ. Applying the inductive hypothesis we get that

Ψ; Θ; Δ; Φ; Γ[σ′/α] ⊢ v[σ′/α] : (σ[ρ/r])[σ′/α] at ρ

Since r does not occur in Θ, and Θ′ ⊂ Θ, therefore r does not occur free in σ′. Therefore we have that

Ψ; Θ; Δ; Φ; Γ[σ′/α] ⊢ v[σ′/α] : (σ[σ′/α])[ρ/r] at ρ

This leads to the lemma. □ Lemma A.1.41 If Ψ; Θ; Δ; Φ, α:Θ′; Γ ⊢ e and Θ′; Δ; Φ ⊢ σ′ then Ψ; Θ; Δ; Φ; Γ[σ′/α] ⊢ e[σ′/α] Proof The proof is by induction over the derivation of e. case open v as ⟨r, x⟩ in e: We have that

Ψ; Θ; Δ; Φ, α:Θ′; Γ ⊢ v : ∃r ∈Θ_{1}.(σ at r)

and Ψ; Θ, r; Δ; Φ, α:Θ′; Γ, x:σ at r ⊢ e. Applying lemma A.1.40 to the derivation for v we get that

Ψ; Θ; Δ; Φ; Γ[σ′/α] ⊢ v[σ′/α] : ∃r ∈Θ_{1}.(σ[σ′/α] at r)

Applying the inductive hypothesis to the derivation for e we get that

Ψ; Θ, r; Δ; Φ; Γ[σ′/α], x:σ[σ′/α] at r ⊢ e[σ′/α]

This leads to the required result. case ifreg (r_{1} = r_{2}) e_{1} e_{2}: We have that Ψ; Θ; Δ; Φ, α:Θ′; Γ ⊢ e_{2} and

Ψ; Θ[r, r/r_{1}, r_{1}]; Δ;
Φ[r, r/r_{1}, r_{1}], α:Θ′[r, r/r_{1}, r_{1}]; Γ[r, r/r_{1}, r_{2}] ⊢
e_{1}[r, r/r_{1}, r_{2}]

Applying the inductive hypothesis to the derivation of e_{2} leads to

Ψ; Θ; Δ; Φ; Γ[σ′/α] ⊢ e_{2}[σ′/α]

By lemma A.1.30 we have that

Θ′[r, r/r_{1}, r_{2}]; Δ; Φ[r, r/r_{1}, r_{2}] ⊢ σ′[r, r/r_{1}, r_{2}]

Substitute σ′[r, r/r_{1}, r_{2}] in the derivation for e_{1} and by applying the inductive hypothesis we get that

\begin{matrix}Ψ; Θ[r, r/r_{1}, r_{1}]; Δ; Φ[r, r/r_{1}, r_{1}]; (Γ[σ′/α])[r, r/r_{1}, r_{2}] ⊢ \\ (e_{1}[σ′/α])[r, r/r_{1}, r_{2}] \end{matrix}

This leads to the required result. case ifreg (r = ν) e_{1} e_{2}: We have that Ψ; Θ; Δ; Φ, α:Θ′; Γ ⊢ e_{2} and

Ψ; Θ[ν/r]; Δ; Φ[ν/r], α:Θ′[ν/r]; Γ[ν/r] ⊢ e_{1}[ν/r]

Applying the inductive hypothesis to the derivation of e_{2} leads to

Ψ; Θ; Δ; Φ; Γ[σ′/α] ⊢ e_{2}[σ′/α]

By lemma A.1.30 we have that

Θ′[ν/r]; Δ; Φ[ν/r] ⊢ σ′[ν/r]

Substituting σ′[ν/r] in the derivation for e_{1} and applying the inductive hypothesis gives us that

Ψ; Θ[ν/r]; Δ; Φ[ν/r]; (Γ[σ′/α])[ν/r] ⊢ (e_{1}[σ′/α])[ν/r]

which leads to the required result. □

Lemma A.1.42 If Ψ; Θ; Δ; Φ; Γ, x:σ′ ⊢ op : σ and Ψ; Θ; Δ; Φ; Γ ⊢ v′ : σ′ then Ψ; Θ; Δ; Φ; Γ ⊢ op[v′/x] : σ

Proof The proof is by induction over the typing derivation for op. The new cases follow in a straightforward way. □

Lemma A.1.43 If Ψ; Θ; Δ; Φ; Γ ⊢ v : σ and Θ_{1}; Δ; Φ|_{Θ_{1}} ⊢ σ and Θ_{1} ⊂ Θ, then Ψ|_{Θ_{1}}; Θ_{1}; Δ; Φ|_{Θ_{1}}; Γ|_{Θ_{1}} ⊢ v : σ Proof The proof is by induction over the derivation for v. We will show only the extra case here. case ⟨r ∈ Θ_{2} = ρ, v⟩: We have that Ψ; Θ; Δ; Φ; Γ ⊢ ⟨r ∈ Θ_{2} = ρ, v⟩ : ∃r ∈Θ_{2}.(σ at r). This implies that Ψ; Θ; Δ; Φ; Γ ⊢ v : σ[ρ/r] at ρ and ρ ∈ Θ_{2} and Θ_{2} ⊂ Θ. We also have that Θ_{1}; Δ; Φ|_{Θ_{1}} ⊢ ∃r ∈Θ_{2}.(σ at r). This implies that Θ_{2} ⊂ Θ_{1}. Moreover, Θ_{1}, r; Δ; Φ|_{Θ_{1}} ⊢ σ. By lemma A.1.30 we get that Θ_{1}, ρ; Δ; (Φ|_{Θ_{1}})[ρ/r] ⊢ σ[ρ/r]. But since ρ ∈ Θ_{1} and r does not occur free in Φ|_{Θ_{1}}, we get that Θ_{1}; Δ; Φ|_{Θ_{1}} ⊢ σ[ρ/r]. Applying the inductive hypothesis to the derivation for v we get that Ψ|_{Θ_{1}}; Θ_{1}; Δ; Φ|_{Θ_{1}}; Γ|_{Θ_{1}} ⊢ v : σ[ρ/r] at ρ. This implies that Ψ|_{Θ_{1}}; Θ_{1}; Δ; Φ|_{Θ_{1}}; Γ|_{Θ_{1}} ⊢ ⟨r ∈ Θ_{2} = ρ, v⟩ : ∃r ∈Θ_{2}.(σ at r)

□

Lemma A.1.44 If Ψ; Θ; Δ; Φ; Γ, x:σ ⊢ e and Ψ; Θ; Δ; Φ; Γ ⊢ v : σ then Ψ; Θ; Δ; Φ; Γ ⊢ e[v/x]

Proof The proof is by induction over the derivation of e. We will only consider the case for ifreg. case ifreg (r = ν) e_{1} e_{2}: By definition we have that

Ψ; Θ[ν/r]; Δ; Φ[ν/r]; Γ[ν/r], x:σ[ν/r] ⊢ e_{1}[ν/r]

and Ψ; Θ; Δ; Φ; Γ, x:σ ⊢ e_{2}. Applying the inductive hypothesis to the derivation of e_{2}, we get that Ψ; Θ; Δ; Φ; Γ ⊢ e_{2}[v/x]. By lemma A.1.33 we know that

Ψ; Θ[ν/r]; Δ; Φ[ν/r]; Γ[ν/r] ⊢ v[ν/r] : σ[ν/r]

Substituting v[ν/r] for x in the derivation for e_{1} leads to the lemma. case ifreg (r_{1} = r_{2}) e_{1} e_{2}: By definition we have that

Ψ; Θ[r, r/r_{1}, r_{2}]; Δ;
Φ[r, r/r_{1}, r_{2}]; Γ[r, r/r_{1}, r_{2}], x:σ[r, r/r_{1}, r_{2}] ⊢
e_{1}[r, r/r_{1}, r_{2}]

and Ψ; Θ; Δ; Φ; Γ, x:σ ⊢ e_{2}. By lemma A.1.33 we know that

\begin{matrix}Ψ; Θ[r, r/r_{1}, r_{2}]; Δ; Φ[r, r/r_{1}, r_{2}]; Γ[r, r/r_{1}, r_{2}] ⊢ \\ v[r, r/r_{1}, r_{2}] : σ[r, r/r_{1}, r_{2}] \end{matrix}

Substituting v[r, r/r_{1}, r_{2}] and v in the derivation of e_{1} and e_{2} respectively, and applying the inductive hypothesis leads to the lemma. □

Proposition A.1.45 (Type Preservation) If ⊢ (M, e) and (M, e) ⟹ (M′, e′) then ⊢ (M′, e′). Proof The proof is by induction over the evaluation relation. We will consider only the additional cases here. The lemma follows in a straightforward way for the ifreg cases. case open ⟨r ∈ Θ = ν, v⟩ as ⟨r, x⟩ in e: We know that

Ψ; Dom(Ψ); ·; ·; · ⊢ open ⟨r ∈ Θ = ν, v⟩ as ⟨r, x⟩ in e

This implies that

Ψ; Dom(Ψ); ·; ·; · ⊢ ⟨r ∈ Θ = ν, v⟩ : ∃r ∈Θ.(σ at r)

where Θ ⊂ Dom(Ψ). By the typing rule for region packages, we have that ν ∈ Dom(Ψ) and that Ψ; Dom(Ψ); ·; ·; · ⊢ v : σ[ν/r] at ν. We also have that Ψ; Dom(Ψ), r; ·; ·; x:σ at r ⊢ e. By lemma A.1.34 and since ν ∈ Dom(Ψ) we get that

Ψ; Dom(Ψ); ·; ·; x:σ[ν/r] at ν ⊢ e[ν/r]

Applying lemma A.1.44 leads to the result. □

Lemma A.1.46 (Canonical forms)

1. If Ψ; Θ; ·; ·; · ⊢ v : int then v = n.

2. If Ψ; Θ; ·; ·; · ⊢ v : σ at ν then v = ν.ℓ.

APPENDIX A. SOUNDNESS PROOFS  183

3. If Ψ; Θ; ·; ·; · ⊢ v : σ_{1} × σ_{2} then v = ⟨v_{1}, v_{2}⟩.

4. If Ψ; Θ; ·; ·; · ⊢ v : ∃t:κ.σ then v = ⟨t=τ, v′:σ⟩.

5. If Ψ; Θ; ·; ·; · ⊢ v : ∃α:Θ′.σ then v = ⟨α:Θ′ =σ_{1}, v′:σ_{2}⟩.

6. If Ψ; Θ; ·; ·; · ⊢ v : ∃r ∈Θ′.(σ at r) then v = ⟨r ∈ Θ′ = ρ, v′⟩.

7. If Ψ; Θ; ·; ·; · ⊢ v : ∀⟦τ⃗⟧[r⃗](σ⃗) ⟶^{ρ} 0 then v = v′⟦τ⃗⟧.

8. If Ψ; Θ; ·; ·; · ⊢ v : ∀[t⃗:κ][r⃗](σ⃗) → 0

then v = λ[t⃗:κ][r⃗](x⃗:σ).e. Proof The proof follows from the inspection of the typing rules for values. □ Proposition A.1.47 (Progress) If ⊢ (M, e) then either e = halt v or there exists a (M′, e′) such that (M, e) ⟹ (M′, e′). Proof The proof is by induction over the structure of e. The ifreg cases follow in a straightforward way. We will consider only the region open construct. case open v as ⟨r, x⟩ in e: We know that

Ψ; Dom(Ψ); ·; ·; · ⊢ open v as ⟨r, x⟩ in e

This means that

Ψ; Dom(Ψ); ·; ·; · ⊢ v : ∃r ∈Θ.(σ at r)

By lemma A.1.46 we get that v = ⟨r ∈ Θ = ρ, v′⟩. By the typing rule for region packages, we get that ρ ∈ Dom(Ψ). This means that ρ = ν for some ν. Therefore, we have that

Ψ; Dom(Ψ); ·; ·; · ⊢ open ⟨r ∈ Θ = ν, v′⟩ as ⟨r, x⟩ in e

But by the operational semantics this goes to (M, e[ν, v′/r, x]). □

A.2 Soundness of typed regions

In this section again, I will assume that variable names are unique. It is easy to show by induction that extending Δ, or Γ with additional bindings is safe. Also I will assume that those environments use unique variable names, so there is no duplicate bindings. Finally, the order of bindings in Ψ and Γ is irrelevant.

A.2.1 Soundness of the core language

Lemma A.2.1 (Value Substitution) If Ψ; Δ; Θ; Γ ⊢ v : σ then:

• If Ψ; Δ; Θ; Γ, x:σ ⊢ v′ : σ′ then Ψ; Δ; Θ; Γ ⊢ v′[v/x] : σ′.

• If Ψ; Δ; Θ; Γ, x:σ ⊢ e then Ψ; Δ; Θ; Γ ⊢ e[v/x].

• and similarly for all rules using Γ.

Proof The cases for n, x, λ, and ν.ℓ are immediate, the rest follows by induction on the typing derivation, using the fact that Γ is not ordered. □

Lemma A.2.2 (Type Substitution) If Δ ⊢^{CiC} φ : κ then:

• If Δ, t:κ ⊢^{CiC} φ′ : κ′ then Δ[φ/t] ⊢^{CiC} φ′[φ/t] : κ′[φ/t].

• If Ψ; Δ, t:κ; Θ; Γ ⊢ v : σ then Ψ; Δ[φ/t]; Θ[φ/t]; Γ[φ/t] ⊢ v[φ/t] : σ[φ/t].

• If Ψ; Δ, t:κ; Θ; Γ ⊢ e then Ψ; Δ[φ/t]; Θ[φ/t]; Γ[φ/t] ⊢ e[φ/t].

• and similarly for all rules using Δ.

Proof The first case is actually a property of CiC. The cases for n and x are immediate. The case for λ follows from the fact that Δ, Θ, σ⃗, and e are all closed w.r.t Δ′.

The case for ν.ℓ follows from the fact that Ψ is closed w.r.t type variables, so τ is also necessarily closed. The rest follows by induction on the typing derivations. □

Lemma A.2.3 (Ψ Weakening) If Ψ; Δ; Θ; Γ ⊢ v : σ and ν.n ∉ Dom(Ψ) and ⊢ Ψ, ν.n ↦ τ then Ψ, ν.n ↦ τ; Δ; Θ; Γ ⊢ v : σ. Proof By induction on the typing derivation: the only place where Ψ is used is in the rule for ν.n where weakening is trivially safe. □

Lemma A.2.4 (Θ Irrelevance) If Ψ; Δ; Θ, ρ↦(φ, n); Γ ⊢ v : σ then Ψ; Δ; Θ, ρ↦(φ′, n′); Γ ⊢ v : σ. Proof Trivial since only Dom(Θ) is actually used in the rules. □

Lemma A.2.5 (Θ Equivalence) If Θ ∼ Θ′ and Ψ; Δ; Θ; Γ ⊢ e then Ψ; Δ; Θ′; Γ ⊢ e and similarly for all judgments using Θ. Proof The only possible difference between Θ and Θ′ is ordering and induction over the judgments shows that ordering of Θ is not relevant. □

Lemma A.2.6 (Type Preservation) If ⊢ (M; Θ; Ψ; e) and (M; Θ; Ψ; e) ⟹ (M′; Θ′; Ψ′; e′) then ⊢ (M′; Θ′; Ψ′; e′). Proof

Case let x = v in e ⟹ e[v/x]:

⊢ (M; Θ; Ψ; let x = v in e)
Ψ; Θ ⊢ M  • ⊢ Θ  ⊢ Ψ  Ψ; •; Θ; •, ⊢ let x = v in e Ψ; •; Θ; • ⊢ v : σ  Ψ; •; Θ; •, x:σ ⊢ e (inv) Ψ; •; Θ; • ⊢ e[v/x] (subst)

(inv)

⊢ (M; Θ; Ψ; e[v/x])

Case let x = π_{i}(v_{1}, v_{2}) in e ⟹ e[v^{i}/x]:

As before M, Ψ, and Θ are unchanged, so we just need to show that Ψ; •; Θ; •, ⊢ let x = π_{i}(v_{1}, v_{2}) in e implies Ψ; •; Θ; • ⊢ e[v^{i}/x]:

Ψ; •; Θ; •, ⊢ let x = π_{i}(v_{1}, v_{2}) in e
Ψ; •; Θ; •, x:σ_{i} ⊢ e  {Ψ; •; Θ; • ⊢ π_{i}(v_{1}, v_{2}) : σ_{i} \atop Ψ; •; Θ; • ⊢ (v_{1}, v_{2}) : σ_{1} × σ_{2}} (inv) Ψ; •; Θ; • ⊢ v_{i} : σ_{i} (inv)

(inv)

Ψ; •; Θ; • ⊢ e[v^{i}/x] (subst)

Case ν.n[φ⃗](v⃗) ⟹ e[φ⃗, v⃗/t⃗, x⃗]:

Again, M, Θ, and Ψ are unchanged.

Ψ; Θ ⊢ M M(ν.n) = λ[\overrightarrow{t:κ}]\{Θ′\}(\overrightarrow{x:σ}).e

{Ψ; •; Θ; • ⊢ ν.n[φ⃗](v⃗) \atop Ψ; •; Θ; • ⊢ ν.n ↦ ∀[\overrightarrow{t:κ}]\{Θ′\}(σ⃗) → 0} (inv)

ν ∈ Dom(Θ) (inv)

Ψ; •; •; • ⊢ λ[\overrightarrow{t:κ}]\{Θ′\}(\overrightarrow{x:σ}).e : ∀[\overrightarrow{t:κ}]\{Θ′\}(σ⃗) → 0 (inv)

{\overrightarrow{t:κ} ⊢ Θ′ \atop • ⊢ Θ′[φ⃗/t⃗]}  {Ψ; \overrightarrow{t:κ}; Θ′; \overrightarrow{x:σ} ⊢ e \atop Ψ; •; Θ′[φ⃗/t⃗]; \overrightarrow{x:σ}[φ⃗/t⃗] ⊢ e[φ⃗/t⃗]} (type subst) (inv)
Ψ; •; Θ; \overrightarrow{x:σ}[φ⃗/t⃗] ⊢ e[φ⃗/t⃗]

(Θ equiv)

Ψ; •; Θ; • ⊢ e[φ⃗, v⃗/t⃗, x⃗]

(value subst)

The two type substitution steps rely implicitly on the fact that • ⊢ φ⃗ : \overrightarrow{t:κ} which we get by inversion from Ψ; •; Θ; • ⊢ ν.n[φ⃗](v⃗). This same inversion gives us Θ ∼ Θ′[φ⃗/t⃗] which is implicitly used in the Θ equivalence step and Ψ; •; Θ; • ⊢ v_{i} : σ_{i}[φ⃗/t⃗], also used implicitly in the value substitution.

Case let x = get ν.n in e ⟹ e[M(ν.n)/x]:

Again M, Ψ, and Θ stay unchanged, so we only need to show:

Ψ; •; Θ; • ⊢ let x = get ν.n in e

Ψ; •; Θ; •, x:φ n τ ⊢ e  {Ψ; •; Θ; • ⊢ get ν.n : φ n τ \atop Ψ; •; Θ; • ⊢ ν.n ↦ φ n τ} (inv) Θ(ν) = (φ, m) {Ψ; •; Θ; • ⊢ ν.n : τ at ν.n \atop τ = Ψ(ν.n)} (inv) Ψ; •; Θ; • ⊢ M(ν.n) : φ n τ (Ψ; Θ ⊢ M) (inv)

(inv)

Ψ; •; Θ; • ⊢ e[M(ν.n)/x] (subst)

Case let x = put[ν, τ] v in e ⟹ e[ν.n/x]:

This time M, Ψ, and Θ change. So we need to show well-formedness of environments. The case of Θ, ν ↦(φ, n+1) is trivial since the well-formedness only requires a natural number but does not care about its value. The proof for Ψ, ν.n ↦ τ is:

⊢ Ψ Ψ; •; Θ, ν ↦(φ, n); • ⊢ let x = put[ν, τ] v in e • ⊢^{CiC} ν : R κ  • ⊢^{CiC} τ : κ (inv)
⊢ Ψ, ν.n ↦ τ

The proof for M, ν.n ↦ v is:

Ψ; Θ, ν ↦(φ, n) ⊢ M

Ψ; •; Θ, ν ↦(φ, n); • ⊢ let x = put[ν, τ] v in e

Ψ; •; Θ; • ⊢ v : φ n τ (inv)

Ψ, ν.n ↦ τ; •; Θ; • ⊢ v : φ n τ

(Ψ weakening)

Ψ, ν.n ↦ τ; Θ, ν ↦(φ, n+1) ⊢ M, ν.n ↦ v

The weakening step is justified by the fact that ⊢ Ψ, ν.n ↦ τ and Ψ; Θ, ν ↦(φ, n) ⊢ M implies ν.n ∉ Dom(Ψ) by inversion. We still need to show the well-formedness of e[ν.n/x]:

Ψ; •; Θ, ν ↦(φ, n); • ⊢ let x = put[ν, τ] v in e

Ψ; •; Θ, ν ↦(φ, n+1); •, x:τ at ν.n ⊢ e (inv)

Ψ, ν.n ↦ τ; •; Θ, ν ↦(φ, n+1); •, x:τ at ν.n ⊢ e

(Ψ weakening)

Ψ, ν.n ↦ τ; •; Θ, ν ↦(φ, n+1); • ⊢ e[ν.n/x] (subst)

The weakening step is justified as above. The substitution step relies on the fact that Ψ, ν.n ↦ τ; •; Θ, ν ↦(φ, n+1); • ⊢ ν.n : τ at ν.n which follows from the typing rule of ν.n.

Case freergn ν; e ⟹ e:

Since M, Ψ, and Θ change we need to show their well-formedness. For Ψ and Θ this is trivial since their elements are independent. For M, we have that Δ ⊢ Θ, ν ↦(φ, n) implies that ν ∉ Dom(Θ), so Ψ\ν; Θ ⊢ M\ν derives straightforwardly from Ψ; Θ, ν ↦(φ, n) ⊢ M. The proof for the well-formedness of the term is:

Ψ; •; Θ, ν ↦(φ, n); • ⊢ freergn ν; e

Ψ; •; Θ; • ⊢ e (inv)

Ψ\ν; •; Θ; • ⊢ e (ν ∉ Dom(Θ))

where the last step is justified by the fact that Ψ is only used in the typing rule for ν.n and that only if ν ∈ Dom(Θ).

Case let r = newrgn φ in e ⟹ e[ν/r]:

Since Θ changes, so we need to show well-formedness of Θ and M. For M it’s easy since the new region has size zero, so we only need to show ν.j ∉ Dom(Ψ) which follows from the fact that ν is a brand new unique value. For Θ the proof is:

• ⊢ Θ ν ∉ Dom(Θ) • ⊢^{CiC} ν : R κ • ⊢^{CiC} 0 : Nat Ψ; •; Θ; • ⊢ let r = newrgn φ in e • ⊢^{CiC} φ : Nat → κ → Ω

• ⊢ Θ, ν ↦(φ, 0)

The proof for the well-formedness of e[ν/r] is:

• ⊢^{CiC} ν : R κ Ψ; •; Θ; • ⊢ let r = newrgn φ in e Ψ; •, r:R κ; Θ, r ↦(φ, 0); • ⊢ e (inv)

Ψ; •; Θ, ν ↦(φ, 0); • ⊢ e[ν/r] (subst)

Case cast[P] ν ↦ φ; e ⟹ e:

Since Θ changes, we need to show well-formedness of M and Θ. For Θ, we first need to show that the new type of the region has the proper kind. I could have added it to the typing rule as well, but it can be inferred:

{• ⊢ Θ, ν ↦(φ, n) \atop • ⊢^{CiC} φ : Nat → κ → Ω} (inv)

Ψ; •; Θ, ν ↦(φ, n); • ⊢ cast[P] ν ↦ φ′; e

• ⊢^{CiC} P : Πi:0..n−1. Πt:κ. (φ i t) = (φ′ i t) (inv)

kindof(φ) = kindof(φ′)

(CiC)

• ⊢^{CiC} φ′ : Nat → κ → Ω

The well-formedness of Θ is then immediate:

• ⊢^{CiC} φ′ : Nat → κ → Ω  • ⊢ Θ, ν ↦(φ, n) • ⊢ Θ r ∉ Dom(Θ) Δ ⊢^{CiC} ν : R κ Δ ⊢^{CiC} n : Nat
• ⊢ Θ, ν ↦(φ′, n)

The well-formedness of M is trivially preserved for regions other than ν, so we only need to show preservation of M for ν. Since the size does not change and Ψ does not change, the part of the rule that checks ν.j ∉ Dom(Ψ) when j ≥ n is also trivially preserved and the rest follows:

{∀j:0..n−1 .Ψ; •; Θ, ν ↦(φ, n); • ⊢ M(ν.j) : φ j (Ψ(ν.j)) \atop ∀j:0..n−1 .Ψ; •; Θ, ν ↦(φ, n); • ⊢ M(ν.j) : φ′ j (Ψ(ν.j))} (∀t:κ.(φ′ j t) = (φ j t))
∀j:0..n−1 .Ψ; •; Θ, ν ↦(φ′, n); • ⊢ M(ν.j) : φ′ j (Ψ(ν.j))

(Θ irrelevance)

Finally, the well-formedness of e is trivial:

Ψ; •; Θ, ν ↦(φ′, n); • ⊢ cast[P] ν ↦ φ; e

Ψ; •; Θ, ν ↦(φ, n); • ⊢ e (inv)

Case set ν.n :=^{φ} v; e ⟹ e:

Since M and Θ change, we need to show their well-formedness. For Θ all that changes is the φ′ type of the region ν, so we only need to show that the new type has the proper kind:

• ⊢^{CiC} φ′ : Nat → κ → Ω {Ψ; •; Θ, ν ↦(φ′, n′); • ⊢ set ν.n :=^{φ} v; e \atop • ⊢^{CiC} φ : κ → Ω} (inv)
• ⊢^{CiC} upd φ′ n φ : Nat → κ → Ω

The proof for M is decomposed into the part that changed (i.e. the location ν.n):

Ψ; •; Θ, ν ↦(φ′, n′); • ⊢ set ν.n :=^{φ} v; e

{Ψ; •; Θ, ν ↦(φ′, n′); • ⊢ v : φ τ \atop Ψ; •; Θ, ν ↦(upd φ′ n φ, n′); • ⊢ v : φ τ}  {Ψ; •; Θ, ν ↦(φ′, n′); • ⊢ ν.n : τ at ν.n \atop τ = Ψ(ν.n)}

(inv)

Ψ; •; Θ, ν ↦(upd φ′ n φ, n′); • ⊢ v : (upd φ′ n φ) n (Ψ(ν.n))

where the two middle steps use Θ irrelevance and inversion respectively. And the rest follows from Θ irrelevance:

j ≠ n Ψ; •; Θ, ν ↦(φ′, n′); • ⊢ M(ν.j) : φ′ j (Ψ(ν.j))
Ψ; •; Θ, ν ↦(upd φ′ n φ, n′); • ⊢ M(ν.j) : (upd φ′ n φ) j (Ψ(ν.j))

Finally, the term well-formedness follows immediately by inversion of the typing rule:

Ψ; •; Θ, ν ↦(φ′, n′); • ⊢ set ν.n :=^{φ} v; e

Ψ; •; Θ, ν ↦(upd φ′ n φ, n′); • ⊢ e (inv)

□

Lemma A.2.7 (Progress) If ⊢ (M; Θ; Ψ; e), either e = halt v or (M; Θ; Ψ; e) ⟹ (M′; Θ′; Ψ′; e′). Proof By case analysis: if ⊢ (M; Θ; Ψ; e) then the type derivation of e can be one of the following:

APPENDIX A. SOUNDNESS PROOFS  191

Case halt v: Trivial. Case let x = op in e: This case is subdivided into the next three subcases since op can be either a value v or a projection π_{i}v or a memory access get v. Case let x = v in e: This case is trivial: (M; Θ; Ψ; let x = v in e) ⟹ (M; Θ; Ψ; e[v/x]) Case let x = π_{i}v in e:

Ψ; •; Θ; • ⊢ let x = π_{i}v in e

Ψ; •; Θ; • ⊢ π_{i}v : σ (inv)

Ψ; •; Θ; • ⊢ v : σ_{1} × σ_{2} (inv)

v = (v_{1}, v_{2})

(inv knowing that Γ = •)

So we can reduce with: (M; Θ; Ψ; let x = π_{i}(v_{1}, v_{2}) in e) ⟹ (M; Θ; Ψ; e[v^{i}/x]) Case let x = get v in e:

Ψ; •; Θ; • ⊢ let x = get v in e

Ψ; •; Θ; • ⊢ get v : σ

(inv)

Ψ; •; Θ; • ⊢ v ↦ σ (inv)

Ψ; •; Θ; • ⊢ v : τ at ρ.n Θ(ρ) = (φ, m)

(inv)

v = ν.n Ψ(ν.n) = τ

(inv knowing that Γ = •)

Combining the above with Ψ; Θ ⊢ M, we can derive that M(ν.n) exists (and has type σ), so we can reduce with: (M; Θ; Ψ; let x = get ν.n in e) ⟹ (M; Θ; Ψ; e[M(ν.n)/x]) Case v[φ⃗](v⃗):

{Ψ; •; Θ; • ⊢ v[φ⃗](v⃗) \atop Ψ; •; Θ; • ⊢ v ↦ ∀[\overrightarrow{t:κ}]\{Θ′\}(σ⃗) → 0} (inv)
φ n τ = ∀[\overrightarrow{t:κ}]\{Θ′\}(σ⃗) → 0  \begin{matrix}Ψ; •; Θ; • ⊢ v : τ at ρ.n & Θ(ρ) = (φ, m) \\ ρ = ν  v = ν.n  τ = Ψ(ν.n) \end{matrix} (inv & Γ = •) (inv)
φ n (Ψ(ν.n)) = ∀[\overrightarrow{t:κ}]\{Θ′\}(σ⃗) → 0 Θ(ν) = (φ, m)

APPENDIX A. SOUNDNESS PROOFS  192

Combining the above with Ψ; Θ ⊢ M we get:

Ψ; Θ ⊢ M Θ(ν) = (φ, m) φ n (Ψ(ν.n)) = ∀[\overrightarrow{t:κ}]\{Θ′\}(σ⃗) → 0
Ψ; •; Θ; • ⊢ M(ν.n) : ∀[\overrightarrow{t:κ}]\{Θ′\}(σ⃗) → 0
M(ν.n) = λ[\overrightarrow{t:κ}]\{Θ′\}(\overrightarrow{x:σ}).e

(inv knowing that Γ = •)

So we can reduce with: (M; Θ; Ψ; ν.n[φ⃗](v⃗)) ⟹ (M; Θ; Ψ; e[φ⃗, v⃗/t⃗, x⃗]) Case let x = put[ρ, τ] v in e:

• ⊢ Θ Ψ; •; Θ; • ⊢ let x = put[ρ, τ] v in e Θ = Θ′, ρ↦(φ, n) (inv)

• ⊢^{CiC} ρ : R κ (inv)

ρ = ν

(inv with Δ = •)

So we can reduce with: (M; Θ; Ψ; let x = put[ν, τ] v in e) ⟹ (M, ν.n ↦ v; Θ′, ν ↦(φ, n+1); Ψ, ν.n ↦ τ; e[ν.n/x]) Case freergn ρ; e: The proof for this case is actually the same as the proof above for put. We end up with ρ = ν and can thus reduce with: (M; Θ; Ψ; freergn ν; e) ⟹ (M\ν; Θ′; Ψ\ν; e) Case cast[P] ρ ↦ φ; e: Here the proof is also the same as above, and we reduce with: (M; Θ; Ψ; cast[P] ν ↦ φ; e) ⟹ (M; Θ′, ν ↦(φ, n); Ψ; e) Case let r = newrgn φ in e:

Ψ; •; Θ; • ⊢ let r = newrgn φ in e

• ⊢^{CiC} φ : Nat → κ → Ω (inv)

So we can reduce with: (M; Θ; Ψ; let r = newrgn φ in e) ⟹ (M; Θ, ν ↦ (φ, 0); Ψ; e[ν/r])

APPENDIX A. SOUNDNESS PROOFS  193

Case set v :=^{φ} v′; e:

Ψ; •; Θ; • ⊢ set v :=^{φ} v′; e

Θ = Θ′, ρ↦(φ, n)  {Ψ; •; Θ; • ⊢ v : τ at ρ.m \atop ρ = ν  v = ν.m} (inv with Γ = •)

(inv)

So we can reduce with:

(M; Θ, ν ↦(φ′, n′); Ψ; set ν.n :=^{φ} v; e) ⟹ (M, ν.n ↦ v; Θ, ν ↦(upd φ′ n φ, n′); Ψ; e) □

A.2.2 Soundness of the full language

The statements and proofs of Ψ Weakening, Value Substitution, and Type Substitution lemmas are the same as they were for the core language, so I will not restate them.

Lemma A.2.8 (Θ irrelevance) If Ψ; Δ; Θ, ρ↦(φ, n); Γ ⊢ v : σ then Ψ; Δ; Θ, ρ↦(φ′, n′); Γ ⊢ v : σ. Proof Simple proof by induction on the type derivation of v. There are only two cases where Θ is used: First, in the typing of ν.n where only Dom(Θ) is used, so the proof is immediate. Second, in the typing of ⟨ρ, v⟩, where we need to show that Θ ⊢ ρ ∈ ρ⃗ implies Θ′ ⊢ ρ ∈ ρ⃗, which is also immediate because this judgment only depends on region aliases and not region types. □

Lemma A.2.9 (Θ equivalence) If Θ′ ◁ Θ then:

• Θ ⊢ ρ ∈ ρ⃗ implies Θ′ ⊢ ρ ∈ ρ⃗.

• Θ ◁ Θ′′ implies Θ′ ◁ Θ′′.

• Ψ; Δ; Θ; Γ ⊢ v : σ implies Ψ; Δ; Θ′; Γ ⊢ v : σ.

• Θ ⊢ τ at ρ.n ↦ σ implies Θ′ ⊢ τ at ρ.n ↦ σ.

• Ψ; Δ; Θ; Γ ⊢ v ↦ σ implies Ψ; Δ; Θ′; Γ ⊢ v ↦ σ.

• Ψ; Δ; Θ; Γ ⊢ op : σ implies Ψ; Δ; Θ′; Γ ⊢ op : σ.

• Ψ; Δ; Θ, Θ′′; Γ ⊢ e implies Ψ; Δ; Θ,′ Θ′′; Γ ⊢ e.

Proof The equivalence relation of the full language is more complex than the one of the core language, so the proof is not as immediate. But it is still fairly simple since the only thing that can change is ordering, which we have seen to be unimportant, and region aliases.

All proofs are done by induction over the main derivation, except for the proof for Θ ⊢ ρ ∈ ρ⃗ which is done by induction over the derivation of Θ′ ◁ Θ. All cases of this first proof follow immediately by induction or by inversion. The only moderately interesting cases of the proof for Θ ◁ Θ′′ are the ones for region aliases:

{Θ, ρ↦ρ⃗ ◁ Θ′′ \atop Θ ◁ Θ′′} {Θ′ ◁ Θ, ρ↦ρ⃗ \atop Θ′ ◁ Θ}

Θ′ ◁ Θ′′ and

Θ′ ◁ Θ  {Θ ◁ Θ′′, ρ↦ρ⃗ \atop Θ ◁ Θ′′  Θ ⊢ ρ ∈ ρ⃗} (inv)

Θ′ ◁ Θ′′ Θ′ ⊢ ρ ∈ ρ⃗ (ind)

Θ′ ◁ Θ′′, ρ↦ρ⃗

For the proof for v : σ, the only case where Θ is used is for ν.n where one only needs to show that Θ(ν) = (φ, m) implies Θ′(ν) = (φ, m). The rest of the proofs follows directly by induction. □

Lemma A.2.10 (Subsumption) If Δ ⊢ σ ◁ σ′ and Δ ⊢^{CiC} σ : Ω then Δ ⊢^{CiC} σ′ : Ω. If Δ ⊢ σ ◁ σ′ and Ψ; Δ; Θ; • ⊢ v : σ then Ψ; Δ; Θ; • ⊢ v : σ′. Proof The first part is by induction on the derivation of σ ◁ σ′. The second is by induction over the derivation of v : σ. The interesting case is for the existential. Inverting Δ ⊢ ∃r ∈ρ⃗.σ ◁ ∃r ∈ρ⃗′.σ′ gives us ρ⃗ ⊂ ρ⃗′, and Δ, r:R κ ⊢ σ ◁ σ′, and Δ ⊢^{CiC} ρ′_{i} : R κ. Inverting Ψ; Δ; Θ; • ⊢ ⟨ρ, v⟩ : ∃r ∈ρ⃗.σ gives us Θ ⊢ ρ ∈ ρ⃗, and Ψ; Δ; Θ; • ⊢ v : σ[ρ/r], and Δ ⊢^{CiC} ρ : R κ, and Δ, r:R κ ⊢^{CiC} σ : Ω. The interesting part is:

\begin{matrix}ρ⃗ ⊂ ρ⃗′ & Θ ⊢ ρ ∈ ρ⃗ \\ Θ ⊢ ρ ∈ ρ⃗′ \end{matrix} \begin{matrix}Δ, r:R κ ⊢ σ ◁ σ′ & Δ ⊢^{CiC} ρ : R κ \\ Δ ⊢ σ[ρ/r] ◁ σ′[ρ/r] \end{matrix} Ψ; Δ; Θ; • ⊢ v : σ[ρ/r]

Ψ; Δ; Θ; • ⊢ v : σ′[ρ/r]
Ψ; Δ; Θ; Γ ⊢ ⟨ρ, v⟩ : ∃r ∈ρ⃗′.σ′

The inference at the extreme left is actually a small lemma that we need to show separately via trivial induction over the derivation of Θ ⊢ ρ ∈ ρ⃗. □

Lemma A.2.11 (Type Preservation) If ⊢ (M; Θ; Ψ; e) and (M; Θ; Ψ; e) ⟹ (M′; Θ′; Ψ′; e′) then ⊢ (M′; Θ′; Ψ′; e′). Proof The typing and reduction rules for the terms that were already in the core language are the same and their proof is also the same except of course that they use the new lemmas above. For cast the rule has been slightly modified to use ◁ rather than equality, but the proof is not affected. None of the new cases modify the environment, so we only need to show the well-formedness of the term:

Case let x = cast[P] v in e ⟹ e[v/x]:

Ψ; •; Θ; • ⊢ let x = cast[P] v in e

Ψ; •; Θ; •, x:σ ⊢ e  Ψ; •; Θ; • ⊢ cast[P] v : σ Ψ; •; Θ; • ⊢ v : σ′  {• ⊢^{CiC} P : σ′ ◁ σ \atop • ⊢ σ′ ◁ σ} (inv) Ψ; •; Θ; • ⊢ v : σ (subsumption)

(inv)

Ψ; •; Θ; • ⊢ e[v/x] (subst)

Case let ⟨t, x⟩ = open ⟨φ, v⟩ in e ⟹ e[φ, v/t, x]:

Ψ; •; Θ; • ⊢ let ⟨t, x⟩ = open ⟨φ, v⟩ in e

Ψ; •, t:κ; Θ; •, x:σ[t/t_{′}] ⊢ e  {Ψ; •; Θ; • ⊢ ⟨φ, v⟩ : ∃t′.κσ \atop • ⊢^{CiC} φ : κ  Ψ; •; Θ; • ⊢ v : σ[φ/t_{′}]} (inv) (inv)

Ψ; •; Θ[φ/t_{′}]; •, x:σ[φ/t_{′}] ⊢ e[φ/t] Ψ; •; Θ; • ⊢ v : σ[φ/t_{′}] (subst)

Ψ; •; Θ; •, x:σ[φ/t_{′}] ⊢ e[φ/t] Ψ; •; Θ; • ⊢ v : σ[φ/t_{′}]

(t′ free in Θ)

Ψ; •; Θ; • ⊢ e[φ, v/t, x] (subst)

Case let ⟨r, x⟩ = openrgn ⟨ρ, v⟩ in e ⟹ e[ρ, v/r, x]:

Ψ; •; Θ; • ⊢ let ⟨r, x⟩ = openrgn ⟨ρ, v⟩ in e

Ψ; •, r:R κ; Θ, r ↦ρ⃗; •, x:σ[r/r_{′}] ⊢ e Ψ; •; Θ; • ⊢ ⟨ρ, v⟩ : ∃r′ ∈ρ⃗.σ • ⊢^{CiC} ρ_{i} : R κ Ψ; •; Θ; • ⊢ v : σ[ρ/r_{′}] \begin{matrix}Θ ⊢ ρ ∈ ρ⃗ & • ⊢^{CiC} ρ_{i} : R κ \\ • ⊢^{CiC} ρ : R κ \end{matrix}

Ψ; •; Θ[ρ/r], ρ↦ρ⃗[ρ/r]; •, x:σ[ρ/r_{′}] ⊢ e[ρ/r] Ψ; •; Θ; • ⊢ v : σ[ρ/r_{′}]

Ψ; •; Θ, ρ↦ρ⃗; •, x:σ[ρ/r_{′}] ⊢ e[ρ/r] Ψ; •; Θ; • ⊢ v : σ[ρ/r_{′}] (r free in Θ)

Ψ; •; Θ, ρ↦ρ⃗; • ⊢ e[ρ, v/r, x] (subst)

Ψ; •; Θ; • ⊢ e[ρ, v/r, x] (Θ equiv)

The last step implicitly uses the fact that Θ ⊢ ρ ∈ ρ⃗ which implies that Θ ◁ Θ, ρ↦ρ⃗. The step that derives • ⊢^{CiC} ρ : R κ implicitly uses • ⊢ Θ and is easy to show.

Case iffirst ν (x, t . e_{1}) (e_{2}) ⟹ e_{2}:

Ψ; •; Θ, ν ↦(φ, 0); • ⊢ iffirst ν (x, t . e_{1}) (e_{2})

Ψ; •; Θ, ν ↦(φ, 0); • ⊢ e_{2} (inv)

Case iffirst ν (x, t . e_{1}) (e_{2}) ⟹ e_{1}[τ, ν.0/t, x]:

Ψ; •; Θ, ν ↦(φ, n); • ⊢ iffirst ν (x, t . e_{1}) (e_{2})

Ψ; •, t:κ; Θ, ν ↦(φ, n); •, x:t at ν.0 ⊢ e_{1} \begin{matrix}{• ⊢^{CiC} ν : R κ \atop • ⊢^{CiC} Ψ(ν.0) : κ} & (⊢ Ψ) \\ • ⊢^{CiC} τ : κ \end{matrix} ; (Ψ(ν.0) = τ) (inv)

Ψ; •; Θ[τ/t], ν ↦(φ[τ/t], n); •, x:τ at ν.0 ⊢ e_{1}[τ/t] (subst)

Ψ; •; Θ, ν ↦(φ, n); •, x:τ at ν.0 ⊢ e_{1}[τ/t]

(t free in Θ)

Ψ; •; Θ, ν ↦(φ, n); • ⊢ e_{1}[τ, ν.0/t, x] (subst)

The last step uses the fact that ν.0 : τ at ν.0 which follows from the fact that Ψ(ν.0) = τ.

Case ifnext ν.n (x, t . e_{1}) (e_{2}) ⟹ e_{2}:

Ψ; •; Θ, ν ↦(φ, n+1); • ⊢ ifnext ν.n (x, t . e_{1}) (e_{2})

Ψ; •; Θ, ν ↦(φ, n+1); • ⊢ ν.n : τ at ν.n Ψ; •; Θ, ν ↦(φ, n+1); • ⊢ e_{2} (inv)

Ψ; •; Θ, ν ↦(φ, n+1); • ⊢ e_{2}

Case ifnext ν.m (x, t . e_{1}) (e_{2}) ⟹ e_{1}[τ, ν.m+1/t, x]:

Ψ; •; Θ, ν ↦(φ, n); • ⊢ ifnext ν.m (x, t . e_{1}) (e_{2})

\begin{matrix}{• ⊢^{CiC} ν : R κ \atop • ⊢^{CiC} Ψ(ν.m+1) : κ} & (⊢ Ψ) \\ • ⊢^{CiC} τ : κ \end{matrix} ; (Ψ(ν.m+1) = τ) Ψ; •, t:κ; Θ, ν ↦(φ, n); •, x:t at ν.m+1 ⊢ e_{1}

(inv)

⋮ .
Ψ; •; Θ, ν ↦(φ, n); • ⊢ e_{1}[τ, ν.m+1/t, x]

The last steps are the same as for the proof of iffirst.

□

Lemma A.2.12 (Progress) If ⊢ (M; Θ; Ψ; e), either e = halt v or (M; Θ; Ψ; e) ⟹ (M′; Θ′; Ψ′; e′).

Proof By induction on the typing derivation of e. As was the case for the proof of type preservation, for all the reduction steps that were already in the core language, the proof is the same so I omit it.

Case let ⟨t, x⟩ = open v in e:

Ψ; •; Θ; • ⊢ let ⟨t, x⟩ = open v in e

Ψ; •; Θ; • ⊢ v : ∃t′.κσ (inv)

v = ⟨φ, v′⟩

(inv knowing that Γ = •)

So we reduce with: (M; Θ; Ψ; let ⟨t, x⟩ = open ⟨φ, v′⟩ in e) ⟹ (M; Θ; Ψ; e[φ, v′/t, x])

Case let ⟨r, x⟩ = openrgn v in e:

Ψ; •; Θ; • ⊢ let ⟨r, x⟩ = openrgn v in e

Ψ; •; Θ; • ⊢ v : ∃r′ ∈ρ⃗.σ (inv)

v = ⟨ρ, v′⟩

(inv knowing that Γ = •)

So we reduce with: (M; Θ; Ψ; let ⟨r, x⟩ = openrgn ⟨ρ, v⟩ in e) ⟹ (M; Θ; Ψ; e[ρ, v/r, x])

Case iffirst ρ (x, t . e_{1}) (e_{2}):

{Ψ; •; Θ, ρ↦(φ, n); • ⊢ iffirst ρ (x, t . e_{1}) (e_{2}) \atop ρ = ν} (Δ is empty)

If n = 0 we reduce with: (M; Θ; Ψ; iffirst ν (x, t . e_{1}) (e_{2})) ⟹ (M; Θ; Ψ; e_{2}). Otherwise:

\begin{aligned}n ≠ 0 & ν ∈ Dom(Θ, ρ↦(φ, n)) & Ψ; Θ, ρ↦(φ, n) ⊢ M \\ & τ = Ψ(ν.0) \end{aligned}

so we can reduce with: (M; Θ; Ψ; iffirst ν (x, t . e_{1}) (e_{2})) ⟹ (M; Θ; Ψ; e_{1}[τ, ν.0/t, x])

Case ifnext v (x, t . e_{1}) (e_{2}):

{Δ = • \atop ρ = ν} Ψ; •; Θ, ρ↦(φ, n); • ⊢ ifnext v (x, t . e_{1}) (e_{2}) Ψ; •; Θ, ρ↦(φ, n); • ⊢ v : τ at ρ.m (inv)
Ψ; •; Θ, ν ↦(φ, n); • ⊢ v : τ at ν.m
v = ν.m

(inv knowing that Γ = •)

If n = m+1 we reduce with: (M; Θ; Ψ; ifnext ν.m (x, t . e_{1}) (e_{2})) ⟹ (M; Θ; Ψ; e_{2}). Otherwise:

Ψ; Θ, ν ↦(φ, n) ⊢ M \begin{matrix}Ψ; Θ, ν ↦(φ, n) ⊢ M & {Ψ; •; Θ, ν ↦(φ, n); • ⊢ ν.m : τ at ν.m \atop Ψ(ν.m) = τ} \\ m+1 ≤ n \end{matrix} ; n ≠ m+1 ; m+1 < n
τ = Ψ(ν.m+1)

so we can reduce with: (M; Θ; Ψ; ifnext ν.m (x, t . e_{1}) (e_{2})) ⟹ (M; Θ; Ψ; e_{1}[τ, ν.m+1/t, x]) □