Async fn Future should impl Debug

This is an irlo thread mainly because I don’t know exactly where it would go on the Rust tracker.

Even a minimal impl Debug for async fn-created futures would be very valuable for users of async fn, if only because more types would be able to be dbg!-traced.

A minimal implementation:

// line 1:
async fn foo() {
// line 2:
    bar().await;
// line 3:
}

// expands to roughly

fn foo() -> impl Fututre<Output=()> {
    enum __foo {
        _2,
        _3,
        _4,
    }

    impl Future for __foo {
        fn poll(..) -> _ {
            match self {
                _2 => { /* run from line 2 to 3 */ }
                _3 => { /* run from line 3 to exit */ }
                _4 => panic!(..),
            }
        }
    }

    impl Debug for __foo {
        fn fmt(..) -> _ {
            "async fn() {foo}"
        }
    }

    __foo::_2
}

This could be confused for a debug implementation for the impl Fn ZST though (which I think we should provide as well! though it currently prints as fn() -> impl std::future::Future {foo} in error messages), so as an alternative that provides more information about the future state:

    impl Debug for __foo {
        fn fmt(..) -> _ {
            match self {
                __foo::_2 => "async fn() {foo} @ entry",
                __foo::_3 => "async fn() {foo} @ 2:16",
                __foo::_4 => "async fn() {foo} @ exhausted",
            }
        }
    }

The idea being to provide the fn which the future came from as well as where it’s currently suspended.

(Rough specification: impl Fn ZST debug representation would be effectively the current compiler printing of the type (from an external crate), representing that it is a function type for the function at this path, and the async fn future debug implementation would be the async fn’s debug implementation followed by a representation of where the future is currently suspended.

5 Likes

In the name of zero cost perhaps they should not derive Debug by default.

Would it be possible and reasonable to have this syntax?

#[derive(Debug)]
async fn foo() {
  ...
}
2 Likes

Agreed. I think at some point in the future we should generate debug impls for all futures created from async items (using some sort of default {...} text for fields which don’t implement Debug).

This doesn’t interact with the Send issue if we do it this way because all async items will implement Debug, theres never a possibility of failing that.

3 Likes

Are you imagining we might add this and (effectively) change the desugaring to impl Future + Debug? (modulo Send)

Yes, but not in a way where we would need to have ?Debug or disallow having part of the state not implement Debug (we would just have the compiler-generated Debug impl properly handle those fields in some way, such as by eliding them with ...).

Leaving out Debug impls (and others) can be a rather large compile-time optimization, sometimes done intentionally. I know this is just talking about “some point in the future” but I’d like to avoid that cost somehow (even if that’s “just” by optimizing the compiler itself so leaving out Debug impls is no longer such an improvement).

2 Likes

sure, all of these factors can be considered in the design. The important thing is that there are no backward compatibility hazards requiring us to work through this now.

1 Like

It could also just be super conservative and output something like “async { … }”, in a similar fashion to if closures got automatic Debug impls…

1 Like

Can the Future returned by an async fn be repr(packed(N)) ?

Definitely not: there’s alignment assumptions that it makes in order to take references to the pinned future space.

AFAICT the only assumption that has to be made is that the Future is properly aligned, which will be the case - note that creating references to a packed Future is ok.

What might not be properly aligned are the internal fields of the Future, which is probably ok as long as one does not create references to them. However, deriving Debug requires being able to create references to the fields of the Future, which can’t be done if the Future is packed.

There’s no way for users to control the layout of these compiler generated types, we can generate them however is necessary for correctness.

More than correctness, I was wondering whether deriving Debug would impose constraints on how the compiler must lay out these types that do not exist today.

AFAIK the layout of these types is unspecified, would it be possible for the compiler to pack these types today, i.e., to put some fields at unaligned offsets within the Future to reduce padding ?

1 Like

Has this been brought up anywhere before? I spent a while looking through rust-lang/{rust,rfcs} issues and couldn’t find anything, it seems surprising that nobody would have mentioned it before since it seems like a super-useful feature. Even just printing the type_name or the representation used in compiler errors would be useful (so that when you see types like std::iter::Map you know which closure they’re running on the items).

It doesn’t seem possible to “derive” Debug for the generated futures, the layout used is not a simple struct or enum, it’s a custom layout. So the async transform would have to generate a custom implementation of Debug for it, which could do whatever it needs to make it work with the layout. (Similar to how it currently generates a custom Drop implementation).

3 Likes

That makes sense. A manual Debug implementation could move/memcpy a misaligned field to an aligned memory location, format it, and then leak it.

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