diff --git a/theories/numbers.v b/theories/numbers.v
index a08b7bf855df1e63ab376f0e610f2c9282a6ab8b..5775026105a5bc4f4117840b4484f8fb6da3dd62 100644
--- a/theories/numbers.v
+++ b/theories/numbers.v
@@ -486,6 +486,48 @@ Lemma Zmod_in_range q a c :
   a `mod` c = a - q * c.
 Proof. intros ?. symmetry. apply Z.mod_unique_pos with q; lia. Qed.
 
+Lemma Z_bounded_iff_bits_nonneg k n :
+  0 ≤ k → 0 ≤ n →
+  n < 2^k ↔ ∀ l, k ≤ l → Z.testbit n l = false.
+Proof.
+  intros. destruct (decide (n = 0)) as [->|].
+  { naive_solver eauto using Z.bits_0, Z.pow_pos_nonneg with lia. }
+  split.
+  { intros Hb%Z.log2_lt_pow2 l Hl; [|lia]. apply Z.bits_above_log2; lia. }
+  intros Hl. apply Z.nle_gt; intros ?.
+  assert (Z.testbit n (Z.log2 n) = false) as Hbit.
+  { apply Hl, Z.log2_le_pow2; lia. }
+  by rewrite Z.bit_log2 in Hbit by lia.
+Qed.
+
+(* Goals of the form [0 ≤ n ≤ 2^k] appear often. So we also define the
+derived version [Z_bounded_iff_bits_nonneg'] that does not require
+proving [0 ≤ n] twice in that case. *)
+Lemma Z_bounded_iff_bits_nonneg' k n :
+  0 ≤ k → 0 ≤ n →
+  0 ≤ n < 2^k ↔ ∀ l, k ≤ l → Z.testbit n l = false.
+Proof. intros ??. rewrite <-Z_bounded_iff_bits_nonneg; lia. Qed.
+
+Lemma Z_bounded_iff_bits k n :
+  0 ≤ k →
+  -2^k ≤ n < 2^k ↔ ∀ l, k ≤ l → Z.testbit n l = bool_decide (n < 0).
+Proof.
+  intros Hk.
+  case_bool_decide; [ | rewrite <-Z_bounded_iff_bits_nonneg; lia].
+  assert(n = - Z.abs n)%Z as -> by lia.
+  split.
+  { intros [? _] l Hl.
+    rewrite Z.bits_opp, negb_true_iff by lia.
+    apply Z_bounded_iff_bits_nonneg with k; lia. }
+  intros Hbit. split.
+  - rewrite <-Z.opp_le_mono, <-Z.lt_pred_le.
+    apply Z_bounded_iff_bits_nonneg; [lia..|]. intros l Hl.
+    rewrite <-negb_true_iff, <-Z.bits_opp by lia.
+    by apply Hbit.
+  - etrans; [|apply Z.pow_pos_nonneg]; lia.
+Qed.
+
+
 Local Close Scope Z_scope.
 
 (** * Injectivity of casts *)