Abstract. We present an overview of Z3 internals to outline the main data-structures and algorithms used in Z3. The overarching aim is to help users gain more insight into formula encodings that have a better chance of being solved, what controls a user has over search behavior, and what information a user can extract from the solver. While we will try to address these questions by providing background on principles and examples of experiences, it is impossible to provide comprehensive answers. The hope is that shedding light into the internal workings of Z3 at a conceptual level offers a bridge towards developing an understanding of how to operate Z3 as well.
This document is written from the perspective of a developer of the SMT solver Z3. While it attempts to introduce an audience that includes many users to how to build an SMT solver, the main interest among users is more likely about how to best use an SMT solver. By introducing the internals of Z3 from the point of view of foundations, data-structures, algorithm design, and heuristics we introduce the reader to the development process. But, in this process, we also indirectly aim to address readers whose aim is to understand the how and why of using Z3. Many of the principles underlying the implementation of Z3 have been described in papers over several years and this document is not a substitute for these papers. We therefore often resort to references when material is covered elsewhere and instead attempt to reflect updated systems aspects.
Figure 1 shows a bird's eye view of the main components of Z3.
The main point of reference for Z3 is the GitHub repository
This document builds on top of and extends the material presented in [9] as part of a shorter tutorial.
You can use Z3 online from your browser. No installation is required
This guide complements the tutorial
Some of the internals are already described in depth in the Programming Z3 tutorial so they are covered in less detail here.
Z3 contains a collection of core engines for solving formulas. The engine used by default is based on the CDCL(T) architecture, but other engines are available and invoked for special classes of formulas where they are more suitable.
We here describe the solvers marked as SAT and SMT in Figure 1.
When SAT solving, as implemented using conflict driven clause learning, CDCL, is combined with theory solving
it augments propositional satisfiability with theory reasoning. The CDCL solver maintains a set of
formulas and a partial assignment to literals in that we refer to as .
The solver starts in a state , where is initially empty.
It then attempts to complete to a full model of to show that is satisfiable
and at the same time adds consequences
to to establish that is unsatisfiable. The transition between the search for a satisfying
solution and a consequence is handled by a conflict resolution phase.
The state during conflict resolution is a triple ,
where, besides the partial model and formula , there is also a conflict clause
false under .
The auxiliary function Theory is used to advance decisions, propagations and identify conflicts.
If Theory determines that is conflicting with respect to the literals in it produces a conflict clause , that
contains a subset of conflicting literals from . It can also produce a trail assignment , which is either a propagation
or decision and finally, if it determines that is satisfiable under trail it produces .
From the point of view of the CDCL(T) solver, theory reasoning is a module that can take a state during search and produce verdicts on how search should progress. We use the following verdicts of a theory invocation :
Thus, the verdict of Theory determines whether the partial model extends to a model of the theories, can identify a subset of as an unsatisfiable core, propagate the truth assignment of a literal , or create a new case split for a literal that has not already been assigned in . We write when the verdict is that extends to a valid theory model of , we write when is a conflict clause, based on negated literals from and , when the verdict is either a propagation or decision.
Example 1.
Consider the formula .
The initial state of search is
based on the empty partial assignment and the original formula. A possible next state is to propagate on the unit literal , producing
This step may be followed by a case split setting to false.
which triggers a unit-propagation
The resulting state is satisfiable in the theory of arithmetic. On the other hand, if we had chosen to set to true as well as , we would have encountered a conflicting state with conflict clause 1:
The last decision is then reverted to produce the satisfiable state
A third scenario uses theory propagation. In this scenario, the decision is made, but instead of making a decision , the theory solver for arithmetic is given a chance to find opportunities for propagation. It deduces that together with implies , and therefore establishes the theory propagation
We are again eliding the unit literal from the explanation for . In practice, solvers automatically remove literals from conflict clauses that are necessarily false.
In the initial overview of the CDCL(T) loop, we left out other inference rules that are integral to efficient search.
Learn: Conflict clauses are added to the set of formulas . Conflict clauses are marked as redundant.
Restart: A suffix of the current partial model is abandoned.
In-processes: Clauses are inferred or marked redundant based on global inferences. This is usually done after a restart.
Garbage Collection: Redundant clauses are removed from .
To be well-behaved we expect Theory to produce propagations on literals that don't already appear in , and crucially enforce the main invariants:
That is, each conflict clause is a consequence of and each propagation is also a consequence of , and the premises of a propagation is justified by .
Z3 contains a dedicated engine for non-linear arithmetic over polynomials (Tarski's fragment [44]) [34]. The theory is decidable using cylindrical algebraic decomposition (CAD) [16]. The NLSAT engine implements the decision procedure for polynomial arithmetic. It uses a model constructing satisfiability engine (MC-SAT) to guide uses of CAD to a focused set of conflict clauses. Thus, the CAD algorithm is used on fragments of the entire formula for determining satisfiability, as opposed to globally on an entire formula.
The NLSAT engine is available as a one-shot engine for non-linear arithmetic formulas. It is also integrated with decision procedures in the CDCL(T) engine as an end-game solver.
Formulas that range over Constrained Horn Clauses (CHC) [7, 27] are solved using a dedicated engine, SPACER, [28] that builds on an engine inspired by IC3, [6, 29]. A Constrained Horn Clause is a formula of the form
where is a formula that is interpreted over a combination of theories, are uninterpreted predicates, and are terms over the bound variables and the theories from . The SPACER engine searches for a model for such that all clauses are true under the model.
The QSAT algorithm determines satisfiability of quantified formulas using theories that allow quantifier projection. There are two instances of QSAT, one for combinations of linear arithmetic, algebraic data-types, bit-vectors and arrays, the other is for non-linear polynomial arithmetic formulas over reals. They build on results developed in [3, 4, 41] and for solving NLSAT and Model-based projection introduced for SPACER. The algorithm is described in Programming Z3.
We will apply the following taxonomy when discussing the theory solvers. It extends the reduction approach to decision procedures [35] as explained in the context of Z3 in [17].
Bit-vectors are in the current solver treated as tuples of Boolean variables and all bit-vector operations are translated to Boolean SAT. The approach is called bit-blasting. Only mild equational reasoning is performed over bit-vectors. The benefits of the CDCL SAT engine have been mostly enjoyed for applications in symbolic execution of 32-bit arithmetic instructions. Bit-blasting has its limitations: applications that use quantifiers and applications around smart contracts that use 256 bits per word are stressing this approach. A revised bit-vector solver that integrates algebraic reasoning and lazy bit-blasting is therefore currently being developed.
Cardinality and Pseudo-Boolean inequalities can also be translated into CNF clauses. It is often an advantage when there are very few variables in the summations, but the overhead of translation can quickly become impractical. Z3 therefore contains dedicated solvers for cardinality and Pseudo-Boolean constraints.
Example 2.
Bit-blasting the arithmetic expression assuming both and consist of bits results in the formula
the result of the summation is given by . is the carry of adding the first bits of and . Pseudo-Boolean constraints like can be translated in multiple ways like translating it to bit-vector addition/multiplication as before, translating it to decision-diagrams or sorting-networks.
A decision diagram for this constraint looks like
which can be translated to a propositional formula. The sorting network approach sorts the bit-sequence to a sequence and asserts that has to be true.
The native solver instead tracks the partial sum of already assigned booleans and sets some of the remaining ones in case it is not possible to exceed the desired bound without them.
This theory captures a shared property of all theory: that equality is a congruence relation. The theory is described in many places, including in [8].
Let us use a simple example to illustrate the scope and main functionality of congruence closure based decision procedures for equality. We are given two equalities and one disequality
Their conjunction is unsatisfiable. Unsatisfiability is established by combining union-find [@UnionFind] data-structures to maintain equivalence classes and a data-structure of E-graphs to enforce congruences. In a first step, the terms are represented by unique nodes in a DAG structure for every sub-term. We represent the DAG structure as a sequence of definitions. Then equalities and disequalities are between nodes in DAG.
Process equality atoms using union-find. It establishes the equivalence classes
Then congruence closure triggers whenever there are two nodes, from different equivalence classes, labeled by the same function , with equal children. In our case belong to the same equivalence class so the nodes are merged.
When the children of the equality term are merged into the same equivalence class, the term is set to true. This contradicts that is already set to false.
The E-Node data-structure [21, 23, 38] is used to implement congruence closure efficiently. The main fields of an E-Node are
When a term occurs in a formula used by the solver it is compiled into an E-node . The E-node is initialized to
Example 3.
The result of internalizing the following term
is
The field is used to implement union-find. We use three fields to represent the union-find data-structure, comprising of:
Z3 performs eager path compression. It has the effect that the root node can always be accessed by a single pointer dereference. The singly-linked cyclic list of siblings of an equivalence class is used by operations that need to iterate over equivalence classes. Access to the equivalence class is used by E-matching and for propagation
Besides the E-nodes, the solver maintains a hash-table, we call etable, that is used to find the congruence root of a function application. It maps a function symbol and list of arguments to that are represented by root E-nodes into a congruence closure root. For the congruence closure root it maintains the invariant . We write to indicate that the etable tracks function applications to root nodes. In reality, the etable just stores the node and uses the function when computing the current hash of and equalities with other nodes.
The main functionality of congruence closure is to ensure that all equivalence classes that follow from equalities are inferred. It exposes the main function merge() to establish that two nodes are equal under justification . We describe justifications later in more detail. The main steps of merge are outlined below.
Roots | |
assume | |
assume | |
Erase | for each where : |
erase | |
Update Root | |
Justify | justify() |
Insert | for each : |
if then | |
etable | |
if then | |
append to | |
else | |
add to tomerge |
Roots: congruence closure always merges current roots of equivalence classes. If the roots are equal, there is nothing to merge. We assume that is chosen as root. The main mechanism for ensuring congruence closure has sub-quadratic running time is obtained by applying Hopcroft's method of merging the lesser half. Z3 makes an exception to this rule if is labeled by a term that qualifies as a value. To quickly identify if a congruence class contains a value, such as , it sets the root of a class to be the node representing the value.
Erase: Entries of the etable that are stale are removed. An entry is stale if it points to the old root as congruence closure representative.
Update Root: The root is updated along with size of equivalence classes and links into the cyclic list of siblings.
Justify: The justification for a merged equality is updated. We describe this functionality later.
Insert: Finally, the etable is updated by updating the field of parents from . It maintains a list tomerge of new nodes that are discovered congruent. An outer loop invokes merge on the list of new nodes until it is empty.
All operations on the E-Graph can be inverted. For this purpose, every merge is recorded in an undo trail. Other operations that update nodes are recorded in the same trail. For example, when is appended to the insertion into the cyclic list of parents is recorded so that it can be reverted. Reverting a merge requires updating the etable and is sensitive to whether a node was a congruence root:
The effect of unmerge() is as follows:
Erase | for each added from : |
erase | |
UnJustify | unjustify() |
Revert Root | |
Insert | for each : |
if | |
A justification is a reason for merging two nodes. There are two possible reasons for merging nodes:
NB: is justified recursively by justifying .
Invariant: Every non-root node points to a linked list of justifications leading to the root
NB The linked list does not follow direction of union-find.
Note that not all possible justifications are tracked, if merge() is invoked but already , then the justification for the equality of is dropped. In contrast, egg [25] keeps track of potential extra paths to find short proofs. Use cases within CDCL(T), that leverages amortized effect of backtracking search typically hedge on that the cost of finding a strongest conflict up front is outweighed by multiple attempts that converge on sufficiently strong conflicts.
We can create proofs from first-principles by using justifications. Suppose follows from a sequence merge(), merge(),, merge().
Then a proof of can be extracted using overloaded functions :
In the setting of CDCL(T), congruence closure is integrated with Boolean reasoning. For example, if is assigned true, , then the Boolean predicate can be propagated to true. Thus, congruence closure can be benefitial in propagation. On the other hand, propagations made by the CDCL(T) core need not be replicated by congruence closure: if the literal corresponding to is assigned to , then clausification of introduces clauses and , the ensure propagation to . Z3 therefore dampens the role of congruence closure for Boolean connectives that are handled by CDCL.
To track Boolean assignments the E-node data-structure includes the fields
Furthermore, equality is treated as a special function symbol. If the children of are merged, then the of is set to .
The E-graph also dispatches equality and disequality propagation between theories.
There are several alternate engines for arithmetical constraints in Z3. Some of the engines are engineered for fragments of arithmetic, such as difference arithmetic, where all inequalities are of the form , for a constant, and unit-two-variable-per-inequality (UTVPI), where all inequalities are of the form . A new main solver for general arithmetic formulas has emerged recently, with the longer term objective of entirely replacing Z3's legacy arithmetic solver. We will here describe internals of the newer solver in more detail.
In overview, the arithmetic solver uses a waterfall model for solving arithmetic constraints.
The solver for rational linear inequalities uses a dual simplex solver, as explained in [24]. It maintains a global set of equalities of the form , and each variable is assigned lower and upper bounds during search. The solver then checks for feasibility of the resulting system for dynamically changing bounds . The bounds are justified by assignments in .
Example 4.
For the following formula
x, y = Reals('x y')
solve([x >= 0, Or(x + y <= 2, x + 2*y >= 6),
Or(x + y >= 2, x + 2*y > 4)])
Z3 introduces auxiliary variables and represents the formula as
Only bounds (e.g., ) are asserted during search.
The first two equalities form the tableau. Thus, the definitions produce the equalities
They are equivalent to the normal form:
where are basic (dependent) and are non-basic. In dual Simplex tableaux, values of a non-basic variable can be chosen between and . The value of a basic variable is a function of non-basic variable values. It is the unique value that satisfies the unique row where the basic variable occurs. Pivoting swaps basic and non-basic variables and is used to get values of basic variables within bounds. For example, assume we start with a set of initial values and bounds . Then has to be 2 and it is made non-basic. Instead, becomes basic:
The new tableau updates the assignment of variables to . The resulting assignment is a model for the original formula.
Example 5.
From equalities infer that . Based on the tableau form, the solver is presented with the original equality atoms via slack variables
The tableau can be solved by setting . The slack variables are bounded when the equalities are asserted
The original solution is no longer valid, the values for are out of bounds. Pivoting re-establishes feasibility using a different solution, for example
with assignment . The variables and have the same value, but must they be equal under all assignments? We can establish this directly by subtracting the right-hand sides and from another and by factoring in the constant bounds to obtain the result . But subtraction is generally expensive if there are many bounded constants in the rows. The arithmetical operations are not required to infer that . Z3 uses two facts to infer the equality
Variables that satisfy these two criteria must be equal.
The mixed integer linear solver consists of several layers.
Each row is first normalized by multiplying it with the least common multiple of the denominators of each coefficient. For each row, it assembles a value from the fixed variables. A variable is fixed if the current values , are equal. Then it checks that the gcd of the coefficients to variables divide the fixed value. If they don't the row has no integer solution.
Example 6.
Assume we are given a row , where are fixed at , , and is the base variable.
Then it follows that which has no solution over the integers: The greatest common divisor of coefficients to the non-fixed variables () does not divide the constant offset from the fixed variables ().
This GCD test is extended to as follows. For each row , where
Let . That is, the lower and upper bounds for based on the bounds for .
Let , . If , there is no solution for within the bounds for .
When a tableau with integer variables is feasible, all non-basic variables can be assumed to be integers: Dual simplex moves non-basic variables to their bounds and the bounds are integers. Only the basic variables could be non-integral (they are still within their bounds). Patching seeks changing values to non-basic values to move as many basic variables to integers.
Example 7.
Suppose we are given a tableau of the form
where are basic variables and has bounds , has bounds , has bounds . The variable is initially assigned at the bound . Then and . But neither nor is close to their bounds. We can move to without violating the bound for because of . Thus, the freedom interval for is the range and within this range there is a solution, , where are integers and within their bounds.
Z3 patching works by shifting a non-basic variable by to make a dependent basic variable integral.
for in basic variables, such that :
for in row of with :
find such that the update
is feasible and fixes more basic variables than it breaks
Given a row , where is a basic variable, is non-basic variable multiplied by the fraction , and is the remainding of the row, the shift amount is computed based on the following analysis.
Take first the fractional parts of and :
The goal is to compute two values for , one negative, one positive, of minimal absolute values, such that , where is the set of integers. Rewriting, we have .
If then is also an integer. Therefore . That means for some , because and are coprime. By substituting with we get and . Since , and and are coprime, . Therefore, we search for in form , . We obtain . From follows for some . We can rewrite the last equality as . Because , and and are coprime, and are mutually prime too. That means that for some we have . We can show that if then .
From the other side, for any satisfying holds . Since and are coprime, , and . We conclude that , and .
An important factor in solving more satisfiable integer arithmetic instances is a method by Bromberger and Weidenbach [12, 13]. It allows detecting feasible inequalities over integer variables by solving a stronger linear system. In addition, we observed that the default strengthening proposed by Bromberger and Weidenbach can often be avoided: integer solutions can be guaranteed from weaker systems.
We will here recall the main method and our twist. In the following we let range over integer matrices and , , over integer vectors. The 1-norm of a matrix is a column vector, such that each entry is the sum of the absolute values of the elements in the corresponding row . We write to directly access the 1-norm of a row .
A (unit) cube is a polyhedron that is a Cartesian product of intervals of length one for each variable. Since each variable therefore contains an integer point, the interior of the polyhedron contains an integer point. The condition for a convex polyhedron to contain a cube can be recast as follows:
Example 8.
Suppose we have and wish to find an integer solution.
By solving we find
a model where . After rounding to and maintaining at we obtain an
integer solution to the original inequalities.
Our twist on Bromberger and Weidenbach's method is to avoid strengthening on selected inequalities. First, we note that difference inequalities of the form , where are integer variables and is an integer offset need not be strengthened. For octagon constraints , there is a boundary condition: they need only require strengthening if are assigned at mid-points between integral solutions. For example, if and , for . Our approach is described in detail in [5].
Similar to traditional MIP branch-and-bound methods, the solver creates somewhat eagerly case splits on bounds of integer variables if the dual simplex solver fails to assign them integer values. For example, Simplex may assign an integer variable , the value , in which case z3 creates a literal that triggers two branches and .
The arithmetic solver produces Gomory cuts from rows where the basic variables are non-integers after the non-basic variables have been pushed to the bounds. It also incorporates algorithms from [14, 22] to generate cuts after the linear systems have been transformed into Hermite matrices.
Similar to solving for integer feasibility, the arithmetic solver solves constraints over polynomials using a waterfall model for non-linear constraints. At the basis it maintains for every monomial term an equation , where is a variable that represents the monomial . The module for non-linear arithmetic then attempts to establish a valuation where , or derive a consequence that no such valuation exists. The stages in the waterfall model are summarized in the following paragraphs.
A relatively inexpensive step is to propagate and check bounds based on non-linear constraints. For example, for , then , if furthermore , we have the strengthened bound . Bounds propagation can also flow from bounds on to bounds on the variables that make up the monomial, such that when , then we learn the stronger bound on .
If , then and therefore , but we would not be able to deduce this fact if combining bounds individually for and because no bounds can be inferred for in isolation. The solver therefore attempts different re-distribution of multiplication in an effort to find stronger bounds.
Many constraints using polynomials can be checked for consistency by normalization. The Gröbner basis reduction algorithms provide a means to check consistency of a set of polynomial equations (which is complete if adding the Nullstellensatz checks). Z3 uses a best effort Gröbner basis reduction to find inconsistencies, cheaply, and propagate consequences. It first extracts equalities from Simplex rows where the basic variable is bounded. It adds equations for arithmetic variables that are defined as monomials.
Equations are pre-solved if they are linear and can be split into two groups, one containing a single variable that has a lower (upper) bound, the other with more than two variables with upper (lower) bounds. This avoids losing bounds information during completion.
After (partial) completion, perform constant propagation for equalities of the form . The equality is propagated to the linear solver.
After (partial) completion, perform factorization for factors of the form where are variables an is linear. We infer the clause .
We use an adaptation of ZDD (Zero suppressed decision diagrams [37, 39]) to represent polynomials. The representation has the advantage that polynomials are stored in a shared data-structure and operations over polynomials are memorized. A polynomial over the real is represented as an acyclic graph, where nodes are labeled by variables and edges are labeled by coefficients. For example, the polynomial is represented by the acyclic graph shown below.
The root node labeled by represents the polynomial , where is the polynomial of the left sub-graph and the polynomial of the right sub-graph. The left sub-graph is allowed to be labeled again by , but the right sub-graph may only have nodes labeled by variables that are smaller in a fixed ordering. The fixed ordering used in this example sets above . Then the polynomial for the right sub-graph is , and the polynomial with the left sub-graph is .
The Gröbner module performs a set of partial completion steps, preferring to eliminate variables that can be isolated, and expanding a bounded number of super-position steps (reductions by S-polynomials).
Following [15] we incrementally linearize monomial constraints. For example, we include lemmas of the form and , for .
As an end-game attempt, the solver attempts to solver the non-linear constraints using a complete solver for Tarski's fragment supported by the NLSat solver [34].
Z3 uses infinite precision integers and rationals. This ensures that all operations are sound and not subject to errors introduced by rounding. It uses a sparse matrix representation for the Dual Simplex tableau. Note that pivoting may turn an otherwise sparse matrix into a dense form. We also created a version that uses an LRU decomposition and floating point numbers but found that extending this version with efficient backtracking was not practical compared to the straight-forward sparse matrix format.
The arithmetic solver reduces operations such as integer modulus, remainder, and division into equations. It eagerly compiles bounds axioms of the form for atomic formulas .
Let us illustrate a use of reduction from richer theories to base theories based on a simple example based on refinement types. It encodes refinement types using auxiliary functions, as explained in [30]. Refinement types are not part of Z3, so the decision procedure we outline here is not available out of the box. One way to realize this theory is externally through the User-Propagator facility.
Abstractly, a refinement type of sort uses a predicate over . At least one element of must satisfy for the construction to make sense. The refinement type represents the elements of that satisfy . The properties we need to know about elements of can be encoded using two auxiliary functions that form a surjection from into with a partial inverse that maps elements from into . The properties of these functions are summarized as follows:
Let us illustrate the sort of natural numbers as a refinement type of integers:
Example 9.
We obtain a theory solver for formulas with refinement types by instantiating these axioms whenever there is a term introduced of sort introduced as part of the input or during search (from instantiating quantifiers). The main challenge with supporting this theory is to ensure that the new terms introduced from axiom instantiation are bounded. We don't want the solver to create terms .
For every sub-term of the form , where is not instantiate the axiom:
For every term of sort instantiate the axioms:
The theory of arrays in SMTLIB is formally based on two functions select
and store
and an axiomatization
for every array , index and value . Alluding to the intent that the theory of arrays is useful for modeling arrays in programming languages will henceforth use when we mean .
Z3 reduces the theory of arrays to reasoning about uninterpreted functions. It furthermore treats arrays as function spaces, which assuming more axioms about arrays. The first-order theory of arrays axiomatized above enjoys compactness and so the following formula is satisfiable2
The same formula is not satisfiable when arrays range over function spaces. The distinction is only relevant when checking satisfiability of quantified formulas over arrays. In addition to functions and Z3 includes built-in functions , and the operator and lambdas. It includes also the function that provides a witness term for distinct arrays, and for accessing a default value.
The central functionality of the decision procedure for arrays is to ensure that a satisfying model under the theory of EUF translates to a satisfying model in the theory of arrays. To this end, the main service of the theory solver is to saturate the search state with reduction axioms for array terms that admit beta-reduction. We call these terms terms and they are defined by the beta-reduction axioms:
The reduction into EUF, is then, in a nutshell, an application of the following inference rule:
together with an inference rule to enforce extensionality
The decision procedure enforces several throttles [17] to avoid instantiating axioms when they are redundant. We here describe the main data-structures used by the instantiation engine.
The decision procedure maintains for every array node the following sets
When is merged with , and is the new root, the sets from are added to . The merge also looks for new redexes when and are merged:
For enforcing , the solver only instantiates the axiom because the axiom is instantiated eagerly when the term is created.
Extensionality is enforced on pairs of array terms , that are in shared positions. An array term is in a shared position when it occurs on one side of an equality or under a function that is not part of the theory of arrays. Furthermore, to ensure congruence for lambdas, if is a lambda term in a shared position and is in a shared position, the axiom is instantiated. Thus, lambda equality is at the time of writing enforced by a reduction to quantified formulas.
The theory of algebraic data-types is an SMTLIB2 standard [2]. The theory
allows declaring a datatype
sort, or a set of mutually recursive datatypes
sorts.
The elements of an algebraic datatype are the least set generated by the constructors in the type declaration.
You can declare algebraic data-types in SMTLIB2 using the declare-datatypes
command
(declare-datatypes ((Tree 1) (TreeList 1))
(
(par (T) (Tree leaf (node (value T) (children (TreeList T)))))
(par (T) (TreeList nil (cons (car (Tree T)) (cdr (TreeList T)))))
)
)
A legacy format that is less flexible, but a bit easier to formulate, is also available.
(declare-datatypes (T) ((Tree leaf (node (value T) (children TreeList)))
(TreeList nil (cons (car Tree) (cdr TreeList)))))
Z3 supports datatypes nested with sequences and arrays. The example below uses `Stmt' nested in a sequence.
(declare-sort Expr)
(declare-sort Var)
(declare-datatypes ((Stmt 0))
(((Assignment (lval Var) (rval Expr))
(If (cond Expr) (th Stmt) (el Stmt))
(Seq (stmts (Seq Stmt))))))
For arrays, datatypes are allowed only in the range of arrays.
Provide example with nested arrays
We will in the following use the notation for a constructor, for example Assignment
is a constructor;
for accessors corresponding to , for example lval
and rval
are accessors
corresponding to the constructor Assignment
. The predicate symbol is true if the term
is equal to a term headed by the constructor . The construct
assigns the term to an accessor field for the term . Note that it is possible to apply an
accessor to a data-type constructor that does not match. For example, taking the head of an empty list,
. The interpretation of is not fixed by the
theory, instead is treated as an uninterpreted function.
The theory solver for algebraic datatypes is implemented by adding theory axioms on demand. The theory axioms rely on the theory of uninterpreted functions. It builds a finite interpretation for every node of sort data-type and adds theory axioms on demand. Every E-node of data-type sort is assigned a constructor representation, initially null. If is equated to a node whose function is labeled by a constructor , the representative for is labeled by .
The saturation rules add theory axioms on demand to enforce that the theory of algebraic datatypes axioms are satisfied in a consistent state. Accessor axioms, update-field axioms are added as soon as terms are created. Recognizer axioms are added when the truth value of a recognizer is asserted. If a datatype has a single constructor, the recognizer is asserted as a unit. The occurs check is applied lazily. It ensures that the partially constructed model corresponds to acyclic algebraic datatypes.
If is a constructor node assert the axioms
If is of the form , where is an accessor for constructor , then assert the axioms
For recognizer atom we have to ensure that a node is assigned to a constructor that is consistent with the assignment to the recognizer atom. Thus, if is asserted to true, then must be equated to a term with constructor . Conversely, if is assigned with a constructor , then is false. The decision procedure ensures this correspondence lazily. If is asserted to true, then it ensures the axiom
where are the accessors for .
If is asserted to false, but equal to a node that is headed by , then it creates the conflict clause:
Something about covering constructors so all possible constructors are tested.
An occurs check violation is detected when there is a state satisfying the properties where each is of the form . Occurs checks are performed lazily in a final check.
Dually to saturation rules, the theory solver builds a finite model for algebraic datatypes. Model construction is interleaved with the CDCL(T) engine in a way that has subtle consequences. Model construction drives the constraints to a goal state where all terms ranging over an algebraic datatype have been assigned a constructor and all saturation rules have been applied. If there is a term that has not been assigned a constructor, the solver attempts to first guess an assignment to the term based on a non-recursive base case for the data-type (for mutually recursive data-types some types don't have non-recursive base cases, it is possible to drive towards a nested sub-term that has). The fact that the solver can guess a fixed base case constructor during model construction relies on the assumptions for theory combinations: other theories need only distinguish whether data-type terms are equal or distinct. The shape of terms is opaque to other theories. If a term cannot be assigned a non-recursive base case (say nil), it is assigned a non-recursive constructor (say, cons), that eventually allows assigning to a term that is arbitrarily deep and therefore can be distinct from any set of other terms .
The approach for model construction does not work if we introduce a sub-term predicate . The theory of algebraic data-types can be extended to the subterm relation while remaining decidable. If range over an algebraic data-type with two base case constructors and , and . Then unfairly guessing only before guessing cons, but neglecting leads to non-termination of a model constructing decision procedure with the sub-term relation.
The theory of algebraic data-types admits partial quantifier elimination.
Present MBP for ADT here specifically?
A prime example of a hybrid theory in Z3 is the theory of strings, regular expressions and sequences.
The theory of strings and regular expressions has entered mainstream SMT solving thanks to community efforts around standardization and solvers. The SMTLIB2 format for unicode strings [2]. It integrates operations that mix equational solving over the free monoid with integer arithmetic (for string lengths and extracting sub-strings). Regular expression constraints furthermore effectively introduce constraints that require unfolding recursive relations. Z3 uses symbolic derivatives [43] to handle regular expressions, noteworthy, with complementation and intersection handled by derivatives.
A second prolific example of a hybrid theory is Z3's model-based quantifier instantiation engine (MBQI). Here, a theory is encoded using a quantifier. The MBQI engine supports extensions of Bradley-Manna-Sipma's array property fragment [11] that effectively combines arithmetic with uninterpreted functions.
Floating point semantics can mainly be defined in terms of bit-vector operations. The solver for floating points uses this connection to reduce the theory of floating points to bit-vectors. There are also operations on floating points that connect with the theory of Reals. For formulas combining floating points and reals, the theory solver bridges bit-vector values with bounds over the Reals.
This section pops the abstraction level and examines the main data-structures, organization, algorithms and heuristics that are central to implementation of Z3. We examine the data-structures used for terms and formulas and then describe the setting for how theory solvers interact with the main CDCL(T) engine.
Z3 treats terms and formulas as a single entity, expressions. Expressions of Boolean type can be used as formulas, but they can also be nested within non-Boolean terms, as arguments to functions. Expressions are hash-consed, meaning they are unique up to term structure. It uses a hash-table when constructing a fresh term, such that if the same function is applied to the same arguments as a previously constructed term, the result of creating the term is the same pointer as the old term.
Variables that are to be bound by quantifiers are indexed using de-Brujin conventions, such that the index used for the innermost quantified variable is 0, and incremented outwards. For example, uses de-Bruijn indices 0 for and 1 for , and 2 for . To spare users an error-prone process of keeping track of indices, the external API offers a facility to create a quantified expression using a set of constant expressions instead of variables.
A representation of Z3 terms using the Koka language is as follows
type Expr
Var{ index : int; sort : Sort }
App{ f : FuncDecl; args : list<Expr> }
Quantifier{ b : Binder; decl : list<Declaration>; body : Expr; ...}
From input and during pre-processing assertions are represented in a structure of the form
For a standard input assertion the structure is initialized as . The proof of is that it was asserted. There are no dependencies for .
Input assertions can also be tracked for unsatisfiable cores.
The SMTLIB2 contains syntax for adding named formulas. The formulas are tracked by their names
and the solver can produce unsatisfiable cores over the tracking names.
Internally, the names are treated as (fresh) Boolean literals. Thus, formulas can also be
tracked by arbitrary formulas. The programmatic API contains a function Z3_solver_assert_and_track
(and variants surfaced in various programming languages) that allows asserting a formula modulo
a tracking assumption. When adding tracked formulas using tracking assumption , the
structure is initialized as .
Tactics (8) perform updates on the triple representation by applying inferences. Properly producing proof objects for tactics and tracking dependencies is supported in a best-effort manner: several tactics are disabled when the user requires proof objects or tracking dependencies. Dependencies can be viewed as light-weight proof objects. The mechanism for tracking dependencies is based on tracking a cone of influence, by taking unions, of formulas used to derive a new consequence.
While the input to Z3 is a set of formulas, the core solver works with clauses and E-nodes. The internalization process converts formulas into the internal data-structures bottom-up. Every sub-term that corresponds to a Boolean connective is compiled, thanks to the Tseitin transformation, into a fresh literal with associated set of clauses that define the literal. The terms from Boolean connectives may be assigned E-nodes, but the nodes do not participate in congruence rules, nor are, by default, allowed to be merged with the constants or : The CDCL(T) engine already enforces congruence rules and consistency with assignments to . The default is overridden if a term occurs as an argument to a non-Boolean connective. For example, in the term , the node merges with when the truth value to the literal is propagated. In this way, congruence closure can ensure consistency with respect to occurrences of .
Compilation of terms into E-nodes uses a top-down traversal of terms. The top-down traversal is replicated in each theory solver, where the compilation of terms depends on the solver. The arithmetic solver, for instance, compiles the term into a node and adds the basic row . Top-down compilation does not play well with small static stack sizes allocated by runtimes. The new core in Z3 therefore introduces a visitor pattern to allow top-down processing without using recursion stacks.
E-nodes are compiled dynamically during search. When a quantifier is instantiated or a solver introduces a new lemma that uses new terms, the E-nodes corresponding to new terms are introduced at a search level that may later be popped. Learned clauses may refer to these new terms in a scope where they are no longer internalized into E-nodes. The core solver therefore interacts with the scope mechanism of the CDCL solver to replay literals that are defined in deeper scopes.
In Z3, the solver for the theory of equality acts as a gate-keeper between the SAT solver and solvers for other theories. As Section 2.1 outlines, the SAT solver maintains a set of ground clauses and performs case splits and propagations and in this process consults the theory solver for the next propagation, case split or whether is a theory consistent model of .
There is a good reason why the theory of equality is central: this theory is shared among all other theories. Disjoint theory solvers establish satisfiability by reconciling equalities among shared sub-terms.
Every E-node has an attached set of theory solvers. A theory solver for a theory gets attached to a node when
Then, when a Boolean node is assigned to or by the CDCL solver, the truth assignment to is broadcast to all solvers attached to . Similarly, when two nodes are merged, and the theory is attached to a sibling of and a sibling of , then the equality is broadcast to .
In some cases, the core also broadcasts disequalities between E-nodes. Solvers cannot rely on complete propagation of disequalities, it is generally too costly to enumerate all pairwise disequalities. Propagation of disequalities is triggered by when the CDCL(T) core asserts a disequality , or when a theory solver propagates a disequality.
The set of terms tracked by a given theory is always a subset of all terms used in a formula. Particularly, if a formula encodes bit-vector properties, there are thousands of terms used for encoding Boolean formulas. Those terms are irrelevant to the theories. Each theory therefore maintains a dense encoding of the terms it tracks by maintaining an array of E-nodes associated with the theory. The index into this array is the identifier of the theory variable. This identifier is stored in the E-node data-structure so that when the core broadcasts an equality about it accesses the theory variables and that are indices to the array and broadcast . The original nodes are accessed from the theory solver's array, together with other attributes of that are specific to the theory solver. For example, the array solver maintained several attributes, such as the select terms where an array node occurs.
Theory solvers generate theory lemmas, axioms and unit propagations. Theory axioms follow the following life-time contract:
Axioms are never garbage collected within the current scope.
Axioms are always garbage collected when the current scope is popped.
Lemmas are possibly garbage collected within the current scope.
Lemmas that survive garbage collected are re-asserted with lower scopes if they
Theory propagations are not replayed. The solver also behaves differently when relevancy propagation is set to 2: then theory lemmas that would otherwise correspond to binary clauses whose atomic formulas have been internalized at the base level (so they do not have to be re-created during backtracking) are added to the watch lists directly without creating a clause. The watch list is not garbage collected so they persist.
The main theories, arrays, arithmetic, algebraic data-types, uninterpreted functions are signature disjoint. A central ingredient in combining theory solvers into a complete solver is to reconcile equalities between shared sub-terms. Z3 uses a method called model-based theory combination to broadcast equalities on demand.
Example 10.
Consider the following formula that combines arithmetic for integers , and the uninterpreted function .
The purified version of the formula takes the form, where we introduce a proxy variable for the term :
Purification is a formal tool to help us reason about theory combinations. In practice, no new variables are introduced. The proxy variable is just a shorthand for the term .
Returning to our example, the theory solver for and the solver for arithmetic only need to reconcile equalities between shared sub-terms in order to establish satisfiability of the overall formula. The theory of equality that handles is agnostic to the specific values of but needs to know which values are equal and disequal.
Example 11.
Suppose we have a model over arithmetic that sets and a model for that
sets ,
.
The two models cannot be reconciled, as the equality from the arithmetical model contradicts the
partition imposed by the EUF model.
The way Z3 reconciles the equalities is by inspecting the E-graph on final checks. If a theory solver induces an equality between two nodes , but (the nodes are not in the same equivalence class), then the theory solver creates a fresh atom and sets the phase of the atom to . The atom is introduced to the CDCL(T) solver that performs a case split on it. The phase of a literal is a flag that instructs the solver to choose the case split. It first case splits on and only backtracking and propagation may force the case .
Example 12.
The arithmetic solver creates the literal and the CDCL(T) core assumes this equality.
Since are shared, the equality is known to both theories. The equality contradicts the EUF
theory, and it infers . The arithmetic solver is able to produce a new model, however,
where . It creates the equality atom , which does not contradict the
EUF model.
Note that equalities for finite domain theories, such as bit-vectors, are determined by complete propositional assignments in final states that are visible to all theories because all terms are assigned values from the finite domain.
The E-matching and MBQI methods for instantiating quantifiers, and the methods for solving the array theory, rely on essentially checking constraints that a satisfying model must meet. The number of quantifier instantiations and axiom instantiations grows with the number of constraints and they are often more harmful, as they increase the search space, than of use. Z3 uses relevancy filtering to filter the Boolean assignments that are made visible to the theory solvers. It is inspired by search in semantic Tableau, where establishing satisfiability of a disjunction is split into separate branches that process the disjuncts in isolation.
In Z3, relevancy filtering is set up during clausification. The result of clausifying a Boolean formula is a set of root clauses and a set of definition clauses. The goal is to establish a labeling of literals as relevant such that
the set of relevant literals satisfies roots
there is a set of blocked literals that can be used to satisfy the clauses in defintions independent of their real assignment.
The idea is that the definition clauses are obtained from Tseitin transformation so they can be grouped by the blocked literal that was used to introduce them. For example, when clausifying we have the clauses
then the literal for is blocked. And recursively for clauses introduced for if they use Boolean connectives at top level.
Relevancy filtering applies a set of state transitions to update whether literals and terms are relevant and should be exposed to the theory solvers. The transitions for setting a literal and node to relevant are:
Roots and definitions are updated as well:
When is assigned relevant, then all definitions where occurs negatively are added to roots.
When a clause is added to roots then:
When a clause is added to definitions and it contains a negative relevant literal, the clause is added instead to roots.
When is set relevant, arguments that don't represent Boolean connectives are set relevant. If-then-else terms are treated as Boolean connectives because they are compiled into clauses where, the clausal relevancy propagation takes effect.
Efficient theory propagation can have a profound effect on overall efficiency of solving. Congruence closure is therefore tightly integrated with Boolean propagation: when equality reasoning can infer that an equality atom is true or false, it is useful to propagate this fact eagerly. Congruence closure in Z3 therefore has special handling of equality literals. An equality is both a term with the binary function and an atom. Other atoms are also tracked. For example, if is a unary predicate then the E-node data-structure also tracks the Boolean variable that is associated with . When is equated to and is assigned to true or false, then congruence closure propagates the assignment on to . The propagation is justified by the premises for the equality . Any overhead of propagating within the E-graph is outweighed by the benefits it brings to pruning the search space.
Basic implementations of CDCL(T) as presented in Section 2.1 produce a proof system that has the power of regular resolution for propositional logic. The proof system is relatively weak even for EUF when search is limited over the space of initial set of literals from a formula.
Example 13.
The following clauses are unsatisfiable over the theory of equalities, but the shortest resolution proof that only uses the atomic formulas from the input is in the order of clauses. The resolution proof involves clauses with all combinations of and for ranging from 0 to 99.
The proofs are linear if we admit clauses using fresh literals of the form
Z3 dynamically introduces such auxiliary clauses based on transitivity of equality and congruence rules of the form
The axioms are introduced for uninterpreted functions and related axioms are introduced for bit-vectors
where are compiled into new propositional literals for each bit position of bit-vectors .
Dynamic Ackerman axioms can also, in theory, have the same effect for other theories, such as arithmetic.
Example 14.
The following arithmetic formula has no short vanilla CDCL(T) proof, but has a short proof if adding axioms for transitivity of inequality.
In Z3, the EUF and bit-vector theories track the number of times transitivity of equality and congruences are applied in conflicts. It introduces the Ackermann axioms based on thresholds. A user can control the thresholds. An important question for applications is when is dynamic Ackermann reduction useful? The cost-benefit of introducing new literals into the search space has to be weighed. Importantly, for applications where the main expectation is that formulas are satisfiable, dynamic Ackermann reduction can be detrimental. Users can disable dynamic Ackermann reduction to gain significant performance improvement.
You can declare and reason about recursive functions with Z3. The SMTLIB2 standard introduces recursive function definitions and you can add recursive function declarations over the programmatic APIs.
In the following, we define two recursive functions length
(the length of a list) and nat-list
(a predicate whether a list
of integers contains only non-negative integers).
Example 15.
(define-fun-rec length ((ls (List Int))) Int
(ite ((_ is nil) ls) 0 (+ 1 (length (tail ls)))))
(define-fun-rec nat-list ((ls (List Int))) Bool
(ite ((_ is nil) ls)
true
(and (>= (head ls) 0) (nat-list (tail ls)))))
(declare-const list1 (List Int))
(declare-const list2 (List Int))
(assert (> (length list1) (length list2)))
(assert (not (nat-list list2)))
(assert (nat-list list1))
To check satisfiability of a set of ground assertions of these functions, Z3 unfolds the definitions incrementally. It uses iterative deepening to control the unfolding depth. Internally, it uses assumption literals that are introduced by the theory solver for recursive functions to control the unfolding depth.3
The main CDCL(T) solver is now wrapped in an outer loop where assumption literals are supplied by theory solvers, and unsatisfiable cores that contain these assumption literals are presented to the theory solvers that can then extend their depth bounds. There is an advantage to maintaining finer-grained depth bounds, that is, separate assumption literals for each recursive function and each recursive branch within a function [40].
The theory of sequences also uses iterative deepening to control the search for finite length sequences.
The sequence (string) theory also uses a contains
predicate that captures when a sequence is a subsequence of another.
Positive occurrences of contains
can be reduced to solving string equalities, but negative occurrences of contains
are more tricky. They are treated as recursive functions over sequences. Their unfolding is controlled by size bounds
of the arguments to contains
.
The verdicts of satisfiability or unsatisfiability do not reveal by themselves any information about the reason for the outcome. Model object represent certificates for satisfiability, and proof objects or unsatisfiable subsets certificates for unsatisfiability. Furthermore, retrieving and using models is integral to many applications. For example, symbolic execution uses satisfying assignments to input variables of a path condition to produce new inputs to a program that explore a so-far uncovered execution branch. Propositional models are simply assignments of true/false to propositional variables. Similar, models of linear arithmetical formulas are assignments of integers or rationals to arithmetical variables. Model construction is more involved when considering a combination of theories, non-linear arithmetic formulas and quantified formulas.
This section describes how models are constructed from a satisfiable state. A starting point for model construction in Z3 is a T-consistent assignment to atomic formulas such that all ground clauses are satisfied and the conjunction of assignments to atomic formulas are consistent with respect to all theories. Model construction then works by layers:
For each E-node it collects dependencies of E-nodes that must be assigned a value before can be assigned a value. Dependencies are specific to theories. There are no dependencies when ranges over Bool, Int, or Real.
Dependency collection is well-founded: for arrays, the dependencies come from the range of the array sort; for algebraic datatypes dependencies are sub-terms; for sequences dependencies are characters.
Dependencies are then topologically sorted such that nodes without dependencies are processed first and assigned values.
Boolean nodes are evaluated by the SAT solver. They are assigned or . Nodes ranging over an uninterpreted sort are assigned to an abstract value that is distinct from values from other equivalence classes. Values of nodes with associated theory solvers are assigned by the solvers.
Int and Real nodes are assigned values assigned by the arithmetic solver. For non-linear arithmetic formulas, the values of a Real can be an algebraic numeral. Algebraic numerals are represented as a 'th root to polynomials (whose coefficients can be algebraic numerals). They can be evaluated to decimal representations using an arbitrary degree of precision.
An array node is assigned the value .
The algebraic data-type is assigned .
After all nodes have been assigned a value, model construction assigns interpretations to uninterpreted functions. Uninterpreted functions are assigned to lookup tables. is assigned the value , where is one of the values from . Z3 synthesizes interpretations of uninterpreted functions that go beyond finite lookup tables. In these cases, it mines quantified formulas for macros. For example, asserting creates the function interpretation for , where is a fresh function. The function is replaced everywhere by the macro definition.
This completes the description of model extraction in Z3.
We make the following observations
Models provide explicit interpretations of terms and functions using values. We call such models constructive symbolic models. There are cases where first-order saturation theorem provers can determine that a set of first-order clauses are satisfiable, without presenting a symbolic model. Z3 does not include such saturation criteria. Model construction for the Horn clause solver SPACER also produces symbolic models.
Models produced by the solver map terms to values. Given a solution to , the solver will assign to a numeral that is one greater than the numeral assigned to . Pre-processing, on the other hand, may choose to solve for and record the solution . In this case, the model for can be retrieved as . Z3 supports a setting to force to be evaluated to a numeral if the user wants this.
add example
Values to base types, such as Int, Real are dictated by the solvers. Values to terms of type Bool are controlled mainly by the SAT solver.
Ensuring diverse model values for Booleans is partially possible by controlling options on the SAT solver [@SoosUniSAT].
There are no corresponding knobs for values from theories. For instance, for linear arithmetic based on dual simplex, values are determined by
an algorithm that seeks to find solutions along vertices in a polytope, which in layman's terms means
finding values to bounded variables at their bounds.
Thus, configurations such as smt.arith.random_initial_value=true
, provide no strong assurances
(furthermore, only the solver configured by setting smt.arith.solver=2
for arithmetic responds to this configuration).
(set-option :smt.arith.solver 2)
(set-option :smt.random_seed 5)
(set-option :smt.arith.random_initial_value true)
(declare-const x Int)
(declare-const y Int)
(push)
(assert (> x y))
(check-sat)
(get-model)
A main knob for controlling model values are over optimization, [@NuZ]. By specifying one or more optimization objectives you can control model values of selected terms to be minimum or maximum.
add optimization example
Z3 includes auxiliary definitions in models. For example, if you define a recursive function, the recursive function definition is part of the model that is displayed in SMTLIB2 mode.
The new core in Z3 produces proof logs that extend DRUP proofs. SAT solvers produce mainly DRUP proofs. The DRAT format (RAT = Reverse Asymmetric Tautology, while RUP = Reverse Unit Propagation, and D = with Deletion) is widely adopted for certifying proofs of SAT solvers. Z3 extends the DRUP format for SMT. It uses an extension of the SMTLIB2 format to record proof traces. We introduce it by an example using EUF.
From SMTLIB:
(declare-fun f (Int) Int)
(define-const $24 Int (f x))
(define-const $25 Int (f $24))
(define-const $26 Int (f $25))
(define-const $27 Bool (= $26 x))
(define-const $28 Bool (= $25 x))
(assume $27 $28)
(define-const $30 Int (f $26))
(define-const $31 Int (f $30))
(define-const $32 Int (f $31))
(define-const $33 Bool (= $32 x))
(assume (not $33))
(declare-fun rup () Proof)
(infer (not $33) rup)
(declare-fun euf (Bool Bool Proof Proof Proof Proof) Proof)
(declare-fun cc (Bool) Proof)
(define-const $42 Bool (= $32 $30))
(define-const $43 Proof (cc $42))
(define-const $40 Bool (= $31 $24))
(define-const $41 Proof (cc $40))
(define-const $38 Bool (= $30 $25))
(define-const $39 Proof (cc $38))
(define-const $36 Bool (= $24 $26))
(define-const $37 Proof (cc $36))
(define-const $34 Bool (not $33))
(define-const $44 Proof (euf $34 $28 $37 $39 $41 $43))
(infer (not $28) $33 $44)
(infer (not $28) rup)
(infer $27 rup)
(declare-fun euf (Bool Bool Proof Proof Proof) Proof)
(define-const $49 Bool (= $32 $26))
(define-const $50 Proof (cc $49))
(define-const $47 Bool (= $31 $25))
(define-const $48 Proof (cc $47))
(define-const $45 Bool (= $24 $30))
(define-const $46 Proof (cc $45))
(define-const $51 Proof (euf $34 $27 $46 $48 $50))
(infer $33 $51)
(infer rup)
It is possible to use Z3 to parse this format and pretty print it from Python:
assume(Or(f(f(f(x))) == x, f(f(x)) == x))
assume(Not(f(f(f(f(f(f(x)))))) == x))
infer(rup, Not(f(f(f(f(f(f(x)))))) == x))
infer(euf(Not(f(f(f(f(f(f(x)))))) == x),
f(f(x)) == x,
cc(f(x) == f(f(f(x)))),
cc(f(f(f(f(x)))) == f(f(x))),
cc(f(f(f(f(f(x))))) == f(x)),
cc(f(f(f(f(f(f(x)))))) == f(f(f(f(x)))))),
Or(Not(f(f(x)) == x), f(f(f(f(f(f(x)))))) == x))
infer(rup, Not(f(f(x)) == x))
infer(rup, f(f(f(x))) == x)
infer(euf(Not(f(f(f(f(f(f(x)))))) == x),
f(f(f(x))) == x,
cc(f(x) == f(f(f(f(x))))),
cc(f(f(f(f(f(x))))) == f(f(x))),
cc(f(f(f(f(f(f(x)))))) == f(f(f(x))))),
f(f(f(f(f(f(x)))))) == x)
infer(rup, False)
The following script was used to pretty print the proof
from z3 import *
def on_clause(j, deps, c):
if deps:
print(deps[0], j, c, deps[1:])
else:
print(j, c)
s = Solver()
OnClause(s, on_clause)
s.from_file("name-of-file-with-proof-log.smt2")
There are the following layers to proof logs in Z3:
Propositional: the main search engine is based on CDCL(T) where a SAT solver performs case analysis over a
propositional abstraction of the SMT formula. The proof steps produced by the SAT solver are rup
(reverse unit propagation)
and del
(clause deletion) steps.
Assumptions: input assumptions are clauses that are based on the input formula. If Z3 uses pre-processing simplifications, then the assumptions are obtained from the original input by a sequence of pre-processing steps. Pre-processing can be turned off or justified separately using the legacy proof format. A tight integration of proofs from pre-processing and proof logs is currently not available.
Theory inferences: Theory inferences are logged as infererences that are justified by theory axioms. Each theory supports its own set of theory axioms and the format for how theory axioms are justified is theory dependent. The solver for EUF combines inferences from theories using equality propagation.
The core of the CDCL(T) solver is a core based on EUF reasoning. It works as a dispatch between theory solvers and manages shared equalities.
The CDCL core integrates with the EUF solver when resolving conflicts using the following function.
It takes a literal that is assigned to true. The literal can also be (or false), in which case the state of the theory solvers is conflicting.
The requirement is that for a literal :
The explain function returns a list of literals, assigned by the CDCL core that justify the propagation of . The literal is associated with a reference to a theory justificadtion or an EUF justification.
Each theory solver also implements a explain
specific for the theories.
The internal signature for explain
:
where
The E-graph is used to explain equalities, or explain conflicts.
The list of constraints and equalities have their own justifications. The dependencies on equalities
is well-founded so applying explain
recursively produces just a list of literals.
We assume that each theory implements a hint function that logs a justification for the inference it made. It produces hint to justify the clauses
And(lits @ [a == b for (jst, (a, b)) in jsts]) => x == y where (lits, jsts, cc) = euf-explain(x, y)
And(lits @ [a == b for (jst, (a, b)) in jsts]) => false where (lits, jsts, cc) = euf-explain(false)
And(lits @ [a == b for (a, b) in eqs]) => L where (lits, eqs, thjst) = theory-explain(L)
The version of explain that is exposed to the SAT core is as follows:
explain(L) =
match justification(L) with
| EUF -> explain(false)
| jst -> explain(jst)
explain(eq_or_false) =
let (lits, jsts, cc) = euf-explain(eq_or_false)
lits @ concat([explain(jst) for (eq, jst) in jsts])
explain(jst) =
let (lits, eqs, thjst) = theory-explain(jst)
lits @ concat([explain(eq) for eq in eqs])
To create certificates we need a decomposition.
proof(L) =
match justification(L) with
| EUF -> yield from proof(false)
| jst -> yield from proof(jst)
proof(eq) =
let (lits, jsts, cc) = euf-explain(eq)
for (eq, jst) in jsts:
yield from proof(jst)
yield lits & [eq for (eq, jst) in jsts] => eq by cc
proof(jst) =
let (lits, eqs, thjst) = theory-explain(jst)
for eq in eqs:
yield from proof(eq)
yield lits @ eqs => literal(jst) by thjst
We introduced a new function
that produces either a literal propagation or conflict (where the literal is ).
Equalities that are propagated by theory propagation satisfy euf-explain(eq) = ([], [(eq,jst)], [])
.
For these cases there is no need to produce the implication eq => eq
.
Implementation-wise, the justification for EUF is created on the fly when calling euf-explain. It is the product of
combining literals, equations and the congruence closure steps.
The justification for theory axioms is all encoded in thjst
.
The two versions of explain
can sometimes be combined: if the explanation for a literal propagation or a conflict only uses
a single theory axiom or single EUF proof it is tempting to interleave the functionality. It gets dicier when it comes to
controlling whether the E-graph should produce congruence closure justifications.
Proposition 1.
Not(And(explain(L)))
can be proved using RUP from clauses produced by proof(L)
.
We don't always need to justify a clause using RUP, but can use justifications directly.
Proposition 2.
Suppose that justification(L) = EUF
and euf-explain(false) = (lits, [], cc)
.
Then the literals produced in the clause from proof(L)
coincide with explain(L)
.
The same is the case when justification(L) = jst
and theory-explain(jst) = (lits, [], thjst)
.
There is only one kind of axiom for EUF inferences. The example indicated that EUF axioms are labeled as
euf
terms that take a single disequality, a set of equalities and a set of equalities under a function named cc
.
The idea is that the conjunction equalities implies the negated equality by taking the equivalence class of terms
listed as equalities and under cc
. Furthermore, each equality under a cc
is justified from all previous equalities
and cc
terms before it by a single application of the congruence rule. Thus, if a cc
equality is of the
form cc(f(x,y) == f(z,y))
, then x
, z
are equal modulo the previous seen equalities, and therefore is equal to .
A variant of cc
, called comm
, is used for congruence of binary functions that are commutative.
For example, comm(f(x,y) == f(y,z))
is justified if f
is commutative and the equality x == z
(and y == y
) was justified.
A validator for equality axioms can check that each equality and disequality under the euf
justification corresponds
to a literal in the clause that is inferred from the theory axiom. It can then check that the clause is a tautology for EUF
by creating equivalence classes of the equalities and congruences it is supplied with. It checks that each application
of a congruence is well-formed (the arguments to f
are equivalent), and then it can check that the left and right side
of the disequality are in the same equivalence class.
Example 16.
Consider the inferece
euf(Not(f(f(f(f(f(f(x)))))) == x),
f(f(f(x))) == x,
cc(f(x) == f(f(f(f(x))))),
cc(f(f(f(f(f(x))))) == f(f(x))),
cc(f(f(f(f(f(f(x)))))) == f(f(f(x)))))
The conjunction of
Not(f(f(f(f(f(f(x)))))) == x), f(f(f(x))) == x)
is unsatisfiable.
To justify this consider first the
equality f(f(f(x))) == x
and three applications of congruence closure to establish
f(f(f(f(f(f(x)))))) == f(f(f(x))))
. By transitivity of equality, it follows that f(f(f(f(f(f(x)))))) == x
and therefore the negated equality is conflicting.
Arithmetic solver produces (so far) theory axioms of the form
<rule> (<numeral> <literal>)*
where <rule>
is one of farkas
, implied-eq
, bound
, cut
, nla
.
A <literal>
is an equality, inequality. For the implied-eq
rule, the last literal is a disequality (negated equality).
The rules are assumed to be checkable using basic inferences. They are implemented within Z3 as part of a self-validator.
farkas
: The set of inequalities are multiplied by the numeral coefficients and added to form a single inequality. The equalities are solved by Gauss-Jordan elimination and the resulting solution is substituted into the inequality. The resulting inequality is expected to be infeasible.
bound
: all but the last inequality are treated similar to the Farkas rule. Then the resulting inequality is subjected to a cut rule and the last inequality is checked to be contradicotry with the reduced inequality.
implied-eq
: the last literal is a negated equality. The equalities are solved using Gauss-Jordan elimination, and the inequalities are assumed to imply equalties. These implied equalities can be retrieved by applying exhaustive Fourier-Motzkin elimination. The resulting set of equalities obtained from the inequalities are then solved using Gauss-Jordan elimination. The solution is substituted into the last negated equality which should simplify to false.
There is currently no self-validator for cut
and nla
rules.
There are a few sources for cut
rules: Z3 applies a Gomoroy-Chvatal cut as described in
the technical report associated with [24], and cuts based on [14] (see 3.3.2.5),
as well as a GCD filter 3.3.2.1. We leave it to future work to establish self-validators for cuts.
It is noteworthy that SMTInterpol's proof format does not directly have to deal with cuts. The SMTInterpol solver
does not assert inequalities that come from cuts but instead creates a fresh atom. When the cut formula is created from a
tableau where non-basic variables are at their bounds it can only have one truth assignment; the existing proof rules are sufficient to establish this.
There are several sources for nla
rules and an elaboration into
certificates that are efficient to check is left to future work.
Example 17.
The inference
farkas(1, 0 == y + -1*z, 1, y + -1*u <= 0, 1, z + -1*u >= 1)
Justifies the clause
[Not(y + -1*u <= 0), Not(z + -1*u >= 1), Not(0 == y + -1*z)]
because normalizing the inequalities to - y + u >= 0, z - u >= 1
(multiplying both by 1)
and adding them produces z - y >= 1
. However, applying Gauss-Jordan elimination to
the equations produces a solution where z
and y
are equated and therefore the inequality z - y >= 1
simplifies to 0 >= 1
.
The inference
implied-eq(1,0 == y + -1*u, 1, 0 == y + -1*u, 1, Not(y == u))
Justifies the clause
[Not(0 == y + -1*u), Not(0 == y + -1*u), y == u]
because Gauss-Jordan elimination applied to the set of equality premises produces a solution that equates y
and u
.
The dedicated engines QSAT 2.4 and SPACER 2.3 handle classes of quantified formulas. The QSAT engine solves formulas using quantifier alternation over theories that allow quantifier elimination using model-based projection. The SPACER engine solves constrained Horn clauses over a limited set of theories (with arithmetic being central and including emerging support for algebraic datatypes, arrays and bit-vectors).
Quantifier reasoning is also available within the CDCL(T) core. Here it relies on quantifier instantiation engines. We describe the main engines in use.
The quantifier instantiation engines rely on auxiliary annotations with quantifiers. We elided them when first introducing terms in Section 4.1. The fields that are of relevance to quantifier instantiation are expanded below:
type Expr
Var{ index : int; sort : Sort }
App{ f : FuncDecl; args : list<Expr> }
Quantifier{ b : Binder; decl : list<Declaration>; body : Expr;
patterns : list<list<Expr>>;
nopatterns : list<Expr>;
weight : int;
qid : symbol }
A set of multi patterns are used by the E-matching engine to search for instantiations. Quantifiers can be manually annotated by patterns or if no patterns are annotated, z3 infers a set of patterns based on smallest subterms. More than one pattern might be needed to find a full instantiation of all quantified variables.
A set of non-patterns that identify sub-terms that cannot be used as patterns. It provides a user with control over quantifier instantiation by the E-matching engine.
A weight to provide priortization of quantifier instantiation.
A quantifier identifier (qid) which can be used to trace instantiations of a quantifier in logs.
Let us start with an example of quantifier instantiation using E-matching in action.
Example 18.
Consider the formula
The smallest subterm properly containing the bound variable is in . The only other occurrence of the function is in the equality . Based on the ground equality it follows so we can instantiate by and infer
The formulas are now ground unsatisfiable.
Quantifier instantiation using E-matching in Z3 serves a sweet spot by producing instantiations that take congruences of equalities into account, quickly, across a large set of terms and incrementally.
E-matching is a first-order matching algorithm that takes a congruence closure into account when checking for equality. We can describe a basic E-matching algorithm as taking
and producing substitutions that represent the set of matches for n with respect to pattern.
def match(pattern, , ):
pattern = pattern
if pattern == :
yield
elif is_var(pattern):
yield pattern
else:
f(patterns) = pattern
for f(terms) in :
# e.g., with same head function symbol f
for in matches(patterns, terms, ):
yield
def matches(patterns, terms, ):
if not patterns:
yield
return
for in match(patterns[0], terms[0], ):
for in matches(patterns[1:], terms[1:], ):
yield
The same algorithm can be presented in equational form with an extra argument that accumulates substitutions.
The straight-forward E-matching algorithm is expensive when it is invoked from scratch for every possible candidate of pattern and node during search. To share work across patterns and terms, Z3 compiles patterns into a code tree data-structure that contains a dynamically compiled set of instructions to perform matching. The instructions can be regarded as a partial evaluation of the naive algorithm for fixed patterns. The instruction set allows merging code sequences from different patterns by sharing common prefixes. Substitutions are stored in registers, backtracking just updates the registers.
The abstract matching machine uses an array of registers to store sub-terms. Instructions are stored at program counters and each instruction refers to a next program counter.
Consider the following pattern
It compiles into a sequence of instructions as follows:
PC | Instruction | |
---|---|---|
init(f, ) | add arguments of to registers 1-4 | |
check(4,,) | check if is congruent to | |
bind(2, , 5, ) | bind terms in with to | |
compare(1, 5, ) | check if equals | |
check(6, , ) | check if is congruent to | |
bind(3, , 7, ) | bind terms in with to | |
yield(1,7) | output binding from | |
Let us trace the instructions for the term
PC | Instruction | ||
---|---|---|---|
init(f, ) | |||
check(4,,) | |||
bind(2, , 5, ) | |||
compare(1, 5, ) | |||
check(6, , ) | |||
bind(3, , 7, ) | |||
yield(1,7) | |||
If we instead use the term , we get the following execution
PC | Instruction | ||
---|---|---|---|
init(f, ) | |||
check(4,,) | |||
bind(2, , 5, ) | |||
compare(1, 5, ) | |||
check(6, , ) | |||
bind(3, , 7, ) | |||
yield(1,7) | |||
Patterns that share common (top-down) term structure can share code sequences.
This saves on common work.
Use the choice instruction to branch when patterns end up having different sub-terms.
Other instructions are possible,
forward pruning: lookahead multiple function symbols in immediate subterms before diving into any subterm.
to deal with multi-patterns, when matching more than one pattern at a time.
During search, congruence classes are merged and we would like to learn which merges should trigger new pattern match attempts. Z3 uses a filter called an inverted path index that tracks when a pattern is potentially useful for matching.
Example 19.
The pattern contains the subterm in position 2.
The merge between and can potentially trigger a new match when
contains a node labeled by , and has a parent labeled by ,
and occurs as a second position of .
Z3 pre-computes an index based on pairs like these and check if it is triggered for some pair and pattern when nodes are merged.
Z3 uses relevance propagation to harness quantifier instantiation. Relevance propagation attempts to emulate semantic tableau rules on top of the CDCL(T) engine. In a semantic tableau, a formula is decomposed based on top-level connectives. Depending on the connectives, it creates one or more branches. Different branches then contain only atomic subformulas that are relevant to establishing that the formula in the root is true. We list tableau rules for a minimal set of logical connectives as inference rules:
The rules are read top-down: To establish satisfiability of the formula at the root tableau rules create branches, separated by . A branch is closed if it contains a pair of complementary sub-formulas (one is the negation of the other). Otherwise a branch is open.
Relevancy propagation follows the flow of the inference rules:
When formulas are parsed and asserted, they are converted into clauses. Quantified formulas are treated as atomic formulas. They are opaque relative to the CDCL(T) engine and treated as if they are propositional atoms. It is only when quantified formulas are asserted, either at base level during search or during backtracking, that E-matching kicks in.
The E-matching engine reacts to the following updates to the state during search:
Note that all state updates are reversed during backtracking. Only lemmas that were created as a side-effect of conflicts survive backtracking and become the seeds for what quantifiers, terms and equivalences that survive.
When E-matching triggers a quantifier instantiation it creates a formula , which is universally valid. The formula is asserted as an axiom. The formula is never garbage collected as long as search stays within the backtracking scope of where the instance was created. It gets garbage collected during backtracking if the instantiation happens within a nested search level.
The weight field on quantifiers is used to assign a priority to quantifier instantiation. Quantifiers with weight set to 0 are instantiated without delay. Quantifiers with higher weights are inserted into a quantifier instantiation queue. The queue is emptied based on a set of rules:
The cost of a quantifier is computed as a function of the weight other attributes using an expression
that can be configured by setting the parameter smt.qi.cost
. The default value of smt.qi.cost
is
(+ weight generation)
, where weight is obtained from the quantifier and generation keeps track of instantiation depths.
All terms have initially generation 0, terms created as a by-product of quantifier instantiation have 1 plus the maximal
generation of previous terms. Other parameters that can be used to encode quantifier priorities include
instances
number of instances of current branch
size
number of sub-expression of the quantifier
depth
depth of quantified expression
vars
number of bound variables
pattern_width
number of patterns in multi-pattern annotation
total_instances
the number of existing instances based on the quantifier
scope
the scope level of search (how many case splits)
cs_factor
a case split factor computed by considering number of disjuncts in a quantified body
Quantifiers are instantiated eagerly if their cost is below a configurable smt.qi.eager_threshold
.
A partial checker can be enabled by setting smt.qi.quick_checker
to 1 or 2 to promote instantiation where quantified instantiations are known to close a branch.
Quantifiers whose cost are above the eager threshold and does not pass a quick checker are inserted into a lazy queue.
The lazy queue is instantiated on final checks. It uses a cost threshold to iteratively increase the number of instances that are processed.
smt.qi.lazy_threshold
are instantiated during final check.
Model-based Quantifier Instantiation, MBQI, is based on a simple concept of using ground models to model check quantifiers, and extracting substitutions from quantifiers that fail the model check. Conceptually, MBQI uses the procedure:
Check
while is SAT with model :
if is UNSAT return SAT
model for
find , such that .
return UNSAT
where is partially evaluated using interpretation .
Example 20.
Assume we have a model
and term .
Then the specialization of with respect to is
MBQI offers a powerful setting to perform quantifier reasoning when the search space for instantiation terms is finite. This is the case for several fragments: EPR, UFBV, Array property fragment, Essentially UF. MBQI can also be combined with a set of templates to search over instantiation terms.
Effectively Propositional Reasoning, also known as Bernays-Schoenfinkel-Ramsey class comprises for formulas with relations, equalities, universally quantified variables, constants but no functions. Formally, it can be described as a set of formulas of the form:
EPR is decidable, as seen from a brute-force decision procedure:
MBQI can be used to harness the set of groundings.
Skolemize
Models for bind variables to free constants
The number of possible such models is bounded by .
UFBV is decidable:
All variables range over finite domains.
Quantifier-free fragment is not only NP hard, it is NEXPTIME hard.
Quantified fragment is another complexity jump.
UFBV using MBQI [45]
where are arrays and denotes just .
Example 21.
Let us express the following in the Map Property Fragment:
is the constant -array.
is equal to except at index .
can only have two values, or .
The Array Property Fragment extends the Map Property Fragment by allowing limited arithmetic.
The claim is that the array property fragment admits a finite set of instantiations.
Given formula with free.
Let be set of a set of from .
Definition 1.
The set is a sufficient set of instances for , if
Proposition 3.
If admits a finite sufficient set of instances , it can be evaluated using those.
Proposition 4.
Formulas in the array and map property fragments admit a sufficient set of instantiations.
Proposition 5.
MBQI is a decision procedure for the array and map property fragments by using the
instantiation sets computed from the formulas.
Instantiations based on are sufficient for establishing unsatisfiability. To establish satisfiability, MBQI updates models that are based on how uninterpreted functions evaluate on arguments in to how they evaluate on all integers. Models are updated in ways that do not change the interpretation of quantifier-free formulas. This is accomplished by keeping interpretation of ground terms the same, but adjusting the interpretations of functions for values that are not in the ground interpretation. For arithmetic and bit-vectors it updates functions to be piecewise constant between values from . In other words, suppose , and the set happens to be sorted such that , and suppose is a function from integers to integers. The ground model interprets and has some dummy interpretation for for values outside of . Model fixing changes the dummy interpretation of to a piecewise constant interpretation by using the values from this set. For it sets the interpretation of . For between and it has the option to set the the interpretation of to either or .
Z3 represents the updated interpretations to the function using projection functions. In other words, it uses the original interpretation of as an interpretation to a fresh function , and then interprets as , where is a projection function. The projection function maps an integer to the bound or , where .
Models are also updated by detecting macro patterns in quantified formulas. For example, a quantified formula , where is not used in defines as a macro. The interpretation of is a function of . Z3 uses several techniques to extract macro interpretations from quantified formulas. After exhausting the search for macro interpretations it fixes the models for remaining functions to be piecewise constant.
The Array Property Fragment can be seen as an instance of a class of formulas, where instantiations can be extracted based on a set of grammar rules that are extracted from the formulas.
This fragment thus applies to a wider range of formulas than the syntactic array property fragment. It also captures fragments studied in different contexts:
Example 22.
(set-option :smt.mbqi true)
;; f an g are "streams"
(declare-fun f (Int) Int)
(declare-fun g (Int) Int)
;; the segment [a, n + a] of stream f is equal to the segment [0, n] of stream g.
(declare-const n Int)
(declare-const a Int)
(assert (forall ((x Int)) (=> (and (<= 0 x) (<= x n))
(= (f (+ x a)) (g x)))))
;; adding some constraints to a
(assert (> a 10))
(assert (>= (f a) 2))
(assert (<= (g 3) (- 10)))
(check-sat)
(get-model)
An important ingredient in quantifier reasoning is found in pre-processing. Pre-processing techniques may identify quantified assertions as macro definitions. Z3 contains several heuristics for identifying macros.
The tactic finds implicit macro definitions in quantifiers.
A main instance of a macro an equality that defines a function f
using some term t
that does not contain f
.
Other instances of macros are also recognized by the macro finder.
(forall (x) (= (f x) t))
not (= (p x) t)
is recognized as (p x) = (not t)
(iff (= (f x) t) cond)
rewrites to (f x) = (if cond t else (k x))
(not (= (k x) t))
(= (+ (f x) s) t)
becomes (= (f x) (- t s))
(= (+ (* -1 (f x)) x) t)
becomes (= (f x) (- (- t s)))
The elim-predicates subsumes macro-finder
(and the quasi-macro
tactic that we skip describing).
In addition to the macro-finder
macros it identifies rewrite opportunities of the form
(assert (forall ((x Int) (y Int)) (= (f x y (+ x y)) (* 2 x y))))
that implies that f
can be treated
as a function (f x y z) = (if (= z (+ x y)) (* 2 x y) (f' x y z))
Predicates that occur at most in every clause and such that the cross-product of positive
and negative occurrences is small relative to the original set of occurrences are eliminated.
The elimination process was introduced by Davis and Putnam in a seminal paper that preceeded the
Davis Logeman Loveland backtracking SAT procedure. We illustrate the functionality of elim-predicates
with an example
(declare-fun f (Int Int Int) Int)
(declare-fun p (Int) Bool)
(declare-const a Int)
(declare-const b Int)
(assert (forall ((x Int) (y Int)) (= (f x y (+ x y)) (* 2 x y))))
(assert (p (f 8 a (+ a 8))))
(assert (not (p (f 0 a (+ a 8)))))
(assert (not (p (f 2 a (+ a 8)))))
(assert (not (p (f 1 a (+ a b)))))
(apply elim-predicates)
The result of the tactic is a goal of the form:
(goal
(not (= (* 16 a) (f!0 0 a (+ 8 a))))
(not (= (* 16 a) (f!0 2 a (+ 8 a))))
(let ((a!1 (= (* 16 a) (ite (= 1 b) (* 2 a) (f!0 1 a (+ a b))))))
(not a!1))
:precision precise :depth 1)
Z3 exposes more than 100 tactics for solving or decomposing goals into one or more sub-goals. Tactics can be composed to form new more powerful tactics that meet needs of custom solving tasks. Tactics, in the context of Z3, were introduced in [18], there is a set of samples for using tactics online, and many tactics are summarized with examples. While we have to refer to the online sources for what various tactics accomplish and how to configure them we will here summarize an ontology of tactics and provide a high-level guide towards using main tactics.
The first aim of a user is likely well captured by an aim to gain insights into selection of suitable tactics for specific problems. We will not here attempt to catalog a mapping from problem classes to tactic combinations, the space is too open ended and a partial categorization requires significant effort. The reality is that selecting suitable tactics remains an experimental craft: for a given class of problems a user can try available tactics on small instances to measure their effects to deduce overall effects. Tactic selection has even been the focus of integration with machine learning tools [1]. In this guide we will try to give the reader tools to understand tactics in context to guide experimentation.
Alive2 [36] is a translation validation tool for the LLVM compiler. It works by snapshotting the code before and after optimizations and proving that the optimized code refines the original code. Alive2 uses Z3 to solve all the logic formulas it generates.
Alive2 uses a combination of features and logics of Z3: bit-vectors, arrays, lambdas, floating-point numbers, UFs, and quantifiers. Sometimes it uses all of these in a same query. This is an unusual mix of theories, that is not observed in the SMT-LIB benchmarks. Since Z3's tactics are mostly tuned for the SMT-LIB benchmarks, it is natural that Z3's standard tactic pipelines are not suitable for Alive2.
Using tactics in Alive2 was really necessary! For some of the larger benchmarks, using its custom tactic is an order of magnitude faster than using Z3's standard tactic pipelines. Another thing to keep in mind is that not all of Z3's tactic are hardened to be used from the API. For example, some tactics may not respect resource limits (e.g., time, memory) or have a super-linear cost for certain formulas. Hence, using a small set of hardened tactics is advisable for production applications.
For example, Alive2 queries have a lot of UFs.
However, many of those UFs have constant arguments or they become constant
after using the simplify
tactic.
It is then very useful to run the reduce-args
tactic to remove arguments of UFs
that are constant, or even to transform UF applications into variables (e.g.,
f(#x42)
is replaced with f!1
).
After removing UFs, other tactics that don't work with UFs can be used now.
For example, Alive2 runs elim-uncnstr
to remove single uses of variables,
either boolean, or BV variables used in a single constraint.
Alive2 also produces a lot of quantifiers. Many of these formulas are not too
complicated and can be solved through Z3's simple quantifier elimination
tactic (qe-light
).
Z3 also has a more advanced quantifier elimination tactic (qe
), but it is too
slow for our large formulas and often doesn't pay off.
In the compiler world, the phase ordering problem is well known: finding the best order of optimizations for each program is a very hard problem. The problem space is huge given that the number of optimizations is large, and it's often beneficial to run some optimizations more than once. The same issue arises with SMT solvers: the right sequence of tactics depends on the problem instance. Similarly, some tactics should be run multiple times.
For example, Alive2 runs qe-light twice. The first run exposes several opportunities for other tactics (like removing unconstrained expressions). Running these tactics, then allows qe-light to eliminate a few more quantifiers.
Most of Z3's tactics are not useful for Alive2, which emphasizes the need to
customize the tactic pipeline.
First, some tactics target theories that Alive2 doesn't use.
Second, some tactics are only useful when formulas are written in a way that
Alive2 will never generate.
For example, the demodulator
tactic essentially instantiates axioms (a particular kind
of forall quantified formulas) present in the formula.
Alive2's vcgen algorithm already instantiates all the relevant axioms, as it
has more domain information, so it can do a better job.
Therefore, Alive2 doesn't benefit from Z3's demodulator
tactic.
Finally, some of Z3's tactics are expansive: they increase the size of the formula,
hoping the rewritten formula will simplify further later on.
For example, the blast-term-ite
tactic will rewrite a + ite(c, b, d)
into ite(c, a + b, a + d)
.
This can open opportunities for further simplification, but it can also
increase the size of the formula exponentially.
Alive2, in particular, often generates formulas with deeply nested ite
expressions, hence it's not possible to use such a tactic.
Eager bit-blasting, as mentioned before, doesn't yield good results
as variables tend to have large bit-widths.
Besides tactics, there are also the SMT solver (and tactics) parameters.
For example, Z3 has two algorithms for quantifier instantiation: E-matching
and MBQI.
These algorithms have different characteristics and each will perform best with
certain formulas and queries. It's left to the user to pick the best algorithm
(through the smt.ematching
config option).
Another thing to consider is theories' options. Some theories have options
to do small changes to their semantics to improve performance.
For example, when converting a float number to a bit-vector, Z3 internally uses a UF
to represent an arbitrary NaN payload. However, if this behavior is irrelevant to
the application (e.g., because NaNs are irrelevant or because NaNs are already
handled properly), it can be disabled by setting rewriter.hi_fp_unspecified
to true.
Other cases include, for example, bit-vector division by zero.
To summarize, Alive2's main tactic and solver options are as follows:
(set-option :model.partial true)
(set-option :smt.ematching false)
(set-option :smt.mbqi.max_iterations 1000000)
(set-option :memory_high_watermark 2147483648) ; soft 2 GB memory limit
(set-option :rewriter.hi_fp_unspecified true)
(and-then simplify propagate-values simplify elim-uncnstr qe-light
simplify elim-uncnstr reduce-args qe-light simplify smt)
All tactics create at least one sub-goal. If a tactic is able to determine satisfiability of a goal, the subgoal contains no assertions. If it determines unsatisfiability, the subgoal contains the assertion . Otherwise, the subgoal is a set of assertions. Several subgoals can be viewed as a subgoal containing the disjunction of assertions from each subgoal. The relationship between the original goal and subgoals is determined by what the tactic accomplishes. The tactic may produce a subgoal that either
relationship | description | example |
---|---|---|
preserves equivalence | the subgoal is equivalent to the original goal | simplify |
preserves satisfiability | the subgoal is equi-satisfiable, but not equivalent | solve-eqs |
weakening | if the subgoal is unsatisfiable, the original goal is too | sat |
strengthening | if the subgoal is satisfiable, the original goal is too | nla2bv |
The simplify
tactic performs rewriting simplification steps. The rewrites implemented within simplify
are equivalence preserving. The solve-eqs
tactic eliminates uninterpreted constants when they can be
written as solutions to equations. For example allows solving as . It removes
from the subgoal which is therefore not equivalent to the original goal. The sat
tactic
solves satisfiability of a propositional abstraction of the original goal. The nla2bv
tactic approximates
reals and integers by bit-vectors and therefore constrains the values of possible solutions to a small
space that can be expressed using the bit-vectors.
Tactics that preserve satisfiability, but not equivalence, produce as a side-effect a model converter. The model converter is maintained internally. It can be accessed on the subgoal to convert a model of the subgoal to a model of an original goal. Within the satisfiability preserving tactics there is a further distinction between the model converters associated with tactics. We classify them next.
effect on models | example input | example output | model converter |
---|---|---|---|
rigid constrained | |||
under-constrained | |||
under-constrained | |||
over-constrained | n/a | ||
The base functionality of solve-eqs
tactic is an example of a rigid constrained tactic. It includes the substitution in the model converter.
Rigid constrained tactics have the property that the model converter can be applied as a substitution to new formulas while
preserving satisfiability. In other words, rigid constrained tactics can be applied incrementally without undoing the effect
of a transformation. The tseitin
tactic is also rigid constrained. It introduces fresh variables to encode Boolean connectives,
but the evaluation of those variables are hidden from the model of the original formula. Thus, there is another category
of model conversions (removing interpretations of variables from the model for the original formula) that
can be applied incrementally.
The solve-eqs
tactic has an option, which is enabled by default, to solve equations within disjunctive context.
If is a variable that occurs within a single disjunction and nowhere else, it can be solved for as well. The resulting formula
is weaker than the original formula because it allows solutions to and where .
To apply under-constrained tactics incrementally it is necessary to replay formulas that were modified by the transformations.
Another example of an under-constrained tactic is elim-unconstr
. It solves for variables that occur uniquely in a goal.
For example, if the only occurrence of is in the inequality we can solve for by setting . At this point,
both and are unique and we can solve for by setting them to . The elim-uncstr
tactic is more general than our simplified example suggests,
it can eliminate unique variables in arbitrary contexts.
It replaces by a fresh predicate , and sets the model substitution .
Conversely, tactics can strengthen formulas. The tactic symmetry-reduce
applies symmetry reduction for formulas over uninterpreted functions.
For illustrative purposes we use a tactic that applies symmetry reduction for arithmetical variables that occur symmetric in the formula.
Z3 does not contain symmetry reduction tactics with arithmetic so users that produce formulas with symmetries should apply their own symmetry breaking
methods.
Over-constrained tactics cannot be applied incrementally if the strengthening formulas are added to the set of assertions of the goal.
Tactics are used internally to provide best-effort pre-processing for a set of known benchmark classes.
For example, the qfufbv
tactic applies a pre-amble comprising of several steps, some generic, others that are specific to bit-vector formulas.
(and-then simplify propagate-values bv-bound-checks solve-eqs
elim-uncnstr bv-size-reduction max-bv-sharing ackermannize_bv)
We have to refer to the online guide for an updated description of these tactics and what they accomplish.
As a rule of thumb there is a set of core pre-processing tactics that apply well across formula types. These are
simplify
- applies algebraic simplification
propagate-values
- detects equations of the form and replaces by the value for other occurrences of .
elim-uncnstr
- removes sub-terms that occur only once and can be solved for.
solve-eqs
- eliminates uninterpreted variables by solving equalities.
There are also several general purpose tactics that apply only well in selected settings as they may carry more overhead.
ctx-simplify
- eliminates sub-formulas based on a contextual analysis. For example can be reduced to
because the second occurrence of is true in the conjunctive context.
demodulator
- extracts rewrite rules from quantified equalities and applies them.
Then there are tactics developed to handle specific kinds of formulas. They allow targeting features of a backend that are better amenable
to solving classes of formulas. For example, pure bit-vector formulas can often be solved most efficiently by a dedicated SAT solver.
The pre-processing steps for the QF_UFBV
formulas is designed to convert a goal whenever possible into a format where it does not
contain uninterpreted functions and where it uses bit-vectors of smallest length.
The main categories of specialized pre-processing tactics are:
fm
factor
purify-arith
qe-light
qe
bvarray2uf
Some tactics also support proofs and extraction of unsatisfiable cores. To support proof generation, a tactic has to map a proof of the subgoal(s) into a proof of the original goal.
A goal assertion can be tracked for the purpose of extracting unsatisfiable cores. Tactics that support unsatisfiable core extraction have to track dependencies of the original goal. Thus, if two assertions and are used to infer an assertion , then the dependencies for and are combined into the dependencies for .
The classification of model converters allow us to understand when tactics can be applied incrementally. We will consider tactics that just produce a single subgoal. Tactics that are equivalence preserving are directly incremental. Tactics that are rigid constrained can be applied incrementally by substituting in the effect of the model converter on new formulas. Tactics that are under-constrained can be applied incrementally by re-adding formulas that were modified, and retracting the effect of the model converter.
A common user issue is around inconsistent behavior of the solver whether it is used in incremental or non-incremental mode. Under the hood,
the difference in behaviors is caused by non-incremental mode using a combination of default tactics to pre-process the (only) formula being solved.
Once the solver is used in incremental mode (after a context push
or after the first check-sat
) Z3 switches to a solver mode that does not use
the pre-processing tactics. Specifically, the solve-eqs
and elim-uncnstr
tactics can have dramatic effect on solvability over many user scenarios
and they are not incremental.
As a sneak peek into functionality in the works,
configuring z3 with sat.smt=true
from the command-line switches to a new CDCL(T) core where a set of pre-set tactics,
including solve-eqs
and elim-uncnstr
are applied incrementally.
As we outlined in the introduction, users of Z3 are likely less interested in the finer details of internal representations than how to make Z3, whatever is under the hood, work better for their applications. The impatient reader might have already given up going over the internals, or just skipped to this section to learn about secret recipes for best using Z3.
In the previous sections, we have included material in an effort to address questions such as:
But we have not given focused answers to questions that cut to the chase of using Z3 to make informed decisions on solving formulas. The purpose of this section is to touch on some broad themes around using Z3 effectively with an eye on how the internals affect using Z3.
The first layer of interaction with SMT solvers is invariably about how to formulate problems in a way that they match better tuned back-ends. Formula encoding is arguably a source for where the most speedup can be gained. This experience is shared with constraint programming (CP) and mixed-integer programming (MIP) solvers. The SMT format offers some flexibility in encoding formats (you can write disjunctions without introducing big variables on your own), but using all features is not necessarily an advantage.
We here mention a few anecdotal examples of encoding scenarios.
Using string constants to represent cases of a finite domain is not great.
The string and sequence solver in Z3 is tailored for solving string equations. It has no special handling when it only processes equations of the form that comprise a variable and a string constant. Applications should not use strings in this case. They can use a finite enumeration sort or bit-vectors.
Solving Sudoku problems with integers is far worse than with bit-vectors.
Sudoku problems have natural formalization using integers. They really only require the distinct
constraint over a finite domain 1..9
.
The SAT solver is much more effective at solving such constraints (the all-difference constraint is not built into Z3, so has to be programmed
externally if it is useful).
Stick with integers or bit-vectors. Mixed formulas are not solved efficiently.
There are functions for converting integers to bit-vectors (int2bv
) and from bit-vectors to integers (bv2nat
).
The trigger creating axioms that capture the meaning of these functions. The axioms incur overhead both for bit-vector
reasoning and integer reasoning. The solvers decoupled and don't integrate strong propagation for mixed integer/bit-vector reasoning.
Adding constraints that break symmetries tends to have a dramatic effect on problems with symmetries.
Symmetries are a major obstacle to CDCL and essentially all solvers. There is limited built-in support for breaking symmetries of problems over EUF, but otherwise applications should carefully encode problems in a way that resolve symmetries.
SAT is a solved problem, right?
Z3 integrates a reasonably efficient SAT solver. There are more efficient modern SAT solvers, specifically Kissat is available for one-shot problems and CaDiCaL supports incrementality. While there can be a significant gap between problems solved by Kissat and Z3's SAT engine on SAT problems, they share some underlying principles that have known limitations.
CDCL is horrible at xor reasoning. By nature, CDCL SAT searches for regular resolution proofs. Applications that produce parity constraints tend to not have any short regular resolution proofs. A domain solver, based on CryptoMiniSAT, for xor constraints is currently being integrated as a theory solver. Yet, even with native xor reasoning, the art of synthesizing strong lemmas during xor search remains an open problem.
Z3 is more stable for unsatisfiable problems than satisfiable problems. CDCL solvers are particularly good at finding
small resolution proofs. Favoring phase caching (e.g., smt.phase_caching_on=80000
) is observed to help on satisfiable problems.
Tuning of phase caching is among one of the recent advances in Kissat and Z3 borrows some of the ideas.
Parallel Z3 doesn't offer any speedups
For problems expected with a satisfiable outcome, you can often with some advantage
activate parallel SAT solving within the SAT core by setting sat.threads=N
. It spawns N
threads that work on the same problem.
Running separate instances with different random seeds may also reduce overall latency in getting answers.
There is an option to run solvers in parallel by cube and conquer decomposition. The idea of cube and conquer is to partition a problem based on one or more case splits (cubes). The cubes are found using a lookahead solver that measures and estimates the benefits of case splitting on Boolean variables. Cube and conquer solving has been used successfully for solving small but hard combinatorial problems. A sweet spot for cube and conquer is to decompose problems into more constrained partitions that each have small resolution proofs. Z3's lookahead solver is exposed over the API. It can be used to produce cubes and integrated in a specialized solver. It has shown to be useful also on decomposing larger combinatorial problems solved in a distributed cloud environment. Note that Z3 does not offer any automatic guidance for tuning the ratio and frequency of cubing to conquering and the default settings are unlikely to produce speedups, and much more likely to slow down overall solving.
Shifting reasoning about uninterpreted functions to the CDCL SAT core can produce significant speedups
Z3 contains both pre-processing and in-processing techniques for Ackermann reduction. It replaces uninterpreted functions by propositional logic. Ackermann reduction incurs significant asymptotic overhead, quadratic in the number of a function occurrence, so it is only useful when the number of function occurrences is small (in the tens). The EUF decision procedure is much more scalable in general.
Shifting reasoning about uninterpreted functions to the CDCL SAT core is not practical for large formulas
Shifting work to the CDCL SAT core can produce significant speedups
There are several tactics that support transformations from integer arithmetic to propositional reasoning.
lia2pb
from bounded arithmetic to Pseudo-Boolean formulas
card2bv
from Pseudo-Boolean formulas to bit-vector formulas or other circuit encodings
dt2bv
from finite enumeration sorts to bit-vectors
bit-blaster
from bit-vectors to propositional formulas
Bit-vectors are a convenient intermediary representation that retains equality reasoning or can be fully converted to SAT. The CDCL(T) cores also reason natively with cardinality and Pseudo-Boolean formulas. Native reasoning is an advantage when the clausal forms incur a significant overhead, otherwise, compiling to clausal form very often offers superior performance.
Compiling to SAT can produce significant slowdowns
Conversion into Pseudo-Boolean constraints or bit-vectors is a disadvantage when constraints are mostly convex and can be solved using dual Simplex. The native solver for Pseudo-Booleans in Z3 requires compiling bounded variables into a set of 0-1 variables, and it does not integrate dual Simplex reasoning (yet).
Z3 subsumes LP and MIP, I should be able to use it for solving LP and MIP problems
Z3's solvers for arithmetic are arguably trained on the instances submitted by users and available in the SMTLIB2 benchmark set. It uses a dual Simplex tableau where constants are represented using infinite precision arithmetic. There are no specialized representations using floating points for fixed-point arithmetic that can be significantly faster for important applications. Reasonable linear programming solvers uses floating point representations. Infinite precision incurs an impractical overhead on standard linear programming applications. On the other hand, there is very little overhead incurred of infinite precision arithmetic for a significant set of applications where Z3 is trained. It can be significant however, on important application scenarios such as Solidity smart contracts that involve very large constants.
There are more reals than integers, so isn't search more constrained over integers?
Problems don't get easier because you formulate them as integer problems instead of reals.
Quantified formulas over reals polynomials are decided using the nlqsat
tactic. On the other hand,
the quantifier free theory of integer polynomials is undecidable by Matiyasevich's theorem.
I want to solve formulas using trigonometric functions and exponents. Z3 allows such formulas so why doesn't it work?
There is no reasoning support for trigonometric functions and exponentiation. Z3's API supports calculation with extension fields over algebraic numbers, a feature that is disjoint from any of the solver capabilities.
Encoding problems with quantified formulas is extensively studied by users in the programming verification space. A thorough overview of trouble-shooting Z3 behavior for formulas using quantifiers is provided in the context of .
Z3 creates a lot of instantiations. How can I diagnose when there are too many quantifier instantiations?
Z3 fails to instantiate a quantifier I thought would be useful. How do I diagnose a failed quantifier instantiation?
How does Z3 prioritize quantifier instantiation?
The primary means for retrieving information from z3 is by
extracting models, unsatisfiable cores, proof terms,
or clause traces. Clause traces can be stored to a file or
monitored directly during search. When run from the
command-line you can specify to create a trace
log.
We summarize the methods below.
Interface | Purpose |
model | Retrieve an interpretation of a satisfiable formula |
Set smt.candidate_models=true to to enable model production even if search is incomplete | |
core | Retrieve an unsatisfiable subset for the assumption literals passed to the solver |
The cores can optionally be minimized | |
proof | Retrieve a proof term that tracks main inferences |
Proof terms are supported for a limited set of pre-processing rules | |
clause trace | Extract a DRUP(T) style clause trail from search |
It is relatively new functionality and subject to extensions and revisions | |
trace | Log a low level trace that can be processed using the AxiomProfiler from ETHZ |
API log | Instrument the solver to log API calls into a sequence of instructions that can be replayed |
SMT log | Instrument a solver exposed over the API to log assertions, push, pop as SMTLIB2 commands |
Lemma log | Instrument a solver to append to standard output a set of learned lemmas in SMTLIB2 format |
For development and debugging purposes, you can configure Z3 in verbose mode and instrument Z3's code with low level tracing.
The guide on programming quantifiers 9.2.5 points to detailed information of using feedback from Z3 to understand search behavior as a user.
This document developed a bird's eye view of Z3 internals. It offered some ideas of the data-structures and algorithms used at a level to capture the gist in the format of an extended tool description. It also provided background on how users can interact with Z3 to develop encodings and operate the solver. A vast amount of details is omitted from this document. Some are described in papers that originally introduced the features, other details have yet to be distilled from implementations into a literate format.