Heads up: allowing recursive static variables


#1

I don’t think this needs an RFC, but I do think it should get some attention.

There is a PR which allows recursive references in statics, e.g. (from the PR):

static L1: List = List{prev: &L3, next: &L2, data: 1, head: true};
static L2: List = List{prev: &L1, next: &L3, data: 2, head: false};
static L3: List = List{prev: &L2, next: &L1, data: 3, head: false};

Seems like a win to me, anyone see any problems?


#2

Copy could be a bit counter-intuitive because the reference cycle will be implicitly broken but this looks safe (and useful).


#3

I initially thought there may be problems due to the cycle, and the fact that this would be the only way to make one with & (i.e. there may be code relying on the fact that & can’t be cyclic)…

But this isn’t right: it is already possible to make & cycles with TypedArena:

#![feature(rustc_private)]
extern crate arena;
use std::cell::Cell;

struct Foo<'a> {
    x: Cell<Option<&'a Foo<'a>>>
}

fn main() {
    let t = arena::TypedArena::new();
    let a = t.alloc(Foo { x: Cell::new(None) });
    
    a.x.set(Some(a));
}

However, I believe all such examples require interior mutability (atm?), so maybe this new case (which doesn’t require it) could still be problematic.

Thoughts?


#4

You don’t currently need TypedArena, you can just have variables with the same lifetime by declaring them in the same expression:

use std::cell::Cell;

struct Foo<'a> {
    r: Cell<Option<&'a Foo<'a>>>
}

fn main() {
    let (x, y);
    x = Foo { r: Cell::new(None) };
    y = Foo { r: Cell::new(None) };

    x.r.set(Some(&y));
    y.r.set(Some(&x));
}

My question is, is there any reason to limit this to static, rather than just equal lifetimes?

In my intuition, if you can do this with 'static, you should be able to do it with any variables with the same lifetime; the lifetimes are the same, as seen by the fact that the above example with Cell and Option works properly, and there is no observable point in which any one of them is uninitialized. But my intuition could be wrong.

struct Foo<'a> {
    r: &'a Foo<'a>
}

fn main() {
    let (x, y);
    
    (x, y) = (Foo { r: &y }, Foo { r: &x })
}

#5

Happy to be wrong, but my understanding is that “two variables with the same lifetime” is, strictly speaking, not possible. One must be dropped first.


#6

Each reference has lifetime 'a in the above. They really do have the same lifetime.

This doesn’t work if the types involved implement Drop; then you do have to have one lifetime outlive the other, because you do need one to be dropped before the other. That’s what the Drop-Check rule enforces.

But if Drop isn’t involved (or, as detailed in the RFC, even if a type that implements Drop is involved, but is not parameterized on the lifetime in question), then it really is possible to have two objects with the same lifetime which refer to each other.

Right now, you can’t actually get them into the state of referring to each other without mutability; but it doesn’t seem like that should necessarily be the case, especially if you’re going to relax that rule for 'static references.


#7

You are correct, however the current regions are too approximate to capture this in all cases. This can actually be quite unfortunate, because it means that dropck sometimes flags errors where none are needed (particularly around temporaries). Unfortunately I think there are also cases where it allows things to type-check that wouldn’t necessarily otherwise if we were more precise (another way to think of it is that sometimes we allow the lifetime of a value to be extended past the time when it’s been destructed, if we can show that any remaining references to it are inert). This is sort of what dropck is all about…