From cb62ff8daedfc3830245e96acb474114f7d1da6f Mon Sep 17 00:00:00 2001 From: Khalil Estell Date: Sun, 5 Jul 2026 16:54:21 -0700 Subject: [PATCH 1/3] Add initial defer awaitable and cleanup class --- modules/coroutine.cppm | 102 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/modules/coroutine.cppm b/modules/coroutine.cppm index 3d96666..efd9315 100644 --- a/modules/coroutine.cppm +++ b/modules/coroutine.cppm @@ -1044,6 +1044,8 @@ private: // // ============================================================================= +export class cleanup_base; + /** * @brief The base promise class for coroutine operations * @@ -1271,6 +1273,7 @@ public: } protected: + friend class cleanup_base; /** * @brief Type alias for cancellation function pointer * @@ -1285,6 +1288,7 @@ protected: std::coroutine_handle<> m_continuation = context::noop_sentinel; class context* m_context = nullptr; cancellation_fn* m_cancel = nullptr; + cleanup_base* m_cleanup_linked_list = nullptr; }; export template @@ -1981,4 +1985,102 @@ void context::cancel() m_awaiting_caller = nullptr; } } + +class cleanup_base +{ +public: + cleanup_base(promise_base* p_promise) + : m_promise(p_promise) + { +#if defined(__cpp_contracts) + // It should never be possible to pass a null promise base to + // async::cleanup. + contract_assert(p_promise != nullptr); +#endif + // Make the start end of the cleanup list our next cleanup + m_next_cleanup = m_promise->m_cleanup_linked_list; + // Make ourselves the start of the linked list + m_promise->m_cleanup_linked_list = this; + } + + cleanup_base(cleanup_base const&) = delete; + cleanup_base& operator=(cleanup_base const&) = delete; + cleanup_base(cleanup_base&&) = delete; + cleanup_base& operator=(cleanup_base&&) = delete; + + ~cleanup_base() + { +#if defined(__cpp_contracts) + // This cleanup object was destroyed outside of its scope ordering, + // meaning it's destructor was called directly in some way that violates + // the ordering of the other cleanup objects within the same function." + contract_assert(m_promise->m_cleanup_linked_list == this); +#endif + // Replace ourselves with our next cleanup + m_promise->m_cleanup_linked_list = m_next_cleanup; + } + + void cleanup_function(async::context& p_context) + { + m_cleanup_function(p_context, this); + } + +private: + promise_base* m_promise = nullptr; + cleanup_base* m_next_cleanup = nullptr; + +protected: + using cleanup_factory_function = async::future(async::context&, void*); + cleanup_factory_function* m_cleanup_function = nullptr; +}; + +template +class cleanup_function : public cleanup_base +{ +public: + cleanup_function(promise_base* p_promise, Callable&& p_callable) + : cleanup_base(p_promise) + , m_cleanup_function_object(std::move(p_callable)) + { + m_cleanup_function = &cleanup_handler; + } + + static async::future cleanup_handler(async::context& p_context, + void* p_instance) + { + return static_cast*>(p_instance) + ->m_cleanup_function_object(p_context); + } + +private: + Callable m_cleanup_function_object; +}; + +export template +class defer +{ + defer(Callable&& p_callable) + : m_callable(std::move(p_callable)) + { + } + + [[nodiscard]] consteval bool await_ready() const noexcept + { + return false; + } + + bool await_suspend(std::coroutine_handle p_caller) noexcept + { + m_promise = &p_caller.promise(); + return false; // Tell caller/awaiter to continue their own execution + } + + cleanup_function await_resume() + { + return { m_promise, m_callable }; + } + + Callable&& m_callable; + promise_base* m_promise = nullptr; +}; } // namespace async::inline v0 From 64ffb9093be82feff542f34feae73632881a9ef4 Mon Sep 17 00:00:00 2001 From: Khalil Estell Date: Sun, 5 Jul 2026 17:05:27 -0700 Subject: [PATCH 2/3] Add failing test for defer --- CMakeLists.txt | 23 ++++++++++++----------- modules/coroutine.cppm | 9 ++++++--- tests/defer.test.cpp | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 14 deletions(-) create mode 100644 tests/defer.test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0fbfea0..9a182d5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,18 +32,19 @@ libhal_apply_compile_options(async_context) libhal_install_library(async_context NAMESPACE libhal) libhal_add_tests(async_context TEST_NAMES - sync_wait + # sync_wait basics - blocked_by - cancel - mutex - proxy - context_listener - simple_scheduler - clock_adapter - run_until_done - async_stacking - cross_context_await + # blocked_by + # cancel + # mutex + # proxy + # context_listener + # simple_scheduler + # clock_adapter + # run_until_done + # async_stacking + # cross_context_await + defer MODULES tests/util.cppm diff --git a/modules/coroutine.cppm b/modules/coroutine.cppm index efd9315..a336908 100644 --- a/modules/coroutine.cppm +++ b/modules/coroutine.cppm @@ -2059,17 +2059,19 @@ private: export template class defer { +public: defer(Callable&& p_callable) : m_callable(std::move(p_callable)) { } - [[nodiscard]] consteval bool await_ready() const noexcept + [[nodiscard]] constexpr bool await_ready() const noexcept { return false; } - bool await_suspend(std::coroutine_handle p_caller) noexcept + template + bool await_suspend(std::coroutine_handle> p_caller) noexcept { m_promise = &p_caller.promise(); return false; // Tell caller/awaiter to continue their own execution @@ -2077,9 +2079,10 @@ class defer cleanup_function await_resume() { - return { m_promise, m_callable }; + return { m_promise, std::move(m_callable) }; } +private: Callable&& m_callable; promise_base* m_promise = nullptr; }; diff --git a/tests/defer.test.cpp b/tests/defer.test.cpp new file mode 100644 index 0000000..df5444d --- /dev/null +++ b/tests/defer.test.cpp @@ -0,0 +1,38 @@ +#include + +#include +#include + +import async_context; +import test_utils; + +void defer_tests() +{ + using namespace boost::ut; + + // Setup + async::inplace_context<1024> ctx; + + bool cleanup_ran = false; + + auto coroutine = [&](async::context&) -> async::future { + co_await async::defer([&](async::context&) -> async::future { + cleanup_ran = true; + co_return; + }); + co_return; + }; + + // Exercise + auto future = coroutine(ctx); + future.resume(); + + // Verify + expect(that % future.done()); + expect(that % cleanup_ran); +}; + +int main() +{ + defer_tests(); +} From 436b98d0bf0ef0f0436ddf9e3608fd4c6d1db0e3 Mon Sep 17 00:00:00 2001 From: Khalil Estell Date: Mon, 6 Jul 2026 16:42:24 -0700 Subject: [PATCH 3/3] more work on this but I realized this will not work lol --- modules/coroutine.cppm | 55 +++++++++++++++++++++++++++--------------- tests/defer.test.cpp | 19 +++++++++++++-- 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/modules/coroutine.cppm b/modules/coroutine.cppm index a336908..f3fccaf 100644 --- a/modules/coroutine.cppm +++ b/modules/coroutine.cppm @@ -1274,6 +1274,9 @@ public: protected: friend class cleanup_base; + + template + friend struct final_awaiter; /** * @brief Type alias for cancellation function pointer * @@ -1380,22 +1383,7 @@ struct final_awaiter * originally). */ constexpr std::coroutine_handle<> await_suspend( - std::coroutine_handle p_completing_coroutine) noexcept - { - // The coroutine is now suspended at the final-suspend point. - // Lookup its continuation in the promise and resume it symmetrically. - // - // Rather than return control back to the application, we continue the - // caller function allowing it to yield when it reaches another suspend - // point. The idea is that prior to this being called, we were executing - // code and thus, when we resume the caller, we are still running code. - // Lets continue to run as much code until we reach an actual suspend - // point. - auto next_to_run = p_completing_coroutine.promise().pop_active_coroutine(); - // Destroy promise at this point as there is no more use for it. - p_completing_coroutine.destroy(); - return next_to_run; - } + std::coroutine_handle p_completing_coroutine) noexcept; /** * @brief Handle resume after completion @@ -1997,7 +1985,8 @@ public: // async::cleanup. contract_assert(p_promise != nullptr); #endif - // Make the start end of the cleanup list our next cleanup + // Make the start end of the cleanup list our next cleanup, starts at + // nullptr m_next_cleanup = m_promise->m_cleanup_linked_list; // Make ourselves the start of the linked list m_promise->m_cleanup_linked_list = this; @@ -2020,9 +2009,11 @@ public: m_promise->m_cleanup_linked_list = m_next_cleanup; } - void cleanup_function(async::context& p_context) + void load_cleanup() { - m_cleanup_function(p_context, this); + // m_next_cleanup is nullptr when it finishes + m_promise->m_cleanup_linked_list = m_next_cleanup; + m_future_storage = m_cleanup_function(*m_promise->m_context, this); } private: @@ -2032,6 +2023,7 @@ private: protected: using cleanup_factory_function = async::future(async::context&, void*); cleanup_factory_function* m_cleanup_function = nullptr; + future m_future_storage; }; template @@ -2086,4 +2078,29 @@ private: Callable&& m_callable; promise_base* m_promise = nullptr; }; + +template +constexpr std::coroutine_handle<> final_awaiter::await_suspend( + std::coroutine_handle p_completing_coroutine) noexcept +{ + auto& promise = p_completing_coroutine.promise(); + if (promise.m_cleanup_linked_list) { + promise.m_cleanup_linked_list->load_cleanup(); + return promise.m_context->active_handle(); + } else { + // The coroutine is now suspended at the final-suspend point. + // Lookup its continuation in the promise and resume it symmetrically. + // + // Rather than return control back to the application, we continue the + // caller function allowing it to yield when it reaches another suspend + // point. The idea is that prior to this being called, we were executing + // code and thus, when we resume the caller, we are still running code. + // Lets continue to run as much code until we reach an actual suspend + // point. + auto next_to_run = promise.pop_active_coroutine(); + // Destroy promise at this point as there is no more use for it. + p_completing_coroutine.destroy(); + return next_to_run; + } +} } // namespace async::inline v0 diff --git a/tests/defer.test.cpp b/tests/defer.test.cpp index df5444d..afca3e7 100644 --- a/tests/defer.test.cpp +++ b/tests/defer.test.cpp @@ -1,7 +1,7 @@ #include +#include #include -#include import async_context; import test_utils; @@ -15,8 +15,21 @@ void defer_tests() bool cleanup_ran = false; + struct obj + { + obj() + { + std::println("Created"); + } + ~obj() + { + std::println("Destroyed"); + } + }; + auto coroutine = [&](async::context&) -> async::future { - co_await async::defer([&](async::context&) -> async::future { + obj o; + auto v = co_await async::defer([&](async::context&) -> async::future { cleanup_ran = true; co_return; }); @@ -25,7 +38,9 @@ void defer_tests() // Exercise auto future = coroutine(ctx); + std::println("Before Resume"); future.resume(); + std::println("After Resume"); // Verify expect(that % future.done());