Skip to content

Commit 5c74909

Browse files
authored
fix: normalization of bitvector literals in grind (#14371)
This PR fixes two bugs in `grind` caused by non-normalized bitvector literals. `BitVec.ofNatLT` literals and out-of-range `OfNat.ofNat` literals (e.g., `(17 : BitVec 4)`) were not reduced to the `OfNat.ofNat` normal form used by `grind`, so two representations of the same value were treated as distinct values, and `grind` produced invalid proofs rejected by the kernel: ```lean example (x : BitVec 4) (_h1 : x = BitVec.ofNatLT 1 (by decide)) (_h2 : x = 1#4) : True := by grind -- kernel error before this PR ```
1 parent 1d69dd5 commit 5c74909

5 files changed

Lines changed: 68 additions & 1 deletion

File tree

src/Init/Data/BitVec/Lemmas.lean

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,6 @@ theorem ofBool_eq_iff_eq : ∀ {b b' : Bool}, BitVec.ofBool b = BitVec.ofBool b'
336336
getMsbD (x#'h) i = (decide (i < n) && x.testBit (n - 1 - i)) := by
337337
simp [getMsbD, getLsbD]
338338

339-
@[grind =]
340339
theorem ofNatLT_eq_ofNat {w : Nat} {n : Nat} (hn) : BitVec.ofNatLT n hn = BitVec.ofNat w n :=
341340
eq_of_toNat_eq (by simp [Nat.mod_eq_of_lt hn])
342341

src/Init/Grind/Norm.lean

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,5 +234,7 @@ init_grind_norm
234234
-- Semiring
235235
Semiring.one_mul Semiring.mul_one
236236
Semiring.zero_mul Semiring.mul_zero
237+
-- Bitvectors
238+
BitVec.ofNatLT_eq_ofNat
237239

238240
end Lean.Grind

src/Lean/Meta/Tactic/Simp/BuiltinSimprocs/BitVec.lean

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,19 @@ builtin_dsimproc [simp, seval] reduceOfNat (BitVec.ofNat _ _) := fun e => do
258258
-/
259259
return .done (← mkNumeral (mkApp (mkConst ``BitVec) n) v)
260260

261+
/--
262+
Simplification procedure for ensuring `OfNat.ofNat` bitvector literals are normalized.
263+
For example, `(17 : BitVec 4)` is reduced to `(1 : BitVec 4)` when `bitVecOfNat := false`,
264+
and to `1#4` when `bitVecOfNat := true`.
265+
-/
266+
builtin_dsimproc [simp, seval] isValue ((OfNat.ofNat _ : BitVec _)) := fun e => do
267+
let_expr OfNat.ofNat _ m _ ← e | return .continue
268+
let some m ← Nat.fromExpr? m | return .continue
269+
let some ⟨_, v⟩ ← getBitVecValue? e | return .continue
270+
if v.toNat == m && !(← Simp.getConfig).bitVecOfNat then
271+
return .done e -- already normalized; `OfNat.ofNat` is the normal form when `bitVecOfNat := false`
272+
return .done (← toExpr' v)
273+
261274
/-- Simplification procedure for `=` on `BitVec`s. -/
262275
builtin_simproc [simp, seval] reduceEq (( _ : BitVec _) = _) := reduceBinPred ``Eq 3 (. = .)
263276
/-- Simplification procedure for `≠` on `BitVec`s. -/
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/-!
2+
Tests that `grind` reduces out-of-range `OfNat.ofNat` bitvector literals. The `grind` normal
3+
form for bitvector literals is `OfNat.ofNat` (see #14370). If a literal whose value exceeds
4+
`2^w` (e.g. `(17 : BitVec 4)`) is not reduced modulo `2^w`, then `(17 : BitVec 4)` and `1#4`
5+
are treated as two distinct interpreted values.
6+
-/
7+
8+
/--
9+
This example used to expose the bug: merging the two representations of the same value made
10+
`grind` produce an invalid inconsistency proof rejected by the kernel.
11+
-/
12+
example (x : BitVec 4) (_h1 : x = (17 : BitVec 4)) (_h2 : x = 1#4) : True := by
13+
grind
14+
15+
example (x : BitVec 4) (h : x = (17 : BitVec 4)) : x = 1#4 := by
16+
grind
17+
18+
/-- Width-zero edge case: every literal must reduce to `0`. -/
19+
example (x : BitVec 0) (h : x = (1 : BitVec 0)) : x = 0#0 := by
20+
grind
21+
22+
/-- `grind` must still detect genuine disequalities involving out-of-range literals. -/
23+
example (x : BitVec 4) (h1 : x = (17 : BitVec 4)) (h2 : x = 2#4) : False := by
24+
grind
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/-!
2+
Tests that `grind` handles `BitVec.ofNatLT` literals correctly. The `grind` normal form for
3+
bitvector literals is `OfNat.ofNat` (see #14370), but `BitVec.ofNatLT` literals are not
4+
normalized to this form. Thus, `BitVec.ofNatLT 1 h : BitVec 4` and `1#4` are treated as two
5+
distinct interpreted values.
6+
-/
7+
8+
/--
9+
This example exposes the bug: merging the two representations of the same value makes `grind`
10+
produce an invalid inconsistency proof rejected by the kernel.
11+
-/
12+
example (x : BitVec 4) (_h1 : x = BitVec.ofNatLT 1 (by decide)) (_h2 : x = 1#4) : True := by
13+
grind
14+
15+
/--
16+
The following two examples are not unsound, just incomplete: `grind` cannot connect the two
17+
representations. They should pass once `ofNatLT` literals are normalized.
18+
-/
19+
20+
example (x : BitVec 4) (h : x = BitVec.ofNatLT 1 (by decide)) : x = 1#4 := by
21+
grind
22+
23+
open BitVec in
24+
example (x : BitVec 4) (h : x = 1#'(by decide)) : x = 1#4 := by
25+
grind
26+
27+
/-- `grind` must still detect genuine disequalities involving `ofNatLT` literals. -/
28+
example (x : BitVec 4) (h1 : x = BitVec.ofNatLT 1 (by decide)) (h2 : x = 2#4) : False := by
29+
grind

0 commit comments

Comments
 (0)