Smtlink 2.0

by   Yan Peng, et al.
The University of British Columbia

Smtlink is an extension of ACL2 with Satisfiability Modulo Theories (SMT) solvers. We presented an earlier version at ACL2'2015. Smtlink 2.0 makes major improvements over the initial version with respect to soundness, extensibility, ease-of-use, and the range of types and associated theory-solvers supported. Most theorems that one would want to prove using an SMT solver must first be translated to use only the primitive operations supported by the SMT solver -- this translation includes function expansion and type inference. Smtlink 2.0 performs this translation using a sequence of steps performed by verified clause processors and computed hints. These steps are ensured to be sound. The final transliteration from ACL2 to Z3's Python interface requires a trusted clause processor. This is a great improvement in soundness and extensibility over the original Smtlink which was implemented as a single, monolithic, trusted clause processor. Smtlink 2.0 provides support for FTY defprod, deflist, defalist, and defoption types by using Z3's arrays and user-defined data types. We have identified common usage patterns and simplified the configuration and hint information needed to use Smtlink.


page 1

page 2

page 3

page 4


Astra Version 1.0: Evaluating Translations from Alloy to SMT-LIB

We present a variety of translation options for converting Alloy to SMT-...

Smt-Switch: a solver-agnostic C++ API for SMT Solving

This extended abstract describes work in progress on Smt-Switch, an open...

Politeness for the Theory of Algebraic Datatypes

Algebraic datatypes, and among them lists and trees, have attracted a lo...

Toward SMT-Based Refinement Types in Agda

Dependent types offer great versatility and power, but developing proofs...

Representing Hybrid Automata by Action Language Modulo Theories

Both hybrid automata and action languages are formalisms for describing ...

Deciding and Interpolating Algebraic Data Types by Reduction (Technical Report)

Recursive algebraic data types (term algebras, ADTs) are one of the most...

SMT-Based Refutation of Spurious Bug Reports in the Clang Static Analyzer

We describe and evaluate a bug refutation extension for the Clang Static...

1 Introduction

Interactive theorem proving and SMT solving are complementary verification techniques. SMT solvers can automatically discharge proof goals with thousands to hundreds of thousands of variables when the goals are within the theories of the solver. These theories include linear and non-linear arithmetic, uninterpreted functions, arrays, and bit-vector theories. On the other hand, verification of realistic hardware and software systems often involves models with a rich variety of data structures. Their proofs involve induction, encapsulation (or other forms of higher-order reasoning), and careful design of rules to avoid pushing the solver over an exponential cliff of search complexity. Ideally, we would like to use the capabilities of SMT solvers to avoid proving large numbers of simple but tedious lemmas and combine those results in an interactive theorem prover to enable the reasoning of properties in large, realistic hardware and software designs.

Smtlink is a book developed for integrating SMT solvers into ACL2. In the previous work [23], we implemented an interface using a large trusted clause-processor, and only supported a limited subset of SMT theories. In this work, we introduce an architecture based on a collection of verified clause processors that transform an ACL2 goal into a form amenable for discharging with an SMT solver. The final step transliterates the ACL2 goal into the syntax of the SMT solver, invokes the solver, and discharges the goal or reports a counter-example based on the results from the solver. This final step is, performed by a trusted-clause processor because we are trusting the SMT solver. Because most of the translation is performed by verified clause processors, the soundness for those steps is guaranteed. Furthermore, the task of the final, trusted clause processor is simple to describe, and that description suggests the form for the corresponding soundness argument. The modularized architecture is readily extensible which makes introducing new clause transformation steps straightforward.

Our initial motivation for linking SMT solvers with ACL2 was for proving linear and non-linear arithmetic for Analog and Mixed-Signal (AMS) designs [24]. This original version supported the SMT solver’s theories of boolean satisfiability and integer and rational arithmetic, but provided no support for other types. For example, to verify theorems involving lists, one would need to expand functions involving lists to a user-specified depth, treat remaining calls as uninterpreted functions which must return a boolean, integer, rational, or real. The lack of support for a rich set of types restricted the applicability of the original Smtlink to low-level lemmas that are primarily based on arithmetic. The new Smtlink supports symbols, lists, and algebraic datatypes, with a convenient interface to FTY types.

Other changes include better support for function expansion and uninterpreted functions. The new smtlink-hint interface is simpler. Smtlink generates auxiliary subgoals for ACL2 in “obvious” cases, such as when the user adds a hypothesis for Smtlink that is not stated in the original goal. Hints for these subgoals share the same structure as the ACL2 hints and are attached by the Smtlink hint syntax to the relevant subgoal – no subgoal specifiers are required! When the SMT solver refutes a goal, the putative counterexample is returned to ACL2 for user examination. Currently, Smtlink supports the Z3 SMT solver [21] using Z3’s Python API. Smtlink is compatible with both Python 2 and Python 3, and can be used in ACL2(r) with realp.

Documentation is online under the topic :doc smtlink. It describes how to install Z3, configure Smtlink, certify Smtlink and test the installation. It also describes the new interface of the Smtlink hint, contains several tutorial examples, and some developer documentation. Examples shown in this paper assumes proper installation and setups are done as is described in :doc smtlink. It is our belief that given a more compelling argument of soundness for the architecture, a richer set of supported types or theories, and a more user-friendly user interface, Smtlink can support broader and larger proofs.

In Section 2 we present the architecture of Smtlink based on verified clause processors, hint wrappers, and computed hints. Section 3 describes the SMT theories supported by Smtlink and sketches the soundness argument. Section 4 gives a simple example where we use lists, alists, symbols, and booleans to model the behavior of a ring oscillator circuit. Section 5 provides a summary of related work, and we summarize the paper along with describing ongoing and planned extensions to Smtlink in Section 6.

2 Smtlink 2.0 Architecture

This section describes the architecture of Smtlink 2.0. We first introduce a running example to illustrate each of the steps taken by the clause processor. Section 2.2 describes the top-level architecture, followed by a brief description of each of the clause processors used in Smtlink.

2.1 An Nonlinear Inequality Example

To illustrate the architecture of Smtlink, we use a running example throughout this section. Consider proving the following theorem in ACL2:

Theorem 2.1

and , if and , then .

1(defun x^2-y^2 (x y) (- (* x x) (* y y)))
3(defthm poly-ineq-example
4  (implies (and (real/rationalp x) (real/rationalp y)
5                (<= (+ (* (/ 9 8) x x) (* y y)) 1)
6                (<=  (x^2-y^2 x y) 1))
7           (< y (- (* 3 (- x (/ 17 8)) (- x (/ 17 8))) 3)))
8  :hints(("Goal"
9          :smtlink nil)))
Program 2.1 A nonlinear inequality problem

Program 2.1 shows the corresponding theorem definition in ACL2. To use Smtlink to prove a theorem, a hint :smtlink [smt-hint] can be provided using ACL2’s standard :hints interface. SMT::smt-hint is the hint provided to Smtlink. SMT::smt-hint follows a structure that is described in :doc smt-hint. In this example, :smtlink nil suggests using Smtlink without any additional hints provided to Smtlink. The initial goal (underlining represented as clauses) is:

              (<= (+ (* (* 9 (/ 8)) X X) (* Y Y)) 1)
              (<= (X^2-Y^2 X Y) 1))
          (< Y (+ -3 (* 3 (+ X (- (* 17 (/ 8))))
                          (+ X (- (* 17 (/ 8))))))))

2.2 The Architecture

Figure 1: Smtlink Architecture

Let denote the goal to be proven. As shown in Figure 1, the Smtlink hint invokes the verified clause processor called SMT::process-hint. The arguments to this clause-processor are the clause, , and a list of hints provided by the user. The SMT::process-hint performs syntactic checks on the user’s hints and translates them into an internal representation used by the subsequent clause processors. The user can specify default hints to be used with all invocations of Smtlink. These are merged with any hints that are specific for this theorem to produce combined-hint. The SMT::process-hint adds a SMT::hint-please wrapper to the clause and returns a term of the form
  ‘(((SMT::hint-please (:clause-processor (SMT::add-hypo-cp clause ,combined-hint))) ,@G))

Smtlink 2.0 uses the hint wrapper approach from books/hints/hint-wrapper.lisp. In particular, we define a “hint-wrapper” called SMT::hint-please in package smtlink that always returns nil. Clauses in ACL2 are represented as lists of disjuncts, and our hint-wrapper can be added as a disjunct to any clause without changing the validity of the clause. SMT::hint-please takes one input argument – the list of hints.

The computed-hint called SMT::SMT-computed-hint searches for a disjunct of the form
(SMT::hint-please ...) in each goal generated by ACL2. When it finds such an instance, the SMT::SMT-computed-hint will return a computed-hint-replacement of the form:

    ((SMT::SMT-computed-hint clause))
    (:clause-processor (SMT::some-verified-cp clause ,combined-hint)))

This applies the next verified clause-processor called SMT::some-verified-cp to the current subgoal and installs the computed-hint SMT::SMT-computed-hint on subsequent subgoals again. The SMT::some-verified-cp clause-processor is one step in a sequence of verified clause processors. Smtlink uses a configuration table called *smt-architecture* to specify the sequence of clause processors, as shown in Figure 1. Each clause processor consults this table to determine its successor. By updating this table, Smtlink is easily reconfigured.

As described above, the initial clause processor for Smtlink, SMT::process-hint, adds a
SMT::hint-please disjunct to the clause to indicate that the clause processor, SMT::add-hypo-cp should be the next step in translating the clause to a form amenable for a SMT solver.

2.2.1 add-hypo-cp

A key to using an SMT solver effectively is to tell it what it needs to know but to avoid overwhelming it with facts that push it over an exponential complexity cliff. The user can guide this process by adding hypotheses for the SMT solver – typically these are facts from the ACL2 logical world that don’t need to be stated as hypotheses of the theorem. The clause-processor SMT::add-hypo-cp is a verified clause processor that adds user-provided hypotheses to the goal. Let denote the original goal and , … denote the new hypotheses. Each added hypothesis is returned by the clause processors as a subgoal to be discharged by ACL2. The user can attach hints to these hypotheses – for example, showing that the hypothesis is a particular instantiation of a previously proven theorem. The soundness of SMT::add-hypo-cp is established by the theorem:


The term is the main clause that gets passed onto the next clause-processor. In the following, let denote .

As for the example in Program 2.1, the user didn’t provide any additional guidance. We see that the clauses generated are almost unchanged. The only place that changed, is that Smtlink has installed the next clause-processor to be SMT::expand-cp.

               (<= (+ (* (* 9 (/ 8)) X X) (* Y Y)) 1)
               (<= (X^2-Y^2 X Y) 1))
          (< Y (+ -3 (* 3 (+ X (- (* 17 (/ 8))))
                          (+ X (- (* 17 (/ 8)))))))))

2.2.2 expand-cp

The clause-processor SMT::expand-cp expands function definitions to produce a goal where all operations have SMT equivalents. Function definitions are accessed using meta-extract-formula. By default, all non-recursive functions including disabled functions are expanded. Recursive functions are expanded once and then treated as uninterpreted functions. We attempt one level of expansion for recursive functions given the reasoning that if the theorem can indeed be proved only knowing the return type of the function, it should still be provable when expanded once. If proving the theorem requires expansion of more than one level, or if all occurrences of the function should uninterpreted, then we rely on the user to specify this in a hint to Smtlink. Let be the clause given to SMT::expand-cp and be the expanded version. The soundness of SMT::expand-cp is established by the theorem:


Presently, the clause is returned to ACL2 for proof, and is the main clause that gets passed onto the next clause-processor.

For the running example, function expansion produces the two subgoals. The main clause that get passed onto the next clause-processor is:

               (<= (+ (* (* 9 (/ 8)) X X) (* Y Y)) 1)
               (<= (LET NIL (+ (* X X) (- (* Y Y)))) 1))
          (< Y (+ -3 (* 3 (+ X (- (* 17 (/ 8))))
                          (+ X (- (* 17 (/ 8)))))))))

It tells Smtlink the next clause-processor is SMT::type-extract-cp which does type declaration extraction. The other subgoal is:

                    (<= (+ (* (* 9 (/ 8)) X X) (* Y Y)) 1)
                    (<= (LET NIL (+ (* X X) (- (* Y Y)))) 1))
               (< Y (+ -3 (* 3 (+ X (- (* 17 (/ 8))))
                               (+ X (- (* 17 (/ 8)))))))))
 (OR ... ;; some term essentially equal to nil
                   (<= (+ (* (* 9 (/ 8)) X X) (* Y Y)) 1)
                   (<= (X^2-Y^2 X Y) 1))
           (< Y (+ -3 (* 3 (+ X (- (* 17 (/ 8))))
                           (+ X (- (* 17 (/ 8))))))))))

This second subgoal essentially proves that the expanded clause implies the original clause. Notice how SMT::hint-please is used again for passing other sorts of hints (that are not clause-processor hints) to ACL2 for help with the proof.

2.2.3 type-extract-cp

The logic of ACL2 is untyped; whereas SMT solvers such as Z3 use a many-sorted logic. To bridge this gap, the clause-processor SMT::type-extract-cp is a verified clause processor for extracting type information for free variables from the hypotheses of a clause. SMT::type-extract-cp traverses the clause and identifies terms that syntactically are hypotheses of the form (type-p var) where type-p is a known type recognizer, and var is a symbol. Let denote the clause given to SMT::type-extract-cp; , … denote the extracted type hypotheses; denote with the type-hypotheses removed; and


where SMT::type-hyp logically computes the conjunction of the elements in the list (list ) – using SMT::type-hyp makes these hypotheses easily identified by subsequent clause processors. The soundness of SMT::type-extract-cp is established by the theorem:


is the auxiliary clause returned back into ACL2 for proof. is the main clause that gets passed onto the next clause-processor.

For the running example, the main clause that gets passed onto the next clause-processor is:

 (IMPLIES (AND (<= (+ (* (* 9 (/ 8)) X X) (* Y Y)) 1)
               (<= (LET NIL (+ (* X X) (- (* Y Y)))) 1))
          (< Y (+ -3 (* 3 (+ X (- (* 17 (/ 8))))
                          (+ X (- (* 17 (/ 8)))))))))

The other auxiliary clause is:

                       :EXPAND ((:FREE (SMT::X) (HIDE SMT::X))))))
                (IMPLIES (AND (<= (+ (* (* 9 (/ 8)) X X) (* Y Y)) 1)
                              (<= (LET NIL (+ (* X X) (- (* Y Y)))) 1))
                            (< Y (+ -3 (* 3 (+ X (- (* 17 (/ 8))))
                                            (+ X (- (* 17 (/ 8))))))))))
               (<= (+ (* (* 9 (/ 8)) X X) (* Y Y)) 1)
               (<= (LET NIL (+ (* X X) (- (* Y Y)))) 1))
              (< Y (+ -3  (* 3 (+ X (- (* 17 (/ 8))))
                               (+ X (- (* 17 (/ 8)))))))))

This clause is returned for proof by ACL2. The proof is straightforward – essentially ACL2 simply needs to confirm that the terms we identified as type-hypotheses really are hypotheses of the goal.

2.2.4 uninterpreted-fn-cp

Useful facts about recursive functions can be proven using the SMT solver’s support for uninterpreted functions. As with variables, the return-types for these functions must be specified. For soundness, Smtlink must show that the ACL2 function satisfies the user given type constraints. This is done by SMT::uninterpreted-fn-cp. Let denote the clause given to SMT::uninterpreted-fn-cp, and let , … denote the assertions about the types for each call to an uninterpreted function. Let be the list of clauses


Finally, let be the clause


The soundness of expand-cp is established by the theorem:


The list of clauses is returned to ACL2 for proof, and is tagged with a hint to be checked by the trusted clause processor.

Our running example does not make use of uninterpreted functions in the SMT solver; so, the clause is the same as except for the detail that the SMT::hint-please term now specifies that the next clause-processor to use is the final trusted clause-processor SMT::smt-trusted-cp:

     (IMPLIES (AND (<= (+ (* (* 9 (/ 8)) X X) (* Y Y)) 1)
                   (<= (LET NIL (+ (* X X) (- (* Y Y)))) 1))
              (< Y (+ -3 (* 3 (+ X (- (* 17 (/ 8))))
                              (+ X (- (* 17 (/ 8))))))))))

2.2.5 smtlink-trusted-cp

Figure 2: The trusted clause processor

Figure 2 shows the internal architecture of the trusted clause-processor, for which its correctness hasn’t been proven. Smtlink returns counter-examples generated by the SMT solver back into ACL2. The Python interface to Z3 for Smtlink includes code to translate a counterexample from Z3 into list-syntax for ACL2. The form is not necessarily in ACL2 syntax. We wrote a printing function that takes the Z3 counter-examples and prints it out in an ACL2-readable form. These are all done through the trusted clause processor. Currently, the forms returned into ACL2 are not evaluable. For example, counter-examples for real numbers can take the form of an algebraic number, e.g.

((Y (CEX-ROOT-OBJ Y STATE (+ (^ X 2) (- 2)) 1)) (X -2))

We plan to generate evaluable forms in the future. Learn more about counter-example generation, see :doc tutorial.

The trusted clause-processor returns a subgoal called “SMT precondition” back into ACL2. By ensuring that certain preconditions are satisfied, we are able to bridge the logical meaning for tricky cases, and therefore ensure soundness. We will see explanations on this issue in Section 3. For this running example, there are no preconditions to be satisfied and the following clause trivially holds:

                       :EXPAND ((:FREE (SMT::X) (HIDE SMT::X))))))
              (NOT (AND (OR (NOT (<= (+ (* (* 9 (/ 8)) X X) (* Y Y)) 1))
                            (LET NIL T))
     (IMPLIES (AND (<= (+ (* (* 9 (/ 8)) X X) (* Y Y)) 1)
                   (<= (LET NIL (+ (* X X) (- (* Y Y)))) 1))
              (< Y (+ -3 (* 3 (+ X (- (* 17 (/ 8))))
                              (+ X (- (* 17 (/ 8))))))))))

2.3 A Few Notes about the Architecture

The new architecture clearly separates what’s verified from what’s trusted. As is shown in Figure 1, given an original goal, the Smtlink workflow goes through a series of verified clause-processor, which won’t change the logical meaning of the original goal. A computed-hint is installed to provide hints for the next step, but won’t change the logical meaning of the goals either. The final trusted clause-processor takes a goal that logically implies the original goal and derives information needed for the translation through three sources of information it uses. First, information encoded in the clause using SMT::type-hyp. This is sound because the definition of SMT::type-hyp is a conjunction of the input boolean list. Second, information about FTY types from fty::flextypes-table. Presently, we trust this table to be correct and intact. We plan to use rules about FTY types to derive this information in the future. Third, we use information stored in SMT::smt-hint about configurations, including what is the Python command, whether to treat integers as reals, where is the ACL2toZ3 class file and so on. We believe this information can not accidentally introduce soundness issues from the user. For experimenters and developers, we allow this low-level interface to be overridden by the user using :smtlink-custom hint. Using such a “customized” Smtlink requires a different trust-tag than the standard version, thus providing a firewall between experiments and production versions. See :doc tutorial for how to use customizable Smtlink.

3 Types, Theories, and Soundness

Smtlink translates the original goal, to an expanded goal, for the trusted clause processor through a series of verified clause processors. Thus, we regard the translation to as sound. The trusted clause processor translates into a form that can be checked by the SMT solver; we refer to this translated form as . Let , , …, denote the free variables in , let denote the translated goal, and , , …, denote the free variables of . For soundness, we want


In the remainder, we assume

  • ACL2 and the SMT solver are both sound for their respective theories.

  • The SMT solver is a decision procedure for a decidable fragment of first-order logic. In particular, this holds for Z3, the only SMT solver that is currently supported by Smtlink. In addition, we are working with a quantifier-free fragment of Z3’s logic.

  • There is a one-to-one correspondence between the free variables of and the free variables of . This is the case with the current implementation of Smtlink.

Now, suppose that is not a theorem in ACL2. Then, by Gödel’s Completeness Theorem, there exists a model of the ACL2 axioms that satisfies . We need to show that in this case there exists a model of the SMT solver’s axioms that satisfies . There are two issues that we must address. First, we need to provide, for the interpretation of any function symbol in , an interpretation for the corresponding function symbol in . This brings us to the second issue: the logic of ACL2 is untyped, but the logic of SMT solvers including Z3 is many-sorted. Thus, there are models of the ACL2 axioms that have no correspondence with the models of the SMT solver. We restrict our attention to goals, where the type of each subterm in the formula can be deduced. We refer to such terms as translatable. If is not translatable, then Smtlink will fail to prove it.

For the remainder, we restrict our attention to translatable goals. Because is translatable, there is a set of unary recognizer functions (primitives such as rationalp that return a boolean) and also a set of other functions, such that every function symbol in is a member of or of , and every function in is “well-typed” with respect to in some sense that we can define roughly as follows. We associate each function symbol in with a function symbol of Z3, and each predicate in with a type in Z3. The trusted clause processor checks that there is a “type-hypothesis” associated with every free variable of and a fixing function for every type-ambiguous constant (e.g. nil) – holds trivially if any of these type-hypotheses are violated. For every function in , we associate a member of to each of its arguments (i.e. a “type”) and also to the result. For user-defined functions (i.e. uninterpreted function for the SMT solver), Smtlink generates a subgoal for each call to : if the arguments satisfy their declared types (i.e., predicates from ), then the result must satisfy its declared type as well. For built-in ACL2 functions (e.g. binary-plus) we assume the “obvious” theorems are present in the ACL2 logical world. Now suppose we have a model, , of , and consider the submodel, , containing just those objects such that satisfies at least one predicate in that occurs in . Note that is closed under (the interpretation of) every operation in , because implies that all of the “type-hypotheses” of are true in . This essentially excludes “bad atoms”, as defined by the function acl2::bad-atom. Then because is quantifier-free, also satisfies . We can turn into a model for the language of Z3, by assigning the appropriate type to every object. (As noted in Section 3.1, satisfies the theory of Z3 if is a model of ACL2(r); but for ACL2 that is not the case, so in future work, we expect to construct an extension of that satisfies all of the axioms for real closed fields.) Then we have the claim: for every assignment from the free variables of to with corresponding typed assignment from the free variables of to , if is true in under , then is true in under . Thus, if is translatable, and is unsatisfiable, we conclude that is a theorem in ACL2.

In the rest of this section, we discuss for each of the recognizer functions and each of the basic functions in ACL2, how we associate them with the corresponding Z3 functions.

3.1 Booleans, Integers, Rationals, and Reals

If a term is a boolean constant, then the translation to the SMT solver is direct. Likewise, if is free in and (booleanp ) is one of the hypotheses of , then holds trivially in the case that . Thus, in Smtlink can represent the hypothesis (booleanp ) with the declaration
 x_i = Bool(’x_i’)
without excluding any satisfying assignments. We assume that the boolean operations of the SMT solver (e.g. And, Or, Not) correspond exactly to their ACL2 equivalents when their arguments are boolean. If a boolean operator is applied to a non-boolean value, then Z3 throws an exception, and we regard as non-translatable.

Similar arguments apply in the case that is an integer, rational, or real number. We represent ACL2 rational numbers as Z3 real numbers. Because every rational number is a real number, any satisfying assignment of rational numbers to rational variables in has a corresponding assignment for . Thus, is a generalization of . We note that for ACL2, formally proving the soundness of this generalization requires extending our previously discussed model into a model that satisfies the theory of real closed fields 111, because we are translating rationals in ACL2 to reals in Z3. We haven’t wrapped our heads around how to do that extension in a many-sorted setting, therefore we designate this to be future work. As with booleans, we assume that arithmetic and comparison operators have equivalent semantics in ACL2 and the SMT solver. In fact, we use the Python interface code to enforce this assumption. As an example, ACL2 allows the boolean values t and nil to be used in arithmetic expressions – both are treated as 0. Z3 also allows True and False to be used in integer arithmetic, with True treated as 1 and False treated as 0. To ensure that , our Python code checks the sorts of the arguments to arithmetic operators to ensure that they are integers or reals, where the interpretations are the same for both ACL2 and Z3.

When Smtlink is used with ACL2(r), non-classical functions are non-translatable. We believe that if is classical and satisfiable, then there exists a satisfying assignment to where all real-valued variables are bound to standard values. We believe the sketched proof in the beginning of Section 3 works well for ACL2(r). If we are wrong, we hope that one of the experts in non-standard analysis at the workshop will correct us.

3.2 Symbols

A very important basic type in ACL2 is symbolp. We represent symbols using an algebraic datatype in Z3. In the z3 interface class, we define a Datatype called z3sym, with a single field of type IntSort. Symbol variables are defined using the datatype z3sym. We then define a class called Symbol. This class provides a variable count and a variable dict. It also provides a function called intern for generating a symbol constant. This class keeps a dictionary mapping from symbol names to the generated z3sym symbol constants. This creates an injective mapping from symbols to natural numbers. All symbol constants that appeared in the term are mapped onto the first several, distinct, naturals.

If a satisfying assignment to binds a symbol-valued variable to a symbol-constant that doesn’t appear in , then in our soundness argument, we construct a new symbol value for using an integer value distinct from the ones used so far – we won’t run out. Thus, all symbol values in a satisfying assignment to can be translated to corresponding values for . The only operations that we support for symbols are comparisons for equality or not-equals. We assume that these operations have corresponding semantics in ACL2 and the SMT solver.

3.3 FTY types

We have added support for common fty types that enable Smtlink to automatically construct bridges from the untyped logic of ACL2 to the typed logic of Z3. Currently, Smtlink infers constructor/destructor relations and other properties of these types from the fty::flextypes-table. Thus, the use of fty types extends the trusted code to include the correctness of these tables. This trust is mitigated by two considerations. First, Smtlink only uses fty types that have been specified by the user in a hint to the Smtlink clause processor. If the user provides no such hints, no fty types are used by Smtlink, and no soundness concerns arise.

Second, we expect that the information that Smtlink obtains from these tables could be obtained instead from the ACL2 logical world using meta-extract in Smtlink’s verified clause-processor chain. We see the current implementation as a useful prototype to explore how to seamlessly infer type information from code written according to a well-defined type discipline.

3.3.1 fty::defprod

The algebraic datatypes of Z3 correspond directly to fty::defprod. Smtlink simply declares a Z3 datatype with a single constructor whose destructor operators are the field accessors of the product type. Smtlink requires that the arguments to the fty constructors satisfy the constructors’ guards – otherwise is non-translatable. The only operations on product types are field accessors, i.e. destructors. For translatable terms, the SMT type has the same construct/destructor theorems as the FTY type. Thus, the Smtlink translation maintains equivalence constructors and field accessors of product types.

3.3.2 fty::deflist

Lists are essentially a special case of a product type. For example,

(deflist integer-list
  :elt-type integerp
  :true-listp t)
Listing 1: ACL2 deflist
integer_list= z3.Datatype(’integer_list’)
                     (’car’, _SMT_.IntSort()),
                     (’cdr’, integer_list))
integer_list = integer_list.create()
def integer_list_consp(l):
  return Not(l == integer\_list.nil)
Listing 2: Z3 Datatype

ACL2 overloads cons, car, and cdr to apply to any list. In contrast, Z3’s typed logic has a separate cons, car, cdr functions for each list type. This is why our examples from Section 4 require fixing functions to convey the type information to the trusted-clause processor. We believe that most of the users’ burden of typed lists will be removed in a future release by adding type-inference to Smtlink. There are soundness issues that must be addressed. In ACL2, (equal (car nil) nil). In Z3,
is an arbitrary integer. To ensure soundness, the trusted-clause processor produces the proof obligation (consp x) for every occurrence of (car x) that it encounters. Under this precondition, the Z3 translation preserves the constructor/destructor relationship for lists. Likewise
is an arbitrary integer_list. Thus, Smtlink enforces the :true-listp t declaration for list types. Because “arbitrary” includes integer_list.nil in addition to all other integer_lists, these construction ensures that the SMT solver can choose the value for integer_list.cdr() for that corresponds to the value of (cdr x) for any assignments for . The is a generalization of .

3.3.3 fty::defalist

Smtlink represents alists with SMT arrays. We only support the operations acons and assoc-equal for alists. Then we have:

(defthm alist-axioms
  (implies (not (equal key1 key2))
           (and (equal (assoc key1 (acons key1 value alist)) value))
                (equal (assoc key1 (acons key2 value alist)) (assoc key1 alist)))
                (equal (assoc key1 nil) nil))

The corresponding theorem in the theory of arrays is

(defthm array-axioms
  (implies (not (equal addr1 addr2))
           (and (equal (load addr1 (store addr1 value array)) value))
                (equal (load addr1 (store addr2 value array)) (load addr1 array))))

Note that Smtlink does not support operations such as cdr, nth, member, or delete-assoc that would “remove” elements from an alist. Also, Z3 arrays are typed.

The key issue in the translation is how to handle the case when a key is not found in the alist (ACL2) or array (SMT). Our solution is to make the element type of the SMT array be an option type called keyType_elementType. This type can either be a (key, value) tuple or keyType_elementType.nil. Thus, any value returned by assoc-equal with proper alist and key types has a corresponding keyType_elementType value. Thus, any value for an assoc-equal terms in can be represented in .

When applying cdr to a keyType_elementType, we must ensure that the keyType_elementType value is not nil. This is analogous to the issue with lists: in ACL2, (equal (cdr nil) nil) but in Z3,
is an arbitrary value of elementType. Thus, the trusted-clause processor produces the proof obligation for ACL2 (not (null x)) for every occurrence of (cdr x) when x is the return value from assoc-equal. Under this precondition, cdr is only applied to non-nil values from assoc-equal and we maintain correspondence of values for terms in and .

By only providing acons and assoc-equal, the Smtlink support for alists is rather limited. Nevertheless, we have found it to be very useful when reasoning about problems where alists are used as simple dictionaries.

3.3.4 fty::defoption

As is shown in Program 3.3.4, the translation of defoption is straightforward.

(defoption maybe-integer
Listing 3: ACL2 deflist
maybe_integer= z3.Datatype(’maybe_integer’)
maybe_integer.declare(’some’, (’val’, IntSort()))
maybe_integer = maybe_integer.create()
Listing 4: Z3 Datatype

In this example, the maybe-integer-p recognizer maps to maybeinteger type. The constructor maybe-integer-some maps to maybeinteger.some. The destructor maybe-integer-some->val maps to maybeinteger.val. The none type nil maps to maybeinteger.nil. Typical users of FTY types won’t write maybe-integer-some constructor and maybe-integer-some->val destructors. They will first check if a term is nil, and then assume the term is an integerp. When a program returns an integerp, ACL2 knows it is also a maybe-integerp. Due to lack of type inference capabilities, Smtlink currently requires the user to use those constructors, therefore maintaining the option type through function calls where a maybe-integerp is needed and use the destructors where an integerp is needed. The constructor function satisfies the same theorems in ACL2 and Z3. Therefore, it’s sound. For the field-accessor, when trying to access field of a nil, ACL2 returns the fixed default value, while Z3 will return arbitrary value of that some type. The Z3 values include the ACL2 value. nil is trivially the same. Thus, the Smtlink translation maintains equivalence of values of terms for constructors and field accessors of option types.

3.4 Uninterpreted Functions

The user can direct Smtlink to represent some functions as uninterpreted functions in the SMT theories. Let (f arg1 arg2 …argk) be a function instance in . Smtlink translates this to
 f_smt(arg_smt1, arg_smt2, …, arg_smtk)
The constraints for f_smt are the types of the argument, the type of the result, and any user-specified constraints. If the function instance in violates the argument type constraints, then the term is untranslatable – currently, Smtlink produces an SMT term that provokes a z3types.Z3exception. For each function instance in that is translated to an uninterpreted function, Smtlink produces a proof obligation for ACL2 that the function instances in satisfy the given type-recognizers. Likewise, if the user specified any other constraints for this function, they are returned as further ACL2 proof obligations. Under these preconditions, any value that can be produced by f satisfies the constraints for f_smt. Thus, we maintain correspondence of values for terms in and .

4 A Ring Oscillator Example

In order to show the power of Smtlink, especially what benefit FTY types bring us, in this section, we take a look at how Smtlink

can be used in proving an invariant of a small circuit. This example uses extensively the FTY types, including product types, list types, alist types, and option types. The simple circuit we want to model is a 3-stage ring oscillator. A ring oscillator is an oscillator circuit consisting of an odd number of inverters in a ring as is shown in Figure 

3. A 3-stage ring oscillator consists of three inverters. It oscillates to provide a signal of a certain frequency.

Figure 3: 3-Stage Ring Oscillator

For each inverter in this circuit, we say it is stable if its input is not equal to its output, otherwise, we say the inverter is ready-to-fire. One interesting invariant of this circuit is defined in Theorem 4.1:

Theorem 4.1

Starting from a state where there is one and only one inverter ready-to-fire, for all future states, the ring oscillator will stay in a state where there is only one inverter ready-to-fire.

In order to prove this property of this ring oscillator, we will discuss how we used FTY types extensively for circuit modeling and how Smtlink helped greatly at proving the theorem. To check the details of the proof, see projects/smtlink/examples/ringosc.lisp

4.1 Circuit and Trace Modeling using FTY Types

We model an inverter gate by defining a product type with two fields – input and output. We then model the 3-stage ring oscillator using a product type with six fields – three internal nodes n1 through n3 and three submodule inverters inv1 through inv3. We call it ringosc3-p. We then define a connection function specifying the connections between the upper-level ring oscillator nodes and the lower-level inverter nodes. This describes the shape of the circuit.

As for modeling the behavior of the circuit, we use traces [8]. We define a circuit state as an alist mapping from signal names to its values. A trace is a list of circuits states, called any-trace-p. We define a step recognizer for an inverter. This recognizer function takes two consecutive steps from a trace as inputs and serves as a constraint function of what are the allowed behaviors in a step for an inverter. A valid trace for an inverter is defined recursively using the step function. A valid trace for the 3-stage ring oscillator is then defined requiring the trace to be valid for all three inverters. We call the recognizer function for a valid trace of a ring oscillator, ringosc3-valid.

We define a counting function for an inverter. The counting function returns when an inverter is ready-to-fire and when it’s stable. Then we define a counting function for the ring oscillator based on the counting function for an inverter. We say a state of the ring oscillator is one-safe if only one inverter is ready-to-fire. We call the function ringosc3-one-safe-state. Using this function, we can define ringosc3-one-safe-trace, which means all states in a trace are one-safe. Theorem 4.1 is defined as Program 4.1.

1(defthm ringosc3-one-safe 2  (implies (and (ringosc3-p r) 3                (any-trace-p tr) 4                (consp tr) 5                (ringosc3-valid r tr) 6                (ringosc3-one-safe-state r (car tr))) 7           (ringosc3-one-safe-trace r tr)) 8  :hints (("Goal" 9           :induct (ringosc3-one-safe-trace r tr) 10           :in-theory (e/d (ringosc3-one-safe-trace 11                            ringosc3-valid 12                            inverter-valid) 13                           (ringosc3-one-safe-lemma))) 14          ("Subgoal *1/1.1" 15           :use ((:instance ringosc3-one-safe-lemma 16                            (r r) 17                            (tr tr))) 18           )))
Program 4.1 The ringosc3-one-safe theorem

In this theorem, Smtlink helped to prove the inductive step. Due to space constraints, the details of ringosc3-one-safe-lemma are elided in this paper.. We note that proving this theorem using just ACL2 requires proving detailed lemmas about possible transitions of the ring oscillator. More specifically, our trace model requires asking if a signal exists in the state table, which causes a huge amount of case splits. However, this has not been a problem for Smtlink and the large amount of cases is handled efficiently. Using Smtlink, we are able to expand the functions out to proper steps and then prove the theorem without much human intervention. This demonstrates the potential of applying Smtlink to systems and proofs about systems. All this is made convenient because of the new theories supported and the useful user-interface.

5 Related Work

Integrating external procedures like SAT and SMT solvers into ACL2 has been done in several works in the past. Srinivasan [26] integrated the Yices [9] SMT solver into ACL2 for verifying bit-level pipelined machines. They use a trusted clause processor with a translation process. They appear to have mostly used the bit-vector arithmetic and SAT solving capabilities of Yices. Prior to that, in [18], they integrated a decision procedure called UCLID [17] into ACL2 to solve a similar problem. These are works that require fully trusting the integration.

A typical way of ensuring soundness and avoiding trusting external procedures is to followed Harrison and Théry’s “skeptical” approach [13] and reconstruct proofs in the theorem prover. Recent work by Luís [14] allows refutation proofs of SAT problems to be reconstructed and replayed inside of ACL2. Their work focused on generating efficient refutation proofs that can be checked by a theorem prover in a short amount of time. Integrating SMT solvers into theorem provers has been a consistently developing area in the past decade [19, 12, 5, 20, 7, 6, 2, 11]. Erkök [11] integrated the SMT solver Yices into Isabelle/HOL. Similar to Smtlink, they not only have basic theories but also support algebraic datatypes. They trust Yices as an oracle. Works like  [6, 10] do proof reconstruction. Sledgehammer [6, 15] is a proof assistant that integrates a bunch of SMT solvers into the theorem prover Isabelle/HOL. Proof reconstruction task is hard, and as pointed out in [15] the reconstruction can fail, and sometimes take a tremendous amount of time. Armand [2, 10] developed a framework in the theorem prover Coq for integrating external SMT solver results. They developed a set of “small checkers” that are able to take a certificate and call corresponding small checker for proof checks and used Coq tactics for automation. They report having achieved better performance than [6]. Also working on bridging the gap between interactive theorem proving and automated theorem proving (aka solvers), Moura [22] chooses a different path to build a theorem prover called Lean which also uses Z3, the SMT solver, as a back-end, but also provides benefits of an interactive theorem prover.

Several papers showed how their methods could be used for the verification of concurrent algorithms such as clock synchronization [12], and the Bakery and Memoir algorithms [20]. Erkök [11] uses the integration to prove memory safety of small C programs. [12] used the CVC-Lite [3] SMT solver to verify properties of simple quadratic inequalities. SMT solvers have drawn interests in the programming language research where  [27] integrates SMT solvers into Haskell for proving refinement types.

Our work is based on our previous work [23]. Previously we showed how the integration of SMT solvers with theorem provers can help to prove properties of Analog/Mixed-Signal Designs. We used a single trusted clause-processor like is done in [26]. This paper describes our recent work of re-architecting Smtlink to verify the majority of it using verified clause-processors, therefore greatly improved soundness. We now depend on a very minimal core in a trusted clause-processor. In addition to that, our added support for algebraic datatypes largely broadened the horizon of problems Smtlink is able to handle.

6 Conclusion and Future Work

In this paper, we discussed an updated Smtlink. Comparing to the previous version, the current Smtlink has a more compelling argument of soundness, is extensible, and supports more theories. The architecture of Smtlink now clearly separates into verified and trusted parts. We make the trusted core as small as possible. We outlined an approach for verifying soundness, explaining how the gap is bridged between logic of ACL2 and the logic of an SMT solver. The new architecture through a sequence of verified clause-processors makes it extensible and easy to maintain. There are still many aspects we want to keep working on.

First, we want to use the meta-extract [16] capability introduced in last year’s workshop. We believe if we can use the meta-extract to fully verify several of our clause-processors, for example, the function expansion clause processor, then the clause-processor won’t need to return the clause back to ACL2 for proof. We observe that when projects get larger, the auxiliary clauses can become hard to prove, and making the clause-processor fully verified will reduce time spent on proving the auxiliary clauses to 0.

Second, given that we have the meta-extract capability, we are wondering if we can make a verified type inference engine. Currently, Smtlink knows nothing about types of terms and replies on the user for type instrumentation. Fixing functions have to be added to places where such a type information is required. For example, when dealing with a nil, which type of nil is it? We would love to deduce the types of terms and relieve the burden on the users for specifying types.

Third, we sketched a soundness proof in this paper, but this proof is not complete. Specifically we need to extend the model as described in Section 3 to a model that satisfies the real-closure axioms in a many-sorted setting. The current soundness proof sketch works well with ACL2(r) but not ACL2. We’d like to complete this proof.

Fourth, we want to make all counter-examples evaluable. This will require knowing FTY types and how to translate their constructors back. We also have to figure out how to translate algebraic numbers.

Fifth, we note Satlink has implemented a proof reconstruction interface that allows proofs to be returned from an SAT solver and replayed in ACL2. This can make the single trusted clause-processor goes away and remove all trusts we give to external SMT solvers. Proof reconstruction is an interesting direction that we might want to research more.

For applications, we believe the current Smtlink have enough capability that it can be applied to a lot of problems. We have been working on using it to verify properties of an asynchronous FIFO. The results are promising. In the future, we want to use it to prove timing properties making use of its arithmetic reasoning ability.


We would like to thank the ACL2 community for all the help in answering our questions while using ACL2 and developing Smtlink. We especially want to thank Matt Kaufmann for teaching us model theory and the many discussions to form a sketch of the soundness proof for Smtlink. We are also thankful to the anonymous reviewers for the insightful and constructive feedback.