Compare commits

...

88 Commits

Author SHA1 Message Date
Ed Hennis
121786aada Merge remote-tracking branch 'XRPLF/ximinez/number-round-maxrep-down' into ximinez/number-round-maxrep
* XRPLF/ximinez/number-round-maxrep-down:
  Fix formatting, add an assert
2026-06-06 16:02:59 -04:00
Ed Hennis
d7ce0e2dd3 Fix formatting, add an assert 2026-06-06 15:38:29 -04:00
Ed Hennis
b2cea736d0 Merge branch 'ximinez/number-round-maxrep-down' into ximinez/number-round-maxrep
* ximinez/number-round-maxrep-down:
  Revert "Rollback Number class changes; show the fix works without side effects"
  Rollback Number class changes; show the fix works without side effects
  Include rounding in failed unit tests
  Improve comment descriptions
  Rework subtraction rounding (again) for more accuracy
  build: Create single test binary xrpl_tests (7327)
  ci: [DEPENDABOT] bump actions/checkout from 6.0.2 to 6.0.3 (7414)
  ci: Refactor build-related nix / docker / workflows (7408)
  refactor: Construct Number::Guard from MantissaRange or relevant fields
  ci: Use multiple directories in dependabot config (7413)
  ci: Update clang-tidy to nix-based v22 (7412)
  clang-tidy: template param names, const correctness, braces
2026-06-06 15:25:56 -04:00
Ed Hennis
c165af497e Revert "Rollback Number class changes; show the fix works without side effects"
This reverts commit 8743be8eae.
2026-06-06 14:35:35 -04:00
Ed Hennis
8743be8eae Rollback Number class changes; show the fix works without side effects 2026-06-06 14:34:31 -04:00
Ed Hennis
3a5c85067a Include rounding in failed unit tests 2026-06-06 14:33:31 -04:00
Ed Hennis
2f701121b4 Merge remote-tracking branch 'upstream/develop' into ximinez/number-round-maxrep-down
* upstream/develop:
  build: Create single test binary xrpl_tests (7327)
  ci: [DEPENDABOT] bump actions/checkout from 6.0.2 to 6.0.3 (7414)
  ci: Refactor build-related nix / docker / workflows (7408)
  ci: Use multiple directories in dependabot config (7413)
  ci: Update clang-tidy to nix-based v22 (7412)
2026-06-06 14:04:24 -04:00
Ed Hennis
961ac6671e Improve comment descriptions 2026-06-06 13:09:52 -04:00
Ed Hennis
012c67a7eb 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-06 01:04:26 -04:00
Ed Hennis
74c66d0944 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-05 12:06:41 -04:00
Ed Hennis
f1bb4ded21 clang-tidy: template param names, const correctness, braces 2026-06-04 20:05:53 -04:00
Ed Hennis
e1295d1ce8 Merge branch 'ximinez/number-round-maxrep-down' into ximinez/number-round-maxrep 2026-06-04 19:03:43 -04:00
Ed Hennis
aa8888732e Merge branch 'develop' into ximinez/number-round-maxrep-down 2026-06-04 19:03:32 -04:00
Ed Hennis
50c0d9f2b0 Handle a whole bunch of edge cases
- Add more tests
2026-06-04 18:53:50 -04:00
Ed Hennis
b5574baa36 Add line numbers to Number::to_string test 2026-06-04 14:40:39 -04:00
Ed Hennis
015d9a6cb9 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.
2026-06-04 14:14:01 -04:00
Ed Hennis
51902cd6b4 Revert "Remove the kMaxRep+1 rounding tests"
This reverts commit c84939ccea.
2026-06-02 16:03:08 -04:00
Ed Hennis
c84939ccea Remove the kMaxRep+1 rounding tests 2026-06-02 15:59:26 -04:00
Ed Hennis
9e8c3caef4 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-02 15:35:42 -04:00
Ed Hennis
35bee87909 Experimental: Scale addition operands up to preserve accuracy 2026-06-01 21:52:31 -04:00
Ed Hennis
261508a0ec Merge remote-tracking branch 'XRPLF/develop' into ximinez/number-round-maxrep-down
* XRPLF/develop:
  ci: Check binaries separately from building them (7355)
  ci: [DEPENDABOT] bump eps1lon/actions-label-merge-conflict from 3.0.3 to 3.1.0 (7375)
  refactor: Use `STLedgerEntry` type aliases instead of `std::shared_ptr` (7282)
  fix: Adjust xrpld systemd service and update timer (7374)
  release: Bump version to 3.2.0-rc3 (7371)
  fix: Pin overpayment principal reduction to exact on-grid value (7360)
  fix: Improve upward rounding edge cases for Number::operator/= (7328)
  refactor: Revert "perf: Remove unnecessary caches (5439)" (7359)
  fix: Add zero domainID check for permissionedDomain (7362)
2026-06-01 15:33:34 -04:00
Ed Hennis
9f872f2179 Include upward, write tests based on "expected" behavior 2026-05-29 19:46:50 -04:00
Ed Hennis
d1af39d9dd test: Add another rounding unit test 2026-05-29 19:03:34 -04:00
Ed Hennis
f6a26ca34f CI feedback: Add test cases covering other rounding modes
- Downward with a negative result, and ToNearest with a remainder
  slightly larger than 0.5.
2026-05-29 18:54:08 -04:00
Ed Hennis
c0569037f8 Apply suggestions from code review
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-29 16:34:51 -04:00
Ed Hennis
be9ae88d48 Accept AI suggestion
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-28 23:38:54 -04:00
Ed Hennis
cd21d74538 Update the testUpwardRoundsDown test to run under all scales
- Demonstrates the incorrect "before" behavior
2026-05-28 19:58:52 -04:00
Ed Hennis
2fdfd2b420 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.
2026-05-28 18:41:19 -04:00
Ed Hennis
06a3f76ccd Skip clang-tidy false positive: misc-include-cleaner 2026-05-28 17:47:41 -04:00
Ed Hennis
dadf4d737d Merge branch 'develop' into ximinez/number-division-accuracy 2026-05-28 17:46:40 -04:00
Ed Hennis
7b66b42713 Merge branch 'develop' into ximinez/number-division-accuracy 2026-05-27 17:00:59 -04:00
Ed Hennis
18ac8a0583 Merge branch 'develop' into ximinez/number-division-accuracy 2026-05-27 15:18:17 -04:00
Ed Hennis
de2efa5cb9 Remove TMax entirely from normalizeToRange; check type matching directly 2026-05-27 13:06:52 -04:00
Ed Hennis
8dcd88e83c Tweak how the denominator is handled in division
- Removes one int64 to 128 conversion
2026-05-27 12:34:49 -04:00
Ed Hennis
5333422402 Merge remote-tracking branch 'XRPLF/develop' into ximinez/number-division-accuracy
* XRPLF/develop:
  fix: Fix a rounding error at the `Number::maxRep` cusp (7051)
2026-05-27 12:17:30 -04:00
Ed Hennis
4ec049e727 Minor fixes: missing include, variable init, typo 2026-05-27 12:09:13 -04:00
Ed Hennis
ae9c72bb7c Test optimization: Number_test::pow10 2026-05-27 00:36:38 -04:00
Ed Hennis
5abecb9fcb 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.
2026-05-27 00:07:48 -04:00
Ed Hennis
12670b0c3f Merge branch 'ximinez/number-fix-maxrepcusp' into ximinez/number-division-accuracy 2026-05-26 19:21:05 -04:00
Ed Hennis
1e7876a03c Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-26 19:21:01 -04:00
Ed Hennis
e851e80de0 Merge branch 'ximinez/number-fix-maxrepcusp' into ximinez/number-division-accuracy 2026-05-26 16:56:47 -04:00
Ed Hennis
a963035f76 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-26 16:56:43 -04:00
Ed Hennis
8ab904de57 Merge branch 'ximinez/number-fix-maxrepcusp' into ximinez/number-division-accuracy 2026-05-26 16:01:44 -04:00
Ed Hennis
100ec464d9 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-26 16:01:40 -04:00
Ed Hennis
e89e6f50e8 Merge remote-tracking branch 'XRPLF/ximinez/number-fix-maxrepcusp' into ximinez/number-division-accuracy
* XRPLF/ximinez/number-fix-maxrepcusp:
  clang-tidy: implicit bool conversion
  Address some AI review feedback: predeclare, include, format, comment
  fix: Fix `VaultInvariant` and `VaultDeposit` precision bugs at IOU scale boundaries (7272)
  ci: Add clang to nix images (7308)
  fix: Include management-fee delta in doOverpayment assertion (7039)
  fix: Fix clang-tidy pre-commit hook to locate compile_commands.json from repo root (7325)
  fix: Use consistent scale for `debtTotal` (7093)
  fix: Skip deleted book directories and non-root modifications in `ValidBookDirectory` invariant (7312)
  fix: Address review feedback on FD/handle guarding (5823 follow-up) (7310)
  fix: Fix non-canonical MPT amount (7117)
2026-05-26 15:53:36 -04:00
Ed Hennis
27456fa439 Use the local range instead of calling a function 2026-05-26 15:52:25 -04:00
Ed Hennis
d6844311c0 clang-tidy: missing header 2026-05-26 15:48:29 -04:00
Ed Hennis
fbee0349f5 clang-tidy: implicit bool conversion 2026-05-26 15:21:42 -04:00
Ed Hennis
84ca271d95 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
2026-05-26 13:51:06 -04:00
Ed Hennis
75dfc65f5f Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-26 13:47:33 -04:00
Ed Hennis
48b1716e6f 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.)
2026-05-23 19:02:03 +01:00
Ed Hennis
4ab886bcbc Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-22 17:56:15 -04:00
Ed Hennis
7f64c337d8 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-21 14:25:50 -04:00
Ed Hennis
61bdd6fb78 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-21 10:10:00 -04:00
Ed Hennis
8e06e78f11 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-21 07:11:49 -04:00
Ed Hennis
42fda85fbc Fix more AMM tests, and to not exclude fixCleanup3_2_0 2026-05-21 12:04:29 +01:00
Ed Hennis
3a4b92b050 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
2026-05-21 00:53:07 +01:00
Ed Hennis
aea19df3c1 Apply suggestions from @Tapanito code review
Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com>
2026-05-20 18:46:17 -04:00
Ed Hennis
8b56749ca3 Apply suggestions from Copilot code review
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-20 18:39:46 -04:00
Ed Hennis
71cf996fc6 Review feedback from @tapanito: lambda checks condition in doRoundUp 2026-05-20 23:28:26 +01:00
Ed Hennis
4c7ea64b6c Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-19 16:53:33 -04:00
Ed Hennis
c8947c6f75 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-19 10:15:10 -04:00
Ed Hennis
09ae5b719f Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-19 05:15:47 -04:00
Ed Hennis
09f2d06dd4 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-15 21:32:09 -04:00
Ed Hennis
6964013941 Merge remote-tracking branch 'XRPLF/develop' into ximinez/number-fix-maxrepcusp
* XRPLF/develop:
  release: Set version to 3.3.0-b0 (7280)
  refactor: Rename static constants (7120)
  refactor: Use `isFlag` where possible instead of bitwise math (7278)
  ci: Update XRPLF/actions (7281)
2026-05-15 21:14:34 -04:00
Ed Hennis
70c6e01d7e clang-tidy: this is ridiculous 2026-05-14 21:02:49 -04:00
Ed Hennis
ddfb7ee69c clang-tidy: {} 2026-05-14 20:48:34 -04:00
Ed Hennis
b69b9242e2 fixup! clang-tidy: Avoid nested "?:" in global Rules initialization 2026-05-14 20:33:15 -04:00
Ed Hennis
cd2fcf0a5e Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-14 18:25:17 -04:00
Ed Hennis
69656d6b67 clang-tidy: Avoid nested "?:" in global Rules initialization 2026-05-14 18:24:05 -04:00
Ed Hennis
46b946b22e clang-tidy: missing include 2026-05-13 22:32:37 -04:00
Ed Hennis
ae03b30f29 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-13 20:16:19 -04:00
Ed Hennis
4c7c019add Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-13 12:03:35 -04:00
Ed Hennis
47f30c913d 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.
2026-05-12 21:26:54 -04:00
Ed Hennis
d7d5b83f6d Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-12 19:18:45 -04:00
Ed Hennis
e22938d69f 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.
2026-05-12 18:44:05 -04:00
Ed Hennis
7c9a56ff24 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-12 16:26:09 -04:00
Ed Hennis
5a40416673 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-12 15:59:19 -04:00
Ed Hennis
30334cd1f4 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-11 13:34:11 -04:00
Ed Hennis
5558e1b522 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-07 18:10:03 -04:00
Ed Hennis
cd0f49a003 Address more nitpicky AI comments 2026-05-07 17:05:10 -04:00
Ed Hennis
22d2703ce8 What is it going to take to get clang-tidy to shut up? 2026-05-07 16:50:43 -04:00
Ed Hennis
d03274b731 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-07 14:18:28 -04:00
Ed Hennis
1b6047afe1 More clang-tidy changes: AMMExtended_test member and Number_test includes 2026-05-07 13:51:48 -04:00
Ed Hennis
175a04160d Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-07 13:28:33 -04:00
Ed Hennis
b050c151f8 Fix clang-tidy issues, and more AI complaints 2026-05-06 23:26:11 -04:00
Ed Hennis
a2b21d75ce Fix AI-identified mistakes 2026-05-06 22:56:07 -04:00
Ed Hennis
b40d2a8e7d 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.
2026-05-06 22:49:44 -04:00
3 changed files with 888 additions and 303 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;
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.
*
@@ -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);
@@ -319,6 +325,8 @@ public:
static constexpr internalrep kMaxRep = std::numeric_limits<rep>::max();
static_assert(kMaxRep == 9'223'372'036'854'775'807);
static_assert(-kMaxRep == std::numeric_limits<rep>::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
@@ -543,9 +551,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
@@ -560,7 +574,7 @@ private:
int& exponent,
internalrep const& minMantissa,
internalrep const& maxMantissa,
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled);
MantissaRange::CuspRoundingFix cuspRoundingFix);
template <class T>
friend void
@@ -570,7 +584,7 @@ private:
int& exponent,
MantissaRange::rep const& minMantissa,
MantissaRange::rep const& maxMantissa,
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled,
MantissaRange::CuspRoundingFix cuspRoundingFix,
bool dropped);
[[nodiscard]] bool
@@ -588,8 +602,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
@@ -867,6 +879,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_;

View File

@@ -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,40 +224,51 @@ public:
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.
[[nodiscard]] int
round() const noexcept;
// Modify the result to the correctly rounded value
template <UnsignedMantissa T>
void
doRoundUp(bool& negative, T& mantissa, int& exponent, std::string location);
// 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);
// 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
doRound(rep& drops, std::string location) const;
doRound(rep& drops, MantissaRange::CuspRoundingFix cuspRoundingFix, std::string location);
private:
template <class T>
void
pushOverflow(T const& mantissa, MantissaRange::CuspRoundingFix cuspRoundingFix);
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. 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;
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 +318,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
@@ -310,63 +345,99 @@ Number::Guard::doDropDigit<uint128_t>(uint128_t& mantissa, int& exponent) noexce
++exponent;
}
template <class T>
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
int
// 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
{
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 <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
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{};
@@ -378,22 +449,17 @@ 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))
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;
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".
@@ -409,20 +475,20 @@ 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.
doDropDigit(mantissa, exponent);
XRPL_ASSERT_PARTS(
safeToIncrement(mantissa),
"xrpl::Number::Guard::doRoundUp",
"can't recurse more than once");
doRoundUp(
negative,
mantissa,
exponent,
minMantissa,
maxMantissa,
cuspRoundingFixEnabled,
location);
return;
if (mantissa > kMaxRep && mantissa < kMaxRepUp)
{
mantissa = kMaxRepUp;
}
else
{
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
@@ -432,7 +498,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,38 +506,62 @@ Number::Guard::doRoundUp(
}
}
}
bringIntoRange(negative, mantissa, exponent, minMantissa);
else if (
cuspRoundingFix_ != MantissaRange::CuspRoundingFix::Disabled && mantissa > kMaxRep &&
mantissa < kMaxRepUp)
{
mantissa = kMaxRep;
}
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)
{
// Do not pushOverflow here.
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.
XRPL_ASSERT(
empty() || mantissa > maxMantissa_,
"xrpl::Number::Guard::doRoundDown : mantissa is expected size");
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
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 == 1 || (r == 0 && (drops & 1) == 1))
if (r == Round::Up || (r == Round::Even && (drops & 1) == 1))
{
if (drops >= kMaxRep)
{
@@ -486,6 +576,14 @@ Number::Guard::doRound(rep& drops, std::string location) const
}
++drops;
}
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.
drops = kMaxRep;
}
if (isNegative())
drops = -drops;
}
@@ -530,12 +628,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 repLimit = cuspRoundingFix == MantissaRange::CuspRoundingFix::Disabled
? Number::kMaxRep
: Number::kMaxRepUp;
using Guard = Number::Guard;
@@ -553,7 +653,7 @@ doNormalize(
m *= 10;
--exponent;
}
Guard g;
Guard g(minMantissa, maxMantissa, cuspRoundingFix);
if (negative)
g.setNegative();
if (dropped)
@@ -585,27 +685,20 @@ 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,
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 +713,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 +730,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 +747,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,7 +821,19 @@ 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_;
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
// 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,26 +853,14 @@ 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;
if (xm > maxMantissa || xm > kMaxRep)
if (xm > maxMantissa || xm > repLimit)
{
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 +874,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<uint128_t>(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 <= repLimit)
{
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<internalrep>(xm);
exponent_ = xe;
normalize(range);
return *this;
}
@@ -818,35 +941,28 @@ 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_;
auto const cuspRoundingFix = g.cuspRoundingFix_;
auto const repLimit =
cuspRoundingFix == MantissaRange::CuspRoundingFix::Disabled ? kMaxRep : kMaxRepUp;
while (zm > maxMantissa || zm > kMaxRep)
while (zm > maxMantissa || zm > repLimit)
{
g.doDropDigit(zm, ze);
}
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 +998,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 +1130,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;
@@ -1033,9 +1149,11 @@ Number::operator/=(Number const& y)
Number::
operator rep() const
{
auto const& range = kRange.get();
rep drops = mantissa();
int offset = exponent();
Guard g;
Guard g(kRange);
if (drops != 0)
{
if (negative_)
@@ -1053,7 +1171,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;
}

View File

@@ -43,6 +43,20 @@ class Number_test : public beast::unit_test::Suite
return out;
}
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;
}
using dec = boost::multiprecision::cpp_dec_float_50;
template <class T = dec>
@@ -169,28 +183,37 @@ 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}}});
BEAST_EXPECT(Number::getround() == Number::RoundingMode::ToNearest);
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
@@ -198,45 +221,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 +279,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 +290,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)
@@ -308,80 +348,149 @@ 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}}});
auto const cLarge = std::to_array<Case>(
Number{1'000'000'000'000'000, -30},
__LINE__}});
auto const cLargeAll = std::to_array<Case>(
// Note that items with extremely large mantissas need to be
// calculated, because otherwise they overflow uint64. Items from C
// with larger mantissa
{
{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{false, Number::kMaxRep + 1, 0, Number::Normalized{}},
Number{1, 0},
Number{(Number::kMaxRep / 10) + 1, 1}},
{Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}},
Number{3, 0},
Number{Number::kMaxRep}},
{power(2, 63), Number{3, 0}, Number{Number::kMaxRep}},
Number{1'000'000'000'000'000'000, -36},
__LINE__},
{Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep - 1}, __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<Case>({
// 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<Case>({
// 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] : 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)
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;
}
}
@@ -1281,38 +1390,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);
@@ -1320,62 +1429,136 @@ 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<std::int64_t>::max(), -3}, "9223372036854775");
test(
Number{std::numeric_limits<std::int64_t>::max(), -3},
"9223372036854775",
__LINE__);
test(
-(Number{std::numeric_limits<std::int64_t>::max(), -3}),
"-9223372036854775");
"-9223372036854775",
__LINE__);
test(
Number{std::numeric_limits<std::int64_t>::min(), 0}, "-9223372036854775e3");
Number{std::numeric_limits<std::int64_t>::min(), 0},
"-9223372036854775e3",
__LINE__);
test(
-(Number{std::numeric_limits<std::int64_t>::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<std::int64_t>::max(), 0}, "9223372036854775807");
Number{std::numeric_limits<std::int64_t>::max(), 0},
"9223372036854775807",
__LINE__);
test(
-(Number{std::numeric_limits<std::int64_t>::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<std::int64_t>::min(), 0},
"-9223372036854775800");
test(
-(Number{std::numeric_limits<std::int64_t>::min(), 0}),
"9223372036854775800");
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<std::int64_t>::min(), 0},
"-9223372036854775800",
__LINE__);
test(
-(Number{std::numeric_limits<std::int64_t>::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<std::int64_t>::min(), 0},
"-9223372036854775807",
__LINE__);
test(
-(Number{std::numeric_limits<std::int64_t>::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<std::int64_t>::max(), 0} + 1,
"9223372036854775810",
__LINE__);
test(
-(Number{std::numeric_limits<std::int64_t>::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<std::int64_t>::max(), 0} + 1,
"9223372036854775807",
__LINE__);
test(
-(Number{std::numeric_limits<std::int64_t>::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<std::int64_t>::max(), 0} + 1, "9223372036854775810");
Number{std::numeric_limits<std::int64_t>::max(), 0} + 2,
"9223372036854775810",
__LINE__);
test(
-(Number{std::numeric_limits<std::int64_t>::max(), 0} + 1),
"-9223372036854775810");
-(Number{std::numeric_limits<std::int64_t>::max(), 0} + 2),
"-9223372036854775810",
__LINE__);
break;
default:
BEAST_EXPECT(false);
@@ -1624,7 +1807,7 @@ public:
}
void
testUpwardRoundsDown()
testEdgeCases()
{
auto const scale = Number::getMantissaScale();
{
@@ -1644,21 +1827,18 @@ 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;
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)
@@ -1731,15 +1911,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.
@@ -1781,15 +1960,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.
@@ -1838,15 +2016,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.
@@ -1869,6 +2046,264 @@ 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<std::uint64_t>(kMaxRep) + 1;
Number const below{static_cast<std::int64_t>(kMaxRep), 0};
Number const above{
false, static_cast<std::uint64_t>(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 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:
// Covers "Large" and any newly added scales
// 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;
}
}
{
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)
{
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"});
}
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<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 << " 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 << "\n";
log.flush();
switch (scale)
{
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));
}
else
{
BEAST_EXPECTS(sums.at(r).first > exact, to_string(r));
}
}
break;
}
default: {
for (auto const& [r, sum] : sums)
{
auto const epsilon = pow10<BigInt>(sum.second.exponent());
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));
break;
default:
BEAST_EXPECTS(sum.first < exact, to_string(r));
BEAST_EXPECTS(-diff < epsilon, to_string(r));
}
}
}
}
}
{
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<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();
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.
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<BigInt>(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));
}
}
}
}
}
}
void
@@ -1899,7 +2334,7 @@ public:
testRounding();
testInt64();
testUpwardRoundsDown();
testEdgeCases();
}
}
};