Understanding async/await lowering (captures/upvars) and borrow checking

Note: This is not a user question, I'm trying to debug a potential compiler bug (Indexing via `index` method and `[idx]` sugar works differently in `async` blocks/functions · Issue #72956 · rust-lang/rust · GitHub) and understand borrow checking and async/await lowering better.

I'm trying to understand why this program compiles:

use std::ops::Index;

pub struct A(*mut ());

unsafe impl Send for A {}

impl A {
    pub fn new() -> &'static mut A {
        static mut ASD: A = A(std::ptr::null_mut());
        unsafe { &mut ASD }
    }
}

pub struct B<'a>(pub &'a mut A);

impl<'a> std::ops::Index<usize> for B<'a> {
    type Output = ();

    fn index(&self, _idx: usize) -> &() {
        &()
    }
}

pub async fn baz(_v: &()) {}

pub fn assert_send<T: Send>(_: T) {}

pub async fn bar() -> () {
    let a = B(A::new());
    let r = a.index(0);
    baz(r).await;
}

pub fn test() {
    assert_send(bar())
}

but if I change a.index(0) to &a[0] I get this error:

error: future cannot be sent between threads safely
  --> test_fails.rs:33:5
   |
24 | pub fn assert_send<T: Send>(_: T) {}
   |                       ---- required by this bound in `assert_send`
...
33 |     assert_send(bar())
   |     ^^^^^^^^^^^ future returned by `bar` is not `Send`
   |
   = help: within `B<'_>`, the trait `Sync` is not implemented for `*mut ()`
note: future is not `Send` as this value is used across an await
  --> test_fails.rs:29:5
   |
28 |     let r = &a[0];
   |              - has type `&B<'_>` which is not `Send`
29 |     baz(r).await;
   |     ^^^^^^^^^^^^ await occurs here, with `a` maybe used later
30 | }
   | - `a` is later dropped here
help: consider moving this into a `let` binding to create a shorter lived borrow
  --> test_fails.rs:28:14
   |
28 |     let r = &a[0];
   |              ^^^^

error: aborting due to previous error

I've been trying to debug this on and off for a few months now. At first I thought this is a borrow checking issue, caused by the extra intermediate value in the version with index expression (generated by lowering of index expressions to MIR). However I asked this on Zulip and someone suggested that this could be an issue with capturing locals in async structs, which I think makes sense.

Now I'm wondering how to further debug this. I asked on Zulip about seeing structs, or captures/upvars, used at the yield points, but I currently don't have an answer.

Is there a way to see struct definitions for async functions at yield points? For example, in the repro above, I'd like to see the struct used (with values for the fields) returned in line baz(r).await. Is this possible?

If not, does anyone have any suggestions on how to debug this further?

Thanks.

This is better suited for users.rust-lang.org.

2 Likes

How so? This is about debugging the compiler. Why do you think this is a user question?

Reading the title of this post and reading the beginning and skimming the rest of its contents, one can easily get the impression that you’re trying to understand and debug some Rust code instead of the compiler. You might want to make this more clear :wink: — well, perhaps you last comment already did some of the clarification needed.

I myself certainly got this impression on my first read to the point of dismissing your post before realizing what it’s actually about.

When you want people to read this thoroughly, it also doesn’t help that the example code contains some obviously unsound API. It doesn’t really matter for your point but starting with a huge code block containing questionable Rust code before explaining anything is probably suboptimal.

Sorry for the confusion -- I edited the title and added a note at the beginning of my question. Hopefully it's more clear now. If you have any suggestions on how to make my question more clear let me know and I'll update!

2 Likes

This seems like it might be an instance of https://github.com/rust-lang/rust/issues/57017.

My bad — I skimmed the question and didn't realize you were debugging the compiler.

1 Like

Hm, I think this is plausible, yes. As far as I understand the problem in #57017 is captures in generators are computed in HIR, and that yields captures that are not as precise as we'd like. Capturing more than necessary is one explanation of the problem in my original post, so perhaps they're are the same issue.

The question of how to proceed here is still open :slight_smile:

I'll try to see captures/upvars using prints, and see if I can improve it somehow in the version with an index expression.