[libcxx-commits] [libcxx] [libc++][POC] to_chars/from_chars for _BitInt wider than 128 bits (PR #204355)

Xavier Roche via libcxx-commits libcxx-commits at lists.llvm.org
Wed Jun 17 22:17:44 PDT 2026


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

>From 87d44538ec6381cdfcaac02054edac884b2394cc Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Wed, 17 Jun 2026 15:12:33 +0200
Subject: [PATCH 1/2] [libc++] Support to_chars/from_chars for _BitInt wider
 than 128 bits

std::to_chars, std::from_chars (and the integer types they accept)
previously rejected any integer wider than 128 bits: the value was
promoted through __make_32_64_or_128_bit_t, which yields no type past
__int128, so a _BitInt(N > 128) was a hard compile error.

A _BitInt is a native arbitrary-precision integer; the compiler already
implements /, %, * and + on it. The base-2/8/16 paths, the generic
per-base loop, and the from_chars Horner accumulation already work on
any width through these operators. Only the base-10 fast paths route
through __itoa::__traits, which is specialized for 32/64/128-bit only.

Detect a wide _BitInt with __is_wide_bitint_v and route its base-10
conversion through the generic loop instead of the table-based path.
All other bases reuse the existing code unchanged.

Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>
---
 .../include/__charconv/from_chars_integral.h  | 14 ++++--
 libcxx/include/__charconv/to_chars_integral.h | 43 +++++++++++++------
 .../__type_traits/make_32_64_or_128_bit.h     | 13 ++++++
 3 files changed, 53 insertions(+), 17 deletions(-)

diff --git a/libcxx/include/__charconv/from_chars_integral.h b/libcxx/include/__charconv/from_chars_integral.h
index 3063c0978c8fa..c00fe5b6fce84 100644
--- a/libcxx/include/__charconv/from_chars_integral.h
+++ b/libcxx/include/__charconv/from_chars_integral.h
@@ -19,6 +19,7 @@
 #include <__type_traits/is_integral.h>
 #include <__type_traits/is_signed.h>
 #include <__type_traits/is_unsigned.h>
+#include <__type_traits/make_32_64_or_128_bit.h>
 #include <__type_traits/make_unsigned.h>
 #include <limits>
 
@@ -171,8 +172,11 @@ inline constexpr float __from_chars_log2f_lut[35] = {
 template <typename _Tp, __enable_if_t<is_unsigned<_Tp>::value, int> = 0>
 inline _LIBCPP_CONSTEXPR_SINCE_CXX23 _LIBCPP_HIDE_FROM_ABI from_chars_result
 __from_chars_integral(const char* __first, const char* __last, _Tp& __value, int __base) {
-  if (__base == 10)
-    return std::__from_chars_atoi(__first, __last, __value);
+  if (__base == 10) {
+    if constexpr (!__is_wide_bitint_v<_Tp>)
+      return std::__from_chars_atoi(__first, __last, __value);
+    // A wide _BitInt has no base-10 fast path; use the generic accumulator below.
+  }
 
   return std::__subject_seq_combinator(
       __first,
@@ -219,7 +223,11 @@ __from_chars_integral(const char* __first, const char* __last, _Tp& __value, int
 template <typename _Tp, __enable_if_t<is_integral<_Tp>::value, int> = 0>
 inline _LIBCPP_CONSTEXPR_SINCE_CXX23 _LIBCPP_HIDE_FROM_ABI from_chars_result
 from_chars(const char* __first, const char* __last, _Tp& __value) {
-  return std::__from_chars_atoi(__first, __last, __value);
+  if constexpr (__is_wide_bitint_v<_Tp>)
+    // A wide _BitInt routes through the base-aware generic accumulator.
+    return std::__from_chars_integral(__first, __last, __value, 10);
+  else
+    return std::__from_chars_atoi(__first, __last, __value);
 }
 
 template <typename _Tp, __enable_if_t<is_integral<_Tp>::value, int> = 0>
diff --git a/libcxx/include/__charconv/to_chars_integral.h b/libcxx/include/__charconv/to_chars_integral.h
index 6d425139260b6..86da51d484198 100644
--- a/libcxx/include/__charconv/to_chars_integral.h
+++ b/libcxx/include/__charconv/to_chars_integral.h
@@ -269,16 +269,25 @@ _LIBCPP_CONSTEXPR_SINCE_CXX23 _LIBCPP_HIDE_FROM_ABI int __to_chars_integral_widt
 template <class _Tp, __enable_if_t<!is_signed<_Tp>::value, int> >
 inline _LIBCPP_CONSTEXPR_SINCE_CXX23 _LIBCPP_HIDE_FROM_ABI __to_chars_result
 __to_chars_integral(char* __first, char* __last, _Tp __value, int __base) {
-  if (__base == 10) [[likely]]
+  if (__base == 10) [[likely]] {
+    // A wide _BitInt has no base-10 fast path; fall through to the generic loop.
+    // This helper is not guarded by _LIBCPP_STD_VER, so it must still parse in
+    // C++03, where `if constexpr` is unavailable; charconv is unused there.
+#if _LIBCPP_STD_VER >= 17
+    if constexpr (!__is_wide_bitint_v<_Tp>)
+      return std::__to_chars_itoa(__first, __last, __value, false_type());
+#else
     return std::__to_chars_itoa(__first, __last, __value, false_type());
-
-  switch (__base) {
-  case 2:
-    return std::__to_chars_integral<2>(__first, __last, __value);
-  case 8:
-    return std::__to_chars_integral<8>(__first, __last, __value);
-  case 16:
-    return std::__to_chars_integral<16>(__first, __last, __value);
+#endif
+  } else {
+    switch (__base) {
+    case 2:
+      return std::__to_chars_integral<2>(__first, __last, __value);
+    case 8:
+      return std::__to_chars_integral<8>(__first, __last, __value);
+    case 16:
+      return std::__to_chars_integral<16>(__first, __last, __value);
+    }
   }
 
   ptrdiff_t __cap = __last - __first;
@@ -321,9 +330,13 @@ to_chars_result to_chars(char*, char*, bool, int = 10) = delete;
 template <typename _Tp, __enable_if_t<is_integral<_Tp>::value, int> = 0>
 inline _LIBCPP_CONSTEXPR_SINCE_CXX23 _LIBCPP_HIDE_FROM_ABI to_chars_result
 to_chars(char* __first, char* __last, _Tp __value) {
-  using _Type = __make_32_64_or_128_bit_t<_Tp>;
-  static_assert(!is_same<_Type, void>::value, "unsupported integral type used in to_chars");
-  return std::__to_chars_itoa(__first, __last, static_cast<_Type>(__value), is_signed<_Tp>());
+  if constexpr (__is_wide_bitint_v<_Tp>)
+    // A _BitInt wider than 128 bits has no native promotion target; the
+    // base-aware path converts it using the type's own arithmetic.
+    return std::__to_chars_integral(__first, __last, __value, 10);
+  else
+    return std::__to_chars_itoa(
+        __first, __last, static_cast<__make_32_64_or_128_bit_t<_Tp>>(__value), is_signed<_Tp>());
 }
 
 template <typename _Tp, __enable_if_t<is_integral<_Tp>::value, int> = 0>
@@ -331,8 +344,10 @@ inline _LIBCPP_CONSTEXPR_SINCE_CXX23 _LIBCPP_HIDE_FROM_ABI to_chars_result
 to_chars(char* __first, char* __last, _Tp __value, int __base) {
   _LIBCPP_ASSERT_UNCATEGORIZED(2 <= __base && __base <= 36, "base not in [2, 36]");
 
-  using _Type = __make_32_64_or_128_bit_t<_Tp>;
-  return std::__to_chars_integral(__first, __last, static_cast<_Type>(__value), __base);
+  if constexpr (__is_wide_bitint_v<_Tp>)
+    return std::__to_chars_integral(__first, __last, __value, __base);
+  else
+    return std::__to_chars_integral(__first, __last, static_cast<__make_32_64_or_128_bit_t<_Tp>>(__value), __base);
 }
 
 #endif // _LIBCPP_STD_VER >= 17
diff --git a/libcxx/include/__type_traits/make_32_64_or_128_bit.h b/libcxx/include/__type_traits/make_32_64_or_128_bit.h
index 7016209ec9c0a..e19517851b969 100644
--- a/libcxx/include/__type_traits/make_32_64_or_128_bit.h
+++ b/libcxx/include/__type_traits/make_32_64_or_128_bit.h
@@ -44,6 +44,19 @@ using __make_32_64_or_128_bit_t _LIBCPP_NODEBUG =
     > > >;
 // clang-format on
 
+// True for a _BitInt wider than 128 bits, i.e. an integer with no 32/64/128-bit
+// promotion target. charconv converts these with a bignum path built on the
+// type's own arithmetic. __make_32_64_or_128_bit_t is not usable to detect this:
+// for such a type it is not void but ill-formed (it feeds void to __make_unsigned),
+// so the test is on sizeof, matching the trait's own logic.
+template <class _Tp>
+inline const bool __is_wide_bitint_v =
+#if _LIBCPP_HAS_INT128
+    sizeof(_Tp) > sizeof(__int128_t);
+#else
+    sizeof(_Tp) > sizeof(long long);
+#endif
+
 _LIBCPP_END_NAMESPACE_STD
 
 #endif // _LIBCPP___TYPE_TRAITS_MAKE_32_64_OR_128_BIT_H

>From 96dc857171aba75da011a3c3ec2e7362cb5d5005 Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Wed, 17 Jun 2026 15:24:57 +0200
Subject: [PATCH 2/2] [libc++][test] Cover to_chars/from_chars for _BitInt
 wider than 128 bits

Extend the existing charconv integral tests with a _BitInt(N > 128)
section, guarded on __BITINT_MAXWIDTH__. The native harness does not
apply to wide _BitInt (its buffer is fixed-size and its reference is
strtoll), so the wide cases use hand-verified literals plus round-trip.

Coverage: signed min via the unsigned-counterpart magnitude, signed and
unsigned max, exact value_too_large boundaries, lowercase digits with no
base prefix (0x and 0b both rejected), from_chars out-of-range with value
left unmodified and ptr past the maximal match, the most-negative success
asymmetry, and a round-trip sweep over bases 2..36 and several widths.

The headline 4096-bit width runs in constant evaluation too, both on a
focused base/value subset and against an independent oracle: a sparse
4096-bit value whose decimal and hex strings are computed by Python's
arbitrary-precision int, which catches a bug to_chars and from_chars
might share that a round-trip cannot. Non-byte-aligned widths run at
runtime only, pending the clang-23 fix for constexpr
__builtin_mul_overflow on those widths (llvm.org/PR204085).

Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>
---
 .../charconv.from.chars/integral.pass.cpp     |  86 +++++++++
 .../integral.roundtrip.pass.cpp               | 174 ++++++++++++++++++
 .../charconv.to.chars/integral.pass.cpp       |  65 ++++++-
 3 files changed, 324 insertions(+), 1 deletion(-)

diff --git a/libcxx/test/std/utilities/charconv/charconv.from.chars/integral.pass.cpp b/libcxx/test/std/utilities/charconv/charconv.from.chars/integral.pass.cpp
index 4708da4c38b9b..4f653db802a25 100644
--- a/libcxx/test/std/utilities/charconv/charconv.from.chars/integral.pass.cpp
+++ b/libcxx/test/std/utilities/charconv/charconv.from.chars/integral.pass.cpp
@@ -137,6 +137,85 @@ struct test_signed
     }
 };
 
+#if defined(__BITINT_MAXWIDTH__) && __BITINT_MAXWIDTH__ >= 256
+// _BitInt wider than 128 bits parses via the generic Horner accumulator. The
+// error paths must leave value unmodified; check that with a sentinel.
+template <class T, std::size_t N>
+TEST_CONSTEXPR_CXX23 void fc_ok(const char (&s)[N], T expect, std::size_t consumed, int base = 10) {
+  T v    = 0x55;
+  auto r = std::from_chars(s, s + N - 1, v, base);
+  assert(r.ec == std::errc{});
+  assert(r.ptr == s + consumed);
+  assert(v == expect);
+}
+
+template <class T, std::size_t N>
+TEST_CONSTEXPR_CXX23 void fc_invalid(const char (&s)[N], int base = 10) {
+  T v    = 0x55;
+  auto r = std::from_chars(s, s + N - 1, v, base);
+  assert(r.ec == std::errc::invalid_argument);
+  assert(r.ptr == s);   // no characters match -> ptr == first
+  assert(v == T(0x55)); // value unmodified
+}
+
+template <class T, std::size_t N>
+TEST_CONSTEXPR_CXX23 void fc_oor(const char (&s)[N], std::size_t consumed, int base = 10) {
+  T v    = 0x55;
+  auto r = std::from_chars(s, s + N - 1, v, base);
+  assert(r.ec == std::errc::result_out_of_range);
+  assert(r.ptr == s + consumed); // past the maximal matching sequence
+  assert(v == T(0x55));          // value unmodified
+}
+
+TEST_CONSTEXPR_CXX23 bool test_wide() {
+  using S = signed _BitInt(256);
+  using U = unsigned _BitInt(256);
+
+  // Success and ptr placement.
+  fc_ok<U>("42", U(42), 2);
+  fc_ok<S>("-42", S(-42), 3);
+  fc_ok<S>("-0", S(0), 2);
+  fc_ok<U>("007", U(7), 3); // leading zeros consumed
+  fc_ok<U>("ff", U(255), 2, 16);
+  fc_ok<U>("FF", U(255), 2, 16); // case-insensitive
+  fc_ok<U>("123abc", U(123), 3); // base 10: stops at 'a'
+  fc_ok<U>("123abc", U(0x123abc), 6, 16);
+  fc_ok<U>("0x10", U(0), 1, 16); // no 0x prefix: matches '0', stops at 'x'
+  fc_ok<U>("0b10", U(0), 1, 2);  // no 0b prefix: matches '0', stops at 'b'
+  fc_ok<U>("12", U(1), 1, 2);    // '2' >= base 2: stops
+
+  // No match: value unmodified, ptr == first.
+  fc_invalid<U>("");
+  fc_invalid<U>("+5"); // '+' is never accepted
+  fc_invalid<S>("+5");
+  fc_invalid<U>("-5"); // '-' only for signed types
+  fc_invalid<S>("-");  // sign with no digits
+  fc_invalid<U>(" 5"); // leading whitespace is not skipped
+  fc_invalid<U>("z");
+
+  // Out of range: value unmodified, ptr past every matching digit.
+  fc_oor<U>("115792089237316195423570985008687907853269984665640564039457584007913129639936", 78); // 2^256
+  fc_oor<U>("115792089237316195423570985008687907853269984665640564039457584007913129639936zzz", 78);
+  fc_oor<S>("57896044618658097711785492504343953926634992332820282019728792003956564819968",
+            77); // 2^255 = signed max + 1
+
+  // Most-negative -2^255 succeeds: magnitude is signed max + 1 but valid as a
+  // negative value.
+  fc_ok<S>("-57896044618658097711785492504343953926634992332820282019728792003956564819968",
+           std::numeric_limits<S>::min(),
+           78);
+
+  // Exact boundaries: max accepted, max + 1 rejected.
+  fc_ok<U>("115792089237316195423570985008687907853269984665640564039457584007913129639935",
+           std::numeric_limits<U>::max(),
+           78); // 2^256 - 1
+  fc_ok<S>("57896044618658097711785492504343953926634992332820282019728792003956564819967",
+           std::numeric_limits<S>::max(),
+           77); // 2^255 - 1
+  return true;
+}
+#endif
+
 TEST_CONSTEXPR_CXX23 bool test()
 {
     run<test_basics>(integrals);
@@ -151,5 +230,12 @@ int main(int, char**) {
     static_assert(test());
 #endif
 
+#if defined(__BITINT_MAXWIDTH__) && __BITINT_MAXWIDTH__ >= 256
+    test_wide();
+#  if TEST_STD_VER > 20
+    static_assert(test_wide());
+#  endif
+#endif
+
     return 0;
 }
diff --git a/libcxx/test/std/utilities/charconv/charconv.from.chars/integral.roundtrip.pass.cpp b/libcxx/test/std/utilities/charconv/charconv.from.chars/integral.roundtrip.pass.cpp
index b1f2c77b34f8a..46c97305374d1 100644
--- a/libcxx/test/std/utilities/charconv/charconv.from.chars/integral.roundtrip.pass.cpp
+++ b/libcxx/test/std/utilities/charconv/charconv.from.chars/integral.roundtrip.pass.cpp
@@ -77,6 +77,160 @@ struct test_signed : roundtrip_test_base<T>
     }
 };
 
+#if defined(__BITINT_MAXWIDTH__) && __BITINT_MAXWIDTH__ >= 4096
+// Round-trip is the broad-coverage backbone for wide _BitInt: to_chars then
+// from_chars must recover the value and consume exactly what was written.
+template <int Bits>
+TEST_CONSTEXPR_CXX23 void rt_one(unsigned _BitInt(Bits) u, int base) {
+  char buf[Bits + 2]; // base 2 is the widest: up to Bits digits, plus a sign
+
+  std::to_chars_result r = std::to_chars(buf, buf + sizeof(buf), u, base);
+  assert(r.ec == std::errc{});
+  unsigned _BitInt(Bits) ub = 1;
+  std::from_chars_result f  = std::from_chars(buf, r.ptr, ub, base);
+  assert(f.ec == std::errc{} && f.ptr == r.ptr && ub == u);
+
+  signed _BitInt(Bits) s = static_cast<signed _BitInt(Bits)>(u);
+  r                      = std::to_chars(buf, buf + sizeof(buf), s, base);
+  assert(r.ec == std::errc{});
+  signed _BitInt(Bits) sb = 1;
+  f                       = std::from_chars(buf, r.ptr, sb, base);
+  assert(f.ec == std::errc{} && f.ptr == r.ptr && sb == s);
+}
+
+template <int Bits>
+TEST_CONSTEXPR_CXX23 void rt_width() {
+  using U = unsigned _BitInt(Bits);
+  using S = signed _BitInt(Bits);
+  for (int b = 2; b <= 36; ++b) {
+    rt_one<Bits>(U(0), b);
+    rt_one<Bits>(U(1), b);
+    rt_one<Bits>(std::numeric_limits<U>::max(), b);                 // all ones
+    rt_one<Bits>(static_cast<U>(std::numeric_limits<S>::max()), b); // signed max
+    rt_one<Bits>(static_cast<U>(std::numeric_limits<S>::min()), b); // signed min pattern
+    rt_one<Bits>(std::numeric_limits<U>::max() / 3, b);             // arbitrary spread
+  }
+}
+
+// Byte-aligned widths only, kept small enough for constant evaluation.
+TEST_CONSTEXPR_CXX23 bool test_wide() {
+  rt_width<256>();
+  return true;
+}
+
+// 4096 bits is the headline "reasonable width": exercise it in constant
+// evaluation too, on a representative base/value subset. The full base sweep at
+// this width is kept at runtime (test_wide_runtime) to bound constexpr cost.
+TEST_CONSTEXPR_CXX23 bool test_wide_4096() {
+  using U        = unsigned _BitInt(4096);
+  using S        = signed _BitInt(4096);
+  const U vals[] = {
+      U(0),
+      U(1),
+      std::numeric_limits<U>::max(),                 // all ones
+      static_cast<U>(std::numeric_limits<S>::min()), // signed min pattern
+      std::numeric_limits<U>::max() / 3,             // arbitrary spread
+  };
+  for (U v : vals) {
+    rt_one<4096>(v, 2);
+    rt_one<4096>(v, 10);
+    rt_one<4096>(v, 16);
+  }
+  return true;
+}
+
+// Independent oracle for a huge 4096-bit value. The decimal and hex strings are
+// computed by Python's arbitrary-precision int, so they catch a bug that
+// to_chars and from_chars might share, which a pure round-trip cannot. The value
+// is built from its bits, independent of the strings; the sparse pattern gives
+// long interior-zero digit runs plus a few clusters.
+TEST_CONSTEXPR_CXX23 bool test_wide_oracle() {
+  using U   = unsigned _BitInt(4096);
+  const U v = (U(1) << 4095) | (U(1) << 3000) | (U(1) << 1500) | (U(1) << 700) | U(0xDEADBEEFCAFEULL);
+
+  const char dec[] =
+      "522194440706576253345876355358312191289982124523691890192116741641976953985778728424413405967498779170445053357"
+      "219631418993786719092896803631618043925682638972978488271854999170180795067191859157214035005927973113188159419"
+      "698856372836167342172293308748403954352901852035642024370059304557233988891799014503343469488440893892973454045"
+      "327052631416966658275225011404027988935425249341701966051886267829647532452891421103226957550676830796693159988"
+      "124656750434275418911327793613319612182498160234866841889152188408311919545372889132335138169799010480395144069"
+      "285672659513941271465489792446864983055373122330734549035017206965872628152160774541392717285230405353948204585"
+      "721558937253615907178075626191061140618987126175926194937994062055639754879354953746504582267597824430780046407"
+      "654767442218179275002597906567544454462957002216329001106694021676565785487186595814555929076464240677469561177"
+      "107218357758641620381943183829806874856274226199817314163151233513560130957415780849310362681536620722609148912"
+      "812834526196870692951390912269511897170617673817942753906754404005592596448147882135379795095255338256287637038"
+      "130680736166406732152594023784824442544596004877042110465174074339660336326179460333127852406170867605207327911"
+      "022565509886";
+  const char hex[] =
+      "80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
+      "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
+      "00000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000"
+      "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
+      "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
+      "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000"
+      "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
+      "00000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000"
+      "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
+      "0000000000000000000000deadbeefcafe";
+  // Base 36 exercises the generic loop's full 0-9a-z alphabet, which the base-10
+  // and base-16 (LUT) checks above do not reach.
+  const char b36[] =
+      "1c58ccvc205beuqph4wbfhcolylc3mogv6cfu44q0ny092hqw421ukr2pnoci21sapp8q00xd2kotzuf65r9stubez1tfplsprvrzegyah5l9h"
+      "71zr6zddlj4112jbkymn7hv9porwxfhp8jmu3gjgabuvjoc0t3p140isx85z3k4yuaoxq4r9wmummh8rujjxrazfodidhk9o6apnne4pacu79i"
+      "n1cf5qaedb7s1dpskq2r9wgdswck57yqvobgs1d2xg154sb95jg39tt0bzyf5aptbw9z7ahqjnt3w7jp1tth21xwfn8dgzb5wvlc3zeijz0asb"
+      "5d38l7qxbsj8pg8tkibhs3ooie5aizfh2657d6tz8ysut8g3zm254dqifpqj1i91wmfkofgvks973a706bz0ejpjdf5ucmra7d1x9x9wuiz044"
+      "g7jcu71ofwdn4iwk8sxj17n3wrucuxed308dhf5aqumg19q7hftzrmp3obtygzt0zmut76adjffjwsmh3q5wcmx11d0rvigxuklx1i4r6stzuq"
+      "y2fzxxhwberb6qmssvqffyt0mtzpaiez6s2p0api2i57fb6sfjkdj0c9ofdlnwo852fauo2bcxgtz62qe2ym2gt4g25haatt7xm9ck7wf2wwg7"
+      "j9gpc8774a0u1p3wrolmysbzqagimzsg5foifbriuwyqkjlysp6qe3tvy658zmwpk2ib8p7b063v079ue0un8k61d7cabadmbjhki1c7g02y7d"
+      "t6wrr3szp6wzcmcfe8tw16m";
+
+  char buf[4100];
+  std::to_chars_result r = std::to_chars(buf, buf + sizeof(buf), v, 10);
+  assert(r.ec == std::errc{} && static_cast<std::size_t>(r.ptr - buf) == sizeof(dec) - 1);
+  for (std::size_t i = 0; i + 1 < sizeof(dec); ++i)
+    assert(buf[i] == dec[i]);
+
+  r = std::to_chars(buf, buf + sizeof(buf), v, 16);
+  assert(r.ec == std::errc{} && static_cast<std::size_t>(r.ptr - buf) == sizeof(hex) - 1);
+  for (std::size_t i = 0; i + 1 < sizeof(hex); ++i)
+    assert(buf[i] == hex[i]);
+
+  r = std::to_chars(buf, buf + sizeof(buf), v, 36);
+  assert(r.ec == std::errc{} && static_cast<std::size_t>(r.ptr - buf) == sizeof(b36) - 1);
+  for (std::size_t i = 0; i + 1 < sizeof(b36); ++i)
+    assert(buf[i] == b36[i]);
+
+  U back                   = 0;
+  std::from_chars_result f = std::from_chars(dec, dec + sizeof(dec) - 1, back, 10);
+  assert(f.ec == std::errc{} && f.ptr == dec + sizeof(dec) - 1 && back == v);
+
+  back = 0;
+  f    = std::from_chars(hex, hex + sizeof(hex) - 1, back, 16);
+  assert(f.ec == std::errc{} && f.ptr == hex + sizeof(hex) - 1 && back == v);
+
+  back = 0;
+  f    = std::from_chars(b36, b36 + sizeof(b36) - 1, back, 36);
+  assert(f.ec == std::errc{} && f.ptr == b36 + sizeof(b36) - 1 && back == v);
+  return true;
+}
+
+// Wider and non-byte-aligned widths at runtime. Non-aligned widths are excluded
+// from constant evaluation pending the clang-23 fix for constexpr
+// __builtin_mul_overflow on non-byte-aligned _BitInt (llvm.org/PR204085).
+void test_wide_runtime() {
+  rt_width<512>();
+  rt_width<129>();
+  rt_width<257>();
+#  if __SIZEOF_POINTER__ >= 8
+  // The widest sweeps are quadratic and, with 32-bit limbs, overrun the lit
+  // timeout on a 32-bit target (seen on clang-21 i686-mingw). 64-bit configs
+  // cover them; 32-bit keeps the smaller wide widths above.
+  rt_width<1000>();
+  rt_width<4096>();
+#  endif
+}
+#endif
+
 TEST_CONSTEXPR_CXX23 bool test()
 {
     run<test_basics>(integrals);
@@ -91,5 +245,25 @@ int main(int, char**) {
     static_assert(test());
 #endif
 
+#if defined(__BITINT_MAXWIDTH__) && __BITINT_MAXWIDTH__ >= 4096
+    test_wide();
+    test_wide_runtime();
+    // 4096-bit round-trips (runtime and constant-evaluated) run on 64-bit targets
+    // only. On a 32-bit target the quadratic base-10 conversion is slow enough to
+    // exceed the lit timeout (seen on clang-21 i686-mingw); the smaller wide
+    // widths in test_wide / test_wide_runtime still cover 32-bit.
+#  if __SIZEOF_POINTER__ >= 8
+    test_wide_4096();
+    test_wide_oracle();
+#  endif
+#  if TEST_STD_VER > 20
+    static_assert(test_wide());
+#    if __SIZEOF_POINTER__ >= 8
+    static_assert(test_wide_4096());
+    static_assert(test_wide_oracle());
+#    endif
+#  endif
+#endif
+
     return 0;
 }
diff --git a/libcxx/test/std/utilities/charconv/charconv.to.chars/integral.pass.cpp b/libcxx/test/std/utilities/charconv/charconv.to.chars/integral.pass.cpp
index e89b340bdfcbe..170a8ff5c5c35 100644
--- a/libcxx/test/std/utilities/charconv/charconv.to.chars/integral.pass.cpp
+++ b/libcxx/test/std/utilities/charconv/charconv.to.chars/integral.pass.cpp
@@ -293,6 +293,62 @@ struct test_signed : to_chars_test_base<T>
     }
 };
 
+#if defined(__BITINT_MAXWIDTH__) && __BITINT_MAXWIDTH__ >= 4096
+// _BitInt wider than 128 bits has no native promotion target, so the native
+// harness above (fixed buffer, strtoll reference) does not apply. Check it with
+// hand-verified literals plus exact-fit / one-too-short buffer handling.
+template <class T, std::size_t N>
+TEST_CONSTEXPR_CXX23 void test_wide_one(T v, const char (&expect)[N], int base = 10) {
+  const std::size_t len = N - 1;
+  char buf[4200];
+
+  std::to_chars_result r = std::to_chars(buf, buf + sizeof(buf), v, base);
+  assert(r.ec == std::errc{});
+  assert(static_cast<std::size_t>(r.ptr - buf) == len);
+  for (std::size_t i = 0; i < len; ++i)
+    assert(buf[i] == expect[i]);
+
+  // Exact-fit buffer succeeds; one byte short fails with ptr == last and no
+  // assertion on the (unspecified) buffer contents.
+  r = std::to_chars(buf, buf + len, v, base);
+  assert(r.ec == std::errc{} && r.ptr == buf + len);
+  r = std::to_chars(buf, buf + len - 1, v, base);
+  assert(r.ec == std::errc::value_too_large && r.ptr == buf + len - 1);
+}
+
+TEST_CONSTEXPR_CXX23 bool test_wide() {
+  using S   = signed _BitInt(256);
+  using U   = unsigned _BitInt(256);
+  using SLn = std::numeric_limits<S>;
+  using ULn = std::numeric_limits<U>;
+
+  test_wide_one(U(0), "0");
+  test_wide_one(S(0), "0", 16);
+  test_wide_one(U(255), "255");
+  test_wide_one(U(255), "ff", 16); // lowercase, no 0x prefix
+  test_wide_one(U(0xabcu), "abc", 16);
+  test_wide_one(U(10), "1010", 2);
+  test_wide_one(S(-12), "-12");
+  test_wide_one(S(-255), "-ff", 16);
+  test_wide_one(S(-10), "-1010", 2);
+
+  // Trailing/interior zeros must survive (no stray digit dropping).
+  test_wide_one(U(1000000000u), "1000000000");
+
+  // Signed min = -2^255: magnitude 2^255 is not representable in S, so it must
+  // be formed in the unsigned counterpart. (Base 2 is covered by the round-trip
+  // test to avoid a 257-character literal.)
+  test_wide_one(SLn::min(), "-57896044618658097711785492504343953926634992332820282019728792003956564819968");
+  test_wide_one(SLn::min(), "-8000000000000000000000000000000000000000000000000000000000000000", 16);
+
+  // Signed max = 2^255-1, unsigned max = 2^256-1.
+  test_wide_one(SLn::max(), "57896044618658097711785492504343953926634992332820282019728792003956564819967");
+  test_wide_one(ULn::max(), "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16);
+
+  return true;
+}
+#endif
+
 TEST_CONSTEXPR_CXX23 bool test()
 {
     run<test_basics>(integrals);
@@ -308,5 +364,12 @@ int main(int, char**)
     static_assert(test());
 #endif
 
-  return 0;
+#if defined(__BITINT_MAXWIDTH__) && __BITINT_MAXWIDTH__ >= 4096
+    test_wide();
+#  if TEST_STD_VER > 20
+    static_assert(test_wide());
+#  endif
+#endif
+
+    return 0;
 }



More information about the libcxx-commits mailing list