Cannot borrow as immutable because it is also borrowed as mutable in previous block scope

I figure borrows got out of scope after the block their declared in, so I don't understand why I can't borrow as mutable in a block, and then after that block borrow again as immutable?

This is sort of convoluted but I think it will show you what I'm trying to do, build a DSP graph.

type Sample = f32;
type Index = usize;

pub enum SignalInput<'a> {
    Single { val: Sample, len: Index },
    Vector(&'a [Sample]),
}
pub enum SignalOutput<'a> {
    Dummy { len: Index },
    Vector(&'a mut [Sample]),
}

struct BlahSignalObject;

impl BlahSignalObject {
    fn perform(
        &mut self,
        _nframes: Index,
        _inputs: &[&SignalInput; 2],
        _outputs: &mut [&mut SignalOutput; 3],
    ) {
    }
}
struct Foo {
    blah0: BlahSignalObject,
    blah1: BlahSignalObject,
}

impl Foo {
    fn perform(&mut self, nframes: Index, _inputs: &[&[Sample]], _outputs: &mut [&mut [Sample]]) {
        let mut internal0: [Sample; 64] = [0.0; 64];
        let _in0: SignalInput = SignalInput::Single {
            val: 0.0,
            len: nframes,
        };
        let _in1: SignalInput = SignalInput::Single {
            val: 0.0,
            len: nframes,
        };
        let in2: SignalInput = SignalInput::Single {
            val: 0.0,
            len: nframes,
        };
        let in3: SignalInput = SignalInput::Single {
            val: 0.0,
            len: nframes,
        };
        let zero: SignalInput = SignalInput::Single {
            val: 0.0,
            len: nframes,
        };

        let mut _out0: SignalOutput = SignalOutput::Dummy { len: nframes };
        let mut out1: SignalOutput = SignalOutput::Dummy { len: nframes };
        let mut out2: SignalOutput = SignalOutput::Dummy { len: nframes };

        {
            let mut i0 = SignalOutput::Vector(&mut internal0);
            let ins = [&in2, &zero];
            let mut outs = [&mut i0, &mut out2, &mut out1];
            self.blah0.perform(nframes, &ins, &mut outs);
        }
        {
            let i0 = SignalInput::Vector(&internal0);
            let ins = [&i0, &i0];
            let mut outs = [
                &mut out2,
                &mut SignalOutput::Dummy { len: nframes },
                &mut out1,
            ];
            self.blah1.perform(nframes, &ins, &mut outs);
        }
    }
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
warning: unused variable: `in3`
  --> src/lib.rs:44:13
   |
44 |         let in3: SignalInput = SignalInput::Single {
   |             ^^^ help: if this is intentional, prefix it with an underscore: `_in3`
   |
   = note: `#[warn(unused_variables)]` on by default

error[E0502]: cannot borrow `internal0` as immutable because it is also borrowed as mutable
  --> src/lib.rs:64:42
   |
58 |             let mut i0 = SignalOutput::Vector(&mut internal0);
   |                                               -------------- mutable borrow occurs here
...
64 |             let i0 = SignalInput::Vector(&internal0);
   |                                          ^^^^^^^^^^ immutable borrow occurs here
...
67 |                 &mut out2,
   |                 --------- mutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
warning: `playground` (lib) generated 1 warning
error: could not compile `playground` due to previous error; 1 warning emitted

The &mut [&mut SignalOutput<'_>; 3] type allows, at least in theory, you to swap around the SignalOutputs. Hence after the call to

self.blah0.perform(nframes, &ins, &mut outs);

in principle, the targets of the references in outs

let mut outs = [&mut i0, &mut out2, &mut out1];

could have been swapped around and

SignalOutput::Vector(&mut internal0);

could have ended up in out2 or out1 on the scope further up.

Hence internal0 must be conservatively considered borrowed as long as out2 is still used which is why the "later used here" for the borrow of internal0 points to a usage of out2.

In fact, the calls to .perform are not really necessary as this analysis of the compiler (which can be important as explained above, as these are practical safety concerns of how long references could have been used) is powered by the fact that the slice/array simply requires all elements, and hence the lifetimes of references contained in the array elements, to be the same. So the act of creating the outs array already relates the lifetime of the &mut internal0 borrow to the lifetimes in the types of out1 and out2.

6 Likes

Lifetimes aren't limited to the scope they're declared in. As a trivial example:

static I: i32 = 0;
fn foo() {
    let _: &'static i32 = &I;
}

Or in this reduced version, the borrow of internal0 is still active in the second block for the reasons @steffahn explained.

Borrow checking involves solving lifetime constraints, and the inferred constraints (or explicit annotations) can force a lifetime created in a block to be alive outside of the block. Blocks mainly enter into the equation by defining when values go out of scope and when destructors may run, as you can't create dangling references and non-trivial destructors count as a use of the value being dropped.

The inferred lifetime of a reference in particular is not effected by where the reference value goes out of scope (implicitly dropping), and cannot be shortened by explicitly dropping the reference either. (All that can do is extend the lifetime by adding a use of the reference, as explicit drops are uses whereas implicit drops, like going out of scope, are no-ops for references.)

2 Likes

Interesting, thanks for the insights @steffahn and @quinedot ! I still need to study that a little bit to fully grok it but your info helped me get to a version that does build, removing the arrays from the BlahSignalObject:::perform call. There is something I liked about the arrays, it lets me create a bunch of different types of "signal objects" that have the same function signature, besides the length of their inputs and outputs. I wonder, is there another way?

In case that fits your real code, too, you could consider a re-borrowing style method on SignalOutput and then pass a newly created array of possibly-shorter-lived re-borrows of the originals. Fancy terminology, I know… :see_no_evil: … here’s the API I mean:

impl SignalOutput<'_> {
    fn reborrow(&mut self) -> SignalOutput<'_> {
        todo!()
    }
}

This takes a &'a mut SignalOutput<'b> and turns it into SignalOutput<'a>.

On that note, SignalInput can also be made duplicatable, even easier, as it would support Copy. Then perform could take

impl BlahSignalObject {
    fn perform(
        &mut self,
        _nframes: Index,
        _inputs: [SignalInput<'_>; 2],
        _outputs: [SignalOutput<'_>; 3],
    ) {
    }
}

And here’s the whole code including the call sites:

type Sample = f32;
type Index = usize;

#[derive(Copy, Clone)]
pub enum SignalInput<'a> {
    Single { val: Sample, len: Index },
    Vector(&'a [Sample]),
}
pub enum SignalOutput<'a> {
    Dummy { len: Index },
    Vector(&'a mut [Sample]),
}
impl SignalOutput<'_> {
    fn reborrow(&mut self) -> SignalOutput<'_> {
        use SignalOutput::*;
        match *self {
            Dummy { len } => Dummy { len },
            Vector(ref mut r) => Vector(r),
        }
    }
}

struct BlahSignalObject;

impl BlahSignalObject {
    fn perform(
        &mut self,
        _nframes: Index,
        _inputs: [SignalInput<'_>; 2],
        _outputs: [SignalOutput<'_>; 3],
    ) {
    }
}
struct Foo {
    blah0: BlahSignalObject,
    blah1: BlahSignalObject,
}

impl Foo {
    fn perform(&mut self, nframes: Index, _inputs: &[&[Sample]], _outputs: &mut [&mut [Sample]]) {
        let mut internal0: [Sample; 64] = [0.0; 64];
        let _in0: SignalInput<'_> = SignalInput::Single {
            val: 0.0,
            len: nframes,
        };
        let _in1: SignalInput<'_> = SignalInput::Single {
            val: 0.0,
            len: nframes,
        };
        let in2: SignalInput<'_> = SignalInput::Single {
            val: 0.0,
            len: nframes,
        };
        let in3: SignalInput<'_> = SignalInput::Single {
            val: 0.0,
            len: nframes,
        };
        let zero: SignalInput<'_> = SignalInput::Single {
            val: 0.0,
            len: nframes,
        };

        let mut _out0: SignalOutput<'_> = SignalOutput::Dummy { len: nframes };
        let mut out1: SignalOutput<'_> = SignalOutput::Dummy { len: nframes };
        let mut out2: SignalOutput<'_> = SignalOutput::Dummy { len: nframes };

        {
            let i0 = SignalOutput::Vector(&mut internal0);
            let ins = [in2, zero];
            let outs = [i0, out2.reborrow(), out1.reborrow()];
            self.blah0.perform(nframes, ins, outs);
        }
        {
            let i0 = SignalInput::Vector(&internal0);
            let ins = [i0, i0];
            let outs = [
                out2.reborrow(),
                SignalOutput::Dummy { len: nframes },
                out1.reborrow(),
            ];
            self.blah1.perform(nframes, ins, outs);
        }
    }
}

Rust Playground

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.