From b40d2a8e7d1f81ccee6d9fbd9afdcdf4397b732b Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 28 Apr 2026 22:28:14 -0400 Subject: [PATCH 01/77] fix: Fix a rounding error at the Number::maxRep cusp - Add helper function, doDropDigit, to wrap the common pattern: push(mantissa % 10); mantissa /= 10; ++exponent; - Might have been helpful to catch this issue when developing. --- include/xrpl/basics/Number.h | 120 +++++--- include/xrpl/protocol/STAmount.h | 2 +- src/libxrpl/basics/Number.cpp | 347 ++++++++++++++++-------- src/libxrpl/protocol/IOUAmount.cpp | 2 +- src/libxrpl/protocol/Rules.cpp | 10 +- src/libxrpl/protocol/STNumber.cpp | 3 +- src/test/app/AMMClawbackMPT_test.cpp | 2 +- src/test/app/AMMClawback_test.cpp | 4 +- src/test/app/AMMExtended_test.cpp | 105 +++---- src/test/app/AMM_test.cpp | 12 +- src/test/app/Invariants_test.cpp | 206 +++++++------- src/test/basics/IOUAmount_test.cpp | 3 +- src/test/basics/Number_test.cpp | 82 +++++- src/test/jtx/AMMTest.h | 6 +- src/test/jtx/impl/AMMTest.cpp | 2 +- src/test/protocol/STNumber_test.cpp | 3 +- src/test/rpc/GetAggregatePrice_test.cpp | 6 +- 17 files changed, 570 insertions(+), 345 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index cdb9014a87..fb32b42060 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -7,6 +7,7 @@ #include #include #include +#include #include namespace xrpl { @@ -44,9 +45,9 @@ isPowerOfTen(T value) * * min is a power of 10, and * * max = min * 10 - 1. * - * The mantissa_scale enum indicates whether the range is "small" or "large". - * This intentionally restricts the number of MantissaRanges that can be - * instantiated to two: one for each scale. + * The MantissaScale enum indicates properties of the range: size, and some behavioral + * options. This intentionally restricts the number of unique MantissaRanges that can + * be instantiated: one for each scale. * * The "small" scale is based on the behavior of STAmount for IOUs. It has a min * value of 10^15, and a max value of 10^16-1. This was sufficient for @@ -70,18 +71,39 @@ isPowerOfTen(T value) struct MantissaRange { using rep = std::uint64_t; - enum class MantissaScale { Small, Large }; + enum class MantissaScale { + Small, + // LargeLegacy can be removed when fixCleanup3_2_0 is retired + LargeLegacy, + Large, + }; + + // This entire enum can be removed when fixCleanup3_2_0 is retired + enum class CuspRoundingFix : bool { + Disabled = false, + Enabled = true, + }; explicit constexpr MantissaRange(MantissaScale scale) - : min(getMin(scale)), log(logTen(min).value_or(-1)), scale(scale) + : min(getMin(scale)) + , cuspRoundingFixEnabled(isCuspFixEnabled(scale)) + , log(logTen(min).value_or(-1)) + , scale(scale) { } rep min; rep max{(min * 10) - 1}; + CuspRoundingFix cuspRoundingFixEnabled; int log; MantissaScale scale; + static MantissaRange const& + getMantissaRange(MantissaScale scale); + + static std::set const& + getAllScales(); + private: static constexpr rep getMin(MantissaScale scale) @@ -90,6 +112,7 @@ private: { case MantissaScale::Small: return 1'000'000'000'000'000ULL; + case MantissaScale::LargeLegacy: case MantissaScale::Large: return 1'000'000'000'000'000'000ULL; default: @@ -99,6 +122,27 @@ private: throw std::runtime_error("Unknown mantissa scale"); } } + + static constexpr CuspRoundingFix + isCuspFixEnabled(MantissaScale scale_) + { + switch (scale_) + { + case MantissaScale::Small: + case MantissaScale::LargeLegacy: + return CuspRoundingFix::Disabled; + case MantissaScale::Large: + return CuspRoundingFix::Enabled; + default: + // Since this can never be called outside a non-constexpr + // context, this throw assures that the build fails if an + // invalid scale is used. + throw std::runtime_error("Unknown mantissa scale"); + } + } + + static std::unordered_map const& + getRanges(); }; // Like std::integral, but only 64-bit integral types. @@ -424,49 +468,29 @@ public: return kRANGE.get().log; } - /// oneSmall is needed because the ranges are private - constexpr static Number - oneSmall(); - /// oneLarge is needed because the ranges are private - constexpr static Number - oneLarge(); - - // And one is needed because it needs to choose between oneSmall and - // oneLarge based on the current range static Number one(); - template + template < + auto minMantissa, + auto maxMantissa, + Integral64 T = std::decay_t, + Integral64 TMax = std::decay_t> [[nodiscard]] std::pair - normalizeToRange(T minMantissa, T maxMantissa) const; + normalizeToRange() const; private: static thread_local RoundingMode mode; // The available ranges for mantissa - constexpr static MantissaRange kSMALL_RANGE{MantissaRange::MantissaScale::Small}; - static_assert(isPowerOfTen(kSMALL_RANGE.min)); - static_assert(kSMALL_RANGE.min == 1'000'000'000'000'000LL); - static_assert(kSMALL_RANGE.max == 9'999'999'999'999'999LL); - static_assert(kSMALL_RANGE.log == 15); - static_assert(kSMALL_RANGE.min < kMAX_REP); - static_assert(kSMALL_RANGE.max < kMAX_REP); - constexpr static MantissaRange kLARGE_RANGE{MantissaRange::MantissaScale::Large}; - static_assert(isPowerOfTen(kLARGE_RANGE.min)); - static_assert(kLARGE_RANGE.min == 1'000'000'000'000'000'000ULL); - static_assert(kLARGE_RANGE.max == internalrep(9'999'999'999'999'999'999ULL)); - static_assert(kLARGE_RANGE.log == 18); - static_assert(kLARGE_RANGE.min < kMAX_REP); - static_assert(kLARGE_RANGE.max > kMAX_REP); - // The range for the mantissa when normalized. // Use reference_wrapper to avoid making copies, and prevent accidentally // changing the values inside the range. static thread_local std::reference_wrapper kRANGE; void - normalize(); + normalize(MantissaRange const& range); /** Normalize Number components to an arbitrary range. * @@ -481,7 +505,8 @@ private: T& mantissa, int& exponent, internalrep const& minMantissa, - internalrep const& maxMantissa); + internalrep const& maxMantissa, + MantissaRange::CuspRoundingFix cuspRoundingFixEnabled); template friend void @@ -490,7 +515,8 @@ private: T& mantissa, int& exponent, MantissaRange::rep const& minMantissa, - MantissaRange::rep const& maxMantissa); + MantissaRange::rep const& maxMantissa, + MantissaRange::CuspRoundingFix cuspRoundingFixEnabled); [[nodiscard]] bool isnormal() const noexcept; @@ -526,7 +552,7 @@ constexpr static Number kNUM_ZERO{}; inline Number::Number(bool negative, internalrep mantissa, int exponent, Normalized) : Number(negative, mantissa, exponent, Unchecked{}) { - normalize(); + normalize(kRANGE); } inline Number::Number(internalrep mantissa, int exponent, Normalized) @@ -696,10 +722,19 @@ Number::isnormal() const noexcept kMIN_EXPONENT <= exponent_ && exponent_ <= kMAX_EXPONENT); } -template +template std::pair -Number::normalizeToRange(T minMantissa, T maxMantissa) const +Number::normalizeToRange() const { + static_assert(std::is_same_v || std::is_same_v); + static_assert(std::is_same_v); + auto constexpr min = static_cast(minMantissa); + auto constexpr max = static_cast(maxMantissa); + static_assert(min > 0); + static_assert(min % 10 == 0); + static_assert(max % 10 == 9); + static_assert((max + 1) / 10 == min); + bool negative = negative_; internalrep mantissa = mantissa_; int exponent = exponent_; @@ -711,7 +746,10 @@ Number::normalizeToRange(T minMantissa, T maxMantissa) const "xrpl::Number::normalizeToRange", "Number is non-negative for unsigned range."); } - Number::normalize(negative, mantissa, exponent, minMantissa, maxMantissa); + // Don't need to worry about the cuspRounding fix because rounding up will never take the + // mantissa over maxMantissa with a ones digit value other than 0. 0 can safely be truncated. + Number::normalize( + negative, mantissa, exponent, min, max, MantissaRange::CuspRoundingFix::Disabled); auto const sign = negative ? -1 : 1; return std::make_pair(static_cast(sign * mantissa), exponent); @@ -762,9 +800,11 @@ to_string(MantissaRange::MantissaScale const& scale) switch (scale) { case MantissaRange::MantissaScale::Small: - return "small"; + return "Small"; + case MantissaRange::MantissaScale::LargeLegacy: + return "LargeLegacy"; case MantissaRange::MantissaScale::Large: - return "large"; + return "Large"; default: throw std::runtime_error("Bad scale"); } diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index 7fa6ef88ae..c685ae1e17 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -540,7 +540,7 @@ STAmount::fromNumber(A const& a, Number const& number) return STAmount{asset, intValue, 0, negative}; } - auto const [mantissa, exponent] = working.normalizeToRange(kMIN_VALUE, kMAX_VALUE); + auto const [mantissa, exponent] = working.normalizeToRange(); return STAmount{asset, mantissa, exponent, negative}; } diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 08ead182bf..1d89b05893 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -28,7 +28,76 @@ using int128_t = __int128_t; namespace xrpl { thread_local Number::RoundingMode Number::mode = Number::RoundingMode::ToNearest; -thread_local std::reference_wrapper Number::kRANGE = kLARGE_RANGE; +thread_local std::reference_wrapper Number::kRANGE = + MantissaRange::getMantissaRange(MantissaRange::MantissaScale::Large); + +std::set const& +MantissaRange::getAllScales() +{ + static std::set const scales = { + MantissaRange::MantissaScale::Small, + MantissaRange::MantissaScale::LargeLegacy, + MantissaRange::MantissaScale::Large, + }; + return scales; +} + +std::unordered_map const& +MantissaRange::getRanges() +{ + static auto const map = []() { + std::unordered_map map; + for (auto const scale : getAllScales()) + { + map.emplace(scale, scale); + } + + // Use these constexpr declarations to do static_asserts to verify the MantissaRanges are + // created correctly, but nothing else. + { + [[maybe_unused]] + constexpr static MantissaRange range{MantissaRange::MantissaScale::Small}; + static_assert(isPowerOfTen(range.min)); + static_assert(range.min == 1'000'000'000'000'000LL); + static_assert(range.max == 9'999'999'999'999'999LL); + static_assert(range.log == 15); + static_assert(range.min < Number::kMAX_REP); + static_assert(range.max < Number::kMAX_REP); + static_assert(range.cuspRoundingFixEnabled == CuspRoundingFix::Disabled); + } + { + [[maybe_unused]] + constexpr static MantissaRange range{MantissaRange::MantissaScale::LargeLegacy}; + static_assert(isPowerOfTen(range.min)); + static_assert(range.min == 1'000'000'000'000'000'000ULL); + static_assert(range.max == rep(9'999'999'999'999'999'999ULL)); + static_assert(range.log == 18); + static_assert(range.min < Number::kMAX_REP); + static_assert(range.max > Number::kMAX_REP); + static_assert(range.cuspRoundingFixEnabled == CuspRoundingFix::Disabled); + } + { + [[maybe_unused]] + constexpr static MantissaRange range{MantissaRange::MantissaScale::Large}; + static_assert(isPowerOfTen(range.min)); + static_assert(range.min == 1'000'000'000'000'000'000ULL); + static_assert(range.max == rep(9'999'999'999'999'999'999ULL)); + static_assert(range.log == 18); + static_assert(range.min < Number::kMAX_REP); + static_assert(range.max > Number::kMAX_REP); + static_assert(range.cuspRoundingFixEnabled == CuspRoundingFix::Enabled); + } + return map; + }(); + + return map; +} + +MantissaRange const& +MantissaRange::getMantissaRange(MantissaScale scale) +{ + return getRanges().at(scale); +} Number::RoundingMode Number::getround() @@ -51,10 +120,37 @@ Number::getMantissaScale() void Number::setMantissaScale(MantissaRange::MantissaScale scale) { - if (scale != MantissaRange::MantissaScale::Small && - scale != MantissaRange::MantissaScale::Large) + if (!MantissaRange::getAllScales().contains(scale)) logicError("Unknown mantissa scale"); - kRANGE = scale == MantissaRange::MantissaScale::Small ? kSMALL_RANGE : kLARGE_RANGE; + kRANGE = MantissaRange::getMantissaRange(scale); +} + +// Optimization equivalent to: +// auto r = static_cast(u % 10); +// u /= 10; +// return r; +// Derived from Hacker's Delight Second Edition Chapter 10 +// by Henry S. Warren, Jr. +static inline unsigned +divu10(uint128_t& u) +{ + // q = u * 0.75 + auto q = (u >> 1) + (u >> 2); + // iterate towards q = u * 0.8 + q += q >> 4; + q += q >> 8; + q += q >> 16; + q += q >> 32; + q += q >> 64; + // q /= 8 approximately == u / 10 + q >>= 3; + // r = u - q * 10 approximately == u % 10 + auto r = static_cast(u - ((q << 3) + (q << 1))); + // correction c is 1 if r >= 10 else 0 + auto c = (r + 6) >> 4; + u = q + c; + r -= c * 10; + return r; } // Guard @@ -92,6 +188,18 @@ public: unsigned pop() noexcept; + /** Drop a digit from the mantissa, and increment the exponent, storing the dropped digit in + * this Guard. + * + * Substitute for: + push(mantissa % 10); + mantissa /= 10; + ++exponent; + */ + template + void + doDropDigit(T& mantissa, int& exponent) noexcept; + // 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. @@ -107,6 +215,7 @@ public: int& exponent, internalrep const& minMantissa, internalrep const& maxMantissa, + MantissaRange::CuspRoundingFix cuspRoundingFixEnabled, std::string location); // Modify the result to the correctly rounded value @@ -168,6 +277,27 @@ Number::Guard::pop() noexcept return d; } +template +void +Number::Guard::doDropDigit(T& mantissa, int& exponent) noexcept +{ + push(mantissa % 10); + mantissa /= 10; + ++exponent; +} + +// Use the divu10 optimization for uint128s +template <> +void +Number::Guard::doDropDigit(uint128_t& mantissa, int& exponent) noexcept +{ + // The following is optimization for: + // push(static_cast(mantissa % 10)); + // mantissa /= 10; + push(divu10(mantissa)); + ++exponent; +} + // Returns: // -1 if Guard is less than half // 0 if Guard is exactly half @@ -242,18 +372,58 @@ Number::Guard::doRoundUp( int& exponent, internalrep const& minMantissa, internalrep const& maxMantissa, + MantissaRange::CuspRoundingFix cuspRoundingFixEnabled, std::string location) { auto r = round(); if (r == 1 || (r == 0 && (mantissa & 1) == 1)) { - ++mantissa; - // Ensure mantissa after incrementing fits within both the - // min/maxMantissa range and is a valid "rep". - if (mantissa > maxMantissa || mantissa > kMAX_REP) + if (cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled) { - mantissa /= 10; - ++exponent; + // Ensure mantissa after incrementing fits within both the + // min/maxMantissa range and is a valid "rep". + if (mantissa < maxMantissa && mantissa < kMAX_REP) + { + // Nothing unusual here, just increment the mantissa + ++mantissa; + } + 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 kMAX_REP, so adding 1 will have no + // change of bringing it back over. + doDropDigit(mantissa, exponent); + XRPL_ASSERT_PARTS( + mantissa < maxMantissa && mantissa < kMAX_REP, + "xrpl::Number::Guard::doRoundUp", + "can't recurse more than once"); + // Here be dragons + doRoundUp( + negative, + mantissa, + exponent, + minMantissa, + maxMantissa, + cuspRoundingFixEnabled, + location); + return; + } + } + else + { + // Need to preserve the incorrect behavior until the fix amendment can be retired, + // because otherwise would risk an unplanned ledger fork. + ++mantissa; + // Ensure mantissa after incrementing fits within both the + // min/maxMantissa range and is a valid "rep". + if (mantissa > maxMantissa || mantissa > kMAX_REP) + { + // Don't use doDropDigit here + mantissa /= 10; + ++exponent; + } } } bringIntoRange(negative, mantissa, exponent, minMantissa); @@ -293,9 +463,9 @@ Number::Guard::doRound(rep& drops, std::string location) const { static_assert(sizeof(internalrep) == sizeof(rep)); // This should be impossible, because it's impossible to represent - // "maxRep + 0.6" in Number, regardless of the scale. There aren't - // enough digits available. You'd either get a mantissa of "maxRep" - // or "(maxRep + 1) / 10", neither of which will round up when + // "kMAX_REP + 0.6" in Number, regardless of the scale. There aren't + // enough digits available. You'd either get a mantissa of "kMAX_REP" + // or "(kMAX_REP + 1) / 10", neither of which will round up when // converting to rep, though the latter might overflow _before_ // rounding. Throw(std::string(location)); // LCOV_EXCL_LINE @@ -331,29 +501,11 @@ Number::externalToInternal(rep mantissa) return static_cast(-temp); } -constexpr Number -Number::oneSmall() -{ - return Number{false, Number::kSMALL_RANGE.min, -Number::kSMALL_RANGE.log, Number::Unchecked{}}; -}; - -constexpr Number kONE_SML = Number::oneSmall(); - -constexpr Number -Number::oneLarge() -{ - return Number{false, Number::kLARGE_RANGE.min, -Number::kLARGE_RANGE.log, Number::Unchecked{}}; -}; - -constexpr Number kONE_LRG = Number::oneLarge(); - Number Number::one() { - if (&kRANGE.get() == &kSMALL_RANGE) - return kONE_SML; - XRPL_ASSERT(&kRANGE.get() == &kLARGE_RANGE, "Number::one() : valid range"); - return kONE_LRG; + auto const& range = kRANGE.get(); + return Number{false, range.min, -range.log, Number::Unchecked{}}; } // Use the member names in this static function for now so the diff is cleaner @@ -365,7 +517,8 @@ doNormalize( T& mantissa, int& exponent, MantissaRange::rep const& minMantissa, - MantissaRange::rep const& maxMantissa) + MantissaRange::rep const& maxMantissa, + MantissaRange::CuspRoundingFix cuspRoundingFixEnabled) { auto constexpr kMIN_EXPONENT = Number::kMIN_EXPONENT; auto constexpr kMAX_EXPONENT = Number::kMAX_EXPONENT; @@ -394,9 +547,7 @@ doNormalize( { if (exponent >= kMAX_EXPONENT) throw std::overflow_error("Number::normalize 1"); - g.push(m % 10); - m /= 10; - ++exponent; + g.doDropDigit(m, exponent); } if ((exponent < kMIN_EXPONENT) || (m < minMantissa)) { @@ -407,7 +558,7 @@ doNormalize( } // When using the largeRange, "m" needs fit within an int64, even if - // the final mantissa_ is going to end up larger to fit within the + // the final mantissa is going to end up larger to fit within the // MantissaRange. Cut it down here so that the rounding will be done while // it's smaller. // @@ -415,26 +566,31 @@ doNormalize( // so "m" will be modified to 990,000,000,000,012,345. Then that value // will be rounded to 990,000,000,000,012,345 or // 990,000,000,000,012,346, depending on the rounding mode. Finally, - // mantissa_ will be "m*10" so it fits within the range, and end up as + // mantissa will be "m*10" so it fits within the range, and end up as // 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. + // mantissa() will return mantissa / 10, and exponent() will return + // exponent + 1. if (m > kMAX_REP) { if (exponent >= kMAX_EXPONENT) throw std::overflow_error("Number::normalize 1.5"); - g.push(m % 10); - m /= 10; - ++exponent; + g.doDropDigit(m, exponent); } // Before modification, m should be within the min/max range. After - // modification, it must be less than maxRep. In other words, the original - // value should have been no more than maxRep * 10. - // (maxRep * 10 > maxMantissa) + // modification, it must be less than kMAX_REP. In other words, the original + // value should have been no more than kMAX_REP * 10. + // (kMAX_REP * 10 > maxMantissa) XRPL_ASSERT_PARTS(m <= kMAX_REP, "xrpl::doNormalize", "intermediate mantissa fits in int64"); mantissa = m; - g.doRoundUp(negative, mantissa, exponent, minMantissa, maxMantissa, "Number::normalize 2"); + g.doRoundUp( + negative, + mantissa, + exponent, + minMantissa, + maxMantissa, + cuspRoundingFixEnabled, + "Number::normalize 2"); XRPL_ASSERT_PARTS( mantissa >= minMantissa && mantissa <= maxMantissa, "xrpl::doNormalize", @@ -448,9 +604,10 @@ Number::normalize( uint128_t& mantissa, int& exponent, internalrep const& minMantissa, - internalrep const& maxMantissa) + internalrep const& maxMantissa, + MantissaRange::CuspRoundingFix cuspRoundingFixEnabled) { - doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa); + doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled); } template <> @@ -460,9 +617,10 @@ Number::normalize( unsigned long long& mantissa, int& exponent, internalrep const& minMantissa, - internalrep const& maxMantissa) + internalrep const& maxMantissa, + MantissaRange::CuspRoundingFix cuspRoundingFixEnabled) { - doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa); + doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled); } template <> @@ -472,16 +630,16 @@ Number::normalize( unsigned long& mantissa, int& exponent, internalrep const& minMantissa, - internalrep const& maxMantissa) + internalrep const& maxMantissa, + MantissaRange::CuspRoundingFix cuspRoundingFixEnabled) { - doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa); + doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled); } void -Number::normalize() +Number::normalize(MantissaRange const& range) { - auto const& range = kRANGE.get(); - normalize(negative_, mantissa_, exponent_, range.min, range.max); + normalize(negative_, mantissa_, exponent_, range.min, range.max, range.cuspRoundingFixEnabled); } // Copy the number, but set a new exponent. Because the mantissa doesn't change, @@ -542,9 +700,7 @@ Number::operator+=(Number const& y) g.setNegative(); do { - g.push(xm % 10); - xm /= 10; - ++xe; + g.doDropDigit(xm, xe); } while (xe < ye); } else if (xe > ye) @@ -553,26 +709,30 @@ Number::operator+=(Number const& y) g.setNegative(); do { - g.push(ym % 10); - ym /= 10; - ++ye; + g.doDropDigit(ym, ye); } while (xe > ye); } auto const& range = kRANGE.get(); auto const& minMantissa = range.min; auto const& maxMantissa = range.max; + auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled; if (xn == yn) { xm += ym; if (xm > maxMantissa || xm > kMAX_REP) { - g.push(xm % 10); - xm /= 10; - ++xe; + g.doDropDigit(xm, xe); } - g.doRoundUp(xn, xm, xe, minMantissa, maxMantissa, "Number::addition overflow"); + g.doRoundUp( + xn, + xm, + xe, + minMantissa, + maxMantissa, + cuspRoundingFixEnabled, + "Number::addition overflow"); } else { @@ -598,38 +758,10 @@ Number::operator+=(Number const& y) negative_ = xn; mantissa_ = static_cast(xm); exponent_ = xe; - normalize(); + normalize(range); return *this; } -// Optimization equivalent to: -// auto r = static_cast(u % 10); -// u /= 10; -// return r; -// Derived from Hacker's Delight Second Edition Chapter 10 -// by Henry S. Warren, Jr. -static inline unsigned -divu10(uint128_t& u) -{ - // q = u * 0.75 - auto q = (u >> 1) + (u >> 2); - // iterate towards q = u * 0.8 - q += q >> 4; - q += q >> 8; - q += q >> 16; - q += q >> 32; - q += q >> 64; - // q /= 8 approximately == u / 10 - q >>= 3; - // r = u - q * 10 approximately == u % 10 - auto r = static_cast(u - ((q << 3) + (q << 1))); - // correction c is 1 if r >= 10 else 0 - auto c = (r + 6) >> 4; - u = q + c; - r -= c * 10; - return r; -} - Number& Number::operator*=(Number const& y) { @@ -667,15 +799,13 @@ Number::operator*=(Number const& y) auto const& range = kRANGE.get(); auto const& minMantissa = range.min; auto const& maxMantissa = range.max; + auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled; while (zm > maxMantissa || zm > kMAX_REP) { - // The following is optimization for: - // g.push(static_cast(zm % 10)); - // zm /= 10; - g.push(divu10(zm)); - ++ze; + g.doDropDigit(zm, ze); } + xm = static_cast(zm); xe = ze; g.doRoundUp( @@ -684,12 +814,13 @@ Number::operator*=(Number const& y) xe, minMantissa, maxMantissa, + cuspRoundingFixEnabled, "Number::multiplication overflow : exponent is " + std::to_string(xe)); negative_ = zn; mantissa_ = xm; exponent_ = xe; - normalize(); + normalize(range); return *this; } @@ -721,6 +852,7 @@ Number::operator/=(Number const& y) auto const& range = kRANGE.get(); auto const& minMantissa = range.min; auto const& maxMantissa = range.max; + auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled; // Shift by 10^17 gives greatest precision while not overflowing // uint128_t or the cast back to int64_t @@ -728,8 +860,6 @@ Number::operator/=(Number const& y) // log(2^128,10) ~ 38.5 // largeRange.log = 18, fits in 10^19 // f can be up to 10^(38-19) = 10^19 safely - static_assert(kSMALL_RANGE.log == 15); - static_assert(kLARGE_RANGE.log == 18); bool const small = Number::getMantissaScale() == MantissaRange::MantissaScale::Small; uint128_t const f = small ? 100'000'000'000'000'000 : 10'000'000'000'000'000'000ULL; XRPL_ASSERT_PARTS(f >= minMantissa * 10, "Number::operator/=", "factor expected size"); @@ -779,7 +909,7 @@ Number::operator/=(Number const& y) ze -= 3; } } - normalize(zn, zm, ze, minMantissa, maxMantissa); + normalize(zn, zm, ze, minMantissa, maxMantissa, cuspRoundingFixEnabled); negative_ = zn; mantissa_ = static_cast(zm); exponent_ = ze; @@ -801,10 +931,9 @@ operator rep() const g.setNegative(); drops = -drops; } - for (; offset < 0; ++offset) + while (offset < 0) { - g.push(drops % 10); - drops /= 10; + g.doDropDigit(drops, offset); } for (; offset > 0; --offset) { @@ -831,7 +960,7 @@ Number::truncate() const noexcept } // We are guaranteed that normalize() will never throw an exception // because exponent is either negative or zero at this point. - ret.normalize(); + ret.normalize(kRANGE); return ret; } diff --git a/src/libxrpl/protocol/IOUAmount.cpp b/src/libxrpl/protocol/IOUAmount.cpp index e4326d611e..9e3c024ca0 100644 --- a/src/libxrpl/protocol/IOUAmount.cpp +++ b/src/libxrpl/protocol/IOUAmount.cpp @@ -56,7 +56,7 @@ IOUAmount::fromNumber(Number const& number) // to normalize, which calls fromNumber IOUAmount result{}; std::tie(result.mantissa_, result.exponent_) = - number.normalizeToRange(kMIN_MANTISSA, kMAX_MANTISSA); + number.normalizeToRange(); return result; } diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index 2c971749b6..8040571c75 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -38,11 +38,13 @@ setCurrentTransactionRules(std::optional r) // Make global changes associated with the rules before the value is moved. // Push the appropriate setting, instead of having the class pull every time // the value is needed. That could get expensive fast. - bool const enableLargeNumbers = - !r || (r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol)); + bool const enableCuspRoundingFix = !r || r->enabled(fixCleanup3_2_0); + bool const enableLargeNumbers = enableCuspRoundingFix || + (r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol)); Number::setMantissaScale( - enableLargeNumbers ? MantissaRange::MantissaScale::Large - : MantissaRange::MantissaScale::Small); + enableCuspRoundingFix ? MantissaRange::MantissaScale::Large + : enableLargeNumbers ? MantissaRange::MantissaScale::LargeLegacy + : MantissaRange::MantissaScale::Small); *getCurrentTransactionRulesRef() = std::move(r); } diff --git a/src/libxrpl/protocol/STNumber.cpp b/src/libxrpl/protocol/STNumber.cpp index f6481a4d5d..60008bbe77 100644 --- a/src/libxrpl/protocol/STNumber.cpp +++ b/src/libxrpl/protocol/STNumber.cpp @@ -96,7 +96,8 @@ STNumber::add(Serializer& s) const // Json. Regardless, the only time we should be serializing an // STNumber is when the scale is large. XRPL_ASSERT_PARTS( - Number::getMantissaScale() == MantissaRange::MantissaScale::Large, + Number::getMantissaScale() == MantissaRange::MantissaScale::LargeLegacy || + Number::getMantissaScale() == MantissaRange::MantissaScale::Large, "xrpl::STNumber::add", "STNumber only used with large mantissa scale"); #endif diff --git a/src/test/app/AMMClawbackMPT_test.cpp b/src/test/app/AMMClawbackMPT_test.cpp index 6f0fff0282..523a4af7fc 100644 --- a/src/test/app/AMMClawbackMPT_test.cpp +++ b/src/test/app/AMMClawbackMPT_test.cpp @@ -1829,7 +1829,7 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite testLastHolderLPTokenBalance(all - fixAMMv1_3 - fixAMMClawbackRounding); testLastHolderLPTokenBalance( all - fixAMMv1_3 - fixAMMClawbackRounding - featureSingleAssetVault - - featureLendingProtocol); + featureLendingProtocol - fixCleanup3_2_0); testLastHolderLPTokenBalance(all - fixAMMClawbackRounding); testClawAssetCheck(all); } diff --git a/src/test/app/AMMClawback_test.cpp b/src/test/app/AMMClawback_test.cpp index 9683e8ac17..500fd84ebe 100644 --- a/src/test/app/AMMClawback_test.cpp +++ b/src/test/app/AMMClawback_test.cpp @@ -2506,8 +2506,8 @@ class AMMClawback_test : public beast::unit_test::Suite { // For now, just disable SAV entirely, which locks in the small Number // mantissas - FeatureBitset const all = - jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol; + FeatureBitset const all = jtx::testableAmendments() - featureSingleAssetVault - + featureLendingProtocol - fixCleanup3_2_0; testInvalidRequest(all); testInvalidRequest(all - featureMPTokensV2); diff --git a/src/test/app/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp index 8132c012ad..68208f5d36 100644 --- a/src/test/app/AMMExtended_test.cpp +++ b/src/test/app/AMMExtended_test.cpp @@ -70,6 +70,11 @@ struct AMMExtended_test : public jtx::AMMTest // Use small Number mantissas for the life of this test. NumberMantissaScaleGuard const sg{xrpl::MantissaRange::MantissaScale::Small}; + // For now, just disable SAV entirely, which locks in the small Number + // mantissas + FeatureBitset const all_{ + testableAmendments() - featureSingleAssetVault - featureLendingProtocol - fixCleanup3_2_0}; + private: void testRmFundedOffer(FeatureBitset features) @@ -1348,37 +1353,33 @@ private: testOffers() { using namespace jtx; - // For now, just disable SAV entirely, which locks in the small Number - // mantissas - FeatureBitset const all{ - testableAmendments() - featureSingleAssetVault - featureLendingProtocol}; - testRmFundedOffer(all); - testRmFundedOffer(all - fixAMMv1_1 - fixAMMv1_3); - testEnforceNoRipple(all); - testFillModes(all); - testOfferCrossWithXRP(all); - testOfferCrossWithLimitOverride(all); - testCurrencyConversionEntire(all); - testCurrencyConversionInParts(all); - testCrossCurrencyStartXRP(all); - testCrossCurrencyEndXRP(all); - testCrossCurrencyBridged(all); - testOfferFeesConsumeFunds(all); - testOfferCreateThenCross(all); - testSellFlagExceedLimit(all); - testGatewayCrossCurrency(all); - testGatewayCrossCurrency(all - fixAMMv1_1 - fixAMMv1_3); - testBridgedCross(all); - testSellWithFillOrKill(all); - testTransferRateOffer(all); - testSelfIssueOffer(all); - testBadPathAssert(all); - testSellFlagBasic(all); - testDirectToDirectPath(all); - testDirectToDirectPath(all - fixAMMv1_1 - fixAMMv1_3); - testRequireAuth(all); - testMissingAuth(all); + testRmFundedOffer(all_); + testRmFundedOffer(all_ - fixAMMv1_1 - fixAMMv1_3); + testEnforceNoRipple(all_); + testFillModes(all_); + testOfferCrossWithXRP(all_); + testOfferCrossWithLimitOverride(all_); + testCurrencyConversionEntire(all_); + testCurrencyConversionInParts(all_); + testCrossCurrencyStartXRP(all_); + testCrossCurrencyEndXRP(all_); + testCrossCurrencyBridged(all_); + testOfferFeesConsumeFunds(all_); + testOfferCreateThenCross(all_); + testSellFlagExceedLimit(all_); + testGatewayCrossCurrency(all_); + testGatewayCrossCurrency(all_ - fixAMMv1_1 - fixAMMv1_3); + testBridgedCross(all_); + testSellWithFillOrKill(all_); + testTransferRateOffer(all_); + testSelfIssueOffer(all_); + testBadPathAssert(all_); + testSellFlagBasic(all_); + testDirectToDirectPath(all_); + testDirectToDirectPath(all_ - fixAMMv1_1 - fixAMMv1_3); + testRequireAuth(all_); + testMissingAuth(all_); } void @@ -3513,15 +3514,11 @@ private: testFlow() { using namespace jtx; - // For now, just disable SAV entirely, which locks in the small Number - // mantissas in the transaction engine - FeatureBitset const all{ - testableAmendments() - featureSingleAssetVault - featureLendingProtocol}; - testFalseDry(all); - testBookStep(all); - testTransferRateNoOwnerFee(all); - testTransferRateNoOwnerFee(all - fixAMMv1_1 - fixAMMv1_3); + testFalseDry(all_); + testBookStep(all_); + testTransferRateNoOwnerFee(all_); + testTransferRateNoOwnerFee(all_ - fixAMMv1_1 - fixAMMv1_3); testLimitQuality(); testXRPPathLoop(); } @@ -3530,34 +3527,22 @@ private: testCrossingLimits() { using namespace jtx; - // For now, just disable SAV entirely, which locks in the small Number - // mantissas in the transaction engine - FeatureBitset const all{ - testableAmendments() - featureSingleAssetVault - featureLendingProtocol}; - testStepLimit(all); - testStepLimit(all - fixAMMv1_1 - fixAMMv1_3); + testStepLimit(all_); + testStepLimit(all_ - fixAMMv1_1 - fixAMMv1_3); } void testDeliverMin() { using namespace jtx; - // For now, just disable SAV entirely, which locks in the small Number - // mantissas in the transaction engine - FeatureBitset const all{ - testableAmendments() - featureSingleAssetVault - featureLendingProtocol}; - testConvertAllOfAnAsset(all); - testConvertAllOfAnAsset(all - fixAMMv1_1 - fixAMMv1_3); + testConvertAllOfAnAsset(all_); + testConvertAllOfAnAsset(all_ - fixAMMv1_1 - fixAMMv1_3); } void testDepositAuth() { - // For now, just disable SAV entirely, which locks in the small Number - // mantissas in the transaction engine - FeatureBitset const all{ - jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol}; - testPayment(all); + testPayment(all_); testPayIOU(); } @@ -3565,13 +3550,9 @@ private: testFreeze() { using namespace test::jtx; - // For now, just disable SAV entirely, which locks in the small Number - // mantissas in the transaction engine - FeatureBitset const sa{ - testableAmendments() - featureSingleAssetVault - featureLendingProtocol}; - testRippleState(sa); - testGlobalFreeze(sa); - testOffersWhenFrozen(sa); + testRippleState(all_); + testGlobalFreeze(all_); + testOffersWhenFrozen(all_); } void diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index 8e2c0255ef..b2792ab66f 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -81,7 +81,8 @@ private: { // For now, just disable SAV entirely, which locks in the small Number // mantissas - return jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol; + return jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol - + fixCleanup3_2_0; } void @@ -2624,10 +2625,6 @@ private: using namespace jtx; using namespace std::chrono; - // For now, just disable SAV entirely, which locks in the small Number - // mantissas - features = features - featureSingleAssetVault - featureLendingProtocol; - // Auction slot initially is owned by AMM creator, who pays 0 price. // Bid 110 tokens. Pay bidMin. @@ -3337,11 +3334,6 @@ private: testcase("Basic Payment"); using namespace jtx; - // For now, just disable SAV entirely, which locks in the small Number - // mantissas - features = - features - featureSingleAssetVault - featureLendingProtocol - featureLendingProtocol; - // Payment 100USD for 100XRP. // Force one path with tfNoRippleDirect. testAMM( diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 965414553f..3021f192ba 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -4256,109 +4256,119 @@ class Invariants_test : public beast::unit_test::Suite std::vector values; }; - NumberMantissaScaleGuard const g{MantissaRange::MantissaScale::Large}; - - auto makeDelta = [&vaultAsset](Number const& n) -> ValidVault::DeltaInfo { - return {.delta = n, .scale = scale(n, vaultAsset.raw())}; - }; - - auto const testCases = std::vector{ - { - .name = "No values", - .expectedMinScale = 0, - .values = {}, - }, - { - .name = "Mixed integer and Number values", - .expectedMinScale = -15, - .values = {makeDelta(1), makeDelta(-1), makeDelta(Number{10, -1})}, - }, - { - .name = "Mixed scales", - .expectedMinScale = -17, - .values = - {makeDelta(Number{1, -2}), makeDelta(Number{5, -3}), makeDelta(Number{3, -2})}, - }, - { - .name = "Equal scales", - .expectedMinScale = -16, - .values = - {makeDelta(Number{1, -1}), makeDelta(Number{5, -1}), makeDelta(Number{1, -1})}, - }, - { - .name = "Mixed mantissa sizes", - .expectedMinScale = -12, - .values = - {makeDelta(Number{1}), - makeDelta(Number{1234, -3}), - makeDelta(Number{12345, -6}), - makeDelta(Number{123, 1})}, - }, - }; - - for (auto const& tc : testCases) + for (auto const mantissaScale : { + MantissaRange::MantissaScale::LargeLegacy, + MantissaRange::MantissaScale::Large, + }) { - testcase("vault computeCoarsestScale: " + tc.name); + NumberMantissaScaleGuard const g{mantissaScale}; - auto const actualScale = ValidVault::computeCoarsestScale(tc.values); + auto makeDelta = [&vaultAsset](Number const& n) -> ValidVault::DeltaInfo { + return {.delta = n, .scale = scale(n, vaultAsset.raw())}; + }; - BEAST_EXPECTS( - actualScale == tc.expectedMinScale, - "expected: " + std::to_string(tc.expectedMinScale) + - ", actual: " + std::to_string(actualScale)); - for (auto const& num : tc.values) - { - // None of these scales are far enough apart that rounding the - // values would lose information, so check that the rounded - // value matches the original. - auto const actualRounded = roundToAsset(vaultAsset, num.delta, actualScale); - BEAST_EXPECTS( - actualRounded == num.delta, - "number " + to_string(num.delta) + " rounded to scale " + - std::to_string(actualScale) + " is " + to_string(actualRounded)); - } - } - - auto const testCases2 = std::vector{ - { - .name = "False equivalence", - .expectedMinScale = -15, - .values = - { - makeDelta(Number{1234567890123456789, -18}), - makeDelta(Number{12345, -4}), - makeDelta(Number{1}), - }, - }, - }; - - // Unlike the first set of test cases, the values in these test could - // look equivalent if using the wrong scale. - for (auto const& tc : testCases2) - { - testcase("vault computeCoarsestScale: " + tc.name); - - auto const actualScale = ValidVault::computeCoarsestScale(tc.values); - - BEAST_EXPECTS( - actualScale == tc.expectedMinScale, - "expected: " + std::to_string(tc.expectedMinScale) + - ", actual: " + std::to_string(actualScale)); - std::optional first; - Number firstRounded; - for (auto const& num : tc.values) - { - if (!first) + auto const testCases = std::vector{ { - first = num.delta; - firstRounded = roundToAsset(vaultAsset, num.delta, actualScale); - continue; - } - auto const numRounded = roundToAsset(vaultAsset, num.delta, actualScale); + .name = "No values", + .expectedMinScale = 0, + .values = {}, + }, + { + .name = "Mixed integer and Number values", + .expectedMinScale = -15, + .values = {makeDelta(1), makeDelta(-1), makeDelta(Number{10, -1})}, + }, + { + .name = "Mixed scales", + .expectedMinScale = -17, + .values = + {makeDelta(Number{1, -2}), + makeDelta(Number{5, -3}), + makeDelta(Number{3, -2})}, + }, + { + .name = "Equal scales", + .expectedMinScale = -16, + .values = + {makeDelta(Number{1, -1}), + makeDelta(Number{5, -1}), + makeDelta(Number{1, -1})}, + }, + { + .name = "Mixed mantissa sizes", + .expectedMinScale = -12, + .values = + {makeDelta(Number{1}), + makeDelta(Number{1234, -3}), + makeDelta(Number{12345, -6}), + makeDelta(Number{123, 1})}, + }, + }; + + for (auto const& tc : testCases) + { + testcase("vault computeCoarsestScale: " + tc.name); + + auto const actualScale = ValidVault::computeCoarsestScale(tc.values); + BEAST_EXPECTS( - numRounded != firstRounded, - "at a scale of " + std::to_string(actualScale) + " " + to_string(num.delta) + - " == " + to_string(*first)); + actualScale == tc.expectedMinScale, + "expected: " + std::to_string(tc.expectedMinScale) + + ", actual: " + std::to_string(actualScale)); + for (auto const& num : tc.values) + { + // None of these scales are far enough apart that rounding the + // values would lose information, so check that the rounded + // value matches the original. + auto const actualRounded = roundToAsset(vaultAsset, num.delta, actualScale); + BEAST_EXPECTS( + actualRounded == num.delta, + "number " + to_string(num.delta) + " rounded to scale " + + std::to_string(actualScale) + " is " + to_string(actualRounded)); + } + } + + auto const testCases2 = std::vector{ + { + .name = "False equivalence", + .expectedMinScale = -15, + .values = + { + makeDelta(Number{1234567890123456789, -18}), + makeDelta(Number{12345, -4}), + makeDelta(Number{1}), + }, + }, + }; + + // Unlike the first set of test cases, the values in these test could + // look equivalent if using the wrong scale. + for (auto const& tc : testCases2) + { + testcase("vault computeCoarsestScale: " + tc.name); + + auto const actualScale = ValidVault::computeCoarsestScale(tc.values); + + BEAST_EXPECTS( + actualScale == tc.expectedMinScale, + "expected: " + std::to_string(tc.expectedMinScale) + + ", actual: " + std::to_string(actualScale)); + std::optional first; + Number firstRounded; + for (auto const& num : tc.values) + { + if (!first) + { + first = num.delta; + firstRounded = roundToAsset(vaultAsset, num.delta, actualScale); + continue; + } + auto const numRounded = roundToAsset(vaultAsset, num.delta, actualScale); + BEAST_EXPECTS( + numRounded != firstRounded, + "at a scale of " + std::to_string(actualScale) + " " + + to_string(num.delta) + " == " + to_string(*first)); + } } } } diff --git a/src/test/basics/IOUAmount_test.cpp b/src/test/basics/IOUAmount_test.cpp index 738990aac2..275a71cff1 100644 --- a/src/test/basics/IOUAmount_test.cpp +++ b/src/test/basics/IOUAmount_test.cpp @@ -156,8 +156,7 @@ public: BEAST_EXPECTS(result == expected, ss.str()); }; - for (auto const mantissaSize : - {MantissaRange::MantissaScale::Small, MantissaRange::MantissaScale::Large}) + for (auto const mantissaSize : MantissaRange::getAllScales()) { NumberMantissaScaleGuard const mg(mantissaSize); diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 9e06d6de53..c26816348d 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -6,6 +6,8 @@ #include #include +#include + #include #include #include @@ -19,6 +21,24 @@ namespace xrpl { class Number_test : public beast::unit_test::Suite { + using BigInt = boost::multiprecision::cpp_int; + + static std::string + fmt(BigInt const& value) + { + auto s = to_string(value); + std::string out; + int count = 0; + for (auto it = s.rbegin(); it != s.rend(); ++it) + { + if (count && count % 3 == 0) + out.insert(out.begin(), '_'); + out.insert(out.begin(), *it); + ++count; + } + return out; + } + public: void testZero() @@ -178,7 +198,6 @@ public: {Number{true, 9'999'999'999'999'999'999ULL, -37, Number::Normalized{}}, Number{1'000'000'000'000'000'000, -18}, Number{false, 9'999'999'999'999'999'990ULL, -19, Number::Normalized{}}}, - {Number{Number::kMAX_REP}, Number{6, -1}, Number{Number::kMAX_REP / 10, 1}}, {Number{Number::kMAX_REP - 1}, Number{1, 0}, Number{Number::kMAX_REP}}, // Test extremes { @@ -189,16 +208,22 @@ public: Number{2, 19}, }, { - // Does not round. Mantissas are going to be > maxRep, so if + // Does not round. Mantissas are going to be > kMAX_REP, so if // added together as uint64_t's, the result will overflow. // With addition using uint128_t, there's no problem. After // normalizing, the resulting mantissa ends up less than - // maxRep. + // kMAX_REP. Number{false, 9'999'999'999'999'999'990ULL, 0, Number::Normalized{}}, Number{false, 9'999'999'999'999'999'990ULL, 0, Number::Normalized{}}, Number{false, 1'999'999'999'999'999'998ULL, 1, Number::Normalized{}}, }, }); + auto const cLargeLegacy = std::to_array({ + {Number{Number::kMAX_REP}, Number{6, -1}, Number{Number::kMAX_REP / 10, 1}}, + }); + auto const cLargeCorrected = std::to_array({ + {Number{Number::kMAX_REP}, Number{6, -1}, Number{Number::kMAX_REP / 10 + 1, 1}}, + }); auto test = [this](auto const& c) { for (auto const& [x, y, z] : c) { @@ -215,6 +240,10 @@ public: else { test(cLarge); + if (scale == MantissaRange::MantissaScale::LargeLegacy) + test(cLargeLegacy); + else + test(cLargeCorrected); } { bool caught = false; @@ -835,7 +864,7 @@ public: /* auto tests = [&](auto const& cSmall, auto const& cLarge) { test(cSmall); - if (scale != MantissaRange::mantissa_scale::small) + if (scale != MantissaRange::MantissaScale::Small) test(cLarge); }; */ @@ -1266,6 +1295,7 @@ public: "9223372036854775e3"); } break; + case MantissaRange::MantissaScale::LargeLegacy: case MantissaRange::MantissaScale::Large: // Test the edges // ((exponent < -(28)) || (exponent > -(8))))) @@ -1551,11 +1581,49 @@ public: } } + void + testUpwardRoundsDown() + { + testcase << "upward rounding produces a value below exact at kMAX_REP cusp"; + + NumberMantissaScaleGuard const mg{MantissaRange::MantissaScale::Large}; + NumberRoundModeGuard const rg{Number::RoundingMode::Upward}; + + constexpr std::int64_t aValue = 1'000'000'000'000'049'863LL; + constexpr std::int64_t bValue = 9'223'372'036'854'315'903LL; + + // Public conversion operator: STAmount::operator Number() const. + Number const a = aValue; + Number const b = bValue; + Number const product = a * b; + + // Exact reference in BigInt. + BigInt const exactProduct = BigInt(aValue) * BigInt(bValue); + + // What Number actually stored. + BigInt storedValue = BigInt(product.mantissa()); + for (int i = 0; i < product.exponent(); ++i) + storedValue *= 10; + + BigInt const signedDifference = storedValue - exactProduct; + + log << "\n" + << " a = " << fmt(BigInt(aValue)) << "\n" + << " b = " << fmt(BigInt(bValue)) << "\n" + << " exact a*b = " << fmt(exactProduct) << "\n" + << " stored = " << fmt(storedValue) << "\n" + << " stored - exact = " << fmt(signedDifference) << "\n" + << " upward = " << (signedDifference >= 0 ? "held" : "VIOLATED") << "\n"; + + BEAST_EXPECT(signedDifference >= 0); + BEAST_EXPECT(product.mantissa() == (std::numeric_limits::max() / 10) + 1); + BEAST_EXPECT(product.exponent() == 19); + } + void run() override { - for (auto const scale : - {MantissaRange::MantissaScale::Small, MantissaRange::MantissaScale::Large}) + for (auto const scale : MantissaRange::getAllScales()) { NumberMantissaScaleGuard const sg(scale); testZero(); @@ -1580,6 +1648,8 @@ public: testRounding(); testInt64(); } + // This test sets its own number range + testUpwardRoundsDown(); } }; diff --git a/src/test/jtx/AMMTest.h b/src/test/jtx/AMMTest.h index a2e6c38db3..fc8cd1b1e6 100644 --- a/src/test/jtx/AMMTest.h +++ b/src/test/jtx/AMMTest.h @@ -21,7 +21,8 @@ struct TestAMMArg std::vector features = { // For now, just disable SAV entirely, which locks in the small Number // mantissas - jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol}; + jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol - + fixCleanup3_2_0}; bool noLog = false; }; @@ -87,7 +88,8 @@ public: { // For now, just disable SAV entirely, which locks in the small Number // mantissas - return jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol; + return jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol - + fixCleanup3_2_0; } protected: diff --git a/src/test/jtx/impl/AMMTest.cpp b/src/test/jtx/impl/AMMTest.cpp index 5801e8fb44..3bb4cca484 100644 --- a/src/test/jtx/impl/AMMTest.cpp +++ b/src/test/jtx/impl/AMMTest.cpp @@ -143,7 +143,7 @@ AMMTestBase::testAMM(std::function const& cb, TestAM // mantissas Env env{ *this, - features - featureSingleAssetVault - featureLendingProtocol, + features - featureSingleAssetVault - featureLendingProtocol - fixCleanup3_2_0, arg.noLog ? std::make_unique(&logs) : nullptr}; auto const [asset1, asset2] = arg.pool ? *arg.pool : std::make_pair(XRP(10000), USD(10000)); diff --git a/src/test/protocol/STNumber_test.cpp b/src/test/protocol/STNumber_test.cpp index 914e2d003f..2f84ccee82 100644 --- a/src/test/protocol/STNumber_test.cpp +++ b/src/test/protocol/STNumber_test.cpp @@ -280,8 +280,7 @@ struct STNumber_test : public beast::unit_test::Suite { static_assert(!std::is_convertible_v); - for (auto const scale : - {MantissaRange::MantissaScale::Small, MantissaRange::MantissaScale::Large}) + for (auto const scale : MantissaRange::getAllScales()) { NumberMantissaScaleGuard const sg(scale); testcase << to_string(Number::getMantissaScale()); diff --git a/src/test/rpc/GetAggregatePrice_test.cpp b/src/test/rpc/GetAggregatePrice_test.cpp index 418b36d703..7505f69851 100644 --- a/src/test/rpc/GetAggregatePrice_test.cpp +++ b/src/test/rpc/GetAggregatePrice_test.cpp @@ -176,10 +176,10 @@ public: // or time threshold { auto const all = testableAmendments(); - for (auto const& feats : {all - featureSingleAssetVault - featureLendingProtocol, all}) + for (auto const& feats : + {all - featureSingleAssetVault - featureLendingProtocol - fixCleanup3_2_0, all}) { - for (auto const mantissaSize : - {MantissaRange::MantissaScale::Small, MantissaRange::MantissaScale::Large}) + for (auto const mantissaSize : MantissaRange::getAllScales()) { // Regardless of the features enabled, RPC is controlled by // the global mantissa size. And since it's a thread-local, From a2b21d75ce72837eb0bd87b39ed3d7deb2f9fc86 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 6 May 2026 22:56:07 -0400 Subject: [PATCH 02/77] Fix AI-identified mistakes --- include/xrpl/basics/Number.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index fb32b42060..1db265fa6b 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -9,6 +9,7 @@ #include #include #include +#include namespace xrpl { @@ -475,7 +476,7 @@ public: auto minMantissa, auto maxMantissa, Integral64 T = std::decay_t, - Integral64 TMax = std::decay_t> + Integral64 TMax = std::decay_t> [[nodiscard]] std::pair normalizeToRange() const; From b050c151f89594fb08fbffcdca30ac1895fae2b8 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 6 May 2026 23:26:11 -0400 Subject: [PATCH 03/77] Fix clang-tidy issues, and more AI complaints --- include/xrpl/basics/Number.h | 46 +++++++++++++----------- src/libxrpl/basics/Number.cpp | 58 ++++++++++++++++--------------- src/libxrpl/protocol/Rules.cpp | 12 +++++-- src/test/app/AMMExtended_test.cpp | 2 +- src/test/basics/Number_test.cpp | 22 +++++++----- 5 files changed, 79 insertions(+), 61 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 1db265fa6b..ebdb53aa83 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -50,7 +50,7 @@ isPowerOfTen(T value) * options. This intentionally restricts the number of unique MantissaRanges that can * be instantiated: one for each scale. * - * The "small" scale is based on the behavior of STAmount for IOUs. It has a min + * The "Small" scale is based on the behavior of STAmount for IOUs. It has a min * value of 10^15, and a max value of 10^16-1. This was sufficient for * uses before Lending Protocol was implemented, mostly related to AMM. * @@ -61,12 +61,16 @@ isPowerOfTen(T value) * STNumber field type, and for internal calculations. That necessitated the * "large" scale. * - * The "large" scale is intended to represent all values that can be represented + * The "Large" scales are intended to represent all values that can be represented * by an STAmount - IOUs, XRP, and MPTs. It has a min value of 10^18, and a max - * value of 10^19-1. + * value of 10^19-1. "LargeLegacy" is like "Large", but preserves + * a rounding error when a computation results in a mantissa of + * Number::kMAX_REP that needs to be rounded up, but rounds down + * instead. It will maintain consistent behavior until the fixCleanup3_2_0 + * amendment is enabled. * * Note that if the mentioned amendments are eventually retired, this class - * should be left in place, but the "small" scale option should be removed. This + * should be left in place, but the "Small" scale option should be removed. This * will allow for future expansion beyond 64-bits if it is ever needed. */ struct MantissaRange @@ -125,9 +129,9 @@ private: } static constexpr CuspRoundingFix - isCuspFixEnabled(MantissaScale scale_) + isCuspFixEnabled(MantissaScale scale) { - switch (scale_) + switch (scale) { case MantissaScale::Small: case MantissaScale::LargeLegacy: @@ -473,10 +477,10 @@ public: one(); template < - auto minMantissa, - auto maxMantissa, - Integral64 T = std::decay_t, - Integral64 TMax = std::decay_t> + auto MinMantissa, + auto MaxMantissa, + Integral64 T = std::decay_t, + Integral64 TMax = std::decay_t> [[nodiscard]] std::pair normalizeToRange() const; @@ -723,18 +727,18 @@ Number::isnormal() const noexcept kMIN_EXPONENT <= exponent_ && exponent_ <= kMAX_EXPONENT); } -template +template std::pair Number::normalizeToRange() const { static_assert(std::is_same_v || std::is_same_v); static_assert(std::is_same_v); - auto constexpr min = static_cast(minMantissa); - auto constexpr max = static_cast(maxMantissa); - static_assert(min > 0); - static_assert(min % 10 == 0); - static_assert(max % 10 == 9); - static_assert((max + 1) / 10 == min); + auto constexpr kMIN = static_cast(MinMantissa); + auto constexpr kMAX = static_cast(MaxMantissa); + static_assert(kMIN > 0); + static_assert(kMIN % 10 == 0); + static_assert(kMAX % 10 == 9); + static_assert((kMAX + 1) / 10 == kMIN); bool negative = negative_; internalrep mantissa = mantissa_; @@ -750,7 +754,7 @@ Number::normalizeToRange() const // Don't need to worry about the cuspRounding fix because rounding up will never take the // mantissa over maxMantissa with a ones digit value other than 0. 0 can safely be truncated. Number::normalize( - negative, mantissa, exponent, min, max, MantissaRange::CuspRoundingFix::Disabled); + negative, mantissa, exponent, kMIN, kMAX, MantissaRange::CuspRoundingFix::Disabled); auto const sign = negative ? -1 : 1; return std::make_pair(static_cast(sign * mantissa), exponent); @@ -801,11 +805,11 @@ to_string(MantissaRange::MantissaScale const& scale) switch (scale) { case MantissaRange::MantissaScale::Small: - return "Small"; + return "small"; case MantissaRange::MantissaScale::LargeLegacy: - return "LargeLegacy"; + return "largeLegacy"; case MantissaRange::MantissaScale::Large: - return "Large"; + return "large"; default: throw std::runtime_error("Bad scale"); } diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 1d89b05893..608c7b9a42 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -10,9 +10,11 @@ #include #include #include +#include #include #include #include +#include #include #ifdef _MSC_VER @@ -34,18 +36,18 @@ thread_local std::reference_wrapper Number::kRANGE = std::set const& MantissaRange::getAllScales() { - static std::set const scales = { + static std::set const kSCALES = { MantissaRange::MantissaScale::Small, MantissaRange::MantissaScale::LargeLegacy, MantissaRange::MantissaScale::Large, }; - return scales; + return kSCALES; } std::unordered_map const& MantissaRange::getRanges() { - static auto const map = []() { + static auto const kMAP = []() { std::unordered_map map; for (auto const scale : getAllScales()) { @@ -56,41 +58,41 @@ MantissaRange::getRanges() // created correctly, but nothing else. { [[maybe_unused]] - constexpr static MantissaRange range{MantissaRange::MantissaScale::Small}; - static_assert(isPowerOfTen(range.min)); - static_assert(range.min == 1'000'000'000'000'000LL); - static_assert(range.max == 9'999'999'999'999'999LL); - static_assert(range.log == 15); - static_assert(range.min < Number::kMAX_REP); - static_assert(range.max < Number::kMAX_REP); - static_assert(range.cuspRoundingFixEnabled == CuspRoundingFix::Disabled); + constexpr static MantissaRange kRANGE{MantissaRange::MantissaScale::Small}; + static_assert(isPowerOfTen(kRANGE.min)); + static_assert(kRANGE.min == 1'000'000'000'000'000LL); + static_assert(kRANGE.max == 9'999'999'999'999'999LL); + static_assert(kRANGE.log == 15); + static_assert(kRANGE.min < Number::kMAX_REP); + static_assert(kRANGE.max < Number::kMAX_REP); + static_assert(kRANGE.cuspRoundingFixEnabled == CuspRoundingFix::Disabled); } { [[maybe_unused]] - constexpr static MantissaRange range{MantissaRange::MantissaScale::LargeLegacy}; - static_assert(isPowerOfTen(range.min)); - static_assert(range.min == 1'000'000'000'000'000'000ULL); - static_assert(range.max == rep(9'999'999'999'999'999'999ULL)); - static_assert(range.log == 18); - static_assert(range.min < Number::kMAX_REP); - static_assert(range.max > Number::kMAX_REP); - static_assert(range.cuspRoundingFixEnabled == CuspRoundingFix::Disabled); + constexpr static MantissaRange kRANGE{MantissaRange::MantissaScale::LargeLegacy}; + static_assert(isPowerOfTen(kRANGE.min)); + static_assert(kRANGE.min == 1'000'000'000'000'000'000ULL); + static_assert(kRANGE.max == rep(9'999'999'999'999'999'999ULL)); + static_assert(kRANGE.log == 18); + static_assert(kRANGE.min < Number::kMAX_REP); + static_assert(kRANGE.max > Number::kMAX_REP); + static_assert(kRANGE.cuspRoundingFixEnabled == CuspRoundingFix::Disabled); } { [[maybe_unused]] - constexpr static MantissaRange range{MantissaRange::MantissaScale::Large}; - static_assert(isPowerOfTen(range.min)); - static_assert(range.min == 1'000'000'000'000'000'000ULL); - static_assert(range.max == rep(9'999'999'999'999'999'999ULL)); - static_assert(range.log == 18); - static_assert(range.min < Number::kMAX_REP); - static_assert(range.max > Number::kMAX_REP); - static_assert(range.cuspRoundingFixEnabled == CuspRoundingFix::Enabled); + constexpr static MantissaRange kRANGE{MantissaRange::MantissaScale::Large}; + static_assert(isPowerOfTen(kRANGE.min)); + static_assert(kRANGE.min == 1'000'000'000'000'000'000ULL); + static_assert(kRANGE.max == rep(9'999'999'999'999'999'999ULL)); + static_assert(kRANGE.log == 18); + static_assert(kRANGE.min < Number::kMAX_REP); + static_assert(kRANGE.max > Number::kMAX_REP); + static_assert(kRANGE.cuspRoundingFixEnabled == CuspRoundingFix::Enabled); } return map; }(); - return map; + return kMAP; } MantissaRange const& diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index 8040571c75..8274947e83 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -38,12 +38,20 @@ setCurrentTransactionRules(std::optional r) // Make global changes associated with the rules before the value is moved. // Push the appropriate setting, instead of having the class pull every time // the value is needed. That could get expensive fast. + + // The improved accuracy that will come from enabling large + // mantissas is a goal separate from SAV and LP. It was originally + // only tied to those two amendments to avoid needing a new + // amendment of it's own, and because they require that behavior. + // Because fixCleanup3_2_0 fixes a separate bug related to the large + // mantissas, that can take precedence and activate the large + // mantissas even in the absence of the other two amendments. bool const enableCuspRoundingFix = !r || r->enabled(fixCleanup3_2_0); - bool const enableLargeNumbers = enableCuspRoundingFix || + bool const enableVaultNumbers = enableCuspRoundingFix || (r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol)); Number::setMantissaScale( enableCuspRoundingFix ? MantissaRange::MantissaScale::Large - : enableLargeNumbers ? MantissaRange::MantissaScale::LargeLegacy + : enableVaultNumbers ? MantissaRange::MantissaScale::LargeLegacy : MantissaRange::MantissaScale::Small); *getCurrentTransactionRulesRef() = std::move(r); diff --git a/src/test/app/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp index 68208f5d36..9b80ff2414 100644 --- a/src/test/app/AMMExtended_test.cpp +++ b/src/test/app/AMMExtended_test.cpp @@ -65,7 +65,7 @@ namespace xrpl::test { /** * Tests of AMM that use offers too. */ -struct AMMExtended_test : public jtx::AMMTest +class AMMExtended_test : public jtx::AMMTest { // Use small Number mantissas for the life of this test. NumberMantissaScaleGuard const sg{xrpl::MantissaRange::MantissaScale::Small}; diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index c26816348d..351edcce51 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -31,7 +31,7 @@ class Number_test : public beast::unit_test::Suite int count = 0; for (auto it = s.rbegin(); it != s.rend(); ++it) { - if (count && count % 3 == 0) + if (count != 0 && count % 3 == 0) out.insert(out.begin(), '_'); out.insert(out.begin(), *it); ++count; @@ -222,7 +222,7 @@ public: {Number{Number::kMAX_REP}, Number{6, -1}, Number{Number::kMAX_REP / 10, 1}}, }); auto const cLargeCorrected = std::to_array({ - {Number{Number::kMAX_REP}, Number{6, -1}, Number{Number::kMAX_REP / 10 + 1, 1}}, + {Number{Number::kMAX_REP}, Number{6, -1}, Number{(Number::kMAX_REP / 10) + 1, 1}}, }); auto test = [this](auto const& c) { for (auto const& [x, y, z] : c) @@ -241,9 +241,13 @@ public: { test(cLarge); if (scale == MantissaRange::MantissaScale::LargeLegacy) + { test(cLargeLegacy); + } else + { test(cLargeCorrected); + } } { bool caught = false; @@ -1589,16 +1593,16 @@ public: NumberMantissaScaleGuard const mg{MantissaRange::MantissaScale::Large}; NumberRoundModeGuard const rg{Number::RoundingMode::Upward}; - constexpr std::int64_t aValue = 1'000'000'000'000'049'863LL; - constexpr std::int64_t bValue = 9'223'372'036'854'315'903LL; + constexpr std::int64_t kA_VALUE = 1'000'000'000'000'049'863LL; + constexpr std::int64_t kB_VALUE = 9'223'372'036'854'315'903LL; // Public conversion operator: STAmount::operator Number() const. - Number const a = aValue; - Number const b = bValue; + Number const a = kA_VALUE; + Number const b = kB_VALUE; Number const product = a * b; // Exact reference in BigInt. - BigInt const exactProduct = BigInt(aValue) * BigInt(bValue); + BigInt const exactProduct = BigInt(kA_VALUE) * BigInt(kB_VALUE); // What Number actually stored. BigInt storedValue = BigInt(product.mantissa()); @@ -1608,8 +1612,8 @@ public: BigInt const signedDifference = storedValue - exactProduct; log << "\n" - << " a = " << fmt(BigInt(aValue)) << "\n" - << " b = " << fmt(BigInt(bValue)) << "\n" + << " a = " << fmt(BigInt(kA_VALUE)) << "\n" + << " b = " << fmt(BigInt(kB_VALUE)) << "\n" << " exact a*b = " << fmt(exactProduct) << "\n" << " stored = " << fmt(storedValue) << "\n" << " stored - exact = " << fmt(signedDifference) << "\n" From 1b6047afe18e4c7625485c39c9dcfa6497fad518 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 7 May 2026 13:27:03 -0400 Subject: [PATCH 04/77] More clang-tidy changes: AMMExtended_test member and Number_test includes --- src/test/app/AMMExtended_test.cpp | 2 +- src/test/basics/Number_test.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/app/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp index 9b80ff2414..83327f94cb 100644 --- a/src/test/app/AMMExtended_test.cpp +++ b/src/test/app/AMMExtended_test.cpp @@ -68,7 +68,7 @@ namespace xrpl::test { class AMMExtended_test : public jtx::AMMTest { // Use small Number mantissas for the life of this test. - NumberMantissaScaleGuard const sg{xrpl::MantissaRange::MantissaScale::Small}; + NumberMantissaScaleGuard const sg_{xrpl::MantissaRange::MantissaScale::Small}; // For now, just disable SAV entirely, which locks in the small Number // mantissas diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 351edcce51..893bff3154 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -6,7 +6,7 @@ #include #include -#include +#include #include #include From 22d2703ce875cc7376cf6adc059960f870eb1d6e Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 7 May 2026 16:50:43 -0400 Subject: [PATCH 05/77] What is it going to take to get clang-tidy to shut up? --- src/libxrpl/protocol/Rules.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index 8274947e83..3503dc8d67 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -50,9 +50,9 @@ setCurrentTransactionRules(std::optional r) bool const enableVaultNumbers = enableCuspRoundingFix || (r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol)); Number::setMantissaScale( - enableCuspRoundingFix ? MantissaRange::MantissaScale::Large - : enableVaultNumbers ? MantissaRange::MantissaScale::LargeLegacy - : MantissaRange::MantissaScale::Small); + enableCuspRoundingFix ? MantissaRange::MantissaScale::Large + : (enableVaultNumbers ? MantissaRange::MantissaScale::LargeLegacy + : MantissaRange::MantissaScale::Small)); *getCurrentTransactionRulesRef() = std::move(r); } From cd0f49a00362c129a7ea06b3988b8fd1e89a7a0d Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 7 May 2026 17:05:10 -0400 Subject: [PATCH 06/77] Address more nitpicky AI comments --- include/xrpl/basics/Number.h | 6 ++---- src/libxrpl/protocol/Rules.cpp | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index ebdb53aa83..5026dd112a 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -121,8 +121,7 @@ private: case MantissaScale::Large: return 1'000'000'000'000'000'000ULL; default: - // Since this can never be called outside a non-constexpr - // context, this throw assures that the build fails if an + // If called in a constexpr context, this throw assures that the build fails if an // invalid scale is used. throw std::runtime_error("Unknown mantissa scale"); } @@ -139,8 +138,7 @@ private: case MantissaScale::Large: return CuspRoundingFix::Enabled; default: - // Since this can never be called outside a non-constexpr - // context, this throw assures that the build fails if an + // If called in a constexpr context, this throw assures that the build fails if an // invalid scale is used. throw std::runtime_error("Unknown mantissa scale"); } diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index 3503dc8d67..bffe3602d0 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -42,7 +42,7 @@ setCurrentTransactionRules(std::optional r) // The improved accuracy that will come from enabling large // mantissas is a goal separate from SAV and LP. It was originally // only tied to those two amendments to avoid needing a new - // amendment of it's own, and because they require that behavior. + // amendment of its own, and because they require that behavior. // Because fixCleanup3_2_0 fixes a separate bug related to the large // mantissas, that can take precedence and activate the large // mantissas even in the absence of the other two amendments. From e22938d69f46308b0eb2c28e919077799ddf807d Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 12 May 2026 18:44:05 -0400 Subject: [PATCH 07/77] AI review feedback: createGuards - Refactor the Guard decision in withTxnType into createGuards, which lives in Rules.cpp. It is physically located near setCurrentTransactionRules, and documented to explain that changes need to be synchronized. --- include/xrpl/basics/Number.h | 4 ++-- include/xrpl/protocol/Rules.h | 9 +++++++++ src/libxrpl/protocol/Rules.cpp | 32 ++++++++++++++++++++++++++++++++ src/libxrpl/tx/applySteps.cpp | 17 +++-------------- 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 5026dd112a..30cb2b73cb 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -73,7 +73,7 @@ isPowerOfTen(T value) * should be left in place, but the "Small" scale option should be removed. This * will allow for future expansion beyond 64-bits if it is ever needed. */ -struct MantissaRange +struct MantissaRange final { using rep = std::uint64_t; enum class MantissaScale { @@ -250,7 +250,7 @@ concept Integral64 = std::is_same_v || std::is_same_v saved_; }; +class NumberSO; + +void +createGuards( + Rules const& rules, + std::optional& stNumberSO, + std::optional& rulesGuard, + std::optional& mantissaScaleGuard); + } // namespace xrpl diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index bffe3602d0..483b0c282e 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -46,6 +47,9 @@ setCurrentTransactionRules(std::optional r) // Because fixCleanup3_2_0 fixes a separate bug related to the large // mantissas, that can take precedence and activate the large // mantissas even in the absence of the other two amendments. + // + // If any new conditions with new amendments are added, those amendments must also be added to + // createGuards. bool const enableCuspRoundingFix = !r || r->enabled(fixCleanup3_2_0); bool const enableVaultNumbers = enableCuspRoundingFix || (r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol)); @@ -57,6 +61,34 @@ setCurrentTransactionRules(std::optional r) *getCurrentTransactionRulesRef() = std::move(r); } +void +createGuards( + Rules const& rules, + std::optional& stNumberSO, + std::optional& rulesGuard, + std::optional& mantissaScaleGuard) +{ + // The list amendments used to decide which guard(s) to create must be a superset of the list + // used to figure out which mantissa scale to use in setCurrentTransactionRules. Additional + // amendments can be added if desired. + // + // As soon as any one of these amendments is retired, this whole function can be removed, and + // the first set of guards can be created directly at the call site, without using optional. + if (rules.enabled(fixCleanup3_2_0) || rules.enabled(featureSingleAssetVault) || + rules.enabled(featureLendingProtocol)) + { + // raii classes for the current ledger rules. + // fixUniversalNumber predates the rulesGuard and should be replaced. + stNumberSO.emplace(rules.enabled(fixUniversalNumber)); + rulesGuard.emplace(rules); + } + else + { + // Without those features enabled, always use the old number rules. + mantissaScaleGuard.emplace(MantissaRange::MantissaScale::Small); + } +} + class Rules::Impl { private: diff --git a/src/libxrpl/tx/applySteps.cpp b/src/libxrpl/tx/applySteps.cpp index 99b3425275..4ac6f49f13 100644 --- a/src/libxrpl/tx/applySteps.cpp +++ b/src/libxrpl/tx/applySteps.cpp @@ -66,26 +66,15 @@ withTxnType(Rules const& rules, TxType txnType, F&& f) // so these need to be more global. // // To prevent unintentional side effects on existing checks, they will be - // set for every operation only once SingleAssetVault (or later - // LendingProtocol) are enabled. + // set for every operation only once at least one of the relevant amendments + // are enabled. // // See also Transactor::operator(). // std::optional stNumberSO; std::optional rulesGuard; std::optional mantissaScaleGuard; - if (rules.enabled(featureSingleAssetVault) || rules.enabled(featureLendingProtocol)) - { - // raii classes for the current ledger rules. - // fixUniversalNumber predates the rulesGuard and should be replaced. - stNumberSO.emplace(rules.enabled(fixUniversalNumber)); - rulesGuard.emplace(rules); - } - else - { - // Without those features enabled, always use the old number rules. - mantissaScaleGuard.emplace(MantissaRange::MantissaScale::Small); - } + createGuards(rules, stNumberSO, rulesGuard, mantissaScaleGuard); switch (txnType) { From 47f30c913de83984f403fe78312778f72d24cc9a Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 12 May 2026 21:26:54 -0400 Subject: [PATCH 08/77] Fix broken unit tests: EscrowToken - Some EscrowToken tests used a hard-coded list of amendments to determine whether to expect large mantissa logic. That ignored the effects of fixCleanup3_2_0, especially as applied to the previous fix affecting preflight, preclaim, etc. - Add a helper function, useRulesGuards, which will return the same decision as createGuards and setCurrentRulesImpl. Use that helper function in the relevant tests. - Also remove an #include that clang-tidy was complaining about. --- include/xrpl/protocol/Rules.h | 3 +++ src/libxrpl/protocol/Rules.cpp | 26 ++++++++++++++++++-------- src/libxrpl/tx/applySteps.cpp | 1 - src/test/app/EscrowToken_test.cpp | 6 ++---- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/include/xrpl/protocol/Rules.h b/include/xrpl/protocol/Rules.h index 449573c36a..b34e7995d3 100644 --- a/include/xrpl/protocol/Rules.h +++ b/include/xrpl/protocol/Rules.h @@ -124,6 +124,9 @@ private: class NumberSO; +bool +useRulesGuards(Rules const& rules); + void createGuards( Rules const& rules, diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index 483b0c282e..b5861a8ebd 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -53,6 +53,9 @@ setCurrentTransactionRules(std::optional r) bool const enableCuspRoundingFix = !r || r->enabled(fixCleanup3_2_0); bool const enableVaultNumbers = enableCuspRoundingFix || (r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol)); + XRPL_ASSERT( + !r || useRulesGuards(*r) == (enableCuspRoundingFix || enableVaultNumbers), + "setCurrentTransactionRules : rule decisions match"); Number::setMantissaScale( enableCuspRoundingFix ? MantissaRange::MantissaScale::Large : (enableVaultNumbers ? MantissaRange::MantissaScale::LargeLegacy @@ -61,6 +64,20 @@ setCurrentTransactionRules(std::optional r) *getCurrentTransactionRulesRef() = std::move(r); } +bool +useRulesGuards(Rules const& rules) +{ + // The list of amendments used here - to decide whether to create a RulesGuard - must be a + // superset of the list used to figure out which mantissa scale to use in + // setCurrentTransactionRules. Additional amendments can be added if desired. + // + // As soon as any one of these amendments is retired, this whole function can be removed, along + // with createGuards, and any other callers, and the first set of guards can be created directly + // at the call site, without using optional. + return rules.enabled(fixCleanup3_2_0) || rules.enabled(featureSingleAssetVault) || + rules.enabled(featureLendingProtocol); +} + void createGuards( Rules const& rules, @@ -68,14 +85,7 @@ createGuards( std::optional& rulesGuard, std::optional& mantissaScaleGuard) { - // The list amendments used to decide which guard(s) to create must be a superset of the list - // used to figure out which mantissa scale to use in setCurrentTransactionRules. Additional - // amendments can be added if desired. - // - // As soon as any one of these amendments is retired, this whole function can be removed, and - // the first set of guards can be created directly at the call site, without using optional. - if (rules.enabled(fixCleanup3_2_0) || rules.enabled(featureSingleAssetVault) || - rules.enabled(featureLendingProtocol)) + if (useRulesGuards(rules)) { // raii classes for the current ledger rules. // fixUniversalNumber predates the rulesGuard and should be replaced. diff --git a/src/libxrpl/tx/applySteps.cpp b/src/libxrpl/tx/applySteps.cpp index 4ac6f49f13..d532c8e71a 100644 --- a/src/libxrpl/tx/applySteps.cpp +++ b/src/libxrpl/tx/applySteps.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp index bf7cf90bf2..fada0ba070 100644 --- a/src/test/app/EscrowToken_test.cpp +++ b/src/test/app/EscrowToken_test.cpp @@ -572,8 +572,7 @@ struct EscrowToken_test : public beast::unit_test::Suite env(pay(gw, bob, usd(1))); env.close(); - bool const largeMantissa = - features[featureSingleAssetVault] || features[featureLendingProtocol]; + bool const largeMantissa = useRulesGuards(env.current()->rules()); // alice cannot create escrow for 1/10 iou - precision loss env(escrow::create(alice, bob, usd(1)), @@ -2136,8 +2135,7 @@ struct EscrowToken_test : public beast::unit_test::Suite env(pay(gw, bob, usd(1))); env.close(); - bool const largeMantissa = - features[featureSingleAssetVault] || features[featureLendingProtocol]; + bool const largeMantissa = useRulesGuards(env.current()->rules()); // alice cannot create escrow for 1/10 iou - precision loss env(escrow::create(alice, bob, usd(1)), From 46b946b22e9c863d1b8c93c598c11ed2640e4626 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 13 May 2026 22:32:37 -0400 Subject: [PATCH 09/77] clang-tidy: missing include --- src/test/app/EscrowToken_test.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp index fada0ba070..e56516c9ca 100644 --- a/src/test/app/EscrowToken_test.cpp +++ b/src/test/app/EscrowToken_test.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include From 69656d6b675e61ab194b6d1f44c03961bb33cc77 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 14 May 2026 18:22:39 -0400 Subject: [PATCH 10/77] clang-tidy: Avoid nested "?:" in global Rules initialization --- src/libxrpl/protocol/Rules.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index b5861a8ebd..1461734465 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -56,10 +56,16 @@ setCurrentTransactionRules(std::optional r) XRPL_ASSERT( !r || useRulesGuards(*r) == (enableCuspRoundingFix || enableVaultNumbers), "setCurrentTransactionRules : rule decisions match"); - Number::setMantissaScale( - enableCuspRoundingFix ? MantissaRange::MantissaScale::Large - : (enableVaultNumbers ? MantissaRange::MantissaScale::LargeLegacy - : MantissaRange::MantissaScale::Small)); + + // Declare the range this way to keep clang-tidy from complaining + auto const range = [enableCuspRoundingFix, enableVaultNumbers]() { + if (enableCuspRoundingFix) + return MantissaRange::MantissaScale::Large; + else if (enableVaultNumbers) + return MantissaRange::MantissaScale::LargeLegacy; + return MantissaRange::MantissaScale::Small; + }(); + Number::setMantissaRange(range); *getCurrentTransactionRulesRef() = std::move(r); } From b69b9242e23de59d1ac8b05dd54a8b8ae1db6922 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 14 May 2026 20:33:15 -0400 Subject: [PATCH 11/77] fixup! clang-tidy: Avoid nested "?:" in global Rules initialization --- src/libxrpl/protocol/Rules.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index 1461734465..0df0376f9a 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -65,7 +65,7 @@ setCurrentTransactionRules(std::optional r) return MantissaRange::MantissaScale::LargeLegacy; return MantissaRange::MantissaScale::Small; }(); - Number::setMantissaRange(range); + Number::setMantissaScale(range); *getCurrentTransactionRulesRef() = std::move(r); } From ddfb7ee69c57d45cf73b4fd71f79736acfd52c89 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 14 May 2026 20:48:34 -0400 Subject: [PATCH 12/77] clang-tidy: {} --- src/libxrpl/protocol/Rules.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index 0df0376f9a..3b4961832b 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -60,9 +60,13 @@ setCurrentTransactionRules(std::optional r) // Declare the range this way to keep clang-tidy from complaining auto const range = [enableCuspRoundingFix, enableVaultNumbers]() { if (enableCuspRoundingFix) + { return MantissaRange::MantissaScale::Large; + } else if (enableVaultNumbers) + { return MantissaRange::MantissaScale::LargeLegacy; + } return MantissaRange::MantissaScale::Small; }(); Number::setMantissaScale(range); From 70c6e01d7e9373ba9721458d5072382d89001e4b Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 14 May 2026 21:02:49 -0400 Subject: [PATCH 13/77] clang-tidy: this is ridiculous --- src/libxrpl/protocol/Rules.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index 3b4961832b..eb468c3bc5 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -63,7 +63,7 @@ setCurrentTransactionRules(std::optional r) { return MantissaRange::MantissaScale::Large; } - else if (enableVaultNumbers) + if (enableVaultNumbers) { return MantissaRange::MantissaScale::LargeLegacy; } From 71cf996fc60f943f49b48c290fca2472ffb67719 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 20 May 2026 23:28:26 +0100 Subject: [PATCH 14/77] Review feedback from @tapanito: lambda checks condition in doRoundUp --- src/libxrpl/basics/Number.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 191cd6421e..56fe79e1d3 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -380,11 +380,14 @@ Number::Guard::doRoundUp( auto r = round(); if (r == 1 || (r == 0 && (mantissa & 1) == 1)) { + auto const safeToIncrement = [&maxMantissa](auto const& mantissa) { + return mantissa < maxMantissa && mantissa < kMaxRep; + }; if (cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled) { // Ensure mantissa after incrementing fits within both the // min/maxMantissa range and is a valid "rep". - if (mantissa < maxMantissa && mantissa < kMaxRep) + if (safeToIncrement(mantissa)) { // Nothing unusual here, just increment the mantissa ++mantissa; @@ -398,7 +401,7 @@ Number::Guard::doRoundUp( // change of bringing it back over. doDropDigit(mantissa, exponent); XRPL_ASSERT_PARTS( - mantissa < maxMantissa && mantissa < kMaxRep, + safeToIncrement(mantissa), "xrpl::Number::Guard::doRoundUp", "can't recurse more than once"); // Here be dragons From 8b56749ca33ff84589540c3fd4a747d4afcd5c4a Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 20 May 2026 18:39:46 -0400 Subject: [PATCH 15/77] Apply suggestions from Copilot code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- include/xrpl/basics/Number.h | 2 +- src/libxrpl/protocol/Rules.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 3357607d60..aaecb5d5b5 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -65,7 +65,7 @@ isPowerOfTen(T value) * by an STAmount - IOUs, XRP, and MPTs. It has a min value of 10^18, and a max * value of 10^19-1. "LargeLegacy" is like "Large", but preserves * a rounding error when a computation results in a mantissa of - * Number::kMAX_REP that needs to be rounded up, but rounds down + * Number::kMaxRep that needs to be rounded up, but rounds down * instead. It will maintain consistent behavior until the fixCleanup3_2_0 * amendment is enabled. * diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index eb468c3bc5..7f8978778a 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -49,7 +49,7 @@ setCurrentTransactionRules(std::optional r) // mantissas even in the absence of the other two amendments. // // If any new conditions with new amendments are added, those amendments must also be added to - // createGuards. + // useRulesGuards. bool const enableCuspRoundingFix = !r || r->enabled(fixCleanup3_2_0); bool const enableVaultNumbers = enableCuspRoundingFix || (r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol)); From aea19df3c1831af1f34eff8104ff478461e3c53d Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 20 May 2026 18:46:17 -0400 Subject: [PATCH 16/77] Apply suggestions from @Tapanito code review Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com> --- include/xrpl/basics/Number.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index aaecb5d5b5..b5a11d2ab6 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -123,7 +123,7 @@ private: default: // If called in a constexpr context, this throw assures that the build fails if an // invalid scale is used. - throw std::runtime_error("Unknown mantissa scale"); + throw std::runtime_error("Unknown mantissa scale"); //LCOV_EXCL_LINE } } @@ -140,7 +140,7 @@ private: default: // If called in a constexpr context, this throw assures that the build fails if an // invalid scale is used. - throw std::runtime_error("Unknown mantissa scale"); + throw std::runtime_error("Unknown mantissa scale"); //LCOV_EXCL_LINE } } From 3a4b92b0505625a4a4230cb5e2a42f84714d528a Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 21 May 2026 00:12:31 +0100 Subject: [PATCH 17/77] Change the priority of the amendments for large mantissas - Order the checks so that large mantissa is only enabled if SAV or LP are enabled. fixCleanup3_2_0 only enables the rounding fix. - Fix tests, and don't exclude fixCleanup3_2_0 in AMM tests - Also fix formatting --- include/xrpl/basics/Number.h | 4 ++-- src/libxrpl/protocol/Rules.cpp | 20 ++++++-------------- src/test/app/AMMClawbackMPT_test.cpp | 2 +- src/test/app/AMMClawback_test.cpp | 4 ++-- src/test/app/AMMExtended_test.cpp | 2 +- src/test/app/AMM_test.cpp | 3 +-- src/test/app/EscrowToken_test.cpp | 6 ++++-- 7 files changed, 17 insertions(+), 24 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index b5a11d2ab6..3aeb4b5bcd 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -123,7 +123,7 @@ private: default: // If called in a constexpr context, this throw assures that the build fails if an // invalid scale is used. - throw std::runtime_error("Unknown mantissa scale"); //LCOV_EXCL_LINE + throw std::runtime_error("Unknown mantissa scale"); // LCOV_EXCL_LINE } } @@ -140,7 +140,7 @@ private: default: // If called in a constexpr context, this throw assures that the build fails if an // invalid scale is used. - throw std::runtime_error("Unknown mantissa scale"); //LCOV_EXCL_LINE + throw std::runtime_error("Unknown mantissa scale"); // LCOV_EXCL_LINE } } diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index 7f8978778a..08a95145eb 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -40,31 +40,23 @@ setCurrentTransactionRules(std::optional r) // Push the appropriate setting, instead of having the class pull every time // the value is needed. That could get expensive fast. - // The improved accuracy that will come from enabling large - // mantissas is a goal separate from SAV and LP. It was originally - // only tied to those two amendments to avoid needing a new - // amendment of its own, and because they require that behavior. - // Because fixCleanup3_2_0 fixes a separate bug related to the large - // mantissas, that can take precedence and activate the large - // mantissas even in the absence of the other two amendments. - // // If any new conditions with new amendments are added, those amendments must also be added to // useRulesGuards. + bool const enableVaultNumbers = + !r || (r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol)); bool const enableCuspRoundingFix = !r || r->enabled(fixCleanup3_2_0); - bool const enableVaultNumbers = enableCuspRoundingFix || - (r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol)); XRPL_ASSERT( !r || useRulesGuards(*r) == (enableCuspRoundingFix || enableVaultNumbers), "setCurrentTransactionRules : rule decisions match"); // Declare the range this way to keep clang-tidy from complaining auto const range = [enableCuspRoundingFix, enableVaultNumbers]() { - if (enableCuspRoundingFix) - { - return MantissaRange::MantissaScale::Large; - } if (enableVaultNumbers) { + if (enableCuspRoundingFix) + { + return MantissaRange::MantissaScale::Large; + } return MantissaRange::MantissaScale::LargeLegacy; } return MantissaRange::MantissaScale::Small; diff --git a/src/test/app/AMMClawbackMPT_test.cpp b/src/test/app/AMMClawbackMPT_test.cpp index f80b68530f..6facafde4a 100644 --- a/src/test/app/AMMClawbackMPT_test.cpp +++ b/src/test/app/AMMClawbackMPT_test.cpp @@ -1829,7 +1829,7 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite testLastHolderLPTokenBalance(all - fixAMMv1_3 - fixAMMClawbackRounding); testLastHolderLPTokenBalance( all - fixAMMv1_3 - fixAMMClawbackRounding - featureSingleAssetVault - - featureLendingProtocol - fixCleanup3_2_0); + featureLendingProtocol); testLastHolderLPTokenBalance(all - fixAMMClawbackRounding); testClawAssetCheck(all); } diff --git a/src/test/app/AMMClawback_test.cpp b/src/test/app/AMMClawback_test.cpp index 500fd84ebe..9683e8ac17 100644 --- a/src/test/app/AMMClawback_test.cpp +++ b/src/test/app/AMMClawback_test.cpp @@ -2506,8 +2506,8 @@ class AMMClawback_test : public beast::unit_test::Suite { // For now, just disable SAV entirely, which locks in the small Number // mantissas - FeatureBitset const all = jtx::testableAmendments() - featureSingleAssetVault - - featureLendingProtocol - fixCleanup3_2_0; + FeatureBitset const all = + jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol; testInvalidRequest(all); testInvalidRequest(all - featureMPTokensV2); diff --git a/src/test/app/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp index d67382600d..a0a7d0fb15 100644 --- a/src/test/app/AMMExtended_test.cpp +++ b/src/test/app/AMMExtended_test.cpp @@ -73,7 +73,7 @@ class AMMExtended_test : public jtx::AMMTest // For now, just disable SAV entirely, which locks in the small Number // mantissas FeatureBitset const all_{ - testableAmendments() - featureSingleAssetVault - featureLendingProtocol - fixCleanup3_2_0}; + testableAmendments() - featureSingleAssetVault - featureLendingProtocol}; private: void diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index 9aa67309e4..2c63ff6d61 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -82,8 +82,7 @@ private: { // For now, just disable SAV entirely, which locks in the small Number // mantissas - return jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol - - fixCleanup3_2_0; + return jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol; } void diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp index 38f86ee5b4..bac2f798ad 100644 --- a/src/test/app/EscrowToken_test.cpp +++ b/src/test/app/EscrowToken_test.cpp @@ -573,7 +573,8 @@ struct EscrowToken_test : public beast::unit_test::Suite env(pay(gw, bob, usd(1))); env.close(); - bool const largeMantissa = useRulesGuards(env.current()->rules()); + bool const largeMantissa = + features[featureSingleAssetVault] || features[featureLendingProtocol]; // alice cannot create escrow for 1/10 iou - precision loss env(escrow::create(alice, bob, usd(1)), @@ -2200,7 +2201,8 @@ struct EscrowToken_test : public beast::unit_test::Suite env(pay(gw, bob, usd(1))); env.close(); - bool const largeMantissa = useRulesGuards(env.current()->rules()); + bool const largeMantissa = + features[featureSingleAssetVault] || features[featureLendingProtocol]; // alice cannot create escrow for 1/10 iou - precision loss env(escrow::create(alice, bob, usd(1)), From 42fda85fbc602d3fb296abdd5b76057635b97c86 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 21 May 2026 12:02:42 +0100 Subject: [PATCH 18/77] Fix more AMM tests, and to not exclude fixCleanup3_2_0 --- src/libxrpl/basics/Number.cpp | 1 - src/test/jtx/AMMTest.h | 6 ++---- src/test/jtx/impl/AMMTest.cpp | 2 +- src/test/rpc/GetAggregatePrice_test.cpp | 3 +-- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 56fe79e1d3..11f5934b04 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -404,7 +404,6 @@ Number::Guard::doRoundUp( safeToIncrement(mantissa), "xrpl::Number::Guard::doRoundUp", "can't recurse more than once"); - // Here be dragons doRoundUp( negative, mantissa, diff --git a/src/test/jtx/AMMTest.h b/src/test/jtx/AMMTest.h index fc8cd1b1e6..a2e6c38db3 100644 --- a/src/test/jtx/AMMTest.h +++ b/src/test/jtx/AMMTest.h @@ -21,8 +21,7 @@ struct TestAMMArg std::vector features = { // For now, just disable SAV entirely, which locks in the small Number // mantissas - jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol - - fixCleanup3_2_0}; + jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol}; bool noLog = false; }; @@ -88,8 +87,7 @@ public: { // For now, just disable SAV entirely, which locks in the small Number // mantissas - return jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol - - fixCleanup3_2_0; + return jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol; } protected: diff --git a/src/test/jtx/impl/AMMTest.cpp b/src/test/jtx/impl/AMMTest.cpp index 3bb4cca484..5801e8fb44 100644 --- a/src/test/jtx/impl/AMMTest.cpp +++ b/src/test/jtx/impl/AMMTest.cpp @@ -143,7 +143,7 @@ AMMTestBase::testAMM(std::function const& cb, TestAM // mantissas Env env{ *this, - features - featureSingleAssetVault - featureLendingProtocol - fixCleanup3_2_0, + features - featureSingleAssetVault - featureLendingProtocol, arg.noLog ? std::make_unique(&logs) : nullptr}; auto const [asset1, asset2] = arg.pool ? *arg.pool : std::make_pair(XRP(10000), USD(10000)); diff --git a/src/test/rpc/GetAggregatePrice_test.cpp b/src/test/rpc/GetAggregatePrice_test.cpp index cfa8ce78d4..37ecc54172 100644 --- a/src/test/rpc/GetAggregatePrice_test.cpp +++ b/src/test/rpc/GetAggregatePrice_test.cpp @@ -175,8 +175,7 @@ public: // or time threshold { auto const all = testableAmendments(); - for (auto const& feats : - {all - featureSingleAssetVault - featureLendingProtocol - fixCleanup3_2_0, all}) + for (auto const& feats : {all - featureSingleAssetVault - featureLendingProtocol, all}) { for (auto const mantissaSize : MantissaRange::getAllScales()) { From 48b1716e6f5a2bcb80f40d9bddebaadb45ccb2f0 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 23 May 2026 18:45:36 +0100 Subject: [PATCH 19/77] Make Number::operator/= significantly more accurate - Prevents extreme dust rounding from getting lost, especially when rounding away from zero. (Upward for positive, downward for negative.) --- src/libxrpl/basics/Number.cpp | 55 +++++++++---- src/test/basics/Number_test.cpp | 137 ++++++++++++++++++++++++++------ 2 files changed, 149 insertions(+), 43 deletions(-) diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 11f5934b04..0a22d439ea 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -858,31 +858,51 @@ Number::operator/=(Number const& y) auto const& maxMantissa = range.max; auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled; + // Cache power of 10 computations to not waste time on subsequent calls + auto getPower10 = [](int exponent) { + static std::unordered_map powersOf10; + if (!powersOf10.contains(exponent)) + { + uint128_t result = 1; + for (int i = 0; i < exponent; ++i) + result *= 10; + powersOf10[exponent] = result; + } + return powersOf10.at(exponent); + }; + // Shift by 10^17 gives greatest precision while not overflowing - // uint128_t or the cast back to int64_t - // TODO: Can/should this be made bigger for largeRange? - // log(2^128,10) ~ 38.5 - // largeRange.log = 18, fits in 10^19 - // f can be up to 10^(38-19) = 10^19 safely + // uint128_t or the cast back to int64_t for small mantissas. + // + // For large mantissas: + // * log(2^128,10) ~ 38.5 + // * largeRange.log = 18, fits in 10^19 + // * The expanded numerator must fit in 10^38 + // * f can be up to 10^(38-19) = 10^19 safely bool const small = Number::getMantissaScale() == MantissaRange::MantissaScale::Small; - uint128_t const f = small ? 100'000'000'000'000'000 : 10'000'000'000'000'000'000ULL; + auto const factorExponent = small ? 17 : 19; + + uint128_t const f = getPower10(factorExponent); XRPL_ASSERT_PARTS(f >= minMantissa * 10, "Number::operator/=", "factor expected size"); // unsigned denominator auto const dmu = static_cast(dm); - // correctionFactor can be anything between 10 and f, depending on how much - // extra precision we want to only use for rounding with the - // largeRange. Three digits seems like plenty, and is more than - // the smallRange uses. - uint128_t const correctionFactor = 1'000; - auto const numerator = uint128_t(nm) * f; auto zm = numerator / dmu; - auto ze = ne - de - (small ? 17 : 19); + auto ze = ne - de - factorExponent; bool zn = (ns * ds) < 0; if (!small) { + // zm may now hold up to 20 digits. The correctionFactor must be small enough to not cause + // overflow, but as large as possible otherwise + // * zm fits in 10^21 + // * correctionFactor can be up to 10^(38-21) = 10^17 safely + bool const useBigCorrection = + cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled; + auto const correctionExponent = useBigCorrection ? 17 : 3; + uint128_t const correctionFactor = getPower10(correctionExponent); + // Virtually multiply numerator by correctionFactor. Since that would // overflow in the existing uint128_t, we'll do that part separately. // The math for this would work for small mantissas, but we need to @@ -906,11 +926,12 @@ Number::operator/=(Number const& y) if (remainder != 0) { zm *= correctionFactor; - auto const correction = remainder * correctionFactor / dmu; - zm += correction; - // divide by 1000 by moving the exponent, so we don't lose the + // divide by the correctionFactor by moving the exponent, so we don't lose the // integer value we just computed - ze -= 3; + ze -= correctionExponent; + + auto const correction = (remainder * correctionFactor) / dmu; + zm += correction; } } normalize(zn, zm, ze, minMantissa, maxMantissa, cuspRoundingFixEnabled); diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 52af19ed36..9a80caa8e4 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -6,10 +6,12 @@ #include #include +#include #include #include #include +#include #include #include #include @@ -39,6 +41,30 @@ class Number_test : public beast::unit_test::Suite return out; } + using dec = boost::multiprecision::cpp_dec_float_50; + + template + static T + pow10(int n) + { + T p = 1; + if (n >= 0) + for (int i = 0; i < n; ++i) + p *= 10; + else + for (int i = 0; i < -n; ++i) + p /= 10; + return p; + } + + static std::string + fmt(dec const& v) + { + std::ostringstream os; + os << std::setprecision(40) << v; + return os.str(); + } + public: void testZero() @@ -1588,40 +1614,99 @@ public: void testUpwardRoundsDown() { - testcase << "upward rounding produces a value below exact at kMaxRep cusp"; + { + testcase << "upward rounding produces a value below exact at kMaxRep cusp"; - NumberMantissaScaleGuard const mg{MantissaRange::MantissaScale::Large}; - NumberRoundModeGuard const rg{Number::RoundingMode::Upward}; + NumberMantissaScaleGuard const mg{MantissaRange::MantissaScale::Large}; + NumberRoundModeGuard const rg{Number::RoundingMode::Upward}; - constexpr std::int64_t kAValue = 1'000'000'000'000'049'863LL; - constexpr std::int64_t kBValue = 9'223'372'036'854'315'903LL; + constexpr std::int64_t kAValue = 1'000'000'000'000'049'863LL; + constexpr std::int64_t kBValue = 9'223'372'036'854'315'903LL; - // Public conversion operator: STAmount::operator Number() const. - Number const a = kAValue; - Number const b = kBValue; - Number const product = a * b; + // Public conversion operator: STAmount::operator Number() const. + Number const a = kAValue; + Number const b = kBValue; + Number const product = a * b; - // Exact reference in BigInt. - BigInt const exactProduct = BigInt(kAValue) * BigInt(kBValue); + // Exact reference in BigInt. + BigInt const exactProduct = BigInt(kAValue) * BigInt(kBValue); - // What Number actually stored. - BigInt storedValue = BigInt(product.mantissa()); - for (int i = 0; i < product.exponent(); ++i) - storedValue *= 10; + // What Number actually stored. + BigInt storedValue = BigInt(product.mantissa()); + for (int i = 0; i < product.exponent(); ++i) + storedValue *= 10; - BigInt const signedDifference = storedValue - exactProduct; + BigInt const signedDifference = storedValue - exactProduct; - log << "\n" - << " a = " << fmt(BigInt(kAValue)) << "\n" - << " b = " << fmt(BigInt(kBValue)) << "\n" - << " exact a*b = " << fmt(exactProduct) << "\n" - << " stored = " << fmt(storedValue) << "\n" - << " stored - exact = " << fmt(signedDifference) << "\n" - << " upward = " << (signedDifference >= 0 ? "held" : "VIOLATED") << "\n"; + log << "\n" + << " a = " << fmt(BigInt(kAValue)) << "\n" + << " b = " << fmt(BigInt(kBValue)) << "\n" + << " exact a*b = " << fmt(exactProduct) << "\n" + << " stored = " << fmt(storedValue) << "\n" + << " stored - exact = " << fmt(signedDifference) << "\n" + << " upward = " << (signedDifference >= 0 ? "held" : "VIOLATED") << "\n"; - BEAST_EXPECT(signedDifference >= 0); - BEAST_EXPECT(product.mantissa() == (std::numeric_limits::max() / 10) + 1); - BEAST_EXPECT(product.exponent() == 19); + BEAST_EXPECT(signedDifference >= 0); + BEAST_EXPECT(signedDifference < pow10(product.exponent())); + BEAST_EXPECT(product.mantissa() == (std::numeric_limits::max() / 10) + 1); + BEAST_EXPECT(product.exponent() == 19); + log.flush(); + } + + { + /* Companion to NumberUpwardWrongDirection_test (which targets + * `operator*=` Upward at the kMaxRep cusp on LargeLegacy), but for + * `operator/=` on the cusp-fix-ENABLED `Large` scale. + * + * Under `Large` (`CuspRoundingFix::Enabled`), `operator/=` with Upward + * rounding can return a value STRICTLY LESS than the exact quotient, + * violating Upward's directional invariant. + * + * Mechanism (fix-enabled path): + * 1. `operator/=` computes `numerator = nm * 10^19` and + * `zm = numerator / dm` (integer division, truncates remainder). + * 2. If `remainder != 0`, the correction block runs: + * zm *= 1000 + * correction = (remainder * 1000) / dm // also truncates + * zm += correction + * ze -= 3 + * The truncation in `correction` discards a sub-1/1000 residual. + * 3. `normalize`'s shift loop reduces zm to fit, but the discarded + * residual is BELOW the Guard's visibility, so the Guard sees fraction = 0. + * 4. Under Upward + positive, `round()` returns -1 (no round-up), and + * the algorithm returns the truncated zm + */ + testcase << "operator/= Upward on Large returns value < truth "; + + NumberMantissaScaleGuard const scaleGuard{MantissaRange::MantissaScale::Large}; + NumberRoundModeGuard const roundGuard{Number::RoundingMode::Upward}; + + constexpr std::int64_t aValue = 2LL; + constexpr std::int64_t bValue = 1'000'000'000'000'000'007LL; + // bValue = 10^18 + 7 (prime, in [minMantissa, kMaxRep]). + + Number const a{aValue, 0}; + Number const b{bValue, 0}; + Number const quotient = a / b; + + dec const exact = dec(aValue) / dec(bValue); + dec const stored = dec(quotient.mantissa()) * pow10(quotient.exponent()); + dec const diff = stored - exact; + + log << "\n" + << " a = " << aValue << "\n" + << " b = " << bValue << "\n" + << " exact a/b = " << fmt(exact) << "\n" + << " stored a/b = " << fmt(stored) << "\n" + << " stored - exact = " << fmt(diff) + << " (negative => Upward gave value BELOW truth)\n" + << " quotient.mantissa = " << quotient.mantissa() << "\n" + << " quotient.exponent = " << quotient.exponent() << "\n"; + + // Upward invariant: stored >= exact. Bug: stored < exact. + BEAST_EXPECT(stored >= exact); + BEAST_EXPECT(diff < pow10(quotient.exponent())); + } } void From 84ca271d95dfaa82fa6dfcc11647de532132765f Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 26 May 2026 13:45:55 -0400 Subject: [PATCH 20/77] Address some AI review feedback: predeclare, include, format, comment - Predeclare type reference in Rules.h - Remove an unneeded include in EscrowToken_test - Number_test will format negative BigInts correctly (unused) - Remove an inaccurate comment --- include/xrpl/protocol/Rules.h | 1 + src/test/app/EscrowToken_test.cpp | 1 - src/test/basics/Number_test.cpp | 3 +-- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/include/xrpl/protocol/Rules.h b/include/xrpl/protocol/Rules.h index b34e7995d3..9c17ff2391 100644 --- a/include/xrpl/protocol/Rules.h +++ b/include/xrpl/protocol/Rules.h @@ -123,6 +123,7 @@ private: }; class NumberSO; +class NumberMantissaScaleGuard; bool useRulesGuards(Rules const& rules); diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp index bac2f798ad..5bb1303dba 100644 --- a/src/test/app/EscrowToken_test.cpp +++ b/src/test/app/EscrowToken_test.cpp @@ -28,7 +28,6 @@ #include #include #include -#include #include #include #include diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 52af19ed36..243bdb017b 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -31,7 +31,7 @@ class Number_test : public beast::unit_test::Suite int count = 0; for (auto it = s.rbegin(); it != s.rend(); ++it) { - if (count != 0 && count % 3 == 0) + if (count != 0 && count % 3 == 0 && isdigit(*it)) out.insert(out.begin(), '_'); out.insert(out.begin(), *it); ++count; @@ -1596,7 +1596,6 @@ public: constexpr std::int64_t kAValue = 1'000'000'000'000'049'863LL; constexpr std::int64_t kBValue = 9'223'372'036'854'315'903LL; - // Public conversion operator: STAmount::operator Number() const. Number const a = kAValue; Number const b = kBValue; Number const product = a * b; From fbee0349f53f76464e97e44f659007a5fa23e0cf Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 26 May 2026 15:21:42 -0400 Subject: [PATCH 21/77] clang-tidy: implicit bool conversion --- src/test/basics/Number_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 243bdb017b..7d04022a19 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -31,7 +31,7 @@ class Number_test : public beast::unit_test::Suite int count = 0; for (auto it = s.rbegin(); it != s.rend(); ++it) { - if (count != 0 && count % 3 == 0 && isdigit(*it)) + if (count != 0 && count % 3 == 0 && (isdigit(*it) != 0)) out.insert(out.begin(), '_'); out.insert(out.begin(), *it); ++count; From d6844311c0b87a9280cfd04ade7375cc7f587899 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 26 May 2026 15:48:29 -0400 Subject: [PATCH 22/77] clang-tidy: missing header --- src/test/basics/Number_test.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 7d04022a19..26060c70e9 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include From 27456fa439ef283bfa2f2304640f725a804f69c3 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 26 May 2026 15:50:31 -0400 Subject: [PATCH 23/77] Use the local range instead of calling a function --- src/libxrpl/basics/Number.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 0a22d439ea..cd89e163d3 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -879,7 +879,7 @@ Number::operator/=(Number const& y) // * largeRange.log = 18, fits in 10^19 // * The expanded numerator must fit in 10^38 // * f can be up to 10^(38-19) = 10^19 safely - bool const small = Number::getMantissaScale() == MantissaRange::MantissaScale::Small; + bool const small = range.scale == MantissaRange::MantissaScale::Small; auto const factorExponent = small ? 17 : 19; uint128_t const f = getPower10(factorExponent); From 5abecb9fcb8f91aff466b58b95af09d2c0aa7742 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 27 May 2026 00:07:48 -0400 Subject: [PATCH 24/77] Significant rewrite - Simplify Number::operator/= to use more constexpr values, and fewer variations. - Most significantly, rounding up doesn't need more precision, it only needs to know if there's a remainder after the current precision work is done. Tracked similarly Guard::xbit_. - Build a constexpr lookup array for powers of 10. Only a handful of values are used, but since it's built at compile time, and constexpr, unused values do not affect memory or performance. --- include/xrpl/basics/Number.h | 87 +++++++++++++--- src/libxrpl/basics/Number.cpp | 169 ++++++++++++++++++++++---------- src/test/basics/Number_test.cpp | 4 + 3 files changed, 194 insertions(+), 66 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 3aeb4b5bcd..37899a1cf4 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -40,6 +40,47 @@ isPowerOfTen(T value) return logTen(value).has_value(); } +namespace detail { + +/** Builds a table of the powers of 10 + * + * This function is marked consteval, so it can only be run in + * a constexpr context. This assures that it is and can only be run at + * compile time. Doing it at runtime would be pretty wasteful and + * inefficient. + */ +constexpr std::size_t int64digits = 20; +consteval std::array +buildPowersOfTen() +{ + std::array result; + + std::uint64_t power = 1; + std::size_t exponent = 0; + // end the loop early so it doesn't overflow; + for (; exponent < result.size() - 1; ++exponent, power *= 10) + { + result[exponent] = power; + if (power > std::numeric_limits::max() / 10) + throw std::logic_error("Power of 10 table is too big"); + } + result[exponent] = power; + if (power < std::numeric_limits::max() / 10) + throw std::logic_error("Power of 10 table is not big enough for the uint64_t type"); + + return result; +} + +} // namespace detail + +constexpr std::array kPowerOfTen = detail::buildPowersOfTen(); + +static_assert(kPowerOfTen[0] == 1); +static_assert(kPowerOfTen[1] == 10); +static_assert(kPowerOfTen[10] == 10'000'000'000); +static_assert( + isPowerOfTen(kPowerOfTen.back()) && *logTen(kPowerOfTen.back()) == detail::int64digits - 1); + /** MantissaRange defines a range for the mantissa of a normalized Number. * * The mantissa is in the range [min, max], where @@ -76,6 +117,7 @@ isPowerOfTen(T value) struct MantissaRange final { using rep = std::uint64_t; + enum class MantissaScale { Small, // LargeLegacy can be removed when fixCleanup3_2_0 is retired @@ -89,19 +131,15 @@ struct MantissaRange final Enabled = true, }; - explicit constexpr MantissaRange(MantissaScale scale) - : min(getMin(scale)) - , cuspRoundingFixEnabled(isCuspFixEnabled(scale)) - , log(logTen(min).value_or(-1)) - , scale(scale) + explicit constexpr MantissaRange(MantissaScale sc) : scale(sc) { } - rep min; - rep max{(min * 10) - 1}; - CuspRoundingFix cuspRoundingFixEnabled; - int log; - MantissaScale scale; + MantissaScale const scale; + int const log{getExponent(scale)}; + rep const min{getMin(scale, log)}; + rep const max{(min * 10) - 1}; + CuspRoundingFix const cuspRoundingFixEnabled{isCuspFixEnabled(scale)}; static MantissaRange const& getMantissaRange(MantissaScale scale); @@ -110,16 +148,34 @@ struct MantissaRange final getAllScales(); private: - static constexpr rep - getMin(MantissaScale scale) + static constexpr int + getExponent(MantissaScale scale) { switch (scale) { case MantissaScale::Small: - return 1'000'000'000'000'000ULL; + return 15; case MantissaScale::LargeLegacy: case MantissaScale::Large: - return 1'000'000'000'000'000'000ULL; + return 18; + default: + // If called in a constexpr context, this throw assures that the build fails if an + // invalid scale is used. + throw std::runtime_error("Unknown mantissa scale"); // LCOV_EXCL_LINE + } + } + + // Keep this function for future use with different ways to compute + // the ranges. + static constexpr rep + getMin(MantissaScale scale, int exponent) + { + switch (scale) + { + case MantissaScale::Small: + case MantissaScale::LargeLegacy: + case MantissaScale::Large: + return kPowerOfTen[exponent]; default: // If called in a constexpr context, this throw assures that the build fails if an // invalid scale is used. @@ -519,7 +575,8 @@ private: int& exponent, MantissaRange::rep const& minMantissa, MantissaRange::rep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled); + MantissaRange::CuspRoundingFix cuspRoundingFixEnabled, + bool dropped); [[nodiscard]] bool isnormal() const noexcept; diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index cd89e163d3..cd2d213e29 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -178,6 +178,10 @@ public: setPositive() noexcept; void setNegative() noexcept; + // Should only be called by doNormalize, and then only for division + // operations with remainders. + void + setDropped() noexcept; [[nodiscard]] bool isNegative() const noexcept; @@ -250,6 +254,12 @@ Number::Guard::setNegative() noexcept sbit_ = 1; } +inline void +Number::Guard::setDropped() noexcept +{ + xbit_ = 1; +} + inline bool Number::Guard::isNegative() const noexcept { @@ -512,8 +522,6 @@ Number::one() return Number{false, range.min, -range.log, Number::Unchecked{}}; } -// Use the member names in this static function for now so the diff is cleaner -// TODO: Rename the function parameters to get rid of the "_" suffix template void doNormalize( @@ -522,7 +530,8 @@ doNormalize( int& exponent, MantissaRange::rep const& minMantissa, MantissaRange::rep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled) + MantissaRange::CuspRoundingFix cuspRoundingFixEnabled, + bool dropped) { static constexpr auto kMinExponent = Number::kMinExponent; static constexpr auto kMaxExponent = Number::kMaxExponent; @@ -547,6 +556,8 @@ doNormalize( Guard g; if (negative) g.setNegative(); + if (dropped) + g.setDropped(); while (m > maxMantissa) { if (exponent >= kMaxExponent) @@ -611,7 +622,8 @@ Number::normalize( internalrep const& maxMantissa, MantissaRange::CuspRoundingFix cuspRoundingFixEnabled) { - doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled); + doNormalize( + negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false); } template <> @@ -624,7 +636,8 @@ Number::normalize( internalrep const& maxMantissa, MantissaRange::CuspRoundingFix cuspRoundingFixEnabled) { - doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled); + doNormalize( + negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false); } template <> @@ -637,7 +650,8 @@ Number::normalize( internalrep const& maxMantissa, MantissaRange::CuspRoundingFix cuspRoundingFixEnabled) { - doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled); + doNormalize( + negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false); } void @@ -858,32 +872,62 @@ Number::operator/=(Number const& y) auto const& maxMantissa = range.max; auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled; - // Cache power of 10 computations to not waste time on subsequent calls - auto getPower10 = [](int exponent) { - static std::unordered_map powersOf10; - if (!powersOf10.contains(exponent)) - { - uint128_t result = 1; - for (int i = 0; i < exponent; ++i) - result *= 10; - powersOf10[exponent] = result; - } - return powersOf10.at(exponent); - }; - - // Shift by 10^17 gives greatest precision while not overflowing - // uint128_t or the cast back to int64_t for small mantissas. + // Division operates on two large integers (16-digit for small + // mantissas, 19-digit for large) using integer math. If the values + // were just divided directly, the result would be at most 9. + // Introduce a power-of-ten multiplication factor for the numerator + // which will ensure the result has a meaningful number of digits. + // + // Consider numbers with a 2-digit mantissa: + // * Assume both numbers have an exponent of 0, using "ToNearest" rounding + // * 23 / 67 = 0 + // * Use a factor of 10^4 + // * 230'000 / 67 = 3432 with an exponent of -4 + // * The normalized result will be 34, exponent -2 + // + // The most extreme results are 10/99 and 99/10 + // * 100'000 / 99 = 1'010e-4 = 10e-2 + // * 990'000 / 10 = 99'000e-4 = 99e-1 + // + // Note that the computations give 2 or 3 digits after the + // decimal point to determine which way to round for most scenarios. + // + // For small mantissas (where the MantissaRange.log == 15), shifting by 10^17 gives sufficient + // precision while not overflowing uint128_t or the cast back to int64_t. (This is legacy + // behavior, which must not be changed.) + // + // For large mantissas (where the MantissaRange.log == 18), a shift by 10^20 would be optimal + // for most scenarios. However, larger mantissa values would overflow 2^128 (log(2^128,10) + // ~ 38.5). // - // For large mantissas: // * log(2^128,10) ~ 38.5 // * largeRange.log = 18, fits in 10^19 // * The expanded numerator must fit in 10^38 - // * f can be up to 10^(38-19) = 10^19 safely - bool const small = range.scale == MantissaRange::MantissaScale::Small; - auto const factorExponent = small ? 17 : 19; + // * f not be more than 10^(38-19) = 10^19 safely + // + // So, we do the division into stages: + // + // Stage 1: Use the same factor of 10^17, for the initial division. This + // will frequently not result in a whole number quotient. + // + // Stage 2: If there is a remainder from the first step, repeat the + // process with a "correction" factor of 10^5. Shift the + // result of Stage 1 over by 5 places, and add the second result to it. + // This is equivalent to if we had used an initial factor of 10^22, + // a couple digits more than we actually need. + // + // Stage 3: If there is still a remainder, and the CuspRoundingFix + // is enabled, pass a flag indicating such to doNormalize. The Guard + // in doNormalize will treat that flag as if non-zero digits had + // been dropped from the mantissa when shrinking it into range. + // This is only relevant when rounding away from zero (Upward for + // positive numbers, Downward for negative), or if the "regular" + // remainder is exactly 0.5 for "ToNearest". This will give the + // rounding the most accurate result possible, as if infinite + // precision was used in the initial calculation. + auto constexpr factorExponent = 17; - uint128_t const f = getPower10(factorExponent); - XRPL_ASSERT_PARTS(f >= minMantissa * 10, "Number::operator/=", "factor expected size"); + uint128_t constexpr f = kPowerOfTen[factorExponent]; // unsigned denominator auto const dmu = static_cast(dm); @@ -892,21 +936,21 @@ Number::operator/=(Number const& y) auto zm = numerator / dmu; auto ze = ne - de - factorExponent; bool zn = (ns * ds) < 0; - if (!small) - { - // zm may now hold up to 20 digits. The correctionFactor must be small enough to not cause - // overflow, but as large as possible otherwise - // * zm fits in 10^21 - // * correctionFactor can be up to 10^(38-21) = 10^17 safely - bool const useBigCorrection = - cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled; - auto const correctionExponent = useBigCorrection ? 17 : 3; - uint128_t const correctionFactor = getPower10(correctionExponent); + // dropped is used in the same way as Guard::xbit_. In the case of + // division, it indicates if there's any remainder left over after + // we have been as precise as reasonable. If there is, it would be as + // if we were using infinite precision math, and a non-zero digit + // had been shifted off the end of the result when normalizing. + bool dropped = false; - // Virtually multiply numerator by correctionFactor. Since that would - // overflow in the existing uint128_t, we'll do that part separately. + if (range.scale != MantissaRange::MantissaScale::Small) + { + // Stage 2 + // + // If there is a remainder, treat is as a secondary numerator. + // Multiply by correctionFactor separately from stage 1. // The math for this would work for small mantissas, but we need to - // preserve existing behavior. + // preserve legacy behavior. // // Consider: // ((numerator * correctionFactor) / dmu) / correctionFactor @@ -917,24 +961,47 @@ Number::operator/=(Number const& y) // // = ((numerator / dmu * correctionFactor) // + ((numerator % dmu) * correctionFactor) / dmu) / correctionFactor + // = ((zm * correctionFactor) + // + (remainder * correctionFactor) / dmu) / correctionFactor // - // We have already set `mantissa_ = numerator / dmu`. Now we - // compute `remainder = numerator % dmu`, and if it is - // nonzero, we do the rest of the arithmetic. If it's zero, we can skip - // it. + // The trick is that multiplication by correctionFactor is done on the mantissa, but + // division by correctionFactor is done by modifying the exponent, so no precision is lost + // until we normalize. + // + // If remainder is zero, we can skip this stage entirely because + // the first stage gave an exact answer. + auto constexpr correctionExponent = 5; + uint128_t constexpr correctionFactor = kPowerOfTen[correctionExponent]; + static_assert(factorExponent + correctionExponent == 22); + auto const remainder = (numerator % dmu); if (remainder != 0) { - zm *= correctionFactor; - // divide by the correctionFactor by moving the exponent, so we don't lose the - // integer value we just computed - ze -= correctionExponent; + auto const partialNumerator = remainder * correctionFactor; + auto const correction = partialNumerator / dmu; - auto const correction = (remainder * correctionFactor) / dmu; - zm += correction; + if (correction != 0) + { + zm *= correctionFactor; + // divide by the correctionFactor by moving the exponent, so we don't lose the + // integer value we just computed + ze -= correctionExponent; + + zm += correction; + } + + // Stage 3: If there's still anything left, and the cusp + // rounding fix is enabled, flag if there is still + // a remainder from stage 2. + bool const useTrailingRemainder = + cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled; + if (useTrailingRemainder) + { + dropped = partialNumerator % dmu != 0; + } } } - normalize(zn, zm, ze, minMantissa, maxMantissa, cuspRoundingFixEnabled); + doNormalize(zn, zm, ze, minMantissa, maxMantissa, cuspRoundingFixEnabled, dropped); negative_ = zn; mantissa_ = static_cast(zm); exponent_ = ze; diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index a50038363d..ddf3e35ce3 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -50,11 +50,15 @@ class Number_test : public beast::unit_test::Suite { T p = 1; if (n >= 0) + { for (int i = 0; i < n; ++i) p *= 10; + } else + { for (int i = 0; i < -n; ++i) p /= 10; + } return p; } From ae9c72bb7c7ec9e59c3502ae42f3700cfb06ef08 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 27 May 2026 00:36:38 -0400 Subject: [PATCH 25/77] Test optimization: Number_test::pow10 --- src/test/basics/Number_test.cpp | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index ddf3e35ce3..230752d37c 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -48,17 +48,23 @@ class Number_test : public beast::unit_test::Suite static T pow10(int n) { + if (n == 0) + return 1; + if (n == 1) + return 10; + + if (n > 1) + { + auto r = pow10(n / 2); + r *= r; + if (n % 2 != 0) + r *= 10; + return r; + } + + // n < 0 T p = 1; - if (n >= 0) - { - for (int i = 0; i < n; ++i) - p *= 10; - } - else - { - for (int i = 0; i < -n; ++i) - p /= 10; - } + p /= pow10(-n); return p; } From 4ec049e727b5f5d19afd6334decb7ceb5a6df265 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 27 May 2026 12:09:13 -0400 Subject: [PATCH 26/77] Minor fixes: missing include, variable init, typo --- include/xrpl/basics/Number.h | 3 ++- src/libxrpl/basics/Number.cpp | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 37899a1cf4..2dfe3a408f 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -53,7 +54,7 @@ constexpr std::size_t int64digits = 20; consteval std::array buildPowersOfTen() { - std::array result; + std::array result{}; std::uint64_t power = 1; std::size_t exponent = 0; diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index cd2d213e29..4dba1e8474 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -408,7 +408,7 @@ Number::Guard::doRoundUp( // _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 - // change of bringing it back over. + // chance of bringing it back over. doDropDigit(mantissa, exponent); XRPL_ASSERT_PARTS( safeToIncrement(mantissa), From 8dcd88e83cf63b7e74db6af79685fa3ab130027f Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 27 May 2026 12:34:49 -0400 Subject: [PATCH 27/77] Tweak how the denominator is handled in division - Removes one int64 to 128 conversion --- src/libxrpl/basics/Number.cpp | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 4dba1e8474..28ae3ad12b 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -864,8 +864,10 @@ Number::operator/=(Number const& y) bool const dp = y.negative_; int const ds = (dp ? -1 : 1); - auto dm = y.mantissa_; - auto de = y.exponent_; + // Create the denominator as 128-bit unsigned, since that's what we + // need to work with. + uint128_t const dm = static_cast(y.mantissa_); + auto const de = y.exponent_; auto const& range = kRange.get(); auto const& minMantissa = range.min; @@ -929,11 +931,9 @@ Number::operator/=(Number const& y) uint128_t constexpr f = kPowerOfTen[factorExponent]; - // unsigned denominator - auto const dmu = static_cast(dm); auto const numerator = uint128_t(nm) * f; - auto zm = numerator / dmu; + auto zm = numerator / dm; auto ze = ne - de - factorExponent; bool zn = (ns * ds) < 0; // dropped is used in the same way as Guard::xbit_. In the case of @@ -953,16 +953,16 @@ Number::operator/=(Number const& y) // preserve legacy behavior. // // Consider: - // ((numerator * correctionFactor) / dmu) / correctionFactor - // = ((numerator / dmu) * correctionFactor) / correctionFactor) + // ((numerator * correctionFactor) / dm) / correctionFactor + // = ((numerator / dm) * correctionFactor) / correctionFactor) // // But that assumes infinite precision. With integer math, this is // equivalent to // - // = ((numerator / dmu * correctionFactor) - // + ((numerator % dmu) * correctionFactor) / dmu) / correctionFactor + // = ((numerator / dm * correctionFactor) + // + ((numerator % dm) * correctionFactor) / dm) / correctionFactor // = ((zm * correctionFactor) - // + (remainder * correctionFactor) / dmu) / correctionFactor + // + (remainder * correctionFactor) / dm) / correctionFactor // // The trick is that multiplication by correctionFactor is done on the mantissa, but // division by correctionFactor is done by modifying the exponent, so no precision is lost @@ -974,11 +974,11 @@ Number::operator/=(Number const& y) uint128_t constexpr correctionFactor = kPowerOfTen[correctionExponent]; static_assert(factorExponent + correctionExponent == 22); - auto const remainder = (numerator % dmu); + auto const remainder = (numerator % dm); if (remainder != 0) { auto const partialNumerator = remainder * correctionFactor; - auto const correction = partialNumerator / dmu; + auto const correction = partialNumerator / dm; if (correction != 0) { @@ -997,7 +997,7 @@ Number::operator/=(Number const& y) cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled; if (useTrailingRemainder) { - dropped = partialNumerator % dmu != 0; + dropped = partialNumerator % dm != 0; } } } From de2efa5cb9f1c445d9ed345ef11fa7b59bf0aec1 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 27 May 2026 13:06:52 -0400 Subject: [PATCH 28/77] Remove TMax entirely from normalizeToRange; check type matching directly --- include/xrpl/basics/Number.h | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 2dfe3a408f..f7ef23e605 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -534,8 +534,7 @@ public: template < auto MinMantissa, auto MaxMantissa, - Integral64 T = std::decay_t, - Integral64 TMax = std::decay_t> + Integral64 T = std::decay_t> [[nodiscard]] std::pair normalizeToRange() const; @@ -783,16 +782,18 @@ Number::isnormal() const noexcept kMinExponent <= exponent_ && exponent_ <= kMaxExponent); } -template +template std::pair Number::normalizeToRange() const { static_assert(std::is_same_v || std::is_same_v); - static_assert(std::is_same_v); + static_assert(std::is_same_v>); + static_assert(std::is_same_v>); auto constexpr kMIN = static_cast(MinMantissa); auto constexpr kMAX = static_cast(MaxMantissa); static_assert(kMIN > 0); static_assert(kMIN % 10 == 0); + static_assert(isPowerOfTen(kMIN)); static_assert(kMAX % 10 == 9); static_assert((kMAX + 1) / 10 == kMIN); From 06a3f76ccdf8edda8cf1c31f1fa5c2c07c3bdd64 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 28 May 2026 17:45:53 -0400 Subject: [PATCH 29/77] Skip clang-tidy false positive: misc-include-cleaner --- src/test/basics/Number_test.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 230752d37c..a4caed39f6 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -6,6 +6,7 @@ #include #include +// NOLINTNEXTLINE(misc-include-cleaner) #include #include From 2fdfd2b42015ac25e68c8804b1101540089b5fc4 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 28 May 2026 18:37:15 -0400 Subject: [PATCH 30/77] Review feedback from Tapanito and AI - Add missing headers. - Improve code coverage exclusions. - Clean up several variable names. - Improve explanatory comments. - Remove the switch statement from MantissaRange::getMin. Change it to a straight power of ten lookup. --- include/xrpl/basics/Number.h | 29 ++++++++++-------------- src/libxrpl/basics/Number.cpp | 42 ++++++++++++++++++++++++++--------- 2 files changed, 43 insertions(+), 28 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index f7ef23e605..93bef82a8c 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -50,11 +51,11 @@ namespace detail { * compile time. Doing it at runtime would be pretty wasteful and * inefficient. */ -constexpr std::size_t int64digits = 20; -consteval std::array +constexpr std::size_t kInt64Digits = 20; +consteval std::array buildPowersOfTen() { - std::array result{}; + std::array result{}; std::uint64_t power = 1; std::size_t exponent = 0; @@ -74,13 +75,13 @@ buildPowersOfTen() } // namespace detail -constexpr std::array kPowerOfTen = detail::buildPowersOfTen(); +constexpr std::array kPowerOfTen = detail::buildPowersOfTen(); static_assert(kPowerOfTen[0] == 1); static_assert(kPowerOfTen[1] == 10); static_assert(kPowerOfTen[10] == 10'000'000'000); static_assert( - isPowerOfTen(kPowerOfTen.back()) && *logTen(kPowerOfTen.back()) == detail::int64digits - 1); + isPowerOfTen(kPowerOfTen.back()) && *logTen(kPowerOfTen.back()) == detail::kInt64Digits - 1); /** MantissaRange defines a range for the mantissa of a normalized Number. * @@ -159,10 +160,12 @@ private: case MantissaScale::LargeLegacy: case MantissaScale::Large: return 18; + // LCOV_EXCL_START default: // If called in a constexpr context, this throw assures that the build fails if an // invalid scale is used. - throw std::runtime_error("Unknown mantissa scale"); // LCOV_EXCL_LINE + throw std::runtime_error("Unknown mantissa scale"); + // LCOV_EXCL_STOP } } @@ -171,17 +174,9 @@ private: static constexpr rep getMin(MantissaScale scale, int exponent) { - switch (scale) - { - case MantissaScale::Small: - case MantissaScale::LargeLegacy: - case MantissaScale::Large: - return kPowerOfTen[exponent]; - default: - // If called in a constexpr context, this throw assures that the build fails if an - // invalid scale is used. - throw std::runtime_error("Unknown mantissa scale"); // LCOV_EXCL_LINE - } + if (exponent < 0 || exponent >= kPowerOfTen.size()) + throw std::runtime_error("Invalid exponent"); // LCOV_EXCL_LINE + return kPowerOfTen[exponent]; } static constexpr CuspRoundingFix diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 28ae3ad12b..275d82d8c9 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -622,8 +622,12 @@ Number::normalize( internalrep const& maxMantissa, MantissaRange::CuspRoundingFix cuspRoundingFixEnabled) { + // Not used by every compiler version, and thus not necessarily + // counted by coverage build + // LCOV_EXCL_START doNormalize( negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false); + // LCOV_EXCL_STOP } template <> @@ -636,8 +640,12 @@ Number::normalize( internalrep const& maxMantissa, MantissaRange::CuspRoundingFix cuspRoundingFixEnabled) { + // Not used by every compiler version, and thus not necessarily + // counted by coverage build + // LCOV_EXCL_START doNormalize( negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false); + // LCOV_EXCL_STOP } template <> @@ -852,7 +860,9 @@ Number::operator/=(Number const& y) return *this; // n* = numerator // d* = denominator - // *p = negative (positive?) + // z* = result (quotient) + // *p = negative (p for positive, even though the value means not + // positive?) // *s = sign // *m = mantissa // *e = exponent @@ -876,7 +886,10 @@ Number::operator/=(Number const& y) // Division operates on two large integers (16-digit for small // mantissas, 19-digit for large) using integer math. If the values - // were just divided directly, the result would be at most 9. + // were just divided directly, the result would be only ever be one + // digit or zero - not very useful. + // e.g. 9'876'543'210'987'654 / 1'234'567'890'123'456 = 8 + // 1'234'567'890'123'456 / 9'876'543'210'987'654 = 0 // Introduce a power-of-ten multiplication factor for the numerator // which will ensure the result has a meaningful number of digits. // @@ -885,11 +898,11 @@ Number::operator/=(Number const& y) // * 23 / 67 = 0 // * Use a factor of 10^4 // * 230'000 / 67 = 3432 with an exponent of -4 - // * The normalized result will be 34, exponent -2 + // * The normalized result will be 34, exponent -2, or 0.34 // // The most extreme results are 10/99 and 99/10 - // * 100'000 / 99 = 1'010e-4 = 10e-2 - // * 990'000 / 10 = 99'000e-4 = 99e-1 + // * 100'000 / 99 = 1'010e-4 = 10e-2 or 0.10 + // * 990'000 / 10 = 99'000e-4 = 99e-1 or 9.9 // // Note that the computations give 2 or 3 digits after the // decimal point to determine which way to round for most scenarios. @@ -899,8 +912,7 @@ Number::operator/=(Number const& y) // behavior, which must not be changed.) // // For large mantissas (where the MantissaRange.log == 18), a shift by 10^20 would be optimal - // for most scenarios. However, larger mantissa values would overflow 2^128 (log(2^128,10) - // ~ 38.5). + // for most scenarios. However, larger mantissa values would overflow 2^128. // // * log(2^128,10) ~ 38.5 // * largeRange.log = 18, fits in 10^19 @@ -927,6 +939,8 @@ Number::operator/=(Number const& y) // remainder is exactly 0.5 for "ToNearest". This will give the // rounding the most accurate result possible, as if infinite // precision was used in the initial calculation. + + // Stage 1: Do the initial division with a factor of 10^17. auto constexpr factorExponent = 17; uint128_t constexpr f = kPowerOfTen[factorExponent]; @@ -935,7 +949,7 @@ Number::operator/=(Number const& y) auto zm = numerator / dm; auto ze = ne - de - factorExponent; - bool zn = (ns * ds) < 0; + bool zp = (ns * ds) < 0; // dropped is used in the same way as Guard::xbit_. In the case of // division, it indicates if there's any remainder left over after // we have been as precise as reasonable. If there is, it would be as @@ -947,7 +961,7 @@ Number::operator/=(Number const& y) { // Stage 2 // - // If there is a remainder, treat is as a secondary numerator. + // If there is a remainder, treat it as a secondary numerator. // Multiply by correctionFactor separately from stage 1. // The math for this would work for small mantissas, but we need to // preserve legacy behavior. @@ -980,6 +994,12 @@ Number::operator/=(Number const& y) auto const partialNumerator = remainder * correctionFactor; auto const correction = partialNumerator / dm; + // If the correction is zero, we do not have to make any + // modifications to z*, because it will not have any + // effect on the final result. (We'd be adding a bunch of + // zeros to the end of zm that would just be removed in + // normalize.) However, if that is the case, then Stage 3 is + // even more important for accuracy. if (correction != 0) { zm *= correctionFactor; @@ -1001,8 +1021,8 @@ Number::operator/=(Number const& y) } } } - doNormalize(zn, zm, ze, minMantissa, maxMantissa, cuspRoundingFixEnabled, dropped); - negative_ = zn; + doNormalize(zp, zm, ze, minMantissa, maxMantissa, cuspRoundingFixEnabled, dropped); + negative_ = zp; mantissa_ = static_cast(zm); exponent_ = ze; XRPL_ASSERT_PARTS(isnormal(), "xrpl::Number::operator/=", "result is normalized"); From cd21d745384487fc9bcf7abf68159eddbd261364 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 28 May 2026 19:58:52 -0400 Subject: [PATCH 31/77] Update the testUpwardRoundsDown test to run under all scales - Demonstrates the incorrect "before" behavior --- src/test/basics/Number_test.cpp | 73 ++++++++++++++++++++++++++------- 1 file changed, 59 insertions(+), 14 deletions(-) diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index a4caed39f6..f635747153 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -1626,10 +1626,11 @@ public: void testUpwardRoundsDown() { + auto const scale = Number::getMantissaScale(); { - testcase << "upward rounding produces a value below exact at kMaxRep cusp"; + testcase << "upward rounding produces a value below exact at kMaxRep cusp " + << to_string(scale); - NumberMantissaScaleGuard const mg{MantissaRange::MantissaScale::Large}; NumberRoundModeGuard const rg{Number::RoundingMode::Upward}; constexpr std::int64_t kAValue = 1'000'000'000'000'049'863LL; @@ -1655,13 +1656,41 @@ public: << " exact a*b = " << fmt(exactProduct) << "\n" << " stored = " << fmt(storedValue) << "\n" << " stored - exact = " << fmt(signedDifference) << "\n" - << " upward = " << (signedDifference >= 0 ? "held" : "VIOLATED") << "\n"; - - BEAST_EXPECT(signedDifference >= 0); - BEAST_EXPECT(signedDifference < pow10(product.exponent())); - BEAST_EXPECT(product.mantissa() == (std::numeric_limits::max() / 10) + 1); - BEAST_EXPECT(product.exponent() == 19); + << " upward = " << (signedDifference >= 0 ? "held" : "VIOLATED") << "\n" + << " stored.mantissa = " << product.mantissa() << "\n" + << " stored.exponent = " << product.exponent() << "\n"; log.flush(); + + switch (scale) + { + case MantissaRange::MantissaScale::Large: + BEAST_EXPECT(signedDifference >= 0); + BEAST_EXPECT(signedDifference < pow10(product.exponent())); + BEAST_EXPECT( + product.mantissa() == (std::numeric_limits::max() / 10) + 1); + BEAST_EXPECT(product.exponent() == 19); + break; + + case MantissaRange::MantissaScale::LargeLegacy: + BEAST_EXPECT(signedDifference < 0); + BEAST_EXPECT( + product.mantissa() == + (std::numeric_limits::max() / 100) * 100); + BEAST_EXPECT(product.exponent() == 18); + break; + + case MantissaRange::MantissaScale::Small: + // The seemingly weird rounding here is because + // a & b are both normalized, and both round up when + // being converted to Number, so you're really + // getting 1_000_000_000_000_050 * 9_223_372_036_854_316 + BEAST_EXPECT(signedDifference >= 0); + BEAST_EXPECT( + product.mantissa() == + (std::numeric_limits::max() / 1000) + 3); + BEAST_EXPECT(product.exponent() == 21); + break; + } } { @@ -1687,9 +1716,8 @@ public: * 4. Under Upward + positive, `round()` returns -1 (no round-up), and * the algorithm returns the truncated zm */ - testcase << "operator/= Upward on Large returns value < truth "; + testcase << "operator/= Upward on Large returns value < truth " << to_string(scale); - NumberMantissaScaleGuard const scaleGuard{MantissaRange::MantissaScale::Large}; NumberRoundModeGuard const roundGuard{Number::RoundingMode::Upward}; constexpr std::int64_t aValue = 2LL; @@ -1713,10 +1741,27 @@ public: << " (negative => Upward gave value BELOW truth)\n" << " quotient.mantissa = " << quotient.mantissa() << "\n" << " quotient.exponent = " << quotient.exponent() << "\n"; + log.flush(); // Upward invariant: stored >= exact. Bug: stored < exact. - BEAST_EXPECT(stored >= exact); - BEAST_EXPECT(diff < pow10(quotient.exponent())); + switch (scale) + { + case MantissaRange::MantissaScale::Large: + BEAST_EXPECT(stored >= exact); + BEAST_EXPECT(diff < pow10(quotient.exponent())); + break; + + case MantissaRange::MantissaScale::LargeLegacy: + BEAST_EXPECT(stored < exact); + BEAST_EXPECT(diff >= -pow10(quotient.exponent())); + break; + + case MantissaRange::MantissaScale::Small: + // Small mantissa doesn't have the correction for + // dropped remainders + BEAST_EXPECT(stored < exact); + break; + } } } @@ -1747,9 +1792,9 @@ public: testTruncate(); testRounding(); testInt64(); + + testUpwardRoundsDown(); } - // This test sets its own number range - testUpwardRoundsDown(); } }; From be9ae88d48a1037b66ab859e9cd80c9d817e5e48 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 28 May 2026 23:38:54 -0400 Subject: [PATCH 32/77] Accept AI suggestion Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/test/basics/Number_test.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index f635747153..f407072df7 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -1703,14 +1703,14 @@ public: * violating Upward's directional invariant. * * Mechanism (fix-enabled path): - * 1. `operator/=` computes `numerator = nm * 10^19` and + * 1. `operator/=` computes `numerator = nm * 10^17` and * `zm = numerator / dm` (integer division, truncates remainder). * 2. If `remainder != 0`, the correction block runs: - * zm *= 1000 - * correction = (remainder * 1000) / dm // also truncates + * zm *= 100000 + * correction = (remainder * 100000) / dm // also truncates * zm += correction - * ze -= 3 - * The truncation in `correction` discards a sub-1/1000 residual. + * ze -= 5 + * The truncation in `correction` discards a sub-1/100000 residual. * 3. `normalize`'s shift loop reduces zm to fit, but the discarded * residual is BELOW the Guard's visibility, so the Guard sees fraction = 0. * 4. Under Upward + positive, `round()` returns -1 (no round-up), and From c0569037f8cad7d1c157a96133cd8aae6da5c812 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 29 May 2026 16:34:51 -0400 Subject: [PATCH 33/77] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/test/basics/Number_test.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index f407072df7..c1b122637d 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -1694,12 +1694,11 @@ public: } { - /* Companion to NumberUpwardWrongDirection_test (which targets - * `operator*=` Upward at the kMaxRep cusp on LargeLegacy), but for + /* Companion regression for the kMaxRep cusp behavior, but for * `operator/=` on the cusp-fix-ENABLED `Large` scale. * - * Under `Large` (`CuspRoundingFix::Enabled`), `operator/=` with Upward - * rounding can return a value STRICTLY LESS than the exact quotient, + * Before the dropped-remainder fix, `operator/=` with Upward + * rounding could return a value STRICTLY LESS than the exact quotient, * violating Upward's directional invariant. * * Mechanism (fix-enabled path): From f6a26ca34f1739fb8f51bd9079f0c03a9c4bff00 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 29 May 2026 18:54:08 -0400 Subject: [PATCH 34/77] CI feedback: Add test cases covering other rounding modes - Downward with a negative result, and ToNearest with a remainder slightly larger than 0.5. --- src/test/basics/Number_test.cpp | 107 ++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index c1b122637d..81019970ad 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -1762,6 +1762,113 @@ public: break; } } + { + /* Companion test case for Upward positive operator/=: Downward negative + */ + testcase << "operator/= Downward on Large returns value < truth " << to_string(scale); + + NumberRoundModeGuard const roundGuard{Number::RoundingMode::Downward}; + + constexpr std::int64_t aValue = -2LL; + constexpr std::int64_t bValue = 1'000'000'000'000'000'007LL; + // bValue = 10^18 + 7 (prime, in [minMantissa, kMaxRep]). + + Number const a{aValue, 0}; + Number const b{bValue, 0}; + Number const quotient = a / b; + + dec const exact = dec(aValue) / dec(bValue); + dec const stored = dec(quotient.mantissa()) * pow10(quotient.exponent()); + dec const diff = stored - exact; + + log << "\n" + << " a = " << aValue << "\n" + << " b = " << bValue << "\n" + << " exact a/b = " << fmt(exact) << "\n" + << " stored a/b = " << fmt(stored) << "\n" + << " stored - exact = " << fmt(diff) + << " (positive => Downward gave value ABOVE truth)\n" + << " quotient.mantissa = " << quotient.mantissa() << "\n" + << " quotient.exponent = " << quotient.exponent() << "\n"; + log.flush(); + + // invariant: stored <= exact. Bug: stored > exact. + switch (scale) + { + case MantissaRange::MantissaScale::Large: + BEAST_EXPECT(stored <= exact); + BEAST_EXPECT(diff > -pow10(quotient.exponent())); + break; + + case MantissaRange::MantissaScale::LargeLegacy: + BEAST_EXPECT(stored > exact); + BEAST_EXPECT(diff <= pow10(quotient.exponent())); + break; + + case MantissaRange::MantissaScale::Small: + // Small mantissa doesn't have the correction for + // dropped remainders + BEAST_EXPECT(stored < exact); + break; + } + } + { + /* Companion test case for Upward positive operator/=: ToNearest + * + * With ToNearest, if the dropped digits are exactly "5", then the mantissa will be + * rounded to even. The numbers below result in a value where the unrounded mantissa + * ends in an even digit, and "infinite precision" would drop + * "500000000000000000145...", but doNormalize only sees "5". Without the rounding fix, + * doNormalize rounds down to the even value. With the rounding fix, doNormalize knows + * there are more digits beyond "5", and so rounds _up_ to the odd value. + */ + testcase << "operator/= ToNearest on Large returns value < truth " << to_string(scale); + + NumberRoundModeGuard const roundGuard{Number::RoundingMode::ToNearest}; + + constexpr std::int64_t aValue = 1'269'917'268'816'087'809LL; + constexpr std::int64_t bValue = 3'458'525'013'821'685'511LL; + // bValue = 10^18 + 7 (prime, in [minMantissa, kMaxRep]). + + Number const a{aValue, 0}; + Number const b{bValue, 0}; + Number const quotient = a / b; + + dec const exact = dec(aValue) / dec(bValue); + dec const stored = dec(quotient.mantissa()) * pow10(quotient.exponent()); + dec const diff = stored - exact; + + log << "\n" + << " a = " << aValue << "\n" + << " b = " << bValue << "\n" + << " exact a/b = " << fmt(exact) << "\n" + << " stored a/b = " << fmt(stored) << "\n" + << " stored - exact = " << fmt(diff) + << " (negative => ToNearest gave value BELOW truth)\n" + << " quotient.mantissa = " << quotient.mantissa() << "\n" + << " quotient.exponent = " << quotient.exponent() << "\n"; + log.flush(); + + // invariant: stored >= exact. Bug: stored < exact. + switch (scale) + { + case MantissaRange::MantissaScale::Large: + BEAST_EXPECT(stored >= exact); + BEAST_EXPECT(diff < pow10(quotient.exponent())); + break; + + case MantissaRange::MantissaScale::LargeLegacy: + BEAST_EXPECT(stored < exact); + BEAST_EXPECT(diff >= -pow10(quotient.exponent())); + break; + + case MantissaRange::MantissaScale::Small: + // Small mantissa doesn't have the correction for + // dropped remainders + BEAST_EXPECT(stored < exact); + break; + } + } } void From d1af39d9dd0507b0a024c372ef4abb228875dd6f Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 29 May 2026 19:03:34 -0400 Subject: [PATCH 35/77] test: Add another rounding unit test --- src/test/basics/Number_test.cpp | 41 +++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 81019970ad..7760f189f3 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -1869,6 +1869,47 @@ public: break; } } + + { + testcase << "normalization cusp: ToNearest and Downward disagree" << to_string(scale); + + constexpr auto kMaxRep = Number::kMaxRep; + + // Both ToNearest and Downward should round to `below` + auto const actual = static_cast(kMaxRep) + 1; + Number const below{static_cast(kMaxRep), 0}; + Number const above{ + false, static_cast(kMaxRep) + 3, 0, Number::Unchecked{}}; + + Number toNearest; + { + NumberRoundModeGuard const roundGuard{Number::RoundingMode::ToNearest}; + toNearest = Number(false, actual, 0, Number::Normalized{}); + } + + Number downward; + { + NumberRoundModeGuard const roundGuard{Number::RoundingMode::Downward}; + downward = Number(false, actual, 0, Number::Normalized{}); + } + + log << "\n" + << " actual = " << actual << " (kMaxRep + 1)\n" + << " below = " << below << " (kMaxRep, distance 1)\n" + << " above = " << above << " (kMaxRep + 3, distance 2)\n" + << " ToNearest = " << toNearest << "\n" + << " Downward = " << downward << "\n\n"; + + // ToNearest rounds UP when the DOWN neighbor is strictly closer + BEAST_EXPECT(toNearest == above); + BEAST_EXPECT(toNearest != below); + + // Downward undershoots: it returns a value below `below` + BEAST_EXPECT(downward < below); + + // Both should have given the same answer, but they differ + BEAST_EXPECT(toNearest != downward); + } } void From 9f872f21794e15ab323e57f920c41195e952795d Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 29 May 2026 19:46:50 -0400 Subject: [PATCH 36/77] Include upward, write tests based on "expected" behavior --- src/test/basics/Number_test.cpp | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 7760f189f3..f9ccdc21d8 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -1871,7 +1871,7 @@ public: } { - testcase << "normalization cusp: ToNearest and Downward disagree" << to_string(scale); + testcase << "normalization cusp: ToNearest and Downward disagree " << to_string(scale); constexpr auto kMaxRep = Number::kMaxRep; @@ -1881,34 +1881,37 @@ public: Number const above{ false, static_cast(kMaxRep) + 3, 0, Number::Unchecked{}}; - Number toNearest; - { - NumberRoundModeGuard const roundGuard{Number::RoundingMode::ToNearest}; - toNearest = Number(false, actual, 0, Number::Normalized{}); - } + auto construct = [](Number::RoundingMode mode) { + NumberRoundModeGuard const roundGuard{mode}; + return Number(false, actual, 0, Number::Normalized{}); + }; + Number const upward = construct(Number::RoundingMode::Upward); - Number downward; - { - NumberRoundModeGuard const roundGuard{Number::RoundingMode::Downward}; - downward = Number(false, actual, 0, Number::Normalized{}); - } + Number const toNearest = construct(Number::RoundingMode::ToNearest); + + Number const downward = construct(Number::RoundingMode::Downward); log << "\n" << " actual = " << actual << " (kMaxRep + 1)\n" << " below = " << below << " (kMaxRep, distance 1)\n" << " above = " << above << " (kMaxRep + 3, distance 2)\n" + << " Upward = " << upward << "\n" << " ToNearest = " << toNearest << "\n" << " Downward = " << downward << "\n\n"; + log.flush(); + + // Upward round UP + BEAST_EXPECT(upward == above); // ToNearest rounds UP when the DOWN neighbor is strictly closer - BEAST_EXPECT(toNearest == above); - BEAST_EXPECT(toNearest != below); + BEAST_EXPECT(toNearest != above); + BEAST_EXPECT(toNearest == below); // Downward undershoots: it returns a value below `below` - BEAST_EXPECT(downward < below); + BEAST_EXPECT(downward == below); // Both should have given the same answer, but they differ - BEAST_EXPECT(toNearest != downward); + BEAST_EXPECT(toNearest == downward); } } From 35bee87909c15decebf49c315627a87083383e00 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 1 Jun 2026 21:48:57 -0400 Subject: [PATCH 37/77] Experimental: Scale addition operands up to preserve accuracy --- include/xrpl/basics/Number.h | 24 ++-- src/libxrpl/basics/Number.cpp | 79 +++++++++-- src/test/basics/Number_test.cpp | 242 +++++++++++++++++++++++--------- 3 files changed, 262 insertions(+), 83 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 93bef82a8c..ffb991a41d 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -51,37 +51,43 @@ namespace detail { * compile time. Doing it at runtime would be pretty wasteful and * inefficient. */ -constexpr std::size_t kInt64Digits = 20; -consteval std::array +constexpr std::size_t kUint64Digits = 20; +constexpr std::size_t kUint128Digits = 39; + +template +consteval std::array buildPowersOfTen() { - std::array result{}; + std::array result{}; - std::uint64_t power = 1; + T power = 1; std::size_t exponent = 0; // end the loop early so it doesn't overflow; for (; exponent < result.size() - 1; ++exponent, power *= 10) { result[exponent] = power; - if (power > std::numeric_limits::max() / 10) + if (power > std::numeric_limits::max() / 10) throw std::logic_error("Power of 10 table is too big"); } result[exponent] = power; - if (power < std::numeric_limits::max() / 10) - throw std::logic_error("Power of 10 table is not big enough for the uint64_t type"); + if (power < std::numeric_limits::max() / 10) + throw std::logic_error("Power of 10 table is not big enough for the given type"); return result; } } // namespace detail -constexpr std::array kPowerOfTen = detail::buildPowersOfTen(); +template +constexpr std::array kPowerOfTenImpl = detail::buildPowersOfTen(); + +constexpr auto kPowerOfTen = kPowerOfTenImpl; static_assert(kPowerOfTen[0] == 1); static_assert(kPowerOfTen[1] == 10); static_assert(kPowerOfTen[10] == 10'000'000'000); static_assert( - isPowerOfTen(kPowerOfTen.back()) && *logTen(kPowerOfTen.back()) == detail::kInt64Digits - 1); + isPowerOfTen(kPowerOfTen.back()) && *logTen(kPowerOfTen.back()) == detail::kUint64Digits - 1); /** MantissaRange defines a range for the mantissa of a normalized Number. * diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 275d82d8c9..23be06475b 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -720,26 +720,54 @@ Number::operator+=(Number const& y) uint128_t ym = y.mantissa_; auto ye = y.exponent_; Guard g; + + auto const& range = kRange.get(); + + // Bring the exponents of both values into agreement, so the mantissas are on the same scale + // and can be added directly together + // expandM / expandE: First try to expand the mantissa and bring the exponent down + // shringM / shrinkE: Then shrink the mantissa and bring the exponent up, if necessary + auto const adjust = [&g, &range]( + uint128_t& expandM, int& expandE, uint128_t& shrinkM, int& shrinkE) { + constexpr uint128_t kSafeLimit = kPowerOfTenImpl[37]; + + if (range.cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled) + { + while (shrinkE < expandE && shrinkM % 10 == 0) + { + g.doDropDigit(shrinkM, shrinkE); + } + + // We've got 128 bits of mantissa to work with here. Don't throw away data unless we + // have to + while (shrinkE < expandE && expandE > kMinExponent && expandM < kSafeLimit) + { + expandM *= 10; + --expandE; + } + } + + while (shrinkE < expandE) + { + g.doDropDigit(shrinkM, shrinkE); + } + }; + if (xe < ye) { if (xn) g.setNegative(); - do - { - g.doDropDigit(xm, xe); - } while (xe < ye); + + adjust(ym, ye, xm, xe); } else if (xe > ye) { if (yn) g.setNegative(); - do - { - g.doDropDigit(ym, ye); - } while (xe > ye); + + adjust(xm, xe, ym, ye); } - auto const& range = kRange.get(); auto const& minMantissa = range.min; auto const& maxMantissa = range.max; auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled; @@ -747,9 +775,19 @@ Number::operator+=(Number const& y) if (xn == yn) { xm += ym; - if (xm > maxMantissa || xm > kMaxRep) + if (range.cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled) { - g.doDropDigit(xm, xe); + while (xm > maxMantissa || xm > kMaxRep) + { + g.doDropDigit(xm, xe); + } + } + else + { + if (xm > maxMantissa || xm > kMaxRep) + { + g.doDropDigit(xm, xe); + } } g.doRoundUp( xn, @@ -779,6 +817,25 @@ Number::operator+=(Number const& y) --xe; } g.doRoundDown(xn, xm, xe, minMantissa); + if (range.cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled) + { + // make a new guard + Guard g; + if (xn) + g.setNegative(); + while (xm > maxMantissa || xm > kMaxRep) + { + g.doDropDigit(xm, xe); + } + g.doRoundUp( + xn, + xm, + xe, + minMantissa, + maxMantissa, + cuspRoundingFixEnabled, + "Number::addition overflow"); + } } negative_ = xn; diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index f9ccdc21d8..620ea798bc 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -43,6 +43,15 @@ class Number_test : public beast::unit_test::Suite return out; } + static BigInt + toBigInt(Number const& n) + { + BigInt v = n.mantissa(); + for (int i = 0; i < n.exponent(); ++i) + v *= 10; + return v; + } + using dec = boost::multiprecision::cpp_dec_float_50; template @@ -169,28 +178,34 @@ public: auto const scale = Number::getMantissaScale(); testcase << "test_add " << to_string(scale); - using Case = std::tuple; + using Case = std::tuple; auto const cSmall = std::to_array( {{Number{1'000'000'000'000'000, -15}, Number{6'555'555'555'555'555, -29}, - Number{1'000'000'000'000'066, -15}}, + Number{1'000'000'000'000'066, -15}, + __LINE__}, {Number{-1'000'000'000'000'000, -15}, Number{-6'555'555'555'555'555, -29}, - Number{-1'000'000'000'000'066, -15}}, + Number{-1'000'000'000'000'066, -15}, + __LINE__}, {Number{-1'000'000'000'000'000, -15}, Number{6'555'555'555'555'555, -29}, - Number{-9'999'999'999'999'344, -16}}, + Number{-9'999'999'999'999'344, -16}, + __LINE__}, {Number{-6'555'555'555'555'555, -29}, Number{1'000'000'000'000'000, -15}, - Number{9'999'999'999'999'344, -16}}, - {Number{}, Number{5}, Number{5}}, - {Number{5}, Number{}, Number{5}}, + Number{9'999'999'999'999'344, -16}, + __LINE__}, + {Number{}, Number{5}, Number{5}, __LINE__}, + {Number{5}, Number{}, Number{5}, __LINE__}, {Number{5'555'555'555'555'555, -32768}, Number{-5'555'555'555'555'554, -32768}, - Number{0}}, + Number{0}, + __LINE__}, {Number{-9'999'999'999'999'999, -31}, Number{1'000'000'000'000'000, -15}, - Number{9'999'999'999'999'990, -16}}}); + Number{9'999'999'999'999'990, -16}, + __LINE__}}); auto const cLarge = std::to_array( // Note that items with extremely large mantissas need to be // calculated, because otherwise they overflow uint64. Items from C @@ -198,45 +213,57 @@ public: { {Number{1'000'000'000'000'000, -15}, Number{6'555'555'555'555'555, -29}, - Number{1'000'000'000'000'065'556, -18}}, + Number{1'000'000'000'000'065'556, -18}, + __LINE__}, {Number{-1'000'000'000'000'000, -15}, Number{-6'555'555'555'555'555, -29}, - Number{-1'000'000'000'000'065'556, -18}}, + Number{-1'000'000'000'000'065'556, -18}, + __LINE__}, {Number{-1'000'000'000'000'000, -15}, Number{6'555'555'555'555'555, -29}, - Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}}, + Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}, + __LINE__}, {Number{-6'555'555'555'555'555, -29}, Number{1'000'000'000'000'000, -15}, - Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}}, - {Number{}, Number{5}, Number{5}}, - {Number{5}, Number{}, Number{5}}, + Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}, + __LINE__}, + {Number{}, Number{5}, Number{5}, __LINE__}, + {Number{5}, Number{}, Number{5}, __LINE__}, {Number{5'555'555'555'555'555'000, -32768}, Number{-5'555'555'555'555'554'000, -32768}, - Number{0}}, + Number{0}, + __LINE__}, {Number{-9'999'999'999'999'999, -31}, Number{1'000'000'000'000'000, -15}, - Number{9'999'999'999'999'990, -16}}, + Number{9'999'999'999'999'990, -16}, + __LINE__}, // Items from cSmall expanded for the larger mantissa {Number{1'000'000'000'000'000'000, -18}, Number{6'555'555'555'555'555'555, -35}, - Number{1'000'000'000'000'000'066, -18}}, + Number{1'000'000'000'000'000'066, -18}, + __LINE__}, {Number{-1'000'000'000'000'000'000, -18}, Number{-6'555'555'555'555'555'555, -35}, - Number{-1'000'000'000'000'000'066, -18}}, + Number{-1'000'000'000'000'000'066, -18}, + __LINE__}, {Number{-1'000'000'000'000'000'000, -18}, Number{6'555'555'555'555'555'555, -35}, - Number{true, 9'999'999'999'999'999'344ULL, -19, Number::Normalized{}}}, + Number{true, 9'999'999'999'999'999'344ULL, -19, Number::Normalized{}}, + __LINE__}, {Number{-6'555'555'555'555'555'555, -35}, Number{1'000'000'000'000'000'000, -18}, - Number{false, 9'999'999'999'999'999'344ULL, -19, Number::Normalized{}}}, - {Number{}, Number{5}, Number{5}}, + Number{false, 9'999'999'999'999'999'344ULL, -19, Number::Normalized{}}, + __LINE__}, + {Number{}, Number{5}, Number{5}, __LINE__}, {Number{5'555'555'555'555'555'555, -32768}, Number{-5'555'555'555'555'555'554, -32768}, - Number{0}}, + Number{0}, + __LINE__}, {Number{true, 9'999'999'999'999'999'999ULL, -37, Number::Normalized{}}, Number{1'000'000'000'000'000'000, -18}, - Number{false, 9'999'999'999'999'999'990ULL, -19, Number::Normalized{}}}, - {Number{Number::kMaxRep - 1}, Number{1, 0}, Number{Number::kMaxRep}}, + Number{false, 9'999'999'999'999'999'990ULL, -19, Number::Normalized{}}, + __LINE__}, + {Number{Number::kMaxRep - 1}, Number{1, 0}, Number{Number::kMaxRep}, __LINE__}, // Test extremes { // Each Number operand rounds up, so the actual mantissa is @@ -244,6 +271,7 @@ public: Number{false, 9'999'999'999'999'999'999ULL, 0, Number::Normalized{}}, Number{false, 9'999'999'999'999'999'999ULL, 0, Number::Normalized{}}, Number{2, 19}, + __LINE__, }, { // Does not round. Mantissas are going to be > kMaxRep, so if @@ -254,21 +282,25 @@ public: Number{false, 9'999'999'999'999'999'990ULL, 0, Number::Normalized{}}, Number{false, 9'999'999'999'999'999'990ULL, 0, Number::Normalized{}}, Number{false, 1'999'999'999'999'999'998ULL, 1, Number::Normalized{}}, + __LINE__, }, }); auto const cLargeLegacy = std::to_array({ - {Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep / 10, 1}}, + {Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep / 10, 1}, __LINE__}, }); auto const cLargeCorrected = std::to_array({ - {Number{Number::kMaxRep}, Number{6, -1}, Number{(Number::kMaxRep / 10) + 1, 1}}, + {Number{Number::kMaxRep}, + Number{6, -1}, + Number{(Number::kMaxRep / 10) + 1, 1}, + __LINE__}, }); auto test = [this](auto const& c) { - for (auto const& [x, y, z] : c) + for (auto const& [x, y, z, line] : c) { auto const result = x + y; std::stringstream ss; ss << x << " + " << y << " = " << result << ". Expected: " << z; - BEAST_EXPECTS(result == z, ss.str()); + expect(result == z, ss.str(), __FILE__, line); } }; if (scale == MantissaRange::MantissaScale::Small) @@ -308,21 +340,28 @@ public: auto const scale = Number::getMantissaScale(); testcase << "test_sub " << to_string(scale); - using Case = std::tuple; + using Case = std::tuple; auto const cSmall = std::to_array( {{Number{1'000'000'000'000'000, -15}, Number{6'555'555'555'555'555, -29}, - Number{9'999'999'999'999'344, -16}}, + Number{9'999'999'999'999'344, -16}, + __LINE__}, {Number{6'555'555'555'555'555, -29}, Number{1'000'000'000'000'000, -15}, - Number{-9'999'999'999'999'344, -16}}, - {Number{1'000'000'000'000'000, -15}, Number{1'000'000'000'000'000, -15}, Number{0}}, + Number{-9'999'999'999'999'344, -16}, + __LINE__}, + {Number{1'000'000'000'000'000, -15}, + Number{1'000'000'000'000'000, -15}, + Number{0}, + __LINE__}, {Number{1'000'000'000'000'000, -15}, Number{1'000'000'000'000'001, -15}, - Number{-1'000'000'000'000'000, -30}}, + Number{-1'000'000'000'000'000, -30}, + __LINE__}, {Number{1'000'000'000'000'001, -15}, Number{1'000'000'000'000'000, -15}, - Number{1'000'000'000'000'000, -30}}}); + Number{1'000'000'000'000'000, -30}, + __LINE__}}); auto const cLarge = std::to_array( // Note that items with extremely large mantissas need to be // calculated, because otherwise they overflow uint64. Items from C @@ -330,49 +369,63 @@ public: { {Number{1'000'000'000'000'000, -15}, Number{6'555'555'555'555'555, -29}, - Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}}, + Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}, + __LINE__}, {Number{6'555'555'555'555'555, -29}, Number{1'000'000'000'000'000, -15}, - Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}}, - {Number{1'000'000'000'000'000, -15}, Number{1'000'000'000'000'000, -15}, Number{0}}, + Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}, + __LINE__}, + {Number{1'000'000'000'000'000, -15}, + Number{1'000'000'000'000'000, -15}, + Number{0}, + __LINE__}, {Number{1'000'000'000'000'000, -15}, Number{1'000'000'000'000'001, -15}, - Number{-1'000'000'000'000'000, -30}}, + Number{-1'000'000'000'000'000, -30}, + __LINE__}, {Number{1'000'000'000'000'001, -15}, Number{1'000'000'000'000'000, -15}, - Number{1'000'000'000'000'000, -30}}, + Number{1'000'000'000'000'000, -30}, + __LINE__}, // Items from cSmall expanded for the larger mantissa {Number{1'000'000'000'000'000'000, -18}, Number{6'555'555'555'555'555'555, -32}, - Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}}, + Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}, + __LINE__}, {Number{6'555'555'555'555'555'555, -32}, Number{1'000'000'000'000'000'000, -18}, - Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}}, + Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}, + __LINE__}, {Number{1'000'000'000'000'000'000, -18}, Number{1'000'000'000'000'000'000, -18}, - Number{0}}, + Number{0}, + __LINE__}, {Number{1'000'000'000'000'000'000, -18}, Number{1'000'000'000'000'000'001, -18}, - Number{-1'000'000'000'000'000'000, -36}}, + Number{-1'000'000'000'000'000'000, -36}, + __LINE__}, {Number{1'000'000'000'000'000'001, -18}, Number{1'000'000'000'000'000'000, -18}, - Number{1'000'000'000'000'000'000, -36}}, - {Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep - 1}}, + Number{1'000'000'000'000'000'000, -36}, + __LINE__}, + {Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep - 1}, __LINE__}, {Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}}, Number{1, 0}, - Number{(Number::kMaxRep / 10) + 1, 1}}, + Number{(Number::kMaxRep / 10) + 1, 1}, + __LINE__}, {Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}}, Number{3, 0}, - Number{Number::kMaxRep}}, - {power(2, 63), Number{3, 0}, Number{Number::kMaxRep}}, + Number{Number::kMaxRep}, + __LINE__}, + {power(2, 63), Number{3, 0}, Number{Number::kMaxRep}, __LINE__}, }); auto test = [this](auto const& c) { - for (auto const& [x, y, z] : c) + for (auto const& [x, y, z, line] : c) { auto const result = x - y; std::stringstream ss; ss << x << " - " << y << " = " << result << ". Expected: " << z; - BEAST_EXPECTS(result == z, ss.str()); + expect(result == z, ss.str(), __FILE__, line); } }; if (scale == MantissaRange::MantissaScale::Small) @@ -1644,9 +1697,7 @@ public: BigInt const exactProduct = BigInt(kAValue) * BigInt(kBValue); // What Number actually stored. - BigInt storedValue = BigInt(product.mantissa()); - for (int i = 0; i < product.exponent(); ++i) - storedValue *= 10; + BigInt storedValue = toBigInt(product); BigInt const signedDifference = storedValue - exactProduct; @@ -1900,18 +1951,83 @@ public: << " Downward = " << downward << "\n\n"; log.flush(); - // Upward round UP - BEAST_EXPECT(upward == above); + switch (scale) + { + case MantissaRange::MantissaScale::Small: + // With the small mantissa, everything rounds up - // ToNearest rounds UP when the DOWN neighbor is strictly closer - BEAST_EXPECT(toNearest != above); - BEAST_EXPECT(toNearest == below); + // Upward round UP + BEAST_EXPECT(upward > above); - // Downward undershoots: it returns a value below `below` - BEAST_EXPECT(downward == below); + // ToNearest rounds UP when the DOWN neighbor is strictly closer + BEAST_EXPECT(toNearest > above); + BEAST_EXPECT(toNearest == below); - // Both should have given the same answer, but they differ - BEAST_EXPECT(toNearest == downward); + // Downward undershoots: it returns a value below `below` + BEAST_EXPECT(downward < below); + + // Both should have given the same answer, but they differ + BEAST_EXPECT(toNearest > downward); + + break; + + case MantissaRange::MantissaScale::LargeLegacy: + // Upward round UP + BEAST_EXPECT(upward == above); + + // ToNearest rounds UP when the DOWN neighbor is strictly closer + BEAST_EXPECT(toNearest == above); + BEAST_EXPECT(toNearest > below); + + // Downward undershoots: it returns a value below `below` + BEAST_EXPECT(downward < below); + + // Both should have given the same answer, but they differ + BEAST_EXPECT(toNearest > downward); + + break; + default: + // Upward round UP + BEAST_EXPECT(upward == above); + + // ToNearest rounds UP when the DOWN neighbor is strictly closer + BEAST_EXPECT(toNearest != above); + BEAST_EXPECT(toNearest == below); + + // Downward undershoots: it returns a value below `below` + BEAST_EXPECT(downward == below); + + // Both should have given the same answer, but they differ + BEAST_EXPECT(toNearest == downward); + } + } + { + testcase << "operator+ TowardsZero rounds away from zero " << to_string(scale); + + Number const a{1LL, 20}; + Number const b{-1'000'000'000'000'000'001LL}; + + BEAST_EXPECT(toBigInt(a) == BigInt{"100000000000000000000"}); + if (scale != MantissaRange::MantissaScale::Small) + BEAST_EXPECT(toBigInt(b) == BigInt{"-1000000000000000001"}); + else + BEAST_EXPECT(toBigInt(b) == BigInt{"-1000000000000000000"}); + + Number sum; + { + NumberRoundModeGuard const roundGuard{Number::RoundingMode::TowardsZero}; + sum = a + b; + } + + BigInt const exact = toBigInt(a) + toBigInt(b); + BigInt const stored = toBigInt(sum); + + log << "\n exact a + b = " << exact.str() << "\n TowardsZero = " << stored.str() + << "\n"; + log.flush(); + + if (scale != MantissaRange::MantissaScale::LargeLegacy) + BEAST_EXPECT(stored == exact); } } From 9e8c3caef4fb5a73361d40778eac609ecb4e03b6 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 2 Jun 2026 15:35:42 -0400 Subject: [PATCH 38/77] Improve accuracy of Number::operator+= - Use more of the available range of the uint128 operands. - Also refactor Number::Guard::round() to return an enum. --- src/libxrpl/basics/Number.cpp | 38 ++++++++++------- src/test/basics/Number_test.cpp | 75 ++++++++++++++++++++------------- 2 files changed, 67 insertions(+), 46 deletions(-) diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 23be06475b..8e3887e475 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -206,10 +206,16 @@ public: void doDropDigit(T& mantissa, int& exponent) noexcept; + enum class Round { + Down = -1, + Even = 0, + 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]] int + [[nodiscard]] Round round() const noexcept; // Modify the result to the correctly rounded value @@ -314,41 +320,41 @@ Number::Guard::doDropDigit(uint128_t& mantissa, int& exponent) noexce // -1 if Guard is less than half // 0 if Guard is exactly half // 1 if Guard is greater than half -int +Number::Guard::Round Number::Guard::round() const noexcept { auto mode = Number::getround(); if (mode == RoundingMode::TowardsZero) - return -1; + return Round::Down; if (mode == RoundingMode::Downward) { if (sbit_) { if (digits_ > 0 || xbit_) - return 1; + return Round::Up; } - return -1; + return Round::Down; } if (mode == RoundingMode::Upward) { if (sbit_) - return -1; + return Round::Down; if (digits_ > 0 || xbit_) - return 1; - return -1; + return Round::Up; + return Round::Down; } // assume round to nearest if mode is not one of the predefined values if (digits_ > 0x5000'0000'0000'0000) - return 1; + return Round::Up; if (digits_ < 0x5000'0000'0000'0000) - return -1; + return Round::Down; if (xbit_) - return 1; - return 0; + return Round::Up; + return Round::Even; } template @@ -388,7 +394,7 @@ Number::Guard::doRoundUp( std::string location) { auto r = round(); - if (r == 1 || (r == 0 && (mantissa & 1) == 1)) + if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1)) { auto const safeToIncrement = [&maxMantissa](auto const& mantissa) { return mantissa < maxMantissa && mantissa < kMaxRep; @@ -454,7 +460,7 @@ Number::Guard::doRoundDown( internalrep const& minMantissa) { auto r = round(); - if (r == 1 || (r == 0 && (mantissa & 1) == 1)) + if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1)) { --mantissa; if (mantissa < minMantissa) @@ -471,7 +477,7 @@ void Number::Guard::doRound(rep& drops, std::string location) const { auto r = round(); - if (r == 1 || (r == 0 && (drops & 1) == 1)) + if (r == Round::Up || (r == Round::Even && (drops & 1) == 1)) { if (drops >= kMaxRep) { @@ -817,7 +823,7 @@ Number::operator+=(Number const& y) --xe; } g.doRoundDown(xn, xm, xe, minMantissa); - if (range.cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled) + if (range.cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled && xm != 0) { // make a new guard Guard g; diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 620ea798bc..034b6b1983 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -179,33 +179,34 @@ public: testcase << "test_add " << to_string(scale); using Case = std::tuple; - auto const cSmall = std::to_array( - {{Number{1'000'000'000'000'000, -15}, - Number{6'555'555'555'555'555, -29}, - Number{1'000'000'000'000'066, -15}, - __LINE__}, - {Number{-1'000'000'000'000'000, -15}, - Number{-6'555'555'555'555'555, -29}, - Number{-1'000'000'000'000'066, -15}, - __LINE__}, - {Number{-1'000'000'000'000'000, -15}, - Number{6'555'555'555'555'555, -29}, - Number{-9'999'999'999'999'344, -16}, - __LINE__}, - {Number{-6'555'555'555'555'555, -29}, - Number{1'000'000'000'000'000, -15}, - Number{9'999'999'999'999'344, -16}, - __LINE__}, - {Number{}, Number{5}, Number{5}, __LINE__}, - {Number{5}, Number{}, Number{5}, __LINE__}, - {Number{5'555'555'555'555'555, -32768}, - Number{-5'555'555'555'555'554, -32768}, - Number{0}, - __LINE__}, - {Number{-9'999'999'999'999'999, -31}, - Number{1'000'000'000'000'000, -15}, - Number{9'999'999'999'999'990, -16}, - __LINE__}}); + auto const cSmall = std::to_array({ + {Number{1'000'000'000'000'000, -15}, + Number{6'555'555'555'555'555, -29}, + Number{1'000'000'000'000'066, -15}, + __LINE__}, + {Number{-1'000'000'000'000'000, -15}, + Number{-6'555'555'555'555'555, -29}, + Number{-1'000'000'000'000'066, -15}, + __LINE__}, + {Number{-1'000'000'000'000'000, -15}, + Number{6'555'555'555'555'555, -29}, + Number{-9'999'999'999'999'344, -16}, + __LINE__}, + {Number{-6'555'555'555'555'555, -29}, + Number{1'000'000'000'000'000, -15}, + Number{9'999'999'999'999'344, -16}, + __LINE__}, + {Number{}, Number{5}, Number{5}, __LINE__}, + {Number{5}, Number{}, Number{5}, __LINE__}, + {Number{5'555'555'555'555'555, -32768}, + Number{-5'555'555'555'555'554, -32768}, + Number{0}, + __LINE__}, + {Number{-9'999'999'999'999'999, -31}, + Number{1'000'000'000'000'000, -15}, + Number{9'999'999'999'999'990, -16}, + __LINE__}, + }); auto const cLarge = std::to_array( // Note that items with extremely large mantissas need to be // calculated, because otherwise they overflow uint64. Items from C @@ -2021,13 +2022,27 @@ public: BigInt const exact = toBigInt(a) + toBigInt(b); BigInt const stored = toBigInt(sum); + BigInt const diff = stored - exact; - log << "\n exact a + b = " << exact.str() << "\n TowardsZero = " << stored.str() - << "\n"; + log << "\n a = " << a << "\n b = " << b + << "\n exact a + b = " << exact.str() << "\n TowardsZero = " << stored.str() + << "\n difference = " << diff.str() << "\n"; log.flush(); - if (scale != MantissaRange::MantissaScale::LargeLegacy) + if (scale == MantissaRange::MantissaScale::Small) + { BEAST_EXPECT(stored == exact); + } + else if (scale == MantissaRange::MantissaScale::LargeLegacy) + { + BEAST_EXPECT(stored > exact); + } + else + { + BEAST_EXPECT(stored < exact); + BEAST_EXPECT(diff < 0); + BEAST_EXPECT(-diff < pow10(sum.exponent())); + } } } From c84939cceaf177b61964321c648b16c0d0d11845 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 2 Jun 2026 15:59:26 -0400 Subject: [PATCH 39/77] Remove the kMaxRep+1 rounding tests --- src/test/basics/Number_test.cpp | 80 --------------------------------- 1 file changed, 80 deletions(-) diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 034b6b1983..a68dcd3286 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -1922,86 +1922,6 @@ public: } } - { - testcase << "normalization cusp: ToNearest and Downward disagree " << to_string(scale); - - constexpr auto kMaxRep = Number::kMaxRep; - - // Both ToNearest and Downward should round to `below` - auto const actual = static_cast(kMaxRep) + 1; - Number const below{static_cast(kMaxRep), 0}; - Number const above{ - false, static_cast(kMaxRep) + 3, 0, Number::Unchecked{}}; - - auto construct = [](Number::RoundingMode mode) { - NumberRoundModeGuard const roundGuard{mode}; - return Number(false, actual, 0, Number::Normalized{}); - }; - Number const upward = construct(Number::RoundingMode::Upward); - - Number const toNearest = construct(Number::RoundingMode::ToNearest); - - Number const downward = construct(Number::RoundingMode::Downward); - - log << "\n" - << " actual = " << actual << " (kMaxRep + 1)\n" - << " below = " << below << " (kMaxRep, distance 1)\n" - << " above = " << above << " (kMaxRep + 3, distance 2)\n" - << " Upward = " << upward << "\n" - << " ToNearest = " << toNearest << "\n" - << " Downward = " << downward << "\n\n"; - log.flush(); - - switch (scale) - { - case MantissaRange::MantissaScale::Small: - // With the small mantissa, everything rounds up - - // Upward round UP - BEAST_EXPECT(upward > above); - - // ToNearest rounds UP when the DOWN neighbor is strictly closer - BEAST_EXPECT(toNearest > above); - BEAST_EXPECT(toNearest == below); - - // Downward undershoots: it returns a value below `below` - BEAST_EXPECT(downward < below); - - // Both should have given the same answer, but they differ - BEAST_EXPECT(toNearest > downward); - - break; - - case MantissaRange::MantissaScale::LargeLegacy: - // Upward round UP - BEAST_EXPECT(upward == above); - - // ToNearest rounds UP when the DOWN neighbor is strictly closer - BEAST_EXPECT(toNearest == above); - BEAST_EXPECT(toNearest > below); - - // Downward undershoots: it returns a value below `below` - BEAST_EXPECT(downward < below); - - // Both should have given the same answer, but they differ - BEAST_EXPECT(toNearest > downward); - - break; - default: - // Upward round UP - BEAST_EXPECT(upward == above); - - // ToNearest rounds UP when the DOWN neighbor is strictly closer - BEAST_EXPECT(toNearest != above); - BEAST_EXPECT(toNearest == below); - - // Downward undershoots: it returns a value below `below` - BEAST_EXPECT(downward == below); - - // Both should have given the same answer, but they differ - BEAST_EXPECT(toNearest == downward); - } - } { testcase << "operator+ TowardsZero rounds away from zero " << to_string(scale); From 51902cd6b4895d47cd6bdb75b722879ca38a90ca Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 2 Jun 2026 16:03:08 -0400 Subject: [PATCH 40/77] Revert "Remove the kMaxRep+1 rounding tests" This reverts commit c84939cceaf177b61964321c648b16c0d0d11845. --- src/test/basics/Number_test.cpp | 80 +++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index a68dcd3286..034b6b1983 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -1922,6 +1922,86 @@ public: } } + { + testcase << "normalization cusp: ToNearest and Downward disagree " << to_string(scale); + + constexpr auto kMaxRep = Number::kMaxRep; + + // Both ToNearest and Downward should round to `below` + auto const actual = static_cast(kMaxRep) + 1; + Number const below{static_cast(kMaxRep), 0}; + Number const above{ + false, static_cast(kMaxRep) + 3, 0, Number::Unchecked{}}; + + auto construct = [](Number::RoundingMode mode) { + NumberRoundModeGuard const roundGuard{mode}; + return Number(false, actual, 0, Number::Normalized{}); + }; + Number const upward = construct(Number::RoundingMode::Upward); + + Number const toNearest = construct(Number::RoundingMode::ToNearest); + + Number const downward = construct(Number::RoundingMode::Downward); + + log << "\n" + << " actual = " << actual << " (kMaxRep + 1)\n" + << " below = " << below << " (kMaxRep, distance 1)\n" + << " above = " << above << " (kMaxRep + 3, distance 2)\n" + << " Upward = " << upward << "\n" + << " ToNearest = " << toNearest << "\n" + << " Downward = " << downward << "\n\n"; + log.flush(); + + switch (scale) + { + case MantissaRange::MantissaScale::Small: + // With the small mantissa, everything rounds up + + // Upward round UP + BEAST_EXPECT(upward > above); + + // ToNearest rounds UP when the DOWN neighbor is strictly closer + BEAST_EXPECT(toNearest > above); + BEAST_EXPECT(toNearest == below); + + // Downward undershoots: it returns a value below `below` + BEAST_EXPECT(downward < below); + + // Both should have given the same answer, but they differ + BEAST_EXPECT(toNearest > downward); + + break; + + case MantissaRange::MantissaScale::LargeLegacy: + // Upward round UP + BEAST_EXPECT(upward == above); + + // ToNearest rounds UP when the DOWN neighbor is strictly closer + BEAST_EXPECT(toNearest == above); + BEAST_EXPECT(toNearest > below); + + // Downward undershoots: it returns a value below `below` + BEAST_EXPECT(downward < below); + + // Both should have given the same answer, but they differ + BEAST_EXPECT(toNearest > downward); + + break; + default: + // Upward round UP + BEAST_EXPECT(upward == above); + + // ToNearest rounds UP when the DOWN neighbor is strictly closer + BEAST_EXPECT(toNearest != above); + BEAST_EXPECT(toNearest == below); + + // Downward undershoots: it returns a value below `below` + BEAST_EXPECT(downward == below); + + // Both should have given the same answer, but they differ + BEAST_EXPECT(toNearest == downward); + } + } { testcase << "operator+ TowardsZero rounds away from zero " << to_string(scale); From 015d9a6cb923a506cf8d75670d6f5f7185364d53 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 3 Jun 2026 18:37:53 -0400 Subject: [PATCH 41/77] Round mantissas between kMaxRep and kMaxRepUp - Treat values in between kMaxRep (2^63-1) and kMaxRepUp (((kMaxRep / 10) + 1) * 10, which is the next multiple of 10 above kMaxRep) as if those values were sequential, and values in between were "fractional". - This results in values above the midpoint rounding up to kMaxRepUp, and below the midpoint to kMaxRep when rounding to nearest. Other rounding modes act along the same lines. - Also refactor "Number::Guard::round()` to return an enum making it clearer what's going on. --- include/xrpl/basics/Number.h | 8 +- src/libxrpl/basics/Number.cpp | 228 ++++++++++++++++++++------------ src/test/basics/Number_test.cpp | 2 + 3 files changed, 154 insertions(+), 84 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index ffb991a41d..984e492d65 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -147,7 +147,7 @@ struct MantissaRange final int const log{getExponent(scale)}; rep const min{getMin(scale, log)}; rep const max{(min * 10) - 1}; - CuspRoundingFix const cuspRoundingFixEnabled{isCuspFixEnabled(scale)}; + CuspRoundingFix const cuspRoundingFix{isCuspFixEnabled(scale)}; static MantissaRange const& getMantissaRange(MantissaScale scale); @@ -325,6 +325,8 @@ public: static constexpr internalrep kMaxRep = std::numeric_limits::max(); static_assert(kMaxRep == 9'223'372'036'854'775'807); static_assert(-kMaxRep == std::numeric_limits::min() + 1); + static constexpr internalrep kMaxRepUp = ((kMaxRep / 10) + 1) * 10; + static_assert(kMaxRepUp == 9'223'372'036'854'775'810ULL); // May need to make unchecked private struct Unchecked @@ -566,7 +568,7 @@ private: int& exponent, internalrep const& minMantissa, internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled); + MantissaRange::CuspRoundingFix cuspRoundingFix); template friend void @@ -576,7 +578,7 @@ private: int& exponent, MantissaRange::rep const& minMantissa, MantissaRange::rep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled, + MantissaRange::CuspRoundingFix cuspRoundingFix, bool dropped); [[nodiscard]] bool diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 8e3887e475..d7463130ae 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -65,7 +65,7 @@ MantissaRange::getRanges() static_assert(kRange.log == 15); static_assert(kRange.min < Number::kMaxRep); static_assert(kRange.max < Number::kMaxRep); - static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Disabled); + static_assert(kRange.cuspRoundingFix == CuspRoundingFix::Disabled); } { [[maybe_unused]] @@ -76,7 +76,7 @@ MantissaRange::getRanges() static_assert(kRange.log == 18); static_assert(kRange.min < Number::kMaxRep); static_assert(kRange.max > Number::kMaxRep); - static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Disabled); + static_assert(kRange.cuspRoundingFix == CuspRoundingFix::Disabled); } { [[maybe_unused]] @@ -87,7 +87,7 @@ MantissaRange::getRanges() static_assert(kRange.log == 18); static_assert(kRange.min < Number::kMaxRep); static_assert(kRange.max > Number::kMaxRep); - static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Enabled); + static_assert(kRange.cuspRoundingFix == CuspRoundingFix::Enabled); } return map; }(); @@ -206,18 +206,6 @@ public: void doDropDigit(T& mantissa, int& exponent) noexcept; - enum class Round { - Down = -1, - Even = 0, - 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 @@ -227,25 +215,52 @@ public: int& exponent, internalrep const& minMantissa, internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled, + MantissaRange::CuspRoundingFix cuspRoundingFix, std::string location); // Modify the result to the correctly rounded value template void - doRoundDown(bool& negative, T& mantissa, int& exponent, internalrep const& minMantissa); + doRoundDown( + bool& negative, + T& mantissa, + int& exponent, + internalrep const& minMantissa, + MantissaRange::CuspRoundingFix cuspRoundingFix); // Modify the result to the correctly rounded value void - doRound(rep& drops, std::string location) const; + doRound(rep& drops, MantissaRange::CuspRoundingFix cuspRoundingFix, std::string location); private: + template + void + pushOverflow(T const& mantissa, MantissaRange::CuspRoundingFix cuspRoundingFix); + + enum class Round { + Down = -1, + Even = 0, + 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; + void doPush(unsigned d) noexcept; template void - bringIntoRange(bool& negative, T& mantissa, int& exponent, internalrep const& minMantissa); + bringIntoRange( + bool& negative, + T& mantissa, + int& exponent, + internalrep const& minMantissa, + MantissaRange::CuspRoundingFix cuspRoundingFix); }; inline void @@ -316,10 +331,42 @@ Number::Guard::doDropDigit(uint128_t& mantissa, int& exponent) noexce ++exponent; } +template +void +Number::Guard::pushOverflow(T const& mantissa, MantissaRange::CuspRoundingFix cuspRoundingFix) +{ + XRPL_ASSERT(mantissa <= kMaxRepUp, "xrpl::Number::Guard::doRoundUp : valid mantissa"); + if (cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled && mantissa > kMaxRep && + mantissa < kMaxRepUp) + { + // Special case rounding rules for the values between kMaxRep and kMaxRepUp. + // Scale the spread between kMaxRep and kMaxRepUp from 1 to 9, and push it onto the guard as + // if it was a digit that got removed, but don't remove it. This method is future-proof in + // case the number of mantissa bits ever changes. Effects: + // * For round to nearest + // * if the mantissa is below the midpoint, it'll round "down" to kMaxRepUp + // * if above the midpoint, it'll round "down" to kMaxRep + // * if can never be exactly at the midpoint, because kMaxRepUp is always even, and + // kMaxRep is always odd, so don't worry about it. + // * For round upward, will round up to kMaxRepUp for positive values, down for negative. + // * For round downward, does the opposite of upward. + // * For round toward zero, always rounds down. + auto constexpr spread = kMaxRepUp - kMaxRep; + static_assert(spread < 10 && spread >= 0); + + auto const diff = mantissa - kMaxRep; + auto const digit = (diff * 10) / spread; + XRPL_ASSERT(digit > 0 && digit < 10, "xrpld::Number::Guard::xxxx : 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 +// 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 { @@ -363,16 +410,19 @@ Number::Guard::bringIntoRange( bool& negative, T& mantissa, int& exponent, - internalrep const& minMantissa) + internalrep const& minMantissa, + MantissaRange::CuspRoundingFix cuspRoundingFix) { // Bring mantissa back into the minMantissa / maxMantissa range AFTER // rounding - if (mantissa < minMantissa) + if (mantissa < minMantissa && + (cuspRoundingFix == MantissaRange::CuspRoundingFix::Disabled || mantissa != 0)) { mantissa *= 10; --exponent; } - if (exponent < kMinExponent) + if (exponent < kMinExponent || + (cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled && mantissa == 0)) { static constexpr Number kZero = Number{}; @@ -390,16 +440,18 @@ Number::Guard::doRoundUp( int& exponent, internalrep const& minMantissa, internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled, + MantissaRange::CuspRoundingFix cuspRoundingFix, std::string location) { - auto r = round(); + pushOverflow(mantissa, cuspRoundingFix); + + auto const r = round(); if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1)) { auto const safeToIncrement = [&maxMantissa](auto const& mantissa) { return mantissa < maxMantissa && mantissa < kMaxRep; }; - if (cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled) + if (cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled) { // Ensure mantissa after incrementing fits within both the // min/maxMantissa range and is a valid "rep". @@ -415,6 +467,9 @@ Number::Guard::doRoundUp( // 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. + if (cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled && + mantissa > kMaxRep && mantissa < kMaxRepUp) + mantissa = kMaxRepUp; doDropDigit(mantissa, exponent); XRPL_ASSERT_PARTS( safeToIncrement(mantissa), @@ -426,7 +481,7 @@ Number::Guard::doRoundUp( exponent, minMantissa, maxMantissa, - cuspRoundingFixEnabled, + cuspRoundingFix, location); return; } @@ -446,7 +501,11 @@ Number::Guard::doRoundUp( } } } - bringIntoRange(negative, mantissa, exponent, minMantissa); + else if (cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled && mantissa > kMaxRep) + { + mantissa = kMaxRep; + } + bringIntoRange(negative, mantissa, exponent, minMantissa, cuspRoundingFix); if (exponent > kMaxExponent) Throw(std::string(location)); } @@ -457,8 +516,11 @@ Number::Guard::doRoundDown( bool& negative, T& mantissa, int& exponent, - internalrep const& minMantissa) + internalrep const& minMantissa, + MantissaRange::CuspRoundingFix cuspRoundingFix) { + // Do not pushOverflow here. + auto r = round(); if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1)) { @@ -469,13 +531,18 @@ Number::Guard::doRoundDown( --exponent; } } - bringIntoRange(negative, mantissa, exponent, minMantissa); + bringIntoRange(negative, mantissa, exponent, minMantissa, cuspRoundingFix); } // Modify the result to the correctly rounded value void -Number::Guard::doRound(rep& drops, std::string location) const +Number::Guard::doRound( + rep& drops, + MantissaRange::CuspRoundingFix cuspRoundingFix, + std::string location) { + pushOverflow(drops, cuspRoundingFix); + auto r = round(); if (r == Round::Up || (r == Round::Even && (drops & 1) == 1)) { @@ -492,6 +559,12 @@ Number::Guard::doRound(rep& drops, std::string location) const } ++drops; } + else if (cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled && drops > kMaxRep) + { + // This will probably be impossible because this function is not called by mutating + // functions, so the Number will already be normalized. + drops = kMaxRep; + } if (isNegative()) drops = -drops; } @@ -536,12 +609,14 @@ doNormalize( int& exponent, MantissaRange::rep const& minMantissa, MantissaRange::rep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled, + MantissaRange::CuspRoundingFix cuspRoundingFix, bool dropped) { static constexpr auto kMinExponent = Number::kMinExponent; static constexpr auto kMaxExponent = Number::kMaxExponent; - static constexpr auto kMaxRep = Number::kMaxRep; + auto const kRepLimit = cuspRoundingFix == MantissaRange::CuspRoundingFix::Disabled + ? Number::kMaxRep + : Number::kMaxRepUp; using Guard = Number::Guard; @@ -591,7 +666,7 @@ 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 > kRepLimit) { if (exponent >= kMaxExponent) throw std::overflow_error("Number::normalize 1.5"); @@ -601,7 +676,7 @@ doNormalize( // 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"); + XRPL_ASSERT_PARTS(m <= kRepLimit, "xrpl::doNormalize", "intermediate mantissa fits in limit"); mantissa = m; g.doRoundUp( @@ -610,7 +685,7 @@ doNormalize( exponent, minMantissa, maxMantissa, - cuspRoundingFixEnabled, + cuspRoundingFix, "Number::normalize 2"); XRPL_ASSERT_PARTS( mantissa >= minMantissa && mantissa <= maxMantissa, @@ -626,13 +701,12 @@ Number::normalize( int& exponent, internalrep const& minMantissa, internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled) + MantissaRange::CuspRoundingFix cuspRoundingFix) { // Not used by every compiler version, and thus not necessarily // counted by coverage build // LCOV_EXCL_START - doNormalize( - negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false); + doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFix, false); // LCOV_EXCL_STOP } @@ -644,13 +718,12 @@ Number::normalize( int& exponent, internalrep const& minMantissa, internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled) + MantissaRange::CuspRoundingFix cuspRoundingFix) { // Not used by every compiler version, and thus not necessarily // counted by coverage build // LCOV_EXCL_START - doNormalize( - negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false); + doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFix, false); // LCOV_EXCL_STOP } @@ -662,16 +735,15 @@ Number::normalize( int& exponent, internalrep const& minMantissa, internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled) + MantissaRange::CuspRoundingFix cuspRoundingFix) { - doNormalize( - negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false); + doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFix, false); } void Number::normalize(MantissaRange const& range) { - normalize(negative_, mantissa_, exponent_, range.min, range.max, range.cuspRoundingFixEnabled); + normalize(negative_, mantissa_, exponent_, range.min, range.max, range.cuspRoundingFix); } // Copy the number, but set a new exponent. Because the mantissa doesn't change, @@ -729,6 +801,12 @@ Number::operator+=(Number const& y) auto const& range = kRange.get(); + auto const& minMantissa = range.min; + auto const& maxMantissa = range.max; + auto const cuspRoundingFix = range.cuspRoundingFix; + auto const kRepLimit = + cuspRoundingFix == MantissaRange::CuspRoundingFix::Disabled ? kMaxRep : kMaxRepUp; + // Bring the exponents of both values into agreement, so the mantissas are on the same scale // and can be added directly together // expandM / expandE: First try to expand the mantissa and bring the exponent down @@ -737,7 +815,7 @@ Number::operator+=(Number const& y) uint128_t& expandM, int& expandE, uint128_t& shrinkM, int& shrinkE) { constexpr uint128_t kSafeLimit = kPowerOfTenImpl[37]; - if (range.cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled) + if (range.cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled) { while (shrinkE < expandE && shrinkM % 10 == 0) { @@ -774,35 +852,25 @@ Number::operator+=(Number const& y) adjust(xm, xe, ym, ye); } - auto const& minMantissa = range.min; - auto const& maxMantissa = range.max; - auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled; - if (xn == yn) { xm += ym; - if (range.cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled) + if (range.cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled) { - while (xm > maxMantissa || xm > kMaxRep) + while (xm > maxMantissa || xm > kRepLimit) { g.doDropDigit(xm, xe); } } else { - if (xm > maxMantissa || xm > kMaxRep) + if (xm > maxMantissa || xm > kRepLimit) { g.doDropDigit(xm, xe); } } g.doRoundUp( - xn, - xm, - xe, - minMantissa, - maxMantissa, - cuspRoundingFixEnabled, - "Number::addition overflow"); + xn, xm, xe, minMantissa, maxMantissa, cuspRoundingFix, "Number::addition overflow"); } else { @@ -816,31 +884,25 @@ Number::operator+=(Number const& y) xe = ye; xn = yn; } - while (xm < minMantissa && xm * 10 <= kMaxRep) + while (xm < minMantissa && xm * 10 <= kRepLimit) { xm *= 10; xm -= g.pop(); --xe; } - g.doRoundDown(xn, xm, xe, minMantissa); - if (range.cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled && xm != 0) + g.doRoundDown(xn, xm, xe, minMantissa, cuspRoundingFix); + if (range.cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled && xm != 0) { // make a new guard Guard g; if (xn) g.setNegative(); - while (xm > maxMantissa || xm > kMaxRep) + while (xm > maxMantissa || xm > kRepLimit) { g.doDropDigit(xm, xe); } g.doRoundUp( - xn, - xm, - xe, - minMantissa, - maxMantissa, - cuspRoundingFixEnabled, - "Number::addition overflow"); + xn, xm, xe, minMantissa, maxMantissa, cuspRoundingFix, "Number::addition overflow"); } } @@ -888,9 +950,11 @@ Number::operator*=(Number const& y) auto const& range = kRange.get(); auto const& minMantissa = range.min; auto const& maxMantissa = range.max; - auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled; + auto const cuspRoundingFix = range.cuspRoundingFix; + auto const kRepLimit = + cuspRoundingFix == MantissaRange::CuspRoundingFix::Disabled ? kMaxRep : kMaxRepUp; - while (zm > maxMantissa || zm > kMaxRep) + while (zm > maxMantissa || zm > kRepLimit) { g.doDropDigit(zm, ze); } @@ -903,7 +967,7 @@ Number::operator*=(Number const& y) xe, minMantissa, maxMantissa, - cuspRoundingFixEnabled, + cuspRoundingFix, "Number::multiplication overflow : exponent is " + std::to_string(xe)); negative_ = zn; mantissa_ = xm; @@ -945,7 +1009,7 @@ Number::operator/=(Number const& y) auto const& range = kRange.get(); auto const& minMantissa = range.min; auto const& maxMantissa = range.max; - auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled; + auto const cuspRoundingFix = range.cuspRoundingFix; // Division operates on two large integers (16-digit for small // mantissas, 19-digit for large) using integer math. If the values @@ -1077,14 +1141,14 @@ Number::operator/=(Number const& y) // rounding fix is enabled, flag if there is still // a remainder from stage 2. bool const useTrailingRemainder = - cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled; + cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled; if (useTrailingRemainder) { dropped = partialNumerator % dm != 0; } } } - doNormalize(zp, zm, ze, minMantissa, maxMantissa, cuspRoundingFixEnabled, dropped); + doNormalize(zp, zm, ze, minMantissa, maxMantissa, cuspRoundingFix, dropped); negative_ = zp; mantissa_ = static_cast(zm); exponent_ = ze; @@ -1096,6 +1160,8 @@ Number::operator/=(Number const& y) Number:: operator rep() const { + auto const& range = kRange.get(); + rep drops = mantissa(); int offset = exponent(); Guard g; @@ -1116,7 +1182,7 @@ operator rep() const throw std::overflow_error("Number::operator rep() overflow"); drops *= 10; } - g.doRound(drops, "Number::operator rep() rounding overflow"); + g.doRound(drops, range.cuspRoundingFix, "Number::operator rep() rounding overflow"); } return drops; } diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 034b6b1983..4ec9d41ee5 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -178,6 +178,8 @@ public: auto const scale = Number::getMantissaScale(); testcase << "test_add " << to_string(scale); + BEAST_EXPECT(Number::getround() == Number::RoundingMode::ToNearest); + using Case = std::tuple; auto const cSmall = std::to_array({ {Number{1'000'000'000'000'000, -15}, From b5574baa36a5b40ff23db2f5186327e8d26ae43b Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 4 Jun 2026 14:40:39 -0400 Subject: [PATCH 42/77] Add line numbers to Number::to_string test --- src/test/basics/Number_test.cpp | 91 ++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 35 deletions(-) diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 4ec9d41ee5..1fe88f2f85 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -1337,38 +1337,38 @@ public: auto const scale = Number::getMantissaScale(); testcase << "testToString " << to_string(scale); - auto test = [this](Number const& n, std::string const& expected) { + auto test = [this](Number const& n, std::string const& expected, int line) { auto const result = to_string(n); std::stringstream ss; ss << "to_string(" << result << "). Expected: " << expected; - BEAST_EXPECTS(result == expected, ss.str()); + expect(result == expected, ss.str(), __FILE__, line); }; - test(Number(-2, 0), "-2"); - test(Number(0, 0), "0"); - test(Number(2, 0), "2"); - test(Number(25, -3), "0.025"); - test(Number(-25, -3), "-0.025"); - test(Number(25, 1), "250"); - test(Number(-25, 1), "-250"); - test(Number(2, 20), "2e20"); - test(Number(-2, -20), "-2e-20"); + test(Number(-2, 0), "-2", __LINE__); + test(Number(0, 0), "0", __LINE__); + test(Number(2, 0), "2", __LINE__); + test(Number(25, -3), "0.025", __LINE__); + test(Number(-25, -3), "-0.025", __LINE__); + test(Number(25, 1), "250", __LINE__); + test(Number(-25, 1), "-250", __LINE__); + test(Number(2, 20), "2e20", __LINE__); + test(Number(-2, -20), "-2e-20", __LINE__); // Test the edges // ((exponent < -(25)) || (exponent > -(5))))) // or ((exponent < -(28)) || (exponent > -(8))))) - test(Number(2, -10), "0.0000000002"); - test(Number(2, -11), "2e-11"); + test(Number(2, -10), "0.0000000002", __LINE__); + test(Number(2, -11), "2e-11", __LINE__); - test(Number(-2, 10), "-20000000000"); - test(Number(-2, 11), "-2e11"); + test(Number(-2, 10), "-20000000000", __LINE__); + test(Number(-2, 11), "-2e11", __LINE__); switch (scale) { case MantissaRange::MantissaScale::Small: - test(Number::min(), "1e-32753"); - test(Number::max(), "9999999999999999e32768"); - test(Number::lowest(), "-9999999999999999e32768"); + test(Number::min(), "1e-32753", __LINE__); + test(Number::max(), "9999999999999999e32768", __LINE__); + test(Number::lowest(), "-9999999999999999e32768", __LINE__); { NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero); @@ -1376,62 +1376,83 @@ public: BEAST_EXPECT(maxMantissa == 9'999'999'999'999'999); test( Number{false, (maxMantissa * 1000) + 999, -3, Number::Normalized()}, - "9999999999999999"); + "9999999999999999", + __LINE__); test( Number{true, (maxMantissa * 1000) + 999, -3, Number::Normalized()}, - "-9999999999999999"); + "-9999999999999999", + __LINE__); - test(Number{std::numeric_limits::max(), -3}, "9223372036854775"); + test( + Number{std::numeric_limits::max(), -3}, + "9223372036854775", + __LINE__); test( -(Number{std::numeric_limits::max(), -3}), - "-9223372036854775"); + "-9223372036854775", + __LINE__); test( - Number{std::numeric_limits::min(), 0}, "-9223372036854775e3"); + Number{std::numeric_limits::min(), 0}, + "-9223372036854775e3", + __LINE__); test( -(Number{std::numeric_limits::min(), 0}), - "9223372036854775e3"); + "9223372036854775e3", + __LINE__); } break; case MantissaRange::MantissaScale::LargeLegacy: case MantissaRange::MantissaScale::Large: // Test the edges // ((exponent < -(28)) || (exponent > -(8))))) - test(Number::min(), "1e-32750"); - test(Number::max(), "9223372036854775807e32768"); - test(Number::lowest(), "-9223372036854775807e32768"); + test(Number::min(), "1e-32750", __LINE__); + test(Number::max(), "9223372036854775807e32768", __LINE__); + test(Number::lowest(), "-9223372036854775807e32768", __LINE__); { NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero); auto const maxMantissa = Number::maxMantissa(); BEAST_EXPECT(maxMantissa == 9'999'999'999'999'999'999ULL); test( - Number{false, maxMantissa, 0, Number::Normalized{}}, "9999999999999999990"); + Number{false, maxMantissa, 0, Number::Normalized{}}, + "9999999999999999990", + __LINE__); test( - Number{true, maxMantissa, 0, Number::Normalized{}}, "-9999999999999999990"); + Number{true, maxMantissa, 0, Number::Normalized{}}, + "-9999999999999999990", + __LINE__); test( - Number{std::numeric_limits::max(), 0}, "9223372036854775807"); + Number{std::numeric_limits::max(), 0}, + "9223372036854775807", + __LINE__); test( -(Number{std::numeric_limits::max(), 0}), - "-9223372036854775807"); + "-9223372036854775807", + __LINE__); // Because the absolute value of min is larger than max, it // will be scaled down to fit under max. Since we're // rounding towards zero, the 8 at the end is dropped. test( Number{std::numeric_limits::min(), 0}, - "-9223372036854775800"); + "-9223372036854775800", + __LINE__); test( -(Number{std::numeric_limits::min(), 0}), - "9223372036854775800"); + "9223372036854775800", + __LINE__); } test( - Number{std::numeric_limits::max(), 0} + 1, "9223372036854775810"); + Number{std::numeric_limits::max(), 0} + 1, + "9223372036854775810", + __LINE__); test( -(Number{std::numeric_limits::max(), 0} + 1), - "-9223372036854775810"); + "-9223372036854775810", + __LINE__); break; default: BEAST_EXPECT(false); From 50c0d9f2b00842c48e8b20fc0a0bb081a223a15c Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 4 Jun 2026 18:53:50 -0400 Subject: [PATCH 43/77] Handle a whole bunch of edge cases - Add more tests --- src/libxrpl/basics/Number.cpp | 64 ++++++----- src/test/basics/Number_test.cpp | 193 ++++++++++++++++++++++++-------- 2 files changed, 182 insertions(+), 75 deletions(-) diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index d7463130ae..083ed7fad9 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -467,23 +467,27 @@ Number::Guard::doRoundUp( // 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. - if (cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled && - mantissa > kMaxRep && mantissa < kMaxRepUp) + if (mantissa > kMaxRep && mantissa < kMaxRepUp) + { mantissa = kMaxRepUp; - doDropDigit(mantissa, exponent); - XRPL_ASSERT_PARTS( - safeToIncrement(mantissa), - "xrpl::Number::Guard::doRoundUp", - "can't recurse more than once"); - doRoundUp( - negative, - mantissa, - exponent, - minMantissa, - maxMantissa, - cuspRoundingFix, - location); - return; + } + else + { + doDropDigit(mantissa, exponent); + XRPL_ASSERT_PARTS( + safeToIncrement(mantissa), + "xrpl::Number::Guard::doRoundUp", + "can't recurse more than once"); + doRoundUp( + negative, + mantissa, + exponent, + minMantissa, + maxMantissa, + cuspRoundingFix, + location); + return; + } } } else @@ -501,7 +505,9 @@ Number::Guard::doRoundUp( } } } - else if (cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled && mantissa > kMaxRep) + else if ( + cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled && mantissa > kMaxRep && + mantissa < kMaxRepUp) { mantissa = kMaxRep; } @@ -559,7 +565,9 @@ Number::Guard::doRound( } ++drops; } - else if (cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled && drops > kMaxRep) + else if ( + cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled && drops > kMaxRep && + drops < kMaxRepUp) { // This will probably be impossible because this function is not called by mutating // functions, so the Number will already be normalized. @@ -614,7 +622,7 @@ doNormalize( { static constexpr auto kMinExponent = Number::kMinExponent; static constexpr auto kMaxExponent = Number::kMaxExponent; - auto const kRepLimit = cuspRoundingFix == MantissaRange::CuspRoundingFix::Disabled + auto const repLimit = cuspRoundingFix == MantissaRange::CuspRoundingFix::Disabled ? Number::kMaxRep : Number::kMaxRepUp; @@ -666,7 +674,7 @@ 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 > kRepLimit) + if (m > repLimit) { if (exponent >= kMaxExponent) throw std::overflow_error("Number::normalize 1.5"); @@ -676,7 +684,7 @@ doNormalize( // 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 <= kRepLimit, "xrpl::doNormalize", "intermediate mantissa fits in limit"); + XRPL_ASSERT_PARTS(m <= repLimit, "xrpl::doNormalize", "intermediate mantissa fits in limit"); mantissa = m; g.doRoundUp( @@ -804,7 +812,7 @@ Number::operator+=(Number const& y) auto const& minMantissa = range.min; auto const& maxMantissa = range.max; auto const cuspRoundingFix = range.cuspRoundingFix; - auto const kRepLimit = + auto const repLimit = cuspRoundingFix == MantissaRange::CuspRoundingFix::Disabled ? kMaxRep : kMaxRepUp; // Bring the exponents of both values into agreement, so the mantissas are on the same scale @@ -857,14 +865,14 @@ Number::operator+=(Number const& y) xm += ym; if (range.cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled) { - while (xm > maxMantissa || xm > kRepLimit) + while (xm > maxMantissa || xm > repLimit) { g.doDropDigit(xm, xe); } } else { - if (xm > maxMantissa || xm > kRepLimit) + if (xm > maxMantissa || xm > repLimit) { g.doDropDigit(xm, xe); } @@ -884,7 +892,7 @@ Number::operator+=(Number const& y) xe = ye; xn = yn; } - while (xm < minMantissa && xm * 10 <= kRepLimit) + while (xm < minMantissa && xm * 10 <= repLimit) { xm *= 10; xm -= g.pop(); @@ -897,7 +905,7 @@ Number::operator+=(Number const& y) Guard g; if (xn) g.setNegative(); - while (xm > maxMantissa || xm > kRepLimit) + while (xm > maxMantissa || xm > repLimit) { g.doDropDigit(xm, xe); } @@ -951,10 +959,10 @@ Number::operator*=(Number const& y) auto const& minMantissa = range.min; auto const& maxMantissa = range.max; auto const cuspRoundingFix = range.cuspRoundingFix; - auto const kRepLimit = + auto const repLimit = cuspRoundingFix == MantissaRange::CuspRoundingFix::Disabled ? kMaxRep : kMaxRepUp; - while (zm > maxMantissa || zm > kRepLimit) + while (zm > maxMantissa || zm > repLimit) { g.doDropDigit(zm, ze); } diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 1fe88f2f85..6249362c8e 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -365,7 +365,7 @@ public: Number{1'000'000'000'000'000, -15}, Number{1'000'000'000'000'000, -30}, __LINE__}}); - auto const cLarge = std::to_array( + auto const cLargeAll = std::to_array( // Note that items with extremely large mantissas need to be // calculated, because otherwise they overflow uint64. Items from C // with larger mantissa @@ -412,16 +412,55 @@ public: Number{1'000'000'000'000'000'000, -36}, __LINE__}, {Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep - 1}, __LINE__}, - {Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}}, - Number{1, 0}, - Number{(Number::kMaxRep / 10) + 1, 1}, - __LINE__}, - {Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}}, - Number{3, 0}, - Number{Number::kMaxRep}, - __LINE__}, - {power(2, 63), Number{3, 0}, Number{Number::kMaxRep}, __LINE__}, }); + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items from C + // with larger mantissa + auto const cLargeLegacy = std::to_array({ + // Anything larger than kMaxRep rounds up + {Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}}, + Number{1, 0}, + Number{(Number::kMaxRep / 10) + 1, 1}, + __LINE__}, + {Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}}, + Number{3, 0}, + Number{Number::kMaxRep}, + __LINE__}, + {Number{false, Number::kMaxRep + 2, 0, Number::Normalized{}}, + Number{1, 0}, + Number{(Number::kMaxRep / 10) + 1, 1}, + __LINE__}, + {Number{false, Number::kMaxRep + 2, 0, Number::Normalized{}}, + Number{3, 0}, + Number{Number::kMaxRep}, + __LINE__}, + {power(2, 63), Number{3, 0}, Number{Number::kMaxRep}, __LINE__}, + }); + auto const cLarge = std::to_array({ + // kMaxRep + 1 is below the half-way point, so it rounds down to kMaxRep when the Number + // is created. + {Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}}, + Number{1, 0}, + Number{Number::kMaxRep - 1}, + __LINE__}, + {Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}}, + Number{3, 0}, + Number{Number::kMaxRep - 3}, + __LINE__}, + // kMaxRepUp -1 is above the half-way point, so it rounds up to kMaxRepUp when the + // Number is created. Subtracting 1 from that rounds up again. A little non-intuitive. + {Number{false, Number::kMaxRepUp - 1, 0, Number::Normalized{}}, + Number{1, 0}, + Number{(Number::kMaxRep / 10) + 1, 1}, + __LINE__}, + // Subtracting 3 gets back down to kMaxRep + {Number{false, Number::kMaxRepUp - 1, 0, Number::Normalized{}}, + Number{3, 0}, + Number{Number::kMaxRep}, + __LINE__}, + // 2^63 is the same as kMaxRep+1 + {power(2, 63), Number{3, 0}, Number{Number::kMaxRep - 3}, __LINE__}, + }); auto test = [this](auto const& c) { for (auto const& [x, y, z, line] : c) { @@ -431,13 +470,22 @@ public: expect(result == z, ss.str(), __FILE__, line); } }; - if (scale == MantissaRange::MantissaScale::Small) + switch (scale) { - test(cSmall); - } - else - { - test(cLarge); + case MantissaRange::MantissaScale::Small: + test(cSmall); + break; + case MantissaRange::MantissaScale::LargeLegacy: + test(cLargeAll); + test(cLargeLegacy); + break; + case MantissaRange::MantissaScale::Large: + test(cLargeAll); + test(cLarge); + break; + default: + BEAST_EXPECT(false); + break; } } @@ -1432,25 +1480,78 @@ public: "-9223372036854775807", __LINE__); - // Because the absolute value of min is larger than max, it - // will be scaled down to fit under max. Since we're - // rounding towards zero, the 8 at the end is dropped. - test( - Number{std::numeric_limits::min(), 0}, - "-9223372036854775800", - __LINE__); - test( - -(Number{std::numeric_limits::min(), 0}), - "9223372036854775800", - __LINE__); + switch (scale) + { + case MantissaRange::MantissaScale::LargeLegacy: + // Because the absolute value of min() is larger than max(), it + // will be scaled down to fit under max(). Since we're + // rounding towards zero, the 8 at the end is dropped. + test( + Number{std::numeric_limits::min(), 0}, + "-9223372036854775800", + __LINE__); + test( + -(Number{std::numeric_limits::min(), 0}), + "9223372036854775800", + __LINE__); + break; + case MantissaRange::MantissaScale::Large: + // Because the absolute value of min() is larger than max(), it + // will be rounded down toward max() + test( + Number{std::numeric_limits::min(), 0}, + "-9223372036854775807", + __LINE__); + test( + -(Number{std::numeric_limits::min(), 0}), + "9223372036854775807", + __LINE__); + break; + default: + BEAST_EXPECT(false); + break; + } } + switch (scale) + { + case MantissaRange::MantissaScale::LargeLegacy: + // Rounding to nearest, since the mantissa is bigger than kMaxRep, the 8 + // will be dropped, and since that is bigger than 5, the result will be + // rounded up from 0 to 1. + test( + Number{std::numeric_limits::max(), 0} + 1, + "9223372036854775810", + __LINE__); + test( + -(Number{std::numeric_limits::max(), 0} + 1), + "-9223372036854775810", + __LINE__); + break; + case MantissaRange::MantissaScale::Large: + // Rounding to nearest, since the mantissa is below the halfway point from + // kMaxRep to kMaxRep up, it will be rounded down to kMaxRep + test( + Number{std::numeric_limits::max(), 0} + 1, + "9223372036854775807", + __LINE__); + test( + -(Number{std::numeric_limits::max(), 0} + 1), + "-9223372036854775807", + __LINE__); + break; + default: + BEAST_EXPECT(false); + break; + } + // Rounding to nearest, since the mantissa is above the halfway point from kMaxRep + // to kMaxRep up, it will be rounded up to kMaxRepUp. test( - Number{std::numeric_limits::max(), 0} + 1, + Number{std::numeric_limits::max(), 0} + 2, "9223372036854775810", __LINE__); test( - -(Number{std::numeric_limits::max(), 0} + 1), + -(Number{std::numeric_limits::max(), 0} + 2), "-9223372036854775810", __LINE__); break; @@ -1701,7 +1802,7 @@ public: } void - testUpwardRoundsDown() + testEdgeCases() { auto const scale = Number::getMantissaScale(); { @@ -1725,15 +1826,14 @@ public: BigInt const signedDifference = storedValue - exactProduct; - log << "\n" - << " a = " << fmt(BigInt(kAValue)) << "\n" + log << " a = " << fmt(BigInt(kAValue)) << "\n" << " b = " << fmt(BigInt(kBValue)) << "\n" << " exact a*b = " << fmt(exactProduct) << "\n" << " stored = " << fmt(storedValue) << "\n" << " stored - exact = " << fmt(signedDifference) << "\n" << " upward = " << (signedDifference >= 0 ? "held" : "VIOLATED") << "\n" << " stored.mantissa = " << product.mantissa() << "\n" - << " stored.exponent = " << product.exponent() << "\n"; + << " stored.exponent = " << product.exponent() << "\n\n"; log.flush(); switch (scale) @@ -1806,15 +1906,14 @@ public: dec const stored = dec(quotient.mantissa()) * pow10(quotient.exponent()); dec const diff = stored - exact; - log << "\n" - << " a = " << aValue << "\n" + log << " a = " << aValue << "\n" << " b = " << bValue << "\n" << " exact a/b = " << fmt(exact) << "\n" << " stored a/b = " << fmt(stored) << "\n" << " stored - exact = " << fmt(diff) << " (negative => Upward gave value BELOW truth)\n" << " quotient.mantissa = " << quotient.mantissa() << "\n" - << " quotient.exponent = " << quotient.exponent() << "\n"; + << " quotient.exponent = " << quotient.exponent() << "\n\n"; log.flush(); // Upward invariant: stored >= exact. Bug: stored < exact. @@ -1856,15 +1955,14 @@ public: dec const stored = dec(quotient.mantissa()) * pow10(quotient.exponent()); dec const diff = stored - exact; - log << "\n" - << " a = " << aValue << "\n" + log << " a = " << aValue << "\n" << " b = " << bValue << "\n" << " exact a/b = " << fmt(exact) << "\n" << " stored a/b = " << fmt(stored) << "\n" << " stored - exact = " << fmt(diff) << " (positive => Downward gave value ABOVE truth)\n" << " quotient.mantissa = " << quotient.mantissa() << "\n" - << " quotient.exponent = " << quotient.exponent() << "\n"; + << " quotient.exponent = " << quotient.exponent() << "\n\n"; log.flush(); // invariant: stored <= exact. Bug: stored > exact. @@ -1913,15 +2011,14 @@ public: dec const stored = dec(quotient.mantissa()) * pow10(quotient.exponent()); dec const diff = stored - exact; - log << "\n" - << " a = " << aValue << "\n" + log << " a = " << aValue << "\n" << " b = " << bValue << "\n" << " exact a/b = " << fmt(exact) << "\n" << " stored a/b = " << fmt(stored) << "\n" << " stored - exact = " << fmt(diff) << " (negative => ToNearest gave value BELOW truth)\n" << " quotient.mantissa = " << quotient.mantissa() << "\n" - << " quotient.exponent = " << quotient.exponent() << "\n"; + << " quotient.exponent = " << quotient.exponent() << "\n\n"; log.flush(); // invariant: stored >= exact. Bug: stored < exact. @@ -1966,8 +2063,7 @@ public: Number const downward = construct(Number::RoundingMode::Downward); - log << "\n" - << " actual = " << actual << " (kMaxRep + 1)\n" + log << " actual = " << actual << " (kMaxRep + 1)\n" << " below = " << below << " (kMaxRep, distance 1)\n" << " above = " << above << " (kMaxRep + 3, distance 2)\n" << " Upward = " << upward << "\n" @@ -2011,6 +2107,8 @@ public: break; default: + // Covers "Large" and any newly added scales + // Upward round UP BEAST_EXPECT(upward == above); @@ -2023,6 +2121,7 @@ public: // Both should have given the same answer, but they differ BEAST_EXPECT(toNearest == downward); + break; } } { @@ -2047,9 +2146,9 @@ public: BigInt const stored = toBigInt(sum); BigInt const diff = stored - exact; - log << "\n a = " << a << "\n b = " << b + log << " a = " << a << "\n b = " << b << "\n exact a + b = " << exact.str() << "\n TowardsZero = " << stored.str() - << "\n difference = " << diff.str() << "\n"; + << "\n difference = " << diff.str() << "\n\n"; log.flush(); if (scale == MantissaRange::MantissaScale::Small) @@ -2097,7 +2196,7 @@ public: testRounding(); testInt64(); - testUpwardRoundsDown(); + testEdgeCases(); } } }; From f1bb4ded21ca2f9e430b9a661fbe630c4410109b Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 4 Jun 2026 20:05:53 -0400 Subject: [PATCH 44/77] clang-tidy: template param names, const correctness, braces --- include/xrpl/basics/Number.h | 10 +++++----- src/test/basics/Number_test.cpp | 6 +++++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index ffb991a41d..8043a850a8 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -54,11 +54,11 @@ namespace detail { constexpr std::size_t kUint64Digits = 20; constexpr std::size_t kUint128Digits = 39; -template -consteval std::array +template +consteval std::array buildPowersOfTen() { - std::array result{}; + std::array result{}; T power = 1; std::size_t exponent = 0; @@ -78,8 +78,8 @@ buildPowersOfTen() } // namespace detail -template -constexpr std::array kPowerOfTenImpl = detail::buildPowersOfTen(); +template +constexpr std::array kPowerOfTenImpl = detail::buildPowersOfTen(); constexpr auto kPowerOfTen = kPowerOfTenImpl; diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index a68dcd3286..24cf5d6967 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -1698,7 +1698,7 @@ public: BigInt const exactProduct = BigInt(kAValue) * BigInt(kBValue); // What Number actually stored. - BigInt storedValue = toBigInt(product); + BigInt const storedValue = toBigInt(product); BigInt const signedDifference = storedValue - exactProduct; @@ -1930,9 +1930,13 @@ public: BEAST_EXPECT(toBigInt(a) == BigInt{"100000000000000000000"}); if (scale != MantissaRange::MantissaScale::Small) + { BEAST_EXPECT(toBigInt(b) == BigInt{"-1000000000000000001"}); + } else + { BEAST_EXPECT(toBigInt(b) == BigInt{"-1000000000000000000"}); + } Number sum; { From 74c66d0944f9da3dd94ac670e7b6c5524f15af65 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 5 Jun 2026 12:06:41 -0400 Subject: [PATCH 45/77] refactor: Construct Number::Guard from MantissaRange or relevant fields - Simplifies the function signatures in Guard, because it doesn't need to have those values passed in constantly. - Also simplifies some of the functions because they don't need to store values just to pass them to Guard functions. --- include/xrpl/basics/Number.h | 14 ++- src/libxrpl/basics/Number.cpp | 194 ++++++++++++++-------------------- 2 files changed, 86 insertions(+), 122 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 8043a850a8..dd74208485 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -147,7 +147,7 @@ struct MantissaRange final int const log{getExponent(scale)}; rep const min{getMin(scale, log)}; rep const max{(min * 10) - 1}; - CuspRoundingFix const cuspRoundingFixEnabled{isCuspFixEnabled(scale)}; + CuspRoundingFix const cuspRoundingFix{isCuspFixEnabled(scale)}; static MantissaRange const& getMantissaRange(MantissaScale scale); @@ -549,9 +549,15 @@ private: // changing the values inside the range. static thread_local std::reference_wrapper kRange; + class Guard; + void normalize(MantissaRange const& range); + // Guard has the fields that we need, as well as MantissaRange, so if we have a guard, use that + void + normalize(Guard const& guard); + /** Normalize Number components to an arbitrary range. * * min/maxMantissa are parameters because this function is used by both @@ -566,7 +572,7 @@ private: int& exponent, internalrep const& minMantissa, internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled); + MantissaRange::CuspRoundingFix cuspRoundingFix); template friend void @@ -576,7 +582,7 @@ private: int& exponent, MantissaRange::rep const& minMantissa, MantissaRange::rep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled, + MantissaRange::CuspRoundingFix cuspRoundingFix, bool dropped); [[nodiscard]] bool @@ -594,8 +600,6 @@ private: // UB, and can vary across compilers. static internalrep externalToInternal(rep mantissa); - - class Guard; }; constexpr Number::Number(bool negative, internalrep mantissa, int exponent, Unchecked) noexcept diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 8e3887e475..7717495cbe 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -65,7 +65,7 @@ MantissaRange::getRanges() static_assert(kRange.log == 15); static_assert(kRange.min < Number::kMaxRep); static_assert(kRange.max < Number::kMaxRep); - static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Disabled); + static_assert(kRange.cuspRoundingFix == CuspRoundingFix::Disabled); } { [[maybe_unused]] @@ -76,7 +76,7 @@ MantissaRange::getRanges() static_assert(kRange.log == 18); static_assert(kRange.min < Number::kMaxRep); static_assert(kRange.max > Number::kMaxRep); - static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Disabled); + static_assert(kRange.cuspRoundingFix == CuspRoundingFix::Disabled); } { [[maybe_unused]] @@ -87,7 +87,7 @@ MantissaRange::getRanges() static_assert(kRange.log == 18); static_assert(kRange.min < Number::kMaxRep); static_assert(kRange.max > Number::kMaxRep); - static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Enabled); + static_assert(kRange.cuspRoundingFix == CuspRoundingFix::Enabled); } return map; }(); @@ -171,7 +171,21 @@ class Number::Guard std::uint8_t sbit_ : 1 {0}; // the sign of the guard digits public: - explicit Guard() = default; + internalrep const minMantissa_; + internalrep const maxMantissa_; + MantissaRange::CuspRoundingFix const cuspRoundingFix_; + + explicit Guard( + internalrep const& minMantissa, + internalrep const& maxMantissa, + MantissaRange::CuspRoundingFix cuspRoundingFix) + : minMantissa_(minMantissa), maxMantissa_(maxMantissa), cuspRoundingFix_(cuspRoundingFix) + { + } + + explicit Guard(MantissaRange const& range) : Guard(range.min, range.max, range.cuspRoundingFix) + { + } // set & test the sign bit void @@ -221,19 +235,12 @@ public: // Modify the result to the correctly rounded value template void - doRoundUp( - bool& negative, - T& mantissa, - int& exponent, - internalrep const& minMantissa, - internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled, - std::string location); + doRoundUp(bool& negative, T& mantissa, int& exponent, std::string location); // Modify the result to the correctly rounded value template void - doRoundDown(bool& negative, T& mantissa, int& exponent, internalrep const& minMantissa); + doRoundDown(bool& negative, T& mantissa, int& exponent); // Modify the result to the correctly rounded value void @@ -245,7 +252,7 @@ private: template void - bringIntoRange(bool& negative, T& mantissa, int& exponent, internalrep const& minMantissa); + bringIntoRange(bool& negative, T& mantissa, int& exponent); }; inline void @@ -359,15 +366,11 @@ Number::Guard::round() const noexcept template void -Number::Guard::bringIntoRange( - bool& negative, - T& mantissa, - int& exponent, - internalrep const& minMantissa) +Number::Guard::bringIntoRange(bool& negative, T& mantissa, int& exponent) { // Bring mantissa back into the minMantissa / maxMantissa range AFTER // rounding - if (mantissa < minMantissa) + if (mantissa < minMantissa_) { mantissa *= 10; --exponent; @@ -384,22 +387,15 @@ Number::Guard::bringIntoRange( template void -Number::Guard::doRoundUp( - bool& negative, - T& mantissa, - int& exponent, - internalrep const& minMantissa, - internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled, - std::string location) +Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string location) { auto r = round(); if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1)) { - auto const safeToIncrement = [&maxMantissa](auto const& mantissa) { - return mantissa < maxMantissa && mantissa < kMaxRep; + auto const safeToIncrement = [this](auto const& mantissa) { + return mantissa < maxMantissa_ && mantissa < kMaxRep; }; - if (cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled) + if (cuspRoundingFix_ == MantissaRange::CuspRoundingFix::Enabled) { // Ensure mantissa after incrementing fits within both the // min/maxMantissa range and is a valid "rep". @@ -420,14 +416,7 @@ Number::Guard::doRoundUp( safeToIncrement(mantissa), "xrpl::Number::Guard::doRoundUp", "can't recurse more than once"); - doRoundUp( - negative, - mantissa, - exponent, - minMantissa, - maxMantissa, - cuspRoundingFixEnabled, - location); + doRoundUp(negative, mantissa, exponent, location); return; } } @@ -438,7 +427,7 @@ Number::Guard::doRoundUp( ++mantissa; // Ensure mantissa after incrementing fits within both the // min/maxMantissa range and is a valid "rep". - if (mantissa > maxMantissa || mantissa > kMaxRep) + if (mantissa > maxMantissa_ || mantissa > kMaxRep) { // Don't use doDropDigit here mantissa /= 10; @@ -446,30 +435,26 @@ Number::Guard::doRoundUp( } } } - bringIntoRange(negative, mantissa, exponent, minMantissa); + bringIntoRange(negative, mantissa, exponent); if (exponent > kMaxExponent) Throw(std::string(location)); } template void -Number::Guard::doRoundDown( - bool& negative, - T& mantissa, - int& exponent, - internalrep const& minMantissa) +Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) { auto r = round(); if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1)) { --mantissa; - if (mantissa < minMantissa) + if (mantissa < minMantissa_) { mantissa *= 10; --exponent; } } - bringIntoRange(negative, mantissa, exponent, minMantissa); + bringIntoRange(negative, mantissa, exponent); } // Modify the result to the correctly rounded value @@ -536,7 +521,7 @@ doNormalize( int& exponent, MantissaRange::rep const& minMantissa, MantissaRange::rep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled, + MantissaRange::CuspRoundingFix cuspRoundingFix, bool dropped) { static constexpr auto kMinExponent = Number::kMinExponent; @@ -559,7 +544,7 @@ doNormalize( m *= 10; --exponent; } - Guard g; + Guard g(minMantissa, maxMantissa, cuspRoundingFix); if (negative) g.setNegative(); if (dropped) @@ -604,14 +589,7 @@ doNormalize( XRPL_ASSERT_PARTS(m <= kMaxRep, "xrpl::doNormalize", "intermediate mantissa fits in int64"); mantissa = m; - g.doRoundUp( - negative, - mantissa, - exponent, - minMantissa, - maxMantissa, - cuspRoundingFixEnabled, - "Number::normalize 2"); + g.doRoundUp(negative, mantissa, exponent, "Number::normalize 2"); XRPL_ASSERT_PARTS( mantissa >= minMantissa && mantissa <= maxMantissa, "xrpl::doNormalize", @@ -626,13 +604,12 @@ Number::normalize( int& exponent, internalrep const& minMantissa, internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled) + MantissaRange::CuspRoundingFix cuspRoundingFix) { // Not used by every compiler version, and thus not necessarily // counted by coverage build // LCOV_EXCL_START - doNormalize( - negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false); + doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFix, false); // LCOV_EXCL_STOP } @@ -644,13 +621,12 @@ Number::normalize( int& exponent, internalrep const& minMantissa, internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled) + MantissaRange::CuspRoundingFix cuspRoundingFix) { // Not used by every compiler version, and thus not necessarily // counted by coverage build // LCOV_EXCL_START - doNormalize( - negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false); + doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFix, false); // LCOV_EXCL_STOP } @@ -662,16 +638,27 @@ Number::normalize( int& exponent, internalrep const& minMantissa, internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled) + MantissaRange::CuspRoundingFix cuspRoundingFix) { - doNormalize( - negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false); + doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFix, false); } void Number::normalize(MantissaRange const& range) { - normalize(negative_, mantissa_, exponent_, range.min, range.max, range.cuspRoundingFixEnabled); + normalize(negative_, mantissa_, exponent_, range.min, range.max, range.cuspRoundingFix); +} + +void +Number::normalize(Guard const& guard) +{ + normalize( + negative_, + mantissa_, + exponent_, + guard.minMantissa_, + guard.maxMantissa_, + guard.cuspRoundingFix_); } // Copy the number, but set a new exponent. Because the mantissa doesn't change, @@ -725,19 +712,20 @@ Number::operator+=(Number const& y) bool const yn = y.negative_; uint128_t ym = y.mantissa_; auto ye = y.exponent_; - Guard g; + Guard g(kRange); - auto const& range = kRange.get(); + auto const& minMantissa = g.minMantissa_; + auto const& maxMantissa = g.maxMantissa_; + auto const cuspRoundingFix = g.cuspRoundingFix_; // Bring the exponents of both values into agreement, so the mantissas are on the same scale // and can be added directly together // expandM / expandE: First try to expand the mantissa and bring the exponent down // shringM / shrinkE: Then shrink the mantissa and bring the exponent up, if necessary - auto const adjust = [&g, &range]( - uint128_t& expandM, int& expandE, uint128_t& shrinkM, int& shrinkE) { + auto const adjust = [&g](uint128_t& expandM, int& expandE, uint128_t& shrinkM, int& shrinkE) { constexpr uint128_t kSafeLimit = kPowerOfTenImpl[37]; - if (range.cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled) + if (g.cuspRoundingFix_ == MantissaRange::CuspRoundingFix::Enabled) { while (shrinkE < expandE && shrinkM % 10 == 0) { @@ -774,14 +762,10 @@ Number::operator+=(Number const& y) adjust(xm, xe, ym, ye); } - auto const& minMantissa = range.min; - auto const& maxMantissa = range.max; - auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled; - if (xn == yn) { xm += ym; - if (range.cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled) + if (cuspRoundingFix == MantissaRange::CuspRoundingFix::Enabled) { while (xm > maxMantissa || xm > kMaxRep) { @@ -795,14 +779,7 @@ Number::operator+=(Number const& y) g.doDropDigit(xm, xe); } } - g.doRoundUp( - xn, - xm, - xe, - minMantissa, - maxMantissa, - cuspRoundingFixEnabled, - "Number::addition overflow"); + g.doRoundUp(xn, xm, xe, "Number::addition overflow"); } else { @@ -822,32 +799,25 @@ Number::operator+=(Number const& y) xm -= g.pop(); --xe; } - g.doRoundDown(xn, xm, xe, minMantissa); - if (range.cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled && xm != 0) + g.doRoundDown(xn, xm, xe); + if (cuspRoundingFix == MantissaRange::CuspRoundingFix::Enabled && xm != 0) { - // make a new guard - Guard g; + // this will be going away + Guard g(kRange); if (xn) g.setNegative(); while (xm > maxMantissa || xm > kMaxRep) { g.doDropDigit(xm, xe); } - g.doRoundUp( - xn, - xm, - xe, - minMantissa, - maxMantissa, - cuspRoundingFixEnabled, - "Number::addition overflow"); + g.doRoundUp(xn, xm, xe, "Number::addition overflow"); } } negative_ = xn; mantissa_ = static_cast(xm); exponent_ = xe; - normalize(range); + normalize(g); return *this; } @@ -881,14 +851,11 @@ Number::operator*=(Number const& y) auto ze = xe + ye; auto zs = xs * ys; bool zn = (zs == -1); - Guard g; + Guard g(kRange); if (zn) g.setNegative(); - auto const& range = kRange.get(); - auto const& minMantissa = range.min; - auto const& maxMantissa = range.max; - auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled; + auto const& maxMantissa = g.maxMantissa_; while (zm > maxMantissa || zm > kMaxRep) { @@ -897,19 +864,12 @@ Number::operator*=(Number const& y) xm = static_cast(zm); xe = ze; - g.doRoundUp( - zn, - xm, - xe, - minMantissa, - maxMantissa, - cuspRoundingFixEnabled, - "Number::multiplication overflow : exponent is " + std::to_string(xe)); + g.doRoundUp(zn, xm, xe, "Number::multiplication overflow : exponent is " + std::to_string(xe)); negative_ = zn; mantissa_ = xm; exponent_ = xe; - normalize(range); + normalize(g); return *this; } @@ -945,7 +905,7 @@ Number::operator/=(Number const& y) auto const& range = kRange.get(); auto const& minMantissa = range.min; auto const& maxMantissa = range.max; - auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled; + auto const cuspRoundingFix = range.cuspRoundingFix; // Division operates on two large integers (16-digit for small // mantissas, 19-digit for large) using integer math. If the values @@ -1077,14 +1037,14 @@ Number::operator/=(Number const& y) // rounding fix is enabled, flag if there is still // a remainder from stage 2. bool const useTrailingRemainder = - cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled; + cuspRoundingFix == MantissaRange::CuspRoundingFix::Enabled; if (useTrailingRemainder) { dropped = partialNumerator % dm != 0; } } } - doNormalize(zp, zm, ze, minMantissa, maxMantissa, cuspRoundingFixEnabled, dropped); + doNormalize(zp, zm, ze, minMantissa, maxMantissa, cuspRoundingFix, dropped); negative_ = zp; mantissa_ = static_cast(zm); exponent_ = ze; @@ -1098,7 +1058,7 @@ operator rep() const { rep drops = mantissa(); int offset = exponent(); - Guard g; + Guard g(kRange); if (drops != 0) { if (negative_) From 012c67a7eb8ed2ec3f12e770ebaec32fa8282404 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 5 Jun 2026 18:02:45 -0400 Subject: [PATCH 46/77] Rework subtraction rounding (again) for more accuracy - Go back to the old method of computing the mantissa, but when post processing, expand the mantissa to slightly larger than maxMantissa, then in doRoundDown, if the result is not exact, subtract one. Finally, let doNormalize figure out the rounding of the result. --- include/xrpl/basics/Number.h | 20 ++++ src/libxrpl/basics/Number.cpp | 133 +++++++++++++--------- src/test/basics/Number_test.cpp | 190 +++++++++++++++++++++++++------- 3 files changed, 254 insertions(+), 89 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index dd74208485..61b63fe234 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -877,6 +877,26 @@ to_string(MantissaRange::MantissaScale const& scale) } } +inline std::string +to_string(Number::RoundingMode const& round) +{ + switch (round) + { + enum class RoundingMode { ToNearest, TowardsZero, Downward, Upward }; + + case Number::RoundingMode::ToNearest: + return "ToNearest"; + case Number::RoundingMode::TowardsZero: + return "TowardsZero"; + case Number::RoundingMode::Downward: + return "Downward"; + case Number::RoundingMode::Upward: + return "Upward"; + default: + throw std::runtime_error("Bad rounding mode"); + } +} + class SaveNumberRoundMode { Number::RoundingMode mode_; diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 7717495cbe..71feed822d 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -208,6 +208,10 @@ public: unsigned pop() noexcept; + // if true, there are no more digits to recover with pop() + bool + empty() const noexcept; + /** Drop a digit from the mantissa, and increment the exponent, storing the dropped digit in * this Guard. * @@ -221,8 +225,17 @@ public: doDropDigit(T& mantissa, int& exponent) noexcept; enum class Round { + // The result is exact. No rounding is needed. + Exact = -2, + // Round down. Since we use integer math, that usually means no change is needed. + // Exceptions are for when the result is between kMaxRap 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 whichever of + // the two is even. Even = 0, + // Round up. Always adds 1 (or subtracts 1 in some cases if cuspRoundingFix is not enabled) Up = 1, }; @@ -302,6 +315,13 @@ Number::Guard::pop() noexcept return d; } +// if true, there are no more digits to recover with pop() +inline bool +Number::Guard::empty() const noexcept +{ + return digits_ == 0 && !xbit_; +} + template void Number::Guard::doDropDigit(T& mantissa, int& exponent) noexcept @@ -332,6 +352,12 @@ Number::Guard::round() const noexcept { auto mode = Number::getround(); + if (cuspRoundingFix_ != MantissaRange::CuspRoundingFix::Disabled && empty()) + { + // No remainder + return Round::Exact; + } + if (mode == RoundingMode::TowardsZero) return Round::Down; @@ -445,13 +471,31 @@ void Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) { auto r = round(); - if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1)) + if (cuspRoundingFix_ != MantissaRange::CuspRoundingFix::Disabled) { - --mantissa; - if (mantissa < minMantissa_) + // If there was any remainder, subtract 1 from the result, and pad with 9s. + // Example with 4 digit mantissas: + // 1000 - 0.0000000001 = 999.9999999999 + // In operator+=, the result will be: 1000, with Guard holding (0, xbit=true) + // * Rounding away from zero, the result should be 1000 + // * Rounding towards zero, the result should be 999.9 + // * Rounding to nearest, the result should be 999.9 + // The most accurate result is always 999.9 + if (r != Round::Exact) { - mantissa *= 10; - --exponent; + --mantissa; + } + } + else + { + if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1)) + { + --mantissa; + if (mantissa < minMantissa_) + { + mantissa *= 10; + --exponent; + } } } bringIntoRange(negative, mantissa, exponent); @@ -720,46 +764,24 @@ Number::operator+=(Number const& y) // Bring the exponents of both values into agreement, so the mantissas are on the same scale // and can be added directly together - // expandM / expandE: First try to expand the mantissa and bring the exponent down - // shringM / shrinkE: Then shrink the mantissa and bring the exponent up, if necessary - auto const adjust = [&g](uint128_t& expandM, int& expandE, uint128_t& shrinkM, int& shrinkE) { - constexpr uint128_t kSafeLimit = kPowerOfTenImpl[37]; - - if (g.cuspRoundingFix_ == MantissaRange::CuspRoundingFix::Enabled) - { - while (shrinkE < expandE && shrinkM % 10 == 0) - { - g.doDropDigit(shrinkM, shrinkE); - } - - // We've got 128 bits of mantissa to work with here. Don't throw away data unless we - // have to - while (shrinkE < expandE && expandE > kMinExponent && expandM < kSafeLimit) - { - expandM *= 10; - --expandE; - } - } - - while (shrinkE < expandE) - { - g.doDropDigit(shrinkM, shrinkE); - } - }; - + // Then shrink the mantissa and bring the exponent up of the value with the lower exponent if (xe < ye) { if (xn) g.setNegative(); - - adjust(ym, ye, xm, xe); + do + { + g.doDropDigit(xm, xe); + } while (xe < ye); } else if (xe > ye) { if (yn) g.setNegative(); - - adjust(xm, xe, ym, ye); + do + { + g.doDropDigit(ym, ye); + } while (xe > ye); } if (xn == yn) @@ -793,31 +815,38 @@ Number::operator+=(Number const& y) xe = ye; xn = yn; } - while (xm < minMantissa && xm * 10 <= kMaxRep) + if (cuspRoundingFix == MantissaRange::CuspRoundingFix::Enabled) { - xm *= 10; - xm -= g.pop(); - --xe; - } - g.doRoundDown(xn, xm, xe); - if (cuspRoundingFix == MantissaRange::CuspRoundingFix::Enabled && xm != 0) - { - // this will be going away - Guard g(kRange); - if (xn) - g.setNegative(); - while (xm > maxMantissa || xm > kMaxRep) + // Grow xm/xe and pull digits out of the Guard until it's just past the range, so that + // normalize will have enough information to make an accurate rounding decision, but + // stop if the Guard empties out. Note that if the xbit is set, the Guard will never be + // empty. + while (xm <= maxMantissa && !g.empty()) { - g.doDropDigit(xm, xe); + xm *= 10; + xm -= g.pop(); + --xe; } - g.doRoundUp(xn, xm, xe, "Number::addition overflow"); } + else + { + // Grow xm/xe and pull digits out of the Guard until it's back in range. + while (xm < minMantissa && xm * 10 <= kMaxRep) + { + xm *= 10; + xm -= g.pop(); + --xe; + } + } + // Round down, based on whether there is any data left in the Guard (depending on + // cuspRoundingFix) + g.doRoundDown(xn, xm, xe); } + doNormalize(xn, xm, xe, minMantissa, maxMantissa, cuspRoundingFix, false); negative_ = xn; mantissa_ = static_cast(xm); exponent_ = xe; - normalize(g); return *this; } diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 24cf5d6967..9e928f4e9f 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -43,12 +43,17 @@ class Number_test : public beast::unit_test::Suite return out; } - static BigInt + BigInt toBigInt(Number const& n) { BigInt v = n.mantissa(); for (int i = 0; i < n.exponent(); ++i) v *= 10; + for (int i = 0; i > n.exponent(); --i) + { + BEAST_EXPECT(v % 10 == 0); + v /= 10; + } return v; } @@ -1923,49 +1928,160 @@ public: } { - testcase << "operator+ TowardsZero rounds away from zero " << to_string(scale); + testcase << "subtraction rounding " << to_string(scale); - Number const a{1LL, 20}; - Number const b{-1'000'000'000'000'000'001LL}; - - BEAST_EXPECT(toBigInt(a) == BigInt{"100000000000000000000"}); - if (scale != MantissaRange::MantissaScale::Small) - { - BEAST_EXPECT(toBigInt(b) == BigInt{"-1000000000000000001"}); - } - else - { - BEAST_EXPECT(toBigInt(b) == BigInt{"-1000000000000000000"}); - } - - Number sum; - { - NumberRoundModeGuard const roundGuard{Number::RoundingMode::TowardsZero}; - sum = a + b; - } - - BigInt const exact = toBigInt(a) + toBigInt(b); - BigInt const stored = toBigInt(sum); - BigInt const diff = stored - exact; - - log << "\n a = " << a << "\n b = " << b - << "\n exact a + b = " << exact.str() << "\n TowardsZero = " << stored.str() - << "\n difference = " << diff.str() << "\n"; - log.flush(); + auto const exp = Number::mantissaLog(); + Number const a{1LL, exp + 2}; + Number const b{-(Number{1, exp} + 1)}; if (scale == MantissaRange::MantissaScale::Small) { - BEAST_EXPECT(stored == exact); - } - else if (scale == MantissaRange::MantissaScale::LargeLegacy) - { - BEAST_EXPECT(stored > exact); + BEAST_EXPECT(toBigInt(a) == BigInt{"100000000000000000"}); + BEAST_EXPECT(toBigInt(b) == BigInt{"-1000000000000001"}); } else { - BEAST_EXPECT(stored < exact); - BEAST_EXPECT(diff < 0); - BEAST_EXPECT(-diff < pow10(sum.exponent())); + BEAST_EXPECT(toBigInt(a) == BigInt{"100000000000000000000"}); + BEAST_EXPECT(toBigInt(b) == BigInt{"-1000000000000000001"}); + } + + auto construct = [&a, &b, this](Number::RoundingMode r) { + NumberRoundModeGuard const roundGuard{r}; + auto const sum = a + b; + BigInt const stored = toBigInt(sum); + return std::make_pair(r, std::make_pair(stored, sum)); + }; + + auto const bigA = toBigInt(a); + auto const bigB = toBigInt(b); + BigInt const exact = bigA + bigB; + + auto const sums = [&]() { + std::map> sums; + sums.emplace(construct(Number::RoundingMode::TowardsZero)); + sums.emplace(construct(Number::RoundingMode::Upward)); + sums.emplace(construct(Number::RoundingMode::Downward)); + sums.emplace(construct(Number::RoundingMode::ToNearest)); + return sums; + }(); + + log << "\n a = " << a << " (" << fmt(bigA) << ")\n b = " << b + << " (" << fmt(bigB) << ")\n exact a + b = " << fmt(exact) << "\n"; + for (auto const& [r, sum] : sums) + { + auto const diff = sum.first - exact; + auto const rLabel = to_string(r); + log << std::string(15 - rLabel.length(), ' ') << rLabel << " = " << fmt(sum.first) + << "\n difference = " << fmt(diff) << "\n"; + } + log.flush(); + + switch (scale) + { + case MantissaRange::MantissaScale::Small: + case MantissaRange::MantissaScale::LargeLegacy: { + // Without the fix, all the results but one round up + BEAST_EXPECT(sums.at(Number::RoundingMode::TowardsZero).first > exact); + BEAST_EXPECT(sums.at(Number::RoundingMode::Upward).first > exact); + BEAST_EXPECT(sums.at(Number::RoundingMode::ToNearest).first > exact); + // Downward works because the Guard sign is negative, and Downward returns Up + // instead of Down if negative and there's a remainder, whereas TowardsZero + // always returns Down. + BEAST_EXPECT(sums.at(Number::RoundingMode::Downward).first < exact); + break; + } + default: { + for (auto const& [r, sum] : sums) + { + auto const epsilon = pow10(sum.second.exponent()); + BEAST_EXPECT(epsilon == 100); + auto diff = sum.first - exact; + switch (r) + { + case Number::RoundingMode::Upward: + case Number::RoundingMode::ToNearest: + BEAST_EXPECT(sum.first > exact); + BEAST_EXPECT(diff < epsilon); + break; + default: + BEAST_EXPECT(sum.first < exact); + BEAST_EXPECT(-diff < epsilon); + } + } + } + } + } + { + auto const offset = 30; + testcase << "subtraction rounding offset of " << offset << " " << to_string(scale); + + auto const exp = Number::mantissaLog(); + Number const a{1LL, exp + offset}; + Number const b{-1}; + + auto construct = [&a, &b, this](Number::RoundingMode r) { + NumberRoundModeGuard const roundGuard{r}; + auto const sum = a + b; + BigInt const stored = toBigInt(sum); + return std::make_pair(r, std::make_pair(stored, sum)); + }; + + auto const bigA = toBigInt(a); + auto const bigB = toBigInt(b); + BigInt const exact = bigA + bigB; + + auto const sums = [&]() { + std::map> sums; + sums.emplace(construct(Number::RoundingMode::TowardsZero)); + sums.emplace(construct(Number::RoundingMode::Upward)); + sums.emplace(construct(Number::RoundingMode::Downward)); + sums.emplace(construct(Number::RoundingMode::ToNearest)); + return sums; + }(); + + log << "\n a = " << a << " (" << fmt(bigA) << ")\n b = " << b + << " (" << fmt(bigB) << ")\n exact a + b = " << fmt(exact) << "\n"; + for (auto const& [r, sum] : sums) + { + auto const diff = sum.first - exact; + auto const rLabel = to_string(r); + log << std::string(15 - rLabel.length(), ' ') << rLabel << " = " << fmt(sum.first) + << "\n difference = " << fmt(diff) << "\n"; + } + log.flush(); + + switch (scale) + { + case MantissaRange::MantissaScale::Small: + case MantissaRange::MantissaScale::LargeLegacy: { + // Without the fix, all the results but one round up + BEAST_EXPECT(sums.at(Number::RoundingMode::TowardsZero).first > exact); + BEAST_EXPECT(sums.at(Number::RoundingMode::Upward).first > exact); + BEAST_EXPECT(sums.at(Number::RoundingMode::ToNearest).first > exact); + // Downward works because the Guard sign is negative, and Downward returns Up + // instead of Down if negative and there's a remainder, whereas TowardsZero + // always returns Down. + BEAST_EXPECT(sums.at(Number::RoundingMode::Downward).first < exact); + break; + } + default: { + for (auto const& [r, sum] : sums) + { + auto const epsilon = pow10(sum.second.exponent()); + auto diff = sum.first - exact; + switch (r) + { + case Number::RoundingMode::Upward: + case Number::RoundingMode::ToNearest: + BEAST_EXPECT(sum.first > exact); + BEAST_EXPECT(diff < epsilon); + break; + default: + BEAST_EXPECT(sum.first < exact); + BEAST_EXPECT(-diff < epsilon); + } + } + } } } } From 961ac6671ecad8136b81ed51ddafc988120e28d3 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 6 Jun 2026 13:09:52 -0400 Subject: [PATCH 47/77] Improve comment descriptions --- src/libxrpl/basics/Number.cpp | 49 ++++++++++++----------------------- 1 file changed, 17 insertions(+), 32 deletions(-) diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 71feed822d..69e48650ee 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -208,7 +208,7 @@ public: unsigned pop() noexcept; - // if true, there are no more digits to recover with pop() + // if true, there are no digits in the guard, including dropped digits (xbit_) bool empty() const noexcept; @@ -225,15 +225,14 @@ public: doDropDigit(T& mantissa, int& exponent) noexcept; enum class Round { - // The result is exact. No rounding is needed. + // The result is exact. No rounding is needed. Only used if cuspRoundingFix is enabled. Exact = -2, // Round down. Since we use integer math, that usually means no change is needed. // Exceptions are for when the result is between kMaxRap 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 whichever of - // the two is even. + // 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, @@ -315,7 +314,6 @@ Number::Guard::pop() noexcept return d; } -// if true, there are no more digits to recover with pop() inline bool Number::Guard::empty() const noexcept { @@ -473,14 +471,8 @@ Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) auto r = round(); if (cuspRoundingFix_ != MantissaRange::CuspRoundingFix::Disabled) { - // If there was any remainder, subtract 1 from the result, and pad with 9s. - // Example with 4 digit mantissas: - // 1000 - 0.0000000001 = 999.9999999999 - // In operator+=, the result will be: 1000, with Guard holding (0, xbit=true) - // * Rounding away from zero, the result should be 1000 - // * Rounding towards zero, the result should be 999.9 - // * Rounding to nearest, the result should be 999.9 - // The most accurate result is always 999.9 + // If there was any remainder, subtract 1 from the result. This is sufficient to get the + // best rounding. if (r != Round::Exact) { --mantissa; @@ -763,8 +755,9 @@ Number::operator+=(Number const& y) auto const cuspRoundingFix = g.cuspRoundingFix_; // Bring the exponents of both values into agreement, so the mantissas are on the same scale - // and can be added directly together - // Then shrink the mantissa and bring the exponent up of the value with the lower exponent + // and can be added directly together. + // Shrink the mantissa and bring the exponent up of the value with the lower exponent. Store any + // dropped digits in the Guard. if (xe < ye) { if (xn) @@ -787,19 +780,9 @@ Number::operator+=(Number const& y) if (xn == yn) { xm += ym; - if (cuspRoundingFix == MantissaRange::CuspRoundingFix::Enabled) + if (xm > maxMantissa || xm > kMaxRep) { - while (xm > maxMantissa || xm > kMaxRep) - { - g.doDropDigit(xm, xe); - } - } - else - { - if (xm > maxMantissa || xm > kMaxRep) - { - g.doDropDigit(xm, xe); - } + g.doDropDigit(xm, xe); } g.doRoundUp(xn, xm, xe, "Number::addition overflow"); } @@ -817,11 +800,13 @@ Number::operator+=(Number const& y) } if (cuspRoundingFix == MantissaRange::CuspRoundingFix::Enabled) { - // Grow xm/xe and pull digits out of the Guard until it's just past the range, so that - // normalize will have enough information to make an accurate rounding decision, but - // stop if the Guard empties out. Note that if the xbit is set, the Guard will never be - // empty. - while (xm <= maxMantissa && !g.empty()) + // Grow xm/xe and pull digits out of the Guard until it's a little bit larger than + // maxMantissa, so that normalize will have enough information to make an accurate + // rounding decision, but stop if the Guard empties out, because no rounding will be + // necessary. (Normalize will pad it back into range.) Note that if any digits were lost + // (xbit), the Guard will never be empty, so xm will get big. + auto const upperLimit = static_cast(minMantissa) * 1000; + while (xm < upperLimit && !g.empty()) { xm *= 10; xm -= g.pop(); From 3a5c85067a0251dc4c60c4bf8eb5ddccbdaf851a Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 6 Jun 2026 14:33:31 -0400 Subject: [PATCH 48/77] Include rounding in failed unit tests --- src/test/basics/Number_test.cpp | 53 +++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 9e928f4e9f..aa57af01b3 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -1981,13 +1981,17 @@ public: case MantissaRange::MantissaScale::Small: case MantissaRange::MantissaScale::LargeLegacy: { // Without the fix, all the results but one round up - BEAST_EXPECT(sums.at(Number::RoundingMode::TowardsZero).first > exact); - BEAST_EXPECT(sums.at(Number::RoundingMode::Upward).first > exact); - BEAST_EXPECT(sums.at(Number::RoundingMode::ToNearest).first > exact); - // Downward works because the Guard sign is negative, and Downward returns Up - // instead of Down if negative and there's a remainder, whereas TowardsZero - // always returns Down. - BEAST_EXPECT(sums.at(Number::RoundingMode::Downward).first < exact); + for (auto const& [r, sum] : sums) { + if (r == Number::RoundingMode::Downward) { + // Downward works because the Guard sign is negative, and Downward returns Up + // instead of Down if negative and there's a remainder, whereas TowardsZero + // always returns Down. + BEAST_EXPECTS(sums.at(Number::RoundingMode::Downward).first < exact, to_string(r)); + } + else { + BEAST_EXPECTS(sums.at(r).first > exact, to_string(r)); + } + } break; } default: { @@ -2000,12 +2004,12 @@ public: { case Number::RoundingMode::Upward: case Number::RoundingMode::ToNearest: - BEAST_EXPECT(sum.first > exact); - BEAST_EXPECT(diff < epsilon); + BEAST_EXPECTS(sum.first > exact, to_string(r)); + BEAST_EXPECTS(diff < epsilon, to_string(r)); break; default: - BEAST_EXPECT(sum.first < exact); - BEAST_EXPECT(-diff < epsilon); + BEAST_EXPECTS(sum.first < exact, to_string(r)); + BEAST_EXPECTS(-diff < epsilon, to_string(r)); } } } @@ -2054,14 +2058,17 @@ public: { case MantissaRange::MantissaScale::Small: case MantissaRange::MantissaScale::LargeLegacy: { - // Without the fix, all the results but one round up - BEAST_EXPECT(sums.at(Number::RoundingMode::TowardsZero).first > exact); - BEAST_EXPECT(sums.at(Number::RoundingMode::Upward).first > exact); - BEAST_EXPECT(sums.at(Number::RoundingMode::ToNearest).first > exact); - // Downward works because the Guard sign is negative, and Downward returns Up - // instead of Down if negative and there's a remainder, whereas TowardsZero - // always returns Down. - BEAST_EXPECT(sums.at(Number::RoundingMode::Downward).first < exact); + for (auto const& [r, sum] : sums) { + if (r == Number::RoundingMode::Downward) { + // Downward works because the Guard sign is negative, and Downward returns Up + // instead of Down if negative and there's a remainder, whereas TowardsZero + // always returns Down. + BEAST_EXPECTS(sums.at(Number::RoundingMode::Downward).first < exact, to_string(r)); + } + else { + BEAST_EXPECTS(sums.at(r).first > exact, to_string(r)); + } + } break; } default: { @@ -2073,12 +2080,12 @@ public: { case Number::RoundingMode::Upward: case Number::RoundingMode::ToNearest: - BEAST_EXPECT(sum.first > exact); - BEAST_EXPECT(diff < epsilon); + BEAST_EXPECTS(sum.first > exact, to_string(r)); + BEAST_EXPECTS(diff < epsilon, to_string(r)); break; default: - BEAST_EXPECT(sum.first < exact); - BEAST_EXPECT(-diff < epsilon); + BEAST_EXPECTS(sum.first < exact, to_string(r)); + BEAST_EXPECTS(-diff < epsilon, to_string(r)); } } } From 8743be8eae2a28b05f3683d3f0164dd50737305e Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 6 Jun 2026 14:34:31 -0400 Subject: [PATCH 49/77] Rollback Number class changes; show the fix works without side effects --- include/xrpl/basics/Number.h | 38 ++--- src/libxrpl/basics/Number.cpp | 285 +++++++++++++++------------------- 2 files changed, 138 insertions(+), 185 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 61b63fe234..0b4caaa722 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -51,43 +51,37 @@ namespace detail { * compile time. Doing it at runtime would be pretty wasteful and * inefficient. */ -constexpr std::size_t kUint64Digits = 20; -constexpr std::size_t kUint128Digits = 39; - -template -consteval std::array +constexpr std::size_t kInt64Digits = 20; +consteval std::array buildPowersOfTen() { - std::array result{}; + std::array result{}; - T power = 1; + std::uint64_t power = 1; std::size_t exponent = 0; // end the loop early so it doesn't overflow; for (; exponent < result.size() - 1; ++exponent, power *= 10) { result[exponent] = power; - if (power > std::numeric_limits::max() / 10) + if (power > std::numeric_limits::max() / 10) throw std::logic_error("Power of 10 table is too big"); } result[exponent] = power; - if (power < std::numeric_limits::max() / 10) - throw std::logic_error("Power of 10 table is not big enough for the given type"); + if (power < std::numeric_limits::max() / 10) + throw std::logic_error("Power of 10 table is not big enough for the uint64_t type"); return result; } } // namespace detail -template -constexpr std::array kPowerOfTenImpl = detail::buildPowersOfTen(); - -constexpr auto kPowerOfTen = kPowerOfTenImpl; +constexpr std::array kPowerOfTen = detail::buildPowersOfTen(); static_assert(kPowerOfTen[0] == 1); static_assert(kPowerOfTen[1] == 10); static_assert(kPowerOfTen[10] == 10'000'000'000); static_assert( - isPowerOfTen(kPowerOfTen.back()) && *logTen(kPowerOfTen.back()) == detail::kUint64Digits - 1); + isPowerOfTen(kPowerOfTen.back()) && *logTen(kPowerOfTen.back()) == detail::kInt64Digits - 1); /** MantissaRange defines a range for the mantissa of a normalized Number. * @@ -147,7 +141,7 @@ struct MantissaRange final int const log{getExponent(scale)}; rep const min{getMin(scale, log)}; rep const max{(min * 10) - 1}; - CuspRoundingFix const cuspRoundingFix{isCuspFixEnabled(scale)}; + CuspRoundingFix const cuspRoundingFixEnabled{isCuspFixEnabled(scale)}; static MantissaRange const& getMantissaRange(MantissaScale scale); @@ -549,15 +543,9 @@ private: // changing the values inside the range. static thread_local std::reference_wrapper kRange; - class Guard; - void normalize(MantissaRange const& range); - // Guard has the fields that we need, as well as MantissaRange, so if we have a guard, use that - void - normalize(Guard const& guard); - /** Normalize Number components to an arbitrary range. * * min/maxMantissa are parameters because this function is used by both @@ -572,7 +560,7 @@ private: int& exponent, internalrep const& minMantissa, internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFix); + MantissaRange::CuspRoundingFix cuspRoundingFixEnabled); template friend void @@ -582,7 +570,7 @@ private: int& exponent, MantissaRange::rep const& minMantissa, MantissaRange::rep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFix, + MantissaRange::CuspRoundingFix cuspRoundingFixEnabled, bool dropped); [[nodiscard]] bool @@ -600,6 +588,8 @@ private: // UB, and can vary across compilers. static internalrep externalToInternal(rep mantissa); + + class Guard; }; constexpr Number::Number(bool negative, internalrep mantissa, int exponent, Unchecked) noexcept diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index d43eebc314..23e913bbdc 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -65,7 +65,7 @@ MantissaRange::getRanges() static_assert(kRange.log == 15); static_assert(kRange.min < Number::kMaxRep); static_assert(kRange.max < Number::kMaxRep); - static_assert(kRange.cuspRoundingFix == CuspRoundingFix::Disabled); + static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Disabled); } { [[maybe_unused]] @@ -76,7 +76,7 @@ MantissaRange::getRanges() static_assert(kRange.log == 18); static_assert(kRange.min < Number::kMaxRep); static_assert(kRange.max > Number::kMaxRep); - static_assert(kRange.cuspRoundingFix == CuspRoundingFix::Disabled); + static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Disabled); } { [[maybe_unused]] @@ -87,7 +87,7 @@ MantissaRange::getRanges() static_assert(kRange.log == 18); static_assert(kRange.min < Number::kMaxRep); static_assert(kRange.max > Number::kMaxRep); - static_assert(kRange.cuspRoundingFix == CuspRoundingFix::Enabled); + static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Enabled); } return map; }(); @@ -171,21 +171,7 @@ class Number::Guard std::uint8_t sbit_ : 1 {0}; // the sign of the guard digits public: - internalrep const minMantissa_; - internalrep const maxMantissa_; - MantissaRange::CuspRoundingFix const cuspRoundingFix_; - - explicit Guard( - internalrep const& minMantissa, - internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFix) - : minMantissa_(minMantissa), maxMantissa_(maxMantissa), cuspRoundingFix_(cuspRoundingFix) - { - } - - explicit Guard(MantissaRange const& range) : Guard(range.min, range.max, range.cuspRoundingFix) - { - } + explicit Guard() = default; // set & test the sign bit void @@ -208,10 +194,6 @@ public: unsigned pop() noexcept; - // if true, there are no digits in the guard, including dropped digits (xbit_) - bool - empty() const noexcept; - /** Drop a digit from the mantissa, and increment the exponent, storing the dropped digit in * this Guard. * @@ -224,35 +206,28 @@ public: void doDropDigit(T& mantissa, int& exponent) noexcept; - enum class Round { - // The result is exact. No rounding is needed. Only used if cuspRoundingFix is enabled. - Exact = -2, - // Round down. Since we use integer math, that usually means no change is needed. - // Exceptions are for when the result is between kMaxRap 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 + [[nodiscard]] int round() const noexcept; // Modify the result to the correctly rounded value template void - doRoundUp(bool& negative, T& mantissa, int& exponent, std::string location); + doRoundUp( + bool& negative, + T& mantissa, + int& exponent, + internalrep const& minMantissa, + internalrep const& maxMantissa, + MantissaRange::CuspRoundingFix cuspRoundingFixEnabled, + std::string location); // Modify the result to the correctly rounded value template void - doRoundDown(bool& negative, T& mantissa, int& exponent); + doRoundDown(bool& negative, T& mantissa, int& exponent, internalrep const& minMantissa); // Modify the result to the correctly rounded value void @@ -264,7 +239,7 @@ private: template void - bringIntoRange(bool& negative, T& mantissa, int& exponent); + bringIntoRange(bool& negative, T& mantissa, int& exponent, internalrep const& minMantissa); }; inline void @@ -314,12 +289,6 @@ Number::Guard::pop() noexcept return d; } -inline bool -Number::Guard::empty() const noexcept -{ - return digits_ == 0 && !xbit_; -} - template void Number::Guard::doDropDigit(T& mantissa, int& exponent) noexcept @@ -345,56 +314,54 @@ Number::Guard::doDropDigit(uint128_t& mantissa, int& exponent) noexce // -1 if Guard is less than half // 0 if Guard is exactly half // 1 if Guard is greater than half -Number::Guard::Round +int Number::Guard::round() const noexcept { auto mode = Number::getround(); - if (cuspRoundingFix_ != MantissaRange::CuspRoundingFix::Disabled && empty()) - { - // No remainder - return Round::Exact; - } - if (mode == RoundingMode::TowardsZero) - return Round::Down; + return -1; if (mode == RoundingMode::Downward) { if (sbit_) { if (digits_ > 0 || xbit_) - return Round::Up; + return 1; } - return Round::Down; + return -1; } if (mode == RoundingMode::Upward) { if (sbit_) - return Round::Down; + return -1; if (digits_ > 0 || xbit_) - return Round::Up; - return Round::Down; + return 1; + return -1; } // assume round to nearest if mode is not one of the predefined values if (digits_ > 0x5000'0000'0000'0000) - return Round::Up; + return 1; if (digits_ < 0x5000'0000'0000'0000) - return Round::Down; + return -1; if (xbit_) - return Round::Up; - return Round::Even; + return 1; + return 0; } template void -Number::Guard::bringIntoRange(bool& negative, T& mantissa, int& exponent) +Number::Guard::bringIntoRange( + bool& negative, + T& mantissa, + int& exponent, + internalrep const& minMantissa) { // Bring mantissa back into the minMantissa / maxMantissa range AFTER // rounding - if (mantissa < minMantissa_) + if (mantissa < minMantissa) { mantissa *= 10; --exponent; @@ -411,15 +378,22 @@ Number::Guard::bringIntoRange(bool& negative, T& mantissa, int& exponent) template void -Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string location) +Number::Guard::doRoundUp( + bool& negative, + T& mantissa, + int& exponent, + internalrep const& minMantissa, + internalrep const& maxMantissa, + MantissaRange::CuspRoundingFix cuspRoundingFixEnabled, + std::string location) { auto r = round(); - if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1)) + if (r == 1 || (r == 0 && (mantissa & 1) == 1)) { - auto const safeToIncrement = [this](auto const& mantissa) { - return mantissa < maxMantissa_ && mantissa < kMaxRep; + auto const safeToIncrement = [&maxMantissa](auto const& mantissa) { + return mantissa < maxMantissa && mantissa < kMaxRep; }; - if (cuspRoundingFix_ == MantissaRange::CuspRoundingFix::Enabled) + if (cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled) { // Ensure mantissa after incrementing fits within both the // min/maxMantissa range and is a valid "rep". @@ -440,7 +414,14 @@ Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string safeToIncrement(mantissa), "xrpl::Number::Guard::doRoundUp", "can't recurse more than once"); - doRoundUp(negative, mantissa, exponent, location); + doRoundUp( + negative, + mantissa, + exponent, + minMantissa, + maxMantissa, + cuspRoundingFixEnabled, + location); return; } } @@ -451,7 +432,7 @@ Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string ++mantissa; // Ensure mantissa after incrementing fits within both the // min/maxMantissa range and is a valid "rep". - if (mantissa > maxMantissa_ || mantissa > kMaxRep) + if (mantissa > maxMantissa || mantissa > kMaxRep) { // Don't use doDropDigit here mantissa /= 10; @@ -459,38 +440,30 @@ Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string } } } - bringIntoRange(negative, mantissa, exponent); + bringIntoRange(negative, mantissa, exponent, minMantissa); if (exponent > kMaxExponent) Throw(std::string(location)); } template void -Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) +Number::Guard::doRoundDown( + bool& negative, + T& mantissa, + int& exponent, + internalrep const& minMantissa) { auto r = round(); - if (cuspRoundingFix_ != MantissaRange::CuspRoundingFix::Disabled) + if (r == 1 || (r == 0 && (mantissa & 1) == 1)) { - // If there was any remainder, subtract 1 from the result. This is sufficient to get the - // best rounding. - if (r != Round::Exact) + --mantissa; + if (mantissa < minMantissa) { - --mantissa; + mantissa *= 10; + --exponent; } } - else - { - if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1)) - { - --mantissa; - if (mantissa < minMantissa_) - { - mantissa *= 10; - --exponent; - } - } - } - bringIntoRange(negative, mantissa, exponent); + bringIntoRange(negative, mantissa, exponent, minMantissa); } // Modify the result to the correctly rounded value @@ -498,7 +471,7 @@ void Number::Guard::doRound(rep& drops, std::string location) const { auto r = round(); - if (r == Round::Up || (r == Round::Even && (drops & 1) == 1)) + if (r == 1 || (r == 0 && (drops & 1) == 1)) { if (drops >= kMaxRep) { @@ -557,7 +530,7 @@ doNormalize( int& exponent, MantissaRange::rep const& minMantissa, MantissaRange::rep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFix, + MantissaRange::CuspRoundingFix cuspRoundingFixEnabled, bool dropped) { static constexpr auto kMinExponent = Number::kMinExponent; @@ -580,7 +553,7 @@ doNormalize( m *= 10; --exponent; } - Guard g(minMantissa, maxMantissa, cuspRoundingFix); + Guard g; if (negative) g.setNegative(); if (dropped) @@ -625,7 +598,14 @@ doNormalize( XRPL_ASSERT_PARTS(m <= kMaxRep, "xrpl::doNormalize", "intermediate mantissa fits in int64"); mantissa = m; - g.doRoundUp(negative, mantissa, exponent, "Number::normalize 2"); + g.doRoundUp( + negative, + mantissa, + exponent, + minMantissa, + maxMantissa, + cuspRoundingFixEnabled, + "Number::normalize 2"); XRPL_ASSERT_PARTS( mantissa >= minMantissa && mantissa <= maxMantissa, "xrpl::doNormalize", @@ -640,12 +620,13 @@ Number::normalize( int& exponent, internalrep const& minMantissa, internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFix) + MantissaRange::CuspRoundingFix cuspRoundingFixEnabled) { // Not used by every compiler version, and thus not necessarily // counted by coverage build // LCOV_EXCL_START - doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFix, false); + doNormalize( + negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false); // LCOV_EXCL_STOP } @@ -657,12 +638,13 @@ Number::normalize( int& exponent, internalrep const& minMantissa, internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFix) + MantissaRange::CuspRoundingFix cuspRoundingFixEnabled) { // Not used by every compiler version, and thus not necessarily // counted by coverage build // LCOV_EXCL_START - doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFix, false); + doNormalize( + negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false); // LCOV_EXCL_STOP } @@ -674,27 +656,16 @@ Number::normalize( int& exponent, internalrep const& minMantissa, internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFix) + MantissaRange::CuspRoundingFix cuspRoundingFixEnabled) { - doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFix, false); + doNormalize( + negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false); } void Number::normalize(MantissaRange const& range) { - normalize(negative_, mantissa_, exponent_, range.min, range.max, range.cuspRoundingFix); -} - -void -Number::normalize(Guard const& guard) -{ - normalize( - negative_, - mantissa_, - exponent_, - guard.minMantissa_, - guard.maxMantissa_, - guard.cuspRoundingFix_); + normalize(negative_, mantissa_, exponent_, range.min, range.max, range.cuspRoundingFixEnabled); } // Copy the number, but set a new exponent. Because the mantissa doesn't change, @@ -748,16 +719,7 @@ Number::operator+=(Number const& y) bool const yn = y.negative_; uint128_t ym = y.mantissa_; auto ye = y.exponent_; - Guard g(kRange); - - auto const& minMantissa = g.minMantissa_; - auto const& maxMantissa = g.maxMantissa_; - auto const cuspRoundingFix = g.cuspRoundingFix_; - - // Bring the exponents of both values into agreement, so the mantissas are on the same scale - // and can be added directly together. - // Shrink the mantissa and bring the exponent up of the value with the lower exponent. Store any - // dropped digits in the Guard. + Guard g; if (xe < ye) { if (xn) @@ -777,6 +739,11 @@ Number::operator+=(Number const& y) } while (xe > ye); } + auto const& range = kRange.get(); + auto const& minMantissa = range.min; + auto const& maxMantissa = range.max; + auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled; + if (xn == yn) { xm += ym; @@ -784,7 +751,14 @@ Number::operator+=(Number const& y) { g.doDropDigit(xm, xe); } - g.doRoundUp(xn, xm, xe, "Number::addition overflow"); + g.doRoundUp( + xn, + xm, + xe, + minMantissa, + maxMantissa, + cuspRoundingFixEnabled, + "Number::addition overflow"); } else { @@ -798,40 +772,19 @@ Number::operator+=(Number const& y) xe = ye; xn = yn; } - if (cuspRoundingFix == MantissaRange::CuspRoundingFix::Enabled) + while (xm < minMantissa && xm * 10 <= kMaxRep) { - // Grow xm/xe and pull digits out of the Guard until it's a little bit larger than - // maxMantissa, so that normalize will have enough information to make an accurate - // rounding decision, but stop if the Guard empties out, because no rounding will be - // necessary. (Normalize will pad it back into range.) Note that if any digits were lost - // (xbit), the Guard will never be empty, so xm will get big. - auto const upperLimit = static_cast(minMantissa) * 1000; - while (xm < upperLimit && !g.empty()) - { - xm *= 10; - xm -= g.pop(); - --xe; - } + xm *= 10; + xm -= g.pop(); + --xe; } - else - { - // Grow xm/xe and pull digits out of the Guard until it's back in range. - while (xm < minMantissa && xm * 10 <= kMaxRep) - { - xm *= 10; - xm -= g.pop(); - --xe; - } - } - // Round down, based on whether there is any data left in the Guard (depending on - // cuspRoundingFix) - g.doRoundDown(xn, xm, xe); + g.doRoundDown(xn, xm, xe, minMantissa); } - doNormalize(xn, xm, xe, minMantissa, maxMantissa, cuspRoundingFix, false); negative_ = xn; mantissa_ = static_cast(xm); exponent_ = xe; + normalize(range); return *this; } @@ -865,11 +818,14 @@ Number::operator*=(Number const& y) auto ze = xe + ye; auto zs = xs * ys; bool zn = (zs == -1); - Guard g(kRange); + Guard g; if (zn) g.setNegative(); - auto const& maxMantissa = g.maxMantissa_; + auto const& range = kRange.get(); + auto const& minMantissa = range.min; + auto const& maxMantissa = range.max; + auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled; while (zm > maxMantissa || zm > kMaxRep) { @@ -878,12 +834,19 @@ Number::operator*=(Number const& y) xm = static_cast(zm); xe = ze; - g.doRoundUp(zn, xm, xe, "Number::multiplication overflow : exponent is " + std::to_string(xe)); + g.doRoundUp( + zn, + xm, + xe, + minMantissa, + maxMantissa, + cuspRoundingFixEnabled, + "Number::multiplication overflow : exponent is " + std::to_string(xe)); negative_ = zn; mantissa_ = xm; exponent_ = xe; - normalize(g); + normalize(range); return *this; } @@ -919,7 +882,7 @@ Number::operator/=(Number const& y) auto const& range = kRange.get(); auto const& minMantissa = range.min; auto const& maxMantissa = range.max; - auto const cuspRoundingFix = range.cuspRoundingFix; + auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled; // Division operates on two large integers (16-digit for small // mantissas, 19-digit for large) using integer math. If the values @@ -1051,14 +1014,14 @@ Number::operator/=(Number const& y) // rounding fix is enabled, flag if there is still // a remainder from stage 2. bool const useTrailingRemainder = - cuspRoundingFix == MantissaRange::CuspRoundingFix::Enabled; + cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled; if (useTrailingRemainder) { dropped = partialNumerator % dm != 0; } } } - doNormalize(zp, zm, ze, minMantissa, maxMantissa, cuspRoundingFix, dropped); + doNormalize(zp, zm, ze, minMantissa, maxMantissa, cuspRoundingFixEnabled, dropped); negative_ = zp; mantissa_ = static_cast(zm); exponent_ = ze; @@ -1072,7 +1035,7 @@ operator rep() const { rep drops = mantissa(); int offset = exponent(); - Guard g(kRange); + Guard g; if (drops != 0) { if (negative_) From c165af497e15c6ee72cdbc3ba88bc3565c052fcd Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 6 Jun 2026 14:35:35 -0400 Subject: [PATCH 50/77] Revert "Rollback Number class changes; show the fix works without side effects" This reverts commit 8743be8eae2a28b05f3683d3f0164dd50737305e. --- include/xrpl/basics/Number.h | 38 +++-- src/libxrpl/basics/Number.cpp | 285 +++++++++++++++++++--------------- 2 files changed, 185 insertions(+), 138 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 0b4caaa722..61b63fe234 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -51,37 +51,43 @@ namespace detail { * compile time. Doing it at runtime would be pretty wasteful and * inefficient. */ -constexpr std::size_t kInt64Digits = 20; -consteval std::array +constexpr std::size_t kUint64Digits = 20; +constexpr std::size_t kUint128Digits = 39; + +template +consteval std::array buildPowersOfTen() { - std::array result{}; + std::array result{}; - std::uint64_t power = 1; + T power = 1; std::size_t exponent = 0; // end the loop early so it doesn't overflow; for (; exponent < result.size() - 1; ++exponent, power *= 10) { result[exponent] = power; - if (power > std::numeric_limits::max() / 10) + if (power > std::numeric_limits::max() / 10) throw std::logic_error("Power of 10 table is too big"); } result[exponent] = power; - if (power < std::numeric_limits::max() / 10) - throw std::logic_error("Power of 10 table is not big enough for the uint64_t type"); + if (power < std::numeric_limits::max() / 10) + throw std::logic_error("Power of 10 table is not big enough for the given type"); return result; } } // namespace detail -constexpr std::array kPowerOfTen = detail::buildPowersOfTen(); +template +constexpr std::array kPowerOfTenImpl = detail::buildPowersOfTen(); + +constexpr auto kPowerOfTen = kPowerOfTenImpl; static_assert(kPowerOfTen[0] == 1); static_assert(kPowerOfTen[1] == 10); static_assert(kPowerOfTen[10] == 10'000'000'000); static_assert( - isPowerOfTen(kPowerOfTen.back()) && *logTen(kPowerOfTen.back()) == detail::kInt64Digits - 1); + isPowerOfTen(kPowerOfTen.back()) && *logTen(kPowerOfTen.back()) == detail::kUint64Digits - 1); /** MantissaRange defines a range for the mantissa of a normalized Number. * @@ -141,7 +147,7 @@ struct MantissaRange final int const log{getExponent(scale)}; rep const min{getMin(scale, log)}; rep const max{(min * 10) - 1}; - CuspRoundingFix const cuspRoundingFixEnabled{isCuspFixEnabled(scale)}; + CuspRoundingFix const cuspRoundingFix{isCuspFixEnabled(scale)}; static MantissaRange const& getMantissaRange(MantissaScale scale); @@ -543,9 +549,15 @@ private: // changing the values inside the range. static thread_local std::reference_wrapper kRange; + class Guard; + void normalize(MantissaRange const& range); + // Guard has the fields that we need, as well as MantissaRange, so if we have a guard, use that + void + normalize(Guard const& guard); + /** Normalize Number components to an arbitrary range. * * min/maxMantissa are parameters because this function is used by both @@ -560,7 +572,7 @@ private: int& exponent, internalrep const& minMantissa, internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled); + MantissaRange::CuspRoundingFix cuspRoundingFix); template friend void @@ -570,7 +582,7 @@ private: int& exponent, MantissaRange::rep const& minMantissa, MantissaRange::rep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled, + MantissaRange::CuspRoundingFix cuspRoundingFix, bool dropped); [[nodiscard]] bool @@ -588,8 +600,6 @@ private: // UB, and can vary across compilers. static internalrep externalToInternal(rep mantissa); - - class Guard; }; constexpr Number::Number(bool negative, internalrep mantissa, int exponent, Unchecked) noexcept diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 23e913bbdc..d43eebc314 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -65,7 +65,7 @@ MantissaRange::getRanges() static_assert(kRange.log == 15); static_assert(kRange.min < Number::kMaxRep); static_assert(kRange.max < Number::kMaxRep); - static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Disabled); + static_assert(kRange.cuspRoundingFix == CuspRoundingFix::Disabled); } { [[maybe_unused]] @@ -76,7 +76,7 @@ MantissaRange::getRanges() static_assert(kRange.log == 18); static_assert(kRange.min < Number::kMaxRep); static_assert(kRange.max > Number::kMaxRep); - static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Disabled); + static_assert(kRange.cuspRoundingFix == CuspRoundingFix::Disabled); } { [[maybe_unused]] @@ -87,7 +87,7 @@ MantissaRange::getRanges() static_assert(kRange.log == 18); static_assert(kRange.min < Number::kMaxRep); static_assert(kRange.max > Number::kMaxRep); - static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Enabled); + static_assert(kRange.cuspRoundingFix == CuspRoundingFix::Enabled); } return map; }(); @@ -171,7 +171,21 @@ class Number::Guard std::uint8_t sbit_ : 1 {0}; // the sign of the guard digits public: - explicit Guard() = default; + internalrep const minMantissa_; + internalrep const maxMantissa_; + MantissaRange::CuspRoundingFix const cuspRoundingFix_; + + explicit Guard( + internalrep const& minMantissa, + internalrep const& maxMantissa, + MantissaRange::CuspRoundingFix cuspRoundingFix) + : minMantissa_(minMantissa), maxMantissa_(maxMantissa), cuspRoundingFix_(cuspRoundingFix) + { + } + + explicit Guard(MantissaRange const& range) : Guard(range.min, range.max, range.cuspRoundingFix) + { + } // set & test the sign bit void @@ -194,6 +208,10 @@ public: unsigned pop() noexcept; + // if true, there are no digits in the guard, including dropped digits (xbit_) + bool + empty() const noexcept; + /** Drop a digit from the mantissa, and increment the exponent, storing the dropped digit in * this Guard. * @@ -206,28 +224,35 @@ public: void doDropDigit(T& mantissa, int& exponent) noexcept; + enum class Round { + // The result is exact. No rounding is needed. Only used if cuspRoundingFix is enabled. + Exact = -2, + // Round down. Since we use integer math, that usually means no change is needed. + // Exceptions are for when the result is between kMaxRap 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]] int + [[nodiscard]] Round round() const noexcept; // Modify the result to the correctly rounded value template void - doRoundUp( - bool& negative, - T& mantissa, - int& exponent, - internalrep const& minMantissa, - internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled, - std::string location); + doRoundUp(bool& negative, T& mantissa, int& exponent, std::string location); // Modify the result to the correctly rounded value template void - doRoundDown(bool& negative, T& mantissa, int& exponent, internalrep const& minMantissa); + doRoundDown(bool& negative, T& mantissa, int& exponent); // Modify the result to the correctly rounded value void @@ -239,7 +264,7 @@ private: template void - bringIntoRange(bool& negative, T& mantissa, int& exponent, internalrep const& minMantissa); + bringIntoRange(bool& negative, T& mantissa, int& exponent); }; inline void @@ -289,6 +314,12 @@ Number::Guard::pop() noexcept return d; } +inline bool +Number::Guard::empty() const noexcept +{ + return digits_ == 0 && !xbit_; +} + template void Number::Guard::doDropDigit(T& mantissa, int& exponent) noexcept @@ -314,54 +345,56 @@ Number::Guard::doDropDigit(uint128_t& mantissa, int& exponent) noexce // -1 if Guard is less than half // 0 if Guard is exactly half // 1 if Guard is greater than half -int +Number::Guard::Round Number::Guard::round() const noexcept { auto mode = Number::getround(); + if (cuspRoundingFix_ != MantissaRange::CuspRoundingFix::Disabled && empty()) + { + // No remainder + return Round::Exact; + } + if (mode == RoundingMode::TowardsZero) - return -1; + return Round::Down; if (mode == RoundingMode::Downward) { if (sbit_) { if (digits_ > 0 || xbit_) - return 1; + return Round::Up; } - return -1; + return Round::Down; } if (mode == RoundingMode::Upward) { if (sbit_) - return -1; + return Round::Down; if (digits_ > 0 || xbit_) - return 1; - return -1; + return Round::Up; + return Round::Down; } // assume round to nearest if mode is not one of the predefined values if (digits_ > 0x5000'0000'0000'0000) - return 1; + return Round::Up; if (digits_ < 0x5000'0000'0000'0000) - return -1; + return Round::Down; if (xbit_) - return 1; - return 0; + return Round::Up; + return Round::Even; } template void -Number::Guard::bringIntoRange( - bool& negative, - T& mantissa, - int& exponent, - internalrep const& minMantissa) +Number::Guard::bringIntoRange(bool& negative, T& mantissa, int& exponent) { // Bring mantissa back into the minMantissa / maxMantissa range AFTER // rounding - if (mantissa < minMantissa) + if (mantissa < minMantissa_) { mantissa *= 10; --exponent; @@ -378,22 +411,15 @@ Number::Guard::bringIntoRange( template void -Number::Guard::doRoundUp( - bool& negative, - T& mantissa, - int& exponent, - internalrep const& minMantissa, - internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled, - std::string location) +Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string location) { auto r = round(); - if (r == 1 || (r == 0 && (mantissa & 1) == 1)) + if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1)) { - auto const safeToIncrement = [&maxMantissa](auto const& mantissa) { - return mantissa < maxMantissa && mantissa < kMaxRep; + auto const safeToIncrement = [this](auto const& mantissa) { + return mantissa < maxMantissa_ && mantissa < kMaxRep; }; - if (cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled) + if (cuspRoundingFix_ == MantissaRange::CuspRoundingFix::Enabled) { // Ensure mantissa after incrementing fits within both the // min/maxMantissa range and is a valid "rep". @@ -414,14 +440,7 @@ Number::Guard::doRoundUp( safeToIncrement(mantissa), "xrpl::Number::Guard::doRoundUp", "can't recurse more than once"); - doRoundUp( - negative, - mantissa, - exponent, - minMantissa, - maxMantissa, - cuspRoundingFixEnabled, - location); + doRoundUp(negative, mantissa, exponent, location); return; } } @@ -432,7 +451,7 @@ Number::Guard::doRoundUp( ++mantissa; // Ensure mantissa after incrementing fits within both the // min/maxMantissa range and is a valid "rep". - if (mantissa > maxMantissa || mantissa > kMaxRep) + if (mantissa > maxMantissa_ || mantissa > kMaxRep) { // Don't use doDropDigit here mantissa /= 10; @@ -440,30 +459,38 @@ Number::Guard::doRoundUp( } } } - bringIntoRange(negative, mantissa, exponent, minMantissa); + bringIntoRange(negative, mantissa, exponent); if (exponent > kMaxExponent) Throw(std::string(location)); } template void -Number::Guard::doRoundDown( - bool& negative, - T& mantissa, - int& exponent, - internalrep const& minMantissa) +Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) { auto r = round(); - if (r == 1 || (r == 0 && (mantissa & 1) == 1)) + if (cuspRoundingFix_ != MantissaRange::CuspRoundingFix::Disabled) { - --mantissa; - if (mantissa < minMantissa) + // If there was any remainder, subtract 1 from the result. This is sufficient to get the + // best rounding. + if (r != Round::Exact) { - mantissa *= 10; - --exponent; + --mantissa; } } - bringIntoRange(negative, mantissa, exponent, minMantissa); + else + { + if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1)) + { + --mantissa; + if (mantissa < minMantissa_) + { + mantissa *= 10; + --exponent; + } + } + } + bringIntoRange(negative, mantissa, exponent); } // Modify the result to the correctly rounded value @@ -471,7 +498,7 @@ void Number::Guard::doRound(rep& drops, std::string location) const { auto r = round(); - if (r == 1 || (r == 0 && (drops & 1) == 1)) + if (r == Round::Up || (r == Round::Even && (drops & 1) == 1)) { if (drops >= kMaxRep) { @@ -530,7 +557,7 @@ doNormalize( int& exponent, MantissaRange::rep const& minMantissa, MantissaRange::rep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled, + MantissaRange::CuspRoundingFix cuspRoundingFix, bool dropped) { static constexpr auto kMinExponent = Number::kMinExponent; @@ -553,7 +580,7 @@ doNormalize( m *= 10; --exponent; } - Guard g; + Guard g(minMantissa, maxMantissa, cuspRoundingFix); if (negative) g.setNegative(); if (dropped) @@ -598,14 +625,7 @@ doNormalize( XRPL_ASSERT_PARTS(m <= kMaxRep, "xrpl::doNormalize", "intermediate mantissa fits in int64"); mantissa = m; - g.doRoundUp( - negative, - mantissa, - exponent, - minMantissa, - maxMantissa, - cuspRoundingFixEnabled, - "Number::normalize 2"); + g.doRoundUp(negative, mantissa, exponent, "Number::normalize 2"); XRPL_ASSERT_PARTS( mantissa >= minMantissa && mantissa <= maxMantissa, "xrpl::doNormalize", @@ -620,13 +640,12 @@ Number::normalize( int& exponent, internalrep const& minMantissa, internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled) + MantissaRange::CuspRoundingFix cuspRoundingFix) { // Not used by every compiler version, and thus not necessarily // counted by coverage build // LCOV_EXCL_START - doNormalize( - negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false); + doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFix, false); // LCOV_EXCL_STOP } @@ -638,13 +657,12 @@ Number::normalize( int& exponent, internalrep const& minMantissa, internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled) + MantissaRange::CuspRoundingFix cuspRoundingFix) { // Not used by every compiler version, and thus not necessarily // counted by coverage build // LCOV_EXCL_START - doNormalize( - negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false); + doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFix, false); // LCOV_EXCL_STOP } @@ -656,16 +674,27 @@ Number::normalize( int& exponent, internalrep const& minMantissa, internalrep const& maxMantissa, - MantissaRange::CuspRoundingFix cuspRoundingFixEnabled) + MantissaRange::CuspRoundingFix cuspRoundingFix) { - doNormalize( - negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false); + doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFix, false); } void Number::normalize(MantissaRange const& range) { - normalize(negative_, mantissa_, exponent_, range.min, range.max, range.cuspRoundingFixEnabled); + normalize(negative_, mantissa_, exponent_, range.min, range.max, range.cuspRoundingFix); +} + +void +Number::normalize(Guard const& guard) +{ + normalize( + negative_, + mantissa_, + exponent_, + guard.minMantissa_, + guard.maxMantissa_, + guard.cuspRoundingFix_); } // Copy the number, but set a new exponent. Because the mantissa doesn't change, @@ -719,7 +748,16 @@ Number::operator+=(Number const& y) bool const yn = y.negative_; uint128_t ym = y.mantissa_; auto ye = y.exponent_; - Guard g; + Guard g(kRange); + + auto const& minMantissa = g.minMantissa_; + auto const& maxMantissa = g.maxMantissa_; + auto const cuspRoundingFix = g.cuspRoundingFix_; + + // Bring the exponents of both values into agreement, so the mantissas are on the same scale + // and can be added directly together. + // Shrink the mantissa and bring the exponent up of the value with the lower exponent. Store any + // dropped digits in the Guard. if (xe < ye) { if (xn) @@ -739,11 +777,6 @@ Number::operator+=(Number const& y) } while (xe > ye); } - auto const& range = kRange.get(); - auto const& minMantissa = range.min; - auto const& maxMantissa = range.max; - auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled; - if (xn == yn) { xm += ym; @@ -751,14 +784,7 @@ Number::operator+=(Number const& y) { g.doDropDigit(xm, xe); } - g.doRoundUp( - xn, - xm, - xe, - minMantissa, - maxMantissa, - cuspRoundingFixEnabled, - "Number::addition overflow"); + g.doRoundUp(xn, xm, xe, "Number::addition overflow"); } else { @@ -772,19 +798,40 @@ Number::operator+=(Number const& y) xe = ye; xn = yn; } - while (xm < minMantissa && xm * 10 <= kMaxRep) + if (cuspRoundingFix == MantissaRange::CuspRoundingFix::Enabled) { - xm *= 10; - xm -= g.pop(); - --xe; + // Grow xm/xe and pull digits out of the Guard until it's a little bit larger than + // maxMantissa, so that normalize will have enough information to make an accurate + // rounding decision, but stop if the Guard empties out, because no rounding will be + // necessary. (Normalize will pad it back into range.) Note that if any digits were lost + // (xbit), the Guard will never be empty, so xm will get big. + auto const upperLimit = static_cast(minMantissa) * 1000; + while (xm < upperLimit && !g.empty()) + { + xm *= 10; + xm -= g.pop(); + --xe; + } } - g.doRoundDown(xn, xm, xe, minMantissa); + else + { + // Grow xm/xe and pull digits out of the Guard until it's back in range. + while (xm < minMantissa && xm * 10 <= kMaxRep) + { + xm *= 10; + xm -= g.pop(); + --xe; + } + } + // Round down, based on whether there is any data left in the Guard (depending on + // cuspRoundingFix) + g.doRoundDown(xn, xm, xe); } + doNormalize(xn, xm, xe, minMantissa, maxMantissa, cuspRoundingFix, false); negative_ = xn; mantissa_ = static_cast(xm); exponent_ = xe; - normalize(range); return *this; } @@ -818,14 +865,11 @@ Number::operator*=(Number const& y) auto ze = xe + ye; auto zs = xs * ys; bool zn = (zs == -1); - Guard g; + Guard g(kRange); if (zn) g.setNegative(); - auto const& range = kRange.get(); - auto const& minMantissa = range.min; - auto const& maxMantissa = range.max; - auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled; + auto const& maxMantissa = g.maxMantissa_; while (zm > maxMantissa || zm > kMaxRep) { @@ -834,19 +878,12 @@ Number::operator*=(Number const& y) xm = static_cast(zm); xe = ze; - g.doRoundUp( - zn, - xm, - xe, - minMantissa, - maxMantissa, - cuspRoundingFixEnabled, - "Number::multiplication overflow : exponent is " + std::to_string(xe)); + g.doRoundUp(zn, xm, xe, "Number::multiplication overflow : exponent is " + std::to_string(xe)); negative_ = zn; mantissa_ = xm; exponent_ = xe; - normalize(range); + normalize(g); return *this; } @@ -882,7 +919,7 @@ Number::operator/=(Number const& y) auto const& range = kRange.get(); auto const& minMantissa = range.min; auto const& maxMantissa = range.max; - auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled; + auto const cuspRoundingFix = range.cuspRoundingFix; // Division operates on two large integers (16-digit for small // mantissas, 19-digit for large) using integer math. If the values @@ -1014,14 +1051,14 @@ Number::operator/=(Number const& y) // rounding fix is enabled, flag if there is still // a remainder from stage 2. bool const useTrailingRemainder = - cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled; + cuspRoundingFix == MantissaRange::CuspRoundingFix::Enabled; if (useTrailingRemainder) { dropped = partialNumerator % dm != 0; } } } - doNormalize(zp, zm, ze, minMantissa, maxMantissa, cuspRoundingFixEnabled, dropped); + doNormalize(zp, zm, ze, minMantissa, maxMantissa, cuspRoundingFix, dropped); negative_ = zp; mantissa_ = static_cast(zm); exponent_ = ze; @@ -1035,7 +1072,7 @@ operator rep() const { rep drops = mantissa(); int offset = exponent(); - Guard g; + Guard g(kRange); if (drops != 0) { if (negative_) From d7ce0e2dd375fc1d1847d88452989ed66a794c93 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 6 Jun 2026 15:37:03 -0400 Subject: [PATCH 51/77] Fix formatting, add an assert --- src/libxrpl/basics/Number.cpp | 3 +++ src/test/basics/Number_test.cpp | 38 +++++++++++++++++++++------------ 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index d43eebc314..9fc464f487 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -473,6 +473,9 @@ Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) { // If there was any remainder, subtract 1 from the result. This is sufficient to get the // best rounding. + XRPL_ASSERT( + empty() || mantissa > maxMantissa_, + "xrpl::Number::Guard::doRoundDown : mantissa is expected size"); if (r != Round::Exact) { --mantissa; diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index aa57af01b3..d8b87c5624 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -1981,14 +1981,19 @@ public: case MantissaRange::MantissaScale::Small: case MantissaRange::MantissaScale::LargeLegacy: { // Without the fix, all the results but one round up - for (auto const& [r, sum] : sums) { - if (r == Number::RoundingMode::Downward) { - // Downward works because the Guard sign is negative, and Downward returns Up - // instead of Down if negative and there's a remainder, whereas TowardsZero - // always returns Down. - BEAST_EXPECTS(sums.at(Number::RoundingMode::Downward).first < exact, to_string(r)); + for (auto const& [r, sum] : sums) + { + if (r == Number::RoundingMode::Downward) + { + // Downward works because the Guard sign is negative, and Downward + // returns Up instead of Down if negative and there's a remainder, + // whereas TowardsZero always returns Down. + BEAST_EXPECTS( + sums.at(Number::RoundingMode::Downward).first < exact, + to_string(r)); } - else { + else + { BEAST_EXPECTS(sums.at(r).first > exact, to_string(r)); } } @@ -2058,14 +2063,19 @@ public: { case MantissaRange::MantissaScale::Small: case MantissaRange::MantissaScale::LargeLegacy: { - for (auto const& [r, sum] : sums) { - if (r == Number::RoundingMode::Downward) { - // Downward works because the Guard sign is negative, and Downward returns Up - // instead of Down if negative and there's a remainder, whereas TowardsZero - // always returns Down. - BEAST_EXPECTS(sums.at(Number::RoundingMode::Downward).first < exact, to_string(r)); + for (auto const& [r, sum] : sums) + { + if (r == Number::RoundingMode::Downward) + { + // Downward works because the Guard sign is negative, and Downward + // returns Up instead of Down if negative and there's a remainder, + // whereas TowardsZero always returns Down. + BEAST_EXPECTS( + sums.at(Number::RoundingMode::Downward).first < exact, + to_string(r)); } - else { + else + { BEAST_EXPECTS(sums.at(r).first > exact, to_string(r)); } } From fe64b99147e5930e0fb4b37540db2df9ec05ab22 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 6 Jun 2026 16:19:25 -0400 Subject: [PATCH 52/77] Reorganize the subtraction tests --- src/test/basics/Number_test.cpp | 40 ++++++++++++++++----------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index d8b87c5624..4b929799ac 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -1976,45 +1976,43 @@ public: } log.flush(); - switch (scale) + for (auto const& [r, sum] : sums) { - case MantissaRange::MantissaScale::Small: - case MantissaRange::MantissaScale::LargeLegacy: { - // Without the fix, all the results but one round up - for (auto const& [r, sum] : sums) - { + auto const epsilon = pow10(sum.second.exponent()); + auto diff = sum.first - exact; + auto const rLabel = to_string(r); + switch (scale) + { + case MantissaRange::MantissaScale::Small: + case MantissaRange::MantissaScale::LargeLegacy: { + // Without the fix, all the results but one round up if (r == Number::RoundingMode::Downward) { // Downward works because the Guard sign is negative, and Downward // returns Up instead of Down if negative and there's a remainder, // whereas TowardsZero always returns Down. - BEAST_EXPECTS( - sums.at(Number::RoundingMode::Downward).first < exact, - to_string(r)); + BEAST_EXPECTS(sum.first < exact, rLabel); + BEAST_EXPECTS(diff == -(epsilon - 1), rLabel); } else { - BEAST_EXPECTS(sums.at(r).first > exact, to_string(r)); + BEAST_EXPECTS(sum.first > exact, rLabel); + BEAST_EXPECTS(diff == 1, rLabel); } + break; } - break; - } - default: { - for (auto const& [r, sum] : sums) - { - auto const epsilon = pow10(sum.second.exponent()); + default: { BEAST_EXPECT(epsilon == 100); - auto diff = sum.first - exact; switch (r) { case Number::RoundingMode::Upward: case Number::RoundingMode::ToNearest: - BEAST_EXPECTS(sum.first > exact, to_string(r)); - BEAST_EXPECTS(diff < epsilon, to_string(r)); + BEAST_EXPECTS(sum.first > exact, rLabel); + BEAST_EXPECTS(diff == 1, rLabel); break; default: - BEAST_EXPECTS(sum.first < exact, to_string(r)); - BEAST_EXPECTS(-diff < epsilon, to_string(r)); + BEAST_EXPECTS(sum.first < exact, rLabel); + BEAST_EXPECTS(diff == -(epsilon - 1), rLabel); } } } From 308e46d0dabdb7fef09b526590d773a1bcea9a51 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 8 Jun 2026 19:00:24 -0400 Subject: [PATCH 53/77] Clean up tests --- src/libxrpl/basics/Number.cpp | 11 +- src/test/basics/Number_test.cpp | 257 +++++++++++++------------------- 2 files changed, 109 insertions(+), 159 deletions(-) diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 9fc464f487..9e61cdc76d 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -474,7 +474,7 @@ Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) // If there was any remainder, subtract 1 from the result. This is sufficient to get the // best rounding. XRPL_ASSERT( - empty() || mantissa > maxMantissa_, + r == Round::Exact || mantissa > maxMantissa_, "xrpl::Number::Guard::doRoundDown : mantissa is expected size"); if (r != Round::Exact) { @@ -759,7 +759,7 @@ Number::operator+=(Number const& y) // Bring the exponents of both values into agreement, so the mantissas are on the same scale // and can be added directly together. - // Shrink the mantissa and bring the exponent up of the value with the lower exponent. Store any + // Shrink the mantissa and raise the exponent of the value with the lower exponent. Store any // dropped digits in the Guard. if (xe < ye) { @@ -801,13 +801,13 @@ Number::operator+=(Number const& y) xe = ye; xn = yn; } - if (cuspRoundingFix == MantissaRange::CuspRoundingFix::Enabled) + if (cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled) { // Grow xm/xe and pull digits out of the Guard until it's a little bit larger than // maxMantissa, so that normalize will have enough information to make an accurate // rounding decision, but stop if the Guard empties out, because no rounding will be // necessary. (Normalize will pad it back into range.) Note that if any digits were lost - // (xbit), the Guard will never be empty, so xm will get big. + // (xbit), the Guard will never be empty, so xm will get larger than upperLimit. auto const upperLimit = static_cast(minMantissa) * 1000; while (xm < upperLimit && !g.empty()) { @@ -818,7 +818,8 @@ Number::operator+=(Number const& y) } else { - // Grow xm/xe and pull digits out of the Guard until it's back in range. + // 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) { xm *= 10; diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 4b929799ac..50be994b7c 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -1928,172 +1928,121 @@ public: } { - testcase << "subtraction rounding " << to_string(scale); - auto const exp = Number::mantissaLog(); - Number const a{1LL, exp + 2}; - Number const b{-(Number{1, exp} + 1)}; - - if (scale == MantissaRange::MantissaScale::Small) + // SubCase is + // * offset: offset from exp + // * extraB: whether to include 1e"exp" in "b" + // * aString: expected string value for "a" + // * bString: expected string value for "b" + // There aren't too many valid combinations for test cases here. If extraB is true, + // offset can really only be 2, because any larger and the mantissa can't be represented + // without loss. Offset can't be less than 2, or there's no error. + using SubCase = std::tuple; + auto const c = std::to_array({ + {2, + true, + scale == MantissaRange::MantissaScale::Small ? "100000000000000000" + : "100000000000000000000", + scale == MantissaRange::MantissaScale::Small ? "-1000000000000001" + : "-1000000000000000001"}, + {2, + false, + scale == MantissaRange::MantissaScale::Small ? "100000000000000000" + : "100000000000000000000", + "-1"}, + {30, + false, + scale == MantissaRange::MantissaScale::Small + ? "1000000000000000000000000000000000000000000000" + : "1000000000000000000000000000000000000000000000000", + "-1"}, + }); + for (auto const& [offset, extraB, aString, bString] : c) { - BEAST_EXPECT(toBigInt(a) == BigInt{"100000000000000000"}); - BEAST_EXPECT(toBigInt(b) == BigInt{"-1000000000000001"}); - } - else - { - BEAST_EXPECT(toBigInt(a) == BigInt{"100000000000000000000"}); - BEAST_EXPECT(toBigInt(b) == BigInt{"-1000000000000000001"}); - } + testcase << "subtraction rounding. offset: " << offset + << ", scale: " << to_string(scale); - auto construct = [&a, &b, this](Number::RoundingMode r) { - NumberRoundModeGuard const roundGuard{r}; - auto const sum = a + b; - BigInt const stored = toBigInt(sum); - return std::make_pair(r, std::make_pair(stored, sum)); - }; + Number const a{1LL, exp + offset}; + Number const b{-((extraB ? Number{1, exp} : kNumZero) + 1)}; - auto const bigA = toBigInt(a); - auto const bigB = toBigInt(b); - BigInt const exact = bigA + bigB; + auto const bigA = toBigInt(a); + auto const bigB = toBigInt(b); - auto const sums = [&]() { - std::map> sums; - sums.emplace(construct(Number::RoundingMode::TowardsZero)); - sums.emplace(construct(Number::RoundingMode::Upward)); - sums.emplace(construct(Number::RoundingMode::Downward)); - sums.emplace(construct(Number::RoundingMode::ToNearest)); - return sums; - }(); + BEAST_EXPECT(bigA == BigInt{aString}); + BEAST_EXPECT(bigB == BigInt{bString}); - log << "\n a = " << a << " (" << fmt(bigA) << ")\n b = " << b - << " (" << fmt(bigB) << ")\n exact a + b = " << fmt(exact) << "\n"; - for (auto const& [r, sum] : sums) - { - auto const diff = sum.first - exact; - auto const rLabel = to_string(r); - log << std::string(15 - rLabel.length(), ' ') << rLabel << " = " << fmt(sum.first) - << "\n difference = " << fmt(diff) << "\n"; - } - log.flush(); + auto construct = [&a, &b, this](Number::RoundingMode r) { + NumberRoundModeGuard const roundGuard{r}; + auto const sum = a + b; + BigInt const stored = toBigInt(sum); + return std::make_pair(r, std::make_pair(stored, sum)); + }; - for (auto const& [r, sum] : sums) - { - auto const epsilon = pow10(sum.second.exponent()); - auto diff = sum.first - exact; - auto const rLabel = to_string(r); - switch (scale) + BigInt const exact = bigA + bigB; + + auto const sums = [&]() { + std::map> sums; + sums.emplace(construct(Number::RoundingMode::TowardsZero)); + sums.emplace(construct(Number::RoundingMode::Upward)); + sums.emplace(construct(Number::RoundingMode::Downward)); + sums.emplace(construct(Number::RoundingMode::ToNearest)); + return sums; + }(); + + log << "\n a = " << a << " (" << fmt(bigA) + << ")\n b = " << b << " (" << fmt(bigB) + << ")\n exact a + b = " << fmt(exact) << "\n"; + for (auto const& [r, sum] : sums) { - case MantissaRange::MantissaScale::Small: - case MantissaRange::MantissaScale::LargeLegacy: { - // Without the fix, all the results but one round up - if (r == Number::RoundingMode::Downward) - { - // Downward works because the Guard sign is negative, and Downward - // returns Up instead of Down if negative and there's a remainder, - // whereas TowardsZero always returns Down. - BEAST_EXPECTS(sum.first < exact, rLabel); - BEAST_EXPECTS(diff == -(epsilon - 1), rLabel); - } - else - { - BEAST_EXPECTS(sum.first > exact, rLabel); - BEAST_EXPECTS(diff == 1, rLabel); - } - break; - } - default: { - BEAST_EXPECT(epsilon == 100); - switch (r) - { - case Number::RoundingMode::Upward: - case Number::RoundingMode::ToNearest: - BEAST_EXPECTS(sum.first > exact, rLabel); - BEAST_EXPECTS(diff == 1, rLabel); - break; - default: + auto const diff = sum.first - exact; + auto const rLabel = to_string(r); + log << std::string(15 - rLabel.length(), ' ') << rLabel << " = " + << fmt(sum.first) << "\n difference = " << fmt(diff) << "\n"; + } + log.flush(); + + auto const expectedExponent = + offset - (scale == MantissaRange::MantissaScale::Small && extraB ? 1 : 0); + auto const epsilon = pow10(expectedExponent); + for (auto const& [r, sum] : sums) + { + auto diff = sum.first - exact; + auto const rLabel = to_string(r); + switch (scale) + { + case MantissaRange::MantissaScale::Small: + case MantissaRange::MantissaScale::LargeLegacy: { + // Without the fix, all the results but one round up + if (r == Number::RoundingMode::Downward) + { + // Downward works because the Guard sign is negative, and Downward + // returns Up instead of Down if negative and there's a remainder, + // whereas TowardsZero always returns Down. BEAST_EXPECTS(sum.first < exact, rLabel); BEAST_EXPECTS(diff == -(epsilon - 1), rLabel); + } + else + { + BEAST_EXPECTS(sum.first > exact, rLabel); + BEAST_EXPECTS(diff == 1, rLabel); + } + break; } - } - } - } - } - { - auto const offset = 30; - testcase << "subtraction rounding offset of " << offset << " " << to_string(scale); - - auto const exp = Number::mantissaLog(); - Number const a{1LL, exp + offset}; - Number const b{-1}; - - auto construct = [&a, &b, this](Number::RoundingMode r) { - NumberRoundModeGuard const roundGuard{r}; - auto const sum = a + b; - BigInt const stored = toBigInt(sum); - return std::make_pair(r, std::make_pair(stored, sum)); - }; - - auto const bigA = toBigInt(a); - auto const bigB = toBigInt(b); - BigInt const exact = bigA + bigB; - - auto const sums = [&]() { - std::map> sums; - sums.emplace(construct(Number::RoundingMode::TowardsZero)); - sums.emplace(construct(Number::RoundingMode::Upward)); - sums.emplace(construct(Number::RoundingMode::Downward)); - sums.emplace(construct(Number::RoundingMode::ToNearest)); - return sums; - }(); - - log << "\n a = " << a << " (" << fmt(bigA) << ")\n b = " << b - << " (" << fmt(bigB) << ")\n exact a + b = " << fmt(exact) << "\n"; - for (auto const& [r, sum] : sums) - { - auto const diff = sum.first - exact; - auto const rLabel = to_string(r); - log << std::string(15 - rLabel.length(), ' ') << rLabel << " = " << fmt(sum.first) - << "\n difference = " << fmt(diff) << "\n"; - } - log.flush(); - - switch (scale) - { - case MantissaRange::MantissaScale::Small: - case MantissaRange::MantissaScale::LargeLegacy: { - for (auto const& [r, sum] : sums) - { - if (r == Number::RoundingMode::Downward) - { - // Downward works because the Guard sign is negative, and Downward - // returns Up instead of Down if negative and there's a remainder, - // whereas TowardsZero always returns Down. + default: { BEAST_EXPECTS( - sums.at(Number::RoundingMode::Downward).first < exact, - to_string(r)); - } - else - { - BEAST_EXPECTS(sums.at(r).first > exact, to_string(r)); - } - } - break; - } - default: { - for (auto const& [r, sum] : sums) - { - auto const epsilon = pow10(sum.second.exponent()); - auto diff = sum.first - exact; - switch (r) - { - case Number::RoundingMode::Upward: - case Number::RoundingMode::ToNearest: - BEAST_EXPECTS(sum.first > exact, to_string(r)); - BEAST_EXPECTS(diff < epsilon, to_string(r)); - break; - default: - BEAST_EXPECTS(sum.first < exact, to_string(r)); - BEAST_EXPECTS(-diff < epsilon, to_string(r)); + sum.second.exponent() <= expectedExponent, + to_string(sum.second.exponent())); + switch (r) + { + case Number::RoundingMode::Upward: + case Number::RoundingMode::ToNearest: + BEAST_EXPECTS(sum.first > exact, rLabel); + BEAST_EXPECTS(diff == 1, rLabel); + break; + default: + BEAST_EXPECTS(sum.first < exact, rLabel); + BEAST_EXPECTS(diff == -(epsilon - 1), rLabel); + } } } } From 5bccfe2b6ded5ba936c0bc7eed2e2605e448f81c Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 8 Jun 2026 19:03:53 -0400 Subject: [PATCH 54/77] clang-tidy: Guard public member variable names; Missing include --- src/libxrpl/basics/Number.cpp | 40 ++++++++++++++++----------------- src/test/basics/Number_test.cpp | 1 + 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 9e61cdc76d..9386b411d0 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -171,15 +171,15 @@ class Number::Guard std::uint8_t sbit_ : 1 {0}; // the sign of the guard digits public: - internalrep const minMantissa_; - internalrep const maxMantissa_; - MantissaRange::CuspRoundingFix const cuspRoundingFix_; + internalrep const minMantissa; + internalrep const maxMantissa; + MantissaRange::CuspRoundingFix const cuspRoundingFix; explicit Guard( internalrep const& minMantissa, internalrep const& maxMantissa, MantissaRange::CuspRoundingFix cuspRoundingFix) - : minMantissa_(minMantissa), maxMantissa_(maxMantissa), cuspRoundingFix_(cuspRoundingFix) + : minMantissa(minMantissa), maxMantissa(maxMantissa), cuspRoundingFix(cuspRoundingFix) { } @@ -209,7 +209,7 @@ public: pop() noexcept; // if true, there are no digits in the guard, including dropped digits (xbit_) - bool + [[nodiscard]] bool empty() const noexcept; /** Drop a digit from the mantissa, and increment the exponent, storing the dropped digit in @@ -350,7 +350,7 @@ Number::Guard::round() const noexcept { auto mode = Number::getround(); - if (cuspRoundingFix_ != MantissaRange::CuspRoundingFix::Disabled && empty()) + if (cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled && empty()) { // No remainder return Round::Exact; @@ -394,7 +394,7 @@ Number::Guard::bringIntoRange(bool& negative, T& mantissa, int& exponent) { // Bring mantissa back into the minMantissa / maxMantissa range AFTER // rounding - if (mantissa < minMantissa_) + if (mantissa < minMantissa) { mantissa *= 10; --exponent; @@ -417,9 +417,9 @@ Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1)) { auto const safeToIncrement = [this](auto const& mantissa) { - return mantissa < maxMantissa_ && mantissa < kMaxRep; + return mantissa < maxMantissa && mantissa < kMaxRep; }; - if (cuspRoundingFix_ == MantissaRange::CuspRoundingFix::Enabled) + if (cuspRoundingFix == MantissaRange::CuspRoundingFix::Enabled) { // Ensure mantissa after incrementing fits within both the // min/maxMantissa range and is a valid "rep". @@ -451,7 +451,7 @@ Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string ++mantissa; // Ensure mantissa after incrementing fits within both the // min/maxMantissa range and is a valid "rep". - if (mantissa > maxMantissa_ || mantissa > kMaxRep) + if (mantissa > maxMantissa || mantissa > kMaxRep) { // Don't use doDropDigit here mantissa /= 10; @@ -469,12 +469,12 @@ void Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) { auto r = round(); - if (cuspRoundingFix_ != MantissaRange::CuspRoundingFix::Disabled) + if (cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled) { // If there was any remainder, subtract 1 from the result. This is sufficient to get the // best rounding. XRPL_ASSERT( - r == Round::Exact || mantissa > maxMantissa_, + r == Round::Exact || mantissa > maxMantissa, "xrpl::Number::Guard::doRoundDown : mantissa is expected size"); if (r != Round::Exact) { @@ -486,7 +486,7 @@ Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1)) { --mantissa; - if (mantissa < minMantissa_) + if (mantissa < minMantissa) { mantissa *= 10; --exponent; @@ -695,9 +695,9 @@ Number::normalize(Guard const& guard) negative_, mantissa_, exponent_, - guard.minMantissa_, - guard.maxMantissa_, - guard.cuspRoundingFix_); + guard.minMantissa, + guard.maxMantissa, + guard.cuspRoundingFix); } // Copy the number, but set a new exponent. Because the mantissa doesn't change, @@ -753,9 +753,9 @@ Number::operator+=(Number const& y) auto ye = y.exponent_; Guard g(kRange); - auto const& minMantissa = g.minMantissa_; - auto const& maxMantissa = g.maxMantissa_; - auto const cuspRoundingFix = g.cuspRoundingFix_; + auto const& minMantissa = g.minMantissa; + auto const& maxMantissa = g.maxMantissa; + auto const cuspRoundingFix = g.cuspRoundingFix; // Bring the exponents of both values into agreement, so the mantissas are on the same scale // and can be added directly together. @@ -873,7 +873,7 @@ Number::operator*=(Number const& y) if (zn) g.setNegative(); - auto const& maxMantissa = g.maxMantissa_; + auto const& maxMantissa = g.maxMantissa; while (zm > maxMantissa || zm > kMaxRep) { diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 50be994b7c..d2e9ac2638 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -20,6 +20,7 @@ #include #include #include +#include namespace xrpl { From 85980aec6ad03a859bebc2b76985d5698a755e40 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 8 Jun 2026 19:44:44 -0400 Subject: [PATCH 55/77] Fix broken unit tests --- src/test/app/LoanBroker_test.cpp | 54 ++++++++++++++++++++++++++------ src/test/app/Vault_test.cpp | 36 ++++++++++----------- 2 files changed, 63 insertions(+), 27 deletions(-) diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 0edb955b90..4ad046f2ac 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -1452,18 +1452,54 @@ class LoanBroker_test : public beast::unit_test::Suite env(tx2, Ter(temINVALID)); } + if (Number::getMantissaScale() == MantissaRange::MantissaScale::Large) { - auto const dm = power(2, 63) - 1; - BEAST_EXPECTS(dm > kMaxMpTokenAmount, to_string(dm)); - tx2[sfDebtMaximum] = dm; - env(tx2, Ter(temINVALID)); - } + // For the Large scale, 2^63 rounds _down_ to Number::kMaxRep + { + auto const dm = power(2, 63); + BEAST_EXPECTS(dm == kMaxMpTokenAmount, to_string(dm)); + tx2[sfDebtMaximum] = dm; + env(tx2, Ter(tesSUCCESS)); + } + { + auto const dm = power(2, 63) - 1; + BEAST_EXPECTS(dm < kMaxMpTokenAmount, to_string(dm)); + tx2[sfDebtMaximum] = dm; + env(tx2, Ter(tesSUCCESS)); + } + + { + auto const dm = power(2, 63) - 3; + BEAST_EXPECTS(dm < kMaxMpTokenAmount, to_string(dm)); + tx2[sfDebtMaximum] = dm; + env(tx2, Ter(tesSUCCESS)); + } + + { + auto const dm = power(2, 63) + 3; + BEAST_EXPECTS(dm > kMaxMpTokenAmount, to_string(dm)); + tx2[sfDebtMaximum] = dm; + env(tx2, Ter(temINVALID)); + } + } + else { - auto const dm = power(2, 63) - 3; - BEAST_EXPECTS(dm == kMaxMpTokenAmount, to_string(dm)); - tx2[sfDebtMaximum] = dm; - env(tx2, Ter(tesSUCCESS)); + // For other scales, 2^63 rounds _up_ to Number::kMaxRepUp. Subtracting 1 rounds up + // again. + { + auto const dm = power(2, 63) - 1; + BEAST_EXPECTS(dm > kMaxMpTokenAmount, to_string(dm)); + tx2[sfDebtMaximum] = dm; + env(tx2, Ter(temINVALID)); + } + + { + auto const dm = power(2, 63) - 3; + BEAST_EXPECTS(dm == kMaxMpTokenAmount, to_string(dm)); + tx2[sfDebtMaximum] = dm; + env(tx2, Ter(tesSUCCESS)); + } } { diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 2c83ad91ec..8c01031773 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -5809,9 +5809,9 @@ class Vault_test : public beast::unit_test::Suite BEAST_EXPECT(maxInt64 == "9223372036854775807"); // Naming things is hard - auto const maxInt64Plus1 = std::to_string( - static_cast(std::numeric_limits::max()) + 1); - BEAST_EXPECT(maxInt64Plus1 == "9223372036854775808"); + auto const maxInt64Plus2 = std::to_string( + static_cast(std::numeric_limits::max()) + 2); + BEAST_EXPECT(maxInt64Plus2 == "9223372036854775809"); auto const initialXRP = to_string(kInitialXrp); BEAST_EXPECT(initialXRP == "100000000000000000"); @@ -5839,15 +5839,15 @@ class Vault_test : public beast::unit_test::Suite env(tx); env.close(); - tx[sfAssetsMaximum] = maxInt64Plus1; + tx[sfAssetsMaximum] = maxInt64Plus2; env(tx, Ter(tefEXCEPTION)); env.close(); // This value will be rounded - auto const insertAt = maxInt64Plus1.size() - 3; - auto const decimalTest = maxInt64Plus1.substr(0, insertAt) + "." + - maxInt64Plus1.substr(insertAt); // (max int64+1) / 1000 - BEAST_EXPECT(decimalTest == "9223372036854775.808"); + auto const insertAt = maxInt64Plus2.size() - 3; + auto const decimalTest = maxInt64Plus2.substr(0, insertAt) + "." + + maxInt64Plus2.substr(insertAt); // (max int64+1) / 1000 + BEAST_EXPECT(decimalTest == "9223372036854775.809"); tx[sfAssetsMaximum] = decimalTest; auto const newKeylet = keylet::vault(owner.id(), env.seq(owner)); env(tx); @@ -5891,15 +5891,15 @@ class Vault_test : public beast::unit_test::Suite env(tx); env.close(); - tx[sfAssetsMaximum] = maxInt64Plus1; + tx[sfAssetsMaximum] = maxInt64Plus2; env(tx, Ter(tefEXCEPTION)); env.close(); // This value will be rounded - auto const insertAt = maxInt64Plus1.size() - 1; - auto const decimalTest = maxInt64Plus1.substr(0, insertAt) + "." + - maxInt64Plus1.substr(insertAt); // (max int64+1) / 10 - BEAST_EXPECT(decimalTest == "922337203685477580.8"); + auto const insertAt = maxInt64Plus2.size() - 1; + auto const decimalTest = maxInt64Plus2.substr(0, insertAt) + "." + + maxInt64Plus2.substr(insertAt); // (max int64+1) / 10 + BEAST_EXPECT(decimalTest == "922337203685477580.9"); tx[sfAssetsMaximum] = decimalTest; auto const newKeylet = keylet::vault(owner.id(), env.seq(owner)); env(tx); @@ -5936,7 +5936,7 @@ class Vault_test : public beast::unit_test::Suite env(tx); env.close(); - tx[sfAssetsMaximum] = maxInt64Plus1; + tx[sfAssetsMaximum] = maxInt64Plus2; env(tx); env.close(); @@ -5948,10 +5948,10 @@ class Vault_test : public beast::unit_test::Suite // These values will be rounded to 15 significant digits { - auto const insertAt = maxInt64Plus1.size() - 1; - auto const decimalTest = maxInt64Plus1.substr(0, insertAt) + "." + - maxInt64Plus1.substr(insertAt); // (max int64+1) / 10 - BEAST_EXPECT(decimalTest == "922337203685477580.8"); + auto const insertAt = maxInt64Plus2.size() - 1; + auto const decimalTest = maxInt64Plus2.substr(0, insertAt) + "." + + maxInt64Plus2.substr(insertAt); // (max int64+1) / 10 + BEAST_EXPECT(decimalTest == "922337203685477580.9"); tx[sfAssetsMaximum] = decimalTest; auto const newKeylet = keylet::vault(owner.id(), env.seq(owner)); env(tx); From 12741a247a50f848afd855230996fe6c104548d8 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 8 Jun 2026 20:21:38 -0400 Subject: [PATCH 56/77] Cleanups, mostly from previous merge, plus a few outdated comments --- src/libxrpl/basics/Number.cpp | 41 +++++++++++++++++---------------- src/test/basics/Number_test.cpp | 8 +++---- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index ccba534009..05179be0d8 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -236,12 +236,12 @@ public: // Modify the result to the correctly rounded value void - doRound(rep& drops, MantissaRange::CuspRoundingFix cuspRoundingFix, std::string location); + doRound(rep& drops, std::string location); private: template void - pushOverflow(T const& mantissa, MantissaRange::CuspRoundingFix cuspRoundingFix); + pushOverflow(T const& mantissa); enum class Round { // The result is exact. No rounding is needed. Only used if cuspRoundingFix is enabled. @@ -347,7 +347,7 @@ Number::Guard::doDropDigit(uint128_t& mantissa, int& exponent) noexce template void -Number::Guard::pushOverflow(T const& mantissa, MantissaRange::CuspRoundingFix cuspRoundingFix) +Number::Guard::pushOverflow(T const& mantissa) { XRPL_ASSERT(mantissa <= kMaxRepUp, "xrpl::Number::Guard::doRoundUp : valid mantissa"); if (cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled && mantissa > kMaxRep && @@ -359,18 +359,23 @@ Number::Guard::pushOverflow(T const& mantissa, MantissaRange::CuspRoundingFix cu // case the number of mantissa bits ever changes. Effects: // * For round to nearest // * if the mantissa is below the midpoint, it'll round "down" to kMaxRepUp - // * if above the midpoint, it'll round "down" to kMaxRep + // * if above the midpoint, it'll round "up" to kMaxRepUp // * if can never be exactly at the midpoint, because kMaxRepUp is always even, and // kMaxRep is always odd, so don't worry about it. - // * For round upward, will round up to kMaxRepUp for positive values, down for negative. + // * 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. + // * For round toward zero, always rounds down to kMaxRep. auto constexpr spread = kMaxRepUp - kMaxRep; - static_assert(spread < 10 && spread >= 0); + static_assert(spread < 10 && spread > 0); + // This should absolutely be impossible + if constexpr (spread == 0) + return; // LCOV_EXCL_LINE auto const diff = mantissa - kMaxRep; auto const digit = (diff * 10) / spread; - XRPL_ASSERT(digit > 0 && digit < 10, "xrpld::Number::Guard::xxxx : valid overflow digit"); + XRPL_ASSERT( + digit > 0 && digit < 10, "xrpld::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); @@ -378,9 +383,10 @@ Number::Guard::pushOverflow(T const& mantissa, MantissaRange::CuspRoundingFix cu } // Returns: -// Down if Guard is less than half -// Even if Guard is exactly half -// Up 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 { @@ -451,7 +457,7 @@ template void Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string location) { - pushOverflow(mantissa, cuspRoundingFix); + pushOverflow(mantissa); auto const r = round(); if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1)) @@ -553,12 +559,9 @@ Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) // Modify the result to the correctly rounded value void -Number::Guard::doRound( - rep& drops, - MantissaRange::CuspRoundingFix cuspRoundingFix, - std::string location) +Number::Guard::doRound(rep& drops, std::string location) { - pushOverflow(drops, cuspRoundingFix); + pushOverflow(drops); auto r = round(); if (r == Round::Up || (r == Round::Even && (drops & 1) == 1)) @@ -1150,8 +1153,6 @@ Number::operator/=(Number const& y) Number:: operator rep() const { - auto const& range = kRange.get(); - rep drops = mantissa(); int offset = exponent(); Guard g(kRange); @@ -1172,7 +1173,7 @@ operator rep() const throw std::overflow_error("Number::operator rep() overflow"); drops *= 10; } - g.doRound(drops, range.cuspRoundingFix, "Number::operator rep() rounding overflow"); + g.doRound(drops, "Number::operator rep() rounding overflow"); } return drops; } diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 2bfda241d9..60ea7b638c 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -2082,7 +2082,7 @@ public: case MantissaRange::MantissaScale::Small: // With the small mantissa, everything rounds up - // Upward round UP + // Upward rounds UP BEAST_EXPECT(upward > above); // ToNearest rounds UP when the DOWN neighbor is strictly closer @@ -2118,14 +2118,14 @@ public: // Upward round UP BEAST_EXPECT(upward == above); - // ToNearest rounds UP when the DOWN neighbor is strictly closer + // ToNearest rounds to the strictly closer DOWN neighbor BEAST_EXPECT(toNearest != above); BEAST_EXPECT(toNearest == below); - // Downward undershoots: it returns a value below `below` + // Downward also rounds to `below` BEAST_EXPECT(downward == below); - // Both should have given the same answer, but they differ + // ToNearest rounds to downward BEAST_EXPECT(toNearest == downward); break; } From d9c63cb9bc15c9dcbe5926e2f06c5be55a7746ca Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 9 Jun 2026 16:43:24 -0400 Subject: [PATCH 57/77] Update to use a new amendment, since this PR will not be part of 3.2.0 - This requires creating yet another MantissaScale, and CuspRoundingFix option. --- include/xrpl/basics/Number.h | 30 ++++++++++------ include/xrpl/protocol/detail/features.macro | 2 ++ src/libxrpl/basics/Number.cpp | 38 ++++++++++++++------- src/libxrpl/protocol/Rules.cpp | 35 +++++++++++-------- src/libxrpl/protocol/STNumber.cpp | 5 +-- src/test/app/Invariants_test.cpp | 7 ++-- src/test/basics/Number_test.cpp | 21 ++++++------ 7 files changed, 83 insertions(+), 55 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 61b63fe234..7e2c8b06c9 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -130,13 +130,16 @@ struct MantissaRange final Small, // LargeLegacy can be removed when fixCleanup3_2_0 is retired LargeLegacy, - Large, + // Large3_2_0 can be removed when fixCleanup3_3_0 is retired + Large3_2_0, + LargeNew, // TODO: Convert this back to Large after conversion is done }; // This entire enum can be removed when fixCleanup3_2_0 is retired - enum class CuspRoundingFix : bool { - Disabled = false, - Enabled = true, + enum class CuspRoundingFix : std::uint8_t { + Disabled = 0, + Enabled3_2_0 = 1, + EnabledNew = 2, // TODO: Convert this back to Enabled after conversion is done }; explicit constexpr MantissaRange(MantissaScale sc) : scale(sc) @@ -164,7 +167,8 @@ private: case MantissaScale::Small: return 15; case MantissaScale::LargeLegacy: - case MantissaScale::Large: + case MantissaScale::Large3_2_0: + case MantissaScale::LargeNew: return 18; // LCOV_EXCL_START default: @@ -193,8 +197,10 @@ private: case MantissaScale::Small: case MantissaScale::LargeLegacy: return CuspRoundingFix::Disabled; - case MantissaScale::Large: - return CuspRoundingFix::Enabled; + case MantissaScale::Large3_2_0: + return CuspRoundingFix::Enabled3_2_0; + case MantissaScale::LargeNew: + return CuspRoundingFix::EnabledNew; default: // If called in a constexpr context, this throw assures that the build fails if an // invalid scale is used. @@ -867,11 +873,13 @@ to_string(MantissaRange::MantissaScale const& scale) switch (scale) { case MantissaRange::MantissaScale::Small: - return "small"; + return "Small"; case MantissaRange::MantissaScale::LargeLegacy: - return "largeLegacy"; - case MantissaRange::MantissaScale::Large: - return "large"; + return "LargeLegacy"; + case MantissaRange::MantissaScale::Large3_2_0: + return "Large320"; + case MantissaRange::MantissaScale::LargeNew: + return "Large"; default: throw std::runtime_error("Bad scale"); } diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index c99c1e5ce8..f83442926d 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -15,6 +15,8 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. +// The name "NumberStuff" is a placeholder +XRPL_FIX (NumberStuff, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (Cleanup3_2_0, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(MPTokensV2, Supported::No, VoteBehavior::DefaultNo) XRPL_FIX (Cleanup3_1_3, Supported::Yes, VoteBehavior::DefaultYes) diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 9386b411d0..8253e6c6ee 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -31,7 +31,7 @@ namespace xrpl { thread_local Number::RoundingMode Number::mode = Number::RoundingMode::ToNearest; thread_local std::reference_wrapper Number::kRange = - MantissaRange::getMantissaRange(MantissaRange::MantissaScale::Large); + MantissaRange::getMantissaRange(MantissaRange::MantissaScale::LargeNew); std::set const& MantissaRange::getAllScales() @@ -39,7 +39,8 @@ MantissaRange::getAllScales() static std::set const kScales = { MantissaRange::MantissaScale::Small, MantissaRange::MantissaScale::LargeLegacy, - MantissaRange::MantissaScale::Large, + MantissaRange::MantissaScale::Large3_2_0, + MantissaRange::MantissaScale::LargeNew, }; return kScales; } @@ -80,14 +81,25 @@ MantissaRange::getRanges() } { [[maybe_unused]] - constexpr static MantissaRange kRange{MantissaRange::MantissaScale::Large}; + constexpr static MantissaRange kRange{MantissaRange::MantissaScale::Large3_2_0}; static_assert(isPowerOfTen(kRange.min)); static_assert(kRange.min == 1'000'000'000'000'000'000ULL); static_assert(kRange.max == rep(9'999'999'999'999'999'999ULL)); static_assert(kRange.log == 18); static_assert(kRange.min < Number::kMaxRep); static_assert(kRange.max > Number::kMaxRep); - static_assert(kRange.cuspRoundingFix == CuspRoundingFix::Enabled); + static_assert(kRange.cuspRoundingFix == CuspRoundingFix::Enabled3_2_0); + } + { + [[maybe_unused]] + constexpr static MantissaRange kRange{MantissaRange::MantissaScale::LargeNew}; + static_assert(isPowerOfTen(kRange.min)); + static_assert(kRange.min == 1'000'000'000'000'000'000ULL); + static_assert(kRange.max == rep(9'999'999'999'999'999'999ULL)); + static_assert(kRange.log == 18); + static_assert(kRange.min < Number::kMaxRep); + static_assert(kRange.max > Number::kMaxRep); + static_assert(kRange.cuspRoundingFix == CuspRoundingFix::EnabledNew); } return map; }(); @@ -225,7 +237,7 @@ public: doDropDigit(T& mantissa, int& exponent) noexcept; enum class Round { - // The result is exact. No rounding is needed. Only used if cuspRoundingFix is enabled. + // The result is exact. No rounding is needed. Only used if cuspRoundingFix is EnabledNew. Exact = -2, // Round down. Since we use integer math, that usually means no change is needed. // Exceptions are for when the result is between kMaxRap and kMaxRepUp (round to kMaxRep), @@ -234,7 +246,8 @@ public: 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) + // Round up. Always adds 1 (or subtracts 1 in some cases if cuspRoundingFix is not + // EnabledNew) Up = 1, }; @@ -350,7 +363,7 @@ Number::Guard::round() const noexcept { auto mode = Number::getround(); - if (cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled && empty()) + if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::EnabledNew && empty()) { // No remainder return Round::Exact; @@ -419,7 +432,7 @@ Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string auto const safeToIncrement = [this](auto const& mantissa) { return mantissa < maxMantissa && mantissa < kMaxRep; }; - if (cuspRoundingFix == MantissaRange::CuspRoundingFix::Enabled) + if (cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled) { // Ensure mantissa after incrementing fits within both the // min/maxMantissa range and is a valid "rep". @@ -469,7 +482,7 @@ void Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) { auto r = round(); - if (cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled) + if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::EnabledNew) { // If there was any remainder, subtract 1 from the result. This is sufficient to get the // best rounding. @@ -801,7 +814,7 @@ Number::operator+=(Number const& y) xe = ye; xn = yn; } - if (cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled) + if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::EnabledNew) { // Grow xm/xe and pull digits out of the Guard until it's a little bit larger than // maxMantissa, so that normalize will have enough information to make an accurate @@ -836,6 +849,7 @@ Number::operator+=(Number const& y) negative_ = xn; mantissa_ = static_cast(xm); exponent_ = xe; + XRPL_ASSERT(isnormal(), "xrpl::Number::operator+= : result is normal"); return *this; } @@ -971,7 +985,7 @@ Number::operator/=(Number const& y) // This is equivalent to if we had used an initial factor of 10^22, // a couple digits more than we actually need. // - // Stage 3: If there is still a remainder, and the CuspRoundingFix + // Stage 3: If there is still a remainder, and the cuspRoundingFix // is enabled, pass a flag indicating such to doNormalize. The Guard // in doNormalize will treat that flag as if non-zero digits had // been dropped from the mantissa when shrinking it into range. @@ -1055,7 +1069,7 @@ Number::operator/=(Number const& y) // rounding fix is enabled, flag if there is still // a remainder from stage 2. bool const useTrailingRemainder = - cuspRoundingFix == MantissaRange::CuspRoundingFix::Enabled; + cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled; if (useTrailingRemainder) { dropped = partialNumerator % dm != 0; diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index 08a95145eb..06f50423b8 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -40,22 +40,29 @@ setCurrentTransactionRules(std::optional r) // Push the appropriate setting, instead of having the class pull every time // the value is needed. That could get expensive fast. - // If any new conditions with new amendments are added, those amendments must also be added to - // useRulesGuards. - bool const enableVaultNumbers = - !r || (r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol)); - bool const enableCuspRoundingFix = !r || r->enabled(fixCleanup3_2_0); - XRPL_ASSERT( - !r || useRulesGuards(*r) == (enableCuspRoundingFix || enableVaultNumbers), - "setCurrentTransactionRules : rule decisions match"); - // Declare the range this way to keep clang-tidy from complaining - auto const range = [enableCuspRoundingFix, enableVaultNumbers]() { + auto const range = [&r]() { + // If any new conditions with new amendments are added, those amendments must also be added + // to useRulesGuards. + bool const enableVaultNumbers = + !r || (r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol)); + bool const enableCuspRounding3_2_0 = !r || r->enabled(fixCleanup3_2_0); + bool const enableCuspRounding3_3_0 = !r || r->enabled(fixNumberStuff); + XRPL_ASSERT( + !r || + useRulesGuards(*r) == + (enableVaultNumbers || enableCuspRounding3_2_0 || enableCuspRounding3_3_0), + "setCurrentTransactionRules : rule decisions match"); + if (enableVaultNumbers) { - if (enableCuspRoundingFix) + if (enableCuspRounding3_3_0) { - return MantissaRange::MantissaScale::Large; + return MantissaRange::MantissaScale::LargeNew; + } + if (enableCuspRounding3_2_0) + { + return MantissaRange::MantissaScale::Large3_2_0; } return MantissaRange::MantissaScale::LargeLegacy; } @@ -76,8 +83,8 @@ useRulesGuards(Rules const& rules) // As soon as any one of these amendments is retired, this whole function can be removed, along // with createGuards, and any other callers, and the first set of guards can be created directly // at the call site, without using optional. - return rules.enabled(fixCleanup3_2_0) || rules.enabled(featureSingleAssetVault) || - rules.enabled(featureLendingProtocol); + return rules.enabled(featureSingleAssetVault) || rules.enabled(featureLendingProtocol) || + rules.enabled(fixCleanup3_2_0) || rules.enabled(fixNumberStuff); } void diff --git a/src/libxrpl/protocol/STNumber.cpp b/src/libxrpl/protocol/STNumber.cpp index 8ef7b9760f..11cde05da2 100644 --- a/src/libxrpl/protocol/STNumber.cpp +++ b/src/libxrpl/protocol/STNumber.cpp @@ -88,7 +88,6 @@ STNumber::add(Serializer& s) const } else { -#if !NDEBUG // There are circumstances where an already-rounded Number is // serialized without being touched by a transactor, and thus // without an asset. We can't know if it's rounded, because it could @@ -96,11 +95,9 @@ STNumber::add(Serializer& s) const // Json. Regardless, the only time we should be serializing an // STNumber is when the scale is large. XRPL_ASSERT_PARTS( - Number::getMantissaScale() == MantissaRange::MantissaScale::LargeLegacy || - Number::getMantissaScale() == MantissaRange::MantissaScale::Large, + Number::getMantissaScale() != MantissaRange::MantissaScale::Small, "xrpl::STNumber::add", "STNumber only used with large mantissa scale"); -#endif } } diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 3654036869..ebc499229d 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -4767,11 +4767,10 @@ class Invariants_test : public beast::unit_test::Suite std::vector values; }; - for (auto const mantissaScale : { - MantissaRange::MantissaScale::LargeLegacy, - MantissaRange::MantissaScale::Large, - }) + for (auto const mantissaScale : MantissaRange::getAllScales()) { + if (mantissaScale == MantissaRange::MantissaScale::Small) + continue; NumberMantissaScaleGuard const g{mantissaScale}; auto makeDelta = [&vaultAsset](Number const& n) -> ValidVault::DeltaInfo { diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index d2e9ac2638..ad55fd4b94 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -1397,8 +1397,7 @@ public: "9223372036854775e3"); } break; - case MantissaRange::MantissaScale::LargeLegacy: - case MantissaRange::MantissaScale::Large: + default: // Test the edges // ((exponent < -(28)) || (exponent > -(8))))) test(Number::min(), "1e-32750"); @@ -1436,9 +1435,6 @@ public: test( -(Number{std::numeric_limits::max(), 0} + 1), "-9223372036854775810"); - break; - default: - BEAST_EXPECT(false); } } @@ -1721,7 +1717,8 @@ public: switch (scale) { - case MantissaRange::MantissaScale::Large: + case MantissaRange::MantissaScale::Large3_2_0: + case MantissaRange::MantissaScale::LargeNew: BEAST_EXPECT(signedDifference >= 0); BEAST_EXPECT(signedDifference < pow10(product.exponent())); BEAST_EXPECT( @@ -1803,7 +1800,8 @@ public: // Upward invariant: stored >= exact. Bug: stored < exact. switch (scale) { - case MantissaRange::MantissaScale::Large: + case MantissaRange::MantissaScale::Large3_2_0: + case MantissaRange::MantissaScale::LargeNew: BEAST_EXPECT(stored >= exact); BEAST_EXPECT(diff < pow10(quotient.exponent())); break; @@ -1853,7 +1851,8 @@ public: // invariant: stored <= exact. Bug: stored > exact. switch (scale) { - case MantissaRange::MantissaScale::Large: + case MantissaRange::MantissaScale::Large3_2_0: + case MantissaRange::MantissaScale::LargeNew: BEAST_EXPECT(stored <= exact); BEAST_EXPECT(diff > -pow10(quotient.exponent())); break; @@ -1910,7 +1909,8 @@ public: // invariant: stored >= exact. Bug: stored < exact. switch (scale) { - case MantissaRange::MantissaScale::Large: + case MantissaRange::MantissaScale::Large3_2_0: + case MantissaRange::MantissaScale::LargeNew: BEAST_EXPECT(stored >= exact); BEAST_EXPECT(diff < pow10(quotient.exponent())); break; @@ -2012,7 +2012,8 @@ public: switch (scale) { case MantissaRange::MantissaScale::Small: - case MantissaRange::MantissaScale::LargeLegacy: { + case MantissaRange::MantissaScale::LargeLegacy: + case MantissaRange::MantissaScale::Large3_2_0: { // Without the fix, all the results but one round up if (r == Number::RoundingMode::Downward) { From 63f1a40b7784eb5fb3edfb9d9bd77d1f42f93231 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 9 Jun 2026 17:06:27 -0400 Subject: [PATCH 58/77] Clean up the "New" names --- include/xrpl/basics/Number.h | 12 ++++++------ src/libxrpl/basics/Number.cpp | 18 +++++++++--------- src/libxrpl/protocol/Rules.cpp | 2 +- src/test/basics/Number_test.cpp | 8 ++++---- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index fed79b78fe..3ecfa64a02 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -132,14 +132,14 @@ struct MantissaRange final LargeLegacy, // Large3_2_0 can be removed when fixCleanup3_3_0 is retired Large3_2_0, - LargeNew, // TODO: Convert this back to Large after conversion is done + Large, }; // This entire enum can be removed when fixCleanup3_2_0 is retired enum class CuspRoundingFix : std::uint8_t { Disabled = 0, Enabled3_2_0 = 1, - EnabledNew = 2, // TODO: Convert this back to Enabled after conversion is done + Enabled = 2, }; explicit constexpr MantissaRange(MantissaScale sc) : scale(sc) @@ -168,7 +168,7 @@ private: return 15; case MantissaScale::LargeLegacy: case MantissaScale::Large3_2_0: - case MantissaScale::LargeNew: + case MantissaScale::Large: return 18; // LCOV_EXCL_START default: @@ -199,8 +199,8 @@ private: return CuspRoundingFix::Disabled; case MantissaScale::Large3_2_0: return CuspRoundingFix::Enabled3_2_0; - case MantissaScale::LargeNew: - return CuspRoundingFix::EnabledNew; + case MantissaScale::Large: + return CuspRoundingFix::Enabled; default: // If called in a constexpr context, this throw assures that the build fails if an // invalid scale is used. @@ -885,7 +885,7 @@ to_string(MantissaRange::MantissaScale const& scale) return "LargeLegacy"; case MantissaRange::MantissaScale::Large3_2_0: return "Large320"; - case MantissaRange::MantissaScale::LargeNew: + case MantissaRange::MantissaScale::Large: return "Large"; default: throw std::runtime_error("Bad scale"); diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 8253e6c6ee..234ee4e40c 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -31,7 +31,7 @@ namespace xrpl { thread_local Number::RoundingMode Number::mode = Number::RoundingMode::ToNearest; thread_local std::reference_wrapper Number::kRange = - MantissaRange::getMantissaRange(MantissaRange::MantissaScale::LargeNew); + MantissaRange::getMantissaRange(MantissaRange::MantissaScale::Large); std::set const& MantissaRange::getAllScales() @@ -40,7 +40,7 @@ MantissaRange::getAllScales() MantissaRange::MantissaScale::Small, MantissaRange::MantissaScale::LargeLegacy, MantissaRange::MantissaScale::Large3_2_0, - MantissaRange::MantissaScale::LargeNew, + MantissaRange::MantissaScale::Large, }; return kScales; } @@ -92,14 +92,14 @@ MantissaRange::getRanges() } { [[maybe_unused]] - constexpr static MantissaRange kRange{MantissaRange::MantissaScale::LargeNew}; + constexpr static MantissaRange kRange{MantissaRange::MantissaScale::Large}; static_assert(isPowerOfTen(kRange.min)); static_assert(kRange.min == 1'000'000'000'000'000'000ULL); static_assert(kRange.max == rep(9'999'999'999'999'999'999ULL)); static_assert(kRange.log == 18); static_assert(kRange.min < Number::kMaxRep); static_assert(kRange.max > Number::kMaxRep); - static_assert(kRange.cuspRoundingFix == CuspRoundingFix::EnabledNew); + static_assert(kRange.cuspRoundingFix == CuspRoundingFix::Enabled); } return map; }(); @@ -237,7 +237,7 @@ public: doDropDigit(T& mantissa, int& exponent) noexcept; enum class Round { - // The result is exact. No rounding is needed. Only used if cuspRoundingFix is EnabledNew. + // The result is exact. No rounding is needed. Only used if cuspRoundingFix is Enabled. Exact = -2, // Round down. Since we use integer math, that usually means no change is needed. // Exceptions are for when the result is between kMaxRap and kMaxRepUp (round to kMaxRep), @@ -247,7 +247,7 @@ public: // 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 - // EnabledNew) + // Enabled) Up = 1, }; @@ -363,7 +363,7 @@ Number::Guard::round() const noexcept { auto mode = Number::getround(); - if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::EnabledNew && empty()) + if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled && empty()) { // No remainder return Round::Exact; @@ -482,7 +482,7 @@ void Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) { auto r = round(); - if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::EnabledNew) + if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled) { // If there was any remainder, subtract 1 from the result. This is sufficient to get the // best rounding. @@ -814,7 +814,7 @@ Number::operator+=(Number const& y) xe = ye; xn = yn; } - if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::EnabledNew) + if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled) { // Grow xm/xe and pull digits out of the Guard until it's a little bit larger than // maxMantissa, so that normalize will have enough information to make an accurate diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index 06f50423b8..c7e9431f81 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -58,7 +58,7 @@ setCurrentTransactionRules(std::optional r) { if (enableCuspRounding3_3_0) { - return MantissaRange::MantissaScale::LargeNew; + return MantissaRange::MantissaScale::Large; } if (enableCuspRounding3_2_0) { diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 24c515af1c..7121948d69 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -1813,7 +1813,7 @@ public: switch (scale) { case MantissaRange::MantissaScale::Large3_2_0: - case MantissaRange::MantissaScale::LargeNew: + case MantissaRange::MantissaScale::Large: BEAST_EXPECT(signedDifference >= 0); BEAST_EXPECT(signedDifference < pow10(product.exponent())); BEAST_EXPECT( @@ -1896,7 +1896,7 @@ public: switch (scale) { case MantissaRange::MantissaScale::Large3_2_0: - case MantissaRange::MantissaScale::LargeNew: + case MantissaRange::MantissaScale::Large: BEAST_EXPECT(stored >= exact); BEAST_EXPECT(diff < pow10(quotient.exponent())); break; @@ -1947,7 +1947,7 @@ public: switch (scale) { case MantissaRange::MantissaScale::Large3_2_0: - case MantissaRange::MantissaScale::LargeNew: + case MantissaRange::MantissaScale::Large: BEAST_EXPECT(stored <= exact); BEAST_EXPECT(diff > -pow10(quotient.exponent())); break; @@ -2005,7 +2005,7 @@ public: switch (scale) { case MantissaRange::MantissaScale::Large3_2_0: - case MantissaRange::MantissaScale::LargeNew: + case MantissaRange::MantissaScale::Large: BEAST_EXPECT(stored >= exact); BEAST_EXPECT(diff < pow10(quotient.exponent())); break; From 4139ebbe6b82219a0cdd94991b3de349184cce51 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 9 Jun 2026 18:31:59 -0400 Subject: [PATCH 59/77] Fix issues introduced by prior merge, and new MantissaRange --- src/libxrpl/basics/Number.cpp | 6 +- src/test/basics/Number_test.cpp | 173 ++++++++++++++++---------------- 2 files changed, 92 insertions(+), 87 deletions(-) diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 0074e871b4..aff0e219e6 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -494,7 +494,8 @@ Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string // 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. - if (mantissa > kMaxRep && mantissa < kMaxRepUp) + if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled && + mantissa > kMaxRep && mantissa < kMaxRepUp) { mantissa = kMaxRepUp; } @@ -964,9 +965,8 @@ Number::operator*=(Number const& y) g.setNegative(); auto const& maxMantissa = g.maxMantissa; - auto const cuspRoundingFix = g.cuspRoundingFix; auto const repLimit = - cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled ? kMaxRepUp : kMaxRep; + g.cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled ? kMaxRepUp : kMaxRep; while (zm > maxMantissa || zm > repLimit) { diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 41a12fcf5f..0c044979e6 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -484,6 +484,7 @@ public: test(cSmall); break; case MantissaRange::MantissaScale::LargeLegacy: + case MantissaRange::MantissaScale::Large3_2_0: test(cLargeAll); test(cLargeLegacy); break; @@ -1545,8 +1546,10 @@ public: __LINE__); break; } - // Rounding to nearest, since the mantissa is above the halfway point from kMaxRep - // to kMaxRep up, it will be rounded up to kMaxRepUp. + // Rounding to nearest, will be rounded up to kMaxRepUp, but for different reasons + // depending on the scale. If older than "Large", it rounds up for the same reason + // "+1" rounds up. For "Large", since the mantissa is above the halfway point from + // kMaxRep to kMaxRepUp, it will be rounded up to kMaxRepUp. test( Number{std::numeric_limits::max(), 0} + 2, "9223372036854775810", @@ -2138,88 +2141,6 @@ public: } } - { - testcase << "normalization cusp: ToNearest and Downward disagree " << to_string(scale); - - constexpr auto kMaxRep = Number::kMaxRep; - - // Both ToNearest and Downward should round to `below` - auto const actual = static_cast(kMaxRep) + 1; - Number const below{static_cast(kMaxRep), 0}; - Number const above{ - false, static_cast(kMaxRep) + 3, 0, Number::Unchecked{}}; - - auto construct = [](Number::RoundingMode mode) { - NumberRoundModeGuard const roundGuard{mode}; - return Number(false, actual, 0, Number::Normalized{}); - }; - Number const upward = construct(Number::RoundingMode::Upward); - - Number const toNearest = construct(Number::RoundingMode::ToNearest); - - Number const downward = construct(Number::RoundingMode::Downward); - - log << " actual = " << actual << " (kMaxRep + 1)\n" - << " below = " << below << " (kMaxRep, distance 1)\n" - << " above = " << above << " (kMaxRep + 3, distance 2)\n" - << " Upward = " << upward << "\n" - << " ToNearest = " << toNearest << "\n" - << " Downward = " << downward << "\n\n"; - log.flush(); - - switch (scale) - { - case MantissaRange::MantissaScale::Small: - // With the small mantissa, everything rounds up - - // Upward rounds UP - BEAST_EXPECT(upward > above); - - // ToNearest rounds UP when the DOWN neighbor is strictly closer - BEAST_EXPECT(toNearest > above); - BEAST_EXPECT(toNearest == below); - - // Downward undershoots: it returns a value below `below` - BEAST_EXPECT(downward < below); - - // Both should have given the same answer, but they differ - BEAST_EXPECT(toNearest > downward); - - break; - - case MantissaRange::MantissaScale::LargeLegacy: - // Upward round UP - BEAST_EXPECT(upward == above); - - // ToNearest rounds UP when the DOWN neighbor is strictly closer - BEAST_EXPECT(toNearest == above); - BEAST_EXPECT(toNearest > below); - - // Downward undershoots: it returns a value below `below` - BEAST_EXPECT(downward < below); - - // Both should have given the same answer, but they differ - BEAST_EXPECT(toNearest > downward); - - break; - default: - // Covers "Large" and any newly added scales - - // Upward round UP - BEAST_EXPECT(upward == above); - - // ToNearest rounds to the strictly closer DOWN neighbor - BEAST_EXPECT(toNearest != above); - BEAST_EXPECT(toNearest == below); - - // Downward also rounds to `below` - BEAST_EXPECT(downward == below); - - // ToNearest rounds to downward - BEAST_EXPECT(toNearest == downward); - break; - } - } { auto const exp = Number::mantissaLog(); // SubCase is @@ -2343,6 +2264,90 @@ public: } } } + + { + testcase << "normalization cusp: ToNearest and Downward disagree " << to_string(scale); + + constexpr auto kMaxRep = Number::kMaxRep; + + // Both ToNearest and Downward should round to `below` + auto const actual = static_cast(kMaxRep) + 1; + Number const below{static_cast(kMaxRep), 0}; + Number const above{ + false, static_cast(kMaxRep) + 3, 0, Number::Unchecked{}}; + + auto construct = [](Number::RoundingMode mode) { + NumberRoundModeGuard const roundGuard{mode}; + return Number(false, actual, 0, Number::Normalized{}); + }; + Number const upward = construct(Number::RoundingMode::Upward); + + Number const toNearest = construct(Number::RoundingMode::ToNearest); + + Number const downward = construct(Number::RoundingMode::Downward); + + log << " actual = " << actual << " (kMaxRep + 1)\n" + << " below = " << below << " (kMaxRep, distance 1)\n" + << " above = " << above << " (kMaxRep + 3, distance 2)\n" + << " Upward = " << upward << "\n" + << " ToNearest = " << toNearest << "\n" + << " Downward = " << downward << "\n\n"; + log.flush(); + + switch (scale) + { + case MantissaRange::MantissaScale::Small: + // With the small mantissa, everything rounds up + + // Upward rounds UP + BEAST_EXPECT(upward > above); + + // ToNearest rounds UP when the DOWN neighbor is strictly closer + BEAST_EXPECT(toNearest > above); + BEAST_EXPECT(toNearest == below); + + // Downward undershoots: it returns a value below `below` + BEAST_EXPECT(downward < below); + + // Both should have given the same answer, but they differ + BEAST_EXPECT(toNearest > downward); + + break; + + case MantissaRange::MantissaScale::LargeLegacy: + case MantissaRange::MantissaScale::Large3_2_0: + // Upward round UP + BEAST_EXPECT(upward == above); + + // ToNearest rounds UP when the DOWN neighbor is strictly closer + BEAST_EXPECT(toNearest == above); + BEAST_EXPECT(toNearest > below); + + // Downward undershoots: it returns a value below `below` + BEAST_EXPECT(downward < below); + + // Both should have given the same answer, but they differ + BEAST_EXPECT(toNearest > downward); + + break; + default: + // Covers "Large" and any newly added scales + + // Upward round UP + BEAST_EXPECT(upward == above); + + // ToNearest rounds to the strictly closer DOWN neighbor + BEAST_EXPECT(toNearest != above); + BEAST_EXPECT(toNearest == below); + + // Downward also rounds to `below` + BEAST_EXPECT(downward == below); + + // ToNearest rounds to downward + BEAST_EXPECT(toNearest == downward); + break; + } + } } void From 66ff72f56d0d3bae9724a11fb8783a8f996e5b3a Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 9 Jun 2026 18:38:03 -0400 Subject: [PATCH 60/77] clang-tidy: rename MantissaScale enums from "3_2_0" to "320" --- include/xrpl/basics/Number.h | 14 +++++++------- src/libxrpl/basics/Number.cpp | 6 +++--- src/libxrpl/protocol/Rules.cpp | 2 +- src/test/basics/Number_test.cpp | 10 +++++----- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 3ecfa64a02..cb04dd9716 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -130,15 +130,15 @@ struct MantissaRange final Small, // LargeLegacy can be removed when fixCleanup3_2_0 is retired LargeLegacy, - // Large3_2_0 can be removed when fixCleanup3_3_0 is retired - Large3_2_0, + // Large320 can be removed when fixCleanup3_3_0 is retired + Large320, Large, }; // This entire enum can be removed when fixCleanup3_2_0 is retired enum class CuspRoundingFix : std::uint8_t { Disabled = 0, - Enabled3_2_0 = 1, + Enabled320 = 1, Enabled = 2, }; @@ -167,7 +167,7 @@ private: case MantissaScale::Small: return 15; case MantissaScale::LargeLegacy: - case MantissaScale::Large3_2_0: + case MantissaScale::Large320: case MantissaScale::Large: return 18; // LCOV_EXCL_START @@ -197,8 +197,8 @@ private: case MantissaScale::Small: case MantissaScale::LargeLegacy: return CuspRoundingFix::Disabled; - case MantissaScale::Large3_2_0: - return CuspRoundingFix::Enabled3_2_0; + case MantissaScale::Large320: + return CuspRoundingFix::Enabled320; case MantissaScale::Large: return CuspRoundingFix::Enabled; default: @@ -883,7 +883,7 @@ to_string(MantissaRange::MantissaScale const& scale) return "Small"; case MantissaRange::MantissaScale::LargeLegacy: return "LargeLegacy"; - case MantissaRange::MantissaScale::Large3_2_0: + case MantissaRange::MantissaScale::Large320: return "Large320"; case MantissaRange::MantissaScale::Large: return "Large"; diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 234ee4e40c..133ec82692 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -39,7 +39,7 @@ MantissaRange::getAllScales() static std::set const kScales = { MantissaRange::MantissaScale::Small, MantissaRange::MantissaScale::LargeLegacy, - MantissaRange::MantissaScale::Large3_2_0, + MantissaRange::MantissaScale::Large320, MantissaRange::MantissaScale::Large, }; return kScales; @@ -81,14 +81,14 @@ MantissaRange::getRanges() } { [[maybe_unused]] - constexpr static MantissaRange kRange{MantissaRange::MantissaScale::Large3_2_0}; + constexpr static MantissaRange kRange{MantissaRange::MantissaScale::Large320}; static_assert(isPowerOfTen(kRange.min)); static_assert(kRange.min == 1'000'000'000'000'000'000ULL); static_assert(kRange.max == rep(9'999'999'999'999'999'999ULL)); static_assert(kRange.log == 18); static_assert(kRange.min < Number::kMaxRep); static_assert(kRange.max > Number::kMaxRep); - static_assert(kRange.cuspRoundingFix == CuspRoundingFix::Enabled3_2_0); + static_assert(kRange.cuspRoundingFix == CuspRoundingFix::Enabled320); } { [[maybe_unused]] diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index c7e9431f81..15136115c7 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -62,7 +62,7 @@ setCurrentTransactionRules(std::optional r) } if (enableCuspRounding3_2_0) { - return MantissaRange::MantissaScale::Large3_2_0; + return MantissaRange::MantissaScale::Large320; } return MantissaRange::MantissaScale::LargeLegacy; } diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 7121948d69..266bc9bc1e 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -1812,7 +1812,7 @@ public: switch (scale) { - case MantissaRange::MantissaScale::Large3_2_0: + case MantissaRange::MantissaScale::Large320: case MantissaRange::MantissaScale::Large: BEAST_EXPECT(signedDifference >= 0); BEAST_EXPECT(signedDifference < pow10(product.exponent())); @@ -1895,7 +1895,7 @@ public: // Upward invariant: stored >= exact. Bug: stored < exact. switch (scale) { - case MantissaRange::MantissaScale::Large3_2_0: + case MantissaRange::MantissaScale::Large320: case MantissaRange::MantissaScale::Large: BEAST_EXPECT(stored >= exact); BEAST_EXPECT(diff < pow10(quotient.exponent())); @@ -1946,7 +1946,7 @@ public: // invariant: stored <= exact. Bug: stored > exact. switch (scale) { - case MantissaRange::MantissaScale::Large3_2_0: + case MantissaRange::MantissaScale::Large320: case MantissaRange::MantissaScale::Large: BEAST_EXPECT(stored <= exact); BEAST_EXPECT(diff > -pow10(quotient.exponent())); @@ -2004,7 +2004,7 @@ public: // invariant: stored >= exact. Bug: stored < exact. switch (scale) { - case MantissaRange::MantissaScale::Large3_2_0: + case MantissaRange::MantissaScale::Large320: case MantissaRange::MantissaScale::Large: BEAST_EXPECT(stored >= exact); BEAST_EXPECT(diff < pow10(quotient.exponent())); @@ -2108,7 +2108,7 @@ public: { case MantissaRange::MantissaScale::Small: case MantissaRange::MantissaScale::LargeLegacy: - case MantissaRange::MantissaScale::Large3_2_0: { + case MantissaRange::MantissaScale::Large320: { // Without the fix, all the results but one round up if (r == Number::RoundingMode::Downward) { From 48d1c70b6c97ac6198d85398296c64fee15d7bba Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 9 Jun 2026 18:44:46 -0400 Subject: [PATCH 61/77] Future proofing: Rename Large and Enabled to Large330 and Enabled330 - If more fixes need to be made in the future, they can be added after, instead of needing to do the "rename dance", I had to do with this PR. --- include/xrpl/basics/Number.h | 50 ++++++------------------------- src/libxrpl/basics/Number.cpp | 52 ++++++++++++++++++++++++++++----- src/libxrpl/protocol/Rules.cpp | 2 +- src/test/basics/Number_test.cpp | 8 ++--- 4 files changed, 59 insertions(+), 53 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index cb04dd9716..ccd448887a 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -132,14 +132,14 @@ struct MantissaRange final LargeLegacy, // Large320 can be removed when fixCleanup3_3_0 is retired Large320, - Large, + Large330, }; // This entire enum can be removed when fixCleanup3_2_0 is retired enum class CuspRoundingFix : std::uint8_t { Disabled = 0, Enabled320 = 1, - Enabled = 2, + Enabled330 = 2, }; explicit constexpr MantissaRange(MantissaScale sc) : scale(sc) @@ -168,7 +168,7 @@ private: return 15; case MantissaScale::LargeLegacy: case MantissaScale::Large320: - case MantissaScale::Large: + case MantissaScale::Large330: return 18; // LCOV_EXCL_START default: @@ -199,8 +199,8 @@ private: return CuspRoundingFix::Disabled; case MantissaScale::Large320: return CuspRoundingFix::Enabled320; - case MantissaScale::Large: - return CuspRoundingFix::Enabled; + case MantissaScale::Large330: + return CuspRoundingFix::Enabled330; default: // If called in a constexpr context, this throw assures that the build fails if an // invalid scale is used. @@ -874,43 +874,11 @@ squelch(Number const& x, Number const& limit) noexcept return x; } -inline std::string -to_string(MantissaRange::MantissaScale const& scale) -{ - switch (scale) - { - case MantissaRange::MantissaScale::Small: - return "Small"; - case MantissaRange::MantissaScale::LargeLegacy: - return "LargeLegacy"; - case MantissaRange::MantissaScale::Large320: - return "Large320"; - case MantissaRange::MantissaScale::Large: - return "Large"; - default: - throw std::runtime_error("Bad scale"); - } -} +std::string +to_string(MantissaRange::MantissaScale const& scale); -inline std::string -to_string(Number::RoundingMode const& round) -{ - switch (round) - { - enum class RoundingMode { ToNearest, TowardsZero, Downward, Upward }; - - case Number::RoundingMode::ToNearest: - return "ToNearest"; - case Number::RoundingMode::TowardsZero: - return "TowardsZero"; - case Number::RoundingMode::Downward: - return "Downward"; - case Number::RoundingMode::Upward: - return "Upward"; - default: - throw std::runtime_error("Bad rounding mode"); - } -} +std::string +to_string(Number::RoundingMode const& round); class SaveNumberRoundMode { diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 133ec82692..c356c51722 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -31,7 +31,45 @@ namespace xrpl { thread_local Number::RoundingMode Number::mode = Number::RoundingMode::ToNearest; thread_local std::reference_wrapper Number::kRange = - MantissaRange::getMantissaRange(MantissaRange::MantissaScale::Large); + MantissaRange::getMantissaRange(MantissaRange::MantissaScale::Large330); + +std::string +to_string(MantissaRange::MantissaScale const& scale) +{ + switch (scale) + { + case MantissaRange::MantissaScale::Small: + return "Small"; + case MantissaRange::MantissaScale::LargeLegacy: + return "LargeLegacy"; + case MantissaRange::MantissaScale::Large320: + return "Large320"; + case MantissaRange::MantissaScale::Large330: + return "Large330"; + default: + throw std::runtime_error("Bad scale"); + } +} + +std::string +to_string(Number::RoundingMode const& round) +{ + switch (round) + { + enum class RoundingMode { ToNearest, TowardsZero, Downward, Upward }; + + case Number::RoundingMode::ToNearest: + return "ToNearest"; + case Number::RoundingMode::TowardsZero: + return "TowardsZero"; + case Number::RoundingMode::Downward: + return "Downward"; + case Number::RoundingMode::Upward: + return "Upward"; + default: + throw std::runtime_error("Bad rounding mode"); + } +} std::set const& MantissaRange::getAllScales() @@ -40,7 +78,7 @@ MantissaRange::getAllScales() MantissaRange::MantissaScale::Small, MantissaRange::MantissaScale::LargeLegacy, MantissaRange::MantissaScale::Large320, - MantissaRange::MantissaScale::Large, + MantissaRange::MantissaScale::Large330, }; return kScales; } @@ -92,14 +130,14 @@ MantissaRange::getRanges() } { [[maybe_unused]] - constexpr static MantissaRange kRange{MantissaRange::MantissaScale::Large}; + constexpr static MantissaRange kRange{MantissaRange::MantissaScale::Large330}; static_assert(isPowerOfTen(kRange.min)); static_assert(kRange.min == 1'000'000'000'000'000'000ULL); static_assert(kRange.max == rep(9'999'999'999'999'999'999ULL)); static_assert(kRange.log == 18); static_assert(kRange.min < Number::kMaxRep); static_assert(kRange.max > Number::kMaxRep); - static_assert(kRange.cuspRoundingFix == CuspRoundingFix::Enabled); + static_assert(kRange.cuspRoundingFix == CuspRoundingFix::Enabled330); } return map; }(); @@ -363,7 +401,7 @@ Number::Guard::round() const noexcept { auto mode = Number::getround(); - if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled && empty()) + if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && empty()) { // No remainder return Round::Exact; @@ -482,7 +520,7 @@ void Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) { auto r = round(); - if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled) + if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330) { // If there was any remainder, subtract 1 from the result. This is sufficient to get the // best rounding. @@ -814,7 +852,7 @@ Number::operator+=(Number const& y) xe = ye; xn = yn; } - if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled) + if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330) { // Grow xm/xe and pull digits out of the Guard until it's a little bit larger than // maxMantissa, so that normalize will have enough information to make an accurate diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index 15136115c7..5c4730e1a6 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -58,7 +58,7 @@ setCurrentTransactionRules(std::optional r) { if (enableCuspRounding3_3_0) { - return MantissaRange::MantissaScale::Large; + return MantissaRange::MantissaScale::Large330; } if (enableCuspRounding3_2_0) { diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 266bc9bc1e..4ebdc6a73d 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -1813,7 +1813,7 @@ public: switch (scale) { case MantissaRange::MantissaScale::Large320: - case MantissaRange::MantissaScale::Large: + case MantissaRange::MantissaScale::Large330: BEAST_EXPECT(signedDifference >= 0); BEAST_EXPECT(signedDifference < pow10(product.exponent())); BEAST_EXPECT( @@ -1896,7 +1896,7 @@ public: switch (scale) { case MantissaRange::MantissaScale::Large320: - case MantissaRange::MantissaScale::Large: + case MantissaRange::MantissaScale::Large330: BEAST_EXPECT(stored >= exact); BEAST_EXPECT(diff < pow10(quotient.exponent())); break; @@ -1947,7 +1947,7 @@ public: switch (scale) { case MantissaRange::MantissaScale::Large320: - case MantissaRange::MantissaScale::Large: + case MantissaRange::MantissaScale::Large330: BEAST_EXPECT(stored <= exact); BEAST_EXPECT(diff > -pow10(quotient.exponent())); break; @@ -2005,7 +2005,7 @@ public: switch (scale) { case MantissaRange::MantissaScale::Large320: - case MantissaRange::MantissaScale::Large: + case MantissaRange::MantissaScale::Large330: BEAST_EXPECT(stored >= exact); BEAST_EXPECT(diff < pow10(quotient.exponent())); break; From 902fbdc3ad48eba3c3251bf84e2c6af67939b3fc Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 9 Jun 2026 19:06:38 -0400 Subject: [PATCH 62/77] Also fix local 3_2_0 variable names --- src/libxrpl/protocol/Rules.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index 5c4730e1a6..2f55d7136b 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -46,21 +46,21 @@ setCurrentTransactionRules(std::optional r) // to useRulesGuards. bool const enableVaultNumbers = !r || (r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol)); - bool const enableCuspRounding3_2_0 = !r || r->enabled(fixCleanup3_2_0); - bool const enableCuspRounding3_3_0 = !r || r->enabled(fixNumberStuff); + bool const enableCuspRounding320 = !r || r->enabled(fixCleanup3_2_0); + bool const enableCuspRounding330 = !r || r->enabled(fixNumberStuff); XRPL_ASSERT( !r || useRulesGuards(*r) == - (enableVaultNumbers || enableCuspRounding3_2_0 || enableCuspRounding3_3_0), + (enableVaultNumbers || enableCuspRounding320 || enableCuspRounding330), "setCurrentTransactionRules : rule decisions match"); if (enableVaultNumbers) { - if (enableCuspRounding3_3_0) + if (enableCuspRounding330) { return MantissaRange::MantissaScale::Large330; } - if (enableCuspRounding3_2_0) + if (enableCuspRounding320) { return MantissaRange::MantissaScale::Large320; } From e4dafa3171dc174dc9e00dcb018d7cadc86df1aa Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 9 Jun 2026 19:11:34 -0400 Subject: [PATCH 63/77] Update names due to prior merge --- src/libxrpl/basics/Number.cpp | 22 +++++++++++----------- src/test/app/LoanBroker_test.cpp | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index bce5525773..7c12ebde0e 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -294,7 +294,7 @@ private: pushOverflow(T const& mantissa); enum class Round { - // The result is exact. No rounding is needed. Only used if cuspRoundingFix is Enabled. + // The result is exact. No rounding is needed. Only used if cuspRoundingFix is Enabled330. Exact = -2, // Round down. Since we use integer math, that usually means no change is needed. // Exceptions are for when the result is between kMaxRap and kMaxRepUp (round to kMaxRep), @@ -304,7 +304,7 @@ private: // 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) + // Enabled330) Up = 1, }; @@ -401,7 +401,7 @@ void Number::Guard::pushOverflow(T const& mantissa) { XRPL_ASSERT(mantissa <= kMaxRepUp, "xrpl::Number::Guard::doRoundUp : valid mantissa"); - if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled && mantissa > kMaxRep && + if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && mantissa > kMaxRep && mantissa < kMaxRepUp) { // Special case rounding rules for the values between kMaxRep and kMaxRepUp. @@ -488,13 +488,13 @@ Number::Guard::bringIntoRange(bool& negative, T& mantissa, int& exponent) // Bring mantissa back into the minMantissa / maxMantissa range AFTER // rounding if (mantissa < minMantissa && - (cuspRoundingFix < MantissaRange::CuspRoundingFix::Enabled || mantissa != 0)) + (cuspRoundingFix < MantissaRange::CuspRoundingFix::Enabled330 || mantissa != 0)) { mantissa *= 10; --exponent; } if (exponent < kMinExponent || - (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled && mantissa == 0)) + (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && mantissa == 0)) { static constexpr Number kZero = Number{}; @@ -532,7 +532,7 @@ Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string // 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. - if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled && + if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && mantissa > kMaxRep && mantissa < kMaxRepUp) { mantissa = kMaxRepUp; @@ -565,7 +565,7 @@ Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string } } else if ( - cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled && mantissa > kMaxRep && + cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && mantissa > kMaxRep && mantissa < kMaxRepUp) { mantissa = kMaxRep; @@ -632,7 +632,7 @@ Number::Guard::doRound(rep& drops, std::string location) ++drops; } else if ( - cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled && drops > kMaxRep && + cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && drops > kMaxRep && drops < kMaxRepUp) { // This will probably be impossible because this function is not called by mutating @@ -688,7 +688,7 @@ doNormalize( { static constexpr auto kMinExponent = Number::kMinExponent; static constexpr auto kMaxExponent = Number::kMaxExponent; - auto const repLimit = cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled + auto const repLimit = cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 ? Number::kMaxRepUp : Number::kMaxRep; @@ -883,7 +883,7 @@ Number::operator+=(Number const& y) auto const cuspRoundingFix = g.cuspRoundingFix; auto const repLimit = - cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled ? kMaxRepUp : kMaxRep; + 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. @@ -1004,7 +1004,7 @@ Number::operator*=(Number const& y) auto const& maxMantissa = g.maxMantissa; auto const repLimit = - g.cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled ? kMaxRepUp : kMaxRep; + g.cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 ? kMaxRepUp : kMaxRep; while (zm > maxMantissa || zm > repLimit) { diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 4ad046f2ac..5f318560c1 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -1452,9 +1452,9 @@ class LoanBroker_test : public beast::unit_test::Suite env(tx2, Ter(temINVALID)); } - if (Number::getMantissaScale() == MantissaRange::MantissaScale::Large) + if (Number::getMantissaScale() == MantissaRange::MantissaScale::Large330) { - // For the Large scale, 2^63 rounds _down_ to Number::kMaxRep + // For the Large330 scale, 2^63 rounds _down_ to Number::kMaxRep { auto const dm = power(2, 63); BEAST_EXPECTS(dm == kMaxMpTokenAmount, to_string(dm)); From d8e03a8018dbef16bae3597ac339c47ed83bcb44 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 10 Jun 2026 13:26:34 -0400 Subject: [PATCH 64/77] Update more names due to prior merge --- src/test/basics/Number_test.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 104c278545..f910af5dac 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -484,11 +484,11 @@ public: test(cSmall); break; case MantissaRange::MantissaScale::LargeLegacy: - case MantissaRange::MantissaScale::Large3_2_0: + case MantissaRange::MantissaScale::Large320: test(cLargeAll); test(cLargeLegacy); break; - case MantissaRange::MantissaScale::Large: + case MantissaRange::MantissaScale::Large330: test(cLargeAll); test(cLarge); break; @@ -1490,7 +1490,7 @@ public: switch (scale) { - case MantissaRange::MantissaScale::Large: + case MantissaRange::MantissaScale::Large330: // Because the absolute value of min() is larger than max(), it // will be rounded down toward max() test( @@ -1520,7 +1520,7 @@ public: switch (scale) { - case MantissaRange::MantissaScale::Large: + case MantissaRange::MantissaScale::Large330: // Rounding to nearest, since the mantissa is below the halfway point from // kMaxRep to kMaxRep up, it will be rounded down to kMaxRep test( @@ -2315,7 +2315,7 @@ public: break; case MantissaRange::MantissaScale::LargeLegacy: - case MantissaRange::MantissaScale::Large3_2_0: + case MantissaRange::MantissaScale::Large320: // Upward round UP BEAST_EXPECT(upward == above); From 1bb1d7156523d89aaab94fe218999873e8a05eb2 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 10 Jun 2026 16:11:43 -0400 Subject: [PATCH 65/77] test: Add more Number edge case tests, showing failures - NumberAddDirectedSignWrong - Addition of two negative numbers with the same exponent rounds ToNearest in the wrong direction. - Also include unit test cases with same exponent, and mixed signs. - No rounding issues in any combination, because the exponent can't change. - NumberAddToNearestPicksFarther - In scenarios where the two operands have different signs, and significantly different exponents, you can end up in a situation where the rounding looks like 0.5, which may round down to even, but is actually 0.5....nnn, which should always round up, you get the wrong result. --- src/test/basics/Number_test.cpp | 220 +++++++++++++++++++++++++++++++- 1 file changed, 218 insertions(+), 2 deletions(-) diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 4ebdc6a73d..1f720f3c11 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -50,9 +50,11 @@ class Number_test : public beast::unit_test::Suite toBigInt(Number const& n) { BigInt v = n.mantissa(); - for (int i = 0; i < n.exponent(); ++i) + auto e = n.exponent(); + + for (; e > 0; --e) v *= 10; - for (int i = 0; i > n.exponent(); --i) + for (; e < 0; ++e) { BEAST_EXPECT(v % 10 == 0); v /= 10; @@ -2147,12 +2149,224 @@ public: } } + void + testNumberAddDirectedSignWrong() + { + auto const scale = Number::getMantissaScale(); + testcase << "operator+ directed rounding wrong for equal-exponent negative sums " + << to_string(scale); + { + // Two negative numbers with the same exponent + Number const a{-6, Number::mantissaLog()}; + Number const b{a - 3}; + BEAST_EXPECT(a.exponent() == b.exponent() && abs(b) > abs(a)); + + BigInt const exact = toBigInt(a) + toBigInt(b); + if (scale == MantissaRange::MantissaScale::Small) + { + BEAST_EXPECT(exact == BigInt{"-12000000000000003"}); + } + else + { + BEAST_EXPECT(exact == BigInt{"-12000000000000000003"}); + } + + Number down, up; + { + NumberRoundModeGuard const g{Number::RoundingMode::Downward}; + down = a + b; + } + { + NumberRoundModeGuard const g{Number::RoundingMode::Upward}; + up = a + b; + } + + auto const valueDown = toBigInt(down); + auto const valueUp = toBigInt(up); + log << " exact = " << fmt(exact) << "\n downward = " << fmt(valueDown) + << " (correct rounding: <= exact)" + << "\n upward = " << fmt(valueUp) << " (correct rounding: >= exact)\n\n"; + log.flush(); + + if (scale == MantissaRange::MantissaScale::Large330) + { + BEAST_EXPECT(valueDown <= exact); // Downward should round away from zero + BEAST_EXPECT(valueUp >= exact); // Upward should round toward 0 + } + else + { + BEAST_EXPECT(valueDown > exact); // Downward rounded toward zero (too high) + BEAST_EXPECT(valueUp < exact); // Upward rounded toward -inf (too low) + } + } + + { + // Positive control: the same magnitudes with a positive result round + Number const pa{6, Number::mantissaLog()}; + Number const pb{pa + 3}; + BEAST_EXPECT(pa.exponent() == pb.exponent() && abs(pb) > abs(pa)); + BigInt const pexact = toBigInt(pa) + toBigInt(pb); // 12'000'000'000'000'000'003 + + Number pdown, pup; + { + NumberRoundModeGuard const g{Number::RoundingMode::Downward}; + pdown = pa + pb; + } + { + NumberRoundModeGuard const g{Number::RoundingMode::Upward}; + pup = pa + pb; + } + auto const valuePDown = toBigInt(pdown); + auto const valuePUp = toBigInt(pup); + log << " exact = " << fmt(pexact) << "\n downward = " << fmt(valuePDown) + << " (correct rounding: <= exact)" + << "\n upward = " << fmt(valuePUp) << " (correct rounding: >= exact)\n\n"; + log.flush(); + + BEAST_EXPECT(valuePDown <= pexact); // correct for positive results + BEAST_EXPECT(valuePUp >= pexact); + } + + { + // Mixed sign numbers with the same exponent: negative second value + Number const a{1, Number::mantissaLog()}; + Number const b{Number{-9, Number::mantissaLog()} - 3}; + BEAST_EXPECT(a.exponent() == b.exponent() && abs(b) > abs(a)); + + BigInt const exact = toBigInt(a) + toBigInt(b); + if (scale == MantissaRange::MantissaScale::Small) + { + BEAST_EXPECT(exact == BigInt{"-8000000000000003"}); + } + else + { + BEAST_EXPECT(exact == BigInt{"-8000000000000000003"}); + } + + Number down, up; + { + NumberRoundModeGuard const g{Number::RoundingMode::Downward}; + down = a + b; + } + { + NumberRoundModeGuard const g{Number::RoundingMode::Upward}; + up = a + b; + } + + auto const valueDown = toBigInt(down); + auto const valueUp = toBigInt(up); + log << " exact = " << fmt(exact) << "\n downward = " << fmt(valueDown) + << " (correct rounding: <= exact)" + << "\n upward = " << fmt(valueUp) << " (correct rounding: >= exact)\n\n"; + log.flush(); + + BEAST_EXPECT(valueDown <= exact); // Downward should round away from zero + BEAST_EXPECT(valueUp >= exact); // Upward should round toward 0 + } + + { + // Mixed sign numbers with the same exponent: negative first value + Number const a{-1, Number::mantissaLog()}; + Number const b{Number{9, Number::mantissaLog()} + 3}; + BEAST_EXPECT(a.exponent() == b.exponent() && abs(b) > abs(a)); + + BigInt const exact = toBigInt(a) + toBigInt(b); + if (scale == MantissaRange::MantissaScale::Small) + { + BEAST_EXPECT(exact == BigInt{"8000000000000003"}); + } + else + { + BEAST_EXPECT(exact == BigInt{"8000000000000000003"}); + } + + Number down, up; + { + NumberRoundModeGuard const g{Number::RoundingMode::Downward}; + down = a + b; + } + { + NumberRoundModeGuard const g{Number::RoundingMode::Upward}; + up = a + b; + } + + auto const valueDown = toBigInt(down); + auto const valueUp = toBigInt(up); + log << " exact = " << fmt(exact) << "\n downward = " << fmt(valueDown) + << " (correct rounding: <= exact)" + << "\n upward = " << fmt(valueUp) << " (correct rounding: >= exact)\n\n"; + log.flush(); + + BEAST_EXPECT(valueDown <= exact); // Downward should round away from zero + BEAST_EXPECT(valueUp >= exact); // Upward should round toward 0 + } + } + + void + testNumberAddToNearestPicksFarther() + { + auto const scale = Number::getMantissaScale(); + + // Case is + using Case = std::pair; + + auto const c = std::to_array({ + {Number{5'175'909'259'972'499'745LL, 22}, -1'074'951'375'311'646'003}, + {Number{1}, -1'074'956'551'220'905'975}, + {Number{1, 10}, -1'074'956'551'220'905'975}, + {Number{1, 20}, -1'074'956'551'220'905'975}, + {Number{1, 27}, -1'074'956'551'220'905'975}, + {Number{1, 28}, -1'074'956'551'220'905'974}, + {Number{1, 31}, -1'074'956'551'220'904'975}, + }); + + testcase << "operator+ ToNearest picks farther representable in cancellation " + << to_string(scale); + + for (auto const& [y, expectedQ] : c) + { + NumberRoundModeGuard const roundGuard{Number::RoundingMode::ToNearest}; + + Number const x{-1'074'956'551'220'905'975LL, 28}; + Number const res = x + y; + + BigInt const exact = toBigInt(x) + toBigInt(y); + BigInt const vres = toBigInt(res); + + BigInt ulp = 1; + for (int i = 0; i < res.exponent(); ++i) + ulp *= 10; + + BigInt const q = (exact - ulp / 2) / ulp; + Number const normalizedExact{static_cast(q), res.exponent()}; + BigInt const norm = toBigInt(normalizedExact); + + log << " x = " << x << "\n y = " << y + << "\n exact = " << fmt(exact) + << "\n result (x + y) = " << fmt(vres) + << "\n normalize(exact) = " << fmt(norm) << "\n\n"; + log.flush(); + + if (scale == MantissaRange::MantissaScale::Small) + { + auto const comp = toBigInt(Number{expectedQ, -3}); + BEAST_EXPECTS(q == comp, fmt(q) + " != " + fmt(comp)); + } + else + { + BEAST_EXPECTS(q == expectedQ, fmt(q) + " != " + fmt(BigInt(expectedQ))); + } + BEAST_EXPECT(normalizedExact == res); + } + } + void run() override { for (auto const scale : MantissaRange::getAllScales()) { NumberMantissaScaleGuard const sg(scale); + testZero(); testLimits(); testToString(); @@ -2176,6 +2390,8 @@ public: testInt64(); testUpwardRoundsDown(); + testNumberAddDirectedSignWrong(); + testNumberAddToNearestPicksFarther(); } } }; From b08754575506a7e4d7ed6a7a86d1104eaf24e7b9 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 10 Jun 2026 16:18:38 -0400 Subject: [PATCH 66/77] Number improvements - Expand documentation. - Refactor Number::Guard::round() to simplify. - Set the Guard sign correctly in += for numbers with the same exponent. - Only really relevant if both values are negative. - In +=, when needed, expand one mantissa to a size large enough to have a few extra digits, which can be used to determine rounding. - If the exponents are still different, trim the other mantissa as before until the exponents match. - For subtraction (where the values' signs are different), pop digits out of the Guard as necessary, but go far enough to have a few extra digits again for rounding later. - Finally, don't discard any "leftover" digits in the Guard when normalizing, to avoid the 0.5....nnn problem. --- src/libxrpl/basics/Number.cpp | 175 +++++++++++++++++++++++++++------- 1 file changed, 138 insertions(+), 37 deletions(-) diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index c356c51722..222c0b611c 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -205,15 +205,37 @@ divu10(uint128_t& u) return r; } -// Guard - -// The Guard class is used to temporarily add extra digits of -// precision to an operation. This enables the final result -// to be correctly rounded to the internal precision of Number. - template concept UnsignedMantissa = std::is_unsigned_v || std::is_same_v; +/** Guard + + The Guard class is used to temporarily add extra digits of + precision to an operation. This enables the final result + to be correctly rounded to the internal precision of Number. + + At it's core, the Guard really only needs three pieces of information to determine how to round: + 1. The rounding mode + 2. The last digit dropped from the mantissa (i.e. the first digit after the decimal point). + (first byte of digits_) + 3. Whether any other non-zero digits were dropped from the mantissa. (xbit_) + + Upward and Downward rounding modes round the unsigned mantissa toward or away from zero + depending on whether the sign is negative (sbit_). For positive values, Upward is away, and + Downward is toward. For negative values, that's reversed. For simplicity, I'm going to describe + the logic using "TowardZero" and "FromZero". + + * TowardZero is the easiest rounding mode. It always rounds down. digits_ and xbit_ are + irrelevant. + * FromZero is almost as simple. If both "digits_" and "xbit_" are zero (0), it rounds down. + Else it rounds up. + * ToNearest is only a little more complicated. If the last dropped digit is < 5, then round + down. If it is > 5, round up. If it is exactly 5, and there are _any_ other digits (the + remainder of "digits_" or "xbit_"), round up, else round to even. + + The current implementation stores 16 digits in "digits_" so that digits can be "pop"ped back + out if needed during subtraction (negative addition) operations. +*/ class Number::Guard { std::uint64_t digits_{0}; // 16 decimal guard digits @@ -410,23 +432,18 @@ Number::Guard::round() const noexcept if (mode == RoundingMode::TowardsZero) return Round::Down; - if (mode == RoundingMode::Downward) + // Also Towards Zero + if ((mode == RoundingMode::Downward && !sbit_) || (mode == RoundingMode::Upward && sbit_)) { - if (sbit_) - { - if (digits_ > 0 || xbit_) - return Round::Up; - } return Round::Down; } - if (mode == RoundingMode::Upward) + // Away from Zero + if ((mode == RoundingMode::Downward && sbit_) || (mode == RoundingMode::Upward && !sbit_)) { - if (sbit_) + if (empty()) return Round::Down; - if (digits_ > 0 || xbit_) - return Round::Up; - return Round::Down; + return Round::Up; } // assume round to nearest if mode is not one of the predefined values @@ -810,35 +827,94 @@ Number::operator+=(Number const& y) // Bring the exponents of both values into agreement, so the mantissas are on the same scale // and can be added directly together. + + auto const upperLimit = static_cast(g.minMantissa) * 1000; + // For the "adjust" lambda + // expandM / expandE: The values for which the mantissa will be expanded, and the exponent + // decreased to match. Mantissa won't be expanded beyond upperLimit. + // (37e8 == 37000e5 == 37000000e2) + // shrinkM / shrinkE: The values for which the mantissa will be shrunk, and exponent increased + // to match, if necessary. + auto const adjust = [&g, &upperLimit]( + uint128_t& expandM, int& expandE, uint128_t& shrinkM, int& shrinkE) { + // Adjust up and down until the exponents match + if (g.cuspRoundingFix == MantissaRange::CuspRoundingFix::Enabled330) + { + // For Enabled330, there are three steps. + // 1. First, shrink the mantissa of shrinkM/shrinkE while shrinkM ends in 0. + while (shrinkE < expandE && shrinkM % 10 == 0) + { + g.doDropDigit(shrinkM, shrinkE); + } + + // 2. Then expand the mantissa of expandM/expandE, with a limit for expandM a few orders + // of magnitude above the MantissaRange. This will leave a few extra digits for rounding + // later, but no excess. + while (shrinkE < expandE && expandE > kMinExponent && expandM < upperLimit) + { + expandM *= 10; + --expandE; + } + } + + // 3. Finally, shrink the mantissa of shrinkM/shrinkE until the exponents match. Any removed + // digits will be put into the Guard. This is the only step for non-Enabled330 modes. + while (shrinkE < expandE) + { + g.doDropDigit(shrinkM, shrinkE); + } + }; + // Shrink the mantissa and raise the exponent of the value with the lower exponent. Store any // dropped digits in the Guard. if (xe < ye) { if (xn) g.setNegative(); - do - { - g.doDropDigit(xm, xe); - } while (xe < ye); + + adjust(ym, ye, xm, xe); } else if (xe > ye) { if (yn) g.setNegative(); - do - { - g.doDropDigit(ym, ye); - } while (xe > ye); + + adjust(xm, xe, ym, ye); + } + else if (g.cuspRoundingFix == MantissaRange::CuspRoundingFix::Enabled330) + { + // Both values have the same exponent. + // Set the sign of the Guard based on the sign of the Number with the smallest + // unsigned _mantissa_ + if ((xm < ym && xn) || (ym < xm && yn)) + g.setNegative(); } if (xn == yn) { xm += ym; - if (xm > maxMantissa || xm > kMaxRep) + + if (g.cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330) { - g.doDropDigit(xm, xe); + // Don't do any adjustments for Enabled330. Normalize will take care of it + // Because of "adjust", the only way there can be data in the Guard is if we first grew + // the mantissa past the maxMantissa. Since we added here, it can only get bigger. + // If xm > maxMantissa, then doNormalize has all the data it needs from the last 3-4 + // digits, plus the "dropped" flag that will be passed in. + // If not, then the mantissa will only need to be padded out with 0s and won't need to + // round. + XRPL_ASSERT( + xm > maxMantissa || g.empty(), + "xrpl::Number::operator+ : rounding state expected after add"); + } + else + { + if (xm > maxMantissa || xm > kMaxRep) + { + g.doDropDigit(xm, xe); + } + g.doRoundUp(xn, xm, xe, "Number::addition overflow"); } - g.doRoundUp(xn, xm, xe, "Number::addition overflow"); } else { @@ -854,18 +930,24 @@ Number::operator+=(Number const& y) } if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330) { - // Grow xm/xe and pull digits out of the Guard until it's a little bit larger than - // maxMantissa, so that normalize will have enough information to make an accurate - // rounding decision, but stop if the Guard empties out, because no rounding will be - // necessary. (Normalize will pad it back into range.) Note that if any digits were lost - // (xbit), the Guard will never be empty, so xm will get larger than upperLimit. - auto const upperLimit = static_cast(minMantissa) * 1000; + // Because we subtracted, xm can have any number of digits from 1 up to + // upperLimit * 10, and g can be in any state. (Note that xm can't be zero, because that + // special case was tested earlier.) + + // Grow xm/xe and pull digits out of the Guard until xm reaches upperLimit, but stop if + // the Guard empties out, because no rounding will be necessary. This will ensure that + // normalize will have enough information to make an accurate rounding decision. + // (Normalize will pad a small mantissa back into range.) Note that if any digits were + // lost (xbit_), the Guard will never be empty, so xm will grow larger than upperLimit. while (xm < upperLimit && !g.empty()) { xm *= 10; xm -= g.pop(); --xe; } + XRPL_ASSERT( + xm > maxMantissa || g.empty(), + "xrpl::Number::operator+ : rounding state expected after subtract"); } else { @@ -878,12 +960,31 @@ Number::operator+=(Number const& y) --xe; } } - // Round down, based on whether there is any data left in the Guard (depending on - // cuspRoundingFix) + // Rounding down can result in decrementing xm, based on whether there is any data left in + // the Guard (depending on cuspRoundingFix). Note that if that happens, then the Guard is + // not empty. For Enabled330, that will also result in the "dropped" flag being passed to + // doNormalize, which may result in the mantissa being incremented again. It doesn't matter + // what the dropped digits are, only that they exist. This is because subtracting one + // "overcorrects", so we know there are still trailing digits to be accounted for in the + // rounding. + // + // This works because + // 1. The rounding up will be done _after_ the mantissa is brought into range. It may not + // be in range right now, and + // 2. The "dropped" flag is only ever used as a tie-breaker, specifically when rounding + // away from zero, and the dropped digits are 0, or when rounding to nearest, and + // the dropped digits represent exactly 0.5. g.doRoundDown(xn, xm, xe); } - doNormalize(xn, xm, xe, minMantissa, maxMantissa, cuspRoundingFix, false); + doNormalize( + xn, + xm, + xe, + minMantissa, + maxMantissa, + cuspRoundingFix, + cuspRoundingFix == MantissaRange::CuspRoundingFix::Enabled330 && !g.empty()); negative_ = xn; mantissa_ = static_cast(xm); exponent_ = xe; From 463fb88cd8bd0e898788694360cba090dc42a9ef Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 11 Jun 2026 23:02:47 -0400 Subject: [PATCH 67/77] Apply suggestions from AI code review Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com> --- src/libxrpl/basics/Number.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index b065e390a8..2590ca7a8d 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -319,7 +319,7 @@ private: // The result is exact. No rounding is needed. Only used if cuspRoundingFix is Enabled330. Exact = -2, // Round down. Since we use integer math, that usually means no change is needed. - // Exceptions are for when the result is between kMaxRap and kMaxRepUp (round to kMaxRep), + // 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, @@ -422,7 +422,7 @@ template void Number::Guard::pushOverflow(T const& mantissa) { - XRPL_ASSERT(mantissa <= kMaxRepUp, "xrpl::Number::Guard::doRoundUp : valid mantissa"); + XRPL_ASSERT(mantissa <= kMaxRepUp, "xrpl::Number::Guard::pushOverflow : valid mantissa"); if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && mantissa > kMaxRep && mantissa < kMaxRepUp) { @@ -431,7 +431,7 @@ Number::Guard::pushOverflow(T const& mantissa) // if it was a digit that got removed, but don't remove it. This method is future-proof in // case the number of mantissa bits ever changes. Effects: // * For round to nearest - // * if the mantissa is below the midpoint, it'll round "down" to kMaxRepUp + // * if the mantissa is below the midpoint, it'll round "down" to kMaxRep // * if above the midpoint, it'll round "up" to kMaxRepUp // * if can never be exactly at the midpoint, because kMaxRepUp is always even, and // kMaxRep is always odd, so don't worry about it. From 095e14193ea5dfcbc0007378efa3995a64580dc7 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 12 Jun 2026 19:29:01 -0400 Subject: [PATCH 68/77] Apply suggestions from AI code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/test/app/Vault_test.cpp | 6 +++--- src/test/basics/Number_test.cpp | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 8c01031773..a0d3fde035 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -5846,7 +5846,7 @@ class Vault_test : public beast::unit_test::Suite // This value will be rounded auto const insertAt = maxInt64Plus2.size() - 3; auto const decimalTest = maxInt64Plus2.substr(0, insertAt) + "." + - maxInt64Plus2.substr(insertAt); // (max int64+1) / 1000 + maxInt64Plus2.substr(insertAt); // (max int64+2) / 1000 BEAST_EXPECT(decimalTest == "9223372036854775.809"); tx[sfAssetsMaximum] = decimalTest; auto const newKeylet = keylet::vault(owner.id(), env.seq(owner)); @@ -5898,7 +5898,7 @@ class Vault_test : public beast::unit_test::Suite // This value will be rounded auto const insertAt = maxInt64Plus2.size() - 1; auto const decimalTest = maxInt64Plus2.substr(0, insertAt) + "." + - maxInt64Plus2.substr(insertAt); // (max int64+1) / 10 + maxInt64Plus2.substr(insertAt); // (max int64+2) / 10 BEAST_EXPECT(decimalTest == "922337203685477580.9"); tx[sfAssetsMaximum] = decimalTest; auto const newKeylet = keylet::vault(owner.id(), env.seq(owner)); @@ -5950,7 +5950,7 @@ class Vault_test : public beast::unit_test::Suite { auto const insertAt = maxInt64Plus2.size() - 1; auto const decimalTest = maxInt64Plus2.substr(0, insertAt) + "." + - maxInt64Plus2.substr(insertAt); // (max int64+1) / 10 + maxInt64Plus2.substr(insertAt); // (max int64+2) / 10 BEAST_EXPECT(decimalTest == "922337203685477580.9"); tx[sfAssetsMaximum] = decimalTest; auto const newKeylet = keylet::vault(owner.id(), env.seq(owner)); diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index c1774ff158..78d31f81a5 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -2267,8 +2267,7 @@ public: } { - testcase << "normalization cusp: ToNearest and Downward disagree " << to_string(scale); - + testcase << "normalization cusp: ToNearest and Downward behavior " << to_string(scale); constexpr auto kMaxRep = Number::kMaxRep; // Both ToNearest and Downward should round to `below` From a63bab2a5ec53dc803dee631009adcea48a071c1 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 12 Jun 2026 19:38:07 -0400 Subject: [PATCH 69/77] Cleanups: Comments, variable names, one test case - "ToNearest and Downward behavior Small" test case --- src/libxrpl/basics/Number.cpp | 13 +++++++------ src/test/basics/Number_test.cpp | 26 ++++++++++---------------- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 8fd8fafdfc..20e1449601 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -431,10 +431,10 @@ Number::Guard::pushOverflow(T const& mantissa) // * For round to nearest // * if the mantissa is below the midpoint, it'll round "down" to kMaxRep // * if above the midpoint, it'll round "up" to kMaxRepUp - // * if can never be exactly at the midpoint, because kMaxRepUp is always even, and - // kMaxRep is always odd, so don't worry about it. + // * 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. + // negative. // * For round downward, does the opposite of upward. // * For round toward zero, always rounds down to kMaxRep. auto constexpr spread = kMaxRepUp - kMaxRep; @@ -501,13 +501,14 @@ void Number::Guard::bringIntoRange(bool& negative, T& mantissa, int& exponent) { // Bring mantissa back into the minMantissa / maxMantissa range AFTER - // rounding - if (mantissa < minMantissa && - (cuspRoundingFix < MantissaRange::CuspRoundingFix::Enabled330 || mantissa != 0)) + // rounding. Mantissa should never be 0. + XRPL_ASSERT(mantissa != 0, "xrpl::Number::Guard::bringIntoRange : valid mantissa"); + if (mantissa < minMantissa) { mantissa *= 10; --exponent; } + // mantissa should never be 0, but if it _is_ make the result kZero. if (exponent < kMinExponent || (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && mantissa == 0)) { diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 78d31f81a5..c0445d9ad0 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -426,7 +426,7 @@ public: // Note that items with extremely large mantissas need to be // calculated, because otherwise they overflow uint64. Items from C // with larger mantissa - auto const cLargeLegacy = std::to_array({ + auto const cLarge = std::to_array({ // Anything larger than kMaxRep rounds up {Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}}, Number{1, 0}, @@ -446,7 +446,7 @@ public: __LINE__}, {power(2, 63), Number{3, 0}, Number{Number::kMaxRep}, __LINE__}, }); - auto const cLarge = std::to_array({ + auto const cLarge330 = std::to_array({ // kMaxRep + 1 is below the half-way point, so it rounds down to kMaxRep when the Number // is created. {Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}}, @@ -488,11 +488,11 @@ public: case MantissaRange::MantissaScale::LargeLegacy: case MantissaRange::MantissaScale::Large320: test(cLargeAll); - test(cLargeLegacy); + test(cLarge); break; case MantissaRange::MantissaScale::Large330: test(cLargeAll); - test(cLarge); + test(cLarge330); break; default: BEAST_EXPECT(false); @@ -2274,7 +2274,7 @@ public: auto const actual = static_cast(kMaxRep) + 1; Number const below{static_cast(kMaxRep), 0}; Number const above{ - false, static_cast(kMaxRep) + 3, 0, Number::Unchecked{}}; + false, static_cast(kMaxRep) + 3, 0, Number::Normalized{}}; auto construct = [](Number::RoundingMode mode) { NumberRoundModeGuard const roundGuard{mode}; @@ -2297,21 +2297,15 @@ public: switch (scale) { case MantissaRange::MantissaScale::Small: - // With the small mantissa, everything rounds up + // With the small mantissa, everything but Downward rounds UP, including the + // reference values, "above" and "below" - // Upward rounds UP - BEAST_EXPECT(upward > above); + BEAST_EXPECT(below == above); + BEAST_EXPECT(upward == above); + BEAST_EXPECT(toNearest == above); - // ToNearest rounds UP when the DOWN neighbor is strictly closer - BEAST_EXPECT(toNearest > above); - BEAST_EXPECT(toNearest == below); - - // Downward undershoots: it returns a value below `below` BEAST_EXPECT(downward < below); - // Both should have given the same answer, but they differ - BEAST_EXPECT(toNearest > downward); - break; case MantissaRange::MantissaScale::LargeLegacy: From 22a7f5bb49a74f60e1bfbece96a1a8afa3222f74 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 12 Jun 2026 19:44:08 -0400 Subject: [PATCH 70/77] Fix assertion typo src/libxrpl/basics/Number.cpp Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com> --- src/libxrpl/basics/Number.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 20e1449601..dd1626d540 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -446,7 +446,7 @@ Number::Guard::pushOverflow(T const& mantissa) auto const diff = mantissa - kMaxRep; auto const digit = (diff * 10) / spread; XRPL_ASSERT( - digit > 0 && digit < 10, "xrpld::Number::Guard::pushOverflow : valid overflow digit"); + digit > 0 && digit < 10, "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); From 8f51891631b09d9ceffeeb2d53a24680dda2c328 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 15 Jun 2026 14:57:39 -0400 Subject: [PATCH 71/77] Use constexpr in one more place --- src/test/basics/Number_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index c0445d9ad0..311270a8e6 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -2271,7 +2271,7 @@ public: constexpr auto kMaxRep = Number::kMaxRep; // Both ToNearest and Downward should round to `below` - auto const actual = static_cast(kMaxRep) + 1; + auto constexpr actual = static_cast(kMaxRep) + 1; Number const below{static_cast(kMaxRep), 0}; Number const above{ false, static_cast(kMaxRep) + 3, 0, Number::Normalized{}}; From da396070ce66186ef9ebaa04a7f6a285b736fe17 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 18 Jun 2026 17:16:29 -0400 Subject: [PATCH 72/77] Address review feedback from @TimothyBanks and @gregtatcam - Remove unnecessary if constexpr check - Update scaling static_assert - Remove unnecessary rounding logic from Number::Guard::doRound() - Handle fractional rounding between kMaxRep and kMaxRepUp --- src/libxrpl/basics/Number.cpp | 87 ++++++++++------ src/test/basics/Number_test.cpp | 174 +++++++++++++++++++++++++++++++- 2 files changed, 228 insertions(+), 33 deletions(-) diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index bf7ea0951b..89b4e1dede 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -302,9 +302,9 @@ public: doRound(rep& drops, std::string location); private: - template + template void - pushOverflow(T const& mantissa); + pushOverflow(T mantissa); enum class Round { // The result is exact. No rounding is needed. Only used if cuspRoundingFix is Enabled330 or @@ -410,20 +410,54 @@ Number::Guard::doDropDigit(uint128_t& mantissa, int& exponent) noexce ++exponent; } -template +template void -Number::Guard::pushOverflow(T const& mantissa) +Number::Guard::pushOverflow(T mantissa) { XRPL_ASSERT(mantissa <= kMaxRepUp, "xrpl::Number::Guard::pushOverflow : valid mantissa"); - if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && mantissa > kMaxRep && + if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && mantissa >= kMaxRep && mantissa < kMaxRepUp) { - // Special case rounding rules for the values between kMaxRep and kMaxRepUp. - // Scale the spread between kMaxRep and kMaxRepUp from 1 to 9, and push it onto the guard as - // if it was a digit that got removed, but don't remove it. This method is future-proof in - // case the number of mantissa bits ever changes. Effects: + // 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 mantissa is below the midpoint, it'll round "down" to kMaxRep + // * 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. @@ -431,16 +465,12 @@ Number::Guard::pushOverflow(T const& mantissa) // negative. // * For round downward, does the opposite of upward. // * For round toward zero, always rounds down to kMaxRep. - auto constexpr spread = kMaxRepUp - kMaxRep; - static_assert(spread < 10 && spread > 0); - // This should absolutely be impossible - if constexpr (spread == 0) - return; // LCOV_EXCL_LINE auto const diff = mantissa - kMaxRep; - auto const digit = (diff * 10) / spread; + auto digit = (diff * 10) / spread; XRPL_ASSERT( - digit > 0 && digit < 10, "xrpl::Number::Guard::pushOverflow : valid overflow digit"); + 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); @@ -542,18 +572,19 @@ 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. 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), @@ -630,8 +661,6 @@ Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) void Number::Guard::doRound(rep& drops, std::string location) { - pushOverflow(drops); - auto r = round(); if (r == Round::Up || (r == Round::Even && (drops & 1) == 1)) { @@ -648,14 +677,8 @@ Number::Guard::doRound(rep& drops, std::string location) } ++drops; } - else if ( - cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && drops > kMaxRep && - drops < kMaxRepUp) - { - // This will probably be impossible because this function is not called by mutating - // functions, so the Number will already be normalized. - drops = kMaxRep; - } + XRPL_ASSERT(drops >= 0, "xrpl::Number::Guard::doRound : positive magnitude"); + if (isNegative()) drops = -drops; } diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 311270a8e6..e9b03574c4 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -301,12 +301,15 @@ public: auto const cLargeLegacy = std::to_array({ {Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep / 10, 1}, __LINE__}, }); - auto const cLargeCorrected = std::to_array({ + auto const cLarge320 = std::to_array({ {Number{Number::kMaxRep}, Number{6, -1}, Number{(Number::kMaxRep / 10) + 1, 1}, __LINE__}, }); + auto const cLargeCorrected = std::to_array({ + {Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep}, __LINE__}, + }); auto test = [this](auto const& c) { for (auto const& [x, y, z, line] : c) { @@ -327,6 +330,10 @@ public: { test(cLargeLegacy); } + else if (scale == MantissaRange::MantissaScale::Large320) + { + test(cLarge320); + } else { test(cLargeCorrected); @@ -2555,6 +2562,170 @@ public: } } + void + testNumberRoundCuspWithFractionalParts() + { + auto const scale = Number::getMantissaScale(); + + testcase << "normalization cusp: rounding behavior with fractional parts " + << to_string(scale); + NumberRoundModeGuard const roundGuard{Number::RoundingMode::ToNearest}; + + Number const below{static_cast(Number::kMaxRep), 0}; + Number const above{false, Number::kMaxRepUp, 0, Number::Normalized{}}; + + log << "Below: " << below << ", Above: " << above << std::endl; + + auto const zeroPointFour = Number(4, -1); + auto const zeroPointSix = Number(6, -1); + auto const onePointFour = Number(14, -1); + auto const onePointFive = Number(15, -1); + auto const onePointSix = Number(16, -1); + auto const twoPointFour = Number(24, -1); + auto const twoPointSix = Number(26, -1); + + auto const operands = std::to_array({ + zeroPointFour, + zeroPointSix, + onePointFour, + onePointFive, + onePointSix, + twoPointFour, + twoPointSix, + }); + + auto const modes = std::to_array({ + Number::RoundingMode::ToNearest, + Number::RoundingMode::TowardsZero, + Number::RoundingMode::Downward, + Number::RoundingMode::Upward, + }); + + // Addition cases test kMaxRep + Operand + for (auto const& mode : modes) + { + for (auto const& operand : operands) + { + NumberRoundModeGuard const rg{mode}; + + auto const expectedValue = [&]() { + if (scale >= MantissaRange::MantissaScale::Large330) + { + if (mode == Number::RoundingMode::ToNearest && operand < onePointFive) + return below; + if (mode == Number::RoundingMode::TowardsZero || + mode == Number::RoundingMode::Downward) + return below; + } + if (scale == MantissaRange::MantissaScale::Large320) + { + if (mode == Number::RoundingMode::ToNearest) + { + if (operand < zeroPointSix) + return below; + } + if (mode == Number::RoundingMode::TowardsZero || + mode == Number::RoundingMode::Downward) + { + if (operand >= onePointFour) + return below - 7; + return below; + } + } + if (scale == MantissaRange::MantissaScale::LargeLegacy) + { + if (mode == Number::RoundingMode::ToNearest) + { + if (operand < zeroPointSix) + return below; + if (operand == zeroPointSix) + return below - 7; + } + if (mode == Number::RoundingMode::TowardsZero || + mode == Number::RoundingMode::Downward) + { + if (operand >= onePointFour) + return below - 7; + return below; + } + if (mode == Number::RoundingMode::Upward && operand <= zeroPointSix) + return below - 7; + } + if (scale == MantissaRange::MantissaScale::Small && + mode == Number::RoundingMode::Upward) + return above + 1000; + return above; + }(); + + Number const actual = below + operand; + + std::stringstream ss; + ss << "kMaxRep + " << operand << " rounded " << to_string(mode) << " to " << actual + << ". Expected: " << expectedValue; + if (BEAST_EXPECTS(actual == expectedValue, ss.str())) + log << "\tSUCCESS: " << to_string(scale) << " " << ss.str() << std::endl; + } + log << std::endl; + } + + // Subtraction cases test kMaxRepUp - Operand + for (auto const& mode : modes) + { + for (auto const& operand : operands) + { + NumberRoundModeGuard const rg{mode}; + + auto const expectedValue = [&]() { + if (scale >= MantissaRange::MantissaScale::Large330) + { + if (mode == Number::RoundingMode::ToNearest && operand > onePointFive) + return below; + if (mode == Number::RoundingMode::TowardsZero || + mode == Number::RoundingMode::Downward) + return below; + } + if (scale == MantissaRange::MantissaScale::LargeLegacy || + scale == MantissaRange::MantissaScale::Large320) + { + if (mode == Number::RoundingMode::ToNearest) + { + if (operand >= twoPointSix) + return below; + } + if (mode == Number::RoundingMode::TowardsZero) + { + if (operand >= onePointFour) + return below - 7; + } + if (mode == Number::RoundingMode::Downward) + { + if (operand <= onePointSix) + return below - 7; + return below; + } + } + if (scale == MantissaRange::MantissaScale::Small) + { + if (mode == Number::RoundingMode::Downward) + return below - 1000; + if (mode == Number::RoundingMode::Upward) + return below; + } + return above; + }(); + + Number const actual = above - operand; + + std::stringstream ss; + ss << "kMaxRepUp - " << operand << " rounded " << to_string(mode) << " to " + << actual << ". Expected: " << expectedValue; + if (BEAST_EXPECTS(actual == expectedValue, ss.str())) + log << "\tSUCCESS: " << to_string(scale) << " " << ss.str() << std::endl; + } + log << std::endl; + } + } + void run() override { @@ -2587,6 +2758,7 @@ public: testEdgeCases(); testNumberAddDirectedSignWrong(); testNumberAddToNearestPicksFarther(); + testNumberRoundCuspWithFractionalParts(); } } }; From 8250aa2375a5478ad1423aca5865b8d778214b14 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 18 Jun 2026 20:15:19 -0400 Subject: [PATCH 73/77] Remove changes to Number.cpp - Show that old behavior is not affected, and that the new tests fail without them. --- src/libxrpl/basics/Number.cpp | 193 ++++++++-------------------------- 1 file changed, 45 insertions(+), 148 deletions(-) diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 89b4e1dede..a694e60486 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -287,25 +287,6 @@ public: void doDropDigit(T& mantissa, int& exponent) noexcept; - // Modify the result to the correctly rounded value - template - void - doRoundUp(bool& negative, T& mantissa, int& exponent, std::string location); - - // Modify the result to the correctly rounded value - template - void - doRoundDown(bool& negative, T& mantissa, int& exponent); - - // Modify the result to the correctly rounded value - void - 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. @@ -318,16 +299,31 @@ private: // 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) + // Enabled) Up = 1, }; - // Indicate round direction. See Round enum above. + // 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 + doRoundUp(bool& negative, T& mantissa, int& exponent, std::string location); + + // Modify the result to the correctly rounded value + template + void + doRoundDown(bool& negative, T& mantissa, int& exponent); + + // Modify the result to the correctly rounded value + void + doRound(rep& drops, std::string location) const; + +private: void doPush(unsigned d) noexcept; @@ -410,78 +406,10 @@ 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: -// 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 +// -1 if Guard is less than half +// 0 if Guard is exactly half +// 1 if Guard is greater than half Number::Guard::Round Number::Guard::round() const noexcept { @@ -530,16 +458,13 @@ void Number::Guard::bringIntoRange(bool& negative, T& mantissa, int& exponent) { // Bring mantissa back into the minMantissa / maxMantissa range AFTER - // rounding. Mantissa should never be 0. - XRPL_ASSERT(mantissa != 0, "xrpl::Number::Guard::bringIntoRange : valid mantissa"); + // rounding if (mantissa < minMantissa) { mantissa *= 10; --exponent; } - // mantissa should never be 0, but if it _is_ make the result kZero. - if (exponent < kMinExponent || - (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && mantissa == 0)) + if (exponent < kMinExponent) { static constexpr Number kZero = Number{}; @@ -553,9 +478,7 @@ template void Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string location) { - pushOverflow(mantissa); - - auto const r = round(); + auto r = round(); if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1)) { auto const safeToIncrement = [this](auto const& mantissa) { @@ -572,27 +495,18 @@ Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string } else { - 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; - } + // 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 @@ -610,12 +524,6 @@ 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)); @@ -625,8 +533,6 @@ template void Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) { - // Do not pushOverflow here. - auto r = round(); if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330) { @@ -659,7 +565,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) +Number::Guard::doRound(rep& drops, std::string location) const { auto r = round(); if (r == Round::Up || (r == Round::Even && (drops & 1) == 1)) @@ -677,8 +583,6 @@ Number::Guard::doRound(rep& drops, std::string location) } ++drops; } - XRPL_ASSERT(drops >= 0, "xrpl::Number::Guard::doRound : positive magnitude"); - if (isNegative()) drops = -drops; } @@ -728,9 +632,7 @@ doNormalize( { static constexpr auto kMinExponent = Number::kMinExponent; static constexpr auto kMaxExponent = Number::kMaxExponent; - auto const repLimit = cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 - ? Number::kMaxRepUp - : Number::kMaxRep; + static constexpr auto kMaxRep = Number::kMaxRep; using Guard = Number::Guard; @@ -780,17 +682,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 > repLimit) + if (m > kMaxRep) { 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 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"); + // 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"); mantissa = m; g.doRoundUp(negative, mantissa, exponent, "Number::normalize 2"); @@ -922,9 +824,6 @@ 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. @@ -1009,7 +908,7 @@ Number::operator+=(Number const& y) } else { - if (xm > maxMantissa || xm > repLimit) + if (xm > maxMantissa || xm > kMaxRep) { g.doDropDigit(xm, xe); } @@ -1053,7 +952,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 <= repLimit) + while (xm < minMantissa && xm * 10 <= kMaxRep) { xm *= 10; xm -= g.pop(); @@ -1127,10 +1026,8 @@ 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 > repLimit) + while (zm > maxMantissa || zm > kMaxRep) { g.doDropDigit(zm, ze); } From 4124a1dd2dedccf1ddffd82c83396b400daea2d3 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 18 Jun 2026 20:16:39 -0400 Subject: [PATCH 74/77] Revert "Remove changes to Number.cpp" This reverts commit 8250aa2375a5478ad1423aca5865b8d778214b14. --- src/libxrpl/basics/Number.cpp | 205 +++++++++++++++++++++++++--------- 1 file changed, 154 insertions(+), 51 deletions(-) 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); } From 7cb224e50fd28829d1acd3323d72066ee33a550d Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 18 Jun 2026 20:59:20 -0400 Subject: [PATCH 75/77] Update some comments, and const correctness --- src/libxrpl/basics/Number.cpp | 46 ++++++++++++++++++--------------- src/test/basics/Number_test.cpp | 10 +++---- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 89b4e1dede..9222c2cb28 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -295,11 +295,11 @@ public: // Modify the result to the correctly rounded value template void - doRoundDown(bool& negative, T& mantissa, int& exponent); + doRoundDown(bool& negative, T& mantissa, int& exponent) const; // Modify the result to the correctly rounded value void - doRound(rep& drops, std::string location); + doRound(rep& drops, std::string location) const; private: template @@ -333,7 +333,7 @@ private: template void - bringIntoRange(bool& negative, T& mantissa, int& exponent); + bringIntoRange(bool& negative, T& mantissa, int& exponent) const; }; inline void @@ -425,15 +425,13 @@ Number::Guard::pushOverflow(T mantissa) // 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) + // The first step uses the digits _already_ in the Guard to possibly round the mantissa up. + // 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 don't want + // to bump up to kMaxRepUp. + if (mantissa % 10 < 9) { // Intentionally use integer math to get the largest value under the midpoint. auto constexpr kMidpoint = kMaxRep + (spread / 2); @@ -447,15 +445,15 @@ Number::Guard::pushOverflow(T mantissa) if (mantissa == kMaxRep) { - // If the mantissa ends up exactly kMaxRep, there's nothing more to do. + // If the mantissa ends up exactly kMaxRep, there's nothing more to do here. 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 + // The second step scales the final digit of the update mantissa proportionally, converting + // from (kMaxRep, kMaxRepUp) to (0 to 9]. It then pushes that scaled digit onto the guard as + // if it was a digit that got removed, but doesn't actually remove it. This method should be // 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: + // of the form 2^(2^x-1), 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 @@ -527,7 +525,7 @@ Number::Guard::round() const noexcept template void -Number::Guard::bringIntoRange(bool& negative, T& mantissa, int& exponent) +Number::Guard::bringIntoRange(bool& negative, T& mantissa, int& exponent) const { // Bring mantissa back into the minMantissa / maxMantissa range AFTER // rounding. Mantissa should never be 0. @@ -575,7 +573,9 @@ Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && mantissa > kMaxRep && mantissa < kMaxRepUp) { - // + // When rounding up a value in between kMaxRep, and kMaxRepUp, round to + // kMaxRepUp. Note that the decision for this rounding is dominated by the + // results of pushOverflow. mantissa = kMaxRepUp; } else @@ -614,6 +614,8 @@ Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && mantissa > kMaxRep && mantissa < kMaxRepUp) { + // When rounding down a value in between kMaxRep, and kMaxRepUp, round to kMaxRep. + // Note that the decision for this rounding is dominated by the results of pushOverflow. mantissa = kMaxRep; } bringIntoRange(negative, mantissa, exponent); @@ -623,7 +625,7 @@ Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string template void -Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) +Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) const { // Do not pushOverflow here. @@ -659,8 +661,10 @@ 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) +Number::Guard::doRound(rep& drops, std::string location) const { + // Do not pushOverflow here. + auto r = round(); if (r == Round::Up || (r == Round::Even && (drops & 1) == 1)) { diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index e9b03574c4..d19eac70f5 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -2574,7 +2574,7 @@ public: Number const below{static_cast(Number::kMaxRep), 0}; Number const above{false, Number::kMaxRepUp, 0, Number::Normalized{}}; - log << "Below: " << below << ", Above: " << above << std::endl; + log << "Below: " << below << ", Above: " << above << "\n"; auto const zeroPointFour = Number(4, -1); auto const zeroPointSix = Number(6, -1); @@ -2663,9 +2663,9 @@ public: ss << "kMaxRep + " << operand << " rounded " << to_string(mode) << " to " << actual << ". Expected: " << expectedValue; if (BEAST_EXPECTS(actual == expectedValue, ss.str())) - log << "\tSUCCESS: " << to_string(scale) << " " << ss.str() << std::endl; + log << "\tSUCCESS: " << to_string(scale) << " " << ss.str() << "\n"; } - log << std::endl; + log << "\n"; } // Subtraction cases test kMaxRepUp - Operand @@ -2720,9 +2720,9 @@ public: ss << "kMaxRepUp - " << operand << " rounded " << to_string(mode) << " to " << actual << ". Expected: " << expectedValue; if (BEAST_EXPECTS(actual == expectedValue, ss.str())) - log << "\tSUCCESS: " << to_string(scale) << " " << ss.str() << std::endl; + log << "\tSUCCESS: " << to_string(scale) << " " << ss.str() << "\n"; } - log << std::endl; + log << "\n"; } } From aadf3c66c9e6b10fdd702283f4d98a6cc84d1d57 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 18 Jun 2026 21:05:03 -0400 Subject: [PATCH 76/77] Remove some overly verbose unit test log messages. --- src/test/basics/Number_test.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index d19eac70f5..92b273eefd 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -2662,10 +2662,8 @@ public: std::stringstream ss; ss << "kMaxRep + " << operand << " rounded " << to_string(mode) << " to " << actual << ". Expected: " << expectedValue; - if (BEAST_EXPECTS(actual == expectedValue, ss.str())) - log << "\tSUCCESS: " << to_string(scale) << " " << ss.str() << "\n"; + BEAST_EXPECTS(actual == expectedValue, ss.str()); } - log << "\n"; } // Subtraction cases test kMaxRepUp - Operand @@ -2719,10 +2717,8 @@ public: std::stringstream ss; ss << "kMaxRepUp - " << operand << " rounded " << to_string(mode) << " to " << actual << ". Expected: " << expectedValue; - if (BEAST_EXPECTS(actual == expectedValue, ss.str())) - log << "\tSUCCESS: " << to_string(scale) << " " << ss.str() << "\n"; + BEAST_EXPECTS(actual == expectedValue, ss.str()); } - log << "\n"; } } From 08736639c5c17761c82ba889b20ab59c1a24ad80 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 22 Jun 2026 11:33:41 -0400 Subject: [PATCH 77/77] Restore an incorrectly removed const --- src/libxrpl/basics/Number.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 9222c2cb28..e79588ee78 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -465,7 +465,7 @@ Number::Guard::pushOverflow(T mantissa) // * For round toward zero, always rounds down to kMaxRep. auto const diff = mantissa - kMaxRep; - auto digit = (diff * 10) / spread; + auto const digit = (diff * 10) / spread; XRPL_ASSERT( digit > 0 && digit < 10 && digit != 5, "xrpl::Number::Guard::pushOverflow : valid overflow digit");