Skip to content

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

Description

@trrk

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

Activity

  1. Noble-Mushtak commented on Sep 12, 2021

    @Noble-Mushtak
    Contributor

    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));
    }
    
  2. trrk commented on Sep 13, 2021

    @trrk
    Author

    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.

  3. Noble-Mushtak commented on Sep 16, 2021

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

  4. trrk commented on Sep 18, 2021

    @trrk
    Author

    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 ();
    }
  5. Noble-Mushtak commented on Sep 19, 2021

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

  6. trrk commented on Aug 7, 2022

    @trrk
    Author

    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)

  7. trrk commented on Aug 7, 2022

    @trrk
    Author

    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)

  8. trrk commented on Oct 9, 2022

    @trrk
    Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

C-bugCategory: This is a bug.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions