Abstract
The literature presents many strategies for enforcing the integrity of types when typed code interacts with untyped code. This article presents a uniform evaluation framework that characterizes the differences among some major existing semantics for typed–untyped interaction. Type system designers can use this framework to analyze the guarantees of their own dynamic semantics.
1 CALLING ALL TYPES
Many programming languages let typed code interact with untyped code in some ways while retaining some desirable aspects of each typing discipline. The currently popular research focus of gradual typing provides many examples. Exactly which interactions are allowed and which desirable aspects are retained, however, varies widely among languages. There are four leading type-enforcement strategies that restrict interactions between typed and untyped code:
• | Erasure (a.k.a. optional typing) is a hands-off method that uses types only for static analysis and imposes no restrictions at run-time [8, 11]. | ||||
• | Transient inserts shape checks1 in typed code to guarantee only that operations cannot “go wrong” in the typed portion of code due to values from the untyped portion [83, 86]. | ||||
• | Natural uses higher-order checks to ensure the integrity of types in the entire program [68, 78]. | ||||
• | Concrete enforces types with tag checks. It ensures the full integrity of types, but requires that every value comes with a fully descriptive type tag [52, 93]. | ||||
In addition, researchers have designed hybrid techniques [9, 31, 34, 61, 64]. An outstanding and unusual exemplar of this kind is Pyret, a language targeting the educational realm (
Each semantic choice denotes a trade-off among static guarantees, expressiveness, and run-time costs. Language designers should understand these trade-offs when they create a new typed–untyped interface. Programmers need to appreciate the trade-offs if they can choose a language for a project. If stringent constraints on untyped code are acceptable, then Concrete offers strong and inexpensive guarantees. If the goal is to interoperate with an untyped language that does not support proxy values, then Transient may be the most desirable option. If fine-grained interoperability demands complete type integrity everywhere, then Natural is the right choice.2 And if predictable behavior and performance matter most, then Erasure may be best—it is certainly the industry favorite.
Unfortunately, the literature provides little guidance about how to compare such different semantics formally. For example, the dynamic gradual guarantee [69]—a widely studied property in the gradual typing world—is satisfied by any type-enforcement strategy, including the no-check Erasure, as long as the type
This article introduces a framework for systematically comparing the behavioral guarantees offered by different semantics of typed–untyped interaction. The comparison begins with a common surface syntax to express programs that can mix typed and untyped code. This surface syntax is then assigned multiple semantics, each of which follows a distinct protocol for enforcing the integrity of types across boundaries. With this framework, one can directly study the possible behaviors for a single program.
Using the framework, the article compares the three implemented semantics explained above (Natural (\(\mathsf {N}\)), Transient (\(\mathsf {T}\)), Erasure (\(\mathsf {E}\))) and three theoretical ones (Co-Natural (\({\mathsf {C}}\)), Forgetful (\({\mathsf {F}}\)), and Amnesic (\({\mathsf {A}}\))). Co-Natural enforces data structures lazily rather than eagerly. Forgetful is lazy in the same way and also ignores type obligations that are not strictly required for type soundness. Amnesic is a variation of Transient that uses wrappers to improve its blame guarantees.
The comparison excludes two classes of prior work: Concrete, because of the stringent constraints it places on untyped code, and semantics that rely on an analysis of the untyped code (such as References [2, 13, 91]). That is, the focus is on enforcement strategies that can deal with untyped code as a “dusty deck” without recompiling the untyped world each time a new type boundary appears.
Table 1 sketches the results of the evaluation. The six letters in the top row correspond to different semantics for the common surface language. Each row introduces one discriminating property. Type soundness guarantees the validity of types in typed code. Complete monitoring—a property adapted from research on contracts [23]—guarantees that the type system moderates all boundaries between typed and untyped code—even boundaries that arise at run-time. Blame soundness ensures that when a run-time check goes wrong, the error message contains only boundaries that are relevant to the problem. Blame completeness guarantees that error messages come with all relevant information, though possibly with some irrelevant extras. For both blame soundness and completeness, the notion of relevant boundaries is determined by an independent (axiomatic) specification that tracks values as they cross boundaries between typed and untyped code. Last, the error preorder compares the relative permissiveness of types in two semantics. Natural (\(\mathsf {N}\)) accepts the fewest programs without raising a run-time type mismatch and Erasure (\(\mathsf {E}\)) accepts the greatest number of programs. Additionally, Transient and Erasure are the only strategies that can avoid the complexity of wrapper values.
Full results in Table 2 (page 51).
Table 1. Informal Sketch of Contributions
Full results in Table 2 (page 51).
In sum, the five properties enable a uniform analysis of existing strategies and can guide the search for new strategies. Indeed, the synthetic Amnesic semantics (\(\mathsf {A}\)) is the result of a search for a semantics that fails complete monitoring but guarantees sound and complete blame.
1.1 Performance and Pragmatics Are Out of Scope
Understanding the formal properties of typed–untyped interactions is only one third of the challenge. Two parallel and ongoing quests aim to uncover the performance implications of different strategies [6, 24, 34, 37, 38, 44] and the pragmatics of the semantics for working developers [45]. These efforts fall outside the scope of this article.
1.2 Relation to Prior Work
This article is a synthesis of results that have been published piecemeal in two conference papers [34, 35] and a dissertation chapter [33]. It is the only article to compare the six semantics on equal grounds. In addition to the synthesis, it brings three contributions: a survey of type-enforcement strategies, a high-level comparison of the six semantics, and refined meta-theoretic results.
1.3 Outline
Sections 2 through 5 explain the what, why, and how of our design-space analysis. There is a huge body of work on languages that support typed–untyped interactions that needs organizing principles (Section 2). The properties listed in the top five rows of Table 1 offer an expressive and scalable basis for comparison (Section 3). By starting with a common surface language and defining semantics that explore various strategies for enforcing types, the properties enable apples-to-apples comparisons of the dynamics of typed–untyped interactions (Section 4). This article focuses on six type-enforcement strategies in particular (Section 5).
Section 6 formally presents the six semantics and the key results. Expert readers who are not interested in informal discussions may wish to begin there and use Section 5 as needed for a high-level picture. The supplementary materialpresents the essential definitions, lemmas, and proof sketches that support the results.
2 ASSORTED BEHAVIORS BY EXAMPLE
There are many languages that allow typed and untyped code to interact. Figure 1 arranges a few of their names into a rough picture of the design space. Languages marked with a star (\(\star {}\)) are gradual in the sense that they come with a universal dynamic type, often styled as
Fig. 1. Landscape of mixed-typed languages, \(\displaystyle \dagger\) = migratory, \(\displaystyle \star\) = gradual.
For the most part, these mixed-typed languages fit into the broad forms introduced in Section 1. Erasure is by far the most popular strategy; perhaps because of its uncomplicated semantics and ease of implementation. The Natural languages come from academic teams that are interested in types that offer strong guarantees, Transient is gaining attention as a compromise between types and performance, and Concrete has generated interest among industry teams as well as academics. Several languages exhibit a hybrid approach. Sorbet adds types to Ruby and optionally checks method signatures at run-time. Thorn and StrongScript offer both concrete and erased types [61, 93]. Pyret uses Natural-style checks to validate fixed-size data and Transient-style checks for recursive types (e.g., lists) and higher-order types.4 Static Python combines Transient and Co-Natural to mitigate the restrictions of the latter [46]. Grift has a second mode that implements a monotonic semantics [4]. Prior to its 2.0release, Dart took a hybrid approach. Developers could toggle between a checked mode and an Erasure mode. Monotonic is similar to Natural, but uses a checked heap instead of wrappers and rejects additional programs [58, 60, 64, 73]. A final variant is from the literature. Castagna and Lanvin [15] present a semantics that creates wrappers like Natural but also removes wrapper that do not matter for type soundness. This semantics is similar to the forgetful contract semantics [31].
Our goal is a systematic comparison of type guarantees across the wide design space. Such a comparison is possible, because, despite the variety, the different guarantees arise from choices about how to enforce types at the boundaries between statically typed code and dynamically typed code. The following three subsections present illustrative examples of interactions between typed and untyped code in four programming languages: Flow [16], Reticulated [86], Typed Racket [81], and Nom [52]. These languages use the Erasure, Transient, Natural, and Concrete strategies, respectively. Flow is a migratory typing system for JavaScript, Reticulated equips Python with gradual types, Typed Racket extends Racket, and Nom is a new gradual-from-the-start object-oriented language.
2.1 Enforcing a Base Type
One of the simplest ways that a typed–untyped interaction can go wrong is for untyped code to send incorrect input to a typed context that expects a first-order value. The first example illustrates one such interaction:

Figure 2 translates the program to the four chosen languages. Each white box represents type-checked code, and each grey box represents untyped and un-analyzed code. The arrows represent the boundary behavior: the solid arrow stands for the call from one area to the other, and the dashed one for the return. Nom is an exception, however, because it cannot interact with truly untyped code (Section 2.2). Despite the differences in syntax and types, each clearly defines a typed function that expects an integer and applies the function to itself in an untyped context.
Fig. 2. Program (1) translated to four languages.
In Flow (Figure 2(a)), the program does not detect a type mismatch. The typed function receives a function from untyped JavaScript and surprisingly computes a string (
Flow does not detect the run-time type mismatch, because it follows the erasure, or optional typing, approach to type enforcement. Erasure is hands-off; types have no effect on the behavior of a program. These static-only types help find typo-level mistakes and enable type-directed IDE tools, but disappear during compilation. Consequently, the author of a typed function in Flow cannot assume that it receives only well-typed input at run-time.
The other languages enforce static types with some kind of dynamic check. For base types, the check validates the shape of incoming data. The checks for other types reveal differences among these non-trivial type enforcement strategies.
2.2 Validating an Untyped Data Structure
The second example is about pairs. It asks what happens when typed code declares a pair type and receives an untyped pair:

Figure 3 translates this idea into Reticulated, Typed Racket, and Nom. The encodings in Reticulated and Typed Racket define a pair in untyped code and impose a type in typed code. The encoding in Nom is substantially different. Figure 3(c) presents a Nom program in which the typed code expects an instance of one data structure but the untyped code provides something else. This shape mismatch leads to a run-time error.
Fig. 3. Program (2) translations.
Nom cannot express program (2) directly, because the language does not allow truly untyped values. There is no common pair constructor that: (1) untyped code can use without constraints and (2) typed code can instantiate at a specific type. Instead, programmers must declare one kind of pair for every two types they wish to combine. On the one hand, this requirement greatly simplifies run-time validation, because the outermost shape of any value determines the full type of its elements. On the other hand, it imposes a significant programming burden. To add refined static type checking at the use-sites of an untyped data structure, a programmer must either add a cast to each use in typed code or edit the untyped code for a new data definition. Because of this rigidity, the model in Section 6 supports neither Nom nor other concrete languages [19, 52, 61, 93],
Both Reticulated and Typed Racket raise an error on program (2), but for different reasons. Typed Racket rejects the untyped pair at the boundary to the typed context, because the pair does not fully match the declared type. Reticulated accepts the value at the boundary, because it is a pair, but raises an exception at the elimination form
2.3 Debugging Higher-order Interactions
Figures 4 and 5 present simplified excerpts from realistic programs that mix typed and untyped code. These examples follow a common general structure: an untyped client interacts with an untyped library through a thin layer of typed code. The solid arrows indicate these statically visible dependencies. Additionally, the untyped client supplies an argument to the untyped service module that, due to type annotations, dynamically opens a back channel to the client; the dashed arrow indicates this dynamic dependency of the two untyped modules. Both programs also happen to signal run-time errors, but do so for different reasons and with rather different implications.
Fig. 4. Typed Racket detects and reports a higher-order type mismatch.
Fig. 5. Reticulated does not catch errors that occur in untyped Python code.
The first example shows how Typed Racket’s implementation of the Natural semantics, which monitors all interactions that cross type boundaries, can detect a mistake in a type declaration. The second example uses Reticulated’s implementation of the Transient semantics to demonstrate how a type-sound language can fail to detect a mismatch between a value and a type.
2.3.1 A Mistaken Type Declaration.
Figure 4 consists of an untyped library, an incorrect layer of type annotations, and an untyped client of the typed layer. The module at the top left,
Operationally, the library function flows from
Fortunately, Typed Racket compiles types to contracts and thereby catches the mismatch. Here, the compilation of
Alternative Possibility. If Typed Racket was merely type-sound, then it would not be guaranteed to catch the type mismatch between the interface and the client. In this case, the client function (underlined) passed to
2.3.2 A Data Structure Mismatch.
Figure 5 presents an arrangement of three Transient Reticulated modules, similar to the code in Figure 4. The module on the top left exports a function that retrieves data from a URL.6 This function accepts several optional and keyword arguments. The typed adaptor module on the right formulates types for one valid use of the function; namely, a client may supply a URL as a string and a timeout as a pair of floats. These types are correct, but the client module on the bottom left sends a tuple that contains an integer and a string.
Reticulated’s run-time checks ensure that the typed function receives a string and a tuple, but do not validate the tuple’s contents. These same arguments thus pass to the untyped
In this example, the developer is lucky, because the call to the typed version of
Alternative Possibility. If Reticulated chose to traverse the bad tuple at the type boundary, then it would discover the type mismatch. Similarly, if Reticulated checked all reads from the tuple in untyped contexts, then it could detect the mismatch and raise an appropriate error. Both alternatives go beyond what is strictly required for type soundness, but would help for debugging this program.
3 COMPARING SEMANTICS
The design of a type-enforcement strategy is a multi-faceted problem. A strategy determines: whether mismatches between type specifications and value flows are discovered; whether the typed portion of the code is statically typed in a conventional sense or a weaker one; what typed APIs mean for untyped client code; and whether an error message can pinpoint which type specification does not match which value. All decisions have implications for language designers and programmers.
The examples in Section 2 illustrate that various languages choose different points in this design space. But, examples can only motivate a systematic analysis; they cannot serve as an analysis. After all, examples tell us little about the broader implications of each choice.
A systematic analysis needs a suite of formal properties that differentiate the design choices for the language designer and working developer. These properties must apply to a large part of the design space. Finally, they should clarify which guarantees type specifications offer to the developers of typed and untyped code, respectively. While the literature focuses on type soundness and the blame theorem, our analysis adds new properties to the toolbox, which all parties should find helpful in making design choices or selecting languages for a project.
3.1 Type Soundness and the Blame Theorem
Type soundness is one formal property that meets the above criteria. A type soundness theorem can be tailored to a range of type systems, has meaning for typed and untyped code, and can be proven via a syntactic technique that scales to a variety of language features [92]. The use of type soundness in the literature, however, does not promote informed comparisons. Consider the four example languages from the previous section. Chaudhuri et al. [16] present a model of Flow and prove a conventional type soundness theorem under the assumption that all code is statically typed. Vitousek et al. [86] prove a type soundness theorem for Reticulated Python that focuses on shapes of values rather than types. Muehlboeck and Tate [52] prove a full type soundness theorem for Nom. Tobin-Hochstadt and Felleisen [78] prove a full type soundness theorem for a prototypical Typed Racket that includes a weak blame property. These four type soundness theorems differ in several regards: one focuses on the typed half of the language; a second proves a claim about a loose relationship between values and types; a third is a truly conventional type soundness theorem; and the last one incorporates a claim about the quality of error messages.
Another well-studied property is the blame theorem [1, 64, 78, 86–88]. It states that a run-time mismatch may occur only when an untyped—or less-precisely typed—value enters a typed context. The property is a useful design principle, but too many languages satisfy this property too easily.
3.2 Our Analysis
The primary formal property has to be type soundness, because it tells a programmer that evaluation is well-defined in each component of a mixed-typed programs. The different levels of soundness that arise in the literature must, however, be clearly separated. For one, the canonical forms lemmas that support these different levels of soundness set limits on the type-directed optimizations that a compiler may safely perform.
The second property, complete monitoring, asks whether types guard all statically declared and dynamically created channels of communication between typed and untyped code. That is, whether every interaction between typed and untyped code is mediated by run-time checks. Section 2.3 illustrates this point with two contrasting example. Both open channels of communication between untyped pieces of code at run time—see the dashed arrows in Figures 4 and 5—that are due to value flows through typed pieces of code. While Typed Racket’s type-enforcement mechanism catches this problem, Reticulated’s does not. (The problem is caught by the run-time checks of Python.)
When a run-time check discovers a mismatch between a type specification and a flow of values and the run-time system issues an error message, the question arises how informative the message is to a debugging programmer. Blame soundness and blame completeness ask whether a semantics can identify the responsible parties when a run-time type mismatch occurs. Soundness asks for a subset of the potential culprits; completeness asks for a superset.
Furthermore, the differences among type soundness theorems and the gap between type soundness and complete monitoring suggests the question of how many errors an enforcement regime discovers. The answer is given by an error preorder relation, which compares semantics in terms of the run-time mismatches that they discover.
Individually, each property characterizes a particular aspect of a type-enforcement strategy. Together, the properties inform us about the nature of the multi-faceted design space that this semantics problem opens up. Additionally, this work should help with the investigation of the consequences of design choices for the working developer.
4 EVALUATION FRAMEWORK
To formulate different type-enforcement stategies on an equal footing, the framework is based on a single mixed-typed surface language (Section 4.1). This syntax is then equipped with distinct semantics to model the different type-enforcement strategies (Section 4.2). Type soundness (Section 4.3) and complete monitoring (Section 4.4) characterize the type mismatches that a semantics can detect. Blame soundness and blame completeness (Section 4.5) measure the theoretical quality of error messages. The error preorder (Section 4.6) is a direct comparison of the semantics.
4.1 Surface Language
The surface syntax is a multi-language that combines two independent pieces in the style of Matthews and Findler [48]. Statically typed expressions constitute one piece; dynamically typed expressions are the other half. Technically, these expression languages are identified by two judgments: typed expressions \(e_0\) satisfy \(\vdash e_0 : \tau _0\) for some type \(\tau _0\), and untyped expressions \(e_1\) satisfy \(\vdash e_1 : {\mathcal {U}}\) for the uni-type. Boundary expressions connect the two pieces.
The uni-type \({\mathcal {U}}\) is not the flexible dynamic type from the theory of gradual typing that can replace any static type [5, 68, 77], rather, it describes all well-formed untyped expressions [48].7 There is consequently no need for a type precision judgment in the surface language, because all typed–untyped interactions occur through boundary expressions. In this way, our surface language closely resembles the cast calculi that serve as intermediate languages in the gradual typing literature, e.g., References [67, 69].
The sets of statically typed (\(v_{s}\)) and dynamically typed (\(v_{d}\)) values consist of integers, natural numbers, pairs, and functions: \(\begin{align*} \begin{array}{l@{\qquad}l} v_{s} = i\mid n\mid \langle v_{s},v_{s} \rangle \mid \lambda (x:\tau).\, e_{s}, & \tau = \mathsf {Int}\mid \mathsf {Nat}\mid \tau \!\Rightarrow \!\tau \mid \tau \!\times \!\tau ,\\ v_{d} = i\mid n\mid \langle v_{d},v_{d} \rangle \mid \lambda x.\, e_{d}.& \end{array} \end{align*}\)These core value sets are relatively small, but they suffice to illustrate the behavior of types for the basic ingredients of a full language. First, the values include atomic data, finite structures, and higher-order values. Second, the natural numbers \(n\) are a subset of the integers \(i\) to motivate a subtyping judgment for the typed half of the language. Subtyping adds some realism to the model8 and allows it to distinguish between two sound enforcing methods (declaration-site vs. use-site).
Surface expressions include function application, primitive operations, and boundaries. The details of the first two are fairly standard (Section 6.1), although function application comes with an explicit \(\mathsf {app}\) operator (\(\mathsf {app}\, e_0\, e_1\)). Boundary expressions (\(\mathsf {dyn}\) and \(\mathsf {stat}\)) are the glue that enables typed–untyped interactions. A program starts with named chunks of code, called components. Boundary expressions link these chunks together with a static type to describe the values that may cross the boundary. Suppose that a typed component named \(\ell _0\) imports and applies an untyped function from component \(\ell _1\):

\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} (3) \simeq \mathsf {app} (\mathsf {dyn}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft}\,\mathsf {Nat}\!\Rightarrow \!\mathsf {Nat} {\scriptscriptstyle \blacktriangleleft }\,\ell _1) (\lambda x_0.\, \mathsf {sum}\, x_0\, 2))\, 9. \end{array}\)
In turn, this two-component expression may be imported into a larger untyped component. The sketch below shows an untyped component in the center that imports two typed components: a new typed function on the left and the expression (3) on the right:

\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} (4) \simeq \mathsf {app} (\mathsf {stat}^{}\,(\ell _2 {\scriptscriptstyle \blacktriangleleft }\,\mathsf {Int}\!\times \!\mathsf {Int}\!\Rightarrow \!\mathsf {Int} {\scriptscriptstyle \blacktriangleleft }\,\ell _3) (\lambda (x_1:\mathsf {Int}\!\times \!\mathsf {Int}).\, \mathsf {fst}\, x_1)) \\ \hphantom{(4) = \mathsf {app} }(\mathsf {stat}^{}\,(\ell _2 {\scriptscriptstyle \blacktriangleleft }\,\mathsf {Nat} {\scriptscriptstyle \blacktriangleleft }\,\ell _0) (3)). \end{array}\)
Technically, a boundary expression combines a boundary specification \(b\) and a sender expression. A \(\mathsf {dyn}\) boundary embeds dynamically typed code in a typed context; a \(\mathsf {stat}\) boundary embeds statically typed code in an untyped context.9 The specification includes the names of the interacting components along with a type to describe values that are intended to cross the boundary. Names such as \(\ell _0\) come from some countable set \(\ell\) (i.e., \(\ell _0 \in \ell\)). The boundary types guide the static type checker, but are mere suggestions unless a semantics decides to enforce them: \(\[\begin{array}{l@{\qquad}l} e_{s} = \ldots \mid \mathsf {dyn}^{}\,b\, e_{d},& b = (\ell {\scriptscriptstyle \blacktriangleleft }\,\tau {\scriptscriptstyle \blacktriangleleft }\,\ell), \\ e_{d} = \ldots \mid \mathsf {stat}^{}\,b\, e_{s}, & \ell = \textrm {countable{} set of names.}\end{array} \end{align*}\)The typing judgments for typed and untyped expressions require a mutual dependence to handle boundary expressions. A well-typed expression may include any well-formed dynamically typed code. Conversely, a well-formed untyped expression may include any typed expression that matches the specified annotation:

The purpose of the names is to support blame assignment when an typed–untyped interaction goes wrong. Suppose a program halts due to a mismatch between a type \(\mathsf {Nat}\) and a value \({-2}\). If the semantics has knowledge of both the client and sender of the bad value, then an error report can include this boundary where \(\mathsf {Nat}\) is required and \({-2}\) arrived.
4.2 Semantic Framework
The first ingredient a reduction semantics must supply is the set of result values \(v\) to which expressions may reduce. Our result sets extend the sets of core values introduced in the preceding subsection (\(v\supseteq v_{s}\cup v_{d}\)). Potential reasons for extending the value set include the following:
(1) | to associate a value with a delayed type-check; | ||||
(2) | to record the boundaries that a value has previously crossed; | ||||
(3) | to permit untyped values in typed code, and vice versa; and | ||||
(4) | to track the identity of values on a heap. | ||||
Reasons 1 and 2 call for two kinds of wrapper value.10 A guard wrapper (\(\mathbb{G}^{}\,b v\)) associates a boundary specification with a value to achieve delayed type checks. Guards are similar to boundary expressions; they separate a context component from a value component. A trace wrapper (\(\mathbb{T}_{}\,\bar {b}\,v\)) attaches a list of boundaries to a value as metadata. Trace wrappers simply annotate values.
The second ingredient is a set of notions of reduction, most importantly those for boundary expressions. For example, the Natural semantics (Section 6.5) fully enforces types via the classic wrapper techniques [25, 48], which is expressed as follows where a filled triangle (\(\blacktriangleright\)) describes a step in untyped code and an open triangle (\(\vartriangleright\)) describes a step in typed code: \(\begin{align*} \mathsf {stat}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,\mathsf {Nat} {\scriptscriptstyle \blacktriangleleft }\,\ell_1)\, 42 & \blacktriangleright_N \ 42,\qquad \tag{a}\\ \mathsf {dyn}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,(\mathsf {Int}\!\Rightarrow \!\mathsf {Nat}) {\scriptscriptstyle \blacktriangleleft }\,\ell _1) (\lambda x_0.\, {-8}) & \vartriangleright_N \ \mathbb{G}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,(\mathsf {Int}\!\Rightarrow \!\mathsf {Nat}) {\scriptscriptstyle \blacktriangleleft }\,\ell _1) (\lambda x_0.\, {-8}).\qquad \tag{b} \end{align*}\) According to the first rule, a typed number may enter an untyped context without further ado. According to the second rule, typed code may access an untyped function only through a newly created guard wrapper. Guard wrappers are a higher-order tool for enforcing types for first-class functions. As such, wrappers require elimination rules. To complete its type-enforcement strategy, the Natural semantics includes the following rule to unfold the application of a guarded function into two boundaries: \(\begin{align*} & \mathsf {app} {(\mathbb{G}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,(\mathsf {Int}\!\Rightarrow \!\mathsf {Nat}) {\scriptscriptstyle \blacktriangleleft }\,\ell _1) (\lambda x_0.\, {-8}))}\, {1} \vartriangleright_N \ \\ & \qquad \mathsf {dyn}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,\mathsf {Nat} {\scriptscriptstyle \blacktriangleleft }\,\ell _1) (\mathsf {app} {(\lambda x_0.\, {-8})} {(\mathsf {stat}^{}\,(\ell _1 {\scriptscriptstyle \blacktriangleleft }\,\mathsf {Int} {\scriptscriptstyle \blacktriangleleft }\,\ell _0)\, 1)}).\tag{c} \end{align*}\) Other semantics have different behavior at boundaries and different supporting rules. The Transient semantics (Section 6.8) takes a first-order approach to boundaries. Instead of using wrappers, it checks shapes at a boundary and guards elimination forms with shape-check expressions. For example, the following simplified reduction demonstrates a successful shape check: \(\begin{align*} \mathsf {check}{\lbrace (\mathsf {Nat}\!\times \!\mathsf {Nat})\rbrace }\,{\langle {-1},{-2} \rangle }\,{} & ▸_T \ {\langle {-1},{-2} \rangle .} \tag{d} \end{align*}\) The triangle is filled gray (\(▸\)), because Transient is defined via a single notion of reduction that handles both typed and untyped code.
These two points, values and checking rules, are the distinctive aspects of a semantics. Other ingredients can be shared, such as the errors, evaluation contexts, and interpretation of primitive operations. Indeed, Section 6.2 defines three baseline evaluation languages—higher-order, first-order, and erasure—that abstract over the common ingredients.
4.3 Type Soundness
Type soundness asks whether evaluation is well-defined and whether a surface-language type predicts properties of the result. Since there are two kinds of surface expressions, soundness has two parts: one for statically typed code and one for dynamically typed code.
For typed code, the question is the extent to which surface types predict the result of an evaluation. There are a range of possible answers. Suppose that an expression with surface type \(\tau _0\) reduces to a value. At one end, the result value may match the full type \(\tau _0\) according to an evaluation-language typing judgment. The other extreme is that the result is merely a well-formed value, with no stronger prediction about its shape. Even in this weak extreme, however, the language guarantees that typed reductions cannot reach an undefined state.
For untyped code, there is one surface type. Soundness guarantees that evaluation cannot reach an undefined state, but it cannot predict the shape of result values.
Both parts combine into the following definition, where the function \(F\) and judgment \(\vdash _{F}\) are parameters. The function \(F\) maps surface types to observations that one can make about a result; varying the choice of \(F\) offers a spectrum of soundness for typed code. For example, for Natural, \(F\) is the identify function and for Transient, it is a function that ignores all but the top-level constructor of a type. The judgment \(\vdash _{F}\) matches a value with a description.
Definition Sketch (\(F{}\)-type Soundness).
4.4 Complete Monitoring
The complete monitoring property holds if a language has complete control over every type-induced channel of communication between two components in a world that mixes typed and untyped code. Consider an identity function that flows from an untyped component \(\ell _0\) to a typed one \(\ell _1\), through an \((\mathsf {Int}\!\Rightarrow \!\mathsf {Int})\) type annotation. Now imagine that this function flows into untyped component \(\ell _2\), which applies this function to itself. This application opens a channel of communication between \(\ell _0\) and \(\ell _2\) at run time. This channel is type-induced, because the identity function migrated to this point through a type boundary. If the language satisfies complete monitoring, then it rejects this application, because the argument is a function and not an integer; an error report could point back to the boundary between \(\ell _0\) and \(\ell _1\), which imposed the obligation that arguments must be of type \(\mathsf {Int}\).
At first glance, this example seems to inject sophistication where none is needed. In particular, applying the identity function to itself does no harm. But, as Section 2.3 explains with a distilled real-world example, such mis-applications can be the result of type specifications for untyped code that are simply wrong. Thus, while the type checker may bless the typed code, its interactions with untyped code may reveal the mismatch between the obligation that a type imposes and the computations that the code performs.
Our approach to validating complete monitoring uses the well-known subject-reduction technique for a semantics modified to track obligations imposed by type boundaries. Tracking these obligations relies on tracking boundary crossings via component labels, dubbed ownership labels by Dimoulas et al. [22]. A sequence of labels on a value reflects the path that the value has taken through components and, by implication, which type obligations the value has incurred. These labels enrich the semantics with information without changing it. A meta-type system describes desired properties of the evaluation in terms of the labels, and subject reduction establishes that the properties hold.
Labels track information as follows. At the start of an evaluation, no interactions have occurred yet and every expression has exactly one label that names the component in which it resides. When a boundary term reduces, an interaction happens and the labels in the result term change as follows:
• | If the sender component supplies a value whose adherence to a client’s type specification can be fully checked, then the value loses its old labels and comes under full control of the client. | ||||||||||||||||
• | If the check has to be partial, because the value is higher-order, there are two possible outcomes depending on how the value crosses the boundary:
In short, the ownership labels on a value denotes the parties responsible for the behavior of the value. Storing these labels as a sequence keeps track of the order in which they gained responsibility for the value. | ||||||||||||||||
A semantics that prevents joint-responsibility situations satisfies the goal of complete monitoring; it controls every typed–untyped interaction. When a language is in control, it can present useful error messages as demonstrated in Section 2.3.1. When a language is not in control, misleading errors can arise due to issues at type boundaries as the example in Section 2.3.2 illustrates.
An ownership label \({}^{\ell _0}\) names one source-code component. Expressions and values come with at least one ownership label; for example, \({(42)}^{\ell _0}\) is an integer with one owner \({}^{\ell _0}\) and \({({({(42)}^{\ell _0})}^{\ell _1})}^{\ell _2}\)—short-hand: \({(\!(42)\!)}^{\ell _0 \ell _1 \ell _2}\)—is an integer with three owners.
A complete monitoring theorem requires two ingredients that manage these labels. First, a reduction relation \(\mathrel {\rightarrow _{_{\!{\bf r}}}^{\hspace{-1.00006pt}*}}\) must propagate ownership labels to reflect interactions and checks. Second, a single-ownership judgment \(\Vdash\) must test whether every value in an expression has a unique owner relative to a map \(\mathcal {L}_0\) from variables to their binding component. To satisfy complete monitoring, reduction must preserve single-ownership.
The key single-ownership rules deal with labeled expressions and boundary terms:

Values such as \({(\!(42)\!)}^{\ell _0 \ell _1}\) represent a communication that slipped through the run-time checking protocol and therefore fail to satisfy single ownership.
The definition of complete monitoring states that a labeled reduction relation must preserve the single-ownership invariant.
Definition Sketch (Complete Monitoring).
For all \(\cdot ; \ell _0 \Vdash e_0\), any reduction \(e_0 \mathrel {\rightarrow _{_{\!{\bf r}}}^{\hspace{-1.00006pt}*}}e_1\) implies \(\cdot ; \ell _0 \Vdash e_1\).
4.4.1 How to Uniformly Equip a Reduction Relation with Labels.
In practice, a language comes with an unlabeled reduction system, and it is up to a researcher to design a lifted relation that propagates labels without changing the underlying relations. Lifting thus requires insight. If labels do not transfer correctly, then a complete monitoring theorem loses (some of) its meaning. Similarly, if the behavior of a lifted relation depends on labels, then a theorem about it does not apply to the original, un-lifted reduction system.
Section 6 present six reduction relations as the semantics of our single mixed-typed syntax. Each relation needs a lifted version to support an attempt at a complete monitoring theorem. Normally, the design of any lifted reduction relation is a challenge in itself [22, 23, 51, 76]. Labels must reflect the communications that arise at run-time, and the possible communications depend on the unlabeled semantics. The six lifted relations for this article, however, follow a common pattern. Section 6 therefore presents one lifted relation as an example (Section 6.5) and defers to the supplementary material for the others.
To give readers an intuition for how each lifted relation comes about, this section presents informal guidelines for managing labels in a path-based way. Each guideline describes one way that labels may be transferred or dropped during evaluation and comes with an illustrative reduction.
Because labels are an analytical tool that (in principle) apply to any reduction relation, the examples are posed in terms of a hypothetical reduction relation \(\mathrel { {{\bf r}} }\) over the surface language. To read an example, assume the unlabeled notion of reduction \(e\!\mathrel { {{\bf r}} }\!e\) is given and focus on how the labels (superscripts) change in response. Recall that \(\mathsf {stat}\) and \(\mathsf {dyn}\) are boundary terms; they link two different components, a client context and an enclosed sender expression, via a type.
Although guideline G4 refers specifically to functions, the concept generalizes to reference cells and to other values that accept inputs.
To demonstrate how these guidelines influence a lifted reduction relation, the following rules lift the examples from Section 4.2. Each rule accepts input with any sequence of labels (\(\bar {\ell }\)), pattern-matches the important labels, and shuffles labels in accordance with the guidelines. The first rule (a\(^{\prime }\)) demonstrates a base-type boundary (G1). The second (b\(^{\prime }\)) demonstrates a higher-order boundary (G2); the new guard on the right-hand side implicitly inherits the context label. The third rule (c\(^{\prime }\)) sends an input (G4) and creates new application and boundary expressions. The fourth rule (d\(^{\prime }\)) applies G3 for an output:

4.5 Blame Soundness, Blame Completeness
Blame soundness and blame completeness ask whether a semantics can identify the responsible parties in the event of a run-time mismatch. A type mismatch occurs when a typed context receives an unexpected value. The value may be the result of a boundary expression or an elimination form, and the underlying issue may lie with either the value, the current type expectation, or some prior communication. To begin debugging, a programmer should know which boundaries the value traversed; after all, it is these boundaries that imposed the violated obligations. A semantics may offer information by blaming a set of boundaries. Then the question is whether those boundaries have any connection to the value at hand.
Suppose that a reduction halts on the value \(v_0\) and blames the set \(b^{*}_0\) of boundaries. Ownership labels let us check whether the set \(b^{*}_0\) has anything to do with the boundaries that the lifted semantics recorded, that is, the sequence of labels attached to the \(v_0\) value. Relative to this source-of-truth, blame soundness asks whether the names in \(b^{*}_0\) are a subset of the labels. Blame completeness asks for a superset of the labels.
A semantics can trivially satisfy blame soundness by reporting an empty set of boundaries. Conversely, the trivial way to achieve blame completeness is to blame every boundary for every possible mismatch. The technical challenge is to either satisfy both or find a middle ground.
Definition Sketch (Blame Soundness).
For all reductions that end in a mismatch for value \(v_0\) blaming boundaries \(b^{*}_0\), the names in \(b^{*}_0\) are a subset of the labels on \(v_0\).
Definition Sketch (Blame Completeness).
For all reductions that end in a mismatch for value \(v_0\) blaming boundaries \(b^{*}_0\), the names in \(b^{*}_0\) are a superset of the labels on \(v_0\).
4.6 Error Preorder
Whereas the above properties characterize semantics independently of one another, the error preorder relation sets up a direct comparison. One semantics is below another in this preorder, written \(X\lesssim Y\), if it raises errors on at least as many well-formed programs. Put another way, \(X{} \lesssim Y{}\) holds when \(X\) is less permissive than \(Y\) is. When two semantics agree about which expressions raise run-time errors, we use the notation \(X{} \eqsim Y{}\).
Definition Sketch (Error Preorder \(\lesssim\)).
\(X\lesssim Y\) iff \(e_0 {\rightarrow}^*_{\rm Y}\ \textsf {Err}\) implies \(e_0 {\rightarrow}^*_{\rm X}\ \textsf {Err}\).
Definition Sketch (Error Equivalence \(\eqsim\)).
\(X\eqsim Y\) iff \(X\lesssim Y\) and \(Y\lesssim X\).
The six semantics in this article are especially close to one another. Although they use different methods for enforcing types, they agree on other behaviors. In particular, these semantics diverge on the same expressions and compute equivalent values ignoring wrappers. This close correspondence lets us view the error preorder in another way: \(X{} \lesssim Y{}\) holds for these semantics if and only if \(Y\) reduces at least as many expressions to a result value (\(\lbrace e_0 \mid \exists \,v_0. e_0 {\rightarrow}^*_{\rm X}\ v_0\rbrace \subseteq \lbrace e_1 \mid \exists \,v_1. e_1 {\rightarrow}^*_{\rm Y}\ v_1\rbrace\)). The supplementary material presents bisimulations that establish the correspondences.
5 TYPE-ENFORCEMENT STRATEGIES
The six chosen type-enforcement strategies share some commonalities and exhibit significant differences in philosophy and technicalities. This section supplies the ideas behind each strategy and serves as a quick, informal reference. Readers who prefer formal definitions may wish to skip to Section 6.
The overview begins with the strategy that is lowest on the error preorder and ascends to the most lenient strategy:
| Natural | : | Wrap higher-order values; eagerly check first-order values. |
| Co-Natural | : | Wrap higher-order and first-order values. |
| Forgetful | : | Wrap higher-order and first-order values, but drop inner wrappers. |
| Transient | : | Never use wrappers; check the shape of all values that appear in typed code. |
| Amnesic | : | Check shapes like Transient; use wrappers only to remember boundary types. |
| Erasure | : | Never use wrappers; check nothing. Do not enforce static types at run-time. |
Three of these strategies have been implemented in full-fledged languages: Natural, Transient, and Erasure. Two, Co-Natural and Forgetful, originate in prior work [31, 34] and, sitting between the Natural and Transient strategies, highlight the variety of designs. Finally, Amnesic is a synthetic semantics, created to demonstrate how the analysis framework can be used to address problems, specifically the impoverished nature of blame assignment in Transient.
5.1 Natural
Natural strictly enforces the boundaries between typed and untyped code. Every time a typed context imports an untyped value, the value undergoes a comprehensive check. For first-order values, this implies a deep traversal of the incoming value. For higher-order values, a full check at the time of crossing the boundary means creating a wrapper to monitor its future behavior.
Figure 6 describes in more detail the checks that happen when a value reaches a boundary. The descriptions omit component names and blame to keep the focus on types. These checks either validate an untyped value entering typed code (left column) or protect a typed value before it enters untyped code (right column).
Fig. 6. Natural boundary checks (omitting blame).
5.1.1 Theoretical Costs, Motivation for Alternative Methods.
Implementations of Natural have struggled with the performance overhead of enforcing types [25, 38]. A glance at the sketch above suggests three sources for this overhead: checking that a value matches a type, the layer of indirection that a wrapper adds, and the allocation cost.
For base types and higher-order types, the cost of checking is presumably low. Testing whether a value is an integer or a function is a cheap operation in languages that support dynamic typing. Pairs and other first-order values, however, illustrate the potential for serious overhead. When a deeply nested pair value reaches a boundary, Natural follows the type to conduct an eager and comprehensive check whose cost is linear in the size of the type. To check recursive types such as lists, the cost is linear in the size of the incoming value.
The indirection cost grows in proportion to the number of wrappers on a value. There is no limit to the number of wrappers in Natural, so this cost can grow without bound. Indeed, the combined cost of checking and indirection can lead to exponential slowdown even in simple programs [24, 31, 41, 44, 74].
Last, creating a wrapper initializes a data structure. Creating an unbounded number of wrappers incurs a proportional cost, which may add up to a significant fraction of a program’s running time.
Researchers have addressed these costs to some extent with implementation techniques that lower the time and space bounds for Natural [6, 14, 24, 31, 41, 44, 63, 66] without changing its behavior. The next three type-enforcement strategies can, however, offer more drastic improvements. First, the Co-Natural strategy (Section 5.2) reduces the up-front cost of checks by creating wrappers for pairs. Second, the Forgetful strategy (Section 5.3) reduces indirection by keeping at most two wrappers on any value and discarding the rest. Third, the Transient strategy (Section 5.4) removes wrappers altogether by enforcing a weaker type soundness invariant.
5.1.2 Origins of the Natural strategy.
The name “Natural” is due to Matthews and Findler [48], who use it to describe a proxy method for transporting untyped functions into a typed context. Prior works on higher-order contracts [25], remote procedure calls [55], and typed foreign function interfaces [56] employ a similar type-directed proxy method. In the gradual typing literature, Natural is also called “guarded” [84], “behavioral” [18], and “deep” [82]. This strategy has an interesting justification via work on AGT [28]; namely, its checks ensure that a proof of type preservation is still possible given the untyped values that have arisen at runtime.
5.2 Co-Natural
The Co-Natural strategy checks only the shape of values at a boundary. Instead of eagerly validating the contents of a data structure, Co-Natural creates a wrapper to perform validation by need. The cost of checking at a boundary is thereby reduced to the worst-case cost of a shape check. Allocation and indirection costs may increase, however, because even first-order values are wrapped in monitors. Figure 7 outlines the strategy.
Fig. 7. Co-Natural boundary checks.
5.2.1 Origins of the Co-Natural strategy.
The Co-Natural strategy introduces a small amount of laziness. By contrast to Natural, which eagerly validates immutable data structures, Co-Natural waits until the data structure is accessed to perform a check. The choice is analogous to the question of initial algebra versus final algebra semantics for such datatypes [7, 12, 89], hence the prefix “Co” is a reminder that some checks now happen at an opposite time. Findler et al. [27] implement exactly the Co-Natural strategy for Racket struct contracts. Other researchers have explored variations on lazy contracts [17, 20, 21, 42]; for instance, by delaying even shape checks until a computation depends on the value.
5.3 Forgetful
The goal of Forgetful is to guarantee type soundness and to limit the number of wrappers around a value. A non-goal is to enforce types in any way that is not strictly required by soundness. Consequently, types in Forgetful are not compositionally valid claims about code. Typed code can rely on the static types that it declares, nothing more. Untyped code cannot trust type annotations, because those types may be forgotten without ever getting checked.
The Forgetful strategy is to keep at most two wrappers around a value. An untyped value gets one wrapper when it enters a typed context and loses this wrapper upon exit. A typed value gets a “sticky” inner wrapper the first time it exits typed code and gains a “temporary” outer wrapper whenever it re-enters a typed context. The sticky wrapper protects the function from bad inputs. The temporary outer wrappers protect callers. Figure 8 presents an outline of the strategy.
Fig. 8. Forgetful boundary checks.
5.3.1 Comparison to Natural.
Figure 9 present two examples to demonstrate how Forgetful manages guard wrappers as compared to the Natural semantics.11 Each example term sends an identity function across three boundaries. To keep the illustration concise, let \(\textsf {A}\), \(\textsf {B}\), and \(\textsf {C}\) be three types such that the example terms are well-typed. The three boundaries at hand use the function types \(\textsf {A}\!\Rightarrow \!\textsf {A}\), \(\textsf {B}\!\Rightarrow \!\textsf {B}\), and \(\textsf {C}\!\Rightarrow \!\textsf {C}\).
Fig. 9. Natural vs. Forgetful.
These examples are formatted in a tabular layout. Each row of the table corresponds to a type-enforcement strategy. From left to right, the cells in a row show how a value accumulates guard wrappers. Each column states whether the current redex is untyped or typed. Untyped columns have a shaded background. Typed columns come with an expected type. Similarly, the arrows between the columns are open (\(\vartriangleright\)) when the value passes through a \(\textsf {dyn}\) boundary and filled (\(\blacktriangleright\)) when the value passes through a \(\textsf {stat}\) boundary. The top of each figure presents a full example term that can be reduced using the semantics in Section 6.
Example: Untyped Identity Function. Figure 9 (top) shows how Natural and Forgetful add wrappers to an untyped function that crosses three boundaries. Natural creates one wrapper for each boundary. Forgetful creates a temporary wrapper whenever the function enters a typed context and removes this wrapper when the function exits.
Example: Typed Identity Function. Figure 9 (bottom) shows how Natural and Forgetful add wrappers to a typed function that crosses three boundaries. Natural creates one guard wrapper for each boundary. Forgetful creates an initial “sticky” guard wrapper when a typed function first exits typed code. This wrapper enforces the function’s domain type. When the function re-enters typed code, Forgetful adds a wrapper to record its new type. When it exits typed code, this outer wrapper gets forgotten.
5.3.2 Origins of the Forgetful strategy.
Greenberg [30, 31] introduces forgetful manifest contracts, proves their type soundness, and observes that unlike normal types, forgetful types cannot support abstraction and information hiding. Castagna and Lanvin [15] present a gradual language with union and intersection types that has a forgetful semantics to keep the formalism simple without affecting type soundness.
There are other strategies that limit the number of wrappers on a value without sacrificing type guarantees [31, 41, 63]. These methods require an implementation of wrappers that can be merged with one another, whereas Forgetful can treat wrappers as black boxes.
5.4 Transient
The Transient strategy aims to prevent typed code from “going wrong” [49] in the sense of applying a primitive operation to a value outside its domain. For example, every application \((e_0\, e_1)\) in Transient-typed code can trust that the value of \(e_0\) is a function.
Transient meets this goal without wrappers and without traversing data structures by rewriting typed code ahead-of-time in a conservative fashion. Every type boundary, every typed elimination form, and every typed function body gets rewritten to execute a shape check. These shape checks match the top-level constructor of a value against the top-level constructor of a type. By applying shape checks wherever an ill-typed value might sneak in, Transient protects typed code against undefined primitive operations.
Figure 10 describes the checks that happen at a boundary in the Transient semantics. Unlike the other semantics, however, these boundary checks are only part of the story. Additional \(\textsf {dyn}\)-style checks appear within typed code because of the rewriting pass.
Fig. 10. Transient boundary checks.
In general, Transient checks add up to a greater number of run-time validation points than those that arise in a wrapper-based semantics, because every expression in typed code may require a check. The net cost of these checks, however, may be lower and easier to predict than in higher-order strategies, because each check has a low cost [29, 37, 62, 86]. Often a tag check suffices, although unions and other expressive types require a deeper check [36]. Static analysis can further reduce costs by identifying overly conservative checks [85], and JIT compilers have been effective at reducing the costs of Transient [29, 46, 62, 85].
5.4.1 Origins of the Transient strategy.
Vitousek [83] invented Transient for Reticulated Python. The name suggests the nature of its run-time checks: Transient type-enforcement enforces local assumptions in typed code but has no long-lasting ability to influence untyped behaviors [84]. Transient has been adapted to Typed Racket [34, 36] and has inspired closely related approaches in Grace [29, 62] and in Static Python [46].
5.5 Amnesic
The goal of the Amnesic semantics is to specify basically the same behavior as Transient but improve the error messages when a type mismatch occurs. Amnesic demonstrates that wrappers offer more than a way to detect errors; they seem essential for informative errors.
The Amnesic strategy wraps values, discards all but three wrappers, and keeps a record of discarded boundary specifications. To record boundaries, Amnesic uses trace wrappers. When a type mismatch occurs, Amnesic presents the recorded boundaries to the programmer.
If an untyped function enters a typed component, then Amnesic wraps the function in a guard. If the function travels back to untyped code, then Amnesic replaces the guard with a trace wrapper that records two boundaries. Future round-trips extend the trace. Conversely, a typed function that flows to untyped code and back \(N{+}1\) times gets three wrappers: an outer guard to protect its current typed client, a middle trace to record its last \(N\) trips, and an inner guard to protect its body. Figure 11 outlines the strategy.
Fig. 11. Amnesic boundary checks.
5.5.1 Comparison to Forgetful and Transient.
The design of Amnesic is best understood as a variation of Transient that accepts a limited number of wrappers per value. Like the Forgetful semantics, it puts at most two guard wrappers around a value. It also uses at most one trace wrapper to remember all boundaries that the value has crossed.
The following two examples compare Forgetful, Transient, and Amnesic side-by-side using the same example terms as in Figure 9. As before, let \(\textsf {A}\!\Rightarrow \!\textsf {A}\), \(\textsf {B}\!\Rightarrow \!\textsf {B}\), and \(\textsf {C}\!\Rightarrow \!\textsf {C}\) be three function types such that the example terms are well-typed.
Example: Untyped Identity Function. Figure 12 (top) shows how Forgetful, Transient, and Amnesic manage an untyped function that crosses three boundaries. Forgetful creates a wrapper when the function enters typed code and removes a wrapper when it leaves. Transient lets the function cross boundaries without creating wrappers. Amnesic creates the same guard wrappers as Forgetful and also uses a trace wrapper to record the obligations from forgotten guards.
Fig. 12. Forgetful vs. Transient vs. Amnesic.
Example: Typed Identity Function. Figure 12 (bottom) shows how Forgetful, Transient, and Amnesic manage a typed function that crosses three boundaries. Both Forgetful and Amnesic create a sticky wrapper when the function leaves typed code. When the function re-enters typed code, they add a second guard wrapper that gets removed on the next exit. Amnesic additionally uses a trace wrapper to collect all boundaries that the function has crossed. Transient does not create wrappers.
5.5.2 Theoretical Costs.
Amnesic is a theoretical design that may not be realizable in practice. In particular, an implementation must find an efficient representation of trace wrappers. Trace wrappers track every boundary that a value has crossed. Consequently, they have a space-efficiency problem similar to the unbounded number of guard wrappers in the Natural and Co-Natural semantics. One simple fix is to settle for worse blame by putting an upper bound on the number of boundaries that a trace wrapper can hold. Another option is to invent a compression scheme that exploits redundancies among boundaries to reduce the space needs of a large set.
5.5.3 Origins of the Amnesic strategy.
Amnesic is a synthesis of Forgetful and Transient that demonstrates how our framework can guide the design of new checking strategies [35]. The name suggests a connection to forgetful and the Greek origin of the second author.
5.6 Erasure
The Erasure strategy is based on a view of types as an optional syntactic artifact. Type annotations are a structured form of comment that help developers and tools read a codebase. At run-time, types check nothing (Figure 13). Any value may flow into any context.
Fig. 13. Erasure boundary checks.
Despite the complete lack of type enforcement, the Erasure strategy is widely used (Figure 1) and has a number of pragmatic benefits. The static type checker can point out logical errors in type-annotated code. An IDE may use the static types in auto-completion and in refactoring tools. An implementation does not require any instrumentation to enforce types. Users that are familiar with the host language do not need to learn a new semantics to understand the behavior of type-annotated programs. Finally, Erasure programs run as fast as a host-language program.
5.6.1 Origins of the Erasure strategy.
Erasure is also known as optional typing and dates back to the type hints of MACLISP [50] and Common Lisp [71]. StrongTalk is another early and influential optionally typed language [11]. Models of optional typing exist for JavaScript [8, 16], Lua [47], and Clojure [10].
6 TECHNICAL DEVELOPMENT
The technical analysis consists of three major pieces: the precise surface syntax (Section 6.1); the six reduction semantics, each equipped with a typed evaluation syntax (Section 6.2); and a set of theorems concerning the properties that each semantics satisfies. Figure 14 displays a diagram that outlines the presentation. As the diagram indicates, four of the semantics share a common evaluation syntax; the intrinsically first-order transient semantics is separate from those.
Fig. 14. Map of definitions in Section 6.
Several properties depend on lifted semantics that propagate ownership labels in accordance with the guidelines from Section 4.4.1. Meaning, the map in Figure 14 is only half of the formal development. Each syntax and semantics comes with a parallel, lifted version. Since the differences are in small details, the section presents only one lifting in full. The others appear in the supplement.
6.1 Surface Syntax, Types, and Ownership
Figure 15 presents the syntax and typing judgments for the surface language. Expressions \(e\) include variables, integers, pairs, functions, primitive operations, applications, and boundary expressions. The primitive operations are pair projections and arithmetic functions; these model interactions with a runtime system. A boundary expression either embeds a dynamically typed expression in a statically typed context (\(\textsf {dyn}\)) or a typed expression in an untyped context (\(\textsf {stat}\)).
Fig. 15. Surface syntax and typing rules.
A type specification \(\tau/{\mathcal {U}}\) is either a static type \(\tau\) or the symbol \({\mathcal {U}}\) for untyped code. Fine-grained mixtures of \(\tau\) and \({\mathcal {U}}\), such as \(\textsf {Int} \!\times \! {\mathcal {U}}\), are not grammatical; the model describes two parallel syntaxes that are connected through boundary expressions (Section 4.1). A statically typed expression \(e_0\) is one for which the judgment \(\Gamma _0 \vdash e_0 : \tau _0\) holds for some type environment and type. This judgment depends on a standard notion of subtyping (\(\mathrel {\leqslant {:}}\)) that is based on the relation \(\textsf {Nat}\mathrel {\leqslant {:}}\textsf {Int}\), covariant for pairs and function codomains, and contravariant for function domains. The metafunction \(\Delta\) determines the output type of a primitive operation. For example, the sum of two natural numbers is a natural (\(\Delta (\textsf {sum}, \textsf {Nat}, \textsf {Nat}) = \textsf {Nat}\)) but the sum of two integers returns an integer. A dynamically typed expression \(e_1\) is one for which \(\Gamma _1 \vdash e_1 : {\mathcal {U}}\) holds for some environment \(\Gamma _1\).
Every function application and operator application comes with a type specification \(\tau/{\mathcal {U}}\) for the expected result. These annotations serve two purposes: to determine the behavior of the Transient and Amnesic semantics, and to disambiguate statically typed and dynamically typed redexes. An implementation could reconstruct valid annotations from the term and its context. The model keeps them explicit to easily formulate examples where subtyping affects behavior; for instance, the terms \({unop} {\lbrace \textsf{Nat}\rbrace }\,e_0\) and \({unop}{\lbrace \textsf {Int}\rbrace }\,e_0\) may give different results for the same input expression.
Figure 16 augments the surface syntax with ownership labels and introduces a single-owner ownership consistency relation. These labels record the component from which an expression originates. The augmented syntax brings one addition, labeled expressions \({(e)}^{\ell }\), and a requirement that boundary expressions label their inner component. The single-owner consistency judgment (\(\mathcal {L}; \ell \Vdash e\)) ensures that every subterm of an expression has a unique owner. This judgment is parameterized by a mapping from variables to labels (\(\mathcal {L}\)) and a context label (\(\ell\)). Every variable reference must occur in a context that matches the map entry for that variable; every labeled expression must match the context; and every boundary expressions must have a client name that matches the context label. For example, the expression \({(\textsf {dyn}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,\textsf {Nat} {\scriptscriptstyle \blacktriangleleft }\,\ell _1) {(x_0)}^{\ell _1})}^{\ell _0}\) is consistent under a mapping that contains \((x_0:\ell _1)\) and the \(\ell _0\) context label. The expression \({({(42)}^{\ell _0})}^{\ell _1}\), also written \({(\!(42)\!)}^{\ell _0 \ell _1}\) (Figure 18), is inconsistent for any parameters.
Fig. 16. Ownership syntax and single-owner consistency.
Labels correspond one-to-one to component names but come from a distinct set. Thus, the expression \((\textsf {dyn}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,\textsf {Nat} {\scriptscriptstyle \blacktriangleleft }\,\ell _1) {(x_0)}^{\ell _1})\) contains two names (\(\ell _0\) and \(\ell _1\)) and one label (\({}^{\ell _1}\)). The label matches the inner component name, which means that the inner component is responsible for the variable inside the boundary. The reason for using two distinct sets is to keep our analysis framework separate from the semantics that it analyzes. Whereas a semantics can freely inspect and manipulate component names (which would be realized as symbols or addresses in an implementation), it cannot use labels to determine its behavior (labels would not be part of an implementation).
Last, a surface expression is well-formed (\(e : \tau/{\mathcal {U}} \ \mathbf {wf}\)) if it satisfies a typing judgment—either static or dynamic—and single-owner consistency under some labeling and context label. The theorems below all require well-formed expressions (though some ignore the ownership labels).
6.2 Three Evaluation Syntaxes
Each semantics requires a unique evaluation syntax, but overlaps among these six languages motivate three common platforms. A higher-order evaluation syntax supports type-enforcement strategies that require wrappers. A first-order syntax, with simple checks rather than wrappers, supports Transient. And an erased syntax supports the compilation of typed and untyped code to a common untyped host.
Figure 17 defines common aspects of the evaluation syntax. These include errors \(\textsf {Err}\), shapes (or, constructors) \(s\), evaluation contexts, and evaluation metafunctions.
Fig. 17. Common evaluation syntax and metafunctions.
The evaluation syntax extends the surface syntax in a technical sense; namely, the grammar presented in Figure 17 would be complete if it included a copy of the grammar from Figure 15. Every occurrence of the word “extends” in a figure has a similar meaning. For example, the typing judgments in Figure 19 would be complete if the judgment rules from Figure 15 were copied in.
A program evaluation may signal four kinds of errors.
The four shapes, \(s\), correspond both to type constructors and to value constructors. Half of the correpondence is defined by the \(\lfloor \cdot \rfloor\) metafunction, which maps a type to a shape. The \({shape\hbox{-}match}\) metafunction is the other half; it checks the top-level shape of a value.
Both metafunctions use an \(\,\cdot \in \cdot \,\) judgment, which holds if a value is a member of a set. The claim \(v_0 \in n\), for example, holds when the value \(v_0\) is a member of the set of natural numbers. By convention, a variable without a subscript refers to a set and a term containing a set describes a comprehension. The term \((\lambda (x:\tau).\, v)\), for instance, describes the set \(\lbrace (\lambda (x_i:\tau _j).\, v_k) \mid x_i \in x\wedge \tau _j \in \tau \wedge v_k \in v\rbrace\) of all typed functions that return a value (rather than an expression).
The \({shape\hbox{-}match}\) metafunction also makes reference to two value constructors unique to the higher-order evaluation syntax: guard \((\mathbb{G}^{}\,b v)\) and trace \((\mathbb{T}_{}\,b^{*}\,v)\) wrappers. A guard has a shape determined by the type in its boundary. A trace is metadata, so \({shape\hbox{-}match}\) looks past it. Section 4.2 informally justifies the design. Figure 19 formally introduces these wrapper values.
The final components of Figure 17 are the \(\delta\) metafunctions. These provide a standard and partial specification of the primitive operations.
Figure 18 defines additional metafunctions for boundaries and ownership labels. For boundaries, \({rev}\) flips every client and sender name in a set of specifications. Both Transient and Amnesic reverse boundaries at function calls. The \({senders}\) metafunction extracts the sender names from the right-hand side of every boundary specification in a set. For labels, \({rev}\) reverses a sequence. The \({owners}\) metafunction collects the labels around an unlabeled value stripped of any trace-wrapper metadata. Guard wrappers are not stripped, because they represent boundaries. Last, the abbreviation \({(\!(\cdot)\!)}^{\cdot }\) captures a list of boundaries. The term \({(\!(4)\!)}^{\ell _0 \ell _1}\) is short for \({({(4)}^{\ell _0})}^{\ell _1}\) and \({(\!(5)\!)}^{\bar {\ell }_0}\) matches 5 with \(\bar {\ell }_0\) bound to the empty list.
Fig. 18. Metafunctions for boundaries and labels.
6.2.1 Higher-order Syntax, Path-based Ownership Consistency.
The higher-order evaluation syntax (Figure 19) introduces the two wrapper values described in Section 4.2. A guard wrapper \((\mathbb{G}^{}\,(\ell {\scriptscriptstyle \blacktriangleleft }\,\tau {\scriptscriptstyle \blacktriangleleft }\,\ell) v)\) represents a boundary between two components.12 A trace wrapper \((\mathbb{T}_{}\,b^{*}\,v)\) attaches metadata to a value.
Fig. 19. Higher-order syntax, typing rules, and ownership consistency.
Type-enforcement strategies typically use guard wrappers to constrain the behavior of a value. For example, the Co-Natural semantics wraps any pair that crosses a boundary with a guard; this wrapper validates the elements of the pair upon future projections. Trace wrappers do not constrain behavior. A traced value simply comes with extra information; namely, a collection of the boundaries that the value has previously crossed.
The higher-order typing judgments, \(\Gamma \vdash _{\mathbf {1}}e: \tau/{\mathcal {U}}\), extend the surface typing judgments with rules for wrappers and errors. Guard wrappers may appear in both typed and untyped code; the rules in each case mirror those for boundary expressions. Trace wrappers may only appear in untyped code; this restriction simplifies the Amnesic semantics (Figure 28). A traced expression is well-formed iff the enclosed value is well-formed. An error term is well-typed in any context.
Figure 19 also extends the single-owner consistency judgment to handle wrapped values. For a guard wrapper, the outer client name must match the context and the enclosed value must be single-owner consistent with the inner sender name. For a trace wrapper, the inner value must be single-owner consistent relative to the context label.
6.2.2 First-order Syntax.
The first-order syntax (Figure 20) supports typed–untyped interaction without proxy wrappers. A new expression form, \((\textsf {check}{\lbrace \tau/{\mathcal {U}} \rbrace }\,{e_0}\,{\textsf {p}_0})\), represents a shape check. The intended meaning is that the given type must match the value of the enclosed expression. If not, then the location \(\textsf {p}_0\) may be the source of the fault. Locations are names for the pairs and functions in a program. These names map to pre-values in a heap (\(\mathcal {H}\)) and to sets of boundaries in a blame map (\(\mathcal {B}\)). Pairs and functions are now second-class pre-values (\(\textsf {w}\)) that must be allocated before they may be used.
Fig. 20. First-order syntax and typing rules.
Three meta-functions define heap operations: \(\cdot (\cdot)\), \(\cdot [\cdot \mapsto \cdot ]\), and \(\cdot [\cdot \mathrel {{\cup }}\cdot ]\). The first gets an item from a finite map, the second replaces a blame heap entry, and the third extends a blame heap entry. Because maps are sets, set union suffices to add new entries.
The first-order typing judgments state basic invariants. For statically typed expressions, the judgment checks the top-level shape (\(s\)) of an expression and the well-formedness of any subexpressions. This judgment depends on a subtyping judgment for shapes, which is reflexive, allows \(\textsf {Nat}\mathrel {\leqslant {:}}\textsf {Int}\), and nothing more. For dynamically typed expressions, the judgment checks well-formedness. Both judgments rely on a store typing environment (\(\mathcal {T}\)) to describe heap-allocated values. Store types must be consistent with the actual values on the heap, a standard technical device that is spelled out in the supplement.
Two aspects of the first-order typing judgments deserve special mention. First, untyped functions may appear in typed contexts and typed functions may appear in untyped contexts. This behavior is an essential aspect of the first-order language, which allows typed-untyped interoperability and does not use wrappers to enforce a separation between the two worlds. Second, shape-check expressions are allowed in typed and untyped contexts. This is a technical device. In particular, checks arise after a function call to separate the substituted body from the calling context, and this separation allows the typing judgments to switch from static mode to dynamic mode as needed.
6.2.3 Erased Syntax.
Figure 21 defines an evaluation syntax for type-erased programs. Expressions include error terms. The typing judgment holds for any expression without free variables. Aside from the type annotations left over from the surface syntax, which could be removed with a translation step, the result is a conventional dynamically typed language.
Fig. 21. Erased evaluation syntax and typing.
6.3 Properties of Interest
Type soundness guarantees that the evaluation of a well-formed expression (1) cannot end in an invariant error and (2) preserves an evaluation-language image of the surface type. Note that an invariant error captures the classic idea of an evaluation going wrong [49].
(\(F{}\)-type Soundness).
Let \(F\) map surface types to evaluation types. A semantics \(X\) satisfies \(\mathbf {TS}\, {(}F {)}\) if for all \(e_0 : \tau/{\mathcal {U}} \ \mathbf {wf}\) one of the following holds:
Three surface-to-evaluation maps (\(F\)) suffice for the evaluation languages: an identity map \(\mathbf {1}\), a type-shape map \(\mathbf {s}\) that extends the metafunction from Figure 17, and a constant map \(\mathbf {0}\):
\( \mathbf {1}(\tau/{\mathcal {U}}) =\tau/{\mathcal {U}} , \qquad\qquad\qquad\qquad \mathbf {s}(\tau/{\mathcal {U}}) =\left\lbrace \begin{array}{ll} {\mathcal {U}} & \mbox{if $\,\tau/{\mathcal {U}} ={\mathcal {U}}$} \\ \lfloor \tau _0 \rfloor & \mbox{if $\,\tau/{\mathcal {U}} =\tau _0$} \end{array},\right. \qquad\qquad\qquad\qquad {\mathbf {0}(\tau/{\mathcal {U}}) ={\mathcal {U}}}. \)
Complete monitoring guarantees that a semantics can enforce types for all interactions between components. The definition of “all interactions” comes from the propagation guidelines (Section 4.4.1). In particular, the labels on a value enumerate all partially responsible components. Relative to this specification, a reduction that preserves single-owner consistency (\({\Vdash }\), Figure 16) ensures that a value cannot enter a new component without a full type check or a wrapper.
(Complete Monitoring).
A semantics \(X\) satisfies \(\mathbf {CM}{}\) if for all \({(e_0)}^{\ell _0} : \tau/{\mathcal {U}} \ \mathbf {wf}\) and all \(e_1\) such that \(e_0 {\rightarrow}^*_{\rm X}\ e_1\), the contractum is single-owner consistent: \(\ell _0 \Vdash e_1\).
Blame soundness and blame completeness measure the quality of error messages relative to a specification of the components that handled a value during an evaluation. A blame-sound semantics reports a subset of the true senders, though it may miss some or even all. A blame-complete semantics reports all the true senders, though it may also report irrelevant extras. A sound and complete semantics reports exactly the responsible components.
The path-based definitions for blame soundness and blame completeness rely on the propagation guidelines from Section 4.4.1. Relative to these guidelines, the definitions relate the sender names in a set of boundaries (Figure 18) to the true owners of the mismatched value.
(Path-based Blame Soundness and Blame Completeness).
For all well-formed \(e_0\) such that \(e_0 {\rightarrow}^*_{\rm X}\ \textsf {BoundaryErr}\,(b^{*}_0, v_0)\):
• | \(X\) satisfies \(\mathbf {BS}\) iff \({senders}\,(b^{*}_0) \subseteq {owners}\,(v_0),\) | ||||
• | \(X\) satisfies \(\mathbf {BC}\) iff \({senders}\,(b^{*}_0) \supseteq {owners}\,(v_0)\). | ||||
Last, the error preorder relation allows direct behavioral comparisons. If \(X\) and \(Y\) represent two strategies for type enforcement, then \(X\lesssim Y\) states that the \(X\) semantics is less permissive than the \(Y\) semantics (or, as Section 4.6 notes, \(Y\) reduces at least as many expressions to a value as \(X\)).
(Error Preorder).
\(X\lesssim Y\) iff \(e_0 {\rightarrow}^*_{\rm Y}\ \textsf {Err}_0\) implies \(e_0 {\rightarrow}^*_{\rm X}\ \textsf {Err}_1\) for all well-formed expressions \(e_0\).
If two semantics lie below one another according to the error preorder, then they report type mismatches on exactly the same well-formed expressions.
6.4 Common Higher-order Notions of Reduction
Four of the semantics build on the higher-order evaluation syntax. In redexes that do not mix typed and untyped values, these semantics share the common behavior specified in Figure 22. The rules for typed code (\(\vartriangleright\)) handle elimination forms for unwrapped values and raise an invariant error (\(\textsf {InvariantErr}\)) for invalid input. Type soundness ensures that such errors do not occur. The rules for untyped code (\(\blacktriangleright\)) raise a tag error for a malformed redex. Later definitions, for example, Figure 23, combine these relations (\(\vartriangleright\), \(\blacktriangleright\)) with others to define a semantics.
Fig. 22. Common notions of reduction for Natural, Co-Natural, Forgetful, and Amnesic.
Fig. 23. Natural notions of reduction.
6.5 Natural and Its Properties
Figure 23 presents the values and key reduction rules for the Natural semantics. Conventional reductions handle primitives and unwrapped functions (\(\blacktriangleright\) and \(\vartriangleright\), Figure 22).
A successful Natural reduction yields either an unwrapped value or a guard-wrapped function. Guards arise when a function value reaches a function-type boundary. Thus, the possible wrapped values are drawn from the following two sets:
\( \begin{array}[t]{lcl} v_{s} & = & \mathbb{G}^{}\,(\ell {\scriptscriptstyle \blacktriangleleft }\,(\tau \!\Rightarrow \!\tau) {\scriptscriptstyle \blacktriangleleft }\,\ell)\, (\lambda x.\, e), \\ & \mid & \mathbb{G}^{}\,(\ell {\scriptscriptstyle \blacktriangleleft }\,(\tau \!\Rightarrow \!\tau) {\scriptscriptstyle \blacktriangleleft }\,\ell)\, v_{d}, \end{array} \qquad\qquad\qquad\qquad \begin{array}[t]{lcl} v_{d} & = & \mathbb{G}^{}\,(\ell {\scriptscriptstyle \blacktriangleleft }\,(\tau \!\Rightarrow \!\tau) {\scriptscriptstyle \blacktriangleleft }\,\ell)\, (\lambda (x:\tau).\, e), \\ & \mid & \mathbb{G}^{}\,(\ell {\scriptscriptstyle \blacktriangleleft }\,(\tau \!\Rightarrow \!\tau) {\scriptscriptstyle \blacktriangleleft }\,\ell)\, {v_{s}}. \end{array} \)
The presented reduction rules are those relevant to the Natural strategy for enforcing static types. When a dynamically typed value reaches a typed context (\(\textsf {dyn}\)), Natural checks the shape of the value against the type. If the type and value match, then Natural wraps functions and recursively checks the elements of a pair. Otherwise, Natural raises an error at the current boundary. When a wrapped function receives an argument, Natural creates two new boundaries: one to protect the input to the inner, untyped function and one to validate the result.
Reduction in dynamically typed code (\(\blacktriangleright_{\rm N}\)) follows a dual strategy. The rules for \(\textsf {stat}\) boundaries wrap functions and recursively protect the contents of pairs. The application of a wrapped function creates boundaries to validate the input to a typed function and to protect the result.
Unsurprisingly, this checking protocol ensures the validity of types in typed code and the well-formedness of expressions in untyped code. The Natural approach additionally keeps boundary types honest throughout the execution.
Proof Sketch. By progress and preservation lemmas for the higher-order typing judgment (\(\vdash _{\mathbf {1}}\)). For example, if an untyped pair reaches a boundary, then a typed step (\(\vartriangleright_N \ \)) makes progress to either a new pair or to an error. In the former case, the new pair contains two boundary expressions:
\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} \textsf {dyn}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,\tau _0\!\times \!\tau _1 {\scriptscriptstyle \blacktriangleleft }\,\ell _1)\, \langle v_0,v_1 \rangle & \vartriangleright_N \ & \langle \textsf {dyn}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,\tau _0 {\scriptscriptstyle \blacktriangleleft }\,\ell _1)\, v_0,\textsf {dyn}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,\tau _1 {\scriptscriptstyle \blacktriangleleft }\,\ell _1)\, v_1 \rangle . \end{array}\)
The typing rules for pairs and for \(\textsf {dyn}\) boundaries validate the type of the result.
A second interesting case is for the rule that applies a wrapped function in a typed context:
\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} \textsf {app}{\lbrace \tau _0\rbrace }\,(\mathbb{G}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,(\tau _1\!\Rightarrow \!\tau _2) {\scriptscriptstyle \blacktriangleleft }\,\ell _1)\, v_0)\, v_1 &\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\! \vartriangleright_N \ \\ \quad{\mbox{ $\textsf {dyn}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,\tau _2 {\scriptscriptstyle \blacktriangleleft }\,\ell _1) (\textsf {app}{\lbrace {\mathcal {U}}\rbrace }\,v_0\, (\textsf {stat}^{}\,(\ell _1 {\scriptscriptstyle \blacktriangleleft }\,\tau _1 {\scriptscriptstyle \blacktriangleleft }\,\ell _2)\, v_1)).$}} \end{array}\)
If the redex is well-typed, then \(v_1\) has type \(\tau _1\) and the inner \(\textsf {stat}\) boundary is well-typed. Similar reasoning for \(v_0\) shows that the untyped application in the result is well-typed. Thus, the \(\textsf {dyn}\) boundary has type \(\tau _2\), which, by the types on the redex, is a subtype of \(\tau _0\).\(\square\)
Figure 24 presents a labeled variant of the Natural semantics for typed code. Ignoring labels, the rules in this figure are a combination of those in Figures 22 and 23. The labels reflect communications and changes of ownership. The labeled rules for untyped code are similar and appear in the supplementary material.
Fig. 24. Natural labeled notion of reduction for typed code.
Proof Sketch. By showing that a lifted variant of the \({\rightarrow}^*_{\rm N}\) relation preserves single-owner consistency (\(\Vdash\)). Full lifted rules for Natural appear in the supplementary material, but one can derive the rules by applying the guidelines from Section 4.4.1. For example, consider the \(\blacktriangleright_{\rm N}\) rule, which wraps a function. The lifted version (\(\blacktriangleright_{\overline{\rm N}}\)) accepts a term with arbitrary ownership labels and propagates these labels to the result:
\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} {(\textsf {stat}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,(\tau _0\!\Rightarrow \!\tau _1) {\scriptscriptstyle \blacktriangleleft }\,\ell _1)\, {(\!(v_0)\!)}^{\bar {\ell }_2})}^{\ell _3} & \blacktriangleright_{\overline{\rm N}} & {(\mathbb{G}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,(\tau _0\!\Rightarrow \!\tau _1) {\scriptscriptstyle \blacktriangleleft }\,\ell _1)\, {(\!(v_0)\!)}^{\bar {\ell }_2})}^{\ell _3} \\ {\quad\mbox{ if ${shape\hbox{-}match}\,(\lfloor \tau _0\!\Rightarrow \!\tau _1 \rfloor ,v_0).$}} \end{array}\)
If the redex satisfies single-owner consistency, then the context label matches the client name (\(\ell _3 = \ell _0\)) and the labels inside the boundary match the sender name (\(\bar {\ell }_2 = \ell _1 \cdots \ell _1\)). Under these premises, the result also satisfies single-owner consistency.
As a second example, consider the lifted rule that applies a wrapped function:
\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} {(\textsf {app}{\lbrace \tau _0\rbrace }\,{(\!(\mathbb{G}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,(\tau _1\!\Rightarrow \!\tau _2) {\scriptscriptstyle \blacktriangleleft }\,\ell _1)\, {(v_0)}^{\ell _2})\!)}^{\bar {\ell }_3} v_1)}^{\ell _4} &\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\! \triangleright_{\overline{\rm N}} \\ {\quad\mbox{ ${(\textsf {dyn}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,\tau _2 {\scriptscriptstyle \blacktriangleleft }\,\ell _1) {(\textsf {app}{\lbrace {\mathcal {U}}\rbrace }\,v_0 (\textsf {stat}^{}\,(\ell _1 {\scriptscriptstyle \blacktriangleleft }\,\tau _1 {\scriptscriptstyle \blacktriangleleft }\,\ell _0) {(v_1)}^{\ell _4 {rev}\,(\bar {\ell }_3)}))}^{\ell _2})}^{\bar {\ell }_3 \ell _4}$}}. \end{array}\)
If the redex satisfies single-owner consistency, then \(\ell _0 = \bar {\ell }_3 = \ell _4\) and \(\ell _1 = \ell _2\). Hence, both sequences of labels in the result contain nothing but the context label \(\ell _4\).\(\square\)
Blame soundness and completeness ask whether Natural identifies the components responsible for a boundary error. Here, complete monitoring helps to simplify the questions. Specifically, complete monitoring implies that the Natural semantics detects every mismatch between two components—either immediately, or as soon as a function computes an incorrect result. Hence, every mismatch is due to a single boundary.
If \(e_0\) is well-formed and \(e_0 {\rightarrow}^*_{\rm N} \!\textsf {BoundaryErr}\,(b^{*}_0, v_0)\), then \({senders}\,(b^{*}_0)\!=\!{owners}\,(v_0)\) and furthermore \(b^{*}_0\) contains exactly one boundary specification.
The sole Natural rule that detects a mismatch blames a single boundary:
\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} {(e_0)}^{\ell _0} & {\rightarrow}^*_{\rm N} & E[\textsf {dyn}^{}\,(\ell _1 {\scriptscriptstyle \blacktriangleleft }\,\tau _0 {\scriptscriptstyle \blacktriangleleft }\,\ell _2)\, v_0] \\ & {\rightarrow}^*_{\rm N} & \textsf {BoundaryErr}\,(\lbrace (\ell _1 {\scriptscriptstyle \blacktriangleleft }\,\tau _0 {\scriptscriptstyle \blacktriangleleft }\,\ell _2)\rbrace , v_0). \end{array}\)
Thus, \(b^{*}_0 = \lbrace (\ell _1 {\scriptscriptstyle \blacktriangleleft }\,\tau _0 {\scriptscriptstyle \blacktriangleleft }\,\ell _2)\rbrace\) and \({senders}\,(b^{*}_0) =\lbrace \ell _2\rbrace\). This boundary is the correct one to blame only if it matches the true owner of the value; that is, \({owners}\,(v_0) =\lbrace \ell _2\rbrace\). Complete monitoring guarantees a match via \(\ell _0 \Vdash E[\textsf {dyn}^{}\,(\ell _1 {\scriptscriptstyle \blacktriangleleft }\,\tau _0 {\scriptscriptstyle \blacktriangleleft }\,\ell _2) {(v_0)}^{\ell _2}]\).\(\square\)
6.6 Co-Natural and Its Properties
Figure 25 presents the Co-Natural strategy. Co-Natural is a lazier variant of the Natural approach. Instead of eagerly validating pairs at a boundary, Co-Natural creates a wrapper to delay element-checks until they are needed.
Fig. 25. Co-Natural notions of reduction.
Relative to Natural, there are two changes in the notions of reduction. First, the rules for a pair value at a pair-type boundary create guards. Second, new projection rules handle guarded pairs; these rules make a new boundary to validate the projected element.
Co-Natural still satisfies both a strong type soundness theorem and complete monitoring. Blame soundness and blame completeness follow from complete monitoring. Nevertheless, Co-Natural and Natural can behave differently.
Proof Sketch. By progress and preservation lemmas for the higher-order typing judgment (\(\vdash _{\mathbf {1}}\)). Many of the proof cases are similar to cases for Natural. One case unique to Co-Natural is for pairs that cross a boundary:
\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} \textsf {dyn}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,\tau _0\!\times \!\tau _1 {\scriptscriptstyle \blacktriangleleft }\,\ell _1) \langle v_0,v_1 \rangle & \triangleright_{{\rm C}} & \mathbb{G}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,\tau _0\!\times \!\tau _1 {\scriptscriptstyle \blacktriangleleft }\,\ell _1) \langle v_0,v_1 \rangle . \end{array}\)
The typing rule for guard wrappers validates the result.\(\square\)
Proof Sketch. By preservation of single-owner consistency for the lifted \({\rightarrow}^*_{\rm C}\) relation. For example, consider the lifted rule that extracts the first element from a wrapped, untyped pair:
\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} {(\textsf {fst}{\lbrace {\mathcal {U}}\rbrace }\,{(\!(\mathbb{G}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,\tau _0\!\times \!\tau _1 {\scriptscriptstyle \blacktriangleleft }\,\ell _1) {(v_0)}^{\ell _2})\!)}^{\bar {\ell }_3})}^{\ell _4} & \blacktriangleright_{\overline{\rm C}} & {(\textsf {stat}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,\tau _0 {\scriptscriptstyle \blacktriangleleft }\,\ell _1) {(\textsf {fst}{\lbrace \tau _0\rbrace }\,{(v_0)}^{\ell _2})}^{\ell _2})}^{\bar {\ell }_3 \ell _4}. \end{array}\)
If the redex satisfies single-owner consistency, then \(\ell _0 = \bar {\ell }_3 = \ell _4\) and \(\ell _1 = \ell _2\).\(\square\)
Proof Sketch. By the same line of reasoning that supports Natural; refer to Lemma 6.8.\(\square\)
Proof Sketch. By a stuttering simulation between Natural and Co-Natural. Natural takes additional steps when a pair reaches a boundary, because it immediately checks the contents, whereas Co-Natural creates a guard wrapper. Co-Natural takes additional steps when eliminating a wrapped pair. The supplement defines the simulation relation.\(\square\)
Proof Sketch. The pair wrappers in Co-Natural imply \(C{} \not\lesssim N{}\). Consider a statically typed expression that imports an untyped pair with an ill-typed first element:
\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} \textsf {dyn}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,\textsf {Nat}\!\times \!\textsf {Nat} {\scriptscriptstyle \blacktriangleleft }\,\ell _1) \langle {-2},2 \rangle . \end{array}\)
Natural detects the mismatch at the boundary, but Co-Natural will raise an error only if the first element is accessed.\(\square\)
6.7 Forgetful and Its Properties
The Forgetful semantics (Figure 26) creates wrappers to enforce pair and function types, but strictly limits the number of wrappers on any one value. An untyped value acquires at most one wrapper. A typed value acquires at most two wrappers: one to protect itself from inputs and a second to protect its current client:
Fig. 26. Forgetful notions of reduction.
\(\begin{array}[t]{lcl} v_{s} & = & \mathbb{G}^{}\,b \, \langle v,v \rangle \\ & \mid & \mathbb{G}^{}\,b \, \lambda x.\, e \\ & \mid & \mathbb{G}^{}\,b \, (\mathbb{G}^{}\,b \langle v,v \rangle) \\ & \mid & {\mathbb{G}^{}\,b \,(\mathbb{G}^{}\,b \lambda (x:\tau).\, e)} \end{array} \qquad\qquad\qquad\qquad \begin{array}[t]{lcl} v_{d} & = & \mathbb{G}^{}\,b \, \langle v,v \rangle \\ & \mid & \mathbb{G}^{}\,b \, \lambda (x:\tau).\, e \end{array}\)
Forgetful enforces this two-wrapper limit by removing the outer wrapper of any guarded value that flows to untyped code. An untyped-to-typed boundary always makes a new wrapper, but these wrappers do not accumulate, because a value cannot enter typed code twice in a row; it must first exit typed code and lose one wrapper.
Removing outer wrappers does not affect the type soundness of untyped code; all well-formed values match \({\mathcal {U}}\), with or without wrappers. Type soundness for typed code is guaranteed by the temporary outer wrappers. Complete monitoring is lost, however, because the removal of a wrapper creates a joint-ownership situation. When a type mismatch occurs, Forgetful blames one boundary. Though sound, this one boundary is generally not enough information to find the source of the problem; in other words, Forgetful fails to satisfy blame completeness. Forgetful lies above Co-Natural and Natural in the error preorder, because it fails to enforce certain type obligations.
Proof Sketch. By progress and preservation lemmas for the higher-order typing judgment (\(\vdash _{\mathbf {1}}\)). The most interesting proof case shows that dropping a guard wrapper does not break type preservation. Suppose that a pair \(v_0\) with static type \(\textsf {Int}\!\times \!\textsf {Int}\) crosses two boundaries and re-enters typed code at a different type:
\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} \textsf {dyn}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,(\textsf {Nat}\!\times \!\textsf {Nat}) {\scriptscriptstyle \blacktriangleleft }\,\ell _1) (\textsf {stat}^{}\,(\ell _1 {\scriptscriptstyle \blacktriangleleft }\,\textsf {Int}\!\times \!\textsf {Int} {\scriptscriptstyle \blacktriangleleft }\,\ell _2)\, v_0) & {\rightarrow}^*_{\rm F} \\ {\quad\mbox{ $\mathbb{G}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,(\textsf {Nat}\!\times \!\textsf {Nat}) {\scriptscriptstyle \blacktriangleleft }\,\ell _1) (\mathbb{G}^{}\,(\ell _1 {\scriptscriptstyle \blacktriangleleft }\,\textsf {Int}\!\times \!\textsf {Int} {\scriptscriptstyle \blacktriangleleft }\,\ell _2)\, v_0)$.}} \end{array}\)
No matter what value \(v_0\) is, the result is well-typed, because the context trusts the outer wrapper. If this double-wrapped value—call it \(v_2\)—crosses another boundary, then Forgetful drops the outer wrapper. Nevertheless, the result is a dynamically typed wrapper value with sufficient type information:
\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} \textsf {stat}^{}\,(\ell _3 {\scriptscriptstyle \blacktriangleleft }\,(\textsf {Nat}\!\times \!\textsf {Nat}) {\scriptscriptstyle \blacktriangleleft }\,\ell _0)\, v_2 & {\rightarrow}^*_{\rm F} \\ {\quad\mbox{ $\mathbb{G}^{}\,(\ell _1 {\scriptscriptstyle \blacktriangleleft }\,\textsf {Int}\!\times \!\textsf {Int} {\scriptscriptstyle \blacktriangleleft }\,\ell _2)\, v_0$}}. \end{array}\)
When this single-wrapped wrapped pair reenters a typed context, it again gains a wrapper to document the context’s expectation:
\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} \textsf {dyn}^{}\,(\ell _4 {\scriptscriptstyle \blacktriangleleft }\,(\tau _1\!\times \!\tau _2) {\scriptscriptstyle \blacktriangleleft }\,\ell _3) (\mathbb{G}^{}\,(\ell _1 {\scriptscriptstyle \blacktriangleleft }\,\textsf {Int}\!\times \!\textsf {Int} {\scriptscriptstyle \blacktriangleleft }\,\ell _2)\, v_0) & {\rightarrow}^*_{\rm F} \\ {\quad\mbox{ $\mathbb{G}^{}\,(\ell _4 {\scriptscriptstyle \blacktriangleleft }\,(\tau _1\!\times \!\tau _2) {\scriptscriptstyle \blacktriangleleft }\,\ell _3) (\mathbb{G}^{}\,(\ell _1 {\scriptscriptstyle \blacktriangleleft }\,\textsf {Int}\!\times \!\textsf {Int} {\scriptscriptstyle \blacktriangleleft }\,\ell _2)\, v_0)$.}} \end{array}\)
The new wrapper preserves types.\(\square\)
Consider the lifted variant of the \(\textsf {stat}\) rule that removes an outer guard wrapper:
\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} {(\textsf {stat}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,\tau _0 {\scriptscriptstyle \blacktriangleleft }\,\ell _1) {(\!(\mathbb{G}^{}\,b_1\, v_0)\!)}^{\bar {\ell }_2})}^{\ell _3} &\!\!\!\!\!\!\!\!\!\! \blacktriangleright_{\overline{\rm F}} & {(\!(v_0)\!)}^{\bar {\ell }_2 \ell _3} \\ {\quad\mbox{ if ${shape\hbox{-}match}\,(\lfloor \tau _0 \rfloor ,(\mathbb{G}^{}\,b_1\, v_0)).$}} \end{array}\)
Suppose \(\ell _0 \ne \ell _1\). If the redex satisfies single-owner consistency, then \(\bar {\ell }_2\) contains \(\ell _1\) and \(\ell _3 = \ell _0\). Thus, the rule produces a value with two distinct labels.\(\square\)
By a preservation lemma for a weakened version of the \(\Vdash\) judgment. The weak judgment asks whether the owners on a value contain at least the name of the current component. Forgetful easily satisfies this invariant, because the ownership guidelines (Section 4.4.1) never drop an un-checked label. Thus, when a boundary error occurs,
\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} \textsf {dyn}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,\tau _0 {\scriptscriptstyle \blacktriangleleft }\,\ell _1)\, v_0 &\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\! \triangleright_{{\rm F}} & \textsf {BoundaryErr}\,(\lbrace (\ell _0 {\scriptscriptstyle \blacktriangleleft }\,\tau _0 {\scriptscriptstyle \blacktriangleleft }\,\ell _1)\rbrace , v_0) \\ {\quad \mbox{if $\lnot {shape\hbox{-}match}\,(\lfloor \tau _0 \rfloor ,v_0),$}} \end{array}\)
the sender name \(\ell _1\) matches one of the ownership labels on \(v_0\).\(\square\)
The proof of Theorem 6.16 shows how a value can acquire two labels. If such a value triggers a boundary error, then the error will be incomplete:
\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} \textsf {dyn}^{}\,(\ell _2 {\scriptscriptstyle \blacktriangleleft }\,\textsf {Int} {\scriptscriptstyle \blacktriangleleft }\,\ell _1) {(\!(\lambda x_0.\, x_0)\!)}^{\ell _0 \ell _1} & \triangleright_{\overline{\rm F}} & \textsf {BoundaryErr}\,(\lbrace (\ell _2 {\scriptscriptstyle \blacktriangleleft }\,\textsf {Int} {\scriptscriptstyle \blacktriangleleft }\,\ell _1)\rbrace , {(\!(\lambda x_0.\, x_0)\!)}^{\ell _0 \ell _1}). \end{array}\)
In this example, the error output does not point to component \(\ell _0\).\(\square\)
Proof Sketch. By a stuttering simulation. Co-Natural can take extra steps at an elimination form to unwrap an arbitrary number of wrappers; Forgetful has at most two to unwrap. The Forgetful semantics shown above never steps ahead of Co-Natural, but the supplement presents a variant with Amnesic-style trace wrappers that does step ahead.\(\square\)
Proof Sketch. \(F{} \not\lesssim C{}\), because Forgetful drops checks. Let:
\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} e_0 = \textsf {stat}^{}\,b_0 (\textsf {dyn}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,(\textsf {Nat}\!\Rightarrow \!\textsf {Nat}) {\scriptscriptstyle \blacktriangleleft }\,\ell _1) (\lambda x_0.\, x_0)), \\ e_1 = \textsf {app}{\lbrace {\mathcal {U}}\rbrace }\,e_0 \langle 2,8 \rangle . \end{array}\)
Then, \(e_1 {\rightarrow}^*_{\rm F} {\langle 2,8 \rangle }\) and Co-Natural raises a boundary error.\(\square\)
6.8 Transient and Its Properties
The Transient semantics in Figure 27 builds on the first-order evaluation syntax (Figure 20); it stores pairs and functions on a heap as indicated by the syntax of Figure 20, and aims to enforce type constructors (\(s\), the codomain of \(\lfloor \cdot \rfloor\)) through shape checks. For every pre-value \(\textsf {w}\) stored on a heap \(\mathcal {H}\), there is a corresponding entry in a blame map \(\mathcal {B}\) that points to a set of boundaries. The blame map provides information if a mismatch occurs, following Reticulated Python [83, 86].
Fig. 27. Transient notions of reduction.
Fig. 28. Amnesic notions of reduction.
Unlike for the higher-order-checking semantics, there is a significant overlap between the Transient rules for typed and untyped redexes. Figure 27 thus presents one notion of reduction. The first group of rules in Figure 27 handle boundary expressions and check expressions. When a value reaches a boundary, Transient matches its shape against the expected type. If successful, then the value crosses the boundary and its blame map records the fact; otherwise, the program halts. For a \(\textsf {dyn}\) boundary, the result is a boundary error. For a \(\textsf {stat}\) boundary, the mismatch reflects an invariant error in typed code. Check expressions similarly match a value against a type-shape. On success, the blame map gains the boundaries associated with the location \(\textsf {p}_0\) from which the value originated. On failure, these same boundaries may help the programmer diagnose the fault.
The second group of rules handles primitives and application. Pair projections and function applications must be followed by a check in typed contexts to enforce the type annotation at the elimination form. In untyped contexts, a check for the dynamic type embeds a possibly typed subexpression. The binary operations are not elimination forms, so they are not followed by a check. Applications of typed functions additionally check the input value against the function’s domain type. If successful, then the blame map records the check. Otherwise, Transient reports the boundaries associated with the function and its argument.13 Note that untyped functions may appear in typed contexts and vice-versa, because Transient does not create wrappers.
Applications of untyped functions in untyped code do not update the blame map. This allows an implementation to insert checks by rewriting only typed code, leaving untyped code as is. Protected typed code can thus interact with any untyped libraries [86], just like other variants.
Not shown in Figure 27 are rules for elimination forms that halt the program. When \(\delta\) is undefined or when a non-function is applied, the result is either an invariant error or a tag error depending on the context.
Transient shape checks do not guarantee full type soundness, complete monitoring, or blame soundness and completeness. They do, however, preserve the top-level shape of all values in typed code. Blame completeness fails, because Transient does not update the blame map when an untyped function is applied in an untyped context.
Proof Sketch. Let \(e_0 = \textsf {dyn}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,(\textsf {Nat}\!\Rightarrow \!\textsf {Nat}) {\scriptscriptstyle \blacktriangleleft }\,\ell _1) (\lambda x_0.\, {-4})\).
but \(\not\vdash _{\mathbf {1}}(\lambda x_0.\, {-4}) : \textsf {Nat}\!\Rightarrow \!\textsf {Nat}\).\(\square\)
Proof Sketch. Recall that \(\mathbf {s}\) maps types to type shapes and the unitype to itself. The proof depends on progress and preservation lemmas for the first-order typing judgment (\(\vdash _{\mathbf {s}}\)). Although Transient lets any well-shaped value cross a boundary, the check expressions that appear after elimination forms preserve soundness. Suppose that an untyped function crosses a boundary and eventually computes an ill-typed result:
\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} (\textsf {app}{\lbrace \textsf {Int}\rbrace }\,\textsf {p}_0 4); \mathcal {H}_0; \mathcal {B}_0 &\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\!\! \blacktriangleright_{{\top}} & (\textsf {check}{\lbrace \textsf {Int}\rbrace }\,{{\langle 4,\textsf {sum}{\lbrace {\mathcal {U}}\rbrace }\,4\,1 \rangle }}\,{\textsf {p}_0}); \mathcal {H}_0; \mathcal {B}_1 \\ {\quad \mbox{if $\mathcal {H}_{0}(\textsf {p}_0)=\lambda x_0.\, \langle x_0,\textsf {sum}{\lbrace {\mathcal {U}}\rbrace }\,x_0\,1 \rangle $}} \\ {\quad \mbox{and $\mathcal {B}_1 = \mathcal {B}_0[v_0 \mathrel {{\cup }}{rev}\,(\mathcal {B}_0(\textsf {p}_0))].$}} \end{array}\)
The check expression guards the context.\(\square\)
A structured value can cross any boundary with a matching shape, regardless of the deeper type structure. For example, the following lifted rule (\(\blacktriangleright_{\overline{\top}}\)) adds a new label to a pair:
\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} {(\textsf {dyn}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,\tau _0\!\times \!\tau _1 {\scriptscriptstyle \blacktriangleleft }\,\ell _1) {(\!(\textsf {p}_0)\!)}^{\bar {\ell }_2})}^{\ell _3}; \mathcal {H}_0; \mathcal {B}_0 & \blacktriangleright_{\overline{\top}} & {(\!(\textsf {p}_0)\!)}^{\bar {\ell }_2 \ell _3}; \mathcal {H}_0; \mathcal {B}_1, \\ {\quad\mbox{ where $\mathcal {H}_0(\textsf {p}_0) \in \langle v,v \rangle .$}} \end{array}\)\(\square\)
Let component \(\ell _0\) define a function \(f_0\) and export it to components \(\ell _1\) and \(\ell _2\). If component \(\ell _2\) triggers a type mismatch, as sketched below, then Transient blames both \(\ell _2\) and the irrelevant \(\ell _1\):

The following term expresses this scenario using a let-expression to abbreviate untyped function application:
\( \begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} (\textsf {let}\, f_0=(\lambda x_0.\, \langle x_0,x_0 \rangle) \textsf {in} \\ \textsf {let}\, f_1=(\textsf {stat}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,(\textsf {Int}\!\Rightarrow \!\textsf {Int}) {\scriptscriptstyle \blacktriangleleft }\,\ell _1) {(\textsf {dyn}^{}\,(\ell _1 {\scriptscriptstyle \blacktriangleleft }\,(\textsf {Int}\!\Rightarrow \!\textsf {Int}) {\scriptscriptstyle \blacktriangleleft }\,\ell _0) {(f_0)}^{\ell _0})}^{\ell _1}) \textsf {in} \\ \textsf {stat}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,\textsf {Int} {\scriptscriptstyle \blacktriangleleft }\,\ell _2) {(\textsf {app}{\lbrace \textsf {Int}\rbrace }\,(\textsf {dyn}^{}\,(\ell _2 {\scriptscriptstyle \blacktriangleleft }\,(\textsf {Int}\!\Rightarrow \!\textsf {Int}) {\scriptscriptstyle \blacktriangleleft }\,\ell _0) {(f_0)}^{\ell _0}) 5)}^{\ell _2})^{{\scriptstyle \ell _0}}; \emptyset ; \emptyset . \end{array} \)
Reduction ends in a boundary error that blames three components.\(\square\)
An untyped function application in untyped code does not update the blame map:
\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} (\textsf {app}{\lbrace {\mathcal {U}}\rbrace }\,\textsf {p}_0\, v_0); \mathcal {H}_0; \mathcal {B}_0 & \blacktriangleright_{{\top}} & (e_0[x_0\!\leftarrow \!v_0]); \mathcal {H}_0; \mathcal {B}_0 \\ {\quad\mbox{ if $\mathcal {H}_{0}(\textsf {p}_0)=\lambda x_0.\, e_0$.}} \end{array}\)
Such applications lead to incomplete blame when the function has previously crossed a type boundary. To illustrate, the term below uses an untyped identity function \(f_1\) to coerce the type of another function \(f_0\). After the coercion, an application leads to type mismatch:
\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} (\textsf {let}\, f_0=\textsf {stat}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,\tau _0 {\scriptscriptstyle \blacktriangleleft }\,\ell _1) {(\textsf {dyn}^{}\,(\ell _1 {\scriptscriptstyle \blacktriangleleft }\,\tau _0 {\scriptscriptstyle \blacktriangleleft }\,\ell _2) {(\lambda x_0.\, x_0)}^{\ell _2})}^{\ell _1}\, \textsf {in} \\ \textsf {let}\, f_1=\textsf {stat}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,(\tau _0\!\Rightarrow \!\tau _1) {\scriptscriptstyle \blacktriangleleft }\,\ell _3) {(\textsf {dyn}^{}\,(\ell _3 {\scriptscriptstyle \blacktriangleleft }\,(\tau _0\!\Rightarrow \!\tau _1) {\scriptscriptstyle \blacktriangleleft }\,\ell _4) {(\lambda x_1.\, x_1)}^{\ell _4})}^{\ell _3} \, \textsf {in} \\ \textsf {stat}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,(\textsf {Int}\!\times \!\textsf {Int}) {\scriptscriptstyle \blacktriangleleft }\,\ell _5) \\ \qquad {{(\textsf {app}{\lbrace \textsf {Int}\!\times \!\textsf {Int}\rbrace }\,(\textsf {dyn}^{}\,(\ell _5 {\scriptscriptstyle \blacktriangleleft }\,\tau _1 {\scriptscriptstyle \blacktriangleleft }\,\ell _0) {(\textsf {app}{\lbrace {\mathcal {U}}\rbrace }\,f_1\, f_0)}^{\ell _0}) 42)}^{\ell _5})^{{\scriptstyle \ell _0}}}; \emptyset ; \emptyset . \end{array}\)
Reduction ends in a boundary error that does not report the crucial labels \(\ell _3\) and \(\ell _4\).\(\square\)
Finally, Transient is more permissive than Forgetful in the error pre-order.
Proof Sketch. Indirectly, via \(T{} \eqsim A{}\) (Theorem 6.30) and \(F{} \lesssim A{}\) (Theorem 6.31).\(\square\)
The results about the wrapper-free Transient semantics are negative. It fails \(\mathbf {CM}{}\) and \(\mathbf {BC}{}\), because it has no interposition mechanism to keep track of type implications for untyped code. Its heap-based approach to blame fails \(\mathbf {BS}{}\), because the blame heap conflates different paths in a program.14
If several clients use the same library function and one client encounters a type mismatch, then every component gets blamed. The reader should keep in mind, however, that the chosen properties are of a purely theoretical nature. In practice, Transient has played an important role when it comes to performance [33, 36, 37]. Furthermore, the work of Lazarek et al. [45] has also raised questions concerning the pragmatics of blame soundness (and completeness).
6.9 Amnesic and Its Properties
The Amnesic semantics (Figure 28) employs the same dynamic checks as Transient and supports the synthesis of error messages with path-based blame information. While Transient attempts to track blame with heap addresses, Amnesic uses trace wrappers to attach blame information to values.
Amnesic bears a strong resemblance to the Forgetful semantics. Both use guard wrappers in the same way, keeping a sticky “inner” wrapper around typed values and a temporary “outer” wrapper in typed contexts. There are two crucial differences:
The elimination rules for guarded pairs show the clearest difference between checks in Amnesic (which mimics Transient) and Forgetful. Amnesic ignores the type in the guard. Forgetful ignores the type annotation on the pair projection.
The following wrapped values can occur at run-time in Amnesic. The notation \((\mathbb{T}_{?}\,b^{*}\,e)\) is short for an expression that may or may not have a trace wrapper.
Fig. 29. Metafunctions for Amnesic.
\(\begin{array}[t]{lcl} v_{s} & = & \mathbb{G}^{}\,b\, (\mathbb{T}_{?}\,b^{*}\,\langle v,v \rangle) \\ [1pt] & \mid & \mathbb{G}^{}\,b\, (\mathbb{T}_{?}\,b^{*}\,\lambda x.\, e) \\ [1pt] & \mid & \mathbb{G}^{}\,b\, (\mathbb{T}_{?}\,b^{*}\,(\mathbb{G}^{}\,b \langle v,v \rangle)) \\ [1pt] & \mid & {\mathbb{G}^{}\,b\, (\mathbb{T}_{?}\,b^{*}\,(\mathbb{G}^{}\,b \lambda (x:\tau).\, e)),} \end{array} \qquad\qquad\qquad\qquad \begin{array}[t]{lcl} v_{d} & = & \mathbb{T}_{}\,b^{*}\,i \\ & \mid & \mathbb{T}_{}\,b^{*}\,\langle v,v \rangle \\ & \mid & \mathbb{T}_{}\,b^{*}\,\lambda x.\, e \\ & \mid & \mathbb{T}_{?}\,b^{*}\,(\mathbb{G}^{}\,b \langle v,v \rangle) \\ & \mid & \mathbb{T}_{?}\,b^{*}\,(\mathbb{G}^{}\,b \, \lambda (x:\tau).\, e). \end{array} \)
Figure 29 defines three metafunctions and one abbreviation for trace wrappers. The metafunctions extend, retrieve, and remove the boundaries associated with a value. The abbreviation simplifies the formulation of the reduction rules as they now accept optionally traced values.
Amnesic satisfies full type soundness thanks to guard wrappers and fails complete monitoring, because it drops wrappers. This is no surprise, because Amnesic creates and removes guard wrappers in the same manner as Forgetful. Unlike the Forgetful semantics, Amnesic uses trace wrappers to remember the boundaries that a value has crossed. This information leads to sound and complete blame messages.
Proof Sketch. By progress and preservation lemmas for the higher-order typing judgment (\(\vdash _{\mathbf {1}}\)). Amnesic creates and drops wrappers in the same manner as Forgetful (Theorem 6.15), so the only interesting proof cases concern elimination forms. For example, when Amnesic extracts an element from a guarded pair, it ignores the type in the guard (\(\tau _1\!\times \!\tau _2\)):
\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} \textsf {fst}{\lbrace \tau _0\rbrace }\,(\mathbb{G}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,\tau _1\!\times \!\tau _2 {\scriptscriptstyle \blacktriangleleft }\,\ell _1)\, v_0) & \triangleright_{{\rm A}} & \textsf {dyn}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,\tau _0 {\scriptscriptstyle \blacktriangleleft }\,\ell _1) (\textsf {fst}{\lbrace {\mathcal {U}}\rbrace }\,v_0). \end{array}\)
The new boundary enforces the context’s assumption (\(\tau _0\)), which is enough to satisfy type soundness.\(\square\)
Proof Sketch. Removing a wrapper creates a value with more than one label:
\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} {(\textsf {stat}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,(\tau _0\!\Rightarrow \!\tau _1) {\scriptscriptstyle \blacktriangleleft }\,\ell _1) {(\!(\mathbb{G}^{}\,b_1 {(\!(\mathbb{T}_{}\,b^{*}_0\,{(\!(\lambda x_0.\, x_0)\!)}^{\bar {\ell }_2})\!)}^{\bar {\ell }_3})\!)}^{\bar {\ell }_4})}^{\ell _5} &\!\!\! \blacktriangleright_{\overline{\rm A}} \\ {\quad \mbox{${(\!(\textsf {trace}\,(\lbrace (\ell _0 {\scriptscriptstyle \blacktriangleleft }\,(\tau _0\!\Rightarrow \!\tau _1) {\scriptscriptstyle \blacktriangleleft }\,\ell _1), b_1\rbrace \cup b^{*}_0)\,{(\!(\lambda x_0.\, x_0)\!)}^{\bar {\ell }_2})\!)}^{\bar {\ell }_3 \bar {\ell }_4 \ell _5}$}}. \end{array}\) \({\square}\)
Proof Sketch. By progress and preservation lemmas for a path-based consistency judgment, \(\Vdash _{p}\), that weakens single-owner consistency to allow multiple labels around a trace-wrapped value. Unlike the heap-based consistency for Transient, which requires an entirely new judgment, path-based consistency replaces only the rules for trace wrappers (shown in Figure 30) and trace expressions. Now consider the guard-dropping rule:
Fig. 30. Path-based ownership consistency for trace wrappers.
\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} {(\textsf {stat}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,(\tau _0\!\Rightarrow \!\tau _1) {\scriptscriptstyle \blacktriangleleft }\,\ell _1) {(\!(\mathbb{G}^{}\,b_1 {(\!(\mathbb{T}_{}\,b^{*}_0\,{(\!(\lambda x_0.\, x_0)\!)}^{\bar {\ell }_2})\!)}^{\bar {\ell }_3})\!)}^{\bar {\ell }_4})}^{\ell _5} & \blacktriangleright_{\overline{\rm A}} \\ {\quad \mbox{${(\!(\textsf {trace}\,(\lbrace (\ell _0 {\scriptscriptstyle \blacktriangleleft }\,(\tau _0\!\Rightarrow \!\tau _1) {\scriptscriptstyle \blacktriangleleft }\,\ell _1), b_1\rbrace \cup b^{*}_0)\,{(\!(\lambda x_0.\, x_0)\!)}^{\bar {\ell }_2})\!)}^{\bar {\ell }_3 \bar {\ell }_4 \ell _5}$}}. \end{array}\)
Path-consistency for the redex implies that \(\bar {\ell }_3\) and \(\bar {\ell }_4\) match the component names on the boundary \(b_1\), and that the client side of \(b_1\) matches the outer sender \(\ell _1\). Thus, the new labels on the result match the sender names on the two new boundaries in the trace. \(\square\)
Proof Sketch. By a stuttering simulation between Transient and Amnesic. Amnesic may take extra steps at an elimination form and to combine traces into one wrapper. Transient takes extra steps to place pre-values on the heap and to check the result of elimination forms. In fact, the two compute equivalent results up to wrappers and blame. \(\square\)
Proof Sketch. By a lock-step bisimulation. The only difference between Forgetful and Amnesic comes from subtyping. Forgetful uses wrappers to enforce the type on a boundary. Amnesic uses boundary types only for an initial shape check and instead uses the static types in typed code to guide checks at elimination forms. \(\square\)
Proof Sketch. In the following \(A{} \not\lesssim F{}\) example, a boundary declares one type and an elimination form requires a weaker type:
\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} \textsf {fst}{\lbrace \textsf {Int}\rbrace }\,(\textsf {dyn}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,(\textsf {Nat}\!\times \!\textsf {Nat}) {\scriptscriptstyle \blacktriangleleft }\,\ell _1) \langle {-4},4 \rangle). \end{array}\)
Since \({{-4}}\) is an \(\textsf {Int}\), Amnesic reduces the expression to a value. Forgetful detects an error. \(\square\)
6.10 Erasure and Its Properties
Figure 31 presents the values and notions of reduction for the Erasure semantics. Erasure ignores all types at run-time. As the first two reduction rules show, any value may cross any boundary. When an incompatible value reaches an elimination form, the result depends on the context. In untyped code, the redex steps to a tag error. In typed code, the malformed redex indicates that an ill-typed value crossed a boundary. Thus, Erasure ends with a boundary error at the last possible moment. These errors come with no information, because there is no record of the relevant boundary to point back to.
Fig. 31. Erasure notions of reduction.
Erasure satisfies neither \(\mathbf {TS}\, {(}\mathbf {1} {)}\) nor \(\mathbf {TS}\, {(}\mathbf {s} {)}\).
Dynamic-to-static boundaries are unsound. An untyped function, for example, can enter a typed context that expects an integer: \(\textsf {dyn}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,\textsf {Int} {\scriptscriptstyle \blacktriangleleft }\,\ell _1) (\lambda x_0.\, 42) \blacktriangleright_{\rm{E}}(\lambda x_0.\, 42) .\)\(\square\)
Proof Sketch. By progress and preservation lemmas for the erased “dynamic-typing” judgment (\(\vdash _{\mathbf {0}}\)). Given well-formed input, every \(\blacktriangleright_{\rm{E}}\) rule yields a dynamically typed result. \(\square\)
Proof Sketch. This static-to-dynamic transition \({(\textsf {stat}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,\tau _0 {\scriptscriptstyle \blacktriangleleft }\,\ell _1) {(v_0)}^{\ell _2})}^{\ell _3} \blacktriangleright_{\overline{\rm{E}}} {(\!(v_0)\!)}^{\ell _2 \ell _3}\) adds multiple labels to a value. \(\square\)
Proof Sketch. An Erasure boundary error blames an empty set, for example,
\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} \textsf {fst}{\lbrace \textsf {Int}\rbrace }\,(\lambda x_0.\, x_0) & \blacktriangleright_{\rm{R}} & \textsf {BoundaryErr}\,(\emptyset , (\lambda x_0.\, x_0)). \end{array}\)
The empty set is trivially sound and incomplete. \(\square\)
Proof Sketch. By a stuttering simulation. Amnesic takes extra steps at elimination forms, to enforce types, and to create trace wrappers. \(\square\)
Proof Sketch. As a counterexample showing \(E{} \not\lesssim A{}\), the following term applies an untyped function:
\(\begin{array}{l@{\hspace{5.0pt}}c@{\hspace{5.0pt}}l} \textsf {app}{\lbrace \textsf {Nat}\rbrace }\,(\textsf {dyn}^{}\,(\ell _0 {\scriptscriptstyle \blacktriangleleft }\,(\textsf {Nat}\!\Rightarrow \!\textsf {Nat}) {\scriptscriptstyle \blacktriangleleft }\,\ell _1) (\lambda x_0.\, {-9})) 4. \end{array}\)
Amnesic checks for a natural-number result and errors, but Erasure checks nothing. \(\square\)
7 RELATED WORK
Several authors have used cast calculi to design and analyze variants of the Natural semantics. The original work in this lineage is Henglein’s coercion calculus [40]. Siek et al. [67] discover several variants by studying two design choices: laziness in higher-order casts and blame-assignment strategies for the dynamic type. Siek et al. [63] present two space-efficient calculi and prove them equivalent to a Natural blame calculus. Siek and Chen [65] generalize these calculi with a parameterized framework and directly model six of them.
The literature has many other variants of the Natural semantics. Some of these are eager, such as AGT [28] and monotonic [64]; others are lazy like Co-Natural [20, 21, 27]. All can be positioned relative to one another by our error preorder.
The \(\textsf {KafKa}\) framework expresses all four type-enforcement strategies compared in Section 2: Natural (Behavioral), Erasure (Optional), Transient, and Concrete [18]. It thus enables direct comparisons of example programs. The framework is mechanized and has a close correspondence to practical implementations, because each type-enforcement strategy is realized as a compiler to a common core language. \(\textsf {KafKa}\) does not, however, include a meta-theoretical analysis.
New et al. [53, 54] develop an axiomatic theory of term precision to formalize the gradual guarantee and subsequently derive an order-theoretic specification of casts. This specification of casts is a guideline for how to enforce types in a way that preserves standard type-based reasoning principles. Only the Natural strategy satisfies the axioms.
8 DISCUSSION
One central design issue for languages that can mix typed and untyped code is the semantics of types and specifically how their integrity is enforced as values flow from typed to untyped code and back. Among other things, the choice determines whether static types can be trusted and whether error messages come with useful information when an interaction goes wrong. The first helps the compiler with type-based optimization and influences how a programmer thinks about performance. The second might play a key role when programmers must debug mismatches between types and code. Without an interaction story, mixed-typed programs are no better than dynamically typed programs when it comes to run-time errors. Properties that hold for the typed half of the language are only valid under a closed-world assumption [8, 16, 58]; such properties are a starting point, but make no contribution to the overall goal.
As our analysis demonstrates, the limitations of the host language determine the invariants that a language designer can hope to enforce. First, higher-order wrappers enable strong guarantees but require functional APIs15 or support from the host runtime system. A language without wrappers of any sort sets up weak guarantees by rewriting typed code.
Technically speaking, the article presents six distinct semantics from four different angles (Table 2) and establishes an error preorder relation:
| \(\mbox{Natural}\) | \(\mbox{Co-Natural}\) | \(\mbox{Forgetful}\) | \(\mbox{Transient}\) | \(\mbox{Amnesic}\) | \(\mbox{Erasure}\) | |
| Type soundness | \(\mathbf {1}\) | \(\mathbf {1}\) | \(\mathbf {1}\) | \({\mathbf {s}}\) | \(\mathbf {1}\) | \(\mathbf {0}\) |
| Complete monitoring | \( {✓ } \) | \( {✓ } \) | \({✗ }\) | \({✗ }\) | \({✗ }\) | \({✗ }\) |
| Blame soundness | \( {✓ } \) | \( {✓ } \) | \( {✓ } \) | \({{✗ }}\) | \( {✓ } \) | \(\emptyset\) |
| Blame completeness | \( {✓ } \) | \( {✓ } \) | \({{✗ }}\)† | \({✗ }\) | \( {✓ } \) | \({✗ }\) |
| Error preorder | \(\textsf {N}\quad {\lesssim }\) | \(\textsf {C}\quad {\lesssim }\) | \(\textsf {F}\quad {\lesssim }\) | \(\textsf {T}\quad {\eqsim }\) | \(\textsf {A}\quad {\lesssim }\) | \(\textsf {E}\) |
Table 2. Technical Contributions
• | Type soundness is a relatively weak property; it determines whether typed code can trust its own types. Except for the Erasure semantics, which does nothing to enforce types, type soundness does not clearly distinguish the various strategies. | ||||
• | Complete monitoring is a stronger property, adapted from the literature on higher-order contracts [23]. It holds when untyped code can trust type specifications and vice-versa. | ||||
The last two properties tell a developer what aid to expect if a type mismatch occurs.
• | Blame soundness ensures that every boundary in a blame message is potentially responsible. Four strategies satisfy blame soundness relative to a path-based notion of responsibility. Transient fails to satisfies blame soundness, because it merges blame information for distinct references to a heap-allocated value (Theorem 6.24). Erasure is trivially blame-sound, because it gives the programmer zero information. | ||||
• | Blame completeness ensures that every blame error comes with an overapproximation of the responsible parties. Three of the blame-sound semantics satisfy blame completeness, and Forgetful can be made complete with a straightforward modification. The Erasure strategy trivially fails blame completeness. The Transient strategy fails because it has no way to supervise untyped values that flow through a typed context. | ||||
Transient and Erasure provide the weakest guarantees, but they also have a strength that Table 2 does not bring across; namely, they are the only strategies that do not require wrapper values. Wrappers impose space costs and time costs; they also raise object identity issues [26, 43, 72, 84]. A wrapper-free strategy with stronger guarantees would therefore be promising. A related topic for future work is to test whether the weaker guarantees of wrapper-free strategies are sufficiently useful in practice. Lazarek et al. [45] find that the gap between Natural blame and Transient blame is smaller than expected across thousands of simulated debugging scenarios. It remains to be seen whether this small gap nevertheless has large implications for working programmers.
The choice of semantics of type enforcement has implications for two major aspects of language design: the performance of an implementation and its acceptance by working developers. Greenman et al. [38] developed an evaluation framework for the performance concern that is slowly gaining in acceptance; Tunnell Wilson et al. [82] present rather preliminary results concerning the acceptance by programmers. In conclusion, though, much remains to be done before the community can truly claim to understand this multi-faceted design space.
ACKNOWLEDGMENTS
Michael Ballantyne inspired the strategy-oriented comparisons in Section 5. Michael M. Vitousek suggested that Transient is not as unsound as it first seems, which led us toward the bisimilar, sound Amnesic semantics. Amal Ahmed, Stephen Chang, and Max New criticized several of our attempts to explain complete monitoring. Max also provided a brief technical description of his dissertation work.
Footnotes
1 A shape check enforces a correspondence between a top-level value constructor and the top-level constructor of a type. It generalizes the tag checks found in many runtime systems.
Footnote2 Implementations of Natural can yield performance improvements relative to untyped code, especially when typed code rarely interacts with untyped code [44, 75].
Footnote3 Thanks to the TOPLAS reviewers for reminding us that the gradual guarantees are not meant to distinguish semantics in terms of how they enforce types. The guarantees address a separate dimension; namely, the behavior of type
FootnoteDynamic .4 Personal communication with Benjamin Lerner and Shriram Krishnamurthi.
Footnote- Footnote
- Footnote
7 How to add a dynamic type is a separate dimension that is orthogonal to the question of how to enforce types. With or without such a type, our results apply to the language’s type-enforcement strategy.
Footnote8 Adding this form of subtyping also ensures model can scale to include true union types, which are an integral part of the idiomatic type systems added to untyped languages [15, 80, 81].
Footnote9 Boundary terms are similar to casts from the gradual typing literature, but provide more structure for blame assignment. A boundary connects a typed component to an untyped component. A cast connects typed code to less-precisely typed code; both sides of a cast may be part of the same component.
Footnote10 A language with the dynamic type will need a third wrapper for base values that have been assigned type dynamic.
Footnote11 Since these examples use only function types, they exhibit the same behavior according to Co-Natural as well as Natural.
Footnote12 Correction note: our prior work uses the name monitor wrapper and value constructor \(\textsf {mon}\) [34, 35]. The name guard wrapper better matches earlier work [23, 76], in which
Footnotemon creates an expression andG creates a wrapper.13 Blaming the argument as well as the function is a change to the original Transient semantics [86] that may provide more information in some cases (personal communication with Michael M. Vitousek).
Footnote14 It is possible to adapt the path-based notion of ownership to a form of “shared” ownership that partially matches Transient’s “collaborative” blame strategy [35]. A notion of ownership that matches Transient fully remains an open problem.
Footnote15 A language with first-class functions can always use lambda as a wrapper [70].
Footnote
Supplemental Material
Available for Download
Supplementary material
- [1] . 2011. Blame for all. In Proceedings of the POPL. 201–214.Google Scholar
Digital Library
- [2] . 1994. Soft typing with conditional types. In Proceedings of the POPL. 163–173.Google Scholar
Digital Library
- [3] . 2013. Gradual typing for smalltalk. Sci. Comput. Program. 96, 1 (2013), 52–69.Google Scholar
- [4] . 2020. Towards Efficient Gradual Typing via Monotonic References and Coercions. Ph.D. Dissertation. Indiana University.Google Scholar
Digital Library
- [5] . 2003. BabyJ: From object based to class based programming via types. WOOD 82, 7 (2003), 53–81.Google Scholar
- [6] . 2017. Sound gradual typing: only mostly dead. PACMPL 1, OOPSLA (2017), 54:1–54:24.Google Scholar
- [7] . 1983. Initial and final algebra semantics for data type specifications: Two characterization theorems. SIAM J. Comput. 12, 2 (1983), 366–387.Google Scholar
Digital Library
- [8] . 2014. Understanding TypeScript. In Proceedings of the ECOOP. 257–281.Google Scholar
Digital Library
- [9] . 2009. Thorn: Robust, concurrent, extensible scripting on the JVM. In Proceedings of the OOPSLA. 117–136.Google Scholar
Digital Library
- [10] . 2016. Practical optional types for clojure. In Proceedings of the ESOP. 68–94.Google Scholar
Cross Ref
- [11] . 1993. Strongtalk: Typechecking smalltalk in a production environment. In Proceedings of the OOPSLA. 215–230.Google Scholar
Digital Library
- [12] . 1980. A constructive alternative to data type definitions. In Proceedings of the LFP. 46–55.Google Scholar
Digital Library
- [13] . 1991. Soft typing. In Proceedings of the PLDI. 278–292.Google Scholar
Digital Library
- [14] . 2019. A space-efficient call-by-value virtual machine for gradual set-theoretic types. In Proceedings of the IFL. 8:1–8:12.Google Scholar
Digital Library
- [15] . 2017. Gradual typing with union and intersection types. PACMPL 1, ICFP (2017), 41:1–41:28.Google Scholar
- [16] . 2017. Fast and precise type checking for JavaScript. PACMPL 1, OOPSLA (2017), 56:1–56:30.Google Scholar
- [17] . 2012. Practical typed lazy contracts. In Proceedings of the ICFP. 67–76.Google Scholar
Digital Library
- [18] . 2018. KafKa: Gradual typing for objects. In Proceedings of the ECOOP. 12:1–12:23.Google Scholar
- [19] . 2020. The Dart Type System. Retrieved from https://dart.dev/guides/language/type-system.Google Scholar
- [20] . 2012. The interaction of contracts and laziness. In Proceedings of the PEPM. 97–106.Google Scholar
Digital Library
- [21] . 2011. On contract satisfaction in a higher-order world. Trans. Program. Lang. Syst. 33, 5 (2011), 16:1–16:29.Google Scholar
- [22] . 2011. Correct blame for contracts: No more scapegoating. In Proceedings of the POPL. 215–226.Google Scholar
Digital Library
- [23] . 2012. Complete monitors for behavioral contracts. In Proceedings of the ESOP. 214–233.Google Scholar
Digital Library
- [24] . 2018. Collapsible contracts: Fixing a pathology of gradual typing. PACMPL 2, OOPSLA (2018), 133:1–133:27.Google Scholar
- [25] . 2002. Contracts for higher-order functions. In Proceedings of the ICFP. 48–59.Google Scholar
Digital Library
- [26] . 2004. Semantic casts: Contracts and structural subtyping in a nominal world. In Proceedings of the ECOOP. 364–388.Google Scholar
Cross Ref
- [27] . 2007. Lazy contract checking for immutable data structures. In Proceedings of the IFL. 111–128.Google Scholar
- [28] . 2016. Abstracting gradual typing. In Proceedings of the POPL. 429–442.Google Scholar
Digital Library
- [29] . 2019. Which of my transient type checks are not (almost) free? In Proceedings of the VMIL. 58–66.Google Scholar
Digital Library
- [30] . 2014. Space-efficient manifest contracts. Retrieved from https://arxiv.org/abs/1410.2813.Google Scholar
- [31] . 2015. Space-efficient manifest contracts. In Proceedings of the POPL. 181–194.Google Scholar
Digital Library
- [32] . 2019. The dynamic practice and static theory of gradual typing. In Proceedings of the SNAPL. 6:1–6:20.Google Scholar
- [33] . 2020. Deep and Shallow Types Ph.D. Dissertation. Northeastern University.Google Scholar
Digital Library
- [34] . 2018. A spectrum of type soundness and performance. PACMPL 2, ICFP (2018), 71:1–71:32.Google Scholar
- [35] . 2019. Complete monitors for gradual types. PACMPL 3, OOPSLA (2019), 122:1–122:29.Google Scholar
- [36] . 2022. A transient semantics for typed racket. Programming 6, 2 (2022), 1–25.Google Scholar
- [37] . 2018. On the cost of type-tag soundness. In Proceedings of the PEPM. 30–39.Google Scholar
Cross Ref
- [38] . 2019. How to evaluate the performance of gradual type systems. J. Funct. Program. 29, e4 (2019), 1–45.Google Scholar
- [39] . 2018. Pallene: A statically typed companion language for Lua. In Proceedings of the SBLP. 19–26.Google Scholar
Digital Library
- [40] . 1994. Dynamic typing: Syntax and proof theory. Sci. Comput. Program. 22, 3 (1994), 197–230.Google Scholar
Digital Library
- [41] . 2010. Space-efficient gradual typing. Higher-order Symbol. Comput. 23, 2 (2010), 167–189.Google Scholar
Digital Library
- [42] . 2006. Typed contracts for functional programming. In Proceedings of the FLOPS. 208–225.Google Scholar
Digital Library
- [43] . 2015. Transparent object proxies in JavaScript. In Proceedings of the ECOOP. 149–173.Google Scholar
- [44] . 2019. Toward efficient gradual typing for structural types via coercions. In Proceedings of the PLDI. 517–532.Google Scholar
Digital Library
- [45] . 2021. How to evaluate blame for gradual types. PACMPL 5, ICFP (2021), 68:1–68:29.Google Scholar
- [46] . 2023. Gradual soundness: Lessons from static python. Programming 7, 1 (2023), 2:1–2:40.Google Scholar
- [47] . 2015. A formalization of typed lua. In Proceedings of the DLS. 13–25.Google Scholar
Digital Library
- [48] . 2009. Operational semantics for multi-language programs. Trans. Program. Lang. Syst. 31, 3 (2009), 1–44.Google Scholar
Digital Library
- [49] . 1978. A theory of type polymorphism in programming. J. Comput. Syst. Sci. 17, 3 (1978), 348–375.Google Scholar
Cross Ref
- [50] . 1974. MACLISP Reference Manual, Revision 0.
Technical Report . MIT Project MAC.Google Scholar - [51] . 2016. Extensible access control with authorization contracts. In Proceedings of the OOPSLA. 214–233.Google Scholar
Digital Library
- [52] . 2017. Sound gradual typing is nominally alive and well. PACMPL (2017), 56:1–56:30.Google Scholar
- [53] . 2020. A Semantic Foundation for Sound Gradual Typing Ph.D. Dissertation. Northeastern University.Google Scholar
Digital Library
- [54] . 2019. Gradual type theory. PACMPL (2019), 15:1–15:31.Google Scholar
- [55] . 1993. Semantics for communication primitives in a Polymorphic language. In Proceedings of the POPL. 99–112.Google Scholar
Digital Library
- [56] . 2008. Embedding an interpreted language using higher-order functions and types. J. Funct. Program. 21, 6 (2008), 585–615.Google Scholar
Digital Library
- [57] . 2012. The ins and outs of gradual type inference. In Proceedings of the POPL. 481–494.Google Scholar
Digital Library
- [58] . 2015. Safe & efficient gradual typing for TypeScript. In Proceedings of the POPL. 167–180.Google Scholar
Digital Library
- [59] . 2013. The ruby type checker. In Proceedings of the SAC. 1565–1572.Google Scholar
Digital Library
- [60] . 2017. The VM already knew that: Leveraging compile-time knowledge to optimize gradual typing. PACMPL (2017), 55:1–55:27.Google Scholar
- [61] . 2015. Concrete types for TypeScript. In Proceedings of the ECOOP. 76–100.Google Scholar
- [62] . 2019. Transient typechecks are (almost) free. In Proceedings of the ECOOP. 15:1–15:29.Google Scholar
- [63] . 2015. Blame and coercion: Together again for the first time. In Proceedings of the PLDI. 425–435.Google Scholar
Digital Library
- [64] . 2015. Monotonic references for efficient gradual typing. In Proceedings of the ESOP. 432–456.Google Scholar
Digital Library
- [65] . 2021. Parameterized cast calculi and reusable meta-theory for gradually typed lambda calculi. J. Funct. Program. 31 (2021), e30.Google Scholar
Cross Ref
- [66] . 2012. Interpretations of the gradually typed lambda calculus. In Proceedings of the SFP. 68–80.Google Scholar
Digital Library
- [67] . 2009. Exploring the design space of higher-order casts. In Proceedings of the ESOP. 17–31.Google Scholar
Digital Library
- [68] . 2006. Gradual typing for functional languages. In Proceedings of the SFP. 81–92.Google Scholar
- [69] . 2015. Refined criteria for gradual typing. In Proceedings of the SNAPL. 274–293.Google Scholar
- [70] 1976. Lambda The Ultimate Declarative.
Technical Report AI Memo 379. MIT.Google Scholar - [71] 1990. Common Lisp (2nd ed.). Digital Press.Google Scholar
- [72] . 2012. Chaperones and impersonators: Run-time support for reasonable interposition. In Proceedings of the OOPSLA. 943–962.Google Scholar
Digital Library
- [73] . 2014. Gradual typing embedded securely in JavaScript. In Proceedings of the POPL. 425–437.Google Scholar
Digital Library
- [74] . 2015. Towards practical gradual typing. In Proceedings of the ECOOP. 4–27.Google Scholar
- [75] . 2016. Is sound gradual typing dead? In Proceedings of the POPL. 456–468.Google Scholar
Digital Library
- [76] . 2012. Gradual typing for first-class classes. In Proceedings of the OOPSLA. 793–810.Google Scholar
Digital Library
- [77] . 1990. Quasi-static typing. In Proceedings of the POPL. 367–381.Google Scholar
Digital Library
- [78] . 2006. Interlanguage migration: From scripts to programs. In Proceedings of the DLS. 964–974.Google Scholar
Digital Library
- [79] . 2008. The design and implementation of typed scheme. In Proceedings of the POPL. 395–406.Google Scholar
Digital Library
- [80] . 2010. Logical types for Untyped languages. In Proceedings of the ICFP. 117–128.Google Scholar
Digital Library
- [81] . 2017. Migratory typing: Ten years later. In Proceedings of the SNAPL. 17:1–17:17.Google Scholar
- [82] . 2018. The behavior of gradual types: A user study. In Proceedings of the DLS. 1–12.Google Scholar
- [83] . 2019. Gradual Typing for Python, Unguarded Ph.D. Dissertation. Indiana University.Google Scholar
- [84] . 2014. Design and evaluation of gradual typing for python. In Proceedings of the DLS. 45–56.Google Scholar
Digital Library
- [85] . 2019. Optimizing and evaluating transient gradual typing. In Proceedings of the DLS. 28–41.Google Scholar
Digital Library
- [86] . 2017. Big types in little runtime: Open-world soundness and collaborative blame for gradual type systems. In Proceedings of the POPL. 762–774.Google Scholar
Digital Library
- [87] . 2015. A complement to blame. In Proceedings of the SNAPL. 309–320.Google Scholar
- [88] . 2009. Well-typed programs can’t be blamed. In Proceedings of the ESOP. 1–15.Google Scholar
Digital Library
- [89] . 1979. Final algebra semantics and data type extensions. J. Comput. Syst. Sci. 19 (1979), 27–44.Google Scholar
Cross Ref
- [90] . 2017. Mixed messages: Measuring conformance and non-interference in TypeScript. In Proceedings of the ECOOP. 28:1–28:29.Google Scholar
- [91] . 1994. A practical soft type system for scheme. In Proceedings of the LFP. 250–262.Google Scholar
Digital Library
- [92] . 1994. A syntactic approach to type soundness. Info. Comput. 115, 1 (1994), 38–94.
First appeared as Technical Report TR160, Rice University, 1991. Google ScholarDigital Library
- [93] . 2010. Integrating typed and untyped code in a Scripting language. In Proceedings of the POPL. 377–388.Google Scholar
Digital Library
Index Terms
Typed–Untyped Interactions: A Comparative Analysis
Recommendations
Complete monitors for gradual types
In the context of gradual typing, type soundness guarantees the safety of typed code. When untyped code fails to respect types, a runtime check finds the discrepancy. As for untyped code, type soundness makes no promises; it does not protect untyped ...
Integrating typed and untyped code in a scripting language
POPL '10: Proceedings of the 37th annual ACM SIGPLAN-SIGACT symposium on Principles of programming languagesMany large software systems originate from untyped scripting language code. While good for initial development, the lack of static type annotations can impact code-quality and performance in the long run. We present an approach for integrating untyped ...
Partial type inference for untyped functional programs
LFP '90: Proceedings of the 1990 ACM conference on LISP and functional programmingThis extended abstract describes a way of inferring as much type information as possible about programs written in an untyped programming language. We present an algorithm that underlines the untypable parts of a program and assigns types to the rest. ...





































Comments