From a16a587ea8641b3d3279d96478d1eb0931bb94dd Mon Sep 17 00:00:00 2001 From: linhongyu Date: Thu, 1 Oct 2026 17:05:12 +0800 Subject: [PATCH] fix(dom): mark mantissa truncated on the slow path with an exponent When parsing a number whose integer part is longer than 19 digits on the slow path, digits beyond the first 19 are dropped from 'man' and counted into 'exp10', but 'trunc' was never set. Without an explicit exponent the code falls through the Integer section which sets 'trunc' before the fast path, but with an explicit exponent it jumps straight to 'double_exp' and then to ParseFloatingNormalFast, which requires an exact significand. The truncated 19-digit mantissa is then treated as exact, producing a result that is 1 ULP below the correctly rounded value. Setting 'trunc' on the slow path makes the Eisel-Lemire consistency check kick in and fall back to the native parser when the truncated mantissa is not exactly representable, matching correctly-rounded strtod output. Adds a regression test with bit-exact expectations (EXPECT_DOUBLE_EQ tolerates a 4-ULP difference and would not catch this). --- include/sonic/dom/parser.h | 1 + tests/parsenumber_test.cpp | 44 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/include/sonic/dom/parser.h b/include/sonic/dom/parser.h index 3521e6c2..575b1424 100644 --- a/include/sonic/dom/parser.h +++ b/include/sonic/dom/parser.h @@ -353,6 +353,7 @@ class Parser { man_nd++; } else { exp10++; + trunc = 1; // digits beyond the 19 kept in man are dropped } i++; } diff --git a/tests/parsenumber_test.cpp b/tests/parsenumber_test.cpp index 614486c9..dceec07e 100644 --- a/tests/parsenumber_test.cpp +++ b/tests/parsenumber_test.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -299,6 +300,49 @@ TEST(ParserTest, RoundUp) { TestParseDouble(1.0905441441816094e+30, "1090544144181609348835077142190"); } +void TestParseDoubleBits(double num, const std::string& input) { + { + Document doc; + doc.Parse(input.data(), input.size()); + EXPECT_FALSE(doc.HasParseError()) << input; + EXPECT_TRUE(doc.IsDouble()) << input; + uint64_t expect_bits, actual_bits; + static_assert(sizeof(double) == sizeof(uint64_t)); + memcpy(&expect_bits, &num, sizeof(double)); + double a = doc.GetDouble(); + memcpy(&actual_bits, &a, sizeof(double)); + EXPECT_EQ(expect_bits, actual_bits) << input; + } + { + uint64_t expect_bits, actual_bits; + memcpy(&expect_bits, &num, sizeof(double)); + double a = internal::AtofNative(input.data(), input.size()); + memcpy(&actual_bits, &a, sizeof(double)); + EXPECT_EQ(expect_bits, actual_bits) << input; + } +} + +TEST(ParserTest, LongMantissaWithExponent) { + // Integers longer than 19 digits are truncated to 19 digits on the slow + // path. When an explicit exponent follows, the truncated mantissa must not + // be treated as exact by the fast path; otherwise the result is off by + // 1 ULP. EXPECT_DOUBLE_EQ tolerates a 4-ULP difference, so compare bits. + TestParseDoubleBits(-3.2707769411490447e+19, "-32707769411490445312e0"); + TestParseDoubleBits(1.9794616963580959e+20, "19794616963580957495e+1"); + TestParseDoubleBits(2.6453884816999669e+36, + "264538848169996678698051993472e+7"); + TestParseDoubleBits( + 8.3368019727581649e+58, + "833680197275816431386138346101649404102522117776285986458397e-1"); + TestParseDoubleBits(-2.637588794821583e+26, "-26375887948215827999E+7"); + TestParseDoubleBits( + 1.2147161201801101e+61, + "121471612018011001563790188522981306789313383875185792679411e+2"); + TestParseDoubleBits( + -5.0707170799712052e-250, + "-507071707997120489163576838330990277669096644300656345458938E-309"); +} + TEST(ParserTest, ParseInvalidNumber) { TestParseInf(7, "1e+9999"); TestParseInf(26, "-1234567890123456789e+9999");