Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upRFC: Enable nested method calls #2025
Conversation
nikomatsakis
added
the
T-lang
label
Jun 9, 2017
This comment has been minimized.
This comment has been minimized.
|
I can remember struggling with the |
Ixrec
reviewed
Jun 9, 2017
| ### 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 |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
From the internals thread, I think all two-phase borrows can be desguared into |
This comment has been minimized.
This comment has been minimized.
|
@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 aboveWe 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:
|
oli-obk
reviewed
Jun 10, 2017
|
|
||
| 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 |
This comment has been minimized.
This comment has been minimized.
oli-obk
Jun 10, 2017
•
Contributor
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.
This comment has been minimized.
This comment has been minimized.
nikomatsakis
Jun 12, 2017
Author
Contributor
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.
This comment has been minimized.
This comment has been minimized.
| 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 |
This comment has been minimized.
This comment has been minimized.
arielb1
reviewed
Jun 11, 2017
| (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`. | ||
|
|
This comment has been minimized.
This comment has been minimized.
arielb1
Jun 11, 2017
Contributor
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).
This comment has been minimized.
This comment has been minimized.
nikomatsakis
Jun 12, 2017
Author
Contributor
@arielb1 actually, we do emit it, but we optimize it out later (post borrowck).
arielb1
reviewed
Jun 11, 2017
|
|
||
| ```rust | ||
| v[0].push_str(&format!("{}", v.len())); | ||
| ``` |
This comment has been minimized.
This comment has been minimized.
arielb1
Jun 11, 2017
Contributor
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.
This comment has been minimized.
This comment has been minimized.
nikomatsakis
Jun 12, 2017
Author
Contributor
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).
RalfJung
reviewed
Jun 13, 2017
| 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. |
This comment has been minimized.
This comment has been minimized.
RalfJung
Jun 13, 2017
•
Member
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 comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
|
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:
|
This comment has been minimized.
This comment has been minimized.
|
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. |
frewsxcv
reviewed
Jun 14, 2017
| 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/ |
This comment has been minimized.
This comment has been minimized.
frewsxcv
Jun 14, 2017
Member
s/a blog/this blog/ https://github.com/rust-lang/rfcs/pull/2025/files#r121198786
This comment has been minimized.
This comment has been minimized.
antoyo
commented
Jun 14, 2017
|
What about interior mutability? |
This comment has been minimized.
This comment has been minimized.
briansmith
commented
Jun 15, 2017
•
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. |
This comment has been minimized.
This comment has been minimized.
burdges
commented
Jun 15, 2017
|
I frequently notice situations where a call to |
This comment has been minimized.
This comment has been minimized.
|
@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 |
This comment has been minimized.
This comment has been minimized.
|
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);
} |
This comment has been minimized.
This comment has been minimized.
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 ( |
This comment has been minimized.
This comment has been minimized.
It ought to, yes. UPDATE: Except that you wouldn't have to explicitly drop, I wouldn't think. |
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
Looks very much like a bug. Replacing "+=" by EDIT: Yep, it's a bug. A soundness bug: rust-lang/rust#27868 (comment) |
RalfJung
referenced this pull request
Jun 15, 2017
Open
Inconsistent evaluation order for assignment operations #27868
nikomatsakis
self-assigned this
Jun 15, 2017
This comment has been minimized.
This comment has been minimized.
briansmith
commented
Jun 15, 2017
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. |
This comment has been minimized.
This comment has been minimized.
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 |
This comment has been minimized.
This comment has been minimized.
briansmith
commented
Jun 16, 2017
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. |
rfcbot
added
the
proposed-final-comment-period
label
Jun 29, 2017
This comment has been minimized.
This comment has been minimized.
|
(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 comment has been minimized.
This comment has been minimized.
rfcbot
commented
Jul 18, 2017
|
|
rfcbot
added
final-comment-period
and removed
proposed-final-comment-period
labels
Jul 18, 2017
This comment has been minimized.
This comment has been minimized.
VictorGavrish
commented
Jul 19, 2017
•
|
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 callThis 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. |
This comment has been minimized.
This comment has been minimized.
golddranks
commented
Jul 19, 2017
|
@VictorGavrish Your fear is fortunately unfounded: first of all, neither of the examples there wouldn't compile, because |
This comment has been minimized.
This comment has been minimized.
|
@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 |
This comment has been minimized.
This comment has been minimized.
rfcbot
commented
Jul 28, 2017
|
The final comment period is now complete. |
steveklabnik
referenced this pull request
Jul 31, 2017
Closed
Explain when temporary 'anonymous' values will be dropped #850
aturon
referenced this pull request
Aug 3, 2017
Open
Language ergonomic/learnability improvements #17
withoutboats
added
the
Ergonomics Initiative
label
Aug 14, 2017
nikomatsakis
referenced this pull request
Aug 24, 2017
Open
give this current proposal a better name #43
aturon
referenced this pull request
Aug 26, 2017
Closed
Tracking issue for RFC 2025: Enabled nested method calls #44100
This comment has been minimized.
This comment has been minimized.
|
Huzzah! This RFC has been merged! |
aturon
merged commit fdf35c1
into
rust-lang:master
Aug 26, 2017
bstrie
referenced this pull request
Sep 14, 2017
Open
borrowed referent of a `&T` sometimes incorrectly allowed #38899
This comment has been minimized.
This comment has been minimized.
vitalyd
commented
Dec 29, 2017
|
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? |
nikomatsakis commentedJun 9, 2017
•
edited by pnkfelix
Enable "nested method calls" where the outer call is an
&mut selfborrow, 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&mutborrows 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