Flexible Type Analysis^{∗}

Karl Crary

Carnegie Mellon University Stephanie Weirich

Cornell University

Abstract

Run-time type dispatch enables a variety of advanced optimization techniques for polymorphic languages, including tag-free garbage collection, unboxed function arguments, and flattened data structures. However, modern type-preserving compilers transform types between stages of compilation, making type dispatch prohibitively complex at low levels of typed compilation. It is crucial therefore for type analysis at these low levels to refer to the types of previous stages. Unfortunately, no current intermediate language supports this facility.

To fill this gap, we present the language LX, which provides a rich language of type constructors supporting type analysis (possibly of previous-stage types) as a programming idiom. This language is quite flexible, supporting a variety of other applications such as analysis of quantified types, analysis with incomplete type information, and type classes. We also show that LX is compatible with a type-erasure semantics.

1 Introduction

Type-directed compilers use type information to enable optimizations and transformations that are impossible (or prohibitively difficult) without such information [16, 12, 21, 2, 25, 26, etc.]. However, type-directed compilers for some languages such as Modula-3 and ML face the difficulty that some type information cannot be known at compile time. For example, polymorphic code in ML may operate on inputs of type α where α is not only unknown, but may in fact be instantiated by a variety of different types.

In order to use type information in contexts where it cannot be provided statically, a number of advanced implementation techniques process type information at run time [12, 21, 30, 23, 26]. Such type information is used in two ways: behind the scenes, typically by tagfree garbage collectors [30, 1], and explicitly in program code, for a variety of purposes such as efficient data representation and marshalling [21, 12, 27]. In this paper we focus on the latter area of applications.

To lay a solid foundation for programs that analyze types at run time, Harper and Morrisett [12] proposed an internal language, called λi^{ML}, that supports firstclass intensional analysis of types (that is, analysis of the structure of types). The λi^{ML} language and its derivatives were then used extensively in the highperformance ML compilers TIL/ML [29, 20] and FLINT [27]. The primary novelty of these languages is the presence of “typecase” operators at the level of terms and types, that allow computations and type expressions to depend upon the values of other type expressions at run time.

Like most type-directed compilers, TIL/ML and FLINT preserve types through much of compilation, but discard types at a certain point and finish compilation without them. Nevertheless, there are compelling advantages to preserving types through the entirety of the compiler: types may be used to perform optimizations that are only feasible at low levels, the ability to typecheck intermediate code provides an invaluable tool for debugging a compiler, and types may be used to certify the safety of the output executables [24].

Unfortunately, existing type-analyzing languages are not well suited for further typed compilation. This is because existing such languages are hardwired so that the language’s own types are the subject of type analysis. Such a design is quite natural when the language is considered in isolation—what other types are there?— but in the context of a multi-stage, type-directed compiler we face the problem that type-altering transformations (e.g., closure conversion) are applied to intermediate programs that perform type analysis. After such a transformation, we would prefer to preserve the algorithmic structure of our program by continuing to pass and inspect the types that were used before the transformation. Existing type-analyzing languages, which have Λα:Type. λx:α.

∗This material is based on work supported in part by ARPA grant F19628-95-C-C533, AFOSR grant F49620-97-1-0013, and ARPA/RADC grant F30602-1-0317. The second author is also supported by a National Science Foundation Graduate Fellowship.

To appear in the 1999 International Conference on Functional Programming, Paris, France, September 1999.

typecase α of
int                 => . . . (* x has type int *) . . .
β × γ  => . . . (* x has type β × γ *) . . .

∃δ.((δ × β) → γ) × δ => … (* x has type ∃δ.((δ × β) → γ) × δ *) …

_=> . . . (* x has some other type *) . . .

Figure 1: Compilation of type analysis, first try

Λα:MLType. λx:(interp α).

typecase α of
[int]_{ML} => . . . (* x has type int *) . . .

[β × γ]_{ML} => … (* x has type (interp β) × (interp γ) *) … [β → γ]_{ML} => … (* x has type ∃δ.((δ × (interp β)) → (interp γ)) × δ *) …

Figure 2: Second try

only a single notion of type, cannot permit this operation, so the types that are passed and inspected at run time must be altered in the same way as all other types. This alteration disrupts the algorithmic structure of the program to no good purpose, and it also presents some severe practical problems:

• After the compiler transforms types, they usually become larger, often substantially. Passing and analyzing the altered types, instead of the original, leads to unnecessary inefficiency.

• The transformations are usually not surjective. Consequently, typecases that had been exhaustive before transformation can become inexhaustive, leaving the compiler to insert additional clauses to fill out every typecase. At best such clauses are wasteful; at worst they may be impossible to write in a type-safe manner.

• Sometimes the transformations are not even injective, making it impossible to appropriately transform typecase expressions in a meaning-preserving manner.

To solve these problems, we would like a type system that allows two distinct notions of type to coexist: the current types and the types used in some earlier stage of compilation. To clarify what we have in mind, we begin with a simple example. Consider the code fragment:

Λα:Type. λx:α.

typecase α of
int   => . . . (* x has type int *) . . .
β × γ => . . . (* x has type β × γ *) . . .
β → γ => . . . (* x has type β → γ *) . . .

Now suppose the compiler performs closure conversion [19, 24], thereby transforming function types τ_{1} → τ_{2} into ∃δ.((δ × τ_{1}) → τ_{2}) × δ. In existing languages, this code must become the code in Figure 1.

Instead, we would like α to be a “high-level” type, but upon finding it to be β → γ we want to be able to conclude that x has the closure-converted type. Naively, then, we would like the language to supply two different kinds of types, Type (current types) and MLType (types before closure conversion), and a function interp : MLType → Type to translate between them. With these operations, we could transform the code fragment to something like Figure 2.

This naive language solves the hardwiring problem discussed above, but replaces it with another one. In this language the source’s type system is hardwired (as MLType) and the type translation from the source is also hardwired (as interp). Thus, the language is defective as a general-purpose intermediate language; it specifies both the source language and the compilation strategy, and it ought to specify neither.

1.1 Our Solution

In this paper we introduce a new language, called LX, for expressing programs that analyze types. LX provides a very expressive type system in which one can program MLType and interp. In this manner we solve the hardwiring problem without having to specialize to a particular source language or compilation strategy. LX makes this solution possible by providing a rich programming language of type constructors. In this language, the kind MLType is definable using sum, product, and inductive kinds, and the operator interp is definable using primitive recursion.

Although LX was devised to support type analysis, it contains no constructs for analyzing types per se. This fact about LX reveals that intensional type analysis is simply a programming idiom that is possible in a language with sufficiently rich type constructors. The flexibility afforded by this language allows idioms going well beyond what has been previously possible in typeanalyzing languages. In this paper we discuss three such applications:

• We present the first account of how to conduct intensional type analysis in the presence of polymorphic types and other types with binding structure.

• We show how to make “shallow” type analysis possible without passing entire types. This optimization is useful in applications where it is only necessary to determine the top-level structure of types, as in some garbage collectors.

• We illustrate an elegant way to express Haskellstyle type classes [15] or ML equality types.

We also discuss another particularly important application of LX: As discussed in Crary, Weirich, and Morrisett [5] (hereafter, CWM), many aspects of compilation are greatly simplified by adopting a type-erasure semantics, but such a semantics seems problematic in the presence of type analysis. CWM reconciled type analysis with type-erasure semantics using explicit runtime terms to represent erasable type information in their language λ_{R}. In CWM those type representations were required to be primitive but we show that they are definable in LX.

The remainder of this paper is organized as follows: In Section 2 we discuss informally how to analyze types in LX. In Section 3 we formally define LX and state some important properties of it. In Section 4, we formally revisit the examples of Section 2, and also discuss polymorphic types, shallow type analysis and type classes. In Section 5 we show how to reconcile LX with a type-erasure semantics. Concluding discussion appears in Section 6. We assume some familiarity with the notions of type constructors and kinds.

2 Informal Presentation

We begin with a simple example to illustrate informally how type analysis is conducted in LX. Suppose we wish to store arrays of pairs efficiently. In a naive implementation, each pair in the array must be boxed so that array entries are uniformly word-sized. This representation wastes a word for every array entry, or more if the pair components are pairs themselves. We may store such arrays more efficiently by transforming them from arrays of pairs to pairs of arrays. This latter representation costs only a few words for the entire array.^{1}

We would like the compiler to employ this optimization automatically for all arrays of pairs, including polymorphic arrays that happen to be arrays of pairs. This application is precisely the purpose of intensional type analysis; using intensional type analysis, a polymorphic function can analyze its type argument and dispatch to different code depending on that argument. To make what we mean concrete, we will first implement this optimization in the style of a conventional type analysis language, and then translate it into LX.

To implement this optimization, we define a type operator optarray and a corresponding subscript function optsub operating on optimized arrays. The optarray operator recursively splits arrays of pairs into pairs of arrays and uses ordinary arrays at all other types. We assume the built-in function sub has type ∀α.array α → int → α.^{2}

                                                             optarray(int) =^{def} array(int)
                                                           optarray(τ_{1} × τ_{2}) =^{def} (optarray τ_{1}) ×
                                                                                (optarray τ_{2})

optarray(τ_{1} → τ_{2}) =^{def} array(τ_{1} → τ_{2})

                                                          optarray(array τ) =^{def} array(array τ)

                                                      val rec optsub : ∀α. optarray α → int → α =
                                                        Fn [α] =>
                                                          fn a:optarray α => fn n:int =>
                                                            typecase α of
                                                              β × γ => (optsub[β] (#1 a) n,
                                                                        optsub[γ] (#2 a) n)
                                                                    => sub[α] a n

In an LX version of this example, optarray and optsub will no longer operate on types, they will operate on type constructors that encode types. In particular, we inductively define a kind MLType whose members specify the abstract syntax of a type. In this section we use an informal notation borrowed from ML datatypes; we will show how this example is formalized in the next section.

                                                      kind MLType = Int

| Prod of MLType * MLType | Arrow of MLType * MLType

                                                                   | Array of MLType

Members of MLType have no built-in interpretation as types; they are merely data that may be computed with at the level of type constructors. The first thing to do then is to define their meaning by a function mapping MLType to Type:

interp(Int) =^{def} int
interp(Prod(c_{1}, c_{2})) =^{def} interp(c_{1}) × interp(c_{2})
interp(Arrow(c_{1}, c_{2})) =^{def} interp(c_{1}) → interp(c_{2})
interp(Array(c)) =^{def} array(interp(c))

Note that the function interp is primitive recursive. In order to ensure that computation with type constructors always terminates, arbitrary recursive functions are not permitted in LX, only primitive recursive ones.

Now that we have defined type encodings and their interpretations as actual types, we can proceed with the example as before. The new operator OptArray has kind MLType → MLType and is defined primitive recursively.

1An ever better representation would be to use arrays of unboxed, flattened tuples. This also can be done straightforwardly using type analysis [12], but is a more complicated example.

2While most of our examples resemble the syntax of ML, we use prefix notation for constructor application.

(kinds)  k ::= Type | 1 | k_{1} → k_{2} | k_{1} × k_{2} | k_{1} + k_{2} | j | μj.k
(constructors) c, τ ::= ∗ | α | λα:k.c | c_{1}c_{2}  unit, variables and functions
| ⟨c_{1}, c_{2}⟩ | prj_{1} c | prj_{2} c  products
| inj_{1}^{k_{1}+k_{2}} c | inj_{2}^{k_{1}+k_{2}} c | case(c, α_{1}.c_{1}, α_{2}.c_{2})  sums
| fold_{μj.k} c | pr(j, α:k, β:j →k′.c)  primitive recursion
| int | τ_{1} → τ_{2} | τ_{1} × τ_{2} | τ_{1} + τ_{2} | ∀α:k.τ | ∃α:k.τ types
| unit | void | rec_{k}(c_{1}, c_{2})  types

Figure 3: LX Kinds and Constructors

The corresponding subscript function, optsub, now analyzes members of MLType rather than actual types.

OptArray(Int) =^{def} Array(Int)
OptArray(Prod(c_{1}, c_{2})) =^{def} Prod(\begin{matrix}OptArray(c_{1}), \\ OptArray(c_{2})) \end{matrix}
OptArray(Arrow(c_{1}, c_{2})) =^{def} Array(Arrow(c_{1}, c_{2}))
OptArray(Array(c)) =^{def} Array(Array(c))

val rec optsub :

∀α:MLType. interp(OptArray(α))

→ int → interp α =

Fn [α : MLType] =>

fn a: interp(OptArray(α)) => fn n:int =>
case α of
Prod(β, γ) => (optsub[β] (#1 a) n,
optsub[γ] (#2 a) n)
=> sub[interp α] a n

Translating this example into LX has certainly made it more verbose, but it also makes it robust under further compilation. Suppose the compiler performs closure conversion, thereby transforming function types τ_{1} → τ_{2} into ∃δ.((δ × τ_{1}) → τ_{2}) × δ. All that needs happen is a change to the appropriate clause of the interp function,

interp′(Arrow(c_{1}, c_{2}))

=^{def}

∃δ.((δ × interp′(c_{1})) → interp′(c_{2})) × δ

but no changes to OptArray or optsub are required (other than the closure conversion itself, of course).

3 A Language for Flexible Type Analysis

In this section we discuss LX and its semantics. We present the constructor and term levels individually, concentrating discussion on the novel features of each. The syntax of LX (shown in Figures 3 and 4) is based on Girard’s F_{ω} [10, 9] augmented mainly by a rich programming language at the constructor level, and constructor refinement operators at the term level. The full static and operational semantics of LX are given in Appendices A and B.

3.1 Kinds and Constructors

The constructor and kind levels, shown in Figure 3, contain both base constructors of kind Type (called types) for classifying terms, and a variety of programming constructs for computing types. In addition to the variables and lambda abstractions of F_{ω}, LX also includes a unit kind, products, sums, and the usual introduction and elimination constructs for those kinds.

We denote the simultaneous, capture-avoiding substitution of E_{1}, …, E_{n} for X_{1}, …, X_{n} in E by E[E_{1}, …, E_{n}/X_{1}, …, X_{n}]. As usual, we consider alpha-equivalent expressions to be identical. A few constructs (inj_{i}, fold, pr, and rec) are labeled with kinds to assist in kind checking; we will omit such kinds when they are clear from context. When a constructor is intended to have kind Type, we often use the metavariable τ.

To support computing with abstract syntax trees, LX includes kind variables (j) and inductive kinds (μj.k). A prospective inductive kind μj.k will be well-formed provided that j appears only positively within k. Inductive kinds are formed using the introductory operator fold_{μj.k}, which coerces constructors from kind k[μj.k/j] to kind μj.k. For example, consider the kind of natural numbers N, defined as μj.(1+j). The constructor (inj_{1}^{1+N} ∗) has kind (1 + j)[N/j]. Therefore fold_{N}(inj_{1}^{1+N} ∗) has kind N.

Inductive kinds are eliminated using the primitive recursion operator pr. Intuitively, pr(j, α:k, φ:j → k′.c) may be thought of as a recursive function with domain μj.k in which α stands for the argument unfolded and φ recursively stands for the full function. However, in order to ensure that constructor expressions always terminate, we restrict pr to define only primitive recursive functions. Informally speaking, a function is primitive recursive if it can only call itself recursively on a subcomponent of its argument. Following Mendler [17], we ensure this using abstract kind variables. Since α stands for the argument unfolded, we could consider it to have the kind k[μj.k/j], but instead of substituting for j in k, we hold j abstract. Then the recursive variable φ is given kind j → k′ (instead of j[μj.k/j] → k′) thereby ensuring that φ is called only on a subcomponent of α.

The kind k′ in pr(j, α:k, φ:j →k′.c) is permitted to con-

(terms) e ::= i | ∗ | x | λx:τ.e | e_{1}e_{2}  ints, unit, variables, abstractions
| ⟨e_{1}, e_{2}⟩ | prj_{1} e | prj_{2} e  products
| inj_{1}^{τ_{1}+τ_{2}} e | inj_{2}^{τ_{1}+τ_{2}} e | case(e, x_{1}.e_{1}, x_{2}.e_{2})  sums
| Λα:k.v | e[c] | fix f:τ.e  constructor abstractions and recursion
| pack e as ∃α:k.τ hiding c | unpack ⟨α, x⟩ = e_{1} in e_{2} existential packages
| fold_{rec_{k}(c,c′)} e | unfold e  parameterized recursive types
| let_{τ} ⟨β, γ⟩ = c in e | let_{τ} (fold β) = c in e  constructor refinement operations
| ccase_{τ} (c, α_{1}.e_{1}, α_{2}.e_{2})

Figure 4: Terms in LX

tain (positive) free occurrences of j. In that case, the function’s result kind employs the substitution for j that was internally eschewed. Hence, the result kind of the above constructor is k′[μj.k/j]. This is useful so that some part of the argument may be passed through without φ operating on it. As a particularly useful application, we can define the constructor unfold_{μj.k} with kind μj.k → k[μj.k/j] to be pr(j, α:k, φ:j →k.α).

Given a constructor n with kind N, we can use primitive recursion to construct the type of (n + 1)-tuples of integers (using an informal, expanded notation for

case):

ntuple =^{def} pr(j, α:1+j, φ:j →Type.

case α of
inj_{1} β ⇒ int

inj_{2} γ ⇒ φ(γ) × int)

Suppose we apply ntuple to \overline{1} (that is, the encoding of the natural number 1, fold(inj_{2}(fold(inj_{1} ∗)))). By unrolling the pr expression, we may show :

(pr(j, α:1+j, φ:j →Type.

case α of
inj_{1} β ⇒ int

inj_{2} γ ⇒ φ(γ) × int)) \overline{1}

= case (inj_{2}(fold(inj_{1} ∗))) of
inj_{1} β ⇒ int

inj_{2} γ ⇒ ntuple(γ) × int

= (ntuple(fold(inj_{1} ∗))) × int

= (case (inj_{1} ∗) of
inj_{1} β ⇒ int

inj_{2} γ ⇒ ntuple(γ) × int) × int

= int × int

The unrolling process is formalized by the following constructor equivalence rule (the relevant judgment forms are summarized in Figure 5):

Δ ⊢ c′ : k[μj.k/j]  Δ, j ⊢ k′ kind
Δ, j, α:k, φ:j →k′ ⊢ c : k′ Δ ⊢ μj.k kind

Δ ⊢ pr(j, α:k, φ:j → k′.c)(fold_{μj.k} c′) = \begin{matrix}c[μj.k, c′, pr(j, α:k, φ:j →k′.c)/j, α, φ] \\ : k′[μj.k/j] \end{matrix}

(j only positive in k′ and j, α, φ ∉ Δ)

Notation 3.1 If k_{1} is of the form μj.k, then we write k_{1}[k_{2}] to mean k[k_{2}/j].

3.2 Terms

The syntax of LX terms is given in Figure 4. Most LX terms are standard, including the usual introduction and elimination forms for functions, products, sums, unit, and universal and existential types. Constructor abstractions are limited by a value restriction, in anticipation of the type erasure interpretation in Section 5. The value forms of LX are given in Appendix B. Recursive functions are expressible using fix terms, the bodies of which are syntactically restricted to be functions (possibly polymorphic) by their typing rule (Appendix A). As at the constructor level, some constructs are labeled with types to assist in type checking; we omit these when clear from context.

Parameterized recursive types are written rec_{k}(c_{1}, c_{2}), where k is the parameter kind and c_{1} is a type constructor with kind (k → Type) → (k → Type). Intuitively, c_{1} recursively defines a type constructor with kind k → Type, which is then instantiated with the parameter c_{2} (having kind k). Thus, members of rec_{k}(c_{1}, c_{2}) unfold into the type c_{1}(λα:k. rec_{k}(c_{1}, α))c_{2}, and fold the opposite way. The special case of non-parameterized recursive types are defined as rec(α.τ) = rec_{1}(λφ:1 → Type. λβ:1. τ[φ(∗)/α], ∗). Unlike inductive kinds, no positivity condition is imposed on recursive types.

Refinement The novel features of the LX term language are the three refinement operations. To perform constructor analysis at run time, we require a mechanism for branching on sum kinds at the term level. This branching is done using the ccase construct. If c normalizes to inj_{1}(c′), then the term ccase(c, α_{1}.e_{1}, α_{2}.e_{2}) evaluates to e_{1}[c′/α_{1}], and similarly if it normalizes to inj_{2}(c′).

However, we require more than a term that evaluates in the desired manner. After branching, we have learned something about the constructor in question, and this information may result in additional knowledge about the types of our data. We wish the type system to be able to exploit that knowledge. Consequently, the typing rule for ccase, when the constructor in question is some variable α, substitutes for α to propagate the new information:

Δ, β:k_{1}, Δ′; Γ[inj_{1} β/α] ⊢

e_{1}[inj_{1} β/α] : τ[inj_{1} β/α]

Δ, β:k_{2}, Δ′; Γ[inj_{2} β/α] ⊢

e_{2}[inj_{2} β/α] : τ[inj_{2} β/α]

Δ, α:k_{1} + k_{2}, Δ′ ⊢ c = α : k_{1} + k_{2}

Δ, α:k_{1} + k_{2}, Δ′; Γ ⊢ ccase_{τ} (c, β.e_{1}, β.e_{2}) : τ (β ∉ Δ)

Within the branches, types that depend upon α can be reduced using the new information. For example, if x has type case(α, β.int, β.bool), its type can be reduced in either branch, allowing its use as an integer in one branch and as a boolean in the other.

In order for LX to enjoy the subject reduction property, we also require two trivialization rules [6] for ccase, for use when the argument to ccase is a sum introduction:

Δ ⊢ c = inj_{1} c′ : k_{1} + k_{2} Δ; Γ ⊢ e_{1}[c′/α] : τ Δ; Γ ⊢ ccase_{τ} (c, α.e_{1}, α.e_{2}) : τ

Δ ⊢ c = inj_{2} c′ : k_{1} + k_{2} Δ; Γ ⊢ e_{2}[c′/α] : τ Δ; Γ ⊢ ccase_{τ} (c, α.e_{1}, α.e_{2}) : τ

Path refinement There may also be useful refinement to perform when the constructor to be branched on is not a variable. For example, suppose α has kind (1+1)×Type and x has type case(prj_{1} α, β.int, β.bool). When branching on prj_{1} α, we should again be able to consider x an integer or boolean, but the ordinary ccase rule above no longer applies since prj_{1} α is not a variable. This is solved using the product refinement operation, let_{τ} ⟨β, γ⟩ = α in e. Like ccase, the product refinement operation substitutes everywhere for α:

\begin{matrix}Δ, β:k_{1}, γ:k_{2}, Δ′; Γ[⟨β, γ⟩/α] ⊢ e[⟨β, γ⟩/α] : τ[⟨β, γ⟩/α] \\ Δ, α:k_{1} × k_{2}, Δ′ ⊢ c = α : k_{1} × k_{2} \end{matrix}

Δ, α:k_{1} × k_{2}, Δ′; Γ ⊢ let_{τ} ⟨β, γ⟩ = c in e : τ (β, γ ∉ Δ)

A similar refinement operation exists for inductive types, and each operation also has a trivialization and a non-refining rule similar to those of ccase.

We may use these refinement operations to turn paths into variables and thereby take advantage of ccase. For example, suppose α has kind N × N and we wish to branch on unfold (prj_{1} α). We do it using product and inductive kind refinement in turn:

let ⟨β_{1}, β_{2}⟩ = α in

let (fold γ) = β_{1} in

ccase(γ, δ.e_{1}, δ.e_{2})

Non-path refinement Since there is no refinement operation for functions, sometimes a constructor cannot be reduced to a path. Nevertheless, it is still possible to gain some of the benefits of refinement, using a device due to Harper and Morrisett [12]. Suppose φ has kind N → (1+1), x has type case(φ(\overline{1}), β.int, β.bool), and we wish to branch on φ(\overline{1}) to learn the type of x. First we use a constructor abstraction to assign a variable α to φ(\overline{1}), thereby enabling ccase, and then we use an ordinary abstraction to rebind x with type

case(α, β.int, β.bool):

(Λα:1+1. λx: case(α, β.int, β.bool).
ccase_{τ} (α, β.e_{1}, β.e_{2})) [φ(\overline{1})] x

Within e_{1}, x will be an integer, and similarly within e_{2}. This device has all the expressive power of refinement, but is less efficient because of the need for extra betaexpansions. However, this is the best that can be done with unknown functions.

3.3 Properties of LX

\underline{Judgment}  \underline{Meaning}
Δ ⊢ k kind  k is a well-formed kind
Δ ⊢ c : k  c is a valid constructor of kind k
Δ ⊢ c_{1} = c_{2} : k c_{1} and c_{2} are equal constructors
Δ; Γ ⊢ e : τ  e is a term of type τ

\underline{Contexts}

\begin{aligned}Δ & ::= & ϵ | Δ, j | Δ, α:k \\ Γ & ::= & ϵ | Γ, x:τ \end{aligned}

Figure 5: Judgments of LX

The judgments of the static semantics of LX appear in Figure 5. The important properties to show are decidable type checking and type safety. Due to space considerations, we do not present proofs of these properties here; details appear in the companion technical report [4]. For typechecking, the challenging part is deciding equality of type constructors. We do this using a normalize and compare method employing a reduction relation extracted from the equality rules in the obvious manner.

Lemma 3.2 Reduction of well-formed constructors is strongly normalizing, confluent, preserves kinds, and is respected by equality.

Strong normalization is proven using Mendler’s variation on Girard’s method [17]. Given Lemma 3.2 it is easy to show the normalize and compare algorithm to be terminating, sound and complete, and decidability of type checking follows in a straightforward manner.

Theorem 3.3 (Decidability) It is decidable whether or not Δ; Γ ⊢ e : τ is derivable in LX.

We say that a term is stuck if it is not a value and if no rule of the operational semantics applies to it. Type safety requires that no well-typed term can become stuck:

Theorem 3.4 (Type Safety) If ∅ ⊢ e : τ and e ↦^{∗} e′ then e′ is not stuck.

This is shown using the usual subject reduction and progress lemmas.

4 Programming Type Analysis

In this section, we discuss how to implement type analysis in general and as a specific example we formalize the example from Section 2. We then show how to extend this formulation through simple modifications to implement applications of type analysis that were previously inexpressible.

The basic idea of the type analysis programming idiom is to use elements of the constructor language to represent types, and to define an interpretation function such that at any point the type it represents may be extracted. Instead of destructing types through an additional language construct, as in Harper and Morrisett [12] or CWM, the representations are examined with the built-in features of LX.

Recall the kind MLType and its interpretation function from Section 2:

kind MLType = Int

| Prod of MLType * MLType | Arrow of MLType * MLType

| Array of MLType

interp(Int) =^{def} int
interp(Prod(c_{1}, c_{2})) =^{def} interp(c_{1}) × interp(c_{2})
interp(Arrow(c_{1}, c_{2})) =^{def} interp(c_{1}) → interp(c_{2})
interp(Array(c)) =^{def} array(interp(c))

If we add an array type constructor to LX for this example, we can formalize these definitions in LX by melding the datatype definition of MLType into a recursive sum of products:

in the following manner:

MLType =^{def} μj.(1 + (j × j) + (j × j) + j)
interp =^{def}

pr(j, α:MLType[j], φ:j →Type.

case α of
inj_{1} β ⇒ int
inj_{2} β ⇒
(case β of

inj_{1} β ⇒ φ(prj_{1} β) × φ(prj_{2} β)

inj_{2} β ⇒
(case β of

inj_{1} β ⇒ φ(prj_{1} β) → φ(prj_{2} β) inj_{2} β ⇒ array(φ(β)))))

Now recall the function optsub from Section 2. To formalize optsub in LX, we need ccase and inductive kind refinement:

 optsub =^{def}

fixoptsub : (∀α:MLType. interp(OptArray(α)) → int → interp α).

Λα:MLType. λa: interp(OptArray(α)). λn:int.

let_{(interp α)} (fold α′) = α in ccase_{(interp α)} α′ of

inj_{1} β ⇒ sub[interp α] a n

         inj_{2} β ⇒

(ccase_{(interp α)} β of

             inj_{1} γ ⇒ ⟨\begin{matrix}optsub[prj_{1} γ] (prj_{1} a) n, \\ optsub[prj_{2} γ] (prj_{2} a) n⟩ \end{matrix}

inj_{2} γ ⇒ …)

Let us verify that optsub is well-typed using the typing rules from the previous section. The interesting branch is the one dealing with products (beginning with “inj_{1} γ ⇒ … ”). The let operation creates a new variable α′ with kind MLType[MLType] and substitutes fold(α′) everywhere that α appears. In the product branch, after two uses of ccase, γ has kind MLType × MLType and inj_{2}(inj_{1}(γ)) is substituted for α′.

The required result type is interp α, which (after substitution) has become interp(fold(inj_{2}(inj_{1}(γ)))), which in turn is equal to interp(prj_{1} γ) × interp(prj_{2} γ). The type of a is interp(OptArray(α)), which has become interp(OptArray(fold(inj_{2}(inj_{1}(γ))))), which in turn is equal to interp(OptArray(prj_{1} γ)) × interp(OptArray(prj_{2} γ)). Thus prj_{1} a and prj_{2} a have the appropriate type and the branch typechecks.

Clearly the official LX syntax is quite verbose, so we will use the datatype-style notation in what follows.

4.1 Types with Binding Structure

Previous accounts of intentional type analysis have been unable to deal with types with binding structure, such as universal, existential or recursive types. In LX it is easy to deal with binding structure, simply by appropriate programming.

For example, we can encode the polymorphic lambda calculus using de Bruijn indices as follows:

  kind FType = Var of N

| Arrow of FType * FType

             | Forall of FType

To interpret an FType we also need to provide an environment ρ that maps type variables (natural numbers) to types, thus interp will have kind FType → (N → Type) → Type. In the variable case, we just look it up in the environment, and in the ∀ branch, we interpret the body with an appropriately extended environment.

interp(Var(c)) =^{def} λρ:N→Type. ρ(c)
interp(Arrow(c_{1}, c_{2})) =^{def} λρ:N→Type.

interp(c_{1})(ρ) → interp(c_{2})(ρ)

interp(Forall(c))

=^{def}

λρ:N→Type. ∀α:Type. interp(c) (λβ:N.

caseunfold β of

inj_{1} γ ⇒ α inj_{2} γ ⇒ ρ(γ))

Type analysis of this language at the term level can be defined in a similar manner to the previous example.

It is important to note that this technique is limited to parametrically polymorphic functions, and cannot account for functions that perform intensional type analysis. It seems possible that through more complicated LX programming one might account for some functions that analyze types, but recent results [7] suggest that complete bootstrapping is probably impossible.

4.2 Shallow Representations

Some applications of type analysis are “shallow,” and rely on the outermost structure of the type only, and not on its subcomponents. For example, a tag-free garbage collector needs to know if a given location is a pointer to code, but may not need the types of the arguments to that code [29, 3].

However, even though at run time only part of the type information might be used, the interpretation function interp must be able to reconstruct the entire type. We can implement this by including the type itself in the representation. The following definition, SType, describes representations that do not support analysis of function domains or codomains:

kind SType = Int

| Prod of SType * SType

| Arrow of Type * Type

Because the types appear literally in the constructors, the interpretation function does not need to recur in the third branch.

interp(Int) =^{def} int
interp(Prod(c_{1}, c_{2})) =^{def} interp(c_{1}) × interp(c_{2})
interp(Arrow(τ_{1}, τ_{2})) =^{def} τ_{1} → τ_{2}

In the formulation of the type erasable version of LX in Section 5, we will see that the unused portion of the type can indeed be erased and so will not be passed at runtime.

4.3 Type Classes

Some applications of type analysis may wish to limit analysis only to a subset of the types of the language. A canonical example of this sort of application is polymorphic equality in ML, an operation that is defined on only those data objects that admit equality, such as integers, booleans, and lists, but not functions. Also, the language Haskell [15] provides a general mechanism for defining classes of types with associated operations on them.

Previous type analyzing languages have implemented non-total dynamic type dispatch through the use of a “characteristic function” over the domain. This function is defined to be the identity at types that are allowed, and void elsewhere. For example, Harper and Morrisett define the class of types that admit equality using Typerec as (assuming the addition of the type

bool):

Eq(int) =^{def} int

           Eq(bool) =^{def} bool

Eq(c_{1} × c_{2}) =^{def} Eq(c_{1}) × Eq(c_{2})
Eq(c_{1} → c_{2}) =^{def} void

With this predicate, they define a polymorphic equality function eq with type ∀α:Type. Eq α → Eq α → bool recursively dispatching to primitive equality functions and providing a trivial function with type void → void at illegal types. However, this encoding is not entirely satisfactory because eq[c_{1} → c_{2}] can be a well-typed expression. The function resulting from evaluation of this expression can only be applied to values of type void, so this function cannot be used, but we would prefer the type error to be generated at the point of instantiation, not application.

LX, on the other hand, can define the kind EqType as

  kind EqType = Int
              | Bool

| Prod of EqType * EqType

representing integers, booleans, and products of EqTypes, but not including function types. If eq has type ∀α: EqType . (interp α) → (interp α) → bool, where interp is defined similarly to before, it is simply impossible to instantiate it illegally at a function type.

5 Type Erasure

The most important contribution of CWM is its reconciliation of type analysis with type-erasure semantics, through the use of primitive terms that express the representations of types at run time. This mechanism allows a semantics where types and type constructors may be erased, as their representations remain to be examined. Accounting for type erasure is an important step in extending type analysis to low-level languages.

What prevents type erasure in LX as presented thus far is the ccase construct: evaluation of ccase depends on its argument constructor. However, sometimes it is possible to know at compile-time which branch the ccase will take from the types of the branches. For example, if a branch produces a value of type void, we can infer that it is never taken as there are no values of that type.

We can form a type-erasable version of LX by requiring this always to happen. In particular, we replace the ccase construct with vcase (virtual case), in which one branch is required to be dead code (and is so marked), but which is otherwise identical. Since the dead branch is marked syntactically, the operational semantics need not examine the constructor argument, in a sense that is made precise in Appendix C. The formation rule for vcase with a dead left branch is (the right case is similar):

Δ, β:k_{1}, Δ′; Γ[inj_{1} β/α] ⊢
v[inj_{1} β/α] : void
Δ, β:k_{2}, Δ′; Γ[inj_{2} β/α] ⊢
e[inj_{2} β/α] : τ[inj_{2} β/α]
Δ, α:k_{1} + k_{2}, Δ′ ⊢ c = α : k_{1} + k_{2}

Δ, α:k_{1} + k_{2}, Δ′; Γ ⊢ vcase_{τ} (c, β. dead v, β.e) : τ (β ∉ Δ)

We list the complete rules for vcase in Appendix A.5.

This restriction would seem to reduce the expressive power of the language, but as in CWM, we can use representation terms to capture the structure of the constructors being erased. However, unlike CWM, in LX these representation terms are programmable without adding any new mechanisms. For example, a unit constructor is represented by the unit term, and a pair of constructors is represented by a pair of terms, and so forth.

This idea is formalized in Figures 6 and 7. If c is a constructor with kind k, then ⌜c⌝ is its representation and that representation has type R(c : k). Note that types have trivial representations so they cannot be analyzed, but this is no loss since types are not directly analyzable in full LX either.

The following proposition makes precise the notion that a constructor’s representation does represent it, by stating that in an appropriate context, the translation of a constructor has the correct type:

Proposition 5.1 Define R_{con} and R_{val} as:

R_{con}(ϵ) =^{def} ϵ
R_{val}(ϵ) =^{def} ϵ
R_{con}(Δ, j) =^{def} R_{con}(Δ), j, φ_{j}:j →Type
R_{val}(Δ, j) =^{def} R_{val}(Δ)
R_{con}(Δ, α:k) =^{def} R_{con}(Δ), α:k
R_{val}(Δ, α:k) =^{def} R_{val}(Δ), x_{α}:R(α : k)

If Δ ⊢ c : k then R_{con}(Δ); R_{val}(Δ) ⊢ ⌜c⌝ : R(c : k).

R(c : 1) =^{def} unit
R(c : k_{1} → k_{2}) =^{def} ∀α:k_{1}.R(α : k_{1}) → R(cα : k_{2})

(where α is fresh)

R(c : k_{1} × k_{2}) =^{def} R(prj_{1} c : k_{1}) × R(prj_{2} c : k_{2})

R(c : k_{1} + k_{2})

=^{def}

case(c, α.R(α : k_{1}), α.void)+ case(c, α.void, α.R(α : k_{2}))

R(c : j) =^{def} φ_{j}c

R(c : μj.k) =^{def} rec_{μj.k}(λφ_{j}:μj.k → Type.

λα:μj.k.R(unfold α : k), c) (where α is fresh)

R(c : Type) =^{def} unit

Figure 6: Representation types

It remains to show that representation terms are sufficient for simulating ccase using vcase. Suppose c has kind k_{1} + k_{2}. Then ⌜c⌝ has type R(c : k_{1} + k_{2}). Branching on ⌜c⌝ provides a value with type case(c, β.R(β:k_{1}), β.void) or with the converse type. A value with the given type determines that c must be a left injection, because the other choice provides an impossible value of type void. A value with the converse type similarly determines c to be a right injection. Either way, we can propagate this information into the type system using vcase. To make this intuition precise, observe that any well-typed term of the form ccase_{τ} (c, α.e_{1}, α.e_{2}) can be replaced by the term

                                                            case ⌜c⌝ of

inj_{1} x ⇒ vcase_{τ} (c, α.e_{1}, α. dead x)
inj_{2} x ⇒ vcase_{τ} (c, α. dead x, α.e_{2})

provided that representations for every free variable of c are in scope, as required by Proposition 5.1.

This strategy can be used to encode the entire λ_{R} language of CWM into the erasable version of LX, demonstrating that LX has the full expressive power of previous type-analyzing languages. Space considerations prevent us from including the complete details of the encoding here; those details appear in the companion technical report [4].

6 Related Work and Conclusions

The properties and applications of languages with inductive types similar to the constructor level of LX have been well-studied by Mendler [18, 17], Werner [31], Howard [13, 14], and Gordon [11], among others. Most of those studies include coinductive and polymorphic types as well as inductive types. It appears as though extending LX with coinductive and polymorphic kinds would not be problematic. We have omitted such extensions at present in order to simplify the language and because it is not immediately clear how useful such extensions would be.

Duggan [8] proposes another typed framework for intensional type analysis that is similar in some ways to LX.

⌜∗⌝ =^{def} ∗
⌜α⌝ =^{def} x_{α}
⌜λα:k.c⌝ =^{def} Λα:k. λx_{α}:R(α : k). ⌜c⌝
⌜c_{1}c_{2}⌝ =^{def} ⌜c_{1}⌝[c_{2}]⌜c_{2}⌝
⌜⟨c_{1}, c_{2}⟩⌝ =^{def} ⟨⌜c_{1}⌝, ⌜c_{2}⌝⟩
⌜prj_{i} c⌝ =^{def} prj_{i}⌜c⌝
⌜inj_{i}^{k_{1}+k_{2}} c⌝ =^{def} inj_{i}^{R(inj_{i} c:k_{1}+k_{2})}⌜c⌝

⌜case(c, α.c_{1}, α.c_{2})⌝ =^{def} (Λβ:k_{1} + k_{2}. λx:R(β:k_{1} + k_{2}).

                              case x of

inj_{1} x_{α} ⇒ vcase_{R(case(β,α.c_{1},α.c_{2}):k)}(β, α.⌜c_{1}⌝, α. dead x_{α})
inj_{2} x_{α} ⇒ vcase_{R(case(β,α.c_{1},α.c_{2}):k)}(β, α. dead x_{α}, α.⌜c_{2}⌝))[c]⌜c⌝
(where β is fresh, k_{1} + k_{2} is the kind of c, and k is the kind of case(c, α.c_{1}, α.c_{2}))
⌜fold_{μj.k} c⌝ =^{def} fold_{R(c:μj.k)}⌜c⌝

⌜pr(j, α:k, φ:j → k′.c)⌝ =^{def} fix x_{φ} : R(pr(j, α:k, φ:j → k′.c) : μj.k → k′[μj.k/j]).

Λβ:μj.k. λx:R(β : μj.k).

(λx_{α}:R(unfold β : k[μj.k/j]).
⌜c⌝[μj.k, (λγ:μj.k.R(γ:μj.k)), unfold β, pr(j, α:k, φ:j → k′.c)/j, φ_{j}, α, φ])

                               (unfold x)

(where β is fresh)

⌜int⌝, ⌜τ_{1} → τ_{2}⌝, … =^{def} ∗

Figure 7: Representation terms

Duggan’s system passes types implicitly and primitively allows for the intensional analysis of types at the term level, but does not support intensional type analysis at the constructor level. It does add a facility for defining type classes (using union and recursive kinds) and allows type analysis to be restricted to members of such classes.

Morrisett et al. [24] developed typing mechanisms for low-level intermediate and target languages that allow type information to be preserved all the way to the end of compilation. It would be desirable, in a system based on those mechanisms, to exploit that type information using intensional type analysis. While CWM extended type analysis to the type-erasure semantics necessary for low-level typing mechanisms, remaining issues have prevented the use of the mechanisms of Morrisett et al. in type-analyzing compilers such as TIL/ML [20, 29] and FLINT [27, 28], and have made it as yet infeasible to use intensional type analysis in an end-to-end typed compiler.

The ambition of our work is to lay the foundation for an end-to-end typed compiler that supports intensional type analysis. LX provides a type-theoretic framework that supports the passing and analysis of type information at run time, but without native type analysis constructs. Because type analysis must be programmed within LX, much flexibility in the type system analyzed is afforded, resolving many of the issues hindering type analysis in later stages of typed compilation.

In pursuance of the aim of a type-analyzing end-to-end compiler, an important direction for future work is to extend the mechanisms of LX into lower-level typed assembly languages, and create a type-analyzing Typed Assembly Language. To evaluate this system in the framework of compilation, we plan to extend the Popcorn compiler and its target language TALx86 [22] to support type analysis.

References

[1] Shail Aditya, Christine Flood, and James Hicks. Garbage collection for strongly-typed languages using run-time type reconstruction. In 1994 ACM Conference on Lisp and Functional Programming, pages 12–23, Orlando, June 1994.

[2] Lars Birkedal, Mads Tofte, and Magnus Vejlstrup. From region inference to von Neumann machines via region representation inference. In Twenty-Third ACM Symposium on Principles of Programming Languages, pages 171–183, St. Petersburg, Florida, January 1996.

[3] Perry Cheng, Robert Harper, and Peter Lee. Generational stack collection and profile-driven pretenuring. In 1998 SIGPLAN Conference on Programming Language Design and Implementation, pages 162–173, 1998.

[4] Karl Crary and Stephanie Weirich. Flexible type analysis (extended version). Technical report, Cornell University, 1999.

[5] Karl Crary, Stephanie Weirich, and Greg Morrisett. Intensional polymorphism in type-erasure semantics. In 1998 ACM International Conference on Functional Programming, pages 301–312, Baltimore, September 1998. Extended version published as Cornell University technical report TR98-1721.

[6] Karl Crary, Stephanie Weirich, and Greg Morrisett. Intensional polymorphism in type-erasure semantics. Technical Report TR98-1721, Cornell University, 1998.

[7] Joëlle Despeyroux, Frank Pfenning, and Carsten Schürmann. Primitive recursion for higher-order abstract syntax. In Third International Conference on Typed Lambda Calculi and Applications, volume 1210 of Lecture Notes in Computer Science, pages 147–163, Nancy, France, April 1997. Springer-Verlag. Extended version published as CMU technical report CMU-CS-96-172.

[8] Dominic Duggan. A type-based semantics for userdefined marshalling in polymorphic languages. In Second Workshop on Types in Compilation, March 1998.

[9] Jean-Yves Girard. Une extension de l’interprétation de Gödel à l’analyse, et son application à l’élimination de coupures dans l’analyse et la théorie des types. In J. E. Fenstad, editor, Proceedings of the Second Scandinavian Logic Symposium, pages 63–92. North-Holland Publishing Co., 1971.

[10] Jean-Yves Girard. Interprétation fonctionelle et élimination des coupures de l’arithmétique d’ordre supérieur. PhD thesis, Université Paris VII, 1972.

[11] Andrew D. Gordon. Functional Programming and Input/Output. Distinguished Dissertations in Computer Science. Cambridge University Press, 1994.

[12] Robert Harper and Greg Morrisett. Compiling polymorphism using intensional type analysis. In Twenty-Second ACM Symposium on Principles of Programming Languages, pages 130–141, San Francisco, January 1995.

[13] Brian T. Howard. Fixed Points and Extensionality in Typed Functional Programming Languages. PhD thesis, Stanford University, 1992. Published as Stanford Computer Science Department Technical Report STAN-CS-92-1455.

[14] Brian T. Howard. Inductive, coinductive, and pointed types. In Proceedings of the 1996 ACM SIGPLAN International Conference on Functional Programming, pages 102–109, 1996.

[15] Paul Hudak, Simon L. Peyton Jones, and Philip Wadler. Report on the programming language Haskell, version 1.2. SIGPLAN Notices, 27(5), May 1992.

[16] Xavier Leroy. Unboxed objects and polymorphic typing. In Nineteenth ACM Symposium on Principles of Programming Languages, pages 177–188, 1992.

[17] Nax Paul Mendler. Inductive types and type constraints in the second-order lambda calculus. Annals of Pure and Applied Logic, 51(1–2):159–172, 1991. Earlier version in LICS ’88. (pp. 15,19,134,135).

[18] Paul Francis Mendler. Inductive Definition in Type Theory. PhD thesis, Department of Computer Science, Cornell University, Ithaca, New York, September 1987.

[19] Yasuhiko Minamide, Greg Morrisett, and Robert Harper. Typed closure conversion. In Twenty-Third ACM Symposium on Principles of Programming Languages, pages 271–283, St. Petersburg, Florida, January 1996.

[20] G. Morrisett, D. Tarditi, P. Cheng, C. Stone, R. Harper, and P. Lee. The TIL/ML compiler: Performance and safety through types. In Workshop on Compiler Support for Systems Software, Tucson, February 1996.

[21] Greg Morrisett. Compiling with Types. PhD thesis, Carnegie Mellon University, School of Computer Science, Pittsburgh, Pennsylvania, December 1995.

[22] Greg Morrisett, Karl Crary, Neal Glew, Dan Grossman, Richard Samuels, Fred Smith, Dave Walker, Stephanie Weirich, and Steve Zdancewic. TALx86: A realistic typed assembly language. Technical report, Cornell University, 1999. Submitted to the 1999 ACM SIGPLAN Workshop on Compiler Support for System Software.

[23] Greg Morrisett and Robert Harper. Semantics of memory management for polymorphic languages. In A. D. Gordon and A. M. Pitts, editors, Higher Order Operational Techniques in Semantics. Cambridge University Press, 1997.

[24] Greg Morrisett, David Walker, Karl Crary, and Neal Glew. From System F to typed assembly language. ACM Transactions on Programming Languages and Systems, 1999. To appear. An earlier version appeared in the 1998 Symposium on Principles of Programming Languages.

[25] Erik Ruf. Partitioning dataflow analyses using types. In Twenty-Fourth ACM Symposium on Principles of Programming Languages, pages 15–26, Paris, January 1997.

[26] Zhong Shao. Flexible representation analysis. In 1997 ACM International Conference on Functional Programming, pages 85–98, Amsterdam, June 1997.

[27] Zhong Shao. An overview of the FLINT/ML compiler. In 1997 Workshop on Types in Compilation, Amsterdam, June 1997. Published as Boston College Computer Science Department Technical Report BCCS-97-03.

[28] Zhong Shao, Christopher League, and Stefan Monnier. Implementing typed intermediate languages. In 1998 ACM International Conference on Functional Programming, pages 313–323, Baltimore, Maryland, September 1998.

[29] D. Tarditi, G. Morrisett, P. Cheng, C. Stone, R. Harper, and P. Lee. TIL: A type-directed optimizing compiler for ML. In 1996 SIGPLAN Conference on Programming Language Design and Implementation, pages 181– 192, May 1996.

[30] Andrew Tolmach. Tag-free garbage collection using explicit type parameters. In 1994 ACM Conference on Lisp and Functional Programming, pages 1–11, Orlando, June 1994.

[31] Benjamin Werner. Une Théorie des Constructiones Inductives. PhD thesis, Université Paris VII, 1994.

A Static Semantics

A.1 Kind formation

Δ ⊢ k kind

Δ ⊢ Type kind

Δ ⊢ 1 kind

Δ ⊢ j kind (j ∈ Δ)
Δ, j ⊢ k kind
Δ ⊢ μj.k kind

(\begin{matrix}j only positive in k \\ j ∉ Δ \end{matrix})

Δ ⊢ k_{1} kind Δ ⊢ k_{2} kind

Δ ⊢ k_{1} → k_{2} kind

Δ ⊢ k_{1} kind Δ ⊢ k_{2} kind

Δ ⊢ k_{1} + k_{2} kind

Δ ⊢ k_{1} kind Δ ⊢ k_{2} kind

Δ ⊢ k_{1} × k_{2} kind A.2 Constructor Formation

Δ ⊢ c : k

Δ ⊢ ∗ : 1
Δ ⊢ α : Δ(α)
{Δ, α:k′ ⊢ c : k  Δ ⊢ k′ kind \atop Δ ⊢ λα:k′.c : k′ → k} (α ∉ Δ)

{Δ ⊢ c_{1} : k′ → k Δ ⊢ c_{2} : k′ \atop Δ ⊢ c_{1}c_{2} : k}

{Δ ⊢ c_{1} : k_{1} Δ ⊢ c_{2} : k_{2} \atop Δ ⊢ ⟨c_{1}, c_{2}⟩ : k_{1} × k_{2}}

{Δ ⊢ c : k_{1} × k_{2} \atop Δ ⊢ prj_{1} c : k_{1}}

{Δ ⊢ c : k_{1} × k_{2} \atop Δ ⊢ prj_{2} c : k_{2}}

Δ ⊢ c : k_{1} Δ ⊢ k_{2} kind Δ ⊢ inj_{1}^{k_{1}+k_{2}} c : k_{1} + k_{2} Δ ⊢ c : k_{2} Δ ⊢ k_{1} kind Δ ⊢ inj_{2}^{k_{1}+k_{2}} c : k_{1} + k_{2} Δ ⊢ c : k_{1} + k_{2} Δ, α:k_{1} ⊢ c_{1} : k Δ, α:k_{2} ⊢ c_{2} : k

Δ ⊢ case(c, α.c_{1}, α.c_{2}) : k (α ∉ Δ)

{Δ ⊢ c : k[μj.k/j] \atop Δ ⊢ fold_{μj.k} c : μj.k}

Δ, j, α:k, φ:j → k′ ⊢ c : k′ Δ ⊢ μj.k kind Δ, j ⊢ k′ kind

Δ ⊢ pr(j, α:k, φ:j →k′.c) : μj.k → k′[μj.k/j]

(j only positive in k′, and j, α, φ ∉ Δ)

Δ ⊢ int : Type Δ ⊢ τ_{1} : Type Δ ⊢ τ_{2} : Type

Δ ⊢ τ_{1} → τ_{2} : Type Δ ⊢ τ_{1} : Type Δ ⊢ τ_{2} : Type

Δ ⊢ τ_{1} × τ_{2} : Type Δ ⊢ τ_{1} : Type Δ ⊢ τ_{2} : Type

Δ ⊢ τ_{1} + τ_{2} : Type
Δ, α:k ⊢ τ : Type Δ ⊢ k kind

Δ ⊢ ∀α:k.τ : Type (α ∉ Δ)

Δ, α:k ⊢ τ : Type Δ ⊢ k kind

Δ ⊢ ∃α:k.τ : Type (α ∉ Δ)

Δ ⊢ void : Type Δ ⊢ unit : Type Δ ⊢ c : (k → Type) → k → Type Δ ⊢ k kind Δ ⊢ c′ : k Δ ⊢ rec_{k}(c, c′) : Type

A.3 Constructor Equivalence

Δ ⊢ c = c′ : k

Δ ⊢ c′ : k[μj.k/j]  Δ, j ⊢ k′ kind
Δ, j, α:k, φ:j → k′ ⊢ c : k′ Δ ⊢ μj.k kind

Δ ⊢ pr(j, α:k, φ:j → k′.c)(fold_{μj.k} c′) = \begin{matrix}c[μj.k, c′, pr(j, α:k, φ:j →k′.c)/j, α, φ] \\ : k′[μj.k/j] \end{matrix}

(j only positive in k′ and j, α, φ ∉ Δ)

{Δ ⊢ c_{1} : k Δ ⊢ c_{2} : k′ \atop Δ ⊢ prj_{1}⟨c_{1}, c_{2}⟩ = c_{1} : k}

{Δ ⊢ c_{1} : k′ Δ ⊢ c_{2} : k \atop Δ ⊢ prj_{2}⟨c_{1}, c_{2}⟩ = c_{2} : k}

Δ ⊢ c : k_{1} × k_{2} Δ ⊢ ⟨prj_{1} c, prj_{2} c⟩ = c : k_{1} × k_{2}

\begin{matrix}Δ ⊢ k′ kind \\ Δ, α:k′ ⊢ c : k′ Δ ⊢ c′ : k \end{matrix}

Δ ⊢ (λα:k′.c)c′ = c[c′/α] : k (α ∉ Δ)

{Δ ⊢ c : k′ → k \atop Δ ⊢ (λα:k′.cα) = c : k′ → k} (α ∉ F V (c))

Δ, α:k_{1} ⊢ c_{1} : k Δ, α:k_{2} ⊢ c_{2} : k

Δ ⊢ c : k_{1} Δ ⊢ k_{2} kind

Δ ⊢ case(inj_{1}^{k_{1}+k_{2}} c, α.c_{1}, α.c_{2}) = c_{1}[c/α] : k Δ, α:k_{1} ⊢ c_{1} : k Δ, α:k_{2} ⊢ c_{2} : k

Δ ⊢ c : k_{2} Δ ⊢ k_{1} kind

Δ ⊢ case(inj_{2}^{k_{1}+k_{2}} c, α.c_{1}, α.c_{2}) = c_{2}[c/α] : k

Δ ⊢ c : k_{1} + k_{2}

Δ ⊢ case(c, α_{1}. inj_{1}^{k_{1}+k_{2}} α_{1}, α_{2}. inj_{2}^{k_{1}+k_{2}} α_{2}) = c : k_{1} + k_{2}

{Δ ⊢ c : k \atop Δ ⊢ c = c : k}

{Δ ⊢ c′ = c : k \atop Δ ⊢ c = c′ : k}

{Δ ⊢ c_{1} = c_{2} : k Δ ⊢ c_{2} = c_{3} : k \atop Δ ⊢ c_{1} = c_{3} : k}

{Δ, α:k′ ⊢ c = c′ : k  Δ ⊢ k′ kind \atop Δ ⊢ λα:k′.c = λα:k′.c′ : k′ → k} (α ∉ Δ)

{Δ ⊢ c_{1} = c′_{1} : k′ → k Δ ⊢ c_{2} = c′_{2} : k′ \atop Δ ⊢ c_{1}c_{2} = c′_{1}c′_{2} : k}

{Δ ⊢ c_{1} = c′_{1} : k_{1} Δ ⊢ c_{2} = c′_{2} : k_{2} \atop Δ ⊢ ⟨c_{1}, c_{2}⟩ = ⟨c′_{1}, c′_{2}⟩ : k_{1} × c_{2}}

Δ ⊢ c = c′ : k_{1} × k_{2} Δ ⊢ prj_{1} c = prj_{1} c′ : k_{1}

Δ ⊢ c = c′ : k_{1} × k_{2} Δ ⊢ prj_{2} c = prj_{2} c′ : k_{2}

Δ ⊢ c = c′ : k_{1} Δ ⊢ k_{2} kind Δ ⊢ inj_{1}^{k_{1}+k_{2}} c = inj_{1}^{k_{1}+k_{2}} c′ : k_{1} + k_{2}

Δ ⊢ c = c′ : k_{2} Δ ⊢ k_{1} kind Δ ⊢ inj_{2}^{k_{1}+k_{2}} c = inj_{2}^{k_{1}+k_{2}} c′ : k_{1} + k_{2}

Δ ⊢ c = c′ : k_{1} + k_{2} Δ, α:k_{1} ⊢ c_{1} = c′_{1} : k Δ, α:k_{2} ⊢ c_{2} = c′_{2} : k

Δ ⊢ case(c, α.c_{1}, α.c_{2}) = case(c′, α.c′_{1}, α.c′_{2}) : k (α ∉ Δ)

Δ ⊢ c = c′ : k[μj.k/j] Δ ⊢ fold_{μj.k} c = fold_{μj.k} c′ : μj.k Δ, j, α:k, φ:j → k′ ⊢ c_{1} = c_{2} : k′ Δ ⊢ μj.k kind Δ, j ⊢ k′ kind

Δ ⊢ pr(j, α:k, φ:j →k′.c_{1}) = pr(j, α:k, φ:j →k′.c_{2}) : μj.k → k′[μj.k/j] (j only positive in k′ and j, α, φ ∉ Δ)

Δ ⊢ τ_{1} = τ′_{1} : Type Δ ⊢ τ_{2} = τ′_{2} : Type Δ ⊢ τ_{1} → τ_{2} = τ′_{1} → τ′_{2} : Type Δ ⊢ τ_{1} = τ′_{1} : Type Δ ⊢ τ_{2} = τ′_{2} : Type

Δ ⊢ τ_{1} × τ_{2} = τ′_{1} × τ′_{2} : Type

Δ ⊢ τ_{1} = τ′_{1} : Type Δ ⊢ τ_{2} = τ′_{2} : Type

Δ ⊢ τ_{1} + τ_{2} = τ′_{1} + τ′_{2} : Type
Δ, α:k ⊢ τ = τ′ : Type Δ ⊢ k kind

Δ ⊢ ∀α:k.τ = ∀α:k.τ′ : Type (α ∉ Δ)

Δ, α:k ⊢ τ = τ′ : Type Δ ⊢ k kind

Δ ⊢ ∃α:k.τ = ∃α:k.τ′ : Type (α ∉ Δ)

Δ ⊢ k kind Δ ⊢ c_{2} = c′_{2} : k

Δ ⊢ c_{1} = c′_{1} : (k → Type) → k → Type Δ ⊢ rec_{k}(c_{1}, c_{2}) = rec_{k}(c′_{1}, c′_{2}) : Type

A.4 Term Formation

Δ; Γ ⊢ e : τ

Δ; Γ ⊢ i : int Δ; Γ ⊢ ∗ : unit Δ; Γ ⊢ x : Γ(x)
Δ; Γ, x:τ′ ⊢ e : τ Δ ⊢ τ′ : Type

Δ; Γ ⊢ λx:τ′.e : τ′ → τ (x ∉ Γ)

{Δ; Γ ⊢ e_{1} : τ′ → τ Δ; Γ ⊢ e_{2} : τ′ \atop Δ; Γ ⊢ e_{1}e_{2} : τ}

{Δ; Γ ⊢ e_{1} : τ_{1} Δ; Γ ⊢ e_{2} : τ_{2} \atop Δ; Γ ⊢ ⟨e_{1}, e_{2}⟩ : τ_{1} × τ_{2}}

{Δ; Γ ⊢ e : τ_{1} × τ_{2} \atop Δ; Γ ⊢ prj_{1} e : τ_{1}}

{Δ; Γ ⊢ e : τ_{1} × τ_{2} \atop Δ; Γ ⊢ prj_{2} e : τ_{2}}

Δ; Γ ⊢ e : τ_{1} Δ ⊢ τ_{2} : Type

Δ; Γ ⊢ inj_{1}^{τ_{1}+τ_{2}} e : τ_{1} + τ_{2}

Δ; Γ ⊢ e : τ_{2} Δ ⊢ τ_{1} : Type

Δ; Γ ⊢ inj_{2}^{τ_{1}+τ_{2}} e : τ_{1} + τ_{2}
Δ; Γ ⊢ e : τ_{1} + τ_{2}
Δ; Γ, x:τ_{1} ⊢ e_{1} : τ
Δ; Γ, x:τ_{2} ⊢ e_{2} : τ

Δ; Γ ⊢ case(e, x.e_{1}, x.e_{2}) : τ (x ∉ Γ)

Δ, α:k; Γ ⊢ v : τ Δ ⊢ k kind

Δ; Γ ⊢ Λα:k.v : ∀α:k.τ (α ∉ Δ)

{Δ; Γ ⊢ e : ∀α:k.τ Δ ⊢ c′ : k \atop Δ; Γ ⊢ e[c′] : τ[c′/α]}

Δ; Γ, f:τ ⊢ e : τ Δ ⊢ τ : Type

Δ; Γ ⊢ fix f:τ.e : τ

(f ∉ Γ and e = Λα_{1}:k_{1} … Λα_{n}:k_{n}.λx:τ′.e′)

Δ, α:k ⊢ τ : Type Δ ⊢ c : k Δ; Γ ⊢ e : τ[c/α]

Δ; Γ ⊢ pack e as ∃α:k.τ hiding c : ∃α:k.τ

(α ∉ Δ)

\begin{matrix}Δ; Γ ⊢ e_{1} : ∃α:k.τ_{2} \\ Δ, α:k; Γ, x:τ_{2} ⊢ e_{2} : τ_{1} \end{matrix}

Δ; Γ ⊢ unpack ⟨α, x⟩ = e_{1} in e_{2} : τ_{1} (\begin{matrix}α ∉ Δ, F V (τ) \\ x ∉ Γ \end{matrix})

Δ; Γ ⊢ e : rec_{k}(c, c′) Δ; Γ ⊢ unfold e : c(λα:k. rec_{k}(c, α))c′

Δ; Γ ⊢ e : c(λα:k. rec_{k}(c, α))c′

Δ ⊢ rec_{k}(c, c′) : Type Δ; Γ ⊢ fold_{rec_{k}(c,c′)} e : rec_{k}(c, c′)

Δ, β:k_{1}, Δ′; Γ[inj_{1}^{k_{1}+k_{2}} β/α] ⊢

e_{1}[inj_{1}^{k_{1}+k_{2}} β/α] : τ[inj_{1}^{k_{1}+k_{2}} β/α]

Δ, β:k_{2}, Δ′; Γ[inj_{2}^{k_{1}+k_{2}} β/α] ⊢

e_{2}[inj_{2}^{k_{1}+k_{2}} β/α] : τ[inj_{2}^{k_{1}+k_{2}} β/α] Δ, α:k_{1} + k_{2}, Δ′ ⊢ c = α : k_{1} + k_{2}

Δ, α:k_{1} + k_{2}, Δ′; Γ ⊢ ccase_{τ} (c, β.e_{1}, β.e_{2}) : τ (β ∉ Δ)

\begin{matrix}Δ, β:k_{1}, γ:k_{2}, Δ′; Γ[⟨β, γ⟩/α] ⊢ e[⟨β, γ⟩/α] : τ[⟨β, γ⟩/α] \\ Δ, α:k_{1} × k_{2}, Δ′ ⊢ c = α : k_{1} × k_{2} \end{matrix}

Δ, α:k_{1} × k_{2}, Δ′; Γ ⊢ let_{τ} ⟨β, γ⟩ = c in e : τ (β, γ ∉ Δ)

Δ, β:k[μj.k/j], Δ′; Γ[fold_{μj.k} β/α] ⊢

e[fold_{μj.k} β/α] : τ[fold_{μj.k} β/α] Δ, α:μj.k, Δ′ ⊢ c = α : μj.k

Δ, α, Δ′:μj.k; Γ ⊢ let_{τ} (fold_{μj.k} β) = c in e : τ

(β ∉ Δ)

Δ ⊢ c = inj_{1}^{k_{1}+k_{2}} c′ : k_{1} + k_{2} Δ; Γ ⊢ e_{1}[c′/α] : τ Δ; Γ ⊢ ccase_{τ} (c, α.e_{1}, α.e_{2}) : τ

Δ ⊢ c = inj_{2}^{k_{1}+k_{2}} c′ : k_{1} + k_{2} Δ; Γ ⊢ e_{2}[c′/α] : τ Δ; Γ ⊢ ccase_{τ} (c, α.e_{1}, α.e_{2}) : τ

Δ ⊢ c = ⟨c_{1}, c_{2}⟩ : k_{1} × k_{2} Δ; Γ ⊢ e[c_{1}, c_{2}/β, γ] : τ

Δ; Γ ⊢ let_{τ} ⟨β, γ⟩ = c in e : τ

Δ ⊢ c = fold_{μj.k}(c′) Δ; Γ ⊢ e[c′/β] : τ

Δ; Γ ⊢ let_{τ} (fold_{μj.k} β) = c in e : τ

Δ; Γ ⊢ e : τ′ Δ ⊢ τ = τ′ : Type
Δ; Γ ⊢ e : τ

A.5 Erasure-compatible typing rules (vcase)

Δ, β:k_{1}, Δ′; Γ[inj_{1}^{k_{1}+k_{2}} β/α] ⊢

v[inj_{1}^{k_{1}+k_{2}} β/α] : void

Δ, β:k_{2}, Δ′; Γ[inj_{2}^{k_{1}+k_{2}} β/α] ⊢

e[inj_{2}^{k_{1}+k_{2}} β/α] : τ[inj_{2}^{k_{1}+k_{2}} β/α]

Δ, α:k_{1} + k_{2}, Δ′ ⊢ c = α : k_{1} + k_{2} Δ, α:k_{1} + k_{2}, Δ′; Γ ⊢ vcase_{τ} (c, β. dead v, β.e) : τ

(β ∉ Δ)

Δ, β:k_{1}, Δ′; Γ[inj_{1}^{k_{1}+k_{2}} β/α] ⊢

e[inj_{1}^{k_{1}+k_{2}} β/α] : τ[inj_{1}^{k_{1}+k_{2}} β/α]

Δ, β:k_{2}, Δ′; Γ[inj_{2}^{k_{1}+k_{2}} β/α] ⊢

v[inj_{2}^{k_{1}+k_{2}} β/α] : void

Δ, α:k_{1} + k_{2}, Δ′ ⊢ c = α : k_{1} + k_{2} Δ, α:k_{1} + k_{2}, Δ′; Γ ⊢ vcase_{τ} (c, β.e, β. dead v) : τ

(β ∉ Δ)

Δ ⊢ c = inj_{1}^{k_{1}+k_{2}} c′ : k_{1} + k_{2} Δ; Γ ⊢ e_{1}[c′/α] : τ Δ; Γ ⊢ vcase_{τ} (c, α.e_{1}, α. dead v) : τ

Δ ⊢ c = inj_{2}^{k_{1}+k_{2}} c′ : k_{1} + k_{2} Δ; Γ ⊢ e_{2}[c′/α] : τ Δ; Γ ⊢ vcase_{τ} (c, α. dead v, α.e_{2}) : τ

B Operational Semantics

Value syntax

v ::= i | ∗ | λx:c.e | ⟨v_{1}, v_{2}⟩ | inj_{1}^{τ_{1}+τ_{2}} v | inj_{2}^{τ_{1}+τ_{2}} v
| Λα:k.v | fix f:τ.v | fold_{rec_{k}(c,c′)} v

     | pack v as ∃α.c_{1} hiding c_{2}

| x | prj_{1} v | prj_{2} v

Evaluation rules

(λx:c.e)v ↦ e[v/x]
{e_{1} ↦ e′_{1} \atop e_{1}e_{2} ↦ e′_{1}e_{2}}  {e_{2} ↦ e′_{2} \atop ve_{2} ↦ ve′_{2}}
prj_{1}⟨v_{1}, v_{2}⟩ ↦ v_{1}  prj_{2}⟨v_{1}, v_{2}⟩ ↦ v_{2}
{e ↦ e′ \atop prj_{1} e ↦ prj_{1} e′}  {e ↦ e′ \atop prj_{2} e ↦ prj_{2} e′}

{e_{1} ↦ e′_{1} \atop ⟨e_{1}, e_{2}⟩ ↦ ⟨e′_{1}, e_{2}⟩}  {e_{2} ↦ e′_{2} \atop ⟨v, e_{2}⟩ ↦ ⟨v, e′_{2}⟩}

case(inj_{1}^{τ_{1}+τ_{2}} v, x_{1}.e_{1}, x_{2}.e_{2}) ↦ e_{1}[v/x_{1}]

case(inj_{2}^{τ_{1}+τ_{2}} v, x_{1}.e_{1}, x_{2}.e_{2}) ↦ e_{2}[v/x_{2}]

{e ↦ e′ \atop inj_{1}^{τ_{1}+τ_{2}} e ↦ inj_{1}^{τ_{1}+τ_{2}} e′}

{e ↦ e′ \atop inj_{2}^{τ_{1}+τ_{2}} e ↦ inj_{2}^{τ_{1}+τ_{2}} e′}

e ↦ e′

case(e, x_{1}.e_{1}, x_{2}.e_{2}) ↦ case(e′, x_{1}.e_{1}, x_{2}.e_{2})

Λα:k.v[c] ↦ v[c/α]  {e ↦ e′ \atop e[c] ↦ e′[c]}

c normalizes to inj_{1} c′

ccase(c, α_{1}.e_{1}, α_{2}.e_{2}) ↦ e_{1}[c′/α_{1}]

c normalizes to inj_{2} c′

ccase(c, α_{1}.e_{1}, α_{2}.e_{2}) ↦ e_{2}[c′/α_{2}]

c normalizes to ⟨c_{1}, c_{2}⟩ let⟨β, γ⟩ = c in e ↦ e[c_{1}, c_{2}/β, γ]

c normalizes to fold_{μj.k} c′ let(fold_{μj.k} β) = c in e ↦ e[c′/β]

(fix f:τ.v)v′ ↦ (v[fix f:τ.v/f])v′

(fix f:τ.v)[c] ↦ (v[fix f:τ.v/f])[c]

e ↦ e′

pack e as ∃β.c_{1} hiding c_{2} ↦ pack e′ as ∃β.c_{1} hiding c_{2}

e ↦ e′

unpack ⟨α, x⟩ = e in e_{2} ↦ unpack ⟨α, x⟩ = e′ in e_{2}

unfold (fold_{rec_{k}(c,c′)} v) ↦ v

e ↦ e′

fold_{rec_{k}(c,c′)} e ↦ fold_{rec_{k}(c,c′)} e′

e ↦ e′

unfold e ↦ unfold e′

B.1 Erasure-compatible operational rules

      (vcase)

Value syntax

v ::= … | v[c]

Evaluation rules Delete evaluation rules for fix and ccase and replace them with the following:

(fix f:τ.v)[c_{1}] ⋯ [c_{n}]v′ ↦ (v[fix f:τ.v/f])[c_{1}] ⋯ [c_{n}]v′

c normalizes to inj_{1} c′

vcase(c, α_{1}.e_{1}, α_{2}. dead v) ↦ e_{1}[c′/α_{1}]

c normalizes to inj_{2} c′

vcase(c, α_{1}. dead v, α_{2}.e_{2}) ↦ e_{2}[c′/α_{2}]

C Type Erasure Formulation

Although the formal static and operational semantics for the erasable version of LX (Section 5) are for a typed language, we would like to emphasize the point that types are unnecessary for computation and can be safely erased. To do this we exhibit an untyped language, LX^{∘}, a translation from LX through type erasure, and the following theorem, which states that execution in the untyped language mirrors execution in the typed language:

Theorem C.1 1. If e_{1} ↦^{∗} e_{2} then e_{1}^{∘} ↦^{∗} e_{2}^{∘}.

2. If ∅ ⊢ e_{1} : τ and e_{1}^{∘} ↦^{∗} u then there exists e_{2} such that e_{1} ↦^{∗} e_{2} and e_{2}^{∘} = u.

From this theorem and the type safety of LX it follows that our untyped semantics is safe.

Corollary C.2 If ∅ ⊢ e : τ and e^{∘} ↦^{∗} u then u is not stuck.

C.1 Syntax of Untyped Calculus

(terms) u :: = ∗ | i | x | λx.u | fix f.w | u_{1}u_{2}
| ⟨u_{1}, u_{2}⟩ | prj_{1} u | prj_{2} u | inj_{1} u
| inj_{2} u | case(u, x_{1}.u_{1}, x_{2}.u_{2})
(values) w :: = x | i | λx.u | fix f.w | ⟨w_{1}, w_{2}⟩

                 | inj_{1} w | inj_{2} w | prj_{1} w | prj_{2} w

C.2 Type Erasure

x^{∘} = x
i^{∘} = i
⟨e_{1}, e_{2}⟩^{∘} = ⟨e_{1}^{∘}, e_{2}^{∘}⟩
(prj_{i} e)^{∘} = prj_{i} e^{∘}
(λx:τ.e)^{∘} = λx.e^{∘}
(Λα:k.v)^{∘} = v^{∘}
(fix f:τ.e)^{∘} = fix f.e^{∘}
(e_{1}e_{2})^{∘} = e_{1}^{∘}e_{2}^{∘}
e[c]^{∘} = e^{∘}

       pack e as c hiding c′^{∘} = e^{∘}

unpack ⟨α, x⟩ = e_{1} in e_{2}^{∘} = (λx.e_{2}^{∘}) e_{1}^{∘}
inj_{i}^{τ_{1}+τ_{2}} e^{∘} = inj_{i} e^{∘}
case(e, x_{1}.e_{1}, x_{2}.e_{2})^{∘} = \begin{matrix}case(e^{∘}, \\ x_{1}.e_{1}^{∘}, x_{2}.e_{2}^{∘}) \end{matrix}
∗^{∘} = ∗
fold_{rec_{k}(c,c′)} e^{∘} = e^{∘}

                unfold e^{∘} = e^{∘}

vcase(c, α_{1}.e, α_{2}. dead v)^{∘} = e^{∘}
vcase(c, α_{1}. dead v, α_{2}.e)^{∘} = e^{∘}

C.3 Operational Semantics of LX^{∘}

(λx.u)w ↦ u[w/x]

(fix f.w)w′ ↦ (w[fix f.w/f])w′

{u_{1} ↦ u′_{1} \atop u_{1}u_{2} ↦ u′_{1}u_{2}}  {u ↦ u′ \atop wu ↦ wu′}

prj_{1}⟨w_{1}, w_{2}⟩ ↦ w_{1} prj_{2}⟨w_{1}, w_{2}⟩ ↦ w_{2}

{u_{1} ↦ u′_{1} \atop ⟨u_{1}, u_{2}⟩ ↦ ⟨u′_{1}, u_{2}⟩}  {u ↦ u′ \atop ⟨w, u⟩ ↦ ⟨w, u′⟩}

{u ↦ u′ \atop prj_{1} u ↦ prj_{1} u′}  {u ↦ u′ \atop prj_{2} u ↦ prj_{2} u′}

case(inj_{1} w, x_{1}.u_{1}, x_{2}.u_{2}) ↦ u_{1}[w/x_{1}]

case(inj_{2} w, x_{1}.u_{1}, x_{2}.u_{2}) ↦ u_{2}[w/x_{2}]

{u ↦ u′ \atop inj_{1} u ↦ inj_{1} u′}  {u ↦ u′ \atop inj_{2} u ↦ inj_{2} u′}

u ↦ u′
case(u, x_{1}.u_{1}, x_{2}.u_{2}) ↦ case(u′, x_{1}.u_{1}, x_{2}.u_{2})