From 6835acd5ce0cd62cc052be92f8613cac83f73f3a Mon Sep 17 00:00:00 2001 From: Ramya Eliger Date: Tue, 11 Aug 2026 23:27:43 +0530 Subject: [PATCH] fix double rounding of hex-floats in StringToFloat Repro: StringToFloat("0x8a4.d047p-140") returns 1.587615108e-39 where strtof and the exact value round to the neighbouring float 1.587616509e-39. Hex-floats landing in the subnormal range, or whose exact significand exceeds 53 bits (e.g. "0x100000100000008p0"), can be a ULP out. Cause: RadixStringToIeee rounds the significand to single precision for the float path, and StringToFloat then casts the resulting double to float, so the value is rounded twice. The decimal path already avoids this double rounding; the hex-float path did not. Fix: keep the full double significand for hex-floats and round it to odd, leaving the single rounding to the caller's cast. Regression tests for the subnormal and >53-bit cases sit beside the existing hex-float tests. --- double-conversion/string-to-double.cc | 43 +++++++++++++++++++-------- test/cctest/test-conversions.cc | 23 ++++++++++++++ 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/double-conversion/string-to-double.cc b/double-conversion/string-to-double.cc index ac3f48a..af5c352 100644 --- a/double-conversion/string-to-double.cc +++ b/double-conversion/string-to-double.cc @@ -287,7 +287,14 @@ static double RadixStringToIeee(Iterator* current, const int kDoubleSize = Double::kSignificandSize; const int kSingleSize = Single::kSignificandSize; - const int kSignificandSize = read_as_double? kDoubleSize: kSingleSize; + // A hex-float is formed here as a double and rounded to float by the caller + // (StringToFloat casts the result). Rounding the significand to single + // precision here would double-round both subnormal floats and floats whose + // exact significand exceeds 53 bits, so keep the full double significand and + // round it to odd, which makes that final single-precision cast correct. + const bool round_hex_float_to_single = parse_as_hex_float && !read_as_double; + const int kSignificandSize = + (read_as_double || parse_as_hex_float) ? kDoubleSize : kSingleSize; *result_is_junk = true; @@ -375,21 +382,31 @@ static double RadixStringToIeee(Iterator* current, return junk_string_value; } - int middle_value = (1 << (overflow_bits_count - 1)); - if (dropped_bits > middle_value) { - number++; // Rounding up. - } else if (dropped_bits == middle_value) { - // Rounding to even to consistency with decimals: half-way case rounds - // up if significant part is odd and down otherwise. - if ((number & 1) != 0 || !zero_tail) { + if (round_hex_float_to_single) { + // Round the significand to odd: set the lowest kept bit whenever any + // bit was dropped. The caller rounds this double to float; rounding to + // nearest here would double-round, but round-to-odd leaves that final + // single rounding correct for normal and subnormal results alike. + if (dropped_bits != 0 || !zero_tail) { + number |= 1; + } + } else { + int middle_value = (1 << (overflow_bits_count - 1)); + if (dropped_bits > middle_value) { number++; // Rounding up. + } else if (dropped_bits == middle_value) { + // Rounding to even to consistency with decimals: half-way case rounds + // up if significant part is odd and down otherwise. + if ((number & 1) != 0 || !zero_tail) { + number++; // Rounding up. + } } - } - // Rounding up may cause overflow. - if ((number & ((int64_t)1 << kSignificandSize)) != 0) { - exponent++; - number >>= 1; + // Rounding up may cause overflow. + if ((number & ((int64_t)1 << kSignificandSize)) != 0) { + exponent++; + number >>= 1; + } } break; } diff --git a/test/cctest/test-conversions.cc b/test/cctest/test-conversions.cc index 9280f45..e14b4b7 100644 --- a/test/cctest/test-conversions.cc +++ b/test/cctest/test-conversions.cc @@ -5364,6 +5364,29 @@ TEST(StringToFloatHexString) { CHECK_EQ(0.0f, StrToF("0x1.p-10000000000000000", flags, 0.0, &processed, &all_used)); CHECK(all_used); + + // Hex-floats must be rounded straight to float, without an intermediate + // rounding to a double significand. Subnormal results carry fewer than 24 + // significand bits, so the value below rounds up when taken directly to + // float but rounds down if first rounded to a 24-bit normal significand. + CHECK_EQ(Single(0x001149a1u).value(), + StrToF("0x8a4.d047p-140", flags, 0.0, &processed, &all_used)); + CHECK(all_used); + + CHECK_EQ(Single(0x002f3e11u).value(), + StrToF("0x2f3.e10ap-137", flags, 0.0, &processed, &all_used)); + CHECK(all_used); + + // The double-rounding examples from the hex-integer cases above, written as + // hex-floats. Rounding the significand to a double first would land on + // 72057594037927936.0f; the direct single rounding gives the value below. + CHECK_EQ(72057602627862528.0f, + StrToF("0x100000100000008p0", flags, 0.0, &processed, &all_used)); + CHECK(all_used); + + CHECK_EQ(72057602627862528.0f, + StrToF("0x1000002FFFFFFF8p0", flags, 0.0, &processed, &all_used)); + CHECK(all_used); }