From 1e19b0b57bdff354a4f399acc9ea38f11382898e Mon Sep 17 00:00:00 2001 From: Scott McMurray Date: Fri, 17 Jul 2026 23:33:10 -0700 Subject: [PATCH 01/31] rustc_abi: move WrappingRange to its own file --- compiler/rustc_abi/src/lib.rs | 150 +--------------------- compiler/rustc_abi/src/wrapping_range.rs | 152 +++++++++++++++++++++++ 2 files changed, 155 insertions(+), 147 deletions(-) create mode 100644 compiler/rustc_abi/src/wrapping_range.rs diff --git a/compiler/rustc_abi/src/lib.rs b/compiler/rustc_abi/src/lib.rs index bff4c9bdf47ef..cd9522f7c2f62 100644 --- a/compiler/rustc_abi/src/lib.rs +++ b/compiler/rustc_abi/src/lib.rs @@ -41,7 +41,7 @@ use std::fmt; #[cfg(feature = "nightly")] use std::iter::Step; use std::num::{NonZeroUsize, ParseIntError}; -use std::ops::{Add, AddAssign, Deref, Mul, RangeFull, Sub}; +use std::ops::{Add, AddAssign, Deref, Mul, Sub}; use std::range::RangeInclusive; use std::str::FromStr; @@ -65,6 +65,7 @@ mod extern_abi; mod layout; #[cfg(test)] mod tests; +mod wrapping_range; pub use callconv::{Heterogeneous, HomogeneousAggregate, Reg, RegKind}; pub use canon_abi::{ArmCall, CanonAbi, InterruptKind, X86Call}; @@ -74,6 +75,7 @@ pub use extern_abi::{ExternAbi, all_names}; pub use layout::{FIRST_VARIANT, FieldIdx, LayoutCalculator, LayoutCalculatorError, VariantIdx}; #[cfg(feature = "nightly")] pub use layout::{Layout, TyAbiInterface, TyAndLayout}; +pub use wrapping_range::WrappingRange; #[derive(Clone, Copy, PartialEq, Eq, Default)] #[cfg_attr(feature = "nightly", derive(Encodable_NoContext, Decodable_NoContext, StableHash))] @@ -1471,152 +1473,6 @@ impl Primitive { } } -/// Inclusive wrap-around range of valid values, that is, if -/// start > end, it represents `start..=MAX`, followed by `0..=end`. -/// -/// That is, for an i8 primitive, a range of `254..=2` means following -/// sequence: -/// -/// 254 (-2), 255 (-1), 0, 1, 2 -/// -/// This is intended specifically to mirror LLVM’s `!range` metadata semantics. -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "nightly", derive(StableHash))] -pub struct WrappingRange { - pub start: u128, - pub end: u128, -} - -impl WrappingRange { - fn debug_as(&self, size: Size, is_signed: bool) -> impl fmt::Debug { - let range = *self; - fmt::from_fn(move |f| { - if range == WrappingRange::full(size) { - // This is intentionally not using `is_full_for` so that we ensure - // different values always debug-print differently. - // We don't need the full details when it's the canonical full range, - // but if one is looking at the debug output it might be that seeing - // `u8 is (..=0) | (1..)` instead of `u8 is ..` is the information - // you needed because the problem is that despite being *a* full - // range it's not *the* canonical one you expected it was. - f.write_str("..") - } else if is_signed { - let start = size.sign_extend(range.start); - let end = size.sign_extend(range.end); - if start > end { - write!(f, "(..={}) | ({}..)", end, start) - } else { - write!(f, "{}..={}", start, end) - } - } else { - write!(f, "{:?}", range) - } - }) - } - - pub fn full(size: Size) -> Self { - Self { start: 0, end: size.unsigned_int_max() } - } - - /// Returns `true` if `v` is contained in the range. - #[inline(always)] - pub fn contains(&self, v: u128) -> bool { - if self.start <= self.end { - self.start <= v && v <= self.end - } else { - self.start <= v || v <= self.end - } - } - - /// Returns `true` if all the values in `other` are contained in this range, - /// when the values are considered as having width `size`. - #[inline(always)] - pub fn contains_range(&self, other: Self, size: Size) -> bool { - if self.is_full_for(size) { - true - } else { - let trunc = |x| size.truncate(x); - - let delta = self.start; - let max = trunc(self.end.wrapping_sub(delta)); - - let other_start = trunc(other.start.wrapping_sub(delta)); - let other_end = trunc(other.end.wrapping_sub(delta)); - - // Having shifted both input ranges by `delta`, now we only need to check - // whether `0..=max` contains `other_start..=other_end`, which can only - // happen if the other doesn't wrap since `self` isn't everything. - (other_start <= other_end) && (other_end <= max) - } - } - - /// Returns `self` with replaced `start` - #[inline(always)] - fn with_start(mut self, start: u128) -> Self { - self.start = start; - self - } - - /// Returns `self` with replaced `end` - #[inline(always)] - fn with_end(mut self, end: u128) -> Self { - self.end = end; - self - } - - /// Returns `true` if `size` completely fills the range. - /// - /// Note that this is *not* the same as `self == WrappingRange::full(size)`. - /// Niche calculations can produce full ranges which are not the canonical one; - /// for example `Option>` gets `valid_range: (..=0) | (1..)`. - #[inline] - fn is_full_for(&self, size: Size) -> bool { - let max_value = size.unsigned_int_max(); - debug_assert!(self.start <= max_value && self.end <= max_value); - self.start == (self.end.wrapping_add(1) & max_value) - } - - /// Checks whether this range is considered non-wrapping when the values are - /// interpreted as *unsigned* numbers of width `size`. - /// - /// Returns `Ok(true)` if there's no wrap-around, `Ok(false)` if there is, - /// and `Err(..)` if the range is full so it depends how you think about it. - #[inline] - pub fn no_unsigned_wraparound(&self, size: Size) -> Result { - if self.is_full_for(size) { Err(..) } else { Ok(self.start <= self.end) } - } - - /// Checks whether this range is considered non-wrapping when the values are - /// interpreted as *signed* numbers of width `size`. - /// - /// This is heavily dependent on the `size`, as `100..=200` does wrap when - /// interpreted as `i8`, but doesn't when interpreted as `i16`. - /// - /// Returns `Ok(true)` if there's no wrap-around, `Ok(false)` if there is, - /// and `Err(..)` if the range is full so it depends how you think about it. - #[inline] - pub fn no_signed_wraparound(&self, size: Size) -> Result { - if self.is_full_for(size) { - Err(..) - } else { - let start: i128 = size.sign_extend(self.start); - let end: i128 = size.sign_extend(self.end); - Ok(start <= end) - } - } -} - -impl fmt::Debug for WrappingRange { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.start > self.end { - write!(fmt, "(..={}) | ({}..)", self.end, self.start)?; - } else { - write!(fmt, "{}..={}", self.start, self.end)?; - } - Ok(()) - } -} - /// Information about one scalar component of a Rust type. #[derive(Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(feature = "nightly", derive(StableHash))] diff --git a/compiler/rustc_abi/src/wrapping_range.rs b/compiler/rustc_abi/src/wrapping_range.rs new file mode 100644 index 0000000000000..b0b2b268eb189 --- /dev/null +++ b/compiler/rustc_abi/src/wrapping_range.rs @@ -0,0 +1,152 @@ +use std::fmt; +use std::ops::RangeFull; + +use crate::Size; +#[cfg(feature = "nightly")] +use crate::StableHash; + +/// Inclusive wrap-around range of valid values, that is, if +/// start > end, it represents `start..=MAX`, followed by `0..=end`. +/// +/// That is, for an i8 primitive, a range of `254..=2` means following +/// sequence: +/// +/// 254 (-2), 255 (-1), 0, 1, 2 +/// +/// This is intended specifically to mirror LLVM’s `!range` metadata semantics. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "nightly", derive(StableHash))] +pub struct WrappingRange { + pub start: u128, + pub end: u128, +} + +impl WrappingRange { + pub(crate) fn debug_as(&self, size: Size, is_signed: bool) -> impl fmt::Debug { + let range = *self; + fmt::from_fn(move |f| { + if range == WrappingRange::full(size) { + // This is intentionally not using `is_full_for` so that we ensure + // different values always debug-print differently. + // We don't need the full details when it's the canonical full range, + // but if one is looking at the debug output it might be that seeing + // `u8 is (..=0) | (1..)` instead of `u8 is ..` is the information + // you needed because the problem is that despite being *a* full + // range it's not *the* canonical one you expected it was. + f.write_str("..") + } else if is_signed { + let start = size.sign_extend(range.start); + let end = size.sign_extend(range.end); + if start > end { + write!(f, "(..={}) | ({}..)", end, start) + } else { + write!(f, "{}..={}", start, end) + } + } else { + write!(f, "{:?}", range) + } + }) + } + + pub fn full(size: Size) -> Self { + Self { start: 0, end: size.unsigned_int_max() } + } + + /// Returns `true` if `v` is contained in the range. + #[inline(always)] + pub fn contains(&self, v: u128) -> bool { + if self.start <= self.end { + self.start <= v && v <= self.end + } else { + self.start <= v || v <= self.end + } + } + + /// Returns `true` if all the values in `other` are contained in this range, + /// when the values are considered as having width `size`. + #[inline(always)] + pub fn contains_range(&self, other: Self, size: Size) -> bool { + if self.is_full_for(size) { + true + } else { + let trunc = |x| size.truncate(x); + + let delta = self.start; + let max = trunc(self.end.wrapping_sub(delta)); + + let other_start = trunc(other.start.wrapping_sub(delta)); + let other_end = trunc(other.end.wrapping_sub(delta)); + + // Having shifted both input ranges by `delta`, now we only need to check + // whether `0..=max` contains `other_start..=other_end`, which can only + // happen if the other doesn't wrap since `self` isn't everything. + (other_start <= other_end) && (other_end <= max) + } + } + + /// Returns `self` with replaced `start` + #[inline(always)] + pub(crate) fn with_start(mut self, start: u128) -> Self { + self.start = start; + self + } + + /// Returns `self` with replaced `end` + #[inline(always)] + pub(crate) fn with_end(mut self, end: u128) -> Self { + self.end = end; + self + } + + /// Returns `true` if `size` completely fills the range. + /// + /// Note that this is *not* the same as `self == WrappingRange::full(size)`. + /// Niche calculations can produce full ranges which are not the canonical one; + /// for example `Option>` gets `valid_range: (..=0) | (1..)`. + #[inline] + pub fn is_full_for(&self, size: Size) -> bool { + let max_value = size.unsigned_int_max(); + debug_assert!(self.start <= max_value && self.end <= max_value); + self.start == (self.end.wrapping_add(1) & max_value) + } + + /// Checks whether this range is considered non-wrapping when the values are + /// interpreted as *unsigned* numbers of width `size`. + /// + /// Returns `Ok(true)` if there's no wrap-around, `Ok(false)` if there is, + /// and `Err(..)` if the range is full so it depends how you think about it. + #[inline] + pub fn no_unsigned_wraparound(&self, size: Size) -> Result { + if self.is_full_for(size) { Err(..) } else { Ok(self.start <= self.end) } + } + + /// Checks whether this range is considered non-wrapping when the values are + /// interpreted as *signed* numbers of width `size`. + /// + /// This is heavily dependent on the `size`, as `100..=200` does wrap when + /// interpreted as `i8`, but doesn't when interpreted as `i16`. + /// + /// Returns `Ok(true)` if there's no wrap-around, `Ok(false)` if there is, + /// and `Err(..)` if the range is full so it depends how you think about it. + #[inline] + pub fn no_signed_wraparound(&self, size: Size) -> Result { + if self.is_full_for(size) { + Err(..) + } else { + let start: i128 = size.sign_extend(self.start); + let end: i128 = size.sign_extend(self.end); + Ok(start <= end) + } + } +} + +impl fmt::Debug for WrappingRange { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.start > self.end { + write!(fmt, "(..={}) | ({}..)", self.end, self.start)?; + } else { + write!(fmt, "{}..={}", self.start, self.end)?; + } + Ok(()) + } +} From 679bdc1180ae963027d9db63a216f6afa1bf4b48 Mon Sep 17 00:00:00 2001 From: Ramon de C Valle Date: Sat, 18 Jul 2026 21:47:48 -0700 Subject: [PATCH 02/31] CFI: Add support for the adt_const_params feature Adds support for encoding constants of types allowed by the adt_const_params and unsized_const_params features (i.e., char, str, tuple, array, slice, struct, enum, and reference values) as literal arguments, and fixes the encoding of bool values to be 0 and 1, and of negative integer values to be their decimal values preceded by n. --- .../src/cfi/typeid/itanium_cxx_abi/encode.rs | 151 ++++++++++++++---- ...adata-id-itanium-cxx-abi-const-generics.rs | 51 ++++++ tests/ui/sanitizer/cfi/const-generics.rs | 110 +++++++++++++ tests/ui/sanitizer/kcfi/const-generics.rs | 109 +++++++++++++ 4 files changed, 393 insertions(+), 28 deletions(-) create mode 100644 tests/ui/sanitizer/cfi/const-generics.rs create mode 100644 tests/ui/sanitizer/kcfi/const-generics.rs diff --git a/compiler/rustc_sanitizers/src/cfi/typeid/itanium_cxx_abi/encode.rs b/compiler/rustc_sanitizers/src/cfi/typeid/itanium_cxx_abi/encode.rs index 873ed9bb10398..2869da32604a2 100644 --- a/compiler/rustc_sanitizers/src/cfi/typeid/itanium_cxx_abi/encode.rs +++ b/compiler/rustc_sanitizers/src/cfi/typeid/itanium_cxx_abi/encode.rs @@ -129,34 +129,8 @@ fn encode_const<'tcx>( // Element type s.push_str(&encode_ty(tcx, cv.ty, dict, options)); - // The only allowed types of const values are bool, u8, u16, u32, - // u64, u128, usize i8, i16, i32, i64, i128, isize, and char. The - // bool value false is encoded as 0 and true as 1. - match cv.ty.kind() { - ty::Int(ity) => { - let bits = cv - .try_to_bits(tcx, ty::TypingEnv::fully_monomorphized()) - .expect("expected monomorphic const in cfi"); - let val = Integer::from_int_ty(&tcx, *ity).size().sign_extend(bits) as i128; - if val < 0 { - s.push('n'); - } - let _ = write!(s, "{val}"); - } - ty::Uint(_) => { - let val = cv - .try_to_bits(tcx, ty::TypingEnv::fully_monomorphized()) - .expect("expected monomorphic const in cfi"); - let _ = write!(s, "{val}"); - } - ty::Bool => { - let val = cv.try_to_bool().expect("expected monomorphic const in cfi"); - let _ = write!(s, "{val}"); - } - _ => { - bug!("encode_const: unexpected type `{:?}`", cv.ty); - } - } + // Element value + s.push_str(&encode_const_value(tcx, cv, dict, options)); } _ => { @@ -172,6 +146,127 @@ fn encode_const<'tcx>( s } +/// Encodes a const value using the Itanium C++ ABI as the element value of a literal argument (see +/// ). +fn encode_const_value<'tcx>( + tcx: TyCtxt<'tcx>, + cv: ty::Value<'tcx>, + dict: &mut FxHashMap, usize>, + options: EncodeTyOptions, +) -> String { + let mut s = String::new(); + + match cv.ty.kind() { + // Primitive types + + // The bool value false is encoded as 0 and true as 1. + ty::Bool => { + let val = cv.try_to_bool().expect("expected monomorphic const in cfi"); + s.push(if val { '1' } else { '0' }); + } + + // Integer values are encoded as their decimal values, with negative values preceded by n. + ty::Int(ity) => { + let bits = cv + .try_to_bits(tcx, ty::TypingEnv::fully_monomorphized()) + .expect("expected monomorphic const in cfi"); + let val = Integer::from_int_ty(&tcx, *ity).size().sign_extend(bits) as i128; + if val < 0 { + s.push('n'); + } + let _ = write!(s, "{}", val.unsigned_abs()); + } + + ty::Uint(..) => { + let val = cv + .try_to_bits(tcx, ty::TypingEnv::fully_monomorphized()) + .expect("expected monomorphic const in cfi"); + let _ = write!(s, "{val}"); + } + + // char values are encoded as their Unicode scalar values (i.e., as their decimal u32 + // values). + ty::Char => { + let val = cv + .try_to_bits(tcx, ty::TypingEnv::fully_monomorphized()) + .expect("expected monomorphic const in cfi"); + let _ = write!(s, "{val}"); + } + + // str values are encoded as their UTF-8 encodings in hexadecimal. + ty::Str => { + // Hide the str type behind a reference for try_to_raw_bytes (i.e., the valtree of a + // str value is the valtree of its reference). + let ref_ty = Ty::new_imm_ref(tcx, tcx.lifetimes.re_erased, cv.ty); + let cv = ty::Value { ty: ref_ty, valtree: cv.valtree }; + let bytes = cv.try_to_raw_bytes(tcx).expect("expected monomorphic const in cfi"); + for byte in bytes { + let _ = write!(s, "{byte:02x}"); + } + } + + // Sequence types + // Array, slice, and tuple values are encoded as their element values as literal arguments. + ty::Array(..) | ty::Slice(..) | ty::Tuple(..) => { + for field in cv.to_branch() { + let ty::ConstKind::Value(field_cv) = field.kind() else { + bug!("encode_const_value: unexpected kind `{:?}`", field.kind()); + }; + s.push_str(&encode_const(tcx, *field, field_cv.ty, dict, options)); + } + } + + // User-defined types + // Struct and enum values are encoded as their field values as literal arguments, preceded + // by V for enum values. + ty::Adt(adt_def, ..) => { + let contents = cv.destructure_adt_const(); + if adt_def.is_enum() { + let _ = write!(s, "V{}", contents.variant.as_u32()); + } + for field in contents.fields { + let ty::ConstKind::Value(field_cv) = field.kind() else { + bug!("encode_const_value: unexpected kind `{:?}`", field.kind()); + }; + s.push_str(&encode_const(tcx, *field, field_cv.ty, dict, options)); + } + } + + // Pointer types + // Reference values are encoded as the values of their referents (i.e., the valtree of a + // reference value is the valtree of its referent). + ty::Ref(_, ty0, ..) => { + let cv = ty::Value { ty: *ty0, valtree: cv.valtree }; + s.push_str(&encode_const_value(tcx, cv, dict, options)); + } + + // Unexpected types + ty::Float(..) + | ty::Never + | ty::Foreign(..) + | ty::Pat(..) + | ty::FnDef(..) + | ty::FnPtr(..) + | ty::RawPtr(..) + | ty::Closure(..) + | ty::CoroutineClosure(..) + | ty::Coroutine(..) + | ty::CoroutineWitness(..) + | ty::Dynamic(..) + | ty::UnsafeBinder(..) + | ty::Param(..) + | ty::Alias(..) + | ty::Bound(..) + | ty::Error(..) + | ty::Infer(..) + | ty::Placeholder(..) => { + bug!("encode_const_value: unexpected type `{:?}`", cv.ty); + } + } + + s +} + /// Encodes a FnSig using the Itanium C++ ABI with vendor extended type qualifiers and types for /// Rust types that are not used at the FFI boundary. fn encode_fnsig<'tcx>( diff --git a/tests/codegen-llvm/sanitizer/cfi/emit-type-metadata-id-itanium-cxx-abi-const-generics.rs b/tests/codegen-llvm/sanitizer/cfi/emit-type-metadata-id-itanium-cxx-abi-const-generics.rs index cf26c17af1ed3..98591b0d4f1d2 100644 --- a/tests/codegen-llvm/sanitizer/cfi/emit-type-metadata-id-itanium-cxx-abi-const-generics.rs +++ b/tests/codegen-llvm/sanitizer/cfi/emit-type-metadata-id-itanium-cxx-abi-const-generics.rs @@ -5,10 +5,15 @@ //@ compile-flags: -Clto -Cno-prepopulate-passes -Ctarget-feature=-crt-static -Zsanitizer=cfi -Copt-level=0 -C unsafe-allow-abi-mismatch=sanitizer #![crate_type = "lib"] +#![feature(adt_const_params)] #![feature(type_alias_impl_trait)] +#![feature(unsized_const_params)] +#![allow(incomplete_features)] extern crate core; +use std::marker::ConstParamTy; + pub type Type1 = impl Send; #[define_opaque(Type1)] @@ -27,6 +32,52 @@ pub fn foo2(_: Type1, _: Type1) {} pub fn foo3(_: Type1, _: Type1, _: Type1) {} // CHECK: define{{.*}}4foo3{{.*}}!type ![[TYPE3:[0-9]+]] !type !{{[0-9]+}} !type !{{[0-9]+}} !type !{{[0-9]+}} +#[derive(PartialEq, Eq, ConstParamTy)] +pub struct Struct1 { + pub x: u16, + pub y: u16, +} + +#[derive(PartialEq, Eq, ConstParamTy)] +pub enum Enum1 { + Variant1, + Variant2(u8), +} + +pub struct BoolHolder; +pub struct IntHolder; +pub struct CharHolder; +pub struct StrHolder; +pub struct StructHolder; +pub struct EnumHolder; +pub struct ArrayHolder; +pub struct TupleHolder; + +pub fn foo4(_: &BoolHolder) {} +// CHECK: define{{.*}}4foo4{{.*}}!type ![[TYPE4:[0-9]+]] !type !{{[0-9]+}} !type !{{[0-9]+}} !type !{{[0-9]+}} +pub fn foo5(_: &IntHolder<-1>) {} +// CHECK: define{{.*}}4foo5{{.*}}!type ![[TYPE5:[0-9]+]] !type !{{[0-9]+}} !type !{{[0-9]+}} !type !{{[0-9]+}} +pub fn foo6(_: &CharHolder<'x'>) {} +// CHECK: define{{.*}}4foo6{{.*}}!type ![[TYPE6:[0-9]+]] !type !{{[0-9]+}} !type !{{[0-9]+}} !type !{{[0-9]+}} +pub fn foo7(_: &StrHolder<"hello">) {} +// CHECK: define{{.*}}4foo7{{.*}}!type ![[TYPE7:[0-9]+]] !type !{{[0-9]+}} !type !{{[0-9]+}} !type !{{[0-9]+}} +pub fn foo8(_: &StructHolder<{ Struct1 { x: 1, y: 2 } }>) {} +// CHECK: define{{.*}}4foo8{{.*}}!type ![[TYPE8:[0-9]+]] !type !{{[0-9]+}} !type !{{[0-9]+}} !type !{{[0-9]+}} +pub fn foo9(_: &EnumHolder<{ Enum1::Variant2(5) }>) {} +// CHECK: define{{.*}}4foo9{{.*}}!type ![[TYPE9:[0-9]+]] !type !{{[0-9]+}} !type !{{[0-9]+}} !type !{{[0-9]+}} +pub fn foo10(_: &ArrayHolder<{ [3, 4] }>) {} +// CHECK: define{{.*}}5foo10{{.*}}!type ![[TYPE10:[0-9]+]] !type !{{[0-9]+}} !type !{{[0-9]+}} !type !{{[0-9]+}} +pub fn foo11(_: &TupleHolder<{ (6, true) }>) {} +// CHECK: define{{.*}}5foo11{{.*}}!type ![[TYPE11:[0-9]+]] !type !{{[0-9]+}} !type !{{[0-9]+}} !type !{{[0-9]+}} + // CHECK: ![[TYPE1]] = !{i64 0, !"_ZTSFvu{{[0-9]+}}NtNvC{{[[:print:]]+}}_{{[[:print:]]+}}3foo3FooIu3i32Lu5usize32EEE"} // CHECK: ![[TYPE2]] = !{i64 0, !"_ZTSFvu{{[0-9]+}}NtNvC{{[[:print:]]+}}_{{[[:print:]]+}}3foo3FooIu3i32Lu5usize32EES2_E"} // CHECK: ![[TYPE3]] = !{i64 0, !"_ZTSFvu{{[0-9]+}}NtNvC{{[[:print:]]+}}_{{[[:print:]]+}}3foo3FooIu3i32Lu5usize32EES2_S2_E"} +// CHECK: ![[TYPE4]] = !{i64 0, !"_ZTSFvu3refIu{{[0-9]+}}NtC{{[[:print:]]+}}_{{[[:print:]]+}}10BoolHolderILb1EEEE"} +// CHECK: ![[TYPE5]] = !{i64 0, !"_ZTSFvu3refIu{{[0-9]+}}NtC{{[[:print:]]+}}_{{[[:print:]]+}}9IntHolderILu3i32n1EEEE"} +// CHECK: ![[TYPE6]] = !{i64 0, !"_ZTSFvu3refIu{{[0-9]+}}NtC{{[[:print:]]+}}_{{[[:print:]]+}}10CharHolderILu4char120EEEE"} +// CHECK: ![[TYPE7]] = !{i64 0, !"_ZTSFvu3refIu{{[0-9]+}}NtC{{[[:print:]]+}}_{{[[:print:]]+}}9StrHolderILu3refIu3strE68656c6c6fEEEE"} +// CHECK: ![[TYPE8]] = !{i64 0, !"_ZTSFvu3refIu{{[0-9]+}}NtC{{[[:print:]]+}}_{{[[:print:]]+}}12StructHolderILu{{[0-9]+}}NtC{{[[:print:]]+}}_{{[[:print:]]+}}7Struct1Lu3u161ELS0_2EEEEE"} +// CHECK: ![[TYPE9]] = !{i64 0, !"_ZTSFvu3refIu{{[0-9]+}}NtC{{[[:print:]]+}}_{{[[:print:]]+}}10EnumHolderILu{{[0-9]+}}NtC{{[[:print:]]+}}_{{[[:print:]]+}}5Enum1V1Lu2u85EEEEE"} +// CHECK: ![[TYPE10]] = !{i64 0, !"_ZTSFvu3refIu{{[0-9]+}}NtC{{[[:print:]]+}}_{{[[:print:]]+}}11ArrayHolderILA2u3u16LS_3ELS_4EEEEE"} +// CHECK: ![[TYPE11]] = !{i64 0, !"_ZTSFvu3refIu{{[0-9]+}}NtC{{[[:print:]]+}}_{{[[:print:]]+}}11TupleHolderILu5tupleIu3u16bELS_6ELb1EEEEE"} diff --git a/tests/ui/sanitizer/cfi/const-generics.rs b/tests/ui/sanitizer/cfi/const-generics.rs new file mode 100644 index 0000000000000..42fff233dd84b --- /dev/null +++ b/tests/ui/sanitizer/cfi/const-generics.rs @@ -0,0 +1,110 @@ +// Verifies that functions with types with const generics as argument types can +// be called through function pointers. +// +//@ needs-sanitizer-cfi +// FIXME(#122848) Remove only-linux once OSX CFI binaries work +//@ only-linux +//@ ignore-backends: gcc +//@ compile-flags: -Ctarget-feature=-crt-static -Ccodegen-units=1 -Clto -Cprefer-dynamic=off -Copt-level=0 -Zsanitizer=cfi -Cunsafe-allow-abi-mismatch=sanitizer +//@ run-pass + +#![feature(adt_const_params)] +#![feature(unsized_const_params)] +#![allow(incomplete_features)] + +use std::marker::ConstParamTy; + +#[derive(PartialEq, Eq, ConstParamTy)] +struct Struct2 { + x: u16, + y: u16, +} + +#[derive(PartialEq, Eq, ConstParamTy)] +enum Enum1 { + Variant1, + Variant2(u8), +} + +struct Struct1([i32; N]); +struct BoolHolder(bool); +struct IntHolder(i32); +struct CharHolder(char); +struct StrHolder(&'static str); +struct StructHolder(Struct2); +struct EnumHolder(Enum1); +struct ArrayHolder([u16; 2]); +struct TupleHolder((u16, bool)); + +fn foo1(x: Struct1<2>) { + assert_eq!(x.0, [1, 2]); +} + +fn foo2(x: &Struct1<4>) { + assert_eq!(x.0, [1, 2, 3, 4]); +} + +fn foo3(x: BoolHolder) { + assert!(x.0); +} + +fn foo4(x: IntHolder<-1>) { + assert_eq!(x.0, -1); +} + +fn foo5(x: CharHolder<'x'>) { + assert_eq!(x.0, 'x'); +} + +fn foo6(x: StrHolder<"hello">) { + assert_eq!(x.0, "hello"); +} + +fn foo7(x: StructHolder<{ Struct2 { x: 1, y: 2 } }>) { + assert_eq!(x.0.x, 1); + assert_eq!(x.0.y, 2); +} + +fn foo8(x: EnumHolder<{ Enum1::Variant1 }>) { + assert!(matches!(x.0, Enum1::Variant1)); +} + +fn foo9(x: EnumHolder<{ Enum1::Variant2(5) }>) { + match x.0 { + Enum1::Variant1 => unreachable!(), + Enum1::Variant2(v) => assert_eq!(v, 5), + } +} + +fn foo10(x: ArrayHolder<{ [3, 4] }>) { + assert_eq!(x.0, [3, 4]); +} + +fn foo11(x: TupleHolder<{ (6, true) }>) { + assert_eq!(x.0, (6, true)); +} + +fn main() { + let f: fn(Struct1<2>) = foo1; + f(Struct1([1, 2])); + let f: fn(&Struct1<4>) = foo2; + f(&Struct1([1, 2, 3, 4])); + let f: fn(BoolHolder) = foo3; + f(BoolHolder(true)); + let f: fn(IntHolder<-1>) = foo4; + f(IntHolder(-1)); + let f: fn(CharHolder<'x'>) = foo5; + f(CharHolder('x')); + let f: fn(StrHolder<"hello">) = foo6; + f(StrHolder("hello")); + let f: fn(StructHolder<{ Struct2 { x: 1, y: 2 } }>) = foo7; + f(StructHolder(Struct2 { x: 1, y: 2 })); + let f: fn(EnumHolder<{ Enum1::Variant1 }>) = foo8; + f(EnumHolder(Enum1::Variant1)); + let f: fn(EnumHolder<{ Enum1::Variant2(5) }>) = foo9; + f(EnumHolder(Enum1::Variant2(5))); + let f: fn(ArrayHolder<{ [3, 4] }>) = foo10; + f(ArrayHolder([3, 4])); + let f: fn(TupleHolder<{ (6, true) }>) = foo11; + f(TupleHolder((6, true))); +} diff --git a/tests/ui/sanitizer/kcfi/const-generics.rs b/tests/ui/sanitizer/kcfi/const-generics.rs new file mode 100644 index 0000000000000..86f487bb9ea1e --- /dev/null +++ b/tests/ui/sanitizer/kcfi/const-generics.rs @@ -0,0 +1,109 @@ +// Verifies that functions with types with const generics as argument types can +// be called through function pointers. +// +//@ needs-sanitizer-kcfi +//@ only-linux +//@ ignore-backends: gcc +//@ compile-flags: -Ctarget-feature=-crt-static -Cpanic=abort -Cprefer-dynamic=off -Copt-level=0 -Zsanitizer=kcfi -Cunsafe-allow-abi-mismatch=sanitizer +//@ run-pass + +#![feature(adt_const_params)] +#![feature(unsized_const_params)] +#![allow(incomplete_features)] + +use std::marker::ConstParamTy; + +#[derive(PartialEq, Eq, ConstParamTy)] +struct Struct2 { + x: u16, + y: u16, +} + +#[derive(PartialEq, Eq, ConstParamTy)] +enum Enum1 { + Variant1, + Variant2(u8), +} + +struct Struct1([i32; N]); +struct BoolHolder(bool); +struct IntHolder(i32); +struct CharHolder(char); +struct StrHolder(&'static str); +struct StructHolder(Struct2); +struct EnumHolder(Enum1); +struct ArrayHolder([u16; 2]); +struct TupleHolder((u16, bool)); + +fn foo1(x: Struct1<2>) { + assert_eq!(x.0, [1, 2]); +} + +fn foo2(x: &Struct1<4>) { + assert_eq!(x.0, [1, 2, 3, 4]); +} + +fn foo3(x: BoolHolder) { + assert!(x.0); +} + +fn foo4(x: IntHolder<-1>) { + assert_eq!(x.0, -1); +} + +fn foo5(x: CharHolder<'x'>) { + assert_eq!(x.0, 'x'); +} + +fn foo6(x: StrHolder<"hello">) { + assert_eq!(x.0, "hello"); +} + +fn foo7(x: StructHolder<{ Struct2 { x: 1, y: 2 } }>) { + assert_eq!(x.0.x, 1); + assert_eq!(x.0.y, 2); +} + +fn foo8(x: EnumHolder<{ Enum1::Variant1 }>) { + assert!(matches!(x.0, Enum1::Variant1)); +} + +fn foo9(x: EnumHolder<{ Enum1::Variant2(5) }>) { + match x.0 { + Enum1::Variant1 => unreachable!(), + Enum1::Variant2(v) => assert_eq!(v, 5), + } +} + +fn foo10(x: ArrayHolder<{ [3, 4] }>) { + assert_eq!(x.0, [3, 4]); +} + +fn foo11(x: TupleHolder<{ (6, true) }>) { + assert_eq!(x.0, (6, true)); +} + +fn main() { + let f: fn(Struct1<2>) = foo1; + f(Struct1([1, 2])); + let f: fn(&Struct1<4>) = foo2; + f(&Struct1([1, 2, 3, 4])); + let f: fn(BoolHolder) = foo3; + f(BoolHolder(true)); + let f: fn(IntHolder<-1>) = foo4; + f(IntHolder(-1)); + let f: fn(CharHolder<'x'>) = foo5; + f(CharHolder('x')); + let f: fn(StrHolder<"hello">) = foo6; + f(StrHolder("hello")); + let f: fn(StructHolder<{ Struct2 { x: 1, y: 2 } }>) = foo7; + f(StructHolder(Struct2 { x: 1, y: 2 })); + let f: fn(EnumHolder<{ Enum1::Variant1 }>) = foo8; + f(EnumHolder(Enum1::Variant1)); + let f: fn(EnumHolder<{ Enum1::Variant2(5) }>) = foo9; + f(EnumHolder(Enum1::Variant2(5))); + let f: fn(ArrayHolder<{ [3, 4] }>) = foo10; + f(ArrayHolder([3, 4])); + let f: fn(TupleHolder<{ (6, true) }>) = foo11; + f(TupleHolder((6, true))); +} From e2781051ee20d12dc7dc8bede683dbab0264d429 Mon Sep 17 00:00:00 2001 From: Scott McMurray Date: Sat, 18 Jul 2026 00:38:16 -0700 Subject: [PATCH 03/31] WrappingRange: add a constructor from the desired covered values --- compiler/rustc_abi/src/wrapping_range.rs | 57 ++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/compiler/rustc_abi/src/wrapping_range.rs b/compiler/rustc_abi/src/wrapping_range.rs index b0b2b268eb189..ecdc7dae88a67 100644 --- a/compiler/rustc_abi/src/wrapping_range.rs +++ b/compiler/rustc_abi/src/wrapping_range.rs @@ -98,6 +98,11 @@ impl WrappingRange { self } + /// The wrapping distance from `self.start` to `self.end`. + fn width(&self, size: Size) -> u128 { + size.truncate(u128::wrapping_sub(self.end, self.start)) + } + /// Returns `true` if `size` completely fills the range. /// /// Note that this is *not* the same as `self == WrappingRange::full(size)`. @@ -138,6 +143,58 @@ impl WrappingRange { Ok(start <= end) } } + + /// Returns a `WrappingRange` that contains all of the values from the iterator, + /// when they're treated as values `size` wide. + /// + /// # Examples + /// + /// + /// ``` + /// use rustc_abi::{Size, WrappingRange}; + /// + /// let range = WrappingRange::smallest_range_containing([2, 6, 12, 4], Size::from_bytes(2)); + /// assert_eq!(range.unwrap(), WrappingRange { start: 2, end: 12 }); + /// + /// let range = WrappingRange::smallest_range_containing(0..=127, Size::from_bytes(1)); + /// assert_eq!(range.unwrap(), WrappingRange { start: 0, end: 127 }); + /// let range = WrappingRange::smallest_range_containing([129, 128, 127], Size::from_bytes(1)); + /// assert_eq!(range.unwrap(), WrappingRange { start: 127, end: 129 }); + /// + /// // The size matters because it changes where the wrapping can happen: + /// let range = WrappingRange::smallest_range_containing([1, 254], Size::from_bytes(1)); + /// assert_eq!(range.unwrap(), WrappingRange { start: 254, end: 1 }); + /// let range = WrappingRange::smallest_range_containing([1, 254], Size::from_bytes(4)); + /// assert_eq!(range.unwrap(), WrappingRange { start: 1, end: 254 }); + /// + /// // Both `100..=228` and `..=228 | 100..` are the same size, but we pick the one without zero. + /// let range = WrappingRange::smallest_range_containing([100, 228], Size::from_bytes(1)); + /// assert_eq!(range.unwrap(), WrappingRange { start: 100, end: 228 }); + /// // These 4 values are evenly spaced so all 4 candidate ranges have length 193: + /// // `(..=32) | (96..)`, `(..=96) | (160..)`, `(..=160) | (224..)`, and `32..=224`. + /// // We pick the last one as the only one that doesn't contain zero. + /// let range = WrappingRange::smallest_range_containing([0xA0, 0xE0, 0x20, 0x60], Size::from_bytes(1)); + /// assert_eq!(range.unwrap(), WrappingRange { start: 0x20, end: 0xE0 }); + /// ``` + pub fn smallest_range_containing( + values: impl IntoIterator, + size: Size, + ) -> Option { + let mut values: Vec<_> = values.into_iter().collect(); + let umax = size.unsigned_int_max(); + for value in &values { + debug_assert!(*value <= umax, "Value {value:?} is too big for {size:?}"); + } + values.sort_unstable(); + + // Having sorted all the values, every element is a possible start point for the + // range of values, up to the previous element (wrapping around the end of the vec). + // Look at all those candidates and pick the one that's as narrow as possible. + let pairs = std::iter::zip(values.iter().copied(), values.iter().copied().cycle().skip(1)); + let ranges = pairs.map(|(end, start)| WrappingRange { start, end }); + let smallest_range = ranges.min_by_key(|r| (r.width(size), r.start)); + smallest_range + } } impl fmt::Debug for WrappingRange { From e5f0680ee2f1e436bd8a781df2c3fcd20cc08e99 Mon Sep 17 00:00:00 2001 From: Scott McMurray Date: Sat, 18 Jul 2026 03:28:39 -0700 Subject: [PATCH 04/31] Stop passing the discriminant as `i128` when `Discr` makes it as `u128` --- compiler/rustc_abi/src/layout.rs | 8 ++++---- compiler/rustc_ty_utils/src/layout.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/compiler/rustc_abi/src/layout.rs b/compiler/rustc_abi/src/layout.rs index 2218780092287..944b1996ee11b 100644 --- a/compiler/rustc_abi/src/layout.rs +++ b/compiler/rustc_abi/src/layout.rs @@ -350,7 +350,7 @@ impl LayoutCalculator { is_enum: bool, is_special_no_niche: bool, discr_range_of_repr: impl Fn(i128, i128) -> (Integer, bool), - discriminants: impl Iterator, + discriminants: impl Iterator, always_sized: bool, ) -> LayoutCalculatorResult { let (present_first, present_second) = { @@ -583,7 +583,7 @@ impl LayoutCalculator { repr: &ReprOptions, variants: &IndexSlice>, discr_range_of_repr: impl Fn(i128, i128) -> (Integer, bool), - discriminants: impl Iterator, + discriminants: impl Iterator, ) -> LayoutCalculatorResult { let dl = self.cx.data_layout(); // bail if the enum has an incoherent repr that cannot be computed @@ -767,9 +767,9 @@ impl LayoutCalculator { if discr_type.is_signed() { // sign extend the raw representation to be an i128 // FIXME: do this at the discriminant iterator creation sites - discr_int.size().sign_extend(val as u128) + discr_int.size().sign_extend(val) } else { - val + val as i128 } }) .collect(); diff --git a/compiler/rustc_ty_utils/src/layout.rs b/compiler/rustc_ty_utils/src/layout.rs index abec1850502b6..8a3c8bd7b3ff3 100644 --- a/compiler/rustc_ty_utils/src/layout.rs +++ b/compiler/rustc_ty_utils/src/layout.rs @@ -718,7 +718,7 @@ fn layout_of_uncached<'tcx>( let discriminants_iter = || { def.is_enum() - .then(|| def.discriminants(tcx).map(|(v, d)| (v, d.val as i128))) + .then(|| def.discriminants(tcx).map(|(v, d)| (v, d.val))) .into_flat_iter() }; From 934f93bb55376e542ab7f7e22ea50215738dddca Mon Sep 17 00:00:00 2001 From: Scott McMurray Date: Sat, 18 Jul 2026 00:39:32 -0700 Subject: [PATCH 05/31] Add some enum niche choice tests --- .../src/attributes/rustc_dump.rs | 4 +- .../rustc_hir/src/attrs/data_structures.rs | 1 + compiler/rustc_passes/src/layout_test.rs | 3 + compiler/rustc_span/src/symbol.rs | 1 + tests/ui/layout/enum-signedness.rs | 114 +++++++++++++++++ tests/ui/layout/enum-signedness.stderr | 119 ++++++++++++++++++ tests/ui/layout/enum-unusual-variants.rs | 78 ++++++++++++ tests/ui/layout/enum-unusual-variants.stderr | 44 +++++++ 8 files changed, 362 insertions(+), 2 deletions(-) create mode 100644 tests/ui/layout/enum-signedness.rs create mode 100644 tests/ui/layout/enum-signedness.stderr create mode 100644 tests/ui/layout/enum-unusual-variants.rs create mode 100644 tests/ui/layout/enum-unusual-variants.stderr diff --git a/compiler/rustc_attr_parsing/src/attributes/rustc_dump.rs b/compiler/rustc_attr_parsing/src/attributes/rustc_dump.rs index 0fd3d5d65e3a5..27d69f6c13cc1 100644 --- a/compiler/rustc_attr_parsing/src/attributes/rustc_dump.rs +++ b/compiler/rustc_attr_parsing/src/attributes/rustc_dump.rs @@ -123,8 +123,7 @@ impl CombineAttributeParser for RustcDumpLayoutParser { Allow(Target::TyAlias), ]); - const TEMPLATE: AttributeTemplate = - template!(List: &["abi", "align", "size", "homogenous_aggregate", "debug"]); + const TEMPLATE: AttributeTemplate = template!(List: &["abi", "align", "size", "homogenous_aggregate", "largest_niche", "debug"]); const STABILITY: AttributeStability = unstable!(rustc_attrs); fn extend( @@ -150,6 +149,7 @@ impl CombineAttributeParser for RustcDumpLayoutParser { sym::backend_repr => RustcDumpLayoutKind::BackendRepr, sym::debug => RustcDumpLayoutKind::Debug, sym::homogeneous_aggregate => RustcDumpLayoutKind::HomogenousAggregate, + sym::largest_niche => RustcDumpLayoutKind::LargestNiche, sym::size => RustcDumpLayoutKind::Size, _ => { cx.adcx().expected_specific_argument( diff --git a/compiler/rustc_hir/src/attrs/data_structures.rs b/compiler/rustc_hir/src/attrs/data_structures.rs index 765954d3c7369..eec9f44c0272e 100644 --- a/compiler/rustc_hir/src/attrs/data_structures.rs +++ b/compiler/rustc_hir/src/attrs/data_structures.rs @@ -809,6 +809,7 @@ pub enum RustcDumpLayoutKind { BackendRepr, Debug, HomogenousAggregate, + LargestNiche, Size, } diff --git a/compiler/rustc_passes/src/layout_test.rs b/compiler/rustc_passes/src/layout_test.rs index 19f2ca3af9232..85235906c38f0 100644 --- a/compiler/rustc_passes/src/layout_test.rs +++ b/compiler/rustc_passes/src/layout_test.rs @@ -85,6 +85,9 @@ fn dump_layout_of(tcx: TyCtxt<'_>, item_def_id: LocalDefId, kinds: &[RustcDumpLa ty_layout.homogeneous_aggregate(&UnwrapLayoutCx { tcx, typing_env }); format!("homogeneous_aggregate: {data:?}") } + RustcDumpLayoutKind::LargestNiche => { + format!("largest_niche: {:?}", ty_layout.largest_niche) + } RustcDumpLayoutKind::Size => format!("size: {:?}", ty_layout.size), }; tcx.dcx().span_err(span, message); diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs index 287b790ea6d67..1da142db46eaa 100644 --- a/compiler/rustc_span/src/symbol.rs +++ b/compiler/rustc_span/src/symbol.rs @@ -1174,6 +1174,7 @@ symbols! { lang, lang_items, large_assignments, + largest_niche, last, lasx, late_bound_turbofishing, diff --git a/tests/ui/layout/enum-signedness.rs b/tests/ui/layout/enum-signedness.rs new file mode 100644 index 0000000000000..f5f5fa0a9d311 --- /dev/null +++ b/tests/ui/layout/enum-signedness.rs @@ -0,0 +1,114 @@ +//@ only-64bit +#![feature(rustc_attrs)] +#![feature(never_type)] +#![crate_type = "lib"] + +// When picking a representation for things that don't force a particular discriminant type, +// we look at how the value was actually written, which means that "equivalent" things +// actually need to come out different. + +// The tests run only on 64-bit because we have to give things as `isize` in these cases. + +#[rustc_dump_layout(largest_niche)] +#[repr(Rust)] +enum NegativeByteRust { + //~^ ERROR: value: i8, valid_range: 155..=156 + A = -100, + B = -101, +} + +#[rustc_dump_layout(largest_niche)] +#[repr(C)] +enum NegativeByteC { + //~^ ERROR: value: i32, valid_range: 4294967195..=4294967196 + A = -100, + B = -101, +} + +#[rustc_dump_layout(largest_niche)] +#[repr(Rust)] +enum PositiveByteRust { + //~^ ERROR: value: u8, valid_range: 155..=156 + A = 256 - 100, + B = 256 - 101, +} + +#[rustc_dump_layout(largest_niche)] +#[repr(C)] +enum PositiveByteC { + //~^ ERROR: value: u32, valid_range: 155..=156 + A = 256 - 100, + B = 256 - 101, +} + +#[rustc_dump_layout(largest_niche)] +#[repr(Rust)] +enum Negative32BitRust { + //~^ ERROR: value: i32, valid_range: (..=0) | (2147483648..) + A = 0, + B = i32::MIN as isize, +} + +#[rustc_dump_layout(largest_niche)] +#[repr(C)] +enum Negative32BitC { + //~^ ERROR: value: i32, valid_range: (..=0) | (2147483648..) + A = 0, + B = i32::MIN as isize, +} + +#[rustc_dump_layout(largest_niche)] +#[repr(Rust)] +enum Positive32BitRust { + //~^ ERROR: value: u32, valid_range: 0..=2147483648 + A = 0, + B = i32::MIN.cast_unsigned() as isize, +} + +#[rustc_dump_layout(largest_niche)] +#[repr(C)] +enum Positive32BitC { + //~^ ERROR: value: u32, valid_range: 0..=2147483648 + A = 0, + B = i32::MIN.cast_unsigned() as isize, +} + +#[rustc_dump_layout(largest_niche)] +#[repr(Rust)] +enum Negative64BitRust { + //~^ ERROR: value: i64, valid_range: 9223372036854775808..=9223372036854775809 + A = i64::MIN as isize, + B = i64::MIN as isize + 1, +} + +#[rustc_dump_layout(largest_niche)] +#[repr(C)] +enum Negative64BitC { + //~^ ERROR: value: i64, valid_range: 9223372036854775808..=9223372036854775809 + A = i64::MIN as isize, + //~^ WARN: enum discriminant does not fit into C + //~| WARN: previously accepted + B = i64::MIN as isize + 1, + //~^ WARN: enum discriminant does not fit into C + //~| WARN: previously accepted +} + +#[rustc_dump_layout(largest_niche)] +#[repr(Rust)] +enum Positive64BitRust { + //~^ ERROR: value: u64, valid_range: 9223372036854775806..=9223372036854775807 + A = i64::MAX as isize - 1, + B = i64::MAX as isize, +} + +#[rustc_dump_layout(largest_niche)] +#[repr(C)] +enum Positive64BitC { + //~^ ERROR: value: u64, valid_range: 9223372036854775806..=9223372036854775807 + A = i64::MAX as isize - 1, + //~^ WARN: enum discriminant does not fit into C + //~| WARN: previously accepted + B = i64::MAX as isize, + //~^ WARN: enum discriminant does not fit into C + //~| WARN: previously accepted +} diff --git a/tests/ui/layout/enum-signedness.stderr b/tests/ui/layout/enum-signedness.stderr new file mode 100644 index 0000000000000..b676473e9bb18 --- /dev/null +++ b/tests/ui/layout/enum-signedness.stderr @@ -0,0 +1,119 @@ +warning: `repr(C)` enum discriminant does not fit into C `int` nor into C `unsigned int` + --> $DIR/enum-signedness.rs:88:5 + | +LL | A = i64::MIN as isize, + | ^ + | + = note: `repr(C)` enums with big discriminants are non-portable, and their size in Rust might not match their size in C + = help: use `repr($int_ty)` instead to explicitly set the size of this enum + = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release! + = note: for more information, see issue #124403 + = note: `#[warn(repr_c_enums_larger_than_int)]` (part of `#[warn(future_incompatible)]`) on by default + +warning: `repr(C)` enum discriminant does not fit into C `int` nor into C `unsigned int` + --> $DIR/enum-signedness.rs:91:5 + | +LL | B = i64::MIN as isize + 1, + | ^ + | + = note: `repr(C)` enums with big discriminants are non-portable, and their size in Rust might not match their size in C + = help: use `repr($int_ty)` instead to explicitly set the size of this enum + = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release! + = note: for more information, see issue #124403 + +warning: `repr(C)` enum discriminant does not fit into C `int` nor into C `unsigned int` + --> $DIR/enum-signedness.rs:108:5 + | +LL | A = i64::MAX as isize - 1, + | ^ + | + = note: `repr(C)` enums with big discriminants are non-portable, and their size in Rust might not match their size in C + = help: use `repr($int_ty)` instead to explicitly set the size of this enum + = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release! + = note: for more information, see issue #124403 + +warning: `repr(C)` enum discriminant does not fit into C `int` nor into C `unsigned int` + --> $DIR/enum-signedness.rs:111:5 + | +LL | B = i64::MAX as isize, + | ^ + | + = note: `repr(C)` enums with big discriminants are non-portable, and their size in Rust might not match their size in C + = help: use `repr($int_ty)` instead to explicitly set the size of this enum + = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release! + = note: for more information, see issue #124403 + +error: largest_niche: Some(Niche { offset: Size(0 bytes), value: i8, valid_range: 155..=156 }) + --> $DIR/enum-signedness.rs:14:1 + | +LL | enum NegativeByteRust { + | ^^^^^^^^^^^^^^^^^^^^^ + +error: largest_niche: Some(Niche { offset: Size(0 bytes), value: i32, valid_range: 4294967195..=4294967196 }) + --> $DIR/enum-signedness.rs:22:1 + | +LL | enum NegativeByteC { + | ^^^^^^^^^^^^^^^^^^ + +error: largest_niche: Some(Niche { offset: Size(0 bytes), value: u8, valid_range: 155..=156 }) + --> $DIR/enum-signedness.rs:30:1 + | +LL | enum PositiveByteRust { + | ^^^^^^^^^^^^^^^^^^^^^ + +error: largest_niche: Some(Niche { offset: Size(0 bytes), value: u32, valid_range: 155..=156 }) + --> $DIR/enum-signedness.rs:38:1 + | +LL | enum PositiveByteC { + | ^^^^^^^^^^^^^^^^^^ + +error: largest_niche: Some(Niche { offset: Size(0 bytes), value: i32, valid_range: (..=0) | (2147483648..) }) + --> $DIR/enum-signedness.rs:46:1 + | +LL | enum Negative32BitRust { + | ^^^^^^^^^^^^^^^^^^^^^^ + +error: largest_niche: Some(Niche { offset: Size(0 bytes), value: i32, valid_range: (..=0) | (2147483648..) }) + --> $DIR/enum-signedness.rs:54:1 + | +LL | enum Negative32BitC { + | ^^^^^^^^^^^^^^^^^^^ + +error: largest_niche: Some(Niche { offset: Size(0 bytes), value: u32, valid_range: 0..=2147483648 }) + --> $DIR/enum-signedness.rs:62:1 + | +LL | enum Positive32BitRust { + | ^^^^^^^^^^^^^^^^^^^^^^ + +error: largest_niche: Some(Niche { offset: Size(0 bytes), value: u32, valid_range: 0..=2147483648 }) + --> $DIR/enum-signedness.rs:70:1 + | +LL | enum Positive32BitC { + | ^^^^^^^^^^^^^^^^^^^ + +error: largest_niche: Some(Niche { offset: Size(0 bytes), value: i64, valid_range: 9223372036854775808..=9223372036854775809 }) + --> $DIR/enum-signedness.rs:78:1 + | +LL | enum Negative64BitRust { + | ^^^^^^^^^^^^^^^^^^^^^^ + +error: largest_niche: Some(Niche { offset: Size(0 bytes), value: i64, valid_range: 9223372036854775808..=9223372036854775809 }) + --> $DIR/enum-signedness.rs:86:1 + | +LL | enum Negative64BitC { + | ^^^^^^^^^^^^^^^^^^^ + +error: largest_niche: Some(Niche { offset: Size(0 bytes), value: u64, valid_range: 9223372036854775806..=9223372036854775807 }) + --> $DIR/enum-signedness.rs:98:1 + | +LL | enum Positive64BitRust { + | ^^^^^^^^^^^^^^^^^^^^^^ + +error: largest_niche: Some(Niche { offset: Size(0 bytes), value: u64, valid_range: 9223372036854775806..=9223372036854775807 }) + --> $DIR/enum-signedness.rs:106:1 + | +LL | enum Positive64BitC { + | ^^^^^^^^^^^^^^^^^^^ + +error: aborting due to 12 previous errors; 4 warnings emitted + diff --git a/tests/ui/layout/enum-unusual-variants.rs b/tests/ui/layout/enum-unusual-variants.rs new file mode 100644 index 0000000000000..1ea78417f1013 --- /dev/null +++ b/tests/ui/layout/enum-unusual-variants.rs @@ -0,0 +1,78 @@ +#![feature(rustc_attrs)] +#![feature(never_type)] +#![crate_type = "lib"] + +// Regression test for https://github.com/rust-lang/rust/issues/159438 +// where with `i8` it was getting an unnecessarily-broad `valid_range` + +#[rustc_dump_layout(largest_niche)] +enum With128Variants { + //~^ ERROR: value: u8, valid_range: 0..=127 + _0 = 0, + _1 = 1, + _2 = 2, + // layout doesn't actually care if we elide a bunch here + _125 = 125, + _126 = 126, + _127 = 127, +} + +#[rustc_dump_layout(largest_niche)] +#[repr(i8)] +enum With128VariantsI8 { + //~^ ERROR: value: i8, valid_range: (..=2) | (125..) + _0 = 0, + _1 = 1, + _2 = 2, + // layout doesn't actually care if we elide a bunch here + _125 = 125, + _126 = 126, + _127 = 127, +} + +#[rustc_dump_layout(largest_niche)] +#[repr(u8)] +enum With128VariantsU8 { + //~^ ERROR: value: u8, valid_range: 0..=127 + _0 = 0, + _1 = 1, + _2 = 2, + // layout doesn't actually care if we elide a bunch here + _125 = 125, + _126 = 126, + _127 = 127, +} + +// For these either the wrapping or the non-wrapping `valid_range` have the same size, +// but it would be nice to consistently pick the one that leaves `0` unclaimed +// so that it can be used for `None` later. + +#[rustc_dump_layout(largest_niche)] +enum Symmetric { + //~^ ERROR: value: u8, valid_range: 1..=129 + A = 1, + B = 129, +} + +#[rustc_dump_layout(largest_niche)] +#[repr(u8)] +enum SymmetricU8 { + //~^ ERROR: value: u8, valid_range: (..=1) | (129..) + A = 1, + B = 129, +} + +#[rustc_dump_layout(largest_niche)] +enum SymmetricSigned { + //~^ ERROR: value: i8, valid_range: (..=1) | (129..) + A = -127, + B = 1, +} + +#[rustc_dump_layout(largest_niche)] +#[repr(i8)] +enum SymmetricSignedI8 { + //~^ ERROR: value: i8, valid_range: (..=1) | (129..) + A = -127, + B = 1, +} diff --git a/tests/ui/layout/enum-unusual-variants.stderr b/tests/ui/layout/enum-unusual-variants.stderr new file mode 100644 index 0000000000000..75e93a6a07e82 --- /dev/null +++ b/tests/ui/layout/enum-unusual-variants.stderr @@ -0,0 +1,44 @@ +error: largest_niche: Some(Niche { offset: Size(0 bytes), value: u8, valid_range: 0..=127 }) + --> $DIR/enum-unusual-variants.rs:9:1 + | +LL | enum With128Variants { + | ^^^^^^^^^^^^^^^^^^^^ + +error: largest_niche: Some(Niche { offset: Size(0 bytes), value: i8, valid_range: (..=2) | (125..) }) + --> $DIR/enum-unusual-variants.rs:22:1 + | +LL | enum With128VariantsI8 { + | ^^^^^^^^^^^^^^^^^^^^^^ + +error: largest_niche: Some(Niche { offset: Size(0 bytes), value: u8, valid_range: 0..=127 }) + --> $DIR/enum-unusual-variants.rs:35:1 + | +LL | enum With128VariantsU8 { + | ^^^^^^^^^^^^^^^^^^^^^^ + +error: largest_niche: Some(Niche { offset: Size(0 bytes), value: u8, valid_range: 1..=129 }) + --> $DIR/enum-unusual-variants.rs:51:1 + | +LL | enum Symmetric { + | ^^^^^^^^^^^^^^ + +error: largest_niche: Some(Niche { offset: Size(0 bytes), value: u8, valid_range: (..=1) | (129..) }) + --> $DIR/enum-unusual-variants.rs:59:1 + | +LL | enum SymmetricU8 { + | ^^^^^^^^^^^^^^^^ + +error: largest_niche: Some(Niche { offset: Size(0 bytes), value: i8, valid_range: (..=1) | (129..) }) + --> $DIR/enum-unusual-variants.rs:66:1 + | +LL | enum SymmetricSigned { + | ^^^^^^^^^^^^^^^^^^^^ + +error: largest_niche: Some(Niche { offset: Size(0 bytes), value: i8, valid_range: (..=1) | (129..) }) + --> $DIR/enum-unusual-variants.rs:74:1 + | +LL | enum SymmetricSignedI8 { + | ^^^^^^^^^^^^^^^^^^^^^^ + +error: aborting due to 7 previous errors + From 9cbba6e19ed3aa94885a2ef724e26dde282334a4 Mon Sep 17 00:00:00 2001 From: Scott McMurray Date: Sat, 18 Jul 2026 02:20:34 -0700 Subject: [PATCH 06/31] Fix signedness handling when picking enum ranges --- compiler/rustc_abi/src/layout.rs | 99 ++++++++------------ compiler/rustc_middle/src/ty/layout.rs | 25 +++-- compiler/rustc_ty_utils/src/layout.rs | 7 +- tests/ui/layout/enum-signedness.rs | 4 +- tests/ui/layout/enum-signedness.stderr | 4 +- tests/ui/layout/enum-unusual-variants.rs | 11 ++- tests/ui/layout/enum-unusual-variants.stderr | 12 +-- 7 files changed, 77 insertions(+), 85 deletions(-) diff --git a/compiler/rustc_abi/src/layout.rs b/compiler/rustc_abi/src/layout.rs index 944b1996ee11b..10ab91fecff81 100644 --- a/compiler/rustc_abi/src/layout.rs +++ b/compiler/rustc_abi/src/layout.rs @@ -1,7 +1,6 @@ -use std::collections::BTreeSet; use std::fmt::{self, Write}; use std::ops::Deref; -use std::range::RangeInclusive; +use std::range::{RangeFrom, RangeInclusive, RangeToInclusive}; use std::{cmp, iter}; use rustc_hashes::Hash64; @@ -349,7 +348,7 @@ impl LayoutCalculator { variants: &IndexSlice>, is_enum: bool, is_special_no_niche: bool, - discr_range_of_repr: impl Fn(i128, i128) -> (Integer, bool), + discr_range_of_repr: impl Fn(RangeFrom, RangeToInclusive) -> (Integer, bool), discriminants: impl Iterator, always_sized: bool, ) -> LayoutCalculatorResult { @@ -582,7 +581,7 @@ impl LayoutCalculator { &self, repr: &ReprOptions, variants: &IndexSlice>, - discr_range_of_repr: impl Fn(i128, i128) -> (Integer, bool), + discr_range_of_repr: impl Fn(RangeFrom, RangeToInclusive) -> (Integer, bool), discriminants: impl Iterator, ) -> LayoutCalculatorResult { let dl = self.cx.data_layout(); @@ -755,63 +754,36 @@ impl LayoutCalculator { let niche_filling_layout = calculate_niche_filling_layout(); let discr_type = repr.discr_type(); - let discr_int = Integer::from_attr(dl, discr_type); - // Because we can only represent one range of valid values, we'll look for the - // largest range of invalid values and pick everything else as the range of valid - // values. + let discr_size = Integer::from_attr(dl, discr_type).size(); - // First we need to sort the possible discriminant values so that we can look for the largest gap: - let valid_discriminants: BTreeSet = discriminants + let necessary_discriminants: Vec = discriminants .filter(|&(i, _)| repr.c() || variants[i].iter().all(|f| !f.is_uninhabited())) - .map(|(_, val)| { - if discr_type.is_signed() { - // sign extend the raw representation to be an i128 - // FIXME: do this at the discriminant iterator creation sites - discr_int.size().sign_extend(val) - } else { - val as i128 - } - }) + .map(|(_, val)| val) .collect(); - trace!(?valid_discriminants); - let discriminants = valid_discriminants.iter().copied(); - //let next_discriminants = discriminants.clone().cycle().skip(1); - let next_discriminants = - discriminants.clone().chain(valid_discriminants.first().copied()).skip(1); - // Iterate over pairs of each discriminant together with the next one. - // Since they were sorted, we can now compute the niche sizes and pick the largest. - let discriminants = discriminants.zip(next_discriminants); - let largest_niche = discriminants.max_by_key(|&(start, end)| { - trace!(?start, ?end); - // If this is a wraparound range, the niche size is `MAX - abs(diff)`, as the diff between - // the two end points is actually the size of the valid discriminants. - let dist = if start > end { - // Overflow can happen for 128 bit discriminants if `end` is negative. - // But in that case casting to `u128` still gets us the right value, - // as the distance must be positive if the lhs of the subtraction is larger than the rhs. - let dist = start.wrapping_sub(end); - if discr_type.is_signed() { - discr_int.signed_max().wrapping_sub(dist) as u128 - } else { - discr_int.size().unsigned_int_max() - dist as u128 - } - } else { - // Overflow can happen for 128 bit discriminants if `start` is negative. - // But in that case casting to `u128` still gets us the right value, - // as the distance must be positive if the lhs of the subtraction is larger than the rhs. - end.wrapping_sub(start) as u128 - }; - trace!(?dist); - dist - }); - trace!(?largest_niche); - - // `max` is the last valid discriminant before the largest niche - // `min` is the first valid discriminant after the largest niche - let (max, min) = largest_niche + + // When picking the integer to use, we respect how the discriminants were written + // in the original rust code, rather than looking only at the bit pattern. + let (min_negative, max_positive): (i128, u128) = if discr_type.is_signed() { + necessary_discriminants.iter().copied().map(|val| discr_size.sign_extend(val)).fold( + (0_i128, 0_u128), + |(min, max), val| { + if let Ok(val) = u128::try_from(val) { + (min, max.max(val)) + } else { + (min.min(val), max) + } + }, + ) + } else { // We might have no inhabited variants, so pretend there's at least one. - .unwrap_or((0, 0)); - let (min_ity, signed) = discr_range_of_repr(min, max); //Integer::discr_range_of_repr(tcx, ty, &repr, min, max); + (0, necessary_discriminants.iter().copied().max().unwrap_or(0)) + }; + trace!(?min_negative, ?max_positive); + + let (min_ity, signed) = discr_range_of_repr( + RangeFrom { start: min_negative }, + RangeToInclusive { last: max_positive }, + ); //Integer::discr_range_of_repr(tcx, ty, &repr, min, max); let mut align = dl.aggregate_align; let mut max_repr_align = repr.align; @@ -929,13 +901,16 @@ impl LayoutCalculator { } } - let tag_mask = ity.size().unsigned_int_max(); + let tag_valid_range = { + let tag_size = ity.size(); + let tags = necessary_discriminants.into_iter().map(|d| tag_size.truncate(d)); + WrappingRange::smallest_range_containing(tags, tag_size) + // We might have no inhabited variants, so pretend there's at least one. + .unwrap_or(WrappingRange { start: 0, end: 0 }) + }; let tag = Scalar::Initialized { value: Primitive::Int(ity, signed), - valid_range: WrappingRange { - start: (min as u128 & tag_mask), - end: (max as u128 & tag_mask), - }, + valid_range: tag_valid_range, }; let mut abi = BackendRepr::Memory { sized: true }; diff --git a/compiler/rustc_middle/src/ty/layout.rs b/compiler/rustc_middle/src/ty/layout.rs index a8bae1efc2470..a2a9e7bea663c 100644 --- a/compiler/rustc_middle/src/ty/layout.rs +++ b/compiler/rustc_middle/src/ty/layout.rs @@ -68,9 +68,11 @@ impl abi::Integer { } /// Finds the appropriate Integer type and signedness for the given - /// signed discriminant range and `#[repr]` attribute. - /// N.B.: `u128` values above `i128::MAX` will be treated as signed, but - /// that shouldn't affect anything, other than maybe debuginfo. + /// discriminant range and `#[repr]` attribute. + /// + /// To represent the way the values were written in the rust source, min and max + /// are in different types. It's thus possible to pass in an unrepresentable range, + /// and the method will panic in those cases. /// /// This is the basis for computing the type of the *tag* of an enum (which can be smaller than /// the type of the *discriminant*, which is determined by [`ReprOptions::discr_type`]). @@ -78,15 +80,24 @@ impl abi::Integer { tcx: TyCtxt<'tcx>, ty: Ty<'tcx>, repr: &ReprOptions, - min: i128, - max: i128, + min_negative: i128, + max_positive: u128, ) -> (abi::Integer, bool) { + assert!( + min_negative >= 0 || max_positive <= i128::MAX.cast_unsigned(), + "No type can represent the full range of {min_negative}..={max_positive}", + ); + // Theoretically, negative values could be larger in unsigned representation // than the unsigned representation of the signed minimum. However, if there // are any negative values, the only valid unsigned representation is u128 // which can fit all i128 values, so the result remains unaffected. - let unsigned_fit = abi::Integer::fit_unsigned(cmp::max(min as u128, max as u128)); - let signed_fit = cmp::max(abi::Integer::fit_signed(min), abi::Integer::fit_signed(max)); + let unsigned_fit = + abi::Integer::fit_unsigned(cmp::max(min_negative.cast_unsigned(), max_positive)); + let signed_fit = cmp::max( + abi::Integer::fit_signed(min_negative), + abi::Integer::fit_signed(max_positive.cast_signed()), + ); if let Some(ity) = repr.int { let discr = abi::Integer::from_attr(&tcx, ity); diff --git a/compiler/rustc_ty_utils/src/layout.rs b/compiler/rustc_ty_utils/src/layout.rs index 8a3c8bd7b3ff3..c59a31ef2e504 100644 --- a/compiler/rustc_ty_utils/src/layout.rs +++ b/compiler/rustc_ty_utils/src/layout.rs @@ -1,3 +1,5 @@ +use std::range::{RangeFrom, RangeToInclusive}; + use hir::def_id::DefId; use rustc_abi as abi; use rustc_abi::Integer::{I8, I32}; @@ -713,8 +715,9 @@ fn layout_of_uncached<'tcx>( // UnsafeCell and UnsafePinned both disable niche optimizations let is_special_no_niche = def.is_unsafe_cell() || def.is_unsafe_pinned(); - let discr_range_of_repr = - |min, max| abi::Integer::discr_range_of_repr(tcx, ty, &def.repr(), min, max); + let discr_range_of_repr = |min: RangeFrom, max: RangeToInclusive| { + abi::Integer::discr_range_of_repr(tcx, ty, &def.repr(), min.start, max.last) + }; let discriminants_iter = || { def.is_enum() diff --git a/tests/ui/layout/enum-signedness.rs b/tests/ui/layout/enum-signedness.rs index f5f5fa0a9d311..c40955d39ef27 100644 --- a/tests/ui/layout/enum-signedness.rs +++ b/tests/ui/layout/enum-signedness.rs @@ -44,7 +44,7 @@ enum PositiveByteC { #[rustc_dump_layout(largest_niche)] #[repr(Rust)] enum Negative32BitRust { - //~^ ERROR: value: i32, valid_range: (..=0) | (2147483648..) + //~^ ERROR: value: i32, valid_range: 0..=2147483648 A = 0, B = i32::MIN as isize, } @@ -52,7 +52,7 @@ enum Negative32BitRust { #[rustc_dump_layout(largest_niche)] #[repr(C)] enum Negative32BitC { - //~^ ERROR: value: i32, valid_range: (..=0) | (2147483648..) + //~^ ERROR: value: i32, valid_range: 0..=2147483648 A = 0, B = i32::MIN as isize, } diff --git a/tests/ui/layout/enum-signedness.stderr b/tests/ui/layout/enum-signedness.stderr index b676473e9bb18..3e2b9fa69971f 100644 --- a/tests/ui/layout/enum-signedness.stderr +++ b/tests/ui/layout/enum-signedness.stderr @@ -67,13 +67,13 @@ error: largest_niche: Some(Niche { offset: Size(0 bytes), value: u32, valid_rang LL | enum PositiveByteC { | ^^^^^^^^^^^^^^^^^^ -error: largest_niche: Some(Niche { offset: Size(0 bytes), value: i32, valid_range: (..=0) | (2147483648..) }) +error: largest_niche: Some(Niche { offset: Size(0 bytes), value: i32, valid_range: 0..=2147483648 }) --> $DIR/enum-signedness.rs:46:1 | LL | enum Negative32BitRust { | ^^^^^^^^^^^^^^^^^^^^^^ -error: largest_niche: Some(Niche { offset: Size(0 bytes), value: i32, valid_range: (..=0) | (2147483648..) }) +error: largest_niche: Some(Niche { offset: Size(0 bytes), value: i32, valid_range: 0..=2147483648 }) --> $DIR/enum-signedness.rs:54:1 | LL | enum Negative32BitC { diff --git a/tests/ui/layout/enum-unusual-variants.rs b/tests/ui/layout/enum-unusual-variants.rs index 1ea78417f1013..a79ae0e1f32cc 100644 --- a/tests/ui/layout/enum-unusual-variants.rs +++ b/tests/ui/layout/enum-unusual-variants.rs @@ -20,7 +20,7 @@ enum With128Variants { #[rustc_dump_layout(largest_niche)] #[repr(i8)] enum With128VariantsI8 { - //~^ ERROR: value: i8, valid_range: (..=2) | (125..) + //~^ ERROR: value: i8, valid_range: 0..=127 _0 = 0, _1 = 1, _2 = 2, @@ -57,14 +57,17 @@ enum Symmetric { #[rustc_dump_layout(largest_niche)] #[repr(u8)] enum SymmetricU8 { - //~^ ERROR: value: u8, valid_range: (..=1) | (129..) + //~^ ERROR: value: u8, valid_range: 1..=129 A = 1, B = 129, } +// Note that to get this one to work it's essential that the `valid_range` is +// calculated on the *tag* values, because if calculated on the *discriminants* +// the best range is different because of the wider range of `isize`. #[rustc_dump_layout(largest_niche)] enum SymmetricSigned { - //~^ ERROR: value: i8, valid_range: (..=1) | (129..) + //~^ ERROR: value: i8, valid_range: 1..=129 A = -127, B = 1, } @@ -72,7 +75,7 @@ enum SymmetricSigned { #[rustc_dump_layout(largest_niche)] #[repr(i8)] enum SymmetricSignedI8 { - //~^ ERROR: value: i8, valid_range: (..=1) | (129..) + //~^ ERROR: value: i8, valid_range: 1..=129 A = -127, B = 1, } diff --git a/tests/ui/layout/enum-unusual-variants.stderr b/tests/ui/layout/enum-unusual-variants.stderr index 75e93a6a07e82..4eabd903a0337 100644 --- a/tests/ui/layout/enum-unusual-variants.stderr +++ b/tests/ui/layout/enum-unusual-variants.stderr @@ -4,7 +4,7 @@ error: largest_niche: Some(Niche { offset: Size(0 bytes), value: u8, valid_range LL | enum With128Variants { | ^^^^^^^^^^^^^^^^^^^^ -error: largest_niche: Some(Niche { offset: Size(0 bytes), value: i8, valid_range: (..=2) | (125..) }) +error: largest_niche: Some(Niche { offset: Size(0 bytes), value: i8, valid_range: 0..=127 }) --> $DIR/enum-unusual-variants.rs:22:1 | LL | enum With128VariantsI8 { @@ -22,20 +22,20 @@ error: largest_niche: Some(Niche { offset: Size(0 bytes), value: u8, valid_range LL | enum Symmetric { | ^^^^^^^^^^^^^^ -error: largest_niche: Some(Niche { offset: Size(0 bytes), value: u8, valid_range: (..=1) | (129..) }) +error: largest_niche: Some(Niche { offset: Size(0 bytes), value: u8, valid_range: 1..=129 }) --> $DIR/enum-unusual-variants.rs:59:1 | LL | enum SymmetricU8 { | ^^^^^^^^^^^^^^^^ -error: largest_niche: Some(Niche { offset: Size(0 bytes), value: i8, valid_range: (..=1) | (129..) }) - --> $DIR/enum-unusual-variants.rs:66:1 +error: largest_niche: Some(Niche { offset: Size(0 bytes), value: i8, valid_range: 1..=129 }) + --> $DIR/enum-unusual-variants.rs:69:1 | LL | enum SymmetricSigned { | ^^^^^^^^^^^^^^^^^^^^ -error: largest_niche: Some(Niche { offset: Size(0 bytes), value: i8, valid_range: (..=1) | (129..) }) - --> $DIR/enum-unusual-variants.rs:74:1 +error: largest_niche: Some(Niche { offset: Size(0 bytes), value: i8, valid_range: 1..=129 }) + --> $DIR/enum-unusual-variants.rs:77:1 | LL | enum SymmetricSignedI8 { | ^^^^^^^^^^^^^^^^^^^^^^ From 01954614ffc1eaa2d09b4838cbb54c6502fa1628 Mon Sep 17 00:00:00 2001 From: reucru01 Date: Fri, 27 Feb 2026 15:54:41 +0000 Subject: [PATCH 07/31] Adds `-Z implicit-sysroot-deps` boolean flag --- compiler/rustc_interface/src/tests.rs | 1 + compiler/rustc_session/src/options.rs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/compiler/rustc_interface/src/tests.rs b/compiler/rustc_interface/src/tests.rs index 22b643e74e582..4aa9f4d2eb1b0 100644 --- a/compiler/rustc_interface/src/tests.rs +++ b/compiler/rustc_interface/src/tests.rs @@ -828,6 +828,7 @@ fn test_unstable_options_tracking_hash() { tracked!(function_sections, Some(false)); tracked!(hint_mostly_unused, true); tracked!(human_readable_cgu_names, true); + tracked!(implicit_sysroot_deps, false); tracked!(incremental_ignore_spans, true); tracked!(indirect_branch_cs_prefix, true); tracked!(inline_mir, Some(true)); diff --git a/compiler/rustc_session/src/options.rs b/compiler/rustc_session/src/options.rs index 5b71c0435185a..ecfff0e64baf1 100644 --- a/compiler/rustc_session/src/options.rs +++ b/compiler/rustc_session/src/options.rs @@ -2531,6 +2531,8 @@ options! { "display unnamed regions as `'`, using a non-ident unique id (default: no)"), ignore_directory_in_diagnostics_source_blocks: Vec = (Vec::new(), parse_string_push, [UNTRACKED], "do not display the source code block in diagnostics for files in the directory"), + implicit_sysroot_deps: bool = (true, parse_bool, [TRACKED], + "allows rust to search sysroot for a crate's dependencies (default: yes)"), incremental_ignore_spans: bool = (false, parse_bool, [TRACKED], "ignore spans during ICH computation -- used for testing (default: no)"), incremental_info: bool = (false, parse_bool, [UNTRACKED], From 7b0ecd33742457979b5f5d2743709f89e2611338 Mon Sep 17 00:00:00 2001 From: reucru01 Date: Tue, 3 Mar 2026 11:31:03 +0000 Subject: [PATCH 08/31] Implements `implicit-sysroot-deps=false` When `-Z implicit-sysroot-deps=false`, the sysroot is not added to the list of search paths for resolving top level dependencies. --- compiler/rustc_session/src/filesearch.rs | 16 ++++++++++++++-- compiler/rustc_session/src/session.rs | 15 ++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/compiler/rustc_session/src/filesearch.rs b/compiler/rustc_session/src/filesearch.rs index bf1e1f5f12811..19a0e988f2918 100644 --- a/compiler/rustc_session/src/filesearch.rs +++ b/compiler/rustc_session/src/filesearch.rs @@ -12,6 +12,7 @@ use crate::search_paths::{PathKind, SearchPath}; pub struct FileSearch { cli_search_paths: Vec, tlib_path: SearchPath, + use_implicit_sysroot_deps: bool, } impl FileSearch { @@ -20,16 +21,27 @@ impl FileSearch { } pub fn search_paths<'b>(&'b self, kind: PathKind) -> impl Iterator { + //If the crate is `PathKind::Crate` (a top level dependency) + //and `-Z implicit-sysroot-deps=false`, then don't include the sysroot in the search paths. + let maybe_tlib = (self.use_implicit_sysroot_deps || !kind.matches(PathKind::Crate)) + .then_some(&self.tlib_path); + self.cli_search_paths .iter() .filter(move |sp| sp.kind.matches(kind)) - .chain(std::iter::once(&self.tlib_path)) + .chain(maybe_tlib.into_iter()) } - pub fn new(cli_search_paths: &[SearchPath], tlib_path: &SearchPath, target: &Target) -> Self { + pub fn new( + cli_search_paths: &[SearchPath], + tlib_path: &SearchPath, + target: &Target, + use_implicit_sysroot_deps: bool, + ) -> Self { let this = FileSearch { cli_search_paths: cli_search_paths.to_owned(), tlib_path: tlib_path.clone(), + use_implicit_sysroot_deps, }; this.refine(&["lib", &target.staticlib_prefix, &target.dll_prefix]) } diff --git a/compiler/rustc_session/src/session.rs b/compiler/rustc_session/src/session.rs index 0cf75fe1e2280..6a8a9392ac838 100644 --- a/compiler/rustc_session/src/session.rs +++ b/compiler/rustc_session/src/session.rs @@ -1336,9 +1336,18 @@ pub fn build_session( }); let asm_arch = if target.allow_asm { InlineAsmArch::from_arch(&target.arch) } else { None }; - let target_filesearch = - filesearch::FileSearch::new(&sopts.search_paths, &target_tlib_path, &target); - let host_filesearch = filesearch::FileSearch::new(&sopts.search_paths, &host_tlib_path, &host); + let target_filesearch = filesearch::FileSearch::new( + &sopts.search_paths, + &target_tlib_path, + &target, + sopts.unstable_opts.implicit_sysroot_deps, + ); + let host_filesearch = filesearch::FileSearch::new( + &sopts.search_paths, + &host_tlib_path, + &host, + sopts.unstable_opts.implicit_sysroot_deps, + ); let timings = TimingSectionHandler::new(sopts.json_timings); From 56c4f45c077bb1340303621ade8f228c64dad3a8 Mon Sep 17 00:00:00 2001 From: reucru01 Date: Tue, 3 Mar 2026 11:37:48 +0000 Subject: [PATCH 09/31] Adds tests for `implicit-sysroot-deps=false One UI test that fails to find crate `std` as it does not look in the sysroot. One UI test that which only transitively depends on std, and thus it is searched for in the sysroot. --- tests/ui/crate-loading/auxiliary/crate-dep-std.rs | 3 +++ .../crate-loading/no-implicit-sysroot-deps-pass.rs | 13 +++++++++++++ tests/ui/crate-loading/no-implicit-sysroot-deps.rs | 9 +++++++++ .../crate-loading/no-implicit-sysroot-deps.stderr | 9 +++++++++ 4 files changed, 34 insertions(+) create mode 100644 tests/ui/crate-loading/auxiliary/crate-dep-std.rs create mode 100644 tests/ui/crate-loading/no-implicit-sysroot-deps-pass.rs create mode 100644 tests/ui/crate-loading/no-implicit-sysroot-deps.rs create mode 100644 tests/ui/crate-loading/no-implicit-sysroot-deps.stderr diff --git a/tests/ui/crate-loading/auxiliary/crate-dep-std.rs b/tests/ui/crate-loading/auxiliary/crate-dep-std.rs new file mode 100644 index 0000000000000..5eec10bfc3e4a --- /dev/null +++ b/tests/ui/crate-loading/auxiliary/crate-dep-std.rs @@ -0,0 +1,3 @@ +pub fn bar() { + println!("I depend on std."); +} diff --git a/tests/ui/crate-loading/no-implicit-sysroot-deps-pass.rs b/tests/ui/crate-loading/no-implicit-sysroot-deps-pass.rs new file mode 100644 index 0000000000000..7a02dec8cc388 --- /dev/null +++ b/tests/ui/crate-loading/no-implicit-sysroot-deps-pass.rs @@ -0,0 +1,13 @@ +//@ check-pass +//@ aux-build:crate-dep-std.rs +//@ compile-flags: --crate-type=lib -Zimplicit-sysroot-deps=false -Cpanic=abort + +#![feature(no_core)] +#![no_std] +#![no_core] + +extern crate crate_dep_std as foo; +use foo::bar; +pub fn bark() { + bar(); +} diff --git a/tests/ui/crate-loading/no-implicit-sysroot-deps.rs b/tests/ui/crate-loading/no-implicit-sysroot-deps.rs new file mode 100644 index 0000000000000..71bee6794b4c4 --- /dev/null +++ b/tests/ui/crate-loading/no-implicit-sysroot-deps.rs @@ -0,0 +1,9 @@ +//~ ERROR can't find crate for `std` +//~| NOTE can't find crate +//~| NOTE target may not be installed +//~| HELP consider building the standard library from source with `cargo build -Zbuild-std` +//~| HELP consider downloading the target with + +//@ compile-flags: --target x86_64-unknown-linux-gnu -Z implicit-sysroot-deps=false +//@ needs-llvm-components: x86 +fn main() {} diff --git a/tests/ui/crate-loading/no-implicit-sysroot-deps.stderr b/tests/ui/crate-loading/no-implicit-sysroot-deps.stderr new file mode 100644 index 0000000000000..688664cad9596 --- /dev/null +++ b/tests/ui/crate-loading/no-implicit-sysroot-deps.stderr @@ -0,0 +1,9 @@ +error[E0463]: can't find crate for `std` + | + = note: the `x86_64-unknown-linux-gnu` target may not be installed + = help: consider downloading the target with `rustup target add x86_64-unknown-linux-gnu` + = help: consider building the standard library from source with `cargo build -Zbuild-std` + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0463`. From fd9a0148c9060f54db77c26bde5ee7b4484f41e1 Mon Sep 17 00:00:00 2001 From: reucru01 Date: Thu, 5 Mar 2026 17:49:48 +0000 Subject: [PATCH 10/31] Adds more tests for `-Z implicit-syroot-deps=false` 1) Depending transitively on a lib in the sysroot resolves fine with `-Zimplicit-sysroot-deps=false` 2) Depending directly on a lib in the sysroot does not resolve implicitly with `-Zimplicit-sysroot-deps=false` --- tests/run-make/implicit-sysroot-deps/bar.rs | 4 +++ tests/run-make/implicit-sysroot-deps/baz.rs | 3 ++ tests/run-make/implicit-sysroot-deps/foo.rs | 4 +++ tests/run-make/implicit-sysroot-deps/rmake.rs | 34 +++++++++++++++++++ 4 files changed, 45 insertions(+) create mode 100644 tests/run-make/implicit-sysroot-deps/bar.rs create mode 100644 tests/run-make/implicit-sysroot-deps/baz.rs create mode 100644 tests/run-make/implicit-sysroot-deps/foo.rs create mode 100644 tests/run-make/implicit-sysroot-deps/rmake.rs diff --git a/tests/run-make/implicit-sysroot-deps/bar.rs b/tests/run-make/implicit-sysroot-deps/bar.rs new file mode 100644 index 0000000000000..ececc69fe9514 --- /dev/null +++ b/tests/run-make/implicit-sysroot-deps/bar.rs @@ -0,0 +1,4 @@ +#![feature(no_core)] +#![no_core] +#![no_std] +extern crate baz; diff --git a/tests/run-make/implicit-sysroot-deps/baz.rs b/tests/run-make/implicit-sysroot-deps/baz.rs new file mode 100644 index 0000000000000..ef13d56b7d4f0 --- /dev/null +++ b/tests/run-make/implicit-sysroot-deps/baz.rs @@ -0,0 +1,3 @@ +#![feature(no_core)] +#![no_core] +#![no_std] diff --git a/tests/run-make/implicit-sysroot-deps/foo.rs b/tests/run-make/implicit-sysroot-deps/foo.rs new file mode 100644 index 0000000000000..90d13bd02b1db --- /dev/null +++ b/tests/run-make/implicit-sysroot-deps/foo.rs @@ -0,0 +1,4 @@ +#![feature(no_core)] +#![no_core] +#![no_std] +extern crate bar; diff --git a/tests/run-make/implicit-sysroot-deps/rmake.rs b/tests/run-make/implicit-sysroot-deps/rmake.rs new file mode 100644 index 0000000000000..8192e96852131 --- /dev/null +++ b/tests/run-make/implicit-sysroot-deps/rmake.rs @@ -0,0 +1,34 @@ +use run_make_support::rfs::create_dir_all; +use run_make_support::{rustc, target}; + +fn main() { + // Create test sysroot + let test_sysroot = format!("./testsysroot/lib/rustlib/{}/lib/", target()); + create_dir_all(&test_sysroot); + + // Layout: + // - Foo depends directly on Bar + // - Bar depends directly on Baz + // - Baz can be found in the sysroot. + + // 1) Depending transitively on a lib in the sysroot resolves + // fine with `-Zimplicit-sysroot-deps=false` + rustc().input("baz.rs").crate_type("lib").out_dir(test_sysroot).run(); + rustc().input("bar.rs").crate_type("lib").sysroot("./testsysroot").run(); + rustc() + .input("foo.rs") + .crate_type("lib") + .extern_("bar", "libbar.rlib") + .sysroot("./testsysroot") + .arg("-Zimplicit-sysroot-deps=false") + .run(); + + // 2) Depending directly on a lib in the sysroot does not resolve + // implicitly with `-Zimplicit-sysroot-deps=false` + rustc() + .input("bar.rs") + .crate_type("lib") + .sysroot("./testsysroot") + .arg("-Zimplicit-sysroot-deps=false") + .run_fail(); +} From 0ac1461b6d52b25892a290c9a9ecadedbdde9ddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ber=C3=A1nek?= Date: Mon, 27 Jul 2026 08:58:18 +0200 Subject: [PATCH 11/31] Show jobs where a given test was executed in `test-dashboard` --- src/ci/citool/Cargo.lock | 27 ++++++---- src/ci/citool/src/test_dashboard.rs | 57 +++++++++++++++++++--- src/ci/citool/templates/test_group.askama | 8 +-- src/ci/citool/templates/test_suites.askama | 26 +++++++++- 4 files changed, 95 insertions(+), 23 deletions(-) diff --git a/src/ci/citool/Cargo.lock b/src/ci/citool/Cargo.lock index a208de47256ca..185eecbde1a02 100644 --- a/src/ci/citool/Cargo.lock +++ b/src/ci/citool/Cargo.lock @@ -66,9 +66,9 @@ checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] name = "askama" -version = "0.15.4" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08e1676b346cadfec169374f949d7490fd80a24193d37d2afce0c047cf695e57" +checksum = "f1bf825125edd887a019d0a3a837dcc5499a68b0d034cc3eb594070c3e18addc" dependencies = [ "askama_macros", "itoa", @@ -79,12 +79,13 @@ dependencies = [ [[package]] name = "askama_derive" -version = "0.15.4" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7661ff56517787343f376f75db037426facd7c8d3049cef8911f1e75016f3a37" +checksum = "e1c7065972a130eafa84215f21352ae15b4a7393da48c1f5e103904490736738" dependencies = [ "askama_parser", "basic-toml", + "glob", "memchr", "proc-macro2", "quote", @@ -96,18 +97,18 @@ dependencies = [ [[package]] name = "askama_macros" -version = "0.15.4" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713ee4dbfd1eb719c2dab859465b01fa1d21cb566684614a713a6b7a99a4e47b" +checksum = "0e23b1d2c4bd39a41971f6124cef4cc6fd0540913ecb90919b69ab3bbe44ae1a" dependencies = [ "askama_derive", ] [[package]] name = "askama_parser" -version = "0.15.4" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d62d674238a526418b30c0def480d5beadb9d8964e7f38d635b03bf639c704c" +checksum = "7db09fde9143e7ac4513358fb32ee32847125b63b18ea715afd487956da715da" dependencies = [ "rustc-hash", "serde", @@ -380,6 +381,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + [[package]] name = "glob-match" version = "0.2.1" @@ -1105,9 +1112,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.6" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ "memchr", ] diff --git a/src/ci/citool/src/test_dashboard.rs b/src/ci/citool/src/test_dashboard.rs index c9de38852e5a8..bfcb2ba790e3e 100644 --- a/src/ci/citool/src/test_dashboard.rs +++ b/src/ci/citool/src/test_dashboard.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::fs::File; use std::io::BufWriter; use std::path::{Path, PathBuf}; @@ -17,12 +17,14 @@ pub fn generate_test_dashboard( output_dir: &Path, ) -> anyhow::Result<()> { let metrics = download_auto_job_metrics(&db, None, current)?; - let suites = gather_test_suites(&metrics); + let mut suites = gather_test_suites(&metrics); std::fs::create_dir_all(output_dir)?; + let jobsets = assign_jobsets(&mut suites); + let test_count = suites.test_count(); - write_page(output_dir, "index.html", &TestSuitesPage { suites, test_count })?; + write_page(output_dir, "index.html", &TestSuitesPage { suites, test_count, jobsets })?; Ok(()) } @@ -33,6 +35,45 @@ fn write_page(dir: &Path, name: &str, template: &T) -> anyhow::Resu Ok(()) } +struct JobSets { + sets: Vec<(u32, Vec)>, +} + +fn assign_jobsets(suites: &mut TestSuites) -> JobSets { + let mut jobsets: HashMap, u32> = HashMap::new(); + + fn visit(jobsets: &mut HashMap, u32>, group: &mut TestGroup) { + for (_, test) in &mut group.root_tests { + for (_, results) in &mut test.revisions { + let mut jobset: HashSet = HashSet::new(); + for test in &results.passed { + jobset.insert(test.job.to_string()); + } + let mut jobset: Vec = jobset.into_iter().collect(); + jobset.sort(); + + let jobset_count = jobsets.len() as u32; + let jobset_id = jobsets.entry(jobset).or_insert_with(|| jobset_count); + results.passed_jobset = Some(*jobset_id); + } + } + for (_, group) in &mut group.groups { + visit(jobsets, group); + } + } + for suite in &mut suites.suites { + visit(&mut jobsets, &mut suite.group); + } + + let mut jobsets: Vec<(u32, Vec)> = jobsets.into_iter().map(|(k, v)| (v, k)).collect(); + jobsets.sort_unstable_by(|a, b| a.0.cmp(&b.0)); + for (_, set) in &mut jobsets { + set.sort_unstable(); + } + + JobSets { sets: jobsets } +} + fn gather_test_suites(job_metrics: &HashMap) -> TestSuites<'_> { struct CoarseTestSuite<'a> { tests: BTreeMap>, @@ -68,10 +109,9 @@ fn gather_test_suites(job_metrics: &HashMap) -> TestSuites< .tests .entry(test_name.clone()) .or_insert_with(|| Test { revisions: Default::default() }); - let variant_entry = test_entry - .revisions - .entry(variant_name) - .or_insert_with(|| TestResults { passed: vec![], ignored: vec![] }); + let variant_entry = test_entry.revisions.entry(variant_name).or_insert_with(|| { + TestResults { passed: vec![], passed_jobset: None, ignored: vec![] } + }); match test.outcome { TestOutcome::Passed => { @@ -161,6 +201,8 @@ struct TestSuite<'a> { struct TestResults<'a> { passed: Vec>, + /// An id representing a set of jobs on which this test has passed. + passed_jobset: Option, ignored: Vec>, } @@ -213,4 +255,5 @@ impl<'a> TestGroup<'a> { struct TestSuitesPage<'a> { suites: TestSuites<'a>, test_count: u64, + jobsets: JobSets, } diff --git a/src/ci/citool/templates/test_group.askama b/src/ci/citool/templates/test_group.askama index 1a72c47d3788a..2e9a8327c8537 100644 --- a/src/ci/citool/templates/test_group.askama +++ b/src/ci/citool/templates/test_group.askama @@ -1,5 +1,5 @@ -{% macro test_result(r) -%} -passed: {{ r.passed.len() }}, ignored: {{ r.ignored.len() }} +{% macro test_result(r, jobset) -%} +passed: {{ r.passed.len() }}, ignored: {{ r.ignored.len() }} {%- endmacro %}
  • @@ -24,12 +24,12 @@ passed: {{ r.passed.len() }}, ignored: {{ r.ignored.len() }} {% for (name, test) in root_tests %}
  • {% if let Some(result) = test.single_test() %} - {{ name }} ({% call test_result(result) %}{% endcall %}) + {{ name }} ({% call test_result(result, result.passed_jobset.as_ref().unwrap()) %}{% endcall %}) {% else %} {{ name }} ({{ test.revisions.len() }} revision{{ test.revisions.len() | pluralize }})
      {% for (revision, result) in test.revisions %} -
    • #{{ revision }} ({% call test_result(result) %}{% endcall %})
    • +
    • #{{ revision }} ({% call test_result(result, result.passed_jobset.as_ref().unwrap()) %}{% endcall %})
    • {% endfor %}
    {% endif %} diff --git a/src/ci/citool/templates/test_suites.askama b/src/ci/citool/templates/test_suites.askama index 4997f6a3f1c9a..59ba2b7e86916 100644 --- a/src/ci/citool/templates/test_suites.askama +++ b/src/ci/citool/templates/test_suites.askama @@ -20,11 +20,26 @@ the count includes all combinations of "stage" x "target" x "CI job where the te -
      +
        {% for suite in suites.suites %} {{ suite.group|safe }} {% endfor %}
      +
      +

      Job sets

      +
      + {% for (id, jobs) in jobsets.sets %} + {% if !jobs.is_empty() %} +
      J{{ id }}
      +
        + {% for job in jobs %} +
      • {{ job }}
      • + {% endfor %} +
      + {% endif %} + {% endfor %} +
      +
      {% endblock %} @@ -34,6 +49,9 @@ h1 { color: #333333; margin-bottom: 30px; } +a:visited { + color: blue; +} .summary { display: flex; @@ -55,10 +73,14 @@ ul { padding-left: 0; } -li { +.tests li { list-style: none; padding-left: 20px; } +.jobsets li { + margin-left: 30px; +} + summary { margin-bottom: 5px; padding: 6px; From e62c86f41cda5c96cbfb5a47465f0abe925280d3 Mon Sep 17 00:00:00 2001 From: zakrad <49591476+zakrad@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:33:04 +0330 Subject: [PATCH 12/31] Add regression test for closure in array-length const generic A closure inside an array-length constant in a generic async function used to ICE with "expected type of closure to be a closure"; it now emits ordinary errors. Add a regression test locking that in. --- .../closure-in-array-len-ice-119316.rs | 11 +++++++++++ .../closure-in-array-len-ice-119316.stderr | 14 ++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 tests/ui/const-generics/generic_const_exprs/closure-in-array-len-ice-119316.rs create mode 100644 tests/ui/const-generics/generic_const_exprs/closure-in-array-len-ice-119316.stderr diff --git a/tests/ui/const-generics/generic_const_exprs/closure-in-array-len-ice-119316.rs b/tests/ui/const-generics/generic_const_exprs/closure-in-array-len-ice-119316.rs new file mode 100644 index 0000000000000..bb7597afaa47c --- /dev/null +++ b/tests/ui/const-generics/generic_const_exprs/closure-in-array-len-ice-119316.rs @@ -0,0 +1,11 @@ +//@ edition: 2018 +// regression test for #119316 +#![feature(generic_const_exprs)] +#![allow(incomplete_features)] + +async fn foo<'a>() { + let _data = &mut [0u8; { N + (|| 42)() }]; + //~^ ERROR cannot find value `N` in this scope +} + +fn main() {} diff --git a/tests/ui/const-generics/generic_const_exprs/closure-in-array-len-ice-119316.stderr b/tests/ui/const-generics/generic_const_exprs/closure-in-array-len-ice-119316.stderr new file mode 100644 index 0000000000000..a98f9a29b0707 --- /dev/null +++ b/tests/ui/const-generics/generic_const_exprs/closure-in-array-len-ice-119316.stderr @@ -0,0 +1,14 @@ +error[E0425]: cannot find value `N` in this scope + --> $DIR/closure-in-array-len-ice-119316.rs:7:30 + | +LL | let _data = &mut [0u8; { N + (|| 42)() }]; + | ^ not found in this scope + | +help: you might be missing a const parameter + | +LL | async fn foo<'a, const N: /* Type */>() { + | +++++++++++++++++++++ + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0425`. From 1544f42928fd89d2d33012db0221941810de6dd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ber=C3=A1nek?= Date: Tue, 21 Jul 2026 08:12:44 +0200 Subject: [PATCH 13/31] Add `./x test std-semver-check` test command --- src/bootstrap/src/core/build_steps/test.rs | 89 ++++++++++++++++++++++ src/bootstrap/src/core/builder/mod.rs | 1 + src/bootstrap/src/core/download.rs | 39 ++++++++-- 3 files changed, 121 insertions(+), 8 deletions(-) diff --git a/src/bootstrap/src/core/build_steps/test.rs b/src/bootstrap/src/core/build_steps/test.rs index 57d7518792926..54992d2ae5b69 100644 --- a/src/bootstrap/src/core/build_steps/test.rs +++ b/src/bootstrap/src/core/build_steps/test.rs @@ -13,6 +13,8 @@ use std::path::{Path, PathBuf}; use std::process::Command; use std::{env, fs, iter}; +use build_helper::git::get_closest_upstream_commit; + use crate::core::build_steps::compile::{ArtifactKeepMode, Std, run_cargo}; use crate::core::build_steps::doc::{DocumentationFormat, prepare_doc_compiler}; use crate::core::build_steps::gcc::{Gcc, GccTargetPair, add_cg_gcc_cargo_flags}; @@ -4613,3 +4615,90 @@ impl CommandLineStep for RemoteTestClientTests { ); } } + +fn check_if_cargo_semver_checks_is_installed(builder: &Builder<'_>) -> bool { + command("cargo") + .allow_failure() + .arg("semver-checks") + .arg("--version") + // Cache the output to avoid running this command more than once (per builder). + .cached() + .run_capture_stdout(builder) + .is_success() +} + +/// Run cargo-semver-checks on the standard library and compare its API +/// versus a previous baseline, using rustdoc JSON data. +/// +/// Fails if a semver-breaking change is detected. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct StdSemverCheck { + build_compiler: Compiler, + target: TargetSelection, + /// The baseline commit that we are comparing the local stdlib API against. + commit: String, +} + +impl Step for StdSemverCheck { + type Output = (); + + fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> { + run.alias("std-semver-check") + } + + fn make_run(run: RunConfig<'_>) { + if !check_if_cargo_semver_checks_is_installed(run.builder) { + panic!("cargo-semver-checks was not found, please install it"); + } + + let baseline_commit = match get_closest_upstream_commit( + Some(&run.builder.config.src), + &run.builder.config.git_config(), + run.builder.config.ci_env, + ) { + Ok(Some(commit)) => commit, + Ok(None) => { + panic!("No baseline parent commit found for std-semver-check"); + } + Err(error) => { + panic!("Cannot get baseline parent commit for std-semver-check: {error:?}"); + } + }; + + run.builder.ensure(Self { + build_compiler: run.builder.compiler_for_std(run.builder.top_stage), + target: run.target, + commit: baseline_commit, + }); + } + + fn run(self, builder: &Builder<'_>) { + let Some(docs_dir) = builder.config.download_std_json_docs(self.target, &self.commit) + else { + return; + }; + + let directory = builder.ensure(crate::core::build_steps::doc::Std::from_build_compiler( + self.build_compiler, + self.target, + DocumentationFormat::Json, + )); + let baseline_dir = docs_dir.join("share").join("doc").join("rust").join("json"); + + for library in ["core", "alloc", "std"] { + println!("Checking semver compatibility of {library}"); + let mut cmd = command("cargo"); + cmd.arg("semver-checks") + .arg("-Z") + .arg("unstable-options") + .arg("--stability-aware") + .arg("--release-type") + .arg("minor") + .arg("--current-rustdoc") + .arg(directory.join(format!("{library}.json"))) + .arg("--baseline-rustdoc") + .arg(baseline_dir.join(format!("{library}.json"))); + cmd.run(builder); + } + } +} diff --git a/src/bootstrap/src/core/builder/mod.rs b/src/bootstrap/src/core/builder/mod.rs index 50188887a5314..ce476883c8839 100644 --- a/src/bootstrap/src/core/builder/mod.rs +++ b/src/bootstrap/src/core/builder/mod.rs @@ -974,6 +974,7 @@ impl<'a> Builder<'a> { test::RunMake, test::RunMakeCargo, test::BuildStd, + test::StdSemverCheck, test::IntrinsicTest, ), Kind::Miri => describe!(test::Crate), diff --git a/src/bootstrap/src/core/download.rs b/src/bootstrap/src/core/download.rs index e6d55fd530adb..d3bc5718b171e 100644 --- a/src/bootstrap/src/core/download.rs +++ b/src/bootstrap/src/core/download.rs @@ -173,6 +173,30 @@ impl Config { ); } + pub(crate) fn download_std_json_docs( + &self, + target: TargetSelection, + commit: &str, + ) -> Option { + if self.dry_run() { + return None; + } + + self.do_if_verbose(|| println!("using downloaded std json docs from CI (commit {commit})")); + + let version = self.artifact_version_part(commit); + download_component( + DownloadContext::from(self), + &self.out, + DownloadSource::CI, + format!("rust-docs-json-{version}-{target}.tar.xz"), + "rust-docs-json-preview", + // When using DownloadSource::CI, the key is assumed to end with -llvm-assertions + &format!("{commit}-{}", self.llvm_assertions), + "ci-docs-json", + ) + } + fn download_toolchain( &self, version: &str, @@ -785,11 +809,11 @@ fn download_component<'a>( prefix: &str, key: &str, destination: &str, -) { +) -> Option { let dwn_ctx = dwn_ctx.as_ref(); if dwn_ctx.exec_ctx.dry_run() { - return; + return None; } let cache_dst = @@ -834,8 +858,7 @@ fn download_component<'a>( let sha256 = dwn_ctx.stage0_metadata.checksums_sha256.get(&url).expect(&error); if tarball.exists() { if verify(dwn_ctx.exec_ctx, &tarball, sha256) { - unpack(dwn_ctx.exec_ctx, &tarball, &bin_root, prefix); - return; + return Some(unpack(dwn_ctx.exec_ctx, &tarball, &bin_root, prefix)); } else { dwn_ctx.exec_ctx.do_if_verbose(|| { println!( @@ -848,8 +871,7 @@ fn download_component<'a>( } Some(sha256) } else if tarball.exists() { - unpack(dwn_ctx.exec_ctx, &tarball, &bin_root, prefix); - return; + return Some(unpack(dwn_ctx.exec_ctx, &tarball, &bin_root, prefix)); } else { None }; @@ -872,7 +894,7 @@ download-rustc = false panic!("failed to verify {}", tarball.display()); } - unpack(dwn_ctx.exec_ctx, &tarball, &bin_root, prefix); + Some(unpack(dwn_ctx.exec_ctx, &tarball, &bin_root, prefix)) } pub(crate) fn verify(exec_ctx: &ExecutionContext, path: &Path, expected: &str) -> bool { @@ -916,7 +938,7 @@ pub(crate) fn verify(exec_ctx: &ExecutionContext, path: &Path, expected: &str) - verified } -fn unpack(exec_ctx: &ExecutionContext, tarball: &Path, dst: &Path, pattern: &str) { +fn unpack(exec_ctx: &ExecutionContext, tarball: &Path, dst: &Path, pattern: &str) -> PathBuf { eprintln!("extracting {} to {}", tarball.display(), dst.display()); if !dst.exists() { t!(fs::create_dir_all(dst)); @@ -979,6 +1001,7 @@ fn unpack(exec_ctx: &ExecutionContext, tarball: &Path, dst: &Path, pattern: &str if dst_dir.exists() { t!(fs::remove_dir_all(&dst_dir), format!("failed to remove {}", dst_dir.display())); } + dst.to_path_buf() } fn download_file<'a>( From 73ba9630c0560a07a5ea17426bcd131b295ac8e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ber=C3=A1nek?= Date: Mon, 27 Jul 2026 22:52:20 +0200 Subject: [PATCH 14/31] Add CLI snapshot test for semver check step --- src/bootstrap/src/core/build_steps/test.rs | 2 +- .../builder/cli_paths/snapshots/x_test_semver_check.snap | 7 +++++++ src/bootstrap/src/core/builder/cli_paths/tests.rs | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 src/bootstrap/src/core/builder/cli_paths/snapshots/x_test_semver_check.snap diff --git a/src/bootstrap/src/core/build_steps/test.rs b/src/bootstrap/src/core/build_steps/test.rs index 54992d2ae5b69..a2184f75487fe 100644 --- a/src/bootstrap/src/core/build_steps/test.rs +++ b/src/bootstrap/src/core/build_steps/test.rs @@ -4639,7 +4639,7 @@ pub struct StdSemverCheck { commit: String, } -impl Step for StdSemverCheck { +impl CommandLineStep for StdSemverCheck { type Output = (); fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> { diff --git a/src/bootstrap/src/core/builder/cli_paths/snapshots/x_test_semver_check.snap b/src/bootstrap/src/core/builder/cli_paths/snapshots/x_test_semver_check.snap new file mode 100644 index 0000000000000..0a2720e55b785 --- /dev/null +++ b/src/bootstrap/src/core/builder/cli_paths/snapshots/x_test_semver_check.snap @@ -0,0 +1,7 @@ +--- +source: src/bootstrap/src/core/builder/cli_paths/tests.rs +expression: test std-semver-check +--- +[Test] test::StdSemverCheck + targets: [aarch64-unknown-linux-gnu] + - Set({test::std-semver-check}) diff --git a/src/bootstrap/src/core/builder/cli_paths/tests.rs b/src/bootstrap/src/core/builder/cli_paths/tests.rs index 465a370ad69f8..45655b71e611a 100644 --- a/src/bootstrap/src/core/builder/cli_paths/tests.rs +++ b/src/bootstrap/src/core/builder/cli_paths/tests.rs @@ -170,6 +170,7 @@ declare_tests!( (x_test_librustdoc_rustdoc_html, "test librustdoc rustdoc-html"), (x_test_rustdoc, "test rustdoc"), (x_test_rustdoc_html, "test rustdoc-html"), + (x_test_semver_check, "test std-semver-check"), (x_test_skip_coverage, "test --skip=coverage"), (x_test_skip_coverage_map, "test --skip=coverage-map"), (x_test_skip_coverage_run, "test --skip=coverage-run"), From b2ed4e878fc9e3d4c72358ef6d0bbbdde36ad324 Mon Sep 17 00:00:00 2001 From: Adam Gemmell Date: Tue, 28 Jul 2026 15:18:36 +0100 Subject: [PATCH 15/31] Review updates --- compiler/rustc_session/src/filesearch.rs | 8 ++++---- tests/run-make/implicit-sysroot-deps/rmake.rs | 10 +++++++--- .../ui/crate-loading/no-implicit-sysroot-deps-pass.rs | 3 +++ tests/ui/crate-loading/no-implicit-sysroot-deps.rs | 5 +++++ 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/compiler/rustc_session/src/filesearch.rs b/compiler/rustc_session/src/filesearch.rs index 19a0e988f2918..b2290f92aae12 100644 --- a/compiler/rustc_session/src/filesearch.rs +++ b/compiler/rustc_session/src/filesearch.rs @@ -21,10 +21,10 @@ impl FileSearch { } pub fn search_paths<'b>(&'b self, kind: PathKind) -> impl Iterator { - //If the crate is `PathKind::Crate` (a top level dependency) - //and `-Z implicit-sysroot-deps=false`, then don't include the sysroot in the search paths. - let maybe_tlib = (self.use_implicit_sysroot_deps || !kind.matches(PathKind::Crate)) - .then_some(&self.tlib_path); + // If the crate is `PathKind::Crate` (a top level dependency) + // and `-Z implicit-sysroot-deps=false`, then don't include the sysroot in the search paths. + let exclude_sysroot = kind.matches(PathKind::Crate) && !self.use_implicit_sysroot_deps; + let maybe_tlib = (!exclude_sysroot).then_some(&self.tlib_path); self.cli_search_paths .iter() diff --git a/tests/run-make/implicit-sysroot-deps/rmake.rs b/tests/run-make/implicit-sysroot-deps/rmake.rs index 8192e96852131..a748bda4db271 100644 --- a/tests/run-make/implicit-sysroot-deps/rmake.rs +++ b/tests/run-make/implicit-sysroot-deps/rmake.rs @@ -1,5 +1,8 @@ use run_make_support::rfs::create_dir_all; -use run_make_support::{rustc, target}; +use run_make_support::{rust_lib_name, rustc, target}; + +// Tests `-Zimplicit-sysroot-deps=false` with arbitrary crates passed via `--extern` +// See `tests/ui/crate-loading` for tests with the standard library fn main() { // Create test sysroot @@ -18,7 +21,7 @@ fn main() { rustc() .input("foo.rs") .crate_type("lib") - .extern_("bar", "libbar.rlib") + .extern_("bar", rust_lib_name("bar")) .sysroot("./testsysroot") .arg("-Zimplicit-sysroot-deps=false") .run(); @@ -30,5 +33,6 @@ fn main() { .crate_type("lib") .sysroot("./testsysroot") .arg("-Zimplicit-sysroot-deps=false") - .run_fail(); + .run_fail() + .assert_stderr_contains("can't find crate for `baz`"); } diff --git a/tests/ui/crate-loading/no-implicit-sysroot-deps-pass.rs b/tests/ui/crate-loading/no-implicit-sysroot-deps-pass.rs index 7a02dec8cc388..a2ce18597e0b8 100644 --- a/tests/ui/crate-loading/no-implicit-sysroot-deps-pass.rs +++ b/tests/ui/crate-loading/no-implicit-sysroot-deps-pass.rs @@ -2,6 +2,9 @@ //@ aux-build:crate-dep-std.rs //@ compile-flags: --crate-type=lib -Zimplicit-sysroot-deps=false -Cpanic=abort +// This test ensures that `-Zimplicit-sysroot-deps=false` allows loading transitive +// dependencies from the sysroot when required. + #![feature(no_core)] #![no_std] #![no_core] diff --git a/tests/ui/crate-loading/no-implicit-sysroot-deps.rs b/tests/ui/crate-loading/no-implicit-sysroot-deps.rs index 71bee6794b4c4..9a280cc681571 100644 --- a/tests/ui/crate-loading/no-implicit-sysroot-deps.rs +++ b/tests/ui/crate-loading/no-implicit-sysroot-deps.rs @@ -6,4 +6,9 @@ //@ compile-flags: --target x86_64-unknown-linux-gnu -Z implicit-sysroot-deps=false //@ needs-llvm-components: x86 + +// This program has an implicit dependency on std, injected by rust. This test ensures that rustc +// does not search in the sysroot for it when `-Zimplicit-sysroot-deps` is false, and that an +// error is thrown when std is not available on any other search paths. + fn main() {} From 46ac557f15fcd8bc77deb4bbfed2868a157c8ac8 Mon Sep 17 00:00:00 2001 From: Thierry Berger Date: Fri, 5 Jun 2026 14:39:10 +0200 Subject: [PATCH 16/31] rustdoc: add #[doc(label_trait)] to render a trait badge --- compiler/rustc_ast_passes/src/feature_gate.rs | 1 + .../rustc_attr_parsing/src/attributes/doc.rs | 2 + compiler/rustc_feature/src/unstable.rs | 2 + .../rustc_hir/src/attrs/data_structures.rs | 3 ++ compiler/rustc_middle/src/queries.rs | 5 +++ compiler/rustc_middle/src/ty/util.rs | 6 +++ compiler/rustc_passes/src/check_attr.rs | 2 + compiler/rustc_span/src/symbol.rs | 2 + src/doc/rustdoc/src/unstable-features.md | 12 ++++++ src/librustdoc/clean/mod.rs | 1 + src/librustdoc/clean/types.rs | 3 ++ src/librustdoc/html/render/mod.rs | 43 ++++++++++++++++++- src/librustdoc/html/render/print_item.rs | 39 ++++++++++++++++- src/librustdoc/html/static/css/rustdoc.css | 19 ++++++++ src/librustdoc/html/templates/print_item.html | 13 +++++- src/librustdoc/json/conversions.rs | 2 + .../label_trait/label-trait-badge.rs | 25 +++++++++++ .../label_trait/label-trait-generic.rs | 13 ++++++ .../label_trait/label-trait-negative.rs | 10 +++++ .../label_trait/label-trait-supertrait.rs | 16 +++++++ .../lints/invalid-doc-attr-2.stderr | 2 +- tests/ui/attributes/malformed-attrs.stderr | 4 +- .../feature-gate-doc_label_trait.rs | 4 ++ .../feature-gate-doc_label_trait.stderr | 13 ++++++ .../ui/malformed/malformed-regressions.stderr | 2 +- 25 files changed, 236 insertions(+), 8 deletions(-) create mode 100644 tests/rustdoc-html/label_trait/label-trait-badge.rs create mode 100644 tests/rustdoc-html/label_trait/label-trait-generic.rs create mode 100644 tests/rustdoc-html/label_trait/label-trait-negative.rs create mode 100644 tests/rustdoc-html/label_trait/label-trait-supertrait.rs create mode 100644 tests/ui/feature-gates/feature-gate-doc_label_trait.rs create mode 100644 tests/ui/feature-gates/feature-gate-doc_label_trait.stderr diff --git a/compiler/rustc_ast_passes/src/feature_gate.rs b/compiler/rustc_ast_passes/src/feature_gate.rs index bec96621fbfed..10cc292bfc4f4 100644 --- a/compiler/rustc_ast_passes/src/feature_gate.rs +++ b/compiler/rustc_ast_passes/src/feature_gate.rs @@ -170,6 +170,7 @@ impl<'a> Visitor<'a> for PostExpansionVisitor<'a> { auto_cfg => doc_cfg masked => doc_masked notable_trait => doc_notable_trait + label_trait => doc_label_trait } "meant for internal use only" { attribute => rustdoc_internals diff --git a/compiler/rustc_attr_parsing/src/attributes/doc.rs b/compiler/rustc_attr_parsing/src/attributes/doc.rs index cba00f5f068a6..f5b27cd9ab802 100644 --- a/compiler/rustc_attr_parsing/src/attributes/doc.rs +++ b/compiler/rustc_attr_parsing/src/attributes/doc.rs @@ -598,6 +598,7 @@ impl DocParser { match path.word_sym() { Some(sym::alias) => self.parse_alias(cx, path, args), Some(sym::hidden) => no_args!(hidden), + Some(sym::label_trait) => no_args!(label_trait), Some(sym::html_favicon_url) => string_arg_and_crate_level!(html_favicon_url), Some(sym::html_logo_url) => string_arg_and_crate_level!(html_logo_url), Some(sym::html_no_source) => no_args_and_crate_level!(html_no_source), @@ -783,6 +784,7 @@ impl AttributeParser for DocParser { "masked", "cfg", "notable_trait", + "label_trait", "keyword", "fake_variadic", "search_unbox", diff --git a/compiler/rustc_feature/src/unstable.rs b/compiler/rustc_feature/src/unstable.rs index 6505cca2473f8..7e9d5721d370d 100644 --- a/compiler/rustc_feature/src/unstable.rs +++ b/compiler/rustc_feature/src/unstable.rs @@ -317,6 +317,8 @@ declare_features! ( (unstable, box_patterns, "1.0.0", Some(29641)), /// Allows builtin # foo() syntax (internal, builtin_syntax, "1.71.0", Some(110680)), + /// Allows `#[doc(label_trait)]`. + (unstable, doc_label_trait, "CURRENT_RUSTC_VERSION", Some(156865)), /// Allows `#[doc(notable_trait)]`. /// Renamed from `doc_spotlight`. (unstable, doc_notable_trait, "1.52.0", Some(45040)), diff --git a/compiler/rustc_hir/src/attrs/data_structures.rs b/compiler/rustc_hir/src/attrs/data_structures.rs index ff2e06eaca1d7..008d2c37e6739 100644 --- a/compiler/rustc_hir/src/attrs/data_structures.rs +++ b/compiler/rustc_hir/src/attrs/data_structures.rs @@ -581,6 +581,7 @@ pub struct DocAttribute { pub aliases: FxIndexMap, pub hidden: Option, + pub label_trait: Option, // Because we need to emit the error if there is more than one `inline` attribute on an item // at the same time as the other doc attributes, we store a list instead of using `Option`. pub inline: ThinVec<(DocInline, Span)>, @@ -619,6 +620,7 @@ impl rustc_serialize::Encodable for DocAttribute first_span, aliases, hidden, + label_trait, inline, cfg, auto_cfg, @@ -642,6 +644,7 @@ impl rustc_serialize::Encodable for DocAttribute rustc_serialize::Encodable::::encode(first_span, encoder); rustc_serialize::Encodable::::encode(aliases, encoder); rustc_serialize::Encodable::::encode(hidden, encoder); + rustc_serialize::Encodable::::encode(label_trait, encoder); // FIXME: The `doc(inline)` attribute is never encoded, but is it actually the right thing // to do? I suspect the condition was broken, should maybe instead not encode anything if we diff --git a/compiler/rustc_middle/src/queries.rs b/compiler/rustc_middle/src/queries.rs index a72f7021b7768..017db88e58247 100644 --- a/compiler/rustc_middle/src/queries.rs +++ b/compiler/rustc_middle/src/queries.rs @@ -1495,6 +1495,11 @@ rustc_queries! { separate_provide_extern } + /// Determines whether an item is annotated with `#[doc(label_trait)]`. + query is_doc_label_trait(def_id: DefId) -> bool { + desc { "checking whether `{}` is `doc(label_trait)`", tcx.def_path_str(def_id) } + } + /// Determines whether an item is annotated with `#[doc(notable_trait)]`. query is_doc_notable_trait(def_id: DefId) -> bool { desc { "checking whether `{}` is `doc(notable_trait)`", tcx.def_path_str(def_id) } diff --git a/compiler/rustc_middle/src/ty/util.rs b/compiler/rustc_middle/src/ty/util.rs index 8e84ee6ab03e1..579ffa9bdf523 100644 --- a/compiler/rustc_middle/src/ty/util.rs +++ b/compiler/rustc_middle/src/ty/util.rs @@ -1704,6 +1704,11 @@ pub fn is_doc_notable_trait(tcx: TyCtxt<'_>, def_id: DefId) -> bool { find_attr!(tcx, def_id, Doc(doc) if doc.notable_trait.is_some()) } +/// Determines whether an item is annotated with `doc(notable_trait)`. +pub fn is_doc_label_trait(tcx: TyCtxt<'_>, def_id: DefId) -> bool { + find_attr!(tcx, def_id, Doc(doc) if doc.label_trait.is_some()) +} + /// Determines whether an item is an intrinsic (which may be via Abi or via the `rustc_intrinsic` attribute). /// /// We double check the feature gate here because whether a function may be defined as an intrinsic causes @@ -1731,6 +1736,7 @@ pub fn provide(providers: &mut Providers) { *providers = Providers { reveal_opaque_types_in_bounds, is_doc_hidden, + is_doc_label_trait, is_doc_notable_trait, intrinsic_raw, ..*providers diff --git a/compiler/rustc_passes/src/check_attr.rs b/compiler/rustc_passes/src/check_attr.rs index 572d9cd1da957..122758a60f9f1 100644 --- a/compiler/rustc_passes/src/check_attr.rs +++ b/compiler/rustc_passes/src/check_attr.rs @@ -1052,6 +1052,8 @@ impl<'tcx> CheckAttrVisitor<'tcx> { // valid pretty much anywhere, not checked here? // FIXME: should we? hidden: _, + // FIXME: valid for traits, should be checked in attr_parsing + label_trait: _, inline, // FIXME: currently unchecked cfg: _, diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs index e533458ab53aa..525014f29d94c 100644 --- a/compiler/rustc_span/src/symbol.rs +++ b/compiler/rustc_span/src/symbol.rs @@ -842,6 +842,7 @@ symbols! { doc_cfg, doc_cfg_hide, doc_keyword, + doc_label_trait, doc_masked, doc_notable_trait, doc_primitive, @@ -1170,6 +1171,7 @@ symbols! { kreg0, label, label_break_value, + label_trait, lahfsahf_target_feature, lang, lang_items, diff --git a/src/doc/rustdoc/src/unstable-features.md b/src/doc/rustdoc/src/unstable-features.md index 26985e67abb6e..422934667124b 100644 --- a/src/doc/rustdoc/src/unstable-features.md +++ b/src/doc/rustdoc/src/unstable-features.md @@ -56,6 +56,18 @@ It is also not emitted for foreign items, aliases, extern crates and imports. These features operate by extending the `#[doc]` attribute, and thus can be caught by the compiler and enabled with a `#![feature(...)]` attribute in your crate. +### Making your trait more discoverable + + * Tracking issue: [#156865](https://github.com/rust-lang/rust/issues/156865) + +Important traits can be difficult to discover when lost in the noise. +This `#![feature(doc_label_trait)]` allows you to tag traits important for your code base. + +The traits with the attribute #![doc(label_trait)]` are rendered with a colored badge at the top of their dedicated page. + +Consider lookint into the `notable_trait` unstable attribure, which help with +discoverability in other ways. + ### Adding your trait to the "Notable traits" dialog * Tracking issue: [#45040](https://github.com/rust-lang/rust/issues/45040) diff --git a/src/librustdoc/clean/mod.rs b/src/librustdoc/clean/mod.rs index a81d56e708173..9e83f0b49c388 100644 --- a/src/librustdoc/clean/mod.rs +++ b/src/librustdoc/clean/mod.rs @@ -2820,6 +2820,7 @@ fn add_without_unwanted_attributes<'hir>( first_span: _, aliases, hidden, + label_trait: _, inline, cfg, auto_cfg: _, diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index 56a73955abce0..71e2dcd4a0651 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -1365,6 +1365,9 @@ impl Trait { pub(crate) fn is_auto(&self, tcx: TyCtxt<'_>) -> bool { tcx.trait_is_auto(self.def_id) } + pub(crate) fn is_label_trait(&self, tcx: TyCtxt<'_>) -> bool { + tcx.is_doc_label_trait(self.def_id) + } pub(crate) fn is_notable_trait(&self, tcx: TyCtxt<'_>) -> bool { tcx.is_doc_notable_trait(self.def_id) } diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index 4f5fa2b51cf85..09405995e21c7 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -41,7 +41,7 @@ mod write_shared; use std::borrow::Cow; use std::cmp::Ordering; -use std::collections::VecDeque; +use std::collections::{BTreeMap, VecDeque}; use std::fmt::{self, Display as _, Write}; use std::iter::Peekable; use std::path::PathBuf; @@ -1790,6 +1790,47 @@ fn notable_traits_json<'a>(tys: impl Iterator, cx: &Cont serde_json::to_string(&mp).expect("serialize (string, string) -> json object cannot fail") } +pub(crate) struct LabelTraitInfo { + pub name: String, + pub full_path: String, + /// Relative URL to the trait page, or `None` if it cannot be linked. + pub href: Option, +} + +/// Returns all `#[doc(label_trait)]` traits that `item` implements. +pub(crate) fn label_traits_for_item(item: &clean::Item, cx: &Context<'_>) -> Vec { + let Some(did) = item.def_id() else { return Vec::new() }; + + if Some(did) == cx.tcx().lang_items().owned_box() + || Some(did) == cx.tcx().lang_items().pin_type() + { + return Vec::new(); + } + + let Some(impls) = cx.cache().impls.get(&did) else { return Vec::new() }; + + impls + .iter() + .map(Impl::inner_impl) + .filter(|impl_| impl_.polarity == ty::ImplPolarity::Positive) + .filter_map(|impl_| { + let path_ = impl_.trait_.as_ref()?; + let trait_did = path_.def_id(); + if !cx.cache().traits.get(&trait_did)?.is_label_trait(cx.tcx()) { + return None; + } + let name = cx.tcx().item_name(trait_did).to_string(); + let (full_path, href) = match href(trait_did, cx) { + Ok(info) => (join_path_syms(&info.rust_path), Some(info.url)), + Err(_) => (cx.tcx().def_path_str(trait_did), None), + }; + Some((name.clone(), LabelTraitInfo { name, full_path, href })) + }) + .collect::>() + .into_values() + .collect() +} + #[derive(Clone, Copy, Debug)] struct ImplRenderingParameters { show_def_docs: bool, diff --git a/src/librustdoc/html/render/print_item.rs b/src/librustdoc/html/render/print_item.rs index 7c1cc4ba7f9d9..6a10d01dc229f 100644 --- a/src/librustdoc/html/render/print_item.rs +++ b/src/librustdoc/html/render/print_item.rs @@ -1,6 +1,8 @@ use std::borrow::Cow; use std::cmp::Ordering; +use std::collections::hash_map::DefaultHasher; use std::fmt::{self, Display, Write as _}; +use std::hash::{Hash, Hasher}; use std::iter; use askama::Template; @@ -38,7 +40,7 @@ use crate::html::format::{ }; use crate::html::markdown::{HeadingOffset, MarkdownSummaryLine}; use crate::html::render::sidebar::filters; -use crate::html::render::{document_full, document_item_info}; +use crate::html::render::{document_full, document_item_info, label_traits_for_item}; use crate::html::url_parts_builder::UrlPartsBuilder; const ITEM_TABLE_OPEN: &str = "
      "; @@ -51,6 +53,15 @@ struct PathComponent { name: Symbol, } +struct LabelTraitVars { + name: String, + full_path: String, + /// Relative URL to the trait page, or empty when not linkable. + href: String, + /// Pre-rendered `style="..."` attribute. + style_attr: String, +} + #[derive(Template)] #[template(path = "print_item.html")] struct ItemVars<'a> { @@ -59,6 +70,7 @@ struct ItemVars<'a> { item_type: &'a str, path_components: Vec, stability_since_raw: &'a str, + impl_label_traits: Vec, src_href: Option<&'a str>, } @@ -112,6 +124,30 @@ pub(super) fn print_item(cx: &Context<'_>, item: &clean::Item) -> impl fmt::Disp let src_href = if cx.info.include_sources && !item.is_primitive() { cx.src_href(item) } else { None }; + let impl_label_traits: Vec = label_traits_for_item(item, cx) + .into_iter() + .map(|info| { + // Stable per-trait color from a hash of the DefId so the same + // trait gets the same badge color across pages. + // This won't be stable between releases though. + let mut h = DefaultHasher::new(); + info.full_path.hash(&mut h); + let v = h.finish(); + let style_attr = format!( + "style=\"background: rgb({}, {}, {})\"", + v as u8, + (v >> 8) as u8, + (v >> 16) as u8, + ); + LabelTraitVars { + name: info.name, + full_path: info.full_path, + href: info.href.unwrap_or_default(), + style_attr, + } + }) + .collect(); + let path_components = if item.is_fake_item() { vec![] } else { @@ -135,6 +171,7 @@ pub(super) fn print_item(cx: &Context<'_>, item: &clean::Item) -> impl fmt::Disp item_type: &item.type_().to_string(), path_components, stability_since_raw: &stability_since_raw, + impl_label_traits, src_href: src_href.as_deref(), }; diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css index 731c8ad8b8223..30e1bfe3078a1 100644 --- a/src/librustdoc/html/static/css/rustdoc.css +++ b/src/librustdoc/html/static/css/rustdoc.css @@ -1646,6 +1646,25 @@ so that we can apply CSS-filters to change the arrow color in themes */ font-size: initial; } +.impl-label-trait-full-badge-container { + padding: 0.5rem 0; + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.impl-label-trait-full-badge { + display: flex; + align-items: center; + width: fit-content; + height: 1.5rem; + padding: 0 0.5rem; + border-radius: 0.75rem; + font-size: 1rem; + font-weight: normal; + color: white; +} + .rightside { padding-left: 12px; float: right; diff --git a/src/librustdoc/html/templates/print_item.html b/src/librustdoc/html/templates/print_item.html index 640fd3dfee498..306c13fbb5cf5 100644 --- a/src/librustdoc/html/templates/print_item.html +++ b/src/librustdoc/html/templates/print_item.html @@ -3,7 +3,7 @@
      {% for (i, component) in path_components.iter().enumerate() %} {% if i != 0 %} - :: + :: {% endif %} {{component.name}} {% endfor %} @@ -11,7 +11,7 @@ {% endif %}

      {{typ}} - + {{name|wrapped|safe}}  {# #}