Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is this error due to the compiler's special knowledge about RefCell?

fn works<'a>(foo: &Option<&'a mut String>, s: &'a mut String) {}
fn error<'a>(foo: &RefCell<Option<&'a mut String>>, s: &'a mut String) {}

let mut s = "hi".to_string();

let foo = None;
works(&foo, &mut s);

// with this, it errors
// let bar = RefCell::new(None);
// error(&bar, &mut s);

s.len();

If I put in the two lines with the comment, the following error occurs:

error[E0502]: cannot borrow `s` as immutable because it is also borrowed as mutable
  --> <anon>:16:5
   |
14 |     error(&bar, &mut s);
   |                      - mutable borrow occurs here
15 |     
16 |     s.len();
   |     ^ immutable borrow occurs here
17 | }
   | - mutable borrow ends here

The signatures of works() and errors() look fairly similar. But apparently the compiler knows that you can cheat on it with a RefCell, because the borrow checker behaves differently.

I can even "hide" the RefCell in another type of my own, but the compiler still always does the right thing (errors in case a RefCell could be used). How does the compiler know all that stuff and how does it work? Does the compiler mark types as "interior mutability container" or something like that?

like image 476
Lukas Kalbertodt Avatar asked Apr 04 '17 14:04

Lukas Kalbertodt


1 Answers

RefCell<T> contains an UnsafeCell<T> which is a special lang item. It is UnsafeCell that causes the error. You could check with:

fn error<'a>(foo: &UnsafeCell<Option<&'a mut String>>, s: &'a mut String) {}

...

let bar = UnsafeCell::new(None);
error(&bar, &mut s);

But the error is not due to compiler recognizing an UnsafeCell introduces interior mutability, but that an UnsafeCell is invariant in T. In fact, we could reproduce the error using PhantomData:

struct Contravariant<T>(PhantomData<fn(T)>);

fn error<'a>(foo: Contravariant<&'a i32>, s: &'a mut String) {}

...

let bar = Contravariant(PhantomData);
error(bar, &mut s);

or even just anything that is contravariant or invariant in the lifetime 'a:

fn error<'a>(foo: Option<fn(&'a i32)>, s: &'a mut String) {}

let bar = None;
error(bar, &mut s);

The reason you can't hide a RefCell is because variance is derived through the fields of the structure. Once you used RefCell<T> somewhere, no matter how deep, the compiler will figure out T is invariant.


Now let's see how the compiler determine the E0502 error. First, it's important to remember that the compiler has to choose two specific lifetimes here: the lifetime in the type of the expression &mut s ('a) and the lifetime in the type of bar (let's call it 'x). Both are restricted: the former lifetime 'a has to be shorter than the scope of s, otherwise we would end up with a reference living longer than the original string. 'x has to be larger than the scope of bar, otherwise we could access an dangling pointer through bar (if a type has a lifetime parameter the compiler assume the type can access a value with that lifetime).

With these two basic restriction, the compiler goes through the following steps:

  1. The type bar is Contravariant<&'x i32>.
  2. The error function accepts any subtype of Contravariant<&'a i32>, where 'a is the lifetime of that &mut s expression.
  3. Thus bar should be a subtype of Contravariant<&'a i32>
  4. Contravariant<T> is contravariant over T, i.e. if U <: T, then Contravariant<T> <: Contravariant<U>.
  5. So the subtyping relation can be satisfied when &'x i32 is a supertype of &'a i32.
  6. Thus 'x should be shorter than 'a, i.e. 'a should outlive 'x.

Similarly, for an invariant type, the derived relation is 'a == 'x, and for convariant, 'x outlives 'a.

Now, the problem here is that the lifetime in the type of bar lives until the end of scope (as per restriction mentioned above):

    let bar = Contravariant(PhantomData);   // <--- 'x starts here -----+
    error(bar,                              //                          |
          &mut s);                          // <- 'a starts here ---+   |
    s.len();                                //                      |   |
                                            // <--- 'x ends here¹ --+---+
                                            //                      |
                                            // <--- 'a ends here² --+
}

// ¹ when `bar` goes out of scope
// ² 'a has to outlive 'x

In both contravariant and invariant cases, 'a outlives (or equals to) 'x means the statement s.len() must be included in the range, causing borrowck error.

Only in the covariant case we could make the range of 'a shorter than 'x, allowing the temporary object &mut s be dropped before s.len() is called (meaning: at s.len(), s is not considered borrowed anymore):

    let bar = Covariant(PhantomData);       // <--- 'x starts here -----+
                                            //                          |
    error(bar,                              //                          |
          &mut s);                          // <- 'a starts here --+    |
                                            //                     |    |
                                            // <- 'a ends here ----+    |
    s.len();                                //                          |
}                                           // <--- 'x ends here -------+
like image 54
kennytm Avatar answered Nov 11 '22 16:11

kennytm