[intl] Add new semantics + compat fallback to Intl constructor

ECMA 402 v2 made Intl constructors more strict in terms of how they would
initialize objects, refusing to initialize objects which have already
been constructed. However, when Chrome tried to ship these semantics,
we ran into web compatibility issues.

This patch tries to square the circle and implement the simpler v2 object
semantics while including a compatibility workaround to allow objects to
sort of be initialized later, storing the real underlying Intl object
in a symbol-named property.

The new semantics are described in this PR against the ECMA 402 spec:
https://github.com/tc39/ecma402/pull/84

BUG=v8:4360, v8:4870
LOG=Y

Review-Url: https://codereview.chromium.org/2582993002
Cr-Commit-Position: refs/heads/master@{#41943}
diff --git a/src/heap-symbols.h b/src/heap-symbols.h
index d5d25db..2db9c0c 100644
--- a/src/heap-symbols.h
+++ b/src/heap-symbols.h
@@ -226,6 +226,7 @@
 
 #define PUBLIC_SYMBOL_LIST(V)                \
   V(iterator_symbol, Symbol.iterator)        \
+  V(intl_fallback_symbol, IntlFallback)      \
   V(match_symbol, Symbol.match)              \
   V(replace_symbol, Symbol.replace)          \
   V(search_symbol, Symbol.search)            \
diff --git a/src/js/i18n.js b/src/js/i18n.js
index 803f550..15e87df 100644
--- a/src/js/i18n.js
+++ b/src/js/i18n.js
@@ -23,6 +23,7 @@
 var GlobalNumber = global.Number;
 var GlobalRegExp = global.RegExp;
 var GlobalString = global.String;
+var IntlFallbackSymbol = utils.ImportNow("intl_fallback_symbol");
 var InstallFunctions = utils.InstallFunctions;
 var InstallGetter = utils.InstallGetter;
 var InternalArray = utils.InternalArray;
@@ -57,7 +58,8 @@
 /**
  * Adds bound method to the prototype of the given object.
  */
-function AddBoundMethod(obj, methodName, implementation, length, type) {
+function AddBoundMethod(obj, methodName, implementation, length, typename,
+                        compat) {
   %CheckIsBootstrapping();
   var internalName = %CreatePrivateSymbol(methodName);
   // Making getter an anonymous function will cause
@@ -66,32 +68,30 @@
   // than (as utils.InstallGetter would) on the SharedFunctionInfo
   // associated with all functions returned from AddBoundMethod.
   var getter = ANONYMOUS_FUNCTION(function() {
-    if (!%IsInitializedIntlObjectOfType(this, type)) {
-      throw %make_type_error(kMethodCalledOnWrongObject, methodName);
-    }
-    if (IS_UNDEFINED(this[internalName])) {
+    var receiver = Unwrap(this, typename, obj, methodName, compat);
+    if (IS_UNDEFINED(receiver[internalName])) {
       var boundMethod;
       if (IS_UNDEFINED(length) || length === 2) {
         boundMethod =
-          ANONYMOUS_FUNCTION((fst, snd) => implementation(this, fst, snd));
+          ANONYMOUS_FUNCTION((fst, snd) => implementation(receiver, fst, snd));
       } else if (length === 1) {
-        boundMethod = ANONYMOUS_FUNCTION(fst => implementation(this, fst));
+        boundMethod = ANONYMOUS_FUNCTION(fst => implementation(receiver, fst));
       } else {
         boundMethod = ANONYMOUS_FUNCTION((...args) => {
           // DateTimeFormat.format needs to be 0 arg method, but can still
           // receive an optional dateValue param. If one was provided, pass it
           // along.
           if (args.length > 0) {
-            return implementation(this, args[0]);
+            return implementation(receiver, args[0]);
           } else {
-            return implementation(this);
+            return implementation(receiver);
           }
         });
       }
       %SetNativeFlag(boundMethod);
-      this[internalName] = boundMethod;
+      receiver[internalName] = boundMethod;
     }
-    return this[internalName];
+    return receiver[internalName];
   });
 
   %FunctionRemovePrototype(getter);
@@ -99,6 +99,43 @@
   %SetNativeFlag(getter);
 }
 
+function IntlConstruct(receiver, constructor, initializer, newTarget, args,
+                       compat) {
+  var locales = args[0];
+  var options = args[1];
+
+  if (IS_UNDEFINED(newTarget)) {
+    if (compat && receiver instanceof constructor) {
+      let success = %object_define_property(receiver, IntlFallbackSymbol,
+                           { value: new constructor(locales, options) });
+      if (!success) {
+        throw %make_type_error(kReinitializeIntl, constructor);
+      }
+      return receiver;
+    }
+
+    return new constructor(locales, options);
+  }
+
+  return initializer(receiver, locales, options);
+}
+
+
+
+function Unwrap(receiver, typename, constructor, method, compat) {
+  if (!%IsInitializedIntlObjectOfType(receiver, typename)) {
+    if (compat && receiver instanceof constructor) {
+      let fallback = receiver[IntlFallbackSymbol];
+      if (%IsInitializedIntlObjectOfType(fallback, typename)) {
+        return fallback;
+      }
+    }
+    throw %make_type_error(kIncompatibleMethodReceiver, method, receiver);
+  }
+  return receiver;
+}
+
+
 // -------------------------------------------------------------------
 
 var Intl = {};
@@ -1029,29 +1066,18 @@
  *
  * @constructor
  */
-InstallConstructor(Intl, 'Collator', function() {
-    var locales = arguments[0];
-    var options = arguments[1];
-
-    if (!this || this === Intl) {
-      // Constructor is called as a function.
-      return new Intl.Collator(locales, options);
-    }
-
-    return initializeCollator(TO_OBJECT(this), locales, options);
-  }
-);
+function Collator() {
+  return IntlConstruct(this, Collator, initializeCollator, new.target,
+                       arguments);
+}
+InstallConstructor(Intl, 'Collator', Collator);
 
 
 /**
  * Collator resolvedOptions method.
  */
 InstallFunction(Intl.Collator.prototype, 'resolvedOptions', function() {
-    if (!%IsInitializedIntlObjectOfType(this, 'collator')) {
-      throw %make_type_error(kResolvedOptionsCalledOnNonObject, "Collator");
-    }
-
-    var coll = this;
+    var coll = Unwrap(this, 'collator', Collator, 'resolvedOptions', false);
     var locale = getOptimalLanguageTag(coll[resolvedSymbol].requestedLocale,
                                        coll[resolvedSymbol].locale);
 
@@ -1096,7 +1122,7 @@
 };
 
 
-AddBoundMethod(Intl.Collator, 'compare', compare, 2, 'collator');
+AddBoundMethod(Intl.Collator, 'compare', compare, 2, 'collator', false);
 
 /**
  * Verifies that the input is a well-formed ISO 4217 currency code.
@@ -1262,29 +1288,19 @@
  *
  * @constructor
  */
-InstallConstructor(Intl, 'NumberFormat', function() {
-    var locales = arguments[0];
-    var options = arguments[1];
-
-    if (!this || this === Intl) {
-      // Constructor is called as a function.
-      return new Intl.NumberFormat(locales, options);
-    }
-
-    return initializeNumberFormat(TO_OBJECT(this), locales, options);
-  }
-);
+function NumberFormat() {
+  return IntlConstruct(this, NumberFormat, initializeNumberFormat, new.target,
+                       arguments, true);
+}
+InstallConstructor(Intl, 'NumberFormat', NumberFormat);
 
 
 /**
  * NumberFormat resolvedOptions method.
  */
 InstallFunction(Intl.NumberFormat.prototype, 'resolvedOptions', function() {
-    if (!%IsInitializedIntlObjectOfType(this, 'numberformat')) {
-      throw %make_type_error(kResolvedOptionsCalledOnNonObject, "NumberFormat");
-    }
-
-    var format = this;
+    var format = Unwrap(this, 'numberformat', NumberFormat,
+                        'resolvedOptions', true);
     var locale = getOptimalLanguageTag(format[resolvedSymbol].requestedLocale,
                                        format[resolvedSymbol].locale);
 
@@ -1345,7 +1361,8 @@
 }
 
 
-AddBoundMethod(Intl.NumberFormat, 'format', formatNumber, 1, 'numberformat');
+AddBoundMethod(Intl.NumberFormat, 'format', formatNumber, 1, 'numberformat',
+               true);
 
 /**
  * Returns a string that matches LDML representation of the options object.
@@ -1638,27 +1655,19 @@
  *
  * @constructor
  */
-InstallConstructor(Intl, 'DateTimeFormat', function() {
-    var locales = arguments[0];
-    var options = arguments[1];
-
-    if (!this || this === Intl) {
-      // Constructor is called as a function.
-      return new Intl.DateTimeFormat(locales, options);
-    }
-
-    return initializeDateTimeFormat(TO_OBJECT(this), locales, options);
-  }
-);
+function DateTimeFormat() {
+  return IntlConstruct(this, DateTimeFormat, initializeDateTimeFormat,
+                       new.target, arguments, true);
+}
+InstallConstructor(Intl, 'DateTimeFormat', DateTimeFormat);
 
 
 /**
  * DateTimeFormat resolvedOptions method.
  */
 InstallFunction(Intl.DateTimeFormat.prototype, 'resolvedOptions', function() {
-    if (!%IsInitializedIntlObjectOfType(this, 'dateformat')) {
-      throw %make_type_error(kResolvedOptionsCalledOnNonObject, "DateTimeFormat");
-    }
+    var format = Unwrap(this, 'dateformat', DateTimeFormat,
+                        'resolvedOptions', true);
 
     /**
      * Maps ICU calendar names to LDML/BCP47 types for key 'ca'.
@@ -1671,7 +1680,6 @@
       'ethiopic-amete-alem': 'ethioaa'
     };
 
-    var format = this;
     var fromPattern = fromLDMLString(format[resolvedSymbol][patternSymbol]);
     var userCalendar = ICU_CALENDAR_MAP[format[resolvedSymbol].calendar];
     if (IS_UNDEFINED(userCalendar)) {
@@ -1758,7 +1766,8 @@
 
 
 // 0 because date is optional argument.
-AddBoundMethod(Intl.DateTimeFormat, 'format', formatDate, 0, 'dateformat');
+AddBoundMethod(Intl.DateTimeFormat, 'format', formatDate, 0, 'dateformat',
+               true);
 
 
 /**
@@ -1847,18 +1856,11 @@
  *
  * @constructor
  */
-InstallConstructor(Intl, 'v8BreakIterator', function() {
-    var locales = arguments[0];
-    var options = arguments[1];
-
-    if (!this || this === Intl) {
-      // Constructor is called as a function.
-      return new Intl.v8BreakIterator(locales, options);
-    }
-
-    return initializeBreakIterator(TO_OBJECT(this), locales, options);
-  }
-);
+function v8BreakIterator() {
+  return IntlConstruct(this, v8BreakIterator, initializeBreakIterator,
+                       new.target, arguments);
+}
+InstallConstructor(Intl, 'v8BreakIterator', v8BreakIterator);
 
 
 /**
@@ -1870,11 +1872,9 @@
       throw %make_type_error(kOrdinaryFunctionCalledAsConstructor);
     }
 
-    if (!%IsInitializedIntlObjectOfType(this, 'breakiterator')) {
-      throw %make_type_error(kResolvedOptionsCalledOnNonObject, "v8BreakIterator");
-    }
+    var segmenter = Unwrap(this, 'breakiterator', v8BreakIterator,
+                           'resolvedOptions', false);
 
-    var segmenter = this;
     var locale =
         getOptimalLanguageTag(segmenter[resolvedSymbol].requestedLocale,
                               segmenter[resolvedSymbol].locale);
diff --git a/src/messages.h b/src/messages.h
index ea0db8d..74b6dba 100644
--- a/src/messages.h
+++ b/src/messages.h
@@ -458,9 +458,6 @@
   T(RegExpNonObject, "% getter called on non-object %")                        \
   T(RegExpNonRegExp, "% getter called on non-RegExp object")                   \
   T(ReinitializeIntl, "Trying to re-initialize % object.")                     \
-  T(ResolvedOptionsCalledOnNonObject,                                          \
-    "resolvedOptions method called on a non-object or on a object that is "    \
-    "not Intl.%.")                                                             \
   T(ResolverNotAFunction, "Promise resolver % is not a function")              \
   T(RestrictedFunctionProperties,                                              \
     "'caller' and 'arguments' are restricted function properties and cannot "  \
diff --git a/test/cctest/interpreter/bytecode_expectations/ForOf.golden b/test/cctest/interpreter/bytecode_expectations/ForOf.golden
index 3c615bf..7f1f8bd 100644
--- a/test/cctest/interpreter/bytecode_expectations/ForOf.golden
+++ b/test/cctest/interpreter/bytecode_expectations/ForOf.golden
@@ -88,7 +88,7 @@
                 B(TestEqualStrict), R(12), U8(20),
                 B(JumpIfFalse), U8(4),
                 B(Jump), U8(18),
-                B(Wide), B(LdaSmi), U16(131),
+                B(Wide), B(LdaSmi), U16(130),
                 B(Star), R(12),
                 B(LdaConstant), U8(9),
                 B(Star), R(13),
@@ -233,7 +233,7 @@
                 B(TestEqualStrict), R(13), U8(20),
                 B(JumpIfFalse), U8(4),
                 B(Jump), U8(18),
-                B(Wide), B(LdaSmi), U16(131),
+                B(Wide), B(LdaSmi), U16(130),
                 B(Star), R(13),
                 B(LdaConstant), U8(9),
                 B(Star), R(14),
@@ -391,7 +391,7 @@
                 B(TestEqualStrict), R(12), U8(22),
                 B(JumpIfFalse), U8(4),
                 B(Jump), U8(18),
-                B(Wide), B(LdaSmi), U16(131),
+                B(Wide), B(LdaSmi), U16(130),
                 B(Star), R(12),
                 B(LdaConstant), U8(9),
                 B(Star), R(13),
@@ -539,7 +539,7 @@
                 B(TestEqualStrict), R(11), U8(24),
                 B(JumpIfFalse), U8(4),
                 B(Jump), U8(18),
-                B(Wide), B(LdaSmi), U16(131),
+                B(Wide), B(LdaSmi), U16(130),
                 B(Star), R(11),
                 B(LdaConstant), U8(11),
                 B(Star), R(12),
diff --git a/test/cctest/interpreter/bytecode_expectations/Generators.golden b/test/cctest/interpreter/bytecode_expectations/Generators.golden
index 8ad3e01..94c9ab2 100644
--- a/test/cctest/interpreter/bytecode_expectations/Generators.golden
+++ b/test/cctest/interpreter/bytecode_expectations/Generators.golden
@@ -491,7 +491,7 @@
                 B(TestEqualStrict), R(10), U8(20),
                 B(JumpIfFalse), U8(4),
                 B(Jump), U8(18),
-                B(Wide), B(LdaSmi), U16(131),
+                B(Wide), B(LdaSmi), U16(130),
                 B(Star), R(10),
                 B(LdaConstant), U8(14),
                 B(Star), R(11),
diff --git a/test/intl/assert.js b/test/intl/assert.js
index 26405e8..d8cc858 100644
--- a/test/intl/assert.js
+++ b/test/intl/assert.js
@@ -180,12 +180,12 @@
 function assertInstanceof(obj, type) {
   if (!(obj instanceof type)) {
     var actualTypeName = null;
-    var actualConstructor = Object.prototypeOf(obj).constructor;
+    var actualConstructor = Object.getPrototypeOf(obj).constructor;
     if (typeof actualConstructor == "function") {
       actualTypeName = actualConstructor.name || String(actualConstructor);
     }
     throw new Error('Object <' + obj + '> is not an instance of <' +
-         (type.name || type) + '>' +
-         (actualTypeName ? ' but of < ' + actualTypeName + '>' : ''));
+                    (type.name || type) + '>' +
+                    (actualTypeName ? ' but of < ' + actualTypeName + '>' : ''));
   }
 }
diff --git a/test/intl/general/constructor.js b/test/intl/general/constructor.js
new file mode 100644
index 0000000..4fb1389
--- /dev/null
+++ b/test/intl/general/constructor.js
@@ -0,0 +1,44 @@
+// Copyright 2015 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+let compatConstructors = [
+  {c: Intl.DateTimeFormat, m: "format"},
+  {c: Intl.NumberFormat, m: "format"},
+];
+
+for (let {c, m} of compatConstructors) {
+  let i = Object.create(c.prototype);
+  assertTrue(i instanceof c);
+  assertThrows(() => i[m], TypeError);
+  assertEquals(i, c.call(i));
+  assertEquals(i[m], i[m]);
+  assertTrue(i instanceof c);
+
+  for ({c: c2, m: m2} of compatConstructors) {
+    if (c2 === c)  {
+      assertThrows(() => c2.call(i), TypeError);
+    } else {
+      let i2 = c2.call(i);
+      assertTrue(i2 != i);
+      assertFalse(i2 instanceof c);
+      assertTrue(i2 instanceof c2);
+      assertEquals(i2[m2], i2[m2]);
+    }
+  }
+}
+
+let noCompatConstructors = [
+  {c: Intl.Collator, m: "compare"},
+  {c: Intl.v8BreakIterator, m: "next"},
+];
+
+for (let {c, m} of noCompatConstructors) {
+  let i = Object.create(c.prototype);
+  assertTrue(i instanceof c);
+  assertThrows(() => i[m], TypeError);
+  let i2 = c.call(i);
+  assertTrue(i2 != i);
+  assertEquals('function', typeof i2[m]);
+  assertTrue(i2 instanceof c);
+}
diff --git a/test/mjsunit/regress/regress-4870.js b/test/mjsunit/regress/regress-4870.js
new file mode 100644
index 0000000..3d52015
--- /dev/null
+++ b/test/mjsunit/regress/regress-4870.js
@@ -0,0 +1,8 @@
+// Copyright 2016 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+assertThrows(() => Object.getOwnPropertyDescriptor(Intl.Collator.prototype,
+                                                   'compare')
+                   .get.call(new Intl.DateTimeFormat())('a', 'b'),
+             TypeError)
diff --git a/test/test262/test262.status b/test/test262/test262.status
index 3c4d996..5ad12a6 100644
--- a/test/test262/test262.status
+++ b/test/test262/test262.status
@@ -101,7 +101,6 @@
   ###### END REGEXP SUBCLASSING SECTION ######
 
   # https://code.google.com/p/v8/issues/detail?id=4360
-  'intl402/Collator/10.1.1_1': [FAIL],
   'intl402/DateTimeFormat/12.1.1_1': [FAIL],
   'intl402/NumberFormat/11.1.1_1': [FAIL],