diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 1f2c41809a..eb3e21dbf1 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -260,6 +260,11 @@ public: unsigned pop() noexcept; + // if true, there are no recoverable digits in the guard, though there may be dropped digits + // (xbit_) + [[nodiscard]] bool + unrecoverable() const noexcept; + // if true, there are no digits in the guard, including dropped digits (xbit_) [[nodiscard]] bool empty() const noexcept; @@ -277,6 +282,17 @@ public: void doDropDigit(T& mantissa, int& exponent) noexcept; + /** + * Drop a digit from the mantissa, and increment the exponent, storing the dropped digit in + * this Guard. + * + * If a drop will not do anything meaningful (there are no recoverable digits in the guard, and + * the mantissa is 0), and if targetExponent > exponent, simply set exponent to targetExponent. + */ + template + void + doDropDigitWithTarget(T& mantissa, int& exponent, int const targetExponent) noexcept; + // Modify the result to the correctly rounded value template void @@ -374,10 +390,16 @@ Number::Guard::pop() noexcept return d; } +inline bool +Number::Guard::unrecoverable() const noexcept +{ + return digits_ == 0; +} + inline bool Number::Guard::empty() const noexcept { - return digits_ == 0 && !xbit_; + return unrecoverable() && !xbit_; } template @@ -401,6 +423,22 @@ Number::Guard::doDropDigit(uint128_t& mantissa, int& exponent) noexce ++exponent; } +template +void +Number::Guard::doDropDigitWithTarget(T& mantissa, int& exponent, int const targetExponent) noexcept +{ + XRPL_ASSERT( + targetExponent > exponent, "Number::Guard::doDropDigitWithTarget : something to do"); + if (mantissa == 0 && unrecoverable() && targetExponent >= exponent) + { + // No number of dropped digits is going to change any of the operative parameters at this + // point + exponent = targetExponent; + return; + } + doDropDigit(mantissa, exponent); +} + template void Number::Guard::pushOverflow(T mantissa) @@ -935,6 +973,8 @@ Number::operator+=(Number const& y) // 1. First, shrink the mantissa of shrinkM/shrinkE while shrinkM ends in 0. while (shrinkE < expandE && shrinkM % 10 == 0) { + // Don't use doDropDigitWithTarget here, because the loop will stop before the + // mantissa gets to 0. g.doDropDigit(shrinkM, shrinkE); } @@ -952,7 +992,7 @@ Number::operator+=(Number const& y) // digits will be put into the Guard. This is the only step for non-Enabled330 modes. while (shrinkE < expandE) { - g.doDropDigit(shrinkM, shrinkE); + g.doDropDigitWithTarget(shrinkM, shrinkE, expandE); } }; diff --git a/src/test/protocol/STNumber_test.cpp b/src/test/protocol/STNumber_test.cpp index 74792e0a70..7dd296806d 100644 --- a/src/test/protocol/STNumber_test.cpp +++ b/src/test/protocol/STNumber_test.cpp @@ -176,61 +176,30 @@ struct STNumber_test : public beast::unit_test::Suite numberFromJson(sfNumber, std::to_string(kUMax)) == STNumber(sfNumber, Number(kUMax, 0))); + auto const expectJsonThrows = [this]( + json::Value const& num, std::string const& expected) { + try + { + numberFromJson(sfNumber, num); + fail(); + } + catch (std::exception const& e) + { + BEAST_EXPECT(std::string(e.what()) == expected); + } + }; + + // Obvious overflows tested here + expectJsonThrows("1e2000000", "Number::normalize 2"); + expectJsonThrows("1e2000000000", "Number::normalize 2"); + expectJsonThrows("1e2000000000000", "Number::normalize 2"); + // Obvious non-numbers tested here - try - { - auto _ = numberFromJson(sfNumber, ""); - BEAST_EXPECT(false); - } - catch (std::runtime_error const& e) - { - std::string const expected = "'' is not a number"; - BEAST_EXPECT(e.what() == expected); - } - - try - { - auto _ = numberFromJson(sfNumber, "e"); - BEAST_EXPECT(false); - } - catch (std::runtime_error const& e) - { - std::string const expected = "'e' is not a number"; - BEAST_EXPECT(e.what() == expected); - } - - try - { - auto _ = numberFromJson(sfNumber, "1e"); - BEAST_EXPECT(false); - } - catch (std::runtime_error const& e) - { - std::string const expected = "'1e' is not a number"; - BEAST_EXPECT(e.what() == expected); - } - - try - { - auto _ = numberFromJson(sfNumber, "e2"); - BEAST_EXPECT(false); - } - catch (std::runtime_error const& e) - { - std::string const expected = "'e2' is not a number"; - BEAST_EXPECT(e.what() == expected); - } - - try - { - auto _ = numberFromJson(sfNumber, json::Value()); - BEAST_EXPECT(false); - } - catch (std::runtime_error const& e) - { - std::string const expected = "not a number"; - BEAST_EXPECT(e.what() == expected); - } + expectJsonThrows("", "'' is not a number"); + expectJsonThrows("e", "'e' is not a number"); + expectJsonThrows("1e", "'1e' is not a number"); + expectJsonThrows("e2", "'e2' is not a number"); + expectJsonThrows(json::Value(), "not a number"); try { diff --git a/src/tests/libxrpl/basics/Number.cpp b/src/tests/libxrpl/basics/Number.cpp index 36e1b4a700..946edf56e3 100644 --- a/src/tests/libxrpl/basics/Number.cpp +++ b/src/tests/libxrpl/basics/Number.cpp @@ -183,6 +183,17 @@ TEST(NumberTest, limits) } EXPECT_TRUE(caught); + try + { + Number{1, 2000000, Number::Normalized{}}; + ADD_FAILURE(); + } + catch (std::overflow_error const& e) + { + std::string const expected = "Number::normalize 2"; + EXPECT_EQ(e.what(), expected) << e.what(); + } + if (scale == MantissaRange::MantissaScale::Large330) { // Normalization with the other scales, including the older large mantissa scales, will @@ -403,6 +414,37 @@ TEST(NumberTest, add) } EXPECT_TRUE(caught); } + + // Special case: Exponents at each end of the allowable range + for (auto const round : + {Number::RoundingMode::ToNearest, + Number::RoundingMode::TowardsZero, + Number::RoundingMode::Downward, + Number::RoundingMode::Upward}) + { + NumberRoundModeGuard const rg{round}; + auto const x = + Number{Number::minMantissa(), Number::kMaxExponent, Number::Normalized{}}; + auto const y = + Number{Number::minMantissa(), Number::kMinExponent, Number::Normalized{}}; + EXPECT_EQ(x.exponent(), Number::kMaxExponent); + EXPECT_NE(x, beast::kZero); + EXPECT_EQ(y.exponent(), Number::kMinExponent); + EXPECT_NE(y, beast::kZero); + auto const result = x + y; + + if (round == Number::RoundingMode::Upward) + { + // Rounding upward will take that little x-bit and round result up to the next + // representable value. + EXPECT_NE(result, x); + EXPECT_EQ(result, (Number{x.mantissa() + 1, x.exponent()})); + } + else + { + EXPECT_EQ(result, x); + } + } } }