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

Compile error if lifetime parameter is not bounded by itself #88884

Closed
trrk opened this issue Sep 12, 2021 · 8 comments
Closed

Compile error if lifetime parameter is not bounded by itself #88884

trrk opened this issue Sep 12, 2021 · 8 comments
Assignees
Labels
C-bug Category: This is a bug.

Comments

@trrk
Copy link

trrk commented Sep 12, 2021

The compiler behaves strangely. For a given lifetime, if it bounds itself ('a: 'a), it will compile, otherwise it will not. I think it is a bug that this subtle difference can change whether it can be compiled or not.

I tried this code:

fn main() {
    let wrapped_vec = WrappedVec {
        vec: vec!["1"],
    };
    
    let first = first_str_len(wrapped_vec);
    println!("first: {}", first);
}

struct WrappedVec<'a> {
    vec: Vec<&'a str>,
}

impl<'a> WrappedVec<'a> {
    fn iter(&'a self) -> impl Iterator<Item=&'a &'a str> {
        self.vec.iter()
    }
}

fn first_str_len<'a>(arg: WrappedVec<'a>) -> usize {
    let first = arg.iter().next().unwrap();

    let return_arg = |str: &'a str| -> &'a str {
        str
    };

    return_arg(first).len()
}

This code does not compile.

error[E0597]: `arg` does not live long enough
  --> src/main.rs:21:17
   |
20 | fn first_str_len<'a>(arg: WrappedVec<'a>) -> usize {
   |                  -- lifetime `'a` defined here
21 |     let first = arg.iter().next().unwrap();
   |                 ^^^-------
   |                 |
   |                 borrowed value does not live long enough
   |                 argument requires that `arg` is borrowed for `'a`
...
28 | }
   | - `arg` dropped here while still borrowed

For more information about this error, try `rustc --explain E0597`.

You can compile it by rewriting the part shown below.

fn first_str_len<'a>(arg: WrappedVec<'a>) -> usize {
// ↓ <'a> -> <'a: 'a>
fn first_str_len<'a: 'a>(arg: WrappedVec<'a>) -> usize {

The code is unnatural because it was rebuilt by extracting only the parts that seemed necessary for replication from the code where the problem was found.

Meta

rustc --version --verbose:

rustc 1.55.0 (c8dfcfe04 2021-09-06)
binary: rustc
commit-hash: c8dfcfe046a7680554bf4eb612bad840e7631c4b
commit-date: 2021-09-06
host: x86_64-pc-windows-msvc
release: 1.55.0
LLVM version: 12.0.1

I tried it in the Rust playground, and the same thing happened in beta and nightly.
https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=05a374855198719d56d41ac02be0d59e

Backtrace

There was no backtrace

@trrk trrk added the C-bug Category: This is a bug. label Sep 12, 2021
@Noble-Mushtak
Copy link
Contributor

Noble-Mushtak commented Sep 12, 2021

I was able to find a smaller minimal example (here's the playground):

fn evil_iter<'a>(arg: &'a Vec<&'a str>) -> impl Iterator<Item=&'a &'a str> {
    arg.iter()
}

fn first_str_len<'z: 'z>(arg: Vec<&'z str>) -> usize {
    let first = evil_iter(&arg).next().unwrap();
    let return_arg = |str: &'z str| str;
    return_arg(first).len()
}

I think the compiler error is correct, i.e. arg is not being borrowed long enough for first to be a &'z &'z str, but the fact that doing 'z : 'z fixes the error message is the bug.

@rustbot claim

UPDATE: Found a smaller minimal example (here's the playground):

fn ident<'a>(arg: &'a &'a ()) -> &'a &'a () { arg }

fn do_nothing<'z: 'z>(arg: &'z ()) {
    (|s: &'z ()| s)(ident(&arg));
}

@trrk
Copy link
Author

trrk commented Sep 13, 2021

Thank you for finding a smaller minimal example.

I tried based on this and I have a different opinion. My hypothesis is that it is a bug that it does not compile. It is possible that I am not understanding something.

  |
3 | fn do_nothing<'z>(arg: &'z ()) {
  |               -- lifetime `'z` defined here
4 |     (|s: &'z ()| s)(ident(&arg));
  |                     ------^^^^-
  |                     |     |
  |                     |     borrowed value does not live long enough
  |                     argument requires that `arg` is borrowed for `'z`
5 | }
  | - `arg` dropped here while still borrowed

The compiler says `arg` dropped here while still borrowed at the end of the function, but in my opinion, the result of (|s: &'z ()| s)(ident(&arg)) should be dropped before that.

The next one isn't about 'z: 'z. Here is a functions with the same name but different lifetime parameters. In my opinion, all these functions need to be able to compile without errors.

// will not compile
fn ident<'a>(arg: &'a &'a ()) -> &'a &'a () { arg }

// will compile
// two different lifetime parameters
fn ident<'a, 'b>(arg: &'a &'b ()) -> &'a &'b () { arg }

// will not compile
// Return a reference with the shorter of the two lifetime parameters (if my understanding of lifetimes is correct)
fn ident<'a, 'b: 'a>(arg: &'a &'b ()) -> &'a &'a () { arg }

fn do_nothing<'z>(arg: &'z ()) {
    (|s: &'z ()| s)(*ident(&arg));
}

Again, it's possible that I'm not understanding something.

@Noble-Mushtak
Copy link
Contributor

I think you may be misunderstanding the borrow checker and the effect the |s: &'z ()| s has. The closure |s: &'z ()| s is the identity function, but it has a type annotation on its argument which has a notable effect: it requires the argument to this function to be &'z (). Therefore, the shared reference passed into this function must live for at least the lifetime 'z. Otherwise, it will not satisfy the argument type of this closure.

Ergo, for do_nothing to typecheck, ident(&arg) must be a &&'z (), i.e. it is a shared reference to the shared reference. However, ident specifically says the return type is &'a &'a () for some lifetime 'a. Notice how both lifetimes are the same, i.e. both the shared reference itself and the borrow of the shared reference must live for the lifetime 'a. Since we are requiring the shared reference to live for 'z, this means the borrow of the shared reference also lives for 'z, i.e. ident(&arg) is a &'z &'z (). Ergo, since ident is just the identity function, this means &arg must be a &'z &'z (). Ergo, our borrow of the shared reference arg must live for the whole lifetime 'z. However, &arg is a borrow created inside this function, which means that borrow must necessarily be dropped at the end of the body of do_nothing. Since 'z is a lifetime parameter of do_nothing, 'z outlives the body of do_nothing because it comes from outside the call to do_nothing. Ergo, our borrow &arg inside the body of do_nothing can not possibly live for the whole lifetime 'z. That is why the borrow checker says "arg dropped here while still borrowed": Our type annotations requires the borrow &arg to live for the whole lifetime 'z, but the borrow gets dropped at the end of the function while the lifetime 'z is still active.

The other function fn ident<'a, 'b: 'a>(arg: &'a &'b ()) -> &'a &'a () { arg } has the same problem because it also returns &'a &'a (). Since you are requiring 'b: 'a, it is true that 'a is a shorter or equal lifetime to 'b, but in the end, our type annotation on the closure |s : 'z ()| s will still require ident(&arg) to be a &&'z (), so since the return type of this function is &'a &'a (), that means ident(&arg) must be a &'z &'z (), which causes the same problem caused above.

@trrk
Copy link
Author

trrk commented Sep 18, 2021

I didn't understand the code correctly. I think I understand it now thanks to your explanation. Incidentally, I also didn't understand that references with an unacceptable lifetime, such as &'z &'z () generated in do_nothing, cannot even be generated, regardless of whether they are retained after generation or not.

By the way, I have a new discovery. This code may have the same problem as 'z: 'z.

fn ident<'a>(arg: &'a &'a ()) -> &'a &'a () { arg }

fn do_nothing<'z: 'y, 'y>(arg: &'z ()) {
    (|s: &'y ()| s)(ident(&arg));
}

It can be compiled. However, I think it should be a compile error because the lifetime 'y cannot live outside the function. This is only true if my understanding is correct that the lifetime defined in the function signature must live outside the function, regardless of whether it is written as an argument type or not. I believe this is the case because the following code does not compile.

fn ident<'a>(arg: &'a &'a ()) -> &'a &'a () { arg }

fn do_nothing<'z: 'y, 'y>(arg: &'z ()) {
    ident(&arg) as &'y &'y ();
}

@Noble-Mushtak
Copy link
Contributor

the lifetime defined in the function signature must live outside the function, regardless of whether it is written as an argument type or not

My understanding is that you are correct: Any lifetime parameter of a function must outlive the body of that function.

Also, I agree that your new example should not compile, it has the same problem as the original example.

@trrk
Copy link
Author

trrk commented Aug 7, 2022

Maybe this issue should be closed.
We concluded that 'z: 'z should also cause an error, but it compiles without error, and that this is a bug.
We have some code that should compile error if there is no bug. These cause errors when compiled with the current nightly build. This is probably because #98835 has been merged.

version: 1.65.0-nightly (2022-08-06 2befdef)

@trrk
Copy link
Author

trrk commented Aug 7, 2022

I used Rust Playground to check the behavior.

Stable version: 1.62.1
Nightly version: 1.65.0-nightly (2022-08-06 2befdef)

This code can be compiled in stable and cannot be compiled in nightly.

Code that should compile error if there is no bug
fn ident<'a>(arg: &'a &'a ()) -> &'a &'a () { arg }

fn do_nothing<'z: 'z>(arg: &'z ()) {
    (|s: &'z ()| s)(ident(&arg));
}

Copied from #88884 (comment)

fn evil_iter<'a>(arg: &'a Vec<&'a str>) -> impl Iterator<Item=&'a &'a str> {
    arg.iter()
}

fn first_str_len<'z: 'z>(arg: Vec<&'z str>) -> usize {
    let first = evil_iter(&arg).next().unwrap();
    let return_arg = |str: &'z str| str;
    return_arg(first).len()
}

Copied from #88884 (comment)

fn ident<'a>(arg: &'a &'a ()) -> &'a &'a () { arg }

fn do_nothing<'z: 'y, 'y>(arg: &'z ()) {
    (|s: &'y ()| s)(ident(&arg));
}

Copied from #88884 (comment)

fn main() {
    let wrapped_vec = WrappedVec {
        vec: vec!["1"],
    };
    
    let first = first_str_len(wrapped_vec);
    println!("first: {}", first);
}

struct WrappedVec<'a> {
    vec: Vec<&'a str>,
}

impl<'a> WrappedVec<'a> {
    fn iter(&'a self) -> impl Iterator<Item=&'a &'a str> {
        self.vec.iter()
    }
}

fn first_str_len<'a: 'a>(arg: WrappedVec<'a>) -> usize {
    let first = arg.iter().next().unwrap();

    let return_arg = |str: &'a str| -> &'a str {
        str
    };

    return_arg(first).len()
}

from #88884 (comment)

@trrk
Copy link
Author

trrk commented Oct 9, 2022

With rust 1.64.0, the expected behavior occurs, i.e. compile errors. So I will close this as resolved.

@trrk trrk closed this as completed Oct 9, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-bug Category: This is a bug.
Projects
None yet
Development

No branches or pull requests

2 participants