Compare commits

...

4 Commits

Author SHA1 Message Date
Ed Hennis
e305a7546e clang-tidy: Add headers 2026-07-16 20:05:38 -04:00
Ed Hennis
cbf2c87d7c Fix a couple of issues: fix error check, remove incorrect test 2026-07-16 19:27:37 -04:00
Ed Hennis
e0ced0e375 Merge branch 'develop' into ximinez/number-exponents 2026-07-16 14:30:05 -04:00
Ed Hennis
e17d381879 perf: Speed up addition time for drastically different exponents
- When dropping digits, short circuit the loop if the smaller value
  loses all significant digits in both the mantissa, and the Guard.
- Adds unit tests demonstrating some extreme examples.
2026-07-15 18:45:52 -04:00
3 changed files with 111 additions and 56 deletions

View File

@@ -260,6 +260,11 @@ public:
unsigned
pop() noexcept;
// if true, there are no recoverable digits in the guard, though there may be dropped digits
// (xbit_)
[[nodiscard]] bool
unrecoverable() const noexcept;
// if true, there are no digits in the guard, including dropped digits (xbit_)
[[nodiscard]] bool
empty() const noexcept;
@@ -277,6 +282,17 @@ public:
void
doDropDigit(T& mantissa, int& exponent) noexcept;
/**
* Drop a digit from the mantissa, and increment the exponent, storing the dropped digit in
* this Guard.
*
* If a drop will not do anything meaningful (there are no recoverable digits in the guard, and
* the mantissa is 0), and if targetExponent > exponent, simply set exponent to targetExponent.
*/
template <class T>
void
doDropDigitWithTarget(T& mantissa, int& exponent, int const targetExponent) noexcept;
// Modify the result to the correctly rounded value
template <UnsignedMantissa T>
void
@@ -374,10 +390,16 @@ Number::Guard::pop() noexcept
return d;
}
inline bool
Number::Guard::unrecoverable() const noexcept
{
return digits_ == 0;
}
inline bool
Number::Guard::empty() const noexcept
{
return digits_ == 0 && !xbit_;
return unrecoverable() && !xbit_;
}
template <class T>
@@ -401,6 +423,22 @@ Number::Guard::doDropDigit<uint128_t>(uint128_t& mantissa, int& exponent) noexce
++exponent;
}
template <class T>
void
Number::Guard::doDropDigitWithTarget(T& mantissa, int& exponent, int const targetExponent) noexcept
{
XRPL_ASSERT(
targetExponent > exponent, "Number::Guard::doDropDigitWithTarget : something to do");
if (mantissa == 0 && unrecoverable() && targetExponent > exponent)
{
// No number of dropped digits is going to change any of the operative parameters at this
// point.
exponent = targetExponent;
return;
}
doDropDigit(mantissa, exponent);
}
template <UnsignedMantissa T>
void
Number::Guard::pushOverflow(T mantissa)
@@ -935,6 +973,8 @@ Number::operator+=(Number const& y)
// 1. First, shrink the mantissa of shrinkM/shrinkE while shrinkM ends in 0.
while (shrinkE < expandE && shrinkM % 10 == 0)
{
// Don't use doDropDigitWithTarget here, because the loop will stop before the
// mantissa gets to 0.
g.doDropDigit(shrinkM, shrinkE);
}
@@ -952,7 +992,7 @@ Number::operator+=(Number const& y)
// digits will be put into the Guard. This is the only step for non-Enabled330 modes.
while (shrinkE < expandE)
{
g.doDropDigit(shrinkM, shrinkE);
g.doDropDigitWithTarget(shrinkM, shrinkE, expandE);
}
};

View File

@@ -12,6 +12,7 @@
#include <exception>
#include <initializer_list>
#include <limits>
#include <sstream>
#include <stdexcept>
#include <string>
#include <type_traits>
@@ -176,61 +177,32 @@ struct STNumber_test : public beast::unit_test::Suite
numberFromJson(sfNumber, std::to_string(kUMax)) ==
STNumber(sfNumber, Number(kUMax, 0)));
auto const expectJsonThrows = [this](
json::Value const& num, std::string const& expected) {
try
{
numberFromJson(sfNumber, num);
fail();
}
catch (std::exception const& e)
{
std::ostringstream out;
out << "Json: " << num.asString() << " got exception: " << e.what()
<< ", expected: " << expected;
BEAST_EXPECTS(std::string(e.what()) == expected, out.str());
}
};
// Obvious overflows tested here
expectJsonThrows("1e2000000", "Number::normalize 2");
expectJsonThrows("1e2000000000", "Number::normalize 2");
// Obvious non-numbers tested here
try
{
auto _ = numberFromJson(sfNumber, "");
BEAST_EXPECT(false);
}
catch (std::runtime_error const& e)
{
std::string const expected = "'' is not a number";
BEAST_EXPECT(e.what() == expected);
}
try
{
auto _ = numberFromJson(sfNumber, "e");
BEAST_EXPECT(false);
}
catch (std::runtime_error const& e)
{
std::string const expected = "'e' is not a number";
BEAST_EXPECT(e.what() == expected);
}
try
{
auto _ = numberFromJson(sfNumber, "1e");
BEAST_EXPECT(false);
}
catch (std::runtime_error const& e)
{
std::string const expected = "'1e' is not a number";
BEAST_EXPECT(e.what() == expected);
}
try
{
auto _ = numberFromJson(sfNumber, "e2");
BEAST_EXPECT(false);
}
catch (std::runtime_error const& e)
{
std::string const expected = "'e2' is not a number";
BEAST_EXPECT(e.what() == expected);
}
try
{
auto _ = numberFromJson(sfNumber, json::Value());
BEAST_EXPECT(false);
}
catch (std::runtime_error const& e)
{
std::string const expected = "not a number";
BEAST_EXPECT(e.what() == expected);
}
expectJsonThrows("", "'' is not a number");
expectJsonThrows("e", "'e' is not a number");
expectJsonThrows("1e", "'1e' is not a number");
expectJsonThrows("e2", "'e2' is not a number");
expectJsonThrows(json::Value(), "not a number");
try
{

View File

@@ -1,5 +1,6 @@
#include <xrpl/basics/Number.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/protocol/IOUAmount.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/STAmount.h>
@@ -183,6 +184,17 @@ TEST(NumberTest, limits)
}
EXPECT_TRUE(caught);
try
{
Number{1, 2000000, Number::Normalized{}};
ADD_FAILURE();
}
catch (std::overflow_error const& e)
{
std::string const expected = "Number::normalize 2";
EXPECT_EQ(e.what(), expected) << e.what();
}
if (scale == MantissaRange::MantissaScale::Large330)
{
// Normalization with the other scales, including the older large mantissa scales, will
@@ -403,6 +415,37 @@ TEST(NumberTest, add)
}
EXPECT_TRUE(caught);
}
// Special case: Exponents at each end of the allowable range
for (auto const round :
{Number::RoundingMode::ToNearest,
Number::RoundingMode::TowardsZero,
Number::RoundingMode::Downward,
Number::RoundingMode::Upward})
{
NumberRoundModeGuard const rg{round};
auto const x =
Number{Number::minMantissa(), Number::kMaxExponent, Number::Normalized{}};
auto const y =
Number{Number::minMantissa(), Number::kMinExponent, Number::Normalized{}};
EXPECT_EQ(x.exponent(), Number::kMaxExponent);
EXPECT_NE(x, beast::kZero);
EXPECT_EQ(y.exponent(), Number::kMinExponent);
EXPECT_NE(y, beast::kZero);
auto const result = x + y;
if (round == Number::RoundingMode::Upward)
{
// Rounding upward will take that little x-bit and round result up to the next
// representable value.
EXPECT_NE(result, x);
EXPECT_EQ(result, (Number{x.mantissa() + 1, x.exponent()}));
}
else
{
EXPECT_EQ(result, x);
}
}
}
}