diff --git a/integer/src/repr.rs b/integer/src/repr.rs index c92a8a69..c0dad977 100644 --- a/integer/src/repr.rs +++ b/integer/src/repr.rs @@ -428,9 +428,10 @@ impl Repr { buffer.push(ones_word(hi_bits as _)); } - // SAFETY: the bit length has been checked and capacity >= length, - // so capacity is nonzero and larger than 2 - unsafe { mem::transmute::(buffer) } + // Route through `from_buffer` to canonicalise: a 128-bit ones value + // has length 2 here and must become the inline form, otherwise the + // (now scale-sensitive) PartialEq wouldn't see it as equal. + Self::from_buffer(buffer) } } @@ -554,7 +555,44 @@ impl Drop for Repr { impl PartialEq for Repr { #[inline] fn eq(&self, other: &Self) -> bool { - self.as_sign_slice() == other.as_sign_slice() + // The encoding is canonical, so a sign or scale (inline vs heap) + // mismatch is immediately unequal; otherwise compare the words directly + // instead of materialising slices via `as_sign_slice`. + let cap_a = self.capacity.get(); + let cap_b = other.capacity.get(); + // Sign mismatch: zero is canonically positive (no negative zero), so + // a sign disagreement always means unequal values. + if (cap_a > 0) != (cap_b > 0) { + return false; + } + let abs_a = cap_a.unsigned_abs(); + let abs_b = cap_b.unsigned_abs(); + let inline_a = abs_a <= 2; + let inline_b = abs_b <= 2; + // Mixed scales can never be equal: heap representation requires + // length ≥ 3, which is a strictly larger magnitude than anything + // inline. + if inline_a != inline_b { + return false; + } + // SAFETY: capacity tells us which union variant is live. + unsafe { + if inline_a { + // Compare as one DoubleWord (tighter codegen than [Word; 2]). + let dw_a = double_word(self.data.inline[0], self.data.inline[1]); + let dw_b = double_word(other.data.inline[0], other.data.inline[1]); + dw_a == dw_b + } else { + let len_a = self.data.heap.1; + let len_b = other.data.heap.1; + if len_a != len_b { + return false; + } + let slice_a = slice::from_raw_parts(self.data.heap.0, len_a); + let slice_b = slice::from_raw_parts(other.data.heap.0, len_b); + slice_a == slice_b + } + } } } impl Eq for Repr {} diff --git a/integer/tests/cmp.rs b/integer/tests/cmp.rs index e066f93f..b58ca4f4 100644 --- a/integer/tests/cmp.rs +++ b/integer/tests/cmp.rs @@ -59,3 +59,32 @@ fn test_abs_ord() { assert!(ubig!(12).abs_cmp(&ibig!(-12)).is_eq()); assert!(ubig!(12).abs_cmp(&ibig!(-14)).is_le()); } + +#[test] +fn test_eq_across_representations() { + // Regression for Repr::eq's inline-DoubleWord path and its sign/scale + // short-circuits, plus the canonical-encoding requirement it relies on. + // inline equal / unequal (capacity 1 and capacity 2) + assert_eq!(ubig!(5), ubig!(5)); + assert_ne!(ubig!(5), ubig!(7)); + assert_eq!(ubig!(0x10000000000000000), ubig!(0x10000000000000000)); // 2^64 (cap 2) + assert_ne!(ubig!(0x10000000000000000), ubig!(0x10000000000000001)); + // scale mismatch: an inline value can never equal a heap value + assert_ne!(ubig!(5), ubig!(1) << 200); + assert_ne!(ubig!(1) << 200, ubig!(5)); + // heap equal / unequal, same length and different length + assert_eq!(ubig!(1) << 200, ubig!(1) << 200); + assert_ne!((ubig!(1) << 200) + ubig!(1), (ubig!(1) << 200) + ubig!(2)); + assert_ne!(ubig!(1) << 200, ubig!(1) << 201); + // cross-representation canonical equality: `ones` must compare equal to the + // inline form of the same value (the from_buffer canonicalisation). + assert_eq!(UBig::ones(128), UBig::from(u128::MAX)); + assert_eq!(UBig::ones(256), (ubig!(1) << 256) - ubig!(1)); + // IBig: zero is canonically positive, so sign disagreement is never equal + assert_eq!(ibig!(-5), ibig!(-5)); + assert_ne!(ibig!(5), ibig!(-5)); + assert_eq!(ibig!(0), ibig!(0)); + assert_ne!(ibig!(0), ibig!(1)); + assert_eq!(-(ibig!(1) << 200), -(ibig!(1) << 200)); + assert_ne!(-(ibig!(1) << 200), ibig!(1) << 200); +}