Compare commits

...

1 Commits

Author SHA1 Message Date
Ed Hennis
c01bfb2b60 [WIP] Checkpoint 2026-01-23 16:01:11 -05:00
5 changed files with 139 additions and 117 deletions

View File

@@ -36,18 +36,37 @@ class Number;
std::string
to_string(Number const& amount);
/** Returns a rough estimate of log10(value).
*
* The return value is a pair (log, rem), where log is the estimated log10,
* and rem is value divided by 10^log. If rem is 1, then value is an exact
* power of ten, and log is the exact log10(value).
*
* This function only works for positive values.
*/
template <typename T>
constexpr std::pair<int, T>
logTenEstimate(T value)
{
int log = 0;
T remainder = value;
while (value >= 10)
{
if (value % 10 == 0)
remainder = remainder / 10;
value /= 10;
++log;
}
return {log, remainder};
}
template <typename T>
constexpr std::optional<int>
logTen(T value)
{
int log = 0;
while (value >= 10 && value % 10 == 0)
{
value /= 10;
++log;
}
if (value == 1)
return log;
auto const est = logTenEstimate(value);
if (est.second == 1)
return est.first;
return std::nullopt;
}
@@ -61,8 +80,6 @@ isPowerOfTen(T value)
/** MantissaRange defines a range for the mantissa of a normalized Number.
*
* The mantissa is in the range [min, max], where
* * min is a power of 10, and
* * max = min * 10 - 1.
*
* The mantissa_scale enum indicates whether the range is "small" or "large".
* This intentionally restricts the number of MantissaRanges that can be
@@ -80,8 +97,8 @@ isPowerOfTen(T value)
* "large" scale.
*
* The "large" scale is intended to represent all values that can be represented
* by an STAmount - IOUs, XRP, and MPTs. It has a min value of 10^18, and a max
* value of 10^19-1.
* by an STAmount - IOUs, XRP, and MPTs. It has a min value of 2^63/10+1
* (truncated), and a max value of 2^63-1.
*
* Note that if the mentioned amendments are eventually retired, this class
* should be left in place, but the "small" scale option should be removed. This
@@ -93,28 +110,38 @@ struct MantissaRange
enum mantissa_scale { small, large };
explicit constexpr MantissaRange(mantissa_scale scale_)
: min(getMin(scale_))
, max(min * 10 - 1)
, log(logTen(min).value_or(-1))
: max(getMax(scale_))
, min(computeMin(max))
, log(computeLog(min))
, scale(scale_)
{
// Since this is constexpr, if any of these throw, it won't compile
if (min * 10 <= max)
throw std::out_of_range("min * 10 <= max");
if (max / 10 >= min)
throw std::out_of_range("max / 10 >= min");
if ((min - 1) * 10 > max)
throw std::out_of_range("(min - 1) * 10 > max");
// This is a little hacky
if ((max + 10) / 10 < min)
throw std::out_of_range("(max + 10) / 10 < min");
}
rep min;
rep max;
rep min;
int log;
mantissa_scale scale;
private:
static constexpr rep
getMin(mantissa_scale scale_)
getMax(mantissa_scale scale_)
{
switch (scale_)
{
case small:
return 1'000'000'000'000'000ULL;
return 9'999'999'999'999'999ULL;
case large:
return 1'000'000'000'000'000'000ULL;
return std::numeric_limits<std::int64_t>::max();
default:
// Since this can never be called outside a non-constexpr
// context, this throw assures that the build fails if an
@@ -122,6 +149,24 @@ private:
throw std::runtime_error("Unknown mantissa scale");
}
}
static constexpr rep
computeMin(rep max)
{
auto min = max + 1;
auto const r = min % 10;
min /= 10;
if (r != 0)
++min;
return min;
}
static constexpr rep
computeLog(rep min)
{
auto const estimate = logTenEstimate(min);
return estimate.first + (estimate.second == 1 ? 0 : 1);
}
};
// Like std::integral, but only 64-bit integral types.
@@ -156,9 +201,7 @@ concept Integral64 =
* 1. Normalization can be disabled by using the "unchecked" ctor tag. This
* should only be used at specific conversion points, some constexpr
* values, and in unit tests.
* 2. The max of the "large" range, 10^19-1, is the largest 10^X-1 value that
* fits in an unsigned 64-bit number. (10^19-1 < 2^64-1 and
* 10^20-1 > 2^64-1). This avoids under- and overflows.
* 2. The max of the "large" range, 2^63-1, TODO: explain the large range.
*
* ---- External Interface ----
*
@@ -172,7 +215,7 @@ concept Integral64 =
*
* Note:
* 1. 2^63-1 is between 10^18 and 10^19-1, which are the limits of the "large"
* mantissa range.
* mantissa range. TODO: update this explanation.
* 2. The functions mantissa() and exponent() return the external view of the
* Number value, specifically using a signed 63-bit mantissa. This may
* require altering the internal representation to fit into that range
@@ -232,6 +275,7 @@ class Number
using rep = std::int64_t;
using internalrep = MantissaRange::rep;
// TODO: Get rid of negative_ and convert mantissa back to rep
bool negative_{false};
internalrep mantissa_{0};
int exponent_{std::numeric_limits<int>::lowest()};
@@ -241,9 +285,11 @@ public:
constexpr static int minExponent = -32768;
constexpr static int maxExponent = 32768;
#if MAXREP
constexpr static internalrep maxRep = std::numeric_limits<rep>::max();
static_assert(maxRep == 9'223'372'036'854'775'807);
static_assert(-maxRep == std::numeric_limits<rep>::min() + 1);
#endif
// May need to make unchecked private
struct unchecked
@@ -477,15 +523,27 @@ private:
static_assert(smallRange.min == 1'000'000'000'000'000LL);
static_assert(smallRange.max == 9'999'999'999'999'999LL);
static_assert(smallRange.log == 15);
#if MAXREP
static_assert(smallRange.min < maxRep);
static_assert(smallRange.max < maxRep);
#endif
constexpr static MantissaRange largeRange{MantissaRange::large};
static_assert(isPowerOfTen(largeRange.min));
static_assert(largeRange.min == 1'000'000'000'000'000'000ULL);
static_assert(largeRange.max == internalrep(9'999'999'999'999'999'999ULL));
static_assert(!isPowerOfTen(largeRange.min));
static_assert(largeRange.min == 922337203685477581ULL);
static_assert(largeRange.max == internalrep(9223372036854775807ULL));
static_assert(largeRange.max == std::numeric_limits<rep>::max());
static_assert(largeRange.log == 18);
// There are 2 values that will not fit in largeRange without some extra
// work
// * 9223372036854775808
// * 9223372036854775809
// They both end up < min, but with a leftover. If they round up, everything
// will be fine. If they don't, well need to bring them up into range.
#if MAXREP
static_assert(largeRange.min < maxRep);
static_assert(largeRange.max > maxRep);
#endif
// The range for the mantissa when normalized.
// Use reference_wrapper to avoid making copies, and prevent accidentally
@@ -536,6 +594,9 @@ private:
externalToInternal(rep mantissa);
class Guard;
public:
constexpr static internalrep largestMantissa = largeRange.max;
};
inline constexpr Number::Number(
@@ -589,17 +650,8 @@ inline Number::Number(rep mantissa) : Number{mantissa, 0}
inline constexpr Number::rep
Number::mantissa() const noexcept
{
auto m = mantissa_;
if (m > maxRep)
{
XRPL_ASSERT_PARTS(
!isnormal() || (m % 10 == 0 && m / 10 <= maxRep),
"xrpl::Number::mantissa",
"large normalized mantissa has no remainder");
m /= 10;
}
auto const sign = negative_ ? -1 : 1;
return sign * static_cast<Number::rep>(m);
return sign * static_cast<Number::rep>(mantissa_);
}
/** Returns the exponent of the external view of the Number.
@@ -610,16 +662,7 @@ Number::mantissa() const noexcept
inline constexpr int
Number::exponent() const noexcept
{
auto e = exponent_;
if (mantissa_ > maxRep)
{
XRPL_ASSERT_PARTS(
!isnormal() || (mantissa_ % 10 == 0 && mantissa_ / 10 <= maxRep),
"xrpl::Number::exponent",
"large normalized mantissa has no remainder");
++e;
}
return e;
return exponent_;
}
inline constexpr Number
@@ -715,15 +758,13 @@ Number::min() noexcept
inline Number
Number::max() noexcept
{
return Number{
false, std::min(range_.get().max, maxRep), maxExponent, unchecked{}};
return Number{false, range_.get().max, maxExponent, unchecked{}};
}
inline Number
Number::lowest() noexcept
{
return Number{
true, std::min(range_.get().max, maxRep), maxExponent, unchecked{}};
return Number{true, range_.get().max, maxExponent, unchecked{}};
}
inline bool
@@ -732,9 +773,8 @@ Number::isnormal() const noexcept
MantissaRange const& range = range_;
auto const abs_m = mantissa_;
return *this == Number{} ||
(range.min <= abs_m && abs_m <= range.max &&
(abs_m <= maxRep || abs_m % 10 == 0) && minExponent <= exponent_ &&
exponent_ <= maxExponent);
(range.min <= abs_m && abs_m <= range.max && //
minExponent <= exponent_ && exponent_ <= maxExponent);
}
template <Integral64 T>

View File

@@ -252,7 +252,7 @@ std::size_t constexpr maxMPTokenMetadataLength = 1024;
/** The maximum amount of MPTokenIssuance */
std::uint64_t constexpr maxMPTokenAmount = 0x7FFF'FFFF'FFFF'FFFFull;
static_assert(Number::maxRep >= maxMPTokenAmount);
static_assert(Number::largestMantissa >= maxMPTokenAmount);
/** The maximum length of Data payload */
std::size_t constexpr maxDataPayloadLength = 256;

View File

@@ -43,7 +43,7 @@ systemName()
/** Number of drops in the genesis account. */
constexpr XRPAmount INITIAL_XRP{100'000'000'000 * DROPS_PER_XRP};
static_assert(INITIAL_XRP.drops() == 100'000'000'000'000'000);
static_assert(Number::maxRep >= INITIAL_XRP.drops());
static_assert(Number::largestMantissa >= INITIAL_XRP.drops());
/** Returns true if the amount does not exceed the initial XRP in existence. */
inline bool

View File

@@ -279,7 +279,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 > maxRep)
if (mantissa > maxMantissa)
{
mantissa /= 10;
++exponent;
@@ -318,7 +318,7 @@ Number::Guard::doRound(rep& drops, std::string location)
auto r = round();
if (r == 1 || (r == 0 && (drops & 1) == 1))
{
if (drops >= maxRep)
if (drops >= maxMantissa())
{
static_assert(sizeof(internalrep) == sizeof(rep));
// This should be impossible, because it's impossible to represent
@@ -406,7 +406,6 @@ doNormalize(
{
auto constexpr minExponent = Number::minExponent;
auto constexpr maxExponent = Number::maxExponent;
auto constexpr maxRep = Number::maxRep;
using Guard = Number::Guard;
@@ -443,33 +442,8 @@ doNormalize(
return;
}
// When using the largeRange, "m" needs fit within an int64, even if
// the final mantissa_ is going to end up larger to fit within the
// MantissaRange. Cut it down here so that the rounding will be done while
// it's smaller.
//
// Example: 9,900,000,000,000,123,456 > 9,223,372,036,854,775,807,
// so "m" will be modified to 990,000,000,000,012,345. Then that value
// will be rounded to 990,000,000,000,012,345 or
// 990,000,000,000,012,346, depending on the rounding mode. Finally,
// mantissa_ will be "m*10" so it fits within the range, and end up as
// 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 > maxRep)
{
if (exponent_ >= maxExponent)
throw std::overflow_error("Number::normalize 1.5");
g.push(m % 10);
m /= 10;
++exponent_;
}
// Before modification, m should be within the min/max range. After
// modification, it must be less than maxRep. In other words, the original
// value should have been no more than maxRep * 10.
// (maxRep * 10 > maxMantissa)
XRPL_ASSERT_PARTS(
m <= maxRep,
m <= maxMantissa,
"xrpl::doNormalize",
"intermediate mantissa fits in int64");
mantissa_ = m;
@@ -616,7 +590,7 @@ Number::operator+=(Number const& y)
if (xn == yn)
{
xm += ym;
if (xm > maxMantissa || xm > maxRep)
if (xm > maxMantissa)
{
g.push(xm % 10);
xm /= 10;
@@ -637,7 +611,7 @@ Number::operator+=(Number const& y)
xe = ye;
xn = yn;
}
while (xm < minMantissa && xm * 10 <= maxRep)
while (xm < minMantissa)
{
xm *= 10;
xm -= g.pop();
@@ -719,7 +693,7 @@ Number::operator*=(Number const& y)
auto const& minMantissa = range.min;
auto const& maxMantissa = range.max;
while (zm > maxMantissa || zm > maxRep)
while (zm > maxMantissa)
{
// The following is optimization for:
// g.push(static_cast<unsigned>(zm % 10));
@@ -861,7 +835,7 @@ Number::operator rep() const
}
for (; offset > 0; --offset)
{
if (drops > maxRep / 10)
if (drops > largeRange.max / 10)
throw std::overflow_error("Number::operator rep() overflow");
drops *= 10;
}

View File

@@ -217,12 +217,12 @@ public:
9'999'999'999'999'999'990ULL,
-19,
Number::normalized{}}},
{Number{Number::maxRep},
{Number{Number::largestMantissa},
Number{6, -1},
Number{Number::maxRep / 10, 1}},
{Number{Number::maxRep - 1},
Number{Number::largestMantissa / 10, 1}},
{Number{Number::largestMantissa - 1},
Number{1, 0},
Number{Number::maxRep}},
Number{Number::largestMantissa}},
// Test extremes
{
// Each Number operand rounds up, so the actual mantissa is
@@ -240,11 +240,11 @@ public:
Number{2, 19},
},
{
// Does not round. Mantissas are going to be > maxRep, so if
// added together as uint64_t's, the result will overflow.
// With addition using uint128_t, there's no problem. After
// normalizing, the resulting mantissa ends up less than
// maxRep.
// Does not round. Mantissas are going to be >
// largestMantissa, so if added together as uint64_t's, the
// result will overflow. With addition using uint128_t,
// there's no problem. After normalizing, the resulting
// mantissa ends up less than largestMantissa.
Number{
false,
9'999'999'999'999'999'990ULL,
@@ -371,16 +371,24 @@ public:
{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::maxRep},
{Number{Number::largestMantissa},
Number{6, -1},
Number{Number::maxRep - 1}},
{Number{false, Number::maxRep + 1, 0, Number::normalized{}},
Number{Number::largestMantissa - 1}},
{Number{
false,
Number::largestMantissa + 1,
0,
Number::normalized{}},
Number{1, 0},
Number{Number::maxRep / 10 + 1, 1}},
{Number{false, Number::maxRep + 1, 0, Number::normalized{}},
Number{Number::largestMantissa / 10 + 1, 1}},
{Number{
false,
Number::largestMantissa + 1,
0,
Number::normalized{}},
Number{3, 0},
Number{Number::maxRep}},
{power(2, 63), Number{3, 0}, Number{Number::maxRep}},
Number{Number::largestMantissa}},
{power(2, 63), Number{3, 0}, Number{Number::largestMantissa}},
});
auto test = [this](auto const& c) {
for (auto const& [x, y, z] : c)
@@ -489,8 +497,8 @@ public:
Number{false, maxMantissa, 0, Number::normalized{}},
Number{1, 38}},
// Maximum int64 range
{Number{Number::maxRep, 0},
Number{Number::maxRep, 0},
{Number{Number::largestMantissa, 0},
Number{Number::largestMantissa, 0},
Number{85'070'591'730'234'615'85, 19}},
});
tests(cSmall, cLarge);
@@ -567,8 +575,8 @@ public:
Number::normalized{}}},
// Maximum int64 range
// 85'070'591'730'234'615'847'396'907'784'232'501'249
{Number{Number::maxRep, 0},
Number{Number::maxRep, 0},
{Number{Number::largestMantissa, 0},
Number{Number::largestMantissa, 0},
Number{85'070'591'730'234'615'84, 19}},
});
tests(cSmall, cLarge);
@@ -645,8 +653,8 @@ public:
Number::normalized{}}},
// Maximum int64 range
// 85'070'591'730'234'615'847'396'907'784'232'501'249
{Number{Number::maxRep, 0},
Number{Number::maxRep, 0},
{Number{Number::largestMantissa, 0},
Number{Number::largestMantissa, 0},
Number{85'070'591'730'234'615'84, 19}},
});
tests(cSmall, cLarge);
@@ -715,8 +723,8 @@ public:
Number{1, 38}},
// Maximum int64 range
// 85'070'591'730'234'615'847'396'907'784'232'501'249
{Number{Number::maxRep, 0},
Number{Number::maxRep, 0},
{Number{Number::largestMantissa, 0},
Number{Number::largestMantissa, 0},
Number{85'070'591'730'234'615'85, 19}},
});
tests(cSmall, cLarge);
@@ -1008,10 +1016,10 @@ public:
2,
Number{
false, 3'162'277'660'168'379'330, -9, Number::normalized{}}},
{Number{Number::maxRep},
{Number{Number::largestMantissa},
2,
Number{false, 3'037'000'499'976049692, -9, Number::normalized{}}},
{Number{Number::maxRep},
{Number{Number::largestMantissa},
4,
Number{false, 55'108'98747006743627, -14, Number::normalized{}}},
});
@@ -1070,7 +1078,7 @@ public:
Number{5, -1},
Number{0},
Number{5625, -4},
Number{Number::maxRep},
Number{Number::largestMantissa},
});
test(cSmall);
bool caught = false;
@@ -1690,7 +1698,7 @@ public:
Number const initalXrp{INITIAL_XRP};
BEAST_EXPECT(initalXrp.exponent() > 0);
Number const maxInt64{Number::maxRep};
Number const maxInt64{Number::largestMantissa};
BEAST_EXPECT(maxInt64.exponent() > 0);
// 85'070'591'730'234'615'865'843'651'857'942'052'864 - 38 digits
BEAST_EXPECT(
@@ -1710,7 +1718,7 @@ public:
Number const initalXrp{INITIAL_XRP};
BEAST_EXPECT(initalXrp.exponent() <= 0);
Number const maxInt64{Number::maxRep};
Number const maxInt64{Number::largestMantissa};
BEAST_EXPECT(maxInt64.exponent() <= 0);
// 85'070'591'730'234'615'847'396'907'784'232'501'249 - 38 digits
BEAST_EXPECT(