Proposal about expired references

Indeed, the mechanism is very different -- but for your motivating problem, it works just as well, and I feel it can be easier integrated into the existing framework. But your proposal probably enables things that partial borrow do not.

I don't think that partial borrow would solve that problem. Partial borrows mostly solve problems that can already be worked around by "shadow structs" or inlining. However, in this case replacing acc.get_sum() with acc.sum would not work because acc contains a reference to state which is dropped, and therefore cannot exist at that line.

I consider partial borrow and expired references to be two separate concepts. You can have one, the other, or both, and they solve different problems and work together. The only thing in common is that they are both related to the borrow checker, but the former concerns structs only and the latter concerns reference lifetimes.

Just a side note, I wrote in the Motivation part of the proposal that the intent is not just to enable specific use cases, but also to make the logic around lifetimes explicit. For example, I expect to provide a rigorous alternative to dropck so as to avoid hacks like 1327-dropck-param-eyepatch - The Rust RFC Book.

Good point, I think you are right.

Yes, that's what I meant by the other things that this proposal enables.

I think it should be possible for borrow checker to understand that get_sum accesses a region without any references. As you say, using borrow regions can be viewed as a creation of shadow types, so your motivation example can be desugared as:

struct Accumulator1<'a> { ptr: &'a State }
struct Accumulator2 { sum: u64 }

// impls
fn new(ptr: &'a State) -> (Accumulator1<'a>, Accumulator2) {
    (Accumulator1 { ptr }, Accumulator2 { sum: 0 })
}

fn add(a1: &mut Accumulator1, a2: &mut Accumulator2) {
    a2.sum += a1.ptr.observe()
}

fn get_sum(a1: &Accumulator2) -> u64 { a1.sum }

// user code
let (mut a1, mut a2) = Accumulator::new(&state);
state.set_val(3); add(&mut a1, &mut a2);
// ...
state.shutdown();
println!("{}", get_sum(&a2));

For "shadow structs" I meant by "view structs" in niko's blog post: http://smallcultfollowing.com/babysteps/blog/2018/11/01/after-nll-interprocedural-conflicts/#view-structs-as-a-general-but-extreme-solution. A struct that contains references only.

The problem is that at this point:

// user code
let (mut a1, mut a2) = Accumulator::new(&state);
state.set_val(3); add(&mut a1, &mut a2);
// ...
state.shutdown();
// HERE: a1 doesn't exist at all
println!("{}", get_sum(&a2));

the variable a1 is gone and the struct is no longer complete. Partial borrow does not split structs; it simply allows a reference to a subset of a struct's fields. I can't imagine how structs could be splitted in this way without a huge change to how impl, PhantomData, Drop and many other things work.

In my understanding the struct still exists, i.e. Accumulator bytes stay on the stack and ptr value is intact even after state got "destroyed" (its stack representation stays intact after drop execution). It's just that borrow checker prevents code from accessing the struct after shutdown, since the ptr field value is no longer valid. So I don't think any serious changes are needed to borrow checker after addition of regions apart from relaxing access rules when regions are present (not that I am an expert on the subject though, I hope @RalfJung will comment on it).

In other words, with borrow regions borrow checker will see Accumulator divided into two parts, it will not be a black box for it anymore. And when one part becomes invalid because of the pointer, it still will be able to allow access to the second valid part.

I think the problem is deeper than that, and I've described that in the text following the example code. Just replace acc.get_sum() with acc.sum, and all arguments should still apply. That should be a case that expired references are meant to solve. Feel free to ask if there is anything you are still unsure about.

I don't quite see the problem. With regions a field can be part of only one region (well, we could envision nested regions, but let's work with flat ones here). Accumulator type is divided into two regions (parts) sum and ptr with one field each. After state.shutdown() is called the former region becomes invalid since it contains a dangling pointer, but the latter region is still valid. The sum field is part of a valid region, so we can access it safely, be it via a method or directly. So I don't see how acc.get_sum() and acc.sum are different here.

I'm afraid that such regions have a very limited scope of usage. For example, if such a struct were to implement a Drop handler, how would it work? It has to be executed before state.shutdown(), and the struct assumes that it would not be ever used after the call to drop. In trait implementations one can only use self, &self and &mut self, which view the struct as a whole. I can't see how one could use regions as the type of self in a trait's signature. If a struct with PhantomData contains an unsafe method that gives you a reference, one might accidentally get a reference outside its expected lifetime because PhantomData was ignored. Also if every field can be part of only one region, why not define separate structs for them?

One option is for shutdown to return a new type that holds all valid data (no references).

struct Foo<'a> {
    ptr: &'a State
    sum: u64
}

struct Sum {
    sum: u64
}

impl Foo<'_> {
    fn shutdown(self) -> Sum {
        // shutdown work
        Sum { sum: self.sum }
    }
}

You can then implement the relavant api for Sum

Sorry for answering with a question, but how would it work with "expired references"? You have to implement drop for the whole struct (i.e. after drop got executed, the whole type becomes invalid and thus inaccessible), so IIUC in such scenario you will not be able to access sum with your proposal as well.

// struct with two regions `a` and `b`, named exactly as the fields
struct Foo {
    pub region a: u32,
    pub region b: u32,
}

trait Bar {
    region r1;
    region r2;
    // take `r1` as mutable and `r1` as immutable
    fn bar1(self(&mut r1, &r2));
    // consume `r1` and mutate `r2`
    fn bar1(self(r1, &mut r2))'
}

impl Bar for Foo {
    // regions `a` and `b` must be defined as part of `Foo`
    region r1 = a;
    region r2 = b; 

    // `r1` and `r2` are aliases to regions `a` and `b` respectively
    fn bar1(self(&mut r1, &r2)) { .. }
    // ...
}

It simply means that unsafe method was written incorrectly. Nothing more.

The same reason why you don't define separate type states in your motivation example: convenience and significant reduction of boilerplate.

hmm.. I guess one could imagine this?

fn foo() -> Accumulator<'none> {
    let state : State = ...;
    let accum = Accumulator{ &state, ..};
    ..
    accum
}

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.