Commit b0a09d78 authored by littledan's avatar littledan Committed by Commit bot

[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}
parent e92118bb
...@@ -226,6 +226,7 @@ ...@@ -226,6 +226,7 @@
#define PUBLIC_SYMBOL_LIST(V) \ #define PUBLIC_SYMBOL_LIST(V) \
V(iterator_symbol, Symbol.iterator) \ V(iterator_symbol, Symbol.iterator) \
V(intl_fallback_symbol, IntlFallback) \
V(match_symbol, Symbol.match) \ V(match_symbol, Symbol.match) \
V(replace_symbol, Symbol.replace) \ V(replace_symbol, Symbol.replace) \
V(search_symbol, Symbol.search) \ V(search_symbol, Symbol.search) \
......
...@@ -23,6 +23,7 @@ var GlobalDate = global.Date; ...@@ -23,6 +23,7 @@ var GlobalDate = global.Date;
var GlobalNumber = global.Number; var GlobalNumber = global.Number;
var GlobalRegExp = global.RegExp; var GlobalRegExp = global.RegExp;
var GlobalString = global.String; var GlobalString = global.String;
var IntlFallbackSymbol = utils.ImportNow("intl_fallback_symbol");
var InstallFunctions = utils.InstallFunctions; var InstallFunctions = utils.InstallFunctions;
var InstallGetter = utils.InstallGetter; var InstallGetter = utils.InstallGetter;
var InternalArray = utils.InternalArray; var InternalArray = utils.InternalArray;
...@@ -57,7 +58,8 @@ function InstallConstructor(object, name, func) { ...@@ -57,7 +58,8 @@ function InstallConstructor(object, name, func) {
/** /**
* Adds bound method to the prototype of the given object. * 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(); %CheckIsBootstrapping();
var internalName = %CreatePrivateSymbol(methodName); var internalName = %CreatePrivateSymbol(methodName);
// Making getter an anonymous function will cause // Making getter an anonymous function will cause
...@@ -66,32 +68,30 @@ function AddBoundMethod(obj, methodName, implementation, length, type) { ...@@ -66,32 +68,30 @@ function AddBoundMethod(obj, methodName, implementation, length, type) {
// than (as utils.InstallGetter would) on the SharedFunctionInfo // than (as utils.InstallGetter would) on the SharedFunctionInfo
// associated with all functions returned from AddBoundMethod. // associated with all functions returned from AddBoundMethod.
var getter = ANONYMOUS_FUNCTION(function() { var getter = ANONYMOUS_FUNCTION(function() {
if (!%IsInitializedIntlObjectOfType(this, type)) { var receiver = Unwrap(this, typename, obj, methodName, compat);
throw %make_type_error(kMethodCalledOnWrongObject, methodName); if (IS_UNDEFINED(receiver[internalName])) {
}
if (IS_UNDEFINED(this[internalName])) {
var boundMethod; var boundMethod;
if (IS_UNDEFINED(length) || length === 2) { if (IS_UNDEFINED(length) || length === 2) {
boundMethod = boundMethod =
ANONYMOUS_FUNCTION((fst, snd) => implementation(this, fst, snd)); ANONYMOUS_FUNCTION((fst, snd) => implementation(receiver, fst, snd));
} else if (length === 1) { } else if (length === 1) {
boundMethod = ANONYMOUS_FUNCTION(fst => implementation(this, fst)); boundMethod = ANONYMOUS_FUNCTION(fst => implementation(receiver, fst));
} else { } else {
boundMethod = ANONYMOUS_FUNCTION((...args) => { boundMethod = ANONYMOUS_FUNCTION((...args) => {
// DateTimeFormat.format needs to be 0 arg method, but can still // DateTimeFormat.format needs to be 0 arg method, but can still
// receive an optional dateValue param. If one was provided, pass it // receive an optional dateValue param. If one was provided, pass it
// along. // along.
if (args.length > 0) { if (args.length > 0) {
return implementation(this, args[0]); return implementation(receiver, args[0]);
} else { } else {
return implementation(this); return implementation(receiver);
} }
}); });
} }
%SetNativeFlag(boundMethod); %SetNativeFlag(boundMethod);
this[internalName] = boundMethod; receiver[internalName] = boundMethod;
} }
return this[internalName]; return receiver[internalName];
}); });
%FunctionRemovePrototype(getter); %FunctionRemovePrototype(getter);
...@@ -99,6 +99,43 @@ function AddBoundMethod(obj, methodName, implementation, length, type) { ...@@ -99,6 +99,43 @@ function AddBoundMethod(obj, methodName, implementation, length, type) {
%SetNativeFlag(getter); %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 = {}; var Intl = {};
...@@ -1029,29 +1066,18 @@ function initializeCollator(collator, locales, options) { ...@@ -1029,29 +1066,18 @@ function initializeCollator(collator, locales, options) {
* *
* @constructor * @constructor
*/ */
InstallConstructor(Intl, 'Collator', function() { function Collator() {
var locales = arguments[0]; return IntlConstruct(this, Collator, initializeCollator, new.target,
var options = arguments[1]; arguments);
}
if (!this || this === Intl) { InstallConstructor(Intl, 'Collator', Collator);
// Constructor is called as a function.
return new Intl.Collator(locales, options);
}
return initializeCollator(TO_OBJECT(this), locales, options);
}
);
/** /**
* Collator resolvedOptions method. * Collator resolvedOptions method.
*/ */
InstallFunction(Intl.Collator.prototype, 'resolvedOptions', function() { InstallFunction(Intl.Collator.prototype, 'resolvedOptions', function() {
if (!%IsInitializedIntlObjectOfType(this, 'collator')) { var coll = Unwrap(this, 'collator', Collator, 'resolvedOptions', false);
throw %make_type_error(kResolvedOptionsCalledOnNonObject, "Collator");
}
var coll = this;
var locale = getOptimalLanguageTag(coll[resolvedSymbol].requestedLocale, var locale = getOptimalLanguageTag(coll[resolvedSymbol].requestedLocale,
coll[resolvedSymbol].locale); coll[resolvedSymbol].locale);
...@@ -1096,7 +1122,7 @@ function compare(collator, x, y) { ...@@ -1096,7 +1122,7 @@ function compare(collator, x, y) {
}; };
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. * Verifies that the input is a well-formed ISO 4217 currency code.
...@@ -1262,29 +1288,19 @@ function initializeNumberFormat(numberFormat, locales, options) { ...@@ -1262,29 +1288,19 @@ function initializeNumberFormat(numberFormat, locales, options) {
* *
* @constructor * @constructor
*/ */
InstallConstructor(Intl, 'NumberFormat', function() { function NumberFormat() {
var locales = arguments[0]; return IntlConstruct(this, NumberFormat, initializeNumberFormat, new.target,
var options = arguments[1]; arguments, true);
}
if (!this || this === Intl) { InstallConstructor(Intl, 'NumberFormat', NumberFormat);
// Constructor is called as a function.
return new Intl.NumberFormat(locales, options);
}
return initializeNumberFormat(TO_OBJECT(this), locales, options);
}
);
/** /**
* NumberFormat resolvedOptions method. * NumberFormat resolvedOptions method.
*/ */
InstallFunction(Intl.NumberFormat.prototype, 'resolvedOptions', function() { InstallFunction(Intl.NumberFormat.prototype, 'resolvedOptions', function() {
if (!%IsInitializedIntlObjectOfType(this, 'numberformat')) { var format = Unwrap(this, 'numberformat', NumberFormat,
throw %make_type_error(kResolvedOptionsCalledOnNonObject, "NumberFormat"); 'resolvedOptions', true);
}
var format = this;
var locale = getOptimalLanguageTag(format[resolvedSymbol].requestedLocale, var locale = getOptimalLanguageTag(format[resolvedSymbol].requestedLocale,
format[resolvedSymbol].locale); format[resolvedSymbol].locale);
...@@ -1345,7 +1361,8 @@ function formatNumber(formatter, value) { ...@@ -1345,7 +1361,8 @@ function formatNumber(formatter, value) {
} }
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. * Returns a string that matches LDML representation of the options object.
...@@ -1638,27 +1655,19 @@ function initializeDateTimeFormat(dateFormat, locales, options) { ...@@ -1638,27 +1655,19 @@ function initializeDateTimeFormat(dateFormat, locales, options) {
* *
* @constructor * @constructor
*/ */
InstallConstructor(Intl, 'DateTimeFormat', function() { function DateTimeFormat() {
var locales = arguments[0]; return IntlConstruct(this, DateTimeFormat, initializeDateTimeFormat,
var options = arguments[1]; new.target, arguments, true);
}
if (!this || this === Intl) { InstallConstructor(Intl, 'DateTimeFormat', DateTimeFormat);
// Constructor is called as a function.
return new Intl.DateTimeFormat(locales, options);
}
return initializeDateTimeFormat(TO_OBJECT(this), locales, options);
}
);
/** /**
* DateTimeFormat resolvedOptions method. * DateTimeFormat resolvedOptions method.
*/ */
InstallFunction(Intl.DateTimeFormat.prototype, 'resolvedOptions', function() { InstallFunction(Intl.DateTimeFormat.prototype, 'resolvedOptions', function() {
if (!%IsInitializedIntlObjectOfType(this, 'dateformat')) { var format = Unwrap(this, 'dateformat', DateTimeFormat,
throw %make_type_error(kResolvedOptionsCalledOnNonObject, "DateTimeFormat"); 'resolvedOptions', true);
}
/** /**
* Maps ICU calendar names to LDML/BCP47 types for key 'ca'. * Maps ICU calendar names to LDML/BCP47 types for key 'ca'.
...@@ -1671,7 +1680,6 @@ InstallFunction(Intl.DateTimeFormat.prototype, 'resolvedOptions', function() { ...@@ -1671,7 +1680,6 @@ InstallFunction(Intl.DateTimeFormat.prototype, 'resolvedOptions', function() {
'ethiopic-amete-alem': 'ethioaa' 'ethiopic-amete-alem': 'ethioaa'
}; };
var format = this;
var fromPattern = fromLDMLString(format[resolvedSymbol][patternSymbol]); var fromPattern = fromLDMLString(format[resolvedSymbol][patternSymbol]);
var userCalendar = ICU_CALENDAR_MAP[format[resolvedSymbol].calendar]; var userCalendar = ICU_CALENDAR_MAP[format[resolvedSymbol].calendar];
if (IS_UNDEFINED(userCalendar)) { if (IS_UNDEFINED(userCalendar)) {
...@@ -1758,7 +1766,8 @@ function FormatDateToParts(dateValue) { ...@@ -1758,7 +1766,8 @@ function FormatDateToParts(dateValue) {
// 0 because date is optional argument. // 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 @@ function initializeBreakIterator(iterator, locales, options) { ...@@ -1847,18 +1856,11 @@ function initializeBreakIterator(iterator, locales, options) {
* *
* @constructor * @constructor
*/ */
InstallConstructor(Intl, 'v8BreakIterator', function() { function v8BreakIterator() {
var locales = arguments[0]; return IntlConstruct(this, v8BreakIterator, initializeBreakIterator,
var options = arguments[1]; new.target, arguments);
}
if (!this || this === Intl) { InstallConstructor(Intl, 'v8BreakIterator', v8BreakIterator);
// Constructor is called as a function.
return new Intl.v8BreakIterator(locales, options);
}
return initializeBreakIterator(TO_OBJECT(this), locales, options);
}
);
/** /**
...@@ -1870,11 +1872,9 @@ InstallFunction(Intl.v8BreakIterator.prototype, 'resolvedOptions', ...@@ -1870,11 +1872,9 @@ InstallFunction(Intl.v8BreakIterator.prototype, 'resolvedOptions',
throw %make_type_error(kOrdinaryFunctionCalledAsConstructor); throw %make_type_error(kOrdinaryFunctionCalledAsConstructor);
} }
if (!%IsInitializedIntlObjectOfType(this, 'breakiterator')) { var segmenter = Unwrap(this, 'breakiterator', v8BreakIterator,
throw %make_type_error(kResolvedOptionsCalledOnNonObject, "v8BreakIterator"); 'resolvedOptions', false);
}
var segmenter = this;
var locale = var locale =
getOptimalLanguageTag(segmenter[resolvedSymbol].requestedLocale, getOptimalLanguageTag(segmenter[resolvedSymbol].requestedLocale,
segmenter[resolvedSymbol].locale); segmenter[resolvedSymbol].locale);
......
...@@ -458,9 +458,6 @@ class ErrorUtils : public AllStatic { ...@@ -458,9 +458,6 @@ class ErrorUtils : public AllStatic {
T(RegExpNonObject, "% getter called on non-object %") \ T(RegExpNonObject, "% getter called on non-object %") \
T(RegExpNonRegExp, "% getter called on non-RegExp object") \ T(RegExpNonRegExp, "% getter called on non-RegExp object") \
T(ReinitializeIntl, "Trying to re-initialize % 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(ResolverNotAFunction, "Promise resolver % is not a function") \
T(RestrictedFunctionProperties, \ T(RestrictedFunctionProperties, \
"'caller' and 'arguments' are restricted function properties and cannot " \ "'caller' and 'arguments' are restricted function properties and cannot " \
......
...@@ -88,7 +88,7 @@ bytecodes: [ ...@@ -88,7 +88,7 @@ bytecodes: [
B(TestEqualStrict), R(12), U8(20), B(TestEqualStrict), R(12), U8(20),
B(JumpIfFalse), U8(4), B(JumpIfFalse), U8(4),
B(Jump), U8(18), B(Jump), U8(18),
B(Wide), B(LdaSmi), U16(131), B(Wide), B(LdaSmi), U16(130),
B(Star), R(12), B(Star), R(12),
B(LdaConstant), U8(9), B(LdaConstant), U8(9),
B(Star), R(13), B(Star), R(13),
...@@ -233,7 +233,7 @@ bytecodes: [ ...@@ -233,7 +233,7 @@ bytecodes: [
B(TestEqualStrict), R(13), U8(20), B(TestEqualStrict), R(13), U8(20),
B(JumpIfFalse), U8(4), B(JumpIfFalse), U8(4),
B(Jump), U8(18), B(Jump), U8(18),
B(Wide), B(LdaSmi), U16(131), B(Wide), B(LdaSmi), U16(130),
B(Star), R(13), B(Star), R(13),
B(LdaConstant), U8(9), B(LdaConstant), U8(9),
B(Star), R(14), B(Star), R(14),
...@@ -391,7 +391,7 @@ bytecodes: [ ...@@ -391,7 +391,7 @@ bytecodes: [
B(TestEqualStrict), R(12), U8(22), B(TestEqualStrict), R(12), U8(22),
B(JumpIfFalse), U8(4), B(JumpIfFalse), U8(4),
B(Jump), U8(18), B(Jump), U8(18),
B(Wide), B(LdaSmi), U16(131), B(Wide), B(LdaSmi), U16(130),
B(Star), R(12), B(Star), R(12),
B(LdaConstant), U8(9), B(LdaConstant), U8(9),
B(Star), R(13), B(Star), R(13),
...@@ -539,7 +539,7 @@ bytecodes: [ ...@@ -539,7 +539,7 @@ bytecodes: [
B(TestEqualStrict), R(11), U8(24), B(TestEqualStrict), R(11), U8(24),
B(JumpIfFalse), U8(4), B(JumpIfFalse), U8(4),
B(Jump), U8(18), B(Jump), U8(18),
B(Wide), B(LdaSmi), U16(131), B(Wide), B(LdaSmi), U16(130),
B(Star), R(11), B(Star), R(11),
B(LdaConstant), U8(11), B(LdaConstant), U8(11),
B(Star), R(12), B(Star), R(12),
......
...@@ -491,7 +491,7 @@ bytecodes: [ ...@@ -491,7 +491,7 @@ bytecodes: [
B(TestEqualStrict), R(10), U8(20), B(TestEqualStrict), R(10), U8(20),
B(JumpIfFalse), U8(4), B(JumpIfFalse), U8(4),
B(Jump), U8(18), B(Jump), U8(18),
B(Wide), B(LdaSmi), U16(131), B(Wide), B(LdaSmi), U16(130),
B(Star), R(10), B(Star), R(10),
B(LdaConstant), U8(14), B(LdaConstant), U8(14),
B(Star), R(11), B(Star), R(11),
......
...@@ -180,12 +180,12 @@ function assertDoesNotThrow(code, user_message = '') { ...@@ -180,12 +180,12 @@ function assertDoesNotThrow(code, user_message = '') {
function assertInstanceof(obj, type) { function assertInstanceof(obj, type) {
if (!(obj instanceof type)) { if (!(obj instanceof type)) {
var actualTypeName = null; var actualTypeName = null;
var actualConstructor = Object.prototypeOf(obj).constructor; var actualConstructor = Object.getPrototypeOf(obj).constructor;
if (typeof actualConstructor == "function") { if (typeof actualConstructor == "function") {
actualTypeName = actualConstructor.name || String(actualConstructor); actualTypeName = actualConstructor.name || String(actualConstructor);
} }
throw new Error('Object <' + obj + '> is not an instance of <' + throw new Error('Object <' + obj + '> is not an instance of <' +
(type.name || type) + '>' + (type.name || type) + '>' +
(actualTypeName ? ' but of < ' + actualTypeName + '>' : '')); (actualTypeName ? ' but of < ' + actualTypeName + '>' : ''));
} }
} }
// 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);
}
// 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)
...@@ -101,7 +101,6 @@ ...@@ -101,7 +101,6 @@
###### END REGEXP SUBCLASSING SECTION ###### ###### END REGEXP SUBCLASSING SECTION ######
# https://code.google.com/p/v8/issues/detail?id=4360 # 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/DateTimeFormat/12.1.1_1': [FAIL],
'intl402/NumberFormat/11.1.1_1': [FAIL], 'intl402/NumberFormat/11.1.1_1': [FAIL],
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment