-
Notifications
You must be signed in to change notification settings - Fork 274
fix(exec): make write_env transparent to sequence sender semantics #2294
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
290b7fb
bb40ed0
3b2abb0
7140398
fbf50b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -225,6 +225,100 @@ namespace experimental::execution | |
| template <class _Sequence> | ||
| 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 <class _Tag> | ||
| 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 <class _Env, class _Data> | ||
| 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<STDEXEC::__write_env_t> | ||
| { | ||
| static constexpr bool __transparent = true; | ||
|
|
||
| template <class _Env, class _Data> | ||
| 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 <class _Sequence> | ||
| concept __transparent_sequence_adaptor = | ||
| STDEXEC::__minvocable_q<STDEXEC::tag_of_t, STDEXEC::__decay_t<_Sequence>> | ||
| && __sequence_adaptor_traits<STDEXEC::tag_of_t<STDEXEC::__decay_t<_Sequence>>>::__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 <class _Tag, class _Env, class _Data> | ||
| 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 <class _Tag, class _Env, class _Data> | ||
| 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 <class _Tag, class _Env, class _Data> | ||
| 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 <auto _DescriptorFn> | ||
| requires __transparent_sequence_adaptor<STDEXEC::__sexpr<_DescriptorFn>> | ||
| inline constexpr bool enable_sequence_sender<STDEXEC::__sexpr<_DescriptorFn>> = true; | ||
|
|
||
| STDEXEC_MODULE_EXPORT | ||
| template <class... _Senders> | ||
| 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<STDEXEC::__decay_t<_Sequence>>; | ||
| using __data_t = STDEXEC::__decay_t<STDEXEC::__data_of<_Sequence>>; | ||
| 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<STDEXEC::__child_of<_Sequence>, | ||
| __adaptor_child_env_t<__tag_t, _Env, __data_t>...>(); | ||
| } | ||
| } | ||
| else if constexpr (sender_in<_Sequence, _Env...> | ||
| && !enable_sequence_sender<STDEXEC::__decay_t<_Sequence>>) | ||
| { | ||
|
|
@@ -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 <class _Receiver, class _Env> | ||
| struct __adaptor_rcvr | ||
| { | ||
| using receiver_concept = STDEXEC::receiver_tag; | ||
|
|
||
| template <class _Item> | ||
| auto set_next(_Item&& __item) & noexcept(__nothrow_callable<set_next_t, _Receiver&, _Item>) | ||
| -> 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 <class _Error> | ||
| void set_error(_Error&& __error) noexcept | ||
| { | ||
| STDEXEC::set_error(static_cast<_Receiver&&>(__rcvr_), static_cast<_Error&&>(__error)); | ||
| } | ||
|
|
||
| void set_stopped() noexcept | ||
| requires __callable<set_stopped_t, _Receiver> | ||
| { | ||
| 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 <class _Sequence, class _Receiver> | ||
| 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<STDEXEC::__decay_t<__tfx_seq_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<subscribe_t, __child_t, __rcvr_t>; | ||
| __check_operation_state<__result_t>(); | ||
| constexpr bool __nothrow_subscribe = __nothrow_callable<subscribe_t, __child_t, __rcvr_t>; | ||
| return __declfn<__result_t, __nothrow_subscribe && __nothrow_tfx_seq>(); | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same objection here
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in 3b2abb0. This branch is generic now: it keys off the same |
||
| 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<STDEXEC::__decay_t<__tfx_seq_t>>; | ||
| using __data_t = __decay_t<decltype(__data)>; | ||
| 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<decltype(__env)&&>(__env), | ||
| STDEXEC::__forward_like<decltype(__data)>(__data))}); | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ... and here
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same rework here: the runtime branch handles any transparent adaptor — it destructures the expression, builds the child's environment through the adaptor's |
||
| 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), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <test_common/catch2.hpp> | ||
|
|
||
| #include <test_common/receivers.hpp> | ||
| #include <test_common/senders.hpp> | ||
| #include <test_common/type_helpers.hpp> | ||
|
|
||
| #include <utility> | ||
|
|
||
| namespace | ||
| { | ||
| // A minimal sequence sender that produces a single `just(42)` item. | ||
| template <int Value> | ||
| struct single_item_sequence | ||
| { | ||
| using sender_concept = exec::sequence_sender_tag; | ||
| using item_types = exec::item_types<decltype(STDEXEC::just(int{}))>; | ||
| using completion_signatures = | ||
| STDEXEC::completion_signatures<STDEXEC::set_value_t(), STDEXEC::set_stopped_t()>; | ||
|
|
||
| template <class Rcvr> | ||
| 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 <STDEXEC::receiver Rcvr> | ||
| auto subscribe(Rcvr rcvr) const -> op<Rcvr> | ||
| { | ||
| return op<Rcvr>{static_cast<Rcvr&&>(rcvr)}; | ||
| } | ||
| }; | ||
|
|
||
| TEST_CASE("write_env is transparent to sequence senders - item types", "[sequence][write_env]") | ||
| { | ||
| using wrapper_t = STDEXEC::__decay_t<decltype(STDEXEC::write_env(single_item_sequence<42>{}, | ||
| STDEXEC::env<>{}))>; | ||
| using items_t = decltype(exec::get_item_types<wrapper_t, STDEXEC::env<>>()); | ||
| static_assert(STDEXEC::__same_as<items_t, exec::item_types<decltype(STDEXEC::just(int{}))>>); | ||
| } | ||
|
|
||
| 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<wrapper_t, STDEXEC::env<>>()); | ||
| static_assert(STDEXEC::__same_as<items_t, exec::item_types<decltype(STDEXEC::just(int{}))>>); | ||
| } | ||
|
|
||
| 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 <STDEXEC::sender Sndr> | ||
| auto my_adapt(Sndr&& sndr) | ||
| { | ||
| return STDEXEC::__make_sexpr<my_adapt_t>(STDEXEC::env<>{}, static_cast<Sndr&&>(sndr)); | ||
| } | ||
|
|
||
| struct my_adapt_impl : STDEXEC::__sexpr_defaults | ||
| { | ||
| template <class Sndr, class... Env> | ||
| static consteval auto __get_completion_signatures() | ||
| { | ||
| return STDEXEC::get_completion_signatures<STDEXEC::__child_of<Sndr>, Env...>(); | ||
| } | ||
| }; | ||
| } // namespace | ||
|
|
||
| template <> | ||
| struct STDEXEC::__sexpr_impl<my_adapt_t> : my_adapt_impl | ||
| {}; | ||
|
|
||
| template <> | ||
| struct experimental::execution::__sequence_adaptor_traits<my_adapt_t> | ||
| { | ||
| static constexpr bool __transparent = true; | ||
| }; | ||
|
|
||
| namespace | ||
| { | ||
| TEST_CASE("a custom adaptor can declare itself transparent", "[sequence][write_env]") | ||
| { | ||
| using wrapper_t = STDEXEC::__decay_t<decltype(my_adapt(single_item_sequence<42>{}))>; | ||
| using items_t = decltype(exec::get_item_types<wrapper_t, STDEXEC::env<>>()); | ||
| static_assert(STDEXEC::__same_as<items_t, exec::item_types<decltype(STDEXEC::just(int{}))>>); | ||
| } | ||
|
|
||
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is not the right fix. the set of sender adaptors is open. we can't hard-code all of them into into
get_item_types. i'm not familiar enough with the sequence sender stuff to have a different suggestion.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair point — fixed in 3b2abb0. Nothing in
get_item_types(orsubscribe) names an adaptor anymore.There's now an open customization point next to
enable_sequence_sender:An adaptor whose tag is marked transparent is treated as a wrapper that forwards
set_nextand all completions through to a single child.get_item_typesunwraps such senders one layer at a time and computes the child's item types in the child's environment; a traits member__child_env_fn<_Env, _Data>names the environment transformation (identity by default).write_envis now just a specialization of that trait, declaring the child's environment as the written env joined with the query environment — the same thing its operation state presents when connecting. Sincesequence_sendergoes throughenable_sequence_sender, marking the trait is the only thing an adaptor author writes.To check that the extension point is actually open, the new tests include a user-defined adaptor that opts in by specializing the trait alone, with no changes to the machinery.
Happy to reshape this if you'd prefer a different form — e.g. splitting the flag into its own variable template, or hanging it off
__sexpr_defaultsinstead.