Skip to content

fix(exec): make write_env transparent to sequence sender semantics - #2294

Open
alwaysprince05 wants to merge 5 commits into
NVIDIA:mainfrom
alwaysprince05:fix/sequence-env-2053
Open

alwaysprince05 wants to merge 5 commits into
NVIDIA:mainfrom
alwaysprince05:fix/sequence-env-2053

Conversation

@alwaysprince05

@alwaysprince05 alwaysprince05 commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Problem

write_env around a sequence sender breaks sequence semantics (#2053, first repro case). When the child of write_env is a sequence sender, the wrapper is not one: get_item_types has no branch that looks through stdexec sender adaptors, so exec::subscribe falls back to treating the wrapper as a sequence of one. Downstream, exec::transform_each then sees the collapsed item types and the pipeline stops compiling:

error: deduced type '...' does not satisfy '__well_formed_sender'

Fix

Make the sequence machinery see through environment-modifying adaptor senders, via an open customization point in include/exec/sequence_senders.hpp — nothing adaptor-specific is hard-coded there, since the set of sender adaptors is open:

  • Add __sequence_adaptor_traits, a traits template keyed on the adaptor's tag. Adaptors default to opaque. An adaptor that forwards set_next and all completions through to a single child declares itself transparent, optionally naming the environment its child observes with a nested __child_env_fn. Senders whose traits mark them transparent count as sequence senders through enable_sequence_sender.
  • get_item_types and subscribe each gained one generic branch that unwraps any transparent adaptor one layer at a time — computing the child's item types, resp. subscribing to the child, in the transformed environment — so stacked wrappers keep working.
  • write_env opts in with a single trait specialization declaring the child's environment as the written env joined with the query env, which is the same environment write_env's own operation state presents to its child when connecting.

Scope

This fixes the write_env half of #2053. The starts_on half rewrites to __sequence(continues_on(just(), sched), child), which goes through the continues_on machinery that #2277 is currently reworking, so I left it alone for now to avoid colliding with that work. Happy to take it up once #2277 settles.

Partially addresses #2053.

Verification

  • The repro from the issue (Bug A, write_env + transform_each) now compiles clean with -Wall -Wextra -Werror, and the item actually flows: I checked at runtime that the then([](int)) lambda receives 42, both with one and two stacked write_env wrappers.
  • New regression tests in test/exec/sequence/test_write_env_sequence.cpp cover item types (single and stacked wrappers) and the runtime item flow, plus a user-defined adaptor that opts in through the trait alone, to check the extension point is actually open.
  • test.stdexec + test.exec rebuilt and pass: 100% of tests, both in the normal build and with STDEXEC_ENABLE_EXTRA_TYPE_CHECKING=ON, and the C++20 module-purview build still compiles and passes its suite.
  • The starts_on case from the issue still fails as expected; it needs the continues_on work mentioned above.
  • clang-format run on the touched hunks.

A write_env sender wrapping a sequence sender was not itself a sequence
sender: get_item_types has no branch that looks through stdexec sender
adaptors, so sequence-aware algorithms like exec::transform_each treated
the wrapper as a sequence of one and collapsed the item types. Make
write_env senders transparent to sequence semantics:

- specialize enable_sequence_sender for __write_env_t sexprs
- compute item types of a write_env sender from its child in the joined
  environment (recursing through stacked wrappers)
- subscribe to the child through a receiver that joins the written
  environment with the wrapped receiver's environment

First half of NVIDIA#2053. The starts_on half goes through the continues_on
machinery that NVIDIA#2277 is reworking, so it is left for a follow-up.
@copy-pr-bot

copy-pr-bot Bot commented Sep 29, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@alwaysprince05

Copy link
Copy Markdown
Contributor Author

/ok to test

Comment thread include/exec/sequence_senders.hpp Outdated
// semantics from downstream sequence-aware algorithms. See issue #2053.
template <auto _DescriptorFn>
requires STDEXEC::__same_as<
typename STDEXEC::__desc_of_t<STDEXEC::__sexpr<_DescriptorFn>>::__tag,

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.

could this be simply:

Suggested change
typename STDEXEC::__desc_of_t<STDEXEC::__sexpr<_DescriptorFn>>::__tag,
STDEXEC::tag_of_t<STDEXEC::__sexpr<_DescriptorFn>>,

?

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.

Done — thanks. The specialization now reaches the tag through tag_of_t (it actually goes through the new __transparent_sequence_adaptor concept described below, which queries it the same way). The hunk has moved because of the rework from your other comments, so GitHub couldn't apply the suggestion in place; the change is in 3b2abb0.

// signatures from its child's signatures in the joined environment.
using __child_env_t = __join_env_t<__decay_t<__data_of<_Sequence>> const &, _Env...>;
return __get_item_types_helper<__child_of<_Sequence>, __child_env_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.

__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.

__write_env_rcvr<_Receiver, __written_t>{
static_cast<_Receiver&&>(__rcvr),
STDEXEC::__forward_like<decltype(__env)>(__env)});
}

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.

…oint

Review feedback on NVIDIA#2294: get_item_types and subscribe must not
hard-code individual sender adaptors, since the set of adaptors is
open. Replace the write_env-specific branches with a generic
mechanism driven by a new __sequence_adaptor_traits customization
point: an adaptor that forwards set_next and all completions to a
single child declares itself transparent for its tag, optionally
naming the environment its child observes. write_env now only
specializes that trait, and the sequence machinery handles every
transparent adaptor the same way. The enable_sequence_sender
specialization also goes through tag_of_t rather than digging into
the sender's descriptor.

Also merge upstream/main, and extend the regression tests with a
user-defined adaptor that opts in via the new trait, to show that
the extension point is open.
@alwaysprince05

Copy link
Copy Markdown
Contributor Author

/ok to test

alwaysprince05 and others added 2 commits October 4, 2026 00:06
Empty struct bodies written as `{ };` fail the style CI job, which runs
clang-format-21 --dry-run --Werror over all tracked sources.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
@alwaysprince05

Copy link
Copy Markdown
Contributor Author

/ok to test

@alwaysprince05

Copy link
Copy Markdown
Contributor Author

@ericniebler Circling back — all four comments are addressed in 3b2abb0, with fbf50b4 on top of that just for a clang-format fix, and I've merged main back in so the branch is current.

The tag_of_t simplification went in as suggested. For the other three: they were all the same concern, and nothing adaptor-specific is left in get_item_types, subscribe, or the runtime branch. What I ended up with is an open __sequence_adaptor_traits<_Tag> next to enable_sequence_sender — an adaptor stays opaque by default, and one that forwards set_next and all completions to a single child marks itself transparent, optionally naming the environment its child observes with a nested __child_env_fn. Each of the three sites now just peels one transparent layer off and recurses, so stacked wrappers fall out for free, and write_env itself is down to a single trait specialization. To make sure the extension point is genuinely open, the new test file includes a user-defined adaptor that opts in by specializing the trait alone, with no changes to the machinery.

Everything passes locally: test.stdexec and test.exec in both the normal build and STDEXEC_ENABLE_EXTRA_TYPE_CHECKING=ON, plus clang-format-21 --dry-run --Werror across the repo. The only thing outstanding is runner approval, so CI (CPU) and CI (GPU) can report.

Still happy to reshape the traits design if you'd rather hang it off __sexpr_defaults or a separate variable template.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants