From b895ca232c184bb0fe270b1ef1f8f79b6ae6dd9b Mon Sep 17 00:00:00 2001 From: Manuel Zahariev Date: Thu, 9 Jan 2025 06:31:51 -0800 Subject: [PATCH] LibCrypto: Improve precision of Crypto::BigFraction::to_double() Before: - FIXME: very naive implementation - was preventing passing some Temporal tests - https://github.com/tc39/test262 - https://github.com/LadybirdBrowser/libjs-test262 --- .../LibCrypto/BigFraction/BigFraction.cpp | 62 ++++++++++++++++++- Tests/LibCrypto/TestBigFraction.cpp | 40 ++++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/Libraries/LibCrypto/BigFraction/BigFraction.cpp b/Libraries/LibCrypto/BigFraction/BigFraction.cpp index d51c64bf7584b..d882308197736 100644 --- a/Libraries/LibCrypto/BigFraction/BigFraction.cpp +++ b/Libraries/LibCrypto/BigFraction/BigFraction.cpp @@ -1,14 +1,18 @@ /* * Copyright (c) 2022, Lucas Chollet + * Copyright (c) 2025, Manuel Zahariev * * SPDX-License-Identifier: BSD-2-Clause */ #include "BigFraction.h" +#include "LibCrypto/BigInt/UnsignedBigInteger.h" #include #include #include #include +#include +#include namespace Crypto { @@ -134,10 +138,64 @@ BigFraction::BigFraction(double d) m_numerator.set_to(negative ? (m_numerator.negated_value()) : m_numerator); } +/* + * Complexity O(N), where N=total number of words in numerator and denominator: + * - shifts: O(N) + * - division: constant (fixed bound on size of shifted numerator and denominator + * - conversion to double: constant (64-bit quotient) + */ double BigFraction::to_double() const { - // FIXME: very naive implementation - return m_numerator.to_double() / m_denominator.to_double(); + // 1. Shift the numerator and denominator so that: + // - the denominator is at most 64 bits + // - the numerator is exactly 64 bits larger than the denominator + size_t const bit_precision = 64; // divide the fraction to this precision + bool const sign = m_numerator.is_negative(); + + int denominator_right_shift = 0; + + size_t bit_size_numerator = m_numerator.unsigned_value().one_based_index_of_highest_set_bit(); + size_t bit_size_denominator = m_denominator.one_based_index_of_highest_set_bit(); + + if (bit_size_denominator > bit_precision) { // reduce precision of a large denominator + denominator_right_shift = bit_size_denominator - bit_precision; + bit_size_denominator = bit_precision; + } + + int numerator_right_shift = bit_size_numerator - bit_size_denominator - bit_precision; + + UnsignedBigInteger shifted_denominator = m_denominator.shift_right(denominator_right_shift); + UnsignedBigInteger shifted_numerator; + if (numerator_right_shift < 0) + shifted_numerator.set_to(m_numerator.unsigned_value().shift_left(-numerator_right_shift)); // increase numerator to increase precision + else + shifted_numerator.set_to(m_numerator.unsigned_value().shift_right(numerator_right_shift)); // decrease precision of numerator + + // 2. Divide the shifted numerator to the shifted denominator. The result will have 64-bit precision. + // Then, convert the quotient to double. + double result = SignedBigInteger { shifted_numerator.divided_by(shifted_denominator).quotient, sign }.to_double(); + + // 3. Convert the result to a double, including the denominator/numerator shifts in the exponent. + using Extractor = FloatExtractor; + + Extractor double_extractor; + double_extractor.d = result; + + int exponent = double_extractor.exponent + numerator_right_shift - denominator_right_shift; + + if ((exponent < 0) && sign) // undeflow + return -0.0; + if (exponent < 0) + return +0.0; + if ((exponent > int(Extractor::exponent_max)) && sign) // overflow + return -std::numeric_limits::infinity(); + if (exponent > int(Extractor::exponent_max)) + return std::numeric_limits::infinity(); + + double_extractor.sign = sign; + double_extractor.exponent += (numerator_right_shift - denominator_right_shift); + + return double_extractor.d; } bool BigFraction::is_zero() const diff --git a/Tests/LibCrypto/TestBigFraction.cpp b/Tests/LibCrypto/TestBigFraction.cpp index b0ce75fabe261..99ddab192b479 100644 --- a/Tests/LibCrypto/TestBigFraction.cpp +++ b/Tests/LibCrypto/TestBigFraction.cpp @@ -1,12 +1,28 @@ /* * Copyright (c) 2024, Tim Ledbetter + * Copyright (c) 2025, Manuel Zahariev * * SPDX-License-Identifier: BSD-2-Clause */ #include +#include +#include +#include #include +static Crypto::UnsignedBigInteger bigint_fibonacci(size_t n) +{ + Crypto::UnsignedBigInteger num1(0); + Crypto::UnsignedBigInteger num2(1); + for (size_t i = 0; i < n; ++i) { + Crypto::UnsignedBigInteger t = num1.plus(num2); + num2 = num1; + num1 = t; + } + return num1; +} + TEST_CASE(roundtrip_from_string) { Array valid_number_strings { @@ -26,3 +42,27 @@ TEST_CASE(roundtrip_from_string) EXPECT_EQ(result.to_string(precision), valid_number_string); } } + +TEST_CASE(big_fraction_to_double) +{ + // Golden ratio: + // - limit (inf) ratio of two consecutive fibonacci numbers + // - also ( 1 + sqrt( 5 ))/2 + Crypto::BigFraction phi(Crypto::SignedBigInteger { bigint_fibonacci(500) }, bigint_fibonacci(499)); + // Power 64 of golden ratio: + // - limit ratio of two 64-separated fibonacci numbers + // - also (23725150497407 + 10610209857723 * sqrt( 5 ))/2 + Crypto::BigFraction phi_64(Crypto::SignedBigInteger { bigint_fibonacci(564) }, bigint_fibonacci(500)); + + EXPECT_EQ(phi.to_double(), 1.618033988749895); // 1.6180339887498948482045868343656381177203091798057628621... (https://oeis.org/A001622) + EXPECT_EQ(phi_64.to_double(), 23725150497407); // 23725150497406.9999999999999578506361799772097881088769... (https://www.calculator.net/big-number-calculator.html) +} + +TEST_CASE(big_fraction_temporal_duration_precision_support) +{ + // https://github.com/tc39/test262/blob/main/test/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-1.js + // Express 4000h and 1ns in hours, as a double + Crypto::BigFraction temporal_duration_precision_test = Crypto::BigFraction { Crypto::SignedBigInteger { "14400000000000001"_bigint }, "3600000000000"_bigint }; + + EXPECT_EQ(temporal_duration_precision_test.to_double(), 4000.0000000000005); +}