Skip to content

internal: implement lifetime elision - #22927

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

internal: implement lifetime elision#22927
dfireBird wants to merge 4 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
fn bar(x: &u32) {}
"#,
expect!["ty: &'_ &'_ u32, name: x"],
expect!["ty: &'_ &'<erased> u32, name: x"],

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.

we probably need to check our rendering now, we shouldn't show erased lifetimes to the user

@dfireBird dfireBird Jul 27, 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.

I could make the RegionKind::ReErased to display '_ for now, would that be fine?

Comment on lines +2675 to +2676
lc world &WorldSnapshot [type_could_unify+name+local]
ex world [type_could_unify]

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 module 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
self.lower_type_ref(
it,
impl_trait_lower_fn,
&mut Self::elided_lifetime_placeholder_allocator,

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 look correct. We should first lower the arguments. If there is exactly one HRTB lifetime in them, any elided lifetime in the return type should refer to it. Otherwise, it's an error lifetime (plus a diagnostic). In fact, I'm pretty sure we should remove LifetimeRef::Placeholder.

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 use LifetimeRef::Placeholder for return types, so that I can resolve it correctly during hir-ty lowering. But I missed to copy from my old WIP of the lifetime elision.

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.

I see no reason to resolve it during hir-ty lowering and complicate the code with additional variant when we can resolve it during hir lowering instead.

Comment thread crates/hir-def/src/expr_store/lower.rs Outdated
&mut self,
node: ast::Type,
impl_trait_lower_fn: ImplTraitLowerFn<'_>,
lifetime_elision_fn: LifetimeElisionFn<'_>,

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.

I'm not pleased with passing another callback everywhere. I would prefer an enum field saying what to do: an error, a 'static lifetime (for const and static types), a specific lifetime, or a new anonymous lifetime. In fact you can look at the old LifetimeElisionKind, which implements this (but only for diagnostics) and was copied from rustc.

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 would anyways need access to lifetime Arena in GenericParamCollector.

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.

What do you mean?

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.

The callback works by pushing a '_ and using that Idx to create the LifetimeParamId.

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.

So? Why can't this be done via an enum field?

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.

Maybe instead of having multiple callback factories, we can have that a state in ExprCollector and based on which a the callback (so a single callback factory) returns appropriate LifetimeRef?

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.

You can borrow just the parts you need, like you do for a callback.

@dfireBird dfireBird Aug 2, 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.

I don't think it would work as well, only when lowering functions, because it would be borrowed mutably twice, once when I borrow it for the Arena and the other when it borrows for collect_impl_trait.

It might work in other places tho.

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.

Closures borrow only the fields they use since edition 2024. If they wouldn't the code using callbacks wouldn't compile either.

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 found a way to work around it, without callbacks, took a lot of trial and error to get the borrow checker right 😅

Now to implement it and see if it doesn't panic anywhere.

{
TypeBound::ForLifetime(binder, path)
} else {
TypeBound::Path(path, m)

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.

Do we even need TypeBound::Path now? After all in rustc_type_ir every predicate has a binder. We could just always produce ForLifetime.

@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

rustbot commented Aug 4, 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.

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.

(def, is_trait_assoc_item)
};

if collector.argument_elision_context.is_some() && !is_trait_assoc_item {

@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.

I think this branch means means omitted lifetime arguments are materialized only while lowering parameters. with_param_lt_elision restores argument_elision_context before the return type is lowered, so a signature like:

struct Wrapper<'a>(&'a str);
fn wrap(value: &str) -> Wrapper;

creates a lifetime for value, but does not add the selected lifetime to the return Wrapper.

View changes since the review

@dfireBird dfireBird Aug 4, 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.

I think we can change that branch to match the new lifetime_elision_kind instead, i.e if it's Static or Error don't call that function?

Comment on lines 1186 to 1225
pub(in crate::expr_store) fn lower_generic_args_from_fn_path(
&mut self,
args: Option<ast::ParenthesizedArgList>,
ret_type: Option<ast::RetType>,
impl_trait_lower_fn: ImplTraitLowerFn<'_>,
) -> Option<GenericArgs> {
let params = args?;
let mut param_types = Vec::new();
for param in params.type_args() {
let type_ref = self.lower_type_ref_opt(param.ty(), impl_trait_lower_fn);
param_types.push(type_ref);
}
let args = Box::new([GenericArg::Type(
self.alloc_type_ref_desugared(TypeRef::Tuple(ThinVec::from_iter(param_types))),
)]);
let bindings = if let Some(ret_type) = ret_type {
let type_ref = self.lower_type_ref_opt(ret_type.ty(), impl_trait_lower_fn);
Box::new([AssociatedTypeBinding {
name: Name::new_symbol_root(sym::Output),
args: None,
type_ref: Some(type_ref),
bounds: Box::default(),
}])
} else {
// -> ()
let type_ref = self.alloc_type_ref_desugared(TypeRef::unit());
Box::new([AssociatedTypeBinding {
name: Name::new_symbol_root(sym::Output),
args: None,
type_ref: Some(type_ref),
bounds: Box::default(),
}])
};
Some(GenericArgs {
args,
has_self_type: false,
bindings,
parenthesized: GenericArgsParentheses::ParenSugar,
})
}

@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.

Parenthesized Fn syntax needs its own lifetime-elision state, like the FnPtrType branch.

type Callback = dyn Fn(&str) -> &str;

This should lower as dyn for<'a> Fn(&'a str) -> &'a str.

In a function parameter its a bit different as well:

fn run(callback: &dyn Fn(&str) -> &str) -> &dyn Fn(&str) -> &str;

should lower to

fn run<'p>(
    callback: &'p dyn for<'a> Fn(&'a str) -> &'a str,
) -> &'p dyn for<'b> Fn(&'b str) -> &'b str;

View changes since the review

@dfireBird dfireBird Aug 4, 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.

Oof, I was reminding myself to do it, only to forget it :D

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