El 18/06/2026 a las 20:17, Vinnie Falco escribió:
Colleagues,
I am requesting a formal review of P4003R3, "A Minimal Coroutine Execution Model." It is published in the May 2026 mailing, so it is eligible under the rules, and I would like it read closely before it next goes to LEWG.
I sincerely find the paper lacking too much context for a reader like me that is not an expert on coroutines. If the paper is not designed for people like me, fine, but it might indicate some context/info is missing in the paper. I've taken the latest revision (P4003R4) to write some comments: ------------------------------------------------------------ ------------------------------------------------------------ 1) Defining a bit more what's offered in the introduction. ------------------------------------------------------------ ------------------------------------------------------------ I can deduce from the name capitalization and colors something, but in in section "What std provides", it should say if the protocol talks about a concept or a class probably (and "What std provides" is a bit: Type Name What users can write concept IoAwaitable /**/ .... class execution_context /**/ .... If IoAwaitable is proposed for the standard, then it should be written as other standard concepts ------------------------------------------------------------ ------------------------------------------------------------ 2) I miss the "purpose" of each name and which C++ actor will use it ------------------------------------------------------------ ------------------------------------------------------------ std::io_env / std author writes it / carried to every suspension point of IoAwaitable types in a.await_suspend(h, env). IoAwaitable / A type A such that a.await_suspend(h, env) is valid / std provides the concept and library authors write awaitables that satisfy the concept (e.g. a socket's read_some) IoRunnable / A concept refining IoAwaitable for It's the contract between the return type of a coroutine and a launcher function that need access to the environment. / std provides the concept and authors write couroutine return types (e.g. task<T>) that satisfy it [....] ------------------------------------------------------------ ------------------------------------------------------------ 3) In "IoRunnable and Launch Functions" it is not clear what type requires IoRunnable. ------------------------------------------------------------ ------------------------------------------------------------ Types returned from "server_main()" and "compute()" in the run_async example, etc. It's confusing what "run_async" and "run" are, they are examples of launchers that need to modify the environment, but they are introduced with much information to understand them ------------------------------------------------------------ ------------------------------------------------------------ 4) Some text repetitions: ------------------------------------------------------------ ------------------------------------------------------------ There are repetitions like "Without handlers, the result is discarded and exceptions rethrow". The IA could catch them and rewrite them not to make small paper repetitive ------------------------------------------------------------ ------------------------------------------------------------ 5) Novel concepts without context ------------------------------------------------------------ ------------------------------------------------------------ The paper defines important concepts like "reactor" without any context. I find this makes as a not solid grounded for non-experts like me. I can imagine what "reactor" could mean but I feel lost. ------------------------------------------------------------ ------------------------------------------------------------ 6) Basic concepts are missing ------------------------------------------------------------ ------------------------------------------------------------ And now my main objection (taking into account my little knowledge about coroutines in theory and in practice, please be patient. I feel the facility presentation lacks enough context/concepts to connect all facilities. Example: the IoAwaitable concept requires a signature that it's different from the standard coroutine machinery that calls await_suspend with a single argument (non type-erased handle). The paper says "The caller's await_transform injects the environment as a pointer parameter - no templates, no type leakage", but there is IMHO an important missing point. For someone that has very basic coroutine knowledge, it isn't clear how this happens in the protocol. The language's contract for "await_suspend" is fixed and one-argument. How is that translated to the two-argument await_suspend required by the IoAwaitable written by the user? The two-argument form in the IoAwaitable concept is a private convention between the promise of the caller coroutine that understands the protocol and the IoAwaitable callee. But I don't see any utility or concept that helps the user write such coroutine. The protocol does not define any task or task concept that precisely is the other end of the IoAwaitable requirement. It relies on a "compilation error" if we try to await a IoAwaitable from a coroutine that is not "IoAwaitableProtocol-aware". This is IMHO a weak point of the proposal, as this compilation error type is not good enough in 2026. When you co_await an IoAwaitable inside a non-conforming coroutine, the diagnostic would be some deep "no matching call to await_suspend(coroutine_handle<>)" template bloat, not something like "this coroutine's return type does not satisfy the IoAwaitableProtocol". IoRunnable partially names the coroutine side, but only the launch surface. The goal of the paper is "a small normative protocol that unlocks interoperable tasks." But "interoperable task" is precisely the thing left undefined. I'm not sure if a "IoCoroutine"/"IoPromise" concepts can be precisely defined, but at least we should try to get a more formalized specification. of all the pieces needed to understand and implement the whole IoAwaitableProtocol. I'm pretty sure I don't understand the whole protocol, let me just throw some half-baked proposal just to test if my understanding (and the understanding of other readers like me) is partially correct, overspecified, or underspecified... Problem #1: The language itself does not define an "std::awaiter" concept (which is what the promise must produce from IoAwaitable), if we had one, then we could have the following concepts: +++++++++++++++++ CONCEPT HIERARCHY +++++++++++++++++ Awaiter (the language's awaiter shape: 1-arg await_suspend), maybe we could try to deduce it and add it to the standard ^ IoAwaitable (the protocol operation: 2-arg await_suspend(h, env)) [from the paper] ^ IoPromise (a promise that can be given an env and whose await_transform turns ANY IoAwaitable into an Awaiter) [the missing piece] ^ IoCoroutine (a return type whose promise is an IoPromise) [the missing piece] ^ IoRunnable (an IoCoroutine that is also launchable) +++++++++++++++++ Some sketches: Note: +++++++++++++++++ // A maximally-general IoAwaitable archetype struct io_awaitable_archetype { struct distinct_result {}; bool await_ready() const noexcept { return false; } void await_suspend (std::coroutine_handle<>, const io_env*) const noexcept {} distinct_result await_resume() const noexcept { return {}; } }; static_assert(IoAwaitable<io_awaitable_archetype>); // A promise that can deal with IoAwaitables template <class P> concept IoPromise = // (1) it can be handed an environment. // NORMATIVE PROSE (not checkable here): set_environment(e) shall store e // such that subsequent await_transform calls deliver *e to the // transformed awaitable, and such that any child coroutine constructed // during this coroutine's execution observes e->frame_allocator as the // cached frame allocator. e shall outlive the coroutine. Throws nothing. requires(P& p, const io_env* env) { { p.set_environment(env) } noexcept; } && // (2) it can be told its continuation (called by the awaiting parent on // every // child await, and by a launch function at the root). // NORMATIVE PROSE: set_continuation(c) shall store c such that the // coroutine resumes c at its final suspend point (or noop_coroutine() if // c is null). Throws nothing. requires(P& p) { { p.set_continuation(std::coroutine_handle<>{}) } noexcept; } && // (3) its await_transform turns a protocol awaitable into a language // Awaiter. // NORMATIVE PROSE: await_transform(a) shall yield an Awaiter that, when // the coroutine suspends, invokes a.await_suspend(h, env) with env being // the environment most recently installed via set_environment. requires(P& p, any_io_awaitable a) { { p.await_transform(a) }; } && Awaiter<decltype(std::declval<P&>().await_transform( std::declval<any_io_awaitable>()))> && // (4) it provides a class-scope operator new / operator delete (its own or // inherited), so frame allocation does NOT fall through to the global // ::operator new. This rejects a promise that ignores frame allocation // entirely; note it can only check PRESENCE, not behavior (see prose). // NORMATIVE PROSE (the part the concept CANNOT verify): the coroutine // frame shall be allocated, at the point of allocation, using the memory // resource returned by get_cached_frame_allocator(), and the matching // operator delete shall return that storage to the same resource. A // class-scope operator new that merely forwards to ::operator new // satisfies this concept clause but does NOT satisfy this normative // requirement. requires(std::size_t n, void* ptr) { { P::operator new(n) } -> std::same_as<void*>; { P::operator delete(ptr, n) }; }; template <class T> concept IoCoroutine = requires { typename T::promise_type; } && IoPromise<typename T::promise_type>; template <class T> concept IoRunnable = IoCoroutine<T> && IoAwaitable<T> && requires(T& t, const T& ct, typename T::promise_type& p) { { ct.handle() } noexcept -> std::same_as<std::coroutine_handle<typename T::promise_type>>; { ct.exception() } noexcept -> std::same_as<std::exception_ptr>; { t.release() } noexcept; }; ------------------------------------------------------------ ------------------------------------------------------------ 7) Leaf awaitable vs tasks ------------------------------------------------------------ ------------------------------------------------------------ I find it's hard to specify the requirements for IoAwaitable types that are leaf (NOT a coroutine, no promise type, probably calls the OS) vs an IoAwaitable that is a "task". In the later, the environment must be propagated and I really don't know see how/where. Not sure if that should be part of the protocol, but certainly, if I want to properly implement the whole protocol, I need to know which is the correct time to call p.set_enviroment and p.set_continuation. Isn't that a requirement to fulfill the protocol? ------------------------------------------------------------ ------------------------------------------------------------ 8) Implementation details? ------------------------------------------------------------ ------------------------------------------------------------ Execution_ref shows private parts, shouldn't those be "exposition-only" members or just absent? void const* ex_ = nullptr; detail::executor_vtable const* vt_ = nullptr; ------------------------------------------------------------ ------------------------------------------------------------ 9) execution_conext and executor_ref seem orphan concepts ------------------------------------------------------------ ------------------------------------------------------------ The relationship between those and the rest is confusing. As I understand it, when a leaf IoAwaitable suspends, it picks one of a small set of outcomes — that's its decision. And that's why the executor_ref is needed in the environment. I think it should be clearly stated: "This operation isn't ready → park until the reactor says it's ready." → it registers its continuation with env->executor.context() (the reactor). "This operation is already done / cancelled / synchronously complete → don't park, resume soon, safely." → it calls env->executor.post(cont). "I'm provably on the right thread at a safe point → resume immediately if allowed." → it calls env->executor.dispatch(cont). So the awaitable chooses among park / defer / immediate-transfer. That is a real decision, and it's the awaitable's job because only the awaitable knows the state of its operation. But there is no explanation about the operations provided by executor_ref and execution_context and its service. I think this paper should at least give a non-normative text sayins what those functions do. Finally once you need to go to the execution context (calling "context() on the executor_ref) then isn't the type erasure going away? "use_service" requires a concrete T type that the IoAwaitable must know so aren't we breaking the generic executor promise-land? ------------------------------------------------------------ ------------------------------------------------------------ Summary ------------------------------------------------------------ ------------------------------------------------------------ I know my coroutine knowledge is far from minimal so take all these points with a grain of salt. But if the intention is to have a minimal paper that gives an overview that minimally connects the presented facilities, I think the paper is missing some basic explanations. Best, Ion