Useless failure of borrow check with float in struct


#1

This seems like a bad outcome of borrow checks:

Struct S {
x: f64,
}

fn f(s: &mut S, y: f64) {
s.x = -s.x;
s.x == y
}

fn main() {
let mut s = S { x: 0.01 };
println!("{}", f(&mut s, s.x));
}

error[E0503]: cannot use s.x because it was mutably borrowed
–> main.rs:38:30
|
38 | println!("{}", f(&mut s, s.x));
| - ^^^ use of borrowed s
| |
| borrow of s occurs here

But s.x is passed by value—there is no possibility of aliasing in
this case—or is there?

The obvious workaround:

let xx = s.x;
…f(&mut s, xx)…

is a bit lame. Is there a way of moving x out of s as an expression,
so at least I don’t have to add a line? I suspect not, since any
expression involving s is going to have to borrow it. Ouch!


#2

Yeah, once you borrow s, which occurs first in the arg evaluation order, you cannot access s until the borrow ends; that borrow, however, is to a temporary that you cannot access. Moreover, if you had an explicit binding that held &mut S, passing it to f() would cause a reborrow, and again you cannot access the x field.

If you rearrange the arg order of f(), like fn f(y: f64, s: &mut S), then passing in that order will be fine in your original example (with the args reversed, of course).


#3

Is it possible that this is actually a bug? There are two distincts phases here: argument evaluation, and call. The borrow checker should only verify that the borrow rules apply for the call. The arguments can be evaluated separately, and that should not cause conflicts.


#4

Arguments can be expression blocks themselves, with arbitrary code inside the block. I don’t think you want to special case argument borrowck for blocks vs not.


#5

You’ve hit the nail on the head with that choice of terminology. There’s actually an upcoming feature called “two-phase borrows” that does something similar:

There’s even a prototype already in nightly as part of NLL…

…But for now, the feature is somewhat arbitrarily limited to method calls that take &mut self; it isn’t applied for general &mut arguments. I’m not sure what the reasoning for this is.


#6

In what way do blocks make a difference? The question is whether it is possible to compute the “borrow set” of the result of an expression, rather than what needs to be borrowed temporarily for the expression evaluation.


#7

comex, thank you for the pointer. Yes, that RFC seems overly complicated and unnecessarily restrictive. There may be a simpler, more general rule, which includes the case at hand. I would imagine that this has been thoroughly discussed, and I am hoping that my reasoning (i.e. the correct one :wink: ) will prevail.


#8

This is a big mistake in e.g. C that rust does not have as arguments are always assessed left to right. One arguments evaluation can influence the other.

Personally see the reference creation as part of the argument evaluation (even if trivial) rather than as you would like just part of the call.

I agree it’s a bit annoying. Using a helper function/macro is one way to avoid the extra line if your doing such call many times.


#9

You would have to consider whether code such as the following should be allowed:

let mut v = vec![1,2,3];
f({ v.clear(); &mut v}, v[0]); // panics if allowed

You would also have to be super careful and principled to ensure you don’t allow unsoundness to slip through. It requires quite a bit of thinking to find the right approach here. I’m sure the complexity of the compiler would go up significantly. And that’s without taking into account what type of stylistic code you’d want to allow (such as the above).

You also have to consider what the alternative is to work around this issue. In your case, it’s downright trivial and we’re just discussing this for intellectual curiosity sake.

Finally, I urge you to write up an RFC that details how this should work if you feel you have a design in mind.


#10

Sorry but I don’t see how this example applies to the original issue. What if you have this:

f({ v.clear(); &v}, v[0]);

This is allowed now (I think), but fails in exactly the same way.

What I have in mind is the situation, as in my original example, is a situation in which the compiler behavior differs between

    let x = <expr>;
    f(..., x, ...)

and

    f(..., <expr>, ...)

But this should be just a simple transformation, in which the compiler rewrites the call by first evaluating its parameters into temporary variables, then passing those. If that’s not possible (not even internally), this language is weirder than I thought.

Well not exactly. I wasted some time trying to understand if there was a good reason for this—and I still don’t see one.

I’d be happy to do that, but it seems that it’s already been discussed.


#11

Yeah, it’s allowed with an immutable borrow. Admittedly this isn’t a stellar example, but the gist is the mutable borrow “gates” further side-effects as evaluation moves to the right. Allowing more side-effects here seems like moving in the wrong direction, stylistically at least.

The compiler behavior differs, but I think that’s expected given the sequence is different? The expr placeholder is important in Rust because it can influence how code after it and x behave. In your example, it is immaterial. But you’d need to somehow generalize that “immaterialness” or else we’ll continue having band-aid solutions where some code shape works and other doesn’t. This is what I mean by a principled approach.

I suspect if it was “a simple transformation” it’d be done already :slight_smile:. So it might be possible, and perhaps we’ll get there some day, but I don’t think it’s as simple as you make it sound.

Perhaps @pnkfelix can share some of his thoughts on this - he’d be the closest thing to authority on this matter.


#12

You might also be interested in this issue report, which was created just hours ago as of this post (maybe even because of this thread?):