As pointed out in Section 1.4, given a Maude system module, we can distinguish two levels of specification:
a system specification level, provided by the rewrite theory specified by that system module which defines the behavior of the system, and
a property specification level, given by some property (or properties) that we want to state and prove about our module.
This chapter discusses how a specific property specification logic, linear temporal logic (LTL), and a decision procedure for it, model checking, can be used to prove properties when the set of states reachable from an initial state in a system module is finite. It also explains how this is supported in Maude by its MODEL-CHECKER module, and other related modules, including the SAT-SOLVER module that can be used to check both satisfiability of an LTL formula and LTL tautologies. These modules are found in the file model-checker.maude. In Section 12.6 we consider a simple, but useful, extension of LTL that we call LTL+ and whose model checking is also supported by Maude.
Temporal logic allows specification of properties such as safety properties (ensuring that something bad never happens) and liveness properties (ensuring that something good eventually happens). These properties are related to the infinite behavior of a system. There are different temporal logics [19]; we focus on linear temporal logic [85, 19], because of its intuitive appeal, widespread use, and well-developed proof methods and decision procedures.
Given a set of atomic propositions, we define the formulas of the propositional linear temporal logic inductively as follows:
True: .
Atomic propositions: If , then .
Next operator: If , then .
Until operator: If , then .
Boolean connectives: If , then the formulas , and are in LTL(AP).
Other LTL connectives can be defined in terms of the above minimal set of connectives as follows:
Other Boolean connectives:
False:
Conjunction:
Implication: .
Other temporal operators:
.
The LTL syntax, in a typewriter approximation of the above mathematical syntax, is supported in Maude by the following LTL functional module (in the file model-checker.maude).
fmod LTL is protecting BOOL . sorts Formula . *** primitive LTL operators ops True False : -> Formula [ctor format (g o)] . op ~_ : Formula -> Formula [ctor prec 53 format (r o d)] . op _/\_ : Formula Formula -> Formula [comm ctor gather (E e) prec 55 format (d r o d)] . op _\/_ : Formula Formula -> Formula [comm ctor gather (E e) prec 59 format (d r o d)] . op O_ : Formula -> Formula [ctor prec 53 format (r o d)] . op _U_ : Formula Formula -> Formula [ctor prec 63 format (d r o d)] . op _R_ : Formula Formula -> Formula [ctor prec 63 format (d r o d)] . *** defined LTL operators op _->_ : Formula Formula -> Formula [gather (e E) prec 65 format (d r o d)] . op _<->_ : Formula Formula -> Formula [prec 65 format (d r o d)] . op <>_ : Formula -> Formula [prec 53 format (r o d)] . op []_ : Formula -> Formula [prec 53 format (r d o d)] . op _W_ : Formula Formula -> Formula [prec 63 format (d r o d)] . op _|->_ : Formula Formula -> Formula [prec 63 format (d r o d)] . op _=>_ : Formula Formula -> Formula [gather (e E) prec 65 format (d r o d)] . op _<=>_ : Formula Formula -> Formula [prec 65 format (d r o d)] . vars f g : Formula . eq f -> g = ~ f \/ g . eq f <-> g = (f -> g) /\ (g -> f) . eq <> f = True U f . eq [] f = False R f . eq f W g = (f U g) \/ [] f . eq f |-> g = [](f -> (<> g)) . eq f => g = [] (f -> g) . eq f <=> g = [] (f <-> g) . *** negative normal form eq ~ True = False . eq ~ False = True . eq ~ ~ f = f . eq ~ (f \/ g) = ~ f /\ ~ g . eq ~ (f /\ g) = ~ f \/ ~ g . eq ~ O f = O ~ f . eq ~ (f U g) = (~ f) R (~ g) . eq ~ (f R g) = (~ f) U (~ g) . endfm
Note that, for the moment, no set of atomic propositions has been specified in the LTL module. Section 12.2 explains how such atomic propositions are defined for a given system module M, and Section 12.3 explains how they are added to the LTL module as a subsort Prop of Formula in the MODEL-CHECKER module.
Note that the nonconstructor connectives have been defined in terms of more basic constructor connectives in the first set of equations. But since there are good reasons to put an LTL formula in negative normal form by pushing the negations next to the atomic propositions (this is specified by the second set of equations) we need to consider also the duals of the basic connectives , and (respectively, True, O_, _U_, and _\/_) as constructors. That is, we need to also have as constructors the dual connectives: , and (respectively, False, _R_, and _/\_). Note that is self-dual.
Since the models of temporal logic are Kripke structures, we need to explain how we can associate a Kripke structure to the rewrite theory specified by a Maude system module M.
Kripke structures are the natural models for propositional temporal logic. Essentially, a Kripke structure is a (total) unlabeled transition system to which we have added a collection of unary state predicates on its set of states.
A binary relation on a set is called total if and only if for each there is at least one such that . If is not total, it can be made total by defining .
A Kripke structure is a triple such that is a set, called the set of states, is a total binary relation on , called the transition relation, and is a function, called the labeling function, associating to each state the set of those atomic propositions in that hold in the state .
The semantics of is defined by means of a satisfaction relation
between a Kripke structure having as its atomic propositions, a state , and an LTL formula . Specifically, holds if and only if for each path the path satisfaction relation
holds, where we define the set of computation paths starting at state as the set of functions of the form such that and, for each , we have
We can define the path satisfaction relation (for any path, beginning at any state) inductively as follows:
We always have .
For ,
For ,
where is the successor function, and where .
For ,
For ,
For ,
How can we associate a Kripke structure to the rewrite theory specified by a Maude system module M? We just need to make explicit two things:
the intended kind of states in the signature , and
the relevant state predicates, that is, the relevant set of atomic propositions.
In general, the state predicates need not be part of the system specification and therefore they need not be specified in our system module M. They are typically part of the property specification. This is because the state predicates need not be related to the operational semantics of M: they are just certain predicates about the states of the system specified by M that are needed to specify some properties.
Therefore, after choosing a given kind, say [Foo], in M as our kind for states we can specify the relevant state predicates in a module M-PREDS protecting M according to the following general pattern:
where the dots ‘...’ indicate the part in which the syntax and semantics of the relevant state predicates are specified, as further explained in what follows.
The module SATISFACTION (contained in the model-checker.maude file) is very simple, and has the following specification:
fmod SATISFACTION is protecting BOOL . sorts State Prop . op _|=_ : State Prop ~> Bool [frozen] . endfm
all terms of sort Foo in M are also made terms of sort State. Note that we then have the kind identity .
The operator
is crucial to define the semantics of the relevant state predicates in M-PREDS. Each such state predicate is declared as an operator of sort Prop. In standard LTL propositional logic, the set of atomic propositions is assumed to be a set of constants. In Maude, however, we can define parametric state predicates, that is, operators of sort Prop which need not be constants, but may have one or more sorts as parameter arguments. We then define the semantics of such state predicates (when the predicate holds) by appropriate equations.
Note that the operator _|=_ is declared as partial, using ~> (see Section 3.5). As we will see below, a user is only required to ensure that it reduces to true in the positive case(s) and it can return anything in the negative cases. Declaring it to be partial means that it returns a result in the kind [Bool] rather than sort Bool, thus protecting BOOL, as it is declared in the module SATISFACTION.
We can illustrate all this by means of a simple mutual exclusion example. Suppose that our original system module M is the following module MUTEX, in which two processes, one named a and another named b, can be either waiting or in their critical section, and take turns accessing their critical section by passing each other a different token (either $ or *).
mod MUTEX is sorts Name Mode Proc Token Conf . subsorts Token Proc < Conf . op none : -> Conf [ctor] . op __ : Conf Conf -> Conf [ctor assoc comm id: none] . ops a b : -> Name [ctor] . ops wait critical : -> Mode [ctor] . op [_,_] : Name Mode -> Proc [ctor] . ops * $ : -> Token [ctor] . rl [a-enter] : $ [a, wait] => [a, critical] . rl [b-enter] : * [b, wait] => [b, critical] . rl [a-exit] : [a, critical] => [a, wait] * . rl [b-exit] : [b, critical] => [b, wait] $ . endm
Our obvious kind for states is the kind [Conf] of configurations. In order to state the desired safety and liveness properties we need state predicates telling us whether a process is waiting or is in its critical section. We can make these predicates parametric on the name of the process and define their semantics as follows:
mod MUTEX-PREDS is protecting MUTEX . including SATISFACTION . subsort Conf < State . op crit : Name -> Prop . op wait : Name -> Prop . var N : Name . var C : Conf . var P : Prop . eq [N, critical] C |= crit(N) = true . eq [N, wait] C |= wait(N) = true . eq C |= P = false [owise] . endm
Note the equations, defining when each of the two parametric state predicates holds in a given state.
The above example illustrates a general method by which desired state predicates for a module M are defined in a protecting extension, say M-PREDS, of M which imports SATISFACTION. One specifies the desired states by choosing a sort in M and declaring it as a subsort of State. One then defines the syntax of the desired state predicates as operators of sort Prop, and defines their semantics by means of a set of equations that specify for what states a given state predicate evaluates to true. We assume that those equations, when added to those of M, are (ground) Church-Rosser and terminating, and, furthermore, that M’s equational part is protected when the new operators and equations defining the state predicates are added.
Since we should protect BOOL, it is important to make sure that satisfaction of state predicates is fully defined. This can be checked with Maude’s Sufficient Completeness Checker (SCC) tool. This means that we should give equations for when the predicates are true and when they are false. In practice, however, this can often be reduced to specifying when a predicate is true by means of (possibly conditional) equations of the general form,
because we can use the owise feature (described in Section 4.5.4) to cover all the remaining cases, when it is false, with an equation
In some cases, however—for example, because we want to perform reasoning using formal tools which do not accept owise declarations—we may fully define the true and false cases of a predicate not by using the owise attribute, but by explicit (possibly conditional) equations of the more general form,
where is an arbitrary Boolean expression.
We are now ready to associate with a system module M specifying a rewrite theory (with a selected kind of states and with state predicates defined by means of equations in a protecting extension M-PREDS) a Kripke structure whose atomic predicates are specified by the set
where by convention we use the simplified notation to denote the ground term . This defines a labeling function on the set of states assigning to each the set of atomic propositions
The Kripke structure we are interested in is then
where denotes the total relation extending the one-step -rewriting relation among states of kind , that is, holds if and only if there are and such that is the result of applying one of the rules in to at some position. Under the usual assumptions that is (ground) Church-Rosser and terminating (possibly modulo some axioms contained in ) and is (ground) coherent relative to (again, possibly modulo ), can always be chosen to be the canonical form of under the equations .
Suppose that, given a system module M specifying a rewrite theory , we have:
chosen a kind in M as our kind of states;
defined some state predicates and their semantics in a module, say M-PREDS, protecting M by the method already explained in Section 12.2.
Then, as explained in Section 12.2, this defines a Kripke structure on the set of atomic propositions . Given an initial state and an LTL formula we would like to have a procedure to decide the satisfaction relation
In general this relation is undecidable. It becomes decidable if two conditions hold:
both and are (ground) Church-Rosser and terminating, perhaps modulo some axioms , with a protecting extension, and
is (ground) coherent relative to (again, perhaps modulo some axioms ).
Under these assumptions, both the state predicates and the transition relation are computable and, given the finite reachability assumption, we can then settle the above satisfaction problem using a model-checking procedure.
Specifically, Maude uses an on-the-fly LTL model-checking procedure of the style described in [19]. The basis of this procedure is the following. Each LTL formula has an associated Büchi automaton whose acceptance -language is exactly that of the behaviors satisfying . We can then reduce the satisfaction problem
to the emptiness problem of the language accepted by the synchronous product of and (the Büchi automaton associated with) . The formula is satisfied if and only if such a language is empty. The model-checking procedure checks emptiness by looking for a counterexample, that is, an infinite computation belonging to the language recognized by the synchronous product. For a detailed explanation of this general method of on-the-fly LTL model checking, see [19]; and for a discussion of the specific techniques used in the Maude LTL model-checker implementation, see [53].
This makes clear our interest in obtaining the negative normal form of a formula , since we need it to build the Büchi automaton . For efficiency purposes we need to make as small as possible. The following module LTL-SIMPLIFIER (also in the model-checker.maude file) tries to further simplify the negative normal form of the formula in the hope of generating a smaller Büchi automaton . This module is optional (the user may choose to include it or not when doing model checking) but tends to help building a smaller .
fmod LTL-SIMPLIFIER is including LTL . *** The simplifier is based on: *** Kousha Etessami and Gerard J. Holzman, *** "Optimizing Buchi Automata", CONCUR 2000, LNCS 1877. *** We use the Maude sort system to do much of the work. sorts TrueFormula FalseFormula PureFormula PE-Formula PU-Formula . subsort TrueFormula FalseFormula < PureFormula < PE-Formula PU-Formula < Formula . op True : -> TrueFormula [ctor ditto] . op False : -> FalseFormula [ctor ditto] . op _/\_ : PE-Formula PE-Formula -> PE-Formula [ctor ditto] . op _/\_ : PU-Formula PU-Formula -> PU-Formula [ctor ditto] . op _/\_ : PureFormula PureFormula -> PureFormula [ctor ditto] . op _\/_ : PE-Formula PE-Formula -> PE-Formula [ctor ditto] . op _\/_ : PU-Formula PU-Formula -> PU-Formula [ctor ditto] . op _\/_ : PureFormula PureFormula -> PureFormula [ctor ditto] . op O_ : PE-Formula -> PE-Formula [ctor ditto] . op O_ : PU-Formula -> PU-Formula [ctor ditto] . op O_ : PureFormula -> PureFormula [ctor ditto] . op _U_ : PE-Formula PE-Formula -> PE-Formula [ctor ditto] . op _U_ : PU-Formula PU-Formula -> PU-Formula [ctor ditto] . op _U_ : PureFormula PureFormula -> PureFormula [ctor ditto] . op _U_ : TrueFormula Formula -> PE-Formula [ctor ditto] . op _U_ : TrueFormula PU-Formula -> PureFormula [ctor ditto] . op _R_ : PE-Formula PE-Formula -> PE-Formula [ctor ditto] . op _R_ : PU-Formula PU-Formula -> PU-Formula [ctor ditto] . op _R_ : PureFormula PureFormula -> PureFormula [ctor ditto] . op _R_ : FalseFormula Formula -> PU-Formula [ctor ditto] . op _R_ : FalseFormula PE-Formula -> PureFormula [ctor ditto] . vars p q r s : Formula . var pe : PE-Formula . var pu : PU-Formula . var pr : PureFormula . *** Rules 1, 2 and 3; each with its dual. eq (p U r) /\ (q U r) = (p /\ q) U r . eq (p R r) \/ (q R r) = (p \/ q) R r . eq (p U q) \/ (p U r) = p U (q \/ r) . eq (p R q) /\ (p R r) = p R (q /\ r) . eq True U (p U q) = True U q . eq False R (p R q) = False R q . *** Rules 4 and 5 do most of the work. eq p U pe = pe . eq p R pu = pu . *** An extra rule in the same style. eq O pr = pr . *** We also use the rules from: *** Fabio Somenzi and Roderick Bloem, *** "Efficient Buchi Automata from LTL Formulae", *** p247-263, CAV 2000, LNCS 1633. *** that are not subsumed by the previous system. *** Four pairs of duals. eq O p /\ O q = O (p /\ q) . eq O p \/ O q = O (p \/ q) . eq O p U O q = O (p U q) . eq O p R O q = O (p R q) . eq True U O p = O (True U p) . eq False R O p = O (False R p) . eq (False R (True U p)) \/ (False R (True U q)) = False R (True U (p \/ q)) . eq (True U (False R p)) /\ (True U (False R q)) = True U (False R (p /\ q)) . *** <= relation on formula op _<=_ : Formula Formula -> Bool [prec 75] . eq p <= p = true . eq False <= p = true . eq p <= True = true . ceq p <= (q /\ r) = true if (p <= q) /\ (p <= r) . ceq p <= (q \/ r) = true if p <= q . ceq (p /\ q) <= r = true if p <= r . ceq (p \/ q) <= r = true if (p <= r) /\ (q <= r) . ceq p <= (q U r) = true if p <= r . ceq (p R q) <= r = true if q <= r . ceq (p U q) <= r = true if (p <= r) /\ (q <= r) . ceq p <= (q R r) = true if (p <= q) /\ (p <= r) . ceq (p U q) <= (r U s) = true if (p <= r) /\ (q <= s) . ceq (p R q) <= (r R s) = true if (p <= r) /\ (q <= s) . *** conditional rules depending on <= relation ceq p /\ q = p if p <= q . ceq p \/ q = q if p <= q . ceq p /\ q = False if p <= ~ q . ceq p \/ q = True if ~ p <= q . ceq p U q = q if p <= q . ceq p R q = q if q <= p . ceq p U q = True U q if p =/= True /\ ~ q <= p . ceq p R q = False R q if p =/= False /\ q <= ~ p . ceq p U (q U r) = q U r if p <= q . ceq p R (q R r) = q R r if q <= p . endfm
Suppose that all the requirements listed above to perform model checking are satisfied. How do we then model check a given LTL formula in Maude for a given initial state in a module M? We define a new module, say M-CHECK, according to the following pattern:
mod M-CHECK is protecting M-PREDS . including MODEL-CHECKER . including LTL-SIMPLIFIER . *** optional op init : -> k . *** optional eq init = t . *** optional endm
The module MODEL-CHECKER (in the file model-checker.maude) is as follows:
fmod MODEL-CHECKER is protecting QID . including SATISFACTION . including LTL . subsort Prop < Formula . *** transitions and results sorts RuleName Transition TransitionList ModelCheckResult . subsort Qid < RuleName . subsort Transition < TransitionList . subsort Bool < ModelCheckResult . ops unlabeled deadlock : -> RuleName . op {_,_} : State RuleName -> Transition [ctor] . op nil : -> TransitionList [ctor] . op __ : TransitionList TransitionList -> TransitionList [ctor assoc id: nil] . op counterexample : TransitionList TransitionList -> ModelCheckResult [ctor] . op modelCheck : State Formula ~> ModelCheckResult [special (...)] . endfm
Its key operator is modelCheck (whose special attribute has been omitted here), which takes a state and an LTL formula and returns either the Boolean true if the formula is satisfied, or a counterexample when it is not satisfied. Note that the sort Prop coming from the SATISFACTION module is declared as a subsort of Formula in LTL. In each concrete use of MODEL-CHECKER, such as that in M-CHECK above, the atomic propositions in Prop will have been specified in a module such as M-PREDS.
Let us illustrate the use of this operator with our MUTEX example. Following the pattern described above, we can define the module
mod MUTEX-CHECK is protecting MUTEX-PREDS . including MODEL-CHECKER . including LTL-SIMPLIFIER . ops initial1 initial2 : -> Conf . eq initial1 = $ [a, wait] [b, wait] . eq initial2 = * [a, wait] [b, wait] . endm
The relationships between all the modules involved at this point is illustrated in Figure 12.1, where a single arrow represents an including importation and a triple arrow represents a protecting importation.
We are then ready to model check different LTL properties of MUTEX. The first obvious property to check is mutual exclusion:
Maude> red modelCheck(initial1, [] ~(crit(a) /\ crit(b))) . reduce in MUTEX-CHECK : modelCheck(initial1, []~ (crit(a) /\ crit(b))) . result Bool: true Maude> red modelCheck(initial2, [] ~(crit(a) /\ crit(b))) . reduce in MUTEX-CHECK : modelCheck(initial2, []~ (crit(a) /\ crit(b))) . result Bool: true
We can also model check the strong liveness property that if a process waits infinitely often, then it is in its critical section infinitely often:
Maude> red modelCheck(initial1, ([]<> wait(a)) -> ([]<> crit(a))) . reduce in MUTEX-CHECK : modelCheck(initial1, []<> wait(a) -> []<> crit(a)) . result Bool: true Maude> red modelCheck(initial1, ([]<> wait(b)) -> ([]<> crit(b))) . reduce in MUTEX-CHECK : modelCheck(initial1, []<> wait(b) -> []<> crit(b)) . result Bool: true Maude> red modelCheck(initial2, ([]<> wait(a)) -> ([]<> crit(a))) . reduce in MUTEX-CHECK : modelCheck(initial2, []<> wait(a) -> []<> crit(a)) . result Bool: true Maude> red modelCheck(initial2, ([]<> wait(b)) -> ([]<> crit(b))) . reduce in MUTEX-CHECK : modelCheck(initial2, []<> wait(b) -> []<> crit(b)) . result Bool: true
Of course, not all properties are true. Therefore, instead of a success we can get a counterexample showing why a property fails. Suppose that we want to check whether, beginning in the state initial1, process b will always be waiting. We then get the counterexample:
Maude> red modelCheck(initial1, [] wait(b)) . reduce in MUTEX-CHECK : modelCheck(initial1, []wait(b)) . result ModelCheckResult: counterexample({$ [a, wait] [b, wait], ’a-enter} {[a, critical] [b, wait], ’a-exit} {* [a, wait] [b, wait], ’b-enter}, {[a, wait] [b, critical], ’b-exit} {$ [a, wait] [b, wait], ’a-enter} {[a, critical] [b, wait], ’a-exit} {* [a, wait] [b, wait], ’b-enter})
The main constructors used in the syntax of a counterexample term are:
op {_,_} : State RuleName -> Transition . op nil : -> TransitionList [ctor] . op __ : TransitionList TransitionList -> TransitionList [ctor assoc id: nil] . op counterexample : TransitionList TransitionList -> ModelCheckResult [ctor] .
Therefore, a counterexample is a pair consisting of two lists of transitions, where the first corresponds to a finite path beginning in the initial state, and the second describes a loop. This is because, if an LTL formula is not satisfied by a finite Kripke structure, it is always possible to find a counterexample for having the form of a path of transitions followed by a cycle [19]. Note that each transition is represented as a pair, consisting of a state and the label of the rule applied to reach the next state.
Let us finish this section with an example of how not to use the model checker. Consider the following specification:
mod MODEL-CHECK-BAD-EX is including MODEL-CHECKER . extending LTL . sort Foo . op a : -> Foo . op f : Foo -> Foo . rl a => f(a) . subsort Foo < State . op p : -> Prop . endm
This module describes an infinite transition system of the form
where the property p is not satisfied anywhere. Therefore the state a does not satisfy, e.g., the property []p. However, if we try to invoke Maude with the command
we run into a nonterminating computation.Making explicit that p does not hold in a by adding the equation
does not help. We run into the same problem even if the formula does not contain a temporal operator: modelCheck(a, p) does not terminate either. The reason is that the set of states reachable from a is not finite, and hence one of the main requirements for the model-checking algorithm is not satisfied.A formula is satisfiable if there is a Kripke structure , with , and a computation path such that Satisfiability of a formula is a decidable property. In Maude, the satisfiability decision procedure is supported by the following predefined functional module SAT-SOLVER (also in the file model-checker.maude).
fmod SAT-SOLVER is protecting LTL . *** formula lists and results sorts FormulaList SatSolveResult TautCheckResult . subsort Formula < FormulaList . subsort Bool < SatSolveResult TautCheckResult . op nil : -> FormulaList [ctor] . op _;_ : FormulaList FormulaList -> FormulaList [ctor assoc id: nil] . op model : FormulaList FormulaList -> SatSolveResult [ctor] . op satSolve : Formula ~> SatSolveResult [special (...)] . op counterexample : FormulaList FormulaList -> TautCheckResult [ctor] . op tautCheck : Formula ~> TautCheckResult . op $invert : SatSolveResult -> TautCheckResult . var F : Formula . vars L C : FormulaList . eq tautCheck(F) = $invert(satSolve(~ F)) . eq $invert(false) = true . eq $invert(model(L, C)) = counterexample(L, C) . endfm
One can define the desired atomic predicates in a module extending SAT-SOLVER, such as, for example,
fmod SAT-SOLVER-TEST is extending SAT-SOLVER . extending LTL . ops a b c d e p q r : -> Formula . endfm
The user can then decide the satisfiability of an LTL formula involving those atomic propositions by applying the operator satSolve (whose special attribute has also been omitted in the module above) to the given formula and evaluating the expression. The resulting solution of sort SatSolveResult is then either false, if no model exists, or a finite model satisfying the formula. Such a model is described by a comma-separated pair of finite paths of states: an initial path leading to a cycle. Each state is described by a conjunction of atomic propositions or negated atomic propositions, with the propositions not mentioned in the conjunction being “don’t care” ones. For example, we can evaluate
Maude> red satSolve(a /\ (O b) /\ (O O ((~ c) /\ [](c \/ (O c))))) . reduce in SAT-SOLVER-TEST : satSolve(O O (~ c /\ [](c \/ O c)) /\ (a /\ O b)) . result SatSolveResult: model(a ; b, (~ c) ; c)
We call a tautology if and only if holds for every Kripke structure with , and every state . It then follows easily that is a tautology if and only if is unsatisfiable. Therefore, the module SAT-SOLVER can also be used as a tautology checker. This is accomplished by using the tautCheck, $invert, and counterexample operators and their corresponding equations in SAT-SOLVER. The tautCheck function returns either true if the formula is a tautology, or a finite model that does not satisfy the formula. For example, we can evaluate:
Maude> red tautCheck((a => (O a)) <-> (a => ([] a))) . reduce in SAT-SOLVER-TEST : tautCheck((a => O a) <-> a => []a) . result Bool: true Maude> red tautCheck((a -> (O a)) <-> (a -> ([] a))) . reduce in SAT-SOLVER-TEST : tautCheck((a -> O a) <-> a -> []a) . result TautCheckResult: counterexample(a ; a ; (~ a), True)
The tautology checker gives us also a decision procedure for semantic LTL equality, which is further discussed in [53].
In this section we present a specification of a round-robin scheduling algorithm, and the mutual exclusion and guaranteed reentrance properties are proven about it. Both the algorithm and the property guaranteeing that all processes reenter their critical sections are parameterized by the number of processes. We use Maude’s model checker to prove the mutual exclusion and guaranteed reentrance properties.
This example, though quite simple, is interesting in several ways. The use of parameterization is exploited at both the system and the property level. At the system level, it allows a succinct specification of a parametric system. At the property level, it makes possible the specification of the relevant properties for each value of the parameter, also in a very succinct way. This is quite useful, because the property formulas vary as the parameter changes, and the parametric description encompasses an infinite number of instance formulas.
We first give a specification of natural numbers modulo. Since we want to be able to have any number of processes, we define the NAT/ module parameterized by the functional theory NZNAT#, which requires a constant of sort Nat. Thus, having a view, say 5 from TRIV to NZNAT# mapping # to the natural number 5, the module expression NAT/{5} specifies the natural numbers modulo 5.
fth NZNAT# is protecting NAT . op # : -> NzNat . endfth
fmod NAT/{N :: NZNAT#} is sort Nat/{N} . op [_] : Nat -> Nat/{N} [ctor] . op _+_ : Nat/{N} Nat/{N} -> Nat/{N} . op _*_ : Nat/{N} Nat/{N} -> Nat/{N} . vars N M : Nat . ceq [N] = [N rem #] if N >= # . eq [N] + [M] = [N + M] . eq [N] * [M] = [N * M] . endfm
The round-robin scheduling algorithm is specified in the module RROBIN below. Processes are represented as objects of class Proc, which may be in wait or critical mode, meaning that a process may be either in its critical section or waiting to enter into it. The process getting the token, which is represented as the message go, can enter its critical section. Once a process gets out of its critical section it forwards the token to the next process. The init operator sets up the initial configuration for a given number of processes. Note that Nat/{N} is made a subsort of Oid, making in this way natural numbers modulo N valid object identifiers.
omod RROBIN{N :: NZNAT#} is protecting NAT/{N} . sort Mode . ops wait critical : -> Mode [ctor] . subsort Nat/{N} < Oid . class Proc | mode : Mode . msg go : Nat/{N} -> Msg . var N : Nat . rl [enter] : go([N]) < [N] : Proc | mode : wait > => < [N] : Proc | mode : critical > . rl [exit] : < [N] : Proc | mode : critical > => < [N] : Proc | mode : wait > go([s(N)]) . op init : -> Configuration . op make-init : Nat/{N} -> Configuration . ceq init = go([0]) make-init([N]) if s(N) := # . ceq make-init([s(N)]) = < [s(N)] : Proc | mode : wait > make-init([N]) if N < # . eq make-init([0]) = < [0] : Proc | mode : wait > . endom
For proving mutual exclusion and guaranteed reentrance, we declare the propositions inCrit and twoInCrit in the module CHECK-RROBIN below. The property inCrit takes a Nat/{N} as argument, thus making this property parameterized by the number of processes, and is true when such a process is in its critical section. The property twoInCrit is true if any two processes are in their critical sections simultaneously. Mutual exclusion will be proved directly below, while for proving guaranteed reentrance we use the auxiliary formula guaranteedReentrance, which allows us to specify the property of all processes reentering their critical sections in exactly steps, for the number of processes. For a formula F, nextIter F returns O…O F (where O denotes the modal next operator), which specifies that the property is true in the next iteration, that is, steps later. Note that the expression 2 * # will become two times once the module is instantiated.
omod CHECK-RROBIN{N :: NZNAT#} is pr RROBIN{N} . inc MODEL-CHECKER . inc SATISFACTION . ex LTL-SIMPLIFIER . inc LTL . subsort Configuration < State . op inCrit : Nat/{N} -> Prop . op twoInCrit : -> Prop . var N : Nat . vars X Y : Nat/{N} . var C : Configuration . var F : Formula . eq < X : Proc | mode : critical > C |= inCrit(X) = true . eq < X : Proc | mode : critical > < Y : Proc | mode : critical > C |= twoInCrit = true . op guaranteedReentrance : -> Formula . op allProcessesReenter : Nat -> Formula . op nextIter_ : Formula -> Formula . op nextIterAux : Nat Formula -> Formula . eq guaranteedReentrance = allProcessesReenter(#) . eq allProcessesReenter(s N) = (inCrit([s N]) -> nextIter inCrit([s N])) /\ allProcessesReenter(N) . eq allProcessesReenter(0) = inCrit([0]) -> nextIter inCrit([0]) . eq nextIter F = nextIterAux(2 * #, F) . eq nextIterAux(s N, F) = O nextIterAux(N, F) . eq nextIterAux(0, F) = F . endom
Note that the LTL formula describing the guaranteedReentrance property is not a single LTL formula, but an infinite parametric family of formulas
The use of equations in the above CHECK-RROBIN parameterized module allows us to define this infinite family of formulas by means of a few recursive equations. When this module is instantiated for a concrete value of , we then obtain the concrete LTL formula allProcessesReenter() for that .
We now prove mutual exclusion and guaranteed reentrance for the case of five processes using the model checker.
view 5 from NZNAT# to NAT is op # to term 5 . endv
Of course the answer depends on the property checked and is not always true. The following example shows how the model checker gives a counterexample as result when trying to prove that, for a configuration of five processes, process [1] is in its critical section three steps after it was in it.
Maude> red in CHECK-RROBIN{5} : modelCheck(init, [] (inCrit([1]) -> O O O inCrit([1]))) . result ModelCheckResult: counterexample( { go([0]) < [0] : Proc | mode : wait > < [1] : Proc | mode : wait > < [2] : Proc | mode : wait > < [3] : Proc | mode : wait > < [4] : Proc | mode : wait >, ’enter } { < [0] : Proc | mode : critical > < [1] : Proc | mode : wait > < [2] : Proc | mode : wait > < [3] : Proc | mode : wait > < [4] : Proc | mode : wait >, ’exit } { go([1]) < [0] : Proc | mode : wait > < [1] : Proc | mode : wait > < [2] : Proc | mode : wait > < [3] : Proc | mode : wait > < [4] : Proc | mode : wait >, ’enter } { < [0] : Proc | mode : wait > < [1] : Proc | mode : critical > < [2] : Proc | mode : wait > < [3] : Proc | mode : wait > < [4] : Proc | mode : wait >, ’exit } { go([2]) < [0] : Proc | mode : wait > < [1] : Proc | mode : wait > < [2] : Proc | mode : wait > < [3] : Proc | mode : wait > < [4] : Proc | mode : wait >, ’enter } { < [0] : Proc | mode : wait > < [1] : Proc | mode : wait > < [2] : Proc | mode : critical > < [3] : Proc | mode : wait > < [4] : Proc | mode : wait >, ’exit }, { go([3]) < [0] : Proc | mode : wait > < [1] : Proc | mode : wait > < [2] : Proc | mode : wait > < [3] : Proc | mode : wait > < [4] : Proc | mode : wait >,’enter } { < [0] : Proc | mode : wait > < [1] : Proc | mode : wait > < [2] : Proc | mode : wait > < [3] : Proc | mode : critical > < [4] : Proc | mode : wait >, ’exit } { go([4]) < [0] : Proc | mode : wait > < [1] : Proc | mode : wait > < [2] : Proc | mode : wait > < [3] : Proc | mode : wait > < [4] : Proc | mode : wait >, ’enter } { < [0] : Proc | mode : wait > < [1] : Proc | mode : wait > < [2] : Proc | mode : wait > < [3] : Proc | mode : wait > < [4] : Proc | mode : critical >, ’exit } { go([0]) < [0] : Proc | mode : wait > < [1] : Proc | mode : wait > < [2] : Proc | mode : wait > < [3] : Proc | mode : wait > < [4] : Proc | mode : wait >,’enter} { < [0] : Proc | mode : critical > < [1] : Proc | mode : wait > < [2] : Proc | mode : wait > < [3] : Proc | mode : wait > < [4] : Proc | mode : wait >,’exit} { go([1]) < [0] : Proc | mode : wait > < [1] : Proc | mode : wait > < [2] : Proc | mode : wait > < [3] : Proc | mode : wait > < [4] : Proc | mode : wait >,’enter} { < [0] : Proc | mode : wait > < [1] : Proc | mode : critical > < [2] : Proc | mode : wait > < [3] : Proc | mode : wait > < [4] : Proc | mode : wait >,’exit} { go([2]) < [0] : Proc | mode : wait > < [1] : Proc | mode : wait > < [2] : Proc | mode : wait > < [3] : Proc | mode : wait > < [4] : Proc | mode : wait >, ’enter } { < [0] : Proc | mode : wait > < [1] : Proc | mode : wait > < [2] : Proc | mode : critical > < [3] : Proc | mode : wait > < [ 4] : Proc | mode : wait >,’exit } )
In [24, Section 16.6] some properties of a Mobile Maude application are model checked. This example is interesting because two levels of reflection (see Chapter 17) are involved: the object level, at which Mobile Maude system code executes, and the metalevel, at which application code is executed.
An LTL formula is a property that (tacitly) is universally quantified over all the computation paths starting at an initial state . That is, if is a Maude system module in which the state predicates appearing in have been equationally defined and is an initial state in , then holds iff for all computation paths of starting at , holds (where we have left implicit the Kripke structure associated to ). In a logic like CTL* [19] such a universal path quantifier would be made explicit, so that the LTL formula would be written as the universally path quantified formula A . There are useful properties of a concurrent system not expressible in LTL itself.
Consider, for example, the following object-oriented module specifying a simple communication protocol in Maude:
fmod ANOTHER-NAT-LIST is protecting NAT . sort List . subsorts Nat < List . op nil : -> List . op _;_ : List List -> List [assoc id: nil] . op |_| : List -> Nat . *** length function var N : Nat . var L : List . eq | nil | = 0 . eq | N ; L | = s(| L |) . endfm
omod COMM-PROTOCOL is protecting ANOTHER-NAT-LIST . protecting QID . subsort Qid < Oid . class Sender | buff : List, rec : Oid, cnt : Nat, ack-w : Bool . class Receiver | buff : List, snd : Oid, cnt : Nat . msg to_from_val_cnt_ : Oid Oid Nat Nat -> Msg . msg to_from_ack_ : Oid Oid Nat -> Msg . op init : Oid Oid List -> Configuration . vars N M : Nat . vars L Q : List . vars A B : Oid . var TV : Bool . eq init(A,B,L) = < A : Sender | buff : L, rec : B, cnt : 0, ack-w : false > < B : Receiver | buff : nil, snd : A, cnt : 0 > . rl [snd] : < A : Sender | buff : (N ; L), rec : B, cnt : M, ack-w : false > => (to B from A val N cnt M) < A : Sender | buff : L, cnt : M, ack-w : true > . rl [rec] : < B : Receiver | buff : L, snd : A, cnt : M > (to B from A val N cnt M) => (to A from B ack M) < B : Receiver | buff : (L ; N), snd : A, cnt : s(M) > . rl [ack-rec] : (to A from B ack M) < A : Sender | buff : L, rec : B, cnt : M, ack-w : true > => < A : Sender | buff : L, rec : B, cnt : s(M), ack-w : false > . endom
There are two classes, for Senders and for Receivers, and two messages, for sending a value and for sending an acknowledgement. The equation defines an initial state with one Sender and one Receiver; the first two arguments of the init operator provide the respective names for those two objects, while the third argument provides the list of values that are going to be sent.
There are three rules: snd creates a message to send a value, rec puts the value from the message into the receiver’s buffer while creating an acknowledgement message, and finally ack-rec handles this last message.
We could ask the following question about this protocol:
Are there states reachable from init(A,B,L) such that the counters of A and B hold different values?
This property about COMM-PROTOCOL cannot be expressed in LTL, but has a simple expression as a formula existentially quantified over paths, namely, the formula:
where the predicate is defined as follows:
omod COMM-PROTOCOL-PREDS is protecting COMM-PROTOCOL . including SATISFACTION . subsort Configuration < State . op same.cnts : Oid Oid -> Prop . vars N M : Nat . vars L1 L2 : List . vars A B : Oid . var TV : Bool . var C : Configuration . eq < A : Sender | buff : L2, rec : B, cnt : N, ack-w : TV > < B : Receiver | buff : L1, snd : A, cnt : M > C |= same.cnts(A,B) = N == M . endom
By definition, holds iff there is a computation path in COMM-PROTOCOL-PREDS starting at init(A,B,L) such that
holds (where we have again left implicit the associated Kripke structure).
Two points should be noted about the above definitional equivalence: (i) since our formula is not an LTL formula but belongs to an extended temporal logic that we shall call LTL+, the semantic satisfaction relation is qualified as ; and (ii) in such a definitional extension the path satisfaction relation used is . That is, the meaning of our LTL+ formula can be expressed in terms of the LTL path satisfaction relation. What is LTL+? In Maude it is defined as the following functional module (in the file model-checker.maude).
fmod LTL+ is protecting LTL . sort Formula+ . op E_ : Formula -> Formula+ [ctor] . op A_ : Formula -> Formula+ [ctor] . op ~_ : Formula+ -> Formula+ . var f : Formula . eq ~(E f) = A (~ f) . eq ~(A f) = E (~ f) . endfm
That is, an LTL+ formula has one of two forms: it is either an existentially path quantified formula or a universally path quantified formula , with an LTL formula. LTL+ formulas are closed under negation as expressed by the two equations in LTL+.
Using the duality of path quantifiers , we have the following two definitional equivalences that define the semantics of LTL+ formulas in terms of the semantics of LTL formulas:
These equivalences give us an easy way of extending Maude’s LTL model checker to an LTL+ model checker, which is defined in Maude (also in the file model-checker.maude) as follows:
fmod MODEL-CHECKER+ is protecting MODEL-CHECKER . including LTL+ . sort ModelCheck+Result . subsort ModelCheckResult < ModelCheck+Result . op witness : TransitionList TransitionList -> ModelCheck+Result [ctor] . op modelCheck+ : State Formula+ ~> ModelCheck+Result . op neg : ModelCheck+Result -> ModelCheck+Result . var f : Formula . var st : State . vars TL1 TL2 : TransitionList . eq modelCheck+(st, (E f)) = neg(modelCheck(st, (~ f))) . eq modelCheck+(st, (A f)) = modelCheck(st, f) . eq neg(true) = false . eq neg(false) = true . eq neg(counterexample(TL1, TL2)) = witness(TL1, TL2) . eq neg(witness(TL1, TL2)) = counterexample(TL1, TL2) . endfm
Using MODEL-CHECKER+ we can answer our original question about whether in some states reachable from init(A,B,L) the values in the counters of A and B differ. We can do so by loading the module:
omod COMM-PROTOCOL-CHECK is protecting COMM-PROTOCOL-PREDS . including MODEL-CHECKER+ . endom
and then giving the command
Maude> red in COMM-PROTOCOL-CHECK : modelCheck+(init(’a, ’b, 1 ; 2 ; 3), E (<> ~ same.cnts(’a, ’b))) . result ModelCheck+Result: witness( {< ’a : Sender | buff : (1 ; 2 ; 3), rec : ’b, cnt : 0, ack-w : false > < ’b : Receiver | buff : nil, cnt : 0, snd :’a >, ’snd} {< ’a : Sender | buff : (2 ; 3), rec : ’b, cnt : 0, ack-w : true > < ’b : Receiver | buff : nil, cnt : 0, snd : ’a > to ’b from ’a val 1 cnt 0, ’rec} {< ’a : Sender | buff : (2 ; 3), rec : ’b, cnt : 0, ack-w : true > < ’b : Receiver | buff : 1, cnt : 1, snd : ’a > to ’a from ’b ack 0, ’ack-rec} {< ’a : Sender | buff : (2 ; 3), rec : ’b, cnt : 1, ack-w : false > < ’b : Receiver | buff : 1, cnt : 1, snd : ’a >, ’snd} {< ’a : Sender | buff : 3, rec : ’b, cnt : 1, ack-w : true > < ’b : Receiver | buff : 1, cnt : 1, snd : ’a > to ’b from ’a val 2 cnt 1, ’rec} {< ’a : Sender | buff : 3, rec : ’b, cnt : 1, ack-w : true > < ’b : Receiver | buff : (1 ; 2), cnt : 2, snd : ’a > to ’a from ’b ack 1, ’ack-rec} {< ’a : Sender | buff : 3, rec : ’b, cnt : 2, ack-w : false > < ’b : Receiver | buff : (1 ; 2), cnt : 2, snd : ’a >, ’snd} {< ’a : Sender | buff : nil, rec : ’b, cnt : 2, ack-w : true > < ’b : Receiver | buff : (1 ; 2), cnt : 2, snd : ’a > to ’b from ’a val 3 cnt 2, ’rec} {< ’a : Sender | buff : nil, rec : ’b, cnt : 2, ack-w : true > < ’b : Receiver | buff : (1 ; 2 ; 3), cnt : 3, snd : ’a > to ’a from ’b ack 2, ’ack-rec}, {< ’a : Sender | buff : nil, rec : ’b, cnt : 3, ack-w : false > < ’b : Receiver | buff : (1 ; 2 ; 3), cnt : 3, snd : ’a >, deadlock})
That is, the LTL+ model cheker verifies that our LTL+ property E (<> ~ same.cnts( ’a, ’b)) holds from the initial state init(’a, ’b, 1 ; 2 ; 3) in a constructive way, i.e., by providing a witness infinite path showing states where the values of the two counters differ.
Note that the equations
eq modelCheck+(st, (E f)) = neg(modelCheck(st, (~ f))) . eq neg(witness(TL1, TL2)) = counterexample(TL1, TL2) .
in MODEL-CHECKER+ identify a witness for an LTL+ formula with a counterexample for the negated LTL formula . For example, the above witness proving the LTL+ formula
from the initial state init(’a, ’b, 1 ; 2 ; 3) exactly corresponds to a counterexample for the negated LTL formula
from the same initial state, i.e., a counterexample for the equivalent LTL formula
stating that is an invariant from that initial state. Indeed, we get:
Maude> red in COMM-PROTOCOL-CHECK : modelCheck(init(’a,’b,(1 ; 2 ; 3)),([] same.cnts(’a,’b))) . result ModelCheckResult: counterexample( {< ’a : Sender | buff : (1 ; 2 ; 3), rec : ’b, cnt : 0, ack-w : false > < ’b : Receiver | buff : nil, cnt : 0, snd : ’a >, ’snd} {< ’a : Sender | buff : (2 ; 3), rec : ’b, cnt : 0, ack-w : true > < ’b : Receiver | buff : nil, cnt : 0, snd : ’a > to ’b from ’a val 1 cnt 0, ’rec} {< ’a : Sender | buff : (2 ; 3), rec : ’b, cnt : 0, ack-w : true > < ’b : Receiver | buff : 1, cnt : 1, snd : ’a > to ’a from ’b ack 0, ’ack-rec} {< ’a : Sender | buff : (2 ; 3), rec : ’b, cnt : 1, ack-w : false > < ’b : Receiver | buff : 1, cnt : 1, snd : ’a >, ’snd} {< ’a : Sender | buff : 3, rec : ’b, cnt : 1, ack-w : true > < ’b : Receiver | buff : 1, cnt : 1, snd : ’a > to ’b from ’a val 2 cnt 1, ’rec} {< ’a : Sender | buff : 3, rec : ’b, cnt : 1, ack-w : true > < ’b : Receiver | buff : (1 ; 2), cnt : 2, snd : ’a > to ’a from ’b ack 1, ’ack-rec} {< ’a : Sender | buff : 3, rec : ’b, cnt : 2, ack-w : false > < ’b : Receiver | buff : (1 ; 2), cnt : 2, snd : ’a >, ’snd} {< ’a : Sender | buff : nil, rec : ’b, cnt : 2, ack-w : true > < ’b : Receiver | buff : (1 ; 2), cnt : 2, snd : ’a > to ’b from ’a val 3 cnt 2, ’rec} {< ’a : Sender | buff : nil, rec : ’b, cnt : 2, ack-w : true > < ’b : Receiver | buff : (1 ; 2 ; 3), cnt : 3, snd : ’a > to ’a from ’b ack 2, ’ack-rec}, {< ’a : Sender | buff : nil, rec : ’b, cnt : 3, ack-w : false > < ’b : Receiver | buff : (1 ; 2 ; 3), cnt : 3, snd : ’a >, deadlock})
I.e., the same computation path is, simultaneously, a witness for the truth of the LTL+ formula and a counterexample for the LTL formula from the same initial state init(’a, ’b, 1 ; 2 ; 3).