wiki

SMT

Satisfiability modulo theories (SMT) generalizes Boolean satisfiability to formulas over richer vocabularies[1]. An SMT formula may contain integer and real variables with arithmetic, fixed-width bit-vectors with machine operations, arrays with reads and writes, algebraic datatypes, strings, and uninterpreted function symbols, all combined by Boolean connectives. A theory fixes the meaning of these symbols, and the question is whether some assignment satisfies the formula in that theory. SMT solvers answer it by pairing a SAT solver for the Boolean structure with a theory solver for each theory, an arrangement called DPLL(T)[4]. Leading solvers include Z3, cvc5 and Yices[2][5][6]. Their common input language, SMT-LIB, and their annual competition have made SMT a standard back end for software and hardware verification[3].

§ 01

Theories and logics

SMT-LIB defines theories by their signatures and axioms, and logics as fragments of them[3]. Common logics include QF_UF, quantifier-free formulas with uninterpreted functions, decided by congruence closure; QF_LRA and QF_LIA, linear real and integer arithmetic, decided by variants of the simplex method with branch-and-bound for integers; QF_IDL, integer difference logic; QF_BV, bit-vectors, usually decided by bit-blasting into SAT; and QF_AX, arrays. Formulas mixing theories are handled by Nelson–Oppen combination[14].

The quantifier-free fragments of these theories are NP-complete, being at least as hard as SAT, although their conjunctions are often polynomial. Nonlinear real arithmetic is decidable, by Tarski's quantifier elimination, and practical solvers use cylindrical algebraic decomposition inside the search[9]. Nonlinear integer arithmetic is undecidable by Matiyasevich's solution of Hilbert's tenth problem[15], so solvers for it are incomplete.

§ 02

Architecture

SMT-LIB script(assert ...)preprocessingsimplify, abstractSAT searchover atomsEUFarithmeticbit-vectorsarrays, …theory solverssat + model / unsat
The structure of a lazy SMT solver. The formula is simplified and its theory atoms are abstracted as Boolean variables for a SAT search; theory solvers check the atoms the search assigns and return conflicts and implied atoms.

Early SMT solvers translated the whole problem to SAT; modern ones are lazy[1]. The SAT solver treats each theory atom as a propositional variable and searches for an assignment satisfying the Boolean structure. Whenever propagation settles, the theory solvers check the assigned atoms. An inconsistency is reported with an explanation, which the SAT solver learns as a clause, and theory solvers can also report atoms that are implied, which the SAT solver then assigns. Preprocessing simplifies the formula beforehand, eliminating variables, normalizing arithmetic and purifying mixed terms.

A small solver for integer difference logic that reads a subset of SMT-LIB, Tseitin-encodes the Boolean structure and runs a DPLL(T) search with a difference-logic theory solver.

(* A tiny SMT solver for integer difference logic (QF_IDL) that reads a
subset of SMT-LIB: declare-const, assert, check-sat, get-model. *)
type sexp = Atom of string | List of sexp list
let parse_all (text : string) : sexp list =
let toks = ref [] and buf = Buffer.create 16 in
let flush () = if Buffer.length buf > 0 then (toks := Buffer.contents buf :: !toks; Buffer.clear buf) in
let in_comment = ref false in
String.iter (fun ch ->
if !in_comment then (if ch = '\n' then in_comment := false)
else match ch with
| ';' -> flush (); in_comment := true
| '(' | ')' -> flush (); toks := String.make 1 ch :: !toks
| ' ' | '\n' | '\t' -> flush ()
| c -> Buffer.add_char buf c) text;
flush ();
let rec sexp = function
| "(" :: rest -> let items, rest = list rest in (List items, rest)
| t :: rest -> (Atom t, rest)
| [] -> failwith "unexpected end"
and list = function
| ")" :: rest -> ([], rest)
| ts -> let s, rest = sexp ts in let items, rest = list rest in (s :: items, rest) in
let rec all = function [] -> [] | ts -> let s, rest = sexp ts in s :: all rest in
all (List.rev !toks)
(* Boolean structure over difference atoms. A lone variable x stands for
x - zero, where zero is a variable fixed to 0 in the model. *)
type formula = Lit of Dl.atom | Not of formula | And of formula list | Or of formula list
let rec term = function
| List [ Atom "-"; Atom x; Atom y ] -> (x, y)
| Atom x -> (x, "zero")
| _ -> failwith "only x or (- x y) may be compared"
let rec formula = function
| List [ Atom op; lhs; Atom k ] when List.mem op [ "<="; "<"; ">="; ">"; "=" ] ->
let x, y = term lhs and k = int_of_string k in
let le x y c = Lit { Dl.x; y; c } in
(match op with
| "<=" -> le x y k | "<" -> le x y (k - 1)
| ">=" -> le y x (-k) | ">" -> le y x (-k - 1)
| _ -> And [ le x y k; le y x (-k) ])
| List (Atom "and" :: args) -> And (List.map formula args)
| List (Atom "or" :: args) -> Or (List.map formula args)
| List [ Atom "not"; a ] -> Not (formula a)
| List [ Atom "=>"; a; b ] -> Or [ Not (formula a); formula b ]
| s -> failwith ("unsupported formula: " ^ (match s with Atom a -> a | _ -> "(...)"))
(* Tseitin encoding into clauses. Auxiliary variables are atoms without
theory content (None), which the theory ignores. *)
let encode (fs : formula list) =
let atoms = ref [] and n = ref 0 in
let fresh a = incr n; atoms := a :: !atoms; !n in
let table = Hashtbl.create 16 in
let atom a = match Hashtbl.find_opt table a with
| Some i -> i | None -> let i = fresh (Some a) in Hashtbl.add table a i; i in
let clauses = ref [] in
let rec lit = function
| Lit a -> atom a
| Not f -> - (lit f)
| And fs -> let ls = List.map lit fs and g = fresh None in
List.iter (fun l -> clauses := [ -g; l ] :: !clauses) ls;
clauses := (g :: List.map (fun l -> -l) ls) :: !clauses; g
| Or fs -> let ls = List.map lit fs and g = fresh None in
List.iter (fun l -> clauses := [ g; -l ] :: !clauses) ls;
clauses := (-g :: ls) :: !clauses; g in
List.iter (fun f -> let l = lit f in clauses := [ l ] :: !clauses) fs;
(Array.of_list (List.rev !atoms), !clauses)
module Theory = struct
type atom = Dl.atom option
type model = (string * int) list
let check lits =
match Dl.Theory.check (List.filter_map (fun (a, b) -> Option.map (fun a -> (a, b)) a) lits) with
| Ok m -> Ok m
| Error expl -> Error (List.map (fun (a, b) -> (Some a, b)) expl)
end
module S = Dpllt.Make (Theory)
(* Run a script, printing the answers to check-sat and get-model. *)
let run (script : string) =
let decls = ref [] and asserts = ref [] and last = ref None in
List.iter (function
| List [ Atom "set-logic"; _ ] -> ()
| List [ Atom "declare-const"; Atom x; Atom "Int" ] -> decls := !decls @ [ x ]
| List [ Atom "assert"; f ] -> asserts := !asserts @ [ formula f ]
| List [ Atom "check-sat" ] ->
let atoms, clauses = encode !asserts in
let r, _ = S.solve atoms clauses in
last := Option.map fst r;
print_endline (if r = None then "unsat" else "sat")
| List [ Atom "get-model" ] ->
(match !last with
| Some m ->
let z = try List.assoc "zero" m with Not_found -> 0 in
print_endline "(";
List.iter (fun x -> Printf.printf " (define-fun %s () Int %d)\n" x
((try List.assoc x m with Not_found -> z) - z)) !decls;
print_endline ")"
| None -> print_endline "(error \"no model\")")
| _ -> failwith "unsupported command") (parse_all script)

Two jobs on one machine: can they finish by hour 8, and by hour 6?

(* Two jobs of 3 and 4 hours share one machine; the second must start
within 5 hours of the first. Can both finish by hour 8? by hour 6? *)
let script deadline = Printf.sprintf {|
(set-logic QF_IDL)
(declare-const a Int) ; start of job A, 3 hours
(declare-const b Int) ; start of job B, 4 hours
(assert (>= a 0))
(assert (>= b 0))
(assert (or (<= (- a b) -3) ; A finishes before B starts
(<= (- b a) -4))) ; or B finishes before A starts
(assert (<= (- b a) 5))
(assert (<= a %d))
(assert (<= b %d))
(check-sat)
(get-model)
|} (deadline - 3) (deadline - 4)

Running it.

(set-logic QF_IDL)
(declare-const a Int) ; start of job A, 3 hours
(declare-const b Int) ; start of job B, 4 hours
(assert (>= a 0))
(assert (>= b 0))
(assert (or (<= (- a b) -3) ; A finishes before B starts
(<= (- b a) -4))) ; or B finishes before A starts
(assert (<= (- b a) 5))
(assert (<= a 5))
(assert (<= b 4))
(check-sat)
(get-model)
; output:
sat
(
(define-fun a () Int 0)
(define-fun b () Int 3)
)
; with a deadline of 6:
unsat
(error "no model")
§ 03

Quantifiers

Formulas with quantifiers, typical of program verification where axioms describe data structures, are undecidable in general. Solvers handle them by instantiation: E-matching instantiates a quantified axiom when ground terms matching a trigger pattern appear in the current assignment[7], and model-based quantifier instantiation checks a candidate model against each quantified formula and instantiates it with a counterexample if one exists[8]. Neither is complete, and a solver may answer unknown.

§ 04

Applications

SMT solvers are the reasoning engines of many program analysis tools. Deductive verifiers such as Dafny generate verification conditions from annotated programs and discharge them with Z3[11]. Symbolic execution tools such as KLEE explore program paths and ask a solver for inputs that follow each path, generating tests[10]. Refinement type checkers encode subtyping as SMT queries. Amazon Web Services uses SMT solvers to analyze the access-control policies that customers attach to their cloud resources[12]. Other uses include bounded model checking of hardware and software, scheduling and planning, and program synthesis.

§ 05

History

Decision procedures for combinations of theories go back to Nelson and Oppen's cooperating procedures of 1979[14] and Shostak's method, developed for program verifiers at Stanford and SRI. The Stanford Validity Checker of Barrett, Dill and Levitt in 1996 combined them with case splitting[13]. Around 2002 several groups coupled decision procedures with SAT solvers lazily, and the DPLL(T) framework of 2004–2006 gave the approach its standard form[4]. The SMT-LIB initiative began in 2003 to provide a common language and benchmark library, and the SMT competition has been held annually since 2005; version 2.0 of the language, in 2010, introduced the command-based format still used[3]. Z3 from Microsoft Research, released in 2008, became the most widely used solver[2], alongside the CVC line, now cvc5[5], and Yices[6].

see also

referenced by

further reading

  1. [1]C. Barrett, R. Sebastiani, S. A. Seshia, C. Tinelli, “Satisfiability modulo theories”, in A. Biere et al. (eds.), Handbook of Satisfiability, IOS Press (2nd ed., 2021).
  2. [2]L. de Moura, N. Bjørner, “Z3: an efficient SMT solver”, TACAS (2008).
  3. [3]C. Barrett, P. Fontaine, C. Tinelli, The SMT-LIB Standard, version 2.6 (2017).
  4. [4]R. Nieuwenhuis, A. Oliveras, C. Tinelli, “Solving SAT and SAT Modulo Theories: from an abstract Davis–Putnam–Logemann–Loveland procedure to DPLL(T)”, Journal of the ACM 53 (2006).
  5. [5]H. Barbosa et al., “cvc5: a versatile and industrial-strength SMT solver”, TACAS (2022).
  6. [6]B. Dutertre, “Yices 2.2”, CAV (2014).
  7. [7]L. de Moura, N. Bjørner, “Efficient E-matching for SMT solvers”, CADE (2007).
  8. [8]Y. Ge, L. de Moura, “Complete instantiation for quantified formulas in satisfiability modulo theories”, CAV (2009).
  9. [9]D. Jovanović, L. de Moura, “Solving non-linear arithmetic”, IJCAR (2012).
  10. [10]C. Cadar, D. Dunbar, D. Engler, “KLEE: unassisted and automatic generation of high-coverage tests for complex systems programs”, OSDI (2008).
  11. [11]K. R. M. Leino, “Dafny: an automatic program verifier for functional correctness”, LPAR (2010).
  12. [12]J. Backes et al., “Semantic-based automated reasoning for AWS access policies using SMT”, FMCAD (2018).
  13. [13]C. Barrett, D. L. Dill, J. R. Levitt, “Validity checking for combinations of theories with equality”, FMCAD (1996).
  14. [14]G. Nelson, D. C. Oppen, “Simplification by cooperating decision procedures”, ACM Transactions on Programming Languages and Systems 1 (1979).
  15. [15]Yu. V. Matiyasevich, “Enumerable sets are Diophantine”, Soviet Mathematics Doklady 11 (1970).

last updated