Skip to content

[E0521] coroutine should own upvars and assigning internal references to moved captures does not leak the references #140132

Open
@dingxiangfei2009

Description

@dingxiangfei2009

I tried this code:

#![feature(coroutines, stmt_expr_attributes)]

fn foo(x: &i32) {
    let mut a = &3;
    let mut b = #[coroutine]
    move || {
        yield ();
        let b = 5;
        a = &b;
        //~^ ERROR borrowed data escapes outside of coroutine
    };
}

I expected to see this happen:
This should compile. The following is the current MIR of the coroutine at the analysis phase.

fn foo::{closure#0}(_1: {[email protected]:6:5: 6:12}, _2: ()) -> ()
yields ()
 {
    debug a => (_1.0: &i32);
    let mut _0: ();
    let _3: ();
    let mut _4: ();
    let _5: i32;
    let mut _6: &i32;
    let _7: &i32;
    scope 1 {
        debug b => _5;
    }

    bb0: {
        StorageLive(_3);
        StorageLive(_4);
        _4 = ();
        _3 = yield(move _4) -> [resume: bb1, drop: bb3];
    }

    bb1: {
        StorageDead(_4);
        StorageDead(_3);
        StorageLive(_5);
        _5 = const 5_i32;
        FakeRead(ForLet(None), _5);
        StorageLive(_6);
        StorageLive(_7);
        _7 = &_5;
        _6 = &(*_7);
        (_1.0: &i32) = move _6;
        StorageDead(_6);
        StorageDead(_7);
        _0 = const ();
        StorageDead(_5);
        drop(_1) -> [return: bb2, unwind: bb5];
    }

    bb2: {
        return;
    }

    bb3: {
        StorageDead(_4);
        StorageDead(_3);
        drop(_1) -> [return: bb4, unwind: bb5];
    }

    bb4: {
        coroutine_drop;
    }

    bb5 (cleanup): {
        resume;
    }
}

Instead, this happened:
This unfortunately does not compile.

error[E0521]: borrowed data escapes outside of closure
 --> test.rs:7:9
  |
4 |     let mut a = &3;
  |         ----- `a` declared here, outside of the closure body
...
9 |         a = &b;
  |         ^^^^--
  |         |   |
  |         |   borrow is only valid in the closure body
  |         reference to `b` escapes the closure body here

This boils down to a borrow not living long enough due to the StorageDead(_5) statement in bb1, at which _1 is still alive. However, _1.0 is a capture moved into the closure and is no longer accessible outside of this coroutine.

Original issue description

I tried this code:

fn fnonce(_: impl FnOnce()) {}

fn foo(x: &i32) {
    let mut a = &3;
    fnonce(move || {
        let b = 5;
        a = &b;
        //~^ ERROR borrowed data escapes outside of coroutine
    });
}

I expected to see this happen:
This should compile. The following is the current MIR of the closure at the analysis phase.

// MIR for `foo::{closure#0}` after analysis

fn foo::{closure#0}(_1: {[email protected]:5:12: 5:19}) -> () {
    debug a => (_1.0: &i32);
    let mut _0: ();
    let _2: i32;
    let mut _3: &i32;
    let _4: &i32;
    scope 1 {
        debug b => _2;
    }

    bb0: {
        StorageLive(_2);
        _2 = const 5_i32;
        FakeRead(ForLet(None), _2);
        StorageLive(_3);
        StorageLive(_4);
        _4 = &_2;
        _3 = &(*_4);
        (_1.0: &i32) = move _3;
        StorageDead(_3);
        StorageDead(_4);
        _0 = const ();
        StorageDead(_2);
        return;
    }
}

Note that the closure value _1 has a correct type for a FnOnce so that it is entirely owned by the closure body.

Instead, this happened:
This unfortunately does not compile.

error[E0521]: borrowed data escapes outside of closure
 --> fnonce.rs:7:9
  |
4 |     let mut a = &3;
  |         ----- `a` declared here, outside of the closure body
...
7 |         a = &b;
  |         ^^^^--
  |         |   |
  |         |   borrow is only valid in the closure body
  |         reference to `b` escapes the closure body here

This boils down to a borrow not living long enough due to the StorageDead(_2) statement, at which _1 is still alive.

A similar situation also applies to coroutines as implemented today. See this test suite

Meta

rustc --version --verbose:

rustc 1.88.0-dev
binary: rustc
commit-hash: unknown
commit-date: unknown
host: x86_64-unknown-linux-gnu
release: 1.88.0-dev
LLVM version: 20.1.2

The commit hash should be 49e5e4e3a5610c240a717cb99003a5d5d3356679.

Possible way forward

We could generate a separate body for FnOnce variant of closures, when requested, and apply a MIR transformation early to lift the upvars into their own locals. This is a known technique in #135527.

Backtrace

N/A

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-coroutinesArea: CoroutinesC-enhancementCategory: An issue proposing an enhancement or a PR with one.F-coroutines`#![feature(coroutines)]`

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions