Add CodeExtent::Remainder
variant; pre-req for new scoping/drop rules. by pnkfelix · Pull Request #21657 · rust-lang/rust (original) (raw)
Add CodeExtent::Remainder
variant; pre-req for new scoping/drop rules.
This new enum variant introduces finer-grain code extents, i.e. we now track that a binding lives only for a suffix of a block, and (importantly) will be dropped when it goes out of scope before the bindings that occurred earlier in the block.
Both of these notions are neatly captured by marking the block (and each suffix) as an enclosing scope of the next suffix beneath it.
This is work that is part of the foundation for issue #8861.
(It actually has been seen in earlier posted pull requests, in particular #21022; I have just factored it out into its own PR to ease my own near-future rebasing, and also get people used to the new rules.)
These finer grained scopes do mean that some code is newly rejected by rustc
; for example:
let mut map : HashMap<u8, &u8> = HashMap::new(); let tmp = Box::new(2); map.insert(43, &*tmp);
This will now fail to compile with a message that *tmp
does not live long enough, because the scope of tmp
is now strictly smaller than
that of map
, and the use of &u8
in map's type requires that the borrowed references are all to data that live at least as long as the map.
The usual fix for a case like this is to move the binding for tmp
up above that of map
; note that you can still leave the initialization in the original spot, like so:
let tmp; let mut map : HashMap<u8, &u8> = HashMap::new(); tmp = box 2; map.insert(43, &*tmp);
Similarly, one can encounter an analogous situation with Vec
: one would need to rewrite:
let mut vec = Vec::new(); let tmp = 'c'; vec.push(&tmp);
as:
let tmp; let mut vec = Vec::new(); tmp = 'c'; vec.push(&tmp);
In some corner cases, it does not suffice to reorder the bindings; in particular, when the types for both bindings need to reflect exactly the same code extent, and a parent/child relationship between them does not work.
In pnkfelix's experience this has arisen most often when mixing uses of cyclic data structures while also allowing a lifetime parameter 'a
to flow into a type parameter context where the type is invariant with respect to the type parameter. An important instance of this is arena::TypedArena<T>
, which is invariant with respect to T
.
The reason that variance is relevant is this: if TypedArena
were covariant with respect to its type parameter, then we could assign it the longer lifetime when it is initialized, and then convert it to a subtype (via covariance) with a shorter lifetime when necessary. But TypedArena
is invariant with respect to its type parameter, and thus if S
is a subtype of T
(in particular, if S
has a lifetime parameter that is shorter than that of T
), then a TypedArena<S>
is unrelated to TypedArena<T>
). Note that the fact that TypedArena<T>
is invariant with respect to T
comes straight from the API of that trait; nikomatsakis and pnkfelix has discussed "arena-like" API's that would somehow be covariant with respect to T
, but have not actually managed to develop such an API that is actually expressible in pure Rust without further linguistic extensions/changes.
Concretely, consider code like this:
struct Node<'a> { sibling: Option<&'a Node<'a>> }
struct Context<'a> {
// because of this field, Context<'a>
is invariant with respect to 'a
.
arena: &'a TypedArena<Node<'a>>,
...
}
fn new_ctxt<'a>(arena: &'a TypedArena<Node<'a>>) -> Context<'a> { ... }
fn use_ctxt<'a>(fcx: &'a Context<'a>) { ... }
let arena = TypedArena::new(); let ctxt = new_ctxt(&arena);
use_ctxt(&ctxt);
In these situations, if you try to introduce two bindings via two distinct let
statements, each is (with this commit) assigned a distinct extent, and the region inference system cannot find a single region to assign to the lifetime 'a
that works for both of the bindings. So you get an error that ctxt
does not live long enough; but moving its binding up above that of arena
just shifts the error so now the compiler complains that arena
does not live long enough.
- SO: What to do? The easiest fix in this case is to ensure that the two bindings do get assigned the same static extent, by stuffing both
bindings into the same let statement, like so:
let (arena, ctxt): (TypedArena, Context); arena = TypedArena::new(); ctxt = new_ctxt(&arena);
use_ctxt(&ctxt);
Due to the new code restrictions outlined above, this is a ...
[breaking-change]