Modular Session Types for Objects

Modular Session Types for Objects

Simon J. Gay  University of Glasgow Simon.Gay@glasgow.ac.uk Nils Gesbert  Grenoble INP – Ensimag Nils.Gesbert@grenoble-inp.fr António Ravara  Universidade Nova de Lisboa aravara@fct.unl.pt  and  Vasco T. Vasconcelos  Universidade de Lisboa vmvasconcelos@ciencias.ulisboa.pt
Abstract.

Session types allow communication protocols to be specified type-theoretically so that protocol implementations can be verified by static type checking. We extend previous work on session types for distributed object-oriented languages in three ways. (1) We attach a session type to a class definition, to specify the possible sequences of method calls. (2) We allow a session type (protocol) implementation to be modularized, i.e. partitioned into separately-callable methods. (3) We treat session-typed communication channels as objects, integrating their session types with the session types of classes. The result is an elegant unification of communication channels and their session types, distributed object-oriented programming, and a form of typestate supporting non-uniform objects, i.e. objects that dynamically change the set of available methods. We define syntax, operational semantics, a sound type system, and a sound and complete type checking algorithm for a small distributed class-based object-oriented language with structural subtyping. Static typing guarantees that both sequences of messages on channels, and sequences of method calls on objects, conform to type-theoretic specifications, thus ensuring type-safety. The language includes expected features of session types, such as delegation, and expected features of object-oriented programming, such as encapsulation of local state.

Key words and phrases:
typestate, session types, object-oriented calculus, Non-uniform method availability
copyright: ©:

\@sect

section1[Introduction]Introduction Computing infrastructure has become inherently concurrent and distributed, from the internals of machines, with the generalisation of many- and multi-core architectures, to data storage and sharing solutions, with “the cloud”. Both hardware and software systems are now not only distributed but also collaborative and communication-centred. Therefore, the precise specification of the protocols governing the interactions, as well as the rigorous verification of their correctness, are critical factors to ensure the reliability of such infrastructures.

Software developers need to work with technologies that may provide correctness guarantees and are well integrated with the tools usually used. Since Java is one of the most widely used programming languages, the incorporation of support to specify and implement correct software components and their interaction protocols would be a step towards more reliable systems.

Behavioural types represent abstractly and concisely the interactive conduct of software components. These kind of types are simple yet expressive languages, that characterise the permitted interaction within a distributed system. The key idea is that some aspects of dynamic behaviour can be verified statically, at compile-time rather than at run-time. In particular, when working with a programming language equipped with a behavioural type system, one can statically ensure that an implementation of a distributed protocol, specified with types, is not only safe in the standard sense (“will not go wrong”), but also that the sequences of interactions foreseen by the protocol are realizable by its distributed implementation. Thorough descriptions of the state-of-the-art of research on the topic have been prepared by COST Action IC1201 (Behavioural Types for Reliable Large-Scale Software Systems, “BETTY”) [betty:wg3, betty:wg1].

The problem we address herein is the following: can one specify the full interactive behaviour of a given protocol as a collection of types and check that a Java implementation of that protocol realises safely such behaviour?

The solution we present works like this: first, specify as a session type (a particular idiom of behavioural types) the behaviour of each party involved in the protocol; second, using each such term to type a class, implement the protocol as a (distributed, using channel-based communication where channels are objects) Java program; third, simply compile the code and if the type checker accepts it, then the code is safe and realises the protocol. Details follow.

Session types [HondaK:lanptd, HondaK:intblt] allow communication protocols to be specified type-theoretically, so that protocol implementations can be verified by static type checking. The underlying assumption is that we have a concurrent or distributed system with bi-directional point-to-point communication channels. These are implemented in Java by TCP/IP socket connections. A session type describes the protocol that should be followed on a particular channel; that is to say, it defines the permitted sequences, types and directions of messages. For example, the session type specifies that an integer must be sent and then a boolean must be received, and there is no further communication. More generally, branching and repetition can also be specified. A session type can be regarded as a finite-state automaton whose transitions are annotated with types and directions, and whose language defines the protocol.

Session types were originally formulated for languages closely based on process calculus. Since then, the idea has been applied to functional languages [GaySJ:sestip, GaySJ:lintta, NeubauerM:impst, PucellaR:hassta, VasconcelosVT:typmfl], the process-oriented programming language Erlang [MostrousVasconcelos:featherweightErlang], component-based object systems [VallecilloA:typbsc], object-oriented languages [CapecchiS:amasmo, Dezani-CiancagliniM:bousto, Dezani-CiancagliniMetal:ost, Dezani-CiancagliniM:sestoo, Dezani-CiancagliniM:disool, HuR:sesbdp], operating system services [FahndrichM:lansfr], and more general service-oriented systems [CarboneM:strgpc, cruz-filipe.lanese.etal:stream-based-service-centered-calculus]. Session types have also been generalised from two-party to multi-party systems [BonelliE:mulstd, HondaK:mulast], although in the present paper we will only consider the two-party case.

In previous work [GaySJ:modstd] we proposed a new approach to combining session-typed communication channels and distributed object-oriented programming. Our approach extends earlier work and allows increased programming flexibility. We adopted the principle that it should be possible to store a channel in a field of an object and allow the object’s methods to use the field like any other; we then followed the consequences of this idea. For example, consider a field containing a channel of type above, and suppose that method m sends the integer and method n receives the boolean. Because the session type of the channel requires the send to occur first, it follows that m must be called before n. We therefore need to work with non-uniform objects, in which the availability of methods depends on the state of the object: method n is not available until after method m has been called. In order to develop a static type system for object-oriented programming with session-typed channels, we use a form of typestate [typestates] that we have previously presented under the name of dynamic interfaces [GaySJ:dyni]. In this type system, the availability of a class’s methods (i.e., the possible sequences of method calls) is specified in a style that itself resembles a form of session type, giving a pleasing commonality of notation at both the channel and class levels.

The result of this combination of ideas is a language that allows a natural integration of programming with session-based channels and with non-uniform objects. In particular, the implementation of a session can be modularized by dividing it into separate methods that can be called in turn. This is not possible in SJ [HuR:sesbdp], the most closely related approach to combining sessions and objects (we discuss related work thoroughly in Section LABEL:sec:related). We believe that we have achieved a smooth and elegant combination of three important high-level abstractions: the object-oriented abstraction for structuring computation and data, the typestate abstraction for structuring state-dependent method availability, and the session abstraction for structuring communication.


\@sect

paragraph4[Contributions]Contributions In the present paper we formalize a core distributed class-based object-oriented language with a static type system that combines session-typed channels and a form of typestate. The language is intended to model programming with TCP/IP sockets in Java. The formal language differs from that introduced in our previous work [GaySJ:modstd] by using structural rather than nominal types. This allows several simplifications of the type system. We have also simplified the semantics, and revised and extended the presentation. We prove that static typing guarantees two runtime safety properties: first, that the sequence of method calls on every non-uniform object follows the specification of its class’s session type; second, as a consequence (because channel operations are implemented as method calls), that the sequence of messages on every channel follows the specification of its session type. This paper includes full statements and proofs of type safety, in contrast to the abbreviated presentation in our conference paper. We also formalize a type checking algorithm and prove its correctness, again with a revised and expanded presentation in comparison with the conference paper.

There is a substantial literature of related work, which we discuss in detail in Section LABEL:sec:related. Very briefly, the contributions of our paper are the following.

  • In contrast to other work on session types for object-oriented languages, we do not require a channel to be created and completely used (or delegated) within a single method. Several methods can operate on the same channel, thus allowing effective encapsulation of channels in objects, while retaining the usual object-oriented development practice. This is made possible by our integration of channels and non-uniform objects. This contribution was the main motivation for our work.

  • In contrast to other typestate systems, we use a global specification of method availability, inspired by session types, as part of a class definition. This replaces pre- and post-condition annotations on method definitions, except in the particular case of recursive methods.

  • When an object’s typestate depends on the result (in an enumerated type) of a method call, meaning that the result must be case-analyzed before using the object further, we do not force the case-analysis to be done immediately by using a combined “switch-call” primitive. Instead, the method result can be stored in a field and the case-analysis can happen at any subsequent point. Although this feature significantly increases the complexity of the formal system and could be omitted for simplicity, it supports a natural programming style and gives more options to future programming language designers.

  • Our structural definition of subtyping provides a flexible treatment of relationships between typestates, which can also support inheritance; this is discussed further in Section LABEL:sec:nominal.

The remainder of the paper is structured as follows. In Section Modular Session Types for Objects we illustrate the concept of dynamic interfaces by means of a sequential example. In Section Modular Session Types for Objects we formalize a core sequential language and in Section LABEL:sec:seq-extensions we describe some extensions. In Section LABEL:sec:distributed-example we extend the sequential example to a distributed setting and in Section LABEL:sec:distributed we extend the formal language to a core distributed language. In Section LABEL:sec:results we state and prove the key properties of the type system. In Section LABEL:sec:algorithm we present a type checking algorithm and prove its soundness and completeness, and describe a prototype implementation of a programming language based on the ideas of the paper. Section LABEL:sec:related contains a more extensive discussion of related work; Section LABEL:sec:conclusion outlines future work and concludes.


\@sect

section1[A Sequential Example]A Sequential Example

1class File {
2  session Init
3  where Init  = { {OK, ERROR} open(String): <OK: Open, ERROR: Init>}
4        Open  = { {TRUE, FALSE} hasNext(): <TRUE: Read, FALSE: Close>,
5                  Null close(): Init}
6        Read  = {String read(): Open, Null close(): Init}
7        Close = {Null close(): Init}
8  open(filename) {…}
9  hasNext() {…}
10  read() {…}
11  close() {…}
12}
Figure 1. A class describing a file in some API

A file is a natural example of an object for which the availability of its operations depends on its state. The file must first be opened, then it can be read repeatedly, and finally it must be closed. Before reading from the file, a test must be carried out in order to determine whether or not any data is present. The file can be closed at any time.

There is a variety of terminology for objects of this kind. Ravara and Vasconcelos [ravara.vasconcelos:typco] refer to them as non-uniform. We have previously used the term dynamic interface [GaySJ:dyni] to indicate that the interface, i.e. the set of available operations, changes with time. The term typestate [typestates] is also well established.

Figure 1 defines the class File, which we imagine to be part of an API for using a file system. The definition does not include method bodies, as these would typically be implemented natively by the file system. What it does contain is method signatures and, crucially, a session type definition which specifies the availability of methods. We will refer to a skeleton class definition of this kind as an interface, using the term informally to mean that method definitions are omitted. The figure shows the method signatures in the session type and not as part of the method definitions as is normal in many programming languages. This style is closer to the formal language that we define in Section Modular Session Types for Objects.

Line 3 declares the initial session type Init for the class. This and other session types are defined on lines 4–7. We will explain them in detail; they are types of objects, indicating which methods are available at a given point and which is the type after calling a method. In a session type, the constructor {…}, which we call branch, indicates that certain methods are available. In this example, Init declares the availability of one method (open), states Open and Read allow for two methods each, and state Close for a single method (close). For technical convenience, the presence of data is tested by calling the method hasNext, in the style of a Java iterator, rather than by calling an endOfFile method. If desired, method hasNext could also be included in state Read.

The constructor <…>, which we call variant, indicates that a method returns a value from an enumeration, and that the subsequent type depends on the result. For example, from state Init the only available method is open, and it returns a value from an enumeration comprising the constants (or labels) OK and ERROR. If the result is OK then the next state is Open; if the result is ERROR then the state remains Init. It is also possible for a session type to be the empty set of methods, meaning that no methods are available; this feature is not used in the present example, but would indicate the end of an object’s useful life.

I

O

R

TRUE

C

FALSE

hasNext()

OK

open()

close()

read()

close()

ERROR

close()

Figure 2. Diagrammatic representation of the session type of class File in Figure 1

The session type can be regarded as a finite state automaton whose transitions correspond to method calls and results. This is illustrated in Figure 2. Notice the two types of nodes ({…} and <…>) and the two types of labels in arcs (method names issuing from {…} nodes and enumeration constants issuing from <…> nodes).

Our language does not include constructor methods as a special category, but the method open must be called first and can therefore be regarded as doing initialisation that might be included in a constructor. Notice that open has the filename as a parameter. Unlike a typical file system API, creating an object of class File does not associate it with a particular file; instead this happens when open is called.

The reader might expect a declaration void close() rather than Null close(); for simplicity, we do not address procedures in this paper, instead working with the type Null inhabited by a single value, null. Methods open and hasNext return a constant from an enumeration: OK or ERROR for method open, and TRUE or FALSE for method hasNext. Enumerations are simply sets of labels, and do not need to be declared with names.

1class FileReader {
2  session Init
3  where Init = {Null init (): {Null read (String): Final}}
4        Final = {String toString(): Final}
5
6  file; text;
7
8  init() {
9    file = new File();
10    text = ""; // Evaluates to null
11  }
12  read(filename) {
13    switch (file.open(filename)) {
14      case ERROR:
15        null;
16      case OK:
17        while (file.hasNext())
18          text = text +++ file.read();
19        file.close(); // Returns null
20    }
21  }
22  toString() { text; }
23}
Figure 3. A client that reads from a File

Figure 3 defines the class FileReader, which uses an object of class File. FileReader has a session type of its own, defined on lines 2–3. It specifies that methods must be called in the sequence init, read, toString, toString, …. Line 5 defines the fields of FileReader. The formal language does not require a type declaration for fields, since fields always start with type Null, and are initialised to value null. Fields are always private to a class, even if we do not use a corresponding keyword. Lines 7–10 define the method init, which has initialisation behaviour typical of a constructor. Lines 12–19 illustrate the switch construct. In this particular case the switch is on the result of a method call. One of the distinctive features of our language is that it is possible, instead, to store the result of the method call in a field and later switch on the field; we will explain this in detail later. This contrasts with, for example, Sing# [FahndrichM:lansfr], in which the call/switch idiom is the only possibility. The while loop (lines 16–17) is similar in the sense that the result of file.hasNext must be tested in order to find out whether the loop can continue, calling file.read, or must terminate. Line 21 defines the method toString which simply accesses a field.

Typechecking the class FileReader according to our type system detects many common mistakes. Each of the following code fragments contains a type error.

  • file.open(filename);
    file.read() …;

    The open method returns either OK or ERROR, and the type of file is the variant type <OK: Open, ERROR: Init>. The tag for this variant type is the result of open. Because the type of file is a variant, a method cannot be called on it; first we must use a switch statement to analyse the result of open and discover which part of the variant we are in.

  • switch(file.open(filename))
      case OK: text = file.read();

    Here, a switch is correctly used to find out whether or not the file was successfully opened. However, if file is in state Open, the read method cannot be called immediately. First, hasNext must be called, with a corresponding switch.

  • result = file.open(filename);
    switch(result)
      case ERROR: file.close();

    In state ERROR, method close is not available (because the file was not opened successfully). The only available method is open.

  • file.close();
    if (file.hasNext()) 

    After calling close, the file is in state Init, so the method hasNext is not available. Only open is available.

Clearly, correctness of the code in Figure 3 requires that the sequence of method calls on field file within class FileReader matches the available methods in the session type of class File, and that the appropriate switch or while loops are performed when prescribed by session types of the form <…> in class File. Our static type system, defined in Section Modular Session Types for Objects, enables this consistency to be checked at compile-time. A distinctive feature of our type system is that methods are checked in a precise order: that prescribed by the session type (init, read, toString in class FileReader, Figure 3). As such the type of the private reference file always has the right type (and no further annotations—pre/post conditions—are required when in presence of non-recursive methods). Also, in order to check statically that an object with a dynamic interface such as file is used correctly, our type system treats the reference linearly so that aliases to it cannot be created. This restriction is not a problem for a simple example such as this one, but there is a considerable literature devoted to more flexible approaches to unique ownership. We discuss this issue further in Sections LABEL:subsec:shared-types, LABEL:sec:related and LABEL:sec:conclusion.

class FileReadToEnd {
  session Init
  where Init  = {{OK,ERROR} open(String): <OK: Open, ERROR: {Init}>}
        Open  = {{TRUE,FALSE} hasNext(): <TRUE: Read, FALSE: Close>}
        Read  = {String read(): Open}
        Close = {Null close(): {Init}}
}
Figure 4. Interface for class FileReadToEnd

In order to support separate compilation we require only the interface of a class, including the class name and the session type (which in turn includes the signature of each method). For example, in order to typecheck classes that are clients of FileReader, we only need its interface. Similarly, to typecheck class FileReader, which is a client of File, it suffices to use the interface for class File, thus effectively supporting typing clients of classes containing native methods.

Figure 4 defines the interface for a class FileReadToEnd. This class has the same method definitions as File, but the close method is not available until all of the data has been read. According to the subtyping relation defined in Section Modular Session Types for Objects, type Init of File is a subtype of type Init of FileReadToEnd, which we express as File.Init FileReadToEnd.Init. Subtyping guarantees safe substitution: an object of type File.Init can be used whenever an object of type FileReadToEnd.Init is expected, by forgetting that close is available in more states. As it happens, FileReader reads all of the data from its File object and could use a FileReadToEnd instead.


\@sect

section1[A Core Sequential Language]A Core Sequential Language

We now present the formal syntax, operational semantics, and type system of a core sequential language. As usual, the formal language makes a number of simplifications with respect to the more practical syntax used in the examples in Section Modular Session Types for Objects. We summarise below the main differences with what was discussed in the previous section; in Section LABEL:sec:seq-extensions, we will discuss in more detail how some usual programming idioms, which would be expected in a full programming language, can be encoded into this formal core.

  • Every method has exactly one parameter. This does not affect expressivity, as multiple parameters can be passed within an object, and a dummy parameter can be added if necessary: we consider a method call of the form as an abbreviation for .

  • Field access and assignment are defined in terms of a swap operation which puts the value of into the field and evaluates to the former content of . This operation is formally convenient because our type system forbids aliasing. In Java, the expression computes the result of and then both puts it into and evaluates to it, allowing expressions such as which create aliases; reading a field without removing its content also allows creation of aliases. The swap operation is a combined read-write which does not permit aliasing.

    The normal assignment operation is an abbreviation for (where the sequence operator explicitly discards the former content of ) and field read as the standalone expression is an abbreviation for . They differ from usual semantics by the fact that field read is destructive and that the assigment expression evaluates to .

  • In the examples, all method signatures appearing in a branch session type indicate both a return type and a subsequent session type. In general, those types are two separate things. However, when the subsequent behaviour of the object depends on the returned value, like the case of Open in type Init on line 3 of Fig. 1, the return type is an enumerated set of labels and the subsequent session type is a variant which must provide cases for exactly these labels. To simplify definitions, we avoid this redundant specification in the formal language, and when the subsequent session type is a variant, the return type of the method is always the special type , which indicates that the method will return a label from the variant.

Class dec
Class session types
Method dec
Types
Label sets
Values
Expressions
Figure 5. Top level syntax
Types
Field types
Values
Paths
Expressions
Object records
Heaps
States
Contexts

The productions for types, values and expressions extend those in Figure 5. Session types may never contain types of the form , even in the extended syntax.

Figure 6. Extended syntax, used only in the type system and semantics

\@sect

subsection2[Syntax]Syntax

We separate the syntax into the top-level language (Figure 5) and the extensions required by the type system and operational semantics (Figure 6). Identifiers , , and are taken from disjoint countable sets representing names of classes, methods, fields and labels respectively. The vector arrow indicates a sequence of zero or more elements of the syntactic class it is above. Similarly, constructs indexed by a set denote a finite sequence. We use to specifically denote finite sets of labels , whereas is any finite indexing set.

Field names always refer to fields of the current object; there is no qualified field specification . In other words, all fields are private. Method call is only available on a field, not an arbitrary expression. This is because calling a method changes the session type of the object on which the method is called, and in order for the type system to record this change, the object must be in a specified location (field).

Conversely, there is no unqualified method call in the core language. Calling a method on the current object , which we call a self-call, behaves differently from external calls with respect to typing and will be discussed as an extension in Section LABEL:subsec:self-calls,

A program consists of a sequence of class declarations . In the core language, types in a top-level program only occur in the session part of a class declaration: no type is declared for fields because they can vary at run-time and are always initially , and method declarations are also typeless, as explained earlier.

A session type corresponds to a view of an object from outside. It shows which methods can be called, and their signatures, but the fields are not visible. We refer to as a branch type and to as a variant type. Session type abbreviates the empty branch type . The core language does not include named session types, or the session and where clauses from the examples; we just work with recursive session type expressions of the form , which are required to be contractive, i.e. containing no subexpression of the form . We require contractivity so that every session type defines some behaviour. The operator is a binder, giving rise, in the standard way, to notions of bound and free variables and alpha-equivalence. A type is closed if it includes no free variables. We denote by the capture-avoiding substitution of for in .

Value types which can occur either as parameter or return type for a method are: which has the single value , a session type which is the type of an object, or an enumerated type which is an arbitrary finite set of labels . Additionally, the specific return type is used for method occurrences after which the resulting session type is a variant, and means that the method result will be the tag of the variant. The set of possible labels appears in the variant construct of the session type, so it is not necessary to specify it in the return type of the method. However, in the example code, the set of labels is written instead of , so that the method signature shows the return type in the usual way.

The type system, which we will describe later, enforces the following restrictions on session types: the immediate components of a variant type are always branch types, and the session type in a class declaration is always a branch. This is because a variant type is used only to represent the effect of method calls and the dependency between a method result and the subsequent session type, so it only makes sense immediately within a branch type.

Figure 6 defines additional syntax that is needed for the formal system but is not used in top-level programs. This includes:

  • some extra forms of types, which are used internally to type some subexpressions but cannot be the argument type or return type of a method, and thus are never written in a program;

  • intermediate expressions that cannot appear in a program but arise from the operational semantics;

  • syntax for the heap.

\@sect

paragraph4[Internal types.]Internal types. The first internal type we add is the type , where is the name of a field. This type is related to variant session types and the type, in the way illustrated by the following example: suppose that, in some context, field of the current object contains an object whose type is . This means that the expression is allowed in this context and will both: change the abstract state of the object in to one of the , and return the label corresponding to that particular state. Thus, there is a link between the value of the expression and the type of field after evaluating the expression, and the type system needs to keep track of this link; to this end, the expression is given, internally, the type , rather than which does not contain enough information. The use of is also illustrated in Figure 13.

The second internal type is an alternative form of object type, , which has a field typing instead of a session type. Recall that in our language, all object fields are private; therefore, normally the type of an object is a session type which only refers to methods. However, an object has access to its own fields, so for typechecking a method definition, the type environment needs to provide types for the fields of the current object (). Since the types of the fields change throughout the life of the object, the class definition is not enough to know their types at a particular point. We thus use a field typing , which is usually a record type associating one type to each field of the object. For example, represents an object of class with exactly two fields, , which currently contains a value of type , and which currently contains an object in state . Note that in , must provide types for exactly all the fields of class .

The other form of field typing is a variant field typing, which regroups several possible sets of field types indexed by labels. For example, represents an object of class , whose two fields are and , and where either has type and has type , or has type and has type , depending on the value of the label.

These field typings cannot be the type of expressions (which cannot evaluate to ); they only represent the type of the current object in type environments. The relation between field typings and session types will be discussed in Section Modular Session Types for Objects.


\@sect

paragraph4[Internal expressions, heap, and states.]Internal expressions, heap, and states. These other additions are used to define the operational semantics. A heap maps object identifiers , taken from yet another countable set of names, to object records . We write for the set of object identifiers in . The identifiers are values, which may occur in expressions. The operation represents adding a record for identifier to the heap and we consider it to be associative and commutative, that is, is essentially an unordered set of bindings. It is only defined if . Paths represent locations in the heap. A path consists of a top-level object identifier followed by an arbitrary number of field specifications. We use the following notation to interpret paths relative to a given heap.

Definition 0.1 (Heap locations).
  • If , we define (for all ) and . For any value and any , we also define where for and .

  • If , we define , and for any field of , .

  • If and , then we also define and .

  • In any other case, these operations are not defined. Note in particular that is not defined if is a path that exists in but does not point to an object identifier.

There is a new form of expression, , which is used to represent an ongoing method call.

Finally, a state consists of a heap and an expression, and the operational semantics will be defined as a reduction relation on states; are evaluation contexts in the style of Wright and Felleisen [WrightAK:synats], used in the definition of reduction.

The semantic and typing rules we will present next are implicitly parameterized by the set of declarations which constitute the program. It is assumed that the whole set is available at any point and that any class is declared only once. We do not require the sets of method or field names to be disjoint from one class to another. We will use the following notation: if is one of the declarations, means and means , and if then is .

                                         (R-Call)                          (R-Return) 
 
                                       (R-New)                                                                       (R-Swap)                                                                                            (R-Switch)                              (R-Seq) 
 
                                     (R-Context)                               

Figure 7. Reduction rules for states

\@sect

subsection2[Operational Semantics]Operational Semantics

Figure 7 defines an operational semantics on states consisting of a heap , a path in the heap indicating the current object, and an expression . In general, is an expression obtained by a series of reduction steps from a method body, where the method was called on the object identified by the path . All rules have the implicit premise that the expressions appearing in them must be defined. For example, only reduces if is an object record containing a field named . An example of reduction, together with typing, is presented in Figure 13 and discussed at the end of the present section.

The current object path is used to resolve field references appearing in the expression . It behaves like a call stack: as shown in R-Call, when a method call on a field (relative to the current object located at ) is entered, the object in becomes the current object; this is indicated by changing the path to . Additionally, the method body, with the actual parameter substituted for the formal parameter, is wrapped in a expression and replaces the method call. When the body has reduced to a value, this value is unwrapped by R-Return which also pops the field specification from the path, recovering the previous current object . This is illustrated in Figure 13, which also shows the typing of expressions in a series of reductions. R-New creates a new object in the heap, with fields. R-Swap updates the value of a field and reduces to its former value.

R-Switch is standard. R-Seq discards the result of the first part of a sequential composition. R-Context is the usual rule for reduction in contexts.

To complete the definition of the semantics we need to define the initial state. The idea is to designate a particular method of a particular class as the main method, which is called in order to begin execution. The most convenient way to express this is to have an initial heap that contains an object of class , which is also chosen as the current object, and an initial expression which is the body of . The initial state is therefore

where is the identifier of the top-level object of class , which is the only object in the heap, and the current object path is also . Strictly speaking, method must have a parameter ; we take to be of type and assume that it does not occur in .


\@sect

subsection2[Example of reduction]Example of reduction

Figure 8. A series of reduction steps.

Assume that the top-level class is , containing fields and . Assume also that there is another class which defines the set of methods . Finally, assume that the body of the main method of is

where is some element of .

The initial state is

where for simplicity we have ignored the parameter of . Figure 8 shows the sequence of reduction steps until one of the cases of the is reached.

The first step is expansion of the syntactic sugar for assignment, translating it into a swap followed by . Another similar translation step occurs later. The first real reduction step is R-New, creating an object , followed by R-Swap to complete the assignment into field and then R-Seq to tidy up. Next, the step labelled “simplify” informally removes in order to avoid carrying it through to an uninteresting R-Seq reduction later.

Now assume that the body of method is . Reduction by R-Call changes the current object path to because the current object is now . Several reduction steps convert to a particular element of the enumerated type , which we call . After that, R-Return changes the current object path back to , and then some R-Swap and R-Seq steps bring into the guard of the , finally allowing the appropriate case to be selected.

We will return to this example in Section Modular Session Types for Objects, to show how each state is typed.


\@sect

subsection2[Subtyping]Subtyping

                                    (S-Record)                                                                      (S-Variant)                                                                        (S-Field)                           

Figure 9. Subtyping rules for fields

Subtyping is an essential ingredient of the theory of session types. Originally proposed by Gay and Hole [GaySJ:substp], it has been widely used in other session-based systems, with subject-reduction and type-safety holding. The guiding principle is the “safe substitutability principle” of Liskov and Wing [liskov.wing:behavioural-subtyping], which states that, if is a subtype of , then objects of type in a program may be safely replaced with objects of type .

Two kinds of types in the top-level core language are subject to subtyping: enumerated types and session types. The internal language also has field typings; subtyping on them is derived from subtyping on top-level types by the rules in Figure 9.

Subtyping for enumerated types is defined as simple set inclusion: if and only if . We refer to subtyping for session types as the sub-session relation. Because session types can be recursive, the sub-session relation is defined coinductively, by defining necessary conditions it must satisfy and taking the largest relation satisfying them. The definition involves checking compatibility between different method signatures, which itself is dependent on the whole subtyping relation. We proceed as follows: given a candidate sub-session relation , we define an -compatibility relation between types and between method signatures which uses as a sub-session relation. We then use -compatibility in the structural conditions that must satisfy in order to effectively be a sub-session relation.

Let denote the set of contractive, closed, class session types. We deal with recursive types using the following operator:

Definition 0.2 (Unfolding).

The operator is defined inductively on by and if is not of the form . Since the types in are contractive, this definition is well-founded.

We now define the two compatibility relations we need.

Definition 0.3 (-Compatibility (Types)).

Let be a binary relation on . We say that type is -compatible with type if one of the following conditions is true.

  1. and are enumerated types and

  2. and .

Definition 0.4 (-Compatibility (Signatures)).

Let be a binary relation on . Let and be components of branch types, both for the same method name , i.e. method signatures with subsequent session types. We say that is -compatible with if is -compatible with and either:

  1. is -compatible with and , or

  2. is an enumerated type , and .

The compatibility relation on method signatures is, as expected, covariant in the return type and the subsequent session type and contravariant in the parameter type, but with one addition: if a method has an enumerated return type and subsequent session type , then it can always be used as if it had a return type of and were followed by the uniform variant session type . Indeed, both signatures mean that the method can return any label in and will always leave the object in state .

We can now state the necessary conditions for a sub-session relation.

Definition 0.5 (Sub-session).

Let be a binary relation on . We say that is a sub-session relation if implies:

  1. If then is of the form with , and for all , is -compatible with .

  2. If then is of the form with and for all , .

For the sake of simplicity we will now, when we refer to this definition later on, make the unfolding step implicit by assuming, without loss of generality, that neither nor is of the form .

Lemma 0.6.

The union of several sub-session relations is a sub-session relation.

Proof.

Let , where the are sub-session relations. Let . Then there is in such that . This implies that satisfies the conditions in Definition 0.5 with respect to . Just notice that, because , the conditions are satisfied with respect to as well — in particular, -compatibility implies -compatibility. Indeed, the conditions for only differ from those for by requiring particular pairs of session types to be in rather than in , so they are looser.        

We now define the subtyping relation on session types to be the largest sub-session relation, i.e. the union of all sub-session relations. The subtyping relation on general top-level types is just -compatibility.

Subtyping on session types means that either both are branches or both are variants. In the former case, the supertype must allow fewer methods and their signatures must be compatible; in the latter case, the supertype must allow more labels and the common cases must be in the subtyping relation. Like the definition of subtyping for channel session types [GaySJ:substp], the type that allows a choice to be made (the branch type here, the type for channels) has contravariant subtyping in the set of choices.

The following lemma shows that the necessary conditions of Definition 0.5 are also sufficient in the case of .

Lemma 0.7.
  1. Let and with . If for all , is -compatible with , then .

  2. Let and with . If for all in we have , then .

Proof.

The relation is a sub-session relation.        

Finally, we prove that this subtyping relation provides a preorder on types.

Proposition 0.8.

The subtyping relation is reflexive and transitive.

Proof.

First note that session types can only be related by subtyping to other session types; the same applies to enumerated types and, in the internal system, field typings. Since the relation for enumerated types is just set inclusion, we already know the result for it. We now prove the properties for session types; the fact that they hold for field typings is then a straightforward consequence.

For reflexivity, just notice that the diagonal relation is a sub-session relation, hence included in .

For transitivity, what we need to prove is that the relation is a sub-session relation. Let and let be as given by the definition of .

In case (1) where we have , we know that:

  • with , and for all , is -compatible with .

  • Therefore, is of the form with , and for all , is -compatible with .

Straightforwardly . For every in , we have to prove that is -compatible with . We deduce it from the two -compatibilities we know by looking into the definition of compatibility point by point:

  • We have and . Either these types are all session types, and then by definition of , or none of them is and we have by transitivity of subtyping on base types. In both cases, is -compatible with .

  • We also have either:

    • , , and . In this case, the former two conditions imply, similarly to the above, that is -compatible with . The latter two imply .

    • Or is an enumerated type , , and . Then we have , which is all we need.

    • Or, finally, is an enumerated type , is an enumerated type such that , and . Then from and we deduce, using case (2) of Lemma 0.7, . We thus have, again, which is the required condition.

In case (2) where we have , we obtain and , with and for any in , and , which imply by definition of that is in .        

Definition 0.9 (Type equivalence).

We define equivalence of session types and as and . This corresponds precisely to and having the same infinite unfoldings (up to the ordering of cases in branches and variants). Henceforth types are understood up to type equivalence, so that, for example, in any mathematical context, types and can be used interchangeably, effectively adopting the equi-recursive approach [PierceBC:typpl, Chapter 21].


\@sect

subsection2[Type System]Type System

We introduce a static type system whose purpose is to ensure that typable programs satisfy a number of safety properties. As usual, we make use of a type preservation theorem, which states that reduction of a typable expression produces another typable expression. Therefore the type system is formulated not only for top-level expressions but for the states (i.e. (heap, expression) pairs) on which the reduction relation is defined.

An important feature of the type system is that the method definitions within a particular class are not checked independently, but are analyzed in the order specified by the session type of the class. This is expressed by rule T-Class, the last rule in Figure 10, which uses a consistency relation between field typings and session types, defined in Section Modular Session Types for Objects. Checking this relation requires checking the definitions of the methods occurring in , in order. Checking method definitions uses the typing judgement for expressions, which is defined by the other rules in Figure 10.

In the following sections we describe the type system in several stages.

(T-Null)  (T-Label)  (T-New)  (T-LinVar)                       is not an object type (T-Var)                                                                        is not a variant (T-Swap)                                                                                                                                  if , otherwise (T-Call)                                                                                              or (T-Seq)                                                                                                                                   (T-Switch)                                                                                                                (T-SwitchLink)                                                                                              is a record (T-VarF)                                                                               (T-Sub)