Mutable borrows in nested function calls

Why doesn’t this work? I’d think that the mutable borrow ends when the nested function finishes, so that it’s now available for an immutable borrow in the outer function:

Playground

struct Foo{}

impl Foo {
    fn new() -> Self {
        Self {}
    }

    fn as_mutable(&mut self) {
    }
    
    fn as_immutable(&self, _:()) {
    }
    
}


fn main() {
    let mut foo = Foo::new();

    //This works fine
    foo.as_mutable();
    foo.as_immutable(());
    
    //This fails
    foo.as_immutable(foo.as_mutable());
}

The error is:

   |
25 |     foo.as_immutable(foo.as_mutable());
   |     --- ------------ ^^^^^^^^^^^^^^^^ mutable borrow occurs here
   |     |   |
   |     |   immutable borrow later used by call
   |     immutable borrow occurs here

I am also wondering how the reverse happens to work:

Playground

struct Foo{}

impl Foo {
    fn as_mutable(&mut self, _:()) {
    }
    
    fn as_immutable(&self, _:()) {
    }
}

fn main() {
    let mut foo = Foo{};

    foo.as_mutable(foo.as_immutable(()));
}

The MIR code for that happens to have interesting lines

        StorageLive(_3);                 // bb0[1]: scope 2 at src/main.rs:14:5: 14:8
        _3 = &mut _1;                    // bb0[2]: scope 2 at src/main.rs:14:5: 14:8
        StorageLive(_4);                 // bb0[3]: scope 2 at src/main.rs:14:20: 14:40
        StorageLive(_5);                 // bb0[4]: scope 2 at src/main.rs:14:20: 14:23
        _5 = &_1;                        // bb0[5]: scope 2 at src/main.rs:14:20: 14:23

which if I am understanding correctly implies that MIR code can have immutable borrow while having mutable borrow.

// WARNING: This output format is intended for human consumers only
// and is subject to change without notice. Knock yourself out.
fn  <impl at src/main.rs:3:1: 9:2>::as_mutable(_1: &mut Foo, _2: ()) -> () {
    let mut _0: ();                      // return place in scope 0 at src/main.rs:4:36: 4:36

    bb0: {
        return;                          // bb0[0]: scope 0 at src/main.rs:5:6: 5:6
    }
}

fn  main() -> () {
    let mut _0: ();                      // return place in scope 0 at src/main.rs:11:11: 11:11
    let mut _2: ();                      // in scope 0 at src/main.rs:14:5: 14:41
    let mut _3: &mut Foo;                // in scope 0 at src/main.rs:14:5: 14:8
    let mut _4: ();                      // in scope 0 at src/main.rs:14:20: 14:40
    let mut _5: &Foo;                    // in scope 0 at src/main.rs:14:20: 14:23
    let mut _6: ();                      // in scope 0 at src/main.rs:14:37: 14:39
    scope 1 {
        let mut _1: Foo;                 // "foo" in scope 1 at src/main.rs:12:9: 12:16
    }
    scope 2 {
    }

    bb0: {
        StorageLive(_1);                 // bb0[0]: scope 0 at src/main.rs:12:9: 12:16
        StorageLive(_3);                 // bb0[1]: scope 2 at src/main.rs:14:5: 14:8
        _3 = &mut _1;                    // bb0[2]: scope 2 at src/main.rs:14:5: 14:8
        StorageLive(_4);                 // bb0[3]: scope 2 at src/main.rs:14:20: 14:40
        StorageLive(_5);                 // bb0[4]: scope 2 at src/main.rs:14:20: 14:23
        _5 = &_1;                        // bb0[5]: scope 2 at src/main.rs:14:20: 14:23
        StorageLive(_6);                 // bb0[6]: scope 2 at src/main.rs:14:37: 14:39
        _4 = const Foo::as_immutable(move _5, move _6) -> bb1; // bb0[7]: scope 2 at src/main.rs:14:20: 14:40
                                         // ty::Const
                                         // + ty: for<'r> fn(&'r Foo, ()) {Foo::as_immutable}
                                         // + val: Scalar(Bits { size: 0, bits: 0 })
                                         // mir::Constant
                                         // + span: src/main.rs:14:24: 14:36
                                         // + ty: for<'r> fn(&'r Foo, ()) {Foo::as_immutable}
                                         // + literal: Const { ty: for<'r> fn(&'r Foo, ()) {Foo::as_immutable}, val: Scalar(Bits { size: 0, bits: 0 }) }
    }

    bb1: {
        StorageDead(_6);                 // bb1[0]: scope 2 at src/main.rs:14:39: 14:40
        StorageDead(_5);                 // bb1[1]: scope 2 at src/main.rs:14:39: 14:40
        _2 = const Foo::as_mutable(move _3, move _4) -> bb2; // bb1[2]: scope 2 at src/main.rs:14:5: 14:41
                                         // ty::Const
                                         // + ty: for<'r> fn(&'r mut Foo, ()) {Foo::as_mutable}
                                         // + val: Scalar(Bits { size: 0, bits: 0 })
                                         // mir::Constant
                                         // + span: src/main.rs:14:9: 14:19
                                         // + ty: for<'r> fn(&'r mut Foo, ()) {Foo::as_mutable}
                                         // + literal: Const { ty: for<'r> fn(&'r mut Foo, ()) {Foo::as_mutable}, val: Scalar(Bits { size: 0, bits: 0 }) }
    }

    bb2: {
        StorageDead(_4);                 // bb2[0]: scope 2 at src/main.rs:14:40: 14:41
        StorageDead(_3);                 // bb2[1]: scope 2 at src/main.rs:14:40: 14:41
        StorageDead(_1);                 // bb2[2]: scope 0 at src/main.rs:15:1: 15:2
        return;                          // bb2[3]: scope 0 at src/main.rs:15:2: 15:2
    }
}

fn  <impl at src/main.rs:3:1: 9:2>::as_immutable(_1: &Foo, _2: ()) -> () {
    let mut _0: ();                      // return place in scope 0 at src/main.rs:7:34: 7:34

    bb0: {
        return;                          // bb0[0]: scope 0 at src/main.rs:8:6: 8:6
    }
}

That’s because order of evaluation evaluates foo. before the arguments, so at this point foo is already “locked” for use by the method call.

There are plans to relax that. Search for two-phase borrows.

For now the solution is to split it into two expressions.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.