From 5539c6077d6141ef6ca7f0a94b058d88c7d840b1 Mon Sep 17 00:00:00 2001 From: Eric Niebler Date: Fri, 6 Mar 2026 10:44:12 -0800 Subject: [PATCH] re-express `starts_on` in terms of the `__sequence` algorithm --- include/exec/sequence.hpp | 355 +--------------- include/nvexec/stream/repeat_n.cuh | 2 +- include/stdexec/__detail/__sequence.hpp | 383 ++++++++++++++++++ include/stdexec/__detail/__starts_on.hpp | 18 +- include/stdexec/__detail/__variant.hpp | 4 + test/CMakeLists.txt | 1 + test/exec/CMakeLists.txt | 1 - .../algos/adaptors}/test_sequence.cpp | 0 8 files changed, 406 insertions(+), 358 deletions(-) create mode 100644 include/stdexec/__detail/__sequence.hpp rename test/{exec => stdexec/algos/adaptors}/test_sequence.cpp (100%) diff --git a/include/exec/sequence.hpp b/include/exec/sequence.hpp index 7c8365962..0626f4aef 100644 --- a/include/exec/sequence.hpp +++ b/include/exec/sequence.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 NVIDIA Corporation + * Copyright (c) 2026 NVIDIA Corporation * * Licensed under the Apache License Version 2.0 with LLVM Exceptions * (the "License"); you may not use this file except in compliance with @@ -15,361 +15,12 @@ */ #pragma once -#include "../stdexec/__detail/__tuple.hpp" -#include "../stdexec/__detail/__variant.hpp" -#include "../stdexec/execution.hpp" - -#include "completion_signatures.hpp" - -#include - -STDEXEC_PRAGMA_PUSH() -STDEXEC_PRAGMA_IGNORE_GNU("-Wmissing-braces") +#include "../stdexec/__detail/__sequence.hpp" namespace experimental::execution { - namespace _seq - { - template - struct _sndr; - - struct sequence_t - { - template - STDEXEC_ATTRIBUTE(nodiscard, host, device) - 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 - noexcept(STDEXEC::__nothrow_move_constructible) -> _sndr; - }; - - template - struct _opstate_base - { - template - STDEXEC_ATTRIBUTE(host, device) - constexpr void _set_value([[maybe_unused]] Args&&... args) noexcept - { - STDEXEC::set_value(static_cast(_rcvr), static_cast(args)...); - } - - STDEXEC_ATTRIBUTE(host, device) - constexpr void _start_next() noexcept - { - (*_start_next_)(this); - } - - Rcvr _rcvr; - void (*_start_next_)(_opstate_base*) noexcept = nullptr; - }; - - template - struct _rcvr_base - { - using receiver_concept = STDEXEC::receiver_t; - - template - STDEXEC_ATTRIBUTE(host, device) - constexpr void set_error(Error&& err) && noexcept - { - STDEXEC::set_error(static_cast(_opstate->_rcvr), static_cast(err)); - } - - STDEXEC_ATTRIBUTE(host, device) void set_stopped() && noexcept - { - STDEXEC::set_stopped(static_cast(_opstate->_rcvr)); - } - - // TODO: use the predecessor's completion scheduler as the current scheduler here. - STDEXEC_ATTRIBUTE(nodiscard, host, device) - constexpr auto get_env() const noexcept -> STDEXEC::env_of_t - { - return STDEXEC::get_env(_opstate->_rcvr); - } - - _opstate_base* _opstate; - }; - - template - struct _rcvr : _rcvr_base - { - using receiver_concept = STDEXEC::receiver_t; - - template - STDEXEC_ATTRIBUTE(always_inline, host, device) - constexpr void set_value(Args&&... args) && noexcept - { - if constexpr (IsLast) - { - this->_opstate->_set_value(static_cast(args)...); - } - else - { - this->_opstate->_start_next(); - } - } - }; - - template - struct __convert_tuple_fn - { - template - STDEXEC_ATTRIBUTE(host, device, always_inline) - constexpr _Tuple operator()(_Ts&&... __ts) const - noexcept(STDEXEC::__nothrow_constructible_from<_Tuple, _Ts...>) - { - return _Tuple{static_cast<_Ts&&>(__ts)...}; - } - }; - - template - struct _opstate; - - template - struct _opstate : _opstate_base - { - using operation_state_concept = STDEXEC::operation_state_t; - - // We will be connecting the first sender in the opstate constructor, so we don't need to - // store it in the opstate. The use of `STDEXEC::__ignore` causes the first sender to not - // be stored. - using _senders_tuple_t = STDEXEC::__tuple; - - template - using _rcvr_t = _seq::_rcvr; - - template - using _child_opstate_t = STDEXEC::connect_result_t>; - - using _mk_child_ops_variant_fn = - STDEXEC::__mzip_with2, STDEXEC::__qq>; - - using __is_last_mask_t = - STDEXEC::__mfill_c>; - - using _ops_variant_t = STDEXEC::__minvoke<_mk_child_ops_variant_fn, - STDEXEC::__tuple, - __is_last_mask_t>; - - template - STDEXEC_ATTRIBUTE(host, device) - constexpr explicit _opstate(Rcvr&& rcvr, CvSndrs&& sndrs) - noexcept(::STDEXEC::__nothrow_applicable<__convert_tuple_fn<_senders_tuple_t>, CvSndrs> - && ::STDEXEC::__nothrow_connectable<::STDEXEC::__tuple_element_t<0, CvSndrs>, - _rcvr_t>) - : _opstate_base{static_cast(rcvr)} - // move all but the first sender into the opstate: - , _sndrs{ - STDEXEC::__apply(__convert_tuple_fn<_senders_tuple_t>{}, static_cast(sndrs))} - { - // Below, it looks like we are using `sndrs` after it has been moved from. This is not the - // case. `sndrs` is moved into a tuple type that has `__ignore` for the first element. The - // result is that the first sender in `sndrs` is not moved from, but the rest are. - _ops.template __emplace_from<0>(STDEXEC::connect, - STDEXEC::__get<0>(static_cast(sndrs)), - _rcvr_t{this}); - } - - template - static constexpr void _start_next(_opstate_base* _self) noexcept - { - constexpr auto __nth = sizeof...(Senders) - Remaining; - auto* self = static_cast<_opstate*>(_self); - auto& sndr = STDEXEC::__get<__nth + 1>(self->_sndrs); - constexpr bool nothrow = - STDEXEC::__nothrow_connectable, - _rcvr_t>; - STDEXEC_TRY - { - auto& op = self->_ops.template __emplace_from<__nth + 1>(STDEXEC::connect, - std::move(sndr), - _rcvr_t{self}); - if constexpr (Remaining > 1) - { - self->_start_next_ = &_start_next; - } - STDEXEC::start(op); - } - STDEXEC_CATCH_ALL - { - if constexpr (nothrow) - { - STDEXEC::__std::unreachable(); - } - else - { - STDEXEC::set_error(static_cast(static_cast<_opstate*>(_self)->_rcvr), - std::current_exception()); - } - } - } - - STDEXEC_ATTRIBUTE(host, device) - constexpr void start() noexcept - { - if (sizeof...(Senders) != 0) - { - this->_start_next_ = &_start_next; - } - STDEXEC::start(STDEXEC::__var::__get<0>(_ops)); - } - - _senders_tuple_t _sndrs; - _ops_variant_t _ops{STDEXEC::__no_init}; - }; - - template - concept __has_eptr_completion = - STDEXEC::sender_in - && exec::transform_completion_signatures(STDEXEC::get_completion_signatures(), - exec::ignore_completion(), - exec::decay_arguments(), - exec::ignore_completion()) - .__contains(STDEXEC::__fn_ptr_t()); - - template - struct _sndr - { - using sender_concept = STDEXEC::sender_t; - - // Even without an Env, we can sometimes still determine the completion signatures - // of the sequence sender. If any of the child senders has a - // set_error(exception_ptr) completion, then the sequence sender has a - // set_error(exception_ptr) completion. We don't have to ask if any connect call - // throws. - template - requires(sizeof...(Env) > 0) - || __has_eptr_completion> - || (__has_eptr_completion || ...) - STDEXEC_ATTRIBUTE(host, device) - static consteval auto get_completion_signatures() - { - if constexpr (!STDEXEC::__decay_copyable) - { - return STDEXEC::__throw_compile_time_error< - STDEXEC::_SENDER_TYPE_IS_NOT_DECAY_COPYABLE_, - STDEXEC::_WITH_PRETTY_SENDER_<_sndr>>(); - } - else - { - using __env_t = STDEXEC::__mfront>; - using __rcvr_t = STDEXEC::__receiver_archetype<__env_t>; - constexpr bool __is_nothrow = (STDEXEC::__nothrow_connectable && ...); - - // The completions of the sequence sender are the error and stopped completions of all the - // child senders plus the value completions of the last child sender. - return exec::concat_completion_signatures( - exec::transform_completion_signatures( - STDEXEC::get_completion_signatures, Env...>(), - exec::ignore_completion()), - exec::transform_completion_signatures( - STDEXEC::get_completion_signatures(), - exec::ignore_completion())..., - STDEXEC::get_completion_signatures, Env...>(), - STDEXEC::__eptr_completion_unless_t>()); - } - } - - template - STDEXEC_ATTRIBUTE(host, device) - constexpr STDEXEC_EXPLICIT_THIS_BEGIN(auto connect)(this Self&& self, Rcvr rcvr) - noexcept(STDEXEC::__nothrow_constructible_from< - _opstate, Senders...>, - Rcvr, - decltype((static_cast(self)._sndrs))>) - { - return _opstate, Senders...>{ - static_cast(rcvr), - static_cast(self)._sndrs}; - } - STDEXEC_EXPLICIT_THIS_END(connect) - - template - STDEXEC_ATTRIBUTE(always_inline, host, device) - static constexpr auto&& static_get(Self&& self) noexcept - { - if constexpr (Index == 0) - { - return static_cast(self)._tag; - } - else if constexpr (Index == 1) - { - return static_cast(self)._ign; - } - else - { - return STDEXEC::__get(static_cast(self)._sndrs); - } - } - - template - STDEXEC_ATTRIBUTE(always_inline, host, device) - constexpr auto&& get() && noexcept - { - return static_get(static_cast<_sndr&&>(*this)); - } - - template - STDEXEC_ATTRIBUTE(always_inline, host, device) - constexpr auto&& get() & noexcept - { - return static_get(*this); - } - - template - STDEXEC_ATTRIBUTE(always_inline, host, device) - constexpr auto&& get() const & noexcept - { - return static_get(*this); - } - - STDEXEC_ATTRIBUTE(no_unique_address, maybe_unused) sequence_t _tag; - STDEXEC_ATTRIBUTE(no_unique_address, maybe_unused) STDEXEC::__ _ign; - STDEXEC::__tuple _sndrs; - }; - - template - STDEXEC_ATTRIBUTE(host, device) - constexpr auto sequence_t::operator()(Sender sndr) const - noexcept(STDEXEC::__nothrow_move_constructible) -> Sender - { - return sndr; - } - - template - requires(sizeof...(Senders) > 1) - STDEXEC_ATTRIBUTE(host, device) - constexpr auto sequence_t::operator()(Senders... sndrs) const - noexcept(STDEXEC::__nothrow_move_constructible) -> _sndr - { - return _sndr{{}, {}, {static_cast(sndrs)...}}; - } - } // namespace _seq - - using _seq::sequence_t; + using sequence_t = STDEXEC::__sequence_t; inline constexpr sequence_t sequence{}; } // namespace experimental::execution namespace exec = experimental::execution; - -namespace std -{ - template - struct tuple_size> - : std::integral_constant - {}; - - template - struct tuple_element> - { - using type = STDEXEC::__m_at_c; - }; -} // namespace std - -STDEXEC_PRAGMA_POP() diff --git a/include/nvexec/stream/repeat_n.cuh b/include/nvexec/stream/repeat_n.cuh index ca566dfac..e75f4894f 100644 --- a/include/nvexec/stream/repeat_n.cuh +++ b/include/nvexec/stream/repeat_n.cuh @@ -124,7 +124,7 @@ namespace nv::execution::_strm } STDEXEC_CATCH_ALL { - this->propagate_completion_signal(Tag{}, std::current_exception()); + this->propagate_completion_signal(STDEXEC::set_error, std::current_exception()); } } diff --git a/include/stdexec/__detail/__sequence.hpp b/include/stdexec/__detail/__sequence.hpp new file mode 100644 index 000000000..7c1b0f404 --- /dev/null +++ b/include/stdexec/__detail/__sequence.hpp @@ -0,0 +1,383 @@ +/* + * Copyright (c) 2024 NVIDIA Corporation + * + * Licensed under the Apache License Version 2.0 with LLVM Exceptions + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://llvm.org/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include "__concepts.hpp" +#include "__connect.hpp" +#include "__receivers.hpp" +#include "__transform_completion_signatures.hpp" +#include "__tuple.hpp" +#include "__variant.hpp" + +#include + +STDEXEC_PRAGMA_PUSH() +STDEXEC_PRAGMA_IGNORE_GNU("-Wmissing-braces") + +namespace STDEXEC +{ + namespace __seq + { + template + struct __sndr; + } // namespace __seq + + struct __sequence_t + { + template + STDEXEC_ATTRIBUTE(nodiscard, host, device) + constexpr auto operator()(_Sender __sndr) const + noexcept(STDEXEC::__nothrow_move_constructible<_Sender>) -> _Sender; + + template + requires(sizeof...(_Senders) > 1) + STDEXEC_ATTRIBUTE(nodiscard, host, device) + constexpr auto operator()(_Senders... sndrs) const + noexcept(STDEXEC::__nothrow_move_constructible<_Senders...>) -> __seq::__sndr<_Senders...>; + }; + + namespace __seq + { + template + struct __opstate_base + { + template + STDEXEC_ATTRIBUTE(host, device) + constexpr void __set_value([[maybe_unused]] Args&&... args) noexcept + { + STDEXEC::set_value(static_cast<_Receiver&&>(__rcvr_), static_cast(args)...); + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr void __start_next() noexcept + { + (*__start_next_)(this); + } + + _Receiver __rcvr_; + void (*__start_next_)(__opstate_base*) noexcept = nullptr; + }; + + template + struct __rcvr_base + { + using receiver_concept = STDEXEC::receiver_t; + + template + STDEXEC_ATTRIBUTE(host, device) + constexpr void set_error(_Error&& err) && noexcept + { + STDEXEC::set_error(static_cast<_Receiver&&>(__opstate_->__rcvr_), + static_cast<_Error&&>(err)); + } + + STDEXEC_ATTRIBUTE(host, device) void set_stopped() && noexcept + { + STDEXEC::set_stopped(static_cast<_Receiver&&>(__opstate_->__rcvr_)); + } + + // TODO: use the predecessor's completion scheduler as the current scheduler here. + STDEXEC_ATTRIBUTE(nodiscard, host, device) + constexpr auto get_env() const noexcept -> STDEXEC::env_of_t<_Receiver> + { + return STDEXEC::get_env(__opstate_->__rcvr_); + } + + __opstate_base<_Receiver>* __opstate_; + }; + + template + struct __rcvr : __rcvr_base<_Receiver> + { + using receiver_concept = STDEXEC::receiver_t; + + template + STDEXEC_ATTRIBUTE(always_inline, host, device) + constexpr void set_value(_Args&&... __args) && noexcept + { + if constexpr (_IsLast) + { + this->__opstate_->__set_value(static_cast<_Args&&>(__args)...); + } + else + { + this->__opstate_->__start_next(); + } + } + }; + + template + struct __convert_tuple_fn + { + template + STDEXEC_ATTRIBUTE(host, device, always_inline) + constexpr _Tuple operator()(_Ts&&... __ts) const + noexcept(STDEXEC::__nothrow_constructible_from<_Tuple, _Ts...>) + { + return _Tuple{static_cast<_Ts&&>(__ts)...}; + } + }; + + template + struct __opstate; + + template + struct __opstate<_Receiver, _CvSender0, _Senders...> : __opstate_base<_Receiver> + { + using operation_state_concept = STDEXEC::operation_state_t; + + // We will be connecting the first sender in the opstate constructor, so we don't need to + // store it in the opstate. The use of `STDEXEC::__ignore` causes the first sender to not + // be stored. + using __senders_t = STDEXEC::__tuple; + + template + using __rcvr_t = __seq::__rcvr<_Receiver, IsLast>; + + template + using __child_opstate_t = STDEXEC::connect_result_t<_Sender, __rcvr_t>; + + using __mk_child_ops_variant_fn = + STDEXEC::__mzip_with2, STDEXEC::__qq>; + + using __is_last_mask_t = + STDEXEC::__mfill_c>; + + using __ops_variant_t = STDEXEC::__minvoke<__mk_child_ops_variant_fn, + STDEXEC::__tuple<_CvSender0, _Senders...>, + __is_last_mask_t>; + + template + STDEXEC_ATTRIBUTE(host, device) + constexpr explicit __opstate(_Receiver&& __rcvr, _CvSenders&& __sndrs) + noexcept(::STDEXEC::__nothrow_applicable<__convert_tuple_fn<__senders_t>, _CvSenders> + && ::STDEXEC::__nothrow_connectable<::STDEXEC::__tuple_element_t<0, _CvSenders>, + __rcvr_t>) + : __opstate_base<_Receiver>{static_cast<_Receiver&&>(__rcvr)} + // move all but the first sender into the opstate: + , __sndrs_{ + STDEXEC::__apply(__convert_tuple_fn<__senders_t>{}, static_cast<_CvSenders&&>(__sndrs))} + { + // Below, it looks like we are using `__sndrs` after it has been moved from. This + // is not the case. `__sndrs` is moved into a tuple type that has `__ignore` for + // the first element. The result is that the first sender in `__sndrs` is not + // moved from, but the rest are. + __ops_.template __emplace_from<0>(STDEXEC::connect, + STDEXEC::__get<0>(static_cast<_CvSenders&&>(__sndrs)), + __rcvr_t{this}); + } + + template + static constexpr void __start_next(__opstate_base<_Receiver>* __self_) noexcept + { + constexpr auto __nth = sizeof...(_Senders) - _Remaining; + auto* __self = static_cast<__opstate*>(__self_); + auto& __sndr = STDEXEC::__get<__nth + 1>(__self->__sndrs_); + constexpr bool __is_nothrow = + STDEXEC::__nothrow_connectable, + __rcvr_t<_Remaining == 1>>; + STDEXEC_TRY + { + auto& __op = + __self->__ops_.template __emplace_from<__nth + 1>(STDEXEC::connect, + std::move(__sndr), + __rcvr_t<(_Remaining == 1)>{__self}); + if constexpr (_Remaining > 1) + { + __self->__start_next_ = &__start_next<_Remaining - 1>; + } + STDEXEC::start(__op); + } + STDEXEC_CATCH_ALL + { + if constexpr (__is_nothrow) + { + STDEXEC::__std::unreachable(); + } + else + { + STDEXEC::set_error(static_cast<_Receiver&&>(static_cast<__opstate*>(__self_)->__rcvr_), + std::current_exception()); + } + } + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr void start() noexcept + { + if (sizeof...(_Senders) != 0) + { + this->__start_next_ = &__start_next; + } + STDEXEC::start(STDEXEC::__var::__get<0>(__ops_)); + } + + __senders_t __sndrs_; + __ops_variant_t __ops_{STDEXEC::__no_init}; + }; + + template + concept __has_eptr_completion = + STDEXEC::sender_in<_Sender> + && __transform_completion_signatures(STDEXEC::get_completion_signatures<_Sender>(), + __ignore_completion(), + __decay_arguments(), + __ignore_completion()) + .__contains(STDEXEC::__fn_ptr_t()); + + template + struct __sndr<_Sender0, _Senders...> + { + using sender_concept = STDEXEC::sender_t; + using __senders_t = STDEXEC::__tuple<_Sender0, _Senders...>; + + // Even without an Env, we can sometimes still determine the completion signatures + // of the sequence sender. If any of the child senders has a + // set_error(exception_ptr) completion, then the sequence sender has a + // set_error(exception_ptr) completion. We don't have to ask if any connect call + // throws. + template + requires(sizeof...(_Env) > 0) + || __has_eptr_completion> + || (__has_eptr_completion<_Senders> || ...) + STDEXEC_ATTRIBUTE(host, device) + static consteval auto get_completion_signatures() + { + if constexpr (!STDEXEC::__decay_copyable<_Self>) + { + return STDEXEC::__throw_compile_time_error< + STDEXEC::_SENDER_TYPE_IS_NOT_DECAY_COPYABLE_, + STDEXEC::_WITH_PRETTY_SENDER_<__sndr<_Sender0, _Senders...>>>(); + } + else + { + using __env_t = STDEXEC::__mfront<_Env..., STDEXEC::env<>>; + using __rcvr_t = STDEXEC::__receiver_archetype<__env_t>; + constexpr bool __is_nothrow = (STDEXEC::__nothrow_connectable<_Senders, __rcvr_t> && ...); + + // The completions of the sequence sender are the error and stopped completions of all the + // child senders plus the value completions of the last child sender. + return __concat_completion_signatures( + __transform_completion_signatures( + STDEXEC::get_completion_signatures, + _Env...>(), + __ignore_completion()), + __transform_completion_signatures( + STDEXEC::get_completion_signatures<_Senders, _Env...>(), + __ignore_completion())..., + STDEXEC::get_completion_signatures, _Env...>(), + STDEXEC::__eptr_completion_unless_t<__mbool<__is_nothrow>>()); + } + } + + template + STDEXEC_ATTRIBUTE(host, device) + constexpr STDEXEC_EXPLICIT_THIS_BEGIN(auto connect)(this _Self&& __self, _Receiver __rcvr) + noexcept(STDEXEC::__nothrow_constructible_from< + __opstate<_Receiver, STDEXEC::__copy_cvref_t<_Self, _Sender0>, _Senders...>, + _Receiver, + __copy_cvref_t<_Self, __senders_t>>) + { + return __opstate<_Receiver, STDEXEC::__copy_cvref_t<_Self, _Sender0>, _Senders...>{ + static_cast<_Receiver&&>(__rcvr), + static_cast<_Self&&>(__self).__sndrs_}; + } + STDEXEC_EXPLICIT_THIS_END(connect) + + template + STDEXEC_ATTRIBUTE(always_inline, host, device) + static constexpr auto&& __static_get(_Self&& __self) noexcept + { + if constexpr (_Index == 0) + { + return static_cast<_Self&&>(__self).__tag_; + } + else if constexpr (_Index == 1) + { + return static_cast<_Self&&>(__self).__ign_; + } + else + { + return STDEXEC::__get<_Index - 2>(static_cast<_Self&&>(__self).__sndrs_); + } + } + + template + STDEXEC_ATTRIBUTE(always_inline, host, device) + constexpr auto&& get() && noexcept + { + return __static_get<_Index>(static_cast<__sndr&&>(*this)); + } + + template + STDEXEC_ATTRIBUTE(always_inline, host, device) + constexpr auto&& get() & noexcept + { + return __static_get<_Index>(*this); + } + + template + STDEXEC_ATTRIBUTE(always_inline, host, device) + constexpr auto&& get() const & noexcept + { + return __static_get<_Index>(*this); + } + + STDEXEC_ATTRIBUTE(no_unique_address, maybe_unused) __sequence_t __tag_; + STDEXEC_ATTRIBUTE(no_unique_address, maybe_unused) STDEXEC::__ __ign_; + __senders_t __sndrs_; + }; + } // namespace __seq + + template + STDEXEC_ATTRIBUTE(host, device) + constexpr auto __sequence_t::operator()(_Sender __sndr) const + noexcept(STDEXEC::__nothrow_move_constructible<_Sender>) -> _Sender + { + return __sndr; + } + + template + requires(sizeof...(_Senders) > 1) + STDEXEC_ATTRIBUTE(host, device) + constexpr auto __sequence_t::operator()(_Senders... sndrs) const + noexcept(STDEXEC::__nothrow_move_constructible<_Senders...>) -> __seq::__sndr<_Senders...> + { + return __seq::__sndr<_Senders...>{{}, {}, {static_cast<_Senders&&>(sndrs)...}}; + } + + inline constexpr __sequence_t __sequence{}; +} // namespace STDEXEC + +namespace exec = experimental::execution; + +namespace std +{ + template + struct tuple_size> + : std::integral_constant + {}; + + template + struct tuple_element> + { + using type = STDEXEC::__m_at_c; + }; +} // namespace std + +STDEXEC_PRAGMA_POP() diff --git a/include/stdexec/__detail/__starts_on.hpp b/include/stdexec/__detail/__starts_on.hpp index f482cf8c9..8ea88d6cf 100644 --- a/include/stdexec/__detail/__starts_on.hpp +++ b/include/stdexec/__detail/__starts_on.hpp @@ -18,16 +18,17 @@ #include "__execution_fwd.hpp" // include these after __execution_fwd.hpp -#include "../functional.hpp" #include "__completion_signatures_of.hpp" #include "__concepts.hpp" #include "__diagnostics.hpp" #include "__domain.hpp" #include "__env.hpp" #include "__just.hpp" -#include "__let.hpp" #include "__schedulers.hpp" +#include "__senders.hpp" +#include "__sequence.hpp" #include "__utility.hpp" +#include "__write_env.hpp" namespace STDEXEC { @@ -47,8 +48,17 @@ namespace STDEXEC static constexpr auto transform_sender(set_value_t, _Sender&& __sndr, __ignore) { auto& [__tag, __sched, __child] = __sndr; - return let_value(continues_on(just(), __sched), - __always{STDEXEC::__forward_like<_Sender>(__child)}); + // FUTURE: once __sequence updates the environment of the receiver connected to the + // nth+1 sender to contain the completion scheduler of the nth sender, we can remove + // the write_env here. + auto __sndr2 = write_env(STDEXEC::__forward_like<_Sender>(__child), __sched_env(__sched)); + // NOT TO SPEC: the specification requires that this be implemented in terms of + // let_value(schedule(sch), []{ return child; }), but that implementation + // is inefficient on the GPU. We could customize starts_on for the GPU to use this + // implementation, but this is a good change to make for all platforms since it + // avoids unnecessarily making the child sender dependent on the completion of the + // schedule operation. + return __sequence(continues_on(just(), __sched), std::move(__sndr2)); } template diff --git a/include/stdexec/__detail/__variant.hpp b/include/stdexec/__detail/__variant.hpp index 601fbe5a6..c2cd610aa 100644 --- a/include/stdexec/__detail/__variant.hpp +++ b/include/stdexec/__detail/__variant.hpp @@ -169,6 +169,7 @@ namespace STDEXEC STDEXEC_ATTRIBUTE(host, device) constexpr __variant(__variant &&__other) noexcept(__nothrow_move_constructible<_Ts...>) + requires(std::move_constructible<_Ts> && ...) { if (!__other.__is_valueless()) { @@ -178,6 +179,7 @@ namespace STDEXEC STDEXEC_ATTRIBUTE(host, device) constexpr __variant(__variant const &__other) noexcept(__nothrow_copy_constructible<_Ts...>) + requires(std::copy_constructible<_Ts> && ...) { if (!__other.__is_valueless()) { @@ -193,6 +195,7 @@ namespace STDEXEC STDEXEC_ATTRIBUTE(host, device) constexpr __variant &operator=(__variant &&__other) noexcept + requires(std::move_constructible<_Ts> && ...) { if (this != &__other) { @@ -207,6 +210,7 @@ namespace STDEXEC STDEXEC_ATTRIBUTE(host, device) constexpr __variant &operator=(__variant const &__other) + requires(std::copy_constructible<_Ts> && ...) { if (this != &__other) { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 2a8ff9a9d..7d0eb9dfb 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -54,6 +54,7 @@ set(stdexec_test_sources stdexec/algos/adaptors/test_when_all.cpp stdexec/algos/adaptors/test_transfer_when_all.cpp stdexec/algos/adaptors/test_into_variant.cpp + stdexec/algos/adaptors/test_sequence.cpp stdexec/algos/adaptors/test_stopped_as_optional.cpp stdexec/algos/adaptors/test_stopped_as_error.cpp stdexec/algos/adaptors/test_write_env.cpp diff --git a/test/exec/CMakeLists.txt b/test/exec/CMakeLists.txt index 5bf920ce7..316dc82a8 100644 --- a/test/exec/CMakeLists.txt +++ b/test/exec/CMakeLists.txt @@ -41,7 +41,6 @@ set(exec_test_sources $<$:test_windows_thread_pool_context.cpp> test_trampoline_scheduler.cpp test_sequence_senders.cpp - test_sequence.cpp test_static_thread_pool.cpp test_just_from.cpp test_fork_join.cpp diff --git a/test/exec/test_sequence.cpp b/test/stdexec/algos/adaptors/test_sequence.cpp similarity index 100% rename from test/exec/test_sequence.cpp rename to test/stdexec/algos/adaptors/test_sequence.cpp