RFC2229: Capture by move

#31

struct S { x: String, y: String } fn x(s: &mut S) { let mut c = move || { // a `&mut` access to `(*s).x` // ^ passed through the deref of an `&mut` value s.x.truncate(0); }; c(); }
struct S { x: &mut String, y: String } fn x(s: S) { let mut c = move || { // a `&mut` access to `*(s.x)` // ^ // we would want to capture `&mut *s.x` s.x.truncate(0); }; c(); }

Interesting corner cases:

  • &mut access to deref of shared borrow
    • guaranteed error
let mut s = String::new();
let t = (&mut s, 10);

let c = || {
    // We see that a capture in its entirety is being 
    // modfied
    // i.e. we don't build a place on top of it
    // Therefore the capture is mutable if the root variable of the capture is mutable
    *t.0 = format!("Some other string");
}

Is this only relevant to move closures?

let c = || s.x.truncate(0); // `&mut` access to `(*s).x`, so you get a mutable ref mode for the upvar let c = move || s.x.truncate(0); // without this, you would move `s` because you stop at the deref
let c = || { s.x.truncate(0); // without this, you would move `s` because you stop at the deref drop(s.x); // (error) };
struct S { x: String, y: String } fn foo(s: &mut S) { // Closure will: // * move `s` because we don't move through derefs, so we can't move `s.y let c = || { s.x.truncate(0); // without this, you would move `s` because you stop at the deref drop(s.y); // (error) }; }
struct S { x: &mut String, y: &mut String } fn foo(s: S) { // Closure will: // * mut-borrow `*s.x` // * move `s.y` // but this doesn't require our special code. let c = || { s.x.truncate(0); // without this, you would move `s` because you stop at the deref drop(s.y); // (error) }; }
struct S { x: String, y: &mut String } fn foo(s: Box<S>) { // Closure will: // * move `s` let c = || { s.x.truncate(0); // without this, you would move `s` because you stop at the deref drop(s.y); // (error) }; }
struct S { x: String, y: &mut String } fn foo(s: S) { // Closure will: // * move `s` let c = || { s.x.truncate(0); // without this, you would move `s` because you stop at the deref drop(s.x.something); }; }

what is the reborrowing change:

  • if you are in a move closure,

    • and you have mut-ref access to a place P that involves a deref of an &mut reference,
    • you will have a mut-ref upvar for P
  • if you are in a move closure,

    • and you have ref access to a place P that involves a deref of an & reference,
    • you will have a ref upvar for P
  • two phases for the analysis

    • compute a set S of accesses {(Mode, Place)} where:
      • Mode is by-value, ref, or mut-ref
      • and Place is a place
    • min-analysis that takes S and produces a set of S' = {(Mode, Place)} of captures
  • first part, computing the set S of accesses, before reborrows:

    • if we have a move closure C:
      • for any access (_, P) in C:
        • we create an access in S like (Move, P)
    • if we have a non-move closure C:
      • for any access (M, P) in C:
        • we create an access in S like (M, P)
  • first part, computing the set S of accesses, after reborrows:

    • if we have a move closure C:
      • for any access (M, P) in C:
        • if M = mut-ref and P contains a deref of an &mut, add (MutRef, P) to S
        • else if M = ref and P contains a deref of an &, add (Ref, P) to S
        • else create an access in S like (Move, P)
    • if we have a non-move closure C:
      • for any access (M, P) in C:
        • we create an access in S like (M, P)
  • second part, min-capture-analysis will reconcile accesses to overlapping paths:

    • for moves, it will truncate at derefs
    • in general if you have an access to P and an access to some suffix P.Q, we prefer P
    • pick the strictest mode (mode, mut-ref, ref)

Examples

struct S { x: &mut String, y: &mut String } fn foo(s: S) { // Closure will: // * move `s` let c = move || { s.x.truncate(0); // without this, you would move `s` because you stop at the deref drop(s.x.something); }; // The set S is (MutRef, *s.x), (Move, (*s.x).something) // Min-capture-analysis is going to truncate the move to `s.x` // we can drop the `(MutRef, *s.x)` access as redundant // So we wind up capturing `s.x` by move }
struct S { x: &mut String, y: &mut String } fn foo(s: S) { // Closure will: // * move `s` let c = move || { s.x.truncate(0); // without this, you would move `s` because you stop at the deref drop(s.y.something); }; // The set S is (MutRef, *s.x), (Move, (*s.y).something) // Min-capture-analysis is going to truncate the move to `s.y` // So we wind up capturing `*s.x` by mut ref, s.y by move }
struct S { x: String, y: String } fn foo(s: &mut S) { // Closure will: // * move `s` let c = move || { s.x.truncate(0); // without this, you would move `s` because you stop at the deref drop(s.y); }; // The set S is (MutRef, (*s).x), (Move, (*s).y) // Min-capture-analysis is going to truncate the move to `s`, mut-ref is redundant // So we wind up capturing `s` by move }

2021-02-03

playground

struct S(i32);

// E0
fn foo() {
    let b = Box::new(S(10));
    
    let c = #[rustc_capture_analysis] move  || {
        let _b = b.0; // consume *b.0, Copy
    };
    
    c();
    
}
  • Treat a Consume of a Copy type as "mode = ref"
  • Algorithm above doesn't apply because there is no deref of a &T, only a deref of a Box
  • Niko's hypothesis
    • previous example E0 wants to capture (*b).0 by ref no, this is false
    • we don't want to do this because it would limit the closure to not outlive the local variable b
  • Behavior: just capture b by move

If you wanted to not move b, would have to remove move or do:

struct S(i32);

// E1
fn foo() {
    let b = Box::new(S(10));
    
    let b1 = &b;
    let c = #[rustc_capture_analysis] move  || {
        let _b = b1.0; // consume *b.0, Copy
    };
    
    c();
    
}
Select a repo