-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
Tracking Issue for enabling elided_lifetimes_in_paths
lints
#91639
Comments
I agree it's high time we turned this lint by default. I'd even go as far as making it a future-compatibility lint and make it a hard error in a few releases. |
Yeah, I don't think we ever came to a confident opinion of where the right balance is. Spitballing: one simple syntactic rule that's (hopefully) uncontroversial would be "all lifetimes in return types must be visible". So it's always I don't think just that much is sufficient, but it could be a good starting point. |
One example where this makes life harder. Yesterday I wanted to add one more lifetime parameter to That would require changing about 170 uses of If elided lifetimes in paths are kept explicit, then I guess we need something like |
I think that would be useful in impl headers too. I just noticed this one: -impl<'a, 'b, 'tcx> fmt::Debug for Elaborator<'a, 'b, 'tcx> {
+impl fmt::Debug for Elaborator<'_, '_, '_> { Where being able to say EDIT: Oh, hey, I found a Sept 2018 post where I brought up |
Here's a case where it would be useful to have an explicit In #91831 (comment), the following code: struct Foo<'a>(&'a i32);
impl<'a> Foo<'a> {
fn modify(&'a mut self) {}
}
fn bar(foo: &mut Foo) {
foo.modify();
} produces the following error:
The error message itself is not great ("these two types" doesn't make much sense), but it's made even worse by the fact that As mentioned earlier, having
|
I sincerely hope that this isn't made into a hard error, because as soon as this lint is turned on by default I'll be permanently disabling it for all of my projects. As @petrochenkov said this just makes it painful to refactor code in certain cases (no, I don't want to add a Working with Rust every day the number of times I've hit an issue with which this would help is maybe once or twice a year. I don't know, maybe I'm just good with lifetimes, so I'm not going to say whether it should be the default or not, but I personally very strongly do not want this lint, as to me it essentially brings no value and is just a lint in search of a problem. If the issue here is confusing error messages we could just simply improve them so that they show the elided lifetimes. |
In contrast to:
Helping beginners in Rust every day, I can't even count the number of times where someone has been asking for help by posting a snippet of code that had elided lifetimes in some paths which made it obscure for others to understand the lifetime issue that was involved. |
This sounds like perhaps the error messages could be improved to make it less confusing for the beginners? Or perhaps improve the IDE experience so that the IDE automatically shows you those Type elision can also be confusing for beginners, and yet we aren't advocating for people to always explicitly type them out and we're happy with an IDE-based solution; can't we just do the same for elided lifetimes? |
Rust currently does not allow any kind of type elision in paths though, which is the only scope affected by this lint. You always have to explicit the types in your function's signature and we don't allow eliding any kind of type or const generic parameters either. fn foo(x: Vec) { … } is not valid and probably for a good reason: it would be hard to see at a glance that |
What about just enabling |
I'd like to add some First, (embarrassingly) I had to ask what this means with regards to the hidden lifetime: struct A<'a>(&'a ());
trait Foo {}
impl<F: Fn(A)> Foo for F {} The answer is obvious enough when you see it: impl<F> Foo for F
where
for<'a> F: Fn(A<'a>),
{} So, in this case (as a bound on a generic parameter) it may be useful to require explicit lifetimes mostly to clarify where the lifetime is introduced (here: "for all 'a, F must satisy ..."). Second, for parameters passed into methods, this can come up a lot, and it's tedious. This is mentioned above:
In KAS I've been using this design pattern a lot... pub trait Layout {
fn size_rules(&mut self, size_mgr: SizeMgr, axis: AxisInfo) -> SizeRules;
fn set_rect(&mut self, mgr: &mut ConfigMgr, rect: Rect, align: AlignHints);
fn draw(&mut self, draw: DrawMgr);
}
pub trait Widget: Layout + .. {
fn configure(&mut self, mgr: &mut ConfigMgr);
fn handle_event(&mut self, mgr: &mut EventMgr, event: Event) -> Response;
fn handle_message(&mut self, mgr: &mut EventMgr, index: usize);
// (and more)
} What are those "manager" types? pub struct SizeMgr<'a>(&'a dyn ThemeSize);
pub struct ConfigMgr<'a> {
sh: &'a dyn ThemeSize,
ds: &'a mut dyn DrawShared,
pub(crate) ev: &'a mut EventState,
}
pub struct EventMgr<'a> {
state: &'a mut EventState,
shell: &'a mut dyn ShellWindow,
messages: Vec<Message>,
scroll: Scroll,
action: TkAction,
}
pub struct DrawMgr<'a> {
h: &'a mut dyn ThemeDraw,
id: WidgetId,
} These are contexts, passed frequently into methods.
Is it really useful to force users to annotate with Druid has similar context types, but with two lifetime parameters. Again, example usage omits these elided lifetimes. Small aside: these types cannot impl impl<'a> SizeMgr<'a> {
pub fn re<'b>(&'b self) -> SizeMgr<'b>
where
'a: 'b,
{
SizeMgr(self.0)
}
} Finally, regarding this error message mentioned above:
There is room for an alternative solution: the compiler inserts the elided lifetimes in the message (as it already does in quite a few lifetime error messages). Further note: eliding lifetimes is the norm in rust. Passing |
We discussed this a bit in the @rust-lang/lang team meeting today. While, AFAIK, some members personally like the strict version of this, I don't think there's enough consensus to warn-by-default the lint in its current state right now. So how do we make progress? Here's my attempt to categorize the different possible locations of lifetimes in order of most important to be visible to least important to be visible:
(Note for clarity that "visible" is " I would thus propose that |
That's disappointing. I think that the lang team underestimates how tricky and confusing a missing path lifetime really is. @kpreid, @Globidev and I (and many others) are active on the rust community discord and what we see is that the absence of this lint is a common pain point. Implicit lifetimes is something that trips up newbies frequently, and it's not uncommon to see even intermediate and experienced rustaceans get confused by this. You can see this for yourself; go to the discord (the community one, not official) and search for
That is exactly the point - someone who is not as familiar with this part of the compiler as you, no matter their Rust expertise, will get tripped by a struct that appears to have one lifetime but actually has two. Something like For reference, this is something I have done. Back when I discovered this lint I turned it on in the If you don't like the warning, you can shut them off - with And yes; that's something we do in
I don't think this will accomplish all that much; some people are still going to get annoyed by having to add Speaking of the newbies and telling them to add this lint - once they understand this lint and what it does for them the reaction tends to be "I wish I would have been warned". Can we make it so they will get warned from now on? |
Since I just fell into the trap of hidden lifetime parameters myself, I'd like to provide another example where it really wasn't obvious (to me) what's wrong: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=e7a8987750461aa92afa167ca91c4e82 The very nice people on the Rust Discord pointed me to this issue, making it clear what the problem was. Probably could've saved myself quite a few headscratches with this warning on by default. |
Agreed, @markus-k -- hidden lifetimes in return types like that are the case where there's broad agreement that this lint is most valuable, and thus should definitely be on by default. This is |
I'd love contributing to this as it seems like a manageable task (at least at face value) and I'd really like this warning to become enabled by default, at least in return position, but I've never touched the compiler before and wouldn't really know where to start to be honest. 🙁 |
I'd recommend searching for As far as the lint itself goes:
That is kinda where my knowledge ends... I'm not familiar with this part, but I think that you want to do something in And of course, you should ask on zulip: https://rust-lang.zulipchat.com/ |
The lint logic is in If you want to split the parameter vs return path lint, the easiest way is to make |
Nowadays, this is mostly a leftover that still includes `elided_lifetimes_in_paths`, see rust-lang/rust#91639 for the tracking issue.
Nowadays, this is mostly a leftover that still includes `elided_lifetimes_in_paths`, see rust-lang/rust#91639 for the tracking issue.
The status quo for this lint is quite confusing: it is err-by-default in So at least in function signatures this should be made at least warn-by-default IMO (potentially also downgrading the |
My guess would be that because So in the taxonomy above, it'd be basically case 1, since it's included in the return type of the function, which is the case where I think there's the most agreement that it's important to have it visible. (I might be getting the |
I don't see how that justifies a hard error. And for linting, the reasoning equally applies e.g. to return position impl trait.
|
I do have an opinion (above), but agree moreover that the inconsistency is bad. There is not yet an RFC regarding this issue (a significant breaking change), so it is hard to say what the eventual status will be.
In practical terms, the difference between warn and error is what you can get away with in a debug build, so making Corollary: the lint should not be error-by-default when it could be warn (or allow) for |
I suspect the reason that the lint level differs in async functions is because we intended to make this a hard error universally and we were just doing it in the place we could easily do so -- i.e., async functions didn't have backwards compatibility constraints back in 2018. :) I'm generally in favor of people writing |
Based on discussion in today's @rust-lang/lang meeting: cc @willcrichton, to get feedback on how this will work out for new Rust users. Will ensuring that people write these previously elided lifetimes, and warning if they don't, make Rust easier or harder to teach? |
As @joshtriplett mentioned, we discussed this in the T-lang triage call today. We were in favor of what @scottmcm described here, which is to break this into separate lints so that we can make a separate decision about each. Using the categories that @scottmcm laid out, there are some (e.g. 1 and 2) that seem more likely that they'll have consensus than others. The next step here would be for someone to break it out in that way (or to propose some other reasonable way of breaking these out). We agreed that this does not seem relevant to the Rust 2024 edition. |
I'm first thinking about: when would a Rust learner encounter lifetimes-on-structs for the first time? Here's some plausible scenarios:
In the first three settings, I think requiring learners to write elided lifetimes (or understand them, if the code is auto-generated by the IDE / Copilot / etc.) seems to be a good thing. It's conceptually important to understand that The final setting is what concerns me more. A Rust newbie who goes "I want to learn Rust by building a game" will copy/paste code from a tutorial like this: fn greet_people(time: Res<Time>, query: Query<&Name, With<Person>>) {
for name in &query {
println!("hello {}!", name.0);
}
} The fact that fn greet_people(time: Res<'_, Time>, query: Query<'_, '_, &Name, With<Person>>) The Rust newbie reads this and goes "WTF is all this Looking back at the comments, I'm raising a similar concern to @Andlon's example. I also think @mejrs's points are totally reasonable. I did actually glance through the Rust community discord for discussions of So, more directly towards @joshtriplett's question:
|
I'm echoing a lot of #91639 (comment), but in addition... Taking a hypothetical language like C and adding a borrow checker would make a language that is harder to write a compiling program, but the program is more likely to do what you want in the aggregate. The total amount you need to teach for both cases is probably roughly the same, the difference is when the teaching occurs and how much of a mental model has already been built up that needs to be changed. I think the same thing occurs here — it is easier to type |
I think that is addressed by the idea to split the lint into multiple. As I read it, the Bevy and NAlgebra cases listed here would not be warned about by default. |
Thanks for the detailed response, @willcrichton ! One split I've been thinking about here, compared to the current implementation of the lint, is that only parameter lifetimes that elision ties to a lifetime in the return value would need to be written out. That would mean that fn greet_people(time: Res<Time>, query: Query<&Name, With<Person>>) { would still be un-linted, as there are no return lifetimes, but fn foo_texture(pack: Res<Pack>) -> Res<Foo> { would lint and require fn foo_texture(pack: Res<'_, Pack>) -> Res<'_, Foo> { I'm not sure if that's something someone would ever write, so similarly it would be that fn foo_texture(pack: Res<Pack>) -> &Foo { would lint, expecting fn foo_texture(pack: Res<'_, Pack>) -> &Foo { But that it would still be allowed to do things like fn foo_image(foo: Res<Foo>) -> OwnedImage { or fn frobble(a: MatrixView<u8, 3, 3>, b: MatrixView<u8, 3, 3>) -> Matrix<u8, 3, 3> { since the "interior references" in the Do you think that would be helpful in only requiring it when it's most important, or would the inconsistency about sometimes needing it be more confusing than helpful? |
@scottmcm that proposal seems like a reasonable middle-ground. As you say, ideally Rust only requires annotations "when it's most important", and I think the challenge is to agree upon when an annotation is important. I would say an annotation is important when it conveys key information that is not obvious without the annotation. So:
By contrast:
This still punts the question of what constitutes "key information". As @shepmaster suggests, a fuzzy definition would be "information that would meaningfully influence your understanding of a function's design when you write it, and without this information you might make stupid bugs." To address the related question of consistency: I think Rust's inference/elision of lifetimes is already inconsistent, so you're not really breaking anyone's expectations with more heuristics. For instance, Rust says: "if it would be ambiguous which lifetime an output type contains, then you must specify it" (great!). But then Rust says: "...except for methods, when I will just assume the lifetime is the same as |
Also, I wanted to point out one other reason to be an "annotate all lifetimes" extremist: if a novice syntactically looks at two types If Rust had a first-class notion of metavariables like in proof assistants, then one could just say "lifetimes variables are just like a type metavariable." But as it stands, lifetimes are qualitatively different from types in terms of developer experience. |
I am in support of Scott's proposal. I enjoyed reading the discussion about learning. It feels like something that would benefit from actually doing some user studies and trials. If we had lints to enforce the various rules, it'd be particularly easy to do. |
Yeah, exactly. I disliked when lints first started suggesting putting in the elided lifetimes (IIRC in the 2018 edition idiom lint) but I've since encountered a situation multiple times where even experienced Rustaceans get confused about how a type can be used because of missing lifetime annotations. (And this still happens to me too.) So I would like it very much if we had warn by default lints that start requiring showing all lifetimes required for a type. |
Except.. Type and const generics with defaults. Which is really in the same place, a type's generic parameter list. So regardless of lifetime elision, people will learn sooner or later that |
Work on splitting the lint is discussed at https://rust-lang.zulipchat.com/#narrow/stream/182449-t-compiler.2Fhelp/topic/splitting.20elided_lifetimes_in_paths.20into.20separate.20lints and a draft PR is at #120808 |
As discussed in #131725, this lint has a severe negative impact on Bevy code. |
As a Bevy maintainer, this isn't just a beginner issue. If this lint is shipped for Bevy, the engine itself, all of our crates, all of our users, and all of our documentation will turn it off. It's substantially more verbose, less clear and surfaces irrelevant information. The impact is definitely the most severe for beginners, but mostly because the first thing they'll need to do before they can start using our crate is learn how to disable lints. |
As I've stated previously in this thread, I don't believe there is any intent to ship this lint as it currently stands. I've even mentioned work being done to split the lint into smaller parts. Unfortunately, that PR is currently in review hell and no one wants to touch it. Here is the output of compiling Bevy with what I think should be warn / deny by default. Aside from the 11 existing warnings in the Bevy version I compiled with, I've selected a few samples to demonstrate. I think these all should be changed to follow the lint's suggestion:
As a wild guess, it looks like many of the warnings come from macros, so presumably addressing these would be limited to a smaller amount of source locations. From a quick skim, only one warning shows up from an example. Assuming that there are a good number of examples and that the examples are representative of what Bevy's users write, then enabling this specific subset of the lint should only be a small burden for users. And, in this case, the warning seems really useful because I (not a Bevy user!) would never have expected a typed called
|
Bevy maintainer: I like the output of the return position lint a lot here, and would merge a PR with those changes. The end user impact is minimal, and I think that it helps explain the calling contract of these methods. |
This issue tracks getting the currently-allow-by-default
elided_lifetimes_in_paths
lint https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html#elided-lifetimes-in-paths into a place where it can be enabled by default.Context
(This was formerly in the in-band lifetimes tracking issue, #44524, but is being split out because it doesn't actually depend on in-band specifically now that
'_
is stable.)There seems to be general agreement amongst lang that lifetimes that participate in elision should be visible. That means things like
and
However, there's less agreement whether they're needed in other places. For example, whether it's valuable to require showing the lifetime in
A comment against needing that kind of
'_
: #44524 (comment)And one saying it's helpful to show it: #44524 (comment)
(If anyone remembers where lang discussed this and has notes about it, please post or edit this comment!)
Perhaps one way to make progress is to start splitting up the lint to isolate the different cases?
Related discussions
rust_2018_idioms
lint is very noisy and results in dramatically degraded APIs for Bevy #131725Current status
elided_lifetimes_in_paths
into two separate lints under the same lint group: Split elided_lifetime_in_paths into tied and untied #120808Unresolved questions
The text was updated successfully, but these errors were encountered: