arXiv is now an independent nonprofit! Learn more
License: arXiv.org perpetual non-exclusive license
arXiv:2305.19202v1 [cs.PL] 30 May 2023
\jdate

May 2023 \pagerangeIntegrating Logic Rules with Everything Else, Seamlessly \submittedFebruary 2023

Integrating Logic Rules with Everything Else, Seamlessly

YANHONG A. LIU   SCOTT D. STOLLER   YI TONG   BO LIN
Stony Brook University
   Stony Brook    NY 11794    USA Email: {liu,stoller,yittong,bolin}@cs.stonybrook.edu
2023; Revised  April 2023; Accepted  May 2023
Abstract

This paper presents a language, Alda, that supports all of logic rules, sets, functions, updates, and objects as seamlessly integrated built-ins. The key idea is to support predicates in rules as set-valued variables that can be used and updated in any scope, and support queries using rules as either explicit or implicit automatic calls to an inference function. We have defined a formal semantics of the language, implemented a prototype compiler that builds on an object-oriented language that supports concurrent and distributed programming and on an efficient logic rule system, and successfully used the language and implementation on benchmarks and problems from a wide variety of application domains. We describe the compilation method and results of experimental evaluation.

keywords
language design and implementation, logic rules, sets, comprehension, aggregation, quantification, functions, updates, objects, concurrent and distributed

1 Introduction

Logic rules are powerful for expressing complex reasoning and analysis problems, especially in critical areas such as program analysis, decision support, networking, and security [69, 35]. However, developing application programs that use logic rules remains challenging:

  • Powerful logic languages and systems support succinct use of logic rules for complex reasoning and analysis, but not as directly or conveniently for many other aspects of applications—e.g., data aggregation, numerical computation, input/output, modular construction, and concurrency—that are more easily expressed using set queries, functions, state updates, and object encapsulation [48].

  • At the same time, commonly-used languages for building applications support many powerful features but not logic rules, and to use a logic rule system, tedious and error-prone interface code is required—to pass rules and data to the rule system, invoke operations of the rule system for answering queries, and pass the results back—manually solving an impedance mismatch, similarly as in interfaces with relational databases [19], making logic rules harder to use than necessary.

What’s lacking is (1) a simple and powerful language that can express application problems by directly using logic rules as well as all other features without extra interface code, and with a clear semantics for analysis as well as execution, plus (2) a compilation framework for implementing this powerful language, in a practical way by extending a widely-used programming language, and levergaing best performance of logic programming systems.

We have developed such a powerful language, Alda, that combines the advantages of logic languages and commonly-used languages for building applications, by supporting direct use of all of logic rules, sets, functions, updates, and objects including concurrent and distributed processes as seamlessly integrated built-ins with no extra interfaces.

  • Sets of rules can be specified directly as other definitions can, where predicates in rules are simply set-valued variables holding the set of tuples for which the predicate is true. Thus, predicates can be used directly as set-valued variables and vice versa without needing any extra interface, and predicates being set-valued variables are completely different from functions or procedures, unlike in prior logic rule languages and extensions.

  • Queries using rule sets are calls to an inference function that computes desired values of derived predicates (i.e., predicates in conclusions of rules) given values of base predicates (i.e., predicates not in conclusions of rules). Thus, queries as function calls need no extra interface either, and a rule set can be used with predicates in it holding the values of any appropriate set-valued variables.

  • Values of predicates can be updated either directly as for other variables or by the inference function; declarative semantics of rules are ensured by automatically maintaining values of derived predicates when values of base predicates are updated, through appropriate implicit calls to the inference function.

  • Predicates and rule sets can be object attributes as well as global and local names, just as variables and functions can.

We also defined 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. Note that predicates as variables, and queries as calls with different predicate values, also avoid the need for higher-order predicates or more sophisticated features for reusing rules on different predicates in more complex logic languages.

Implementing such a powerful language is nontrivial, especially to support logic rules together with updates and objects. We describe a compilation framework for implementation that achieves generally good performance.

  • The framework implements Alda by building on an object-oriented language that supports all other features but not logic rules, and uses an efficient logic rule system for queries using rules.

  • The framework considers and analyzes different kinds of updates to predicates in different scopes and uses an efficient implementation for each kind to minimize calls to the inference function while still ensuring the declarative semantics of rules.

  • The framework also allows optimizations from decades of study of logic rules to be added for further efficiency improvements, both for queries using rules and for incremental queries under updates.

There has been a significant amount of related research, as discussed in Section 5. Our work contains two main contributions:

  • A language that supports direct use of logic rules with sets, functions, updates, and objects, all as built-ins, seamlessly integrated, with a formal semantics.

  • A compilation framework for implementation in a widely-used programming language, where additional optimizations for rules can be exploited when available.

We have developed a prototype implementation of the compilation framework for Alda and experimented with a variety of programming and performance benchmarks. Our experiments strongly confirm the power and benefit of a seamlessly integrated language and the generally good performance of the implementation. Our implementation and benchmarks are publicly available [67].

2 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 2.12.6 when used as examples. A complete exposition of the formal semantics is in A.

1 class CoreRBAC: # class for Core RBAC component/object
2 def setup(): # method to set up the object, with no arguments
3 self.USERS, self.ROLES, self.UR := {},{},{}
4 # set users, roles, user-role pairs to empty sets
5 def AddRole(role): # method to add a role
6 ROLES.add(role) # add the role to ROLES
7 def AssignedUsers(role): # method to return assigned users of a role
8 return {u: u in USERS | (u,role) in UR} # return set of users having the role
...
9 class HierRBAC extends CoreRBAC: # Hierarchical RBAC extending Core RBAC
10 def setup():
11 super().setup() # call setup of CoreRBAC, to set sets as in there
12 self.RH := {} # set ascendant-descendant role pairs to empty set
13 def AddInheritance(a,d): # to add inherit. of an ascendant by a descendant
14 RH.add((a,d)) # add pair (a,d) to RH
15 rules trans_rs: # rule set defining transitive closure
16 path(x,y) if edge(x,y) # path holds for (x,y) if edge holds for (x,y)
17 path(x,y) if edge(x,z), path(z,y) # ... if edge(x,z) holds and path(z,y) holds
18 def transRH(): # to return transitive RH and reflexive role pairs
19 return infer(path, edge=RH, rules=trans_rs) + {(r,r): r in ROLES}
20 def AuthorizedUsers(role): # to return users having a role transitively
21 return {u: u in USERS, r in ROLES | (u,r) in UR and (r,role) in transRH()}
...
22 h = new(HierRBAC, []) # create HierRBAC object h, with no args to setup
23 h.AddRole(’chair’) # call AddRole of h with role ’chair’
...
24 h.AuthorizedUsers(’chair’) # call AuthorizedUsers of h with role ‘chair’
...
Figure 1: An example program in Alda, for Role-Based Access Control (RBAC), demonstrating logic rules used with sets, functions, updates, and objects.

2.1 Logic rules

We support rule sets of the following form, where 𝑛𝑎𝑚𝑒\it name is the name of the rule set, 𝑑𝑒𝑐𝑙𝑎𝑟𝑎𝑡𝑖𝑜𝑛𝑠\it declarations is a set of predicate declarations, and the body is a set of rules.

  rules 𝑛𝑎𝑚𝑒\it name (𝑑𝑒𝑐𝑙𝑎𝑟𝑎𝑡𝑖𝑜𝑛𝑠\it declarations):
    𝑟𝑢𝑙𝑒\it rule+

A rule is either one of the two equivalent forms below (for users accustomed to either form), meaning that if ℎ𝑦𝑝𝑜𝑡ℎ𝑒𝑠𝑖𝑠1\it hypothesis_{1} through ℎ𝑦𝑝𝑜𝑡ℎ𝑒𝑠𝑖𝑠h\it hypothesis_{h} all hold, then 𝑐𝑜𝑛𝑐𝑙𝑢𝑠𝑖𝑜𝑛\it conclusion holds.

  𝑐𝑜𝑛𝑐𝑙𝑢𝑠𝑖𝑜𝑛\it conclusion if  ℎ𝑦𝑝𝑜𝑡ℎ𝑒𝑠𝑖𝑠1\it hypothesis_{1}, ℎ𝑦𝑝𝑜𝑡ℎ𝑒𝑠𝑖𝑠2\it hypothesis_{2}, \it..., ℎ𝑦𝑝𝑜𝑡ℎ𝑒𝑠𝑖𝑠h\it hypothesis_{h}
  if  ℎ𝑦𝑝𝑜𝑡ℎ𝑒𝑠𝑖𝑠1\it hypothesis_{1}, ℎ𝑦𝑝𝑜𝑡ℎ𝑒𝑠𝑖𝑠2\it hypothesis_{2}, \it..., ℎ𝑦𝑝𝑜𝑡ℎ𝑒𝑠𝑖𝑠h\it hypothesis_{h}: 𝑐𝑜𝑛𝑐𝑙𝑢𝑠𝑖𝑜𝑛\it conclusion

If a conclusion holds without a hypothesis, then if and : are omitted.

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 [38] that are expressed in typed languages [53]. They may also specify assumptions about predicates [39] to support different desired semantics [40, 41]. 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 [1, 48] 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

p\it p(𝑎𝑟𝑔1\it arg_{1},\it...,𝑎𝑟𝑔a\it arg_{a})

where p\it p is a predicate, and each 𝑎𝑟𝑔k\it 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 2.1.

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.   Consider a set of rules. Predicates not in any conclusion are called

Definition 1.

base predicates, and the other predicates are called

Definition 2.

derived predicates. We say that a predicate p\it p

Definition 3.

depends on a predicate q\it q if p\it p is in the conclusion of a rule whose hypotheses contain q\it q or contain a predicate that depends on q\it q recursively. We say that a derived predicate p\it p

Definition 4.

fully depends on a set s\it s of base predicates if p\it p does not depend on other base predicates.

Example 2.2.

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.

2.2 Integrating rules with sets, functions, updates, and objects

Our overall language supports all of rule sets and the following language constructs as built-ins; 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 data of other types, 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

Definition 5.

global if it is introduced (declared or defined) at the global scope; is an

Definition 6.

object field if it is introduced for that object; or is

Definition 7.

local to the function, method, or rule set that contains it otherwise. After a name is defined, the value that it is holding is available: 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 2.3.

Rule set trans_rs in Figure 1 (defined on lines 15-17 and queried using a call to an inference function. infer, 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-9, 18-19, and 20-21), procedures (defined on lines 2-3, 5-6, 10-12, and 13-14), 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 the 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 𝑟𝑠\it rs is completely declarative, exactly following the standard least fixed-point semantics of rules [16, 38]:

  • Given values of any set s\it s of base predicates in 𝑟𝑠\it rs, the meaning of 𝑟𝑠\it rs is, for all derived predicates in 𝑟𝑠\it rs that fully depend on s\it s, the least set of values that can be inferred, directly or indirectly, by using the given values and the rules in 𝑟𝑠\it rs;

    for any derived predicate in 𝑟𝑠\it rs that does not fully depend on s\it s, i.e., depends on any base predicate whose values are not given, its value is

    Definition 8.

    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 2.32.6.

    2.3 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:

    • For any predicate p\it p over values x1\it x_{1},\it...,xa\it x_{a}, assertion p\it p(x1\it x_{1},\it...,xa\it x_{a}) is true—i.e., p\it p(x1\it x_{1},\it...,xa\it x_{a}) is a fact—if and only if tuple (x1\it x_{1},\it...,xa\it x_{a}) is in set p\it p. Formally,

      p(x1,,xa)(x1,,xain p\vskip-4.30554pt\mbox{\footnotesize\tt\mbox{$\it p$}(\mbox{$\it x_{1}$},\mbox{$\it...$},\mbox{$\it x_{a}$})}~\Longleftrightarrow~\mbox{\footnotesize\tt(\mbox{$\it x_{1}$},\mbox{$\it...$},\mbox{$\it x_{a}$}) {\color[rgb]{0,0,1}in} \mbox{$\it p$}}

    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 2.4.

    In rule set trans_rs in Figure 1, predicate edge is exactly a variable holding a set of pairs, such that edge(xx,yy) is true iff (xx,yy) 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:

        𝑒𝑥𝑝\it exp in 𝑠𝑒𝑥𝑝\it sexp        membership
        𝑒𝑥𝑝\it exp not in 𝑠𝑒𝑥𝑝\it sexp        negated membership
        𝑠𝑒𝑥𝑝1\it sexp_{1} + 𝑠𝑒𝑥𝑝2\it sexp_{2}        union
        {𝑒𝑥𝑝\it exp: v1\it v_{1} in 𝑠𝑒𝑥𝑝1\it sexp_{1},\it...,vk\it v_{k} in 𝑠𝑒𝑥𝑝k\it sexp_{k} | 𝑏𝑒𝑥𝑝\it bexp}        comprehension
        𝑎𝑔𝑔\it agg 𝑠𝑒𝑥𝑝\it sexp,    where 𝑎𝑔𝑔\it agg is count, max, min, sum        aggregation
        some v1\it v_{1} in 𝑠𝑒𝑥𝑝1\it sexp_{1},\it...,vk\it v_{k} in 𝑠𝑒𝑥𝑝k\it sexp_{k} | 𝑏𝑒𝑥𝑝\it bexp        existential quantification

    A comprehension returns the set of values of 𝑒𝑥𝑝\it exp for all combinations of values of variables that satisfy all membership clauses vi\it v_{i} in 𝑠𝑒𝑥𝑝i\it sexp_{i} and condition 𝑏𝑒𝑥𝑝\it bexp. An aggregation returns the count, max, etc. of the set value of 𝑠𝑒𝑥𝑝\it sexp. An existential quantification returns true iff for some combination of values of variables that satisfies all vi\it v_{i} in 𝑠𝑒𝑥𝑝\it sexp clauses, condition 𝑏𝑒𝑥𝑝\it bexp holds. When an existential quantification returns true, variables v1\it v_{1},…,vk\it v_{k} are bound to a witness. Note that these set queries, as in [42], are more powerful than those in Python.

    Example 2.5.

    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 for 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))

    In the comprehension and aggregation forms, each vi\it v_{i} can also be a tuple pattern that elements of the set value of 𝑠𝑒𝑥𝑝i\it sexp_{i} must match [42]. A

    Definition 9.

    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 =; multiple occurrences of a variable must be assigned the same value; corresponding components of wildcard are ignored.

    Example 2.6.

    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 arguments of predicates in rules, are like pattern variables, i.e., variables not prefixed with = in patterns. These variables are used for relating values, through what is generally called unification; they do not hold values, unlike variables prefixed with = in patterns.

    2.4 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 𝑞𝑢𝑒𝑟𝑦k\it query_{k}’s and pk\it p_{k}=𝑠𝑒𝑥𝑝k\it sexp_{k}’s being optional:

      infer(𝑞𝑢𝑒𝑟𝑦1\it query_{1}, \it..., 𝑞𝑢𝑒𝑟𝑦j\it query_{j}, p1\it p_{1}=𝑠𝑒𝑥𝑝1\it sexp_{1}, \it..., pi\it p_{i}=𝑠𝑒𝑥𝑝i\it sexp_{i}, rules=𝑟𝑠\it rs)
    

    𝑟𝑠\it rs is the name of a rule set. Each 𝑠𝑒𝑥𝑝k\it sexp_{k} is a set-valued expression. Each pk\it p_{k} is a base predicate of 𝑟𝑠\it rs and is local to 𝑟𝑠\it rs. Each 𝑞𝑢𝑒𝑟𝑦k\it query_{k} is of the form p\it p(𝑎𝑟𝑔1\it arg_{1},\it...,𝑎𝑟𝑔a\it arg_{a}), where p\it p is a derived predicate of 𝑟𝑠\it rs, and each argument 𝑎𝑟𝑔k\it 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 𝑎𝑟𝑔k\it arg_{k}’s are _, the abbreviated form p\it 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 2.7.

    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=trans_rs)
    infer(path(_,_), edge=RH, rules=trans_rs)
    infer(path(1,_), path(_,=R), edge=RH, rules=trans_rs)

    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. 1)

      For each value k\it k from 1 to i\it i, assign the set value of expression 𝑠𝑒𝑥𝑝k\it sexp_{k} to predicate pk\it p_{k} that is a base predicate of rule set 𝑟𝑠\it rs.

    2. 2)

      Perform inference using the rules in 𝑟𝑠\it rs and the given values of base predicates of 𝑟𝑠\it rs following the declarative semantics, including assigning to derived predicates that are not local.

    3. 3)

      For each value k\it k from 1 to j\it j, return the result of query 𝑞𝑢𝑒𝑟𝑦k\it query_{k} as the k\it kth component of the return value. The result of a query with ll distinct variables not prefixed with = is a set of tuples of ll components, one for each of the distinct variables in their order of first occurrence in the query.

    Note that when there are no pk\it p_{k}=𝑠𝑒𝑥𝑝k\it sexp_{k}’s, only defined values of base predicates that are not local to 𝑟𝑠\it rs are used; and when there are no 𝑞𝑢𝑒𝑟𝑦k\it query_{k}’s, only values of derived predicates that are not local to 𝑟𝑠\it rs may be inferred and no value is returned. This is the case for implicit calls to infer on 𝑟𝑠\it rs.

    2.5 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 𝑟𝑠\it rs that are local to 𝑟𝑠\it rs are assigned values at calls to infer on 𝑟𝑠\it rs, as described earlier. Base predicates that are not local can be updated by assignment statements or set update operations. We use

      𝑙𝑒𝑥𝑝\it lexp := 𝑒𝑥𝑝\it exp
    

    for assignments, where 𝑙𝑒𝑥𝑝\it lexp can also be a nested tuple of variables, and each variable is assigned the corresponding component of the value of 𝑒𝑥𝑝\it exp.

    Derived predicates of a rule set 𝑟𝑠\it rs can be updated only by calls to the inference function on 𝑟𝑠\it rs. The updates must ensure the declarative semantics of 𝑟𝑠\it rs:

    • Whenever a base predicate of 𝑟𝑠\it rs is updated in the program, the values of the derived predicates in 𝑟𝑠\it rs are maintained according to the declarative semantics of 𝑟𝑠\it rs by calling infer on 𝑟𝑠\it rs.

      Updates to derived predicates of 𝑟𝑠\it rs outside 𝑟𝑠\it 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.

    Example 2.8.

    Consider rule set trans_rs in Figure 1. If edge is not local, one may assign a set of pairs to edge:

    edge := {(1,8),(2,9),(1,2)}

    If edge is local, the calls to infer in the example in Section 2.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 example in Section 2.4 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 call to infer in the example in Section 2.4, this can be

    from1,toR := infer(path(1,_), path(_,=R), edge=RH, rules=trans_rs)

    If both edge and path are not local, then whenever edge is updated, an implicit call infer(rules=trans_rs) is made automatically to update path.

    For the RBAC example in Figure 1, different ways of using rules are possible, including (1) allloc: adding a rule path(x,x) if role(x,x) to the rule set, adding role=ROLES in the call to infer, and removing the union in function transRH, so all predicates are local variables; (2) nonloc: as in allloc, except to replace predicates edge, role, and path with RH, ROLES, and a new field transRH, respectively, replace call transRH() with field transRH, and remove function transRH; (3) union: as in Figure 1; and other combinations of aspects of (1)–(3).

    2.6 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 2.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 2.9.

    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 [3] was approved in 2004 after several rounds of public review [59, 25, 15], building on much research during the preceding decade and earlier. High-level executable specifications were developed for the entire RBAC standard [37], 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 [37].11 1 Only a few selected sets and functions are included, and with small changes to names and syntax.

    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 [37],1 except that function transRH() in [37] computes the transitive closure of RH plus reflexive role pairs for all roles in ROLES by using a complex and inefficient while loop much worse than that in Section 2.3 (due to Python’s lack of some with witness) 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 [37, 31] 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, 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 [3, 37].

    3 Compilation

    We describe our compilation framework for implementing Alda, by building on an object-oriented language that supports all features except rules and queries and on an efficient logic rule engine for queries using rules. Three main tasks are (1) compiling rule sets to generate rules accepted by the rule engine, (2) compiling queries using rules to generate queries accepted by the rule engine, together with automatic conversion of data and query results, and (3) compiling updates to predicates that require implicit automatic queries and updates of the query results. The compiler must appropriately handle scoping of rule sets and predicates for all three tasks. Besides that, task (1) is straightforward, task (2) is also straightforward but tedious, and task (3) requires the most analysis, so we focus on task (3) below.

    We first describe how to compile all possible updates to predicates, starting with the checks and actions needed to correctly handle updates for a single rule set with implicit and explicit calls to infer. We then describe how to implement the inference in infer. In B, we systematize powerful optimizations that can be added in the overall compilation framework; clearly separated handling of updates and queries in our compilation framework allows optimizations to be added in a modular fashion.

    3.1 Compiling updates to predicates

    The operational semantics to ensure the declarative semantics of a rule set 𝑟𝑠\it rs is conceptually simple, but for efficiency, the implementation required varies, depending on the kind of updates to base predicates of 𝑟𝑠\it rs outside 𝑟𝑠\it rs. Note that inside 𝑟𝑠\it rs there are no updates to base predicates of 𝑟𝑠\it rs, by definition of base predicate.

    1. 1)

      Local updates. Local variables of 𝑟𝑠\it rs, i.e., predicates local to 𝑟𝑠\it rs, can be assigned values only at explicit calls to infer on 𝑟𝑠\it rs. Such a call passes in values of local variables that are base predicates of 𝑟𝑠\it rs before doing the inference. Values of local variables that are derived predicates of 𝑟𝑠\it rs can only be used in constructing answers to the queries in the call, and the answers are returned from the call.

      There are no updates outside 𝑟𝑠\it rs to local variables that are derived predicates of 𝑟𝑠\it rs, by definition of local variables.

    2. 2)

      Non-local updates. For updates to non-local variables of 𝑟𝑠\it rs, an implicit call to infer on 𝑟𝑠\it rs needs to be made only after every update to a base predicate of 𝑟𝑠\it rs.

      Statements outside 𝑟𝑠\it rs that update derived predicates of 𝑟𝑠\it rs are identified and reported as errors.

      In languages or application programs where variables hold data values, such as in database languages and applications, these updates can be determined simply at compile time, e.g., if s holds a set value, then s := s+{x} updates the set value of s. This is also the case when logic rules are used in these languages and programs.

      In programs where variables may be references to data values, each update needs to check whether the updated variable may alias a predicate of 𝑟𝑠\it rs, conservatively at compile-time if possible, and at runtime otherwise.

    To satisfy these requirements, the overall method for compiling an update to a variable v outside rule sets is:

    • In languages or application programs where variables hold data values, report a compile-time error if v is a derived predicate of any rule set; otherwise, for each rule set 𝑟𝑠\it rs that contains v as a base predicate, insert code, after the update, that calls infer on 𝑟𝑠\it rs with no arguments for base predicates and no queries.

    • Otherwise, if v may refer to a predicate in a rule set, 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 𝑟𝑠\it rs, if v refers to a base predicate of 𝑟𝑠\it rs, call infer on 𝑟𝑠\it rs with no arguments for base predicates and no queries.

    Our method for 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, to answering specific queries using specific rules and specific sets of values of specific base predicates at explicit calls.

    Obviously, updates in different cases may have significant impact on program efficiency. Update analysis is needed to determine the case and generate correct code. Our compilation method above minimizes calls to infer in each case.

    3.2 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 [27, 65, 66], 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 [57, 63], or compiled to specialized standalone executable code as in, e.g., [38, 53, 26], that is then executed. Our current implementation uses the former approach, by indeed using the well-known XSB system, as described in Section 4, because it allows easier extensions to support more kinds of rules and optimizations that are already supported in XSB. Other powerful logic rule engines, including efficient Answer Set Programming (ASP) systems such as Clingo [18], can certainly be used also.

    4 Implementation and experimental evaluation

    We have implemented a prototype compiler for Alda. The compiler generates executable code in Python. The generated code calls the XSB logic rule engine [57, 63] for inference using rules.

    We implemented Alda by extending the DistAlgo compiler [43, 42, 33]. DistAlgo 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 2, allowing any user with Python to run Alda directly.

    The Alda implementation extends the DistAlgo compiler to support rule-set definitions, function infer, and maintenance of derived predicates at updates to non-local variables. It handles direct updates to variables used as predicates, not updates through aliasing, as we found this to be the only update case in all benchmarks and other examples we have seen; we think this is because using logic rules with updates is similar to using queries and updates in relational databases, with no need of updates through aliasing. Currently Datalog rules extended with unrestricted negation are supported, and well-founded semantics computed by XSB is used; 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 rule sets.

    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.

      def rules (name = 𝑟𝑠𝑛𝑎𝑚𝑒\it rsname):
        𝑐𝑜𝑛𝑐𝑙𝑢𝑠𝑖𝑜𝑛\it conclusion, if_(ℎ𝑦𝑝𝑜𝑡ℎ𝑒𝑠𝑖𝑠1\it hypothesis_{1}, ℎ𝑦𝑝𝑜𝑡ℎ𝑒𝑠𝑖𝑠2\it hypothesis_{2}, \it..., ℎ𝑦𝑝𝑜𝑡ℎ𝑒𝑠𝑖𝑠h\it hypothesis_{h})
        if (ℎ𝑦𝑝𝑜𝑡ℎ𝑒𝑠𝑖𝑠1\it hypothesis_{1}, ℎ𝑦𝑝𝑜𝑡ℎ𝑒𝑠𝑖𝑠2\it hypothesis_{2}, \it..., ℎ𝑦𝑝𝑜𝑡ℎ𝑒𝑠𝑖𝑠h\it hypothesis_{h}): 𝑐𝑜𝑛𝑐𝑙𝑢𝑠𝑖𝑜𝑛\it conclusion
        \it...
    

    Rule sets are translated into Prolog rules at compile time. The directive :- auto_table. 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.22 2 A version for Unix, not yet Windows, has been released: passing data of size 100 million in memory took about 30 nanoseconds per element [63, release notes]. So even the largest data in our experiments, of size a few millions, would take 0.1–0.2 seconds to pass in memory, instead of 10–20 seconds with the current external interface. 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 [32] 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 support, especially for high-level queries, already in the DistAlgo compiler and to the powerful query 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 slightly richer interface between the two.

    Benchmark      sets Benchmarks Variants
     and timing
    Problem kinds Code/data size
    Open- RuleBench [32] 13 incl. LUBM,
    Mondial, DBLP,
    TC, WordNet,
    Wine
    TCrev,
    TCda,
    TCpy,
    ORBtimer
    many kinds of
    rules and queries,
    but missing
    aggregate queries
    largest rule set:
    967 rules,
    largest data size:
    2.4M+
    RBAC
    as in
    Section 2.6
    RBACallloc,
    RBACnonloc,
    RBACunion
    RBACda, RBACpy, RBACtimer interleaved object
    queries and updates
    with function and
    recursive rules
    program size:
    385–423,
    data size:
    10K+
    Program Analysis PA (on any prog.:
    numpy, pandas,
    matplot, pytorch,
    sympy, etc.)
    PAopt,
    PAtimer
    interleaved rules,
    aggregate and set
    queries, and
    recursive functions
    program size:
    55 XSB, 33 Alda,
    largest data size:
    5.1M+
    Table 1: Benchmarks from different kinds of problems. RBAC benchmarks are for different ways of using rules as at the end of Section 2.4. PA is a mixture of problems from class hierarchy analysis. Under Variants, suffixes py and da indicate using while loops like that in Section 2.3 in Python and DistAlgo, respectively, instead of using rules.

    We discuss our experiments on the benchmarks summarized in Table 1. Detailed description of the benchmarks are in [44, 45]. Just as the benchmarks selected, the experiments selected are also meant to show generally good performance even under the most extreme overhead penalties we have encountered—runs with large data (DBLP and PA), large query results (transitive closure TC), large rules (Wine), frequent switches among different ways of using rules and other features (RBAC and PA), and frequent external invocations of the rule engine (RBAC). Our extensive experiments with other uses of Alda have experienced minimum performance overhead.

    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 when calling XSB. Program sizes are numbers of lines excluding comments and empty lines. Data sizes are number of facts.

    We summarize the results from the experiments below. Detailed measurements and explanations are in [44, 45].

    • Compared with XSB programs in OpenRuleBench, the corresponding Alda programs are much smaller, almost all by dozens or even hundreds of lines, because all benchmarking code is in a single shared 45-line ORBtimer, much easier in Python than XSB. Compilation times are all 0.6 seconds or less.

    • Running times for all benchmarks and variants, except for PA, are as expected, e.g., TC is drastically faster than TCpy and TCda, and essentially as fast as XSB if not for the overhead of using external interface with XSB; and RBACnonloc is much faster than RBACallloc due to updates being much less frequent than queries. The overhead of using external interface is obvious: e.g., for TC, up to 5.9 seconds, out of 29.2, for graphs of 100K edges; for PA, 13.1 seconds, out of 15.2, on the largest program, SymPy; and worst for DBLP, 26.9 seconds, out of 30.6, on over 2.4M facts.

      However, even so, Alda is competitive, as described above, and the overhead is expected to be reduced to 1% of it with an in-memory Python-XSB interface.

    • For PA, the corresponding XSB programs were all slower and even drastically slower than Alda programs, even 120 times slower on PyTorch. Significant effort was spent on performance debugging and manual optimization before we eventually created a version that is faster than Alda—5.1 vs. 15.2 seconds on SymPy.

    5 Related work and conclusion

    There has been extensive effort in 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. [27, 29]. For example, see Maier et al. [48] 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 [62]; Flora [71, 28] builds on XSB and supports objects (F-logic), higher-order programming (HiLog), and updates (Transaction Logic); and Picat [72] builds on B-Prolog and supports updates, comprehensions, etc. Lambda Prolog [50] extends Prolog with simply typed lambda terms and higher-order programming. Functional logic languages, such as Mercury [61] and Curry [23], combine functional programming and logic programming. Some logic programming systems are driven by scripting externally, e.g., using Lua for IDP [8], and shell scripts for LogicBlox [4]. Additional examples of Datalog extensions include Flix [47, 46], which supports lattices and monotone functions, and DDlog [56], which supports incremental maintenance under updates to input relations. These languages and extensions do not support predicates as set-valued variables together with commonly-used updates and objects in a simple and direct way, or do not support them at all.

    Many languages in other programming paradigms, especially including imperative languages and object-oriented languages, have been extended to support rules by being a host language. This is generally through explicit library interfaces of the host languages to connect with a particular logic language, for example, a Java interface for XSB through InterProlog [11, 63], C++ and Python interfaces for answer-set programming systems dlvhex [51] and Potassco [5], a Python interface for IDP [68], Rust and other interfaces for DDlog [56], and many more, e.g., for miniKanren [9]. Hosting logic languages through explicit interfaces requires programmers to write extra wrapper code for going to the rule language and coming back—declare predicates and/or logic variables, wrap features in special objects, functions, macros, etc., and/or convert data to and from special representations. They are in the same spirit as interfaces such as JDBC [52] 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 [55] 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 and/or objects include LOGRES [10], which integrates object-oriented data modeling and updates with rules under inflationary semantics; TLA+ [30], a logic language for specifying actions; CLAIRE [12], an object-oriented language that supports functions, sets, and rules whose conclusions are actions; LINQ [49, 34], an extension of C# for SQL-like queries; IceDust [24], a Java-based language for querying data with path-based navigation and incremental computation; extended LogiQL in SolverBlox [7], for mathematical and logic programming on top of Datalog with updates and constraints; and other logic-based query languages, e.g., Datomic [2] and SOUL [14]. These are either logic languages lacking general imperative and objected-oriented programming constructs, or imperative and object-oriented languages lacking the power and full declarativeness of logic rules.

    In conclusion, Alda supports ease of programming with logic rules together with all of sets, functions, updates, and objects as seamlessly integrated built-ins, without extra interfaces or boiler-plate code. As a direction for future work, many optimizations can be added to improve the efficiency of implementations. This includes optimizing the logic rule engines used [38, 66], the interfaces and interactions with them, and using other efficient rule systems such as Clingo [18] and specialized rule implementations such as Souffle [26] to obtain the best possible performance.

    Acknowledgments

    We thank David S. Warren for an initial 28-line XSB program for interface to XSB, and Tuncay Tekle for help implementing some benchmarks and running some preliminary experiments. We also thank Thang Bui for additional applications in program analysis and optimization, and students in undergraduate and graduate courses for using Alda and its earlier versions, called DA-rules.

    References
    • Abiteboul et al. (1995) Abiteboul, S., Hull, R., and Vianu, V. 1995. Foundations of Databases: The Logical Level. Addison-Wesley.
    • Anderson et al. (2016) Anderson, J., Gaare, M., Holguín, J., Bailey, N., and Pratley, T. 2016. The Datomic database. In Professional Clojure. Wiley Online Library, Chapter 6, 169–215.
    • ANSI INCITS (2004) ANSI INCITS. 2004. Role-Based Access Control. ANSI INCITS 359-2004, American National Standards Institute, International Committee for Information Technology Standards.
    • Aref et al. (2015) Aref, M., ten Cate, B., Green, T. J., Kimelfeld, B., Olteanu, D., Pasalic, E., Veldhuizen, T. L., and Washburn, G. 2015. Design and implementation of the LogicBlox system. In Proceedings of the 2015 ACM SIGMOD International Conference on Management of Data. 1371–1382. https://doi.org/10.1145/2723372.2742796.
    • Banbara et al. (2017) Banbara, M., Kaufmann, B., Ostrowski, M., and Schaub, T. 2017. Clingcon: The next generation. Theory and Practice of Logic Programming 17, 4, 408–461.
    • Bancilhon et al. (1986) Bancilhon, F., Maier, D., Sagiv, Y., and Ullman, J. D. 1986. Magic sets and other strange ways to implement logic programs. In Proceedings of the 5th ACM SIGACT-SIGMOD Symposium on Principles of Database Systems. 1–16.
    • Borraz-Sánchez et al. (2018) Borraz-Sánchez, C., Klabjan, D., Pasalic, E., and Aref, M. 2018. SolverBlox: Algebraic modeling in Datalog. In Declarative Logic Programming: Theory, Systems, and Applications, M. Kifer and Y. A. Liu, Eds. ACM and Morgan & Claypool, Chapter 6, 331–356. https://doi.org/10.1145/3191315.3191322.
    • Bruynooghe et al. (2014) Bruynooghe, M., Blockeel, H., Bogaerts, B., De Cat, B., De Pooter, S., Jansen, J., Labarre, A., Ramon, J., Denecker, M., and Verwer, S. 2014. Predicate logic as a modeling language: Modeling and solving some machine learning and data mining problems with IDP3. Theory and Practice of Logic Programming 15, 6, 783–817. https://doi.org/10.1017/S147106841400009X.
    • Byrd (2009) Byrd, W. E. 2009. Relational programming in miniKanren: Techniques, applications, and implementations. Ph.D. thesis, Indiana University.
    • Cacace et al. (1990) Cacace, F., Ceri, S., Crespi-Reghizzi, S., Tanca, L., and Zicari, R. 1990. Integrating object-oriented data modelling with a rule-based programming paradigm. In Proceedings of the 1990 ACM SIGMOD international conference on Management of data. 225–236.
    • Calejo (2004) Calejo, M. 2004. InterProlog: Towards a declarative embedding of logic programming in Java. In Proceedings of the 9th European Conference on Logics in Artificial Intelligence. LNCS, vol. 3229. Springer, 714–717.
    • Caseau et al. (2002) Caseau, Y., Josset, F.-X., and Laburthe, F. 2002. Claire: Combining sets, search and rules to better express algorithms. Theory and Practice of Logic Programming 2, 6, 769–805.
    • Chen and Warren (1996) Chen, W. and Warren, D. S. 1996. Tabled evaluation with delaying for general logic programs. Journal of the ACM 43, 1, 20–74.
    • De Roover et al. (2011) De Roover, C., Noguera, C., Kellens, A., and Jonckers, V. 2011. The SOUL tool suite for querying programs in symbiosis with Eclipse. In Proceedings of the 9th International Conference on Principles and Practice of Programming in Java. 71–80.
    • Ferraiolo et al. (2001) Ferraiolo, D. F., Sandhu, R., Gavrila, S., Kuhn, D. R., and Chandramouli, R. 2001. Proposed NIST standard for role-based access control. ACM Transactions on Information and Systems Security 4, 3, 224–274.
    • Fitting (2002) Fitting, M. 2002. Fixpoint semantics for logic programming: A survey. Theoretical Computer Science 278, 1, 25–51.
    • Fong and Ullman (1976) Fong, A. C. and Ullman, J. D. 1976. Inductive variables in very high level languages. In Conference Record of the 3rd Annual ACM Symposium on Principles of Programming Languages. 104–112.
    • Gebser et al. (2019) Gebser, M., Kaminski, R., Kaufmann, B., and Schaub, T. 2019. Multi-shot ASP solving with clingo. Theory and Practice of Logic Programming 19, 1, 27–82. https://doi.org/10.1017/S1471068418000054.
    • Geiger (1995) Geiger, K. 1995. Inside ODBC. Microsoft Press.
    • Gorbovitski et al. (2010) Gorbovitski, M., Liu, Y. A., Stoller, S. D., Rothamel, T., and Tekle, T. 2010. Alias analysis for optimization of dynamic languages. In Proceedings of the 6th Symposium on Dynamic Languages. ACM Press, 27–42. https://doi.org/10.1145/1869631.1869635.
    • Goyal (2005) Goyal, D. 2005. Transformational derivation of an improved alias analysis algorithm. Higher-Order and Symbolic Computation 18, 1–2, 15–49.
    • Gupta and Mumick (1999) Gupta, A. and Mumick, I. S. 1999. Maintenance of materialized views: Problems, techniques, and applications. In Materialized Views: Techniques, Implementations, and Applications. MIT Press, 145–157.
    • Hanus (2013) Hanus, M. 2013. Functional logic programming: From theory to Curry. In Programming Logics. Springer, 123–168.
    • Harkes et al. (2016) Harkes, D. C., Groenewegen, D. M., and Visser, E. 2016. IceDust: Incremental and eventual computation of derived values in persistent object graphs. In 30th European Conference on Object-Oriented Programming. LIPIcs, vol. 56. Schloss Dagstuhl–Leibniz-Zentrum fuer Informatik, 11:1–11:26.
    • Jaeger and Tidswell (2000) Jaeger, T. and Tidswell, J. 2000. Rebuttal to the NIST RBAC model proposal. In Proceedings of the 5th ACM Workshop on Role Based Access Control. 66.
    • Jordan et al. (2016) Jordan, H., Scholz, B., and Subotić, P. 2016. Soufflé: On synthesis of program analyzers. In Proceedings of the International Conference on Computer Aided Verification. Springer, 422–430.
    • Kifer and Liu (2018) Kifer, M. and Liu, Y. A., Eds. 2018. Declarative Logic Programming: Theory, Systems, and Applications. ACM and Morgan & Claypool.
    • Kifer et al. (2020) Kifer, M., Yang, G., Wan, H., and Zhao, C. 2020. Ergo Lite (a.k.a. Flora-2): User’s Manual Version 2.1. Stony Brook University. http://flora.sourceforge.net/. Accessed May 25, 2023.
    • Körner et al. (2022) Körner, P., Leuschel, M., Barbosa, J. a., Costa, V. S., Dahl, V., Hermenegildo, M. V., Morales, J. F., Wielemaker, J., Diaz, D., Abreu, S., and Ciatto, G. 2022. Fifty years of Prolog and beyond. Theory and Practice of Logic Programming 22, 6, 776–858. https://doi.org/10.1017/S1471068422000102.
    • Lamport (1994) Lamport, L. 1994. The temporal logic of actions. ACM Transactions on Programming Languages and Systems 16, 3, 872–923.
    • Li et al. (2007) Li, N., Byun, J.-W., and Bertino, E. 2007. A critique of the ANSI standard on role-based access control. IEEE Security and Privacy 5, 6, 41–49.
    • Liang et al. (2009) Liang, S., Fodor, P., Wan, H., and Kifer, M. 2009. OpenRuleBench: An analysis of the performance of rule engines. In Proceedings of the 18th International Conference on World Wide Web. ACM Press, 601–610.
    • Lin and Liu (2022) Lin, B. and Liu, Y. A. 2014 (Latest update January 30, 2022). DistAlgo: A language for distributed algorithms. http://github.com/DistAlgo. Accessed May 25, 2023.
    • LINQ (2023) LINQ 2023. Language Integrated Query (LINQ). https://docs.microsoft.com/dotnet/csharp/linq. Accessed May 25, 2023.
    • Liu (2018) Liu, Y. A. 2018. Logic programming applications: What are the abstractions and implementations? In Declarative Logic Programming: Theory, Systems, and Applications, M. Kifer and Y. A. Liu, Eds. ACM and Morgan & Claypool, Chapter 10, 519–557. Also https://arxiv.org/abs/1802.07284.
    • Liu et al. (2016) Liu, Y. A., Brandvein, J., Stoller, S. D., and Lin, B. 2016. Demand-driven incremental object queries. In Proceedings of the 18th International Symposium on Principles and Practice of Declarative Programming. ACM Press, 228–241. https://doi.org/10.1145/2967973.2968610.
    • Liu and Stoller (2007) Liu, Y. A. and Stoller, S. D. 2007. Role-based access control: A corrected and simplified specification. In Department of Defense Sponsored Information Security Research: New Methods for Protecting Against Cyber Threats. Wiley, 425–439.
    • Liu and Stoller (2009) Liu, Y. A. and Stoller, S. D. 2009. From Datalog rules to efficient programs with time and space guarantees. ACM Transactions on Programming Languages and Systems 31, 6, 1–38. https://doi.org/10.1145/1552309.1552311.
    • Liu and Stoller (2020) Liu, Y. A. and Stoller, S. D. 2020. Founded semantics and constraint semantics of logic rules. Journal of Logic and Computation 30, 8 (Dec.), 1609–1638. Also http://arxiv.org/abs/1606.06269.
    • Liu and Stoller (2021) Liu, Y. A. and Stoller, S. D. 2021. Knowledge of uncertain worlds: Programming with logical constraints. Journal of Logic and Computation 31, 1 (Jan.), 193–212. Also https://arxiv.org/abs/1910.10346.
    • Liu and Stoller (2022) Liu, Y. A. and Stoller, S. D. 2022. Recursive rules with aggregation: A simple unified semantics. Journal of Logic and Computation 32, 8 (Dec.), 1659–1693. Also http://arxiv.org/abs/2007.13053.
    • Liu et al. (2017) Liu, Y. A., Stoller, S. D., and Lin, B. 2017. From clarity to efficiency for distributed algorithms. ACM Transactions on Programming Languages and Systems 39, 3 (May), 12:1–12:41. Also http://arxiv.org/abs/1412.8461.
    • Liu et al. (2012) Liu, Y. A., Stoller, S. D., Lin, B., and Gorbovitski, M. 2012. From clarity to efficiency for distributed algorithms. In Proceedings of the 27th ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages and Applications. 395–410. https://doi.org/10.1145/2384616.2384645.
    • Liu et al. (2022) Liu, Y. A., Stoller, S. D., Tong, Y., Lin, B., and Tekle, K. T. 2022. Programming with rules and everything else, seamlessly. Computing Research Repository arXiv:2205.15204 [cs.PL]. http://arxiv.org/abs/2205.15204.
    • Liu et al. (2023) Liu, Y. A., Stoller, S. D., Tong, Y., and Tekle, K. T. 2023. Benchmarking for integrating logic rules with everything else. In Proceedings of the 39th International Conference on Logic Programming (Technical Communications). Open Publishing Association.
    • Madsen and Lhoták (2020) Madsen, M. and Lhoták, O. 2020. Fixpoints for the masses: Programming with first-class Datalog constraints. Proceedings of the ACM on Programming Languages 4, OOPSLA, 1–28.
    • Madsen et al. (2016) Madsen, M., Yee, M.-H., and Lhoták, O. 2016. From Datalog to Flix: A declarative language for fixed points on lattices. ACM SIGPLAN Notices 51, 6, 194–208.
    • Maier et al. (2018) Maier, D., Tekle, K. T., Kifer, M., and Warren, D. S. 2018. Datalog: Concepts, history and outlook. In Declarative Logic Programming: Theory, Systems, and Applications, M. Kifer and Y. A. Liu, Eds. ACM and Morgan & Claypool, Chapter 1, 3–120.
    • Meijer et al. (2006) Meijer, E., Beckman, B., and Bierman, G. 2006. LINQ: reconciling object, relations and XML in the .NET framework. In Proceedings of the 2006 ACM SIGMOD international conference on Management of data. 706–706.
    • Miller and Nadathur (2012) Miller, D. and Nadathur, G. 2012. Programming with Higher-Order Logic. Cambridge University Press.
    • Redl (2016) Redl, C. 2016. The DLVHEX system for knowledge representation: Recent advances (system description). Theory and Practice of Logic Programming 16, 5-6, 866–883.
    • Reese (2000) Reese, G. 2000. Database Programming with JDBC and JAVA. O’Reilly Media, Inc.
    • Rothamel and Liu (2007) Rothamel, T. and Liu, Y. A. 2007. Efficient implementation of tuple pattern based retrieval. In Proceedings of the ACM SIGPLAN 2007 Workshop on Partial Evaluation and Program Manipulation. 81–90. https://doi.org/10.1145/1244381.1244394.
    • Rothamel and Liu (2008) Rothamel, T. and Liu, Y. A. 2008. Generating incremental implementations of object-set queries. In Proceedings of the 7th International Conference on Generative Programming and Component Engineering. ACM Press, 55–66. https://doi.org/10.1145/1449913.1449923.
    • Roy and Haridi (2004) Roy, P. V. and Haridi, S. 2004. Concepts, Techniques, and Models of Computer Programming. MIT Press.
    • Ryzhyk and Budiu (2019) Ryzhyk, L. and Budiu, M. 2019. Differential datalog. In Datalog 2.0, 3rd International Workshop on the Resurgence of Datalog in Academia and Industry. 56–67.
    • Sagonas et al. (1994) Sagonas, K., Swift, T., and Warren, D. S. 1994. XSB as an efficient deductive database engine. In Proceedings of the 1994 ACM SIGMOD International Conference on Management of Data. ACM Press, 442–453.
    • Saha and Ramakrishnan (2003) Saha, D. and Ramakrishnan, C. R. 2003. Incremental evaluation of tabled logic programs. In Proceedings of the 19th International Conference on Logic Programming. Springer, 392–406. https://doi.org/10.1007/978-3-540-24599-5_27.
    • Sandhu et al. (2000) Sandhu, R., Ferraiolo, D., and Kuhn, R. 2000. The NIST model for role-based access control: Towards a unified standard. In Proceedings of the 5th ACM Workshop on Role-Based Access Control. 47–63.
    • Serbanuta et al. (2009) Serbanuta, T. F., Rosu, G., and Meseguer, J. 2009. A rewriting logic approach to operational semantics. Information and Computation 207, 305–340.
    • Somogyi et al. (1995) Somogyi, Z., Henderson, F. J., and Conway, T. C. 1995. Mercury, an efficient purely declarative logic programming language. Australian Computer Science Communications 17, 499–512.
    • Sterling and Shapiro (1994) Sterling, L. and Shapiro, E. 1994. The Art of Prolog, 2nd ed. MIT Press.
    • Swift et al. (2022) Swift, T., Warren, D. S., Sagonas, K., Freire, J., Rao, P., Cui, B., Johnson, E., de Castro, L., Marques, R. F., Saha, D., Dawson, S., and Kifer, M. 2022. The XSB System Version 5.0,x. http://xsb.sourceforge.net. Latest release May 12, 2022.
    • Tamaki and Sato (1986) Tamaki, H. and Sato, T. 1986. OLD resolution with tabulation. In Proceedings of the 3rd International Conference on Logic Programming. Springer, 84–98.
    • Tekle and Liu (2010) Tekle, K. T. and Liu, Y. A. 2010. Precise complexity analysis for efficient Datalog queries. In Proceedings of the 12th International ACM SIGPLAN Symposium on Principles and Practice of Declarative Programming. 35–44. https://doi.org/10.1145/1836089.1836094.
    • Tekle and Liu (2011) Tekle, K. T. and Liu, Y. A. 2011. More efficient Datalog queries: Subsumptive tabling beats magic sets. In Proceedings of the 2011 ACM SIGMOD International Conference on Management of Data. 661–672. http://doi.acm.org/10.1145/1989323.1989393.
    • Tong et al. (2023) Tong, Y., Lin, B., Liu, Y. A., and Stoller, S. D. 2023. ALDA. http://github.com/DistAlgo/alda. Accessed May 25, 2023.
    • Vennekens (2017) Vennekens, J. 2017. Lowering the learning curve for declarative programming: A Python API for the IDP system. In Proceedings of 19th International Symposium on Practical Aspects of Declarative Languages. Springer, 86–102.
    • Warren and Liu (2017) Warren, D. S. and Liu, Y. A. 2017. AppLP: A dialogue on applications of logic programming. Computing Research Repository arXiv:1704.02375 [cs.PL]. http://arxiv.org/abs/1704.02375.
    • Wright and Felleisen (1994) Wright, A. K. and Felleisen, M. 1994. A syntactic approach to type soundness. Information and Computation 115, 38–94.
    • Yang and Kifer (2000) Yang, G. and Kifer, M. 2000. FLORA: Implementing an efficient DOOD system using a tabling logic engine. In Proceedings of the 1st International Conference on Computational Logic. Springer, 1078–1093. https://doi.org/10.1007/3-540-44957-4_72.
    • Zhou (2016) Zhou, N.-F. 2016. Programming in Picat. In Proceedings of the 10th International Symposium on Rule Technologies: Research, Tools, and Applications. Springer, 3–18.

    Appendix A Formal Semantics

    We give a complete abstract syntax and formal semantics for our language. The operational semantics is a reduction semantics with evaluation contexts [70, 60]. It builds on the standard least fixed-point semantics for Datalog [16] and the formal operational semantics for DistAlgo [42]. Relative to the latter, we removed the constructs specific to distributed algorithms, added an abstract syntax for rule sets and calls to infer, added a transition rule for calls to infer, extended the state with a stack that keeps track of 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. The removed DistAlgo constructs can easily be restored; we removed them simply to avoid repeating them.

    A.1 Abstract syntax

    The abstract syntax is defined in Figures 23. Tuples are immutable values, not mutable objects. Sets and sequences are mutable objects. They are instances of the predefined classes set and sequence, respectively. Methods of set include add, del, contains, size, and any (which returns an element of the set, if the set is non-empty, otherwise it returns None). 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 not expressions, because they all have the side-effect of creating one or more new objects. Semantically, the for loop copies the contents of a (mutable) set or sequence into an (immutable) tuple before iterating over it, to ensure that changes to the set or sequence 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. We use some syntactic sugar in sample code, e.g., we use infix notation for some binary operators, such as is and and.

    We refer to rule sets defined in global scope and class scope as “global rule sets” and “class scope rule sets”, respectively.

    Note that method parameters are not variables and cannot be assigned to, and that methods do not have local variables. These choices simplify the semantics by eliminating the need for a call stack. The only local variables are local variables of rule sets. We refer to the other kinds of variables, namely global variables and instance variables, as non-local variables. For brevity, we use “variables” without a qualifier to refer to non-local variables.

    𝑃𝑟𝑜𝑔𝑟𝑎𝑚\it Program ::= 𝑅𝑢𝑙𝑒𝑠𝑒𝑡\it Ruleset* 𝐶𝑙𝑎𝑠𝑠\it Class* 𝑆𝑡𝑎𝑡𝑒𝑚𝑒𝑛𝑡\it Statement
    𝑅𝑢𝑙𝑒𝑠𝑒𝑡\it Ruleset ::= rules 𝑅𝑢𝑙𝑒𝑠𝑒𝑡𝑁𝑎𝑚𝑒\it RulesetName 𝑅𝑢𝑙𝑒\it Rule+
    𝑅𝑢𝑙𝑒\it Rule ::= 𝐷𝑒𝑟𝑖𝑣𝑒𝑑𝑃𝑟𝑒𝑑𝑖𝑐𝑎𝑡𝑒\it DerivedPredicate(𝑃𝑟𝑒𝑑𝑖𝑐𝑎𝑡𝑒𝐴𝑟𝑔\it PredicateArg*) if 𝐵𝑎𝑠𝑒𝑃𝑟𝑒𝑑𝑖𝑐𝑎𝑡𝑒\it BasePredicate(𝑃𝑟𝑒𝑑𝑖𝑐𝑎𝑡𝑒𝐴𝑟𝑔\it PredicateArg*)*
    𝐷𝑒𝑟𝑖𝑣𝑒𝑑𝑃𝑟𝑒𝑑𝑖𝑐𝑎𝑡𝑒\it DerivedPredicate ::= 𝐺𝑙𝑜𝑏𝑎𝑙𝑉𝑎𝑟𝑖𝑎𝑏𝑙𝑒\it GlobalVariable
    self.𝐹𝑖𝑒𝑙𝑑\it Field
    𝐿𝑜𝑐𝑎𝑙𝑉𝑎𝑟𝑖𝑎𝑏𝑙𝑒\it LocalVariable
    𝐵𝑎𝑠𝑒𝑃𝑟𝑒𝑑𝑖𝑐𝑎𝑡𝑒\it BasePredicate ::= 𝐺𝑙𝑜𝑏𝑎𝑙𝑉𝑎𝑟𝑖𝑎𝑏𝑙𝑒\it GlobalVariable[.𝐹𝑖𝑒𝑙𝑑\it Field*]
    self.𝐹𝑖𝑒𝑙𝑑\it Field+
    𝐿𝑜𝑐𝑎𝑙𝑉𝑎𝑟𝑖𝑎𝑏𝑙𝑒\it LocalVariable
    𝑃𝑟𝑒𝑑𝑖𝑐𝑎𝑡𝑒𝐴𝑟𝑔\it PredicateArg ::= 𝐿𝑜𝑔𝑖𝑐𝑉𝑎𝑟𝑖𝑎𝑏𝑙𝑒\it LogicVariable
    𝐿𝑖𝑡𝑒𝑟𝑎𝑙\it Literal
    𝐶𝑙𝑎𝑠𝑠\it Class ::= class 𝐶𝑙𝑎𝑠𝑠𝑁𝑎𝑚𝑒\it ClassName [extends 𝐶𝑙𝑎𝑠𝑠𝑁𝑎𝑚𝑒\it ClassName] : 𝑅𝑢𝑙𝑒𝑠𝑒𝑡\it Ruleset* 𝑀𝑒𝑡ℎ𝑜𝑑\it Method*
    𝑀𝑒𝑡ℎ𝑜𝑑\it Method ::= def 𝑀𝑒𝑡ℎ𝑜𝑑𝑁𝑎𝑚𝑒\it MethodName(𝑃𝑎𝑟𝑎𝑚𝑒𝑡𝑒𝑟\it Parameter*) 𝑆𝑡𝑎𝑡𝑒𝑚𝑒𝑛𝑡\it Statement
    defun 𝑀𝑒𝑡ℎ𝑜𝑑𝑁𝑎𝑚𝑒\it MethodName(𝑃𝑎𝑟𝑎𝑚𝑒𝑡𝑒𝑟\it Parameter*) 𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression
    𝑆𝑡𝑎𝑡𝑒𝑚𝑒𝑛𝑡\it Statement ::= 𝑁𝑜𝑛𝐿𝑜𝑐𝑎𝑙𝑉𝑎𝑟𝑖𝑎𝑏𝑙𝑒\it NonLocalVariable := 𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression
    𝑁𝑜𝑛𝐿𝑜𝑐𝑎𝑙𝑉𝑎𝑟𝑖𝑎𝑏𝑙𝑒\it NonLocalVariable := new 𝐶𝑙𝑎𝑠𝑠𝑁𝑎𝑚𝑒\it ClassName
    𝑁𝑜𝑛𝐿𝑜𝑐𝑎𝑙𝑉𝑎𝑟𝑖𝑎𝑏𝑙𝑒\it NonLocalVariable := { 𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression : 𝐼𝑡𝑒𝑟𝑎𝑡𝑜𝑟\it Iterator* | 𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression }
    𝑆𝑡𝑎𝑡𝑒𝑚𝑒𝑛𝑡\it Statement ; 𝑆𝑡𝑎𝑡𝑒𝑚𝑒𝑛𝑡\it Statement
    if 𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression : 𝑆𝑡𝑎𝑡𝑒𝑚𝑒𝑛𝑡\it Statement else : 𝑆𝑡𝑎𝑡𝑒𝑚𝑒𝑛𝑡\it Statement
    for 𝐼𝑡𝑒𝑟𝑎𝑡𝑜𝑟\it Iterator : 𝑆𝑡𝑎𝑡𝑒𝑚𝑒𝑛𝑡\it Statement
    while 𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression : 𝑆𝑡𝑎𝑡𝑒𝑚𝑒𝑛𝑡\it Statement
    ifSome 𝐼𝑡𝑒𝑟𝑎𝑡𝑜𝑟\it Iterator | 𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression : 𝑆𝑡𝑎𝑡𝑒𝑚𝑒𝑛𝑡\it Statement
    whileSome 𝐼𝑡𝑒𝑟𝑎𝑡𝑜𝑟\it Iterator | 𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression : 𝑆𝑡𝑎𝑡𝑒𝑚𝑒𝑛𝑡\it Statement
    𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression.𝑀𝑒𝑡ℎ𝑜𝑑𝑁𝑎𝑚𝑒\it MethodName(𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression*)
    𝑁𝑜𝑛𝐿𝑜𝑐𝑎𝑙𝑉𝑎𝑟𝑖𝑎𝑏𝑙𝑒\it NonLocalVariable* := [𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression.]infer( 𝑄𝑢𝑒𝑟𝑦\it Query*, 𝐾𝑒𝑦𝑤𝑜𝑟𝑑𝐴𝑟𝑔\it KeywordArg*,
    rules=𝑅𝑢𝑙𝑒𝑠𝑒𝑡𝑁𝑎𝑚𝑒\it RulesetName)
    skip
    𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression ::= 𝐿𝑖𝑡𝑒𝑟𝑎𝑙\it Literal
    𝑃𝑎𝑟𝑎𝑚𝑒𝑡𝑒𝑟\it Parameter
    𝑁𝑜𝑛𝐿𝑜𝑐𝑎𝑙𝑉𝑎𝑟𝑖𝑎𝑏𝑙𝑒\it NonLocalVariable
    𝑇𝑢𝑝𝑙𝑒\it Tuple
    𝑈𝑛𝑎𝑟𝑦𝑂𝑝\it UnaryOp(𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression)
    𝐵𝑖𝑛𝑎𝑟𝑦𝑂𝑝\it BinaryOp(𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression,𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression)
    isinstance(𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression,𝐶𝑙𝑎𝑠𝑠𝑁𝑎𝑚𝑒\it ClassName)
    and(𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression,𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression) / / conjunction (short-circuiting)
    or(𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression,𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression) / / disjunction (short-circuiting)
    each 𝐼𝑡𝑒𝑟𝑎𝑡𝑜𝑟\it Iterator | 𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression
    some 𝐼𝑡𝑒𝑟𝑎𝑡𝑜𝑟\it Iterator | 𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression
    𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression.𝑀𝑒𝑡ℎ𝑜𝑑𝑁𝑎𝑚𝑒\it MethodName(𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression*)
    Figure 2: Abstract syntax, Part 1.
    𝑁𝑜𝑛𝐿𝑜𝑐𝑎𝑙𝑉𝑎𝑟𝑖𝑎𝑏𝑙𝑒\it NonLocalVariable := 𝐺𝑙𝑜𝑏𝑎𝑙𝑉𝑎𝑟𝑖𝑎𝑏𝑙𝑒\it GlobalVariable
    𝐼𝑛𝑠𝑡𝑎𝑛𝑐𝑒𝑉𝑎𝑟𝑖𝑎𝑏𝑙𝑒\it InstanceVariable
    𝐼𝑛𝑠𝑡𝑎𝑛𝑐𝑒𝑉𝑎𝑟𝑖𝑎𝑏𝑙𝑒\it InstanceVariable ::= 𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression.𝐹𝑖𝑒𝑙𝑑\it Field
    𝐿𝑖𝑡𝑒𝑟𝑎𝑙\it Literal ::= None
    Bool
    Int
    Bool ::= True
    False
    Int ::= …
    𝐼𝑡𝑒𝑟𝑎𝑡𝑜𝑟\it Iterator ::= 𝑃𝑎𝑡𝑡𝑒𝑟𝑛\it Pattern in 𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression
    𝑃𝑎𝑡𝑡𝑒𝑟𝑛\it Pattern ::= 𝑁𝑜𝑛𝐿𝑜𝑐𝑎𝑙𝑉𝑎𝑟𝑖𝑎𝑏𝑙𝑒\it NonLocalVariable
    𝑇𝑢𝑝𝑙𝑒𝑃𝑎𝑡𝑡𝑒𝑟𝑛\it TuplePattern
    𝑇𝑢𝑝𝑙𝑒𝑃𝑎𝑡𝑡𝑒𝑟𝑛\it TuplePattern ::= (𝑃𝑎𝑡𝑡𝑒𝑟𝑛𝐸𝑙𝑒𝑚𝑒𝑛𝑡\it PatternElement*)
    𝑃𝑎𝑡𝑡𝑒𝑟𝑛𝐸𝑙𝑒𝑚𝑒𝑛𝑡\it PatternElement ::= 𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression
    _
    =𝑁𝑜𝑛𝐿𝑜𝑐𝑎𝑙𝑉𝑎𝑟𝑖𝑎𝑏𝑙𝑒\it NonLocalVariable
    𝑄𝑢𝑒𝑟𝑦\it Query := 𝑃𝑟𝑒𝑑𝑖𝑐𝑎𝑡𝑒\it Predicate[𝑇𝑢𝑝𝑙𝑒𝑃𝑎𝑡𝑡𝑒𝑟𝑛\it TuplePattern]
    𝐾𝑒𝑦𝑤𝑜𝑟𝑑𝐴𝑟𝑔\it KeywordArg ::= 𝐿𝑜𝑐𝑎𝑙𝑉𝑎𝑟𝑖𝑎𝑏𝑙𝑒\it LocalVariable = 𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression
    𝑇𝑢𝑝𝑙𝑒\it Tuple ::= (𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression*)
    𝑈𝑛𝑎𝑟𝑦𝑂𝑝\it UnaryOp ::= not / / Boolean negation
    isTuple / / test whether a value is a tuple
    len / / length of a tuple
    𝐵𝑖𝑛𝑎𝑟𝑦𝑂𝑝\it BinaryOp ::= is / / identity-based equality
    plus / / sum
    select / / select(tt,ii) returns the ii’th component of tuple tt
    Figure 3: Abstract syntax, Part 2. Ellipses (“…”) are used for common syntactic categories whose details are unimportant. Details of the identifiers allowed for non-terminals 𝑅𝑢𝑙𝑒𝑠𝑒𝑡𝑁𝑎𝑚𝑒\it RulesetName, 𝐺𝑙𝑜𝑏𝑎𝑙𝑉𝑎𝑟𝑖𝑎𝑏𝑙𝑒\it GlobalVariable, 𝐹𝑖𝑒𝑙𝑑\it Field, 𝐿𝑜𝑐𝑎𝑙𝑉𝑎𝑟𝑖𝑎𝑏𝑙𝑒\it LocalVariable, 𝐿𝑜𝑔𝑖𝑐𝑉𝑎𝑟𝑖𝑎𝑏𝑙𝑒\it LogicVariable, 𝐶𝑙𝑎𝑠𝑠𝑁𝑎𝑚𝑒\it ClassName, 𝑀𝑒𝑡ℎ𝑜𝑑𝑁𝑎𝑚𝑒\it MethodName, and 𝑃𝑎𝑟𝑎𝑚𝑒𝑡𝑒𝑟\it Parameter are also unimportant and hence unspecified, except that 𝐶𝑙𝑎𝑠𝑠𝑁𝑎𝑚𝑒\it ClassName must include set and sequence, and 𝑃𝑎𝑟𝑎𝑚𝑒𝑡𝑒𝑟\it Parameter must include self.

    Notation in the grammar.   A symbol in the grammar is a terminal symbol if it is in typewriter font or 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”.

    Well-formedness requirements on programs.   In global rule sets, predicates cannot contain self. In class scope rule sets, derived predicates cannot be global variables. Each global variable appears as a derived predicate in at most one rule set in the program. In each class, for each field ff, self.ff appears as a derived predicate in at most one rule set in that class. In each rule in each rule set, each logic variable that appears in the conclusion appears in a hypothesis. Within each rule set, all uses of the same predicate have the same number of arguments.

    In assignments with a call to infer on the right side of the assignment, the number of variables on the left of the assignment equals the number of queries, the predicates queried are derived predicates of the rule set used, and local variables in keyword arguments are base predicates of the rule set used.

    Invocations of methods defined using def appear only as statements. Invocations of methods defined using defun appear only as expressions; we also refer to these methods as “functions”. The program does not contain definitions of classes named set and sequence.

    Class names are unique; in other words, each class name is defined at most once. Method names are unique within the scope of each class. Rule set names are unique within each scope.

    A.1.1 Constructs whose semantics is given by translation

    Notation.   A (partial) function is represented as a set of mappings xyx\mapsto y. We represent substitutions as functions from parameters and variables to expressions. tθt\,\theta denotes the result of applying substitution θ\theta to tt.

    Class scope rule set names.   The program is transformed so that rule set names are unique across all scopes. A straightforward way to do this is to prefix the name of every class scope rule set with the name of the enclosing class.

    Global variables.   Global variables are replaced with instance variables, and global rule sets are transformed to have the same form as class scope rule sets, by the following transformations. Choose an address a𝑔𝑣a_{\it gv} whose fields will be used to represent global variables. Everywhere except in global rule sets and in queries in calls to infer on global rule sets, replace each global variable xx with a𝑔𝑣.xa_{\it gv}.x. Introduce a class name C𝑔𝑣C_{\it gv}, put all global rule sets into this class, and replace each global variable xx with self.xx in those rule sets and in queries in calls to infer on those rule sets. Calls to infer on those rule sets are also transformed by prefixing a𝑔𝑣.a_{\it gv}{\tt.} to the call, i.e., 𝚒𝚗𝚏𝚎𝚛(){\color[rgb]{0,0,1}\tt infer}(\cdots) is replaced with a𝑔𝑣.𝚒𝚗𝚏𝚎𝚛()a_{\it gv}{\tt.}{\color[rgb]{0,0,1}\tt infer}{\tt(}\cdots{\tt)}, so all calls to infer have a target object. The initial state of the program is defined so that an object of type C𝑔𝑣C_{\it gv} is at address a𝑔𝑣a_{\it gv}. These transformations simplify the transition rules related to inference, by allowing global rule sets and class scope rule sets to be handled in a uniform way.

    Boolean operators.   The Boolean operators and and each are eliminated as follows: e1e_{1} and e2e_{2} is replaced with not(not(e1e_{1}) or not(e2e_{2})), and each iter | ee is replaced with not(some iter | not(ee)).

    Non-variable expressions in tuple patterns.   Non-variable expressions in tuple patterns are replaced with variables prefixed by “=”. Specifically, for each expression ee in a tuple pattern that is not a variable, a variable prefixed with “=”, or wildcard, an assignment vv := ee to a fresh variable vv is inserted before the statement that contains the tuple pattern, and ee is replaced with =vv in the tuple pattern.

    Wildcards.   Wildcards are eliminated from tuple patterns in for loops, comprehensions, and quantifications (i.e., everywhere except as 𝑄𝑢𝑒𝑟𝑦\it Query in infer) 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 x1,,xnx_{1},\ldots,x_{n} := [e.][e.]infer(p1(𝑝𝑎𝑡1),p_{1}({\it pat}_{1}), ,\ldots, pn(patn),𝑘𝑤𝑎𝑟𝑔𝑠,p_{n}(pat_{n}),{\it kwargs}, rules=rsrs). Let xi,1,x_{i,1}, ,\ldots, xi,kix_{i,k_{i}} be the components of 𝑝𝑎𝑡i{\it pat}_{i}, in order and without repetitions, that are variables not prefixed by “=”. Let y1,,yny_{1},\ldots,y_{n} be fresh variables. The above statement is transformed to:

    y1,,yny_{1},\ldots,y_{n} := [e.][e.]infer(p1,,pn,𝑘𝑤𝑎𝑟𝑔𝑠,p_{1},\ldots,p_{n},{\it kwargs}, rules=rsrs)
    x1x_{1} := { (x1,1,,x1,k1x_{1,1},\ldots,x_{1,k_{1}}) : 𝑝𝑎𝑡1{\it pat}_{1} in y1y_{1} | True }
    \ldots
    xnx_{n} := { (xn,1,,xn,knx_{n,1},\ldots,x_{n,k_{n}}) : 𝑝𝑎𝑡n{\it pat}_{n} in yny_{n} | True }
    

    ifSome statements.   ifSome is statically eliminated as follows. Consider the statement ifSome pat in ee | bbss. Let i1,,iki_{1},\ldots,i_{k} be indices, in order of appearance from left to right, of elements of pat that are variables not prefixed by “=”. Let xi1,,xikx_{i_{1}},\ldots,x_{i_{k}} be those variables. Let foundOne and xi1,,xikx^{\prime}_{i_{1}},\ldots,x^{\prime}_{i_{k}} be fresh variables. Let substitution θ\theta be [xi1xi1,,xikxik][x_{i_{1}}\mapsto x^{\prime}_{i_{1}},\ldots,x_{i_{k}}\mapsto x^{\prime}_{i_{k}}]. Let 𝑝𝑎𝑡=𝑝𝑎𝑡θ{\it pat}^{\prime}={\it pat}\,\theta and b=bθb^{\prime}=b\,\theta. The above ifSome statement is transformed to:

    foundOne := False
    for patpat^{\prime} in ee:
      if bb^{\prime} and not foundOne:
        xi1:=xi1x_{i_{1}}:=x^{\prime}_{i_{1}}
        \ldots
        xik:=xikx_{i_{k}}:=x^{\prime}_{i_{k}}
        ss
        foundOne := True
    

    whileSome statements.   whileSome is statically eliminated as follows. Consider the statement whileSome pat in ee | bbss. Using the same definitions as in the previous item, this statement is transformed to:

    foundOne := True
    while foundOne:
      foundOne := False
      for patpat^{\prime} in ee:
        if bb^{\prime} and not foundOne:
          xi1:=xi1x_{i_{1}}:=x^{\prime}_{i_{1}}
          \ldots
          xik:=xikx_{i_{k}}:=x^{\prime}_{i_{k}}
          ss
          foundOne := True
    

    Comprehensions.   First, comprehensions are transformed to eliminate the use of variables prefixed with “=”. Specifically, for a variable xx prefixed with “=” in a comprehension, replace occurrences of =x in the comprehension with occurrences of a fresh variable yy, and add the conjunct yy is xx to the Boolean condition. Second, all comprehensions are statically eliminated as follows. The comprehension xx := { ee | pat1pat_{1} in e1e_{1}, \ldots, patnpat_{n} in ene_{n} | bb } is replaced with

    xx := new set
    for pat1pat_{1} in e1e_{1}:
      ...
        for patnpat_{n} in ene_{n}:
          if bb:
            xx.add(ee)
    

    Tuple patterns in iterators.   Iterators containing tuple patterns are rewritten as iterators without tuple patterns.

    Consider the existential quantification some (e1,,ene_{1},\ldots,e_{n}) in ee | bb. Let xx be a fresh variable. Let θ\theta be the substitution that replaces eie_{i} with select(xx,ii) for each ii such that eie_{i} is a variable not prefixed with “=”. Let {j1,,jm}\{j_{1},\ldots,j_{m}\} contain the indices of the constants and the variables prefixed with “=” in (e1,,ene_{1},\ldots,e_{n}). Let e¯j\bar{e}_{j} denote eje_{j} after removing the “=” prefix, if any. The quantification is rewritten as some xx in ee | isTuple(xx) and len(xx) is nn and (select(xx,j1j_{1}), \ldots, select(xx,jmj_{m})) is (e¯j1\bar{e}_{j_{1}}, \ldots, e¯jm\bar{e}_{j_{m}}) and bθb\,\theta.

    Consider the loop for (e1,,ene_{1},\ldots,e_{n}) in eess. Let xx and SS be fresh variables. Let {i1,,ik}\{i_{1},\ldots,i_{k}\} contain the indices in (e1,,ene_{1},\ldots,e_{n}) of variables not prefixed with “=”. Let {j1,,jm}\{j_{1},\ldots,j_{m}\} be as in the previous paragraph. Let e¯j\bar{e}_{j} denote eje_{j} after removing the “=” prefix, if any. Note that ee may evaluate to a set or sequence, and duplicate bindings for the tuple of variables (ei1,,eik)(e_{i_{1}},\ldots,e_{i_{k}}) are filtered out if ee evaluates to a set but not if ee evaluates to a sequence. The loop is rewritten as the code in Figure 4.

        SS := ee
        if isinstance(SS,set):
          SS := { xx : xx in SS | isTuple(xx) and len(xx) is nn
              and (select(xx,j1j_{1}), \ldots, select(xx,jmj_{m}))
                 is (e¯j1\bar{e}_{j_{1}}, \ldots, e¯jm\bar{e}_{j_{m}}) }
          for xx in SS:
            ei1:=𝚜𝚎𝚕𝚎𝚌𝚝(x,i1)e_{i_{1}}:={\color[rgb]{0,0,1}\tt select}(x,i_{1})
            \ldots
            eik:=𝚜𝚎𝚕𝚎𝚌𝚝(x,ik)e_{i_{k}}:={\color[rgb]{0,0,1}\tt select}(x,i_{k})
            ss
        else:  / / SS is a sequence
          for xx in SS:
            if (isTuple(xx) and len(xx) is nn
                and (select(xx,j1j_{1}), \ldots, select(xx,jmj_{m}))
                   is (e¯j1\bar{e}_{j_{1}}, \ldots, e¯jm\bar{e}_{j_{m}}):
              ei1:=𝚜𝚎𝚕𝚎𝚌𝚝(x,i1)e_{i_{1}}:={\color[rgb]{0,0,1}\tt select}(x,i_{1})
              \ldots
              eik:=𝚜𝚎𝚕𝚎𝚌𝚝(x,ik)e_{i_{k}}:={\color[rgb]{0,0,1}\tt select}(x,i_{k})
              ss
            else:
              skip
    
    Figure 4: Translation of for loop to eliminate tuple pattern.

    A.2 Semantic domains

    The semantic domains are defined in Figure 5, using the following notation. DD^{*} is the set of finite sequences of values from domain DD. Set(D){\rm Set}(D) is the set of finite sets of values from domain DD. D1D2D_{1}\rightarrow D_{2} and D1D2D_{1}\rightharpoonup D_{2} are the sets of (total) functions and partial functions, respectively, from D1D_{1} to D2D_{2}. 𝑑𝑜𝑚(f){\it dom}(f) and 𝑟𝑎𝑛𝑔𝑒(f){\it range}(f) are the domain and range, respectively, of a partial function ff, i.e., 𝑑𝑜𝑚(f)={x|y:xyf}{\it dom}(f)=\{x\;|\;\exists y:x\mapsto y\in f\} and 𝑟𝑎𝑛𝑔𝑒(f)={y|x:xyf}{\it range}(f)=\{y\;|\;\exists x:x\mapsto y\in f\}.

    In a state (s,h,ht)(s,h,ht), ss is the statement to be executed, hh is the heap that maps an address to the object at that address, and htht is the heap type map that maps an address to the type of the object on the heap at that address.

    𝐵𝑜𝑜𝑙\displaystyle{\it Bool} =\displaystyle= {𝚃𝚛𝚞𝚎,𝙵𝚊𝚕𝚜𝚎}\displaystyle\{{\color[rgb]{0,0,1}\tt True},{\color[rgb]{0,0,1}\tt False}\}
    𝐼𝑛𝑡\displaystyle{\it Int} =\displaystyle= \displaystyle...
    𝐴𝑑𝑑𝑟𝑒𝑠𝑠\it Address =\displaystyle= \displaystyle...
    𝑇𝑢𝑝𝑙𝑒\it Tuple =\displaystyle= 𝑉𝑎𝑙\displaystyle\mbox{$\it Val$}^{*}
    𝑉𝑎𝑙\it Val =\displaystyle= 𝐵𝑜𝑜𝑙𝐼𝑛𝑡𝐴𝑑𝑑𝑟𝑒𝑠𝑠𝑇𝑢𝑝𝑙𝑒{𝙽𝚘𝚗𝚎}\displaystyle{\it Bool}\cup{\it Int}\cup{\mbox{$\it Address$}}\cup{\mbox{$\it Tuple$}}\cup\{{\color[rgb]{0,0,1}\tt None}\}
    𝑂𝑏𝑗𝑒𝑐𝑡\displaystyle{\it Object} =\displaystyle= (𝐹𝑖𝑒𝑙𝑑𝑉𝑎𝑙)Set(𝑉𝑎𝑙)𝑉𝑎𝑙\displaystyle({\mbox{$\it Field$}}\rightharpoonup{\mbox{$\it Val$}})\cup{\rm Set}({\mbox{$\it Val$}})\cup{\mbox{$\it Val$}}^{*}
    𝐻𝑒𝑎𝑝𝑇𝑦𝑝𝑒\displaystyle{\it HeapType} =\displaystyle= 𝐴𝑑𝑑𝑟𝑒𝑠𝑠𝐶𝑙𝑎𝑠𝑠𝑁𝑎𝑚𝑒\displaystyle\mbox{$\it Address$}\rightharpoonup{\mbox{$\it ClassName$}}
    𝐻𝑒𝑎𝑝\displaystyle{\it Heap} =\displaystyle= 𝐴𝑑𝑑𝑟𝑒𝑠𝑠𝑂𝑏𝑗𝑒𝑐𝑡\displaystyle\mbox{$\it Address$}\rightharpoonup{\it Object}
    𝑆𝑡𝑎𝑡𝑒\displaystyle{\it State} =\displaystyle= 𝑆𝑡𝑎𝑡𝑒𝑚𝑒𝑛𝑡×𝐻𝑒𝑎𝑝×𝐻𝑒𝑎𝑝𝑇𝑦𝑝𝑒\displaystyle\mbox{$\it Statement$}\times{\it Heap}\times{\it HeapType}
    Figure 5: Semantic domains. Ellipses are used for semantic domains of primitive values whose details are standard or unimportant.

    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 during evaluation. The new productions appear below. The statement for vv inTuple ttss iterates over the elements of tuple tt, in the obvious way.

    𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression ::= Address
    Address.𝐹𝑖𝑒𝑙𝑑\it Field
    𝑆𝑡𝑎𝑡𝑒𝑚𝑒𝑛𝑡\it Statement ::= for Variable inTuple 𝑇𝑢𝑝𝑙𝑒\it Tuple: 𝑆𝑡𝑎𝑡𝑒𝑚𝑒𝑛𝑡\it Statement

    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 6. 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 (lines 3–4 of Figure 6) says 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.

    C\it C ::= [ ]
    (𝑉𝑎𝑙\it Val*, C\it C, 𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression*)
    C\it C.𝑀𝑒𝑡ℎ𝑜𝑑𝑁𝑎𝑚𝑒\it MethodName(𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression*)
    𝐴𝑑𝑑𝑟𝑒𝑠𝑠\it Address.𝑀𝑒𝑡ℎ𝑜𝑑𝑁𝑎𝑚𝑒\it MethodName(𝑉𝑎𝑙\it Val*, C\it C, 𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression*)
    𝑈𝑛𝑎𝑟𝑦𝑂𝑝\it UnaryOp(C\it C)
    𝐵𝑖𝑛𝑎𝑟𝑦𝑂𝑝\it BinaryOp(C\it C, 𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression)
    𝐵𝑖𝑛𝑎𝑟𝑦𝑂𝑝\it BinaryOp(𝑉𝑎𝑙\it Val, C\it C)
    isinstance(C\it C, 𝐶𝑙𝑎𝑠𝑠𝑁𝑎𝑚𝑒\it ClassName)
    or(C\it C, 𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression)
    some 𝑃𝑎𝑡𝑡𝑒𝑟𝑛\it Pattern in C\it C | 𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression
    C\it C.𝐹𝑖𝑒𝑙𝑑\it Field := 𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression
    C\it C.𝐹𝑖𝑒𝑙𝑑\it Field := new 𝐶𝑙𝑎𝑠𝑠𝑁𝑎𝑚𝑒\it ClassName
    𝐴𝑑𝑑𝑟𝑒𝑠𝑠\it Address.𝐹𝑖𝑒𝑙𝑑\it Field := C\it C
    C\it C ; 𝑆𝑡𝑎𝑡𝑒𝑚𝑒𝑛𝑡\it Statement
    if C\it C: 𝑆𝑡𝑎𝑡𝑒𝑚𝑒𝑛𝑡\it Statement else: 𝑆𝑡𝑎𝑡𝑒𝑚𝑒𝑛𝑡\it Statement
    for 𝐼𝑛𝑠𝑡𝑎𝑛𝑐𝑒𝑉𝑎𝑟𝑖𝑎𝑏𝑙𝑒\it InstanceVariable in C\it C: 𝑆𝑡𝑎𝑡𝑒𝑚𝑒𝑛𝑡\it Statement
    for 𝐼𝑛𝑠𝑡𝑎𝑛𝑐𝑒𝑉𝑎𝑟𝑖𝑎𝑏𝑙𝑒\it InstanceVariable inTuple 𝑇𝑢𝑝𝑙𝑒\it Tuple: C\it C
    (𝐴𝑑𝑑𝑟𝑒𝑠𝑠\it Address.𝐹𝑖𝑒𝑙𝑑\it Field)*, C\it C.𝐹𝑖𝑒𝑙𝑑\it Field, (𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression.𝐹𝑖𝑒𝑙𝑑\it Field)* :=
       [𝐸𝑥𝑝𝑟𝑒𝑠𝑠𝑖𝑜𝑛\it Expression.]infer( 𝑄𝑢𝑒𝑟𝑦\it Query*, 𝐾𝑒𝑦𝑤𝑜𝑟𝑑𝐴𝑟𝑔\it KeywordArg*,
    rules=𝑅𝑢𝑙𝑒𝑠𝑒𝑡𝑁𝑎𝑚𝑒\it RulesetName)
    (𝐴𝑑𝑑𝑟𝑒𝑠𝑠\it Address.𝐹𝑖𝑒𝑙𝑑\it Field)* := C\it C.infer( 𝑄𝑢𝑒𝑟𝑦\it Query*, 𝐾𝑒𝑦𝑤𝑜𝑟𝑑𝐴𝑟𝑔\it KeywordArg*,
    rules=𝑅𝑢𝑙𝑒𝑠𝑒𝑡𝑁𝑎𝑚𝑒\it RulesetName)
    (𝐴𝑑𝑑𝑟𝑒𝑠𝑠\it Address.𝐹𝑖𝑒𝑙𝑑\it Field)* :=
       [𝐴𝑑𝑑𝑟𝑒𝑠𝑠\it Address.]infer(𝑄𝑢𝑒𝑟𝑦\it Query*, (𝑃𝑎𝑟𝑎𝑚𝑒𝑡𝑒𝑟\it Parameter=𝑉𝑎𝑙\it Val)*,
       𝑃𝑎𝑟𝑎𝑚𝑒𝑡𝑒𝑟\it Parameter=C\it C, 𝐾𝑒𝑦𝑤𝑜𝑟𝑑𝐴𝑟𝑔\it KeywordArg*, rules=𝑅𝑢𝑙𝑒𝑠𝑒𝑡𝑁𝑎𝑚𝑒\it RulesetName)
    Figure 6: Evaluation contexts for expressions and statements.

    A.5 Transition relations

    The transition relation for expressions has the form h,hteeh,ht\vdash e\rightarrow e^{\prime}, where ee and ee^{\prime} are expressions, h𝐻𝑒𝑎𝑝h\in{\it Heap}, and ht𝐻𝑒𝑎𝑝𝑇𝑦𝑝𝑒ht\in{\it HeapType}. The transition relation for statements has the form 𝑠𝑡𝑎𝑡𝑒𝑠𝑡𝑎𝑡𝑒{\it state}\rightarrow{\it state}^{\prime} where 𝑠𝑡𝑎𝑡𝑒𝑆𝑡𝑎𝑡𝑒{\it state}\in{\it State} and 𝑠𝑡𝑎𝑡𝑒𝑆𝑡𝑎𝑡𝑒{\it state}^{\prime}\in{\it 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 7. The transition relation for statements is defined in Figures 910, using auxiliary functions defined in Figure 8. The context rules for expressions and statements at the top of Figure 9 allow the expression or statement in the evaluation context’s hole to take a transition, while the rest of the program, denoted by CC, is carried along unchanged.

    / / field accessh,hta.fh(a)(f)if ht(a){𝚜𝚎𝚝,𝚜𝚎𝚚}f𝑑𝑜𝑚(h(a))/ / invoke function in user-defined classh,hta.m(v1,,vn)e[𝚜𝚎𝚕𝚏a,x1v1,,xnvn]if 𝑚𝑒𝑡ℎ𝑜𝑑𝐷𝑒𝑓(ht(a),m,𝚍𝚎𝚏𝚞𝚗m(x1,,xn)e)/ / invoke function in pre-defined class (example)h,hta.𝚊𝚗𝚢()vif ht(a)=𝚜𝚎𝚝vh(a)h,hta.𝚊𝚗𝚢()𝙽𝚘𝚗𝚎if ht(a)=𝚜𝚎𝚝h(a)=/ / unary operationsh,ht𝚗𝚘𝚝(𝚃𝚛𝚞𝚎)𝙵𝚊𝚕𝚜𝚎h,ht𝚗𝚘𝚝(𝙵𝚊𝚕𝚜𝚎)𝚃𝚛𝚞𝚎h,ht𝚒𝚜𝚃𝚞𝚙𝚕𝚎(v)𝚃𝚛𝚞𝚎if v is a tupleh,ht𝚒𝚜𝚃𝚞𝚙𝚕𝚎(v)𝙵𝚊𝚕𝚜𝚎if v is not a tupleh,ht𝚕𝚎𝚗(v)nif v is a tuple with n components/ / binary operationsh,ht𝚒𝚜(v1,v2)𝚃𝚛𝚞𝚎if v1 and v2 are the same (identical) valueh,ht𝚙𝚕𝚞𝚜(v1,v2)v3if v1𝐼𝑛𝑡v2𝐼𝑛𝑡v3=v1+v2h,ht𝚜𝚎𝚕𝚎𝚌𝚝(v1,v2)v3if v2𝐼𝑛𝑡v2>0(v1 is a tuple with length at least v2)(v3 is the v2’th component of v1)/ / isinstanceh,ht𝚒𝚜𝚒𝚗𝚜𝚝𝚊𝚗𝚌𝚎(a,c)𝚃𝚛𝚞𝚎if ht(a)=ch,ht𝚒𝚜𝚒𝚗𝚜𝚝𝚊𝚗𝚌𝚎(a,c)𝙵𝚊𝚕𝚜𝚎if ht(a)c/ / disjunctionh,ht𝚘𝚛(𝚃𝚛𝚞𝚎,e)𝚃𝚛𝚞𝚎h,ht𝚘𝚛(𝙵𝚊𝚕𝚜𝚎,e)e/ / existential quantificationh,ht𝚜𝚘𝚖𝚎x𝚒𝚗a|ee[xv1]𝚘𝚛𝚘𝚛e[xvn]if (ht(a)=𝚜𝚎𝚚𝚞𝚎𝚗𝚌𝚎h(a)=v1,,vn)(ht(a)=𝚜𝚎𝚝v1,,vnis a linearization ofh(a))\begin{array}[]{@{}l@{}}\mbox{/\,/\ field access}\\ h,ht\vdash a.f\rightarrow h(a)(f)\hskip 10.00002pt\mbox{if }ht(a)\not\in\{{\color[rgb]{0,0,1}\tt set},{\color[rgb]{0,0,1}\tt seq}\}\land f\in{\it dom}(h(a))\\ \\ \mbox{/\,/\ invoke function in user-defined class}\\ h,ht\vdash a{\tt.}m(v_{1},\ldots,v_{n})\rightarrow e[{\color[rgb]{0,0,1}\tt self}\mapsto a,x_{1}\mapsto v_{1},\ldots,x_{n}\mapsto v_{n}]\\ \hskip 7.5pt\mbox{if }{\it methodDef}(ht(a),m,{\color[rgb]{0,0,1}\tt defun}~m(x_{1},\ldots,x_{n})~e)\\ \\ \mbox{/\,/\ invoke function in pre-defined class (example)}\\ h,ht\vdash a{\tt.{\color[rgb]{0,0,1}any}()}\rightarrow v\hskip 10.00002pt\mbox{if }ht(a)={\color[rgb]{0,0,1}\tt set}\land v\in h(a)\\ h,ht\vdash a{\tt.{\color[rgb]{0,0,1}any}()}\rightarrow{\color[rgb]{0,0,1}\tt None}\hskip 10.00002pt\mbox{if }ht(a)={\color[rgb]{0,0,1}\tt set}\land h(a)=\emptyset\\ \\ \mbox{/\,/\ unary operations}\\ h,ht\vdash{\tt{\color[rgb]{0,0,1}not}({\color[rgb]{0,0,1}True})}\rightarrow{\color[rgb]{0,0,1}\tt False}\\ h,ht\vdash{\tt{\color[rgb]{0,0,1}not}({\color[rgb]{0,0,1}False})}\rightarrow{\color[rgb]{0,0,1}\tt True}\\ h,ht\vdash{\color[rgb]{0,0,1}\tt isTuple}{\tt(}v{\tt)}\rightarrow{\color[rgb]{0,0,1}\tt True}\hskip 10.00002pt\mbox{if $v$ is a tuple}\\ h,ht\vdash{\color[rgb]{0,0,1}\tt isTuple}{\tt(}v{\tt)}\rightarrow{\color[rgb]{0,0,1}\tt False}\hskip 10.00002pt\mbox{if $v$ is not a tuple}\\ h,ht\vdash{\color[rgb]{0,0,1}\tt len}{\tt(}v{\tt)}\rightarrow n\hskip 10.00002pt\mbox{if $v$ is a tuple with $n$ components}\\ \\ \mbox{/\,/\ binary operations}\\ h,ht\vdash{\tt{\color[rgb]{0,0,1}is}(}v_{1},v_{2}{\tt)}\rightarrow{\color[rgb]{0,0,1}\tt True}\\ \hskip 7.5pt\mbox{if $v_{1}$ and $v_{2}$ are the same (identical) value}\\ \\ h,ht\vdash{\tt{\color[rgb]{0,0,1}plus}(}v_{1},v_{2}{\tt)}\rightarrow v_{3}\\ \hskip 7.5pt\mbox{if }v_{1}\in{\it Int}\land v_{2}\in{\it Int}\land v_{3}=v_{1}+v_{2}\\ \\ h,ht\vdash{\tt{\color[rgb]{0,0,1}select}(}v_{1},v_{2}{\tt)}\rightarrow v_{3}\\ \hskip 7.5pt\mbox{if }v_{2}\in{\it Int}\land v_{2}>0\land\mbox{($v_{1}$ is a tuple with length at least $v_{2}$)}\\ \hskip 7.5pt{}\land\mbox{($v_{3}$ is the $v_{2}$'th component of $v_{1}$)}\\ \\ \mbox{/\,/\ isinstance}\\ h,ht\vdash{\tt{\color[rgb]{0,0,1}isinstance}(}a,c{\tt)}\rightarrow{\color[rgb]{0,0,1}\tt True}\hskip 10.00002pt\mbox{if }ht(a)=c\\ h,ht\vdash{\tt{\color[rgb]{0,0,1}isinstance}(}a,c{\tt)}\rightarrow{\color[rgb]{0,0,1}\tt False}\hskip 10.00002pt\mbox{if }ht(a)\neq c\\ \\ \mbox{/\,/\ disjunction}\\ h,ht\vdash{\tt{\color[rgb]{0,0,1}or}({\color[rgb]{0,0,1}True}},e{\tt)}\rightarrow{\color[rgb]{0,0,1}\tt True}\\ h,ht\vdash{\tt{\color[rgb]{0,0,1}or}({\color[rgb]{0,0,1}False}},e{\tt)}\rightarrow e\\ \\ \mbox{/\,/\ existential quantification}\\ h,ht\vdash{\color[rgb]{0,0,1}\tt some}~x~{\color[rgb]{0,0,1}\tt in}~a~|~e~~\rightarrow~~e[x\mapsto v_{1}]~{\tt{\color[rgb]{0,0,1}or}}~\cdots~{\color[rgb]{0,0,1}\tt or}~e[x\mapsto v_{n}]\\ \hskip 7.5pt\mbox{if }(ht(a)={\color[rgb]{0,0,1}\tt sequence}\land h(a)=\langle v_{1},\ldots,v_{n}\rangle)\\ \hskip 7.5pt{}\lor(ht(a)={\color[rgb]{0,0,1}\tt set}\land\langle v_{1},\ldots,v_{n}\rangle~\mbox{is a linearization of}~h(a))\end{array}
    Figure 7: Transition relation for expressions.
    𝑑𝑒𝑟𝑒𝑓(h,a,F)=if a𝑑𝑜𝑚(h) then elif 𝑙𝑒𝑛𝑔𝑡ℎ(F)=1 then (if F𝑑𝑜𝑚(h(a)) then h(a)(F) else )else 𝑓𝑖𝑟𝑠𝑡(F)𝑑𝑜𝑚(h(a)) then 𝑑𝑒𝑟𝑒𝑓(h,h(a)(𝑓𝑖𝑟𝑠𝑡(F)),𝑟𝑒𝑠𝑡(path)) else 𝑎𝑙𝑙𝐵𝑎𝑠𝑒𝐴𝑟𝑒𝑆𝑒𝑡𝑠(h,ht)=a𝑑𝑜𝑚(h),𝑟𝑠𝑟𝑢𝑙𝑒𝑠𝑒𝑡𝑠(ht(a)),𝚜𝚎𝚕𝚏.F𝑛𝑙𝐵𝑎𝑠𝑒(𝑟𝑢𝑙𝑒𝑠(𝑟𝑠)):𝑑𝑒𝑟𝑒𝑓(h,a,F)=(𝑑𝑒𝑟𝑒𝑓(h,a,F)𝐴𝑑𝑑𝑟𝑒𝑠𝑠ht(𝑑𝑒𝑟𝑒𝑓(h,a,F))=𝚜𝚎𝚝)𝑢𝑝𝑑𝑎𝑡𝑒𝑉𝑎𝑟(h,𝑟𝑠,a.f,S)=if h(a)(f)𝐴𝑑𝑑𝑟𝑒𝑠𝑠 then {h(a)(f)S}else {ah(a)[f𝑛𝑒𝑤𝐴𝑑𝑑𝑟(𝑟𝑠,a.f,h)],𝑛𝑒𝑤𝐴𝑑𝑑𝑟(𝑟𝑠,a.f,h)S}𝑖𝑛𝑓𝑈𝑝𝑑𝑎𝑡𝑒(h,𝑟𝑠,a,𝑎𝑟𝑔𝑠)=let 𝑓𝑎𝑐𝑡𝑠B={a.F(v):𝚜𝚎𝚕𝚏.F𝑛𝑙𝐵𝑎𝑠𝑒(𝑟𝑢𝑙𝑒𝑠(𝑟𝑠))v𝑑𝑒𝑟𝑒𝑓(h,a,F)}𝑓𝑎𝑐𝑡𝑠L={p(v):p𝑑𝑜𝑚(𝑎𝑟𝑔𝑠)vh(𝑎𝑟𝑔𝑠(p))}𝑟𝑒𝑠𝑢𝑙𝑡=𝑒𝑣𝑎𝑙𝑅𝑢𝑙𝑒𝑠(𝑟𝑢𝑙𝑒𝑠(rs)[𝚜𝚎𝚕𝚏a]𝑓𝑎𝑐𝑡𝑠B𝑓𝑎𝑐𝑡𝑠L)θ=𝚜𝚎𝚕𝚏.f𝑛𝑙𝐷𝑒𝑟𝑖𝑣𝑒𝑑(𝑟𝑢𝑙𝑒𝑠(𝑟𝑠))𝑢𝑝𝑑𝑎𝑡𝑒𝑉𝑎𝑟(h,𝑟𝑠,a.f,𝑟𝑒𝑠𝑢𝑙𝑡(a.f))in (θ,𝑟𝑒𝑠𝑢𝑙𝑡)𝑚𝑎𝑖𝑛𝑡𝑎𝑖𝑛(h,ht)=let θ=a𝑑𝑜𝑚(h),𝑟𝑠𝑟𝑢𝑙𝑒𝑠𝑒𝑡𝑠(ht(a))π1(𝑖𝑛𝑓𝑈𝑝𝑑𝑎𝑡𝑒(h,𝑟𝑠,a,{}))OPENθT={a𝚜𝚎𝚝|a𝑑𝑜𝑚(θ)θ(a)𝑉𝑎𝑙})(h,ht)=(h,ht)(θ,θT)in if (h,ht)=(h,ht) then (h,ht) else 𝑚𝑎𝑖𝑛𝑡𝑎𝑖𝑛(h,ht)\begin{array}[]{@{}l@{}}{\it deref}(h,a,F)=\begin{array}[t]{@{}l@{}}\mbox{if }a\not\in{\it dom}(h)\mbox{ then }\mathord{\perp}\\ \mbox{elif }{\it length}(F)=1\mbox{ then }(\mbox{if }F\in{\it dom}(h(a))\mbox{ then }h(a)(F)\mbox{ else }\mathord{\perp})\\ \mbox{else }{\it first}(F)\in{\it dom}(h(a))\mbox{ then }{\it deref}(h,h(a)({\it first}(F)),{\it rest}(path))\mbox{ else }\mathord{\perp}\\ \end{array}\\ \\ {\it allBaseAreSets}(h,ht)=\\ \hskip 7.5pt\begin{array}[t]{@{}l@{}}\forall a\in{\it dom}(h),{\it rs}\in{\it rulesets}(ht(a)),{\color[rgb]{0,0,1}\tt self}.F\in{\it nlBase}({\it rules}({\it rs})):\\ {\it deref}(h,a,F)=\mathord{\perp}\lor({\it deref}(h,a,F)\in{\mbox{$\it Address$}}\land ht({\it deref}(h,a,F))={\color[rgb]{0,0,1}\tt set})\end{array}\\ \\ {\it updateVar}(h,{\it rs},a.f,S)=\begin{array}[t]{@{}l@{}}\mbox{if }h(a)(f)\in{\mbox{$\it Address$}}\mbox{ then }\{h(a)(f)\mapsto S\}\\ \mbox{else }\{a\mapsto h(a)[f\mapsto{\it newAddr}({\it rs},a.f,h)],{\it newAddr}({\it rs},a.f,h)\mapsto S\}\end{array}\\ \\ {\it infUpdate}(h,{\it rs},a,{\it args})=\\ \hskip 7.5pt\begin{array}[]{@{}l@{}}\mbox{let }\begin{array}[t]{@{}l@{}}{\it facts}_{B}=\{a.F(v):{\color[rgb]{0,0,1}\tt self}.F\in{\it nlBase}({\it rules}({\it rs}))\land v\in{\it deref}(h,a,F)\}\\ {\it facts}_{L}=\{p(v):p\in{\it dom}({\it args})\land v\in h({\it args}(p))\}\\ {\it result}={\it evalRules}({\it rules}(rs)[{\color[rgb]{0,0,1}\tt self}\mapsto a]\cup{\it facts}_{B}\cup{\it facts}_{L})\\ \theta=\bigcup_{{\color[rgb]{0,0,1}\tt self}.f\in{\it nlDerived}({\it rules}({\it rs}))}{\it updateVar}(h,{\it rs},a.f,{\it result}(a.f))\end{array}\\ \mbox{in }(\theta,{\it result})\end{array}\\ \\ {\it maintain}(h,ht)=\begin{array}[t]{@{}l@{}}\mbox{let }\begin{array}[t]{@{}l@{}}\theta=\bigcup_{a\in{\it dom}(h),{\it rs}\in{\it rulesets}(ht(a))}\pi_{1}({\it infUpdate}(h,{\it rs},a,\{\}))\\ \theta_{T}=\{a\mapsto{\color[rgb]{0,0,1}\tt set}\;|\;a\in{\it dom}(\theta)\land\theta(a)\subseteq{\mbox{$\it Val$}}\})\\ (h^{\prime},ht^{\prime})=(h,ht)\sqcup(\theta,\theta_{T})\end{array}\\ \mbox{in if }(h^{\prime},ht^{\prime})=(h,ht)\mbox{ then }(h,ht)\mbox{ else }{\it maintain}(h^{\prime},ht^{\prime})\end{array}\end{array}
    Figure 8: Definitions of auxiliary functions related to inference.

    Notation.   In the transition rules, aa matches an address, and vv matches a value (i.e., an element of 𝑉𝑎𝑙\it Val).

    fgf\cup g is the union of functions ff and gg with disjoint domains. For any functions ff and gg, fg={xf(x)|x𝑑𝑜𝑚(f)𝑑𝑜𝑚(g)}gf\sqcup g=\{x\mapsto f(x)\;|\;x\in{\it dom}(f)\setminus{\it dom}(g)\}\cup g. For a function ff, f[xy]=f{xy}f[x\mapsto y]=f\sqcup\{x\mapsto y\}. When a function θ\theta is intended to be used to compute an updated version fθf\sqcup\theta of a function ff, we refer to θ\theta as an “update” to ff.

    Sequences are denoted with angle brackets, e.g., 0,1,2𝐼𝑛𝑡\langle 0,1,2\rangle\in{\it Int}^{*}. s@ts@t is the concatenation of sequences ss and tt. 𝑓𝑖𝑟𝑠𝑡(s){\it first}(s) is the first element of sequence ss. 𝑟𝑒𝑠𝑡(s){\it rest}(s) is the sequence obtained by removing the first element of ss. 𝑙𝑒𝑛𝑔𝑡ℎ(s){\it length}(s) is the length of sequence ss.

    Auxiliary definitions.   𝑛𝑒𝑤(c){\it new}(c) returns a new instance of class cc, for c𝐶𝑙𝑎𝑠𝑠𝑁𝑎𝑚𝑒c\in{\mbox{$\it ClassName$}}. When cc is the name of user-defined class, 𝑛𝑒𝑤(c){\it new}(c) returns an empty set representing the empty function.

    𝑛𝑒𝑤(c)=if c=𝚜𝚎𝚚𝚞𝚎𝚗𝚌𝚎 then  else {}\begin{array}[]{@{}l@{}}{\it new}(c)=\mbox{if }c={\color[rgb]{0,0,1}\tt sequence}\mbox{ then }\langle\rangle\mbox{ else }\{\}\end{array}

    𝑙𝑒𝑔𝑎𝑙𝐴𝑠𝑠𝑖𝑔𝑛(ht,a,f){\it legalAssign}(ht,a,f) holds if assigning to field ff of the object with address aa is legal, in the sense that aa refers to an object with fields (not an instance of a pre-defined class without fields), and a.fa.f is not a derived predicate of any rule set. 𝑙𝑒𝑔𝑎𝑙𝐴𝑠𝑠𝑖𝑔𝑛(ht,a,f)=ht(a){𝚜𝚎𝚝,𝚜𝚎𝚚𝚞𝚎𝚗𝚌𝚎}((a=a𝑔𝑣a𝑔𝑣.f𝑔𝑙𝑏𝑙𝐷𝑒𝑟𝑖𝑣𝑒𝑑)(aa𝑔𝑣𝚜𝚎𝚕𝚏.𝚏𝑛𝑙𝐷𝑒𝑟𝑖𝑣𝑒𝑑(ht(a)))){\it legalAssign}(ht,a,f)=ht(a)\not\in\{{\color[rgb]{0,0,1}\tt set},{\color[rgb]{0,0,1}\tt sequence}\}\land((a=a_{\it gv}\land a_{\it gv}.f\not\in{\it glblDerived})\lor(a\neq a_{\it gv}\land{\tt{\color[rgb]{0,0,1}self}.f}\not\in{\it nlDerived}(ht(a)))).

    𝑚𝑒𝑡ℎ𝑜𝑑𝐷𝑒𝑓(c,m,𝑑𝑒𝑓){\it methodDef}(c,m,{\it def}) holds iff cc is a user-defined class and either (1) cc defines method mm, and def is the definition of mm in cc, or (2) cc does not define mm, and def is the definition of mm in the nearest ancestor of cc in the inheritance hierarchy that defines mm.

    𝑔𝑙𝑏𝑙𝑅𝑢𝑙𝑒𝑠𝑒𝑡𝑠{\it glblRulesets} is the set of names of global rule sets in the program. 𝑟𝑢𝑙𝑒𝑠𝑒𝑡𝑠(c){\it rulesets}(c) is the set of names of rule sets defined in class cc in the program. By definition, 𝑟𝑢𝑙𝑒𝑠𝑒𝑡𝑠(C𝑔𝑣)=𝑔𝑙𝑏𝑙𝑅𝑢𝑙𝑒𝑠𝑒𝑡𝑠{\it rulesets}(C_{\it gv})={\it glblRulesets}, and for convenience, we also define 𝑟𝑢𝑙𝑒𝑠𝑒𝑡𝑠(𝚜𝚎𝚝)={\it rulesets}({\color[rgb]{0,0,1}\tt set})=\emptyset and 𝑟𝑢𝑙𝑒𝑠𝑒𝑡𝑠(𝚜𝚎𝚚)={\it rulesets}({\color[rgb]{0,0,1}\tt seq})=\emptyset. For any rule set name 𝑟𝑠{\it rs} in the program, 𝑟𝑢𝑙𝑒𝑠(𝑟𝑠){\it rules}({\it rs}) is the set of rules in that rule set (recall from Section A.1.1 that rule set names have been transformed to be unique across all scopes).

    For a set of rules RR, 𝑛𝑙𝐵𝑎𝑠𝑒(R){\it nlBase}(R) and 𝑛𝑙𝐷𝑒𝑟𝑖𝑣𝑒𝑑(R){\it nlDerived}(R) are the sets of non-local base predicates and non-local derived predicates, respectively, in RR. For c𝐶𝑙𝑎𝑠𝑠𝑁𝑎𝑚𝑒c\in{\mbox{$\it ClassName$}}, 𝑛𝑙𝐷𝑒𝑟𝑖𝑣𝑒𝑑(c){\it nlDerived}(c) is the set of non-local derived predicates in rule sets defined in class cc. 𝑔𝑙𝑏𝑙𝐷𝑒𝑟𝑖𝑣𝑒𝑑{\it glblDerived} is the set of global variables that are derived predicates in any rule set.

    For a derived predicate pp of rule set 𝑟𝑠{\it rs}, and heap hh, 𝑛𝑒𝑤𝐴𝑑𝑑𝑟(𝑟𝑠,p,h){\it newAddr}({\it rs},p,h) selects a fresh address for pp of 𝑟𝑠{\it rs}; specifically, it returns an address that is not in 𝑑𝑜𝑚(h){\it dom}(h) and is different from 𝑛𝑒𝑤𝐴𝑑𝑑𝑟(𝑟𝑠,p,h){\it newAddr}({\it rs}^{\prime},p^{\prime},h) whenever 𝑟𝑠𝑟𝑠pp{\it rs}\neq{\it rs}^{\prime}\lor p\neq p^{\prime}. Using function 𝑛𝑒𝑤𝐴𝑑𝑑𝑟{\it newAddr} to select fresh addresses, instead of selecting them non-deterministically, is inessential but simplifies the definitions of auxiliary functions related to inference in Figure 8 and the transition rule for infer in Figure 10.

    The following five auxiliary functions and relation are defined in Figure 8.

    𝑑𝑒𝑟𝑒𝑓(h,a,F){\it deref}(h,a,F) returns the value obtained by starting at address aa in heap hh and dereferencing the sequence FF of one or more fields. If aa is not an address in 𝑑𝑜𝑚(h){\it dom}(h), or if a field in FF is not in the domain of the appropriate object, then 𝑑𝑒𝑟𝑒𝑓{\it deref} returns \mathord{\perp}.

    𝑎𝑙𝑙𝐵𝑎𝑠𝑒𝐴𝑟𝑒𝑆𝑒𝑡𝑠(h,ht){\it allBaseAreSets}(h,ht) returns true if, in heap hh with heap type map htht, for each rule set, for each non-local base predicate of the rule set, either it is uninitialized (indicated by 𝑑𝑒𝑟𝑒𝑓{\it deref} returning \mathord{\perp}) or its value is a set.

    𝑢𝑝𝑑𝑎𝑡𝑒𝑉𝑎𝑟(h,𝑟𝑠,a.f,S){\it updateVar}(h,{\it rs},a.f,S) returns an update to the heap that makes variable a.fa.f to refer to a set with content SS. If the value of a.fa.f is already an address aa^{\prime}, a set with content SS is stored at aa^{\prime}, otherwise a.fa.f is assigned a fresh address aa^{\prime}, and a set with content SS is stored at aa^{\prime}.

    𝑖𝑛𝑓𝑈𝑝𝑑𝑎𝑡𝑒(h,𝑟𝑠,a,𝑎𝑟𝑔𝑠){\it infUpdate}(h,{\it rs},a,{\it args}) computes an update expressing the result of inference for rule set 𝑟𝑠{\it rs} instantiated with 𝚜𝚎𝚕𝚏a{\color[rgb]{0,0,1}\tt self}\mapsto a, with heap hh and using 𝑎𝑟𝑔𝑠{\it args} to obtain values for local variables of \rightarrow. 𝑖𝑛𝑓𝑈𝑝𝑑𝑎𝑡𝑒{\it infUpdate} returns a pair containing the update to apply to the heap hh and a function 𝑟𝑒𝑠𝑢𝑙𝑡{\it result} that maps each defined derived predicate in the rule set to its value; a derived predicate is undefined after this inference if it depends on a local variable that is a base predicate whose value is not provided by 𝑎𝑟𝑔𝑠{\it args}. For explicit calls to infer, 𝑎𝑟𝑔𝑠{\it args} contains values provided by keyword arguments; for automatic maintenance, 𝑎𝑟𝑔𝑠{\it args} is the empty function. Note that the condition v𝑑𝑒𝑟𝑒𝑓(h,a,F)v\in{\it deref}(h,a,F) is false if 𝑑𝑒𝑟𝑒𝑓(h,a,F){\it deref}(h,a,F) is \mathord{\perp}; this has the effect that uninitialized base predicates are equivalent to empty sets. 𝑖𝑛𝑓𝑈𝑝𝑑𝑎𝑡𝑒{\it infUpdate} uses the auxiliary function 𝑒𝑣𝑎𝑙𝑅𝑢𝑙𝑒𝑠(R){\it evalRules}(R), which evaluates the set of rules RR and returns a function from the set of predicates that appear in the rules to their meanings, represented as sets of tuples.

    𝑚𝑎𝑖𝑛𝑡𝑎𝑖𝑛(θ,θT,h,ht){\it maintain}(\theta,\theta_{T},h,ht) returns a pair whose first and second components are updates to hh and htht, respectively, that express the result of automatic maintenance of all rule sets in heap hh and heap type map htht. For each set of rules that needs to be maintained, it calls 𝑖𝑛𝑓𝑈𝑝𝑑𝑎𝑡𝑒{\it infUpdate} to compute an update expressing the result of inference for that rule set, and uses function π1\pi_{1}, which select the first component of a tuple, to extract that update from the tuple returned by 𝑖𝑛𝑓𝑈𝑝𝑑𝑎𝑡𝑒{\it infUpdate}. It combines the resulting updates using union, since the well-formedness restrictions on programs ensure that these updates have disjoint domains. 𝑚𝑎𝑖𝑛𝑡𝑎𝑖𝑛{\it maintain} uses recursion to repeatedly evaluate all rule sets until a fixed-point is reached.

    Notes.   The transition rules enforce the invariant that each non-local base predicate is either uninitialized or its value is a set. Inference treats uninitialized variables used as base predicates as empty sets. This is consistent with the semantics of Datalog and Prolog, which treats predicates for which no information has been supplied as false for all arguments. This principle is realized implicitly in the set comprehensions defining 𝑓𝑎𝑐𝑡𝑠B{\it facts}_{B} and 𝑓𝑎𝑐𝑡𝑠L{\it facts}_{L} in 𝑖𝑛𝑓𝑈𝑝𝑑𝑎𝑡𝑒{\it infUpdate}: the resulting sets do not contain any facts for those base predicates. This principle applies whenever an uninitialized field is encountered in the sequence of field dereferences used to read the value of a base predicate.

    The transition rules include premises that check for run-time errors; in case of an error, the premise is false, and evaluation is stuck. Examples of such errors include trying to select a component from a value that is not a tuple, invoke a non-existent method of an object, read the value of a non-existent (uninitialized) field of an object, assign a value to a derived predicate using an assignment statement, or assign a non-set value to a base predicate. The transition rules check for this error at updates to fields of instances of all classes—not only classes that define rule sets—because base predicates may contain multiple field dereferences.

    Transition rules for methods of pre-defined classes set and sequence are similar in style, so only one representative example is given, for set.add. Note that 𝑚𝑎𝑖𝑛𝑡𝑎𝑖𝑛{\it maintain} needs to be called only in transition rules for methods of set that update the content of the set.

    The transition rule for invoking a method in a user-defined class executes a copy of the method body ss that has been instantiated by substituting argument values for parameters.

    The transition rule for an explicit call to infer on a rule set rsrs with class scope instantiates rsrs using the target object aa for self and values given by keyword arguments for local variables, calls 𝑖𝑛𝑓𝑈𝑝𝑑𝑎𝑡𝑒{\it infUpdate} to evaluate the instantiated rule set, and calls 𝑚𝑎𝑖𝑛𝑡𝑎𝑖𝑛{\it maintain} to determine the effects of automatic maintenance. Note that θ\theta is an update to the heap that updates the values of non-local derived predicates of 𝑟𝑠{\it rs}; 𝑟𝑒𝑠𝑢𝑙𝑡{\it result} maps each derived predicate of RR to its value; θ𝑄𝑁𝐿\theta_{\it QNL} and θ𝑄𝐿\theta_{\it QL} are updates to the heap that together update the values of a1.f1,,an.fna_{1}.f_{1},\ldots,a_{n}.f_{n} to contain the query results, with the former handling queries of non-local derived predicates, and the latter handling queries of local derived predicates; and θT\theta_{T} is an update to the heap type map that updates the types of addresses containing sets created by this call to infer.

    / / context rule for expressionsh,htee(C[e],h,ht)(C[e],h,ht)/ / context rule for statements(s,h,ht)(s,h,ht)(C[s],h,ht)(C[s],h,ht)/ / field assignment(a.f:=v,h,ht)(𝚜𝚔𝚒𝚙,hθ,htθT)if 𝑙𝑒𝑔𝑎𝑙𝐴𝑠𝑠𝑖𝑔𝑛(ht,a,f)h=h[ah(a)[fv]]𝑎𝑙𝑙𝐵𝑎𝑠𝑒𝐴𝑟𝑒𝑆𝑒𝑡𝑠(h,ht)(θ,θT)=𝑚𝑎𝑖𝑛𝑡𝑎𝑖𝑛(h,ht)/ / object creation(a.f:=𝚗𝚎𝚠c,h,ht)(𝚜𝚔𝚒𝚙,hθ,htθT)if a𝑑𝑜𝑚(ht)a𝐴𝑑𝑑𝑟𝑒𝑠𝑠𝑙𝑒𝑔𝑎𝑙𝐴𝑠𝑠𝑖𝑔𝑛(ht,a,f)ht=ht[ac]h=h[ah(a)[fa],a𝑛𝑒𝑤(c)]𝑎𝑙𝑙𝐵𝑎𝑠𝑒𝐴𝑟𝑒𝑆𝑒𝑡𝑠(h,ht)(θ,θT)=𝑚𝑎𝑖𝑛𝑡𝑎𝑖𝑛(h,ht)/ / sequential composition(𝚜𝚔𝚒𝚙,s,h,ht)(s,h,ht)/ / conditional statement(𝚒𝚏𝚃𝚛𝚞𝚎:s1𝚎𝚕𝚜𝚎:s2,h,ht)(s1,h,ht)(𝚒𝚏𝙵𝚊𝚕𝚜𝚎:s1𝚎𝚕𝚜𝚎:s2,h,ht)(s2,h,ht)/ / for loop(𝚏𝚘𝚛x𝚒𝚗a:s,h,ht)(𝚏𝚘𝚛x𝚒𝚗𝚃𝚞𝚙𝚕𝚎(v1,,vn):s,h,ht)if (ht(a)=𝚜𝚎𝚚𝚞𝚎𝚗𝚌𝚎h(a)=v1,,vn)(ht(a)=𝚜𝚎𝚝v1,,vnis a linearization ofh(a))(𝚏𝚘𝚛x𝚒𝚗𝚃𝚞𝚙𝚕𝚎(v1,,vn):s,h,ht)(s[xv1];𝚏𝚘𝚛x𝚒𝚗𝚃𝚞𝚙𝚕𝚎(v2,,vn):s,h,ht)(𝚏𝚘𝚛x𝚒𝚗𝚃𝚞𝚙𝚕𝚎():s,h,ht)(𝚜𝚔𝚒𝚙,h,ht)/ / while loop(𝚠𝚑𝚒𝚕𝚎e:s,h,ht)(𝚒𝚏e:(s;𝚠𝚑𝚒𝚕𝚎e:s)𝚎𝚕𝚜𝚎:𝚜𝚔𝚒𝚙,h,ht)\begin{array}[]{@{}l@{}}\mbox{/\,/\ context rule for expressions}\\ \dfrac{h,ht\vdash e\rightarrow e^{\prime}}{(C[e],h,ht)\rightarrow(C[e^{\prime}],h,ht)}\\ \\ \mbox{/\,/\ context rule for statements}\\ \dfrac{(s,h,ht)\rightarrow(s^{\prime},h^{\prime},ht^{\prime})}{(C[s],h,ht)\rightarrow(C[s^{\prime}],h^{\prime},ht^{\prime})}\\ \\ \mbox{/\,/\ field assignment}\\ (a{\tt.}f\;{\tt:=}\;v,h,ht)\\ {}\rightarrow({\color[rgb]{0,0,1}\tt skip},h^{\prime}\sqcup\theta,ht\sqcup\theta_{T})\\ \hskip 7.5pt{}\mbox{if }{\it legalAssign}(ht,a,f)\land h^{\prime}=h[a\mapsto h(a)[f\mapsto v]]\land{\it allBaseAreSets}(h^{\prime},ht)\\ \hskip 7.5pt{}\land(\theta,\theta_{T})={\it maintain}(h^{\prime},ht)\\ \\ \mbox{/\,/\ object creation}\\ (a{\tt.}f\;{\tt:=}\;{\color[rgb]{0,0,1}\tt new}~c,h,ht)\\ {}\rightarrow({\color[rgb]{0,0,1}\tt skip},h^{\prime}\sqcup\theta,ht^{\prime}\sqcup\theta_{T})\\ \hskip 7.5pt\mbox{if }a^{\prime}\not\in{\it dom}(ht)\land a^{\prime}\in{\mbox{$\it Address$}}\land{\it legalAssign}(ht,a,f)\\ \hskip 7.5pt{}\land ht^{\prime}=ht\,[a^{\prime}\mapsto c]\land h^{\prime}=h[a\mapsto h(a)[f\mapsto a^{\prime}],a^{\prime}\mapsto{\it new}(c)]\land{\it allBaseAreSets}(h^{\prime},ht^{\prime})\\ \hskip 7.5pt{}\land(\theta,\theta_{T})={\it maintain}(h^{\prime},ht^{\prime})\\ \\ \mbox{/\,/\ sequential composition}\\ ({\tt{\color[rgb]{0,0,1}skip};}\,s,h,ht)\rightarrow(s,h,ht)\\ \\ \mbox{/\,/\ conditional statement}\\ ({\color[rgb]{0,0,1}\tt if}~{\tt{\color[rgb]{0,0,1}True}:}~s_{1}~{\tt{\color[rgb]{0,0,1}else}:}~s_{2},h,ht)\rightarrow(s_{1},h,ht)\\ \\ ({\color[rgb]{0,0,1}\tt if}~{\tt{\color[rgb]{0,0,1}False}:}~s_{1}~{\tt{\color[rgb]{0,0,1}else}:}~s_{2},h,ht)\rightarrow(s_{2},h,ht)\\ \\ \mbox{/\,/\ for loop}\\ ({\color[rgb]{0,0,1}\tt for}~x~{\color[rgb]{0,0,1}\tt in}~a{\tt:}~s,h,ht)\\ {}\rightarrow({\color[rgb]{0,0,1}\tt for}~x~{\color[rgb]{0,0,1}\tt inTuple}~{\tt(}v_{1},\ldots,v_{n}{\tt):}~s,h,ht)\\ \hskip 7.5pt\mbox{if }(ht(a)={\color[rgb]{0,0,1}\tt sequence}\land h(a)=\langle v_{1},\ldots,v_{n}\rangle)\\ \hskip 7.5pt{}\lor(ht(a)={\color[rgb]{0,0,1}\tt set}\land\langle v_{1},\ldots,v_{n}\rangle~\mbox{is a linearization of}~h(a))\\ \\ ({\color[rgb]{0,0,1}\tt for}~x~{\color[rgb]{0,0,1}\tt inTuple}~{\tt(}v_{1},\ldots,v_{n}{\tt):}~s,h,ht)\\ {}\rightarrow(s[x\mapsto v_{1}];{\color[rgb]{0,0,1}\tt for}~x~{\color[rgb]{0,0,1}\tt inTuple}~{\tt(}v_{2},\ldots,v_{n}{\tt):}~s,h,ht)\\ \\ ({\color[rgb]{0,0,1}\tt for}~x~{\color[rgb]{0,0,1}\tt inTuple}~{\tt():}~s,h,ht)\rightarrow({\color[rgb]{0,0,1}\tt skip},h,ht)\\ \\ \mbox{/\,/\ while loop}\\ ({\color[rgb]{0,0,1}\tt while}~e{\tt:}~s,h,ht)\\ {}\rightarrow({\color[rgb]{0,0,1}\tt if}~e{\tt:}~{\tt(}s{\tt;}~{\color[rgb]{0,0,1}\tt while}~e{\tt:}~s{\tt)}~{\tt{\color[rgb]{0,0,1}else}:}~{\color[rgb]{0,0,1}\tt skip},h,ht)\end{array}
    Figure 9: Transition relation for statements, Part 1.
    / / invoke method in pre-defined class (example)(a.𝚊𝚍𝚍(v1),h,ht)(𝚜𝚔𝚒𝚙,hθ,htθT)if ht(a)=𝚜𝚎𝚝a𝑔𝑣.f𝑔𝑙𝑏𝑙𝐷𝑒𝑟𝑖𝑣𝑒𝑑:a=h(a𝑔𝑣)(f)a𝑑𝑜𝑚(ht),𝚜𝚎𝚕𝚏.f𝑛𝑙𝐷𝑒𝑟𝑖𝑣𝑒𝑑(ht(a)):a=h(a)(f)h=h[ah(a){v1}](θ,θT)=𝑚𝑎𝑖𝑛𝑡𝑎𝑖𝑛(h,ht)/ / invoke method in user-defined class(a.m(v1,,vn),h,ht)(s[𝚜𝚎𝚕𝚏a,x1v1,,xnvn],h,ht)if 𝑚𝑒𝑡ℎ𝑜𝑑𝐷𝑒𝑓(ht(a),m,𝚍𝚎𝚏m(x1,,xn)s)/ / invoke infer on a rule set defined in class scope(a1.f1,,an.fn:=a.𝚒𝚗𝚏𝚎𝚛(q1,,qn,x1=v1,,xk=vk,𝚛𝚞𝚕𝚎𝚜=rs),h,ht)(𝚜𝚔𝚒𝚙,hθ,htθTθT)if rs𝑟𝑢𝑙𝑒𝑠𝑒𝑡𝑠(ht(a))i{1..n}:𝑙𝑒𝑔𝑎𝑙𝐴𝑠𝑠𝑖𝑔𝑛(ht,ai,fi)(i{1..k}:vi𝑑𝑜𝑚(ht)ht(vi)=𝚜𝚎𝚝)𝑎𝑟𝑔𝑠={xivi|i{1..k}}(θ,𝑟𝑒𝑠𝑢𝑙𝑡)=𝑖𝑛𝑓𝑈𝑝𝑑𝑎𝑡𝑒(h,ht,𝑟𝑠,a,𝑎𝑟𝑔𝑠)θ𝑄𝑁𝐿=i{1..n} s.t. qi is a non-local predicate 𝚜𝚎𝚕𝚏.f{aih(ai)[fi(hθ)(a)(f)]}θ𝑄𝐿=i{1..n} s.t. qi is a local predicate{aih(ai)[fi𝑛𝑒𝑤𝐴𝑑𝑑𝑟(𝑟𝑠,qi,h)],𝑛𝑒𝑤𝐴𝑑𝑑𝑟(𝑟𝑠,qi,h)𝑟𝑒𝑠𝑢𝑙𝑡(qi)}θT={a𝚜𝚎𝚝|a𝑑𝑜𝑚(θ)θ(a)𝑉𝑎𝑙}{a𝚜𝚎𝚝|a𝑑𝑜𝑚(θ𝑄𝐿)θ𝑄𝐿(a)𝑉𝑎𝑙}h=hθθ𝑄𝑁𝐿θ𝑄𝐿θU𝑎𝑙𝑙𝐵𝑎𝑠𝑒𝐴𝑟𝑒𝑆𝑒𝑡𝑠(h,htθT)(θ,θT)=𝑚𝑎𝑖𝑛𝑡𝑎𝑖𝑛(h,htθT)\begin{array}[]{@{}l@{}}\mbox{/\,/\ invoke method in pre-defined class (example)}\\ (a{\tt.add(}v_{1}{\tt)},h,ht)\rightarrow({\color[rgb]{0,0,1}\tt skip},h^{\prime}\sqcup\theta,ht\sqcup\theta_{T})\\ \hskip 7.5pt\mbox{if }ht(a)={\color[rgb]{0,0,1}\tt set}\land\nexists a_{\it gv}.f\in{\it glblDerived}:\;a=h(a_{\it gv})(f)\\ \hskip 7.5pt{}\land\nexists a^{\prime}\in{\it dom}(ht),{\color[rgb]{0,0,1}\tt self}.f\in{\it nlDerived}(ht(a^{\prime})):\;a=h(a^{\prime})(f)\\ \hskip 7.5pt{}\land h^{\prime}=h[a\mapsto h(a)\cup\{v_{1}\}]\land(\theta,\theta_{T})={\it maintain}(h^{\prime},ht)\\ \\ \mbox{/\,/\ invoke method in user-defined class}\\ (a{\tt.}m(v_{1},\ldots,v_{n}),h,ht)\\ {}\rightarrow(s[{\color[rgb]{0,0,1}\tt self}\mapsto a,x_{1}\mapsto v_{1},\ldots,x_{n}\mapsto v_{n}],h,ht)\\ \hskip 7.5pt\mbox{if }{\it methodDef}(ht(a),m,{\color[rgb]{0,0,1}\tt def}~m{\tt(}x_{1},\ldots,x_{n}{\tt)}~s)\\ \\ \mbox{/\,/\ invoke {\color[rgb]{0,0,1}\tt infer} on a rule set defined in class scope}\\ (a_{1}.f_{1},\ldots,a_{n}.f_{n}:=a{\tt.}{\color[rgb]{0,0,1}\tt infer(}q_{1},\ldots,q_{n},x_{1}=v_{1},\ldots,x_{k}=v_{k},{\color[rgb]{0,0,1}\tt rules}{\tt=}rs{\tt)},h,ht)\\ \hskip 7.5pt{}\rightarrow({\color[rgb]{0,0,1}\tt skip},h^{\prime}\sqcup\theta^{\prime},ht\sqcup\theta_{T}\sqcup\theta_{T}^{\prime})\\ \hskip 7.5pt\mbox{if }rs\in{\it rulesets}(ht(a))\\ \hskip 7.5pt\begin{array}[]{@{}l@{}}{}\land\forall i\in\{1..n\}:{\it legalAssign}(ht,a_{i},f_{i})\\ {}\land(\forall i\in\{1..k\}:v_{i}\in{\it dom}(ht)\land ht(v_{i})={\color[rgb]{0,0,1}\tt set})\\ {}\land{\it args}=\{x_{i}\mapsto v_{i}\;|\;i\in\{1..k\}\}\\ {}\land(\theta,{\it result})={\it infUpdate}(h,ht,{\it rs},a,{\it args})\\ {}\land\theta_{\it QNL}=\bigcup_{i\in\{1..n\}\mbox{ s.t. $q_{i}$ is a non-local predicate }{\color[rgb]{0,0,1}\tt self}.f}\{a_{i}\mapsto h(a_{i})[f_{i}\mapsto(h\sqcup\theta)(a)(f)]\}\\ {}\land\theta_{\it QL}=\bigcup_{i\in\{1..n\}\mbox{ s.t. $q_{i}$ is a local predicate}}\{\begin{array}[t]{@{}l@{}}a_{i}\mapsto h(a_{i})[f_{i}\mapsto{\it newAddr}({\it rs},q_{i},h)],\\ {\it newAddr}({\it rs},q_{i},h)\mapsto{\it result}(q_{i})\}\end{array}\\ {}\land\theta_{T}=\begin{array}[t]{@{}l@{}}\{a\mapsto{\color[rgb]{0,0,1}\tt set}\;|\;a\in{\it dom}(\theta)\land\theta(a)\subseteq{\mbox{$\it Val$}}\}\\ {}\cup\{a\mapsto{\color[rgb]{0,0,1}\tt set}\;|\;a\in{\it dom}(\theta_{\it QL})\land\theta_{\it QL}(a)\subseteq{\mbox{$\it Val$}}\}\end{array}\\ {}\land h^{\prime}=h\sqcup\theta\sqcup\theta_{\it QNL}\sqcup\theta_{\it QL}\sqcup\theta_{U}\\ {}\land{\it allBaseAreSets}(h^{\prime},ht\sqcup\theta_{T})\\ {}\land(\theta^{\prime},\theta_{T}^{\prime})={\it maintain}(h^{\prime},ht\sqcup\theta_{T})\end{array}\end{array}
    Figure 10: Transition relation for statements, Part 2.

    Executions.   An execution is a sequence of transitions σ0σ1σ2\sigma_{0}\rightarrow\sigma_{1}\rightarrow\sigma_{2}\rightarrow\cdots such that σ0\sigma_{0} is the initial state of the program, given by σ0=(s0,{a𝑔𝑣{}},{a𝑔𝑣C𝑔𝑣})\sigma_{0}=(s_{0},\{a_{\it gv}\mapsto\{\}\},\{a_{\it gv}\mapsto C_{\it gv}\}), where s0s_{0} is the top-level statement that appears in the program after the rule set definitions and class definitions, and a𝑔𝑣a_{\it gv} is the address of the object introduced by the transformation that eliminates global variables (see Section A.1.1).

    Execution of a program may eventually (1) terminate (i.e., the statement in the first component of the state becomes skip, meaning that there is nothing left to do), (2) get stuck (i.e., the statement is not skip, and the process has no enabled transitions, meaning an error in the program), or (3) run forever (due to an infinite loop or infinite recursion).

    Appendix B 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., [21, 20], 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., [17, 54, 36], instead of immediately after updates to base predicates.

    Incremental inference.

    More fundamentally, even when derived predicates are frequently used, infer may 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., [22, 58].

    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 [64, 13] (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 [65, 66], which improves over magic sets [6, 1] 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 [48, 35].

    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 [38, 36] are needed to avoid such severe slowdowns.