Skip to content

simp [h] cannot use h : ¬ v = 0 on BitVec, because the goal's 0 becomes 0#w before h is tried #16260

Closed

Description

@benaadams

Description

simp normalises a bit-vector literal in the goal from OfNat.ofNat 0 to 0#w through the BitVec.isValue dsimproc and BitVec.ofNat_eq_ofNat. A hypothesis passed as a simp argument is turned into a rewrite rule as written, so its left side keeps OfNat.ofNat 0. The rule no longer matches, the linter reports the argument as unused, and the goal is left open:

example (v : BitVec 32) (zero : ¬ v = 0) : ¬ v = 0 := by
  simp [zero]
-- unsolved goals
-- zero : ¬v = 0
-- ⊢ ¬v = 0#32
-- warning: This simp argument is unused: zero

set_option trace.Meta.Tactic.simp true shows zero:1000, failed to unify v = 0 with v = 0#32. The literal change itself does not appear in trace.Meta.Tactic.simp.rewrite.

The same happens through a definition:

def copied (values : List (BitVec 32)) : List (BitVec 32) := values.takeWhile (· ≠ 0)

example (v : BitVec 32) (rest : List (BitVec 32)) (zero : ¬ v = 0) :
    copied (v :: rest) = v :: copied rest := by
  simp [copied, List.takeWhile_cons, zero]   -- leaves ⊢ ¬v = 0#32

Expected behavior

simp [zero] closes both goals. They already close with simp -bitVecOfNat [zero], or after attribute [-simp] BitVec.isValue BitVec.ofNat_eq_ofNat.

One possible fix: normalise bit-vector literals in the left side of rules built from simp arguments the same way the goal is normalised.

Notes

  • Downstream libraries cannot work around this by default: attribute [-simp] does not carry into importing modules, and bitVecOfNat is a per-call config.
  • Making a library's own tactics emit the OfNat form instead makes later default simp calls convert every literal back. One measured proof's bv_omega preprocessing went from 994k to 1785k heartbeats.

Versions

Lean (version 4.35.0), Windows x86_64. Not checked on a nightly.

Activity

  1. Rob23oba commented on Nov 5, 2026

    @Rob23oba
    Contributor

    Wouldn't the fix simply be: use 0#32 everywhere instead? It's not really anything new that you might not be able to use a hypothesis in simp if the hypothesis is not in simp normal form, e.g.

    example (h : (true && true) = x) : (true && true) = x := by
      simp [h]

    so the fix should just be to use the simp normal form (in the example, h : x = true), instead; unless that does not work for you for some reason?

  2. benaadams commented on Nov 9, 2026

    @benaadams
    Author

    Yes, this works. Using 0#32 consistently in the predicate and hypotheses lets the CopyUntilZero proof use ordinary simp. The updated proof passed verification and independent kernel replay.

    For an existing hypothesis written with an ordinary numeral, an explicitly typed restatement also works:

    example (value : BitVec 32) (nonzero : value ≠ 0) : value ≠ 0#32 := by
      simp [show value ≠ 0#32 from nonzero]

    We've replaced the approach that disabled bitVecOfNat in LeanCIL's tactics with canonical literals and added regression tests: NethermindEth/LeanCIL#471

    Thanks for the suggestion. Closing this as resolved for our use case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions