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
35 changes: 30 additions & 5 deletions include/boost/sml.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,10 @@ template <class R, class T, int N>
constexpr R get_id(type_id_type<N, T> *) {
return static_cast<R>(N);
}
template <class T, int N>
constexpr true_type has_type_id(type_id_type<N, T> *);
template <class T>
constexpr false_type has_type_id(...);
Comment on lines +767 to +769

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new has_type_id functions (lines 767-769) are added but only declared, not defined. The pointer overload should return true_type but there's no function body. While this works for SFINAE and decltype contexts (as used on line 2113), the lack of definition could cause linker errors if someone tries to actually call these functions. Consider adding = delete or {} bodies to make the intent clearer and prevent accidental ODR violations.

Suggested change
constexpr true_type has_type_id(type_id_type<N, T> *);
template <class T>
constexpr false_type has_type_id(...);
constexpr true_type has_type_id(type_id_type<N, T> *) {
return {};
}
template <class T>
constexpr false_type has_type_id(...) {
return {};
}

Copilot uses AI. Check for mistakes.
template <template <class...> class, class T>
struct is : false_type {};
template <template <class...> class T, class... Ts>
Expand Down Expand Up @@ -1299,6 +1303,15 @@ struct transitions<aux::false_type> {
return false;
}
};
template <class TEvent, class...>
struct has_terminate_parent_transition : aux::false_type {};
template <class TEvent, class TTransition, class... TRest>
struct has_terminate_parent_transition<TEvent, TTransition, TRest...>
: aux::conditional<
aux::is_same<terminate_state, typename TTransition::dst_state>::value &&
aux::is_same<TEvent, typename TTransition::event>::value,
aux::true_type,
has_terminate_parent_transition<TEvent, TRest...>>::type {};
template <class Tsm, class T, class... Ts>
struct transitions_sub<sm<Tsm>, T, Ts...> {
template <class TEvent, class SM, class TDeps, class TSubs>
Expand All @@ -1314,7 +1327,14 @@ struct transitions_sub<sm<Tsm>, T, Ts...> {
}
template <class TEvent, class SM, class TDeps, class TSubs>
constexpr static bool execute_impl(const TEvent &event, SM &sm, TDeps &deps, TSubs &subs, typename SM::state_t &current_state) {
const auto handled = sub_sm<sm_impl<Tsm>>::get(&subs).process_event(event, deps, subs);
auto &sub = sub_sm<sm_impl<Tsm>>::get(&subs);
const auto handled = sub.process_event(event, deps, subs);

if (handled && sub.is_terminated() && has_terminate_parent_transition<TEvent, T, Ts...>::value) {
const auto propagated = transitions<T, Ts...>::execute(event, sm, deps, subs, current_state);
return propagated ? propagated : handled;
}
Comment on lines +1333 to +1336

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition on line 1333 checks both handled && sub.is_terminated() and has_terminate_parent_transition. However, the propagation logic on line 1334 returns propagated ? propagated : handled. This means if the parent transition doesn't handle the event (propagated is false), we still return true (handled). This could mask bugs where an event is handled by the child but should also trigger parent behavior. Consider whether this is the intended semantics or if it should be propagated || handled.

Copilot uses AI. Check for mistakes.

return handled ? handled : transitions<T, Ts...>::execute(event, sm, deps, subs, current_state);
}
template <class _, class TEvent, class SM, class TDeps, class TSubs>
Expand Down Expand Up @@ -2088,12 +2108,17 @@ struct sm_impl : aux::conditional_t<aux::should_not_subclass_statemachine_class<
constexpr static void visit_state(const TVisitor &visitor) {
visitor(aux::string<TState>{});
}
constexpr bool is_terminated() const { return is_terminated_impl(aux::make_index_sequence<regions>{}); }
constexpr bool is_terminated_impl(aux::index_sequence<0>) const {
return current_state_[0] == aux::get_id<state_t, terminate_state>((states_ids_t *)0);
constexpr bool is_terminated() const {
return is_terminated_impl(aux::make_index_sequence<regions>{},
decltype(aux::has_type_id<terminate_state>((states_ids_t *)0)){});
}
template <int... Ns>
constexpr bool is_terminated_impl(aux::index_sequence<Ns...>, aux::false_type) const {
(void)sizeof...(Ns);
return false;
}
template <int... Ns>
constexpr bool is_terminated_impl(aux::index_sequence<Ns...>) const {
constexpr bool is_terminated_impl(aux::index_sequence<Ns...>, aux::true_type) const {
#if defined(__cpp_fold_expressions)
return ((current_state_[Ns] == aux::get_id<state_t, terminate_state>((states_ids_t *)0)) && ...);
#else
Expand Down
9 changes: 8 additions & 1 deletion include/boost/sml/utility/dispatch_table.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,14 @@ auto make_dispatch_table(SM &fsm, const aux::index_sequence<Ns...> &) {
using dispatch_table_t = bool (*)(SM &, const TEvent &);
const static dispatch_table_t dispatch_table[sizeof...(Ns) ? sizeof...(Ns) : 1] = {
&get_event_t<Ns + EventRangeBegin, events_ids_t>::template execute<SM, TEvent>...};
return dispatch_table[id - EventRangeBegin](fsm, e);
const auto id64 = static_cast<long long>(id);
const auto begin64 = static_cast<long long>(EventRangeBegin);
const auto count64 = static_cast<long long>(sizeof...(Ns));
if (id64 < begin64 || id64 >= begin64 + count64) {
return false;
}
const auto dispatch_id = static_cast<int>(id64 - begin64);

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bounds checking logic uses signed long long conversions to detect out-of-range values, but then casts back to int for the array index. This could cause issues on platforms where int and long long have different ranges. Consider using size_t or std::size_t for array indexing operations, or at least ensure the cast to int is safe by checking that dispatch_id fits within int range before the cast.

Suggested change
const auto dispatch_id = static_cast<int>(id64 - begin64);
const auto dispatch_id = static_cast<std::size_t>(id64 - begin64);

Copilot uses AI. Check for mistakes.
return dispatch_table[dispatch_id](fsm, e);
};
}
} // namespace detail
Expand Down
3 changes: 3 additions & 0 deletions scripts/quality_gates.sh
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,9 @@ fi
if [[ -n "${EXTRA_CXX_FLAGS}" ]]; then
BASE_CXX_FLAGS="${BASE_CXX_FLAGS} ${EXTRA_CXX_FLAGS}"
fi
if [[ "${COMPILER_FAMILY}" == "clang" ]]; then
BASE_CXX_FLAGS="${BASE_CXX_FLAGS} -Wno-unknown-warning-option -Wno-error=mismatched-tags -Wno-error=stack-exhausted -Wno-stack-exhausted"

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clang-specific warning suppressions on line 531 include -Wno-error=stack-exhausted and -Wno-stack-exhausted. The stack-exhausted warning suggests that the large test file (4504 lines with deep template instantiations) may be causing stack issues during compilation. While suppressing these warnings allows the build to succeed, this could mask real stack overflow issues in the compiler or tests. Consider splitting issues_test.cpp into multiple smaller files to reduce compilation resource usage and avoid hitting compiler limits.

Suggested change
BASE_CXX_FLAGS="${BASE_CXX_FLAGS} -Wno-unknown-warning-option -Wno-error=mismatched-tags -Wno-error=stack-exhausted -Wno-stack-exhausted"
BASE_CXX_FLAGS="${BASE_CXX_FLAGS} -Wno-unknown-warning-option -Wno-error=mismatched-tags"

Copilot uses AI. Check for mistakes.
fi

require_defaults

Expand Down
4 changes: 4 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,9 @@ elseif(IS_COMPILER_OPTION_GCC_LIKE)
add_compile_options(-include common/test.hpp)
endif()

if(MINGW AND IS_COMPILER_OPTION_GCC_LIKE)
add_compile_options(-Wa,-mbig-obj)
endif()

add_subdirectory(ft)
add_subdirectory(ut)
6 changes: 6 additions & 0 deletions test/ft/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ add_test(test_policies_testing test_policies_testing)
add_executable(test_policies_dispatch policies_dispatch.cpp)
add_test(test_policies_dispatch test_policies_dispatch)

add_executable(test_extended_tests extended_tests.cpp)
add_test(test_extended_tests test_extended_tests)

add_executable(test_issues_test issues_test.cpp)
add_test(test_issues_test test_issues_test)

add_executable(test_policies_thread_safe policies_thread_safe.cpp)
add_test(test_policies_thread_safe test_policies_thread_safe)
target_link_libraries(test_policies_thread_safe -lpthread)
Expand Down
198 changes: 198 additions & 0 deletions test/ft/extended_tests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
#include <algorithm>
#include <boost/sml.hpp>
#include <string>
#include <vector>

namespace sml = boost::sml;

inline std::string canonical_state_name(std::string state) {
if (0 == state.find("class ")) {
state.erase(0, 6);
} else if (0 == state.find("struct ")) {
state.erase(0, 7);
}
const auto scope_pos = state.rfind("::");
if (scope_pos != std::string::npos) {
state.erase(0, scope_pos + 2);
}
return state;
}

template <class TSM>
std::vector<std::string> sorted_current_states(const TSM& sm) {
std::vector<std::string> states;
sm.visit_current_states([&](auto state) { states.push_back(canonical_state_name(state.c_str())); });
std::sort(states.begin(), states.end());
return states;
}

template <class TSM>
bool all_regions_terminated(const TSM& sm) {
const auto states = sorted_current_states(sm);
return std::all_of(states.cbegin(), states.cend(), [](const auto& state) { return state == "terminate"; });
}

struct e_single_start {};
struct e_single_finish {};
struct e_single_idle {};

struct e_ortho_left_start {};
struct e_ortho_left_finish {};
struct e_ortho_right_start {};
struct e_ortho_right_finish {};
struct e_ortho_unused {};
struct e_ortho_right_done {};

const auto qa_idle = sml::state<class qa_idle>;
const auto qa_active = sml::state<class qa_active>;
const auto qb_idle = sml::state<class qb_idle>;
const auto qb_active = sml::state<class qb_active>;

const auto qb_region_left = sml::state<class qb_region_left>;
const auto qb_region_right = sml::state<class qb_region_right>;
const auto qb_region_left_next = sml::state<class qb_region_left_next>;
const auto qb_region_right_next = sml::state<class qb_region_right_next>;
const auto qb_region_right_done = sml::state<class qb_region_right_done>;

struct e_return_left {};
struct e_return_right {};
struct e_return_unused {};

test is_single_region_termination_and_event_visibility = [] {
struct machine {
auto operator()() {
using namespace sml;
// clang-format off
return make_transition_table(
*qa_idle + event<e_single_start> = qa_active
, qa_active + event<e_single_finish> = sml::X
);
// clang-format on
}
};

sml::sm<machine> sm{};
expect((std::vector<std::string>{"qa_idle"} == sorted_current_states(sm)));
expect(sm.process_event(e_single_start{}));
expect((std::vector<std::string>{"qa_active"} == sorted_current_states(sm)));
expect(!sm.process_event(e_single_idle{}));
expect(sm.process_event(e_single_finish{}));
expect((std::vector<std::string>{"terminate"} == sorted_current_states(sm)));
expect(sm.is(sml::X));
expect(!sm.process_event(e_single_idle{}));
};

test orthogonal_regions_partial_termination = [] {
struct machine {
auto operator()() {
using namespace sml;
// clang-format off
return make_transition_table(
*qb_region_left + event<e_ortho_left_start> = qb_region_left_next
, qb_region_left_next + event<e_ortho_left_finish> = sml::X
, *qb_region_right + event<e_ortho_right_start> = qb_region_right_next
, qb_region_right_next + event<e_ortho_right_finish> = sml::X
);
// clang-format on
}
};

sml::sm<machine> sm{};
expect((std::vector<std::string>{"qb_region_left", "qb_region_right"} == sorted_current_states(sm)));
expect(!all_regions_terminated(sm));

expect(sm.process_event(e_ortho_left_start{}));
expect((std::vector<std::string>{"qb_region_left_next", "qb_region_right"} == sorted_current_states(sm)));
expect(!all_regions_terminated(sm));

expect(sm.process_event(e_ortho_left_finish{}));
expect((std::vector<std::string>{"qb_region_right", "terminate"} == sorted_current_states(sm)));
expect(!all_regions_terminated(sm));
expect(!sm.process_event(e_ortho_unused{}));
expect(!all_regions_terminated(sm));

expect(sm.process_event(e_ortho_right_start{}));
expect((std::vector<std::string>{"qb_region_right_next", "terminate"} == sorted_current_states(sm)));
expect(sm.process_event(e_ortho_right_finish{}));
expect((std::vector<std::string>{"terminate", "terminate"} == sorted_current_states(sm)));
expect(all_regions_terminated(sm));
};

test visit_current_states_reports_all_regions = [] {
struct machine {
auto operator()() {
using namespace sml;
// clang-format off
return make_transition_table(
*qb_region_left + event<e_ortho_left_start> = qb_region_left_next
, qb_region_left_next + event<e_ortho_left_finish> = sml::X
, *qb_region_right + event<e_ortho_right_start> = qb_region_right_next
, qb_region_right_next + event<e_ortho_right_finish> = sml::X
);
// clang-format on
}
};

sml::sm<machine> sm{};

expect((std::vector<std::string>{"qb_region_left", "qb_region_right"} == sorted_current_states(sm)));
sm.process_event(e_ortho_left_start{});
sm.process_event(e_ortho_right_start{});
expect((std::vector<std::string>{"qb_region_left_next", "qb_region_right_next"} == sorted_current_states(sm)));

sm.process_event(e_ortho_left_finish{});
sm.process_event(e_ortho_right_finish{});
expect((std::vector<std::string>{"terminate", "terminate"} == sorted_current_states(sm)));
};

test process_event_return_value_in_orthogonal_regions = [] {
struct machine {
auto operator()() {
using namespace sml;
// clang-format off
return make_transition_table(
*qa_idle + event<e_return_left> = qa_active
, qa_active + event<e_return_left> = qa_active
, *qb_idle + event<e_return_right> = qb_active
);
// clang-format on
}
};

sml::sm<machine> sm{};
expect(sm.process_event(e_return_left{}));
expect((std::vector<std::string>{"qa_active", "qb_idle"} == sorted_current_states(sm)));
expect(!sm.process_event(e_return_unused{}));
expect((std::vector<std::string>{"qa_active", "qb_idle"} == sorted_current_states(sm)));
expect(sm.process_event(e_return_right{}));
expect((std::vector<std::string>{"qa_active", "qb_active"} == sorted_current_states(sm)));
};

test orthogonal_regions_still_process_after_partial_termination = [] {
struct machine {
auto operator()() {
using namespace sml;
// clang-format off
return make_transition_table(
*qb_region_left + event<e_ortho_left_start> = qb_region_left_next
, qb_region_left_next + event<e_ortho_left_finish> = sml::X
, *qb_region_right + event<e_ortho_right_start> = qb_region_right_next
, qb_region_right_next + event<e_ortho_right_done> = qb_region_right_done
);
// clang-format on
}
};

sml::sm<machine> sm{};
expect((std::vector<std::string>{"qb_region_left", "qb_region_right"} == sorted_current_states(sm)));
expect(sm.process_event(e_ortho_left_start{}));
expect((std::vector<std::string>{"qb_region_left_next", "qb_region_right"} == sorted_current_states(sm)));
expect(sm.process_event(e_ortho_left_finish{}));
expect((std::vector<std::string>{"qb_region_right", "terminate"} == sorted_current_states(sm)));

expect(sm.process_event(e_ortho_right_start{}));
expect((std::vector<std::string>{"qb_region_right_next", "terminate"} == sorted_current_states(sm)));
expect(sm.process_event(e_ortho_right_done{}));
expect((std::vector<std::string>{"qb_region_right_done", "terminate"} == sorted_current_states(sm)));
expect(!sm.process_event(e_ortho_unused{}));
};
Loading
Loading