Unclear borrow checker warning about unsound late-bound lifetimes

#1

Edit: I opened an issue on GitHub: https://github.com/rust-lang/rust/issues/58868.
Edit 2: With the help of someone at Github I was able to get a working version. Key was to add a HRTB to the definition of Controller inside AsWorkspaceController:

trait AsWorkspaceController<'a> {
    type Controller: for<'b> WorkspaceController<'b>+'a;

    fn get_controller(&'a mut self) -> Self::Controller;
}

While working on a project I hit a brick wall when designing a more complicated way to reborrow an object so that a different interface is exposed. This is the definition of the types used (stripped down):

trait WorkspaceLog {
    fn get(&self) -> usize;
}

struct TheLog<'a>(&'a FSWorkspaceController<'a>);

impl<'a> WorkspaceLog for TheLog<'a> {
    fn get(&self) -> usize {
        ((self.0).0).0
    }
}

trait WorkspaceController<'a> {
    type Log: WorkspaceLog+'a;

    fn get_log(&'a self) -> Self::Log;
    fn set_log(&mut self, x: usize);
}

struct FilesystemOverlay(usize);

struct FSWorkspaceController<'a>(&'a mut FilesystemOverlay);

impl<'a, 'b> WorkspaceController<'b> for FSWorkspaceController<'a> {
    type Log = TheLog<'b>;

    fn get_log(&'b self) -> Self::Log {
        TheLog(&*self)
    }
    
    fn set_log(&mut self, x: usize) {
        (self.0).0 = x;
    }
}

trait AsWorkspaceController<'a, 'b> {
    type Controller: WorkspaceController<'b>+'a;

    fn get_controller(&'a mut self) -> Self::Controller;
}

impl<'a, 'b> AsWorkspaceController<'a, 'b> for FilesystemOverlay {
    type Controller = FSWorkspaceController<'a>;

    fn get_controller(&'a mut self) -> FSWorkspaceController<'a> {
        FSWorkspaceController(self)
    }
}

fn init1(control_dir: &mut FilesystemOverlay) -> usize {
    let controller = control_dir.get_controller();
    let log = controller.get_log();
    log.get()
}

Now, if I directly use the type FilesystemOverlay everything is just fine:

fn init1(control_dir: &mut FilesystemOverlay) -> usize {
    let controller = control_dir.get_controller();
    let log = controller.get_log();
    log.get()
}

However, if I try to generalize control_dir, I get a problem:

fn init2<O>(control_dir: &mut O) -> usize
    where for<'a, 'b> O: AsWorkspaceController<'a, 'b> {
    let controller = control_dir.get_controller();
    let log = controller.get_log();
    log.get()
}

Although this compiles, there is a warning which might eventually be turned into an error as there seems to be an unsoundness that was fixed by NLL. However, I do not understand what the problem is and how to fix it:

warning[E0597]: `controller` does not live long enough
  --> src/main.rs:59:15
   |
59 |     let log = controller.get_log();
   |               ^^^^^^^^^^ borrowed value does not live long enough
60 |     log.get()
61 | }
   | -
   | |
   | `controller` dropped here while still borrowed
   | borrow might be used here, when `controller` is dropped and runs the destructor for type `<O as AsWorkspaceController<'_, '_>>::Controller`
   |
   = warning: This error has been downgraded to a warning for backwards compatibility with previous releases.
           It represents potential unsoundness in your code.
           This warning will become a hard error in the future.

How do I need to change the code to get rid of the unsoundness? This is the full code in the playground.

#2

Nothing good comes from putting lifetime on self.

#3

Nonsense. The lifetimes on self here are perfectly reasonable and are only there because the author is trying to simulate generic associated types.

The lifetimes in the code take a while to grok, but they are methodical and precise, and I am hard-pressed to find any issues with them. This is a puzzler!

1 Like
#4

My (somewhat wild) guess is AsWorkspaceController<'a, 'b> ends up with a 'b: 'a constraint due to how get_controller and the associated type are defined. But the compiler doesn’t understand this when HRTB is involved (there’s no way to say for<'a, 'b: 'a> .... Or rather, it realizes it can’t enforce this as it sees <O as AsWorkspaceController<'fresh, 'fresh>::Controller<'_> + '_ with fresh inference regions but has no way of knowing/enforcing the aforementioned constraint holds. But I’m not an expert.

I agree this is a nice puzzler. I’d file a Rust github issue for it. Or maybe @nikomatsakis/@pnkfelix will grace us with an answer here.

2 Likes
#5

I think what @vitalyd said comes pretty close (although I think that it’s 'a: 'b): The compiler complains that controller might live too short because I’m unable to express that controller must outlive log.

To be honest do hesitate a bit to open an issue as for me it’s no clear compiler bug. Is there any ongoing work that would allow me to specify 'a: 'b for AsWorkspaceController? Does that make sense at all for late-bound lifetimes or are they always supposed to be “free” aka unbounded?

#6

I think it’s 'b: 'a :slight_smile:

trait AsWorkspaceController<'a, 'b> {
    type Controller: WorkspaceController<'b>+'a;

    fn get_controller(&'a mut self) -> Self::Controller;
}

WorkspaceController<'b> + 'a implies 'b: 'a on its own, no? This is saying that WorkspaceController is bound over some lifetime 'b, but also outlives 'a (i.e. can stay valid for as long as 'a is) - the only way that would make sense is if 'b: 'a.

I think an issue is worth it, if nothing else, then to have a bit more documentation somewhere. And to provide an example where this could go wrong. I’m not sure if there’s any work around late-bound lifetime regions, but this is something @nikomatsakis would know (although I don’t think he frequents this forum much). And, of course, to help us solve the puzzle :slight_smile:.

#7

Interesting :slight_smile: Maybe this is a hint towards the reason why the compiler is confused by my code, since my intention is indeed the opposite: 'a has to outlive 'b as I intended to assign 'a to any references that directly or indirectly lead to FilesystemOverlay, whereas 'b shall be the lifetime of WorkspaceController::Log. Since an instance of Log shall not outlive 'a, I wanted 'a to outlive 'b, aka 'a: 'b.

Let me elaborate why I think that this is correct in the sample code: As @ExpHP already correctly figured, the lifetimes of the traits are there to mimic GATs since I’d like my code to compile on Stable (side node: The same code with GATs crash Nightly at the moment). Inside the definition of AsWorkspaceController<'a, 'b>, the associated type Controller then gets a trait/lifetime bound of WorkspaceController<'b>+'a so that the associated type Log of trait WorkspaceController is lifetime-bounded by a shorter-living lifetime 'b. However, the actual type of Controller may well live beyond 'b since its lifetime directly depends on the lifetime of the type that implements AsWorkspaceController - which is FilesystemOverlay in this example, so it should fit. Do you agree with this logic or is there a mistake in my thinking?