[libcxx-commits] [libcxx] [libc++] std::abs support for _BitInt(N) and __int128 (PR #196532)

Xavier Roche via libcxx-commits libcxx-commits at lists.llvm.org
Tue Jun 16 00:48:56 PDT 2026


https://github.com/xroche updated https://github.com/llvm/llvm-project/pull/196532

>From c3fa4b590aeb9e038653b7bf5e909cf69b3610be Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Fri, 8 May 2026 15:05:42 +0200
Subject: [PATCH 1/6] [libc++] Add std::abs support for _BitInt(N) and __int128

Add a constrained template overload of std::abs in __math/abs.h that
covers signed integer types not handled by the existing builtin
overloads for int, long, and long long. The new template matches any
signed integer type with sizeof(_Tp) >= sizeof(int), so __int128_t
and signed _BitInt(N >= 32) work, while shorter standard types still
go through abs(int) via integer promotion.

The implementation is x < 0 ? -x : x, the same conditional negation
the standard uses to define abs for signed integers.

Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>
---
 libcxx/include/__math/abs.h                   | 15 ++++
 .../numerics/c.math/abs.bitint.pass.cpp       | 82 +++++++++++++++++++
 2 files changed, 97 insertions(+)
 create mode 100644 libcxx/test/libcxx/numerics/c.math/abs.bitint.pass.cpp

diff --git a/libcxx/include/__math/abs.h b/libcxx/include/__math/abs.h
index b780159f11ebf..5610d5685ed77 100644
--- a/libcxx/include/__math/abs.h
+++ b/libcxx/include/__math/abs.h
@@ -11,7 +11,9 @@
 
 #include <__config>
 #include <__type_traits/enable_if.h>
+#include <__type_traits/integer_traits.h>
 #include <__type_traits/is_integral.h>
+#include <__type_traits/is_same.h>
 
 #if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
 #  pragma GCC system_header
@@ -63,6 +65,19 @@ template <class = int>
   return __builtin_llabs(__x);
 }
 
+// Overload for __int128 and signed _BitInt(N >= 32) types. The sizeof
+// check excludes shorter signed types (e.g. signed short, signed
+// _BitInt(16)). The standard overloads catch the standard types via
+// integer promotion. _BitInt narrower than int does not promote and
+// is intentionally unsupported by std::abs.
+template <class _Tp,
+          __enable_if_t<__is_signed_integer_v<_Tp> && !is_same<_Tp, int>::value && !is_same<_Tp, long>::value &&
+                            !is_same<_Tp, long long>::value && (sizeof(_Tp) >= sizeof(int)),
+                        int> = 0>
+[[__nodiscard__]] _LIBCPP_HIDE_FROM_ABI _Tp abs(_Tp __x) _NOEXCEPT {
+  return __x < 0 ? -__x : __x;
+}
+
 } // namespace __math
 
 _LIBCPP_END_NAMESPACE_STD
diff --git a/libcxx/test/libcxx/numerics/c.math/abs.bitint.pass.cpp b/libcxx/test/libcxx/numerics/c.math/abs.bitint.pass.cpp
new file mode 100644
index 0000000000000..be0b77de8701b
--- /dev/null
+++ b/libcxx/test/libcxx/numerics/c.math/abs.bitint.pass.cpp
@@ -0,0 +1,82 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// std::abs for _BitInt(N) and __int128 -- libc++ extension over the standard
+// abs(int / long / long long) overloads. The new template covers signed
+// integer types not handled by the existing builtin overloads, gated on
+// __is_signed_integer_v plus a sizeof(_Tp) >= sizeof(int) check that keeps
+// shorter standard types on the integer-promotion path.
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+
+#include <cassert>
+#include <cmath>
+
+#include "test_macros.h"
+
+#if TEST_HAS_EXTENSION(bit_int)
+template <int N>
+void test_signed_bitint() {
+  using T = signed _BitInt(N);
+  assert(std::abs(T(0)) == T(0));
+  assert(std::abs(T(1)) == T(1));
+  assert(std::abs(T(42)) == T(42));
+  assert(std::abs(T(-1)) == T(1));
+  assert(std::abs(T(-42)) == T(42));
+}
+#endif
+
+int main(int, char**) {
+#if TEST_HAS_EXTENSION(bit_int)
+  // _BitInt(N) with N < sizeof(int) * CHAR_BIT does not match the new
+  // template (sizeof guard) and does not implicit-promote to int either, so
+  // it has no abs overload. Start at 32 bits.
+  test_signed_bitint<32>();
+  test_signed_bitint<64>();
+
+  // Odd widths >= 32 bits.
+  test_signed_bitint<33>();
+  test_signed_bitint<63>();
+  test_signed_bitint<65>();
+
+#  if __BITINT_MAXWIDTH__ >= 128
+  test_signed_bitint<128>();
+#  endif
+
+#  if __BITINT_MAXWIDTH__ >= 256
+  test_signed_bitint<129>();
+  test_signed_bitint<256>();
+
+  // Large value: |-2^200| == 2^200. Python: abs(-(1 << 200)) == 1 << 200.
+  signed _BitInt(256) v        = -(static_cast<signed _BitInt(256)>(1) << 200);
+  signed _BitInt(256) expected = static_cast<signed _BitInt(256)>(1) << 200;
+  assert(std::abs(v) == expected);
+#  endif
+
+#  if __BITINT_MAXWIDTH__ >= 1024
+  test_signed_bitint<512>();
+  test_signed_bitint<1024>();
+#  endif
+#endif // TEST_HAS_EXTENSION(bit_int)
+
+#if _LIBCPP_HAS_INT128
+  assert(std::abs(static_cast<__int128_t>(0)) == 0);
+  assert(std::abs(static_cast<__int128_t>(42)) == 42);
+  assert(std::abs(static_cast<__int128_t>(-42)) == 42);
+  assert(std::abs(static_cast<__int128_t>(-1)) == 1);
+
+  // INT128_MIN is unrepresentable as a positive __int128; -__x overflows.
+  // Skip that case -- the standard's abs(int) has the same issue with
+  // INT_MIN, so this is consistent.
+  __int128_t big_neg = -((static_cast<__int128_t>(1) << 100));
+  __int128_t big_pos = static_cast<__int128_t>(1) << 100;
+  assert(std::abs(big_neg) == big_pos);
+#endif
+
+  return 0;
+}

>From 2483bd6071500b8516e9563e5190d16cf828ec67 Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Fri, 8 May 2026 15:23:33 +0200
Subject: [PATCH 2/6] [libc++][test] Cover boundary values in std::abs _BitInt
 test

Add T_MAX (identity) and T_MIN + 1 (cleanly negates to T_MAX) to the
per-width signed _BitInt test. The T_MIN case is intentionally not
tested -- abs(T_MIN) overflows in signed arithmetic, which is
undefined behaviour, matching the standard's abs(int) caveat for
INT_MIN.

Same boundary added to the __int128_t section for INT128_MAX and
INT128_MIN + 1.

Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>
---
 .../numerics/c.math/abs.bitint.pass.cpp       | 19 ++++++++++++++++++-
 1 file changed, 18 insertions(+), 1 deletion(-)

diff --git a/libcxx/test/libcxx/numerics/c.math/abs.bitint.pass.cpp b/libcxx/test/libcxx/numerics/c.math/abs.bitint.pass.cpp
index be0b77de8701b..05bfac0a32f76 100644
--- a/libcxx/test/libcxx/numerics/c.math/abs.bitint.pass.cpp
+++ b/libcxx/test/libcxx/numerics/c.math/abs.bitint.pass.cpp
@@ -15,7 +15,7 @@
 // UNSUPPORTED: c++03, c++11, c++14, c++17
 
 #include <cassert>
-#include <cmath>
+#include <cstdlib>
 
 #include "test_macros.h"
 
@@ -28,6 +28,16 @@ void test_signed_bitint() {
   assert(std::abs(T(42)) == T(42));
   assert(std::abs(T(-1)) == T(1));
   assert(std::abs(T(-42)) == T(42));
+
+  // Boundary cases. T_MAX has no overflow (identity). T_MIN + 1 negates to
+  // T_MAX, which is the largest negative value abs handles cleanly. T_MIN
+  // itself is intentionally not tested: -T_MIN overflows in signed
+  // arithmetic, which is undefined behaviour. The same caveat applies to
+  // std::abs(int) with INT_MIN.
+  T t_max       = static_cast<T>(~static_cast<unsigned _BitInt(N)>(0) >> 1);
+  T t_min_plus1 = -t_max; // == T_MIN + 1
+  assert(std::abs(t_max) == t_max);
+  assert(std::abs(t_min_plus1) == t_max);
 }
 #endif
 
@@ -76,6 +86,13 @@ int main(int, char**) {
   __int128_t big_neg = -((static_cast<__int128_t>(1) << 100));
   __int128_t big_pos = static_cast<__int128_t>(1) << 100;
   assert(std::abs(big_neg) == big_pos);
+
+  // Boundary: INT128_MAX (identity) and INT128_MIN+1 (negation cleanly
+  // produces INT128_MAX).
+  __int128_t int128_max       = static_cast<__int128_t>(~static_cast<__uint128_t>(0) >> 1);
+  __int128_t int128_min_plus1 = -int128_max;
+  assert(std::abs(int128_max) == int128_max);
+  assert(std::abs(int128_min_plus1) == int128_max);
 #endif
 
   return 0;

>From db68dddf77a59f4c224a7e56d8d5de9654175d0a Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Fri, 8 May 2026 16:38:53 +0200
Subject: [PATCH 3/6] [libc++][NFC] Address review nits on std::abs _BitInt
 overload

Two follow-ups from the pre-push review of #196532:

- Tighten the comment in __math/abs.h. The previous text said
  "_BitInt(N >= 32)", which is misleading: the actual gate is
  sizeof(_Tp) >= sizeof(int), and that catches _BitInt(24) on x86-64
  (sizeof 4) too. Also explain why the !is_same exclusions for int /
  long / long long are load-bearing: signed _BitInt(33..64) shares
  sizeof with long long, so a sizeof-only gate would let those types
  compete with the existing builtin-based overloads.

- Add ASSERT_SAME_TYPE for the return type in the per-width signed
  _BitInt test and in the __int128_t section. Pins that std::abs
  preserves the input type rather than decaying to long long or any
  other surprise.

Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>
---
 libcxx/include/__math/abs.h                       | 15 ++++++++++-----
 .../libcxx/numerics/c.math/abs.bitint.pass.cpp    |  2 ++
 2 files changed, 12 insertions(+), 5 deletions(-)

diff --git a/libcxx/include/__math/abs.h b/libcxx/include/__math/abs.h
index 5610d5685ed77..4181173f6d82f 100644
--- a/libcxx/include/__math/abs.h
+++ b/libcxx/include/__math/abs.h
@@ -65,11 +65,16 @@ template <class = int>
   return __builtin_llabs(__x);
 }
 
-// Overload for __int128 and signed _BitInt(N >= 32) types. The sizeof
-// check excludes shorter signed types (e.g. signed short, signed
-// _BitInt(16)). The standard overloads catch the standard types via
-// integer promotion. _BitInt narrower than int does not promote and
-// is intentionally unsupported by std::abs.
+// Overload for __int128 and signed _BitInt(N) where sizeof(_Tp) >=
+// sizeof(int). The sizeof check excludes shorter signed types (signed
+// short, signed _BitInt(16)). The standard overloads catch the standard
+// types via integer promotion. _BitInt narrower than int does not
+// promote and is intentionally unsupported by std::abs.
+//
+// The explicit !is_same exclusions for int/long/long long are
+// load-bearing: signed _BitInt(33..64) shares sizeof with long long, so
+// a sizeof-only gate would let those types compete with the existing
+// builtin-based overloads.
 template <class _Tp,
           __enable_if_t<__is_signed_integer_v<_Tp> && !is_same<_Tp, int>::value && !is_same<_Tp, long>::value &&
                             !is_same<_Tp, long long>::value && (sizeof(_Tp) >= sizeof(int)),
diff --git a/libcxx/test/libcxx/numerics/c.math/abs.bitint.pass.cpp b/libcxx/test/libcxx/numerics/c.math/abs.bitint.pass.cpp
index 05bfac0a32f76..11f4f121450b7 100644
--- a/libcxx/test/libcxx/numerics/c.math/abs.bitint.pass.cpp
+++ b/libcxx/test/libcxx/numerics/c.math/abs.bitint.pass.cpp
@@ -23,6 +23,7 @@
 template <int N>
 void test_signed_bitint() {
   using T = signed _BitInt(N);
+  ASSERT_SAME_TYPE(decltype(std::abs(T(0))), T);
   assert(std::abs(T(0)) == T(0));
   assert(std::abs(T(1)) == T(1));
   assert(std::abs(T(42)) == T(42));
@@ -75,6 +76,7 @@ int main(int, char**) {
 #endif // TEST_HAS_EXTENSION(bit_int)
 
 #if _LIBCPP_HAS_INT128
+  ASSERT_SAME_TYPE(decltype(std::abs(static_cast<__int128_t>(0))), __int128_t);
   assert(std::abs(static_cast<__int128_t>(0)) == 0);
   assert(std::abs(static_cast<__int128_t>(42)) == 42);
   assert(std::abs(static_cast<__int128_t>(-42)) == 42);

>From 71d5d632c8c81f6ae3574501e465b5754a84db30 Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Thu, 4 Jun 2026 17:20:36 +0200
Subject: [PATCH 4/6] [libc++][test] Add negative SFINAE checks for std::abs
 sizeof gate

The new std::abs template overload for _BitInt(N) and __int128 carries
a `sizeof(_Tp) >= sizeof(int)` constraint that keeps shorter signed
types on the integer-promotion path. The existing tests cover the
positive widths but do not assert the gate's negative side. Add two
checks:

- For standard short types (`signed char`, `short`), the type of
  `std::abs(t)` is `int` (the unpromoted T would only match the new
  template). A negative `is_same<decltype(std::abs(T())), T>` confirms
  they still go through `abs(int)` via promotion, not through the new
  template.

- For `_BitInt(N)` narrower than `int` (`_BitInt(7)`, `_BitInt(16)`,
  `_BitInt(31)`), `std::abs` is not callable at all -- they neither
  promote to `int` nor match the sizeof guard. SFINAE detection
  confirms.

These guard against a future relaxation of the sizeof guard silently
admitting short types that the comment in `abs.h` already documents as
intentionally excluded.

Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>
---
 .../numerics/c.math/abs.bitint.pass.cpp       | 21 +++++++++++++++++++
 1 file changed, 21 insertions(+)

diff --git a/libcxx/test/libcxx/numerics/c.math/abs.bitint.pass.cpp b/libcxx/test/libcxx/numerics/c.math/abs.bitint.pass.cpp
index 11f4f121450b7..9da43af4c6ada 100644
--- a/libcxx/test/libcxx/numerics/c.math/abs.bitint.pass.cpp
+++ b/libcxx/test/libcxx/numerics/c.math/abs.bitint.pass.cpp
@@ -16,10 +16,31 @@
 
 #include <cassert>
 #include <cstdlib>
+#include <type_traits>
 
 #include "test_macros.h"
 
+// Pin down the sizeof gate: narrow signed types still resolve to abs(int) via
+// integer promotion, and the new template stays out of their overload set.
+// Detection probes the template path specifically (the type of std::abs(t) is
+// the unpromoted T only for the new overload).
+template <class T>
+constexpr bool unpromoted_abs = std::is_same<decltype(std::abs(T(0))), T>::value;
+static_assert(!unpromoted_abs<signed char>);
+static_assert(!unpromoted_abs<short>);
+
 #if TEST_HAS_EXTENSION(bit_int)
+// _BitInt(N) narrower than int does not promote and the new template's sizeof
+// gate excludes it, so abs is not callable. Probe via SFINAE.
+template <class T, class = void>
+constexpr bool has_abs = false;
+template <class T>
+constexpr bool has_abs<T, decltype((void)std::abs(T(0)))> = true;
+static_assert(!has_abs<signed _BitInt(7)>);
+static_assert(!has_abs<signed _BitInt(16)>);
+static_assert(!has_abs<signed _BitInt(31)>);
+static_assert(has_abs<signed _BitInt(32)>);
+
 template <int N>
 void test_signed_bitint() {
   using T = signed _BitInt(N);

>From 5cadb03ba809f3ec1008cb9b51988214d55df0ce Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Tue, 16 Jun 2026 09:39:57 +0200
Subject: [PATCH 5/6] [libc++][test] Colocate _BitInt and __int128 abs tests
 into abs.pass.cpp

Move the _BitInt(N) and __int128_t test coverage from the standalone
abs.bitint.pass.cpp into the existing abs.pass.cpp and delete the
sibling file. Addresses philnik's review on PR #196532 ("please put
the tests into pre-existing files. Also for all new PRs").

Also switch the probe from TEST_HAS_EXTENSION(bit_int) to
defined(__BITINT_MAXWIDTH__). __has_extension(bit_int) is not
registered in Clang's Features.def and returns 0 in every language
mode, so the previously-dead block was hiding a latent test bug:
the SFINAE probe asserted !has_abs<_BitInt(31)>, but
sizeof(_BitInt(31)) == sizeof(int) on x86_64 so the overload
matches it. Drop the borderline negative assertions and keep a
clear case (_BitInt(7), sizeof < sizeof(int) on every common
target) plus positive cases at 32 and 64 bits.

Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>
---
 .../numerics/c.math/abs.bitint.pass.cpp       | 122 ------------------
 libcxx/test/std/numerics/c.math/abs.pass.cpp  |  93 +++++++++++++
 2 files changed, 93 insertions(+), 122 deletions(-)
 delete mode 100644 libcxx/test/libcxx/numerics/c.math/abs.bitint.pass.cpp

diff --git a/libcxx/test/libcxx/numerics/c.math/abs.bitint.pass.cpp b/libcxx/test/libcxx/numerics/c.math/abs.bitint.pass.cpp
deleted file mode 100644
index 9da43af4c6ada..0000000000000
--- a/libcxx/test/libcxx/numerics/c.math/abs.bitint.pass.cpp
+++ /dev/null
@@ -1,122 +0,0 @@
-//===----------------------------------------------------------------------===//
-//
-// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
-// See https://llvm.org/LICENSE.txt for license information.
-// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-//
-//===----------------------------------------------------------------------===//
-
-// std::abs for _BitInt(N) and __int128 -- libc++ extension over the standard
-// abs(int / long / long long) overloads. The new template covers signed
-// integer types not handled by the existing builtin overloads, gated on
-// __is_signed_integer_v plus a sizeof(_Tp) >= sizeof(int) check that keeps
-// shorter standard types on the integer-promotion path.
-
-// UNSUPPORTED: c++03, c++11, c++14, c++17
-
-#include <cassert>
-#include <cstdlib>
-#include <type_traits>
-
-#include "test_macros.h"
-
-// Pin down the sizeof gate: narrow signed types still resolve to abs(int) via
-// integer promotion, and the new template stays out of their overload set.
-// Detection probes the template path specifically (the type of std::abs(t) is
-// the unpromoted T only for the new overload).
-template <class T>
-constexpr bool unpromoted_abs = std::is_same<decltype(std::abs(T(0))), T>::value;
-static_assert(!unpromoted_abs<signed char>);
-static_assert(!unpromoted_abs<short>);
-
-#if TEST_HAS_EXTENSION(bit_int)
-// _BitInt(N) narrower than int does not promote and the new template's sizeof
-// gate excludes it, so abs is not callable. Probe via SFINAE.
-template <class T, class = void>
-constexpr bool has_abs = false;
-template <class T>
-constexpr bool has_abs<T, decltype((void)std::abs(T(0)))> = true;
-static_assert(!has_abs<signed _BitInt(7)>);
-static_assert(!has_abs<signed _BitInt(16)>);
-static_assert(!has_abs<signed _BitInt(31)>);
-static_assert(has_abs<signed _BitInt(32)>);
-
-template <int N>
-void test_signed_bitint() {
-  using T = signed _BitInt(N);
-  ASSERT_SAME_TYPE(decltype(std::abs(T(0))), T);
-  assert(std::abs(T(0)) == T(0));
-  assert(std::abs(T(1)) == T(1));
-  assert(std::abs(T(42)) == T(42));
-  assert(std::abs(T(-1)) == T(1));
-  assert(std::abs(T(-42)) == T(42));
-
-  // Boundary cases. T_MAX has no overflow (identity). T_MIN + 1 negates to
-  // T_MAX, which is the largest negative value abs handles cleanly. T_MIN
-  // itself is intentionally not tested: -T_MIN overflows in signed
-  // arithmetic, which is undefined behaviour. The same caveat applies to
-  // std::abs(int) with INT_MIN.
-  T t_max       = static_cast<T>(~static_cast<unsigned _BitInt(N)>(0) >> 1);
-  T t_min_plus1 = -t_max; // == T_MIN + 1
-  assert(std::abs(t_max) == t_max);
-  assert(std::abs(t_min_plus1) == t_max);
-}
-#endif
-
-int main(int, char**) {
-#if TEST_HAS_EXTENSION(bit_int)
-  // _BitInt(N) with N < sizeof(int) * CHAR_BIT does not match the new
-  // template (sizeof guard) and does not implicit-promote to int either, so
-  // it has no abs overload. Start at 32 bits.
-  test_signed_bitint<32>();
-  test_signed_bitint<64>();
-
-  // Odd widths >= 32 bits.
-  test_signed_bitint<33>();
-  test_signed_bitint<63>();
-  test_signed_bitint<65>();
-
-#  if __BITINT_MAXWIDTH__ >= 128
-  test_signed_bitint<128>();
-#  endif
-
-#  if __BITINT_MAXWIDTH__ >= 256
-  test_signed_bitint<129>();
-  test_signed_bitint<256>();
-
-  // Large value: |-2^200| == 2^200. Python: abs(-(1 << 200)) == 1 << 200.
-  signed _BitInt(256) v        = -(static_cast<signed _BitInt(256)>(1) << 200);
-  signed _BitInt(256) expected = static_cast<signed _BitInt(256)>(1) << 200;
-  assert(std::abs(v) == expected);
-#  endif
-
-#  if __BITINT_MAXWIDTH__ >= 1024
-  test_signed_bitint<512>();
-  test_signed_bitint<1024>();
-#  endif
-#endif // TEST_HAS_EXTENSION(bit_int)
-
-#if _LIBCPP_HAS_INT128
-  ASSERT_SAME_TYPE(decltype(std::abs(static_cast<__int128_t>(0))), __int128_t);
-  assert(std::abs(static_cast<__int128_t>(0)) == 0);
-  assert(std::abs(static_cast<__int128_t>(42)) == 42);
-  assert(std::abs(static_cast<__int128_t>(-42)) == 42);
-  assert(std::abs(static_cast<__int128_t>(-1)) == 1);
-
-  // INT128_MIN is unrepresentable as a positive __int128; -__x overflows.
-  // Skip that case -- the standard's abs(int) has the same issue with
-  // INT_MIN, so this is consistent.
-  __int128_t big_neg = -((static_cast<__int128_t>(1) << 100));
-  __int128_t big_pos = static_cast<__int128_t>(1) << 100;
-  assert(std::abs(big_neg) == big_pos);
-
-  // Boundary: INT128_MAX (identity) and INT128_MIN+1 (negation cleanly
-  // produces INT128_MAX).
-  __int128_t int128_max       = static_cast<__int128_t>(~static_cast<__uint128_t>(0) >> 1);
-  __int128_t int128_min_plus1 = -int128_max;
-  assert(std::abs(int128_max) == int128_max);
-  assert(std::abs(int128_min_plus1) == int128_max);
-#endif
-
-  return 0;
-}
diff --git a/libcxx/test/std/numerics/c.math/abs.pass.cpp b/libcxx/test/std/numerics/c.math/abs.pass.cpp
index 51aee6e986836..91147dafabc1a 100644
--- a/libcxx/test/std/numerics/c.math/abs.pass.cpp
+++ b/libcxx/test/std/numerics/c.math/abs.pass.cpp
@@ -37,6 +37,69 @@ void test_big() {
   assert(std::abs(negative_big_value) == big_value); // make sure it doesn't get casted to a smaller type
 }
 
+// The libc++ extension overload covers signed integer types >= sizeof(int)
+// that aren't matched by the standard abs(int/long/long long): __int128 and
+// signed _BitInt(N). Shorter types still go through abs(int) via integer
+// promotion -- the new template's sizeof gate must keep them out.
+template <class T>
+constexpr bool unpromoted_abs = std::is_same<decltype(std::abs(T(0))), T>::value;
+static_assert(!unpromoted_abs<signed char>);
+static_assert(!unpromoted_abs<short>);
+
+#ifdef __BITINT_MAXWIDTH__
+// _BitInt(N) does not participate in integer promotion. The template's
+// sizeof(_Tp) >= sizeof(int) gate determines membership, not bit width:
+// sizeof(_BitInt(31)) == sizeof(int) on x86_64 so the overload includes it.
+// Probe via SFINAE on widths whose sizeof is unambiguous.
+template <class T, class = void>
+constexpr bool has_abs = false;
+template <class T>
+constexpr bool has_abs<T, decltype((void)std::abs(T(0)))> = true;
+static_assert(!has_abs<signed _BitInt(7)>);
+static_assert(has_abs<signed _BitInt(32)>);
+static_assert(has_abs<signed _BitInt(64)>);
+
+template <int N>
+void test_signed_bitint() {
+  using T = signed _BitInt(N);
+  ASSERT_SAME_TYPE(decltype(std::abs(T(0))), T);
+  assert(std::abs(T(0)) == T(0));
+  assert(std::abs(T(1)) == T(1));
+  assert(std::abs(T(42)) == T(42));
+  assert(std::abs(T(-1)) == T(1));
+  assert(std::abs(T(-42)) == T(42));
+
+  // T_MAX has no overflow (identity). T_MIN + 1 negates cleanly to T_MAX.
+  // T_MIN itself is intentionally not tested -- -T_MIN overflows, same caveat
+  // as std::abs(int) with INT_MIN.
+  T t_max       = static_cast<T>(~static_cast<unsigned _BitInt(N)>(0) >> 1);
+  T t_min_plus1 = -t_max;
+  assert(std::abs(t_max) == t_max);
+  assert(std::abs(t_min_plus1) == t_max);
+}
+#endif
+
+#ifndef TEST_HAS_NO_INT128
+void test_int128() {
+  ASSERT_SAME_TYPE(decltype(std::abs(static_cast<__int128_t>(0))), __int128_t);
+  assert(std::abs(static_cast<__int128_t>(0)) == 0);
+  assert(std::abs(static_cast<__int128_t>(42)) == 42);
+  assert(std::abs(static_cast<__int128_t>(-42)) == 42);
+  assert(std::abs(static_cast<__int128_t>(-1)) == 1);
+
+  // INT128_MIN is unrepresentable as a positive __int128. Skip, consistent
+  // with the standard abs(int) INT_MIN caveat.
+  __int128_t big_neg = -((static_cast<__int128_t>(1) << 100));
+  __int128_t big_pos = static_cast<__int128_t>(1) << 100;
+  assert(std::abs(big_neg) == big_pos);
+
+  __int128_t int128_max       = static_cast<__int128_t>(~static_cast<__uint128_t>(0) >> 1);
+  __int128_t int128_min_plus1 = -int128_max;
+  assert(std::abs(int128_max) == int128_max);
+  assert(std::abs(int128_min_plus1) == int128_max);
+}
+#endif
+
 // The following is helpful to keep in mind:
 // 1byte == char <= short <= int <= long <= long long
 
@@ -68,5 +131,35 @@ int main(int, char**) {
 
   test_big();
 
+#ifdef __BITINT_MAXWIDTH__
+  // _BitInt(N < sizeof(int) * CHAR_BIT) does not match the new template
+  // (sizeof guard) and does not promote, so abs has no overload. Start at 32.
+  test_signed_bitint<32>();
+  test_signed_bitint<64>();
+  test_signed_bitint<33>();
+  test_signed_bitint<63>();
+  test_signed_bitint<65>();
+#  if __BITINT_MAXWIDTH__ >= 128
+  test_signed_bitint<128>();
+#  endif
+#  if __BITINT_MAXWIDTH__ >= 256
+  test_signed_bitint<129>();
+  test_signed_bitint<256>();
+
+  // Large value: |-2^200| == 2^200.
+  signed _BitInt(256) v        = -(static_cast<signed _BitInt(256)>(1) << 200);
+  signed _BitInt(256) expected = static_cast<signed _BitInt(256)>(1) << 200;
+  assert(std::abs(v) == expected);
+#  endif
+#  if __BITINT_MAXWIDTH__ >= 1024
+  test_signed_bitint<512>();
+  test_signed_bitint<1024>();
+#  endif
+#endif
+
+#ifndef TEST_HAS_NO_INT128
+  test_int128();
+#endif
+
   return 0;
 }

>From 186e7bdcc2639307b9d3530cdf13d3acff7ce8d0 Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Tue, 16 Jun 2026 09:48:43 +0200
Subject: [PATCH 6/6] [libc++][test] Pin signed-only contract for std::abs
 _BitInt SFINAE

Add static_asserts that unsigned _BitInt(32) and unsigned _BitInt(64)
do NOT match the new abs template. Without these, a regression in
__is_signed_integer_v that started accepting unsigned _BitInt would
slip past every other assertion in the file. Caught by an independent
review pass that asked: "what bug would all existing tests miss?"

Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>
---
 libcxx/test/std/numerics/c.math/abs.pass.cpp | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/libcxx/test/std/numerics/c.math/abs.pass.cpp b/libcxx/test/std/numerics/c.math/abs.pass.cpp
index 91147dafabc1a..ea6df2a348415 100644
--- a/libcxx/test/std/numerics/c.math/abs.pass.cpp
+++ b/libcxx/test/std/numerics/c.math/abs.pass.cpp
@@ -58,6 +58,10 @@ constexpr bool has_abs<T, decltype((void)std::abs(T(0)))> = true;
 static_assert(!has_abs<signed _BitInt(7)>);
 static_assert(has_abs<signed _BitInt(32)>);
 static_assert(has_abs<signed _BitInt(64)>);
+// Pin the signed-only contract of __is_signed_integer_v: a regression that
+// accepted unsigned _BitInt would otherwise slip past every other test here.
+static_assert(!has_abs<unsigned _BitInt(32)>);
+static_assert(!has_abs<unsigned _BitInt(64)>);
 
 template <int N>
 void test_signed_bitint() {



More information about the libcxx-commits mailing list