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 ith bit of x * y can be obtained by
evaluating the ith 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_eqandmulRec_succ_eq, which are applied repeatedly when the width is 0and when the width isw' + 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[idx], 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 wis(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.
Carry function for bitwise addition.
Equations
- BitVec.adcb x y c = (x.atLeastTwo y c, x ^^ (y ^^ c))
Instances For
add #
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
-xis the same as the least significant bit ofx, and - The
i+1-th least significant bit of-xis 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 ith 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 ith 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.The denominator is positive.
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 ofnobey the invariant for division.
Instances For
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
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.
Only perform a round of shift-subtract if we have dividend bits.
Instances For
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)
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.