[libcxx-commits] [libcxx] [libc++][format] Fix the precision of the alternate form of g for values below 1 (PR #230004)

via libcxx-commits libcxx-commits at lists.llvm.org
Wed Oct 7 23:53:45 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-libcxx

Author: Michiel van Slobbe (mvanslobbe)

<details>
<summary>Changes</summary>

For the `g`/`G` presentation types with the `#` option, [format.string.std] requires that trailing zeros are not removed, i.e. the output must match `printf("%#.*g")`. For the `f` style, C17 7.21.6.1 gives the precision as `P - 1 - X`, where `X` is the exponent the `e` style conversion would have.

The formatter computed `X` as the number of integral digits minus one. That is only correct for `|value| >= 1` (and zero). For `0 < |value| < 1` the integral digit is `0` and `X` is negative, so too few digits were produced:

```cpp
#include <format>
#include <print>

int main() {
  std::println("{:#g}", 0.5);           // expected 0.500000
  std::println("{:#.6g}", 0.0012);      // expected 0.00120000
  std::println("{:#.17g}", 0.09765625); // expected 0.097656250000000000
}
```

| | `{:#g}` 0.5 | `{:#.6g}` 0.0012 | `{:#.17g}` 0.09765625 |
|---|---|---|---|
| printf `%#g` / libstdc++ | `0.500000` | `0.00120000` | `0.097656250000000000` |
| libc++ before | `0.50000` | `0.00120` | `0.0976562500000000` |
| libc++ after | `0.500000` | `0.00120000` | `0.097656250000000000` |

This has been present since LLVM 16 (f92c733cc2, which added keeping the trailing zeros for the alternate form).

The fix derives `X` from the first nonzero digit after the radix point when the integral digit is `0`. This uses the rendered digits, so `X` is taken after rounding, as C requires (e.g. `{:#.2g}` of `0.0999999` is `0.10`, of `0.99999999` is `1.0`).

Tests are added for `g`, `G` and the locale-specific form (`{:#Lg}`). The existing alternate-form tests only covered `0` and values `>= 1`.

Found by differential fuzzing `std::format` against `snprintf`. A sweep of float and double, precisions 0 to 1100, values between `denorm_min` and 1, `-0.0`, `G`, `wchar_t` and `L` had 97/280 mismatches before this change and 0 after.

This change was found with a libFuzzer differential harness and fixed with the help of Claude Code. I reviewed and tested the change myself.


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


3 Files Affected:

- (modified) libcxx/include/__format/formatter_floating_point.h (+15-4) 
- (modified) libcxx/test/std/utilities/format/format.functions/format_tests.h (+24) 
- (modified) libcxx/test/std/utilities/format/format.functions/locale-specific_form.pass.cpp (+11) 


``````````diff
diff --git a/libcxx/include/__format/formatter_floating_point.h b/libcxx/include/__format/formatter_floating_point.h
index 44e352a9687cc..737161b12a673 100644
--- a/libcxx/include/__format/formatter_floating_point.h
+++ b/libcxx/include/__format/formatter_floating_point.h
@@ -693,11 +693,22 @@ __format_floating_point(_Tp __value, _FormatContext& __ctx, __format_spec::__par
       // specified, or 1 if the precision is 0. Then, if a conversion with
       // style E would have an exponent of X:
       int __p = std::max<int>(1, (__specs.__has_precision() ? __specs.__precision_ : 6));
-      if (__result.__exponent == __result.__last)
+      if (__result.__exponent == __result.__last) {
         // if P > X >= -4, the conversion is with style f or F and precision P - 1 - X.
-        // By including the radix point it calculates P - (1 + X)
-        __p -= __result.__radix_point - __result.__integral;
-      else
+        //
+        // X is the exponent of the first significant digit. For |value| >= 1, and for zero, that is the number of
+        // integral digits minus one. For 0 < |value| < 1 the integral digit is 0 and X is negative: the first
+        // significant digit is the first nonzero digit after the radix point.
+        ptrdiff_t __x = __result.__radix_point - __result.__integral - 1;
+        if (*__result.__integral == '0') {
+          const char* __it = __result.__radix_point + 1;
+          while (__it != __result.__last && *__it == '0')
+            ++__it;
+          if (__it != __result.__last) // 0 < |value| < 1
+            __x = -(__it - __result.__radix_point);
+        }
+        __p -= 1 + __x;
+      } else
         // otherwise, the conversion is with style e or E and precision P - 1.
         --__p;
 
diff --git a/libcxx/test/std/utilities/format/format.functions/format_tests.h b/libcxx/test/std/utilities/format/format.functions/format_tests.h
index 4cba83afd429c..cbfc9cffec503 100644
--- a/libcxx/test/std/utilities/format/format.functions/format_tests.h
+++ b/libcxx/test/std/utilities/format/format.functions/format_tests.h
@@ -2414,6 +2414,22 @@ void format_test_floating_point_general_lower_case(TestFunction check) {
   check(SV("answer is '1.2000'"), SV("answer is '{:#.{}g}'"), 1.2, 5);
   check(SV("answer is '1.20000'"), SV("answer is '{:#.{}g}'"), 1.2, 6);
 
+  // For 0 < |value| < 1 the leading zeros are not significant digits.
+  check(SV("answer is '0.500000'"), SV("answer is '{:#g}'"), 0.5);
+  check(SV("answer is '0.5'"), SV("answer is '{:#.{}g}'"), 0.5, 0);
+  check(SV("answer is '0.5'"), SV("answer is '{:#.{}g}'"), 0.5, 1);
+  check(SV("answer is '0.50'"), SV("answer is '{:#.{}g}'"), 0.5, 2);
+  check(SV("answer is '0.500000'"), SV("answer is '{:#.{}g}'"), 0.5, 6);
+  check(SV("answer is '-0.250000'"), SV("answer is '{:#.{}g}'"), -0.25, 6);
+  check(SV("answer is '0.001'"), SV("answer is '{:#.{}g}'"), 0.0012, 1);
+  check(SV("answer is '0.0012'"), SV("answer is '{:#.{}g}'"), 0.0012, 2);
+  check(SV("answer is '0.00120000'"), SV("answer is '{:#.{}g}'"), 0.0012, 6);
+  check(SV("answer is '0.0001'"), SV("answer is '{:#.{}g}'"), 0.0001, 1);
+  check(SV("answer is '0.000100000'"), SV("answer is '{:#.{}g}'"), 0.0001, 6);
+  // X is determined after rounding.
+  check(SV("answer is '0.10'"), SV("answer is '{:#.{}g}'"), 0.0999999, 2);
+  check(SV("answer is '1.0'"), SV("answer is '{:#.{}g}'"), 0.99999999, 2);
+
   check(SV("answer is '1.e+03'"), SV("answer is '{:#.{}g}'"), 1200.0, 0);
   check(SV("answer is '1.e+03'"), SV("answer is '{:#.{}g}'"), 1200.0, 1);
   check(SV("answer is '1.2e+03'"), SV("answer is '{:#.{}g}'"), 1200.0, 2);
@@ -2567,6 +2583,14 @@ void format_test_floating_point_general_upper_case(TestFunction check) {
   check(SV("answer is '1.2000'"), SV("answer is '{:#.{}G}'"), 1.2, 5);
   check(SV("answer is '1.20000'"), SV("answer is '{:#.{}G}'"), 1.2, 6);
 
+  // For 0 < |value| < 1 the leading zeros are not significant digits.
+  check(SV("answer is '0.500000'"), SV("answer is '{:#G}'"), 0.5);
+  check(SV("answer is '0.50'"), SV("answer is '{:#.{}G}'"), 0.5, 2);
+  check(SV("answer is '-0.250000'"), SV("answer is '{:#.{}G}'"), -0.25, 6);
+  check(SV("answer is '0.00120000'"), SV("answer is '{:#.{}G}'"), 0.0012, 6);
+  check(SV("answer is '0.000100000'"), SV("answer is '{:#.{}G}'"), 0.0001, 6);
+  check(SV("answer is '1.0'"), SV("answer is '{:#.{}G}'"), 0.99999999, 2);
+
   check(SV("answer is '1.E+03'"), SV("answer is '{:#.{}G}'"), 1200.0, 0);
   check(SV("answer is '1.E+03'"), SV("answer is '{:#.{}G}'"), 1200.0, 1);
   check(SV("answer is '1.2E+03'"), SV("answer is '{:#.{}G}'"), 1200.0, 2);
diff --git a/libcxx/test/std/utilities/format/format.functions/locale-specific_form.pass.cpp b/libcxx/test/std/utilities/format/format.functions/locale-specific_form.pass.cpp
index e573f8d47dc99..c6ca1f4ccc6e9 100644
--- a/libcxx/test/std/utilities/format/format.functions/locale-specific_form.pass.cpp
+++ b/libcxx/test/std/utilities/format/format.functions/locale-specific_form.pass.cpp
@@ -1731,6 +1731,17 @@ void test_floating_point_general_lower_case() {
   test(SV("$$$-1_23_4#57"), loc, SV("{:$>13.6Lg}"), F(-1.234567e3));
   test(SV("$-1_23_4#57$$"), loc, SV("{:$^13.6Lg}"), F(-1.234567e3));
   test(SV("-0001_23_4#57"), loc, SV("{:013.6Lg}"), F(-1.234567e3));
+
+  // *** alternate form: for 0 < |value| < 1 the leading zeros are not significant digits ***
+  std::locale::global(en_US);
+  test(SV("0.500000"), SV("{:#Lg}"), F(0.5));
+  test(SV("-0.250000"), SV("{:#.6Lg}"), F(-0.25));
+  test(SV("0.500000"), en_US, SV("{:#Lg}"), F(0.5));
+
+  std::locale::global(loc);
+  test(SV("0#500000"), SV("{:#Lg}"), F(0.5));
+  test(SV("-0#250000"), SV("{:#.6Lg}"), F(-0.25));
+  test(SV("0#500000"), loc, SV("{:#Lg}"), F(0.5));
 }
 
 template <class F, class CharT>

``````````

</details>


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


More information about the libcxx-commits mailing list