[libcxx-commits] [libcxx] [libc++] std::byteswap support for _BitInt(N) (PR #196512)

via libcxx-commits libcxx-commits at lists.llvm.org
Fri May 8 08:48:48 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-libcxx

Author: Xavier Roche (xroche)

<details>
<summary>Changes</summary>

Add a byte-reversal loop fallback for `std::byteswap` when `sizeof(T) > 16`,
so the function works for `_BitInt(N)` with `N > 128` and any future wider
integer type. Without it, those calls hit `static_assert(sizeof(_Tp) == 0)`
and fail to compile.

Reject `_BitInt(N)` where `N` is not a multiple of `CHAR_BIT`. The existing
`__builtin_bswap{16,32,64,128}` paths swap the storage representation
including padding bits, and the resulting value's meaning is unspecified.
A new `static_assert` catches that case and reports it. Size-1 types are
exempt from the check, since no bytes move there.

Part of the [_BitInt(N) libc++ effort](https://discourse.llvm.org/t/bitint-n-support-in-libc-investigations-possible-improvements-looking-for-guidance/90063).

Assisted-by: Claude (Anthropic)

---
Full diff: https://github.com/llvm/llvm-project/pull/196512.diff


5 Files Affected:

- (modified) libcxx/include/__bit/byteswap.h (+39-12) 
- (modified) libcxx/test/libcxx/transitive_includes/cxx23.csv (+1) 
- (modified) libcxx/test/libcxx/transitive_includes/cxx26.csv (+1) 
- (added) libcxx/test/std/numerics/bit/byteswap.bitint.pass.cpp (+131) 
- (added) libcxx/test/std/numerics/bit/byteswap.bitint.verify.cpp (+78) 


``````````diff
diff --git a/libcxx/include/__bit/byteswap.h b/libcxx/include/__bit/byteswap.h
index 7ce7e069b4142..2754265080a34 100644
--- a/libcxx/include/__bit/byteswap.h
+++ b/libcxx/include/__bit/byteswap.h
@@ -12,7 +12,10 @@
 
 #include <__concepts/arithmetic.h>
 #include <__config>
+#include <__cstddef/size_t.h>
+#include <climits>
 #include <cstdint>
+#include <limits>
 
 #if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
 #  pragma GCC system_header
@@ -25,24 +28,48 @@ _LIBCPP_BEGIN_NAMESPACE_STD
 template <integral _Tp>
 [[nodiscard]] _LIBCPP_HIDE_FROM_ABI constexpr _Tp byteswap(_Tp __val) noexcept {
   if constexpr (sizeof(_Tp) == 1) {
+    // Identity for size-1 types: no bytes move and no padding gets shuffled
+    // into significant positions. bool, char, and _BitInt(N <= CHAR_BIT)
+    // all land here.
     return __val;
-  } else if constexpr (sizeof(_Tp) == 2) {
-    return __builtin_bswap16(__val);
-  } else if constexpr (sizeof(_Tp) == 4) {
-    return __builtin_bswap32(__val);
-  } else if constexpr (sizeof(_Tp) == 8) {
-    return __builtin_bswap64(__val);
+  } else {
+    // Reject types whose value bits do not fill the entire object
+    // representation (e.g. _BitInt(13) has 3 padding bits in 2 bytes of
+    // storage). The byte-level builtins below would swap those padding
+    // bits into significant positions, and the resulting value's meaning
+    // is unspecified. The size-1 case above is exempt because no bytes
+    // move.
+    static_assert(numeric_limits<_Tp>::digits + numeric_limits<_Tp>::is_signed == sizeof(_Tp) * CHAR_BIT,
+                  "std::byteswap requires a type whose value bits fill the entire "
+                  "object representation; types like _BitInt(N) where N is not a "
+                  "multiple of CHAR_BIT have padding bits and are rejected");
+    if constexpr (sizeof(_Tp) == 2) {
+      return __builtin_bswap16(__val);
+    } else if constexpr (sizeof(_Tp) == 4) {
+      return __builtin_bswap32(__val);
+    } else if constexpr (sizeof(_Tp) == 8) {
+      return __builtin_bswap64(__val);
 #  if _LIBCPP_HAS_INT128
-  } else if constexpr (sizeof(_Tp) == 16) {
+    } else if constexpr (sizeof(_Tp) == 16) {
 #    if __has_builtin(__builtin_bswap128)
-    return __builtin_bswap128(__val);
+      return __builtin_bswap128(__val);
 #    else
-    return (static_cast<_Tp>(byteswap(static_cast<uint64_t>(__val))) << 64) |
-           static_cast<_Tp>(byteswap(static_cast<uint64_t>(__val >> 64)));
+      return (static_cast<_Tp>(byteswap(static_cast<uint64_t>(__val))) << 64) |
+             static_cast<_Tp>(byteswap(static_cast<uint64_t>(__val >> 64)));
 #    endif // __has_builtin(__builtin_bswap128)
 #  endif   // _LIBCPP_HAS_INT128
-  } else {
-    static_assert(sizeof(_Tp) == 0, "byteswap is unimplemented for integral types of this size");
+    } else {
+      // Generic byte-reversal for wide integer types (e.g. _BitInt(N) with
+      // N > 128). Reads the value 8 bits at a time and writes the bytes
+      // back in reverse order. Left-shift on signed integral types is
+      // well-defined modulo 2^width since C++20.
+      _Tp __result = 0;
+      for (size_t __i = 0; __i < sizeof(_Tp); ++__i) {
+        __result |= static_cast<_Tp>(static_cast<unsigned char>(__val >> (__i * CHAR_BIT)))
+                 << ((sizeof(_Tp) - 1 - __i) * CHAR_BIT);
+      }
+      return __result;
+    }
   }
 }
 
diff --git a/libcxx/test/libcxx/transitive_includes/cxx23.csv b/libcxx/test/libcxx/transitive_includes/cxx23.csv
index c5cc61f06678c..073f698786117 100644
--- a/libcxx/test/libcxx/transitive_includes/cxx23.csv
+++ b/libcxx/test/libcxx/transitive_includes/cxx23.csv
@@ -42,6 +42,7 @@ barrier ctime
 barrier limits
 barrier ratio
 barrier version
+bit climits
 bit cstdint
 bit limits
 bit version
diff --git a/libcxx/test/libcxx/transitive_includes/cxx26.csv b/libcxx/test/libcxx/transitive_includes/cxx26.csv
index 253cf64703076..5b4ba7918ae96 100644
--- a/libcxx/test/libcxx/transitive_includes/cxx26.csv
+++ b/libcxx/test/libcxx/transitive_includes/cxx26.csv
@@ -40,6 +40,7 @@ barrier ctime
 barrier limits
 barrier ratio
 barrier version
+bit climits
 bit cstdint
 bit limits
 bit version
diff --git a/libcxx/test/std/numerics/bit/byteswap.bitint.pass.cpp b/libcxx/test/std/numerics/bit/byteswap.bitint.pass.cpp
new file mode 100644
index 0000000000000..ce432c1432c15
--- /dev/null
+++ b/libcxx/test/std/numerics/bit/byteswap.bitint.pass.cpp
@@ -0,0 +1,131 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20
+
+// <bit>
+
+// std::byteswap for _BitInt(N).
+//
+// Byte-aligned widths (N % CHAR_BIT == 0) work via the existing builtins
+// for sizeof <= 16 and via the new generic loop for sizeof > 16. Non-byte-
+// aligned widths are rejected by static_assert; that case is covered in
+// byteswap.bitint.verify.cpp.
+
+#include <bit>
+#include <cassert>
+#include <cstdint>
+
+#include "test_macros.h"
+
+#if TEST_HAS_EXTENSION(bit_int)
+
+template <class T>
+constexpr void test_roundtrip(T v) {
+  assert(std::byteswap(std::byteswap(v)) == v);
+  ASSERT_SAME_TYPE(decltype(std::byteswap(v)), T);
+  ASSERT_NOEXCEPT(std::byteswap(v));
+}
+
+constexpr bool test() {
+  // sizeof == 1: identity. The size-1 branch returns the input unchanged
+  // and the padding-bit static_assert is bypassed -- no bytes move and no
+  // padding gets shuffled into significant positions, so non-byte-aligned
+  // widths up to CHAR_BIT (e.g. _BitInt(7)) are also identity.
+  assert(std::byteswap(static_cast<unsigned _BitInt(8)>(0xAB)) == static_cast<unsigned _BitInt(8)>(0xAB));
+  test_roundtrip<unsigned _BitInt(8)>(0xAB);
+  test_roundtrip<signed _BitInt(8)>(0x12);
+  // _BitInt(7) signed has a padding bit but stays identity at sizeof == 1.
+  assert(std::byteswap(static_cast<signed _BitInt(7)>(42)) == static_cast<signed _BitInt(7)>(42));
+  assert(std::byteswap(static_cast<unsigned _BitInt(7)>(42)) == static_cast<unsigned _BitInt(7)>(42));
+
+  // sizeof == 2: __builtin_bswap16
+  assert(std::byteswap(static_cast<unsigned _BitInt(16)>(0xCDEF)) == static_cast<unsigned _BitInt(16)>(0xEFCD));
+  test_roundtrip<unsigned _BitInt(16)>(0xCDEF);
+  test_roundtrip<signed _BitInt(16)>(0x1234);
+
+  // sizeof == 4: __builtin_bswap32
+  assert(std::byteswap(static_cast<unsigned _BitInt(32)>(0x01234567U)) ==
+         static_cast<unsigned _BitInt(32)>(0x67452301U));
+  test_roundtrip<unsigned _BitInt(32)>(0x01234567U);
+  test_roundtrip<signed _BitInt(32)>(0x01234567);
+
+  // sizeof == 8: __builtin_bswap64
+  assert(std::byteswap(static_cast<unsigned _BitInt(64)>(0x0123456789ABCDEFULL)) ==
+         static_cast<unsigned _BitInt(64)>(0xEFCDAB8967452301ULL));
+  test_roundtrip<unsigned _BitInt(64)>(0x0123456789ABCDEFULL);
+  test_roundtrip<signed _BitInt(64)>(0x0123456789ABCDEFLL);
+
+#  if __BITINT_MAXWIDTH__ >= 128
+  // sizeof == 16: __builtin_bswap128 (or 2x bswap64 fallback). Same path
+  // as the existing __int128_t / __uint128_t coverage in byteswap.pass.cpp.
+  unsigned _BitInt(128) v128 =
+      (static_cast<unsigned _BitInt(128)>(0x0123456789ABCDEFULL) << 64) |
+      static_cast<unsigned _BitInt(128)>(0x13579BDF02468ACEULL);
+  test_roundtrip<unsigned _BitInt(128)>(v128);
+  test_roundtrip<signed _BitInt(128)>(static_cast<signed _BitInt(128)>(v128));
+#  endif
+
+#  if __BITINT_MAXWIDTH__ >= 256
+  // sizeof == 32: hits the new generic loop fallback.
+  unsigned _BitInt(256) v256 =
+      (static_cast<unsigned _BitInt(256)>(0xDEADBEEFCAFEBABEULL) << 128) |
+      (static_cast<unsigned _BitInt(256)>(0x1234567890ABCDEFULL) << 64) |
+      static_cast<unsigned _BitInt(256)>(0xFEDCBA9876543210ULL);
+  test_roundtrip<unsigned _BitInt(256)>(v256);
+  test_roundtrip<signed _BitInt(256)>(static_cast<signed _BitInt(256)>(v256));
+
+  // Spot check for the wide loop: low byte of input must end up as the
+  // high byte of the output, and high byte of input as the low byte.
+  unsigned _BitInt(256) lo_only = 0xAB;
+  auto lo_swapped               = std::byteswap(lo_only);
+  assert(static_cast<unsigned char>(lo_swapped >> ((sizeof(lo_swapped) - 1) * CHAR_BIT)) == 0xAB);
+  unsigned _BitInt(256) hi_only =
+      static_cast<unsigned _BitInt(256)>(0xCD) << ((sizeof(unsigned _BitInt(256)) - 1) * CHAR_BIT);
+  auto hi_swapped = std::byteswap(hi_only);
+  assert(static_cast<unsigned char>(hi_swapped) == 0xCD);
+
+  // Mid-value test: distinct byte at every position so an off-by-one in
+  // the loop indexing surfaces directly. Build 0x00010203...1F at bytes
+  // 0..31 then verify byteswap reverses the byte sequence.
+  unsigned _BitInt(256) ramp = 0;
+  for (int __i = 0; __i < 32; ++__i)
+    ramp |= static_cast<unsigned _BitInt(256)>(__i) << (__i * CHAR_BIT);
+  auto ramp_swapped = std::byteswap(ramp);
+  for (int __i = 0; __i < 32; ++__i)
+    assert(static_cast<unsigned char>(ramp_swapped >> ((31 - __i) * CHAR_BIT)) == __i);
+#  endif
+
+#  if __BITINT_MAXWIDTH__ >= 1024
+  // Larger width still in the generic loop.
+  unsigned _BitInt(1024) v1024 = static_cast<unsigned _BitInt(1024)>(0xAB) << ((128 - 1) * CHAR_BIT);
+  test_roundtrip<unsigned _BitInt(1024)>(v1024);
+#  endif
+
+#  if __BITINT_MAXWIDTH__ >= 4096
+  // Largest width tested. Picked to cover the upper end of what the
+  // dev-branch experiments exercised; values larger than this take a
+  // long time to constexpr-evaluate without adding much coverage.
+  unsigned _BitInt(4096) v4096 = static_cast<unsigned _BitInt(4096)>(0xAB) << ((512 - 1) * CHAR_BIT);
+  test_roundtrip<unsigned _BitInt(4096)>(v4096);
+#  endif
+
+  return true;
+}
+
+int main(int, char**) {
+  test();
+  static_assert(test());
+  return 0;
+}
+
+#else
+
+int main(int, char**) { return 0; }
+
+#endif
diff --git a/libcxx/test/std/numerics/bit/byteswap.bitint.verify.cpp b/libcxx/test/std/numerics/bit/byteswap.bitint.verify.cpp
new file mode 100644
index 0000000000000..7ff60a9c94463
--- /dev/null
+++ b/libcxx/test/std/numerics/bit/byteswap.bitint.verify.cpp
@@ -0,0 +1,78 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20
+
+// <bit>
+
+// std::byteswap rejects _BitInt(N) where N is not a multiple of CHAR_BIT.
+//
+// The byte-level builtins (and the generic loop fallback) treat the
+// storage representation as the value, so for a type with padding bits
+// they would shuffle padding into significant positions and produce a
+// value whose semantic meaning is unspecified. The static_assert added
+// in [libc++] Reject byteswap of types with padding bits pins the
+// rejection so the diagnostic does not regress silently into a wrong
+// value.
+
+#include <bit>
+
+#include "test_macros.h"
+
+#if TEST_HAS_EXTENSION(bit_int)
+
+void f_unsigned_13() {
+  unsigned _BitInt(13) v = 0;
+  // expected-error-re@*:* {{{{(static assertion|static_assert)}} failed{{.*}}"std::byteswap requires{{.*}}"}}
+  (void)std::byteswap(v);
+}
+
+void f_signed_13() {
+  signed _BitInt(13) v = 0;
+  // expected-error-re@*:* {{{{(static assertion|static_assert)}} failed{{.*}}"std::byteswap requires{{.*}}"}}
+  (void)std::byteswap(v);
+}
+
+void f_unsigned_17() {
+  unsigned _BitInt(17) v = 0;
+  // expected-error-re@*:* {{{{(static assertion|static_assert)}} failed{{.*}}"std::byteswap requires{{.*}}"}}
+  (void)std::byteswap(v);
+}
+
+void f_signed_33() {
+  signed _BitInt(33) v = 0;
+  // expected-error-re@*:* {{{{(static assertion|static_assert)}} failed{{.*}}"std::byteswap requires{{.*}}"}}
+  (void)std::byteswap(v);
+}
+
+void f_unsigned_65() {
+  unsigned _BitInt(65) v = 0;
+  // expected-error-re@*:* {{{{(static assertion|static_assert)}} failed{{.*}}"std::byteswap requires{{.*}}"}}
+  (void)std::byteswap(v);
+}
+
+#  if __BITINT_MAXWIDTH__ >= 129
+// _BitInt(129) is wider than __int128 and only available where
+// __BITINT_MAXWIDTH__ supports it (x86 / RISC-V 64). The wide-type
+// generic loop also relies on the rejection; cover it here.
+void f_unsigned_129() {
+  unsigned _BitInt(129) v = 0;
+  // expected-error-re@*:* {{{{(static assertion|static_assert)}} failed{{.*}}"std::byteswap requires{{.*}}"}}
+  (void)std::byteswap(v);
+}
+
+void f_signed_255() {
+  signed _BitInt(255) v = 0;
+  // expected-error-re@*:* {{{{(static assertion|static_assert)}} failed{{.*}}"std::byteswap requires{{.*}}"}}
+  (void)std::byteswap(v);
+}
+#  endif
+
+#else
+// expected-no-diagnostics
+#endif

``````````

</details>


https://github.com/llvm/llvm-project/pull/196512


More information about the libcxx-commits mailing list