Bitblasting of bitvectors #
This module provides theorems for showing the equivalence between BitVec operations using
the Fin 2^n
representation and Boolean vectors. It is still under development, but
intended to provide a path for converting SAT and SMT solver proofs about BitVectors
as vectors of bits into proofs about Lean BitVec
values.
The module is named for the bit-blasting operation in an SMT solver that converts bitvector expressions into expressions about individual bits in each vector.
Example: How bitblasting works for multiplication #
We explain how the lemmas here are used for bitblasting,
by using multiplication as a prototypical example.
Other bitblasters for other operations follow the same pattern.
To bitblast a multiplication of the form x * y
,
we must unfold the above into a form that the SAT solver understands.
We assume that the solver already knows how to bitblast addition.
This is known to bv_decide
, by exploiting the lemma add_eq_adc
,
which says that x + y : BitVec w
equals (adc x y false).2
,
where adc
builds an add-carry circuit in terms of the primitive operations
(bitwise and, bitwise or, bitwise xor) that bv_decide already understands.
In this way, we layer bitblasters on top of each other,
by reducing the multiplication bitblaster to an addition operation.
The core lemma is given by getLsbD_mul
:
x y : BitVec w ⊢ (x * y).getLsbD i = (mulRec x y w).getLsbD i
Which says that the i
th bit of x * y
can be obtained by
evaluating the i
th bit of (mulRec x y w)
.
Once again, we assume that bv_decide
knows how to implement getLsbD
,
given that mulRec
can be understood by bv_decide
.
We write two lemmas to enable bv_decide
to unfold (mulRec x y w)
into a complete circuit, when w
is a known constant. This is given by two recurrence lemmas,
mulRec_zero_eqand
mulRec_succ_eq, which are applied repeatedly when the width is
0and when the width is
w' + 1`:
mulRec_zero_eq :
mulRec x y 0 =
if y.getLsbD 0 then x else 0
mulRec_succ_eq
mulRec x y (s + 1) =
mulRec x y s +
if y.getLsbD (s + 1) then (x <<< (s + 1)) else 0 := rfl
By repeatedly applying the lemmas mulRec_zero_eq
and mulRec_succ_eq
,
one obtains a circuit for multiplication.
Note that this circuit uses BitVec.add
, BitVec.getLsbD
, BitVec.shiftLeft
.
Here, BitVec.add
and BitVec.shiftLeft
are (recursively) bitblasted by bv_decide
,
using the lemmas add_eq_adc
and shiftLeft_eq_shiftLeftRec
,
and BitVec.getLsbD
is a primitive that bv_decide
knows how to reduce to SAT.
The two lemmas, mulRec_zero_eq
, and mulRec_succ_eq
,
are used in Std.Tactic.BVDecide.BVExpr.bitblast.blastMul
to prove the correctness of the circuit that is built by bv_decide
.
def blastMul (aig : AIG BVBit) (input : AIG.BinaryRefVec aig w) : AIG.RefVecEntry BVBit w
theorem denote_blastMul (aig : AIG BVBit) (lhs rhs : BitVec w) (assign : Assignment) :
...
⟦(blastMul aig input).aig, (blastMul aig input).vec.get idx hidx, assign.toAIGAssignment⟧
=
(lhs * rhs).getLsbD idx
The definition and theorem above are internal to bv_decide
,
and use mulRec_{zero,succ}_eq
to prove that the circuit built by bv_decide
computes the correct value for multiplication.
To zoom out, therefore, we follow two steps:
First, we prove bitvector lemmas to unfold a high-level operation (such as multiplication)
into already bitblastable operations (such as addition and left shift).
We then use these lemmas to prove the correctness of the circuit that bv_decide
builds.
We use this workflow to implement bitblasting for all SMT-LIB2 operations.
Main results #
x + y : BitVec w
is(adc x y false).2
.
Future work #
All other operations are to be PR'ed later and are already proved in https://github.com/mhk119/lean-smt/blob/bitvec/Smt/Data/Bitwise.lean.
Preliminaries #
Addition #
If x &&& y = 0
, then the carry bit (x + y + 0)
is always false
for any index i
.
Intuitively, this is because a carry is only produced when at least two of x
, y
, and the
previous carry are true. However, since x &&& y = 0
, at most one of x, y
can be true,
and thus we never have a previous carry, which means that the sum cannot produce a carry.
Bitwise addition implemented via a ripple carry adder.
Equations
- x.adc y = BitVec.iunfoldr fun (i : Fin w) (c : Bool) => BitVec.adcb (x.getLsbD ↑i) (y.getLsbD ↑i) c
Instances For
add #
Adding a bitvector to its own complement yields the all ones bitpattern
Subtracting x
from the all ones bitvector is equivalent to taking its complement
Sub #
Negation #
Remember that negating a bitvector is equal to incrementing the complement
by one, i.e., -x = ~~~x + 1
. See also neg_eq_not_add
.
This computation has two crucial properties:
- The least significant bit of
-x
is the same as the least significant bit ofx
, and - The
i+1
-th least significant bit of-x
is the complement of thei+1
-th bit ofx
, unless all of the preceding bits arefalse
, in which case the bit is equal to thei+1
-th bit ofx
abs #
Inequalities (le / lt) #
mul recurrence for bitblasting #
A recurrence that describes multiplication as repeated addition. Is useful for bitblasting multiplication.
Equations
Instances For
Recurrence lemma: truncating to i+1
bits and then zero extending to w
equals truncating upto i
bits [0..i-1]
, and then adding the i
th bit of x
.
Recurrence lemma: truncating to i+1
bits and then zero extending to w
equals truncating upto i
bits [0..i-1]
, and then adding the i
th bit of x
.
Equations
Instances For
Recurrence lemma: multiplying x
with the first s
bits of y
is the
same as truncating y
to s
bits, then zero extending to the original length,
and performing the multplication.
Recurrence lemma: multiplying x
with the first s
bits of y
is the
same as truncating y
to s
bits, then zero extending to the original length,
and performing the multplication.
Instances For
shiftLeft recurrence for bitblasting #
shiftLeftRec x y n
shifts x
to the left by the first n
bits of y
.
The theorem shiftLeft_eq_shiftLeftRec
proves the equivalence of (x <<< y)
and shiftLeftRec
.
Together with equations shiftLeftRec_zero
, shiftLeftRec_succ
,
this allows us to unfold shiftLeft
into a circuit for bitblasting.
Equations
- x.shiftLeftRec y 0 = x <<< (y &&& BitVec.twoPow w₂ 0)
- x.shiftLeftRec y s_1.succ = x.shiftLeftRec y s_1 <<< (y &&& BitVec.twoPow w₂ s_1.succ)
Instances For
shiftLeftRec x y n
shifts x
to the left by the first n
bits of y
.
Show that x <<< y
can be written in terms of shiftLeftRec
.
This can be unfolded in terms of shiftLeftRec_zero
, shiftLeftRec_succ
for bitblasting.
udiv/urem recurrence for bitblasting #
In order to prove the correctness of the division algorithm on the integers,
one shows that n.div d = q
and n.mod d = r
iff n = d * q + r
and 0 ≤ r < d
.
Mnemonic: n
is the numerator, d
is the denominator, q
is the quotient, and r
the remainder.
This uniqueness of decomposition is not true for bitvectors.
For n = 0, d = 3, w = 3
, we can write:
Such examples can be created by choosing different (q, r)
for a fixed (d, n)
such that (d * q + r)
overflows and wraps around to equal n
.
This tells us that the division algorithm must have more restrictions than just the ones
we have for integers. These restrictions are captured in DivModState.Lawful
.
The key idea is to state the relationship in terms of the toNat values of {n, d, q, r}.
If the division equation d.toNat * q.toNat + r.toNat = n.toNat
holds,
then n.udiv d = q
and n.umod d = r
.
Following this, we implement the division algorithm by repeated shift-subtract.
References:
- Fast 32-bit Division on the DSP56800E: Minimized nonrestoring division algorithm by David Baca
- Bitwuzla sources for bitblasting.h
DivModState #
DivModState
is a structure that maintains the state of recursive divrem
calls.
- wn : Nat
The number of bits in the numerator that are not yet processed
- wr : Nat
The number of bits in the remainder (and quotient)
- q : BitVec w
The current quotient.
- r : BitVec w
The current remainder.
Instances For
DivModArgs
contains the arguments to a divrem
call which remain constant throughout
execution.
Instances For
A DivModState
is lawful if the remainder width wr
plus the numerator width wn
equals w
,
and the bitvectors r
and n
have values in the bounds given by bitwidths wr
, resp. wn
.
This is a proof engineering choice: an alternative world could have been
r : BitVec wr
and n : BitVec wn
, but this required much more dependent typing coercions.
Instead, we choose to declare all involved bitvectors as length w
, and then prove that
the values are within their respective bounds.
We start with wn = w
and wr = 0
, and then in each step, we decrement wn
and increment wr
.
In this way, we grow a legal remainder in each loop iteration.
The sum of widths of the dividend and remainder is
w
.- hdPos : 0 < args.d
The denominator is positive.
- hrLtDivisor : qr.r.toNat < args.d.toNat
The remainder is strictly less than the denominator.
The remainder is morally a
Bitvec wr
, and so has value less than2^wr
.The quotient is morally a
Bitvec wr
, and so has value less than2^wr
.The low
(w - wn)
bits ofn
obey the invariant for division.
Instances For
The sum of widths of the dividend and remainder is w
.
The denominator is positive.
The remainder is strictly less than the denominator.
The remainder is morally a Bitvec wr
, and so has value less than 2^wr
.
The quotient is morally a Bitvec wr
, and so has value less than 2^wr
.
The low (w - wn)
bits of n
obey the invariant for division.
A lawful DivModState implies w > 0
.
Equations
- ⋯ = ⋯
Instances For
An initial value with both q, r = 0
.
Equations
- BitVec.DivModState.init w = { wn := w, wr := 0, q := 0#w, r := 0#w }
Instances For
The initial state is lawful.
Equations
- ⋯ = ⋯
Instances For
A lawful DivModState with a fully consumed dividend (wn = 0
) witnesses that the
quotient has been correctly computed.
A lawful DivModState with a fully consumed dividend (wn = 0
) witnesses that the
remainder has been correctly computed.
DivModState.Poised #
A Poised
DivModState is a state which is Lawful
and furthermore, has at least
one numerator bit left to process (0 < wn)
The input to the shift subtractor is a legal input to divrem
, and we also need to have an
input bit to perform shift subtraction on, and thus we need 0 < wn
.
- hdPos : 0 < args.d
- hrLtDivisor : qr.r.toNat < args.d.toNat
- hwn_lt : 0 < qr.wn
Only perform a round of shift-subtract if we have dividend bits.
Instances For
Only perform a round of shift-subtract if we have dividend bits.
In the shift subtract input, the dividend is at least one bit long (wn > 0
), so
the remainder has bits to be computed (wr < w
).
Equations
- ⋯ = ⋯
Instances For
Division shift subtractor #
One round of the division algorithm, that tries to perform a subtract shift.
Note that this should only be called when r.msb = false
, so we will not overflow.
Equations
- One or more equations did not get rendered due to their size.
Instances For
The value of shifting right by wn - 1
equals shifting by wn
and grabbing the lsb at (wn - 1)
.
We show that the output of divSubtractShift
is lawful, which tells us that it
obeys the division equation.
Core division algorithm circuit #
A recursive definition of division for bitblasting, in terms of a shift-subtraction circuit.
Equations
- BitVec.divRec 0 args qr = qr
- BitVec.divRec s_1.succ args qr = BitVec.divRec s_1 args (BitVec.divSubtractShift args qr)
Instances For
The output of divRec
is a lawful state
The output of divRec
has no more bits left to process (i.e., wn = 0
)
The result of udiv
agrees with the result of the division recurrence.
The result of umod
agrees with the result of the division recurrence.
sshiftRightRec x y n
shifts x
arithmetically/signed to the right by the first n
bits of y
.
The theorem sshiftRight_eq_sshiftRightRec
proves the equivalence of (x.sshiftRight y)
and sshiftRightRec
.
Together with equations sshiftRightRec_zero
, sshiftRightRec_succ
,
this allows us to unfold sshiftRight
into a circuit for bitblasting.
Equations
- x.sshiftRightRec y 0 = x.sshiftRight' (y &&& BitVec.twoPow w₂ 0)
- x.sshiftRightRec y s_1.succ = (x.sshiftRightRec y s_1).sshiftRight' (y &&& BitVec.twoPow w₂ s_1.succ)
Instances For
If y &&& z = 0
, x.sshiftRight (y ||| z) = (x.sshiftRight y).sshiftRight z
.
This follows as y &&& z = 0
implies y ||| z = y + z
,
and thus x.sshiftRight (y ||| z) = x.sshiftRight (y + z) = (x.sshiftRight y).sshiftRight z
.
Show that x.sshiftRight y
can be written in terms of sshiftRightRec
.
This can be unfolded in terms of sshiftRightRec_zero_eq
, sshiftRightRec_succ_eq
for bitblasting.
ushiftRightRec x y n
shifts x
logically to the right by the first n
bits of y
.
The theorem shiftRight_eq_ushiftRightRec
proves the equivalence
of (x >>> y)
and ushiftRightRec
.
Together with equations ushiftRightRec_zero
, ushiftRightRec_succ
,
this allows us to unfold ushiftRight
into a circuit for bitblasting.
Equations
- x.ushiftRightRec y 0 = x >>> (y &&& BitVec.twoPow w₂ 0)
- x.ushiftRightRec y s_1.succ = x.ushiftRightRec y s_1 >>> (y &&& BitVec.twoPow w₂ s_1.succ)
Instances For
Show that x >>> y
can be written in terms of ushiftRightRec
.
This can be unfolded in terms of ushiftRightRec_zero
, ushiftRightRec_succ
for bitblasting.