From 26c8fa8110e418625f93d9899e8dc9d49c07f621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bargull?= Date: Mon, 23 Jan 2017 08:33:43 -0800 Subject: [PATCH] Bug 1328386 - Part 6: Implement legacy constructor semantics for Intl.NumberFormat per ECMA-402, 4th edition. r=Waldo --- js/public/Class.h | 2 +- js/src/builtin/Intl.cpp | 162 ++++++------- js/src/builtin/Intl.js | 77 +++++-- js/src/jit/InlinableNatives.h | 1 + js/src/jit/MCallOptimize.cpp | 2 + js/src/tests/Intl/NumberFormat/call.js | 167 ++++++++++++++ js/src/tests/Intl/NumberFormat/unwrapping.js | 226 +++++++++++++++++++ js/src/tests/jstests.list | 1 + js/src/vm/GlobalObject.h | 7 + js/src/vm/SelfHosting.cpp | 23 ++ 10 files changed, 554 insertions(+), 114 deletions(-) create mode 100644 js/src/tests/Intl/NumberFormat/call.js create mode 100644 js/src/tests/Intl/NumberFormat/unwrapping.js diff --git a/js/public/Class.h b/js/public/Class.h index 12b94cecaa30..acabe1deb239 100644 --- a/js/public/Class.h +++ b/js/public/Class.h @@ -852,7 +852,7 @@ struct JSClass { // application. #define JSCLASS_GLOBAL_APPLICATION_SLOTS 5 #define JSCLASS_GLOBAL_SLOT_COUNT \ - (JSCLASS_GLOBAL_APPLICATION_SLOTS + JSProto_LIMIT * 2 + 39) + (JSCLASS_GLOBAL_APPLICATION_SLOTS + JSProto_LIMIT * 2 + 40) #define JSCLASS_GLOBAL_FLAGS_WITH_SLOTS(n) \ (JSCLASS_IS_GLOBAL | JSCLASS_HAS_RESERVED_SLOTS(JSCLASS_GLOBAL_SLOT_COUNT + (n))) #define JSCLASS_GLOBAL_FLAGS \ diff --git a/js/src/builtin/Intl.cpp b/js/src/builtin/Intl.cpp index 2c5bc0030873..4f464af0c7ff 100644 --- a/js/src/builtin/Intl.cpp +++ b/js/src/builtin/Intl.cpp @@ -806,7 +806,32 @@ IntlInitialize(JSContext* cx, HandleObject obj, Handle initialize RootedValue thisv(cx, NullValue()); RootedValue ignored(cx); - return js::CallSelfHostedFunction(cx, initializer, thisv, args, &ignored); + if (!js::CallSelfHostedFunction(cx, initializer, thisv, args, &ignored)) + return false; + + MOZ_ASSERT(ignored.isUndefined(), + "Unexpected return value from non-legacy Intl object initializer"); + return true; +} + +static bool +LegacyIntlInitialize(JSContext* cx, HandleObject obj, Handle initializer, + HandleValue thisValue, HandleValue locales, HandleValue options, + MutableHandleValue result) +{ + FixedInvokeArgs<4> args(cx); + + args[0].setObject(*obj); + args[1].set(thisValue); + args[2].set(locales); + args[3].set(options); + + RootedValue thisv(cx, NullValue()); + if (!js::CallSelfHostedFunction(cx, initializer, thisv, args, result)) + return false; + + MOZ_ASSERT(result.isObject(), "Legacy Intl object initializer must return an object"); + return true; } static bool @@ -1426,64 +1451,33 @@ static const JSPropertySpec numberFormat_properties[] = { static bool NumberFormat(JSContext* cx, const CallArgs& args, bool construct) { - RootedObject obj(cx); + // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code). - // 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) + // 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; - 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; + Rooted numberFormat(cx); + numberFormat = NewObjectWithGivenProto(cx, proto); + if (!numberFormat) + 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::UNUMBER_FORMAT_SLOT, - PrivateValue(nullptr)); - } + numberFormat->setReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT, PrivateValue(nullptr)); + RootedValue thisValue(cx, construct ? ObjectValue(*numberFormat) : args.thisv()); RootedValue locales(cx, args.get(0)); RootedValue options(cx, args.get(1)); // Step 3. - if (!IntlInitialize(cx, obj, cx->names().InitializeNumberFormat, locales, options)) - return false; - - args.rval().setObject(*obj); - return true; + return LegacyIntlInitialize(cx, numberFormat, cx->names().InitializeNumberFormat, thisValue, + locales, options, args.rval()); } static bool @@ -1523,7 +1517,8 @@ NumberFormatObject::finalize(FreeOp* fop, JSObject* obj) } static JSObject* -CreateNumberFormatPrototype(JSContext* cx, HandleObject Intl, Handle global) +CreateNumberFormatPrototype(JSContext* cx, HandleObject Intl, Handle global, + MutableHandleObject constructor) { RootedFunction ctor(cx); ctor = GlobalObject::createConstructor(cx, &NumberFormat, cx->names().NumberFormat, 0); @@ -1574,17 +1569,20 @@ CreateNumberFormatPrototype(JSContext* cx, HandleObject Intl, Handlenames().InitializeNumberFormat, UndefinedHandleValue, - options)) + RootedValue thisOrResult(cx, ObjectValue(*proto)); + if (!LegacyIntlInitialize(cx, proto, cx->names().InitializeNumberFormat, thisOrResult, + UndefinedHandleValue, options, &thisOrResult)) { return nullptr; } + MOZ_ASSERT(&thisOrResult.toObject() == proto); // 8.1 RootedValue ctorValue(cx, ObjectValue(*ctor)); if (!DefineProperty(cx, Intl, cx->names().NumberFormat, ctorValue, nullptr, nullptr, 0)) return nullptr; + constructor.set(ctor); return proto; } @@ -1713,7 +1711,7 @@ NewUNumberFormatForPluralRules(JSContext* cx, Handle pluralR * of the given NumberFormat. */ static UNumberFormat* -NewUNumberFormat(JSContext* cx, HandleObject numberFormat) +NewUNumberFormat(JSContext* cx, Handle numberFormat) { RootedValue value(cx); @@ -2326,56 +2324,30 @@ js::intl_FormatNumber(JSContext* cx, unsigned argc, Value* vp) MOZ_ASSERT(args[1].isNumber()); MOZ_ASSERT(args[2].isBoolean()); - RootedObject numberFormat(cx, &args[0].toObject()); + Rooted numberFormat(cx, &args[0].toObject().as()); - // Obtain a UNumberFormat object, cached if possible. - bool isNumberFormatInstance = numberFormat->is(); - 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. + // Obtain a cached UNumberFormat object. + void* priv = + numberFormat->getReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT).toPrivate(); + UNumberFormat* nf = static_cast(priv); + if (!nf) { nf = NewUNumberFormat(cx, numberFormat); if (!nf) return false; + numberFormat->setReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT, PrivateValue(nf)); } // Use the UNumberFormat to actually format the number. - double d = args[1].toNumber(); - RootedValue result(cx); - - bool success; #if defined(ICU_UNUM_HAS_FORMATDOUBLEFORFIELDS) if (args[2].toBoolean()) { - success = intl_FormatNumberToParts(cx, nf, d, &result); - } else -#endif // defined(ICU_UNUM_HAS_FORMATDOUBLEFORFIELDS) - { - MOZ_ASSERT(!args[2].toBoolean(), - "shouldn't be doing formatToParts without an ICU that " - "supports it"); - success = intl_FormatNumber(cx, nf, d, &result); + return intl_FormatNumberToParts(cx, nf, args[1].toNumber(), args.rval()); } - - if (!isNumberFormatInstance) - unum_close(nf); - if (!success) - return false; - args.rval().set(result); - return true; +#else + MOZ_ASSERT(!args[2].toBoolean(), + "shouldn't be doing formatToParts without an ICU that " + "supports it"); +#endif // defined(ICU_UNUM_HAS_FORMATDOUBLEFORFIELDS) + return intl_FormatNumber(cx, nf, args[1].toNumber(), args.rval()); } @@ -4375,7 +4347,8 @@ GlobalObject::initIntlObject(JSContext* cx, Handle global) RootedObject dateTimeFormatProto(cx, CreateDateTimeFormatPrototype(cx, intl, global)); if (!dateTimeFormatProto) return false; - RootedObject numberFormatProto(cx, CreateNumberFormatPrototype(cx, intl, global)); + RootedObject numberFormatProto(cx), numberFormat(cx); + numberFormatProto = CreateNumberFormatPrototype(cx, intl, global, &numberFormat); if (!numberFormatProto) return false; @@ -4398,6 +4371,7 @@ GlobalObject::initIntlObject(JSContext* cx, Handle global) // baggage we don't need or want, so we use one-off reserved slots. global->setReservedSlot(COLLATOR_PROTO, ObjectValue(*collatorProto)); global->setReservedSlot(DATE_TIME_FORMAT_PROTO, ObjectValue(*dateTimeFormatProto)); + global->setReservedSlot(NUMBER_FORMAT, ObjectValue(*numberFormat)); global->setReservedSlot(NUMBER_FORMAT_PROTO, ObjectValue(*numberFormatProto)); // Also cache |Intl| to implement spec language that conditions behavior diff --git a/js/src/builtin/Intl.js b/js/src/builtin/Intl.js index ce87338511f0..ced73930dbc1 100644 --- a/js/src/builtin/Intl.js +++ b/js/src/builtin/Intl.js @@ -1852,10 +1852,12 @@ function resolveNumberFormatInternals(lazyNumberFormatData) { /** - * Returns an object containing the NumberFormat internal properties of |obj|, - * or throws a TypeError if |obj| isn't NumberFormat-initialized. + * Returns an object containing the NumberFormat internal properties of |obj|. */ function getNumberFormatInternals(obj, methodName) { + assert(IsObject(obj), "getNumberFormatInternals called with non-object"); + assert(IsNumberFormat(obj), "getNumberFormatInternals called with non-NumberFormat"); + var internals = getIntlObjectInternals(obj, "NumberFormat", methodName); assert(internals.type === "NumberFormat", "bad type escaped getIntlObjectInternals"); @@ -1870,6 +1872,25 @@ function getNumberFormatInternals(obj, methodName) { return internalProps; } + +/** + * UnwrapNumberFormat(nf) + */ +function UnwrapNumberFormat(nf, methodName) { + // Step 1. + if ((!IsObject(nf) || !IsNumberFormat(nf)) && nf instanceof GetNumberFormatConstructor()) { + nf = nf[intlFallbackSymbol()]; + } + + // Step 2. + if (!IsObject(nf) || !IsNumberFormat(nf)) + ThrowTypeError(JSMSG_INTL_OBJECT_NOT_INITED, "NumberFormat", methodName, "NumberFormat"); + + // Step 3. + return nf; +} + + /** * Applies digit options used for number formatting onto the intl object. * @@ -1920,12 +1941,13 @@ function SetNumberFormatDigitOptions(lazyData, options, mnfdDefault, mxfdDefault * * Spec: ECMAScript Internationalization API Specification, 11.1.1. */ -function InitializeNumberFormat(numberFormat, locales, options) { - assert(IsObject(numberFormat), "InitializeNumberFormat"); +function InitializeNumberFormat(numberFormat, thisValue, locales, options) { + assert(IsObject(numberFormat), "InitializeNumberFormat called with non-object"); + assert(IsNumberFormat(numberFormat), "InitializeNumberFormat called with non-NumberFormat"); - // Step 1. - if (isInitializedIntlObject(numberFormat)) - ThrowTypeError(JSMSG_INTL_OBJECT_REINITED); + // Steps 1-2 (These steps are no longer required and should be removed + // from the spec; https://github.com/tc39/ecma402/issues/115). + assert(!isInitializedIntlObject(numberFormat), "numberFormat mustn't be initialized"); // Step 2. var internals = initializeIntlObject(numberFormat); @@ -2031,6 +2053,18 @@ function InitializeNumberFormat(numberFormat, locales, options) { // We've done everything that must be done now: mark the lazy data as fully // computed and install it. setLazyData(internals, "NumberFormat", lazyNumberFormatData); + + if (numberFormat !== thisValue && thisValue instanceof GetNumberFormatConstructor()) { + if (!IsObject(thisValue)) + ThrowTypeError(JSMSG_NOT_NONNULL_OBJECT, typeof thisValue); + + _DefineDataProperty(thisValue, intlFallbackSymbol(), numberFormat, + ATTR_NONENUMERABLE | ATTR_NONCONFIGURABLE | ATTR_NONWRITABLE); + + return thisValue; + } + + return numberFormat; } @@ -2127,29 +2161,32 @@ function numberFormatFormatToBind(value) { * Spec: ECMAScript Internationalization API Specification, 11.3.2. */ function Intl_NumberFormat_format_get() { - // Check "this NumberFormat object" per introduction of section 11.3. - var internals = getNumberFormatInternals(this, "format"); + // Steps 1-3. + var nf = UnwrapNumberFormat(this, "format"); - // Step 1. + var internals = getNumberFormatInternals(nf, "format"); + + // Step 4. if (internals.boundFormat === undefined) { - // Step 1.a. + // Step 4.a. var F = numberFormatFormatToBind; - // Step 1.b-d. - var bf = callFunction(FunctionBind, F, this); + // Steps 4.b-d. + var bf = callFunction(FunctionBind, F, nf); internals.boundFormat = bf; } - // Step 2. + + // Step 5. return internals.boundFormat; } _SetCanonicalName(Intl_NumberFormat_format_get, "get format"); function Intl_NumberFormat_formatToParts(value) { - // Step 1. - var nf = this; + // Steps 1-3. + var nf = UnwrapNumberFormat(this, "formatToParts"); - // Steps 2-3. + // Ensure the NumberFormat internals are resolved. getNumberFormatInternals(nf, "formatToParts"); // Step 4. @@ -2166,8 +2203,10 @@ function Intl_NumberFormat_formatToParts(value) { * Spec: ECMAScript Internationalization API Specification, 11.3.3 and 11.4. */ function Intl_NumberFormat_resolvedOptions() { - // Check "this NumberFormat object" per introduction of section 11.3. - var internals = getNumberFormatInternals(this, "resolvedOptions"); + // Invoke |UnwrapNumberFormat| per introduction of section 11.3. + var nf = UnwrapNumberFormat(this, "resolvedOptions"); + + var internals = getNumberFormatInternals(nf, "resolvedOptions"); var result = { locale: internals.locale, diff --git a/js/src/jit/InlinableNatives.h b/js/src/jit/InlinableNatives.h index 3ca5d568425c..c09fb8e15ed3 100644 --- a/js/src/jit/InlinableNatives.h +++ b/js/src/jit/InlinableNatives.h @@ -29,6 +29,7 @@ _(AtomicsIsLockFree) \ \ _(IntlIsCollator) \ + _(IntlIsNumberFormat) \ _(IntlIsPluralRules) \ \ _(MathAbs) \ diff --git a/js/src/jit/MCallOptimize.cpp b/js/src/jit/MCallOptimize.cpp index c81af004c28b..ee34586bafbc 100644 --- a/js/src/jit/MCallOptimize.cpp +++ b/js/src/jit/MCallOptimize.cpp @@ -111,6 +111,8 @@ IonBuilder::inlineNativeCall(CallInfo& callInfo, JSFunction* target) // Intl natives. case InlinableNative::IntlIsCollator: return inlineHasClass(callInfo, &CollatorObject::class_); + case InlinableNative::IntlIsNumberFormat: + return inlineHasClass(callInfo, &NumberFormatObject::class_); case InlinableNative::IntlIsPluralRules: return inlineHasClass(callInfo, &PluralRulesObject::class_); diff --git a/js/src/tests/Intl/NumberFormat/call.js b/js/src/tests/Intl/NumberFormat/call.js new file mode 100644 index 000000000000..0387ecc0d1cd --- /dev/null +++ b/js/src/tests/Intl/NumberFormat/call.js @@ -0,0 +1,167 @@ +// |reftest| skip-if(!this.hasOwnProperty("Intl")) + +function IsConstructor(o) { + try { + new (new Proxy(o, {construct: () => ({})})); + return true; + } catch (e) { + return false; + } +} + +function IsObject(o) { + return Object(o) === o; +} + +function thisValues() { + const intlConstructors = Object.getOwnPropertyNames(Intl).map(name => Intl[name]).filter(IsConstructor); + + return [ + // Primitive values. + ...[undefined, null, true, "abc", Symbol(), 123], + + // Object values. + ...[{}, [], /(?:)/, function(){}, new Proxy({}, {})], + + // Intl objects. + ...[].concat(...intlConstructors.map(ctor => [ + // Instance of an Intl constructor. + new ctor(), + + // Instance of a subclassed Intl constructor. + new class extends ctor {}, + + // Object inheriting from an Intl constructor prototype. + Object.create(ctor.prototype), + + // Intl object not inheriting from its default prototype. + Object.setPrototypeOf(new ctor(), Object.prototype), + ])), + ]; +} + +const intlFallbackSymbol = Object.getOwnPropertySymbols(Intl.NumberFormat.call(Object.create(Intl.NumberFormat.prototype)))[0]; + +// Invoking [[Call]] for Intl.NumberFormat returns a new instance unless called +// with an instance inheriting from Intl.NumberFormat.prototype. +for (let thisValue of thisValues()) { + let obj = Intl.NumberFormat.call(thisValue); + + if (!Intl.NumberFormat.prototype.isPrototypeOf(thisValue)) { + assertEq(Object.is(obj, thisValue), false); + assertEq(obj instanceof Intl.NumberFormat, true); + if (IsObject(thisValue)) + assertEqArray(Object.getOwnPropertySymbols(thisValue), []); + } else { + assertEq(Object.is(obj, thisValue), true); + assertEq(obj instanceof Intl.NumberFormat, true); + assertEqArray(Object.getOwnPropertySymbols(thisValue), [intlFallbackSymbol]); + } +} + +// Intl.NumberFormat uses the legacy Intl constructor compromise semantics. +// - Test when InstanceofOperator(thisValue, %NumberFormat%) returns true. +for (let thisValue of thisValues()) { + let hasInstanceCalled = false; + Object.defineProperty(Intl.NumberFormat, Symbol.hasInstance, { + value() { + assertEq(hasInstanceCalled, false); + hasInstanceCalled = true; + return true; + }, configurable: true + }); + if (!IsObject(thisValue)) { + // A TypeError is thrown when Intl.NumberFormat tries to install the + // [[FallbackSymbol]] property on |thisValue|. + assertThrowsInstanceOf(() => Intl.NumberFormat.call(thisValue), TypeError); + delete Intl.NumberFormat[Symbol.hasInstance]; + } else { + let obj = Intl.NumberFormat.call(thisValue); + delete Intl.NumberFormat[Symbol.hasInstance]; + assertEq(Object.is(obj, thisValue), true); + assertEqArray(Object.getOwnPropertySymbols(thisValue), [intlFallbackSymbol]); + } + assertEq(hasInstanceCalled, true); +} +// - Test when InstanceofOperator(thisValue, %NumberFormat%) returns false. +for (let thisValue of thisValues()) { + let hasInstanceCalled = false; + Object.defineProperty(Intl.NumberFormat, Symbol.hasInstance, { + value() { + assertEq(hasInstanceCalled, false); + hasInstanceCalled = true; + return false; + }, configurable: true + }); + let obj = Intl.NumberFormat.call(thisValue); + delete Intl.NumberFormat[Symbol.hasInstance]; + assertEq(Object.is(obj, thisValue), false); + assertEq(obj instanceof Intl.NumberFormat, true); + if (IsObject(thisValue)) + assertEqArray(Object.getOwnPropertySymbols(thisValue), []); + assertEq(hasInstanceCalled, true); +} + +// Throws an error when attempting to install [[FallbackSymbol]] twice. +{ + let thisValue = Object.create(Intl.NumberFormat.prototype); + assertEqArray(Object.getOwnPropertySymbols(thisValue), []); + + assertEq(Intl.NumberFormat.call(thisValue), thisValue); + assertEqArray(Object.getOwnPropertySymbols(thisValue), [intlFallbackSymbol]); + + assertThrowsInstanceOf(() => Intl.NumberFormat.call(thisValue), TypeError); + assertEqArray(Object.getOwnPropertySymbols(thisValue), [intlFallbackSymbol]); +} + +// Throws an error when the thisValue is non-extensible. +{ + let thisValue = Object.create(Intl.NumberFormat.prototype); + Object.preventExtensions(thisValue); + + assertThrowsInstanceOf(() => Intl.NumberFormat.call(thisValue), TypeError); + assertEqArray(Object.getOwnPropertySymbols(thisValue), []); +} + +// [[FallbackSymbol]] is installed as a frozen property holding an Intl.NumberFormat instance. +{ + let thisValue = Object.create(Intl.NumberFormat.prototype); + Intl.NumberFormat.call(thisValue); + + let desc = Object.getOwnPropertyDescriptor(thisValue, intlFallbackSymbol); + assertEq(desc !== undefined, true); + assertEq(desc.writable, false); + assertEq(desc.enumerable, false); + assertEq(desc.configurable, false); + assertEq(desc.value instanceof Intl.NumberFormat, true); +} + +// Ensure [[FallbackSymbol]] is installed last by changing the [[Prototype]] +// during initialization. +{ + let thisValue = {}; + let options = { + get useGrouping() { + Object.setPrototypeOf(thisValue, Intl.NumberFormat.prototype); + return false; + } + }; + let obj = Intl.NumberFormat.call(thisValue, undefined, options); + assertEq(Object.is(obj, thisValue), true); + assertEqArray(Object.getOwnPropertySymbols(thisValue), [intlFallbackSymbol]); +} +{ + let thisValue = Object.create(Intl.NumberFormat.prototype); + let options = { + get useGrouping() { + Object.setPrototypeOf(thisValue, Object.prototype); + return false; + } + }; + let obj = Intl.NumberFormat.call(thisValue, undefined, options); + assertEq(Object.is(obj, thisValue), false); + assertEqArray(Object.getOwnPropertySymbols(thisValue), []); +} + +if (typeof reportCompare === "function") + reportCompare(true, true); diff --git a/js/src/tests/Intl/NumberFormat/unwrapping.js b/js/src/tests/Intl/NumberFormat/unwrapping.js new file mode 100644 index 000000000000..f12eb25c57ff --- /dev/null +++ b/js/src/tests/Intl/NumberFormat/unwrapping.js @@ -0,0 +1,226 @@ +// |reftest| skip-if(!this.hasOwnProperty("Intl")) + +// Test UnwrapNumberFormat operation. + +const numberFormatFunctions = []; +numberFormatFunctions.push(Intl.NumberFormat.prototype.resolvedOptions); +numberFormatFunctions.push(Object.getOwnPropertyDescriptor(Intl.NumberFormat.prototype, "format").get); +// "formatToParts" isn't yet enabled by default. +if ("formatToParts" in Intl.NumberFormat.prototype) + numberFormatFunctions.push(Intl.NumberFormat.prototype.formatToParts); + +function IsConstructor(o) { + try { + new (new Proxy(o, {construct: () => ({})})); + return true; + } catch (e) { + return false; + } +} + +function IsObject(o) { + return Object(o) === o; +} + +function intlObjects(ctor) { + return [ + // Instance of an Intl constructor. + new ctor(), + + // Instance of a subclassed Intl constructor. + new class extends ctor {}, + + // Intl object not inheriting from its default prototype. + Object.setPrototypeOf(new ctor(), Object.prototype), + ]; +} + +function thisValues(C) { + const intlConstructors = Object.getOwnPropertyNames(Intl).map(name => Intl[name]).filter(IsConstructor); + + return [ + // Primitive values. + ...[undefined, null, true, "abc", Symbol(), 123], + + // Object values. + ...[{}, [], /(?:)/, function(){}, new Proxy({}, {})], + + // Intl objects. + ...[].concat(...intlConstructors.filter(ctor => ctor !== C).map(intlObjects)), + + // Object inheriting from an Intl constructor prototype. + ...intlConstructors.map(ctor => Object.create(ctor.prototype)), + ]; +} + +const intlFallbackSymbol = Object.getOwnPropertySymbols(Intl.NumberFormat.call(Object.create(Intl.NumberFormat.prototype)))[0]; + +// Test Intl.NumberFormat.prototype methods. +for (let numberFormatFunction of numberFormatFunctions) { + // Test a TypeError is thrown when the this-value isn't an initialized + // Intl.NumberFormat instance. + for (let thisValue of thisValues(Intl.NumberFormat)) { + assertThrowsInstanceOf(() => numberFormatFunction.call(thisValue), TypeError); + } + + // And test no error is thrown for initialized Intl.NumberFormat instances. + for (let thisValue of intlObjects(Intl.NumberFormat)) { + numberFormatFunction.call(thisValue); + } + + // Manually add [[FallbackSymbol]] to objects and then repeat the tests from above. + for (let thisValue of thisValues(Intl.NumberFormat)) { + assertThrowsInstanceOf(() => numberFormatFunction.call({ + __proto__: Intl.NumberFormat.prototype, + [intlFallbackSymbol]: thisValue, + }), TypeError); + } + + for (let thisValue of intlObjects(Intl.NumberFormat)) { + numberFormatFunction.call({ + __proto__: Intl.NumberFormat.prototype, + [intlFallbackSymbol]: thisValue, + }); + } + + // Ensure [[FallbackSymbol]] isn't retrieved for Intl.NumberFormat instances. + for (let thisValue of intlObjects(Intl.NumberFormat)) { + Object.defineProperty(thisValue, intlFallbackSymbol, { + get() { assertEq(false, true); } + }); + numberFormatFunction.call(thisValue); + } + + // Ensure [[FallbackSymbol]] is only retrieved for objects inheriting from Intl.NumberFormat.prototype. + for (let thisValue of thisValues(Intl.NumberFormat)) { + if (!IsObject(thisValue) || Intl.NumberFormat.prototype.isPrototypeOf(thisValue)) + continue; + Object.defineProperty(thisValue, intlFallbackSymbol, { + get() { assertEq(false, true); } + }); + assertThrowsInstanceOf(() => numberFormatFunction.call(thisValue), TypeError); + } + + // Repeat the test from above, but also change Intl.NumberFormat[@@hasInstance] + // so it always returns |null|. + for (let thisValue of thisValues(Intl.NumberFormat)) { + let hasInstanceCalled = false, symbolGetterCalled = false; + Object.defineProperty(Intl.NumberFormat, Symbol.hasInstance, { + value() { + assertEq(hasInstanceCalled, false); + hasInstanceCalled = true; + return true; + }, configurable: true + }); + let isUndefinedOrNull = thisValue !== undefined || thisValue !== null; + let symbolHolder; + if (!isUndefinedOrNull) { + symbolHolder = IsObject(thisValue) ? thisValue : Object.getPrototypeOf(thisValue); + Object.defineProperty(symbolHolder, intlFallbackSymbol, { + get() { + assertEq(symbolGetterCalled, false); + symbolGetterCalled = true; + return null; + }, configurable: true + }); + } + + assertThrowsInstanceOf(() => numberFormatFunction.call(thisValue), TypeError); + + delete Intl.NumberFormat[Symbol.hasInstance]; + if (!isUndefinedOrNull && !IsObject(thisValue)) + delete symbolHolder[intlFallbackSymbol]; + + assertEq(hasInstanceCalled, true); + assertEq(symbolGetterCalled, !isUndefinedOrNull); + } +} + +// Test format() returns the correct result for objects initialized as Intl.NumberFormat instances. +{ + // An actual Intl.NumberFormat instance. + let numberFormat = new Intl.NumberFormat(); + + // An object initialized as a NumberFormat instance. + let thisValue = Object.create(Intl.NumberFormat.prototype); + Intl.NumberFormat.call(thisValue); + + // Object with [[FallbackSymbol]] set to NumberFormat instance. + let fakeObj = { + __proto__: Intl.NumberFormat.prototype, + [intlFallbackSymbol]: numberFormat, + }; + + for (let number of [0, 1, 1.5, Infinity, NaN]) { + let expected = numberFormat.format(number); + assertEq(thisValue.format(number), expected); + assertEq(thisValue[intlFallbackSymbol].format(number), expected); + assertEq(fakeObj.format(number), expected); + } +} + +// Test formatToParts() returns the correct result for objects initialized as Intl.NumberFormat instances. +if ("formatToParts" in Intl.NumberFormat.prototype) { + // An actual Intl.NumberFormat instance. + let numberFormat = new Intl.NumberFormat(); + + // An object initialized as a NumberFormat instance. + let thisValue = Object.create(Intl.NumberFormat.prototype); + Intl.NumberFormat.call(thisValue); + + // Object with [[FallbackSymbol]] set to NumberFormat instance. + let fakeObj = { + __proto__: Intl.NumberFormat.prototype, + [intlFallbackSymbol]: numberFormat, + }; + + function assertEqParts(actual, expected) { + assertEq(actual.length, expected.length, "parts count mismatch"); + for (var i = 0; i < expected.length; i++) { + assertEq(actual[i].type, expected[i].type, "type mismatch at " + i); + assertEq(actual[i].value, expected[i].value, "value mismatch at " + i); + } + } + + for (let number of [0, 1, 1.5, Infinity, NaN]) { + let expected = numberFormat.formatToParts(number); + assertEqParts(thisValue.formatToParts(number), expected); + assertEqParts(thisValue[intlFallbackSymbol].formatToParts(number), expected); + assertEqParts(fakeObj.formatToParts(number), expected); + } +} + +// Test resolvedOptions() returns the same results. +{ + // An actual Intl.NumberFormat instance. + let numberFormat = new Intl.NumberFormat(); + + // An object initialized as a NumberFormat instance. + let thisValue = Object.create(Intl.NumberFormat.prototype); + Intl.NumberFormat.call(thisValue); + + // Object with [[FallbackSymbol]] set to NumberFormat instance. + let fakeObj = { + __proto__: Intl.NumberFormat.prototype, + [intlFallbackSymbol]: numberFormat, + }; + + function assertEqOptions(actual, expected) { + actual = Object.entries(actual); + expected = Object.entries(expected); + + assertEq(actual.length, expected.length, "options count mismatch"); + for (var i = 0; i < expected.length; i++) { + assertEq(actual[i][0], expected[i][0], "key mismatch at " + i); + assertEq(actual[i][1], expected[i][1], "value mismatch at " + i); + } + } + + let expected = numberFormat.resolvedOptions(); + assertEqOptions(thisValue.resolvedOptions(), expected); + assertEqOptions(thisValue[intlFallbackSymbol].resolvedOptions(), expected); + assertEqOptions(fakeObj.resolvedOptions(), expected); +} + +if (typeof reportCompare === "function") + reportCompare(true, true); diff --git a/js/src/tests/jstests.list b/js/src/tests/jstests.list index 6153fad9e862..3254dfc6c89e 100644 --- a/js/src/tests/jstests.list +++ b/js/src/tests/jstests.list @@ -60,6 +60,7 @@ skip script test262/ch10/10.6/10.6-13-b-2-s.js # (bug 1328386). skip script test262/intl402/ch10/10.1/10.1.1_1.js skip script test262/intl402/ch10/10.1/10.1.2_a.js +skip script test262/intl402/ch11/11.1/11.1.1_1.js ####################################################################### # Tests disabled due to jstest limitations wrt imported test262 tests # diff --git a/js/src/vm/GlobalObject.h b/js/src/vm/GlobalObject.h index ef99d53093a2..222baf95a54d 100644 --- a/js/src/vm/GlobalObject.h +++ b/js/src/vm/GlobalObject.h @@ -101,6 +101,7 @@ class GlobalObject : public NativeObject MAP_ITERATOR_PROTO, SET_ITERATOR_PROTO, COLLATOR_PROTO, + NUMBER_FORMAT, NUMBER_FORMAT_PROTO, DATE_TIME_FORMAT_PROTO, PLURAL_RULES_PROTO, @@ -483,6 +484,12 @@ class GlobalObject : public NativeObject return getOrCreateObject(cx, global, COLLATOR_PROTO, initIntlObject); } + static JSFunction* + getOrCreateNumberFormatConstructor(JSContext* cx, Handle global) { + JSObject* obj = getOrCreateObject(cx, global, NUMBER_FORMAT, initIntlObject); + return obj ? &obj->as() : nullptr; + } + static JSObject* getOrCreateNumberFormatPrototype(JSContext* cx, Handle global) { return getOrCreateObject(cx, global, NUMBER_FORMAT_PROTO, initIntlObject); diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index a0c179d4f0c0..7060dc090735 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -1856,6 +1856,23 @@ intrinsic_RuntimeDefaultLocale(JSContext* cx, unsigned argc, Value* vp) return true; } +using GetOrCreateIntlConstructor = JSFunction* (*)(JSContext*, Handle); + +template +static bool +intrinsic_GetBuiltinIntlConstructor(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + JSFunction* constructor = getOrCreateIntlConstructor(cx, cx->global()); + if (!constructor) + return false; + + args.rval().setObject(*constructor); + return true; +} + static bool intrinsic_AddContentTelemetry(JSContext* cx, unsigned argc, Value* vp) { @@ -2485,9 +2502,15 @@ static const JSFunctionSpec intrinsic_functions[] = { JS_INLINABLE_FN("IsCollator", intrinsic_IsInstanceOfBuiltin, 1,0, IntlIsCollator), + JS_INLINABLE_FN("IsNumberFormat", + intrinsic_IsInstanceOfBuiltin, 1,0, + IntlIsNumberFormat), JS_INLINABLE_FN("IsPluralRules", intrinsic_IsInstanceOfBuiltin, 1,0, IntlIsPluralRules), + JS_FN("GetNumberFormatConstructor", + intrinsic_GetBuiltinIntlConstructor, + 0,0), JS_INLINABLE_FN("IsRegExpObject", intrinsic_IsInstanceOfBuiltin, 1,0,