Continue with Step 1

- Track down and fix edge cases.
- Some refactoring and renaming for clarity and simplicity
This commit is contained in:
Ed Hennis
2025-11-12 20:32:47 -05:00
parent 343824332c
commit d2d403da90
6 changed files with 139 additions and 101 deletions

View File

@@ -22,14 +22,14 @@ template <typename T>
constexpr std::optional<int>
logTen(T value)
{
int power = 0;
int log = 0;
while (value >= 10 && value % 10 == 0)
{
value /= 10;
++power;
++log;
}
if (value == 1)
return power;
return log;
return std::nullopt;
}
@@ -50,16 +50,16 @@ using numberint128 = __int128_t;
struct MantissaRange
{
using rep = numberint128;
using internalrep = numberint128;
explicit constexpr MantissaRange(rep min_)
: min(min_), max(min_ * 10 - 1), power(logTen(min).value_or(-1))
explicit constexpr MantissaRange(internalrep min_)
: min(min_), max(min_ * 10 - 1), log(logTen(min).value_or(-1))
{
}
rep min;
rep max;
int power;
internalrep min;
internalrep max;
int log;
};
class Number
@@ -68,7 +68,7 @@ class Number
using int128_t = numberint128;
using rep = std::int64_t;
using internalrep = MantissaRange::rep;
using internalrep = MantissaRange::internalrep;
internalrep mantissa_{0};
int exponent_{std::numeric_limits<int>::lowest()};
@@ -121,11 +121,11 @@ public:
operator/=(Number const& x);
static constexpr Number
min(MantissaRange const& range) noexcept;
min() noexcept;
static constexpr Number
max(MantissaRange const& range) noexcept;
max() noexcept;
static constexpr Number
lowest(MantissaRange const& range) noexcept;
lowest() noexcept;
/** Conversions to Number are implicit and conversions away from Number
* are explicit. This design encourages and facilitates the use of Number
@@ -194,7 +194,7 @@ public:
while (ret.exponent_ < 0 && ret.mantissa_ != 0)
{
ret.exponent_ += 1;
ret.mantissa_ /= rep(10);
ret.mantissa_ /= internalrep(10);
}
// We are guaranteed that normalize() will never throw an exception
// because exponent is either negative or zero at this point.
@@ -249,17 +249,21 @@ public:
return range_.get().max;
}
inline static internalrep
mantissaPower()
inline static int
mantissaLog()
{
return range_.get().power;
return range_.get().log;
}
/// oneSmall is needed because the ranges are private
constexpr static Number
oneSmall();
/// oneLarge is needed because the ranges are private
constexpr static Number
oneLarge();
// And one is needed because it needs to choose between oneSmall and
// oneLarge based on the current range
static Number
one();
@@ -275,10 +279,12 @@ private:
constexpr static MantissaRange smallRange{1'000'000'000'000'000LL};
static_assert(isPowerOfTen(smallRange.min));
static_assert(smallRange.max == 9'999'999'999'999'999LL);
static_assert(smallRange.log == 15);
// maxint64 9,223,372,036,854,775,808
constexpr static MantissaRange largeRange{1'000'000'000'000'000'000LL};
static_assert(isPowerOfTen(largeRange.min));
static_assert(largeRange.max == internalrep(9'999'999'999'999'999'999ULL));
static_assert(largeRange.log == 18);
static_assert(largeRange.min < std::numeric_limits<std::int64_t>::max());
static_assert(largeRange.max > std::numeric_limits<std::int64_t>::max());
@@ -298,12 +304,7 @@ private:
internalrep const& maxMantissa);
constexpr bool
isnormal(MantissaRange const& range) const noexcept;
friend Number
root(Number f, unsigned d);
friend Number
root2(Number f);
isnormal() const noexcept;
class Guard;
};
@@ -421,26 +422,27 @@ operator/(Number const& x, Number const& y)
}
inline constexpr Number
Number::min(MantissaRange const& range) noexcept
Number::min() noexcept
{
return Number{range.min, minExponent, unchecked{}};
return Number{range_.get().min, minExponent, unchecked{}};
}
inline constexpr Number
Number::max(MantissaRange const& range) noexcept
Number::max() noexcept
{
return Number{range.max, maxExponent, unchecked{}};
return Number{range_.get().max, maxExponent, unchecked{}};
}
inline constexpr Number
Number::lowest(MantissaRange const& range) noexcept
Number::lowest() noexcept
{
return -Number{range.max, maxExponent, unchecked{}};
return -Number{range_.get().max, maxExponent, unchecked{}};
}
inline constexpr bool
Number::isnormal(MantissaRange const& range) const noexcept
Number::isnormal() const noexcept
{
MantissaRange const& range = range_;
auto const abs_m = mantissa_ < 0 ? -mantissa_ : mantissa_;
return range.min <= abs_m && abs_m <= range.max &&
minExponent <= exponent_ && exponent_ <= maxExponent;

View File

@@ -14,7 +14,22 @@
#ifdef _MSC_VER
#pragma message("Using boost::multiprecision::uint128_t")
#endif
#include <boost/multiprecision/cpp_int.hpp>
using uint128_t = boost::multiprecision::uint128_t;
#else // !defined(_MSC_VER)
using uint128_t = __uint128_t;
#endif // !defined(_MSC_VER)
static_assert(std::is_same_v<uint128_t, ripple::numberuint128>);
namespace std {
template <>
struct make_unsigned<ripple::numberint128>
{
using type = typename uint128_t;
};
} // namespace std
namespace ripple {
@@ -61,14 +76,9 @@ public:
is_negative() const noexcept;
// add a digit
template <class T>
void
push(numberuint128 d) noexcept;
#ifdef _MSC_VER
void
push(numberint128 d) noexcept;
void
push(std::int64_t d) noexcept;
#endif
push(T d) noexcept;
// recover a digit
unsigned
@@ -79,6 +89,10 @@ public:
// tie, round towards even.
int
round() noexcept;
private:
void
doPush(unsigned d) noexcept;
};
inline void
@@ -100,29 +114,20 @@ Number::Guard::is_negative() const noexcept
}
inline void
Number::Guard::push(numberuint128 d128) noexcept
Number::Guard::doPush(unsigned d) noexcept
{
unsigned d = static_cast<unsigned>(d128);
xbit_ = xbit_ || (digits_ & 0x0000'0000'0000'000F) != 0;
digits_ >>= 4;
digits_ |= (d & 0x0000'0000'0000'000FULL) << 60;
}
#ifdef _MSC_VER
void
Number::Guard::push(numberint128 d) noexcept
template <class T>
inline void
Number::Guard::push(T d) noexcept
{
push(static_cast<numberuint128>(d));
doPush(static_cast<unsigned>(d));
}
void
Number::Guard::push(std::int64_t d) noexcept
{
push(static_cast<numberuint128>(d));
}
#endif
inline unsigned
Number::Guard::pop() noexcept
{
@@ -178,23 +183,27 @@ constexpr Number
Number::oneSmall()
{
return Number{
Number::smallRange.min, -Number::smallRange.power, Number::unchecked{}};
Number::smallRange.min, -Number::smallRange.log, Number::unchecked{}};
};
constexpr Number oneSml = Number::oneSmall();
constexpr Number
Number::oneLarge()
{
return Number{
Number::largeRange.min, -Number::largeRange.power, Number::unchecked{}};
Number::largeRange.min, -Number::largeRange.log, Number::unchecked{}};
};
constexpr Number oneLrg = Number::oneLarge();
Number
Number::one()
{
if (&range_.get() == &smallRange)
return oneSmall();
return oneSml;
XRPL_ASSERT(&range_.get() == &largeRange, "Number::one() : valid range_");
return oneLarge();
return oneLrg;
}
// Use the member names in this static function for now so the diff is cleaner
@@ -205,17 +214,16 @@ Number::normalize(
internalrep const& minMantissa,
internalrep const& maxMantissa)
{
constexpr Number zero = Number{};
if (mantissa_ == 0)
{
constexpr Number zero = Number{};
mantissa_ = zero.mantissa_;
exponent_ = zero.exponent_;
return;
}
bool const negative = (mantissa_ < 0);
auto m = static_cast<std::make_unsigned_t<rep>>(mantissa_);
if (negative)
m = -m;
auto m = static_cast<std::make_unsigned_t<internalrep>>(
negative ? -mantissa_ : mantissa_);
while ((m < minMantissa) && (exponent_ > minExponent))
{
m *= 10;
@@ -235,7 +243,6 @@ Number::normalize(
mantissa_ = m;
if ((exponent_ < minExponent) || (mantissa_ < minMantissa))
{
constexpr Number zero = Number{};
mantissa_ = zero.mantissa_;
exponent_ = zero.exponent_;
return;
@@ -281,7 +288,7 @@ Number::operator+=(Number const& y)
return *this;
}
XRPL_ASSERT(
isnormal(Number::range_) && y.isnormal(Number::range_),
isnormal() && y.isnormal(),
"ripple::Number::operator+=(Number) : is normal");
auto xm = mantissa();
auto xe = exponent();
@@ -394,7 +401,7 @@ Number::operator+=(Number const& y)
// Derived from Hacker's Delight Second Edition Chapter 10
// by Henry S. Warren, Jr.
static inline unsigned
divu10(numberuint128& u)
divu10(uint128_t& u)
{
// q = u * 0.75
auto q = (u >> 1) + (u >> 2);
@@ -426,7 +433,7 @@ Number::operator*=(Number const& y)
return *this;
}
XRPL_ASSERT(
isnormal(Number::range_) && y.isnormal(Number::range_),
isnormal() && y.isnormal(),
"ripple::Number::operator*=(Number) : is normal");
auto xm = mantissa();
auto xe = exponent();
@@ -444,7 +451,7 @@ Number::operator*=(Number const& y)
ym = -ym;
yn = -1;
}
auto zm = numberuint128(xm) * numberuint128(ym);
auto zm = uint128_t(xm) * uint128_t(ym);
auto ze = xe + ye;
auto zn = xn * yn;
Guard g;
@@ -461,7 +468,7 @@ Number::operator*=(Number const& y)
g.push(divu10(zm));
++ze;
}
xm = static_cast<rep>(zm);
xm = static_cast<internalrep>(zm);
xe = ze;
auto r = g.round();
if (r == 1 || (r == 0 && (xm & 1) == 1))
@@ -485,7 +492,7 @@ Number::operator*=(Number const& y)
mantissa_ = xm * zn;
exponent_ = xe;
XRPL_ASSERT(
isnormal(Number::range_) || *this == Number{},
isnormal() || *this == Number{},
"ripple::Number::operator*=(Number) : result is normal");
return *this;
}
@@ -514,12 +521,15 @@ Number::operator/=(Number const& y)
dp = -1;
}
// Shift by 10^17 gives greatest precision while not overflowing
// numberuint128 or the cast back to int64_t
// uint128_t or the cast back to int64_t
// TODO: Can/should this be made bigger for largeRange?
constexpr numberuint128 f = 100'000'000'000'000'000;
// log(2^127,10) ~ 38.2
// largeRange.log = 18
// f can be up to 10^(37-18) = 10^19 safely
constexpr uint128_t f = 100'000'000'000'000'000;
static_assert(f == smallRange.min * 100);
static_assert(smallRange.power == 15);
mantissa_ = numberuint128(nm) * f / numberuint128(dm);
static_assert(smallRange.log == 15);
mantissa_ = uint128_t(nm) * f / uint128_t(dm);
exponent_ = ne - de - 17;
mantissa_ *= np * dp;
normalize();
@@ -543,14 +553,19 @@ Number::operator rep() const
g.push(drops % 10);
drops /= 10;
}
if (offset == 0 && drops > std::numeric_limits<rep>::max())
{
// If offset == 0, then the loop won't run, and the overflow check
// won't be made, but a int128 can overflow int64 by itself, so
// check here.
throw std::overflow_error("Number::operator rep() overflow");
}
for (; offset > 0; --offset)
{
if (drops > std::numeric_limits<rep>::max() / 10)
throw std::overflow_error("Number::operator rep() overflow");
drops *= 10;
}
if (drops > std::numeric_limits<rep>::max() / 10)
throw std::overflow_error("Number::operator rep() overflow");
auto r = g.round();
if (r == 1 || (r == 0 && (drops & 1) == 1))
{
@@ -573,9 +588,9 @@ to_string(Number const& amount)
auto mantissa = amount.mantissa();
// Use scientific notation for exponents that are too small or too large
auto const rangePower = Number::mantissaPower();
auto const rangeLog = Number::mantissaLog();
if (((exponent != 0) &&
((exponent < -(rangePower + 10)) || (exponent > -(rangePower - 10)))))
((exponent < -(rangeLog + 10)) || (exponent > -(rangeLog - 10)))))
{
std::string ret = to_string(mantissa);
ret.append(1, 'e');
@@ -591,11 +606,12 @@ to_string(Number const& amount)
negative = true;
}
// TODO: These numbers are probably wrong for largeRange
XRPL_ASSERT(
exponent + 43 > 0, "ripple::to_string(Number) : minimum exponent");
ptrdiff_t const pad_prefix = 27;
ptrdiff_t const pad_suffix = 23;
ptrdiff_t const pad_prefix = Number::mantissaLog() + 12;
ptrdiff_t const pad_suffix = Number::mantissaLog() + 8;
std::string const raw_value(to_string(mantissa));
std::string val;
@@ -605,7 +621,7 @@ to_string(Number const& amount)
val.append(raw_value);
val.append(pad_suffix, '0');
ptrdiff_t const offset(exponent + 43);
ptrdiff_t const offset(exponent + pad_prefix + 16);
auto pre_from(val.begin());
auto const pre_to(val.begin() + offset);

View File

@@ -1,8 +1,10 @@
#include <xrpl/protocol/IOUAmount.h>
//
#include <xrpl/basics/LocalValue.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/protocol/IOUAmount.h>
#include <xrpl/protocol/STAmount.h>
#include <boost/multiprecision/cpp_int.hpp>
@@ -40,11 +42,13 @@ setSTNumberSwitchover(bool v)
}
/* The range for the mantissa when normalized */
static std::int64_t constexpr minMantissa = 1'000'000'000'000'000ull;
static std::int64_t constexpr maxMantissa = minMantissa * 10 - 1;
// log(2^63,10) ~ 18.96
//
static std::int64_t constexpr minMantissa = STAmount::cMinValue;
static std::int64_t constexpr maxMantissa = STAmount::cMaxValue;
/* The range for the exponent when normalized */
static int constexpr minExponent = -96;
static int constexpr maxExponent = 80;
static int constexpr minExponent = STAmount::cMinOffset;
static int constexpr maxExponent = STAmount::cMaxOffset;
std::pair<IOUAmount::mantissa_type, IOUAmount::exponent_type>
IOUAmount::scaleNumber(Number const& number)

View File

@@ -310,8 +310,8 @@ STAmount&
STAmount::operator=(IOUAmount const& iou)
{
XRPL_ASSERT(
native() == false,
"ripple::STAmount::operator=(IOUAmount) : is not XRP");
integral() == false,
"ripple::STAmount::operator=(IOUAmount) : is not integral");
mOffset = iou.exponent();
mIsNegative = iou < beast::zero;
if (mIsNegative)
@@ -851,8 +851,9 @@ STAmount::canonicalize()
if (getSTNumberSwitchover())
{
auto const value = unsafe_cast<std::int64_t>(mValue);
Number num(
mIsNegative ? -mValue : mValue, mOffset, Number::unchecked{});
mIsNegative ? -value : value, mOffset, Number::unchecked{});
auto set = [&](auto const& val) {
mIsNegative = val.value() < 0;
mValue = mIsNegative ? -val.value() : val.value();

View File

@@ -50,11 +50,25 @@ STNumber::add(Serializer& s) const
XRPL_ASSERT(
getFName().fieldType == getSType(),
"ripple::STNumber::add : field type match");
constexpr std::int64_t min = 100'000'000'000'000'000LL;
constexpr std::int64_t max = min * 10 - 1;
auto const [mantissa, exponent] = value_.normalizeToRange(min, max);
s.add64(mantissa);
s.add32(exponent);
if (value_.mantissa() <= std::numeric_limits<std::int64_t>::max() &&
value_.mantissa() >= std::numeric_limits<std::int64_t>::min())
{
// If the mantissa fits in the range of std::int64_t, write it directly.
// This preserves the maximum available precision.
// With the small range, all numbers should be written this way. With
// the large range, it's likely that most numbers will be written this
// way.
s.add64(static_cast<std::int64_t>(value_.mantissa()));
s.add32(value_.exponent());
}
else
{
constexpr std::int64_t min = 100'000'000'000'000'000LL;
constexpr std::int64_t max = min * 10 - 1;
auto const [mantissa, exponent] = value_.normalizeToRange(min, max);
s.add64(mantissa);
s.add32(exponent);
}
}
Number const&

View File

@@ -17,14 +17,15 @@ public:
{
testcase("zero");
Number const z{0, 0};
for (Number const& z : {Number{0, 0}, Number{0}})
{
BEAST_EXPECT(z.mantissa() == 0);
BEAST_EXPECT(z.exponent() == Number{}.exponent());
BEAST_EXPECT(z.mantissa() == 0);
BEAST_EXPECT(z.exponent() == Number{}.exponent());
BEAST_EXPECT((z + z) == z);
BEAST_EXPECT((z - z) == z);
BEAST_EXPECT(z == -z);
BEAST_EXPECT((z + z) == z);
BEAST_EXPECT((z - z) == z);
BEAST_EXPECT(z == -z);
}
}
void
@@ -738,12 +739,12 @@ public:
BEAST_EXPECT(
std::numeric_limits<std::int64_t>::max() > INITIAL_XRP.drops());
BEAST_EXPECT(Number::maxMantissa() > INITIAL_XRP.drops());
BEAST_EXPECT(Number::maxMantissa() < INITIAL_XRP.drops());
Number initalXrp{INITIAL_XRP};
BEAST_EXPECT(initalXrp.exponent() <= 0);
BEAST_EXPECT(initalXrp.exponent() > 0);
Number maxInt64{std::numeric_limits<std::int64_t>::max()};
BEAST_EXPECT(maxInt64.exponent() <= 0);
BEAST_EXPECT(maxInt64.exponent() > 0);
// TODO: square maxInt64 and square Number::max()