[Withdrawn] Reassigning reborrowed mutable references


#1

Edit: Just reread the NLL proposal, and saw this was problem case #4. I’d forgetten this was addressed there.

When borrowing through mutable reference stored in a variable, you have two options for how to treat the variable, either moving out of it or reborrowing it. If you intend to reassign the variable while the loan exists, you have to move out of the variable, and otherwise you typically reborrow the variable so you can continue to use it after the loan expires. However, in some cases, you want to conditionally reassign the variable. If you move out of the variable, then it becomes invalid in the case where you don’t reassign it. If you reborrow it, then you can’t reassign it while the reborrow exists. Consider the case where you’re traversing a simple linked list, such as to get a reference to the foot:

struct Node {
    value: i32,
    next: Option<Box<Node>>,
}

impl Node {
    fn get_next_mut(&mut self) -> &mut Option<Box<Node>> {
        &mut self.next
    }
}

fn cons(value: i32, next: Option<Box<Node>>) -> Box<Node> {
    Box::new(Node{value, next})
}

fn main() {
    let mut list = cons(0, None);
    
    for i in 1..10 {
        list = cons(i, Some(list));
    }
    
    let mut curr = &mut *list;
    if let Some(ref mut next) = curr.get_next_mut() {
        // curr = next; // error: cannot assign to `curr` because it is borrowed
        // println!("{}", curr.value);
    }
    println!("{}", curr.value);
    
    if let Some(ref mut next) = {curr}.get_next_mut() {
        curr = next;
        println!("{}", curr.value);
    }
    // println!("{}", curr.value); // error: use of moved value: `curr.value`
}

This is unfortunate. Notionally, reassigning the variable shouldn’t invalidate the sub-borrow in this case: the sub-borrow is valid whether you chose to move or reborrow the variable. As far as I can tell, there would be no soundness problem with allowing a reborrowed mutable reference variable to be reassigned during the lifetime of the reborrow (though, this could be due to lack of imagination on my part :wink:).

Questions: Would this be sound? Is this feasible to implement? Is this desirable?

With non-lexical lifetimes it should be possible to work around this limitation:

    let mut curr = &mut *list;
    {
        let temp = curr;
        curr = match temp.get_next_mut() {
            Some(ref mut next) => next,
            None => temp,
        };
        println!("{}", curr.value);
    }

However, the code can get more convoluted as looping gets involved, forcing a somewhat unnatural style on the code:

    let mut curr = &mut *list;
    loop {
        let temp = curr;
        if let Some(ref mut next) = temp.get_next_mut() {
            curr = next;
        } else {
            curr = temp;
            break;
        }
    }
    println!("{}", curr.value);

compared to:

    let mut curr = &mut *list;
    while let Some(ref mut next) = curr.get_next_mut() {
        curr = next;
    }
    println!("{}", curr.value);

#2

While this example would be sound, I think that the difficulty comes from the fact that the compiler isn’t aware whether the there still exists an outstanding reference to the curr variable itself. What if get_next_mut() returns a reference to curr, not next? Then we would have two mutable references to curr. For this to work, there would have to be some mechanism for get_next_mut to declare that it’s “lease” to the curr variable ends. Note that it would still be unsound if curr was read after that, because that allows getting another mutable reference to next, so even with such a mechanism curr would have to be “write only” until it has been reassigned, or until the end of the lifetime of next.

Edit: Ah, I got your point now right after I posted this. Your point was it can’t have a reference to curr because the method call reborrows the original. My mental models was that it borrows like &mut curr and then auto-derefs that to get the original, which means that curr is still “tainted” by the lifetime, but if re-thinking it; yes, the compiler can see locally that it’s just a reborrow and the method can’t have a reference to the variable itself.


#3

Additional note: I still think that my initial mental model is correct in the sense that this all works through Deref traits, which initially takes a reference to the variable itself (here: curr). Since the Deref traits are not unsafe to implement, nothing stops arbitrary Deref implementations from doing nasty stuff. The question is then, should the compiler special case some trusted Deref impls, such as the one of Box, &T and &mut T? (Downside: Special cases are frowned upon.) Should there be a general mechanism for implementing a Deref trait that is trusted to reborrow “cleanly”? (Downside: Complexity and design effort.)

Anyway, I think that 1) lifetimes are one of the most important features of Rust that make it the language it is 2) they are still underergonomic, like the case presented here and many others show. 3) There’s still a lot of potential for improvements – and Rust is in unique position to lead the way.

So I for one generally support finding a solution for this :slight_smile:


#4

Dereferencing reference types is a primitive operation. *p desugars to *Deref::deref(&p) or *DerefMut::deref_mut(&mut p). This wouldn’t work without a base case. The Deref impls for references appear circular unless dereferencing them is understood to be a primitive operation:

#[stable(feature = "rust1", since = "1.0.0")]
impl<'a, T: ?Sized> Deref for &'a T {
    type Target = T;

    fn deref(&self) -> &T { *self }
}