diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index a694e60486..89b4e1dede 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -287,28 +287,6 @@ public: void doDropDigit(T& mantissa, int& exponent) noexcept; - enum class Round { - // The result is exact. No rounding is needed. Only used if cuspRoundingFix is Enabled330 or - // higher. - Exact = -2, - // Round down. Since we use integer math, that usually means no change is needed. - // Exceptions are for when the result is between kMaxRep and kMaxRepUp (round to kMaxRep), - // or after subtraction where _any_ remainder will modify the result. The latter is what - // distinguishes Exact from Down. - Down = -1, - // The result was exactly half-way between two integers. This will round to even. - Even = 0, - // Round up. Always adds 1 (or subtracts 1 in some cases if cuspRoundingFix is not - // Enabled) - Up = 1, - }; - - // Indicate round direction: 1 is up, -1 is down, 0 is even - // This enables the client to round towards nearest, and on - // tie, round towards even. - [[nodiscard]] Round - round() const noexcept; - // Modify the result to the correctly rounded value template void @@ -321,9 +299,35 @@ public: // Modify the result to the correctly rounded value void - doRound(rep& drops, std::string location) const; + doRound(rep& drops, std::string location); private: + template + void + pushOverflow(T mantissa); + + enum class Round { + // The result is exact. No rounding is needed. Only used if cuspRoundingFix is Enabled330 or + // higher. + Exact = -2, + // Round down. Since we use integer math, that usually means no change is needed. + // Exceptions are for when the result is between kMaxRep and kMaxRepUp (round to kMaxRep), + // or after subtraction where _any_ remainder will modify the result. The latter is what + // distinguishes Exact from Down. + Down = -1, + // The result was exactly half-way between two integers. This will round to even. + Even = 0, + // Round up. Always adds 1 (or subtracts 1 in some cases if cuspRoundingFix is not + // Enabled330) + Up = 1, + }; + + // Indicate round direction. See Round enum above. + // This enables the client to round towards nearest, and on + // tie, round towards even. + [[nodiscard]] Round + round() const noexcept; + void doPush(unsigned d) noexcept; @@ -406,10 +410,78 @@ Number::Guard::doDropDigit(uint128_t& mantissa, int& exponent) noexce ++exponent; } +template +void +Number::Guard::pushOverflow(T mantissa) +{ + XRPL_ASSERT(mantissa <= kMaxRepUp, "xrpl::Number::Guard::pushOverflow : valid mantissa"); + if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && mantissa >= kMaxRep && + mantissa < kMaxRepUp) + { + // Special case rounding rules for the values in the range [kMaxRep, kMaxRepUp). + + auto constexpr spread = kMaxRepUp - kMaxRep; + static_assert(spread == 3); + + // Round in two steps. + + // The first step uses the digits _already_ in the Guard to round the + // intermediate mantissa, using only the last digit. Then update the mantissa for the + // second step. Ultimately, the purpose of this step is to capture rounding where the stored + // digits would change the decision without those digits. (e.g. From just _below_ the + // midpoint to just _above_ the midpoint for ToNearest, or from kMaxRep into the in-between + // for Upward. + // Make an exception if the final digit is 9, because it can only get larger, and we want to + // stay in single digits. + if (auto finalDigit = mantissa % 10; finalDigit < 9) + { + // Intentionally use integer math to get the largest value under the midpoint. + auto constexpr kMidpoint = kMaxRep + (spread / 2); + static_assert(kMidpoint == kMaxRep + 1); + auto const r = round(); + if (r == Round::Up || (r == Round::Even && mantissa == kMidpoint)) + { + ++mantissa; + } + } + + if (mantissa == kMaxRep) + { + // If the mantissa ends up exactly kMaxRep, there's nothing more to do. + return; + } + + // The second step scales the final digit of the update mantissa proportionally from kMaxRep + // and kMaxRepUp to 1 to 9. It then pushes that scaled digit onto the guard as if it was a + // digit that got removed, but don't actually remove it. This method should be is + // future-proof in case the number of mantissa bits ever changes. (Though for integer values + // that are a power of two themselves, the spread will always be the same.) Effects: + // * For round to nearest + // * if the updated mantissa is below the midpoint, it'll round "down" to kMaxRep + // * if above the midpoint, it'll round "up" to kMaxRepUp + // * it can never be exactly at the midpoint, because kMaxRepUp is always even, and + // kMaxRep is always odd, so don't worry about that case. + // * For round upward, will round up to kMaxRepUp for positive values, down to kMaxRep for + // negative. + // * For round downward, does the opposite of upward. + // * For round toward zero, always rounds down to kMaxRep. + + auto const diff = mantissa - kMaxRep; + auto digit = (diff * 10) / spread; + XRPL_ASSERT( + digit > 0 && digit < 10 && digit != 5, + "xrpl::Number::Guard::pushOverflow : valid overflow digit"); + + // Don't remove the digit from the mantissa, but add it to the guard as if it was. + push(digit); + } +} + // Returns: -// -1 if Guard is less than half -// 0 if Guard is exactly half -// 1 if Guard is greater than half +// Exact if Guard is _zero_, and appropriate amendments are enabled +// Down if Guard is less than half +// Even if Guard is exactly half +// Up if Guard is greater than half Number::Guard::Round Number::Guard::round() const noexcept { @@ -458,13 +530,16 @@ void Number::Guard::bringIntoRange(bool& negative, T& mantissa, int& exponent) { // Bring mantissa back into the minMantissa / maxMantissa range AFTER - // rounding + // rounding. Mantissa should never be 0. + XRPL_ASSERT(mantissa != 0, "xrpl::Number::Guard::bringIntoRange : valid mantissa"); if (mantissa < minMantissa) { mantissa *= 10; --exponent; } - if (exponent < kMinExponent) + // mantissa should never be 0, but if it _is_ make the result kZero. + if (exponent < kMinExponent || + (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && mantissa == 0)) { static constexpr Number kZero = Number{}; @@ -478,7 +553,9 @@ template void Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string location) { - auto r = round(); + pushOverflow(mantissa); + + auto const r = round(); if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1)) { auto const safeToIncrement = [this](auto const& mantissa) { @@ -495,18 +572,27 @@ Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string } else { - // Incrementing the mantissa will require dividing, which will require rounding. So - // _don't_ increment the mantissa. Instead, divide and round recursively. It should - // be impossible to recurse more than once, because once the mantissa is divided by - // 10, it will be _well_ under maxMantissa and kMaxRep, so adding 1 will have no - // chance of bringing it back over. - doDropDigit(mantissa, exponent); - XRPL_ASSERT_PARTS( - safeToIncrement(mantissa), - "xrpl::Number::Guard::doRoundUp", - "can't recurse more than once"); - doRoundUp(negative, mantissa, exponent, location); - return; + if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && + mantissa > kMaxRep && mantissa < kMaxRepUp) + { + // + mantissa = kMaxRepUp; + } + else + { + // Incrementing the mantissa will require dividing, which will require rounding. + // So _don't_ increment the mantissa. Instead, divide and round recursively. It + // should be impossible to recurse more than once, because once the mantissa is + // divided by 10, it will be _well_ under maxMantissa and kMaxRep, so adding 1 + // will have no chance of bringing it back over. + doDropDigit(mantissa, exponent); + XRPL_ASSERT_PARTS( + safeToIncrement(mantissa), + "xrpl::Number::Guard::doRoundUp", + "can't recurse more than once"); + doRoundUp(negative, mantissa, exponent, location); + return; + } } } else @@ -524,6 +610,12 @@ Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string } } } + else if ( + cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && mantissa > kMaxRep && + mantissa < kMaxRepUp) + { + mantissa = kMaxRep; + } bringIntoRange(negative, mantissa, exponent); if (exponent > kMaxExponent) Throw(std::string(location)); @@ -533,6 +625,8 @@ template void Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) { + // Do not pushOverflow here. + auto r = round(); if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330) { @@ -565,7 +659,7 @@ Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) // Modify the result to the correctly rounded value void -Number::Guard::doRound(rep& drops, std::string location) const +Number::Guard::doRound(rep& drops, std::string location) { auto r = round(); if (r == Round::Up || (r == Round::Even && (drops & 1) == 1)) @@ -583,6 +677,8 @@ Number::Guard::doRound(rep& drops, std::string location) const } ++drops; } + XRPL_ASSERT(drops >= 0, "xrpl::Number::Guard::doRound : positive magnitude"); + if (isNegative()) drops = -drops; } @@ -632,7 +728,9 @@ doNormalize( { static constexpr auto kMinExponent = Number::kMinExponent; static constexpr auto kMaxExponent = Number::kMaxExponent; - static constexpr auto kMaxRep = Number::kMaxRep; + auto const repLimit = cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 + ? Number::kMaxRepUp + : Number::kMaxRep; using Guard = Number::Guard; @@ -682,17 +780,17 @@ doNormalize( // 9,900,000,000,000,123,450 or 9,900,000,000,000,123,460. // mantissa() will return mantissa / 10, and exponent() will return // exponent + 1. - if (m > kMaxRep) + if (m > repLimit) { if (exponent >= kMaxExponent) throw std::overflow_error("Number::normalize 1.5"); g.doDropDigit(m, exponent); } // Before modification, m should be within the min/max range. After - // modification, it must be less than kMaxRep. In other words, the original - // value should have been no more than kMaxRep * 10. - // (kMaxRep * 10 > maxMantissa) - XRPL_ASSERT_PARTS(m <= kMaxRep, "xrpl::doNormalize", "intermediate mantissa fits in int64"); + // modification, it must be less than repLimit. In other words, the original + // value should have been no more than repLimit * 10. + // (repLimit * 10 > maxMantissa) + XRPL_ASSERT_PARTS(m <= repLimit, "xrpl::doNormalize", "intermediate mantissa fits in limit"); mantissa = m; g.doRoundUp(negative, mantissa, exponent, "Number::normalize 2"); @@ -824,6 +922,9 @@ Number::operator+=(Number const& y) auto const& maxMantissa = g.maxMantissa; auto const cuspRoundingFix = g.cuspRoundingFix; + auto const repLimit = + cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 ? kMaxRepUp : kMaxRep; + // Bring the exponents of both values into agreement, so the mantissas are on the same scale // and can be added directly together. @@ -908,7 +1009,7 @@ Number::operator+=(Number const& y) } else { - if (xm > maxMantissa || xm > kMaxRep) + if (xm > maxMantissa || xm > repLimit) { g.doDropDigit(xm, xe); } @@ -952,7 +1053,7 @@ Number::operator+=(Number const& y) { // Grow xm/xe and pull digits out of the Guard until it's back in the // minMantissa/maxMantissa range. - while (xm < minMantissa && xm * 10 <= kMaxRep) + while (xm < minMantissa && xm * 10 <= repLimit) { xm *= 10; xm -= g.pop(); @@ -1026,8 +1127,10 @@ Number::operator*=(Number const& y) g.setNegative(); auto const& maxMantissa = g.maxMantissa; + auto const repLimit = + g.cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 ? kMaxRepUp : kMaxRep; - while (zm > maxMantissa || zm > kMaxRep) + while (zm > maxMantissa || zm > repLimit) { g.doDropDigit(zm, ze); }