Bit-blasting
Bit-blasting decides formulas over fixed-width bit-vectors, the machine integers of programs and hardware, by reducing them to propositional satisfiability. Each -bit variable becomes Boolean variables, each operation becomes a Boolean circuit over those bits, the circuits are encoded as clauses with the Tseitin transformation, and a SAT solver decides the result[1]. Addition becomes a ripple-carry adder, comparison a chain of bit comparisons, and multiplication an array of adders. The method is complete, since every bit-vector operation is a finite Boolean function, and it is how SMT solvers decide the theory of bit-vectors after simplifying at the word level[3][4][5]. Its weakness is multiplication, whose circuits are quadratic in the width and notoriously hard for SAT solvers to reason about[7].
Bit-vector semantics
A bit-vector of width is a sequence of bits, read as an unsigned integer in or as a two's-complement integer in . Arithmetic is modulo : is , and overflow wraps around, exactly as in machine arithmetic. The SMT-LIB theory of fixed-size bit-vectors, QF_BV in its quantifier-free form, provides arithmetic, bitwise operations, shifts, signed and unsigned comparisons, concatenation and extraction[11]. Bit-precise reasoning is what program analysis needs: a verifier that treats machine integers as mathematical integers misses overflow bugs, and one that treats them as bit-vectors finds them.
Encoding
Every Boolean gate becomes a fresh variable with clauses that force it to equal its function of the inputs[2]:
Arithmetic is built from these. A full adder computes the sum bit and the carry of three bits:
and of them in a chain add two -bit numbers modulo , using gates. Multiplication adds shifted partial products , using gates. Equality is a conjunction of bitwise equivalences, and unsigned less-than a chain that decides at the most significant differing bit[1].
A small DPLL solver with watched literals, used by the examples.
(* A small DPLL solver with two watched literals and no clause learning,enough for the circuits below. Literals are non-zero integers. *)type t = {mutable nvars : int;mutable clauses : int array list;}let create () = { nvars = 0; clauses = [] }let fresh s = s.nvars <- s.nvars + 1; s.nvarslet add s c = s.clauses <- Array.of_list c :: s.clauseslet decisions = ref 0let solve s =let n = s.nvars inlet cls = Array.of_list s.clauses inlet assign = Array.make (n + 1) 0 and trail = Array.make (n + 1) 0 and len = ref 0 and qhead = ref 0 inlet idx l = if l > 0 then 2 * l else (2 * -l) + 1 inlet watches = Array.make ((2 * n) + 2) [] inlet value l = let v = assign.(abs l) in if l > 0 then v else -v inlet set l = assign.(abs l) <- (if l > 0 then 1 else -1); trail.(!len) <- l; incr len inlet units = ref [] inArray.iteri (fun ci c ->if Array.length c = 1 then units := c.(0) :: !unitselse (watches.(idx c.(0)) <- ci :: watches.(idx c.(0)); watches.(idx c.(1)) <- ci :: watches.(idx c.(1)))) cls;let propagate () =let ok = ref true inwhile !ok && !qhead < !len dolet f = - trail.(!qhead) inincr qhead;let pending = ref watches.(idx f) inwatches.(idx f) <- [];while !pending <> [] dolet ci = List.hd !pending inpending := List.tl !pending;let c = cls.(ci) inif c.(0) = f then (c.(0) <- c.(1); c.(1) <- f);let keep () = watches.(idx f) <- ci :: watches.(idx f) inif value c.(0) = 1 then keep ()else beginlet k = ref 2 inwhile !k < Array.length c && value c.(!k) = -1 do incr k done;if !k < Array.length c then (c.(1) <- c.(!k); c.(!k) <- f; watches.(idx c.(1)) <- ci :: watches.(idx c.(1)))else beginkeep ();if value c.(0) = -1 then (ok := false; watches.(idx f) <- !pending @ watches.(idx f); pending := [])else set c.(0)endenddonedone;!okinlet undo mark = for i = !len - 1 downto mark do assign.(abs trail.(i)) <- 0 done; len := mark; qhead := mark inlet rec dpll () =if not (propagate ()) then falseelselet rec first v = if v > n then 0 else if assign.(v) = 0 then v else first (v + 1) inlet v = first 1 inif v = 0 then trueelse beginlet mark = !len inincr decisions;set (-v);dpll () || (undo mark; set v; dpll ()) || (undo mark; false)endinlet ok = List.for_all (fun l -> if value l = -1 then false else (if value l = 0 then set l; true)) !units inif ok && dpll () then Some (Array.copy assign) else None
Bit-vector circuits encoded as clauses.
(* Bit-blasting: a bit-vector is an array of SAT literals, leastsignificant bit first, and every operation is a circuit whose gatesare encoded as clauses (the Tseitin encoding). Each instance of thefunctor has its own solver. *)module type S = sigval s : Sat.tval clause : int list -> unitval var : int -> int arrayval add : int array -> int array -> int arrayval mul : int array -> int array -> int arrayval eq : int array -> int array -> intendmodule Make () = structlet s = Sat.create ()let fresh () = Sat.fresh slet clause = Sat.add s(* Constants 1 and 0 as literals. *)let tt = let v = fresh () in clause [ v ]; vlet ff = - tt(* g <-> a /\ b, g <-> a \/ b, g <-> a xor b *)let and_ a b = let g = fresh () in clause [ -g; a ]; clause [ -g; b ]; clause [ g; -a; -b ]; glet or_ a b = - (and_ (-a) (-b))let xor_ a b =let g = fresh () inclause [ -g; a; b ]; clause [ -g; -a; -b ]; clause [ g; -a; b ]; clause [ g; a; -b ]; glet var w = Array.init w (fun _ -> fresh ())let const w k = Array.init w (fun i -> if (k lsr i) land 1 = 1 then tt else ff)(* Ripple-carry addition modulo 2^w: a full adder per bit. *)let add a b =let carry = ref ff inArray.init (Array.length a) (fun i ->let s = xor_ (xor_ a.(i) b.(i)) !carry incarry := or_ (and_ a.(i) b.(i)) (and_ !carry (xor_ a.(i) b.(i)));s)(* Shift-and-add multiplication modulo 2^w: w partial products. *)let mul a b =let w = Array.length a inlet acc = ref (const w 0) infor i = 0 to w - 1 dolet partial = Array.init w (fun j -> if j < i then ff else and_ a.(j - i) b.(i)) inacc := add !acc partialdone;!acc(* Equality of two vectors as one literal, and unsigned a < b. *)let eq a b = let d = Array.map2 (fun x y -> - (xor_ x y)) a b in Array.fold_left and_ tt dlet ult a b =(* scan from the least significant bit: lt := (not a_i and b_i) or (a_i = b_i and lt) *)let lt = ref ff inArray.iteri (fun i x -> let y = b.(i) in lt := or_ (and_ (-x) y) (and_ (- (xor_ x y)) !lt)) a;!ltlet value model v = Array.fold_left (fun (acc, k) l -> ((if model.(abs l) = (if l > 0 then 1 else -1) then acc lor (1 lsl k) else acc), k + 1)) (0, 0) v |> fstend
Solving
A query asserts the output literal of a circuit and asks for a satisfying assignment, from which the values of the bit-vectors are read back. Asking for with finds a factorization:
Factoring 143 with a multiplier circuit, with and without overflow.
(* Factor 143 with multiplier circuits. Bit-vector multiplication ismodulo 2^w, so the first query finds a product that wraps around; thesecond zero-extends 8-bit operands to 16 bits so that it cannot. *)let query w ext =let module B = Bv.Make () inlet open B inlet x = var 8 and y = var 8 inlet widen v = if ext then Array.append v (const 8 0) else v inlet p = mul (widen x) (widen y) inclause [ eq p (const w 143) ];clause [ ult (const 8 1) x ];clause [ ult (const 8 1) y ];let nv = s.Sat.nvars and nc = List.length s.Sat.clauses inmatch Sat.solve s with| Some m -> Printf.printf "%2d-bit product: %4d variables, %5d clauses: x = %3d, y = %3d, x * y = %d\n" w nv nc (value m x) (value m y) (value m x * value m y)| None -> Printf.printf "%2d-bit product: unsatisfiable\n" w
Running it.
8-bit product: 517 variables, 1720 clauses: x = 129, y = 15, x * y = 193516-bit product: 1785 variables, 6108 clauses: x = 13, y = 11, x * y = 143
The 8-bit query finds : a correct answer to the question that was asked, since 8-bit multiplication wraps around. Zero-extending the operands to 16 bits before multiplying rules out overflow and gives the factorization . The distinction is the one a program verifier has to get right, and the kind of bug bit-precise reasoning is used to find.
Proving identities
A property holds for all inputs exactly when its negation is unsatisfiable. Commutativity of addition and multiplication at width is proved by asserting or and finding no model:
Proving commutativity by unsatisfiability.
(* x + y = y + x and x * y = y * x hold for all w-bit x and y exactly whentheir negations are unsatisfiable. *)let prove w name op =let module B = Bv.Make () inlet x = B.var w and y = B.var w inlet l = op (module B : Bv.S) x y and r = op (module B : Bv.S) y x inB.clause [ - (B.eq l r) ];Sat.decisions := 0;let result = Sat.solve B.s inPrintf.printf "%-3s w = %-2d %6d vars %7d clauses %s %9d decisions\n%!" name w B.s.Sat.nvars(List.length B.s.Sat.clauses) (if result = None then "proved " else "counterexample") !Sat.decisions
Running it with the DPLL solver above.
x+y w = 4 65 vars 198 clauses proved 255 decisionsx+y w = 6 97 vars 296 clauses proved 4095 decisionsx+y w = 8 129 vars 394 clauses proved 65535 decisionsx+y w = 10 161 vars 492 clauses proved 1048575 decisionsx*y w = 3 133 vars 437 clauses proved 42 decisionsx*y w = 4 229 vars 762 clauses proved 170 decisionsx*y w = 5 351 vars 1177 clauses proved 682 decisionsx*y w = 6 499 vars 1682 clauses proved 2730 decisionsx*y w = 7 673 vars 2277 clauses proved 10922 decisions
Without clause learning, the solver proves the adder identity by trying every combination of inputs, decisions, since it branches on the input bits first and only finds a conflict once all of them are set. A CDCL solver learns clauses that summarize why each carry must agree and proves adder identities for any practical width almost immediately. Multipliers are different: equivalence of two multiplier circuits, even commutativity of one, remains hard for CDCL solvers even at moderate widths, and dedicated methods based on computer algebra are used to verify large multipliers[7]. Binary decision diagrams have the same difficulty: Bryant proved that every BDD for the middle output bit of a multiplier has exponential size[8].
In SMT solvers
Solvers do not bit-blast a formula as written. Word-level preprocessing first rewrites it: constant propagation, normalization of arithmetic, elimination of variables defined by equations, and simplifications such as replacing multiplication by a constant with shifts and additions. What remains is bit-blasted, eagerly, all at once, or lazily, with parts of the formula handled at the word level and blasted only when needed[6]. STP[3] and Boolector[4] pioneered this for program analysis; Z3[5], cvc5 and Bitwuzla are current examples. Because the result is a SAT problem, bit-vector solvers inherit every improvement in SAT solving.
Uses
Symbolic execution tools such as KLEE represent program values as bit-vector expressions and ask a solver for inputs that reach each branch[9]. Bounded model checking unrolls a hardware or software transition system for a fixed number of steps and bit-blasts the result into one SAT query[10]. Equivalence checking of circuits, superoptimization of instruction sequences, translation validation in compilers and the analysis of cryptographic code all reduce to bit-vector queries.
History
Reducing arithmetic on machine words to propositional logic is as old as circuit design, and Tseitin's encoding of 1968 made the translation to clauses linear[2]. Bounded model checking made bit-blasted SAT queries a mainstream verification technique in 1999[10]. Ganesh and Dill's STP of 2007 and Brummayer and Biere's Boolector of 2009 showed that word-level preprocessing followed by bit-blasting could handle the formulas produced by program analysis[3][4], and bit-vectors became one of the most used theories of SMT-LIB[11].
see also
- SATThe Boolean satisfiability problem, SAT, asks whether some assignment of true and false to the variables of a propositional formula makes the formula true. It was the first problem shown to be NP-complete, so every problem in NP reduces to it, and no polynomial-time algorithm is known. In practice SAT solvers based on conflict-driven clause learning decide industrial instances with millions of variables, which has made SAT a general-purpose engine for hardware and software verification, planning, scheduling and combinatorics.
- Tseitin transformationThe Tseitin transformation converts an arbitrary propositional formula or Boolean circuit into conjunctive normal form in linear time by introducing a new variable for each subformula and adding a few clauses that define it as the result of its operator: g ↔ a ∧ b becomes (¬g ∨ a), (¬g ∨ b) and (g ∨ ¬a ∨ ¬b). The result is not equivalent to the original formula but equisatisfiable, and every model of one extends or restricts to a model of the other. It is how SAT solvers receive circuits and structured formulas; the Plaisted–Greenbaum variant emits only the implications required by each subformula's polarity, roughly halving the clauses.
- SMTSatisfiability modulo theories (SMT) is the problem of deciding whether a first-order formula is satisfiable when some of its symbols are interpreted by background theories, such as integer or real arithmetic, bit-vectors, arrays and uninterpreted functions. SMT solvers such as Z3 and cvc5 combine a SAT solver, which handles the Boolean structure, with specialized decision procedures for the theories, usually in the DPLL(T) architecture. They read the standard SMT-LIB language and are the reasoning engines of program verifiers, symbolic execution tools, test generators and many other analysis tools.
- CNFA propositional formula is in conjunctive normal form (CNF) when it is a conjunction of clauses, each clause a disjunction of literals, and each literal a variable or its negation. Every formula is equivalent to one in CNF, but the equivalent CNF can be exponentially larger; the Tseitin transformation instead produces an equisatisfiable CNF of linear size by adding variables. CNF is the input format of SAT solvers and resolution provers, and restricted forms such as Horn clauses and 2-CNF can be solved in linear time.
- CDCLConflict-driven clause learning (CDCL) is the algorithm of modern SAT solvers. It extends DPLL backtracking search with three ideas: when propagation falsifies a clause, conflict analysis derives a new clause that explains the conflict and adds it to the formula; the solver then backjumps non-chronologically to the decision level at which the new clause forces a literal; and branching follows the variables involved in recent conflicts. With watched literals, restarts and clause deletion, CDCL solvers decide industrial formulas with millions of variables, and with restarts they are as strong as general resolution, exponentially stronger than DPLL.
further reading
- [1]D. Kroening, O. Strichman, Decision Procedures: An Algorithmic Point of View, ch. 6, Springer (2nd ed., 2016).
- [2]G. S. Tseitin, “On the complexity of derivation in propositional calculus”, Studies in Constructive Mathematics and Mathematical Logic, Part II (1968).
- [3]V. Ganesh, D. L. Dill, “A decision procedure for bit-vectors and arrays”, Computer Aided Verification, LNCS 4590 (2007).
- [4]R. Brummayer, A. Biere, “Boolector: an efficient SMT solver for bit-vectors and arrays”, TACAS, LNCS 5505 (2009).
- [5]L. de Moura, N. Bjørner, “Z3: an efficient SMT solver”, TACAS, LNCS 4963 (2008).
- [6]L. Hadarean, K. Bansal, D. Jovanović, C. Barrett, C. Tinelli, “A tale of two solvers: eager and lazy approaches to bit-vectors”, Computer Aided Verification, LNCS 8559 (2014).
- [7]D. Kaufmann, A. Biere, M. Kauers, “Verifying large multipliers by combining SAT and computer algebra”, Formal Methods in Computer-Aided Design (2019).
- [8]R. E. Bryant, “On the complexity of VLSI implementations and graph representations of Boolean functions with application to integer multiplication”, IEEE Transactions on Computers 40 (1991).
- [9]C. Cadar, D. Dunbar, D. Engler, “KLEE: unassisted and automatic generation of high-coverage tests for complex systems programs”, OSDI (2008).
- [10]A. Biere, A. Cimatti, E. Clarke, Y. Zhu, “Symbolic model checking without BDDs”, TACAS, LNCS 1579 (1999).
- [11]C. Barrett, P. Fontaine, C. Tinelli, The SMT-LIB Standard: Version 2.6 (2017).
last updated