From b72727495c15ce53a0ca5ec0c559e1f9bbe7dd83 Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Tue, 14 Jul 2026 15:24:18 -0400 Subject: [PATCH 01/16] make debug builders with closures impl with cell Signed-off-by: Connor Tsui --- library/core/src/fmt/builders.rs | 156 ++++++++++++++++++------------- 1 file changed, 92 insertions(+), 64 deletions(-) diff --git a/library/core/src/fmt/builders.rs b/library/core/src/fmt/builders.rs index 19dd13967fdd6..ceec98c8659fb 100644 --- a/library/core/src/fmt/builders.rs +++ b/library/core/src/fmt/builders.rs @@ -1,5 +1,6 @@ #![allow(unused_imports)] +use crate::cell::Cell; use crate::fmt::{self, Debug, Formatter}; struct PadAdapter<'buf, 'state> { @@ -50,6 +51,29 @@ impl fmt::Write for PadAdapter<'_, '_> { } } +/// Wraps an `FnOnce` formatting closure in a type that implements [`fmt::Debug`] by calling the +/// closure, allowing the `*_with` builder methods to forward to their `&dyn fmt::Debug` +/// counterparts. +/// +/// By doing this, the builder logic is monomorphized only once and not for every closure type +/// (see #149745). +/// +/// Formatting a `DebugOnce` consumes the closure, so attempting to format it more than once +/// panics. This never happens because the debug builders format each value exactly once. +struct DebugOnce(Cell>); + +impl fmt::Debug for DebugOnce +where + F: FnOnce(&mut fmt::Formatter<'_>) -> fmt::Result, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0.take() { + Some(value_fmt) => value_fmt(f), + None => panic!("formatting closure called more than once"), + } + } +} + /// A struct to help with [`fmt::Debug`](Debug) implementations. /// /// This is useful when you wish to output a formatted struct as a part of your @@ -130,18 +154,6 @@ impl<'a, 'b: 'a> DebugStruct<'a, 'b> { /// ``` #[stable(feature = "debug_builders", since = "1.2.0")] pub fn field(&mut self, name: &str, value: &dyn fmt::Debug) -> &mut Self { - self.field_with(name, |f| value.fmt(f)) - } - - /// Adds a new field to the generated struct output. - /// - /// This method is equivalent to [`DebugStruct::field`], but formats the - /// value using a provided closure rather than by calling [`Debug::fmt`]. - #[unstable(feature = "debug_closure_helpers", issue = "117729")] - pub fn field_with(&mut self, name: &str, value_fmt: F) -> &mut Self - where - F: FnOnce(&mut fmt::Formatter<'_>) -> fmt::Result, - { self.result = self.result.and_then(|_| { if self.is_pretty() { if !self.has_fields { @@ -152,14 +164,14 @@ impl<'a, 'b: 'a> DebugStruct<'a, 'b> { let mut writer = PadAdapter::wrap(self.fmt, &mut slot, &mut state); writer.write_str(name)?; writer.write_str(": ")?; - value_fmt(&mut writer)?; + value.fmt(&mut writer)?; writer.write_str(",\n") } else { let prefix = if self.has_fields { ", " } else { " { " }; self.fmt.write_str(prefix)?; self.fmt.write_str(name)?; self.fmt.write_str(": ")?; - value_fmt(self.fmt) + value.fmt(self.fmt) } }); @@ -167,6 +179,18 @@ impl<'a, 'b: 'a> DebugStruct<'a, 'b> { self } + /// Adds a new field to the generated struct output. + /// + /// This method is equivalent to [`DebugStruct::field`], but formats the + /// value using a provided closure rather than by calling [`Debug::fmt`]. + #[unstable(feature = "debug_closure_helpers", issue = "117729")] + pub fn field_with(&mut self, name: &str, value_fmt: F) -> &mut Self + where + F: FnOnce(&mut fmt::Formatter<'_>) -> fmt::Result, + { + self.field(name, &DebugOnce(Cell::new(Some(value_fmt)))) + } + /// Marks the struct as non-exhaustive, indicating to the reader that there are some other /// fields that are not shown in the debug representation. /// @@ -327,18 +351,6 @@ impl<'a, 'b: 'a> DebugTuple<'a, 'b> { /// ``` #[stable(feature = "debug_builders", since = "1.2.0")] pub fn field(&mut self, value: &dyn fmt::Debug) -> &mut Self { - self.field_with(|f| value.fmt(f)) - } - - /// Adds a new field to the generated tuple struct output. - /// - /// This method is equivalent to [`DebugTuple::field`], but formats the - /// value using a provided closure rather than by calling [`Debug::fmt`]. - #[unstable(feature = "debug_closure_helpers", issue = "117729")] - pub fn field_with(&mut self, value_fmt: F) -> &mut Self - where - F: FnOnce(&mut fmt::Formatter<'_>) -> fmt::Result, - { self.result = self.result.and_then(|_| { if self.is_pretty() { if self.fields == 0 { @@ -347,12 +359,12 @@ impl<'a, 'b: 'a> DebugTuple<'a, 'b> { let mut slot = None; let mut state = Default::default(); let mut writer = PadAdapter::wrap(self.fmt, &mut slot, &mut state); - value_fmt(&mut writer)?; + value.fmt(&mut writer)?; writer.write_str(",\n") } else { let prefix = if self.fields == 0 { "(" } else { ", " }; self.fmt.write_str(prefix)?; - value_fmt(self.fmt) + value.fmt(self.fmt) } }); @@ -360,6 +372,18 @@ impl<'a, 'b: 'a> DebugTuple<'a, 'b> { self } + /// Adds a new field to the generated tuple struct output. + /// + /// This method is equivalent to [`DebugTuple::field`], but formats the + /// value using a provided closure rather than by calling [`Debug::fmt`]. + #[unstable(feature = "debug_closure_helpers", issue = "117729")] + pub fn field_with(&mut self, value_fmt: F) -> &mut Self + where + F: FnOnce(&mut fmt::Formatter<'_>) -> fmt::Result, + { + self.field(&DebugOnce(Cell::new(Some(value_fmt)))) + } + /// Marks the tuple struct as non-exhaustive, indicating to the reader that there are some /// other fields that are not shown in the debug representation. /// @@ -453,10 +477,7 @@ struct DebugInner<'a, 'b: 'a> { } impl<'a, 'b: 'a> DebugInner<'a, 'b> { - fn entry_with(&mut self, entry_fmt: F) - where - F: FnOnce(&mut fmt::Formatter<'_>) -> fmt::Result, - { + fn entry(&mut self, entry: &dyn fmt::Debug) { self.result = self.result.and_then(|_| { if self.is_pretty() { if !self.has_fields { @@ -465,19 +486,26 @@ impl<'a, 'b: 'a> DebugInner<'a, 'b> { let mut slot = None; let mut state = Default::default(); let mut writer = PadAdapter::wrap(self.fmt, &mut slot, &mut state); - entry_fmt(&mut writer)?; + entry.fmt(&mut writer)?; writer.write_str(",\n") } else { if self.has_fields { self.fmt.write_str(", ")? } - entry_fmt(self.fmt) + entry.fmt(self.fmt) } }); self.has_fields = true; } + fn entry_with(&mut self, entry_fmt: F) + where + F: FnOnce(&mut fmt::Formatter<'_>) -> fmt::Result, + { + self.entry(&DebugOnce(Cell::new(Some(entry_fmt)))); + } + fn is_pretty(&self) -> bool { self.fmt.alternate() } @@ -546,7 +574,7 @@ impl<'a, 'b: 'a> DebugSet<'a, 'b> { /// ``` #[stable(feature = "debug_builders", since = "1.2.0")] pub fn entry(&mut self, entry: &dyn fmt::Debug) -> &mut Self { - self.inner.entry_with(|f| entry.fmt(f)); + self.inner.entry(entry); self } @@ -738,7 +766,7 @@ impl<'a, 'b: 'a> DebugList<'a, 'b> { /// ``` #[stable(feature = "debug_builders", since = "1.2.0")] pub fn entry(&mut self, entry: &dyn fmt::Debug) -> &mut Self { - self.inner.entry_with(|f| entry.fmt(f)); + self.inner.entry(entry); self } @@ -969,18 +997,6 @@ impl<'a, 'b: 'a> DebugMap<'a, 'b> { /// ``` #[stable(feature = "debug_map_key_value", since = "1.42.0")] pub fn key(&mut self, key: &dyn fmt::Debug) -> &mut Self { - self.key_with(|f| key.fmt(f)) - } - - /// Adds the key part of a new entry to the map output. - /// - /// This method is equivalent to [`DebugMap::key`], but formats the - /// key using a provided closure rather than by calling [`Debug::fmt`]. - #[unstable(feature = "debug_closure_helpers", issue = "117729")] - pub fn key_with(&mut self, key_fmt: F) -> &mut Self - where - F: FnOnce(&mut fmt::Formatter<'_>) -> fmt::Result, - { self.result = self.result.and_then(|_| { assert!( !self.has_key, @@ -995,13 +1011,13 @@ impl<'a, 'b: 'a> DebugMap<'a, 'b> { let mut slot = None; self.state = Default::default(); let mut writer = PadAdapter::wrap(self.fmt, &mut slot, &mut self.state); - key_fmt(&mut writer)?; + key.fmt(&mut writer)?; writer.write_str(": ")?; } else { if self.has_fields { self.fmt.write_str(", ")? } - key_fmt(self.fmt)?; + key.fmt(self.fmt)?; self.fmt.write_str(": ")?; } @@ -1012,6 +1028,18 @@ impl<'a, 'b: 'a> DebugMap<'a, 'b> { self } + /// Adds the key part of a new entry to the map output. + /// + /// This method is equivalent to [`DebugMap::key`], but formats the + /// key using a provided closure rather than by calling [`Debug::fmt`]. + #[unstable(feature = "debug_closure_helpers", issue = "117729")] + pub fn key_with(&mut self, key_fmt: F) -> &mut Self + where + F: FnOnce(&mut fmt::Formatter<'_>) -> fmt::Result, + { + self.key(&DebugOnce(Cell::new(Some(key_fmt)))) + } + /// Adds the value part of a new entry to the map output. /// /// This method, together with `key`, is an alternative to `entry` that @@ -1045,28 +1073,16 @@ impl<'a, 'b: 'a> DebugMap<'a, 'b> { /// ``` #[stable(feature = "debug_map_key_value", since = "1.42.0")] pub fn value(&mut self, value: &dyn fmt::Debug) -> &mut Self { - self.value_with(|f| value.fmt(f)) - } - - /// Adds the value part of a new entry to the map output. - /// - /// This method is equivalent to [`DebugMap::value`], but formats the - /// value using a provided closure rather than by calling [`Debug::fmt`]. - #[unstable(feature = "debug_closure_helpers", issue = "117729")] - pub fn value_with(&mut self, value_fmt: F) -> &mut Self - where - F: FnOnce(&mut fmt::Formatter<'_>) -> fmt::Result, - { self.result = self.result.and_then(|_| { assert!(self.has_key, "attempted to format a map value before its key"); if self.is_pretty() { let mut slot = None; let mut writer = PadAdapter::wrap(self.fmt, &mut slot, &mut self.state); - value_fmt(&mut writer)?; + value.fmt(&mut writer)?; writer.write_str(",\n")?; } else { - value_fmt(self.fmt)?; + value.fmt(self.fmt)?; } self.has_key = false; @@ -1077,6 +1093,18 @@ impl<'a, 'b: 'a> DebugMap<'a, 'b> { self } + /// Adds the value part of a new entry to the map output. + /// + /// This method is equivalent to [`DebugMap::value`], but formats the + /// value using a provided closure rather than by calling [`Debug::fmt`]. + #[unstable(feature = "debug_closure_helpers", issue = "117729")] + pub fn value_with(&mut self, value_fmt: F) -> &mut Self + where + F: FnOnce(&mut fmt::Formatter<'_>) -> fmt::Result, + { + self.value(&DebugOnce(Cell::new(Some(value_fmt)))) + } + /// Adds the contents of an iterator of entries to the map output. /// /// # Examples From 88ff4b02135dc96bc83df3beb81c1f2c2480d9ea Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Tue, 26 May 2026 17:37:55 +0200 Subject: [PATCH 02/16] interpret: properly check for inhabitedness of nested references --- .../src/interpret/validity.rs | 11 +- compiler/rustc_middle/src/queries.rs | 5 + .../rustc_middle/src/ty/inhabitedness/mod.rs | 181 +++++++++++++++++- .../rustc_ty_utils/src/layout/invariant.rs | 10 + .../fail/validity/ref_to_uninhabited1.rs | 2 +- .../fail/validity/ref_to_uninhabited1.stderr | 2 +- .../fail/validity/ref_to_uninhabited2.rs | 2 +- .../fail/validity/ref_to_uninhabited2.stderr | 6 +- .../consts/const-eval/raw-bytes.32bit.stderr | 4 +- .../consts/const-eval/raw-bytes.64bit.stderr | 4 +- tests/ui/consts/const-eval/ub-uninhabit.rs | 26 ++- .../ui/consts/const-eval/ub-uninhabit.stderr | 54 +++++- tests/ui/consts/cycle-static-promoted.rs | 12 -- tests/ui/consts/recursive-type.rs | 18 ++ tests/ui/consts/validate_never_arrays.stderr | 2 +- 15 files changed, 304 insertions(+), 35 deletions(-) delete mode 100644 tests/ui/consts/cycle-static-promoted.rs create mode 100644 tests/ui/consts/recursive-type.rs diff --git a/compiler/rustc_const_eval/src/interpret/validity.rs b/compiler/rustc_const_eval/src/interpret/validity.rs index 60e1e39140471..9eefc6718ceb2 100644 --- a/compiler/rustc_const_eval/src/interpret/validity.rs +++ b/compiler/rustc_const_eval/src/interpret/validity.rs @@ -35,6 +35,7 @@ use super::{ format_interp_error, }; use crate::enter_trace_span; +use crate::interpret::ensure_monomorphic_enough; // for the validation errors #[rustfmt::skip] @@ -686,11 +687,11 @@ impl<'rt, 'tcx, M: Machine<'tcx>> ValidityVisitor<'rt, 'tcx, M> { ) } // Do not allow references to uninhabited types. - if place.layout.is_uninhabited() { + if !place.layout.ty.is_opsem_inhabited(*self.ecx.tcx, self.ecx.typing_env) { let ty = place.layout.ty; throw_validation_failure!( self.path, - format!("encountered a {ptr_kind} pointing to uninhabited type {ty}") + format!("encountered a {ptr_kind} pointing to uninhabited type `{ty}`") ) } @@ -1524,8 +1525,9 @@ impl<'rt, 'tcx, M: Machine<'tcx>> ValueVisitor<'tcx, M> for ValidityVisitor<'rt, } // Assert that we checked everything there is to check about this type. + // `is_opsem_inhabited` implies that the layout is inhabited (checked by layout invariants). assert!( - !val.layout.is_uninhabited(), + val.layout.ty.is_opsem_inhabited(*self.ecx.tcx, self.ecx.typing_env), "a value of type `{}` passed validation but that type is uninhabited", val.layout.ty ); @@ -1583,6 +1585,9 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> { ) -> InterpResult<'tcx> { trace!("validate_operand_internal: {:?}, {:?}", *val, val.layout.ty); + // We can't check validity if there are any generics left. + ensure_monomorphic_enough(*self.tcx, val.layout.ty)?; + // Run the visitor. self.run_for_validation_mut(|ecx| { let reset_padding = reset_provenance_and_padding && { diff --git a/compiler/rustc_middle/src/queries.rs b/compiler/rustc_middle/src/queries.rs index 86eca36eac084..e81c3bb9f61ba 100644 --- a/compiler/rustc_middle/src/queries.rs +++ b/compiler/rustc_middle/src/queries.rs @@ -2179,6 +2179,11 @@ rustc_queries! { desc { "computing the uninhabited predicate of `{}`", key } } + /// Do not call this query directly: invoke `Ty::is_opsem_inhabited` instead. + query is_opsem_inhabited_raw(env: ty::PseudoCanonicalInput<'tcx, Ty<'tcx>>) -> bool { + desc { "computing whether `{}` is inhabited on the opsem level", env.value } + } + query crate_dep_kind(_: CrateNum) -> CrateDepKind { eval_always desc { "fetching what a dependency looks like" } diff --git a/compiler/rustc_middle/src/ty/inhabitedness/mod.rs b/compiler/rustc_middle/src/ty/inhabitedness/mod.rs index 12c13ad74cb24..600a99e384967 100644 --- a/compiler/rustc_middle/src/ty/inhabitedness/mod.rs +++ b/compiler/rustc_middle/src/ty/inhabitedness/mod.rs @@ -43,6 +43,9 @@ //! This code should only compile in modules where the uninhabitedness of `Foo` //! is visible. +use std::assert_matches; + +use rustc_data_structures::fx::FxHashSet; use rustc_span::def_id::LocalModId; use rustc_type_ir::TyKind::*; use tracing::instrument; @@ -55,7 +58,12 @@ pub mod inhabited_predicate; pub use inhabited_predicate::InhabitedPredicate; pub(crate) fn provide(providers: &mut Providers) { - *providers = Providers { inhabited_predicate_adt, inhabited_predicate_type, ..*providers }; + *providers = Providers { + inhabited_predicate_adt, + inhabited_predicate_type, + is_opsem_inhabited_raw, + ..*providers + }; } /// Returns an `InhabitedPredicate` that is generic over type parameters and @@ -191,7 +199,10 @@ impl<'tcx> Ty<'tcx> { self.inhabited_predicate(tcx).apply(tcx, typing_env, module) } - /// Returns true if the type is uninhabited without regard to visibility + /// Returns true if the type is uninhabited without regard to visibility. + /// + /// This is still conservative; for instance, a `#[non_exhaustive]` enum *in another crate* + /// is always considered inhabited. pub fn is_privately_uninhabited( self, tcx: TyCtxt<'tcx>, @@ -199,6 +210,16 @@ impl<'tcx> Ty<'tcx> { ) -> bool { !self.inhabited_predicate(tcx).apply_ignore_module(tcx, typing_env) } + + /// Returns whether `self` is considered inhabited on the opsem level, i.e., its validity + /// invariant might be satisfiable. `self` is expected to be monomorphic and normalized. + pub fn is_opsem_inhabited(self, tcx: TyCtxt<'tcx>, typing_env: ty::TypingEnv<'tcx>) -> bool { + // Handle simple cases directly, use the query with its cache for the rest. + is_opsem_inhabited_recursor(self, tcx, &mut (), /* stop_at_ref */ false, &|ty, _, _| { + // ADT handler: stop recursing, invoke the query. + tcx.is_opsem_inhabited_raw(typing_env.as_query_input(ty)) + }) + } } /// N.B. this query should only be called through `Ty::inhabited_predicate` @@ -221,3 +242,159 @@ fn inhabited_predicate_type<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>) -> InhabitedP _ => bug!("unexpected TyKind, use `Ty::inhabited_predicate`"), } } + +/// Recurse over a type to determine whether it is inhabited on the opsem level. +/// Key constraints are: +/// - if a type's validity invariant is satisfiable, it must be opsem-inhabited. +/// - if a type's layout is marked uninhabited, it must be opsem-uninhabited. +/// +/// Beyond that, the value returned by this function is not a stable guarantee. +/// +/// When we encounter an ADT, we call `adt_handler`, giving it as its last argument a closure that +/// it can invoke to continue the recursion. This lets us share the logic for "simple" cases +/// (i.e., everything except for ADTs) between `Ty::is_opsem_inhabited` and the query. +/// +/// `seen` is used to detect infinite recursion: the set contains all ADTs that we encountered +/// on our path to the current type. +/// If `stop_at_ref` is true, we stop recursing at the next reference we encounter. +fn is_opsem_inhabited_recursor<'tcx, SEEN>( + ty: Ty<'tcx>, + tcx: TyCtxt<'tcx>, + seen: &mut SEEN, + stop_at_ref: bool, + adt_handler: &impl Fn( + Ty<'tcx>, + &mut SEEN, + &dyn Fn(Ty<'tcx>, &mut SEEN, /* stop_at_ref */ bool) -> bool, + ) -> bool, +) -> bool { + match *ty.kind() { + // Trivially (un)inhabited types + ty::Int(_) + | ty::Uint(_) + | ty::Float(_) + | ty::Bool + | ty::Char + | ty::Str + | ty::Foreign(..) + | ty::RawPtr(..) + | ty::FnPtr(..) + | ty::FnDef(..) => true, + ty::Dynamic(..) => true, // We can't reason about traits, assume they are inhabited + ty::Slice(..) => true, // Slices can always be empty + ty::Never => false, + + // Types where we recurse + ty::Ref(_, pointee, _) => { + if stop_at_ref { + // Bailing out here is safe as the layout code always considers references + // inhabited, so the implication ("layout uninhabited => opsem uninhabited") + // is upheld. + return true; + } + is_opsem_inhabited_recursor(pointee, tcx, seen, stop_at_ref, adt_handler) + } + ty::Tuple(tys) => tys + .iter() + .all(|ty| is_opsem_inhabited_recursor(ty, tcx, seen, stop_at_ref, adt_handler)), + ty::Array(elem, len) => { + len.try_to_target_usize(tcx).unwrap() == 0 + || is_opsem_inhabited_recursor(elem, tcx, seen, stop_at_ref, adt_handler) + } + ty::Pat(inner, _pat) => { + is_opsem_inhabited_recursor(inner, tcx, seen, stop_at_ref, adt_handler) + } + ty::Closure(_def, args) => { + let args = args.as_closure(); + args.upvar_tys() + .iter() + .all(|ty| is_opsem_inhabited_recursor(ty, tcx, seen, stop_at_ref, adt_handler)) + } + ty::Coroutine(_def, args) => { + let args = args.as_coroutine(); + args.upvar_tys() + .iter() + .all(|ty| is_opsem_inhabited_recursor(ty, tcx, seen, stop_at_ref, adt_handler)) + } + ty::CoroutineClosure(_def, args) => { + let args = args.as_coroutine_closure(); + args.upvar_tys() + .iter() + .all(|ty| is_opsem_inhabited_recursor(ty, tcx, seen, stop_at_ref, adt_handler)) + } + ty::UnsafeBinder(base) => { + let base = tcx.instantiate_bound_regions_with_erased((*base).into()); + is_opsem_inhabited_recursor(base, tcx, seen, stop_at_ref, adt_handler) + } + ty::Adt(..) => { + // ADTs need a special handler to avoid infinite recursion. That handler is meant to + // call back into the recursor. Ideally it'd just call `is_opsem_inhabited_recursor` but + // then it would have to pass itself as the adt_handler argument which is not possible + // in Rust... so we provide the handler with a callback that it can use to continue the + // recursion with the same `adt_handler`. + adt_handler(ty, seen, &|ty, seen, stop_at_ref| { + is_opsem_inhabited_recursor(ty, tcx, seen, stop_at_ref, adt_handler) + }) + } + + ty::Error(_) + | ty::Infer(..) + | ty::Placeholder(..) + | ty::Bound(..) + | ty::Param(..) + | ty::Alias(..) + | ty::CoroutineWitness(..) => { + bug!("non-normalized type in `is_opsem_uninhabited`: `{ty}`") + } + } +} + +fn is_opsem_inhabited_raw<'tcx>( + tcx: TyCtxt<'tcx>, + env: ty::PseudoCanonicalInput<'tcx, Ty<'tcx>>, +) -> bool { + let (ty, typing_env) = (env.value, env.typing_env); + assert_matches!( + ty.kind(), + ty::Adt(..), + "the query should only be invoked by `Ty::is_opsem_inhabited`" + ); + + is_opsem_inhabited_recursor( + ty, + tcx, + &mut FxHashSet::::default(), + /* stop_at_ref */ false, + &|ty, seen, rec| { + let ty::Adt(adt_def, adt_args) = *ty.kind() else { + unreachable! {} + }; + if adt_def.is_union() { + // Unions are always inhabited. + return true; + } + + let new_adt = seen.insert(adt_def.did()); + // If we have seen this ADT before, stop at the next reference to avoid infinite + // recursion. We can't stop here since we have to ensure that "layout inhabited" + // implies "opsem inhabited". + let stop_at_ref = !new_adt; + + // We are inhabited if in some variant all fields are inhabited. + let inhabited = adt_def.variants().iter().any(|variant| { + variant.fields.iter().all(|field| { + let ty = field.ty(tcx, adt_args); + let ty = tcx.normalize_erasing_regions(typing_env, ty); + rec(ty, seen, stop_at_ref) + }) + }); + + // Remove the type again so that we allow it to appear on other branches. + if new_adt { + seen.remove(&adt_def.did()); + } + + inhabited + }, + ) +} diff --git a/compiler/rustc_ty_utils/src/layout/invariant.rs b/compiler/rustc_ty_utils/src/layout/invariant.rs index 70e980490f36b..d426593c8d519 100644 --- a/compiler/rustc_ty_utils/src/layout/invariant.rs +++ b/compiler/rustc_ty_utils/src/layout/invariant.rs @@ -1,6 +1,7 @@ use std::assert_matches; use rustc_abi::{BackendRepr, FieldsShape, Scalar, Size, TagEncoding, Variants}; +use rustc_middle::ty::TypeVisitableExt; use rustc_middle::ty::layout::{HasTyCtxt, LayoutCx, TyAndLayout}; use rustc_middle::{bug, ty}; @@ -34,6 +35,15 @@ pub(super) fn layout_sanity_check<'tcx>(cx: &LayoutCx<'tcx>, layout: &TyAndLayou layout.ty ); } + // ABI uninhabitedness should imply opsem uninhabitedness. However, we can only check that if + // the type is really monomorphic (while we can compute a layout for some generic types). + if layout.is_uninhabited() && !layout.ty.has_param() { + assert!( + !layout.ty.is_opsem_inhabited(tcx, cx.typing_env), + "{:?} is ABI-uninhabited but not opsem-uninhabited?", + layout.ty + ); + } /// Yields non-ZST fields of the type fn non_zst_fields<'tcx, 'a>( diff --git a/src/tools/miri/tests/fail/validity/ref_to_uninhabited1.rs b/src/tools/miri/tests/fail/validity/ref_to_uninhabited1.rs index 2e6be8b971c64..25e21375cd746 100644 --- a/src/tools/miri/tests/fail/validity/ref_to_uninhabited1.rs +++ b/src/tools/miri/tests/fail/validity/ref_to_uninhabited1.rs @@ -3,7 +3,7 @@ use std::mem::{forget, transmute}; fn main() { unsafe { - let x: Box = transmute(&mut 42); //~ERROR: encountered a box pointing to uninhabited type ! + let x: Box = transmute(&mut 42); //~ERROR: encountered a box pointing to uninhabited type `!` forget(x); } } diff --git a/src/tools/miri/tests/fail/validity/ref_to_uninhabited1.stderr b/src/tools/miri/tests/fail/validity/ref_to_uninhabited1.stderr index 645a9789207a0..889596092de60 100644 --- a/src/tools/miri/tests/fail/validity/ref_to_uninhabited1.stderr +++ b/src/tools/miri/tests/fail/validity/ref_to_uninhabited1.stderr @@ -1,4 +1,4 @@ -error: Undefined Behavior: constructing invalid value of type std::boxed::Box: encountered a box pointing to uninhabited type ! +error: Undefined Behavior: constructing invalid value of type std::boxed::Box: encountered a box pointing to uninhabited type `!` --> tests/fail/validity/ref_to_uninhabited1.rs:LL:CC | LL | let x: Box = transmute(&mut 42); diff --git a/src/tools/miri/tests/fail/validity/ref_to_uninhabited2.rs b/src/tools/miri/tests/fail/validity/ref_to_uninhabited2.rs index 8934a06b5d73a..3ac987309f783 100644 --- a/src/tools/miri/tests/fail/validity/ref_to_uninhabited2.rs +++ b/src/tools/miri/tests/fail/validity/ref_to_uninhabited2.rs @@ -4,6 +4,6 @@ enum Void {} fn main() { unsafe { - let _x: &(i32, Void) = transmute(&42); //~ERROR: encountered a reference pointing to uninhabited type (i32, Void) + let _x: &&(i32, Void) = transmute(&&42); //~ERROR: encountered a reference pointing to uninhabited type `&(i32, Void)` } } diff --git a/src/tools/miri/tests/fail/validity/ref_to_uninhabited2.stderr b/src/tools/miri/tests/fail/validity/ref_to_uninhabited2.stderr index 37b265a771bc1..cba028eeca3fe 100644 --- a/src/tools/miri/tests/fail/validity/ref_to_uninhabited2.stderr +++ b/src/tools/miri/tests/fail/validity/ref_to_uninhabited2.stderr @@ -1,8 +1,8 @@ -error: Undefined Behavior: constructing invalid value of type &(i32, Void): encountered a reference pointing to uninhabited type (i32, Void) +error: Undefined Behavior: constructing invalid value of type &&(i32, Void): encountered a reference pointing to uninhabited type `&(i32, Void)` --> tests/fail/validity/ref_to_uninhabited2.rs:LL:CC | -LL | let _x: &(i32, Void) = transmute(&42); - | ^^^^^^^^^^^^^^ Undefined Behavior occurred here +LL | let _x: &&(i32, Void) = transmute(&&42); + | ^^^^^^^^^^^^^^^ Undefined Behavior occurred here | = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information diff --git a/tests/ui/consts/const-eval/raw-bytes.32bit.stderr b/tests/ui/consts/const-eval/raw-bytes.32bit.stderr index 37c715e003e8f..ef45f840f70a5 100644 --- a/tests/ui/consts/const-eval/raw-bytes.32bit.stderr +++ b/tests/ui/consts/const-eval/raw-bytes.32bit.stderr @@ -218,7 +218,7 @@ LL | const DATA_FN_PTR: fn() = unsafe { mem::transmute(&13) }; ╾ALLOC$ID╼ │ ╾──╼ } -error[E0080]: constructing invalid value of type &Bar: encountered a reference pointing to uninhabited type Bar +error[E0080]: constructing invalid value of type &Bar: encountered a reference pointing to uninhabited type `Bar` --> $DIR/raw-bytes.rs:109:1 | LL | const BAD_BAD_REF: &Bar = unsafe { mem::transmute(1usize) }; @@ -458,7 +458,7 @@ LL | const RAW_TRAIT_OBJ_VTABLE_INVALID: *const dyn Trait = unsafe { mem::transm ╾ALLOC$ID╼ ╾ALLOC$ID╼ │ ╾──╼╾──╼ } -error[E0080]: constructing invalid value of type &[!; 1]: encountered a reference pointing to uninhabited type [!; 1] +error[E0080]: constructing invalid value of type &[!; 1]: encountered a reference pointing to uninhabited type `[!; 1]` --> $DIR/raw-bytes.rs:187:1 | LL | const _: &[!; 1] = unsafe { &*(1_usize as *const [!; 1]) }; diff --git a/tests/ui/consts/const-eval/raw-bytes.64bit.stderr b/tests/ui/consts/const-eval/raw-bytes.64bit.stderr index b74aee7980aa4..0e6ac4448efe6 100644 --- a/tests/ui/consts/const-eval/raw-bytes.64bit.stderr +++ b/tests/ui/consts/const-eval/raw-bytes.64bit.stderr @@ -218,7 +218,7 @@ LL | const DATA_FN_PTR: fn() = unsafe { mem::transmute(&13) }; ╾ALLOC$ID╼ │ ╾──────╼ } -error[E0080]: constructing invalid value of type &Bar: encountered a reference pointing to uninhabited type Bar +error[E0080]: constructing invalid value of type &Bar: encountered a reference pointing to uninhabited type `Bar` --> $DIR/raw-bytes.rs:109:1 | LL | const BAD_BAD_REF: &Bar = unsafe { mem::transmute(1usize) }; @@ -458,7 +458,7 @@ LL | const RAW_TRAIT_OBJ_VTABLE_INVALID: *const dyn Trait = unsafe { mem::transm ╾ALLOC$ID╼ ╾ALLOC$ID╼ │ ╾──────╼╾──────╼ } -error[E0080]: constructing invalid value of type &[!; 1]: encountered a reference pointing to uninhabited type [!; 1] +error[E0080]: constructing invalid value of type &[!; 1]: encountered a reference pointing to uninhabited type `[!; 1]` --> $DIR/raw-bytes.rs:187:1 | LL | const _: &[!; 1] = unsafe { &*(1_usize as *const [!; 1]) }; diff --git a/tests/ui/consts/const-eval/ub-uninhabit.rs b/tests/ui/consts/const-eval/ub-uninhabit.rs index 188ad768a0e98..0bd2e196d14e7 100644 --- a/tests/ui/consts/const-eval/ub-uninhabit.rs +++ b/tests/ui/consts/const-eval/ub-uninhabit.rs @@ -1,6 +1,6 @@ // Strip out raw byte dumps to make comparison platform-independent: //@ normalize-stderr: "(the raw bytes of the constant) \(size: [0-9]*, align: [0-9]*\)" -> "$1 (size: $$SIZE, align: $$ALIGN)" -//@ normalize-stderr: "([0-9a-f][0-9a-f] |╾─*ALLOC[0-9]+(\+[a-z0-9]+)?()?─*╼ )+ *│.*" -> "HEX_DUMP" +//@ normalize-stderr: "([0-9a-f][0-9a-f] |╾─*ALLOC\$ID(\+[a-z0-9]+)?()?─*╼ )+ *│.*" -> "HEX_DUMP" //@ dont-require-annotations: NOTE #![feature(core_intrinsics)] @@ -18,19 +18,35 @@ union MaybeUninit { } const BAD_BAD_BAD: Bar = unsafe { MaybeUninit { uninit: () }.init }; -//~^ ERROR constructing invalid value +//~^ ERROR value of zero-variant enum `Bar` const BAD_BAD_REF: &Bar = unsafe { mem::transmute(1usize) }; -//~^ ERROR constructing invalid value +//~^ ERROR reference pointing to uninhabited type `Bar` const BAD_BAD_ARRAY: [Bar; 1] = unsafe { MaybeUninit { uninit: () }.init }; -//~^ ERROR constructing invalid value +//~^ ERROR value of zero-variant enum `Bar` const READ_NEVER: () = unsafe { let mem = [0u32; 8]; let ptr = mem.as_ptr().cast::(); let _val = intrinsics::read_via_copy(ptr); - //~^ ERROR constructing invalid value + //~^ ERROR value of the never type }; +const BAD_NESTED_REF: &&! = unsafe { mem::transmute(&&0) }; +//~^ ERROR reference pointing to uninhabited type `&!` + +const BAD_NESTED_UNSIZED_REF: &&(!, [i32]) = unsafe { mem::transmute(&(&[0] as &[i32])) }; +//~^ ERROR reference pointing to uninhabited type `&(!, [i32])` + +const BAD_UNINHABITED_SLICE: &[!] = unsafe { mem::transmute(&[()] as &[()]) }; +//~^ ERROR value of the never type + +// This is an interesting type since it looks somewhat recursive even though it is not. +struct Wrap(T); +const WRAPPED_TWICE: Wrap> = unsafe { mem::transmute(()) }; +//~^ ERROR value of the never type +const WRAPPED_TWICE_REF: &Wrap> = unsafe { mem::transmute(&()) }; +//~^ ERROR reference pointing to uninhabited type `Wrap>` + fn main() {} diff --git a/tests/ui/consts/const-eval/ub-uninhabit.stderr b/tests/ui/consts/const-eval/ub-uninhabit.stderr index 64da16121b828..59c7e95adc7f7 100644 --- a/tests/ui/consts/const-eval/ub-uninhabit.stderr +++ b/tests/ui/consts/const-eval/ub-uninhabit.stderr @@ -4,7 +4,7 @@ error[E0080]: constructing invalid value of type Bar: encountered a value of zer LL | const BAD_BAD_BAD: Bar = unsafe { MaybeUninit { uninit: () }.init }; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ evaluation of `BAD_BAD_BAD` failed here -error[E0080]: constructing invalid value of type &Bar: encountered a reference pointing to uninhabited type Bar +error[E0080]: constructing invalid value of type &Bar: encountered a reference pointing to uninhabited type `Bar` --> $DIR/ub-uninhabit.rs:23:1 | LL | const BAD_BAD_REF: &Bar = unsafe { mem::transmute(1usize) }; @@ -27,6 +27,56 @@ error[E0080]: constructing invalid value of type !: encountered a value of the n LL | let _val = intrinsics::read_via_copy(ptr); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ evaluation of `READ_NEVER` failed here -error: aborting due to 4 previous errors +error[E0080]: constructing invalid value of type &&!: encountered a reference pointing to uninhabited type `&!` + --> $DIR/ub-uninhabit.rs:36:1 + | +LL | const BAD_NESTED_REF: &&! = unsafe { mem::transmute(&&0) }; + | ^^^^^^^^^^^^^^^^^^^^^^^^^ it is undefined behavior to use this value + | + = note: the rules on what exactly is undefined behavior aren't clear, so this check might be overzealous. Please open an issue on the rustc repository if you believe it should not be considered undefined behavior. + = note: the raw bytes of the constant (size: $SIZE, align: $ALIGN) { + HEX_DUMP + } + +error[E0080]: constructing invalid value of type &&(!, [i32]): encountered a reference pointing to uninhabited type `&(!, [i32])` + --> $DIR/ub-uninhabit.rs:39:1 + | +LL | const BAD_NESTED_UNSIZED_REF: &&(!, [i32]) = unsafe { mem::transmute(&(&[0] as &[i32])) }; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ it is undefined behavior to use this value + | + = note: the rules on what exactly is undefined behavior aren't clear, so this check might be overzealous. Please open an issue on the rustc repository if you believe it should not be considered undefined behavior. + = note: the raw bytes of the constant (size: $SIZE, align: $ALIGN) { + HEX_DUMP + } + +error[E0080]: constructing invalid value of type &[!]: at .[0], encountered a value of the never type `!` + --> $DIR/ub-uninhabit.rs:42:1 + | +LL | const BAD_UNINHABITED_SLICE: &[!] = unsafe { mem::transmute(&[()] as &[()]) }; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ it is undefined behavior to use this value + | + = note: the rules on what exactly is undefined behavior aren't clear, so this check might be overzealous. Please open an issue on the rustc repository if you believe it should not be considered undefined behavior. + = note: the raw bytes of the constant (size: $SIZE, align: $ALIGN) { + HEX_DUMP + } + +error[E0080]: constructing invalid value of type Wrap>: at .0.0, encountered a value of the never type `!` + --> $DIR/ub-uninhabit.rs:47:47 + | +LL | const WRAPPED_TWICE: Wrap> = unsafe { mem::transmute(()) }; + | ^^^^^^^^^^^^^^^^^^ evaluation of `WRAPPED_TWICE` failed here + +error[E0080]: constructing invalid value of type &Wrap>: encountered a reference pointing to uninhabited type `Wrap>` + --> $DIR/ub-uninhabit.rs:49:1 + | +LL | const WRAPPED_TWICE_REF: &Wrap> = unsafe { mem::transmute(&()) }; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ it is undefined behavior to use this value + | + = note: the rules on what exactly is undefined behavior aren't clear, so this check might be overzealous. Please open an issue on the rustc repository if you believe it should not be considered undefined behavior. + = note: the raw bytes of the constant (size: $SIZE, align: $ALIGN) { + HEX_DUMP + } + +error: aborting due to 9 previous errors For more information about this error, try `rustc --explain E0080`. diff --git a/tests/ui/consts/cycle-static-promoted.rs b/tests/ui/consts/cycle-static-promoted.rs deleted file mode 100644 index d648d04861189..0000000000000 --- a/tests/ui/consts/cycle-static-promoted.rs +++ /dev/null @@ -1,12 +0,0 @@ -//@ check-pass - -struct Value { - values: &'static [&'static Value], -} - -// This `static` recursively points to itself through a promoted (the slice). -static VALUE: Value = Value { - values: &[&VALUE], -}; - -fn main() {} diff --git a/tests/ui/consts/recursive-type.rs b/tests/ui/consts/recursive-type.rs new file mode 100644 index 0000000000000..b0207733a2f3d --- /dev/null +++ b/tests/ui/consts/recursive-type.rs @@ -0,0 +1,18 @@ +//@ check-pass +use std::marker::PhantomData; + +struct Value { + values: &'static [&'static Value], +} + +// This `static` recursively points to itself through a promoted (the slice). +static VALUE: Value = Value { + values: &[&VALUE], +}; + +// If we just unfold this type going down the first variant of every enum, we'll never stop; we'll +// never even encounter the same type a second time. +struct S(&'static S<(T, T)>, PhantomData); +const C: &Result, ()> = &Err(()); + +fn main() {} diff --git a/tests/ui/consts/validate_never_arrays.stderr b/tests/ui/consts/validate_never_arrays.stderr index 517b632bdd7be..a006c19587f50 100644 --- a/tests/ui/consts/validate_never_arrays.stderr +++ b/tests/ui/consts/validate_never_arrays.stderr @@ -1,4 +1,4 @@ -error[E0080]: constructing invalid value of type &[!; 1]: encountered a reference pointing to uninhabited type [!; 1] +error[E0080]: constructing invalid value of type &[!; 1]: encountered a reference pointing to uninhabited type `[!; 1]` --> $DIR/validate_never_arrays.rs:6:1 | LL | const _: &[!; 1] = unsafe { &*(1_usize as *const [!; 1]) }; From ba17d29629282ab1accce6b1b8be0916bd7255bd Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Thu, 25 Jun 2026 01:18:21 +0200 Subject: [PATCH 03/16] address some review feedback --- .../rustc_middle/src/ty/inhabitedness/mod.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/compiler/rustc_middle/src/ty/inhabitedness/mod.rs b/compiler/rustc_middle/src/ty/inhabitedness/mod.rs index 600a99e384967..1d4c55aa34690 100644 --- a/compiler/rustc_middle/src/ty/inhabitedness/mod.rs +++ b/compiler/rustc_middle/src/ty/inhabitedness/mod.rs @@ -213,6 +213,12 @@ impl<'tcx> Ty<'tcx> { /// Returns whether `self` is considered inhabited on the opsem level, i.e., its validity /// invariant might be satisfiable. `self` is expected to be monomorphic and normalized. + /// + /// Key constraints are: + /// - if a type's validity invariant is satisfiable, it must be opsem-inhabited. + /// - if a type's layout is marked uninhabited, it must be opsem-uninhabited. + /// + /// Beyond that, the value returned by this function is not a stable guarantee. pub fn is_opsem_inhabited(self, tcx: TyCtxt<'tcx>, typing_env: ty::TypingEnv<'tcx>) -> bool { // Handle simple cases directly, use the query with its cache for the rest. is_opsem_inhabited_recursor(self, tcx, &mut (), /* stop_at_ref */ false, &|ty, _, _| { @@ -244,11 +250,7 @@ fn inhabited_predicate_type<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>) -> InhabitedP } /// Recurse over a type to determine whether it is inhabited on the opsem level. -/// Key constraints are: -/// - if a type's validity invariant is satisfiable, it must be opsem-inhabited. -/// - if a type's layout is marked uninhabited, it must be opsem-uninhabited. -/// -/// Beyond that, the value returned by this function is not a stable guarantee. +/// See `is_opsem_inhabited` above for the spec of what we compute. /// /// When we encounter an ADT, we call `adt_handler`, giving it as its last argument a closure that /// it can invoke to continue the recursion. This lets us share the logic for "simple" cases @@ -376,8 +378,9 @@ fn is_opsem_inhabited_raw<'tcx>( let new_adt = seen.insert(adt_def.did()); // If we have seen this ADT before, stop at the next reference to avoid infinite - // recursion. We can't stop here since we have to ensure that "layout inhabited" - // implies "opsem inhabited". + // recursion. We can't stop here since we have to ensure that "layout uninhabited" + // implies "opsem uninhabited". References are always layout-inhabited so the + // implication is vacuously true. let stop_at_ref = !new_adt; // We are inhabited if in some variant all fields are inhabited. From d65ad0711ac5b43b13c0f9013b424782e0361710 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Thu, 2 Jul 2026 17:40:26 +0200 Subject: [PATCH 04/16] remove a crash test that no longer crashes (but we didn't actually fix the underlying issue) --- tests/crashes/150296.rs | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 tests/crashes/150296.rs diff --git a/tests/crashes/150296.rs b/tests/crashes/150296.rs deleted file mode 100644 index f7d5a54ca9b65..0000000000000 --- a/tests/crashes/150296.rs +++ /dev/null @@ -1,14 +0,0 @@ -//@ known-bug: #150296 -#[derive(PartialEq)] -pub struct Thing; - -impl Thing { - const A: Self = Thing; -} - -fn broken(x: Thing) { - match x { - >::A => {} - _ => {} - } -} From 9c81f660da8aa35f57b5a37628e0a06a4ab64850 Mon Sep 17 00:00:00 2001 From: albab-hasan Date: Thu, 16 Jul 2026 13:00:44 +0600 Subject: [PATCH 05/16] point at method call chain when a return-position `impl Trait` assoc type diverges a mismatch on `-> impl Iterator` only pointed at the signature, not at the call in the returned chain where `Item` actually changed. the failed predicate is useless for this, its already rewritten through the impls it was derived from, so the expected assoc types are read from the opaques own bounds and probed against the returned expression. on divergence the existing `point_at_chain` walk kicks in, same as it does for function arguments. handles the chain at the tail expression, behind a `let` binding, and derived through adapters like `flatten`. unrelated failures stay silent since the probe just doesnt resolve there. fixes https://github.com/rust-lang/rust/issues/106993 --- .../src/error_reporting/traits/suggestions.rs | 126 ++++++++++++++++++ .../duplicate-bound-err.stderr | 8 ++ ...valid-iterator-chain-in-return-position.rs | 39 ++++++ ...d-iterator-chain-in-return-position.stderr | 69 ++++++++++ tests/ui/lint/issue-106991.stderr | 9 ++ 5 files changed, 251 insertions(+) create mode 100644 tests/ui/iterators/invalid-iterator-chain-in-return-position.rs create mode 100644 tests/ui/iterators/invalid-iterator-chain-in-return-position.stderr diff --git a/compiler/rustc_trait_selection/src/error_reporting/traits/suggestions.rs b/compiler/rustc_trait_selection/src/error_reporting/traits/suggestions.rs index ff823594ce1c0..2a1f0e7935ea0 100644 --- a/compiler/rustc_trait_selection/src/error_reporting/traits/suggestions.rs +++ b/compiler/rustc_trait_selection/src/error_reporting/traits/suggestions.rs @@ -4487,6 +4487,29 @@ impl<'a, 'tcx> TypeErrCtxt<'a, 'tcx> { tcx.disabled_nightly_features(err, [(String::new(), sym::trivial_bounds)]); } ObligationCauseCode::OpaqueReturnType(expr_info) => { + // Point at the method call in the returned expression's chain where an + // associated type diverged from what the signature's opaque type expects, + // regardless of how the failed predicate was derived from that expectation. + if let Some(typeck_results) = self.typeck_results.as_deref() { + let chain_expr = match expr_info { + Some((_, hir_id)) => Some(tcx.hir_expect_expr(hir_id)), + None => tcx.hir_node_by_def_id(body_def_id).body_id().and_then(|body_id| { + match tcx.hir_body(body_id).value.kind { + hir::ExprKind::Block(block, _) => block.expr, + _ => None, + } + }), + }; + if let Some(chain_expr) = chain_expr { + self.point_at_chain_in_return_position( + body_def_id, + chain_expr, + typeck_results, + param_env, + err, + ); + } + } let (expr_ty, expr) = if let Some((expr_ty, hir_id)) = expr_info { let expr = tcx.hir_expect_expr(hir_id); (expr_ty, expr) @@ -5438,6 +5461,109 @@ impl<'a, 'tcx> TypeErrCtxt<'a, 'tcx> { assocs_in_this_method } + /// When a `-> impl Trait` return type obligation fails, walk the method call + /// chain in the returned expression to point at where the associated type diverged from + /// what the signature expects. + /// + /// ```text + /// note: the method call chain might not have had the expected associated types + /// --> $DIR/invalid-iterator-chain-in-return-position.rs:16:18 + /// | + /// LL | x.iter_mut().map(foo) + /// | - ---------- ^^^^^^^^ `Iterator::Item` changed to `()` here + /// | | | + /// | | `Iterator::Item` is `&mut Vec` here + /// | this expression has type `Vec>` + /// ``` + fn point_at_chain_in_return_position( + &self, + body_def_id: LocalDefId, + expr: &hir::Expr<'_>, + typeck_results: &TypeckResults<'tcx>, + param_env: ty::ParamEnv<'tcx>, + err: &mut Diag<'_, G>, + ) { + let tcx = self.tcx; + if !matches!(tcx.def_kind(body_def_id), DefKind::Fn | DefKind::AssocFn) { + return; + } + let output = + tcx.fn_sig(body_def_id).instantiate_identity().skip_norm_wip().output().skip_binder(); + let &ty::Alias(_, ty::AliasTy { kind: ty::Opaque { def_id: opaque_def_id }, args, .. }) = + output.kind() + else { + return; + }; + + // The predicate that reaches here has been rewritten through the impls it was + // derived from (e.g. `Iterator for Map` turns `Iterator::Item` requirements + // into requirements on `F`'s return type), so the associated types the user wrote + // in the signature are recovered from the opaque's bounds instead. + let mut probe_diffs = vec![]; + for clause in tcx.item_bounds(opaque_def_id).instantiate(tcx, args).skip_norm_wip() { + let Some(proj) = clause.as_projection_clause() else { continue }; + let proj = self.instantiate_binder_with_fresh_vars( + expr.span, + BoundRegionConversionTime::FnCall, + proj, + ); + let Some(expected_term) = proj.term.as_type() else { continue }; + // Only the projection (for its `DefId`) is used when probing the chain; the + // bound's own term is carried in `found` for the divergence check below and + // is replaced with the probed type afterwards. + probe_diffs.push(TypeError::Sorts(ty::error::ExpectedFound { + expected: proj.projection_term.expect_ty().to_ty(tcx, ty::IsRigid::No), + found: expected_term, + })); + } + if probe_diffs.is_empty() { + return; + } + + // If the returned expression is a binding, walk the chain that created it instead. + let expr = if let hir::ExprKind::Path(hir::QPath::Resolved(None, path)) = expr.kind + && let hir::Path { res: Res::Local(hir_id), .. } = path + && let hir::Node::Pat(binding) = tcx.hir_node(*hir_id) + && let hir::Node::LetStmt(local) = tcx.parent_hir_node(binding.hir_id) + && let Some(binding_expr) = local.init + { + binding_expr + } else { + expr + }; + + // Resolve what each bound associated type actually is for the returned expression, + // and keep only the ones that diverged from the signature. + let expr_ty = self.resolve_vars_if_possible( + typeck_results.expr_ty_adjusted_opt(expr).unwrap_or(Ty::new_misc_error(tcx)), + ); + let assocs = self.probe_assoc_types_at_expr( + &probe_diffs, + expr.span, + expr_ty, + expr.hir_id, + param_env, + ); + let mut type_diffs = vec![]; + for (probe_diff, assoc) in iter::zip(probe_diffs, assocs) { + let TypeError::Sorts(ty::error::ExpectedFound { expected, found: expected_term }) = + probe_diff + else { + continue; + }; + let Some((_, (_, actual_ty))) = assoc else { continue }; + if !self.can_eq(param_env, expected_term, actual_ty) { + type_diffs.push(TypeError::Sorts(ty::error::ExpectedFound { + expected, + found: actual_ty, + })); + } + } + if !type_diffs.is_empty() { + self.point_at_chain(expr, typeck_results, type_diffs, param_env, err); + } + } + /// If the type that failed selection is an array or a reference to an array, /// but the trait is implemented for slices, suggest that the user converts /// the array into a slice. diff --git a/tests/ui/associated-type-bounds/duplicate-bound-err.stderr b/tests/ui/associated-type-bounds/duplicate-bound-err.stderr index 8b172cd5ca133..f685b01cd8cc3 100644 --- a/tests/ui/associated-type-bounds/duplicate-bound-err.stderr +++ b/tests/ui/associated-type-bounds/duplicate-bound-err.stderr @@ -107,6 +107,14 @@ LL | fn foo() -> impl Iterator { ... LL | [2u32].into_iter() | ------------------ return type was inferred to be `std::array::IntoIter` here + | +note: the method call chain might not have had the expected associated types + --> $DIR/duplicate-bound-err.rs:110:16 + | +LL | [2u32].into_iter() + | ------ ^^^^^^^^^^^ `Iterator::Item` is `u32` here + | | + | this expression has type `[u32; 1]` error[E0271]: expected `impl Iterator` to be an iterator that yields `i32`, but it yields `u32` --> $DIR/duplicate-bound-err.rs:107:17 diff --git a/tests/ui/iterators/invalid-iterator-chain-in-return-position.rs b/tests/ui/iterators/invalid-iterator-chain-in-return-position.rs new file mode 100644 index 0000000000000..c87438818e17d --- /dev/null +++ b/tests/ui/iterators/invalid-iterator-chain-in-return-position.rs @@ -0,0 +1,39 @@ +//! Check that a `-> impl Iterator` return type mismatch points at the +//! method call in the returned expression's chain where `Iterator::Item` diverged +//! from the signature's expectation, instead of only pointing at the signature. +//! Regression test for . + +fn foo(items: &mut Vec) { + items.sort(); +} + +fn bar() -> impl Iterator { + //~^ ERROR expected `foo` to return `i32`, but it returns `()` + let mut x: Vec> = vec![ + vec![0, 2, 1], + vec![5, 4, 3], + ]; + x.iter_mut().map(foo) +} + +fn baz() -> impl Iterator { + //~^ ERROR expected `foo` to return `i32`, but it returns `()` + let mut x: Vec> = vec![ + vec![0, 2, 1], + vec![5, 4, 3], + ]; + let it = x.iter_mut().map(foo); + it +} + +fn chained() -> impl Iterator { + //~^ ERROR expected `IntoIter` to be an iterator that yields `i32`, but it yields `u32` + let x = vec![0u32, 1, 2]; + x.into_iter().filter(|x| *x > 0).map(|x| x.checked_add(1)).flatten() +} + +fn main() { + bar(); + baz(); + chained(); +} diff --git a/tests/ui/iterators/invalid-iterator-chain-in-return-position.stderr b/tests/ui/iterators/invalid-iterator-chain-in-return-position.stderr new file mode 100644 index 0000000000000..a21acc3dd84b3 --- /dev/null +++ b/tests/ui/iterators/invalid-iterator-chain-in-return-position.stderr @@ -0,0 +1,69 @@ +error[E0271]: expected `foo` to return `i32`, but it returns `()` + --> $DIR/invalid-iterator-chain-in-return-position.rs:10:13 + | +LL | fn bar() -> impl Iterator { + | ^^^^^^^^^^^^^^^^^^^^^^^^^ expected `i32`, found `()` +... +LL | x.iter_mut().map(foo) + | --------------------- return type was inferred to be `Map>, for<'a> fn(&'a mut Vec) {foo}>` here + | + = note: required for `Map>, for<'a> fn(&'a mut Vec) {foo}>` to implement `Iterator` +note: the method call chain might not have had the expected associated types + --> $DIR/invalid-iterator-chain-in-return-position.rs:16:18 + | +LL | let mut x: Vec> = vec![ + | _______________________________- +LL | | vec![0, 2, 1], +LL | | vec![5, 4, 3], +LL | | ]; + | |_____- this expression has type `Vec>` +LL | x.iter_mut().map(foo) + | ---------- ^^^^^^^^ `Iterator::Item` changed to `()` here + | | + | `Iterator::Item` is `&mut Vec` here + +error[E0271]: expected `foo` to return `i32`, but it returns `()` + --> $DIR/invalid-iterator-chain-in-return-position.rs:19:13 + | +LL | fn baz() -> impl Iterator { + | ^^^^^^^^^^^^^^^^^^^^^^^^^ expected `i32`, found `()` +... +LL | it + | -- return type was inferred to be `Map>, for<'a> fn(&'a mut Vec) {foo}>` here + | + = note: required for `Map>, for<'a> fn(&'a mut Vec) {foo}>` to implement `Iterator` +note: the method call chain might not have had the expected associated types + --> $DIR/invalid-iterator-chain-in-return-position.rs:25:27 + | +LL | let mut x: Vec> = vec![ + | _______________________________- +LL | | vec![0, 2, 1], +LL | | vec![5, 4, 3], +LL | | ]; + | |_____- this expression has type `Vec>` +LL | let it = x.iter_mut().map(foo); + | ---------- ^^^^^^^^ `Iterator::Item` changed to `()` here + | | + | `Iterator::Item` is `&mut Vec` here + +error[E0271]: expected `IntoIter` to be an iterator that yields `i32`, but it yields `u32` + --> $DIR/invalid-iterator-chain-in-return-position.rs:29:17 + | +LL | fn chained() -> impl Iterator { + | ^^^^^^^^^^^^^^^^^^^^^^^^^ expected `i32`, found `u32` + | +note: the method call chain might not have had the expected associated types + --> $DIR/invalid-iterator-chain-in-return-position.rs:32:7 + | +LL | let x = vec![0u32, 1, 2]; + | ---------------- this expression has type `Vec` +LL | x.into_iter().filter(|x| *x > 0).map(|x| x.checked_add(1)).flatten() + | ^^^^^^^^^^^ ------------------ ------------------------- ^^^^^^^^^ `Iterator::Item` changed to `u32` here + | | | | + | | | `Iterator::Item` changed to `Option` here + | | `Iterator::Item` remains `u32` here + | `Iterator::Item` is `u32` here + +error: aborting due to 3 previous errors + +For more information about this error, try `rustc --explain E0271`. diff --git a/tests/ui/lint/issue-106991.stderr b/tests/ui/lint/issue-106991.stderr index 15e0ba5337f14..5c31c240dfff5 100644 --- a/tests/ui/lint/issue-106991.stderr +++ b/tests/ui/lint/issue-106991.stderr @@ -8,6 +8,15 @@ LL | x.iter_mut().map(foo) | --------------------- return type was inferred to be `Map>, for<'a> fn(&'a mut Vec) {foo}>` here | = note: required for `Map>, for<'a> fn(&'a mut Vec) {foo}>` to implement `Iterator` +note: the method call chain might not have had the expected associated types + --> $DIR/issue-106991.rs:8:18 + | +LL | let mut x: Vec> = vec![vec![0, 2, 1], vec![5, 4, 3]]; + | ---------------------------------- this expression has type `Vec>` +LL | x.iter_mut().map(foo) + | ---------- ^^^^^^^^ `Iterator::Item` changed to `()` here + | | + | `Iterator::Item` is `&mut Vec` here error: aborting due to 1 previous error From e2159107f6ebc615ba6a92960d65528baf90cd35 Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Mon, 13 Jul 2026 15:17:30 +0200 Subject: [PATCH 06/16] Update global_asm tests for LLVM 23 There is now only a single "module asm", not one before each line. --- tests/codegen-llvm/asm/global_asm.rs | 4 ++-- tests/codegen-llvm/asm/global_asm_include.rs | 4 ++-- tests/codegen-llvm/asm/global_asm_x2.rs | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/codegen-llvm/asm/global_asm.rs b/tests/codegen-llvm/asm/global_asm.rs index 32075daa3cf23..8c1c4a3b5881f 100644 --- a/tests/codegen-llvm/asm/global_asm.rs +++ b/tests/codegen-llvm/asm/global_asm.rs @@ -7,10 +7,10 @@ use std::arch::global_asm; -// CHECK-LABEL: foo // CHECK: module asm +// CHECK-LABEL: foo // this regex will capture the correct unconditional branch inst. -// CHECK: module asm "{{[[:space:]]+}}jmp baz" +// CHECK: "{{[[:space:]]+}}jmp baz" global_asm!( r#" .global foo diff --git a/tests/codegen-llvm/asm/global_asm_include.rs b/tests/codegen-llvm/asm/global_asm_include.rs index 98be9c3e33322..cad9901336bf3 100644 --- a/tests/codegen-llvm/asm/global_asm_include.rs +++ b/tests/codegen-llvm/asm/global_asm_include.rs @@ -7,9 +7,9 @@ use std::arch::global_asm; -// CHECK-LABEL: foo // CHECK: module asm -// CHECK: module asm "{{[[:space:]]+}}jmp baz" +// CHECK-LABEL: foo +// CHECK: "{{[[:space:]]+}}jmp baz" global_asm!(include_str!("foo.s")); extern "C" { diff --git a/tests/codegen-llvm/asm/global_asm_x2.rs b/tests/codegen-llvm/asm/global_asm_x2.rs index 9e3a00f068053..8c6f38289de33 100644 --- a/tests/codegen-llvm/asm/global_asm_x2.rs +++ b/tests/codegen-llvm/asm/global_asm_x2.rs @@ -8,12 +8,12 @@ use core::arch::global_asm; -// CHECK-LABEL: foo // CHECK: module asm -// CHECK: module asm "{{[[:space:]]+}}jmp baz" +// CHECK-LABEL: foo +// CHECK: "{{[[:space:]]+}}jmp baz" // any other global_asm will be appended to this first block, so: // CHECK-LABEL: bar -// CHECK: module asm "{{[[:space:]]+}}jmp quux" +// CHECK: "{{[[:space:]]+}}jmp quux" global_asm!( r#" .global foo From d4a24cd6e18b98326429d8c7e7bbb269da14a5aa Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Mon, 13 Jul 2026 15:21:18 +0200 Subject: [PATCH 07/16] Adjust codegen test for LLVM 23 The assume here is not really relevant to the purpose of the test, and it's position changed in LLVM 23. --- tests/codegen-llvm/issues/issue-107681-unwrap_unchecked.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/codegen-llvm/issues/issue-107681-unwrap_unchecked.rs b/tests/codegen-llvm/issues/issue-107681-unwrap_unchecked.rs index 5834255f3d313..c3090a20e4d14 100644 --- a/tests/codegen-llvm/issues/issue-107681-unwrap_unchecked.rs +++ b/tests/codegen-llvm/issues/issue-107681-unwrap_unchecked.rs @@ -14,7 +14,6 @@ pub unsafe fn foo(x: &mut Copied>) -> u32 { // CHECK-NOT: br {{.*}} // CHECK-NOT: select // CHECK: [[RET:%.*]] = load i32, ptr - // CHECK-NEXT: assume - // CHECK-NEXT: ret i32 [[RET]] + // CHECK: ret i32 [[RET]] x.next().unwrap_unchecked() } From 283a60b64339d611b76a4ed2f9286f417b269b2f Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Mon, 13 Jul 2026 16:18:03 +0200 Subject: [PATCH 08/16] Update simd mask test for LLVM 23 Codegen changed in: https://github.com/llvm/llvm-project/commit/66c1b6f0194bec99396e358f997d771d5e3bd28d --- .../simd-intrinsic-mask-reduce.rs | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/tests/assembly-llvm/simd-intrinsic-mask-reduce.rs b/tests/assembly-llvm/simd-intrinsic-mask-reduce.rs index 40d16886c6d7d..d3698fda5fe85 100644 --- a/tests/assembly-llvm/simd-intrinsic-mask-reduce.rs +++ b/tests/assembly-llvm/simd-intrinsic-mask-reduce.rs @@ -1,12 +1,16 @@ // verify that simd mask reductions do not introduce additional bit shift operations //@ add-minicore -//@ revisions: x86 aarch64 +//@ revisions: x86 aarch64-llvm22 aarch64 //@ [x86] compile-flags: --target=x86_64-unknown-linux-gnu -C llvm-args=-x86-asm-syntax=intel // Set the base cpu explicitly, in case the default has been changed. //@ [x86] compile-flags: -C target-cpu=x86-64 //@ [x86] needs-llvm-components: x86 +//@ [aarch64-llvm22] compile-flags: --target=aarch64-unknown-linux-gnu +//@ [aarch64-llvm22] needs-llvm-components: aarch64 +//@ [aarch64-llvm22] max-llvm-major-version: 22 //@ [aarch64] compile-flags: --target=aarch64-unknown-linux-gnu //@ [aarch64] needs-llvm-components: aarch64 +//@ [aarch64] min-llvm-version: 23 //@ assembly-output: emit-asm //@ compile-flags: --crate-type=lib -Copt-level=3 -C panic=abort @@ -33,12 +37,19 @@ pub unsafe extern "C" fn mask_reduce_all(m: mask8x16) -> bool { // x86-NEXT: {{cmp ax, -1|cmp eax, 65535|xor eax, 65535}} // x86-NEXT: sete al // + // aarch64-llvm22-NOT: shl + // aarch64-llvm22: cmge v0.16b, v0.16b, #0 + // aarch64-llvm22-DAG: mov [[REG1:[a-z0-9]+]], #1 + // aarch64-llvm22-DAG: umaxv b0, v0.16b + // aarch64-llvm22-NEXT: fmov [[REG2:[a-z0-9]+]], s0 + // aarch64-llvm22-NEXT: bic w0, [[REG1]], [[REG2]] + // // aarch64-NOT: shl // aarch64: cmge v0.16b, v0.16b, #0 - // aarch64-DAG: mov [[REG1:[a-z0-9]+]], #1 - // aarch64-DAG: umaxv b0, v0.16b - // aarch64-NEXT: fmov [[REG2:[a-z0-9]+]], s0 - // aarch64-NEXT: bic w0, [[REG1]], [[REG2]] + // aarch64-NEXT: addp [[REG1:d[0-9]+]], v0.2d + // aarch64-NEXT: fmov [[REG2:x[0-9]+]], [[REG1]] + // aarch64-NEXT: cmp [[REG2]], #0 + // aarch64-NEXT: cset w0, eq simd_reduce_all(m) } @@ -50,10 +61,17 @@ pub unsafe extern "C" fn mask_reduce_any(m: mask8x16) -> bool { // x86-NEXT: test eax, eax // x86-NEXT: setne al // + // aarch64-llvm22-NOT: shl + // aarch64-llvm22: cmlt v0.16b, v0.16b, #0 + // aarch64-llvm22-NEXT: umaxv b0, v0.16b + // aarch64-llvm22-NEXT: fmov [[REG:[a-z0-9]+]], s0 + // aarch64-llvm22-NEXT: and w0, [[REG]], #0x1 + // // aarch64-NOT: shl // aarch64: cmlt v0.16b, v0.16b, #0 - // aarch64-NEXT: umaxv b0, v0.16b - // aarch64-NEXT: fmov [[REG:[a-z0-9]+]], s0 - // aarch64-NEXT: and w0, [[REG]], #0x1 + // aarch64-NEXT: addp [[REG1:d[0-9]+]], v0.2d + // aarch64-NEXT: fmov [[REG2:x[0-9]+]], [[REG1]] + // aarch64-NEXT: cmp [[REG2]], #0 + // aarch64-NEXT: cset w0, ne simd_reduce_any(m) } From 61d9ba28a99f073fbeb19dbb552ddf343aef62f0 Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Mon, 13 Jul 2026 16:37:25 +0200 Subject: [PATCH 09/16] Disable slice is_ascii() test on LLVM 23 This doesn't optimize as desired since: https://github.com/llvm/llvm-project/commit/21f439f13250bd9b7c19c8dd838177a04bf091ef --- tests/assembly-llvm/slice-is_ascii.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/assembly-llvm/slice-is_ascii.rs b/tests/assembly-llvm/slice-is_ascii.rs index e53cd5160cf56..6d660e78147c2 100644 --- a/tests/assembly-llvm/slice-is_ascii.rs +++ b/tests/assembly-llvm/slice-is_ascii.rs @@ -6,6 +6,11 @@ //@ only-x86_64 //@ ignore-sgx +// No longer optimizes as desired, tracked at: +// https://github.com/rust-lang/rust/issues/154141 +// https://github.com/llvm/llvm-project/issues/209216 +//@ max-llvm-major-version: 22 + #![feature(str_internals)] // CHECK-LABEL: is_ascii_simple_demo: From df6afdf60f37a0b552b764ccb7bf9ecae21039c7 Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Tue, 14 Jul 2026 14:29:17 +0200 Subject: [PATCH 10/16] Adjust PGO tests for LLVM 23 These now have additional !guid metadata. --- tests/run-make/pgo-branch-weights/filecheck-patterns.txt | 6 +++--- tests/run-make/pgo-embed-bc-lto/interesting.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/run-make/pgo-branch-weights/filecheck-patterns.txt b/tests/run-make/pgo-branch-weights/filecheck-patterns.txt index 70d5a645c1454..ac9de0b16e49d 100644 --- a/tests/run-make/pgo-branch-weights/filecheck-patterns.txt +++ b/tests/run-make/pgo-branch-weights/filecheck-patterns.txt @@ -2,16 +2,16 @@ # First, establish that certain !prof labels are attached to the expected # functions and branching instructions. -CHECK: define void @function_called_twice(i32 {{.*}} !prof [[function_called_twice_id:![0-9]+]] { +CHECK: define void @function_called_twice(i32 {{.*}} !prof [[function_called_twice_id:![0-9]+]] CHECK: br i1 {{.*}}, label {{.*}}, label {{.*}}, !prof [[branch_weights0:![0-9]+]] -CHECK: define void @function_called_42_times(i32{{.*}} %c) {{.*}} !prof [[function_called_42_times_id:![0-9]+]] { +CHECK: define void @function_called_42_times(i32{{.*}} %c) {{.*}} !prof [[function_called_42_times_id:![0-9]+]] CHECK: switch i32 %c, label {{.*}} [ CHECK-NEXT: i32 97, label {{.*}} CHECK-NEXT: i32 98, label {{.*}} CHECK-NEXT: ], !prof [[branch_weights1:![0-9]+]] -CHECK: define void @function_called_never(i32 {{.*}} !prof [[function_called_never_id:![0-9]+]] { +CHECK: define void @function_called_never(i32 {{.*}} !prof [[function_called_never_id:![0-9]+]] diff --git a/tests/run-make/pgo-embed-bc-lto/interesting.rs b/tests/run-make/pgo-embed-bc-lto/interesting.rs index 13105c17e126d..94c7621457917 100644 --- a/tests/run-make/pgo-embed-bc-lto/interesting.rs +++ b/tests/run-make/pgo-embed-bc-lto/interesting.rs @@ -10,7 +10,7 @@ pub fn function_called_once() { } // CHECK-LABEL: @function_called_once -// CHECK-SAME: !prof [[function_called_once_id:![0-9]+]] { +// CHECK-SAME: !prof [[function_called_once_id:![0-9]+]] // CHECK: "CG Profile" // CHECK-NOT: "CG Profile" // CHECK-DAG: [[function_called_once_id]] = !{!"function_entry_count", i64 1} From 810a7aa41f08354e2829ccdb03a767b40d5c04e7 Mon Sep 17 00:00:00 2001 From: Jieyou Xu Date: Fri, 17 Jul 2026 00:16:09 +0800 Subject: [PATCH 11/16] tests: gate `tests/deubinfo/function-call.rs` on min GDB 15.1 See RUST-159073, this test has been flaky with ``` /checkout/obj/build/x86_64-unknown-linux-gnu/test/debuginfo/function-call.gdb/function-call.debugger.script:11: Error in sourced command file: Couldn't write extended state status: Bad address. ``` on linux CI jobs under various environments and hosts/targets. I tried running this under GDB 15.1 locally (WSL, `x86_64-unknown-linux-gnu`) but could not reproduce. CI typically uses GDB 12.1 with Ubuntu 22.04. So gate this test with min GDB 15.1 which prevents it from running in CI to workaround the flakiness but still allow running it locally under GDB >= 15.1. It's *possible* there's a genuine problem here. --- tests/debuginfo/function-call.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/debuginfo/function-call.rs b/tests/debuginfo/function-call.rs index 37eda165d99ca..104b43012a83e 100644 --- a/tests/debuginfo/function-call.rs +++ b/tests/debuginfo/function-call.rs @@ -1,5 +1,9 @@ // This test does not passed with gdb < 8.0. See #53497. -//@ min-gdb-version: 10.1 +// +// This test seems to have become very flaky with "Couldn't write extended state status: Bad +// address." since around June 2026, where CI typically uses GDB 12.1 on Ubuntu 22.04. I tried +// running this locally with GDB 15.1 and could not reproduce the flakiness. See #159073. +//@ min-gdb-version: 15.1 //@ compile-flags:-g //@ ignore-backends: gcc From 05bebcde5cb1db1f65de4c1ddf77bf2db980608a Mon Sep 17 00:00:00 2001 From: rustbot <47979223+rustbot@users.noreply.github.com> Date: Thu, 16 Jul 2026 18:18:53 +0200 Subject: [PATCH 12/16] Update books --- src/doc/book | 2 +- src/doc/edition-guide | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/doc/book b/src/doc/book index dd7ab4f4f4541..917544888a55e 160000 --- a/src/doc/book +++ b/src/doc/book @@ -1 +1 @@ -Subproject commit dd7ab4f4f4541adf4aa2a872cdac06c206b73288 +Subproject commit 917544888a55e4da7109bdba8c88c893c0da70f4 diff --git a/src/doc/edition-guide b/src/doc/edition-guide index 53686db907c45..5a14f7276ebc0 160000 --- a/src/doc/edition-guide +++ b/src/doc/edition-guide @@ -1 +1 @@ -Subproject commit 53686db907c45268d1b323afd9a3545a37abbced +Subproject commit 5a14f7276ebc0bd100974f02b918f30472c782fc From cbd88d9f5b3896f5350a5236e92b711f43f6b05c Mon Sep 17 00:00:00 2001 From: Yilin Chen <1479826151@qq.com> Date: Fri, 17 Jul 2026 01:01:18 +0800 Subject: [PATCH 13/16] Fix safety doc in intrinsics::simd --- library/core/src/intrinsics/simd/mod.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/library/core/src/intrinsics/simd/mod.rs b/library/core/src/intrinsics/simd/mod.rs index 9311dcc9bd00e..09eda851bc3d8 100644 --- a/library/core/src/intrinsics/simd/mod.rs +++ b/library/core/src/intrinsics/simd/mod.rs @@ -109,13 +109,13 @@ pub const unsafe fn simd_rem(lhs: T, rhs: T) -> T; /// Shifts vector left elementwise, with UB on overflow. /// -/// Shifts `lhs` left by `rhs`, shifting in sign bits for signed types. +/// Shifts `lhs` left by `rhs`, shifting in zeros. /// /// `T` must be a vector of integers. /// /// # Safety /// -/// Each element of `rhs` must be less than `::BITS`. +/// Each element of `rhs` must be in `0..::BITS`. #[rustc_intrinsic] #[rustc_nounwind] pub const unsafe fn simd_shl(lhs: T, rhs: T) -> T; @@ -128,7 +128,7 @@ pub const unsafe fn simd_shl(lhs: T, rhs: T) -> T; /// /// # Safety /// -/// Each element of `rhs` must be less than `::BITS`. +/// Each element of `rhs` must be in `0..::BITS`. #[rustc_intrinsic] #[rustc_nounwind] pub const unsafe fn simd_shr(lhs: T, rhs: T) -> T; @@ -145,7 +145,7 @@ pub const unsafe fn simd_shr(lhs: T, rhs: T) -> T; /// /// # Safety /// -/// Each element of `shift` must be less than `::BITS`. +/// Each element of `shift` must be in `0..::BITS`. #[rustc_intrinsic] #[rustc_nounwind] pub const unsafe fn simd_funnel_shl(a: T, b: T, shift: T) -> T; @@ -162,7 +162,7 @@ pub const unsafe fn simd_funnel_shl(a: T, b: T, shift: T) -> T; /// /// # Safety /// -/// Each element of `shift` must be less than `::BITS`. +/// Each element of `shift` must be in `0..::BITS`. #[rustc_intrinsic] #[rustc_nounwind] pub const unsafe fn simd_funnel_shr(a: T, b: T, shift: T) -> T; @@ -433,6 +433,9 @@ pub enum SimdAlign { /// # Safety /// `ptr` must be aligned according to the `ALIGN` parameter, see [`SimdAlign`] for details. /// +/// Each pointer offset from `ptr` whose corresponding value in `mask` is `!0` must be readable as if +/// by [`ptr::read`][crate::ptr::read]. +/// /// `mask` must only contain `0` or `!0` values. #[rustc_intrinsic] #[rustc_nounwind] @@ -455,6 +458,9 @@ pub const unsafe fn simd_masked_load(mask: V, p /// # Safety /// `ptr` must be aligned according to the `ALIGN` parameter, see [`SimdAlign`] for details. /// +/// Each pointer offset from `ptr` whose corresponding value in `mask` is `!0` must be writable as if +/// by [`ptr::write`][crate::ptr::write]. +/// /// `mask` must only contain `0` or `!0` values. #[rustc_intrinsic] #[rustc_nounwind] From 14713e5af87efe93a2bd4e21a9ca084ccbe49576 Mon Sep 17 00:00:00 2001 From: Daniel Paoliello Date: Thu, 16 Jul 2026 10:47:53 -0700 Subject: [PATCH 14/16] [aarch64][win] Pass oversized c-variadic args indirectly on Arm64EC On Arm64EC the variadic portion of a c-variadic call must follow the MS x64 ABI: any argument that does not fit in 8 bytes, or whose size is not 1, 2, 4, or 8 bytes, is passed by reference. rustc's `va_arg` implementation already reads such arguments indirectly, but the caller side in `aarch64::compute_abi_info` passed a scalar `i128` by value (it is not an aggregate, so it bypassed `classify_arg`). The callee then dereferenced the inline value as a pointer, causing an access violation (0xC0000005) at runtime. Mark variadic-tail arguments larger than 8 bytes, or whose size is not a power of two, as indirect on Arm64EC so the caller and the `va_arg` reader agree. Fixed arguments are unaffected: a fixed `i128` is still passed by value, matching MSVC and Clang. Add a codegen test verifying that a variadic `i128` is passed as a pointer while a fixed `i128` is passed by value. --- compiler/rustc_target/src/callconv/aarch64.rs | 20 +++++++++- tests/codegen-llvm/arm64ec-c-variadic-i128.rs | 39 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 tests/codegen-llvm/arm64ec-c-variadic-i128.rs diff --git a/compiler/rustc_target/src/callconv/aarch64.rs b/compiler/rustc_target/src/callconv/aarch64.rs index ec2c30756ddc0..0162aa838cb6b 100644 --- a/compiler/rustc_target/src/callconv/aarch64.rs +++ b/compiler/rustc_target/src/callconv/aarch64.rs @@ -3,7 +3,7 @@ use std::iter; use rustc_abi::{BackendRepr, HasDataLayout, Primitive, TyAbiInterface}; use crate::callconv::{ArgAbi, FnAbi, Reg, RegKind, Uniform}; -use crate::spec::{HasTargetSpec, RustcAbi, Target}; +use crate::spec::{Arch, HasTargetSpec, RustcAbi, Target}; /// Indicates the variant of the AArch64 ABI we are compiling for. /// Used to accommodate Apple and Microsoft's deviations from the usual AAPCS ABI. @@ -166,10 +166,26 @@ where classify_ret(cx, &mut fn_abi.ret, kind); } - for arg in fn_abi.args.iter_mut() { + // On Arm64EC the variadic portion of a c-variadic call follows the MS x64 ABI: + // "Any argument that doesn't fit in 8 bytes, or is not 1, 2, 4, or 8 bytes, must + // be passed by reference". + let c_variadic = fn_abi.c_variadic; + let fixed_count = fn_abi.fixed_count as usize; + let is_arm64ec = cx.target_spec().arch == Arch::Arm64EC; + + for (idx, arg) in fn_abi.args.iter_mut().enumerate() { if arg.is_ignore() { continue; } + + if is_arm64ec && c_variadic && idx >= fixed_count { + let size = arg.layout.size.bytes(); + if size > 8 || !size.is_power_of_two() { + arg.make_indirect(); + continue; + } + } + classify_arg(cx, arg, kind); } } diff --git a/tests/codegen-llvm/arm64ec-c-variadic-i128.rs b/tests/codegen-llvm/arm64ec-c-variadic-i128.rs new file mode 100644 index 0000000000000..b98463469eee3 --- /dev/null +++ b/tests/codegen-llvm/arm64ec-c-variadic-i128.rs @@ -0,0 +1,39 @@ +//! Verify the arm64ec calling convention for `i128` passed through the variadic +//! portion of a C-variadic call. +//! +//! On arm64ec the variadic tail follows the MS x64 ABI: any argument that does not +//! fit in 8 bytes, or is not 1/2/4/8 bytes, is passed by reference. So a variadic +//! `i128`/`u128` is passed indirectly (as a pointer), which must stay in sync with +//! how `va_arg` reads it back. A *fixed* `i128` argument is still passed by value. + +//@ add-minicore +//@ compile-flags: -Copt-level=3 --target arm64ec-pc-windows-msvc +//@ needs-llvm-components: aarch64 + +#![crate_type = "lib"] +#![no_std] +#![no_core] +#![feature(no_core)] + +extern crate minicore; + +extern "C" { + fn variadic(fixed: u32, ...); + fn fixed(arg: i128); +} + +// A variadic `i128` argument is passed by reference (as a pointer). +#[no_mangle] +pub unsafe extern "C" fn pass_variadic_i128(x: i128) { + // CHECK-LABEL: @pass_variadic_i128( + // CHECK: call void (i32, ...) @variadic(i32 {{.*}}, ptr {{.*}}) + variadic(0, x); +} + +// A fixed `i128` argument is still passed by value. +#[no_mangle] +pub unsafe extern "C" fn pass_fixed_i128(x: i128) { + // CHECK-LABEL: @pass_fixed_i128( + // CHECK: call void @fixed(i128 + fixed(x); +} From 5245634306972ba8dd02f56a7e78f882c464194c Mon Sep 17 00:00:00 2001 From: Folkert de Vries Date: Thu, 16 Jul 2026 15:38:47 +0200 Subject: [PATCH 15/16] add a fallback for `fmuladdf*` --- library/core/src/intrinsics/mod.rs | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/library/core/src/intrinsics/mod.rs b/library/core/src/intrinsics/mod.rs index 58c68408e6a80..c5da5055e16d9 100644 --- a/library/core/src/intrinsics/mod.rs +++ b/library/core/src/intrinsics/mod.rs @@ -1432,7 +1432,7 @@ pub fn log2f128(x: f128) -> f128 { libm::maybe_available::log2f128(x) } -/// Returns `a * b + c` for `f16` values. +/// Returns `a * b + c` without rounding the intermediate result for `f16` values. /// /// The stabilized version of this intrinsic is /// [`f16::mul_add`](../../std/primitive.f16.html#method.mul_add) @@ -1440,7 +1440,7 @@ pub fn log2f128(x: f128) -> f128 { #[rustc_intrinsic] #[rustc_nounwind] pub const fn fmaf16(a: f16, b: f16, c: f16) -> f16; -/// Returns `a * b + c` for `f32` values. +/// Returns `a * b + c` without rounding the intermediate result for `f32` values. /// /// The stabilized version of this intrinsic is /// [`f32::mul_add`](../../std/primitive.f32.html#method.mul_add) @@ -1448,7 +1448,7 @@ pub const fn fmaf16(a: f16, b: f16, c: f16) -> f16; #[rustc_intrinsic] #[rustc_nounwind] pub const fn fmaf32(a: f32, b: f32, c: f32) -> f32; -/// Returns `a * b + c` for `f64` values. +/// Returns `a * b + c` without rounding the intermediate result for `f64` values. /// /// The stabilized version of this intrinsic is /// [`f64::mul_add`](../../std/primitive.f64.html#method.mul_add) @@ -1456,7 +1456,7 @@ pub const fn fmaf32(a: f32, b: f32, c: f32) -> f32; #[rustc_intrinsic] #[rustc_nounwind] pub const fn fmaf64(a: f64, b: f64, c: f64) -> f64; -/// Returns `a * b + c` for `f128` values. +/// Returns `a * b + c` without rounding the intermediate result for `f128` values. /// /// The stabilized version of this intrinsic is /// [`f128::mul_add`](../../std/primitive.f128.html#method.mul_add) @@ -1475,9 +1475,12 @@ pub const fn fmaf128(a: f128, b: f128, c: f128) -> f128; /// and add instructions. It is unspecified whether or not a fused operation /// is selected, and that may depend on optimization level and context, for /// example. +#[inline] #[rustc_intrinsic] #[rustc_nounwind] -pub const fn fmuladdf16(a: f16, b: f16, c: f16) -> f16; +pub const fn fmuladdf16(a: f16, b: f16, c: f16) -> f16 { + a * b + c +} /// Returns `a * b + c` for `f32` values, non-deterministically executing /// either a fused multiply-add or two operations with rounding of the /// intermediate result. @@ -1488,9 +1491,12 @@ pub const fn fmuladdf16(a: f16, b: f16, c: f16) -> f16; /// and add instructions. It is unspecified whether or not a fused operation /// is selected, and that may depend on optimization level and context, for /// example. +#[inline] #[rustc_intrinsic] #[rustc_nounwind] -pub const fn fmuladdf32(a: f32, b: f32, c: f32) -> f32; +pub const fn fmuladdf32(a: f32, b: f32, c: f32) -> f32 { + a * b + c +} /// Returns `a * b + c` for `f64` values, non-deterministically executing /// either a fused multiply-add or two operations with rounding of the /// intermediate result. @@ -1501,9 +1507,12 @@ pub const fn fmuladdf32(a: f32, b: f32, c: f32) -> f32; /// and add instructions. It is unspecified whether or not a fused operation /// is selected, and that may depend on optimization level and context, for /// example. +#[inline] #[rustc_intrinsic] #[rustc_nounwind] -pub const fn fmuladdf64(a: f64, b: f64, c: f64) -> f64; +pub const fn fmuladdf64(a: f64, b: f64, c: f64) -> f64 { + a * b + c +} /// Returns `a * b + c` for `f128` values, non-deterministically executing /// either a fused multiply-add or two operations with rounding of the /// intermediate result. @@ -1514,9 +1523,12 @@ pub const fn fmuladdf64(a: f64, b: f64, c: f64) -> f64; /// and add instructions. It is unspecified whether or not a fused operation /// is selected, and that may depend on optimization level and context, for /// example. +#[inline] #[rustc_intrinsic] #[rustc_nounwind] -pub const fn fmuladdf128(a: f128, b: f128, c: f128) -> f128; +pub const fn fmuladdf128(a: f128, b: f128, c: f128) -> f128 { + a * b + c +} /// Returns the largest integer less than or equal to an `f16`. /// From 403637b8d3eb65f957ef96ece92074238f6ce1b0 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Thu, 16 Jul 2026 14:04:53 -0700 Subject: [PATCH 16/16] rustdoc: remove old `--emit` types We deprecated these awhile ago, and now all the usages are changed. --- src/librustdoc/config.rs | 3 --- src/librustdoc/html/render/write_shared.rs | 2 +- tests/run-make/emit-shared-files/rmake.rs | 2 +- tests/run-make/rustdoc-scrape-examples-dep-info/rmake.rs | 2 +- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs index a8c27c5615007..27e3c49c36ef9 100644 --- a/src/librustdoc/config.rs +++ b/src/librustdoc/config.rs @@ -336,9 +336,6 @@ impl FromStr for EmitType { fn from_str(s: &str) -> Result { match s { - // old nightly-only choices that are going away soon - "toolchain-shared-resources" => Ok(Self::HtmlStaticFiles), - "invocation-specific" => Ok(Self::HtmlNonStaticFiles), // modern choices "html-static-files" => Ok(Self::HtmlStaticFiles), "html-non-static-files" => Ok(Self::HtmlNonStaticFiles), diff --git a/src/librustdoc/html/render/write_shared.rs b/src/librustdoc/html/render/write_shared.rs index ad8c6588e521f..f98e42057fbef 100644 --- a/src/librustdoc/html/render/write_shared.rs +++ b/src/librustdoc/html/render/write_shared.rs @@ -11,7 +11,7 @@ //! contents, so they do not include a hash in their filename and are not safe to //! cache with `Cache-Control: immutable`. They include the contents of the //! --resource-suffix flag and are emitted when --emit-type is empty (default) -//! or contains "invocation-specific". +//! or contains "html-non-static-files". use std::cell::RefCell; use std::ffi::{OsStr, OsString}; diff --git a/tests/run-make/emit-shared-files/rmake.rs b/tests/run-make/emit-shared-files/rmake.rs index 9841dce27fa2f..326aaa54bbc03 100644 --- a/tests/run-make/emit-shared-files/rmake.rs +++ b/tests/run-make/emit-shared-files/rmake.rs @@ -12,7 +12,7 @@ use run_make_support::{has_extension, has_prefix, path, rustdoc, shallow_find_fi fn main() { rustdoc() .arg("-Zunstable-options") - .arg("--emit=invocation-specific") + .arg("--emit=html-non-static-files") .out_dir("invocation-only") .arg("--resource-suffix=-xxx") .args(&["--theme", "y.css"]) diff --git a/tests/run-make/rustdoc-scrape-examples-dep-info/rmake.rs b/tests/run-make/rustdoc-scrape-examples-dep-info/rmake.rs index 5a612fd130052..13c62906f1687 100644 --- a/tests/run-make/rustdoc-scrape-examples-dep-info/rmake.rs +++ b/tests/run-make/rustdoc-scrape-examples-dep-info/rmake.rs @@ -9,7 +9,7 @@ fn main() { scrape::scrape( &["--scrape-tests", "--emit=dep-info"], - &["--emit=dep-info,invocation-specific"], + &["--emit=dep-info,html-non-static-files"], ); let content = rfs::read_to_string("rustdoc/foobar.d").replace(r"\", "/");