Dr. David J. Pearce

Puzzling Strong Updates in Rust


The idea of a strong update comes from earlier work on static analysis and, in particular, pointer analysis. To understand this, let’s imagine a hypothetical non-null analysis for C:

int* r = (int*) malloc(sizeof(int));
int** p = &r;

At this point, our non-null analysis would conclude that p was nonnull and that r was nullable. That is, r could be NULL if the allocation failed. Let’s continue the example:

...
int* q = (int*) malloc(sizeof(int));
if(q == NULL) { return; }
// Strong Update
*p = q;

At this point, the analysis should conclude that r is nonnull. In the terminology of static analysis, this requires what we call a strong update. To perform a strong update, the non-null analysis must know there is only one possible target for p. If we change the example as follows, then we lose this property (where flag is unknown):

int* r = (int*) malloc(sizeof(int));
int* s = (int*) malloc(sizeof(int));
int** p = flag ? &r : &s;
int* q = (int*) malloc(sizeof(int));
if(q == NULL) { return; }
// Weak Update
*p = q;

Now, the analysis must report both r and s as nullable. This is because, although it knows one of them is nonnull, it doesn’t know which one.

Strong Updates in Rust

Alright, that completes the intro on strong updates. But, what has this got to do with Rust? Well, the borrow checker performs strong updates in some situations. But, it doesn’t always apply them when it could. Whilst considering edge cases like this is a somewhat academic exercise, I find it useful for understanding the borrow checker. And, perhaps, it could even suggest ways to improve the borrow checker.

Here is a simple example accepted by rustc:

fn main() {
 let mut r = 1;
 let mut s = 2;
 let mut p = &mut r;
 p = &mut s;
 println!("r={}, *p={}",r,*p);
} 

As expected, this compiles and prints r=1, *p=2. They key is that, since Rust knows p is overwritten in the assignment, it can relinquish the borrow &mut r. However, if we change this a bit, it breaks:

fn main() {
 let mut r = 1;
 let mut s = 2;
 let mut p = Box::new(&mut r);
 *p = &mut s;
 println!("r={},**p={}",r,**p);
}

This is pretty much the same example as before. The contents of *p must be overwritten in the assignment, and so the borrow &mut r should be relinquished as before. Unfortunately, rustc rejects this program with the following error:

let mut p = Box::new(&mut r);
 mutable borrow here ------
*p = &mut s;
println!("r={},**p={}",r,**p);
  mutable borrow  here --- ^
                           |
       immutable borrow here

One could argue this is expected since Box<T> has no special status in Rust (i.e. it is just a regular user-defined type). Hence, the borrow checker cannot be expected to know about its invariants. Ok, so let’s just change up the example like this:

fn main() {
 let mut r = 1;
 let mut s = 2;
 let mut q = &mut r;
 let p = &mut q;
 *p = &mut s;
 println!("r={}, **p={}",r,**p);
}

Now, instead of using Box<T>, we’re using a mutable borrow. Its roughly the same thing but, in this case, mutable borrows do have special status. So, we could reasonably expect the borrow checker to know about the invariants they maintain (i.e. and allow a strong update here). And yet this example is still rejected.

Is that it?

Well, not quite. The above suggests the borrow checker doesn’t perform strong updates in some situations when it could. Based on that, I was thinking the following would fail:

fn f<'a>(p: &mut &'a mut i32, 
         q: &'a mut i32) {
  let r = &mut **p;
  *p = q;
  println!("r={}, **p={}",r,**p);
}

This is somehow similar to before, in that it requires a strong update on *p to know that p and r do not interfere on the last statement. But, in fact, the above program is accepted! So, the borrow checker does perform strong updates sometimes…

Conclusion

Well, I’m not sure what the conclusion is here. Its not clear that these kinds of situations are likely to arise in practice. Perhaps with some further refinement we could figure out a realistic example which could (in principle) be accepted, but currently is not. Eitherway, exploring the limits of the borrow checker is fun!


Follow the discussion on Reddit