Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions compiler/rustc_error_codes/src/error_codes/E0373.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,48 @@ A captured variable in a closure may not live long enough.
Erroneous code example:

```compile_fail,E0373
fn foo() -> Box<dyn Fn(u32) -> u32> {
let x = 0u32;
Box::new(|y| x + y)
fn foo() -> Box<dyn Fn(usize) -> usize> {
let arr = [42_u32; 17];
Box::new(|y| arr.len() + y)
}
```

This error occurs when an attempt is made to use data captured by a closure,
when that data may no longer exist. It's most commonly seen when attempting to
return a closure as shown in the previous code example.

Notice that `x` is stack-allocated by `foo()`. By default, Rust captures
closed-over data by reference. This means that once `foo()` returns, `x` no
longer exists. An attempt to access `x` within the closure would thus be
unsafe.
Notice that `arr` is stack-allocated by `foo()`. Within the closure, it is used
by reference (as the receiver of the `.len()` call). This means that once
`foo()` returns, `arr` no longer exists. An attempt to access `list` within the
closure would thus be unsafe.

Another situation where this might be encountered is when spawning threads:

```compile_fail,E0373
fn foo() {
let x = 0u32;
let y = 1u32;
let arr = [42_u32; 17];
let y = 1usize;

let thr = std::thread::spawn(|| {
x + y
arr.len() + y
});
}
```

Since our new thread runs in parallel, the stack frame containing `x` and `y`
may well have disappeared by the time we try to use them. Even if we call
`thr.join()` within foo (which blocks until `thr` has completed, ensuring the
stack frame won't disappear), we will not succeed: the compiler cannot prove
that this behavior is safe, and so won't let us do it.
Since our new thread runs in parallel, the stack frame containing `arr` may well
have disappeared by the time we try to use them. Even if we call `thr.join()`
within foo (which blocks until `thr` has completed, ensuring the stack frame
won't disappear), we will not succeed: the compiler cannot prove that this
behavior is safe, and so won't let us do it.

The solution to this problem is usually to switch to using a `move` closure.
This approach moves (or copies, where possible) data into the closure, rather
than taking references to it. For example:

```
fn foo() -> Box<dyn Fn(u32) -> u32> {
let x = 0u32;
Box::new(move |y| x + y)
fn foo() -> Box<dyn Fn(usize) -> usize> {
let arr = [42_u32; 17];
Box::new(move |y| arr.len() + y)
}
```

Expand Down
51 changes: 27 additions & 24 deletions compiler/rustc_hir_typeck/src/coercion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,26 +505,6 @@ impl<'f, 'tcx> Coerce<'f, 'tcx> {
}
};

if coerced_a == a && mt_a.mutbl.is_not() && autoderef.step_count() == 1 {
// As a special case, if we would produce `&'a *x`, that's
// a total no-op. We end up with the type `&'a T` just as
// we started with. In that case, just skip it altogether.
//
// Unfortunately, this can actually effect capture analysis
// which in turn means this effects borrow checking. This can
// also effect diagnostics.
// FIXME(BoxyUwU): we should always emit reborrow coercions
//
// Note that for `&mut`, we DO want to reborrow --
// otherwise, this would be a move, which might be an
// error. For example `foo(self.x)` where `self` and
// `self.x` both have `&mut `type would be a move of
// `self.x`, but we auto-coerce it to `foo(&mut *self.x)`,
// which is a borrow.
assert!(mutbl_b.is_not()); // can only coerce &T -> &U
return success(vec![], coerced_a, obligations);
}

let InferOk { value: mut adjustments, obligations: o } =
self.adjust_steps_as_infer_ok(&autoderef);
obligations.extend(o);
Expand All @@ -536,10 +516,31 @@ impl<'f, 'tcx> Coerce<'f, 'tcx> {
coerced_a
);

// Now apply the autoref
let mutbl = AutoBorrowMutability::new(mutbl_b, self.allow_two_phase);
adjustments
.push(Adjustment { kind: Adjust::Borrow(AutoBorrow::Ref(mutbl)), target: coerced_a });
if let Some(Adjustment { kind: Adjust::Deref(DerefAdjustKind::Builtin), .. }) =
adjustments.last()
&& let ty::Ref(_, _, ty::Mutability::Not) = if adjustments.len() >= 2 {
*adjustments[adjustments.len() - 2].target.kind()
} else {
ty::Ref(r_a, mt_a.ty, mt_a.mutbl)
}
&& mutbl_b.is_not()
{
// As a special case, if we would produce `&'a *x`, that's
// a total no-op. We end up with the type `&'a T` just as
// we started with. In that case, just skip it altogether.
//
// FIXME: Ideally we would also skip `&mut` -> `&mut` reborrow,
// and handle that in MIR. But unfortunately that would affect
// closure capture inference, which affects borrowcheck
adjustments.pop();
} else {
// Now apply the autoref
let mutbl = AutoBorrowMutability::new(mutbl_b, self.allow_two_phase);
adjustments.push(Adjustment {
kind: Adjust::Borrow(AutoBorrow::Ref(mutbl)),
target: coerced_a,
});
}

debug!("coerce_to_ref: succeeded coerced_a={:?} adjustments={:?}", coerced_a, adjustments);

Expand Down Expand Up @@ -630,6 +631,8 @@ impl<'f, 'tcx> Coerce<'f, 'tcx> {

// Handle reborrows before selecting `Source: CoerceUnsized<Target>`.
let reborrow = match (source.kind(), target.kind()) {
// Don't emit a no-op shared -> shared reborrow
(&ty::Ref(_, _, ty::Mutability::Not), &ty::Ref(_, _, ty::Mutability::Not)) => None,
(&ty::Ref(_, ty_a, mutbl_a), &ty::Ref(_, _, mutbl_b)) => {
coerce_mutbls(mutbl_a, mutbl_b)?;

Expand Down
3 changes: 3 additions & 0 deletions compiler/rustc_hir_typeck/src/expr_use_visitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1099,6 +1099,9 @@ impl<'tcx, Cx: TypeInformationCtxt<'tcx>, D: Delegate<'tcx>> ExprUseVisitor<'tcx
upvar_borrow,
);
}
ty::UpvarCapture::ByCopy => {
self.delegate.borrow_mut().copy(&place_with_id, place_with_id.hir_id);
}
}
}
}
Expand Down
Loading
Loading