Unclear borrow checker warning about unsound late-bound lifetimes

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.

Nothing good comes from putting lifetime on self.

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

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

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?

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:.

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?