Skip to content
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

NLL: named lifetimes in closures can be unrelated to those in parent function #98589

Closed
aliemjay opened this issue Jun 27, 2022 · 9 comments · Fixed by #98835
Closed

NLL: named lifetimes in closures can be unrelated to those in parent function #98589

aliemjay opened this issue Jun 27, 2022 · 9 comments · Fixed by #98835
Labels
A-NLL Area: Non-lexical lifetimes (NLL) C-bug Category: This is a bug. P-critical Critical priority regression-from-stable-to-nightly Performance or correctness regression from stable to nightly. T-types Relevant to the types team, which will review and decide on the PR/issue.

Comments

@aliemjay
Copy link
Member

aliemjay commented Jun 27, 2022

Update: I originally thought it's a soundness issue, but it may not be, see comment #98589 (comment).

This code compiles after #95565 although it shouldn't: (playground)

fn lives_as_long<'a, T>() where T: 'a, {}

fn test<'a, T>()
where
    &'a (): Sized, // any predicate containing `'a`
{
    || {
        lives_as_long::<'a, T>();
    };
}

@rustbot label C-bug T-types regression-from-stable-to-nightly I-unsound A-NLL

@rustbot rustbot added A-NLL Area: Non-lexical lifetimes (NLL) C-bug Category: This is a bug. I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness regression-from-stable-to-nightly Performance or correctness regression from stable to nightly. T-types Relevant to the types team, which will review and decide on the PR/issue. I-prioritize Issue: Indicates that prioritization has been requested for this issue. labels Jun 27, 2022
@apiraino
Copy link
Contributor

WG-prioritization assigning priority (Zulip discussion).

@rustbot label -I-prioritize +P-critical

@rustbot rustbot added P-critical Critical priority and removed I-prioritize Issue: Indicates that prioritization has been requested for this issue. labels Jun 27, 2022
@steffahn
Copy link
Member

steffahn commented Jun 27, 2022

I have yet to find a way to exploit this code for actual unsound behavior. Perhaps you know a way @aliemjay? Preliminary experiments make it seem … possibly … more as if the lives_as_long::<'a, T>() call kinda ignores the explicit 'a and infers a shorter lifetime instead.

@steffahn
Copy link
Member

steffahn commented Jun 27, 2022

E.g. this works

use std::fmt::Display;

fn lives_as_long<'a, T: Display>(x: T) -> Box<dyn Display + 'a>
where
    T: 'a,
{
    Box::new(x)
}

fn test<'a, T: Display>(x: T)
where
    &'a (): Sized, // any predicate containing `'a`
{
    let f = |x: T| -> Box<dyn Display + 'a> { lives_as_long::<'a, T>(x) };
    let r: Box<dyn Display + '_> = f(x); // <- inferred short lifetime for '_
}

but this doesn’t

use std::fmt::Display;

fn lives_as_long<'a, T: Display>(x: T) -> Box<dyn Display + 'a>
where
    T: 'a,
{
    Box::new(x)
}

fn test<'a, T: Display>(x: T)
where
    &'a (): Sized, // any predicate containing `'a`
{
    let f = |x: T| -> Box<dyn Display + 'a> { lives_as_long::<'a, T>(x) };
    let r: Box<dyn Display + 'a> = f(x); // <- explicit lifetime 'a from the function signature
}
error[E0309]: the parameter type `T` may not live long enough
  --> src/lib.rs:14:13
   |
14 |     let f = |x: T| -> Box<dyn Display + 'a> { lives_as_long::<'a, T>(x) };
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ...so that the type `T` will meet its required lifetime bounds
   |
help: consider adding an explicit lifetime bound...
   |
10 | fn test<'a, T: Display + 'a>(x: T)
   |                        ++++

Same story with

fn test<'a, T: Display>(x: T)
where
    &'a (): Sized, // any predicate containing `'a`
{
    let mut r: Option<Box<dyn Display + '_>> = None; // <- inferred short lifetime for '_
    let f = |x: T| {
        r = Some(lives_as_long::<'a, T>(x));
    };
}

vs

fn test<'a, T: Display>(x: T)
where
    &'a (): Sized, // any predicate containing `'a`
{
    let mut r: Option<Box<dyn Display + 'a>> = None; // <- explicit lifetime 'a from the function signature
    let f = |x: T| {
        r = Some(lives_as_long::<'a, T>(x));
    };
}
error[E0309]: the parameter type `T` may not live long enough
  --> src/lib.rs:15:13
   |
15 |       let f = |x: T| {
   |  _____________^
16 | |         r = Some(lives_as_long::<'a, T>(x));
17 | |     };
   | |_____^ ...so that the type `T` will meet its required lifetime bounds
   |
help: consider adding an explicit lifetime bound...
   |
10 | fn test<'a, T: Display + 'a>(x: T)
   |                        ++++

@steffahn
Copy link
Member

I feel like the “predicate containing 'a” makes 'a early-bound instead of late-bound which is probably what makes “a difference” here in terms of compiler behavior.

@jackh726
Copy link
Member

Early-bound vs late-bound makes sense for the change in behavior

@steffahn
Copy link
Member

steffahn commented Jun 27, 2022

I’m increasingly convinced that this is not really a soundness issue. The behavior is confusing though, so it is an issue. It’s almost as if the closure struct implementing the closure (which would be necessarily generic over the lifetimes it “captures” if implemented manually) did not get instantiated with the right lifetimes, but instead those were left inferred. I.e.

fn lives_as_long<'a, T>() where T: 'a, {}

fn test<'a, T>()
where
    &'a (): Sized, // any predicate containing `'a`
{
    || {
        lives_as_long::<'a, T>();
    };
}

would turn into something like

fn lives_as_long<'a, T>() where T: 'a, {}

fn test<'a, T>()
where
    &'a (): Sized, // any predicate containing `'a`
{
    struct Closure<'a, T>() where T: 'a;
    impl<'a, T> FnOnce<…> for Closure<'a, T> {} // and also FnMut and Fn

    Closure::<'a, T>();
}

if implemented by hand, but it currently behaves as if instead of Closure::<'a, T>(…); something like Closure::<'_, T>(…); is used, leaving the lifetime inferred/unspecified, and meaning that an error due to the T: 'a constraint only appears if the calls (or other usages of the closure) put bounds on that inferred lifetime that are in conflict with the fact that T: 'a is not true.

@aliemjay
Copy link
Member Author

I agree that this is most likely not a soundness issue, but it's still confusing and fixing it in the future would be a breaking change.

By looking closer at the source, the named lifetime 'a that appears in the closure is indeed a free inference variable independent from the 'a found in the defining scope. It behaves similarly to how @steffahn theorized in the previous comment.

Perhaps this counts an unintended stabilization. Re-requesting prioritization in light of this.

@rustbot label -I-unsound +I-prioritize

@rustbot rustbot added I-prioritize Issue: Indicates that prioritization has been requested for this issue. and removed I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness labels Jun 27, 2022
@aliemjay aliemjay changed the title NLL: unsound propagation of region constraints from closures NLL: named lifetimes in closures can be unrelated to those in parent function Jun 27, 2022
@apiraino
Copy link
Contributor

Prioritization assessment P-critical still holds (discussion on Zulip discussion).

@rustbot label -I-prioritize

@rustbot rustbot removed the I-prioritize Issue: Indicates that prioritization has been requested for this issue. label Jun 28, 2022
@steffahn
Copy link
Member

steffahn commented Jun 30, 2022

I haven’t more thoroughly tested it, but adding (): Dummy<'a> bounds for all early-bound lifetime arguments of a function for a trait

trait Dummy<'a> {}
impl<'a> Dummy<'a> for () {}

as in:

trait Dummy<'a> {}
impl<'a> Dummy<'a> for () {}

fn lives_as_long<'a, T>() where T: 'a, {}

fn test<'a, T>()
where
    &'a (): Sized, // any predicate containing `'a`
    (): Dummy<'a>, // <- this is new; added for every early-bound lifetime parameter
{
    || {
        lives_as_long::<'a, T>();
    };
}

seems to “fix” the problem (of incorrectly accepting this function). If code after this transformation does indeed behave as desired, all that would be necessary is to identify what needs to be changed so it behaves as if such a dummy trait bound was present.


Edit: Nevermind, this breaks down quickly, e.g. even for this example, being systematic about the thing as in

trait Dummy<'a> {}
impl<'a> Dummy<'a> for () {}

fn lives_as_long<'a, T>()
where
    T: 'a,
    (): Dummy<'a>, // <- added for every early-bound lifetime parameter on every function
{
}

fn test<'a, T>()
where
    &'a (): Sized, // any predicate containing `'a`
    (): Dummy<'a>, // <- added for every early-bound lifetime parameter on every function
{
    || {
        lives_as_long::<'_, T>(); // no longer 'a
    };
}

will not compile, despite the fact that the original code

trait Dummy<'a> {}
impl<'a> Dummy<'a> for () {}

fn lives_as_long<'a, T>()
where
    T: 'a,
{
}

fn test<'a, T>()
where
    &'a (): Sized, // any predicate containing `'a`
{
    || {
        lives_as_long::<'_, T>(); // no longer 'a
    };
}

does (and should) compile.


Nonetheless, I still think it’s an interesting observation that adding such a trait bound (only to test so it is not even used for calling lives_as_long) somehow makes the code (“successfully”) fail again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-NLL Area: Non-lexical lifetimes (NLL) C-bug Category: This is a bug. P-critical Critical priority regression-from-stable-to-nightly Performance or correctness regression from stable to nightly. T-types Relevant to the types team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants