From 31f816a6d88ca9d1f06ff5aaf2c8f0c14ed3f436 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Sat, 21 Mar 2026 08:42:24 -0500 Subject: [PATCH] AK: Add SaturatingMath.h with branchless saturating arithmetic Add standalone saturating_add(), saturating_sub(), and saturating_mul() free functions for integral types. The signed implementations are fully branchless, using __builtin_add/sub/mul_overflow combined with bitmask selection. --- AK/SaturatingMath.h | 85 +++++++++ Tests/AK/CMakeLists.txt | 1 + Tests/AK/TestSaturatingMath.cpp | 302 ++++++++++++++++++++++++++++++++ 3 files changed, 388 insertions(+) create mode 100644 AK/SaturatingMath.h create mode 100644 Tests/AK/TestSaturatingMath.cpp diff --git a/AK/SaturatingMath.h b/AK/SaturatingMath.h new file mode 100644 index 00000000000..7e45aa48d7e --- /dev/null +++ b/AK/SaturatingMath.h @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace AK { + +template +requires(Signed) +constexpr T saturating_add(T a, T b) +{ + using U = MakeUnsigned; + T result; + U overflowed = __builtin_add_overflow(a, b, &result); + T saturated = static_cast(static_cast(NumericLimits::max()) + (static_cast(a) >> (sizeof(T) * 8 - 1))); + U mask = -overflowed; + return static_cast((static_cast(saturated) & mask) | (static_cast(result) & ~mask)); +} + +template +requires(Unsigned) +constexpr T saturating_add(T a, T b) +{ + T result = a + b; + result |= -static_cast(result < a); + return result; +} + +template +requires(Signed) +constexpr T saturating_sub(T a, T b) +{ + using U = MakeUnsigned; + T result; + U overflowed = __builtin_sub_overflow(a, b, &result); + T saturated = static_cast(static_cast(NumericLimits::max()) + (static_cast(a) >> (sizeof(T) * 8 - 1))); + U mask = -overflowed; + return static_cast((static_cast(saturated) & mask) | (static_cast(result) & ~mask)); +} + +template +requires(Unsigned) +constexpr T saturating_sub(T a, T b) +{ + T result = a - b; + result &= -static_cast(result <= a); + return result; +} + +template +requires(Signed) +constexpr T saturating_mul(T a, T b) +{ + using U = MakeUnsigned; + T result; + U overflowed = __builtin_mul_overflow(a, b, &result); + // Same signs → positive overflow → max. Different signs → negative overflow → min. + T saturated = static_cast(static_cast(NumericLimits::max()) + ((static_cast(a) ^ static_cast(b)) >> (sizeof(T) * 8 - 1))); + U mask = -overflowed; + return static_cast((static_cast(saturated) & mask) | (static_cast(result) & ~mask)); +} + +template +requires(Unsigned) +constexpr T saturating_mul(T a, T b) +{ + T result; + if (__builtin_mul_overflow(a, b, &result)) + return NumericLimits::max(); + return result; +} + +} + +#if USING_AK_GLOBALLY +using AK::saturating_add; +using AK::saturating_mul; +using AK::saturating_sub; +#endif diff --git a/Tests/AK/CMakeLists.txt b/Tests/AK/CMakeLists.txt index b64c0d56293..a85bbb4f45e 100644 --- a/Tests/AK/CMakeLists.txt +++ b/Tests/AK/CMakeLists.txt @@ -15,6 +15,7 @@ set(AK_TEST_SOURCES TestByteString.cpp TestCharacterTypes.cpp TestChecked.cpp + TestSaturatingMath.cpp TestCircularBuffer.cpp TestCircularQueue.cpp TestDemangle.cpp diff --git a/Tests/AK/TestSaturatingMath.cpp b/Tests/AK/TestSaturatingMath.cpp new file mode 100644 index 00000000000..ff21469445c --- /dev/null +++ b/Tests/AK/TestSaturatingMath.cpp @@ -0,0 +1,302 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +#include +#include + +// Normal signed add (nowhere near saturation) + +TEST_CASE(signed_add_normal) +{ + EXPECT_EQ(saturating_add(0, 0), 0); + EXPECT_EQ(saturating_add(1, 1), 2); + EXPECT_EQ(saturating_add(3, 4), 7); + EXPECT_EQ(saturating_add(-3, -4), -7); + EXPECT_EQ(saturating_add(3, -4), -1); + EXPECT_EQ(saturating_add(-3, 4), 1); + EXPECT_EQ(saturating_add(100, 200), 300); + EXPECT_EQ(saturating_add(-100, -200), -300); + EXPECT_EQ(saturating_add(100, -200), -100); + EXPECT_EQ(saturating_add(1000000, 2000000), 3000000); +} + +// Signed add: cross-boundary (large values that cancel out, should NOT saturate) + +TEST_CASE(signed_add_cross_boundary) +{ + EXPECT_EQ(saturating_add(NumericLimits::max(), NumericLimits::min()), -1); + EXPECT_EQ(saturating_add(NumericLimits::min(), NumericLimits::max()), -1); + EXPECT_EQ(saturating_add(NumericLimits::max(), -NumericLimits::max()), 0); + EXPECT_EQ(saturating_add(NumericLimits::max() / 2, -(NumericLimits::max() / 2)), 0); +} + +// Signed add: off-by-one at saturation point + +TEST_CASE(signed_add_off_by_one) +{ + // Exactly at max: no saturation + EXPECT_EQ(saturating_add(NumericLimits::max() - 1, 1), NumericLimits::max()); + EXPECT_EQ(saturating_add(NumericLimits::max() - 10, 10), NumericLimits::max()); + // One past max: saturates + EXPECT_EQ(saturating_add(NumericLimits::max(), 1), NumericLimits::max()); + EXPECT_EQ(saturating_add(NumericLimits::max() - 9, 10), NumericLimits::max()); + + // Exactly at min: no saturation + EXPECT_EQ(saturating_add(NumericLimits::min() + 1, -1), NumericLimits::min()); + EXPECT_EQ(saturating_add(NumericLimits::min() + 10, -10), NumericLimits::min()); + // One past min: saturates + EXPECT_EQ(saturating_add(NumericLimits::min(), -1), NumericLimits::min()); + EXPECT_EQ(saturating_add(NumericLimits::min() + 9, -10), NumericLimits::min()); +} + +// Signed add: extreme overflow + +TEST_CASE(signed_add_extreme_overflow) +{ + EXPECT_EQ(saturating_add(NumericLimits::max(), NumericLimits::max()), NumericLimits::max()); + EXPECT_EQ(saturating_add(NumericLimits::min(), NumericLimits::min()), NumericLimits::min()); +} + +// Signed add: identity + +TEST_CASE(signed_add_identity) +{ + EXPECT_EQ(saturating_add(NumericLimits::max(), 0), NumericLimits::max()); + EXPECT_EQ(saturating_add(NumericLimits::min(), 0), NumericLimits::min()); + EXPECT_EQ(saturating_add(0, NumericLimits::max()), NumericLimits::max()); + EXPECT_EQ(saturating_add(0, NumericLimits::min()), NumericLimits::min()); +} + +// Normal signed sub (nowhere near saturation) + +TEST_CASE(signed_sub_normal) +{ + EXPECT_EQ(saturating_sub(0, 0), 0); + EXPECT_EQ(saturating_sub(7, 4), 3); + EXPECT_EQ(saturating_sub(4, 7), -3); + EXPECT_EQ(saturating_sub(-7, -4), -3); + EXPECT_EQ(saturating_sub(3, -4), 7); + EXPECT_EQ(saturating_sub(-3, 4), -7); + EXPECT_EQ(saturating_sub(300, 200), 100); + EXPECT_EQ(saturating_sub(-300, -200), -100); + EXPECT_EQ(saturating_sub(1000000, 2000000), -1000000); +} + +// Signed sub: cross-boundary (should NOT saturate) + +TEST_CASE(signed_sub_cross_boundary) +{ + EXPECT_EQ(saturating_sub(NumericLimits::max(), NumericLimits::max()), 0); + EXPECT_EQ(saturating_sub(NumericLimits::min(), NumericLimits::min()), 0); + EXPECT_EQ(saturating_sub(0, NumericLimits::max()), NumericLimits::min() + 1); +} + +// Signed sub: off-by-one at saturation point + +TEST_CASE(signed_sub_off_by_one) +{ + // Exactly at max: no saturation + EXPECT_EQ(saturating_sub(NumericLimits::max() - 1, -1), NumericLimits::max()); + EXPECT_EQ(saturating_sub(NumericLimits::max() - 10, -10), NumericLimits::max()); + // One past max: saturates + EXPECT_EQ(saturating_sub(NumericLimits::max(), -1), NumericLimits::max()); + EXPECT_EQ(saturating_sub(NumericLimits::max() - 9, -10), NumericLimits::max()); + + // Exactly at min: no saturation + EXPECT_EQ(saturating_sub(NumericLimits::min() + 1, 1), NumericLimits::min()); + EXPECT_EQ(saturating_sub(NumericLimits::min() + 10, 10), NumericLimits::min()); + // One past min: saturates + EXPECT_EQ(saturating_sub(NumericLimits::min(), 1), NumericLimits::min()); + EXPECT_EQ(saturating_sub(NumericLimits::min() + 9, 10), NumericLimits::min()); +} + +// Signed sub: extreme overflow + +TEST_CASE(signed_sub_extreme_overflow) +{ + EXPECT_EQ(saturating_sub(NumericLimits::max(), NumericLimits::min()), NumericLimits::max()); + EXPECT_EQ(saturating_sub(NumericLimits::min(), NumericLimits::max()), NumericLimits::min()); +} + +// Signed sub: identity + +TEST_CASE(signed_sub_identity) +{ + EXPECT_EQ(saturating_sub(NumericLimits::max(), 0), NumericLimits::max()); + EXPECT_EQ(saturating_sub(NumericLimits::min(), 0), NumericLimits::min()); +} + +// Normal unsigned add (nowhere near saturation) + +TEST_CASE(unsigned_add_normal) +{ + EXPECT_EQ(saturating_add(0u, 0u), 0u); + EXPECT_EQ(saturating_add(1u, 1u), 2u); + EXPECT_EQ(saturating_add(3u, 4u), 7u); + EXPECT_EQ(saturating_add(100u, 200u), 300u); + EXPECT_EQ(saturating_add(1000000u, 2000000u), 3000000u); +} + +// Unsigned add: off-by-one at saturation point + +TEST_CASE(unsigned_add_off_by_one) +{ + EXPECT_EQ(saturating_add(NumericLimits::max() - 1, 1u), NumericLimits::max()); + EXPECT_EQ(saturating_add(NumericLimits::max() - 10, 10u), NumericLimits::max()); + EXPECT_EQ(saturating_add(NumericLimits::max(), 1u), NumericLimits::max()); + EXPECT_EQ(saturating_add(NumericLimits::max() - 9, 10u), NumericLimits::max()); +} + +// Unsigned add: extreme overflow and identity + +TEST_CASE(unsigned_add_extreme_and_identity) +{ + EXPECT_EQ(saturating_add(NumericLimits::max(), NumericLimits::max()), NumericLimits::max()); + EXPECT_EQ(saturating_add(NumericLimits::max(), 0u), NumericLimits::max()); + EXPECT_EQ(saturating_add(0u, NumericLimits::max()), NumericLimits::max()); +} + +// Normal unsigned sub (nowhere near saturation) + +TEST_CASE(unsigned_sub_normal) +{ + EXPECT_EQ(saturating_sub(0u, 0u), 0u); + EXPECT_EQ(saturating_sub(7u, 4u), 3u); + EXPECT_EQ(saturating_sub(4u, 4u), 0u); + EXPECT_EQ(saturating_sub(300u, 200u), 100u); + EXPECT_EQ(saturating_sub(NumericLimits::max(), NumericLimits::max()), 0u); +} + +// Unsigned sub: off-by-one at saturation point + +TEST_CASE(unsigned_sub_off_by_one) +{ + EXPECT_EQ(saturating_sub(1u, 1u), 0u); + EXPECT_EQ(saturating_sub(10u, 10u), 0u); + EXPECT_EQ(saturating_sub(0u, 1u), 0u); + EXPECT_EQ(saturating_sub(9u, 10u), 0u); +} + +// Unsigned sub: extreme underflow and identity + +TEST_CASE(unsigned_sub_extreme_and_identity) +{ + EXPECT_EQ(saturating_sub(0u, NumericLimits::max()), 0u); + EXPECT_EQ(saturating_sub(5u, 10u), 0u); + EXPECT_EQ(saturating_sub(NumericLimits::max(), 0u), NumericLimits::max()); +} + +// Normal signed mul (nowhere near saturation) + +TEST_CASE(signed_mul_normal) +{ + EXPECT_EQ(saturating_mul(0, 0), 0); + EXPECT_EQ(saturating_mul(1, 1), 1); + EXPECT_EQ(saturating_mul(3, 4), 12); + EXPECT_EQ(saturating_mul(-3, 4), -12); + EXPECT_EQ(saturating_mul(3, -4), -12); + EXPECT_EQ(saturating_mul(-3, -4), 12); + EXPECT_EQ(saturating_mul(100, 200), 20000); + EXPECT_EQ(saturating_mul(0, NumericLimits::max()), 0); + EXPECT_EQ(saturating_mul(NumericLimits::max(), 0), 0); + EXPECT_EQ(saturating_mul(1, NumericLimits::max()), NumericLimits::max()); + EXPECT_EQ(saturating_mul(NumericLimits::max(), 1), NumericLimits::max()); + EXPECT_EQ(saturating_mul(-1, NumericLimits::max()), -NumericLimits::max()); +} + +TEST_CASE(signed_mul_overflow) +{ + EXPECT_EQ(saturating_mul(NumericLimits::max(), 2), NumericLimits::max()); + EXPECT_EQ(saturating_mul(NumericLimits::max(), NumericLimits::max()), NumericLimits::max()); + EXPECT_EQ(saturating_mul(NumericLimits::min(), 2), NumericLimits::min()); + EXPECT_EQ(saturating_mul(NumericLimits::min(), NumericLimits::min()), NumericLimits::max()); + EXPECT_EQ(saturating_mul(NumericLimits::max(), -2), NumericLimits::min()); + EXPECT_EQ(saturating_mul(NumericLimits::max(), NumericLimits::min()), NumericLimits::min()); +} + +// Normal unsigned mul (nowhere near saturation) + +TEST_CASE(unsigned_mul_normal) +{ + EXPECT_EQ(saturating_mul(0u, 0u), 0u); + EXPECT_EQ(saturating_mul(3u, 4u), 12u); + EXPECT_EQ(saturating_mul(100u, 200u), 20000u); + EXPECT_EQ(saturating_mul(0u, NumericLimits::max()), 0u); + EXPECT_EQ(saturating_mul(1u, NumericLimits::max()), NumericLimits::max()); +} + +TEST_CASE(unsigned_mul_overflow) +{ + EXPECT_EQ(saturating_mul(NumericLimits::max(), 2u), NumericLimits::max()); + EXPECT_EQ(saturating_mul(NumericLimits::max(), NumericLimits::max()), NumericLimits::max()); +} + +// All integer sizes + +TEST_CASE(i8_saturation) +{ + EXPECT_EQ(saturating_add(static_cast(50), static_cast(30)), static_cast(80)); + EXPECT_EQ(saturating_add(static_cast(100), static_cast(100)), NumericLimits::max()); + EXPECT_EQ(saturating_add(static_cast(-100), static_cast(-100)), NumericLimits::min()); + EXPECT_EQ(saturating_add(NumericLimits::max(), NumericLimits::min()), static_cast(-1)); + + EXPECT_EQ(saturating_sub(static_cast(50), static_cast(30)), static_cast(20)); + EXPECT_EQ(saturating_sub(static_cast(-100), static_cast(100)), NumericLimits::min()); + EXPECT_EQ(saturating_sub(static_cast(100), static_cast(-100)), NumericLimits::max()); +} + +TEST_CASE(i16_saturation) +{ + EXPECT_EQ(saturating_add(static_cast(1000), static_cast(2000)), static_cast(3000)); + EXPECT_EQ(saturating_add(NumericLimits::max(), static_cast(1)), NumericLimits::max()); + EXPECT_EQ(saturating_add(NumericLimits::min(), static_cast(-1)), NumericLimits::min()); + EXPECT_EQ(saturating_add(NumericLimits::max(), NumericLimits::min()), static_cast(-1)); + + EXPECT_EQ(saturating_sub(static_cast(3000), static_cast(1000)), static_cast(2000)); + EXPECT_EQ(saturating_sub(NumericLimits::max(), static_cast(-1)), NumericLimits::max()); + EXPECT_EQ(saturating_sub(NumericLimits::min(), static_cast(1)), NumericLimits::min()); +} + +TEST_CASE(i64_saturation) +{ + EXPECT_EQ(saturating_add(static_cast(1000000000), static_cast(2000000000)), static_cast(3000000000)); + EXPECT_EQ(saturating_add(NumericLimits::max(), static_cast(1)), NumericLimits::max()); + EXPECT_EQ(saturating_add(NumericLimits::min(), static_cast(-1)), NumericLimits::min()); + EXPECT_EQ(saturating_add(NumericLimits::max(), NumericLimits::min()), static_cast(-1)); + + EXPECT_EQ(saturating_sub(NumericLimits::min(), static_cast(1)), NumericLimits::min()); + EXPECT_EQ(saturating_sub(NumericLimits::max(), static_cast(-1)), NumericLimits::max()); + EXPECT_EQ(saturating_sub(NumericLimits::max(), NumericLimits::max()), static_cast(0)); +} + +TEST_CASE(u8_saturation) +{ + EXPECT_EQ(saturating_add(static_cast(50), static_cast(30)), static_cast(80)); + EXPECT_EQ(saturating_add(static_cast(200), static_cast(200)), NumericLimits::max()); + + EXPECT_EQ(saturating_sub(static_cast(80), static_cast(30)), static_cast(50)); + EXPECT_EQ(saturating_sub(static_cast(0), static_cast(1)), static_cast(0)); +} + +TEST_CASE(u16_saturation) +{ + EXPECT_EQ(saturating_add(static_cast(1000), static_cast(2000)), static_cast(3000)); + EXPECT_EQ(saturating_add(NumericLimits::max(), static_cast(1)), NumericLimits::max()); + + EXPECT_EQ(saturating_sub(static_cast(3000), static_cast(1000)), static_cast(2000)); + EXPECT_EQ(saturating_sub(static_cast(0), static_cast(1)), static_cast(0)); +} + +TEST_CASE(u64_saturation) +{ + EXPECT_EQ(saturating_add(static_cast(1000000000), static_cast(2000000000)), static_cast(3000000000)); + EXPECT_EQ(saturating_add(NumericLimits::max(), static_cast(1)), NumericLimits::max()); + + EXPECT_EQ(saturating_sub(static_cast(3000000000), static_cast(1000000000)), static_cast(2000000000)); + EXPECT_EQ(saturating_sub(static_cast(0), static_cast(1)), static_cast(0)); +}