From 3ab77cff58283b75811a1bc5f289f228bc282cac Mon Sep 17 00:00:00 2001 From: Scott Dixon Date: Wed, 21 Oct 2026 15:16:14 -0700 Subject: [PATCH 1/3] Add C++26 saturation arithmetic polyfills (#39) libcyphal and nunavut both need saturating integer operations, and C++26 standardized them (P0543R3, renamed by P4052R0 to saturating_add, saturating_sub, saturating_mul, saturating_div and saturating_cast). CETL now provides those five functions for C++14 and newer, exact to the standard: one operand type that is never promoted, bool and the character types excluded, a y != 0 precondition on division, and min / -1 saturating to max. The compiler's overflow builtins are used where present and a portable implementation that never forms an overflowing expression otherwise; both are constant expressions in C++14 and both are tested. cetl/pf26/cetlpf.hpp selects the implementation by the library feature-test macro rather than __cplusplus, because current compilers still report an interim value for -std=c++26 and because the functions were renamed late: 202603L uses the standard names directly, 202311L forwards to the earlier std::add_sat family under the final names, and anything else uses cetl::pf26. saturating_cast needs sign-correct comparisons, so the C++20 integer comparison functions (cmp_equal, cmp_less, ..., in_range) are added as cetl::pf20 polyfills, and nunavut's "saturated intN" fields need a clamp to an arbitrary range, so std::clamp is added as a cetl::pf17 polyfill. Both are wired into their cetlpf.hpp headers. Tests: exhaustive 8-bit and sampled 16-bit operand pairs, random 32/64-bit operands against a 128-bit oracle, exhaustive saturating_cast from the 8- and 16-bit types into every fixed-width type, an integer-comparison sweep against a sign-aware oracle, debug-assert death tests, compile-failure tests for bool and mixed operand types, and a differential libFuzzer target. A new cpp-26 preset is generated for every toolchain except GCC 7 and added to the amd64 CI matrix so the standard-library paths are exercised. Example 13 and CETL_CPP_STANDARD_23/26 document the addition. Refs #39; the cetl::saturating wrapper type follows in a separate PR. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/cetlvast.yml | 2 +- cetlvast/CMakeLists.txt | 2 +- cetlvast/CMakePresets.json | 649 +++++++++++++++++- cetlvast/CMakePresetsVendorTemplate.json | 9 +- cetlvast/suites/compile/CMakeLists.txt | 3 + .../compile/test_pf20_cmp_less_bool.cpp | 19 + .../compile/test_pf26_saturating_add_bool.cpp | 19 + .../test_pf26_saturating_add_mixed_types.cpp | 19 + cetlvast/suites/docs/examples/CMakeLists.txt | 1 + .../example_13_saturating_arithmetic.cpp | 71 ++ cetlvast/suites/fuzz/CMakeLists.txt | 47 +- .../saturation_arithmetic/int32_min_div_neg1 | Bin 0 -> 17 bytes .../int64_min_times_neg1 | Bin 0 -> 17 bytes .../saturation_arithmetic/int8_min_minus_one | Bin 0 -> 17 bytes .../uint64_max_times_two | Bin 0 -> 17 bytes .../saturation_arithmetic/uint8_max_plus_one | Bin 0 -> 17 bytes .../fuzz/fuzz_saturation_arithmetic.cpp | 139 ++++ cetlvast/suites/unittest/CMakeLists.txt | 3 + .../suites/unittest/test_pf17_algorithm.cpp | 94 +++ .../suites/unittest/test_pf20_utility.cpp | 211 ++++++ .../suites/unittest/test_pf26_numeric.cpp | 495 +++++++++++++ include/cetl/cetl.hpp | 15 + include/cetl/pf17/algorithm.hpp | 47 ++ include/cetl/pf17/cetlpf.hpp | 8 + include/cetl/pf20/cetlpf.hpp | 20 + include/cetl/pf20/utility.hpp | 149 ++++ include/cetl/pf26/cetlpf.hpp | 105 +++ include/cetl/pf26/numeric.hpp | 249 +++++++ 28 files changed, 2345 insertions(+), 31 deletions(-) create mode 100644 cetlvast/suites/compile/test_pf20_cmp_less_bool.cpp create mode 100644 cetlvast/suites/compile/test_pf26_saturating_add_bool.cpp create mode 100644 cetlvast/suites/compile/test_pf26_saturating_add_mixed_types.cpp create mode 100644 cetlvast/suites/docs/examples/example_13_saturating_arithmetic.cpp create mode 100644 cetlvast/suites/fuzz/corpus/saturation_arithmetic/int32_min_div_neg1 create mode 100644 cetlvast/suites/fuzz/corpus/saturation_arithmetic/int64_min_times_neg1 create mode 100644 cetlvast/suites/fuzz/corpus/saturation_arithmetic/int8_min_minus_one create mode 100644 cetlvast/suites/fuzz/corpus/saturation_arithmetic/uint64_max_times_two create mode 100644 cetlvast/suites/fuzz/corpus/saturation_arithmetic/uint8_max_plus_one create mode 100644 cetlvast/suites/fuzz/fuzz_saturation_arithmetic.cpp create mode 100644 cetlvast/suites/unittest/test_pf17_algorithm.cpp create mode 100644 cetlvast/suites/unittest/test_pf20_utility.cpp create mode 100644 cetlvast/suites/unittest/test_pf26_numeric.cpp create mode 100644 include/cetl/pf17/algorithm.hpp create mode 100644 include/cetl/pf20/utility.hpp create mode 100644 include/cetl/pf26/cetlpf.hpp create mode 100644 include/cetl/pf26/numeric.hpp 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..3b82b1cf 100644 --- a/cetlvast/suites/compile/CMakeLists.txt +++ b/cetlvast/suites/compile/CMakeLists.txt @@ -11,6 +11,9 @@ 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_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_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 0000000000000000000000000000000000000000..c80321b80b45978513b93f985acc435711f3357a GIT binary patch literal 17 ScmZQ!U|?tfVut@f0HOgPy99v% literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5d8d55ea75c3e85800d7ddfe1b34eb1ee0bd95bc GIT binary patch literal 17 OcmZQ$fPjYoPyhfZ(+Gb6 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9c58e7dd4851247cbafb3fb8986949add0ab779e GIT binary patch literal 17 OcmZQ*V1R)CP#ORb2?2lr literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a4241b8dcec80752c6461b793f702c5fbc0bd645 GIT binary patch literal 17 OcmZSo4+TsN5C8yVGzbC! literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..66455be81a0d7d6a82cb1a1d30aadf209b57a2b7 GIT binary patch literal 17 OcmZSl&j108P#ORb8UX?T literal 0 HcmV?d00001 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..d72f6e54 --- /dev/null +++ b/cetlvast/suites/unittest/test_pf20_utility.cpp @@ -0,0 +1,211 @@ +/// @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"); + +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..effb2212 --- /dev/null +++ b/cetlvast/suites/unittest/test_pf26_numeric.cpp @@ -0,0 +1,495 @@ +/// @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"); + +// 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..a64d1d40 --- /dev/null +++ b/include/cetl/pf20/utility.hpp @@ -0,0 +1,149 @@ +/// @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 +{ +/// True for the standard and extended integer types of [basic.fundamental]. False for `bool` and the character +/// types, which the standard excludes from the integer-comparison functions and from saturation arithmetic. +template +struct is_standard_or_extended_integer + : std::integral_constant::value && !std::is_same::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_standard_or_extended_integer : std::false_type +{}; +#endif + +/// 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 == std::is_signed::value) ? 0 + : std::is_signed::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 +struct 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, T>::value, + "in_range requires standard or extended integer types (not bool or character types)."); + return cmp_greater_equal(t, std::numeric_limits::min()) && cmp_less_equal(t, std::numeric_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..f071358f --- /dev/null +++ b/include/cetl/pf26/cetlpf.hpp @@ -0,0 +1,105 @@ +/// @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 standard's constraints apply. +template +CETL_NODISCARD constexpr T saturating_add(T x, T y) noexcept +{ + return std::add_sat(x, y); +} +template +CETL_NODISCARD constexpr T saturating_sub(T x, T y) noexcept +{ + return std::sub_sat(x, y); +} +template +CETL_NODISCARD constexpr T saturating_mul(T x, T y) noexcept +{ + return std::mul_sat(x, y); +} +template +CETL_NODISCARD constexpr T saturating_div(T x, T y) noexcept +{ + return std::div_sat(x, y); +} +template +CETL_NODISCARD constexpr T saturating_cast(U x) noexcept +{ + 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..17a82bc6 --- /dev/null +++ b/include/cetl/pf26/numeric.hpp @@ -0,0 +1,249 @@ +/// @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. +template +using is_signed_or_unsigned_integer = pf20::detail::is_standard_or_extended_integer; + +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) ? std::numeric_limits::min() : std::numeric_limits::max(); +} +template +constexpr T add_saturated(T, std::false_type /*unsigned*/) noexcept +{ + return std::numeric_limits::max(); +} +template +constexpr T sub_saturated(T x, std::true_type /*signed*/) noexcept +{ + return (x < 0) ? std::numeric_limits::min() : std::numeric_limits::max(); +} +template +constexpr T sub_saturated(T, std::false_type /*unsigned*/) noexcept +{ + return std::numeric_limits::min(); +} +template +constexpr T mul_saturated(T x, T y, std::true_type /*signed*/) noexcept +{ + return ((x < 0) != (y < 0)) ? std::numeric_limits::min() : std::numeric_limits::max(); +} +template +constexpr T mul_saturated(T, T, std::false_type /*unsigned*/) noexcept +{ + return std::numeric_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(std::numeric_limits::max() - y)) + : (x < static_cast(std::numeric_limits::min() - y)); +} +template +constexpr bool add_overflows(T x, T y, std::false_type /*unsigned*/) noexcept +{ + return x > static_cast(std::numeric_limits::max() - y); +} +template +constexpr bool sub_overflows(T x, T y, std::true_type /*signed*/) noexcept +{ + return (y < 0) ? (x > static_cast(std::numeric_limits::max() + y)) + : (x < static_cast(std::numeric_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 > std::numeric_limits::max() / y) : (y < std::numeric_limits::min() / x)) + : ((y > 0) ? (x < std::numeric_limits::min() / y) : (y < std::numeric_limits::max() / x)); +} +template +constexpr bool mul_overflows(T x, T y, std::false_type /*unsigned*/) noexcept +{ + return (y != 0) && (x > std::numeric_limits::max() / y); +} +template +constexpr bool is_min_over_minus_one(T x, T y, std::true_type /*signed*/) noexcept +{ + return (x == std::numeric_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, std::is_signed{}) ? add_saturated(x, std::is_signed{}) : static_cast(x + y); +} +template +constexpr T saturating_sub_portable(T x, T y) noexcept +{ + return sub_overflows(x, y, std::is_signed{}) ? sub_saturated(x, std::is_signed{}) : static_cast(x - y); +} +template +constexpr T saturating_mul_portable(T x, T y) noexcept +{ + return mul_overflows(x, y, std::is_signed{}) ? mul_saturated(x, y, std::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, std::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, std::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, std::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_limits::min() / -1`, returns `numeric_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, std::is_signed{}) ? std::numeric_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, std::numeric_limits::min()) ? std::numeric_limits::min() + : pf20::cmp_greater(x, std::numeric_limits::max()) ? std::numeric_limits::max() + : static_cast(x); +} + +} // namespace pf26 +} // namespace cetl + +#endif // CETL_PF26_NUMERIC_HPP_INCLUDED From ef0082ffdd0affeb62053c561a100b48026b4b0c Mon Sep 17 00:00:00 2001 From: Scott Dixon Date: Thu, 22 Oct 2026 10:25:30 -0700 Subject: [PATCH 2/3] Honour cv-qualifiers and extended integers in the integer traits Addresses the Codex review of #255. The shared integer traits in cetl/pf20/utility.hpp now ignore cv-qualifiers, so explicit template arguments such as saturating_add or cmp_less are rejected exactly like their unqualified forms, while const int is accepted on every implementation path. GCC's __int128 is now supported in the strict -std=c++NN modes CETLVaSt builds with, where std::is_integral, std::is_signed, std::make_unsigned and std::numeric_limits do not describe it: the traits carry their own signedness, unsigned counterpart and two's-complement limits for the 128-bit types, and cetl/pf26/numeric.hpp uses those traits throughout instead of the standard ones. The forwarders used when the standard library only has the P0543R3 names get trailing return types that depend on the standard call, so they participate in overload resolution exactly when the standard function would. Tests: cv-qualified explicit template arguments; 128-bit constant expressions for the comparison functions, in_range and all five saturation functions including the portable implementations; a compile-failure test for const bool. Co-Authored-By: Claude Fable 5.1 --- cetlvast/suites/compile/CMakeLists.txt | 1 + .../test_pf26_saturating_add_const_bool.cpp | 19 +++ .../suites/unittest/test_pf20_utility.cpp | 25 ++++ .../suites/unittest/test_pf26_numeric.cpp | 35 +++++ include/cetl/pf20/utility.hpp | 140 +++++++++++++++--- include/cetl/pf26/cetlpf.hpp | 14 +- include/cetl/pf26/numeric.hpp | 60 ++++---- 7 files changed, 240 insertions(+), 54 deletions(-) create mode 100644 cetlvast/suites/compile/test_pf26_saturating_add_const_bool.cpp diff --git a/cetlvast/suites/compile/CMakeLists.txt b/cetlvast/suites/compile/CMakeLists.txt index 3b82b1cf..542643ff 100644 --- a/cetlvast/suites/compile/CMakeLists.txt +++ b/cetlvast/suites/compile/CMakeLists.txt @@ -13,6 +13,7 @@ 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 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/unittest/test_pf20_utility.cpp b/cetlvast/suites/unittest/test_pf20_utility.cpp index d72f6e54..8c36b0b8 100644 --- a/cetlvast/suites/unittest/test_pf20_utility.cpp +++ b/cetlvast/suites/unittest/test_pf20_utility.cpp @@ -55,6 +55,31 @@ 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"); diff --git a/cetlvast/suites/unittest/test_pf26_numeric.cpp b/cetlvast/suites/unittest/test_pf26_numeric.cpp index effb2212..2ce0a382 100644 --- a/cetlvast/suites/unittest/test_pf26_numeric.cpp +++ b/cetlvast/suites/unittest/test_pf26_numeric.cpp @@ -104,6 +104,41 @@ 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, ""); diff --git a/include/cetl/pf20/utility.hpp b/include/cetl/pf20/utility.hpp index a64d1d40..6d17db86 100644 --- a/include/cetl/pf20/utility.hpp +++ b/include/cetl/pf20/utility.hpp @@ -22,27 +22,127 @@ namespace pf20 { namespace detail { -/// True for the standard and extended integer types of [basic.fundamental]. False for `bool` and the character -/// types, which the standard excludes from the integer-comparison functions and from saturation arithmetic. +// +----------------------------------------------------------------------------------------------------------------+ +// | 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 -struct is_standard_or_extended_integer +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 && !std::is_same::value> + std::is_same::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_standard_or_extended_integer : std::false_type +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 == std::is_signed::value) ? 0 - : std::is_signed::value ? 1 - : 2>; + (is_signed_integer::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 @@ -52,12 +152,12 @@ constexpr bool cmp_equal_impl(T t, U u, std::integral_constant) noexcept template constexpr bool cmp_equal_impl(T t, U u, std::integral_constant) noexcept { - return (t < 0) ? false : (static_cast>(t) == u); + 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)); + return (u < 0) ? false : (t == static_cast>(u)); } template @@ -68,19 +168,18 @@ constexpr bool cmp_less_impl(T t, U u, std::integral_constant) noexcept template constexpr bool cmp_less_impl(T t, U u, std::integral_constant) noexcept { - return (t < 0) ? true : (static_cast>(t) < u); + 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)); + return (u < 0) ? false : (t < static_cast>(u)); } template -struct are_comparable_integers - : std::integral_constant::value && is_standard_or_extended_integer::value> -{}; +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 @@ -138,9 +237,10 @@ CETL_NODISCARD constexpr bool cmp_greater_equal(T t, U u) noexcept template CETL_NODISCARD constexpr bool in_range(T t) noexcept { - static_assert(detail::are_comparable_integers, T>::value, + 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, std::numeric_limits::min()) && cmp_less_equal(t, std::numeric_limits::max()); + return cmp_greater_equal(t, detail::integer_limits>::min()) && + cmp_less_equal(t, detail::integer_limits>::max()); } } // namespace pf20 diff --git a/include/cetl/pf26/cetlpf.hpp b/include/cetl/pf26/cetlpf.hpp index f071358f..6db4f431 100644 --- a/include/cetl/pf26/cetlpf.hpp +++ b/include/cetl/pf26/cetlpf.hpp @@ -60,29 +60,31 @@ using std::saturating_cast; namespace cetl { -// The standard library implements P0543R3 but not the P4052R0 renaming. The standard's constraints apply. +// 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 T saturating_add(T x, T y) noexcept +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 T saturating_sub(T x, T y) noexcept +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 T saturating_mul(T x, T y) noexcept +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 T saturating_div(T x, T y) noexcept +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 T saturating_cast(U x) noexcept +CETL_NODISCARD constexpr auto saturating_cast(U x) noexcept -> decltype(std::saturate_cast(x)) { return std::saturate_cast(x); } diff --git a/include/cetl/pf26/numeric.hpp b/include/cetl/pf26/numeric.hpp index 17a82bc6..9cc38551 100644 --- a/include/cetl/pf26/numeric.hpp +++ b/include/cetl/pf26/numeric.hpp @@ -46,9 +46,15 @@ namespace pf26 { namespace detail { -/// The "signed or unsigned integer types" of [basic.fundamental]: the standard and extended integer types. +/// 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>; @@ -59,32 +65,32 @@ using enable_if_integer_t = std::enable_if_t::v template constexpr T add_saturated(T x, std::true_type /*signed*/) noexcept { - return (x < 0) ? std::numeric_limits::min() : std::numeric_limits::max(); + return (x < 0) ? limits::min() : limits::max(); } template constexpr T add_saturated(T, std::false_type /*unsigned*/) noexcept { - return std::numeric_limits::max(); + return limits::max(); } template constexpr T sub_saturated(T x, std::true_type /*signed*/) noexcept { - return (x < 0) ? std::numeric_limits::min() : std::numeric_limits::max(); + return (x < 0) ? limits::min() : limits::max(); } template constexpr T sub_saturated(T, std::false_type /*unsigned*/) noexcept { - return std::numeric_limits::min(); + return limits::min(); } template constexpr T mul_saturated(T x, T y, std::true_type /*signed*/) noexcept { - return ((x < 0) != (y < 0)) ? std::numeric_limits::min() : std::numeric_limits::max(); + return ((x < 0) != (y < 0)) ? limits::min() : limits::max(); } template constexpr T mul_saturated(T, T, std::false_type /*unsigned*/) noexcept { - return std::numeric_limits::max(); + return limits::max(); } // +----------------------------------------------------------------------------------------------------------------+ @@ -95,19 +101,17 @@ constexpr T mul_saturated(T, T, std::false_type /*unsigned*/) noexcept template constexpr bool add_overflows(T x, T y, std::true_type /*signed*/) noexcept { - return (y > 0) ? (x > static_cast(std::numeric_limits::max() - y)) - : (x < static_cast(std::numeric_limits::min() - y)); + 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(std::numeric_limits::max() - y); + 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(std::numeric_limits::max() + y)) - : (x < static_cast(std::numeric_limits::min() + y)); + 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 @@ -118,18 +122,18 @@ 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 > std::numeric_limits::max() / y) : (y < std::numeric_limits::min() / x)) - : ((y > 0) ? (x < std::numeric_limits::min() / y) : (y < std::numeric_limits::max() / x)); + : (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 > std::numeric_limits::max() / y); + 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 == std::numeric_limits::min()) && (y == static_cast(-1)); + return (x == limits::min()) && (y == static_cast(-1)); } template constexpr bool is_min_over_minus_one(T, T, std::false_type /*unsigned*/) noexcept @@ -143,17 +147,17 @@ constexpr bool is_min_over_minus_one(T, T, std::false_type /*unsigned*/) noexcep template constexpr T saturating_add_portable(T x, T y) noexcept { - return add_overflows(x, y, std::is_signed{}) ? add_saturated(x, std::is_signed{}) : static_cast(x + y); + 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, std::is_signed{}) ? sub_saturated(x, std::is_signed{}) : static_cast(x - y); + 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, std::is_signed{}) ? mul_saturated(x, y, std::is_signed{}) : static_cast(x * y); + return mul_overflows(x, y, is_signed{}) ? mul_saturated(x, y, is_signed{}) : static_cast(x * y); } #if CETL_PF26_DETAIL_OVERFLOW_BUILTINS @@ -161,19 +165,19 @@ template constexpr T saturating_add_builtin(T x, T y) noexcept { T result{}; - return __builtin_add_overflow(x, y, &result) ? add_saturated(x, std::is_signed{}) : 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, std::is_signed{}) : 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, std::is_signed{}) : result; + return __builtin_mul_overflow(x, y, &result) ? mul_saturated(x, y, is_signed{}) : result; } #endif } // namespace detail @@ -219,7 +223,7 @@ CETL_NODISCARD constexpr T saturating_mul(T x, T y) noexcept /// 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_limits::min() / -1`, returns `numeric_limits::max()`. +/// `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. @@ -227,8 +231,8 @@ 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, std::is_signed{}) ? std::numeric_limits::max() - : static_cast(x / y); + 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]). @@ -238,9 +242,9 @@ CETL_NODISCARD constexpr T saturating_div(T x, T y) noexcept template = 0, detail::enable_if_integer_t = 0> CETL_NODISCARD constexpr T saturating_cast(U x) noexcept { - return pf20::cmp_less(x, std::numeric_limits::min()) ? std::numeric_limits::min() - : pf20::cmp_greater(x, std::numeric_limits::max()) ? std::numeric_limits::max() - : static_cast(x); + 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 From 55dfa94fbb58685a056e9cfadae0c37d93835f08 Mon Sep 17 00:00:00 2001 From: Scott Dixon Date: Sat, 24 Oct 2026 21:00:08 -0700 Subject: [PATCH 3/3] Add the cpp-26 amd64 jobs to the release contract The release tooling's required_jobs() must list every CETLVaSt matrix entry, and its contract test fails when the workflow matrix and the list disagree. The cpp-26 standard added to the amd64 matrix was missing from the list, which failed release-tests and therefore release-ready on #255. Co-Authored-By: Claude Fable 5.1 --- scripts/release_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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,)), ):