From e7565ff1aef14709057cfc777b4bf78b602f2848 Mon Sep 17 00:00:00 2001 From: Martok Date: Mon, 13 Feb 2023 01:33:19 +0100 Subject: [PATCH] Issue #2046 - Move Intl.NumberFormat functionality into builtin/intl/NumberFormat.* --- js/src/builtin/Intl.cpp | 895 +------------------------ js/src/builtin/Intl.h | 45 -- js/src/builtin/SelfHostingDefines.h | 2 + js/src/builtin/intl/CommonFunctions.h | 1 + js/src/builtin/intl/ICUHeader.h | 23 + js/src/builtin/intl/NumberFormat.cpp | 904 ++++++++++++++++++++++++++ js/src/builtin/intl/NumberFormat.h | 89 +++ js/src/moz.build | 1 + js/src/vm/SelfHosting.cpp | 1 + 9 files changed, 1025 insertions(+), 936 deletions(-) create mode 100644 js/src/builtin/intl/ICUHeader.h create mode 100644 js/src/builtin/intl/NumberFormat.cpp create mode 100644 js/src/builtin/intl/NumberFormat.h diff --git a/js/src/builtin/Intl.cpp b/js/src/builtin/Intl.cpp index 688b7698a8..4aa46ad7d0 100644 --- a/js/src/builtin/Intl.cpp +++ b/js/src/builtin/Intl.cpp @@ -23,21 +23,11 @@ #include "jsobj.h" #include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/ICUHeader.h" +#include "builtin/intl/NumberFormat.h" #include "builtin/intl/ScopedICUObject.h" #include "builtin/IntlTimeZoneData.h" #include "ds/Sort.h" -#include "unicode/plurrule.h" -#include "unicode/ucal.h" -#include "unicode/ucol.h" -#include "unicode/udat.h" -#include "unicode/udatpg.h" -#include "unicode/udisplaycontext.h" -#include "unicode/uenum.h" -#include "unicode/unum.h" -#include "unicode/unumsys.h" -#include "unicode/upluralrules.h" -#include "unicode/ureldatefmt.h" -#include "unicode/ustring.h" #include "vm/DateTime.h" #include "vm/GlobalObject.h" #include "vm/Interpreter.h" @@ -53,7 +43,6 @@ using namespace js; using mozilla::AssertedCast; using mozilla::IsFinite; -using mozilla::IsNaN; using mozilla::IsNegativeZero; using mozilla::PodCopy; @@ -566,261 +555,6 @@ js::intl_CompareStrings(JSContext* cx, unsigned argc, Value* vp) return true; } - -/******************** NumberFormat ********************/ - -static void numberFormat_finalize(FreeOp* fop, JSObject* obj); - -static const uint32_t UNUMBER_FORMAT_SLOT = 0; -static const uint32_t NUMBER_FORMAT_SLOTS_COUNT = 1; - -static const ClassOps NumberFormatClassOps = { - nullptr, /* addProperty */ - nullptr, /* delProperty */ - nullptr, /* getProperty */ - nullptr, /* setProperty */ - nullptr, /* enumerate */ - nullptr, /* resolve */ - nullptr, /* mayResolve */ - numberFormat_finalize -}; - -static const Class NumberFormatClass = { - js_Object_str, - JSCLASS_HAS_RESERVED_SLOTS(NUMBER_FORMAT_SLOTS_COUNT) | - JSCLASS_FOREGROUND_FINALIZE, - &NumberFormatClassOps -}; - -#if JS_HAS_TOSOURCE -static bool -numberFormat_toSource(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - args.rval().setString(cx->names().NumberFormat); - return true; -} -#endif - -static const JSFunctionSpec numberFormat_static_methods[] = { - JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_NumberFormat_supportedLocalesOf", 1, 0), - JS_FS_END -}; - -static const JSFunctionSpec numberFormat_methods[] = { - JS_SELF_HOSTED_FN("resolvedOptions", "Intl_NumberFormat_resolvedOptions", 0, 0), - JS_SELF_HOSTED_FN("formatToParts", "Intl_NumberFormat_formatToParts", 1, 0), -#if JS_HAS_TOSOURCE - JS_FN(js_toSource_str, numberFormat_toSource, 0, 0), -#endif - JS_FS_END -}; - -/** - * 11.2.1 Intl.NumberFormat([ locales [, options]]) - * - * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b - */ -static bool -NumberFormat(JSContext* cx, const CallArgs& args, bool construct) -{ - RootedObject obj(cx); - - // We're following ECMA-402 1st Edition when NumberFormat is called - // because of backward compatibility issues. - // See https://github.com/tc39/ecma402/issues/57 - if (!construct) { - // ES Intl 1st ed., 11.1.2.1 step 3 - JSObject* intl = GlobalObject::getOrCreateIntlObject(cx, cx->global()); - if (!intl) - return false; - RootedValue self(cx, args.thisv()); - if (!self.isUndefined() && (!self.isObject() || self.toObject() != *intl)) { - // ES Intl 1st ed., 11.1.2.1 step 4 - obj = ToObject(cx, self); - if (!obj) - return false; - - // ES Intl 1st ed., 11.1.2.1 step 5 - bool extensible; - if (!IsExtensible(cx, obj, &extensible)) - return false; - if (!extensible) - return Throw(cx, obj, JSMSG_OBJECT_NOT_EXTENSIBLE); - } else { - // ES Intl 1st ed., 11.1.2.1 step 3.a - construct = true; - } - } - if (construct) { - // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). - RootedObject proto(cx); - if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto)) - return false; - - if (!proto) { - proto = GlobalObject::getOrCreateNumberFormatPrototype(cx, cx->global()); - if (!proto) - return false; - } - - obj = NewObjectWithGivenProto(cx, &NumberFormatClass, proto); - if (!obj) - return false; - - obj->as().setReservedSlot(UNUMBER_FORMAT_SLOT, PrivateValue(nullptr)); - } - - RootedValue locales(cx, args.length() > 0 ? args[0] : UndefinedValue()); - RootedValue options(cx, args.length() > 1 ? args[1] : UndefinedValue()); - - // Step 3. - if (!intl::InitializeObject(cx, obj, cx->names().InitializeNumberFormat, locales, options)) - return false; - - args.rval().setObject(*obj); - return true; -} - -static bool -NumberFormat(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - return NumberFormat(cx, args, args.isConstructing()); -} - -bool -js::intl_NumberFormat(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 2); - MOZ_ASSERT(!args.isConstructing()); - // intl_NumberFormat is an intrinsic for self-hosted JavaScript, so it - // cannot be used with "new", but it still has to be treated as a - // constructor. - return NumberFormat(cx, args, true); -} - -static void -numberFormat_finalize(FreeOp* fop, JSObject* obj) -{ - MOZ_ASSERT(fop->onMainThread()); - - // This is-undefined check shouldn't be necessary, but for internal - // brokenness in object allocation code. For the moment, hack around it by - // explicitly guarding against the possibility of the reserved slot not - // containing a private. See bug 949220. - const Value& slot = obj->as().getReservedSlot(UNUMBER_FORMAT_SLOT); - if (!slot.isUndefined()) { - if (UNumberFormat* nf = static_cast(slot.toPrivate())) - unum_close(nf); - } -} - -static JSObject* -CreateNumberFormatPrototype(JSContext* cx, HandleObject Intl, Handle global) -{ - RootedFunction ctor(cx); - ctor = GlobalObject::createConstructor(cx, &NumberFormat, cx->names().NumberFormat, 0); - if (!ctor) - return nullptr; - - RootedNativeObject proto(cx, GlobalObject::createBlankPrototype(cx, global, - &NumberFormatClass)); - if (!proto) - return nullptr; - proto->setReservedSlot(UNUMBER_FORMAT_SLOT, PrivateValue(nullptr)); - - if (!LinkConstructorAndPrototype(cx, ctor, proto)) - return nullptr; - - // 11.2.2 - if (!JS_DefineFunctions(cx, ctor, numberFormat_static_methods)) - return nullptr; - - // 11.3.2 and 11.3.3 - if (!JS_DefineFunctions(cx, proto, numberFormat_methods)) - return nullptr; - - /* - * Install the getter for NumberFormat.prototype.format, which returns a - * bound formatting function for the specified NumberFormat object (suitable - * for passing to methods like Array.prototype.map). - */ - RootedValue getter(cx); - if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().NumberFormatFormatGet, - &getter)) - { - return nullptr; - } - if (!DefineProperty(cx, proto, cx->names().format, UndefinedHandleValue, - JS_DATA_TO_FUNC_PTR(JSGetterOp, &getter.toObject()), - nullptr, JSPROP_GETTER | JSPROP_SHARED)) - { - return nullptr; - } - - RootedValue options(cx); - if (!intl::CreateDefaultOptions(cx, &options)) - return nullptr; - - // 11.2.1 and 11.3 - if (!intl::InitializeObject(cx, proto, cx->names().InitializeNumberFormat, UndefinedHandleValue, - options)) - { - return nullptr; - } - - // 8.1 - RootedValue ctorValue(cx, ObjectValue(*ctor)); - if (!DefineProperty(cx, Intl, cx->names().NumberFormat, ctorValue, nullptr, nullptr, 0)) - return nullptr; - - return proto; -} - -bool -js::intl_NumberFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 0); - - RootedValue result(cx); - if (!GetAvailableLocales(cx, unum_countAvailable, unum_getAvailable, &result)) - return false; - args.rval().set(result); - return true; -} - -bool -js::intl_numberingSystem(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 1); - MOZ_ASSERT(args[0].isString()); - - JSAutoByteString locale(cx, args[0].toString()); - if (!locale) - return false; - - UErrorCode status = U_ZERO_ERROR; - UNumberingSystem* numbers = unumsys_open(IcuLocale(locale.ptr()), &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - ScopedICUObject toClose(numbers); - - const char* name = unumsys_getName(numbers); - RootedString jsname(cx, JS_NewStringCopyZ(cx, name)); - if (!jsname) - return false; - - args.rval().setString(jsname); - return true; -} - /** * * This creates new UNumberFormat with calculated digit formatting @@ -903,629 +637,6 @@ NewUNumberFormatForPluralRules(JSContext* cx, HandleObject pluralRules) return toClose.forget(); } - -/** - * Returns a new UNumberFormat with the locale and number formatting options - * of the given NumberFormat. - */ -static UNumberFormat* -NewUNumberFormat(JSContext* cx, HandleObject numberFormat) -{ - RootedValue value(cx); - - RootedObject internals(cx, intl::GetInternalsObject(cx, numberFormat)); - if (!internals) - return nullptr; - - if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) - return nullptr; - JSAutoByteString locale(cx, value.toString()); - if (!locale) - return nullptr; - - // UNumberFormat options with default values - UNumberFormatStyle uStyle = UNUM_DECIMAL; - const UChar* uCurrency = nullptr; - uint32_t uMinimumIntegerDigits = 1; - uint32_t uMinimumFractionDigits = 0; - uint32_t uMaximumFractionDigits = 3; - int32_t uMinimumSignificantDigits = -1; - int32_t uMaximumSignificantDigits = -1; - bool uUseGrouping = true; - - // Sprinkle appropriate rooting flavor over things the GC might care about. - RootedString currency(cx); - AutoStableStringChars stableChars(cx); - - // We don't need to look at numberingSystem - it can only be set via - // the Unicode locale extension and is therefore already set on locale. - - if (!GetProperty(cx, internals, internals, cx->names().style, &value)) - return nullptr; - JSAutoByteString style(cx, value.toString()); - if (!style) - return nullptr; - - if (StringsAreEqual(style, "currency")) { - if (!GetProperty(cx, internals, internals, cx->names().currency, &value)) - return nullptr; - currency = value.toString(); - MOZ_ASSERT(currency->length() == 3, - "IsWellFormedCurrencyCode permits only length-3 strings"); - if (!currency->ensureFlat(cx) || !stableChars.initTwoByte(cx, currency)) - return nullptr; - // uCurrency remains owned by stableChars. - uCurrency = Char16ToUChar(stableChars.twoByteRange().begin().get()); - if (!uCurrency) - return nullptr; - - if (!GetProperty(cx, internals, internals, cx->names().currencyDisplay, &value)) - return nullptr; - JSAutoByteString currencyDisplay(cx, value.toString()); - if (!currencyDisplay) - return nullptr; - if (StringsAreEqual(currencyDisplay, "code")) { - uStyle = UNUM_CURRENCY_ISO; - } else if (StringsAreEqual(currencyDisplay, "symbol")) { - uStyle = UNUM_CURRENCY; - } else { - MOZ_ASSERT(StringsAreEqual(currencyDisplay, "name")); - uStyle = UNUM_CURRENCY_PLURAL; - } - } else if (StringsAreEqual(style, "percent")) { - uStyle = UNUM_PERCENT; - } else { - MOZ_ASSERT(StringsAreEqual(style, "decimal")); - uStyle = UNUM_DECIMAL; - } - - RootedId id(cx, NameToId(cx->names().minimumSignificantDigits)); - bool hasP; - if (!HasProperty(cx, internals, id, &hasP)) - return nullptr; - if (hasP) { - if (!GetProperty(cx, internals, internals, cx->names().minimumSignificantDigits, - &value)) - return nullptr; - uMinimumSignificantDigits = value.toInt32(); - if (!GetProperty(cx, internals, internals, cx->names().maximumSignificantDigits, - &value)) - return nullptr; - uMaximumSignificantDigits = value.toInt32(); - } else { - if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits, - &value)) - return nullptr; - uMinimumIntegerDigits = AssertedCast(value.toInt32()); - if (!GetProperty(cx, internals, internals, cx->names().minimumFractionDigits, - &value)) - return nullptr; - uMinimumFractionDigits = AssertedCast(value.toInt32()); - if (!GetProperty(cx, internals, internals, cx->names().maximumFractionDigits, - &value)) - return nullptr; - uMaximumFractionDigits = AssertedCast(value.toInt32()); - } - - if (!GetProperty(cx, internals, internals, cx->names().useGrouping, &value)) - return nullptr; - uUseGrouping = value.toBoolean(); - - UErrorCode status = U_ZERO_ERROR; - UNumberFormat* nf = unum_open(uStyle, nullptr, 0, IcuLocale(locale.ptr()), nullptr, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return nullptr; - } - ScopedICUObject toClose(nf); - - if (uCurrency) { - unum_setTextAttribute(nf, UNUM_CURRENCY_CODE, uCurrency, 3, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return nullptr; - } - } - if (uMinimumSignificantDigits != -1) { - unum_setAttribute(nf, UNUM_SIGNIFICANT_DIGITS_USED, true); - unum_setAttribute(nf, UNUM_MIN_SIGNIFICANT_DIGITS, uMinimumSignificantDigits); - unum_setAttribute(nf, UNUM_MAX_SIGNIFICANT_DIGITS, uMaximumSignificantDigits); - } else { - unum_setAttribute(nf, UNUM_MIN_INTEGER_DIGITS, uMinimumIntegerDigits); - unum_setAttribute(nf, UNUM_MIN_FRACTION_DIGITS, uMinimumFractionDigits); - unum_setAttribute(nf, UNUM_MAX_FRACTION_DIGITS, uMaximumFractionDigits); - } - unum_setAttribute(nf, UNUM_GROUPING_USED, uUseGrouping); - unum_setAttribute(nf, UNUM_ROUNDING_MODE, UNUM_ROUND_HALFUP); - - return toClose.forget(); -} - -using FormattedNumberChars = Vector; - -static bool -PartitionNumberPattern(JSContext* cx, UNumberFormat* nf, double* x, - UFieldPositionIterator* fpositer, FormattedNumberChars& formattedChars) -{ - // PartitionNumberPattern doesn't consider -0.0 to be negative. - if (IsNegativeZero(*x)) - *x = 0.0; - - return CallICU(cx, formattedChars, [nf, x, fpositer](UChar* chars, int32_t size, UErrorCode* status) { - return unum_formatDoubleForFields(nf, *x, chars, size, fpositer, status); - }) >= 0; -} - -static bool -intl_FormatNumber(JSContext* cx, UNumberFormat* nf, double x, MutableHandleValue result) -{ - // Passing null for |fpositer| will just not compute partition information, - // letting us common up all ICU number-formatting code. - FormattedNumberChars chars(cx); - if (!PartitionNumberPattern(cx, nf, &x, nullptr, chars)) - return false; - - JSString* str = NewStringCopyN(cx, chars.begin(), chars.length()); - if (!str) - return false; - - result.setString(str); - return true; -} - -using FieldType = ImmutablePropertyNamePtr JSAtomState::*; - -static FieldType -GetFieldTypeForNumberField(UNumberFormatFields fieldName, double d) -{ - // 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 - // this code is compiled with a different ICU with more UNumberFormatFields - // enum initializers. Please guard such cases with appropriate ICU - // 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; - return &JSAtomState::integer; - - case UNUM_GROUPING_SEPARATOR_FIELD: - return &JSAtomState::group; - - case UNUM_DECIMAL_SEPARATOR_FIELD: - return &JSAtomState::decimal; - - case UNUM_FRACTION_FIELD: - 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; - } - - case UNUM_PERCENT_FIELD: - return &JSAtomState::percentSign; - - case UNUM_CURRENCY_FIELD: - return &JSAtomState::currency; - - case UNUM_PERMILL_FIELD: - MOZ_ASSERT_UNREACHABLE("unexpected permill field found, even though " - "we don't use any user-defined patterns that " - "would require a permill field"); - break; - - case UNUM_EXPONENT_SYMBOL_FIELD: - case UNUM_EXPONENT_SIGN_FIELD: - case UNUM_EXPONENT_FIELD: - MOZ_ASSERT_UNREACHABLE("exponent field unexpectedly found in " - "formatted number, even though UNUM_SCIENTIFIC " - "and scientific notation were never requested"); - break; - - case UNUM_FIELD_COUNT: - MOZ_ASSERT_UNREACHABLE("format field sentinel value returned by " - "iterator!"); - break; - } - - MOZ_ASSERT_UNREACHABLE("unenumerated, undocumented format field returned " - "by iterator"); - return nullptr; -} - -static bool -intl_FormatNumberToParts(JSContext* cx, UNumberFormat* nf, double x, MutableHandleValue result) -{ - UErrorCode status = U_ZERO_ERROR; - - UFieldPositionIterator* fpositer = ufieldpositer_open(&status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - MOZ_ASSERT(fpositer); - ScopedICUObject toClose(fpositer); - - FormattedNumberChars chars(cx); - if (!PartitionNumberPattern(cx, nf, &x, fpositer, chars)) - return false; - - RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx)); - if (!partsArray) - return false; - - RootedString overallResult(cx, NewStringCopyN(cx, chars.begin(), chars.length())); - if (!overallResult) - return false; - - // First, vacuum up fields in the overall formatted string. - - struct Field - { - uint32_t begin; - uint32_t end; - FieldType type; - - // Needed for vector-resizing scratch space. - Field() = default; - - Field(uint32_t begin, uint32_t end, FieldType type) - : begin(begin), end(end), type(type) - {} - }; - - using FieldsVector = Vector; - FieldsVector fields(cx); - - int32_t fieldInt, beginIndexInt, endIndexInt; - while ((fieldInt = ufieldpositer_next(fpositer, &beginIndexInt, &endIndexInt)) >= 0) { - MOZ_ASSERT(beginIndexInt >= 0); - MOZ_ASSERT(endIndexInt >= 0); - MOZ_ASSERT(beginIndexInt < endIndexInt, - "erm, aren't fields always non-empty?"); - - FieldType type = GetFieldTypeForNumberField(UNumberFormatFields(fieldInt), x); - if (!fields.emplaceBack(uint32_t(beginIndexInt), uint32_t(endIndexInt), type)) - return false; - } - - // Second, merge sort the fields vector. Expand the vector to have scratch - // space for performing the sort. - size_t fieldsLen = fields.length(); - if (!fields.resizeUninitialized(fieldsLen * 2)) - return false; - - MOZ_ALWAYS_TRUE(MergeSort(fields.begin(), fieldsLen, fields.begin() + fieldsLen, - [](const Field& left, const Field& right, - bool* lessOrEqual) - { - // Sort first by begin index, then to place - // enclosing fields before nested fields. - *lessOrEqual = left.begin < right.begin || - (left.begin == right.begin && - left.end > right.end); - return true; - })); - - // Deallocate the scratch space. - if (!fields.resize(fieldsLen)) - return false; - - // Third, iterate over the sorted field list to generate a sequence of - // parts (what ECMA-402 actually exposes). A part is a maximal character - // sequence entirely within no field or a single most-nested field. - // - // Diagrams may be helpful to illustrate how fields map to parts. Consider - // formatting -28,114,774,228,750.32, the US national surplus (negative - // because it's actually a debt) on March 31, 2021. - // - // var options = - // { style: "currency", currency: "USD", currencyDisplay: "name" }; - // var usdFormatter = new Intl.NumberFormat("en-US", options); - // usdFormatter.format(-28114774228750.32); - // - // The formatted result is "-28,114,774,228,750.32 US dollars". ICU - // identifies these fields in the string: - // - // UNUM_GROUPING_SEPARATOR_FIELD - // | - // UNUM_SIGN_FIELD | UNUM_DECIMAL_SEPARATOR_FIELD - // | __________/| | - // | / | | | | - // "-28,114,774,228,750.32 US dollars" - // \________________/ |/ \_______/ - // | | | - // UNUM_INTEGER_FIELD | UNUM_CURRENCY_FIELD - // | - // UNUM_FRACTION_FIELD - // - // These fields map to parts as follows: - // - // integer decimal - // _____|________ | - // / /| |\ |\ |\ | literal - // /| / | | \ | \ | \| | - // "-28,114,774,228,750.32 US dollars" - // | \___|___|___/ |/ \________/ - // | | | | - // | group | currency - // | | - // minusSign fraction - // - // The sign is a part. Each comma is a part, splitting the integer field - // into parts for trillions/billions/&c. digits. The decimal point is a - // part. Cents are a part. The space between cents and currency is a part - // (outside any field). Last, the currency field is a part. - // - // Because parts fully partition the formatted string, we only track the - // end of each part -- the beginning is implicitly the last part's end. - struct Part - { - uint32_t end; - FieldType type; - }; - - class PartGenerator - { - // The fields in order from start to end, then least to most nested. - const FieldsVector& fields; - - // Index of the current field, in |fields|, being considered to - // determine part boundaries. |lastEnd <= fields[index].begin| is an - // invariant. - size_t index; - - // The end index of the last part produced, always less than or equal - // to |limit|, strictly increasing. - uint32_t lastEnd; - - // The length of the overall formatted string. - const uint32_t limit; - - Vector enclosingFields; - - void popEnclosingFieldsEndingAt(uint32_t end) { - MOZ_ASSERT_IF(enclosingFields.length() > 0, - fields[enclosingFields.back()].end >= end); - - while (enclosingFields.length() > 0 && fields[enclosingFields.back()].end == end) - enclosingFields.popBack(); - } - - bool nextPartInternal(Part* part) { - size_t len = fields.length(); - MOZ_ASSERT(index <= len); - - // If we're out of fields, all that remains are part(s) consisting - // of trailing portions of enclosing fields, and maybe a final - // literal part. - if (index == len) { - if (enclosingFields.length() > 0) { - const auto& enclosing = fields[enclosingFields.popCopy()]; - part->end = enclosing.end; - part->type = enclosing.type; - - // If additional enclosing fields end where this part ends, - // pop them as well. - popEnclosingFieldsEndingAt(part->end); - } else { - part->end = limit; - part->type = &JSAtomState::literal; - } - - return true; - } - - // Otherwise we still have a field to process. - const Field* current = &fields[index]; - MOZ_ASSERT(lastEnd <= current->begin); - MOZ_ASSERT(current->begin < current->end); - - // But first, deal with inter-field space. - if (lastEnd < current->begin) { - if (enclosingFields.length() > 0) { - // Space between fields, within an enclosing field, is part - // of that enclosing field, until the start of the current - // field or the end of the enclosing field, whichever is - // earlier. - const auto& enclosing = fields[enclosingFields.back()]; - part->end = std::min(enclosing.end, current->begin); - part->type = enclosing.type; - popEnclosingFieldsEndingAt(part->end); - } else { - // If there's no enclosing field, the space is a literal. - part->end = current->begin; - part->type = &JSAtomState::literal; - } - - return true; - } - - // Otherwise, the part spans a prefix of the current field. Find - // the most-nested field containing that prefix. - const Field* next; - do { - current = &fields[index]; - - // If the current field is last, the part extends to its end. - if (++index == len) { - part->end = current->end; - part->type = current->type; - return true; - } - - next = &fields[index]; - MOZ_ASSERT(current->begin <= next->begin); - MOZ_ASSERT(current->begin < next->end); - - // If the next field nests within the current field, push an - // enclosing field. (If there are no nested fields, don't - // bother pushing a field that'd be immediately popped.) - if (current->end > next->begin) { - if (!enclosingFields.append(index - 1)) - return false; - } - - // Do so until the next field begins after this one. - } while (current->begin == next->begin); - - part->type = current->type; - - if (current->end <= next->begin) { - // The next field begins after the current field ends. Therefore - // the current part ends at the end of the current field. - part->end = current->end; - popEnclosingFieldsEndingAt(part->end); - } else { - // The current field encloses the next one. The current part - // ends where the next field/part will start. - part->end = next->begin; - } - - return true; - } - - public: - PartGenerator(JSContext* cx, const FieldsVector& vec, uint32_t limit) - : fields(vec), index(0), lastEnd(0), limit(limit), enclosingFields(cx) - {} - - bool nextPart(bool* hasPart, Part* part) { - // There are no parts left if we've partitioned the entire string. - if (lastEnd == limit) { - MOZ_ASSERT(enclosingFields.length() == 0); - *hasPart = false; - return true; - } - - if (!nextPartInternal(part)) - return false; - - *hasPart = true; - lastEnd = part->end; - return true; - } - }; - - // Finally, generate the result array. - size_t lastEndIndex = 0; - uint32_t partIndex = 0; - RootedObject singlePart(cx); - RootedValue propVal(cx); - - PartGenerator gen(cx, fields, chars.length()); - do { - bool hasPart; - Part part; - if (!gen.nextPart(&hasPart, &part)) - return false; - - if (!hasPart) - break; - - FieldType type = part.type; - size_t endIndex = part.end; - - MOZ_ASSERT(lastEndIndex < endIndex); - - singlePart = NewBuiltinClassInstance(cx); - if (!singlePart) - return false; - - propVal.setString(cx->names().*type); - if (!DefineProperty(cx, singlePart, cx->names().type, propVal)) - return false; - - JSLinearString* partSubstr = - NewDependentString(cx, overallResult, lastEndIndex, endIndex - lastEndIndex); - if (!partSubstr) - return false; - - propVal.setString(partSubstr); - if (!DefineProperty(cx, singlePart, cx->names().value, propVal)) - return false; - - propVal.setObject(*singlePart); - if (!DefineElement(cx, partsArray, partIndex, propVal)) - return false; - - lastEndIndex = endIndex; - partIndex++; - } while (true); - - MOZ_ASSERT(lastEndIndex == chars.length(), - "result array must partition the entire string"); - - result.setObject(*partsArray); - return true; -} - -bool -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[2].isBoolean()); - - RootedObject numberFormat(cx, &args[0].toObject()); - - // Obtain a UNumberFormat object, cached if possible. - bool isNumberFormatInstance = numberFormat->getClass() == &NumberFormatClass; - UNumberFormat* nf; - if (isNumberFormatInstance) { - void* priv = - numberFormat->as().getReservedSlot(UNUMBER_FORMAT_SLOT).toPrivate(); - nf = static_cast(priv); - if (!nf) { - nf = NewUNumberFormat(cx, numberFormat); - if (!nf) - return false; - numberFormat->as().setReservedSlot(UNUMBER_FORMAT_SLOT, PrivateValue(nf)); - } - } else { - // There's no good place to cache the ICU number format for an object - // that has been initialized as a NumberFormat but is not a - // NumberFormat instance. One possibility might be to add a - // NumberFormat instance as an internal property to each such object. - nf = NewUNumberFormat(cx, numberFormat); - if (!nf) - return false; - } - - // Use the UNumberFormat to actually format the number. - double d = args[1].toNumber(); - RootedValue result(cx); - - bool success; - if (args[2].toBoolean()) { - success = intl_FormatNumberToParts(cx, nf, d, &result); - } else { - MOZ_ASSERT(!args[2].toBoolean(), - "shouldn't be doing formatToParts without an ICU that " - "supports it"); - success = intl_FormatNumber(cx, nf, d, &result); - } - - if (!isNumberFormatInstance) - unum_close(nf); - if (!success) - return false; - args.rval().set(result); - return true; -} - - /******************** DateTimeFormat ********************/ static void dateTimeFormat_finalize(FreeOp* fop, JSObject* obj); @@ -2337,6 +1448,8 @@ intl_FormatDateTime(JSContext* cx, UDateFormat* df, double x, MutableHandleValue return true; } +using FieldType = ImmutablePropertyNamePtr JSAtomState::*; + static FieldType GetFieldTypeForFormatField(UDateFormatField fieldName) { diff --git a/js/src/builtin/Intl.h b/js/src/builtin/Intl.h index 330b8a4963..1026da5e5e 100644 --- a/js/src/builtin/Intl.h +++ b/js/src/builtin/Intl.h @@ -218,51 +218,6 @@ extern MOZ_MUST_USE bool intl_CompareStrings(JSContext* cx, unsigned argc, Value* vp); -/******************** NumberFormat ********************/ - -/** - * Returns a new instance of the standard built-in NumberFormat constructor. - * Self-hosted code cannot cache this constructor (as it does for others in - * Utilities.js) because it is initialized after self-hosted code is compiled. - * - * Usage: numberFormat = intl_NumberFormat(locales, options) - */ -extern MOZ_MUST_USE bool -intl_NumberFormat(JSContext* cx, unsigned argc, Value* vp); - -/** - * Returns an object indicating the supported locales for number formatting - * by having a true-valued property for each such locale with the - * canonicalized language tag as the property name. The object has no - * prototype. - * - * Usage: availableLocales = intl_NumberFormat_availableLocales() - */ -extern MOZ_MUST_USE bool -intl_NumberFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp); - -/** - * Returns the numbering system type identifier per Unicode - * Technical Standard 35, Unicode Locale Data Markup Language, for the - * default numbering system for the given locale. - * - * Usage: defaultNumberingSystem = intl_numberingSystem(locale) - */ -extern MOZ_MUST_USE bool -intl_numberingSystem(JSContext* cx, unsigned argc, Value* vp); - -/** - * Returns a string representing the number x according to the effective - * locale and the formatting options of the given NumberFormat. - * - * Spec: ECMAScript Internationalization API Specification, 11.3.2. - * - * Usage: formatted = intl_FormatNumber(numberFormat, x) - */ -extern MOZ_MUST_USE bool -intl_FormatNumber(JSContext* cx, unsigned argc, Value* vp); - - /******************** DateTimeFormat ********************/ /** diff --git a/js/src/builtin/SelfHostingDefines.h b/js/src/builtin/SelfHostingDefines.h index e233f5c397..5bd31f58ec 100644 --- a/js/src/builtin/SelfHostingDefines.h +++ b/js/src/builtin/SelfHostingDefines.h @@ -96,6 +96,8 @@ #define REGEXP_STRING_ITERATOR_FLAGS_SLOT 2 #define REGEXP_STRING_ITERATOR_DONE_SLOT 3 +#define INTL_INTERNALS_OBJECT_SLOT 0 + #define MODULE_OBJECT_ENVIRONMENT_SLOT 1 #define MODULE_OBJECT_STATUS_SLOT 3 #define MODULE_OBJECT_EVALUATION_ERROR_SLOT 4 diff --git a/js/src/builtin/intl/CommonFunctions.h b/js/src/builtin/intl/CommonFunctions.h index 90d3a5636b..aae221b011 100644 --- a/js/src/builtin/intl/CommonFunctions.h +++ b/js/src/builtin/intl/CommonFunctions.h @@ -14,6 +14,7 @@ #include #include +#include "builtin/intl/ICUHeader.h" #include "js/RootingAPI.h" #include "js/Vector.h" #include "vm/String.h" diff --git a/js/src/builtin/intl/ICUHeader.h b/js/src/builtin/intl/ICUHeader.h new file mode 100644 index 0000000000..0a0e280838 --- /dev/null +++ b/js/src/builtin/intl/ICUHeader.h @@ -0,0 +1,23 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * 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/. */ + +#ifndef builtin_intl_ICUHeader_h +#define builtin_intl_ICUHeader_h + +#include "unicode/plurrule.h" +#include "unicode/ucal.h" +#include "unicode/ucol.h" +#include "unicode/udat.h" +#include "unicode/udatpg.h" +#include "unicode/udisplaycontext.h" +#include "unicode/uenum.h" +#include "unicode/unum.h" +#include "unicode/unumsys.h" +#include "unicode/upluralrules.h" +#include "unicode/ureldatefmt.h" +#include "unicode/ustring.h" + +#endif /* builtin_intl_ICUHeader_h */ diff --git a/js/src/builtin/intl/NumberFormat.cpp b/js/src/builtin/intl/NumberFormat.cpp new file mode 100644 index 0000000000..4fc08a0920 --- /dev/null +++ b/js/src/builtin/intl/NumberFormat.cpp @@ -0,0 +1,904 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * 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/. */ + +/* Intl.NumberFormat implementation. */ + +#include "builtin/intl/NumberFormat.h" + +#include "mozilla/Assertions.h" +#include "mozilla/FloatingPoint.h" + +#include +#include +#include + +#include "jscntxt.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/ICUHeader.h" +#include "builtin/intl/ScopedICUObject.h" +#include "ds/Sort.h" +#include "js/RootingAPI.h" + +#include "js/TypeDecls.h" +#include "vm/SelfHosting.h" +#include "vm/Stack.h" + +#include "jsobjinlines.h" + +using namespace js; + +using mozilla::AssertedCast; +using mozilla::IsFinite; +using mozilla::IsNaN; +using mozilla::IsNegativeZero; +using js::intl::CallICU; +using js::intl::GetAvailableLocales; +using js::intl::IcuLocale; +using js::intl::INITIAL_CHAR_BUFFER_SIZE; +using js::intl::StringsAreEqual; + +/******************** NumberFormat ********************/ + +const ClassOps NumberFormatObject::classOps_ = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* setProperty */ + nullptr, /* enumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + NumberFormatObject::finalize +}; + +const Class NumberFormatObject::class_ = { + js_Object_str, + JSCLASS_HAS_RESERVED_SLOTS(NumberFormatObject::SLOT_COUNT) | + JSCLASS_FOREGROUND_FINALIZE, + &NumberFormatObject::classOps_ +}; + +#if JS_HAS_TOSOURCE +static bool +numberFormat_toSource(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().NumberFormat); + return true; +} +#endif + +static const JSFunctionSpec numberFormat_static_methods[] = { + JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_NumberFormat_supportedLocalesOf", 1, 0), + JS_FS_END +}; + +static const JSFunctionSpec numberFormat_methods[] = { + JS_SELF_HOSTED_FN("resolvedOptions", "Intl_NumberFormat_resolvedOptions", 0, 0), + JS_SELF_HOSTED_FN("formatToParts", "Intl_NumberFormat_formatToParts", 1, 0), +#if JS_HAS_TOSOURCE + JS_FN(js_toSource_str, numberFormat_toSource, 0, 0), +#endif + JS_FS_END +}; + +/** + * 11.2.1 Intl.NumberFormat([ locales [, options]]) + * + * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b + */ +static bool +NumberFormat(JSContext* cx, const CallArgs& args, bool construct) +{ + RootedObject obj(cx); + + // We're following ECMA-402 1st Edition when NumberFormat is called + // because of backward compatibility issues. + // See https://github.com/tc39/ecma402/issues/57 + if (!construct) { + // ES Intl 1st ed., 11.1.2.1 step 3 + JSObject* intl = GlobalObject::getOrCreateIntlObject(cx, cx->global()); + if (!intl) + return false; + RootedValue self(cx, args.thisv()); + if (!self.isUndefined() && (!self.isObject() || self.toObject() != *intl)) { + // ES Intl 1st ed., 11.1.2.1 step 4 + obj = ToObject(cx, self); + if (!obj) + return false; + + // ES Intl 1st ed., 11.1.2.1 step 5 + bool extensible; + if (!IsExtensible(cx, obj, &extensible)) + return false; + if (!extensible) + return Throw(cx, obj, JSMSG_OBJECT_NOT_EXTENSIBLE); + } else { + // ES Intl 1st ed., 11.1.2.1 step 3.a + construct = true; + } + } + if (construct) { + // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto)) + return false; + + if (!proto) { + proto = GlobalObject::getOrCreateNumberFormatPrototype(cx, cx->global()); + if (!proto) + return false; + } + + obj = NewObjectWithGivenProto(cx, proto); + if (!obj) + return false; + + obj->as().setReservedSlot(NumberFormatObject::INTERNALS_SLOT, NullValue()); + obj->as().setReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT, PrivateValue(nullptr)); + } + + RootedValue locales(cx, args.length() > 0 ? args[0] : UndefinedValue()); + RootedValue options(cx, args.length() > 1 ? args[1] : UndefinedValue()); + + // Step 3. + if (!intl::InitializeObject(cx, obj, cx->names().InitializeNumberFormat, locales, options)) + return false; + + args.rval().setObject(*obj); + return true; +} + +static bool +NumberFormat(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return NumberFormat(cx, args, args.isConstructing()); +} + +bool +js::intl_NumberFormat(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(!args.isConstructing()); + // intl_NumberFormat is an intrinsic for self-hosted JavaScript, so it + // cannot be used with "new", but it still has to be treated as a + // constructor. + return NumberFormat(cx, args, true); +} + +void +js::NumberFormatObject::finalize(FreeOp* fop, JSObject* obj) +{ + MOZ_ASSERT(fop->onMainThread()); + + // This is-undefined check shouldn't be necessary, but for internal + // brokenness in object allocation code. For the moment, hack around it by + // explicitly guarding against the possibility of the reserved slot not + // containing a private. See bug 949220. + const Value& slot = obj->as().getReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT); + if (!slot.isUndefined()) { + if (UNumberFormat* nf = static_cast(slot.toPrivate())) + unum_close(nf); + } +} + +JSObject* +js::CreateNumberFormatPrototype(JSContext* cx, HandleObject Intl, Handle global) +{ + RootedFunction ctor(cx); + ctor = GlobalObject::createConstructor(cx, &NumberFormat, cx->names().NumberFormat, 0); + if (!ctor) + return nullptr; + + RootedNativeObject proto(cx, GlobalObject::createBlankPrototype(cx, global, + &NumberFormatObject::class_)); + if (!proto) + return nullptr; + proto->setReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT, PrivateValue(nullptr)); + + if (!LinkConstructorAndPrototype(cx, ctor, proto)) + return nullptr; + + // 11.2.2 + if (!JS_DefineFunctions(cx, ctor, numberFormat_static_methods)) + return nullptr; + + // 11.3.2 and 11.3.3 + if (!JS_DefineFunctions(cx, proto, numberFormat_methods)) + return nullptr; + + /* + * Install the getter for NumberFormat.prototype.format, which returns a + * bound formatting function for the specified NumberFormat object (suitable + * for passing to methods like Array.prototype.map). + */ + RootedValue getter(cx); + if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().NumberFormatFormatGet, + &getter)) + { + return nullptr; + } + if (!DefineProperty(cx, proto, cx->names().format, UndefinedHandleValue, + JS_DATA_TO_FUNC_PTR(JSGetterOp, &getter.toObject()), + nullptr, JSPROP_GETTER | JSPROP_SHARED)) + { + return nullptr; + } + + RootedValue options(cx); + if (!intl::CreateDefaultOptions(cx, &options)) + return nullptr; + + // 11.2.1 and 11.3 + if (!intl::InitializeObject(cx, proto, cx->names().InitializeNumberFormat, UndefinedHandleValue, + options)) + { + return nullptr; + } + + // 8.1 + RootedValue ctorValue(cx, ObjectValue(*ctor)); + if (!DefineProperty(cx, Intl, cx->names().NumberFormat, ctorValue, nullptr, nullptr, 0)) + return nullptr; + + return proto; +} + +bool +js::intl_NumberFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + RootedValue result(cx); + if (!GetAvailableLocales(cx, unum_countAvailable, unum_getAvailable, &result)) + return false; + args.rval().set(result); + return true; +} + +bool +js::intl_numberingSystem(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + JSAutoByteString locale(cx, args[0].toString()); + if (!locale) + return false; + + UErrorCode status = U_ZERO_ERROR; + UNumberingSystem* numbers = unumsys_open(IcuLocale(locale.ptr()), &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + ScopedICUObject toClose(numbers); + + const char* name = unumsys_getName(numbers); + RootedString jsname(cx, JS_NewStringCopyZ(cx, name)); + if (!jsname) + return false; + + args.rval().setString(jsname); + return true; +} + +/** + * Returns a new UNumberFormat with the locale and number formatting options + * of the given NumberFormat. + */ +static UNumberFormat* +NewUNumberFormat(JSContext* cx, HandleObject numberFormat) +{ + RootedValue value(cx); + + RootedObject internals(cx, intl::GetInternalsObject(cx, numberFormat)); + if (!internals) + return nullptr; + + if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) + return nullptr; + JSAutoByteString locale(cx, value.toString()); + if (!locale) + return nullptr; + + // UNumberFormat options with default values + UNumberFormatStyle uStyle = UNUM_DECIMAL; + const UChar* uCurrency = nullptr; + uint32_t uMinimumIntegerDigits = 1; + uint32_t uMinimumFractionDigits = 0; + uint32_t uMaximumFractionDigits = 3; + int32_t uMinimumSignificantDigits = -1; + int32_t uMaximumSignificantDigits = -1; + bool uUseGrouping = true; + + // Sprinkle appropriate rooting flavor over things the GC might care about. + RootedString currency(cx); + AutoStableStringChars stableChars(cx); + + // We don't need to look at numberingSystem - it can only be set via + // the Unicode locale extension and is therefore already set on locale. + + if (!GetProperty(cx, internals, internals, cx->names().style, &value)) + return nullptr; + JSAutoByteString style(cx, value.toString()); + if (!style) + return nullptr; + + if (StringsAreEqual(style, "currency")) { + if (!GetProperty(cx, internals, internals, cx->names().currency, &value)) + return nullptr; + currency = value.toString(); + MOZ_ASSERT(currency->length() == 3, + "IsWellFormedCurrencyCode permits only length-3 strings"); + if (!currency->ensureFlat(cx) || !stableChars.initTwoByte(cx, currency)) + return nullptr; + // uCurrency remains owned by stableChars. + uCurrency = Char16ToUChar(stableChars.twoByteRange().begin().get()); + if (!uCurrency) + return nullptr; + + if (!GetProperty(cx, internals, internals, cx->names().currencyDisplay, &value)) + return nullptr; + JSAutoByteString currencyDisplay(cx, value.toString()); + if (!currencyDisplay) + return nullptr; + if (StringsAreEqual(currencyDisplay, "code")) { + uStyle = UNUM_CURRENCY_ISO; + } else if (StringsAreEqual(currencyDisplay, "symbol")) { + uStyle = UNUM_CURRENCY; + } else { + MOZ_ASSERT(StringsAreEqual(currencyDisplay, "name")); + uStyle = UNUM_CURRENCY_PLURAL; + } + } else if (StringsAreEqual(style, "percent")) { + uStyle = UNUM_PERCENT; + } else { + MOZ_ASSERT(StringsAreEqual(style, "decimal")); + uStyle = UNUM_DECIMAL; + } + + RootedId id(cx, NameToId(cx->names().minimumSignificantDigits)); + bool hasP; + if (!HasProperty(cx, internals, id, &hasP)) + return nullptr; + if (hasP) { + if (!GetProperty(cx, internals, internals, cx->names().minimumSignificantDigits, + &value)) + return nullptr; + uMinimumSignificantDigits = value.toInt32(); + if (!GetProperty(cx, internals, internals, cx->names().maximumSignificantDigits, + &value)) + return nullptr; + uMaximumSignificantDigits = value.toInt32(); + } else { + if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits, + &value)) + return nullptr; + uMinimumIntegerDigits = AssertedCast(value.toInt32()); + if (!GetProperty(cx, internals, internals, cx->names().minimumFractionDigits, + &value)) + return nullptr; + uMinimumFractionDigits = AssertedCast(value.toInt32()); + if (!GetProperty(cx, internals, internals, cx->names().maximumFractionDigits, + &value)) + return nullptr; + uMaximumFractionDigits = AssertedCast(value.toInt32()); + } + + if (!GetProperty(cx, internals, internals, cx->names().useGrouping, &value)) + return nullptr; + uUseGrouping = value.toBoolean(); + + UErrorCode status = U_ZERO_ERROR; + UNumberFormat* nf = unum_open(uStyle, nullptr, 0, IcuLocale(locale.ptr()), nullptr, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return nullptr; + } + ScopedICUObject toClose(nf); + + if (uCurrency) { + unum_setTextAttribute(nf, UNUM_CURRENCY_CODE, uCurrency, 3, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return nullptr; + } + } + if (uMinimumSignificantDigits != -1) { + unum_setAttribute(nf, UNUM_SIGNIFICANT_DIGITS_USED, true); + unum_setAttribute(nf, UNUM_MIN_SIGNIFICANT_DIGITS, uMinimumSignificantDigits); + unum_setAttribute(nf, UNUM_MAX_SIGNIFICANT_DIGITS, uMaximumSignificantDigits); + } else { + unum_setAttribute(nf, UNUM_MIN_INTEGER_DIGITS, uMinimumIntegerDigits); + unum_setAttribute(nf, UNUM_MIN_FRACTION_DIGITS, uMinimumFractionDigits); + unum_setAttribute(nf, UNUM_MAX_FRACTION_DIGITS, uMaximumFractionDigits); + } + unum_setAttribute(nf, UNUM_GROUPING_USED, uUseGrouping); + unum_setAttribute(nf, UNUM_ROUNDING_MODE, UNUM_ROUND_HALFUP); + + return toClose.forget(); +} + +static JSString* +PartitionNumberPattern(JSContext* cx, UNumberFormat* nf, double* x, + UFieldPositionIterator* fpositer) +{ + // PartitionNumberPattern doesn't consider -0.0 to be negative. + if (IsNegativeZero(*x)) + *x = 0.0; + + return CallICU(cx, [nf, x, fpositer](UChar* chars, int32_t size, UErrorCode* status) { + return unum_formatDoubleForFields(nf, *x, chars, size, fpositer, status); + }); +} + +bool +js::intl_FormatNumber(JSContext* cx, UNumberFormat* nf, double 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); + if (!str) + return false; + + result.setString(str); + return true; +} + +using FieldType = ImmutablePropertyNamePtr JSAtomState::*; + +static FieldType +GetFieldTypeForNumberField(UNumberFormatFields fieldName, double d) +{ + // 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 + // this code is compiled with a different ICU with more UNumberFormatFields + // enum initializers. Please guard such cases with appropriate ICU + // 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; + return &JSAtomState::integer; + + case UNUM_GROUPING_SEPARATOR_FIELD: + return &JSAtomState::group; + + case UNUM_DECIMAL_SEPARATOR_FIELD: + return &JSAtomState::decimal; + + case UNUM_FRACTION_FIELD: + 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; + } + + case UNUM_PERCENT_FIELD: + return &JSAtomState::percentSign; + + case UNUM_CURRENCY_FIELD: + return &JSAtomState::currency; + + case UNUM_PERMILL_FIELD: + MOZ_ASSERT_UNREACHABLE("unexpected permill field found, even though " + "we don't use any user-defined patterns that " + "would require a permill field"); + break; + + case UNUM_EXPONENT_SYMBOL_FIELD: + case UNUM_EXPONENT_SIGN_FIELD: + case UNUM_EXPONENT_FIELD: + MOZ_ASSERT_UNREACHABLE("exponent field unexpectedly found in " + "formatted number, even though UNUM_SCIENTIFIC " + "and scientific notation were never requested"); + break; + + case UNUM_FIELD_COUNT: + MOZ_ASSERT_UNREACHABLE("format field sentinel value returned by " + "iterator!"); + break; + } + + MOZ_ASSERT_UNREACHABLE("unenumerated, undocumented format field returned " + "by iterator"); + return nullptr; +} + +static bool +intl_FormatNumberToParts(JSContext* cx, UNumberFormat* nf, double x, MutableHandleValue result) +{ + UErrorCode status = U_ZERO_ERROR; + + UFieldPositionIterator* fpositer = ufieldpositer_open(&status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + MOZ_ASSERT(fpositer); + ScopedICUObject toClose(fpositer); + + RootedString overallResult(cx, PartitionNumberPattern(cx, nf, &x, fpositer)); + if (!overallResult) + return false; + + RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx)); + if (!partsArray) + return false; + + // First, vacuum up fields in the overall formatted string. + + struct Field + { + uint32_t begin; + uint32_t end; + FieldType type; + + // Needed for vector-resizing scratch space. + Field() = default; + + Field(uint32_t begin, uint32_t end, FieldType type) + : begin(begin), end(end), type(type) + {} + }; + + using FieldsVector = Vector; + FieldsVector fields(cx); + + int32_t fieldInt, beginIndexInt, endIndexInt; + while ((fieldInt = ufieldpositer_next(fpositer, &beginIndexInt, &endIndexInt)) >= 0) { + MOZ_ASSERT(beginIndexInt >= 0); + MOZ_ASSERT(endIndexInt >= 0); + MOZ_ASSERT(beginIndexInt < endIndexInt, + "erm, aren't fields always non-empty?"); + + FieldType type = GetFieldTypeForNumberField(UNumberFormatFields(fieldInt), x); + if (!fields.emplaceBack(uint32_t(beginIndexInt), uint32_t(endIndexInt), type)) + return false; + } + + // Second, merge sort the fields vector. Expand the vector to have scratch + // space for performing the sort. + size_t fieldsLen = fields.length(); + if (!fields.resizeUninitialized(fieldsLen * 2)) + return false; + + MOZ_ALWAYS_TRUE(MergeSort(fields.begin(), fieldsLen, fields.begin() + fieldsLen, + [](const Field& left, const Field& right, + bool* lessOrEqual) + { + // Sort first by begin index, then to place + // enclosing fields before nested fields. + *lessOrEqual = left.begin < right.begin || + (left.begin == right.begin && + left.end > right.end); + return true; + })); + + // Deallocate the scratch space. + if (!fields.resize(fieldsLen)) + return false; + + // Third, iterate over the sorted field list to generate a sequence of + // parts (what ECMA-402 actually exposes). A part is a maximal character + // sequence entirely within no field or a single most-nested field. + // + // Diagrams may be helpful to illustrate how fields map to parts. Consider + // formatting -28,114,774,228,750.32, the US national surplus (negative + // because it's actually a debt) on March 31, 2021. + // + // var options = + // { style: "currency", currency: "USD", currencyDisplay: "name" }; + // var usdFormatter = new Intl.NumberFormat("en-US", options); + // usdFormatter.format(-28114774228750.32); + // + // The formatted result is "-28,114,774,228,750.32 US dollars". ICU + // identifies these fields in the string: + // + // UNUM_GROUPING_SEPARATOR_FIELD + // | + // UNUM_SIGN_FIELD | UNUM_DECIMAL_SEPARATOR_FIELD + // | __________/| | + // | / | | | | + // "-28,114,774,228,750.32 US dollars" + // \________________/ |/ \_______/ + // | | | + // UNUM_INTEGER_FIELD | UNUM_CURRENCY_FIELD + // | + // UNUM_FRACTION_FIELD + // + // These fields map to parts as follows: + // + // integer decimal + // _____|________ | + // / /| |\ |\ |\ | literal + // /| / | | \ | \ | \| | + // "-28,114,774,228,750.32 US dollars" + // | \___|___|___/ |/ \________/ + // | | | | + // | group | currency + // | | + // minusSign fraction + // + // The sign is a part. Each comma is a part, splitting the integer field + // into parts for trillions/billions/&c. digits. The decimal point is a + // part. Cents are a part. The space between cents and currency is a part + // (outside any field). Last, the currency field is a part. + // + // Because parts fully partition the formatted string, we only track the + // end of each part -- the beginning is implicitly the last part's end. + struct Part + { + uint32_t end; + FieldType type; + }; + + class PartGenerator + { + // The fields in order from start to end, then least to most nested. + const FieldsVector& fields; + + // Index of the current field, in |fields|, being considered to + // determine part boundaries. |lastEnd <= fields[index].begin| is an + // invariant. + size_t index; + + // The end index of the last part produced, always less than or equal + // to |limit|, strictly increasing. + uint32_t lastEnd; + + // The length of the overall formatted string. + const uint32_t limit; + + Vector enclosingFields; + + void popEnclosingFieldsEndingAt(uint32_t end) { + MOZ_ASSERT_IF(enclosingFields.length() > 0, + fields[enclosingFields.back()].end >= end); + + while (enclosingFields.length() > 0 && fields[enclosingFields.back()].end == end) + enclosingFields.popBack(); + } + + bool nextPartInternal(Part* part) { + size_t len = fields.length(); + MOZ_ASSERT(index <= len); + + // If we're out of fields, all that remains are part(s) consisting + // of trailing portions of enclosing fields, and maybe a final + // literal part. + if (index == len) { + if (enclosingFields.length() > 0) { + const auto& enclosing = fields[enclosingFields.popCopy()]; + part->end = enclosing.end; + part->type = enclosing.type; + + // If additional enclosing fields end where this part ends, + // pop them as well. + popEnclosingFieldsEndingAt(part->end); + } else { + part->end = limit; + part->type = &JSAtomState::literal; + } + + return true; + } + + // Otherwise we still have a field to process. + const Field* current = &fields[index]; + MOZ_ASSERT(lastEnd <= current->begin); + MOZ_ASSERT(current->begin < current->end); + + // But first, deal with inter-field space. + if (lastEnd < current->begin) { + if (enclosingFields.length() > 0) { + // Space between fields, within an enclosing field, is part + // of that enclosing field, until the start of the current + // field or the end of the enclosing field, whichever is + // earlier. + const auto& enclosing = fields[enclosingFields.back()]; + part->end = std::min(enclosing.end, current->begin); + part->type = enclosing.type; + popEnclosingFieldsEndingAt(part->end); + } else { + // If there's no enclosing field, the space is a literal. + part->end = current->begin; + part->type = &JSAtomState::literal; + } + + return true; + } + + // Otherwise, the part spans a prefix of the current field. Find + // the most-nested field containing that prefix. + const Field* next; + do { + current = &fields[index]; + + // If the current field is last, the part extends to its end. + if (++index == len) { + part->end = current->end; + part->type = current->type; + return true; + } + + next = &fields[index]; + MOZ_ASSERT(current->begin <= next->begin); + MOZ_ASSERT(current->begin < next->end); + + // If the next field nests within the current field, push an + // enclosing field. (If there are no nested fields, don't + // bother pushing a field that'd be immediately popped.) + if (current->end > next->begin) { + if (!enclosingFields.append(index - 1)) + return false; + } + + // Do so until the next field begins after this one. + } while (current->begin == next->begin); + + part->type = current->type; + + if (current->end <= next->begin) { + // The next field begins after the current field ends. Therefore + // the current part ends at the end of the current field. + part->end = current->end; + popEnclosingFieldsEndingAt(part->end); + } else { + // The current field encloses the next one. The current part + // ends where the next field/part will start. + part->end = next->begin; + } + + return true; + } + + public: + PartGenerator(JSContext* cx, const FieldsVector& vec, uint32_t limit) + : fields(vec), index(0), lastEnd(0), limit(limit), enclosingFields(cx) + {} + + bool nextPart(bool* hasPart, Part* part) { + // There are no parts left if we've partitioned the entire string. + if (lastEnd == limit) { + MOZ_ASSERT(enclosingFields.length() == 0); + *hasPart = false; + return true; + } + + if (!nextPartInternal(part)) + return false; + + *hasPart = true; + lastEnd = part->end; + return true; + } + }; + + // Finally, generate the result array. + size_t lastEndIndex = 0; + uint32_t partIndex = 0; + RootedObject singlePart(cx); + RootedValue propVal(cx); + + PartGenerator gen(cx, fields, overallResult->length()); + do { + bool hasPart; + Part part; + if (!gen.nextPart(&hasPart, &part)) + return false; + + if (!hasPart) + break; + + FieldType type = part.type; + size_t endIndex = part.end; + + MOZ_ASSERT(lastEndIndex < endIndex); + + singlePart = NewBuiltinClassInstance(cx); + if (!singlePart) + return false; + + propVal.setString(cx->names().*type); + if (!DefineProperty(cx, singlePart, cx->names().type, propVal)) + return false; + + JSLinearString* partSubstr = + NewDependentString(cx, overallResult, lastEndIndex, endIndex - lastEndIndex); + if (!partSubstr) + return false; + + propVal.setString(partSubstr); + if (!DefineProperty(cx, singlePart, cx->names().value, propVal)) + return false; + + propVal.setObject(*singlePart); + if (!DefineElement(cx, partsArray, partIndex, propVal)) + return false; + + lastEndIndex = endIndex; + partIndex++; + } while (true); + + MOZ_ASSERT(lastEndIndex == chars.length(), + "result array must partition the entire string"); + + result.setObject(*partsArray); + return true; +} + +bool +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[2].isBoolean()); + + RootedObject numberFormat(cx, &args[0].toObject()); + + // Obtain a UNumberFormat object, cached if possible. + bool isNumberFormatInstance = numberFormat->getClass() == &NumberFormatObject::class_; + UNumberFormat* nf; + if (isNumberFormatInstance) { + void* priv = + numberFormat->as().getReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT).toPrivate(); + nf = static_cast(priv); + if (!nf) { + nf = NewUNumberFormat(cx, numberFormat); + if (!nf) + return false; + numberFormat->as().setReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT, PrivateValue(nf)); + } + } else { + // There's no good place to cache the ICU number format for an object + // that has been initialized as a NumberFormat but is not a + // NumberFormat instance. One possibility might be to add a + // NumberFormat instance as an internal property to each such object. + nf = NewUNumberFormat(cx, numberFormat); + if (!nf) + return false; + } + + // Use the UNumberFormat to actually format the number. + double d = args[1].toNumber(); + RootedValue result(cx); + + bool success; + if (args[2].toBoolean()) { + success = intl_FormatNumberToParts(cx, nf, d, &result); + } else { + MOZ_ASSERT(!args[2].toBoolean(), + "shouldn't be doing formatToParts without an ICU that " + "supports it"); + success = js::intl_FormatNumber(cx, nf, d, &result); + } + + if (!isNumberFormatInstance) + unum_close(nf); + if (!success) + return false; + args.rval().set(result); + return true; +} + diff --git a/js/src/builtin/intl/NumberFormat.h b/js/src/builtin/intl/NumberFormat.h new file mode 100644 index 0000000000..37cf943972 --- /dev/null +++ b/js/src/builtin/intl/NumberFormat.h @@ -0,0 +1,89 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * 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/. */ + +#ifndef builtin_intl_NumberFormat_h +#define builtin_intl_NumberFormat_h + +#include "mozilla/Attributes.h" + +#include + +#include "unicode/unum.h" // for UNumberFormat +#include "builtin/SelfHostingDefines.h" +#include "js/Class.h" +#include "vm/NativeObject.h" + +namespace js { + +class FreeOp; + +class NumberFormatObject : public NativeObject +{ + public: + static const Class class_; + + static constexpr uint32_t INTERNALS_SLOT = 0; + static constexpr uint32_t UNUMBER_FORMAT_SLOT = 1; + static constexpr uint32_t SLOT_COUNT = 2; + + static_assert(INTERNALS_SLOT == INTL_INTERNALS_OBJECT_SLOT, + "INTERNALS_SLOT must match self-hosting define for internals object slot"); + private: + static const ClassOps classOps_; + + static void finalize(FreeOp* fop, JSObject* obj); +}; + +extern JSObject* +CreateNumberFormatPrototype(JSContext* cx, HandleObject Intl, Handle global); + +/** + * Returns a new instance of the standard built-in NumberFormat constructor. + * Self-hosted code cannot cache this constructor (as it does for others in + * Utilities.js) because it is initialized after self-hosted code is compiled. + * + * Usage: numberFormat = intl_NumberFormat(locales, options) + */ +extern MOZ_MUST_USE bool +intl_NumberFormat(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns an object indicating the supported locales for number formatting + * by having a true-valued property for each such locale with the + * canonicalized language tag as the property name. The object has no + * prototype. + * + * Usage: availableLocales = intl_NumberFormat_availableLocales() + */ +extern MOZ_MUST_USE bool +intl_NumberFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns the numbering system type identifier per Unicode + * Technical Standard 35, Unicode Locale Data Markup Language, for the + * default numbering system for the given locale. + * + * Usage: defaultNumberingSystem = intl_numberingSystem(locale) + */ +extern MOZ_MUST_USE bool +intl_numberingSystem(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns a string representing the number x according to the effective + * locale and the formatting options of the given NumberFormat. + * + * Spec: ECMAScript Internationalization API Specification, 11.3.2. + * + * Usage: formatted = intl_FormatNumber(numberFormat, x) + */ +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); + +} // namespace js + +#endif /* builtin_intl_NumberFormat_h */ \ No newline at end of file diff --git a/js/src/moz.build b/js/src/moz.build index 165ccd2d0c..a741ed521e 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -116,6 +116,7 @@ UNIFIED_SOURCES += [ 'builtin/Eval.cpp', 'builtin/Intl.cpp', 'builtin/intl/CommonFunctions.cpp', + 'builtin/intl/NumberFormat.cpp', 'builtin/MapObject.cpp', 'builtin/ModuleObject.cpp', 'builtin/Object.cpp', diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index 48b2da7b2c..d4c8395aaa 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -23,6 +23,7 @@ #include "selfhosted.out.h" #include "builtin/Intl.h" +#include "builtin/intl/NumberFormat.h" #include "builtin/MapObject.h" #include "builtin/ModuleObject.h" #include "builtin/Object.h"