Skip to content

Remove kw::Empty uses from hir::Lifetime::ident #138965

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 28, 2025

Conversation

nnethercote
Copy link
Contributor

@nnethercote nnethercote commented Mar 26, 2025

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

r? @lcnr

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Mar 26, 2025
@nnethercote
Copy link
Contributor Author

Best reviewed one commit at a time.

hir::LifetimeName::Infer
}
LifetimeRes::Static { .. } => {
debug_assert!(matches!(ident.name, kw::StaticLifetime | kw::UnderscoreLifetime));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when does this happen to be an underscore lifetime?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 2412 to 2413
hir::LifetimeName::ImplicitObjectLifetimeDefault,
IsPathAnon::No,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be easier to have a LifetimeName::Anon|ElidedInPath? and not have two enums inside of hir::Lifetime?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

#![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:

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?

@nnethercote nnethercote force-pushed the less-kw-Empty-hir-Lifetime branch 2 times, most recently from 99843d6 to 4f1b408 Compare March 27, 2025 05:03
@nnethercote
Copy link
Contributor Author

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.

@rust-log-analyzer

This comment has been minimized.

@nnethercote nnethercote force-pushed the less-kw-Empty-hir-Lifetime branch from 4f1b408 to 26bc215 Compare March 27, 2025 08:00
Comment on lines 62 to 63
/// const A: Name = Name("a"); // LifetimeName::Static, name='_, IsAnonInPath::Yes
/// const B: &str = ""; // LifetimeName::Static, name='_, IsAnonInPath::No
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

   // 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:

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 matches, 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.

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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when is this part of the condition ever false. I would expect all AnonInPath to have empty spans

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +575 to +576
} else if lifetime.ident.name == kw::UnderscoreLifetime
&& lifetime.ident.span.is_empty()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is where changing ident to a source enum would be helpful

Copy link
Contributor

@lcnr lcnr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a suggestion and one final nit

r=me after fixing the nit if u don't apply the suggestion

HIR printing currently gets very little testing. This increases coverage
a bit, with a focus on lifetimes.

There are some FIXME comments for cases that are printed in a dubious
fashion. This PR won't address those; the point of adding this test is
to ensure that the subsequent commits don't hurt pretty-printing.
They both are only used in `Lifetime::suggestion`. This commit inlines
and removes them.
It has no effect on anything in the test suite.

This means it can also be rewritten as a neater pairwise `match`.
`hir::Lifetime::ident` currently sometimes uses `kw::Empty` for elided
lifetimes and sometimes uses `kw::UnderscoreLifetime`, and the
distinction is used when creating some error suggestions, e.g. in
`Lifetime::suggestion` and `ImplicitLifetimeFinder::visit_ty`. I found
this *really* confusing, and it took me a while to understand what was
going on.

This commit replaces all uses of `kw::Empty` in `hir::Lifetime::ident`
with `kw::UnderscoreLifetime`. It adds a new field
`hir::Lifetime::is_path_anon` that mostly replaces the old
empty/underscore distinction and makes things much clearer.

Some other notable changes:

- Adds a big comment to `Lifetime` talking about permissable field
  values.

- Adds some assertions in `new_named_lifetime` about what ident values
  are permissible for the different `LifetimeRes` values.

- Adds a `Lifetime::new` constructor that does some checking to make
  sure the `is_elided` and `is_anonymous` states are valid.

- `add_static_impl_trait_suggestion` now looks at `Lifetime::res`
  instead of the ident when creating the suggestion. This is the one
  case where `is_path_anon` doesn't replace the old empty/underscore
  distinction.

- A couple of minor pretty-printing improvements.
@nnethercote nnethercote force-pushed the less-kw-Empty-hir-Lifetime branch from 26bc215 to 8d2c63f Compare March 27, 2025 23:25
@nnethercote
Copy link
Contributor Author

I didn't change the is_empty check for the reason explained above.

@bors r=lcnr

@bors
Copy link
Collaborator

bors commented Mar 28, 2025

📌 Commit 8d2c63f has been approved by lcnr

It is now in the queue for this repository.

@bors bors added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Mar 28, 2025
@bors
Copy link
Collaborator

bors commented Mar 28, 2025

⌛ Testing commit 8d2c63f with merge 3f690c2...

@bors
Copy link
Collaborator

bors commented Mar 28, 2025

☀️ Test successful - checks-actions
Approved by: lcnr
Pushing 3f690c2 to master...

@bors bors added the merged-by-bors This PR was explicitly merged by bors. label Mar 28, 2025
@bors bors merged commit 3f690c2 into rust-lang:master Mar 28, 2025
7 checks passed
@rustbot rustbot added this to the 1.87.0 milestone Mar 28, 2025
Copy link

What is this? This is an experimental post-merge analysis report that shows differences in test outcomes between the merged PR and its parent PR.

Comparing 7586a9f (parent) -> 3f690c2 (this PR)

Test differences

Show 17 test diffs

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

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%)
How to interpret the job duration changes?

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.

@rust-timer
Copy link
Collaborator

Finished benchmarking commit (3f690c2): comparison URL.

Overall result: no relevant changes - no action needed

@rustbot label: -perf-regression

Instruction count

This benchmark run did not return any relevant results for this metric.

Max RSS (memory usage)

Results (secondary -2.1%)

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 ❌
(primary)
- - 0
Regressions ❌
(secondary)
- - 0
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
-2.1% [-2.1%, -2.1%] 2
All ❌✅ (primary) - - 0

Cycles

Results (secondary -0.3%)

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 ❌
(primary)
- - 0
Regressions ❌
(secondary)
2.3% [2.3%, 2.3%] 1
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
-2.8% [-2.8%, -2.8%] 1
All ❌✅ (primary) - - 0

Binary size

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%)

@shepmaster
Copy link
Member

shepmaster commented Mar 28, 2025

Kindly have a look at #138677 and let me know if you think I’ll have to undo (some of?) these changes.

@nnethercote
Copy link
Contributor Author

@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 Add a new mismatched-lifetime-syntaxes lint #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 Improve Lifetime::suggestion #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.

@shepmaster
Copy link
Member

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. 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 by transforming your new field into two. One of the new fields is (I think!) close to the suggestion to have a source enum.

I'm confused by the diffs in Add a new mismatched-lifetime-syntaxes lint #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 😉

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. 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, 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 continues to eliminate the the closure as well. I'm quite happy that duplicated code is being removed.

@nnethercote
Copy link
Contributor Author

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 😉

lol, I totally forgot I had done that. Sorry for the confusion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
merged-by-bors This PR was explicitly merged by bors. S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants