figure a
figure b

1 Introduction

Great minds have long dreamed of creating machines that can reason deductively, that is, from a set of assumptions, determine whether a particular conclusion logically follows. The question of whether such a machine is possible was posed formally as a grand challenge by the famous mathematician David Hilbert in 1928, who called it the “Entscheidungsproblem” (decision problem) [24]. In 1936, both Church and Turing showed that, in general, this is impossible—the problem is undecidable [13, 42]. Undeterred, researchers in automated reasoning have searched for ways to solve either special cases of the problem that are decidable or to find heuristics that work well in practice. Satisfiability modulo theories (SMT) has emerged as an approach that seems to fill a sweet spot in this search space. SMT leverages a rich collection of decidable theories to provide considerable expressive power without sacrificing decidability. SMT also permits some queries over problems that are undecidable or whose decidability is unknown. For these, it employs powerful heuristics that often work well in practice.

This tutorial is an introduction to SMT for new users. We explain what kinds of problems are suitable for SMT solvers, describe the capabilities of modern solvers, and provide guidance on how to encode problems as SMT queries.

Throughout the tutorial, we provide examples and exercises to illustrate the concepts being explained. Unless otherwise stated, the exercises can be completed using either the cvc5  [3] or the Z3 SMT solver [32], through either their Python interface or their textual interface based on the SMT-LIB 2 format [8]. The cvc5 website at cvc5.github.io contains documentation that can be used as a reference to supplement the material in this tutorial. An online version of the tutorial is also available on that site by clicking on Tutorials. To work through the examples and exercises, we recommend one of the following options.

  1. A)

    To use a Python API for SMT, first create a virtual environment.

    figure c

    Next, install cvc5 ’s Python API or Z3 ’s Python API, or both.

    figure d

    cvc5 is distributed under the BSD 3-clause license. Some features, however, such as its finite field solver (see Sect. 4.9), are only available in an extended version of cvc5 distributed under the GNU General Public License (GPL).Footnote 1 Since GPL is a problem for some users, the GPL version is not built or distributed by default. To install the GPL version of cvc5, use:

    figure e

    Once a solver API is installed, you can copy example Python code into a script file, e.g., , and then type:

    figure g

    Note that, for the examples below, if you are using Z3 instead of cvc5, you must replace the first line of each Python code snippet with:

    figure h
  1. B)

    Executables for cvc5 and Z3 are available for download. For cvc5, go to the cvc5 website, click on Downloads, and follow the link to the release page on GitHub. Alternatively, for Z3, go to the Z3 releases page at github.com/Z3Prover/z3/releases. From either release page, download the latest release compatible with your machine (for cvc5, choose a GPL download if you want support for finite fields). Once you unzip the downloaded archive, the executable will be in the bin directory. Thus, if the unzipped directory is called release-dir, and you have downloaded cvc5, you can run an SMT-LIB example called Example.smt2 by typing:

    figure i

    from your shell’s command line. If you downloaded Z3, type instead:

    figure j
  2. C)

    From the cvc5 website, click on Try cvc5 online. This links to a page that provides a web interface for running cvc5 on scripts in the SMT-LIB format.

This tutorial has been tested with cvc5 1.2.0, Z3 4.13.0, and Python 3.12.3, but later releases should work as well. Solver outputs shown below are based on cvc5 version 1.2.0. Other versions or solvers should produce conceptually similar results, but the outputs may not be exactly the same. The SMT-LIB examples are based on version 2.6 of the format [5]. cvc5 ’s Python API was designed to be a drop-in replacement for Z3 ’s Python API. The credit for the design of the Python API thus goes to the Z3 authors.

2 Overview

At an intuitive level, SMT solvers are general-purpose problem solving tools. They are somewhat similar to calculators, in that the user provides the problem of interest, and the tool does some calculation to produce an answer. However, they are much more powerful than a simple calculator.

SMT solvers reason symbolically, as is done in grade school algebra. The user provides a set of that describe constraints to be satisfied, and the solver produces a satisfying all of the constraints, if there is one. Consider the following simple example, mimicking a typical algebra word problem.

Example 1

In 10 years, Alice will be twice as old as Bob is now, but in 22 years, Bob will be twice as old as Alice is now. How old are Alice and Bob?

First, let’s see how to solve this using Python.

figure m

The Pythonic API is designed to be as simple and intuitive as possible. We introduce the symbols we are using (SMT solvers always require that symbols be introduced before they are used), and then we call , passing in the two equations in much the same way we would write them naturally. The output is a simple representation of the solution as a Python list.

figure o

Alternatively, SMT solvers can take as input a script written in the SMT-LIB language [5], a standard developed by the SMT community whose syntax is similar to that of LISP. Below is the same example written in SMT-LIB.

figure p

The result is:

figure q

Notice that the solver replies before giving the solution. This is short for “satisfiable,” a word meaning that there is at least one solution. SMT solvers can also identify when a set of assertions has no solution. In this case, the solver replies , which is short for “unsatisfiable.”

Let’s take a closer look at the SMT-LIB input file, which is a sequence of . The command in the first line tells the solver which we are working in. In this case, we are using which stands for quantifier-free linear integer arithmetic. We explain more about logics in Sect. 4 below. The second line tells the solver to produce . A model assigns a concrete meaning to every user-declared symbol. Without turning this option on, a solver will still respond with or , but it may not be able to provide a model. The next two lines declare two called a and b. Informally, we often refer to these as variables, because they play the same role that variables do in math. However, in the automated reasoning literature, a typically refers to a symbol that is bound by a quantifier, whereas an uninterpreted constant is a symbol whose value is determined by a model. SMT-LIB follows the the latter terminology. The next two lines create . An assertion is a way of telling the solver about a formula that we would like to be true in the model that is produced. Note that the formulas too are specified in a LISP-like prefix syntax. Finally, the command tells the solver to check whether the set of assertions made so far is satisfiable, and the command (which is only legal if the solver returns ) prints values for each uninterpreted constant, with the guarantee that assigning these values to the constants makes all the assertions true. The values are printed using legal SMT-LIB syntax in case the user wants to copy and paste them into a new SMT-LIB script.

Exercise 1

Consider a modification of Example 1. The first assertion will stay the same, but for the second, let’s assert that Bob will be twice as old as Alice in only 20 years. Modify the Python program or SMT-LIB script to reflect the new set of constraints. What output does the SMT solver give?

So far, we have seen the most basic use of an SMT solver. Given a set of assertions, determine whether there is a solution for them. We now show that this basic capability can be used to answer several similar questions.

Suppose we have a set X of assumptions about the world, and we want to know whether some hypothetical Y is possible under those assumptions. If we can express X and Y as SMT formulas, then an SMT solver can answer the question. In fact, we simply assert each assumption in X as well as the formula representing Y and check whether this set of assertions is satisfiable.

Example 2

Let x and y be 32-bit integers, with x a multiple of 2. Is it possible for the machine arithmetic product of x and y to be 1?

For this problem, we’ll use . SMT solvers use bit-vectors to model machine arithmetic and other operations on fixed-size vectors of bits. The SMT-LIB encoding is as follows.

figure ag

This time, we use the logic which stands for quantifier-free bit-vectors. The underscore symbol is used in SMT-LIB to indicate that the next symbol is indexed by the following argument. It is used to specify the bit-vector size in this example. The symbol represents bit-vector multiplication, and the notation is the bit-vector constant whose value, in decimal notation, is X. Constant z names the value we must multiply by 2 to get x. Here’s how to solve it using the Pythonic API. This time, we’ll use the API in a way that more closely resembles the SMT-LIB script.

figure al

There is no solution because an even number does not have a multiplicative inverse in machine arithmetic (i.e., when doing arithmetic modulo a power of 2).

Exercise 2

Find the multiplicative inverse of 5 (mod \(2^8\)).

Another common situation is when we have a set X of assumptions, and we want to know whether some Y must hold as a consequence. If so, we say that Y is or by X. Again, assuming we can represent X and Y using formulas, we can start by asserting the formulas representing X. At this point, however, we do not assert the formula for Y. Instead, we assert its negation. If the result is , then Y must follow from X. The reasoning is that if it is not possible for the negation of Y to be true when X is true, then Y itself must be true. Let’s look at a version of the well-known syllogism about Socrates.

Example 3

If all humans are mortal, and Socrates is a human, then must Socrates be mortal?

The Python code is as follows.

figure ap

The SMT-LIB version of the same problem looks like this.

figure aq

This problem illustrates a few new encoding tools. First, we use the logic UF which stands for “uninterpreted functions.” This logic allows us to declare new function symbols. Note that it is also missing the QF prefix we’ve used above, which means that quantifiers are also allowed. We declare a new uninterpreted S. A sort is like a type in programming languages. We use an uninterpreted sort to represent a class of individual objects that cannot be modeled with the predefined sorts provided by SMT-LIB, (so far, we’ve seen the predefined sorts for integers and bit-vectors). Next, we declare two functions, Human and Mortal, each of which takes a single argument of sort S and returns a , the SMT-LIB Boolean sort. A function returning a Boolean is also called a . We then declare an uninterpreted constant called Socrates of sort S. Now, we are ready to encode the first fact, namely that all humans are mortal. To do so, we use the , ForAll. The assertion states that for every individual x of sort S, if the predicate Human holds for that individual, then the predicate Mortal also holds. The next assertion states that the Human predicate holds for Socrates. Finally, we want to see whether the fact that Socrates is mortal necessarily follows from the assumptions. To do this, we assert the negation of the statement and check for satisfiability. Running the example confirms that the result is unsatisfiable and thus, indeed, this statement is entailed.

What we have presented so far should provide a good high-level idea of what is possible with SMT solvers.Footnote 2 We cover these ideas in more detail in the following. In Sect. 3, we briefly describe the formal foundations for SMT. Next, in Sect. 4, we catalog the different supported by SMT solvers and provide examples of how to use them. We cover the different outputs produced by SMT solvers, including models and proofs, in Sect. 5, and conclude in Sect. 6 with pointers to additional resources.

3 Formal Foundations

The satisfiability modulo theories problem can be formalized in many-sorted first-order logic with equality. We briefly outline the necessary concepts here. Due to space constraints, we assume some familiarity with basic concepts and notation from mathematical logic. More details can be found in [21, 25].

3.1 Syntax

In first-order logic, one constructs formulas that are statements about individuals in some domain of discourse and their relationships. Many-sorted logic adds the possibility of talking about multiple, separate domains.

Signatures. The language of formulas is determined by a vocabulary of symbols, called a , which has three main components: (such as \(\textsf{Int}\), \(\textsf{Real}\), \(\textsf{Person}\), etc.) which name, or , domains of interest; (such as, \(+\), \(*\), \(\textsf{log}\), \(\textsf{mother}\), \(\textsf{father}\)) which denote total functions over the domains; and (such as, \(=\), <, \(\textsf{even}\), \(\textsf{married}\)) which denote total relations over the domains. A signature also specifies the of each function symbol f, which is the number of inputs f takes, as well as its , which consists of the sort of f’s inputs and of f’s output.Footnote 3 We say that f has arity n and rank \(\sigma _1\cdots \sigma _n\sigma \) in a signature \(\varSigma \) if f takes n inputs of respective sorts \(\sigma _1, \ldots , \sigma _n\) and returns an output of sort \(\sigma \). A function symbol of arity 0 and rank \(\sigma \) (such as 0, 1, \(\textsf{true}\), ...) is also called a of sort \(\sigma \). It is convenient to consider only signatures that have a distinguished sort \(\textsf{Bool}\), for the Booleans, and treat relation symbols as function symbols whose return type is \(\textsf{Bool}\). In addition, we assume that every signature contains a distinguished function symbol \(\approx _\sigma \) of rank \(\sigma \sigma \textsf{Bool} \), denoting the identity relation, for each sort \(\sigma \) of \(\varSigma \).

A signature \(\varSigma \) is a of a signature \(\varOmega \), and \(\varOmega \) is a of \(\varSigma \), if all the sort and function symbols of \(\varSigma \) are also in \(\varOmega \) and the function symbols have the same rank in \(\varOmega \) as they do in \(\varSigma \).

Variables, Terms and Formulas. To build formulas, in addition to fixing a signature \(\varSigma \), we also fix a set \(\textbf{X}\) of sorted variables, each associated with a sort \(\sigma \) and standing for some element from (the set denoted by) \(\sigma \). We can then build terms out of variables and function symbols from \(\varSigma \). Given a signature \(\varSigma \), a , or just for short, is defined inductively as follows: (i) a variable or constant symbol of sort \(\sigma \) is a term of sort \(\sigma \); (ii) if f is a function symbol of rank \(\sigma _1\cdots \sigma _n\sigma \), with \(n>0\), and \(t_1, \ldots , t_n\) are terms of sort \(\sigma _1\cdots \sigma _n\), respectively, then the expression \(f(t_1, \ldots , t_n)\) is a term of sort \(\sigma \); (iii) if \(\varphi \) is a term of sort \(\textsf{Bool}\) and x is a variable of sort \(\sigma \), then the expressions \(\exists \, x{:}\sigma .\,\varphi \) and \(\forall \, x{:}\sigma .\,\varphi \) are terms of sort \(\textsf{Bool}\). We then identify formulas with terms of sort \(\textsf{Bool}\). The distinguished symbols \(\forall \) and \(\exists \) are . We say that a variable x in a formula \(\varphi \) if x occurs in \(\varphi \) and either \(\varphi \) contains no quantifier symbols or it has the form \(\exists \, y{:}\sigma .\,\varphi '\) or \(\forall \, y{:}\sigma .\,\varphi '\), for some variable y, where x occurs free in \(\varphi '\).

3.2 Semantics

For each signature \(\varSigma \), the meaning of \(\varSigma \)-terms is provided by mathematical structures called interpretations. A \(\mathcal {I} \) maps:

  • each sort \(\sigma \) of \(\varSigma \) to a non-empty set \(\sigma ^\mathcal {I} \), the of \(\sigma \) in \(\mathcal {I}\), with \(\textsf{Bool} ^\mathcal {I} \) being the binary set \(\{\textsf{true} ,\textsf{false} \}\);

  • each variable \(x \in \textbf{X} \) of sort \(\sigma \) to an element \(x^\mathcal {I} \in \sigma ^\mathcal {I} \);

  • each function symbol f of rank \(\sigma _1\cdots \sigma _n\sigma \) to a total function \(f^\mathcal {I} \) of type \(\sigma _1^\mathcal {I} \times \cdots \times \sigma _n^\mathcal {I} \rightarrow \sigma ^\mathcal {I} \) (and, in particular, each constant symbol c of sort \(\sigma \) to an element \(c^\mathcal {I} \in \sigma ^\mathcal {I} \)).

We say that \(\sigma \) (resp. x, f) the set \(\sigma ^\mathcal {I} \) (element \(x^\mathcal {I} \), function \(f^\mathcal {I} \)) in \(\mathcal {I}\). Every \(\varSigma \)-interpretation \(\mathcal {I} \) extends from variables and function symbols to \(\varSigma \)-terms t as follows: (i) a term \(f(t_1,\ldots ,t_n)\) in \(\mathcal {I}\) to \(f^\mathcal {I} (t_1^\mathcal {I},\ldots ,t_n^\mathcal {I})\), the value returned by function \(f^\mathcal {I} \) when applied to the elements denoted by \(t_1,\ldots ,t_n\); (ii) an formula \(\exists \, x{:}\sigma .\,\varphi \) evaluates to \(\textsf{true} \) in \(\mathcal {I}\) if and only if \(\varphi \) evaluates to \(\textsf{true} \) in an interpretation \(\mathcal {I} [x \mapsto a]\) that maps x to some suitable \(a \in \sigma ^\mathcal {I} \) and is otherwise identical to \(\mathcal {I} \); (iii) a formula \(\forall \, x{:}\sigma .\,\varphi \) evaluates to \(\textsf{true} \) in \(\mathcal {I}\) if and only if \(\varphi \) evaluates to \(\textsf{true} \) in \(\mathcal {I} [x \mapsto a]\) for all possible choices of values for x in \(\sigma ^\mathcal {I} \).

An interpretation \(\mathcal {I}\)   a formula \(\varphi \) if \(\varphi ^\mathcal {I} = \textsf{true} \) and it if \(\varphi ^\mathcal {I} = \textsf{false} \). In the former case, we also say that \(\mathcal {I}\) is a of \(\varphi \).

The of an \(\varOmega \)-interpretation \(\mathcal {I}\) to a subsignature \(\varSigma \) of \(\varOmega \) is the (unique) \(\varSigma \)-interpretation that interprets the symbols of \(\varSigma \) exactly as \(\mathcal {I}\). Intuitively, the reduct is obtained by forgetting the symbols of \(\varOmega \) that are not in \(\varSigma \).

In the definition of interpretation above, we have not provided a meaning for the usual Boolean connectives such as \(\lnot , \wedge , \vee , \Rightarrow \) and so on. In SMT, specific interpretations of function symbols are provided by a theory, as explained next.

3.3 Theories

In general, we are not interested in arbitrary interpretations of terms and formulas in a signature \(\varSigma \) but in interpretations belonging to a specific theory \( T \) that constrain the meaning of the symbols in \(\varSigma \); for instance, that interpret \(\lnot \) and \(\wedge \) as logical negation and conjunction, \(0, 1, 2, \ldots \) as the natural numbers, and so on. Traditionally in logic, a theory is defined by a set of formulas, called : one considers only \(\varSigma \)-interpretations that satisfy all the axioms. In SMT, a theory is, more generally, a class of interpretations that can be specified axiomatically or in other ways. More precisely, a \( T \) is a pair \((\varSigma , \textbf{I})\) where \(\varSigma \) is a signature and \(\textbf{I}\) is a class of \(\varSigma \)-interpretations, however specified. We describe and discuss several examples of theories commonly used in SMT in the next section.

Given a theory \( T = (\varSigma , \textbf{I})\), we consider not just \(\varSigma \)-formulas but \(\varOmega \)-formulas for some supersignature \(\varOmega \) of \(\varSigma \). In the context of \( T \), we refer to the symbols of \(\varSigma \) as symbols and to the additional symbols in \(\varOmega \) as symbols. For instance, in the theory of reals, we may write a formula of the form \(a + 1 > b\) where a and b are uninterpreted, or , constants of sort \(\textsf{Real}\). Intuitively, while the meaning of \(+\) and 1 is fixed by the theory, the meaning of a and b is not. Hence, we consider the formula satisfiable if there are real values for a and b which make the formula evaluate to \(\textsf{true} \). This idea is formalized in the notion of satisfiability in \( T \).

Satisfiability Modulo a Theory. If \( T \) is a \(\varSigma \)-theory, a is any \(\varOmega \)-interpretation \(\mathcal {I}\) for some supersignature \(\varOmega \) of \(\varSigma \) whose restriction to \(\varSigma \) differs from an interpretation of \( T \) at most in the way it interprets the variables.

An \(\varOmega \)-formula \(\varphi \) is \( T \) if it is satisfied by some \( T \)-interpretation \(\mathcal {I}\)—which may interpret the variables of \(\varphi \) and the sort, function, and predicate symbols not in \(\varSigma \) arbitrarily. The formula is if it is satisfied by all \( T \)-interpretations. A set \(\varPhi \) of \(\varOmega \)-formulas , written \(\varPhi \models _{ T }\varphi \), if every \( T \)-interpretation that satisfies all formulas in \(\varPhi \) satisfies \(\varphi \) as well. The set \(\varPhi \) is \( T \) if there is a \( T \)-interpretation that satisfies all of its formulas.

4 SMT Theories

A key feature of SMT is that the entire problem is parameterized by the choice of a theory \( T \). This is important because it means that SMT is an algorithmic framework, rather than a fixed algorithm. Thus, if a particular problem cannot easily be encoded in any existing theory supported by SMT solvers, one option is to add support for a new theory which is better suited to the problem. In fact, this is exactly the process by which many of the theories supported by modern SMT solvers were added.

Theories can be used alone or in arbitrary combinations. Besides the theory, other parameters related to the syntax of formulas include whether or not to enable quantifiers and whether to disallow or limit the use of certain theory operations. In the SMT-LIB standard, and in solvers that support it, these parameters are configured by specifying a . A logic identifies the theory (or theories) being used and optionally imposes syntactic restrictions on the allowed formulas. Users can provide the SMT solver with a predefined (like , , and UF seen earlier) to specify which logic is to be used. By default (i.e., if no logic name is provided), SMT solvers typically enable all the theories they support and allow all operations. This is equivalent to using the special logic name ALL. However, solvers are often tuned with specific heuristics for specific logics. Thus, it is advisable to provide the solver with the most specific logic name possible. In this section, we discuss the most common theories and logics supported by SMT solvers, with examples of each.

4.1 Core Theory and Uninterpreted Symbols

The SMT-LIB standard defines a which consists of a core signature with a fixed interpretation that is always present, regardless of which other theories are being used. The core theory defines the Boolean sort ( in Python), the Boolean theory constants and ( and in Python), and the operators , , , , , and ( , , , , , and in Python), all with the usual meanings. The equality symbol is polymorphic: it can be applied to two terms of the same sort, for any predefined or user-declared sort. There are also two more polymorphic operators that require a bit more explanation. The ( ) operator takes two or more arguments of the same sort and returns exactly when all the arguments have pairwise distinct values. The ( ) operator takes three arguments, the first of which must be of Boolean sort. The other two arguments can have any sort as long as it is the same for both. The meaning of the operator is the second argument when the first argument is true, and the third argument otherwise.

The simplest logic that builds on the core theory is , short for “quantifier-free uninterpreted functions.” This logic disallows quantifiers and does not define any new symbols beyond those in the core theory. However, it allows the user to extend the signature with new sorts and symbols. The SMT solver is allowed to interpret these symbols in any way it chooses. This is why they are referred to as uninterpreted: the solver does not impose any restrictions on the interpretation (besides the declared arity and rank). The following example illustrates the use of uninterpreted symbols as well as the and operators.

Example 4

Let f be a unary function from U to U, for some set U. Check that, whatever the meaning of f, if \(f(f(f(x)))=x\) and \(f(f(f(f(f(x)))))=x\), then \(f(x)=x\).

We show a solution in Python followed by one using SMT-LIB.

figure dl
figure dm

We can derive \(f(x) = x\) from the first assertion by performing a series of substitutions, and thus the problem is unsatisfiable. Now, we present a simple example that illustrates the operator. It also shows that in Python, we can use instead of to assert that two terms are distinct.

Example 5

Suppose we know that x is either equal to y or z, depending on the value of the Boolean b. Suppose we further know that w is equal to one of y or z. Does it follow that \(x=w\)?

The Python solution is shown below.Footnote 4

figure dq

cvc5 outputs the following for this example.

figure dr

The result tells us that it does not follow that \(x=w\). The model gives us a to that claim. Because the sort U is uninterpreted, the model returned by cvc5 must choose an interpretation for it. Here, cvc5 tells us that it is interpreting U as a set with two elements, named and . The model then specifies that x and y have one value and z and w have the other, so x is not equal to w.

Exercise 3

Modify Example 4 to make it satisfiable and Example 5 to make it unsatisfiable.

4.2 Arithmetic

Though there are many tools available for arithmetic reasoning, SMT solvers are unique in their ability to reason efficiently about arbitrary Boolean combinations of arithmetic constraints, as well as to combine arithmetic reasoning with reasoning about other theories. It is important to note that SMT solvers reason precisely about both integer and real arithmetic. That is, they use arbitrary-precision arithmetic as opposed to machine integer or floating-point approximations. This means that SMT solvers are not susceptible to the numerical errors that can arise, for instance, when using floating-point arithmetic to approximate real arithmetic. It also means that for problems whose complexity lies mainly in the arithmetic reasoning, as opposed to Boolean reasoning, SMT solvers are typically slower than tools that use floating-point approximations. The underlying algorithms for arithmetic reasoning in SMT solvers are based on standard techniques that have been adapted to the SMT context, such as the Simplex algorithm [20] and Cylindrical Algebraic Decomposition [2].

There are a large number of logics to choose from within the arithmetic umbrella, with reasoning over reals generally more efficient than reasoning over integers, and reasoning over less expressive formulas generally more efficient than reasoning over more expressive ones. We briefly discuss the various logics here.

Difference Logic. In , every arithmetic constraint must be of the form \(x - y \bowtie c\) or \(x \bowtie c\), where \(\bowtie \ \in \{=, <, >, \le , \ge \}\), and c is a numeric theory constant. If x and y range over integers, we call it , and if they range over reals, we call it . Efficient algorithms exist for both [17, 36]. The names of these logics are and , respectively. One application for difference logic is  [41].

Example 6

Suppose we have 3 jobs to complete on 2 machines. Job 1 requires machine 1 for 10 min and then machine 2 for 5 min. Job 2 requires machine 2 for 20 min and then machine 1 for 5 min. And Job 3 requires machine 1 for 5 min and then machine 2 for 5 min. Can all jobs be completed in 30 min?

To solve the problem, we create integer variables for the start times of each task within each job. We assert that the start times are non-negative, each task within each job doesn’t start until the previous task finishes, and tasks on each machine don’t overlap. Finally, we check that each task finishes on time.

figure eb

Exercise 4

What is the minimum amount of time that it will take to complete all of the jobs in Example 6?

Linear Arithmetic. The logic of linear arithmetic allows arithmetic constraints to have any form that is equivalent to \(\sum c_i x_i + b \bowtie 0\), where \(b,c_i\) are numeric theory constants and \(\bowtie \ \in \{=,<,>,\le ,\ge \}\). As before, there are both integer and real variants, and , respectively. One can also mix the two with . Note that, according to the SMT-LIB standard, when using , integers and reals should not be mixed in the same linear sum, but most solvers (including cvc5 and Z3) are more permissive and do allow mixed terms. Example 1 is a good example of a simple problem.

Exercise 5

Repeat Exercise 1, but change the logic to , change the types of the variables from to , and append .0 to each numeric constant. Now, what output does the solver give?

Nonlinear Arithmetic. Moving up the expressiveness hierarchy, we next have logics for quantifier-free . In these logics, arbitrary polynomials are allowed in constraints. The logic is for nonlinear arithmetic over the reals, which is decidable but with doubly exponential complexity [2]. On the other hand, the same logic over integers, , is undecidable. cvc5 implements a decision procedure for based on a combination of heuristic pruning and cylindrical algebraic coverings [29]. cvc5 and other tools implement incomplete heuristic procedures for .

Example 7

Find a solution for \(x^2y + yz + 2xyz + 4xy + 8xz + 16 = 0\).

figure ep

4.3 Arrays

Consider the following Python function which swaps two elements in a dictionary.

figure eq

If a[i] and a[j] happen to be equal, the dictionary a is unchanged by the function. To prove this fact, we could try modeling dictionaries as uninterpreted functions. However, asserting that two functions are equal is not allowed in first-order logic. Alternatively, we could use a quantifier to assert that two functions return the same output when given the same input, for any input. However, we would like to avoid quantifiers when possible, as their use puts us in an undecidable logic.

Fortunately, the SMT-LIB standard includes a theory of arrays [30], which can help in this situation. The theory is perhaps more accurately viewed as a theory of mutable maps and is parameterized by two sorts, one for the index (corresponding to the key type of the dictionary) and one for the elements (values in the dictionary). For example, the SMT-LIB sort represents arrays indexed by integers and containing reals. Note that SMT arrays are always total, in the sense that they have an element for every value in the index sort. In particular, an array indexed by is conceptually infinite.

The theory has two operators: , which takes an array and an index and returns the element at that index, and , which takes an array a, an index i, and an element e, and returns a new array that is the result of updating a with the element e at index i.

Typically, the theory of arrays is used in combination with other theories that make sense for the index and element sorts. For example, the logic allows quantifier-free formulas with variables that range over integers and arrays of integers. The simplest logic with arrays is , in which all the sorts must be uninterpreted.

In the example below, we encode the above problem using the array theory.

Example 8

For the Python program above, show that, for arbitrary index and element sorts, if a[i] and a[j] are equal, then so are a and swap(a,i,j).

figure ex

Exercise 6

Another property of swap that we can prove is that if a[i] and a[j] are distinct, then swap would change a. Modify the solution for Example 8 to prove this property.

4.4 Bit-Vectors

Consider a simple implementation (written in a C-like syntax) for computing the absolute value of a 32-bit integer: \(abs(x) :=x < 0 \;?\; {-x} : x\). Instead of branching on \(x < 0\), it is possible to compute the absolute value of x with three or four branch-free operations [28] as follows. Let \( xrs \) be an abbreviation for the arithmetic right shift (\(\mathop {>>}\nolimits _s \)) of x by 31 bits. Note that the result of this operation is either 0 or \(-1\) (all bits set to 1), depending on the most significant bit (MSB) of x: if the MSB of x is 0, \( xrs \) is 0; otherwise, \( xrs \) is -1. Three branchless alternatives for computing the absolute value of x are as follows.

figure ey

These branchless versions of \( abs (x)\) make use of the 32-bit versions of the bit-wise operations exclusive or (\(\oplus \)), bit-wise and (\( \mathrel { \& } \)), logical shift left (\(\mathop {<<}\)), and arithmetic shift right (\(\mathop {>>}\nolimits _s \)).

We can use an SMT solver to prove whether the branchless versions are equivalent to the original implementation. Note that integers, as discussed in Sect. 4.2, are not a good fit, as it is difficult to model the bitwise operators using the arithmetic operators. However, the SMT-LIB standard includes a theory of fixed-size bit-vectors, which defines the bit-precise semantics of fixed-size machine integers. The name for the quantifier-free logic containing just this theory is . Using this logic, we can easily check the equivalence of the absolute value computations.

Example 9

Show that the first branchless alternative \( abs _1\) is equivalent to \( abs \).

figure fa

Exercise 7

Show that the second and third branchless alternatives \( abs _2\) and \( abs _3\) are equivalent to \( abs \).

4.5 Datatypes

Built into the SMT-LIB language is a mechanism for defining . Datatypes are highly useful in applications for reasoning about data structures like records, lists, and trees [7]. The quantifier-free logic name is .

Example 10

Model a binary tree containing integer data. Find trees x and y such that (i) the left subtree of x is the same as the right subtree of y and (ii) the data stored in x is greater than 100.

Note that we need both datatypes and integer arithmetic for this example. cvc5 supports the logic name , but Z3 does not. Fortunately, we can always use ALL for the logic if a more specific logic is not available.

figure fe

The output gives the values for x and y.

figure ff

Exercise 8

Show that a tree cannot be equal to its own left subtree.

4.6 Floating-Point Arithmetic

The most common representation of real numbers in hardware and software is the binary floating-point number representation system as defined by the IEEE Standard 754-2019 for Floating-Point Arithmetic [27]. Floating-point numbers are encoded as a triple of bit-vectors: the fractional part (the significand), the exponent (a power of 10 by which the significand is multiplied), and a sign bit. This representation is of limited range and precision, and thus, the domain of floating-point numbers is finite. It also includes special values for representing errors as not-a-number and for plus and minus infinity. In SMT-LIB, the IEEE-754 standard is formalized as the theory of floating-point arithmetic [11]. The quantifier-free logic name is .

Example 11

The SMT-LIB standard supports a operator fp.fma. Given three single precision floating-point numbers a, b, and c, show that the floating-point fused multiplication and addition of a, b, and c is different from first multiplying a and b and then adding c.

figure fi

The output gives the solution.

figure fj

Exercise 9

Modify the solution to Example 11 to show that floating-point addition is not associative, i.e., \(a + (b + c) \not = (a + b) + c\).

4.7 Strings

It is often necessary to reason about string data when reasoning about programs. Reasoning about bit-vector representations of strings has the disadvantage that it requires fixing the string length up front. Also, the theory of bit-vectors does not include many of the utility functions for strings that exist in string libraries in programming languages. The SMT-LIB theory of strings provides support for variable-length strings and a large set of string operations. The quantifier-free logic name is . Typically, though, we use since we need arithmetic to reason about string lengths.

Example 12

Given two strings, x1 and x2, each consisting of no more than two characters, is it possible to build the string "abbaabb" using only 3 string concatenations (where each concatenation may use any previous result including x1 and x2)?

We can solve this problem by building a circuit of string concatenations and using nondeterministic choice to pick the inputs for each concatenation.

figure fm

Exercise 10

Use SMT to determine how many concatenations are needed to get "abbaabb" if x1 and x2 are both restricted to have a length of 1.

4.8 Quantifiers

We saw an example of quantified formulas in Example 3. Quantifiers can be enabled in SMT solvers by dropping QF from the logic name. However, enabling quantifiers typically increases the complexity of the decision problem significantly. In fact, solving UF problems is equivalent to solving the decision problem for first-order logic, Hilbert’s original Entscheidungsproblem, which is undecidable. And although LIA, LRA, and NRA are decidable, the decision procedures are expensive. For these reasons, SMT solvers mostly handle quantifiers by attempting to find quantifier that, together with the other quantifier-free assertions, are unsatisfiable. For problems that are expected to be unsatisfiable, this approach can be quite effective. Moreover, by using different instantiation techniques and effort levels, a wide variety of problems can be solved.

cvc5 supports several techniques for handling quantified formulas, which can vary based on the logic. By default, cvc5 limits its effort so that it usually returns quickly with an answer of either or . For logics that include uninterpreted functions, it uses a combination of E-matching [31] and conflict-based instantiation [40]. In case the user wants to invest more effort, these techniques can be supplemented with techniques such as enumerative instantiation [38] (option enum-inst). For logics that admit quantifier elimination (e.g., quantified linear arithmetic or bit-vectors), it uses counterexample-guided quantifier instantiation [34, 39], which is a complete procedure for these logics.

By default, cvc5 will generally not attempt to determine that an input with quantified formulas is satisfiable. However, more advanced techniques can be used to answer in the presence of quantified formulas, including finite model finding [37] (option finite-model-find), model-based quantifier instantiation [23] (option mbqi), and syntax-guided quantifier instantiation [35] (option sygus-inst).

In general, to set options that are not on by default, we can use the solver method in Python, as shown below.

figure fs

4.9 Non-standard Theories

cvc5 and Z3 support several theories that are not (yet) part of the SMT-LIB standard. We discuss a few of them briefly here, focusing on those supported by cvc5. More documentation about non-standard theories, including reference tables describing the supported operators can be found on the cvc5 website.

Sequences. The theory of brings together features of the theories of arrays and strings. Similar to arrays, sequences are parameterized by the sort of their elements. So we can declare a sequence of integers, a sequence of bit-vectors, and so on. Like strings, sequences have a variable but finite length and can be concatenated together. The sequence theory is enabled whenever the string theory is enabled (e.g., by using the logic name or ). Note that Z3 also supports a theory of sequences that is mostly (but not entirely) compatible with the cvc5 version.

Example 13

Let x be a sequence of integers. Find a value for x such that the first and last elements sum to 9, and if we concatenate x with itself, then (3,4,5) appears as a subsequence.

figure fw

Exercise 11

Show that it’s not possible to have sequences x, y, and z such that x is a proper prefix of y, y is a proper prefix of z, and z is a proper prefix of x.

Finite Fields. cvc5 can reason about constraints over finite fields of order p, where p is any prime. It relies on the fact that a field of order p is isomorphic to the integers modulo p. The quantifier-free logic name for finite fields is . At the time of writing, this theory is not supported by other SMT solvers.

Example 14

In a finite field of order 13, find two elements such that their sum and product are both equal to the multiplicative identity in the field.

Running this example requires a GPL build of cvc5, as explained in Sect. 1.

figure fy

Exercise 12

In a finite field of order 13, find an element such that if you square it twice you get the multiplicative identity.

Finite Sets. cvc5 has support for the theory of finite sets. This theory supports basic set operations like membership, union, and intersection, as well as constraints on a set’s cardinality. The quantifier-free logic name is . At the time of writing, this theory is not supported by other SMT solvers.

Example 15

Verify that union distributes over intersection.

figure ga

Exercise 13

Does set difference distribute over intersection? If not, find a counterexample.

4.10 Combinations of Theories

So far, we have mostly seen examples of how to pose queries that involve a single theory. Part of the appeal of SMT solvers is their ability to mix reasoning about different theories. This can be done in a natural way. Any well-sorted formula is allowed, and all sort constructors can take any other sort as an argument.

One slight complication is the question of how to specify the logic name. It is always safe to use ALL as the logic name, though as mentioned above, it may be more efficient to give a more precise logic name. When mixing theories, cvc5 allows any logic name that follows the following rules. First, the logic name must start with the prefix if the intent is to limit reasoning to quantifier-free formulas. The rest of the logic name can include any of the following components, in any order: (i) A for arrays; (ii) UF for uninterpreted functions; (iii) BV for bit-vectors; (iv) FP for floating-point numbers; (v) DT for datatypes; (vi) S for strings and sequences; (vii) either IDL, RDL, LIA, LRA, LIRA, NIA, NRA, or NIRA for arithmetic; (viii) FF for finite fields; and (ix) FS for finite sets. Thus, for example, allows formulas that are quantifier-free and mix arrays, uninterpreted functions, datatypes, bit-vectors, and linear real arithmetic. Examples 1012, and 13 illustrate combinations of theories.

5 SMT Solver Outputs

As we have seen, the main result of an SMT query is either or . In some cases, the solver may also output . This can happen, for example, if the problem includes quantifiers. In this section, we discuss how to obtain more information from the solver in each case.

Satisfiable Queries. When a solver returns , we have already seen that one possible way to get more information is to call , which returns values for all of the uninterpreted constants in the formula. A more fine-grained approach is to call which takes a term as an argument and returns the value of that specific term.

Unsatisfiable Queries. When a solver returns , it makes a quite strong statement: there is no interpretation of the user-declared symbols that satisfies the formula. SMT solvers can provide more information as to why a formula is unsatisfiable via an , a subset of the assertions that is already unsatisfiable. In SMT-LIB scripts, it can be obtained with the command . The unsat core is not guaranteed to be minimal, but solvers generally make an effort to reduce its size as much as possible without having to solve additional SMT queries.

Some solvers can also produce proofs for the unsatisfiability of a formula, i.e., a structured argument showing how an inconsistency can be derived from an unsat core of the formula. A proof can serve as a certificate of the result and be used to independently validate the solver’s response [4] . A proof (if supported) can be obtained in an SMT-LIB script with the command . The result is dependent on the proof system and format the solver uses to represent its reasoning. cvc5 has full support for proofs and unsat cores.

Consider again the Socrates example (Example 3). Below, we show how to retrieve an unsat core and a proof of its unsatisfiability.

figure gn

The first part of the output is the unsat core.

figure go

The core contains all three assertions. In this case, the core is minimal, as all three are needed to derive . The reasoning is shown in the proof. The result of the proof() method is a proof object which connects the input assertions to the conclusion ( ) via a sequence of steps justified by proof rules. The proof rules used by cvc5 are documented on the cvc5 website.

Figure 1 shows a visualization of the proof as a tree. For readability, we use simple names to abbreviate long terms. Each node in the tree shows: (i) the formula proved (the conclusion); (ii) the name of the proof rule used; (iii) a numeric id; and (iv) the total number of descendants. Immediate children of each node represent premises required for the node’s proof rule. The root of the tree is let9, which stands for , where let4, let3, and let2 represent the three assertions. This node has a single child containing the conclusion , based on a proof tree whose leaves are the three assertions. The derivation of depends on instantiating the quantified assertion (let4) with x as Socrates. This is done in node 5, only after (i.e., let4) is rewritten (node 8) into (i.e., let8). The instantiation is named let6. Node 9 concludes from the other assertions. Finally, node 2 concludes from the mutually inconsistent clauses derived by the solver (where let7 is , let2 is , and let5 is ).

Fig. 1.
figure 1

A proof tree generated by cvc5

Unknown Queries. A solver returns when it is unable to solve the input problem. There could be several different reasons for this. One is that the solver’s procedure may be incomplete for the class of problems the input belongs to, which means that it is not always able to determine if the problem is satisfiable or not. Another possible reason is that some resource limit was exceeded, causing the solver to stop before it could find an answer. In SMT-LIB, the command can be used to request more information about why a solver returned .

6 Conclusion

This tutorial is a basic introduction to using SMT solvers. There are numerous resources available for those who wish to learn more.

The SMT-LIB website smt-lib.org has details about the SMT-LIB standard [5], as well as links to software and an extensive collection of benchmarks. More information on the foundations of SMT and how solvers work under the hood can be found in several overview papers and book chapters [6, 9, 18]. There are also tool papers describing the most prominent SMT solvers, including: Alt-Ergo  [15], Bitwuzla  [33], cvc5  [29], MathSAT  [14], OpenSMT2  [26], SMTInterpol  [12], SMT-RAT  [16], STP  [22], veriT [10], Yices2  [19], and Z3  [32]. More information about cvc5 is available on its website.