ecstaticm0rse
ecstaticm0rse

Reputation: 1566

Why can't the Rust borrow checker take advantage of `'static` bounds?

Consider the following Rust program:

#![feature(generic_associated_types)]

pub trait Func {
    type Input<'a>;
    type Output;
    
    fn call(self, input: Self::Input<'_>) -> Self::Output;
}

fn invoke<'cx, F>(f: F, ctx: &'cx mut u8)
    where F: 'static + Func<Input<'cx> = &'cx u8, Output = u8>,
{
    let input = &*ctx;
    let out = f.call(input);
    *ctx = out;
}

I've used #![feature(generic_associated_types)], but I think the question I'm asking is still relevant if you move 'a from Func::Input to Func and use a higher-rank trait bound on invoke.

This code errors, but I don't think it's unsound:

error[E0506]: cannot assign to `*ctx` because it is borrowed
  --> src/lib.rs:15:5
   |
10 | fn invoke<'cx, F>(f: F, ctx: &'cx mut u8)
   |         --- lifetime `'cx` defined here
...
13 |     let input = &*ctx;
   |                 ----- borrow of `*ctx` occurs here
14 |     let out = f.call(input);
   |               ------------- argument requires that `*ctx` is borrowed for `'cx`
15 |     *ctx = out;
   |     ^^^^^^^^^^ assignment to borrowed `*ctx` occurs here

First ctx is reborrowed as input, which is passed to f.call and then never used again. f.call returns a value that does not contain any lifetimes (u8: 'static), so there is no connection between out and ctx.

Likewise, the type of f contains no lifetimes (F: 'static), so it cannot hold a reference with lifetime 'cx. Furthermore, the lifetime 'cx cannot be safely coerced to 'static inside call, so there's no way to "smuggle out" a reference with that lifetime that's accessible beyond the invocation of f.call. Therefore, I don't see how anything can alias ctx, and I think assigning to it in the last line should be sound.

Am I missing something? Would accepting this code be unsound? If not, why does Rust fail to take advantage of 'static bounds in this way?

Upvotes: 1

Views: 221

Answers (2)

Aiden4
Aiden4

Reputation: 2654

The code as written is unsound. The lifetime bound of 'static on F is completely irrelevant, because the lifetime bounds of F::Input and F are two distinct lifetimes, and it's the associated type's lifetime that's causing the error. By declaring F::Input<'ctx> = &'ctx u8, you are declaring that the immutable borrow lives the length of the mutable one, making the mutable reference unsafe to use.

As @Stargateur mentioned, the thing that can make this work are Higher Ranked Trait bounds:

fn invoke<F>(f: F, ctx: &mut u8)
    where F: for<'ctx> Func<Input<'ctx> = &'ctx u8, Output = u8>,
{
    let input = ctx;
    let out = f.call(input);
    *input = out;
}

Playground

That is, instead of declaring that the function call is valid for some specific lifetime 'ctx it is valid for all lifetime's 'ctx. This way, the compiler can freely pick an appropriate lifetime for the reborrow to make this work.

As a side note, you might think that using two specific lifetimes in the function definition would be able to work, but any attempt to do so results in the compiler failing to choose the appropriate lifetime that makes things work.

Upvotes: 1

kmdreko
kmdreko

Reputation: 59902

The lifetime 'cx could be 'static meaning input can be smuggled elsewhere and be invalidated by *ctx = out.

There's no way to constrain that a lifetime is strictly less than another, so I don't think adding a "broader" lifetime constraint to a generic type is even considered by the borrow checker.

Upvotes: 1

Related Questions