Skip to content

Add C++26 saturation arithmetic polyfills (#39) - #265

Open
thirtytwobits wants to merge 3 commits into
mainfrom
claude/issue-39-saturating
Open

thirtytwobits wants to merge 3 commits into
mainfrom
claude/issue-39-saturating

Conversation

@thirtytwobits

@thirtytwobits thirtytwobits commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

Summary

Implements the saturation arithmetic agreed in #39 (proposal, decisions): the C++26 saturation arithmetic as a true polyfill back to C++14, plus the two smaller standard facilities the consumers need around it. The cetl::saturating<T> wrapper type from the proposal is not part of this PR. Supersedes #255.

  • cetl/pf26/numeric.hpp: cetl::pf26::saturating_add, saturating_sub, saturating_mul, saturating_div, saturating_cast, exact to [numeric.sat] under the final P4052R0 names: one operand type that is never promoted, bool and the character types excluded, y != 0 as a precondition on division (CETL_DEBUG_ASSERT, CDE_numeric_001), and min / -1 saturating to max. __builtin_*_overflow is used where the compiler has it (GCC 5+, Clang); otherwise a portable implementation that never forms an overflowing expression. Both are constant expressions in C++14 and both are compiled and tested on every toolchain.
  • cetl/pf26/cetlpf.hpp: selects the implementation by __cpp_lib_saturation_arithmetic, not __cplusplus (every current compiler still reports 202400L for -std=c++26): >= 202603L uses std::saturating_* directly; >= 202311L (libstdc++ 14/15, libc++ 18–22) forwards to std::add_sat/std::saturate_cast under the final names; otherwise cetl::pf26.
  • cetl/pf20/utility.hpp: cmp_equal, cmp_not_equal, cmp_less, cmp_greater, cmp_less_equal, cmp_greater_equal, in_range (C++20 polyfills; saturating_cast needs sign-correct comparisons). Wired into cetl/pf20/cetlpf.hpp.
  • The integer traits behind both headers ignore cv-qualifiers (so saturating_add<const bool> is rejected like bool) and support GCC's __int128 in the strict -std=c++NN modes where the standard traits and std::numeric_limits do not describe it.
  • cetl/pf17/algorithm.hpp: clamp (C++17 polyfill; nunavut's saturated intN fields clamp to an arbitrary range). Wired into cetl/pf17/cetlpf.hpp.
  • cetl/cetl.hpp: CETL_CPP_STANDARD_23 / CETL_CPP_STANDARD_26 and the example 13 page.
  • CETLVaSt: cpp-26 presets generated with tcpm for every toolchain except GCC 7, cpp-26 added to the amd64 CI matrix (in ts26.4.5 both g++ 15 and clang 22 take the 202311L branch, so CI exercises the adapted-std path; the polyfill path is covered by C++14/17/20/23) and to the release contract (required_jobs() in scripts/release_tools.py).

Tests

  • test_pf26_numeric.cpp: constant-expression cases; every operand pair for int8_t/uint8_t; every first operand of int16_t/uint16_t against edge and sampled second operands; edge pairs and 100k random pairs for the 32-bit types against a long long oracle and for the 64-bit types against a 128-bit oracle (where available); saturating_cast from every value of the 8- and 16-bit types into every fixed-width type; 64-bit edge tables; the portable implementations checked alongside the builtin ones; the cetlpf.hpp selection; a death test for division by zero.
  • test_pf20_utility.cpp: the classic mixed-sign cases as constant expressions and a sweep over every pair of fixed-width types against a sign-aware oracle.
  • test_pf17_algorithm.cpp: value, reference-identity, comparator and floating-point cases, and a death test for inverted bounds (CDE_algorithm_001).
  • Compile-failure tests: bool operands and mixed operand types for saturating_add, bool for cmp_less.
  • fuzz_saturation_arithmetic.cpp: differential libFuzzer target against a 128-bit oracle for all five functions and every fixed-width type, with a seed corpus; the fuzz CMake now loops over its targets.
  • example_13_saturating_arithmetic.cpp: counter saturation, uptime narrowing, explicit operand type, clamp and in_range.

Validation

Run on #255 before the branch was rebased onto main.

Toolchain / mode Path exercised Result
GCC 13, C++14, Debug + Release cetl::pf26 polyfill (builtins and portable) full run_unittests + run_examples pass; compile-failure tests 9/9
Homebrew clang 23 / libc++ 23, C++26, Debug + DebugEP + Release std::saturating_* (202603L) new tests pass; compile-failure tests 9/9
g++-15 / libstdc++ 15, C++26, Debug std::add_sat family adapted (202311L) new tests pass; compile-failure tests 9/9
g++-7.5 (toolshed ts24.4.3), C++14 syntax pass of every new test source with CETL's warning set clean
clang-format 19.1.7 (ts24.4.3) and 22.1.2 (ts26.4.3) all touched headers clean
Doxygen 1.14 generate_CETL_docs no new warnings; example 13 page and pf26/cetlpf.hpp snippets render
libFuzzer (Homebrew clang, ASan + UBSan, C++14) both fuzz smoke tests pass

Not covered locally: Apple clang in C++26 mode (CMake 4.1 has no CXX26 flags for AppleClang); the pre-rename libc++ branch was only exercised with a standalone build during the design phase.

Closes #39.

🤖 Generated with Claude Code

thirtytwobits and others added 2 commits October 25, 2026 08:33
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<T> wrapper type follows in a separate PR.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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<const bool> or cmp_less<const char, char> 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hoist saturating math.hpp out of garage, libcyphal and put into CETL

1 participant