From d972016c236bfa846df3d3bbe263a6972eab8ac2 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Tue, 25 Jul 2023 01:41:42 -0500 Subject: [PATCH] Issue #2026 - Part 2a - Support BigInt in NumberFormat and toLocaleString. https://bugzilla.mozilla.org/show_bug.cgi?id=1543677 --- js/public/Value.h | 5 ++ js/src/builtin/BigInt.cpp | 28 +--------- js/src/builtin/BigInt.h | 2 - js/src/builtin/BigInt.js | 34 ++++++++++++ js/src/builtin/intl/NumberFormat.cpp | 82 +++++++++++++++++++--------- js/src/builtin/intl/NumberFormat.h | 2 +- js/src/builtin/intl/NumberFormat.js | 6 +- js/src/builtin/intl/PluralRules.cpp | 4 +- js/src/jsnum.cpp | 3 +- js/src/jsnum.h | 5 +- js/src/moz.build | 1 + js/src/vm/SelfHosting.cpp | 17 +++++- 12 files changed, 120 insertions(+), 69 deletions(-) create mode 100644 js/src/builtin/BigInt.js diff --git a/js/public/Value.h b/js/public/Value.h index 30f4670049..a6ceaad669 100644 --- a/js/public/Value.h +++ b/js/public/Value.h @@ -553,6 +553,10 @@ class MOZ_NON_PARAM alignas(8) Value return isObject() || isNull(); } + bool isNumeric() const { + return isNumber() || isBigInt(); + } + bool isGCThing() const { #if defined(JS_NUNBOX32) /* gcc sometimes generates signed < without explicit casts. */ @@ -1410,6 +1414,7 @@ class WrappedPtrOperations bool isNullOrUndefined() const { return value().isNullOrUndefined(); } bool isObjectOrNull() const { return value().isObjectOrNull(); } + bool isNumeric() const { return value().isNumeric(); } bool toBoolean() const { return value().toBoolean(); } double toNumber() const { return value().toNumber(); } diff --git a/js/src/builtin/BigInt.cpp b/js/src/builtin/BigInt.cpp index 6c78970d74..8a630534a8 100644 --- a/js/src/builtin/BigInt.cpp +++ b/js/src/builtin/BigInt.cpp @@ -139,32 +139,6 @@ BigIntObject::toString(JSContext* cx, unsigned argc, Value* vp) return CallNonGenericMethod(cx, args); } -// BigInt proposal section 5.3.2. "This function is -// implementation-dependent, and it is permissible, but not encouraged, -// for it to return the same thing as toString." -bool -BigIntObject::toLocaleString_impl(JSContext* cx, const CallArgs& args) -{ - HandleValue thisv = args.thisv(); - MOZ_ASSERT(IsBigInt(thisv)); - RootedBigInt bi(cx, thisv.isBigInt() - ? thisv.toBigInt() - : thisv.toObject().as().unbox()); - - RootedString str(cx, BigInt::toString(cx, bi, 10)); - if (!str) - return false; - args.rval().setString(str); - return true; -} - -bool -BigIntObject::toLocaleString(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - return CallNonGenericMethod(cx, args); -} - // BigInt proposal section 5.2.1. BigInt.asUintN ( bits, bigint ) bool BigIntObject::asUintN(JSContext* cx, unsigned argc, Value* vp) @@ -247,7 +221,7 @@ const JSPropertySpec BigIntObject::properties[] = { const JSFunctionSpec BigIntObject::methods[] = { JS_FN("valueOf", valueOf, 0, 0), JS_FN("toString", toString, 0, 0), - JS_FN("toLocaleString", toLocaleString, 0, 0), + JS_SELF_HOSTED_FN("toLocaleString", "BigInt_toLocaleString", 0, 0), JS_FS_END }; diff --git a/js/src/builtin/BigInt.h b/js/src/builtin/BigInt.h index 6971549fc3..be447b2285 100644 --- a/js/src/builtin/BigInt.h +++ b/js/src/builtin/BigInt.h @@ -31,8 +31,6 @@ class BigIntObject : public NativeObject static bool valueOf(JSContext* cx, unsigned argc, JS::Value* vp); static bool toString_impl(JSContext* cx, const CallArgs& args); static bool toString(JSContext* cx, unsigned argc, JS::Value* vp); - static bool toLocaleString_impl(JSContext* cx, const CallArgs& args); - static bool toLocaleString(JSContext* cx, unsigned argc, JS::Value* vp); static bool asUintN(JSContext* cx, unsigned argc, JS::Value* vp); static bool asIntN(JSContext* cx, unsigned argc, JS::Value* vp); diff --git a/js/src/builtin/BigInt.js b/js/src/builtin/BigInt.js new file mode 100644 index 0000000000..3ed3da5933 --- /dev/null +++ b/js/src/builtin/BigInt.js @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Format this BigInt object into a string, using the locale and formatting + * options provided. + * + * Spec PR: https://github.com/tc39/ecma402/pull/236 + */ +function BigInt_toLocaleString() { + // Step 1. Note that valueOf enforces "thisBigIntValue" restrictions. + var x = callFunction(std_BigInt_valueOf, this); + + var locales = arguments.length > 0 ? arguments[0] : undefined; + var options = arguments.length > 1 ? arguments[1] : undefined; + + // Step 2. + var numberFormat; + if (locales === undefined && options === undefined) { + // This cache only optimizes when no explicit locales and options + // arguments were supplied. + if (!IsRuntimeDefaultLocale(numberFormatCache.runtimeDefaultLocale)) { + numberFormatCache.numberFormat = intl_NumberFormat(locales, options); + numberFormatCache.runtimeDefaultLocale = RuntimeDefaultLocale(); + } + numberFormat = numberFormatCache.numberFormat; + } else { + numberFormat = intl_NumberFormat(locales, options); + } + + // Step 3. + return intl_FormatNumber(numberFormat, x, /* formatToParts = */ false); +} diff --git a/js/src/builtin/intl/NumberFormat.cpp b/js/src/builtin/intl/NumberFormat.cpp index c659733bac..453ccc05e9 100644 --- a/js/src/builtin/intl/NumberFormat.cpp +++ b/js/src/builtin/intl/NumberFormat.cpp @@ -33,6 +33,7 @@ using namespace js; using mozilla::AssertedCast; using mozilla::IsFinite; +using mozilla::IsNegative; using mozilla::IsNaN; using mozilla::IsNegativeZero; using js::intl::CallICU; @@ -401,24 +402,43 @@ NewUNumberFormat(JSContext* cx, Handle numberFormat) } static JSString* -PartitionNumberPattern(JSContext* cx, UNumberFormat* nf, double* x, +PartitionNumberPattern(JSContext* cx, UNumberFormat* nf, HandleValue x, UFieldPositionIterator* fpositer) { - // PartitionNumberPattern doesn't consider -0.0 to be negative. - if (IsNegativeZero(*x)) - *x = 0.0; + if (x.isNumber()) { + double num = x.toNumber(); - return CallICU(cx, [nf, x, fpositer](UChar* chars, int32_t size, UErrorCode* status) { - return unum_formatDoubleForFields(nf, *x, chars, size, fpositer, status); - }); + // PartitionNumberPattern doesn't consider -0.0 to be negative. + if (IsNegativeZero(num)) + num = 0.0; + + return CallICU(cx, [nf, num, fpositer](UChar* chars, int32_t size, UErrorCode* status) { + return unum_formatDoubleForFields(nf, num, chars, size, fpositer, status); + }); + } else if(x.isBigInt()) { + RootedBigInt bi(cx, x.toBigInt()); + JSLinearString* str = BigInt::toString(cx, bi, 10); + if (!str) { + return nullptr; + } + MOZ_ASSERT(str->hasLatin1Chars()); + + JS::AutoCheckCannotGC noGC(cx); + const char* latinchars = reinterpret_cast(str->latin1Chars(noGC)); + size_t length = str->length(); + return CallICU(cx, [nf, latinchars, length](UChar* chars, int32_t size, UErrorCode* status) { + return unum_formatDecimal(nf, latinchars, length, chars, size, nullptr, status); + }); + } + return nullptr; } bool -js::intl_FormatNumber(JSContext* cx, UNumberFormat* nf, double x, MutableHandleValue result) +js::FormatNumeric(JSContext* cx, UNumberFormat* nf, HandleValue x, MutableHandleValue result) { // Passing null for |fpositer| will just not compute partition information, // letting us common up all ICU number-formatting code. - JSString* str = PartitionNumberPattern(cx, nf, &x, nullptr); + JSString* str = PartitionNumberPattern(cx, nf, x, nullptr); if (!str) return false; @@ -429,7 +449,7 @@ js::intl_FormatNumber(JSContext* cx, UNumberFormat* nf, double x, MutableHandleV using FieldType = ImmutablePropertyNamePtr JSAtomState::*; static FieldType -GetFieldTypeForNumberField(UNumberFormatFields fieldName, double d) +GetFieldTypeForNumberField(UNumberFormatFields fieldName, HandleValue x) { // See intl/icu/source/i18n/unicode/unum.h for a detailed field list. This // list is deliberately exhaustive: cases might have to be added/removed if @@ -438,10 +458,15 @@ GetFieldTypeForNumberField(UNumberFormatFields fieldName, double d) // version-testing #ifdefs, should cross-version divergence occur. switch (fieldName) { case UNUM_INTEGER_FIELD: - if (IsNaN(d)) - return &JSAtomState::nan; - if (!IsFinite(d)) - return &JSAtomState::infinity; + if (x.isNumber()) { + double d = x.toNumber(); + if (IsNaN(d)) { + return &JSAtomState::nan; + } + if (!IsFinite(d)) { + return &JSAtomState::infinity; + } + } return &JSAtomState::integer; case UNUM_GROUPING_SEPARATOR_FIELD: @@ -454,13 +479,17 @@ GetFieldTypeForNumberField(UNumberFormatFields fieldName, double d) return &JSAtomState::fraction; case UNUM_SIGN_FIELD: { - MOZ_ASSERT(!IsNegativeZero(d), - "-0 should have been excluded by PartitionNumberPattern"); - - // Manual trawling through the ICU call graph appears to indicate that - // the basic formatting we request will never include a positive sign. - // But this analysis may be mistaken, so don't absolutely trust it. - return d < 0 ? &JSAtomState::minusSign : &JSAtomState::plusSign; + // Manual trawling through the ICU call graph appears to indicate that + // the basic formatting we request will never include a positive sign. + // But this analysis may be mistaken, so don't absolutely trust it. + MOZ_ASSERT(!x.isNumber() || !IsNaN(x.toNumber()), + "ICU appearing not to produce positive-sign among fields, " + "plus our coercing all NaNs to one with sign bit unset " + "(i.e. \"positive\"), means we shouldn't reach here with a " + "NaN value"); + bool isNegative = + x.isNumber() ? IsNegative(x.toNumber()) : x.toBigInt()->isNegative(); + return isNegative ? &JSAtomState::minusSign : &JSAtomState::plusSign; } case UNUM_PERCENT_FIELD: @@ -495,7 +524,7 @@ GetFieldTypeForNumberField(UNumberFormatFields fieldName, double d) } static bool -intl_FormatNumberToParts(JSContext* cx, UNumberFormat* nf, double x, MutableHandleValue result) +FormatNumericToParts(JSContext* cx, UNumberFormat* nf, HandleValue x, MutableHandleValue result) { UErrorCode status = U_ZERO_ERROR; @@ -508,7 +537,7 @@ intl_FormatNumberToParts(JSContext* cx, UNumberFormat* nf, double x, MutableHand MOZ_ASSERT(fpositer); ScopedICUObject toClose(fpositer); - RootedString overallResult(cx, PartitionNumberPattern(cx, nf, &x, fpositer)); + RootedString overallResult(cx, PartitionNumberPattern(cx, nf, x, fpositer)); if (!overallResult) return false; @@ -824,7 +853,7 @@ js::intl_FormatNumber(JSContext* cx, unsigned argc, Value* vp) CallArgs args = CallArgsFromVp(argc, vp); MOZ_ASSERT(args.length() == 3); MOZ_ASSERT(args[0].isObject()); - MOZ_ASSERT(args[1].isNumber()); + MOZ_ASSERT(args[1].isNumeric()); MOZ_ASSERT(args[2].isBoolean()); Rooted numberFormat(cx, &args[0].toObject().as()); @@ -842,8 +871,7 @@ js::intl_FormatNumber(JSContext* cx, unsigned argc, Value* vp) // Use the UNumberFormat to actually format the number. if (args[2].toBoolean()) { - return intl_FormatNumberToParts(cx, nf, args[1].toNumber(), args.rval()); + return FormatNumericToParts(cx, nf, args.get(1), args.rval()); } - return intl_FormatNumber(cx, nf, args[1].toNumber(), args.rval()); + return FormatNumeric(cx, nf, args.get(1), args.rval()); } - diff --git a/js/src/builtin/intl/NumberFormat.h b/js/src/builtin/intl/NumberFormat.h index a6a25b07ec..e55efebd80 100644 --- a/js/src/builtin/intl/NumberFormat.h +++ b/js/src/builtin/intl/NumberFormat.h @@ -71,7 +71,7 @@ intl_numberingSystem(JSContext* cx, unsigned argc, Value* vp); extern MOZ_MUST_USE bool intl_FormatNumber(JSContext* cx, unsigned argc, Value* vp); extern MOZ_MUST_USE bool -intl_FormatNumber(JSContext* cx, UNumberFormat* nf, double x, MutableHandleValue result); +FormatNumeric(JSContext* cx, UNumberFormat* nf, HandleValue x, MutableHandleValue result); } // namespace js diff --git a/js/src/builtin/intl/NumberFormat.js b/js/src/builtin/intl/NumberFormat.js index a6bc504224..2d6420ee7a 100644 --- a/js/src/builtin/intl/NumberFormat.js +++ b/js/src/builtin/intl/NumberFormat.js @@ -454,14 +454,14 @@ function numberFormatFormatToBind(value) { // ES5.1 10.5, step 4.d.ii. // Step 1.a.ii-iii. - var x = ToNumber(value); + var x = ToNumeric(value); return intl_FormatNumber(this, x, /* formatToParts = */ false); } /** * Returns a function bound to this NumberFormat that returns a String value - * representing the result of calling ToNumber(value) according to the + * representing the result of calling ToNumeric(value) according to the * effective locale and the formatting options of this NumberFormat. * * Spec: ECMAScript Internationalization API Specification, 11.4.3. @@ -497,7 +497,7 @@ function Intl_NumberFormat_formatToParts(value) { getNumberFormatInternals(nf); // Step 4. - var x = ToNumber(value); + var x = ToNumeric(value); // Step 5. return intl_FormatNumber(nf, x, /* formatToParts = */ true); diff --git a/js/src/builtin/intl/PluralRules.cpp b/js/src/builtin/intl/PluralRules.cpp index e6ad27577b..a610d0e3a0 100644 --- a/js/src/builtin/intl/PluralRules.cpp +++ b/js/src/builtin/intl/PluralRules.cpp @@ -292,8 +292,6 @@ js::intl_SelectPluralRule(JSContext* cx, unsigned argc, Value* vp) if (!type) return false; - double x = args[1].toNumber(); - // We need a NumberFormat in order to format the number // using the number formatting options (minimum/maximum*Digits) // before we push the result to PluralRules @@ -302,7 +300,7 @@ js::intl_SelectPluralRule(JSContext* cx, unsigned argc, Value* vp) // API: http://bugs.icu-project.org/trac/ticket/12763 // RootedValue fmtNumValue(cx); - if (!intl_FormatNumber(cx, nf, x, &fmtNumValue)) + if (!FormatNumeric(cx, nf, args[1], &fmtNumValue)) return false; RootedString fmtNumValueString(cx, fmtNumValue.toString()); AutoStableStringChars stableChars(cx); diff --git a/js/src/jsnum.cpp b/js/src/jsnum.cpp index 573b55cc85..4e8a5288e5 100644 --- a/js/src/jsnum.cpp +++ b/js/src/jsnum.cpp @@ -1503,8 +1503,7 @@ js::ToInt8Slow(JSContext *cx, const HandleValue v, int8_t *out) bool js::ToNumericSlow(ExclusiveContext* cx, MutableHandleValue vp) { - MOZ_ASSERT(!vp.isNumber()); - MOZ_ASSERT(!vp.isBigInt()); + MOZ_ASSERT(!vp.isNumeric()); // Step 1. if (!vp.isPrimitive()) { diff --git a/js/src/jsnum.h b/js/src/jsnum.h index bd53fdc1a0..ee07d0a35d 100644 --- a/js/src/jsnum.h +++ b/js/src/jsnum.h @@ -378,10 +378,9 @@ ToNumericSlow(ExclusiveContext* cx, JS::MutableHandleValue vp); MOZ_ALWAYS_INLINE MOZ_MUST_USE bool ToNumeric(ExclusiveContext* cx, JS::MutableHandleValue vp) { - if (vp.isNumber()) - return true; - if (vp.isBigInt()) + if (vp.isNumeric()) { return true; + } return ToNumericSlow(cx, vp); } diff --git a/js/src/moz.build b/js/src/moz.build index b75afc2628..8e14de6e85 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -705,6 +705,7 @@ selfhosted.inputs = [ 'builtin/Utilities.js', 'builtin/Array.js', 'builtin/AsyncIteration.js', + 'builtin/BigInt.js', 'builtin/Classes.js', 'builtin/Date.js', 'builtin/Error.js', diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index e73e55d9a0..2a60a1885c 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -22,6 +22,7 @@ #include "jswrapper.h" #include "selfhosted.out.h" +#include "builtin/BigInt.h" #include "builtin/intl/Collator.h" #include "builtin/intl/DateTimeFormat.h" #include "builtin/intl/IntlObject.h" @@ -2207,6 +2208,17 @@ static bool intrinsic_ToBigInt(JSContext* cx, unsigned argc, Value* vp) return true; } +static bool intrinsic_ToNumeric(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + if (!ToNumeric(cx, args[0])) { + return false; + } + args.rval().set(args[0]); + return true; +} + // The self-hosting global isn't initialized with the normal set of builtins. // Instead, individual C++-implemented functions that're required by // self-hosted code are defined as global functions. Accessing these @@ -2230,6 +2242,8 @@ static const JSFunctionSpec intrinsic_functions[] = { JS_FN("std_Array_reverse", array_reverse, 0,0), JS_FNINFO("std_Array_splice", array_splice, &array_splice_info, 2,0), + JS_FN("std_BigInt_valueOf", BigIntObject::valueOf, 0,0), + JS_FN("std_Date_now", date_now, 0,0), JS_FN("std_Date_valueOf", date_valueOf, 0,0), @@ -2637,7 +2651,8 @@ static const JSFunctionSpec intrinsic_functions[] = { JS_FN("PromiseResolve", intrinsic_PromiseResolve, 2, 0), JS_FN("ToBigInt", intrinsic_ToBigInt, 1, 0), - + JS_FN("ToNumeric", intrinsic_ToNumeric, 1, 0), + JS_FS_END };