diff --git a/.github/workflows/cetlvast.yml b/.github/workflows/cetlvast.yml index 419f2475..3ae33eae 100644 --- a/.github/workflows/cetlvast.yml +++ b/.github/workflows/cetlvast.yml @@ -88,7 +88,7 @@ jobs: strategy: matrix: toolchain: ["gcc-native-32", "clang-native"] - standard: ["cpp-14", "cpp-17", "cpp-20", "cpp-23"] + standard: ["cpp-14", "cpp-17", "cpp-20", "cpp-23", "cpp-26"] steps: - uses: actions/checkout@v7 with: diff --git a/cetlvast/CMakeLists.txt b/cetlvast/CMakeLists.txt index d63e2930..4107c99d 100644 --- a/cetlvast/CMakeLists.txt +++ b/cetlvast/CMakeLists.txt @@ -78,7 +78,7 @@ message(STATUS # +---------------------------------------------------------------------------+ set(CETLVAST_CPP_STANDARD "20" CACHE STRING "C++ standard to use when compiling.") -set_property(CACHE CETLVAST_CPP_STANDARD PROPERTY STRINGS 14 17 20) +set_property(CACHE CETLVAST_CPP_STANDARD PROPERTY STRINGS 14 17 20 23 26) option(CETLVAST_ENABLE_FUZZING "Build native libFuzzer targets with ASan and UBSan." OFF) if(CETLVAST_ENABLE_FUZZING AND CMAKE_CROSSCOMPILING) diff --git a/cetlvast/CMakePresets.json b/cetlvast/CMakePresets.json index 950d500a..ca07ac03 100644 --- a/cetlvast/CMakePresets.json +++ b/cetlvast/CMakePresets.json @@ -445,6 +445,85 @@ "configure-standard-cpp-23", "configure-update-offline" ] + }, + { + "name": "configure-standard-cpp-26", + "hidden": true, + "cacheVariables": { + "CETLVAST_CPP_STANDARD": "26" + } + }, + { + "name": "configure-gcc-native-cpp-26-online", + "inherits": [ + "configure-common", + "configure-toolchain-gcc-native", + "configure-standard-cpp-26", + "configure-update-online" + ] + }, + { + "name": "configure-gcc-native-cpp-26-offline", + "inherits": [ + "configure-common", + "configure-toolchain-gcc-native", + "configure-standard-cpp-26", + "configure-update-offline" + ] + }, + { + "name": "configure-gcc-native-32-cpp-26-online", + "inherits": [ + "configure-common", + "configure-toolchain-gcc-native-32", + "configure-standard-cpp-26", + "configure-update-online" + ] + }, + { + "name": "configure-gcc-native-32-cpp-26-offline", + "inherits": [ + "configure-common", + "configure-toolchain-gcc-native-32", + "configure-standard-cpp-26", + "configure-update-offline" + ] + }, + { + "name": "configure-clang-native-cpp-26-online", + "inherits": [ + "configure-common", + "configure-toolchain-clang-native", + "configure-standard-cpp-26", + "configure-update-online" + ] + }, + { + "name": "configure-clang-native-cpp-26-offline", + "inherits": [ + "configure-common", + "configure-toolchain-clang-native", + "configure-standard-cpp-26", + "configure-update-offline" + ] + }, + { + "name": "configure-arm-none-eabi-gcc-cpp-26-online", + "inherits": [ + "configure-common", + "configure-toolchain-arm-none-eabi-gcc", + "configure-standard-cpp-26", + "configure-update-online" + ] + }, + { + "name": "configure-arm-none-eabi-gcc-cpp-26-offline", + "inherits": [ + "configure-common", + "configure-toolchain-arm-none-eabi-gcc", + "configure-standard-cpp-26", + "configure-update-offline" + ] } ], "buildPresets": [ @@ -2011,6 +2090,358 @@ "run_examples", "gcovr_html_report_for_examples" ] + }, + { + "name": "build-Release-gcc-native-cpp-26-online", + "configurePreset": "configure-gcc-native-cpp-26-online", + "configuration": "Release", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-Release-gcc-native-cpp-26-offline", + "configurePreset": "configure-gcc-native-cpp-26-offline", + "configuration": "Release", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-Release-gcc-native-32-cpp-26-online", + "configurePreset": "configure-gcc-native-32-cpp-26-online", + "configuration": "Release", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-Release-gcc-native-32-cpp-26-offline", + "configurePreset": "configure-gcc-native-32-cpp-26-offline", + "configuration": "Release", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-Release-clang-native-cpp-26-online", + "configurePreset": "configure-clang-native-cpp-26-online", + "configuration": "Release", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-Release-clang-native-cpp-26-offline", + "configurePreset": "configure-clang-native-cpp-26-offline", + "configuration": "Release", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-ReleaseEP-gcc-native-cpp-26-online", + "configurePreset": "configure-gcc-native-cpp-26-online", + "configuration": "ReleaseEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-ReleaseEP-gcc-native-cpp-26-offline", + "configurePreset": "configure-gcc-native-cpp-26-offline", + "configuration": "ReleaseEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-ReleaseEP-gcc-native-32-cpp-26-online", + "configurePreset": "configure-gcc-native-32-cpp-26-online", + "configuration": "ReleaseEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-ReleaseEP-gcc-native-32-cpp-26-offline", + "configurePreset": "configure-gcc-native-32-cpp-26-offline", + "configuration": "ReleaseEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-ReleaseEP-clang-native-cpp-26-online", + "configurePreset": "configure-clang-native-cpp-26-online", + "configuration": "ReleaseEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-ReleaseEP-clang-native-cpp-26-offline", + "configurePreset": "configure-clang-native-cpp-26-offline", + "configuration": "ReleaseEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-ReleaseEP-arm-none-eabi-gcc-cpp-26-online", + "configurePreset": "configure-arm-none-eabi-gcc-cpp-26-online", + "configuration": "ReleaseEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-ReleaseEP-arm-none-eabi-gcc-cpp-26-offline", + "configurePreset": "configure-arm-none-eabi-gcc-cpp-26-offline", + "configuration": "ReleaseEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-Debug-gcc-native-cpp-26-online", + "configurePreset": "configure-gcc-native-cpp-26-online", + "configuration": "Debug", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-Debug-gcc-native-cpp-26-offline", + "configurePreset": "configure-gcc-native-cpp-26-offline", + "configuration": "Debug", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-Debug-gcc-native-32-cpp-26-online", + "configurePreset": "configure-gcc-native-32-cpp-26-online", + "configuration": "Debug", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-Debug-gcc-native-32-cpp-26-offline", + "configurePreset": "configure-gcc-native-32-cpp-26-offline", + "configuration": "Debug", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-Debug-clang-native-cpp-26-online", + "configurePreset": "configure-clang-native-cpp-26-online", + "configuration": "Debug", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-Debug-clang-native-cpp-26-offline", + "configurePreset": "configure-clang-native-cpp-26-offline", + "configuration": "Debug", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-DebugEP-gcc-native-cpp-26-online", + "configurePreset": "configure-gcc-native-cpp-26-online", + "configuration": "DebugEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-DebugEP-gcc-native-cpp-26-offline", + "configurePreset": "configure-gcc-native-cpp-26-offline", + "configuration": "DebugEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-DebugEP-gcc-native-32-cpp-26-online", + "configurePreset": "configure-gcc-native-32-cpp-26-online", + "configuration": "DebugEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-DebugEP-gcc-native-32-cpp-26-offline", + "configurePreset": "configure-gcc-native-32-cpp-26-offline", + "configuration": "DebugEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-DebugEP-clang-native-cpp-26-online", + "configurePreset": "configure-clang-native-cpp-26-online", + "configuration": "DebugEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-DebugEP-clang-native-cpp-26-offline", + "configurePreset": "configure-clang-native-cpp-26-offline", + "configuration": "DebugEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-DebugEP-arm-none-eabi-gcc-cpp-26-online", + "configurePreset": "configure-arm-none-eabi-gcc-cpp-26-online", + "configuration": "DebugEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-DebugEP-arm-none-eabi-gcc-cpp-26-offline", + "configurePreset": "configure-arm-none-eabi-gcc-cpp-26-offline", + "configuration": "DebugEP", + "targets": [ + "build", + "run_unittests", + "run_examples" + ] + }, + { + "name": "build-Coverage-gcc-native-cpp-26-online", + "configurePreset": "configure-gcc-native-cpp-26-online", + "configuration": "Coverage", + "targets": [ + "build", + "run_unittests", + "gcovr_html_report_for_unittest", + "run_examples", + "gcovr_html_report_for_examples" + ] + }, + { + "name": "build-Coverage-gcc-native-cpp-26-offline", + "configurePreset": "configure-gcc-native-cpp-26-offline", + "configuration": "Coverage", + "targets": [ + "build", + "run_unittests", + "gcovr_html_report_for_unittest", + "run_examples", + "gcovr_html_report_for_examples" + ] + }, + { + "name": "build-Coverage-gcc-native-32-cpp-26-online", + "configurePreset": "configure-gcc-native-32-cpp-26-online", + "configuration": "Coverage", + "targets": [ + "build", + "run_unittests", + "gcovr_html_report_for_unittest", + "run_examples", + "gcovr_html_report_for_examples" + ] + }, + { + "name": "build-Coverage-gcc-native-32-cpp-26-offline", + "configurePreset": "configure-gcc-native-32-cpp-26-offline", + "configuration": "Coverage", + "targets": [ + "build", + "run_unittests", + "gcovr_html_report_for_unittest", + "run_examples", + "gcovr_html_report_for_examples" + ] + }, + { + "name": "build-Coverage-clang-native-cpp-26-online", + "configurePreset": "configure-clang-native-cpp-26-online", + "configuration": "Coverage", + "targets": [ + "build", + "run_unittests", + "gcovr_html_report_for_unittest", + "run_examples", + "gcovr_html_report_for_examples" + ] + }, + { + "name": "build-Coverage-clang-native-cpp-26-offline", + "configurePreset": "configure-clang-native-cpp-26-offline", + "configuration": "Coverage", + "targets": [ + "build", + "run_unittests", + "gcovr_html_report_for_unittest", + "run_examples", + "gcovr_html_report_for_examples" + ] } ], "workflowPresets": [ @@ -2927,6 +3358,206 @@ "name": "build-DebugEP-arm-none-eabi-gcc-cpp-23-offline" } ] + }, + { + "name": "workflow-gcc-native-cpp-26-online", + "displayName": "gcc native cpp 26 online", + "description": "autogenerated workflow", + "steps": [ + { + "type": "configure", + "name": "configure-gcc-native-cpp-26-online" + }, + { + "type": "build", + "name": "build-Release-gcc-native-cpp-26-online" + }, + { + "type": "build", + "name": "build-ReleaseEP-gcc-native-cpp-26-online" + }, + { + "type": "build", + "name": "build-Debug-gcc-native-cpp-26-online" + }, + { + "type": "build", + "name": "build-DebugEP-gcc-native-cpp-26-online" + } + ] + }, + { + "name": "workflow-gcc-native-cpp-26-offline", + "displayName": "gcc native cpp 26 offline", + "description": "autogenerated workflow", + "steps": [ + { + "type": "configure", + "name": "configure-gcc-native-cpp-26-offline" + }, + { + "type": "build", + "name": "build-Release-gcc-native-cpp-26-offline" + }, + { + "type": "build", + "name": "build-ReleaseEP-gcc-native-cpp-26-offline" + }, + { + "type": "build", + "name": "build-Debug-gcc-native-cpp-26-offline" + }, + { + "type": "build", + "name": "build-DebugEP-gcc-native-cpp-26-offline" + } + ] + }, + { + "name": "workflow-gcc-native-32-cpp-26-online", + "displayName": "gcc native 32 cpp 26 online", + "description": "autogenerated workflow", + "steps": [ + { + "type": "configure", + "name": "configure-gcc-native-32-cpp-26-online" + }, + { + "type": "build", + "name": "build-Release-gcc-native-32-cpp-26-online" + }, + { + "type": "build", + "name": "build-ReleaseEP-gcc-native-32-cpp-26-online" + }, + { + "type": "build", + "name": "build-Debug-gcc-native-32-cpp-26-online" + }, + { + "type": "build", + "name": "build-DebugEP-gcc-native-32-cpp-26-online" + } + ] + }, + { + "name": "workflow-gcc-native-32-cpp-26-offline", + "displayName": "gcc native 32 cpp 26 offline", + "description": "autogenerated workflow", + "steps": [ + { + "type": "configure", + "name": "configure-gcc-native-32-cpp-26-offline" + }, + { + "type": "build", + "name": "build-Release-gcc-native-32-cpp-26-offline" + }, + { + "type": "build", + "name": "build-ReleaseEP-gcc-native-32-cpp-26-offline" + }, + { + "type": "build", + "name": "build-Debug-gcc-native-32-cpp-26-offline" + }, + { + "type": "build", + "name": "build-DebugEP-gcc-native-32-cpp-26-offline" + } + ] + }, + { + "name": "workflow-clang-native-cpp-26-online", + "displayName": "clang native cpp 26 online", + "description": "autogenerated workflow", + "steps": [ + { + "type": "configure", + "name": "configure-clang-native-cpp-26-online" + }, + { + "type": "build", + "name": "build-Release-clang-native-cpp-26-online" + }, + { + "type": "build", + "name": "build-ReleaseEP-clang-native-cpp-26-online" + }, + { + "type": "build", + "name": "build-Debug-clang-native-cpp-26-online" + }, + { + "type": "build", + "name": "build-DebugEP-clang-native-cpp-26-online" + } + ] + }, + { + "name": "workflow-clang-native-cpp-26-offline", + "displayName": "clang native cpp 26 offline", + "description": "autogenerated workflow", + "steps": [ + { + "type": "configure", + "name": "configure-clang-native-cpp-26-offline" + }, + { + "type": "build", + "name": "build-Release-clang-native-cpp-26-offline" + }, + { + "type": "build", + "name": "build-ReleaseEP-clang-native-cpp-26-offline" + }, + { + "type": "build", + "name": "build-Debug-clang-native-cpp-26-offline" + }, + { + "type": "build", + "name": "build-DebugEP-clang-native-cpp-26-offline" + } + ] + }, + { + "name": "workflow-arm-none-eabi-gcc-cpp-26-online", + "displayName": "arm none eabi gcc cpp 26 online", + "description": "autogenerated workflow", + "steps": [ + { + "type": "configure", + "name": "configure-arm-none-eabi-gcc-cpp-26-online" + }, + { + "type": "build", + "name": "build-ReleaseEP-arm-none-eabi-gcc-cpp-26-online" + }, + { + "type": "build", + "name": "build-DebugEP-arm-none-eabi-gcc-cpp-26-online" + } + ] + }, + { + "name": "workflow-arm-none-eabi-gcc-cpp-26-offline", + "displayName": "arm none eabi gcc cpp 26 offline", + "description": "autogenerated workflow", + "steps": [ + { + "type": "configure", + "name": "configure-arm-none-eabi-gcc-cpp-26-offline" + }, + { + "type": "build", + "name": "build-ReleaseEP-arm-none-eabi-gcc-cpp-26-offline" + }, + { + "type": "build", + "name": "build-DebugEP-arm-none-eabi-gcc-cpp-26-offline" + } + ] } ], "packagePresets": [], @@ -2991,7 +3622,8 @@ "cpp-14", "cpp-17", "cpp-20", - "cpp-23" + "cpp-23", + "cpp-26" ], "update": [ "online", @@ -3004,7 +3636,8 @@ "standard": [ "cpp-17", "cpp-20", - "cpp-23" + "cpp-23", + "cpp-26" ] } ], @@ -3048,7 +3681,8 @@ "cpp-14", "cpp-17", "cpp-20", - "cpp-23" + "cpp-23", + "cpp-26" ], "update": [ "online", @@ -3061,7 +3695,8 @@ "standard": [ "cpp-17", "cpp-20", - "cpp-23" + "cpp-23", + "cpp-26" ] }, { @@ -3098,7 +3733,8 @@ "cpp-14", "cpp-17", "cpp-20", - "cpp-23" + "cpp-23", + "cpp-26" ], "update": [ "online", @@ -3120,7 +3756,8 @@ "standard": [ "cpp-17", "cpp-20", - "cpp-23" + "cpp-23", + "cpp-26" ] }, { diff --git a/cetlvast/CMakePresetsVendorTemplate.json b/cetlvast/CMakePresetsVendorTemplate.json index 720a95c9..e6106d83 100644 --- a/cetlvast/CMakePresetsVendorTemplate.json +++ b/cetlvast/CMakePresetsVendorTemplate.json @@ -58,7 +58,8 @@ "cpp-14", "cpp-17", "cpp-20", - "cpp-23" + "cpp-23", + "cpp-26" ], "update": [ "online", @@ -68,7 +69,7 @@ "exclude": [ { "toolchain": "gcc7-native", - "standard": ["cpp-17", "cpp-20", "cpp-23"] + "standard": ["cpp-17", "cpp-20", "cpp-23", "cpp-26"] } ], "shape": { @@ -101,7 +102,7 @@ "exclude": [ { "toolchain": "gcc7-native", - "standard": ["cpp-17", "cpp-20", "cpp-23"] + "standard": ["cpp-17", "cpp-20", "cpp-23", "cpp-26"] }, { "toolchain": "arm-none-eabi-gcc", @@ -129,7 +130,7 @@ "exclude": [ { "toolchain": "gcc7-native", - "standard": ["cpp-17", "cpp-20", "cpp-23"] + "standard": ["cpp-17", "cpp-20", "cpp-23", "cpp-26"] }, { "configuration": "Coverage" diff --git a/cetlvast/suites/compile/CMakeLists.txt b/cetlvast/suites/compile/CMakeLists.txt index 1f8a7a57..542643ff 100644 --- a/cetlvast/suites/compile/CMakeLists.txt +++ b/cetlvast/suites/compile/CMakeLists.txt @@ -11,6 +11,10 @@ include(CompileTest) set(COMPILE_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/test_cetl_h_erase.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test_pf20_cmp_less_bool.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test_pf26_saturating_add_bool.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test_pf26_saturating_add_const_bool.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test_pf26_saturating_add_mixed_types.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_span_empty.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_unbounded_variant_footprint_get_const.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_unbounded_variant_footprint_get_non_const.cpp diff --git a/cetlvast/suites/compile/test_pf20_cmp_less_bool.cpp b/cetlvast/suites/compile/test_pf20_cmp_less_bool.cpp new file mode 100644 index 00000000..7ab7df61 --- /dev/null +++ b/cetlvast/suites/compile/test_pf20_cmp_less_bool.cpp @@ -0,0 +1,19 @@ +/// @file +/// Compile test that ensures bool is not accepted by the integer comparison functions, as in C++20. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT +/// +#include "cetl/pf20/utility.hpp" + +int main() +{ +#ifndef CETLVAST_COMPILETEST_PRECHECK + // Mandated: bool is not a standard or extended integer type, so this fails a static assertion. + return cetl::pf20::cmp_less(false, 1) ? 0 : 1; +#else + return cetl::pf20::cmp_less(0, 1) ? 0 : 1; +#endif +} diff --git a/cetlvast/suites/compile/test_pf26_saturating_add_bool.cpp b/cetlvast/suites/compile/test_pf26_saturating_add_bool.cpp new file mode 100644 index 00000000..8d0d9a3f --- /dev/null +++ b/cetlvast/suites/compile/test_pf26_saturating_add_bool.cpp @@ -0,0 +1,19 @@ +/// @file +/// Compile test that ensures bool is not accepted by the saturation arithmetic functions, as in C++26. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT +/// +#include "cetl/pf26/numeric.hpp" + +int main() +{ +#ifndef CETLVAST_COMPILETEST_PRECHECK + // bool is not a signed or unsigned integer type: no matching function. + return cetl::pf26::saturating_add(true, true) ? 0 : 1; +#else + return cetl::pf26::saturating_add(1, 1) - 2; +#endif +} diff --git a/cetlvast/suites/compile/test_pf26_saturating_add_const_bool.cpp b/cetlvast/suites/compile/test_pf26_saturating_add_const_bool.cpp new file mode 100644 index 00000000..19c87d8f --- /dev/null +++ b/cetlvast/suites/compile/test_pf26_saturating_add_const_bool.cpp @@ -0,0 +1,19 @@ +/// @file +/// Compile test that ensures cv-qualified bool is rejected by the saturation arithmetic functions as plain bool is. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT +/// +#include "cetl/pf26/numeric.hpp" + +int main() +{ +#ifndef CETLVAST_COMPILETEST_PRECHECK + // const bool is still bool: no matching function. + return cetl::pf26::saturating_add(true, true) ? 0 : 1; +#else + return cetl::pf26::saturating_add(1, 1) - 2; +#endif +} diff --git a/cetlvast/suites/compile/test_pf26_saturating_add_mixed_types.cpp b/cetlvast/suites/compile/test_pf26_saturating_add_mixed_types.cpp new file mode 100644 index 00000000..9c0b952d --- /dev/null +++ b/cetlvast/suites/compile/test_pf26_saturating_add_mixed_types.cpp @@ -0,0 +1,19 @@ +/// @file +/// Compile test that ensures operands of different types are rejected rather than promoted, as in C++26. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT +/// +#include "cetl/pf26/numeric.hpp" + +int main() +{ +#ifndef CETLVAST_COMPILETEST_PRECHECK + // Deduction of T from int and long conflicts, exactly as for std::min(1, 1L). + return static_cast(cetl::pf26::saturating_add(1, 1L)) - 2; +#else + return static_cast(cetl::pf26::saturating_add(1, 1L)) - 2; +#endif +} diff --git a/cetlvast/suites/docs/examples/CMakeLists.txt b/cetlvast/suites/docs/examples/CMakeLists.txt index c9ebfbf3..d43755bc 100644 --- a/cetlvast/suites/docs/examples/CMakeLists.txt +++ b/cetlvast/suites/docs/examples/CMakeLists.txt @@ -33,6 +33,7 @@ set(NATIVE_EXAMPLES example_10_unbounded_variant.cpp example_11_memory_resource_o1heap.cpp example_12_buddy_memory_resource.cpp + example_13_saturating_arithmetic.cpp ) set(ALL_EXAMPLES "") diff --git a/cetlvast/suites/docs/examples/example_13_saturating_arithmetic.cpp b/cetlvast/suites/docs/examples/example_13_saturating_arithmetic.cpp new file mode 100644 index 00000000..e351d9bd --- /dev/null +++ b/cetlvast/suites/docs/examples/example_13_saturating_arithmetic.cpp @@ -0,0 +1,71 @@ +/// @file +/// Example of using the CETL C++26 polyfill header for saturation arithmetic. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT +/// +#include "cetl/pf26/cetlpf.hpp" +#include "cetl/pf17/cetlpf.hpp" + +#include +#include +#include +#include + +#include + +TEST(example_13_saturating_arithmetic, saturating_add) +{ + //! [example_13_saturating_add] + // A drop counter that must stop at its maximum instead of wrapping back to zero. + // The cetl/pf26/cetlpf.hpp header provides cetl::saturating_add from the standard library when it has it + // (under either the 2023 or the final 2026 names) and from cetl::pf26 otherwise. + std::uint16_t dropped = 65530U; + for (int i = 0; i < 10; ++i) + { + dropped = cetl::saturating_add(dropped, std::uint16_t{1}); + } + std::cout << "dropped: " << dropped << std::endl; // 65535, not 4 + //! [example_13_saturating_add] + ASSERT_EQ(65535U, dropped); +} + +TEST(example_13_saturating_arithmetic, saturating_cast) +{ + //! [example_13_saturating_cast] + // Uptime in seconds is a 32-bit field on the wire (for example uavcan.node.Heartbeat). A static_cast would + // wrap after 136 years; saturating_cast pins the value at the field's maximum instead. + const std::chrono::seconds uptime{INT64_C(5000000000)}; + const std::uint32_t field = cetl::saturating_cast(uptime.count()); + std::cout << "uptime field: " << field << std::endl; // 4294967295 + //! [example_13_saturating_cast] + ASSERT_EQ(std::numeric_limits::max(), field); +} + +TEST(example_13_saturating_arithmetic, operands_share_one_type) +{ + //! [example_13_one_type] + // As with std::min and std::max, both operands share one type and nothing is promoted. Name the type when the + // operands differ; otherwise the call does not compile. + const std::int8_t a = 100; + const std::int8_t sum = cetl::saturating_add(a, 100); // 127 + //! [example_13_one_type] + ASSERT_EQ(127, sum); +} + +TEST(example_13_saturating_arithmetic, clamp_and_in_range) +{ + //! [example_13_clamp] + // A value destined for a 10-bit signed field (DSDL `saturated int10`) has no matching fundamental type, so it is + // clamped to the field's range with std::clamp, which cetl/pf17/cetlpf.hpp provides for C++14... + const std::int16_t raw = 0x5555; + const std::int16_t field = cetl::clamp(raw, std::int16_t{-512}, std::int16_t{511}); // 511 + // ...and std::in_range (cetl/pf20/cetlpf.hpp) answers "would saturating_cast change this value?" without + // performing the conversion. + const bool fits = cetl::in_range(raw); // false + //! [example_13_clamp] + ASSERT_EQ(511, field); + ASSERT_FALSE(fits); +} diff --git a/cetlvast/suites/fuzz/CMakeLists.txt b/cetlvast/suites/fuzz/CMakeLists.txt index 2e7dd041..d647ea00 100644 --- a/cetlvast/suites/fuzz/CMakeLists.txt +++ b/cetlvast/suites/fuzz/CMakeLists.txt @@ -35,24 +35,33 @@ if(NOT CETLVAST_HAS_LIBFUZZER) message(FATAL_ERROR "Clang cannot link -fsanitize=fuzzer,address,undefined; install the compiler-rt runtimes.") endif() -add_executable(fuzz_variable_length_array fuzz_variable_length_array.cpp) -target_link_libraries(fuzz_variable_length_array PRIVATE cetl) -target_compile_options(fuzz_variable_length_array PRIVATE - -O1 -g -fno-omit-frame-pointer -fsanitize=fuzzer,address,undefined -fno-sanitize-recover=all - -Wall -Wextra -Wpedantic -Werror) -target_link_options(fuzz_variable_length_array PRIVATE - -fsanitize=fuzzer,address,undefined -fno-sanitize-recover=all) - -# Keep generated inputs and crash artifacts in the build tree, leaving the seed corpus unchanged. -set(FUZZ_WORK_DIR "${CMAKE_CURRENT_BINARY_DIR}/variable_length_array") -file(MAKE_DIRECTORY "${FUZZ_WORK_DIR}/corpus" "${FUZZ_WORK_DIR}/artifacts") -set(FUZZ_SMOKE_ARGS - "${FUZZ_WORK_DIR}/corpus" "${CMAKE_CURRENT_SOURCE_DIR}/corpus/variable_length_array" - -runs=10000 -seed=1 -max_len=1024 -timeout=10 - "-artifact_prefix=${FUZZ_WORK_DIR}/artifacts/") -add_test(NAME fuzz_variable_length_array_smoke COMMAND fuzz_variable_length_array ${FUZZ_SMOKE_ARGS}) -set_tests_properties(fuzz_variable_length_array_smoke PROPERTIES LABELS fuzz TIMEOUT 120) +# Each target is a libFuzzer entry point with a seed corpus in corpus/; generated inputs and crash artifacts +# stay in the build tree so the seed corpus is left unchanged. +set(FUZZ_TARGETS + variable_length_array + saturation_arithmetic +) +set(FUZZ_SMOKE_COMMANDS "") +foreach(FUZZ_NAME ${FUZZ_TARGETS}) + set(FUZZ_TARGET "fuzz_${FUZZ_NAME}") + add_executable(${FUZZ_TARGET} ${FUZZ_TARGET}.cpp) + target_link_libraries(${FUZZ_TARGET} PRIVATE cetl) + target_compile_options(${FUZZ_TARGET} PRIVATE + -O1 -g -fno-omit-frame-pointer -fsanitize=fuzzer,address,undefined -fno-sanitize-recover=all + -Wall -Wextra -Wpedantic -Werror) + target_link_options(${FUZZ_TARGET} PRIVATE + -fsanitize=fuzzer,address,undefined -fno-sanitize-recover=all) + set(FUZZ_WORK_DIR "${CMAKE_CURRENT_BINARY_DIR}/${FUZZ_NAME}") + file(MAKE_DIRECTORY "${FUZZ_WORK_DIR}/corpus" "${FUZZ_WORK_DIR}/artifacts") + set(FUZZ_SMOKE_ARGS + "${FUZZ_WORK_DIR}/corpus" "${CMAKE_CURRENT_SOURCE_DIR}/corpus/${FUZZ_NAME}" + -runs=10000 -seed=1 -max_len=1024 -timeout=10 + "-artifact_prefix=${FUZZ_WORK_DIR}/artifacts/") + add_test(NAME ${FUZZ_TARGET}_smoke COMMAND ${FUZZ_TARGET} ${FUZZ_SMOKE_ARGS}) + set_tests_properties(${FUZZ_TARGET}_smoke PROPERTIES LABELS fuzz TIMEOUT 120) + list(APPEND FUZZ_SMOKE_COMMANDS COMMAND $ ${FUZZ_SMOKE_ARGS}) +endforeach() add_custom_target(run_fuzztests - COMMAND $ ${FUZZ_SMOKE_ARGS} - DEPENDS fuzz_variable_length_array + ${FUZZ_SMOKE_COMMANDS} + DEPENDS fuzz_variable_length_array fuzz_saturation_arithmetic USES_TERMINAL VERBATIM) diff --git a/cetlvast/suites/fuzz/corpus/saturation_arithmetic/int32_min_div_neg1 b/cetlvast/suites/fuzz/corpus/saturation_arithmetic/int32_min_div_neg1 new file mode 100644 index 00000000..c80321b8 Binary files /dev/null and b/cetlvast/suites/fuzz/corpus/saturation_arithmetic/int32_min_div_neg1 differ diff --git a/cetlvast/suites/fuzz/corpus/saturation_arithmetic/int64_min_times_neg1 b/cetlvast/suites/fuzz/corpus/saturation_arithmetic/int64_min_times_neg1 new file mode 100644 index 00000000..5d8d55ea Binary files /dev/null and b/cetlvast/suites/fuzz/corpus/saturation_arithmetic/int64_min_times_neg1 differ diff --git a/cetlvast/suites/fuzz/corpus/saturation_arithmetic/int8_min_minus_one b/cetlvast/suites/fuzz/corpus/saturation_arithmetic/int8_min_minus_one new file mode 100644 index 00000000..9c58e7dd Binary files /dev/null and b/cetlvast/suites/fuzz/corpus/saturation_arithmetic/int8_min_minus_one differ diff --git a/cetlvast/suites/fuzz/corpus/saturation_arithmetic/uint64_max_times_two b/cetlvast/suites/fuzz/corpus/saturation_arithmetic/uint64_max_times_two new file mode 100644 index 00000000..a4241b8d Binary files /dev/null and b/cetlvast/suites/fuzz/corpus/saturation_arithmetic/uint64_max_times_two differ diff --git a/cetlvast/suites/fuzz/corpus/saturation_arithmetic/uint8_max_plus_one b/cetlvast/suites/fuzz/corpus/saturation_arithmetic/uint8_max_plus_one new file mode 100644 index 00000000..66455be8 Binary files /dev/null and b/cetlvast/suites/fuzz/corpus/saturation_arithmetic/uint8_max_plus_one differ diff --git a/cetlvast/suites/fuzz/fuzz_saturation_arithmetic.cpp b/cetlvast/suites/fuzz/fuzz_saturation_arithmetic.cpp new file mode 100644 index 00000000..036f569d --- /dev/null +++ b/cetlvast/suites/fuzz/fuzz_saturation_arithmetic.cpp @@ -0,0 +1,139 @@ +/// @file +/// Differential fuzz test: the cetl::pf26 saturation functions (both the builtin-backed and the portable +/// implementations) against a 128-bit oracle, for every fixed-width integer type. +/// +/// Input: byte 0 selects the operand type (low three bits); bytes 1..16 are the two operands. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// SPDX-License-Identifier: MIT + +#include "cetl/pf26/numeric.hpp" + +#include +#include +#include +#include +#include +#include + +__extension__ typedef __int128 fuzz_int128; +__extension__ typedef unsigned __int128 fuzz_uint128; + +namespace +{ + +template +using wide_t = typename std::conditional::value, fuzz_int128, fuzz_uint128>::type; + +template +T clamp_wide(fuzz_int128 v) +{ + const fuzz_int128 lo = std::numeric_limits::min(); + const fuzz_int128 hi = std::numeric_limits::max(); + return static_cast(std::min(std::max(v, lo), hi)); +} + +template +void require(bool condition, const char* what, T x, T y) +{ + if (!condition) + { + std::fprintf(stderr, + "saturation fuzz invariant failed: %s (%zu-byte %s operands %lld, %lld)\n", + what, + sizeof(T), + std::is_signed::value ? "signed" : "unsigned", + static_cast(x), + static_cast(y)); + __builtin_trap(); + } +} + +template +void check_cast(From x) +{ + const fuzz_int128 expected = clamp_wide(static_cast(x)); + require(static_cast(cetl::pf26::saturating_cast(x)) == expected, "saturating_cast", x, x); +} + +template +void check(const std::uint8_t* const operands) +{ + T x{}; + T y{}; + std::memcpy(&x, operands, sizeof(T)); + std::memcpy(&y, operands + 8, sizeof(T)); + + const fuzz_int128 a = x; + const fuzz_int128 b = y; + const T sum = clamp_wide(a + b); + require(cetl::pf26::saturating_add(x, y) == sum, "saturating_add", x, y); + require(cetl::pf26::detail::saturating_add_portable(x, y) == sum, "saturating_add_portable", x, y); + + const T difference = clamp_wide(a - b); + require(cetl::pf26::saturating_sub(x, y) == difference, "saturating_sub", x, y); + require(cetl::pf26::detail::saturating_sub_portable(x, y) == difference, "saturating_sub_portable", x, y); + + // The product of two 64-bit values fits in the 128-bit type of the same signedness. + const wide_t product_wide = static_cast>(x) * static_cast>(y); + const T product = std::is_signed::value + ? clamp_wide(static_cast(product_wide)) + : static_cast(std::min(static_cast(product_wide), + static_cast(std::numeric_limits::max()))); + require(cetl::pf26::saturating_mul(x, y) == product, "saturating_mul", x, y); + require(cetl::pf26::detail::saturating_mul_portable(x, y) == product, "saturating_mul_portable", x, y); + + if (y != 0) + { + require(cetl::pf26::saturating_div(x, y) == clamp_wide(a / b), "saturating_div", x, y); + } + + check_cast(x); + check_cast(x); + check_cast(x); + check_cast(x); + check_cast(x); + check_cast(x); + check_cast(x); + check_cast(x); +} + +} // namespace + +extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t* data, std::size_t size) +{ + if (size < 17) + { + return 0; + } + const std::uint8_t* const operands = data + 1; + switch (data[0] & 7U) + { + case 0: + check(operands); + break; + case 1: + check(operands); + break; + case 2: + check(operands); + break; + case 3: + check(operands); + break; + case 4: + check(operands); + break; + case 5: + check(operands); + break; + case 6: + check(operands); + break; + default: + check(operands); + break; + } + return 0; +} diff --git a/cetlvast/suites/unittest/CMakeLists.txt b/cetlvast/suites/unittest/CMakeLists.txt index 9efa983c..32c64a19 100644 --- a/cetlvast/suites/unittest/CMakeLists.txt +++ b/cetlvast/suites/unittest/CMakeLists.txt @@ -86,6 +86,7 @@ set(NATIVE_TESTS test_buffer_memory_resource.cpp test_deprecated_names.cpp test_o1heap_memory_resource.cpp + test_pf17_algorithm.cpp test_pf17_new.cpp test_pf17_memory_resource.cpp test_pf17_monotonic_buffer_resource.cpp @@ -108,6 +109,8 @@ set(NATIVE_TESTS test_pf20_span_asserts.cpp test_pf20_span.cpp test_pf20.cpp + test_pf20_utility.cpp + test_pf26_numeric.cpp test_rtti.cpp test_type_traits_ext.cpp test_typelist.cpp diff --git a/cetlvast/suites/unittest/test_pf17_algorithm.cpp b/cetlvast/suites/unittest/test_pf17_algorithm.cpp new file mode 100644 index 00000000..dce65bb3 --- /dev/null +++ b/cetlvast/suites/unittest/test_pf17_algorithm.cpp @@ -0,0 +1,94 @@ +/// @file +/// Unit tests for cetl/pf17/algorithm.hpp +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#include "cetlvast/helpers_gtest.hpp" +#include "cetl/pf17/algorithm.hpp" +#include "cetl/pf17/cetlpf.hpp" + +#include +#include + +namespace +{ + +// +----------------------------------------------------------------------+ +// | Constant expressions +// +----------------------------------------------------------------------+ +static_assert(cetl::pf17::clamp(5, 1, 3) == 3, "above hi"); +static_assert(cetl::pf17::clamp(-5, 1, 3) == 1, "below lo"); +static_assert(cetl::pf17::clamp(2, 1, 3) == 2, "inside"); +static_assert(cetl::pf17::clamp(1, 1, 3) == 1, "at lo"); +static_assert(cetl::pf17::clamp(3, 1, 3) == 3, "at hi"); +static_assert(cetl::pf17::clamp(2, 2, 2) == 2, "degenerate range"); +static_assert(cetl::pf17::clamp(5, 3, 1, std::greater{}) == 3, "reversed comparison, above"); +static_assert(cetl::pf17::clamp(0, 3, 1, std::greater{}) == 1, "reversed comparison, below"); +static_assert(cetl::pf17::clamp(2, 3, 1, std::greater{}) == 2, "reversed comparison, inside"); +static_assert(cetl::clamp(5, 1, 3) == 3, "cetlpf alias"); + +// +----------------------------------------------------------------------+ +// | Reference semantics +// +----------------------------------------------------------------------+ +TEST(PF17AlgorithmTest, ClampReturnsReferenceToArgument) +{ + const int v = 2; + const int lo = 1; + const int hi = 3; + const int below = 0; + const int above = 9; + EXPECT_EQ(&v, &cetl::pf17::clamp(v, lo, hi)); + EXPECT_EQ(&lo, &cetl::pf17::clamp(below, lo, hi)); + EXPECT_EQ(&hi, &cetl::pf17::clamp(above, lo, hi)); + // At a bound, v itself is returned (it is not less than lo and hi is not less than it). + EXPECT_EQ(&lo, &cetl::pf17::clamp(lo, lo, hi)); + EXPECT_NE(&lo, &cetl::pf17::clamp(v, v, hi)); + EXPECT_EQ(&v, &cetl::pf17::clamp(v, v, hi)); +} + +TEST(PF17AlgorithmTest, ClampWithComparator) +{ + const auto by_length = [](const std::string& a, const std::string& b) { return a.size() < b.size(); }; + const std::string lo{"ab"}; + const std::string hi{"abcd"}; + const std::string too_long{"abcdef"}; + const std::string too_short{"a"}; + const std::string inside{"abc"}; + EXPECT_EQ(hi, cetl::pf17::clamp(too_long, lo, hi, by_length)); + EXPECT_EQ(lo, cetl::pf17::clamp(too_short, lo, hi, by_length)); + EXPECT_EQ(inside, cetl::pf17::clamp(inside, lo, hi, by_length)); +} + +TEST(PF17AlgorithmTest, ClampFloatingPoint) +{ + EXPECT_DOUBLE_EQ(1.5, cetl::pf17::clamp(1.5, 1.0, 2.0)); + EXPECT_DOUBLE_EQ(1.0, cetl::pf17::clamp(-1.5, 1.0, 2.0)); + EXPECT_DOUBLE_EQ(2.0, cetl::pf17::clamp(100.0, 1.0, 2.0)); +} + +// +----------------------------------------------------------------------+ +// | Precondition +// +----------------------------------------------------------------------+ +#if defined(CETL_ENABLE_DEBUG_ASSERT) && CETL_ENABLE_DEBUG_ASSERT + +static void TestClampWithInvertedBounds() +{ + flush_coverage_on_death(); + const int lo = 3; + const int hi = 1; + const int v = 2; + const int& result = cetl::pf17::clamp(v, lo, hi); + (void) result; +} + +TEST(DeathTestPF17Algorithm, ClampWithInvertedBounds) +{ + EXPECT_DEATH(TestClampWithInvertedBounds(), "CDE_algorithm_001"); +} + +#endif // CETL_ENABLE_DEBUG_ASSERT + +} // namespace diff --git a/cetlvast/suites/unittest/test_pf20_utility.cpp b/cetlvast/suites/unittest/test_pf20_utility.cpp new file mode 100644 index 00000000..8c36b0b8 --- /dev/null +++ b/cetlvast/suites/unittest/test_pf20_utility.cpp @@ -0,0 +1,236 @@ +/// @file +/// Unit tests for cetl/pf20/utility.hpp +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#include "cetlvast/helpers_gtest.hpp" +#include "cetl/pf20/utility.hpp" +#include "cetl/pf20/cetlpf.hpp" + +#include +#include +#include +#include +#include + +namespace +{ +using cetl::pf20::cmp_equal; +using cetl::pf20::cmp_greater; +using cetl::pf20::cmp_greater_equal; +using cetl::pf20::cmp_less; +using cetl::pf20::cmp_less_equal; +using cetl::pf20::cmp_not_equal; +using cetl::pf20::in_range; + +// +----------------------------------------------------------------------+ +// | Constant expressions: the cases where the built-in operators, after the usual arithmetic conversions, give the +// | mathematically wrong answer. +// +----------------------------------------------------------------------+ +static_assert(cmp_less(-1, 1U), "-1 < 1U is false with the built-in operator"); +static_assert(cmp_greater(1U, -1), ""); +static_assert(!cmp_equal(-1, std::numeric_limits::max()), + "-1 == UINT_MAX is true with the built-in operator"); +static_assert(cmp_not_equal(-1, std::numeric_limits::max()), ""); +static_assert(cmp_equal(1, 1U), ""); +static_assert(cmp_equal(std::int8_t{-1}, std::int64_t{-1}), ""); +static_assert(cmp_less_equal(std::int8_t{-1}, std::uint64_t{0}), ""); +static_assert(cmp_greater_equal(std::uint8_t{0}, std::int64_t{-1}), ""); +static_assert(!cmp_less(std::numeric_limits::max(), std::numeric_limits::max()), ""); +static_assert(cmp_greater(std::numeric_limits::max(), std::numeric_limits::max()), ""); +static_assert(cmp_less(std::numeric_limits::min(), std::uint8_t{0}), ""); + +static_assert(in_range(255), ""); +static_assert(!in_range(256), ""); +static_assert(!in_range(-1), ""); +static_assert(in_range(-128), ""); +static_assert(!in_range(-129), ""); +static_assert(!in_range(128), ""); +static_assert(!in_range(-1), ""); +static_assert(in_range(std::numeric_limits::max()), ""); +static_assert(!in_range(std::numeric_limits::max()), ""); +static_assert(in_range(std::numeric_limits::max()), ""); +static_assert(in_range(-32768), "cv-qualified R"); + +// cv-qualified explicit template arguments name the same integer types. +static_assert(cmp_less(1, 2L), ""); +static_assert(cmp_equal(1U, 1), ""); +static_assert(in_range(255), ""); + +#if defined(__SIZEOF_INT128__) +// Extended 128-bit integers, including GCC's strict -std=c++NN modes where std::is_integral does not report them. +__extension__ typedef __int128 test_int128; +__extension__ typedef unsigned __int128 test_uint128; +static_assert(cmp_less(std::numeric_limits::min(), test_int128{0}), ""); +static_assert(cmp_less(test_int128{-1}, std::uint8_t{0}), ""); +static_assert(cmp_equal(test_int128{-1}, std::int8_t{-1}), ""); +static_assert(!cmp_equal(test_int128{-1}, ~test_uint128{0}), ""); +static_assert(cmp_greater(static_cast(std::numeric_limits::max()) + 1, + std::numeric_limits::max()), + ""); +static_assert(cmp_greater_equal(~test_uint128{0}, test_int128{-1}), ""); +static_assert(in_range(std::numeric_limits::max()), ""); +static_assert(!in_range(static_cast(1) << 70), ""); +static_assert(!in_range(-1), ""); +static_assert(in_range(std::numeric_limits::max()), ""); +static_assert(in_range(test_int128{-128}), ""); +static_assert(!in_range(test_int128{128}), ""); +#endif + +static_assert(cetl::cmp_less(-1, 1U), "cetlpf alias"); +static_assert(!cetl::in_range(256), "cetlpf alias"); + +// +----------------------------------------------------------------------+ +// | Sweep over a value set for every pair of fixed-width integer types, against an oracle that compares the +// | mathematical values by first looking at the signs. +// +----------------------------------------------------------------------+ +template +constexpr bool is_negative(T t, std::true_type) +{ + return t < 0; +} +template +constexpr bool is_negative(T, std::false_type) +{ + return false; +} +template +constexpr bool is_negative(T t) +{ + return is_negative(t, std::is_signed{}); +} + +/// -1, 0 or +1 for t <=> u using the mathematical values. +template +int oracle_compare(T t, U u) +{ + const bool t_negative = is_negative(t); + const bool u_negative = is_negative(u); + if (t_negative != u_negative) + { + return t_negative ? -1 : 1; + } + if (t_negative) + { + // Both negative, so both signed and both representable as long long. + const long long a = static_cast(t); + const long long b = static_cast(u); + return (a < b) ? -1 : ((a > b) ? 1 : 0); + } + // Both non-negative, so both representable as unsigned long long. + const unsigned long long a = static_cast(t); + const unsigned long long b = static_cast(u); + return (a < b) ? -1 : ((a > b) ? 1 : 0); +} + +template +bool fits(long long candidate) +{ + if (std::is_signed::value) + { + return (candidate >= static_cast(std::numeric_limits::min())) && + (candidate <= static_cast(std::numeric_limits::max())); + } + return (candidate >= 0) && (static_cast(candidate) <= + static_cast(std::numeric_limits::max())); +} + +/// The limits of T plus every boundary of the narrower fixed-width types that T can represent. +template +std::vector samples() +{ + static const long long candidates[] = {-4294967296LL, -2147483649LL, -2147483648LL, -65537LL, -65536LL, -32769LL, + -32768LL, -257LL, -256LL, -129LL, -128LL, -2LL, + -1LL, 0LL, 1LL, 2LL, 127LL, 128LL, + 255LL, 256LL, 32767LL, 32768LL, 65535LL, 65536LL, + 2147483647LL, 2147483648LL, 4294967295LL, 4294967296LL}; + std::vector out{std::numeric_limits::min(), + static_cast(std::numeric_limits::min() + 1), + static_cast(std::numeric_limits::max() - 1), + std::numeric_limits::max()}; + for (const long long candidate : candidates) + { + if (fits(candidate)) + { + out.push_back(static_cast(candidate)); + } + } + return out; +} + +template +std::string show(T value, std::true_type) +{ + return std::to_string(static_cast(value)); +} +template +std::string show(T value, std::false_type) +{ + return std::to_string(static_cast(value)); +} +template +std::string show(T value) +{ + return show(value, std::is_signed{}); +} + +template +void check_pair() +{ + for (const T t : samples()) + { + for (const U u : samples()) + { + const int c = oracle_compare(t, u); + const std::string context = show(t) + " vs " + show(u); + EXPECT_EQ(c == 0, cmp_equal(t, u)) << context; + EXPECT_EQ(c != 0, cmp_not_equal(t, u)) << context; + EXPECT_EQ(c < 0, cmp_less(t, u)) << context; + EXPECT_EQ(c > 0, cmp_greater(t, u)) << context; + EXPECT_EQ(c <= 0, cmp_less_equal(t, u)) << context; + EXPECT_EQ(c >= 0, cmp_greater_equal(t, u)) << context; + const bool u_fits_in_t = (oracle_compare(std::numeric_limits::min(), u) <= 0) && + (oracle_compare(u, std::numeric_limits::max()) <= 0); + EXPECT_EQ(u_fits_in_t, in_range(u)) << context; + } + } +} + +template +void check_against_all() +{ + check_pair(); + check_pair(); + check_pair(); + check_pair(); + check_pair(); + check_pair(); + check_pair(); + check_pair(); +} + +TEST(PF20UtilityTest, IntegerComparisonsAgainstOracle) +{ + check_against_all(); + check_against_all(); + check_against_all(); + check_against_all(); + check_against_all(); + check_against_all(); + check_against_all(); + check_against_all(); +} + +TEST(PF20UtilityTest, NonFixedWidthTypes) +{ + EXPECT_TRUE(cmp_less(-1, 1UL)); + EXPECT_TRUE(cmp_less(-1LL, 1ULL)); + EXPECT_TRUE(cmp_greater(std::numeric_limits::max(), -1L)); + EXPECT_FALSE(in_range(-1)); + EXPECT_TRUE(in_range(std::numeric_limits::min())); +} + +} // namespace diff --git a/cetlvast/suites/unittest/test_pf26_numeric.cpp b/cetlvast/suites/unittest/test_pf26_numeric.cpp new file mode 100644 index 00000000..2ce0a382 --- /dev/null +++ b/cetlvast/suites/unittest/test_pf26_numeric.cpp @@ -0,0 +1,530 @@ +/// @file +/// Unit tests for cetl/pf26/numeric.hpp and the saturation arithmetic selected by cetl/pf26/cetlpf.hpp. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#include "cetlvast/helpers_gtest.hpp" +#include "cetl/pf26/numeric.hpp" +#include "cetl/pf26/cetlpf.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#if defined(__SIZEOF_INT128__) +// 128-bit integers are an extension; they are only used as a test oracle for the 64-bit types. +__extension__ typedef __int128 cetlvast_int128; +__extension__ typedef unsigned __int128 cetlvast_uint128; +# define CETLVAST_HAS_INT128 1 +#else +# define CETLVAST_HAS_INT128 0 +#endif + +namespace +{ +using cetl::pf26::saturating_add; +using cetl::pf26::saturating_cast; +using cetl::pf26::saturating_div; +using cetl::pf26::saturating_mul; +using cetl::pf26::saturating_sub; +using cetl::pf26::detail::saturating_add_portable; +using cetl::pf26::detail::saturating_mul_portable; +using cetl::pf26::detail::saturating_sub_portable; + +constexpr std::int64_t i64_min = std::numeric_limits::min(); +constexpr std::int64_t i64_max = std::numeric_limits::max(); +constexpr std::uint64_t u64_max = std::numeric_limits::max(); + +// +----------------------------------------------------------------------+ +// | Constant expressions ([numeric.sat] examples and the classic edge cases) +// +----------------------------------------------------------------------+ +static_assert(saturating_add(100, 100) == 127, ""); +static_assert(saturating_add(-100, -100) == -128, ""); +static_assert(saturating_add(-128, 127) == -1, ""); +static_assert(saturating_add(250, 10) == 255, ""); +static_assert(saturating_add(i64_max, 1) == i64_max, ""); +static_assert(saturating_add(i64_min, -1) == i64_min, ""); +static_assert(saturating_add(u64_max, 1) == u64_max, ""); +static_assert(saturating_add(3, 4) == 7, ""); + +static_assert(saturating_sub(3, 5) == 0, ""); +static_assert(saturating_sub(-128, 1) == -128, ""); +static_assert(saturating_sub(127, -1) == 127, ""); +static_assert(saturating_sub(-128, -128) == 0, ""); +static_assert(saturating_sub(0, i64_min) == i64_max, ""); +static_assert(saturating_sub(i64_min, i64_max) == i64_min, ""); +static_assert(saturating_sub(0, 1) == 0, ""); + +static_assert(saturating_mul(-128, -1) == 127, ""); +static_assert(saturating_mul(-1, -128) == 127, ""); +static_assert(saturating_mul(100, -100) == -128, ""); +static_assert(saturating_mul(-100, -100) == 127, ""); +static_assert(saturating_mul(-64, 2) == -128, ""); +static_assert(saturating_mul(16, 16) == 255, ""); +static_assert(saturating_mul(15, 17) == 255, ""); +static_assert(saturating_mul(INT64_C(4000000000), INT64_C(4000000000)) == i64_max, ""); +static_assert(saturating_mul(INT64_C(-4000000000), INT64_C(4000000000)) == i64_min, ""); +static_assert(saturating_mul(i64_min, -1) == i64_max, ""); +static_assert(saturating_mul(-3, i64_max) == i64_min, ""); +static_assert(saturating_mul(u64_max, 2) == u64_max, ""); +static_assert(saturating_mul(0, u64_max) == 0, ""); + +static_assert(saturating_div(-128, -1) == 127, ""); +static_assert(saturating_div(7, -2) == -3, "truncation toward zero"); +static_assert(saturating_div(std::numeric_limits::min(), -1) == + std::numeric_limits::max(), + ""); +static_assert(saturating_div(i64_min, -1) == i64_max, ""); +static_assert(saturating_div(i64_min, 1) == i64_min, ""); +static_assert(saturating_div(255, 1) == 255, ""); +static_assert(saturating_div(u64_max, u64_max) == 1, ""); + +static_assert(saturating_cast(696) == 127, ""); +static_assert(saturating_cast(696) == 255, ""); +static_assert(saturating_cast(-696) == -128, ""); +static_assert(saturating_cast(-696) == 0, ""); +static_assert(saturating_cast(std::uint64_t{5}) == 5, ""); +static_assert(saturating_cast(std::uint32_t{5}) == 5, ""); +static_assert(saturating_cast(std::uint32_t{0x80000000U}) == std::numeric_limits::max(), + ""); +static_assert(saturating_cast(std::int8_t{-1}) == 0, ""); +static_assert(saturating_cast(std::uint8_t{200}) == 127, ""); +static_assert(saturating_cast(u64_max) == i64_max, ""); +static_assert(saturating_cast(i64_min) == 0, ""); +static_assert(saturating_cast(i64_max) == static_cast(i64_max), ""); +static_assert(saturating_cast(i64_min) == -128, ""); +static_assert(saturating_cast(std::int64_t{-1}) == 0, ""); +static_assert(saturating_cast(INT64_C(4294967296)) == std::numeric_limits::max(), ""); +static_assert(saturating_cast(std::int64_t{42}) == 42, "same type"); + +// cv-qualified explicit template arguments name the same integer types. +static_assert(saturating_add(100, 100) == 127, ""); +static_assert(saturating_cast(-1) == 0, ""); + +#if CETLVAST_HAS_INT128 +// Extended 128-bit integers, including GCC's strict -std=c++NN modes where std::numeric_limits does not describe them. +constexpr cetlvast_int128 i128_max = static_cast(~cetlvast_uint128{0} >> 1); +constexpr cetlvast_int128 i128_min = -i128_max - 1; +constexpr cetlvast_uint128 u128_max = ~cetlvast_uint128{0}; +static_assert(saturating_add(i128_max, cetlvast_int128{1}) == i128_max, ""); +static_assert(saturating_add(i128_min, cetlvast_int128{-1}) == i128_min, ""); +static_assert(saturating_add(cetlvast_int128{1} << 100, cetlvast_int128{1} << 100) == cetlvast_int128{1} << 101, ""); +static_assert(saturating_sub(i128_min, cetlvast_int128{1}) == i128_min, ""); +static_assert(saturating_sub(cetlvast_int128{0}, i128_min) == i128_max, ""); +static_assert(saturating_mul(i128_min, cetlvast_int128{-1}) == i128_max, ""); +static_assert(saturating_mul(cetlvast_int128{1} << 64, cetlvast_int128{1} << 64) == i128_max, ""); +static_assert(saturating_mul(cetlvast_int128{1} << 63, cetlvast_int128{1} << 63) == cetlvast_int128{1} << 126, ""); +static_assert(saturating_mul(-(cetlvast_int128{1} << 64), cetlvast_int128{1} << 64) == i128_min, ""); +static_assert(saturating_div(i128_min, cetlvast_int128{-1}) == i128_max, ""); +static_assert(saturating_div(i128_min, cetlvast_int128{2}) == -(cetlvast_int128{1} << 126), ""); +static_assert(saturating_add(u128_max, cetlvast_uint128{1}) == u128_max, ""); +static_assert(saturating_sub(cetlvast_uint128{0}, cetlvast_uint128{1}) == 0, ""); +static_assert(saturating_mul(u128_max, cetlvast_uint128{2}) == u128_max, ""); +static_assert(saturating_cast(cetlvast_int128{1} << 70) == i64_max, ""); +static_assert(saturating_cast(i128_min) == i64_min, ""); +static_assert(saturating_cast(cetlvast_int128{-1}) == 0, ""); +static_assert(saturating_cast(u128_max) == 255, ""); +static_assert(saturating_cast(u64_max) == static_cast(u64_max), ""); +static_assert(saturating_cast(i64_min) == 0, ""); +static_assert(saturating_cast(u128_max) == i128_max, ""); +static_assert(saturating_add_portable(i128_max, cetlvast_int128{1}) == i128_max, ""); +static_assert(saturating_sub_portable(cetlvast_uint128{0}, cetlvast_uint128{1}) == 0, ""); +static_assert(saturating_mul_portable(i128_min, cetlvast_int128{-1}) == i128_max, ""); +#endif + +// The portable implementations are constant expressions as well. +static_assert(saturating_add_portable(100, 100) == 127, ""); +static_assert(saturating_sub_portable(3, 5) == 0, ""); +static_assert(saturating_mul_portable(i64_min, -1) == i64_max, ""); + +// Nothing is promoted. +static_assert(std::is_same::value, ""); +static_assert(std::is_same::value, ""); +static_assert(std::is_same(std::int64_t{1})), std::uint16_t>::value, ""); + +// Whatever cetl/pf26/cetlpf.hpp selected (std, adapted std or cetl::pf26) agrees with the standard. +static_assert(cetl::saturating_add(100, 100) == 127, ""); +static_assert(cetl::saturating_sub(3, 5) == 0, ""); +static_assert(cetl::saturating_mul(-128, -1) == 127, ""); +static_assert(cetl::saturating_div(-128, -1) == 127, ""); +static_assert(cetl::saturating_cast(-5) == 0, ""); +static_assert(cetl::saturating_cast(696) == 127, ""); +static_assert(std::is_same::value, ""); + +// +----------------------------------------------------------------------+ +// | Oracles. long long is exact for every operation on the types up to 32 bits (signed) and for add/sub on the +// | unsigned 32-bit type; unsigned long long covers unsigned 32-bit multiplication. +// +----------------------------------------------------------------------+ +template +std::string show(T value) +{ + return std::is_signed::value ? std::to_string(static_cast(value)) + : std::to_string(static_cast(value)); +} + +template +long long clamp_ll(long long v) +{ + const long long lo = static_cast(std::numeric_limits::min()); + const long long hi = static_cast(std::numeric_limits::max()); + return std::min(std::max(v, lo), hi); +} + +template +unsigned long long clamp_ull(unsigned long long v) +{ + return std::min(v, static_cast(std::numeric_limits::max())); +} + +/// Checks every operation on one operand pair against the long long oracle (types up to 32 bits only). +template +void check_pair_ll(T x, T y) +{ + static_assert(sizeof(T) <= 4, "long long oracle"); + const long long a = static_cast(x); + const long long b = static_cast(y); + const std::string context = show(x) + ", " + show(y); + ASSERT_EQ(clamp_ll(a + b), static_cast(saturating_add(x, y))) << context; + ASSERT_EQ(clamp_ll(a + b), static_cast(saturating_add_portable(x, y))) << context; + ASSERT_EQ(clamp_ll(a - b), static_cast(saturating_sub(x, y))) << context; + ASSERT_EQ(clamp_ll(a - b), static_cast(saturating_sub_portable(x, y))) << context; + if (std::is_signed::value) + { + ASSERT_EQ(clamp_ll(a * b), static_cast(saturating_mul(x, y))) << context; + ASSERT_EQ(clamp_ll(a * b), static_cast(saturating_mul_portable(x, y))) << context; + } + else + { + const unsigned long long product = static_cast(a) * static_cast(b); + ASSERT_EQ(clamp_ull(product), static_cast(saturating_mul(x, y))) << context; + ASSERT_EQ(clamp_ull(product), static_cast(saturating_mul_portable(x, y))) << context; + } + if (b != 0) + { + ASSERT_EQ(clamp_ll(a / b), static_cast(saturating_div(x, y))) << context; + } +} + +template +std::vector edge_values() +{ + return {std::numeric_limits::min(), + static_cast(std::numeric_limits::min() + 1), + static_cast(0), + static_cast(1), + static_cast(2), + static_cast(-1), + static_cast(-2), + static_cast(std::numeric_limits::max() / 2), + static_cast(std::numeric_limits::max() - 1), + std::numeric_limits::max()}; +} + +// +----------------------------------------------------------------------+ +// | 8-bit types: every operand pair. +// +----------------------------------------------------------------------+ +template +void exhaustive_8bit() +{ + static_assert(sizeof(T) == 1, ""); + const int lo = std::numeric_limits::min(); + const int hi = std::numeric_limits::max(); + for (int a = lo; a <= hi; ++a) + { + for (int b = lo; b <= hi; ++b) + { + check_pair_ll(static_cast(a), static_cast(b)); + } + } +} + +TEST(PF26NumericTest, ExhaustiveInt8) +{ + exhaustive_8bit(); +} + +TEST(PF26NumericTest, ExhaustiveUint8) +{ + exhaustive_8bit(); +} + +// +----------------------------------------------------------------------+ +// | 16-bit types: every value of the first operand against the edge values and a sample of the second. +// +----------------------------------------------------------------------+ +template +void sampled_16bit() +{ + static_assert(sizeof(T) == 2, ""); + std::vector second = edge_values(); + for (long v = std::numeric_limits::min(); v <= std::numeric_limits::max(); v += 257) + { + second.push_back(static_cast(v)); + } + for (long a = std::numeric_limits::min(); a <= std::numeric_limits::max(); ++a) + { + for (const T y : second) + { + check_pair_ll(static_cast(a), y); + } + } +} + +TEST(PF26NumericTest, SampledInt16) +{ + sampled_16bit(); +} + +TEST(PF26NumericTest, SampledUint16) +{ + sampled_16bit(); +} + +// +----------------------------------------------------------------------+ +// | 32-bit types: edge values crossed with each other and with random operands. +// +----------------------------------------------------------------------+ +template +void random_32bit() +{ + static_assert(sizeof(T) == 4, ""); + const std::vector edges = edge_values(); + for (const T x : edges) + { + for (const T y : edges) + { + check_pair_ll(x, y); + } + } + std::mt19937 generator(39U); + std::uniform_int_distribution any(std::numeric_limits::min(), std::numeric_limits::max()); + for (int i = 0; i < 100000; ++i) + { + const T x = any(generator); + const T y = (i % 4 == 0) ? edges[static_cast(i / 4) % edges.size()] : any(generator); + check_pair_ll(x, y); + } +} + +TEST(PF26NumericTest, RandomInt32) +{ + random_32bit(); +} + +TEST(PF26NumericTest, RandomUint32) +{ + random_32bit(); +} + +// +----------------------------------------------------------------------+ +// | 64-bit types: edge values against hand-computed results, and (where the compiler has 128-bit integers) random +// | operands against a 128-bit oracle. +// +----------------------------------------------------------------------+ +TEST(PF26NumericTest, Int64Edges) +{ + EXPECT_EQ(i64_max, saturating_add(i64_max, std::int64_t{1})); + EXPECT_EQ(i64_max, saturating_add_portable(i64_max, std::int64_t{1})); + EXPECT_EQ(i64_min, saturating_add(i64_min, std::int64_t{-1})); + EXPECT_EQ(std::int64_t{-1}, saturating_add(i64_max, i64_min)); + EXPECT_EQ(i64_min, saturating_sub(i64_min, std::int64_t{1})); + EXPECT_EQ(i64_min, saturating_sub_portable(i64_min, std::int64_t{1})); + EXPECT_EQ(i64_max, saturating_sub(std::int64_t{0}, i64_min)); + EXPECT_EQ(i64_max, saturating_sub(i64_max, i64_min)); + EXPECT_EQ(i64_max, saturating_mul(i64_min, std::int64_t{-1})); + EXPECT_EQ(i64_max, saturating_mul_portable(i64_min, std::int64_t{-1})); + EXPECT_EQ(i64_min, saturating_mul(i64_min, std::int64_t{1})); + EXPECT_EQ(i64_min, saturating_mul(std::int64_t{2}, i64_min)); + EXPECT_EQ(i64_max, saturating_mul(std::int64_t{-2}, i64_min)); + EXPECT_EQ(INT64_C(-9223372036854775807), saturating_mul(std::int64_t{-1}, i64_max)); + EXPECT_EQ(i64_max, saturating_div(i64_min, std::int64_t{-1})); + EXPECT_EQ(INT64_C(-9223372036854775807), saturating_div(i64_max, std::int64_t{-1})); + EXPECT_EQ(std::int64_t{0}, saturating_div(std::int64_t{0}, i64_min)); +} + +TEST(PF26NumericTest, Uint64Edges) +{ + EXPECT_EQ(u64_max, saturating_add(u64_max, std::uint64_t{1})); + EXPECT_EQ(u64_max, saturating_add_portable(u64_max, std::uint64_t{1})); + EXPECT_EQ(u64_max, saturating_add(u64_max, u64_max)); + EXPECT_EQ(std::uint64_t{0}, saturating_sub(std::uint64_t{0}, u64_max)); + EXPECT_EQ(std::uint64_t{0}, saturating_sub_portable(std::uint64_t{0}, std::uint64_t{1})); + EXPECT_EQ(std::uint64_t{1}, saturating_sub(u64_max, static_cast(u64_max - 1U))); + EXPECT_EQ(u64_max, saturating_mul(u64_max, std::uint64_t{2})); + EXPECT_EQ(u64_max, saturating_mul_portable(u64_max, std::uint64_t{2})); + EXPECT_EQ(u64_max, saturating_mul(UINT64_C(4294967296), UINT64_C(4294967296))); + EXPECT_EQ(UINT64_C(18446744065119617025), saturating_mul(UINT64_C(4294967295), UINT64_C(4294967295))); + EXPECT_EQ(std::uint64_t{1}, saturating_div(u64_max, u64_max)); +} + +#if CETLVAST_HAS_INT128 + +template +using wide_t = std::conditional_t::value, cetlvast_int128, cetlvast_uint128>; + +template +T clamp_wide(cetlvast_int128 v) +{ + const cetlvast_int128 lo = std::numeric_limits::min(); + const cetlvast_int128 hi = std::numeric_limits::max(); + return static_cast(std::min(std::max(v, lo), hi)); +} + +template +void check_pair_wide(T x, T y) +{ + // Products of two 64-bit values fit in the 128-bit type of the same signedness; sums and differences are + // evaluated in the signed 128-bit type so that unsigned subtraction cannot wrap in the oracle. + const cetlvast_int128 a = x; + const cetlvast_int128 b = y; + const wide_t product = static_cast>(x) * static_cast>(y); + const std::string context = show(x) + ", " + show(y); + ASSERT_EQ(clamp_wide(a + b), saturating_add(x, y)) << context; + ASSERT_EQ(clamp_wide(a + b), saturating_add_portable(x, y)) << context; + ASSERT_EQ(clamp_wide(a - b), saturating_sub(x, y)) << context; + ASSERT_EQ(clamp_wide(a - b), saturating_sub_portable(x, y)) << context; + const T expected_product = + std::is_signed::value + ? clamp_wide(static_cast(product)) + : static_cast(std::min(static_cast(product), static_cast(u64_max))); + ASSERT_EQ(expected_product, saturating_mul(x, y)) << context; + ASSERT_EQ(expected_product, saturating_mul_portable(x, y)) << context; + if (b != 0) + { + ASSERT_EQ(clamp_wide(a / b), saturating_div(x, y)) << context; + } +} + +template +void random_64bit() +{ + static_assert(sizeof(T) == 8, ""); + const std::vector edges = edge_values(); + for (const T x : edges) + { + for (const T y : edges) + { + check_pair_wide(x, y); + } + } + std::mt19937_64 generator(39U); + std::uniform_int_distribution any(std::numeric_limits::min(), std::numeric_limits::max()); + for (int i = 0; i < 100000; ++i) + { + const T x = (i % 3 == 0) ? edges[static_cast(i / 3) % edges.size()] : any(generator); + const T y = (i % 5 == 0) ? edges[static_cast(i / 5) % edges.size()] : any(generator); + check_pair_wide(x, y); + // Values near the limits are where multiplication overflow decisions are made. + check_pair_wide(static_cast(x / 2), static_cast(y % 4)); + } +} + +TEST(PF26NumericTest, RandomInt64) +{ + random_64bit(); +} + +TEST(PF26NumericTest, RandomUint64) +{ + random_64bit(); +} + +#endif // CETLVAST_HAS_INT128 + +// +----------------------------------------------------------------------+ +// | saturating_cast: every value of the 8- and 16-bit source types into every fixed-width type. +// +----------------------------------------------------------------------+ +template +To expected_cast(long long v) +{ + if (std::is_signed::value) + { + return static_cast(clamp_ll(v)); + } + return (v < 0) ? static_cast(0) : static_cast(clamp_ull(static_cast(v))); +} + +template +void exhaustive_cast() +{ + static_assert(sizeof(From) <= 2, ""); + for (long v = std::numeric_limits::min(); v <= std::numeric_limits::max(); ++v) + { + const From x = static_cast(v); + ASSERT_EQ(expected_cast(v), saturating_cast(x)) << show(x) << " to " << sizeof(To) << " bytes"; + } +} + +template +void exhaustive_casts_from() +{ + exhaustive_cast(); + exhaustive_cast(); + exhaustive_cast(); + exhaustive_cast(); + exhaustive_cast(); + exhaustive_cast(); + exhaustive_cast(); + exhaustive_cast(); +} + +TEST(PF26NumericTest, ExhaustiveCastsFromNarrowTypes) +{ + exhaustive_casts_from(); + exhaustive_casts_from(); + exhaustive_casts_from(); + exhaustive_casts_from(); +} + +TEST(PF26NumericTest, CastsBetweenWideTypes) +{ + EXPECT_EQ(std::numeric_limits::max(), saturating_cast(i64_max)); + EXPECT_EQ(std::numeric_limits::min(), saturating_cast(i64_min)); + EXPECT_EQ(std::numeric_limits::max(), saturating_cast(u64_max)); + EXPECT_EQ(std::uint32_t{0}, saturating_cast(i64_min)); + EXPECT_EQ(i64_max, saturating_cast(u64_max)); + EXPECT_EQ(static_cast(i64_max), saturating_cast(i64_max)); + EXPECT_EQ(std::uint64_t{0}, saturating_cast(std::int64_t{-1})); + EXPECT_EQ(std::int64_t{INT64_C(4294967295)}, + saturating_cast(std::numeric_limits::max())); +} + +// +----------------------------------------------------------------------+ +// | The polyfill header's selection. +// +----------------------------------------------------------------------+ +TEST(PF26CetlPfTest, SelectedFunctionsSaturate) +{ + EXPECT_EQ(127, cetl::saturating_add(100, 100)); + EXPECT_EQ(0U, cetl::saturating_sub(3, 5)); + EXPECT_EQ(127, cetl::saturating_mul(-128, -1)); + EXPECT_EQ(127, cetl::saturating_div(-128, -1)); + EXPECT_EQ(std::numeric_limits::max(), cetl::saturating_cast(INT64_C(5000000000))); + EXPECT_EQ(0U, cetl::saturating_cast(std::int64_t{-1})); + EXPECT_EQ(u64_max, cetl::saturating_mul(u64_max, u64_max)); +} + +// +----------------------------------------------------------------------+ +// | Precondition +// +----------------------------------------------------------------------+ +#if defined(CETL_ENABLE_DEBUG_ASSERT) && CETL_ENABLE_DEBUG_ASSERT + +static void TestDivideByZero() +{ + flush_coverage_on_death(); + const int zero = 0; + const int result = saturating_div(1, zero); + (void) result; +} + +TEST(DeathTestPF26Numeric, DivideByZero) +{ + EXPECT_DEATH(TestDivideByZero(), "CDE_numeric_001"); +} + +#endif // CETL_ENABLE_DEBUG_ASSERT + +} // namespace diff --git a/include/cetl/cetl.hpp b/include/cetl/cetl.hpp index 02c17cac..7a5a10f5 100644 --- a/include/cetl/cetl.hpp +++ b/include/cetl/cetl.hpp @@ -33,6 +33,7 @@ /// - @subpage example_10_unbounded_variant /// - @subpage example_11_memory_resource_o1heap /// - @subpage example_12_buddy_memory_resource +/// - @subpage example_13_saturating_arithmetic /// /// @page example_01_polyfill_20 Example 1: CETL C++20 Polyfill Header /// Full example for @ref cetl/pf20/cetlpf.hpp @@ -83,6 +84,10 @@ /// In-place buddy merging with the same application operations as std::vector. /// @include example_12_buddy_memory_resource.cpp /// +/// @page example_13_saturating_arithmetic Example 13: Saturation arithmetic with the C++26 polyfill header +/// Full example for @ref cetl/pf26/cetlpf.hpp +/// @include example_13_saturating_arithmetic.cpp +/// #ifndef CETL_H_INCLUDED #define CETL_H_INCLUDED @@ -195,6 +200,16 @@ /// ``` #define CETL_CPP_STANDARD_20 202002L +/// @def CETL_CPP_STANDARD_23 +/// Provides the proper value to test against `__cplusplus` for c++23. +#define CETL_CPP_STANDARD_23 202302L + +/// @def CETL_CPP_STANDARD_26 +/// Provides the proper value to test against `__cplusplus` for c++26. Compilers released before the standard was +/// finalized report an interim value (for example `202400L`) for `-std=c++26`, so C++26 library features are detected +/// with their feature-test macros (see cetl/pf26/cetlpf.hpp) rather than with this value. +#define CETL_CPP_STANDARD_26 202603L + /// @} // Ensure base support. diff --git a/include/cetl/pf17/algorithm.hpp b/include/cetl/pf17/algorithm.hpp new file mode 100644 index 00000000..7e69ec97 --- /dev/null +++ b/include/cetl/pf17/algorithm.hpp @@ -0,0 +1,47 @@ +/// @file +/// Defines C++17 entities from the \c algorithm header. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef CETL_PF17_ALGORITHM_HPP_INCLUDED +#define CETL_PF17_ALGORITHM_HPP_INCLUDED + +#include + +#ifndef CETL_H_ERASE +# include "cetl/cetl.hpp" +#endif + +namespace cetl +{ +namespace pf17 +{ + +/// Polyfill for std::clamp with a user-provided comparison (C++17, [alg.clamp]). +/// Returns `lo` if `v` is less than `lo`, `hi` if `hi` is less than `v`, otherwise `v`, where "less" is `comp`. +/// The result is a reference to one of the arguments, so none of them may be a temporary that outlives the call. +/// +/// @pre `!comp(hi, lo)` (CDE_algorithm_001). +template +CETL_NODISCARD constexpr const T& clamp(const T& v, const T& lo, const T& hi, Compare comp) +{ + CETL_DEBUG_ASSERT(!comp(hi, lo), "CDE_algorithm_001: clamp requires that hi is not less than lo."); + return comp(v, lo) ? lo : (comp(hi, v) ? hi : v); +} + +/// Polyfill for std::clamp using `operator<` (C++17, [alg.clamp]). +/// @pre `!(hi < lo)` (CDE_algorithm_001). +template +CETL_NODISCARD constexpr const T& clamp(const T& v, const T& lo, const T& hi) +{ + // Qualified: an unqualified call would also find std::clamp through ADL on std::less (and on std types T). + return cetl::pf17::clamp(v, lo, hi, std::less<>{}); +} + +} // namespace pf17 +} // namespace cetl + +#endif // CETL_PF17_ALGORITHM_HPP_INCLUDED diff --git a/include/cetl/pf17/cetlpf.hpp b/include/cetl/pf17/cetlpf.hpp index d4858045..c4b407bd 100644 --- a/include/cetl/pf17/cetlpf.hpp +++ b/include/cetl/pf17/cetlpf.hpp @@ -45,6 +45,7 @@ # include # include # include +# include namespace cetl { @@ -128,6 +129,9 @@ using std::holds_alternative; using std::basic_string_view; using std::string_view; +// algorithm +using std::clamp; + } // namespace cetl #else @@ -138,6 +142,7 @@ using std::string_view; # include "cetl/pf17/optional.hpp" # include "cetl/pf17/variant.hpp" # include "cetl/pf17/string_view.hpp" +# include "cetl/pf17/algorithm.hpp" namespace cetl { @@ -226,6 +231,9 @@ using cetl::pf17::bad_variant_access; using cetl::pf17::basic_string_view; using cetl::pf17::string_view; +// algorithm +using cetl::pf17::clamp; + } // namespace cetl #endif diff --git a/include/cetl/pf20/cetlpf.hpp b/include/cetl/pf20/cetlpf.hpp index deba905d..31c8cfd6 100644 --- a/include/cetl/pf20/cetlpf.hpp +++ b/include/cetl/pf20/cetlpf.hpp @@ -51,6 +51,7 @@ #if (__cplusplus >= CETL_CPP_STANDARD_20 && !defined(CETL_DOXYGEN)) # include +# include namespace cetl { @@ -58,9 +59,19 @@ constexpr std::size_t dynamic_extent = std::dynamic_extent; template using span = std::span; + +// utility +using std::cmp_equal; +using std::cmp_not_equal; +using std::cmp_less; +using std::cmp_greater; +using std::cmp_less_equal; +using std::cmp_greater_equal; +using std::in_range; } // namespace cetl #else # include "cetl/pf20/span.hpp" +# include "cetl/pf20/utility.hpp" namespace cetl { @@ -69,6 +80,15 @@ constexpr std::size_t dynamic_extent = cetl::pf20::dynamic_extent; template using span = cetl::pf20::span; +// utility +using cetl::pf20::cmp_equal; +using cetl::pf20::cmp_not_equal; +using cetl::pf20::cmp_less; +using cetl::pf20::cmp_greater; +using cetl::pf20::cmp_less_equal; +using cetl::pf20::cmp_greater_equal; +using cetl::pf20::in_range; + } // namespace cetl #endif diff --git a/include/cetl/pf20/utility.hpp b/include/cetl/pf20/utility.hpp new file mode 100644 index 00000000..6d17db86 --- /dev/null +++ b/include/cetl/pf20/utility.hpp @@ -0,0 +1,249 @@ +/// @file +/// Defines C++20 entities from the \c utility header. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef CETL_PF20_UTILITY_HPP_INCLUDED +#define CETL_PF20_UTILITY_HPP_INCLUDED + +#include +#include + +#ifndef CETL_H_ERASE +# include "cetl/cetl.hpp" +#endif + +namespace cetl +{ +namespace pf20 +{ +namespace detail +{ +// +----------------------------------------------------------------------------------------------------------------+ +// | Integer traits shared by the integer-comparison functions and by cetl/pf26/numeric.hpp. +// | +// | The standard traits are not enough on their own: GCC's 128-bit extended integers are not reported by +// | std::is_integral, std::is_signed, std::make_unsigned or std::numeric_limits in its strict -std=c++NN modes, and +// | cv-qualifiers must be ignored so that explicit template arguments such as `cmp_less` are still +// | rejected. +// +----------------------------------------------------------------------------------------------------------------+ +#if defined(__SIZEOF_INT128__) +__extension__ typedef __int128 int128_type; +__extension__ typedef unsigned __int128 uint128_type; +template +using is_int128 = + std::integral_constant::value || std::is_same::value>; +template +using is_signed_int128 = std::is_same; +#else +template +using is_int128 = std::false_type; +template +using is_signed_int128 = std::false_type; +#endif + +/// `bool` and the character types: integral, but excluded from the integer-comparison functions and from saturation +/// arithmetic by the standard. +template +struct is_bool_or_character + : std::integral_constant::value || std::is_same::value || + std::is_same::value || std::is_same::value || + std::is_same::value> +{}; +#if defined(__cpp_char8_t) +template <> +struct is_bool_or_character : std::true_type +{}; +#endif + +/// True for the standard and extended integer types of [basic.fundamental], ignoring cv-qualifiers. +template +using is_standard_or_extended_integer = std::integral_constant>::value && + !is_bool_or_character>::value) || + is_int128>::value>; + +/// True for the signed types among the standard and extended integer types. +template +using is_signed_integer = + std::integral_constant::value && (std::is_signed>::value || + is_signed_int128>::value)>; + +template ::value> +struct make_unsigned_integer +{ + using type = std::make_unsigned_t; +}; +#if defined(__SIZEOF_INT128__) +template +struct make_unsigned_integer +{ + using type = uint128_type; +}; +#endif +/// The unsigned counterpart of a standard or extended integer type. +template +using make_unsigned_integer_t = typename make_unsigned_integer>::type; + +/// The limits of a standard or extended integer type. std::numeric_limits is used where it is specialized for T. +template ::is_specialized> +struct integer_limits +{ + static constexpr T min() noexcept + { + return std::numeric_limits::min(); + } + static constexpr T max() noexcept + { + return std::numeric_limits::max(); + } +}; +/// Extended integers that the standard library does not describe (GCC's __int128 in strict modes) are two's +/// complement, so their limits follow from their width. +template +struct integer_limits +{ + using unsigned_type = make_unsigned_integer_t; + static constexpr T max() noexcept + { + return max_impl(is_signed_integer{}); + } + static constexpr T min() noexcept + { + return min_impl(is_signed_integer{}); + } + +private: + static constexpr T max_impl(std::true_type /*signed*/) noexcept + { + return static_cast(static_cast(~unsigned_type{0}) >> 1); + } + static constexpr T max_impl(std::false_type /*unsigned*/) noexcept + { + return static_cast(~T{0}); + } + static constexpr T min_impl(std::true_type /*signed*/) noexcept + { + return static_cast(-max() - 1); + } + static constexpr T min_impl(std::false_type /*unsigned*/) noexcept + { + return T{0}; + } +}; + +/// Selects the comparison strategy: 0 = same signedness, 1 = T signed and U unsigned, 2 = T unsigned and U signed. +template +using cmp_case_t = std::integral_constant::value == is_signed_integer::value) ? 0 + : is_signed_integer::value ? 1 + : 2>; + +template +constexpr bool cmp_equal_impl(T t, U u, std::integral_constant) noexcept +{ + return t == u; +} +template +constexpr bool cmp_equal_impl(T t, U u, std::integral_constant) noexcept +{ + return (t < 0) ? false : (static_cast>(t) == u); +} +template +constexpr bool cmp_equal_impl(T t, U u, std::integral_constant) noexcept +{ + return (u < 0) ? false : (t == static_cast>(u)); +} + +template +constexpr bool cmp_less_impl(T t, U u, std::integral_constant) noexcept +{ + return t < u; +} +template +constexpr bool cmp_less_impl(T t, U u, std::integral_constant) noexcept +{ + return (t < 0) ? true : (static_cast>(t) < u); +} +template +constexpr bool cmp_less_impl(T t, U u, std::integral_constant) noexcept +{ + return (u < 0) ? false : (t < static_cast>(u)); +} + +template +using are_comparable_integers = + std::integral_constant::value && is_standard_or_extended_integer::value>; +} // namespace detail + +/// Polyfill for std::cmp_equal (C++20, [utility.intcmp]): `t == u` with the mathematical values, so that +/// negative values never compare equal to unsigned values. +/// @pre T and U are standard or extended integer types (mandated by static assertion). +template +CETL_NODISCARD constexpr bool cmp_equal(T t, U u) noexcept +{ + static_assert(detail::are_comparable_integers::value, + "cmp_equal requires standard or extended integer types (not bool or character types)."); + return detail::cmp_equal_impl(t, u, detail::cmp_case_t{}); +} + +/// Polyfill for std::cmp_not_equal (C++20, [utility.intcmp]). +template +CETL_NODISCARD constexpr bool cmp_not_equal(T t, U u) noexcept +{ + return !cmp_equal(t, u); +} + +/// Polyfill for std::cmp_less (C++20, [utility.intcmp]): `t < u` with the mathematical values, so that +/// `cmp_less(-1, 1U)` is true where `-1 < 1U` is false. +/// @pre T and U are standard or extended integer types (mandated by static assertion). +template +CETL_NODISCARD constexpr bool cmp_less(T t, U u) noexcept +{ + static_assert(detail::are_comparable_integers::value, + "cmp_less requires standard or extended integer types (not bool or character types)."); + return detail::cmp_less_impl(t, u, detail::cmp_case_t{}); +} + +/// Polyfill for std::cmp_greater (C++20, [utility.intcmp]). +template +CETL_NODISCARD constexpr bool cmp_greater(T t, U u) noexcept +{ + return cmp_less(u, t); +} + +/// Polyfill for std::cmp_less_equal (C++20, [utility.intcmp]). +template +CETL_NODISCARD constexpr bool cmp_less_equal(T t, U u) noexcept +{ + return !cmp_less(u, t); +} + +/// Polyfill for std::cmp_greater_equal (C++20, [utility.intcmp]). +template +CETL_NODISCARD constexpr bool cmp_greater_equal(T t, U u) noexcept +{ + return !cmp_less(t, u); +} + +/// Polyfill for std::in_range (C++20, [utility.intcmp]): true if the value of `t` is representable in `R`. +/// @pre R and T are standard or extended integer types (mandated by static assertion). +template +CETL_NODISCARD constexpr bool in_range(T t) noexcept +{ + static_assert(detail::are_comparable_integers::value, + "in_range requires standard or extended integer types (not bool or character types)."); + return cmp_greater_equal(t, detail::integer_limits>::min()) && + cmp_less_equal(t, detail::integer_limits>::max()); +} + +} // namespace pf20 +} // namespace cetl + +#endif // CETL_PF20_UTILITY_HPP_INCLUDED diff --git a/include/cetl/pf26/cetlpf.hpp b/include/cetl/pf26/cetlpf.hpp new file mode 100644 index 00000000..6db4f431 --- /dev/null +++ b/include/cetl/pf26/cetlpf.hpp @@ -0,0 +1,107 @@ +/// @file +/// CETL polyfill header for C++26 types +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT +/// +/// @warning +/// This header violates AUTOSAR-14 M7-3-6, A17-0-1, and A16-0-1 (and possibly other rules). Don't use CETL polyfill +/// headers in AUTOSAR code. +/// +/// @warning +/// polyfill headers cannot be used if CETL_H_ERASE is defined. +/// +/// The saturation arithmetic functions are selected by the library feature-test macro rather than by `__cplusplus`, +/// because the standard library's support lags the language mode and because the functions were renamed late: +/// +/// - `__cpp_lib_saturation_arithmetic >= 202603L`: the standard's `std::saturating_add` family is used directly. +/// - `__cpp_lib_saturation_arithmetic >= 202311L`: the standard library only has the earlier P0543R3 names +/// (`std::add_sat`, `std::saturate_cast`, ...). CETL forwards to them under the final names, so user code is +/// written once. +/// - otherwise: cetl::pf26 provides the functions. +/// +/// For example, a counter that must stop at its maximum rather than wrap: +/// +/// @snippet{trimleft} example_13_saturating_arithmetic.cpp example_13_saturating_add +/// +/// and a narrowing conversion that clamps instead of truncating: +/// +/// @snippet{trimleft} example_13_saturating_arithmetic.cpp example_13_saturating_cast +/// +/// (@ref example_13_saturating_arithmetic "See full example here...") +/// + +#ifndef CETL_PF26_H_INCLUDED +#define CETL_PF26_H_INCLUDED + +/// @namespace cetl::pf26 This namespace contains C++26 polyfill (pf) types. +/// The types within this namespace adhere to the C++26 specification and should be drop-in replaceable +/// with said standard types where they are available. CETL polyfill types may implement a sub-set of +/// the required functionality but they will not implement non-compliant functionality. +#include "cetl/cetl.hpp" +#include "cetl/pf20/cetlpf.hpp" + +#include // Defines __cpp_lib_saturation_arithmetic where the standard library provides the feature. + +#if defined(__cpp_lib_saturation_arithmetic) && (__cpp_lib_saturation_arithmetic >= 202603L) && !defined(CETL_DOXYGEN) + +namespace cetl +{ +using std::saturating_add; +using std::saturating_sub; +using std::saturating_mul; +using std::saturating_div; +using std::saturating_cast; +} // namespace cetl + +#elif defined(__cpp_lib_saturation_arithmetic) && (__cpp_lib_saturation_arithmetic >= 202311L) && !defined(CETL_DOXYGEN) + +namespace cetl +{ +// The standard library implements P0543R3 but not the P4052R0 renaming. The trailing return types make each +// forwarder's participation in overload resolution depend on the standard call, so the standard's constraints apply +// here exactly as they would to the renamed functions. +template +CETL_NODISCARD constexpr auto saturating_add(T x, T y) noexcept -> decltype(std::add_sat(x, y)) +{ + return std::add_sat(x, y); +} +template +CETL_NODISCARD constexpr auto saturating_sub(T x, T y) noexcept -> decltype(std::sub_sat(x, y)) +{ + return std::sub_sat(x, y); +} +template +CETL_NODISCARD constexpr auto saturating_mul(T x, T y) noexcept -> decltype(std::mul_sat(x, y)) +{ + return std::mul_sat(x, y); +} +template +CETL_NODISCARD constexpr auto saturating_div(T x, T y) noexcept -> decltype(std::div_sat(x, y)) +{ + return std::div_sat(x, y); +} +template +CETL_NODISCARD constexpr auto saturating_cast(U x) noexcept -> decltype(std::saturate_cast(x)) +{ + return std::saturate_cast(x); +} +} // namespace cetl + +#else +# include "cetl/pf26/numeric.hpp" + +namespace cetl +{ +using cetl::pf26::saturating_add; +using cetl::pf26::saturating_sub; +using cetl::pf26::saturating_mul; +using cetl::pf26::saturating_div; +using cetl::pf26::saturating_cast; +} // namespace cetl + +#endif + +#endif // CETL_PF26_H_INCLUDED diff --git a/include/cetl/pf26/numeric.hpp b/include/cetl/pf26/numeric.hpp new file mode 100644 index 00000000..9cc38551 --- /dev/null +++ b/include/cetl/pf26/numeric.hpp @@ -0,0 +1,253 @@ +/// @file +/// Defines the C++26 saturation arithmetic functions of the \c numeric header ([numeric.sat]) for C++14 and newer. +/// +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT +/// +/// The functions follow ISO/IEC 14882:2026 as renamed by P4052R0: `saturating_add`, `saturating_sub`, +/// `saturating_mul`, `saturating_div` and `saturating_cast`. As in the standard, the operands share one type that +/// is never promoted, mixed operand types fail deduction, and `bool` and the character types are excluded. +/// +/// Where the compiler provides `__builtin_add_overflow` and friends (GCC 5 and newer, Clang) they are used; +/// otherwise a portable implementation that never forms an overflowing expression is used. Both are constant +/// expressions in C++14. + +#ifndef CETL_PF26_NUMERIC_HPP_INCLUDED +#define CETL_PF26_NUMERIC_HPP_INCLUDED + +#include +#include + +#ifndef CETL_H_ERASE +# include "cetl/cetl.hpp" +#endif +#include "cetl/pf20/utility.hpp" + +/// @def CETL_PF26_DETAIL_OVERFLOW_BUILTINS +/// 1 when the compiler's overflow-checking builtins implement the saturating operations, 0 when the portable +/// implementation does. Not a configuration knob; exposed only so that tests can report which path is active. +#if defined(__has_builtin) +# if __has_builtin(__builtin_add_overflow) && __has_builtin(__builtin_sub_overflow) && \ + __has_builtin(__builtin_mul_overflow) +# define CETL_PF26_DETAIL_OVERFLOW_BUILTINS 1 +# endif +#elif defined(__GNUC__) && (__GNUC__ >= 5) +# define CETL_PF26_DETAIL_OVERFLOW_BUILTINS 1 +#endif +#ifndef CETL_PF26_DETAIL_OVERFLOW_BUILTINS +# define CETL_PF26_DETAIL_OVERFLOW_BUILTINS 0 +#endif + +namespace cetl +{ +namespace pf26 +{ +namespace detail +{ +/// The "signed or unsigned integer types" of [basic.fundamental]: the standard and extended integer types +/// (cv-qualifiers ignored, `bool` and the character types excluded). Extended 128-bit integers are supported even +/// where std::is_integral and std::numeric_limits do not describe them (GCC in strict -std=c++NN modes). +template +using is_signed_or_unsigned_integer = pf20::detail::is_standard_or_extended_integer; +template +using is_signed = pf20::detail::is_signed_integer; +template +using limits = pf20::detail::integer_limits; + +template +using enable_if_integer_t = std::enable_if_t::value, int>; + +// +----------------------------------------------------------------------------------------------------------------+ +// | Bounds returned on overflow. For signed types the operands of an overflowing operation determine the direction. +// +----------------------------------------------------------------------------------------------------------------+ +template +constexpr T add_saturated(T x, std::true_type /*signed*/) noexcept +{ + return (x < 0) ? limits::min() : limits::max(); +} +template +constexpr T add_saturated(T, std::false_type /*unsigned*/) noexcept +{ + return limits::max(); +} +template +constexpr T sub_saturated(T x, std::true_type /*signed*/) noexcept +{ + return (x < 0) ? limits::min() : limits::max(); +} +template +constexpr T sub_saturated(T, std::false_type /*unsigned*/) noexcept +{ + return limits::min(); +} +template +constexpr T mul_saturated(T x, T y, std::true_type /*signed*/) noexcept +{ + return ((x < 0) != (y < 0)) ? limits::min() : limits::max(); +} +template +constexpr T mul_saturated(T, T, std::false_type /*unsigned*/) noexcept +{ + return limits::max(); +} + +// +----------------------------------------------------------------------------------------------------------------+ +// | Portable overflow predicates. No expression below can itself overflow or divide by zero: the limit is moved to +// | the side of the comparison that keeps it in range, and divisors are paired with dividends of the sign that +// | cannot produce min / -1. +// +----------------------------------------------------------------------------------------------------------------+ +template +constexpr bool add_overflows(T x, T y, std::true_type /*signed*/) noexcept +{ + return (y > 0) ? (x > static_cast(limits::max() - y)) : (x < static_cast(limits::min() - y)); +} +template +constexpr bool add_overflows(T x, T y, std::false_type /*unsigned*/) noexcept +{ + return x > static_cast(limits::max() - y); +} +template +constexpr bool sub_overflows(T x, T y, std::true_type /*signed*/) noexcept +{ + return (y < 0) ? (x > static_cast(limits::max() + y)) : (x < static_cast(limits::min() + y)); +} +template +constexpr bool sub_overflows(T x, T y, std::false_type /*unsigned*/) noexcept +{ + return x < y; +} +template +constexpr bool mul_overflows(T x, T y, std::true_type /*signed*/) noexcept +{ + return ((x == 0) || (y == 0)) ? false + : (x > 0) ? ((y > 0) ? (x > limits::max() / y) : (y < limits::min() / x)) + : ((y > 0) ? (x < limits::min() / y) : (y < limits::max() / x)); +} +template +constexpr bool mul_overflows(T x, T y, std::false_type /*unsigned*/) noexcept +{ + return (y != 0) && (x > limits::max() / y); +} +template +constexpr bool is_min_over_minus_one(T x, T y, std::true_type /*signed*/) noexcept +{ + return (x == limits::min()) && (y == static_cast(-1)); +} +template +constexpr bool is_min_over_minus_one(T, T, std::false_type /*unsigned*/) noexcept +{ + return false; +} + +// +----------------------------------------------------------------------------------------------------------------+ +// | Portable implementations. These are always compiled (and unit-tested) even where the builtins are used. +// +----------------------------------------------------------------------------------------------------------------+ +template +constexpr T saturating_add_portable(T x, T y) noexcept +{ + return add_overflows(x, y, is_signed{}) ? add_saturated(x, is_signed{}) : static_cast(x + y); +} +template +constexpr T saturating_sub_portable(T x, T y) noexcept +{ + return sub_overflows(x, y, is_signed{}) ? sub_saturated(x, is_signed{}) : static_cast(x - y); +} +template +constexpr T saturating_mul_portable(T x, T y) noexcept +{ + return mul_overflows(x, y, is_signed{}) ? mul_saturated(x, y, is_signed{}) : static_cast(x * y); +} + +#if CETL_PF26_DETAIL_OVERFLOW_BUILTINS +template +constexpr T saturating_add_builtin(T x, T y) noexcept +{ + T result{}; + return __builtin_add_overflow(x, y, &result) ? add_saturated(x, is_signed{}) : result; +} +template +constexpr T saturating_sub_builtin(T x, T y) noexcept +{ + T result{}; + return __builtin_sub_overflow(x, y, &result) ? sub_saturated(x, is_signed{}) : result; +} +template +constexpr T saturating_mul_builtin(T x, T y) noexcept +{ + T result{}; + return __builtin_mul_overflow(x, y, &result) ? mul_saturated(x, y, is_signed{}) : result; +} +#endif +} // namespace detail + +/// Polyfill for std::saturating_add (C++26, [numeric.sat.func]). +/// Returns `x + y` if it is representable in `T`, otherwise the bound of `T` nearest to the mathematical result. +/// @tparam T A signed or unsigned integer type (not `bool` or a character type); never promoted. +template = 0> +CETL_NODISCARD constexpr T saturating_add(T x, T y) noexcept +{ +#if CETL_PF26_DETAIL_OVERFLOW_BUILTINS + return detail::saturating_add_builtin(x, y); +#else + return detail::saturating_add_portable(x, y); +#endif +} + +/// Polyfill for std::saturating_sub (C++26, [numeric.sat.func]). +/// Returns `x - y` if it is representable in `T`, otherwise the bound of `T` nearest to the mathematical result. +/// @tparam T A signed or unsigned integer type (not `bool` or a character type); never promoted. +template = 0> +CETL_NODISCARD constexpr T saturating_sub(T x, T y) noexcept +{ +#if CETL_PF26_DETAIL_OVERFLOW_BUILTINS + return detail::saturating_sub_builtin(x, y); +#else + return detail::saturating_sub_portable(x, y); +#endif +} + +/// Polyfill for std::saturating_mul (C++26, [numeric.sat.func]). +/// Returns `x * y` if it is representable in `T`, otherwise the bound of `T` nearest to the mathematical result. +/// @tparam T A signed or unsigned integer type (not `bool` or a character type); never promoted. +template = 0> +CETL_NODISCARD constexpr T saturating_mul(T x, T y) noexcept +{ +#if CETL_PF26_DETAIL_OVERFLOW_BUILTINS + return detail::saturating_mul_builtin(x, y); +#else + return detail::saturating_mul_portable(x, y); +#endif +} + +/// Polyfill for std::saturating_div (C++26, [numeric.sat.func]). +/// Returns `x / y` (truncated toward zero) if it is representable in `T`; the only other case, +/// `numeric_detail::limits::min() / -1`, returns `numeric_detail::limits::max()`. +/// @tparam T A signed or unsigned integer type (not `bool` or a character type); never promoted. +/// @pre `y != 0` (CDE_numeric_001). As in the standard, a zero divisor is undefined behaviour and a call that +/// divides by zero is not a constant expression. +template = 0> +CETL_NODISCARD constexpr T saturating_div(T x, T y) noexcept +{ + CETL_DEBUG_ASSERT(y != 0, "CDE_numeric_001: saturating_div requires a non-zero divisor."); + return detail::is_min_over_minus_one(x, y, detail::is_signed{}) ? detail::limits::max() + : static_cast(x / y); +} + +/// Polyfill for std::saturating_cast (C++26, [numeric.sat.cast]). +/// Returns `x` if it is representable in `T`, otherwise the bound of `T` nearest to `x`. +/// @tparam T The signed or unsigned integer type to convert to (not `bool` or a character type). +/// @tparam U The signed or unsigned integer type of the value (deduced). +template = 0, detail::enable_if_integer_t = 0> +CETL_NODISCARD constexpr T saturating_cast(U x) noexcept +{ + return pf20::cmp_less(x, detail::limits::min()) ? detail::limits::min() + : pf20::cmp_greater(x, detail::limits::max()) ? detail::limits::max() + : static_cast(x); +} + +} // namespace pf26 +} // namespace cetl + +#endif // CETL_PF26_NUMERIC_HPP_INCLUDED diff --git a/scripts/release_tools.py b/scripts/release_tools.py index cc7f7f96..74c88c9a 100644 --- a/scripts/release_tools.py +++ b/scripts/release_tools.py @@ -307,7 +307,7 @@ def required_jobs(): """ names = {"version", "release-tests", "cache-warmup", "docs", "release-ready", "release-assets"} for architecture, compilers, standards in ( - ("amd64", ("gcc-native-32", "clang-native"), (14, 17, 20, 23)), + ("amd64", ("gcc-native-32", "clang-native"), (14, 17, 20, 23, 26)), ("arm64", ("gcc-native", "clang-native"), (14, 17, 20)), ("legacy-arm64", ("gcc7-native",), (14,)), ):