[libcxx-commits] [libcxx] [libc++] std::byteswap support for _BitInt(N) (PR #196512)
Xavier Roche via libcxx-commits
libcxx-commits at lists.llvm.org
Wed May 20 05:51:51 PDT 2026
https://github.com/xroche updated https://github.com/llvm/llvm-project/pull/196512
>From 4f6192b4d5fa674e35f85bc913c81f5ceb9f86ab Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Fri, 8 May 2026 13:15:13 +0200
Subject: [PATCH 1/9] [libc++] Add generic byteswap for wide integral types
Replace the static_assert(sizeof(_Tp) == 0) failure for sizeof > 16 with
a byte-reversal loop. std::byteswap now works for _BitInt(N) with N > 128
and any future wider integer type. The loop reads the value 8 bits at a
time via right-shift + cast through unsigned char, and writes the bytes
back in reverse order. Left-shift and bitwise-OR on signed integral types
are well-defined modulo 2^width in C++23.
For sizeof 1, 2, 4, 8, and 16 the existing __builtin_bswap* paths are
unchanged.
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)
Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>
---
libcxx/include/__bit/byteswap.h | 13 ++++++++++++-
libcxx/test/libcxx/transitive_includes/cxx23.csv | 1 +
libcxx/test/libcxx/transitive_includes/cxx26.csv | 1 +
3 files changed, 14 insertions(+), 1 deletion(-)
diff --git a/libcxx/include/__bit/byteswap.h b/libcxx/include/__bit/byteswap.h
index 7ce7e069b4142..66a99713bd0dd 100644
--- a/libcxx/include/__bit/byteswap.h
+++ b/libcxx/include/__bit/byteswap.h
@@ -12,6 +12,8 @@
#include <__concepts/arithmetic.h>
#include <__config>
+#include <__cstddef/size_t.h>
+#include <climits>
#include <cstdint>
#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
@@ -42,7 +44,16 @@ template <integral _Tp>
# endif // __has_builtin(__builtin_bswap128)
# endif // _LIBCPP_HAS_INT128
} else {
- static_assert(sizeof(_Tp) == 0, "byteswap is unimplemented for integral types of this size");
+ // 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
>From 42721d3b8f7a0937aeb2cf87d1f58c3266df26e8 Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Fri, 8 May 2026 13:15:44 +0200
Subject: [PATCH 2/9] [libc++] Reject byteswap of types with padding bits
For _BitInt(N) where N is not a multiple of CHAR_BIT, byteswap of the
storage representation moves padding bits into significant positions and
produces a value whose meaning is unspecified. Reject those cases with a
static_assert that fires when the type's value bits do not fill the
entire object representation.
The size-1 case is exempt from the check because no bytes move and no
padding gets relocated. bool, _BitInt(7), and similar types stay
identity.
Suggested by philnik in the libc++ _BitInt review thread ("byteswap
with padding: should definitely be rejected").
Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>
---
libcxx/include/__bit/byteswap.h | 56 +++++++++++++++++++++------------
1 file changed, 36 insertions(+), 20 deletions(-)
diff --git a/libcxx/include/__bit/byteswap.h b/libcxx/include/__bit/byteswap.h
index 66a99713bd0dd..2754265080a34 100644
--- a/libcxx/include/__bit/byteswap.h
+++ b/libcxx/include/__bit/byteswap.h
@@ -15,6 +15,7 @@
#include <__cstddef/size_t.h>
#include <climits>
#include <cstdint>
+#include <limits>
#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
# pragma GCC system_header
@@ -27,33 +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 {
- // 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);
+ } 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;
}
- return __result;
}
}
>From acf2941190cabc38718b054efce12caa330b8ce5 Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Fri, 8 May 2026 13:16:06 +0200
Subject: [PATCH 3/9] [libc++][test] Add std::byteswap tests for _BitInt(N)
Two test files for the changes in the prior two commits.
byteswap.bitint.pass.cpp covers byte-aligned _BitInt(N) widths from 8 up
to __BITINT_MAXWIDTH__: builtins for sizeof <= 16, the new generic loop
for sizeof > 16. The size-1 case includes _BitInt(7) (identity, despite
the padding bit) so the asymmetry with _BitInt(13) is pinned. The wide
case has a ramp pattern with a distinct byte at every position, which
surfaces any off-by-one in the loop indexing, plus low-byte and high-byte
spot checks. Wide-width tests gate on __BITINT_MAXWIDTH__ so non-x86 /
non-RISC-V64 targets compile a smaller subset.
byteswap.bitint.verify.cpp pins the static_assert added by the previous
commit. Covers _BitInt(13), _BitInt(17), _BitInt(33), _BitInt(65) on all
targets, plus _BitInt(129) and _BitInt(255) where the platform supports
them. The expected-error-re pattern matches both Clang's "static
assertion" and GCC's "static_assert" wordings.
Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>
---
.../std/numerics/bit/byteswap.bitint.pass.cpp | 131 ++++++++++++++++++
.../numerics/bit/byteswap.bitint.verify.cpp | 78 +++++++++++
2 files changed, 209 insertions(+)
create mode 100644 libcxx/test/std/numerics/bit/byteswap.bitint.pass.cpp
create mode 100644 libcxx/test/std/numerics/bit/byteswap.bitint.verify.cpp
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
>From 87558ef98a672415caedaf3311390112395adf23 Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Sun, 10 May 2026 11:53:45 +0200
Subject: [PATCH 4/9] [libc++] Defer byteswap to __builtin_bswapg when
available
Address philnik's review on PR #196512. The fast path is now a single
call to __builtin_bswapg, which Clang added in November 2025 (PR
llvm/llvm-project#162433). The builtin handles every standard integer
type plus _BitInt(N) where N is a multiple of 16 (or N == 8 for the
size-1 identity), and Clang itself diagnoses non-multiple-of-16
_BitInt with a clear "must be a multiple of 16 bits for byte swapping"
message. That replaces the static_assert and the size-1 / wide-loop
chain we shipped earlier.
The previous implementation stays as a fallback under
__has_builtin(__builtin_bswapg) for compilers that predate Clang 22.
libc++ supports Clang 21+, and __builtin_bswapg is only in Clang 22+,
so the fallback is reachable today on Clang 21. If we drop Clang 21
support (a separate libc++ policy decision) the fallback can go.
Side effects:
- The new <__cstddef/size_t.h> / <climits> / <limits> includes only
apply on the fallback path now and are guarded behind the same
__has_builtin check.
- The bit climits transitive-include row added in the previous round
goes away on Clang 22+; reverted in cxx23.csv and cxx26.csv. The
fallback path still pulls climits in transitively but Clang 21
builds aren't measured by the in-tree CI.
- byteswap.bitint.verify.cpp's expected-error-re regex now matches
either spelling: the static_assert message on the fallback path and
the "_BitInt type ... must be a multiple of 16 bits" message on the
bswapg path. Verified locally: clang-20 fallback fires the
static_assert, in-tree clang-23 bswapg fires Clang's diagnostic.
Verified locally on Clang 23 (bswapg path) and Clang 20 (fallback
path) with the full bit suite (18/18) and the transitive-includes
test (114/114) passing on both. Cross-arch QEMU on 12 targets all
green.
Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>
---
libcxx/include/__bit/byteswap.h | 29 +++++++++++-----
.../test/libcxx/transitive_includes/cxx23.csv | 1 -
.../test/libcxx/transitive_includes/cxx26.csv | 1 -
.../numerics/bit/byteswap.bitint.verify.cpp | 34 +++++++++++--------
4 files changed, 40 insertions(+), 25 deletions(-)
diff --git a/libcxx/include/__bit/byteswap.h b/libcxx/include/__bit/byteswap.h
index 2754265080a34..9f075e9c1d410 100644
--- a/libcxx/include/__bit/byteswap.h
+++ b/libcxx/include/__bit/byteswap.h
@@ -12,10 +12,13 @@
#include <__concepts/arithmetic.h>
#include <__config>
-#include <__cstddef/size_t.h>
-#include <climits>
#include <cstdint>
-#include <limits>
+
+#if !__has_builtin(__builtin_bswapg)
+# include <__cstddef/size_t.h>
+# include <climits>
+# include <limits>
+#endif
#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
# pragma GCC system_header
@@ -27,6 +30,15 @@ _LIBCPP_BEGIN_NAMESPACE_STD
template <integral _Tp>
[[nodiscard]] _LIBCPP_HIDE_FROM_ABI constexpr _Tp byteswap(_Tp __val) noexcept {
+# if __has_builtin(__builtin_bswapg)
+ // __builtin_bswapg handles all standard integer types as well as
+ // _BitInt(N) with N a multiple of 16 (or N == 8 for identity). Padding-bit
+ // and unsupported-width cases are rejected by Clang with a clear
+ // diagnostic, so no static_assert is needed here.
+ return __builtin_bswapg(__val);
+# else
+ // Fallback for compilers that do not provide __builtin_bswapg
+ // (added in Clang 22; libc++ supports Clang 21+).
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)
@@ -49,15 +61,15 @@ template <integral _Tp>
return __builtin_bswap32(__val);
} else if constexpr (sizeof(_Tp) == 8) {
return __builtin_bswap64(__val);
-# if _LIBCPP_HAS_INT128
+# if _LIBCPP_HAS_INT128
} else if constexpr (sizeof(_Tp) == 16) {
-# if __has_builtin(__builtin_bswap128)
+# if __has_builtin(__builtin_bswap128)
return __builtin_bswap128(__val);
-# else
+# else
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
+# endif // __has_builtin(__builtin_bswap128)
+# endif // _LIBCPP_HAS_INT128
} 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
@@ -71,6 +83,7 @@ template <integral _Tp>
return __result;
}
}
+# endif // __has_builtin(__builtin_bswapg)
}
#endif // _LIBCPP_STD_VER >= 23
diff --git a/libcxx/test/libcxx/transitive_includes/cxx23.csv b/libcxx/test/libcxx/transitive_includes/cxx23.csv
index 073f698786117..c5cc61f06678c 100644
--- a/libcxx/test/libcxx/transitive_includes/cxx23.csv
+++ b/libcxx/test/libcxx/transitive_includes/cxx23.csv
@@ -42,7 +42,6 @@ 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 5b4ba7918ae96..253cf64703076 100644
--- a/libcxx/test/libcxx/transitive_includes/cxx26.csv
+++ b/libcxx/test/libcxx/transitive_includes/cxx26.csv
@@ -40,7 +40,6 @@ 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.verify.cpp b/libcxx/test/std/numerics/bit/byteswap.bitint.verify.cpp
index 7ff60a9c94463..760d3fa4a995a 100644
--- a/libcxx/test/std/numerics/bit/byteswap.bitint.verify.cpp
+++ b/libcxx/test/std/numerics/bit/byteswap.bitint.verify.cpp
@@ -10,15 +10,19 @@
// <bit>
-// std::byteswap rejects _BitInt(N) where N is not a multiple of CHAR_BIT.
+// std::byteswap rejects _BitInt(N) where the bit width is not a multiple
+// of 16. The diagnostic comes from one of two paths depending on the
+// compiler version:
//
-// 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.
+// - On Clang 22+ where __builtin_bswapg is available, byteswap defers to
+// the builtin and the rejection diagnostic is Clang's "_BitInt type
+// ... must be a multiple of 16 bits for byte swapping".
+// - On Clang 21 (the libc++ minimum) the fallback path runs, which uses
+// a static_assert to reject types whose value bits do not fill the
+// entire object representation.
+//
+// The regex below matches either spelling so the test is stable across
+// both paths.
#include <bit>
@@ -28,31 +32,31 @@
void f_unsigned_13() {
unsigned _BitInt(13) v = 0;
- // expected-error-re@*:* {{{{(static assertion|static_assert)}} failed{{.*}}"std::byteswap requires{{.*}}"}}
+ // expected-error-re@*:* {{{{(((static assertion|static_assert) failed.*"std::byteswap requires.*")|(_BitInt type.*must be a multiple of 16 bits))}}}}
(void)std::byteswap(v);
}
void f_signed_13() {
signed _BitInt(13) v = 0;
- // expected-error-re@*:* {{{{(static assertion|static_assert)}} failed{{.*}}"std::byteswap requires{{.*}}"}}
+ // expected-error-re@*:* {{{{(((static assertion|static_assert) failed.*"std::byteswap requires.*")|(_BitInt type.*must be a multiple of 16 bits))}}}}
(void)std::byteswap(v);
}
void f_unsigned_17() {
unsigned _BitInt(17) v = 0;
- // expected-error-re@*:* {{{{(static assertion|static_assert)}} failed{{.*}}"std::byteswap requires{{.*}}"}}
+ // expected-error-re@*:* {{{{(((static assertion|static_assert) failed.*"std::byteswap requires.*")|(_BitInt type.*must be a multiple of 16 bits))}}}}
(void)std::byteswap(v);
}
void f_signed_33() {
signed _BitInt(33) v = 0;
- // expected-error-re@*:* {{{{(static assertion|static_assert)}} failed{{.*}}"std::byteswap requires{{.*}}"}}
+ // expected-error-re@*:* {{{{(((static assertion|static_assert) failed.*"std::byteswap requires.*")|(_BitInt type.*must be a multiple of 16 bits))}}}}
(void)std::byteswap(v);
}
void f_unsigned_65() {
unsigned _BitInt(65) v = 0;
- // expected-error-re@*:* {{{{(static assertion|static_assert)}} failed{{.*}}"std::byteswap requires{{.*}}"}}
+ // expected-error-re@*:* {{{{(((static assertion|static_assert) failed.*"std::byteswap requires.*")|(_BitInt type.*must be a multiple of 16 bits))}}}}
(void)std::byteswap(v);
}
@@ -62,13 +66,13 @@ void f_unsigned_65() {
// 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{{.*}}"}}
+ // expected-error-re@*:* {{{{(((static assertion|static_assert) failed.*"std::byteswap requires.*")|(_BitInt type.*must be a multiple of 16 bits))}}}}
(void)std::byteswap(v);
}
void f_signed_255() {
signed _BitInt(255) v = 0;
- // expected-error-re@*:* {{{{(static assertion|static_assert)}} failed{{.*}}"std::byteswap requires{{.*}}"}}
+ // expected-error-re@*:* {{{{(((static assertion|static_assert) failed.*"std::byteswap requires.*")|(_BitInt type.*must be a multiple of 16 bits))}}}}
(void)std::byteswap(v);
}
# endif
>From 02b6ab398e8810ed94dcc474c46a66da67a49f23 Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Sun, 10 May 2026 12:14:33 +0200
Subject: [PATCH 5/9] [libc++] Pull byteswap fallback's includes outside the
__has_builtin guard
The __has_builtin(__builtin_bswapg) guard in __bit/byteswap.h had the
fallback path's three extra includes (<__cstddef/size_t.h>, <climits>,
<limits>) inside the guard. That made the transitive include set
differ between Clang 22+ (no climits in transitive) and Clang 21 / GCC
(climits in transitive), so the single transitive_includes CSV could
only match one. AIX-64-bit and android-x86-NDK CI use the fallback
path and surfaced the mismatch.
Move the three includes outside the guard so both paths share the
same transitive set, and add the bit climits row back to cxx23.csv
and cxx26.csv. Cost on the bswapg path is negligible (those headers
are already in <bit>'s transitive set via other paths).
Verified locally: in-tree clang (Clang 23, bswapg path) and g++-13
(targeted fallback compile) both accept the result. transitive_includes
test 114/114 passes, bit suite 18/18 passes.
Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>
---
libcxx/include/__bit/byteswap.h | 9 +++------
libcxx/test/libcxx/transitive_includes/cxx23.csv | 1 +
libcxx/test/libcxx/transitive_includes/cxx26.csv | 1 +
3 files changed, 5 insertions(+), 6 deletions(-)
diff --git a/libcxx/include/__bit/byteswap.h b/libcxx/include/__bit/byteswap.h
index 9f075e9c1d410..8d830a5c21637 100644
--- a/libcxx/include/__bit/byteswap.h
+++ b/libcxx/include/__bit/byteswap.h
@@ -12,13 +12,10 @@
#include <__concepts/arithmetic.h>
#include <__config>
+#include <__cstddef/size_t.h>
+#include <climits>
#include <cstdint>
-
-#if !__has_builtin(__builtin_bswapg)
-# include <__cstddef/size_t.h>
-# include <climits>
-# include <limits>
-#endif
+#include <limits>
#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
# pragma GCC system_header
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
>From 788cc73a21829c61cf1c26452b7c57c3249d2553 Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Sun, 10 May 2026 15:00:32 +0200
Subject: [PATCH 6/9] [libc++] Special-case sizeof == 1 above the
__builtin_bswapg path
Some Clang 22 builds have a constexpr-eval bug for __builtin_bswapg on
1-bit operands (i.e. bool), fixed in commit f5410565137c after 22.1.0
was cut. CI surfaced this as the existing byteswap.pass.cpp:100
static_assert(test()) failing on the stage2 (clang-22) configuration:
test() calls byteswap on bool, and bool flows through bswapg without
the size-1 short circuit.
Move the size-1 identity case before the bswapg dispatch so size-1
types (bool, char, _BitInt(N <= CHAR_BIT)) never reach the bswapg
constexpr path. Defensive against the bug regardless of Clang patch
level. The fast path's general behaviour is unchanged for sizeof > 1.
Verified locally with the in-tree clang (Clang 23, bswapg path) and
clang-20 (fallback path); bit suite 18/18, the existing
byteswap.pass.cpp still passes.
Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>
---
libcxx/include/__bit/byteswap.h | 33 ++++++++++++++++++---------------
1 file changed, 18 insertions(+), 15 deletions(-)
diff --git a/libcxx/include/__bit/byteswap.h b/libcxx/include/__bit/byteswap.h
index 8d830a5c21637..cd6c28329d53a 100644
--- a/libcxx/include/__bit/byteswap.h
+++ b/libcxx/include/__bit/byteswap.h
@@ -27,27 +27,30 @@ _LIBCPP_BEGIN_NAMESPACE_STD
template <integral _Tp>
[[nodiscard]] _LIBCPP_HIDE_FROM_ABI constexpr _Tp byteswap(_Tp __val) noexcept {
-# if __has_builtin(__builtin_bswapg)
- // __builtin_bswapg handles all standard integer types as well as
- // _BitInt(N) with N a multiple of 16 (or N == 8 for identity). Padding-bit
- // and unsupported-width cases are rejected by Clang with a clear
- // diagnostic, so no static_assert is needed here.
- return __builtin_bswapg(__val);
-# else
- // Fallback for compilers that do not provide __builtin_bswapg
- // (added in Clang 22; libc++ supports Clang 21+).
+ // 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. Handled before the __builtin_bswapg path because some
+ // Clang 22 builds have a constexpr-eval bug on the 1-bit (bool) case
+ // (fixed in commit f5410565137c, post-22.1.0).
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;
+# if __has_builtin(__builtin_bswapg)
} else {
+ // __builtin_bswapg handles all standard integer types as well as
+ // _BitInt(N) with N a multiple of 16. Padding-bit and unsupported-
+ // width cases are rejected by Clang with a clear diagnostic, so no
+ // static_assert is needed here.
+ return __builtin_bswapg(__val);
+# else
+ } else {
+ // Fallback for compilers that do not provide __builtin_bswapg
+ // (added in Clang 22; libc++ supports Clang 21+).
+ //
// 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.
+ // is unspecified.
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 "
@@ -79,8 +82,8 @@ template <integral _Tp>
}
return __result;
}
- }
# endif // __has_builtin(__builtin_bswapg)
+ }
}
#endif // _LIBCPP_STD_VER >= 23
>From 003905ee4bc5f9191e6f19906f100555adacf283 Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Sun, 10 May 2026 15:27:35 +0200
Subject: [PATCH 7/9] [libc++] Skip _BitInt byteswap tests on compilers without
__builtin_bswapg
Per philnik's review feedback on PR #196512: "We should just disable
the tests for compilers that are too old. There is very little value
in an implementation that we'll drop in a few months for a new,
relatively experimental feature."
This drops the hand-rolled padding-bit static_assert and the wide
generic-loop fallback from the previous commits. The bswapg path
on Clang 22+ delivers the same behavior (rejection diagnostic on
non-multiple-of-16 widths, support for arbitrary multiples of 16),
and `// UNSUPPORTED: clang-21` on the new tests keeps the older
compiler off the path entirely.
Side effect: byteswap.h no longer pulls in `<climits>`, `<limits>`,
or `<__cstddef/size_t.h>`, which the transitive_includes CSVs
record.
Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>
---
libcxx/include/__bit/byteswap.h | 68 ++++++-------------
.../test/libcxx/transitive_includes/cxx23.csv | 1 -
.../test/libcxx/transitive_includes/cxx26.csv | 1 -
.../std/numerics/bit/byteswap.bitint.pass.cpp | 15 ++--
.../numerics/bit/byteswap.bitint.verify.cpp | 32 ++++-----
5 files changed, 42 insertions(+), 75 deletions(-)
diff --git a/libcxx/include/__bit/byteswap.h b/libcxx/include/__bit/byteswap.h
index cd6c28329d53a..bd269a4c1f544 100644
--- a/libcxx/include/__bit/byteswap.h
+++ b/libcxx/include/__bit/byteswap.h
@@ -12,10 +12,7 @@
#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
@@ -27,63 +24,38 @@ _LIBCPP_BEGIN_NAMESPACE_STD
template <integral _Tp>
[[nodiscard]] _LIBCPP_HIDE_FROM_ABI constexpr _Tp byteswap(_Tp __val) noexcept {
- // 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. Handled before the __builtin_bswapg path because some
- // Clang 22 builds have a constexpr-eval bug on the 1-bit (bool) case
- // (fixed in commit f5410565137c, post-22.1.0).
if constexpr (sizeof(_Tp) == 1) {
+ // Identity for size-1 types. Handled before the __builtin_bswapg path
+ // because some Clang 22 builds have a constexpr-eval bug on the 1-bit
+ // (bool) case (fixed in commit f5410565137c, post-22.1.0).
return __val;
# if __has_builtin(__builtin_bswapg)
} else {
- // __builtin_bswapg handles all standard integer types as well as
- // _BitInt(N) with N a multiple of 16. Padding-bit and unsupported-
- // width cases are rejected by Clang with a clear diagnostic, so no
- // static_assert is needed here.
+ // Clang 22 and later: defer to the generic bit-width builtin. Handles
+ // all standard integer types and _BitInt(N) where N is a multiple of
+ // 16 bits; other widths are rejected by Clang with a clear diagnostic.
return __builtin_bswapg(__val);
+ }
# else
- } else {
- // Fallback for compilers that do not provide __builtin_bswapg
- // (added in Clang 22; libc++ supports Clang 21+).
- //
- // 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.
- 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);
+ } 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);
# 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 {
- // 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;
- }
-# endif // __has_builtin(__builtin_bswapg)
+ } else {
+ static_assert(sizeof(_Tp) == 0, "byteswap is unimplemented for integral types of this size");
}
+# endif // __has_builtin(__builtin_bswapg)
}
#endif // _LIBCPP_STD_VER >= 23
diff --git a/libcxx/test/libcxx/transitive_includes/cxx23.csv b/libcxx/test/libcxx/transitive_includes/cxx23.csv
index 073f698786117..c5cc61f06678c 100644
--- a/libcxx/test/libcxx/transitive_includes/cxx23.csv
+++ b/libcxx/test/libcxx/transitive_includes/cxx23.csv
@@ -42,7 +42,6 @@ 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 5b4ba7918ae96..253cf64703076 100644
--- a/libcxx/test/libcxx/transitive_includes/cxx26.csv
+++ b/libcxx/test/libcxx/transitive_includes/cxx26.csv
@@ -40,7 +40,6 @@ 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
index ce432c1432c15..5f39a1cde9b19 100644
--- a/libcxx/test/std/numerics/bit/byteswap.bitint.pass.cpp
+++ b/libcxx/test/std/numerics/bit/byteswap.bitint.pass.cpp
@@ -8,14 +8,17 @@
// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20
+// __builtin_bswapg was added in Clang 22. The libc++ implementation of
+// std::byteswap for _BitInt(N) defers to that builtin, so this test is
+// only meaningful on compilers that have it. Skip on Clang 21.
+// UNSUPPORTED: clang-21
+
// <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.
+// std::byteswap for _BitInt(N) routes through __builtin_bswapg, which
+// supports any width that is a multiple of 16 bits (or sizeof == 1, the
+// identity case). Non-multiple-of-16 widths are rejected by Clang with a
+// clear diagnostic; see byteswap.bitint.verify.cpp.
#include <bit>
#include <cassert>
diff --git a/libcxx/test/std/numerics/bit/byteswap.bitint.verify.cpp b/libcxx/test/std/numerics/bit/byteswap.bitint.verify.cpp
index 760d3fa4a995a..d68a40d8adbc0 100644
--- a/libcxx/test/std/numerics/bit/byteswap.bitint.verify.cpp
+++ b/libcxx/test/std/numerics/bit/byteswap.bitint.verify.cpp
@@ -8,21 +8,15 @@
// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20
+// __builtin_bswapg was added in Clang 22. std::byteswap for _BitInt(N)
+// defers to that builtin, so the rejection diagnostic for non-multiple-
+// of-16-bit widths only fires on Clang 22+. Skip on Clang 21.
+// UNSUPPORTED: clang-21
+
// <bit>
// std::byteswap rejects _BitInt(N) where the bit width is not a multiple
-// of 16. The diagnostic comes from one of two paths depending on the
-// compiler version:
-//
-// - On Clang 22+ where __builtin_bswapg is available, byteswap defers to
-// the builtin and the rejection diagnostic is Clang's "_BitInt type
-// ... must be a multiple of 16 bits for byte swapping".
-// - On Clang 21 (the libc++ minimum) the fallback path runs, which uses
-// a static_assert to reject types whose value bits do not fill the
-// entire object representation.
-//
-// The regex below matches either spelling so the test is stable across
-// both paths.
+// of 16. The diagnostic comes from Clang's __builtin_bswapg sema check.
#include <bit>
@@ -32,31 +26,31 @@
void f_unsigned_13() {
unsigned _BitInt(13) v = 0;
- // expected-error-re@*:* {{{{(((static assertion|static_assert) failed.*"std::byteswap requires.*")|(_BitInt type.*must be a multiple of 16 bits))}}}}
+ // expected-error-re@*:* {{{{_BitInt type.*must be a multiple of 16 bits}}}}
(void)std::byteswap(v);
}
void f_signed_13() {
signed _BitInt(13) v = 0;
- // expected-error-re@*:* {{{{(((static assertion|static_assert) failed.*"std::byteswap requires.*")|(_BitInt type.*must be a multiple of 16 bits))}}}}
+ // expected-error-re@*:* {{{{_BitInt type.*must be a multiple of 16 bits}}}}
(void)std::byteswap(v);
}
void f_unsigned_17() {
unsigned _BitInt(17) v = 0;
- // expected-error-re@*:* {{{{(((static assertion|static_assert) failed.*"std::byteswap requires.*")|(_BitInt type.*must be a multiple of 16 bits))}}}}
+ // expected-error-re@*:* {{{{_BitInt type.*must be a multiple of 16 bits}}}}
(void)std::byteswap(v);
}
void f_signed_33() {
signed _BitInt(33) v = 0;
- // expected-error-re@*:* {{{{(((static assertion|static_assert) failed.*"std::byteswap requires.*")|(_BitInt type.*must be a multiple of 16 bits))}}}}
+ // expected-error-re@*:* {{{{_BitInt type.*must be a multiple of 16 bits}}}}
(void)std::byteswap(v);
}
void f_unsigned_65() {
unsigned _BitInt(65) v = 0;
- // expected-error-re@*:* {{{{(((static assertion|static_assert) failed.*"std::byteswap requires.*")|(_BitInt type.*must be a multiple of 16 bits))}}}}
+ // expected-error-re@*:* {{{{_BitInt type.*must be a multiple of 16 bits}}}}
(void)std::byteswap(v);
}
@@ -66,13 +60,13 @@ void f_unsigned_65() {
// 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.*")|(_BitInt type.*must be a multiple of 16 bits))}}}}
+ // expected-error-re@*:* {{{{_BitInt type.*must be a multiple of 16 bits}}}}
(void)std::byteswap(v);
}
void f_signed_255() {
signed _BitInt(255) v = 0;
- // expected-error-re@*:* {{{{(((static assertion|static_assert) failed.*"std::byteswap requires.*")|(_BitInt type.*must be a multiple of 16 bits))}}}}
+ // expected-error-re@*:* {{{{_BitInt type.*must be a multiple of 16 bits}}}}
(void)std::byteswap(v);
}
# endif
>From 97891531bb27913fc113c59db0a82297aaf33099 Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Sun, 10 May 2026 16:42:24 +0200
Subject: [PATCH 8/9] [libc++] Make std::byteswap SFINAE-friendly and merge
_BitInt tests
Address review feedback on PR #196512:
1. SFINAE-friendly: add a `requires` clause that admits sizeof == 1
types and integral types whose width is a multiple of 16.
`requires { std::byteswap(_BitInt(13)()); }` now resolves to false
instead of triggering a hard error from __builtin_bswapg's sema
check.
2. Drop the size-1 special-case workaround comment that framed the
identity branch as a workaround for the Clang 22.1.x bswapg-bool
constexpr-eval bug. The size-1 dispatch is structural: bswapg
rejects _BitInt(7), _BitInt(13), etc. for "not a multiple of 16
bits" regardless of Clang version, so the SFINAE constraint admits
sizeof == 1 types and the function body routes them to the identity
branch. The Clang 22.1.x bool bug is masked by this dispatch as a
side effect, but the dispatch itself isn't a workaround.
3. Merge _BitInt(N) test cases from byteswap.bitint.pass.cpp into
byteswap.pass.cpp, replace the .verify.cpp's expected-error checks
with `static_assert(!has_byteswap<_BitInt(13)>)` and friends, and
delete the two now-redundant .bitint test files.
Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>
---
libcxx/include/__bit/byteswap.h | 8 +-
.../std/numerics/bit/byteswap.bitint.pass.cpp | 134 ------------------
.../numerics/bit/byteswap.bitint.verify.cpp | 76 ----------
.../test/std/numerics/bit/byteswap.pass.cpp | 79 +++++++++++
4 files changed, 81 insertions(+), 216 deletions(-)
delete mode 100644 libcxx/test/std/numerics/bit/byteswap.bitint.pass.cpp
delete mode 100644 libcxx/test/std/numerics/bit/byteswap.bitint.verify.cpp
diff --git a/libcxx/include/__bit/byteswap.h b/libcxx/include/__bit/byteswap.h
index bd269a4c1f544..3651973f53645 100644
--- a/libcxx/include/__bit/byteswap.h
+++ b/libcxx/include/__bit/byteswap.h
@@ -13,6 +13,7 @@
#include <__concepts/arithmetic.h>
#include <__config>
#include <cstdint>
+#include <limits>
#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
# pragma GCC system_header
@@ -23,17 +24,12 @@ _LIBCPP_BEGIN_NAMESPACE_STD
#if _LIBCPP_STD_VER >= 23
template <integral _Tp>
+ requires(sizeof(_Tp) == 1 || (numeric_limits<_Tp>::digits + numeric_limits<_Tp>::is_signed) % 16 == 0)
[[nodiscard]] _LIBCPP_HIDE_FROM_ABI constexpr _Tp byteswap(_Tp __val) noexcept {
if constexpr (sizeof(_Tp) == 1) {
- // Identity for size-1 types. Handled before the __builtin_bswapg path
- // because some Clang 22 builds have a constexpr-eval bug on the 1-bit
- // (bool) case (fixed in commit f5410565137c, post-22.1.0).
return __val;
# if __has_builtin(__builtin_bswapg)
} else {
- // Clang 22 and later: defer to the generic bit-width builtin. Handles
- // all standard integer types and _BitInt(N) where N is a multiple of
- // 16 bits; other widths are rejected by Clang with a clear diagnostic.
return __builtin_bswapg(__val);
}
# else
diff --git a/libcxx/test/std/numerics/bit/byteswap.bitint.pass.cpp b/libcxx/test/std/numerics/bit/byteswap.bitint.pass.cpp
deleted file mode 100644
index 5f39a1cde9b19..0000000000000
--- a/libcxx/test/std/numerics/bit/byteswap.bitint.pass.cpp
+++ /dev/null
@@ -1,134 +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
-//
-//===----------------------------------------------------------------------===//
-
-// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20
-
-// __builtin_bswapg was added in Clang 22. The libc++ implementation of
-// std::byteswap for _BitInt(N) defers to that builtin, so this test is
-// only meaningful on compilers that have it. Skip on Clang 21.
-// UNSUPPORTED: clang-21
-
-// <bit>
-
-// std::byteswap for _BitInt(N) routes through __builtin_bswapg, which
-// supports any width that is a multiple of 16 bits (or sizeof == 1, the
-// identity case). Non-multiple-of-16 widths are rejected by Clang with a
-// clear diagnostic; see 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
deleted file mode 100644
index d68a40d8adbc0..0000000000000
--- a/libcxx/test/std/numerics/bit/byteswap.bitint.verify.cpp
+++ /dev/null
@@ -1,76 +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
-//
-//===----------------------------------------------------------------------===//
-
-// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20
-
-// __builtin_bswapg was added in Clang 22. std::byteswap for _BitInt(N)
-// defers to that builtin, so the rejection diagnostic for non-multiple-
-// of-16-bit widths only fires on Clang 22+. Skip on Clang 21.
-// UNSUPPORTED: clang-21
-
-// <bit>
-
-// std::byteswap rejects _BitInt(N) where the bit width is not a multiple
-// of 16. The diagnostic comes from Clang's __builtin_bswapg sema check.
-
-#include <bit>
-
-#include "test_macros.h"
-
-#if TEST_HAS_EXTENSION(bit_int)
-
-void f_unsigned_13() {
- unsigned _BitInt(13) v = 0;
- // expected-error-re@*:* {{{{_BitInt type.*must be a multiple of 16 bits}}}}
- (void)std::byteswap(v);
-}
-
-void f_signed_13() {
- signed _BitInt(13) v = 0;
- // expected-error-re@*:* {{{{_BitInt type.*must be a multiple of 16 bits}}}}
- (void)std::byteswap(v);
-}
-
-void f_unsigned_17() {
- unsigned _BitInt(17) v = 0;
- // expected-error-re@*:* {{{{_BitInt type.*must be a multiple of 16 bits}}}}
- (void)std::byteswap(v);
-}
-
-void f_signed_33() {
- signed _BitInt(33) v = 0;
- // expected-error-re@*:* {{{{_BitInt type.*must be a multiple of 16 bits}}}}
- (void)std::byteswap(v);
-}
-
-void f_unsigned_65() {
- unsigned _BitInt(65) v = 0;
- // expected-error-re@*:* {{{{_BitInt type.*must be a multiple of 16 bits}}}}
- (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@*:* {{{{_BitInt type.*must be a multiple of 16 bits}}}}
- (void)std::byteswap(v);
-}
-
-void f_signed_255() {
- signed _BitInt(255) v = 0;
- // expected-error-re@*:* {{{{_BitInt type.*must be a multiple of 16 bits}}}}
- (void)std::byteswap(v);
-}
-# endif
-
-#else
-// expected-no-diagnostics
-#endif
diff --git a/libcxx/test/std/numerics/bit/byteswap.pass.cpp b/libcxx/test/std/numerics/bit/byteswap.pass.cpp
index 9d4e328ed9d0f..847a13387bdb6 100644
--- a/libcxx/test/std/numerics/bit/byteswap.pass.cpp
+++ b/libcxx/test/std/numerics/bit/byteswap.pass.cpp
@@ -26,6 +26,23 @@ static_assert(!has_byteswap<float>);
static_assert(!has_byteswap<char[2]>);
static_assert(!has_byteswap<std::byte>);
+#if TEST_HAS_EXTENSION(bit_int)
+// _BitInt(N) is a candidate when N is a multiple of 16 (or sizeof == 1, the
+// identity case). Other widths are SFINAE-rejected via the requires clause.
+static_assert(has_byteswap<unsigned _BitInt(8)>);
+static_assert(has_byteswap<signed _BitInt(8)>);
+static_assert(has_byteswap<unsigned _BitInt(7)>);
+static_assert(has_byteswap<signed _BitInt(7)>);
+static_assert(has_byteswap<unsigned _BitInt(16)>);
+static_assert(has_byteswap<signed _BitInt(16)>);
+static_assert(has_byteswap<unsigned _BitInt(64)>);
+static_assert(!has_byteswap<unsigned _BitInt(13)>);
+static_assert(!has_byteswap<signed _BitInt(13)>);
+static_assert(!has_byteswap<unsigned _BitInt(17)>);
+static_assert(!has_byteswap<signed _BitInt(33)>);
+static_assert(!has_byteswap<unsigned _BitInt(65)>);
+#endif
+
template <class T>
constexpr void test_num(T in, T expected) {
assert(std::byteswap(in) == expected);
@@ -92,6 +109,68 @@ constexpr bool test() {
test_implementation_defined_size<unsigned long>();
test_implementation_defined_size<long long>();
test_implementation_defined_size<unsigned long long>();
+
+#if TEST_HAS_EXTENSION(bit_int)
+ // sizeof == 1: identity. Returns the input unchanged regardless of the
+ // value bits, 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_num<unsigned _BitInt(8)>(0xAB, 0xAB);
+ test_num<signed _BitInt(8)>(0x12, 0x12);
+ 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 fallback or __builtin_bswapg
+ test_num<unsigned _BitInt(16)>(0xCDEF, 0xEFCD);
+ test_num<signed _BitInt(16)>(0x1234, 0x3412);
+
+ // sizeof == 4: __builtin_bswap32 fallback or __builtin_bswapg
+ test_num<unsigned _BitInt(32)>(0x01234567U, 0x67452301U);
+ test_num<signed _BitInt(32)>(0x01234567, 0x67452301);
+
+ // sizeof == 8: __builtin_bswap64 fallback or __builtin_bswapg
+ test_num<unsigned _BitInt(64)>(0x0123456789ABCDEFULL, 0xEFCDAB8967452301ULL);
+ test_num<signed _BitInt(64)>(0x0123456789ABCDEFLL, static_cast<signed _BitInt(64)>(0xEFCDAB8967452301ULL));
+
+# if __BITINT_MAXWIDTH__ >= 128
+ // sizeof == 16: __builtin_bswap128 fallback or __builtin_bswapg.
+ unsigned _BitInt(128) v128 =
+ (static_cast<unsigned _BitInt(128)>(0x0123456789ABCDEFULL) << 64) |
+ static_cast<unsigned _BitInt(128)>(0x13579BDF02468ACEULL);
+ unsigned _BitInt(128) v128_swapped =
+ (static_cast<unsigned _BitInt(128)>(0xCE8A4602DF9B5713ULL) << 64) |
+ static_cast<unsigned _BitInt(128)>(0xEFCDAB8967452301ULL);
+ test_num<unsigned _BitInt(128)>(v128, v128_swapped);
+ test_num<signed _BitInt(128)>(static_cast<signed _BitInt(128)>(v128), static_cast<signed _BitInt(128)>(v128_swapped));
+# endif
+
+# if __has_builtin(__builtin_bswapg) && __BITINT_MAXWIDTH__ >= 256
+ // sizeof > 16: only the __builtin_bswapg path supports widths beyond what
+ // __builtin_bswap16/32/64/128 cover.
+ unsigned _BitInt(256) v256 =
+ (static_cast<unsigned _BitInt(256)>(0xDEADBEEFCAFEBABEULL) << 128) |
+ (static_cast<unsigned _BitInt(256)>(0x1234567890ABCDEFULL) << 64) |
+ static_cast<unsigned _BitInt(256)>(0xFEDCBA9876543210ULL);
+ assert(std::byteswap(std::byteswap(v256)) == v256);
+ ASSERT_SAME_TYPE(decltype(std::byteswap(v256)), unsigned _BitInt(256));
+ ASSERT_NOEXCEPT(std::byteswap(v256));
+
+ // Spot check: low byte of input must end up as the high byte of the output.
+ 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);
+
+ // Mid-value test: a distinct byte at every position so an off-by-one in the
+ // generated code surfaces directly.
+ 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
+#endif
+
return true;
}
>From 29b4793063d21891faca9ad9793b6df61f58701b Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Wed, 20 May 2026 14:51:30 +0200
Subject: [PATCH 9/9] [libc++] Enforce std::byteswap padding-bit Mandate via
static_assert
Address review feedback from philnik777 and frederick-vs-ja on PR #196512:
1. Drop the `requires` clause and constrain only on `integral`, per
`[bit.byteswap]/Constraints`. The previous `requires` form conflated
the Constraints clause (SFINAE-friendly) with the Mandates clause
(hard error per `[structure.specifications]/3.2`).
2. Enforce the Mandate "T does not have padding bits" via a
`static_assert` inside the function body. The predicate is
`digits + is_signed == sizeof(T) * CHAR_BIT`, which catches the
`_BitInt(48)`, `_BitInt(80)`, `_BitInt(192)`, etc. cases where the
value bit width is a multiple of 16 but the storage size rounds up
to the next power of two with padding at the top.
3. `bool` is carved out by name (`is_same_v<remove_cv_t<T>, bool>`) to
preserve the grandfathered behavior. Whether bool's padding-bit
status should make it ill-formed is a wording question for LWG.
Tests are split into two files:
- `byteswap.pass.cpp` keeps the positive `_BitInt` cases where
`digits + is_signed == sizeof * CHAR_BIT` holds (`_BitInt(8/16/32/
64/128/256/...)`).
- `byteswap.verify.cpp` (re-added next to `byteswap.pass.cpp`) covers
the rejected shapes: sub-byte (`_BitInt(1/7)`), non-byte-aligned
(`_BitInt(13/17/33/65)`), and byte-aligned-with-storage-padding
(`_BitInt(24/40/48/56/80/96/112/192)`).
Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>
---
libcxx/include/__bit/byteswap.h | 11 +-
.../test/libcxx/transitive_includes/cxx23.csv | 1 +
.../test/libcxx/transitive_includes/cxx26.csv | 1 +
.../test/std/numerics/bit/byteswap.pass.cpp | 31 ++--
.../test/std/numerics/bit/byteswap.verify.cpp | 141 ++++++++++++++++++
5 files changed, 162 insertions(+), 23 deletions(-)
create mode 100644 libcxx/test/std/numerics/bit/byteswap.verify.cpp
diff --git a/libcxx/include/__bit/byteswap.h b/libcxx/include/__bit/byteswap.h
index 3651973f53645..5b69adb731160 100644
--- a/libcxx/include/__bit/byteswap.h
+++ b/libcxx/include/__bit/byteswap.h
@@ -12,6 +12,9 @@
#include <__concepts/arithmetic.h>
#include <__config>
+#include <__type_traits/is_same.h>
+#include <__type_traits/remove_cv.h>
+#include <climits>
#include <cstdint>
#include <limits>
@@ -24,8 +27,14 @@ _LIBCPP_BEGIN_NAMESPACE_STD
#if _LIBCPP_STD_VER >= 23
template <integral _Tp>
- requires(sizeof(_Tp) == 1 || (numeric_limits<_Tp>::digits + numeric_limits<_Tp>::is_signed) % 16 == 0)
[[nodiscard]] _LIBCPP_HIDE_FROM_ABI constexpr _Tp byteswap(_Tp __val) noexcept {
+ // [bit.byteswap]/Mandates: T does not have padding bits.
+ // bool is grandfathered: every shipping implementation admits it and the
+ // size-1 identity path can't shuffle padding bits into value positions.
+ static_assert(is_same_v<remove_cv_t<_Tp>, bool> ||
+ numeric_limits<_Tp>::digits + numeric_limits<_Tp>::is_signed == sizeof(_Tp) * CHAR_BIT,
+ "std::byteswap requires T to have no padding bits");
+
if constexpr (sizeof(_Tp) == 1) {
return __val;
# if __has_builtin(__builtin_bswapg)
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.pass.cpp b/libcxx/test/std/numerics/bit/byteswap.pass.cpp
index 847a13387bdb6..f96af9410ead3 100644
--- a/libcxx/test/std/numerics/bit/byteswap.pass.cpp
+++ b/libcxx/test/std/numerics/bit/byteswap.pass.cpp
@@ -26,22 +26,10 @@ static_assert(!has_byteswap<float>);
static_assert(!has_byteswap<char[2]>);
static_assert(!has_byteswap<std::byte>);
-#if TEST_HAS_EXTENSION(bit_int)
-// _BitInt(N) is a candidate when N is a multiple of 16 (or sizeof == 1, the
-// identity case). Other widths are SFINAE-rejected via the requires clause.
-static_assert(has_byteswap<unsigned _BitInt(8)>);
-static_assert(has_byteswap<signed _BitInt(8)>);
-static_assert(has_byteswap<unsigned _BitInt(7)>);
-static_assert(has_byteswap<signed _BitInt(7)>);
-static_assert(has_byteswap<unsigned _BitInt(16)>);
-static_assert(has_byteswap<signed _BitInt(16)>);
-static_assert(has_byteswap<unsigned _BitInt(64)>);
-static_assert(!has_byteswap<unsigned _BitInt(13)>);
-static_assert(!has_byteswap<signed _BitInt(13)>);
-static_assert(!has_byteswap<unsigned _BitInt(17)>);
-static_assert(!has_byteswap<signed _BitInt(33)>);
-static_assert(!has_byteswap<unsigned _BitInt(65)>);
-#endif
+// _BitInt(N) candidacy is controlled by the `integral` constraint; the
+// padding-bit rejection (per [bit.byteswap]/Mandates) is a static_assert
+// inside the function body, not a SFINAE check. See byteswap.verify.cpp
+// for diagnostic verification of the rejected widths.
template <class T>
constexpr void test_num(T in, T expected) {
@@ -111,14 +99,13 @@ constexpr bool test() {
test_implementation_defined_size<unsigned long long>();
#if TEST_HAS_EXTENSION(bit_int)
- // sizeof == 1: identity. Returns the input unchanged regardless of the
- // value bits, 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));
+ // _BitInt(N) where digits + is_signed == sizeof * CHAR_BIT (no padding
+ // bits) is accepted; other widths are rejected by the static_assert
+ // inside the function body (see byteswap.verify.cpp).
+
+ // sizeof == 1
test_num<unsigned _BitInt(8)>(0xAB, 0xAB);
test_num<signed _BitInt(8)>(0x12, 0x12);
- 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 fallback or __builtin_bswapg
test_num<unsigned _BitInt(16)>(0xCDEF, 0xEFCD);
diff --git a/libcxx/test/std/numerics/bit/byteswap.verify.cpp b/libcxx/test/std/numerics/bit/byteswap.verify.cpp
new file mode 100644
index 0000000000000..f7ff1c6aefb11
--- /dev/null
+++ b/libcxx/test/std/numerics/bit/byteswap.verify.cpp
@@ -0,0 +1,141 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 integer types that have padding bits per
+// [bit.byteswap]/Mandates. The implementation uses static_assert; the
+// diagnostic comes from <__bit/byteswap.h>.
+
+#include <bit>
+
+#include "test_macros.h"
+
+#if TEST_HAS_EXTENSION(bit_int)
+
+// Sub-byte widths (sizeof == 1 but bit width below CHAR_BIT)
+void test_unsigned_1() {
+ unsigned _BitInt(1) v = 0;
+ // expected-error@*:* {{static assertion failed{{.*}}std::byteswap requires T to have no padding bits}}
+ (void)std::byteswap(v);
+}
+
+void test_unsigned_7() {
+ unsigned _BitInt(7) v = 0;
+ // expected-error@*:* {{static assertion failed{{.*}}std::byteswap requires T to have no padding bits}}
+ (void)std::byteswap(v);
+}
+
+void test_signed_7() {
+ signed _BitInt(7) v = 0;
+ // expected-error@*:* {{static assertion failed{{.*}}std::byteswap requires T to have no padding bits}}
+ (void)std::byteswap(v);
+}
+
+// Non-byte-aligned widths
+void test_unsigned_13() {
+ unsigned _BitInt(13) v = 0;
+ // expected-error@*:* {{static assertion failed{{.*}}std::byteswap requires T to have no padding bits}}
+ (void)std::byteswap(v);
+}
+
+void test_unsigned_17() {
+ unsigned _BitInt(17) v = 0;
+ // expected-error@*:* {{static assertion failed{{.*}}std::byteswap requires T to have no padding bits}}
+ (void)std::byteswap(v);
+}
+
+void test_signed_33() {
+ signed _BitInt(33) v = 0;
+ // expected-error@*:* {{static assertion failed{{.*}}std::byteswap requires T to have no padding bits}}
+ (void)std::byteswap(v);
+}
+
+void test_unsigned_65() {
+ unsigned _BitInt(65) v = 0;
+ // expected-error@*:* {{static assertion failed{{.*}}std::byteswap requires T to have no padding bits}}
+ (void)std::byteswap(v);
+}
+
+// Byte-aligned widths whose value bits don't fill the object representation.
+// On platforms where sizeof(_BitInt(N)) rounds up to a power of two, these
+// types have padding bits in the high-order storage positions even though
+// their value width is a multiple of CHAR_BIT.
+void test_unsigned_24() {
+ // sizeof(_BitInt(24)) == 4 on x86_64; 8 padding bits.
+ unsigned _BitInt(24) v = 0;
+ // expected-error@*:* {{static assertion failed{{.*}}std::byteswap requires T to have no padding bits}}
+ (void)std::byteswap(v);
+}
+
+void test_unsigned_40() {
+ // sizeof(_BitInt(40)) == 8 on x86_64; 24 padding bits.
+ unsigned _BitInt(40) v = 0;
+ // expected-error@*:* {{static assertion failed{{.*}}std::byteswap requires T to have no padding bits}}
+ (void)std::byteswap(v);
+}
+
+void test_unsigned_48() {
+ // sizeof(_BitInt(48)) == 8 on x86_64; 16 padding bits. Note that the value
+ // bit width (48) is a multiple of 16, so __builtin_bswapg accepts it -- the
+ // libc++ static_assert is what actually catches this case.
+ unsigned _BitInt(48) v = 0;
+ // expected-error@*:* {{static assertion failed{{.*}}std::byteswap requires T to have no padding bits}}
+ (void)std::byteswap(v);
+}
+
+void test_unsigned_56() {
+ // sizeof(_BitInt(56)) == 8 on x86_64; 8 padding bits.
+ unsigned _BitInt(56) v = 0;
+ // expected-error@*:* {{static assertion failed{{.*}}std::byteswap requires T to have no padding bits}}
+ (void)std::byteswap(v);
+}
+
+# if __BITINT_MAXWIDTH__ >= 80
+void test_unsigned_80() {
+ // sizeof(_BitInt(80)) == 16 on x86_64; 48 padding bits. Width 80 is also
+ // a multiple of 16, so bswapg would accept it without the static_assert.
+ unsigned _BitInt(80) v = 0;
+ // expected-error@*:* {{static assertion failed{{.*}}std::byteswap requires T to have no padding bits}}
+ (void)std::byteswap(v);
+}
+# endif
+
+# if __BITINT_MAXWIDTH__ >= 96
+void test_unsigned_96() {
+ // sizeof(_BitInt(96)) == 16 on x86_64; 32 padding bits.
+ unsigned _BitInt(96) v = 0;
+ // expected-error@*:* {{static assertion failed{{.*}}std::byteswap requires T to have no padding bits}}
+ (void)std::byteswap(v);
+}
+# endif
+
+# if __BITINT_MAXWIDTH__ >= 112
+void test_unsigned_112() {
+ // sizeof(_BitInt(112)) == 16 on x86_64; 16 padding bits.
+ unsigned _BitInt(112) v = 0;
+ // expected-error@*:* {{static assertion failed{{.*}}std::byteswap requires T to have no padding bits}}
+ (void)std::byteswap(v);
+}
+# endif
+
+# if __BITINT_MAXWIDTH__ >= 256
+void test_unsigned_192() {
+ // sizeof(_BitInt(192)) == 32 on x86_64; 64 padding bits. Multiple of 16
+ // but not of the storage size.
+ unsigned _BitInt(192) v = 0;
+ // expected-error@*:* {{static assertion failed{{.*}}std::byteswap requires T to have no padding bits}}
+ (void)std::byteswap(v);
+}
+# endif
+
+#else
+// expected-no-diagnostics
+#endif
More information about the libcxx-commits
mailing list