Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 197 additions & 0 deletions include/exec/sequence_senders.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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>...>();
}
}

Copy link
Copy Markdown
Collaborator

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.

Copy link
Copy Markdown
Contributor Author

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 (or subscribe) names an adaptor anymore.

There's now an open customization point next to enable_sequence_sender:

template <class _Tag>
struct __sequence_adaptor_traits
{
  // Adaptors default to opaque.
  static constexpr bool __transparent = false;
};

An adaptor whose tag is marked transparent is treated as a wrapper that forwards set_next and all completions through to a single child. get_item_types unwraps 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_env is 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. Since sequence_sender goes through enable_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_defaults instead.

else if constexpr (sender_in<_Sequence, _Env...>
&& !enable_sequence_sender<STDEXEC::__decay_t<_Sequence>>)
{
Expand Down Expand Up @@ -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>;
Expand Down Expand Up @@ -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>();
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same objection here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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 __transparent_sequence_adaptor concept as get_item_types, takes the tag and data from the sender expression, computes the child's environment through the adaptor's __child_env_fn, and recurses into subscribe with a receiver that presents that environment (the wrapper formerly known as __write_env_rcvr, renamed to __adaptor_rcvr). No adaptor names appear here anymore.

else if constexpr (__subscribable_with_static_member<__tfx_seq_t, _Receiver>)
{
using __result_t = decltype(STDEXEC_REMOVE_REFERENCE(
Expand Down Expand Up @@ -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))});
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... and here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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 __child_env_fn, and recurses into the CPO one layer at a time, so stacked wrappers peel off one per level. Nothing write_env-specific remains in subscribe.

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),
Expand Down
1 change: 1 addition & 0 deletions test/exec/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
153 changes: 153 additions & 0 deletions test/exec/sequence/test_write_env_sequence.cpp
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
Loading