Index: include/CMakeLists.txt =================================================================== --- include/CMakeLists.txt +++ include/CMakeLists.txt @@ -86,6 +86,7 @@ experimental/string experimental/string_view experimental/system_error + experimental/task experimental/tuple experimental/type_traits experimental/unordered_map Index: include/experimental/__memory =================================================================== --- include/experimental/__memory +++ include/experimental/__memory @@ -73,6 +73,13 @@ > {}; +// Round __s up to next multiple of __a. +inline _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR +size_t __aligned_allocation_size(size_t __s, size_t __a) _NOEXCEPT +{ + return (__s + __a - 1) & ~(__a - 1); +} + template inline _LIBCPP_INLINE_VISIBILITY void __lfts_user_alloc_construct( Index: include/experimental/coroutine =================================================================== --- include/experimental/coroutine +++ include/experimental/coroutine @@ -60,9 +60,9 @@ #ifdef _LIBCPP_HAS_NO_COROUTINES # if defined(_LIBCPP_WARNING) - _LIBCPP_WARNING(" cannot be used with this compiler") + _LIBCPP_WARNING(" requires a compiler with support for coroutines") # else -# warning cannot be used with this compiler +# warning requires a compiler with support for coroutines # endif #endif Index: include/experimental/memory_resource =================================================================== --- include/experimental/memory_resource +++ include/experimental/memory_resource @@ -86,14 +86,6 @@ _LIBCPP_BEGIN_NAMESPACE_LFTS_PMR -// Round __s up to next multiple of __a. -inline _LIBCPP_INLINE_VISIBILITY -size_t __aligned_allocation_size(size_t __s, size_t __a) _NOEXCEPT -{ - _LIBCPP_ASSERT(__s + __a > __s, "aligned allocation size overflows"); - return (__s + __a - 1) & ~(__a - 1); -} - // 8.5, memory.resource class _LIBCPP_TYPE_VIS memory_resource { Index: include/experimental/task =================================================================== --- /dev/null +++ include/experimental/task @@ -0,0 +1,536 @@ +// -*- C++ -*- +//===------------------------------- task ---------------------------------===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#ifndef _LIBCPP_EXPERIMENTAL_TASK +#define _LIBCPP_EXPERIMENTAL_TASK + +#include +#include +#include + +#include +#include +#include +#include + +#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER) +#pragma GCC system_header +#endif + +#ifdef _LIBCPP_HAS_NO_COROUTINES +#if defined(_LIBCPP_WARNING) +_LIBCPP_WARNING(" requires a compiler with support for coroutines") +#else +#warning requires a compiler with support for coroutines +#endif +#endif + +_LIBCPP_BEGIN_NAMESPACE_EXPERIMENTAL_COROUTINES + +////// task + +template +class task; + +struct __task_promise_final_awaitable { + _LIBCPP_INLINE_VISIBILITY + _LIBCPP_CONSTEXPR bool await_ready() const _NOEXCEPT { return false; } + + template + _LIBCPP_INLINE_VISIBILITY coroutine_handle<> + await_suspend(coroutine_handle<_TaskPromise> __coro) const _NOEXCEPT { + _LIBCPP_ASSERT( + __coro.promise().__continuation_, + "Coroutine completed without a valid continuation attached."); + return __coro.promise().__continuation_; + } + + _LIBCPP_INLINE_VISIBILITY + void await_resume() const _NOEXCEPT {} +}; + +class _LIBCPP_TYPE_VIS __task_promise_base { + using _DeallocFunc = void(void* __ptr, size_t __size) _NOEXCEPT; + + template + static constexpr bool __allocator_needs_to_be_stored = + !allocator_traits<_Alloc>::is_always_equal::value || + !is_default_constructible_v<_Alloc>; + + static _LIBCPP_CONSTEXPR size_t + __get_dealloc_func_offset(size_t __frameSize) _NOEXCEPT { + return _VSTD_LFTS::__aligned_allocation_size(__frameSize, + alignof(_DeallocFunc*)); + } + + static _LIBCPP_CONSTEXPR size_t + __get_padded_frame_size(size_t __frameSize) _NOEXCEPT { + return __get_dealloc_func_offset(__frameSize) + sizeof(_DeallocFunc*); + } + + template + static _LIBCPP_CONSTEXPR size_t + __get_allocator_offset(size_t __frameSize) _NOEXCEPT { + return _VSTD_LFTS::__aligned_allocation_size( + __get_padded_frame_size(__frameSize), alignof(_Alloc)); + } + + template + static _LIBCPP_CONSTEXPR size_t + __get_padded_frame_size_with_allocator(size_t __frameSize) _NOEXCEPT { + if constexpr (__allocator_needs_to_be_stored<_Alloc>) { + return __get_allocator_offset<_Alloc>(__frameSize) + sizeof(_Alloc); + } else { + return __get_padded_frame_size(__frameSize); + } + } + + _LIBCPP_INLINE_VISIBILITY + static _DeallocFunc*& __get_dealloc_func(void* __frameStart, + size_t __frameSize) _NOEXCEPT { + return *reinterpret_cast<_DeallocFunc**>( + static_cast(__frameStart) + + __get_dealloc_func_offset(__frameSize)); + } + + template + _LIBCPP_INLINE_VISIBILITY static _Alloc& + __get_allocator(void* __frameStart, size_t __frameSize) _NOEXCEPT { + return *reinterpret_cast<_Alloc*>( + static_cast(__frameStart) + + __get_allocator_offset<_Alloc>(__frameSize)); + } + +public: + __task_promise_base() _NOEXCEPT = default; + + // Explicitly disable special member functions. + __task_promise_base(const __task_promise_base&) = delete; + __task_promise_base(__task_promise_base&&) = delete; + __task_promise_base& operator=(const __task_promise_base&) = delete; + __task_promise_base& operator=(__task_promise_base&&) = delete; + + static void* operator new(size_t __size) { + // Allocate space for an extra pointer immediately after __size that holds + // the type-erased deallocation function. + void* __pointer = ::operator new(__get_padded_frame_size(__size)); + + _DeallocFunc*& __deallocFunc = __get_dealloc_func(__pointer, __size); + __deallocFunc = [](void* __pointer, size_t __size) _NOEXCEPT { + ::operator delete(__pointer, __get_padded_frame_size(__size)); + }; + + return __pointer; + } + + template + static void* operator new(size_t __size, allocator_arg_t, _Alloc& __alloc, + _Args&...) { + using _CharAlloc = + typename allocator_traits<_Alloc>::template rebind_alloc; + + _CharAlloc __charAllocator{__alloc}; + + void* __pointer = __charAllocator.allocate( + __get_padded_frame_size_with_allocator<_CharAlloc>(__size)); + + _DeallocFunc*& __deallocFunc = __get_dealloc_func(__pointer, __size); + __deallocFunc = [](void* __pointer, size_t __size) _NOEXCEPT { + // Allocators are required to not throw from their move constructors + // however they aren't required to be declared noexcept so we can't + // actually check this with a static_assert. + // + // static_assert(is_nothrow_move_constructible_v<_Alloc>, + // "task coroutine custom allocator requires a noexcept " + // "move constructor"); + + size_t __paddedSize = + __get_padded_frame_size_with_allocator<_CharAlloc>(__size); + + if constexpr (__allocator_needs_to_be_stored<_CharAlloc>) { + _CharAlloc& __allocatorInFrame = + __get_allocator<_CharAlloc>(__pointer, __size); + _CharAlloc __allocatorOnStack = _VSTD::move(__allocatorInFrame); + __allocatorInFrame.~_CharAlloc(); + // Allocator requirements state that deallocate() must not throw. + // See [allocator.requirements] from C++ standard. + // We are relying on that here. + __allocatorOnStack.deallocate(static_cast(__pointer), + __paddedSize); + } else { + _CharAlloc __alloc; + __alloc.deallocate(static_cast(__pointer), __paddedSize); + } + }; + + // Copy the allocator into the heap frame (if required) + if constexpr (__allocator_needs_to_be_stored<_CharAlloc>) { + // task coroutine custom allocation requires the copy constructor to + // not throw but we can't rely on it being declared noexcept. + // If it did throw we'd leak the allocation here. + ::new (static_cast( + _VSTD::addressof(__get_allocator<_CharAlloc>(__pointer, __size)))) + _CharAlloc(_VSTD::move(__charAllocator)); + } + + return __pointer; + } + + template + static void* operator new(size_t __size, _This&, allocator_arg_t __allocArg, _Alloc& __alloc, + _Args&...) { + return __task_promise_base::operator new(__size, __allocArg, __alloc); + } + + _LIBCPP_INLINE_VISIBILITY + static void operator delete(void* __pointer, size_t __size)_NOEXCEPT { + __get_dealloc_func(__pointer, __size)(__pointer, __size); + } + + _LIBCPP_INLINE_VISIBILITY + suspend_always initial_suspend() const _NOEXCEPT { return {}; } + + _LIBCPP_INLINE_VISIBILITY + __task_promise_final_awaitable final_suspend() _NOEXCEPT { return {}; } + + _LIBCPP_INLINE_VISIBILITY + void __set_continuation(coroutine_handle<> __continuation) { + _LIBCPP_ASSERT(!__continuation_, "task already has a continuation"); + __continuation_ = __continuation; + } + +private: + friend __task_promise_final_awaitable; + + coroutine_handle<> __continuation_; +}; + +template +class _LIBCPP_TEMPLATE_VIS __task_promise final : public __task_promise_base { + using _Handle = coroutine_handle<__task_promise>; + + static_assert( + std::is_destructible_v<_Tp>, + "task requires that T is destructible"); + +public: + __task_promise() _NOEXCEPT : __state_(_State::__no_value) {} + + ~__task_promise() { + switch (__state_) { + case _State::__value: + __value_.~_Tp(); + break; + case _State::__exception: +#ifndef _LIBCPP_NO_EXCEPTIONS + __exception_.~exception_ptr(); +#endif + break; + case _State::__no_value: + break; + } + } + + _LIBCPP_INLINE_VISIBILITY + task<_Tp> get_return_object() _NOEXCEPT; + + void unhandled_exception() _NOEXCEPT { +#ifndef _LIBCPP_NO_EXCEPTIONS + if (__state_ == _State::__value) { + __state_ = _State::__no_value; + __value_.~_Tp(); + } + _LIBCPP_ASSERT( + __state_ == _State::__no_value, + "Storage for the result should not already hold a value."); + ::new (static_cast(&__exception_)) + exception_ptr(current_exception()); + __state_ = _State::__exception; +#else + _LIBCPP_ASSERT( + false, "task coroutine unexpectedly called unhandled_exception()"); +#endif + } + + // Only enable return_value() overload if _Tp is implicitly constructible from + // _Value + template , int> = 0> + void return_value(_Value&& __value) + _NOEXCEPT_((is_nothrow_constructible_v<_Tp, _Value>)) { + __construct_value(static_cast<_Value&&>(__value)); + } + + template >, int> = 0> + void return_value(initializer_list<_Value> __initializer) _NOEXCEPT_( + (is_nothrow_constructible_v<_Tp, initializer_list<_Value>>)) { + __construct_value(_VSTD::move(__initializer)); + } + + auto return_value(_Tp&& __value) + _NOEXCEPT_((is_nothrow_move_constructible_v<_Tp>)) + -> enable_if_t> { + __construct_value(static_cast<_Tp&&>(__value)); + } + + _Tp& __lvalue_result() { + __throw_if_exception(); + return __value_; + } + + _Tp __rvalue_result() { + __throw_if_exception(); + return static_cast<_Tp&&>(__value_); + } + +private: + template + void __construct_value(_Args&&... __args) { + // It's possible that this could be called multiple times in the case that + // an exception is thrown from a destructor of an in-scope variable after + // executing co_return, that exception is then caught and a new co_return + // statement is executed. So make sure we destruct any prior constructed + // return value before constructing a new value in its place. + if (__state_ == _State::__value) { + __state_ = _State::__no_value; + __value_.~_Tp(); + } + _LIBCPP_ASSERT( + __state_ == _State::__no_value, + "Storage for the return-value should not already hold a value."); + ::new (static_cast(_VSTD::addressof(__value_))) + _Tp(static_cast<_Args&&>(__args)...); + + // Only set __state_ after successfully constructing the value. + // If constructor throws then exception will propagate to the + // coroutine and if uncaught may eventually end up calling + // unhandled_exception(). + __state_ = _State::__value; + } + + void __throw_if_exception() { +#ifndef _LIBCPP_NO_EXCEPTIONS + if (__state_ == _State::__exception) { + rethrow_exception(__exception_); + } +#endif + } + + enum class _State { __no_value, __value, __exception }; + + _State __state_; + union { + char __empty_; + _Tp __value_; + exception_ptr __exception_; + }; +}; + +template +class __task_promise<_Tp&> final : public __task_promise_base { + using _Ptr = _Tp*; + using _Handle = coroutine_handle<__task_promise>; + +public: + __task_promise() _NOEXCEPT = default; + + ~__task_promise() { +#ifndef _LIBCPP_NO_EXCEPTIONS + if (__has_exception_) { + __exception_.~exception_ptr(); + } +#endif + } + + _LIBCPP_INLINE_VISIBILITY + task<_Tp&> get_return_object() _NOEXCEPT; + + void unhandled_exception() _NOEXCEPT { +#ifndef _LIBCPP_NO_EXCEPTIONS + ::new (static_cast(&__exception_)) + exception_ptr(current_exception()); + __has_exception_ = true; +#else + _LIBCPP_ASSERT( + false, "task coroutine unexpectedly called unhandled_exception()"); +#endif + } + + void return_value(_Tp& __value) _NOEXCEPT { + ::new (static_cast(&__pointer_)) _Ptr(_VSTD::addressof(__value)); + } + + _Tp& __lvalue_result() { + __throw_if_exception(); + return *__pointer_; + } + + _Tp& __rvalue_result() { return __lvalue_result(); } + +private: + void __throw_if_exception() { +#ifndef _LIBCPP_NO_EXCEPTIONS + if (__has_exception_) { + rethrow_exception(__exception_); + } +#endif + } + + union { + char __empty_; + _Ptr __pointer_; + exception_ptr __exception_; + }; + bool __has_exception_ = false; +}; + +template <> +class __task_promise final : public __task_promise_base { + using _Handle = coroutine_handle<__task_promise>; + +public: + task get_return_object() _NOEXCEPT; + + void return_void() _NOEXCEPT {} + + void unhandled_exception() _NOEXCEPT { +#ifndef _LIBCPP_NO_EXCEPTIONS + __exception_ = current_exception(); +#endif + } + + void __lvalue_result() { __throw_if_exception(); } + + void __rvalue_result() { __throw_if_exception(); } + +private: + void __throw_if_exception() { +#ifndef _LIBCPP_NO_EXCEPTIONS + if (__exception_) { + rethrow_exception(__exception_); + } +#endif + } + + exception_ptr __exception_; +}; + +template +class _LIBCPP_TEMPLATE_VIS _LIBCPP_NODISCARD_AFTER_CXX17 task { +public: + using promise_type = __task_promise<_Tp>; + + static_assert( + std::is_same_v<_Tp, void> || + std::is_lvalue_reference_v<_Tp> || + (std::is_destructible_v<_Tp> && + std::is_object_v<_Tp> && + !std::is_array_v<_Tp>), + "task supports only 'void', lvalue-reference or " + "a destructible, non-array object result types."); + +private: + using _Handle = coroutine_handle<__task_promise<_Tp>>; + + class _AwaiterBase { + public: + _AwaiterBase(_Handle __coro) _NOEXCEPT : __coro_(__coro) {} + + _LIBCPP_INLINE_VISIBILITY + bool await_ready() const { return __coro_.done(); } + + _LIBCPP_INLINE_VISIBILITY + _Handle await_suspend(coroutine_handle<> __continuation) const { + __coro_.promise().__set_continuation(__continuation); + return __coro_; + } + + protected: + _Handle __coro_; + }; + +public: + _LIBCPP_INLINE_VISIBILITY + task(task&& __other) _NOEXCEPT + : __coro_(_VSTD::exchange(__other.__coro_, {})) {} + + task(const task&) = delete; + task& operator=(const task&) = delete; + + _LIBCPP_INLINE_VISIBILITY + ~task() { + if (__coro_) + __coro_.destroy(); + } + + _LIBCPP_INLINE_VISIBILITY + void swap(task& __other) _NOEXCEPT { _VSTD::swap(__coro_, __other.__coro_); } + + _LIBCPP_INLINE_VISIBILITY + auto operator co_await() & { + class _Awaiter : public _AwaiterBase { + public: + using _AwaiterBase::_AwaiterBase; + + _LIBCPP_INLINE_VISIBILITY + decltype(auto) await_resume() { + return this->__coro_.promise().__lvalue_result(); + } + }; + + _LIBCPP_ASSERT(__coro_, + "Undefined behaviour to co_await an invalid task"); + return _Awaiter{__coro_}; + } + + _LIBCPP_INLINE_VISIBILITY + auto operator co_await() && { + class _Awaiter : public _AwaiterBase { + public: + using _AwaiterBase::_AwaiterBase; + + _LIBCPP_INLINE_VISIBILITY + decltype(auto) await_resume() { + return this->__coro_.promise().__rvalue_result(); + } + }; + + _LIBCPP_ASSERT(__coro_, + "co_await on an invalid task has undefined behaviour"); + return _Awaiter{__coro_}; + } + +private: + friend __task_promise<_Tp>; + + _LIBCPP_INLINE_VISIBILITY + task(_Handle __coro) _NOEXCEPT : __coro_(__coro) {} + + _Handle __coro_; +}; + +template +task<_Tp> __task_promise<_Tp>::get_return_object() _NOEXCEPT { + return task<_Tp>{_Handle::from_promise(*this)}; +} + +template +task<_Tp&> __task_promise<_Tp&>::get_return_object() _NOEXCEPT { + return task<_Tp&>{_Handle::from_promise(*this)}; +} + +task __task_promise::get_return_object() _NOEXCEPT { + return task{_Handle::from_promise(*this)}; +} + +_LIBCPP_END_NAMESPACE_EXPERIMENTAL_COROUTINES + +#endif Index: include/module.modulemap =================================================================== --- include/module.modulemap +++ include/module.modulemap @@ -579,6 +579,11 @@ header "experimental/string" export * } + module task { + requires coroutines + header "experimental/task" + export * + } module type_traits { header "experimental/type_traits" export * Index: test/std/experimental/task/awaitable_traits.hpp =================================================================== --- /dev/null +++ test/std/experimental/task/awaitable_traits.hpp @@ -0,0 +1,121 @@ +// -*- C++ -*- +//===----------------------------------------------------------------------===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#ifndef TEST_EXPERIMENTAL_TASK_AWAITABLE_TRAITS_HPP +#define TEST_EXPERIMENTAL_TASK_AWAITABLE_TRAITS_HPP + +#include +#include + +namespace test_detail { + +template +struct is_coroutine_handle : std::false_type {}; + +template +struct is_coroutine_handle> : + std::true_type +{}; + +template +struct is_valid_await_suspend_result : + std::disjunction< + std::is_void, + std::is_same, + is_coroutine_handle> +{}; + +} // namespace test_detail + +template +struct is_awaiter : std::false_type {}; + +template +struct is_awaiter().await_ready()), + decltype(std::declval().await_resume()), + decltype(std::declval().await_suspend( + std::declval>()))>> : + std::conjunction< + std::is_same().await_ready()), bool>, + test_detail::is_valid_await_suspend_result().await_suspend( + std::declval>()))>> +{}; + +template +constexpr bool is_awaiter_v = is_awaiter::value; + +namespace test_detail { + +template +struct has_member_operator_co_await : std::false_type {}; + +template +struct has_member_operator_co_await().operator co_await())>> +: is_awaiter().operator co_await())> +{}; + +template +struct has_non_member_operator_co_await : std::false_type {}; + +template +struct has_non_member_operator_co_await()))>> +: is_awaiter()))> +{}; + +} // namespace test_detail + +template +struct is_awaitable : std::disjunction< + is_awaiter, + test_detail::has_member_operator_co_await, + test_detail::has_non_member_operator_co_await> +{}; + +template +constexpr bool is_awaitable_v = is_awaitable::value; + +template< + typename Tp, + std::enable_if_t, int> = 0> +decltype(auto) get_awaiter(Tp&& awaitable) +{ + if constexpr (test_detail::has_member_operator_co_await::value) + { + return static_cast(awaitable).operator co_await(); + } + else if constexpr (test_detail::has_non_member_operator_co_await::value) + { + return operator co_await(static_cast(awaitable)); + } + else + { + return static_cast(awaitable); + } +} + +template +struct await_result +{}; + +template +struct await_result>> +{ +private: + using awaiter = decltype(get_awaiter(std::declval())); +public: + using type = decltype(std::declval().await_resume()); +}; + +template +using await_result_t = typename await_result::type; + +#endif Index: test/std/experimental/task/counted.hpp =================================================================== --- /dev/null +++ test/std/experimental/task/counted.hpp @@ -0,0 +1,89 @@ +// -*- C++ -*- +//===----------------------------------------------------------------------===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#ifndef TEST_EXPERIMENTAL_TASK_COUNTED_HPP +#define TEST_EXPERIMENTAL_TASK_COUNTED_HPP + +class counted +{ +public: + + counted() : id_(nextId_++) + { + ++defaultConstructedCount_; + } + + counted(const counted& other) : id_(other.id_) + { + ++copyConstructedCount_; + } + + counted(counted&& other) : id_(std::exchange(other.id_, 0)) + { + ++moveConstructedCount_; + } + + ~counted() + { + ++destructedCount_; + } + + static void reset() + { + nextId_ = 1; + defaultConstructedCount_ = 0; + copyConstructedCount_ = 0; + moveConstructedCount_ = 0; + destructedCount_ = 0; + } + + static std::size_t active_instance_count() + { + return + defaultConstructedCount_ + + copyConstructedCount_ + + moveConstructedCount_ - + destructedCount_; + } + + static std::size_t copy_constructor_count() + { + return copyConstructedCount_; + } + + static std::size_t move_constructor_count() + { + return moveConstructedCount_; + } + + static std::size_t default_constructor_count() + { + return defaultConstructedCount_; + } + + static std::size_t destructor_count() + { + return destructedCount_; + } + + std::size_t id() const { return id_; } + +private: + std::size_t id_; + + inline static std::size_t nextId_; + inline static std::size_t defaultConstructedCount_; + inline static std::size_t copyConstructedCount_; + inline static std::size_t moveConstructedCount_; + inline static std::size_t destructedCount_; + +}; + +#endif Index: test/std/experimental/task/lit.local.cfg =================================================================== --- /dev/null +++ test/std/experimental/task/lit.local.cfg @@ -0,0 +1,9 @@ +# If the compiler doesn't support coroutines mark all of the tests under +# this directory as unsupported. Otherwise add the required `-fcoroutines-ts` +# flag. +if 'fcoroutines-ts' not in config.available_features: + config.unsupported = True +else: + import copy + config.test_format.cxx = copy.deepcopy(config.test_format.cxx) + config.test_format.cxx.compile_flags += ['-fcoroutines-ts'] Index: test/std/experimental/task/manual_reset_event.hpp =================================================================== --- /dev/null +++ test/std/experimental/task/manual_reset_event.hpp @@ -0,0 +1,127 @@ +// -*- C++ -*- +//===----------------------------------------------------------------------===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#ifndef TEST_EXPERIMENTAL_TASK_MANUAL_RESET_EVENT_HPP +#define TEST_EXPERIMENTAL_TASK_MANUAL_RESET_EVENT_HPP + +#include +#include +#include + +// manual_reset_event is a coroutine synchronisation tool that allows one +// coroutine to await the event object. If the event is in the 'set' state +// then it continues without suspending, otherwise the coroutine suspends +// until some thread calls .set() on the event. +class manual_reset_event +{ + class awaiter + { + public: + + awaiter(const manual_reset_event* event) noexcept + : event_(event) + {} + + bool await_ready() const noexcept + { + return event_->is_set(); + } + + bool await_suspend(std::experimental::coroutine_handle<> coro) noexcept + { + assert( + (event_->state_.load(std::memory_order_relaxed) != + state_t::not_set_waiting_coroutine) && + "This manual_reset_event already has another coroutine awaiting it. " + "Only one awaiting coroutine is supported."); + + event_->awaitingCoroutine_ = coro; + + // If the compare-exchange fails then the event was already 'set' + // so we should not suspend. This code path requires 'acquire' + // semantics so we have visibility of writes prior to the + // .set() operation that transitioned the event to the 'set' state. + // If the compare-exchange succeeds then this needs 'release' semantics + // so that a subsequent call to .set() has visibility of our writes + // to the coroutine frame and to event_->awaitingCoroutine_ after + // reading our write to event_->state_. + state_t oldState = state_t::not_set; + return event_->state_.compare_exchange_strong( + oldState, + state_t::not_set_waiting_coroutine, + std::memory_order_release, + std::memory_order_acquire); + } + + void await_resume() const noexcept {} + + private: + const manual_reset_event* event_; + }; + +public: + + manual_reset_event(bool initiallySet = false) noexcept + : state_(initiallySet ? state_t::set : state_t::not_set) + {} + + bool is_set() const noexcept + { + return state_.load(std::memory_order_acquire) == state_t::set; + } + + void set() noexcept + { + // Needs to be 'acquire' in case the old value was a waiting coroutine + // so that we have visibility of the writes to the coroutine frame in + // the current thread before we resume it. + // Also needs to be 'release' in case the old value was 'not-set' so that + // another thread that subsequently awaits the event and reads the 'set' + // value with 'acquire' semantics has visibility of the prior writes that + // this thread performed. + state_t oldState = state_.exchange(state_t::set, std::memory_order_acq_rel); + if (oldState == state_t::not_set_waiting_coroutine) + { + std::exchange(awaitingCoroutine_, {}).resume(); + } + } + + void reset() noexcept + { + assert( + (state_.load(std::memory_order_relaxed) != state_t::not_set_waiting_coroutine) && + "Illegal to call reset() if a coroutine is currently awaiting the event."); + + // Note, we use 'relaxed' memory order here since it is considered an + // API-race to call reset() concurrently either with operator co_await() + // or with set(). + state_.store(state_t::not_set, std::memory_order_relaxed); + } + + awaiter operator co_await() const noexcept + { + return awaiter{ this }; + } + +private: + + enum class state_t { + not_set, + not_set_waiting_coroutine, + set + }; + + // TODO: Can we combine these two members into a single std::atomic? + mutable std::atomic state_; + mutable std::experimental::coroutine_handle<> awaitingCoroutine_; + +}; + +#endif Index: test/std/experimental/task/sync_wait.hpp =================================================================== --- /dev/null +++ test/std/experimental/task/sync_wait.hpp @@ -0,0 +1,285 @@ +// -*- C++ -*- +//===----------------------------------------------------------------------===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#ifndef TEST_EXPERIMENTAL_TASK_SYNC_WAIT_HPP +#define TEST_EXPERIMENTAL_TASK_SYNC_WAIT_HPP + +#include +#include +#include +#include +#include +#include + +#include "awaitable_traits.hpp" +#include "test_macros.h" + +namespace test_detail { + +// Thread-synchronisation helper that allows one thread to block in a call +// to .wait() until another thread signals the thread by calling .set(). +class oneshot_event +{ +public: + oneshot_event() : isSet_(false) {} + + void set() noexcept + { + std::unique_lock lock{ mutex_ }; + isSet_ = true; + cv_.notify_all(); + } + + void wait() noexcept + { + std::unique_lock lock{ mutex_ }; + cv_.wait(lock, [this] { return isSet_; }); + } + +private: + std::mutex mutex_; + std::condition_variable cv_; + bool isSet_; +}; + +template +class sync_wait_promise_base +{ +public: + + using handle_t = std::experimental::coroutine_handle; + +private: + + struct FinalAwaiter + { + bool await_ready() noexcept { return false; } + void await_suspend(handle_t coro) noexcept + { + sync_wait_promise_base& promise = coro.promise(); + promise.event_.set(); + } + void await_resume() noexcept {} + }; + +public: + + handle_t get_return_object() { return handle(); } + std::experimental::suspend_always initial_suspend() { return {}; } + FinalAwaiter final_suspend() { return {}; } + +private: + + handle_t handle() noexcept + { + return handle_t::from_promise(static_cast(*this)); + } + +protected: + + // Start the coroutine and then block waiting for it to finish. + void run() noexcept + { + handle().resume(); + event_.wait(); + } + +private: + + oneshot_event event_; + +}; + +template +class sync_wait_promise final + : public sync_wait_promise_base> +{ +public: + + sync_wait_promise() : state_(state_t::empty) {} + + ~sync_wait_promise() + { + switch (state_) + { + case state_t::empty: + case state_t::value: + break; + case state_t::exception: +#ifndef TEST_HAS_NO_EXCEPTIONS + exception_.~exception_ptr(); +#endif + break; + } + } + + void return_void() noexcept + { + // Should be unreachable since coroutine should always + // suspend at `co_yield` point where it will be destroyed + // or will fail with an exception and bypass return_void() + // and call unhandled_exception() instead. + std::abort(); + } + + void unhandled_exception() noexcept + { +#ifndef TEST_HAS_NO_EXCEPTIONS + ::new (static_cast(&exception_)) std::exception_ptr( + std::current_exception()); + state_ = state_t::exception; +#else + std::abort(); +#endif + } + + auto yield_value(Tp&& value) noexcept + { + valuePtr_ = std::addressof(value); + state_ = state_t::value; + return this->final_suspend(); + } + + Tp&& get() + { + this->run(); + +#ifndef TEST_HAS_NO_EXCEPTIONS + if (state_ == state_t::exception) + { + std::rethrow_exception(std::move(exception_)); + } +#endif + + assert(state_ == state_t::value); + return static_cast(*valuePtr_); + } + +private: + + enum class state_t { + empty, + value, + exception + }; + + state_t state_; + union { + std::add_pointer_t valuePtr_; + std::exception_ptr exception_; + }; + +}; + +template<> +struct sync_wait_promise final + : public sync_wait_promise_base> +{ +public: + + void unhandled_exception() noexcept + { +#ifndef TEST_HAS_NO_EXCEPTIONS + exception_ = std::current_exception(); +#endif + } + + void return_void() noexcept {} + + void get() + { + this->run(); + +#ifndef TEST_HAS_NO_EXCEPTIONS + if (exception_) + { + std::rethrow_exception(std::move(exception_)); + } +#endif + } + +private: + + std::exception_ptr exception_; + +}; + +template +class sync_wait_task final +{ +public: + using promise_type = sync_wait_promise; + +private: + using handle_t = typename promise_type::handle_t; + +public: + + sync_wait_task(handle_t coro) noexcept : coro_(coro) {} + + ~sync_wait_task() + { + assert(coro_ && "Should always have a valid coroutine handle"); + coro_.destroy(); + } + + decltype(auto) get() + { + return coro_.promise().get(); + } +private: + handle_t coro_; +}; + +template +struct remove_rvalue_reference +{ + using type = Tp; +}; + +template +struct remove_rvalue_reference +{ + using type = Tp; +}; + +template +using remove_rvalue_reference_t = + typename remove_rvalue_reference::type; + +template< + typename Awaitable, + typename AwaitResult = await_result_t, + std::enable_if_t, int> = 0> +sync_wait_task make_sync_wait_task(Awaitable&& awaitable) +{ + co_await static_cast(awaitable); +} + +template< + typename Awaitable, + typename AwaitResult = await_result_t, + std::enable_if_t, int> = 0> +sync_wait_task make_sync_wait_task(Awaitable&& awaitable) +{ + co_yield co_await static_cast(awaitable); +} + +} // namespace test_detail + +template +auto sync_wait(Awaitable&& awaitable) + -> test_detail::remove_rvalue_reference_t> +{ + return test_detail::make_sync_wait_task( + static_cast(awaitable)).get(); +} + +#endif Index: test/std/experimental/task/task.basic/task_custom_allocator.pass.cpp =================================================================== --- /dev/null +++ test/std/experimental/task/task.basic/task_custom_allocator.pass.cpp @@ -0,0 +1,233 @@ +// -*- C++ -*- +//===----------------------------------------------------------------------===// +// +// The LLVM Compiler Infrastructure +// +// This file is dual licensed under the MIT and the University of Illinois Open +// Source Licenses. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: c++98, c++03, c++11, c++14 + +#include +#include +#include +#include +#include +#include + +#include "../sync_wait.hpp" + +namespace coro = std::experimental::coroutines_v1; + +namespace +{ + std::size_t allocator_instance_count = 0; + + // A custom allocator that tracks the number of allocator instances that + // have been constructed/destructed as well as the number of bytes that + // have been allocated/deallocated using the allocator. + template + class my_allocator { + public: + using value_type = T; + // Omit size_type, difference_type, is_always_equal to check that task + // implementation is using allocator_traits which provides defaults for these. + + explicit my_allocator( + std::shared_ptr totalAllocated) noexcept + : totalAllocated_(std::move(totalAllocated)) + { + ++allocator_instance_count; + assert(totalAllocated_); + } + + my_allocator(const my_allocator& other) + : totalAllocated_(other.totalAllocated_) + { + ++allocator_instance_count; + } + + my_allocator(my_allocator&& other) + : totalAllocated_(std::move(other.totalAllocated_)) + { + ++allocator_instance_count; + } + + template + my_allocator(const my_allocator& other) + : totalAllocated_(other.totalAllocated_) + { + ++allocator_instance_count; + } + + template + my_allocator(my_allocator&& other) + : totalAllocated_(std::move(other.totalAllocated_)) + { + ++allocator_instance_count; + } + + ~my_allocator() + { + assert(allocator_instance_count > 0); + --allocator_instance_count; + } + + T* allocate(std::size_t n) { + assert(totalAllocated_); + const auto byteCount = n * sizeof(T); + void* p = std::malloc(byteCount); + if (!p) { + throw std::bad_alloc{}; + } + *totalAllocated_ += byteCount; + return static_cast(p); + } + + void deallocate(char* p, std::size_t n) { + const auto byteCount = n * sizeof(T); + assert(totalAllocated_); + assert(byteCount <= *totalAllocated_); + *totalAllocated_ -= byteCount; + std::free(p); + } + private: + template + friend class my_allocator; + + std::shared_ptr totalAllocated_; + }; +} + +template +coro::task f(std::allocator_arg_t, [[maybe_unused]] Allocator alloc) +{ + co_return; +} + +void test_custom_allocator_is_destructed() +{ + auto totalAllocated = std::make_shared(0); + + assert(allocator_instance_count == 0); + + { + std::vector> tasks; + tasks.push_back( + f(std::allocator_arg, my_allocator{ totalAllocated })); + tasks.push_back( + f(std::allocator_arg, my_allocator{ totalAllocated })); + + assert(allocator_instance_count == 4); + assert(*totalAllocated > 0); + } + + assert(allocator_instance_count == 0); + assert(*totalAllocated == 0); +} + +void test_custom_allocator_type_rebinding() +{ + auto totalAllocated = std::make_shared(0); + { + std::vector> tasks; + tasks.emplace_back( + f(std::allocator_arg, my_allocator{ totalAllocated })); + ::sync_wait(tasks[0]); + } + assert(*totalAllocated == 0); + assert(allocator_instance_count == 0); +} + +void test_mixed_custom_allocator_type_erasure() +{ + assert(allocator_instance_count == 0); + + // Show that different allocators can be used within a vector of tasks + // of the same type. ie. that the allocator is type-erased inside the + // coroutine. + std::vector> tasks; + tasks.push_back(f( + std::allocator_arg, std::allocator{})); + tasks.push_back(f( + std::allocator_arg, + std::experimental::pmr::polymorphic_allocator{ + std::experimental::pmr::new_delete_resource() })); + tasks.push_back(f( + std::allocator_arg, + my_allocator{ std::make_shared(0) })); + + assert(allocator_instance_count > 0); + + for (auto& t : tasks) + { + ::sync_wait(t); + } + + tasks.clear(); + + assert(allocator_instance_count == 0); +} + +template +coro::task add_async(std::allocator_arg_t, [[maybe_unused]] Allocator alloc, int a, int b) +{ + co_return a + b; +} + +void test_task_custom_allocator_with_extra_args() +{ + std::vector> tasks; + + for (int i = 0; i < 5; ++i) { + tasks.push_back(add_async( + std::allocator_arg, + std::allocator{}, + i, 2 * i)); + } + + for (int i = 0; i < 5; ++i) + { + assert(::sync_wait(std::move(tasks[i])) == 3 * i); + } +} + +struct some_type { + template + coro::task get_async(std::allocator_arg_t, [[maybe_unused]] Allocator alloc) { + co_return 42; + } + + template + coro::task add_async(std::allocator_arg_t, [[maybe_unused]] Allocator alloc, int a, int b) { + co_return a + b; + } +}; + +void test_task_custom_allocator_on_member_function() +{ + assert(allocator_instance_count == 0); + + auto totalAllocated = std::make_shared(0); + some_type obj; + assert(::sync_wait(obj.get_async(std::allocator_arg, std::allocator{})) == 42); + assert(::sync_wait(obj.get_async(std::allocator_arg, my_allocator{totalAllocated})) == 42); + assert(::sync_wait(obj.add_async(std::allocator_arg, std::allocator{}, 2, 3)) == 5); + assert(::sync_wait(obj.add_async(std::allocator_arg, my_allocator{totalAllocated}, 2, 3)) == 5); + + assert(allocator_instance_count == 0); + assert(*totalAllocated == 0); +} + +int main(int, char**) +{ + test_custom_allocator_is_destructed(); + test_custom_allocator_type_rebinding(); + test_mixed_custom_allocator_type_erasure(); + test_task_custom_allocator_with_extra_args(); + test_task_custom_allocator_on_member_function(); + + return 0; +} Index: test/std/experimental/task/task.basic/task_of_value.pass.cpp =================================================================== --- /dev/null +++ test/std/experimental/task/task.basic/task_of_value.pass.cpp @@ -0,0 +1,70 @@ +// -*- C++ -*- +//===----------------------------------------------------------------------===// +// +// The LLVM Compiler Infrastructure +// +// This file is dual licensed under the MIT and the University of Illinois Open +// Source Licenses. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: c++98, c++03, c++11, c++14 + +#include +#include +#include +#include +#include "../sync_wait.hpp" + +void test_returning_move_only_type() +{ + auto move_only_async = + [](bool x) -> std::experimental::task> { + if (x) { + auto p = std::make_unique(123); + co_return p; // Should be implicit std::move(p) here. + } + + co_return std::make_unique(456); + }; + + assert(*::sync_wait(move_only_async(true)) == 123); + assert(*::sync_wait(move_only_async(false)) == 456); +} + +void test_co_return_with_curly_braces() +{ + auto t = []() -> std::experimental::task> + { + co_return { 123, "test" }; + }(); + + auto result = ::sync_wait(std::move(t)); + + assert(std::get<0>(result) == 123); + assert(std::get<1>(result) == "test"); +} + +void test_co_return_by_initialiser_list() +{ + auto t = []() -> std::experimental::task> + { + co_return { 2, 10, -1 }; + }(); + + auto result = ::sync_wait(std::move(t)); + + assert(result.size() == 3); + assert(result[0] == 2); + assert(result[1] == 10); + assert(result[2] == -1); +} + +int main(int, char**) +{ + test_returning_move_only_type(); + test_co_return_with_curly_braces(); + test_co_return_by_initialiser_list(); + + return 0; +} Index: test/std/experimental/task/task.basic/task_of_void.pass.cpp =================================================================== --- /dev/null +++ test/std/experimental/task/task.basic/task_of_void.pass.cpp @@ -0,0 +1,96 @@ +// -*- C++ -*- +//===----------------------------------------------------------------------===// +// +// The LLVM Compiler Infrastructure +// +// This file is dual licensed under the MIT and the University of Illinois Open +// Source Licenses. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: c++98, c++03, c++11, c++14 + +#include +#include "../manual_reset_event.hpp" +#include "../sync_wait.hpp" + +#include +#include + +namespace coro = std::experimental::coroutines_v1; + +static bool has_f_executed = false; + +static coro::task f() +{ + has_f_executed = true; + co_return; +} + +static void test_coroutine_executes_lazily() +{ + coro::task t = f(); + assert(!has_f_executed); + ::sync_wait(t); + assert(has_f_executed); +} + +static std::optional last_value_passed_to_g; + +static coro::task g(int a) +{ + last_value_passed_to_g = a; + co_return; +} + +void test_coroutine_accepts_arguments() +{ + auto t = g(123); + assert(!last_value_passed_to_g); + ::sync_wait(t); + assert(last_value_passed_to_g); + assert(*last_value_passed_to_g == 123); +} + +int shared_value = 0; +int read_value = 0; + +coro::task consume_async(manual_reset_event& event) +{ + co_await event; + read_value = shared_value; +} + +void produce(manual_reset_event& event) +{ + shared_value = 101; + event.set(); +} + +void test_async_completion() +{ + manual_reset_event e; + std::thread t1{ [&e] + { + ::sync_wait(consume_async(e)); + }}; + + assert(read_value == 0); + + std::thread t2{ [&e] { produce(e); }}; + + t1.join(); + + assert(read_value == 101); + + t2.join(); +} + +int main(int, char**) +{ + test_coroutine_executes_lazily(); + test_coroutine_accepts_arguments(); + test_async_completion(); + + return 0; +} Index: test/std/experimental/task/task.lifetime/task_parameter_lifetime.pass.cpp =================================================================== --- /dev/null +++ test/std/experimental/task/task.lifetime/task_parameter_lifetime.pass.cpp @@ -0,0 +1,56 @@ +// -*- C++ -*- +//===----------------------------------------------------------------------===// +// +// The LLVM Compiler Infrastructure +// +// This file is dual licensed under the MIT and the University of Illinois Open +// Source Licenses. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: c++98, c++03, c++11, c++14 + +#include +#include + +#include "../counted.hpp" +#include "../sync_wait.hpp" + +void test_parameter_lifetime() +{ + counted::reset(); + + auto f = [](counted c) -> std::experimental::task + { + co_return c.id(); + }; + + { + auto t = f({}); + + assert(counted::active_instance_count() == 1); + assert(counted::copy_constructor_count() == 0); + assert(counted::move_constructor_count() <= 2); // Ideally <= 1 + + auto id = ::sync_wait(t); + assert(id == 1); + + assert(counted::active_instance_count() == 1); + assert(counted::copy_constructor_count() == 0); + + // We are relying on C++17 copy-elision when passing the temporary counter + // into f(). Then f() must move the parameter into the coroutine frame by + // calling the move-constructor. This move could also potentially be + // elided by the compiler if the parameter is not referenced after the + // first suspend-point. + assert(counted::move_constructor_count() <= 1); + } + + assert(counted::active_instance_count() == 0); +} + +int main(int, char**) +{ + test_parameter_lifetime(); + return 0; +} Index: test/std/experimental/task/task.lifetime/task_return_value_lifetime.pass.cpp =================================================================== --- /dev/null +++ test/std/experimental/task/task.lifetime/task_return_value_lifetime.pass.cpp @@ -0,0 +1,157 @@ +// -*- C++ -*- +//===----------------------------------------------------------------------===// +// +// The LLVM Compiler Infrastructure +// +// This file is dual licensed under the MIT and the University of Illinois Open +// Source Licenses. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: c++98, c++03, c++11, c++14 + +#include +#include +#include + +#include "../counted.hpp" +#include "../sync_wait.hpp" + +void test_return_value_lifetime() +{ + counted::reset(); + + auto f = [](bool x) -> std::experimental::task + { + if (x) { + counted c; + co_return std::move(c); + } + co_return {}; + }; + + { + auto t = f(true); + + assert(counted::active_instance_count() == 0); + assert(counted::copy_constructor_count() == 0); + assert(counted::move_constructor_count() == 0); + + { + auto c = ::sync_wait(std::move(t)); + assert(c.id() == 1); + + assert(counted::active_instance_count() == 2); + assert(counted::copy_constructor_count() == 0); + assert(counted::move_constructor_count() > 0); + assert(counted::default_constructor_count() == 1); + } + + // The result value in 't' is still alive until 't' destructs. + assert(counted::active_instance_count() == 1); + } + + assert(counted::active_instance_count() == 0); + + counted::reset(); + + { + auto t = f(false); + + assert(counted::active_instance_count() == 0); + assert(counted::copy_constructor_count() == 0); + assert(counted::move_constructor_count() == 0); + + { + auto c = ::sync_wait(std::move(t)); + assert(c.id() == 1); + + assert(counted::active_instance_count() == 2); + assert(counted::copy_constructor_count() == 0); + assert(counted::move_constructor_count() > 0); + assert(counted::default_constructor_count() == 1); + } + + // The result value in 't' is still alive until 't' destructs. + assert(counted::active_instance_count() == 1); + } +} + +struct my_error {}; + +struct throws_on_destruction +{ + ~throws_on_destruction() noexcept(false) + { + throw my_error{}; + } +}; + +void test_uncaught_exception_thrown_after_co_return() +{ + counted::reset(); + + assert(counted::active_instance_count() == 0); + assert(counted::copy_constructor_count() == 0); + assert(counted::move_constructor_count() == 0); + + { + auto t = []() -> std::experimental::task + { + throws_on_destruction d; + co_return counted{}; + }(); + + try { + (void)::sync_wait(std::move(t)); + assert(false); + } catch (const my_error&) { + } + + assert(counted::active_instance_count() == 0); + assert(counted::copy_constructor_count() == 0); + assert(counted::move_constructor_count() > 0); + assert(counted::default_constructor_count() == 1); + } + + assert(counted::active_instance_count() == 0); +} + +void test_exception_thrown_and_caught_after_co_return() +{ + counted::reset(); + + assert(counted::active_instance_count() == 0); + assert(counted::copy_constructor_count() == 0); + assert(counted::move_constructor_count() == 0); + + { + auto t = []() -> std::experimental::task + { + try { + throws_on_destruction d; + co_return counted{}; + } catch(const my_error&) { + co_return counted{}; + } + }(); + + auto c = ::sync_wait(std::move(t)); + assert(c.id() == 2); + + assert(counted::active_instance_count() == 2); + assert(counted::copy_constructor_count() == 0); + assert(counted::move_constructor_count() > 0); + assert(counted::default_constructor_count() == 2); + } + + assert(counted::active_instance_count() == 0); +} + +int main(int, char**) +{ + test_return_value_lifetime(); + test_uncaught_exception_thrown_after_co_return(); + test_exception_thrown_and_caught_after_co_return(); + return 0; +}