NLL seems to not work under HRTB

#![allow(unused)]

struct Foo<'a> {
    field: &'a mut u32,
}

fn main() {
    let mut x = 22;
    let g: G = Box::new(|u| Box::new(Foo { field: u }));
    let a = (g)(&mut x);
    // drop(a); // must explicitly drop it to stop using &mut x
    &mut x;
}

trait Trait {}
impl Trait for Foo<'_> {}
type G = Box<dyn for<'r> Fn(&'r mut u32) -> Box<dyn Trait + 'r>>;

playground

error[E0499]: cannot borrow `x` as mutable more than once at a time
  --> src/main.rs:12:5
   |
10 |     let a = (g)(&mut x);
   |                 ------ first mutable borrow occurs here
11 |     //drop(a); // drop to stop using &mut x
12 |     &mut x;
   |     ^^^^^^ second mutable borrow occurs here
13 | }
   | - first borrow might be used here, when `a` is dropped and runs the destructor for type `Box<dyn Trait>`

NLL vs. drop glue

This is not about HRTBs, but about drop glue: the borrow-checking rules always require that the lifetime of a borrow in some type not dangle when an instance of that type is used / may be used. Thus, the lifetime must still be valid until the point of last use.

What NLL introduces, is the knowledge of "lack of significant drop glue" involving (the type of) such instance. At that point, when an instance goes out of scope, and it does not have drop glue, then it doesn't have to count as a use.

To illustrate, compare the following fine snippet (which NLL allows):

{
  let r;
  {
    let s = String::from("…");
    r = &s;
  } // <- `s` goes out of scope and is dropped

  /* r is technically dangling here */

} // <- `r` goes out of scope but there is no drop glue πŸ†—

to the following problematic one (which NLL correctly denies):

{
  let r;
  {
    let s = String::from("…");
    r = ::scopeguard::guard(&s, /* on drop */ |r| println!("{}", *r));
  } // <- `s` goes out of scope and is dropped

  /* r is dangling here */

} // <- πŸ’₯ `r` goes out of scope and is dropped, running `println!("{}", *r)`

Drop glue and erased types

In your case, the issue then stemmed from the usage of dyn … types, such as dyn 'r + Trait (but they same can be said of opaque types such as -> impl Trait), since those are conservatively deemed to have drop glue.

Indeed, consider the following snippet:

/// Potentially from some other crate
fn erased<'r>(r: &'r String) -> impl 'r + Sized {
    /* ? */
}

{
  let r;
  {
    let s = String::from("…");
    r = erased(&s);
  } // <- `s` goes out of scope and is dropped

  /* r is dangling here */

} // <-  `r` goes out of scope; is there drop glue ❓❓❓
  • if erased is implemented as ::scopeguard::guard(r, …), then there is drop glue and this should be denied for soundness;

  • if erased is implemented as the identity r function, then there is no drop glue so this could be fine.

But the whole point is that erased involves an opaque type, meaning the implementor purposedly wants to avoid leaking implementation details (e.g., it could go with the latter initially, and then in a semver-compatible release change the implementation to use the former!).

So, conservatively, even in the latter case the compiler/borrow-checker has to deny this snippet!

  • (there is one exception, which is when Copy is part of the bounds, since Copy is known by the compiler to preclude any kind of drop glue whatsoever)

And it turns out the same applies for dyn Bounds… types (even more so given that type erasure really makes the compiler not know what the real type is (unless it made cross-function analysis, which is purposedly not the case)).


Application to your snippet

    //              +----------------+ connected as per `g`'s signature.
    //             ++                v
    let a: Box<dyn '_ + Trait> = (g)(&mut x);

    &mut x; // for this unique borrow to be valid,
            // the `&mut x` above must have ended.
            // Thus, `'_` dangles from now on.

} // <- `a` goes out of scope here and is dropped.
  //    `dyn '_ + Trait` is assumed to have drop glue
  //    which requires `'_` not to dangle: πŸ’₯
5 Likes