{'fraction_non_alphanumeric': 0.036090036992914916, 'fraction_numerical': 0.008652580099065773, 'mean_word_length': 4.851199647809818, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 8, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 7, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
From Clarity to Efficiency for Distributed Algorithms

This article describes a very high-level language for clear description of distributed algorithms and optimizations necessary for generating efficient implementations. The language supports high-level control flows where complex synchronization conditions can be expressed using high-level queries, especially logic quantifications, over message history sequences. Unfortunately, the programs would be extremely inefficient, including consuming unbounded memory, if executed straightforwardly. We present new optimizations that automatically transform complex synchronization conditions into incremental updates of necessary auxiliary values as messages are sent and received. The core of the optimizations is the first general method for efficient implementation of logic quantifications. We have developed an operational semantics of the language, implemented a prototype of the compiler and the optimizations, and successfully used the language and implementation on a variety of important distributed algorithms.


Introduction
Distributed algorithms are at the core of distributed systems. Yet, developing practical implementations of distributed algorithms with correctness and efficiency assurances remains a challenging, recurring task.
• Study of distributed algorithms has relied on either pseudocode with English, which is high-level but imprecise, or formal specification languages, which are precise but harder to understand, lacking mechanisms for building real distributed systems, or not executable at all.
• At the same time, programming of distributed systems has mainly been concerned with program efficiency and has relied mostly on the use of low-level or complex libraries and to a lesser extent on built-in mechanisms in restricted programming models.
What's lacking is (1) a simple and powerful language that can express distributed algorithms at a high level and yet has a clear semantics for precise execution as well as for verification, and is fully integrated into widely used programming languages for building real distributed systems, together with (2) powerful optimizations that can transform high-level algorithm descriptions into efficient implementations. This article describes a very high-level language, DistAlgo, for clear description of distributed algorithms, combining advantages of pseudocode, formal specification languages, and programming languages.
• The main control flow of a process, including sending messages and waiting on conditions about received messages, can be stated directly as in sequential programs; yield points where message handlers execute can be specified explicitly and declaratively.
• Complex synchronization conditions can be expressed using high-level queries, especially quantifications, over message history sequences, without manually writing message handlers that perform low-level incremental updates and obscure control flows.
DistAlgo supports these features by building on an objectoriented programming language. We also developed an operational semantics for the language. The result is that distributed algorithms can be expressed in DistAlgo clearly at a high level, like in pseudocode, but also precisely, like in formal specification languages, facilitating formal verification, and can be executed as part of real applications, as in programming languages. Unfortunately, programs containing control flows with synchronization conditions expressed at such a high level are extremely inefficient if executed straightforwardly: each quantifier can introduce a linear factor in running time, and any use of the history of messages sent and received may cause space usage to be unbounded.
We present new optimizations that allow efficient implementations to be generated automatically, extending previous optimizations to distributed programs and to the most challenging quantifications.
• Our method transforms sending and receiving of messages into updates to message history sequences, incrementally maintains the truth values of synchronization conditions and necessary auxiliary values as those sequences are updated, and finally removes those sequences as dead code when appropriate.
• To incrementally maintain the truth values of general quantifications, our method first transforms them into aggregations, also called aggregate queries. In general, however, translating nested quantifications simply into nested aggregations can incur asymptotically more space and time overhead than necessary. Our transformations minimize the nesting of the resulting queries.
• Quantified order comparisons are used extensively in nontrivial distributed algorithms. They can be incrementalized easily when not mixed with other conditions or with each other. We systematically extract single quantified order comparisons and transform them into efficient incremental operations.
Overall, our method significantly improves time complexities and reduces the unbounded space used for message history sequences to the auxiliary space needed for incremental computation. Systematic incrementalization also allows the time and space complexity of the generated programs to be analyzed easily. There has been a significant amount of related research, as discussed in Section 7. Our work contains three main contributions: • A simple and powerful language for expressing distributed algorithms with high-level control flows and synchronization conditions, an operational semantics, and full integration into an object-oriented language.
• A systematic method for incrementalizing complex synchronization conditions with respect to all sending and receiving of messages in distributed programs. • A general and systematic method for generating efficient implementations of arbitrary logic quantifications together with general high-level queries.
We have implemented a prototype of the compiler and the optimizations and experimented with a variety of important distributed algorithms, including Paxos, Byzantine Paxos, and multi-Paxos. Our experiments strongly confirm the benefits of the language and the effectiveness of the optimizations.
This article is a revised version of Liu et al. [56]. The main changes are revised and extended descriptions of the language and the optimization method, a new formal operational semantics, an abridged and updated description of the implementation, and a new description of our experience of using DistAlgo in teaching.

Expressing distributed algorithms
Even when a distributed algorithm appears simple at a high level, it can be subtle when necessary details are considered, making it difficult to understand how the algorithm works precisely. The difficulty comes from the fact that multiple processes must coordinate and synchronize to achieve global goals, but at the same time, delays, failures, and attacks can occur. Even determining the ordering of events is nontrivial, which is why Lamport's logical clock [43] is so fundamental for distributed systems.
Running example. We use Lamport's distributed mutual exclusion algorithm [43] as a running example. Lamport developed it to illustrate the logical clock he invented. The problem is that n processes access a shared resource, and need to access it mutually exclusively, in what is called a critical section (CS), i.e., there can be at most one process in a critical section at a time. The processes have no shared memory, so they must communicate by sending and receiving messages. Lamport's algorithm assumes that communication channels are reliable and first-in-first-out (FIFO). Figure 1 contains Lamport's original description of the algorithm, except with the notation < instead of −→ in rule 5 (for comparing pairs of timestamps and process ids using lexical ordering: (a,b) < (a2,b2) iff a < a2 or a = a2 and b < b2) and with the word "acknowledgment" added in rule 5 (for simplicity when omitting a commonly omitted [29,59] small optimization mentioned in a footnote). This description is the most authoritative, is at a high level, and uses the most precise English we found.
The algorithm satisfies safety, liveness, and fairness, and has a message complexity of 3(n − 1). It is safe in that at most one process can be in a critical section at a time. It is live in that some process will be in a critical section if there are requests. It is fair in that requests are served in the order of the logical timestamps of the request messages. Its message complexity is 3(n − 1) in that 3(n − 1) messages are required to serve each request.
Challenges. To understand how this algorithm is carried out precisely, one must understand how each of the n processes acts as both P i and P j in interactions with all other pro-The algorithm is then defined by the following five rules. For convenience, the actions defined by each rule are assumed to form a single event.
1. To request the resource, process Pi sends the message Tm:Pi requests resource to every other process, and puts that message on its request queue, where Tm is the timestamp of the message.
2. When process Pj receives the message Tm:Pi requests resource, it places it on its request queue and sends a (timestamped) acknowledgment message to Pi.
3. To release the resource, process Pi removes any Tm:Pi requests resource message from its request queue and sends a (timestamped) Pi releases resource message to every other process.
4. When process Pj receives a Pi releases resource message, it removes any Tm:Pi requests resource message from its request queue.
5. Process Pi is granted the resource when the following two conditions are satisfied: (i) There is a Tm:Pi requests resource message in its request queue which is ordered before any other request in its queue by the relation <. (To define the relation < for messages, we identify a message with the event of sending it.) (ii) Pi has received an acknowledgment message from every other process timestamped later than Tm. Note that conditions (i) and (ii) of rule 5 are tested locally by Pi. cesses. Each process must have an order of handling all the events according to the five rules, trying to reach its own goal of entering and exiting a critical section while also responding to messages from other processes. It must also keep testing the complex condition in rule 5 as events happen.
State machine based formal specifications have been used to fill in such details precisely, but at the same time, they are lower-level and harder to understand. For example, a formal specification of Lamport's algorithm in I/O automata [59, pages 647-648] occupies about one and a fifth pages, most of which is double-column.
To actually implement distributed algorithms, details for many additional aspects must be added, for example, creating processes, letting them establish communication channels with each other, incorporating appropriate logical clocks (e.g., Lamport clock or vector clock [60]) if needed, guaranteeing the specified channel properties (e.g., reliable, FIFO), and integrating the algorithm with the application (e.g., specifying critical section tasks and invoking the code for the algorithm as part of the overall application). Furthermore, how to do all of these in an easy and modular fashion?
Our approach. We address these challenges with the DistAlgo language, compilation to executable programs, and especially optimization by incrementalization of expensive synchronizations, described in Sections 3, 4, and 5, respectively. An unexpected result is that incrementalization led us to discover simplifications of Lamport's original algorithm in Figure 1; the simplified algorithm can be expressed using basically two send statements, a receive definition, and an await statement.
The results on the running example are shown in Figures 2-5, with details explained later. Figure 2 shows Lamport's original algorithm expressed in DistAlgo; it also includes configuration and setup for running 50 processes each trying to enter critical section at some point during its execution. Figures 3 and 4 show two alternative optimized programs after incrementalization; all lines with comments are new except that the await statement is simplified. Figure 5 shows the simplified algorithm.

DistAlgo Language
To support distributed programming at a high level, four main concepts can be added to commonly used high-level programming languages, especially object-oriented languages, such as Python and Java: (1) distributed processes, and sending messages, (2) control flows with yield points and waits, and receiving messages, (3) synchronization conditions using high-level queries of message history sequences, and (4) configuration of processes and communication mechanisms. DistAlgo supports these concepts, with options and generalizations for ease of programming, as described below. A formal operational semantics for DistAlgo is presented in Appendix A.
Processes and sending of messages. Distributed processes are concurrent executions of programmed instructions, like threads in Java and Python, except that each process has its private memory, not shared with other processes, and processes communicate by message passing. Three main constructs are used, for defining processes, creating processes, and sending messages.
A process definition is of form (1) below. It defines a type p of processes, by defining a class p that extends class process. The process_body is a set of method definitions and handler definitions, to be described.
class p extends process: (1) process_body A special method setup may be defined in process_body for initially setting up data in the process before the process's execution starts. A special method run() may be defined in process_body for carrying out the main flow of execution. A special variable self refers to the process itself.
A process creation statement is of form (2) below. It creates n new processes of type p at each node in the value of expression node_exp, and returns the resulting process or set of processes. A node is a running DistAlgo program on a machine, and is identified by the host name of the machine plus the name of the running DistAlgo program that can be specified when starting the program.
The number n and the at clause are optional; the defaults are 1 and the local node, respectively. A new process can be set up by calling its setup method. A call start() on the process then starts the execution of its run() method. A statement for sending messages is of form (3) below. It sends the message that is the value of expression mexp to the process or set of processes that is the value of expression pexp.
A message can be any value but is by convention a tuple whose first component is a string, called a tag, indicating the kind of the message.
Control flows and handling of received messages. The key idea is to use labels to specify program points where control flow can yield to handling of messages and resume afterwards. Three main constructs are used, for specifying yield points, handling of received messages, and synchronization.
A yield point preceding a statement is of form (4) below, where identifier l is a label. It specifies that point in the program as a place where control yields to handling of unhandled messages, if any, and resumes afterwards.
The label l is optional; it can be omitted when this yield point is not explicitly referred to in any handler definitions, defined next. A handler definition, also called a receive definition, is of form (5) below. It handles, at yield points labeled l 1 , ..., l j , un-handled messages that match some mexp i sent from pexp i , where mexp i and pexp i are parts of a tuple pattern; previously unbound variables in a pattern are bound to the corresponding components in the value matched. The handler_body is a sequence of statements to be executed for the matched messages. receive mexp1 from pexp1, ..., mexp k from pexp k at l1, ..., lj : The from and at clauses are optional; the defaults are any process and all yield points, respectively. If the from clause is used, each message is automatically extended with the process id of the sender. A tuple pattern is a tuple in which each component is a non-variable expression, a variable possibly prefixed with "=", a wildcard, or recursively a tuple pattern. A non-variable expression or a variable prefixed with "=" means that the corresponding component of the tuple being matched must equal the value of the non-variable expression or the variable, respectively, for pattern matching to succeed. A variable not prefixed with "=" matches any value and becomes bound to the corresponding component of the tuple being matched. A wildcard, written as "_", matches any value. Support for receive mimics common usage in pseudocode, allowing a message handler to be associated with multiple yield points without using method definition and invocations. As syntactic sugar, a receive that is handled at only one yield point can be written at that point.
Synchronization and associated actions can be expressed using general, nondeterministic await statements. A simple await statement is one of the two forms in (6) below. It waits for the value of Boolean-valued expression bexp to become true, for the first form, or waits for a timeout after time period t, for the second form.
await bexp (6) await timeout t A general, nondeterministic await statement is of form (7) below. It waits for any of the values of expressions bexp 1 , ..., bexp k to become true or a timeout after time period t, and then nondeterministically selects one of statements stmt 1 , ..., stmt k , stmt whose corresponding conditions are satisfied to execute. The or and timeout clauses are optional. await bexp1: stmt1 or ...
(7) or bexp k : stmt k timeout t: stmt An await statement must be preceded by a yield point, for handling messages while waiting; if a yield point is not specified explicitly, the default is that all message handlers can be executed at this point.
These few constructs make it easy to specify any process that has its own flow of control while also responding to messages. It is also easy to specify any process that only responds to messages, for example, by writing just receive definitions and a run() method containing only await false. Synchronization conditions using high-level queries. Synchronization conditions and other conditions can be expressed using high-level queries-quantifications, comprehensions, and aggregations-over sets of processes and sequences of messages. High-level queries are used commonly in distributed algorithms because (1) they make complex synchronization conditions clearer and easier to write, and (2) the complexity of distributed algorithms is measured by round complexity and message complexity, not time complexity of local processing.
Quantifications are especially common because they directly capture the truth values of synchronization conditions. We discovered a number of errors in our initial programs that were written using aggregations in place of quantifications before we developed the method to systematically optimize quantifications. For example, we regularly expressed "v is larger than all elements of s" as v > max s and either forgot to handle the case that s is empty or handled it in an ad hoc fashion. Naive use of aggregation operators like max may also hinder generation of more efficient implementations.
We define operations on sets; operations on sequences are the same except that elements are processed in order, and square brackets are used in place of curly braces.
• A quantification is a query of one of the two forms in (8) below, called existential and universal quantifications, respectively, plus a set of parameters-variables whose values are bound before the query. For a query to be well-formed, every variable in it must be reachable from a parameter-be a parameter or recursively be the leftside variable of a membership clause whose right-side variables are reachable. Given values of parameters, the query returns true iff for some or all, respectively, combinations of values of variables that satisfy all membership clauses vi in sexpi, expression bexp evaluates to true. When an existential quantification returns true, all variables in the query are also bound to a combination of values, called a witness, that satisfy all the membership clauses and condition bexp.
For example, the following query returns true iff each element in s is greater than each element in s2. each x in s, x2 in s2 | x > x2 For another example, the following query, containing a nested quantification, returns true iff some element in s is greater than each element in s2. Additionally, when the query returns true, variable x is bound to a witness-an element in s that is greater than each element in s2. some x in s | each x2 in s2 | x > x2 • A comprehension is a query of form (9) below. Given values of parameters, the query returns the set of values of exp for all combinations of values of variables that satisfy all membership clauses vi in sexpi and condition bexp.
For example, the following query returns the set of products of x in s and x2 in s2 where x is greater than x2.
• An aggregation, also called an aggregate query, is a query of one of the two forms in (10) below, where agg is an aggregation operator, including count, sum, min, and max. Given values of parameters, the query returns the value of applying agg to the set value of sexp, for the first form, or to the multiset of values of exp for all combinations of values of variables that satisfy all membership clauses vi in sexpi and condition bexp, for the second form.
agg sexp (10) agg { exp: v1 in sexp1, ..., v k in sexp k | bexp } • In the query forms above, each v i can also be a tuple pattern t i . Variables in t i are bound to the corresponding components in the matched elements of the value of sexp i . We omit | bexp when bexp is true.
We use {} for empty set; use s.add(x) and s.del(x) for element addition and deletion, respectively; and use x in s and x not in s for membership test and its negation, respectively. We assume that hashing is used in implementing sets, and the expected time of set initialization, element addition and removal, and membership test is O(1). We consider operations that involve iterations over sets and sequences to be expensive; each iteration over a set or sequence incurs a cost that is linear in the size of the set or sequence. All quantifications, comprehensions, and aggregations are considered expensive. DistAlgo has built-in sequences received and sent, containing all messages received and sent, respectively, by a process.
• Sequence received is updated only at yield points; after a message arrives, it will be handled when execution reaches the next yield point, by adding the message to received and running matching receive definitions, if any, associated with the yield point. We use received m from p interchangeably with m from p in received to mean that message m from process p is in received; from p is optional, but when specified, each message in received is automatically extended with the process id of the sender.
• Sequence sent is updated at each send statement; each message sent to a process is added to sent. We use sent m to p interchangeably with m to p in sent to mean that message m to process p is in sent; to p is optional, but when specified, p is the process to which m was sent as specified in the send statement.
If implemented straightforwardly, received and sent can create a huge memory leak, because they can grow unboundedly, preventing their use in practical programming. Our method can remove them by maintaining only auxiliary values that are needed for incremental computation.
Configuration. One can specify channel types, handling of messages, and other configuration items. Such specifications are declarative, so that algorithms can be expressed without unnecessary implementation details. We describe a few basic kinds of configuration items. First, one can specify the types of channels for passing messages. For example, the following statement configures all channels to be FIFO.

configure channel = fifo
Other options for channel include reliable and {reliable, fifo}. When either fifo or reliable is included, TCP is used for process communication; otherwise, UDP is used. In general, channels can also be configured separately for messages from any set of processes to any set of processes.
One can specify how much effort is spent processing messages at yield points. For example, configure handling = all configures the system to handle all un-handled messages at each yield point; this is the default. For another example, one can specify a time limit. One can also specify different handling effort for different yield points.
Logical clocks [27,43,60] are used in many distributed algorithms. One can specify the logical clock, e.g., Lamport clock, that is used: It configures sending and receiving of messages to update the clock appropriately. A call logical_time() returns the current value of the logical clock.
Overall, a DistAlgo program consists of a set of process definitions, a method main, and possibly other, conventional program parts. Method main specifies the configurations and creates, sets up, and starts a set of processes. DistAlgo language constructs can be used in process definitions and method main and are implemented according to the semantics described; other, conventional program parts are implemented according to their conventional semantics.
Other language constructs. For other constructs, we use those in high-level object-oriented languages. We mostly use Python syntax (indentation for scoping, ':' for elaboration, '#' for comments, etc.) for succinctness, except with v := exp for assignment and with a few conventions from Java (keyword extends for subclass, keyword new for object creation, and omission of self, the equivalent of this in Java, when there is no ambiguity) for ease of reading.

Compiling to executable programs
Compilation generates code to create processes on the specified machine, take care of sending and receiving messages, and realize the specified configuration. In particular, it inserts appropriate message handlers at each yield point.
Processes and sending of messages. Process creation is compiled to creating a process on the specified or default machine and that has a private memory space for its fields. Each process is implemented using two threads: a main thread that executes the main flow of control of the process, and a helper thread that receives and enqueues messages sent to this process. Constructs involving a set of processes, such as n new P, can easily be compiled into loops. Sending a message m to a process p is compiled into calls to a standard message passing API. If the sequence sent is used in the program, we also insert sent.add(m to p). Calling a method on a remote process object is compiled into a remote method call.
Control flows and handling of received messages. Each yield point l is compiled into a call to a message han-dler method l() that updates the sequence received, if received is used in the program, and executes the bodies of the receive definitions whose at clause includes l. Precisely: • Each receive definition is compiled into a method that takes a message m as argument, matches m against the message patterns in the receive clause, and if the matching succeeds, binds the variables in the matched pattern appropriately, and executes the statement in the body of this receive definition.
• Method l() compiled for yield point l does the following: for each un-handled message m from p to be handled, (1) execute received.add(m from p) if received is used in the program, (2) call the methods generated from the receive definitions whose at clause includes l, and (3) remove m from the message queue.
An await statement can be compiled into a synchronization using busy-waiting or blocking. We use blocking to wait until a new message arrives or the timeout specified in await is reached.
Configuration. Configuration options are taken into account during compilation in a straightforward way. Libraries and modules are used as much as possible. For example, when fifo or reliable channel is specified, the compiler can generate code that uses TCP sockets.

Incrementalizing expensive synchronizations
Incrementalization transforms expensive computations into efficient incremental computations with respect to updates to the values on which the computations depend. It (1) identifies all expensive queries, (2) determines all updates that may affect the query result, and (3) transforms the queries and updates into efficient incremental computations. Much of incrementalization has been studied previously, as discussed in Section 7.
The new method here is for (1) systematic handling of quantifications for synchronization as expensive queries, especially nested alternating universal and existential quantifications and quantifications containing complex order comparisons and (2) systematic handling of updates caused by all sending, receiving, and handling of messages in the same way as other updates in the program. The result is a drastic reduction of both time and space complexities.
Expensive computations using quantifications. Expensive computations in general involve repetition, including loops, recursive functions, comprehensions, aggregations, and quantifications over collections. Optimizations were studied most for loops, less for recursive functions, comprehensions, and aggregations, and least for quantifications, basically corresponding to how frequently these constructs have traditionally been used in programming. However, high-level queries are increasingly used in programming, and quantifications are dominantly used in writing synchronization conditions and assertions in specifications and very high-level programs. Unfortunately, if implemented straightforwardly, each quantification introduces a cost factor that is linear in the size of the collection quantified over.
Optimizing expensive quantifications in general is difficult, which is a main reason that they are not used in practical programs, not even logic programs, and programmers manually write more complex and error-prone code. The difficulty comes from expensive enumerations over collections and complex combinations of join conditions. We address this challenge by converting quantifications into aggregations that can be optimized systematically using previously studied methods. However, a quantification can be converted into multiple forms of aggregations. Which one to use depends on what kinds of updates must be handled, and on how the query can be incrementalized under those updates. Direct conversion of nested quantifications into nested aggregations can lead to much more complex incremental computation code and asymptotically worse time and space complexities for maintaining the intermediate query results.
Note that, for an existential quantification, we convert it to a more efficient aggregation if a witness is not needed; if a witness is needed, we incrementally compute the set of witnesses.
Converting quantifications to aggregations.We present all converted forms here and describe which forms to use after we discuss the updates that must be handled. The correctness of all rules presented have been proved, manually, using first-order logic and set theory. These rules ensure that the value of a resulting query expression equals the value of the original quantified expression. Table 1 shows general rules for converting single quantifications into equivalent aggregations that use aggregation operator count. For converting universal quantifications, either rule 2 or 3 could be used. The choice does not affect the asymptotic cost, but only small constant factors: rule 2 requires maintaining count s, and rule 3 requires computing not; the latter is generally faster unless count s is already needed for other purposes, and is certainly faster when not bexp can be simplified, e.g., when bexp is a negation. The rules in Table 1 are general because bexp can be any Boolean expression, but they are for converting single quantifications. Nested quantifications can be converted one at a time from inside out, but the results may be much more complicated than necessary. For example, each x in s | some x2 in s2 | bexp would be converted using rule 1 to each x in s | count {x2 in s2 | bexp} != 0 and then using rule 2 to count {x in s | count {x2 in s2 | bexp} != 0} = count s A simpler conversion is possible for this example, using a rule in Table 2, described next.  Table 2 shows general rules for converting nested quantifications into equivalent, but non-nested, aggregations that use aggregation operator count. These rules yield much simpler results than repeated use of the rules in Table 1. For example, rule 2 in this table yields a much simpler result than using two rules in Table 1 in the previous example. More significantly, rules 1, 4, and 5 generalize to any number of the same quantifier, and rules 2 and 3 generalize to any number of quantifiers with one alternation. We have not encountered more complicated quantifications than these in the algorithms we found. It is well known that more than one alternation is rarely used, so commonly used quantifications can all be converted to non-nested aggregations. For example, in twelve different algorithms expressed in DistAlgo [56], there are a total of 50 quantifications but no occurrence of more than one alternation. Table 3 shows general rules for converting single quantifications with a single order comparison, for any linear order, into equivalent queries that use aggregation operators max and min. These rules are useful because max and min can in general be maintained incrementally in O(log n) time with O(n) space overhead. Additionally, when there are only element additions, max and min can be maintained most efficiently in O(1) time and space. Table 4 shows general rules for decomposing Boolean combinations of conditions in quantifications, to obtain quantifications with simpler conditions. In particular, Boolean combinations of order comparisons and other conditions can be transformed to extract quantifications each with a single order comparison, so the rules in Table 3 can be applied, and Boolean combinations of inner quantifications and other conditions can be transformed to extract directly nested quantifications, so the rules in Table 2 can be applied. For example, each x in s | bexp implies y < x can be converted using rule 8 in Table 4 to each x in {x in s | bexp} | y < x which can then be converted using rule 13 of Table 3 to Updates caused by message passing. Recall that the parameters of a query are variables in the query whose values are bound before the query. Updates that may affect the query result include not only updates to the query parameters but also updates to the objects and collections reachable from the parameter values. The most basic updates are assignments to query parameters, v := exp, where v is a query parameter. Other updates are to objects and collections used in the query. For objects, all updates can be expressed as field assignments, o.f := exp. For collections, all updates can be expressed as initialization to empty and element additions and removals, s.add(x) and s.del(x).
For distributed algorithms, a distinct class of important updates are caused by message passing. Updates are caused in two ways: 1. Sending and receiving messages updates the sequences sent and received, respectively. Before incrementalization, code is generated, as described in Section 4, to explicitly perform these updates.
2. Handling of messages by code in receive definitions updates variables that are parameters of the queries for computing synchronization conditions, or that are used to compute the values of these parameters.
Once these are established, updates can be determined using previously studied analysis methods, e.g., [33,51].
Incremental computation. Given expensive queries and updates to the query parameters, efficient incremental computations can be derived for large classes of queries and updates based on the language constructs used in them or by using a library of rules built on existing data structures [51,53,54,65]. For aggregations converted from quantifications, algebraic properties of the aggregation operators are exploited to efficiently handle possible updates. In particular, each resulting aggregate query result can be obtained in O(1) time and incrementally maintained in O(1) time per update to the sets maintained and affected plus the time for evaluating the conditions in the aggregation once per update. The total maintenance time at each element addition or deletion to a query parameter is at least a linear factor smaller than computing the query result from scratch. Additionally, if aggregation operators max and min are used and there are only element additions, the space overhead is O(1). Note that if max and min are used naively when there are element deletions, there may be an unnecessary overhead of O(n) space and O(log n) maintenance time per update from using more sophisticated data structures to maintain the max or min under element deletion [21,84,85].
Incremental computation improves time complexity only if the total time of repeated expensive queries is larger than that of repeated incremental maintenance. This is generally true for incrementalizing expensive synchronization conditions because (1) expensive queries in the synchronization conditions need to be evaluated repeatedly at each relevant update to the message history, until the condition becomes true, and (2) incremental maintenance at each such update is at least a linear factor faster for single message updates and no slower generally than computing from scratch.   To allow the most efficient incremental computation under all given updates, our method transforms each top-level quantification as follows: • For non-nested quantifications, if the conditions contain no order comparisons or there are deletions from the sets or sequences whose elements are compared, the rules in Table 1 are used. The space overhead is linear in the sizes of the sets maintained and being aggregated over.
• For non-nested quantifications, if the conditions contain order comparisons and there are only additions to the sets or sequences whose elements are compared, the rules in Table 4 are used to extract single quantified order comparisons, and then the rules in Table 3 are used to convert the extracted quantifications. In this case, the space overhead is reduced to constant.
• For nested quantifications with one level of nesting, the rules in Table 4 are used to extract directly nested quantifications, and then the rules in Table 2 are used. If the resulting incremental maintenance has constant-time overhead maintaining a linear-space structure, we are done. If it is linear-time overhead maintaining a quadratic-space structure, and if the conditions contain order comparisons, then the rules in Table 4 are used to extract single quantified order comparisons, and then the rules in Table 3 are used. This can reduce the overhead to logarithmic time and linear space.
• In general, multiple ways of conversion may be possible, besides small constant-factor differences between rules 2 and 3 in Table 1 and rules 4 and 5 in Table 2. In particular, for nested quantifications with two or more alternations, one must choose which two alternating quantifiers to transform first, using rule 2 or 3 in Table 2. We have not encountered such queries and have not studied this aspect further. Our general method is to transform in all ways possible, obtain the time and space complexities for each result, and choose one with the best time and then space. Complexities are calculated using the cost model of the set operations given in Section 3. The number of possible ways is exponential in the worst case in the size of the query, but the query size is usually a small constant. Table 5 summarizes well-known incremental computation methods for these aggregate queries. The methods are expressed as incrementalization rules: if a query in the program matches the query form in the table, and each update to a parameter of the query in the program matches an update form in the table, then transform the query into the corresponding replacement and insert at each update the corresponding maintenance; fresh variables are introduced for each different query to hold the query results or auxiliary data structures. In the third rule, data structure ds stores the argument set s of max and supports priority queue operations. O(log |s|) The rule for min is similar to the rule for max.
The overall incrementalization algorithm [51,53,65] introduces new variables to store the results of expensive queries and subqueries, as well as appropriate additional values, forming a set of invariants, transforms the queries and subqueries to use the stored query results and additional values, and transforms updates to query parameters to also do incremental maintenance of the stored query results and additional values.
In particular, if queries are nested, inner queries are transformed before outer queries. Note that a comprehension such as {x in s | bexp} is incrementalized with respect to changes to parameters of Boolean expression bexp as well as addition and removal of elements of s; if bexp contains nested subqueries, then after the subqueries are transformed, incremental maintenance of their query results become additional updates to the enclosing query.
At the end, variables and computations that are dead in the transformed program are eliminated. In particular, sequences received and sent will be eliminated when appropriate, because queries using them have been compiled into message handlers that only store and maintain values needed for incremental evaluation of the synchronization conditions.
Example. In the program in Figure 2, three quantifications are used in the synchronization condition in the await statement, and two of them are nested. The condition is copied below, except that ('ack',t2,=p2) in received is used in place of received('ack',t2,=p2). each ('request', t2, p2) in q | (t2,p2) != (t,self) implies (t,self) < (t2,p2) and each p2 in s | some ('ack', t2, =p2) in received | t2 > t Converting quantifications into aggregations as described using Tables 1 through 4 proceeds as follows. In the first conjunct, the universal quantification is converted using rule 2 or 3 in Table 1, because it contains an order comparison with elements of q and there are element deletions from q; rule 3 is used here because it is slightly simpler after the negated condition is simplified. In the second conjunct, the nested quantification is converted using rule 2 in Table 2. The resulting expression is: Updates to parameters of the first conjunct are additions and removals of requests to and from q, and also assignment to t. Updates to parameters of the second conjunct are additions of ack messages to received, and assignment to t, after the initial assignment to s.
Incremental computation [51,53,54,65] introduces variables to store the values of all three aggregations in the converted query, transforms the aggregations to use the introduced variables, and incrementally maintains the stored values at each of the updates, as follows, yielding Figure 3.
• For the first conjunct, store the set value and the count value in two variables, say earlier and number1, respectively, so first conjunct becomes number1 = 0; when t is assigned a new value, let earlier be q and let number1 be its size, taking O(|earlier|) time, amortized to O(1) time when each request in earlier is served; when a request is added to q, if t is defined and (t,self) > (t2,p2) holds, add the request to earlier and increment number1 by 1, taking O(1) time; similarly for deletion from q. A test of definedness, here t != undefined, is inserted for any variable that might not be defined in the scope of the maintenance code. Note that when ('request',t,self) in particular is added to or removed from q, earlier and number1 are not updated, because (t,self) > (t,self) is trivially false.
• For the second conjunct, store the set value and the two count values in three variables, say responded, number2, and total, respectively, so the conjunct becomes number2 = total; when s is initialized in setup, assign total the size of s, taking O(|s|) time, done only once for each process; when t is assigned a new value, let responded be {}, and let number2 be 0, taking O(1) time; when an ack message is added to received, if the associated conditions hold, increment number2 by 1, taking O(1) time. A test of definedness of t is omitted in the maintenance for receiving ack messages, because t is always defined there; this small optimization is incorporated in an incrementalization rule, but it could be done with a data-flow analysis that covers distributed data flows.
Note that incrementalization uses basic properties about primitives and libraries. These properties are incorporated in incrementalization rules. For the running example, the property used is that a call to logical_time() returns a timestamp larger than all existing timestamp values, and thus at the assignment to t in method mutex, we have that earlier is q and responded is {}. So, an incrementalization rule for maintaining earlier specifies that at update t := logical_time(), the maintenance is earlier := q; similarly for maintaining responded. These simplifications could be facilitated with data-flow analyses that determine variables holding logical times and sets holding certain element types. Incrementalization rules can use any program analysis results as conditions [54]. Figure 3 shows the optimized program after incrementalization of the synchronization condition on lines 10-11 in Figure 2. All lines with comments are new except that the synchronization condition in the await statement is simplified. The synchronization condition now takes O(1) time, compared with O(|s| 2 ) if computed from scratch. The tradeoff is the amortized O(1) time overhead at updates to t and q and on receiving of ack messages. Using based representation for sets [17,34,64], maintaining earlier and responded can each be done using one bit for each process.
Note that the sequence received used in the synchronization condition in Figure 2 is no longer used after incrementalization. All values needed for evaluating the synchronization condition are stored in new variables introduced: earlier, number1, responded, number2, and total, a drastic space improvement from unbounded for received to linear in the number of processes.
Example with naive use of aggregation operator min. Note that the resulting program in Figure 3 does not need to use a queue at all, even though a queue is used in the original description in Figure 1; the variable q is simply a set, and thus element addition and removal takes O(1) time.
We show that if min is used naively, a more sophisticated data structure [21,84,85] supporting priority queue is needed, incurring an O(log n) time update instead of the O(1) time in Figure 3. Additionally, for a query using min to be correct, special care must be taken to deal with the case when the argument to min is empty, because then min is undefined.  Consider the first conjunct in the synchronization condition in the await statement in Figure 2, copied below: each ('request', t2, p2) in q | (t2,p2) != (t,self) implies (t,self) < (t2,p2) One might have written the following instead, because it seems natural, especially if universal quantification is not supported: (t,self) < min {(t2,p2): ('request', t2, p2) in q | (t2,p2) != (t,self)} However, that is incorrect, because the argument of min may be empty, in which case min is undefined. Instead of resorting to commonly used special values, such as maxint, which is ad hoc and error prone in general, the empty case can be added as the first disjunct of a disjunction: {(t2,p2): ('request', t2, p2) in q | (t2,p2) != (t,self)} = {} or (t,self) < min {(t2,p2): ('request', t2, p2) in q | (t2,p2) != (t,self)} In fact, the original universal quantification in the first conjunct in the await statement can be converted exactly to this disjunction by using rule 8 in Table 4 and then rule 13 in Table 3. Our method does not consider this conversion because it leads to a worse resulting program. Figure 4 shows the resulting program after incrementalization of the synchronization condition that uses the disjunction above, where ds stores the argument set of min and supports priority queue operations. All commented lines are new compared to Figure 2 except that the synchronization condition in the await statement is simplified. The program appears shorter than Figure 3 because the long complex code for maintaining the data structure ds is not included; it is in fact similar to Figure 3 except that ds is used and maintained instead of earlier and number1.
The program in Figure 4 is still a drastic improvement over the original program in Figure 2, with the synchronization condition reduced to O(1) time and with received removed, just as in Figure 3. The difference is that maintaining ds for incrementalizing min under element addition to and deletion from q takes O(log |s|) time, as opposed to O(1) time for maintaining earlier and number1 in Figure 3.
Simplifications to the original algorithm. Consider the original algorithm in Figure 2. Note that incrementalization determined that there is no need for a process to update auxiliary values for its own request, in both Figures 3 and 4. Based on this, we discovered, manually, that updates to q for a process's own request do not affect the two uses of q, on lines 9 and 35, in Figure 3 and the only use of q, on line 30, in Figure 4. So we can remove them in Figures 3 and 4. In addition, we can remove them on lines 9 and 14 in Figure 2 and remove the test (t2,p2) != (t,self), which becomes always true, in the synchronization condition, yielding a simplified original algorithm.
Furthermore, note that the remaining updates to q in Figure 2 merely maintain pending requests by others, so we can remove lines 4, 17, 20, 21, and the entire receive definition for release messages, by using, for the first conjunct in the await statement, each received('request', t2, p2) | not (some received('release', t3, =p2) | t3 > t2) implies (t,self) < (t2,p2) Figure 5 shows the resulting simplified algorithm. Incrementalizing this program yields essentially the same programs as in Figures 3 and 4, except that it needs to use the property that when a message is added to received, all messages from the same process in received have a smaller timestamp. This property follows from the use of logical  clock and FIFO channels. The incrementalization rules for maintaining the result of the new condition incorporate this property in a similar way as described for Figure 3, except it could be facilitated with also a data-flow analysis that determines the component of a received message holding the sender of the message.

Implementation and experiments
We have developed a prototype implementation of the compiler and optimizations for DistAlgo and evaluated it in im-plementing a set of well-known distributed algorithms, as described previously [56]. We have also used DistAlgo in teaching distributed algorithms and distributed systems, and students used the language and system in programming assignments and course projects. We summarize results from the former and describe experience with the latter, after an overview and update about the implementation. Our DistAlgo implementation takes DistAlgo programs written in extended Python, applies analyses and optimizations, especially to the high-level queries, and generates executable Python code. It optionally interfaces with an incrementalizer to apply incrementalization before generating code. Applying incrementalization uses the methods and implementation from previous work: a library of incrementalization rules was developed, manually but mostly following a systematic method [51,53], and applied automatically using InvTS [33,54]. A set of heuristics are currently used to select the best program generated from incrementalizing differently converted aggregations.
A more extensive implementation of DistAlgo than the first prototype [56] has been released and is being gradually improved [25]. Improved methods and implementation for incrementalization are also being developed [57], to replace manually written incrementalization rules, and to better select the best transformed programs.
Evaluation in implementing distributed algorithms. We have used DistAlgo to implement a variety of well-known distributed algorithms, including twelve different algorithms for distributed mutual exclusion, leader election, and atomic commit, as well as Paxos, Byzantine Paxos, and multi-Paxos, as summarized previously [56]; results of evaluation using these programs are as follows: • DistAlgo programs are consistently small, ranging from 22 to 160 lines, and are much smaller than specifications or programs written in other languages, mostly 1/2 to 1/5 of the size; also we were able to find only a few of these algorithms written in other languages. Our own best effort to write Lamport's distributed mutual exclusion in programming languages resulted in 272 lines in C, 216 lines in Java, 122 lines in Python, and 99 lines in Erlang, compared with 32 lines in DistAlgo.
• Compilation times without incrementalization are all under 0.05 seconds on an Intel Core-i7 2600K CPU with 16GB of memory; and incrementalization times are all under 30 seconds. Generated code size ranges from 1395 to 1606 lines of Python, including 1300 lines of fixed library code.
• Execution time and space confirm the analyzed asymptotic time and space complexities. For example, for Lamport's distributed mutual exclusion, total CPU time is linear in the number of processes for the incrementalized program, but superlinear for the original program; for a fixed number of processes, the memory usage is constant for the incremental program, but grows linearly with the number of requests for the original program.
• Compared with running times of our best, manually written programs in programming languages, all running on a single machine, our generated DistAlgo takes about twice as long as our Python version, which takes about twice as long as our Java version, which takes about twice as long as our C version, which takes about four times as long as our Erlang version.
Python is well known to be slow compared Java and C, and we have not focused on optimizing constant factors. Erlang is significantly faster than C and the rest because of its use of light-weight threads to implement processes that is facilitated by its being a functional language. However, among all our programs for Lamport's distributed mutual exclusion, Erlang is the only one besides un-incrementalized DistAlgo whose memory usage for a fixed number of processes grows linearly with the number of requests.
Programming distributed algorithms at a high level has also allowed us to discover several improvements to correctness and efficiency aspects of some of the algorithms [55]. For example, in the pseudocode for multi-Paxos [82], in process Commander, waiting for p2b messages containing ballot b from a majority of acceptors is expressed by starting with a waitfor set initialized to acceptors and then, in a for ever loop, repeatedly updating waitfor and testing |waitfor| < |acceptors|/2 as each p2b message containing ballot b arrives. The test is incorrect if implemented directly in commonly used languages such as Java, and even Python until Python 3, because / is integer division, which discards any fractional part; for example, test 1 < 3/2 becomes false but should be true. In DistAlgo, the entire code can simply be written as await count {a: received ('p2b',=b) from a} > (count acceptors)/2 using the standard majority test, and it is correct whether / is for integer or float.
Experience in teaching distributed algorithms. DistAlgo has also helped us tremendously in teaching distributed algorithms, because it makes complex algorithms completely clear, precise, and directly executable. Students learn DistAlgo quickly through even a small programming assignment, despite that most did not know Python before, thanks to the power and clarity of Python.
All distributed programming features were used extensively in students' programs-easy process creation and setup and sending of messages, high-level control flows with receive definitions as well as await for synchronization, and declarative configurations-with the exception of queries over message histories, because students had been trained in many courses to handle events imperatively; we have not evaluated incrementalization on students' programs, because execution efficiency has not been a problem. Overall, students' experience helps confirm that DistAlgo allows complex distributed algorithms and services to be implemented much more easily than commonly used languages such as C++ and Java. We summarize two specific instances below.
In a graduate class in Fall 2012, most of the 28 students initially planned to use Java or C++ for their course projects, because they were familiar with those and wanted to strengthen their experience of using them instead of using DistAlgo in implementing distributed systems. However, after doing one programming assignment using DistAlgo, all those students switched to DistAlgo for their course projects, except for one student, who had extensive experience with C++, including several years of internship at Microsoft Research programming distributed systems.
• This student wrote about 3000 lines of C++, compared to about 300 lines of DistAlgo written by several other students who chose the same project of implementing multi-Paxos and several optimizations. Furthermore, his C++ program was incomplete, lacking some optimizations that other students' DistAgo programs included.
• The student did a re-implementation in DistAlgo quickly after the course 1 , confirming that it took about 300 lines. His biggest surprise was that his C++ program was an order of magnitude slower than his DistAlgo program. After several weeks of debugging, he found that it was due to an improper use of some C++ library function.
The main contrast that the student concluded was the huge advantage of DistAlgo over C++ in ease of programming and program understanding, not to mention the unexpected performance advantage. In a graduate class in Fall 2014, each team of two students first implemented a fault-tolerant banking service in two languages: DistAlgo and another language of their choice other than Python. We excluded Python as the other language, because implementing the same service in such closely related languages would be less educational. The service uses chain replication [83] to tolerate crash failures. The service offers only a few simple banking operations (get balance, deposit, withdrawal, intra-bank transfer, inter-bank transfer), so most of the code is devoted to distributed systems aspects. The numbers of teams that chose various other languages are: Java 15, C++ 3, Go 3, Erlang 2, Node.js 2, Elixir (a variant of Erlang) 1, JavaScript 1.
• In the last assignment, teams implemented an extension to the banking service in one language of their choice. 59% of the teams chose DistAlgo for this, even though most students (about 80%) did not know Python, and none knew DistAlgo, at the beginning of the class. In other words, a majority of students decided that implementation of this type of system is better in DistAlgo, even compared to languages with which they had more experience and that are more widely used.
• We asked each team to compare their experiences with the two languages. Teams consistently reported that development in DistAlgo was faster and easier than development in the other language (even though most students did not know Python before the project), and that the DistAlgo code was significantly shorter. It is no surprise that Java and C++ require more code, even when students used existing networking libraries, which they were encouraged to do. Comparison with Erlang and Go is more interesting, because they are high-level languages designed to support distributed programming. For the teams that chose Erlang, the average DistAlgo and Erlang code sizes, measured as non-empty non-comment line of code, are 586 and 1303, respectively. For the teams that chose Go, the average DistAlgo and Go code sizes are 465 and 1695, respectively.

Related work and conclusion
A wide spectrum of languages and notations have been used to describe distributed algorithms, e.g., [7,29,41,44,45,59,[70][71][72]81]. At one end, pseudocode with English is used, e.g., [41], which gives a high-level flow of the algorithms, but lacks the details and precision needed for a complete understanding. At the other end, state machine based specification languages are used, e.g., I/O automata [39,59], which is completely precise, but uses low-level control flows that make it harder to write and understand the algorithms. There are also many notations in between these extremes, some being much more precise or completely precise while also giving a high-level control flow, e.g., Raynal's pseudocode [70][71][72] and Lamport's PlusCal [45]. However, all of these languages and notations lack concepts and mechanisms for building real distributed applications, and most of the languages are not executable. Many programming languages support programming of distributed algorithms and applications. Most support distributed programming through messaging libraries, ranging from relatively simple socket libraries to complex libraries such as MPI [62]. Many support Remote Procedure Call (RPC) or Remote Method Invocation (RMI), which allows a process to call a subroutine in another process without the programmer coding the details for this. Many also support asynchronous method invocation (AMI), which allows the caller to not block and get the reply later. Some programming languages, such as Erlang [26,46], which has an actor-like model [2], have support for message passing and process management built into the language. There are also other well-studied languages for distributed programming, e.g., Argus [47], Lynx [76], SR [5], Concert/C [8], and Emerald [15]. These languages all lack constructs for expressing control flows and complex synchronization conditions at a much higher level; such high-level constructs are extremely difficult to implement efficiently. DistAlgo's construct for declaratively and precisely specifying yield points for handling received messages is a new feature that we have not seen in other languages. So is DistAlgo's support of history variables in high-level synchronization conditions in non-deterministic await with timeout in a programming language. Our simple combination of synchronous await and asynchronous receive allows distributed algorithms to be expressed easily and clearly.
There has been much work on producing executable implementations from formal specifications, e.g., from process algebras [37], I/O automata [31], Unity [35], and Seuss [40], as well as from more recently proposed high-level languages for distributed algorithms, e.g., Datalog-based languages Meld [6], Overlog [4], and Bloom [13], a Prologbased language DAHL [58], and a logic-based language EventML [14,67]. An operational semantics was studied recently for a variant of Meld, called Linear Meld, that allows updates to be encoded more conveniently than Meld by using linear logic [22]. Compilation of DistAlgo to executable implementations is easy because it is designed to be so and DistAlgo is given an operational semantics. Highlevel queries and quantifications used for synchronization conditions can be compiled into loops straightforwardly, but they may be extremely inefficient. None of these prior works study powerful optimizations of quantifications. Efficiency concern is a main reason that similar high-level language constructs, whether for queries or assertions, are rarely used, if supported at all, in commonly used languages.
Incrementalization has been studied extensively, e.g., [48,69], both for doing it systematically based on languages, and in applying it in an ad hoc fashion to specific problems. However, all systematic incrementalization methods based on languages have been for centralized sequential programs, e.g., for loops [3,30,52], set languages [36,53,65], recursive functions [1,49,68], logic rules [50,75], and objectoriented languages [51,57,63,73]. This work is the first to extend incrementalization to distributed programs to support high-level synchronization conditions. This allows the large body of previous work on incrementalization, especially on sets and sequences, to be used for optimizing distributed programs.
Quantifications are the centerpiece of first-order logic, and are dominantly used in writing synchronization conditions and assertions in specifications, but there are few results on generating efficient implementations of them. In the database area, despite extensive work on efficient implementation of high-level queries, efficient implementation of quantification has only been studied in limited scope or for extremely restricted query forms, e.g., [9][10][11]20]. In logic programming, handling of universal quantification is based on variants of brute-force Lloyd-Topor transformations, e.g., [28,66]; even state-of-the-art logic programming systems, e.g., [80], do not support universal quantification. Our method is the first general and systematic method for incrementalizing arbitrary quantifications. Although they are much more challenging to optimize than set queries, our method combines a set of general transformations to transform them into aggregations that can be most efficiently incrementalized using the best previous methods.
To conclude, this article presents a powerful language and method for programming and optimizing distributed algorithms. There are many directions for future work, from formal verification on the theoretical side, to generating code in lower-level languages on the practical side, with many additional analyses and optimizations in between. In particular, a language with a high level of abstraction also faciliates formal verification, of not only the high-level programs, but also the generated efficient implementations when they are generated through systematic optimizations. Besides developing systematic optimizations, we have started to study formal verification of distributed algorithms [18] and their implementations by starting with their high-level, concise descriptions in DistAlgo.

APPENDIX A. Semantics of DistAlgo
We give an abstract syntax and operational semantics for a core language for DistAlgo. The operational semantics is a reduction semantics with evaluation contexts [77,86].

A.1 Abstract Syntax
The abstract syntax is defined in Figures 6 and 7. We use some syntactic sugar in sample code, e.g., we use infix notation for some binary operators, such as and and is.

Notation.
• A symbol in the grammar is a terminal symbol if it starts with a lower-case letter.
• A symbol in the grammar is a non-terminal symbol if it starts with an upper-case letter. • In each production, alternatives are separated by a linebreak.
• * after a non-terminal means "0 or more occurrences".
• + after a non-terminal means "1 or more occurrences".  follows: e 1 and e 2 is replaced with not(not(e 1 ) or not(e 2 )), and each iter | e is replaced with not(some iter | not(e)).
5. An aggregation is eliminated by translation into a comprehension followed by a for loop that iterates over the set returned by the comprehension. The for loop updates an accumulator variable using the aggregation operator.
6. Iterators containing tuple patterns are rewritten as iterators without tuple patterns, as follows.
• Consider the existential quantification some (e 1 , . . . , e n ) in s | b. Let x be a fresh variable. Let θ be the substitution that replaces e i with select(x,i) for each i such that e i is a variable not prefixed with "=". Let {j 1 , . . . , j m } contain the indices of the constants and the variables prefixed with "=" in (e 1 , . . . , e n ). Letē j denote e j after removing the "=" prefix, if any. The quantification is rewritten as some x in s | isTuple(x) and len(x) is n and (select(x,j 1 ), . . ., select(x,j m )) is (ē j1 , . . .,ē jm ) and bθ. 10. Remote method invocation, i.e., invocation of a method on another process after that process has been started, is translated into message communication. Notes.
1. ClassName must include process. process is a predefined class; it should not be defined explicitly. process has fields sent and received, and it has a method start.
2. The grammar allows receive definitions to appear in classes that do not extend process, but such receive definitions are useless, so it would be reasonable to make them illegal.
3. The grammar does not allow labels on statements other than await. A label ℓ on a statement s other than await is treated as syntactic sugar for label ℓ on await true : skip followed by statement s.

4.
ClassName must include set and sequence. Sets and sequences are treated as objects, because they are mutable. These are predefined classes that should not be defined explicitly. Methods of set include add, del, contains, min, max, and size. Methods of sequence include add (which adds an element at the end of the sequence), contains, and length. We give the semantics explicitly for a few of these methods; the others are handled similarly.
5. Tuples are treated as immutable values, not as mutable objects.
6. All expressions are side-effect free. For simplicity, we treat quantifications as expressions, so existential quantifications do not have the side-effect of binding vari-ables to a witness. Such existential quantifications could be added as a new form of statement.
7. Object creation and comprehension are statements, not expressions, because they have side-effects. Comprehension has the side-effect of creating a new set.

8.
Parameter must include self. The values of method parameters cannot be updated (e.g., using assignment statements). For brevity, local variables of methods are omitted from the core language. Consequently, assignment is allowed only for instance variables.
9. Semantically, the for loop copies the contents of a (mutable) sequence or set into an (immutable) tuple before iterating over it, to ensure that changes to the sequence or set by the loop body do not affect the iteration. An implementation could use optimizations to achieve this semantics without copying when possible.
10. For brevity, among the standard arithmetic operations (+, -, *, etc.), we include only one representative operation in the abstract syntax and semantics; others are handled similarly.
11. The semantics below does not model real-time, so timeouts in await statements are simply allowed to occur non-deterministically.
12. We omit the concept of node (process location) from the semantics, and we omit the node argument of the constructor when creating instances of process classes, because process location does not affect other aspects of the semantics. 13. We omit configure handling statements from the syntax. The semantics is for configure handling = all. Semantics for other configure handling options can easily be added.
14. To support initialization of a process by its parent, a process can access fields of another process and invoke methods on another process before the latter process is started. 15. We require that all messages be tuples. This is an inessential restriction; it slightly simplifies the specification of pattern matching for matching messages against patterns. 16. A process's sent sequence contains pairs of the form (m, d), where m is a message sent by the process to destination d. A process's received sequence contains pairs of the form (m, s), where m is a message received by the process from sender s.

A.2 Semantic Domains
The semantic domains are defined in Figure 9. Notation.
• D * contains finite sequences of values from domain D.  Notes.
• We require that ProcessAddress and NonProcessAddress be disjoint.
• For a ∈ ProcessAddress and h ∈ Heap, h(a) is the local heap of process a. For a ∈ Address and ht ∈ HeapType, ht(a) is the type of the object with address a. For convenience, we use a single (global) function for HeapType in the semantics, even though the information in that function is distributed in the same way as the heap itself in an implementation.
• The MsgQueue associated with a process by the last component of a state contains messages, paired with the sender, that have arrived at the process but have not yet been handled by matching receive definitions.

A.3 Extended Abstract Syntax
Section A.1 defines the abstract syntax of programs that can be written by the user. Figure 10 extends the abstract syntax to include additional forms into which programs may evolve during evaluation. Only the new productions are shown here; all of the productions given above carry over unchanged. The statement for v intuple t: s iterates over the elements of tuple t, in the obvious way.

A.4 Evaluation Contexts
Evaluation contexts, also called reduction contexts, are used to identify the next part of an expression or statement to be evaluated. An evaluation context is an expression or statement with a hole, denoted [], in place of the next subexpression or sub-statement to be evaluated. Evaluation contexts are defined in Figure 11.

A.5 Transition Relations
The transition relation for expressions has the form ht : h ⊢ e → e ′ , where e and e ′ are expressions, ht ∈ HeapType, and h ∈ LocalHeap. The transition relation for statements has the form σ → σ ′ where σ ∈ State and σ ′ ∈ State.
Both transition relations are implicitly parameterized by the program, which is needed to look up method definitions and configuration information. The transition relation for expressions is defined in Figure 12. The transition relation for statements is defined in Figures 13-14.
Notation and auxiliary functions.
• In the transition rules, a matches an address; v matches a value (i.e., an element of Val); and ℓ matches a label. • For an expression or statement e, e[x := y] denotes e with all occurrences of x replaced with y.
• A function matches the pattern f [x → y] iff f (x) equals y. For example, in transition rules for statements, a function P in ProcessAddress ⇀ Statement matches P [a → s] if P maps process address a to statement s.
• For a function f , f [x := y] denotes the function that is the same as f except that it maps x to y.
• f 0 denotes the empty partial function, i.e., the partial function whose domain is the empty set.
• For a (partial) function f , f ⊖ a denotes the function that is the same as f except that it has no mapping for a.
• s@t is the concatenation of sequences s and t.
• first(s) is the first element of sequence s.
• rest(s) is the sequence obtained by removing the first element of s.
• length(s) is the length of sequence s. • For h,h,h ′ ∈ LocalHeap and ht, ht ′ ∈ HeapType and v,v ∈ Val , the relation isCopy (v, h,h, ht,v,h ′ , ht ′ ) holds iff (1) v is a value in a process with local heap h, i.e., addresses in v are evaluated with respect to h, (2) v is a copy of v for a process whose local heap wash before v was copied into it and whose local heap ish ′ after v is copied into it, i.e.,v is the same as v except that, instead of referencing objects in h, it references newly created copies of those objects inh ′ , and (3)h ′ and ht ′ are versions ofh and ht updated to reflect the creation of those objects. As an exception, because process addresses are used as global identifiers, process addresses in v are copied unchanged intov, and new copies of process objects are not created. We give auxiliary definitions and then a formal definition of isCopy .
For v ∈ Val , let addrs(v, h) denote the set of addresses that appear in v or in any objects or values reachable from v with respect to local heap h; formally, . a ∈ addrs(v ′ , h))) ∨ (∃v 1 , . . . , v n ∈ Val. v = (v 1 , . . . , v n ) ∧ ∃i ∈ [1..n]. a ∈ addrs(v i , h)) For v,v ∈ Val and f ∈ Address ⇀ Address, the relation subst(v,v, f ) holds iff v is obtained fromv by replacing each occurrence of an address a in dom(f ) with f (a) (informally, f maps addresses of new objects inv to addresses of corresponding old objects in v); formally, Similarly, for o,ō ∈ Object and f ∈ Address ⇀ Address, the relation subst (o,ō, f ) holds iff o is obtained fromō by replacing each occurrence of an address a in dom(f ) with f (a). For sets S and S ′ , let S 1−1 → S ′ be the set of bijections between S and S ′ .
Finally, isCopy is defined as follows (intuitively, A contains the addresses of the newly allocated objects): . ht ′ (a) = ht(a)) ∧ (∀a ∈ dom(h).h ′ (a) =h(a)) ∧ (∀a ∈ A. ht ′ (a) = ht(f (a)) ∧ subst(h(a),h ′ (a), f )) • For m ∈ Val, a ∈ ProcessAddress, ℓ ∈ Label , h ∈ LocalHeap, and a receive definition d, if message m can be received from a at label ℓ by a process with local heap h using receive definition d, then matchRcvDef (m, a, ℓ, h, d) returns the appropriately instantiated body of d.
We first define some auxiliary relations and functions. The relation matchesDefLbl (d, ℓ) holds iff receive definition d either lacks an at clause or has an at clause that includes ℓ. bound(P ) returns the set of variables that appear in pattern P prefixed with "=". vars(P ) returns the set of variables that appear in P . findSubstPat(m, a, h, P from x) returns the substitution θ with domain vars(P ) ∪ {x} such that m = P θ ∧ θ(x) = a ∧ (∀y ∈ bound (P ). θ(y) = h(y)), if any, otherwise it returns ⊥. findSubst(m, a, h, d) returns findSubstPat(m, a, h, P from x) for the first receive pattern P from x in d such that findSubstPat(m, a, h, P from x) = ⊥, if any, otherwise it returns ⊥.
• For m ∈ Val , a ∈ ProcessAddress, ℓ ∈ Label , c ∈ ClassName, and h ∈ LocalHeap, if message m can be received from a at label ℓ in class c by a process with local heap h, then receiveAtLabel ((m, a) , a), ℓ, c, h) returns ∅.

A.6 Executions
An execution is a sequence of transitions σ 0 → σ 1 → σ 2 → · · · such that σ 0 is an initial state. The set of initial states is defined in Figure 15. Intuitively, a p is the address of the initial process, a r is the address of the received sequence of the initial process, and a s is the address of the sent sequence of the initial process. Informally, execution of the statement initially associated with a process may eventually (1) terminate (i.e., the statement associated with the process becomes skip, indicating that there is nothing left for the process to do), (2) get stuck (i.e., the statement associated with the process is not skip, and the process has no enabled transitions) due to an unsatisfied await statement or an error (e.g., the statement contains an expression that tries to select a component from a value that is not a tuple, or the statement contains an expression that tries to read the value of a non-existent field), or (3) run forever due to an infinite loop or infinite recursion.