-
Notifications
You must be signed in to change notification settings - Fork 17
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
[borrowck] handling of drops invalidating borrows #40
Comments
NOTE: moved to #42@nikomatsakis had also discovered the following interaction fn foo() {
let x = RefCell::new(42);
let ref_x: &'α RefCell<u32> = &x;
let inner_ref: Ref<'α, u32> = RefCell::borrow(ref_x);
// ^ for all you guys following me, `Ref` maintains a borrow to its parent
// `RefCell` that it clears during its very-not-blind destructor.
drop(inner_ref);
// (†)
RefCell::into_inner(x); // is this legal?
maybe_unwind();
// (‡)
} Here the question is how long
If we care about it, a simple dataflow analysis could discover that the drop at The drop at the unwind path looks like it might be a hard nut to crack - there's only 1 unwind path for the function, ending at the single |
I'm trying to figure out exactly what you are proposing. It seems like there are two independent things at play. The first is the handling of With respect to
But this has more to do with overloading |
fn example_box<'a>(mut v: Box<&'a mut u32>) -> &'a mut u32 {
&mut **v
}
fn main() {} |
That's a constraint on lifetimes, not on borrows. |
I think the confusing thing here is the difference between this issue and #42 - they are actually separate issues. The root cause here is that (both with NLL and lexical lifetimes) there are 2 ways operations feed into borrow checking (excluding "moveck", which has nothing to do with lifetimes):
This issue is about accesses, while #42 is about lifetime constraints. Because values can't be uninitialized while there are active borrows to part of them, dead drops being a no-op is uninteresting to borrowck - if the drop is dead, we already know that there can't be any borrows to it to be invalidated. |
OK, so I had this big response, but in the course of writing it, I think I realized what you are actually talking about. But first off, let me observe that I think this box example you gave is a bug (though not one I am surprised about). We are supposed to be treating I had kind of assumed we would "fix" this bug. But maybe that will break too much stuff in the wild, so I think now what you are saying is (a) presume that we do NOT fix this issue, but we keep box being "special" as it is today. In that case, we might compare your box example to this other example, which does not use a box: fn foo<'a>(x: &mut T) -> &mut T { &mut *x } In this case, the borrow of So, now the question is: if we tried to extend that same approach to the case where An interesting point. It would be nice if we had some rules that also sort of "justified" the drops of references. I agree this is somewhat distinct from "may dangle" -- though not completely unrelated. |
I still hope for N.B. The sequence of actions in #40 (comment) is
The key point from this story for today is that empty boxes need not be borrowed. |
So, @pnkfelix has been working on a writeup and prototype implementation of this idea that we had that we called "dangly paths", but I think it offers a promising solution to the problem here. Since he's going to do a more complete write-up, I'll try to just leave a few notes on the high-level concept. The basic idea is to use the struct Foo<T> { // T "may dangle"
data: (T, T)
}
struct Bar<'a> { // `'a` "may dangle"
data: &'a mut T
}
struct Baz<'a, T> { // T "may dangle", but not `'a`
data: &'a mut T
} Then the set of "dangly paths" for The intuition here is that if a lifetime parameter is declared may dangle, then any references with that lifetime could not be accessed by Drop, or else the declaration is wrong. Similarly for generic types. Now, the idea is that when the Drop executes, it is ok to have a borrow of the referent of some Anyway, there are some subtleties involving nested types and so forth, which is why @pnkfelix was going to do a more complete write-up, but hopefully that's enough to go on for the moment. @Ericson2314 Unless I'm missing something, I think that ideally we wouldn't need to bring But it's true that for these examples to work it does rely on the compiler treating |
Huh? When we return the (FWIW, |
That looks like a complicated and subtle feature that is only useful in an edge-case (structs with destructors, public borrowable fields, and The "magic dereference" of I think we want to wait until we better understand the |
But we don't return the |
@arielb1 wrote:
I agree that it is subtle and an edge case. I'm not sure it needs to be complicated. My main motivation for investigating it was because I wanted to understand how to fix the problem in a general way rather than making it solely ties to In the time since I starting looking at this, I have realized (as you point out), that supporting this feature for
I have finished a nll-based prototype that I'l have a PR up for soon. Its small (since nll is itself small). Maybe we can look at that as part of deciding whether to bother implementing something this general in |
I don't expect that to be so much of a problem -
It's also not that obvious that we won't have soundness problems when translating it to full Rust. |
I wrote:
That PR is here: nikomatsakis/borrowck#17 |
@arielb1 wrote:
I am inclined to agree with that. (in the long run we may replace |
Ah ok, my bad. Well, perhaps this side-steps the point of the thread too much, but if we do I imagine the long-term safe solution to |
Related, I think may-dangling doesn't solve the whole problem, in that we probably would want invalidated fields without any indirection being involved (say if rust-lang/rfcs#2061 is accepted): // somehow indicate some destructuring is OK
struct Thing<T>(uint, T);
// Somehow say we don't care about `Self::1`
impl<T> Drop for Thing<T> {
fn drop(&mut self) {
println!("{}". self.0);
}
}
fn sound<'long>(a_wrapper: Thing<Type>) {
let _move: Type;
{
let tmp = a_wrapper;
_move = tmp.1; // Know `_move` is initialized and `tmp.1` deinitialized here
} // `tmp` dropped here---want to run destructure cause `tmp.1`
// doesn't matter.
// `_move` must be valid, like today. (The error would be above.)
println!("_move; {:?}", borrow);
} This really isn't a drop-specific thing at all, but rather first-class partial borrows, which IIRC @nikomatsakis talked about earlier (maybe the break-from-block thread on whether or not hoisting the block into a function was always possible). |
Another thing to note is with " |
We need to make some call around this for parity with AST borrowck - AST borrowck allows this sort of thing. |
I'm not entirely sure about this one... with the For example, consider struct Baz<'a, T> { // T "may dangle", but not `'a`
data1: &'a mut T,
data2: &'a mut T,
}
unsafe impl<'a, #[may_dangle] T> Drop for Baz<'a, T> {
fn drop(&mut self) {
mem::swap(self.data1, self.data2); // Just moving data of type `T` around; safe according to current rules
}
} I must be missing something here...
Uh-oh, parametricity-based reasoning.^^ And this does not fall out of |
You (or @nikomatsakis's somewhat- description) are off-by-one-layer-of-indirection. The idea is that borrows of dereferences of dangly pointers remain valid after the destructor runs, not that borrows of dangly things are not invalidated. That's it, with If the type being destroyed is covariant, the justification doesn't really even need parametricity - rather, the compiler could coerce to type to a type where all the "dead" lifetimes are already over, and the destructor called at that type obviously should not access a dead reference. I'm less sure of how that would work with invariant types (e.g. associated types). |
Ah. Makes sense :) In the case of Given that |
I think I'm beginning to understand. Your example is a case where moving out of a struct could be legal even though it implements |
Thanks, it had been a while since I said the things I did so I needed a refresher too :).
Well, we could (as long as something was put back in) if it weren't for panicking :D. Let me go remember how the |
Destructors can invalidate borrows of their contents:
Today's rustc's handling of the situation does not provide us much guidance, as can be seen by this example compiling, running, and accessing invalid memory (that is rust-lang/rust#31567).
However, we can't have any
drop
invalidate all interior references. In the most trivial case, drops of "trivial drop" types, like references, don't even appear in MIR. And even if they appeared, having mutable references invalidate their contents when dropped would basically break all of Rust.Note that I don't think we have to worry about the interior of containers in the common case, at least until we implement some sort of
DerefPure
:The desugaring converts that into:
Here the lifetime constraint in
IndexMut
forces the borrow ofv
(fort0
) to live for'a
, which already causes a borrowck error with thestoragedead
.However, technically, we could have a container that "publicly" contains its contents, and which we know can't access its contents because of dropck:
I don't think that is something we have to strive to implement, at least until someone comes with up with a use-case. So I think a reasonable rule would be that a
drop
invalidates all contents that are reachable from a destructor. We already do this check today in order to check whether deinitialization/reinitialization would be valid.However, the question remains - do we want
Box
to have a destructor, or should it be treated like any other type?The text was updated successfully, but these errors were encountered: