diff --git a/libs/core/algorithms/CMakeLists.txt b/libs/core/algorithms/CMakeLists.txt index 1b880638f24d..a7898abeb80b 100644 --- a/libs/core/algorithms/CMakeLists.txt +++ b/libs/core/algorithms/CMakeLists.txt @@ -279,6 +279,7 @@ add_hpx_module( hpx_concepts hpx_concurrency hpx_config + hpx_contracts hpx_coroutines hpx_datastructures hpx_errors diff --git a/libs/core/algorithms/include/hpx/parallel/algorithms/sort.hpp b/libs/core/algorithms/include/hpx/parallel/algorithms/sort.hpp index 8b018d02da16..26cee15f7399 100644 --- a/libs/core/algorithms/include/hpx/parallel/algorithms/sort.hpp +++ b/libs/core/algorithms/include/hpx/parallel/algorithms/sort.hpp @@ -152,6 +152,7 @@ namespace hpx { #include #include #include +#include #include #include #include @@ -392,8 +393,8 @@ namespace hpx { > ) // clang-format on - friend void tag_fallback_invoke( - hpx::sort_t, RandomIt first, RandomIt last, Comp comp = Comp()) + friend void tag_fallback_invoke(hpx::sort_t, RandomIt first, + RandomIt last, Comp comp = Comp()) HPX_PRE(first <= last) { static_assert(std::random_access_iterator, "Requires a random access iterator."); @@ -416,7 +417,7 @@ namespace hpx { // clang-format on friend parallel::util::detail::algorithm_result_t tag_fallback_invoke(hpx::sort_t, ExPolicy&& policy, RandomIt first, - RandomIt last, Comp comp = Comp()) + RandomIt last, Comp comp = Comp()) HPX_PRE(first <= last) { static_assert(std::random_access_iterator, "Requires a random access iterator."); diff --git a/libs/core/algorithms/include/hpx/parallel/algorithms/stable_sort.hpp b/libs/core/algorithms/include/hpx/parallel/algorithms/stable_sort.hpp index 0ef9f99219f8..71d7e8d6a2db 100644 --- a/libs/core/algorithms/include/hpx/parallel/algorithms/stable_sort.hpp +++ b/libs/core/algorithms/include/hpx/parallel/algorithms/stable_sort.hpp @@ -150,6 +150,7 @@ namespace hpx { #include #include +#include #include #include #include @@ -285,7 +286,7 @@ namespace hpx { ) // clang-format on friend void tag_fallback_invoke(hpx::stable_sort_t, RandomIt first, - RandomIt last, Comp comp = Comp()) + RandomIt last, Comp comp = Comp()) HPX_PRE(first <= last) { static_assert(std::random_access_iterator, "Requires a random access iterator."); @@ -310,6 +311,7 @@ namespace hpx { friend hpx::parallel::util::detail::algorithm_result_t tag_fallback_invoke(hpx::stable_sort_t, ExPolicy&& policy, RandomIt first, RandomIt last, Comp comp = Comp()) + HPX_PRE(first <= last) { static_assert(std::random_access_iterator, "Requires a random access iterator."); diff --git a/libs/core/contracts/CMakeLists.txt b/libs/core/contracts/CMakeLists.txt index 8d19136d388a..51e344dd5923 100644 --- a/libs/core/contracts/CMakeLists.txt +++ b/libs/core/contracts/CMakeLists.txt @@ -21,14 +21,46 @@ if(HPX_WITH_CXX26_CONTRACTS) DEFINE HPX_CONTRACTS_HAVE_ASSERTS_AS_CONTRACT_ASSERTS NAMESPACE CONTRACTS ) endif() +else() + hpx_option( + HPX_WITH_CONTRACTS_MODE + STRING + "Contract evaluation mode for pre-C++26 compilers, options are: ENFORCE, OBSERVE, IGNORE" + "ENFORCE" + STRINGS "ENFORCE;OBSERVE;IGNORE" + ) + + # Note: values start at 1 because hpx_add_config_define_namespace treats a + # numeric "0" as falsy and would emit a valueless #define. + if(HPX_WITH_CONTRACTS_MODE STREQUAL "ENFORCE") + hpx_add_config_define_namespace( + DEFINE HPX_HAVE_CONTRACTS_MODE + VALUE 1 + NAMESPACE CONTRACTS + ) + elseif(HPX_WITH_CONTRACTS_MODE STREQUAL "OBSERVE") + hpx_add_config_define_namespace( + DEFINE HPX_HAVE_CONTRACTS_MODE + VALUE 2 + NAMESPACE CONTRACTS + ) + elseif(HPX_WITH_CONTRACTS_MODE STREQUAL "IGNORE") + hpx_add_config_define_namespace( + DEFINE HPX_HAVE_CONTRACTS_MODE + VALUE 3 + NAMESPACE CONTRACTS + ) + endif() endif() # Default location is $HPX_ROOT/libs/contracts/include -set(contracts_headers hpx/contracts.hpp hpx/contracts/macros.hpp) +set(contracts_headers hpx/contracts.hpp hpx/contracts/macros.hpp + hpx/contracts/violation_handler.hpp +) set(contracts_macro_headers hpx/contracts/macros.hpp) # Default location is $HPX_ROOT/libs/contracts/src -set(contracts_sources) +set(contracts_sources violation_handler.cpp) include(HPX_AddModule) add_hpx_module( diff --git a/libs/core/contracts/include/hpx/contracts/macros.hpp b/libs/core/contracts/include/hpx/contracts/macros.hpp index c1f9d0a06469..ee997030d379 100644 --- a/libs/core/contracts/include/hpx/contracts/macros.hpp +++ b/libs/core/contracts/include/hpx/contracts/macros.hpp @@ -1,4 +1,4 @@ -// Copyright (c) 2025 The STE||AR-Group +// Copyright (c) 2025-2026 The STE||AR-Group // Copyright (c) 2025 Alexandros Papadakis // // SPDX-License-Identifier: BSL-1.0 @@ -13,22 +13,25 @@ /// behavior when contracts are not available. /// /// ## API Reference: -/// - **HPX_PRE(condition)**: Precondition contracts (no-op in fallback mode) -/// - **HPX_POST(condition)**: Postcondition contracts (no-op in fallback mode) -/// - **HPX_CONTRACT_ASSERT(condition)**: Contract assertions (always available, maps to HPX_ASSERT) +/// - **HPX_PRE(condition)**: Precondition contracts (declaration specifier in +/// C++26; no-op in fallback mode) +/// - **HPX_POST(condition)**: Postcondition contracts (declaration specifier +/// in C++26; no-op in fallback mode) +/// - **HPX_CONTRACT_ASSERT(condition)**: Contract assertions (always active; +/// dispatches on HPX_WITH_CONTRACTS_MODE in fallback mode) /// /// ## Configuration: -/// Enable with: `cmake -DHPX_WITH_CONTRACTS=ON -DCMAKE_CXX_STANDARD=26` +/// Enable native contracts with: `cmake -DHPX_WITH_CONTRACTS=ON -DCMAKE_CXX_STANDARD=26` +/// Set fallback mode with: `cmake -DHPX_WITH_CONTRACTS_MODE=ENFORCE|OBSERVE|IGNORE` /// /// See docs/index.rst for comprehensive usage guide. #pragma once #include +#include #include -// Contract implementation: automatically selects native C++26 contracts -// or provides appropriate fallback behavior based on compiler capabilities #if defined(HPX_HAVE_CXX26_CONTRACTS) // Native C++26 contracts mode @@ -42,12 +45,33 @@ #define HPX_ASSERT(x) contract_assert(x) #endif -#else +#else // fallback mode -// Fallback mode: PRE/POST become no-ops for forward compatibility, -// CONTRACT_ASSERT maps to HPX_ASSERT for runtime validation +// HPX_PRE and HPX_POST are declaration specifiers in C++26 and cannot be +// replicated as statement macros without changing call sites. Keep them as +// no-ops in fallback regardless of mode. #define HPX_PRE(x) -#define HPX_CONTRACT_ASSERT(x) HPX_ASSERT((x)) #define HPX_POST(x) -#endif +#if defined(HPX_HAVE_CONTRACTS_MODE) && \ + HPX_HAVE_CONTRACTS_MODE == 3 // IGNORE + +#define HPX_CONTRACT_ASSERT(x) + +#else // ENFORCE (0) or OBSERVE (1): runtime checking via violation handler + +#include + +// clang-format off +#define HPX_CONTRACT_ASSERT(x) \ + do { \ + if (!(x)) \ + ::hpx::contracts::invoke_violation_handler( \ + {::hpx::contracts::contract_kind::assertion, #x, \ + HPX_CURRENT_SOURCE_LOCATION()}); \ + } while (false) +// clang-format on + +#endif // HPX_HAVE_CONTRACTS_MODE + +#endif // HPX_HAVE_CXX26_CONTRACTS diff --git a/libs/core/contracts/include/hpx/contracts/violation_handler.hpp b/libs/core/contracts/include/hpx/contracts/violation_handler.hpp new file mode 100644 index 000000000000..1f3455c8db89 --- /dev/null +++ b/libs/core/contracts/include/hpx/contracts/violation_handler.hpp @@ -0,0 +1,36 @@ +// Copyright (c) 2026 The STE||AR-Group +// +// SPDX-License-Identifier: BSL-1.0 +// 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) + +#pragma once + +#include +#include + +namespace hpx::contracts { + + HPX_CXX_CORE_EXPORT enum class contract_kind { pre, post, assertion }; + + HPX_CXX_CORE_EXPORT struct violation_info + { + contract_kind kind; + char const* condition; + hpx::source_location location; + }; + + HPX_CXX_CORE_EXPORT using violation_handler_t = + void (*)(violation_info const&); + + HPX_CXX_CORE_EXPORT HPX_CORE_EXPORT violation_handler_t + set_violation_handler(violation_handler_t handler) noexcept; + HPX_CXX_CORE_EXPORT HPX_CORE_EXPORT violation_handler_t + get_violation_handler() noexcept; + + HPX_CXX_CORE_EXPORT HPX_CORE_EXPORT void default_violation_handler( + violation_info const& info); + HPX_CXX_CORE_EXPORT HPX_CORE_EXPORT void invoke_violation_handler( + violation_info const& info); + +} // namespace hpx::contracts diff --git a/libs/core/contracts/src/violation_handler.cpp b/libs/core/contracts/src/violation_handler.cpp new file mode 100644 index 000000000000..a80838c03cf9 --- /dev/null +++ b/libs/core/contracts/src/violation_handler.cpp @@ -0,0 +1,73 @@ +// Copyright (c) 2026 The STE||AR-Group +// +// SPDX-License-Identifier: BSL-1.0 +// 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 + +namespace hpx::contracts { + + namespace detail { + + [[nodiscard]] violation_handler_t& get_handler() noexcept + { + static violation_handler_t handler = nullptr; + return handler; + } + + } // namespace detail + + violation_handler_t set_violation_handler( + violation_handler_t handler) noexcept + { + violation_handler_t old = detail::get_handler(); + detail::get_handler() = handler; + return old; + } + + violation_handler_t get_violation_handler() noexcept + { + return detail::get_handler(); + } + + void default_violation_handler(violation_info const& info) + { + char const* kind_str = nullptr; + switch (info.kind) + { + case contract_kind::pre: + kind_str = "precondition"; + break; + case contract_kind::post: + kind_str = "postcondition"; + break; + case contract_kind::assertion: + kind_str = "assertion"; + break; + } + + std::cerr << info.location << ": Contract " << kind_str << " '" + << info.condition << "' violated\n"; + +#if !defined(HPX_HAVE_CONTRACTS_MODE) || HPX_HAVE_CONTRACTS_MODE != 2 + // abort in ENFORCE (and by default); continue in OBSERVE + std::abort(); +#endif + } + + void invoke_violation_handler(violation_info const& info) + { + violation_handler_t handler = detail::get_handler(); + if (handler == nullptr) + default_violation_handler(info); + else + handler(info); + } + +} // namespace hpx::contracts diff --git a/libs/core/contracts/tests/unit/CMakeLists.txt b/libs/core/contracts/tests/unit/CMakeLists.txt index 78f1e62e0f93..aab3272ddafc 100644 --- a/libs/core/contracts/tests/unit/CMakeLists.txt +++ b/libs/core/contracts/tests/unit/CMakeLists.txt @@ -1,4 +1,5 @@ # Copyright (c) 2025 Alexandros Papadakis +# Copyright (c) 2026 The STE||AR-Group # HPX Contracts Module Unit Tests # # SPDX-License-Identifier: BSL-1.0 @@ -15,6 +16,9 @@ set(contract_tests fallback_contracts_succeed fallback_contracts_fail disabled_contracts + violation_handler_invoked + default_handler_aborts + handler_receives_correct_info ) foreach(test ${contract_tests}) @@ -29,16 +33,8 @@ foreach(test ${contract_tests}) add_hpx_unit_test("modules.contracts" ${test}) endforeach() -# Set failure expectation for fallback failure test Fallback only fails in Debug -# mode (HPX_ASSERT or HPX_CONTRACT_ASSERT behavior) -set_tests_properties( - tests.unit.modules.contracts.declaration_contracts_fail_contract_assert - PROPERTIES WILL_FAIL $<$:ON> -) - if(HPX_HAVE_CXX26_CONTRACTS) - # Set failure expectations for declaration contract failure tests Native - # contracts should fail when violated + # Native C++26 contracts always fail when violated, regardless of build config set_tests_properties( tests.unit.modules.contracts.declaration_contracts_fail_pre PROPERTIES WILL_FAIL ON @@ -47,9 +43,30 @@ if(HPX_HAVE_CXX26_CONTRACTS) tests.unit.modules.contracts.declaration_contracts_fail_post PROPERTIES WILL_FAIL ON ) + # HPX_CONTRACT_ASSERT still routes through HPX_ASSERT in native mode — Debug + # only + set_tests_properties( + tests.unit.modules.contracts.declaration_contracts_fail_contract_assert + PROPERTIES WILL_FAIL $<$:ON> + ) else() - # Set failure expectation for fallback failure test Fallback only fails in - # Debug mode (HPX_ASSERT behavior) + # Fallback mode: HPX_PRE / HPX_POST are no-ops so fail_pre / fail_post + # succeed. HPX_CONTRACT_ASSERT routes through invoke_violation_handler, whose + # default handler aborts unless HPX_WITH_CONTRACTS_MODE=OBSERVE. + if(NOT HPX_WITH_CONTRACTS_MODE STREQUAL "OBSERVE" + AND NOT HPX_WITH_CONTRACTS_MODE STREQUAL "IGNORE" + ) + set_tests_properties( + tests.unit.modules.contracts.declaration_contracts_fail_contract_assert + PROPERTIES WILL_FAIL ON + ) + set_tests_properties( + tests.unit.modules.contracts.default_handler_aborts PROPERTIES WILL_FAIL + ON + ) + endif() + + # fallback_contracts_fail uses HPX_ASSERT directly, so it only fails in Debug set_tests_properties( tests.unit.modules.contracts.fallback_contracts_fail PROPERTIES WILL_FAIL $<$:ON> diff --git a/libs/core/contracts/tests/unit/default_handler_aborts.cpp b/libs/core/contracts/tests/unit/default_handler_aborts.cpp new file mode 100644 index 000000000000..6b03112188a8 --- /dev/null +++ b/libs/core/contracts/tests/unit/default_handler_aborts.cpp @@ -0,0 +1,13 @@ +// Copyright (c) 2026 The STE||AR-Group +// +// SPDX-License-Identifier: BSL-1.0 +// 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 + +int main() +{ + HPX_CONTRACT_ASSERT(false); // default handler should abort here + return 0; +} diff --git a/libs/core/contracts/tests/unit/handler_receives_correct_info.cpp b/libs/core/contracts/tests/unit/handler_receives_correct_info.cpp new file mode 100644 index 000000000000..f33e331b76b8 --- /dev/null +++ b/libs/core/contracts/tests/unit/handler_receives_correct_info.cpp @@ -0,0 +1,46 @@ +// Copyright (c) 2026 The STE||AR-Group +// +// SPDX-License-Identifier: BSL-1.0 +// 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 + +#if !defined(HPX_HAVE_CXX26_CONTRACTS) && HPX_CONTRACTS_MODE == 2 + +int main() +{ + return 0; +} + +#else + +namespace { + hpx::contracts::violation_info captured{ + hpx::contracts::contract_kind::assertion, nullptr, {}}; + + void capturing_handler(hpx::contracts::violation_info const& info) + { + captured = info; + } +} // namespace + +int main() +{ + hpx::contracts::set_violation_handler(capturing_handler); + + HPX_CONTRACT_ASSERT(1 == 2); // NOLINT: intentional false assertion + + HPX_TEST(captured.condition != nullptr); + HPX_TEST(std::strstr(captured.condition, "1 == 2") != nullptr); + HPX_TEST(captured.kind == hpx::contracts::contract_kind::assertion); + HPX_TEST(captured.location.line() > 0); + + return hpx::util::report_errors(); +} + +#endif diff --git a/libs/core/contracts/tests/unit/violation_handler_invoked.cpp b/libs/core/contracts/tests/unit/violation_handler_invoked.cpp new file mode 100644 index 000000000000..ca0d614e7485 --- /dev/null +++ b/libs/core/contracts/tests/unit/violation_handler_invoked.cpp @@ -0,0 +1,46 @@ +// Copyright (c) 2026 The STE||AR-Group +// +// SPDX-License-Identifier: BSL-1.0 +// 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 + +#if !defined(HPX_HAVE_CXX26_CONTRACTS) && HPX_CONTRACTS_MODE == 2 + +// IGNORE mode: contracts are no-ops, nothing to test here. +int main() +{ + return 0; +} + +#else + +namespace { + int handler_call_count = 0; + hpx::contracts::contract_kind last_kind = + hpx::contracts::contract_kind::assertion; + + void recording_handler(hpx::contracts::violation_info const& info) + { + ++handler_call_count; + last_kind = info.kind; + // deliberately does not abort so the test can continue + } +} // namespace + +int main() +{ + hpx::contracts::set_violation_handler(recording_handler); + + HPX_CONTRACT_ASSERT(false); // should invoke recording_handler + + HPX_TEST_EQ(handler_call_count, 1); + HPX_TEST(last_kind == hpx::contracts::contract_kind::assertion); + + return hpx::util::report_errors(); +} + +#endif diff --git a/libs/core/datastructures/CMakeLists.txt b/libs/core/datastructures/CMakeLists.txt index f8dcfc7630b0..db0d23c5cb02 100644 --- a/libs/core/datastructures/CMakeLists.txt +++ b/libs/core/datastructures/CMakeLists.txt @@ -101,7 +101,13 @@ add_hpx_module( "hpx/datastructures/detail/flat_set.hpp" "hpx/datastructures/detail/intrusive_list.hpp" "hpx/datastructures/detail/small_vector.hpp" - MODULE_DEPENDENCIES hpx_assertion hpx_config hpx_concepts hpx_errors - hpx_serialization hpx_type_support + MODULE_DEPENDENCIES + hpx_assertion + hpx_config + hpx_concepts + hpx_contracts + hpx_errors + hpx_serialization + hpx_type_support CMAKE_SUBDIRS examples tests ) diff --git a/libs/core/datastructures/include/hpx/datastructures/detail/small_vector.hpp b/libs/core/datastructures/include/hpx/datastructures/detail/small_vector.hpp index f601f921d157..4cec16a192b0 100644 --- a/libs/core/datastructures/include/hpx/datastructures/detail/small_vector.hpp +++ b/libs/core/datastructures/include/hpx/datastructures/detail/small_vector.hpp @@ -33,6 +33,7 @@ #include #include +#include #include #include @@ -938,12 +939,13 @@ namespace hpx::detail { } [[nodiscard]] auto operator[](std::size_t idx) const noexcept - -> T const& + HPX_PRE(idx < this->size()) -> T const& { return *(data() + idx); } - [[nodiscard]] auto operator[](std::size_t idx) noexcept -> T& + [[nodiscard]] auto operator[](std::size_t idx) noexcept + HPX_PRE(idx < this->size()) -> T& { return *(data() + idx); } @@ -1034,17 +1036,19 @@ namespace hpx::detail { return const_reverse_iterator{begin()}; } - [[nodiscard]] auto front() const noexcept -> T const& + [[nodiscard]] auto front() const noexcept HPX_PRE(!this->empty()) + -> T const& { return *data(); } - [[nodiscard]] auto front() noexcept -> T& + [[nodiscard]] auto front() noexcept HPX_PRE(!this->empty()) -> T& { return *data(); } - [[nodiscard]] auto back() const noexcept -> T const& + [[nodiscard]] auto back() const noexcept HPX_PRE(!this->empty()) + -> T const& { if (is_direct()) { @@ -1055,7 +1059,7 @@ namespace hpx::detail { data() + size() - 1); } - [[nodiscard]] auto back() noexcept -> T& + [[nodiscard]] auto back() noexcept HPX_PRE(!this->empty()) -> T& { if (is_direct()) { diff --git a/libs/core/futures/CMakeLists.txt b/libs/core/futures/CMakeLists.txt index 86cf24785a9c..98863b63bde0 100644 --- a/libs/core/futures/CMakeLists.txt +++ b/libs/core/futures/CMakeLists.txt @@ -77,6 +77,7 @@ add_hpx_module( hpx_concepts hpx_concurrency hpx_config + hpx_contracts hpx_coroutines hpx_datastructures hpx_errors diff --git a/libs/core/futures/include/hpx/futures/future.hpp b/libs/core/futures/include/hpx/futures/future.hpp index 6c777b0e3574..1f72de3f3d78 100644 --- a/libs/core/futures/include/hpx/futures/future.hpp +++ b/libs/core/futures/include/hpx/futures/future.hpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -662,6 +663,7 @@ namespace hpx { // shared state. // Postcondition: valid() == false. typename hpx::traits::future_traits::result_type get() + HPX_PRE(this->valid()) { if (!this->shared_state_) { @@ -1012,7 +1014,7 @@ namespace hpx { // shared state. // Postcondition: valid() == false. typename hpx::traits::future_traits::result_type get() - const //-V659 + const HPX_PRE(this->valid()) //-V659 { if (!this->shared_state_) {