From ca3d7172e653417ef5a2210088fca06d6c6f6788 Mon Sep 17 00:00:00 2001 From: theirix Date: Fri, 20 Feb 2026 21:06:07 +0000 Subject: [PATCH 1/8] i256: Implement ilog via i256 crate --- arrow-buffer/Cargo.toml | 1 + arrow-buffer/src/bigint/mod.rs | 122 +++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/arrow-buffer/Cargo.toml b/arrow-buffer/Cargo.toml index 02ea49c37c46..5b6451387cc3 100644 --- a/arrow-buffer/Cargo.toml +++ b/arrow-buffer/Cargo.toml @@ -43,6 +43,7 @@ bytes = { version = "1.4" } num-bigint = { version = "0.4.6", default-features = false, features = ["std"] } num-traits = { version = "0.2.19", default-features = false, features = ["std"] } half = { version = "2.1", default-features = false } +i256 = "0.2.3" [dev-dependencies] criterion = { workspace = true, default-features = false } diff --git a/arrow-buffer/src/bigint/mod.rs b/arrow-buffer/src/bigint/mod.rs index 15faed43a130..9fd67f34dccd 100644 --- a/arrow-buffer/src/bigint/mod.rs +++ b/arrow-buffer/src/bigint/mod.rs @@ -17,6 +17,7 @@ use crate::arith::derive_arith; use crate::bigint::div::div_rem; +use ::i256::I256 as ExtI256; use num_bigint::BigInt; use num_traits::{ Bounded, CheckedAdd, CheckedDiv, CheckedMul, CheckedNeg, CheckedRem, CheckedSub, FromPrimitive, @@ -614,6 +615,62 @@ impl i256 { let n = (n.high >> 64) as i64; // throw away the lower 192 bits (n as f64) * f64::powi(2.0, 192 - (k as i32)) // convert to f64 and scale it, as we left-shift k bit previous, so we need to scale it by 2^(192-k) } + + /// Computes the `base` logarithm of the number `self` + /// Returns `None` if `self` is less than or equal to zero, or if `base` is less than 2. + #[inline] + pub fn checked_ilog(self, base: i256) -> Option { + if self <= Self::ZERO || base < i256::from(2) { + None + } else { + // Invariants are enforced, so this call cannot fail + self.to_ext_i256().checked_ilog(base.to_ext_i256()) + } + } + + /// Computes the `base` logarithm of the number `self` + /// Panic if `self` is less than or equal to zero, or if `base` is less than 2. + #[inline] + pub fn ilog(self, base: i256) -> u32 { + self.checked_ilog(base) + .unwrap_or_else(|| panic!("ilog overflow with {self} and base {base}")) + } + + /// Computes the decimal logarithm of the number `self` + /// Returns `None` if `self` is less than or equal to zero. + #[inline] + pub fn checked_ilog10(self) -> Option { + // Switch to I256::checked_ilog10 when I256 switches MSRV + self.checked_ilog(i256::from(10)) + } + + /// Computes the decimal logarithm of the number `self` + /// Panics if `self` is less than or equal to zero. + #[inline] + pub fn ilog10(self) -> u32 { + self.checked_ilog10() + .unwrap_or_else(|| panic!("ilog10 overflow with {self}")) + } + + /// Computes the binary logarithm of the number `self` + /// Returns `None` if `self` is less than or equal to zero. + #[inline] + pub fn checked_ilog2(self) -> Option { + // Switch to I256::checked_ilog2 when I256 switches MSRV + self.checked_ilog(i256::from(2)) + } + + /// Computes the base 2 logarithm of the number, rounded down. + #[inline] + pub fn ilog2(self) -> u32 { + self.checked_ilog2() + .unwrap_or_else(|| panic!("ilog2 overflow with {self}")) + } + + /// Converts self into an `i256::I256` + fn to_ext_i256(self) -> ExtI256 { + ExtI256::from_le_bytes(self.to_le_bytes()) + } } /// Temporary workaround due to lack of stable const array slicing @@ -1580,4 +1637,69 @@ mod tests { assert_eq!(i256::MAX.trailing_zeros(), 0); assert_eq!(i256::from(-1).trailing_zeros(), 0); } + + #[test] + fn test_ilog() { + let value = i256::from(128); + + // log2 + assert_eq!(value.ilog(i256::from(2)), 7); + assert_eq!(value.ilog2(), 7); + + // log10 + assert_eq!(value.ilog(i256::from(10)), 2); + assert_eq!(value.ilog10(), 2); + + // negative base + assert_eq!(value.checked_ilog(i256::from(-2)), None); + assert_eq!(value.checked_ilog(i256::from(-10)), None); + assert_eq!(value.checked_ilog(i256::from(0)), None); + assert_eq!(value.checked_ilog(i256::from(1)), None); + + // negative self + let neg_value = i256::from(-128); + assert_eq!(neg_value.checked_ilog(i256::from(2)), None); + assert_eq!(neg_value.checked_ilog(i256::from(10)), None); + assert_eq!(neg_value.checked_ilog10(), None); + assert_eq!(neg_value.checked_ilog2(), None); + + // zero self + assert_eq!(i256::ZERO.checked_ilog(i256::from(2)), None); + assert_eq!(i256::ZERO.checked_ilog(i256::from(10)), None); + assert_eq!(i256::ZERO.checked_ilog10(), None); + assert_eq!(i256::ZERO.checked_ilog2(), None); + + // Large i256 values + let large = i256::from_parts(100000000, i128::MAX); + // log2 of 2 powered to approximately 255 should be 254 + assert_eq!(large.checked_ilog(i256::from(2)), Some(254)); + + // log10 should be 76 or 77 + let result = large.checked_ilog(i256::from(10)); + assert!(result.is_some()); + assert!(result.unwrap() >= 76 && result.unwrap() <= 77); + + // log5 + let result = large.checked_ilog(i256::from(5)); + assert!(result.is_some()); + assert!(result.unwrap() >= 109 && result.unwrap() <= 110); + } + + #[test] + #[should_panic(expected = "ilog10 overflow")] + fn test_ilog10_zero_panics() { + let _ = i256::ZERO.ilog10(); + } + + #[test] + #[should_panic(expected = "ilog overflow")] + fn test_ilog_zero_panics() { + let _ = i256::ZERO.ilog(i256::from(5)); + } + + #[test] + #[should_panic(expected = "ilog2 overflow")] + fn test_ilog2_zero_panics() { + let _ = i256::ZERO.ilog2(); + } } From 3cd316eb4f58fb4b062f60673421025eaea29d12 Mon Sep 17 00:00:00 2001 From: theirix Date: Fri, 13 Mar 2026 09:41:08 +0000 Subject: [PATCH 2/8] Refine tests --- arrow-buffer/src/bigint/mod.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/arrow-buffer/src/bigint/mod.rs b/arrow-buffer/src/bigint/mod.rs index 9fd67f34dccd..58c68fd18cc8 100644 --- a/arrow-buffer/src/bigint/mod.rs +++ b/arrow-buffer/src/bigint/mod.rs @@ -1676,13 +1676,11 @@ mod tests { // log10 should be 76 or 77 let result = large.checked_ilog(i256::from(10)); - assert!(result.is_some()); - assert!(result.unwrap() >= 76 && result.unwrap() <= 77); + assert_eq!(result.unwrap(), 76); // log5 let result = large.checked_ilog(i256::from(5)); - assert!(result.is_some()); - assert!(result.unwrap() >= 109 && result.unwrap() <= 110); + assert_eq!(result.unwrap(), 109); } #[test] From f09d53f28de7cb62548519946f937b70c50bcc1d Mon Sep 17 00:00:00 2001 From: theirix Date: Fri, 13 Mar 2026 09:41:26 +0000 Subject: [PATCH 3/8] Use available I256::checked_ilog2 --- arrow-buffer/src/bigint/mod.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/arrow-buffer/src/bigint/mod.rs b/arrow-buffer/src/bigint/mod.rs index 58c68fd18cc8..2ff7d883f70b 100644 --- a/arrow-buffer/src/bigint/mod.rs +++ b/arrow-buffer/src/bigint/mod.rs @@ -656,8 +656,12 @@ impl i256 { /// Returns `None` if `self` is less than or equal to zero. #[inline] pub fn checked_ilog2(self) -> Option { - // Switch to I256::checked_ilog2 when I256 switches MSRV - self.checked_ilog(i256::from(2)) + if self <= Self::ZERO { + None + } else { + // Invariants are enforced, so this call cannot fail + self.to_ext_i256().checked_ilog2() + } } /// Computes the base 2 logarithm of the number, rounded down. From 801a7d63acc69871255ae3c37a7aed8f9f89aa2c Mon Sep 17 00:00:00 2001 From: theirix Date: Fri, 13 Mar 2026 09:49:08 +0000 Subject: [PATCH 4/8] Remove checks enforced by i256 --- arrow-buffer/src/bigint/mod.rs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/arrow-buffer/src/bigint/mod.rs b/arrow-buffer/src/bigint/mod.rs index 2ff7d883f70b..45c59f5066cd 100644 --- a/arrow-buffer/src/bigint/mod.rs +++ b/arrow-buffer/src/bigint/mod.rs @@ -620,12 +620,7 @@ impl i256 { /// Returns `None` if `self` is less than or equal to zero, or if `base` is less than 2. #[inline] pub fn checked_ilog(self, base: i256) -> Option { - if self <= Self::ZERO || base < i256::from(2) { - None - } else { - // Invariants are enforced, so this call cannot fail - self.to_ext_i256().checked_ilog(base.to_ext_i256()) - } + self.to_ext_i256().checked_ilog(base.to_ext_i256()) } /// Computes the `base` logarithm of the number `self` @@ -656,12 +651,7 @@ impl i256 { /// Returns `None` if `self` is less than or equal to zero. #[inline] pub fn checked_ilog2(self) -> Option { - if self <= Self::ZERO { - None - } else { - // Invariants are enforced, so this call cannot fail - self.to_ext_i256().checked_ilog2() - } + self.to_ext_i256().checked_ilog2() } /// Computes the base 2 logarithm of the number, rounded down. From fb9d556b4e0239d4a1a7d46c40408f53f97f08ac Mon Sep 17 00:00:00 2001 From: theirix Date: Sat, 25 Apr 2026 23:19:15 +0100 Subject: [PATCH 5/8] Native integer logs implementation --- arrow-buffer/Cargo.toml | 1 - arrow-buffer/src/bigint/mod.rs | 116 ++++++++++++++++++++++++++++----- 2 files changed, 100 insertions(+), 17 deletions(-) diff --git a/arrow-buffer/Cargo.toml b/arrow-buffer/Cargo.toml index 5b6451387cc3..02ea49c37c46 100644 --- a/arrow-buffer/Cargo.toml +++ b/arrow-buffer/Cargo.toml @@ -43,7 +43,6 @@ bytes = { version = "1.4" } num-bigint = { version = "0.4.6", default-features = false, features = ["std"] } num-traits = { version = "0.2.19", default-features = false, features = ["std"] } half = { version = "2.1", default-features = false } -i256 = "0.2.3" [dev-dependencies] criterion = { workspace = true, default-features = false } diff --git a/arrow-buffer/src/bigint/mod.rs b/arrow-buffer/src/bigint/mod.rs index 45c59f5066cd..9e291d140300 100644 --- a/arrow-buffer/src/bigint/mod.rs +++ b/arrow-buffer/src/bigint/mod.rs @@ -17,7 +17,6 @@ use crate::arith::derive_arith; use crate::bigint::div::div_rem; -use ::i256::I256 as ExtI256; use num_bigint::BigInt; use num_traits::{ Bounded, CheckedAdd, CheckedDiv, CheckedMul, CheckedNeg, CheckedRem, CheckedSub, FromPrimitive, @@ -620,7 +619,30 @@ impl i256 { /// Returns `None` if `self` is less than or equal to zero, or if `base` is less than 2. #[inline] pub fn checked_ilog(self, base: i256) -> Option { - self.to_ext_i256().checked_ilog(base.to_ext_i256()) + if base == Self::from(10) { + // Faster implementation for base 10 + return self.checked_ilog10(); + } + + if self <= Self::ZERO { + return None; + } + if base <= Self::ONE { + return None; + } + if self <= base { + return Some(0); + } + + let mut val = 1; + let mut base_exp = base; + + let boundary = self.checked_div(base)?; + while base_exp <= boundary { + val += 1; + base_exp = base_exp.checked_mul(base)?; + } + Some(val) } /// Computes the `base` logarithm of the number `self` @@ -635,8 +657,35 @@ impl i256 { /// Returns `None` if `self` is less than or equal to zero. #[inline] pub fn checked_ilog10(self) -> Option { - // Switch to I256::checked_ilog10 when I256 switches MSRV - self.checked_ilog(i256::from(10)) + if self <= Self::ZERO { + return None; + } + if self < Self::from(10) { + return Some(0); + } + + // Layered approach to calculate logarithm using i128 log operations only + // Consult int_log10.rs stdlib implementiation for u128 + let pow_64: i256 = i256::from(10).checked_pow(64).unwrap(); + let pow_32: i256 = i256::from(10).checked_pow(32).unwrap(); + if self >= pow_64 { + let value = self.checked_div(pow_64)?; + // self is between 10^64 and 10^77 (~i256::MAX). + // `value` is 14 digits max (10^77 / 10^64 = 10^13), + // so it fits to `low` u128 + debug_assert!(value.high == 0); + Some(64 + value.low.checked_ilog10()?) + } else if self >= pow_32 { + let value = self.checked_div(pow_32)?; + // self is between 10^32 and 10^64. + // `value` is 33 digits max (10^64/10^32=10^32) + // so it fits to `low` 128-bit value + debug_assert!(value.high == 0); + Some(32 + value.low.checked_ilog10()?) + } else { + // self fits within u128 (high == 0 and self > 0). + self.low.checked_ilog10() + } } /// Computes the decimal logarithm of the number `self` @@ -651,7 +700,7 @@ impl i256 { /// Returns `None` if `self` is less than or equal to zero. #[inline] pub fn checked_ilog2(self) -> Option { - self.to_ext_i256().checked_ilog2() + self.checked_ilog(i256::from(2)) } /// Computes the base 2 logarithm of the number, rounded down. @@ -660,11 +709,6 @@ impl i256 { self.checked_ilog2() .unwrap_or_else(|| panic!("ilog2 overflow with {self}")) } - - /// Converts self into an `i256::I256` - fn to_ext_i256(self) -> ExtI256 { - ExtI256::from_le_bytes(self.to_le_bytes()) - } } /// Temporary workaround due to lack of stable const array slicing @@ -1663,18 +1707,58 @@ mod tests { assert_eq!(i256::ZERO.checked_ilog10(), None); assert_eq!(i256::ZERO.checked_ilog2(), None); + let value = i256::from_parts(100000000, 1234); + assert_eq!(value.checked_ilog(i256::from(10)), Some(41)); + assert_eq!(value.checked_ilog10(), Some(41)); + // Large i256 values let large = i256::from_parts(100000000, i128::MAX); // log2 of 2 powered to approximately 255 should be 254 assert_eq!(large.checked_ilog(i256::from(2)), Some(254)); - // log10 should be 76 or 77 - let result = large.checked_ilog(i256::from(10)); - assert_eq!(result.unwrap(), 76); + // log10(large)=76 + assert_eq!(large.checked_ilog(i256::from(10)), Some(76)); + assert_eq!(large.checked_ilog10(), Some(76)); + + // log5(large) + assert_eq!(large.checked_ilog(i256::from(5)), Some(109)); + + // Maximum representable value is 2^254 + assert!(i256::from(2).checked_pow(255).is_none()); + let value = i256::from(2).checked_pow(254).expect("construct"); + assert_eq!(value.checked_ilog(i256::from(2)), Some(254)); + + // Logarithm of a maximum representable value is 254 + assert_eq!(i256::MAX.checked_ilog(i256::from(2)), Some(254)); + } + + #[test] + fn test_ilog10() { + // Edge cases + assert_eq!(i256::ZERO.checked_ilog10(), None); + assert_eq!(i256::MINUS_ONE.checked_ilog10(), None); + assert_eq!(i256::MAX.checked_ilog10(), Some(76)); + assert_eq!(i256::from(10).checked_ilog10(), Some(1)); + + // small values + assert_eq!(i256::from(1).checked_ilog10(), Some(0)); + assert_eq!(i256::from(9).checked_ilog10(), Some(0)); + + // case with high == 0 + assert_eq!(i256::from(100).checked_ilog10(), Some(2)); + // case with high == 0 and full low + assert_eq!(i256::from_parts(u128::MAX, 0).checked_ilog10(), Some(38)); + + // case with high > 0 + assert_eq!(i256::from_parts(0, 1).checked_ilog10(), Some(38)); + + // case with non-null high and low, slow branch + let pow50 = i256::from(10).checked_pow(50).unwrap(); + assert_eq!(pow50.checked_ilog10(), Some(50)); - // log5 - let result = large.checked_ilog(i256::from(5)); - assert_eq!(result.unwrap(), 109); + // case with non-null high and low, fast branch + let pow64 = i256::from(10).checked_pow(64).unwrap(); + assert_eq!(pow64.checked_ilog10(), Some(64)); } #[test] From 1cd9c48b4e32f89f04c222641d5ebf5a570488fc Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 2 Jun 2026 13:34:27 -0400 Subject: [PATCH 6/8] arrow-buffer: i256: add tests for ilog where self == base Extends the existing `test_ilog` with cases that the current tests do not cover: small results (0 and 1) for non-base-10 bases, including `self == base`. Per the std contract `n.ilog(n) == 1` (e.g. `2u32.ilog(2) == 1`), but `i256::checked_ilog` currently returns 0 for these inputs. Also cross-checks small results against `u128::ilog` as ground truth. These tests fail until the following fix is applied. Co-Authored-By: Claude Opus 4.8 (1M context) --- arrow-buffer/src/bigint/mod.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/arrow-buffer/src/bigint/mod.rs b/arrow-buffer/src/bigint/mod.rs index 9e291d140300..48789011b34f 100644 --- a/arrow-buffer/src/bigint/mod.rs +++ b/arrow-buffer/src/bigint/mod.rs @@ -1707,6 +1707,31 @@ mod tests { assert_eq!(i256::ZERO.checked_ilog10(), None); assert_eq!(i256::ZERO.checked_ilog2(), None); + // self == base, matches std: `n.ilog(n) == 1` + assert_eq!(i256::from(2).checked_ilog(i256::from(2)), Some(1)); + assert_eq!(i256::from(3).checked_ilog(i256::from(3)), Some(1)); + assert_eq!(i256::from(5).checked_ilog(i256::from(5)), Some(1)); + assert_eq!(i256::from(1000).checked_ilog(i256::from(1000)), Some(1)); + assert_eq!(i256::from(2).checked_ilog2(), Some(1)); + assert_eq!(i256::from(2).ilog2(), 1); + // base 10 goes through the checked_ilog10 fast path + assert_eq!(i256::from(10).checked_ilog(i256::from(10)), Some(1)); + + // self < base is 0 + assert_eq!(i256::from(3).checked_ilog(i256::from(5)), Some(0)); + + // cross-check small results (0 and 1) against u128::ilog + for base in [2i64, 3, 5, 7, 1000] { + for v in 1i64..64 { + let want = (v as u128).ilog(base as u128); + assert_eq!( + i256::from(v).checked_ilog(i256::from(base)), + Some(want), + "checked_ilog({v}, {base})" + ); + } + } + let value = i256::from_parts(100000000, 1234); assert_eq!(value.checked_ilog(i256::from(10)), Some(41)); assert_eq!(value.checked_ilog10(), Some(41)); From 3f8916143ec60ea64c0c4fe02bd2d053f34bfbdf Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 2 Jun 2026 13:34:49 -0400 Subject: [PATCH 7/8] arrow-buffer: i256: fix ilog off-by-one when self == base `checked_ilog` returned the early `Some(0)` for `self == base`, but the correct result is 1 (`base^1 == base <= self`), matching the std contract `n.ilog(n) == 1`. The base-10 path was unaffected since it is handled by `checked_ilog10`, so this affected every other base (including `ilog2(2)`). Narrow the early return from `self <= base` to `self < base`; the loop already computes the correct value for `self == base`. Co-Authored-By: Claude Opus 4.8 (1M context) --- arrow-buffer/src/bigint/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow-buffer/src/bigint/mod.rs b/arrow-buffer/src/bigint/mod.rs index 48789011b34f..5ba87c51b2d1 100644 --- a/arrow-buffer/src/bigint/mod.rs +++ b/arrow-buffer/src/bigint/mod.rs @@ -630,7 +630,7 @@ impl i256 { if base <= Self::ONE { return None; } - if self <= base { + if self < base { return Some(0); } From a7d21c78301c6d35d598fd89a46d51707e546c77 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 2 Jun 2026 17:03:00 -0400 Subject: [PATCH 8/8] chore: fix rustfmt trailing whitespace Co-Authored-By: Claude Opus 4.8 (1M context) --- arrow-buffer/src/bigint/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow-buffer/src/bigint/mod.rs b/arrow-buffer/src/bigint/mod.rs index 5ba87c51b2d1..f52e6473ba8f 100644 --- a/arrow-buffer/src/bigint/mod.rs +++ b/arrow-buffer/src/bigint/mod.rs @@ -1720,7 +1720,7 @@ mod tests { // self < base is 0 assert_eq!(i256::from(3).checked_ilog(i256::from(5)), Some(0)); - // cross-check small results (0 and 1) against u128::ilog + // cross-check small results (0 and 1) against u128::ilog for base in [2i64, 3, 5, 7, 1000] { for v in 1i64..64 { let want = (v as u128).ilog(base as u128);