Merge remote-tracking branch 'origin/ximinez/number-round-maxrep' into ripple/lending-protocol-fv

This commit is contained in:
Vito
2026-06-24 11:06:51 +02:00
5 changed files with 658 additions and 149 deletions

View File

@@ -334,6 +334,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

View File

@@ -287,6 +287,25 @@ public:
void
doDropDigit(T& mantissa, int& exponent) 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
doRoundDown(bool& negative, T& mantissa, int& exponent) const;
// Modify the result to the correctly rounded value
void
doRound(rep& drops, std::string location) const;
private:
template <UnsignedMantissa T>
void
pushOverflow(T mantissa);
enum class Round {
// The result is exact. No rounding is needed. Only used if cuspRoundingFix is Enabled330 or
// higher.
@@ -299,37 +318,22 @@ public:
// The result was exactly half-way between two integers. This will round to even.
Even = 0,
// Round up. Always adds 1 (or subtracts 1 in some cases if cuspRoundingFix is not
// Enabled)
// Enabled330)
Up = 1,
};
// Indicate round direction: 1 is up, -1 is down, 0 is even
// 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;
// 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
doRoundDown(bool& negative, T& mantissa, int& exponent);
// Modify the result to the correctly rounded value
void
doRound(rep& drops, std::string location) const;
private:
void
doPush(unsigned d) noexcept;
template <UnsignedMantissa T>
void
bringIntoRange(bool& negative, T& mantissa, int& exponent);
bringIntoRange(bool& negative, T& mantissa, int& exponent) const;
};
inline void
@@ -406,10 +410,76 @@ Number::Guard::doDropDigit<uint128_t>(uint128_t& mantissa, int& exponent) noexce
++exponent;
}
template <UnsignedMantissa T>
void
Number::Guard::pushOverflow(T mantissa)
{
XRPL_ASSERT(mantissa <= kMaxRepUp, "xrpl::Number::Guard::pushOverflow : valid mantissa");
if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && mantissa >= kMaxRep &&
mantissa < kMaxRepUp)
{
// Special case rounding rules for the values in the range [kMaxRep, kMaxRepUp).
auto constexpr spread = kMaxRepUp - kMaxRep;
static_assert(spread == 3);
// Round in two steps.
// The first step uses the digits _already_ in the Guard to possibly round the mantissa up.
// Ultimately, the purpose of this step is to capture rounding where the stored digits would
// change the decision without those digits. (e.g. From just _below_ the midpoint to just
// _above_ the midpoint for ToNearest, or from kMaxRep into the in-between for Upward. Make
// an exception if the final digit is 9, because it can only get larger, and we don't want
// to bump up to kMaxRepUp.
if (mantissa % 10 < 9)
{
// Intentionally use integer math to get the largest value under the midpoint.
auto constexpr kMidpoint = kMaxRep + (spread / 2);
static_assert(kMidpoint == kMaxRep + 1);
auto const r = round();
if (r == Round::Up || (r == Round::Even && mantissa == kMidpoint))
{
++mantissa;
}
}
if (mantissa == kMaxRep)
{
// If the mantissa ends up exactly kMaxRep, there's nothing more to do here.
return;
}
// The second step scales the final digit of the update mantissa proportionally, converting
// from (kMaxRep, kMaxRepUp) to (0 to 9]. It then pushes that scaled digit onto the guard as
// if it was a digit that got removed, but doesn't actually remove it. This method should be
// future-proof in case the number of mantissa bits ever changes. (Though for integer values
// of the form 2^(2^x-1), the spread will always be the same.) Effects:
// * For round to nearest
// * if the updated mantissa is below the midpoint, it'll round "down" to kMaxRep
// * if above the midpoint, it'll round "up" to kMaxRepUp
// * it can never be exactly at the midpoint, because kMaxRepUp is always even, and
// kMaxRep is always odd, so don't worry about that case.
// * For round upward, will round up to kMaxRepUp for positive values, down to kMaxRep for
// negative.
// * For round downward, does the opposite of upward.
// * For round toward zero, always rounds down to kMaxRep.
auto const diff = mantissa - kMaxRep;
auto const digit = (diff * 10) / spread;
XRPL_ASSERT(
digit > 0 && digit < 10 && digit != 5,
"xrpl::Number::Guard::pushOverflow : valid overflow digit");
// Don't remove the digit from the mantissa, but add it to the guard as if it was.
push(digit);
}
}
// Returns:
// -1 if Guard is less than half
// 0 if Guard is exactly half
// 1 if Guard is greater than half
// Exact if Guard is _zero_, and appropriate amendments are enabled
// Down if Guard is less than half
// Even if Guard is exactly half
// Up if Guard is greater than half
Number::Guard::Round
Number::Guard::round() const noexcept
{
@@ -455,16 +525,19 @@ Number::Guard::round() const noexcept
template <UnsignedMantissa T>
void
Number::Guard::bringIntoRange(bool& negative, T& mantissa, int& exponent)
Number::Guard::bringIntoRange(bool& negative, T& mantissa, int& exponent) const
{
// Bring mantissa back into the minMantissa / maxMantissa range AFTER
// rounding
// rounding. Mantissa should never be 0.
XRPL_ASSERT(mantissa != 0, "xrpl::Number::Guard::bringIntoRange : valid mantissa");
if (mantissa < minMantissa)
{
mantissa *= 10;
--exponent;
}
if (exponent < kMinExponent)
// mantissa should never be 0, but if it _is_ make the result kZero.
if (exponent < kMinExponent ||
(cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && mantissa == 0))
{
static constexpr Number kZero = Number{};
@@ -478,7 +551,9 @@ template <UnsignedMantissa T>
void
Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string location)
{
auto r = round();
pushOverflow(mantissa);
auto const r = round();
if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1))
{
auto const safeToIncrement = [this](auto const& mantissa) {
@@ -495,18 +570,29 @@ Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string
}
else
{
// Incrementing the mantissa will require dividing, which will require rounding. So
// _don't_ increment the mantissa. Instead, divide and round recursively. It should
// be impossible to recurse more than once, because once the mantissa is divided by
// 10, it will be _well_ under maxMantissa and kMaxRep, so adding 1 will have no
// chance of bringing it back over.
doDropDigit(mantissa, exponent);
XRPL_ASSERT_PARTS(
safeToIncrement(mantissa),
"xrpl::Number::Guard::doRoundUp",
"can't recurse more than once");
doRoundUp(negative, mantissa, exponent, location);
return;
if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 &&
mantissa > kMaxRep && mantissa < kMaxRepUp)
{
// When rounding up a value in between kMaxRep, and kMaxRepUp, round to
// kMaxRepUp. Note that the decision for this rounding is dominated by the
// results of pushOverflow.
mantissa = kMaxRepUp;
}
else
{
// Incrementing the mantissa will require dividing, which will require rounding.
// So _don't_ increment the mantissa. Instead, divide and round recursively. It
// should be impossible to recurse more than once, because once the mantissa is
// divided by 10, it will be _well_ under maxMantissa and kMaxRep, so adding 1
// will have no chance of bringing it back over.
doDropDigit(mantissa, exponent);
XRPL_ASSERT_PARTS(
safeToIncrement(mantissa),
"xrpl::Number::Guard::doRoundUp",
"can't recurse more than once");
doRoundUp(negative, mantissa, exponent, location);
return;
}
}
}
else
@@ -524,6 +610,14 @@ Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string
}
}
}
else if (
cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && mantissa > kMaxRep &&
mantissa < kMaxRepUp)
{
// When rounding down a value in between kMaxRep, and kMaxRepUp, round to kMaxRep.
// Note that the decision for this rounding is dominated by the results of pushOverflow.
mantissa = kMaxRep;
}
bringIntoRange(negative, mantissa, exponent);
if (exponent > kMaxExponent)
Throw<std::overflow_error>(std::string(location));
@@ -531,8 +625,10 @@ Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string
template <UnsignedMantissa T>
void
Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent)
Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) const
{
// Do not pushOverflow here.
auto r = round();
if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330)
{
@@ -567,6 +663,8 @@ Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent)
void
Number::Guard::doRound(rep& drops, std::string location) const
{
// Do not pushOverflow here.
auto r = round();
if (r == Round::Up || (r == Round::Even && (drops & 1) == 1))
{
@@ -583,6 +681,8 @@ Number::Guard::doRound(rep& drops, std::string location) const
}
++drops;
}
XRPL_ASSERT(drops >= 0, "xrpl::Number::Guard::doRound : positive magnitude");
if (isNegative())
drops = -drops;
}
@@ -632,7 +732,9 @@ doNormalize(
{
static constexpr auto kMinExponent = Number::kMinExponent;
static constexpr auto kMaxExponent = Number::kMaxExponent;
static constexpr auto kMaxRep = Number::kMaxRep;
auto const repLimit = cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330
? Number::kMaxRepUp
: Number::kMaxRep;
using Guard = Number::Guard;
@@ -682,17 +784,17 @@ doNormalize(
// 9,900,000,000,000,123,450 or 9,900,000,000,000,123,460.
// mantissa() will return mantissa / 10, and exponent() will return
// exponent + 1.
if (m > kMaxRep)
if (m > repLimit)
{
if (exponent >= kMaxExponent)
throw std::overflow_error("Number::normalize 1.5");
g.doDropDigit(m, exponent);
}
// Before modification, m should be within the min/max range. After
// modification, it must be less than kMaxRep. In other words, the original
// value should have been no more than kMaxRep * 10.
// (kMaxRep * 10 > maxMantissa)
XRPL_ASSERT_PARTS(m <= kMaxRep, "xrpl::doNormalize", "intermediate mantissa fits in int64");
// modification, it must be less than repLimit. In other words, the original
// value should have been no more than repLimit * 10.
// (repLimit * 10 > maxMantissa)
XRPL_ASSERT_PARTS(m <= repLimit, "xrpl::doNormalize", "intermediate mantissa fits in limit");
mantissa = m;
g.doRoundUp(negative, mantissa, exponent, "Number::normalize 2");
@@ -824,6 +926,9 @@ Number::operator+=(Number const& y)
auto const& maxMantissa = g.maxMantissa;
auto const cuspRoundingFix = g.cuspRoundingFix;
auto const repLimit =
cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 ? kMaxRepUp : kMaxRep;
// Bring the exponents of both values into agreement, so the mantissas are on the same scale
// and can be added directly together.
@@ -908,7 +1013,7 @@ Number::operator+=(Number const& y)
}
else
{
if (xm > maxMantissa || xm > kMaxRep)
if (xm > maxMantissa || xm > repLimit)
{
g.doDropDigit(xm, xe);
}
@@ -952,7 +1057,7 @@ Number::operator+=(Number const& y)
{
// Grow xm/xe and pull digits out of the Guard until it's back in the
// minMantissa/maxMantissa range.
while (xm < minMantissa && xm * 10 <= kMaxRep)
while (xm < minMantissa && xm * 10 <= repLimit)
{
xm *= 10;
xm -= g.pop();
@@ -1026,8 +1131,10 @@ Number::operator*=(Number const& y)
g.setNegative();
auto const& maxMantissa = g.maxMantissa;
auto const repLimit =
g.cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 ? kMaxRepUp : kMaxRep;
while (zm > maxMantissa || zm > kMaxRep)
while (zm > maxMantissa || zm > repLimit)
{
g.doDropDigit(zm, ze);
}

View File

@@ -1452,18 +1452,54 @@ class LoanBroker_test : public beast::unit_test::Suite
env(tx2, Ter(temINVALID));
}
if (Number::getMantissaScale() == MantissaRange::MantissaScale::Large330)
{
auto const dm = power(2, 63) - 1;
BEAST_EXPECTS(dm > kMaxMpTokenAmount, to_string(dm));
tx2[sfDebtMaximum] = dm;
env(tx2, Ter(temINVALID));
}
// For the Large330 scale, 2^63 rounds _down_ to Number::kMaxRep
{
auto const dm = power(2, 63);
BEAST_EXPECTS(dm == kMaxMpTokenAmount, to_string(dm));
tx2[sfDebtMaximum] = dm;
env(tx2, Ter(tesSUCCESS));
}
{
auto const dm = power(2, 63) - 1;
BEAST_EXPECTS(dm < kMaxMpTokenAmount, to_string(dm));
tx2[sfDebtMaximum] = dm;
env(tx2, Ter(tesSUCCESS));
}
{
auto const dm = power(2, 63) - 3;
BEAST_EXPECTS(dm < kMaxMpTokenAmount, to_string(dm));
tx2[sfDebtMaximum] = dm;
env(tx2, Ter(tesSUCCESS));
}
{
auto const dm = power(2, 63) + 3;
BEAST_EXPECTS(dm > kMaxMpTokenAmount, to_string(dm));
tx2[sfDebtMaximum] = dm;
env(tx2, Ter(temINVALID));
}
}
else
{
auto const dm = power(2, 63) - 3;
BEAST_EXPECTS(dm == kMaxMpTokenAmount, to_string(dm));
tx2[sfDebtMaximum] = dm;
env(tx2, Ter(tesSUCCESS));
// For other scales, 2^63 rounds _up_ to Number::kMaxRepUp. Subtracting 1 rounds up
// again.
{
auto const dm = power(2, 63) - 1;
BEAST_EXPECTS(dm > kMaxMpTokenAmount, to_string(dm));
tx2[sfDebtMaximum] = dm;
env(tx2, Ter(temINVALID));
}
{
auto const dm = power(2, 63) - 3;
BEAST_EXPECTS(dm == kMaxMpTokenAmount, to_string(dm));
tx2[sfDebtMaximum] = dm;
env(tx2, Ter(tesSUCCESS));
}
}
{

View File

@@ -5606,9 +5606,9 @@ class Vault_test : public beast::unit_test::Suite
BEAST_EXPECT(maxInt64 == "9223372036854775807");
// Naming things is hard
auto const maxInt64Plus1 = std::to_string(
static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max()) + 1);
BEAST_EXPECT(maxInt64Plus1 == "9223372036854775808");
auto const maxInt64Plus2 = std::to_string(
static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max()) + 2);
BEAST_EXPECT(maxInt64Plus2 == "9223372036854775809");
auto const initialXRP = to_string(kInitialXrp);
BEAST_EXPECT(initialXRP == "100000000000000000");
@@ -5636,15 +5636,15 @@ class Vault_test : public beast::unit_test::Suite
env(tx);
env.close();
tx[sfAssetsMaximum] = maxInt64Plus1;
tx[sfAssetsMaximum] = maxInt64Plus2;
env(tx, Ter(tefEXCEPTION));
env.close();
// This value will be rounded
auto const insertAt = maxInt64Plus1.size() - 3;
auto const decimalTest = maxInt64Plus1.substr(0, insertAt) + "." +
maxInt64Plus1.substr(insertAt); // (max int64+1) / 1000
BEAST_EXPECT(decimalTest == "9223372036854775.808");
auto const insertAt = maxInt64Plus2.size() - 3;
auto const decimalTest = maxInt64Plus2.substr(0, insertAt) + "." +
maxInt64Plus2.substr(insertAt); // (max int64+2) / 1000
BEAST_EXPECT(decimalTest == "9223372036854775.809");
tx[sfAssetsMaximum] = decimalTest;
auto const newKeylet = keylet::vault(owner.id(), env.seq(owner));
env(tx);
@@ -5688,15 +5688,15 @@ class Vault_test : public beast::unit_test::Suite
env(tx);
env.close();
tx[sfAssetsMaximum] = maxInt64Plus1;
tx[sfAssetsMaximum] = maxInt64Plus2;
env(tx, Ter(tefEXCEPTION));
env.close();
// This value will be rounded
auto const insertAt = maxInt64Plus1.size() - 1;
auto const decimalTest = maxInt64Plus1.substr(0, insertAt) + "." +
maxInt64Plus1.substr(insertAt); // (max int64+1) / 10
BEAST_EXPECT(decimalTest == "922337203685477580.8");
auto const insertAt = maxInt64Plus2.size() - 1;
auto const decimalTest = maxInt64Plus2.substr(0, insertAt) + "." +
maxInt64Plus2.substr(insertAt); // (max int64+2) / 10
BEAST_EXPECT(decimalTest == "922337203685477580.9");
tx[sfAssetsMaximum] = decimalTest;
auto const newKeylet = keylet::vault(owner.id(), env.seq(owner));
env(tx);
@@ -5733,7 +5733,7 @@ class Vault_test : public beast::unit_test::Suite
env(tx);
env.close();
tx[sfAssetsMaximum] = maxInt64Plus1;
tx[sfAssetsMaximum] = maxInt64Plus2;
env(tx);
env.close();
@@ -5745,10 +5745,10 @@ class Vault_test : public beast::unit_test::Suite
// These values will be rounded to 15 significant digits
{
auto const insertAt = maxInt64Plus1.size() - 1;
auto const decimalTest = maxInt64Plus1.substr(0, insertAt) + "." +
maxInt64Plus1.substr(insertAt); // (max int64+1) / 10
BEAST_EXPECT(decimalTest == "922337203685477580.8");
auto const insertAt = maxInt64Plus2.size() - 1;
auto const decimalTest = maxInt64Plus2.substr(0, insertAt) + "." +
maxInt64Plus2.substr(insertAt); // (max int64+2) / 10
BEAST_EXPECT(decimalTest == "922337203685477580.9");
tx[sfAssetsMaximum] = decimalTest;
auto const newKeylet = keylet::vault(owner.id(), env.seq(owner));
env(tx);

View File

@@ -188,6 +188,8 @@ public:
auto const scale = Number::getMantissaScale();
testcase << "test_add " << to_string(scale);
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},
@@ -299,12 +301,15 @@ public:
auto const cLargeLegacy = std::to_array<Case>({
{Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep / 10, 1}, __LINE__},
});
auto const cLargeCorrected = std::to_array<Case>({
auto const cLarge320 = std::to_array<Case>({
{Number{Number::kMaxRep},
Number{6, -1},
Number{(Number::kMaxRep / 10) + 1, 1},
__LINE__},
});
auto const cLargeCorrected = std::to_array<Case>({
{Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep}, __LINE__},
});
auto test = [this](auto const& c) {
for (auto const& [x, y, z, line] : c)
{
@@ -325,6 +330,10 @@ public:
{
test(cLargeLegacy);
}
else if (scale == MantissaRange::MantissaScale::Large320)
{
test(cLarge320);
}
else
{
test(cLargeCorrected);
@@ -373,7 +382,7 @@ public:
Number{1'000'000'000'000'000, -15},
Number{1'000'000'000'000'000, -30},
__LINE__}});
auto const cLarge = std::to_array<Case>(
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
@@ -420,16 +429,55 @@ public:
Number{1'000'000'000'000'000'000, -36},
__LINE__},
{Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep - 1}, __LINE__},
{Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}},
Number{1, 0},
Number{(Number::kMaxRep / 10) + 1, 1},
__LINE__},
{Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}},
Number{3, 0},
Number{Number::kMaxRep},
__LINE__},
{power(2, 63), Number{3, 0}, Number{Number::kMaxRep}, __LINE__},
});
// Note that items with extremely large mantissas need to be
// calculated, because otherwise they overflow uint64. Items from C
// with larger mantissa
auto const cLarge = 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 cLarge330 = 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, line] : c)
{
@@ -439,13 +487,23 @@ public:
expect(result == z, ss.str(), __FILE__, line);
}
};
if (scale == MantissaRange::MantissaScale::Small)
switch (scale)
{
test(cSmall);
}
else
{
test(cLarge);
case MantissaRange::MantissaScale::Small:
test(cSmall);
break;
case MantissaRange::MantissaScale::LargeLegacy:
case MantissaRange::MantissaScale::Large320:
test(cLargeAll);
test(cLarge);
break;
case MantissaRange::MantissaScale::Large330:
test(cLargeAll);
test(cLarge330);
break;
default:
BEAST_EXPECT(false);
break;
}
}
@@ -1345,38 +1403,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);
@@ -1384,61 +1442,132 @@ 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;
default:
// 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::Large330:
// 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:
// 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;
}
}
switch (scale)
{
case MantissaRange::MantissaScale::Large330:
// Rounding to nearest, since the mantissa is below the halfway point from
// kMaxRep to kMaxRep up, it will be rounded down to kMaxRep
test(
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:
// 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;
}
// Rounding to nearest, will be rounded up to kMaxRepUp, but for different reasons
// depending on the scale. If older than "Large", it rounds up for the same reason
// "+1" rounds up. For "Large", since the mantissa is above the halfway point from
// kMaxRep to kMaxRepUp, it will be rounded up to kMaxRepUp.
test(
Number{std::numeric_limits<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;
}
}
@@ -1776,7 +1905,7 @@ public:
}
void
testUpwardRoundsDown()
testEdgeCases()
{
auto const scale = Number::getMantissaScale();
{
@@ -1800,15 +1929,14 @@ public:
BigInt const signedDifference = storedValue - exactProduct;
log << "\n"
<< " a = " << fmt(BigInt(kAValue)) << "\n"
log << " a = " << fmt(BigInt(kAValue)) << "\n"
<< " b = " << fmt(BigInt(kBValue)) << "\n"
<< " exact a*b = " << fmt(exactProduct) << "\n"
<< " stored = " << fmt(storedValue) << "\n"
<< " stored - exact = " << fmt(signedDifference) << "\n"
<< " upward = " << (signedDifference >= 0 ? "held" : "VIOLATED") << "\n"
<< " stored.mantissa = " << product.mantissa() << "\n"
<< " stored.exponent = " << product.exponent() << "\n";
<< " stored.exponent = " << product.exponent() << "\n\n";
log.flush();
switch (scale)
@@ -1882,15 +2010,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.
@@ -1933,15 +2060,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.
@@ -1991,15 +2117,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.
@@ -2086,7 +2211,7 @@ public:
return sums;
}();
log << "\n a = " << a << " (" << fmt(bigA)
log << " a = " << a << " (" << fmt(bigA)
<< ")\n b = " << b << " (" << fmt(bigB)
<< ")\n exact a + b = " << fmt(exact) << "\n";
for (auto const& [r, sum] : sums)
@@ -2096,6 +2221,7 @@ public:
log << std::string(15 - rLabel.length(), ' ') << rLabel << " = "
<< fmt(sum.first) << "\n difference = " << fmt(diff) << "\n";
}
log << "\n";
log.flush();
auto const expectedExponent =
@@ -2146,6 +2272,83 @@ public:
}
}
}
{
testcase << "normalization cusp: ToNearest and Downward behavior " << to_string(scale);
constexpr auto kMaxRep = Number::kMaxRep;
// Both ToNearest and Downward should round to `below`
auto constexpr 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::Normalized{}};
auto construct = [](Number::RoundingMode mode) {
NumberRoundModeGuard const roundGuard{mode};
return Number(false, actual, 0, Number::Normalized{});
};
Number const upward = construct(Number::RoundingMode::Upward);
Number 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 but Downward rounds UP, including the
// reference values, "above" and "below"
BEAST_EXPECT(below == above);
BEAST_EXPECT(upward == above);
BEAST_EXPECT(toNearest == above);
BEAST_EXPECT(downward < below);
break;
case MantissaRange::MantissaScale::LargeLegacy:
case MantissaRange::MantissaScale::Large320:
// Upward round UP
BEAST_EXPECT(upward == above);
// ToNearest rounds UP when the DOWN neighbor is strictly closer
BEAST_EXPECT(toNearest == above);
BEAST_EXPECT(toNearest > below);
// Downward undershoots: it returns a value below `below`
BEAST_EXPECT(downward < below);
// Both should have given the same answer, but they differ
BEAST_EXPECT(toNearest > downward);
break;
default:
// Covers "Large" and any newly added scales
// Upward round UP
BEAST_EXPECT(upward == above);
// ToNearest rounds to the strictly closer DOWN neighbor
BEAST_EXPECT(toNearest != above);
BEAST_EXPECT(toNearest == below);
// Downward also rounds to `below`
BEAST_EXPECT(downward == below);
// ToNearest rounds to downward
BEAST_EXPECT(toNearest == downward);
break;
}
}
}
void
@@ -2359,6 +2562,166 @@ public:
}
}
void
testNumberRoundCuspWithFractionalParts()
{
auto const scale = Number::getMantissaScale();
testcase << "normalization cusp: rounding behavior with fractional parts "
<< to_string(scale);
NumberRoundModeGuard const roundGuard{Number::RoundingMode::ToNearest};
Number const below{static_cast<std::int64_t>(Number::kMaxRep), 0};
Number const above{false, Number::kMaxRepUp, 0, Number::Normalized{}};
log << "Below: " << below << ", Above: " << above << "\n";
auto const zeroPointFour = Number(4, -1);
auto const zeroPointSix = Number(6, -1);
auto const onePointFour = Number(14, -1);
auto const onePointFive = Number(15, -1);
auto const onePointSix = Number(16, -1);
auto const twoPointFour = Number(24, -1);
auto const twoPointSix = Number(26, -1);
auto const operands = std::to_array<Number>({
zeroPointFour,
zeroPointSix,
onePointFour,
onePointFive,
onePointSix,
twoPointFour,
twoPointSix,
});
auto const modes = std::to_array<Number::RoundingMode>({
Number::RoundingMode::ToNearest,
Number::RoundingMode::TowardsZero,
Number::RoundingMode::Downward,
Number::RoundingMode::Upward,
});
// Addition cases test kMaxRep + Operand
for (auto const& mode : modes)
{
for (auto const& operand : operands)
{
NumberRoundModeGuard const rg{mode};
auto const expectedValue = [&]() {
if (scale >= MantissaRange::MantissaScale::Large330)
{
if (mode == Number::RoundingMode::ToNearest && operand < onePointFive)
return below;
if (mode == Number::RoundingMode::TowardsZero ||
mode == Number::RoundingMode::Downward)
return below;
}
if (scale == MantissaRange::MantissaScale::Large320)
{
if (mode == Number::RoundingMode::ToNearest)
{
if (operand < zeroPointSix)
return below;
}
if (mode == Number::RoundingMode::TowardsZero ||
mode == Number::RoundingMode::Downward)
{
if (operand >= onePointFour)
return below - 7;
return below;
}
}
if (scale == MantissaRange::MantissaScale::LargeLegacy)
{
if (mode == Number::RoundingMode::ToNearest)
{
if (operand < zeroPointSix)
return below;
if (operand == zeroPointSix)
return below - 7;
}
if (mode == Number::RoundingMode::TowardsZero ||
mode == Number::RoundingMode::Downward)
{
if (operand >= onePointFour)
return below - 7;
return below;
}
if (mode == Number::RoundingMode::Upward && operand <= zeroPointSix)
return below - 7;
}
if (scale == MantissaRange::MantissaScale::Small &&
mode == Number::RoundingMode::Upward)
return above + 1000;
return above;
}();
Number const actual = below + operand;
std::stringstream ss;
ss << "kMaxRep + " << operand << " rounded " << to_string(mode) << " to " << actual
<< ". Expected: " << expectedValue;
BEAST_EXPECTS(actual == expectedValue, ss.str());
}
}
// Subtraction cases test kMaxRepUp - Operand
for (auto const& mode : modes)
{
for (auto const& operand : operands)
{
NumberRoundModeGuard const rg{mode};
auto const expectedValue = [&]() {
if (scale >= MantissaRange::MantissaScale::Large330)
{
if (mode == Number::RoundingMode::ToNearest && operand > onePointFive)
return below;
if (mode == Number::RoundingMode::TowardsZero ||
mode == Number::RoundingMode::Downward)
return below;
}
if (scale == MantissaRange::MantissaScale::LargeLegacy ||
scale == MantissaRange::MantissaScale::Large320)
{
if (mode == Number::RoundingMode::ToNearest)
{
if (operand >= twoPointSix)
return below;
}
if (mode == Number::RoundingMode::TowardsZero)
{
if (operand >= onePointFour)
return below - 7;
}
if (mode == Number::RoundingMode::Downward)
{
if (operand <= onePointSix)
return below - 7;
return below;
}
}
if (scale == MantissaRange::MantissaScale::Small)
{
if (mode == Number::RoundingMode::Downward)
return below - 1000;
if (mode == Number::RoundingMode::Upward)
return below;
}
return above;
}();
Number const actual = above - operand;
std::stringstream ss;
ss << "kMaxRepUp - " << operand << " rounded " << to_string(mode) << " to "
<< actual << ". Expected: " << expectedValue;
BEAST_EXPECTS(actual == expectedValue, ss.str());
}
}
}
void
run() override
{
@@ -2388,9 +2751,10 @@ public:
testRounding();
testInt64();
testUpwardRoundsDown();
testEdgeCases();
testNumberAddDirectedSignWrong();
testNumberAddToNearestPicksFarther();
testNumberRoundCuspWithFractionalParts();
}
}
};