Keywords

1 Introduction

Ensuring the correctness of software artefacts is one of the most important issues in software engineering. Testing is one of the most frequently used quality assurance activities aimed at discovering software defects. However,

Program testing can be used to show the presence of bugs, but never to show their absence! E.W. Dijkstra [12]

A way of actually ensuring the correctness of a program is to write a proof for it, either manually or with the help of a proof assistant [4, 21]. Although founders of computer science such as Floyd [14], Hoare [15] and others developed logical frameworks that support such proofs, the practice of proving program correctness is mostly limited to academia and few other contexts. In fact, writing program proofs requires mathematical skills that are uncommon in ordinary software developers, as well as considerable amounts of time.

1.1 Automated Verification

These difficulties made the case for trying to automate program proof making, with the ideal aim to produce tools that prove or disprove a program’s correctness with little to no user intervention. Unfortunately, computability theory tells us that most program properties, albeit trivial, are undecidable [16]. This, however, did not prevent the field of program verification from arising.

We can identify two main lines of research on program analysis: Abstract Interpretation [11] and Model Checking [10]. While abstract interpretation bypasses decidability issues by means of abstractions of the properties to be verified, model checking does so by resorting to less powerful, but decidable, models of computation. In fact, if we limit the program’s memory to be finite, the whole system can be modeled as a finite Transition System (TS), which admits verification algorithms for many interesting properties.

The general idea behind model checking is the following: we use an operational formalism to create a model of the system to be checked, we formulate its requirements in an appropriate mathematical formalism, and then we automatically check whether the model satisfies the requirements. If the requirements are not satisfied, then we get a counterexample, i.e. the description of a behavior of the system model that violates the requirements. Of course, we must employ a combination of formalisms for model and requirements that admits a (possibly efficient) model-checking algorithm. While systems are often modeled as finite TS’s, which are quite similar to Finite-State Automata (FSAs) and State Charts, the choice for representing requirements usually falls on temporal logics such as LTL [22], Computation Tree Logic (CTL), and CTL* [9]. These logics allow for naturally expressing constraints about the evolution of the system’s behavior over time, and admit relatively efficient model-checking algorithms.

The trade-off made with model checking to avoid undecidability brings in two main drawbacks [10]:

  1. 1.

    the computational complexity of model checking algorithms makes them intractable in theory-although not in practice-due to the state-space explosion problem;

  2. 2.

    the use of finite transition systems (or even FSA) limits the accuracy with which we can model certain systems, and the use of decidable logics (e.g., LTL or CTL) to express requirements limits the properties we can verify.

Most of the recent research on model checking addresses one or both such issues. My work, in particular, tries to address number 2.

1.2 Model-Checking Computer Programs

Requirements written in LTL say something about a program’s execution traces, seen as a linear timeline of discrete events. We can specify safety properties, which state that certain unwanted behaviors will never occur, with the globally operator: \(\mathop {\square }\varphi \) states that \(\varphi \) will always be true from the moment in which \(\mathop {\square }\varphi \) is evaluated. E.g., \(\mathop {\square }\textbf{exc}\) states that the program will never throw an exception. The eventually operator \(\mathop {\Diamond }\varphi \) states that \(\varphi \) will happen sometime in the future. Together with \(\mathop {\square }\), it can be used to express liveness requirements: \(\mathop {\square }\mathop {\Diamond }(\textbf{call}\wedge \textrm{p}_A)\) states that procedure \(\textrm{p}_A\) is called infinitely many times. The until operator \(\psi \mathbin {\mathcal {U}} \varphi \) is true at an instant i if there is a future time instant j where \(\varphi \) holds, and \(\psi \) holds in all instants between i and j. E.g., with \(\lnot \textrm{p}_A \mathbin {\mathcal {U}} \textrm{p}_B\) we say that procedure \(\textrm{p}_A\) cannot be called before \(\textrm{p}_B\) is executed.

While LTL can express many useful properties, the fact that it considers a linear timeline can be a limitation when reasoning about programs. E.g., suppose we want to say that procedure \(\textrm{p}_A\) always returns normally (i.e., it does terminate and not due to an exception). We could try with \(\mathop {\square }(\textbf{call}\wedge \textrm{p}_A \implies \mathop {\Diamond }(\textbf{ret}\wedge \textrm{p}_A))\), but this would be true in an execution trace such as \(\{\textbf{call}, \textrm{p}_A\}\{\textbf{call}, \textrm{p}_A\}\{\textbf{ret}, \textrm{p}_A\}\{\textbf{exc}\}\), where \(\textrm{p}_A\) calls itself recursively, but only the second instance terminates normally.

The reason why we cannot express this property in LTL is that it is equivalent to the FOL definable fragment of Regular Languages, while the behavior of procedures is context-free, as it is driven by a stack. Thus, a solution would be to devise a temporal logic that can express context-free properties. This was already attempted with logics based on nested words, such as CaRet [3] and Nested Words Temporal Logic (NWTL) [2]. These logics add a binary nesting relation connecting procedure calls with their returns, so that properties like the one above can be expressed.

Nested words, however, have some limitations. In particular, they cannot easily model exceptions, which are quite widespread in modern programming languages. The main contribution of my work fills this gap by presenting a temporal logic able to reason on procedural programs with exceptions. This logic, called POTL [7], is based on OPL, a subclass of deterministic context-free languages introduced by Floyd [13]. We studied POTL in terms of expressiveness and introduced and implemented a satisfiability and model checking procedure for it. The fact that we implemented a model checker for POTL is particularly relevant, considering that other context-free logics (e.g., CaRet and NWTL) had been previously studied only theoretically.

2 Background on Operator Precedence Languages

Here we give an intuitive overview of OPL that only requires basic familiarity with formal language theory; a more extensive treatment is given in [20] and in [5].

OPL have been inspired by precedence relations among operators in arithmetic expressions. They are generated by grammars in operator form, i.e. whose rules’ right-hand sides (rhs’s) have no consecutive non-terminals. Their parsers are guided in recognizing and reducing grammar rhs by three binary Precedence Relations (PRs) among terminal symbols. Given two terminals ab, for any non-terminals ABC and mixed terminal/non-terminal strings \(\alpha , \beta , \gamma \), we say a yields precedence to b (\(a \lessdot b\)) if there exists a rule \(A \rightarrow \alpha a C \beta \), s.t. a string \(B b \gamma \) or \(b \gamma \) derives from C in any number of passes; a is equal in precedence to b (\(a \mathbin {\doteq }b\)) if there exists a rule \(A \rightarrow \alpha a C b \beta \) or \(A \rightarrow \alpha a b \beta \); and a takes precedence over b (\(a \gtrdot b\)) if there is a rule \(A \rightarrow \alpha C b \beta \), s.t. \(\gamma a B\) or \(\gamma a\) derives from C. In practice, \(a \lessdot b\) if b is the beginning of a rhs; \(a \mathbin {\doteq }b\) if they belong to the same rhs; \(a \gtrdot b\) if a is the end of a rhs. If at most one PR holds between any terminal pair, once all PR are collected into an Operator Precedence Matrix (OPM), the Syntax Tree (ST) of any word on the same alphabet is fully determined. By convention, we delimit all words by \(\#\), s.t. \(\# \lessdot a\) and \(a \gtrdot \#\) for any terminal a.

In the following, we consider execution traces that only record when a function is called (\(\textbf{call}\)), returns (\(\textbf{ret}\)), installs an exception handler (\(\textbf{han}\)) or throws an exception (\(\textbf{exc}\)). We use the example trace

$$\begin{aligned} w_ ex = \textbf{call}\, \textbf{han}\, \textbf{call}\, \textbf{call}\, \textbf{call}\, \textbf{exc}\, \textbf{call}\, \textbf{ret}\, \textbf{ret}\end{aligned}$$

resulting from a program where a procedure is called (\(\textbf{call}\)) and installs a handler (\(\textbf{han}\)). Then, three procedures are called recursively (\(\textbf{call}\, \textbf{call}\, \textbf{call}\)), and the last one throws an exception (\(\textbf{exc}\)). Finally, another function is called that terminates immediately (\(\textbf{call}\, \textbf{ret}\)), and the procedure called at the beginning also returns.

Figure 1a shows OPM \(M_\textbf{call}\), which we use to extract the context-free structure from execution traces, while Fig. 1c shows the steps of the parsing algorithm guided by \(M_\textbf{call}\) on the example word \(w_ ex \). To illustrate it, we refer to rows of Fig. 1c. First, we add the delimiter \(\#\) at \(w_ ex \)’s boundaries and write all PR between consecutive characters: the result is row 0. Then, we select all innermost patterns of the form \(a \lessdot c_1 \mathbin {\doteq }\dots \mathbin {\doteq }c_\ell \gtrdot b\). In row 0, the only such pattern is the underscored\(\textbf{call}\) enclosed within the pair (\(\mathord {\lessdot }, \mathord {\gtrdot }\)). This means that the ST we are going to build, if it exists, must contain an internal node with the terminal character \(\textbf{call}\) as its only child. We mark this fact by replacing the pattern \(\lessdot \underline{\textbf{call}} \gtrdot \) with a dummy non-terminal character, say N-i.e., we reduce \(\underline{\textbf{call}}\) to N. The result is row 1. Next, we apply the same labeling to row 1 by simply ignoring symbol N and find a new candidate for reduction, the pattern \(\lessdot \underline{\textbf{call}} \ N \gtrdot \). The reduction of row 2 is similar, so we come to row 3. This time the terminals to be reduced are two, with an \(\mathbin {\doteq }\) and an N in between. This means that they embrace a subtree of the ST whose root is the node represented by N. By executing the reduction leading from row 3 to 4 we produce a new N immediately to the left of a \(\textbf{call}\) which is matched by an equal-in-precedence \(\textbf{ret}\). We repeat the procedure until we obtain row 6, where by convention we state the \(\mathbin {\doteq }\) relation between the two \(\#\)’s. The resulting ST is shown in Fig. 1b, with PR highlighted.

Fig. 1
figure 1

Demonstration of the Operator-Precedence parsing algorithm

The way PR determine the ST of a string is formalized by chains:

Definition 1

A simple chain \( {}^{c_0}[ c_1 c_2 \dots c_\ell ]{}^{c_{\ell +1}} \) is a string \(c_0 c_1 c_2 \dots c_\ell c_{\ell +1}\), such that: \(c_0, c_{\ell +1} \in \Sigma \cup \{\#\}\), \(c_i \in \Sigma \) for every \(i = 1,2, \dots \ell \) (\(\ell \ge 1\)), and \(c_0 \lessdot c_1 \mathbin {\doteq }c_2 \dots c_{\ell -1} \mathbin {\doteq }c_\ell \gtrdot c_{\ell +1}\).

A composed chain is a string \(c_0 s_0 c_1 s_1 c_2 \dots c_\ell s_\ell c_{\ell +1}\), where \({}^{c_0}[ c_1 c_2 \dots c_\ell ]{}^{c_{\ell +1}}\) is a simple chain, and \(s_i \in \Sigma ^*\) is either the empty string or is such that \({}^{c_i}[ s_i ]{}^{c_{i+1}}\) is a chain (simple or composed), for every \(i = 0,1, \dots , \ell \) (\(\ell \ge 1\)). Such a composed chain will be written as \({}^{c_0}[ s_0 c_1 s_1 c_2 \dots c_\ell s_\ell ]{}^{c_{\ell +1}}\).

In a chain, simple or composed, \(c_0\) (resp. \(c_{\ell +1}\)) is called its left (resp. right) context; all terminals between them are called its body.

If we delimit chain bodies in \(w_ ex \) by square brackets, we obtain the following:

$$\begin{aligned} \# [ \textbf{call}[ [ \textbf{han}[ \textbf{call}[ \textbf{call}[ \textbf{call}] ] ] \textbf{exc}] \textbf{call}\; \textbf{ret}] \textbf{ret}] \# \end{aligned}$$

Some simple chains in \(w_ ex \) are \({}^{\textbf{call}}[ \textbf{call} ]{}^{\textbf{exc}}\) and \({}^{\textbf{call}}[ \textbf{han}\, \textbf{exc} ]{}^{\textbf{call}}\); some composed chains are \({}^{\textbf{call}}[ \textbf{call}[ \textbf{call}] ]{}^{\textbf{exc}}\) and \({}^{\textbf{call}}[ [ \textbf{han}[ \textbf{call}[ \textbf{call}[ \textbf{call}] ] ] \textbf{exc}] \textbf{call}\; \textbf{ret} ]{}^{\textbf{call}}\).

In the ST, each chain body corresponds to a sub-tree. Thus, the structure of chains in a given string is isomorphic to its ST.

OPL also have a defining class of pushdown automata, Operator Precedence Automaton(OPA) [19]. We use them for model checking POTL, but we omit their definition for lack of space.

3 OP-Words and POTL

Here we present the temporal logic POTL. Before, however, we need to define Operator Precedence (OP) words, the algebraic structure we use for modeling execution traces.

3.1 Operator-Precedence Words

POTL is a linear-time temporal logic, which extends LTL: its algebraic structure is an extension of LTL’s. The semantics of LTL [22] is defined on a set of word positions,Footnote 1 respectively \(U = \{0, 1, \dots , n\}\), with \(n \in \mathbb {N}\) for the finite-word semantics, and \(U = \mathbb {N}\) for the infinite-word one, equipped with a total ordering < and monadic relations, called atomic propositions (AP).

OP words augment this linear order with an additional binary relation, which we call the chain relation and denote as \(\chi \). The \(\chi \) relation is isomorphic with the context-free structure of a word, and we use it to encode the structure we want to embed in our execution traces. When modelling procedural programs, for example, the \(\chi \) relation models the program’s stack: function calls are linked to their respective returns or to exceptions that terminate them. More formally,

Definition 2

(OP word) An OP word on a finite set of atomic propositions AP is the tuple \(\langle U, <, M_{AP}, P \rangle \), where U and < are as above; \(M_{AP}\) is an OPM on \({\mathcal {P}(AP)}\), and \(P :AP \rightarrow {\mathcal {P}(U)}\) is a function associating each atomic proposition with the set of positions where it holds, with \(0, (n+1) \in P(\#)\).

Here we consider only finite-word languages, while the extensions needed to deal with \(\omega \)-languages are covered in [5].

Definition 3

(Chain relation) The chain relation \(\chi (i, j)\) holds between two positions \(i,j \in U\) if and only if \(i < j-1\), and i and j are respectively the left and right contexts of the same chain according to \(M_{AP}\) and the labeling induced by P.

In the following, given two positions ij and a PR \(\pi \), we write \(i \mathrel {\pi }j\) to say \(a \mathrel {\pi }b\), where \(a = \{\textrm{p} \mid i \in P(\textrm{p})\}\), and \(b = \{\textrm{p} \mid j \in P(\textrm{p})\}\). For notational convenience, we partition AP into structural labels, written in bold face, which define a word’s structure, and normal labels, in round face, defining predicates holding in a position. Thus, an OPM M can be defined on structural labels only, and \(M_{AP}\) is obtained by inverse homomorphism of M on subsets of AP containing exactly one of them.

Fig. 2
figure 2

\(w_ ex \) as an OP word. Chains are highlighted by arrows joining their contexts; structural labels are in bold, and other atomic propositions are shown below them. \(\textrm{p}_l\) means a \(\textbf{call}\) or a \(\textbf{ret}\) is related to procedure \(\textrm{p}_l\). First, procedure \(\textrm{p}_A\) is called (pos. 1), and it installs an exception handler in pos. 2. Then, three nested procedures are called, and the innermost one (\(\textrm{p}_C\)) throws an exception, which is caught by the handler. Function \(\textrm{p}_{ Err }\) is called and, finally, \(\textrm{p}_A\) returns

Figure 2 shows \(w_ ex \) as an OP word, with additional propositions denoting function names. The \(\chi \) relation is represented by edges. Notice how it describes the structure of the execution trace: each call is connected to the event that terminates it (either a \(\textbf{ret}\) or an \(\textbf{exc}\)), and to \(\textbf{call}\)s to nested functions (e.g., \(\chi (1,7)\)).

3.2 Precedence-Oriented Temporal Logic

Here we describe the most important operators in POTL [7], leaving the remaining ones-namely, hierarchical operators-for [5]. Given a finite set of atomic propositions AP, the syntax of POTL follows:

$$\begin{aligned} \varphi \,\,= \textrm{a} \mid \lnot \varphi \mid \varphi \vee \varphi \mid \mathop {\bigcirc ^{t}} \varphi \mid \mathop {\circleddash ^{t}} \varphi \mid \mathop {\chi _F^{t}} \varphi \mid \mathop {\chi _P^{t}} \varphi \mid \varphi \mathbin {\mathcal {U}^{t}_{\chi }} \varphi \mid \varphi \mathbin {\mathcal {S}^{t}_{\chi }} \varphi \end{aligned}$$

where \(\textrm{a} \in AP\), and \(t \in \{d, u\}\).

The truth of POTL formulas is defined w.r.t. a single word position. Let w be an OP word, and \(\textrm{a} \in AP\). Then, for any position \(i \in U\) of w, we have \((w, i) \models \textrm{a}\) if \(\textrm{a} \in P(i)\). Propositional operators \(\wedge \), \(\vee \) and \(\lnot \) have the usual semantics. Next, while giving the formal semantics of POTL operators, we illustrate it by showing how it can be used to express properties on program execution traces, such as Fig. 2.

Next/back operators. The downward next and back operators \(\mathop {\bigcirc ^{d}}\) and \(\mathop {\circleddash ^{d}}\) are like their LTL counterparts, except they are true only if the next (resp. current) position is at a lower or equal ST level than the current (resp. preceding) one. The upward next and back, \(\mathop {\bigcirc ^{u}}\) and \(\mathop {\circleddash ^{u}}\), are symmetric.

Formally, \((w,i) \models \mathop {\bigcirc ^{d}}\varphi \) if and only if \((w,i+1) \models \varphi \) and \(i \lessdot (i+1)\) or \(i \mathbin {\doteq }(i+1)\), and \((w,i) \models \mathop {\circleddash ^{d}}\varphi \) if and only if \((w,i-1) \models \varphi \), and \((i-1) \lessdot i\) or \((i-1) \mathbin {\doteq }i\). Substitute \(\lessdot \) with \(\gtrdot \) to obtain the semantics for \(\mathop {\bigcirc ^{u}}\) and \(\mathop {\circleddash ^{u}}\).

For instance, \(\mathop {\bigcirc ^{d}}\textbf{call}\) means that the next position is an inner call (it holds in pos. 2, 3, 4 of Fig. 2), \(\mathop {\circleddash ^{d}}\textbf{call}\) to say that the previous position is a \(\textbf{call}\), and the current is the first of the body of a function (pos. 2, 4, 5), or the \(\textbf{ret}\) of an empty one (pos. 8).

The chain next and back operators \(\mathop {\chi _F^{t}}\) and \(\mathop {\chi _P^{t}}\) evaluate their operand respectively on future and past positions in the chain relation with the current one. The downward (resp. upward) variant only considers chains whose right context goes down (resp. up) in the ST.

Formally, \((w,i) \models \mathop {\chi _F^{d}}\varphi \) if and only if there exists a position \(j > i\) such that \(\chi (i,j)\), \(i \lessdot j\) or \(i \mathbin {\doteq }j\), and \((w,j) \models \varphi \). \((w,i) \models \mathop {\chi _P^{d}}\varphi \) if and only if there exists a position \(j < i\) such that \(\chi (j,i)\), \(j \lessdot i\) or \(j \mathbin {\doteq }i\), and \((w,j) \models \varphi \). Replace \(\lessdot \) with \(\gtrdot \) for the upward versions.

E.g., in pos. 1 of Fig. 2, \(\mathop {\chi _F^{d}}\textrm{p}_{ Err }\) holds because \(\chi (1,7)\), meaning that \(\textrm{p}_A\) calls \(\textrm{p}_{ Err }\) at least once. Also, \(\mathop {\chi _F^{u}}\textbf{exc}\) is true in \(\textbf{call}\) positions whose procedure is terminated by an exception thrown by an inner procedure (e.g. pos. 3 and 4). \(\mathop {\chi _P^{u}}\textbf{call}\) is true in \(\textbf{exc}\) statements that terminate at least one procedure other than the one raising it, such as the one in pos. 6. \(\mathop {\chi _F^{d}}\textbf{ret}\) and \(\mathop {\chi _F^{u}}\textbf{ret}\) hold in \(\textbf{call}\)s to non-empty procedures that terminate normally, and not due to an uncaught exception (e.g., pos. 1).

Until/Since operators. The summary until \(\psi \mathbin {\mathcal {U}^{t}_{\chi }} \theta \) (resp. since \(\psi \mathbin {\mathcal {S}^{t}_{\chi }} \theta \)) operator is obtained by inductively applying the \(\mathop {\bigcirc }^t\) and \(\mathop {\chi _F^{t}}\) (resp. \(\mathop {\circleddash }^t\) and \(\mathop {\chi _P^{t}}\)) operators. It holds in a position if either \(\theta \) holds, or \(\psi \) holds with \(\mathop {\bigcirc }^t (\psi \mathbin {\mathcal {U}^{t}_{\chi }} \theta )\) (resp. \(\mathop {\circleddash }^t (\psi \mathbin {\mathcal {S}^{t}_{\chi }} \theta )\)) or \(\mathop {\chi _F^{t}} (\psi \mathbin {\mathcal {U}^{t}_{\chi }} \theta )\) (resp. \(\mathop {\chi _P^{t}} (\psi \mathbin {\mathcal {S}^{t}_{\chi }} \theta )\)). It is an until operator on paths that move not only between consecutive positions, but also between contexts of a chain, skipping its body. With \(M_\textbf{call}\), this means skipping function bodies. The downward variants move between positions at the same level in the ST (i.e., in the same simple chain body), or down in the nested chain structure. The upward ones move at the same or to higher ST levels.

For example, formula \({\top } \mathbin {\mathcal {U}_\chi ^u} {\textbf{exc}}\) is true in positions contained in the frame of a function that is terminated by an exception. It is true in pos. 3 of Fig. 2 because of path 3-6, and false in pos. 1, because no path can enter chain \(\chi (1,9)\). Formula \({\top } \mathbin {\mathcal {U}_\chi ^d} {\textbf{exc}}\) is true in call positions whose function frame contains\(\textbf{exc}\)s, but that are not directly terminated by one of them, such as the one in pos. 1 (with path 1-2-6). Moreover, \({\textbf{call}} \mathbin {\mathcal {U}_\chi ^d} {(\textbf{ret}\wedge \textrm{p}_{ Err })}\) holds in pos. 1 because of path 1-7-8, \({(\textbf{call}\vee \textbf{exc})} \mathbin {\mathcal {S}_\chi ^u} {\textrm{p}_B}\) in pos. 7 because of path 3-6-7, and \({(\textbf{call}\vee \textbf{exc})} \mathbin {\mathcal {U}_\chi ^u} {\textbf{ret}}\) in 3 because of path 3-6-7-8.

3.3 Theoretical Results

We proved that POTL is equivalent to FOL on OP words, which is a strong guarantee for its expressive power, as state-of-the-art temporal logics, namely LTL [18] and NWTL [2], are equivalent to FOL on their respective word structures.

Theorem 1

([5, Theorems 9.9 and 9.21] and [8]) POTL = FOL with one free variable on finite and infinite OP words.

Moreover, we developed a procedure for building an automaton (precisely, an OPA) that accepts exactly the execution traces that satisfy a given POTL formula. Thanks to OPA forming a Boolean algebra, we derived a satisifiability and model checking procedure, which allows us to state

Theorem 2

([5, Theorems 10.8, 10.13]) POTL model checking and satisfiability are EXPTIME-complete.

This places POTL at the same level of computational complexity as less expressive logics such as NWTL [2].

4 Verifying Programs with Exceptions

We show how we can use POTL to verify programs with exceptions through a case study. The properties we are interested in are related to exception safety, which has been introduced to help developers deal with pitfalls caused by exceptions in C++ [1].

Let \(\mathop {\square }\psi \) be the LTL globally operator, which states the truth of its operand in all future word positions. POTL can express Hoare-style pre/post-conditions with formulas such as \(\mathop {\square }(\textbf{call}\wedge \rho \implies \mathop {\chi _F^{d}}(\textbf{ret}\wedge \theta ))\), where \(\rho \) is the pre-condition, and \(\theta \) is the post-condition. We can lift this formula to exceptions with the following shortcut:

$$\begin{aligned} CallThr (\psi ):= \mathop {\bigcirc ^{u}}(\textbf{exc}\wedge \psi ) \vee \mathop {\chi _F^{u}}(\textbf{exc}\wedge \psi ),\end{aligned}$$

which, evaluated in a \(\textbf{call}\), states that the procedure currently started is terminated by an \(\textbf{exc}\) in which \(\psi \) holds. So, \(\mathop {\square }(\textbf{call}\wedge \rho \wedge CallThr (\top ) \implies CallThr (\theta ))\) means that if precondition \(\rho \) holds when a procedure is called, then postcondition \(\theta \) must hold if that procedure is terminated by an exception. In object-oriented programming languages, if \(\rho \equiv \theta \) is a class invariant asserting that a class instance’s state is valid, this formula expresses weak (or basic) exception safety, and strong exception safety if \(\rho \) and \(\theta \) express particular states of the class instance. The no-throw guarantee can be stated with \(\mathop {\square }(\textbf{call}\wedge \textrm{p}_A \implies \lnot CallThr (\top ))\), meaning procedure \(\textrm{p}_A\) is never interrupted by an exception.

We take our case study from a famous tutorial [24] on how to make exception-safe generic containers in C++. It presents two implementations of a generic stack data structure, parametric on the element type T. The first one is not exception-safe: if the constructor of T throws an exception during a pop action, the topmost element is removed, but it is not returned, and it is lost. This violates the strong exception safety requirement that each operation is rolled back if an exception is thrown, as the stack is effectively popped even though the pop action failed due to the exception. The second version of the data structure instead satisfies such requirement.

While exception safety is, in general, undecidable, we can prove the stronger requirement that each modification to the data structure is only committed once no more exceptions can be thrown. We can check such requirement with the following formula:

$$\begin{aligned}&\mathop {\square }(\textbf{exc}\implies \nonumber \\&\,\, \lnot ((\mathop {\circleddash ^{u}}\texttt{modified} \vee \mathop {\chi _P^{u}}\texttt{modified}) \wedge \mathop {\chi _P^{u}}(\mathtt {Stack::push} \vee \mathtt {Stack::pop}))) \end{aligned}$$
(1)

Additionally, we can prove that both implementations are exception neutral, i.e. Stack methods do not block exceptions thrown by the underlying element type T’s methods and constructors. This can be done by checking the following formula:

$$\begin{aligned} \mathop {\square }(\textbf{exc}\wedge \mathop {\circleddash ^{u}}\texttt{T} \wedge \mathop {\chi _P^{d}}(\textbf{han}\wedge \mathop {\chi _P^{d}}\texttt{Stack}) \implies \mathop {\chi _P^{d}}\mathop {\chi _P^{d}}\mathop {\chi _F^{u}}\textbf{exc}). \end{aligned}$$
(2)

We implemented an explicit-state model checker for POTL, called POMC [6], which accepts in input programs written in MiniProc, a simple procedural programming language with exceptions, compiles them to an OPA, and checks them against POTL formulas. To check the two Stack implementations against the above requirements, we modelled them in MiniProc, and checked them against (1) and (2). POMC successfully found a counterexample to (1) for the first implementation in 3 s, and proved the safety of the second one in 12 s. Exception-neutrality was proved for both implementations by checking (2) in resp. 13 and 18 s.

More case studies can be found in [5], including a more complex one on a Java implementation of the QuickSort algorithm [23].

5 Conclusions and Future Work

We have presented POTL, a temporal logic based on OPL, proved its expressive completeness, and implemented a model checker for it. Its evaluation on several case studies shows promising results for its applicability to model-checking of procedural programs.

One natural further step could be the investigation of more efficient model checking algorithms, based on symbolic or bounded model-checking techniques [10]. Moreover, POTL model checking could be applied to programs written in real-world programming languages by means of predicate abstraction mechanisms [17].