[libcxx-commits] [libcxx] [libc++] Fix numeric_limits::digits and digits10 for _BitInt(N) (PR #193002)

Xavier Roche via libcxx-commits libcxx-commits at lists.llvm.org
Mon Apr 20 08:35:01 PDT 2026


https://github.com/xroche created https://github.com/llvm/llvm-project/pull/193002

Fix `std::numeric_limits<T>::digits` and `digits10` for `_BitInt(N)`.

`digits` was `sizeof(T) * CHAR_BIT - is_signed`, which counts padding bits for non-byte-aligned widths. `unsigned _BitInt(13)` reported `digits=16` instead of `13`. Computing it from `__builtin_popcountg(~unsigned_type(0))` gives the right answer: padding bits stay zero after bitwise NOT, so the popcount matches the value-bit width.

`digits10 = digits * 3 / 10` was also wrong for `digits >= 256` (gave 76, should be 77). `digits * 643 / 2136` is exact up to `__BITINT_MAXWIDTH__` (8388608 on x86).

Tests cover signed and unsigned `_BitInt` at byte-aligned (8/32/64) and odd (7/13/37/77/128/129/255/256/4096) widths, guarded on `TEST_HAS_EXTENSION(bit_int)` and the `__BITINT_MAXWIDTH__` tiers from C23 7.18.2.5. `min`/`max` also get `_BitInt` coverage since `__min = 1 << digits` flowed through the old buggy value.

Part of the [_BitInt(N) libc++ effort](https://discourse.llvm.org/t/bitint-n-support-in-libc-investigations-possible-improvements-looking-for-guidance/90063). Split out of #185027 at philnik777's request in #130584.

## AI Disclosure

This implementation was developed with assistance from Claude (Anthropic). All
commits carry `Assisted-by: Claude (Anthropic)` and
`Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>` trailers, per the
[LLVM AI Tool Policy](https://llvm.org/docs/DeveloperPolicy.html#use-of-ai-generated-contributions).
All code has been reviewed and validated by the author.

>From f35437844db013ec18eb4ee8c15bb57957027ec5 Mon Sep 17 00:00:00 2001
From: Xavier Roche <xavier.roche at algolia.com>
Date: Mon, 20 Apr 2026 17:34:26 +0200
Subject: [PATCH] [libc++] Fix numeric_limits::digits and digits10 for
 _BitInt(N)

digits was sizeof(T) * CHAR_BIT - is_signed, which counts padding bits
for non-byte-aligned _BitInt(N) widths. unsigned _BitInt(13) reported
digits=16 instead of 13.

Compute it from __builtin_popcountg(~unsigned_type(0)): padding bits
stay zero after bitwise NOT, so the popcount gives the value-bit width.
__numeric_limits_unsigned_type maps signed T to its unsigned counterpart
via __make_unsigned. The old form is the fallback when these builtins
aren't available.

digits10 = digits * 3 / 10 was also wrong for digits >= 256 (gave 76,
should be 77). digits * 643 / 2136 is exact up to __BITINT_MAXWIDTH__
(8388608 on x86).

Split out of #185027 at philnik777's request in issue #130584.

Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.6 <noreply at anthropic.com>
---
 libcxx/include/limits                         | 30 +++++++++++--
 .../numeric.limits.members/digits.pass.cpp    | 42 ++++++++++++++++++-
 .../numeric.limits.members/digits10.pass.cpp  | 34 ++++++++++++++-
 .../numeric.limits.members/max.pass.cpp       | 23 +++++++++-
 .../numeric.limits.members/min.pass.cpp       | 24 ++++++++++-
 5 files changed, 146 insertions(+), 7 deletions(-)

diff --git a/libcxx/include/limits b/libcxx/include/limits
index ff40d2051d06f..923f8a9864e6d 100644
--- a/libcxx/include/limits
+++ b/libcxx/include/limits
@@ -177,6 +177,22 @@ protected:
   static _LIBCPP_CONSTEXPR const float_round_style round_style = round_toward_zero;
 };
 
+// Helper that counts the value-bit width of an integer type T (excluding the
+// sign bit for signed types). For _BitInt(N), sizeof*CHAR_BIT may exceed N
+// due to padding bits; __builtin_popcountg of ~unsigned_type(0) returns the
+// actual value width (padding bits stay zero on bitwise NOT). Standard types
+// have no padding, so the result matches sizeof * CHAR_BIT.
+#if __has_builtin(__builtin_popcountg) && __has_builtin(__make_unsigned)
+template <class _Tp, bool _IsSigned = (_Tp(-1) < _Tp(0))>
+struct __numeric_limits_unsigned_type {
+  using type = _Tp;
+};
+template <class _Tp>
+struct __numeric_limits_unsigned_type<_Tp, true> {
+  using type = __make_unsigned(_Tp);
+};
+#endif
+
 template <class _Tp>
 class __libcpp_numeric_limits<_Tp, true> {
 protected:
@@ -184,9 +200,17 @@ protected:
 
   static _LIBCPP_CONSTEXPR const bool is_specialized = true;
 
-  static _LIBCPP_CONSTEXPR const bool is_signed   = type(-1) < type(0);
-  static _LIBCPP_CONSTEXPR const int digits       = static_cast<int>(sizeof(type) * __CHAR_BIT__ - is_signed);
-  static _LIBCPP_CONSTEXPR const int digits10     = digits * 3 / 10;
+  static _LIBCPP_CONSTEXPR const bool is_signed = type(-1) < type(0);
+#if __has_builtin(__builtin_popcountg) && __has_builtin(__make_unsigned)
+  using __unsigned_type                     = typename __numeric_limits_unsigned_type<type>::type;
+  static _LIBCPP_CONSTEXPR const int digits = __builtin_popcountg(static_cast<__unsigned_type>(~__unsigned_type(0))) - is_signed;
+#else
+  static _LIBCPP_CONSTEXPR const int digits = static_cast<int>(sizeof(type) * __CHAR_BIT__ - is_signed);
+#endif
+  // digits10 = floor(digits * log10(2)); 643/2136 is exact for digits up to
+  // __BITINT_MAXWIDTH__ (8388608 on x86). The old digits*3/10 was wrong for
+  // digits >= 256 (gave 76 instead of 77).
+  static _LIBCPP_CONSTEXPR const int digits10     = static_cast<int>((digits * 643LL) / 2136);
   static _LIBCPP_CONSTEXPR const int max_digits10 = 0;
   static _LIBCPP_CONSTEXPR const type __min       = is_signed ? _Tp(_Tp(1) << digits) : 0;
   static _LIBCPP_CONSTEXPR const type __max       = is_signed ? type(type(~0) ^ __min) : type(~0);
diff --git a/libcxx/test/std/language.support/support.limits/limits/numeric.limits.members/digits.pass.cpp b/libcxx/test/std/language.support/support.limits/limits/numeric.limits.members/digits.pass.cpp
index 4608a3d01ba14..4e3e3c593afd0 100644
--- a/libcxx/test/std/language.support/support.limits/limits/numeric.limits.members/digits.pass.cpp
+++ b/libcxx/test/std/language.support/support.limits/limits/numeric.limits.members/digits.pass.cpp
@@ -53,5 +53,45 @@ int main(int, char**)
     test<double, DBL_MANT_DIG>();
     test<long double, LDBL_MANT_DIG>();
 
-  return 0;
+    // _BitInt(N): digits must equal N for unsigned and N-1 for signed,
+    // regardless of padding. The old sizeof*CHAR_BIT form counted padding
+    // bits for non-byte-aligned widths (e.g. unsigned _BitInt(13) reported
+    // 16 instead of 13).
+#if TEST_HAS_EXTENSION(bit_int)
+    // Byte-aligned widths (historically correct).
+    test<unsigned _BitInt(8), 8>();
+    test<signed _BitInt(8), 7>();
+    test<unsigned _BitInt(32), 32>();
+    test<signed _BitInt(32), 31>();
+    test<unsigned _BitInt(64), 64>();
+    test<signed _BitInt(64), 63>();
+
+    // Odd widths: these are the cases the old formula got wrong.
+    test<unsigned _BitInt(7), 7>();
+    test<signed _BitInt(7), 6>();
+    test<unsigned _BitInt(13), 13>();
+    test<signed _BitInt(13), 12>();
+    test<unsigned _BitInt(37), 37>();
+    test<signed _BitInt(37), 36>();
+#  if __BITINT_MAXWIDTH__ >= 128
+    test<unsigned _BitInt(77), 77>();
+    test<signed _BitInt(77), 76>();
+    test<unsigned _BitInt(128), 128>();
+    test<signed _BitInt(128), 127>();
+#  endif
+#  if __BITINT_MAXWIDTH__ >= 256
+    test<unsigned _BitInt(129), 129>();
+    test<signed _BitInt(129), 128>();
+    test<unsigned _BitInt(255), 255>();
+    test<signed _BitInt(255), 254>();
+    test<unsigned _BitInt(256), 256>();
+    test<signed _BitInt(256), 255>();
+#  endif
+#  if __BITINT_MAXWIDTH__ >= 4096
+    test<unsigned _BitInt(4096), 4096>();
+    test<signed _BitInt(4096), 4095>();
+#  endif
+#endif // TEST_HAS_EXTENSION(bit_int)
+
+    return 0;
 }
diff --git a/libcxx/test/std/language.support/support.limits/limits/numeric.limits.members/digits10.pass.cpp b/libcxx/test/std/language.support/support.limits/limits/numeric.limits.members/digits10.pass.cpp
index 41f134a706bcd..b151a1ba52d07 100644
--- a/libcxx/test/std/language.support/support.limits/limits/numeric.limits.members/digits10.pass.cpp
+++ b/libcxx/test/std/language.support/support.limits/limits/numeric.limits.members/digits10.pass.cpp
@@ -57,5 +57,37 @@ int main(int, char**)
     test<double, DBL_DIG>();
     test<long double, LDBL_DIG>();
 
-  return 0;
+    // _BitInt(N): digits10 = floor((N - is_signed) * log10(2)). The formula
+    // `digits * 3 / 10` was wrong for digits >= 256: e.g. unsigned
+    // _BitInt(256) has digits=256 and log10(2^256) ~= 77.06, so digits10
+    // must be 77, not 76.
+#if TEST_HAS_EXTENSION(bit_int)
+    test<unsigned _BitInt(8), 2>();   // digits=8,   log10=2.4
+    test<signed _BitInt(8), 2>();     // digits=7,   log10=2.1
+    test<unsigned _BitInt(13), 3>();  // digits=13,  log10=3.9
+    test<signed _BitInt(13), 3>();    // digits=12,  log10=3.6
+    test<unsigned _BitInt(32), 9>();  // digits=32,  log10=9.6
+    test<unsigned _BitInt(37), 11>(); // digits=37,  log10=11.1
+    test<unsigned _BitInt(64), 19>(); // digits=64,  log10=19.3
+    test<signed _BitInt(64), 18>();   // digits=63,  log10=18.9
+#  if __BITINT_MAXWIDTH__ >= 128
+    test<unsigned _BitInt(77), 23>();  // digits=77,  log10=23.2
+    test<signed _BitInt(77), 22>();    // digits=76,  log10=22.9
+    test<unsigned _BitInt(128), 38>(); // digits=128, log10=38.5
+    test<signed _BitInt(128), 38>();   // digits=127, log10=38.2
+#  endif
+#  if __BITINT_MAXWIDTH__ >= 256
+    test<unsigned _BitInt(129), 38>(); // digits=129, log10=38.8
+    test<unsigned _BitInt(255), 76>(); // digits=255, log10=76.8
+    test<unsigned _BitInt(256), 77>(); // digits=256, log10=77.1 (old: 76)
+    test<signed _BitInt(256), 76>();   // digits=255, log10=76.8
+    test<unsigned _BitInt(257), 77>(); // digits=257, log10=77.4
+#  endif
+#  if __BITINT_MAXWIDTH__ >= 4096
+    test<unsigned _BitInt(4096), 1233>(); // digits=4096, log10=1233.0
+    test<signed _BitInt(4096), 1232>();   // digits=4095, log10=1232.7
+#  endif
+#endif // TEST_HAS_EXTENSION(bit_int)
+
+    return 0;
 }
diff --git a/libcxx/test/std/language.support/support.limits/limits/numeric.limits.members/max.pass.cpp b/libcxx/test/std/language.support/support.limits/limits/numeric.limits.members/max.pass.cpp
index 2f7c1f899a73f..fe8f039416d3a 100644
--- a/libcxx/test/std/language.support/support.limits/limits/numeric.limits.members/max.pass.cpp
+++ b/libcxx/test/std/language.support/support.limits/limits/numeric.limits.members/max.pass.cpp
@@ -65,5 +65,26 @@ int main(int, char**)
     test<double>(DBL_MAX);
     test<long double>(LDBL_MAX);
 
-  return 0;
+    // _BitInt(N): max is 2^N - 1 for unsigned and 2^(N-1) - 1 for signed.
+    // Exercises the digits fix through `__max = ~0 ^ __min`.
+#if TEST_HAS_EXTENSION(bit_int)
+    test<unsigned _BitInt(8)>((unsigned _BitInt(8)) ~(unsigned _BitInt(8))0);
+    test<signed _BitInt(8)>((signed _BitInt(8))0x7F);
+    test<unsigned _BitInt(13)>((unsigned _BitInt(13))0x1FFF);
+    test<signed _BitInt(13)>((signed _BitInt(13))0x0FFF);
+    test<unsigned _BitInt(64)>((unsigned _BitInt(64)) ~(unsigned _BitInt(64))0);
+    test<signed _BitInt(64)>((signed _BitInt(64))0x7FFFFFFFFFFFFFFFLL);
+#  if __BITINT_MAXWIDTH__ >= 128
+    test<unsigned _BitInt(77)>((unsigned _BitInt(77)) ~(unsigned _BitInt(77))0);
+    test<signed _BitInt(77)>((signed _BitInt(77)) ~((signed _BitInt(77))1 << 76));
+    test<unsigned _BitInt(128)>((unsigned _BitInt(128)) ~(unsigned _BitInt(128))0);
+    test<signed _BitInt(128)>((signed _BitInt(128)) ~((signed _BitInt(128))1 << 127));
+#  endif
+#  if __BITINT_MAXWIDTH__ >= 256
+    test<unsigned _BitInt(256)>((unsigned _BitInt(256)) ~(unsigned _BitInt(256))0);
+    test<signed _BitInt(256)>((signed _BitInt(256)) ~((signed _BitInt(256))1 << 255));
+#  endif
+#endif
+
+    return 0;
 }
diff --git a/libcxx/test/std/language.support/support.limits/limits/numeric.limits.members/min.pass.cpp b/libcxx/test/std/language.support/support.limits/limits/numeric.limits.members/min.pass.cpp
index 09877362447bc..a9c72da2103b4 100644
--- a/libcxx/test/std/language.support/support.limits/limits/numeric.limits.members/min.pass.cpp
+++ b/libcxx/test/std/language.support/support.limits/limits/numeric.limits.members/min.pass.cpp
@@ -65,5 +65,27 @@ int main(int, char**)
     test<double>(DBL_MIN);
     test<long double>(LDBL_MIN);
 
-  return 0;
+    // _BitInt(N): min is 0 for unsigned and -2^(N-1) for signed. The shift
+    // `1 << digits` flowed through the buggy digits field, so this also
+    // exercises the digits fix for non-byte-aligned widths.
+#if TEST_HAS_EXTENSION(bit_int)
+    test<unsigned _BitInt(8)>(0);
+    test<signed _BitInt(8)>(-(signed _BitInt(8))(1 << 7));
+    test<unsigned _BitInt(13)>(0);
+    test<signed _BitInt(13)>(-(signed _BitInt(13))(1 << 12));
+    test<unsigned _BitInt(64)>(0);
+    test<signed _BitInt(64)>(-(signed _BitInt(64))(1ULL << 63));
+#  if __BITINT_MAXWIDTH__ >= 128
+    test<unsigned _BitInt(77)>(0);
+    test<signed _BitInt(77)>(-((signed _BitInt(77))1 << 76));
+    test<unsigned _BitInt(128)>(0);
+    test<signed _BitInt(128)>(-((signed _BitInt(128))1 << 127));
+#  endif
+#  if __BITINT_MAXWIDTH__ >= 256
+    test<unsigned _BitInt(256)>(0);
+    test<signed _BitInt(256)>(-((signed _BitInt(256))1 << 255));
+#  endif
+#endif
+
+    return 0;
 }



More information about the libcxx-commits mailing list