









Study with the several resources on Docsity
Earn points by helping other students or get them with a premium plan
Prepare for your exams
Study with the several resources on Docsity
Earn points to download
Earn points by helping other students or get them with a premium plan
A new linear type system designed for low-level languages that allows memory reuse at different types, object initialization, safe deallocation, and tracking of sharing in data structures. The system uses aliasing constraints and singleton types to accurately model the store and ensure safety in the presence of destructive operations.
Typology: Study notes
1 / 16
This page cannot be seen from the preview
Don't miss anything!










Frederick Smith David Walker Greg Morrisett
Cornell University
Abstract. Linear type systems allow destructive operations such as ob- ject deallocation and imperative updates of functional data structures. These operations and others, such as the ability to reuse memory at different types, are essential in low-level typed languages. However, tra- ditional linear type systems are too restrictive for use in low-level code where it is necessary to exploit pointer aliasing. We present a new typed language that allows functions to specify the shape of the store that they expect and to track the flow of pointers through a computation. Our type system is expressive enough to represent pointer aliasing and yet safely permit destructive operations.
Linear type systems [26, 25] give programmers explicit control over memory re- sources. The critical invariant of a linear type system is that every linear value is used exactly once. After its single use, a linear value is dead and the system can immediately reclaim its space or reuse it to store another value. Although this single-use invariant enables compile-time garbage collection and imperative updates to functional data structures, it also limits the use of linear values. For example, x is used twice in the following expression: let x = 〈 1 , 2 〉 in let y = fst(x) in let z = snd(x) in y + z. Therefore, x cannot be given a linear type, and consequently, cannot be deallocated early. Several authors [26, 9, 3] have extended pure linear type systems to allow greater flexibility. However, most of these efforts have focused on high-level user programming languages and as a result, they have emphasized simple typing rules that programmers can understand and/or typing rules that admit effective type inference techniques. These issues are less important for low-level typed languages designed as compiler intermediate languages [22, 18] or as secure mo- bile code platforms, such as the Java Virtual Machine [10], Proof-Carrying Code (PCC) [13] or Typed Assembly Language (TAL) [12]. These languages are de- signed for machine, not human, consumption. On the other hand, because sys- tems such as PCC and TAL make every machine operation explicit and verify that each is safe, the implementation of these systems requires new type-theoretic mechanisms to make efficient use of computer resources. ? (^) This material is based on work supported in part by the AFOSR grant F49620-97- 1-0013 and the National Science Foundation under Grant No. EIA 97-03470. Any opinions, findings, and conclusions or recommendations expressed in this publication are those of the authors and do not reflect the views of these agencies.
In existing high-level typed languages, every location is stamped with a single type for the lifetime of the program. Failing to maintain this invariant has resulted in unsound type systems or misfeatures (witness the interaction between parametric polymorphism and references in ML [23, 27]). In low-level languages that aim to expose the resources of the underlying machine, this in- variant is untenable. For instance, because machines contain a limited number of registers, each register cannot be stamped with a single type. Also, when two stack-allocated objects have disjoint lifetimes, compilers naturally reuse the stack space, even when the two objects have different types. Finally, in a low- level language exposing initialization, even the simplest objects change type. For example, a pair x of type 〈int, int〉 may be created as follows:
malloc x, 2 ; (* x has type 〈junk, junk〉 ) x[1]:=1 ; ( x has type 〈int, junk〉 ) x[2]:=2 ; ( x has type 〈int, int〉 *) .. . At each step in this computation, the storage bound to x takes on a different type ranging from nonsense (indicated by the type junk) to a fully initialized pair of integers. In this simple example, there are no aliases of the pair and therefore we might be able to use linear types to verify that the code is safe. However, in a more complex example, a compiler might generate code to compute the initial values of the tuple fields between allocation and the initializing assignments. During the computation, a register allocator may be forced to move the unini- tialized or partially initialized value x between stack slots and registers, creating aliases:
OBJECT OBJECT
STACK R1 STACK R
Copy To Register
If x is a linear value, one of the pointers shown above would have to be “invalidated” in some way after each move. Unfortunately, assuming the pointer on the stack is invalidated, future register pressure may force x to be physically copied back onto the stack. Although this additional copy is unnecessary because the register allocator can easily remember that a pointer to the data structure remains on the stack, the limitations of a pure linear type system require it.
Pointer aliasing and data sharing also occur naturally in other data structures introduced by a compiler. For example, compilers often use a top-of-stack pointer and a frame pointer, both of which point to the same data structure. Compiling a language like Pascal using displays [1] generalizes this problem to having an arbitrary (but statically known) number of pointers into the same data structure. In each of these examples, a flexible type system will allow aliasing but ensure that no inconsistencies arise. Type systems for low-level languages, therefore, should support values whose types change even when those values are aliased.
second object might occupy location o. In order to track the flow of pointers to these locations accurately, we reflect locations into the type system: A pointer to a location is given the singleton type ptr(`). Each singleton type contains exactly one value (the pointer in question). This property allows the type sys- tem to reason about pointers in a very fine-grained way. In fact, it allows us to represent the graph structure of our example store precisely:
STACK^ R
BOOL
SP
PTR(lo)
lo: INT
PTR(ls) ls: INT PTR(lo)
We represent this picture in our formal syntax by declaring the program variable sp to have type ptr(s) and r 1 to have type ptr(o). The store itself is described by the constraints {s 7 → 〈int, bool, ptr(o)〉} ⊕ {`o 7 → 〈int〉}, where the type 〈τ 1 ,... , τn〉 denotes a memory block containing values with types τ 1 through τn.
Constraints of the form {7 → τ } are a reasonable starting point for an abstraction of the store. However, they are actually too precise to be useful for general-purpose programs. Consider, for example, the simple function deref, which retrieves an integer from a reference cell. There are two immediate prob- lems if we demand that code call deref when the store has a shape described by { 7 → 〈int〉}. First, deref can only be used to derefence the location , and not, for example, the locations′^ or ′′. This problem is easily solved by adding location polymorphism. The exact name of a location is usually unimportant; we need only establish a dependence between pointer type and constraint. Hence we could specify that deref requires a store {ρ 7 → 〈int〉} where ρ is a location variable instead of some specific location. Second, the constraint {7 → 〈int〉} specifies a store with exactly one location although we may want to dereference a single integer reference amongst a sea of other heap-allocated objects. Since deref does not use or modify any of these other references, we should be able to abstract away the size and shape of the rest of the store. We accomplish this task using store polymorphism. An appropriate constraint for the function deref is ⊕ {ρ 7 → 〈int〉} where is a constraint variable that may instantiated with any other constraint.
The third main feature of our constraint language is the capability to distin- guish between linear constraints {ρ 7 → τ } and non-linear constraints {ρ 7 → τ }ω^. Linear constraints come with the additional guarantee that the location on the left-hand side of the constraint (ρ) is not aliased by any other location (ρ′). This invariant is maintained despite the presence of location polymorphism and store polymorphism. Intuitively, because ρ is unaliased, we can safely deallocate its memory or change the types of the values stored there. The key property that makes our system more expressive than traditional linear systems is that although the aliasing constraints may be linear, the pointer values that flow through a computation are not. Hence, there is no direct restriction on the copy- ing and reuse of pointers.
The following example illustrates how the type system uses aliasing con- straints and singleton types to track the evolution of the store across a series of instructions that allocate, initialize, and then deallocate storage. In this exam- ple, the instruction malloc x, ρ, n allocates n words of storage. The new storage is allocated at a fresh location in the heap and is substituted for ρ in the remaining instructions. A pointer to ` is substitued for x. Both ρ and x are considered bound by this instruction. The free instruction deallocates storage. Deallocated storage has type junk and the type system prevents any future use of that space. Instructions Constraints (Initially the constraints )
This section describes our new type-safe “language of locations” formally. The syntax for the language appears in Figure 1.
3.1 Values, Instructions, and Programs
A program is a pair of a store (S ) and a list of instructions (ι). The store maps locations () to values (v). Normally, the values held in the store are memory blocks (〈τ 1 ,... , τn〉), but after the memory at a location has been deallocated, that location will point to the unusable value junk. Other values include integer constants (i), variables (x or f), and, of course, pointers (ptr()). Figure 2 formally defines the operational semantics of the language.^2 The main instructions of interest manipulate memory blocks. The instruction malloc x, ρ, n
(^2) Here and elsewhere, the notation X[c 1 ,... , cn/x 1 ,... , xn] denotes capture-avoiding substitution of c 1 ,... , cn for variables x 1 ,... , xn in X.
that must be satisfied before the function can be invoked. The type context ∆ binds the set of type variables that can occur free in the term; C is a collection of aliasing constraints that statically approximates a portion of the store; and Γ assigns types to free variables in ι. To call a polymorphic function, code must first instantiate the type variables in ∆ using the value form: v[η] or v[C]. These forms are treated as values because type application has no computational effect (types and constraints are only used for compile-time checking; they can be erased before executing a program).
(S, malloc x, ρ, n; ι) 7 −→ (S{7 → 〈junk 1 ,... , junkn〉}, ι[/ρ][ptr()/x]) where 6 ∈ S (S{7 → v}, free ptr(); ι) 7 −→ (S{7 → junk}, ι) if v = 〈v 1 ,... , vn〉 (S{ 7 → v}, ptr()[i]:=v′^ ; ι) 7 −→ (S{ 7 → 〈v 1 ,... , vi− 1 , v′, vi+1,... , vn〉}, ι) if v = 〈v 1 ,... , vn〉 and 1 ≤ i ≤ n (S{7 → v}, x=ptr()[i]; ι) 7 −→ (S{` 7 → v}, ι[vi/x]) if v = 〈v 1 ,... , vn〉 and 1 ≤ i ≤ n (S, v(v 1 ,... , vn)) 7 −→ (S, ι[c 1 ,... , cm/β 1 ,... , βm][v′, v 1 ,... , vn/f, x 1 ,... , xn]) if v = v′[c 1 ,... , cm] and v′^ = fix f [∆; C; x 1 :τ 1 ,... , xn:τn].ι and Dom(∆) = β 1 ,... , βm (where β ranges over ρ and )
Fig. 2. Language of Locations: Operational Semantics
3.2 Type Constructors
There are three kinds of type constructors: locations^3 (η), types (τ ), and aliasing constraints (C). The simplest types are the base types, which we have chosen to be integers (int). A pointer to a location η is given the singleton type ptr(η). The only value in the type ptr(η) is the pointer ptr(η), so if v 1 and v 2 both have type ptr(η), then they must be aliases. Memory blocks have types (〈τ 1 ,... , τn〉) that describe their contents. A collection of constraints, C, establishes the connection between pointers of type ptr(η) and the contents of the memory blocks they point to. The main form of constraint, written {η 7 → τ }, models a store with a single location η containing a value of type τ. Collections of constraints are constructed from more primitive constraints using the join operator (⊕). The empty constraint is denoted by ∅. We often abbreviate {η 7 → τ } ⊕ {η′^7 → τ ′} with {η 7 → τ, η′^7 → τ ′}.
(^3) We use the meta-variable ` to denote concrete locations, ρ to denote location vari- ables, and η to denote either.
3.3 Static Semantics
Store Typing The central invariant maintained by the type system is that the current constraints C are a faithful description of the current store S. We write this store-typing invariant as the judgement S : C. Intuitively, whenever a location contains a value v of type τ , the constraints should specify that location ` maps to τ (or an equivalent type τ ′). Formally:
·; · v v 1 : τ 1 · · · ·; ·v vn : τn { 1 7 → v 1 ,... , n 7 → vn} : { 1 7 → τ 1 ,... , `n 7 → τn}
where for 1 ≤ i ≤ n, the locations `i are all distinct. And,
S : C′^ · C′^ = C ` S : C
Instruction Typing Instructions are type checked in a context ∆; C; Γ. The judgement ∆; C; Γ ι ι states that the instruction sequence is well-formed. A related judgement, ∆; Γv v : τ , ensures that the value v is well-formed and has type τ. 4 Our presentation of the typing rules for instructions focuses on how each rule maintains the store-typing invariant. With this invariant in mind, consider the rule for projection:
∆; Γ v v : ptr(η) ∆ C = C′^ ⊕ {η 7 → 〈τ 1 ,... , τn〉} ∆; C; Γ, x:τi ι ι ∆; C; Γι x=v[i]; ι
x 6 ∈ Γ 1 ≤ i ≤ n
The first pre-condition ensures that v is a pointer. The second uses C to deter- mine the contents of the location pointed to by v. More precisely, it requires that C equal a store description C′^ ⊕ {η 7 → 〈τ 1 ,... , τn〉}. (Constraint equality uses ∆ to denote the free type variables that may appear on the right-hand side.) The store is unchanged by the operation so the final pre-condition requires that the rest of the instructions be well-formed under the same constraints C. Next, examine the rule for the assignment operation:
∆; Γ v v : ptr(η) ∆; Γv v′^ : τ ′ ∆ C = C′^ ⊕ {η 7 → 〈τ 1 ,... , τn〉} ∆; C′^ ⊕ {η 7 → τafter}; Γι ι ∆; C; Γ `ι v[i]:=v′^ ; ι
(1 ≤ i ≤ n)
where τafter is 〈τ 1 ,... , τi− 1 , τ ′, τi+1,... , τn〉 Once again, the value v must be a pointer to some location η. The type of the contents of η are given in C and must be a block with type 〈τ 1 ,... , τn〉. This time the store has changed, and the remaining instructions are checked under the appropriately modified constraint C′^ ⊕ {η 7 → τafter}. (^4) The subscripts on v andι are used to distinguish judgement forms and for no other purpose.
This function deallocates its two arguments, x and y, before calling its continu- ation with the contents of y. It is easy to check that this function type-checks, but should it? If foo is called in a state where ρ 1 and ρ 2 are aliases, a run-time error will result when the second instruction is executed because the location pointed to by y will already have been deallocated. Fortunately, our type system guarantees that foo can never be called from such a state. Suppose that the store currently contains a single integer reference: {7 → 〈 3 〉}. This store can be described by the constraints { 7 → 〈int〉}. If the program- mer attempts to instantiate both ρ 1 and ρ 2 with the same label , the function call foo[, , ∅](ptr()) will fail to type check because the constraints {7 → 〈int〉} do not equal the pre-condition ∅ ⊕ { 7 → 〈int〉, 7 → 〈int〉}. Figure 3 contains the typing rules for values and instructions. Note that the judgement ∆wf τ indicates that ∆ contains the free type variables in τ.
3.4 Soundness
Our typing rules enforce the property that well-typed programs cannot enter stuck states. A state (S, ι) is stuck when no reductions of the operational seman- tics apply and ι 6 = halt. The following theorem captures this idea formally:
Theorem 1 (Soundness) If S : C and ·; C; ·ι ι and (S, ι) 7 −→... 7 −→ (S′, ι′) then (S′^ , ι′) is not a stuck state.
We prove soundness syntactically in the style of Wright and Felleisen [28]. The proof appears in the companion technical report [19].
Most linear type systems contain a class of non-linear values that can be used in a completely unrestricted fashion. Our system is similar in that it admits non-linear constraints, written {η 7 → τ }ω^. They are characterized by the axiom:
∆ ` {η 7 → τ }ω^ = {η 7 → τ }ω^ ⊕ {η 7 → τ }ω
Unlike the constraints of the previous section, non-linear constraints may be duplicated. Therefore, it is not sound to deallocate memory described by non- linear constraints or to use it at different types. Because there are strictly fewer operations on non-linear constraints than linear constraints, there is a natural subtyping relation between the two: {η 7 → τ } ≤ {η 7 → τ }ω^. We extend the subtyping relationship on single constraints to collections of constraints with rules for reflexivity, transitivity, and congruence. For example, assume add has type ∀[ρ 1 , ρ 2 , ; {ρ 1 7 → 〈int〉}ω^ ⊕ {ρ 2 7 → 〈int〉}ω^ ⊕ ].(ptr(ρ 1 ), ptr(ρ 2 ))→ 0 and consider this code:
Instructions Constraints (Initially ∅) malloc x, ρ, 1; C 1 = {ρ 7 → 〈junk〉}, x : ptr(ρ) x[0]:=3; C 2 = {ρ 7 → 〈int〉} add[ρ, ρ, ∅](x, x) C 2 ≤ {ρ 7 → 〈int〉}ω^ = {ρ 7 → 〈int〉}ω^ ⊕ {ρ 7 → 〈int〉}ω^ ⊕ ∅
Typing rules for non-linear constraints are presented in Figure 4.
∆; Γ `v v : τ
∆; Γ v i : int ∆; Γv x : Γ (x) ∆; Γ `v junk : junk
∆ wf η ∆; Γv ptr(η) : ptr(η)
∆; Γ v v 1 : τ 1 · · · ∆; Γv vn : τn ∆; Γ `v 〈v 1 ,... , vn〉 : 〈τ 1 ,... , τn〉
∆ wf ∀[∆′; C].(τ 1 ,... , τn)→ 0 ∆, ∆′; C; Γ, f :∀[∆′; C].(τ 1 ,... , τn )→ 0 , x 1 :τ 1 ,... , xn:τnι ι ∆; Γ `v fix f [∆′; C; x 1 :τ 1 ,... , xn:τn].ι : ∀[∆′; C].(τ 1 ,... , τn )→ 0 (f, x 1 ,... , xn 6 ∈ Γ )
∆ wf η ∆; Γv v : ∀[ρ, ∆′; C].(τ 1 ,... , τn)→ 0 ∆; Γ `v v[η] : ∀[∆′^ ; C].(τ 1 ,... , τn)→ 0 [η/ρ]
∆ wf C ∆; Γv v : ∀[, ∆; C′].(τ 1 ,... , τn)→ 0 ∆; Γ `v v[C] : ∀[∆; C′].(τ 1 ,... , τn)→ 0 [C/]
∆; Γ v v : τ ′^ ∆ τ ′^ = τ ∆; Γ `v v : τ
∆; C; Γ `ι ι
∆, ρ; C ⊕ {ρ 7 → 〈junk 1 ,... , junkn〉}; Γ, x:ptr(ρ) ι ι ∆; C; Γι malloc x, ρ, n; ι (x 6 ∈ Γ, ρ 6 ∈ ∆)
∆; Γ v v : ptr(η) ∆ C = C′^ ⊕ {η 7 → 〈τ 1 ,... , τn〉} ∆; C′^ ⊕ {η 7 → junk}; Γ ι ι ∆; C; Γι free v; ι
∆; Γ v v : ptr(η) ∆ C = C′^ ⊕ {η 7 → 〈τ 1 ,... , τn〉} ∆; Γ v v′^ : τ ′^ ∆; C′^ ⊕ {η 7 → 〈τ 1 ,... , τi− 1 , τ ′, τi+1 ,... , τn〉}; Γι ι ∆; C; Γ `ι v[i]:=v′; ι
(1 ≤ i ≤ n)
∆; Γ v v : ptr(η′^ ) ∆ C = C′^ ⊕ {η′^7 → 〈τ 1 ,... , τn〉} ∆; C; Γ, x:τi ι ι ∆; C; Γι x=v[i]; ι
x 6 ∈ Γ 1 ≤ i ≤ n
∆; Γ v v : ∀[·; C′].(τ 1 ,... , τn)→ 0 ∆ C = C′ ∆; Γ v v 1 : τ 1 · · · ∆; Γv vn : τn ∆; C; Γ ι v(v 1 ,... , vn) ∆; C; Γι halt
Fig. 3. Language of Locations: Value and Instruction Typing
These additional features of our language are also proven sound in the com- panion technical report [19].
Syntax:
types τ ::=... | ?〈τ 1 ,... , τn〉 | null values v ::=... | null instructions ι ::=... | mknull x, ρ; ι | tosum v, ?〈τ 1 ,... , τn〉 | ifnull v then ι 1 else ι 2
Operational semantics:
(S, mknull x, ρ; ι) 7 −→ (S{7 → null}, ι[/ρ][ptr()/x]) where 6 ∈ S (S, tosum v, ?〈τ 1 ,... , τn 〉; ι) 7 −→ (S, ι) (S{7 → null}, ifnull ptr() then ι 1 else ι 2 ) 7 −→ (S{7 → null}, ι 1 ) (S{ 7 → 〈v 1 ,... , vn〉}, ifnull ptr() then ι 1 else ι 2 ) 7 −→ (S{ 7 → 〈v 1 ,... , vn〉}, ι 2 )
Static Semantics:
∆; Γ `v null : null
∆, ρ; C ⊕ {ρ 7 → null}; Γ, x:ptr(ρ) ι ι ∆; C; Γι mknull x, ρ; ι (x 6 ∈ Γ, ρ 6 ∈ ∆)
∆; Γ v v : ptr(η) ∆ C = C′^ ⊕ {η 7 → null}φ ∆ wf ?〈τ 1 ,... , τn〉 ∆; C′^ ⊕ {η 7 → ?〈τ 1 ,... , τn〉}φ; Γι ι ∆; C; Γ `ι tosum v, ?〈τ 1 ,... , τn〉; ι
∆; Γ v v : ptr(η) ∆ C = C′^ ⊕ {η 7 → 〈τ 1 ,... , τn〉}φ^ ∆; C′^ ⊕ {η 7 → ?〈τ 1 ,... , τn〉}φ; Γ ι ι ∆; C; Γι tosum v, ?〈τ 1 ,... , τn〉; ι
∆; Γ v v : ptr(η) ∆ C = C′^ ⊕ {η 7 → ?〈τ 1 ,... , τn〉}φ ∆; C′^ ⊕ {η 7 → null}φ; Γ ι ι 1 ∆; C′^ ⊕ {η 7 → 〈τ 1 ,... , τn〉}φ; Γι ι 2 ∆; C; Γ `ι ifnull v then ι 1 else ι 2
Fig. 5. Language of Locations: Extensions for option types
Our research extends previous work on linear type systems [26] and syntactic control of interference [16] by allowing both aliasing and safe deallocation. Sev- eral authors [26, 3, 9] have explored alternatives to pure linear type systems to
allow greater flexibility. Wadler [26], for example, introduced a new let-form let! (x) y = e 1 in e 2 that permits the variable x to be used as a non-linear value in e 1 (i .e. it can be used many times, albeit in a restricted fashion) and then later used as a linear value in e 2. We believe we can encode similar behavior by extending our simple subtyping with bounded quantification. For instance, if a function f requires some collection of aliasing constraints that are bounded above by {ρ 1 7 → 〈int〉}ω^ ⊕ {ρ 2 7 → 〈int〉}ω^ , then f may be called with a single linear constraint {ρ 7 → 〈int〉} (instantiating both ρ 1 and ρ 2 with ρ and with {ρ 7 → 〈int〉}). The constraints may now be used non-linearly within the body of f. Provided f expects a continuation with constraints , its continuation will retain the knowledge that {ρ 7 → 〈int〉} is linear and will be able to deallocate the storage associated with ρ when it is called. However, we have not yet imple- mented this feature.
Because our type system is constructed from standard type-theoretic building blocks, including linear and singleton types, it is relatively straightforward to implement these ideas in a modern type-directed compiler. In some ways, our new mechanisms simplify previous work. Previous versions of TAL [12, 11] possessed two separate mechanisms for initializing data structures. Uninitialized heap- allocated data structures were stamped with the type at which they would be used. On the other hand, stack slots could be overwritten with values of arbitrary types. Our new system allows us to treat memory more uniformly. In fact, our new language can encode stack types similar to those described by Morrisett et al. [11] except that activation records are allocated on the heap rather than using a conventional call stack. The companion technical report [19] shows how to compile a simple imperative language in such a way that it allocates and deletes its own stack frames.
This research is also related to other work on type systems for low-level languages. Work on Java bytecode verification [20, 8] also develops type systems that allows locations to hold values of different types. However, the Java bytecode type system is not strong enough to represent aliasing as we do here.
The development of our language was inspired by the Calculus of Capa- bilities (CC) [4]. CC provides an alternative to the region-based type system developed by Tofte and Talpin [24]. Because safe region deallocation requires that no aliases be used in the future, CC tracks region aliases. In our new lan- guage we adapt CC’s techniques to track both object aliases and object type information.
Our work also has close connections with research on alias analyses [5, 21, 17]. Much of that work aims to facilitate program optimizations that require aliasing information in order to be correct. However, these optimizations do not necessarily make it harder to check the safety of the resulting program. Other work [7, 6] attempts to determine when programs written in unsafe languages, such as C, perform potentially unsafe operations. Our goals are closer to the latter application but differ because we are most interested in compiling safe languages and producing low-level code that can be proven safe in a single pass over the program. Moreover, our main result is not a new analysis technique,