Lack of HRTB produces nonsense error message, correct syntax isn't even mentioned in The Book

3ff4ff7
Opened by Christian Iversen at 2024-12-21 05:02:44

I hit a weird error case today. It turns out, I hit a problem with lifetimes where the only solution was HRTBs (Higher-Ranked Trait Bounds).

I've tried to condense the reproduction sample as much as possible, without it being utterly abstract:

use std::io::Result;
use std::fmt::*;

pub trait CanDecode where Self: Sized { fn read(&mut Decoder) -> Result<Self>; }
pub trait CanEncode { fn write(self, &mut Encoder) -> Result<()>; }

pub struct Decoder {}
pub struct Encoder {}


#[derive(Debug)]
struct DataType {}

impl CanDecode for DataType { fn read(_: &mut Decoder) -> Result<Self> { Ok(DataType {}) } }
impl<'a> CanEncode for &'a DataType { fn write(self, _: &mut Encoder) -> Result<()> { Ok(()) } }

fn main()
{
    parse::<DataType>().unwrap();
}

fn parse<'a, P>() -> Result<()> where
    P: CanDecode + Debug,

  /* attempted syntax: */
&'a P: CanEncode,
    P: 'a,

  /* correct syntax */
  // for <'x> &'x P: CanEncode,

{
    let mut rdr = Decoder {};
    let frame = P::read(&mut rdr)?;
    let mut wtr = Encoder {};
    frame.write(&mut wtr)?;
    Ok(())
}

Compiling this code:

$ rustc --version
rustc 1.19.0-nightly (e17a1227a 2017-05-12)

$ rustc hrtb-ergonomics.rs
error: `frame` does not live long enough
  --> hrtb-ergonomics.rs:36:5
   |
36 |     frame.write(&mut wtr)?;
   |     ^^^^^ does not live long enough
37 |     Ok(())
38 | }
   | - borrowed value only lives until here
   |
note: borrowed value must be valid for the lifetime 'a as defined on the body at 32:0...
  --> hrtb-ergonomics.rs:32:1
   |
32 | / {
33 | |     let mut rdr = Decoder {};
34 | |     let frame = P::read(&mut rdr)?;
35 | |     let mut wtr = Encoder {};
36 | |     frame.write(&mut wtr)?;
37 | |     Ok(())
38 | | }
   | |_^

error: aborting due to previous error

Replacing the "attempted code" with the "correct syntax" comment makes this compile fine.

For me, this points towards several sub-issues:

  1. The error message is wrong - it should work just fine according to rustc. It's clearly not ergonomic, at least.

  2. HRTBs are not mentioned at all in the book. The for<'a> syntax was completely new to me, when I heard about it on IRC. The Nomicon mentions this (with the comment "There aren't many places outside of the Fn traits where we encounter HRTBs, and even for those we have a nice magic sugar for the common cases.")

  3. The code is clearly fine (except for the lifetime annotations), but I don't understand why the non-HRTB version doesn't compile. The reference is alive inside the parse() fn, which seems fine to me?

  4. It seems odd to me that I hit a feature that is so obscure that the Nomicon mentions that there "aren't many places" where it is encountered. I don't feel like my code is doing anything super obscure. Is there some other much easier way to express this, that I am missing?

If this issue should be split up into multiple issues, I'd be happy to help - please advice.

  1. The error here is absolutely correct. Here is a reduced test case:

    trait Get {
        fn get() -> Self;
    }
    
    trait Use {
        fn use_up(self);
    }
    
    fn parse<'a, P>()
        where P: Get, &'a P: Use, P: 'a
    {
        let p = P::get();
        p.use_up();
    }
    

    The parse function here uses the bound &'a P: Use - but we only have a local variable p to take a reference to. In particular, we don't know that the reference will live long enough. You might think that this is fine, but then consider the following implementations:

    impl Get for i32 {
        fn get() -> Self { 42 }
    }
    
    impl Use for &'static i32 {
        fn use_up(self) {
            std::thread::spawn(move || println!("{}", self));
        }
    }
    

    Now in this case you can see that the bounds are satisfied for 'a == 'static and P == i32 - but something bad happens! The thread gets spawned with a reference to the local variable p, which then goes out of scope, leaving a dangling reference! This is definitely something we want to avoid, which is why the borrow checker rejects this code.

    djzin at 2017-05-13 22:17:47

  2. With regard to the lack of documentation, though, I agree it is bad. It apparently has not been got to in the second edition of the book, although there appears to be a section planned. I agree that it does come up more often than is described in the Nomicon. Maybe it is worth opening up an issue in the book.

    djzin at 2017-05-13 22:27:38

  3. Thanks for the quick reply!

    I agree, especially after your explanation, that the error is correct, but I absolutely disagree that the error message is correct.

    The error message is literally saying that the value only lives until line 38, but it has to live until line 38. Not only is this (as far as I understand) not correct, but it also does not really help explain what is wrong, to someone who doesn't already know the answer to this problem, imho.

    I think the explanation that makes the most sense to me, is that it would require turning a reference into a an owned value, which obviously is not going to fly. Is this a fair explanation for the problem here? It seems to mesh with your example.

    As an aside, is there some way to have a type parameter only match non-reference types? I know negative matching is (almost?) completely unsupported, but perhaps there's a special case for this, or something in the works?

    Christian Iversen at 2017-05-14 19:28:57

  4. I think the explanation that makes the most sense to me, is that it would require turning a reference into a an owned value, which obviously is not going to fly. Is this a fair explanation for the problem here? It seems to mesh with your example.

    Ah, no this is not the reason. In fact, if you add an extra scope, the error becomes clear:

    trait Get {
        fn get() -> Self;
    }
    
    trait Use {
        fn use_up(self);
    }
    
    fn parse<'a, P>()
        where P: Get, &'a P: Use, P: 'a
    {
        {
            let p = P::get();
            p.use_up();
        }
    }
    
    rustc 1.17.0 (56124baa9 2017-04-24)
    error: `p` does not live long enough
      --> <anon>:14:9
       |
    14 |         p.use_up();
       |         ^ does not live long enough
    15 |     }
       |     - borrowed value only lives until here
       |
    note: borrowed value must be valid for the lifetime 'a as defined on the body at 11:0...
      --> <anon>:11:1
       |
    11 |   {
       |  _^ starting here...
    12 | |     {
    13 | |         let p = P::get();
    14 | |         p.use_up();
    15 | |     }
    16 | | }
       | |_^ ...ending here
    
    error: aborting due to previous error
    

    The error message is literally saying that the value only lives until line 38, but it has to live until line 38. Not only is this (as far as I understand) not correct, but it also does not really help explain what is wrong, to someone who doesn't already know the answer to this problem, imho.

    This is the main problem here IMO. One lifetime is longer than the other one, but they end on the same line, which is very confusing (specifically, one ends before the function returns, the other ends after the function has returned). Adding an extra scope makes it clear by separating them, but the error messages do not distinguish them unless you do that.

    As an aside, is there some way to have a type parameter only match non-reference types? I know negative matching is (almost?) completely unsupported, but perhaps there's a special case for this, or something in the works?

    This kind of type (in)equality is not supported, but I'm not clear how this would help in this case

    djzin at 2017-05-16 21:02:46

  5. re-tagging this as diagnostic; we have already made the call regarding HRTB in the book, so this isn't really a docs issue.

    Steve Klabnik at 2017-08-30 19:45:45

  6. Current output:

    error[E0597]: `frame` does not live long enough
      --> src/main.rs:47:5
       |
    35 | fn parse<'a, P>() -> Result<()>
       |          -- lifetime `'a` defined here
    ...
    47 |     frame.write(&mut wtr)?;
       |     ^^^^^----------------
       |     |
       |     borrowed value does not live long enough
       |     argument requires that `frame` is borrowed for `'a`
    48 |     Ok(())
    49 | }
       | - `frame` dropped here while still borrowed
    

    Still doesn't suggest the hrtb. The other minimized cases have similar output.

    Esteban Kuber at 2020-07-13 23:25:40

  7. Current output:

    error[[E0597]](https://doc.rust-lang.org/stable/error_codes/E0597.html): `frame` does not live long enough
      --> src/main.rs:38:5
       |
    24 | fn parse<'a, P>() -> Result<()> where
       |          -- lifetime `'a` defined here
    ...
    36 |     let frame = P::read(&mut rdr)?;
       |         ----- binding `frame` declared here
    37 |     let mut wtr = Encoder {};
    38 |     frame.write(&mut wtr)?;
       |     ^^^^^^^^^^^^^^^^^^^^^
       |     |
       |     borrowed value does not live long enough
       |     argument requires that `frame` is borrowed for `'a`
    39 |     Ok(())
    40 | }
       | - `frame` dropped here while still borrowed
    
    error[[E0597]](https://doc.rust-lang.org/stable/error_codes/E0597.html): `p` does not live long enough
      --> src/lib.rs:13:5
       |
    9  | fn parse<'a, P>()
       |          -- lifetime `'a` defined here
    ...
    12 |     let p = P::get();
       |         - binding `p` declared here
    13 |     p.use_up();
       |     ^^^^^^^^^^
       |     |
       |     borrowed value does not live long enough
       |     argument requires that `p` is borrowed for `'a`
    14 | }
       | - `p` dropped here while still borrowed
    
    error[[E0597]](https://doc.rust-lang.org/stable/error_codes/E0597.html): `p` does not live long enough
      --> src/lib.rs:14:9
       |
    9  | fn parse<'a, P>()
       |          -- lifetime `'a` defined here
    ...
    13 |         let p = P::get();
       |             - binding `p` declared here
    14 |         p.use_up();
       |         ^^^^^^^^^^
       |         |
       |         borrowed value does not live long enough
       |         argument requires that `p` is borrowed for `'a`
    15 |     }
       |     - `p` dropped here while still borrowed
    

    Esteban Kuber at 2023-08-04 12:23:54