vstack.add(ctx, self.voice_control(ctx));
// error: cannot borrow `*ctx` as mutable more than once at a time
It’s building some UI widgets. The ctx is &mut WindowCtx. It doesn’t compile, and I understand the error message. But … why? In this case voice_control builds a widget and returns Rc<dyn Widget>. It has no lifetime parameter, so shouldn’t it be able to see that the borrow is done once the argument expression (self.voice_control(ctx)) has been evaluated? It seems like it should be able to compile that one line, as if I’d written:
let temp = self.voice_control(ctx);
vstack.add(ctx, temp);
This is because of the way the code is parsed from left to right. By the time the compiler gets to the call site for vstack.add, the call to self.voice_control hasn't been evaluated yet.
This is why changing the order makes it work.
It's unfortunate, but it's one of the current limitations of the static analysis capabilites of the compiler.
(* I guessed what kinds of borrows these are — I would need to see the variable types and the method signatures to be sure.)
The first exclusive reborrow of ctx, that is to be passed to add(), happens beforeself.voice_control(ctx) is evaluated, so self.voice_control(ctx) doesn’t have ctx available to reborrow. In principle, Rust could delay the exclusive borrow of ctx using the already existing “two-phase borrows” feature, but that feature is currently limited to method receivers only, not other function arguments.
If this is a recurring problem in your code, consider redefining the add method so that it takes ctx as the second parameter.
I’m kind of surprised at that. Parsing obviously happens left-to-right, but the borrowing happening like that seems like it could be improved. Probably I’m missing something.
But this borrowing issue motivates the opposite order. I’ll probably just leave it as the first parameter and write the extra lines for the intermediate variables.
I also created a convenience function add_to on my generic widget type.
The exact order of execution is probably arbitrary / a historical accident / however you want to think of it. But the order is observable,[1] so it's a breaking change to switch it up now.
(An alternative for some cases would be to make the borrow checker "smarter", but I don't think this applies to your example.)
e.g. you can replace your two arguments with blocks that do many arbitrary things ↩︎
Why would it not? The evaluation of arguments happens before a function call. So in this case it should be done with one borrow - voice_control(ctx) - before it needs the second one - vstack.add(ctx, ...). ?
I'm assuming both add and voice_control need a &mut WindowCtx, and I was thinking in terms of two phased borrows. Those don't support exclusive uses of the "reserved" place, only shared.
receiver_tmp = &mut2 vstack; // reservation 1
arg_1_tmp = &mut2 *ctx; // reservation 2
// maybe more temps here but ultimately
arg_2 = voice_control(&mut *ctx);
// check here for no oustanding `*ctx` borrows or (other) reservations
// activate 2
// check here for no outstanding `vstack` borrows or (other) reservations
// activate 1
<_>::add(receiver_tmp, arg_1_tmp, arg_2)
In order for an argument to be eligible for these reservations, there would have to be some restriction, like perhaps it is a reborrow and nothing else.[1]
Incidentally, the two-phased borrows we got ended up being less targeted than originally intended. I can't tell you the exactly when it does or doesn't work as it has no spec and last I looked, wasn't even in the reference. And one of the motivations for tree borrows was that stacked borrows struggled to justify two-phased borrows. All that makes me hesitate when it comes to expanding two phase borrows.
But my own reservations aside, it could well be possible. Thank you for prompting me to think it through more.