-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Enable nested method calls #2025
Conversation
I can remember struggling with the |
text/0000-nested-method-calls.md
Outdated
### A broader user of two-phase borrows | ||
|
||
The initial proposal for two-phased borrows (made in | ||
[this blog post][]) was more expansive. In particular, it aimed to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing blog post link?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably this one.
From the internals thread, I think all two-phase borrows can be desguared into |
@Ericson2314 ah thanks for bringing that up. There are some corner cases that probably merit discussion in the RFC text that I think are interesting (places where my text "a reservation acts like a shared borrow" is probably worth clarifying). For example, if you are using shared borrows, there are cases where you would be able to do reassignments that you cannot with a mutable borrow. In particular, when you borrow the referent of a shared reference: let mut x: &i32 = &22;
let y: &i32 = &*x;
x = &44; // allowed even though `*x` was borrowed above We don't at present allow similar assignments for Anyway, my key point is that if we allowed such assignments for let mut x: &mut i32 = &mut 22;
x.some_method({ x = something_else; ... }) which might desugar to:
|
text/0000-nested-method-calls.md
Outdated
|
||
As discussed earlier, this RFC itself only introduces these two-phase | ||
borrows in a limited way. Specifically, we extend the MIR with a new | ||
kind of borrow (written `mut2`, for two-phase), and we generate those |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bikeshed: you mention several times, that the borrow means the value is reserved, why not mirror this and call it a &reserve
for simplicity's sake? Otherwise the words "reserved" and "two-phase" will end up being used interchangeably which probably won't help teaching the concept.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I debated about this. I thought it was strange, because &reserve foo
suggested that it only reserved foo
(rather than later mutating it), but perhaps it's clearer to say that it produces a reserved reference that (upon first use) is "upgraded" to a regular mutable reference.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
&will_mut foo
?
text/0000-nested-method-calls.md
Outdated
as the `v[0].push_str(&format!("{}", v.len()));` example. In that | ||
case, a simple desugaring can be used to show why the compiler | ||
rejects this code -- in particular, a comparison with the errorneous | ||
examples may be helpful. A keen observer may not the contrast with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: note
text/0000-nested-method-calls.md
Outdated
(this is because `vec.push()`, where `vec: &mut Vec<i32>`, is | ||
considered a *borrow* of `vec`, not a move). This "auto-mut-ref" will | ||
be changed from an `&mut` to an `&mut2`. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's not entirely correct:
fn main() {
let v = &mut vec![];
v.push(0);
v.push(1);
}
ATM we do not emit a reborrow:
_1 = &mut _2; // _1 is `v`
StorageLive(_4);
_4 = _1;
_3 = const <std::vec::Vec<T>>::push(_4, const 0i32) -> [return: bb4, unwind: bb3];
That is a bug, however, because if we don't emit a reborrow the code will not borrow-check (because of the multiple use of v
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@arielb1 actually, we do emit it, but we optimize it out later (post borrowck).
text/0000-nested-method-calls.md
Outdated
|
||
```rust | ||
v[0].push_str(&format!("{}", v.len())); | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given the current signature for IndexMut
, this case isn't so obviously harmless - IndexMut
for something like a wrapper of RefCell<Vec<T>>
can e.g. call RefCell::get_mut
, which can then be invalidated by someone calling .borrow_mut().push(..)
from an immutable borrow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed. I meant that it's "obviously" harmless in the sense that we know what those routines do (I can add a note to this effect).
text/0000-nested-method-calls.md
Outdated
straightforward way. But if we adopted discontinuous regions, that | ||
would require making Ralf's system more expressive. This is not | ||
necessarily an argument against doing it, but it does show that it | ||
makes the Rust system qualitatively more complex to reason about. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The two-phase borrow proposal that I describe here should be able to fit into that system in a fairly straightforward way.
I like your optimism here. ;)
This discussion is very interesting to me because modelling the reservation phase as a shared reborrow of the @nikomatsakis, does your comment say that this is or is not a valid way to think about two-phase borrows? You brought up an example, but it seems to me that (assuming we permitted the kind of overwriting of mutably borrowed variables you mention), the example should desugar just fine? I feel like this desugaring also gives a nicer explanation to why programs such as the one you use to demonstrate why the new check is needed should be rejected. (Yay for recursive grammars in natural languages. ;) Specifically, the lifetime of the borrow Another reason I like this is that I found it very useful in my formal model to inherently link sharing and lifetimes -- in the sense that when a variable of a type is shared (i.e., when it is accessible through a shared borrow), then it is always shared for some particular lifetime. This approach has gotten us really far, and even properly handles subtleties like I hope this makes any sense... I want to think more about this and see whether I can come up with a "formal definition" of this desugaring so that we can see whether the set of accepted programs differs. |
All right so here's a proposal for handling this with "shared reborrow of a mutable borrow". I think this is effectively equivalent to doing "old-style" borrow checking after some source-to-source translation, which I will also try to describe. Either way, this proposal involves non-lexical lifetimes -- it doesn't need the full machinery, but it involves lifetimes that are not scopes or expressions. The idea is for
(This looks somewhat like the When borrowing from When accessing a variable of type Effectively, the difference between this model and the RFC seems to be that the "reserved region" is explicit in my model, and merely checked by borrowck. In the RFC, on the other hand, the region is implicit and inferred by borrowck. So, I promised a source-to-source translation. I think it could go somewhat as follows:
|
Yes, that reborrow was what I was thinking of. @nikomatsakis, I was also confused but gathered you were thinking of a different desugaring or something. |
text/0000-nested-method-calls.md
Outdated
the following code would have type-checked, whereas it would not today | ||
or under this RFC: | ||
|
||
[a blog post]: http://smallcultfollowing.com/babysteps/blog/2017/03/01/nested-method-calls-via-two-phase-borrowing/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/a blog/this blog/
https://github.com/rust-lang/rfcs/pull/2025/files#r121198786
What about interior mutability? |
I think these subtleties are already encountered on a regular basis by people, and I don't think that making method calls special simplifies the mental model at all. In particular, it is common to think of functions as something that could be inlined into the caller at the point of the call, and to think of inlining as literally doing that inlining as a source-to-source transformation, instead of as a transformation of an intermediate form. In other words, I think people expect that any function call should be able to be rewritten as copy-pasta of the function's source code and so there's nothing special about functions. As an example of this idea, I think people expect all four of these to work and do the same thing: fn len_then_push(tmp0: &mut Vec<usize>) {
let tmp1 = tmp0.len(); // Used as shared.
Vec::push(tmp0, tmp1); // Used as mutable.
}
fn main() {
// Works fine today.
let mut vec = vec![0];
len_then_push(&mut vec);
// Works fine today as only one reference to `vec` is used.
{
let tmp0 = &mut vec; // `vec` is mutably borrowed.
let tmp1 = tmp0.len(); // Used as shared.
Vec::push(tmp0, tmp1); // Used as mutable.
}
// Now: Fails to compile. Future: OK.
vec.push(vec.len());
// Now: Fails to compile. Future: OK.
{
let tmp0 = &mut vec; // Now: Mutable borrow. Future: reserve `vec`.
let tmp1 = vec.len(); // shared borrow of vec. Now: fails. Future: OK.
Vec::push(tmp0, tmp1); // Future: mutable borrow of `vec` is activated.
}
} So, I do think it is fine to do some special case for function calls today, but I think there should be a more concrete plan to generalize the mechanism so that it isn't specific to function calls forever. |
I frequently notice situations where a call to |
@briansmith The problem here is that you are not just doing inlining of a function call. You are changing which variable is used for a particular function call. So you are asking for much more here than just "there should be nothing special about a function call". Personally, I think it is perfectly reasonable for this code not to compile: // Now: Fails to compile. Future: OK.
{
let tmp0 = &mut vec; // Now: Mutable borrow. Future: reserve `vec`.
let tmp1 = vec.len(); // shared borrow of vec. Now: fails. Future: OK.
Vec::push(tmp0, tmp1); // Future: mutable borrow of `vec` is activated.
} After all, there's overlapping shared and mutable borrows here! And if you write the code like this, it is trivial to just swap the two |
Just curious, how does that compare to use std::ops::AddAssign;
#[derive(Debug)]
struct MyVec<T>(Vec<T>);
impl<T> AddAssign<T> for MyVec<T> {
fn add_assign(&mut self, elem: T) {
self.0.push(elem);
}
}
fn main() {
let mut vec = MyVec(Vec::new());
vec += 3;
vec += vec.0.len();
println!("{:?}", vec);
} |
Yes, I like this formulation. As far as whether "borrowck" infers the lifetime or what, I suppose it's all a matter of perspective. That is, one way that I had imagined implementing this in my head was roughly like so:
So you can view just the second part of it as "borrowck-proper". However, I hadn't considered the idea of limiting shared borrows that occur with a I can rewrite the RFC to describe your alternate description (or maybe add it as an alternative way of looking at things).
This translation seems reasonable. I was assuming that you and @Ericson2314 meant a translation where you did not rewrite all accesses to
I think it's plausible to imagine something along these lines (though it'd be quite complex), but in that scenario, you would want to account for those cases where evaluating the arguments could mutate the original path ( |
It ought to, yes. UPDATE: Except that you wouldn't have to explicitly drop, I wouldn't think. |
Yeah, to be honest, I think that's a bug in the current borrow checker. I was noticing that earlier when preparing this RFC and wanting to dig into it. This RFC would hopefully make actual code that does this still type-check. |
Looks very much like a bug. Replacing "+=" by EDIT: Yep, it's a bug. A soundness bug: rust-lang/rust#27868 (comment) |
Just to be clear, that fragment is from the RFC's description of one of the proposed extensions, with only the comments modified. Also, I do agree the current behavior is reasonable. However, I think it would be closer to ideal if that kind of code fragment compiled, as it is unambiguously safe. My point is that I disagree with the idea that a more powerful borrow checker should be rejected because of concerns about the difficulty of the "mental model." In this case, it isn't even clear that the more powerful proposal would even be more mentally taxing, as what it additionally allows isn't significantly different from what is already allowed. |
I definitely would like to see a more powerful borrow checker, in many respects, but I still think it should be predictable. I'm concerned that the analysis I am proposing here, if scaled up, would not be sufficiently predictable for people. For example, if I write: let x = &mut v;
let y = Vec::len(&v);
use(x, y); and I explain to you that the let x;
x = &mut v;
let y = Vec::len(&v);
use(x, y); and now you get an error (because of how MIR desugaring works; it would introduce a temporary, and hence let x = &mut v;
let t = x; // is this a use?
let y = Vec::len(&v);
use(t, y); and so on. In contrast, a "borrowing for the future" model (or |
Right, I'd see it as a bug in the translation to MIR, or more likely a limitation of the borrow checker's interpretation of copies of mutable references that is best removed.
That code is clearly OK insofar as the reasons for which we have borrow checking are concerned. (How to formulate a valid borrow checking rule that allows it isn't clear, but we can see that the code is safe, so there's probably a way to formulate a rule that allows it.) What matters is whether or not a data race or reference invalidation occurred or whether references escaped the current scope such that we have to assume that races/invalidations would occur since we have to limit ourselves to just local analysis (AFAICT).
Yep. In my origin message up above, I did say that I thought it was fine to do something more limited first. I'm just saying that the assertion that the other alternatives are too complicated for users to understand seems unwarranted, and so such alternatives shouldn't be dismissed for that reason. |
(Just want to throw in my support for @RalfJung's alternative formulation that uses two distinct lifetimes. But that's probably because I prefer how I see that factoring the compiler changes involved here.) |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
I'm a beginner in Rust, so please correct me if I'm wrong, but my understanding is that currently methods are just syntactic sugar for free functions. That is, this struct Foo {
frobbed: bool,
}
fn frob(foo: &mut Foo) -> i32 { foo.frobbed = true; 42 } is equivalent to struct Foo {
frobbed: bool,
}
impl Foo {
fn frob(&mut self) -> i32 { self.frobbed = true; 42 }
} The only difference is the way it is being called. So these should be equivalent: let foo = Foo { frobbed: false };
foo.some_method(foo.frob()); // first call
foo.some_method(frob(&mut foo)); // second call This RFC makes the first call magically work in some cases where the second wouldn't. If everything that I have said so far is correct, I find that I dislike this RFC. It introduces a special case that is bound to trip someone up. Suppose someone new to rust is experimenting with a design, and decides that something would work better as a free function. Suddenly they find that something that used to work breaks for no apparent reason. They google stuff, and find out that it should not have worked in the first place. If they are lucky, they find this RFC and understand what's going on. If they're unlucky, they just assume that methods are somehow special (and with this RFC, they really are) and come out with a flawed understanding of Rust. So in my opinion, this RFC runs counter to the goal of lowering Rust's learning curve. I think that either both the first call and the second call should be made to work using some more general solution, or both should continue to fail. This baby step seems to leave the language in a non-self-consistent state until further steps are taken that make methods not magical again. |
@VictorGavrish Your fear is fortunately unfounded: first of all, neither of the examples there wouldn't compile, because |
@nikomatsakis Anything based on #2025 (comment) that should go in this?
I'd like to think the such FIFO-respecting (FIFO along control flow paths, not surface syntax scopes) events and lifetimes are always in correspondence---that's what I did in my stateful MIR at least. And now we have |
The final comment period is now complete. |
Huzzah! This RFC has been merged! |
What’s the reason @VictorGavrish’s example with nested mutable calls isn’t included in NLL? This came up in https://users.rust-lang.org/t/possibly-stupid-question-about-borrows-created-in-function-call-arguments/. Is there a fundamental problem with this or just a temporary limitation? |
Enable "nested method calls" where the outer call is an
&mut self
borrow, such asvec.push(vec.len())
(wherevec: Vec<usize>
). This is done by extending MIR with the concept of a two-phase borrow; in this model, select&mut
borrows are modified so that they begin with a "reservation" phase and can later be "activated" into a full mutable borrow. During the reservation phase, reads and shared borrows of the borrowed data are permitted (but not mutation), as long as they are confined to the reservation period. Once the mutable borrow is activated, it acts like an ordinary mutable borrow.Two-phase borrows in this RFC are only used when desugaring method calls; this is intended as a conservative step. In the future, if desired, the scheme could be extended to other syntactic forms, or else subsumed as part of non-lexical lifetimes or some other generalization of the lifetime system.
Rendered.Rendered text in rfcs repo