From 77d71a5ffd784dd076c00454b59bcf674d747b69 Mon Sep 17 00:00:00 2001 From: davidot Date: Thu, 25 Aug 2022 17:56:34 +0200 Subject: [PATCH] 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. --- Tests/LibCrypto/TestBigInteger.cpp | 38 +++++- .../LibCrypto/BigInt/SignedBigInteger.cpp | 4 +- .../LibCrypto/BigInt/SignedBigInteger.h | 3 +- .../LibCrypto/BigInt/UnsignedBigInteger.cpp | 112 +++++++++++++++--- .../LibCrypto/BigInt/UnsignedBigInteger.h | 11 +- 5 files changed, 144 insertions(+), 24 deletions(-) diff --git a/Tests/LibCrypto/TestBigInteger.cpp b/Tests/LibCrypto/TestBigInteger.cpp index 117f5e1891..3a94712739 100644 --- a/Tests/LibCrypto/TestBigInteger.cpp +++ b/Tests/LibCrypto/TestBigInteger.cpp @@ -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(0x4415555555555555ULL)); #undef EXPECT_TO_EQUAL_DOUBLE } diff --git a/Userland/Libraries/LibCrypto/BigInt/SignedBigInteger.cpp b/Userland/Libraries/LibCrypto/BigInt/SignedBigInteger.cpp index c4a49190e6..5ea14dbbc3 100644 --- a/Userland/Libraries/LibCrypto/BigInt/SignedBigInteger.cpp +++ b/Userland/Libraries/LibCrypto/BigInt/SignedBigInteger.cpp @@ -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; diff --git a/Userland/Libraries/LibCrypto/BigInt/SignedBigInteger.h b/Userland/Libraries/LibCrypto/BigInt/SignedBigInteger.h index 7b1a6b25f0..4a881df0a4 100644 --- a/Userland/Libraries/LibCrypto/BigInt/SignedBigInteger.h +++ b/Userland/Libraries/LibCrypto/BigInt/SignedBigInteger.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2020, the SerenityOS developers. + * Copyright (c) 2022, David Tuin * * 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 const words() const { return m_unsigned_data.words(); } diff --git a/Userland/Libraries/LibCrypto/BigInt/UnsignedBigInteger.cpp b/Userland/Libraries/LibCrypto/BigInt/UnsignedBigInteger.cpp index d9b8e7566d..e7a82fa7ee 100644 --- a/Userland/Libraries/LibCrypto/BigInt/UnsignedBigInteger.cpp +++ b/Userland/Libraries/LibCrypto/BigInt/UnsignedBigInteger.cpp @@ -1,5 +1,6 @@ /* * Copyright (c) 2020, Itamar S. + * Copyright (c) 2022, David Tuin * * 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(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(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 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; diff --git a/Userland/Libraries/LibCrypto/BigInt/UnsignedBigInteger.h b/Userland/Libraries/LibCrypto/BigInt/UnsignedBigInteger.h index 7a24ba6517..fd413308db 100644 --- a/Userland/Libraries/LibCrypto/BigInt/UnsignedBigInteger.h +++ b/Userland/Libraries/LibCrypto/BigInt/UnsignedBigInteger.h @@ -1,6 +1,7 @@ /* * Copyright (c) 2020, Itamar S. * Copyright (c) 2022, the SerenityOS developers. + * Copyright (c) 2022, David Tuin * * 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 const& words() const { return m_words; }