From 0026e9a4c803101d9cb2f28d0b76fc224a3521e3 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Tue, 19 Jul 2022 12:07:00 -0400 Subject: [PATCH] LibJS: Implement a basic Intl mathematical value The Intl mathematical value is much like ECMA-262's mathematical value in that it is meant to represent an arbitrarily precise number. The Intl MV further allows positive/negative infinity, negative zero, and NaN. This implementation is *not* arbitrarily precise. Rather, it is a replacement for the use of Value within Intl.NumberFormat. The exact syntax of the Intl MV is still being worked on, but abstracting this away into its own class will allow hooking in the finalized Intl MV more easily, and makes implementing Intl.NumberFormat.formatRange easier. Note the methods added here are essentially the same as the static helpers in Intl/NumberFormat.cpp. --- Userland/Libraries/LibJS/CMakeLists.txt | 1 + Userland/Libraries/LibJS/Forward.h | 2 + .../LibJS/Runtime/Intl/MathematicalValue.cpp | 346 ++++++++++++++++++ .../LibJS/Runtime/Intl/MathematicalValue.h | 105 ++++++ 4 files changed, 454 insertions(+) create mode 100644 Userland/Libraries/LibJS/Runtime/Intl/MathematicalValue.cpp create mode 100644 Userland/Libraries/LibJS/Runtime/Intl/MathematicalValue.h diff --git a/Userland/Libraries/LibJS/CMakeLists.txt b/Userland/Libraries/LibJS/CMakeLists.txt index 24f077603d6..bb5d1145350 100644 --- a/Userland/Libraries/LibJS/CMakeLists.txt +++ b/Userland/Libraries/LibJS/CMakeLists.txt @@ -114,6 +114,7 @@ set(SOURCES Runtime/Intl/Locale.cpp Runtime/Intl/LocaleConstructor.cpp Runtime/Intl/LocalePrototype.cpp + Runtime/Intl/MathematicalValue.cpp Runtime/Intl/NumberFormat.cpp Runtime/Intl/NumberFormatConstructor.cpp Runtime/Intl/NumberFormatFunction.cpp diff --git a/Userland/Libraries/LibJS/Forward.h b/Userland/Libraries/LibJS/Forward.h index ac18abc83fc..ddafc417761 100644 --- a/Userland/Libraries/LibJS/Forward.h +++ b/Userland/Libraries/LibJS/Forward.h @@ -225,6 +225,8 @@ namespace Intl { JS_ENUMERATE_INTL_OBJECTS #undef __JS_ENUMERATE +class MathematicalValue; + // Not included in JS_ENUMERATE_INTL_OBJECTS due to missing distinct constructor class Segments; class SegmentsPrototype; diff --git a/Userland/Libraries/LibJS/Runtime/Intl/MathematicalValue.cpp b/Userland/Libraries/LibJS/Runtime/Intl/MathematicalValue.cpp new file mode 100644 index 00000000000..5d819a5f742 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/Intl/MathematicalValue.cpp @@ -0,0 +1,346 @@ +/* + * Copyright (c) 2022, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +namespace JS::Intl { + +bool MathematicalValue::is_number() const +{ + return m_value.has(); +} + +double MathematicalValue::as_number() const +{ + VERIFY(is_number()); + return m_value.get(); +} + +bool MathematicalValue::is_bigint() const +{ + return m_value.has(); +} + +Crypto::SignedBigInteger const& MathematicalValue::as_bigint() const +{ + VERIFY(is_bigint()); + return m_value.get(); +} + +bool MathematicalValue::is_mathematical_value() const +{ + return is_number() || is_bigint(); +} + +bool MathematicalValue::is_positive_infinity() const +{ + if (is_mathematical_value()) + return false; + return m_value.get() == Symbol::PositiveInfinity; +} + +bool MathematicalValue::is_negative_infinity() const +{ + if (is_mathematical_value()) + return false; + return m_value.get() == Symbol::NegativeInfinity; +} + +bool MathematicalValue::is_negative_zero() const +{ + if (is_mathematical_value()) + return false; + return m_value.get() == Symbol::NegativeZero; +} + +bool MathematicalValue::is_nan() const +{ + if (is_mathematical_value()) + return false; + return m_value.get() == Symbol::NotANumber; +} + +void MathematicalValue::negate() +{ + m_value.visit( + [](double& value) { + VERIFY(value != 0.0); + value *= -1.0; + }, + [](Crypto::SignedBigInteger& value) { value.negate(); }, + [](auto) { VERIFY_NOT_REACHED(); }); +} + +MathematicalValue MathematicalValue::plus(Checked addition) const +{ + return m_value.visit( + [&](double value) { + return MathematicalValue { value + addition.value() }; + }, + [&](Crypto::SignedBigInteger const& value) { + return MathematicalValue { value.plus(Crypto::SignedBigInteger::create_from(addition.value())) }; + }, + [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); +} + +MathematicalValue MathematicalValue::plus(MathematicalValue const& addition) const +{ + return m_value.visit( + [&](double value) { + return MathematicalValue { value + addition.as_number() }; + }, + [&](Crypto::SignedBigInteger const& value) { + return MathematicalValue { value.plus(addition.as_bigint()) }; + }, + [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); +} + +MathematicalValue MathematicalValue::minus(Checked subtraction) const +{ + return m_value.visit( + [&](double value) { + return MathematicalValue { value - subtraction.value() }; + }, + [&](Crypto::SignedBigInteger const& value) { + return MathematicalValue { value.minus(Crypto::SignedBigInteger::create_from(subtraction.value())) }; + }, + [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); +} + +MathematicalValue MathematicalValue::minus(MathematicalValue const& subtraction) const +{ + return m_value.visit( + [&](double value) { + return MathematicalValue { value - subtraction.as_number() }; + }, + [&](Crypto::SignedBigInteger const& value) { + return MathematicalValue { value.minus(subtraction.as_bigint()) }; + }, + [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); +} + +MathematicalValue MathematicalValue::multiplied_by(Checked multiplier) const +{ + return m_value.visit( + [&](double value) { + return MathematicalValue { value * multiplier.value() }; + }, + [&](Crypto::SignedBigInteger const& value) { + return MathematicalValue { value.multiplied_by(Crypto::SignedBigInteger::create_from(multiplier.value())) }; + }, + [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); +} + +MathematicalValue MathematicalValue::multiplied_by(MathematicalValue const& multiplier) const +{ + return m_value.visit( + [&](double value) { + return MathematicalValue { value * multiplier.as_number() }; + }, + [&](Crypto::SignedBigInteger const& value) { + return MathematicalValue { value.multiplied_by(multiplier.as_bigint()) }; + }, + [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); +} + +MathematicalValue MathematicalValue::divided_by(Checked divisor) const +{ + return m_value.visit( + [&](double value) { + return MathematicalValue { value / divisor.value() }; + }, + [&](Crypto::SignedBigInteger const& value) { + return MathematicalValue { value.divided_by(Crypto::SignedBigInteger::create_from(divisor.value())).quotient }; + }, + [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); +} + +MathematicalValue MathematicalValue::divided_by(MathematicalValue const& divisor) const +{ + return m_value.visit( + [&](double value) { + return MathematicalValue { value / divisor.as_number() }; + }, + [&](Crypto::SignedBigInteger const& value) { + return MathematicalValue { value.divided_by(divisor.as_bigint()).quotient }; + }, + [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); +} + +static Crypto::SignedBigInteger bigint_power(Checked exponent) +{ + VERIFY(exponent >= 0); + + static auto base = Crypto::SignedBigInteger::create_from(10); + auto result = Crypto::SignedBigInteger::create_from(1); + + for (i32 i = 0; i < exponent; ++i) + result = result.multiplied_by(base); + + return result; +} + +MathematicalValue MathematicalValue::multiplied_by_power(Checked exponent) const +{ + return m_value.visit( + [&](double value) { + return MathematicalValue { value * pow(10, exponent.value()) }; + }, + [&](Crypto::SignedBigInteger const& value) { + if (exponent < 0) + return MathematicalValue { value.divided_by(bigint_power(-exponent.value())).quotient }; + return MathematicalValue { value.multiplied_by(bigint_power(exponent)) }; + }, + [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); +} + +MathematicalValue MathematicalValue::divided_by_power(Checked exponent) const +{ + return m_value.visit( + [&](double value) { + if (exponent < 0) + return MathematicalValue { value * pow(10, -exponent.value()) }; + return MathematicalValue { value / pow(10, exponent.value()) }; + }, + [&](Crypto::SignedBigInteger const& value) { + if (exponent < 0) + return MathematicalValue { value.multiplied_by(bigint_power(-exponent.value())) }; + return MathematicalValue { value.divided_by(bigint_power(exponent)).quotient }; + }, + [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); +} + +bool MathematicalValue::modulo_is_zero(Checked mod) const +{ + return m_value.visit( + [&](double value) { + auto result = MathematicalValue { modulo(value, mod.value()) }; + return result.is_equal_to(MathematicalValue { 0.0 }); + }, + [&](Crypto::SignedBigInteger const& value) { + return modulo(value, Crypto::SignedBigInteger::create_from(mod.value())).is_zero(); + }, + [](auto) -> bool { VERIFY_NOT_REACHED(); }); +} + +int MathematicalValue::logarithmic_floor() const +{ + return m_value.visit( + [](double value) { + return static_cast(floor(log10(value))); + }, + [](Crypto::SignedBigInteger const& value) { + // FIXME: Can we do this without string conversion? + return static_cast(value.to_base(10).length() - 1); + }, + [](auto) -> int { VERIFY_NOT_REACHED(); }); +} + +bool MathematicalValue::is_equal_to(MathematicalValue const& other) const +{ + return m_value.visit( + [&](double value) { + static constexpr double epsilon = 5e-14; + return fabs(value - other.as_number()) < epsilon; + }, + [&](Crypto::SignedBigInteger const& value) { + return value == other.as_bigint(); + }, + [](auto) -> bool { VERIFY_NOT_REACHED(); }); +} + +bool MathematicalValue::is_less_than(MathematicalValue const& other) const +{ + return m_value.visit( + [&](double value) { + if (is_equal_to(other)) + return false; + return value < other.as_number(); + }, + [&](Crypto::SignedBigInteger const& value) { + return value < other.as_bigint(); + }, + [](auto) -> bool { VERIFY_NOT_REACHED(); }); +} + +bool MathematicalValue::is_negative() const +{ + return m_value.visit( + [](double value) { return value < 0.0; }, + [](Crypto::SignedBigInteger const& value) { return value.is_negative(); }, + [](Symbol symbol) { return symbol == Symbol::NegativeInfinity; }); +} + +bool MathematicalValue::is_positive() const +{ + return m_value.visit( + [](double value) { return value > 0.0; }, + [](Crypto::SignedBigInteger const& value) { return !value.is_zero() && !value.is_negative(); }, + [](Symbol symbol) { return symbol == Symbol::PositiveInfinity; }); +} + +bool MathematicalValue::is_zero() const +{ + return m_value.visit( + [&](double value) { return value == 0.0; }, + [](Crypto::SignedBigInteger const& value) { return value.is_zero(); }, + [](auto) { return false; }); +} + +String MathematicalValue::to_string() const +{ + return m_value.visit( + [](double value) { return Value(value).to_string_without_side_effects(); }, + [](Crypto::SignedBigInteger const& value) { return value.to_base(10); }, + [](auto) -> String { VERIFY_NOT_REACHED(); }); +} + +Value MathematicalValue::to_value(GlobalObject& global_object) const +{ + auto& vm = global_object.vm(); + + return m_value.visit( + [](double value) { + return Value(value); + }, + [&](Crypto::SignedBigInteger const& value) { + return Value(js_bigint(vm, value)); + }, + [](auto symbol) { + switch (symbol) { + case Symbol::PositiveInfinity: + return js_infinity(); + case Symbol::NegativeInfinity: + return js_negative_infinity(); + case Symbol::NegativeZero: + return Value(-0.0); + case Symbol::NotANumber: + return js_nan(); + } + + VERIFY_NOT_REACHED(); + }); +} + +MathematicalValue::ValueType MathematicalValue::value_from_number(double number) +{ + Value value(number); + + if (value.is_positive_infinity()) + return Symbol::PositiveInfinity; + if (value.is_negative_infinity()) + return Symbol::NegativeInfinity; + if (value.is_negative_zero()) + return Symbol::NegativeZero; + if (value.is_nan()) + return Symbol::NotANumber; + return number; +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/Intl/MathematicalValue.h b/Userland/Libraries/LibJS/Runtime/Intl/MathematicalValue.h new file mode 100644 index 00000000000..92fae9e0d2d --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/Intl/MathematicalValue.h @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2022, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include + +namespace JS::Intl { + +// https://tc39.es/proposal-intl-numberformat-v3/out/numberformat/proposed.html#intl-mathematical-value +class MathematicalValue { +public: + enum class Symbol { + PositiveInfinity, + NegativeInfinity, + NegativeZero, + NotANumber, + }; + + MathematicalValue() = default; + + explicit MathematicalValue(double value) + : m_value(value_from_number(value)) + { + } + + explicit MathematicalValue(Crypto::SignedBigInteger value) + : m_value(move(value)) + { + } + + explicit MathematicalValue(Symbol symbol) + : m_value(symbol) + { + } + + MathematicalValue(Value value) + : m_value(value.is_number() + ? value_from_number(value.as_double()) + : ValueType(value.as_bigint().big_integer())) + { + } + + bool is_number() const; + double as_number() const; + + bool is_bigint() const; + Crypto::SignedBigInteger const& as_bigint() const; + + bool is_mathematical_value() const; + bool is_positive_infinity() const; + bool is_negative_infinity() const; + bool is_negative_zero() const; + bool is_nan() const; + + void negate(); + + MathematicalValue plus(Checked addition) const; + MathematicalValue plus(MathematicalValue const& addition) const; + + MathematicalValue minus(Checked subtraction) const; + MathematicalValue minus(MathematicalValue const& subtraction) const; + + MathematicalValue multiplied_by(Checked multiplier) const; + MathematicalValue multiplied_by(MathematicalValue const& multiplier) const; + + MathematicalValue divided_by(Checked divisor) const; + MathematicalValue divided_by(MathematicalValue const& divisor) const; + + MathematicalValue multiplied_by_power(Checked exponent) const; + MathematicalValue divided_by_power(Checked exponent) const; + + bool modulo_is_zero(Checked mod) const; + + int logarithmic_floor() const; + + bool is_equal_to(MathematicalValue const&) const; + bool is_less_than(MathematicalValue const&) const; + + bool is_negative() const; + bool is_positive() const; + bool is_zero() const; + + String to_string() const; + Value to_value(GlobalObject&) const; + +private: + using ValueType = Variant; + + static ValueType value_from_number(double number); + + // NOTE: The specific alignment is to avoid an UBSAN error with Clang i686, due to Clang + // disagreeing with UBSAN on the alignment of doubles. See: + // https://github.com/llvm/llvm-project/issues/54845 + // https://github.com/SerenityOS/serenity/issues/13614 + alignas(8) ValueType m_value { 0.0 }; +}; + +}