From 0e141a2be1796c4bab82484fac507166667c687b Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 10 Jun 2025 15:41:02 +0200 Subject: [PATCH 1/3] Implement `_fmt` on `u128` --- library/core/src/fmt/num.rs | 201 +++++++++++++++++++----------------- 1 file changed, 107 insertions(+), 94 deletions(-) diff --git a/library/core/src/fmt/num.rs b/library/core/src/fmt/num.rs index ce878f2da4bd6..eb56e47bfafe7 100644 --- a/library/core/src/fmt/num.rs +++ b/library/core/src/fmt/num.rs @@ -568,120 +568,133 @@ impl_Exp!(i128, u128 as u128 via to_u128 named exp_u128); #[stable(feature = "rust1", since = "1.0.0")] impl fmt::Display for u128 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt_u128(*self, true, f) + const MAX_DEC_N: usize = u128::MAX.ilog(10) as usize + 1; + let mut buf = [MaybeUninit::::uninit(); MAX_DEC_N]; + + f.pad_integral(true, "", self._fmt(&mut buf)) } } #[stable(feature = "rust1", since = "1.0.0")] impl fmt::Display for i128 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt_u128(self.unsigned_abs(), *self >= 0, f) + // This is not a typo, we use the maximum number of digits of `u128`, hence why we use + // `u128::MAX`. + const MAX_DEC_N: usize = u128::MAX.ilog(10) as usize + 1; + let mut buf = [MaybeUninit::::uninit(); MAX_DEC_N]; + + let is_nonnegative = *self >= 0; + f.pad_integral(is_nonnegative, "", self.unsigned_abs()._fmt(&mut buf)) } } -/// Format optimized for u128. Computation of 128 bits is limited by proccessing -/// in batches of 16 decimals at a time. -fn fmt_u128(n: u128, is_nonnegative: bool, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // Optimize common-case zero, which would also need special treatment due to - // its "leading" zero. - if n == 0 { - return f.pad_integral(true, "", "0"); - } +impl u128 { + /// Format optimized for u128. Computation of 128 bits is limited by proccessing + /// in batches of 16 decimals at a time. + #[doc(hidden)] + #[unstable( + feature = "fmt_internals", + reason = "specialized method meant to only be used by `SpecToString` implementation", + issue = "none" + )] + pub fn _fmt<'a>(self, buf: &'a mut [MaybeUninit]) -> &'a str { + const MAX_DEC_N: usize = u128::MAX.ilog(10) as usize + 1; + + // Optimize common-case zero, which would also need special treatment due to + // its "leading" zero. + if self == 0 { + return "0"; + } - // U128::MAX has 39 significant-decimals. - const MAX_DEC_N: usize = u128::MAX.ilog(10) as usize + 1; - // Buffer decimals with right alignment. - let mut buf = [MaybeUninit::::uninit(); MAX_DEC_N]; - - // Take the 16 least-significant decimals. - let (quot_1e16, mod_1e16) = div_rem_1e16(n); - let (mut remain, mut offset) = if quot_1e16 == 0 { - (mod_1e16, MAX_DEC_N) - } else { - // Write digits at buf[23..39]. - enc_16lsd::<{ MAX_DEC_N - 16 }>(&mut buf, mod_1e16); - - // Take another 16 decimals. - let (quot2, mod2) = div_rem_1e16(quot_1e16); - if quot2 == 0 { - (mod2, MAX_DEC_N - 16) + // Take the 16 least-significant decimals. + let (quot_1e16, mod_1e16) = div_rem_1e16(self); + let (mut remain, mut offset) = if quot_1e16 == 0 { + (mod_1e16, MAX_DEC_N) } else { - // Write digits at buf[7..23]. - enc_16lsd::<{ MAX_DEC_N - 32 }>(&mut buf, mod2); - // Quot2 has at most 7 decimals remaining after two 1e16 divisions. - (quot2 as u64, MAX_DEC_N - 32) - } - }; + // Write digits at buf[23..39]. + enc_16lsd::<{ MAX_DEC_N - 16 }>(buf, mod_1e16); - // Format per four digits from the lookup table. - while remain > 999 { - // SAFETY: All of the decimals fit in buf due to MAX_DEC_N - // and the while condition ensures at least 4 more decimals. - unsafe { core::hint::assert_unchecked(offset >= 4) } - // SAFETY: The offset counts down from its initial buf.len() - // without underflow due to the previous precondition. - unsafe { core::hint::assert_unchecked(offset <= buf.len()) } - offset -= 4; + // Take another 16 decimals. + let (quot2, mod2) = div_rem_1e16(quot_1e16); + if quot2 == 0 { + (mod2, MAX_DEC_N - 16) + } else { + // Write digits at buf[7..23]. + enc_16lsd::<{ MAX_DEC_N - 32 }>(buf, mod2); + // Quot2 has at most 7 decimals remaining after two 1e16 divisions. + (quot2 as u64, MAX_DEC_N - 32) + } + }; - // pull two pairs - let quad = remain % 1_00_00; - remain /= 1_00_00; - let pair1 = (quad / 100) as usize; - let pair2 = (quad % 100) as usize; - buf[offset + 0].write(DEC_DIGITS_LUT[pair1 * 2 + 0]); - buf[offset + 1].write(DEC_DIGITS_LUT[pair1 * 2 + 1]); - buf[offset + 2].write(DEC_DIGITS_LUT[pair2 * 2 + 0]); - buf[offset + 3].write(DEC_DIGITS_LUT[pair2 * 2 + 1]); - } + // Format per four digits from the lookup table. + while remain > 999 { + // SAFETY: All of the decimals fit in buf due to MAX_DEC_N + // and the while condition ensures at least 4 more decimals. + unsafe { core::hint::assert_unchecked(offset >= 4) } + // SAFETY: The offset counts down from its initial buf.len() + // without underflow due to the previous precondition. + unsafe { core::hint::assert_unchecked(offset <= buf.len()) } + offset -= 4; + + // pull two pairs + let quad = remain % 1_00_00; + remain /= 1_00_00; + let pair1 = (quad / 100) as usize; + let pair2 = (quad % 100) as usize; + buf[offset + 0].write(DEC_DIGITS_LUT[pair1 * 2 + 0]); + buf[offset + 1].write(DEC_DIGITS_LUT[pair1 * 2 + 1]); + buf[offset + 2].write(DEC_DIGITS_LUT[pair2 * 2 + 0]); + buf[offset + 3].write(DEC_DIGITS_LUT[pair2 * 2 + 1]); + } - // Format per two digits from the lookup table. - if remain > 9 { - // SAFETY: All of the decimals fit in buf due to MAX_DEC_N - // and the if condition ensures at least 2 more decimals. - unsafe { core::hint::assert_unchecked(offset >= 2) } - // SAFETY: The offset counts down from its initial buf.len() - // without underflow due to the previous precondition. - unsafe { core::hint::assert_unchecked(offset <= buf.len()) } - offset -= 2; - - let pair = (remain % 100) as usize; - remain /= 100; - buf[offset + 0].write(DEC_DIGITS_LUT[pair * 2 + 0]); - buf[offset + 1].write(DEC_DIGITS_LUT[pair * 2 + 1]); - } + // Format per two digits from the lookup table. + if remain > 9 { + // SAFETY: All of the decimals fit in buf due to MAX_DEC_N + // and the if condition ensures at least 2 more decimals. + unsafe { core::hint::assert_unchecked(offset >= 2) } + // SAFETY: The offset counts down from its initial buf.len() + // without underflow due to the previous precondition. + unsafe { core::hint::assert_unchecked(offset <= buf.len()) } + offset -= 2; + + let pair = (remain % 100) as usize; + remain /= 100; + buf[offset + 0].write(DEC_DIGITS_LUT[pair * 2 + 0]); + buf[offset + 1].write(DEC_DIGITS_LUT[pair * 2 + 1]); + } - // Format the last remaining digit, if any. - if remain != 0 { - // SAFETY: All of the decimals fit in buf due to MAX_DEC_N - // and the if condition ensures (at least) 1 more decimals. - unsafe { core::hint::assert_unchecked(offset >= 1) } - // SAFETY: The offset counts down from its initial buf.len() - // without underflow due to the previous precondition. - unsafe { core::hint::assert_unchecked(offset <= buf.len()) } - offset -= 1; - - // Either the compiler sees that remain < 10, or it prevents - // a boundary check up next. - let last = (remain & 15) as usize; - buf[offset].write(DEC_DIGITS_LUT[last * 2 + 1]); - // not used: remain = 0; - } + // Format the last remaining digit, if any. + if remain != 0 { + // SAFETY: All of the decimals fit in buf due to MAX_DEC_N + // and the if condition ensures (at least) 1 more decimals. + unsafe { core::hint::assert_unchecked(offset >= 1) } + // SAFETY: The offset counts down from its initial buf.len() + // without underflow due to the previous precondition. + unsafe { core::hint::assert_unchecked(offset <= buf.len()) } + offset -= 1; + + // Either the compiler sees that remain < 10, or it prevents + // a boundary check up next. + let last = (remain & 15) as usize; + buf[offset].write(DEC_DIGITS_LUT[last * 2 + 1]); + // not used: remain = 0; + } - // SAFETY: All buf content since offset is set. - let written = unsafe { buf.get_unchecked(offset..) }; - // SAFETY: Writes use ASCII from the lookup table exclusively. - let as_str = unsafe { - str::from_utf8_unchecked(slice::from_raw_parts( - MaybeUninit::slice_as_ptr(written), - written.len(), - )) - }; - f.pad_integral(is_nonnegative, "", as_str) + // SAFETY: All buf content since offset is set. + let written = unsafe { buf.get_unchecked(offset..) }; + // SAFETY: Writes use ASCII from the lookup table exclusively. + unsafe { + str::from_utf8_unchecked(slice::from_raw_parts( + MaybeUninit::slice_as_ptr(written), + written.len(), + )) + } + } } /// Encodes the 16 least-significant decimals of n into `buf[OFFSET .. OFFSET + /// 16 ]`. -fn enc_16lsd(buf: &mut [MaybeUninit; 39], n: u64) { +fn enc_16lsd(buf: &mut [MaybeUninit], n: u64) { // Consume the least-significant decimals from a working copy. let mut remain = n; From aec32486a8d018b398fc9499898d6ab7dbe93741 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 10 Jun 2025 15:40:49 +0200 Subject: [PATCH 2/3] Specialize `ToString` implementation on `u128` and `i128` --- library/alloc/src/string.rs | 1 + library/core/src/fmt/num.rs | 28 +++++++++++++--------------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/library/alloc/src/string.rs b/library/alloc/src/string.rs index 37614a7ca4571..a3174d74e18cf 100644 --- a/library/alloc/src/string.rs +++ b/library/alloc/src/string.rs @@ -2870,6 +2870,7 @@ impl_to_string! { i32, u32, i64, u64, isize, usize, + i128, u128, } #[cfg(not(no_global_oom_handling))] diff --git a/library/core/src/fmt/num.rs b/library/core/src/fmt/num.rs index eb56e47bfafe7..13cd7f710943d 100644 --- a/library/core/src/fmt/num.rs +++ b/library/core/src/fmt/num.rs @@ -565,11 +565,12 @@ mod imp { } impl_Exp!(i128, u128 as u128 via to_u128 named exp_u128); +const U128_MAX_DEC_N: usize = u128::MAX.ilog(10) as usize + 1; + #[stable(feature = "rust1", since = "1.0.0")] impl fmt::Display for u128 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - const MAX_DEC_N: usize = u128::MAX.ilog(10) as usize + 1; - let mut buf = [MaybeUninit::::uninit(); MAX_DEC_N]; + let mut buf = [MaybeUninit::::uninit(); U128_MAX_DEC_N]; f.pad_integral(true, "", self._fmt(&mut buf)) } @@ -579,9 +580,8 @@ impl fmt::Display for u128 { impl fmt::Display for i128 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // This is not a typo, we use the maximum number of digits of `u128`, hence why we use - // `u128::MAX`. - const MAX_DEC_N: usize = u128::MAX.ilog(10) as usize + 1; - let mut buf = [MaybeUninit::::uninit(); MAX_DEC_N]; + // `U128_MAX_DEC_N`. + let mut buf = [MaybeUninit::::uninit(); U128_MAX_DEC_N]; let is_nonnegative = *self >= 0; f.pad_integral(is_nonnegative, "", self.unsigned_abs()._fmt(&mut buf)) @@ -598,8 +598,6 @@ impl u128 { issue = "none" )] pub fn _fmt<'a>(self, buf: &'a mut [MaybeUninit]) -> &'a str { - const MAX_DEC_N: usize = u128::MAX.ilog(10) as usize + 1; - // Optimize common-case zero, which would also need special treatment due to // its "leading" zero. if self == 0 { @@ -609,26 +607,26 @@ impl u128 { // Take the 16 least-significant decimals. let (quot_1e16, mod_1e16) = div_rem_1e16(self); let (mut remain, mut offset) = if quot_1e16 == 0 { - (mod_1e16, MAX_DEC_N) + (mod_1e16, U128_MAX_DEC_N) } else { // Write digits at buf[23..39]. - enc_16lsd::<{ MAX_DEC_N - 16 }>(buf, mod_1e16); + enc_16lsd::<{ U128_MAX_DEC_N - 16 }>(buf, mod_1e16); // Take another 16 decimals. let (quot2, mod2) = div_rem_1e16(quot_1e16); if quot2 == 0 { - (mod2, MAX_DEC_N - 16) + (mod2, U128_MAX_DEC_N - 16) } else { // Write digits at buf[7..23]. - enc_16lsd::<{ MAX_DEC_N - 32 }>(buf, mod2); + enc_16lsd::<{ U128_MAX_DEC_N - 32 }>(buf, mod2); // Quot2 has at most 7 decimals remaining after two 1e16 divisions. - (quot2 as u64, MAX_DEC_N - 32) + (quot2 as u64, U128_MAX_DEC_N - 32) } }; // Format per four digits from the lookup table. while remain > 999 { - // SAFETY: All of the decimals fit in buf due to MAX_DEC_N + // SAFETY: All of the decimals fit in buf due to U128_MAX_DEC_N // and the while condition ensures at least 4 more decimals. unsafe { core::hint::assert_unchecked(offset >= 4) } // SAFETY: The offset counts down from its initial buf.len() @@ -649,7 +647,7 @@ impl u128 { // Format per two digits from the lookup table. if remain > 9 { - // SAFETY: All of the decimals fit in buf due to MAX_DEC_N + // SAFETY: All of the decimals fit in buf due to U128_MAX_DEC_N // and the if condition ensures at least 2 more decimals. unsafe { core::hint::assert_unchecked(offset >= 2) } // SAFETY: The offset counts down from its initial buf.len() @@ -665,7 +663,7 @@ impl u128 { // Format the last remaining digit, if any. if remain != 0 { - // SAFETY: All of the decimals fit in buf due to MAX_DEC_N + // SAFETY: All of the decimals fit in buf due to U128_MAX_DEC_N // and the if condition ensures (at least) 1 more decimals. unsafe { core::hint::assert_unchecked(offset >= 1) } // SAFETY: The offset counts down from its initial buf.len() From 9b09948897886541bc4f31a9d07bf4a2e1680171 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 10 Jun 2025 15:41:16 +0200 Subject: [PATCH 3/3] Extend num tests on `usize` and `isize` as well --- library/alloctests/tests/num.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/alloctests/tests/num.rs b/library/alloctests/tests/num.rs index 3c76e68c60640..a169bbec8e0c9 100644 --- a/library/alloctests/tests/num.rs +++ b/library/alloctests/tests/num.rs @@ -52,6 +52,8 @@ int_to_s!( i32, test_i64_to_string, i64, + test_isize_to_string, + isize, test_i128_to_string, i128, ); @@ -64,6 +66,8 @@ uint_to_s!( u32, test_u64_to_string, u64, + test_usize_to_string, + usize, test_u128_to_string, u128, );