diff --git a/include/exec/sequence_senders.hpp b/include/exec/sequence_senders.hpp index bdef7457e..17dd704a5 100644 --- a/include/exec/sequence_senders.hpp +++ b/include/exec/sequence_senders.hpp @@ -225,6 +225,100 @@ namespace experimental::execution template inline constexpr bool enable_sequence_sender = __enable_sequence_sender<_Sequence>; + // A sequence-adaptor traits customization point. The set of sender adaptors + // is open, so the sequence machinery cannot know about them individually. + // Instead, an adaptor that is transparent to sequence semantics — one that + // forwards `set_next` and all completions through to a single child sender, + // possibly adjusting the environment that child sees — declares that here, + // and `get_item_types` and `subscribe` handle such senders generically. See + // issue #2053. + template + struct __sequence_adaptor_traits + { + // Adaptors default to opaque: a wrapper around a sequence sender is not + // itself a sequence sender. + static constexpr bool __transparent = false; + }; + + // The default child-environment transformation for transparent adaptors: + // the child is queried in the same environment as the adaptor itself. + struct __identity_child_env_fn + { + template + auto operator()(_Env __env, _Data const &) const noexcept -> _Env + { + return __env; + } + }; + + // `write_env` only augments the environment seen by its children; it + // neither adds nor removes sequence items, and it computes completion + // signatures from its child's signatures in the joined environment. Mark it + // transparent with the matching environment transformation. See issue + // #2053. + template <> + struct __sequence_adaptor_traits + { + static constexpr bool __transparent = true; + + template + struct __child_env_fn + { + auto operator()(_Env __env, _Data const & __data) const noexcept + -> STDEXEC::__join_env_t<_Data const &, _Env> + { + return STDEXEC::__env::__join(__data, static_cast<_Env&&>(__env)); + } + }; + }; + + // True when _Sequence is a sender expression whose adaptor traits (see + // `__sequence_adaptor_traits` above) mark it as transparent to sequence + // sender semantics. + template + concept __transparent_sequence_adaptor = + STDEXEC::__minvocable_q> + && __sequence_adaptor_traits>>::__transparent; + + // The function object that transforms the environment of a transparent + // sequence adaptor: the adaptor's traits may define a nested + // `__child_env_fn<_Env, _Data>` function object type; otherwise, the + // transformation is the identity. + template + consteval auto __adaptor_child_env_fn() + { + if constexpr (requires { + typename __sequence_adaptor_traits<_Tag>::template __child_env_fn<_Env, _Data>; + }) + { + return typename __sequence_adaptor_traits<_Tag>::template __child_env_fn<_Env, _Data>{}; + } + else + { + return __identity_child_env_fn{}; + } + } + + template + using __adaptor_child_env_fn_t = decltype(__adaptor_child_env_fn<_Tag, _Env, _Data>()); + + // The environment in which the child of a transparent sequence adaptor is + // queried, given the adaptor's tag, the environment _Env in which the + // adaptor itself is queried, and the adaptor's data. + template + using __adaptor_child_env_t = + STDEXEC::__call_result_t<__adaptor_child_env_fn_t<_Tag, _Env, _Data>, _Env, _Data const &>; + + // Sender expressions whose adaptor traits mark them transparent (see + // `__sequence_adaptor_traits` above) count as sequence senders: they + // forward `set_next` and all completions through to a single child sender. + // Without this, wrapping a sequence sender in a transparent adaptor (e.g. + // `write_env`) would hide the sequence semantics from downstream + // sequence-aware algorithms. See issue #2053. + template + requires __transparent_sequence_adaptor> + inline constexpr bool enable_sequence_sender> = true; + STDEXEC_MODULE_EXPORT template struct item_types @@ -322,6 +416,26 @@ namespace experimental::execution { return STDEXEC_REMOVE_REFERENCE(_Sequence)::template get_item_types<_Sequence>(); } + else if constexpr (__transparent_sequence_adaptor<_Sequence>) + { + // A transparent sequence adaptor (see `__sequence_adaptor_traits` + // above) is unwrapped one layer at a time: its item types are the + // item types of its child, computed in the transformed environment + // the adaptor presents to its child. + using __tag_t = STDEXEC::tag_of_t>; + using __data_t = STDEXEC::__decay_t>; + if constexpr (sizeof...(_Env) == 0) + { + return __get_item_types_helper< + STDEXEC::__child_of<_Sequence>, + __adaptor_child_env_t<__tag_t, STDEXEC::env<>, __data_t>>(); + } + else + { + return __get_item_types_helper, + __adaptor_child_env_t<__tag_t, _Env, __data_t>...>(); + } + } else if constexpr (sender_in<_Sequence, _Env...> && !enable_sequence_sender>) { @@ -767,6 +881,53 @@ namespace experimental::execution using __subscribe_static_member_result_t = decltype(STDEXEC_REMOVE_REFERENCE( _Sequence)::__static_subscribe(__declval<_Sequence>(), __declval<_Receiver>())); + // Receiver wrapper used to subscribe to the child of a transparent + // sequence adaptor (see `__sequence_adaptor_traits` above): it forwards + // all completions and `set_next` calls to the wrapped receiver, but + // presents the adaptor's transformed environment joined with (and + // shadowing) the wrapped receiver's environment. This mirrors the + // environment the adaptor's own operation state would present to its + // child when connecting. + template + struct __adaptor_rcvr + { + using receiver_concept = STDEXEC::receiver_tag; + + template + auto set_next(_Item&& __item) & noexcept(__nothrow_callable) + -> next_sender_of_t<_Receiver, _Item> + { + return exec::set_next(__rcvr_, static_cast<_Item&&>(__item)); + } + + void set_value() noexcept + { + STDEXEC::set_value(static_cast<_Receiver&&>(__rcvr_)); + } + + template + void set_error(_Error&& __error) noexcept + { + STDEXEC::set_error(static_cast<_Receiver&&>(__rcvr_), static_cast<_Error&&>(__error)); + } + + void set_stopped() noexcept + requires __callable + { + STDEXEC::set_stopped(static_cast<_Receiver&&>(__rcvr_)); + } + + auto get_env() const noexcept -> _Env const & + { + return __env_; + } + + STDEXEC_IMMOVABLE_NO_UNIQUE_ADDRESS + _Receiver __rcvr_; + STDEXEC_IMMOVABLE_NO_UNIQUE_ADDRESS + _Env __env_; + }; + template concept __subscribable_with_static_member = __minvocable_q<__subscribe_static_member_result_t, _Sequence, _Receiver>; @@ -835,6 +996,23 @@ namespace experimental::execution __stopped_means_break<_Receiver>>; return __declfn<__result_t, __nothrow_subscribe && __nothrow_tfx_seq>(); } + else if constexpr (__transparent_sequence_adaptor<__tfx_seq_t>) + { + // A transparent sequence adaptor (see `__sequence_adaptor_traits` + // above) is unwrapped one layer at a time: subscribe to its child + // with a receiver that presents the adaptor's transformed + // environment (joined with, and shadowing, the receiver's own + // environment). See issue #2053. + using __tag_t = STDEXEC::tag_of_t>; + using __data_t = __decay_t<__data_of<__tfx_seq_t>>; + using __child_t = __child_of<__tfx_seq_t>; + using __child_env_t = __adaptor_child_env_t<__tag_t, env_of_t<_Receiver>, __data_t>; + using __rcvr_t = __adaptor_rcvr<_Receiver, __child_env_t>; + using __result_t = STDEXEC::__call_result_t; + __check_operation_state<__result_t>(); + constexpr bool __nothrow_subscribe = __nothrow_callable; + return __declfn<__result_t, __nothrow_subscribe && __nothrow_tfx_seq>(); + } else if constexpr (__subscribable_with_static_member<__tfx_seq_t, _Receiver>) { using __result_t = decltype(STDEXEC_REMOVE_REFERENCE( @@ -889,6 +1067,25 @@ namespace experimental::execution __stopped_means_break<_Receiver>{ static_cast<_Receiver&&>(__rcvr)}); } + else if constexpr (__transparent_sequence_adaptor<__tfx_seq_t>) + { + // Transparent sequence adaptors (see `__sequence_adaptor_traits` + // above) are unwrapped one layer at a time; see the matching branch + // in __get_declfn above and issue #2053. + auto& [__tag, __data, __child] = __tfx_seq; + (void) __tag; + using __tag_t = STDEXEC::tag_of_t>; + using __data_t = __decay_t; + using __child_env_t = __adaptor_child_env_t<__tag_t, env_of_t<_Receiver>, __data_t>; + using __xform_t = __adaptor_child_env_fn_t<__tag_t, env_of_t<_Receiver>, __data_t>; + // Recurse into the CPO with the unwrapped child; each layer of + // adaptor nesting is unwrapped one at a time. + return (*this)(STDEXEC::__forward_like<__tfx_seq_t>(__child), + __adaptor_rcvr<_Receiver, __child_env_t>{ + static_cast<_Receiver&&>(__rcvr), + __xform_t{}(static_cast(__env), + STDEXEC::__forward_like(__data))}); + } else if constexpr (__subscribable_with_static_member<__tfx_seq_t, _Receiver>) { // NOLINT(bugprone-branch-clone) return __tfx_seq.__static_subscribe(static_cast<__tfx_seq_t&&>(__tfx_seq), diff --git a/test/exec/CMakeLists.txt b/test/exec/CMakeLists.txt index 667bdedb6..8b24f960b 100644 --- a/test/exec/CMakeLists.txt +++ b/test/exec/CMakeLists.txt @@ -57,6 +57,7 @@ set(exec_test_sources sequence/test_empty_sequence.cpp sequence/test_ignore_all_values.cpp sequence/test_iterate.cpp + sequence/test_write_env_sequence.cpp sequence/test_transform_each.cpp sequence/test_merge.cpp sequence/test_sequence_any_sender.cpp diff --git a/test/exec/sequence/test_write_env_sequence.cpp b/test/exec/sequence/test_write_env_sequence.cpp new file mode 100644 index 000000000..9b8aea31d --- /dev/null +++ b/test/exec/sequence/test_write_env_sequence.cpp @@ -0,0 +1,153 @@ +/* + * 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 + * 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. + */ + +// Regression tests for issue #2053: `write_env` must be transparent to +// sequence sender semantics. Wrapping a sequence sender in `write_env` (or in +// several stacked `write_env` layers) must preserve the item types, and items +// must flow through the wrapper unchanged at runtime. + +#include "exec/sequence/transform_each.hpp" + +#include "exec/sequence/ignore_all_values.hpp" +#include "exec/sequence_senders.hpp" +#include + +#include +#include +#include + +#include + +namespace +{ + // A minimal sequence sender that produces a single `just(42)` item. + template + struct single_item_sequence + { + using sender_concept = exec::sequence_sender_tag; + using item_types = exec::item_types; + using completion_signatures = + STDEXEC::completion_signatures; + + template + struct op + { + using operation_state_concept = STDEXEC::operation_state_t; + Rcvr rcvr_; + + void start() noexcept + { + auto next = exec::set_next(rcvr_, STDEXEC::just(Value)); + auto item_op = STDEXEC::connect(std::move(next), std::move(rcvr_)); + STDEXEC::start(item_op); + } + }; + + template + auto subscribe(Rcvr rcvr) const -> op + { + return op{static_cast(rcvr)}; + } + }; + + TEST_CASE("write_env is transparent to sequence senders - item types", "[sequence][write_env]") + { + using wrapper_t = STDEXEC::__decay_t{}, + STDEXEC::env<>{}))>; + using items_t = decltype(exec::get_item_types>()); + static_assert(STDEXEC::__same_as>); + } + + TEST_CASE("stacked write_env is transparent to sequence senders", "[sequence][write_env]") + { + using wrapper_t = STDEXEC::__decay_t< + decltype(STDEXEC::write_env(STDEXEC::write_env(single_item_sequence<42>{}, STDEXEC::env<>{}), + STDEXEC::env<>{}))>; + using items_t = decltype(exec::get_item_types>()); + static_assert(STDEXEC::__same_as>); + } + + TEST_CASE("write_env preserves sequence item flow", "[sequence][write_env]") + { + int value = 0; + STDEXEC::sync_wait(STDEXEC::write_env(single_item_sequence<42>{}, STDEXEC::env<>{}) + | exec::transform_each(STDEXEC::then([&](int v) { value = v; })) + | exec::ignore_all_values()); + CHECK(value == 42); + } + + TEST_CASE("stacked write_env preserves sequence item flow", "[sequence][write_env]") + { + int value = 0; + STDEXEC::sync_wait( + STDEXEC::write_env(STDEXEC::write_env(single_item_sequence<42>{}, STDEXEC::env<>{}), + STDEXEC::env<>{}) + | exec::transform_each(STDEXEC::then([&](int v) { value = v; })) | exec::ignore_all_values()); + CHECK(value == 42); + } + + // A user-defined adaptor, to demonstrate that the transparent-adaptor + // customization point is open: any single-child adaptor can declare itself + // transparent to sequence sender semantics by specializing + // `__sequence_adaptor_traits` for its tag, without any changes to the + // sequence machinery. See issue #2053. + struct my_adapt_t + {}; + + template + auto my_adapt(Sndr&& sndr) + { + return STDEXEC::__make_sexpr(STDEXEC::env<>{}, static_cast(sndr)); + } + + struct my_adapt_impl : STDEXEC::__sexpr_defaults + { + template + static consteval auto __get_completion_signatures() + { + return STDEXEC::get_completion_signatures, Env...>(); + } + }; +} // namespace + +template <> +struct STDEXEC::__sexpr_impl : my_adapt_impl +{}; + +template <> +struct experimental::execution::__sequence_adaptor_traits +{ + static constexpr bool __transparent = true; +}; + +namespace +{ + TEST_CASE("a custom adaptor can declare itself transparent", "[sequence][write_env]") + { + using wrapper_t = STDEXEC::__decay_t{}))>; + using items_t = decltype(exec::get_item_types>()); + static_assert(STDEXEC::__same_as>); + } + + TEST_CASE("a custom transparent adaptor preserves sequence item flow", "[sequence][write_env]") + { + int value = 0; + STDEXEC::sync_wait(my_adapt(single_item_sequence<42>{}) + | exec::transform_each(STDEXEC::then([&](int v) { value = v; })) + | exec::ignore_all_values()); + CHECK(value == 42); + } +} // namespace