From d3b4d92b39a6085cdaab102aa9cf6be71a5b16a4 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Sun, 20 Sep 2026 19:07:46 +0000 Subject: [PATCH] Fix `run_loop` sender's zero-environment completion signatures `run_loop::sender::get_completion_signatures()` evaluates `get_stop_token(std::declval()...)`. For an empty `Env` pack that is `get_stop_token()` with no argument, which is ill-formed. Because the member's return type is deduced, determining the type of the call requires instantiating the body, so the error is not in the immediate context: it is a hard error rather than a substitution failure. [exec.getcomplsigs] constrains `sizeof...(Env) <= 1`, so the zero-environment query is well-formed and has to be answered. Two consequences of it not being: * `get_completion_signatures()` fails to compile. * `dependent_sender` is itself spelled in terms of that query inside a requires-expression, so `dependent_sender` is ill-formed rather than false -- and every algorithm that reaches for it fails with it. `when_all` over two `run_loop` senders does not compile today for this reason. Answer the empty pack as for `env<>`. That is what [exec.run.loop]'s own formula gives for `E = env<>`, whose stop token is unstoppable, and it is how the other environment-sensitive senders here already handle the empty pack -- `then_t` and `into_variant_t` both carry template struct get_signatures : get_signatures> {}; The header branch also gains `#include `; the modules branch already imported `beman.execution.detail.env`. Both new assertions in `exec-run-loop-types.test.cpp` fail to compile against the unfixed header, which is what makes them a regression test rather than a restatement. --- include/beman/execution/detail/run_loop.hpp | 17 ++++++++++++++--- .../execution/exec-run-loop-types.test.cpp | 11 +++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/include/beman/execution/detail/run_loop.hpp b/include/beman/execution/detail/run_loop.hpp index 50777110..06a9eab7 100644 --- a/include/beman/execution/detail/run_loop.hpp +++ b/include/beman/execution/detail/run_loop.hpp @@ -31,6 +31,7 @@ import beman.execution.detail.set_value; import beman.execution.detail.unstoppable_token; #else #include +#include #include #include #include @@ -90,10 +91,20 @@ class run_loop { }; struct sender { using sender_concept = ::beman::execution::sender_tag; - template + template static consteval auto get_completion_signatures() noexcept { - if constexpr (::beman::execution::unstoppable_token()...))>) + // [exec.getcomplsigs] permits sizeof...(Env) == 0. Answering that + // query as if for env<> matches [exec.run.loop]'s formula, whose + // E = env<> has an unstoppable stop token, and matches how the + // other environment-sensitive senders here (then, into_variant) + // handle the empty pack. Without this the member's body is + // ill-formed for an empty pack -- get_stop_token() with no + // argument -- and because the return type is deduced, that is a + // hard error rather than a substitution failure. + if constexpr (0u == sizeof...(Env)) + return get_completion_signatures>(); + else if constexpr (::beman::execution::unstoppable_token()...))>) return ::beman::execution::completion_signatures<::beman::execution::set_value_t()>{}; else return ::beman::execution::completion_signatures<::beman::execution::set_value_t(), diff --git a/tests/beman/execution/exec-run-loop-types.test.cpp b/tests/beman/execution/exec-run-loop-types.test.cpp index 4d6f43f8..cced0e8e 100644 --- a/tests/beman/execution/exec-run-loop-types.test.cpp +++ b/tests/beman/execution/exec-run-loop-types.test.cpp @@ -16,6 +16,7 @@ import beman.execution; #include #include #include +#include #include #include #include @@ -98,6 +99,16 @@ TEST(exec_run_loop_types) { ::std::same_as, decltype(test_std::get_completion_signatures())>); + // [exec.getcomplsigs] constrains sizeof...(Env) <= 1, so the + // zero-environment query is well-formed and must be answered. It is + // answered as for env<>, whose stop token is unstoppable. + static_assert(::std::same_as, + decltype(test_std::get_completion_signatures())>); + // ... and because detail::non_dependent_sender is itself spelled with that + // query inside a requires-expression, a sender that cannot answer it makes + // the concept ill-formed rather than false. Asserting the concept is what + // pins that down. + static_assert(not test_std::dependent_sender); // p7: static_assert( test_std::receiver_of())>);