diff --git a/include/exec/elide.hpp b/include/exec/elide.hpp new file mode 100644 index 000000000..71f414cdb --- /dev/null +++ b/include/exec/elide.hpp @@ -0,0 +1,115 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * 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 +#include +#include +#include +#include + +namespace exec { + +namespace detail::elide { + +template +concept result = !std::same_as; + +template +concept c = + std::invocable && + result> && + std::is_reference_v && + (std::is_reference_v && ...); + +template +concept lvalue = + std::invocable && + result>; + +struct tombstone {}; + +template +struct lvalue_result : std::type_identity {}; +template + requires lvalue +struct lvalue_result : std::invoke_result {}; + +} + +template + requires detail::elide::c +struct elide { + elide() = delete; + elide(const elide&) = delete; + elide& operator=(const elide&) = delete; + constexpr explicit elide(T t, Args... args) noexcept + : t_(std::forward(t)), + args_(std::forward(args)...) + {} +private: + static constexpr bool rvalue_noexcept_ = + std::is_nothrow_invocable_v; + static constexpr bool lvalue_noexcept_ = + std::is_nothrow_invocable_v; + using rvalue_result_type_ = std::invoke_result_t; + using lvalue_result_type_ = typename detail::elide::lvalue_result< + T, + Args...>::type; +public: + constexpr operator rvalue_result_type_() const&& noexcept(rvalue_noexcept_) { + return std::apply( + [&](auto&&... args) noexcept(rvalue_noexcept_) { + return std::invoke( + std::forward(t_), + std::forward(args)...); + }, + args_); + } + constexpr rvalue_result_type_ operator()() const&& noexcept(rvalue_noexcept_) + { + return rvalue_result_type_(std::move(*this)); + } + constexpr operator lvalue_result_type_() const& noexcept(lvalue_noexcept_) { + if constexpr (detail::elide::lvalue) { + return std::apply( + [&](auto&&... args) noexcept(lvalue_noexcept_) { + return std::invoke(t_, args...); + }, + args_); + } else { + return {}; + } + } + constexpr lvalue_result_type_ operator()() const& noexcept(lvalue_noexcept_) + requires detail::elide::lvalue + { + return rvalue_result_type_(*this); + } +private: + [[no_unique_address]] + T t_; + [[no_unique_address]] + std::tuple args_; +}; + +template +explicit elide(T&&, Args&&...) -> elide; + +} // namespace exec diff --git a/include/exec/enter_scope_sender.hpp b/include/exec/enter_scope_sender.hpp new file mode 100644 index 000000000..af8746171 --- /dev/null +++ b/include/exec/enter_scope_sender.hpp @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * 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 +#include +#include + +#include "exit_scope_sender.hpp" +#include "../stdexec/execution.hpp" + +namespace exec { + +template +concept enter_scope_sender = ::STDEXEC::sender; + +namespace detail::exit_scope_sender_of { + +template +struct transform_set_value_impl; +template Sender> +struct transform_set_value_impl { + using type = ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(Sender)>; +}; + +template +struct transform_set_value { + template + using fn = transform_set_value_impl::type; +}; + +template +using transform_set_error = ::STDEXEC::completion_signatures<>; + +template +struct impl; +template +struct impl< + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(Sender)>> +{ + using type = Sender; +}; + +} + +template +using exit_scope_sender_of_t = detail::exit_scope_sender_of::impl< + ::STDEXEC::transform_completion_signatures< + ::STDEXEC::completion_signatures_of_t, + ::STDEXEC::completion_signatures<>, + detail::exit_scope_sender_of::transform_set_value::template fn, + detail::exit_scope_sender_of::transform_set_error, + ::STDEXEC::completion_signatures<>>>::type; + +template +concept enter_scope_sender_in = + enter_scope_sender && + ::STDEXEC::sender_in && + requires { + typename exit_scope_sender_of_t; + }; + +} // namespace exec diff --git a/include/exec/enter_scopes.hpp b/include/exec/enter_scopes.hpp new file mode 100644 index 000000000..3e2d00de7 --- /dev/null +++ b/include/exec/enter_scopes.hpp @@ -0,0 +1,386 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * 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 +#include +#include +#include +#include +#include + +#include "elide.hpp" +#include "enter_scope_sender.hpp" +#include "storage_for_completion_signatures.hpp" +#include "../stdexec/execution.hpp" + +#include + +namespace exec { + +namespace detail::enter_scopes { + +struct t { + template<::exec::enter_scope_sender... Senders> + constexpr ::exec::enter_scope_sender auto operator()(Senders&&... senders) + const noexcept( + (std::is_nothrow_constructible_v< + std::remove_cvref_t, + Senders> && ...)) + { + return ::STDEXEC::__make_sexpr( + ::STDEXEC::__(), + (Senders&&)senders...); + } + template<::exec::enter_scope_sender Sender> + constexpr ::exec::enter_scope_sender auto operator()(Sender&& sender) const + noexcept( + std::is_nothrow_constructible_v< + std::remove_cvref_t, + Sender>) + { + return (Sender&&)sender; + } + constexpr ::exec::enter_scope_sender auto operator()() const noexcept { + return ::STDEXEC::just(::STDEXEC::just()); + } +}; + +template< + ::exec::enter_scope_sender Sender, + typename Receiver, + typename RollbackReceiver> +class enter_scope_sender_state { + using env_type_ = ::STDEXEC::env_of_t; + using exit_scope_sender_type_ = ::exec::exit_scope_sender_of_t< + Sender, + env_type_>; + struct receiver_type_ : Receiver { + // Note the parameter is by value which means that we can't possibly have a + // reference back into the operation state, this is exception safe because + // exit senders must be nothrow decay-copyable + constexpr void set_value(exit_scope_sender_type_ sender) && noexcept { + auto base = (Receiver&&)*this; + auto&& self = self_; + // This destroys *this + self.storage_.template emplace( + std::move(sender)); + ::STDEXEC::set_value(std::move(base)); + } + enter_scope_sender_state& self_; + }; + using operation_state_type_ = ::STDEXEC::connect_result_t< + Sender, + receiver_type_>; + using rollback_operation_state_type_ = ::STDEXEC::connect_result_t< + exit_scope_sender_type_, + RollbackReceiver>; + std::variant< + operation_state_type_, + exit_scope_sender_type_, + rollback_operation_state_type_> storage_; + static constexpr bool nothrow_constructible_ = + ::STDEXEC::__nothrow_connectable; +public: + constexpr explicit enter_scope_sender_state(Sender&& sender, Receiver r) + noexcept(nothrow_constructible_) + : storage_( + std::in_place_type, + ::exec::elide([&]() noexcept(nothrow_constructible_) { + return ::STDEXEC::connect( + (Sender&&)sender, + receiver_type_{ + {(Receiver&&)r}, + *this}); + })) + {} + constexpr void start() & noexcept { + const auto ptr = std::get_if(&storage_); + STDEXEC_ASSERT(ptr); + ::STDEXEC::start(*ptr); + } + [[nodiscard]] + constexpr bool connect_rollback(RollbackReceiver r) & noexcept { + const auto ptr = std::get_if(&storage_); + if (!ptr) { + return false; + } + auto sender = std::move(*ptr); + storage_.template emplace( + ::exec::elide([&]() noexcept { + return ::STDEXEC::connect( + std::move(sender), + std::move(r)); + })); + return true; + } + constexpr void start_rollback() & noexcept { + if ( + const auto ptr = std::get_if(&storage_); + ptr) + { + ::STDEXEC::start(*ptr); + } + } + constexpr exit_scope_sender_type_&& complete() && noexcept { + const auto ptr = std::get_if(&storage_); + STDEXEC_ASSERT(ptr); + return std::move(*ptr); + } +}; + +template +using remove_set_value_t = ::STDEXEC::completion_signatures<>; + +template +using exit_sender_t = decltype( + ::STDEXEC::when_all( + std::declval()...)); + +template +struct storage_for_completion_signatures { + template + using __f = ::exec::storage_for_completion_signatures< + ::STDEXEC::transform_completion_signatures< + ::STDEXEC::__concat_completion_signatures_t< + ::STDEXEC::completion_signatures_of_t< + Senders, + Env...>...>, + ::STDEXEC::completion_signatures<>, + remove_set_value_t>>; +}; + +template +class state { + using env_type_ = ::STDEXEC::env_of_t; + struct receiver_base_ { + using receiver_concept = ::STDEXEC::receiver_t; + constexpr env_type_ get_env() const noexcept; + state& self_; + }; + struct receiver_type_ : receiver_base_ { + using receiver_base_::self_; + constexpr void set_value() && noexcept; + template + constexpr void set_error(Args&&...) && noexcept; + template + constexpr void set_stopped(Args&&...) && noexcept; + }; + struct rollback_receiver_type_ : receiver_base_ { + using receiver_base_::self_; + constexpr void set_value() && noexcept; + }; + template + using state_type_ = enter_scope_sender_state< + S, + receiver_type_, + rollback_receiver_type_>; + Receiver r_; + std::tuple...> states_; + // TODO: There's no need for these two members if no child can fail or stop + typename storage_for_completion_signatures< + ::STDEXEC::env_of_t>::template __f storage_; + std::atomic stored_{false}; + std::atomic outstanding_{sizeof...(Senders)}; + constexpr bool is_complete_() noexcept { + return outstanding_.fetch_sub(1, std::memory_order_acq_rel) == 1; + } + constexpr void complete_() noexcept { + if (!is_complete_()) { + return; + } + std::apply( + [&](auto&... states) noexcept { + if (stored_.load(std::memory_order_relaxed)) { + // Starting with one prevents the child operations from finalizing + // the operation out from under us, thereby preventing UB (it does + // require that we check to see if we have to finalize as we + // complete, see below) + outstanding_.store(1, std::memory_order_relaxed); + const auto impl = [&](auto& state) noexcept { + if (state.connect_rollback(rollback_receiver_type_{{*this}})) { + outstanding_.fetch_add(1, std::memory_order_relaxed); + } + }; + (impl(states), ...); + (states.start_rollback(), ...); + rollback_complete_(); + } else { + ::STDEXEC::set_value( + std::move(r_), + ::STDEXEC::when_all(std::move(states).complete()...)); + } + }, + states_); + } + template + constexpr void fail_(Args&&... args) noexcept { + if (!stored_.exchange(true, std::memory_order_relaxed)) { + storage_.arrive((Args&&)args...); + } + complete_(); + } + constexpr void rollback_complete_() noexcept { + if (is_complete_()) { + std::move(storage_).complete(std::move(r_)); + } + } +public: + explicit constexpr state( + Receiver r, + Senders&&... senders) noexcept( + (std::is_nothrow_constructible_v< + state_type_, + Senders, + receiver_type_> && ...)) + : r_((Receiver&&)r), + states_( + ::exec::elide([&]() noexcept( + std::is_nothrow_constructible_v< + state_type_, + Senders, + receiver_type_>) + { + return state_type_( + (Senders&&)senders, + receiver_type_{{*this}}); + })...) + {} + constexpr void start() & noexcept { + std::apply( + [](auto&&... states) noexcept { + (states.start(), ...); + }, + states_); + } +}; + +template +constexpr auto state::receiver_base_::get_env() const + noexcept -> env_type_ +{ + return ::STDEXEC::get_env(self_.r_); +} + +template +constexpr void state::receiver_type_::set_value() && + noexcept +{ + self_.complete_(); +} + +template +template +constexpr void state::receiver_type_::set_error( + Args&&... args) && noexcept +{ + self_.fail_(::STDEXEC::set_error, (Args&&)args...); +} + +template +template +constexpr void state::receiver_type_::set_stopped( + Args&&... args) && noexcept +{ + self_.fail_(::STDEXEC::set_stopped, (Args&&)args...); +} + +template +constexpr void state::rollback_receiver_type_::set_value() + && noexcept +{ + self_.rollback_complete_(); +} + +template +struct completions { + template + using __f = ::STDEXEC::transform_completion_signatures< + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t( + exit_sender_t<::exec::exit_scope_sender_of_t...>)>, + typename storage_for_completion_signatures:: + template __f::completion_signatures>; +}; + +// TODO: We should be able to get a better "message" than this +struct FAILED_TO_FORM_COMPLETION_SIGNATURES {}; + +template +struct connect_result { + template + using __f = state; +}; + +template +struct nothrow_connect { + template + using __f = std::is_nothrow_constructible< + state, + Receiver, + Children...>; +}; + +class impl : public ::STDEXEC::__sexpr_defaults { + template + using completions_ = ::STDEXEC::__children_of>; +public: + template + static consteval auto get_completion_signatures() { + if constexpr (::STDEXEC::__mvalid) { + return completions_{}; + } else if constexpr (sizeof...(Env) == 0) { + return STDEXEC::__dependent_sender(); + } else { + return ::STDEXEC::__throw_compile_time_error< + FAILED_TO_FORM_COMPLETION_SIGNATURES, + ::STDEXEC::_WITH_PRETTY_SENDER_>(); + } + } + static constexpr auto connect = []( + Sender&& sender, Receiver r) noexcept( + ::STDEXEC::__children_of>::value) + -> ::STDEXEC::__children_of> + { + return ::STDEXEC::__apply( + [&](auto&&, auto&&, auto&&... children) noexcept( + ::STDEXEC::__children_of>::value) + { + return ::STDEXEC::__children_of>( + (Receiver&&)r, + (decltype(children)&&)children...); + }, + (Sender&&)sender); + }; +}; + +} + +using enter_scopes_t = detail::enter_scopes::t; +inline constexpr enter_scopes_t enter_scopes; + +} // namespace exec + +namespace STDEXEC { + +template<> +struct __sexpr_impl<::exec::enter_scopes_t> : ::exec::detail::enter_scopes::impl +{}; + +} diff --git a/include/exec/exit_scope_sender.hpp b/include/exec/exit_scope_sender.hpp new file mode 100644 index 000000000..6a710f72d --- /dev/null +++ b/include/exec/exit_scope_sender.hpp @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * 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 + +#include "../stdexec/execution.hpp" + +namespace exec { + +template +concept exit_scope_sender = + ::STDEXEC::sender && + std::is_nothrow_constructible_v< + std::remove_cvref_t, + Sender> && + std::is_nothrow_move_constructible_v< + std::remove_cvref_t>; + +template +concept exit_scope_sender_in = + exit_scope_sender && + ::STDEXEC::sender_in && + ::STDEXEC::__nothrow_connectable< + Sender, + ::STDEXEC::__receiver_archetype> && + std::is_same_v< + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t()>, + ::STDEXEC::completion_signatures_of_t< + Sender, + Env>>; + +} // namespace exec diff --git a/include/exec/invoke.hpp b/include/exec/invoke.hpp new file mode 100644 index 000000000..2a0474065 --- /dev/null +++ b/include/exec/invoke.hpp @@ -0,0 +1,413 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * 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 +#include +#include +#include + +#include "elide.hpp" +#include "like_t.hpp" +#include "../stdexec/execution.hpp" + +namespace exec { + +namespace detail::invoke { + +struct tag {}; + +struct t { + template<::STDEXEC::sender Sender, typename F> + constexpr ::STDEXEC::sender auto operator()(Sender&& sender, F&& f) const + noexcept( + std::is_nothrow_constructible_v< + std::remove_cvref_t, + Sender> && + std::is_nothrow_constructible_v< + std::remove_cvref_t, + F>) + { + return ::STDEXEC::__make_sexpr( + (F&&)f, + (Sender&&)sender); + } + template + constexpr auto operator()(F&& f) const noexcept( + std::is_nothrow_constructible_v< + std::remove_cvref_t, + F>) + { + return ::STDEXEC::__closure(*this, (F&&)f); + } + template + static constexpr auto transform_sender( + ::STDEXEC::set_value_t, + Sender&& sender, + const Env&) noexcept( + std::is_nothrow_constructible_v< + std::remove_cvref_t, + Sender>) + { + auto&& [_, f, predecessor] = (Sender&&)sender; + static_assert(::STDEXEC::sender); + return ::STDEXEC::__make_sexpr( + std::tuple((decltype(f)&&)f, (decltype(predecessor)&&)predecessor)); + } +}; + +template +struct sender_check; +template +struct sender_check { + static constexpr bool value = ::STDEXEC::sender; +}; +template +struct sender_check { + static constexpr bool value = ::STDEXEC::sender_in; +}; + +template +class transform_set_value { + template + class impl_ { + static_assert(std::is_invocable_v); + using sender_ = std::invoke_result_t; + static_assert(sender_check::value); + static constexpr bool nothrow_invoke_ = std::is_nothrow_invocable_v< + F, + Args...>; + static constexpr bool nothrow_connect_ = ::STDEXEC::__nothrow_connectable< + sender_, + ::STDEXEC::__receiver_archetype>; + public: + using type = ::STDEXEC::transform_completion_signatures< + ::STDEXEC::completion_signatures_of_t< + sender_, + Env...>, + std::conditional_t< + nothrow_invoke_ && nothrow_connect_, + ::STDEXEC::completion_signatures<>, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_error_t(std::exception_ptr)>>>; + }; +public: + template + using fn = impl_::type; +}; + +template +using completions = ::STDEXEC::transform_completion_signatures< + ::STDEXEC::completion_signatures_of_t, + ::STDEXEC::completion_signatures<>, + transform_set_value::template fn>; + +// TODO: We should be able to get a better "message" than this +struct FAILED_TO_FORM_COMPLETION_SIGNATURES {}; + +template<::STDEXEC::receiver Receiver> +struct receiver_ref { + using receiver_concept = ::STDEXEC::receiver_t; + Receiver& r_; + constexpr ::STDEXEC::env_of_t get_env() const noexcept { + return ::STDEXEC::get_env(r_); + } + template + requires ::STDEXEC::receiver_of< + Receiver, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(Args...)>> + constexpr void set_value(Args&&... args) && noexcept { + ::STDEXEC::set_value((Receiver&&)r_, (Args&&)args...); + } + template + requires ::STDEXEC::receiver_of< + Receiver, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_error_t(T)>> + constexpr void set_error(T&& t) && noexcept { + ::STDEXEC::set_error((Receiver&&)r_, (T&&)t); + } + constexpr void set_stopped() && noexcept requires ::STDEXEC::receiver_of< + Receiver, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_stopped_t()>> + { + ::STDEXEC::set_stopped((Receiver&&)r_); + } +}; + +template> +struct variant_for_operation_states; + +template +struct variant_for_operation_states< + F, + Receiver, + Additional, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(Args...), + Signatures...>, + std::tuple> +{ + using type = variant_for_operation_states< + F, + Receiver, + Additional, + ::STDEXEC::completion_signatures, + std::tuple< + ::STDEXEC::connect_result_t< + std::invoke_result_t< + F, + Args...>, + receiver_ref>, + States...>>::type; +}; + +template +struct variant_for_operation_states< + F, + Receiver, + Additional, + ::STDEXEC::completion_signatures< + Signature, + Signatures...>, + States> +{ + using type = variant_for_operation_states< + F, + Receiver, + Additional, + ::STDEXEC::completion_signatures, + States>::type; +}; + +template +struct variant_for_operation_states< + F, + Receiver, + std::tuple, + ::STDEXEC::completion_signatures<>, + std::tuple> +{ + using type = ::STDEXEC::__munique< + ::STDEXEC::__qq>::__f; +}; + +template +class state { + using env_type_ = ::STDEXEC::env_of_t; + struct receiver_type_ { + using receiver_concept = ::STDEXEC::receiver_t; + state& self_; + constexpr env_type_ get_env() const noexcept; + template + constexpr void set_value(Args&&...) && noexcept; + template + constexpr void set_error(Args&&...) && noexcept; + template + constexpr void set_stopped(Args&&...) && noexcept; + }; + using operation_state_type_ = ::STDEXEC::connect_result_t< + Sender, + receiver_type_>; + using operation_states_type_ = variant_for_operation_states< + F, + Receiver, + std::tuple, + ::STDEXEC::completion_signatures_of_t>::type; + Receiver r_; + F f_; + operation_states_type_ ops_; + static constexpr bool nothrow_connect_ = ::STDEXEC::__nothrow_connectable< + Sender, + receiver_type_>; +public: + template + explicit constexpr state(Sender&& s, T&& t, Receiver r) noexcept( + std::is_nothrow_constructible_v && nothrow_connect_) + : r_((Receiver&&)r), + f_((T&&)t), + ops_( + std::in_place_type, + ::exec::elide([&]() noexcept(nothrow_connect_) { + return ::STDEXEC::connect( + (Sender&&)s, + receiver_type_{*this}); + })) + {} + constexpr void start() & noexcept { + const auto ptr = std::get_if(&ops_); + STDEXEC_ASSERT(ptr); + ::STDEXEC::start(*ptr); + } +}; + +template +constexpr auto state::receiver_type_::get_env() const + noexcept -> env_type_ +{ + return ::STDEXEC::get_env(self_.r_); +} + +template +template +constexpr void state::receiver_type_::set_value( + Args&&... args) && noexcept +{ + constexpr auto nothrow_connect = ::STDEXEC::__nothrow_connectable< + std::invoke_result_t, + receiver_ref>; + // We store this locally because we're going to destroy the operation state + // and therefore, transitively, *this + auto&& self = self_; + try { + // It's important we use auto&& here not auto because the invocable might + // return a reference to a sender + auto&& sender = std::invoke((F&&)self.f_, (Args&&)args...); + // receiver_ref is important here, imagine we didn't use receiver_ref and + // instead directly moved the final receiver into connect, then if connect + // threw we'd already potentially have "consumed" the receiver and wouldn't + // be able to send the error completion anywhere + using op_state_type = ::STDEXEC::connect_result_t< + decltype(sender), + receiver_ref>; + auto&& op = self.ops_.template emplace( + ::exec::elide([&]() noexcept(nothrow_connect) { + return ::STDEXEC::connect( + (decltype(sender)&&)sender, + receiver_ref{self.r_}); + })); + ::STDEXEC::start(op); + } catch (...) { + if constexpr (std::is_nothrow_invocable_v && nothrow_connect) { + STDEXEC_UNREACHABLE(); + } else { + ::STDEXEC::set_error((Receiver&&)self.r_, std::current_exception()); + } + } +} + +template +template +constexpr void state::receiver_type_::set_error( + Args&&... args) && noexcept +{ + ::STDEXEC::set_error((Receiver&&)self_.r_, (Args&&)args...); +} + +template +template +constexpr void state::receiver_type_::set_stopped( + Args&&... args) && noexcept +{ + ::STDEXEC::set_stopped((Receiver&&)self_.r_, (Args&&)args...); +} + +template +class nothrow_get_state { + using tuple_type_ = std::remove_cvref_t<::STDEXEC::__data_of>; + using sender_type_ = ::exec::like_t< + Sender, + std::tuple_element_t<1, tuple_type_>>; + using invocable_type_ = std::tuple_element_t<0, tuple_type_>; +public: + static constexpr bool value = std::is_nothrow_constructible_v< + state, + sender_type_, + ::exec::like_t, + Receiver>; +}; + +template> +struct get_state_result; +template +struct get_state_result> { + using type = state< + ::exec::like_t, + F, + Receiver>; +}; + +struct impl : public ::STDEXEC::__sexpr_defaults { + template + static consteval auto get_completion_signatures() { + using tuple = std::remove_cvref_t< + ::STDEXEC::__data_of>; + using f = std::tuple_element_t<0, tuple>; + using sender = ::exec::like_t>; + static_assert(::STDEXEC::sender); + if constexpr (sizeof...(Env)) { + static_assert(::STDEXEC::sender_in); + } + if constexpr (::STDEXEC::__mvalid) { + return completions{}; + } else { + return ::STDEXEC::__throw_compile_time_error< + FAILED_TO_FORM_COMPLETION_SIGNATURES, + ::STDEXEC::_WITH_PRETTY_SENDER_>(); + } + } + static constexpr auto get_state = []( + Sender&& sender, Receiver r) noexcept( + nothrow_get_state::value) + -> get_state_result<::STDEXEC::__data_of, Receiver>::type + { + auto&& [_, tuple] = (Sender&&)sender; + auto&& [f, inner] = (decltype(tuple)&&)tuple; + return + typename get_state_result<::STDEXEC::__data_of, Receiver>::type( + (decltype(inner)&&)inner, + (decltype(f)&&)f, + (Receiver&&)r); + }; + static constexpr auto start = [](auto& state) noexcept { + state.start(); + }; +}; + +} + +using invoke_t = detail::invoke::t; +inline constexpr invoke_t invoke; + +} // namespace exec + +namespace STDEXEC { + +template<> +struct __sexpr_impl<::exec::detail::invoke::tag> + : ::exec::detail::invoke::impl {}; + +template<> +struct __sexpr_impl<::exec::invoke_t> : ::STDEXEC::__sexpr_defaults { + template + static consteval auto get_completion_signatures() { + using type = decltype( + ::STDEXEC::transform_sender( + std::declval(), + std::declval()...)); + static_assert(!std::is_same_v< + std::remove_cvref_t, + std::remove_cvref_t>); + return ::STDEXEC::get_completion_signatures(); + } +}; + +} diff --git a/include/exec/lifetime.hpp b/include/exec/lifetime.hpp new file mode 100644 index 000000000..1fdbd435a --- /dev/null +++ b/include/exec/lifetime.hpp @@ -0,0 +1,224 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * 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 +#include +#include +#include +#include +#include + +#include "enter_scopes.hpp" +#include "invoke.hpp" +#include "like_t.hpp" +#include "object.hpp" +#include "within.hpp" +#include "../stdexec/execution.hpp" + +namespace exec { + +namespace detail::lifetime { + +struct t { + template + requires ::STDEXEC::sender< + std::invoke_result_t< + F, + ::exec::type_of_object_t&...>> + constexpr ::STDEXEC::sender auto operator()(F&& f, Objects&&... objects) const + noexcept( + std::is_nothrow_constructible_v< + std::remove_cvref_t, + F> && + (std::is_nothrow_constructible_v< + std::remove_cvref_t, + Objects> && ...)) + { + return ::STDEXEC::__make_sexpr( + std::tuple((F&&)f, (Objects&&)objects...)); + } +}; + +template +class storage_for_object { + union type_ { + char c; + T t; + constexpr type_() noexcept : c() {} + constexpr ~type_() noexcept {} + }; + type_ storage_; +public: + constexpr T* get_storage() noexcept { + return std::addressof(storage_.t); + } + constexpr T& get_object() noexcept { + return *std::launder(get_storage()); + } +}; + +template +using storage_for_object_t = storage_for_object< + ::exec::type_of_object_t>; + +template +using storage_for_objects_t = std::tuple< + storage_for_object_t...>; + +template> +class make_sender_impl; + +template +class make_sender_impl> { + static constexpr auto impl_( + ::exec::like_t&& f, + storage_for_object_t&... storage) noexcept( + std::is_nothrow_constructible_v< + F, + ::exec::like_t>) + { + return [f = (::exec::like_t&&)f, &storage...]() mutable noexcept( + std::is_nothrow_invocable_v< + F, + ::exec::type_of_object_t&...>) + { + return std::invoke( + std::move(f), + storage.get_object()...); + }; + } +public: + static constexpr bool nothrow = noexcept( + ::exec::within( + ::exec::enter_scopes( + std::invoke( + std::declval<::exec::like_t>(), + std::declval<::exec::type_of_object_t*>())...), + ::STDEXEC::just() | ::exec::invoke( + impl_( + std::declval<::exec::like_t>(), + std::declval&>()...)))); + using storage_type = storage_for_objects_t; + static constexpr ::STDEXEC::sender auto impl( + Tuple&& t, + storage_type& storage) noexcept(nothrow) + { + return std::apply( + [&](auto&& f, auto&&... objects) noexcept(nothrow) { + return std::apply( + [&](auto&... storage_for_object) noexcept(nothrow) { + return ::exec::within( + ::exec::enter_scopes( + std::invoke( + (decltype(objects)&&)objects, + storage_for_object.get_storage())...), + ::STDEXEC::just() | ::exec::invoke( + impl_((decltype(f)&&)f, storage_for_object...))); + }, + storage); + }, + (Tuple&&)t); + } + using sender_type = decltype( + impl( + std::declval(), + std::declval&>())); +}; + +template +constexpr ::STDEXEC::sender auto make_sender( + Tuple&& tuple, + std::tuple...>& storage) noexcept( + noexcept( + make_sender_impl::impl( + (Tuple&&)tuple, + storage))) +{ + return make_sender_impl::impl((Tuple&&)tuple, storage); +} + +template +class state { + using impl_ = make_sender_impl; + typename impl_::storage_type storage_; + ::STDEXEC::connect_result_t< + typename impl_::sender_type, + Receiver> op_; +public: + explicit constexpr state(Tuple&& t, Receiver r) noexcept(impl_::nothrow) + : op_( + ::STDEXEC::connect( + impl_::impl((Tuple&&)t, storage_), + (Receiver&&)r)) + {} + constexpr void start() & noexcept { + ::STDEXEC::start(op_); + } +}; + +// TODO: We should be able to get a better "message" than this +struct FAILED_TO_FORM_COMPLETION_SIGNATURES {}; + +class impl : public ::STDEXEC::__sexpr_defaults { + template + using completions_ = ::STDEXEC::completion_signatures_of_t< + typename make_sender_impl<::STDEXEC::__data_of>::sender_type, + Env...>; +public: + template + static consteval auto get_completion_signatures() { + if constexpr (::STDEXEC::__mvalid) { + return completions_{}; + } else { + return ::STDEXEC::__throw_compile_time_error< + FAILED_TO_FORM_COMPLETION_SIGNATURES, + ::STDEXEC::_WITH_PRETTY_SENDER_>(); + } + } + static constexpr auto get_state = []( + Sender&& sender, Receiver r) noexcept( + std::is_nothrow_constructible_v< + state<::STDEXEC::__data_of, Receiver>, + ::STDEXEC::__data_of, + Receiver>) -> state<::STDEXEC::__data_of, Receiver> + { + auto&& [_, tuple] = (Sender&&)sender; + return state( + (decltype(tuple)&&)tuple, + (Receiver&&)r); + }; + static constexpr auto start = [](auto& state) noexcept { + state.start(); + }; +}; + +} + +using lifetime_t = detail::lifetime::t; +inline constexpr lifetime_t lifetime; + +} // namespace exec + +namespace STDEXEC { + +template<> +struct __sexpr_impl<::exec::lifetime_t> : ::exec::detail::lifetime::impl {}; + +} diff --git a/include/exec/like_t.hpp b/include/exec/like_t.hpp new file mode 100644 index 000000000..e2dd04d84 --- /dev/null +++ b/include/exec/like_t.hpp @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * 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 +#include + +namespace exec { + +template +using like_t = decltype( + ::STDEXEC::__forward_like( + std::declval())); + +} // namespace exec diff --git a/include/exec/nest_scopes.hpp b/include/exec/nest_scopes.hpp new file mode 100644 index 000000000..f8557adf5 --- /dev/null +++ b/include/exec/nest_scopes.hpp @@ -0,0 +1,428 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * 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 +#include +#include +#include +#include +#include +#include + +#include "elide.hpp" +#include "enter_scope_sender.hpp" +#include "sequence.hpp" +#include "storage_for_completion_signatures.hpp" +#include "../stdexec/execution.hpp" + +namespace exec { + +namespace detail::nest_scopes { + +template +constexpr auto make_exit_scope_sender_impl(std::tuple tuple) + noexcept(noexcept(::exec::sequence(std::declval()...))) +{ + return std::apply( + [](auto&&... senders) noexcept( + noexcept(::exec::sequence((decltype(senders)&&)senders...))) + { + return ::exec::sequence((decltype(senders)&&)senders...); + }, + std::move(tuple)); +} + +template +constexpr auto make_exit_scope_sender_impl( + std::tuple tuple, + T&& t, + Rest&&... rest) noexcept( + // Assumes exec::sequence just decay-copies its arguments + (std::is_nothrow_constructible_v< + std::remove_cvref_t, + Senders...> && ...) && + std::is_nothrow_constructible_v< + std::remove_cvref_t, + T> && + (std::is_nothrow_constructible_v< + std::remove_cvref_t, + Rest> && ...)) +{ + return std::apply( + [&](auto&&... senders) noexcept( + noexcept(nest_scopes::make_exit_scope_sender_impl( + std::declval>(), + std::declval()...))) + { + return nest_scopes::make_exit_scope_sender_impl( + std::forward_as_tuple( + (T&&)t, + (decltype(senders)&&)senders...), + (Rest&&)rest...); + }, + std::move(tuple)); +} + +// Obtains the composed exit sender by reversing the order and wrapping in +// exec::sequence +template +constexpr auto make_exit_scope_sender(Senders&&... senders) noexcept( + noexcept( + nest_scopes::make_exit_scope_sender_impl( + std::tuple<>{}, + std::declval()...))) +{ + return nest_scopes::make_exit_scope_sender_impl( + std::tuple<>{}, + (Senders&&)senders...); +} + +struct t { + template<::exec::enter_scope_sender... Senders> + constexpr ::exec::enter_scope_sender auto operator()(Senders&&... senders) + const noexcept( + (std::is_nothrow_constructible_v< + std::remove_cvref_t, + Senders> && ...)) + { + return ::STDEXEC::__make_sexpr( + ::STDEXEC::__(), + (Senders&&)senders...); + } + template<::exec::enter_scope_sender Sender> + constexpr ::exec::enter_scope_sender auto operator()(Sender&& sender) const + noexcept( + std::is_nothrow_constructible_v< + std::remove_cvref_t, + Sender>) + { + return (Sender&&)sender; + } + constexpr ::exec::enter_scope_sender auto operator()() const noexcept { + return ::STDEXEC::just(::STDEXEC::just()); + } +}; + +template +inline constexpr bool nothrow_connect_sender = + ::STDEXEC::__nothrow_connectable< + Sender, + ::STDEXEC::__receiver_archetype> && + // Because we move the sender out of storage so we can reuse it + std::is_nothrow_move_constructible_v; + +template +using extra_set_error_signatures_t = std::conditional_t< + (nothrow_connect_sender || ...), + ::STDEXEC::completion_signatures<>, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_error_t(std::exception_ptr)>>; + +template +using remove_set_value_t = ::STDEXEC::completion_signatures<>; + +template +using storage_for_completion_signatures_t = + ::exec::storage_for_completion_signatures< + ::STDEXEC::transform_completion_signatures< + ::STDEXEC::__concat_completion_signatures_t< + // Because the first sender can be connected in the constructor it is + // handled differently + ::STDEXEC::completion_signatures_of_t, + ::STDEXEC::completion_signatures_of_t< + std::remove_cvref_t, + Env>...>, + // Same reason here as above, since we don't connect the first sender as + // part of the asynchronous part of the operation we don't need to check + // it here + extra_set_error_signatures_t, + remove_set_value_t>>; + +template +using exit_scope_sender_t = decltype( + nest_scopes::make_exit_scope_sender( + std::declval<::exec::exit_scope_sender_of_t>(), + std::declval<::exec::exit_scope_sender_of_t< + std::remove_cvref_t, + Env>>()...)); + +template +struct completions { + template + using __f = ::STDEXEC::__concat_completion_signatures_t< + typename storage_for_completion_signatures_t:: + completion_signatures, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t( + exit_scope_sender_t)>>; +}; + +template +class state_impl; +template +class state_impl, First, Rest...> { + using env_type_ = ::STDEXEC::env_of_t; + struct receiver_base_ { + using receiver_concept = ::STDEXEC::receiver_t; + state_impl& self_; + constexpr env_type_ get_env() const noexcept { + return ::STDEXEC::get_env(self_.r_); + } + }; + template + using exit_scope_sender_type_ = ::exec::exit_scope_sender_of_t< + std::tuple_element_t>, + env_type_>; + template + struct receiver_type_ : receiver_base_ { + using receiver_base_::self_; + constexpr void set_value(exit_scope_sender_type_ exit) && noexcept { + if constexpr (N) { + auto&& v = std::get(self_.storage_); + v.template emplace>(std::move(exit)); + } else { + auto&& o = std::get(self_.storage_); + o.emplace(std::move(exit)); + } + if constexpr (N == sizeof...(Rest)) { + std::apply( + [&](auto&& o, auto&&... vs) noexcept { + ::STDEXEC::set_value( + std::move(self_.r_), + nest_scopes::make_exit_scope_sender( + *(decltype(o)&&)o, + std::move(*std::get_if<1>(&vs))...)); + }, + std::move(self_.storage_)); + } else { + auto&& v = std::get(self_.storage_); + const auto ptr = std::get_if<0>(&v); + STDEXEC_ASSERT(ptr); + using sender_type = std::remove_cvref_t; + using receiver_type = receiver_type_; + constexpr bool nothrow = ::STDEXEC::__nothrow_connectable< + sender_type, + receiver_type>; + // Because we're about to destroy *this + auto&& self = self_; + try { + auto&& op = self.op_.template emplace( + ::exec::elide([&]() noexcept(nothrow) { + return ::STDEXEC::connect( + std::move(*ptr), + receiver_type{{self}}); + })); + ::STDEXEC::start(op); + } catch (...) { + if constexpr (nothrow) { + STDEXEC_UNREACHABLE(); + } else { + // It's important we use self and not self_ even here because + // despite an exception being thrown *this may still have been + // destroyed + self.completion_.arrive( + ::STDEXEC::set_error, + std::current_exception()); + // We successfully completed, the failure is because of the next + // operation, so we add one to ensure that we get rolled back + self.template rollback_(); + } + } + } + } + template + constexpr void set_error([[maybe_unused]] Args&&... args) && noexcept { + self_.completion_.arrive(::STDEXEC::set_error, (Args&&)args...); + self_.template rollback_(); + } + template + constexpr void set_stopped([[maybe_unused]] Args&&... args) && noexcept { + self_.completion_.arrive(::STDEXEC::set_stopped, (Args&&)args...); + self_.template rollback_(); + } + }; + template + struct rollback_receiver_type_ : receiver_base_ { + using receiver_base_::self_; + constexpr void set_value() && noexcept { + self_.template rollback_(); + } + }; + template + constexpr void rollback_() noexcept { + if constexpr (N) { + auto&& sender = [&]() noexcept -> decltype(auto) { + if constexpr (N - 1) { + auto&& v = std::get(storage_); + const auto ptr = std::get_if<1>(&v); + STDEXEC_ASSERT(ptr); + return *ptr; + } else { + auto&& o = std::get<0>(storage_); + STDEXEC_ASSERT(o); + return *o; + } + }(); + auto&& op = op_.template emplace( + ::exec::elide([&]() noexcept { + return ::STDEXEC::connect( + std::move(sender), + rollback_receiver_type_{{*this}}); + })); + ::STDEXEC::start(op); + } else { + std::move(completion_).complete(std::move(r_)); + } + } + using first_operation_state_type_ = ::STDEXEC::connect_result_t< + First, + receiver_type_<0>>; + Receiver r_; + std::variant< + first_operation_state_type_, + ::STDEXEC::connect_result_t< + Rest, + receiver_type_>..., + ::STDEXEC::connect_result_t< + exit_scope_sender_type_<0>, + rollback_receiver_type_<0>>, + ::STDEXEC::connect_result_t< + exit_scope_sender_type_, + rollback_receiver_type_>...> op_; + std::tuple< + std::optional>, + std::variant< + Rest, + exit_scope_sender_type_>...> storage_; + storage_for_completion_signatures_t completion_; +public: + template + explicit constexpr state_impl( + Receiver r, + First&& first, + Ts&&... rest) noexcept( + ::STDEXEC::__nothrow_connectable> && + (std::is_nothrow_constructible_v< + Rest, + Ts> && ...)) + : r_((Receiver&&)r), + op_( + std::in_place_type, + ::exec::elide([&]() noexcept( + ::STDEXEC::__nothrow_connectable>) + { + return ::STDEXEC::connect( + (First&&)first, + receiver_type_<0>{{*this}}); + })), + storage_( + std::nullopt, + (Ts&&)rest...) + {} + constexpr void start() & noexcept { + const auto ptr = std::get_if(&op_); + STDEXEC_ASSERT(ptr); + ::STDEXEC::start(*ptr); + } +}; + +template +class state : public state_impl< + Receiver, + std::index_sequence_for, + First, + Rest...> +{ + using base_ = state_impl< + Receiver, + std::index_sequence_for, + First, + Rest...>; +public: + using base_::base_; +}; + +// TODO: We should be able to get a better "message" than this +struct FAILED_TO_FORM_COMPLETION_SIGNATURES {}; + +template +struct connect_result { + template + using __f = state...>; +}; + +template +struct nothrow_connect { + template + using __f = std::is_nothrow_constructible< + typename connect_result::template __f, + Receiver, + First, + Rest...>; +}; + +class impl : public ::STDEXEC::__sexpr_defaults { + template + using completions_ = ::STDEXEC::__children_of>; +public: + template + static consteval auto get_completion_signatures() { + if constexpr (sizeof...(Env) == 0) { + return STDEXEC::__dependent_sender(); + } else if constexpr (::STDEXEC::__mvalid) { + return completions_{}; + } else { + return ::STDEXEC::__throw_compile_time_error< + FAILED_TO_FORM_COMPLETION_SIGNATURES, + ::STDEXEC::_WITH_PRETTY_SENDER_>(); + } + } + static constexpr auto connect = []( + Sender&& sender, Receiver r) noexcept( + ::STDEXEC::__children_of>::value) + -> ::STDEXEC::__children_of> + { + return ::STDEXEC::__apply( + [&](auto&&, auto&&, auto&&... children) noexcept( + ::STDEXEC::__children_of>::value) + -> ::STDEXEC::__children_of> + { + return ::STDEXEC::__children_of>( + (Receiver&&)r, + (decltype(children)&&)children...); + }, + (Sender&&)sender); + }; +}; + +} + +using nest_scopes_t = detail::nest_scopes::t; +inline constexpr nest_scopes_t nest_scopes; + +} // namespace exec + +namespace STDEXEC { + +template<> +struct __sexpr_impl<::exec::nest_scopes_t> : ::exec::detail::nest_scopes::impl +{}; + +} diff --git a/include/exec/object.hpp b/include/exec/object.hpp new file mode 100644 index 000000000..1e306ab0a --- /dev/null +++ b/include/exec/object.hpp @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * 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 +#include +#include + +#include "enter_scope_sender.hpp" + +namespace exec { + +template +concept object = + std::constructible_from< + std::remove_cvref_t, + Object> && + std::move_constructible< + std::remove_cvref_t> && + std::is_object_v< + typename std::remove_cvref_t::type> && + requires(Object o) { + { + std::invoke( + (Object&&)o, + (typename std::remove_cvref_t::type*)nullptr) } + -> enter_scope_sender; + }; + +template +concept object_in = + object && + requires(Object o) { + { + std::invoke( + (Object&&)o, + (typename std::remove_cvref_t::type*)nullptr) } + -> enter_scope_sender_in; + }; + +template +using type_of_object_t = std::remove_cvref_t::type; + +template +using enter_scope_sender_of_object_t = std::invoke_result_t< + Object, + type_of_object_t*>; + +} // namespace exec diff --git a/include/exec/storage_for_completion_signatures.hpp b/include/exec/storage_for_completion_signatures.hpp new file mode 100644 index 000000000..f6df423c6 --- /dev/null +++ b/include/exec/storage_for_completion_signatures.hpp @@ -0,0 +1,252 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * 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 "../stdexec/execution.hpp" + +#include +#include +#include +#include +#include + +namespace exec { + +namespace detail::storage_for_completion_signatures { + +template +struct decay { + using type = std::decay_t; +}; +template +struct decay { + using type = T&; +}; +template +struct decay { + using type = T&&; +}; + +template +struct tuple_for_signature; + +template +struct tuple_for_signature { + using type = std::tuple::type...>; +}; + +template +struct overload; +template +struct overload { + typename tuple_for_signature::type operator()(Tag, Args...) + const; +}; + +template +struct overload_set; +template +struct overload_set<::STDEXEC::completion_signatures> + : overload... +{ + using overload::operator()...; +}; + +template +using tuple_for_arrival = decltype( + overload_set{}( + Tag{}, + std::declval()...)); + +template +struct variant_for_signatures; + +template +struct variant_for_signatures< + ::STDEXEC::completion_signatures> +{ + using type = std::variant< + std::monostate, + typename tuple_for_signature::type...>; +}; + +template +struct signature; + +template +struct signature { + using type = Tag(typename decay::type...); +}; + +template +struct nothrow_visitable; + +template +struct nothrow_visitable { + static constexpr bool value = std::is_nothrow_invocable_v< + Visitor, + Tag, + typename decay::type...>; +}; + +template +struct nothrow_storable; + +template +struct nothrow_storable { + static constexpr bool value = ( + std::is_nothrow_constructible_v< + typename decay::type, + Args> && ...); +}; + +} + +template +class storage_for_completion_signatures; + +template<> +class storage_for_completion_signatures<::STDEXEC::completion_signatures<>> { +public: + using completion_signatures = ::STDEXEC::completion_signatures<>; + template + constexpr bool visit(Visitor&&) && noexcept { + return false; + } + template<::STDEXEC::receiver Receiver> + [[noreturn]] + constexpr void complete(Receiver&&) && noexcept { + STDEXEC_UNREACHABLE(); + } +}; + +template +class storage_for_completion_signatures< + ::STDEXEC::completion_signatures> +{ + using base_signatures_ = ::STDEXEC::completion_signatures< + typename detail::storage_for_completion_signatures::signature< + Signatures>::type...>; + static constexpr auto noexcept_ = + (detail::storage_for_completion_signatures::nothrow_storable< + Signatures>::value && ...); + using maybe_throwing_signature_ = std::conditional_t< + noexcept_, + ::STDEXEC::completion_signatures<>, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_error_t(std::exception_ptr)>>; +public: + using completion_signatures = ::STDEXEC::transform_completion_signatures< + base_signatures_, + maybe_throwing_signature_>; +private: + template + using tuple_type_ = + detail::storage_for_completion_signatures::tuple_for_arrival< + ::STDEXEC::completion_signatures, + Tag, + Args...>; + using storage_type_ = + typename detail::storage_for_completion_signatures::variant_for_signatures< + completion_signatures>::type; + storage_type_ storage_; + template + static constexpr bool nothrow_visitable_ = ( + detail::storage_for_completion_signatures::nothrow_visitable< + Visitor, + Signatures>::value && ...); +public: + template + requires std::is_constructible_v< + storage_type_, + std::in_place_type_t>, + Tag, + Args...> + constexpr void arrive(Tag t, Args&&... args) noexcept { + STDEXEC_ASSERT(std::holds_alternative(storage_)); + constexpr auto nothrow = std::is_nothrow_constructible_v< + tuple_type_, + Tag, + Args...>; + const auto impl = [&]() noexcept(nothrow) { + storage_.template emplace>( + (Tag&&)t, + (Args&&)args...); + }; + if constexpr (nothrow) { + impl(); + } else { + try { + impl(); + } catch (...) { + storage_.template emplace< + std::tuple< + ::STDEXEC::set_error_t, + std::exception_ptr>>( + ::STDEXEC::set_error, + std::current_exception()); + } + } + } + template + constexpr bool visit(Visitor&& visitor) && noexcept(nothrow_visitable_) + { + return std::visit( + [&](auto&& tuple_or_monostate) noexcept(nothrow_visitable_) { + if constexpr (std::is_same_v< + std::monostate, + std::remove_cvref_t>) + { + return false; + } else { + std::apply( + (Visitor&&)visitor, + (decltype(tuple_or_monostate)&&)tuple_or_monostate); + return true; + } + }, + (storage_type_&&)storage_); + } + template<::STDEXEC::receiver_of Receiver> + constexpr void complete(Receiver&& r) && noexcept { + std::visit( + [&](auto&& tuple_or_monostate) noexcept { + if constexpr (std::is_same_v< + std::monostate, + std::remove_cvref_t>) + { + STDEXEC_UNREACHABLE(); + } else { + std::apply( + [&](const auto tag, auto&&... args) noexcept { + tag((Receiver&&)r, (decltype(args)&&)args...); + }, + // Odds are this is inside an operation state, which means that + // sending the completion signal may end our lifetime, which means + // we shouldn't send references into ourselves, therefore we move + // all the non-references onto the stack + std::remove_cvref_t( + std::move(tuple_or_monostate))); + } + }, + (storage_type_&&)storage_); + } +}; + +} // namespace exec diff --git a/include/exec/sync_object.hpp b/include/exec/sync_object.hpp new file mode 100644 index 000000000..29967f1ad --- /dev/null +++ b/include/exec/sync_object.hpp @@ -0,0 +1,101 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * 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 +#include +#include +#include + +#include "elide.hpp" +#include "enter_scope_sender.hpp" +#include "like_t.hpp" +#include "../stdexec/execution.hpp" + +namespace exec { + +template + requires + std::is_constructible_v && + std::is_destructible_v +struct sync_object { + using type = T; + template + requires (std::is_constructible_v && ...) + constexpr explicit sync_object(Ts&&... ts) noexcept( + (std::is_nothrow_constructible_v && ...)) + : args_((Ts&&)ts...) + {} + constexpr enter_scope_sender auto operator()(type* storage) & + noexcept(noexcept(make_sender(*this, storage))) + { + return make_sender(*this, storage); + } + constexpr enter_scope_sender auto operator()(type* storage) const & + noexcept(noexcept(make_sender(*this, storage))) + { + return make_sender(*this, storage); + } + constexpr enter_scope_sender auto operator()(type* storage) && + noexcept(noexcept(make_sender(std::move(*this), storage))) + { + return make_sender(std::move(*this), storage); + } + constexpr enter_scope_sender auto operator()(type* storage) const && + noexcept(noexcept(make_sender(std::move(*this), storage))) + { + return make_sender(std::move(*this), storage); + } +private: + template + static constexpr enter_scope_sender auto make_sender(Self&& self, type* storage) + noexcept( + std::is_nothrow_constructible_v< + std::tuple, + like_t>>) + { + constexpr auto nothrow = std::is_nothrow_constructible_v; + return + ::STDEXEC::just(std::forward(self).args_) | + ::STDEXEC::then([storage](std::tuple&& tuple) noexcept(nothrow) { + const auto ptr = std::construct_at( + storage, + ::exec::elide([&]() noexcept(nothrow) { + return std::make_from_tuple(std::move(tuple)); + })); + return + ::STDEXEC::just() | + // It's important we capture ptr not storage because storage just + // points to storage where ptr actually points to an object + ::STDEXEC::then([ptr]() noexcept { + ptr->~T(); + }); + }); + } + std::tuple args_; +}; + +template +constexpr sync_object...> make_sync_object(Args&&... args) + noexcept((std::is_nothrow_constructible_v, Args> && ...)) +{ + return sync_object...>((Args&&)args...); +} + +} // namespace exec diff --git a/include/exec/within.hpp b/include/exec/within.hpp new file mode 100644 index 000000000..a49d1a4a8 --- /dev/null +++ b/include/exec/within.hpp @@ -0,0 +1,348 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * 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 +#include +#include +#include + +#include "elide.hpp" +#include "enter_scope_sender.hpp" +#include "like_t.hpp" +#include "storage_for_completion_signatures.hpp" +#include "../stdexec/execution.hpp" + +namespace exec { + +namespace detail::within { + +struct t { + template<::exec::enter_scope_sender Scope, ::STDEXEC::sender Sender> + constexpr ::STDEXEC::sender auto operator()(Scope&& scope, Sender&& sender) + const noexcept( + std::is_nothrow_constructible_v< + std::remove_cvref_t, + Scope> && + std::is_nothrow_constructible_v< + std::remove_cvref_t, + Sender>) + { + return ::STDEXEC::__make_sexpr( + ::STDEXEC::__(), + (Scope&&)scope, + (Sender&&)sender); + } +}; + +template +using remove_set_value_t = ::STDEXEC::completion_signatures<>; + +template +inline constexpr bool nothrow_connect_sender = + ::STDEXEC::__nothrow_connectable< + Sender, + ::STDEXEC::__receiver_archetype> && + // Because we move the sender out of storage so we can reuse it + std::is_nothrow_move_constructible_v; + +template +using completion_signatures_of_sender_t = + ::STDEXEC::transform_completion_signatures< + ::STDEXEC::completion_signatures_of_t, + std::conditional_t< + nothrow_connect_sender, + ::STDEXEC::completion_signatures<>, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_error_t(std::exception_ptr)>>>; + +template +using completion_signatures_of_scope_t = + ::STDEXEC::transform_completion_signatures< + ::STDEXEC::completion_signatures_of_t, + ::STDEXEC::completion_signatures<>, + remove_set_value_t>; + +template +using storage_for_completion_signatures_t = + ::exec::storage_for_completion_signatures< + completion_signatures_of_sender_t>; + +template +struct completions { + template + using __f = ::STDEXEC::transform_completion_signatures< + typename storage_for_completion_signatures_t< + std::remove_cvref_t, + Env>::completion_signatures, + completion_signatures_of_scope_t< + Scope, + Env>>; +}; + +template +class state { + using env_type_ = ::STDEXEC::env_of_t; + struct receiver_base_ { + using receiver_concept = ::STDEXEC::receiver_t; + constexpr env_type_ get_env() const noexcept; + state& self_; + }; + using exit_scope_sender_type_ = + ::exec::exit_scope_sender_of_t; + struct enter_receiver_type_ : receiver_base_ { + using receiver_base_::self_; + constexpr void set_value(exit_scope_sender_type_) && noexcept; + template + constexpr void set_error(Args&&...) && noexcept; + template + constexpr void set_stopped(Args&&...) && noexcept; + }; + using enter_operation_state_type_ = ::STDEXEC::connect_result_t< + Scope, + enter_receiver_type_>; + struct exit_receiver_type_ : receiver_base_ { + using receiver_base_::self_; + constexpr void set_value() && noexcept; + }; + using exit_operation_state_type_ = ::STDEXEC::connect_result_t< + exit_scope_sender_type_, + exit_receiver_type_>; + struct receiver_type_ : receiver_base_ { + using receiver_base_::self_; + template + constexpr void set_value(Args&&...) && noexcept; + template + constexpr void set_error(Args&&...) && noexcept; + template + constexpr void set_stopped(Args&&...) && noexcept; + }; + using operation_state_type_ = ::STDEXEC::connect_result_t< + Sender, + receiver_type_>; + Receiver r_; + storage_for_completion_signatures_t storage_; + struct enter_state_type_ { + enter_operation_state_type_ op; + Sender s; + }; + struct state_type_ { + operation_state_type_ op; + exit_scope_sender_type_ s; + }; + std::variant< + enter_state_type_, + state_type_, + exit_operation_state_type_> state_; + constexpr void exit_() noexcept { + const auto ptr = std::get_if(&state_); + auto sender = std::move(ptr->s); + auto&& op = state_.template emplace( + ::exec::elide([&]() noexcept { + return ::STDEXEC::connect( + std::move(sender), + exit_receiver_type_{{*this}}); + })); + ::STDEXEC::start(op); + } + template + constexpr void complete_(Args&&... args) noexcept { + storage_.arrive((Args&&)args...); + exit_(); + } + static constexpr bool nothrow_construct_connect_ = + ::STDEXEC::__nothrow_connectable; +public: + template + explicit constexpr state( + Scope&& scope, + S&& sender, + Receiver r) noexcept( + std::is_nothrow_constructible_v && + nothrow_construct_connect_) + : r_((Receiver&&)r), + state_( + std::in_place_type, + ::exec::elide([&]() noexcept(nothrow_construct_connect_) { + return enter_state_type_{ + ::STDEXEC::connect( + (Scope&&)scope, + enter_receiver_type_{{*this}}), + (S&&)sender}; + })) + { + } + constexpr void start() & noexcept { + const auto ptr = std::get_if(&state_); + STDEXEC_ASSERT(ptr); + ::STDEXEC::start(ptr->op); + } +}; + +template +constexpr auto state::receiver_base_::get_env() const + noexcept -> env_type_ +{ + return ::STDEXEC::get_env(self_.r_); +} + +template +constexpr void state::enter_receiver_type_::set_value( + exit_scope_sender_type_ exit) && noexcept +{ + // Because we're going to destroy the operation state and therefore + // transitively *this + auto&& self = self_; + constexpr auto nothrow = nothrow_connect_sender; + const auto ptr = std::get_if(&self.state_); + STDEXEC_ASSERT(ptr); + try { + auto sender = (Sender&&)ptr->s; + auto&& state = self.state_.template emplace( + ::exec::elide([&]() noexcept(nothrow) { + return state_type_{ + ::STDEXEC::connect( + (Sender&&)sender, + receiver_type_{{self}}), + std::move(exit)}; + })); + ::STDEXEC::start(state.op); + } catch (...) { + if constexpr (nothrow) { + STDEXEC_UNREACHABLE(); + } else { + ::STDEXEC::set_error((Receiver&&)self.r_, std::current_exception()); + } + } +} + +template +template +constexpr void state::enter_receiver_type_::set_error( + Args&&... args) && noexcept +{ + ::STDEXEC::set_error((Receiver&&)self_.r_, (Args&&)args...); +} + +template +template +constexpr void state::enter_receiver_type_:: + set_stopped(Args&&... args) && noexcept +{ + ::STDEXEC::set_stopped((Receiver&&)self_.r_, (Args&&)args...); +} + +template +constexpr void state::exit_receiver_type_::set_value() + && noexcept +{ + std::move(self_.storage_).complete((Receiver&&)self_.r_); +} + +template +template +constexpr void state::receiver_type_::set_value( + Args&&... args) && noexcept +{ + self_.complete_(::STDEXEC::set_value, (Args&&)args...); +} + +template +template +constexpr void state::receiver_type_::set_error( + Args&&... args) && noexcept +{ + self_.complete_(::STDEXEC::set_error, (Args&&)args...); +} + +template +template +constexpr void state::receiver_type_::set_stopped( + Args&&... args) && noexcept +{ + self_.complete_(::STDEXEC::set_stopped, (Args&&)args...); +} + +// TODO: We should be able to get a better "message" than this +struct FAILED_TO_FORM_COMPLETION_SIGNATURES {}; + +template +struct connect_result { + template + using __f = state, Receiver>; +}; + +template +struct nothrow_connect { + template + using __f = std::is_nothrow_constructible< + typename connect_result::template __f, + Scope, + Sender, + Receiver>; +}; + +class impl : public ::STDEXEC::__sexpr_defaults { + template + using completions_ = ::STDEXEC::__children_of>; +public: + template + static consteval auto get_completion_signatures() { + if constexpr (sizeof...(Env) == 0) { + return STDEXEC::__dependent_sender(); + } else if constexpr (::STDEXEC::__mvalid) { + return completions_{}; + } else { + return ::STDEXEC::__throw_compile_time_error< + FAILED_TO_FORM_COMPLETION_SIGNATURES, + ::STDEXEC::_WITH_PRETTY_SENDER_>(); + } + } + static constexpr auto connect = []( + Sender&& sender, Receiver r) noexcept( + ::STDEXEC::__children_of>::value) + -> ::STDEXEC::__children_of> + { + return ::STDEXEC::__apply( + [&](auto&&, auto&&, auto&& scope, auto&& sender) noexcept( + ::STDEXEC::__children_of>::value) + -> ::STDEXEC::__children_of> + { + return ::STDEXEC::__children_of>( + (decltype(scope)&&)scope, + (decltype(sender)&&)sender, + (Receiver&&)r); + }, + (Sender&&)sender); + }; +}; + +} + +using within_t = detail::within::t; +inline constexpr within_t within; + +} // namespace exec + +namespace STDEXEC { + +template<> +struct __sexpr_impl<::exec::within_t> : ::exec::detail::within::impl {}; + +} diff --git a/test/exec/CMakeLists.txt b/test/exec/CMakeLists.txt index 895dc5017..0216bc256 100644 --- a/test/exec/CMakeLists.txt +++ b/test/exec/CMakeLists.txt @@ -46,6 +46,8 @@ set(exec_test_sources test_static_thread_pool.cpp test_just_from.cpp test_fork.cpp + test_object.cpp + test_storage_for_completion_signatures.cpp sequence/test_any_sequence_of.cpp sequence/test_empty_sequence.cpp sequence/test_ignore_all_values.cpp @@ -61,6 +63,14 @@ set(exec_test_sources test_system_context.cpp $<$:test_libdispatch.cpp> test_unless_stop_requested.cpp + test_invoke.cpp + test_exit_scope_sender.cpp + test_enter_scope_sender.cpp + test_enter_scopes.cpp + test_within.cpp + test_lifetime.cpp + test_sync_object.cpp + test_nest_scopes.cpp ) add_executable(test.exec ${exec_test_sources}) diff --git a/test/exec/test_enter_scope_sender.cpp b/test/exec/test_enter_scope_sender.cpp new file mode 100644 index 000000000..97b7948ab --- /dev/null +++ b/test/exec/test_enter_scope_sender.cpp @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * 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. + */ + +#include + +#include +#include +#include + +namespace { + +static_assert( + !::exec::enter_scope_sender_in< + decltype(::STDEXEC::just()), + ::STDEXEC::env<>>); +static_assert( + ::exec::enter_scope_sender_in< + decltype(::STDEXEC::just(::STDEXEC::just())), + ::STDEXEC::env<>>); + +static_assert( + std::is_same_v< + decltype(::STDEXEC::just()), + ::exec::exit_scope_sender_of_t< + decltype(::STDEXEC::just(::STDEXEC::just())), + ::STDEXEC::env<>>>); +static_assert( + std::is_same_v< + decltype(::STDEXEC::just()), + ::exec::exit_scope_sender_of_t< + ::exec::variant_sender< + decltype(::STDEXEC::just(::STDEXEC::just())), + decltype(::STDEXEC::just_stopped())>, + ::STDEXEC::env<>>>); + +} // unnamed namespace diff --git a/test/exec/test_enter_scopes.cpp b/test/exec/test_enter_scopes.cpp new file mode 100644 index 000000000..6200cb25a --- /dev/null +++ b/test/exec/test_enter_scopes.cpp @@ -0,0 +1,180 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * 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. + */ + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "../test_common/receivers.hpp" +#include "../test_common/type_helpers.hpp" + +namespace { + +static_assert( + ::exec::enter_scope_sender_in< + decltype(::exec::enter_scopes()), + ::STDEXEC::env<>>); +static_assert( + ::exec::enter_scope_sender_in< + decltype(::exec::enter_scopes(::STDEXEC::just(::STDEXEC::just()))), + ::STDEXEC::env<>>); + +TEST_CASE("No scopes may be entered", "[enter_scopes]") { + bool invoked = false; + auto sender = ::exec::enter_scopes(); + static_assert( + ::exec::enter_scope_sender_in< + decltype(sender), + ::STDEXEC::env<>>); + auto op = ::STDEXEC::connect( + std::move(sender), + make_fun_receiver([&](auto&& sender) noexcept { + static_assert(::exec::exit_scope_sender); + CHECK(!invoked); + invoked = true; + auto op = ::STDEXEC::connect( + std::forward(sender), + expect_void_receiver{}); + ::STDEXEC::start(op); + })); + CHECK(!invoked); + ::STDEXEC::start(op); + CHECK(invoked); +} + +TEST_CASE("One scope may be entered", "[enter_scopes]") { + std::size_t done = 0; + std::size_t undone = 0; + auto undo = ::STDEXEC::just() | ::STDEXEC::then([&]() noexcept { + ++undone; + }); + auto succeed = ::STDEXEC::just() | ::STDEXEC::then([&]() noexcept { + ++done; + return undo; + }); + auto sender = ::exec::enter_scopes(succeed); + static_assert( + ::exec::enter_scope_sender_in< + decltype(sender), + ::STDEXEC::env<>>); + auto op = ::STDEXEC::connect( + std::move(sender), + make_fun_receiver([&](auto&& sender) noexcept { + static_assert(::exec::exit_scope_sender); + CHECK(done == 1); + CHECK(!undone); + auto op = ::STDEXEC::connect( + std::forward(sender), + expect_void_receiver{}); + CHECK(!undone); + ::STDEXEC::start(op); + CHECK(undone == 1); + })); + CHECK(!done); + CHECK(!undone); + ::STDEXEC::start(op); + CHECK(done == 1); + CHECK(undone == 1); +} + +TEST_CASE("Multiple scopes may be entered", "[enter_scopes]") { + std::size_t done = 0; + std::size_t undone = 0; + auto undo = ::STDEXEC::just() | ::STDEXEC::then([&]() noexcept { + ++undone; + }); + auto succeed = ::STDEXEC::just() | ::STDEXEC::then([&]() noexcept { + ++done; + return undo; + }); + auto sender = ::exec::enter_scopes(succeed, succeed); + static_assert( + ::exec::enter_scope_sender_in< + decltype(sender), + ::STDEXEC::env<>>); + auto op = ::STDEXEC::connect( + std::move(sender), + make_fun_receiver([&](auto&& sender) noexcept { + static_assert(::exec::exit_scope_sender); + CHECK(done == 2); + CHECK(!undone); + auto op = ::STDEXEC::connect( + std::forward(sender), + expect_void_receiver{}); + CHECK(!undone); + ::STDEXEC::start(op); + CHECK(undone == 2); + })); + CHECK(!done); + CHECK(!undone); + ::STDEXEC::start(op); + CHECK(done == 2); + CHECK(undone == 2); +} + +TEST_CASE("When an attempt is made to enter multiple scopes and entering one of them fails the effects of entering the other are undone", "[construct]") { + bool undone = false; + auto undo = ::STDEXEC::just() | ::STDEXEC::then([&]() noexcept { + CHECK(!undone); + undone = true; + }); + auto succeed = ::STDEXEC::just(undo); + auto fail = ::STDEXEC::just_error( + std::make_exception_ptr( + std::logic_error("TEST"))); + using enter_scope_sender_type = ::exec::variant_sender< + decltype(succeed), + decltype(fail)>; + static_assert( + ::exec::enter_scope_sender_in< + enter_scope_sender_type, + ::STDEXEC::env<>>); + auto sender = ::exec::enter_scopes( + enter_scope_sender_type(succeed), + enter_scope_sender_type(fail)); + static_assert( + ::exec::enter_scope_sender_in< + decltype(sender), + ::STDEXEC::env<>>); + static_assert( + set_equivalent< + ::STDEXEC::completion_signatures_of_t< + decltype(sender), + ::STDEXEC::env<>>, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t( + ::exec::exit_scope_sender_of_t< + decltype(sender), + ::STDEXEC::env<>>), + ::STDEXEC::set_error_t(std::exception_ptr)>>); + auto op = ::STDEXEC::connect(std::move(sender), expect_error_receiver{}); + CHECK(!undone); + ::STDEXEC::start(op); + CHECK(undone); +} + +} // unnamed namespace diff --git a/test/exec/test_exit_scope_sender.cpp b/test/exec/test_exit_scope_sender.cpp new file mode 100644 index 000000000..0929a5e67 --- /dev/null +++ b/test/exec/test_exit_scope_sender.cpp @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * 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. + */ + +#include + +#include +#include + +namespace { + +static_assert( + ::exec::exit_scope_sender_in< + decltype(::STDEXEC::just()), + ::STDEXEC::env<>>); +static_assert( + !::exec::exit_scope_sender_in< + decltype(::STDEXEC::just(5)), + ::STDEXEC::env<>>); + +} // unnamed namespace diff --git a/test/exec/test_invoke.cpp b/test/exec/test_invoke.cpp new file mode 100644 index 000000000..7c4464218 --- /dev/null +++ b/test/exec/test_invoke.cpp @@ -0,0 +1,92 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * 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. + */ + +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include "../test_common/receivers.hpp" +#include "../test_common/type_helpers.hpp" + +namespace { + +TEST_CASE("Values from a predecessor are used to predicate the successor", "[invoke]") { + auto f = [](int&& i) noexcept { + return ::STDEXEC::just(i * 2); + }; + auto predecessor = ::STDEXEC::just(5); + auto sender = predecessor | ::exec::invoke(f); + static_assert( + std::is_same_v< + ::exec::detail::invoke::completions< + decltype(predecessor), + decltype(f), + ::STDEXEC::env<>>, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(int)>>); + static_assert( + set_equivalent< + ::exec::detail::invoke::variant_for_operation_states< + decltype(f), + expect_value_receiver<::STDEXEC::env<>, int>, + std::tuple<>, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(int)>>::type, + std::variant< + ::STDEXEC::connect_result_t< + decltype(predecessor), + ::exec::detail::invoke::receiver_ref< + expect_value_receiver<::STDEXEC::env<>, int>>>>>); + static_assert( + std::is_same_v< + ::STDEXEC::completion_signatures_of_t< + decltype(sender), + ::STDEXEC::env<>>, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(int)>>); + auto op = ::STDEXEC::connect( + std::move(sender), + expect_value_receiver(10)); + ::STDEXEC::start(op); +} + +TEST_CASE("If the predecessor never completes successfully then the invocable is never invoked", "[invoke]") { + auto op = ::STDEXEC::connect( + // Notably 5 is not invocable + ::STDEXEC::just_stopped() | ::exec::invoke(5), + expect_stopped_receiver{}); + ::STDEXEC::start(op); +} + +TEST_CASE("When the invocable throws that exception is passed on", "[invoke]") { + auto op = ::STDEXEC::connect( + ::STDEXEC::just() | ::exec::invoke([]() -> decltype(::STDEXEC::just()) { + throw std::logic_error("TEST"); + }), + expect_error_receiver{}); + ::STDEXEC::start(op); +} + +} // unnamed namespace diff --git a/test/exec/test_lifetime.cpp b/test/exec/test_lifetime.cpp new file mode 100644 index 000000000..e418b20e0 --- /dev/null +++ b/test/exec/test_lifetime.cpp @@ -0,0 +1,145 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * 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. + */ + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "../test_common/receivers.hpp" + +namespace { + +struct state { + std::size_t& n; + std::optional constructed; + std::optional destroyed; +}; + +struct object { + state& s; + using type = int; + ::exec::enter_scope_sender auto operator()(int* storage) const noexcept { + return + ::STDEXEC::just() | + ::STDEXEC::then([&s = s, storage]() noexcept { + new(storage) int(5); + CHECK(!s.constructed); + s.constructed = s.n; + ++s.n; + return + ::STDEXEC::just() | + ::STDEXEC::then([&s]() noexcept { + CHECK(!s.destroyed); + s.destroyed = s.n; + ++s.n; + }); + }); + } +}; + +TEST_CASE("Zero async objects work", "[lifetime]") { + bool invoked = false; + auto sender = ::exec::lifetime( + [&]() { + CHECK(!invoked); + invoked = true; + return ::STDEXEC::just(); + }); + auto op = ::STDEXEC::connect( + std::move(sender), + expect_void_receiver{}); + CHECK(!invoked); + ::STDEXEC::start(op); + CHECK(invoked); +} + +TEST_CASE("Implementation detail sender factory works for a single async object", "[lifetime]") { + std::size_t n{}; + state s{n}; + std::tuple<::exec::detail::lifetime::storage_for_object> storage; + auto sender = ::exec::detail::lifetime::make_sender( + std::tuple( + [&](int& i) { + CHECK(i == 5); + return ::STDEXEC::just(); + }, + object{s}), + storage); + auto op = ::STDEXEC::connect( + std::move(sender), + expect_void_receiver{}); + CHECK(!n); + ::STDEXEC::start(op); + CHECK(n == 2); + CHECK(s.constructed == 0); + CHECK(s.destroyed == 1); +} + +TEST_CASE("Single async object works", "[lifetime]") { + std::size_t n{}; + state s{n}; + auto sender = ::exec::lifetime( + [&](int& i) { + CHECK(i == 5); + return ::STDEXEC::just(); + }, + object{s}); + auto op = ::STDEXEC::connect( + std::move(sender), + expect_void_receiver{}); + CHECK(!n); + ::STDEXEC::start(op); + CHECK(n == 2); + CHECK(s.constructed == 0); + CHECK(s.destroyed == 1); +} + +TEST_CASE("Multiple async objects work", "[lifetime]") { + std::size_t n{}; + state a{n}; + state b{n}; + auto sender = ::exec::lifetime( + [&](int& a, int& b) { + CHECK(a == 5); + CHECK(b == 5); + CHECK(&a != &b); + return ::STDEXEC::just(); + }, + object{a}, + object{b}); + auto op = ::STDEXEC::connect( + std::move(sender), + expect_void_receiver{}); + CHECK(!n); + ::STDEXEC::start(op); + CHECK(n == 4); + CHECK(a.constructed == 0); + CHECK(b.constructed == 1); + CHECK(a.destroyed == 2); + CHECK(b.destroyed == 3); +} + +} // unnamed namespace diff --git a/test/exec/test_nest_scopes.cpp b/test/exec/test_nest_scopes.cpp new file mode 100644 index 000000000..238b6c1e5 --- /dev/null +++ b/test/exec/test_nest_scopes.cpp @@ -0,0 +1,197 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * 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. + */ + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "../test_common/receivers.hpp" +#include "../test_common/type_helpers.hpp" + +namespace { + +TEST_CASE("nest_scopes exit sender exits scopes in reverse order", "[nest_scopes]") { + std::size_t n = 0; + std::optional a; + std::optional b; + auto sender = exec::detail::nest_scopes::make_exit_scope_sender( + ::STDEXEC::just() | ::STDEXEC::then([&]() { + CHECK(!a); + a = ++n; + }), + ::STDEXEC::just() | ::STDEXEC::then([&]() { + CHECK(!b); + b = ++n; + })); + auto op = ::STDEXEC::connect( + std::move(sender), + expect_void_receiver{}); + CHECK(!a); + CHECK(!b); + ::STDEXEC::start(op); + REQUIRE(a); + CHECK(*a == 2); + REQUIRE(b); + CHECK(*b == 1); +} + +TEST_CASE("Scopes can be nested", "[nest_scopes]") { + std::size_t n = 0; + std::optional a_entered; + std::optional a_exited; + auto a = ::STDEXEC::just() | ::STDEXEC::then([&]() noexcept { + CHECK(!a_entered); + a_entered = ++n; + return ::STDEXEC::just() | ::STDEXEC::then([&]() noexcept { + CHECK(!a_exited); + a_exited = ++n; + }); + }); + static_assert( + exec::detail::nest_scopes::nothrow_connect_sender< + decltype(a), + ::STDEXEC::env<>>); + static_assert( + !all_contained_in< + ::STDEXEC::completion_signatures< + ::STDEXEC::set_error_t(std::exception_ptr)>, + ::STDEXEC::completion_signatures_of_t< + decltype(a), + ::STDEXEC::env<>>>); + std::optional b_entered; + std::optional b_exited; + auto b = ::STDEXEC::just() | ::STDEXEC::then([&]() noexcept { + CHECK(!b_entered); + b_entered = ++n; + return ::STDEXEC::just() | ::STDEXEC::then([&]() noexcept { + CHECK(!b_exited); + b_exited = ++n; + }); + }); + static_assert( + exec::detail::nest_scopes::nothrow_connect_sender< + decltype(b), + ::STDEXEC::env<>>); + static_assert( + !all_contained_in< + ::STDEXEC::completion_signatures< + ::STDEXEC::set_error_t(std::exception_ptr)>, + ::STDEXEC::completion_signatures_of_t< + decltype(b), + ::STDEXEC::env<>>>); + auto sender = exec::nest_scopes(std::move(a), std::move(b)); + static_assert(exec::enter_scope_sender); + static_assert(exec::enter_scope_sender_in< + decltype(sender), + ::STDEXEC::env<>>); + static_assert( + !all_contained_in< + ::STDEXEC::completion_signatures< + ::STDEXEC::set_error_t(std::exception_ptr)>, + ::STDEXEC::completion_signatures_of_t< + decltype(sender), + ::STDEXEC::env<>>>); + auto op = ::STDEXEC::connect( + std::move(sender) | ::STDEXEC::let_value([&](auto exit) noexcept { + ++n; + return exit; + }), + expect_void_receiver{}); + CHECK(!a_entered); + CHECK(!a_exited); + CHECK(!b_entered); + CHECK(!b_exited); + ::STDEXEC::start(op); + REQUIRE(a_entered); + CHECK(*a_entered == 1); + REQUIRE(a_exited); + CHECK(*a_exited == 5); + REQUIRE(b_entered); + CHECK(*b_entered == 2); + REQUIRE(b_exited); + CHECK(*b_exited == 4); +} + +TEST_CASE("Nested scopes can fail thereby exiting all entered scopes", "[nest_scopes]") { + std::size_t n = 0; + std::optional a_entered; + std::optional a_exited; + auto a = ::STDEXEC::just() | ::STDEXEC::then([&]() noexcept { + CHECK(!a_entered); + a_entered = ++n; + return ::STDEXEC::just() | ::STDEXEC::then([&]() noexcept { + CHECK(!a_exited); + a_exited = ++n; + }); + }); + std::optional b_entered; + std::optional b_exited; + auto b = ::STDEXEC::just() | ::STDEXEC::then([&]() noexcept { + CHECK(!b_entered); + b_entered = ++n; + return ::STDEXEC::just() | ::STDEXEC::then([&]() noexcept { + CHECK(!b_exited); + b_exited = ++n; + }); + }); + auto sender = exec::nest_scopes( + std::move(a), + std::move(b), + ::STDEXEC::just() | ::STDEXEC::then([&]() -> decltype(::STDEXEC::just()) { + throw std::logic_error("TEST"); + })); + static_assert(exec::enter_scope_sender); + static_assert(exec::enter_scope_sender_in< + decltype(sender), + ::STDEXEC::env<>>); + static_assert( + all_contained_in< + ::STDEXEC::completion_signatures< + ::STDEXEC::set_error_t(std::exception_ptr)>, + ::STDEXEC::completion_signatures_of_t< + decltype(sender), + ::STDEXEC::env<>>>); + auto op = ::STDEXEC::connect( + std::move(sender) | ::STDEXEC::then([&](auto&&) noexcept { + FAIL_CHECK("Unexpected invocation!"); + }), + expect_error_receiver{}); + CHECK(!a_entered); + CHECK(!a_exited); + CHECK(!b_entered); + CHECK(!b_exited); + ::STDEXEC::start(op); + REQUIRE(a_entered); + CHECK(*a_entered == 1); + REQUIRE(a_exited); + CHECK(*a_exited == 4); + REQUIRE(b_entered); + CHECK(*b_entered == 2); + REQUIRE(b_exited); + CHECK(*b_exited == 3); +} + +} // unnamed namespace diff --git a/test/exec/test_object.cpp b/test/exec/test_object.cpp new file mode 100644 index 000000000..84ab3a953 --- /dev/null +++ b/test/exec/test_object.cpp @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * 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. + */ + +#include + +#include +#include +#include + +namespace { + +static_assert(::exec::object<::exec::sync_object>); +static_assert(::exec::object_in<::exec::sync_object, ::STDEXEC::env<>>); + +} // unnamed namespace diff --git a/test/exec/test_storage_for_completion_signatures.cpp b/test/exec/test_storage_for_completion_signatures.cpp new file mode 100644 index 000000000..4cafe5632 --- /dev/null +++ b/test/exec/test_storage_for_completion_signatures.cpp @@ -0,0 +1,296 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * 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. + */ + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../test_common/receivers.hpp" +#include "../test_common/type_helpers.hpp" + +using namespace exec; + +namespace { + +TEST_CASE("Storing no completion signatures works", "[storage_for_completion_signatures]") { + storage_for_completion_signatures< + ::STDEXEC::completion_signatures<>> storage; + static_assert( + std::is_same_v< + decltype(storage)::completion_signatures, + ::STDEXEC::completion_signatures<>>); + CHECK(!std::move(storage).visit([&](auto&&...) { + FAIL("Unexpected invocation of visitor"); + })); +} + +TEST_CASE("Storing simple completion signatures and then visiting them works", "[storage_for_completion_signatures]") { + using completion_signatures = ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(int), + ::STDEXEC::set_stopped_t(), + ::STDEXEC::set_error_t(std::error_code)>; + using storage = storage_for_completion_signatures< + completion_signatures>; + static_assert( + set_equivalent< + completion_signatures, + storage::completion_signatures>); + CHECK(!storage{}.visit([&](auto&&...) { + FAIL("Unexpected invocation of visitor"); + })); + struct base { + void operator()(::STDEXEC::set_stopped_t&&) && { + FAIL("Unexpected stop"); + } + void operator()(::STDEXEC::set_value_t&&, int&&) && { + FAIL("Unexpected value"); + } + void operator()(::STDEXEC::set_error_t&&, std::error_code&&) && { + FAIL("Unexpected error"); + } + bool invoked{false}; + protected: + void invoke_() { + CHECK(!invoked); + invoked = true; + } + }; + { + storage s; + static_assert( + noexcept( + s.arrive(::STDEXEC::set_stopped))); + s.arrive(::STDEXEC::set_stopped); + static_assert( + noexcept( + std::move(s).visit([](auto&&...) noexcept {}))); + struct : base { + using base::operator(); + void operator()(::STDEXEC::set_stopped_t&&) && { + invoke_(); + } + } visitor; + static_assert(!noexcept(std::move(s).visit(visitor))); + CHECK(std::move(s).visit(std::move(visitor))); + CHECK(visitor.invoked); + } + { + storage s; + static_assert( + noexcept( + s.arrive(::STDEXEC::set_value, 5))); + s.arrive(::STDEXEC::set_value, 5); + struct : base { + using base::operator(); + void operator()(::STDEXEC::set_value_t&&, int&& i) && { + invoke_(); + CHECK(i == 5); + } + } visitor; + static_assert(!noexcept(std::move(s).visit(visitor))); + CHECK(std::move(s).visit(std::move(visitor))); + CHECK(visitor.invoked); + } + { + storage s; + static_assert( + noexcept( + s.arrive(::STDEXEC::set_error, std::error_code{}))); + s.arrive( + ::STDEXEC::set_error, + make_error_code(std::errc::no_such_file_or_directory)); + struct : base { + using base::operator(); + void operator()(::STDEXEC::set_error_t&&, std::error_code&& ec) && { + invoke_(); + CHECK(ec == make_error_code(std::errc::no_such_file_or_directory)); + } + } visitor; + static_assert(!noexcept(std::move(s).visit(visitor))); + CHECK(std::move(s).visit(std::move(visitor))); + CHECK(visitor.invoked); + } +} + +TEST_CASE("Storing simple completion signatures and then completing a receiver therewith works", "[storage_for_completion_signatures]") { + using storage_type = + storage_for_completion_signatures< + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(), + ::STDEXEC::set_stopped_t(), + ::STDEXEC::set_error_t(std::exception_ptr)>>; + { + storage_type storage; + storage.arrive(::STDEXEC::set_value); + std::move(storage).complete(expect_void_receiver{}); + } + { + std::optional storage(std::in_place); + storage->arrive( + ::STDEXEC::set_error, + std::make_exception_ptr(std::logic_error("TEST"))); + std::exception_ptr ex; + struct receiver { + using receiver_concept = ::STDEXEC::receiver_t; + void set_value() noexcept { + FAIL("Unexpected value invocation"); + } + void set_stopped() noexcept { + FAIL("Unexpected stopped invocation"); + } + void set_error(std::exception_ptr&& ex) noexcept { + // This ensures that the exception_ptr is moved onto the stack + CHECK(storage_); + storage_.reset(); + CHECK(!ex_); + ex_ = std::move(ex); + } + std::optional& storage_; + std::exception_ptr& ex_; + }; + std::move(*storage).complete(receiver{storage, ex}); + REQUIRE(ex); + bool threw = false; + try { + std::rethrow_exception(std::move(ex)); + } catch (const std::logic_error& ex) { + threw = true; + CHECK(ex.what() == std::string_view("TEST")); + } + CHECK(threw); + } +} + +TEST_CASE("When storing a completion signature would throw it is simply coalesced to std::exception_ptr", "[storage_for_completion_signatures]") { + struct maybe_throws_on_move { + maybe_throws_on_move() = default; + maybe_throws_on_move(maybe_throws_on_move&& other) { + if (other.throws) { + throw std::runtime_error("Throwing as requested"); + } + } + bool throws{false}; + }; + { + using storage = storage_for_completion_signatures< + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(maybe_throws_on_move)>>; + static_assert( + set_equivalent< + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(maybe_throws_on_move), + ::STDEXEC::set_error_t(std::exception_ptr)>, + storage::completion_signatures>); + struct base { + void operator()(::STDEXEC::set_value_t&&, maybe_throws_on_move&&) & { + FAIL("Unexpected value invocation"); + } + void operator()(::STDEXEC::set_error_t&&, std::exception_ptr&&) & { + FAIL("Unexpected error invocation"); + } + bool invoked{false}; + protected: + void invoke_() { + CHECK(!invoked); + invoked = true; + } + }; + { + storage s; + static_assert( + noexcept( + s.arrive(::STDEXEC::set_value, maybe_throws_on_move{}))); + maybe_throws_on_move obj; + obj.throws = true; + s.arrive(::STDEXEC::set_value, std::move(obj)); + struct : base { + using base::operator(); + void operator()(::STDEXEC::set_error_t&&, std::exception_ptr&& ex) & { + invoke_(); + REQUIRE(ex); + // TODO? + } + } visitor; + CHECK(std::move(s).visit(visitor)); + CHECK(visitor.invoked); + } + { + storage s; + s.arrive(::STDEXEC::set_value, maybe_throws_on_move{}); + struct : base { + using base::operator(); + void operator()(::STDEXEC::set_value_t&&, maybe_throws_on_move&&) & { + invoke_(); + } + } visitor; + CHECK(std::move(s).visit(visitor)); + CHECK(visitor.invoked); + } + } + // Important that the below cases don't add the std::exception_ptr completion + // since propagating a reference can't throw + { + using signatures = ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(maybe_throws_on_move&)>; + using storage = storage_for_completion_signatures; + static_assert( + std::is_same_v< + storage::completion_signatures, + signatures>); + maybe_throws_on_move obj; + storage s; + s.arrive(::STDEXEC::set_value, obj); + bool invoked = false; + CHECK(std::move(s).visit([&](::STDEXEC::set_value_t, maybe_throws_on_move& stored) { + CHECK(!invoked); + invoked = true; + CHECK(&obj == &stored); + })); + CHECK(invoked); + } + { + using signatures = ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(maybe_throws_on_move&&)>; + using storage = storage_for_completion_signatures; + static_assert( + std::is_same_v< + storage::completion_signatures, + signatures>); + maybe_throws_on_move obj; + storage s; + s.arrive(::STDEXEC::set_value, std::move(obj)); + bool invoked = false; + CHECK(std::move(s).visit([&](::STDEXEC::set_value_t, maybe_throws_on_move&& stored) { + CHECK(!invoked); + invoked = true; + CHECK(&obj == &stored); + })); + CHECK(invoked); + } +} + +} // unnamed namespace diff --git a/test/exec/test_sync_object.cpp b/test/exec/test_sync_object.cpp new file mode 100644 index 000000000..71215cfb6 --- /dev/null +++ b/test/exec/test_sync_object.cpp @@ -0,0 +1,103 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * 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. + */ + +#include + +#include +#include +#include + +#include + +#include +#include + +#include "../test_common/receivers.hpp" + +namespace { + +struct state { + std::size_t constructed{0}; + std::size_t destroyed{0}; +}; + +class object { + state& s_; +public: + int i; + explicit constexpr object(state& s, int i) noexcept : s_(s), i(i) { + ++s_.constructed; + } + object(const object&) = delete; + object& operator=(const object&) = delete; + constexpr ~object() noexcept { + ++s_.destroyed; + } +}; + +// GCC 14 complains about an object being used outside its lifetime trying to +// build this, but doesn't really give any clues about which object so it's +// difficult to address +#ifdef __clang__ +static_assert([]() { + state s; + struct receiver { + using receiver_concept = ::STDEXEC::receiver_t; + bool& b_; + constexpr void set_value(const int i) && noexcept { + b_ = i == 5; + } + }; + auto sender = ::exec::lifetime( + [&](object& o) noexcept { + return ::STDEXEC::just(o.i); + }, + ::exec::make_sync_object( + std::ref(s), + 5)); + bool success = false; + auto op = ::STDEXEC::connect( + std::move(sender), + receiver{success}); + ::STDEXEC::start(op); + return success; +}()); +#endif + +TEST_CASE("Synchronous object may be adapted into asynchronous objects", "[sync_object]") { + state s; + auto sender = ::exec::lifetime( + [&](object& o) { + CHECK(s.constructed == 1); + CHECK(s.destroyed == 0); + return ::STDEXEC::just(o.i); + }, + ::exec::make_sync_object( + std::ref(s), + 5)); + auto op = ::STDEXEC::connect( + std::move(sender), + expect_value_receiver(5)); + CHECK(s.constructed == 0); + CHECK(s.destroyed == 0); + ::STDEXEC::start(op); + CHECK(s.constructed == 1); + CHECK(s.destroyed == 1); +} + +} // unnamed namespace diff --git a/test/exec/test_within.cpp b/test/exec/test_within.cpp new file mode 100644 index 000000000..36ca4917e --- /dev/null +++ b/test/exec/test_within.cpp @@ -0,0 +1,118 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * 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. + */ + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "../test_common/receivers.hpp" + +namespace { + +TEST_CASE("The scope is entered, the wrapped sender is run, and then the scope is exited", "[within]") { + std::size_t n{}; + std::optional entered; + std::optional executed; + std::optional exited; + auto enter = + ::STDEXEC::just() | + ::STDEXEC::then([&]() noexcept { + CHECK(!entered); + entered = n; + ++n; + return + ::STDEXEC::just() | + ::STDEXEC::then([&]() noexcept { + CHECK(!exited); + exited = n; + ++n; + }); + }); + auto sender = + ::STDEXEC::just() | + ::STDEXEC::then([&]() noexcept { + CHECK(!executed); + executed = n; + ++n; + }); + auto within = ::exec::within(enter, sender); + static_assert( + std::is_same_v< + ::STDEXEC::completion_signatures_of_t< + decltype(within), + ::STDEXEC::env<>>, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t()>>); + auto op = ::STDEXEC::connect( + std::move(within), + expect_void_receiver{}); + CHECK(!n); + ::STDEXEC::start(op); + CHECK(entered == 0); + CHECK(executed == 1); + CHECK(exited == 2); +} + +TEST_CASE("If the work throws the scope is still exited", "[within]") { + std::size_t n{}; + std::optional entered; + std::optional executed; + std::optional exited; + auto enter = + ::STDEXEC::just() | + ::STDEXEC::then([&]() noexcept { + CHECK(!entered); + entered = n; + ++n; + return + ::STDEXEC::just() | + ::STDEXEC::then([&]() noexcept { + CHECK(!exited); + exited = n; + ++n; + }); + }); + auto sender = + ::STDEXEC::just() | + ::STDEXEC::then([&]() { + CHECK(!executed); + executed = n; + ++n; + throw std::logic_error("TEST"); + }); + auto op = ::STDEXEC::connect( + ::exec::within( + enter, + sender), + expect_error_receiver{}); + CHECK(!n); + ::STDEXEC::start(op); + CHECK(entered == 0); + CHECK(executed == 1); + CHECK(exited == 2); +} + +} // unnamed namespace