LibCrypto: Add a rounding mode to UnsignedBigInteger::to_double

This allows using different options for rounding, like IEEE
roundTiesToEven, which is the mode that JS requires.

Also fix that the last word read from the bigint for the mantissa could
be shifted incorrectly leading to incorrect results.
This commit is contained in:
davidot 2022-08-25 17:56:34 +02:00 committed by Linus Groh
parent 8ba6e96d05
commit 77d71a5ffd
5 changed files with 144 additions and 24 deletions

View file

@ -750,7 +750,7 @@ EXPECT_EQUAL_TO(zero, -0.0);
TEST_CASE(to_double)
{
#define EXPECT_TO_EQUAL_DOUBLE(bigint, double_value) \
EXPECT_EQ((bigint).to_double(), double_value)
EXPECT_EQ((bigint).to_double(Crypto::UnsignedBigInteger::RoundingMode::RoundTowardZero), double_value)
EXPECT_TO_EQUAL_DOUBLE(Crypto::UnsignedBigInteger(0), 0.0);
// Make sure we don't get negative zero!
@ -825,7 +825,41 @@ TEST_CASE(to_double)
EXPECT_TO_EQUAL_DOUBLE(Crypto::SignedBigInteger::from_base(10, "2345678901234567890"sv),
2345678901234567680.0);
EXPECT_EQ(1234567890123456800.0, 1234567890123456768.0);
EXPECT_EQ(
Crypto::UnsignedBigInteger::from_base(16, "1fffffffffffff00"sv).to_double(Crypto::UnsignedBigInteger::RoundingMode::IEEERoundAndTiesToEvenMantissa),
2305843009213693696.0);
EXPECT_EQ(
Crypto::UnsignedBigInteger::from_base(16, "1fffffffffffff00"sv).to_double(Crypto::UnsignedBigInteger::RoundingMode::RoundTowardZero),
2305843009213693696.0);
EXPECT_EQ(
Crypto::UnsignedBigInteger::from_base(16, "1fffffffffffff80"sv).to_double(Crypto::UnsignedBigInteger::RoundingMode::IEEERoundAndTiesToEvenMantissa),
2305843009213693952.0);
EXPECT_EQ(Crypto::UnsignedBigInteger::from_base(16, "20000000000001"sv).to_double(Crypto::UnsignedBigInteger::RoundingMode::IEEERoundAndTiesToEvenMantissa),
9007199254740992.0);
EXPECT_EQ(Crypto::UnsignedBigInteger::from_base(16, "20000000000002"sv).to_double(Crypto::UnsignedBigInteger::RoundingMode::IEEERoundAndTiesToEvenMantissa),
9007199254740994.0);
// 2^53 = 20000000000000, +3 Rounds up because of tiesRoundToEven
EXPECT_EQ(Crypto::UnsignedBigInteger::from_base(16, "20000000000003"sv).to_double(Crypto::UnsignedBigInteger::RoundingMode::IEEERoundAndTiesToEvenMantissa),
9007199254740996.0);
// +4 is exactly 9007199254740996
EXPECT_EQ(Crypto::UnsignedBigInteger::from_base(16, "20000000000004"sv).to_double(Crypto::UnsignedBigInteger::RoundingMode::IEEERoundAndTiesToEvenMantissa),
9007199254740996.0);
// +5 rounds down because of tiesRoundToEven
EXPECT_EQ(Crypto::UnsignedBigInteger::from_base(16, "20000000000005"sv).to_double(Crypto::UnsignedBigInteger::RoundingMode::IEEERoundAndTiesToEvenMantissa),
9007199254740996.0);
EXPECT_EQ(Crypto::UnsignedBigInteger::from_base(16, "20000000000006"sv).to_double(Crypto::UnsignedBigInteger::RoundingMode::IEEERoundAndTiesToEvenMantissa),
9007199254740998.0);
EXPECT_EQ(Crypto::UnsignedBigInteger::from_base(10, "98382635059784269824"sv).to_double(Crypto::UnsignedBigInteger::RoundingMode::IEEERoundAndTiesToEvenMantissa),
bit_cast<double>(0x4415555555555555ULL));
#undef EXPECT_TO_EQUAL_DOUBLE
}

View file

@ -65,9 +65,9 @@ u64 SignedBigInteger::to_u64() const
return ~(unsigned_value - 1); // equivalent to `-unsigned_value`, but doesn't trigger UBSAN
}
double SignedBigInteger::to_double() const
double SignedBigInteger::to_double(UnsignedBigInteger::RoundingMode rounding_mode) const
{
double unsigned_value = m_unsigned_data.to_double();
double unsigned_value = m_unsigned_data.to_double(rounding_mode);
if (!m_sign)
return unsigned_value;

View file

@ -1,5 +1,6 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* Copyright (c) 2022, David Tuin <davidot@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -67,7 +68,7 @@ public:
[[nodiscard]] String to_base(u16 N) const;
[[nodiscard]] u64 to_u64() const;
[[nodiscard]] double to_double() const;
[[nodiscard]] double to_double(UnsignedBigInteger::RoundingMode rounding_mode = UnsignedBigInteger::RoundingMode::IEEERoundAndTiesToEvenMantissa) const;
[[nodiscard]] UnsignedBigInteger const& unsigned_value() const { return m_unsigned_data; }
[[nodiscard]] Vector<u32, STARTING_WORD_SIZE> const words() const { return m_unsigned_data.words(); }

View file

@ -1,5 +1,6 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* Copyright (c) 2022, David Tuin <davidot@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -113,28 +114,18 @@ u64 UnsignedBigInteger::to_u64() const
return value;
}
double UnsignedBigInteger::to_double() const
double UnsignedBigInteger::to_double(UnsignedBigInteger::RoundingMode rounding_mode) const
{
// NOTE: This function rounds toward zero!
// FIXME: Which is not exactly what we should do for JS when converting to number:
// See: https://tc39.es/ecma262/#sec-number-constructor-number-value
// Which has step 1.b If Type(prim) is BigInt, let n be 𝔽((prim)).
// Which then references: https://tc39.es/ecma262/#sec-ecmascript-language-types-number-type
// Which is equivalent to (This procedure corresponds exactly to the behaviour of the IEEE 754-2019 roundTiesToEven mode.)
VERIFY(!is_invalid());
auto highest_bit = one_based_index_of_highest_set_bit();
if (highest_bit == 0)
return 0;
--highest_bit;
// Simple case if less than 2^53 since those number are all exactly representable in doubles
if (highest_bit < 53)
if (highest_bit < mantissa_size + 1)
return static_cast<double>(to_u64());
constexpr u64 mantissa_size = 52;
constexpr u64 exponent_size = 11;
constexpr auto exponent_bias = (1 << (exponent_size - 1)) - 1;
// If it uses too many bit to represent in a double return infinity
if (highest_bit > exponent_bias)
return __builtin_huge_val();
@ -148,7 +139,7 @@ double UnsignedBigInteger::to_double() const
constexpr auto bits_in_u64 = 64;
static_assert(bits_in_u64 > mantissa_size + 1);
auto bits_to_read = min(mantissa_size + 1, highest_bit);
auto bits_to_read = min(mantissa_size, highest_bit);
auto last_word_index = trimmed_length();
VERIFY(last_word_index > 0);
@ -157,12 +148,19 @@ double UnsignedBigInteger::to_double() const
auto highest_bit_index_in_top_word = highest_bit % BITS_IN_WORD;
// Shift initial word until highest bit is just beyond top of u64.
u64 mantissa = static_cast<u64>(m_words[last_word_index - 1]) << (bits_in_u64 - highest_bit_index_in_top_word);
u64 mantissa = m_words[last_word_index - 1];
if (highest_bit_index_in_top_word != 0)
mantissa <<= (bits_in_u64 - highest_bit_index_in_top_word);
else
mantissa = 0;
auto bits_written = highest_bit_index_in_top_word;
--last_word_index;
Optional<Word> dropped_bits_for_rounding;
u8 bits_dropped_from_final_word = 0;
if (bits_written < bits_to_read && last_word_index > 0) {
// Second word can always just cleanly be shifted upto the final bit of the first word
// since the first has at most BIT_IN_WORD - 1, 31
@ -172,23 +170,101 @@ double UnsignedBigInteger::to_double() const
bits_written += BITS_IN_WORD;
--last_word_index;
if (bits_written < bits_to_read && last_word_index > 0) {
if (bits_written > bits_to_read) {
bits_dropped_from_final_word = bits_written - bits_to_read;
dropped_bits_for_rounding = m_words[last_word_index] & ((1 << bits_dropped_from_final_word) - 1);
} else if (bits_written < bits_to_read && last_word_index > 0) {
// The final word has to be shifted down first to discard any excess bits.
u64 final_word = m_words[last_word_index - 1];
--last_word_index;
auto bits_to_write = bits_to_read - bits_written;
final_word >>= (BITS_IN_WORD - bits_to_write);
bits_dropped_from_final_word = BITS_IN_WORD - bits_to_write;
dropped_bits_for_rounding = final_word & ((1 << bits_dropped_from_final_word) - 1u);
final_word >>= bits_dropped_from_final_word;
// Then move the bits right up to the lowest bits of the second word
VERIFY((mantissa & (final_word << (bits_in_u64 - bits_written - bits_to_write))) == 0);
mantissa |= final_word << (bits_in_u64 - bits_written - BITS_IN_WORD);
mantissa |= final_word << (bits_in_u64 - bits_written - bits_to_write);
}
}
// Now the mantissa should be complete so shift it down
mantissa >>= bits_in_u64 - mantissa_size;
if (rounding_mode == RoundingMode::IEEERoundAndTiesToEvenMantissa) {
bool round_up = false;
if (bits_dropped_from_final_word == 0) {
if (last_word_index > 0) {
Word next_word = m_words[last_word_index - 1];
last_word_index--;
if ((next_word & 0x80000000) != 0) {
// next top bit set check for any other bits
if ((next_word ^ 0x80000000) != 0) {
round_up = true;
} else {
while (last_word_index > 0) {
if (m_words[last_word_index - 1] != 0) {
round_up = true;
break;
}
}
// All other bits are 0 which is a tie thus round to even exponent
// Since we are halfway, if exponent ends with 1 we round up, if 0 we round down
round_up = (mantissa & 1) != 0;
}
} else {
round_up = false;
}
} else {
// If there are no words left the rest is implicitly 0 so just round down
round_up = false;
}
} else {
VERIFY(dropped_bits_for_rounding.has_value());
VERIFY(bits_dropped_from_final_word >= 1);
// In this case the top bit comes form the dropped bits
auto top_bit_extractor = 1u << (bits_dropped_from_final_word - 1u);
if ((*dropped_bits_for_rounding & top_bit_extractor) != 0) {
// Possible tie again, if any other bit is set we round up
if ((*dropped_bits_for_rounding ^ top_bit_extractor) != 0) {
round_up = true;
} else {
while (last_word_index > 0) {
if (m_words[last_word_index - 1] != 0) {
round_up = true;
break;
}
}
round_up = (mantissa & 1) != 0;
}
} else {
round_up = false;
}
}
if (round_up) {
++mantissa;
if ((mantissa & (1ull << mantissa_size)) != 0) {
// we overflowed the mantissa
mantissa = 0;
highest_bit++;
// In which case it is possible we have to round to infinity
if (highest_bit > exponent_bias)
return __builtin_huge_val();
}
}
} else {
VERIFY(rounding_mode == RoundingMode::RoundTowardZero);
}
union FloatExtractor {
struct {
unsigned long long mantissa : mantissa_size;

View file

@ -1,6 +1,7 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* Copyright (c) 2022, the SerenityOS developers.
* Copyright (c) 2022, David Tuin <davidot@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -58,7 +59,15 @@ public:
[[nodiscard]] String to_base(u16 N) const;
[[nodiscard]] u64 to_u64() const;
[[nodiscard]] double to_double() const;
enum class RoundingMode {
IEEERoundAndTiesToEvenMantissa,
RoundTowardZero,
// “the Number value for x”, https://tc39.es/ecma262/#number-value-for
ECMAScriptNumberValueFor = IEEERoundAndTiesToEvenMantissa,
};
[[nodiscard]] double to_double(RoundingMode rounding_mode = RoundingMode::IEEERoundAndTiesToEvenMantissa) const;
[[nodiscard]] Vector<Word, STARTING_WORD_SIZE> const& words() const { return m_words; }