fix queued events dispatched in stale state when anonymous transitions are pending (#542)#671
Merged
kris-jusiak merged 2 commits intoMay 24, 2026
Conversation
1ca8e55 to
bd6dbab
Compare
…s pending (boost-ext#542) process_queued_events previously drained the entire process queue in a single while-loop pass. When actions queued new events via back::process, those events were dispatched immediately inside the same pass without giving the outer event-processing loop a chance to run anonymous (guard-only) transitions between dispatches. Example: state sa [guard] = sb has an anonymous transition that fires after any event in sa satisfies the guard. If an action in sa enqueues a second event, the old code dispatched the second event while still in sa (before the anonymous transition moved to sb), causing guard failures and silent event drops. Fix: change the while-loop to an if-block so process_queued_events dispatches exactly one queued event per call and returns true if there was anything to dispatch. The outer do-while loop already calls process_queued_events repeatedly (see the do-while structure in process_event), so all queued events are still drained -- but now anonymous transitions and defer-queue drains run between each pair of consecutive queued events. BEHAVIOUR CHANGE: Previously all queued events were dispatched atomically before any anonymous transitions fired. After this fix each queued event is followed by a full anonymous-transition + defer-drain cycle. State machines that rely on the old atomic-batch semantics will observe different transition ordering. Fixes boost-ext#542. Regression test added in test/ft/actions_process.cpp.
81c43e3 to
162c65a
Compare
…valid) Upstream master commit e4fdeb2 added a regression test for issue boost-ext#504 that used sm.is<sml::state<sub504>>(sml::X) — sml::state<sub504> is a value expression, not a type, so 'is<T>' rejects it on all compilers. The same bad assertion appears in this branch because dependencies.cpp is inherited from the base commit. Remove the assertion; the action lambda already calls expect(99 == d.val) which is the correct liveness check. (Fixes are tracked by PR boost-ext#675.)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When an action used
back::process<E>to enqueue a new event,process_queued_eventsdrained the entire process queue in a singlewhile-loop pass. The outer event-processing loop never got to run anonymous (guard-only) transitions between consecutive queued-event dispatches.Example: given a machine with
*state<sa> + event<e>[!g] / (++n; enqueue e{count-1}) state<sa> + on_entry<_> / (n = 0) // reset on entry state<sa> [g] = state<sb> // anonymous: leave sa when n>0the first event fires in
saand enqueues a second event. With the old code, the second event is dispatched immediately, still insa, before the anonymous transition tosbruns. The action incrementsnagain, guard!gis now false, and every subsequent queued event is silently dropped.This is the root cause reported in #542. The same bug affects any state machine where the active state changes via an anonymous transition after an action enqueues further events.
Fix
Change the
while-loop inprocess_queued_eventsto anif-block so that each call dispatches exactly one queued event and returnstrueif there was something to dispatch. The outerdo { ... } while (process_queued_events(...))loop already callsprocess_queued_eventsrepeatedly (see the triple-nested do-while structure inprocess_event), so all queued events are still eventually drained—but now anonymous transitions and defer-queue drains run between each pair of consecutive queued events.Previously all queued events were dispatched atomically before any anonymous transitions fired. After this fix, each queued event is followed by a full anonymous-transition + defer-drain cycle. State machines that implicitly relied on the old batch-drain semantics will observe different transition ordering. This is the minimal change required to make the semantics correct, but consumers should be aware.
Test
Regression test added in
test/ft/actions_process.cpp(process_queue_anonymous_transitions_between_queued_events): a four-state cycling machine where each state's on-entry resets a counter used by an anonymous guard. Processinge{5}from the initial state enqueues five more events recursively; with the bug only two fire, with the fix all six fire.Existing tests (including
actions_process_n_defer) pass without modification.Fixes #542.