diff --git a/library/core/src/fmt/num.rs b/library/core/src/fmt/num.rs index e9302a6127f5c..885f461d7ed0d 100644 --- a/library/core/src/fmt/num.rs +++ b/library/core/src/fmt/num.rs @@ -210,10 +210,19 @@ macro_rules! impl_Display { remain /= scale; let pair1 = (quad / 100) as usize; let pair2 = (quad % 100) as usize; - buf[offset + 0].write(DECIMAL_PAIRS[pair1 * 2 + 0]); - buf[offset + 1].write(DECIMAL_PAIRS[pair1 * 2 + 1]); - buf[offset + 2].write(DECIMAL_PAIRS[pair2 * 2 + 0]); - buf[offset + 3].write(DECIMAL_PAIRS[pair2 * 2 + 1]); + // Unchecked indexing here avoids relying on LLVM to elide the + // bounds checks from the surrounding `assert_unchecked` hints; + // this regressed under `opt-level=z` + fat LTO on LLVM 21 + // (see rust-lang/rust#152061). + // + // SAFETY: `offset + 4 <= buf.len()`, and `pair1, pair2 < 100` + // so all `DECIMAL_PAIRS` indices are `< 200 == DECIMAL_PAIRS.len()`. + unsafe { + buf.get_unchecked_mut(offset).write(*DECIMAL_PAIRS.get_unchecked(pair1 * 2)); + buf.get_unchecked_mut(offset + 1).write(*DECIMAL_PAIRS.get_unchecked(pair1 * 2 + 1)); + buf.get_unchecked_mut(offset + 2).write(*DECIMAL_PAIRS.get_unchecked(pair2 * 2)); + buf.get_unchecked_mut(offset + 3).write(*DECIMAL_PAIRS.get_unchecked(pair2 * 2 + 1)); + } } // Format per two digits from the lookup table. @@ -228,8 +237,14 @@ macro_rules! impl_Display { let pair = (remain % 100) as usize; remain /= 100; - buf[offset + 0].write(DECIMAL_PAIRS[pair * 2 + 0]); - buf[offset + 1].write(DECIMAL_PAIRS[pair * 2 + 1]); + // See the outer loop for the rationale (rust-lang/rust#152061). + // + // SAFETY: `offset + 2 <= buf.len()`, and `pair < 100` so the + // `DECIMAL_PAIRS` indices are `< 200 == DECIMAL_PAIRS.len()`. + unsafe { + buf.get_unchecked_mut(offset).write(*DECIMAL_PAIRS.get_unchecked(pair * 2)); + buf.get_unchecked_mut(offset + 1).write(*DECIMAL_PAIRS.get_unchecked(pair * 2 + 1)); + } } // Format the last remaining digit, if any. @@ -242,10 +257,14 @@ macro_rules! impl_Display { 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(DECIMAL_PAIRS[last * 2 + 1]); + // See the outer loop for the rationale (rust-lang/rust#152061). + // + // SAFETY: `offset < buf.len()`, and `last < 16` so + // `last * 2 + 1 < 32 < 200 == DECIMAL_PAIRS.len()`. + unsafe { + buf.get_unchecked_mut(offset).write(*DECIMAL_PAIRS.get_unchecked(last * 2 + 1)); + } // not used: remain = 0; } diff --git a/tests/codegen-llvm/issues/fmt-display-no-bounds-check-152061.rs b/tests/codegen-llvm/issues/fmt-display-no-bounds-check-152061.rs new file mode 100644 index 0000000000000..f268e05647deb --- /dev/null +++ b/tests/codegen-llvm/issues/fmt-display-no-bounds-check-152061.rs @@ -0,0 +1,48 @@ +//@ revisions: opt-3 opt-z +//@[opt-3] compile-flags: -Copt-level=3 +//@[opt-z] compile-flags: -Copt-level=z +// Regression test for https://github.com/rust-lang/rust/issues/152061. +// +// `impl fmt::Display` for integers, lowered through `_fmt_inner` in +// `library/core/src/fmt/num.rs`, used to leave a `panic_bounds_check` +// path in optimised LLVM IR when LLVM failed to propagate the +// `assume`-based range information (notably under `opt-level=z` + fat +// LTO with LLVM 21). The implementation was rewritten to use +// `get_unchecked{_mut}` for the buffer writes, so the `panic_bounds_check` +// path must not appear regardless of the optimiser's propagation. + +#![crate_type = "lib"] + +use std::fmt::Write; + +pub struct NoopWriter; +impl Write for NoopWriter { + fn write_str(&mut self, _s: &str) -> std::fmt::Result { + Ok(()) + } +} + +// CHECK-LABEL: @format_usize +#[no_mangle] +pub fn format_usize(w: &mut NoopWriter, x: usize) { + // The Display path through `_fmt_inner` must not emit a bounds check. + // CHECK-NOT: panic_bounds_check + let _ = write!(w, "{}", x); +} + +// CHECK-LABEL: @format_u64 +#[no_mangle] +pub fn format_u64(w: &mut NoopWriter, x: u64) { + // CHECK-NOT: panic_bounds_check + let _ = write!(w, "{}", x); +} + +// Sanity check: make sure `panic_bounds_check` is still the symbol LLVM +// emits for a non-elidable out-of-bounds index, so the `CHECK-NOT`s +// above are guarding against something real and cannot pass vacuously. +// CHECK-LABEL: @test_check +#[no_mangle] +pub fn test_check(arr: &[u8], i: usize) -> u8 { + // CHECK: panic_bounds_check + arr[i] +}