[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