Papaya: Global Typestate Analysis of Aliased Objects Extended Version

07/27/2021 ∙ by Mathias Jakobsen, et al. ∙ University of Glasgow 0

Typestates are state machines used in object-oriented programming to specify and verify correct order of method calls on an object. To avoid inconsistent object states, typestates enforce linear typing, which eliminates - or at best limits - aliasing. However, aliasing is an important feature in programming, and the state-of-the-art on typestates is too restrictive if we want typestates to be adopted in real-world software systems. In this paper, we present a type system for an object-oriented language with typestate annotations, which allows for unrestricted aliasing, and as opposed to previous approaches it does not require linearity constraints. The typestate analysis is global and tracks objects throughout the entire program graph, which ensures that well-typed programs conform and complete the declared protocols. We implement our framework in the Scala programming language and illustrate our approach using a running example that shows the interplay between typestates and aliases.



There are no comments yet.


page 1

page 2

page 3

page 4

This week in AI

Get the week's most popular data science and artificial intelligence research sent straight to your inbox every Saturday.

1. Introduction

In class-based object-oriented programming languages, a class defines a number of methods that can be invoked on an object of that class. Often, however, there is an implicit order imposed on methods, where some methods should be called before others. For example, a server connection must be opened before sending data, or we might want a clean-up method to be called before freeing resources. These method orderings, or protocols, are often defined in varying degrees of formality through documentation or comments, which makes the process difficult and error-prone. Work has been undertaken to include these protocols in the program itself with the introduction of typestates for object-oriented languages (Kouzapas2016TypecheckingStMungo; BravettiBehaviouralTypes2020; DeLine2004; Aldrich2009Typestate-orientedProgramming). Common to many of these approaches is that they rely on a linear type system, where only a single reference to an object can exist, thus eliminating–or limiting–aliasing. In Mungo (Kouzapas2016TypecheckingStMungo; BravettiBehaviouralTypes2020) linearity is always enforced, whereas other approaches in languages such as Plaid (Bierhoff2007) and Vault (DeLine2001EnforcingSoftware) allow limited aliasing, while preserving compositionality of the type system such that each class can be type checked in isolation (Fahndrich2002; Militao2010). These approaches often require programmer annotations to deal with aliasing control or they simply eliminate aliasing altogether. The difficulty with aliasing in the presence of typestates is that if multiple references exist to a single object, then operations on one object reference can affect the type of multiple other references as well. This is further complicated if we allow aliases to be stored in fields on multiple objects. Consequently, operations on objects of one class impact the well-typedness of other classes, potentially leading to inconsistent objects’ states. Looking at the problem from a more ‘technical’ angle, the difficulty with aliasing in the presence of typestates is due to the discrepancy between the compositional nature of typestate-based type systems and the global nature of aliasing. To address aliasing one can either (i) allow limited access to an object through aliasing control mechanisms or (ii) if we want unrestricted aliasing then use a form of global analysis. The problem with (i) is that it not trivial to find an alias control mechanism to capture OO programming idioms, and for (ii) while we benefit from the most flexible form of aliasing, the drawback is that we lose compositionality. With the above in mind we pose our research question: RQ: Can we define a typestate-based type system for object-oriented languages that guarantees protocol conformance and completion while allowing unrestricted aliasing? In this paper, we answer positively our research question and introduce a global approach to type checking object-oriented programs with typestates, which allows unrestricted aliasing, meaning that objects can be freely aliased, and stored in fields of other objects. This is more representative of the sort of aliasing that can occur in real-world programs. In this work we treat typestates in a similar fashion to the line of work on Mungo (Kouzapas2016TypecheckingStMungo; BravettiBehaviouralTypes2020) and along the same lines, we introduce Papaya, an implementation of a typestate-based type system for Scala.


The contributions of this paper are as follows.

  • Typestates for Aliased Objects. We formalise an object-oriented language with typestate annotations.

    • creftype 3 presents the syntax; creftype 4 presents the type system that performs global typestate analysis of unrestricted aliased objects and LABEL:sec:semantics presents the operational semantics.

    • LABEL:sec:properties covers the meta-theory of our formalisation and we show that our type system is safe by proving subject reduction (LABEL:thm:subject-reduction), progress (LABEL:thm:progress), protocol conformance (LABEL:cor:usage_conformance) and protocol completion (LABEL:lemma:protocol_completion).

  • Papaya Tool. LABEL:sec:implementation presents the Papaya tool, an implementation of our type system for Scala. Protocols are expressed as Scala objects and are added to Scala classes with the @Typestate annotation. Following the formalisation, our implementation allows for unrestricted aliasing, where objects are checked if they conform and complete their declared protocols.

  • The BankAccount Example. We illustrate our work with a running example (starting in creftype 2), which features aliasing. We show how the program is typed in our type system (from creftype 4) and we implement it in Scala (in LABEL:sec:implementation) where use Papaya to perform typestate checking.

In LABEL:sec:relatedwork we discuss related work on typestates and aliasing. Finally, in LABEL:sec:conclusion we conclude the paper and present ideas for future work.

2. Overview

We introduce our approach with an example, which is inspired by (Jakobsen2020). The example is shown using the calculus that will be defined in creftype 3 with the addition of some base types and operations on those. For completeness, since the calculus requires a formal parameter for all methods, one could pass the unit value as an argument. For readability we omit the argument instead. Consider the class BankAccount shown in creftype 1. It is a simple wrapper class around a field storing an amount of money. Notice that there is an implicit ordering of method calls, which the programmer might assume will be followed when using the class: the amount of money should be set prior to using the value of the field, and interest should be applied after setting the money; finally, the money variable should only be read after both setting the money and applying the interest has occurred, so that an intermediate value is not returned.

1class BankAccount[{setMoney;
2                  {applyInterest;
3                  {getMoney; end}}}] {
4  val amount:float;
5  fun setMoney(d:float):void  {
6    this.amount = d;
7  }
8  fun getMoney():float {
9    this.amount;
10  }
11  fun applyInterest(rate:float) {
12    this.amount = this.amount * rate;
13  }
Listing 1: Wrapper class around an amount of money

We can express this implicit order of method calls as an explicit usage:

where denotes that a method where can be called, with the continuation usage . This usage states that the first method called should be setMoney, followed by a call to applyInterest and finally one to getMoney. We introduce two additional classes as shown in creftypeplural 32. The SalaryManager class adds money to a BankAccount and applies a fixed interest rate. The DataStorage class fetches the value of a BankAccount and stores it in a database.

15class SalaryManager[{setAccount;
16                    {addSalary; end}}] {
17  val account:BankAccount
18  fun setAccount(ms:BankAccount):void {
19    this.account = ms;
20  }
21  fun addSalary(amount:float) {
22    this.account.setMoney(amount);
23    this.account.applyInterest(1.05);
24  }
Listing 2: Salary manager that adds funds to a BankAccount object
26class DataStorage[{setAccount;
27                  {store; end}}] {
28  val account:BankAccount
29  fun setAccount(ms:BankAccount):void {
30    this.account = ms;
31  }
32  fun store() {
33    this.account.getMoney();
34    // store value in database
35  }
Listing 3: Data storage class that reads the funds of a BankAccount object

Note that in the three classes we defined so far, there is no explicit mentioning of possible aliasing. However, as shown in creftype 4, an instance of class BankAccount can be aliased and shared between the manager and data store, as long as the joined operations on the instance respect its usage.

37account = new BankAccount;
38manager = new SalaryManager;
39db = new DataStorage;
Listing 4: Aliasing of a BankAccount object

If we were to swap lines 4 and 4, then they would no longer follow the protocol, as the data store would call getMoney before setMoney and applyInterest were called.

3. The Language

We introduce an object-oriented calculus with classes and enumeration types, similar to previous work on Mungo (Kouzapas2016TypecheckingStMungo; Kouzapas2018TypecheckingJava; Dardhaetal2017; BravettiBehaviouralTypes2020; VoineaDG20). The syntax of terms is shown in creftype 0(a). For a sequence we write and let . A program is a list of class and enum-definitions , followed by a class Main which contains the main method. A class definition contains the initial protocol, or usage , field declarations and method declarations . For expressions, the language supports assignment, object initialisation, method calls (on fields, parameters or on the object itself). Note that for simplicity and readability of typing rules later on, method calls and field access use an object-reference as the target, thus call-chaining and nested field access is not allowed. However, the language can be easily extended to facilitate these features, requiring an extra object look-up in the relevant typing rules. The only object reference that can occur in program text is the this reference. The language also supports control structures (conditionals, loops, and sequential composition) and match expressions (switch on an enumeration type). Loops are formalised with a jump-style loop with labelled expressions and continue statements in line with Mungo work.

D ::=& class C {U, ¯F, ¯M} ∣enum L {¯l}
F ::=& val f : t
M ::=& fun m(x : t) : t {e}
r ::=& o ∣o.f ∣x
e ::=& o.f = e ∣o.f = new C ∣e;e ∣r.m(e) ∣unit ∣o.f ∣x
∣& if_ (e) {e} else {e} ∣o.l ∣match (e) { ¯l : e} ∣null
∣& truefalse ∣k : e ∣continue k

(a) Syntax of class definitions

t ::=& C ∣voidbool ∣L
::=& voidbool ∣L ∣
U::=& μX.U∣X ∣{ ¯m; w } ∣end
w ::& ⟨¯l: U⟩ ∣U

(b) Syntax of types
Figure 1. Syntax of terms and types

The syntax of types is shown in creftype 0(b) and it contains the object types , base types bool and void, the null-type , and enumeration types and . The shaded production rules indicate run-time syntax. An object type is composed of an object reference , which is a unique identifier a single object, a class name and a current usage describing the remaining protocol of the object. The enumeration type introduced in (VasconcelosGay2009DynamicInterfaces) is used to track updates in switch-statements and are not declared in the program text. creftype 3.1 presents a labelled transition system for usages, annotated with the method call or the enumeration label, depending on the action performed. If an object has type , then the transitions of describe the permitted operations on the object referenced by . As previously described, branch usages describe a set of available methods, each with a continutation usage. Choice usages describe that based on a enumeration label , the protocol continues with protocol . Recursive behaviour can be specified with recursive usages and the end usage denotes the terminated protocol which has no transitions.

Definition 3.1 (LTS on Usages).

We define a notion of well-formedness for expressions (creftype 3.2), which requires that continue expressions do not show up in places where, after loop unfolding, they would be followed by other expressions. Examples of ill-formed expressions include and . Furthermore, well-formedness also requires a labelled expression has a terminating branch so that is well-formed whereas is not.

Definition 3.2 (Well-formedness).

An expression is well-formed if:

  1. No expression follows a continue expression after unfolding continue expressions in

  2. No free loop-variables in

  3. All continue expressions in are guarded by a branching (if or match) expression

  4. There must be a branch in all labelled expressions in that does not end with a continue expression

We conclude with the definition of well-formed methods.

Definition 3.3 (Well-formed methods).

A method declaration is well formed if is well formed and recursive calls are guarded by a branching expression.

4. Type System

As opposed to previous type systems for Mungo (Kouzapas2016TypecheckingStMungo; Kouzapas2018TypecheckingJava; BravettiBehaviouralTypes2020) the type system presented here performs a global analysis of the program, in order to maintain a global view of aliasing while guaranteeing correct objects’ states. This means that instead of relying on compositionality during type checking, we must explore the entire program graph. Consequently when a method call is encountered during type checking, the type system must ensure that the body of the method is well typed in the current typing environment. We define a typing environment using the production rules shown in creftype 2. A typing environment maps object references to an object-type as well as a field typing environment that contains the types for all fields in the object. Furthermore we use the notation to indicate an update of an existing binding for object , and to update the existing binding of a field of object . A typing environment can only contain a single binding for each object reference . Similarly, a field typing environment can only contain a single binding for each field name.

Γ::= &∅∣Γ, o ↦(T, λ)
λ::= &∅∣λ, f ↦z
z ::= &basetype boolbasetype void
∣ &basetype ⊥∣ basetype L
∣ &reference o

Figure 2. Syntax of typing environments

We define the initial field environment given a set of field declarations . This is used when initialising new objects. Fields with class types are given the initial type of whereas fields of base types retain that type in the field environment.

We also define the following shorthand functions for extracting information from the typing environment and class definitions.

For a class name where we let and define the following functions.

The type system is driven by the following (Main) rule, which states that if the main method is well typed, then the entire program is well typed. As previously mentioned, the type system will expand method calls, hence the type system will visit all reachable parts of the program. In the (Main) rule we require meaning that the resulting type environment must be terminated, meaning that protocols must be finished for all objects. term is defined as: