Keywords

figure a

1 Introduction

SMT solversĀ [6, 21] are used in a multitude of applications, e.g., in formal software analysis, where automated test-case generation [7, 16, 29, 30], SMT-based algorithms for software verificationĀ [10, 34], and interactive theorem provingĀ [27, 44] are used. Applications and users rely on efficiency and expressiveness (supported SMT theories) to compute reasonable results in time. For application developers, the usability and API of the solver are also important aspects, and some features needed in applications, such as interpolation or optimization, are not available in some solvers.

Using the solverā€™s own API directly makes it difficult to switch to another solver without rewriting extensive parts of the application, as there is no standardized binary API for SMT solvers. The SMT-LIB2Ā standardĀ [4] improves this issue by defining a common language to interact with SMT solvers. However, this communication channel does not define a solver interface for special features like optimization or interpolation.Footnote 1 Additionally, the application has to parse the data provided by the SMT solver on its own, and this of course slightly changes from solver to solver.

JavaSMTĀ [37] provides a common API layer across multiple back-end solvers to address these problems. Our Java-based approach creates only minimal overhead, while giving access to most solver features. JavaSMTĀ is available under the Apache 2.0 License on GitHub.Footnote 2

Contribution. Our contribution consists of three parts:

  • We integrated more SMT solvers into the API framework JavaSMT (new: BoolectorĀ [43], CVC4Ā [5], and Yices2Ā [25]).

  • We simplified the steps to get started using JavaSMT, by including support for more operating systems (new: MacOS and Windows) and more build techniques (new: AntĀ and Maven).

  • We evaluated the performance of several algorithms for software verification to show that different SMT solvers have different strengths.

Outline. This paper first provides a brief overview of JavaSMTĀ in Sect. 2, explaining the inner structure and features. Sect. 3 discusses the development since the previous publicationĀ [37]: more integrated SMT solvers and extended support for operating systems and build processes. Sect. 4 describes a case study, based on SMT-based algorithmsĀ [10] in a common verification framework.

Related Work. SMT-LIB2Ā [4] is the established standard format for exchanging SMT queries. It provides simple usage, is easy to debug, and widely known in the community. However, it requires extra effort to parse and transform formulas in the user application. Features like optimization, interpolation, and receiving nested parts of formulas are not defined by the standard, such that some SMT solvers provide their own individual solution for that. Alternatively, several SMT solvers already come with their special bindings for some programming languages. Most SMT solvers are written in C/C++, so interacting with them in these low-level languages is the easiest way. However, the support for higher-level languages is sparse. The most prominent language binding for several SMT solvers is Python, as it directly allows the access to C code and avoids automated memory management operations like asynchronous garbage collection. Bindings for Java are available for some SMT solvers, such as MathSAT5Ā and Z3, but missing, unsupported, or unmaintained for others, such as BoolectorĀ and CVC4.

In the following, we discuss libraries, similar to JavaSMT, that provide access to several underlying SMT solvers via a common user interface in different popular languages, and their binding mechanism, i.e., whether the solver interaction is based on a native interface or text-based on SMT-LIB2. With SMT-LIB2, an arbitrary SMT solver can be queried, but the interaction happens through communicating processes and the solver is mostly limited to features defined in the standard. Accessing a native interface directly allows to support more features of the underlying solver, e.g., using callbacks, simplifying formulas, or eliminating quantifiers.

Table 1 provides an overview of the libraries for interacting with SMT solvers. We enumerate several special features that are not available in some libraries, such as unsat cores, interpolation, or optimization queries. Those features depend on the support by the underlying SMT solver, but can be provided in general by an API on top of them. Most libraries use their own formula representation and not just wrap the objects provided by the SMT solver. This potentially allows for easier formula decomposition and inspection, e.g., by using the visitor pattern. JavaSMTĀ directly provides formula decomposition if available in the SMT solver. The provided numbers of forks and stars of the project repositories on GitHub or Bitbucket can be seen as a measurement of popularity.

Table 1. Comparison of different interface libraries for SMT solvers

PySMTĀ [28] is a Python-based project and aims at rapid prototyping of algorithms using the native API of the installed SMT solvers. It has the ability to perform formula manipulation without a back-end SMT solver and additionally supports the conversion of boolean formulas to plain SAT problems and then apply a SAT solver or a BDD library. This approach comes with the drawback of a noticeable memory overhead and performance of an interpreted language. metaSMTĀ [45], SMT Kit, and Smt-SwitchĀ [38] provide solver-agnostic APIs for interacting with various SMT solvers in C/C++ to focus on the application instead of the solver integration. jSMTLIBĀ [20], Scala SMT-LIB, and ScalaSMTĀ [17] are solver-independent libraries written in Java or Scala and interact via SMT-LIB2Ā with SMT solvers. Scala SMT-LIB and ScalaSMT allow to use an additional domain-specific language to interact with SMT solvers and rewrite Scala syntax into valid SMT-LIB2Ā and back. Both partially extend the SMT-LIB2 standard, e.g., by offering the ability to overload operators or receive interpolants. SBV and what4 are generic Haskell libraries based on process interaction via SMT-LIB2Ā and support several SAT and SMT solvers. rsmt2 offers a generic Rust library that currently supports three SMT solvers.

2 JavaSMTā€™s Architecture and Solver Integration

In the following, we describe the architecture of JavaSMTĀ and its main concepts. Afterwards, we give an overview of the integrated SMT solvers and their features. The architecture did not significantly change, but we added a few new SMT solvers, as shown in Fig. 1.

Architecture. JavaSMTĀ provides a common API for various SMT solvers. The architecture, shown in Fig. 1, consists of several components: As common context, we use a SolverContext that loads the underlying SMT solver and defines the scope and lifetime of all created objects. As long as the context is available, we track memory regions of native SMT-solver libraries. When the context is closed, the corresponding memory is freed and garbage collection wipes all unused objects. Within a given context, JavaSMTĀ provides FormulaManagers for creating formulas in various theories and ProverEnvironments for solving SMT queries.

A FormulaManager allows to create symbols and formulas in the corresponding theories and provides a type-safe way to combine symbols and formulas in order to encode a more complex SMT query. We support the structural analysis (like splitting a formula into its components or counting all function applications in a formula) and transformations (like substituting symbols or applying equisatisfiable simplifications) of formulas.

Each ProverEnvironment represents a solver stack and allows to push/pop boolean formulas and check them for satisfiability (the hard part). This follows the idea of incremental solving (if the underlying SMT solver supports it). After a satisfiability check, the ProverEnvironment provides methods to receive a model, interpolants, or an unsatisfiable core for the given formula.

JavaSMTĀ guarantees that formulas built with a single FormulaManager can be used in several ProverEnvironments, e.g., the same formula can be pushed onto and solved within several distinct ProverEnvironments. The interaction with independent ProverEnvironments works from multiple threads. However, some SMT solvers require synchronization (e.g., locking for an interleaved usage) and other solvers do not require external synchronization (this allows concurrent usage).

Fig. 1.
figure 1

Overview of JavaSMT

Table 2. Size (LOC) of the Java-based solver wrappers and native solver bindings

SMT-Solver Integration and Bindings. Of the eight SMT solvers that are available in JavaSMT, only PrincessĀ [46] and SMTInterpolĀ [18] were ā€˜easyā€™ to integrate, as they are written in Scala and Java, respectively. Those solvers also use the available memory management and garbage collection of the Java Virtual MachineĀ (JVM). All other solvers are written in C/C++ and need a Java Native InterfaceĀ (JNI) wrapper to interface with JavaSMT. Z3Ā [40] and CVC4Ā [5] provide their own Java wrappers, while the bindings used for MathSAT5Ā [19], BoolectorĀ [42], and Yices2Ā [25] are maintained by us. Those bindings are self-written or partially based on a version of the solver developers, extended with exception handling, and usable for debugging in JavaSMT. By providing language bindings for solvers in our library, we relieve the solver developers from this burden, and the implementation of exception handling and memory management is done in an efficient and common manner across several solvers.

Table 2 lists the size (lines of code) of the wrappers to integrate each solver in JavaSMT, in order to get a rough impression of the required effort to get a solver and its bindings usable in JavaSMT. The size information consists of two parts, namely the JNI bindings that are written in C/C++ and the Java code that implements the necessary interfaces of JavaSMT. An expressive solver API (like MathSAT5Ā or OptiMathSATĀ [47]) needs more code for their binding, with only a small increment in complexity compared to other solver bindings.

Note that the evolution of JavaSMTĀ depends on the evolution of the underlying SMT solvers. Z3Ā is well-known, has a large user group, and an active development team. Yet, interpolation support for Z3 was dropped with releaseĀ 4.8.1.Footnote 3 BitwuzlaĀ [41] is the successor of the SMT solver Boolector, for which the developers still provide small fixes. BitwuzlaĀ can be supported in JavaSMTĀ in the future. CVC4Ā has been developed further to CVC5. However, the maintainers dropped the existing Java API, partially because of issues with the Java garbage collection, and plan to replace it.Footnote 4 Yices2Ā is also actively maintained and adds new features regularly. For example, the developers added support for third-party SAT solvers such as CaDiCaLĀ and CryptoMiniSatĀ [48].

3 New Contributions in JavaSMTĀ 3

This section describes the improvements over the JavaSMTĀ version from five years agoĀ [37], split into two parts. First, we describe newly integrated solvers and theory features. Second, we provide information about the build process.

Support for Additional SMT Solvers. JavaSMTĀ 3Ā provides access to eight SMT solvers. Besides the solvers that were already integrated before, MathSAT5, OptiMathSAT, Z3, Princess, and SMTInterpol, the user can now additionally use Boolector, CVC4, and Yices2. Table 3 lists available theories and important features supported by each individual solver. BoolectorĀ is specialized in Bitvector-based theories, but does not support the Integer theory. It is shipped with several back-end SAT solvers, from which the user can choose a favorite: CaDiCaL, CryptoMiniSatĀ [48], Lingeling, MiniSatĀ [26], and PicoSATĀ [13]. All solvers support the input of plain SMT-LIB2Ā formulas. However, the feature most requested by JavaSMTĀ users is the input and output of SMT queries via the API, i.e., parsing and printing boolean formulas for a given context. This feature is required for (de-)serializing formulas to disk, for network transfer, and to translate formulas from one solver to another one. This feature is unfortunately missing for the newly integrated solvers, even though each solver internally already contains code for parsing and printing SMT-LIB2Ā formulas.

For formula manipulation, JavaSMTĀ accesses the components of a formula, e.g., operators and operands. We do not require full access to the internal data structures of the SMT solvers, but only limited access to the most basic parts. Only BoolectorĀ does not provide the necessary API.

Table 3. SMT theories and features supported by SMT solvers in JavaSMTĀ 3

Build Simplification. JavaSMTĀ 3Ā also supports more operating systems than before. Besides the existing support for Linux, we started to provide pre-compiled binaries for MacOS and Windows for more than half of the available solvers. This simplifies the initial steps for new users, which previously were required to compile and link the solvers on their own. This was an involving task, because of the diversity of build systems and dependencies of each solver.

In addition to this, we now offer direct support for two popular build systems for Java applications, namely AntĀ and Maven. JavaSMTĀ comes with several examples and documentation, such that the mentioned build systems can be used to set up JavaSMTĀ in a ready-to-go state on most systems. This eliminates the need for complex manual set up of dependencies and eases the use of JavaSMTĀ and the SMT solvers.

4 Evaluation

Frameworks that provide a unified API to SMT solvers (such as JavaSMT, PySMT, and ScalaSMT) are necessary because the characteristics of the SMT solvers vary a lot. In the evaluation we provide support for this argument.

We inlined a discussion of the features already in the previous section. Table 3 provides the overview of supported theories and shows that certain theories are available only for a subset of SMT solvers. The table also shows that there are several features that restrict the choice of SMT solvers for certain applications.

In terms of performance, we evaluate JavaSMTĀ 3Ā as a component of CPAcheckerĀ [11], which is an open-source software-verification frameworkĀ Footnote 5 that provides a range of different SMT-based algorithms for program analysisĀ [10] and encoding techniques for program control flowĀ [8, 12]. We compare three well-known and successful SMT-based algorithms for software model checking and show that ā€” when using the same algorithm and identical problem encoding ā€” the performance result of an analysis depends on the used SMT solver. Some algorithms depend on special features of the SMT solver, e.g., to provide a certain type of formula (such as interpolants) and operation on a formula (such as access to subformulas). There are SMT solvers that can not be used for some algorithms.

We aim to show that depending on the feature set of the SMT solvers, it is important to support a common API, and additionally, that using the text-based interaction via SMT-LIB2 is not an efficient solution, when it comes to formula analysis like adding additional information into a formula.

Benchmark Programs. We evaluate the usage of JavaSMTĀ on a large subset of the SV-benchmark suiteFootnote 6 containing over 1000 verification tasks. To have a broad variation of benchmark tasks, we include reachability problems from the categories BitVectors, ControlFlow, Heap, and Loops.

BitVectors depends on bit-precise reasoning and thus, the SMT solver needs to support Bitvector logic. Heap depends on modeling heap memory access, e.g., which is either encoded in the theory of Arrays or as Uninterpreted Functions. The category Loops contains tasks where the state space is potentially quite large.

Experimental Setup. We run all our experiments on computers with Intel Xeon E3-1230 v5 CPUs with 3.40Ā GHz, and limit the CPU time to 15Ā min and the memory to 15Ā GB. We use CPAcheckerĀ revisionĀ r36714, which internally uses JavaSMTĀ 3.7.0-73. The time needed for transforming the input program into SMT queries is rather small compared to the analysis time. Additionally, the progress of an algorithm depends on the result (e.g., model values or interpolants) returned from an SMT solver, thus we do not explicitly extract the run time required by the SMT solver itself for answering the satisfiability problem, but we measure the complete CPU time of CPAcheckerĀ for the verification run.

Analysis Configuration. We use three different SMT-based algorithms for software verificationĀ [10]. The first approach is bounded model checking (BMC)Ā [14, 15], which is applied in software and hardware model checking since many years. In this approach, a verification problem is encoded as single large SMT query and given to the SMT solver. No further interaction with the SMT solver is required. In our evaluation, we use a loop bound \(k=10\), which limits the size of the SMT query.

The second approach is Ā [9, 24], which extends BMC, and which uses auxiliary invariants to strengthen the induction hypothesis. In this approach, the algorithm generates several SMT queries (base case, inductive-step case, each with increasing loop bound) and uses an invariant generator that provides the auxiliary invariants. We use an interval-based invariant generator that provides not only the invariants, but also information about pointers and aliases, which must be inserted into the SMT formula using the formula visitor.

The third approach is predicate abstractionĀ [3, 12, 31, 35], which uses Craig interpolationĀ [22, 32, 39] to compute predicate abstractions of the program. This approach does not only query the SMT solver multiple times, but also uses (sequential) interpolation, which is currently supported only by MathSAT5, Princess, and SMTInterpol.

All approaches are executed in two configurations, depending on the used encoding of program statements: First, we apply a bitvector-based encoding that precisely models bit-precise arithmetics and overflows of the program. Second, an encoding based on linear integer arithmetic is used, which approximates the concrete program execution and is sufficient for some programs.

Solver Configuration. Overall, we aim to show that each solver provides a unique fingerprint of features and results. We aim for a precise program analysis and thus configure the SMT solvers to be as precise as possible, but with a reasonable configuration for each solver (i.e., without using a feature combination that is unsupported by the SMT solver).

SMTInterpolĀ does not support efficient solving of SMT queries in Bitvector logic, thus, it is configured to use only Integer logic. BoolectorĀ misses Integer logic, thus, it is applied only to the bit-precise configurations. Additionally, this SMT solver does not support formula inspection and decomposition, which is required by several components in , e.g., to encode proper pointer aliasing for the program analysis. While the code for formula inspection is called quite often, its influence on the results for the selected benchmark tasks is small. In order to be comparable as far as possible, we deactivate pointer aliasing when using Boolector. Yices2Ā misses proper support for Array logic, thus, we use a UF-based encoding of heap memory as alternative for this solver, which results in a slightly unsound analysis, but a comparable formula size and run time.

Fig. 2.
figure 2

Quantile plot for the runtime of with several SMT solvers

Results and Discussion. Figure 2 provides the quantile plot for the results of configurations with bit-precise encoding using several SMT solvers. The plot shows the CPU time for valid analysis results, i.e., proofs or counterexamples found, for both expected results true and false. We aim for providing all result that are useful for a user and do not show results where the tool (or SMT solver) crashes or runs out of resources. We do not subtract the run time required for the framework CPAcheckerĀ itself (which starts a Java virtual machine), as we assume it to be comparable per program task; we are only interested in the asymptotics in this evaluation. The overall performance of SMT solvers is similar for simple verification tasks, i.e., those with a small run time in the analysis. For difficult tasks with harder SMT queries, the differences of the SMT solvers emerge. When applying , the analysis inserts additional constraints into the SMT formula and requires the SMT solver to allow access to components of existing formulas. As BoolectorĀ misses this specific feature, cannot be very effective here. Other SMT solvers are the preferred choice.

Table 4 contains some example tasks from all used algorithms and encodings, where the difference between distinct SMT solvers is noteworthy. Choosing the optimal SMT solvers for an arbitrary problem task is not obvious.

Table 4. Run time for using different SMT solvers for bounded model checking (ā€˜BMCā€™), k-induction (ā€˜KIā€™), and predicate abstraction (ā€˜PAā€™) with the theories of Bitvectors (ā€˜BVā€™) and Integers (ā€˜Intā€™); CPU time given in seconds with two significant digits, ā€˜TOā€™ indicates timeouts (900Ā s), ā€˜ERRā€™ indicates errors, and empty cells indicate that the theory or interpolation was not supported

5 Conclusion

We contribute JavaSMTĀ 3, the third generation of the unifying Java API for SMT solvers. The package now contains more SMT solvers, an improved build process, and support for MacOS and Windows. The project has over 20 contributors, 2500 commits, and overall about 41000 lines of code.Footnote 7 JavaSMTĀ is used in Java applications (e.g.,Ā [23, 33, 36]) as a solution to combine convenience and performance for the interaction with SMT solvers, or to switch between different solvers and compare themĀ [11, 49]. The most prominent application using JavaSMTĀ is the verification framework CPAchecker (a widely-used software projectFootnote 8 with 73Ā forks on GitHub alone), for which JavaSMTĀ was originally developed. In the future, we plan to support more SMT solvers, operating systems, and hardware architectures, while keeping the user interface stable. We hope that even more researchers and developers of Java applications can benefit from SMT solving via a convenient and powerful API.

Data Availability Statement. All benchmark tasks for evaluation, configuration files, a ready-to-run version of our implementation, and tables with detailed results are available in our reproduction package on Zenodo as virtual machineĀ [1] and as ZIP archiveĀ [2]. The source code of the open-source library JavaSMTĀ [37] is available in the project repository; see https://github.com/sosy-lab/java-smt.

Funding. This project was supported by the Deutsche Forschungsgemeinschaft (DFG) ā€“ 378803395 (ConVeY).