From c5727d95f1fc8bd5b34eaed0c40e3ec72fbc73dc Mon Sep 17 00:00:00 2001 From: Zachary S Date: Mon, 28 Jul 2025 16:13:07 -0500 Subject: [PATCH 1/9] Implement some more checks for `ptr_guaranteed_cmp` in consteval: Pointers with different residues modulo their least common allocation alignment are never equal. Pointers to the same static allocation are equal if and only if they have the same offset. Pointers to different non-zero-sized static allocations are unequal if both point within their allocation, and not on opposite ends. --- .../src/const_eval/machine.rs | 92 +++++++++- tests/ui/const-ptr/guaranteed_cmp.rs | 171 ++++++++++++++++++ 2 files changed, 256 insertions(+), 7 deletions(-) create mode 100644 tests/ui/const-ptr/guaranteed_cmp.rs diff --git a/compiler/rustc_const_eval/src/const_eval/machine.rs b/compiler/rustc_const_eval/src/const_eval/machine.rs index a18ae79f318df..a4ba04ef8347f 100644 --- a/compiler/rustc_const_eval/src/const_eval/machine.rs +++ b/compiler/rustc_const_eval/src/const_eval/machine.rs @@ -289,13 +289,91 @@ impl<'tcx> CompileTimeInterpCx<'tcx> { } // Other ways of comparing integers and pointers can never be known for sure. (Scalar::Int { .. }, Scalar::Ptr(..)) | (Scalar::Ptr(..), Scalar::Int { .. }) => 2, - // FIXME: return a `1` for when both sides are the same pointer, *except* that - // some things (like functions and vtables) do not have stable addresses - // so we need to be careful around them (see e.g. #73722). - // FIXME: return `0` for at least some comparisons where we can reliably - // determine the result of runtime inequality tests at compile-time. - // Examples include comparison of addresses in different static items. - (Scalar::Ptr(..), Scalar::Ptr(..)) => 2, + (Scalar::Ptr(a, _), Scalar::Ptr(b, _)) => { + let (a_prov, a_offset) = a.prov_and_relative_offset(); + let (b_prov, b_offset) = b.prov_and_relative_offset(); + let a_allocid = a_prov.alloc_id(); + let b_allocid = b_prov.alloc_id(); + let a_info = self.get_alloc_info(a_allocid); + let b_info = self.get_alloc_info(b_allocid); + + // Check if the pointers cannot be equal due to alignment + if a_info.align > Align::ONE && b_info.align > Align::ONE { + let min_align = Ord::min(a_info.align.bytes(), b_info.align.bytes()); + let a_residue = a_offset.bytes() % min_align; + let b_residue = b_offset.bytes() % min_align; + if a_residue != b_residue { + // If the two pointers have a different residue from their + // common alignment, they cannot be equal. + return interp_ok(0); + } + // The pointers have the same residue modulo their common alignment, + // so they could be equal. Try the other checks. + } + + if a_allocid == b_allocid { + match self.tcx.try_get_global_alloc(a_allocid) { + None => 2, + // A static cannot be duplicated, so if two pointers are into the same + // static, they are equal if and only if their offsets into the static + // are equal + Some(GlobalAlloc::Static(_)) => (a_offset == b_offset) as u8, + // Functions and vtables can be duplicated (and deduplicated), so we + // cannot be sure of runtime equality of pointers to the same one, (or the + // runtime inequality of pointers to different ones) (see e.g. #73722). + Some(GlobalAlloc::Function { .. } | GlobalAlloc::VTable(..)) => 2, + // FIXME: Can these be duplicated (or deduplicated)? + Some(GlobalAlloc::Memory(..) | GlobalAlloc::TypeId { .. }) => 2, + } + } else { + if let (Some(GlobalAlloc::Static(a_did)), Some(GlobalAlloc::Static(b_did))) = ( + self.tcx.try_get_global_alloc(a_allocid), + self.tcx.try_get_global_alloc(b_allocid), + ) { + debug_assert_ne!( + a_did, b_did, + "same static item DefId had two different AllocIds? {a_allocid:?} != {b_allocid:?}, {a_did:?} == {b_did:?}" + ); + + if a_info.size == Size::ZERO || b_info.size == Size::ZERO { + // One or both allocations is zero-sized, so we can't know if the + // pointers are (in)equal. + // FIXME: Can zero-sized static be "within" non-zero-sized statics? + // Conservatively we say yes, since that doesn't cause them to + // "overlap" any bytes, but if not, then we could delete this branch + // and have the other branches handle ZST allocations. + 2 + } else if a_offset > a_info.size || b_offset > b_info.size { + // One or both pointers are out of bounds of their allocation, + // so conservatively say we don't know. + // FIXME: we could reason about how far out of bounds the pointers are, + // e.g. two pointers cannot be equal if them being equal would require + // their statics to overlap. + 2 + } else if (a_offset == Size::ZERO && b_offset == b_info.size) + || (a_offset == a_info.size && b_offset == Size::ZERO) + { + // The pointers are on opposite ends of different allocations, we + // cannot know if they are equal, since the allocations may end up + // adjacent at runtime. + 2 + } else { + // The pointers are within (or one past the end of) different + // non-zero-sized static allocations, and they are not at oppotiste + // ends, so we know they are not equal because statics cannot + // overlap or be deduplicated. + 0 + } + } else { + // Even if one of them is a static, as per https://doc.rust-lang.org/nightly/reference/items/static-items.html#r-items.static.storage-disjointness + // immutable statics can overlap with other kinds of allocations somtimes. + // FIXME: We could be more decisive for mutable statics, which cannot + // overlap with other kinds of allocations. + // FIXME: Can we determine any other cases? + 2 + } + } + } }) } } diff --git a/tests/ui/const-ptr/guaranteed_cmp.rs b/tests/ui/const-ptr/guaranteed_cmp.rs new file mode 100644 index 0000000000000..645ca35598d1d --- /dev/null +++ b/tests/ui/const-ptr/guaranteed_cmp.rs @@ -0,0 +1,171 @@ +//@ build-pass +//@ edition: 2024 +#![feature(const_raw_ptr_comparison)] +#![feature(fn_align)] +// Generally: +// For any `Some` return, `None` would also be valid, unless otherwise noted. +// For any `None` return, only `None` is valid, unless otherwise noted. + +macro_rules! do_test { + ($a:expr, $b:expr, $expected:pat) => { + const _: () = { + let a: *const _ = $a; + let b: *const _ = $b; + assert!(matches!(<*const u8>::guaranteed_eq(a.cast(), b.cast()), $expected)); + }; + }; +} + +#[repr(align(2))] +struct T(#[allow(unused)] u16); + +#[repr(align(2))] +struct AlignedZst; + +static A: T = T(42); +static B: T = T(42); +static mut MUT_STATIC: T = T(42); +static ZST: () = (); +static ALIGNED_ZST: AlignedZst = AlignedZst; +static LARGE_WORD_ALIGNED: [usize; 2] = [0, 1]; +static mut MUT_LARGE_WORD_ALIGNED: [usize; 2] = [0, 1]; + +const FN_PTR: *const () = { + fn foo() {} + unsafe { std::mem::transmute(foo as fn()) } +}; + +const ALIGNED_FN_PTR: *const () = { + #[rustc_align(2)] + fn aligned_foo() {} + unsafe { std::mem::transmute(aligned_foo as fn()) } +}; + +// Only on armv5te-* and armv4t-* +#[cfg(all( + target_arch = "arm", + not(target_feature = "v6"), +))] +const ALIGNED_THUMB_FN_PTR: *const () = { + #[rustc_align(2)] + #[instruction_set(arm::t32)] + fn aligned_thumb_foo() {} + unsafe { std::mem::transmute(aligned_thumb_foo as fn()) } +}; + +trait Trait { + #[allow(unused)] + fn method(&self) -> u8; +} +impl Trait for u32 { + fn method(&self) -> u8 { 1 } +} +impl Trait for i32 { + fn method(&self) -> u8 { 2 } +} + +const VTABLE_PTR_1: *const () = { + let [_data, vtable] = unsafe { + std::mem::transmute::<&dyn Trait, [*const (); 2]>(&42_u32 as &dyn Trait) + }; + vtable +}; +const VTABLE_PTR_2: *const () = { + let [_data, vtable] = unsafe { + std::mem::transmute::<&dyn Trait, [*const (); 2]>(&42_i32 as &dyn Trait) + }; + vtable +}; + +// Cannot be `None`: static's address, references, and `fn` pointers cannot be null, +// and `is_null` is stable with strong guarantees, and `is_null` is implemented using +// `guaranteed_cmp`. +do_test!(&A, std::ptr::null::<()>(), Some(false)); +do_test!(&ZST, std::ptr::null::<()>(), Some(false)); +do_test!(&(), std::ptr::null::<()>(), Some(false)); +do_test!(const { &() }, std::ptr::null::<()>(), Some(false)); +do_test!(FN_PTR, std::ptr::null::<()>(), Some(false)); + +// Statics cannot be duplicated +do_test!(&A, &A, Some(true)); + +// Two non-ZST statics cannot have the same address +do_test!(&A, &B, Some(false)); +do_test!(&A, &raw const MUT_STATIC, Some(false)); + +// One-past-the-end of one static can be equal to the address of another static. +do_test!(&A, (&raw const B).wrapping_add(1), None); + +// Cannot know if ZST static is at the same address with anything non-null (if alignment allows). +do_test!(&A, &ZST, None); +do_test!(&A, &ALIGNED_ZST, None); + +// Unclear if ZST statics can be placed "in the middle of" non-ZST statics. +// For now, we conservatively say they could, and return None here. +do_test!(&ZST, (&raw const A).wrapping_byte_add(1), None); + +// As per https://doc.rust-lang.org/nightly/reference/items/static-items.html#r-items.static.storage-disjointness +// immutable statics are allowed to overlap with const items and promoteds. +do_test!(&A, &T(42), None); +do_test!(&A, const { &T(42) }, None); +do_test!(&A, { const X: T = T(42); &X }, None); + +// These could return Some(false), since only immutable statics can overlap with const items +// and promoteds. +do_test!(&raw const MUT_STATIC, &T(42), None); +do_test!(&raw const MUT_STATIC, const { &T(42) }, None); +do_test!(&raw const MUT_STATIC, { const X: T = T(42); &X }, None); + +// An odd offset from a 2-aligned allocation can never be equal to an even offset from a +// 2-aligned allocation, even if the offsets are out-of-bounds. +do_test!(&A, (&raw const B).wrapping_byte_add(1), Some(false)); +do_test!(&A, (&raw const B).wrapping_byte_add(5), Some(false)); +do_test!(&A, (&raw const ALIGNED_ZST).wrapping_byte_add(1), Some(false)); +do_test!(&ALIGNED_ZST, (&raw const A).wrapping_byte_add(1), Some(false)); +do_test!(&A, (&T(42) as *const T).wrapping_byte_add(1), Some(false)); +do_test!(&A, (const { &T(42) } as *const T).wrapping_byte_add(1), Some(false)); +do_test!(&A, ({ const X: T = T(42); &X } as *const T).wrapping_byte_add(1), Some(false)); + +// Pointers into the same static are equal if and only if their offset is the same, +// even if either is out-of-bounds. +do_test!(&A, &A, Some(true)); +do_test!(&A, &A.0, Some(true)); +do_test!(&A, (&raw const A).wrapping_byte_add(1), Some(false)); +do_test!(&A, (&raw const A).wrapping_byte_add(2), Some(false)); +do_test!(&A, (&raw const A).wrapping_byte_add(51), Some(false)); +do_test!((&raw const A).wrapping_byte_add(51), (&raw const A).wrapping_byte_add(51), Some(true)); + +// Pointers to the same fn may be unequal, since `fn`s can be duplicated. +do_test!(FN_PTR, FN_PTR, None); +do_test!(ALIGNED_FN_PTR, ALIGNED_FN_PTR, None); + +// Pointers to different fns may be equal, since `fn`s can be deduplicated. +do_test!(FN_PTR, ALIGNED_FN_PTR, None); + +// Pointers to the same vtable may be unequal, since vtables can be duplicated. +do_test!(VTABLE_PTR_1, VTABLE_PTR_1, None); + +// Pointers to different vtables may be equal, since vtables can be deduplicated. +do_test!(VTABLE_PTR_1, VTABLE_PTR_2, None); + +// Function pointers to aligned function allocations are not necessarily actually aligned, +// due to platform-specific semantics. +// See https://github.com/rust-lang/rust/issues/144661 +// FIXME: This could return `Some` on platforms where function pointers' addresses actually +// correspond to function addresses including alignment, or on ARM if t32 function pointers +// have their low bit set for consteval. +do_test!(ALIGNED_FN_PTR, ALIGNED_FN_PTR.wrapping_byte_offset(1), None); +#[cfg(all( + target_arch = "arm", + not(target_feature = "v6"), +))] +do_test!(ALIGNED_THUMB_FN_PTR, ALIGNED_THUMB_FN_PTR.wrapping_byte_offset(1), None); + +// Conservatively say we don't know. +do_test!(FN_PTR, VTABLE_PTR_1, None); +do_test!((&raw const LARGE_WORD_ALIGNED).cast::().wrapping_add(1), VTABLE_PTR_1, None); +do_test!((&raw const MUT_LARGE_WORD_ALIGNED).cast::().wrapping_add(1), VTABLE_PTR_1, None); +do_test!((&raw const LARGE_WORD_ALIGNED).cast::().wrapping_add(1), FN_PTR, None); +do_test!((&raw const MUT_LARGE_WORD_ALIGNED).cast::().wrapping_add(1), FN_PTR, None); + +fn main() {} From 0947a941e2d7b6f8f35e7543fd3a010913b842bc Mon Sep 17 00:00:00 2001 From: Zachary S Date: Mon, 18 Aug 2025 23:18:09 -0500 Subject: [PATCH 2/9] Update comments with reasoning in ptr_guaranteed_cmp. `GlobalAlloc::TypeId` exists mostly to prevent consteval from comparing `TypeId`s, so always return 2 for those (whether the pointers are to the same allocation or not). In the different-allocation case, document that `GlobalAlloc::Memory`, `Function`, and `Vtable` can be deduplicated, at least with other allocations of the same kind, so those should return 2. --- .../rustc_const_eval/src/const_eval/machine.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/compiler/rustc_const_eval/src/const_eval/machine.rs b/compiler/rustc_const_eval/src/const_eval/machine.rs index a4ba04ef8347f..4a0e53e9492ea 100644 --- a/compiler/rustc_const_eval/src/const_eval/machine.rs +++ b/compiler/rustc_const_eval/src/const_eval/machine.rs @@ -322,8 +322,11 @@ impl<'tcx> CompileTimeInterpCx<'tcx> { // cannot be sure of runtime equality of pointers to the same one, (or the // runtime inequality of pointers to different ones) (see e.g. #73722). Some(GlobalAlloc::Function { .. } | GlobalAlloc::VTable(..)) => 2, - // FIXME: Can these be duplicated (or deduplicated)? - Some(GlobalAlloc::Memory(..) | GlobalAlloc::TypeId { .. }) => 2, + // FIXME: Can these can be duplicated? + Some(GlobalAlloc::Memory(..)) => 2, + // `GlobalAlloc::TypeId` exists mostly to prevent consteval from comparing + // `TypeId`s, always return 2 + Some(GlobalAlloc::TypeId { .. }) => 2, } } else { if let (Some(GlobalAlloc::Static(a_did)), Some(GlobalAlloc::Static(b_did))) = ( @@ -367,8 +370,12 @@ impl<'tcx> CompileTimeInterpCx<'tcx> { } else { // Even if one of them is a static, as per https://doc.rust-lang.org/nightly/reference/items/static-items.html#r-items.static.storage-disjointness // immutable statics can overlap with other kinds of allocations somtimes. - // FIXME: We could be more decisive for mutable statics, which cannot - // overlap with other kinds of allocations. + // FIXME: We could be more decisive for (non-zero-sized) mutable statics, + // which cannot overlap with other kinds of allocations. + // `GlobalAlloc::{Memory, Function, Vtable}` can at least be deduplicated with + // the same kind, so comparing two of the same kind of those should return 2. + // `GlobalAlloc::TypeId` exists mostly to prevent consteval from comparing + // `TypeId`s, so comparing two of those should always return 2. // FIXME: Can we determine any other cases? 2 } From 252f0e008e654006f8bb4fc259f1978bd8110a57 Mon Sep 17 00:00:00 2001 From: Zachary S Date: Mon, 18 Aug 2025 23:22:36 -0500 Subject: [PATCH 3/9] Update tests/ui/const-ptr/guaranteed_cmp.rs to prepare to combine it with tests/ui/consts/ptr_comparisons.rs --- tests/ui/const-ptr/guaranteed_cmp.rs | 35 +++++++++++++++++++--------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/tests/ui/const-ptr/guaranteed_cmp.rs b/tests/ui/const-ptr/guaranteed_cmp.rs index 645ca35598d1d..86fed1336821f 100644 --- a/tests/ui/const-ptr/guaranteed_cmp.rs +++ b/tests/ui/const-ptr/guaranteed_cmp.rs @@ -1,4 +1,5 @@ -//@ build-pass +//@ compile-flags: --crate-type=lib +//@ check-pass //@ edition: 2024 #![feature(const_raw_ptr_comparison)] #![feature(fn_align)] @@ -77,14 +78,28 @@ const VTABLE_PTR_2: *const () = { vtable }; -// Cannot be `None`: static's address, references, and `fn` pointers cannot be null, -// and `is_null` is stable with strong guarantees, and `is_null` is implemented using -// `guaranteed_cmp`. -do_test!(&A, std::ptr::null::<()>(), Some(false)); -do_test!(&ZST, std::ptr::null::<()>(), Some(false)); -do_test!(&(), std::ptr::null::<()>(), Some(false)); -do_test!(const { &() }, std::ptr::null::<()>(), Some(false)); -do_test!(FN_PTR, std::ptr::null::<()>(), Some(false)); +// Cannot be `None`: `is_null` is stable with strong guarantees about integer-valued pointers. +do_test!(0 as *const u8, 0 as *const u8, Some(true)); +do_test!(0 as *const u8, 1 as *const u8, Some(false)); + +// Cannot be `None`: `static`s' addresses, references, (and within and one-past-the-end of those), +// and `fn` pointers cannot be null, and `is_null` is stable with strong guarantees, and +// `is_null` is implemented using `guaranteed_cmp`. +do_test!(&A, 0 as *const u8, Some(false)); +do_test!((&raw const A).cast::().wrapping_add(1), 0 as *const u8, Some(false)); +do_test!((&raw const A).wrapping_add(1), 0 as *const u8, Some(false)); +do_test!(&ZST, 0 as *const u8, Some(false)); +do_test!(&(), 0 as *const u8, Some(false)); +do_test!(const { &() }, 0 as *const u8, Some(false)); +do_test!(FN_PTR, 0 as *const u8, Some(false)); + +// aside from 0, these pointers might end up pretty much anywhere. +do_test!(&A, align_of::() as *const u8, None); +do_test!(&A, 1 as *const u8, Some(false)); // this one takes into account alignment, so we know that + +// When pointers go out-of-bounds, they *might* become null, so these comparions cannot work. +do_test!((&raw const A).wrapping_add(2), 0 as *const u8, None); +do_test!((&raw const A).wrapping_sub(1), 0 as *const u8, None); // Statics cannot be duplicated do_test!(&A, &A, Some(true)); @@ -167,5 +182,3 @@ do_test!((&raw const LARGE_WORD_ALIGNED).cast::().wrapping_add(1), VTABLE do_test!((&raw const MUT_LARGE_WORD_ALIGNED).cast::().wrapping_add(1), VTABLE_PTR_1, None); do_test!((&raw const LARGE_WORD_ALIGNED).cast::().wrapping_add(1), FN_PTR, None); do_test!((&raw const MUT_LARGE_WORD_ALIGNED).cast::().wrapping_add(1), FN_PTR, None); - -fn main() {} From d61c58a41db103faafcb95c0e99653684237f359 Mon Sep 17 00:00:00 2001 From: Zachary S Date: Mon, 18 Aug 2025 23:45:54 -0500 Subject: [PATCH 4/9] Combine tests/ui/const-ptr/guaranteed_cmp.rs into tests/ui/consts/ptr_comparisons.rs --- tests/ui/const-ptr/guaranteed_cmp.rs | 184 ------------------------ tests/ui/consts/ptr_comparisons.rs | 201 +++++++++++++++++++++++---- 2 files changed, 171 insertions(+), 214 deletions(-) delete mode 100644 tests/ui/const-ptr/guaranteed_cmp.rs diff --git a/tests/ui/const-ptr/guaranteed_cmp.rs b/tests/ui/const-ptr/guaranteed_cmp.rs deleted file mode 100644 index 86fed1336821f..0000000000000 --- a/tests/ui/const-ptr/guaranteed_cmp.rs +++ /dev/null @@ -1,184 +0,0 @@ -//@ compile-flags: --crate-type=lib -//@ check-pass -//@ edition: 2024 -#![feature(const_raw_ptr_comparison)] -#![feature(fn_align)] -// Generally: -// For any `Some` return, `None` would also be valid, unless otherwise noted. -// For any `None` return, only `None` is valid, unless otherwise noted. - -macro_rules! do_test { - ($a:expr, $b:expr, $expected:pat) => { - const _: () = { - let a: *const _ = $a; - let b: *const _ = $b; - assert!(matches!(<*const u8>::guaranteed_eq(a.cast(), b.cast()), $expected)); - }; - }; -} - -#[repr(align(2))] -struct T(#[allow(unused)] u16); - -#[repr(align(2))] -struct AlignedZst; - -static A: T = T(42); -static B: T = T(42); -static mut MUT_STATIC: T = T(42); -static ZST: () = (); -static ALIGNED_ZST: AlignedZst = AlignedZst; -static LARGE_WORD_ALIGNED: [usize; 2] = [0, 1]; -static mut MUT_LARGE_WORD_ALIGNED: [usize; 2] = [0, 1]; - -const FN_PTR: *const () = { - fn foo() {} - unsafe { std::mem::transmute(foo as fn()) } -}; - -const ALIGNED_FN_PTR: *const () = { - #[rustc_align(2)] - fn aligned_foo() {} - unsafe { std::mem::transmute(aligned_foo as fn()) } -}; - -// Only on armv5te-* and armv4t-* -#[cfg(all( - target_arch = "arm", - not(target_feature = "v6"), -))] -const ALIGNED_THUMB_FN_PTR: *const () = { - #[rustc_align(2)] - #[instruction_set(arm::t32)] - fn aligned_thumb_foo() {} - unsafe { std::mem::transmute(aligned_thumb_foo as fn()) } -}; - -trait Trait { - #[allow(unused)] - fn method(&self) -> u8; -} -impl Trait for u32 { - fn method(&self) -> u8 { 1 } -} -impl Trait for i32 { - fn method(&self) -> u8 { 2 } -} - -const VTABLE_PTR_1: *const () = { - let [_data, vtable] = unsafe { - std::mem::transmute::<&dyn Trait, [*const (); 2]>(&42_u32 as &dyn Trait) - }; - vtable -}; -const VTABLE_PTR_2: *const () = { - let [_data, vtable] = unsafe { - std::mem::transmute::<&dyn Trait, [*const (); 2]>(&42_i32 as &dyn Trait) - }; - vtable -}; - -// Cannot be `None`: `is_null` is stable with strong guarantees about integer-valued pointers. -do_test!(0 as *const u8, 0 as *const u8, Some(true)); -do_test!(0 as *const u8, 1 as *const u8, Some(false)); - -// Cannot be `None`: `static`s' addresses, references, (and within and one-past-the-end of those), -// and `fn` pointers cannot be null, and `is_null` is stable with strong guarantees, and -// `is_null` is implemented using `guaranteed_cmp`. -do_test!(&A, 0 as *const u8, Some(false)); -do_test!((&raw const A).cast::().wrapping_add(1), 0 as *const u8, Some(false)); -do_test!((&raw const A).wrapping_add(1), 0 as *const u8, Some(false)); -do_test!(&ZST, 0 as *const u8, Some(false)); -do_test!(&(), 0 as *const u8, Some(false)); -do_test!(const { &() }, 0 as *const u8, Some(false)); -do_test!(FN_PTR, 0 as *const u8, Some(false)); - -// aside from 0, these pointers might end up pretty much anywhere. -do_test!(&A, align_of::() as *const u8, None); -do_test!(&A, 1 as *const u8, Some(false)); // this one takes into account alignment, so we know that - -// When pointers go out-of-bounds, they *might* become null, so these comparions cannot work. -do_test!((&raw const A).wrapping_add(2), 0 as *const u8, None); -do_test!((&raw const A).wrapping_sub(1), 0 as *const u8, None); - -// Statics cannot be duplicated -do_test!(&A, &A, Some(true)); - -// Two non-ZST statics cannot have the same address -do_test!(&A, &B, Some(false)); -do_test!(&A, &raw const MUT_STATIC, Some(false)); - -// One-past-the-end of one static can be equal to the address of another static. -do_test!(&A, (&raw const B).wrapping_add(1), None); - -// Cannot know if ZST static is at the same address with anything non-null (if alignment allows). -do_test!(&A, &ZST, None); -do_test!(&A, &ALIGNED_ZST, None); - -// Unclear if ZST statics can be placed "in the middle of" non-ZST statics. -// For now, we conservatively say they could, and return None here. -do_test!(&ZST, (&raw const A).wrapping_byte_add(1), None); - -// As per https://doc.rust-lang.org/nightly/reference/items/static-items.html#r-items.static.storage-disjointness -// immutable statics are allowed to overlap with const items and promoteds. -do_test!(&A, &T(42), None); -do_test!(&A, const { &T(42) }, None); -do_test!(&A, { const X: T = T(42); &X }, None); - -// These could return Some(false), since only immutable statics can overlap with const items -// and promoteds. -do_test!(&raw const MUT_STATIC, &T(42), None); -do_test!(&raw const MUT_STATIC, const { &T(42) }, None); -do_test!(&raw const MUT_STATIC, { const X: T = T(42); &X }, None); - -// An odd offset from a 2-aligned allocation can never be equal to an even offset from a -// 2-aligned allocation, even if the offsets are out-of-bounds. -do_test!(&A, (&raw const B).wrapping_byte_add(1), Some(false)); -do_test!(&A, (&raw const B).wrapping_byte_add(5), Some(false)); -do_test!(&A, (&raw const ALIGNED_ZST).wrapping_byte_add(1), Some(false)); -do_test!(&ALIGNED_ZST, (&raw const A).wrapping_byte_add(1), Some(false)); -do_test!(&A, (&T(42) as *const T).wrapping_byte_add(1), Some(false)); -do_test!(&A, (const { &T(42) } as *const T).wrapping_byte_add(1), Some(false)); -do_test!(&A, ({ const X: T = T(42); &X } as *const T).wrapping_byte_add(1), Some(false)); - -// Pointers into the same static are equal if and only if their offset is the same, -// even if either is out-of-bounds. -do_test!(&A, &A, Some(true)); -do_test!(&A, &A.0, Some(true)); -do_test!(&A, (&raw const A).wrapping_byte_add(1), Some(false)); -do_test!(&A, (&raw const A).wrapping_byte_add(2), Some(false)); -do_test!(&A, (&raw const A).wrapping_byte_add(51), Some(false)); -do_test!((&raw const A).wrapping_byte_add(51), (&raw const A).wrapping_byte_add(51), Some(true)); - -// Pointers to the same fn may be unequal, since `fn`s can be duplicated. -do_test!(FN_PTR, FN_PTR, None); -do_test!(ALIGNED_FN_PTR, ALIGNED_FN_PTR, None); - -// Pointers to different fns may be equal, since `fn`s can be deduplicated. -do_test!(FN_PTR, ALIGNED_FN_PTR, None); - -// Pointers to the same vtable may be unequal, since vtables can be duplicated. -do_test!(VTABLE_PTR_1, VTABLE_PTR_1, None); - -// Pointers to different vtables may be equal, since vtables can be deduplicated. -do_test!(VTABLE_PTR_1, VTABLE_PTR_2, None); - -// Function pointers to aligned function allocations are not necessarily actually aligned, -// due to platform-specific semantics. -// See https://github.com/rust-lang/rust/issues/144661 -// FIXME: This could return `Some` on platforms where function pointers' addresses actually -// correspond to function addresses including alignment, or on ARM if t32 function pointers -// have their low bit set for consteval. -do_test!(ALIGNED_FN_PTR, ALIGNED_FN_PTR.wrapping_byte_offset(1), None); -#[cfg(all( - target_arch = "arm", - not(target_feature = "v6"), -))] -do_test!(ALIGNED_THUMB_FN_PTR, ALIGNED_THUMB_FN_PTR.wrapping_byte_offset(1), None); - -// Conservatively say we don't know. -do_test!(FN_PTR, VTABLE_PTR_1, None); -do_test!((&raw const LARGE_WORD_ALIGNED).cast::().wrapping_add(1), VTABLE_PTR_1, None); -do_test!((&raw const MUT_LARGE_WORD_ALIGNED).cast::().wrapping_add(1), VTABLE_PTR_1, None); -do_test!((&raw const LARGE_WORD_ALIGNED).cast::().wrapping_add(1), FN_PTR, None); -do_test!((&raw const MUT_LARGE_WORD_ALIGNED).cast::().wrapping_add(1), FN_PTR, None); diff --git a/tests/ui/consts/ptr_comparisons.rs b/tests/ui/consts/ptr_comparisons.rs index e142ab3a754a4..86fed1336821f 100644 --- a/tests/ui/consts/ptr_comparisons.rs +++ b/tests/ui/consts/ptr_comparisons.rs @@ -1,43 +1,184 @@ //@ compile-flags: --crate-type=lib //@ check-pass +//@ edition: 2024 +#![feature(const_raw_ptr_comparison)] +#![feature(fn_align)] +// Generally: +// For any `Some` return, `None` would also be valid, unless otherwise noted. +// For any `None` return, only `None` is valid, unless otherwise noted. -#![feature( - core_intrinsics, - const_raw_ptr_comparison, -)] +macro_rules! do_test { + ($a:expr, $b:expr, $expected:pat) => { + const _: () = { + let a: *const _ = $a; + let b: *const _ = $b; + assert!(matches!(<*const u8>::guaranteed_eq(a.cast(), b.cast()), $expected)); + }; + }; +} -const FOO: &usize = &42; +#[repr(align(2))] +struct T(#[allow(unused)] u16); -macro_rules! check { - (eq, $a:expr, $b:expr) => { - pub const _: () = - assert!(std::intrinsics::ptr_guaranteed_cmp($a as *const u8, $b as *const u8) == 1); - }; - (ne, $a:expr, $b:expr) => { - pub const _: () = - assert!(std::intrinsics::ptr_guaranteed_cmp($a as *const u8, $b as *const u8) == 0); +#[repr(align(2))] +struct AlignedZst; + +static A: T = T(42); +static B: T = T(42); +static mut MUT_STATIC: T = T(42); +static ZST: () = (); +static ALIGNED_ZST: AlignedZst = AlignedZst; +static LARGE_WORD_ALIGNED: [usize; 2] = [0, 1]; +static mut MUT_LARGE_WORD_ALIGNED: [usize; 2] = [0, 1]; + +const FN_PTR: *const () = { + fn foo() {} + unsafe { std::mem::transmute(foo as fn()) } +}; + +const ALIGNED_FN_PTR: *const () = { + #[rustc_align(2)] + fn aligned_foo() {} + unsafe { std::mem::transmute(aligned_foo as fn()) } +}; + +// Only on armv5te-* and armv4t-* +#[cfg(all( + target_arch = "arm", + not(target_feature = "v6"), +))] +const ALIGNED_THUMB_FN_PTR: *const () = { + #[rustc_align(2)] + #[instruction_set(arm::t32)] + fn aligned_thumb_foo() {} + unsafe { std::mem::transmute(aligned_thumb_foo as fn()) } +}; + +trait Trait { + #[allow(unused)] + fn method(&self) -> u8; +} +impl Trait for u32 { + fn method(&self) -> u8 { 1 } +} +impl Trait for i32 { + fn method(&self) -> u8 { 2 } +} + +const VTABLE_PTR_1: *const () = { + let [_data, vtable] = unsafe { + std::mem::transmute::<&dyn Trait, [*const (); 2]>(&42_u32 as &dyn Trait) }; - (!, $a:expr, $b:expr) => { - pub const _: () = - assert!(std::intrinsics::ptr_guaranteed_cmp($a as *const u8, $b as *const u8) == 2); + vtable +}; +const VTABLE_PTR_2: *const () = { + let [_data, vtable] = unsafe { + std::mem::transmute::<&dyn Trait, [*const (); 2]>(&42_i32 as &dyn Trait) }; -} + vtable +}; -check!(eq, 0, 0); -check!(ne, 0, 1); -check!(ne, FOO as *const _, 0); -check!(ne, unsafe { (FOO as *const usize).offset(1) }, 0); -check!(ne, unsafe { (FOO as *const usize as *const u8).offset(3) }, 0); +// Cannot be `None`: `is_null` is stable with strong guarantees about integer-valued pointers. +do_test!(0 as *const u8, 0 as *const u8, Some(true)); +do_test!(0 as *const u8, 1 as *const u8, Some(false)); -// We want pointers to be equal to themselves, but aren't checking this yet because -// there are some open questions (e.g. whether function pointers to the same function -// compare equal: they don't necessarily do at runtime). -check!(!, FOO as *const _, FOO as *const _); +// Cannot be `None`: `static`s' addresses, references, (and within and one-past-the-end of those), +// and `fn` pointers cannot be null, and `is_null` is stable with strong guarantees, and +// `is_null` is implemented using `guaranteed_cmp`. +do_test!(&A, 0 as *const u8, Some(false)); +do_test!((&raw const A).cast::().wrapping_add(1), 0 as *const u8, Some(false)); +do_test!((&raw const A).wrapping_add(1), 0 as *const u8, Some(false)); +do_test!(&ZST, 0 as *const u8, Some(false)); +do_test!(&(), 0 as *const u8, Some(false)); +do_test!(const { &() }, 0 as *const u8, Some(false)); +do_test!(FN_PTR, 0 as *const u8, Some(false)); // aside from 0, these pointers might end up pretty much anywhere. -check!(!, FOO as *const _, 1); // this one could be `ne` by taking into account alignment -check!(!, FOO as *const _, 1024); +do_test!(&A, align_of::() as *const u8, None); +do_test!(&A, 1 as *const u8, Some(false)); // this one takes into account alignment, so we know that // When pointers go out-of-bounds, they *might* become null, so these comparions cannot work. -check!(!, unsafe { (FOO as *const usize).wrapping_add(2) }, 0); -check!(!, unsafe { (FOO as *const usize).wrapping_sub(1) }, 0); +do_test!((&raw const A).wrapping_add(2), 0 as *const u8, None); +do_test!((&raw const A).wrapping_sub(1), 0 as *const u8, None); + +// Statics cannot be duplicated +do_test!(&A, &A, Some(true)); + +// Two non-ZST statics cannot have the same address +do_test!(&A, &B, Some(false)); +do_test!(&A, &raw const MUT_STATIC, Some(false)); + +// One-past-the-end of one static can be equal to the address of another static. +do_test!(&A, (&raw const B).wrapping_add(1), None); + +// Cannot know if ZST static is at the same address with anything non-null (if alignment allows). +do_test!(&A, &ZST, None); +do_test!(&A, &ALIGNED_ZST, None); + +// Unclear if ZST statics can be placed "in the middle of" non-ZST statics. +// For now, we conservatively say they could, and return None here. +do_test!(&ZST, (&raw const A).wrapping_byte_add(1), None); + +// As per https://doc.rust-lang.org/nightly/reference/items/static-items.html#r-items.static.storage-disjointness +// immutable statics are allowed to overlap with const items and promoteds. +do_test!(&A, &T(42), None); +do_test!(&A, const { &T(42) }, None); +do_test!(&A, { const X: T = T(42); &X }, None); + +// These could return Some(false), since only immutable statics can overlap with const items +// and promoteds. +do_test!(&raw const MUT_STATIC, &T(42), None); +do_test!(&raw const MUT_STATIC, const { &T(42) }, None); +do_test!(&raw const MUT_STATIC, { const X: T = T(42); &X }, None); + +// An odd offset from a 2-aligned allocation can never be equal to an even offset from a +// 2-aligned allocation, even if the offsets are out-of-bounds. +do_test!(&A, (&raw const B).wrapping_byte_add(1), Some(false)); +do_test!(&A, (&raw const B).wrapping_byte_add(5), Some(false)); +do_test!(&A, (&raw const ALIGNED_ZST).wrapping_byte_add(1), Some(false)); +do_test!(&ALIGNED_ZST, (&raw const A).wrapping_byte_add(1), Some(false)); +do_test!(&A, (&T(42) as *const T).wrapping_byte_add(1), Some(false)); +do_test!(&A, (const { &T(42) } as *const T).wrapping_byte_add(1), Some(false)); +do_test!(&A, ({ const X: T = T(42); &X } as *const T).wrapping_byte_add(1), Some(false)); + +// Pointers into the same static are equal if and only if their offset is the same, +// even if either is out-of-bounds. +do_test!(&A, &A, Some(true)); +do_test!(&A, &A.0, Some(true)); +do_test!(&A, (&raw const A).wrapping_byte_add(1), Some(false)); +do_test!(&A, (&raw const A).wrapping_byte_add(2), Some(false)); +do_test!(&A, (&raw const A).wrapping_byte_add(51), Some(false)); +do_test!((&raw const A).wrapping_byte_add(51), (&raw const A).wrapping_byte_add(51), Some(true)); + +// Pointers to the same fn may be unequal, since `fn`s can be duplicated. +do_test!(FN_PTR, FN_PTR, None); +do_test!(ALIGNED_FN_PTR, ALIGNED_FN_PTR, None); + +// Pointers to different fns may be equal, since `fn`s can be deduplicated. +do_test!(FN_PTR, ALIGNED_FN_PTR, None); + +// Pointers to the same vtable may be unequal, since vtables can be duplicated. +do_test!(VTABLE_PTR_1, VTABLE_PTR_1, None); + +// Pointers to different vtables may be equal, since vtables can be deduplicated. +do_test!(VTABLE_PTR_1, VTABLE_PTR_2, None); + +// Function pointers to aligned function allocations are not necessarily actually aligned, +// due to platform-specific semantics. +// See https://github.com/rust-lang/rust/issues/144661 +// FIXME: This could return `Some` on platforms where function pointers' addresses actually +// correspond to function addresses including alignment, or on ARM if t32 function pointers +// have their low bit set for consteval. +do_test!(ALIGNED_FN_PTR, ALIGNED_FN_PTR.wrapping_byte_offset(1), None); +#[cfg(all( + target_arch = "arm", + not(target_feature = "v6"), +))] +do_test!(ALIGNED_THUMB_FN_PTR, ALIGNED_THUMB_FN_PTR.wrapping_byte_offset(1), None); + +// Conservatively say we don't know. +do_test!(FN_PTR, VTABLE_PTR_1, None); +do_test!((&raw const LARGE_WORD_ALIGNED).cast::().wrapping_add(1), VTABLE_PTR_1, None); +do_test!((&raw const MUT_LARGE_WORD_ALIGNED).cast::().wrapping_add(1), VTABLE_PTR_1, None); +do_test!((&raw const LARGE_WORD_ALIGNED).cast::().wrapping_add(1), FN_PTR, None); +do_test!((&raw const MUT_LARGE_WORD_ALIGNED).cast::().wrapping_add(1), FN_PTR, None); From 156113b29b157811151f1111138c7d2fde873953 Mon Sep 17 00:00:00 2001 From: Zachary S Date: Mon, 18 Aug 2025 23:49:27 -0500 Subject: [PATCH 5/9] Implement another check in `ptr_guaranteed_cmp` in consteval: When comparing a pointer with an integer(-valued pointer), if the integer and the pointer have a different residue modulo the pointer's allocation's alignment, they can never be equal. Else, we can't know. --- .../src/const_eval/machine.rs | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/compiler/rustc_const_eval/src/const_eval/machine.rs b/compiler/rustc_const_eval/src/const_eval/machine.rs index 4a0e53e9492ea..6c87301405ff9 100644 --- a/compiler/rustc_const_eval/src/const_eval/machine.rs +++ b/compiler/rustc_const_eval/src/const_eval/machine.rs @@ -287,8 +287,33 @@ impl<'tcx> CompileTimeInterpCx<'tcx> { { 0 } - // Other ways of comparing integers and pointers can never be known for sure. - (Scalar::Int { .. }, Scalar::Ptr(..)) | (Scalar::Ptr(..), Scalar::Int { .. }) => 2, + // Other ways of comparing integers and pointers can never be known for sure, + // except for alignment, e.g. `1 as *const _` can never be equal to an even offset + // in an `align(2)` allocation. + (Scalar::Int(int), Scalar::Ptr(ptr, _)) | (Scalar::Ptr(ptr, _), Scalar::Int(int)) => { + let int = int.to_target_usize(*self.tcx); + let (ptr_prov, ptr_offset) = ptr.prov_and_relative_offset(); + let allocid = ptr_prov.alloc_id(); + let allocinfo = self.get_alloc_info(allocid); + + // Check if the pointer cannot be equal to the integer due to alignment. + // For this purpose, an integer can be thought of as an offset into a + // maximally-aligned "allocation" (the whole address space), + // so the least common alignment is the alignment of the pointer's allocation. + let min_align = allocinfo.align.bytes(); + let ptr_residue = ptr_offset.bytes() % min_align; + let int_residue = int % min_align; + if ptr_residue != int_residue { + // The pointer and integer have a different residue modulo their common + // alignment, they can never be equal. + 0 + } else { + // The pointer and integer have the same residue modulo their common alignment, + // so the pointer could end up equal to the integer at runtime; + // we can't know for sure. + 2 + } + } (Scalar::Ptr(a, _), Scalar::Ptr(b, _)) => { let (a_prov, a_offset) = a.prov_and_relative_offset(); let (b_prov, b_offset) = b.prov_and_relative_offset(); @@ -303,7 +328,7 @@ impl<'tcx> CompileTimeInterpCx<'tcx> { let a_residue = a_offset.bytes() % min_align; let b_residue = b_offset.bytes() % min_align; if a_residue != b_residue { - // If the two pointers have a different residue from their + // If the two pointers have a different residue modulo their // common alignment, they cannot be equal. return interp_ok(0); } From 2204d61213847c1217a93d4babb4b5a981b07fd5 Mon Sep 17 00:00:00 2001 From: zachs18 <8355914+zachs18@users.noreply.github.com> Date: Tue, 19 Aug 2025 01:05:14 -0500 Subject: [PATCH 6/9] Update compiler/rustc_const_eval/src/const_eval/machine.rs Co-authored-by: Ralf Jung --- compiler/rustc_const_eval/src/const_eval/machine.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/rustc_const_eval/src/const_eval/machine.rs b/compiler/rustc_const_eval/src/const_eval/machine.rs index 6c87301405ff9..e991a410f74d0 100644 --- a/compiler/rustc_const_eval/src/const_eval/machine.rs +++ b/compiler/rustc_const_eval/src/const_eval/machine.rs @@ -368,8 +368,8 @@ impl<'tcx> CompileTimeInterpCx<'tcx> { // pointers are (in)equal. // FIXME: Can zero-sized static be "within" non-zero-sized statics? // Conservatively we say yes, since that doesn't cause them to - // "overlap" any bytes, but if not, then we could delete this branch - // and have the other branches handle ZST allocations. + // "overlap" any bytes, but if not, then we could delete this branch; + // the other branches would already handle ZST allocations correctly. 2 } else if a_offset > a_info.size || b_offset > b_info.size { // One or both pointers are out of bounds of their allocation, From 4968dfae0be0ebe4af56bbe0fd86a957daf2a81c Mon Sep 17 00:00:00 2001 From: Zachary S Date: Tue, 19 Aug 2025 01:09:15 -0500 Subject: [PATCH 7/9] Add reference links for non-deduplication and non-overlapping of non-zero-sized statics --- compiler/rustc_const_eval/src/const_eval/machine.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/compiler/rustc_const_eval/src/const_eval/machine.rs b/compiler/rustc_const_eval/src/const_eval/machine.rs index e991a410f74d0..0a6b538a5be47 100644 --- a/compiler/rustc_const_eval/src/const_eval/machine.rs +++ b/compiler/rustc_const_eval/src/const_eval/machine.rs @@ -388,8 +388,12 @@ impl<'tcx> CompileTimeInterpCx<'tcx> { } else { // The pointers are within (or one past the end of) different // non-zero-sized static allocations, and they are not at oppotiste - // ends, so we know they are not equal because statics cannot - // overlap or be deduplicated. + // ends, so we know they are not equal because non-zero-sized statics + // cannot overlap or be deduplicated, as per + // https://doc.rust-lang.org/nightly/reference/items/static-items.html#r-items.static.intro + // (non-deduplication), and + // https://doc.rust-lang.org/nightly/reference/items/static-items.html#r-items.static.storage-disjointness + // (non-overlapping) 0 } } else { From 580074a4961272f36b2425e1793689b93a751a1f Mon Sep 17 00:00:00 2001 From: Zachary S Date: Wed, 20 Aug 2025 14:40:03 -0500 Subject: [PATCH 8/9] Add issue link to same-alloc GlobalAlloc::Memory FIXME --- compiler/rustc_const_eval/src/const_eval/machine.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/rustc_const_eval/src/const_eval/machine.rs b/compiler/rustc_const_eval/src/const_eval/machine.rs index 0a6b538a5be47..919b5de56ab58 100644 --- a/compiler/rustc_const_eval/src/const_eval/machine.rs +++ b/compiler/rustc_const_eval/src/const_eval/machine.rs @@ -347,7 +347,8 @@ impl<'tcx> CompileTimeInterpCx<'tcx> { // cannot be sure of runtime equality of pointers to the same one, (or the // runtime inequality of pointers to different ones) (see e.g. #73722). Some(GlobalAlloc::Function { .. } | GlobalAlloc::VTable(..)) => 2, - // FIXME: Can these can be duplicated? + // FIXME: Revisit this once https://github.com/rust-lang/rust/issues/128775 + // is fixed. Some(GlobalAlloc::Memory(..)) => 2, // `GlobalAlloc::TypeId` exists mostly to prevent consteval from comparing // `TypeId`s, always return 2 From 59948c6131adb9d4b9382fc81744c537f32eb62e Mon Sep 17 00:00:00 2001 From: Zachary S Date: Wed, 20 Aug 2025 15:06:50 -0500 Subject: [PATCH 9/9] tidy typo --- compiler/rustc_const_eval/src/const_eval/machine.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/rustc_const_eval/src/const_eval/machine.rs b/compiler/rustc_const_eval/src/const_eval/machine.rs index 919b5de56ab58..a8d25a6c096bc 100644 --- a/compiler/rustc_const_eval/src/const_eval/machine.rs +++ b/compiler/rustc_const_eval/src/const_eval/machine.rs @@ -399,7 +399,7 @@ impl<'tcx> CompileTimeInterpCx<'tcx> { } } else { // Even if one of them is a static, as per https://doc.rust-lang.org/nightly/reference/items/static-items.html#r-items.static.storage-disjointness - // immutable statics can overlap with other kinds of allocations somtimes. + // immutable statics can overlap with other kinds of allocations sometimes. // FIXME: We could be more decisive for (non-zero-sized) mutable statics, // which cannot overlap with other kinds of allocations. // `GlobalAlloc::{Memory, Function, Vtable}` can at least be deduplicated with