Explorar el Código

Fix a number of test262 Intl issues

Jack Horton (CHAKRA) hace 7 años
padre
commit
ecbc1f2dac

+ 5 - 0
lib/Runtime/Base/JnDirectFields.h

@@ -518,6 +518,11 @@ ENTRY(tagPublicLibraryCode)
 ENTRY(winglob)
 ENTRY(platform)
 ENTRY(formatToParts)
+ENTRY(FallbackSymbol)
+
+// This symbol is not part of the regular Symbol API and is only used in rare circumstances in Intl.js for backwards compatibility
+// with the Intl v1 spec. It is visible to the user only using Object.getOwnPropertySymbols(Intl.NumberFormat.call(new Intl.NumberFormat())).
+ENTRY_SYMBOL(_intlFallbackSymbol, _u("Intl.FallbackSymbol"))
 
 ENTRY(NumberFormat)
 ENTRY(__currency)

+ 32 - 19
lib/Runtime/Library/EngineInterfaceObject.cpp

@@ -317,30 +317,43 @@ namespace Js
     {
         EngineInterfaceObject_CommonFunctionProlog(function, callInfo);
 
-        if (callInfo.Count >= 2 && JavascriptFunction::Is(args.Values[1]))
+        AssertOrFailFast((callInfo.Count == 3 || callInfo.Count == 4) && JavascriptFunction::Is(args[1]) && JavascriptString::Is(args[2]));
+
+        JavascriptFunction *func = JavascriptFunction::UnsafeFromVar(args[1]);
+        JavascriptString *methodName = JavascriptString::UnsafeFromVar(args[2]);
+
+        func->GetFunctionProxy()->SetIsPublicLibraryCode();
+
+        // use GetSz rather than GetString because we use wcsrchr below, which expects a null-terminated string
+        const char16 *methodNameBuf = methodName->GetSz();
+        charcount_t methodNameLength = methodName->GetLength();
+        const char16 *shortName = wcsrchr(methodNameBuf, _u('.'));
+        charcount_t shortNameOffset = 0;
+        if (shortName != nullptr)
         {
-            JavascriptFunction* func = JavascriptFunction::FromVar(args.Values[1]);
-            func->GetFunctionProxy()->SetIsPublicLibraryCode();
+            shortName++;
+            shortNameOffset = static_cast<charcount_t>(shortName - methodNameBuf);
+        }
 
-            if (callInfo.Count >= 3 && JavascriptString::Is(args.Values[2]))
-            {
-                JavascriptString* customFunctionName = JavascriptString::FromVar(args.Values[2]);
-                // tagPublicFunction("Intl.Collator", Collator); in Intl.js calls TagPublicLibraryCode the expected name is Collator so we need to calculate the offset
-                const char16 * shortName = wcsrchr(customFunctionName->GetString(), _u('.'));
-                uint shortNameOffset = 0;
-                if (shortName != nullptr)
-                {
-                    // JavascriptString length is bounded by uint max
-                    shortName++;
-                    shortNameOffset = static_cast<uint>(shortName - customFunctionName->GetString());
-                }
-                func->GetFunctionProxy()->EnsureDeserialized()->SetDisplayName(customFunctionName->GetString(), customFunctionName->GetLength(), shortNameOffset);
-            }
+        func->GetFunctionProxy()->EnsureDeserialized()->SetDisplayName(methodNameBuf, methodNameLength, shortNameOffset);
 
-            return func;
+        bool creatingConstructor = true;
+        if (callInfo.Count == 4)
+        {
+            AssertOrFailFast(JavascriptBoolean::Is(args[3]));
+            creatingConstructor = JavascriptBoolean::UnsafeFromVar(args[3])->GetValue();
         }
 
-        return scriptContext->GetLibrary()->GetUndefined();
+        if (!creatingConstructor)
+        {
+            FunctionInfo *info = func->GetFunctionInfo();
+            info->SetAttributes((FunctionInfo::Attributes) (info->GetAttributes() | FunctionInfo::Attributes::ErrorOnNew));
+
+            AssertOrFailFast(func->GetDynamicType()->GetTypeHandler()->IsDeferredTypeHandler());
+            DynamicTypeHandler::SetInstanceTypeHandler(func, scriptContext->GetLibrary()->GetDeferredFunctionWithLengthTypeHandler());
+        }
+
+        return func;
     }
 
     /*

+ 260 - 284
lib/Runtime/Library/InJavascript/Intl.js

@@ -237,6 +237,19 @@
         return platform.tagPublicLibraryCode(f, name);
     };
 
+    const createPublicMethod = function (name, f) {
+        return platform.tagPublicLibraryCode(f, name, false);
+    }
+
+    const OrdinaryCreateFromConstructor = function (constructor, intrinsicDefaultProto) {
+        let proto = constructor.prototype;
+        if (typeof proto !== "object") {
+            proto = intrinsicDefaultProto;
+        }
+
+        return _.create(proto);
+    };
+
     /**
      * Determines the best possible locale available in the system
      *
@@ -479,13 +492,30 @@
             return v !== undefined ? Boolean(v) : undefined;
         },
 
-        ToUint32(n) {
-            var num = Number(n),
-                ret = 0;
-            if (!isNaN(num) && isFinite(num)) {
-                ret = _.abs(num % _.pow(2, 32));
+        ToInteger(n) {
+            const number = Number(n);
+            if (isNaN(number)) {
+                return 0;
+            } else if (number === 0 || !isFinite(number)) {
+                return number;
             }
-            return ret;
+
+            const ret = _.floor(_.abs(number));
+            if (number < 0) {
+                return -ret
+            } else {
+                return ret;
+            }
+        },
+
+        ToLength(n) {
+            const len = Internal.ToInteger(n);
+            if (len <= 0) {
+                return 0;
+            }
+
+            const max = _.pow(2, 53) - 1;
+            return max < len ? max : len;
         }
     });
 
@@ -745,7 +775,7 @@
 
         const seen = [];
         const O = typeof locales === "string" ? [locales] : Internal.ToObject(locales);
-        const len = Internal.ToUint32(O.length);
+        const len = Internal.ToLength(O.length);
         let k = 0;
 
         while (k < len) {
@@ -865,56 +895,9 @@
         return supportedLocales;
     };
 
-    // the following two functions exist solely to prevent calling new Intl.{getCanonicalLocales|*.supportedLocalesOf}
-    // both should be bound to `intlStaticMethodThisArg` which has a hiddenObject with isValid = "Valid"
-    const intlStaticMethodThisArg = _.create();
-    platform.setHiddenObject(intlStaticMethodThisArg, { isValid: "Valid" });
-    const supportedLocalesOf_unconstructable = function (that, functionName, isAvailableLocale, requestedLocales, options) {
-        if (that === null || that === undefined) {
-            platform.raiseNotAConstructor(functionName);
-        }
-
-        const hiddenObj = platform.getHiddenObject(that);
-        if (!hiddenObj || hiddenObj.isValid !== "Valid") {
-            platform.raiseNotAConstructor(functionName);
-        }
-
-        return SupportedLocales(isAvailableLocale, CanonicalizeLocaleList(requestedLocales), options);
-    }
-
-    const getCanonicalLocales_unconstructable = function (that, functionName, locales) {
-        if (that === null || that === undefined) {
-            platform.raiseNotAConstructor(functionName);
-        }
-
-        const hiddenObj = platform.getHiddenObject(that);
-        if (!hiddenObj || hiddenObj.isValid !== "Valid") {
-            platform.raiseNotAConstructor(functionName);
-        }
-
-        return CanonicalizeLocaleList(locales);
-    }
-
-    // We go through a bit of a circus here to create and bind the getCanonicalLocales function for two reasons:
-    // 1. We want its name to be "getCanonicalLocales"
-    // 2. We want to make sure it isnt callable as `new {Intl.}getCanonicalLocales()`
-    // To accomplish (2), since we cant check CallFlags_New in JS Builtins, the next best thing is to bind the function to a known
-    // `this` and ensure that that is properly `this` on call (if not, we were called with `new` and should bail).
-    // However, this makes (1) more difficult, since binding a function changes its name
-    // When https://github.com/Microsoft/ChakraCore/issues/637 is fixed and we have a way
-    // to make built-in functions non-constructible, we can (and should) rethink this strategy
-    // TODO(jahorto): explore making these arrow functions, as suggested in #637, to get non-constructable "for free"
     if (InitType === "Intl") {
-        const getCanonicalLocales_name = "Intl.getCanonicalLocales";
-        const getCanonicalLocales_func = tagPublicFunction(getCanonicalLocales_name, function (locales) {
-            return getCanonicalLocales_unconstructable(this, getCanonicalLocales_name, locales);
-        });
-        const getCanonicalLocales = _.bind(getCanonicalLocales_func, intlStaticMethodThisArg);
-        _.defineProperty(getCanonicalLocales, 'name', {
-            value: 'getCanonicalLocales',
-            writable: false,
-            enumerable: false,
-            configurable: true,
+        const getCanonicalLocales = createPublicMethod("Intl.getCanonicalLocales", function getCanonicalLocales(locales) {
+            return CanonicalizeLocaleList(locales);
         });
         _.defineProperty(Intl, "getCanonicalLocales", {
             value: getCanonicalLocales,
@@ -987,7 +970,7 @@
 
         let localeCompareStateCache;
         // Make arguments undefined to ensure that localeCompare.length === 1
-        platform.registerBuiltInFunction(tagPublicFunction("String.prototype.localeCompare", function (that, locales = undefined, options = undefined) {
+        platform.registerBuiltInFunction(createPublicMethod("String.prototype.localeCompare", function localeCompare(that, locales = undefined, options = undefined) {
             if (this === undefined || this === null) {
                 platform.raiseThis_NullOrUndefined("String.prototype.localeCompare");
             }
@@ -1018,36 +1001,28 @@
             return;
         }
 
-        // using const f = function ... to remain consistent with the rest of the file,
-        // but the following function expressions get a name themselves to satisfy Intl.Collator.name
-        // and Intl.Collator.prototype.compare.name
-        const Collator = tagPublicFunction("Intl.Collator", function Collator(locales = undefined, options = undefined) {
-            if (this === Intl || this === undefined) {
-                return new Collator(locales, options);
-            }
+        const CollatorPrototype = {};
 
-            let obj = Internal.ToObject(this);
-            if (!_.isExtensible(obj)) {
-                platform.raiseObjectIsNonExtensible("Collator");
-            }
+        const Collator = tagPublicFunction("Intl.Collator", function Collator(locales = undefined, options = undefined) {
+            const newTarget = new.target === undefined ? Collator : new.target;
+            const collator = OrdinaryCreateFromConstructor(newTarget, CollatorPrototype);
 
             // Use the hidden object to store data
-            let hiddenObject = platform.getHiddenObject(obj);
-
+            let hiddenObject = platform.getHiddenObject(collator);
             if (hiddenObject === undefined) {
                 hiddenObject = _.create();
-                platform.setHiddenObject(obj, hiddenObject);
+                platform.setHiddenObject(collator, hiddenObject);
             }
 
             InitializeCollator(hiddenObject, locales, options);
 
             // Add the bound compare
-            hiddenObject.boundCompare = _.bind(compare, obj);
+            hiddenObject.boundCompare = _.bind(compare, collator);
             delete hiddenObject.boundCompare.name;
-            return obj;
+            return collator;
         });
 
-        const compare = tagPublicFunction("Intl.Collator.prototype.compare", function compare(x, y) {
+        const compare = createPublicMethod("Intl.Collator.prototype.compare", function compare(x, y) {
             if (typeof this !== "object") {
                 platform.raiseNeedObjectOfType("Collator.prototype.compare", "Collator");
             }
@@ -1060,41 +1035,31 @@
             return platform.localeCompare(String(x), String(y), hiddenObject, /* forStringPrototypeLocaleCompare */ false);
         });
 
-        // See explanation of `getCanonicalLocales`
-        const collator_supportedLocalesOf_name = "Intl.Collator.supportedLocalesOf";
-        const collator_supportedLocalesOf_func = tagPublicFunction(collator_supportedLocalesOf_name, function (locales, options = undefined) {
-            return supportedLocalesOf_unconstructable(this, collator_supportedLocalesOf_name, platform.isCollatorLocaleAvailable, locales, options);
-        });
-        const collator_supportedLocalesOf = _.bind(collator_supportedLocalesOf_func, intlStaticMethodThisArg);
-        _.defineProperty(collator_supportedLocalesOf, "name", {
-            value: "supportedLocalesOf",
-            writable: false,
-            enumerable: false,
-            configurable: true,
+        const supportedLocalesOf = createPublicMethod("Intl.Collator.supportedLocalesOf", function supportedLocalesOf(locales, options = undefined) {
+            return SupportedLocales(platform.isCollatorLocaleAvailable, CanonicalizeLocaleList(locales), options);
         });
         _.defineProperty(Collator, "supportedLocalesOf", {
-            value: collator_supportedLocalesOf,
+            value: supportedLocalesOf,
             writable: true,
             enumerable: false,
             configurable: true,
         });
 
         _.defineProperty(Collator, "prototype", {
-            value: new Collator(),
+            value: CollatorPrototype,
             writable: false,
             enumerable: false,
             configurable: false
         });
-        setPrototype(Collator.prototype, Object.prototype);
 
-        _.defineProperty(Collator.prototype, "constructor", {
+        _.defineProperty(CollatorPrototype, "constructor", {
             value: Collator,
             writable: true,
             enumerable: false,
             configurable: true
         });
-        _.defineProperty(Collator.prototype, "resolvedOptions", {
-            value: function resolvedOptions() {
+        _.defineProperty(CollatorPrototype, "resolvedOptions", {
+            value: createPublicMethod("Intl.Collator.prototype.resolvedOptions", function resolvedOptions() {
                 if (typeof this !== "object") {
                     platform.raiseNeedObjectOfType("Collator.prototype.resolvedOptions", "Collator");
                 }
@@ -1114,14 +1079,14 @@
                 ];
 
                 return createResolvedOptions(options, hiddenObject);
-            },
+            }),
             writable: true,
             enumerable: false,
             configurable: true
         });
 
         // test262's test\intl402\Collator\prototype\compare\name.js checks the name of the descriptor's getter function
-        const getCompare = function () {
+        const getCompare = createPublicMethod("get compare", function () {
             if (typeof this !== "object") {
                 platform.raiseNeedObjectOfType("Collator.prototype.compare", "Collator");
             }
@@ -1132,15 +1097,15 @@
             }
 
             return hiddenObject.boundCompare;
-        };
+        });
         _.defineProperty(getCompare, "name", {
             value: "get compare",
             writable: false,
             enumerable: false,
             configurable: true,
         });
-        _.defineProperty(Collator.prototype, "compare", {
-            get: tagPublicFunction("get compare", getCompare),
+        _.defineProperty(CollatorPrototype, "compare", {
+            get: getCompare,
             enumerable: false,
             configurable: true
         });
@@ -1215,7 +1180,7 @@
             return nf;
         };
 
-        platform.registerBuiltInFunction(tagPublicFunction("Number.prototype.toLocaleString", function () {
+        platform.registerBuiltInFunction(createPublicMethod("Number.prototype.toLocaleString", function toLocaleString() {
             if (typeof this !== "number" && !(this instanceof Number)) {
                 platform.raiseNeedObjectOfType("Number.prototype.toLocaleString", "Number");
             }
@@ -1231,48 +1196,46 @@
             return;
         }
 
-        const NumberFormat = tagPublicFunction("Intl.NumberFormat", function NumberFormat(locales = undefined, options = undefined) {
-            if (this === Intl || this === undefined) {
-                return new NumberFormat(locales, options);
-            }
-
-            const obj = Internal.ToObject(this);
+        const NumberFormatPrototype = {};
 
-            if (!_.isExtensible(obj)) {
-                platform.raiseObjectIsNonExtensible("NumberFormat");
-            }
+        const NumberFormat = tagPublicFunction("Intl.NumberFormat", function NumberFormat(locales = undefined, options = undefined) {
+            const newTarget = new.target === undefined ? NumberFormat : new.target;
+            const numberFormat = OrdinaryCreateFromConstructor(newTarget, NumberFormatPrototype);
 
-            // Use the hidden object to store data
-            let hiddenObject = platform.getHiddenObject(obj);
+            let hiddenObject = platform.getHiddenObject(numberFormat);
             if (hiddenObject === undefined) {
                 hiddenObject = _.create();
-                platform.setHiddenObject(obj, hiddenObject);
+                platform.setHiddenObject(numberFormat, hiddenObject);
             }
 
             InitializeNumberFormat(hiddenObject, locales, options);
 
-            hiddenObject.boundFormat = _.bind(format, obj)
-            delete hiddenObject.boundFormat.name;
+            if (new.target === undefined && this instanceof NumberFormat) {
+                _.defineProperty(this, platform.FallbackSymbol, {
+                    value: numberFormat,
+                    writable: false,
+                    enumerable: false,
+                    configurable: false
+                });
+
+                return this;
+            }
 
-            return obj;
+            return numberFormat;
         });
 
-        const format = tagPublicFunction("Intl.NumberFormat.prototype.format", function format(n) {
+        // format should always be bound to a valid NumberFormat's hiddenObject by getFormat()
+        const format = createPublicMethod("Intl.NumberFormat.prototype.format", function format(n) {
             n = Internal.ToNumber(n);
 
-            if (typeof this !== "object") {
+            if (!this || !this.initializedNumberFormat) {
                 platform.raiseNeedObjectOfType("NumberFormat.prototype.format", "NumberFormat");
             }
 
-            const hiddenObject = platform.getHiddenObject(this);
-            if (hiddenObject === undefined || !hiddenObject.initializedNumberFormat) {
-                platform.raiseNeedObjectOfType("NumberFormat.prototype.format", "NumberFormat");
-            }
-
-            return platform.formatNumber(n, hiddenObject, /* toParts */ false, /* forNumberPrototypeToLocaleString */ false);
+            return platform.formatNumber(n, this, /* toParts */ false, /* forNumberPrototypeToLocaleString */ false);
         });
 
-        const formatToParts = tagPublicFunction("Intl.NumberFormat.prototype.formatToParts", function formatToParts(n) {
+        const formatToParts = createPublicMethod("Intl.NumberFormat.prototype.formatToParts", function formatToParts(n) {
             n = Internal.ToNumber(n);
 
             if (typeof this !== "object") {
@@ -1287,86 +1250,95 @@
             return platform.formatNumber(n, hiddenObject, /* toParts */ true, /* forNumberPrototypeToLocaleString */ false);
         });
 
-        // See explanation of `getCanonicalLocales`
-        const numberFormat_supportedLocalesOf_name = "Intl.NumberFormat.supportedLocalesOf";
-        const numberFormat_supportedLocalesOf_func = tagPublicFunction(numberFormat_supportedLocalesOf_name, function (locales, options = undefined) {
-            return supportedLocalesOf_unconstructable(this, numberFormat_supportedLocalesOf_name, platform.isNFLocaleAvailable, locales, options);
-        });
-        const numberFormat_supportedLocalesOf = _.bind(numberFormat_supportedLocalesOf_func, intlStaticMethodThisArg);
-        _.defineProperty(numberFormat_supportedLocalesOf, "name", {
-            value: "supportedLocalesOf",
-            writable: false,
-            enumerable: false,
-            configurable: true,
+        const supportedLocalesOf = createPublicMethod("Intl.NumberFormat.supportedLocalesOf", function supportedLocalesOf(locales, options = undefined) {
+            return SupportedLocales(platform.isNFLocaleAvailable, CanonicalizeLocaleList(locales), options);
         });
         _.defineProperty(NumberFormat, "supportedLocalesOf", {
-            value: numberFormat_supportedLocalesOf,
+            value: supportedLocalesOf,
             writable: true,
             enumerable: false,
             configurable: true,
         });
 
         _.defineProperty(NumberFormat, "prototype", {
-            value: new NumberFormat(),
+            value: NumberFormatPrototype,
             writable: false,
             enumerable: false,
-            configurable: false,
+            configurable: false
         });
-        setPrototype(NumberFormat.prototype, Object.prototype);
-        _.defineProperty(NumberFormat.prototype, "constructor", {
+
+        _.defineProperty(NumberFormatPrototype, "constructor", {
             value: NumberFormat,
             writable: true,
             enumerable: false,
             configurable: true,
         });
 
-        _.defineProperty(NumberFormat.prototype, "resolvedOptions", {
-            value: function resolvedOptions() {
+        const UnwrapNumberFormat = function (nf) {
+            let hiddenObject = platform.getHiddenObject(nf);
+            if ((!hiddenObject || !hiddenObject.initializedNumberFormat) && nf instanceof NumberFormat) {
+                nf = nf[platform.FallbackSymbol];
+            }
+
+            if (typeof nf !== "object") {
+                platform.raiseNeedObjectOfType("NumberFormat.prototype.format", "NumberFormat");
+            }
+
+            hiddenObject = platform.getHiddenObject(nf);
+            if (!hiddenObject.initializedNumberFormat) {
+                platform.raiseNeedObjectOfType("NumberFormat.prototype.format", "NumberFormat");
+            }
+
+            return hiddenObject;
+        };
+
+        _.defineProperty(NumberFormatPrototype, "resolvedOptions", {
+            value: createPublicMethod("Intl.NumberFormat.prototype.resolvedOptions", function resolvedOptions() {
                 if (typeof this !== "object") {
-                    platform.raiseNeedObjectOfType("NumberFormat.prototype.resolvedOptions", "NumberFormat");
-                }
-                const hiddenObject = platform.getHiddenObject(this);
-                if (hiddenObject === undefined || !hiddenObject.initializedNumberFormat) {
-                    platform.raiseNeedObjectOfType("NumberFormat.prototype.resolvedOptions", "NumberFormat");
+                    platform.raiseNeedObjectOfType("NumberFormat.prototype.format", "NumberFormat");
                 }
 
+                const hiddenObject = UnwrapNumberFormat(this);
+
                 const options = ["locale", "numberingSystem", "style", "currency", "currencyDisplay", "minimumIntegerDigits",
                     "minimumFractionDigits", "maximumFractionDigits", "minimumSignificantDigits", "maximumSignificantDigits",
                     "useGrouping"];
 
                 return createResolvedOptions(options, hiddenObject);
-            },
+            }),
             writable: true,
             enumerable: false,
             configurable: true,
         });
 
         // test262's test\intl402\NumberFormat\prototype\format\name.js checks the name of the descriptor's getter function
-        const getFormat = function () {
+        const getFormat = createPublicMethod("get format", function () {
             if (typeof this !== "object") {
                 platform.raiseNeedObjectOfType("NumberFormat.prototype.format", "NumberFormat");
             }
 
-            const hiddenObject = platform.getHiddenObject(this);
-            if (hiddenObject === undefined || !hiddenObject.initializedNumberFormat) {
-                platform.raiseNeedObjectOfType("NumberFormat.prototype.format", "NumberFormat");
+            const hiddenObject = UnwrapNumberFormat(this);
+
+            if (hiddenObject.boundFormat === undefined) {
+                hiddenObject.boundFormat = _.bind(format, hiddenObject);
+                delete hiddenObject.boundFormat.name;
             }
 
             return hiddenObject.boundFormat;
-        };
+        });
         _.defineProperty(getFormat, "name", {
             value: "get format",
             writable: false,
             enumerable: false,
             configurable: true,
         });
-        _.defineProperty(NumberFormat.prototype, "format", {
-            get: tagPublicFunction("get format", getFormat),
+        _.defineProperty(NumberFormatPrototype, "format", {
+            get: getFormat,
             enumerable: false,
             configurable: true,
         });
 
-        _.defineProperty(NumberFormat.prototype, "formatToParts", {
+        _.defineProperty(NumberFormatPrototype, "formatToParts", {
             value: formatToParts,
             enumerable: false,
             configurable: true,
@@ -1462,7 +1434,8 @@
                 k: "h24",
             };
 
-            return function (dtf, options) {
+            // take the hour12 option by name so that we dont call the getter for options.hour12 twice
+            return function (dtf, options, hour12) {
                 const resolvedOptions = _.reduce(dateTimeComponents, function (resolved, component) {
                     const prop = component[0];
                     const value = GetOption(options, prop, "string", component[1], undefined);
@@ -1473,9 +1446,6 @@
                     return resolved;
                 }, _.create());
 
-                // Providing undefined for the `values` argument allows { hour12: "asd" } to become hour12 = true,
-                // which is apparently a feature of the spec, rather than a bug.
-                const hour12 = GetOption(options, "hour12", "boolean", undefined, undefined);
                 const hc = dtf.hourCycle;
 
                 // Build up a skeleton by repeating skeleton keys (like "G", "y", etc) for a count corresponding to the intl option value.
@@ -1580,8 +1550,15 @@
 
             const opt = _.create();
             opt.localeMatcher = GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], "best fit");
-            // hc is the only option that can be set by -u extension or by options object key
-            opt.hc = GetOption(options, "hourCycle", "string", ["h11", "h12", "h23", "h24"], undefined);
+
+            // Providing undefined for the `values` argument allows { hour12: "asd" } to become hour12 = true,
+            // which is apparently a feature of the spec, rather than a bug.
+            const hour12 = GetOption(options, "hour12", "boolean", undefined, undefined);
+            let hourCycle = GetOption(options, "hourCycle", "string", ["h11", "h12", "h23", "h24"], undefined);
+            if (hour12 !== undefined) {
+                hourCycle = null;
+            }
+            opt.hc = hourCycle;
 
             const r = ResolveLocale(platform.isDTFLocaleAvailable, requestedLocales, opt, ["nu", "ca", "hc"]);
             dateTimeFormat.locale = r.locale;
@@ -1614,7 +1591,7 @@
             GetOption(options, "formatMatcher", "string", ["basic", "best fit"], "best fit");
 
             // this call replaces most of the spec code related to hour12/hourCycle and format negotiation/handling
-            getPatternForOptions(dateTimeFormat, options);
+            getPatternForOptions(dateTimeFormat, options, hour12);
             dateTimeFormat.initializedDateTimeFormat = true;
 
             return dateTimeFormat;
@@ -1677,14 +1654,6 @@
             return options;
         };
 
-        const FormatDateTime = function (dtf, x) {
-            if (_.isNaN(x) || !_.isFinite(x)) {
-                platform.raiseInvalidDate();
-            }
-
-            return platform.formatDateTime(dtf, x, /* toParts */ false, /* forDatePrototypeToLocaleString */ false);
-        };
-
         const FormatDateTimeToParts = function (dtf, x) {
             if (_.isNaN(x) || !_.isFinite(x)) {
                 platform.raiseInvalidDate();
@@ -1732,44 +1701,42 @@
             return platform.formatDateTime(stateObject, Internal.ToNumber(this), /* toParts */ false, /* forDatePrototypeToLocaleString */ true);
         }
 
-        // Note: tagPublicFunction (platform.tagPublicLibraryCode) messes with declared name of the FunctionBody so that
-        // the functions called appear correctly in the debugger and stack traces. Thus, we we cannot call tagPublicFunction in a loop.
+        // Note: createPublicMethod (platform.tagPublicLibraryCode) messes with declared name of the FunctionBody so that
+        // the functions called appear correctly in the debugger and stack traces. Thus, we we cannot call createPublicMethod in a loop.
         // Each entry point needs to have its own unique FunctionBody (which is a function as defined in the source code);
         // this is why we have seemingly repeated ourselves below, instead of having one function and calling it multiple times with
         // different parameters.
         //
-        // The following invocations of `platform.registerBuiltInFunction(tagPublicFunction(name, entryPoint))` are enclosed in IIFEs.
+        // The following invocations of `platform.registerBuiltInFunction(createPublicMethod(name, entryPoint))` are enclosed in IIFEs.
         // The IIFEs are used to group all of the meaningful differences between each entry point into the arguments to the IIFE.
         // The exception to this are the different entryPoint names which are only significant for debugging (and cannot be passed in
         // as arguments, as the name is intrinsic to the function declaration).
         //
         // The `date_toLocale*String_entryPoint` function names are placeholder names that will never be seen from user code.
-        // The function name property and FunctionBody declared name are overwritten by `tagPublicFunction`.
+        // The function name property and FunctionBody declared name are overwritten by `createPublicMethod`.
         // The fact that they are declared with unique names is helpful for debugging.
         // The functions *must not* be declared as anonymous functions (must be declared with a name);
         // converting from an unnnamed function to a named function is not readily supported by the platform code and
         // this has caused us to hit assertions in debug builds in the past.
         //
-        // See invocations of `tagPublicFunction` on the `supportedLocalesOf` entry points for a similar pattern.
-        //
         // The entryPoint functions will be called as `Date.prototype.toLocale*String` and thus their `this` parameters will be a Date.
         // `DateInstanceToLocaleStringImplementation` is not on `Date.prototype`, so we must propagate `this` into the call by using
         // `DateInstanceToLocaleStringImplementation.call(this, ...)`.
 
         (function (name, option1, option2, cacheSlot, platformFunctionID) {
-            platform.registerBuiltInFunction(tagPublicFunction(name, function date_toLocaleString_entryPoint(locales = undefined, options = undefined) {
+            platform.registerBuiltInFunction(createPublicMethod(name, function date_toLocaleString_entryPoint(locales = undefined, options = undefined) {
                 return DateInstanceToLocaleStringImplementation.call(this, name, option1, option2, cacheSlot, locales, options);
             }), platformFunctionID);
         })("Date.prototype.toLocaleString", "any", "all", __DateInstanceToLocaleStringDefaultCacheSlot.toLocaleString, IntlBuiltInFunctionID.DateToLocaleString);
 
         (function (name, option1, option2, cacheSlot, platformFunctionID) {
-            platform.registerBuiltInFunction(tagPublicFunction(name, function date_toLocaleDateString_entryPoint(locales = undefined, options = undefined) {
+            platform.registerBuiltInFunction(createPublicMethod(name, function date_toLocaleDateString_entryPoint(locales = undefined, options = undefined) {
                 return DateInstanceToLocaleStringImplementation.call(this, name, option1, option2, cacheSlot, locales, options);
             }), platformFunctionID);
         })("Date.prototype.toLocaleDateString", "date", "date", __DateInstanceToLocaleStringDefaultCacheSlot.toLocaleDateString, IntlBuiltInFunctionID.DateToLocaleDateString);
 
         (function (name, option1, option2, cacheSlot, platformFunctionID) {
-            platform.registerBuiltInFunction(tagPublicFunction(name, function date_toLocaleTimeString_entryPoint(locales = undefined, options = undefined) {
+            platform.registerBuiltInFunction(createPublicMethod(name, function date_toLocaleTimeString_entryPoint(locales = undefined, options = undefined) {
                 return DateInstanceToLocaleStringImplementation.call(this, name, option1, option2, cacheSlot, locales, options);
             }), platformFunctionID);
         })("Date.prototype.toLocaleTimeString", "time", "time", __DateInstanceToLocaleStringDefaultCacheSlot.toLocaleTimeString, IntlBuiltInFunctionID.DateToLocaleTimeString);
@@ -1779,6 +1746,8 @@
             return;
         }
 
+        const DateTimeFormatPrototype = {};
+
         /**
          * The Intl.DateTimeFormat constructor
          *
@@ -1787,97 +1756,102 @@
          * @param {String|String[]} locales
          * @param {Object} options
          */
-        function DateTimeFormat(locales = undefined, options = undefined) {
-            if (this === Intl || this === undefined) {
-                return new DateTimeFormat(locales, options);
-            }
-
-            const obj = Internal.ToObject(this);
-            if (!_.isExtensible(obj)) {
-                platform.raiseObjectIsNonExtensible("DateTimeFormat");
-            }
-
-            // Use the hidden object to store data
-            let hiddenObject = platform.getHiddenObject(obj);
+        const DateTimeFormat = tagPublicFunction("Intl.DateTimeFormat", function DateTimeFormat(locales = undefined, options = undefined) {
+            const newTarget = new.target === undefined ? DateTimeFormat : new.target;
+            const dateTimeFormat = OrdinaryCreateFromConstructor(newTarget, DateTimeFormatPrototype);
 
+            let hiddenObject = platform.getHiddenObject(dateTimeFormat);
             if (hiddenObject === undefined) {
                 hiddenObject = _.create();
-                platform.setHiddenObject(obj, hiddenObject);
+                platform.setHiddenObject(dateTimeFormat, hiddenObject);
             }
 
             InitializeDateTimeFormat(hiddenObject, locales, options);
 
-            // only format has to be bound and attached to the DateTimeFormat
-            hiddenObject.boundFormat = _.bind(format, obj);
-            delete hiddenObject.boundFormat.name;
-
-            return obj;
-        }
-        tagPublicFunction("Intl.DateTimeFormat", DateTimeFormat);
+            if (new.target === undefined && this instanceof DateTimeFormat) {
+                _.defineProperty(this, platform.FallbackSymbol, {
+                    value: dateTimeFormat,
+                    writable: false,
+                    enumerable: false,
+                    configurable: false
+                });
 
-        /**
-         * Asserts that dtf is a valid DateTimeFormat object, or throws a TypeError otherwise.
-         *
-         * Returns the hiddenObject for the given dtf.
-         *
-         * @param {Object} dtf `this` of a given call to a DateTimeFormat member function
-         * @param {String} name the name of the function requiring dtf to be a valid DateTimeFormat
-         * @returns {Object} the hiddenObject for the given dtf
-         */
-        const ensureMember = function (dtf, name) {
-            if (typeof dtf !== 'object') {
-                platform.raiseNeedObjectOfType(`Intl.DateTimeFormat.prototype.${name}`, "DateTimeFormat");
+                return this;
             }
+
+            return dateTimeFormat;
+        });
+
+        const UnwrapDateTimeFormat = function (dtf) {
             let hiddenObject = platform.getHiddenObject(dtf);
-            if (hiddenObject === undefined || !hiddenObject.initializedDateTimeFormat) {
-                platform.raiseNeedObjectOfType(`Intl.DateTimeFormat.prototype.${name}`, "DateTimeFormat");
+            if ((!hiddenObject || !hiddenObject.initializedDateTimeFormat) && dtf instanceof DateTimeFormat) {
+                dtf = dtf[platform.FallbackSymbol];
+            }
+
+            if (typeof dtf !== "object") {
+                platform.raiseNeedObjectOfType("DateTimeFormat.prototype.format", "DateTimeFormat");
+            }
+
+            hiddenObject = platform.getHiddenObject(dtf);
+            if (!hiddenObject.initializedDateTimeFormat) {
+                platform.raiseNeedObjectOfType("DateTimeFormat.prototype.format", "DateTimeFormat");
             }
 
             return hiddenObject;
         };
 
-        /**
-         * Calls ensureMember on dtf, and then converts the given date to a number.
-         *
-         * Returns the hiddenObject for the given dtf and the resolved date.
-         *
-         * @param {Object} dtf `this` of a given call to a DateTimeFormat member function
-         * @param {Object} date the date to be formatted
-         * @param {String} name the name of the function requiring dtf to be a valid DateTimeFormat
-         */
-        const ensureFormat = function (dtf, date, name) {
-            const hiddenObject = ensureMember(dtf, name);
+        // format should always be bound to a valid DateTimeFormat's hiddenObject by getFormat()
+        const format = createPublicMethod("Intl.DateTimeFormat.prototype.format", function format(date) {
+            if (!this || !this.initializedDateTimeFormat) {
+                platform.raiseNeedObjectOfType("DateTimeFormat.prototype.format", "DateTimeFormat");
+            }
 
             let x;
             if (date === undefined) {
                 x = platform.builtInJavascriptDateEntryNow();
             } else {
                 x = Internal.ToNumber(date);
+
+                if (_.isNaN(x) || !_.isFinite(x)) {
+                    platform.raiseInvalidDate();
+                }
             }
 
-            // list of arguments for FormatDateTime{ToParts}
-            return [hiddenObject, x];
-        };
+            return platform.formatDateTime(this, x, /* toParts */ false, /* forDatePrototypeToLocaleString */ false);
+        });
 
-        const format = function (date) {
-            return _.apply(FormatDateTime, undefined, ensureFormat(this, date, "format"));
-        };
-        tagPublicFunction("Intl.DateTimeFormat.prototype.format", format);
+        const formatToParts = createPublicMethod("Intl.DateTimeFormat.prototype.formatToParts", function formatToParts(date) {
+            if (typeof this !== "object") {
+                platform.raiseNeedObjectOfType("DateTimeFormat.prototype.formatToParts", "DateTimeFormat");
+            }
 
-        const formatToParts = function (date) {
-            return _.apply(FormatDateTimeToParts, undefined, ensureFormat(this, date, "formatToParts"));
-        };
-        tagPublicFunction("Intl.DateTimeFormat.prototype.formatToParts", formatToParts);
+            const hiddenObject = platform.getHiddenObject(this);
+            if (hiddenObject === undefined || !hiddenObject.initializedDateTimeFormat) {
+                platform.raiseNeedObjectOfType("DateTimeFormat.prototype.formatToParts", "DateTimeFormat");
+            }
+
+            let x;
+            if (date === undefined) {
+                x = platform.builtInJavascriptDateEntryNow();
+            } else {
+                x = Internal.ToNumber(date);
+
+                if (_.isNaN(x) || !_.isFinite(x)) {
+                    platform.raiseInvalidDate();
+                }
+            }
+
+            return platform.formatDateTime(hiddenObject, x, /* toParts */ true, /* forDatePrototypeToLocaleString */ false);
+        });
 
         _.defineProperty(DateTimeFormat, "prototype", {
-            value: new DateTimeFormat(),
+            value: DateTimeFormatPrototype,
             writable: false,
             enumerable: false,
             configurable: false
         });
-        setPrototype(DateTimeFormat.prototype, Object.prototype);
 
-        _.defineProperty(DateTimeFormat.prototype, "constructor", {
+        _.defineProperty(DateTimeFormatPrototype, "constructor", {
             value: DateTimeFormat,
             writable: true,
             enumerable: false,
@@ -1885,31 +1859,44 @@
         });
 
         // test262's test\intl402\DateTimeFormat\prototype\format\name.js checks the name of the descriptor's getter function
-        const getFormat = function () {
-            const hiddenObject = ensureMember(this, format);
+        const getFormat = createPublicMethod("get format", function () {
+            if (typeof this !== "object") {
+                platform.raiseNeedObjectOfType("DateTimeFormat.prototype.format", "DateTimeFormat");
+            }
+
+            const hiddenObject = UnwrapDateTimeFormat(this);
+
+            if (hiddenObject.boundFormat === undefined) {
+                hiddenObject.boundFormat = _.bind(format, hiddenObject);
+                delete hiddenObject.boundFormat.name;
+            }
 
             return hiddenObject.boundFormat;
-        };
+        });
         _.defineProperty(getFormat, "name", {
             value: "get format",
             writable: false,
             enumerable: false,
             configurable: true,
         });
-        _.defineProperty(DateTimeFormat.prototype, "format", {
-            get: tagPublicFunction("get format", getFormat),
+        _.defineProperty(DateTimeFormatPrototype, "format", {
+            get: getFormat,
             enumerable: false,
             configurable: true,
         });
-        _.defineProperty(DateTimeFormat.prototype, "formatToParts", {
+        _.defineProperty(DateTimeFormatPrototype, "formatToParts", {
             value: formatToParts,
             enumerable: false,
             configurable: true,
             writable: true,
         });
-        _.defineProperty(DateTimeFormat.prototype, "resolvedOptions", {
-            value: function resolvedOptions() {
-                const hiddenObject = ensureMember(this, "resolvedOptions");
+        _.defineProperty(DateTimeFormatPrototype, "resolvedOptions", {
+            value: createPublicMethod("Intl.DateTimeFormat.prototype.resolvedOptions", function resolvedOptions() {
+                if (typeof this !== "object") {
+                    platform.raiseNeedObjectOfType("DateTimeFormat.prototype.format", "DateTimeFormat");
+                }
+
+                const hiddenObject = UnwrapDateTimeFormat(this);
                 const options = [
                     "locale",
                     "calendar",
@@ -1938,26 +1925,17 @@
                         return true;
                     }
                 });
-            },
+            }),
             writable: true,
             enumerable: false,
             configurable: true,
         });
 
-        // See explanation of `getCanonicalLocales`
-        const dateTimeFormat_supportedLocalesOf_name = "Intl.DateTimeFormat.supportedLocalesOf";
-        const dateTimeFormat_supportedLocalesOf_func = tagPublicFunction(dateTimeFormat_supportedLocalesOf_name, function (locales, options = undefined) {
-            return supportedLocalesOf_unconstructable(this, dateTimeFormat_supportedLocalesOf_name, platform.isDTFLocaleAvailable, locales, options);
-        });
-        const dateTimeFormat_supportedLocalesOf = _.bind(dateTimeFormat_supportedLocalesOf_func, intlStaticMethodThisArg);
-        _.defineProperty(dateTimeFormat_supportedLocalesOf, "name", {
-            value: "supportedLocalesOf",
-            writable: false,
-            enumerable: false,
-            configurable: true,
+        const supportedLocalesOf = createPublicMethod("Intl.DateTimeFormat.supportedLocalesOf", function supportedLocalesOf(locales, options = undefined) {
+            return SupportedLocales(platform.isDTFLocaleAvailable, CanonicalizeLocaleList(locales), options);
         });
         _.defineProperty(DateTimeFormat, "supportedLocalesOf", {
-            value: dateTimeFormat_supportedLocalesOf,
+            value: supportedLocalesOf,
             writable: true,
             enumerable: false,
             configurable: true,
@@ -2017,51 +1995,51 @@
             return platform.pluralRulesSelect(pluralRules, n);
         };
 
+        const PluralRulesPrototype = {};
+
         // params are explicitly `= undefined` to make PluralRules.length === 0
-        const PluralRules = function PluralRules(locales = undefined, options = undefined) {
+        const PluralRules = tagPublicFunction("Intl.PluralRules", function PluralRules(locales = undefined, options = undefined) {
             if (new.target === undefined) {
                 platform.raiseNeedObjectOfType("Intl.PluralRules", "PluralRules");
             }
 
+            const pluralRules = OrdinaryCreateFromConstructor(new.target, PluralRulesPrototype);
+
             const stateObject = _.create();
-            platform.setHiddenObject(this, stateObject);
+            platform.setHiddenObject(pluralRules, stateObject);
 
             InitializePluralRules(stateObject, locales, options);
 
-            return this;
-        };
-        tagPublicFunction("Intl.PluralRules", PluralRules);
+            return pluralRules;
+        });
 
         // ECMA 402: #sec-intl.pluralrules.prototype
         _.defineProperty(PluralRules, "prototype", {
-            value: {},
+            value: PluralRulesPrototype,
             writable: false,
             enumerable: false,
             configurable: false,
         });
 
-        // See explanation of `getCanonicalLocales`
-        // ECMA 402: #sec-intl.pluralrules.supportedlocalesof
-        const pluralRules_supportedLocalesOf_name = "Intl.PluralRules.supportedLocalesOf";
-        const pluralRules_supportedLocalesOf_func = tagPublicFunction(pluralRules_supportedLocalesOf_name, function (locales, options = undefined) {
-            return supportedLocalesOf_unconstructable(this, pluralRules_supportedLocalesOf_name, platform.isPRLocaleAvailable, locales, options);
-        });
-        const pluralRules_supportedLocalesOf = _.bind(pluralRules_supportedLocalesOf_func, intlStaticMethodThisArg);
-        _.defineProperty(pluralRules_supportedLocalesOf, "name", {
-            value: "supportedLocalesOf",
-            writable: false,
+        _.defineProperty(PluralRulesPrototype, "constructor", {
+            value: PluralRules,
+            writable: true,
             enumerable: false,
-            configurable: true,
+            configurable: true
+        });
+
+        const supportedLocalesOf = createPublicMethod("Intl.PluralRules.supportedLocalesOf", function supportedLocalesOf(locales, options = undefined) {
+            return SupportedLocales(platform.isPRLocaleAvailable, CanonicalizeLocaleList(locales), options);
         });
         _.defineProperty(PluralRules, "supportedLocalesOf", {
-            value: pluralRules_supportedLocalesOf,
+            value: supportedLocalesOf,
             writable: true,
             enumerable: false,
             configurable: true,
         });
 
         // ECMA 402: #sec-intl.pluralrules.prototype.select
-        const select = function select(value) {
+        const select = createPublicMethod("Intl.PluralRules.prototype.select", function select(value) {
             const pr = platform.getHiddenObject(this);
             if (!pr || !pr.initializedPluralRules) {
                 platform.raiseNeedObjectOfType("Intl.PluralRules.prototype.select", "PluralRules");
@@ -2069,16 +2047,15 @@
 
             const n = Internal.ToNumber(value);
             return ResolvePlural(pr, n);
-        };
-        tagPublicFunction("Intl.PluralRules.prototype.select", select);
-        _.defineProperty(PluralRules.prototype, "select", {
+        });
+        _.defineProperty(PluralRulesPrototype, "select", {
             value: select,
             enumerable: false,
             configurable: true,
             writable: true,
         });
 
-        const resolvedOptions = function resolvedOptions() {
+        const resolvedOptions = createPublicMethod("Intl.PluralRules.prototype.resolvedOptions", function resolvedOptions() {
             const pr = platform.getHiddenObject(this);
             if (!pr || !pr.initializedPluralRules) {
                 platform.raiseNeedObjectOfType("Intl.PluralRules.prototype.select", "PluralRules");
@@ -2100,9 +2077,8 @@
                     return true;
                 }
             });
-        };
-        tagPublicFunction("Intl.PluralRules.prototype.resolvedOptions", resolvedOptions);
-        _.defineProperty(PluralRules.prototype, "resolvedOptions", {
+        });
+        _.defineProperty(PluralRulesPrototype, "resolvedOptions", {
             value: resolvedOptions,
             enumerable: false,
             configurable: true,

+ 40 - 22
lib/Runtime/Library/IntlEngineInterfaceExtensionObject.cpp

@@ -330,7 +330,7 @@ namespace Js
     typedef FinalizableICUObject<UPluralRules *, uplrules_close> FinalizableUPluralRules;
 
     template<typename TExecutor>
-    static void EnsureBuffer(_In_ TExecutor executor, _In_ Recycler *recycler, _Outptr_result_buffer_(returnLength) char16 **ret, _Out_ int *returnLength, _In_ bool allowZeroLengthStrings = false, _In_ int firstTryLength = 8)
+    static void EnsureBuffer(_In_ TExecutor executor, _In_ Recycler *recycler, _Outptr_result_buffer_(*returnLength) char16 **ret, _Out_ int *returnLength, _In_ bool allowZeroLengthStrings = false, _In_ int firstTryLength = 8)
     {
         UErrorCode status = U_ZERO_ERROR;
         *ret = RecyclerNewArrayLeaf(recycler, char16, firstTryLength);
@@ -547,6 +547,8 @@ namespace Js
 #include "IntlExtensionObjectBuiltIns.h"
 #undef INTL_ENTRY
 
+        library->AddMember(intlNativeInterfaces, PropertyIds::FallbackSymbol, library->CreateSymbol(BuiltInPropertyRecords::_intlFallbackSymbol));
+
 #if INTL_WINGLOB
         library->AddMember(intlNativeInterfaces, Js::PropertyIds::winglob, library->GetTrue());
 #else
@@ -2653,14 +2655,15 @@ DEFINE_ISXLOCALEAVAILABLE(PR, uloc)
             // To accomplish this, we can set the switchover date between julian/gregorian
             // to the ECMAScript beginning of time, which is -8.64e15 according to ecma262 #sec-time-values-and-time-range
             UCalendar *cal = const_cast<UCalendar *>(udat_getCalendar(*dtf));
-            ucal_setGregorianChange(cal, -8.64e15, &status);
-
-            // status can be U_UNSUPPORTED_ERROR if the calendar isn't gregorian, which
-            // there does not seem to be a way to check for ahead of time in the C API
-            AssertOrFailFastMsg(U_SUCCESS(status) || status == U_UNSUPPORTED_ERROR, ICU_ERRORMESSAGE(status));
-
-            // If we passed the previous check, we should reset the status to U_ZERO_ERROR (in case it was U_UNSUPPORTED_ERROR)
-            status = U_ZERO_ERROR;
+            const char *calType = ucal_getType(cal, &status);
+            ICU_ASSERT(status, calType != nullptr);
+            if (strcmp(calType, "gregorian") == 0)
+            {
+                double beginningOfTime = -8.64e15;
+                ucal_setGregorianChange(cal, beginningOfTime, &status);
+                double actualGregorianChange = ucal_getGregorianChange(cal, &status);
+                ICU_ASSERT(status, beginningOfTime == actualGregorianChange);
+            }
 
             INTL_TRACE("Caching new UDateFormat (0x%x) with langtag=%s, pattern=%s, timezone=%s", dtf, langtag->GetSz(), pattern->GetSz(), timeZone->GetSz());
 
@@ -2945,7 +2948,7 @@ DEFINE_ISXLOCALEAVAILABLE(PR, uloc)
     }
 
 #ifdef INTL_ICU
-    static FinalizableUPluralRules *GetOrCreatePluralRulesCache(DynamicObject *stateObject, ScriptContext *scriptContext)
+    static FinalizableUPluralRules *GetOrCreateCachedUPluralRules(DynamicObject *stateObject, ScriptContext *scriptContext)
     {
         Var cachedUPluralRules = nullptr;
         FinalizableUPluralRules *pr = nullptr;
@@ -2977,7 +2980,7 @@ DEFINE_ISXLOCALEAVAILABLE(PR, uloc)
             pr = FinalizableUPluralRules::New(scriptContext->GetRecycler(), uplrules_openForType(localeID, prType, &status));
             ICU_ASSERT(status, true);
 
-            INTL_TRACE("Caching UPluralRules object (0x%x) with langtag %s and type %s", langtag->GetSz(), type->GetSz());
+            INTL_TRACE("Caching UPluralRules object (0x%x) with langtag %s and type %s", pr, langtag->GetSz(), type->GetSz());
 
             stateObject->SetInternalProperty(InternalPropertyIds::CachedUPluralRules, pr, PropertyOperationFlags::PropertyOperation_None, nullptr);
         }
@@ -3001,7 +3004,7 @@ DEFINE_ISXLOCALEAVAILABLE(PR, uloc)
         // This array is only used in resolved options, so the majority of the functionality can remain (namely, select() still works)
 #if defined(ICU_VERSION) && ICU_VERSION >= 61
         DynamicObject *state = DynamicObject::UnsafeFromVar(args[1]);
-        FinalizableUPluralRules *pr = GetOrCreatePluralRulesCache(state, scriptContext);
+        FinalizableUPluralRules *pr = GetOrCreateCachedUPluralRules(state, scriptContext);
 
         UErrorCode status = U_ZERO_ERROR;
         ScopedUEnumeration keywords(uplrules_getKeywords(*pr, &status));
@@ -3044,29 +3047,44 @@ DEFINE_ISXLOCALEAVAILABLE(PR, uloc)
         CHAKRATEL_LANGSTATS_INC_BUILTINCOUNT(PluralRules_Prototype_select);
         INTL_TRACE("Calling PluralRules.prototype.select(%f)", n);
 
-        FinalizableUPluralRules *pr = GetOrCreatePluralRulesCache(state, scriptContext);
+        UErrorCode status = U_ZERO_ERROR;
+
+        FinalizableUPluralRules *pr = GetOrCreateCachedUPluralRules(state, scriptContext);
 
         // ICU has an internal API, uplrules_selectWithFormat, that is equivalent to uplrules_select but will respect digit options of the passed UNumberFormat.
         // Since its an internal API, we can't use it -- however, we can work around it by creating a UNumberFormat with provided digit options,
         // formatting the requested number to a string, and then converting the string back to a double which we can pass to uplrules_select.
         // This workaround was suggested during the May 2018 ECMA-402 discussion.
-        // TODO(jahorto): investigate caching this UNumberFormat on the state as well. This is currently not possible because we are using InternalProperyIds::HiddenObject
-        // for all ICU object caching, but once we move to better names for the cache property IDs, we can cache both the UNumberFormat as well as the UPluralRules.
-        char localeID[ULOC_FULLNAME_CAPACITY] = { 0 };
-        LangtagToLocaleID(AssertStringProperty(state, PropertyIds::locale), localeID);
-        UErrorCode status = U_ZERO_ERROR;
-        FinalizableUNumberFormat *fmt = FinalizableUNumberFormat::New(scriptContext->GetRecycler(), unum_open(UNUM_DECIMAL, nullptr, 0, localeID, nullptr, &status));
+        // The below is similar to GetOrCreateCachedUPluralRules, but since creating a UNumberFormat for Intl.NumberFormat is much more involved and no one else
+        // uses this functionality, it makes more sense to me to just put the logic inline.
+        Var cachedUNumberFormat = nullptr;
+        FinalizableUNumberFormat *nf = nullptr;
+        if (state->GetInternalProperty(state, InternalPropertyIds::CachedUNumberFormat, &cachedUNumberFormat, nullptr, scriptContext))
+        {
+            nf = reinterpret_cast<FinalizableUNumberFormat *>(cachedUNumberFormat);
+            INTL_TRACE("Using previously cached UNumberFormat (0x%x)", nf);
+        }
+        else
+        {
+            char localeID[ULOC_FULLNAME_CAPACITY] = { 0 };
+            LangtagToLocaleID(AssertStringProperty(state, PropertyIds::locale), localeID);
+            nf = FinalizableUNumberFormat::New(scriptContext->GetRecycler(), unum_open(UNUM_DECIMAL, nullptr, 0, localeID, nullptr, &status));
 
-        SetUNumberFormatDigitOptions(*fmt, state);
+            SetUNumberFormatDigitOptions(*nf, state);
+
+            INTL_TRACE("Caching UNumberFormat object (0x%x) with localeID %S", nf, localeID);
+
+            state->SetInternalProperty(InternalPropertyIds::CachedUNumberFormat, nf, PropertyOperationFlags::PropertyOperation_None, nullptr);
+        }
 
         char16 *formattedN = nullptr;
         int formattedNLength = 0;
         EnsureBuffer([&](UChar *buf, int bufLen, UErrorCode *status)
         {
-            return unum_formatDouble(*fmt, n, buf, bufLen, nullptr, status);
+            return unum_formatDouble(*nf, n, buf, bufLen, nullptr, status);
         }, scriptContext->GetRecycler(), &formattedN, &formattedNLength);
 
-        double nWithOptions = unum_parseDouble(*fmt, reinterpret_cast<UChar *>(formattedN), formattedNLength, nullptr, &status);
+        double nWithOptions = unum_parseDouble(*nf, reinterpret_cast<UChar *>(formattedN), formattedNLength, nullptr, &status);
         double roundtripDiff = n - nWithOptions;
         ICU_ASSERT(status, roundtripDiff <= 1.0 && roundtripDiff >= -1.0);
 

+ 9 - 3
test/Intl/IntlIdentities.js

@@ -42,7 +42,10 @@ let tests = [
         name: "Invoking built-in static methods with `new` fails (check name in error message)",
         body: function () {
             for (let i in staticMethods) {
-                assert.throws(() => new staticMethods[i](), TypeError, "", `Function '${longNames[i]}' is not a constructor`);
+                const expectedMessage = WScript.Platform.INTL_LIBRARY === "icu"
+                    ? "Function is not a constructor"
+                    : `Function '${longNames[i]}' is not a constructor`;
+                assert.throws(() => new staticMethods[i](), TypeError, "", expectedMessage);
             }
         }
     },
@@ -50,8 +53,11 @@ let tests = [
         name: "toString of built-in static methods",
         body: function () {
             for (let i in staticMethods) {
-                assert.areEqual('' + staticMethods[i], expectedToString);
-                assert.areEqual(staticMethods[i].toString(), expectedToString);
+                const expectedMessage = WScript.Platform.INTL_LIBRARY === "icu"
+                    ? `function ${shortNames[i]}() { [native code] }`
+                    : expectedToString;
+                assert.areEqual(expectedMessage, "" + staticMethods[i]);
+                assert.areEqual(expectedMessage, staticMethods[i].toString());
             }
         }
     }

+ 6 - 1
test/Intl/SupportedLocalesOf.js

@@ -24,7 +24,12 @@ const tests = [
             const fakeLocales = { get length() { throw new Error("User-provided locale object throws"); } };
 
             function test(ctor) {
-                assert.throws(() => new ctor.supportedLocalesOf(), TypeError, "", `Function 'Intl.${ctor.name}.supportedLocalesOf' is not a constructor`);
+                if (WScript.Platform.INTL_LIBRARY === "icu") {
+                    assert.throws(() => new ctor.supportedLocalesOf(), TypeError, "", "Function is not a constructor");
+                    assert.throws(() => Reflect.construct(function() {}, [], ctor.supportedLocalesOf), TypeError, "", "'newTarget' is not a constructor");
+                } else {
+                    assert.throws(() => new ctor.supportedLocalesOf(), TypeError, "", `Function 'Intl.${ctor.name}.supportedLocalesOf' is not a constructor`);
+                }
                 assert.throws(() => ctor.supportedLocalesOf(["en-US"], { localeMatcher: "incorrect" }), RangeError, "", rangeErrorMessage);
                 assert.throws(() => ctor.supportedLocalesOf(null), TypeError, "", "Object expected");
                 assert.throws(() => ctor.supportedLocalesOf(fakeLocales), Error, "", "User-provided locale object throws");

+ 36 - 0
test/Intl/common.js

@@ -83,5 +83,41 @@ testRunner.runTests([
             test((options) => assert.areEqual("", new Intl.DateTimeFormat("en-US", options).format()), "year", "numeric", true);
             test((options) => assert.areEqual("", new Intl.DateTimeFormat("en-US", options).format()), "minute", "numeric", true);
         }
+    },
+    {
+        name: "Intl.FallbackSymbol behavior",
+        body() {
+            if (WScript.Platform.INTL_LIBRARY === "winglob") {
+                return;
+            }
+
+            function testFallbackSymbol(Ctor, shouldHaveFallbackSymbol) {
+                const objNew = new Ctor();
+                const objCall = Ctor.call(objNew);
+                const symbols = Object.getOwnPropertySymbols(objCall);
+                assert.isTrue(objCall instanceof Ctor, `The given object should be an instance of ${Ctor.name}`);
+                assert.areEqual(0, Object.getOwnPropertyNames(objCall).length, "Incorrect number of OwnPropertyNames");
+                if (shouldHaveFallbackSymbol) {
+                    assert.areEqual(1, symbols.length, "Incorrect number of OwnPropertySymbols");
+                    const fallbackSymbol = symbols[0];
+                    assert.areEqual("Symbol(Intl.FallbackSymbol)", fallbackSymbol.toString(), "Unexpected symbol description");
+                    assert.areEqual("object", typeof objCall[fallbackSymbol], "objCall[fallbackSymbol] should be an object");
+                    assert.isTrue(objCall[fallbackSymbol] instanceof Ctor, `objCall[fallbackSymbol] should be an instance of ${Ctor.name}`);
+
+                    assert.throws(() => Ctor.call(objNew), TypeError, "Should not be able to legacy-construct an already-legacy-constructed Intl object (using original non-legacy new object)", "Cannot modify non-writable property 'Intl.FallbackSymbol'");
+                    assert.throws(() => Ctor.call(objCall), TypeError, "Should not be able to legacy-construct an already-legacy-constructed Intl object (using legacy .call() object", "Cannot modify non-writable property 'Intl.FallbackSymbol'");
+
+                    assert.areEqual(objNew, objCall, "Object created with .call should return `this`");
+                } else {
+                    assert.areEqual(0, symbols.length, "Incorrect number of OwnPropertySymbols");
+                }
+            }
+
+            // only NumberFormat and DateTimeFormat should have Intl.FallbackSymbol behavior. PluralRules has no legacy construction behavior.
+            testFallbackSymbol(Intl.Collator, false);
+            testFallbackSymbol(Intl.NumberFormat, true);
+            testFallbackSymbol(Intl.DateTimeFormat, true);
+            assert.throws(() => Intl.PluralRules.call(new Intl.PluralRules()), TypeError, "Intl.PluralRules requires `new`");
+        }
     }
 ], { verbose: false });