Add optional enforcement of valid integer range to Number

This commit is contained in:
Ed Hennis
2025-11-04 20:44:45 -05:00
parent 173f9f7bb0
commit 3cb447a4fe
3 changed files with 317 additions and 7 deletions

View File

@@ -13,16 +13,47 @@ class Number;
std::string std::string
to_string(Number const& amount); to_string(Number const& amount);
template <typename T>
constexpr bool
isPowerOfTen(T value)
{
while (value >= 10 && value % 10 == 0)
value /= 10;
return value == 1;
}
class Number class Number
{ {
public:
/** Describes whether and how to enforce this number as an integer.
*
* - none: No enforcement. The value may vary freely. This is the default.
* - weak: If the absolute value is greater than maxIntValue, valid() will
* return false.
* - strong: Assignment operations will throw if the absolute value is above
* maxIntValue.
*/
enum EnforceInteger { none, weak, strong };
private:
using rep = std::int64_t; using rep = std::int64_t;
rep mantissa_{0}; rep mantissa_{0};
int exponent_{std::numeric_limits<int>::lowest()}; int exponent_{std::numeric_limits<int>::lowest()};
// The enforcement setting is not serialized, and does not affect the
// ledger. If not "none", the value is checked to be within the valid
// integer range. With "strong", the checks will be made as automatic as
// possible.
EnforceInteger enforceInteger_ = none;
public: public:
// The range for the mantissa when normalized // The range for the mantissa when normalized
constexpr static std::int64_t minMantissa = 1'000'000'000'000'000LL; constexpr static rep minMantissa = 1'000'000'000'000'000LL;
constexpr static std::int64_t maxMantissa = 9'999'999'999'999'999LL; static_assert(isPowerOfTen(minMantissa));
constexpr static rep maxMantissa = minMantissa * 10 - 1;
static_assert(maxMantissa == 9'999'999'999'999'999LL);
constexpr static rep maxIntValue = minMantissa / 10;
// The range for the exponent when normalized // The range for the exponent when normalized
constexpr static int minExponent = -32768; constexpr static int minExponent = -32768;
@@ -35,15 +66,33 @@ public:
explicit constexpr Number() = default; explicit constexpr Number() = default;
Number(rep mantissa); Number(rep mantissa, EnforceInteger enforce = none);
explicit Number(rep mantissa, int exponent); explicit Number(rep mantissa, int exponent, EnforceInteger enforce = none);
explicit constexpr Number(rep mantissa, int exponent, unchecked) noexcept; explicit constexpr Number(rep mantissa, int exponent, unchecked) noexcept;
constexpr Number(Number const& other) = default;
constexpr Number(Number&& other) = default;
~Number() = default;
constexpr Number&
operator=(Number const& other);
constexpr Number&
operator=(Number&& other);
constexpr rep constexpr rep
mantissa() const noexcept; mantissa() const noexcept;
constexpr int constexpr int
exponent() const noexcept; exponent() const noexcept;
void
setIntegerEnforcement(EnforceInteger enforce);
EnforceInteger
integerEnforcement() const noexcept;
bool
valid() const noexcept;
constexpr Number constexpr Number
operator+() const noexcept; operator+() const noexcept;
constexpr Number constexpr Number
@@ -184,6 +233,9 @@ public:
private: private:
static thread_local rounding_mode mode_; static thread_local rounding_mode mode_;
void
checkInteger(char const* what) const;
void void
normalize(); normalize();
constexpr bool constexpr bool
@@ -197,16 +249,52 @@ inline constexpr Number::Number(rep mantissa, int exponent, unchecked) noexcept
{ {
} }
inline Number::Number(rep mantissa, int exponent) inline Number::Number(rep mantissa, int exponent, EnforceInteger enforce)
: mantissa_{mantissa}, exponent_{exponent} : mantissa_{mantissa}, exponent_{exponent}, enforceInteger_(enforce)
{ {
normalize(); normalize();
checkInteger("Number::Number integer overflow");
} }
inline Number::Number(rep mantissa) : Number{mantissa, 0} inline Number::Number(rep mantissa, EnforceInteger enforce)
: Number{mantissa, 0, enforce}
{ {
} }
constexpr Number&
Number::operator=(Number const& other)
{
if (this != &other)
{
mantissa_ = other.mantissa_;
exponent_ = other.exponent_;
enforceInteger_ = std::max(enforceInteger_, other.enforceInteger_);
checkInteger("Number::operator= integer overflow");
}
return *this;
}
constexpr Number&
Number::operator=(Number&& other)
{
if (this != &other)
{
// std::move doesn't really do anything for these types, but
// this is future-proof in case the types ever change
mantissa_ = std::move(other.mantissa_);
exponent_ = std::move(other.exponent_);
if (other.enforceInteger_ > enforceInteger_)
enforceInteger_ = std::move(other.enforceInteger_);
checkInteger("Number::operator= integer overflow");
}
return *this;
}
inline constexpr Number::rep inline constexpr Number::rep
Number::mantissa() const noexcept Number::mantissa() const noexcept
{ {
@@ -219,6 +307,20 @@ Number::exponent() const noexcept
return exponent_; return exponent_;
} }
inline void
Number::setIntegerEnforcement(EnforceInteger enforce)
{
enforceInteger_ = enforce;
checkInteger("Number::setIntegerEnforcement integer overflow");
}
inline Number::EnforceInteger
Number::integerEnforcement() const noexcept
{
return enforceInteger_;
}
inline constexpr Number inline constexpr Number
Number::operator+() const noexcept Number::operator+() const noexcept
{ {

View File

@@ -155,6 +155,13 @@ Number::Guard::round() noexcept
constexpr Number one{1000000000000000, -15, Number::unchecked{}}; constexpr Number one{1000000000000000, -15, Number::unchecked{}};
void
Number::checkInteger(char const* what) const
{
if (enforceInteger_ == strong && !valid())
throw std::overflow_error(what);
}
void void
Number::normalize() Number::normalize()
{ {
@@ -207,9 +214,27 @@ Number::normalize()
mantissa_ = -mantissa_; mantissa_ = -mantissa_;
} }
bool
Number::valid() const noexcept
{
if (enforceInteger_ != none)
{
static Number const max = maxIntValue;
static Number const maxNeg = -maxIntValue;
// Avoid making a copy
if (mantissa_ < 0)
return *this >= maxNeg;
return *this <= max;
}
return true;
}
Number& Number&
Number::operator+=(Number const& y) Number::operator+=(Number const& y)
{ {
// The strictest setting prevails
enforceInteger_ = std::max(enforceInteger_, y.enforceInteger_);
if (y == Number{}) if (y == Number{})
return *this; return *this;
if (*this == Number{}) if (*this == Number{})
@@ -322,6 +347,9 @@ Number::operator+=(Number const& y)
} }
mantissa_ = xm * xn; mantissa_ = xm * xn;
exponent_ = xe; exponent_ = xe;
checkInteger("Number::addition integer overflow");
return *this; return *this;
} }
@@ -356,6 +384,9 @@ divu10(uint128_t& u)
Number& Number&
Number::operator*=(Number const& y) Number::operator*=(Number const& y)
{ {
// The strictest setting prevails
enforceInteger_ = std::max(enforceInteger_, y.enforceInteger_);
if (*this == Number{}) if (*this == Number{})
return *this; return *this;
if (y == Number{}) if (y == Number{})
@@ -422,12 +453,18 @@ Number::operator*=(Number const& y)
XRPL_ASSERT( XRPL_ASSERT(
isnormal() || *this == Number{}, isnormal() || *this == Number{},
"ripple::Number::operator*=(Number) : result is normal"); "ripple::Number::operator*=(Number) : result is normal");
checkInteger("Number::multiplication integer overflow");
return *this; return *this;
} }
Number& Number&
Number::operator/=(Number const& y) Number::operator/=(Number const& y)
{ {
// The strictest setting prevails
enforceInteger_ = std::max(enforceInteger_, y.enforceInteger_);
if (y == Number{}) if (y == Number{})
throw std::overflow_error("Number: divide by 0"); throw std::overflow_error("Number: divide by 0");
if (*this == Number{}) if (*this == Number{})
@@ -455,6 +492,9 @@ Number::operator/=(Number const& y)
exponent_ = ne - de - 17; exponent_ = ne - de - 17;
mantissa_ *= np * dp; mantissa_ *= np * dp;
normalize(); normalize();
checkInteger("Number::division integer overflow");
return *this; return *this;
} }

View File

@@ -2,6 +2,7 @@
#include <xrpl/beast/unit_test.h> #include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/IOUAmount.h> #include <xrpl/protocol/IOUAmount.h>
#include <xrpl/protocol/STAmount.h> #include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/SystemParameters.h>
#include <sstream> #include <sstream>
#include <tuple> #include <tuple>
@@ -725,6 +726,172 @@ public:
BEAST_EXPECT(Number(-100, -30000).truncate() == Number(0, 0)); BEAST_EXPECT(Number(-100, -30000).truncate() == Number(0, 0));
} }
void
testInteger()
{
testcase("Integer enforcement");
using namespace std::string_literals;
{
Number a{100};
BEAST_EXPECT(a.integerEnforcement() == Number::none);
BEAST_EXPECT(a.valid());
a = Number{1, 30};
BEAST_EXPECT(a.valid());
a = -100;
BEAST_EXPECT(a.valid());
}
{
Number a{100, Number::weak};
BEAST_EXPECT(a.integerEnforcement() == Number::weak);
BEAST_EXPECT(a.valid());
a = Number{1, 30, Number::none};
BEAST_EXPECT(!a.valid());
a = -100;
BEAST_EXPECT(a.integerEnforcement() == Number::weak);
BEAST_EXPECT(a.valid());
a = Number{5, Number::strong};
BEAST_EXPECT(a.integerEnforcement() == Number::strong);
BEAST_EXPECT(a.valid());
}
{
Number a{100, Number::strong};
BEAST_EXPECT(a.integerEnforcement() == Number::strong);
BEAST_EXPECT(a.valid());
try
{
a = Number{1, 30};
BEAST_EXPECT(false);
}
catch (std::overflow_error const& e)
{
BEAST_EXPECT(e.what() == "Number::operator= integer overflow"s);
// The throw is done _after_ the number is updated.
BEAST_EXPECT((a == Number{1, 30}));
}
BEAST_EXPECT(!a.valid());
a = -100;
BEAST_EXPECT(a.integerEnforcement() == Number::strong);
BEAST_EXPECT(a.valid());
}
{
Number a{INITIAL_XRP.drops(), Number::weak};
BEAST_EXPECT(!a.valid());
a = -a;
BEAST_EXPECT(!a.valid());
try
{
a.setIntegerEnforcement(Number::strong);
BEAST_EXPECT(false);
}
catch (std::overflow_error const& e)
{
BEAST_EXPECT(
e.what() ==
"Number::setIntegerEnforcement integer overflow"s);
// The throw is internal to the operator before the result is
// assigned to the Number
BEAST_EXPECT(a == -INITIAL_XRP);
BEAST_EXPECT(!a.valid());
}
try
{
++a;
BEAST_EXPECT(false);
}
catch (std::overflow_error const& e)
{
BEAST_EXPECT(e.what() == "Number::addition integer overflow"s);
// The throw is internal to the operator before the result is
// assigned to the Number
BEAST_EXPECT(a == -INITIAL_XRP);
BEAST_EXPECT(!a.valid());
}
a = Number::maxIntValue;
try
{
++a;
BEAST_EXPECT(false);
}
catch (std::overflow_error const& e)
{
BEAST_EXPECT(e.what() == "Number::addition integer overflow"s);
// This time, the throw is done _after_ the number is updated.
BEAST_EXPECT(a == Number::maxIntValue + 1);
BEAST_EXPECT(!a.valid());
}
a = -Number::maxIntValue;
try
{
--a;
BEAST_EXPECT(false);
}
catch (std::overflow_error const& e)
{
BEAST_EXPECT(e.what() == "Number::addition integer overflow"s);
// This time, the throw is done _after_ the number is updated.
BEAST_EXPECT(a == -Number::maxIntValue - 1);
BEAST_EXPECT(!a.valid());
}
a = Number(1, 10);
try
{
a *= Number(1, 10);
BEAST_EXPECT(false);
}
catch (std::overflow_error const& e)
{
BEAST_EXPECT(
e.what() == "Number::multiplication integer overflow"s);
// The throw is done _after_ the number is updated.
BEAST_EXPECT((a == Number{1, 20}));
BEAST_EXPECT(!a.valid());
}
try
{
a = Number::maxIntValue * 2;
BEAST_EXPECT(false);
}
catch (std::overflow_error const& e)
{
BEAST_EXPECT(e.what() == "Number::operator= integer overflow"s);
// The throw is done _after_ the number is updated.
BEAST_EXPECT((a == Number{2, 14}));
BEAST_EXPECT(!a.valid());
}
try
{
a = Number(3, 15, Number::strong);
BEAST_EXPECT(false);
}
catch (std::overflow_error const& e)
{
BEAST_EXPECT(e.what() == "Number::Number integer overflow"s);
// The Number doesn't get updated because the ctor throws
BEAST_EXPECT((a == Number{2, 14}));
BEAST_EXPECT(!a.valid());
}
a = Number(1, 10);
try
{
a /= Number(1, -10);
BEAST_EXPECT(false);
}
catch (std::overflow_error const& e)
{
BEAST_EXPECT(e.what() == "Number::division integer overflow"s);
// The throw is done _after_ the number is updated.
BEAST_EXPECT((a == Number{1, 20}));
BEAST_EXPECT(!a.valid());
}
a /= Number(1, 15);
BEAST_EXPECT((a == Number{1, 5}));
BEAST_EXPECT(a.valid());
}
}
void void
run() override run() override
{ {
@@ -746,6 +913,7 @@ public:
test_inc_dec(); test_inc_dec();
test_toSTAmount(); test_toSTAmount();
test_truncate(); test_truncate();
testInteger();
} }
}; };