rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
98.34k stars 12.72k forks source link

Tracking issue for future-incompatibility lint `late_bound_lifetime_arguments` #42868

Open petrochenkov opened 7 years ago

petrochenkov commented 7 years ago

What is this lint about

In functions not all lifetime parameters are created equal. For example, if you have a function like

fn f<'a, 'b>(arg: &'a u8) -> &'b u8 { .... }

both 'a and 'b are listed in the same parameter list, but when stripped from the surface syntax the function looks more like

for<'a> fn f<'b>(arg: &'a u8) -> &'b u8 { .... }

where 'b is a "true" ("early-bound") parameter of the function, and 'a is an "existential" ("late-bound") parameter. This means the function is not parameterized by 'a. To give some more intuition, let's write a type for function pointer to f:

// PtrF is not parameterized by 'a,
type PtrF<'b> = for<'a> fn(&'a u8) -> &'b u8;
// but it has to be parameterized by 'b
type PtrF = for<'a, 'b> fn(&'a u8) -> &'b u8; // ERROR

See more about this distinction in http://smallcultfollowing.com/babysteps/blog/2013/10/29/intermingled-parameter-lists/#early--vs-late-bound-lifetimes

When lifetime arguments are provided to a function explicitly, e.g.

f::<'my_a, 'my_b>

the first argument doesn't make much sense because the function is not parameterized by 'a. Providing arguments for "late-bound" lifetime parameters in general doesn't make sense, while arguments for "early-bound" lifetime parameters can be provided.

It's not clear how to provide arguments for early-bound lifetime parameters if they are intermixed with late-bound parameters in the same list. For now providing any explicit arguments is prohibited if late-bound parameters are present, so in the future we can adopt any solution without hitting backward compatibility issues.

Note that late-bound lifetime parameters can be introduced implicitly through lifetime elision:

fn f(&u8) {}
// Desugars into
fn f<'a>(&'a u8) {} // 'a is late-bound

The precise rules discerning between early- and late-bound lifetimes can be found here: https://github.com/rust-lang/rust/blob/91aff5775d3b4a95e2b0c2fe50785f3d28fa3dd8/src/librustc/middle/resolve_lifetime.rs#L1541-L1700

How to fix this warning/error

Just removing the lifetime arguments pointed to by the lint should be enough in most cases.

Current status

SimonSapin commented 7 years ago
error: cannot specify lifetime arguments explicitly if late bound lifetime parameters are present
   --> /home/travis/build/servo/servo-with-rust-nightly/servo/components/style/stylesheets/stylesheet.rs:243:27
    |
243 |         self.iter_rules::<'a, 'b, EffectiveRules>(device, guard)
    |                           ^^
    |
note: lint level defined here
   --> /home/travis/build/servo/servo-with-rust-nightly/servo/components/style/lib.rs:26:9
    |
26  | #![deny(warnings)]
    |         ^^^^^^^^
    = note: #[deny(late_bound_lifetime_arguments)] implied by #[deny(warnings)]
    = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
    = note: for more information, see issue #42868 <https://github.com/rust-lang/rust/issues/42868>

What does "late bound" mean in this context? What should we do instead?

petrochenkov commented 7 years ago

@SimonSapin

What does "late bound" mean in this context?

Well, that's a good question, but not something that can be explained in an error message. See for example http://smallcultfollowing.com/babysteps/blog/2013/10/29/intermingled-parameter-lists/#early--vs-late-bound-lifetimes. I believe nomicon had something about it, but I can't find it right now.

This issue should have a description, and @nikomatsakis is probably the best person to write it, but I can try as well.

What should we do instead?

Remove 'a, 'b,, it never did anything anyway.

petrochenkov commented 7 years ago

The error and lint should probably have an extra note pointing to one of these late bound lifetime parameters.

EDIT: Done in https://github.com/rust-lang/rust/pull/43343

SimonSapin commented 7 years ago

What should we do instead?

Remove 'a, 'b, it never did anything anyway.

That’s not true in the case of https://github.com/rust-lang/rust/issues/42508, where it is necessary to make something compile at all.

petrochenkov commented 7 years ago

One possible solution for the problem of intermixed late- and early-bound parameters is to support explicit for<...> clauses on function items in the same way it's done on function pointers:

for<'late> fn f<'early1, 'early2>(a: &'late u8, b: &'early1 u8, c: &/*'elided*/u8) -> &'early2 u8 { ... }

If for is specified then all lifetimes defined in it are considered late-bound, and all lifetimes specified in generic parameters are considered early-bound. For function pointers the equivalent construction is already supported:

type F<'early1, 'early2> = for<'late> fn(&'late u8, &'early1 u8, &/*'elided*/u8) -> &'early2 u8;

The function could then be called with explicitly specified lifetime arguments, despite the presence of late-bound parameters:

f::<'arg_early1, 'arg_early2>()
nikomatsakis commented 7 years ago

@SimonSapin

I believe there exists a workaround to force early-bound lifetimes at a time when they are appropriate. Are you blocked on this point? In particular, you could do something like where 'static: 'a, which would force 'a to be early bound (because it appears in a where-clause).

The definition of late bound lifetimes, mechanically at least, is that they are those lifetimes that both:

All other lifetimes bound on a function are early-bound. So if you have a dummy where clause like where 'static: 'a, which has no particular meaning, it does force 'a to be early-bound.

(The reason that the early- vs late-bound distinction exists is something else, of course.)

I do hope we can clean this up in the future -- perhaps in another epoch -- but for now that is how it works. There are some annoying interactions with inference that make this all hard to alter in a totally backwards compatible way.

SimonSapin commented 7 years ago

I am blocked in the sense that I have warnings that I don’t know how to work around, don’t understand in the first place, and that say “it will become a hard error in a future release”.

arielb1 commented 7 years ago

I think you could use something like (parse_nested_block: fn(&mut Parser<'i, 't>, _))(...) (using type ascription) instead of passing generic parameters. Ugly, but works.

arielb1 commented 7 years ago

Nope, that doesn't work for some reason.

pnkfelix commented 7 years ago

@petrochenkov wrote (in the description):

It's not clear how to provide arguments for early-bound lifetime parameters if they are intermixed with late-bound parameters in the same list. For now providing any explicit arguments is prohibited if late-bound parameters are present, so in the future we can adopt any solution without hitting backward compatibility issues.

Note that late-bound lifetime parameters can be introduced implicitly through lifetime elision [...]

While each of these paragraphs made sense to me when I read them on my own, I am curious about the conclusion that it leads to.

Consider a case like this:

fn all_explicits_are_early<'a, 'b>(arg: &Foo<'a, 'b>) -> Bar<'a, 'b> where 'a: 'a, 'b: 'b { ... }

I would think that a call expression all_expilcits_are_early::<'x, 'y>(param) would be sensible: we are providing arguments for the explicit parameters, which are all early bound.

But unfortunately, the above function definition actually desugars into something with a hidden late-bound lifetime parameter, due to lifetime elision on the arg: &Foo<'a, 'b>. Which then causes the error here to fire in the case described above.

Would it be possible to loosen the error checking here so that if all the explicitly listed lifetime parameters are early-bound, then one still gets to specify the lifetimes explicitly at the call-site, regardless of any hidden late bound lifetimes introduced due to the elision rules?

nikomatsakis commented 7 years ago

@pnkfelix

Would it be possible to loosen the error checking here so that if all the explicitly listed lifetime parameters are early-bound, then one still gets to specify the lifetimes explicitly at the call-site, regardless of any hidden late bound lifetimes introduced due to the elision rules?

That would be possible, I think. It would commit us to supporting that interpretation, of course.

EDIT: But I'm ok with that -- it's actually roughly the model I want going forward. In particular, I want the model to be: if you write explicit lifetime names-- which would no longer be required -- then you must provide a value for them when you write explicit values, even if that value is '_. I'm not sure quite how to get there from where we are, may require some change of behavior around epochs or something.

petrochenkov commented 7 years ago

@pnkfelix The logic (beside just being the most conservative variant) is that elided lifetimes are purely a sugar for the explicitly written form, i.e.

// Full form
fn foo<'early: 'early, 'late>(arg: &'late u8) { ... }
// Short form
fn foo<'early: 'early>(arg: &u8) { ... }

are equivalent and can be switched freely without changing any observable behavior for foo's users.

If the short form doesn't report an error and the full form does, then it's not a sugar and you can't switch between two forms backward-compatibly.

petrochenkov commented 7 years ago

With my proposed solution (https://github.com/rust-lang/rust/issues/42868#issuecomment-325647940) elided lifetimes can always be written explicitly without affecting observable behavior for users, so the restriction is unnecessary and can be relaxed

// Short form
fn foo<'early: 'early>(arg: &u8) { ... } // `foo::<'static>` is okay
// Full desugared form
for<'late> fn foo<'early: 'early>(arg: &'late u8) { ... } // `foo::<'static>` is still okay
SimonSapin commented 5 years ago

This came up again. https://crates.io/crates/cssparser still emits this warning, and I still don’t know of a way to fix it without getting borrowck errors instead.

cole-miller commented 3 years ago

It seems this shows up as either a warn-level lint or a hard error depending on the code.

Example that triggers a warning (on stable and nightly):

static X: u8 = 0;
fn f<'a, 'b>(_x: &'b u8) -> &'a u8  { &X }
fn main() { let _ = f::<'static>; }

Example that triggers a hard error (what led me to open #80618; again, stable or nightly), with no mention of the tracking issue:

fn f<'a>() {}
fn main() { let _ = f::<'static>; }

Is this expected?

petrochenkov commented 3 years ago

@cole-miller It's reported as a lint only in cases where making it a hard error would cause breakage back in 2017. The intent is to produce an error, in general, the lint is only a way to give people some time to fix code.

cole-miller commented 3 years ago

@petrochenkov Thanks, understood. So I can go ahead and assign an error number and --explain text?

fishrockz commented 2 years ago

How do I solve something like

fn lister<'a, 'b, T: LifetimedDisplay<'a>>(
    buffer: &'b mut dyn Write,
    element: project::element::ElementRef,
    project: &'a project::project::Project,
) {
    writeln!(buffer, "{}", T::new(element, project)).unwrap();
}

I get

warning: cannot specify lifetime arguments explicitly if late bound lifetime parameters are present
  --> src/lib.rs:72:18
   |
72 |         lister::<'a, T>(buffer, element.clone(), project);
   |                  ^^
...
78 | fn lister<'a, 'b, T: LifetimedDisplay<'a>>(
   |               -- the late bound lifetime parameter is introduced here
   |
   = note: `#[warn(late_bound_lifetime_arguments)]` on by default
   = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
   = note: for more information, see issue #42868 <https://github.com/rust-lang/rust/issues/42868>

warning: `girderstream` (lib) generated 1 warning

I have spent a few hours on this and am pretty stumpped

fishrockz commented 2 years ago

For anyone else finding things like this you might find this helpful

It was actually solved at the call sight rather than the function

-lister::<'a, T>(buffer, element.clone(), project);
+lister::<T>(buffer, element.clone(), project);

Normally rustc gives the most excellent tips but i think in this case we could do better..

RalfJung commented 1 year ago

I have to say I am quite confused by the issue description here, and I don't understand what the underlying issue is. The core of my confusion seems to be captured by this (repeated) statement

the first argument doesn't make much sense because the function is not parameterized by 'a.

I strongly disagree! The function is parameterized by 'a. Sure, this parameter is a type system fiction (the actual runtime address of the function is the same for all choices of 'a), but that doesn't make 'a any less of a parameter. It can still be valuable to explicitly set that parameter to guide inference. In fact, even if I have a function pointer for<'a> fn(&'a i32) -> &'a i32, I may want to explicitly set the lifetime -- that's a completely meaningful operation and entirely orthogonal to our monomorphization strategy. The issue description seems to presuppose that explicit instantiation is somehow tied to monomorphization, but I don't understand why that would be the case.

Taking a step back: The fact that some lifetime parameters are type system fictions while others are actually relevant for monomorphization is very confusing and surprising. (I have never fully understood why this is the case; I'll need to dig into this some time.) This will clearly incur some friction when creating function pointers since monomorphization-relevant ("early-bound") lifetime parameters need to be chosen the moment the function pointer is created. I can see that we might want to improve that situation. But reading just this issue I really don't understand the motivation for the current change, since there's a big gap between "unfortunately some lifetime parameters actually matter for monomorphization" and "other lifetime parameters (that don't matter for monomorphization) don't make sense and we should disallow people from explicitly stating them". Which problem is being solved here?

amanlai commented 2 months ago

A related Stack Overflow discussion.