diff --git a/.gitignore b/.gitignore index 14a93b42..beb007d0 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ build .vs .cache .vscode +.idea compile_commands.json stagedir diff --git a/include/beman/execution/detail/associate.hpp b/include/beman/execution/detail/associate.hpp index a9adf72d..0eea2a34 100644 --- a/include/beman/execution/detail/associate.hpp +++ b/include/beman/execution/detail/associate.hpp @@ -14,136 +14,154 @@ #include #include #include -#include -#include +#include +#include #include // ---------------------------------------------------------------------------- namespace beman::execution::detail { -template <::beman::execution::scope_token Token, - ::beman::execution::sender Sender> //-dk:TODO detail export +template <::beman::execution::scope_token Token, ::beman::execution::sender Sender> struct associate_data { using wrap_sender = ::std::remove_cvref_t().wrap(::std::declval()))>; + using assoc_t = decltype(::std::declval().try_associate()); + using sender_ref = ::std::unique_ptr; + + explicit associate_data(Token t, Sender&& s) : sender(t.wrap(::std::forward(s))) { + sender_ref guard{::std::addressof(this->sender)}; + this->assoc = t.try_associate(); + if (this->assoc) { + static_cast(guard.release()); + } + } - explicit associate_data(Token t, Sender&& s) : token(t), sender(this->token.wrap(::std::forward(s))) { - if (!token.try_associate()) { - this->sender.reset(); + explicit associate_data(::std::pair parts) : assoc(::std::move(parts.first)) { + if (this->assoc) { + ::std::construct_at(::std::addressof(this->sender), ::std::move(*parts.second)); } } + associate_data(const associate_data& other) noexcept(::std::is_nothrow_copy_constructible_v && - noexcept(token.try_associate())) - : token(other.token), sender() { - if (other.sender && this->token.try_associate()) { - try { - this->sender.emplace(*other.sender); - } catch (...) { - this->token.disassociate(); - } + noexcept(other.assoc.try_associate())) + requires ::std::copy_constructible + : assoc(other.assoc.try_associate()) { + if (this->assoc) { + ::std::construct_at(::std::addressof(this->sender), other.sender); } } + associate_data(associate_data&& other) noexcept(::std::is_nothrow_move_constructible_v) - : token(other.token), sender(::std::move(other.sender)) { - other.sender.reset(); - } + : associate_data(::std::move(other).release()) {} + auto operator=(const associate_data&) -> associate_data& = delete; - auto operator=(associate_data&&) -> associate_data& = delete; + + auto operator=(associate_data&&) -> associate_data& = delete; + ~associate_data() { - if (this->sender) { - this->sender.reset(); - this->token.disassociate(); + if (this->assoc) { + ::std::destroy_at(::std::addressof(this->sender)); } } - auto release() -> ::std::optional<::std::pair> { - return this->sender ? (std::unique_ptr, decltype([](auto* opt) { opt->reset(); })>( - &this->sender), - ::std::optional{::std::pair{::std::move(this->token), ::std::move(*this->sender)}}) - : ::std::optional<::std::pair>{}; + auto release() && noexcept -> ::std::pair { + return {::std::move(assoc), sender_ref{assoc ? ::std::addressof(this->sender) : nullptr}}; } - Token token; - ::std::optional sender; + assoc_t assoc; + union { + wrap_sender sender; + }; }; template <::beman::execution::scope_token Token, ::beman::execution::sender Sender> associate_data(Token, Sender&&) -> associate_data; struct associate_t { template <::beman::execution::sender Sender, ::beman::execution::scope_token Token> - auto operator()(Sender&& sender, Token&& token) const { + auto operator()(Sender&& sender, Token token) const { auto domain(::beman::execution::detail::get_domain_early(sender)); return ::beman::execution::transform_sender( domain, ::beman::execution::detail::make_sender( *this, - ::beman::execution::detail::associate_data(::std::forward(token), - ::std::forward(sender)))); + ::beman::execution::detail::associate_data(::std::move(token), ::std::forward(sender)))); + } + + template <::beman::execution::scope_token Token> + auto operator()(Token token) const { + return ::beman::execution::detail::sender_adaptor{*this, ::std::move(token)}; } }; template <> struct impls_for : ::beman::execution::detail::default_impls { - template - struct get_noexcept : ::std::false_type {}; - template - struct get_noexcept<::beman::execution::detail::basic_sender, Receiver> - : ::std::bool_constant< - ::std::is_nothrow_move_constructible_v::wrap_sender>&& ::beman:: - execution::detail::nothrow_callable<::beman::execution::connect_t, - typename ::std::remove_cvref_t::wrap_sender, - Receiver>> {}; + template + struct get_wrap_sender; + + template + struct get_wrap_sender<::beman::execution::detail::basic_sender> { + using type = typename ::std::remove_cvref_t::wrap_sender; + }; + + template + struct op_state { + using assoc_t = typename AssociateData::assoc_t; + using sender_ref_t = typename AssociateData::sender_ref; + using op_t = ::beman::execution::connect_result_t; + + assoc_t assoc; + union { + Receiver* rcvr; + op_t op; + }; + + explicit op_state(::std::pair parts, Receiver& r) : assoc(::std::move(parts.first)) { + if (assoc) { + ::std::construct_at(::std::addressof(op), + ::beman::execution::connect(::std::move(*parts.second), ::std::move(r))); + } else { + rcvr = ::std::addressof(r); + } + } + + explicit op_state(AssociateData&& ad, Receiver& r) : op_state(::std::move(ad).release(), r) {} + + explicit op_state(const AssociateData& ad, Receiver& r) + requires ::std::copy_constructible + : op_state(AssociateData(ad).release(), r) {} + + op_state(const op_state&) = delete; + + op_state(op_state&&) = delete; + + ~op_state() { + if (this->assoc) { + op.~op_t(); + } + } + + auto operator=(const op_state&) -> op_state& = delete; + + auto operator=(op_state&&) -> op_state& = delete; + + auto run() noexcept -> void { + if (this->assoc) { + ::beman::execution::start(this->op); + } else { + ::beman::execution::set_stopped(::std::move(*this->rcvr)); + } + } + }; struct get_state_impl { template auto operator()(Sender&& sender, Receiver& receiver) const - noexcept(::std::is_nothrow_constructible_v<::std::remove_cvref_t, Sender> && - get_noexcept<::std::remove_cvref_t, Receiver>::value) { + noexcept((::std::same_as> || + ::std::is_nothrow_constructible_v<::std::remove_cvref_t, Sender>) && + execution::detail::nothrow_callable<::beman::execution::connect_t, + typename get_wrap_sender<::std::remove_cvref_t>::type, + Receiver>) { auto [_, data] = ::std::forward(sender); - auto dataParts{data.release()}; - - using scope_token = decltype(dataParts->first); - using wrap_sender = decltype(dataParts->second); - using op_t = decltype(::beman::execution::connect(::std::move(dataParts->second), - ::std::forward(receiver))); - - struct op_state { - using sop_t = op_t; - using sscope_token = scope_token; - struct assoc_t { - sscope_token tok; - sop_t op; - }; - - bool associated{false}; - union { - Receiver* rcvr; - assoc_t assoc; - }; - explicit op_state(Receiver& r) noexcept : rcvr(::std::addressof(r)) {} - explicit op_state(sscope_token tk, wrap_sender&& sndr, Receiver& r) try - : associated(true), assoc(tk, ::beman::execution::connect(::std::move(sndr), ::std::move(r))) { - } catch (...) { - tk.disassociate(); - throw; - } - op_state(op_state&&) = delete; - ~op_state() { - if (this->associated) { - this->assoc.op.~sop_t(); - this->assoc.tok.disassociate(); - this->assoc.tok.~sscope_token(); - } - } - auto run() noexcept -> void { - if (this->associated) { - ::beman::execution::start(this->assoc.op); - } else { - ::beman::execution::set_stopped(::std::move(*this->rcvr)); - } - } - }; - return dataParts ? op_state(::std::move(dataParts->first), ::std::move(dataParts->second), receiver) - : op_state(receiver); + return op_state{::beman::execution::detail::forward_like(data), receiver}; } }; static constexpr auto get_state{get_state_impl{}}; @@ -157,7 +175,8 @@ template struct completion_signatures_for_impl< ::beman::execution::detail::basic_sender<::beman::execution::detail::associate_t, Data>, Env> { - using type = ::beman::execution::completion_signatures<::beman::execution::set_value_t()>; + using child_type_t = typename ::std::remove_cvref_t::wrap_sender; + using type = ::beman::execution::detail::completion_signatures_for; }; } // namespace beman::execution::detail diff --git a/include/beman/execution/detail/counting_scope.hpp b/include/beman/execution/detail/counting_scope.hpp index 413aab92..a3fae783 100644 --- a/include/beman/execution/detail/counting_scope.hpp +++ b/include/beman/execution/detail/counting_scope.hpp @@ -38,7 +38,7 @@ class beman::execution::counting_scope : public ::beman::execution::detail::coun // ---------------------------------------------------------------------------- -class beman::execution::counting_scope::token : public ::beman::execution::detail::counting_scope_base::token { +class beman::execution::counting_scope::token : public beman::execution::counting_scope::token_base { public: template <::beman::execution::sender Sender> auto wrap(Sender&& sender) const noexcept -> ::beman::execution::sender auto { @@ -49,8 +49,7 @@ class beman::execution::counting_scope::token : public ::beman::execution::detai private: friend class beman::execution::counting_scope; - explicit token(::beman::execution::counting_scope* s) - : ::beman::execution::detail::counting_scope_base::token(s) {} + explicit token(::beman::execution::counting_scope* s) : token_base(s) {} }; static_assert(::beman::execution::scope_token<::beman::execution::counting_scope::token>); diff --git a/include/beman/execution/detail/counting_scope_base.hpp b/include/beman/execution/detail/counting_scope_base.hpp index 4f5b722f..32eb394b 100644 --- a/include/beman/execution/detail/counting_scope_base.hpp +++ b/include/beman/execution/detail/counting_scope_base.hpp @@ -37,14 +37,47 @@ class beman::execution::detail::counting_scope_base : ::beman::execution::detail auto start_node(node*) -> void; protected: - class token { + class assoc_t { public: - auto try_associate() const noexcept -> bool { return this->scope->try_associate(); } - auto disassociate() const noexcept -> void { this->scope->disassociate(); } + assoc_t() = default; + + explicit assoc_t(counting_scope_base& scope) noexcept : scope(&scope) {} + + assoc_t(const assoc_t&) = delete; + + assoc_t(assoc_t&& other) noexcept : scope(::std::exchange(other.scope, nullptr)) {} + + ~assoc_t() { + if (this->scope) { + this->scope->disassociate(); + } + } + + auto operator=(assoc_t other) noexcept -> assoc_t& { + std::swap(scope, other.scope); + return *this; + } + + explicit operator bool() const noexcept { return this->scope != nullptr; } + + auto try_associate() const noexcept -> assoc_t { + if (this->scope) { + return this->scope->try_associate(); + } + return assoc_t{}; + } + + private: + counting_scope_base* scope = nullptr; + }; + + class token_base { + public: + auto try_associate() const noexcept -> assoc_t { return this->scope->try_associate(); } protected: - explicit token(::beman::execution::detail::counting_scope_base* s) : scope(s) {} - ::beman::execution::detail::counting_scope_base* scope; + explicit token_base(counting_scope_base* s) : scope(s) {} + counting_scope_base* scope; }; private: @@ -58,10 +91,10 @@ class beman::execution::detail::counting_scope_base : ::beman::execution::detail joined }; - auto try_associate() noexcept -> bool; + auto try_associate() noexcept -> assoc_t; auto disassociate() noexcept -> void; auto complete() noexcept -> void; - auto add_node(node* n, ::std::lock_guard<::std::mutex>&) noexcept -> void; + auto add_node(node* n) noexcept -> void; ::std::mutex mutex; //-dk:TODO fuse state and count and use atomic accesses @@ -100,23 +133,22 @@ inline auto beman::execution::detail::counting_scope_base::close() noexcept -> v } } -inline auto beman::execution::detail::counting_scope_base::add_node(node* n, ::std::lock_guard<::std::mutex>&) noexcept - -> void { +inline auto beman::execution::detail::counting_scope_base::add_node(node* n) noexcept -> void { n->next = std::exchange(this->head, n); } -inline auto beman::execution::detail::counting_scope_base::try_associate() noexcept -> bool { +inline auto beman::execution::detail::counting_scope_base::try_associate() noexcept -> assoc_t { ::std::lock_guard lock(this->mutex); switch (this->state) { default: - return false; + return assoc_t{}; case state_t::unused: this->state = state_t::open; // fall-through! [[fallthrough]]; case state_t::open: case state_t::open_and_joining: ++this->count; - return true; + return assoc_t{*this}; } } @@ -141,12 +173,13 @@ inline auto beman::execution::detail::counting_scope_base::complete() noexcept - } inline auto beman::execution::detail::counting_scope_base::start_node(node* n) -> void { - ::std::lock_guard kerberos(this->mutex); + ::std::unique_lock guard(this->mutex); switch (this->state) { case ::beman::execution::detail::counting_scope_base::state_t::unused: case ::beman::execution::detail::counting_scope_base::state_t::unused_and_closed: case ::beman::execution::detail::counting_scope_base::state_t::joined: this->state = ::beman::execution::detail::counting_scope_base::state_t::joined; + guard.unlock(); n->complete_inline(); return; case ::beman::execution::detail::counting_scope_base::state_t::open: @@ -160,7 +193,7 @@ inline auto beman::execution::detail::counting_scope_base::start_node(node* n) - case ::beman::execution::detail::counting_scope_base::state_t::closed_and_joining: break; } - this->add_node(n, kerberos); + this->add_node(n); } // ---------------------------------------------------------------------------- diff --git a/include/beman/execution/detail/scope_association.hpp b/include/beman/execution/detail/scope_association.hpp new file mode 100644 index 00000000..61daf44d --- /dev/null +++ b/include/beman/execution/detail/scope_association.hpp @@ -0,0 +1,24 @@ +// include/beman/execution/detail/scope_association.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef INCLUDED_BEMAN_EXECUTION_DETAIL_SCOPE_ASSOCIATION +#define INCLUDED_BEMAN_EXECUTION_DETAIL_SCOPE_ASSOCIATION + +#include +#include + +// ---------------------------------------------------------------------------- + +namespace beman::execution { +template +concept scope_association = + ::std::movable && ::std::is_nothrow_move_constructible_v && + ::std::is_nothrow_move_assignable_v && ::std::default_initializable && requires(const Assoc assoc) { + { static_cast(assoc) } noexcept; + { assoc.try_associate() } -> ::std::same_as; + }; +} // namespace beman::execution + +// ---------------------------------------------------------------------------- + +#endif // INCLUDED_BEMAN_EXECUTION_DETAIL_SCOPE_ASSOCIATION diff --git a/include/beman/execution/detail/scope_token.hpp b/include/beman/execution/detail/scope_token.hpp index 7ace3ca3..34d8c6e3 100644 --- a/include/beman/execution/detail/scope_token.hpp +++ b/include/beman/execution/detail/scope_token.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -30,9 +31,8 @@ static_assert(::beman::execution::sender_in<::beman::execution::detail::token_te namespace beman::execution { template -concept scope_token = ::std::copyable && requires(Token token) { - { token.try_associate() } -> ::std::same_as; - { token.disassociate() } noexcept; +concept scope_token = ::std::copyable && requires(const Token token) { + { token.try_associate() } -> ::beman::execution::scope_association; { token.wrap(::std::declval<::beman::execution::detail::token_test_sender>()) } -> ::beman::execution::sender_in<::beman::execution::detail::token_test_env>; diff --git a/include/beman/execution/detail/simple_counting_scope.hpp b/include/beman/execution/detail/simple_counting_scope.hpp index ddad87aa..66849f2f 100644 --- a/include/beman/execution/detail/simple_counting_scope.hpp +++ b/include/beman/execution/detail/simple_counting_scope.hpp @@ -31,7 +31,7 @@ class beman::execution::simple_counting_scope : public ::beman::execution::detai // ---------------------------------------------------------------------------- -class beman::execution::simple_counting_scope::token : public ::beman::execution::detail::counting_scope_base::token { +class beman::execution::simple_counting_scope::token : public beman::execution::simple_counting_scope::token_base { public: template <::beman::execution::sender Sender> auto wrap(Sender&& sender) const noexcept -> Sender&& { @@ -40,8 +40,7 @@ class beman::execution::simple_counting_scope::token : public ::beman::execution private: friend class beman::execution::simple_counting_scope; - explicit token(::beman::execution::detail::counting_scope_base* s) - : ::beman::execution::detail::counting_scope_base::token(s) {} + explicit token(::beman::execution::detail::counting_scope_base* s) : token_base(s) {} }; // ---------------------------------------------------------------------------- diff --git a/include/beman/execution/detail/spawn.hpp b/include/beman/execution/detail/spawn.hpp index fd058303..8b19e554 100644 --- a/include/beman/execution/detail/spawn.hpp +++ b/include/beman/execution/detail/spawn.hpp @@ -37,21 +37,23 @@ struct spawn_t { template struct state : state_base { using op_t = ::beman::execution::connect_result_t; + using assoc_t = ::std::remove_cvref_t().try_associate())>; using alloc_t = typename ::std::allocator_traits::template rebind_alloc; using traits_t = ::std::allocator_traits; state(Alloc a, Sndr&& sndr, Tok tok) - : alloc(a), op(::beman::execution::connect(::std::forward(sndr), receiver{this})), token(tok) { - if (this->token.try_associate()) { + : alloc(a), + op(::beman::execution::connect(::std::forward(sndr), receiver{this})), + assoc(tok.try_associate()) { + if (this->assoc) { ::beman::execution::start(this->op); } else { this->destroy(); } } auto complete() noexcept -> void override { - Tok tok(this->token); + assoc_t _ = ::std::move(this->assoc); this->destroy(); - tok.disassociate(); } auto destroy() noexcept -> void { alloc_t all(this->alloc); @@ -61,7 +63,7 @@ struct spawn_t { alloc_t alloc; op_t op; - Tok token; + assoc_t assoc; }; template <::beman::execution::sender Sender, ::beman::execution::scope_token Token, typename Env> diff --git a/include/beman/execution/detail/spawn_future.hpp b/include/beman/execution/detail/spawn_future.hpp index e3f8d646..884e6826 100644 --- a/include/beman/execution/detail/spawn_future.hpp +++ b/include/beman/execution/detail/spawn_future.hpp @@ -116,6 +116,7 @@ template > { using alloc_t = typename ::std::allocator_traits::template rebind_alloc; + using assoc_t = ::std::remove_cvref_t().try_associate())>; using traits_t = ::std::allocator_traits; using spawned_sender_t = ::beman::execution::detail::future_spawned_sender; using sigs_t = ::beman::execution::detail::spawn_future_sigs; @@ -130,9 +131,8 @@ struct spawn_future_state op(::beman::execution::write_env( ::beman::execution::detail::stop_when(::std::forward(s), source.get_token()), env), receiver_t{this}), - token(::std::move(tok)), - associated(token.try_associate()) { - if (this->associated) { + assoc(tok.try_associate()) { + if (this->assoc) { ::beman::execution::start(this->op); } else { ::beman::execution::set_stopped(receiver_t{this}); @@ -193,24 +193,17 @@ struct spawn_future_state spawn_future_state::complete_receiver(rcvr, this->result); } auto destroy() noexcept -> void { - Token tok{this->token}; - bool assoc{this->associated}; - { - alloc_t a{this->alloc}; - traits_t::destroy(a, this); - traits_t::deallocate(a, this, 1u); - } - if (assoc) { - tok.disassociate(); - } + assoc_t _ = ::std::move(this->assoc); + alloc_t a{this->alloc}; + traits_t::destroy(a, this); + traits_t::deallocate(a, this, 1u); } ::std::mutex gate{}; alloc_t alloc; ::beman::execution::inplace_stop_source source{}; op_t op; - Token token; - bool associated{false}; + assoc_t assoc; void* receiver{}; auto (*fun)(void*, spawn_future_state&) noexcept -> void = nullptr; }; diff --git a/src/beman/execution/CMakeLists.txt b/src/beman/execution/CMakeLists.txt index 10774bac..8328c338 100644 --- a/src/beman/execution/CMakeLists.txt +++ b/src/beman/execution/CMakeLists.txt @@ -141,6 +141,7 @@ target_sources( ${PROJECT_SOURCE_DIR}/include/beman/execution/detail/schedule_result_t.hpp ${PROJECT_SOURCE_DIR}/include/beman/execution/detail/scheduler.hpp ${PROJECT_SOURCE_DIR}/include/beman/execution/detail/scheduler_t.hpp + ${PROJECT_SOURCE_DIR}/include/beman/execution/detail/scope_association.hpp ${PROJECT_SOURCE_DIR}/include/beman/execution/detail/scope_token.hpp ${PROJECT_SOURCE_DIR}/include/beman/execution/detail/sender.hpp ${PROJECT_SOURCE_DIR}/include/beman/execution/detail/sender_adaptor.hpp diff --git a/src/beman/execution/execution.cppm b/src/beman/execution/execution.cppm index 5ff6ed6a..c790bfbc 100644 --- a/src/beman/execution/execution.cppm +++ b/src/beman/execution/execution.cppm @@ -227,8 +227,18 @@ export using ::beman::execution::read_env_t; export using ::beman::execution::read_env; export using ::beman::execution::simple_counting_scope; export using ::beman::execution::counting_scope; + +// [exec.scope.concepts] +export using ::beman::execution::scope_association; export using ::beman::execution::scope_token; +// [exec.associate] +export using ::beman::execution::associate; + +// [exec.spawn] +export using ::beman::execution::spawn; +export using ::beman::execution::spawn_future; + namespace detail { export using ::beman::execution::detail::basic_sender; export using ::beman::execution::detail::product_type; diff --git a/tests/beman/execution/CMakeLists.txt b/tests/beman/execution/CMakeLists.txt index 65238e5c..c0ef355a 100644 --- a/tests/beman/execution/CMakeLists.txt +++ b/tests/beman/execution/CMakeLists.txt @@ -5,12 +5,13 @@ if(BEMAN_USE_MODULES) list(APPEND execution_tests execution-module.test stop-token-module.test) endif() -set(execution_tests_todo exec-associate.test) #-dk:TODO +set(execution_tests_todo) #-dk:TODO list( APPEND execution_tests allocator-requirements-general.test exec-affine-on.test + exec-associate.test exec-awaitable.test exec-bulk.test exec-connect.test diff --git a/tests/beman/execution/exec-associate.test.cpp b/tests/beman/execution/exec-associate.test.cpp index a761027b..ddb78953 100644 --- a/tests/beman/execution/exec-associate.test.cpp +++ b/tests/beman/execution/exec-associate.test.cpp @@ -1,155 +1,232 @@ -// tests/beman/execution/exec-associate.test.cpp -*-C++-*- +// tests/beman/execution/exec-associate.test.cpp -*-C++-*- // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception #include #include +#include +#include +#include +#include #include #include #ifdef BEMAN_HAS_MODULES import beman.execution; #else #include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include #include +#include #endif -// ---------------------------------------------------------------------------- - namespace { -struct sender { - struct state { - using operation_state_concept = test_std::operation_state_t; - auto start() & noexcept {} +// A scope token that always associates successfully and doesn't wrap the sender. +struct null_token { + struct assoc { + constexpr explicit operator bool() const noexcept { return true; } + constexpr auto try_associate() const noexcept -> assoc { return {}; } }; - using sender_concept = test_std::sender_t; - auto connect(auto&&) { return state(); } + + template + constexpr auto wrap(Sender&& sndr) const noexcept -> Sender&& { + return std::forward(sndr); + } + + constexpr auto try_associate() const noexcept -> assoc { return {}; } }; -static_assert(test_std::sender); +static_assert(test_std::scope_token); -template -struct wrap_sender { - using sender_concept = test_std::sender_t; - std::remove_cvref_t sender; - wrap_sender(Sender&& s) : sender(std::forward(s)) {} +// A scope token that always fails association. +struct expired_token { + struct assoc { + constexpr explicit operator bool() const noexcept { return false; } + constexpr auto try_associate() const noexcept -> assoc { return {}; } + }; - template - auto get_completion_signatures(const Env& env) const noexcept { - return test_std::get_completion_signatures(this->sender, env); - } - template - auto connect(Receiver&& receiver) && { - return test_std::connect(std::move(this->sender), std::forward(receiver)); - } - template - auto connect(Receiver&& receiver) const& { - return test_std::connect(this->sender, std::forward(receiver)); + template + constexpr auto wrap(Sender&& sndr) const noexcept -> Sender&& { + return std::forward(sndr); } + + constexpr auto try_associate() const noexcept -> assoc { return {}; } }; -template -wrap_sender(Sender&& sender) -> wrap_sender; - -static_assert(test_std::sender>); - -template -struct token { - bool value{}; - std::size_t* count{}; - auto try_associate() noexcept(Noexcept) -> bool { - if (this->value && this->count) { - ++*this->count; +static_assert(test_std::scope_token); + +// Helper: does the sender complete with a value using sync_wait(). +template +auto completes_with_value(Sender&& snd) -> bool { + // If it completes with stopped or error, sync_wait returns empty optional. + return static_cast(test_std::sync_wait(std::forward(snd))); +} + +struct scope { + bool open{true}; + + struct assoc { + constexpr explicit operator bool() const noexcept { return !!scope_; } + + constexpr auto try_associate() const noexcept -> assoc { + return assoc{scope_ && scope_->open ? scope_ : nullptr}; } - return this->value; - } - auto disassociate() noexcept -> void { - if (this->count) { - --*this->count; + + const scope* scope_{}; + }; + + struct token { + template + constexpr auto wrap(Sender&& sndr) const noexcept -> Sender&& { + return std::forward(sndr); + } + + constexpr auto try_associate() const noexcept -> assoc { + return assoc{scope_ && scope_->open ? scope_ : nullptr}; } - } - template - auto wrap(Sender&& sender) noexcept { - return wrap_sender(std::forward(sender)); - } -}; -static_assert(test_std::scope_token>); -static_assert(test_std::scope_token>); -struct dtor_sender { - std::size_t* count{}; - using sender_concept = test_std::sender_t; + const scope* scope_{}; + }; - explicit dtor_sender(std::size_t* c) : count(c) {} - dtor_sender(dtor_sender&& other) : count(std::exchange(other.count, nullptr)) {} - ~dtor_sender() { ASSERT(count == nullptr || *count == 1u); } + constexpr auto get_token() const noexcept -> token { return token{this}; } }; -static_assert(test_std::sender); +static_assert(test_std::scope_token); +static_assert(test_std::scope_association); -auto test_associate_data() { - using data_t = test_detail::associate_data, sender>; - static_assert(std::same_as, data_t::wrap_sender>); +} // namespace +TEST(exec_associate) { + // associate returns a sender { - test_detail::associate_data data(token{true}, sender{}); - static_assert(std::same_as); - ASSERT(data.sender); - test_detail::associate_data move(std::move(data)); - ASSERT(!data.sender.has_value()); - ASSERT(move.sender); + using snd_t = decltype(test_std::associate(test_std::just(), null_token{})); + static_assert(test_std::sender); + static_assert(test_std::sender); + static_assert(test_std::sender); } + + // completion signatures: this implementation currently reports set_value() only. { - test_detail::associate_data data(token{false}, sender{}); - static_assert(std::same_as); - ASSERT(!data.sender); + using snd0_t = decltype(test_std::associate(test_std::just(), null_token{})); + static_assert(std::same_as, + test_std::completion_signatures_of_t>>); + + using snd1_t = decltype(test_std::associate(test_std::just(std::string{}), null_token{})); + static_assert(std::same_as, + test_std::completion_signatures_of_t>>); + + using snd2_t = decltype(test_std::associate(test_std::just_stopped(), null_token{})); + static_assert(std::same_as, + test_std::completion_signatures_of_t>>); + + using snd3_t = decltype(test_std::associate(test_std::just_error(5), null_token{})); + static_assert(std::same_as, + test_std::completion_signatures_of_t>>); + + int i = 42; + using snd4_t = decltype(test_std::associate(test_std::just(std::ref(i)), null_token{})); + static_assert(std::same_as)>, + test_std::completion_signatures_of_t>>); } + + // Identity behavior with null_token for value path + piping works. { - std::size_t count{}; - ASSERT(count == 0u); { - test_detail::associate_data data(token{true, &count}, dtor_sender{&count}); - ASSERT(count == 1u); + auto r = test_std::sync_wait(test_std::just() | test_std::associate(null_token{})); + ASSERT(r.has_value()); + } + { + auto r = test_std::sync_wait(test_std::just(42) | test_std::associate(null_token{})); + ASSERT(r.has_value()); + ASSERT((std::get<0>(r.value()) == 42)); } - ASSERT(count == 0u); + { + auto r = test_std::sync_wait(test_std::just(42, 67) | test_std::associate(null_token{})); + ASSERT(r.has_value()); + ASSERT((std::get<0>(r.value()) == 42)); + ASSERT((std::get<1>(r.value()) == 67)); + } + + int i = 42; + { + auto r = test_std::sync_wait(test_std::just(std::ref(i)) | test_std::associate(null_token{})); + ASSERT(r.has_value()); + ASSERT((&std::get<0>(r.value()).get() == &i)); + } + + { + auto r = test_std::sync_wait(test_std::just(42) | test_std::associate(null_token{}) | + test_std::then([](int x) noexcept { return x; })); + ASSERT(r.has_value()); + ASSERT((std::get<0>(r.value()) == 42)); + } + + // On error/stopped paths, null_token shouldn't interfere: upon_* should still run. + { + auto r = test_std::sync_wait(test_std::just_error(42) | test_std::associate(null_token{}) | + test_std::upon_error([](int i) { return i; })); + ASSERT(r.has_value()); + ASSERT((std::get<0>(r.value()) == 42)); + } + { + auto r = test_std::sync_wait(test_std::just_stopped() | test_std::associate(null_token{}) | + test_std::upon_stopped([]() noexcept { return 42; })); + ASSERT(r.has_value()); + ASSERT((std::get<0>(r.value()) == 42)); + } + } + + // associate is just_stopped with expired_token + { + auto snd = test_std::just(true) | test_std::associate(expired_token{}) | + test_std::upon_stopped([]() noexcept { return false; }); + auto r = test_std::sync_wait(std::move(snd)); + ASSERT(r.has_value()); + ASSERT((std::get<0>(r.value()) == false)); } + + // Copying an associate-sender re-queries for a new association { - std::size_t count{}; - ASSERT(count == 0u); + scope s; + auto snd = test_std::associate(test_std::just(42), s.get_token()); + // while open, copy should succeed { - test_detail::associate_data data(token{true, &count}, sender{}); - ASSERT(count == 1u); - ASSERT(data.sender); - auto p{data.release()}; - ASSERT(count == 1u); - ASSERT(!data.sender); + auto r = test_std::sync_wait(snd | test_std::upon_stopped([]() noexcept { return 67; })); + ASSERT(r.has_value()); + ASSERT((std::get<0>(r.value()) == 42)); } - ASSERT(count == 1u); - } -} -struct receiver { - using receiver_concept = test_std::receiver_t; + s.open = false; - auto set_value(auto&&...) && noexcept {} - auto set_error(auto&&) && noexcept {} - auto set_stopped() && noexcept {} -}; -static_assert(test_std::receiver); + // after closing, a fresh copy should stop + { + auto r = test_std::sync_wait(snd | test_std::upon_stopped([]() noexcept { return 67; })); + ASSERT(r.has_value()); + ASSERT((std::get<0>(r.value()) == 67)); + } -template -auto test_associate(Sender&& sender, Token&& token) -> void { - auto sndr = test_std::associate(std::forward(sender), std::forward(token)); - static_assert(test_std::sender); - test_std::sync_wait(std::move(sndr)); -} + // original should still succeed even though started after closing + { + auto r = test_std::sync_wait(std::move(snd)); + ASSERT(r.has_value()); + ASSERT((std::get<0>(r.value()) == 42)); + } + } -} // namespace + // The sender argument is eagerly destroyed when try_associate fails. + { + bool deleted = false; + using deleter_t = decltype([](bool* p) noexcept { *p = true; }); + std::unique_ptr ptr(&deleted); -TEST(exec_associate) { - static_assert(std::same_as); + auto snd = test_std::just(std::move(ptr)) | test_std::associate(expired_token{}); - test_associate_data(); + ASSERT(deleted == true); + (void)snd; + } - test_associate(sender{}, token{}); + // A quick runtime sanity check for the stopped vs value branch. + { + ASSERT(completes_with_value(test_std::just(1) | test_std::associate(null_token{}))); + ASSERT(!completes_with_value(test_std::just(1) | test_std::associate(expired_token{}))); + } } diff --git a/tests/beman/execution/exec-scope-concepts.test.cpp b/tests/beman/execution/exec-scope-concepts.test.cpp index 0947b738..996d471e 100644 --- a/tests/beman/execution/exec-scope-concepts.test.cpp +++ b/tests/beman/execution/exec-scope-concepts.test.cpp @@ -48,23 +48,40 @@ struct bad { using sender_concept = test_std::sender_t; }; -template class Wrap> +template +struct association { + association() = default; + + association(const association&) = delete; + + association(association&&) noexcept(Noexcept) = default; + + auto operator=(const association&) -> association& = delete; + + auto operator=(association&&) noexcept(Noexcept) -> association& = default; + + static auto try_associate() noexcept -> association { return {}; } + + explicit operator bool() const noexcept { return false; } +}; + +template class Wrap> struct token { Mem mem{}; - auto try_associate() -> Bool { return {}; } - auto disassociate() noexcept(Noexcept) -> void {} + auto try_associate() const -> Assoc { return {}; } + template - auto wrap(Sender&& sndr) -> Wrap { + auto wrap(Sender&& sndr) const -> Wrap { return Wrap(std::forward(sndr)); } }; } // namespace TEST(exec_scope_concepts) { - static_assert(test_std::scope_token>); - static_assert(not test_std::scope_token>); - static_assert(not test_std::scope_token>); - static_assert(not test_std::scope_token>); - static_assert(not test_std::scope_token>); + + static_assert(test_std::scope_token, wrap>>); + static_assert(not test_std::scope_token, wrap>>); + static_assert(not test_std::scope_token, wrap>>); + static_assert(not test_std::scope_token, bad>>); static_assert(not test_std::scope_token); } diff --git a/tests/beman/execution/exec-scope-counting.test.cpp b/tests/beman/execution/exec-scope-counting.test.cpp index 19ed7ba0..e2eb61fd 100644 --- a/tests/beman/execution/exec-scope-counting.test.cpp +++ b/tests/beman/execution/exec-scope-counting.test.cpp @@ -25,14 +25,9 @@ auto general() -> void { // static_assert(requires(token const& tok){ { tok.wrap(test_std::just(10)) } noexcept; }); static_assert(requires(const token& tok) { - { tok.try_associate() } noexcept -> std::same_as; - }); - static_assert(requires(const token& tok) { tok.try_associate(); }); - static_assert(requires(const token& tok) { - { tok.disassociate() } noexcept; + { tok.try_associate() } noexcept -> test_std::scope_association; }); - static_assert(noexcept(scope{})); static_assert(!std::is_move_constructible_v); static_assert(!std::is_copy_constructible_v); static_assert(requires(scope sc) { @@ -62,30 +57,6 @@ auto ctor() -> void { { test_std::counting_scope scope; } - test::death([] { - test_std::counting_scope scope; - scope.get_token().try_associate(); - }); - test::death([] { - test_std::counting_scope scope; - scope.get_token().try_associate(); - bool called{false}; - auto state(test_std::connect(scope.join(), join_receiver{called})); - test_std::start(state); - }); - test::death([] { - test_std::counting_scope scope; - scope.get_token().try_associate(); - scope.close(); - }); - test::death([] { - test_std::counting_scope scope; - scope.get_token().try_associate(); - bool called{false}; - auto state(test_std::connect(scope.join(), join_receiver{called})); - test_std::start(state); - scope.close(); - }); { test_std::counting_scope scope; scope.close(); @@ -102,24 +73,24 @@ auto mem() -> void { test_std::counting_scope scope; test_std::counting_scope::token token{scope.get_token()}; - ASSERT(true == token.try_associate()); - token.disassociate(); + ASSERT(true == static_cast(token.try_associate())); scope.close(); - ASSERT(false == token.try_associate()); + ASSERT(false == static_cast(token.try_associate())); test_std::sync_wait(scope.join()); } { + bool called{false}; test_std::counting_scope scope; - ASSERT(true == scope.get_token().try_associate()); - bool called{false}; + const auto tok{scope.get_token()}; + std::optional assoc{tok.try_associate()}; + ASSERT(true == static_cast(*assoc)); ASSERT(called == false); auto state(test_std::connect(scope.join(), join_receiver{called})); ASSERT(called == false); test_std::start(state); ASSERT(called == false); - - scope.get_token().disassociate(); + assoc.reset(); ASSERT(called == true); } } @@ -128,16 +99,17 @@ auto token() -> void { const auto tok{scope.get_token()}; [[maybe_unused]] auto sndr{tok.wrap(test_std::just(10))}; - ASSERT(true == tok.try_associate()); + std::optional assoc{tok.try_associate()}; + ASSERT(true == static_cast(*assoc)); bool called{false}; auto state(test_std::connect(scope.join(), join_receiver{called})); test_std::start(state); ASSERT(false == called); scope.close(); ASSERT(false == called); - ASSERT(false == tok.try_associate()); + ASSERT(false == static_cast(tok.try_associate())); ASSERT(false == called); - tok.disassociate(); + assoc.reset(); ASSERT(true == called); } diff --git a/tests/beman/execution/exec-scope-simple-counting.test.cpp b/tests/beman/execution/exec-scope-simple-counting.test.cpp index bc89a175..b612d088 100644 --- a/tests/beman/execution/exec-scope-simple-counting.test.cpp +++ b/tests/beman/execution/exec-scope-simple-counting.test.cpp @@ -24,15 +24,7 @@ auto general() -> void { using token = scope::token; // static_assert(requires(token const& tok){ { tok.wrap(test_std::just(10)) } noexcept; }); - static_assert(requires(const token& tok) { - { tok.try_associate() } noexcept -> std::same_as; - }); - static_assert(requires(const token& tok) { tok.try_associate(); }); - static_assert(requires(const token& tok) { - { tok.disassociate() } noexcept; - }); - static_assert(noexcept(scope{})); static_assert(!std::is_move_constructible_v); static_assert(!std::is_copy_constructible_v); static_assert(requires(scope sc) { @@ -62,30 +54,6 @@ auto ctor() -> void { { test_std::simple_counting_scope scope; } - test::death([] { - test_std::simple_counting_scope scope; - scope.get_token().try_associate(); - }); - test::death([] { - test_std::simple_counting_scope scope; - scope.get_token().try_associate(); - bool called{false}; - auto state(test_std::connect(scope.join(), join_receiver{called})); - test_std::start(state); - }); - test::death([] { - test_std::simple_counting_scope scope; - scope.get_token().try_associate(); - scope.close(); - }); - test::death([] { - test_std::simple_counting_scope scope; - scope.get_token().try_associate(); - bool called{false}; - auto state(test_std::connect(scope.join(), join_receiver{called})); - test_std::start(state); - scope.close(); - }); { test_std::simple_counting_scope scope; scope.close(); @@ -102,24 +70,24 @@ auto mem() -> void { test_std::simple_counting_scope scope; test_std::simple_counting_scope::token token{scope.get_token()}; - ASSERT(true == token.try_associate()); - token.disassociate(); + ASSERT(true == static_cast(token.try_associate())); scope.close(); - ASSERT(false == token.try_associate()); + ASSERT(false == static_cast(token.try_associate())); test_std::sync_wait(scope.join()); } { + bool called{false}; test_std::simple_counting_scope scope; - ASSERT(true == scope.get_token().try_associate()); - bool called{false}; + const auto tok{scope.get_token()}; + std::optional assoc{tok.try_associate()}; + ASSERT(true == static_cast(*assoc)); ASSERT(called == false); auto state(test_std::connect(scope.join(), join_receiver{called})); ASSERT(called == false); test_std::start(state); ASSERT(called == false); - - scope.get_token().disassociate(); + assoc.reset(); ASSERT(called == true); } } @@ -129,16 +97,17 @@ auto token() -> void { auto sndr{tok.wrap(test_std::just(10))}; static_assert(std::same_as); - ASSERT(true == tok.try_associate()); + std::optional assoc{tok.try_associate()}; + ASSERT(true == static_cast(*assoc)); bool called{false}; auto state(test_std::connect(scope.join(), join_receiver{called})); test_std::start(state); ASSERT(false == called); scope.close(); ASSERT(false == called); - ASSERT(false == tok.try_associate()); + ASSERT(false == static_cast(tok.try_associate())); ASSERT(false == called); - tok.disassociate(); + assoc.reset(); ASSERT(true == called); } diff --git a/tests/beman/execution/exec-spawn-future.test.cpp b/tests/beman/execution/exec-spawn-future.test.cpp index 31e1805f..371e90e1 100644 --- a/tests/beman/execution/exec-spawn-future.test.cpp +++ b/tests/beman/execution/exec-spawn-future.test.cpp @@ -64,13 +64,47 @@ static_assert(test_std::sender>); static_assert(test_std::sender>); static_assert(test_std::sender>); +template +struct association { + association() = default; + + association(std::size_t* count, bool associated) noexcept : count(count), associated(associated) {} + + association(const association&) = delete; + + association(association&& other) noexcept(Noexcept) + : count(std::exchange(other.count, {})), associated(std::exchange(other.associated, {})) {} + + ~association() { + if (associated) { + --*count; + } + } + + auto operator=(association other) noexcept(Noexcept) -> association& { + std::swap(this->count, other.count); + std::swap(this->associated, other.associated); + return *this; + } + + auto try_associate() const noexcept -> association { return {this->count, this->count && bool(++*this->count)}; } + + explicit operator bool() const noexcept { return associated; } + + std::size_t* count = nullptr; + bool associated = false; +}; + template struct token { std::size_t* count{nullptr}; - auto try_associate() -> bool { return this->count && bool(++*this->count); } - auto disassociate() noexcept(Noexcept) -> void { --*this->count; } + + auto try_associate() const noexcept -> association { + return {this->count, this->count && bool(++*this->count)}; + } + template - auto wrap(Sender&& sender) -> Sender { + auto wrap(Sender&& sender) const -> Sender { return std::forward(sender); } }; diff --git a/tests/beman/execution/exec-spawn.test.cpp b/tests/beman/execution/exec-spawn.test.cpp index 14f2cce2..e5d0cf7e 100644 --- a/tests/beman/execution/exec-spawn.test.cpp +++ b/tests/beman/execution/exec-spawn.test.cpp @@ -48,15 +48,48 @@ struct sender { static_assert(test_std::sender>); static_assert(!test_std::sender>); +template +struct association { + association() = default; + + explicit association(bool* disassociated) noexcept : disassociated(disassociated) {} + + association(const association&) = delete; + + association(association&& other) noexcept(Noexcept) : disassociated(std::exchange(other.disassociated, {})) {} + + ~association() { + if (disassociated) + *disassociated = true; + } + + auto operator=(association other) noexcept(Noexcept) -> association& { + std::swap(this->disassociated, other.disassociated); + return *this; + } + + auto try_associate() const noexcept -> association { return {disassociated}; } + + explicit operator bool() const noexcept { return disassociated != nullptr; } + + bool* disassociated = nullptr; +}; + template struct token { bool* associated{}; bool* disassociated{}; bool can_associate{true}; - auto try_associate() noexcept(B) -> bool { return this->can_associate && (*this->associated = true); } - auto disassociate() noexcept(B) { *this->disassociated = true; } + auto try_associate() const noexcept { + if (!can_associate) { + return association{}; + } + *this->associated = true; + return association{this->disassociated}; + } + template - auto wrap(Sndr&& sndr) { + auto wrap(Sndr&& sndr) const { return std::forward(sndr); } };