figure a
figure b

1 Introduction

Fig. 1.
figure 1

MPST method

Fig. 2.
figure 2

Example runs of Adder

Fig. 3.
figure 3

Global type for Adder

Fig. 4.
figure 4

Local type for Client in Adder

Construction and analysis of distributed systems is hard. One of the challenges is this: given a specification \(\mathcal {S}\) of the roles and the protocols an implementation \(\mathcal {I}\) of processes and communication sessions should fulfil, can we prove that \(\mathcal {I}\) is safe and live relative to \(\mathcal {S}\)? Safety means “bad” communication actions never happen: if a channel action happens in \(\mathcal {I}\), then it is allowed by \(\mathcal {S}\). Liveness means “good” communication actions eventually happen (communication deadlock freedom).

Multiparty session typing (MPST) [14, 15] is a method to automatically prove safety and liveness of protocol implementations. The idea is shown in Figure 1:

  1. 1.

    First, a protocol among roles \(r_1, \ldots , r_n\) is implemented as a session of processes \(P_1, \ldots , P_n\) (concrete), while it is specified as a global type G (abstract). The global type models the behaviour of all processes together (e.g., “first, a number from Alice to Bob; next, a boolean from Bob to Carol”).

  2. 2.

    Next, G is decomposed into local types \(L_1, \ldots , L_n\) by projecting G onto every role. Each local type models the behaviour of one process alone (e.g., for Bob, “first, he receives from Alice; next, he sends to Carol”).

  3. 3.

    Last, absence of communication errors is verified by type-checking every process \(P_i\) against its local type \(L_i\). MPST theory assures that well-typedness at compile-time implies safety and liveness at run-time.

The following simple example demonstrates global types and local types in Scribble notation [28], as used in the Scribble tool [16, 17] for the MPST method.

Example 1

The Adder protocol [12] consists of two roles: Client (\(\smash {\texttt {{\small C}}}\)) and Server (\(\smash {\texttt {{\small S}}}\)). Client either asks Server to add two numbers (\(\smash {\texttt {{\small Add}}}\)-message with two \(\smash {\texttt {{\small Int}}}\)-payloads) or tells Server goodbye (\(\smash {\texttt {{\small Bye}}}\)-message). In the former case, Server tells Client the result (\(\smash {\texttt {{\small Res}}}\)-message). This is repeated until Server is told goodbye.

Figure 2 shows three example runs as sequence diagrams. Figure 3 shows the global type. Notation “” specifies the communication of a message of type m with payloads of types \(t_1, \ldots , t_n\) from role p to role q. Notation “” specifies a choice among branches \(G_1, \ldots , G_k\) made by role r. Figure 4 shows the local type for Client. The notation for local types resembles the notation for global types, except that communications are broken up into sends (“”) and receives (“”).    \(\square \)

Fig. 5.
figure 5

Workflow of API-generation-based tools for the MPST method

Fig. 6.
figure 6

DFA and Java API for Client in Adder (Scribble-style)

A premier approach to apply the MPST method in combination with mainstream programming languages is based on API generation (Figure 5); it is used in the majority of MPST tools, including Scribble [16, 17], its extensions [5, 8, 9, 22, 23, 25, 27, 32, 35], StMungo [21], \(\upnu \)Scr [34], mpstpp [20], and Pompset [6]. The main ideas, first conceived by Deniélou/Hu/Yoshida and pursued in Scribble, follow two insights: (a) local types can be interpreted as deterministic finite automata (DFA) [10, 11], where every transition models a send/receive action; (b) DFAs can be encoded as object-oriented application programming interfaces (API) [16, 17], where classes and methods model states and transitions.

Fig. 7.
figure 7

Process for Client in Adder

Example 2

Figure 6 shows the DFA and a Java API for Client in Adder (Example 1), in the style of Scribble. Transition labels of the form and in the DFA specify the send to q and the receive from p of a message of type m with payloads of types \(t_1, \ldots , t_n\). Classes , , and in the API correspond to states 1, 2, and 3 of the DFA; the methods of class in the API correspond to the transitions from state i in the DFA.

Figure 7 shows a process for Client, using the Java API. The idea is to write method that consumes an “initial state object” as input and produces a “final state object” as output. First, the only communication actions that can be performed, are those for which has a method. When called, the communication action is performed and a fresh “successor state object” (line 4) or (line 8) is returned. Next, the only communication actions that can be performed, are those for which or has a method. And so on. By using state objects in this way, a run of method simulates a run of the DFA.    \(\square \)

However, existing API-generation-based tools that follow Example 2 in MPST , do not fully meet the promise of MPST , in two ways:

  1. 1.

    Mixed static/dynamic checks: To ensure safety and liveness, every non-final state object must be used linearly (exactly one method call). However, the type systems of most mainstream programming languages are too weak to check linear usage statically. Instead, dynamic checks are needed (e.g., method in Figure 6). As a result, MPST practice is weaker than MPST theory: in MPST practice, some errors are reported late at run-time, whereas in MPST theory, all errors are reported early at compile-time.

  2. 2.

    Resource-inefficient checks: Every time when a communication action is performed, a fresh state object is created. This costs time (allocation; garbage collection) and space. As a result, MPST practice is costlier at run-time than MPST theory: in MPST practice, API-encodings of DFA-interpretations of local types have a real footprint (proportionate to the number of communication actions), whereas in MPST theory, local types are zero cost abstractions.

In this paper, we present BGJ: a new API-generation-based tool to apply the MPST method in combination with Java. The checks performed using BGJ are purely static (all errors are reported early at compile-time) and resource-efficient (near-zero cost abstractions at run-time), thereby addressing the issues above. Instead of building a new static analyser from scratch, we leverage a state-of-the-art deductive verifier for Java, namely VerCors [2]. Under active development for years, VerCors has been used in industrial case studies, too [18, 26, 30]. We note that our approach is generic, though, while our current tool is VerCors-specific.

2 Usage: BGJ in a Nutshell

BGJ follows the same workflow as in Figure 5. We explain the steps below.

Steps 1-3: global types; local types; DFAs. First, the programmer manually writes a global type in Scribble notation (e.g., Figure 3). Next, BGJ automatically projects the global type to local types, and it automatically interprets the local types as DFAs. This is standard and as usual [16, 17].

Step 4: APIs. Next, BGJ automatically encodes the DFAs as APIs. Our approach is to encode a DFA of n states as an API of a single class instead of n classes (Figure 6). At run-time, only one instance of this class is created (“near-zero cost abstraction”); this instance allows any number of usages (method calls). To be able to check that these usages are proper, a key novelty of our approach is that BGJ also generates annotations for method contracts, Hoare-logic-style.

Fig. 8.
figure 8

Java API for Client in Adder (BGJ-style)

Example 3

Figure 8 shows the Java API for Client in Adder (Example 1), generated using BGJ (cf. Figure 6). Field of class identifies the current state; the methods of class correspond to transitions. The annotations (“”) define for each method: a precondition (“”; what must be true before a call?), a postcondition (“”; what will be true after?), and a method invariant (“”; read/write permissions for which fields are needed?).    \(\square \)

Fig. 9.
figure 9

Process for Client in Adder

Step 5: processes. Last, the programmer manually writes processes using the APIs and automatically verifies proper usage with VerCors (i.e., methods are called only if the preconditions hold). These checks are purely static. If successful, safety relative to the global type and liveness (communication deadlock freedom) are assured; else, a bug is found (“all errors are reported early at compile-time”).

Example 4

Figure 9 shows a process for Client in Adder (Example 1), using the Java API in Figure 8. It resembles Figure 7, except that method and the loop are annotated with a simple contract and invariant. Using VerCors, we can verify that the methods are called only if the preconditions hold. Conversely, if we duplicate line 8, then VerCors reports an error: consecutively sending two \(\smash {\texttt {{\small Add}}}\)-messages is forbidden. This can be detected only dynamically in Figure 7 (i.e., a would be thrown in of Figure 6).    \(\square \)

3 Implementation

BGJ is implemented in Java. It reuses the front-end of Scribble for global types, local types, and DFAs in steps 1-3 and, thus, supports the same features (including input branching). The encoder of DFAs as APIs in step 4 is new. It generates two versions of every API: concrete (e.g., Figure 8) and abstract (e.g., Figure 8 without “”). The concrete API is for running a process. The abstract API, which omits all verification-irrelevant details, is for verifying a process.Footnote 1 At run-time, TCP is used to transport messages between processes.

Besides the APIs, BGJ also generates “skeletons” of process code. These skeletons represent the basic control flow (adapted from the DFAs) with and method calls in the right places (guaranteed to pass verification). The skeletons can subsequently be filled in with the actual computations.

4 Preliminary Evaluation

We obtained first practical experience with BGJ to study its two improvements. Regarding “all errors are reported early at compile-time”, we investigated how much time the verification step of VerCors takes for eight example protocols in Scribble’s repository [13]. Figure 10 shows the results, averaged over thirty runs, using generated skeletons as process code. A preliminary conclusion is that the extra time can be low enough (worth the effortFootnote 2) for our approach to be feasible.

Fig. 10.
figure 10

Time of VerCors (in seconds)

Regarding “near-zero cost abstractions at run-time”, we investigated run-time overhead of a Scribble-based process (e.g., Figure 6) vs. a BGJ-based process (e.g., Figure 8) for Client in Adder. We factored out code common to both versions (e.g., actual transport of messages over the wire), to be able to specifically measure the impact of the differences (methodology of Castro et al. [5]). Averaged over thirty runs, the Scribble-based process and the BGJ-based process completed \(2^{31}\) () iterations in 5221ms and 974ms, respectively. Our preliminary conclusion is that our approach is indeed more resource-efficient.

5 Conclusion

Related work. The combination of the MPST method and deductive verification is largely unexplored territory. The only other work, by López et al. [24], uses deductive verifier VCC [7] to statically check safety and liveness of C+MPI protocol implementations relative to MPST-based specifications. Their approach is very different from ours, though, as it is not based on API generation.

The approach of encoding DFAs of n states as APIs of a single class was recently studied by Cledou et al. [6], by leveraging advanced features of the type system of Scala 3. Their approach does not address the issues in Section 1, though, whereas our approach does. Previous attempts to address the issue of “mixed static/dynamic checks” either target a programming language with a stronger type system (Rust) [8, 9, 22, 23], or adopt callback-style APIs in the specific context of event-based programming [34, 35]. In contrast, our approach does not rely on (the strength of) the type system of the targeted programming language, and it supports traditional procedural/object-oriented programming.

Closest to BGJ is StMungo [21]: the approaches of both tools are similar, but the underlying static analysis techniques differ. BGJ leverages method contracts and deductive verification, while StMungo is based on typestate [33]. A key advantage of using deductive verification is that it immediately opens the door to reasoning about functional correctness (next paragraph).

Future work. There are two next steps. First, now that we have the infrastructure to combine the MPST method and deductive verification, we are keen to explore their further integration to reason about functional correctness of distributed systems. VerCors is based on concurrent separation logic [4, 29], so key capabilities to reason about concurrency are already in place. This is connected to work in which separation logic is used to control I/O operations (e.g., Penninckx et al. [31]). Second, while the usage of deductive verification is central to BGJ, our approach does not crucially depend on VerCors: we chose it because it is a fully automated, well-supported deductive verifier for Java, but other tools (e.g., KeY [1], VeriFast [19]) offer opportunities worth investigating, too.