This is an automated email from the ASF dual-hosted git repository.
Jefffrey pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
The following commit(s) were added to refs/heads/main by this push:
new d09e4b434a Make more of i256 available in const code (#10363)
d09e4b434a is described below
commit d09e4b434a269178df9487316efb0051cfb9fa10
Author: Adam Gutglick <[email protected]>
AuthorDate: Sun Jul 19 03:17:40 2026 +0100
Make more of i256 available in const code (#10363)
# Which issue does this PR close?
- Closes #10362.
# Rationale for this change
I think this makes i256 more ergonomic, and closer to the native
primitive types.
# What changes are included in this PR?
Introduces two new public functions:
1. `i256::overflowing_add`
2. `i256::overflowing_sub`
Makes the following functions const:
1. `i256::to_i128`
2. `i256::as_i128`
3. `i256::wrapping_abs`
4. `i256::checked_abs`
5. `i256::wrapping_neg`
6. `i256::checked_neg`
7. `i256::wrapping_add`
8. `i256::checked_add`
9. `i256::wrapping_sub`
10. `i256::checked_sub`
11. `i256::wrapping_mul`
12. `i256::checked_mul`
13. `i256::checked_pow`
14. `i256::wrapping_pow`
# Are these changes tested?
Existing testing suite with some small modifications, and two new tests.
# Are there any user-facing changes?
New functions.
---------
Signed-off-by: Adam Gutglick <[email protected]>
---
arrow-buffer/src/bigint/mod.rs | 189 ++++++++++++++++++++++++++++++++++-------
1 file changed, 157 insertions(+), 32 deletions(-)
diff --git a/arrow-buffer/src/bigint/mod.rs b/arrow-buffer/src/bigint/mod.rs
index aaadbff6d9..6a4b41ee29 100644
--- a/arrow-buffer/src/bigint/mod.rs
+++ b/arrow-buffer/src/bigint/mod.rs
@@ -260,18 +260,22 @@ impl i256 {
/// Converts this `i256` into an `i128` returning `None` if this would
result
/// in truncation/overflow
- pub fn to_i128(self) -> Option<i128> {
+ pub const fn to_i128(self) -> Option<i128> {
let as_i128 = self.low as i128;
let high_negative = self.high < 0;
let low_negative = as_i128 < 0;
let high_valid = self.high == -1 || self.high == 0;
- (high_negative == low_negative && high_valid).then_some(self.low as
i128)
+ if (high_negative == low_negative) && high_valid {
+ Some(self.low as i128)
+ } else {
+ None
+ }
}
/// Wraps this `i256` into an `i128`
- pub fn as_i128(self) -> i128 {
+ pub const fn as_i128(self) -> i128 {
self.low as i128
}
@@ -326,7 +330,7 @@ impl i256 {
/// Computes the absolute value of this i256
#[inline]
- pub fn wrapping_abs(self) -> Self {
+ pub const fn wrapping_abs(self) -> Self {
// -1 if negative, otherwise 0
let sa = self.high >> 127;
let sa = Self::from_parts(sa as u128, sa);
@@ -337,25 +341,33 @@ impl i256 {
/// Computes the absolute value of this i256 returning `None` if `Self ==
Self::MIN`
#[inline]
- pub fn checked_abs(self) -> Option<Self> {
- (self != Self::MIN).then(|| self.wrapping_abs())
+ pub const fn checked_abs(self) -> Option<Self> {
+ if !self.is_eq(Self::MIN) {
+ Some(self.wrapping_abs())
+ } else {
+ None
+ }
}
/// Negates this i256
#[inline]
- pub fn wrapping_neg(self) -> Self {
+ pub const fn wrapping_neg(self) -> Self {
Self::from_parts(!self.low, !self.high).wrapping_add(i256::ONE)
}
/// Negates this i256 returning `None` if `Self == Self::MIN`
#[inline]
- pub fn checked_neg(self) -> Option<Self> {
- (self != Self::MIN).then(|| self.wrapping_neg())
+ pub const fn checked_neg(self) -> Option<Self> {
+ if !self.is_eq(Self::MIN) {
+ Some(self.wrapping_neg())
+ } else {
+ None
+ }
}
/// Performs wrapping addition
#[inline]
- pub fn wrapping_add(self, other: Self) -> Self {
+ pub const fn wrapping_add(self, other: Self) -> Self {
let (low, carry) = self.low.overflowing_add(other.low);
let high = self.high.wrapping_add(other.high).wrapping_add(carry as _);
Self { low, high }
@@ -363,14 +375,15 @@ impl i256 {
/// Performs checked addition
#[inline]
- pub fn checked_add(self, other: Self) -> Option<Self> {
- let r = self.wrapping_add(other);
- ((other.is_negative() && r < self) || (!other.is_negative() && r >=
self)).then_some(r)
+ pub const fn checked_add(self, other: Self) -> Option<Self> {
+ let (r, overflow) = self.overflowing_add(other);
+
+ if overflow { None } else { Some(r) }
}
/// Performs wrapping subtraction
#[inline]
- pub fn wrapping_sub(self, other: Self) -> Self {
+ pub const fn wrapping_sub(self, other: Self) -> Self {
let (low, carry) = self.low.overflowing_sub(other.low);
let high = self.high.wrapping_sub(other.high).wrapping_sub(carry as _);
Self { low, high }
@@ -378,14 +391,15 @@ impl i256 {
/// Performs checked subtraction
#[inline]
- pub fn checked_sub(self, other: Self) -> Option<Self> {
- let r = self.wrapping_sub(other);
- ((other.is_negative() && r > self) || (!other.is_negative() && r <=
self)).then_some(r)
+ pub const fn checked_sub(self, other: Self) -> Option<Self> {
+ let (r, overflow) = self.overflowing_sub(other);
+
+ if overflow { None } else { Some(r) }
}
/// Performs wrapping multiplication
#[inline]
- pub fn wrapping_mul(self, other: Self) -> Self {
+ pub const fn wrapping_mul(self, other: Self) -> Self {
let (low, high) = mulx(self.low, other.low);
// Compute the high multiples, only impacting the high 128-bits
@@ -398,10 +412,15 @@ impl i256 {
}
}
+ /// Const helper to check equality of two `i256` instances
+ const fn is_eq(&self, other: Self) -> bool {
+ (self.high == other.high) && (self.low == other.low)
+ }
+
/// Performs checked multiplication
#[inline]
- pub fn checked_mul(self, other: Self) -> Option<Self> {
- if self == i256::ZERO || other == i256::ZERO {
+ pub const fn checked_mul(self, other: Self) -> Option<Self> {
+ if self.is_eq(Self::ZERO) || other.is_eq(Self::ZERO) {
return Some(i256::ZERO);
}
@@ -423,18 +442,30 @@ impl i256 {
let (low, high) = mulx(l_abs.low, r_abs.low);
// Compute the high multiples, only impacting the high 128-bits
- let hl = (l_abs.high as u128).checked_mul(r_abs.low)?;
- let lh = l_abs.low.checked_mul(r_abs.high as u128)?;
+ let Some(hl) = (l_abs.high as u128).checked_mul(r_abs.low) else {
+ return None;
+ };
+ let Some(lh) = l_abs.low.checked_mul(r_abs.high as u128) else {
+ return None;
+ };
- let high = high.checked_add(hl)?.checked_add(lh)?;
+ let Some(high) = high.checked_add(hl) else {
+ return None;
+ };
+ let Some(high) = high.checked_add(lh) else {
+ return None;
+ };
// Reverse absolute value, if necessary
let (low, c) = (low ^ out_sa).overflowing_sub(out_sa);
let high = (high ^ out_sa).wrapping_sub(out_sa).wrapping_sub(c as
u128) as i128;
// Check for overflow in final conversion
- (high.is_negative() == (self.is_negative() ^ other.is_negative()))
- .then_some(Self { low, high })
+ if high.is_negative() == (self.is_negative() ^ other.is_negative()) {
+ Some(Self { low, high })
+ } else {
+ None
+ }
}
/// Division operation, returns (quotient, remainder).
@@ -521,7 +552,7 @@ impl i256 {
/// Performs checked exponentiation
#[inline]
- pub fn checked_pow(self, mut exp: u32) -> Option<Self> {
+ pub const fn checked_pow(self, mut exp: u32) -> Option<Self> {
if exp == 0 {
return Some(i256::from_i128(1));
}
@@ -531,10 +562,16 @@ impl i256 {
while exp > 1 {
if (exp & 1) == 1 {
- acc = acc.checked_mul(base)?;
+ let Some(next) = acc.checked_mul(base) else {
+ return None;
+ };
+ acc = next;
}
exp /= 2;
- base = base.checked_mul(base)?;
+ let Some(next) = base.checked_mul(base) else {
+ return None;
+ };
+ base = next;
}
// since exp!=0, finally the exp must be 1.
// Deal with the final bit of the exponent separately, since
@@ -545,7 +582,7 @@ impl i256 {
/// Performs wrapping exponentiation
#[inline]
- pub fn wrapping_pow(self, mut exp: u32) -> Self {
+ pub const fn wrapping_pow(self, mut exp: u32) -> Self {
if exp == 0 {
return i256::from_i128(1);
}
@@ -716,6 +753,54 @@ impl i256 {
self.checked_ilog2()
.unwrap_or_else(|| panic!("ilog2 overflow with {self}"))
}
+
+ /// Calculates `self + rhs`.
+ ///
+ /// Returns the wrapping sum and a boolean indicating whether arithmetic
overflow occurred.
+ /// The returned sum wraps around the bounds of [`i256`] when overflow
occurs.
+ #[inline]
+ pub const fn overflowing_add(self, rhs: Self) -> (Self, bool) {
+ // Add the low limbs and capture the carry into the high limb.
+ let (low, carry) = self.low.overflowing_add(rhs.low);
+
+ // Treat the high limbs as raw two's-complement bit patterns.
+ let high = (self.high as u128)
+ .wrapping_add(rhs.high as u128)
+ .wrapping_add(carry as u128) as i128;
+
+ let result = Self { high, low };
+
+ // Signed overflow occurs when:
+ // - both operands have the same sign, and
+ // - the result has the opposite sign.
+ let overflow = (self.high < 0) == (rhs.high < 0) && (high < 0) !=
(self.high < 0);
+
+ (result, overflow)
+ }
+
+ /// Calculates `self - rhs`.
+ ///
+ /// Returns the wrapping difference and a boolean indicating whether
arithmetic overflow
+ /// occurred. The returned difference wraps around the bounds of [`i256`]
when overflow occurs.
+ #[inline]
+ pub const fn overflowing_sub(self, rhs: Self) -> (Self, bool) {
+ // Subtract the low limbs and determine whether we borrowed.
+ let (low, borrow) = self.low.overflowing_sub(rhs.low);
+
+ // Subtract the high limbs as raw bit patterns, including the borrow.
+ let high = (self.high as u128)
+ .wrapping_sub(rhs.high as u128)
+ .wrapping_sub(borrow as u128) as i128;
+
+ let result = Self { high, low };
+
+ // Signed overflow occurs when:
+ // - operands have opposite signs, and
+ // - the result's sign differs from the left operand's sign.
+ let overflow = (self.high < 0) != (rhs.high < 0) && (high < 0) !=
(self.high < 0);
+
+ (result, overflow)
+ }
}
/// Temporary workaround due to lack of stable const array slicing
@@ -738,8 +823,10 @@ const fn split_array<const N: usize, const M: usize>(vals:
[u8; N]) -> ([u8; M],
///
/// This mirrors the x86 mulx instruction but for 128-bit types
#[inline]
-fn mulx(a: u128, b: u128) -> (u128, u128) {
- let split = |a: u128| (a & (u64::MAX as u128), a >> 64);
+const fn mulx(a: u128, b: u128) -> (u128, u128) {
+ const fn split(a: u128) -> (u128, u128) {
+ (a & (u64::MAX as u128), a >> 64)
+ }
const MASK: u128 = u64::MAX as _;
@@ -1240,7 +1327,7 @@ impl Not for i256 {
}
}
-#[cfg(all(test, not(miri)))] // llvm.x86.subborrow.64 not supported by MIRI
+#[cfg(test)]
mod tests {
use super::*;
use num_traits::Signed;
@@ -1315,6 +1402,7 @@ mod tests {
let actual = il.wrapping_add(ir);
let (expected, overflow) = i256::from_bigint_with_overflow(bl.clone()
+ br.clone());
assert_eq!(actual, expected);
+ assert_eq!(il.overflowing_add(ir), (expected, overflow));
let checked = il.checked_add(ir);
match overflow {
@@ -1326,6 +1414,7 @@ mod tests {
let actual = il.wrapping_sub(ir);
let (expected, overflow) = i256::from_bigint_with_overflow(bl.clone()
- br.clone());
assert_eq!(actual.to_string(), expected.to_string());
+ assert_eq!(il.overflowing_sub(ir), (expected, overflow));
let checked = il.checked_sub(ir);
match overflow {
@@ -1443,6 +1532,41 @@ mod tests {
}
#[test]
+ fn test_overflowing_add() {
+ const POSITIVE_OVERFLOW: (i256, bool) =
i256::MAX.overflowing_add(i256::ONE);
+ const NEGATIVE_OVERFLOW: (i256, bool) =
i256::MIN.overflowing_add(i256::MINUS_ONE);
+
+ assert_eq!(POSITIVE_OVERFLOW, (i256::MIN, true));
+ assert_eq!(NEGATIVE_OVERFLOW, (i256::MAX, true));
+ assert_eq!(
+ i256::from_parts(u128::MAX, 0).overflowing_add(i256::ONE),
+ (i256::from_parts(0, 1), false)
+ );
+ assert_eq!(
+ i256::ONE.overflowing_add(i256::from_i128(2)),
+ (i256::from_i128(3), false)
+ );
+ }
+
+ #[test]
+ fn test_overflowing_sub() {
+ const NEGATIVE_OVERFLOW: (i256, bool) =
i256::MIN.overflowing_sub(i256::ONE);
+ const POSITIVE_OVERFLOW: (i256, bool) =
i256::MAX.overflowing_sub(i256::MINUS_ONE);
+
+ assert_eq!(NEGATIVE_OVERFLOW, (i256::MAX, true));
+ assert_eq!(POSITIVE_OVERFLOW, (i256::MIN, true));
+ assert_eq!(
+ i256::from_parts(0, 1).overflowing_sub(i256::ONE),
+ (i256::from_parts(u128::MAX, 0), false)
+ );
+ assert_eq!(
+ i256::from_i128(3).overflowing_sub(i256::from_i128(2)),
+ (i256::ONE, false)
+ );
+ }
+
+ #[test]
+ #[cfg_attr(miri, ignore)]
fn test_i256() {
let candidates = [
i256::ZERO,
@@ -1682,6 +1806,7 @@ mod tests {
}
#[test]
+ #[cfg_attr(miri, ignore)]
fn test_decimal256_to_f64_typical_values() {
let v = i256::from_i128(42_i128);
assert_eq!(v.to_f64().unwrap(), 42.0);