diff --git a/include/boost/sml.hpp b/include/boost/sml.hpp index c0fed2a8..6b86f283 100644 --- a/include/boost/sml.hpp +++ b/include/boost/sml.hpp @@ -2063,7 +2063,9 @@ struct sm_impl : aux::conditional_t...}; bool wasnt_empty = !process_.empty(); - while (!process_.empty()) { + if (!process_.empty()) { + // Dispatch ONE event per call so the outer loop can run anonymous + // transitions and defer drains between queued events (#542). typename process_t::value_type event{static_cast(process_.front())}; process_.pop(); // pop before dispatch so re-entrant calls see an advanced queue (#465) queued_handled &= (this->*dispatch_table[event.id])(deps, subs, event.data); diff --git a/test/ft/actions_process.cpp b/test/ft/actions_process.cpp index fe335f52..f2d81c09 100644 --- a/test/ft/actions_process.cpp +++ b/test/ft/actions_process.cpp @@ -419,4 +419,64 @@ test process_queue_no_double_pop_on_reentrant_process_event = [] { // With the fix: e2 action fires exactly once. expect(1 == c_.e2_action_count); expect(sm.is(sml::X)); +}; + +// Issue #542: process_queued_events drained the entire process queue in one pass, +// giving anonymous (guard-only) transitions no opportunity to fire between dispatches. +// Events were therefore dispatched in the wrong state, causing guard failures and +// silent drops. Fix: dispatch one queued event per process_queued_events call so the +// outer loop runs anonymous transitions before picking up the next queued event. +// +// c542 is defined at file scope because the fixture uses a dedicated counted event +// type (e542) and four cycling states connected by anonymous guard-only transitions. +struct e542 { + int count; +}; + +struct c542 { + int n = 0; + int fires = 0; + + auto operator()() noexcept { + using namespace sml; + auto g = [this] { return n > 0; }; + auto on_ev = [this](const e542 &ev, sml::back::process proc) { + ++fires; + ++n; + if (ev.count > 0) proc(e542{ev.count - 1}); + }; + auto reset = [this] { n = 0; }; + // clang-format off + return make_transition_table( + *sml::state + event[!g] / on_ev + , sml::state + on_entry<_> / reset + , sml::state [g] = sml::state + + , sml::state + event[!g] / on_ev + , sml::state + on_entry<_> / reset + , sml::state [g] = sml::state + + , sml::state + event[!g] / on_ev + , sml::state + on_entry<_> / reset + , sml::state [g] = sml::state + + , sml::state + event[!g] / on_ev + , sml::state + on_entry<_> / reset + , sml::state [g] = sml::state + ); + // clang-format on + } +}; + +test process_queue_anonymous_transitions_between_queued_events = [] { + sml::sm> sm{}; + c542 &c_ = sm; + + sm.process_event(e542{5}); + + // With the bug: only 2 events fire (all queued events dispatched in the second + // state before the anonymous transition runs, so subsequent events see n>0 + // and the guard !g fails — events silently dropped). + // With the fix: all 6 events fire (initial e542{5} + 5 recursive). + expect(6 == c_.fires); }; \ No newline at end of file diff --git a/test/ft/dependencies.cpp b/test/ft/dependencies.cpp index 1e39bccd..63bc07fe 100644 --- a/test/ft/dependencies.cpp +++ b/test/ft/dependencies.cpp @@ -392,6 +392,7 @@ test ref_dep_copy_from_pool_not_dangling = [] { dep504 dep; sml::sm sm{dep}; sm.process_event(e1{}); + // e2 triggers the check action inside sub504 which calls expect(99 == d.val), + // verifying the dep reference is valid (not dangling). sm.process_event(e2{}); - expect(sm.is>(sml::X)); };