|
| 1 | +// Bridge between {fmt}'s float formatting and the _Py_dg_dtoa calling |
| 2 | +// convention used throughout Python/pystrtod.c and Objects/floatobject.c. |
| 3 | +// |
| 4 | +// Exposes a single extern "C" entry point, _Py_fmt_dtoa, with the exact shape |
| 5 | +// of _Py_dg_dtoa (the David Gay dtoa that used to live in Python/dtoa.c) so |
| 6 | +// existing call sites can swap with a one-line change. |
| 7 | +// |
| 8 | +// mode 0 -> shortest round-trip decimal (uses fmt::detail::dragonbox) |
| 9 | +// mode 2 -> max(1, ndigits) significant digits (uses format_float with |
| 10 | +// presentation_type::exp and precision = N - 1) |
| 11 | +// mode 3 -> ndigits digits past the decimal point, ndigits may be negative |
| 12 | +// (uses format_float with presentation_type::fixed) |
| 13 | +// |
| 14 | +// Trailing zeros are stripped so the digit string + decpt shape matches |
| 15 | +// _Py_dg_dtoa byte-for-byte. The "rounded-to-zero-at-target-precision" case |
| 16 | +// for mode 3 is normalised from fmt's "one '0' digit" output to cpython's |
| 17 | +// empty-digit + decpt = -ndigits convention. |
| 18 | +// |
| 19 | +// Memory model: the returned char* is allocated with PyMem_Malloc and must be |
| 20 | +// freed via _Py_fmt_dtoa_free (mirrors _Py_dg_freedtoa). |
| 21 | + |
| 22 | +// Build fmt in header-only mode so we don't need a separate compiled |
| 23 | +// format.cc TU for the Dragonbox tables and the `write_fixed` locale hooks. |
| 24 | +// FMT_OPTIMIZE_SIZE >= 2 disables fmt's std::locale-based locale lookup, |
| 25 | +// which cpython's dtoa never uses anyway and which would otherwise drag |
| 26 | +// libstdc++'s <locale> into libpython. |
| 27 | +#define FMT_HEADER_ONLY 1 |
| 28 | +#define FMT_OPTIMIZE_SIZE 2 |
| 29 | + |
| 30 | +#include "format.h" |
| 31 | +#include "format-inl.h" // brings in out-of-line dragonbox tables; FMT_FUNC |
| 32 | + // resolves to `inline` in header-only mode. |
| 33 | + |
| 34 | +#include <Python.h> |
| 35 | + |
| 36 | +#include <cmath> |
| 37 | +#include <cstdint> |
| 38 | +#include <cstdio> |
| 39 | +#include <cstring> |
| 40 | + |
| 41 | +extern "C" { |
| 42 | + |
| 43 | +void _Py_fmt_dtoa_free(char *s) { |
| 44 | + PyMem_Free(s); |
| 45 | +} |
| 46 | + |
| 47 | +static char *alloc_copy(const char *src, size_t n) { |
| 48 | + char *out = static_cast<char *>(PyMem_Malloc(n + 1)); |
| 49 | + if (!out) return nullptr; |
| 50 | + std::memcpy(out, src, n); |
| 51 | + out[n] = '\0'; |
| 52 | + return out; |
| 53 | +} |
| 54 | + |
| 55 | +char *_Py_fmt_dtoa(double d, int mode, int ndigits, |
| 56 | + int *decpt, int *sign, char **rve) { |
| 57 | + // Sign. _Py_dg_dtoa distinguishes -0.0 from +0.0 via *sign. |
| 58 | + *sign = std::signbit(d) ? 1 : 0; |
| 59 | + double abs_d = *sign ? -d : d; |
| 60 | + |
| 61 | + // Infinity / NaN / zero: cpython conventions, no fmt involvement. |
| 62 | + if (std::isinf(abs_d)) { |
| 63 | + *decpt = 9999; |
| 64 | + char *out = alloc_copy("Infinity", 8); |
| 65 | + if (!out) return nullptr; |
| 66 | + if (rve) *rve = out + 8; |
| 67 | + return out; |
| 68 | + } |
| 69 | + if (std::isnan(abs_d)) { |
| 70 | + *decpt = 9999; |
| 71 | + char *out = alloc_copy("NaN", 3); |
| 72 | + if (!out) return nullptr; |
| 73 | + if (rve) *rve = out + 3; |
| 74 | + return out; |
| 75 | + } |
| 76 | + if (abs_d == 0.0) { |
| 77 | + *decpt = 1; |
| 78 | + char *out = alloc_copy("0", 1); |
| 79 | + if (!out) return nullptr; |
| 80 | + if (rve) *rve = out + 1; |
| 81 | + return out; |
| 82 | + } |
| 83 | + |
| 84 | + // Mode 0 (shortest) uses Dragonbox directly. |
| 85 | + if (mode == 0) { |
| 86 | + auto r = fmt::detail::dragonbox::to_decimal(abs_d); |
| 87 | + char tmp[32]; |
| 88 | + int n = std::snprintf(tmp, sizeof(tmp), "%llu", |
| 89 | + (unsigned long long)r.significand); |
| 90 | + int exp = r.exponent; |
| 91 | + while (n > 1 && tmp[n - 1] == '0') { --n; ++exp; } |
| 92 | + *decpt = exp + n; |
| 93 | + char *out = alloc_copy(tmp, (size_t)n); |
| 94 | + if (!out) return nullptr; |
| 95 | + if (rve) *rve = out + n; |
| 96 | + return out; |
| 97 | + } |
| 98 | + |
| 99 | + // Modes 2 and 3: fmt::detail::format_float writes digits (no dot, no |
| 100 | + // sign, no exponent) into a buffer and returns the decimal exponent. |
| 101 | + fmt::format_specs specs; |
| 102 | + int precision; |
| 103 | + bool fixed; |
| 104 | + if (mode == 2) { |
| 105 | + specs.set_type(fmt::presentation_type::exp); |
| 106 | + // max(1, ndigits) significant digits, fmt wants precision = N - 1. |
| 107 | + precision = (ndigits < 1 ? 1 : ndigits) - 1; |
| 108 | + fixed = false; |
| 109 | + } else { |
| 110 | + // Treat any unrecognised mode as 3 (matches _Py_dg_dtoa's fallthrough). |
| 111 | + specs.set_type(fmt::presentation_type::fixed); |
| 112 | + precision = ndigits; |
| 113 | + fixed = true; |
| 114 | + } |
| 115 | + |
| 116 | + fmt::memory_buffer buf; |
| 117 | + int exp = fmt::detail::format_float(abs_d, precision, specs, |
| 118 | + /*binary32=*/false, buf); |
| 119 | + const char *bd = buf.data(); |
| 120 | + int n = static_cast<int>(buf.size()); |
| 121 | + |
| 122 | + // Normalise fmt's rounded-to-zero output to cpython's "no_digits" shape: |
| 123 | + // fmt may emit a single '0' (e.g. round(5, -1) == 0); cpython returns an |
| 124 | + // empty digit string with decpt = -ndigits. |
| 125 | + if (fixed && n == 1 && bd[0] == '0') { |
| 126 | + char *out = static_cast<char *>(PyMem_Malloc(1)); |
| 127 | + if (!out) return nullptr; |
| 128 | + out[0] = '\0'; |
| 129 | + *decpt = -ndigits; |
| 130 | + if (rve) *rve = out; |
| 131 | + return out; |
| 132 | + } |
| 133 | + // Similarly handle the fixed case where fmt emitted 0 digits (precision |
| 134 | + // was so negative the whole value rounded away). |
| 135 | + if (fixed && n == 0) { |
| 136 | + char *out = static_cast<char *>(PyMem_Malloc(1)); |
| 137 | + if (!out) return nullptr; |
| 138 | + out[0] = '\0'; |
| 139 | + *decpt = -ndigits; |
| 140 | + if (rve) *rve = out; |
| 141 | + return out; |
| 142 | + } |
| 143 | + |
| 144 | + // Strip trailing zeros (cpython mode-2/3 invariant). |
| 145 | + int nz = n; |
| 146 | + while (nz > 1 && bd[nz - 1] == '0') --nz; |
| 147 | + *decpt = exp + n; // decpt uses pre-strip length |
| 148 | + char *out = alloc_copy(bd, (size_t)nz); |
| 149 | + if (!out) return nullptr; |
| 150 | + if (rve) *rve = out + nz; |
| 151 | + return out; |
| 152 | +} |
| 153 | + |
| 154 | +} // extern "C" |
0 commit comments