Skip to content

fix(#622): any-state wildcard fires when composable sub-SM terminates#702

Open
PavelGuzenfeld wants to merge 1 commit into
boost-ext:masterfrom
PavelGuzenfeld:fix/issue-622-any-state-nested
Open

fix(#622): any-state wildcard fires when composable sub-SM terminates#702
PavelGuzenfeld wants to merge 1 commit into
boost-ext:masterfrom
PavelGuzenfeld:fix/issue-622-any-state-nested

Conversation

@PavelGuzenfeld
Copy link
Copy Markdown
Contributor

Problem

When the outer SM is in a composable sub-state (a struct with public operator(), treated as sm<T>) and the sub-SM handles an event that terminates all its regions (reaches X), the outer wildcard transition (any + event<E> = state<S>) was never evaluated.

Root cause: get_state_mapping<sm<T>>::type::execute() short-circuits via || when sub_sm_mapping::execute() returns true — so fallback_event_mapping (the outer _ wildcard) is never tried, even after the sub-SM has terminated.

The author of #622 noted that the same SM declared as class (private operator(), not composable) worked correctly, because non-composable states go through the primary get_state_mapping template which always tries both state_mapping and any_state_mapping.

Fix

In get_state_mapping<sm<T>, TMappings, TUnexpected>::type::execute():

// Before
return sub_sm_mapping::execute(...) || fallback_event_mapping::execute(...);

// After
const auto sub_handled = sub_sm_mapping::execute(...);
if (sub_handled && !sub_sm<sm_impl<T>>::get(&subs).is_terminated()) {
  return true;  // sub-SM handled event and is still running
}
// sub-SM terminated or didn't handle — give outer wildcard a chance
return fallback_event_mapping::execute(...) || sub_handled;

get_id_safe<R,T> is added as a variadic-fallback variant of get_id that returns static_cast<R>(-1) when T is not in the state list. This makes is_terminated_impl safe to call on sub-SMs that have no X-terminating transitions (previously it would fail to compile for them). The value -1 can never equal a valid (non-negative) state ID, so is_terminated() correctly returns false for such SMs.

Test

test/ft/issue_622_any_state_nested.cpp:

  • any_state_fires_after_nested_sub_sm_terminates: outer SM in composable sub_a; e1 terminates the sub-SM and the outer wildcard any + e1 = state_b must fire → outer SM in state_b.
  • any_state_leaf_baseline: wildcard for non-composable (leaf) states works as before.

Verification

Toolchain Standard Result
GCC 14 (Docker) C++14 33/33 (pre-existing C++14 static_assert failure unrelated to this PR, tracked by #698)
GCC 14 (Docker) C++17 33/33 ✅
GCC 14 (Docker) C++20 33/33 ✅
MSVC 19.51 (Wine) C++20 compiles cleanly ✅

Closes #622.

@PavelGuzenfeld PavelGuzenfeld force-pushed the fix/issue-622-any-state-nested branch from 8bd1db5 to 49b5284 Compare May 29, 2026 10:07
…SM terminates

When the outer SM is in a composable sub-state (a struct with public
operator(), treated as sm<T>), and the sub-SM handles an event that
terminates it (all regions reach X), the outer wildcard transition
(`any + event<E> = state<S>`) was never evaluated because the
sub-SM's `true` return short-circuited the logical OR.

Fix: in `get_state_mapping<sm<T>>::type::execute()`, after the sub-SM
handles an event, check `is_terminated()`. If the sub-SM is still
running, return immediately. If it has terminated (or never handled the
event), give `fallback_event_mapping` (the outer `_` wildcard) a
chance to fire.

`is_terminated_impl` is updated to use a new `aux::get_id_safe<R,T>`
helper that returns -1 (never a valid state ID) when T is not in the
SM's state list, making `is_terminated()` safe to call on sub-SMs
that have no X-terminating transitions.

Adds `test/ft/issue_622_any_state_nested.cpp` with two test cases:
- composable sub-SM terminates via e1; outer `any + e1 = state_b` must fire
- leaf-only baseline (wildcard always worked for non-composable states)
@PavelGuzenfeld PavelGuzenfeld force-pushed the fix/issue-622-any-state-nested branch from 49b5284 to 050d780 Compare May 29, 2026 12:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Any state doesn't work correctly with nested sub-states

1 participant