Repository: rust-lang/rust
Pull request #138965: Remove `kw::Empty` uses from `hir::Lifetime::ident`
Author: login: nnethercote
Labels: name: T-compiler
description: Relevant to the compiler team, which will review and decide on the PR/issue.
name: S-waiting-on-bors
description: Status: Waiting on bors to run and complete tests. Bors will change the label on completion.
name: merged-by-bors
description: This PR was explicitly merged by bors.
Created: 2025-03-26T06:05:27Z | Merged: 2025-03-28T06:27:45Z

`hir::Lifetime::ident` is sometimes set to `kw::Empty` and it's really confusing. This PR stops that. Helps with #137978.

r? @lcnr


Diff:
diff --git a/compiler/rustc_ast_lowering/src/item.rs b/compiler/rustc_ast_lowering/src/item.rs
index c03d5e53d976b..43bf951eddc6c 100644
--- a/compiler/rustc_ast_lowering/src/item.rs
+++ b/compiler/rustc_ast_lowering/src/item.rs
@@ -5,7 +5,7 @@ use rustc_ast::*;
 use rustc_errors::ErrorGuaranteed;
 use rustc_hir::def::{DefKind, Res};
 use rustc_hir::def_id::{CRATE_DEF_ID, LocalDefId};
-use rustc_hir::{self as hir, HirId, PredicateOrigin};
+use rustc_hir::{self as hir, HirId, IsAnonInPath, PredicateOrigin};
 use rustc_index::{IndexSlice, IndexVec};
 use rustc_middle::ty::{ResolverAstLowering, TyCtxt};
 use rustc_span::edit_distance::find_best_match_for_name;
@@ -1823,7 +1823,7 @@ impl<'hir> LoweringContext<'_, 'hir> {
             }
             GenericParamKind::Lifetime => {
                 let lt_id = self.next_node_id();
-                let lifetime = self.new_named_lifetime(id, lt_id, ident);
+                let lifetime = self.new_named_lifetime(id, lt_id, ident, IsAnonInPath::No);
                 hir::WherePredicateKind::RegionPredicate(hir::WhereRegionPredicate {
                     lifetime,
                     bounds,
diff --git a/compiler/rustc_ast_lowering/src/lib.rs b/compiler/rustc_ast_lowering/src/lib.rs
index ced9064fd9f41..d5d6dcd8d631d 100644
--- a/compiler/rustc_ast_lowering/src/lib.rs
+++ b/compiler/rustc_ast_lowering/src/lib.rs
@@ -55,7 +55,8 @@ use rustc_errors::{DiagArgFromDisplay, DiagCtxtHandle, StashKey};
 use rustc_hir::def::{DefKind, LifetimeRes, Namespace, PartialRes, PerNS, Res};
 use rustc_hir::def_id::{CRATE_DEF_ID, LOCAL_CRATE, LocalDefId};
 use rustc_hir::{
-    self as hir, ConstArg, GenericArg, HirId, ItemLocalMap, LangItem, ParamName, TraitCandidate,
+    self as hir, ConstArg, GenericArg, HirId, IsAnonInPath, ItemLocalMap, LangItem, ParamName,
+    TraitCandidate,
 };
 use rustc_index::{Idx, IndexSlice, IndexVec};
 use rustc_macros::extension;
@@ -1755,7 +1756,11 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
     }
 
     fn lower_lifetime(&mut self, l: &Lifetime) -> &'hir hir::Lifetime {
-        self.new_named_lifetime(l.id, l.id, l.ident)
+        self.new_named_lifetime(l.id, l.id, l.ident, IsAnonInPath::No)
+    }
+
+    fn lower_lifetime_anon_in_path(&mut self, id: NodeId, span: Span) -> &'hir hir::Lifetime {
+        self.new_named_lifetime(id, id, Ident::new(kw::UnderscoreLifetime, span), IsAnonInPath::Yes)
     }
 
     #[instrument(level = "debug", skip(self))]
@@ -1764,28 +1769,43 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
         id: NodeId,
         new_id: NodeId,
         ident: Ident,
+        is_anon_in_path: IsAnonInPath,
     ) -> &'hir hir::Lifetime {
+        debug_assert_ne!(ident.name, kw::Empty);
         let res = self.resolver.get_lifetime_res(id).unwrap_or(LifetimeRes::Error);
         let res = match res {
             LifetimeRes::Param { param, .. } => hir::LifetimeName::Param(param),
             LifetimeRes::Fresh { param, .. } => {
+                debug_assert_eq!(ident.name, kw::UnderscoreLifetime);
                 let param = self.local_def_id(param);
                 hir::LifetimeName::Param(param)
             }
-            LifetimeRes::Infer => hir::LifetimeName::Infer,
-            LifetimeRes::Static { .. } => hir::LifetimeName::Static,
+            LifetimeRes::Infer => {
+                debug_assert_eq!(ident.name, kw::UnderscoreLifetime);
+                hir::LifetimeName::Infer
+            }
+            LifetimeRes::Static { .. } => {
+                debug_assert!(matches!(ident.name, kw::StaticLifetime | kw::UnderscoreLifetime));
+                hir::LifetimeName::Static
+            }
             LifetimeRes::Error => hir::LifetimeName::Error,
             LifetimeRes::ElidedAnchor { .. } => {
                 panic!("Unexpected `ElidedAnchar` {:?} at {:?}", ident, ident.span);
             }
         };
 
+        #[cfg(debug_assertions)]
+        if is_anon_in_path == IsAnonInPath::Yes {
+            debug_assert_eq!(ident.name, kw::UnderscoreLifetime);
+        }
+
         debug!(?res);
-        self.arena.alloc(hir::Lifetime {
-            hir_id: self.lower_node_id(new_id),
-            ident: self.lower_ident(ident),
+        self.arena.alloc(hir::Lifetime::new(
+            self.lower_node_id(new_id),
+            self.lower_ident(ident),
             res,
-        })
+            is_anon_in_path,
+        ))
     }
 
     fn lower_generic_params_mut(
@@ -2369,11 +2389,12 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
     /// when the bound is written, even if it is written with `'_` like in
     /// `Box<dyn Debug + '_>`. In those cases, `lower_lifetime` is invoked.
     fn elided_dyn_bound(&mut self, span: Span) -> &'hir hir::Lifetime {
-        let r = hir::Lifetime {
-            hir_id: self.next_id(),
-            ident: Ident::new(kw::Empty, self.lower_span(span)),
-            res: hir::LifetimeName::ImplicitObjectLifetimeDefault,
-        };
+        let r = hir::Lifetime::new(
+            self.next_id(),
+            Ident::new(kw::UnderscoreLifetime, self.lower_span(span)),
+            hir::LifetimeName::ImplicitObjectLifetimeDefault,
+            IsAnonInPath::No,
+        );
         debug!("elided_dyn_bound: r={:?}", r);
         self.arena.alloc(r)
     }
diff --git a/compiler/rustc_ast_lowering/src/path.rs b/compiler/rustc_ast_lowering/src/path.rs
index d00c797755f3f..c464c159c34cd 100644
--- a/compiler/rustc_ast_lowering/src/path.rs
+++ b/compiler/rustc_ast_lowering/src/path.rs
@@ -7,7 +7,7 @@ use rustc_hir::def::{DefKind, PartialRes, Res};
 use rustc_hir::def_id::DefId;
 use rustc_middle::span_bug;
 use rustc_session::parse::add_feature_diagnostics;
-use rustc_span::{BytePos, DUMMY_SP, DesugaringKind, Ident, Span, Symbol, kw, sym};
+use rustc_span::{BytePos, DUMMY_SP, DesugaringKind, Ident, Span, Symbol, sym};
 use smallvec::{SmallVec, smallvec};
 use tracing::{debug, instrument};
 
@@ -450,10 +450,7 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
             0,
             (start.as_u32()..end.as_u32()).map(|i| {
                 let id = NodeId::from_u32(i);
-                let l = self.lower_lifetime(&Lifetime {
-                    id,
-                    ident: Ident::new(kw::Empty, elided_lifetime_span),
-                });
+                let l = self.lower_lifetime_anon_in_path(id, elided_lifetime_span);
                 GenericArg::Lifetime(l)
             }),
         );
diff --git a/compiler/rustc_borrowck/src/diagnostics/region_errors.rs b/compiler/rustc_borrowck/src/diagnostics/region_errors.rs
index 6b11f1a3681ff..50a18b04de4ef 100644
--- a/compiler/rustc_borrowck/src/diagnostics/region_errors.rs
+++ b/compiler/rustc_borrowck/src/diagnostics/region_errors.rs
@@ -513,14 +513,14 @@ impl<'infcx, 'tcx> MirBorrowckCtxt<'_, 'infcx, 'tcx> {
             ty::VarianceDiagInfo::Invariant { ty, param_index } => {
                 let (desc, note) = match ty.kind() {
                     ty::RawPtr(ty, mutbl) => {
-                        assert_eq!(*mutbl, rustc_hir::Mutability::Mut);
+                        assert_eq!(*mutbl, hir::Mutability::Mut);
                         (
                             format!("a mutable pointer to `{}`", ty),
                             "mutable pointers are invariant over their type parameter".to_string(),
                         )
                     }
                     ty::Ref(_, inner_ty, mutbl) => {
-                        assert_eq!(*mutbl, rustc_hir::Mutability::Mut);
+                        assert_eq!(*mutbl, hir::Mutability::Mut);
                         (
                             format!("a mutable reference to `{inner_ty}`"),
                             "mutable references are invariant over their type parameter"
@@ -887,7 +887,7 @@ impl<'infcx, 'tcx> MirBorrowckCtxt<'_, 'infcx, 'tcx> {
                     // Skip `async` desugaring `impl Future`.
                 }
                 if let TyKind::TraitObject(_, lt) = alias_ty.kind {
-                    if lt.ident.name == kw::Empty {
+                    if lt.res == hir::LifetimeName::ImplicitObjectLifetimeDefault {
                         spans_suggs.push((lt.ident.span.shrink_to_hi(), " + 'a".to_string()));
                     } else {
                         spans_suggs.push((lt.ident.span, "'a".to_string()));
diff --git a/compiler/rustc_hir/src/hir.rs b/compiler/rustc_hir/src/hir.rs
index 751c379b21a64..e3e96894ed1f7 100644
--- a/compiler/rustc_hir/src/hir.rs
+++ b/compiler/rustc_hir/src/hir.rs
@@ -35,20 +35,60 @@ use crate::def_id::{DefId, LocalDefIdMap};
 pub(crate) use crate::hir_id::{HirId, ItemLocalId, ItemLocalMap, OwnerId};
 use crate::intravisit::{FnKind, VisitorExt};
 
+#[derive(Debug, Copy, Clone, PartialEq, Eq, HashStable_Generic)]
+pub enum IsAnonInPath {
+    No,
+    Yes,
+}
+
+/// A lifetime. The valid field combinations are non-obvious. The following
+/// example shows some of them. See also the comments on `LifetimeName`.
+/// ```
+/// #[repr(C)]
+/// struct S<'a>(&'a u32);       // res=Param, name='a, IsAnonInPath::No
+/// unsafe extern "C" {
+///     fn f1(s: S);             // res=Param, name='_, IsAnonInPath::Yes
+///     fn f2(s: S<'_>);         // res=Param, name='_, IsAnonInPath::No
+///     fn f3<'a>(s: S<'a>);     // res=Param, name='a, IsAnonInPath::No
+/// }
+///
+/// struct St<'a> { x: &'a u32 } // res=Param, name='a, IsAnonInPath::No
+/// fn f() {
+///     _ = St { x: &0 };        // res=Infer, name='_, IsAnonInPath::Yes
+///     _ = St::<'_> { x: &0 };  // res=Infer, name='_, IsAnonInPath::No
+/// }
+///
+/// struct Name<'a>(&'a str);    // res=Param,  name='a, IsAnonInPath::No
+/// const A: Name = Name("a");   // res=Static, name='_, IsAnonInPath::Yes
+/// const B: &str = "";          // res=Static, name='_, IsAnonInPath::No
+/// static C: &'_ str = "";      // res=Static, name='_, IsAnonInPath::No
+/// static D: &'static str = ""; // res=Static, name='static, IsAnonInPath::No
+///
+/// trait Tr {}
+/// fn tr(_: Box<dyn Tr>) {}     // res=ImplicitObjectLifetimeDefault, name='_, IsAnonInPath::No
+///
+/// // (commented out because these cases trigger errors)
+/// // struct S1<'a>(&'a str);   // res=Param, name='a, IsAnonInPath::No
+/// // struct S2(S1);            // res=Error, name='_, IsAnonInPath::Yes
+/// // struct S3(S1<'_>);        // res=Error, name='_, IsAnonInPath::No
+/// // struct S4(S1<'a>);        // res=Error, name='a, IsAnonInPath::No
+/// ```
 #[derive(Debug, Copy, Clone, HashStable_Generic)]
 pub struct Lifetime {
     #[stable_hasher(ignore)]
     pub hir_id: HirId,
 
-    /// Either "`'a`", referring to a named lifetime definition,
-    /// `'_` referring to an anonymous lifetime (either explicitly `'_` or `&type`),
-    /// or "``" (i.e., `kw::Empty`) when appearing in path.
-    ///
-    /// See `Lifetime::suggestion_position` for practical use.
+    /// Either a named lifetime definition (e.g. `'a`, `'static`) or an
+    /// anonymous lifetime (`'_`, either explicitly written, or inserted for
+    /// things like `&type`).
     pub ident: Ident,
 
     /// Semantics of this lifetime.
     pub res: LifetimeName,
+
+    /// Is the lifetime anonymous and in a path? Used only for error
+    /// suggestions. See `Lifetime::suggestion` for example use.
+    pub is_anon_in_path: IsAnonInPath,
 }
 
 #[derive(Debug, Copy, Clone, HashStable_Generic)]
@@ -111,11 +151,12 @@ pub enum LifetimeName {
     /// that was already reported.
     Error,
 
-    /// User wrote an anonymous lifetime, either `'_` or nothing.
-    /// The semantics of this lifetime should be inferred by typechecking code.
+    /// User wrote an anonymous lifetime, either `'_` or nothing (which gets
+    /// converted to `'_`). The semantics of this lifetime should be inferred
+    /// by typechecking code.
     Infer,
 
-    /// User wrote `'static`.
+    /// User wrote `'static` or nothing (which gets converted to `'_`).
     Static,
 }
 
@@ -135,59 +176,57 @@ impl LifetimeName {
 
 impl fmt::Display for Lifetime {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        if self.ident.name != kw::Empty { self.ident.name.fmt(f) } else { "'_".fmt(f) }
+        self.ident.name.fmt(f)
     }
 }
 
-pub enum LifetimeSuggestionPosition {
-    /// The user wrote `'a` or `'_`.
-    Normal,
-    /// The user wrote `&type` or `&mut type`.
-    Ampersand,
-    /// The user wrote `Path` and omitted the `<'_>`.
-    ElidedPath,
-    /// The user wrote `Path<T>`, and omitted the `'_,`.
-    ElidedPathArgument,
-    /// The user wrote `dyn Trait` and omitted the `+ '_`.
-    ObjectDefault,
-}
-
 impl Lifetime {
+    pub fn new(
+        hir_id: HirId,
+        ident: Ident,
+        res: LifetimeName,
+        is_anon_in_path: IsAnonInPath,
+    ) -> Lifetime {
+        let lifetime = Lifetime { hir_id, ident, res, is_anon_in_path };
+
+        // Sanity check: elided lifetimes form a strict subset of anonymous lifetimes.
+        #[cfg(debug_assertions)]
+        match (lifetime.is_elided(), lifetime.is_anonymous()) {
+            (false, false) => {} // e.g. `'a`
+            (false, true) => {}  // e.g. explicit `'_`
+            (true, true) => {}   // e.g. `&x`
+            (true, false) => panic!("bad Lifetime"),
+        }
+
+        lifetime
+    }
+
     pub fn is_elided(&self) -> bool {
         self.res.is_elided()
     }
 
     pub fn is_anonymous(&self) -> bool {
-        self.ident.name == kw::Empty || self.ident.name == kw::UnderscoreLifetime
-    }
-
-    pub fn suggestion_position(&self) -> (LifetimeSuggestionPosition, Span) {
-        if self.ident.name == kw::Empty {
-            if self.ident.span.is_empty() {
-                (LifetimeSuggestionPosition::ElidedPathArgument, self.ident.span)
-            } else {
-                (LifetimeSuggestionPosition::ElidedPath, self.ident.span.shrink_to_hi())
-            }
-        } else if self.res == LifetimeName::ImplicitObjectLifetimeDefault {
-            (LifetimeSuggestionPosition::ObjectDefault, self.ident.span)
-        } else if self.ident.span.is_empty() {
-            (LifetimeSuggestionPosition::Ampersand, self.ident.span)
-        } else {
-            (LifetimeSuggestionPosition::Normal, self.ident.span)
-        }
+        self.ident.name == kw::UnderscoreLifetime
     }
 
     pub fn suggestion(&self, new_lifetime: &str) -> (Span, String) {
         debug_assert!(new_lifetime.starts_with('\''));
-        let (pos, span) = self.suggestion_position();
-        let code = match pos {
-            LifetimeSuggestionPosition::Normal => format!("{new_lifetime}"),
-            LifetimeSuggestionPosition::Ampersand => format!("{new_lifetime} "),
-            LifetimeSuggestionPosition::ElidedPath => format!("<{new_lifetime}>"),
-            LifetimeSuggestionPosition::ElidedPathArgument => format!("{new_lifetime}, "),
-            LifetimeSuggestionPosition::ObjectDefault => format!("+ {new_lifetime}"),
-        };
-        (span, code)
+
+        match (self.is_anon_in_path, self.ident.span.is_empty()) {
+            // The user wrote `Path<T>`, and omitted the `'_,`.
+            (IsAnonInPath::Yes, true) => (self.ident.span, format!("{new_lifetime}, ")),
+
+            // The user wrote `Path` and omitted the `<'_>`.
+            (IsAnonInPath::Yes, false) => {
+                (self.ident.span.shrink_to_hi(), format!("<{new_lifetime}>"))
+            }
+
+            // The user wrote `&type` or `&mut type`.
+            (IsAnonInPath::No, true) => (self.ident.span, format!("{new_lifetime} ")),
+
+            // The user wrote `'a` or `'_`.
+            (IsAnonInPath::No, false) => (self.ident.span, format!("{new_lifetime}")),
+        }
     }
 }
 
diff --git a/compiler/rustc_hir/src/hir/tests.rs b/compiler/rustc_hir/src/hir/tests.rs
index f75b9662132e8..62ef02d2f500c 100644
--- a/compiler/rustc_hir/src/hir/tests.rs
+++ b/compiler/rustc_hir/src/hir/tests.rs
@@ -58,6 +58,7 @@ fn trait_object_roundtrips_impl(syntax: TraitObjectSyntax) {
                     hir_id: HirId::INVALID,
                     ident: Ident::new(sym::name, DUMMY_SP),
                     res: LifetimeName::Static,
+                    is_anon_in_path: IsAnonInPath::No,
                 }
             },
             syntax,
diff --git a/compiler/rustc_trait_selection/src/errors.rs b/compiler/rustc_trait_selection/src/errors.rs
index 7159397c4b1a4..b30390a9330eb 100644
--- a/compiler/rustc_trait_selection/src/errors.rs
+++ b/compiler/rustc_trait_selection/src/errors.rs
@@ -9,7 +9,7 @@ use rustc_errors::{
 use rustc_hir::def::DefKind;
 use rustc_hir::def_id::{DefId, LocalDefId};
 use rustc_hir::intravisit::{Visitor, VisitorExt, walk_ty};
-use rustc_hir::{self as hir, AmbigArg, FnRetTy, GenericParamKind, Node};
+use rustc_hir::{self as hir, AmbigArg, FnRetTy, GenericParamKind, IsAnonInPath, Node};
 use rustc_macros::{Diagnostic, Subdiagnostic};
 use rustc_middle::ty::print::{PrintTraitRefExt as _, TraitRefPrintOnlyTraitPath};
 use rustc_middle::ty::{self, Binder, ClosureKind, FnSig, Region, Ty, TyCtxt};
@@ -567,10 +567,14 @@ impl Subdiagnostic for AddLifetimeParamsSuggestion<'_> {
 
             impl<'v> Visitor<'v> for ImplicitLifetimeFinder {
                 fn visit_ty(&mut self, ty: &'v hir::Ty<'v, AmbigArg>) {
-                    let make_suggestion = |ident: Ident| {
-                        if ident.name == kw::Empty && ident.span.is_empty() {
+                    let make_suggestion = |lifetime: &hir::Lifetime| {
+                        if lifetime.is_anon_in_path == IsAnonInPath::Yes
+                            && lifetime.ident.span.is_empty()
+                        {
                             format!("{}, ", self.suggestion_param_name)
-                        } else if ident.name == kw::UnderscoreLifetime && ident.span.is_empty() {
+                        } else if lifetime.ident.name == kw::UnderscoreLifetime
+                            && lifetime.ident.span.is_empty()
+                        {
                             format!("{} ", self.suggestion_param_name)
                         } else {
                             self.suggestion_param_name.clone()
@@ -584,7 +588,7 @@ impl Subdiagnostic for AddLifetimeParamsSuggestion<'_> {
                                         matches!(
                                             arg,
                                             hir::GenericArg::Lifetime(lifetime)
-                                            if lifetime.ident.name == kw::Empty
+                                                if lifetime.is_anon_in_path == IsAnonInPath::Yes
                                         )
                                     }) {
                                         self.suggestions.push((
@@ -605,7 +609,7 @@ impl Subdiagnostic for AddLifetimeParamsSuggestion<'_> {
                                             {
                                                 self.suggestions.push((
                                                     lifetime.ident.span,
-                                                    make_suggestion(lifetime.ident),
+                                                    make_suggestion(lifetime),
                                                 ));
                                             }
                                         }
@@ -614,8 +618,7 @@ impl Subdiagnostic for AddLifetimeParamsSuggestion<'_> {
                             }
                         }
                         hir::TyKind::Ref(lifetime, ..) if lifetime.is_anonymous() => {
-                            self.suggestions
-                                .push((lifetime.ident.span, make_suggestion(lifetime.ident)));
+                            self.suggestions.push((lifetime.ident.span, make_suggestion(lifetime)));
                         }
                         _ => {}
                     }
diff --git a/tests/pretty/hir-lifetimes.pp b/tests/pretty/hir-lifetimes.pp
new file mode 100644
index 0000000000000..e545b0c8f5764
--- /dev/null
+++ b/tests/pretty/hir-lifetimes.pp
@@ -0,0 +1,96 @@
+//@ pretty-compare-only
+//@ pretty-mode:hir
+//@ pp-exact:hir-lifetimes.pp
+
+// This tests the pretty-printing of lifetimes in lots of ways.
+
+#![allow(unused)]
+#[prelude_import]
+use ::std::prelude::rust_2015::*;
+#[macro_use]
+extern crate std;
+
+struct Foo<'a> {
+    x: &'a u32,
+}
+
+impl <'a> Foo<'a> {
+    fn f<'b>(x: &'b u32) { }
+}
+
+impl  Foo<'_> {
+    fn a(x: &'_ u32) { }
+
+    fn b(x: &'_ u32) { }
+
+    fn c(x: &'_ u32, y: &'static u32) { }
+
+    // FIXME: `'a` before `self` is omitted
+    fn d<'a>(&self, x: &'a u32) { }
+
+    // FIXME: impl Traits printed as just `/*impl Trait*/`, ugh
+    fn iter1<'a>(&self)
+        -> /*impl Trait*/ { #[lang = "Range"]{ start: 0,  end: 1,} }
+
+    fn iter2(&self)
+        -> /*impl Trait*/ { #[lang = "Range"]{ start: 0,  end: 1,} }
+}
+
+fn a(x: Foo<'_>) { }
+
+fn b<'a>(x: Foo<'a>) { }
+
+struct Bar<'a, 'b, 'c, T> {
+    x: &'a u32,
+    y: &'b &'c u32,
+    z: T,
+}
+
+fn f1<'a, 'b, T>(x: Bar<'a, 'b, '_, T>) { }
+
+fn f2(x: Bar<'_, '_, '_, u32>) { }
+
+trait MyTrait<'a, 'b> {
+    fn f(&self, x: Foo<'a>, y: Foo<'b>);
+}
+
+impl <'a, 'b, 'c, T> MyTrait<'a, 'b> for Bar<'a, 'b, 'c, T> {
+    fn f(&self, x: Foo<'a>, y: Foo<'b>) { }
+}
+
+fn g(x: &'_ dyn for<'a, 'b> MyTrait<'a, 'b>) { }
+
+trait Blah { }
+
+type T<'a> = dyn Blah + 'a;
+
+type Q<'a> = dyn MyTrait<'a, 'a> + 'a;
+
+fn h<'b, F>(f: F, y: Foo<'b>) where F: for<'d> MyTrait<'d, 'b> { }
+
+// FIXME(?): attr printing is weird
+#[attr = Repr([ReprC])]
+struct S<'a>(&'a u32);
+
+extern "C" {
+    unsafe fn g1(s: S<'_>);
+    unsafe fn g2(s: S<'_>);
+    unsafe fn g3<'a>(s: S<'a>);
+}
+
+struct St<'a> {
+    x: &'a u32,
+}
+
+fn f() { { let _ = St{ x: &0,}; }; { let _ = St{ x: &0,}; }; }
+
+struct Name<'a>(&'a str);
+
+const A: Name<'_> = Name("a");
+const B: &'_ str = "";
+static C: &'_ str = "";
+static D: &'static str = "";
+
+fn tr(_: Box<dyn Blah>) { }
+
+fn main() { }
diff --git a/tests/pretty/hir-lifetimes.rs b/tests/pretty/hir-lifetimes.rs
new file mode 100644
index 0000000000000..1379be343924e
--- /dev/null
+++ b/tests/pretty/hir-lifetimes.rs
@@ -0,0 +1,91 @@
+//@ pretty-compare-only
+//@ pretty-mode:hir
+//@ pp-exact:hir-lifetimes.pp
+
+// This tests the pretty-printing of lifetimes in lots of ways.
+
+#![allow(unused)]
+
+struct Foo<'a> {
+    x: &'a u32,
+}
+
+impl<'a> Foo<'a> {
+    fn f<'b>(x: &'b u32) {}
+}
+
+impl Foo<'_> {
+    fn a(x: &u32) {}
+
+    fn b(x: &'_ u32) {}
+
+    fn c(x: &'_ u32, y: &'static u32) {}
+
+    // FIXME: `'a` before `self` is omitted
+    fn d<'a>(&'a self, x: &'a u32) {}
+
+    // FIXME: impl Traits printed as just `/*impl Trait*/`, ugh
+    fn iter1<'a>(&self) -> impl Iterator<Item = u32> + 'a { 0..1 }
+
+    fn iter2(&self) -> impl Iterator<Item = u32> + '_ { 0..1 }
+}
+
+fn a(x: Foo<'_>) {}
+
+fn b<'a>(x: Foo<'a>) {}
+
+struct Bar<'a, 'b, 'c, T> {
+    x: &'a u32,
+    y: &'b &'c u32,
+    z: T,
+}
+
+fn f1<'a, 'b, T>(x: Bar<'a, 'b, '_, T>) {}
+
+fn f2(x: Bar<'_, '_, '_, u32>) {}
+
+trait MyTrait<'a, 'b> {
+    fn f(&self, x: Foo<'a>, y: Foo<'b>);
+}
+
+impl<'a, 'b, 'c, T> MyTrait<'a, 'b> for Bar<'a, 'b, 'c, T> {
+    fn f(&self, x: Foo<'a>, y: Foo<'b>) {}
+}
+
+fn g(x: &dyn for<'a, 'b> MyTrait<'a, 'b>) {}
+
+trait Blah {}
+
+type T<'a> = dyn Blah + 'a;
+
+type Q<'a> = dyn MyTrait<'a, 'a> + 'a;
+
+fn h<'b, F>(f: F, y: Foo<'b>) where F: for<'d> MyTrait<'d, 'b> {}
+
+// FIXME(?): attr printing is weird
+#[repr(C)]
+struct S<'a>(&'a u32);
+
+extern "C" {
+    fn g1(s: S);
+    fn g2(s: S<'_>);
+    fn g3<'a>(s: S<'a>);
+}
+
+struct St<'a> { x: &'a u32 }
+
+fn f() {
+    _ = St { x: &0 };
+    _ = St::<'_> { x: &0 };
+}
+
+struct Name<'a>(&'a str);
+
+const A: Name = Name("a");
+const B: &str = "";
+static C: &'_ str = "";
+static D: &'static str = "";
+
+fn tr(_: Box<dyn Blah>) {}
+
+fn main() {}
diff --git a/tests/ui/stats/input-stats.stderr b/tests/ui/stats/input-stats.stderr
index dbc9e7d254c87..191daff2137d8 100644
--- a/tests/ui/stats/input-stats.stderr
+++ b/tests/ui/stats/input-stats.stderr
@@ -119,7 +119,7 @@ hir-stats HIR STATS
 hir-stats Name                Accumulated Size         Count     Item Size
 hir-stats ----------------------------------------------------------------
 hir-stats ForeignItemRef            24 ( 0.3%)             1            24
-hir-stats Lifetime                  24 ( 0.3%)             1            24
+hir-stats Lifetime                  28 ( 0.3%)             1            28
 hir-stats Mod                       32 ( 0.4%)             1            32
 hir-stats ExprField                 40 ( 0.4%)             1            40
 hir-stats TraitItemRef              56 ( 0.6%)             2            28
@@ -155,7 +155,7 @@ hir-stats Generics                 560 ( 6.2%)            10            56
 hir-stats Ty                       720 ( 8.0%)            15            48
 hir-stats - Ptr                       48 ( 0.5%)             1
 hir-stats - Ref                       48 ( 0.5%)             1
-hir-stats - Path                     624 ( 7.0%)            13
+hir-stats - Path                     624 ( 6.9%)            13
 hir-stats Expr                     768 ( 8.6%)            12            64
 hir-stats - InlineAsm                 64 ( 0.7%)             1
 hir-stats - Match                     64 ( 0.7%)             1
@@ -174,5 +174,5 @@ hir-stats - Use                      352 ( 3.9%)             4
 hir-stats Path                   1_240 (13.8%)            31            40
 hir-stats PathSegment            1_920 (21.4%)            40            48
 hir-stats ----------------------------------------------------------------
-hir-stats Total                  8_976                   180
+hir-stats Total                  8_980                   180
 hir-stats


Reviews:
user: login: lcnr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T15:33:38Z
user: login: lcnr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T15:34:23Z
user: login: lcnr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T15:35:19Z
user: login: lcnr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T15:39:36Z
user: login: nnethercote
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T22:24:12Z
user: login: nnethercote
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T22:24:25Z
user: login: nnethercote
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T22:49:29Z
user: login: nnethercote
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T02:34:52Z
user: login: lcnr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T11:24:45Z
user: login: lcnr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T11:26:00Z
user: login: lcnr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T11:26:33Z
user: login: lcnr
body: a suggestion and one final nit

r=me after fixing the nit if u don't apply the suggestion
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T11:28:13Z
user: login: nnethercote
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T23:12:50Z
user: login: nnethercote
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T23:24:38Z

Review comments:
diff_hunk: @@ -1777,7 +1778,11 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
     }
 
     fn lower_lifetime(&mut self, l: &Lifetime) -> &'hir hir::Lifetime {
-        self.new_named_lifetime(l.id, l.id, l.ident)
+        self.new_named_lifetime(l.id, l.id, l.ident, IsPathAnon::No)
+    }
+
+    fn lower_lifetime_is_path_anon(&mut self, id: NodeId, span: Span) -> &'hir hir::Lifetime {
path: compiler/rustc_ast_lowering/src/lib.rs
user: login: lcnr
body: ```suggestion
    fn lower_lifetime_path_anon(&mut self, id: NodeId, span: Span) -> &'hir hir::Lifetime {
```
or `lower_lifetime_in_path_anon` or sth 😅 i'd expect a function with "_is_X" to have an argument to set X
created_at: 2025-03-26T15:33:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1786,28 +1791,38 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
         id: NodeId,
         new_id: NodeId,
         ident: Ident,
+        is_path_anon: IsPathAnon,
     ) -> &'hir hir::Lifetime {
+        debug_assert_ne!(ident.name, kw::Empty);
         let res = self.resolver.get_lifetime_res(id).unwrap_or(LifetimeRes::Error);
         let res = match res {
             LifetimeRes::Param { param, .. } => hir::LifetimeName::Param(param),
             LifetimeRes::Fresh { param, .. } => {
+                debug_assert_eq!(ident.name, kw::UnderscoreLifetime);
                 let param = self.local_def_id(param);
                 hir::LifetimeName::Param(param)
             }
-            LifetimeRes::Infer => hir::LifetimeName::Infer,
-            LifetimeRes::Static { .. } => hir::LifetimeName::Static,
+            LifetimeRes::Infer => {
+                debug_assert_eq!(ident.name, kw::UnderscoreLifetime);
+                hir::LifetimeName::Infer
+            }
+            LifetimeRes::Static { .. } => {
+                debug_assert!(matches!(ident.name, kw::StaticLifetime | kw::UnderscoreLifetime));
path: compiler/rustc_ast_lowering/src/lib.rs
user: login: lcnr
body: when does this happen to be an underscore lifetime?
created_at: 2025-03-26T15:34:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,20 +35,27 @@ use crate::def_id::{DefId, LocalDefIdMap};
 pub(crate) use crate::hir_id::{HirId, ItemLocalId, ItemLocalMap, OwnerId};
 use crate::intravisit::{FnKind, VisitorExt};
 
+#[derive(Debug, Copy, Clone, PartialEq, Eq, HashStable_Generic)]
+pub enum IsPathAnon {
+    No,
+    Yes,
+}
+
 #[derive(Debug, Copy, Clone, HashStable_Generic)]
 pub struct Lifetime {
     #[stable_hasher(ignore)]
     pub hir_id: HirId,
 
-    /// Either "`'a`", referring to a named lifetime definition,
-    /// `'_` referring to an anonymous lifetime (either explicitly `'_` or `&type`),
-    /// or "``" (i.e., `kw::Empty`) when appearing in path.
-    ///
-    /// See `Lifetime::suggestion_position` for practical use.
+    /// Either a named lifetime definition (e.g. `'a`) or an anonymous lifetime
+    /// (`'_`, either explicitly written, or inserted for things like `&type`).
     pub ident: Ident,
 
     /// Semantics of this lifetime.
     pub res: LifetimeName,
+
+    /// Is the lifetime anonymous and in a path? Used only for error
+    /// suggestions. See `Lifetime::suggestion` for example use.
+    pub is_path_anon: IsPathAnon,
path: compiler/rustc_hir/src/hir.rs
user: login: lcnr
body: `IsAnonInPath`? `IsPathAnon` feels very ambiguous to me
created_at: 2025-03-26T15:35:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2391,11 +2406,12 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
     /// when the bound is written, even if it is written with `'_` like in
     /// `Box<dyn Debug + '_>`. In those cases, `lower_lifetime` is invoked.
     fn elided_dyn_bound(&mut self, span: Span) -> &'hir hir::Lifetime {
-        let r = hir::Lifetime {
-            hir_id: self.next_id(),
-            ident: Ident::new(kw::Empty, self.lower_span(span)),
-            res: hir::LifetimeName::ImplicitObjectLifetimeDefault,
-        };
+        let r = hir::Lifetime::new(
+            self.next_id(),
+            Ident::new(kw::UnderscoreLifetime, self.lower_span(span)),
+            hir::LifetimeName::ImplicitObjectLifetimeDefault,
+            IsPathAnon::No,
path: compiler/rustc_ast_lowering/src/lib.rs
user: login: lcnr
body: would it be easier to have a `LifetimeName::Anon|ElidedInPath`? and not have two enums inside of `hir::Lifetime`?
created_at: 2025-03-26T15:39:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1777,7 +1778,11 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
     }
 
     fn lower_lifetime(&mut self, l: &Lifetime) -> &'hir hir::Lifetime {
-        self.new_named_lifetime(l.id, l.id, l.ident)
+        self.new_named_lifetime(l.id, l.id, l.ident, IsPathAnon::No)
+    }
+
+    fn lower_lifetime_is_path_anon(&mut self, id: NodeId, span: Span) -> &'hir hir::Lifetime {
path: compiler/rustc_ast_lowering/src/lib.rs
user: login: nnethercote
body: I'll do `lower_lifetime_anon_in_path` and `IsAnonInPath` for the typename.
created_at: 2025-03-26T22:24:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,20 +35,27 @@ use crate::def_id::{DefId, LocalDefIdMap};
 pub(crate) use crate::hir_id::{HirId, ItemLocalId, ItemLocalMap, OwnerId};
 use crate::intravisit::{FnKind, VisitorExt};
 
+#[derive(Debug, Copy, Clone, PartialEq, Eq, HashStable_Generic)]
+pub enum IsPathAnon {
+    No,
+    Yes,
+}
+
 #[derive(Debug, Copy, Clone, HashStable_Generic)]
 pub struct Lifetime {
     #[stable_hasher(ignore)]
     pub hir_id: HirId,
 
-    /// Either "`'a`", referring to a named lifetime definition,
-    /// `'_` referring to an anonymous lifetime (either explicitly `'_` or `&type`),
-    /// or "``" (i.e., `kw::Empty`) when appearing in path.
-    ///
-    /// See `Lifetime::suggestion_position` for practical use.
+    /// Either a named lifetime definition (e.g. `'a`) or an anonymous lifetime
+    /// (`'_`, either explicitly written, or inserted for things like `&type`).
     pub ident: Ident,
 
     /// Semantics of this lifetime.
     pub res: LifetimeName,
+
+    /// Is the lifetime anonymous and in a path? Used only for error
+    /// suggestions. See `Lifetime::suggestion` for example use.
+    pub is_path_anon: IsPathAnon,
path: compiler/rustc_hir/src/hir.rs
user: login: nnethercote
body: Ok, `IsAnonInPath` works.
created_at: 2025-03-26T22:24:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1786,28 +1791,38 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
         id: NodeId,
         new_id: NodeId,
         ident: Ident,
+        is_path_anon: IsPathAnon,
     ) -> &'hir hir::Lifetime {
+        debug_assert_ne!(ident.name, kw::Empty);
         let res = self.resolver.get_lifetime_res(id).unwrap_or(LifetimeRes::Error);
         let res = match res {
             LifetimeRes::Param { param, .. } => hir::LifetimeName::Param(param),
             LifetimeRes::Fresh { param, .. } => {
+                debug_assert_eq!(ident.name, kw::UnderscoreLifetime);
                 let param = self.local_def_id(param);
                 hir::LifetimeName::Param(param)
             }
-            LifetimeRes::Infer => hir::LifetimeName::Infer,
-            LifetimeRes::Static { .. } => hir::LifetimeName::Static,
+            LifetimeRes::Infer => {
+                debug_assert_eq!(ident.name, kw::UnderscoreLifetime);
+                hir::LifetimeName::Infer
+            }
+            LifetimeRes::Static { .. } => {
+                debug_assert!(matches!(ident.name, kw::StaticLifetime | kw::UnderscoreLifetime));
path: compiler/rustc_ast_lowering/src/lib.rs
user: login: nnethercote
body: When the `'static` is elided, e.g. for both `C` and `D` here:
```
struct Name<'a>(&'a str);
const C: Name = Name("name"); // IsAnonInPath::Yes
static D: &str = "";          // IsAnonInPath::No
```
I didn't even realize `'static` could be elided like this!

I will add a comment about this.

created_at: 2025-03-26T22:49:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2391,11 +2406,12 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
     /// when the bound is written, even if it is written with `'_` like in
     /// `Box<dyn Debug + '_>`. In those cases, `lower_lifetime` is invoked.
     fn elided_dyn_bound(&mut self, span: Span) -> &'hir hir::Lifetime {
-        let r = hir::Lifetime {
-            hir_id: self.next_id(),
-            ident: Ident::new(kw::Empty, self.lower_span(span)),
-            res: hir::LifetimeName::ImplicitObjectLifetimeDefault,
-        };
+        let r = hir::Lifetime::new(
+            self.next_id(),
+            Ident::new(kw::UnderscoreLifetime, self.lower_span(span)),
+            hir::LifetimeName::ImplicitObjectLifetimeDefault,
+            IsPathAnon::No,
path: compiler/rustc_ast_lowering/src/lib.rs
user: login: nnethercote
body: I originally wanted to combine the two enums in exactly this fashion, but it turns out that `IsAnonInPath::Yes` can occur with four of the five `LifetimeName` variants, viz:
```rust
#![allow(unused)]

//---------------------------------------------------------------------------
struct Name<'a>(&'a str);
const C: Name = Name("name"); // LifetimeRes::Static, name='_, IsAnonInPath::Yes
static D: &str = "";          // LifetimeRes::Static, name='_, IsAnonInPath::No

//---------------------------------------------------------------------------
#[repr(C)]
struct Safe<'a>(&'a u32);
extern "C" {
    fn foo(safe: Safe);     // LifetimeRes::Param, name='_, IsAnonInPath::Yes
}

//---------------------------------------------------------------------------
struct St<'a> { f1: &'a u32 }

fn f() {
    let st = St { f1: &0u32 };  // LifetimeRes::Infer, name='_, IsAnonInPath::Yes
}

//---------------------------------------------------------------------------
// commented out because it causes an error
//struct Baz<'a>(&'a str);    // LifetimeRes::Error, name='_, IsAnonInPath::Yes
//struct Baz2(Baz);

fn main() {}
```
Once again, lifetimes can be omitted from paths in more places than I realized.

If you wanted to combine `ident` *and* `res` *and* `is_anon_in_path` you'd have an enum like this:
```rust
enum Combined {
    ParamUserNamed(Ident),         // user-specified name, IsAnonInPath::No
    ParamAnon(Span, IsAnonInPath), // name='_

    Infer(Span, IsAnonInPath),     // name='_

    StaticNamed(Span),             // name='static, IsAnonInPath::No
    StaticAnon(IsAnonInPath),      // name='_

    Error(Ident),                  // user-specified name, IsAnonInPath::No
    ErrorAnon(Span, IsAnonInPath), // name='_

    ImplicitObjectLifetimeDefault, // name='_, IsAnonInPath::No
}
```
I *think* that captures all the possibilities and excludes all invalid combinations. I'm generally very much a fan of "don't let your types express invalid values" but this feels like it's taking it too far?
created_at: 2025-03-27T02:34:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,20 +35,60 @@ use crate::def_id::{DefId, LocalDefIdMap};
 pub(crate) use crate::hir_id::{HirId, ItemLocalId, ItemLocalMap, OwnerId};
 use crate::intravisit::{FnKind, VisitorExt};
 
+#[derive(Debug, Copy, Clone, PartialEq, Eq, HashStable_Generic)]
+pub enum IsAnonInPath {
+    No,
+    Yes,
+}
+
+/// A lifetime. The valid field combinations are non-obvious. The following
+/// example shows some of them. See also the comments on `LifetimeName`.
+/// ```
+/// #[repr(C)]
+/// struct S<'a>(&'a u32);       // LifetimeName::Param, name='a, IsAnonInPath::No
+/// unsafe extern "C" {
+///     fn f1(s: S);             // LifetimeName::Param, name='_, IsAnonInPath::Yes
+///     fn f2(s: S<'_>);         // LifetimeName::Param, name='_, IsAnonInPath::No
+///     fn f3<'a>(s: S<'a>);     // LifetimeName::Param, name='a, IsAnonInPath::No
+/// }
+///
+/// struct St<'a> { x: &'a u32 } // LifetimeName::Param, name='a, IsAnonInPath::No
+/// fn f() {
+///     _ = St { x: &0 };        // LifetimeName::Infer, name='_, IsAnonInPath::Yes
+///     _ = St::<'_> { x: &0 };  // LifetimeName::Infer, name='_, IsAnonInPath::No
+/// }
+///
+/// struct Name<'a>(&'a str);    // LifetimeName::Param,  name='a, IsAnonInPath::No
+/// const A: Name = Name("a");   // LifetimeName::Static, name='_, IsAnonInPath::Yes
+/// const B: &str = "";          // LifetimeName::Static, name='_, IsAnonInPath::No
path: compiler/rustc_hir/src/hir.rs
user: login: lcnr
body: so this PR doesn't change behavior which is fine, however. Even with this cleanup the status quo still feels confusing to me.

I feel like maybe `Lifetime`  should be something like
```rust
struct Lifetime {
    hir_id: HirId,

    source: LifetimeSource,

    res: LifetimeName,
}

enum LifetimeSource {
   // Used for everything where we have an explicit lifetime, including `'_` 
   Explicit { name: Ident },
   // Used for fully elided lifetimes, i.e. all cases of `IsAnonInPath::Yes`
   // as well as `&Type` maybe. Idk whether these should be treated differently.
   FullyElided { insert_span: Span },
}
```
Do you think this approach would make sense? I think merging this PR as is is already an improvement as it makes the weirdness more explicit.
created_at: 2025-03-27T11:24:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -567,10 +567,14 @@ impl Subdiagnostic for AddLifetimeParamsSuggestion<'_> {
 
             impl<'v> Visitor<'v> for ImplicitLifetimeFinder {
                 fn visit_ty(&mut self, ty: &'v hir::Ty<'v, AmbigArg>) {
-                    let make_suggestion = |ident: Ident| {
-                        if ident.name == kw::Empty && ident.span.is_empty() {
+                    let make_suggestion = |lifetime: &hir::Lifetime| {
+                        if lifetime.is_anon_in_path == IsAnonInPath::Yes
+                            && lifetime.ident.span.is_empty()
path: compiler/rustc_trait_selection/src/errors.rs
user: login: lcnr
body: when is this part of the condition ever false. I would expect all `AnonInPath`  to have empty spans
created_at: 2025-03-27T11:26:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -567,10 +567,14 @@ impl Subdiagnostic for AddLifetimeParamsSuggestion<'_> {
 
             impl<'v> Visitor<'v> for ImplicitLifetimeFinder {
                 fn visit_ty(&mut self, ty: &'v hir::Ty<'v, AmbigArg>) {
-                    let make_suggestion = |ident: Ident| {
-                        if ident.name == kw::Empty && ident.span.is_empty() {
+                    let make_suggestion = |lifetime: &hir::Lifetime| {
+                        if lifetime.is_anon_in_path == IsAnonInPath::Yes
+                            && lifetime.ident.span.is_empty()
+                        {
                             format!("{}, ", self.suggestion_param_name)
-                        } else if ident.name == kw::UnderscoreLifetime && ident.span.is_empty() {
+                        } else if lifetime.ident.name == kw::UnderscoreLifetime
+                            && lifetime.ident.span.is_empty()
path: compiler/rustc_trait_selection/src/errors.rs
user: login: lcnr
body: this is where changing `ident` to a source enum would be helpful
created_at: 2025-03-27T11:26:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -567,10 +567,14 @@ impl Subdiagnostic for AddLifetimeParamsSuggestion<'_> {
 
             impl<'v> Visitor<'v> for ImplicitLifetimeFinder {
                 fn visit_ty(&mut self, ty: &'v hir::Ty<'v, AmbigArg>) {
-                    let make_suggestion = |ident: Ident| {
-                        if ident.name == kw::Empty && ident.span.is_empty() {
+                    let make_suggestion = |lifetime: &hir::Lifetime| {
+                        if lifetime.is_anon_in_path == IsAnonInPath::Yes
+                            && lifetime.ident.span.is_empty()
path: compiler/rustc_trait_selection/src/errors.rs
user: login: nnethercote
body: `IsAnonInPath::Yes` lifetimes are inserted in `maybe_insert_elided_lifetimes_in_path`. In that function `elided_lifetime_span` can definitely be non-empty.

However, none of those non-empty spans reach here, at least when running the UI tests. (I checked by adding an assertion.) I don't know if that's guaranteed, or if we just have insufficient test coverage. So I'd prefer to keep the test here for now; it is retained from the original code.
created_at: 2025-03-27T23:12:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,20 +35,60 @@ use crate::def_id::{DefId, LocalDefIdMap};
 pub(crate) use crate::hir_id::{HirId, ItemLocalId, ItemLocalMap, OwnerId};
 use crate::intravisit::{FnKind, VisitorExt};
 
+#[derive(Debug, Copy, Clone, PartialEq, Eq, HashStable_Generic)]
+pub enum IsAnonInPath {
+    No,
+    Yes,
+}
+
+/// A lifetime. The valid field combinations are non-obvious. The following
+/// example shows some of them. See also the comments on `LifetimeName`.
+/// ```
+/// #[repr(C)]
+/// struct S<'a>(&'a u32);       // LifetimeName::Param, name='a, IsAnonInPath::No
+/// unsafe extern "C" {
+///     fn f1(s: S);             // LifetimeName::Param, name='_, IsAnonInPath::Yes
+///     fn f2(s: S<'_>);         // LifetimeName::Param, name='_, IsAnonInPath::No
+///     fn f3<'a>(s: S<'a>);     // LifetimeName::Param, name='a, IsAnonInPath::No
+/// }
+///
+/// struct St<'a> { x: &'a u32 } // LifetimeName::Param, name='a, IsAnonInPath::No
+/// fn f() {
+///     _ = St { x: &0 };        // LifetimeName::Infer, name='_, IsAnonInPath::Yes
+///     _ = St::<'_> { x: &0 };  // LifetimeName::Infer, name='_, IsAnonInPath::No
+/// }
+///
+/// struct Name<'a>(&'a str);    // LifetimeName::Param,  name='a, IsAnonInPath::No
+/// const A: Name = Name("a");   // LifetimeName::Static, name='_, IsAnonInPath::Yes
+/// const B: &str = "";          // LifetimeName::Static, name='_, IsAnonInPath::No
path: compiler/rustc_hir/src/hir.rs
user: login: nnethercote
body: ```rust
   // Used for fully elided lifetimes, i.e. all cases of `IsAnonInPath::Yes`
   // as well as `&Type` maybe. Idk whether these should be treated differently.
   FullyElided { insert_span: Span },
```
The "as well as `&Type` maybe" isn't possible. The `IsAnonInPath` has to be distinct otherwise `LifetimeSuggestion` won't work. You could do this:
```rust
pub enum LifetimeIdent {
    Normal { ident: Ident },
    IsAnonInPath { span: Span },
}
```
It would avoid the invalid combination of `IsAnonInPath::Yes` with a non-underscore lifetime ident, but that's the only benefit, and it adds a bunch of `match`es, etc. I don't think it's worth it.

There might still be a nicer formulation of this code, but I think this PR is worth landing in its current state.
created_at: 2025-03-27T23:24:38Z
author_association: CONTRIBUTOR

PR comments:
user: login: nnethercote
created_at: 2025-03-26T06:05:37Z
body: Best reviewed one commit at a time.
author_association: CONTRIBUTOR
user: login: nnethercote
created_at: 2025-03-27T05:04:06Z
body: I updated, making these changes:
- Renamed `IsPathAnon` to `IsAnonInPath`.
- Added a big comment on `Lifetime`.
- Added an assertion to `new_named_lifetime`: if `IsAnonInPath::Yes`, the lifetime name must be `'_`.
- Expanded pretty-printing test coverage. The main commit now makes some slight improvements to pretty-printing.
author_association: CONTRIBUTOR
user: login: rust-log-analyzer
created_at: 2025-03-27T05:47:52Z
body: 
The job **`x86_64-gnu-llvm-18`** failed! Check out the build log: [(web)](https://github.com/rust-lang/rust/actions/runs/14099247460/job/39492331784) [(plain)](https://github.com/rust-lang/rust/commit/4f1b408d87b8c1c3d92d002268a467454994e42f/checks/39492331784/logs)

<details><summary><i>Click to see the possible cause of the failure (guessed by this bot)</i></summary>

```plain
#19 exporting to docker image format
#19 sending tarball 19.6s done
#19 DONE 33.2s
##[endgroup]
Setting extra environment values for docker:  --env ENABLE_GCC_CODEGEN=1 --env GCC_EXEC_PREFIX=/usr/lib/gcc/
[CI_JOB_NAME=x86_64-gnu-llvm-18]
[CI_JOB_NAME=x86_64-gnu-llvm-18]
debug: `DISABLE_CI_RUSTC_IF_INCOMPATIBLE` configured.
---
sccache: Listening on address 127.0.0.1:4226
##[group]Configure the build
configure: processing command line
configure: 
configure: build.configure-args := ['--build=x86_64-unknown-linux-gnu', '--llvm-root=/usr/lib/llvm-18', '--enable-llvm-link-shared', '--set', 'rust.randomize-layout=true', '--set', 'rust.thin-lto-import-instr-limit=10', '--set', 'build.print-step-timings', '--enable-verbose-tests', '--set', 'build.metrics', '--enable-verbose-configure', '--enable-sccache', '--disable-manage-submodules', '--enable-locked-deps', '--enable-cargo-native-static', '--set', 'rust.codegen-units-std=1', '--set', 'dist.compression-profile=balanced', '--dist-compression-formats=xz', '--set', 'rust.lld=false', '--disable-dist-src', '--release-channel=nightly', '--enable-debug-assertions', '--enable-overflow-checks', '--enable-llvm-assertions', '--set', 'rust.verify-llvm-ir', '--set', 'rust.codegen-backends=llvm,cranelift,gcc', '--set', 'llvm.static-libstdcpp', '--enable-new-symbol-mangling']
configure: build.build          := x86_64-unknown-linux-gnu
configure: target.x86_64-unknown-linux-gnu.llvm-config := /usr/lib/llvm-18/bin/llvm-config
configure: llvm.link-shared     := True
configure: rust.randomize-layout := True
configure: rust.thin-lto-import-instr-limit := 10
---
  Number of decisions:   4447
  longest path:          1159 (code:    152)
  longest backtrack:       66 (code:    428)
Shared 86733 out of 152951 states by creating 14756 new states, saving 71977
/checkout/obj/build/x86_64-unknown-linux-gnu/gcc/src/gcc/expmed.cc: In function ‘rtx_def* extract_bit_field_1(rtx, poly_uint64, poly_uint64, int, rtx, machine_mode, machine_mode, bool, bool, rtx_def**)’:
/checkout/obj/build/x86_64-unknown-linux-gnu/gcc/src/gcc/expmed.cc:1864:45: warning: ‘*(unsigned int*)((char*)&imode + offsetof(scalar_int_mode, scalar_int_mode::m_mode))’ may be used uninitialized [-Wmaybe-uninitialized]
 1864 |       rtx sub = extract_bit_field_as_subreg (mode1, op0, imode,
      |                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~
 1865 |                                              bitsize, bitnum);
      |                                              ~~~~~~~~~~~~~~~~
/checkout/obj/build/x86_64-unknown-linux-gnu/gcc/src/gcc/expmed.cc:1824:19: note: ‘*(unsigned int*)((char*)&imode + offsetof(scalar_int_mode, scalar_int_mode::m_mode))’ was declared here
 1824 |   scalar_int_mode imode;
      |                   ^~~~~
/checkout/obj/build/x86_64-unknown-linux-gnu/gcc/src/gcc/gimple-range-gori.cc: In member function ‘void range_def_chain::dump(FILE*, basic_block, const char*)’:
/checkout/obj/build/x86_64-unknown-linux-gnu/gcc/src/gcc/gimple-range-gori.cc:319:19: warning: format not a string literal and no format arguments [-Wformat-security]
  319 |           fprintf (f, prefix);
      |           ~~~~~~~~^~~~~~~~~~~
---
/checkout/obj/build/x86_64-unknown-linux-gnu/gcc/src/gcc/gcc.cc:7930:9: warning: ignoring return value of ‘ssize_t write(int, const void*, size_t)’ declared with attribute ‘warn_unused_result’ [-Wunused-result]
 7930 |   write (fd, "\n\n", 2);
      |   ~~~~~~^~~~~~~~~~~~~~~
/checkout/obj/build/x86_64-unknown-linux-gnu/gcc/src/gcc/gcc.cc: In member function ‘void driver::final_actions() const’:
/checkout/obj/build/x86_64-unknown-linux-gnu/gcc/src/gcc/gcc.cc:9307:13: warning: ignoring return value of ‘int truncate(const char*, __off_t)’ declared with attribute ‘warn_unused_result’ [-Wunused-result]
 9307 |     truncate(totruncate_file, 0);
      |     ~~~~~~~~^~~~~~~~~~~~~~~~~~~~
/checkout/obj/build/x86_64-unknown-linux-gnu/gcc/src/gcc/lto-wrapper.cc: In function ‘bool find_and_merge_options(int, off_t, const char*, vec<cl_decoded_option>, bool, vec<cl_decoded_option>*, const char*)’:
/checkout/obj/build/x86_64-unknown-linux-gnu/gcc/src/gcc/lto-wrapper.cc:1165:8: warning: ignoring return value of ‘ssize_t read(int, void*, size_t)’ declared with attribute ‘warn_unused_result’ [-Wunused-result]
 1165 |   read (fd, data, length);
      |   ~~~~~^~~~~~~~~~~~~~~~~~
/checkout/obj/build/x86_64-unknown-linux-gnu/gcc/src/gcc/lto/lto-common.cc: In function ‘void lto_resolution_read(splay_tree, FILE*, lto_file*)’:
/checkout/obj/build/x86_64-unknown-linux-gnu/gcc/src/gcc/lto/lto-common.cc:2091:10: warning: ignoring return value of ‘int fscanf(FILE*, const char*, ...)’ declared with attribute ‘warn_unused_result’ [-Wunused-result]
 2091 |   fscanf (resolution, " ");   /* Read white space.  */
      |   ~~~~~~~^~~~~~~~~~~~~~~~~
/checkout/obj/build/x86_64-unknown-linux-gnu/gcc/src/gcc/lto/lto-common.cc:2093:9: warning: ignoring return value of ‘size_t fread(void*, size_t, size_t, FILE*)’ declared with attribute ‘warn_unused_result’ [-Wunused-result]
 2093 |   fread (obj_name, sizeof (char), name_len, resolution);
      |   ~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/checkout/obj/build/x86_64-unknown-linux-gnu/gcc/src/gcc/lto/lto-common.cc:2113:10: warning: ignoring return value of ‘int fscanf(FILE*, const char*, ...)’ declared with attribute ‘warn_unused_result’ [-Wunused-result]
 2113 |   fscanf (resolution, "%u", &num_symbols);
      |   ~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from /checkout/obj/build/x86_64-unknown-linux-gnu/gcc/src/gcc/jit/jit-recording.cc:32:
---
Applying io_quotes_use            to linux/blkzoned.h
Applying io_quotes_use            to linux/ipmi.h
Applying io_quotes_use            to linux/psp-dbc.h
Applying io_quotes_use            to linux/bt-bmc.h
Applying io_quotes_use            to linux/tps6594_pfsm.h
Applying io_quotes_use            to linux/cxl_mem.h
Applying io_quotes_use            to linux/wmi.h
Applying io_quotes_use            to linux/auto_fs.h
Applying io_quotes_use            to linux/mmtimer.h
Applying io_quotes_use            to linux/f2fs.h
Applying io_quotes_use            to linux/vhost.h
---
Applying machine_name             to x86_64-linux-gnu/bits/unistd_ext.h
Applying io_quotes_use            to x86_64-linux-gnu/asm/mtrr.h
Applying io_quotes_use            to x86_64-linux-gnu/asm/amd_hsmp.h
Applying machine_name             to openssl/e_os2.h
Applying io_quotes_use            to drm/xe_drm.h
Applying io_quotes_use            to drm/radeon_drm.h
Applying io_quotes_use            to drm/panfrost_drm.h
Applying io_quotes_use            to drm/etnaviv_drm.h
Applying io_quotes_use            to drm/lima_drm.h
Applying io_quotes_use            to drm/qaic_accel.h
Applying io_quotes_use            to drm/vc4_drm.h
Applying io_quotes_use            to drm/i915_drm.h
Applying io_quotes_use            to drm/omap_drm.h
Applying io_quotes_use            to drm/pvr_drm.h
Applying io_quotes_use            to drm/amdgpu_drm.h
Applying io_quotes_use            to drm/vgem_drm.h
Applying io_quotes_use            to drm/msm_drm.h
Applying io_quotes_use            to drm/v3d_drm.h
Applying io_quotes_use            to drm/exynos_drm.h
Applying io_quotes_use            to drm/nouveau_drm.h
Applying io_quotes_use            to drm/drm.h
Applying io_quotes_use            to drm/habanalabs_accel.h
Applying io_quotes_use            to drm/tegra_drm.h
Applying io_quotes_use            to rdma/rdma_user_ioctl.h
cc1: note: self-tests are not enabled in this build
/checkout/obj/build/x86_64-unknown-linux-gnu/gcc/src/c++tools/server.cc: In function ‘void server(bool, int, module_resolver*)’:
/checkout/obj/build/x86_64-unknown-linux-gnu/gcc/src/c++tools/server.cc:620:10: warning: ignoring return value of ‘int pipe(int*)’ declared with attribute ‘warn_unused_result’ [-Wunused-result]
---
[RUSTC-TIMING] proc_macro test:false 5.227
[RUSTC-TIMING] test test:false 9.276
    Finished `release` profile [optimized] target(s) in 1m 21s
##[endgroup]
[TIMING] core::build_steps::compile::Std { target: x86_64-unknown-linux-gnu, compiler: Compiler { stage: 1, host: x86_64-unknown-linux-gnu, forced_compiler: true }, crates: [], force_recompile: false, extra_rust_args: ["-Csymbol-mangling-version=v0", "-Cpanic=abort"], is_for_mir_opt_tests: false } -- 81.575
Testing GCC stage1 (x86_64-unknown-linux-gnu -> x86_64-unknown-linux-gnu)
 Downloading crates ...
  Downloaded boml v0.3.1
   Compiling boml v0.3.1
[RUSTC-TIMING] boml test:false 0.828
   Compiling y v0.1.0 (/checkout/compiler/rustc_codegen_gcc/build_system)
[RUSTC-TIMING] y test:false 3.017
    Finished `release` profile [optimized] target(s) in 4.14s
     Running `/checkout/obj/build/x86_64-unknown-linux-gnu/stage1-codegen/x86_64-unknown-linux-gnu/release/y test --use-backend gcc --gcc-path /checkout/obj/build/x86_64-unknown-linux-gnu/gcc/install/lib --out-dir /checkout/obj/build/x86_64-unknown-linux-gnu/stage1-tools/cg_gcc --release --mini-tests --std-tests`
`--gcc-path` was provided, ignoring config file. Using `/checkout/obj/build/x86_64-unknown-linux-gnu/gcc/install/lib` as path for libgccjit
[BUILD] mini_core
[RUSTC-TIMING] mini_core test:false 0.198
[BUILD] example
[AOT] mini_core_hello_world
[RUSTC-TIMING] mini_core_hello_world test:false 0.187
---
---- compiler/rustc_hir/src/hir.rs - hir::Lifetime (line 46) stdout ----
error: invalid label name `'_`
  --> compiler/rustc_hir/src/hir.rs:69:59
   |
25 | ...                   name='_, IsAnonInPath::No
   |                            ^^

error: expected `while`, `for`, `loop` or `{` after a label
  --> compiler/rustc_hir/src/hir.rs:69:61
   |
25 | ...                   name='_, IsAnonInPath::No
   |                              ^ expected `while`, `for`, `loop` or `{` after a label
   |
help: add `'` to close the char literal
   |
25 |                                                      name='_', IsAnonInPath::No
   |                                                             +

error: expected one of `.`, `:`, `;`, `?`, `for`, `loop`, `while`, `}`, or an operator, found `,`
  --> compiler/rustc_hir/src/hir.rs:69:61
   |
25 | ...                   name='_, IsAnonInPath::No
   |                              ^ expected one of 9 possible tokens

error: extern blocks must be unsafe
 --> compiler/rustc_hir/src/hir.rs:49:1
  |
5 | / extern "C" {
6 | |     fn f1(s: S);             // LifetimeName::Param, name='_, IsAnonInPath::Yes
7 | |     fn f2(s: S<'_>);         // LifetimeName::Param, name='_, IsAnonInPath::No
8 | |     fn f3<'a>(s: S<'a>);     // LifetimeName::Param, name='a, IsAnonInPath::No
9 | | }
  | |_^

warning: hidden lifetime parameters in types are deprecated
 --> compiler/rustc_hir/src/hir.rs:50:14
  |
6 |     fn f1(s: S);             // LifetimeName::Param, name='_, IsAnonInPath::Yes
  |              ^ expected lifetime parameter
  |
note: the lint level is defined here
 --> <crate attribute>:1:6
  |
1 | warn(rust_2018_idioms)
  |      ^^^^^^^^^^^^^^^^
  = note: `#[warn(elided_lifetimes_in_paths)]` implied by `#[warn(rust_2018_idioms)]`
help: indicate the anonymous lifetime
  |
6 |     fn f1(s: S<'_>);             // LifetimeName::Param, name='_, IsAnonInPath::Yes
  |               ++++

warning: hidden lifetime parameters in types are deprecated
  --> compiler/rustc_hir/src/hir.rs:62:10
   |
18 | const A: Name = Name("a");   // LifetimeName::Static, name='_, IsAnonInPath::Yes
   |          ^^^^ expected lifetime parameter
   |
help: indicate the anonymous lifetime
   |
18 | const A: Name<'_> = Name("a");   // LifetimeName::Static, name='_, IsAnonInPath::Yes
   |              ++++

error: aborting due to 4 previous errors; 2 warnings emitted

Couldn't compile the test.

failures:
    compiler/rustc_hir/src/hir.rs - hir::Lifetime (line 46)

test result: FAILED. 5 passed; 1 failed; 6 ignored; 0 measured; 0 filtered out; finished in 155.19ms

error: doctest failed, to rerun pass `-p rustc_hir --doc`
Build completed unsuccessfully in 0:39:04
  local time: Thu Mar 27 05:47:35 UTC 2025
  network time: Thu, 27 Mar 2025 05:47:35 GMT
##[error]Process completed with exit code 1.
Post job cleanup.
```

</details>

author_association: COLLABORATOR
user: login: nnethercote
created_at: 2025-03-28T01:50:21Z
body: I didn't change the `is_empty` check for the reason explained above.

@bors r=lcnr
author_association: CONTRIBUTOR
user: login: bors
created_at: 2025-03-28T01:50:24Z
body: :pushpin: Commit 8d2c63f514a74752a3186b0bf3e8f3aef57dc12e has been approved by `lcnr`

It is now in the [queue](https://bors.rust-lang.org/homu/queue/rust) for this repository.

<!-- @bors r=lcnr 8d2c63f514a74752a3186b0bf3e8f3aef57dc12e -->
<!-- homu: {"type":"Approved","sha":"8d2c63f514a74752a3186b0bf3e8f3aef57dc12e","approver":"lcnr","queue":"https://bors.rust-lang.org/homu/queue/rust"} -->
author_association: COLLABORATOR
user: login: bors
created_at: 2025-03-28T03:20:09Z
body: :hourglass: Testing commit 8d2c63f514a74752a3186b0bf3e8f3aef57dc12e with merge 3f690c2257b7080cd3a8cce64e082fc972148990...
<!-- homu: {"type":"BuildStarted","head_sha":"8d2c63f514a74752a3186b0bf3e8f3aef57dc12e","merge_sha":"3f690c2257b7080cd3a8cce64e082fc972148990"} -->
author_association: COLLABORATOR
user: login: bors
created_at: 2025-03-28T06:27:42Z
body: :sunny: Test successful - [checks-actions](https://github.com/rust-lang-ci/rust/actions/runs/14121261862/job/39567212128)
Approved by: lcnr
Pushing 3f690c2257b7080cd3a8cce64e082fc972148990 to master...
<!-- homu: {"type":"BuildCompleted","approved_by":"lcnr","base_ref":"master","builders":{"checks-actions":"https://github.com/rust-lang-ci/rust/actions/runs/14121261862/job/39567212128"},"merge_sha":"3f690c2257b7080cd3a8cce64e082fc972148990"} -->
author_association: COLLABORATOR
user: login: github-actions[bot]
created_at: 2025-03-28T06:29:46Z
body: <details>
<summary>What is this?</summary>
This is an experimental post-merge analysis report that shows differences in test outcomes between the merged PR and its parent PR.
</details>


Comparing 7586a9f99ad75287f9eef56c32c8508345ce0a73 (parent) -> 3f690c2257b7080cd3a8cce64e082fc972148990 (this PR)

# Test differences
<details>
<summary>Show 17 test diffs
</summary>

## Stage 1
- `[pretty] tests/pretty/hir-lifetimes.rs`: [missing] -> pass (**J1**)
## Stage 2
- `[pretty] tests/pretty/hir-lifetimes.rs`: [missing] -> pass (**J0**)

Additionally, 15 doctest diffs were found. These are ignored, as they are noisy.

**Job group index**

- **J0**: `aarch64-apple`, `aarch64-gnu`, `i686-gnu-1`, `i686-gnu-nopt-1`, `i686-msvc-1`, `x86_64-apple-1`, `x86_64-gnu`, `x86_64-gnu-llvm-18-1`, `x86_64-gnu-llvm-18-2`, `x86_64-gnu-llvm-19-1`, `x86_64-gnu-llvm-19-2`, `x86_64-gnu-nopt`, `x86_64-gnu-stable`, `x86_64-mingw-1`, `x86_64-msvc-1`
- **J1**: `x86_64-gnu-llvm-18-3`, `x86_64-gnu-llvm-19-3`
</details>

# Job duration changes
1. `aarch64-gnu`: 6389.9s -> 9187.4s (43.8%)
2. `armhf-gnu`: 4230.0s -> 4565.5s (7.9%)
3. `aarch64-gnu-debug`: 6072.9s -> 6518.9s (7.3%)
4. `dist-x86_64-apple`: 9342.2s -> 10014.0s (7.2%)
5. `x86_64-apple-1`: 7733.7s -> 8121.6s (5.0%)
6. `dist-i686-mingw`: 7956.4s -> 8308.4s (4.4%)
7. `i686-mingw-1`: 7321.4s -> 7573.8s (3.4%)
8. `dist-s390x-linux`: 5347.5s -> 5521.1s (3.2%)
9. `aarch64-apple`: 3921.1s -> 4028.3s (2.7%)
10. `dist-i586-gnu-i586-i686-musl`: 5222.4s -> 5350.5s (2.5%)

<details>
<summary>How to interpret the job duration changes?</summary>

Job durations can vary a lot, based on the actual runner instance
that executed the job, system noise, invalidated caches, etc. The table above is provided
mostly for t-infra members, for simpler debugging of potential CI slow-downs.
</details>


author_association: CONTRIBUTOR
user: login: rust-timer
created_at: 2025-03-28T08:11:12Z
body: Finished benchmarking commit (3f690c2257b7080cd3a8cce64e082fc972148990): [comparison URL](https://perf.rust-lang.org/compare.html?start=7586a9f99ad75287f9eef56c32c8508345ce0a73&end=3f690c2257b7080cd3a8cce64e082fc972148990&stat=instructions:u).

### Overall result: no relevant changes - no action needed


@rustbot label: -perf-regression



### [Instruction count](https://perf.rust-lang.org/compare.html?start=7586a9f99ad75287f9eef56c32c8508345ce0a73&end=3f690c2257b7080cd3a8cce64e082fc972148990&stat=instructions:u)
This benchmark run did not return any relevant results for this metric.

### [Max RSS (memory usage)](https://perf.rust-lang.org/compare.html?start=7586a9f99ad75287f9eef56c32c8508345ce0a73&end=3f690c2257b7080cd3a8cce64e082fc972148990&stat=max-rss)
<details>
<summary>Results (secondary -2.1%)</summary>

This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.

|                                    | mean  | range          | count |
|:----------------------------------:|:-----:|:--------------:|:-----:|
| Regressions ❌ <br /> (primary)    | -     | -              | 0     |
| Regressions ❌ <br /> (secondary)  | -     | -              | 0     |
| Improvements ✅ <br /> (primary)   | -     | -              | 0     |
| Improvements ✅ <br /> (secondary) | -2.1% | [-2.1%, -2.1%] | 2     |
| All ❌✅ (primary)                 | -     | -              | 0     |
</details>

### [Cycles](https://perf.rust-lang.org/compare.html?start=7586a9f99ad75287f9eef56c32c8508345ce0a73&end=3f690c2257b7080cd3a8cce64e082fc972148990&stat=cycles:u)
<details>
<summary>Results (secondary -0.3%)</summary>

This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.

|                                    | mean  | range          | count |
|:----------------------------------:|:-----:|:--------------:|:-----:|
| Regressions ❌ <br /> (primary)    | -     | -              | 0     |
| Regressions ❌ <br /> (secondary)  | 2.3%  | [2.3%, 2.3%]   | 1     |
| Improvements ✅ <br /> (primary)   | -     | -              | 0     |
| Improvements ✅ <br /> (secondary) | -2.8% | [-2.8%, -2.8%] | 1     |
| All ❌✅ (primary)                 | -     | -              | 0     |
</details>

### [Binary size](https://perf.rust-lang.org/compare.html?start=7586a9f99ad75287f9eef56c32c8508345ce0a73&end=3f690c2257b7080cd3a8cce64e082fc972148990&stat=size:linked_artifact)
This benchmark run did not return any relevant results for this metric.

**Bootstrap**: 778.708s -> 777.401s (-0.17%)
**Artifact size**: 365.91 MiB -> 365.92 MiB (0.00%)
author_association: COLLABORATOR
user: login: shepmaster
created_at: 2025-03-28T10:52:45Z
body: Kindly have a look at https://github.com/rust-lang/rust/pull/138677 and let me know if you think I’ll have to undo (some of?)  these changes. 
author_association: MEMBER
user: login: nnethercote
created_at: 2025-03-28T22:58:59Z
body: @shepmaster: Oof, that's unfortunate timing, me removing the previously-unused distinction between `kw::Empty` and `kw::UnderscoreLifetime` just as you're adding code that depends on it. It looks like you've been working on the lint for a while, and I don't want my cleanup to block it, so we can revert it if necessary. A few questions before doing that.

- I'm confused by the diffs in #138677. In particular, `Lifetime::suggestion` looks very different to what I expect. Also `Lifetime::suggestion_position` and `LifetimeSuggestionPosition` are present, but neither of them exist on trunk. Where do they come from?

- Is there a reason the lint is done on HIR and not the AST? I would have thought the AST would be more appropriate, being closer to the original source code. Indeed, that's the reason I figured my cleanup would be fine, because HIR is further away from source and the `&u8` vs `&'_ u8` distinction didn't matter.

- I have a follow-up in #139046, currently in draft form, though it's pretty close to what would have been the final version and the commit messages explain what's going on. It eliminates the `make_suggestion` closure in much the same way your code does.
author_association: CONTRIBUTOR
user: login: shepmaster
created_at: 2025-03-29T00:25:40Z
body: > we can revert it if necessary

I don't believe that will be required. I've rebased on top of your changes [and pushed on through](https://github.com/rust-lang/rust/compare/master...shepmaster:rust:rework). That is my entire set of change, which will be spread out over at least three PRs (two of which are open now) — only the first 4 commits will exist in PR #138677. Specifically related to these changes, I've [continued modifying `Lifetime`](https://github.com/rust-lang/rust/commit/9b312a21b78123dd9cb0e503611bfbb97f873bfc) by transforming your new field into two. One of the new fields is (I think!) close to the [suggestion to have a source enum](https://github.com/rust-lang/rust/pull/138965#discussion_r2016307719).

> I'm confused by the diffs in [Add a new `mismatched-lifetime-syntaxes` lint #138677](https://github.com/rust-lang/rust/pull/138677). In particular, `Lifetime::suggestion` looks very different to what I expect. 

I'm not totally sure what you'd expect, but hopefully the reworked version of that function reads more cleanly?

> `Lifetime::suggestion_position` and `LifetimeSuggestionPosition` are present, but neither of them exist on trunk. Where do they come from?

They were [deleted in this very PR](https://github.com/rust-lang/rust/pull/138965/files#diff-67c494d6d292cbe0a96d420fa02cf4589c7fad5a8d9abbdfa317d49a8bdce6daL142-L152) 😉 

> Is there a reason the lint is done on HIR and not the AST? 

When I asked for help on Zulip, it [was suggested that a HIR lint would be nicer](https://rust-lang.zulipchat.com/#narrow/channel/182449-t-compiler.2Fhelp/topic/.E2.9C.94.20Adding.20new.20visitors.20vs.20piggybacking.20on.20existing/near/503645113). Based on that feedback, I scrapped my original lint changes which lived in rustc_resolve and rewrote them (extending them based on lang-team feedback) as acting on HIR.

> I have a follow-up in [Improve `Lifetime::suggestion` #139046](https://github.com/rust-lang/rust/pull/139046), currently in draft form, though it's pretty close to what would have been the final version and the commit messages explain what's going on. 

I think there's some strong similarity to my changes there. Notably, I see you are tracking if there are angle brackets, which is also something I needed to add.

> It eliminates the `make_suggestion` closure in much the same way your code does.

My [reworked code](https://github.com/rust-lang/rust/commit/9b312a21b78123dd9cb0e503611bfbb97f873bfc) continues to eliminate the the closure as well. I'm quite happy that duplicated code is being removed.
author_association: MEMBER
user: login: nnethercote
created_at: 2025-03-29T01:47:15Z
body: > I've rebased on top of your changes and pushed on through

Oh, great news. I will stop working on #139046 until you are finished with the lint, though feel free to steal any ideas from it that work for you :)
 
> > `Lifetime::suggestion_position` and `LifetimeSuggestionPosition` are present, but neither of them exist on trunk. Where do they come from?
> 
> They were [deleted in this very PR](https://github.com/rust-lang/rust/pull/138965/files#diff-67c494d6d292cbe0a96d420fa02cf4589c7fad5a8d9abbdfa317d49a8bdce6daL142-L152) 😉

lol, I totally forgot I had done that. Sorry for the confusion.
author_association: CONTRIBUTOR