Keywords

These keywords were added by machine and not by the authors. This process is experimental and the keywords may be updated as the learning algorithm improves.

1 Introduction

Infinite computations have long lost their negative connotation. Two paradigmatic contexts in which they appear naturally are reactive systems [17] and lazy functional programming. The former contemplate the use of infinite computations in order to capture non-transformational computations, that is computations that cannot be expressed in terms of transformations from inputs to outputs; rather, computations of reactive systems are naturally modelled in terms of ongoing interactions with the environment. Lazy functional programming is acknowledged as a paradigm that fosters software modularity [13] and enables programmers to specify computations over possibly infinite data structures in elegant and concise ways. Nowadays, the synergy between these two contexts has a wide range of potential applications, including stream-processing networks, real-time sensor monitoring, and internet-based media services.

Nonetheless, not all diverging programs – those engaged in an infinite sequence of possibly intertwined computations and communications – are necessarily useful. There exist degenerate forms of divergence where programs do not produce results, in terms of observable data or performed communications. In this paper we investigate the issue by proposing a calculus for expressing computations over possibly infinite data types and involving message passing. The calculus – called SID after Sessions with Infinite Data – combines a call-by-name functional core (inspired by Haskell) with multi-threading and session-based communication primitives.

In the remainder of this section we provide an informal introduction to SID and its key features by means of a few examples. The formal definition of the calculus, of the type system, and its properties are given in the remaining sections. A simple instance of computation producing an infinite data structure is given by

where the function applied to a number n produces the stream (infinite list) \(\langle n\texttt {,}\langle n+1\texttt {,}\langle n+2\texttt {,}\cdots \rangle \rangle \rangle \) of integers starting from n. We can think of this list as abstracting the frames of a video stream, or the samples taken from a sensor.

The key issue we want to address is how infinite data can be exchanged between communicating threads. The most straightforward way of doing this in SID is to take advantage of lazy evaluation. For instance, the SID process

represents two threads \({x}\) and \({y}\) running in parallel and connected by a session \({c}\), of which thread \({x}\) owns one endpoint \({c}^+\) and thread \({y}\) the corresponding peer \({c}^-\). Thread \({x}\) sends a stream of natural numbers on \({c}^+\) and continues as \(f~{c}^+\), where \(f\) is left unspecified. Thread \({y}\) receives the stream from \({c}^-\) and continues as . The bind operator models sequential composition and has the exact same semantics as in Haskell. In particular, it applies the rhs to the result of the action on its lhs. The result of sending a message on the endpoint \({a}^+\) is the endpoint itself, while the result of receiving a message from the endpoint \({a}^-\) is a pair consisting of the message and the endpoint. In this example, the whole stream is sent at once in a single interaction between \({x}\) and \({y}\). This behaviour is made possible by the fact that SID evaluates expressions lazily: the message is not evaluated until it is used by the receiver.

In principle, exchanging “infinite” messages such as between different threads is no big deal. In the real world, though, this interaction poses non-trivial challenges: the message consists in fact of a mixture of data (the parts of the messages that have already been evaluated, like the constant 0) and code (which lazily computes the remaining parts when necessary, like ). This observation suggests an alternative, more viable modelling of this interaction whereby the sender unpacks the stream element-wise, sends each element of the stream as a separate message, and the receiver gradually reconstructs the stream as each element arrives at destination. This modelling is intuitively simpler to realise (especially in a distributed setting) because the messages exchanged at each communication are ground values rather than a mixture of data and code. In SID we can model this as a process

where the functions and are defined as:

(1.1)

The syntax is just syntactic sugar for a function that performs pattern matching on the argument, which must be a pair, in order to access its components. In , pattern matching is used for accessing and sending each element of the stream separately. In , the pair \(\langle {z}\texttt {,}{y}'\rangle \) contains the received head \({z}\) of the stream along with the continuation \({y}'\) of the session endpoint from which the element has been received. The recursive call retrieves the tail of the stream \({z}s\), which is then combined with the head \({z}\) and passed as an argument to \(g\).

The code of looks reasonable at first, but conceals a subtle and catastrophic pitfall: the recursive call is in charge of receiving the whole tail \({z}s\), which is an infinite stream itself, and therefore it involves an infinite number of synchronisations with the producing thread! This means that will hopelessly diverge striving to receive the whole stream before releasing control to \(g\). This is a known problem which has led to the development of primitives (such as unsafeInterleaveIO in Haskell or delayIO in [23]) that allow the execution of I/O actions to interleave with their continuation. In this paper, we call such primitive , since its semantics is also akin to that of future variables [25]. Intuitively, an expression allows \(g\) to reduce even if \(e\), which typically involves I/O, has not been completely performed. The variable \({x}\) acts as a placeholder for the result of \(e\); if \(g\) needs to inspect the structure of \({x}\), its evaluation is suspended until \(e\) produces enough data. Using we can amend the definition of thus

(1.2)

and obtain one that allows \(g\) to start processing the stream as its elements come through the connection with the producer thread. The type system that we develop in this paper allows us to reason on sessions involving the exchange of infinite data and when such exchanges can be done “productively”. In particular, our type system flags in (1.1) as ill typed, while it accepts in (1.2) as well typed. To do so, the type system uses a modal operator \(\bullet \) related to the normalisability of expressions. As hinted by the examples (1.1) and (1.2), this operator plays a major role in the type of .

Related Work. To the best of our knowledge, SID is the first calculus that combines session-based communication primitives [12, 29] with a call-by-need operational semantics [1, 18, 30] guaranteeing progress of processes exchanging infinite data. The operational semantics of related session calculi that appear in the literature is call-by-value, e.g. [9, 11, 28] making them unsuitable for handling potentially infinite data, such as streams. In the context of communication-centric calculi, SSCC [7] offers an explicit primitive to deal with streams. Our language enables the modelling of more intricate interactions between infinite data structures and infinite communications. Besides, the type system of SSCC considers only finite sessions types and does not guarantee progress of processes.

Following [19], we use a modal operator \(\bullet \) to restrict the application of the fixed point operator and exclude degenerate forms of divergence. This paper is an improvement over past typed lambda calculi with a temporal modal operator in two respects. Firstly, we do not need any subtyping relation as in [19] and secondly SID programs are not cluttered with constructs for the introduction and elimination of individuals of type \(\bullet \) as in [3, 14, 14, 15, 26]. A weak criterion to ensure productivity of infinite data is the guardedness condition [5]. We do not need such condition because we can type more normalising expressions (such as in (1.2)) using the modal operator \(\bullet {^{}}\).

Futures originated in functional programming and related paradigms for parallelising a program [10]. The call-by-need \(\lambda \)-calculus with futures in [25] is used for studying contextual equivalence and has no type system.

In the session calculi literature, the word “progress” has two different meanings. Sometimes it is synonym of deadlock freedom [2], at other times it means lock freedom, i.e. that each offered communication in an open session eventually happens [4, 8, 20]. Typed SID processes cannot be stuck, and if they do not terminate they communicate and/or generate new threads infinitely often. This means that the property of progress satisfied by our calculus is stronger than that of [2] and weaker than that of [4, 8, 20].

Contributions and Outline. The SID calculus, defined in Sect. 2, combines in an original way standard constructs from the \(\lambda \)-calculus and process algebras in the spirit of [11, 12]. The type system, given in Sect. 3, has the novelty of using the modal operator \(\bullet {^{}}\) to control the recursion of programs that perform communications. To the best of our knowledge, the interplay between \(\bullet {^{}}\) and the type of is investigated here for the first time. The properties of our framework, presented in Sect. 4, include subject reduction (Theorem 1), normalisation of expressions (Theorem 2), progress and confluence of processes (Theorems 45). Additional examples, definitions, and proofs can be found in the technical report [27].

2 The SID Calculus

We use an infinite set of channels \({a}\), \({b}\), \({c}\) and a disjoint, infinite set of variables \({x}\), \({y}\). We distinguish between two kinds of channels: shared channels are public service identifiers that can only be used to initiate sessions; session channels represent private sessions on which the actual communications take place. We distinguish the two endpoints of a session channel \({c}\) by means of a polarity \(p\in \{ {+}, {-} \}\) and write them as \({c}^+\) and \({c}^-\). We write \(\overline{p}\) for the dual polarity of \(p\), where \(\overline{+} = {-}\) and \(\overline{-} = {+}\), and we say that \({c}^p\) is the peer endpoint of \({c}^{\overline{p}}\). A bindable name \(X\) is either a channel or a variable and a name \(u\) is either a bindable name or an endpoint.

Table 1. Syntax of expressions and processes.

The syntax of expressions and processes is given in Table 1. In addition to the usual constructs of the \(\lambda \)-calculus, expressions include constants, ranged over by , and pair splitting. Constant are the unitary value , the pair constructor , the primitives for session initiation and communication , , and  [11, 12], the monadic operations and  [23], and a primitive to defer computations [21, 22]. We do not need a primitive constant for the fixed point operator because it can be expressed and typed inside the language. For simplicity, we do not include primitives for branching and selection typically found in session calculi. They are straightforward to add and do not invalidate any of the results. Expressions are subject to the usual conventions of the \(\lambda \)-calculus. In particular, we assume that the bodies of abstractions extend as much as possible to the right, that applications associate to the left, and we use parentheses to disambiguate the notation when necessary. Following established notation, we write \(\langle e\texttt {,}f\rangle \) in place of , \(\lambda \langle {x}_1\texttt {,}{x}_2\rangle . e\) in place of , and \(e ~\texttt {>}\!\texttt {>=}~ f\) in place of .

A process can be either the idle process that performs no action, a thread \({x} \Leftarrow e\) with name \({x}\) and body \(e\) that evaluates the body and binds the result to \({x}\) in the rest of the system, a that waits for session initiations on the shared channel \({a}\) and spawns a new thread computing \(e\) at each connection, the parallel composition of processes, and the restriction of a bindable name. In processes, restrictions bind tighter than parallel composition and we may abbreviate \((\nu X_1)\cdots (\nu X_n)P\) with \((\nu X_1\cdots X_n)P\).

We have that binds both \({x}\) and \({y}\) in \(e\) and \((\nu {a})P\) binds \({a}^+\) and \({a}^-\) within \(P\) in addition to \({a}\). The definitions of free and bound names follow as expected. We identify expressions and processes up to renaming of bound names.

Table 2. Reduction semantics of expressions and processes.

The operational semantics of expressions is defined in the upper half of Table 2. Expressions reduce according to a standard call-by-name semantics, for which we define the evaluation contexts for expressions below:

Note that evaluation contexts do not allow to reduce pair components or an expression \(e\) in . We say that \(e\) is in normal form if there is no \(f\) such that \(e\longrightarrow f\).

The operational semantics of processes is given by a structural congruence relation \(\equiv \), which we leave undetailed since it is essentially the same as that of the \(\pi \)-calculus, and a reduction relation, defined in the bottom half of Table 2. The evaluation contexts for processes are defined as

$$ \mathscr {C}{:}{:}= [~]\mid \mathscr {C} ~\texttt {>}\!\texttt {>=}~ e$$

and force the left-to-right execution of monadic actions, as usual.

Rules [r-open] and [r-comm] model session initiation and communication, respectively. According to [r-open], a client thread opens a connection with a server \({a}\). In the reduct, a fresh session channel \({c}\) is created, the in the client reduces to the return of \({c}^+\) and a copy of the server is spawned into a new thread that has a fresh name \({y}\) and a body obtained from that of the server applied to \({c}^-\). According to [r-comm], two threads communicate if one is ready to send some message \(e\) on a session endpoint \({a}^p\) and the other is waiting for a message from the peer endpoint \({a}^{\overline{p}}\). As in [11], the communication primitives return the session endpoint being used, with the difference that in our case the results are monadic actions. In particular, the result for the sender is the same session endpoint and the result for the receiver is a pair consisting of the received message and the session endpoint.

Rules [r-future] and [r-return] deal with futures. The former spawns an I/O action \(e\) in a separate thread \({y}\), so that the spawner is able to reduce (using [r-bind]) even if \(e\) has not been executed yet. The name \({y}\) of the spawned thread can be used as a placeholder for the value yielded by \(e\). Rule [r-return] deals with a future variable \({x}\) that has been evaluated to . In this case, \({x}\) is replaced by \(e\) everywhere within its scope.

Rule [r-thread] lifts reduction of expressions to reduction of threads. The remaining rules close reduction under restrictions, parallel compositions, and structural congruence, as expected.

3 Typing SID

We now develop a typing discipline for SID. The challenge comes from the fact that the calculus allows a mixture of pure computations (handling data) and impure computations (doing I/O). In particular, SID programs can manipulate potentially infinite data while performing I/O operations that produce/consume pieces of such data as shown by the examples of Sect. 1. Some ingredients of the type system are easily identified from the syntax of the calculus. We have a core type language with unit, products, and arrows. As in [11], we distinguish between unlimited and linear arrows for there sometimes is the need to specify that certain functions must be applied exactly once. As in Haskell [21, 23], we use the type constructor to denote monadic I/O actions. For shared and session channels we respectively introduce channel types and session types [12]. Finally, following [19], we introduce the delay type constructor \(\bullet {^{}}\), so that an expression of type \(\bullet {^{}}t\) denotes a value of type \(t\) that is available “at the next moment in time”. This constructor is key to control recursion and attain normalisation of expressions. Moreover, the type constructors \(\bullet {^{}}\) and interact in non-trivial ways as shown later by the type of .

3.1 Types

The syntax of pseudo-types and pseudo-session types is given by the grammar in Table 3, whose productions are meant to be interpreted coinductively. A pseudo (session) type is a possibly infinite tree, where each internal node is labelled by a type constructor and has as many children as the arity of the constructor. The leaves of the tree (if any) are labelled by either basic types or . We use a coinductive syntax to describe infinite data structures (such as streams) and arbitrarily long protocols, such as the one betwen sender and receiver in Sect. 1.

Table 3. Syntax of Pseudo-types and Pseudo-session types.

We distinguish between unlimited pseudo-types (those denoting expressions that can be used any number of times) from linear pseudo-types (those denoting expressions that must be used exactly once). Let \(\mathsf {lin}\) be the smallest predicate defined by

The word “smallest” in the above definition is crucial. For example \(\mathsf {lin}\) does not hold for the type , because does not belong to the smallest set satisfying the above clauses. We say that \(t\) is linear if \(\mathsf {lin}(t)\) holds and that \(t\) is unlimited, written \(\mathsf {un}(t)\), otherwise. Note that all I/O actions are linear, since they may involve communications on session channels which are linear resources.

Definition 1

(Types). A pseudo (session) type \(t\) is a (session) type if:

  1. 1.

    For each sub-term \(t_1 \rightarrow t_2\) of \(t\) such that \(\mathsf {un}(t_2)\) we have \(\mathsf {un}(t_1)\).

  2. 2.

    For each sub-term \(t_1 \multimap t_2\) of \(t\) we have \(\mathsf {lin}(t_2)\).

  3. 3.

    The tree representation of \(t\) is regular, namely it has finitely many distinct sub-trees.

  4. 4.

    Every infinite path in the tree representation of \(t\) has infinitely many \(\bullet \)’s.

All conditions except possibly 4 are natural. Condition 1 essentially says that unlimited functions are pure, namely they do not have side effects. Indeed, an unlimited function (one that does not contain linear names) that accepts a linear argument should return a linear result. Condition 2 states that a linear function (one that may contain linear names) always yields a linear result. This is necessary to keep track of the presence of linear names in the function, even when the function is applied and its linear arrow type eliminated. For example, consider \({z}\) of type and both \({y}\) and \({w}\) of type , then without Condition 2 we could type \((\lambda {x}.{y})({z}\ {w})\) with . This would be incorrect, because it discharges the expression \(({z}\ {w})\) involving the linear name \({z}\). Condition 3 implies that we only consider types admitting a finite representation, for example using the well-known “\(\mu \) notation” for expressing recursive types (for the relation between regular trees and recursive types we refer to [24, Chap. 20]). We define infinite types as trees satisfying a given recursive equation, for which the existence and uniqueness of a solution follow from known results [6]. For example, there are unique pseudo-types , , and that respectively satisfy the equations , and . En passant, note that linearity is decidable on types due to Condition 3.

Condition 4 intuitively means that not all parts of an infinite data structure can be available at once: those whose type is prefixed by a \(\bullet \) are necessarily “delayed” in the sense that recursive calls on them must be deeper. For example, is a type that denotes streams of natural numbers where each subsequent element of the stream is delayed by one \(\bullet \) compared to its predecessor. Instead is not a type: it would denote an infinite stream of natural numbers, whose elements are all available right away. Similarly, and defined by and are session types, while and defined by and are not. The type is somehow degenerate in that it contains no actual data constructors. Unsurprisingly, we will see that non-normalising terms such as can only be typed with . Without Condition 4, could be given any type.

We adopt the usual conventions regarding arrow types (which associate to the right) and assume the following precedence among constructors: \(\rightarrow \), \(\multimap \), \(\times \), , \(\bullet \) with and \(\bullet \) having the highest precedence. We also need a notion of duality to relate the session types associated with peer endpoints. Our definition extends the one of [12] in the obvious way to delayed types. More precisely, the dual of a session type \(T\) is the session type \(\overline{T}\) coinductively defined by the equations:

Sometimes we will write \(\bullet {^{n}}t\) in place of .

3.2 Typing Rules

We show the typing of expressions and processes. First we assign types to constants:

Each constant is polymorphic and we use to denote the set of types assigned to , e.g. .

The types of and are as expected. The type schema of is similar to the type it has in Haskell, except for the two linear arrows. The leftmost linear arrow allows linear functions as the second argument of . The rightmost linear arrow is needed to satisfy Condition 1 of Definition 1, being linear. The type of is also familiar, except that the second arrow is linear or unlimited depending on the first element of the pair. If the first element of the pair is a linear expression, then it can (and actually must) be used for creating exactly one pair. The types of and are almost the same as in [11], except that these primitives return I/O actions instead of performing them as side effects. The type of is standard and obviously justified by its operational semantics. The most interesting type is that of , which commutes delays and the type constructor. Intuitively, applied to a delayed I/O action returns an immediate I/O that yields a delayed expression. This fits with the semantics of , since its argument is evaluated in a separate thread and the one invoking can proceed immediately with a placeholder for the delayed expression. If the body of the new thread reduces to , then \(e\) substitutes the placeholder.

The typing judgements for expressions have the shape \(\Upgamma \vdash e : t\), where typing environments (for used resources) \(\Upgamma \) are mappings from variables to types, from shared channels to shared channel types, and from endpoints to session types:

$$ \Upgamma \qquad {:}{:}= \qquad \emptyset \quad \mid \quad \Upgamma ,x:t\quad \mid \quad \Upgamma ,{a}:\langle T\rangle \quad \mid \quad \Upgamma , {a}^{p}:T$$

A typing environment \(\Upgamma \) is linear, notation \(\mathsf {lin}(\Upgamma )\), if there is \(u: t\in \Upgamma \) such that \(\mathsf {lin}(t)\); otherwise \(\Upgamma \) is unlimited, notation \(\mathsf {un}(\Upgamma )\). As in [11], we use a (partial) combination operator \(+\) for environments, that prevents names with linear types from being duplicated. Formally the environment \(\Upgamma + \Upgamma '\) is defined inductively on \(\Upgamma '\) by

$$ \begin{array}{@{}rcl@{}} \Upgamma + \emptyset &{} = &{} \Upgamma \\ \Upgamma +(\Upgamma ', u:t) &{} = &{} (\Upgamma +\Upgamma ')+u:t \end{array} \quad \text {where}\quad \Upgamma +u:t={\left\{ \begin{array}{ll} \Upgamma ,u:t&{} \text {if }u\not \in \mathsf {dom}(\Upgamma ), \\ \Upgamma &{} \text {if }u:t\in \Upgamma \text { and }\mathsf {un}(t), \\ \text {undefined} &{} \text {otherwise}. \end{array}\right. } $$
Table 4. Typing rules for expressions.

The typing axioms and rules for expressions are given in Table 4. They are essentially the same as those found in [11], except for two crucial details. First of all, each rule allows for an arbitrary delay in front of the types of the entities involved. Intuitively, the number of \(\bullet {^{}}\)’s represents the delay at which a value becomes available. So for example, rule [\(\rightarrow \) I] says that a function which accepts an argument \({x}\) of type \(t\) delayed by n and produces a result of type \(s\) delayed by the same n has type \(\bullet {^{n}}(t\rightarrow s)\), that is a function delayed by n that maps elements of \(t\) into elements of \(s\). The second difference with respect to the type system in [11] is the presence of rule [\(\bullet \) I], which allows to further delay a value of type \(t\). Crucially, it is not possible to anticipate a delayed value: if it is known that a value will only be available with delay n, then it will also be available with any delay \(m \ge n\), but not earlier. Using rule [\(\bullet \) I], we can derive that the fixed point combinator has type \((\bullet {^{}}t\rightarrow t)\rightarrow t\), by assigning to the variable x the type \(s\) such that \(s= \bullet {^{}}s\rightarrow t\) [19]. The side condition \(\mathsf {un}(\Upgamma )\) in \({[{\textsc {const}}]}\), \({[{\textsc {axiom}}]}\), and [\(\rightarrow \) I] is standard [11].

It is possible to derive the following types for the functions in Sect. 1:

where, in the derivation for , we assume type for \(g\). We show the most interesting parts of this derivation. We use the following rules, which are easily derived from those in Table 4 and the types of the constants.

In order to derive the type of we desugar its recursive definition in Sect. 1 as , where

Now we derive

figure a

The derivation \(\nabla \) is as follows.

figure b

Note that the types of the premises of [\(\rightarrow \) E] in the above derivation have a \(\bullet \) constructor in front. Moreover, has a type that pushes the \(\bullet \) inside the ; this is crucial for typing \(e_4\) with . We can assign the type to \(e_4\) by guarding the argument \({z}\) of type under the constructor . Without , the expression \(e_3 ~\texttt {>}\!\texttt {>=}~ e_4\) would have type and would be untypeable.

Table 5. Typing rules for processes.

The typing judgements for processes have the shape \(\Upgamma \vdash P ~\triangleright ~ \Updelta \), where \(\Upgamma \) is a typing environment as before, while \(\Updelta \) is a resource environment, keeping track of the resources defined in \(P\). In particular, \(\Updelta \) maps the names of threads and servers in \(P\) to their types and it is defined by

$$ \Updelta \qquad {:}{:}= \qquad \emptyset \quad \mid \quad \Updelta ,x:t\quad \mid \quad \Updelta ,{a}:\langle T\rangle $$

Table 5 gives the typing rules for processes. A thread is well typed if so is its body, which must be an I/O action. The type of a thread is that of the result of its body, where the delay moves from the I/O action to the result. The side condition makes sure that the thread is unable to use the very value that it is supposed to produce. The resulting environment for defined resources associates the name of the thread with the type of the action of its body. A server is well typed if so is its body \(e\), which must be a function from the dual of \(T\) to an I/O action. This agrees with the reduction rule of the server, where the application of \(e\) to an endpoint becomes the body of a new thread each time the server is invoked. It is natural to forbid occurrences of free variables and shared channels in server bodies. This is assured by the condition \(\mathsf {shared}(\Upgamma )\), which requires \(\Upgamma \) to contain only shared channels. Clearly \(\mathsf {shared}(\Upgamma )\) implies \(\mathsf {un}(\Upgamma )\), and then we can type the body \(e\) with a non linear arrow. The type of the new thread (which will be \(t\) if \(e\) has type ) must be unlimited, since a server can be invoked an arbitrary number of times. The environment \(\Upgamma + {a}: \langle T\rangle \) in the conclusion of the rule makes sure that the type of the server as seen by its clients is consistent with its definition.

The remaining rules are conventional. In a parallel composition we require that the sets of entities (threads and servers) defined by \(P_1\) and \(P_2\) are disjoint. This is enforced by the fact that the respective resource environments \(\Updelta _1\) and \(\Updelta _2\) are combined using the operator \(\_,\_\) which (as usual) implicitly requires that \(\mathsf {dom}(\Updelta _1) \cap \mathsf {dom}(\Updelta _2) = \emptyset \). The restriction of a session channel \({a}\) introduces associations for both its endpoints \({a}^+\) and \({a}^-\) in the typing environment with dual session types, as usual. Finally, the restriction of a bindable name \(X\) introduces associations in both the typing and the resource environment with the same type \(t\). This makes sure that in \(P\) there is exactly one definition for \(X\), which can be either a variable which names a thread or a shared channel which names a server, and that every usage of \(X\) is consistent with its definition.

4 Main Results

In this section we state the main properties enjoyed by typed SID programs. The first expected property is that reduction of expressions preserves their types.

Theorem 1

(Subject Reduction for Expressions). If \(\Upgamma \vdash e : t\) and \(e\longrightarrow e'\), then \(\Upgamma \vdash e' : t\).

Besides the usual substitution lemma, the proof of the above theorem needs the delay lemma, which states that if an expression \(e\) has type \(t\) from \(\Upgamma \), then it has type \(\bullet t\) from \(\bullet \Upgamma \). This property reflects the fact that we can only move forward in time.

As informally motivated in Sect. 3, the type constructor \(\bullet \) controls recursion and guarantees normalisation of any expression that has a type different from .

Theorem 2

(Normalisation of Typeable Expressions). If \(\Upgamma \vdash e : t\) and , then \(e\) reduces (in zero or more steps) to a normal form.

The proof of Theorem 2 makes use of a type interpretation indexed on the set of natural numbers, similar to the one given in [19]. Note that, since \(\textsf {SID}\) is lazy, expressions such as and \(\langle e\texttt {,}f\rangle \) are in normal form for all \(e\) and \(f\).

An initial process models the beginning of a computation and it is formally defined as a closed, well-typed process \(P\) such that

By definition, an initial process does not contain undefined names (hence it is typeable from the empty environment) and consists of only one thread \({x}\) – usually called “main” in most programming languages – and an arbitrary number of servers. In particular, typeability guarantees that all bodies normalise and all ’s refer to existing servers.

We say that a process is reachable if it is the reduct of an initial process. Unlike an initial process, a reachable process may have several threads running in parallel, resulting from either service invocation or .

Theorem 3

(Subject Reduction for Processes). All reachable processes are typeable.

The most original and critical aspect of the proof is to check that reachable processes do not have circular dependencies on session channels and variables. The absence of circularities can be properly formalized by means of a judgement that characterises the sharing of names among threads, inspired by the typing of the parallel composition given in [16]. Intuitively, it captures the following properties of reachable processes and makes them suitable for proving both subject reduction and progress:

  1. 1.

    two threads can share at most one session channel;

  2. 2.

    distinct endpoints of a session channel always occur in different threads;

  3. 3.

    if the name of one thread occurs in the body of another thread, then these threads cannot share session channels nor can the first thread mention the second.

Next, we show several examples of processes that are irrelevant to us because, in spite of being typeable, they are not reachable. Examples (4.1) and (4.2) violate condition (3), (4.3) violates condition (1), and (4.4) violates condition (2).

The first example is given by the process

(4.1)

which is well typed by assigning both \({x}\) and \({y}\) any unlimited type, whereas , which is its reduct, is ill typed, because the thread name \({x}\) occurs free in its body (cf. the side condition of [thread]). Another paradigmatic example is

(4.2)

which is well typed in the environment , \({y}: t\), where , and which reduces to . Again, the reduct is ill typed because the name \({y}\) of the thread occurs free in its body.

Another source of problems that usually requires specific handling [2, 4] is that there exist well-typed processes that are (or reduce to) configurations where mutual dependencies between sessions and/or thread names prevent progress. For instance, both

(4.3)
(4.4)

are well typed but also deadlocked. Again, processes like this one are not reachable hence they are not a concern in our case.

We now turn our attention to the progress property. A computation stops when there are no threads left. Recall that the reduction rule \({[{\textsc {r-return}}]}\) (cf. Table 2) erases threads. Since servers are permanent we say that a process \(P\) is final if

In particular, the idle process is final, since m can be 0.

We can state the progress property as follows:

Theorem 4

(Progress of Reachable Processes). A reachable process either reduces or it is final. Moreover a non-terminating reachable process reduces in a finite number of steps to a process to which one of the rules \({[{\textsc {r-open}}]}\), \({[{\textsc {r-comm}}]}\) or \({[{\textsc {r-future}}]}\) can be applied.

In other words, every infinite reduction of a reachable process performs infinitely many communications and/or spawns infinitely many threads. The proof of Theorem 4 requires to define a precedence between threads and prove that this relation is acyclic.

As an example, let

where

is the process discussed in the Introduction. It is easy to verify that

reduces to process \(Q\). Note that \(P_0\) is typeable, and indeed an initial process. Hence, by Theorems 3 and 4, process \(Q\) is typeable and has progress. The last property of SID we discuss is the diamond property [24, Sect. 30.3].

Theorem 5

(Confluence of Reachable Processes). Let \(P\) be a reachable process. If \(P\longrightarrow P_1\) and \(P\longrightarrow P_2\), then there is \(P_3\) such that \(P_1 \longrightarrow P_3\) and \(P_2 \longrightarrow P_3\).

The proof is trivial for expressions, since there is only one redex at each reduction step. However, for processes we may have several redexes to contract at a time and the proof requires to analyse these possibilities. The fact that we can mix pure evaluations and communications and still preserve determinism is of practical interest.

We conclude this section discussing two initial processes whose progress is somewhat degenerate. The first one realises an infinite sequence of delegations (the act of sending an endpoint as a message), thereby postponing the use of the endpoint forever:

where

We have that and where \( \mathsf{RS}_t = {\texttt {?}}t.{\texttt {!}}t.\bullet {^{}}\mathsf{RS}_t \) and \( \mathsf{SR}_t = {\texttt {!}}t.{\texttt {?}}t.\bullet {^{}}\mathsf{SR}_t \). Since no communication ever takes place on the session created with server \({b}\), violates the progress property as defined in [8].

The second example is the initial process , where . This process only creates new threads.

5 Conclusions

This work addresses the problem of studying the interaction between communications and infinite data structures by means of a calculus that combines sessions with lazy evaluation. A distinguished feature of SID is the possibility of modelling computations in which infinite communications interleave with the production and consumption of infinite data (cf. the examples in Sect. 1). Our examples considered infinite streams for simplicity. However, more general infinite data structures can be handled in SID. An evaluation of the expressiveness of SID in dealing with (distributed) algorithms based on such structures is scope for future investigations.

The typing discipline we have developed for SID guarantees normalisation of expressions with a type other than and progress of (reachable) processes, besides the standard properties of sessions (communication safety, protocol fidelity, determinism). The type system crucially relies on a modal operator \(\bullet \) which has been used in a number of previous works [3, 14, 19, 26] to ensure productivity of well-typed expressions. In this paper, we have uncovered for the first time some intriguing interactions between this operator and the typing of impure expressions with the monadic type constructor. Conventionally, the type of primitive is simply and says nothing about the semantics of the primitive itself. In our type system, the type of reveals its effect as an operator that turns a delayed computation into another that can be performed immediately, but which produces a delayed result.

As observed at the end of Sect. 1 and formalised in Theorem 4, our notion of progress sits somehow in between deadlock and lock freedom. It would be desirable to strengthen the type system so as to guarantee the (eventual) execution of all pending communications and exclude, for instance, the degenerate examples discussed at the end of Sect. 4. This is relatively easy to achieve in conventional process calculi, where expressions only consist of names or ground values [2, 4, 20], but it is far more challenging in the case of SID, where expressions embed the \(\lambda \)-calculus. We conjecture that one critical condition to be imposed is to forbid postponing linear computations, namely restricting the application of [\(\bullet \) I] to non-linear types. Investigations in this direction are left for future work.

Another obvious development, which is key to the practical applicability of our theory, is the definition of a type inference algorithm for our type system. In this respect, the modal operator \(\bullet \) is challenging to deal with because it is intrinsically non-structural, not corresponding to any expression form in the calculus.