{'fraction_non_alphanumeric': 0.03507339432516191, 'fraction_numerical': 0.00865700447232171, 'mean_word_length': 4.614743422766345, 'pattern_counts': {'":': 0, '<': 0, '<?xml version=': 0, '>': 0, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 27, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
Programming with rules and everything else, seamlessly

Logic rules are powerful for expressing complex reasoning and analysis problems. At the same time, they are inconvenient or impossible to use for many other aspects of applications. Integrating rules in a language with sets and functions, and furthermore with updates to objects, has been a subject of significant study. What's lacking is a language that integrates all constructs seamlessly. This paper presents a language, Alda, that supports all of rules, sets, functions, updates, and objects as seamlessly integrated built-ins, including concurrent and distributed processes. The key idea is to support predicates as set-valued variables that can be used and updated in any scope, and support queries and inference with both explicit and automatic calls to an inference function. We develop a complete formal semantics for Alda. We design a compilation framework that ensures the declarative semantics of rules, while also being able to exploit available optimizations. We describe a prototype implementation that builds on a powerful extension of Python and employs an efficient logic rule engine. We develop a range of benchmarks and present results of experiments to demonstrate Alda's power for programming and generally good performance.


Introduction
Logic rules and inference are powerful for specifying and solving complex reasoning and analysis problems [Liu18], especially in critical areas such as program analysis, decision support, networking, and security [WL17]. Many logic rule languages and systems have been developed, and used successfully in many areas of applications, expressing challenging problems succinctly and solving them with general and powerful implementation methods [KL18].
At the same time, logic rules are inconvenient or impossible to use for many other aspects of applications-computations that are better specified using set queries, recursive functions, state updates including input/output operations, and object encapsulation.
Significant effort has been devoted to integrating or interfacing logic programming with other programming paradigms-database programming, functional programming, imperative programming, and object-oriented programming-resulting in many mixtures of languages [KL18,KLB + 22]. What's challenging has been: 1) a simple and powerful language that can express computations using all of rules, sets, functions, updates, and objects, naturally as built-ins without extra interfaces, and with a clear integrated semantics; and 2) a compilation and optimization framework for this powerful language, for implementation in a widely-used programming language, and with generally good performance.
This paper presents a powerful language, Alda, that allows computations to be expressed easily and clearly using all of rules, sets, functions, updates, and objects as well as concurrent and distributed processes.
• Sets of rules can be specified in any scope, just as other built-in constructs in high-level object-oriented languages.
• Predicates in rules are treated as set variables holding the set of tuples for which the predicate is true, and vice versa. Thus, predicates can be used directly in other constructs as variables without needing any interface. In this way, predicates are completely different from functions or procedures, unlike in previous logic languages and extensions.
• Predicates as variables can be global variables, object fields, or local variables in a rule set. They can be aliased if needed for efficiency, just like other variables. Predicates as variables also avoid the need for higher-order predicates or more complicated features for reusing predicate definitions in more complex logic languages.
• A dedicated inference function can be called any time on a rule set, to infer values of derived predicates (i.e., predicates in conclusions of rules) and answer queries, given values of base predicates (i.e., predicates not in conclusions of rules).
• Declarative semantics of rules are maintained automatically after any update that may affect the meaning of the rules, including in the presence of aliasing, through implicit calls to the inference function as needed.
We also define a formal semantics that integrates declarative and operational semantics. The integrated semantics supports, seamlessly, all of logic programming with rules, database programming with sets, functional programming, imperative programming, and object-oriented programming including concurrent and distributed programming. Implementing such a powerful language to support this diverse range of features is also challenging, especially to make all features easily usable in a widely-used language. Furthermore, the conceptually simple semantics for ensuring declarative semantics requires different implementation methods depending on the kinds of updates in the program.
We design a compilation and optimization framework that ensures the correct declarative semantics of rules, while also being able to exploit available optimizations.
• The compilation framework considers different kinds of updates and aliasing and uses the most efficient method for each kind.
• The framework employs well-known optimizations to address different sources of potential inefficiencies. This includes reusing results of expensive queries that use rules, updating derived predicates incrementally when base predicates are updated, as well as optimizations within the inference using rules.
There has been a significant amount of related research, as discussed in Section 7. They either do not integrate rules with all of sets, functions, updates, and objects, or require extra programming interfaces to wrap features in special objects, pass code as special string values, or convert data to special representations.
We also describe a prototype implementation of the language, programming and performance benchmarks, and experimental evaluation results. These benchmarks and results help confirm the power and benefits of a seamlessly integrated language and its generally good performance.
The rest of the paper is organized as follows. Section 2 describes the challenges of programming using rules with other features. Section 3 presents the language and gives an overview of the formal semantics. Section 4 describes the compilation and optimization framework. Section 5 presents programming and performance benchmarks. Section 6 describes the implementation and experimental results. Section 7 discusses related work and concludes. Appendix A presents a complete formal semantics for our language.
2 Programming with rules and other features Logic rules and queries allow complex reasoning and analysis problems to be expressed declaratively, easily and clearly, at a high level, often in ways not possible using set expressions, recursive functions, or other constructs. However, logic rules are not appropriate for many other aspects of applications, causing logic languages to include many non-declarative features, including tricky special features such as cut and negation as failure instead of logical negation [SS94].
At the same time, languages commonly used in practice are imperative and often objectoriented, because updates are essential in modeling the real world, and object encapsulation is fundamental for organizing system components in large applications. In these languages, logic rules are missing, and tedious interfaces are needed to use them, similar to how database interfaces such as ODBC [Gei95] and JDBC [Ree00] are needed to use SQL queries.
The challenge is how to support rules with all other features seamlessly, with no extra interfaces or boilerplate code.
• Using imperative updates with appropriate data structures, transitive closure can be computed following a well-known O(n 3 ) time algorithm, or a better O(mn) time algorithm [LS09].
However, it requires using an adjacency list or adjacency matrix for graph representation, and a depth-first search or breadth-first search for searching the graph, updating the detailed data structures carefully as the search progresses. The resulting program is orders of magnitude larger and drastically more complex.
• Using sets and set expressions, an imperative algorithm can be written much more simply at a higher level. For example, in Python, given a set E of edges, the transitive closure T can be computed as follows, where T starts with E (using a copy so E is not changed when T is), workset W keeps newly discovered pairs, and while W continues if W is not empty: T = E . copy () W = {(x , y ) for (x , z ) in T for ( z2 , y ) in E if z2 == z } -T while W : T . add ( W . pop ()) W = {( x , y ) for (x , z ) in T for ( z2 , y ) in E if z2 == z } -T However, expensive set operations are computed for W in each iteration, and the total is worst-case O(mn 4 ) time. One may find ways to avoid the duplicated code for computing W, but similar set operations in each iteration cannot be avoided. Incrementalization [PK82,Liu13] can derive efficient O(mn) time algorithm, but such transformation is not supported in general in any commonly-used language.
• Using functions, one can wrap the imperative code above, either using high-level sets or not, in a function definition that can be called at uses.
However, if imperative updates and while loops are not allowed, the resulting programs that use recursion would not be fundamentally easier or simpler than the programs above. In fact, writing these functional programs are so nontrivial that it required research papers for individual problems, e.g., for computing the transitive closure [BF15], which develops a Haskell program with no better than O(m 3 ) time, which is worst-case O(n 6 ).
• Using recursive queries in SQL, which are increasingly supported and are essentially set expressions plus recursion, the transitive closure query can be expressed more declaratively than the programs above.
However, using recursive SQL queries is much more complex than using rules [KBL06]. Additionally, tedious interface code is needed to use SQL queries from a host language for programming non-SQL parts of applications [Gei95] Moreover, all these programs are only for computing the transitive closure; to compute the other three kinds of reachability queries, separate programs or additional code are needed, but additional code on top of the transitive closure code will not give the better performance possible for those queries.
Challenges of using rules with everything else for other tasks. Using rules makes complex reasoning and queries easy, but other language constructs are needed for other aspects of real-world applications. How can one integrate rules with everything else without extra interfaces? There are several main challenges, for even very basic questions.
• The most basic question is, how do predicates and logic variables in logic languages relate to constructs in commonly-used languages?
A well-accepted correspondence is: a predicate corresponds exactly to a Boolean-valued function, evaluating to true or false on given arguments. Indeed, in dominant logic programming languages based on Prolog [SS94], a predicate defined using rules is often used like a function or even procedure (when it uses non-declarative features, such as input and output) defined using expressions and statements in commonly-used languages.
However, a big difference is that, instead of following the control flow from function or procedure arguments to the return value or result, techniques like unification are used to equate different occurrences of a variable in a rule. Indeed, logic variables in rules are very different from variables in commonly-used languages: the former equate or relate arguments of predicates, whereas the latter store computed values of expressions.
Thus, the correspondence between predicates and functions, and between logic variables and variables in commonly-used languages, is not proper. A seamlessly integrated language must establish the proper correspondence.
• Another basic puzzle is even within logic languages themselves. How can a set of rules defined over a particular predicate be used over a different predicate? For example, how can rules defining path over predicate edge be used over another predicate, say link?
Supporting such uses has required higher-order predicates or more complex features, e.g., [CKW93]. They incur additional baggage that compromise the ease and clarity of using rules. Moreover, there have not been commonly accepted constructs for them.
• A number of other basic but important language features do not have commonly accepted constructs in logic languages, or are not supported at all: updates, classes and modules, and concurrency with threads and processes [MTKW18].
Even basic declarative features are often only partially supported in logic languages and do not have commonly accepted constructs: set comprehension, aggregation (such as count, sum, or max), and general quantification.
Even the semantics of recursive rules, when simple operations such as negation and aggregation are also used, has been a matter of significant disagreement [Tru18, GZ19, LS20, LS22].
A seamlessly integrated language that supports rules must not create complications for using everything else in commonly-used languages.
We address these challenges with the Alda language, with an integrated declarative and operational semantics, allowing complex queries to be written declaratively, easily and clearly, and be implemented with generally good performance.

Alda language
We first introduce rules and then describe how our overall language supports rules with sets and functions as well as imperative updates and object-oriented programming. Figure 1 shows an example program in Alda that uses all of rules, sets, functions, updates, and objects. It will be explained throughout Sections 3.1-3.6 when used as examples. Section 3.7 provides an overview of the formal semantics. A complete exposition of the abstract syntax and formal semantics is in Appendix A.

Logic rules
We support rule sets of the following form, where name is the name of the rule set, declarations is a set of predicate declarations, and the body is a set of rules. Declarations are about predicates used in the rule set, for advanced uses, and are optional. For example, they may specify argument types of predicates, so rules can be compiled to efficient standalone imperative programs [LS03,LS09] that are expressed in typed languages [RL07]. We omit the details because they are orthogonal to the focus of the paper. In particular, we omit types to avoid unnecessary clutter in code.
We use Datalog rules [AHV95,MTKW18] in examples, but our method of integrating semantics applies to rules in general. Each hypothesis and conclusion in a rule is an assertion, of the form where p is a predicate, and each arg k is a variable or a constant. We use numbers and quoted strings to represent constants, and the rest are variables. As is standard for safe rules, all variables in the conclusion must be in a hypothesis. If a conclusion holds without   a hypothesis, then each argument in the conclusion must be a constant, in which case the conclusion is called a fact. Note that a predicate is also called a relation, relating the arguments of the predicate.
Example. For computing the transitive closure of a graph in the running example, the rule set, named trans rs, in Figure 1 (lines 15-17) can be written. The rules are the same as in dominant logic languages except for the use of lower-case variable names, the change of :-to if, and the omission of dot at the end of each rule.
Terminology. In a rule set, predicates not in any conclusion are called base predicates of the rule set, and the other predicates are called derived predicates of the rule set.
We say that a predicate p depends on a predicate q if p is in the conclusion of a rule whose hypotheses contain q or contain a predicate that depends on q recursively.
We say that a derived predicate p in a rule set rs fully depends on a set s of base predicates in rs if p does not depend on other base predicates in rs.
Example. In rule set trans rs, edge is a base predicate, and path is a derived predicate. path depends on edge and itself. path fully depends on edge.

Integrating rules with sets, functions, updates, and objects
Our overall language supports all of rule sets and the following language constructs as builtins; all of them can appear in any scope-global, class, and local.
• Sets and set expressions (comprehension, aggregation, quantification, and high-level operations such as union) to make non-recursive queries over sets easy to express.
• Function and procedure definitions with optional keyword arguments, and function and procedure calls.
• Imperative updates by assignments and membership changes, to sets and other data, in sequencing, branching, and looping statements.
• Class definitions containing object field and method (function and procedure) definitions, object creations, and inheritance.
A name holding any value is global if it is introduced (declared or defined) at the global scope; is an object field if it is introduced for that object; or is local to the function, method, or rule set that contains it otherwise. The value that a name is holding is available after the name is defined: globally for a global name, on the object for an object field, and in the enclosing function, method, or rule set for a local name.
Example. Rule set trans rs in Figure 1 (defined on line 15 and queried on line 19) is used together with sets (defined on lines 3 and 12), set expressions (on lines 8, 19, and 21), functions (defined on lines 7, 18, and 20), procedures (defined on lines 2, 5, 10, and 13), updates (on lines 3, 6, 12, 14), classes (defined on lines 1 and 9, with inheritance), and objects (created on line 22). No extra code is needed to convert edge and path, declare logic variables, and so on.
The key ideas of our seamless integration of rules with sets, functions, updates, and objects are: (1) a predicate is a set-valued variable that holds a set of tuples for which the predicate is true, (2) queries using rules are calls to an inference function that computes desired sets using given sets, (3) values of predicates can be updated either directly as for other variables or by the inference function, and (4) predicates and rule sets can be object attributes as well as global and local names, just as sets and functions can.
Integrated semantics, ensuring declarative semantics of rules. In our overall language, the meaning of a rule set rs is completely declarative, exactly following the standard least fixed-point semantics of rules [Fit02,LS09]: Given values of any set s of base predicates in rs, the meaning of rs is, for all derived predicates in rs that fully depend on s, the least set of values that can be inferred, directly or indirectly, by using the given values and the rules in rs; for any derived predicate in rs that does not fully depend on s, i.e., depends on any base predicate whose values are not given, its value is undefined.
The operational semantics for the rest of the language ensures this declarative semantics of rules.
The precise constructs for using rules with sets, functions, updates, and objects are described in Sections 3.3-3.6.

Predicates as set-valued variables
For rules to be easily used with everything else, our most basic principle in designing the language is to treat a predicate as a set-valued variable that holds the set of tuples that are true for the predicate, that is: This means that, as variables, predicates in a rule set can be introduced in any scope-as global variables, object fields, or variables local to the rule set-and they can be written into and read from without needing any extra interface.
Example. In rule set trans rs in Figure 1, predicate edge is exactly a variable holding a set of pairs, such that edge(x,y) is true iff (x,y) is in edge, and edge is local to trans rs In general, edge can be a global variable, an object field, or a local variable of trans rs. Similarly for predicate path.
Writing to predicates is discussed later under updates to predicates, but reading and using values of predicates can simply use all operations on sets. We use set expressions including the following: When an existential quantification returns true, variables v 1 ,...,v k are bound to a witness. Note that these set queries, as in [LSL17], are more powerful than those in Python.
Example. For computing the transitive closure T of a set E of edges, the following while loop with quantification can be used (we will see that we use objects and updates as in Python except the syntax := for assignment in this paper): T := E . copy () while some (x , z ) in T , (z , y ) in E | (x , y ) not in T : T . add ((x , y )) This is simpler than the Python while loop in Section 2: it finds a witness pair (x,y) directly using some, instead of constructing workset W and then using pop to get a pair.
In the comprehension and aggregation forms, each v i can also be a tuple pattern that elements of the set value of sexp i must match [LSL17]. 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. For a value to match a tuple pattern, it must have the corresponding tuple structure, with corresponding components equal the values of non-variable expressions and variables prefixed with =, and with corresponding components assigned to variables not prefixed with =; corresponding components for wildcard are ignored.
Example. To return the set of second component of pairs in path whose first component equals the value of variable x, and where that second component is also the first component of pairs in edge whose second component is 1, one may use a set comprehension with tuple patterns: { y : (=x , y ) in path , (y ,1) in edge } Now that predicates in rules correspond to set-valued variables, instead of functions or procedures, we can further see that logic variables, i.e., variables in rules, are like pattern variables, i.e., variables not prefixed with = in patterns. Logic rules do not have variables that store values, i.e., variables prefixed with = in patterns.

Queries as calls to an inference function
For inference and queries using rules, calls to a built-in inference function infer, of the following form, are used, with query k 's and p k =sexp k 's being optional: infer(query 1 , ..., query j , p 1 =sexp 1 , ..., p i =sexp i , rules=rs) rs is the name of a rule set. Each sexp k is a set-valued expression. Each p k is a base predicate of rs and is local to rs. Each query k is of the form p(arg 1 ,...,arg a ), where p is a derived predicate of rs, and each argument arg k is a constant, a variable possibly prefixed with =, or wildcard . A variable prefixed with = indicates a bound variable whose value will be used as a constant when evaluating the query. So arguments of queries are patterns too. If all arg k 's are , the abbreviated form p can be used.
Function infer can be called implicitly by the language implementation or explicitly by the user. It is called automatically as needed and can be called explicitly when desired.
Example. For inference using rule set trans rs in Figure 1, where edge and path are local variables, infer can be called in many ways, including: infer ( path , edge = RH , rules = t r a n s _ r s) infer ( path (_ , _ ) , edge = RH , rules = t r a n s _ r s) infer ( path (1 , _ ) , path (_ ,= R ) , edge = RH , rules = t r a n s _ r s) The first is as in Figure 1 (line 19). The first two calls are equivalent: path and path( , ) both query the set of pairs of vertices having a path from the first vertex to the second vertex, following edges given by the value of variable RH. In the third call, path(1, ) queries the set of vertices having a path from vertex 1, and path( ,=R) queries the set of vertices having a path to the vertex that is the value of variable R.
If edge or path is a global variable or an object field, one may call infer on trans rs without assigning to edge or querying path, respectively.
The operational semantics of a call to infer is exactly like other function calls, except for the special forms of arguments and return values, and of course the inference function performed inside: 1) For each value k from 1 to i, assign the set value of expression sexp k to predicate p k that is a base predicate of rule set rs.
2) Perform inference using the rules in rs and the given values of base predicates of rs following the declarative semantics, including assigning to derived predicates that are not local.
3) For each value k from 1 to j , return the result of query query k as the k th component of the return value. The result of a query with l distinct variables not prefixed with = is a set of tuples of l components, one for each of the distinct variables in their order of first occurrence in the query.
Note that when there are no p k =sexp k 's, only defined values of base predicates that are not local to rs are used; and when there are no query k 's, only values of derived predicates that are not local to rs may be inferred and no value is returned. Section 5.2 on benchmarks using Role-Based Access Control (RBAC) discusses different ways of using rules and different calls to infer: implicit vs. explicit, in an enclosing expression vs. by itself, passing in all base predicates vs. only some, etc.

Updates to predicates
Values of base predicates can be updated directly as for other set-valued variables, and values of derived predicates are updated by the inference function.
Base predicates of a rule set rs that are local to rs are assigned values at calls to infer on rs, as described earlier. Base predicates that are not local can be updated by using assignment statements or set membership update operations. We use lexp := exp for assignments, where lexp can also be a nested tuple of variables, and each variable is assigned the corresponding component of the value of exp.
Derived predicates of a rule set rs can be updated only by calls to the inference function on rs. These updates must ensure the declarative semantics of rs: Whenever a base predicate of rs is updated in the program, the values of the derived predicates in rs are maintained according to the declarative semantics of rs by calling infer on rs. Updates to derived predicates of rs outside rs are not allowed, and any violation will be detected and reported at compile time if possible and at runtime otherwise.
Simply put, updates to base predicates trigger updates to derived predicates, and other updates to derived predicates are not allowed. This ensures the invariants that the derived predicates hold the values defined by the rule set based on values of the base predicates, as required by the declarative semantics. Note that this is the most straightforward semantics, but the implementation can avoid many inefficiencies with optimizations, as described in Section 4.3.
Example. Consider rule set trans rs in Figure 1. If edge is not local, one may assign a set of pairs to edge: If edge is local, the example calls to infer in Section 3.4 assign the value of RH to edge. If path is not local, then a call infer(edge=RH, rules=trans rs) updates path, contrasting the first two calls to infer in the previous example that return the value of path.
If path is local, the return value of infer can be assigned to variables. For example, for the third example call to infer in Section 3.4, this can be from1 , toR := infer ( path (1 , _ ) , path (_ ,= R ) , edge = RH , rules = t r a n s _ r s) If both edge and path are not local, then whenever edge is updated, an implicit call infer(rules=trans rs) is executed automatically to update the value of path.

Using predicates and rules with objects and classes
Predicates and rule sets can be object fields as well as global and local names, just as sets and functions can, as discussed in Section 3.2. This allows predicates and rule sets to be used seamlessly with objects in object-oriented programming.
For other constructs than those described above, we use those in high-level object-oriented languages. We mostly use Python syntax (looping, branching, indentation for scoping, ':' for elaboration, '#' for comments, etc.) for succinctness, but with a few conventions from Java (keyword new for object creation, keyword extends for subclassing, and omission of self, the equivalent of this in Java, when there is no ambiguity) for ease of reading.
Example. We use Role-Based Access Control (RBAC) to show the need of using rules with all of sets, functions, updates, and objects and classes.
RBAC is a security policy framework for controlling user access to resources based on roles and is widely used in large organizations. The ANSI standard for RBAC [ANS04] was approved in 2004 after several rounds of public review [SFK00, JT00, FSG + 01], building on much research during the preceding decade and earlier. High-level executable specifications were developed for the entire RBAC standard [LS07], where all queries are declarative except for computing the transitive role-hierarchy relation in Hierarchical RBAC, which extends Core RBAC.
Core RBAC defines functionalities relating users, roles, permissions, and sessions. It includes the sets and update and query functions in class CoreRBAC in Figure 1, as in [LS07] 1 .
Hierarchical RBAC adds support for a role hierarchy, RH, and update and query functions extended for RH. It includes the update and query functions in class HierRBAC in Figure 1, as in [LS07] 1 , except that function transRH() in [LS07] computes the transitive closure of RH plus reflexive role pairs for all roles in ROLES by using a complex and inefficient while loop similar to that in Section 2 plus a union with the set of reflexive role pairs {(r,r): r in ROLES}, whereas function transRH() in Figure 1 simply calls infer and unions the result with reflexive role pairs. Note though, in the RBAC standard, a relation transRH is used in place of transRH(), intending to maintain the transitive role hierarchy incrementally while RH and ROLES change. It is believed that this is done for efficiency, because the result of transRH() is used continually, while RH and ROLES change infrequently. However, the maintenance was done inappropriately [LS07,LBB07] and warranted the use of transRH() to ensure correctness before efficiency.
Overall, the RBAC specification relies extensively on all of updates, sets, functions, and objects and classes with inheritance, besides rules: (1) updates for setting up and updating the state of the RBAC system, (2) sets and set expressions for holding the system state and expressing set queries exactly as specified in the RBAC standard, (3) methods and functions for defining and invoking update and query operations, including function transRH(), and (4) objects and classes for capturing different components-CoreRBAC, HierRBAC, constraint RBAC, their further refinement, extensions, and combinations, totaling 9 components, corresponding to 9 classes, including 5 subclasses of HierRBAC [ANS04, LS07].

Formal semantics
Formal semantics of logic rules has been studied extensively, including the standard least fixed-point semantics for Datalog and more [Fit02,LS20]. A formal operational semantics for DistAlgo, a powerful language with all of sets, functions, updates, and objects, including even distributed processes as objects, but without rules, has also been given recently [LSL17].
Building on these prior semantics, we developed a formal semantics for a core language for Alda that preserves the semantics from all above and seamlessly connects declarative rule semantics and imperative update semantics. We removed the constructs specific to distributed processes and added the constructs described in this paper. The removed DistAlgo constructs can easily be restored to obtain a semantics for the full language; we removed them simply to avoid repeating them.
Appendix A contains details of the abstract syntax and semantics for our core language. This section presents a brief high-level overview of the semantics.
The operational semantics is a reduction semantics with evaluation contexts [WF94,SRM09]. It culminates in the definition of a transition relation between states. A state has the form s, ht, h, stk . s is the statement to be executed. ht is the heap type map: ht(a) is the type of the object on the heap at address a. h is the heap; it maps addresses to objects. stk is a special call stack used to track the rule sets whose results should be automatically maintained. It is initialized with an entry containing rule sets defined in global scope. When a method is called on an instance at address a of a user-defined class c, the call stack is extended by pushing an entry containing the rule sets defined in c, instantiated by replacing self with a; the entry is popped when the method returns. The transition relation for statements has the form σ → σ ′ where σ and σ ′ are states. It is implicitly parameterized by the program. The transition rules for assignment statements and calls to set membership update operations (e.g., add), user-defined methods, and infer are extended to maintain the results of all rule sets on the call stack. We outline two of these transition rules here as representative examples.
The transition rule for calling a method on an instance at address a of a user-defined class replaces the method call with a copy of the method body instantiated by substituting argument values for parameters, pushes onto the call stack an entry containing instantiated rule sets as described above, and updates the heap type map and heap to capture the results of automatic maintenance using all rule sets on the call stack. Automatic maintenance performs inference for each of those rule sets using values of their base predicates in the current state σ, and then updates the values of all their derived predicates in the next state σ ′ , like a single parallel-assignment statement. Use of parallel assignment is significant, because a derived predicate of one rule set can be a base predicate of another rule set. Values of predicates are instances of the built-in set class and are stored on the heap, just like other objects.
The transition rule for an explicit call to infer on a rule set instantiates that rule set using the given values for the rule set's parameters, updates the heap type map and heap to capture the results of evaluating the instantiated rule set and returning the query results, and then updates the heap type map and heap to capture the results of automatic maintenance using all rule sets on the call stack, as described above.

Compilation and optimization
The operational semantics to ensure the declarative semantics of rules is conceptually simple, but the implementation required can vary widely, depending on the kinds of updates in the programs. It can be extremely expensive or complex when there are arbitrary updates in an object-oriented language that allows aliasing of object references.
We first present how to compile all possible updates to predicates, starting with the checks and actions needed for correctly handling updates for a single rule set with implicit and explicit calls to infer. We then describe how to implement the inference in infer. Additionally, we systematize powerful optimizations that can be exploited in the overall compilation framework.

Compiling updates to predicates
The implementation required by the operational semantics of a rule set rs falls into three cases, based on the nature of updates to base predicates of rs outside rs. Note that inside rs there are no updates to base predicates of rs, by definition of base predicate. 1) Non-local updates with aliasing. For updates to non-local variables of rs in the presence of variable aliasing (i.e., two different variables referring to the same value or object), each update needs to check whether the variable updated may alias a base predicate of rs and, if so, an implicit call to infer on rs needs to be made.
An update outside rs to a non-local variable that is a derived predicate of rs needs to be detected and reported, conservatively at compile-time if possible, and at runtime otherwise. Note that this also solves the most nasty problem of possible aliasing between a derived predicate and a base predicate, because updating a base predicate of rs outside rs would also be updating a derived predicate of rs outside rs, which would be detected and reported.
2) Non-local updates without aliasing. For updates to non-local variables of rs when there is no variable aliasing, an implicit call to infer on rs needs to be made only after every update to a base predicate of rs.
Without aliasing, statements outside rs that update derived predicates of rs can be identified and reported as errors at compile time. There are no updates outside rs to local variables that are derived predicates of rs, by definition of local variables.
To satisfy these requirements, the overall method for compiling an update to a variable v outside rule sets is: • In the presence of aliasing: insert code that does the following after the update: if v refers to a derived predicate of any rule set, report a runtime error and exit; otherwise for each rule set rs, if v refers to a base predicate of rs, call infer on rs with no arguments for base predicates and no queries.
• In the absence of aliasing: report a compile-time error if v is a derived predicate of any rule set; otherwise, for each rule set rs that contains v as a base predicate, insert code, after the update, that calls infer on rs with no arguments for base predicates and no queries.
Compiling an explicit call to infer on a rule set directly follows the operational semantics of infer.
In effect, function infer is called to implement a wide range of control: from inferring everything possible using all rule sets and values of all base predicates at every update in the most extensive case, to answering specific queries using specific rules and specific sets of values of specific base predicates at explicit calls.
Obviously, use of updates in different contexts has significant impact on program efficiency. It is particularly worth noting that the very existence of aliasing, intended to provide efficiency at the cost of tedious and error-prone programming, incurs the most performance penalty.

Implementing inference and queries
Any existing method can be used to implement the functionality inside infer. The inference and queries for a rule set can use either bottom-up or top-down evaluation [KL18,TL10,TL11], so long as they use the rule set and values of the base predicates according to the declarative semantics of rules The inference and queries can be either performed by using a general logic rule engine, e.g., XSB [SW12, SWS + 21], or compiled to specialized standalone executable code as in, e.g., [LS09,RL07], that is then executed. Our implementation uses the former approach, by indeed using the well-known XSB system, as described in Section 6, because it allows easier extensions to support more kinds of rules and optimizations that are already supported in XSB.

Powerful optimizations
Efficient inference and queries using rules is well known to be challenging in general, and especially so if it is done repeatedly to ensure the declarative semantics of rules under updates to predicates. Addressing the challenges has produced an extensive literature in several main areas in computer science-database, logic programming, automated reasoning, and artificial intelligence in general-and is not the topic of this paper.
Here, we describe how well-known analyses and optimizations can be used together to improve the implementation of the overall language as well as the rule language, giving a systemic perspective of all main optimizations for efficient implementations. There are two main areas of optimizations.
The first area is for inference under updates to the predicates used. There are three main kinds of optimizations in this area: (1) reducing inference triggered by updates, (2) performing inference lazily only when the results are demanded, and (3) doing inference incrementally when updates must be handled to give results: Reducing update checks and inference. In the presence of aliasing, it can be extremely inefficient to check, for all rule sets after every update, that the update is not to a derived predicate of the rule set and whether a call to infer on the rule set is needed, not knowing statically whether the update affects a base predicate of the rule set. Alias analysis, e.g., [Goy05, GLS + 10], can help reduce such checks by statically determining updates to variables that possibly alias a predicate of a rule set.
Demand-driven inference. Calling infer after every update to a base predicate can be inefficient and wasteful, because updates can occur frequently while the maintained derived predicates are rarely used. To avoid this inefficiency, infer can be called on demand just before a derived predicate is used, e.g., [FU76,RL08,LBSL16], instead of immediately after updates to base predicates.
Incremental inference. More fundamentally, even when derived predicates are frequently used, infer can easily be called repeatedly on slightly changed or even unchanged base predicates, in which case computing the results from scratch is extremely wasteful. Incremental computation can drastically reduce this inefficiency by maintaining the values of derived predicates incrementally, e.g., [GM99,SR03].
The second area is for efficient implementation of rules by themselves, without considering updates to the predicates used. There are two main groups of optimizations.
Internal demand-driven and incremental inference. Even in a single call to infer, significant optimizations are needed.
In top-down evaluation (which is already driven by the given query as demand), subqueries can be evaluated repeatedly, so tabling [TS86, CW96] (a special kind of incremental computation by memoization) is critical for avoiding not only repeated evaluation of queries but also non-termination when there is recursion.
In bottom-up evaluation (which is already incremental from the ground up), demand transformation [TL10,TL11], which improves over magic sets [BMSU86] exponentially, can transform rules to help avoid computations not needed to answer the given query.
Ordering and indexing for inference. Other factors can also drastically affect the performance of logic queries in a single call to infer [MTKW18,Liu18].
Most prominently, in dominant logic rule engines like XSB, changing the order of joining hypotheses in a rule can impact performance dramatically, e.g., for the transitive closure example, reversing the two hypotheses in the recursive rule can cause a linear factor performance difference. Reordering and indexing [LS09,LBSL16] are needed to avoid such severe slowdowns.

Programming and performance benchmarks
We have used Alda for a variety of well-known problems where rules can be used for both ease of programming and performance of execution. We describe a set of benchmarks for programming and performance evaluation. One can see that even for problems that were previously focused on for using rules, it becomes much easier to program using an integrated language like Alda. We developed three sets of benchmarks, from OpenRuleBench [LFWK09], RBAC [ANS04,LS07], and program analysis. OpenRuleBench benchmarks show the wide range of application problems previously developed using different kinds of rule systems. RBAC benchmarks show the use of rules in an application that requires all of sets, functions, updates, and objects and classes, and show different ways of using rules. Program analysis benchmarks demonstrate seamlessly integrated use of rules with also aggregate queries and recursive functions; we also contrast with using aggregate queries in rule languages, which are not used in any benchmark in OpenRuleBench.

OpenRuleBench-a wide variety of rule-based applications
OpenRuleBench [LFWK09] contains a wide variety of database, knowledge base, and semantic web application problems, written using rules in 11 well-known rule systems from 5 different categories, as well as large data sets and a large number of test scripts for running and measuring the performance. Among 14 benchmarks described in [LFWK09], we consider all except for one that tests interfaces of rule systems with databases (which is a non-issue for Alda because it extends Python which has standard and widely-used database interfaces). Table 1 summarizes the benchmarks. We compare with the benchmark programs in XSB, for three reasons: (1) XSB has been the most advanced rule system supporting well-founded semantics for non-stratified negation and tabling techniques for efficient query evaluation, and has been actively developed for over three decades, to this day; (2) among all systems reported in [LFWK09], XSB was one of the fastest, if not the fastest, and the most consistent across all benchmarks; and (3) among all measurements reported, only XSB, OntoBroker, and DLV could run all benchmarks, but OntoBroker went bankrupt, and measurements for DLV were almost all slower, often by orders of magnitude.
We easily translated all 13 benchmarks into Alda, automatically for all except for three cases where the original rules used features beyond Datalog, which became two cases after we added support for negation. In all cases, it was straightforward to express the desired functionality in Alda, producing a program that is very similar or even simpler. Additionally, the code for reading data, running tests, timing, and writing results is drastically simpler in Alda as it extends Python. These special cases and additional findings are described below.
Result set. In most logic languages, including Prolog and many variants, a query returns only the first result that matches the query. To return the set of all results, some wellknown tricks are used. The LUBM benchmark includes the following extra rules to return all answers of query9 1:  well-known win-not-win game with non-stratified negation 24 * MagicSet non-stratified rules from magic-set transformation 34 * Table 1: Benchmarks for problems in OpenRuleBench [LFWK09]. The three groups (of 6, 4, 3) in order are called large join tests, Datalog recursion, and default negation, respectively. Prog size is the XSB program size in lines of code without comments and empty lines. Data size is the input data size in number of facts; * means that scripts are used to generate input data of desired sizes.
The first rule first queries query9 1 to find an answer (a triple of values for X,Y,Z) and then uses fail to trick the inference into thinking that it failed to find an answer and so continuing to search for an answer; and it does this repeatedly, until query9 1 does fail to find an answer after exhausting all answers. The second rule is necessary, even if with an empty right side, to trick the inference into thinking that it succeeded, because the first rule always ends in failing; this is so that the execution can continue to do the remaining work instead of stopping from failing. In fact, this trick is used for all benchmarks, but other uses are buried inside the code for running, timing, etc., specialized for each benchmark, not as part of the rules for the application logic.
In Alda, such rules and tricks are never needed. A call to infer with query query9 1 returns the set of all query results as desired. If query9 1 is a non-local predicate, then the set value of query9 1 can be used directly, and no explicit call to infer is needed. In case only one result is wanted, a special function for taking any one value can be applied to the result set of calling infer or the non-local predicate; an optimized implementation can then search for only the first result.
Function symbols. Logic rules may use function symbols to form structured data that can be used as arguments to predicates. Uses of function symbols can be translated away. The Mondial benchmark uses a function symbol prov in several intermediate conclusions and hypotheses of the form isa(prov(Y,X),provi) or att(prov(Y,X),number,A). They can simply be translated to isa('prov',Y,X,provi) and att('prov',Y,X,number,A), respectively.
Negation. Logic languages may use negation applied to hypotheses in rules. Most logic languages only support non-stratified negation, where there is no negation involved in cyclic dependencies among predicates. Such negation can be done by set differences. The ModSG benchmark has such a negation, as follows, where sg is defined by the rules in the SG benchmark, and nonsg is defined by two new rules: In Alda, this can be written as sg2 = infer(sg, rules=sg rs) -infer(nonsg, rules=nonsg rs) where sg rs and nonsg rs are the rule sets defining sg and nonsg, respectively, and all base predicates are non-local.
We also added support for negation in our implementation, which translates negation to tabled negation tnot in XSB, instead of Prolog's negation as failure. This handles even nonstratified negation by computing well-founded semantics using XSB. The Win and MagicSet benchmarks have non-stratified negation. Both of them, as well as ModSG, can be expressed directly in Alda rule sets by using not for negation.
Benchmarking and organization. In OpenRuleBench benchmarks, even though the rules to be benchmarked are declarative and succinct, the benchmarking code for reading input, running tests, timing, and writing results are generally much larger. For example, the Join1 benchmark has 4 small rules and 9 small queries similar in size to those in the transitive closure example, plus a manually added tabling directive for optimization. However, for each query, 19 more lines for an import and two much larger rules are used to do the reading, running, timing, and writing.
In general, because benchmarking executes a bundle of commands, scripting those directly is simplest. Furthermore, organizing benchmarking code using procedures, objects, etc., allows easy reuse without duplicated code. These features are much better supported, for both ease of programming and performance, in languages like Python than rule systems.
In fact, OpenRuleBench uses a large number of many different files, in several languages (language of the system being tested, XSB, shell script, Python, makefile) for such scripting. For example for Join1, the 4 rules, tabling directive, and benchmarking code are also duplicated in each of the 9 XSB files, one for each query; a 46-line shell script and a 9-line makefile are also used.
In contrast, our benchmarking code is in Alda, which uses Python functions for scripting. A same 45-line Alda program is used for timing any of the benchmarks, and for pickling (i.e., object serialization in Python, for fast data reading after the first reading) and timing of pickling.
Aggregation. Despite the wide variety of benchmarks in OpenRuleBench, no benchmark uses aggregate queries. Aggregate queries are essential for many database, data mining, and machine learning applications. We discuss them and compare with aggregate queries in a rule language like XSB in Section 5.3.

Name
Features used for computing transitive role hierarchy RBACnonloc rule set transRH rs with implicit infer, without transRH(), Sec. 5.2 RBACallloc rule set trans role rs and transRH() that has only infer, Sec. 5.2 RBACunion rule set trans rs and transRH() that has infer and union, Sec. 3.6 RBACda while loop and high-level set queries in DistAlgo, Sec. 3.3 RBACpy while loop and high-level set comprehensions in Python [LS07] Table 2: Benchmarks for RBAC updates and queries. Each performs a combination of updates to sets and relations USERS, ROLES, UR, and RH and queries with function AuthorizedUsers(role), where the transitive role hierarchy is computed with a different way of using rules, or not using rules. In AuthorizedUsers(role) of all five programs, the call to transRH(), or reference to field transRH, is lifted out of the set query, by assigning its value to a local variable and using that variable in the query.

RBAC-rules with objects, updates, and set queries
As discussed in Section 3.6, a complex and inefficient while loop was used in [LS07] to program the transitive role hierarchy, but as discussed in Section 2, an efficient algorithm with appropriate data structures and updates would be drastically even more complex.
With support for rules, we easily wrote the entire RBAC standard in Alda, similar as in Python [LS07], except with rules for computing the transitive role hierarchy, as described in Section 3.6, and with simpler set queries and omission of self, despite complex class inheritance relationships, yielding a simpler yet more efficient program.
Below, we describe different ways of using rules to compute the transitive role hierarchy and the function AuthorizedUsers(role) in Section 3.6. All these ways are declarative and differ in size by only 1-2 lines. Table 2 summarizes the benchmarks for RBAC that include all RBAC classes with their inheritance relationships and perform update operations and these query functions in different ways.
In particular, in the first way below, a field, transRH, is used and maintained automatically; it avoids calling transRH() repeatedly, as desired in the RBAC standard, and it does so without the extra maintenance code in the RBAC standard for handling updates.
Rules with only non-local predicates. Using rules with only non-local predicates, one can use a relation transRH in place of calls to transRH(), e.g., in function AuthorizedUsers(role), by simply adding a field transRH and using the following rule set in class HierRBAC: 2 rules t r a n s R H _ r s : # no need to use infer e x p l i c i t l y t r a n s R H(x , y ) if RH (x , y ) t r a n s R H(x , y ) if RH (x , z ) , t r a n s R H(z , y ) t r a n s R H(x , x ) if ROLES ( x ) Field transRH is automatically maintained at updates to RH and ROLES by implicit calls to infer; no explicit calls to infer are needed. This eliminates the need of function transRH() and repeated expensive calls to it even when its result is not changed most of the time.
Overall, this simplifies the program, ensures correctness, and improves efficiency.
Rules with only local predicates. Using rules with only local predicates, infer must be called explicitly. One can simply use the function transRH() in Section 3.6, which calls infer using rule set trans rs in the running example and then unions with reflexive role pairs. Alternatively, one can use the rules in trans rs plus a rule that uses a local role set that adds the reflexive role pairs: rules t r a n s _ r o l e _ r s : # as t r a n s _ r s plus the added last rule path (x , y ) if edge (x , y ) path (x , y ) if edge (x , z ) , path (z , y ) path (x , x ) if role ( x ) def t r a n s R H (): # use infer only , pass in also ROLES return infer ( path , edge = RH , role = ROLES , rules = t r a n s _ r o l e _ r s ) Both ways show the ease of using rules by simply calling infer. Despite possible inefficiency in some uses, using only local predicates has the advantage of full reusability of rules and full control of calls to infer.
Rules with both local and non-local predicates. One can also use rules with a combination of local and non-local predicates, e.g., the same rules as above but with field ROLES in place of the local role, removing the need for infer to pass in ROLES. Any other combination can also be used. Different combinations give different controls to infer to pass in and out appropriate sets.
Of course, non-recursive set queries, such as AuthorizedUsers(role) can also be expressed using rules, and use any combination of local and non-local predicates.

Program analysis-rules with aggregate queries and recursive functions
We have used Alda to analyze widely-used Python packages, and designed a benchmark based on our experiences, especially to show integrated use of rules with aggregate queries and recursive functions. Aggregate queries help quantify and characterize the analysis results, and recursive functions help do these on recursive structures. The benchmark is for analysis of class hierarchy of Python programs. It uses logic rules to extract class names and construct the class extension relation; aggregate queries and set queries to characterize the results and find special cases of interest; recursive functions as well as aggregate and set queries to analyze the special cases; and more logic rules, functions, and set and aggregate queries to further analyze the special cases. Table 3 summarizes different parts of this benchmark, called PA. We also use a variant, called PAopt, that is the same as PA except that, in the recursive rule for defining transitive descendant relationship, the two hypotheses are reversed, following previously studied optimizations [LS09,TL10]. Additionally, we compare with corresponding programs written in a rule language like XSB, expressing aggregate queries and reursive functions.

Analysis
Features used 1 Ext classes, extension relation rules (recursive if refined name analysis is used) 2 Stat statistics, roots aggregate and set queries 3 Hgt max height, roots of max height recursive functions, aggregate and set queries 4 Desc max desc, roots of max desc recursive rules, functions, aggregate and set queries Table 3: Benchmark PA for program analysis, integrating different kinds of analysis problems. In 1 and 4 that use rules, not using rules (esp. for recursive analysis, with tabling) would be drastically worse (i.e., harder to program and less efficiency). In 2-4 that use aggregate and set queries, using rules or recursive functions would be clearly worse. In 3 and 4 that use functions, not using functions (with tabling, also called caching) would be much worse.
Because the focus is on evaluating the integrated use of different features, each part that uses a single feature, such as rules, is designed to be small. Compared with making each part larger, which exercises individual features more, this design highlights the overhead of connecting different parts, in terms of both ease of use and efficiency of execution.
The program takes as input the abstract syntax tree (AST) of a Python program (a module or an entire package), represented as set of facts. Each AST node of type T with k children corresponds to a fact for predicate T with k +1 arguments: id of the node, and ids the k children. Lists are represented using Member facts. A Member(lst,elem,i ) fact denotes that list lst has element elem at the i th position.
Part 1: Classes and class extension relation. This examines all ClassDef nodes in the AST. A ClassDef node has 5 children: class name, list of base-class expressions, and three nodes not used for this analysis. The following rules can be used to find all defined class names and build a class extension relation using base-class expressions that are Name nodes. A Name node has two children: name and context. For a dynamic language like Python, analysis involving names can be refined in many ways to give more precise results, e.g., [GLS + 10]. We do not do those here, but Datalog rules are particularly good for such analysis of bindings and aliases for names, e.g., [SB15].
Part 2: Characterizing results and finding special cases. This computes statistics for defined classes and the class extension relation and finds root classes (class with subclass but not super class). These use aggregate queries and set queries. Similar queries can compute many other statistics and cases: maximum number of classes that any class extends, leaf classes, histograms, etc.
Part 3: Analysis of special cases. This computes the maximum height of the extension relation, which is the maximum height of the root classes, and finds root classes of the maximum height. These use a recursive function as well as aggregate and set queries. For efficiency, when a subclass can extend multiple base classes, caching of results of function calls is used. In Python, one can simply add import functools to import module functools, and add @functools.cache just above the definition of height to cache the results of that function.
Part 4: Further analysis of special cases. This computes the maximum number of descendant classes following the extension relation from a root class, and finds root classes of the maximum number. Recursive functions and aggregate queries similar to finding maximum height do not suffice here, due to shared subclasses that may be at any depth. Instead, the following rules can infer all desc(c,r) facts where class c is a descendant following the extension relation from root class r, and aggregate and set queries with function num desc then compute the desired results. For efficiency of the last query, caching is also used for function num desc. If the last query is omitted, function num desc can also be inlined in the max desc query. Benchmark PAopt is the same as PA except that in rule set desc rs, the two hypotheses in the second rule are reversed; this allows default indexing in XSB, which is on the first argument, to find matching values c, b, r faster in that order.
Comparing with aggregate queries and functions in rule languages. While rules in Alda correspond directly to rules in rule languages, expressing aggregate queries and functions using rules require translations that formulate computations as hypotheses and introduce additional variables to relate these hypotheses.
Aggregate queries are used extensively in database and machine learning applications, and are essential for analyzing large data or uncertain information. While these queries are easy to express directly in database languages and scripting languages, they are less so in rule languages like Prolog; most rule languages also do not support general aggregation with recursion due to their subtle semantics [LS22]. For example, the simple query num defined = count(defined) in Alda, if written in XSB, would become: Recursive functions are used extensively in list and tree processing and in solving divideand-conquer problems. They are natural for computing certain information about parse trees, nested scopes, etc. However, in rule languages, they are expressed in a way that mixes function arguments and return values, and require sophisticated mode analysis to differentiate arguments from returns. For example, the height query, if written in XSB, would become:

Experimental evaluation
We have implemented a prototype compiler for Alda. It generates executable code in Python. The generated code calls the XSB logic rule engine [SW12, SWS + 21] for inference using rules. We implemented Alda by extending the DistAlgo compiler [LSLG12, LSL17, LL22]. Dis-tAlgo is an extension of Python with high-level set queries as well as distributed processes. The compiler is implemented in Python 3, and uses the Python parser. So Python syntax is used in place of the ideal syntax presented in Section 3.
The Alda implementation extends the DistAlgo compiler to support rule-set definitions, function infer, and updates to non-local variables. It handles only direct updates to variables used as predicates, not updates through aliasing; a previous alias analysis [GLS + 10] that took several years to develop was only for Python 2. Currently Datalog rules extended with negation are supported, but extensions for more general rules can be handled similarly, and inference using XSB can remain the same. Calls to infer are automatically added at updates to non-local base predicates of a rule set.
In particular, the following Python syntax is used for rule sets, where a rule can be either one of the two forms below, so the only restriction is that the name rules is reserved. is added for automatic tabling in XSB.
For function infer, the implementation translates the values of predicates and the list of queries into facts and queries in standard Prolog syntax, and translates the query answers back to values of set variables. It invokes XSB using a command line in between, passing data through files; this external interface has an obvious overhead, but it has not affected Alda having generally good performance. infer automatically reads and writes non-local predicates used in a rule set.
Note that the overhead of the external interface can be removed with an in-memory interface from Python to XSB, which is actively being developed by the XSB team 3 . However, even with the overhead of the external interface, Alda is still faster or even drastically faster than half or more of the rule engines tested in OpenRuleBench [LFWK09] for all benchmarks measured except DBLP (even though OpenRuleBench uses the fastest manually optimized program for each problem for each rule engine), and than not using rules at all (without manually writing or adapting a drastically more complex, specialized algorithm implementation for each problem).
Building on top of DistAlgo and XSB, the compiler consists of about 1100 lines of Python and about 50 lines of XSB. This is owing critically to the overall framework and comprehensive functions, especially support for high-level queries, already in the DistAlgo compiler and to the powerful search and inference engine of XSB. The parser for the rule extension is about 270 lines, and update analysis and code generation for rules and inference are about 800 lines.
The current compiler does not perform further optimizations, because they are orthogonal to the focus of this paper, and our experiments already showed generally good performance. Further optimizations can be implemented in either the Alda compiler to generate optimized rules and tabling and indexing directives, or in XSB. Incremental maintenance under updates can also be implemented in either one, with a richer interface between the two.
We discuss our experiments on the benchmarks described in Section 5. The experiments selected are meant to show acceptable performance even under the most extreme overhead penalties we have encountered. Our extensive experiments with other uses of Alda have experienced minimum such penalties.
All measurements were taken on a machine with an Intel Xeon X5690 3.47 GHz CPU, 94 GB RAM, running 64-bit Ubuntu 16.04.7, Python 3.9.9, and XSB 4.0.0. For each experiment, the reported running times are CPU times averaged over 10 runs. Garbage collection in Python was disabled for smoother running times. Program sizes are numbers of lines excluding comments and empty lines. Data sizes are number of facts. Table 4 shows the compilation times and program sizes before and after compilation, for all three sets of benchmarks described in Section 5, plus three variants of TC in the first set as  For OpenRuleBench benchmarks, Alda program sizes are all smaller than the corresponding XSB sizes, almost all by dozens or even hundreds of lines, and by an order of magnitude for Join1 and TC. In place of the extra XSB code for benchmarking and manually added optimization directives, all Alda programs use the single shared 45-line file, ORBtimer, for benchmarking and pickling. The generated XSB size is exactly the number of rules plus one line for :-auto table., for each rule set. The generated Python size for benchmarks from OpenRuleBench is larger for benchmarks with more queries.

Compilation times and program sizes
For RBAC benchmarks, all Alda sizes include 3 files of 373 lines total for all 9 RBAC classes, taking a total compilation time of 299.503 milliseconds, generating a total compiled Python size of 456 lines. Each way of computing the query functions is in a separate class extending Hierarchical RBAC; RBACnonloc is over 30 lines more than others because all query functions in Hierarchical RBAC, not just authorizedUsers(role), are overridden to use field transRH in place of calls to transRH().
For PA benchmarks, the benchmarking code for the XSB programs is written in a similar way as the benchmarks in OpenRuleBench, and takes 23 lines for each benchmark.

Performance of classical queries using rules
To evaluate the efficiency of classical queries using rules in Alda, we use four programs for computing transitive closure: (1) TC-the TC benchmark from OpenRuleBench, which is the same as trans rs except with renamed predicates, (2) TCrev-same as trans rs but reversing the two predicate names in the recursive rule, a well-known variant, (3) TCdawhile loops with high-level queries in DistAlgo as in Section 3.3, and (4) TCpy-while loops with comprehensions in Python as in Section 2.
For comparison, we also directly run the XSB program for TC from OpenRuleBench, and its corresponding version for TCrev, except we change load dyn used in OpenRuleBench to load dync, for much faster reading of facts in XSB's canonical form; we call these two programs TCXSB and TCrevXSB, respectively. Note that XSB programs in OpenRuleBench, not using load dync, are significantly slower for large input, e.g., see the DBLP benchmark in Section 6.5.
We use the data generator in OpenRuleBench to generate data. The generator is sophisticated in trying to ensure randomness as well as cyclic vs. acyclic cases. We use the same number of vertices, 1000, and a range of numbers of edges, 10K to 100K, to include the first of two data points (50K and 500K edges) reported in [LFWK09]. For the cyclic graphs generated, even for the smallest data of 10K edges, i.e., each vertex having edges going to only 1% of vertices-10 out of 1000-on average, the resulting transitive closure is already the complete graph of 1M edges. Because of interfacing with XSB through files, the total running time of Alda programs includes not only (1) reading data, (2) executing queries, and (3) returning results, but also (2pre) preparing data, queries, and commands and writing data to files for XSB to start and read before (2), and (2post) reading results from files written by XSB after (2). We report the total running time as well as separate times for pickling and for interfacing with XSB. Figure 2 shows the running times of the TC benchamrks. RawR and PickleW are times for reading facts in XSB/Prolog form as used in OpenRuleBench and writing them in pickled form for use in Alda, respectively. Pickling is done only once; the pickled data is read in all repeated runs and all of TC, TCrev, TCda, and TCpy. TC extra and TCrev extra are the part of TC and TCrev, respectively, for extra work interfacing with XSB, i.e., for 2pre and 2post and for XSB to read data (xsbRdata) and write results (xsbWres). Figure 3 shows the breakdown of TC extra and TCrev extra among 2pre, 2post, xsbRdata, and xsbWres.
Not shown in Figure 2, but the times in TC and TCrev for reading facts are similar to PickleW; the times in TCXSB and TCrevXSB for reading facts are similar to RawR. The remaining times in TCXSB and TCrevXSB are XSB query times only, because XSB programs in OpenRuleBench do not output or even collect the query result in any way.
Also not shown in Figures 2 and 3 are the times in TC and TCrev for starting the XSB process and, as part of 2pre, for preparing the queries to start XSB with proper arguments and status checks. These are small, at 0.1-0.2 seconds and 0.03-0.04 seconds, respectively, because XSB is invoked only once in each run.
We observe that: • The extra times interfacing with XSB are obvious, here dominated by passing query results by files (xsbWres and 2post) as shown in Figure 3 because the results of transitive closure are generally large, and larger for cyclic graphs. This overhead of going through files will be removed when using direct mapping between XSB and Python data structures in memory.
• The remaining times without the interfacing overhead are basically all XSB query times for the different XSB programs. In particular, TCrev is faster than TC in Alda, but TCXSB is faster than TCrevXSB. This is because OpenRuleBench uses the fastest manually optimized program for each problem, which is TCXSB with subsumptive tabling for this specific benchmark, while Alda-generated XSB programs use auto table, which is variant tabling, and these are known to cause the observed performance differences [TL10,TL11]. Alda compiler can be extended to automatically generate optimal programs using previously studied methods [LS09, THL08, TL10, TL11].
• TCpy and TCda, while being drastically easier to write than low-level code despite not as easy as rules, are exceedingly inefficient. In contrast, TC and TCrev that use rules are drastically faster.
Note that both TC and TCrev in Alda, even including the extra times interfacing with XSB, are faster or even drastically faster than all systems reported in OpenRuleBench [LFWK09] except for XSB and 1 or 2 other systems (for 50K edges, 17 seconds for TC in Alda vs. up to 184 seconds and even an error on cyclic data, and 8 seconds for TC in Alda vs. up to 120 seconds on acyclic data, where the XSB query times were similar as in our measurements; note that these are despite OpenRuleBench reporting only the times for queries, not reading data or writing results). This is despite all those programs having been manually optimized in the most advantageous and beneficial ways for each system [LFWK09].  Figure 4: Running times of RBAC benchmarks, for a workload of updates and queries over 5000 users, 500 roles, 5500 user-role assignments, and 550 role hierarchy pairs. Running times for RBACpy and RBACda are not shown because they are much larger and above all times shown: on data point 50, they are 688.677 and 384.623 seconds, respectively, but they increased linearly as expected, to 1381.543 seconds on data point 100 and to 1517.510 seconds on data point 200, respectively, and failed to complete by the time limit of 30 minutes on larger data points.

Integrating with objects, updates, and set queries
To evaluate the performance of using rules with objects and updates, and of different ways of using rules as well as not using rules, we use the RBAC benchmarks in Section 5.2.
We create 5000 users and 500 roles, and randomly generate a user-role assignment UR of size 5500 with a maximum of 10 roles per user, and a role hierarchy RH of size 550 and height 5. We run the following set of workloads: iterate and randomly do one of the following operations in each iteration: add/delete user (50 total each of add and delete), add/delete role (5 total each), add/delete UR pair (55 total each), add/delete RH pair (5 total each), and query authorized users (n total), for n up to 500 at intervals of 50. We measure the running time of the workload for each n. Figure 4 shows the running times of the RBAC benchmarks, all scaling linearly in the number of AuthorizedUsers queries, as expected. Labels with suffix extra indicate the part of the running time of the corresponding program for extra work interacing with XSB: 2pre, 2post, xsbRdata, and xsbWres as in Figure 3 plus here the times for starting XSB processes. Figure 5 shows the breakdown of the times for the extra work. It also highlights the part of 2pre on preparing the queries and commands to start XSB (2pre prepStart), with the rest of 2pre (2pre rest) on writing data to files for XSB to read. xsbStart is the time for starting the XSB process.
We observe that: • The extra times interfacing with XSB are again obvious, but here dominated mostly by preparing queries and commands and starting XSB, as shown in Figure 5, unlike for TC benchmarks, because the data and results are much smaller but all the work associated with invoking XSB through command line is repeated n (50 to 500) times. This overhead from going through files will also be removed when using an in-memory interface between XSB and Python without starting XSB repeatedly.
• RBACallloc and RBACunion are very close, as shown in Figure 4, with a slightly higher interfacing overhead by RBACallloc as expected for the extra data and results passed due to ROLES, but compensated by a slightly faster queries in XSB than set operations in Python. RBACnonloc is more than 3 times as fast as RBACallloc and RBACunion and is the fastest, because the inference for computing transRH is done at updates not queries, and there is a smaller, fixed number of updates. Its performance can be optimized even more with incremental computation, as for either set queries, e.g., [LWG + 06, GLSR12,LBSL16], or logic rules, e.g., [SR03].
• RBACpy and RBACda are again exceedingly inefficient, as expected. In contrast, the three programs that use rules are all significantly faster.

Integrating with aggregate queries and recursive functions
To evaluate the performance of integrated use of rules with aggregate queries and recursive functions, we use two benchmarks for class hierarchy analysis: PA and PAopt, and the corresponding programs in XSB, as described in Section 5.3. We found the XSB programs corresponding to PA and PAopt, which we call PAXSB and PAoptXSB, respectively, to be highly inefficient, being slower and even drastically slower than Alda programs. We tried many manual optimizations by manipulating the rules and  Table 5: Data sizes, analysis results, and running times for program analysis benchmarks. Total is the total number of facts about each package. Total used is the sum of numbers of ClassDef, Name, and Member facts.
adding directives, including with help from an XSB expert, and selected the best version, which we call PAXSBopt, that uses additional directives for targeted tabling that also subsumes some indexing. The programs analyzed include 9 widely-used open-source Python packages, all available on GitHub (https://github.com/): NumPy (v1.21.5) and SciPy (v1.7.3), for scientific computation; MatPlotLib (matplot) (v3.5.1), for visualization; Pandas (v1.3.5), for data analysis; SymPy (sympy-1.9), for symbolic computation; Django (4.0), for web development; Scikit-learn (sklearn) (1.0.1) and PyTorch (v1.10.1), for machine learning; and Blender (v3.0.0), for 3D graphics. Each of these Python packages contains many files and directories. We first parse each file and translate the resulting abstract syntax tree (AST) along with file and directory information into Datalog facts. We then run the benchmarks. Table 5 shows data sizes, analysis results, and running times of the analysis. The columns are sorted by the total number of facts used (i.e., all ClassDef, Name, and Member facts), which mostly coincides with the total number of facts except for the largest two, pytorch and sympy. We can see that, even for the small rule set class extends rs, the total number of facts used is already 38.7-42.6% of the total number of facts, because Member and Name are two of the largest.  Figure 6 shows the breakdown of the time interfaceing with XSB (2pre, 2post, and xsbRdata, as in Figure 3 except that xsbWres is even smaller than 2post and is not shown) plus the remaining time in the total time for PA and PAopt (total rest). We can see that: • The times interfacing with XSB is again obvious, here vastly dominated by the time to pass AST facts to XSB as shown in Figure 6, because of the large data sizes vs. the small result sizes shown in Table 5. This contrasts the times dominated by passing results in Figure 3 and by repeated starting of XSB in Figure 5; XSB is invoked only twice here in each run, once for each rule set in the benchmark. Again, this overhead will be removed by in-memory mapping between XSB and Python data structures.
• The running times of PA and PAopt are similar and mostly increase as the data sizes increase, as shown in Table 5 and Figure 6. PAopt is in most cases (all but numpy and pytorch) very slightly faster than PA, because querying using rules takes only a small part of the total time (0.5-11.6% of PA, and 0.3-2.5% of PAopt), with the rest on interfacing with XSB and on other queries using functions and aggregations. Querying using rules in PAopt is actually 1.4-11.8 times as fast as that in PA.
• PAXSB and PAoptXSB have vastly varying running times, as shown in Table 5, unlike PA and PAopt, and are much slower than PA and PAopt, taking 1.9-121.1 times as long as PA and longer than PAopt. This is after we already manually added tabling for height and num desc to match PA and PAopt, after finding that auto table only tabled predicate desc. We can see that PAXSB and PAoptXSB are mostly slower for larger result sizes, as opposed to input sizes, though all result sizes are orders of magnitude smaller than input sizes.
PAXSBopt, with manual optimizations after trying various combinations of tabling, indexing, and rewriting for the remaining predicates, is 58.0-78.4% faster that PA.  Again, previously studied methods [TL10,TL11] can be added to the Alda compiler to automatically generate optimal tabling and indexing directives as needed; manually applying these sophisticated methods is too tedious.

Scaling with data and rules
We examine how the performance scales for large sizes of data and rules using two benchmarks in OpenRuleBench: DBLP, the last under large join tests, with the largest real-world data set among all benchmarks in OpenRuleBench; and Wine, the last under Datalog recursion, with the largest rule set among all benchmarks in OpenRuleBench. Again, we changed load dyn used in OpenRuleBench to load dync for faster reading of facts in XSB's canonical form.
The DBLP benchmark does a 5-way join with projections, on DBLP data containing 2.4+ million facts. The Wine benchmark in OpenRuleBench has 961 rules and 654 facts; it was originally too slow in XSB but optimized using subsumptive transformations [TL11], resulting in 967 rules. The Wine benchmark in Alda is translated from the optimized rules. Table 6 shows the running times for DBLP and Wine benchmarks, for both the Alda programs and the XSB programs. extra under Alda is the part of the total time on 2pre, 2post, xsbRdata, and xsbWres. OrigTotal under XSB is the Total time for the original program from OpenRuleBench, which uses load dyn instead of load dync. We note that: • For the DBLP benchmark, XSB is more than 3 times as fast as Alda. The large data size causes 2pre and xsbRdata to dominate the interfacing overhead, as for PA and PAopt benchmarks. Again, this overhead of going through files will be removed with an in-memory interface.
Alda's reading of raw data (12.187 seconds) is higher than XSB's for DBLP due to the use of Python regular expressions to parse extra string formats while XSB benefits from drastically reduced checks reading their canonical data form. This can be fixed with a specialized reading function in C similar to the one used by XSB.
Note that the original XSB benchmark from OpenRuleBench (63.494 seconds), without our optimization to use faster data loading, is much slower than even Alda (30.573 seconds) that includes the extra overhead. Because OpenRuleBench reports only query time, which is small (about 1.3 second using XSB from Alda) compared with reading large data for DBLP, our total time that includes reading data and writing results is larger than all times reported in OpenRuleBench [LFWK09], but XSB was the second fastest there, and one system produced an error.
• For the Wine benchmark, XSB is more than 8 times as fast as Alda, due to the use of auto table in Alda generated code. Manually added subsumptive tabling in the XSB benchmark from OpenRuleBench reduces the XSB query time from 27.722 seconds to 3.747 seconds. Again the Alda compiler can be extended with automatic optimizations [LS09,TL10,TL11].
Note that Alda is still faster than half of the systems tested in OpenRuleBench, up to 140 seconds and even errors in three systems, where XSB was the fastest at 4.47 seconds [LFWK09].

Related work and conclusion
There has been extensive effort in the design and implementation of languages to support programming with logic rules together with other programming paradigms, by extending logic languages, extending languages in other paradigms, or developing multi-paradigm or other standalone languages.
A large variety of logic rule languages have been extended to support sets, functions, updates, and/or objects, etc. For example, see Maier et al. [MTKW18] for Datalog and variants extended with sets, functions, objects, updates, higher-order extensions, and more. In particular, many Prolog variants support sets, functions, updates, objects, constraints, etc. For example, Prolog supports assert for updates, as well as cut and negation as failure that are imperative instead of declarative; Flora [YK00, KYWZ20] builds on XSB and supports objects (F-logic), higher-order programming (HiLog), and updates (Transaction Logic); and Picat [Zho16] builds on B-Prolog and supports updates, comprehensions, etc. Lambda Prolog [MN12] extends Prolog with simply typed lambda terms and higher-order programming. Functional logic languages, such as Mercury [SHC95] and Curry [Han13], combine functional programming and logic programming. Some logic programming systems are driven by scripting externally, e.g., using Lua for IDP [BBB + 14], and shell scripts for LogicBlox [AtCG + 15]. Flix [MYL16,ML20] extends Datalog with lattices and monotone functions, and functional programming. These languages are intrinsically driven by logic rules or functional programming, and do not support commonly-used updates, objects, and sets in a simple and direct way as in a general powerful language like Alda, or do not support some or all of them at all.
Many languages in other programming paradigms, especially imperative languages and object-oriented languages, have been extended to support rules. This is notably through explicit interfaces with particular logic languages, for examples, a Java interface for XSB through InterProlog [Cal04, SWS + 21], C++ and Python interfaces for answer-set programming systems dlvhex [Red16] and Potassco [BKOS17], and a Python interface for IDP [Ven17]. While imperative and object-oriented languages support easy and direct updates and object encapsulation, interfacing with logic languages through explicit interfaces requires programmers to write tedious, low-level wrapper code for going to the rule language and coming back. They are in the same spirit as interfaces such as JDBC [Ree00] for using database systems from languages such as Java.
Multi-paradigm languages and other standalone languages have also been developed. For example, the Mozart system for the Oz multi-paradigm programming language [RH04] supports logic, functional, and constraint as well as imperative and concurrent programming. However, it is similar to logic languages extended with other features, because it supports logic variables, but not state variables to be assigned to as in commonly-used imperative languages. Examples of other languages involving logic and constraints with updates include TLA+ [Lam94], a logic language for specifying actions; CLAIRE [CJL02], an object-oriented language that supports functions, sets, and rules whose conclusions are actions; LINQ [MBB06,LIN22], an extension of C# for SQL-like queries; IceDust [HGV16], a Java-based language for querying data with path-based navigation and incremental computation; extended LogiQL in SolverBlox [BSKPA18], for mathematical and logic programming on top of Datalog with updates and constraints; and other logic-based query languages, e.g., Datomic [AGH + 16] and SOUL [DRNKJ11]. These are either logic languages lacking general imperative and objected-oriented programming constructs, or imperative and objectoriented languages lacking the power of logic rules.
In conclusion, Alda allows the use of logic rules with all of sets, functions, updates, and objects in a seamlessly integrated fashion. As a direction for future work, many optimizations can be used to improve the efficiency of implementations. This includes optimizing the logic rule engines used, improving interfaces and interactions with them, and using different and specialized rule engines such as Souffle [JSS16] to obtain the best possible performance. rule sets whose results need to be maintained, extended several existing transition rules to perform automatic maintenance of the results of rule sets, and modified the semantics of existential quantifiers to bind the quantified variables to a witness when one exists.

A.1 Abstract syntax
The abstract syntax is defined in Figures 7 and 8. Tuples are treated as immutable values, not as mutable objects. 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, any (which returns an element of the set, if the set is non-empty, otherwise it returns None), contains, del, and size. Methods of sequence include add (which adds an element at the end of the sequence), contains, and length. For brevity, among the standard arithmetic operations, we include only one representative operation in the abstract syntax and semantics; others are handled similarly. All expressions are side-effect free. Object creation, comprehension, and infer are statements, not expressions, because they have side-effects; comprehension has the side-effect of creating a new set. 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. whileSome and ifSome are similar to while and if, except that they always have an existential quantification as their condition, and they bind the variables in the pattern in the quantification to a witness, if one exists. The literal None is used to represent "undefined". We use some syntactic sugar in sample code, e.g., we use infix notation for some binary operators, such as and and is.
Notation in the grammar. A symbol in the grammar is a terminal symbol if it is in typewriter font. A symbol in the grammar is a non-terminal symbol if it is in italics. In each production, alternatives are separated by a linebreak. Square brackets enclose optional clauses. * after a non-terminal means "0 or more occurrences". + after a non-terminal means "1 or more occurrences". t θ denotes the result of applying substitution θ to t. We represent substitutions as (partial) functions from parameters and variables to expressions.
Well-formedness requirements on programs. In rule sets, parameters can be used only as base predicates, not derived predicates. In rule sets defined in global scope, predicates cannot have the form self.Field . In every rule in every rule set, every logic variable that appears in the conclusion must appear in a hypothesis.
Each global variable can appear as a derived predicate in at most one rule set in the program. Each instance variable can appear as a derived predicate in at most one rule set in each class.
Invocations of methods defined using def appear only in method call statements. Invocations of methods defined using defun appear only in method call expressions; we also refer to these methods as "functions".   Global variables. Global variables are replaced with instance variables by replacing each global variable x with a gv .x, where a gv is the address of an object whose fields are used to represent global variables.
Non-variable expressions in tuple patterns. Non-variable expressions in tuple patterns are replaced with variables prefixed by "=". Specifically, for each expression e in a tuple pattern that is not a variable (possibly prefixed with "=") or wildcard, an assignment v := e to a fresh variable v is inserted before the statement containing the tuple pattern, and e is replaced with =v in the tuple pattern.
Wildcards. Wildcards are eliminated from tuple patterns in for loops and comprehensions (i.e., everywhere except queries) by replacing each wildcard with a fresh variable.
Tuple patterns in infer statements. infer statements are transformed to eliminate tuple patterns in queries. After transformation, each query is simply the name of a predicate. Consider the statement x 1 , . . . , x n := [e.]infer(p 1 (pat 1 ), . . . , p n (pat n ), kwargs, rules=r). Let x i,1 , . . . , x i,k i be the components of pat i that are variables not prefixed by "=". Let y 1 , . . . , y n be fresh variables. The above statement is transformed to: if b ′ and not foundOne: whileSome statements. whileSome is statically eliminated as follows. Consider the statement whileSome pat in e | b : s. Using the same definitions as in the previous item, this statement is transformed to: foundOne := True while foundOne: foundOne := False for pat ′ in e: if b' and not foundOne: Tuple patterns in iterators. Iterators containing tuple patterns are rewritten as iterators without tuple patterns. 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 (ē j 1 , . . .,ē jm ) and b θ.
Consider the loop for (e 1 , . . . , e n ) in e : s. Let x and S be fresh variables. Let {i 1 , . . . , i k } contain the indices in (e 1 , . . . , e n ) of variables not prefixed with "=". Let {j 1 , . . . , j m } contain the indices in (e 1 , . . . , e n ) of the constants and the variables prefixed with "=". Letē j denote e j after removing the "=" prefix, if any. Note that e may denote a set or sequence, and duplicate bindings for the tuple of variables (e i 1 , . . . , e i k ) are filtered out if e is a set but not if e is a sequence. The loop is rewritten as the code in Figure 9.

A.2 Semantic domains
The semantic domains are defined in Figure 10  D. D 1 ⇀ D 2 contains partial functions from D 1 to D 2 . dom(f ) and range(f ) are the domain and range, respectively, of a partial function f . Consider a state (s, ht, h, stk ). s is the statement to be executed. ht is the heap type map; ht(a) is the type of the object on the heap at address a. h is the heap; it maps addresses to objects. stk is a kind of call stack: an entry is pushed on the stack when a method is called, and popped when a method returns. However, entries on the stack do not contain bindings of method parameters to arguments (such bindings are unnecessary, because the transition rule for calls to user-defined methods substitutes the arguments into an inlined copy of the method body). Instead, each entry contains sets of rules whose results should be automatically maintained during the method call; specifically, it contains the sets of rules defined in the class of the target object o of the method call, instantiated by replacing self with o. The stack is initialized with an entry containing sets of rules defined in global scope. That entry is never popped. This ensures that global rule sets are always maintained.

A.3 Extended abstract syntax
Section A.1 defines the abstract syntax of programs that can be written by the user. We extend the abstract syntax to include additional forms into which programs may evolve

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 sub-expression or sub-statement to be evaluated. Evaluation contexts are defined in Figure 11. Note that square brackets enclosing a clause indicate that the clause is optional; this is unrelated to the notation [ ] for the hole. For example, the definition of evaluation contexts for method calls implies that the expression denoting the target object is evaluated first to obtain an address (if the expression isn't already an address); then, the arguments are evaluated from left to right. The left-to-right order holds because an argument can be evaluated only if the arguments to its left are values, as opposed to more complicated unevaluated expressions. The definition of evaluation contexts for infer implies that the expressions for the targets of the assignment are evaluated from left to right, then the expression for the target object, if any (i.e., if the call is for a rule set with class scope), is evaluated, and then the argument expressions are evaluated from left to right.

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 ∈ Heap. The transition relation for statements has the form σ → σ ′ where σ ∈ State and σ ′ ∈ State. Both transition relations, and some of the auxiliary functions defined below, are implicitly parameterized by the program, which is needed to look up method definitions, rule set definitions, etc. The transition relation for expressions is defined in Figure 13. The transition relation for statements is defined in Figures 14-15. The context rules for expressions and statements allow the expression or statement in the evaluation context's hole to take a transition, while its context C (i.e., the rest of the program) is carried along unchanged.
Notation. In the transition rules, a matches an address, and v matches a value (i.e., an element of Val ).
For an expression or statement e, e[x := y] denotes e with all occurrences of x replaced with y.
A function f matches the pattern f [x → y] iff f (x) equals y. 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. For functions f and g with disjoint domains, f ∪ g is their union. For functions f and g with possibly overlapping domains, f ⊔ g is like union but with g having precedence, i.e., Sequences are denoted with angle brackets, e.g., 0, 1, 2 ∈ Int * . 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.
freshAddrs(h, f ) holds if function f maps its domain to distinct fresh addresses, i.e., addresses not used in heap h: extends(c 1 , c 2 ) holds iff class c 1 is a descendant of class c 2 in the program's inheritance hierarchy.
methodDef (c, m, def ) holds iff (1) class c defines method m, and def is the definition of m in c, or (2) c does not define m, and def is the definition of m in the nearest ancestor of c in the inheritance hierarchy that defines m.
glblRulesets is the set of rule sets defined in global scope in the program. For a rule set name r in glblRulesets, rules(r) is the set of rules in rule set r defined in global scope. rulesets(c) is the set of rule sets defined in class c in the program. For a rule set name r in rulesets(c), rules(c, r) is the set of rules in rule set r defined in class c.
For a set of rules R, basePredParams(R) is the set of base predicates in R that are parameters. basePredVars(R) is the set of base predicates in R that are variables. derivedPredVars(R) is the set of derived predicates in R that are variables, and derivedPredParams(R) is the set of derived predicates in rules in R that are parameters. For a sequence of sets of rules stk , derivedPredVars(stk ) is the set of derived predicates that are variables in any rule in stk . A variable's value is "known", for purpose of rule evaluation, if its value is a set, so we define knownVars(vars, ht, h) = {a.f ∈ vars | f ∈ dom(h(a)) ∧ h(a)(f ) ∈ Address ∧ ht(h(a)(f )) = set}. For c ∈ ClassName, derivedPredVars(c) is the set of derived predicates that are variables in any rule set defined in class c. glblDerivedPredVars is the set of global variables that are derived predicates in any rule set in the program.
The relation maintain(θ, θ T , ht, h, stk ) defined in Figure 12 holds if, given heap type map ht, heap h, and stack stk (i.e., the last component of the state, described in Section A.2), θ and θ T are substitutions to apply to h and ht, respectively, to express the result of automatic maintenance of the rule sets in stk . It is defined using the function maintSub(ht, h, stk , newFn) which computes a substitution expressing the heap updates to be performed. Its last argument newFn provides fresh addresses for sets created as a result of maintenance. It takes newFn as an argument, instead of non-deterministically choosing fresh addresses itself, so it can be defined as a function, rather than a relation, which would be less intuitive. To be safe, newFn specifies fresh addresses to use (if needed) for all derived predicates; no harm is done if some go unused (e.g., if some derived predicates remain undefined because they depend on base predicates with invalid values). The function π 1 returns the first component of a tuple.
maintSub calls a function infSub to compute the result of inference for a single rule set, and uses function reduce to iterate infSub over the stack stk . reduce(f, s, v 0 ) iterates over sequence s, accumulating a result by recursively applying the two-argument function f , using v 0 as the initial value; it is the same as in the Python functools library. infSub's arguments include a binding args of parameters to values (for automatic maintenance, args is the empty function). infSub returns a pair containing the substitution to apply to the heap h and a function result that maps each defined derived predicate (i.e., each derived predicate that depends only on base predicates whose values are sets) in the rule set to its value. infSub uses the following auxiliary functions. slice(R, knownBasePreds) returns a subset of the given set of rules R obtained by first identifying the set of derived predicates that depend only on base predicates in knownBasePreds, and then returning only the rules on which those derived predicates depend. evalRules(R) evaluates the given set of rules R and returns a function from the set of predicates that appear in the rules to their meanings, represented as sets of tuples. updateVar(ht, h, newFn, a.f, S), defined in Figure 12, returns a substitution that updates variable a.f to refer to a set with content S. If a.f already contains an address, the object at that address is changed to be a set with content S, otherwise a.f is assigned a fresh address which is updated to contain a set with content S. legalAssign(ht, a, f ) holds if assigning to field f of the object with address a is legal, in the sense that a refers to an object with fields (not an instance of a built-in class without fields), and a.f is not a derived predicate of any rule set. legalAssign(ht, a, f ) = ht(a) ∈ {set, sequence} ∧ ((a = a gv ∧ a gv .f ∈ glblDerivedPredVars ∨ (a = a gv ∧ self.f ∈ derivedPredVars(ht(a))).
Notes. Transition rules for methods of the pre-defined classes set and sequence are similar in style, so only one representative example is given, for set.add. Note that maintain needs to be called only in transition rules for methods of set that update the content of the set.
Informally, the transition rule for invoking a method in a user-defined class inlines a copy of the method body s that has been instantiated by substituting argument values for parameters, appends a return statement to the method body to mark the end of the method call, updates the stack stk by appending instantiated copies of the rule sets defined in class ht(a) (i.e., the class of the target object of the method call), uses the auxiliary relation maintain to identify substitutions θ T and θ that reflect the effect of automatic maintenance of rule sets on the heap type map and heap, and applies those substitutions to obtain the updated heap type map and heap.
The transition rule for an explicit call to infer on a rule set with class scope instantiates the named rule set r using the given values for the rule set's parameters, uses auxiliary function infSub to evaluate the instantiated rule set, and uses maintain to determine the effects of automatic maintenance. To understand the rule, note that newFn provides fresh addresses for sets created as a result of this call to infer; args captures the values of keyword arguments; R is the set of rules in r with self instantiated with the target object a; θ is a substitution to apply to the heap to update the values of derived predicates of r that are variables; result maps each defined derived predicate of R (i.e., each derived predicate of R that depends only on base predicates whose values are sets) to its value; definedParams and undefParams are the sets of derived predicates of r that are parameters and whose values are defined and undefined (i.e., None), respectively, as a result of this call to infer; θ Q and θ Qundef are substitutions to apply to the heap to update the values of a 1 .f 1 , . . . , a n .f n ; and θ T is a substitution to apply to the heap type map ht to update the types of heap locations containing sets created by this call to infer.
A rule set that defines a global variable as a derived predicate that depends on a predicate of the form self.Field has two notable effects. First, when there are nested calls on different target objects that are instances of a class c that defines such a rule set, the substitutions generated by infSub from the stack entries for those nested calls have overlapping domains; this is why maintSub combines them using ⊔ instead of ∪. In particular, they are combined so that the innermost call (topmost stack frame) takes precedence. Second, when the stack does not contain any call to a method of c, the global variable is set to None. Thus, returning from a method call can have the side-effect of changing the value of a global variable to None.
Automatic maintenance has the effect of evaluating all rule sets using values of their base predicates in the current state, and then updating their derived predicates in the next state, like a single "parallel assignment" statement, even if there are dependencies between rule sets. For example, suppose there are rule sets r 1 and r 2 in global scope, and some derived predicate x of r 1 is also a base predicate of r 2 . In a transition from state σ 1 to state σ 2 , r 2 is evaluated using the value of x in σ 1 , not its value in σ 2 .
Informally, execution of a program may eventually (1) terminate (i.e., the statement in the first component of the state becomes skip, indicating that there is nothing left for the process to do), (2) get stuck (i.e., the statement is not skip, and the process has no enabled transitions, due to an error), or (3) run forever due to an infinite loop or infinite recursion. Examples of errors that cause a program to get stuck are trying to select a component from a value that is not a tuple, and trying to access a non-existent field of an object. For brevity and simplicity, we designed the semantics so that errors simply halt execution; the semantics could easily be extended to indicate exactly what error occurred.