Compare commits

...

49 Commits

Author SHA1 Message Date
Ed Hennis
ea6f08d301 Merge branch 'develop' into ximinez/number-round-maxrep-down 2026-06-30 12:26:31 -04:00
Ed Hennis
9529fd97f6 Move MantissaScale NOLINTBEGIN/END to around the enum 2026-06-29 19:09:20 -04:00
Ed Hennis
72bc522fa7 Revert "fixup! Review feedback from @gregtatcam, plus a few extras"
This reverts commit 000cf9f489.
2026-06-29 19:08:03 -04:00
Ed Hennis
027c6f6e57 Merge branch 'develop' into ximinez/number-round-maxrep-down 2026-06-29 11:18:23 -04:00
Ed Hennis
394ea6b69c Merge branch 'develop' into ximinez/number-round-maxrep-down 2026-06-26 18:03:40 -04:00
Ed Hennis
4195c5b1ff Update include/xrpl/basics/Number.h
Bad constexpr

Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com>
2026-06-26 18:03:07 -04:00
Ed Hennis
000cf9f489 fixup! Review feedback from @gregtatcam, plus a few extras 2026-06-26 17:42:38 -04:00
Ed Hennis
2a5b1b4e17 Review feedback from @gregtatcam, plus a few extras
- Rename MantissaRange::Get to MantissaRange::Access. The name doesn't
  really matter, but since the intent of the class is to control access
  to one function, this works.
- Exclude the MantissaScale enum from linting for
  readability-enum-initial-value. clang-tidy was complaining that
  "error: initial values in enum 'xrpl::MantissaRange::MantissaScale'
  are not consistent, consider explicit initialization of all, none or
  only the first enumerator", but I don't care. I just need Large to
  match the last value.
- Add a static_assert in setCurrentTransactionRules, so that if another
  MantissaScale is added (and Large is properly updated), the engineer
  won't forget to add a case for it there.
2026-06-25 19:02:53 -04:00
Ed Hennis
4ff398ba2d Address AI review feedback
- Mostly just clarifying comments and reorganizing functions.
2026-06-25 15:31:51 -04:00
Ed Hennis
6aa9031848 Merge branch 'develop' into ximinez/number-round-maxrep-down 2026-06-25 11:43:30 -04:00
Ed Hennis
e090083dc7 Merge branch 'develop' into ximinez/number-round-maxrep-down 2026-06-23 11:09:59 -04:00
Ed Hennis
2c074d03c7 Merge branch 'develop' into ximinez/number-round-maxrep-down 2026-06-22 16:41:58 -04:00
Ed Hennis
d373cb5db8 clang-tidy: Remove unused header 2026-06-18 13:16:16 -04:00
Ed Hennis
14a43cfc35 Merge branch 'develop' into ximinez/number-round-maxrep-down 2026-06-17 21:46:24 -04:00
Ed Hennis
6504042b8d Review feedback from @TimothyBanks and @gregtatcam
- Get rid of MantissaRange::getRanges() function. Rewrite
  getMantissaScale to use all constexpr values.
- In Number::Guard::round(), document the "intentional" shadowing of
  "mode", and simplify one of the checks. Add an assert.
2026-06-17 21:45:56 -04:00
Ed Hennis
cebd4ecf15 Merge branch 'develop' into ximinez/number-round-maxrep-down 2026-06-17 15:01:35 -04:00
Ed Hennis
dbe34b4a29 Merge branch 'develop' into ximinez/number-round-maxrep-down 2026-06-16 18:56:13 -04:00
Ed Hennis
554bb62a65 Merge branch 'develop' into ximinez/number-round-maxrep-down 2026-06-15 20:31:43 -04:00
Ed Hennis
6bc267c112 Merge branch 'develop' into ximinez/number-round-maxrep-down 2026-06-15 15:28:55 -04:00
Ed Hennis
a8f4d790bd Clean up some comments, coverage exclusions, and an assert 2026-06-15 14:05:44 -04:00
Ed Hennis
bebddb0e3e Merge branch 'develop' into ximinez/number-round-maxrep-down 2026-06-15 12:12:10 -04:00
Ed Hennis
b30a70bbf8 Apply suggestions from AI code review
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-12 18:44:00 -04:00
Ed Hennis
353b3ae07d fixup! AI: Remove extranenous enum copy 2026-06-11 23:00:08 -04:00
Ed Hennis
b2790e2f50 AI: Remove extranenous enum copy
Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com>
2026-06-11 22:57:31 -04:00
Ed Hennis
29d017c2a3 Change placeholder "fixNumberStuff" to "fixCleanup3_3_0" 2026-06-11 22:02:50 -04:00
Ed Hennis
764be7f20f Merge remote-tracking branch 'XRPLF/develop' into ximinez/number-round-maxrep-down
* XRPLF/develop:
  test: Add null check unit test for `Oracle::aggregatePrice` (7306)
  ci: Patch conan recipe for Nix to be able to use on macOS (7532)
  ci: Run sanitizers on release builds too (7527)
  fix: Correct hybrid offer deletion on credential expiry (6843)
  ci: Make sanitizer flags lists in the profile, not a string (7449)
  ci: Make configurations launch on certain event types (7447)
  fix: Add [[maybe_unused]] to fix320Enabled for assert=OFF builds (7446)
  ci: Add `gh` and `file` to nix packages (7444)
  fix: Disable transaction invariants (7409)
  perf: Dispatch "hasInvalidAmount()" on type tag instead of dynamic_cast (7402)
  refactor: Retire fixUniversalNumber amendment (5962)
  test: Do not create data directory for memory databases (7323)
  ci: Launch upload-conan-deps on profile change (7442)
2026-06-11 19:39:47 -04:00
Ed Hennis
5703ca527f 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.
2026-06-11 19:38:21 -04:00
Ed Hennis
a1cfa89e15 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.
2026-06-11 19:38:21 -04:00
Ed Hennis
318c2c2dd3 Also fix local 3_2_0 variable names 2026-06-11 19:38:21 -04:00
Ed Hennis
5c62c15ad8 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.
2026-06-11 19:38:20 -04:00
Ed Hennis
772e0c30f7 clang-tidy: rename MantissaScale enums from "3_2_0" to "320" 2026-06-11 19:38:20 -04:00
Ed Hennis
182ca1c12f Clean up the "New" names 2026-06-11 19:38:20 -04:00
Ed Hennis
2e97056b40 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.
2026-06-11 19:38:03 -04:00
Ed Hennis
693e9015ab clang-tidy: Guard public member variable names; Missing include 2026-06-11 19:38:01 -04:00
Ed Hennis
1162ccf7f4 Clean up tests 2026-06-11 19:37:57 -04:00
Ed Hennis
07ae5fa867 Reorganize the subtraction tests 2026-06-11 19:37:56 -04:00
Ed Hennis
6cc45297d7 Fix formatting, add an assert 2026-06-11 19:37:56 -04:00
Ed Hennis
b263f442be Revert "Rollback Number class changes; show the fix works without side effects"
This reverts commit 8743be8eae.
2026-06-11 19:37:56 -04:00
Ed Hennis
7191574499 Rollback Number class changes; show the fix works without side effects 2026-06-11 19:37:56 -04:00
Ed Hennis
e77b154edc Include rounding in failed unit tests 2026-06-11 19:37:56 -04:00
Ed Hennis
184f936362 Improve comment descriptions 2026-06-11 19:37:55 -04:00
Ed Hennis
64cb53629d 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.
2026-06-11 19:37:55 -04:00
Ed Hennis
8ca90e7d01 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.
2026-06-11 19:37:55 -04:00
Ed Hennis
0a24023797 clang-tidy: template param names, const correctness, braces 2026-06-11 19:37:55 -04:00
Ed Hennis
73bd964917 Remove the kMaxRep+1 rounding tests 2026-06-11 19:37:54 -04:00
Ed Hennis
48e0ca72b0 Improve accuracy of Number::operator+=
- Use more of the available range of the uint128 operands.
- Also refactor Number::Guard::round() to return an enum.
2026-06-11 19:37:54 -04:00
Ed Hennis
6d89fbef7a Experimental: Scale addition operands up to preserve accuracy 2026-06-11 19:37:54 -04:00
Ed Hennis
b624b2eeae Include upward, write tests based on "expected" behavior 2026-06-11 19:37:54 -04:00
Ed Hennis
f255ef6214 test: Add another rounding unit test 2026-06-11 19:37:53 -04:00
6 changed files with 968 additions and 352 deletions

View File

@@ -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<std::uint64_t, kInt64Digits>
constexpr std::size_t kUint64Digits = 20;
[[maybe_unused]] constexpr std::size_t kUint128Digits = 39;
template <typename T, std::size_t Digits>
consteval std::array<T, Digits>
buildPowersOfTen()
{
std::array<std::uint64_t, kInt64Digits> result{};
std::array<T, Digits> 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<std::uint64_t>::max() / 10)
if (power > std::numeric_limits<T>::max() / 10)
throw std::logic_error("Power of 10 table is too big");
}
result[exponent] = power;
if (power < std::numeric_limits<std::uint64_t>::max() / 10)
throw std::logic_error("Power of 10 table is not big enough for the uint64_t type");
if (power < std::numeric_limits<T>::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<std::uint64_t, detail::kInt64Digits> kPowerOfTen = detail::buildPowersOfTen();
template <typename T = std::uint64_t, std::size_t Digits = detail::kUint64Digits>
constexpr std::array<T, Digits> kPowerOfTenImpl = detail::buildPowersOfTen<T, Digits>();
constexpr auto kPowerOfTen = kPowerOfTenImpl<std::uint64_t, detail::kUint64Digits>;
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.
*
@@ -120,17 +126,37 @@ struct MantissaRange final
{
using rep = std::uint64_t;
// NOLINTBEGIN(readability-enum-initial-value)
// The values don't matter, except for Large
enum class MantissaScale {
// Small can be removed when either featureSingleAssetVault or featureLendingProtocol are
// retired
Small,
// LargeLegacy can be removed when fixCleanup3_2_0 is retired
LargeLegacy,
Large,
// Large320 can be removed when fixCleanup3_3_0 is retired
Large320,
// If Large330 is ever the only remaining "Large*" entry, it can be renamed to just "Large".
Large330,
// Large is a de-facto alias for "the latest", and is only here for backward compatibility
// in the extremely unlikely case that a downstream project made use of it. Note that
// because the behavior changed, this may still be a breaking change.
Large = Large330,
};
// NOLINTEND(readability-enum-initial-value)
// This entire enum can be removed when fixCleanup3_2_0 is retired
enum class CuspRoundingFix : bool {
Disabled = false,
Enabled = true,
// This entire enum can be removed when the last relevant amendment is retired
enum class CuspRoundingFix : std::uint8_t {
// Disabled can be removed when fixCleanup3_2_0 is retired
Disabled = 0,
// Enabled320 can be removed when fixCleanup3_3_0 is retired
Enabled320 = 1,
// If we ever get to the point that there's only one entry, remove the entire enum
Enabled330 = 2,
// Enabled is a de-facto alias for "the latest", and is only here for backward compatibility
// in the extremely unlikely case that a downstream project made use of it. Note that
// because the behavior changed, this may still be a breaking change.
Enabled = Enabled330,
};
explicit constexpr MantissaRange(MantissaScale sc) : scale(sc)
@@ -141,13 +167,27 @@ 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)};
static MantissaRange const&
getMantissaRange(MantissaScale scale);
CuspRoundingFix const cuspRoundingFix{isCuspFixEnabled(scale)};
static std::set<MantissaScale> const&
getAllScales();
getAllScales()
{
static std::set<MantissaRange::MantissaScale> const kScales = {
MantissaRange::MantissaScale::Small,
MantissaRange::MantissaScale::LargeLegacy,
MantissaRange::MantissaScale::Large320,
MantissaRange::MantissaScale::Large330,
};
return kScales;
}
class Access
{
static constexpr MantissaRange const&
mantissaRange(MantissaScale scale);
friend Number;
};
private:
static constexpr int
@@ -158,7 +198,8 @@ private:
case MantissaScale::Small:
return 15;
case MantissaScale::LargeLegacy:
case MantissaScale::Large:
case MantissaScale::Large320:
case MantissaScale::Large330:
return 18;
// LCOV_EXCL_START
default:
@@ -187,17 +228,16 @@ private:
case MantissaScale::Small:
case MantissaScale::LargeLegacy:
return CuspRoundingFix::Disabled;
case MantissaScale::Large:
return CuspRoundingFix::Enabled;
case MantissaScale::Large320:
return CuspRoundingFix::Enabled320;
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.
throw std::runtime_error("Unknown mantissa scale"); // LCOV_EXCL_LINE
}
}
static std::unordered_map<MantissaScale, MantissaRange> const&
getRanges();
};
// Like std::integral, but only 64-bit integral types.
@@ -550,9 +590,15 @@ private:
// changing the values inside the range.
static thread_local std::reference_wrapper<MantissaRange const> 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
@@ -567,7 +613,7 @@ private:
int& exponent,
internalrep const& minMantissa,
internalrep const& maxMantissa,
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled);
MantissaRange::CuspRoundingFix cuspRoundingFix);
template <class T>
friend void
@@ -577,7 +623,7 @@ private:
int& exponent,
MantissaRange::rep const& minMantissa,
MantissaRange::rep const& maxMantissa,
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled,
MantissaRange::CuspRoundingFix cuspRoundingFix,
bool dropped);
[[nodiscard]] bool
@@ -595,8 +641,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
@@ -858,21 +902,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::Large:
return "large";
default:
throw std::runtime_error("Bad scale");
}
}
std::string
to_string(MantissaRange::MantissaScale const& scale);
std::string
to_string(Number::RoundingMode const& round);
class SaveNumberRoundMode
{

View File

@@ -14,7 +14,6 @@
#include <stdexcept>
#include <string>
#include <type_traits>
#include <unordered_map>
#include <utility>
#ifdef _MSC_VER
@@ -31,74 +30,103 @@ namespace xrpl {
thread_local Number::RoundingMode Number::mode = Number::RoundingMode::ToNearest;
thread_local std::reference_wrapper<MantissaRange const> Number::kRange =
MantissaRange::getMantissaRange(MantissaRange::MantissaScale::Large);
MantissaRange::Access::mantissaRange(MantissaRange::MantissaScale::Large330);
std::set<MantissaRange::MantissaScale> const&
MantissaRange::getAllScales()
std::string
to_string(MantissaRange::MantissaScale const& scale)
{
static std::set<MantissaRange::MantissaScale> const kScales = {
MantissaRange::MantissaScale::Small,
MantissaRange::MantissaScale::LargeLegacy,
MantissaRange::MantissaScale::Large,
};
return kScales;
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"); // LCOV_EXCL_LINE
}
}
std::unordered_map<MantissaRange::MantissaScale, MantissaRange> const&
MantissaRange::getRanges()
std::string
to_string(Number::RoundingMode const& round)
{
static auto const kMap = []() {
std::unordered_map<MantissaScale, MantissaRange> 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 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::kMaxRep);
static_assert(kRange.max < Number::kMaxRep);
static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Disabled);
}
{
[[maybe_unused]]
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::kMaxRep);
static_assert(kRange.max > Number::kMaxRep);
static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Disabled);
}
{
[[maybe_unused]]
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.cuspRoundingFixEnabled == CuspRoundingFix::Enabled);
}
return map;
}();
return kMap;
switch (round)
{
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"); // LCOV_EXCL_LINE
}
}
MantissaRange const&
MantissaRange::getMantissaRange(MantissaScale scale)
constexpr MantissaRange const&
MantissaRange::Access::mantissaRange(MantissaScale scale)
{
return getRanges().at(scale);
static constexpr MantissaRange kSmall{MantissaScale::Small};
static constexpr MantissaRange kLegacy{MantissaScale::LargeLegacy};
static constexpr MantissaRange kLarge320{MantissaScale::Large320};
static constexpr MantissaRange kLarge330{MantissaScale::Large330};
switch (scale)
{
case MantissaScale::Small:
return kSmall;
case MantissaScale::LargeLegacy:
return kLegacy;
case MantissaScale::Large320:
return kLarge320;
case MantissaScale::Large330:
return kLarge330;
}
throw std::logic_error("Unknown mantissa scale");
// static_asserts are checked at compile time, so it doesn't matter where in the function they
// are located. For readability of the main body, put them after it.
// Small
static_assert(isPowerOfTen(kSmall.min));
static_assert(kSmall.min == 1'000'000'000'000'000LL);
static_assert(kSmall.max == 9'999'999'999'999'999LL);
static_assert(kSmall.log == 15);
static_assert(kSmall.min < Number::kMaxRep);
static_assert(kSmall.max < Number::kMaxRep);
static_assert(kSmall.cuspRoundingFix == CuspRoundingFix::Disabled);
// LargeLegacy
static_assert(isPowerOfTen(kLegacy.min));
static_assert(kLegacy.min == 1'000'000'000'000'000'000ULL);
static_assert(kLegacy.max == rep(9'999'999'999'999'999'999ULL));
static_assert(kLegacy.log == 18);
static_assert(kLegacy.min < Number::kMaxRep);
static_assert(kLegacy.max > Number::kMaxRep);
static_assert(kLegacy.cuspRoundingFix == CuspRoundingFix::Disabled);
// Large320
static_assert(isPowerOfTen(kLarge320.min));
static_assert(kLarge320.min == 1'000'000'000'000'000'000ULL);
static_assert(kLarge320.max == rep(9'999'999'999'999'999'999ULL));
static_assert(kLarge320.log == 18);
static_assert(kLarge320.min < Number::kMaxRep);
static_assert(kLarge320.max > Number::kMaxRep);
static_assert(kLarge320.cuspRoundingFix == CuspRoundingFix::Enabled320);
// Large330
static_assert(isPowerOfTen(kLarge330.min));
static_assert(kLarge330.min == 1'000'000'000'000'000'000ULL);
static_assert(kLarge330.max == rep(9'999'999'999'999'999'999ULL));
static_assert(kLarge330.log == 18);
static_assert(kLarge330.min < Number::kMaxRep);
static_assert(kLarge330.max > Number::kMaxRep);
static_assert(kLarge330.cuspRoundingFix == CuspRoundingFix::Enabled330);
}
Number::RoundingMode
@@ -124,7 +152,7 @@ Number::setMantissaScale(MantissaRange::MantissaScale scale)
{
if (!MantissaRange::getAllScales().contains(scale))
logicError("Unknown mantissa scale");
kRange = MantissaRange::getMantissaRange(scale);
kRange = MantissaRange::Access::mantissaRange(scale);
}
// Optimization equivalent to:
@@ -155,15 +183,38 @@ 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 <class T>
concept UnsignedMantissa = std::is_unsigned_v<T> || std::is_same_v<T, uint128_t>;
/** 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 its 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. (remaining bytes of digits_
and 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 "AwayFromZero".
* TowardZero is the easiest rounding mode. It always rounds down. digits_ and xbit_ are
irrelevant.
* AwayFromZero 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
@@ -171,7 +222,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 +259,10 @@ public:
unsigned
pop() noexcept;
// if true, there are no digits in the guard, including dropped digits (xbit_)
[[nodiscard]] bool
empty() const noexcept;
/** Drop a digit from the mantissa, and increment the exponent, storing the dropped digit in
* this Guard.
*
@@ -206,28 +275,37 @@ 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]] int
[[nodiscard]] Round
round() const noexcept;
// Modify the result to the correctly rounded value
template <UnsignedMantissa T>
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 <UnsignedMantissa T>
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 +317,7 @@ private:
template <UnsignedMantissa T>
void
bringIntoRange(bool& negative, T& mantissa, int& exponent, internalrep const& minMantissa);
bringIntoRange(bool& negative, T& mantissa, int& exponent);
};
inline void
@@ -289,6 +367,12 @@ Number::Guard::pop() noexcept
return d;
}
inline bool
Number::Guard::empty() const noexcept
{
return digits_ == 0 && !xbit_;
}
template <class T>
void
Number::Guard::doDropDigit(T& mantissa, int& exponent) noexcept
@@ -314,50 +398,52 @@ Number::Guard::doDropDigit<uint128_t>(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();
// Local "mode" shadows and has the same value as the static thread_local "Number::mode".
// This ensures the overhead of loading the thread_local is only incurred once.
auto const mode = Number::getround();
if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && empty())
{
// No remainder
return Round::Exact;
}
if (mode == RoundingMode::TowardsZero)
return -1;
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 1;
}
return -1;
return Round::Down;
}
if (mode == RoundingMode::Upward)
// Away from Zero. Since we checked sbit_ in the previous block, we don't need to check it
// again.
if (mode == RoundingMode::Downward || mode == RoundingMode::Upward)
{
if (sbit_)
return -1;
if (digits_ > 0 || xbit_)
return 1;
return -1;
if (empty())
return Round::Down;
return Round::Up;
}
XRPL_ASSERT(
mode == RoundingMode::ToNearest, "xrpl::Number::Guard::Round : fallthrough to ToNearest");
// 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 <UnsignedMantissa T>
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
@@ -378,22 +464,15 @@ Number::Guard::bringIntoRange(
template <UnsignedMantissa T>
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) {
auto const safeToIncrement = [this](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".
@@ -414,14 +493,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;
}
}
@@ -440,30 +512,43 @@ Number::Guard::doRoundUp(
}
}
}
bringIntoRange(negative, mantissa, exponent, minMantissa);
bringIntoRange(negative, mantissa, exponent);
if (exponent > kMaxExponent)
Throw<std::overflow_error>(std::string(location));
}
template <UnsignedMantissa T>
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::Enabled330)
{
--mantissa;
if (mantissa < minMantissa)
// 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,
"xrpl::Number::Guard::doRoundDown : mantissa is expected size");
if (r != Round::Exact)
{
mantissa *= 10;
--exponent;
--mantissa;
}
}
bringIntoRange(negative, mantissa, exponent, minMantissa);
else
{
// Need to preserve the incorrect behavior until the fix amendment can be retired,
// because otherwise would risk an unplanned ledger fork.
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 +556,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 +615,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 +638,7 @@ doNormalize(
m *= 10;
--exponent;
}
Guard g;
Guard g(minMantissa, maxMantissa, cuspRoundingFix);
if (negative)
g.setNegative();
if (dropped)
@@ -598,14 +683,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 +698,12 @@ Number::normalize<uint128_t>(
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 +715,12 @@ Number::normalize<unsigned long long>(
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 +732,27 @@ Number::normalize<unsigned long>(
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,46 +806,102 @@ 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.
auto const upperLimit = static_cast<uint128_t>(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 nothing excessive.
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);
}
auto const& range = kRange.get();
auto const& minMantissa = range.min;
auto const& maxMantissa = range.max;
auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled;
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,
minMantissa,
maxMantissa,
cuspRoundingFixEnabled,
"Number::addition overflow");
}
else
{
@@ -772,19 +915,67 @@ Number::operator+=(Number const& y)
xe = ye;
xn = yn;
}
while (xm < minMantissa && xm * 10 <= kMaxRep)
if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330)
{
xm *= 10;
xm -= g.pop();
--xe;
// 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");
}
g.doRoundDown(xn, xm, xe, minMantissa);
else
{
// 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;
xm -= g.pop();
--xe;
}
}
// 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,
cuspRoundingFix == MantissaRange::CuspRoundingFix::Enabled330 && !g.empty());
negative_ = xn;
mantissa_ = static_cast<internalrep>(xm);
exponent_ = xe;
normalize(range);
XRPL_ASSERT(isnormal(), "xrpl::Number::operator+= : result is normal");
return *this;
}
@@ -818,14 +1009,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 +1022,12 @@ Number::operator*=(Number const& y)
xm = static_cast<internalrep>(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 +1063,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
@@ -930,7 +1111,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.
@@ -1014,14 +1195,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<internalrep>(zm);
exponent_ = ze;
@@ -1035,7 +1216,7 @@ operator rep() const
{
rep drops = mantissa();
int offset = exponent();
Guard g;
Guard g(kRange);
if (drops != 0)
{
if (negative_)

View File

@@ -39,22 +39,30 @@ setCurrentTransactionRules(std::optional<Rules> 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]() {
if (enableVaultNumbers)
auto const range = [&r]() {
// If any new conditions with new amendments are added to "enableLargeNumbers", those
// amendments must also be added to useRulesGuards.
bool const enableLargeNumbers =
!r || (r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol));
// If enableLargeNumbers is true, then useRulesGuard must also return true.
// However, the reverse is not true. Other amendments can cause the rules guard to be used,
// even though large numbers are _not_ used.
XRPL_ASSERT(
!r || !enableLargeNumbers || useRulesGuards(*r),
"setCurrentTransactionRules : rule decisions match");
if (enableLargeNumbers)
{
if (enableCuspRoundingFix)
static_assert(
MantissaRange::MantissaScale::Large == MantissaRange::MantissaScale::Large330);
if (!r || r->enabled(fixCleanup3_3_0))
{
return MantissaRange::MantissaScale::Large;
return MantissaRange::MantissaScale::Large330;
}
if (r->enabled(fixCleanup3_2_0))
{
return MantissaRange::MantissaScale::Large320;
}
return MantissaRange::MantissaScale::LargeLegacy;
}
@@ -69,14 +77,14 @@ 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.
// superset of the list used to determine "enableLargeNumbers" 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);
return rules.enabled(featureSingleAssetVault) || rules.enabled(featureLendingProtocol) ||
rules.enabled(fixCleanup3_2_0) || rules.enabled(fixCleanup3_3_0);
}
void
@@ -87,7 +95,8 @@ createGuards(
{
if (useRulesGuards(rules))
{
// raii classes for the current ledger rules.
// raii classes for the current ledger rules. If the rules are set, the MantissaRange will
// be updated, too.
rulesGuard.emplace(rules);
}
else

View File

@@ -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
}
}

View File

@@ -5059,11 +5059,10 @@ class Invariants_test : public beast::unit_test::Suite
std::vector<ValidVault::DeltaInfo> 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 {

View File

@@ -46,6 +46,22 @@ class Number_test : public beast::unit_test::Suite
return out;
}
BigInt
toBigInt(Number const& n)
{
BigInt v = n.mantissa();
auto e = n.exponent();
for (; e > 0; --e)
v *= 10;
for (; e < 0; ++e)
{
BEAST_EXPECT(v % 10 == 0);
v /= 10;
}
return v;
}
using dec = boost::multiprecision::cpp_dec_float_50;
template <class T = dec>
@@ -172,28 +188,35 @@ public:
auto const scale = Number::getMantissaScale();
testcase << "test_add " << to_string(scale);
using Case = std::tuple<Number, Number, Number>;
auto const cSmall = std::to_array<Case>(
{{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'000, -15},
Number{-6'555'555'555'555'555, -29},
Number{-1'000'000'000'000'066, -15}},
{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{-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{5'555'555'555'555'555, -32768},
Number{-5'555'555'555'555'554, -32768},
Number{0}},
{Number{-9'999'999'999'999'999, -31},
Number{1'000'000'000'000'000, -15},
Number{9'999'999'999'999'990, -16}}});
using Case = std::tuple<Number, Number, Number, int>;
auto const cSmall = std::to_array<Case>({
{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<Case>(
// Note that items with extremely large mantissas need to be
// calculated, because otherwise they overflow uint64. Items from C
@@ -201,45 +224,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
@@ -247,6 +282,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
@@ -257,21 +293,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<Case>({
{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<Case>({
{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)
@@ -311,21 +351,28 @@ public:
auto const scale = Number::getMantissaScale();
testcase << "test_sub " << to_string(scale);
using Case = std::tuple<Number, Number, Number>;
using Case = std::tuple<Number, Number, Number, int>;
auto const cSmall = std::to_array<Case>(
{{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<Case>(
// Note that items with extremely large mantissas need to be
// calculated, because otherwise they overflow uint64. Items from C
@@ -333,49 +380,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)
@@ -1340,8 +1401,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");
@@ -1379,9 +1439,6 @@ public:
test(
-(Number{std::numeric_limits<std::int64_t>::max(), 0} + 1),
"-9223372036854775810");
break;
default:
BEAST_EXPECT(false);
}
}
@@ -1739,9 +1796,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 const storedValue = toBigInt(product);
BigInt const signedDifference = storedValue - exactProduct;
@@ -1758,7 +1813,8 @@ public:
switch (scale)
{
case MantissaRange::MantissaScale::Large:
case MantissaRange::MantissaScale::Large320:
case MantissaRange::MantissaScale::Large330:
BEAST_EXPECT(signedDifference >= 0);
BEAST_EXPECT(signedDifference < pow10<BigInt>(product.exponent()));
BEAST_EXPECT(
@@ -1840,7 +1896,8 @@ public:
// Upward invariant: stored >= exact. Bug: stored < exact.
switch (scale)
{
case MantissaRange::MantissaScale::Large:
case MantissaRange::MantissaScale::Large320:
case MantissaRange::MantissaScale::Large330:
BEAST_EXPECT(stored >= exact);
BEAST_EXPECT(diff < pow10(quotient.exponent()));
break;
@@ -1890,7 +1947,8 @@ public:
// invariant: stored <= exact. Bug: stored > exact.
switch (scale)
{
case MantissaRange::MantissaScale::Large:
case MantissaRange::MantissaScale::Large320:
case MantissaRange::MantissaScale::Large330:
BEAST_EXPECT(stored <= exact);
BEAST_EXPECT(diff > -pow10(quotient.exponent()));
break;
@@ -1947,7 +2005,8 @@ public:
// invariant: stored >= exact. Bug: stored < exact.
switch (scale)
{
case MantissaRange::MantissaScale::Large:
case MantissaRange::MantissaScale::Large320:
case MantissaRange::MantissaScale::Large330:
BEAST_EXPECT(stored >= exact);
BEAST_EXPECT(diff < pow10(quotient.exponent()));
break;
@@ -1964,6 +2023,340 @@ public:
break;
}
}
{
auto const exp = Number::mantissaLog();
// SubCase is <offset, extraB, aString, bString>
// * 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<int, bool, std::string, std::string>;
auto const c = std::to_array<SubCase>({
{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)
{
testcase << "subtraction rounding. offset: " << offset
<< ", scale: " << to_string(scale);
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);
BEAST_EXPECT(bigA == BigInt{aString});
BEAST_EXPECT(bigB == BigInt{bString});
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));
};
BigInt const exact = bigA + bigB;
auto const sums = [&]() {
std::map<Number::RoundingMode, std::pair<BigInt, Number>> 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();
auto const expectedExponent =
offset - (scale == MantissaRange::MantissaScale::Small && extraB ? 1 : 0);
auto const epsilon = pow10<BigInt>(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:
case MantissaRange::MantissaScale::Large320: {
// 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_EXPECTS(
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);
}
}
}
}
}
}
}
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 <y, expected q>
using Case = std::pair<Number, std::int64_t>;
auto const c = std::to_array<Case>({
{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<std::int64_t>(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
@@ -1972,6 +2365,7 @@ public:
for (auto const scale : MantissaRange::getAllScales())
{
NumberMantissaScaleGuard const sg(scale);
testZero();
testLimits();
testToString();
@@ -1995,6 +2389,8 @@ public:
testInt64();
testUpwardRoundsDown();
testNumberAddDirectedSignWrong();
testNumberAddToNearestPicksFarther();
}
}
};