Skip to content

stdexec::as_awaitable(s,p) should not apply p.await_transform() when checking that s.as_awaitable(p) is awaitable #1678

@lewissbaker

Description

@lewissbaker

The implementation of stdexec::as_awaitable() checks that the result satisfies the __awaitable concept in the case that it dispatches to member t.as_awaitable(p) or to tag_invoke(as_awaitable, t, p).

See https://github.com/NVIDIA/stdexec/blob/7fed34ce2139454eef1cbd9cb05f10b5a8ec1a2e/include/stdexec/__detail/__as_awaitable.hpp#L239-248

The __awaitable concept defined here checks that __get_awaiter(x, p) returns a type that satisfies __awaiter<Promise>.

However, the __get_awaiter() method dispatches through promise.__await_transform() and then call operator co_await() on the result of that, rather than directly on the argument.

This means that when you write your own promise_type::await_transform() that wants to return stdexec::as_awaitable(something, *this) that as_awaitable() will then try to pass the result of this through await_transform() again.

It should not be doing this - checking that the result of as_awaitable() is awaitable in a given promise should just be checking that the result has an operator co_await() returns an __awaiter<Promise> or just already is an __awaiter<Promise>.

The current formulation just happens to work for cases where your await_transform() method passes its argument straight through to as_awaitable().
If the .as_awaitable() function returns an awaitable that is awaitable only in the specified Promise context, then when that type is passed through as_awaitable() again, it should fall through to either the __awaitable_sender case (which then doesn't check that the result is __awaitable, although does unnecessarily instantiates __awaitable_sender and potentially __sender_awaitable_t) or to the last case which just returns a reference to the original object.

However, if your await_transform() method performs other transforms, such as the task::promise_type which returns as_awaitable(affine_on(arg), *this), then the recursive call to await_transform() does not necessarily work.

For example, say the first time that this calls through the result of calling affine_on(arg).as_awaitable(*this) is an awaitable that is only awaitable within the specified context. Then when this result is passed through await_transform() again, this ends up passing it to affine_on() again, which will then try to treat the awaitable object as a sender and check to see if it can be adapted to a sender by awaiting it in a manufactured coroutine-type (see __connect_awaitable()).

However, the awaitable object will not be awaitable within the __connect_awaitable coroutine, because it is only awaitable in a coroutine with the original Promise type, and so the call to affine_on() will fail to compile in this case.

I think the as_awaitable() algorithm should be mandating that the result of .as_awaitable() member-function has an operator-co_await and awaiter type that is compatible with the promise type, but should not be trying to apply await_transform() again as as_awaitable() is typically used within await_transform() and thus await_transform() has likely already been applied.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions