figure a

1 Constrained Horn Clause Verification and Our Approach

A constrained Horn clause (CHC) is a first order predicate logic formula usually written in the form \(p(X) \leftarrow \phi , p_1(X_1), \dots , p_k(X_k)\) (\(k \ge 0\)) using Constraint Logic Programming (CLP) syntax, where \(\phi \) is a first order logic formula (constraint) with respect to some background theory, \(X_i, X\) are (possibly empty) tuples of distinct variables, and \(p_1,\ldots ,p_k, p\) are predicate symbols. There is a distinguished predicate symbol \( false \) which is interpreted as \(\textsc {False}\). Clauses with \( false \) head are called integrity constraints. A set of CHCs is called a (CLP) program.

An interpretation of a set of CHCs P is a set of constrained facts of the form \(A \leftarrow \phi \) where A is an atom and \( \phi \) is a formula with respect to some background theory. An interpretation that satisfies each clause is called a model (a solution in some works [6, 34]). In Horn clause verification, integrity constraints represent the safety properties to be verified; other clauses represent the program’s behaviours. The CHC verification problem is to check whether there exists a model of P.

Several verification tools have been developed for CHCs, including SeaHorn [24], QARMC [21], VeriMap [16], Convex polyhedral analyser [31], TRACER [29], ELDARICA [27], and Trace abstraction refinement tool [37]. They exploit either Formulation I or Formulation II for Horn clause verification.

Formulation I (deductive): P has a model if and only if (false is not derivable from P). In CLP terminology, \(P \vdash A\) if and only if the query \(\leftarrow A\) succeeds in P. In this formulation it is sufficient to show that the query \(\leftarrow false \) fails finitely or infinitely. Formulation I forms the basis of the tools described in [25, 37]. As the minimal model of P contains exactly the set of atoms that succeed [28], we have another formulation of the CHC verification problem [20].

Formulation II (model-based): P has a model if and only if \( false \not \in M[\![P ]\!]\), where \(M[\![P ]\!]\) is the minimal model of P. In Formulation II it is sufficient to find a model \(M' \supseteq M[\![P ]\!]\), where \( false \not \in M'\). It forms the basis of tools based on abstract interpretation, interpolation or predicate abstraction [21, 24, 31].

The program in Fig. 1(a) is a simple but challenging problem for many verification tools. \(\mathtt {l(X,Y) \equiv X \ge Y \wedge Y \ge 0}\) is a model of the program, whose solution requires the discovery of the invariants \(\mathtt {X \ge Y}\) and \(\mathtt {Y \ge 0}\). For example neither QARMC [21] nor SeaHorn [24] (using only the PDR engine [7]) terminates on this program. However, SeaHorn (with PDR and the abstract interpreter IKOS [8]) solves it. Rahft solves it with the pre-processing step alone.

Rahft exploits both of the above formulations using techniques based on abstract interpretation over the domain of convex polyhedra, trace abstraction-refinement using finite tree automata (FTAs) and program specialisation using constraint specialisation [30]. The motivations behind this combination are: (i) to benefit from a powerful and scalable technique such as abstract interpretation [13] for verifying properties of programs, (ii) to refine abstract interpretation through automata theoretic operations which offers the advantages of simplicity and generality [31] and (iii) to construct highly parametric and configurable verification tools through program transformation [16].

2 Rahft Architecture and Interface

Figure 1(b) gives an overview of Rahft. It compiles to a standalone command line utility that accepts a set of CHCs as input. It consists of two modules namely, Abstraction (green box) and Refinement (red box). Rahft takes a file containing a set of CHCs P as input and returns safe or unsafe respectively if P has or does not have a model.

Fig. 1.
figure 1

(a) Example program; (b) the architecture of Rahft. (Color figure online)

2.1 Abstraction

The Abstraction module takes a set of CHCs P as input and returns safe, unsafe or a trace representing the abstract derivation of \( false \) together with the set of all derivations (traces) (both represented as FTAs) used while applying abstraction interpretation to P. It consists of the following components:

Pre-processor (PP): Pre-processing is a model-preserving source-to-source program transformation of Horn clauses. In principle, any such transformation can be used as a pre-processor, but we use constraint specialisation [30]. The specialisation consists of strengthening the constraints in the clauses using abstract interpretation [13] and query-answer transformation [3, 17] of the original program. The specialisation is independent of the abstract domain and the background theory underlying the clauses and does not unfold the clauses at all. This has been proven to be an effective transformation [30] for verifying Horn clauses [15] and as a pre-processor to other Horn clause verification tools such as [21].

Abstract Interpreter (AI): The AI implements a fixed point algorithm over the domain of convex polyhedra [12] based on abstract interpretation [13]. It constructs an over-approximation M of the minimal model of a program P, where M contains at most one constrained fact \(p(X) \leftarrow \phi \) for each predicate p. The constraint \(\phi \) is a conjunction of linear inequalities, representing a convex polyhedron. The set of traces used during abstract interpretation of P can be captured by an FTA, say \(\mathcal {A}_P\), using M as shown in [32]. An FTA is a mathematical model capable of capturing tree structured computations (Horn clauses derivations) (see [31] for the correspondence between a program and an FTA).

The approximation M and the pre-processed clauses can be used by other Horn clause tools, for example [21]. These tools can strengthen M (which may contain some useful invariants) incrementally to construct a model of P rather than starting from a coarse abstraction (\(p(X) \leftarrow true \) for each predicate p of P).

Verifier: The verifier receives M and \(\mathcal {A}_P\) and checks the safety of the clauses based on some simple condition. The clauses are safe if there is no constrained fact for \(\textit{false}\) in M (M is called safe inductive invariant or a model of P) or there are no error traces rooted at \( false \). Otherwise we do not know whether the clauses are unsafe or whether the approximation was too imprecise. In this case, the verifier picks a trace, say \(t \in \mathcal {A}_P\), representing the abstract derivation of \( false \) (if any) from the set of traces. If t is feasible (while simulating in P), then P is unsafe and t is a counterexample, otherwise we refine P.

2.2 Refinement

The Refinement module takes as input a program P and two FTAs (i) recognising the set of all possible traces of P; and (ii) recognising a set of infeasible traces. A difference automaton is computed from these automata which recognises all traces except the infeasible ones. A refined program is obtained as output using the difference automaton and P. Rather than eliminating a single infeasible trace in each refinement iteration, we generalise it using an interpolant automaton [25, 32, 37] thereby eliminating a possibly infinite number of infeasible traces. The refinement offers the advantages of simplicity and generality which is independent of the abstract domain and background theory underlying the clauses. The Refinement module consists of following components:

Finite tree automata manipulator (FTAM): FTAM takes as input two FTAs and outputs their difference automaton. The FTA difference construction needs determinisation; we built upon an optimised determinisation algorithm by Gallagher, Ajspur and Kafle [19] which scales well in practice, generating transitions of the determinised automaton in a very compact form called product form.

Clause generator (CG): Given a set of clauses P, and an automaton recognising an over-approximation of all feasible traces of P, CG produces a set of clauses which is equisatisfiable to P. For this purpose, we exploit a correspondence between the traces using the clauses and the language of FTAs to generate a new set of clauses.

The refinement offers two advantages: (i) the refinement is manifested in the clauses generated – we do not need to keep track of the previous refinements; and (ii) the original predicates get split in refined clauses which help improve the precision of analysis [20].

2.3 Implementation

Rahft is implemented in Ciao [26] and is available from https://github.com/bishoksan/RAHFT. It consists of a collection of reusable Prolog modules which rely on state-of-the-art specialised external libraries written in C and C++ for handling constraints. We use the Yices SMT solver [18] and the Parma Polyhedra Library [2] for handling the constraints and the FTA library [19] for manipulating FTAs. The construction of an interpolant tree automaton uses an algorithm presented in [36] for computing an interpolant of two formulas. The code consists of over 7,000 lines of Ciao Prolog code split over 42 modules, interfaced to the above-mentioned external libraries. The implementation of iterative fixpoint algorithms is inspired by the approach to the abstract interpretation of logic programs described by Codish and Søndergaard [10]. Data structures for manipulating Horn clauses are based on terms and the internal Prolog database, reusing the optimizations of the underlying machine (e.g., clause indexing) rather than reimplementing them in our tool. The glue code that ties together the general purpose Prolog engine and the specialised solvers written in C and C++ is generated via the Ciao foreign interface [26].

2.4 Strength and Weakness

Rahft is a verification tool for safety properties of programs expressed as Horn clauses; it can be used as a back end solver by different front end tools outputting in CLP form. It handles clauses whose underlying theory is linear arithmetic; other theories are not supported currently. It accepts input in CLP form.

Since different components of Rahft are loosely coupled, the tool can be reconfigured (with a very little effort) to produce verification tools solely based on (i) program transformation as in iterated specialisation approach [15] by iterating the pre-processing component, (ii) abstract interpretation, only with the AI component, (iii) trace abstraction refinement [25, 37] by iterating the FTAM component, and (iv) a sensible combination thereof – all followed by a lightweight verifier which checks the safety of the clauses based on some condition. Since our tool uses both state abstraction and trace abstraction, it allows application of a wide range of tools and techniques.

We have evaluated Rahft on software verification benchmarks from a variety of sources [4, 5, 22, 23, 27, 29] and the results show that it compares favourably (in time and the number of instances solved) with the other state-of-the-art Horn clause verification tools (see [3032] for the details).

Convex polyhedra is an expensive abstract domain and is a potential bottleneck for verification of large code bases. Instead, we can use cheaper domains supported by the Parma Polyhedra Library such as octagons or intervals at the cost of precision. Rahft is also limited by the hard-coded limits of the libraries and the Prolog implementation used (e.g. arity limit of the predicates), which may be too restrictive for some verification problems and we intend to improve this by some suitable data representation. We are aware of some examples from SV-COMP if not many which cross this limit.

We can leverage state-of-the-art interpolating SMT solvers [9, 33] for the tree interpolant generation which can be used for constructing an interpolant tree automaton; our current implementation does not scale well. Furthermore we aim to handle more advanced data structures such as arrays, maps and sets, requiring more expressive theories than linear arithmetic. One way to achieve this is by composing abstract domains as described in [11, 14]; we are also aware of the support for the reduced product of domains in the PPL library.

Rahft is able to generate a model (a counterexample) if it proves the safety (unsafety) a program. We need bookkeeping to generate these witnesses with respect to the original program; and sometimes it becomes rather challenging because of the use of external libraries, tools or the transformations applied.

3 Future Work

Future work will involve making Rahft a more flexible tool so that the user can configure other parameters such as abstract domains and pre-processors. We are also planning for a detailed performance measurement of the tool to detect bottlenecks; and work on language-based optimisations to minimize them. Generation of a model or a counterexample with respect to the original program, handling clauses with richer background theories (arrays, uninterpreted functions) is on our to-do list. In addition, we are extending Rahft to consider Horn clauses in SMT-LIB format [1], though several Horn clause verification tools use standard CLP notation [16, 21, 31].