[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