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:
parent
8ba6e96d05
commit
77d71a5ffd
Notes:
sideshowbarker
2024-07-17 07:42:58 +09:00
Author: https://github.com/davidot Commit: https://github.com/SerenityOS/serenity/commit/77d71a5ffd Pull-request: https://github.com/SerenityOS/serenity/pull/15029 Reviewed-by: https://github.com/linusg ✅ Reviewed-by: https://github.com/trflynn89
5 changed files with 144 additions and 24 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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(); }
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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; }
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue