Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@ libhal_add_tests(async_context
cancel
exclusive_access
proxy
basics_dep_inject
on_unblock
simple_scheduler
clock_adapter
run_until_done
async_stacking
context_swapping

MODULES
tests/util.cppm
Expand Down
91 changes: 74 additions & 17 deletions modules/coroutine.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,13 @@ export struct bad_coroutine_alloc : std::bad_alloc
* has been cancelled. It indicates that the operation was explicitly cancelled
* before completion.
*/
export class operation_cancelled : public std::exception
export struct operation_cancelled : public std::exception
{
operation_cancelled(void const* p_future_address)
: future_address(p_future_address)
{
}

/**
* @brief Get exception message
*
Expand All @@ -232,6 +237,8 @@ export class operation_cancelled : public std::exception
{
return "This future has been cancelled!";
}

void const* future_address = nullptr;
};

// =============================================================================
Expand Down Expand Up @@ -771,7 +778,8 @@ public:
* destroyed before it is removed from this context.
*
* @param p_listener - the address of the unblock listener to be invoked when
* this context is unblocked.
* this context is unblocked. A nullptr may be passed to this parameter. It
* has the same effect as calling `clear_unblock_listener()`.
*/
void on_unblock(unblock_listener* p_listener)
{
Expand Down Expand Up @@ -1926,6 +1934,11 @@ public:
}
}

constexpr bool cancelled()
{
return std::holds_alternative<cancelled_state>(m_state);
}

constexpr void resume() const
{
if (std::holds_alternative<handle_type>(m_state)) {
Expand Down Expand Up @@ -2004,30 +2017,34 @@ public:
{
future<T>& m_operation;

constexpr explicit awaiter(future<T>& p_operation
[[clang::lifetimebound]]) noexcept
constexpr explicit awaiter(future<T>& p_operation) noexcept
: m_operation(p_operation)
{
}

[[nodiscard]] constexpr bool await_ready() const noexcept
{
return m_operation.m_state.index() >= 1;
return not std::holds_alternative<handle_type>(m_operation.m_state);
}

/**
* @brief Communicates to the awaiter to simply resume this coroutine
* associated with this future.
*
* @tparam U - return type of the promise
* @param p_calling_coroutine - this type is forgotten. The parent calling
* coroutine's handle was captured when the future object was created via
* get_return_object().
* @return std::coroutine_handle<> - self to continue
*/
template<typename U>
std::coroutine_handle<> await_suspend(
std::coroutine_handle<promise<U>> p_calling_coroutine) noexcept
[[maybe_unused]] std::coroutine_handle<promise<U>>
p_calling_coroutine) noexcept
{
// This will not throw because the discriminate check was performed in
// `await_ready()` via the done() function. `done()` checks if the state
// is `handle_type` and if it is, it returns false, causing the code to
// call await_suspend().
auto handle = std::get<handle_type>(m_operation.m_state);
std::coroutine_handle<promise<U>>::from_address(handle.address())
.promise()
.continuation(p_calling_coroutine);
return handle;
// `await_ready()`.
return std::get<handle_type>(m_operation.m_state);
}

[[nodiscard]] constexpr monostate_or<T>& await_resume() const
Expand All @@ -2039,9 +2056,21 @@ public:
m_operation.m_state)) [[unlikely]] {
std::rethrow_exception(
std::get<std::exception_ptr>(m_operation.m_state));
} else if (std::holds_alternative<cancelled_state>(m_operation.m_state))
[[unlikely]] {
throw operation_cancelled{ &m_operation };
}

throw operation_cancelled{};
// In the event that this coroutine awaiting this awaitable has
// requested the result of this awaitable before it has finished, then:
//
// - If contracts are enabled, then contract violation handler is called.
// - Otherwise, std::terminate is called.
#if defined(__cpp_contracts)
contract_assert(std::holds_alternative<handle_type>(m_operation.m_state));
#else
std::terminate();
#endif
}

constexpr void await_resume() const
Expand All @@ -2054,9 +2083,21 @@ public:
m_operation.m_state)) [[unlikely]] {
std::rethrow_exception(
std::get<std::exception_ptr>(m_operation.m_state));
} else if (std::holds_alternative<cancelled_state>(m_operation.m_state))
[[unlikely]] {
throw operation_cancelled{ &m_operation };
}

throw operation_cancelled{};
// In the event that this coroutine awaiting this awaitable has
// requested the result of this awaitable before it has finished, then:
//
// - If contracts are enabled, then contract violation handler is called.
// - Otherwise, std::terminate is called.
#if defined(__cpp_contracts)
contract_assert(std::holds_alternative<handle_type>(m_operation.m_state));
#else
std::terminate();
#endif
}
};

Expand All @@ -2069,15 +2110,27 @@ public:
* @return awaiter - An awaiter object that handles the suspension and
* resumption of coroutines awaiting this future.
*
* @pre The coroutine's context and the future's context are the same.
* @note The awaiter will suspend the calling coroutine until this future
* completes, then resume with either the result value or an exception. The
* future will never be cancelled.
*/
[[nodiscard]] constexpr awaiter operator co_await() noexcept
[[nodiscard]] constexpr awaiter operator co_await() && noexcept
{
return awaiter{ *this };
}

/**
* @brief co_awaiting an l-value future is banned
*
* Co-awaiting an l-value future provides a means to accidentally await on a
* future with a different context than the awaiting coroutine. This is unsafe
* and thus, banned.
*
* @return Nothing, deleted implementation
*/
[[nodiscard]] constexpr awaiter operator co_await() & noexcept = delete;

private:
friend promise_type;

Expand Down Expand Up @@ -2127,6 +2180,10 @@ constexpr future<T> promise<T>::get_return_object() noexcept
{
using future_handle = std::coroutine_handle<promise<T>>;
auto handle = future_handle::from_promise(*this);
// Chain: whatever was active becomes our continuation.
// If nothing was active (noop_sentinel), this is a normal top-level
// coroutine. If something WAS active, we implicitly sit on top of it.
m_continuation = m_context->active_handle();
m_context->active_handle(handle);
return future<T>{ handle };
}
Expand Down
173 changes: 173 additions & 0 deletions tests/async_stacking.test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
#include <coroutine>

#include <boost/ut.hpp>

import async_context;
import test_utils;

// doesn't matter...
// bar runs until completion then foo runs until complete
void async_stacking()
{
using namespace boost::ut;

// LIFO stacking: the last routine pushed onto the context is the first
// to execute. When two routines are loaded onto the same context, the
// second one loaded runs first and the first one loaded runs last.

"two routines lifo order two steps each"_test = []() {
// Setup
async::inplace_context<1024> ctx;

unsigned step = 0;

auto routine_a = [&step](async::context&) -> async::future<void> {
// routine_a is loaded first; it runs last (LIFO)
step = 3;
co_await std::suspend_always{};
step = 4;
co_await std::suspend_always{};
co_return;
};

auto routine_b = [&step](async::context&) -> async::future<void> {
// routine_b is loaded second; it runs first (LIFO)
step = 1;
co_await std::suspend_always{};
step = 2;
co_await std::suspend_always{};
co_return;
};

// Exercise: load routine_a first, then routine_b
auto future_a = routine_a(ctx);
auto future_b = routine_b(ctx);

// Verify: neither has started yet
expect(that % not future_a.done());
expect(that % not future_b.done());
expect(that % 0 == step);

// Exercise: first resume — routine_b (last loaded) runs first, hits step=1
// then suspends
future_b.resume();

// Verify: routine_b ran first and suspended at step 1
expect(that % not future_b.done());
expect(that % not future_a.done());
expect(that % 1 == step);

// Exercise: second resume — routine_b resumes, hits step=2, suspends again
future_b.resume();

// Verify: routine_b suspended at step 2; routine_a still waiting
expect(that % not future_b.done());
expect(that % not future_a.done());
expect(that % 2 == step);

// Exercise: third resume — routine_b hits co_return and completes
future_b.resume();

// Verify: routine_b is done; routine_a still waiting
expect(that % future_b.done());
expect(that % not future_a.done());
expect(that % 3 == step);

// Exercise: fourth resume — routine_a (first loaded) now runs, hits step=3,
// then suspends
future_a.resume();

// Verify: routine_a ran and suspended at step 3
expect(that % not future_a.done());
expect(that % 4 == step);

// Exercise: fifth resume — routine_a resumes, hits step=4, suspends again
future_a.resume();

// Verify: routine_a is done; all memory released
expect(that % future_a.done());
expect(that % 0 == ctx.memory_used());
expect(that % 4 == step);
};

"three routines lifo order"_test = []() {
// Setup
async::inplace_context<2048> ctx;

unsigned step = 0;

auto routine_a = [&step](async::context&) -> async::future<void> {
// loaded first — runs last
step = 7;
co_await std::suspend_always{};
step = 8;
co_await std::suspend_always{};
co_return;
};

auto routine_b = [&step](async::context&) -> async::future<void> {
// loaded second — runs second
step = 4;
co_await std::suspend_always{};
step = 5;
co_await std::suspend_always{};
co_return;
};

auto routine_c = [&step](async::context&) -> async::future<void> {
// loaded third — runs first (LIFO)
step = 1;
co_await std::suspend_always{};
step = 2;
co_await std::suspend_always{};
co_return;
};

// Load in order: a, b, c
auto future_a = routine_a(ctx);
auto future_b = routine_b(ctx);
auto future_c = routine_c(ctx);

expect(that % 0 == step);

// routine_c runs first
future_c.resume();
expect(that % 1 == step);
expect(that % not future_c.done());

future_c.resume();
expect(that % 2 == step);
expect(that % not future_c.done());

future_c.resume();
expect(that % 4 == step);
expect(that % future_c.done());

// routine_b was already started by routine_c's co_return; resume to step=5
future_b.resume();
expect(that % 5 == step);
expect(that % not future_b.done());

// routine_b completes and immediately starts routine_a which runs to step=7
future_b.resume();
expect(that % 7 == step);
expect(that % future_b.done());
expect(that % not future_a.done());

future_a.resume();
expect(that % 8 == step);
expect(that % not future_a.done());

future_a.resume();
expect(that % 8 == step);
expect(that % future_a.done());

// All memory should be released
expect(that % 0 == ctx.memory_used());
};
}

int main()
{
async_stacking();
}
8 changes: 5 additions & 3 deletions tests/basics.test.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#include <coroutine>

#include <boost/ut.hpp>
#include <print>
#include <variant>

import async_context;
import test_utils;
Expand Down Expand Up @@ -164,9 +166,9 @@ void basics()
};
auto co = [&step, &co2](async::context& p_ctx) -> async::future<int> {
step = 1; // skipped as the co2 will immediately start
[[maybe_unused]] auto val = co_await co2(p_ctx);
auto const val = co_await co2(p_ctx);
step = 4;
co_return expected_return_value;
co_return val;
};

// Exercise 1
Expand Down Expand Up @@ -198,7 +200,7 @@ void basics()
expect(that % 4 == step);
};

"co_await coroutine"_test = []() {
"co_await coroutine sync"_test = []() {
// Setup
async::inplace_context<1024> ctx;

Expand Down
Loading
Loading