From 8e516f4621945e44be7a65f5fec9d61e0b5fdbac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Kov=C3=A1cs?= Date: Sat, 9 May 2026 16:18:21 +0200 Subject: [PATCH] add continuation --- include/tinycoro/Promise.hpp | 13 ++++---- include/tinycoro/PromiseBase.hpp | 44 ++++++++++++------------- include/tinycoro/PromiseSchedulable.hpp | 2 +- include/tinycoro/SharedState.hpp | 7 +++- include/tinycoro/TaskAwaiter.hpp | 14 ++++++-- include/tinycoro/TaskResumer.hpp | 36 +++++--------------- test/src/TaskAwaiter_test.cpp | 25 ++++++++------ test/src/TaskResumer_test.cpp | 33 ------------------- test/src/YieldValue_test.cpp | 30 +++++++++++++++++ 9 files changed, 101 insertions(+), 103 deletions(-) delete mode 100644 test/src/TaskResumer_test.cpp diff --git a/include/tinycoro/Promise.hpp b/include/tinycoro/Promise.hpp index 8954f0b6..ae0c3e6b 100644 --- a/include/tinycoro/Promise.hpp +++ b/include/tinycoro/Promise.hpp @@ -24,16 +24,17 @@ namespace tinycoro { { auto& promise = hdl.promise(); - if (promise.parent) + if (auto* parent = promise.Parent()) { - using promise_t = std::remove_pointer_t; - // reset the parent child, - // because we are done. - promise.parent->child = nullptr; + using promise_t = std::remove_pointer_t; + auto sharedState = promise.SharedState(); + + // pop the first element from the stack. + sharedState->conti = parent; // We can strait jump and resume // the parent coroutine. - return std::coroutine_handle::from_promise(*promise.parent); + return std::coroutine_handle::from_promise(*parent); } return std::noop_coroutine(); diff --git a/include/tinycoro/PromiseBase.hpp b/include/tinycoro/PromiseBase.hpp index 9c0f10a6..66d94917 100644 --- a/include/tinycoro/PromiseBase.hpp +++ b/include/tinycoro/PromiseBase.hpp @@ -49,7 +49,7 @@ namespace tinycoro { namespace detail { { auto stopTokenUser = SharedState() ? SharedState()->IsStopTokenUser() : false; - if (parent == nullptr && stopTokenUser == false) + if (Parent() == nullptr && stopTokenUser == false) { // The parent coroutine is nullptr, // that means this is a root @@ -67,11 +67,11 @@ namespace tinycoro { namespace detail { // Disallow copy and move PromiseBase(PromiseBase&&) = delete; - // These are navigation pointers used to - // resume and chain together coroutines, - // enabling continuous execution. - PromiseBase_t* parent{nullptr}; - PromiseBase_t* child{nullptr}; + // Link to the previous promise (parent) in the continuation chain. + // When a task awaits another task, the child promise is pushed + // onto "SharedState::conti" head and this field points to the promise + // that was previously at the top of that stack. + PromiseBase_t* conti{nullptr}; // At the beginning we not initialize // the stop source here, the initialization @@ -106,8 +106,8 @@ namespace tinycoro { namespace detail { // - the current awaitable pointer for CoWait constructs, or // - any data required for resumption in a TaskGroup. void SetCustomData(void* data) noexcept - { - assert(parent == nullptr); // need to be a root corouitne + { + assert(Parent() == nullptr); // need to be a root corouitne assert(_customData == nullptr); // must not be set _customData = data; @@ -116,7 +116,7 @@ namespace tinycoro { namespace detail { // Get the stored custom data and clears it. [[nodiscard]] constexpr auto CustomData() const noexcept { - assert(parent == nullptr); // need to be a root corouitne + assert(Parent() == nullptr); // need to be a root corouitne return _customData; } @@ -127,32 +127,32 @@ namespace tinycoro { namespace detail { constexpr void unhandled_exception() const { std::rethrow_exception(std::current_exception()); } - constexpr auto SharedState() noexcept - { - return detail::PtrVisit(_sharedState); - } + constexpr auto SharedState() noexcept { return detail::PtrVisit(_sharedState); } - constexpr auto SharedState() const noexcept - { - return detail::PtrVisit(_sharedState); - } + constexpr auto SharedState() const noexcept { return detail::PtrVisit(_sharedState); } constexpr void AssignSharedState(auto sharedStatePtr) noexcept { assert(sharedStatePtr); - assert(parent != nullptr); + assert(Parent() != nullptr); _sharedState = sharedStatePtr; } -protected: + constexpr PromiseBase_t* Parent() const noexcept { return conti; } + + protected: constexpr void CreateSharedState(bool initialCancellable = tinycoro::default_initial_cancellable_policy::value) noexcept { // make sure this is called only once assert(SharedState() == nullptr); - assert(parent == nullptr); + assert(Parent() == nullptr); + + // create the shared state + auto& sharedState = _sharedState.emplace(initialCancellable); - _sharedState.emplace(initialCancellable); + // set continuation as this + sharedState.conti = this; } private: @@ -172,4 +172,4 @@ namespace tinycoro { namespace detail { }} // namespace tinycoro::detail -#endif // TINY_CORO_PROMISE_BASE_HPP \ No newline at end of file +#endif // TINY_CORO_PROMISE_BASE_HPP diff --git a/include/tinycoro/PromiseSchedulable.hpp b/include/tinycoro/PromiseSchedulable.hpp index b9006e29..6b8dbf4f 100644 --- a/include/tinycoro/PromiseSchedulable.hpp +++ b/include/tinycoro/PromiseSchedulable.hpp @@ -27,7 +27,7 @@ namespace tinycoro #endif template - struct SchedulablePromise : PromiseBase, detail::DoubleLinkable> + struct SchedulablePromise : PromiseBase, detail::SingleLinkable> { static_assert(BUFFER_SIZE >= PROMISE_BASE_BUFFER_SIZE, "SchedulablePromise: Buffer size is too small to hold the promise object."); diff --git a/include/tinycoro/SharedState.hpp b/include/tinycoro/SharedState.hpp index f610af51..e534f850 100644 --- a/include/tinycoro/SharedState.hpp +++ b/include/tinycoro/SharedState.hpp @@ -104,6 +104,11 @@ namespace tinycoro { // clear the first bits which are responsible for the pause state in scheduler. return _state.fetch_and(~detail::UTypeCast(detail::EPauseState::IDLE), order); }; + + // Type-erased head of the continuation stack. + // The stack contains promise objects linked through + // PromiseBase::conti, with the most recently awaited task on top. + void* conti{nullptr}; private: // atomic flag to store shared state @@ -150,4 +155,4 @@ namespace tinycoro { } // namespace tinycoro -#endif // TINY_CORO_SHARED_STATE_HPP \ No newline at end of file +#endif // TINY_CORO_SHARED_STATE_HPP diff --git a/include/tinycoro/TaskAwaiter.hpp b/include/tinycoro/TaskAwaiter.hpp index f1156e59..8856cc32 100644 --- a/include/tinycoro/TaskAwaiter.hpp +++ b/include/tinycoro/TaskAwaiter.hpp @@ -20,10 +20,18 @@ namespace tinycoro { auto hdl = coroTask->_hdl; auto& promise = hdl.promise(); - parentPromise.child = std::addressof(promise); - promise.parent = std::addressof(parentPromise); + using promiseBase_t = decltype(promise.conti); + + auto* sharedState = parentPromise.SharedState(); + auto* conti = static_cast(sharedState->conti); + + assert(conti); + + promise.conti = conti; + sharedState->conti = &promise; + promise.stopSource = parentPromise.StopSource(); - promise.AssignSharedState(parentPromise.SharedState()); + promise.AssignSharedState(sharedState); return hdl; } diff --git a/include/tinycoro/TaskResumer.hpp b/include/tinycoro/TaskResumer.hpp index e4b1653f..1be3d490 100644 --- a/include/tinycoro/TaskResumer.hpp +++ b/include/tinycoro/TaskResumer.hpp @@ -14,47 +14,29 @@ namespace tinycoro { namespace detail { struct TaskResumer { - // Find the last continuation - // and set up the loop at the end - // if necessary. - template - [[nodiscard]] static inline auto FindContinuation(PromiseBaseT* promisePtr) noexcept - { - assert(promisePtr); - - // Iterate until we found the last corouinte - // in the chain, which we need to resume. - while (promisePtr->child != nullptr) - { - // moving forward... - promisePtr = promisePtr->child; - } - - return promisePtr; - } - template static inline void Resume(PromiseT& promise) { - auto sharedStatePtr = promise.SharedState(); - const auto& stopSource = promise.stopSource; + auto* sharedState = promise.SharedState(); + const auto& stopSource = promise.stopSource; + + assert(sharedState); + assert(sharedState->conti); // reset the pause state by every resume. - sharedStatePtr->ClearPauseStateBits(); + sharedState->ClearPauseStateBits(); - if (sharedStatePtr->IsCancellable() && stopSource.stop_requested()) + if (sharedState->IsCancellable() && stopSource.stop_requested()) { return; // need to cancel the corouitne } // Resets all the flags. - sharedStatePtr->ClearFlags(); + sharedState->ClearFlags(); // check for continuation type using promise_base_t = PromiseT::PromiseBase_t; - - // find the continuation - auto promiseToResume = FindContinuation(std::addressof(promise)); + auto* promiseToResume = static_cast(sharedState->conti); // Resume the coroutine. // diff --git a/test/src/TaskAwaiter_test.cpp b/test/src/TaskAwaiter_test.cpp index 4bb56cf1..c38f2a8d 100644 --- a/test/src/TaskAwaiter_test.cpp +++ b/test/src/TaskAwaiter_test.cpp @@ -15,6 +15,8 @@ struct SharedStateMock { } + void* conti; + size_t val; }; @@ -83,8 +85,7 @@ struct HandleMock template struct PromiseMock { - PromiseMock* child; - PromiseMock* parent; + PromiseMock* conti; StopSourceMock stopSource; SharedStateMock sharedState; @@ -105,8 +106,7 @@ struct PromiseMock template<> struct PromiseMock { - PromiseMock* child; - PromiseMock* parent; + PromiseMock* conti; StopSourceMock stopSource; SharedStateMock sharedState; @@ -125,6 +125,11 @@ struct CoroTaskMock : public AwaiterT> { using handle_type = tinycoro::test::CoroutineHandleMock>; + void InitConti() + { + _hdl.promise().sharedState.conti = &_hdl.promise(); + } + handle_type _hdl; }; @@ -165,13 +170,12 @@ TEST(TaskAwaiterTest, TaskAwaiterTest_await_suspend_int) task._hdl.promise()._value = 42; CoroTaskMock parent; + parent.InitConti(); std::ignore = task.await_suspend(parent._hdl); - EXPECT_EQ(parent._hdl.promise().child->value(), 42); - - EXPECT_EQ(task._hdl.promise().parent->sharedState.val, parent._hdl.promise().sharedState.val); - EXPECT_EQ(task._hdl.promise().parent->stopSource.val, parent._hdl.promise().stopSource.val); + EXPECT_EQ(task._hdl.promise().conti->sharedState.val, parent._hdl.promise().sharedState.val); + EXPECT_EQ(task._hdl.promise().conti->stopSource.val, parent._hdl.promise().stopSource.val); } TEST(TaskAwaiterTest, TaskAwaiterTest_await_suspend_void) @@ -179,9 +183,10 @@ TEST(TaskAwaiterTest, TaskAwaiterTest_await_suspend_void) CoroTaskMock task; CoroTaskMock parent; + parent.InitConti(); std::ignore = task.await_suspend(parent._hdl); - EXPECT_EQ(task._hdl.promise().parent->sharedState.val, parent._hdl.promise().sharedState.val); - EXPECT_EQ(task._hdl.promise().parent->stopSource.val, parent._hdl.promise().stopSource.val); + EXPECT_EQ(task._hdl.promise().conti->sharedState.val, parent._hdl.promise().sharedState.val); + EXPECT_EQ(task._hdl.promise().conti->stopSource.val, parent._hdl.promise().stopSource.val); } \ No newline at end of file diff --git a/test/src/TaskResumer_test.cpp b/test/src/TaskResumer_test.cpp deleted file mode 100644 index 5c2c5b9b..00000000 --- a/test/src/TaskResumer_test.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#include - -#include - -#include - -struct ContinuationMock -{ - ContinuationMock* child{nullptr}; -}; - -TEST(TaskResumerTest, TaskResumerTest_FindContinuationAndConnect) -{ - ContinuationMock p1; - ContinuationMock p2; - ContinuationMock p3; - - p1.child = std::addressof(p2); - p2.child = std::addressof(p3); - - auto promise = tinycoro::detail::TaskResumer::FindContinuation(&p1); - EXPECT_EQ(std::addressof(p3), promise); -} - -TEST(TaskResumerTest, TaskResumerTest_FindContinuationAndConnect_single_root) -{ - ContinuationMock p1; - - auto promise = tinycoro::detail::TaskResumer::FindContinuation(&p1); - EXPECT_EQ(std::addressof(p1), promise); - - EXPECT_EQ(p1.child, nullptr); -} \ No newline at end of file diff --git a/test/src/YieldValue_test.cpp b/test/src/YieldValue_test.cpp index e9949afd..c2f4a1da 100644 --- a/test/src/YieldValue_test.cpp +++ b/test/src/YieldValue_test.cpp @@ -48,6 +48,19 @@ TEST_P(YieldValueTest, YieldValueTest_generator) // See: https://stackoverflow.com/questions/67860049/why-cant-co-await-return-a-string #ifndef TINY_CORO_GCC_11 +tinycoro::Task NestedYieldCoroutine() +{ + co_yield 41; + co_return 42; +} + +tinycoro::Task IntYieldCoroutine() +{ + auto nested = NestedYieldCoroutine(); + co_yield co_await nested; + co_return co_await nested; +} + tinycoro::Task> YieldCoroutine() { co_yield 41; @@ -73,6 +86,23 @@ TEST(YieldValueTest, YieldValueTest_variant_yield) tinycoro::AllOf(scheduler, runner()); } +TEST(YieldValueTest, NestedYieldValueTest_int_yield) +{ + tinycoro::Scheduler scheduler; + + auto runner = []() -> tinycoro::Task { + auto task = IntYieldCoroutine(); + + auto val = co_await task; + EXPECT_EQ(val, 41); + + val = co_await task; + EXPECT_EQ(val, 42); + }; + + tinycoro::AllOf(scheduler, runner()); +} + TEST(YieldValueTest, YieldValueTest_variant_yield_runinline) { auto runner = []() -> tinycoro::Task {