From 8467f419c5496b281849c62c5b1f266f0c18079a Mon Sep 17 00:00:00 2001 From: gabewillen Date: Tue, 24 Feb 2026 10:29:47 -0600 Subject: [PATCH 1/2] Add co_sm utility with policies, tests, and benchmark --- benchmark/simple/CMakeLists.txt | 2 + benchmark/simple/co_sm.cpp | 101 +++++ include/boost/sml/utility/co_sm.hpp | 659 ++++++++++++++++++++++++++++ test/ft/CMakeLists.txt | 3 + test/ft/co_sm.cpp | 321 ++++++++++++++ 5 files changed, 1086 insertions(+) create mode 100644 benchmark/simple/co_sm.cpp create mode 100644 include/boost/sml/utility/co_sm.hpp create mode 100644 test/ft/co_sm.cpp diff --git a/benchmark/simple/CMakeLists.txt b/benchmark/simple/CMakeLists.txt index ee74f45a..9f221312 100644 --- a/benchmark/simple/CMakeLists.txt +++ b/benchmark/simple/CMakeLists.txt @@ -14,3 +14,5 @@ add_example(simple_sc benchmark_simple_sc sc.cpp) if (NOT IS_MSVC_2015) add_example(simple_sml benchmark_simple_sml sml.cpp) endif() + +add_example(simple_co_sm benchmark_simple_co_sm co_sm.cpp) diff --git a/benchmark/simple/co_sm.cpp b/benchmark/simple/co_sm.cpp new file mode 100644 index 00000000..92f2e49e --- /dev/null +++ b/benchmark/simple/co_sm.cpp @@ -0,0 +1,101 @@ +// +// Copyright (c) 2016-2026 Kris Jusiak (kris at jusiak dot net) +// +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// +#include +#include + +#include "benchmark.hpp" + +namespace sml = boost::sml; + +struct play {}; +struct end_pause {}; +struct stop {}; +struct pause {}; +struct open_close {}; +struct cd_detected {}; + +auto start_playback = [] {}; +auto resume_playback = [] {}; +auto close_drawer = [] {}; +auto open_drawer = [] {}; +auto stop_and_open = [] {}; +auto stopped_again = [] {}; +auto store_cd_info = [] {}; +auto pause_playback = [] {}; +auto stop_playback = [] {}; + +struct player { + auto operator()() const noexcept { + using namespace sml; + auto Empty = state; + auto Open = state; + auto Stopped = state; + auto Playing = state; + auto Pause = state; + + // clang-format off + return make_transition_table( + Playing <= Stopped + event / start_playback, + Playing <= Pause + event / resume_playback, + Empty <= Open + event / close_drawer, + Open <= *Empty + event / open_drawer, + Open <= Pause + event / stop_and_open, + Open <= Stopped + event / open_drawer, + Open <= Playing + event / stop_and_open, + Pause <= Playing + event / pause_playback, + Stopped <= Playing + event / stop_playback, + Stopped <= Pause + event / stop_playback, + Stopped <= Empty + event / store_cd_info, + Stopped <= Stopped + event / stopped_again + ); + // clang-format on + } +}; + +int main() { +#if BOOST_SML_UTILITY_CO_SM_ENABLED + using co_sm_t = + sml::utility::co_sm>; + co_sm_t sm{}; + + benchmark_execution_speed([&] { + for (auto i = 0; i < 1'000'000; ++i) { + sm.process_event_async(open_close{}).result(); + sm.process_event_async(open_close{}).result(); + sm.process_event_async(cd_detected{}).result(); + sm.process_event_async(play{}).result(); + sm.process_event_async(pause{}).result(); + sm.process_event_async(end_pause{}).result(); + sm.process_event_async(pause{}).result(); + sm.process_event_async(stop{}).result(); + sm.process_event_async(stop{}).result(); + sm.process_event_async(open_close{}).result(); + sm.process_event_async(open_close{}).result(); + } + }); + benchmark_memory_usage(sm); +#else + sml::sm sm{}; + benchmark_execution_speed([&] { + for (auto i = 0; i < 1'000'000; ++i) { + sm.process_event(open_close{}); + sm.process_event(open_close{}); + sm.process_event(cd_detected{}); + sm.process_event(play{}); + sm.process_event(pause{}); + sm.process_event(end_pause{}); + sm.process_event(pause{}); + sm.process_event(stop{}); + sm.process_event(stop{}); + sm.process_event(open_close{}); + sm.process_event(open_close{}); + } + }); + benchmark_memory_usage(sm); +#endif +} diff --git a/include/boost/sml/utility/co_sm.hpp b/include/boost/sml/utility/co_sm.hpp new file mode 100644 index 00000000..b9c23092 --- /dev/null +++ b/include/boost/sml/utility/co_sm.hpp @@ -0,0 +1,659 @@ +// +// Copyright (c) 2016-2026 Kris Jusiak (kris at jusiak dot net) +// +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// +#ifndef BOOST_SML_UTILITY_CO_SM_HPP +#define BOOST_SML_UTILITY_CO_SM_HPP + +#include "boost/sml.hpp" + +#if defined(_MSVC_LANG) +#define BOOST_SML_UTILITY_CO_SM_LANG _MSVC_LANG +#else +#define BOOST_SML_UTILITY_CO_SM_LANG __cplusplus +#endif + +#if BOOST_SML_UTILITY_CO_SM_LANG >= 202002L && (defined(__cpp_impl_coroutine) || defined(__cpp_coroutines)) +#define BOOST_SML_UTILITY_CO_SM_ENABLED 1 +#else +#define BOOST_SML_UTILITY_CO_SM_ENABLED 0 +#endif + +#if BOOST_SML_UTILITY_CO_SM_ENABLED +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if !BOOST_SML_DISABLE_EXCEPTIONS +#include +#endif + +BOOST_SML_NAMESPACE_BEGIN + +namespace utility { +namespace policy { + +struct inline_scheduler { + static constexpr bool guarantees_fifo = true; + static constexpr bool single_consumer = true; + static constexpr bool run_to_completion = true; + + template + void schedule(TFn&& fn) noexcept(noexcept(std::forward(fn)())) { + std::forward(fn)(); + } +}; + +template +class fifo_scheduler { + public: + static_assert(Capacity > 1, "fifo_scheduler capacity must be greater than 1"); + static_assert((Capacity & (Capacity - 1)) == 0, "fifo_scheduler capacity must be a power of two"); + static_assert(InlineTaskBytes > 0, "fifo_scheduler inline storage must be non-zero"); + + static constexpr bool guarantees_fifo = true; + static constexpr bool single_consumer = true; + static constexpr bool run_to_completion = true; + + fifo_scheduler() = default; + ~fifo_scheduler() { clear(); } + + fifo_scheduler(const fifo_scheduler&) = delete; + fifo_scheduler& operator=(const fifo_scheduler&) = delete; + + fifo_scheduler(fifo_scheduler&&) = delete; + fifo_scheduler& operator=(fifo_scheduler&&) = delete; + + template + bool try_run_immediate(TFn&& fn) noexcept(noexcept(std::forward(fn)())) { + if (draining_ || !empty()) { + return false; + } + + draining_ = true; + std::forward(fn)(); + drain_pending(); + draining_ = false; + return true; + } + + template + void schedule(TFn&& fn) noexcept { + if (try_run_immediate(std::forward(fn))) { + return; + } + + enqueue(std::forward(fn)); + if (draining_) { + return; + } + + // GCOVR_EXCL_START + // Queue non-empty with !draining_ cannot happen under the single-threaded contract. + draining_ = true; + drain_pending(); + draining_ = false; + // GCOVR_EXCL_STOP + } + + private: + struct task_slot { + using invoke_fn = void (*)(void*) noexcept; + using destroy_fn = void (*)(void*) noexcept; + + alignas(std::max_align_t) std::array storage{}; + invoke_fn invoke = nullptr; + destroy_fn destroy = nullptr; + + template + void set(TFn&& fn) noexcept { + using fn_t = typename std::decay::type; + static_assert(sizeof(fn_t) <= InlineTaskBytes, "scheduled task exceeds inline storage capacity"); + static_assert(alignof(fn_t) <= alignof(std::max_align_t), "scheduled task alignment exceeds scheduler storage alignment"); + + new (storage.data()) fn_t(std::forward(fn)); + invoke = [](void* ptr) noexcept { (*static_cast(ptr))(); }; + destroy = [](void* ptr) noexcept { static_cast(ptr)->~fn_t(); }; + } + + void run() noexcept { + invoke(storage.data()); + destroy(storage.data()); + invoke = nullptr; + destroy = nullptr; + } + + void reset() noexcept { // GCOVR_EXCL_LINE + // GCOVR_EXCL_START + if (destroy != nullptr) { + destroy(storage.data()); + } + invoke = nullptr; + destroy = nullptr; + // GCOVR_EXCL_STOP + } // GCOVR_EXCL_LINE + }; + + static constexpr std::size_t next(const std::size_t index) noexcept { return (index + 1) & (Capacity - 1); } + + bool empty() const noexcept { return head_ == tail_; } + bool full() const noexcept { return next(tail_) == head_; } + + template + void enqueue(TFn&& fn) noexcept { + if (full()) { + std::terminate(); // GCOVR_EXCL_LINE + } + + tasks_[tail_].set(std::forward(fn)); + tail_ = next(tail_); + } + + void drain_pending() noexcept { + while (!empty()) { + task_slot& task = tasks_[head_]; + head_ = next(head_); + task.run(); + } + } + + void clear() noexcept { + // GCOVR_EXCL_START + while (!empty()) { + tasks_[head_].reset(); + head_ = next(head_); + } + // GCOVR_EXCL_STOP + tail_ = head_; + draining_ = false; + } + + std::array tasks_{}; + std::size_t head_ = 0; + std::size_t tail_ = 0; + bool draining_ = false; +}; + +template +struct coroutine_scheduler { + using scheduler_type = TScheduler; +}; + +struct heap_coroutine_allocator { + void* allocate(const std::size_t size, const std::size_t alignment) { + return ::operator new(size, std::align_val_t(alignment)); + } + + void deallocate(void* ptr, const std::size_t size, const std::size_t alignment) noexcept { + ::operator delete(ptr, size, std::align_val_t(alignment)); + } +}; + +template +class pooled_coroutine_allocator { + public: + static_assert(SlotSize > 0, "pooled_coroutine_allocator slot size must be non-zero"); + static_assert(SlotCount > 0, "pooled_coroutine_allocator slot count must be non-zero"); + + pooled_coroutine_allocator() noexcept { reset_freelist(); } + + void* allocate(const std::size_t size, const std::size_t alignment) { + if (size <= SlotSize && alignment <= alignof(pool_slot) && free_head_ != invalid_index) { + const std::size_t slot_index = free_head_; + free_head_ = next_free_[slot_index]; + return static_cast(slots_[slot_index].storage.data()); + } + + return ::operator new(size, std::align_val_t(alignment)); + } + + void deallocate(void* ptr, const std::size_t size, const std::size_t alignment) noexcept { + if (ptr == nullptr) { + return; + } + + if (size <= SlotSize && alignment <= alignof(pool_slot) && is_pool_pointer(ptr)) { + const std::size_t slot_index = slot_index_for(ptr); + next_free_[slot_index] = free_head_; + free_head_ = slot_index; + return; + } + + ::operator delete(ptr, size, std::align_val_t(alignment)); + } + + private: + static constexpr std::size_t invalid_index = SlotCount; + + struct pool_slot { + alignas(std::max_align_t) std::array storage{}; + }; + + bool is_pool_pointer(void* ptr) const noexcept { + const auto* begin = reinterpret_cast(slots_.data()); + const auto* end = begin + sizeof(slots_); + const auto* candidate = static_cast(ptr); + if (candidate < begin || candidate >= end) { + return false; + } + + const std::size_t offset = static_cast(candidate - begin); + return (offset % sizeof(pool_slot)) == 0; + } + + std::size_t slot_index_for(void* ptr) const noexcept { + const auto* begin = reinterpret_cast(slots_.data()); + const auto* candidate = static_cast(ptr); + const std::size_t offset = static_cast(candidate - begin); + return offset / sizeof(pool_slot); + } + + void reset_freelist() noexcept { + for (std::size_t i = 0; i + 1 < SlotCount; ++i) { + next_free_[i] = i + 1; + } + next_free_[SlotCount - 1] = invalid_index; + free_head_ = 0; + } + + std::array slots_{}; + std::array next_free_{}; + std::size_t free_head_ = 0; +}; + +template +struct coroutine_allocator { + using allocator_type = TAllocator; +}; + +template +concept valid_coroutine_scheduler_policy = requires { typename TSchedulerPolicy::scheduler_type; }; + +template +concept valid_coroutine_scheduler = requires(TScheduler scheduler, void (*fn_ptr)()) { scheduler.schedule(fn_ptr); }; + +template +concept strict_ordering_scheduler_contract = + requires { + { TScheduler::guarantees_fifo } -> std::convertible_to; + { TScheduler::single_consumer } -> std::convertible_to; + { TScheduler::run_to_completion } -> std::convertible_to; + } && static_cast(TScheduler::guarantees_fifo) && static_cast(TScheduler::single_consumer) && + static_cast(TScheduler::run_to_completion); + +template +concept has_try_run_immediate = requires(TScheduler scheduler) { + { + scheduler.try_run_immediate(+[]() noexcept {}) + } -> std::same_as; +}; + +template +concept valid_coroutine_allocator_policy = requires { typename TAllocatorPolicy::allocator_type; }; + +template +concept valid_coroutine_allocator = requires(TAllocator allocator, void* ptr, std::size_t size, std::size_t alignment) { + { allocator.allocate(size, alignment) } -> std::same_as; + { allocator.deallocate(ptr, size, alignment) } noexcept; +}; + +} // namespace policy + +class bool_task { + public: + struct promise_type; + using handle_type = std::coroutine_handle; + + struct promise_type { + using allocate_fn = void* (*)(void*, std::size_t, std::size_t); + using deallocate_fn = void (*)(void*, void*, std::size_t, std::size_t) noexcept; + + struct frame_header { + void* allocator_ctx = nullptr; + deallocate_fn deallocate = nullptr; + void* allocation_ptr = nullptr; + std::size_t allocation_size = 0; + std::size_t allocation_alignment = 0; + }; + + static void* allocate_frame(const std::size_t frame_size, void* allocator_ctx, allocate_fn allocate, + deallocate_fn deallocate) { + constexpr std::size_t frame_align = alignof(promise_type); + constexpr std::size_t header_align = alignof(frame_header); + constexpr std::size_t alloc_align = frame_align > header_align ? frame_align : header_align; + + const std::size_t allocation_size = frame_size + sizeof(frame_header) + alloc_align - 1; + void* raw = allocate(allocator_ctx, allocation_size, alloc_align); + if (raw == nullptr) { +#if !BOOST_SML_DISABLE_EXCEPTIONS + throw std::bad_alloc(); +#else + std::terminate(); +#endif + } + + void* aligned_frame = static_cast(raw) + sizeof(frame_header); + std::size_t space = allocation_size - sizeof(frame_header); + // GCOVR_EXCL_START + if (std::align(frame_align, frame_size, aligned_frame, space) == nullptr) { + deallocate(allocator_ctx, raw, allocation_size, alloc_align); +#if !BOOST_SML_DISABLE_EXCEPTIONS + throw std::bad_alloc(); +#else + std::terminate(); +#endif + } + // GCOVR_EXCL_STOP + + auto* header = reinterpret_cast(static_cast(aligned_frame) - sizeof(frame_header)); + header->allocator_ctx = allocator_ctx; + header->deallocate = deallocate; + header->allocation_ptr = raw; + header->allocation_size = allocation_size; + header->allocation_alignment = alloc_align; + return aligned_frame; + } + + template + static void* allocate_frame_with_allocator(std::size_t frame_size, TAllocator& allocator) { + static_assert(policy::valid_coroutine_allocator, + "coroutine allocator must provide allocate(size, alignment) and noexcept deallocate(ptr, size, alignment)"); + + return allocate_frame( + frame_size, &allocator, + [](void* ctx, const std::size_t size, const std::size_t alignment) -> void* { + return static_cast(ctx)->allocate(size, alignment); + }, + [](void* ctx, void* ptr, const std::size_t size, const std::size_t alignment) noexcept { + static_cast(ctx)->deallocate(ptr, size, alignment); + }); + } + + static void deallocate_frame(void* frame_ptr) noexcept { + if (frame_ptr == nullptr) { + return; + } + + auto* header = reinterpret_cast(static_cast(frame_ptr) - sizeof(frame_header)); + header->deallocate(header->allocator_ctx, header->allocation_ptr, header->allocation_size, header->allocation_alignment); + } + + static void* operator new(std::size_t frame_size) { + return allocate_frame( + frame_size, nullptr, + [](void*, const std::size_t size, const std::size_t alignment) -> void* { + return ::operator new(size, std::align_val_t(alignment)); + }, + [](void*, void* ptr, const std::size_t size, const std::size_t alignment) noexcept { + ::operator delete(ptr, size, std::align_val_t(alignment)); + }); + } + + template + static void* operator new(std::size_t frame_size, std::allocator_arg_t, TAllocator& allocator, TArgs&&...) { + return allocate_frame_with_allocator(frame_size, allocator); + } + + static void operator delete(void* frame_ptr) noexcept { deallocate_frame(frame_ptr); } + static void operator delete(void* frame_ptr, std::size_t) noexcept { deallocate_frame(frame_ptr); } + + template + static void operator delete(void* frame_ptr, std::allocator_arg_t, TAllocator&, TArgs&&...) noexcept { + deallocate_frame(frame_ptr); + } + + bool value = false; + std::exception_ptr exception = nullptr; + std::coroutine_handle<> continuation = std::noop_coroutine(); + + bool_task get_return_object() noexcept { return bool_task{handle_type::from_promise(*this)}; } + std::suspend_never initial_suspend() noexcept { return {}; } + + auto final_suspend() noexcept { + struct continuation_awaiter { + bool await_ready() const noexcept { return false; } + std::coroutine_handle<> await_suspend(handle_type h) const noexcept { return h.promise().continuation; } + void await_resume() const noexcept {} + }; + return continuation_awaiter{}; + } + + void return_value(const bool v) noexcept { value = v; } + void unhandled_exception() noexcept { exception = std::current_exception(); } + }; + + bool_task() = default; + explicit bool_task(handle_type handle) noexcept : handle_(handle) {} + + static bool_task from_value(const bool value) noexcept { + bool_task task{}; + task.has_immediate_value_ = true; + task.immediate_value_ = value; + return task; + } + + ~bool_task() { + if (handle_) { + handle_.destroy(); + } + } + + bool_task(const bool_task&) = delete; + bool_task& operator=(const bool_task&) = delete; + + bool_task(bool_task&& other) noexcept + : handle_(std::exchange(other.handle_, {})), + has_immediate_value_(other.has_immediate_value_), + immediate_value_(other.immediate_value_) { + other.has_immediate_value_ = false; + other.immediate_value_ = false; + } + + bool_task& operator=(bool_task&& other) noexcept { + if (this == &other) { + return *this; + } + + if (handle_) { + handle_.destroy(); + } + handle_ = std::exchange(other.handle_, {}); + has_immediate_value_ = other.has_immediate_value_; + immediate_value_ = other.immediate_value_; + other.has_immediate_value_ = false; + other.immediate_value_ = false; + return *this; + } + + bool await_ready() const noexcept { + if (has_immediate_value_) { + return true; + } + return (handle_ == nullptr) || handle_.done(); + } + + std::coroutine_handle<> await_suspend(std::coroutine_handle<> awaiting) noexcept { + if (handle_ == nullptr) { + return std::noop_coroutine(); + } + handle_.promise().continuation = awaiting; + return handle_; + } + + bool await_resume() { return result(); } + + bool result() { + if (has_immediate_value_) { + return immediate_value_; + } + if (handle_ == nullptr) { + return false; + } + if (!handle_.done()) { +#if !BOOST_SML_DISABLE_EXCEPTIONS + throw std::logic_error("bool_task result() called before coroutine completion"); +#else + std::terminate(); +#endif + } + if (handle_.promise().exception) { +#if !BOOST_SML_DISABLE_EXCEPTIONS + std::rethrow_exception(handle_.promise().exception); +#else + std::terminate(); +#endif + } + return handle_.promise().value; + } + + private: + handle_type handle_{}; + bool has_immediate_value_ = false; + bool immediate_value_ = false; +}; + +template >, + class TAllocatorPolicy = policy::coroutine_allocator>, class... TPolicies> +class co_sm { + public: + static_assert(policy::valid_coroutine_scheduler_policy, "scheduler_policy must define scheduler_type"); + static_assert(policy::valid_coroutine_allocator_policy, "allocator_policy must define allocator_type"); + + using model_type = T; + using scheduler_policy_type = TSchedulerPolicy; + using scheduler_type = typename scheduler_policy_type::scheduler_type; + using allocator_policy_type = TAllocatorPolicy; + using allocator_type = typename allocator_policy_type::allocator_type; + using state_machine_type = sm; + + static_assert(policy::valid_coroutine_scheduler, "scheduler_type must provide schedule(fn)"); + static_assert(policy::strict_ordering_scheduler_contract, + "scheduler_type must guarantee FIFO ordering, single-consumer dispatch, and run-to-completion"); + static_assert(policy::valid_coroutine_allocator, + "allocator_type must provide allocate(size, alignment) and noexcept deallocate(ptr, size, alignment)"); + + co_sm() = default; + ~co_sm() = default; + + co_sm(const co_sm&) = default; + co_sm(co_sm&&) = default; + co_sm& operator=(const co_sm&) = default; + co_sm& operator=(co_sm&&) = default; + + explicit co_sm(const scheduler_type& scheduler) : scheduler_(scheduler) {} + explicit co_sm(const allocator_type& allocator) : allocator_(allocator) {} + + co_sm(const scheduler_type& scheduler, const allocator_type& allocator) : scheduler_(scheduler), allocator_(allocator) {} + + template + explicit co_sm(TArgs&&... args) : state_machine_(std::forward(args)...) {} + + template + co_sm(const scheduler_type& scheduler, TArgs&&... args) + : state_machine_(std::forward(args)...), scheduler_(scheduler) {} + + template + co_sm(const allocator_type& allocator, TArgs&&... args) + : state_machine_(std::forward(args)...), allocator_(allocator) {} + + template + co_sm(const scheduler_type& scheduler, const allocator_type& allocator, TArgs&&... args) + : state_machine_(std::forward(args)...), scheduler_(scheduler), allocator_(allocator) {} + + template + bool process_event(const TEvent& event) { + return state_machine_.process_event(event); + } + + template + bool_task process_event_async(TEvent&& event) { + using event_t = typename std::decay::type; + event_t event_copy(static_cast(event)); + + if constexpr (std::is_same_v) { + return bool_task::from_value(state_machine_.process_event(event_copy)); + } + + if constexpr (policy::has_try_run_immediate) { + bool accepted = false; + if (scheduler_.try_run_immediate( + [this, &event_copy, &accepted]() { accepted = state_machine_.process_event(event_copy); })) { + return bool_task::from_value(accepted); + } + } + + return process_event_async_impl(std::allocator_arg, allocator_, *this, static_cast(event_copy)); + } + + template + bool is(const TState& state = TState{}) const { + return state_machine_.is(state); + } + + template + void visit_current_states(TVisitor&& visitor) { + state_machine_.visit_current_states(std::forward(visitor)); + } + + scheduler_type& scheduler() noexcept { return scheduler_; } + const scheduler_type& scheduler() const noexcept { return scheduler_; } + allocator_type& allocator() noexcept { return allocator_; } + const allocator_type& allocator() const noexcept { return allocator_; } + + protected: + state_machine_type& raw_sm() { return state_machine_; } + const state_machine_type& raw_sm() const { return state_machine_; } + + private: + template + static bool_task process_event_async_impl(std::allocator_arg_t, allocator_type& allocator, co_sm& self, TEvent&& event) { + (void)allocator; + co_return co_await process_event_awaitable::type>{self, static_cast(event)}; + } // GCOVR_EXCL_LINE + + template + struct process_event_awaitable { + co_sm& self; + TEvent event_value; + bool accepted = false; + + bool await_ready() noexcept { + if constexpr (std::is_same_v) { + accepted = self.state_machine_.process_event(event_value); + return true; + } + return false; + } + + void await_suspend(std::coroutine_handle<> handle) { + self.scheduler_.schedule([this, handle]() mutable { + accepted = self.state_machine_.process_event(event_value); + handle.resume(); + }); + } + + bool await_resume() const noexcept { return accepted; } + }; + + state_machine_type state_machine_{}; + scheduler_type scheduler_{}; + allocator_type allocator_{}; +}; + +} // namespace utility + +BOOST_SML_NAMESPACE_END +#endif + +#undef BOOST_SML_UTILITY_CO_SM_LANG + +#endif // BOOST_SML_UTILITY_CO_SM_HPP diff --git a/test/ft/CMakeLists.txt b/test/ft/CMakeLists.txt index 6c1750b9..1fa75978 100644 --- a/test/ft/CMakeLists.txt +++ b/test/ft/CMakeLists.txt @@ -16,6 +16,9 @@ add_test(test_actions_process_n_defer test_actions_process_n_defer) add_executable(test_composite composite.cpp) add_test(test_composite test_composite) +add_executable(test_co_sm co_sm.cpp) +add_test(test_co_sm test_co_sm) + add_executable(test_constexpr constexpr.cpp) add_test(test_constexpr test_constexpr) diff --git a/test/ft/co_sm.cpp b/test/ft/co_sm.cpp new file mode 100644 index 00000000..239ad52a --- /dev/null +++ b/test/ft/co_sm.cpp @@ -0,0 +1,321 @@ +// +// Copyright (c) 2016-2026 Kris Jusiak (kris at jusiak dot net) +// +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// +#include +#include +#include +#include +#include +#include +#include + +namespace sml = boost::sml; + +#if BOOST_SML_UTILITY_CO_SM_ENABLED +namespace utility = sml::utility; + +struct e1 {}; +const auto idle = sml::state; +const auto s1 = sml::state; + +struct c { + auto operator()() const { + using namespace sml; + // clang-format off + return make_transition_table( + *idle + event = s1 + ); + // clang-format on + } +}; + +test inline_scheduler_runs_immediately = [] { + utility::policy::inline_scheduler scheduler{}; + int calls = 0; + scheduler.schedule([&calls] { ++calls; }); + expect(1 == calls); +}; + +test fifo_scheduler_preserves_fifo_order = [] { + utility::policy::fifo_scheduler<8, 64> scheduler{}; + int order = 0; + bool nested_immediate = true; + + const bool immediate = scheduler.try_run_immediate([&] { + order = (order * 10) + 1; + scheduler.schedule([&] { order = (order * 10) + 2; }); + nested_immediate = scheduler.try_run_immediate([&] {}); + scheduler.schedule([&] { order = (order * 10) + 3; }); + }); + + expect(immediate); + expect(!nested_immediate); + expect(123 == order); +}; + +test fifo_scheduler_schedule_runs_inline_when_idle = [] { + utility::policy::fifo_scheduler<8, 64> scheduler{}; + int calls = 0; + scheduler.schedule([&calls] { ++calls; }); + expect(1 == calls); +}; + +test pooled_allocator_pool_and_heap_paths = [] { + utility::policy::pooled_coroutine_allocator<64, 2> allocator{}; + void* p1 = allocator.allocate(32, alignof(std::max_align_t)); + void* p2 = allocator.allocate(32, alignof(std::max_align_t)); + void* p3 = allocator.allocate(128, alignof(std::max_align_t)); + + expect(nullptr != p1); + expect(nullptr != p2); + expect(nullptr != p3); + + allocator.deallocate(p1, 32, alignof(std::max_align_t)); + allocator.deallocate(p2, 32, alignof(std::max_align_t)); + allocator.deallocate(p3, 128, alignof(std::max_align_t)); + + void* external_ptr = ::operator new(32, std::align_val_t(alignof(std::max_align_t))); + allocator.deallocate(external_ptr, 32, alignof(std::max_align_t)); + + allocator.deallocate(nullptr, 0, alignof(std::max_align_t)); +}; + +test bool_task_default_and_immediate_paths = [] { + utility::bool_task empty{}; + expect(empty.await_ready()); + expect(empty.await_suspend(std::noop_coroutine()) == std::noop_coroutine()); + expect(!empty.result()); + + auto immediate = utility::bool_task::from_value(true); + expect(immediate.await_ready()); + expect(immediate.await_resume()); +}; + +test bool_task_move_paths = [] { + auto src = utility::bool_task::from_value(true); + utility::bool_task dst = std::move(src); + expect(dst.result()); + + auto src2 = utility::bool_task::from_value(false); + dst = std::move(src2); + expect(!dst.result()); + + auto as_rvalue = [](utility::bool_task& task) -> utility::bool_task&& { return static_cast(task); }; + dst = as_rvalue(dst); + expect(!dst.result()); +}; + +utility::bool_task ready_bool_task(const bool value) { co_return value; } + +test bool_task_move_assignment_releases_existing_coroutine_handle = [] { + auto has_handle = ready_bool_task(true); + auto replacement = utility::bool_task::from_value(false); + has_handle = std::move(replacement); + expect(!has_handle.result()); +}; + +test bool_task_promise_helpers = [] { + utility::bool_task::promise_type promise{}; + promise.return_value(true); + expect(promise.value); + promise.final_suspend().await_resume(); + utility::bool_task::promise_type::deallocate_frame(nullptr); +}; + +test co_sm_inline_scheduler_path = [] { + using sm_t = utility::co_sm>; + + sm_t sm{}; + auto task = sm.process_event_async(e1{}); + + expect(task.await_ready()); + expect(task.result()); + expect(sm.is(s1)); +}; + +test co_sm_sync_process_event_and_visitor = [] { + utility::co_sm sm{}; + expect(sm.is(idle)); + expect(sm.process_event(e1{})); + expect(sm.is(s1)); + + int visits = 0; + sm.visit_current_states([&visits](auto) { ++visits; }); + expect(1 == visits); +}; + +struct scheduler_without_try_run_immediate { + static constexpr bool guarantees_fifo = true; + static constexpr bool single_consumer = true; + static constexpr bool run_to_completion = true; + + template + void schedule(TFn&& fn) { + std::forward(fn)(); + } +}; + +struct counting_allocator { + std::size_t allocate_calls = 0; + std::size_t deallocate_calls = 0; + + void* allocate(const std::size_t size, const std::size_t alignment) { + ++allocate_calls; + return ::operator new(size, std::align_val_t(alignment)); + } + + void deallocate(void* ptr, const std::size_t size, const std::size_t alignment) noexcept { + ++deallocate_calls; + ::operator delete(ptr, size, std::align_val_t(alignment)); + } +}; + +test co_sm_coroutine_path_uses_allocator = [] { + using sm_t = utility::co_sm, + utility::policy::coroutine_allocator>; + + sm_t sm{}; + + { + auto task = sm.process_event_async(e1{}); + expect(task.result()); + } + + expect(1u == sm.allocator().allocate_calls); + expect(1u == sm.allocator().deallocate_calls); + expect(sm.is(s1)); +}; + +test co_sm_default_fifo_try_run_immediate_path = [] { + using sm_t = utility::co_sm>, + utility::policy::coroutine_allocator>; + + sm_t sm{}; + auto task = sm.process_event_async(e1{}); + + expect(task.await_ready()); + expect(task.result()); + expect(0u == sm.allocator().allocate_calls); + expect(0u == sm.allocator().deallocate_calls); + expect(sm.is(s1)); +}; + +struct deferred_scheduler { + static constexpr bool guarantees_fifo = true; + static constexpr bool single_consumer = true; + static constexpr bool run_to_completion = true; + + std::function pending{}; + + template + void schedule(TFn&& fn) { + pending = std::forward(fn); + } + + void run_pending() { + auto task = std::move(pending); + pending = {}; + if (task) { + task(); + } + } +}; + +test co_sm_deferred_scheduler_not_done_then_done = [] { + using sm_t = utility::co_sm, + utility::policy::coroutine_allocator>; + + sm_t sm{}; + auto task = sm.process_event_async(e1{}); + + expect(!task.await_ready()); + +#if !BOOST_SML_DISABLE_EXCEPTIONS + bool got_logic_error = false; + try { + (void)task.result(); + } catch (const std::logic_error&) { + got_logic_error = true; + } + expect(got_logic_error); +#endif + + sm.scheduler().run_pending(); + expect(task.result()); + expect(sm.is(s1)); +}; + +utility::bool_task throwing_bool_task() { + throw std::runtime_error("test"); + co_return true; +} + +test bool_task_exception_rethrow = [] { +#if !BOOST_SML_DISABLE_EXCEPTIONS + auto task = throwing_bool_task(); + bool got_runtime_error = false; + try { + (void)task.result(); + } catch (const std::runtime_error&) { + got_runtime_error = true; + } + expect(got_runtime_error); +#endif +}; + +test bool_task_await_suspend_with_non_empty_handle = [] { + using sm_t = utility::co_sm, + utility::policy::coroutine_allocator>; + + sm_t sm{}; + auto task = sm.process_event_async(e1{}); + const auto resumed_handle = task.await_suspend(std::noop_coroutine()); + expect(resumed_handle != std::noop_coroutine()); + sm.scheduler().run_pending(); + expect(task.result()); + expect(sm.is(s1)); +}; + +test bool_task_promise_unhandled_exception_sets_ptr = [] { +#if !BOOST_SML_DISABLE_EXCEPTIONS + utility::bool_task::promise_type promise{}; + try { + throw std::runtime_error("test"); + } catch (...) { + promise.unhandled_exception(); + } + expect(promise.exception != nullptr); +#endif +}; + +struct null_allocator { + void* allocate(std::size_t, std::size_t) { return nullptr; } + void deallocate(void*, std::size_t, std::size_t) noexcept {} +}; + +test co_sm_allocator_failure_throws_bad_alloc = [] { +#if !BOOST_SML_DISABLE_EXCEPTIONS + using sm_t = utility::co_sm, + utility::policy::coroutine_allocator>; + + sm_t sm{}; + bool got_bad_alloc = false; + try { + auto task = sm.process_event_async(e1{}); + (void)task; + } catch (const std::bad_alloc&) { + got_bad_alloc = true; + } + expect(got_bad_alloc); +#endif +}; + +#else + +test co_sm_disabled_without_coroutines = [] { expect(true); }; + +#endif From bf61d2acc199893652eab6c52a12cbc09d1afc3a Mon Sep 17 00:00:00 2001 From: gabewillen Date: Tue, 24 Feb 2026 10:48:11 -0600 Subject: [PATCH 2/2] Fix co_sm CI portability and review findings --- benchmark/simple/CMakeLists.txt | 3 +-- include/boost/sml/utility/co_sm.hpp | 33 ++++++++++++++++++++++++----- test/ft/co_sm.cpp | 3 ++- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/benchmark/simple/CMakeLists.txt b/benchmark/simple/CMakeLists.txt index 9f221312..406fa97c 100644 --- a/benchmark/simple/CMakeLists.txt +++ b/benchmark/simple/CMakeLists.txt @@ -13,6 +13,5 @@ add_example(simple_sc benchmark_simple_sc sc.cpp) if (NOT IS_MSVC_2015) add_example(simple_sml benchmark_simple_sml sml.cpp) + add_example(simple_co_sm benchmark_simple_co_sm co_sm.cpp) endif() - -add_example(simple_co_sm benchmark_simple_co_sm co_sm.cpp) diff --git a/include/boost/sml/utility/co_sm.hpp b/include/boost/sml/utility/co_sm.hpp index b9c23092..65ddc486 100644 --- a/include/boost/sml/utility/co_sm.hpp +++ b/include/boost/sml/utility/co_sm.hpp @@ -80,9 +80,13 @@ class fifo_scheduler { } draining_ = true; + struct draining_reset { + fifo_scheduler* self; + ~draining_reset() noexcept { self->draining_ = false; } + } reset{this}; + std::forward(fn)(); drain_pending(); - draining_ = false; return true; } @@ -194,7 +198,8 @@ struct heap_coroutine_allocator { } void deallocate(void* ptr, const std::size_t size, const std::size_t alignment) noexcept { - ::operator delete(ptr, size, std::align_val_t(alignment)); + (void)size; + ::operator delete(ptr, std::align_val_t(alignment)); } }; @@ -228,7 +233,8 @@ class pooled_coroutine_allocator { return; } - ::operator delete(ptr, size, std::align_val_t(alignment)); + (void)size; + ::operator delete(ptr, std::align_val_t(alignment)); } private: @@ -394,7 +400,8 @@ class bool_task { return ::operator new(size, std::align_val_t(alignment)); }, [](void*, void* ptr, const std::size_t size, const std::size_t alignment) noexcept { - ::operator delete(ptr, size, std::align_val_t(alignment)); + (void)size; + ::operator delete(ptr, std::align_val_t(alignment)); }); } @@ -625,6 +632,7 @@ class co_sm { co_sm& self; TEvent event_value; bool accepted = false; + std::exception_ptr exception{}; bool await_ready() noexcept { if constexpr (std::is_same_v) { @@ -636,12 +644,27 @@ class co_sm { void await_suspend(std::coroutine_handle<> handle) { self.scheduler_.schedule([this, handle]() mutable { +#if !BOOST_SML_DISABLE_EXCEPTIONS + try { + accepted = self.state_machine_.process_event(event_value); + } catch (...) { + exception = std::current_exception(); + } +#else accepted = self.state_machine_.process_event(event_value); +#endif handle.resume(); }); } - bool await_resume() const noexcept { return accepted; } + bool await_resume() const { +#if !BOOST_SML_DISABLE_EXCEPTIONS + if (exception) { + std::rethrow_exception(exception); + } +#endif + return accepted; + } }; state_machine_type state_machine_{}; diff --git a/test/ft/co_sm.cpp b/test/ft/co_sm.cpp index 239ad52a..818ff341 100644 --- a/test/ft/co_sm.cpp +++ b/test/ft/co_sm.cpp @@ -170,7 +170,8 @@ struct counting_allocator { void deallocate(void* ptr, const std::size_t size, const std::size_t alignment) noexcept { ++deallocate_calls; - ::operator delete(ptr, size, std::align_val_t(alignment)); + (void)size; + ::operator delete(ptr, std::align_val_t(alignment)); } };