Academia.eduAcademia.edu

Rewriting-Based Techniques for Runtime Verification

2005, Automated Software Engineering

Techniques for efficiently evaluating future time Linear Temporal Logic (abbreviated LTL) formulae on finite execution traces are presented. While the standard models of LTL are infinite traces, finite traces appear naturally when testing and/or monitoring real applications that only run for limited time periods. A finite trace variant of LTL is formally defined, together with an immediate executable semantics which turns out to be quite inefficient if used directly, via rewriting, as a monitoring procedure. Then three algorithms are investigated. First, a simple synthesis algorithm for monitors based on dynamic programming is presented; despite the efficiency of the generated monitors, they unfortunately need to analyze the trace backwards, thus making them unusable in most practical situations. To circumvent this problem, two rewriting-based practical algorithms are further investigated, one using rewriting directly as a means for online monitoring, and the other using rewriting to generate automata-like monitors, called binary transition tree finite state machines (and abbreviated BTT-FSMs). Both rewriting algorithms are implemented in Maude, an executable specification language based on a very efficient implementation of term rewriting. The first rewriting algorithm essentially consists of a set of equations establishing an executable semantics of LTL, using a simple formula transforming approach. This algorithm is further improved to build automata on-the-fly via caching and reuse of rewrites (called memoization), resulting in a very efficient and small Maude program that can be used to monitor program executions. The second rewriting algorithm builds on the first one and synthesizes provably minimal BTT-FSMs from LTL formulae, which can then be used to analyze execution traces online without the need for a rewriting system. The presented work is part of an ambitious runtime verification and monitoring project at NASA Ames, called Path Explorer, and demonstrates that rewriting can be a tractable and attractive means for experimenting and implementing logics for program monitoring.

Rewriting-based Techniques for Runtime Verification Grigore Roşu∗ Klaus Havelund [email protected] [email protected] Department of Computer Science University of Illinois at Urbana-Champaign Kestrel Technology NASA Ames Research Center Abstract Techniques for efficiently evaluating future time Linear Temporal Logic (abbreviated LTL) formulae on finite execution traces are presented. While the standard models of LTL are infinite traces, finite traces appear naturally when testing and/or monitoring real applications that only run for limited time periods. A finite trace variant of LTL is formally defined, together with an immediate executable semantics which turns out to be quite inefficient if used directly, via rewriting, as a monitoring procedure. Then three algorithms are investigated. First, a simple synthesis algorithm for monitors based on dynamic programming is presented; despite the efficiency of the generated monitors, they unfortunately need to analyze the trace backwards, thus making them unusable in most practical situations. To circumvent this problem, two rewritingbased practical algorithms are further investigated, one using rewriting directly as a means for online monitoring, and the other using rewriting to generate automata-like monitors, called binary transition tree finite state machines (and abbreviated BTT-FSMs). Both rewriting algorithms are implemented in Maude, an executable specification language based on a very efficient implementation of term rewriting. The first rewriting algorithm essentially consists of a set of equations establishing an executable semantics of LTL, using a simple formula transforming approach. This algorithm is further improved to build automata on-the-fly via caching and reuse of rewrites (called memoization), resulting in a very efficient and small Maude program that can be used to monitor program executions. The second rewriting algorithm builds on the first one and synthesizes provably minimal BTT-FSMs from LTL formulae, which can then be used to analyze execution traces online without the need for a rewriting system. The presented work is part of an ambitious runtime verification and monitoring project at NASA Ames, called PathExplorer, and demonstrates that rewriting can be a tractable and attractive means for experimenting and implementing logics for program monitoring. 1 Introduction and Motivation Future time Linear Temporal Logic, abbreviated LTL, was introduced by Pnueli in 1977 [51] (see also [44, 45]) for stating properties about reactive and concurrent systems. LTL provides temporal operators that refer to the future/remaining part of an execution trace relative to a current point of reference. The standard models of LTL are infinite execution traces, reflecting the behavior of such systems as ideally always being ready to respond to requests. Methods, such as model checking, have been developed for proving programs correct with respect to requirements specified as LTL formulae. Several systems are currently being developed that apply model checking to software ∗ Supported in part by joint NSF/NASA grant CCR-0234524. 1 systems written in Java, C and C++ [2, 11, 35, 9, 50, 18, 61, 27, 62]. However, for very large systems, there is little hope that one can actually prove correctness, and one must in those cases rely on debugging and testing. In the context of highly reliable and/or safety critical systems, one would actually want to monitor a program execution during operation and to determine whether it conforms to its specification. Any violation of the specification can then be used to guide the execution of the program into a safe state, either manually or automatically. In this paper we describe a collection of algorithms for monitoring program executions against LTL formulae. It is demonstrated how term rewriting, and in particular the Maude rewriting system [7], can be used to implement some of these algorithms very efficiently and conveniently. The work presented in this paper has been started as part of, and stimulated by, the PathExplorer project at NASA Ames, and in particular the Java PathExplorer (JPaX) tool [28, 29] for monitoring Java programs. JPaX facilitates automated instrumentation of Java byte-code, currently using Compaq’s Jtrek which is not public anymore, but soon using BCEL [10]. The instrumented code emits relevant events to an observer during execution (see Figure 1). The observer can be running a Maude [7] process as a special case, so Maude’s rewriting engine can be used to drive a temporal logic operational semantics with program execution events. The observer may run on a different computer, in which case the events are transmitted over a socket. The system is driven by a specification, stating what properties to be proved and what parts of the code to be instrumented. When the observer receives the events it dispatches these to a set of observer modules, each module performing a particular analysis that has been requested. In addition to checking temporal logic requirements, modules have also been programmed to perform error pattern analysis of multi-threaded programs, predicting deadlock and datarace potentials. Specifications Instrumentation Verification Java Program Observer Deadlock Bytecode Dispatcher Event Stream Compile Datarace LTL Maude Instrument ... Instrumented Bytecode Execute (JVM) Figure 1: Overview of JPaX . Using temporal logic in testing is an idea of broad practical and theoretical interest. One example is the commercial Temporal Rover and DBRover tools [12, 13], in which LTL properties are translated into code, which is then inserted at chosen positions in the program and executed whenever reached during program execution. The MaC tool [43, 38] is another example of a runtime monitoring tool. Here, Java byte-code is automatically instrumented to generate events of interest during the execution. Of special interest is the temporal logic used in MaC, which 2 can be classified as a past time interval logic convenient for expressing monitoring properties in a succinct way. All the systems above try to discharge the program execution events as soon as possible, in order to minimize the space requirements. In contrast, a technique is proposed in [39] where the execution events are stored in an SQL database at runtime and then analyzed by means of queries after the program terminates. The PET tool, described in [24, 23, 22], uses a future time temporal logic formula to guide the execution of a program for debugging purposes. Java MultiPathExplorer [58] is a tool which checks a past time LTL safety formula against a partial order extracted online from an execution trace. POTA [56] is another partial order trace analyzer system. Java-MoP [5] is a generic logic monitoring tool encouraging “monitoring-oriented programming” as a paradigm merging specification and implementation. Complexity results for testing a finite trace against temporal formulae expressed in different temporal logics are investigated in [46]. Algorithms using alternating automata to monitor LTL properties are proposed in [16], and a specialized LTL collecting statistics along the execution trace is described in [15]. Various algorithms to generate testing automata from temporal logic formulae are discussed in [52, 49], and [17] presents a Büchi automata inspired algorithm adapted to finite trace LTL. The major goal of this paper is to present rewriting-based algorithms for effectively and efficiently evaluating LTL formulae on finite execution traces online, that is, by processing each event as it arrives. An important contribution of this paper is to show how a rewriting system, such as Maude, makes it possible to experiment with monitoring logics very efficiently and elegantly, and furthermore can be used as a practical program monitoring engine. This approach allows one to formalize ideas in a framework close to standard mathematics. The presented algorithms are considered in the context of JPaX, but they can be easily adapted and used within other monitoring frameworks. We claim that the techniques presented in this paper, even though applied to LTL, are in fact generic and can be easily applied to other logics for monitoring. For example, in [54, 57] we applied the same generic, “formula transforming”, techniques to obtain rewriting based algorithms for situations in which the logic for monitoring was replaced by extended regular expressions (regular expressions with complement). A non-trivial application of the rewriting based techniques presented in this paper is X9, a test-case generation and monitoring environment for a software system that controls the planetary NASA rover K9. This collaborative effort is described in more detail in [1] and it will be presented in full detail elsewhere soon. The rover controller, programmed in 35,000 lines of C++, essentially executes plans, where a plan is a tree-like structure consisting of actions and sub-actions. The leaf actions control various hardware on the rover, such as for example the camera and the wheels. The execution of a plan must cause the actions to be executed in the right order and must satisfy various time constraints, also part of the plan. Actions can start and eventually either terminate successfully or fail. Plans can specify how failed sub-actions can propagate upwards. Testing the rover controller consists of generating plans and then monitoring that the plan actions are executed in the right order and that failures are propagated correctly. X9 automatically generates plans from a “grammar” of the structure of plans, using the Java PathFinder model checker [62]. For each plan, a set of temporal formulae that an execution of the plan must satisfy is also generated. For example, a plan may state that an action a should be executed by first executing a sub-action a1 and then a sub-action a2 , and that the failure of any of the sub-actions should not propagate: action a should eventually succeed, regardless of whether a1 or a2 fails. The generated temporal formulae will state these requirements, such as for example [](start(a) -> <>succeed(a)) saying that “it is always the case ([]) that when action a starts, then eventually 3 (<>) it terminates successfully”, and execution traces are monitored against them. X9 is currently being turned into a mature system to be used by the developer. It is completely automated, generating a web-page containing all the warnings found. The top-level web-page identifies all the test-cases that have failed (by violating some of the temporal properties), each linked to a web-page containing specifics such as the plan, the execution trace, and the properties that are violated. X9 has itself been tested by seeding errors into the rover controller code. The automated monitoring relieves the programmer from manually analyzing printed execution traces. Extending the logic with real-time, as is planned in future work, is crucial for this application since plans are heavily annotated with time constraints. In Section 2, based on our experience, we give a rough classification of monitoring and runtime analysis algorithms by considering three important criteria. A first criterion is whether the execution trace of the monitored or analyzed program needs to be stored or not. Storing a trace might be very useful for specific types of analysis because one could have random access to events, but storing an execution trace is an expensive operation in practice, so sometimes trace-storing algorithms may not be desirable. A second criterion regards the synchronicity of the monitor, more precisely whether the monitor is able to react as soon as the specification or the requirement has been violated. Synchronicity may often trigger running a validity checker for the logic under consideration, which is typically a very expensive task. Finally, monitoring and analysis algorithms can also be classified as “predictive” versus “exact”, where the “exact” ones monitor the observed execution trace as a flat list of events, while the predictive algorithms try to guess potential erroneous behaviors of programs that can occur under different executions. All the algorithms in this paper are exact. This paper requires a certain amount of mathematical notions and notations, which we introduce in Section 3 together with Maude [7], a high-performance system supporting both membership equational logic [48] and rewriting logic [47]. The current version of Maude can do more than 3 million rewritings per second on standard PCs, and its compiled version is intended to support more than 15 million rewritings per second1 , so it can quite well be used as an implementation language. Section 4 defines the finite trace variant of linear temporal logic that we use in the rest of the paper. We found, by carefully analyzing several practical examples, that the most appropriate assumption to make at the end of the trace is that it is stationary in the last state. Then we define the semantics of the temporal operators using their usual meaning in infinite trace LTL, where the finite trace is infinitely extended by repeating the last state. Another option would be to consider that all atomic predicates are false or true in the state following the last one, but this would be problematic when inter-dependent predicates are involved, such as “gate-up” and “gate-down”. In previous work we described a technique which synthesizes efficient dynamic programming algorithms for checking LTL formulae on finite execution traces [53]. Even though this algorithm is not dependent on rewriting (but it could be easily implemented in Maude by rewriting as we did with its dual variant for past time LTL [32, 5]), for the sake of completeness we present it in some detail in Section 5. This algorithm evaluates a formula bottom-up for each point in the trace, going backwards from the final state towards the initial state. Unfortunately, despite its linear complexity, this algorithm cannot be used online because it is both asynchronous and tracestoring. In [33, 25, 32] we dualize this technique and apply it to past time LTL, in which case the trace more naturally can be examined in a forwards direction synchronously. 1 Personal communication by José Meseguer. 4 Section 6 presents our first practical rewriting-based algorithm, which can directly monitor an LTL formula. This algorithm originates in [31, 53] and it was partially presented at the Automated Software Engineering conference [30]. The algorithm is expressed as a set of equations establishing an executable semantics of LTL using a simple formula transforming approach. The idea is to rewrite or transform an LTL monitoring requirement formula ϕ when an event e is received, to a formula ϕ{e}, which represents the new requirement that the monitored system should fulfill for the remaining part of the trace. This way, the LTL formula to monitor “evolves” into other LTL formulae by subsequent transformations. We show, however, that the size of the evolving formula is in the worst-case exponentially bounded by the size of the original LTL formula, and also that an exponential space explosion cannot be avoided in certain unfortunate cases. The efficiency of this rewriting algorithm can be improved by almost an order of magnitude by caching and reusing rewrites (a.k.a. “memoization”), which is supported by Maude. This algorithm is often synchronous, though there are situations in which it misses reporting a violation at the exact event when it occurs. The violation is, however, detected at a subsequent event. This algorithm can be relatively easily transformed into a synchronous one if one is willing to pay the price of running a validity checker, like the one presented in Subsection 7.3, after processing each event. The practical result of Section 6 is a very efficient and small Maude program that can be used to monitor program executions. The decision to use Maude has made it very easy to experiment with logics and algorithms in monitoring. We finally present an alternative solution to monitoring LTL in Section 7, where a rewritingbased algorithm is used to generate an optimal special observer from an LTL formula. By optimality is meant everything one may expect, such as minimal number of states, forwards traversal of execution traces, synchronicity, efficiency, but also less standard optimality features, such as transiting from one state to another with a minimum amount of computation. In order to effectively do this we introduce the notion of binary transition tree (BTT), as a generalization of binary decision diagrams (BDD) [4], whose purpose is to provide an optimal order in which state predicates need to be evaluated to decide the next state. The motivation for this is that in practical applications evaluating a state predicate is a time consuming task, such as for example to check whether a vector is sorted. The associated finite state machines are called binary transition tree finite state machines (BTT-FSM). BTT-FSMs can be used to analyze execution traces without the need for a rewriting system, and can hence be used by observers written in traditional programming languages. The BTT-FSM generator, which includes a validity checker, is also implemented in Maude and has about 200 lines of code in total. 2 A Taxonomy of Runtime Analysis Techniques A runtime analysis technique is regarded in a broad sense in this section; it can be a method or a concrete algorithm that analyzes the execution trace of a running program and concludes a certain property about that program. Runtime analysis algorithms can be arbitrarily complex, depending upon the kind of properties to be monitored or analyzed. Based on our experience with current procedures implemented in JPaX, in this section we make an attempt to classify runtime analysis techniques. The three criteria below are intended to be neither exhaustive nor always applicable, but we found them quite useful in practice. They are not specific to any particular logic or approach, so we present them before we introduce our logic and algorithms. In fact, this taxonomy will allow us to appropriately discuss the benefits and drawbacks of our algorithms presented in the rest of 5 the paper. 2.1 Trace Storing versus Non-Storing Algorithms As events are received from the monitored system, a runtime analysis algorithm typically maintains a state which allows it to reason about the monitored execution trace. Ideally, the amount of information needed to be stored by the monitor in its state depends only upon the property to be monitored and not upon the number of already processed events. This is desired because, due to the huge amount of events that can be generated during a monitoring session, one would want one’s monitoring algorithms to work in linear time with the number of events processed. There are, however, situations where it is not possible or practically feasible to use storage whose size is a function of only the monitoring requirement. One example is that of monitoring extended regular expressions (ERE), i.e., regular expressions enriched with a complement operator. As shown by the success of scripting languages like PERL or PYTHON, software developers tend to understand and like regular expressions and feel comfortable to describe patterns using those, so ERE is a good candidate formalism to specify monitoring requirements (we limit ourselves to only patterns described via temporal logics in this paper though). It is however known that ERE to automata translation algorithms suffer from a non-elementary state explosion problem, because a complement operation requires nondeterministic-to-deterministic automata conversions, which yield exponential blowups in the number of states. Since complement operations can be nested, generating automata from EREs may often not be feasible in practice. Fortunately, there are algorithms which avoid this state explosion problem, at the expense of having to store the execution trace and then, at the end of the monitoring session, to analyze it by traversing it forwards and backwards many times. The interested reader is referred to [36] for a O(n3 m) dynamic programming algorithm (n is the length of the execution trace and m is the size of the ERE), and to [63, 42] for O(n2 m) non-trivial algorithms. Based on these observations, we propose a first criterion to classify monitoring algorithms, namely on whether they store or do not store the execution trace. In the case of EREs, trace storing algorithms are polynomial in the size of the trace and linear in the ERE requirement, while the non-storing ones are linear in the size of the trace and highly exponential in the size of the requirement. In this paper we show that trace storing algorithms for linear temporal logic can be linear in both the trace and the requirement (see Section 5), while trace non-storing ones are linear in the size of the trace but simply exponential in the size of the requirement. Trace non-storing algorithms are apparently preferred, but, however, their size can be so big that it could make their use unamenable in certain important situations. One should carefully analyze the trade-offs in order to make the best choice in a particular situation. 2.2 Synchronous versus Asynchronous Monitoring There are many safety critical applications in which one would want to report a violation of a requirement as soon as possible, and to not allow the monitored program to take any further action once a requirement is violated. We call this desired functionality synchronous monitoring. Otherwise, if a violation can only be detected after the monitored program executes several more steps or after it is stopped and its entire execution trace is needed to perform the analysis, then we call it asynchronous monitoring. 6 The dynamic programming algorithm presented in Section 5 is not synchronous, because one can detect a violation only after the program is stopped and its execution trace is available for backwards traversal. The algorithm presented in Section 6 is also asynchronous in general because there are universally false formulae which are detected so only at the end of an execution trace or only after several other execution steps. Consider, for example, that one monitors the finite trace LTL formula !<>([]A \/ []!A), which is false because at the end of any execution trace A either holds or not, or the formula ooA /\ oo!A, which is also false but will be detected so only after two more events. However, the rewriting algorithm in Section 6 is synchronous in many practical situations. The algorithm in Section 7 is always synchronous, though one should be aware of the fact that its size may become a problem on large formulae. In order for an LTL monitor to be synchronous, it needs to implement a validity checker for finite trace LTL, such as the one in Subsection 7.3 (Figure 6), and call it on the current formula after each event is processed. Checking validity of a finite trace LTL formula is very expensive (we are not aware of any theoretical result stating its exact complexity, but we believe that it is PSPACEcomplete, like for standard infinite trace LTL [60]). We are currently considering providing a fully synchronous LTL monitoring module within JPaX, at the expense of calling a validity checker after each event, and let the user of the system choose either synchronous or asynchronous monitoring. There are, however, many practical LTL formulae for which violation can be detected synchronously by the formula transforming rewriting-based algorithm presented in Section 6. Consider for example the sample formula of this paper, [](green -> !red U yellow), which is violated if and only if a red event is observed after a green one. The monitoring requirement of our algorithm, which initially is the formula itself, will not be changed unless a green event is received, in which case it will change to (!red U yellow) /\ [](green -> !red U yellow). A yellow event will turn it back into the initial formula, a green event will keep it unchanged, but a red event will turn it into false. If this is the case, then the monitor declares the formula violated and appropriate actions can be taken. Notice that the violation was detected exactly when it occurred. A very interesting, practical and challenging problem is to find criteria that say when a formula can be synchronously monitored without the use of a validity checker. 2.3 Predictive versus Exact Analysis An increasingly important class of runtime analysis algorithms are concerned with predicting anomalies in programs from successful observed executions. One such algorithm can be easily obtained by slightly modifying the wait-for-graph algorithm, which is typically used to detect when a system is in a deadlock state, to make it predict deadlocks. One way to do this is to not remove synchronization objects from the wait-for-graph when threads/processes release them. Then even though a system is not deadlock, a warning can be reported to users if a cycle is found in the wait-for-graph, because that represents a potential of a deadlock. Another algorithm falling into the same category is Eraser [55], a datarace prediction procedure. For each shared memory region, Eraser maintains a set of active locks which protect it, which is intersected with the set of locks held by any accessing thread. If the set of active locks ever becomes empty then a warning is issued to the user, with the meaning that a potential unprotected access can take place. Both the deadlock and the datarace predictive algorithms are very successful in practice because they scale well and find many of the errors they are designed for. We have also implemented improved versions of these algorithms in Java PathExplorer. We are currently also investigating predictive analysis of safety properties expressed using past 7 time temporal logic, and a prototype system called Java MultiPathExplorer is being implemented [58]. The main idea here is to instrument Java classes to emit events timestamped by vector clocks [14], thus enabling the observer to extract a partial order reflecting the causal dependency on the memory accesses of the multithreaded program. If any linearization of that inferred partial order leads to a violation of the safety property then a warning is generated to the user, with the meaning that there can be executions of the multithreaded program, including the current one, which violate the requirements. In this paper we restrict ourselves to only exact analysis of execution traces. That means that the events in the trace are supposed to have occurred exactly in the received order (this can be easily enforced by maintaining a logical clock, then timestamping each event with the current clock, and then delivering the messages in increasing order of timestamps), and that we only check whether that particular order violates the monitoring requirements or not. Techniques for predicting future time LTL violations will be investigated elsewhere soon. Although the taxonomy discussed in this section is intended to only be applied to tools, the problem domain may also admit a similar taxonomy. While such a taxonomy seems to be hard to accomplish in general, it would certainly be very useful because it would allow one to choose the proper runtime analysis technique for a given system and set of properties. However, like this paper shows, it is often the case that one can choose among several types of runtime analysis techniques for a given problem domain. 3 Preliminaries In this section we recall notions and notations that will be used in the paper, including membership equational logic, term rewriting, Maude notation, and (infinite trace) linear temporal logics. 3.1 Membership Equational Logic Membership equational logic (MEL) extends many- and order-sorted equational logic by allowing memberships of terms to sorts in addition to the usual equational sentences. We only recall those MEL notions which are necessary for understanding this paper; the interested reader is referred to [48, 3] for a comprehensive exposition of MEL. 3.1.1 Basic Definition A many-kinded algebraic signature (K, Σ) consists of a set K and a (K ⋆ × K)-indexed set Σ = {Σk1 k2 ...kn ,k | k1 , k2 , ..., kn , k ∈ K} of operations, where an operation σ ∈ Σk1 k2 ...kn ,k is written σ : k1 k2 ...kn → k. A membership signature Ω is a triple (K, Σ, π) where K is a set of kinds, Σ is a K-kinded algebraic signature, and π: S → K is a function that assigns to each element in its domain, called a sort, a kind. Therefore, sorts are grouped according to kinds and operations are defined on kinds. For simplicity, we will call a “membership signature” just a “signature” whenever there is no confusion. For a many-kinded signature (K, Σ), a Σ-algebra A consists of a K-indexed set {Ak | k ∈ K} together with interpretations of operations σ : k1 k2 ...kn → k into functions Aσ : Ak1 × Ak2 × · · · × Akn → Ak . For any given signature Ω = (K, Σ, π), an Ω-membership algebra A is a Σ-algebra together with a set As ⊆ Aπ(s) for each sort s ∈ S. A particular algebra, called term algebra, is of special interest. Given a K-kinded signature Σ and a K-indexed set of variables X, let TΣ (X) be 8 the algebra of Σ-terms over variables in X extending X iteratively as follows: if σ : k1 k2 ...kn → k and t1 ∈ TΣ,k1 (X), t2 ∈ TΣ,k2 (X), ..., tn ∈ TΣ,kn (X), then σ(t1 , t2 , ..., tn ) ∈ TΣ,k (X). Given a signature Ω and a K-indexed set of variables X, an atomic (Ω, X)-equation has the form t = t′ , where t, t′ ∈ TΣ,k (X), and an atomic (Ω, X)-membership has the form t : s, where s is a sort and t ∈ TΣ,π(s) (X). An Ω-sentence in MEL has the form (∀X) a if a1 ∧ . . . ∧ an , where a, a1 , . . . , an are atomic (Ω, X)-equations or (Ω, X)-memberships, and {a1 , . . . , an } is a set (no duplications). If n = 0, then the Ω-sentence is called unconditional and written (∀X) a. Equations are called rewriting rules when they are used only from left to right, as it will happen in this paper. Given an Ω-algebra A and a K-kinded map θ: X → A, then A, θ |=Ω t = t′ iff θ(t) = θ(t′ ), and A, θ |=Ω t : s iff θ(t) ∈ As . A satisfies (∀X) a if a1 ∧...∧an , written A |=Ω (∀X) a if a1 ∧ ... ∧ an , iff for each θ: X → A, if A, θ |=Ω a1 and ... and A, θ |=Ω an , then A, θ |=Ω a. An Ω-specification (or Ω-theory) T = (Ω, E) in MEL consists of a signature Ω and a set E of Ω-sentences. An Ω-algebra A satisfies (or is a model of) T = (Ω, E), written A |= T , iff it satisfies each sentence in E. 3.1.2 Inference Rules MEL admits complete deduction (see [48], where the rule of congruence is stated in a somewhat different but equivalent way). In the congruence rule below, σ ∈ Σk1 ...ki ,k , W is a set of variables w1 : k1 , . . . , wi−1 : ki−1 , wi+1 : ki+1 , . . . , wn : kn , and σ(W, t) is a shorthand for the term σ(w1 , . . . , wi−1 , t, wi+1 , . . . , wn )): (1) Reflexivity : E ⊢Ω (∀X) t = t (2) Symmetry : E ⊢Ω (∀X) t = t′ E ⊢Ω (∀X) t′ = t (3) Transitivity : E ⊢Ω (∀X) t = t′ , E ⊢Ω (∀X) t′ = t′′ E ⊢Ω (∀X) t = t′′ (4) Congruence : E ⊢Ω (∀X) t = t′ E ⊢Ω (∀X, W ) σ(W, t) = σ(W, t′ ), for each σ ∈ Σ (5) Membership : E ⊢Ω (∀X) t = t′ , E ⊢Ω (∀X) t : s E ⊢Ω (∀X) t′ : s (6) Modus Ponens :                Given a sentence in E (∀Y ) t = t′ if t1 = t′1 ∧...∧ tn = t′n ∧ w1 : s1 ∧...∧ wm : sm (resp. (∀Y ) t : s if t1 = t′1 ∧...∧ tn = t′n ∧ w1 : s1 ∧...∧ wm : sm ) and θ: Y → TΣ (X) s.t. for all i ∈ {1, .., n} and j ∈ {1, .., m} E ⊢Ω (∀X) θ(ti ) = θ(t′i ), E ⊢Ω (∀X) θ(wj ) : sj E ⊢Ω (∀X) θ(t) = θ(t′ ) (resp. E ⊢Ω (∀X) θ(t) : s) The rules above can therefore prove any unconditional equation or membership that is true in all membership algebras satisfying E. In order to derive conditional statements, we will therefore consider the standard technique adapting the “deduction theorem” to equational logics, namely 9 deriving the conclusion of the sentence after adding the condition as an axiom; in order for this procedure to be correct, the variables used in the conclusion need to be first transformed into constants. All variables can be transformed into constants, so we only consider the following simplified rules: (7) Theorem of Constants : E ⊢Ω∪X (∀∅) a if a1 ∧ ... ∧ an E ⊢Ω (∀X) a if a1 ∧ ... ∧ an (8) Implication Elimination : E ∪ {a1 , . . . , an } ⊢Ω (∀∅) a E ⊢Ω (∀∅) a if a1 ∧ ... ∧ an Theorem 1 (from [48]) With the notation above, E |=Ω (∀X) a if a1 ∧ ... ∧ an if and only if E ⊢Ω (∀X) a if a1 ∧ ... ∧ an . Moreover, any statement can be proved by first applying rule (7), then (8), and then a series of rules (1) to (6). This theorem is used within the correctness proof of the monitoring algorithm in Section 6. 3.1.3 Initial Semantics and Induction MEL specifications are often intended to allow only a restricted class of models (or algebras). For example, a specification of natural numbers defined using the Peano equational axioms would have many “undesired” models, such as models in which the addition operation is not commutative, or models in which, for example, 10=0. We restrict the class of models of a MEL specification only to those which are initial, that is, those which obey the no junk no confusion principle; therefore, our specifications have initial semantics [20] in this paper. Intuitively, that means that only those models are allowed in which all elements can be “constructed” from smaller elements and in which no terms which cannot be proved equal are interpreted to the same elements. By reducing the class of models, one can enlarge the class of sound inference rules. A major benefit one gets under initial semantics is that proofs by induction become valid. Since the proof of correctness for the main algorithm in this paper is done by induction on the structure of the temporal formula to monitor, it is important for the reader to be aware that the specifications presented from now on have initial semantics. 3.1.4 Syntactic Sugar Conventions To make specifications easier to read, the following syntactic sugar conventions are widely accepted: Subsorts. Given sorts s, s′ with π(s) = π(s′ ) = k, the declaration s < s′ is syntactic sugar for the conditional membership (∀x : k) x : s′ if x : s. Operations. If σ ∈ Ωk1 ...kn ,k and s1 , . . . , sn , s ∈ S with π(s1 ) = k1 , . . . , π(sn ) = kn , π(s) = k, then the declaration σ : s1 · · · sn → s is syntactic sugar for (∀x1 : k1 , . . . , xn : kn ) σ(x1 , . . . , xn ) : s if x1 : s1 ∧ . . . ∧ xn : sn . Variables. (∀x : s, X) a if a1 ∧. . .∧an is syntactic sugar for the Ω-sentence (∀x : π(s), X) a if a1 ∧ . . . ∧ an ∧ x : s. With this, the operation declaration σ : s1 · · · sn → s above is equivalent to (∀x1 : s1 , . . . , xn : sn ) σ(x1 , . . . , xn ) : s. 10 3.2 Maude Maude [7] is a freely distributed high-performance system in the OBJ [21] algebraic specification family, supporting both rewriting logic [47] and membership equational logic [48]. Because of its efficient rewriting engine, able to execute 3 million rewriting steps per second on standard PCs, and because of its metalanguage features, Maude turns out to be an excellent tool to create executable environments for various logics, models of computation, theorem provers, and even programming languages. We were delighted to notice how easily we could implement and efficiently validate our algorithms for testing LTL formulae on finite event traces in Maude, admittedly a tedious task in C++ or Java, and hence decided to use Maude at least for the prototyping stage of our runtime check algorithms. We informally describe some of Maude’s features via examples in this section, referring the interested reader to its manual [7] for more details. The examples discussed in this subsection are not random. On the one hand they show all the major features of Maude that we need, while on the other hand they are part of our current JPaX implementation; several references to them will be made later in the paper. Maude supports modularization in the OBJ style. There are various kinds of modules, but we use only functional modules which follow the pattern “fmod <name> is <body> endfm”, and which have initial semantics. The body of a functional module consists of a collection of declarations, of which we are using importation, sorts, subsorts, operations, variables and equations, usually in this order. 3.2.1 Defining Logics for Monitoring In the following we introduce some modules that we think are general enough to be used within any logical environment for program monitoring that one would want to implement by rewriting. The first one simply defines atomic propositions as an abstract data type having one sort, Atom, and no operations or constraints: fmod ATOM is sort Atom . endfm The actual names of atomic propositions will be automatically generated in another module that extends ATOM, as constants of sort Atom. These will be generated by the observer at the initialization of monitoring, from the actual properties that one wants to monitor. An important concept in program monitoring is that of an (abstract) execution trace, which consists of a finite list of events. We abstract a single event by a list of atoms, those that hold after the action that generated the event took place. The values of the atomic propositions are updated by the observer according to the actual state of the executing program and then sent to Maude as a term of sort Event (more details regarding the communication between the running program and Maude will be given later): fmod TRACE is protecting ATOM . sorts Event Event* Trace . subsorts Atom < Event < Event* < Trace . op empty : -> Event . op __ : Event Event -> Event [assoc comm id: empty prec 23] . var A : Atom . 11 eq A A = A . op _* : Event -> Event* . op _,_ : Event Trace -> Trace [prec 25] . endfm The statement protecting ATOM imports the module ATOM without changing its initial semantics. The above is a compact way to use mix-fix2 and order-sorted notation to define an abstract data type of traces: a trace is a comma separated list of events, where an event is itself a set of atoms. The subsorts declaration declares Atom to be a subsort of Event, which in turn is a subsort of Event* which is a subsort of Trace. Since elements of a subsort can occur as elements of a supersort without explicit lifting, we have as a consequence that a single event is also a trace, consisting of one event. Likewise, an atomic proposition can occur as an event, containing only this atomic proposition. Operations can have attributes, such as associativity (A), commutativity (C), identity (I) as well as precedences, which are written between square brackets. When a binary operation is declared using the attributes A, C, and/or I, Maude uses built-in efficient specialized algorithms for matching and rewriting. However, semantically speaking, the A, C, and/or I attributes can be replaced by there corresponding equations. The attribute prec gives a precedence to an operator3 , thus eliminating the need for most parentheses. Notice the special sort Event* which stays for terminal events, i.e., events that occur at the end of traces. Any event can potentially occur at the end of a trace. It is often the case that ending events are treated differently, like in the case of finite trace linear temporal logic; for this reason, we have introduced the operation _* which marks an event as terminal. An event is defined as a set of atoms which should in fact be thought of as the set of all those atoms which “hold” in the new state of the event emitting program. Note the idempotency equation “eq A A = A”, which ensures that an event is indeed a set. On the other hand, a trace is a an ordered list of events which can potentially have repetitions of events. For example, the event “x = 5” can occur several times during the execution of a program. Note that there is no need and consequently no definition of an empty trace. Syntax and semantics are basic requirements to any logic. The following module introduces what we believe are the basic ingredients of monitoring logics, i.e., logics used for specifying monitoring requirements: fmod LOGICS-BASIC is protecting TRACE . sort Formula . subsort Atom < Formula . ops true false : -> Formula . op [_] : Formula -> Bool . eq [true] = true . eq [false] = false . var var var var 2 3 A : Atom . T : Trace . E : Event . E* : Event* . Underscores are places for arguments. The lower the precedence number, the tighter the binding. 12 op eq eq eq eq eq _{_} : Formula Event* -> Formula [prec 10] . true{E*} = true . false{E*} = false . A{A E} = true . A{E} = false [owise] . A{E *} = A{E} . op _|=_ : Trace Formula -> Bool [prec 30] . eq T |= true = true . eq T |= false = false . eq E |= A = [A{E}] . eq E,T |= A = E |= A . endfm The first block of declarations introduces the sort Formula which can be thought of as a generic sort for any well-formed formula in any logic. There are two designated formulae, namely true and false, with the obvious meaning in any monitoring logic. The sort Bool is built-in in Maude together with two constants true and false, which are different from those of sort Formula, and a generic operator if_then_else_fi. The “interpretation” operator [_] maps a formula to a Boolean value. Each logic implemented on top of LOGICS-BASIC is free to define it appropriately; here we only give the obvious mappings of true and false of Formula into true and false of Bool. The second block defines the operation _{_} which takes a formula and an event and yields another formula. The intuition for this operation is that it “evaluates” the formula in the new state and produces a proof obligation as another formula for the subsequent events. If the returned formula is true or false then it means that the formula was satisfied or violated, regardless of the rest of the execution trace; in this case, a message can be returned by the observer. Each logic will further complete the definition of this operator. Note that the equation “eq A{A E} = true” speculates Maude’s capability of performing matching modulo associativity, commutativity and identity (the attributes of the set concatenation on events); it basically says that A{E} is true if E contains the atom A. The next equation contains the attribute [owise], stating that it should be applied only if any other equation fails to apply at a particular position. Finally, the satisfaction relation is defined. Two obvious equations deal with the formulae true and false. The last two equations state that a trace satisfies an atomic proposition A if evaluating that atomic proposition A on the first event in the trace yields true. The remaining elements in the trace do not matter because A is a simple atom, so it refers to only the current state. 3.2.2 Defining Propositional Calculus A rewriting decision procedure for propositional calculus due to Hsiang [37] is adapted and presented. It provides the usual connectives _/\_ (and), _++_ (exclusive or), _\/_ (or), !_ (negation), _->_ (implication), and _<->_(equivalence). The procedure reduces tautological formulae to the constant true and all the others to some canonical form modulo associativity and commutativity. An unusual aspect of this procedure is that a canonical form consists of an exclusive or of conjunctions. In fact, this choice of basic operators corresponds to regarding propositional calculus as a Boolean ring rather than as a Boolean algebra. A major advantage of this choice is that normal forms are unique modulo associativity and commutativity. Even if propositional calculus is very basic to almost any logical environment, we decided to keep it as a separate logic instead of being 13 part of the logic infrastructure of JPaX. One reason for this decision is that its operational semantics could be in conflict with other logics, for example ones in which conjunctive normal forms are desired. An OBJ3 code for this procedure appeared in [21]. Below we give its obvious translation to Maude together with its finite trace semantics, noticing that Hsiang [37] showed that this rewriting system modulo associativity and commutativity is Church-Rosser and terminates. The Maude team was probably also inspired by this procedure, since the builtin BOOL module is very similar. fmod PROP-CALC is extending LOGICS-BASIC . *** Constructors *** op _/\_ : Formula Formula -> Formula [assoc comm prec 15] . op _++_ : Formula Formula -> Formula [assoc comm prec 17] . vars X Y Z : Formula . eq true /\ X = X . eq false /\ X = false . eq X /\ X = X . eq false ++ X = X . eq X ++ X = false . eq X /\ (Y ++ Z) = X /\ Y ++ X /\ Z . *** Derived operators *** op _\/_ : Formula Formula -> Formula [assoc prec 19] . op !_ : Formula -> Formula [prec 13] . op _->_ : Formula Formula -> Formula [prec 21] . op _<->_ : Formula Formula -> Formula [prec 23] . eq X \/ Y = X /\ Y ++ X ++ Y . eq ! X = true ++ X . eq X -> Y = true ++ X ++ X /\ Y . eq X <-> Y = true ++ X ++ Y . *** Finite trace semantics var T : Trace . var E* : Event* . eq T |= X /\ Y = T |= X and T |= Y . eq T |= X ++ Y = T |= X xor T |= Y . eq (X /\ Y){E*} = X{E*} /\ Y{E*} . eq (X ++ Y){E*} = X{E*} ++ Y{E*} . eq [X /\ Y] = [X] and [Y] . eq [X ++ Y] = [X] xor [Y] . endfm The statement “extending LOGICS-BASIC” imports the module LOGICS-BASIC with the reserve that its initial semantics can be extended. The operators “and” and “xor” come from the Maude’s built-in module BOOL which is automatically imported by any other module. Operators are declared with special attributes, such as assoc and comm, which enable Maude to use its specialized efficient internal rewriting algorithms. Once the module above is loaded4 in Maude, reductions can be done as follows: reduce a -> b /\ c reduce a <-> ! b . 4 <-> (a -> b) /\ (a -> c) . Either by typing it or using the command “in <filename>”. 14 ***> should be true ***> should be a ++ b Notice that one should first declare the constants a, b and c. The last six equations in the module PROP-CALC are related to the semantics of propositional calculus. The default evaluation strategy for [_] is eager, so [X] will first evaluate X using propositional calculus reasoning and then will apply one of the last two equations if needed; these equations will not be applied normally in practical reductions, they are useful only in the correctness proof stated by Theorem 3. 4 Finite Trace Future Time Linear Temporal Logic Classical (infinite trace) linear temporal logic (LTL) [51, 44, 45] provides in addition to the propositional logic operators the temporal operators []_ (always), <>_ (eventually), _U_ (until), and o_ (next). An LTL standard model is a function t : N + → 2P for some set of atomic propositions P, i.e., an infinite trace over the alphabet 2P , which maps each time point (a natural number) into the set of propositions that hold at that point. The operators have the following interpretation on such an infinite trace. Assume formulae X and Y. The formula []X holds if X holds in all time points, while <>X holds if X holds in some future time point. The formula X U Y (X until Y) holds if Y holds in some future time point, and until then X holds (so we consider strict until). Finally, o X holds for a trace if X holds in the suffix trace starting in the next (the second) time point. The propositional operators have their obvious meaning. For example, the formula [](X -> <>Y) is true if for any time point ([]) it holds that if X is true then eventually (<>) Y is true. It is standard to define a core LTL using only atomic propositions, the propositional operators !_ (not) and _/\_ (and), and the temporal operators o_ and _U_, and then define all other propositional and temporal operators as derived constructs. Standard equations are <>X = true U X and []X = !<>!X. Our goal is to develop a framework for testing software systems using temporal logic. Tests are performed on finite execution traces and we therefore need to formalize what it means for a finite trace to satisfy an LTL formula. We first present a semantics of finite trace LTL using standard mathematical notation. Then we present a specification in Maude of a finite trace semantics. Whereas the former semantics uses universal and existential quantification, the second Maude specification is defined using recursive definitions that have a straightforward operational rewriting interpretation and which therefore can be executed. 4.1 Finite Trace Semantics As mentioned in Subsection 3.2.1, a trace is viewed as a non-empty finite sequence of program states, each state denoting the set of propositions that hold at that state. We shall first outline the finite trace LTL semantics using standard mathematical notation rather than Maude notation. The debatable issue here is what happens at the end of the trace. The choice to validate or invalidate all the atomic propositions does not work in practice, because there might be propositions whose values are always opposite to each other, such as, for example, “gate up” and “gate down”. Driven by experiments, we found that a more reasonable assumption is to regard a finite trace as an infinite stationary trace in which the last event is repeated infinitely. Assume two total functions on traces, head : Trace → Event returning the head event of a trace and length returning the length of a finite trace, and a partial function tail : Trace → Trace for taking the tail of a trace. That is, head(e, t) = head(e) = e, tail(e, t) = t, and length(e) = 1 and length(e, t) = 1+length(t). Assume further for any trace t, that ti denotes the suffix trace that starts at position i, with positions starting at 1. The satisfaction relation |= ⊆ Trace × Formula defines 15 when a trace t satisfies a formula f , written t |= f , and is defined inductively over the structure of the formulae as follows, where A is any atomic proposition and X and Y are any formulae: t |= t |= t |= t |= t |= t |= t |= t |= t |= true false A X /\ Y X ++ Y o X <>X []X X U Y iff iff iff iff iff iff iff iff iff true, false, A ∈ head(t), t |= X and t |= Y, t |= X xor t |= Y, (if tail(t) is defined then tail (t) |= X else t |= X), (∃ i ≤ length(t)) ti |= X, (∀ i ≤ length(t)) ti |= X, (∃ i ≤ length(t)) (ti |= Y and (∀ j < i) tj |= X). The semantics of the “next” operator reflects perhaps best the stationarity assumption of last events in finite traces. Notice that finite trace LTL can behave quite differently from standard infinite trace LTL. For example, there are formulae which are not valid in infinite trace LTL but valid in finite trace LTL, such as <>([]A \/ []!A) for any atomic proposition A, and there are formulae which are satisfiable in infinite trace LTL and not satisfiable in finite trace LTL, such as the negation of the above. The formula above is satisfied by any finite trace because the last event/state in the trace either contains A or it does not. 4.2 Finite Trace Semantics in Maude Now it can be relatively easily seen that the following Maude specification correctly “defines” the finite trace semantics of LTL described above. The only important deviation from the rigorous mathematical formulation described above is that the quantifiers over finite sets of indexes are expressed recursively. fmod LTL is extending PROP-CALC . *** syntax op []_ : Formula -> Formula [prec 11] . op <>_ : Formula -> Formula [prec 11]. op _U_ : Formula Formula -> Formula [prec 14] . op o_ : Formula -> Formula [prec 11] . *** semantics vars X Y : Formula . var E : Event . var T : Trace . eq E |= o X = E |= X . eq E,T |= o X = T |= X . eq E |= <> X = E |= X . eq E,T |= <> X = E,T |= X or T |= <> X . eq E |= [] X = E |= X . eq E,T |= [] X = E,T |= X and T |= [] X . eq E |= X U Y = E |= Y . eq E,T |= X U Y = E,T |= Y or E,T |= X and T |= X U Y . endfm 16 Notice that only the temporal operators needed declarations and semantics, the others being already defined in PROP-CALC and LOGICS-BASIC, and that the definitions that involved the functions head and tail were replaced by two alternative equations. One can now directly verify LTL properties on finite traces using Maude’s rewriting engine. Consider as an example a traffic light that switches between the colors green, yellow, and red. The LTL property that after green comes yellow, and its negation, can now be verified on a finite trace using Maude’s rewriting engine, by typing commands to Maude such as: reduce green, yellow, red, green, yellow, red, green, yellow, red, red |= [](green -> !red U yellow) . reduce green, yellow, red, green, yellow, red, green, yellow, red, red |= !([](green -> !red U yellow)) . which should return the expected answers, i.e., true and false, respectively. The algorithm above does nothing but blindly follows the mathematical definition of satisfaction and even runs reasonably fast for relatively small traces. For example, it takes5 about 30ms (74k rewrite steps) to reduce the first formula above and less than 1s (254k rewrite steps) to reduce the second on traces of 100 events (10 times larger than the above). Unfortunately, this algorithm does not seem to be tractable for large event traces, even if run on very fast platforms. As a concrete practical example, it took Maude 7.3 million rewriting steps (3 seconds) to reduce the first formula above and 2.4 billion steps (1000 seconds) for the second on traces of 1,000 events; it could not finish in one night (more than 10 hours) the reduction of the second formula on a trace of 10,000 events. Since the event traces generated by an executing program can easily be larger than 10,000 events, the trivial algorithm above cannot be used in practice. A rigorous complexity analysis of the algorithm above is hard (because it has to take into consideration the evaluation strategy used by Maude for terms of sort Bool) and not worth the effort. However, a simplified worse-case analysis can be easily made if one only counts the maximum number of atoms of the form event |= atom that can occur during the rewriting of a satisfaction term, as if all the Boolean reductions were applied after all the other reductions: let us consider a formula X = []([](...([]A)...)) where the always operator is nested m times, and a trace T of size n, and let T (n, m) be the total number of basic satisfactions event |= atom that occur in the normal form of the term T |= X if no Boolean reductions were applied. Then, the recurrence formula T (n, m) = T (n − 1, m) + T (n, m − 1) follows immediately from the specification above. n−1 ) + (n−1 ), it follows that T (n, m) > (m ), that is, T (n, m) = Ω(nm ), which is of Since (nm ) = (m n m−1 course unacceptable. 5 A Backwards, Asynchronous, but Efficient Algorithm The satisfaction relation above for finite trace LTL can hence be defined recursively, both on the structure of the formulae and on the size of the execution trace. As is often the case for functions defined this way, an efficient dynamic programming algorithm can be generated from any LTL formula. We first show how such an algorithm looks for a particular formula, and then present the main algorithm generator. The work in this section appeared as a technical report [53], but for a slightly different finite trace LTL, namely one in which all the atomic propositions were considered false at the and of the trace. As explained previously in the paper, we are now in the favor of 5 On a 1.7GHz, 1Gb memory PC. 17 a semantics where traces are considered stationary in their last event. The generated dynamic programming algorithms are as efficient as they can be and one can hope: linear in both the trace and the LTL formula. Unfortunately, they need to traverse the execution trace backwards, so they are trace storing and asynchronous. However, a similar but dual technique applies to past time LTL, producing very efficient forwards and synchronous algorithms [33, 32]. 5.1 An Example The formula we choose below is artificial (and will not be used later in the paper), but contains all four temporal operators. We believe that this example would practically be sufficient for the reader to foresee the general algorithm presented in the remaining of the section. Let []((p U q) -> <>(q -> or)) be an LTL formula and let ϕ1 , ϕ2 , ..., ϕ10 be its subformulae, in breadthfirst order: ϕ1 ϕ2 ϕ3 ϕ4 ϕ5 ϕ6 ϕ7 ϕ8 ϕ9 ϕ10 = = = = = = = = = = []((p U q) -> <>(q -> or)), (p U q) -> <>(q -> or), p U q, <>(q -> or), p, q, q -> or, q, or, r. Given any finite trace t = e1 e2 ...en of n events, one can recursively define a matrix s[1..n, 1..10] of Boolean values {0, 1}, with the meaning that s[i, j] = 1 iff ti |= ϕj as follows: s[i, 10] s[i, 9] s[i, 8] s[i, 7] s[i, 6] s[i, 5] s[i, 4] s[i, 3] s[i, 2] s[i, 1] = = = = = = = = = = (r ∈ ei ) s[i + 1, 10] (q ∈ ei ) s[i, 8] implies s[i, 9] (q ∈ ei ) (p ∈ ei ) s[i, 7] or s[i + 1, 4] s[i, 6] or (s[i, 5] and s[i + 1, 3]) s[i, 3] implies s[i, 4] s[i, 2] and s[i + 1, 1], for all i < n, where and, or, implies are ordinary Boolean operations and == is the equality predicate, where s[n, 1..10] are defined as below: 18 s[n, 10] s[n, 9] s[n, 8] s[n, 7] s[n, 6] s[n, 5] s[n, 4] s[n, 3] s[n, 2] s[n, 1] = = = = = = = = = = (r ∈ en ) s[n, 10] (q ∈ en ) s[n, 8] implies s[n, 9] (q ∈ en ) (p ∈ en ) s[n, 7] s[n, 6] s[n, 3] implies s[n, 4] s[n, 2]. Note again that the trace needs to be traversed backwards, and that the row n of s is filled according to the stationary view of finite traces in their last event. An important observation is that, like in many other dynamic programming algorithms, one does not have to store all the table s[1..n, 1..10], which would be quite large in practice; in this case, one needs only two rows, s[i, 1..10] and s[i + 1, 1..10], which we shall write now and next from now on, respectively. It is now only a simple exercise to write up the following algorithm: Input: trace t = e1 e2 ...en next[10] ← (r ∈ en ); next[9] ← n ⁀ ext[10]; next[8] ← (q ∈ en ); next[7] ← next[8] implies next[9]; next[6] ← (q ∈ en ); next[5] ← (p ∈ en ); next[4] ← next[7]; next[3] ← next[6]; next[2] ← next[3] implies next[4]; next[1] ← next[2]; for i = n − 1 downto 1 do { now[10] ← (r ∈ ei ); now[9] ← next[10]; now[8] ← (q ∈ ei ); now[7] ← now[8] implies now[9]; now[6] ← (q ∈ ei ); now[5] ← (p ∈ ei ); now[4] ← now[7] or next[4]; now[3] ← now[6] or (now[5] and next[3]); now[2] ← now[3] implies now[4]; now[1] ← now[2] and next[1]; next ← now } output(next[1]); The algorithm above can be further optimized, noticing that only the bits 10, 4, 3 and 1 are needed in the vectors now and next, as we did for past time LTL in [33, 32]. The analysis of this algorithm is straightforward. Its time complexity is Θ(n · m) while the memory required is 2 · m bits, where n is the length of the trace and m is the size of the LTL formula. 19 5.2 Generating Dynamic Programming Algorithms We now formally describe our algorithm that synthesizes dynamic programming algorithms like the one above from LTL formulae. Our synthesizer is generic, the potential user being expected to adapt it to his/her desired target language. The algorithm consists of three main steps: Breadth First Search. The LTL formula should be first visited in breadth-first search (BFS) order to assign increasing numbers to subformulae as they are visited. Let ϕ1 , ϕ2 , ..., ϕm be the list of all subformulae in BFS order. Because of the semantics of finite trace LTL, this step ensures us that the truth value of ti |= ϕj can be completely determined from the truth values of ti |= ϕj ′ for all j < j ′ ≤ m and the truth values of ti+1 |= ϕj ′ for all j ≤ j ′ ≤ m. This recurrence gives the order in which one should generate the code. Loop Initialization. Before we generate the “for” loop, we should first initialize the vector next[1..m], which basically gives the truth values of the subformulae on the empty trace. According to the semantics of LTL, one should fill the vector next backwards. For a given m ≥ j ≥ 1, next[j] is calculated as follows: • If ϕj is a variable then next[j] = (ϕj ∈ en ). In a more complex setting, where ϕj was a state predicate, one would have to evaluate ϕj in the final state in the execution trace; • If ϕj is !ϕj ′ for some j < j ′ ≤ m, then next[j] = not next[j ′ ], where not is the negation operation on Booleans (bits); • If ϕj is ϕj1 Op ϕj2 for some j < j1 , j2 ≤ m, then next[j] = next[j1 ] op next[j2 ], where Op is any propositional operation and op is its corresponding Boolean operation; • If ϕj is oϕj ′ , []ϕj ′ , or <>ϕj ′ , then clearly next[j] = next[j ′ ] according to the stationary semantics of our finite trace LTL; • If ϕj is ϕj1 U ϕj2 for some j < j1 , j2 ≤ m, then next[j] = next[j2 ] for the same reason as above. Loop Generation. Because of the dependences in the recursive definition of finite trace LTL satisfaction relation, one is expected to visit the remaining of the trace backwards, so the loop index will vary from n − 1 down to 1. The loop body will update/calculate the vector now and in the end will move it into the vector next to serve as basis for the next iteration. At a certain iteration i, the vector now is updated also backwards as follows: • If ϕj is a variable then now[j] = (ϕj ∈ ei ). • If ϕj is !ϕj ′ for some j < j ′ ≤ m, then now[j] = not now[j ′ ]; • If ϕj is ϕj1 Op ϕj2 for j < j1 , j2 ≤ m, then now[j] = now[j1 ] op now[j2 ], where Op is any propositional operation and op is its corresponding Boolean operation; • If ϕj is oϕj ′ then now[j] = next[j ′ ] since ϕj holds now if and only if ϕj ′ held at the previous step (which processed the next event, the i + 1-th); • If ϕj is []ϕj ′ then now[j] = now[j ′ ] and next[j] because ϕj holds now if and only if ϕj ′ holds now and ϕj held at the previous iteration; • If ϕj is <>ϕj ′ then now[j] = now[j ′ ] or next[j] because of similar reasons as above; • If ϕj is ϕj1 U ϕj2 for some j < j1 , j2 ≤ m, then now[j] = now[j2 ] or (now[j1 ] and next[j]). 20 After each iteration, next[1] says whether the initial LTL formula is validated by the trace ei ei+1 ...en . Therefore, the desired output is next[1] after the last iteration. Putting all the above together, one can now write up the generic pseudocode presented below which can be implemented very efficiently on any current platform. Since the BFS procedure is linear, the algorithm synthesizes a dynamic programming algorithm from an LTL formula in linear time and of linear size with the size of the formula. The following generic program implements the discussed technique. It takes as input an LTL formula and generates a “for” loop which traverses the trace of events backwards, thus validating or invalidating the formula. Input: LTL formula ϕ output(“Input: trace t = e1 e2 ...en ”); let ϕ1 , ϕ2 , ..., ϕm be all the subformulae of ϕ is BFS order for j = m downto 1 do { output(“next[”, j, “] ← ”); if ϕj is a variable then output((ϕj ,“∈ en );”); if ϕj = !ϕj ′ then output(“not next[”,j ′ , “];”); if ϕj = ϕj1 Op ϕj2 then output(“next[”,j1 , “] op next[”, j2 , “];”); if ϕj = oϕj ′ then output(“next[”,j ′ , “];”); if ϕj = []ϕj ′ then output(“next[”,j ′ , “];”); if ϕj = <>ϕj ′ then output(“next[”,j ′ , “];”); if ϕj = ϕj1 U ϕj2 then output(“next[”,j2 , “];”); } output(“for i = n − 1 downto 1 do {”); for j = m downto 1 do { output(“ now[”, j, “] ← ”); if ϕj is a variable then output((ϕj ,“∈ ei );”); if ϕj = !ϕj ′ then output(“not now[”,j ′ , “];”); if ϕj = ϕj1 Op ϕj2 then output(“now[”,j1 , “] op now[”, j2 , “];”); if ϕj = oϕj ′ then output(“next[”, j ′ , “];”); if ϕj = []ϕj ′ then output(“now[”, j ′ , “] and next[”, j, “];” ); if ϕj = <>ϕj ′ then output(“now[”, j ′ , “] or next[”, j, “];” ); if ϕj = ϕj1 U ϕj2 then output(“now[”, j2 , “] or (now[”,j1 , “] and next[”, j, “]);”); } output(“ next ← now; }”); output(“output next[1];”); where Op is any propositional connective and op is its corresponding Boolean operator. The Boolean operations used above are usually very efficiently implemented on any microprocessor and the vectors of bits next and now are small enough to be kept in cache. Moreover, the dependencies between instructions in the generated “for” loop are simple to analyze, so a reasonable compiler can easily unfold or/and parallelize it to take advantage of machine’s resources. Consequently, the generated code is expected to run very fast. The dynamic programming technique presented in this section is as efficient as one can hope, but, unfortunately, has a major drawback: it needs to traverse the execution trace backwards. From a practical perspective, that means that the instrumented program is run for some period of 21 time while its execution trace is saved, and then, after the program was stopped, its execution trace is traversed backwards and (efficiently) analyzed. Besides the obvious inconvenience due to storing potentially huge execution traces, this method cannot be used to monitor programs synchronously. 6 A Forwards and Often Synchronous Algorithm In this section we shall present a more efficient rewriting semantics for LTL, based on the idea of consuming the events in the trace, one by one, and updating a data structure (which is also a formula) corresponding to the effect of the event on the value of the formula. An important advantage of this algorithm is that it often detects when a formula is violated or validated before the end of the execution trace, so, unlike the algorithms above, it is suitable for online monitoring. Our decision to write an operational semantics this way was motivated by an attempt to program such an algorithm in Java, where such a solution would be natural. The presented rewriting-based algorithm is linear in the size of the execution trace and worst-case exponential in the size of the monitored LTL formula. 6.1 An Event Consuming Algorithm We will implement this rewriting based algorithm by extending the definition of the event consuming operation _{_} : Formula Event* -> Formula to temporal operators, with the following intuition. Assuming a trace E,T consisting of event E followed by trace T, a formula X holds on this trace if and only if X{E} holds on the remaining trace T. If the event E is terminal then X{E *} holds if and only if X holds under standard LTL semantics on the infinite trace containing only the event E. fmod LTL-REVISED is protecting LTL . vars X Y : Formula . var E : Event . var T : Trace . eq (o X){E} = X . eq (o X){E *} = X{E *} . eq (<> X){E} = X{E} \/ <> X . eq (<> X){E *} = X{E *} . eq ([] X){E} = X{E} /\ [] X . eq ([] X){E *} = X{E *} . eq (X U Y){E} = Y{E} \/ X{E} /\ X U Y . eq (X U Y){E *} = Y{E *} . op _|-_ : Trace Formula -> Bool . eq E |- X = [X{E *}] . eq E,T |- X = T |- X{E} . endfm The rule for the temporal operator []X should be read as follows: the formula X must hold now (X{E}) and also in the future ([]X). The sub-expression X{E} represents the formula that must hold on the rest of the trace in order for X to hold now. As an example, consider again the traffic light controller safety formula [](green -> !red U yellow), which is first rewritten to [](true ++ green ++ green /\ (true ++ red) U yellow) by 22 the equations in module PROP-CALC. This formula modified by an event green yellow (notice that two lights can be lit at the same time) yields the rewriting sequence ([](true ++ green ++ green /\ (true ++ red) U yellow)){green yellow} =*=> (true ++ green{green yellow} ++ green{green yellow} /\ ((true ++ red) U yellow){green yellow}) /\ [](true ++ green ++ green /\ (true ++ red) U yellow) =*=> ((true ++ red) U yellow){green yellow}) /\ [](true ++ green ++ green /\ (true ++ red) U yellow) =*=> (yellow{green yellow} \/ ((true ++ red{green yellow}) /\ (true ++ red) U yellow) /\ [](true ++ green ++ green /\ (true ++ red) U yellow) =*=> [](true ++ green ++ green /\ (true ++ red) U yellow) which is exactly the original formula, while the same formula transformed by just the event green yields ([](true ++ green ++ green /\ (true ++ red) U yellow)){green} =*=> (true ++ green{green} ++ green{green} /\ ((true ++ red) U yellow){green}) /\ [](true ++ green ++ green /\ (true ++ red) U yellow) =*=> ((true ++ red) U yellow){green}) /\ [](true ++ green ++ green /\ (true ++ red) U yellow) =*=> (yellow{green} \/ ((true ++ red{green}) /\ (true ++ red) U yellow) /\ [](true ++ green ++ green /\ (true ++ red) U yellow) =*=> (true ++ red) U yellow /\ [](true ++ green ++ green /\ (true ++ red) U yellow) which further modified by an event red yields (yellow{red} \/ (true ++ red{red}) /\ (true ++ red) U yellow) /\ ([](true ++ green ++ green /\ (true ++ red) U yellow)){red} false /\ ([](true ++ green ++ green /\ (true ++ red) U yellow)){red} false =*=> =*=> When the current formula becomes false, as it happened above, we say that the original formula has been violated. Indeed, the current formula will remain false for any subsequent trace of events, so the result of the monitoring session will be false. Note that the rewriting system described so far obviously terminates, because what it does is to propagate the current event to the atomic subformulae, replace those by either true or false, and eventually canonize the newly obtained formula. Some operators could be defined in terms of others, as is typically the case in the standard semantics for LTL. For example, we could introduce an equation of the form: <>X = true U X, and then eliminate the rewriting rule for <>X in the above module. This turns out to be less efficient in practice though, because more rewrites are needed. This happens regardless of whether one enables memoization (explained in detail in Subsection 6.3) or not, because memoization brings a real benefit only when previously processed terms are to be reduced again. This module eventually defines a new satisfaction relation _|-_ between traces and formulae. The term T |- X is evaluated now by an iterative traversal over the trace, where each event transforms the formula. Note that the new formula that is generated at each step is always kept small by being reduced to normal form via the equations in the PROP-CALC module in Subsection 3.2.2. Our current JPaX implementation of the rewriting algorithm above executes the last two rules of the module LTL-REVISED outside the main rewriting engine. More precisely, Maude is started 23 in its loop mode [8], which provides the capability of enabling rewriting rules in a reactive system style: a “state” term is stored, which can then be modified via rewriting rules that are activated by ASCII text events that are provided via the standard I/O. JPaX starts a Maude process and assigns the formula to be monitored as its loop mode state. Then, as events are received from the monitored program, they are filtered and forwarded to the Maude module, which then enables rewriting on the term X{E}, where X is the current formula and E is the newly received event; the normal form of this reduction, a formula, is stored as the new loop mode state term. The process continues until the last event is received. JPaX tags the last event, asking Maude to reduce a term X{E *}; the result will be either true or false, which is reported to the user at the end of the monitoring session6 . A natural question here is how big the stored formula can grow during a monitoring session. Such a formula will consist of Boolean combinations of sub-formulae of the initial formula, kept in a minimal canonical form. This can grow exponentially in the size of the initial formula in the worst-case (see [54] for a related result for extended regular expressions). Theorem 2 For any formula X of size m and any sequence of events to be monitored E1, E2, ..., En, the formula X{E1}{E2}...{En} needs O(2m ) space to be stored. Moreover, the exponential space cannot be avoided: any synchronous or asynchronous forwards monitoring algorithm for LTL √ m c ) space, where c is some fixed constant. requires space Ω(2 Proof: Due to the Boolean ring simplification rules in PROP-CALC, any LTL formula is kept in a canonical form, which is an exclusive disjunction of conjunctions, where conjuncts have temporal operators at top. Moreover, after processing any number of events, in our case E1, E2, ..., En, the conjuncts in the normal form of X{E1}{E2}...{En} are subterms of the initial formula X, each having a temporal operator at its top. Since there are at most m such subformulae of X, it follows that there are at most 2m possibilities to combine them in a conjunction. Therefore, one needs space O(2m ) to store any exclusive disjunction of such conjunctions. This reasoning only applies on “idealistic” rewriting engines, which carefully optimize space needs during rewriting. It is not clear to us whether Maude is able to attain this space upper bound in all situations. For the space lower bound of any finite trace LTL monitoring algorithm, consider a simplified framework with only two atomic predicate and therefore only four possible states. For simplicity, we encode these four states by 0, 1, # and $. Consider also some natural number k and the language Lk = {σ#w#σ ′ $w | w ∈ {0, 1}k and σ, σ ′ ∈ {0, 1, #}∗ }. This language was previously used in several works [40, 41, 54] to prove lower bounds. The language can be shown to contain exactly those finite traces satisfying the following LTL formula [41] of size Θ(k 2 ): φk = [(!$) U ($ /\ o[](!$))] /\ <>[# /\ on+1 # /\ n ^ ((oi 0 /\ []($->oi 0))\/ (oi 1 /\ []($->oi 1)))]. i=1 Let us define an equivalence relation on finite traces in (0 + 1 + #)⋆ . For a σ ∈ (0 + 1 + #)⋆ , define S(σ) = {w ∈ (0 + 1)k | ∃λ1 , λ2 . λ1 #w#λ2 = σ}. We say that σ1 ≡k σ2 if and only if S(σ1 ) = S(σ2 ). 6 In fact, JPaX reports a similar message also when the current monitoring requirement becomes true or false at any time during the monitoring process. 24 k Now observe that the number of equivalence classes of ≡k is 22 ; this is because for any S ⊆ (0+1)k , there is a σ such that S(σ) = S. ′ ′ 2 Since |φk | = Θ(k 2 ), it follows that √ there is some constant c such that |φk | ≤ c k for all large enough k. Let c be the constant 1/ c′ . We will prove this lower bound result by contradiction. √ m c space for any LTL Suppose A is an LTL forwards monitoring algorithm that uses less that 2 formulae of large enough size m. We will look at the behavior of the algorithm A on inputs of the form φk . So m = |φk | ≤ c′ k 2 , and A uses less than 2k space. Since the number of equivalence classes k of ≡k is 22 , by the pigeon hole principle there must be two strings σ1 6≡k σ2 such that the memory of A on φk after reading σ1 $ is the same as the memory after reading σ2 $. In other words, A running on φk will give the same answer on all traces of the form σ1 $w and σ2 $w. Now since σ1 6≡k σ2 , it follows that (S(σ1 ) \ S(σ2 )) ∪ (S(σ2 ) \ S(σ1 )) 6= ∅. Take w ∈ (S(σ1 ) \ S(σ2 )) ∪ (S(σ2 ) \ S(σ1 )). Then clearly, exactly one out of σ1 $w and σ2 $w is in Lk , and so A running on φk gives the wrong answer on one of these inputs. Therefore, A is not a correct.  It seems, however, that this worst-case exponential complexity in the size of the LTL formula is more of theoretical importance than practical, since in general the size of the formula rarely grew more than twice in our experiments. Verification results are very encouraging and show that this optimized semantics is orders of magnitude faster than the first semantics. Traces of less than 10,000 events are verified in milliseconds, while traces of 100,000 events never needed more than 3 seconds. This technique scales quite well; we were able to monitor even traces of hundreds of millions events. As a concrete example, we created an artificial trace by repeating 10 million times the 10 event trace in Subsection 4.2, and then checked it against the formula [](green -> !red U yellow). There were needed 4.9 billion rewriting steps for a total of about 1,500 seconds. In Subsection 6.3 we will see how this algorithm can be made even more efficient, using memoization. 6.2 Correctness and Completeness In this subsection we prove that the algorithm presented above is correct and complete with respect to the semantics of finite trace LTL presented in Section 4. The proof is done completely in Maude, but since Maude is not intended to be a theorem prover, we actually have to generate the proof obligations by hand. In other words, the proof that follows was not generated automatically. However, it could have been mechanized by using proof assistants and/or theorem provers like Kumo [19], PVS [59], or Maude-ITP [6]. We have already done it in PVS, but we prefer to use only plain Maude in this paper. Theorem 3 For any trace T and any formula X, T |= X if and only if T |- X. Proof: By induction, both on traces and formulae. We first need to prove two lemmas, namely that the following two equations hold in the context of both LTL and LTL-REVISED: (∀ E : Event, X : Formula) E |= X = E |- X, (∀ E : Event, T : Trace, X : Formula) E,T |= X = T |= X{E}. We prove them by structural to prove the first lemma via by structural induction on X, quantified variables E and T, induction on the formula X. Constants e and x are needed in order the theorem of constants. However, since we prove these lemmas we not only have to add two constants e and t for the universally but also two other constants y and z standing for formulas which 25 can be combined via operators to give other formulas. The induction hypotheses are added to the following specification via equations. Notice that we merged the two proofs to save space. A proof assistant like Kumo, PVS or Maude-ITP would prove them independently, generating only the needed constants for each of them. fmod PROOF-OF-LEMMAS is extending LTL . extending LTL-REVISED . op e : -> Event . op t : -> Trace . ops a b c : -> Atom . ops y z : -> Formula . eq e |= y = e |- y . eq e |= z = e |- z . eq e,t |= y = t |= y{e} . eq e,t |= z = t |= z{e} . eq b{e} = true . eq c{e} = false . endfm It is worth reminding the reader at this stage that the functional modules in Maude have initial semantics, so proofs by induction are valid. Before proceeding further, the reader should be aware of the operational semantics of the operation _==_, namely that the two argument terms are first reduced to their normal forms which are then compared syntactically (but modulo associativity and commutativity); it returns true if and only if the two normal forms are equal. Therefore, the answer true means that the two terms are indeed semantically equal, while false only means that they could not be proved equal; they can still be equal. reduce and and and and and and and and and and and and and and and and and and (e (e (e (e (e (e (e (e (e |= |= |= |= |= |= |= |= |= (e,t (e,t (e,t (e,t (e,t (e,t (e,t (e,t (e,t (e,t a true false y /\ z y ++ z [] y <> y y U z o y |= |= |= |= |= |= |= |= |= |= == == == == == == == == == true false b c y /\ z y ++ z [] y <> y y U z o y e e e e e e e e e |||||||||- == == == == == == == == == == t t t t t t t t t t a) true) false) y /\ z) y ++ z) [] y) <> y) y U z) o y) |= |= |= |= |= |= |= |= |= |= true{e}) false{e}) b{e}) c{e}) (y /\ z){e}) (y ++ z){e}) ([] y){e}) (<> y){e}) (y U z){e}) (o y){e}) . It took Maude 129 reductions to prove these lemmas. Therefore, one can safely add now these lemmas as follows: 26 fmod LEMMAS is protecting LTL . protecting LTL-REVISED . var E : Event . var T : Trace . var X : Formula . eq E |= X = E |- X . eq E,T |= X = T |= X{E} . endfm We can now prove the theorem, by induction on traces. More precisely, we show: P(E), and P(T) implies P(E,T), for all events E and traces T, where P(T) is the predicate “for all formulas X, T |= X iff T |- X”. This induction schema can be easily formalized in Maude as follows: fmod PROOF-OF-THEOREM is protecting LEMMAS . op e : -> Event . op t : -> Trace . op x : -> Formula . var X : Formula . eq t |= X = t |- X . endfm reduce e |= x == e |- x . reduce e,t |= x == e,t |- x . Notice the difference in role between the constant x and the variable X. The first reduction proves the base case of the induction, using the theorem of constants for the universally quantified variable X. In order to prove the induction step, we first applied the theorem of constants for the universally quantified variables E and T, then added P(t) to the hypothesis (the equation “eq t |= X = t |X .”), and then reduced P(e t) using again the theorem of constants for the universally quantified variable X. Like in the proofs of the lemmas, we merged the two proofs to save space.  6.3 Further Optimization by Memoization Even though the formula transforming algorithm in Subsection 6.1 can process 100 million events in about 25 minutes, which is relatively reasonable for practical purposes, it can be significantly improved by adding only 5 more characters to the existing Maude code presented so far. More precisely, one can replace the operation declaration op _{_} : Formula Event* -> Formula [prec 10] in module LOGICS-BASIC by the operation declaration op _{_} : Formula Event* -> Formula [memo prec 10] 27 The attribute memo added to an operation declaration instructs Maude to memorize, or cache, the normal forms of terms rooted in that operation, i.e., those terms will be rewritten only once. Memoization is implemented by hashing, where the entry in the hash table is given by the term to be reduced and the value in the hash is its normal form. In our concrete example, memoization has the effect that any LTL formula will be transformed by a given event exactly once during the monitoring sequence; if the same formula and the same event occur in the future, the resulting modified formula is extracted from the hash table without applying any rewriting step. If one thinks of LTL in terms of automata, then our new algorithm corresponds to building the monitoring automaton on the fly. The obvious benefit of this technique is that only the needed part of the automaton is built, namely that part that is reachable during monitoring a particular sequence of events, which is practically very useful because the entire automaton associated to an LTL formula can be exponential in size, so storing it might become a problem. The use of memoization brings a significant improvement in the case of LTL. For example, the same sequence of 100 million events, which took 1500 seconds using the algorithm presented in Subsection 6.1, takes only 185 seconds when one uses memoization, for a total of 2.2 rewritings per processed event and 540,000 events processed per second! We find these numbers amazingly good for any practical purpose we can think of and believe that, taking into account the simplicity, obvious correctness and elegance of the rewriting based algorithm (implemented basically by 8 rewriting rules in LTL-REVISED), it would be hard to argue for any other implementation of LTL monitoring. One should, however, be careful when one uses memoization because hashing slows down the rewriting engine. LTL is a happy case where memoization brings a significant improvement, because the operational semantics of all the operators can be defined recursively, so formulae repeat often during the monitoring process. However, there might be monitoring logics where memoization could be less efficient. Such a logic would probably be an extension of LTL with time, allowing formulae of the form “<5> X” with the meaning “eventually in 5 units of time X”, because of a potentially very large number of terms to be memoized: <5> X, <4> X, etc. Experimentation is certainly needed if one designs a new logic for monitoring and wants to use memoization. 7 Generating Forwards, Synchronous and Efficient Monitors Even though the rewriting based monitoring algorithm presented in the previous section performs quite well in practice, there can be situations in which one wants to minimize the monitoring overhead as much as possible. Additionally, despite its simplicity and elegance, the procedure above requires an efficient rewriting engine modulo associativity and commutativity, which may not be available or may not be desirable on some monitoring platforms, such as, for example, within an embedded system. In this section we give a technique, based on ideas presented previously in the paper, to generate automata-based optimal monitors for future time LTL formulae. By optimality we here mean everything one may expect, such as minimal number of states, forwards traversal of execution traces, synchronicity, efficiency, but also less standard optimality features, such as transiting from one state to another with a minimum amount of computation. In order to effectively do this we introduce the notion of binary transition tree (BTT), as a generalization of binary decision diagrams (BDD) [4], whose purpose is to provide an optimal order in which state predicates need to be evaluated to decide the next state. The motivation for this is that in practical applications evaluating a state predicate is a time consuming task, such as for example to check whether a 28 vector is sorted. The associated finite state machines are called binary transition tree finite state machines (BTT-FSM). The drawback of generating an optimal BTT-FSM statically, i.e., before monitoring, is the worst-case double exponential time/space required at startup. Therefore, the algorithm presented in this section is recommended for situations where the LTL formulae to monitor are relatively small in size but the runtime overhead is desired to be minimal. It is worth noting that the BTT-FSM generation process can potentially take place on a machine different from the one performing the monitoring. In particular, one can think of a WWW fast server offering LTL-to-BTT-FSM services via the Internet, which can also maintain a database of already generated BTT-FSMs to avoid regenerating the same monitors. 7.1 Multi-Transition and Binary Transition Tree Finite State Machines To keep the runtime overhead of monitors low, it is crucial to do as little computation as possible in order to proceed to the next state. In the sequel we assume that finite state monitors are desired to efficiently change their state (when a new event is received) to one of possible states s1 , s2 , ..., sn , under the knowledge that a transition to each such state is enabled deterministically by some Boolean formula, p1 , p2 , ..., pn , respectively, on atomic state predicates. Definition 4 Let S be a set whose elements are called states, and let A be another set, whose elements are called atomic predicates. Let {s1 , s2 , ..., sn } ⊆ S and let p1 , p2 , ..., pn be propositions over atoms in A (using the usual Boolean combinators presented in Subsection 3.2.2), with the property that exactly one of them is true at any moment, that is, p1 ∨ p2 ∨ ... ∨ pn holds and for any distinct pi , pj , it is the case that pi → ¬pj holds. Then we call the n-tuple [p1 ?s1 , p2 ?s2 , ..., pn ?sn ] a multi-transition (MT) over states S and atomic predicates (or simply atoms) A. Let MT (S, E) be the set of multi-transitions over states S and atoms A.  Since the rest of the paper is concerned with rather theoretical results, from now on we use mathematical symbols for the Boolean operators instead of ASCII symbols like in Subsection 3.2.2. The intuition underlying multi-transitions is straightforward: depending on which of the propositions p1 , p2 , ..., pn is true at a given moment, a corresponding transition to exactly one of the states s1 , s2 , ..., sn will take place. Formally, Definition 5 Maps θ : A → {true, false} are called events from now on. With the notation above, given an event θ, we define a map θMT : MT (S, A) → S as θMT ([p1 ?s1 , p2 ?s2 , ..., pn ?sn ]) = si , where θ(pi ) = true; notice that θ(pj ) = false for any 1 ≤ j 6= i ≤ n.  Definition 6 Given an event θ : A → {true, false}, let eθ denote the list of atomic predicates a with θ(a) = true. There is an obvious correspondence between events as maps A → {true, false} and events as lists of atomic predicates, which justifies our implementation of events in Subsection 3.2.1. We take the liberty to use either the map or the list notation for events from now on in the paper. We let E denote the set of all events and call lists, or words, eθ1 . . . eθn ∈ E ⋆ (finite) traces; this is also consistent with our mechanical Maude ASCII notation in Subsection 3.2.1, except that we prefer not to separate events by commas in traces from now on, to avoid mathematical notational conflicts.  We next define binary transition trees. 29 Definition 7 Under the same notations as in the previous definition, a binary transition tree (BTT) over states S and atoms A is a term over syntax ::= S | (A ? BTT : BTT ). BTT We let BTT (S, A) denote the set of binary transition trees over states S and atoms A. Given an event θ : A → {true, false}, we define a map θBTT : BTT (S, A) → S inductively as follows: θBTT (s) = s for any s ∈ S, θBTT (a ? b1 : b2 ) = θBTT (b1 ) if θ(a) = true, and θBTT (a ? b1 : b2 ) = θBTT (b2 ) if θ(a) = false. A binary transition tree b in BTT (S, A) is said to implement a multi-transition t in MT (S, A) if and only if θBTT (b) = θMT (t) for any map θ : A → {true, false}.  BTTs generalize BDDs [4], which can be obtained by taking S = {true, false}. As an example of BTT, a1 ? a2 ? s1 : a3 ? false : s2 : a3 ? s2 : true says “eval a1 ; if a1 then (eval a2 ; if a2 then go to state s1 else (eval a3 ; if a3 then go to state false else go to state s2)) else (eval a3 ; if a3 then go to state s2 else go to state true)”. Note that true and false are just some special states. Depending on the application they can have different meanings, but in our applications true typically means that the monitoring requirement has been fulfilled, while false means that it has been violated. It is often convenient to represent BTTs graphically, such as the one in Figure 2. y n a1 y n y n a2 s1 a3 y n a3 false true s2 s2 Figure 2: Graphical representation for the BTT a1 ? a2 ? s1 : a3 ? false : s2 : a3 ? s2 : true. Definition 8 A multi-transition finite state machine (MT-FSM) is a triple (S, A, µ), where S is a set of states, A is a set of atomic predicates, and µ is a map from S − {true, false} to MT (S, A). If S contains true and/or false, then µ(true) = [true?true] and/or µ(false) = [true?false], respectively. A binary transition tree finite state machine (BTT-FSM) is a triple (S, A, β), where S and A are like before and β is a map from S − {true, false} to BTT (S, A). If S contains true and/or false, then it is the case that β(true) = [true] and/or β(false) = [false], respectively. For an event θ θ : A → {true, false} in any of these machines, we let s → s′ denote the fact that θMT (µ(s)) = s′ or θ θBTT (β(s)) = s′ , respectively. We take the liberty to call s → s′ a transition. Also, we may write θ θ θ θ θ 1 2 1 n n s2 , . . . , sn → sn+1 s1 → s2 → · · · sn → sn+1 and call it a sequence of transitions whenever s1 → are transitions.  30 Note that S is not required to contain the special states true and false, but if it contains them, these states transit only to themselves. The finite state machine notions above are intended to abstract the intuitive concept of a monitor. The states in S are monitor states, and some of them can trigger side effects in practice, such as messages sent or reported to users, actions taken, feedback sent to the monitored program for guidance purposes, etc. θ θ θ 1 2 n Definition 9 If true ∈ S then a sequence of transitions s1 → s2 → · · · sn → true is called a θ1 θ2 θn validating sequence (for s1 ); if false ∈ S then s1 → s2 → · · · sn → false is called an invalidating sequence (for s1 ). These notions naturally extend to corresponding finite traces of events eθ1 . . . eθn .  BTT-FSMs can and should be thought of as efficient “implementations” of MT-FSMs, in the sense that they implement multi-transitions as binary transition trees. These allow one to reduce the amount of computation in a monitor implementing a BTT-FSM to only evaluate at most all the atomic predicates in a given state in order to decide to which state to go next; however, it will only very rarely be the case that all the atomic predicates need to be evaluated. A natural question is why one should bother at all then with defining and investigating MTFSMs. Indeed, what one really looks for in the context of FSM monitoring is efficient BTT-FSMs. However, MT-FSMs are nevertheless an important intermediate concept as it will become clearer later in the paper. This is because they have the nice property of state mergeability, which allows one to elegantly generate MT-FSMs from logical formulae. By state mergeability we mean the following. Suppose that during a monitor generation process, such as that for LTL that will be presented in Subsection 7.3, one proves that states s and s′ are “logically equivalent” (for now, equivalence can be interpreted intuitively: they have the same behavior), and that s and s′ have the multi-transitions [p1 ?s1 , p2 ?s2 , ..., pn ?sn ] and [p′1 ?s′1 , p′2 ?s′2 , ..., p′n ?s′n′ ]. Then we can merge s and s′ into one state whose multi-transition is Merge([p1 ?s1 , p2 ?s2 , ..., pn ?sn ], [p′1 ?s′1 , p′2 ?s′2 , ..., p′n ?s′n′ ]), defined as follows: Merge([p1 ?s1 , p2 ?s2 , ..., pn ?sn ], [p′1 ?s′1 , p′2 ?s′2 , ..., p′n ?s′n′ ]) contains all choices p?s′′ , where s′′ is a state in {s1 , s2 , ..., sn } ∪ {s′1 , s′2 , ..., s′n′ } and • p is pi when s′′ = si for some 1 ≤ i ≤ n and s′′ 6= s′i′ for all 1 ≤ i′ ≤ n′ , or • p is p′i′ when s′′ = s′i′ for some 1 ≤ i′ ≤ n′ and s′′ 6= si for all 1 ≤ i ≤ n, or • p is pi ∨ p′i′ when s′′ = si for some 1 ≤ i ≤ n and s′′ = s′i′ for some 1 ≤ i′ ≤ n′ . It is easy to see that this multi-transition merging operation is well defined, that is, Proposition 10 Merge([p1 ?s1 , p2 ?s2 , ..., pn ?sn ], [p′1 ?s′1 , p′2 ?s′2 , ..., p′n ?s′n′ ]) is a well-formed multitransition whenever both [p1 ?s1 , p2 ?s2 , ..., pn ?sn ] and [p′1 ?s′1 , p′2 ?s′2 , ..., p′n ?s′n′ ] are well-formed multitransitions. Therefore, Merge can be seen as a function Pf (MT (S, A)) → MT (S, A), where Pf is the finite powerset operator. There are situations when a definite answer, true or false is desired at the end of the monitoring session, as it will be in the case of LTL. As explained in Section 4, the intuition for the last event in an execution trace is that the trace is infinite and stationary in that last event. This seems to be the best and simplest assumption about future when a monitoring session is ended. For such situations, we enrich our definitions of MT-FSM and BTT-FSM with support for terminal events: 31 Definition 11 A terminating multi-transition finite state machine (abbreviated MT-FSM*) is a tuple (S, A, µ, µ⋆ ), where (S, A, µ) is an MT-FSM and µ⋆ is a map from S − {true, false} to MT ({true, false}, A). A terminating binary transition tree finite state machine (BTT-FSM*) is a tuple (S, A, β, β ⋆ ), where (S, A, β) is a BTT-FSM and β ⋆ is a map from S − {true, false} to BTT ({true, false}, A). For a given event θ : A → {true, false} in any of these finite state machines, θ⋆ we let s → true (or false) denote the fact that θMT (µ⋆ (s)) = true (or false) or θBTT (β ⋆ (s)) = θ⋆ θ 1 true (or false), respectively. We call s → true (or false) a terminating transition. A sequence s1 → θ ⋆ θn θ θ ⋆ θn 1 2 2 s2 → · · · sn → false s2 → · · · sn → true is called an accepting sequence (for s1 ) and a sequence s1 → is called a rejecting sequence (for s1 ). These notions also naturally extend to corresponding finite traces of events eθ1 . . . eθn .  Languages can be associated to states in MT-FSM*s or BTT-FSM*s as finite words of events. Definition 12 Given a state s ∈ S in an MT-FSM* or in a BTT-FSM* M , we let LM (s) denote θ θ⋆ n 1 true is an accepting sequence the set of finite traces eθ1 . . . eθn with the property that s → ··· → in M . If a state s0 ∈ S is desired to be initial, then we write it at the end of the tuple, such as θ1 θn (S, A, µ, µ∗, s0 ) or (S, A, β, β∗, s0 ), and let LM denote LM (s0 ). If s0 → s1 · · · → sn is a sequence of transitions from the initial state, then eθ1 . . . eθn is called a valid prefix if and only if eθ1 . . . eθn t ∈ LM for any (empty or not) trace t, and it is called an invalid prefix if and only if eθ1 . . . eθn t 6∈ LM for any trace t.  The following is immediate. θ Proposition 13 If true ∈ S then LM (true) = E ⋆ , and if false ∈ S then LM (false) = ∅. If s → s′ in θ θ 1 n M then LM (s′ ) = {t | eθ t ∈ LM (s)}; more generally, if s → s1 · · · → sn is a sequence of transitions in M then LM (sn ) = {t | eθ1 . . . eθn t ∈ LM (s)}. In particular, if s = s0 then eθ1 . . . eθn is a valid prefix if and only if LM (sn ) = E ⋆ , and it is an invalid prefix if and only if LM (sn ) = ∅. 7.2 From MT-FSMs to BTT-FSMs Supposing that one has encoded a logical requirement into an MT-FSM (we shall see how to do it for LTL in the next subsection), the next important step is to generate an efficient equivalent BTT-FSM. In the worst possible case one just has to evaluate all the atomic predicates in order to proceed to the next state of a BTT-FSM, so they are preferred to MT-FSMs. What one needs to do is to develop a procedure that takes a multi-transition and returns a BTT. More BTTs can encode the same multi-transition, so one needs to develop some criteria to select the better ones. A natural selection criterion would be to minimize the average amount of computation. For example, if all atomic predicates are equally probable to hold and if an atomic predicate is very expensive to evaluate, then one would select that BTT that places the expensive predicate as deeply as possible, so its evaluation is delayed as much as possible. Based on the above, we believe that the following is an important theoretical problem in runtime monitoring: Problem: Optimal BTT Input: A set of atomic predicates a1 , ..., ak that hold with probabilities π1 , ..., πk and have costs c1 , ..., ck , respectively, and a multi-transition p1 ?s1 , ..., pn ?sn where p1 , ..., pn are Boolean formulae over a1 , ..., ak . 32 State MT for non-terminal events [ 1 yellow \/ !green !yellow /\ green /\ !red !yellow /\ green /\ red [ 2 yellow !yellow /\ !red !yellow /\ red ? ? ? ? ? ? 1, 2, false MT for terminal events 1, 2, false [ ] yellow \/ !green !yellow /\ green [ ] yellow !yellow ? ? ? ? true, false true, false ] ] Figure 3: MT-FSM for the formula [](green -> !red U yellow). Output: A BTT implementing the multi-transition that probabilistically minimizes the amount of computation to decide the next state. The probabilities and the costs in the problem above can be estimated either by static analysis of the source code of the program, or dynamically by first running and measuring the program several times, or by combinations of those. We do not know how to solve this interesting problem yet, but we conjecture the following result that we currently use in our implementation: Conjecture 14 If π1 = ... = πk and c1 = ... = ck then the solution to the problem above is the BTT of minimal size. Our current multi-transition to BTT algorithm is exponential in the number of atomic predicates; it simply enumerates all possible BTTs recursively and then selects the one of minimal size. Generating the BTTs takes significantly less time than generating the MT-FSM, so we do not regard it as a practical problem yet. However, it seems to be a challenging theoretical problem. Example 15 Consider again the traffic light controller safety property stating that “after green comes yellow”, which was written as [](green -> (!red U yellow)) using LTL notation. Since more than one light can be lit at any moment, one should be very careful when expressing this safety property as an MT-FSM or a BTT-FSM. Let us first express it as an MT-FSM. We need two states, one for the case in which green has not triggered yet the !red U yellow part and another for the case when it has. The condition to stay in state 1 is then yellow \/ !green and the condition to move to state 2 is !yellow /\ green /\ !red. If both a green and a red are seen then the machine should move to state false. The condition to move from state 2 back to state 1 is yellow, while the condition to stay in state 2 is !yellow /\ !red; !yellow /\ red should also move the machine in its false state. If the event is terminal then a yellow would make the reported answer true, i.e., the observed trace is an accepting sequence; if yellow is not true, then in state 1 the answer should be the opposite of green, while in state 2 the answer should be simply false. This MT-FSM is shown in Figure 3. The interesting aspect of our FSMs is that not all the atomic predicates need to always be evaluated. For example, in state 2 of this MT-FSM the predicate green is not needed. A BTTFSM further reduces the amount of predicates to be evaluated, by enforcing an “evaluate-by-need” policy. Figure 4 shows a BTT-FSM implementing the MT-FSM in Figure 3.  33 State BTT for non-terminal events y BTT for terminal events n yellow y 1 n green y n yellow y n 1 red 1 false true y 2 y n green false true n yellow 1 y n red y n yellow 2 false 2 true false Figure 4: A BTT-FSM for the formula [](green -> !red U yellow). 7.3 From LTL Formulae to BTT-FSMs Informally, our algorithm to generate optimal BTT-FSMs from LTL formulae consists of two steps. First, it generates an MT-FSM with a minimum number of states. Then, using the technique presented in the previous subsection, it generates an optimal BTT from each MT. To generate a minimal MT-FSM, our algorithm uses the rewriting based procedure presented in Section 6 on all possible events, until the set of formulae to which the original LTL formula can “evolve” stabilizes. The procedure LTL2MT-FSM shown in Figure 5 builds an MT-FSM whose states are formulae, with the help of a validity checker. Initially, the set S of states contains only the original LTL formula. LTL2MT-FSM is called on the original LTL formula, and then recursively in a depth-first manner on all the formulae to which the initial formula can ever evolve via the event-consuming operator { } introduced in Section 6. For each LTL state formula ϕ in S, multi-transitions µ(ϕ) and µ⋆ (ϕ) are maintained. For each possible event θ, one first updates µ⋆ (ϕ) by considering the case in which θ is the last event in a trace (step 8), and then the current formula ϕ evolves into the corresponding formula ϕθ (step 9). If some equivalent formula ϕ′ to ϕθ has already been discovered then one only needs to modify the multi-transition set of ϕ accordingly in order to point to ϕ′ (step 11). Notice that equivalence of LTL formulae is checked by using a validity procedure (step 10), which is given in Figure 6. If there is no formula in S equivalent to ϕθ , then the new formula ϕθ is added to S, multi-transition µ(ϕ) is updated accordingly, and then the MT-FSM generation procedure is called recursively on ϕθ . This way, one eventually generates all possible LTL formulae into which the initial formula can ever evolve during a monitoring session; this happens, of course, modulo finite trace LTL semantic equivalence, implemented elegantly using the validity checker described below. By Theorem 2, this recursion will eventually terminate, leading to an MT-FSM*. The procedure Valid used in LTL2MT-FSM above is defined in Figure 6. It essentially follows the same idea of generating all possible formulae to which the original LTL formula tested for validity can evolve via the event consumption operator defined by rewriting in Section 6, but for each newly obtained formula ϕ and for each event θ, it also checks whether an execution trace stopping at that event would be a rejecting sequence. The intuition for this check is that a formula is valid under finite trace LTL semantics if and only if any (finite) sequence of events satisfies that formula; since any generated formula corresponds to one into which the initial formula can evolve, 34 1. let S be ϕ 2. procedure LTL2MT-FSM(ϕ) 3. let µ⋆ (ϕ) be ∅ 4. let µ(ϕ) be ∅ 5. foreach θ : A → {true, false} do 6. let eθ be the list of atomsVa with θ(a) = true V 7. let pθ be the proposition {a | θ(a) = true} ∧ {¬a | θ(a) = false} 8. let µ⋆ (ϕ) be Merge([pθ ? ϕ{e⋆θ }], µ⋆ (ϕ)) 9. let ϕθ be ϕ{eθ } 10. if there is ϕ′ ∈ S with Valid(ϕθ ↔ ϕ′ ) 11. then let µ(ϕ) be Merge([pθ ? ϕ′ ], µ(ϕ)) 12. else let S be S ∪ {ϕθ } 13. let µ(ϕ) be Merge([pθ ? ϕθ ], µ(ϕ)) 14. LTL2MT-FSM(ϕθ ) 15. endfor 16. if µ(ϕ) = [true ? ϕ] and µ⋆ (ϕ) = [true ? b] then replace ϕ by b everywhere 17. endprocedure Figure 5: Algorithm to generate a minimal MT-FSM* (S, A, µ, µ⋆ , ϕ) from an LTL formula ϕ. we need to make sure that each of these formulae becomes true under any possible last monitored event. This is done by rewriting the term ϕ{e⋆θ } with the rules in Section 6 to its normal form (step 5.), i.e., true or false. The formula is valid if and only if there is no rejecting sequence; the entire space of evolving formulae is again explored by depth-first search. Notice that Valid does not test for equivalence of formulae, so it can potentially generate a larger number of formulae than LTL2MT-FSM. However, by Theorem 2, this procedure will also eventually terminate. Theorem 16 Given an LTL formula ϕ of size m, the following hold: 1. The procedure Valid is correct, that is, Valid(ϕ) returns true if and only if ϕ is satisfied, as defined in Section 4, by any finite trace; m 2. The space and time complexity of Valid(ϕ) is 2O(2 ) ; Additionally, letting M denote the MT-FSM* (S, A, µ, µ⋆ , ϕ) generated by LTL2MT-FSM(ϕ), the following hold: 3. LTL2MT-FSM(ϕ) is correct; more precisely, M has the property that for any events θ1 , ..., θ θ θ⋆ 1 n θn , θ, it is the case that ϕ → ϕ1 · · · → ϕn → true if and only if the finite trace eθ1 . . . eθn eθ θ θ θ⋆ 1 n satisfies ϕ, and ϕ → ϕ1 · · · → ϕn → false if and only if eθ1 . . . eθn eθ does not satisfy ϕ; θ θ 1 n 4. M is synchronous; more precisely, for any events θ1 , ..., θn , it is the case that ϕ → ϕ1 · · · → eθ eθ θ1 θn true if and only if eθ1 . . . eθn eθ is a valid prefix of ϕ, and ϕ → false if ϕn → ϕ1 · · · → ϕn → and only if eθ1 . . . eθn eθ is a bad prefix of ϕ; 35 1. let S be ϕ 2. function Valid(ϕ) 3. foreach θ : A → {true, false} do 4. let eθ be the list of atoms a with θ(a) = true 5. if ϕ{e⋆θ } = false then return false 6. let ϕθ be ϕ{eθ } 7. if ϕθ ∈ 6 S 8. then let S be S ∪ {ϕθ } 9. if Valid(ϕθ ) = false then return false 10. endfor 11. return true 12. end function Figure 6: Validity checker for an LTL formula ϕ. m 5. The space and time complexity of LTL2MT-FSM(ϕ) is 2O(2 ) ; 6. M is the smallest MT-FSM* which is correct and synchronous (as defined in 3 and 4 above); 7. Monitoring against a BTT-FSM* corresponding to M , as shown in Subsection 7.2, needs O(2m ) space and time. Proof: 1. Using a depth-first search strategy, the procedure Valid visits all possible formulae into which the original formula ϕ can evolve via any possible sequence of events. These formulae are different modulo the rewriting rules in Section 6 which are used to simplify LTL formulae and to remove the derivatives, but those rules were shown to be sound, so Valid indeed explores all possible LTL formulae into which ϕ can semantically evolve. Moreover, for each such formula, say ϕ′ , and each event, Valid checks at Step 5 whether a trace terminating with that event in ϕ′ would lead to rejection. Valid(ϕ) returns true if and only if there is no such rejection, so it is indeed correct: if it had been some finite trace that did not satisfy ϕ then Valid would have found it during its exhaustive search. 2. The space required by Valid(ϕ) is clearly dominated by the size of S. By Theorem 2, each formula ϕ′ into which ϕ can evolve needs O(2m ) space to be stored. That means that there can be m m at most 2O(2 ) such formulae. So the total space needed to store S in the worst case is 2O(2 ) . An amortized analysis of its running time, tells us that Valid runs its “for each event” loop one per formula in S. Since the number of events is much less than 2m and since reducing the derivative of a formula by an event takes time proportional with the size of the formula, we deduce that the m total running time of the Valid procedure is 2O(2 ) . 3. By Theorem 3 and the event consuming procedure described in Subsection 6.1, it follows that eθ1 . . . eθn eθ satisfies ϕ if and only if ϕ{eθ1 }{· · ·}{eθn }{e⋆θ } reduces to true, and that eθ1 . . . eθn eθ does not satisfy ϕ if and only if ϕ{eθ1 }{· · ·}{eθn }{e⋆θ } reduces to false. It is easy to see that Valid(ψ ↔ ψ ′ ) returns true if and only if ψ and ψ ′ are formulae equivalent under the finite trace 36 LTL semantics. That means the MT-FSM* generated by LTL2MT-FSM(ϕ) contains a formulaθ 1 state ϕ1 which is equivalent to ϕ{eθ1 }; moreover, ϕ → ϕ1 in this MT-FSM*. Inductively, one can θ θ 1 n show that there is a series of formulae-states ϕ1 , ..., ϕn such that ϕ → ϕ1 · · · → ϕn is a sequence of transitions in the generated MT-FSM and ϕ{eθ1 }{· · ·}{eθn } is equivalent to ϕn . The rest follows θ⋆ by noting that ϕn {e⋆θ } reduces to true if and only if ϕn → true in the generated MT-FSM, and that θ⋆ ϕn {e⋆θ } reduces to false if and only if ϕn → false in the MT-FSM. 4. As in 3 above, one can inductively show that there is a series of formulae ϕ1 , ..., ϕn , ϕ′ θ1 θn θ such that ϕ → ϕ1 · · · → ϕn → ϕ′ is a sequence of transitions in the generated MT-FSM and ϕ{eθ1 }{· · ·}{eθn }{eθ } is equivalent to ϕ′ . By Proposition 13, due to the use of the validity checker in step 10 of LTL2MT-FSM it follows that eθ1 . . . eθn eθ is a valid prefix of ϕ if and only if ϕ′ is equivalent to true, and that eθ1 . . . eθn eθ is a bad prefix of ϕ if and only if ϕ′ is equivalent to false. The rest follows now by Step 16 in the algorithm in Figure 5, which, due to the validity checker, provides a necessary and sufficient condition for a processed formula to be semantically equivalent to true or false, respectively. 5. The space required by LTL2MT-FSM(ϕ) is again dominated by the size of S. Like in the m analysis of Valid in 2 above, by Theorem 2 we get that there can be at most 2O(2 ) formulae m generated in S, so the total space needed to store S in the worst case is also 2O(2 ) . For each newly added formula in S and for each event, Step 10 calls the procedure Valid potentially once for each already existing formula in S. It is important to notice that the formulae on which Valid is called are exclusive disjunctions of conjunctions of sub-formulae rooted in temporal operators of m the original formula ϕ, so its space and time complexity will also be 2O(2 ) each time it is called by LTL2MT-FSM. One can easily see now that the space and time requirements of LTL2MTm FSM(ϕ) are also 2O(2 ) (the constant in the exponent O(2m ) can be appropriately enlarged). 6. For any MT-FSM* machine M ′ = (S ′ , A, µ′ , µ′⋆ , q0 ), one can associate a formula, more precisely a state in M , to each state in S ′ as follows. ϕ is associated to the initial state q0 . Then for each θ transition q1 → q2 in M ′ such that q1 has a formula ϕ1 associated and q2 has no formula associated, θ then one associates that ϕ2 to q2 with the property that ϕ1 → ϕ2 is a transition in M . For a state q in S ′ let ϕq be the formula in S associated to it. Since M ′ is correct and synchronous for ϕ, it follows that LM ′ (q) = LM (ϕq ) for any state q in S ′ . We can now show that the map associating a formula in S to any state in S ′ is surjective, which shows that M has therefore a minimal number θ1 θn θ of states. Let ϕ′ be a formula in S and let ϕ → ϕ1 · · · → ϕn → ϕ′ be a sequence of transitions in M leading to ϕ′ ; note that all formulae in S are reachable via transitions in M from ϕ. Let q ′ be the θ1 θn θ state in S ′ such that q0 → q1 · · · → qn → q ′ . Then LM ′ (q ′ ) = LM (ϕq′ ). Since by Proposition 13, ′ LM ′ (q ) = {t | eθ1 . . . eθn eθ t ∈ LM ′ (q0 )} and LM (ϕ′ ) = {t | eθ1 . . . eθn eθ t ∈ LM (ϕ)}, it follows that LM (ϕq′ ) = LM (ϕ′ ), that is, that ϕq′ and ϕ′ are equivalent. Since Step 10 of the LTL2MT-FSM eventually uses the validity checker on any pairs of formulae in S, it follows that ϕq′ = ϕ′ . 7. In order to distinguish N pieces of data, log(N ) bits are needed to encode each datum. Therefore, one needs O(2m ) bits to encode a state of M , which is the main memory needed by the monitoring algorithm. Like in Theorem 2, we assume “idealistic” rewriting engines able to optimize the space requirements; we are not aware whether Maude is able to attain this performance or not. To make a transition from one state to another, a BTT-FSM* associated to the MT-FSM* generated 37 by LTL2MT-FSM(ϕ) needs to only evaluate at most all the atomic predicates occurring in the formula, which, assuming that evaluating atom predicates takes unit time, is clearly O(2m ). When the entire BTT is evaluated, it finally has to store the newly obtained state of the BTT-FSM*, which can reuse the space of the previous one, O(2m ), and which also takes time O(2m ).  Once the procedure LTL2MT-FSM terminates, the formulae ϕ, ϕ′ , etc., are not needed anymore, so one can and should replace them by unique labels in order to reduce the amount of storage needed to encode the MT-FSM* and/or the corresponding BTT-FSM*. This algorithm can be relatively easily implemented in any programming language. We have, however, found Maude again a very elegant system for this task, implementing the entire LTL formula to BTT-FSM algorithm in about 200 lines of code. 7.4 Examples The BTT-FSM generation algorithm presented in this section, despite its overall worst-case high startup time, can be very useful when formulae are relatively short, as it is most often the case in practice. For the traffic light controller requirement formula discussed previously in the paper, [](green -> (!red) U yellow), this algorithm generates in about 0.2 seconds the optimal BTT-FSM* in Figure 7, also shown in Figure 4 in flowchart notation; Figure 3 shows its optimal MT-FSM*. For simplicity, the states true and false do not appear in Figure 7. Notice that the atomic predicate State 1 2 Non-terminal event yellow ? 1 : green ? red ? false : 2 : 1 yellow ? 1 : red ? false : 2 Terminal event yellow ? true : green ? false : true yellow ? true : false Figure 7: An optimal BTT-FSM* for the formula [](green -> !red U yellow). does not need to be evaluated on terminal events and that green does not need to be evaluated in state 2. In this example, the colors are not supposed to exclude each other, that is, the traffic controller can potentially be both green and red. The LTL formulae on which our algorithm has the worst performance are those containing many nested temporal operators (which are not frequently used in specifications anyway, because of the high risk of getting them wrong). For example, it takes our Maude implementation of this algorithm 1.3 seconds to generate the minimal 3-state (true and false states are not counted) BTT-FSM* for the formula a U (b U (c U d)) and 13.2 seconds to generate the 7-state minimal BTT-FSM* for the formula ((a U b) U c) U d. It never took our current implementation more than a few seconds to generate the BTT-FSM* of any LTL formula of interest for our applications, i.e., non-artificial. Figure 8 shows the generated BTT-FSM of some artificial LTL formulae, taking together less than 15 seconds to be generated. To keep the figure small, the states true and false together with their self-transitions are not shown in Figure 8, and they are replaced by t and f in BTTs. The generated BTT-FSM*s are monitored most efficiently on RAM machines, due to the fact that conditional statements are implemented via jumps in memory. Monitoring BTT-FSM*s using rewriting does not seem appropriate because it would require linear time, as a function of number of states, to extract the BTT associated to a state in a BTT-FSM*. However, we believe that the algorithm presented in Section 6 is satisfactory in practice if one is willing to use a rewriting engine for monitoring. red 38 Formula []<> a <>([]a ∨ []¬a) [](a -> <>b) a U (b U c) a U (b U (c U d)) ((a U b) U c) U d State 1 − 1 2 1 2 1 2 3 1 2 3 4 5 6 7 Monitoring BTT 1 − a ? (b ? 1 : 2) : 1 b?1:2 c ? t : (a ? 1 : (b ? 2 : f)) c ? t : (b ? 2 : f) d?t:a?1:b?2:c?3:f d?t:b?2:c?3:f d?t:c?3:f d?t:c?1:b?4:a?5:f b?c?t:7:a?c?6:2:f b?d?t:c?1:4:a?d?6:c?3:5:f c?d?t:1:b?d?7:4:a?d?2:5:f b?d?c?t:7:c?1:4:a?d?c?6:2:c?3:5:f b?t:a?6:f c?t:b?7:a?2:f Terminating BTT a?t:f − a ? (b ? t : f) : t b?t:f c?t:f c?t:f d?t:f d?t:f d?t:f d?t:f c?b?t:f:f d?b?t:f:f d?c?t:f:f d?c?b?t:f:f:f b?t:f c?t:f Figure 8: Six BTT-FSM*s generated in less than 15 seconds. 8 Conclusions This paper presented a foundational study in using rewriting in runtime verification and monitoring of systems. After a short discussion on types of monitoring and mathematical and technological preliminaries, a finite trace linear temporal logic was defined, together with an immediate but inefficient implementation of a monitor following directly its semantics. Then an efficient but ineffective implementation based on dynamic programming was presented, which traverses the execution trace backwards. The first effective and relatively efficient rewriting algorithm was further introduced, based on the idea of transforming the monitoring requirements as events are received from the monitored program. A non-trivial improvement of this algorithm based on hashing rewriting results, thereby reducing the number of rewritings performed during trace analysis, was also proposed. The hashing corresponds to building an observer automaton on-the-fly, having the advantage that only the part of the automaton that is needed for analyzing a given trace is generated. The resulting algorithm is very efficient. Since in many cases one would want to generate an observer finite state machine (or automaton) a priori, for example when a rewriting system cannot be used for monitoring, or when minimal runtime overhead is needed, a specialized data-structure called a binary transition tree (BTT) and corresponding finite state machines were introduced, and an algorithm for generating minimal such monitors from temporal formulae was discussed. All algorithms were implemented in surprisingly few lines of Maude code, illustrating the strength of rewriting for this particular domain. In spite of the reduced size of the code, the implementations seem to be efficient for practical purposes. As a consequence, we have demonstrated how rewriting can be used not only to experiment with runtime monitoring logics, but also as an implementation language. As an example of future work is the extension of LTL with realtime constraints. Since Maude by itself provides a high-level specification language, one can argue that Maude in its entirety can be used for writing requirements. Further work will show whether this avenue is fruitful. Some of the discussed results and algorithms have been already used in two NASA applications, JPaX and X9, but an extensive experimental assesment of these techniques 39 is left as future work. Acknowledgement. The authors warmly thank the anonymous reviewers for their very detailed and useful comments and suggestions on how to improve this paper. References [1] C. Artho, D. Drusinsky, A. Goldberg, K. Havelund, M. Lowry, C. Pasareanu, G. Roşu, and W. Visser. Experiments with Test Case Generation and Runtime Analysis. In Proc. of ASM’03: Abstract State Machines, volume 2589 of Lecture Notes in Computer Science, pages 87–107, Taormina, Italy, March 2003. Springer. [2] T. Ball, A. Podelski, and S. Rajamani. Boolean and Cartesian Abstractions for Model Checking C Programs. In Proc. of TACAS’01: Tools and Algorithms for the Construction and Analysis of Systems, Lecture Notes in Computer Science, Genova, Italy, April 2001. [3] A. Bouhoula, J.-P. Jouannaud, and J. Meseguer. Specification and Proof in Membership Equational Logic. Theoretical Computer Science, 236:35–132, 2000. [4] R. E. Bryant. Graph-Based Algorithms for Boolean Function Manipulation. IEEE Transactions on Computers, C-35(8):677–691, August 1986. [5] F. Chen and G. Roşu. Towards Monitoring-Oriented Programming: A Paradigm Combining Specification and Implementation. In Proc. of RV’03: the Third International Workshop on Runtime Verification, volume 89 of Electronic Notes in Theoretical Computer Science, pages 106–125, Boulder, Colorado, USA, 2003. Elsevier Science. [6] M. Clavel. The ITP Tool. In Logic, Language and Information. Proc. of the First Workshop on Logic and Language, pages 55–62. Kronos, 2001. [7] M. Clavel, F. J. Durán, S. Eker, P. Lincoln, N. Martı́-Oliet, J. Meseguer, and J. F. Quesada. Maude: Specification and Programming in Rewriting Logic, March 1999. Maude System documentation at http://maude.csl.sri.com/papers. [8] M. Clavel, F. J. Durán, S. Eker, P. Lincoln, N. Martı́-Oliet, J. Meseguer, and J. F. Quesada. Maude: Specification and Programming in Rewriting Logic. Theoretical Computer Science, 285:187–243, 2002. [9] J. Corbett, M. B. Dwyer, J. Hatcliff, C. S. Pasareanu, Robby, S. Laubach, and H. Zheng. Bandera : Extracting Finite-state Models from Java Source Code. In Proc. of ICSE’00: International Conference on Software Engineering, Limerich, Ireland, June 2000. ACM Press. [10] M. Dahm. BCEL. http://jakarta.apache.org/bcel. [11] C. Demartini, R. Iosif, and R. Sisto. A Deadlock Detection Tool for Concurrent Java Programs. Software Practice and Experience, 29(7):577–603, July 1999. [12] D. Drusinsky. The Temporal Rover and the ATG Rover. In Proc. of SPIN’00: SPIN Model Checking and Software Verification, volume 1885 of Lecture Notes in Computer Science, pages 323–330, Stanford, California, USA, 2000. Springer. 40 [13] D. Drusinsky. Monitoring Temporal Rules Combined with Time Series. In Proc. of CAV’03: Computer Aided Verification, volume 2725 of Lecture Notes in Computer Science, pages 114– 118, Boulder, Colorado, USA, 2003. Springer-Verlag. [14] C. J. Fidge. Partial Orders for Parallel Debugging. In Proc. of the 1988 ACM SIGPLAN and SIGOPS workshop on Parallel and Distributed Debugging, pages 183–194. ACM, 1988. [15] B. Finkbeiner, S. Sankaranarayanan, and H. Sipma. Collecting Statistics over Runtime Executions. In Proc. of RV’02: The Second International Workshop on Runtime Verification, volume 70 of Electronic Notes in Theoretical Computer Science, Paris, France, 2002. Elsevier. [16] B. Finkbeiner and H. Sipma. Checking Finite Traces using Alternating Automata. In Proc. of RV’01: The First International Workshop on Runtime Verification, volume 55(2) of Electronic Notes in Theoretical Computer Science, Paris, France, 2001. Elsevier Science. [17] D. Giannakopoulou and K. Havelund. Automata-Based Verification of Temporal Properties on Running Programs. In Proc. of ASE’01: International Conference on Automated Software Engineering, pages 412–416. Institute of Electrical and Electronics Engineers, Coronado Island, California, 2001. [18] P. Godefroid. Model Checking for Programming Languages using VeriSoft. In Proc. of POPL’97: the 24th ACM Symposium on Principles of Programming Languages, pages 174–186, Paris, France, January 1997. [19] J. Goguen, K. Lin, G. Roşu, A. Mori, and B. Warinschi. An overview of the Tatami project. In Cafe: An Industrial-Strength Algebraic Formal Method, pages 61–78. Elsevier, 2000. [20] J. Goguen, J. Thatcher, E. Wagner, and J. Wright. Initial Algebra Semantics and Continuous Algebras. Journal of the Association for Computing Machinery, 24(1):68–95, January 1977. [21] J. Goguen, T. Winkler, J. Meseguer, K. Futatsugi, and J.-P. Jouannaud. Introducing OBJ. In Software Engineering with OBJ: Algebraic Specification in Action. Kluwer, 2000. [22] E. Gunter and D. Peled. Tracing the Executions of Concurrent Programs. In Proc. of RV’02: Second International Workshop on Runtime Verification, volume 70 of Electronic Notes in Theoretical Computer Science, Copenhagen, Denmark, 2002. Elsevier. [23] E. L. Gunter, R. P. Kurshan, and D. Peled. PET: An Interactive Software Testing Tool. In Proc. of CAV’00: Computer Aided Verification, volume 1885 of Lecture Notes in Computer Science, pages 552–556, Chicago, Illinois, USA, 2003. Springer-Verlag. [24] E. L. Gunter and D. Peled. Using Functional Languages in Formal Methods: The PET System. In Parallel and Distributed Processing Techniques and Applications, pages 2981–2986. CSREA, 2000. [25] K. Havelund, S. Johnson, and G. Roşu. Specification and Error Pattern Based Program Monitoring. In Proc. of the European Space Agency workshop on On-Board Autonomy, Noordwijk, The Netherlands, October 2001. 41 [26] K. Havelund, M. R. Lowry, and J. Penix. Formal Analysis of a Space Craft Controller using SPIN. IEEE Transactions on Software Engineering, 27(8):749–765, August 2001. An earlier version occurred in the Proc. of SPIN’98: the fourth SPIN workshop, Paris, France, 1998. [27] K. Havelund and T. Pressburger. Model Checking Java Programs using Java PathFinder. International Journal on Software Tools for Technology Transfer, 2(4):366–381, April 2000. Special issue containing selected submissions to SPIN’98: the fourth SPIN workshop, Paris, France, 1998. [28] K. Havelund and G. Roşu. Java PathExplorer – A Runtime Verification Tool. In Proc. of i-SAIRAS’01: the 6th International Symposium on Artificial Intelligence, Robotics and Automation in Space, Montreal, Canada, June 2001. [29] K. Havelund and G. Roşu. Monitoring Java Programs with Java PathExplorer. In Proc. of RV’01: the First International Workshop on Runtime Verification, volume 55 of Electronic Notes in Theoretical Computer Science, pages 97–114, Paris, France, July 2001. Elsevier Science. [30] K. Havelund and G. Roşu. Monitoring Programs using Rewriting. In Proc. of ASE’01: International Conference on Automated Software Engineering, pages 135–143. Institute of Electrical and Electronics Engineers, Coronado Island, California, USA, 2001. [31] K. Havelund and G. Roşu. Testing Linear Temporal Logic Formulae on Finite Execution Traces. Technical Report TR 01-08, RIACS, May 2001. Written 20 December 2000. [32] K. Havelund and G. Roşu. Efficient Monitoring of Safety Properties. Software Tools and Technology Transfer, to appear. [33] K. Havelund and G. Roşu. Synthesizing Monitors for Safety Properties. In Proc. of TACAS’02: Tools and Algorithms for Construction and Analysis of Systems, volume 2280 of Lecture Notes in Computer Science, pages 342–356, Grenoble, France, 2002. Springer. [34] K. Havelund and N. Shankar. Experiments in Theorem Proving and Model Checking for Protocol Verification. In Proc. of FME’96: Industrial Benefit and Advances in Formal Methods, volume 1051 of Lecture Notes in Computer Science, pages 662–681, Oxford, England, 1996. Springer. [35] G. J. Holzmann and M. H. Smith. A Practical Method for Verifying Event-Driven Software. In Proc. of ICSE’99: International Conference on Software Engineering, Los Angeles, California, USA, May 1999. IEEE/ACM. [36] J. Hopcroft and J. Ullman. Introduction to Automata Theory, Languages, and Computation. Addison-Wesley, Reading, Massachusetts, 1979. [37] J. Hsiang. Refutational Theorem Proving using Term Rewriting Systems. Artificial Intelligence, 25:255–300, 1985. [38] M. Kim, S. Kannan, I. Lee, and O. Sokolsky. Java-MaC: a Run-time Assurance Tool for Java. In Proc. of RV’01: First International Workshop on Runtime Verification, volume 55 of Electronic Notes in Theoretical Computer Science, Paris, France, 2001. Elsevier Science. 42 [39] D. Kortenkamp, T. Milam, R. Simmons, and J. Fernandez. Collecting and Analyzing Data from Distributed Control Programs. In Proc. of RV’01: First International Workshop on Runtime Verification, volume 55 of Electronic Notes in Theoretical Computer Science, Paris, France, 2001. Elsevier Science. [40] O. Kupferman and M. Y. Vardi. Freedom, Weakness, and Determinism: From Linear-Time to Branching-Time. In Proc. of the IEEE Symposium on Logic in Computer Science, pages 81–92, 1998. [41] O. Kupferman and M. Y. Vardi. Model Checking of Safety Properties. In Proc. of CAV’99: Conference on Computer-Aided Verification, Trento, Italy, 1999. [42] O. Kupferman and S. Zuhovitzky. An Improved Algorithm for the Membership Problem for Extended Regular Expressions. In Proc. of the International Symposium on Mathematical Foundations of Computer Science, volume 2420 of Lecture Notes in Computer Science, 2002. [43] I. Lee, S. Kannan, M. Kim, O. Sokolsky, and M. Viswanathan. Runtime Assurance Based on Formal Specifications. In Proc. of PDPTA’99: International Conference on Parallel and Distributed Processing Techniques and Applications, Las Vegas, Nevada, USA, 1999. [44] Z. Manna and A. Pnueli. The Temporal Logic of Reactive and Concurrent Systems. Springer, New York, 1992. [45] Z. Manna and A. Pnueli. Temporal Verification of Reactive Systems: Safety. Springer, New York, 1995. [46] N. Markey and P. Schnoebelen. Model Checking a Path (Preliminary Report). In Proc. of CONCUR’03: International Conference on Concurrency Theory, volume 2761 of Lecture Notes in Computer Science, pages 251–265, Marseille, France, August 2003. Springer. [47] J. Meseguer. Conditional Rewriting Logic as a Unified Model of Concurrency. Theoretical Computer Science, pages 73–155, 1992. [48] J. Meseguer. Membership Algebra as a Logical Framework for Equational Specification. In Proc. of WADT’97: Workshop on Algebraic Development Techniques, volume 1376 of Lecture Notes in Computer Science, pages 18–61, Tarquinia, Italy, June 1998. Springer. [49] T. O’Malley, D. Richardson, and L. Dillon. Efficient Specification-Based Oracles for Critical Systems. In Proc. of the California Software Symposium, 1996. [50] D. Y. Park, U. Stern, and D. L. Dill. Java Model Checking. In Proc. of the First International Workshop on Automated Program Analysis, Testing and Verification, Limerick, Ireland, June 2000. [51] A. Pnueli. The Temporal Logic of Programs. In Proc. of the 18th IEEE Symposium on Foundations of Computer Science, pages 46–77, 1977. [52] D. J. Richardson, S. L. Aha, and T. O. O’Malley. Specification-Based Test Oracles for Reactive Systems. In Proc. of ICSE’92: International Conference on Software Engineering, pages 105– 118, Melbourne, Australia, 1992. 43 [53] G. Roşu and K. Havelund. Synthesizing Dynamic Programming Algorithms from Linear Temporal Logic Formulae. RIACS Technical report TR 01-08, January 2001. [54] G. Roşu and M. Viswanathan. Testing Extended Regular Language Membership Incrementally by Rewriting. In Proc. of RTA’03: Rewriting Techniques and Applications, volume 2706 of Lecture Notes in Computer Science, pages 499–514, Valencia, Spain, June 2003. SpringerVerlag. [55] S. Savage, M. Burrows, G. Nelson, P. Sobalvarro, and T. Anderson. Eraser: A Dynamic Data Race Detector for Multithreaded Programs. ACM Transactions on Computer Systems, 15(4):391–411, November 1997. [56] A. Sen and V. K. Garg. Partial Order Trace Analyzer (POTA) for Distrubted Programs. In Proc. of RV’03: the Third International Workshop on Runtime Verification, volume 89 of Electronic Notes in Theoretical Computer Science, Boulder, Colorado, USA, 2003. Elsevier Science. [57] K. Sen and G. Roşu. Generating Optimal Monitors for Extended Regular Expressions. In Proc. of RV’03: the Third International Workshop on Runtime Verification, volume 89 of Electronic Notes in Theoretical Computer Science, pages 162–181, Boulder, Colorado, USA, 2003. Elsevier Science. [58] K. Sen, G. Roşu, and G. Agha. Runtime Safety Analysis of Multithreaded Programs. In Proc. of ESEC/FSE’03: European Software Engineering Conference and ACM SIGSOFT International Symposium on the Foundations of Software Engineering. ACM, Helsinki, Finland, September 2003. [59] N. Shankar, S. Owre, and J. M. Rushby. PVS Tutorial. Computer Science Laboratory, SRI International, Menlo Park, CA, February 1993. Also appears in Tutorial Notes, Formal Methods Europe ’93: Industrial-Strength Formal Methods, pages 357–406, Odense, Denmark, April 1993. [60] A. P. Sistla and E. M. Clarke. The Complexity of Propositional Linear Temporal Logics. Journal of the ACM (JACM), 32(3):733–749, 1985. [61] S. D. Stoller. Model-Checking Multi-Threaded Distributed Java Programs. In Proc. of SPIN’00: SPIN Model Checking and Software Verification, volume 1885 of Lecture Notes in Computer Science, pages 224–244, Stanford, California, USA, 2000. Springer. [62] W. Visser, K. Havelund, G. Brat, and S. Park. Model Checking Programs. In Proc. of ASE’00: International Conference on Automated Software Engineering, Grenoble, France, September 2000. IEEE CS Press. [63] H. Yamamoto. An Automata-based Recognition Algorithm for Semi-Extended Regular Expressions. In Proc. of the International Symposium on Mathematical Foundations of Computer Science, volume 1893 of Lecture Notes in Computer Science, pages 699–708, 2000. 44