Hostname: page-component-745bb68f8f-s22k5 Total loading time: 0 Render date: 2025-01-10T22:22:07.187Z Has data issue: false hasContentIssue false

A simple blame calculus for explicit nulls

Published online by Cambridge University Press:  27 December 2024

ONDŘEJ LHOTÁK
Affiliation:
University of Waterloo, Waterloo, Canada (e-mail: olhotak@uwaterloo.ca)
PHILIP WADLER
Affiliation:
School of Informatics, University of Edinburgh, Edinburgh, UK (e-mail: wadler@inf.ed.ac.uk)
Rights & Permissions [Opens in a new window]

Abstract

Gradual typing provides a model for when a legacy language with less precise types interacts with a newer language with more precise types. Casts mediate between types of different precision, allocating blame when a value fails to conform to a type. The blame theorem asserts that blame always falls on the less-precisely typed side of a cast. One instance of interest is when a legacy language (such as Java) permits null values at every type, while a newer language (such as Scala or Kotlin) explicitly indicates which types permit null values. Nieto et al. in 2020 introduced a gradually typed calculus for just this purpose. The calculus requires three distinct constructors for function types and a non-standard proof of the blame theorem; it can embed terms from the legacy language into the newer language (or vice versa) only when they are closed. Here, we define a simpler calculus that is more orthogonal, with one constructor for function types and one for possibly nullable types, and with an entirely standard proof of the blame theorem; it can embed terms from the legacy language into the newer language (and vice versa) even if they are open. All results in the paper have been mechanized in Coq.

Type
Research Article
Creative Commons
Creative Common License - CCCreative Common License - BY
This is an Open Access article, distributed under the terms of the Creative Commons Attribution licence (https://creativecommons.org/licenses/by/4.0/), which permits unrestricted re-use, distribution and reproduction, provided the original article is properly cited.
Copyright
© The Author(s), 2025. Published by Cambridge University Press

1 Introduction

Null pointers are infamous for causing software errors. Hoare (Reference Hoare2009) characterized them as “The Billion Dollar Mistake.”

One way to tame the danger of nulls is via types. Whereas older languages, such as Pascal and Java, permit nulls at any reference type, more recent designs, including Kotlin, Scala, C#, and Swift, adopt type systems that track whether a reference may be null. (In Scala, the type system with explicit nulls (Reference Nieto, Zhao, Lhoták, Chang, Pu, Hirschfeld and PapeNieto et al., 2020b) is available in versions 3.0.0 and later and is enabled is enabled by the compiler flag -Yexplicit-nulls.) How do we permit code in older and newer languages to interact while preserving the type guarantees of the newer languages?

Gradual typing provides a sound theoretical basis for answering such questions, where a legacy language with a less precise type system (such as Java) interacts with a newer language with a more precise type system (such as Kotlin or Scala). Important early systems include those by Siek and Taha (Reference Siek and Taha2006) and Matthews and Findler (Reference Matthews, Findler, Hofmann and Felleisen2007). They introduce casts to model monitoring the barrier between the two languages. A cast is introduced at every place where the boundary is crossed. Each cast checks at runtime whether values passed from the less-precisely typed language violate guarantees expected by the more-precisely typed language.

A key innovation, introduced by Findler and Felleisen (Reference Findler, Felleisen, Wand and Peyton Jones2002), is that when a cast fails blame is attributed to either the source or the target of the cast. Tobin-Hochstadt and Felleisen (Reference Tobin-Hochstadt and Felleisen2006) and Matthews and Findler (Reference Matthews, Findler, Hofmann and Felleisen2007) exploit this innovation to prove that when a cast fails, blame always lies with the less-precisely typed side of the cast. Though the fact is obvious, their proof is not, depending on observational equivalence. Wadler and Findler (Reference Wadler and Findler2009) introduced the blame calculus as an abstraction of the earlier models and offered a simpler proof of the obvious fact based on a simple syntactic notion of blame safety and a straightforward proof based on progress and preservation.

Reference Nieto, Rapoport, Richards, Lhoták, Hirschfeld and PapeNieto et al. (2020a ) applied gradual typing and blame to the case of type systems that track null references. Their $\lambda_\texttt{null}$ calculus supports three function types, which vary in the guarantees they provide about whether a variable or field of that type could hold the null value instead of a function value:

  • $\#(S \to T)$ is a non-nullable function type, corresponding to a non-nullable object reference type such as String in Scala or Kotlin. Values of this type cannot be null. (There are technical exceptions where the value can be null, as explained in that paper.)

  • $?(S \to T)$ is a safe nullable function type, corresponding to a nullable object reference type such as String|Null in Scala or String? in Kotlin. Values of this type can be null, and the type system ensures nulls are properly handled.

  • $!(S \to T)$ is an unsafe nullable function type, corresponding to an object reference type such as String in Java. Values of this type can be null, but the type system guarantees nothing about proper handling of such nulls.

Their system also supports two forms of application, normal application $s\ t$ and safe application $\textit{app}(s,t,u)$ . Both apply s to t when the function is not null, but when the function is null the former gets stuck, while the latter returns u. The two forms of application align with the three function types as follows. Consider the type of a function term s.

  • $\#(S \to T)$ can be applied using standard application $s\ t$ .

  • $?(S \to T)$ can be applied using safe application $\textit{app}(s,t,u)$ .

  • $!(S \to T)$ can be applied using either standard application $s\ t$ or safe application $\textit{app}(s,t,u)$ .

Casts may be used to convert the types of terms and in particular to convert functions between these various types. At runtime, if a cast attempts to convert null from one of the latter two types to the first type the cast will fail, assigning blame appropriately to one side or the other of the cast.

On top of $\lambda_\texttt{null}$ , that paper also defines $\lambda_\texttt{null}^s$ , a calculus representing two languages, one where nulls are implicitly permitted everywhere (like Java) and one with nulls reflected explicitly in its types (like Scala or Kotlin). The syntax of the two languages is mutually recursive with an import construct that makes it possible to embed a term of one of the languages within a term of the other, modeling that it is possible to call either language from the other. The typing rules require each such embedded term to be closed, so it cannot have free variables bound in the other language. Thus, a closure in one language cannot close over bindings from the other language. The semantics of $\lambda_\texttt{null}^s$ is defined by translation to $\lambda_\texttt{null}$ , with import constructs translated to corresponding casts. The key result is that if any of these casts fails, the blame is always assigned to code from the less-precisely typed implicit language, where nulls are implicitly permitted everywhere.

The metatheory of Reference Nieto, Rapoport, Richards, Lhoták, Hirschfeld and PapeNieto et al. (2020a ) was mechanized in Coq in an accompanying artifact (Reference Nieto, Zhao, Lhoták, Chang and PuNieto et al., 2020c ). The Coq mechanization closely follows the development as presented in that paper.

This paper reiterates the development of the earlier paper, but using a simpler system and one that is closer to the standard development of blame calculus.

  • Instead of three variants of function types, our design is more orthogonal. There is a function type , and there is a nullable type , which adds nulls to an existing type . Here, and range over all types, while is restricted to definite types that do not already admit nulls. (This syntax rules out potentially confusing types such as .) The values of type are either or of the form , where is a value of type . The angle brackets designate lifting from type to .

  • Instead of two forms of application, one safe and one unsafe, our orthogonal system of types leads to a corresponding orthogonal system of terms, based on standard forms of application for functions () and case analysis for nullable values .

  • Instead of a high-level language $\lambda_\texttt{null}^s$ with explicit and implicit sublanguages that translates into a core language $\lambda_\texttt{null}$ , we define an explicit language that fulfills the role of both $\lambda_\texttt{null}$ and the explicit half of $\lambda_\texttt{null}^s$ and we define an implicit language that fulfills the role of the implicit half of $\lambda_\texttt{null}^s$ .

  • The implicit language is given a semantics by translation into the explicit language , making it easy to assign a semantics to arbitrary nesting of explicit and implicit terms. There is no longer a requirement that nested terms be closed; free variables of a term in one language can be bound in the other language.

  • The resulting development is simpler and more standard than the previous development. In particular, we adapt the Tangram Lemma of Wadler and Findler (Reference Wadler and Findler2009) to prove that blame is always assigned to the less-precisely typed language. The previous development did not use the Tangram Lemma, relying instead on a more convoluted argument.

Thus, our system can serve as a simpler and more canonical foundation for formal models of the interaction between languages with explicit and implicit nulls.

Our development extends the simply typed lambda calculus with only those features that are motivated by adding nulls. Thus, it is an initial foundation to which other language features can be added in future work to study any hypothetical feature interactions. While our theoretical results can guide the design of full programming languages, a caveat of all work on core language calculi is that it is possible for larger language to violate properties established on a core calculus. For example, although Featherweight Java was proven sound (Igarashi et al., Reference Igarashi, Pierce and Wadler2001), the presence of null values in the full Java language was found to undermine that soundness (Amin and Tate, Reference Amin, Tate, Visser and Smaragdakis2016). There is value both in more complete semantics, in which feature interactions can be explored, and in featherweight calculi, which elucidate the essence of a feature. The value of featherweight calculi was discussed further by Griesemer et al. (Reference Griesemer, Hu, Kokke, Lange, Taylor, Toninho, Wadler and Yoshida2020), who as evidence compare citations counts for the paper on Featherweight Java, Igarashi et al. (Reference Igarashi, Pierce and Wadler2001), with the four most-cited papers on more complete models, Flatt et al. (Reference Flatt, Krishnamurthi, Felleisen, MacQueen and Cardelli1998), Nipkow and von Oheimb (Reference Nipkow, von Oheimb, MacQueen and Cardelli1998), Drossopoulou and Eisenbach (Reference Drossopoulou, Eisenbach, Aksit and Matsuoka1997), and Syme (Reference Syme1999): 1226 as compared with 592, 270, 188, and 172, respectively (Google Scholar, May 2024).

This paper is organized as follows. Section 2 defines the explicit language . Section 3 proves its key properties: type safety, blame safety, and the Tangram Lemma. Section 4 defines the implicit language and its translation to and proves that the translation preserves types. Section 5 explores interoperability of the two languages: we show how terms of each language can be embedded in the other, define the casts needed to mediate between the two, and prove that any failure of these casts always blames the implicit language . Section 6 further compares our calculi to those of Reference Nieto, Rapoport, Richards, Lhoták, Hirschfeld and PapeNieto et al. (2020a ) and discusses connections to full languages such as Scala and Java. Section 7 surveys related work. Section 8 concludes.

We have mechanized all of our lemmas and propositions in Coq and included the mechanization with the paper as additional material. The mechanization follows the locally nameless approach of Aydemir et al. (Reference Aydemir, Charguéraud, Pierce, Pollack, Weirich, G. C. and Wadler2008) and uses the Ott (Sewell et al., Reference Sewell, Nardelli, Owens, Peskine, Ridge, Sarkar and Strnisa2010) and LNGen (Aydemir and Weirich, Reference Aydemir and Weirich2010) tools to automatically generate the associated Coq infrastructure and lemmas from a file of definitions that directly follow those in the paper. Aside from the use of a locally nameless representation, the mechanization directly follows the development as it is presented in the paper.

2 The explicit language

In this section, we introduce a language that tracks the possibility of null references explicitly in types. We call it for short. Following the advice of Patrignani (Reference Patrignani2021), we use a for elements of to distinguish them from those of another language that we will introduce in Section 4. In this section, we define syntax and typing rules, a reduction relation, and four blame subtyping relations for . In Section 3, we prove standard type safety and blame safety properties.

The syntax of is shown in Figure 1. The basic values are constants of a base type , function abstractions of a function type , and the null constant . In addition to the two definite types and , the type system includes nullable types , where is any definite type. The constructors of are the null constant and the lift operation , where is a term of type . When is a value, is also considered a value, and when is a function value, the cast is also a function value.

Fig. 1. syntax.

In addition to values, the calculus includes terms for function application , casts , a case construct that destructs terms of nullable types , and a failure result . As is standard in gradual type systems, each cast has a blame label so that the result of a failing computation can be traced to the cast that failed. A cast with blame label may fail at run time, yielding either or Here, is positive blame and indicates that fault lies with the term contained in the cast, while is negative blame and indicates that fault lies with the context containing the cast. The overbar is an involutive operator on blame labels: .

As an example of positive blame, consider the term:

During reduction, the function will be applied to a constant of type , yielding , causing a cast failure because is not a value of the definite type given as the return type of the function in the target type of the cast. The fault lies with the function within the cast, which returned although the cast promised that it would return a non- value. This example term evaluates to positive .

As an example of negative blame, consider the similar term:

During reduction, the function will be applied to , causing a cast failure because is not a value of the definite type specified as the parameter type of the function. The fault lies with the context containing the cast, namely the value to which the cast term is being applied. This example term evaluates to negative .

The typing rules of are shown in Figure 2. The rules for variables, base type constants and operations, and function abstraction and application are standard. The Null and Lift rules identify the null constant and the lift operation as the constructors of a nullable type . The Case rule specifies that destructs terms of a nullable type . The Cast rule allows casts from type to type as long as and are compatible, written . Informally, two types are compatible if they have the same structure but differ only in the nullability of their components. Finally, the Blame rule specifies that a failure result is possible at any type .

Fig. 2. typing rules.

The operational semantics of is shown in Figure 3. The Binop rule reduces a base operation applied to values and to a base constant specified by an external base operation evaluation function (which requires and returns only base constants, not nulls). The App rule is standard $\beta$ -reduction. Two rules reduce a . When the scrutinee is , the reduces to the term in the branch (Case-Null). When the scrutinee is a lifted value , the reduces to the term in the non-null branch, with substituted for the parameter (Case-Lift). The Wrap rule defines $\beta$ -reduction for a function wrapped in a cast, ensuring that the argument and the final result of the function application are cast accordingly. During reduction, the argument will first be cast from to , then the function will be applied to it, and finally the result will be cast from to . There are four rules for reducing casts from a nullable type . A cast of to another nullable type reduces to just (Cast-Null). A cast of to a non-nullable type reduces to (Downcast-Null). A cast of a lifted value from type to a non-nullable type evaluates to wrapped in a cast from to (Downcast-Lift). When such a lifted value is cast to a nullable type , this result is additionally lifted (Cast-Lift). A cast from base type to itself is the identity (Cast-Base). A grammar of evaluation contexts ensures call-by-value reduction in function applications, inside casts, cases, lift operations, and base type operations. The Ctx rule specifies reduction inside an evaluation context. The Err rule specifies that a failure inside an evaluation context floats up to the top level, terminating the reduction sequence.

Fig. 3. reduction rules.

3 Properties of the explicit language

3.1 Type safety

We prove type safety of by proving preservation and progress (Wright and Felleisen, Reference Wright and Felleisen1994). For each result, we specify the name of the corresponding result in the Coq mechanization accompanying the paper.

Proposition 1 (Preservation). (Coq: preservation)

If and then .

Proof The proof is by induction on the reduction relation. The App and Case-Lift cases depend on a substitution lemma, which is shown below. The Wrap case depends on symmetry of the compatibility relation .

Lemma 2 (Substitution). (Coq: substitution)

If and , then .

Proof The proof is standard, by induction on the typing of N. The Var case depends on a weakening lemma, also proved by straightforward induction.

Lemma 3 (Compatibility Symmetry). (Coq: compat_sym) If then .

Proof The proof is by straightforward induction on the derivation of .

Proposition 4 (Progress). (Coq: progress)

If then either is a value, for some , or for some .

Proof The proof is by induction on the typing derivation. The App case depends on a canonical forms lemma for function types, which is shown below.

Lemma 5 (Canonical Forms Arrow). (Coq: canonical_forms_arrow)

If , then either for some and , or for some , and .

Proof The proof is by straightforward induction on the typing derivation.

3.2 Blame safety

In addition to type safety, we have also proved blame safety following the approach of Wadler and Findler (Reference Wadler and Findler2009) (see also Wadler (Reference Wadler, Ball, Bodík, Krishnamurthi, Lerner and Morrisett2015) for a more accessible summary of the approach). The applicability of this standard approach is one of the benefits of relative to the calculus of Reference Nieto, Rapoport, Richards, Lhoták, Hirschfeld and PapeNieto et al. (2020a ).

The approach depends on four subtyping relations, defined for in Figure 4. Intuitively, a cast between types related by positive subtyping cannot give rise to positive blame (blame with the same label as the cast) and a cast between types related by negative subtyping cannot give rise to negative blame (blame with a label that is the complement of that on the cast). Ordinary subtyping is an intersection of these two relations, so a cast between types related by ordinary subtyping cannot give rise to any blame. We will discuss naive subtyping and its relationship to the other three subtyping relations in Section 3.3. Positive, negative, and ordinary subtyping on functions are contravariant in the domain and covariant in the range, while naive subtyping is covariant in both the domain and the range.

Fig. 4. subtyping rules.

We make the intuitive understanding of positive and negative subtyping precise as follows:

Definition 6 (Safe Term). (Coq: safe) A term is safe for blame label , written , if has no subterm of the form , every cast in of the form satisfies , and every cast in of the form satisfies . An explicit inductive definition is given in Figure 5.

Fig. 5. Definition of safe terms.

With this definition, we can prove blame safety of , that when , cannot reduce to in any number of steps. Note, however, that can reduce to with a different label , and, in particular, can reduce to .

Proposition 7 (Safe Term Preservation). (Coq: safe_preservation)

If and and , then .

Proof The proof is by induction on the derivation of . Each case is straightforward except in the App and Case-Lift cases, we need the following lemma to show that the safety relation is preserved by substitution.

Lemma 8 (Substitution Preserves Safe Terms). (Coq: subst_pres_safe)

If and , then .

Proof By straightforward induction on the structure of .

Corollary 9 (Safe Term Progress). (Coq: safe_progress)

If and and , then .

Proof This follows directly from Proposition 7 and the definition of .

Proposition 10 (Blame Safety). (Coq: safety)

If and and , then .

Proof The proof is by induction on the transitive reduction relation. In the inductive case, it uses Propositions 1 and 7.

3.3 Naive subtyping and the Tangram lemma

Naive subtyping relates types according to how definite they are in the sense of gradual typing. Where we write , Siek et al. (Reference Siek, Vitousek, Cimini, Boyland, Ball, Bodík, Krishnamurthi, Lerner and Morrisett2015) write . In our specific setting, is a naive subtype of if they have the same structure, but some non-nullable components of may be replaced by nullable components in . One function is a naive subtype of another if both the domains and ranges are naive subtypes; note that this is covariant in both the domain and range of the function, as opposed to the other three subtyping relations which are contravariant in the domain and covariant in the range.

The Tangram Lemma of Wadler and Findler (Reference Wadler and Findler2009) relates these four subtyping relations. Part 1 concerns ordinary subtyping and part 2 concerns naive subtyping; note that we have in part 1 and in part 2. We show that the Tangram Lemma holds for the relations defined in Figure 4.

Proposition 11 (Tangram Lemma). (Coq: tangram)

  1. 1. if and only if and .

  2. 2. if and only if and .

Proof Each of the four cases is proved by a straightforward induction on the derivation of , the derivation of , or mutual induction on the derivations of and .

The practical consequence of the Tangram Lemma is that if and is safe for , then the two casts and are also safe for . When we study interoperability between languages with implicit and explicit nulls in Section 5, we will use second half of the Tangram Lemma to conclude that all such casts are safe for blame labels that assign blame to the language with explicit nulls. If something goes wrong, it will be blamed on the language with implicit nulls.

4 The implicit language

Having defined , an explicit language with casts (like Scala), we now define an implicit language that ignores nullability in its types (like Java). We call the implicit language for short.

4.1 Syntax

The syntax of is defined in Figure 6. Following the advice of Patrignani (Reference Patrignani2021), we use a to distinguish elements of from . The syntax of terms mirrors that of , but omits lifting, case, casts, and blame, since those are useless without the distinction between nullable and non-null types.

Fig. 6. syntax.

Types of are not distinguished as nullable or non-null. All types in admit the null constant.

4.2 Typing

The typing rules of are standard and are shown in Figure 7. The constant can have any type .

Fig. 7. typing rules.

4.3 Semantics

We define the semantics of by translation to , whose operational semantics we defined in Section 2. The translation is presented in Figure 8.

Fig. 8. Translation from to

Types of are translated to types in that are equivalent in that they admit equivalent sets of values: in particular, the translated types admit the null constant.

In the translation of terms of , we will make frequent use of . We introduce as shorthand the Elvis operator (as in Kotlin and other languages) that takes a term of nullable type and reduces to when evaluates to and to when evaluates to .

Variable references and the constant are just translated to themselves. Base constants , base operations , and function abstractions have types in that translate to nullable types in , so these terms are translated to lifted terms in . The translation of a base operation uses the Elvis operator to check the whether the operands and are null before performing the operation . The blame label is used to signal that a null check in a base operation failed. (Some might prefer the blame labels and to indicate that the fault lies with the containing context, but arguably the blame lies with the contained term for not providing a non-null value. In any event, the desirability of using overbars is not clear in the absence of a cast, so we follow the advice of Tufte (Reference Tufte2001) to minimize the ink on the page.) Similarly, the translation of a function application first checks whether (the function) evaluates to null before evaluating the argument and performing the application.

4.4 Type preservation of the translation

The translation from to preserves typing:

Proposition 12 (Translation Preserves Typing). (Coq: desugaring_typing)

If , then , where a typing context is obtained by replacing each binding of the form in with .

Proof The proof is by induction on the derivation of . The Imp-Abs rule is parameterized by an arbitrary fresh variable , so a proof for this case must be valid for any such fresh . To prove this case, we need to show that the translation function commutes with $\alpha$ -renaming of variables. In fact, we prove a stronger result that this function commutes with substitution of an arbitrary term for a free variable, which we show below in Lemma 13. All other cases are straightforward.

Lemma 13 (Substitution Commutes With Translation). (Coq: open_trm_of_itrm)

Proof The proof is by straightforward induction on the structure of .

5 Interoperability

In this section, we will explore how terms of can use terms of and vice versa.

5.1 Implicit terms within explicit terms

To use a term of within a term of , we just translate the term first and use within the term. However, the translated term has an inconvenient type, so it cannot be used directly. For example, the translated constant term has the nullable type , so it cannot be an operand of the operator, which requires operands of type . Similarly, the translated function term has the nullable type (where is a type of the body ), so it cannot be used directly in a function application.

We can use an term of type in directly by embedding it within a cast to the desired type:

(1)

Here, is a naive translation of the type , defined in general as follows:

The naive translation maps a base type of to a base type of , and it maps a function type of to a function type of . Thus, the cast enables the term to be used in directly with a definite base or function type. The blame label indicates that the cast is from to .

The cast could fail but only with positive blame, blaming the subterm rather than the surrounding context. This is because is a naive subtype of :

Proposition 14 ( Naive Subtyping). (Coq: imp_naive_subtyp)

For every type .

Proof The proof is by straightforward induction on the structure of .

Then by the Tangram Lemma, term (1) may reduce to but can never reduce to .

5.2 Explicit terms within implicit terms

To use a term of within a term of , we need to apply the translation to the context that surrounds . In symbols, our approach is to translate a mixed term into the term using a natural extension of the translation from Figure 8 to contexts.

In comparison, the approach taken in the previous section was to translate a mixed term into the term . That only required applying the translation to the subterm rather than to an context .

Suppose the subterm has some type . Typing the surrounding context requires an type for the term intended to plug the hole. We define the erasure of a type from to as follows:

We then say that an subterm of type can be used within an context that can be typed under the assumption that an term that plugs its hole has type .

When we translate the context , the resulting context will be typable assuming that the term that plugs its hole has type . Note that it is possible to define directly as follows:

We can use an term of type in directly by embedding it within a cast to the desired type:

(2)

The cast could fail but only with negative blame, blaming the surrounding context rather than the subterm . This is because is a naive subtype of :

Proposition 15 ( Naive Subtyping). (Coq: exp_naive_subtyp)

For every type .

Proof The proof is by straightforward induction on the structure of .

Then by the Tangram Lemma, term (2) may reduce to but can never reduce to .

5.3 Metatheory of contexts

So far, we have taken for granted that when a term is embedded in a context, C[M], the term M can refer to free variables bound in the context C. This deserves to be established formally, particularly when we mix contexts and subterms from and . We make this precise in this section.

One may be tempted by a simpler construction. In an earlier version of this paper, we used the construct , where abbreviates when is the type of . However, that approach has the significant disadvantage of requiring to be closed. Here, in the mixed term , the subterm can refer to free variables bound in the context , an important generalization.

Figure 9 defines grammars of and contexts following the syntax of terms, but with a hole $\Box$ that can be plugged by a subterm.

Fig. 9. Syntax of and contexts.

We introduce the notation for typing judgments for contexts and similarly for contexts. Such a judgment is to be interpreted as follows: if the hole in context is filled with some term such that , then the term that results from plugging the hole in context with the term has type in typing context . Inductive definitions of the typing relations for and contexts are presented in Figures 10 and 11. By design, both the and context typing rules satisfy the intended property:

Fig. 10. context typing rules.

Fig. 11. context typing rules.

Proposition 16 ( Plugged Typing.) (Coq: typing_plugged)

For any context and term , if and for some typing contexts , and types .

Proof The proof is by induction on the typing relation for contexts (Figure 10).

Proposition 17 ( Plugged Typing). (Coq: ityping_plugged)

For any context and term , if and for some typing contexts and types .

Proof The proof is by induction on the typing relation for contexts (Figure 11).

Having defined typing for contexts, we can state a formal criterion for allowing an term to be used within an context, as we sketched in Section 5.2:

Definition 18 (Allowable term for context). An term may plug an context if there exist typing contexts , and type such that and .

In the definition, records the types of the variable bindings in . The subterm has some type in the context , which applies erasure to the types in .

The context needs to be typed under the assumption that the hole has type , the naive translation of , in the typing context , which is the translation of the erased context . To see why is necessary instead of just , consider that an context cannot distinguish whether binds some variable to a definite type such as or a nullable type such as ; both are erased to . Typing the hole in the translated erased typing context solves the problem, since .

This criterion ensures that when an implicit term is plugged into an explicit context , its translation is well typed.

Proposition 19 (Typing of term in context.) (Coq: nest_itrm_in_ctx)

For any context and term that satisfy Definition 18 with typing contexts ,

Proof This follows directly by Proposition 12 and Proposition 16. Proposition 14 is needed to justify the compatibility of the types and in the cast.

Turning our attention to terms embedded in contexts, we define a similar typing criterion for such mixed terms :

Definition 20 (Allowable term for context). An term may plug an context if there exist typing contexts , type , and type such that and .

The term is typed in a typing context , which contains the variables bound in , but with their types translated from to using the translation from Figure 8. In that context, has some type , which erases to the type , so the context must be typable under the assumption that its hole has the erased type .

We can apply the translation from Figure 8. to the context to obtain an context . The translation of contexts is defined to be the same as the translation of terms, with the additional case that a hole translates to a hole. The resulting context will satisfy by the following proposition:

Proposition 21 (Translated Context Typing) (Coq: ictrm_desugaring_typing)

For any context , types and typing contexts , if , then .

Proof The proof is by induction on the derivation of the context typing of . The case of function abstraction requires the following Lemma 22, an analog of Lemma 13 for contexts rather than terms.

Lemma 22 (Substitution Commutes With Translation of Contexts). (Coq: open_ctrm_of_ictrm)

Proof The proof is by straightforward induction on the structure of .

To plug the resulting context with the term , we must use a cast to adapt the type of from to the type that the context requires. We summarize the type safety of the translation as follows.

Proposition 23 (Typing of term in context) (Coq: nest_trm_in_ictx)

For any context and term that satisfy Definition 20 with typing contexts , , type , and type ,

Proof This follows directly by Proposition 21 and Proposition 16. Proposition 15 is needed to justify the compatibility of the types and in the cast.

5.4 A defensive alternative

Although the casts presented in the previous two subsections always blame the implicit language when they fail, it is also possible to prevent any possibility of failure by using to explicitly test for null and manually (and tediously) providing explicit error-handling terms to be evaluated when a null value is encountered.

To show how tedious, consider the term to be embedded in the context . The translated term has type , but the context requires the hole to be plugged by a term of type . Instead of using a cast to mediate between and , we could interpose the context

The resulting term would be . The occurrences of in identify the places where can occur at run time and could be replaced by user-defined defensive compensation code.

In the other direction, consider the term to be embedded in the context . The term has type , but the translated context requires the hole to be plugged by a term of type . Instead of using a cast to mediate between and , we could interpose the context

The resulting term would be . The occurrences of in identify the places where can occur at run time and could be replaced by user-defined defensive compensation code.

Given any pair of compatible types, we can synthesize the necessary compensation code using the following function, which follows the structure of the inductive definition of compatibility from Figure 2:

As before, occurrences of can be replaced by user-defined defensive compensation code.

6 Connections with other languages

Now that we have presented our explicit and implicit languages in full, we briefly discuss the connections with the languages of Reference Nieto, Rapoport, Richards, Lhoták, Hirschfeld and PapeNieto et al. (2020a ) and with complete languages like Scala and Java.

6.1 Connections with $\lambda_\texttt{null}$ and $\lambda_\texttt{null}^s$

Recall that the core language $\lambda_\texttt{null}$ of Reference Nieto, Rapoport, Richards, Lhoták, Hirschfeld and PapeNieto et al. (2020a ) defines three function types, which correspond to types in and as follows:

  • $\#(S \to T)$ in $\lambda_\texttt{null}$ corresponds to in .

  • $?(S \to T)$ in $\lambda_\texttt{null}$ corresponds to in .

  • $!(S \to T)$ in $\lambda_\texttt{null}$ corresponds to in .

Here, we assume that S and T correspond to and or and .

The semantics of safe application $\textit{app}(s,t,u)$ in $\lambda_\texttt{null}$ is analogous to the term , where s, t, u correspond to . This term requires to be of type , while standard function application requires a term of type . Since a $\lambda_\texttt{null}$ term of type $!(S \to T)$ can be used in both forms of application, it does not correspond to any one type in , but more closely to the type : values of that type include both and functions, and terms of that type can be used in function application.

This illuminates a key difference between and $\lambda_\texttt{null}$ . In , all implicit features have been desugared away into casts, and casts are the only terms that can fail with blame. In contrast, $\lambda_\texttt{null}$ retains unsafe nullable function types, an implicit language feature, so applications there can also fail with blame. This difference motivates the various auxiliary relations needed in $\lambda_\texttt{null}$ , such as blame assignment and normalization.

The surface language $\lambda_\texttt{null}^s$ provides $\textrm{import}_e$ and $\textrm{import}_i$ terms that enable an explicit term to be used within an implicit term and vice versa, like the embeddings that we discussed in Sections 5.1 and 5.2. A key difference is that the $\lambda_\texttt{null}^s$ typing rules require the embedded subterms to be closed, so they cannot refer to variables bound in the surrounding context in the other sublanguage.

6.2 Connections with Scala and Java

In this section, we briefly compare the core explicit and implicit languages with Scala and Java.

The types of the implicit language correspond to the reference types of Java, in that every type admits the null value. The nullable types of the explicit language correspond to union types T|Null in Scala, where T is a Scala type corresponding to the definite type . Thus, interoperability between Scala and Java directly follows the interoperability between the explicit and implicit languages. We can embed a Java term, such as a call to a Java method, in Scala following the translation from Section 5.2. We can embed a Scala term, such as a call to a Scala method, in Java following the translation from Section 5.1. Such embedded terms are not limited to calls and can refer to variables bound in the surrounding context in the opposite language, to model higher-level language features such as terms referring to fields and variables defined in other classes, including in code written in the opposite language. The same applies to Kotlin instead of Scala, where a nullable type is written as T?.

There is a minor difference in the values. The explicit language distinguishes a value of definite type and a lifted value of nullable type . In Scala, both values have the same runtime representation and the lifting operation is a no-op at run time.

As core calculi based on simply typed lambda calculus, the explicit and implicit languages obviously omit many features present in Scala and Java, notably objects, classes and class types, and generic methods and generic class types. The calculi serve as a foundation to guide the design of null safety for these additional language features. In particular, one practical challenge in a full language arises when a single type describes a large, linked data structure, such as a linked list or tree. If the type does not say anything about null values within the structure, such values could occur anywhere within it. Extensions of the core languages for classes and objects would add casts to identify places where blame must be assigned for inter-language operations on objects. Those casts would identify the places in practical programs where nulls can arise.

7 Related work

Findler and Felleisen (Reference Findler, Felleisen, Wand and Peyton Jones2002) introduced the concept of blame to function contracts, allowing to assign responsibility for a runtime failure either to a function itself or to the arguments passed to the function. Siek and Taha (Reference Siek and Taha2006, Reference Siek and Taha2007) introduced the concept of gradual typing to enable interoperability between parts of a program with and without static types. Wadler and Findler (Reference Wadler and Findler2009) combined the two concepts and proved that in a gradually typed program, any cast failure on the boundary can always be blamed on the untyped (or, more generally, the less-precisely typed) part of the program. They generalized their result in the Tangram Lemma, which can be instantiated for other gradually typed calculi. Also see Wadler (Reference Wadler, Ball, Bodík, Krishnamurthi, Lerner and Morrisett2015).

Garcia et al. (Reference Garcia, Clark, Tanter, Bodík and Majumdar2016) used the framework of abstract interpretation (Cousot and Cousot, Reference Cousot, Cousot, Graham, Harrison and Sethi1977) to explain what it means for part of a program to be more-precisely or less-precisely typed. This abstract interpretation notion of the precision of a type is identical to naive subtyping. Estep et al. (Reference Estep, Wise, Aldrich, Tanter, Bader, Sunshine, Møller and Sridharan2021) applied this methodology to a static analysis that determines which expressions in a program may evaluate to null at run time. Their baseline static analysis works for an intermediate language in which every variable is explicitly annotated to be either non-null or nullable; they systematically derive a gradual static analysis that works for an intermediate language that allows a subset of variables to be left unannotated. Malewski et al. (Reference Malewski, Greenberg and Tanter2021) applied the abstract interpretation framework to a calculus with algebraic data types (ADTs) to systematically derive a calculus with gradual ADTs. Their (open) gradual datatype designates expressions that evaluate to a value of some ADT, but the specific ADT and its set of constructors is not known statically and can even be open in the sense of allowing extension with new constructors.

Reference Nieto, Rapoport, Richards, Lhoták, Hirschfeld and PapeNieto et al. (2020a ) instantiated the concepts of gradual typing and blame for their explicit-null extension of the Scala language (Reference Nieto, Zhao, Lhoták, Chang, Pu, Hirschfeld and PapeNieto et al., 2020b ). There, the less-precisely typed parts of a program are those written in Java or older versions of Scala, and the more-precisely typed parts are those written in the new version of Scala in which the possibility of a reference being null is made explicit in its type.

Similar issues occur in other languages that make nulls explicit in their type system but interoperate with older code in type systems agnostic to null.

The Kotlin language (JetBrains, 2022) aims for null safety within Kotlin code but adapts Java types to avoid any compile-time errors related to nullability at the boundary between code written in Kotlin and Java. It uses a concept called platform types, which are a subtype of a non-null type but a supertype of a nullable type, to avoid reporting errors in both covariant and contravariant contexts. Platform types are inherently unsound since, by transitivity, they make a nullable type a subtype of the corresponding non-null type, but they are necessary in practice to avoid the overwhelming number of compile-time errors that would result if we insisted on static null safety at the boundary between Java and Kotlin.

Recent versions of the C# language (Microsoft, 2022) have nullable types that indicate that a reference can be null. Types in code written in older versions of the language are interpreted to mean that references are non-null. To enable interoperability, conversions from a nullable to a non-null type and vice versa are allowed but generate a compile-time warning in areas of code designated to issue such warnings. A null value may still flow at run time to a context in which a non-null value is expected, resulting in a run-time exception.

The Swift language (Apple, 2022) has optionals similar to discriminated options like Scala’s Option and Haskell’s Maybe, and implicitly unwrapped optionals which are automatically cast to a non-null type in contexts that require one. When Swift code interoperates with code in Objective-C, which does not make nullability explicit in its types, Objective-C expressions are given an implicitly unwrapped optional type in Swift. Attempting to use an implicitly unwrapped optional that is nil at run time results in a run-time exception.

8 Conclusion

We have defined a pair of core calculi for modeling interoperability between languages that track null references explicitly in their type systems and ones that do not. Our definitions follow the standard blame calculus of Wadler and Findler (Reference Wadler and Findler2009); in particular, their Tangram Lemma approach can be used to assign blame for cast failures to the less precise language whose type system ignores nullability. These core calculi can serve as a basis for modeling nullness interoperability in larger languages, in the same way that foundations such as Featherweight Java (Igarashi et al., Reference Igarashi, Pierce and Wadler2001) and DOT (Amin et al., Reference Amin, Grütter, Odersky, Rompf, Stucki, Lindley, McBride, Trinder and Sannella2016) have guided the design of Java and Scala. Our development is formalized in Coq and is included as an artifact accompanying the paper.

Conflict of interest.

The authors report no conflict of interest.

Supplementary material

For supplementary material for this article, please visit https://doi.org/10.1017/S0956796824000121.

References

Amin, N., Grütter, S., Odersky, M., Rompf, T. & Stucki, S. (2016) The essence of dependent object types. In A List of Successes That Can Change the World - Essays Dedicated to Philip Wadler on the Occasion of His 60th Birthday (Lecture Notes in Computer Science, Vol. 9600), Lindley, S., McBride, C., Trinder, P. W. & Sannella, D. (eds.), Springer, pp. 249–272. https://doi.org/10.1007/978-3-319-30936-1_14 CrossRefGoogle Scholar
Amin, N. & Tate, R. (2016) Java and Scala’s type systems are unsound: The existential crisis of null pointers. In Proceedings of the 2016 ACM SIGPLAN International Conference on Object-Oriented Programming, Systems, Languages, and Applications, OOPSLA 2016, part of SPLASH 2016, Amsterdam, The Netherlands, October 30 – November 4, 2016, Visser, E. & Smaragdakis, Y. (eds.), ACM, pp. 838–848.CrossRefGoogle Scholar
Apple (2022) The Swift Programming Language. https://docs.swift.org/swift-book/ (accessed 17 March 2022).Google Scholar
Aydemir, B. & Weirich, S. (2010) LNgen: Tool Support for Locally Nameless Representations. Technical Report MS-CIS-10-24. Computer and Information Science, University of Pennsylvania.Google Scholar
Aydemir, B. E., Charguéraud, A., Pierce, B. C., Pollack, R. & Weirich, S. (2008) Engineering formal metatheory. In Proceedings of the 35th ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, POPL 2008, San Francisco, California, USA, January 7–12, 2008, G. C., Necula & Wadler, P. (eds.), ACM, pp. 315. https://doi.org/10.1145/1328438.1328443 CrossRefGoogle Scholar
Cousot, P. & Cousot, R. (1977) Abstract interpretation: A unified lattice model for static analysis of programs by construction or approximation of fixpoints. In Conference Record of the Fourth ACM Symposium on Principles of Programming Languages, Los Angeles, California, USA, January 1977, Graham, R. M., Harrison, M. A. & Sethi, R. (eds.), ACM, pp. 238252. https://doi.org/10.1145/512950.512973 CrossRefGoogle Scholar
Drossopoulou, S. & Eisenbach, S. (1997) Java is type safe - Probably. In ECOOP’97 - Object-Oriented Programming, 11th European Conference, Jyväskylä, Finland, June 9-13, 1997, Proceedings (Lecture Notes in Computer Science, Vol. 1241), Aksit, M. & Matsuoka, S. (eds.), Springer, pp. 389418. https://doi.org/10.1007/BFB0053388 CrossRefGoogle Scholar
Estep, S., Wise, J., Aldrich, J., Tanter, É., Bader, J. & Sunshine, J. (2021) Gradual program analysis for null pointers. In 35th European Conference on Object-Oriented Programming, ECOOP 2021, July 11-17, 2021, Aarhus, Denmark (Virtual Conference) (LIPIcs, Vol. 194), Møller, A. & Sridharan, M. (eds.), Schloss Dagstuhl - Leibniz-Zentrum für Informatik, pp. 3:1–3:25. https://doi.org/10.4230/LIPICS.ECOOP.2021.3 CrossRefGoogle Scholar
Findler, R. B. & Felleisen, M. (2002) Contracts for higher-order functions. In Proceedings of the Seventh ACM SIGPLAN International Conference on Functional Programming (ICFP ’02), Pittsburgh, Pennsylvania, USA, October 4-6, 2002, Wand, M. & Peyton Jones, S. L. (eds.), ACM, pp. 4859.CrossRefGoogle Scholar
Flatt, M., Krishnamurthi, S. & Felleisen, M. (1998) Classes and Mixins. In POPL ’98, Proceedings of the 25th ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, San Diego, CA, USA, January 19-21, 1998, MacQueen, D. B. & Cardelli, L. (eds.), ACM, pp. 171183. https://doi.org/10.1145/268946.268961 CrossRefGoogle Scholar
Garcia, R., Clark, A. M. & Tanter, É. (2016) Abstracting gradual typing. In Proceedings of the 43rd Annual ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, POPL 2016, St. Petersburg, FL, USA, January 20–22, 2016, Bodík, R. & Majumdar, R. (Eds.). ACM, pp. 429442. https://doi.org/10.1145/2837614.2837670 CrossRefGoogle Scholar
Griesemer, R., Hu, R., Kokke, W., Lange, J., Taylor, I. L., Toninho, B., Wadler, P. & Yoshida, N. (2020) Featherweight go. Proc. ACM Program. Lang. 4, 149:1–149:29. https://doi.org/10.1145/3428217 CrossRefGoogle Scholar
Hoare, T. (2009) Null References: The Billion Dollar Mistake. https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/ (accessed 17 March 2022).Google Scholar
Igarashi, A., Pierce, B. C. & Wadler, P. 2001. Featherweight Java: A minimal core calculus for Java and GJ. ACM Trans. Program. Lang. Syst. 23(3), 396450. https://doi.org/10.1145/503502.503505 CrossRefGoogle Scholar
JetBrains (2022) Kotlin Programming Language. https://kotlinlang.org/ (accessed 17 March 2022).Google Scholar
Malewski, S., Greenberg, M. & Tanter, É. (2021) Gradually structured data. Proc. ACM Program. Lang. 5, 129. https://doi.org/10.1145/3485503 CrossRefGoogle Scholar
Matthews, J. & Findler, R. B. (2007) Operational semantics for multi-language programs. In Proceedings of the 34th ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, POPL 2007, Nice, France, January 17-19, 2007, Hofmann, M. & Felleisen, M. (eds.), ACM, pp. 310. https://doi.org/10.1145/1190216.1190220 CrossRefGoogle Scholar
Microsoft (2022) C# Language Specification. https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification (accessed 17 March 2022).Google Scholar
Nieto, A., Rapoport, M., Richards, G. & Lhoták, O. (2020a) Blame for null. In 34th European Conference on Object-Oriented Programming, ECOOP 2020, November 15-17, 2020, Berlin, Germany (Virtual Conference) (LIPIcs, Vol. 166), Hirschfeld, R. & Pape, T. (eds.), Schloss Dagstuhl - Leibniz-Zentrum für Informatik, 3:1–3:28. https://doi.org/10.4230/LIPIcs.ECOOP.2020.3 CrossRefGoogle Scholar
Nieto, A., Zhao, Y., Lhoták, O., Chang, A. & Pu, J. (2020b) Scala with explicit nulls. In 34th European Conference on Object-Oriented Programming, ECOOP 2020, November 15-17, 2020, Berlin, Germany (Virtual Conference) (LIPIcs, Vol. 166), Hirschfeld, R. & Pape, T. (eds.), Schloss Dagstuhl - Leibniz-Zentrum für Informatik, pp. 25:1–25:26. https://doi.org/10.4230/LIPIcs.ECOOP.2020.25 CrossRefGoogle Scholar
Nieto, A., Zhao, Y., Lhoták, O., Chang, A. & Pu, J. (2020c) Scala with explicit nulls (artifact). Dagstuhl Artifacts Ser. 6(2), 14:114:2. https://doi.org/10.4230/DARTS.6.2.14 Google Scholar
Nipkow, T. & von Oheimb, D. (1998) Javalight is Type-Safe - Definitely. In POPL ’98, Proceedings of the 25th ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, San Diego, CA, USA, January 19-21, 1998, MacQueen, D. B. & Cardelli, L. (eds.), ACM, pp. 161170. https://doi.org/10.1145/268946.268960 CrossRefGoogle Scholar
Patrignani, M. (2021) Why Should Anyone use Colours? or, Syntax Highlighting Beyond Code Snippets. [arxiv]2001.11334 [cs.SE]Google Scholar
Sewell, P., Nardelli, F. Z., Owens, S., Peskine, G., Ridge, T., Sarkar, S. & Strnisa, R. (2010) Ott: Effective tool support for the working semanticist. J. Funct. Program. 20(1), 71122.CrossRefGoogle Scholar
Siek, J. G. & Taha, W. (2006) Gradual typing for functional languages. In Scheme and Functional Programming Workshop, Vol. 6, pp. 81–92.Google Scholar
Siek, J. G. & Taha, W. (2007) Gradual typing for objects. In ECOOP 2007 - Object-Oriented Programming, 21st European Conference, Berlin, Germany, July 30 - August 3, 2007, Proceedings (Lecture Notes in Computer Science, Vol. 4609), Ernst, E. (ed.), Springer, pp. 227. https://doi.org/10.1007/978-3-540-73589-2_2 CrossRefGoogle Scholar
Siek, J. G., Vitousek, M. M., Cimini, M. & Boyland, J. T. (2015) Refined criteria for gradual typing. In 1st Summit on Advances in Programming Languages, SNAPL 2015, May 3-6, 2015, Asilomar, California, USA (LIPIcs, Vol. 32), Ball, T., Bodík, R., Krishnamurthi, S., Lerner, B. S. & Morrisett, G. (eds.), Schloss Dagstuhl - Leibniz-Zentrum fuer Informatik, pp. 274–293.Google Scholar
Syme, D. (1999) Proving Java type soundness. In Formal Syntax and Semantics of Java (Lecture Notes in Computer Science, Vol. 1523), Alves-Foss, J. (ed.), Springer, pp. 83–118. https://doi.org/10.1007/3-540-48737-9_3 CrossRefGoogle Scholar
Tobin-Hochstadt, S. & Felleisen, M. (2006) Interlanguage migration: From scripts to programs. In Dynamic Languages Symposium, OOPSLA Companion. ACM, pp. 964–974.CrossRefGoogle Scholar
Tufte, E. R. (2001) The Visual Display of Quantitative Information (second ed.). Graphics Press, Cheshire, Connecticut.Google Scholar
Wadler, P. (2015) A complement to blame. In 1st Summit on Advances in Programming Languages, SNAPL 2015, May 3-6, 2015, Asilomar, California, USA (LIPIcs, Vol. 32), Ball, T., Bodík, R., Krishnamurthi, S., Lerner, B. S. & Morrisett, G. (eds.), Schloss Dagstuhl - Leibniz-Zentrum fuer Informatik, pp. 309–320.Google Scholar
Wadler, P. & Findler, R. B. (2009) Well-typed programs can’t be blamed. In Programming Languages and Systems, 18th European Symposium on Programming, ESOP 2009, Held as Part of the Joint European Conferences on Theory and Practice of Software, ETAPS 2009, York, UK, March 22-29, 2009. Proceedings (Lecture Notes in Computer Science, Vol. 5502), Castagna, G. (ed.), Springer, pp. 116.CrossRefGoogle Scholar
Wright, A. K. & Felleisen, M. (1994) A syntactic approach to type soundness. Inf. Comput. 115(1), 3894.CrossRefGoogle Scholar
Figure 0

Fig. 1. syntax.

Figure 1

Fig. 2. typing rules.

Figure 2

Fig. 3. reduction rules.

Figure 3

Fig. 4. subtyping rules.

Figure 4

Fig. 5. Definition of safe terms.

Figure 5

Fig. 6. syntax.

Figure 6

Fig. 7. typing rules.

Figure 7

Fig. 8. Translation from to

Figure 8

Fig. 9. Syntax of and contexts.

Figure 9

Fig. 10. context typing rules.

Figure 10

Fig. 11. context typing rules.

Supplementary material: File

Lhoták and Wadler supplementary material

Lhoták and Wadler supplementary material

Download Lhoták and Wadler supplementary material(File)
File 84.8 KB
Submit a response

Discussions

No Discussions have been published for this article.