Field immutable reference limited by mutable loop

fn main() {
    let mut parent = Parent {
        children: vec![Child::default(), Child::default()],
    };

    for child in &mut parent.children {
        child.field1 = "new".into();

        let field2_vec = &parent
            .children
            .iter()
            .map(|child| child.field2.clone())
            .collect::<Vec<_>>();

        println!("{field2_vec:?}");
    }
}

#[derive(Clone, Debug, Default)]
pub(crate) struct Parent {
    pub(crate) children: Vec<Child>,
}

#[derive(Clone, Debug, Default)]
pub(crate) struct Child {
    pub(crate) field1: String,
    pub(crate) field2: String,
}

In this code, it's safe to immutably borrow field2. Yet, the borrow checker unnecessarily forbids this just because the parent is in a mutable loop (to mutate field1).

The above pattern would be useful to allow because it enables context awareness for each iteration. This is especially useful for debugging purposes and for creating meaningful error messages that contain more useful info. The program might have an interesting field value (bug, or value that triggers a condition, etc) and one might want to know "ok what value does this field have for the other children? What about some other relevant field hidden somewhere in the nested structure?" This contextual info can help the user better understand what's going on in a a complex program, so the user becomes more informed about what's the most appropriate action to take.

While yes, it would be useful to have this work, it is quite difficult to make it work.

for child in &mut parent.children {
    child.field1 = "new".into();
    // nothing preventing the use of `child.field2` here
}

Even with some kind of partial borrows, it would be difficult to describe this situation in a way that the borrow checker could understand.

Do you have a specific proposal on how would you make this work?


Instead, in this specific case, you can just pre-process the field2's, like this:

let field2_vec = parent
    .children
    .iter()
    .map(|child| child.field2.clone())
    .collect::<Vec<_>>();


for child in &mut parent.children {
    child.field1 = "new".into();
        
    println!("{field2_vec:?}");
}

Yeah it looks pretty complicated. Might need to use an incremental approach for implementing this.

Here's one suggestion:

Suggestion 1:

  1. check if mutation operations are actually performed on the mutable reference. If no mutation operations are performed on the mutable reference, then the borrow checker should allow immutable reference. This can be checked by going through each function/method from when the &mut is initiated until when it's dropped. Look at the arguments of each function/method. If it takes &mut foo as an argument, then it means foo can't be immutably referenced. Otherwise it should be allowed to mutably reference f̀oo, even if its mutable reference is still alive
  2. if mutation operations are performed on the mutable reference, and the mutable reference refers to a struct, then check which fields in the struct are actually mutated. If field1 is mutated but field2 is never mutated, then immutable reference should be allowed on field2

I can imagine the 2nd one to be tricky to implement. But if you first implement the 1st one, then the 2nd implementation could be just an extension/modification of how you implement the 1st one

Applying this to your example:

  • the borrow of parent.children starts in the for child in &mut parent.children, where it is used to create a std::slice::IterMut and calls next on it;
  • this is used again in the next iteration of the loop, where next (which takes &mut self) is called again.

If I understand your idea correctly next taking &mut self means that parent.children cannot be immutably references inbetween these two calls, but that's exactly the current cause of the compile error in your code.


As you see at no point child, field1 or field2 are involved, because the issue is regarding parent.children! To actually make your code compile you would need to somewhat instruct the compiler about the properties of IterMut, next, map, etc etc, but what exactly to instruct is the hard part of this issue. The goal would most likely to be able to reduce the aliasing issue from the accesses to parent.children to the two child variables. At that point the already existing analysis can look at the paths being accessed and see that there's no overlap between child.field1 and child.field2.

Suggestion 2:

ok take what I said in my previous comment, but add to it a list of exceptions that are known not to mutate the values. For example .next(&mut) doesn't mutate the value (but rather the mutation happens by some other method/function after you called .next(&mut).

In other words:

use itertools::Itertools;

static MUT_REF_CONSUMERS_THAT_DONT_MUTATE: [&str; 3] = [
    "std::iter::Iterator::next(&mut self)",
    "std::array::IntoIter::next(&mut self)",
    "(etc)",
];

fn main() {
    let list_of_all_mut_ref_consumers: Vec<&str> = list_of_all_mut_ref_consumers();
    let possible_mutators = list_of_all_mut_ref_consumers
        .iter()
        .filter(|mut_consumer| !MUT_REF_CONSUMERS_THAT_DONT_MUTATE.contains(mut_consumer))
        .collect_vec();

    let immutable_ref_is_allowed = possible_mutators.is_empty();

    println!("{immutable_ref_is_allowed}");
}

pub(crate) fn list_of_all_mut_ref_consumers<'a>() -> Vec<&'a str> {
    todo!();
}

Suggestion 3

another approach that could be used:

split &mut into two different variants:

  • A) one that's used by the lowest method/function that actually mutates the value (&mut)
  • B) one that enables child methods/functions to mutate but doesn't do the mutation itself (&muto (or some other keyword))

Traverse the code from when &mut was initialized until it's dropped. if you only have B but not A, then immutable reference can be used. I actually think this approach will be cleaner compared to the one I described earlier in this comment.

There's gonna be a very large number of methods that will take &mut without directly performing any mutations. Not just in standard library, but when people write their own functions/methods and pass &mut arguments. At first, people who feel frustrated with the inability to have immutable references because of &mut can replace it with unsafe {&muto} in their code to tell the compiler that the user has checked the code for borrow safety and has consciously decided to disagree with the borrow checker. Then eventually down the line, the compiler could calculate itself whether a function taking a &mut actually delegates the mutation to another function, in which case the compiler would take &mut in the source code and implicitly turn it into &muto during compilation process

Suggestion 4

Introduce an attribute for function arguments that promises the compiler that the function will never directly mutate a value, but may only delegate mutations to its children. Let's call this attribute #[delegated_mut_ref]. As an example, you'd need to rewrite this:

fn next(&mut self) -> Option<Self::Item>;

into this:

fn next(#[delegated_mut_ref] &mut self) -> Option<Self::Item>; 

The standard library can be manually reviewed for &mut args to assess whether they actually perform mutations directly or if they just empower some child items to perform mutation. If it's the latter, then add the #[delegated_mut_ref] attribute to the &mut argument.

Then, have the compiler follow the given &mut throughout its scope and look for occurrences of &mut that don't have the #[delegated_mut_ref] attribute. If all the places where &mut is used has the #[delegated_mut_ref] attribute, then immutable reference should be allowed