Skip to content

Commit 67dffca

Browse files
kim-emdigama0claude
authored
feat: add BitVec.flattenList and lemmas (#3727)
This PR adds `BitVec.flattenList`, which concatenates a list of bitvectors of a common width into a single bitvector, together with lemmas describing its bits: `getLsbD_flattenList` and `getMsbD_flattenList` compute an individual bit in terms of the corresponding element of the list, and `extractLsb_flattenList` describes extracting a contiguous range that falls within a single element. For efficient execution, `flattenList` is replaced at runtime via `@[csimp]` with a divide-and-conquer implementation costing `O(n * L * log L)` rather than the `O(n * L²)` of a naive left fold (≈900x faster at a million elements), while keeping `O(log L)` recursion depth so it remains stack-safe. --------- Co-authored-by: Mario Carneiro <di.gama@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 548f78b commit 67dffca

3 files changed

Lines changed: 156 additions & 0 deletions

File tree

src/Init/Data/BitVec/Basic.lean

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,23 @@ def replicate : (i : Nat) → BitVec w → BitVec (w*i)
651651
| n+1, x =>
652652
(x ++ replicate n x).cast (by rw [Nat.mul_succ]; omega)
653653

654+
/-! ### flatten -/
655+
656+
/--
657+
Flatten a list of bitvectors into one bitvector.
658+
659+
This is `noncomputable` so that the only compiled implementation is the
660+
divide-and-conquer `BitVec.flattenListFast`, installed via `@[csimp]`; see there
661+
for the cost analysis.
662+
-/
663+
protected noncomputable def flattenList {n : Nat} (xs : List (BitVec n)) : BitVec (n * xs.length) :=
664+
match xs with
665+
| [] => 0#0
666+
| x :: rest =>
667+
have h : n + n * List.length rest = n * List.length (x :: rest) := by
668+
simp [List.length_cons, Nat.mul_add, Nat.add_comm]
669+
BitVec.cast h (x ++ (BitVec.flattenList rest))
670+
654671
/-!
655672
### Cons and Concat
656673
We give special names to the operations of adding a single bit to either end of a bitvector.

src/Init/Data/BitVec/Lemmas.lean

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import all Init.Data.BitVec.BasicAux
1111
public import Init.Data.Fin.Lemmas
1212
public import Init.Data.List.BasicAux
1313
import Init.Data.List.Lemmas
14+
import Init.Data.List.TakeDrop
15+
import Init.Data.List.Nat.TakeDrop
1416
public import Init.Data.BitVec.Basic
1517
import Init.ByCases
1618
import Init.Data.BitVec.Bootstrap
@@ -2934,6 +2936,130 @@ theorem setWidth_append {x : BitVec w} {y : BitVec v} :
29342936
simp only [getElem_xor, getElem_append]
29352937
split <;> simp
29362938

2939+
/-! ## flattenList -/
2940+
2941+
@[simp] theorem getMsbD_flattenList {w : Nat} (vs : List (BitVec w)) (i) :
2942+
(BitVec.flattenList vs).getMsbD i = (vs[i / w]?.getD 0#w).getMsbD (i % w) := by
2943+
by_cases p : 0 < w
2944+
· match vs with
2945+
| [] => simp
2946+
| x :: rest =>
2947+
simp only [BitVec.flattenList, getMsbD_cast, getMsbD_append]
2948+
split <;> rename_i h
2949+
· obtain ⟨j, rfl⟩ := Nat.exists_eq_add_of_le h
2950+
rw [getMsbD_flattenList]
2951+
simp [p, Nat.add_div_left, Nat.add_sub_cancel_left]
2952+
· have hi0 : i / w = 0 := Nat.div_eq_of_lt (by omega)
2953+
have him : i % w = i := Nat.mod_eq_of_lt (by omega)
2954+
rw [hi0, him]
2955+
simp
2956+
· simp at p
2957+
subst p
2958+
simp
2959+
2960+
@[simp] theorem getLsbD_flattenList {w : Nat} (vs : List (BitVec w)) (i) :
2961+
(BitVec.flattenList vs).getLsbD i =
2962+
(decide (i < w * vs.length) && (vs[vs.length - 1 - i / w]?.getD 0#w).getLsbD (i % w)) := by
2963+
by_cases p : 0 < w
2964+
· rw [getLsbD_eq_getMsbD, getMsbD_flattenList, getLsbD_eq_getMsbD]
2965+
have h₁ : i % w < w := Nat.mod_lt i p
2966+
simp only [h₁, decide_true, Bool.true_and]
2967+
by_cases h₂ : i < w * vs.length
2968+
· simp only [h₂, decide_true, Bool.true_and]
2969+
congr
2970+
· rw [Nat.sub_sub, Nat.add_comm, Nat.mul_sub_div _ _ _ h₂, Nat.sub_sub, Nat.add_comm]
2971+
· rw [Nat.sub_sub, Nat.add_comm, Nat.mul_sub_mod h₂, Nat.sub_sub, Nat.add_comm]
2972+
· simp only [h₂, decide_false, Bool.false_and]
2973+
· simp_all
2974+
2975+
@[simp] theorem flattenList_zero_length (vs : List (BitVec 0)) : BitVec.flattenList vs = 0 := by
2976+
apply eq_of_getLsbD_eq
2977+
intro i hi
2978+
simp at hi
2979+
2980+
/--
2981+
Divide-and-conquer worker for `BitVec.flattenList`, returning the underlying
2982+
natural number value with the head of the list in the most significant bits.
2983+
2984+
Splitting the list in half means each bit region is shifted `O(log xs.length)`
2985+
times rather than once per element, so flattening a list of length `L` costs
2986+
`O(n * L * log L)` instead of the `O(n * L²)` of a left fold (which reshifts the
2987+
whole growing accumulator at every step). The recursion depth is `O(log L)`, so
2988+
this also does not overflow the stack.
2989+
-/
2990+
def flattenList.toNatAux {n : Nat} : List (BitVec n) → Nat
2991+
| [] => 0
2992+
| [x] => x.toNat
2993+
| x :: y :: rest =>
2994+
let xs := x :: y :: rest
2995+
let mid := xs.length / 2
2996+
flattenList.toNatAux (xs.take mid) <<< (n * (xs.drop mid).length)
2997+
||| flattenList.toNatAux (xs.drop mid)
2998+
termination_by l => l.length
2999+
decreasing_by
3000+
all_goals simp only [List.length_take, List.length_drop, List.length_cons]
3001+
all_goals omega
3002+
3003+
/--
3004+
Divide-and-conquer implementation of `BitVec.flattenList`, swapped in at runtime
3005+
via `@[csimp]`. See `BitVec.flattenList.toNatAux` for the cost analysis.
3006+
-/
3007+
def flattenListFast {n : Nat} (xs : List (BitVec n)) : BitVec (n * xs.length) :=
3008+
.ofNat (n * xs.length) (BitVec.flattenList.toNatAux xs)
3009+
3010+
theorem toNat_flattenList_append {n : Nat} (l r : List (BitVec n)) :
3011+
(BitVec.flattenList (l ++ r)).toNat
3012+
= (BitVec.flattenList l).toNat <<< (n * r.length) ||| (BitVec.flattenList r).toNat := by
3013+
induction l with
3014+
| nil => simp [BitVec.flattenList]
3015+
| cons x l ih =>
3016+
rw [List.cons_append]
3017+
simp only [BitVec.flattenList, toNat_cast, toNat_append, ih, List.length_append, Nat.mul_add]
3018+
rw [Nat.shiftLeft_or_distrib, ← Nat.shiftLeft_add, Nat.or_assoc]
3019+
3020+
theorem flattenList.toNatAux_eq {n : Nat} (xs : List (BitVec n)) :
3021+
BitVec.flattenList.toNatAux xs = (BitVec.flattenList xs).toNat := by
3022+
induction xs using BitVec.flattenList.toNatAux.induct with
3023+
| case1 => simp [BitVec.flattenList.toNatAux, BitVec.flattenList]
3024+
| case2 x => simp [BitVec.flattenList.toNatAux, BitVec.flattenList]
3025+
| case3 x y rest xs mid ih1 ih2 =>
3026+
rw [BitVec.flattenList.toNatAux, ih1, ih2, ← toNat_flattenList_append, List.take_append_drop]
3027+
3028+
@[csimp] theorem flattenList_eq_flattenListFast :
3029+
@BitVec.flattenList = @BitVec.flattenListFast := by
3030+
funext n xs
3031+
rw [BitVec.flattenListFast, BitVec.flattenList.toNatAux_eq, ofNat_toNat, setWidth_eq]
3032+
3033+
/-! ## extractLsb -/
3034+
3035+
/--
3036+
Extracting a bitvector from `flattenList`, when we only extract from a single bitvector.
3037+
-/
3038+
theorem extractLsb_flattenList (hi lo : Nat) {w : Nat} (vs : List (BitVec w))
3039+
(w₁ : lo ≤ hi) (w₂ : hi < w * vs.length) (h : hi / w = lo / w) :
3040+
extractLsb hi lo (BitVec.flattenList vs) =
3041+
BitVec.cast (by simp [Nat.mod_eq_sub_mul_div, h]; have := Nat.mul_div_le lo w; omega)
3042+
(extractLsb (hi % w) (lo % w) (vs[vs.length - 1 - hi / w]?.getD 0#w)) := by
3043+
by_cases p : 0 < w
3044+
· have t : w * (lo / w) ≤ lo := Nat.mul_div_le lo w
3045+
have t' : hi < w * (lo / w) + w := by rw [← h]; apply Nat.lt_mul_div_self_add p
3046+
apply eq_of_getLsbD_eq
3047+
intro i hi_lt
3048+
have q : i < hi - lo + 1 := hi_lt
3049+
have q' : i < hi % w - lo % w + 1 := by simp [Nat.mod_eq_sub_mul_div, h]; omega
3050+
have q'' : lo + i < w * vs.length := by omega
3051+
have h₁ : (lo + i) / w = lo / w := by
3052+
apply Nat.div_eq_of_lt_le <;> rw [Nat.mul_comm]
3053+
· omega
3054+
· simp only [Nat.mul_add]; omega
3055+
have h₂ : (lo + i) % w = lo % w + i := by simp [Nat.mod_eq_sub_mul_div, h₁]; omega
3056+
simp only [getLsbD_extractLsb, getLsbD_flattenList, q, q', q'', h₁, h₂, h, getLsbD_cast,
3057+
decide_true, Bool.true_and]
3058+
· simp at p
3059+
subst p
3060+
omega
3061+
3062+
/-! ### rev -/
29373063
theorem shiftRight_add {w : Nat} (x : BitVec w) (n m : Nat) :
29383064
x >>> (n + m) = (x >>> n) >>> m:= by
29393065
ext i

src/Init/Data/Nat/Lemmas.lean

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1750,6 +1750,19 @@ grind_pattern shiftLeft_add => (m <<< n) <<< k where
17501750
@[simp] theorem shiftLeft_shiftRight (x n : Nat) : x <<< n >>> n = x := by
17511751
rw [Nat.shiftLeft_eq, Nat.shiftRight_eq_div_pow, Nat.mul_div_cancel _ (Nat.two_pow_pos _)]
17521752

1753+
theorem lt_mul_div_self_add {x k : Nat} (h : 0 < k) : x < k * (x / k) + k := by
1754+
rw [← Int.ofNat_lt]
1755+
simpa using Int.lt_mul_ediv_self_add (by omega)
1756+
1757+
theorem mul_sub_mod {x n p : Nat} (h : x < n * p) : (n * p - (x + 1)) % n = n - (x % n + 1) := by
1758+
rw [mod_eq_sub_div_mul, mul_sub_div _ _ _ h, Nat.sub_sub, Nat.add_comm, ← Nat.sub_sub,
1759+
Nat.mul_comm _ n, ← Nat.mul_sub_left_distrib, Nat.sub_sub_self, mul_succ, Nat.add_comm]
1760+
· conv in (_ + 1) => rw [← mod_add_div x n, Nat.add_right_comm]
1761+
apply Nat.add_sub_add_right
1762+
rw [Nat.succ_le_iff, Nat.div_lt_iff_lt_mul]
1763+
· rwa [Nat.mul_comm]
1764+
· refine Nat.pos_of_mul_pos_right (by omega : 0 < n * p)
1765+
17531766
/-! ### Decidability of predicates -/
17541767

17551768
-- `noncomputable` so the non-tail-recursive code is never compiled; the tail-recursive

0 commit comments

Comments
 (0)