From f5b3643d9ce8b6fcffeaaced9367cf616aa6079c Mon Sep 17 00:00:00 2001 From: Noah Lev Date: Fri, 24 Jul 2026 15:31:27 +0000 Subject: [PATCH 1/3] rustdoc: Skip param_env when making impl inlining decisions We only care about whether the self type is a generic or an item (inlined) in the current crate, so we don't actually need to compute the param_env, which is expensive when done to every external impl. --- src/librustdoc/passes/collect_trait_impls.rs | 43 +++++++++----------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/src/librustdoc/passes/collect_trait_impls.rs b/src/librustdoc/passes/collect_trait_impls.rs index 391cff43beb8e..58f05b558adfe 100644 --- a/src/librustdoc/passes/collect_trait_impls.rs +++ b/src/librustdoc/passes/collect_trait_impls.rs @@ -48,32 +48,29 @@ pub(crate) fn collect_trait_impls(mut krate: Crate, cx: &mut DocContext<'_>) -> let _prof_timer = tcx.sess.prof.generic_activity("build_extern_trait_impls"); for &cnum in tcx.crates(()) { for &impl_def_id in tcx.trait_impls_in_crate(cnum) { - cx.with_param_env(impl_def_id, |cx| { - let opt_trait_ref = tcx.impl_opt_trait_ref(impl_def_id); - if opt_trait_ref.is_some_and(|trait_ref| { - crate_items.contains(&ItemId::DefId(trait_ref.def_id())) - || Some(trait_ref.def_id()) == tcx.lang_items().deref_trait() - || tcx.is_doc_notable_trait(trait_ref.def_id()) - }) { + let opt_trait_ref = tcx.impl_opt_trait_ref(impl_def_id); + if opt_trait_ref.is_some_and(|trait_ref| { + crate_items.contains(&ItemId::DefId(trait_ref.def_id())) + || Some(trait_ref.def_id()) == tcx.lang_items().deref_trait() + || tcx.is_doc_notable_trait(trait_ref.def_id()) + }) { + cx.with_param_env(impl_def_id, |cx| { inline::build_impl(cx, impl_def_id, None, &mut new_items_external); - } else { - let self_ty = - tcx.type_of(impl_def_id).instantiate_identity().skip_norm_wip(); - let self_ty = clean_middle_ty( - ty::Binder::dummy(self_ty), - cx, - Some(impl_def_id), - None, - ); - if self_ty.is_full_generic() - || self_ty - .def_id(&cx.cache) - .is_some_and(|did| crate_items.contains(&ItemId::DefId(did))) - { + }); + } else { + let self_ty = tcx.type_of(impl_def_id).instantiate_identity().skip_norm_wip(); + let self_ty = + clean_middle_ty(ty::Binder::dummy(self_ty), cx, Some(impl_def_id), None); + if self_ty.is_full_generic() + || self_ty + .def_id(&cx.cache) + .is_some_and(|did| crate_items.contains(&ItemId::DefId(did))) + { + cx.with_param_env(impl_def_id, |cx| { inline::build_impl(cx, impl_def_id, None, &mut new_items_external); - } + }); } - }); + } } } } From 40a4f80d493b0f910b2c681dbd7842aacbbffb31 Mon Sep 17 00:00:00 2001 From: Noah Lev Date: Fri, 24 Jul 2026 16:47:21 +0000 Subject: [PATCH 2/3] rustdoc: Only analyze head of self type when deciding impl inlining The last commit removed the expensive param_env computation since we only need to look at the "head" of the self type (i.e. the top-level structure) when making impl inlining decisions. This commit makes that change more sound and improves on it further by creating specialized cleaning logic for types that stops after the head is constructed. --- src/librustdoc/passes/collect_trait_impls.rs | 129 +++++++++++++++++-- 1 file changed, 120 insertions(+), 9 deletions(-) diff --git a/src/librustdoc/passes/collect_trait_impls.rs b/src/librustdoc/passes/collect_trait_impls.rs index 58f05b558adfe..ed0b8d76930b0 100644 --- a/src/librustdoc/passes/collect_trait_impls.rs +++ b/src/librustdoc/passes/collect_trait_impls.rs @@ -3,10 +3,12 @@ //! struct implements that trait. use rustc_data_structures::fx::FxHashSet; +use rustc_errors::FatalError; use rustc_hir::attrs::{AttributeKind, DocAttribute}; -use rustc_hir::def_id::LOCAL_CRATE; +use rustc_hir::def_id::{DefId, LOCAL_CRATE}; use rustc_hir::{Attribute, find_attr}; -use rustc_middle::ty; +use rustc_middle::ty::{self, Ty, TyCtxt}; +use rustc_span::kw; use super::Pass; use crate::clean::*; @@ -59,13 +61,13 @@ pub(crate) fn collect_trait_impls(mut krate: Crate, cx: &mut DocContext<'_>) -> }); } else { let self_ty = tcx.type_of(impl_def_id).instantiate_identity().skip_norm_wip(); - let self_ty = - clean_middle_ty(ty::Binder::dummy(self_ty), cx, Some(impl_def_id), None); - if self_ty.is_full_generic() - || self_ty - .def_id(&cx.cache) - .is_some_and(|did| crate_items.contains(&ItemId::DefId(did))) - { + let self_ty_head = SelfTyHead::of(ty::Binder::dummy(self_ty), tcx, impl_def_id); + let keep_impl = match self_ty_head { + SelfTyHead::Generic => true, + SelfTyHead::Item(def_id) => crate_items.contains(&ItemId::DefId(def_id)), + SelfTyHead::Primitive | SelfTyHead::Other => false, + }; + if keep_impl { cx.with_param_env(impl_def_id, |cx| { inline::build_impl(cx, impl_def_id, None, &mut new_items_external); }); @@ -157,6 +159,115 @@ pub(crate) fn collect_trait_impls(mut krate: Crate, cx: &mut DocContext<'_>) -> krate } +enum SelfTyHead { + Generic, + Primitive, + Item(DefId), + Other, +} + +impl SelfTyHead { + /// Compute the "head" (top-level structure) of a type. + /// + /// When deciding whether to inline an impl, one of the things we look at is + /// whether the Self type (the `Foo` in `impl Foo` or `impl Tr for Foo`) is + /// present in the current crate (usually itself through inlining). However, + /// constructing a full [`clean::Type`](Type) is expensive and more than we need, + /// so this function computes just enough information to determine if the type + /// is in the current crate. + // FIXME: once -Znormalize-docs works properly / becomes the default, + // this should invoke normalization where needed (e.g. if the head is an Alias). + // we'll need to fetch the param_env too. + fn of<'tcx>(bound_ty: ty::Binder<'tcx, Ty<'tcx>>, tcx: TyCtxt<'tcx>, parent: DefId) -> Self { + match *bound_ty.skip_binder().kind() { + ty::Never + | ty::Bool + | ty::Char + | ty::Int(..) + | ty::Uint(..) + | ty::Float(..) + | ty::Str + | ty::Slice(..) + | ty::Array(..) + | ty::RawPtr(..) + | ty::FnDef(..) + | ty::FnPtr(..) + | ty::Tuple(_) => Self::Primitive, + ty::Pat(ty, _) => Self::of(bound_ty.rebind(ty), tcx, parent), + ty::Ref(_, ty, _) => match Self::of(bound_ty.rebind(ty), tcx, parent) { + Self::Generic => Self::Primitive, + head => head, + }, + // FIXME(unsafe_binders): this should probably recurse through the unsafe binder, + // but clean_middle_ty doesn't handle this correctly yet either + ty::UnsafeBinder(_) => Self::Other, + ty::Adt(def, _) => Self::Item(def.did()), + ty::Foreign(did) => Self::Item(did), + ty::Dynamic(obj, _) => { + // HACK: pick the first `did` as the `did` of the trait object. Someone + // might want to implement "native" support for marker-trait-only + // trait objects. + let mut dids = obj.auto_traits(); + let did = obj + .principal_def_id() + .or_else(|| dids.next()) + .unwrap_or_else(|| panic!("found trait object `{obj:?}` with no traits?")); + Self::Item(did) + } + + ty::Alias(_, alias_ty @ ty::AliasTy { kind: ty::Projection { def_id }, .. }) => { + if tcx.is_impl_trait_in_trait(def_id) { + Self::Other + } else { + Self::of(bound_ty.rebind(alias_ty.self_ty()), tcx, parent) + } + } + + ty::Alias(_, alias_ty @ ty::AliasTy { kind: ty::Inherent { .. }, .. }) => { + let alias_ty = bound_ty.rebind(alias_ty); + Self::of(alias_ty.map_bound(|ty| ty.self_ty()), tcx, parent) + } + + ty::Alias(_, ty::AliasTy { kind: ty::Free { def_id }, args, .. }) => { + if tcx.features().checked_type_aliases() { + // Free type alias `data` represents the `type X` in `type X = Y`. If we need `Y`, + // we need to use `type_of`. + Self::Item(def_id) + } else { + let ty = tcx.type_of(def_id).instantiate(tcx, args).skip_norm_wip(); + Self::of(bound_ty.rebind(ty), tcx, parent) + } + } + + ty::Param(ref p) => { + // FIXME: there's a slight behavior difference from clean_middle_ty here + // since here we represent impl traits as Generic not ImplTrait. + // probably doesn't matter for collect trait impls since impl trait + // can't be a self ty + if p.name == kw::SelfUpper { Self::Other } else { Self::Generic } + } + + ty::Bound(_, ref ty) => match ty.kind { + ty::BoundTyKind::Param(_) => Self::Generic, + ty::BoundTyKind::Anon => panic!("unexpected anonymous bound type variable"), + }, + + ty::Alias(_, ty::AliasTy { kind: ty::Opaque { .. }, .. }) => { + panic!("{bound_ty} should not appear as impl self ty") + } + + ty::Closure(..) + | ty::CoroutineClosure(..) + | ty::Coroutine(..) + | ty::Placeholder(..) + | ty::CoroutineWitness(..) + | ty::Infer(..) => panic!("unexpected impl self ty {bound_ty}"), + + ty::Error(_) => FatalError.raise(), + } + } +} + struct SyntheticImplCollector<'a, 'tcx> { cx: &'a mut DocContext<'tcx>, impls: Vec, From 27c97d1380749cf02d8b7b523929bf9555b3d31a Mon Sep 17 00:00:00 2001 From: Noah Lev Date: Mon, 27 Jul 2026 15:58:37 +0000 Subject: [PATCH 3/3] rustdoc: Remove unneeded branches, add test --- src/librustdoc/passes/collect_trait_impls.rs | 25 +++++++++++-------- .../auxiliary/impl-for-projection.rs | 13 ++++++++++ .../impl-for-projection-reexport.rs | 7 ++++++ 3 files changed, 34 insertions(+), 11 deletions(-) create mode 100644 tests/rustdoc-html/inline_cross/auxiliary/impl-for-projection.rs create mode 100644 tests/rustdoc-html/inline_cross/impl-for-projection-reexport.rs diff --git a/src/librustdoc/passes/collect_trait_impls.rs b/src/librustdoc/passes/collect_trait_impls.rs index ed0b8d76930b0..1651690653786 100644 --- a/src/librustdoc/passes/collect_trait_impls.rs +++ b/src/librustdoc/passes/collect_trait_impls.rs @@ -9,6 +9,7 @@ use rustc_hir::def_id::{DefId, LOCAL_CRATE}; use rustc_hir::{Attribute, find_attr}; use rustc_middle::ty::{self, Ty, TyCtxt}; use rustc_span::kw; +use tracing::debug; use super::Pass; use crate::clean::*; @@ -50,24 +51,28 @@ pub(crate) fn collect_trait_impls(mut krate: Crate, cx: &mut DocContext<'_>) -> let _prof_timer = tcx.sess.prof.generic_activity("build_extern_trait_impls"); for &cnum in tcx.crates(()) { for &impl_def_id in tcx.trait_impls_in_crate(cnum) { - let opt_trait_ref = tcx.impl_opt_trait_ref(impl_def_id); - if opt_trait_ref.is_some_and(|trait_ref| { - crate_items.contains(&ItemId::DefId(trait_ref.def_id())) - || Some(trait_ref.def_id()) == tcx.lang_items().deref_trait() - || tcx.is_doc_notable_trait(trait_ref.def_id()) - }) { + let trait_ref = tcx.impl_trait_ref(impl_def_id); + debug!("considering extern trait impl {trait_ref:?}"); + if crate_items.contains(&ItemId::DefId(trait_ref.def_id())) + || Some(trait_ref.def_id()) == tcx.lang_items().deref_trait() + || tcx.is_doc_notable_trait(trait_ref.def_id()) + { + debug!("-> inlining due to trait"); cx.with_param_env(impl_def_id, |cx| { inline::build_impl(cx, impl_def_id, None, &mut new_items_external); }); } else { let self_ty = tcx.type_of(impl_def_id).instantiate_identity().skip_norm_wip(); + debug!(?self_ty); let self_ty_head = SelfTyHead::of(ty::Binder::dummy(self_ty), tcx, impl_def_id); + debug!(?self_ty_head); let keep_impl = match self_ty_head { SelfTyHead::Generic => true, SelfTyHead::Item(def_id) => crate_items.contains(&ItemId::DefId(def_id)), SelfTyHead::Primitive | SelfTyHead::Other => false, }; if keep_impl { + debug!("-> inlining due to self ty"); cx.with_param_env(impl_def_id, |cx| { inline::build_impl(cx, impl_def_id, None, &mut new_items_external); }); @@ -159,6 +164,7 @@ pub(crate) fn collect_trait_impls(mut krate: Crate, cx: &mut DocContext<'_>) -> krate } +#[derive(Debug)] enum SelfTyHead { Generic, Primitive, @@ -216,11 +222,8 @@ impl SelfTyHead { } ty::Alias(_, alias_ty @ ty::AliasTy { kind: ty::Projection { def_id }, .. }) => { - if tcx.is_impl_trait_in_trait(def_id) { - Self::Other - } else { - Self::of(bound_ty.rebind(alias_ty.self_ty()), tcx, parent) - } + debug_assert!(!tcx.is_impl_trait_in_trait(def_id)); + Self::of(bound_ty.rebind(alias_ty.self_ty()), tcx, parent) } ty::Alias(_, alias_ty @ ty::AliasTy { kind: ty::Inherent { .. }, .. }) => { diff --git a/tests/rustdoc-html/inline_cross/auxiliary/impl-for-projection.rs b/tests/rustdoc-html/inline_cross/auxiliary/impl-for-projection.rs new file mode 100644 index 0000000000000..be92bef13a1dd --- /dev/null +++ b/tests/rustdoc-html/inline_cross/auxiliary/impl-for-projection.rs @@ -0,0 +1,13 @@ +#![crate_name = "foo"] + +pub struct Struct; + +pub trait Tr1 {} +pub trait Tr2 { + type Assoc; +} +impl Tr2 for () { + type Assoc = Struct; +} + +impl Tr1 for <() as Tr2>::Assoc {} diff --git a/tests/rustdoc-html/inline_cross/impl-for-projection-reexport.rs b/tests/rustdoc-html/inline_cross/impl-for-projection-reexport.rs new file mode 100644 index 0000000000000..76c9d99fbddfc --- /dev/null +++ b/tests/rustdoc-html/inline_cross/impl-for-projection-reexport.rs @@ -0,0 +1,7 @@ +//@ aux-build:impl-for-projection.rs + +extern crate foo; + +// FIXME: because rustdoc doesn't normalize types, it doesn't inline the impl in foo +// that is for a projection that resolves to `Struct` +pub use foo::Struct;