From 62c7d224073511050046ff1d312424203f997909 Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Sat, 21 Feb 2026 13:28:31 -0500 Subject: [PATCH 1/5] exec::repeat_until et al.: Dependent Child Sender Support The TODO eliminated by this commit not only meant that exec:: repeat_until et al. previously didn't the usual constant evaluation techniques to report errors, it also meant that a hard compile error (rather than a return type indicating that the sender is dependent) resulted if: - The child sender was dependent, and - An attempt was made to determine the completion signatures of the parent without an environment Updated so exec::repeat_until et al. now properly report that they are dependent if their child is dependent. --- include/exec/repeat_until.hpp | 19 +++++++++++++++++-- test/exec/test_repeat_until.cpp | 19 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/include/exec/repeat_until.hpp b/include/exec/repeat_until.hpp index 189f76108..e5f6d5d8d 100644 --- a/include/exec/repeat_until.hpp +++ b/include/exec/repeat_until.hpp @@ -255,13 +255,28 @@ namespace experimental::execution __delete_set_value_t>, __mbind_front_q<__values_t, _Child>::template __f>; + struct _FAILED_TO_FORM_COMPLETION_SIGNATURES + {}; + struct __repeat_until_impl : __sexpr_defaults { template static consteval auto __get_completion_signatures() { - // TODO: port this to use constant evaluation - return __completions_t<__child_of<_Sender>, _Env...>{}; + using __child_t = __child_of<_Sender>; + if constexpr (::STDEXEC::__minvocable_q<__completions_t, __child_t, _Env...>) + { + return __completions_t<__child_t, _Env...>{}; + } + else if constexpr (sizeof...(_Env) == 0) + { + return STDEXEC::__dependent_sender<_Sender>(); + } + else + { + return ::STDEXEC::__throw_compile_time_error<_FAILED_TO_FORM_COMPLETION_SIGNATURES, + ::STDEXEC::_WITH_PRETTY_SENDER_<_Sender>>(); + } }; static constexpr auto __connect = diff --git a/test/exec/test_repeat_until.cpp b/test/exec/test_repeat_until.cpp index 9401f6b94..95bfd52d0 100644 --- a/test/exec/test_repeat_until.cpp +++ b/test/exec/test_repeat_until.cpp @@ -28,9 +28,11 @@ #include #include +#include #include #include #include +#include #include namespace ex = STDEXEC; @@ -407,4 +409,21 @@ namespace { "Missing added set_error_t(std::exception_ptr)"); } } + + TEST_CASE("repeat_until works with a dependent sender", "[adaptors][repeat_until]") + { + std::size_t invoked = 0; + auto snd = exec::repeat_until(ex::read_env(ex::get_stop_token) + | ex::then( + [&](auto token) noexcept + { + ++invoked; + return std::is_same_v; + })); + static_assert(ex::dependent_sender); + auto op = ex::connect(std::move(snd), expect_void_receiver{}); + CHECK(!invoked); + ex::start(op); + CHECK(invoked == 1); + } } // namespace From 03d29fdc36c5f556232ac3ecd9c453c2d84df7b8 Mon Sep 17 00:00:00 2001 From: Eric Niebler Date: Mon, 2 Mar 2026 19:33:40 -0800 Subject: [PATCH 2/5] formatting --- test/exec/test_repeat_until.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/exec/test_repeat_until.cpp b/test/exec/test_repeat_until.cpp index c78d7f5e2..01950bbac 100644 --- a/test/exec/test_repeat_until.cpp +++ b/test/exec/test_repeat_until.cpp @@ -484,4 +484,4 @@ namespace ex::start(op); CHECK(invoked == 1); } -} // namespace +} // namespace From 9533882bb46e6513b189806279309d2cd1ed29d2 Mon Sep 17 00:00:00 2001 From: Eric Niebler Date: Tue, 3 Mar 2026 13:00:30 -0800 Subject: [PATCH 3/5] give `repeat_until`'s `get_completion_signatures` the `consteval` treatment --- include/exec/repeat_until.hpp | 154 +++++++++++++---------- include/exec/sequence.hpp | 12 +- include/stdexec/__detail/__receivers.hpp | 12 +- test/exec/test_repeat_until.cpp | 2 +- 4 files changed, 103 insertions(+), 77 deletions(-) diff --git a/include/exec/repeat_until.hpp b/include/exec/repeat_until.hpp index e5f6d5d8d..ea9f344bb 100644 --- a/include/exec/repeat_until.hpp +++ b/include/exec/repeat_until.hpp @@ -21,6 +21,7 @@ #include "../stdexec/__detail/__optional.hpp" #include "../stdexec/execution.hpp" +#include "completion_signatures.hpp" #include "sequence.hpp" #include "trampoline_scheduler.hpp" @@ -29,6 +30,9 @@ namespace experimental::execution { + struct _EXPECTING_A_SENDER_OF_ONE_VALUE_THAT_IS_CONVERTIBLE_TO_BOOL_; + struct _EXPECTING_A_SENDER_OF_VOID_; + namespace __repeat { using namespace STDEXEC; @@ -81,8 +85,7 @@ namespace experimental::execution else { // Mixed results: - constexpr bool __is_nothrow = noexcept( - (static_cast(static_cast<_Booleans &&>(__bools)) && ...)); + constexpr bool __is_nothrow = (std::is_nothrow_convertible_v<_Booleans, bool> && ...); STDEXEC_TRY { // If the child sender completed with true, we're done @@ -110,11 +113,11 @@ namespace experimental::execution template constexpr void set_error(_Error &&__err) noexcept - { // intentionally pass-by-value + { STDEXEC_TRY { auto __err_copy = static_cast<_Error &&>(__err); // make a local copy of the error... - __state_->__cleanup(); // because this could potentially invalidate it. + __state_->__cleanup(); // ... because this could potentially invalidate it. STDEXEC::set_error(std::move(__state_->__rcvr_), static_cast<_Error &&>(__err_copy)); } STDEXEC_CATCH_ALL @@ -134,9 +137,9 @@ namespace experimental::execution } [[nodiscard]] - constexpr auto get_env() const noexcept -> env_of_t<_Receiver> + constexpr auto get_env() const noexcept -> __fwd_env_t> { - return STDEXEC::get_env(__state_->__rcvr_); + return __fwd_env(STDEXEC::get_env(__state_->__rcvr_)); } __opstate_base<_Receiver> *__state_; @@ -207,84 +210,97 @@ namespace experimental::execution STDEXEC_PRAGMA_POP() - struct _EXPECTING_A_SENDER_OF_ONE_VALUE_THAT_IS_CONVERTIBLE_TO_BOOL_ - {}; - struct _EXPECTING_A_SENDER_OF_VOID_ - {}; - - template - using __values_t = - // There's something funny going on with __if_c here. Use std::conditional_t instead. :-( - std::conditional_t< - ((sizeof...(_Args) == 1) && (__std::convertible_to<_Args, bool> && ...)), - std::conditional_t<(__bool_constant<_Args, false> && ...), - completion_signatures<>, - completion_signatures>, - __mexception<_WHAT_(_INVALID_ARGUMENT_), - _WHERE_(_IN_ALGORITHM_, repeat_until_t), - _WHY_(_EXPECTING_A_SENDER_OF_ONE_VALUE_THAT_IS_CONVERTIBLE_TO_BOOL_), - _WITH_PRETTY_SENDER_<_Child>>>; - - template - using __values_overload_nothrow_bool_convertible_t = - __mand...>; - - template - using __values_nothrow_bool_convertible_t = - __value_types_t<__completion_signatures_of_t<_Sender, _Env...>, // sigs - __qq<__values_overload_nothrow_bool_convertible_t>, // tuple - __qq<__mand> // variant - >; - - template - using __with_eptr_completion_t = __eptr_completion_unless< - __values_nothrow_bool_convertible_t<_Sender, _Env...>::value - && __cmplsigs::__partitions_of_t< - __completion_signatures_of_t<_Sender, _Env...>>::__nothrow_decay_copyable::__errors::value - && (__nothrow_connectable<_Sender, __receiver_archetype<_Env>> && ...)>; - - template - using __delete_set_value_t = completion_signatures<>; - - template - using __completions_t = STDEXEC::transform_completion_signatures< - __completion_signatures_of_t<__decay_t<_Child> &, _Env...>, - STDEXEC::transform_completion_signatures< - __completion_signatures_of_t, _Env...>, - __with_eptr_completion_t<_Child, _Env...>, - __delete_set_value_t>, - __mbind_front_q<__values_t, _Child>::template __f>; - - struct _FAILED_TO_FORM_COMPLETION_SIGNATURES - {}; - struct __repeat_until_impl : __sexpr_defaults { - template - static consteval auto __get_completion_signatures() + template + static constexpr auto __transform_values = []() { - using __child_t = __child_of<_Sender>; - if constexpr (::STDEXEC::__minvocable_q<__completions_t, __child_t, _Env...>) + if constexpr (sizeof...(_Args) != 1 || (!__std::convertible_to<_Args, bool> || ...)) + { + return exec::throw_compile_time_error< + _WHAT_(_INVALID_ARGUMENT_), + _WHERE_(_IN_ALGORITHM_, repeat_until_t), + _WHY_(_EXPECTING_A_SENDER_OF_ONE_VALUE_THAT_IS_CONVERTIBLE_TO_BOOL_), + _WITH_PRETTY_SENDER_<_Child>>(); + } + else if constexpr ((__bool_constant<_Args, false> && ...)) { - return __completions_t<__child_t, _Env...>{}; + return STDEXEC::completion_signatures{}; } - else if constexpr (sizeof...(_Env) == 0) + else if constexpr ((std::is_nothrow_convertible_v<_Args, bool> && ...)) + { + return STDEXEC::completion_signatures(); + } + else + { + return STDEXEC::completion_signatures(); + } + }; + + static constexpr auto __transform_errors = []() noexcept + { + if constexpr (__nothrow_decay_copyable<_Error> || __decays_to<_Error, std::exception_ptr>) { - return STDEXEC::__dependent_sender<_Sender>(); + return STDEXEC::completion_signatures(); } else { - return ::STDEXEC::__throw_compile_time_error<_FAILED_TO_FORM_COMPLETION_SIGNATURES, - ::STDEXEC::_WITH_PRETTY_SENDER_<_Sender>>(); + return STDEXEC::completion_signatures(); + } + }; + + template + static consteval auto __get_completion_signatures() + { + using __child_t = __child_of<_Sender>; + using __bouncer_t = schedule_result_t; + + STDEXEC_COMPLSIGS_LET(__completions, get_completion_signatures<__child_t, _Env...>()) + { + using __eptr_completion_t = set_error_t(std::exception_ptr); + constexpr auto __eptr_completion = (__eptr_completion_t *) nullptr; + constexpr auto __sigs = + exec::transform_completion_signatures(__completions, + __transform_values<__child_t>, + __transform_errors); + // The repeat_until sender is a dependent sender if one of the following is + // true: + // - the child sender is a dependent sender, or + // - the trampoline scheduler's sender is a dependent sender, or + // - sizeof...(_Env) == 0 and the child sender does not have a + // set_error(exception_ptr) completion. + constexpr bool __is_dependent = (sizeof...(_Env) == 0) + && (dependent_sender<__bouncer_t> + || !__sigs.__contains(__eptr_completion)); + if constexpr (__is_dependent) + { + return exec::throw_compile_time_error>(); + } + else + { + constexpr bool __has_nothrow_connect = + (__nothrow_connectable<__child_t, __receiver_archetype<_Env>> || ...); + constexpr auto __eptr_sigs = __eptr_completion_unless<__has_nothrow_connect>(); + constexpr auto __bouncer_sigs = exec::transform_completion_signatures( + get_completion_signatures<__bouncer_t, _Env...>(), + exec::ignore_completion()); // drop the set_value_t() completion from the + // trampoline scheduler. + + return exec::concat_completion_signatures(__sigs, __eptr_sigs, __bouncer_sigs); + } } }; static constexpr auto __connect = [](_Sender &&__sndr, _Receiver __rcvr) noexcept( - noexcept(__opstate(STDEXEC::__get<2>(__declval<_Sender>()), __declval<_Receiver>()))) + __nothrow_constructible_from<__opstate<__child_of<_Sender>, _Receiver>, + __child_of<_Sender>, + _Receiver>) { - return __opstate(STDEXEC::__get<2>(static_cast<_Sender &&>(__sndr)), - static_cast<_Receiver &&>(__rcvr)); + auto &[__tag, __ign, __child] = __sndr; + return __opstate(STDEXEC::__forward_like<_Sender>(__child), std::move(__rcvr)); }; }; diff --git a/include/exec/sequence.hpp b/include/exec/sequence.hpp index 925b679ab..75c4c5fb9 100644 --- a/include/exec/sequence.hpp +++ b/include/exec/sequence.hpp @@ -37,12 +37,14 @@ namespace experimental::execution { template STDEXEC_ATTRIBUTE(nodiscard, host, device) - constexpr auto operator()(Sender sndr) const -> Sender; + constexpr auto operator()(Sender sndr) const + noexcept(STDEXEC::__nothrow_move_constructible) -> Sender; template requires(sizeof...(Senders) > 1) STDEXEC_ATTRIBUTE(nodiscard, host, device) - constexpr auto operator()(Senders... sndrs) const -> _sndr; + constexpr auto operator()(Senders... sndrs) const + noexcept(STDEXEC::__nothrow_move_constructible) -> _sndr; }; template @@ -334,7 +336,8 @@ namespace experimental::execution template STDEXEC_ATTRIBUTE(host, device) - constexpr auto sequence_t::operator()(Sender sndr) const -> Sender + constexpr auto sequence_t::operator()(Sender sndr) const + noexcept(STDEXEC::__nothrow_move_constructible) -> Sender { return sndr; } @@ -342,7 +345,8 @@ namespace experimental::execution template requires(sizeof...(Senders) > 1) STDEXEC_ATTRIBUTE(host, device) - constexpr auto sequence_t::operator()(Senders... sndrs) const -> _sndr + constexpr auto sequence_t::operator()(Senders... sndrs) const + noexcept(STDEXEC::__nothrow_move_constructible) -> _sndr { return _sndr{{}, {}, {static_cast(sndrs)...}}; } diff --git a/include/stdexec/__detail/__receivers.hpp b/include/stdexec/__detail/__receivers.hpp index 8781277fc..167021c6c 100644 --- a/include/stdexec/__detail/__receivers.hpp +++ b/include/stdexec/__detail/__receivers.hpp @@ -250,8 +250,10 @@ namespace STDEXEC }; } - template - struct __receiver_archetype + // Used to test whether a sender has a nothrow connect to a receiver whose environment + // is _Env..., or if _Env... is empty (indicating that the sender is non-dependent), to + // a receiver with an arbitrary environment. + struct __receiver_archetype_base { using receiver_concept = receiver_t; @@ -267,9 +269,13 @@ namespace STDEXEC STDEXEC_ATTRIBUTE(host, device) constexpr void set_stopped() noexcept {} + }; + template + struct __receiver_archetype : __receiver_archetype_base + { STDEXEC_ATTRIBUTE(nodiscard, noreturn, host, device) - _Env get_env() const noexcept + auto get_env() const noexcept -> _Env { STDEXEC_ASSERT(false); STDEXEC_TERMINATE(); diff --git a/test/exec/test_repeat_until.cpp b/test/exec/test_repeat_until.cpp index 01950bbac..d47273841 100644 --- a/test/exec/test_repeat_until.cpp +++ b/test/exec/test_repeat_until.cpp @@ -317,7 +317,7 @@ namespace static_assert( std::same_as, ex::__detail::__not_a_variant>, "Expect no value completions"); - static_assert(ex::sender_of, + static_assert(ex::sender_of>, "Missing set_stopped_t() from upstream"); // operator| and sync_wait require valid completion signatures From 299ba2463d4948f8f167f685b710a007ff48b565 Mon Sep 17 00:00:00 2001 From: Eric Niebler Date: Tue, 3 Mar 2026 13:06:16 -0800 Subject: [PATCH 4/5] fix formatting --- include/exec/repeat_until.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/exec/repeat_until.hpp b/include/exec/repeat_until.hpp index ea9f344bb..c849e1e71 100644 --- a/include/exec/repeat_until.hpp +++ b/include/exec/repeat_until.hpp @@ -253,7 +253,7 @@ namespace experimental::execution template static consteval auto __get_completion_signatures() { - using __child_t = __child_of<_Sender>; + using __child_t = __child_of<_Sender>; using __bouncer_t = schedule_result_t; STDEXEC_COMPLSIGS_LET(__completions, get_completion_signatures<__child_t, _Env...>()) From 5eda5dca09f18eb4d38c594c2440857153094a85 Mon Sep 17 00:00:00 2001 From: Eric Niebler Date: Tue, 3 Mar 2026 15:17:38 -0800 Subject: [PATCH 5/5] fix for nvc++ --- include/exec/repeat_until.hpp | 25 +++++++++++-------- .../__transform_completion_signatures.hpp | 16 ++++++------ 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/include/exec/repeat_until.hpp b/include/exec/repeat_until.hpp index c849e1e71..497ad8f7f 100644 --- a/include/exec/repeat_until.hpp +++ b/include/exec/repeat_until.hpp @@ -28,6 +28,9 @@ #include #include +STDEXEC_PRAGMA_PUSH() +STDEXEC_PRAGMA_IGNORE_EDG(not_used_in_template_function_params) + namespace experimental::execution { struct _EXPECTING_A_SENDER_OF_ONE_VALUE_THAT_IS_CONVERTIBLE_TO_BOOL_; @@ -253,17 +256,17 @@ namespace experimental::execution template static consteval auto __get_completion_signatures() { - using __child_t = __child_of<_Sender>; - using __bouncer_t = schedule_result_t; - - STDEXEC_COMPLSIGS_LET(__completions, get_completion_signatures<__child_t, _Env...>()) + using __child_t = __child_of<_Sender>; + using __bouncer_t = schedule_result_t; + using __eptr_completion_t = set_error_t(std::exception_ptr); + constexpr auto __eptr_completion = (__eptr_completion_t *) nullptr; + + STDEXEC_COMPLSIGS_LET( + __sigs, + exec::transform_completion_signatures(get_completion_signatures<__child_t, _Env...>(), + __transform_values<__child_t>, + __transform_errors)) { - using __eptr_completion_t = set_error_t(std::exception_ptr); - constexpr auto __eptr_completion = (__eptr_completion_t *) nullptr; - constexpr auto __sigs = - exec::transform_completion_signatures(__completions, - __transform_values<__child_t>, - __transform_errors); // The repeat_until sender is a dependent sender if one of the following is // true: // - the child sender is a dependent sender, or @@ -393,3 +396,5 @@ namespace STDEXEC struct __sexpr_impl : exec::__repeat::__repeat_until_impl {}; } // namespace STDEXEC + +STDEXEC_PRAGMA_POP() diff --git a/include/stdexec/__detail/__transform_completion_signatures.hpp b/include/stdexec/__detail/__transform_completion_signatures.hpp index 002741dcf..3860a5e95 100644 --- a/include/stdexec/__detail/__transform_completion_signatures.hpp +++ b/include/stdexec/__detail/__transform_completion_signatures.hpp @@ -325,24 +325,24 @@ namespace STDEXEC template using __transform_result_t = decltype(__declval<_Fn>().template operator()<_Args...>()); - template + template [[nodiscard]] consteval auto - __transform_expr(_Fn const &__fn) -> __transform_result_t<_Fn const &, _SetTag, _Args...> + __transform_expr(_Fn const &__fn, int) -> __transform_result_t<_Fn const &, _Args...> { - return __fn.template operator()<_SetTag, _Args...>(); + return __fn.template operator()<_Args...>(); } template [[nodiscard]] - consteval auto __transform_expr(_Fn const &__fn) -> __call_result_t<_Fn const &> + consteval auto __transform_expr(_Fn const &__fn, long) -> __call_result_t<_Fn const &> { return __fn(); } template - using __transform_expr_t = decltype(__cmplsigs::__transform_expr<_Args...>( - __declval<_Fn const &>())); + using __transform_expr_t = + decltype(__cmplsigs::__transform_expr<_Args...>(__declval<_Fn const &>(), 0)); // transform_completion_signatures: template @@ -354,11 +354,11 @@ namespace STDEXEC using __completions_t = __transform_expr_t<_Fn, _Args...>; if constexpr (__well_formed_completions<__completions_t>) { - return __cmplsigs::__transform_expr<_Args...>(__fn); + return __cmplsigs::__transform_expr<_Args...>(__fn, 0); } else { - (void) __cmplsigs::__transform_expr<_Args...>(__fn); // potentially throwing + (void) __cmplsigs::__transform_expr<_Args...>(__fn, 0); // potentially throwing return STDEXEC::__throw_compile_time_error< _IN_TRANSFORM_COMPLETION_SIGNATURES_, _A_TRANSFORM_FUNCTION_RETURNED_A_TYPE_THAT_IS_NOT_A_COMPLETION_SIGNATURES_SPECIALIZATION_,