Skip to content

Commit 816944b

Browse files
committed
Auto merge of rust-lang#120594 - saethlin:delayed-debug-asserts, r=oli-obk
Toggle assert_unsafe_precondition in codegen instead of expansion The goal of this PR is to make some of the unsafe precondition checks in the standard library available in debug builds. Some UI tests are included to verify that it does that. The diff is large, but most of it is blessing mir-opt tests and I've also split up this PR so it can be reviewed commit-by-commit. This PR: 1. Adds a new intrinsic, `debug_assertions` which is lowered to a new MIR NullOp, and only to a constant after monomorphization 2. Rewrites `assume_unsafe_precondition` to check the new intrinsic, and be monomorphic. 3. Skips codegen of the `assume` intrinsic in unoptimized builds, because that was silly before but with these checks it's *very* silly 4. The checks with the most overhead are `ptr::read`/`ptr::write` and `NonNull::new_unchecked`. I've simply added `#[cfg(debug_assertions)]` to the checks for `ptr::read`/`ptr::write` because I was unable to come up with any (good) ideas for decreasing their impact. But for `NonNull::new_unchecked` I found that the majority of callers can use a different function, often a safe one. Yes, this PR slows down the compile time of some programs. But in our benchmark suite it's never more than 1% icount, and the average icount change in debug-full programs is 0.22%. I think that is acceptable for such an improvement in developer experience. rust-lang#120539 (comment)
2 parents 95fc1c7 + f859096 commit 816944b

File tree

13 files changed

+189
-86
lines changed

13 files changed

+189
-86
lines changed

alloc/src/raw_vec.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -207,11 +207,7 @@ impl<T, A: Allocator> RawVec<T, A> {
207207
// Allocators currently return a `NonNull<[u8]>` whose length
208208
// matches the size requested. If that ever changes, the capacity
209209
// here should change to `ptr.len() / mem::size_of::<T>()`.
210-
Self {
211-
ptr: unsafe { Unique::new_unchecked(ptr.cast().as_ptr()) },
212-
cap: unsafe { Cap(capacity) },
213-
alloc,
214-
}
210+
Self { ptr: Unique::from(ptr.cast()), cap: unsafe { Cap(capacity) }, alloc }
215211
}
216212
}
217213

@@ -239,6 +235,11 @@ impl<T, A: Allocator> RawVec<T, A> {
239235
self.ptr.as_ptr()
240236
}
241237

238+
#[inline]
239+
pub fn non_null(&self) -> NonNull<T> {
240+
NonNull::from(self.ptr)
241+
}
242+
242243
/// Gets the capacity of the allocation.
243244
///
244245
/// This will always be `usize::MAX` if `T` is zero-sized.
@@ -398,7 +399,7 @@ impl<T, A: Allocator> RawVec<T, A> {
398399
// Allocators currently return a `NonNull<[u8]>` whose length matches
399400
// the size requested. If that ever changes, the capacity here should
400401
// change to `ptr.len() / mem::size_of::<T>()`.
401-
self.ptr = unsafe { Unique::new_unchecked(ptr.cast().as_ptr()) };
402+
self.ptr = Unique::from(ptr.cast());
402403
self.cap = unsafe { Cap(cap) };
403404
}
404405

alloc/src/vec/into_iter.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ impl<T, A: Allocator> IntoIter<T, A> {
136136
// struct and then overwriting &mut self.
137137
// this creates less assembly
138138
self.cap = 0;
139-
self.buf = unsafe { NonNull::new_unchecked(RawVec::NEW.ptr()) };
139+
self.buf = RawVec::NEW.non_null();
140140
self.ptr = self.buf;
141141
self.end = self.buf.as_ptr();
142142

alloc/src/vec/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2861,16 +2861,16 @@ impl<T, A: Allocator> IntoIterator for Vec<T, A> {
28612861
#[inline]
28622862
fn into_iter(self) -> Self::IntoIter {
28632863
unsafe {
2864-
let mut me = ManuallyDrop::new(self);
2864+
let me = ManuallyDrop::new(self);
28652865
let alloc = ManuallyDrop::new(ptr::read(me.allocator()));
2866-
let begin = me.as_mut_ptr();
2866+
let buf = me.buf.non_null();
2867+
let begin = buf.as_ptr();
28672868
let end = if T::IS_ZST {
28682869
begin.wrapping_byte_add(me.len())
28692870
} else {
28702871
begin.add(me.len()) as *const T
28712872
};
28722873
let cap = me.buf.capacity();
2873-
let buf = NonNull::new_unchecked(begin);
28742874
IntoIter { buf, phantom: PhantomData, cap, alloc, ptr: buf, end }
28752875
}
28762876
}

core/src/char/convert.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ pub(super) const unsafe fn from_u32_unchecked(i: u32) -> char {
2727
unsafe {
2828
assert_unsafe_precondition!(
2929
"invalid value for `char`",
30-
(i: u32) => char_try_from_u32(i).is_ok()
30+
(i: u32 = i) => char_try_from_u32(i).is_ok()
3131
);
3232
transmute(i)
3333
}

core/src/hint.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ pub const unsafe fn assert_unchecked(cond: bool) {
148148
unsafe {
149149
intrinsics::assert_unsafe_precondition!(
150150
"hint::assert_unchecked must never be called when the condition is false",
151-
(cond: bool) => cond,
151+
(cond: bool = cond) => cond,
152152
);
153153
crate::intrinsics::assume(cond);
154154
}

core/src/intrinsics.rs

Lines changed: 96 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656

5757
use crate::marker::DiscriminantKind;
5858
use crate::marker::Tuple;
59-
use crate::mem;
59+
use crate::mem::{self, align_of};
6060

6161
pub mod mir;
6262
pub mod simd;
@@ -2569,6 +2569,17 @@ extern "rust-intrinsic" {
25692569
#[rustc_nounwind]
25702570
#[cfg(not(bootstrap))]
25712571
pub fn is_val_statically_known<T: Copy>(arg: T) -> bool;
2572+
2573+
#[rustc_const_unstable(feature = "delayed_debug_assertions", issue = "none")]
2574+
#[rustc_safe_intrinsic]
2575+
#[cfg(not(bootstrap))]
2576+
pub(crate) fn debug_assertions() -> bool;
2577+
}
2578+
2579+
#[cfg(bootstrap)]
2580+
#[rustc_const_unstable(feature = "delayed_debug_assertions", issue = "none")]
2581+
pub(crate) const fn debug_assertions() -> bool {
2582+
cfg!(debug_assertions)
25722583
}
25732584

25742585
// FIXME: Seems using `unstable` here completely ignores `rustc_allow_const_fn_unstable`
@@ -2587,10 +2598,27 @@ pub const unsafe fn is_val_statically_known<T: Copy>(_arg: T) -> bool {
25872598
/// Check that the preconditions of an unsafe function are followed, if debug_assertions are on,
25882599
/// and only at runtime.
25892600
///
2590-
/// This macro should be called as `assert_unsafe_precondition!([Generics](name: Type) => Expression)`
2591-
/// where the names specified will be moved into the macro as captured variables, and defines an item
2592-
/// to call `const_eval_select` on. The tokens inside the square brackets are used to denote generics
2593-
/// for the function declarations and can be omitted if there is no generics.
2601+
/// This macro should be called as
2602+
/// `assert_unsafe_precondition!((expr => name: Type, expr => name: Type) => Expression)`
2603+
/// where each `expr` will be evaluated and passed in as function argument `name: Type`. Then all
2604+
/// those arguments are passed to a function via [`const_eval_select`].
2605+
///
2606+
/// These checks are behind a condition which is evaluated at codegen time, not expansion time like
2607+
/// [`debug_assert`]. This means that a standard library built with optimizations and debug
2608+
/// assertions disabled will have these checks optimized out of its monomorphizations, but if a
2609+
/// a caller of the standard library has debug assertions enabled and monomorphizes an expansion of
2610+
/// this macro, that monomorphization will contain the check.
2611+
///
2612+
/// Since these checks cannot be optimized out in MIR, some care must be taken in both call and
2613+
/// implementation to mitigate their compile-time overhead. The runtime function that we
2614+
/// [`const_eval_select`] to is monomorphic, `#[inline(never)]`, and `#[rustc_nounwind]`. That
2615+
/// combination of properties ensures that the code for the checks is only compiled once, and has a
2616+
/// minimal impact on the caller's code size.
2617+
///
2618+
/// Caller should also introducing any other `let` bindings or any code outside this macro in order
2619+
/// to call it. Since the precompiled standard library is built with full debuginfo and these
2620+
/// variables cannot be optimized out in MIR, an innocent-looking `let` can produce enough
2621+
/// debuginfo to have a measurable compile-time impact on debug builds.
25942622
///
25952623
/// # Safety
25962624
///
@@ -2604,26 +2632,24 @@ pub const unsafe fn is_val_statically_known<T: Copy>(_arg: T) -> bool {
26042632
///
26052633
/// So in a sense it is UB if this macro is useful, but we expect callers of `unsafe fn` to make
26062634
/// the occasional mistake, and this check should help them figure things out.
2607-
#[allow_internal_unstable(const_eval_select)] // permit this to be called in stably-const fn
2635+
#[allow_internal_unstable(const_eval_select, delayed_debug_assertions)] // permit this to be called in stably-const fn
26082636
macro_rules! assert_unsafe_precondition {
2609-
($name:expr, $([$($tt:tt)*])?($($i:ident:$ty:ty),*$(,)?) => $e:expr $(,)?) => {
2610-
if cfg!(debug_assertions) {
2611-
// allow non_snake_case to allow capturing const generics
2612-
#[allow(non_snake_case)]
2613-
#[inline(always)]
2614-
fn runtime$(<$($tt)*>)?($($i:$ty),*) {
2637+
($message:expr, ($($name:ident:$ty:ty = $arg:expr),*$(,)?) => $e:expr $(,)?) => {
2638+
{
2639+
#[inline(never)]
2640+
#[rustc_nounwind]
2641+
fn precondition_check($($name:$ty),*) {
26152642
if !$e {
2616-
// don't unwind to reduce impact on code size
26172643
::core::panicking::panic_nounwind(
2618-
concat!("unsafe precondition(s) violated: ", $name)
2644+
concat!("unsafe precondition(s) violated: ", $message)
26192645
);
26202646
}
26212647
}
2622-
#[allow(non_snake_case)]
2623-
#[inline]
2624-
const fn comptime$(<$($tt)*>)?($(_:$ty),*) {}
2648+
const fn comptime($(_:$ty),*) {}
26252649

2626-
::core::intrinsics::const_eval_select(($($i,)*), comptime, runtime);
2650+
if ::core::intrinsics::debug_assertions() {
2651+
::core::intrinsics::const_eval_select(($($arg,)*), comptime, precondition_check);
2652+
}
26272653
}
26282654
};
26292655
}
@@ -2632,30 +2658,47 @@ pub(crate) use assert_unsafe_precondition;
26322658
/// Checks whether `ptr` is properly aligned with respect to
26332659
/// `align_of::<T>()`.
26342660
#[inline]
2635-
pub(crate) fn is_aligned_and_not_null<T>(ptr: *const T) -> bool {
2636-
!ptr.is_null() && ptr.is_aligned()
2661+
pub(crate) fn is_aligned_and_not_null(ptr: *const (), align: usize) -> bool {
2662+
!ptr.is_null() && ptr.is_aligned_to(align)
26372663
}
26382664

2639-
/// Checks whether an allocation of `len` instances of `T` exceeds
2640-
/// the maximum allowed allocation size.
26412665
#[inline]
2642-
pub(crate) fn is_valid_allocation_size<T>(len: usize) -> bool {
2643-
let max_len = const {
2644-
let size = crate::mem::size_of::<T>();
2645-
if size == 0 { usize::MAX } else { isize::MAX as usize / size }
2646-
};
2666+
pub(crate) fn is_valid_allocation_size(size: usize, len: usize) -> bool {
2667+
let max_len = if size == 0 { usize::MAX } else { isize::MAX as usize / size };
26472668
len <= max_len
26482669
}
26492670

2671+
pub(crate) fn is_nonoverlapping_mono(
2672+
src: *const (),
2673+
dst: *const (),
2674+
size: usize,
2675+
count: usize,
2676+
) -> bool {
2677+
let src_usize = src.addr();
2678+
let dst_usize = dst.addr();
2679+
let Some(size) = size.checked_mul(count) else {
2680+
crate::panicking::panic_nounwind(
2681+
"is_nonoverlapping: `size_of::<T>() * count` overflows a usize",
2682+
)
2683+
};
2684+
let diff = src_usize.abs_diff(dst_usize);
2685+
// If the absolute distance between the ptrs is at least as big as the size of the buffer,
2686+
// they do not overlap.
2687+
diff >= size
2688+
}
2689+
26502690
/// Checks whether the regions of memory starting at `src` and `dst` of size
26512691
/// `count * size_of::<T>()` do *not* overlap.
26522692
#[inline]
26532693
pub(crate) fn is_nonoverlapping<T>(src: *const T, dst: *const T, count: usize) -> bool {
26542694
let src_usize = src.addr();
26552695
let dst_usize = dst.addr();
2656-
let size = mem::size_of::<T>()
2657-
.checked_mul(count)
2658-
.expect("is_nonoverlapping: `size_of::<T>() * count` overflows a usize");
2696+
let Some(size) = mem::size_of::<T>().checked_mul(count) else {
2697+
// Use panic_nounwind instead of Option::expect, so that this function is nounwind.
2698+
crate::panicking::panic_nounwind(
2699+
"is_nonoverlapping: `size_of::<T>() * count` overflows a usize",
2700+
)
2701+
};
26592702
let diff = src_usize.abs_diff(dst_usize);
26602703
// If the absolute distance between the ptrs is at least as big as the size of the buffer,
26612704
// they do not overlap.
@@ -2766,10 +2809,16 @@ pub const unsafe fn copy_nonoverlapping<T>(src: *const T, dst: *mut T, count: us
27662809
assert_unsafe_precondition!(
27672810
"ptr::copy_nonoverlapping requires that both pointer arguments are aligned and non-null \
27682811
and the specified memory ranges do not overlap",
2769-
[T](src: *const T, dst: *mut T, count: usize) =>
2770-
is_aligned_and_not_null(src)
2771-
&& is_aligned_and_not_null(dst)
2772-
&& is_nonoverlapping(src, dst, count)
2812+
(
2813+
src: *const () = src as *const (),
2814+
dst: *mut () = dst as *mut (),
2815+
size: usize = size_of::<T>(),
2816+
align: usize = align_of::<T>(),
2817+
count: usize = count,
2818+
) =>
2819+
is_aligned_and_not_null(src, align)
2820+
&& is_aligned_and_not_null(dst, align)
2821+
&& is_nonoverlapping_mono(src, dst, size, count)
27732822
);
27742823
copy_nonoverlapping(src, dst, count)
27752824
}
@@ -2859,9 +2908,15 @@ pub const unsafe fn copy<T>(src: *const T, dst: *mut T, count: usize) {
28592908
// SAFETY: the safety contract for `copy` must be upheld by the caller.
28602909
unsafe {
28612910
assert_unsafe_precondition!(
2862-
"ptr::copy requires that both pointer arguments are aligned and non-null",
2863-
[T](src: *const T, dst: *mut T) =>
2864-
is_aligned_and_not_null(src) && is_aligned_and_not_null(dst)
2911+
"ptr::copy_nonoverlapping requires that both pointer arguments are aligned and non-null \
2912+
and the specified memory ranges do not overlap",
2913+
(
2914+
src: *const () = src as *const (),
2915+
dst: *mut () = dst as *mut (),
2916+
align: usize = align_of::<T>(),
2917+
) =>
2918+
is_aligned_and_not_null(src, align)
2919+
&& is_aligned_and_not_null(dst, align)
28652920
);
28662921
copy(src, dst, count)
28672922
}
@@ -2934,7 +2989,10 @@ pub const unsafe fn write_bytes<T>(dst: *mut T, val: u8, count: usize) {
29342989
unsafe {
29352990
assert_unsafe_precondition!(
29362991
"ptr::write_bytes requires that the destination pointer is aligned and non-null",
2937-
[T](dst: *mut T) => is_aligned_and_not_null(dst)
2992+
(
2993+
addr: *const () = dst as *const (),
2994+
align: usize = align_of::<T>(),
2995+
) => is_aligned_and_not_null(addr, align)
29382996
);
29392997
write_bytes(dst, val, count)
29402998
}

core/src/option.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1033,7 +1033,6 @@ impl<T> Option<T> {
10331033
#[stable(feature = "option_result_unwrap_unchecked", since = "1.58.0")]
10341034
#[rustc_const_unstable(feature = "const_option_ext", issue = "91930")]
10351035
pub const unsafe fn unwrap_unchecked(self) -> T {
1036-
debug_assert!(self.is_some());
10371036
match self {
10381037
Some(val) => val,
10391038
// SAFETY: the safety contract must be upheld by the caller.

core/src/ptr/const_ptr.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -806,13 +806,15 @@ impl<T: ?Sized> *const T {
806806
where
807807
T: Sized,
808808
{
809-
let this = self;
810809
// SAFETY: The comparison has no side-effects, and the intrinsic
811810
// does this check internally in the CTFE implementation.
812811
unsafe {
813812
assert_unsafe_precondition!(
814-
"ptr::sub_ptr requires `this >= origin`",
815-
[T](this: *const T, origin: *const T) => this >= origin
813+
"ptr::sub_ptr requires `self >= origin`",
814+
(
815+
this: *const () = self as *const (),
816+
origin: *const () = origin as *const (),
817+
) => this >= origin
816818
)
817819
};
818820

0 commit comments

Comments
 (0)