Nondeterministic Rewriting Query Language (NDRQL)

NDRQL (Nondeterministic Rewriting Query Language) is a multiset query and update language executable in a term rewriting system. Its most remarkable feature, besides non-standard approach to quantification and introduction of fresh values, is non-determinism — a query result is not uniquely determined by the database. Arguably, this feature is very useful, e.g., in modelling user choices during simulation or reachability analysis of a data-centric business process — the intended application of NDQRL. Query evaluation is implemented by converting the query into a terminating term rewriting system and normalizing the initial term which encapsulates the current database. A normal form encapsulates a query result. One can prove that NDQRL can express any relational algebra query. This page makes available Maude implementatation of NDQRL's syntax and semantics in Maude rewriting language. It also documents the implementation, as well as provides some example specification ready for simulation.

Download NDRQL

Syntax and semantics of NDRQL is defined in the Maude file QuerySemantics.maude

An example specification is contained in Bookings.maude.

Query language

NDQRL consists of several parts, and we present the language according to the division. NDQRL queries are proper Maude terms, and we present the language grammar using sorts and signatures of constructors.

Facts and multisets of facts

Facts are term of sort Fact. Facts can be thought of as reified instances of predicates (or rows in a database table, with a table name thrown in), and, accordingly, constructors of fact terms are like predicate names (or top level functors in Prolog). It is assumed (though nowhere enforced) that the arguments of facts are terms in the “data signature” which does not contain any sort in the [Fact] kind, nor any other kind marking structural parts of queries and conditons. Facts serve as components of quantification patterns in NDRQL, they are also by themselves DML queries (F as a query matches F in the database).

Databases on which NDQRL queries act are represented as multisets of facts of sort FactSet. Non-empty multisets of facts have sort NeFactSet. The subsort relation is defined by:

subsorts Fact < NeFactSet < FactSet .

i.e., every fact is a non-empty multiset of facts. Multisets of facts are constructed from facts with

op noneF : -> FactSet .
op _o_ : FactSet FactSet -> FactSet [assoc comm id: noneF] .
op _o_ : NeFactSet FactSet -> NeFactSet [ditto] .

i.e., noneF is an empty multiset, and _o_ is an associative and commutative multiset constructor with neutral element noneF.

Fresh values

A support for creation of fresh values is a common requirement: If identifiers for objects are to provide unique identity across the whole history, the must not be reused (even if they are not present in the current active database domain). We support creation of fresh values of nominal sorts only since freshness for non-nominal data types is problematic. A sort is nominal if values of this sort have no non-trivial algebraic or relational structure beside equality. To create fresh values, for each nominal sort Nom we have constructor

op {_}Nom : Nat -> Nom [ctor] .

which does not belong to the signature of data values

To make retrieval of fresh values similar to retrieval of data we use fresh facts of sort FactN unrelated to Fact. For each nominal sort Nom we have a single-argument constructor of the form

op CNom : Nom -> FactN [ctor] .

which wraps value {n}Nom such that for all m ≥ n, {m}Nom was never used before. When fresh value of sort Nom is requested we return {n}Nom and update the fresh fact to CNom({n+1}Nom)

Fresh facts are combined into multisets of sort FactSetN. Non-empty mulisets of fresh facts have sort NeFactSetN. The subsort relation is given by

subsorts FactN < NeFactSetN < FactSetN .

i.e., a fresh fact is a non-empty multiset of fresh facts. Multisets of fresh facts are constructed from facts with operators

op noneFN : -> FactSetN .
op _o_ : FactSetN FactSetN -> FactSetN [assoc comm id: noneFN] .
op _o_ : NeFactSetN FactSetN -> NeFactSetN [ditto] .

i.e., _o_ is associative and commutative with identity noneFN. To facilitate updates (also bulk updates) of fresh facts needed in the semantics of DML queries, we define the following function

op succ : FactN -> FactN .
op refresh : FactSetN -> FactSetN .
eq refresh(noneFN) = noneFN .
eq refresh(fn:FactN o FnN:FactSetN) = succ(fn:FactN) o refresh(FnN:FactSetN) .

Function refresh simply applies succ to each element of the multiset passed as an argument. Observe that we do not define (only declare) the succ function. This is because we have to provide a separate equation defining succ for each nominal sort Nom and corresponding nominal value and fact constructors {_}Nom and CNom (which are specification dependent), even though the succ's definition will always follow the pattern

eq succ(CNom({N:Nat}Nom)) = CNom({s N:Nat}Nom)

where s : Nat -> Nat is a successor function. This is an unfortunate consequence of Maude being the first order language which does not provide support for higher order functions. Using parametrized modules would have allowed us to provide a single generic definition, but at the cost of plenty of boilerplate (far more than just providing trivial equalities for each nominal sort). Of course one can use metalevel to generate the equalities automatically.

Quantification patterns

Quantifiers in NDRQL quantify over patterns (of sort Pat) containing non-ground multisets of facts and fresh facts marked by modalities, which control retention of matched facts, and wrap fresh facts. Patterns can be preserving (of sort PatP), semi-terminating (of sort PatST), terminating (of sort PatT), terminating & preserving (of sort PatTP), or neither. The subsort relation is defined by

subsorts  PatTP < PatT < PatST < Pat .
subsorts PatTP < PatP < Pat .

Patterns are constructed with modalities

op [_]0 : NeFactSet -> PatST .
op [_]? : NeFactSet -> PatTP .
op [_]! : NeFactSet -> PatP .
op [_]n : NeFactSetN -> Pat .

and associative and commutative, subsort overloaded operator _o_:

op _o_ : Pat Pat -> Pat [assoc comm] .
op _o_ : PatT Pat -> PatT [ditto] .
op _o_ : PatST Pat -> PatST [ditto] .
op _o_ : PatP PatP -> PatP [ditto] .
op _o_ : PatP PatTP -> PatTP [ditto] .

Thus, a terminating (resp. a semi-terminating) pattern has to contain at least one fact wrapped with [_]? (resp. with either [_]? or [_]0). A terminating and preserving pattern consists of facts marked only with [_]? or [_]!, and it contains at least one fact wrapped with [_]?. Directed equalities

vars F F' : NeFactSet . vars Fn Fn' : NeFactSetN .
eq [ F ]0 o [ F' ]0 = [ F o F' ]0 .
eq [ F ]? o [ F' ]? = [ F o F' ]? .
eq [ F ]! o [ F' ]! = [ F o F' ]! .
eq [ Fn ]n o [ Fn' ]n = [ Fn o Fn' ]n .

guarantee that fully reduced patterns have facts gathered in groups of the same modality.

The informal meaning of modalities is as follows:

The module SYNTAX-BASE defines some operators for extracting parts of a pattern (i.e., fresh part, terminating and preserving, and so on). They are very useful in the code implementing semantics of queries The naming convention used below should be obvious:

  ops get0 get? get! get!? get!?0 : Pat -> FactSet .
  op getn : Pat -> FactSetN .

  var P : Pat . var FnN : FactSetN . var fn : FactN .

  eq get0([ F ]0 o P) = F o get0(P) .
  eq get0([ F ]0) = F .
  eq get0(P) = noneF [owise] .

  eq get?([ F ]? o P) = F o get?(P) .
  eq get?([ F ]?) = F .
  eq get?(P) = noneF [owise] .

  eq get!([ F ]! o P) = F o get!(P) .
  eq get!([ F ]!) = F .
  eq get!(P) = noneF [owise] .

  eq getn([ Fn ]n o P) = Fn o getn(P) .
  eq getn([ Fn ]n) = Fn .
  eq getn(P) = noneFN [owise] .

  eq get!?(P) = get!(P) o get?(P) .
  eq get!?0(P) = get!(P) o get?(P) o get0(P) .

Conditions

The sublanguage NDRQL(COND) of conditions on finite multisets of facts is analogous to first order logic with quantification restricted to the active domain. Formulas of NDRQL(COND) are (generally non-ground) terms of sort Cond constructed with (the definition of syntax of conditions is contained in the module QUERY-SYNTAX).

ops False True : -> Cond .
op {_} : Bool -> Cond [strat (0)] .
op Not_ : Cond -> Cond .
ops _\/_ _/\_ : Cond Cond -> Cond [assoc comm] .
ops Exists_._ Forall_._ : PatTP Cond -> Cond .

Here _\/_ and _/\_ represent, respectively, disjunction and conjunction. Exists_._ and Forall_._ are quantifiers, existential (∃) and universal (∀), respectively. So, naively, exists [R(X, Y)]! o [P(Y)]? . Ψ(X, Y) iff there exists a matching of R(X, Y) o P(Y) with the current database such that Ψ(X, Y) is satisfied (and actually for the particular pattern [R(X, Y)]! o [P(Y)]? the naive explanation is correct). However the actual semantics described in the paper and (as a Maude code) in the implementation section of this document is more subtle, allowing for nondeterministic executions.

Constructor {_} wraps any boolean expression (term of sort Bool) and makes it a condition. Thus, e.g., {X:Nat == Y:Nat} is a condition. It is defined as lazy (with a strategy strat(0)) since otherwise non-ground subconditions of the form {φ} would reduce inapproprietaly. Indeed, Maude term X:Nat = Y:Nat reduces immediately to false (there are no two equal natural numbers).

In first order logic we can define some of the logical connectives in terms of others. It turns out that the usual equivalences hold also for connectives in NDRQL(COND). Thus, to simplify the implementation of semantics we define (on the level of syntax):

var P : PatTP . vars C C' : Cond .
eq True = Not False .
eq C /\ C' = Not (Not C \/ Not C') .
eq Forall P . C = Not (Exists P . Not C) .

DML queries

The sublanguage NDRQL(DML) permits us to define basic DML operations on multisets of facts which delete from and add some facts to the database. One can think of them as generalized term rewriting rules. Queries in NDRQL(DML) are terms of sort DQuery. Unfortunately, we cannot define NDRQL(DML) purely syntactically. Therefore we also use a supersort NDQuery of DQuery for “query” terms which conform to query syntax but are to general to be always terminating. To define DQuery-terms we also need an auxilliary sort DQueryS for “success assured queries”, i.e., those queries in NDRQL(DML) which always execute successfully (i.e., they return something). Finally, to be able to factorise better the common code and avoid repetition, we define a common supersort Query of both conditions in NDRQL(COND) and DML queries in NDRQL(DML). The subsort relation is defined by:

subsorts Fact < DQueryS < DQuery < NDQuery .
subsorts DQuery Cond < Query .

As implied by the subsort relation above, any fact is a “success assured DML query” (adding this fact to the database). More complex queries in NDRQL(DML) are constructed with the following function symbols (the Maude code is lifted from the module QUERY-SYNTAX). Their semantics is informally defined with the bulletpoints below:

op Ok : -> DQueryS .
op _=>_ : Cond NDQuery -> NDQuery .
op _=>_ : Cond DQuery -> DQuery .
op From_._ : Pat NDQuery -> NDQuery [prec 60] .
op From_._ : PatT DQuery -> DQuery [prec 60] .
op From_._ : PatST DQueryS -> DQuery [prec 60] .
op _|>_ : NDQuery NDQuery -> NDQuery [assoc] .
op _|>_ : DQuery DQuery -> DQuery [assoc] .
op _|>_ : DQueryS DQuery -> DQueryS [assoc] .
op _|>_ : DQuery DQueryS -> DQueryS [assoc] .

Specifications

A specification is a (multi)-set of rewrite rules defined as “big business steps” using a sequence of DML queries executed one after the other. More precisely, a specification is a multiset (of sort QSpec) of business steps of sort LDQuery (labelled DML query). The subsort relation is given by

subsort LDQuery < QSpec .

and multisets of business steps are constructed with

op EmptySpec : -> QSpec .
op _ | _ : QSpec QSpec -> QSpec [assoc comm id: EmptySpec] .

Terms of sort LDQuery have one constructor:

op case : Qid NeDQSeq -> LDQuery .

The first argument is the step's label (where we use Maude's quoted identifiers for the purpose). The second argument is a non-empty sequence of DML queries which are executed (in sequence) upon execution of the given business step. Sequences of DML queries have sort DQSeq, non-empty ones have sort NeDQSeq and the subsort relation is given by:

subsorts DQuery < NeDQSeq < DQSeq .
and the sequences are constructed with operators
op Empty : -> DQSeq .
op _;_ : DQSeq DQSeq -> DQSeq [assoc id: Empty] .
op _;_ : NeDQSeq DQSeq -> NeDQSeq [ditto] .
op _;_ : DQSeq NeDQSeq -> NeDQSeq [ditto] .

Operators related to specifications are defined in module QUERY-ALGEBRA.

Implementation of semantics

In this section we will describe the remaining Maude code related (mostly) to semantics of queries (explaining the semantics along the way). The section is structured around Maude modules not described above. For the description of modules QUERY-BASE, QUERY-SYNTAX and QUERY-ALGEBRA see the previous section.

Module VARIABLES — Helper functions to deal with lists of variables in terms

The module contains helper functions to deal with sets of metarepresentations of variables as well as to extract lists of metarepresentations of variables contained in metarepresentations of terms. In what follows we drop the “meta” part since whether we mean terms or their meta-representations should be clear from the context. Sets of variables are represented simply as lists of terms (of sort TermList) from the META-LEVEL module. The functions below which expect list of variables (such as, e.g., Union or Insert) would not reduce when given as argument a list of terms which contain anything but variables. This is why we define their codomains as having kind but not sort.

First, we have set operations “element of” (_In_), “subset of” (_Sub_), Insert() and Union() defined by the following, rather unsurprising code (note that Insert() and Union() do not permit creation of duplicates):

op _In_ : Variable TermList -> Bool .
op _Sub_ : TermList TermList -> [Bool] .
op Insert : Variable TermList -> [TermList] .
op Union : TermList TermList -> [TermList] .

vars V V' : Variable . vars VV VV' : TermList .

eq V In empty = false .
eq V In (V,  VV) = true .
eq V In (V', VV) = V In VV [owise] .

eq empty Sub VV = true .
eq (V , VV) Sub VV' = (V In VV') and-then (VV Sub VV') .

eq Insert(V, empty) = V .
eq Insert(V, (V, VV)) = V, VV .
eq Insert(V, (V', VV)) = V', Insert(V, VV) [owise] .

eq Union(empty, VV) = VV .
eq Union((V, VV), VV') = Union(VV, Insert(V, VV')) .

Next, there are two mutually recursive functions which extract set of variables occuring in a given term(s). We need VarL which returns variables occurring in a list of terms in addition to the function Var we are really interested in in order to define what Var is on a term constructed with non-constant function symbol.

op Vars : Term -> TermList .
op VarsL : TermList -> TermList .

var C : Constant . var T : Term . var TL : TermList . var Q : Qid . var NTL : NeTermList .

eq Vars(C) = empty .
eq Vars(V) = V .
eq VarsL(empty) = empty .
eq VarsL((T, TL)) = Union(Vars(T), VarsL(TL)) .
eq Vars(Q[NTL]) = VarsL(NTL) .

Finally, there is a function which returns length of a list of variables.

op Len : TermList -> [Nat] .
eq Len(empty) = 0 .
eq Len((V, VV)) = s Len(VV) .

Module IS-CLOSED — Functions related to checking if a query is closed

The module defines functions checking if a NDRQL query is closed. Here closed?(Q) reduces to true if query Q is closed and to false otherwise. Term closed?(Q, VV) reduces to true if Q becomes closed after variables in the list VV are grounded in Q. Functions closed?() have the signature

op closed? : QSpec -> Bool .
op closed? : Query -> Bool .
op closed? : Query TermList -> Bool .
op closed? : QSpec TermList -> Bool .

Remember that Query is the supersort of Cond and DQuery.

To define the above functions we use two helpers: Function Vars() extracting a set of variables occurring in the pattern:

op Vars : Pat -> TermList .
eq Vars(P) = Vars(upTerm(P)) .

and function ext() which adds variables occurring in a pattern to a set of variables:

op ext : Pat TermList -> TermList .
eq ext(P, VV) = Union(Vars(P), VV) .

Two-argument closed?() functions are defined in an obvious way in terms of the two-argument ones:

eq closed?(DQS) = closed?(DQS, empty) .
eq closed?(QS) = closed?(QS, empty) .

Now we can define closedness for conditions (which reflect the fact that quantifiers are binding constructs):

eq closed?(False, VV) = true .
eq closed?({B}, VV) = Vars(upTerm({B})) Sub VV .
eq closed?(Not C, VV) = closed?(C, VV) .
eq closed?(C \/ C', VV) = closed?(C, VV) and closed?(C', VV) .
eq closed?(Exists Ptp . C, VV) = closed?(C, ext(Ptp, VV)) .

The closedness for DML queries (where From_._ operator is binding construct):

eq closed?(Ok, VV) = true .
eq closed?(f, VV) = Vars(upTerm(f)) Sub VV .
eq closed?(C => D, VV) = closed?(C, VV) and closed?(D, VV) .
eq closed?(D |> D', VV) = closed?(D, VV) and closed?(D', VV) .
eq closed?(From P . D, VV) =  closed?(D, ext(P, VV)) .

And finally the closedness of specifications which contain no binding constructs:

eq closed?(Empty, VV) = true .
eq closed?(EmptySpec, VV) = true .
eq closed?(D ; DQS, VV) = closed?(D, VV) and closed?(DQS, VV) .
eq closed?(case(Q, DQS) | QS, VV) = closed?(DQS, VV) and closed?(QS, VV) .

Module REWRITE-BASE — Base module for generated rewrite system

This module defines basic constructors and sorts for the generated rewriting system which executes the query. First of all, the terms which are rewritten have the sort State and they are constructed with:

op new : FactSet FactSetN Bool -> State .
op {_,_} : Conf Stack -> State .

Here new(FS, FSn, B) is the state at the beginning of evaluation of of a DML query, where FS is the current database, FSn are the current fresh fact database (from which we take fresh values), and B indicates if the execution of the previous DML query ended in success or not. Terms of the form {C, S} describe intermediate states in the execution of a DML query, where S is an execution stack used to recursively evaluate the query and C stores intermediate results of the evaluation. Terms of sort Conf are constructed with

op <_|_|_> : FactSet FactSetN FactSet -> Conf .

The first argument is the database of facts the query acts on, the second one is a multiset of fresh facts used to generate fresh values, and the last one is a multiset of facts added so far by the query execution. Those facts are added to the database only after the query execution ends, while facts removed from the database are removed immediately while the query executes.

Terms of sort Stack represent stacks of sort Frame with subsort relation defined by

subsort Frame < Stack .

Stacks are constructed from frames with the following operators (stacks are effectively lists of frames):

op emptySt : -> Stack .
op __ : Stack Stack -> Stack [assoc id: emptySt] .

Most constructors of frames are named after identifier of the corresponding subquery, and so they are declared dynamically during compilation. The only exceptions are the following:

op res : Bool -> Frame .
op neg : -> Frame .

Constant neg is used when evaluating (negated) conditions — it negates the value of the subsequent frame. Every frame is supposed to be, eventually, rewritten to either res(true) or res(false). In case of conditions it encodes whether the condition is satisfied. In case of DML subqueries it encodes whether the execution was successful (res(true)) or not (res(false)). Pairs of result frames next to one another get reduced by “oring” the wrapped booleans:

vars B B' : Bool .
eq res(B) res(B') = res(B or B') .

Frames with names parametrized by the identifier of a subquery wrap local binding of variables. Additionally, frames related to evaluation of quantifiers or From_._ operator store iteration state represented as terms of sort LConf constructed with

op l : FactSet -> LConf .
op l : FactSet Bool -> LConf .
op l : FactSet Bool FactSet -> LConf .

The first argument in all the constructors above is a multiset of facts available for matching (which changes as some facts are removed by (semi)terminating part of the pattern in each step). The boolean argument is used when evaluating From_._ construct to indicate if the iteration was already successful (generated some new facts or Ok). Finally, the last argument in a ternary constructor stores facts removed by the [_]0 pattern in case the computation branch is unsuccessful and the removed facts have to be returned to the database.

Module COMPILE-BASE — Helper functions related to generation of system module from queries

The module contains toolbox of functions which simplify generation of system module from queries. Most of the functions create values in the metalevel. It was found out that constructing values directly, with the constructors from the META-TERM module lead to unreadable, long and tedious code, so it was wothwhile to define even special functions which construct commonly used variables and constants on the metalevel.

First, there is a group of functions which generate metarepresentations of commonly used variables, constants and terms:

ops @true @false @B $true $false $B $B1 $B2 $noneF : -> Term .
eq @true = 'res[$true] .
eq @false = 'res[$false] .
eq @B = 'res['B:Bool] .
eq $noneF = 'noneF.FactSet .
eq $true = 'true.Bool .
eq $false = 'false.Bool .
eq $B = 'B:Bool .
eq $B1 = 'B1:Bool .
eq $B2 = 'B2:Bool .

Next, we define functions fs() and fsn() which, given a name return variable with this name and sort FactSet or FactSetN, respectively. We use those functions to define some more commonly used variables:

ops fs fsn : String -> Variable .
eq fs(S) = qid(S + ":FactSet") .
eq fsn(S) = qid(S + ":FactSetN") .
ops $F $F' $Fn $Ft $FN : -> Variable .
eq $F = fs("F") . eq $F' = fs("F'") . eq $Fn = fsn("Fn") .
eq $Ft = fs("Ft") . eq $FN = fs("FN") .

Most frames store values of variables in their arguments. They are of one of two forms

op [_,_,…,_]N : S1 S2 … Sn -> Frame .
op [_,_,…,_|_]N : S1 S2 … Sn LConf -> Frame .

where N is an identifier of a subquery (or a subcondition) and S1, S2, …, Sn is a sequence of sorts of variables the bindings of which are stored in the frame. Thus, in order to construct the above declarations on the metalevel it is helpful to have the following functions two functions acception a list VV of (representations) of variables: args() which returns a string of the form "_,_,...,_" with as many underscores as there are variables in VV, and sorts() which returns a TypeList with sorts of variables in VV:

op args : Nat -> String .
op args : TermList -> String .
eq args(0) = "" .
eq args(1) = "_" .
eq args(s (s N)) = "_," + args(s N) .
eq args(VV) = args(Len(VV)) .

op sorts : TermList -> TypeList .
eq sorts(empty) = nil .
eq sorts((V, VV)) = getType(V) sorts(VV) .

Using the above functions one defines the following functions which construct names of frame constructors (name() and sname() return names of frame constructors of the form shown above, and sname() returns names of frames used in evaluating specifications):

op sname : Nat -> Qid .
ops name qName : Nat TermList -> Qid .
eq name(N, VV) = qid("[" + args(VV) + "]" + string(N, 10)) .
eq qName(N, VV) = qid("[" + args(VV) + "|_]" + string(N, 10)) .
eq sname(N) = qid("s" + string(N, 10)) .

Functions which return meta-representation of declarations of frame constructors, which use the above name creating functions are as follows:

op declSOp : Nat -> OpDecl .
ops declOp declQOp  : TermList Nat -> OpDecl .
eq declOp(VV, N) = op name(N, VV) : sorts(VV) -> 'Frame [none] . .
eq declQOp(VV, N) = op qName(N, VV) : sorts(VV) 'LConf -> 'Frame [none] . .
eq declSOp(N) = op sname(N) : 'Bool -> 'Frame [none] . .

The meta-level frame constructors are needed when constructing meta-level representation of frame terms in meta-level representations of rules. To make expressions which construct such representations shorter we use the following operators:

op s : Nat Term -> Term .
op $ : Nat TermList -> Term .
op $ : Nat TermList Term -> Term .
eq s(N, T) = sname(N)[T] .
eq $(N, empty) = qid("[]" + string(N, 10) + ".Frame") .
eq $(N, VV) = name(N, VV)[VV] [owise] .
eq $(N, VV, T) = qName(N, VV)[VV, T] .

Rewrite rules we compile our queries into on the metalevel rewrite terms of sort State. In those rules we need to match configuration term (of sort Conf) and the stack with one or two frames of particular shape at the top of stack plus, possibly, the bottom of the stack). Accordingly we have the following functions:

op %1 : Term Term -> Term .
op %2 : Term Term Term -> Term .
op %3 : Term Term Term Term -> Term .
eq %1(T, T') = '`{_`,_`}[T, T'] .
eq %2(T, T1, T2) = %1(T, '__[T1, T2]) .
eq %3(T, T1, T2, T3) = %1(T, '__[T1, T2, T3]) .

Meta-terms constructed with %1(), %2(), and %3() are in most cases too general. Most rules abstract away the bottom of the stack as a variable, and some of the rules do not need to match against the contents of configuration so they abstract the Conf term away as a variable. Accordingly, we have the following functions:

op @ : Term -> Term .
op @2 : Term Term -> Term .
op @c : Term Term -> Term .
op @c2 : Term Term Term -> Term .
eq @(T) = %2('Cf:Conf, 'S:Stack, T) .
eq @2(T1, T2) = %3('Cf:Conf, 'S:Stack, T1, T2) .
eq @c(T, T') = %2(T, 'S:Stack, T') .
eq @c2(T, T1, T2) = %3(T, 'S:Stack, T1, T2) .

Finally, in rules which do need to match against configuration we have the following function which shortens construction of metalevel representation of Conf terms:

op c : Term Term Term -> Term .
eq c(T1, T2, T3) = qid("<_|_|_>")[T1, T2, T3] .

As before, we define some common specializations of the above helper:

ops cF cI cN : -> Term .
eq cF = c($F, $Fn, $FN) .
eq cI = c($F, $Fn, $noneF) .
eq cN = c('_o_[$F, $FN], $Fn, $noneF) .

Module COMPUTE-IDS — Computing identifiers of subqueries

A component of a frame name is the unique identifier of the subquery related to the frame. The identifiers correspond to preorder numbering of the parse tree of the query. We build the rewriting system corresponding to the query recursively and hence, it is necessary to know at the calling function what will be the next identifier available after recursive call. We could make recursively called function return the next identifier as part of the returned value, but we decided that we get easier to understand code if we define a separate function which computes the next identifier after the supquery is processed. More precisely, assuming that N is the current “next available identifier”, and Q is the subquery, then next(N,Q) returns next available identifier after Q is processed. Function next() is defined as follows:

var N : Nat . vars C C' : Cond . var Ptp : PatTP . var B : Bool .
var F : Fact . vars D D' : NDQuery .  var P : Pat . var Q : Qid .

op next : Nat Query -> Nat .
eq next(N, False) = s N .
eq next(N, {B}) = s N .
eq next(N, Not C) = next(s N, C) .
eq next(N, Exists Ptp . C) = next(s N, C) .
eq next(N, C \/ C') = next(next(s N, C), C') .
eq next(N, Ok) = s N .
eq next(N, F) = s N .
eq next(N, C => D) = next(next(s N, D), C) .
eq next(N, D |> D') = next(next(s N, D'), D) .
eq next(N, From P . D) = next(s N, D) .

Module REC-MODULE — A “monad” for building system modules

Each subquery gives rise to both rewrite rules and declarations of corresponding frame constuctors (and possibly further recursive calls of the compiler). In order to simplify building the system module which is the result of compilation of the query (or specification) we introduce a representation of meta-representation (of sort RecModule) of a module together with operations which allow to add equations,rewrite rules and declarations in a way which resembles writer-like monads (in, e.g., Haskell). Terms of sort RecModule are constructed with

op rec : Qid Qid OpDeclSet EquationSet RuleSet Nat -> RecModule .

The last argument is the smallest unused subquery identifier (to be used as part of corresponding frame name). The meaning of remaing arguments are best explained by their sorts as well as the definition of a function which converts RecModule term into SModule term (meta-representation of a system module):

op toModule : RecModule -> SModule .
eq toModule(rec(Q, Q', O, E, R, N)) = (mod Q is
     including Q' .
     sorts none .
     none
     O
     none
     E
     R
  endm) .

Initially (at the start of query compilation) the module contains no operator declarations, equations nor rules, and the next subquery identifier is 0. The following operator constructs this initial term:

op initRec : Qid Qid -> RecModule .
eq initRec(Q, Q') = rec(Q, Q', none, none, none, 0) .

During query compilation declarations, equalities and rewrite rules are added to the RecModule term with operators

op _*_ : RecModule OpDeclSet -> RecModule .
op _*_ : RecModule RuleSet -> RecModule .
op _*_ : RecModule EquationSet -> RecModule .
eq rec(Q, Q', O, E, R, N) * O' = rec(Q, Q', O O', E, R, N) .
eq rec(Q, Q', O, E, R, N) * R' = rec(Q, Q', O, E, R R', N) .
eq rec(Q, Q', O, E, R, N) * E' = rec(Q, Q', O, E E', R, N ) .

Finally, the “next subquery identifier” counter is updated and extracted with operators

op _+_ : RecModule Nat -> RecModule .
eq rec(Q, Q', O, E, R, N) + M = rec(Q, Q', O, E, R, N + M) .

op #_ : RecModule -> Nat .
eq # rec(Q, Q', O, E, R, N) = N .

Module COMPILE-QUERY — Common base module for compiling all sublanguages

This module contains further functions common to the compilation of all the query sublanguages. First, we have helper functions to convert parts of quantifier patterns with a given marking to matching patterns on the metalevel to match against the database (various get functions were defined in the SYNTAX-BASE module):

ops m! m!? m!?0 mn : Term Pat -> Term .

eq m!(T, P) = '_o_[T, upTerm(get!(P))] .
eq m!?(T, P) = '_o_[T, upTerm(get!?(P))] .
eq m!?0(T, P) = '_o_[T, upTerm(get!?0(P))] .
eq mn(T, P) = '_o_[T, upTerm(getn(P))] .

The first argument of the above functions will usually be a metarepresentation of a variable of sort FactSet or FactSetN to match “the rest of the database”

Next, for all binding constructs (quantifiers in conditions and From_._ operator, which for the sake of brevity we refer here simply as “quantifiers”) we need a function which decides when quantifier evaluation must stop, i.e., when there is no more submultisets of facts in the database matchable against the given quantification pattern. Such function is used in a conditional equation (actually, the only case where we use conditional equations and rules). The “end testing” functions for quantifiers with pattern P have signatures of the form

op endN : FactSet S1 S2 … Sn -> Bool .

and they are defined with equations of the form

eq endN(FS o f, V1, V2, …, Vn) = true .
eq endN(FS, V1, V2, …, Vn) = false [owise] .

where f := get!?0(P), and V1, V2, …, Vn are variables bound by both the quantifier and the surrounding context. Since endN() is called with actual values of those of variables V1, V2, …, Vn which are bound by the context (and so fixed for the time of quantifier evaluation) the rule where endN() returns true guarantees that also corresponding variables in the matching pattern are properly bound. Finally, FS is a variable of sort FactSet. Thus, quantifier evaluation must stop when endN(FS, V1, V2, …, Vn)=false.

Since endN() has signature and definition specific to a given quantifying subquery it cannot be defined prior to the compilation of the query, and must instead be added to the constructed metalevel system module constructed during the compilation process. The following code is devoted to that purpose: First, we have the function which creates end function's name from integer:

op eName : Nat -> Qid .
eq eName(N) = qid("end" + string(N, 10)) .

Then, there is the helper which creates an ending condition:

op eCond : Term TermList Nat -> Condition .
eq eCond(T, VV, N) = (eName(N)[T, VV] = $false) .

Finally, we have the function which adds relevant declarations and defining equations to the constructed module (observe that we also declare with declQOp() the relevant frame):

op declQ : Pat TermList RecModule -> RecModule .
ceq declQ(P, VV, RM) = RM * declQOp(VV, N) * (
    eq eName(N)[m!?0($F, P), VV] = $true [none] .
    eq eName(N)[$F, VV] = $false [owise] .
) * (
    op eName(N) : 'FactSet sorts(VV) -> 'Bool [none] .
) if N := (# RM) .

Next, we have the declaration of the main compilation function (for queries only, not specifications) together with the helper which has as the additional argument first unused subquery identifier. The remaining arguments are: the (sub)query to be compiled, a list of variables bound by the context, and initial RecModule:

op compRec : Query TermList RecModule -> RecModule .
op compRec : Query TermList RecModule Nat -> RecModule .
eq compRec(Q, VV, RM) = compRec(Q, VV, RM, (# RM)) .

For a common purpose just declaring a simple frame storing bindings of a given list of variables and advancing the subquery identifier counter we have the following helper operator:

op _^_ : RecModule TermList -> RecModule .
eq RM ^ VV = RM * declOp(VV, (# RM)) + 1 .

The following function is used when compiling the quantifier constructs (either Exists P . Q or From P . Q). It creates declarations of appropriate frame constructors and ending functions (through _^_ and declQ()) and it calls recursively compRec() function with the subquery under quantifier (Q) and list of variables extended with the new variables occurring in the quantification pattern P (with function ext()):

op compQ : Query Pat TermList RecModule -> RecModule .
eq compQ(Q, P, VV, RM) = compRec(Q, ext(P, VV), declQ(P, VV, RM) ^ VV) .

Finally the following function is used when compiling binary operators (i.e., expressions Q1 \/ Q2, Q1 => Q2 and Q1 |> Q2):

op comp2 : Query Query TermList RecModule -> RecModule .
eq comp2(Q1, Q2, VV, RM) = compRec(Q1, VV, compRec(Q2, VV, RM ^ VV)) .

Observe that in addition to calling functions such as compQ, comp2(), etc., we need to add specific equations and rules to the constructed rewriting system which actually evaluate the query.

Module COMPILE-COND — Compiling conditions

To understand the code below it is necessary to recall definitions of the functions from the COMPILE-BASE module. The present module contains definitions of function compRec for compiling conditions. We will proceed by cases corresponding to constructors of conditions.

First, we have condition false:

eq compRec(False, VV, RM, N) = (RM ^ VV) * (eq @($(N, VV)) = @(@false) [none] .) .

Thus we declare a simple frame constructors corresponding to the given occurrence of False which stores the bindings of variables from the context and we also declare the equality which actually evaluates False. The equation, if moved to object level would read:

eq {C:Conf, S:Stack [V1, V2, …, Vn]N} = {C:Conf, S:Stack res(false)} .

Next, we evaluate the embedded booleans

ceq compRec({B}, VV, RM, N) = (RM ^ VV) * (
    eq @($(N, VV)) = @('res[T]) [none] .
) if '`{_`}[T] := upTerm({B}) .

Thus, the equality which evaluates the occurrence (identified by N) of a term {B} is as follows:

eq {C:Conf, S:Stack [V1, V2, … Vn]} = {C:Conf, S:stack res(B)} .

Observe that since variables contained in B must occur among V1, V2, … Vn, it follows that the equality is well defined and moreover B will evaluate fully to either true or false.

Next we evaluate the subcondition of the form Not C:

eq compRec(Not C, VV, RM, N) = compRec(C, VV, RM ^ VV) * (
    eq @($(N, VV)) = @2('neg.Frame, $(s N, VV)) [none] .
    eq @2('neg.Frame, 'res[$B]) = @('res['not_[$B]]) [none] .
) .
Thus, the above code creates both the simple frame with identifier N corresponding to Not C as well as a simple frame with identifier N + 1 corresponding to C. It also adds two equalities, the first of which replaces the frame corresponding to Not C with frame not and frame corresponding to C. The second one negates the result of evaluating the frame corresponding to C. When rewritten in the object level the equations read:
eq {C:Conf, S:Stack [V1, V2, …, Vn]N} = {C:Conf, S:Stack neg [V1, V2, …, Vn]N+1} .

Next there comes evaluation of disjunction:

eq compRec(C \/ C', VV, RM, N) = comp2(C', C, VV, RM) * (
    eq @($(N, VV)) = @2($(s N, VV), $(next(s N, C), VV))  [none] .
    eq @2($(s N, VV), @false) = @($(s N, VV)) [none] .
    eq @2($(s N, VV), @true) = @(@true) [none] .
) .

Note that, in the code above, comp2() call adds definitions of three (simple) frames: frame with identifier N corresponding to the disjunction, frame with identifier N + 1 corresponding to C, and frame with identifier next(N + 1, C) corresponding to C'. Thus, the three equations added by the above code read when descended to object level (as before, N, N+1, next(N+1, C) are to be understood symbolically, as part of frame's name, and not as actual Maude terms):

eq {C:Conf, S:Stack [V1, V2, …, Vn]N} = {C:Conf, S:Stack [V1, V2, …, Vn]N+1 [V1, V2, …, Vn]next(N+1, C)} .
eq {C:Conf, S:Stack [V1, V2, …, Vn]N+1 res(false)} =  {C:Conf, S:Stack [V1, V2, …, Vn]N+1} .
eq {C:Conf, S:Stack [V1, V2, …, Vn]N+1 res(true)} = {C:Conf, S:Stack  res(true)} .

Thus, the first equality replaces the frame corresponding to C \/ C' with frames corresponding to C and to C' (with C' on top). The next two equations implement short-circuted implementation of evaluation of disjunction: first C' is evaluated, if true then C is not evaluated and the result of conjunction is true. Otherwise, C is evaluated.

Finally, we have code which compiles the existential quantifier:
eq compRec((Exists P . C), VV, RM, N) = compQ(C, P, VV, RM) * (
    eq @c(cF, $(N, VV)) = @c(cF, $(N, VV, 'l[$F])) [none] .
    eq @2($(N, VV, 'l[$F]), @false) = @($(N, VV, 'l[$F])) [none] .
    eq @2($(N, VV, 'l[$F]), @true) = @(@true) [none] .
    ceq @($(N, VV, 'l[$F])) = @(@false) if eCond($F, VV, N) [none] .
) * (
    rl @($(N, VV, 'l[m!?($F, P)]))
    			=> @2($(N, VV, 'l[m!($F, P)]), $(s N, ext(P, VV))) [label('Quant)] .
) .
  

The code above defines two: simple and complex (with additional arguments) frames for subformula Exists P . C with identifier N and a simple frame with identifier N + 1 for C. In addition it defines four equations and and a rewrite rule which on the object level read as follows:

eq {< F:FactSet | Fn:FactSetN | FN:FactSet >, S:Stack [V1, V2, …, Vn]N}
     = {< F:FactSet | Fn:FactSetN | FN:FactSet >, S:Stack [V1, V2, …, Vn | l(F:FactSet)]N} .
eq {C:Conf, S:Stack [V1, V2, …, Vn | l(F:FactSet)]N res(false)}
     = {C:Conf, S:Stack [V1, V2, …, Vn | l(F:FactSet)]N} .
eq {C:Conf, S:Stack [V1, V2, …, Vn | l(F:FactSet)]N res(true)} = {C:Conf, S:Stack res(true)} .
ceq {C:Conf, S:Stack [V1, V2, …, Vn | l(F:FactSet)]N}
     = {C:Conf, S:Stack res(false)} if endN(F:FactSet, V1, V2, …, Vn) = false .
rl [Quant] : {C:Conf, S:Stack [V1, V2, …, Vn | l(F:FactSet o P!?)]}
     => {C:Conf, S:Stack [V1, V2, …, Vn | l(F:FactSet o P!)] [V1, V2, …, Vn, W1, W2, …, Wm]N + 1} .

where P!? is the multiset of facts formed by the parts of P marked by [_]! and [_]? and W1, …, Wm are variables of P not among V1, …, Vm.

Module COMPILE-DQUERY — Compiling DML queries

Here we describe definition of compRec() on DML queries. We consider each DML query in turn. First we have the code compiling Ok:

eq compRec(Ok, VV, RM, N) = (RM ^ VV) * (eq @($(N, VV)) = @(@true) [none] .) .

Thus, we add definition of a simple frame and the equation which on the object level reads (Ok always succeeds):

eq {C:Conf, S:Stack [V1, V2, …, Vn]N} = {C:Conf, S:Stack res(true)} .

Next, we have code compiling fact occurrence:

eq compRec(f, VV, RM, N) = (RM ^ VV) * (
    eq @c(cF, $(N, VV)) =  @c(c($F, $Fn, '_o_[$FN, upTerm(f)]), @true) [none] .
) .

The fact is simply added to the multiset of new facts inside the Conf term and the succeeds (by adding res(true) at the top of stack) as expressed by the equation added by the above code, which on the object level reads:

eq {< F | Fn | FN >, S:Stack [V1, V2, …, Vn]N} = {< F | Fn | FN o f >, S:Stack res(true)} .

Next, we have conditional. Using comp2() we create three frames: for the conditional C => D (identifier N), the condition C (identifier next(s N, D)), and conditionally executed subquery D (identifier N + 1). We also add three equations:

eq compRec(C => D, VV, RM, N) = comp2(C, D, VV, RM) * (
    eq @($(N, VV)) = @2($(s N, VV), $(next(s N, D), VV)) [none] .
    eq @2($(s N, VV), @false) = @(@false) [none] .
    eq @2($(s N, VV), @true) = @($(s N, VV)) [none] .
) .

The equations read as follows when translated into object level:

eq {C:Conf, S:Stack [V1, V2, …, Vn]N}
   = {C:Conf, S:Stack [V1, V2, …, Vn]N + 1 [V1, V2, …, Vn]next(s N, D)} .
eq {C:Conf, S:Stack [V1, V2, …, Vn]N + 1 res(false)} = {C:Conf, S:Stack res(false)} .
eq {C:Conf, S:Stack [V1, V2, …, Vn]N + 1 res(true)} = {C:Conf, S:Stack [V1, V2, …, Vn]N + 1} .

Thus, we first evaluate the condition (placed on top of the stack). If it evaluates as true, we evaluate the DML query. Otherwise we fail.

Next we deal with terms of the form D |> D' which must be executed one after the other:

eq compRec(D |> D', VV, RM, N) = comp2(D, D', VV, RM) * (
    eq @($(N, VV)) = @2($(s N, VV), $(next(s N, D'), VV)) [none] .
    eq @2($(s N, VV), @B)  = @2(@B, $(s N, VV)) [none] .
) .

Thus, we create frames for D |> D' (with identifier N), for D' (with identifier N+1) and for D (with identifier next(s N, D')). We also add two equations which when translated to object level read:

eq {C:Conf, S:Stack [V1, V2, …, Vn]N}
     = {C:Conf, S:Stack [V1, V2, …, Vn]N + 1 [V1, V2, …, Vn]next(s N, D')} .
eq {C:Conf, S:Stack [V1, V2, …, Vn]N + 1 res(B:Bool)} = {C:Conf, S:Stack res(B:Bool) [V1, V2, …, Vn]N + 1} .

Observe that after the frame corresponding to D' will be evaluated, we will be left with two res() terms on the top of the stack (call them res(B) and res(B')). Then, another equation will cause those two frames to be replaced by res(B or B2).

Finally, we have the code compiling subqueries of the form From P . D:

eq compRec(From P . D, VV, RM, N) = compQ(D, P, VV, RM) * (
   eq @c(cF, $(N, VV)) = @c(cF, $(N, VV, 'l[$F, $false])) [none] .
   eq @c2(cF, $(N, VV, 'l[$F', $B, $Ft]), @false)
    		    = @c(c('_o_[$F, $Ft], $Fn, $FN), $(N, VV, 'l['_o_[$F', $Ft], $B])) [none] .
   eq @c2(cF, $(N, VV, 'l[$F', $B, $Ft]), @true) = @c(cF, $(N, VV, 'l[$F', $true])) [none] .
   ceq @($(N, VV, 'l[$F', $B])) = @(@B) if eCond($F', VV, N) [none] .
) * (
   rl @c(c(m!?0($F, P), mn($Fn, P), $FN), $(N, VV, 'l[m!?0($F', P), $B]))
    	=> @c2(c(m!?($F, P), '_o_[$Fn, 'refresh[upTerm(getn(P))]], $FN),
           $(N, VV, 'l[m!($F', P), $B, upTerm(get0(P))]), $(s N, ext(P, VV))) [label('From)] .
) .

In order to understand what the above code does, first observe that frames corresponding to From P . D have identifier N and the frame corresponding to D has identifier N + 1. Then, observe that equations and rules added by the above code read when moved to object level as:

eq {< F:FactSet | Fn:FactSetN | FN:FactSet >, S:Stack [V1, V2, …, Vn]N}
    = {< F:FactSet | Fn:FactSetN | FN:FactSet >, S:Stack [V1, V2, …, Vn | l(F:FactSet, false)]N} .
eq {< F:FactSet | Fn:FactSetN | FN:FactSet >, S:Stack [V1, V2, …, Vn | l(F':FactSet, B:Bool, Ft:FactSet)]N res(false)}
    = {< F:FactSet o Ft:FactSet | Fn:FactSetN | FN:FactSet >, S:Stack [V1, V2, …, Vn | l(F':FactSet o Ft:FactSet, B:Bool)]N} .
eq {C:Conf, S:Stack [V1, V2, …, Vn | l(F:FactSet, B:Bool, Ft:FactSet)]N res(true)}
    =  {C:Conf, S:Stack [V1, V2, …, Vn | l(F:FactSet, true)]N} .
ceq {C:Conf, S:Stack [V1, V2, …, Vn | l(F:FactSet, B:Bool)]N}
    = {C:Conf, S:Stack res(B:Bool)} if endN(F:FactSet, V1, V2, …, Vn) = false .
rl [From] : {< P!?0 o F:FactSet | Fn o Pn | FN >, S:Stack [V1, V2, …, Vn | l(P!?0 o F':FactSet, B:Bool)]N}
   => {< P!? o F:FactSet | Fn o refresh(Pn) | FN >, S:Stack [V1, V2, …, Vn | l(P! o F':FactSet, B:Bool, P!)]N [V1, …, Vn, W1, … Wm]N+1} .

where P!?, Pn, etc. denotes the respectively marked part of P, and W1, …, Wm are the variables in P not in V1, …, Vn.

Module COMPILE-SPEC — Compiling specifications

Here we describe functions which are used to compile specifications. The entrypoint of compilation is the following function:

op compile : QSpec Qid Qid -> SModule .

The first argument is the specification, the second the name of the module created as the result of compilation, finally, the third is the name of the module included in the compiled module. The compile() function is defined in terms of two helper functions:

op compRSpec : QSpec RecModule -> RecModule .
op compRSeq : DQSeq RecModule -> RecModule .
eq compile(QS, Q, Q') = toModule(compRSpec(QS, initRec(Q, Q'))) .

Functions compRSpec() and compRSeq recurse through the structure of the specification. First, let us consider definition of compRSpec which compiles terms of sort QSpec:

eq compRSpec(EmptySpec, RM) = RM .
ceq compRSpec(case(Q, DQS) | QS, RM) = compRSpec(QS, compRSeq(DQS, RM * declSOp(# RM) * (
     rl 'new[$F, $Fn, $true] => %2(cI, s(N, $false), $(N, empty)) [label(Q)] .
))) if N := (# RM) .

Recall that declSOp(N) adds declaration of the frame of the form

op sN : Bool -> Frame .

The boolean argument stores the result of execution of a given case. The rule added in the definition of compRSpec() when translated to object level reads as follows:

rl new(F:FactSet, Fn:FactSetN, true) => {< F:FactSet | Fn:FactSetN | noneF >, sN(false) []N} .

Note that only successful execution of a previous case (marked by boolean true in the argument of new() state) get to be rewritten further. The justification is that when searching for reachable states we want to avoid superfulous executions of useles (i.e., doing nothing) transitions, and we cut executions with such transitions early. Note that this also makes this system unsuitable for simple simulations with rewrite command since such execution is likely to stack quickly with unsuccessful transition. After execution of a rule we end with a state with an empty multiset of new facts and two frames on the stack: one, constructed with s(false) which corresponds to the current case and the other, a simple frame with empty set of variable bindings which corresponds to the first DML query in sequence inside the case. Now we are ready do describe compilation of the sequence of DML queries. First we have the code which deals with empty sequence of DML queries which is the last recursive call of compRSeq(), and hence this code must deal with ending of the case execution:

ceq compRSeq(Empty, RM) = (RM + 1) * declOp(empty, # RM) * (
      eq %2(cI, s(N, $B), $(N, empty)) = 'new[$F, $Fn, $B] [none] .
) if N := (# RM) .

There are two things to wonder about in the above code. The first is that it declares an empty simple frame (with declareOp) which is not really used. This is because this frame would be the initial frame for the computation of the next DML query, and previous recursive call assumes its existence. Secondly, the frame constructed with sN() is replaced after each DML query execution in sequence. Thus, N here is usually larger than at the beginning of the execution of the case. It will become clear after showing the rest of the code. The equality introduced above reads (after moving it to the object level):

eq {< F:FactSet | Fn:FactSetN | noneF >, sN(B:Bool) []N} = new(F:FactSet, Fn:FactSetN, B:Bool) .

Next we deal with a DML query in the sequence:

ceq compRSeq((D ; DQS), RM) = compRSeq(DQS, compRec(D, empty, RM * declSOp(N') * (
      eq %2(cF, s(N, $B1), 'res[$B2]) = %2(cN, s(N', '_or_[$B1, $B2]), $(N', empty)) [none] .
))) if N := (# RM) /\ N' := next(N, D) .

Note that we declare here the next “s” - frame in the sequence (with declSOp()). The equation introduced here reads in the object level as follows:

eq {< F:FactSet | Fn:FactSetN | FN:FactSet >, sN(B1:Bool) res(B2:Bool)}
      = {< F:FactSet o FN:FactSet | Fn:FactSetN | noneF >, snext(N, D)(B1:Bool or B2:Bool) []next(N, D)} .

The left-hand side corresponds to what the state looks like after the DML query is evaluated with the result of evaluation on top of the stack. Thus, for the case to succeed, we need at least one of the DML queries in the sequence to succeed.

Example of specification

Example specification (restaurant booking example from the paper) is contained in the file Bookings.maude. In order to run the example run Maude with a command

    maude Bookings.maude
  

(the file also inputs QuerySemantics). Then, e.g., in order to check if the state in which there exists a closed offer (and accepted associated booking) reachable in seven or less business steps from the database with no bookings and offers run the following command (in Maude prompt):

    red searchFinalized(7) .
  

We will now describle each module in Bookings.maude.

BOOKINGS-SYNTAX

In this module we define a syntactic part of the specification. More precisely, we define fact constructors and constants to describe the state of booking (including the token #):

sorts OState BState Rest Person Offer Book Url .
op rest : Rest -> Fact . 
ops agent cust : Person -> Fact .
op offer : Offer OState Rest Person -> Fact .
op book : Book BState Offer Person -> Fact .
op host : Book Person -> Fact .
op prop : Book Url -> Fact .
ops onHold available closed beingBooked : -> OState .
ops drafting submitted canceled finalized accepted toBeValidated : -> BState .
op # : Nat -> Fact .

We also define fresh facts and fresh object constructors:

op C : Offer -> FactN .
op C : Book -> FactN .
op C : Url -> FactN .
op C : Person -> FactN .
op {_}Offer : Nat -> Offer .
op {_}Book : Nat -> Book .
op {_}Url : Nat -> Url .
op {_}Person : Nat -> Person .

as well as the related cases of succ(_) function definition (the function updates the fresh facts):

eq succ(C({N:Nat}Offer)) = C({s N:Nat}Offer) .
eq succ(C({N:Nat}Book)) = C({s N:Nat}Book) .
eq succ(C({N:Nat}Url)) = C({s N:Nat}Url) .
eq succ(C({N:Nat}Person)) = C({s N:Nat}Person) .

Finally, we define some constants to be used later in the specification of the business process:

*** restaurants
ops r1 r2 r3 r4 : -> Rest .

*** agents, customers and hosts
ops a1 a2 a3 c1 c2 c3 c4 h1 h2 h3 h4 : -> Person .

*** offers and bookings
op o : -> Offer .
op b : -> Book .
op u : -> Url .

BOOKINGS-SPEC

The module BOOKINGS-SPEC contains the actual definition of the business process. It is also the module which is included in the generated module during compilation of the specification, so it is important that it includes the REWRITE-BASE module.

We want to define a constant which reduces to a specification (for a future reference). The problem is that the specification uses actual Maude variables, and in equations the set of variables on the right hand side must be contained in the variables on the left hand side. To solve this problem we define a set of constants which reduce to downTerm applied to metarepresentation of a variable. This is an ugly hack but it works:

  ops O1 O2 O3 : -> Offer .
  ops B1 B2 B3 : -> Book .
  ops A1 A2 A3 C1 C2 H1 H2 : -> Person .
  ops R1 R2 : -> Rest .
  ops U1 U2 : -> Url .
  op N : -> Nat .

  eq N = downTerm('N:Nat, 0) .
  eq O1 = downTerm('O1:Offer, o) .
  eq O2 = downTerm('O2:Offer, o) .
  eq O3 = downTerm('O3:Offer, o) .
  eq B1 = downTerm('B1:Book, b) .
  eq B2 = downTerm('B2:Book, b) .
  eq B3 = downTerm('B3:Book, b) .
  eq A1 = downTerm('A1:Person, a1) .
  eq A2 = downTerm('A2:Person, a1) .
  eq A3 = downTerm('A3:Person, a1) .
  eq C1 = downTerm('C1:Person, a1) .
  eq C2 = downTerm('C2:Person, a1) .
  eq H1 = downTerm('H1:Person, a1) .
  eq H2 = downTerm('H2:Person, a1) .
  eq R1 = downTerm('R1:Rest, r1) .
  eq R2 = downTerm('R2:Rest, r1) .
  eq U1 = downTerm('U1:Url, u) .
  eq U2 = downTerm('U2:Url, u) .

Finally, we define a constant bookingSpec which stores the specification of the business process:

  op bookingSpec : -> QSpec .
  eq bookingSpec 
       = case('newOffer, 
           From [#(s N)]0 o [C(O1)]n o [agent(A1)]? o [rest(R1)]! . (
                (Forall [offer(O2, beingBooked, R2, A1)]? . False)
                    => offer(O1, available, R1, A1) 
                        |> (
                              From [offer(O3, available, R2, A1)]0 . offer(O3, onHold, R2, A1)
                        ) |> #(N)
            ) 
        )  
        | case('resume, 
            From [#(s N) o offer(O1, onHold, R1, A1)]0 o [agent(A2)]? . (
                (Forall [offer(O2, beingBooked, R2, A2)]? . False)
                   => (
                       offer(O1, available, R1, A2)
                        |> (
                            From [offer(O2, available, R2, A2)]0 . offer(O2, onHold, R2, A2)
                        ) |> #(N)
                   )
            ) 
        )
        | case('newBooking, 
            From [#(s N) o offer(O1, available, R1, A1)]0 o [C(B1)]n o [cust(C1)]! . (
                    offer(O1, beingBooked, R1, A1) 
                        |> book(B1, drafting, O1, C1)
                        |> #(N)
            )
        )
        | case('closeOffer, 
                From [#(s N) o offer(O1, available, R1, A1)]0 . (
                    offer(O1, closed, R1, A1)
                      |> #(N)
                )
        ) 
        | case('addHost0, 
               From [#(s N)]0 o [C(H1)]n o [book(B1, drafting, O1, C1)]! . (
                   host(B1, H1)
                     |> #(N)
               )
        ) 
        | case('addHost1, 
            From [#(s N)]0 o [book(B1, drafting, O1, C1)]! o [host(B1 , H1)]? . (
                   (Forall [host(B1, H1)]? . False) => (
                       host(B1, H1) 
                         |> #(N)
                   )
            ) 
        ) 
        | case('submit, 
            From [#(s N) o book(B1, drafting, O1, C1)]0 . (
                book(B1, submitted, O1, C1)
                   |> #(N)
            )
        )
        | case('determineProposal, 
            From [#(s N) o book(B1, submitted, O1, C1)]0 o [C(U1)]n . (
                   prop(B1,U1) 
                     |> book(B1,finalized,O1,C1) 
                     |> (From [host(B1, H1)]0 . Ok) 
                     |> #(N)
            )
        ) 
        | case('reject, 
            From [#(s N) o book(B1, submitted, O1, C1) o offer(O1, beingBooked, R1, A1)]0 . (
                book(B1, canceled, O1, C1)
                  |> offer(O1, available, R1, A1)
                  |> (From [host(B1, H1)]0 . Ok)
                  |> #(N)
            )
        ) 
        | case('cancel, 
            From [#(s N) o book(B1, finalized, O1, C1) o offer(O1, beingBooked, R1, A1)]0 . (
                book(B1, canceled, O1, C1)
                  |> offer(O1, available, R1, A1)
                  |> #(N)
            )
        ) 
        | case('accept1,
              From [#(s N) o book(B1, finalized, O1, C1) o offer(O1, beingBooked, R1, A1)]0 o [cust(C1)]? . (
                  (
                      Exists [
                           offer(O2, closed, R1, A2) o book(B2, accepted, O2, C1)
                              o offer(O3, closed, R1, A3) o book(B3, accepted, O3, C1)
                      ]? . True
                  ) => (
                      book(B1, accepted, O1, C1) 
                        |> offer(O1, closed, R1, A1)
                        |> #(N)
                  )
              )
        ) 
        | case('accept2,
              From [#(s N) o book(B1, finalized, O1, C1)]0 o [offer(O1, beingBooked, R1, A1)]? . (
                  Not (
                      Exists [
                           offer(O2, closed, R1, A2) o book(B2, accepted, O2, C1)
                              o offer(O3, closed, R1, A3) o book(B3, accepted, O3, C1)
                      ]? . True
                  ) => (
                      book(B1, toBeValidated, O1, C1) 
                        |> #(N)
                  )
              )
        ) 
        | case('reject2,
              From [#(s N) o book(B1, toBeValidated, O1, C1) o offer(O1, beingBooked, R1, A1)]0 . (
                   book(B1, canceled, O1, C1)
                     |> offer(O1, available, R1, A1)
                     |> #(N)
              )
        ) 
        | case('confirm,
             From [#(s N) o book(B1, toBeValidated, O1, C1) o offer(O1, beingBooked, R1, A1)]0 . (
                 book(B1, accepted, O1, C1)
                     |> offer(O1, closed, R1, A1)
                     |> #(N)
             )
        ) .

EXPERIMENTS

The last module, EXPERIMENTS defines (as a convinience) checking if a state where an offer is closed and a corresponding booking is accepted is reachable from a state with no offers and bookings and a few predefined agents, restaurants, and customers.

First, we compile a specification (BSPEC is the name of compiled module):

op compSpec : -> SModule .
eq compSpec = compile(bookingSpec, 'BSPEC, 'BOOKINGS-SPEC) .

The module defines 196 operators, 285 equations, and 83 rules.

First, we define an initial database, an initial multiset of fresh facts, and an initial state build from them:

  op initDB : Nat -> FactSet .
  op initDBN : -> FactSetN .
  op initState : Nat -> State .
  eq initDB(N:Nat) = agent(a1) o agent(a2) o cust(c1) o cust(c2) o rest(r1) o rest(r2) o #(N:Nat) .
  eq initDBN = C({0}Offer) o C({0}Book) o C({0}Url) o C({0}Person) .
  eq initState(N:Nat) = new(initDB(N:Nat), initDBN, true) .

The initial database (initDB) and state (initState) are parametrized with an initial value of a token. Each business step consumes the token and decreases its parameter by 1. Thus this parameter of initDB limits the depth of search measured in business steps.

Now we define a main operator in this module which performs the actual search:

  op searchFinalized : Nat ~> ResultTriple? .
  eq searchFinalized(N:Nat) = metaSearch(
        compSpec,
        upTerm(initState(N:Nat)),
        upTerm(new(FS o offer(O1, closed, R1, A1) o book(B1, accepted, O1, C1), FSN, true)),
        nil, '+, unbounded, 0
  ) .

Above O1, R1, A1, B1, C1 (imported from module BOOKINGS-SPEC) and FS, FSN defined locally as

    op FS : -> FactSet .
    op FSN : -> FactSetN .
    eq FS = downTerm('FS:FactSet, noneF) .
    eq FSN = downTerm('FSN:FactSetN, noneFN) .

are variables defined as operators with metalevel operators (see the description of the previous module).

Reducing searchFinalized(7) yields the answer:

  reduce in EXPERIMENTS : searchFinalized(7) .
  rewrites: 525165 in 2528ms cpu (2535ms real) (207660 rewrites/second)
  result ResultTriple: {'new['_o_['rest['r1.Rest],'rest['r2.Rest],'agent['a1.Person],'agent['a2.Person],'cust['c1.Person],'cust[
      'c2.Person],'#['s_['0.Zero]],'prop['`{_`}Book['0.Zero],'`{_`}Url['0.Zero]],'offer['`{_`}Offer['0.Zero],'closed.OState,'r1.Rest,
      'a1.Person],'book['`{_`}Book['0.Zero],'accepted.BState,'`{_`}Offer['0.Zero],'c1.Person]],'_o_['C['`{_`}Offer['s_['0.Zero]]],'C[
      '`{_`}Book['s_['0.Zero]]],'C['`{_`}Url['s_['0.Zero]]],'C['`{_`}Person['0.Zero]]],'true.Bool],'State,
          'A1:Person <- 'a1.Person ; 
          'B1:Book <- '`{_`}Book['0.Zero] ; 
          'C1:Person <- 'c1.Person ; 
          'FS:FactSet <- '_o_['rest['r1.Rest],'rest['r2.Rest],'agent['a1.Person],'agent['a2.Person],'cust['c1.Person],'cust['c2.Person],
      '#['s_['0.Zero]],'prop['`{_`}Book['0.Zero],'`{_`}Url['0.Zero]]] ; 
          'FSN:FactSetN <- '_o_['C['`{_`}Offer['s_['0.Zero]]],'C['`{_`}Book['s_['0.Zero]]],'C['`{_`}Url['s_['0.Zero]]],'C['`{_`}Person[
      '0.Zero]]] ; 
          'O1:Offer <- '`{_`}Offer['0.Zero] ; 
          'R1:Rest <- 'r1.Rest}