Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 64 additions & 3 deletions library/core/src/num/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1518,6 +1518,29 @@ pub enum FpCategory {
Normal,
}

/// Largest `k` such that `radix.pow(k) <= 2.pow(bits)` — the largest digit
/// count for which the unchecked `result = result * radix + d` accumulator is
/// guaranteed not to overflow an integer of magnitude `2.pow(bits) - 1`.
/// `radix` must be in `2..=36`.
///
/// Implemented as a macro because a `const fn` defeats cross-crate inlining.
macro_rules! max_safe_digits {
($bits:expr, $radix:expr) => {{
let bits: u32 = $bits;
let radix_u128 = ($radix) as u128;
if bits < 128 {
(1u128 << bits).ilog(radix_u128)
} else {
// 2.pow(128) doesn't fit u128. `u128::MAX.ilog(radix)` agrees with
// the true answer except when `radix` exactly tiles 128 bits
// (power of two dividing 128), where it undercounts by 1.
let base = u128::MAX.ilog(radix_u128);
let exact_tile = ($radix).is_power_of_two() && 128 % ($radix).ilog2() == 0;
base + exact_tile as u32
}
}};
}

/// Determines if a string of text of that length of that radix could be guaranteed to be
/// stored in the given type T.
/// Note that if the radix is known to the compiler, it is just the check of digits.len that
Expand All @@ -1526,7 +1549,8 @@ pub enum FpCategory {
#[inline(always)]
#[unstable(issue = "none", feature = "std_internals")]
pub const fn can_not_overflow<T>(radix: u32, is_signed_ty: bool, digits: &[u8]) -> bool {
radix <= 16 && digits.len() <= size_of::<T>() * 2 - is_signed_ty as usize
let bits = (size_of::<T>() * 8 - is_signed_ty as usize) as u32;
digits.len() <= max_safe_digits!(bits, radix) as usize
}

#[cfg_attr(not(panic = "immediate-abort"), inline(never))]
Expand Down Expand Up @@ -1711,7 +1735,9 @@ macro_rules! from_str_int_impl {
#[doc = concat!("assert!(", stringify!($int_ty), "::from_ascii_radix(b\"1 \", 10).is_err());")]
/// ```
#[unstable(feature = "int_from_ascii", issue = "134821")]
#[inline]
// `inline(always)` so the body's `radix`-dependent `ilog` bound

@ds84182 ds84182 May 18, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bits and is_signed_ty are both compile time constants, and there are only 34 possible radix', so there is a better solution than forcing this to be inlined.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried, but the perf was worse. We could make the always inline just be for arm or we could close this and accept that at some point arm64 llvm will improve?

// and fast-prefix size const-fold at literal-radix call sites.
#[inline(always)]
pub const fn from_ascii_radix(src: &[u8], radix: u32) -> Result<$int_ty, ParseIntError> {
use self::IntErrorKind::*;
use self::ParseIntError as PIE;
Expand Down Expand Up @@ -1747,7 +1773,42 @@ macro_rules! from_str_int_impl {
};
}

if can_not_overflow::<$int_ty>(radix, is_signed_ty, digits) {
// Capture the original-input overflow decision *before* the
// u64 fast-prefix consumes any digits — consuming a safe
// prefix only shrinks the remaining-digit count, so an
// originally-safe input stays safe.
let no_overflow = can_not_overflow::<$int_ty>(radix, is_signed_ty, digits);

// u64 fast-prefix for types wider than `u64` (i.e. `i128`/`u128`):
// 128-bit multiply/add are several times slower than 64-bit on
// 64-bit hardware, so parse as many leading digits as fit in
// `u64`, then promote and let the main loop finish the rest.
if size_of::<$int_ty>() > size_of::<u64>() && !digits.is_empty() {
let prefix_max = max_safe_digits!(64u32, radix) as usize;
// `usize::min` is `const fn` but gated by an unstable
// feature this function can't depend on yet.
let take = if digits.len() < prefix_max { digits.len() } else { prefix_max };
let (mut head, tail) = digits.split_at(take);
let mut r64: u64 = 0;
while let [c, rest @ ..] = head {
let d = unwrap_or_PIE!((*c as char).to_digit(radix), InvalidDigit);
// `head.len() <= prefix_max` keeps `r64` within `u64`.
r64 = r64 * radix as u64 + d as u64;
head = rest;
}
// For unsigned `$int_ty`, `is_positive` is always `true`
// (the `-` arm of the sign match requires `is_signed_ty`),
// so the negative branch is dead and gets eliminated.
// For `i128`, `r64 < 2^64` so the negation cannot overflow.
result = if is_positive {
r64 as $int_ty
} else {
(r64 as $int_ty).wrapping_neg()
};
digits = tail;
}

if no_overflow {
// If the len of the str is short compared to the range of the type
// we are parsing into, then we can be certain that an overflow will not occur.
// This bound is when `radix.pow(digits.len()) - 1 <= T::MAX` but the condition
Expand Down
7 changes: 5 additions & 2 deletions library/coretests/tests/num/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,11 @@ fn test_can_not_overflow() {
for base in 2..=36 {
let num = <u128>::MAX;
let max_len_string = format_radix(num, base as u128);
// base 16 fits perfectly for u128 and won't overflow:
assert_eq!(can_overflow::<u128>(base, &max_len_string), base != 16);
// Powers of two that divide 128 (2, 4, 16) produce a max-length
// string equal to u128::MAX itself, so the precise overflow check
// sees it as safe.
let fits_exactly = matches!(base, 2 | 4 | 16);
assert_eq!(can_overflow::<u128>(base, &max_len_string), !fits_exactly);
}
}

Expand Down
Loading