Skip to content

feat: implement lifetime elision - #22927

Open
dfireBird wants to merge 9 commits into
rust-lang:masterfrom
dfireBird:lifetime_elision
Open

feat: implement lifetime elision#22927
dfireBird wants to merge 9 commits into
rust-lang:masterfrom
dfireBird:lifetime_elision

Conversation

@dfireBird

Copy link
Copy Markdown
Member

No description provided.

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Jul 27, 2026
@dfireBird

Copy link
Copy Markdown
Member Author

Not sure why this fails in CI and passes in my system?

@ShoyuVanilla

ShoyuVanilla commented Jul 27, 2026

Copy link
Copy Markdown
Member

Not sure why this fails in CI and passes in my system?

Use `env RUN_SLOW_TESTS=1 cargo test` to run the full suite.

You need to set an extra env var to run slow tests

@dfireBird

Copy link
Copy Markdown
Member Author

Oh I didn't know, we ran slow tests. My bad 😅

@Veykril Veykril left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Only a partial review so far

View changes since this review

Comment thread crates/hir-def/src/expr_store/pretty.rs Outdated
Comment thread crates/hir-def/src/lib.rs Outdated
Comment thread crates/hir-def/src/resolver.rs Outdated
Comment thread crates/hir-ty/src/display.rs Outdated
Comment thread crates/ide-completion/src/context/tests.rs Outdated
Comment on lines +2675 to +2676
lc world &WorldSnapshot [type_could_unify+name+local]
ex world [type_could_unify]

@Veykril Veykril Jul 27, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Interesting, so we lose a lot of type equality now which I guess makes sense. Worth to keep in mind if we want t o have something like type-equal modulo lifetimes

Comment thread crates/ide/src/signature_help.rs
Comment thread crates/hir-def/src/expr_store/lower/generics.rs Outdated
@Veykril

Veykril commented Jul 27, 2026

Copy link
Copy Markdown
Member

Very excited for this, I've been wanting this for so long!

@ChayimFriedman2 ChayimFriedman2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Comment thread crates/ide/src/hover/tests.rs Outdated
Comment thread crates/hir-expand/src/name.rs Outdated
Comment thread crates/hir-def/src/lib.rs Outdated
Comment thread crates/hir-def/src/expr_store/lower.rs Outdated
Comment thread crates/hir-def/src/expr_store/lower.rs Outdated
Comment thread crates/hir-def/src/expr_store/lower.rs Outdated
@dfireBird

Copy link
Copy Markdown
Member Author

Two tests will be failing, one of which I think value in expect is correct but after infer it gets turned into a RegionKind::Error, which make the test fail (within the expect)
Other fails because of failure of const eval query, because we are passing empty generic args to query (here), not sure how to fix this or should we let the const eval fail?.

@ChayimFriedman2

Copy link
Copy Markdown
Contributor

Const eval doesn't care about lifetimes, as long as you only have lifetimes there you can pass erased or identity.

@dfireBird

Copy link
Copy Markdown
Member Author

I'm not sure what you mean?

@ChayimFriedman2

Copy link
Copy Markdown
Contributor

If all args are lifetime, you can pass a dummy GenericArgs, with RegionKind::ReErased or GenericArgs::identity_for_item().

@dfireBird

Copy link
Copy Markdown
Member Author

Passing the GenericArgs::identity_for_item() (let me know if I made a mistake there, once I push) make the test pass as is. Thanks for the help.

@dfireBird

Copy link
Copy Markdown
Member Author

Found out why we get error region on other test, when we resolve the types, we fold the ReVar into a ReError here: https://github.com/rust-lang/rust-analyzer/blob/master/crates/hir-ty/src/infer/unify.rs#L705-L713, then I think this is expected behavior for this test, if so then I'll revert the expect on that test as well?

@rustbot

This comment has been minimized.

@rustbot

This comment has been minimized.

Comment on lines +749 to +770
fn elide_return_lifetime(&mut self) {
let old_elision_kind =
mem::replace(&mut self.lifetime_elision_kind, LifetimeElisionKind::Error);

let new_elision_kind =
if let LifetimeElisionKind::NewLifetimeParam { return_lt, total_created, .. } =
old_elision_kind
{
let lifetime_param_id =
return_lt.and_then(|(lifetime_param_id, elided_source)| match elided_source {
ArgumentElisionContext::Self_ => Some(lifetime_param_id),
ArgumentElisionContext::Param if total_created == 1 => {
Some(lifetime_param_id)
}
ArgumentElisionContext::Param => None,
});
LifetimeElisionKind::Lifetime(lifetime_param_id)
} else {
unreachable!("Method should not be called with other lifetime_elision_kind")
};
self.lifetime_elision_kind = new_elision_kind;
}

@Veykril Veykril Aug 4, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This setup doesn't quite handle these correctly i think

fn get<'a>(&'a self) -> &str
fn identity<'a>(value: &'a str) -> &str
type Callback = for<'a> fn(&'a str) -> &str;

These are valid signatures that participate in elision. The return types here all have 'a as their elided lifetimes.

And for this it incorrectly elides to y's lifetime.

fn bad<'a>(x: &'a str, y: &str) -> &str

That signature is an error, return type elision only happens if the args have exactly one lifetime (or if there is a self param).

Reason being that lower_lifetime_ref doesn't participate (from what I can tell in lifetime_elision_kind).

View changes since the review

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I have done the return type elision very haphazardly, I should take a look at it and fix it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think I should introduce a new LifetimeRef for elided lifetimes or designate LifetimeRef:::Param and LifetimeRef::HrtbParam only to be used for elided (by renaming or something). Because with the fix I'm doing for this, a function like this:

fn identity<'a>(value: &'a str) -> &str

// would be lowered and printed as
fn identity<'a>(value: &'a str) -> &'a str

Because I use the LifetimeRef itself, since this is also a valid elision:

fn identity(value: &'static str) -> &str

// it would lower and print 
fn identity(value: &'static str) -> &'static str

Note that the LifetimeRefId are different in argument and return but the LifetimeRef itself will be same.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The way elision works, is that first you lowered the args. Then you take all lifetimes defined from the args and for<...> in the fn, in other words all HRTB lifetimes in the last HRTB stack entry for fn ptrs or all lifetime params in a fn definition. If there is exactly one, all elided lifetimes in the return type get it. Otherwise, eliding lifetimes in the return type is an error.

@dfireBird dfireBird Aug 5, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah I understand that now and is somewhat similar to what I have done, but as you can see from question above since I re-use the LifetimeRef of the lifetime (be it elided or named or static) that can be used for return type, it would result in lowering and analysis that comes after HIR lowering to think, the lifetime in return type was actually provided.

@dfireBird dfireBird Aug 5, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

What is the problem that the code will think that the lifetimes were provided? What does it matter for?

Like for the two test we ignored in ide-diagnostic.

Not sure what the actual behavior is, need to check rustc.

Well I didn't check the rustc code itself, but I printed out the HIR tree for some of the functions discussed here and it seems it's all input lifetimes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Like for the two test we ignored in ide-diagnostic.

The end-goal is to remove the elision in hir-ty.

Well I didn't check the rustc code itself, but I printed out the HIR tree for some of the functions discussed here and it seems it's all input lifetimes.

But what is an input lifetime? Any lifetime mentioned in the input?

@Veykril Veykril Aug 5, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Input lifetimes are lifetimes used in the parameters, output are the ones in the return type

https://doc.rust-lang.org/reference/lifetime-elision.html?highlight=elision#lifetime-elision-in-functions

  • Each elided lifetime in the parameters becomes a distinct lifetime parameter.
  • If there is exactly one lifetime used in the parameters (elided or not), that lifetime is assigned to all elided output lifetimes.

In method signatures there is another rule

  • If the receiver has type &Self or &mut Self, then the lifetime of that reference to Self is assigned to all elided output lifetime parameters.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

So this is the function in rustc that retrieves the candidate for return elision: resolve_fn_params

It records distinct lifetime usages separately for each input type. I think I make some refactors based on this. But I think my current implementation (unpushed) is very similar.

@dfireBird dfireBird Aug 5, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Also based on this: https://github.com/rust-lang/rust/blob/main/compiler/rustc_hir/src/def.rs#L932-L964
We can consider LifetimeRef::Param to be elided always?

Comment thread crates/hir-def/src/expr_store/lower/path.rs Outdated
Comment thread crates/hir-def/src/expr_store/lower.rs

@ChayimFriedman2 ChayimFriedman2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review round 2.

I do wonder if we should resolve all lifetimes during hir lowering, instead of hir-ty, and tend towards yes. But this is not for this PR.

View changes since this review

is_lowering_coroutine: bool,

for_type_binder: Option<ThinVec<Name>>,
lifetime_arena: Option<&'a mut Arena<LifetimeParamData>>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This should be in LifetimeElisionKind::NewLifetime. This way we avoid the Option and unwrap().

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This does not prevent it from being a field on LifetimeElisionKind::NewLifetime, this is the active variant when that function is called.

@dfireBird dfireBird Aug 6, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It's not an active variant, since we call it after the lowering the return type, LifetimeElisionKind::Lifetime for the return type lifetime and we need the named lifetime in return as well for the method to work.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not sure what you talk about (and the links have rotten now), but this is certainly possible, see how I implemented that in https://github.com/ChayimFriedman2/rust-analyzer/tree/lt-arena-in-variant.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

So when the update_to_late_bound_lifetimes is called, the lifetime_elision_kind is Lifetime variant not the NewGenericLifetime because the variant is changed before lowering the return type.

Comment thread crates/hir-def/src/expr_store/lower.rs Outdated
Comment thread crates/hir-def/src/hir/type_ref.rs
Comment thread crates/hir-def/src/expr_store/lower.rs Outdated
Comment thread crates/hir-def/src/expr_store/lower.rs Outdated
Comment thread crates/hir-def/src/expr_store/lower/path.rs Outdated
Comment thread crates/hir-def/src/expr_store/lower/path.rs Outdated
Comment thread crates/hir-def/src/expr_store/lower/path.rs Outdated
Comment thread crates/hir-def/src/expr_store/lower.rs Outdated
Comment thread crates/hir-def/src/expr_store/lower/path.rs
@Veykril

Veykril commented Aug 5, 2026

Copy link
Copy Markdown
Member

I do wonder if we should resolve all lifetimes during hir lowering, instead of hir-ty, and tend towards yes. But this is not for this PR.

Yes I think we should

@Veykril

Veykril commented Aug 5, 2026

Copy link
Copy Markdown
Member

Also I really appreciate you tackling this complex task up @dfireBird! (whether you knew how much work this was gonna end up being or not 😄)

@Veykril Veykril changed the title internal: implement lifetime elision feat: implement lifetime elision Aug 5, 2026
@dfireBird

Copy link
Copy Markdown
Member Author

Also I really appreciate you tackling this complex task up @dfireBird! (whether you knew how much work this was gonna end up being or not 😄)

I did not know it would be this much but this is fun compared to my day-to-day work :)

Also, sorry I've been ignoring @ChayimFriedman2 round 2 Review, will get to it, once I finish the return elision correctly.

@dfireBird

dfireBird commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

Now that we merged two TypeBound variants and it has 3 fields, should we make it a struct variant instead of a tuple variant? I think it would much more readable as well while matching?

@rustbot

This comment has been minimized.

@rustbot

This comment has been minimized.

@rustbot

This comment has been minimized.

@ChayimFriedman2 ChayimFriedman2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review round 3, focusing on after-hir-def changes and answering your previous question. I will review the rest of the changes but didn't want to delay the rest for that.

This is now feeling close to merge-able! Excited for this, and thank you for all the hard work!

View changes since this review

Comment thread crates/hir-ty/src/variance.rs Outdated
"{name}[{}]\n",
generics(&db, def)
.iter(false)
.iter(true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actually this is not correct: late bound parameters have no variance. We can still solve for them (it's just meaningless), but displaying them here is confusing.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

So if have to change it to false here, I'll have to change to false here as well, would that be fine?

let count = generics.len(true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It should be but only if you verify that this does not cause bugs due to wrong generic param indices.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I can check rustc once and see what they do.

Comment thread crates/ide/src/signature_help.rs Outdated
Comment thread crates/hir-def/src/expr_store.rs
Comment thread crates/hir-def/src/expr_store/tests/signatures.rs Outdated
is_lowering_coroutine: bool,

for_type_binder: Option<ThinVec<Name>>,
lifetime_arena: Option<&'a mut Arena<LifetimeParamData>>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not sure what you talk about (and the links have rotten now), but this is certainly possible, see how I implemented that in https://github.com/ChayimFriedman2/rust-analyzer/tree/lt-arena-in-variant.

}

#[derive(Debug)]
pub enum LifetimeElisionKind {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Placeholder, which means we cannot remove it...

Comment thread crates/hir-def/src/expr_store/lower/path.rs Outdated
Comment thread crates/hir-def/src/expr_store/lower/path.rs
let mod_path = Interned::new(ModPath::from_segments(kind, segments));

let (resolved_module_def_id, is_trait_assoc_item) = {
let (per_ns, remaining_idx) = collector.def_map.resolve_path(

@ChayimFriedman2 ChayimFriedman2 Aug 28, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Wait a minute... This resolves every path, even in the body. You should absolutely not do that.

View changes since the review

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Perhaps we can put this under the same conditions that happen below like: collector.lifetime_elision_kind.can_elide() || old_lifetimes_constrained_by_input.is_some()?

@rustbot

This comment has been minimized.

@rustbot

This comment has been minimized.

@rustbot

This comment has been minimized.

@rustbot

rustbot commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

This PR was rebased onto a different master commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-review Status: Awaiting review from the assignee but also interested parties.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants