Add Array support for @@species and subclassing

This patch implements @@species, guarded behind the --harmony-species
flag, on Arrays. Methods which return an Array will instead return
the appropriate instance based on the ArraySpeciesCreate algorithm.
The algorithm is implemented in C++ to get access to realm information
and to implement some Array methods in C++, but it is also accessed
from JavaScript through a new runtime function. A couple interactive
Octane runs show no performance regression with the flag turned off,
but turning --harmony-species on will surely have a significant
regression, as Array methods now heavily use ObjectDefineProperty.

BUG=v8:4093
LOG=Y
R=adamk,cbruni

Review URL: https://codereview.chromium.org/1560763002

Cr-Commit-Position: refs/heads/master@{#33144}
This commit is contained in:
littledan 2016-01-06 18:29:50 -08:00 committed by Commit bot
parent 48bc94253f
commit 6e96223750
11 changed files with 314 additions and 59 deletions

View File

@ -458,6 +458,14 @@ BUILTIN(ArraySlice) {
int relative_end = 0;
bool is_sloppy_arguments = false;
// TODO(littledan): Look up @@species only once, not once here and
// again in the JS builtin. Pass the species out?
Handle<Object> species;
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, species, Object::ArraySpeciesConstructor(isolate, receiver));
if (*species != isolate->context()->native_context()->array_function()) {
return CallJsIntrinsic(isolate, isolate->array_slice(), args);
}
if (receiver->IsJSArray()) {
DisallowHeapAllocation no_gc;
JSArray* array = JSArray::cast(*receiver);
@ -543,6 +551,14 @@ BUILTIN(ArraySplice) {
if (!maybe_elms_obj.ToHandle(&elms_obj)) {
return CallJsIntrinsic(isolate, isolate->array_splice(), args);
}
// TODO(littledan): Look up @@species only once, not once here and
// again in the JS builtin. Pass the species out?
Handle<Object> species;
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, species, Object::ArraySpeciesConstructor(isolate, receiver));
if (*species != isolate->context()->native_context()->array_function()) {
return CallJsIntrinsic(isolate, isolate->array_splice(), args);
}
Handle<JSArray> array = Handle<JSArray>::cast(receiver);
DCHECK(!array->map()->is_observed());

View File

@ -13,6 +13,7 @@
var AddIndexedProperty;
var FLAG_harmony_tolength;
var FLAG_harmony_species;
var GetIterator;
var GetMethod;
var GlobalArray = global.Array;
@ -48,10 +49,35 @@ utils.Import(function(from) {
utils.ImportFromExperimental(function(from) {
FLAG_harmony_tolength = from.FLAG_harmony_tolength;
FLAG_harmony_species = from.FLAG_harmony_species;
});
// -------------------------------------------------------------------
function ArraySpeciesCreate(array, length) {
var constructor;
if (FLAG_harmony_species) {
constructor = %ArraySpeciesConstructor(array);
} else {
constructor = GlobalArray;
}
return new constructor(length);
}
function DefineIndexedProperty(array, i, value) {
if (FLAG_harmony_species) {
var result = ObjectDefineProperty(array, i, {
value: value, writable: true, configurable: true, enumerable: true
});
if (!result) throw MakeTypeError(kStrictCannotAssign, i);
} else {
AddIndexedProperty(array, i, value);
}
}
// Global list of arrays visited during toString, toLocaleString and
// join invocations.
var visited_arrays = new InternalArray();
@ -251,7 +277,7 @@ function SparseSlice(array, start_i, del_count, len, deleted_elements) {
for (var i = start_i; i < limit; ++i) {
var current = array[i];
if (!IS_UNDEFINED(current) || i in array) {
AddIndexedProperty(deleted_elements, i - start_i, current);
DefineIndexedProperty(deleted_elements, i - start_i, current);
}
}
} else {
@ -262,7 +288,7 @@ function SparseSlice(array, start_i, del_count, len, deleted_elements) {
if (key >= start_i) {
var current = array[key];
if (!IS_UNDEFINED(current) || key in array) {
AddIndexedProperty(deleted_elements, key - start_i, current);
DefineIndexedProperty(deleted_elements, key - start_i, current);
}
}
}
@ -342,9 +368,7 @@ function SimpleSlice(array, start_i, del_count, len, deleted_elements) {
var index = start_i + i;
if (HAS_INDEX(array, index, is_array)) {
var current = array[index];
// The spec requires [[DefineOwnProperty]] here, AddIndexedProperty is
// close enough (in that it ignores the prototype).
AddIndexedProperty(deleted_elements, i, current);
DefineIndexedProperty(deleted_elements, i, current);
}
}
}
@ -759,7 +783,7 @@ function ArraySlice(start, end) {
if (end_i > len) end_i = len;
}
var result = [];
var result = ArraySpeciesCreate(array, MaxSimple(end_i - start_i, 0));
if (end_i < start_i) return result;
@ -861,7 +885,7 @@ function ArraySplice(start, delete_count) {
var start_i = ComputeSpliceStartIndex(TO_INTEGER(start), len);
var del_count = ComputeSpliceDeleteCount(delete_count, num_arguments, len,
start_i);
var deleted_elements = [];
var deleted_elements = ArraySpeciesCreate(array, del_count);
deleted_elements.length = del_count;
var num_elements_to_add = num_arguments > 2 ? num_arguments - 2 : 0;
@ -1201,26 +1225,23 @@ function ArraySort(comparefn) {
// The following functions cannot be made efficient on sparse arrays while
// preserving the semantics, since the calls to the receiver function can add
// or delete elements from the array.
function InnerArrayFilter(f, receiver, array, length) {
if (!IS_CALLABLE(f)) throw MakeTypeError(kCalledNonCallable, f);
var accumulator = new InternalArray();
var accumulator_length = 0;
function InnerArrayFilter(f, receiver, array, length, result) {
var result_length = 0;
var is_array = IS_ARRAY(array);
for (var i = 0; i < length; i++) {
if (HAS_INDEX(array, i, is_array)) {
var element = array[i];
if (%_Call(f, receiver, element, i, array)) {
accumulator[accumulator_length++] = element;
DefineIndexedProperty(result, result_length, element);
result_length++;
}
}
}
var result = new GlobalArray();
%MoveArrayContents(accumulator, result);
return result;
}
function ArrayFilter(f, receiver) {
CHECK_OBJECT_COERCIBLE(this, "Array.prototype.filter");
@ -1228,7 +1249,9 @@ function ArrayFilter(f, receiver) {
// loop will not affect the looping and side effects are visible.
var array = TO_OBJECT(this);
var length = TO_LENGTH_OR_UINT32(array.length);
return InnerArrayFilter(f, receiver, array, length);
if (!IS_CALLABLE(f)) throw MakeTypeError(kCalledNonCallable, f);
var result = ArraySpeciesCreate(array, 0);
return InnerArrayFilter(f, receiver, array, length, result);
}
@ -1307,23 +1330,6 @@ function ArrayEvery(f, receiver) {
}
function InnerArrayMap(f, receiver, array, length) {
if (!IS_CALLABLE(f)) throw MakeTypeError(kCalledNonCallable, f);
var accumulator = new InternalArray(length);
var is_array = IS_ARRAY(array);
for (var i = 0; i < length; i++) {
if (HAS_INDEX(array, i, is_array)) {
var element = array[i];
accumulator[i] = %_Call(f, receiver, element, i, array);
}
}
var result = new GlobalArray();
%MoveArrayContents(accumulator, result);
return result;
}
function ArrayMap(f, receiver) {
CHECK_OBJECT_COERCIBLE(this, "Array.prototype.map");
@ -1331,7 +1337,16 @@ function ArrayMap(f, receiver) {
// loop will not affect the looping and side effects are visible.
var array = TO_OBJECT(this);
var length = TO_LENGTH_OR_UINT32(array.length);
return InnerArrayMap(f, receiver, array, length);
if (!IS_CALLABLE(f)) throw MakeTypeError(kCalledNonCallable, f);
var result = ArraySpeciesCreate(array, length);
var is_array = IS_ARRAY(array);
for (var i = 0; i < length; i++) {
if (HAS_INDEX(array, i, is_array)) {
var element = array[i];
DefineIndexedProperty(result, i, %_Call(f, receiver, element, i, array));
}
}
return result;
}
@ -1948,7 +1963,6 @@ utils.Export(function(to) {
to.InnerArrayIndexOf = InnerArrayIndexOf;
to.InnerArrayJoin = InnerArrayJoin;
to.InnerArrayLastIndexOf = InnerArrayLastIndexOf;
to.InnerArrayMap = InnerArrayMap;
to.InnerArrayReduce = InnerArrayReduce;
to.InnerArrayReduceRight = InnerArrayReduceRight;
to.InnerArraySome = InnerArraySome;

View File

@ -173,19 +173,6 @@ function PostNatives(utils) {
"ErrorToString",
"GetIterator",
"GetMethod",
"InnerArrayEvery",
"InnerArrayFilter",
"InnerArrayForEach",
"InnerArrayIndexOf",
"InnerArrayJoin",
"InnerArrayLastIndexOf",
"InnerArrayMap",
"InnerArrayReduce",
"InnerArrayReduceRight",
"InnerArrayReverse",
"InnerArraySome",
"InnerArraySort",
"InnerArrayToLocaleString",
"IsNaN",
"MakeError",
"MakeTypeError",

View File

@ -29,7 +29,6 @@ var InnerArrayIncludes;
var InnerArrayIndexOf;
var InnerArrayJoin;
var InnerArrayLastIndexOf;
var InnerArrayMap;
var InnerArrayReduce;
var InnerArrayReduceRight;
var InnerArraySome;
@ -80,7 +79,6 @@ utils.Import(function(from) {
InnerArrayIndexOf = from.InnerArrayIndexOf;
InnerArrayJoin = from.InnerArrayJoin;
InnerArrayLastIndexOf = from.InnerArrayLastIndexOf;
InnerArrayMap = from.InnerArrayMap;
InnerArrayReduce = from.InnerArrayReduce;
InnerArrayReduceRight = from.InnerArrayReduceRight;
InnerArraySome = from.InnerArraySome;
@ -493,12 +491,19 @@ function TypedArrayFill(value, start, end) {
// ES6 draft 07-15-13, section 22.2.3.9
function TypedArrayFilter(predicate, thisArg) {
function TypedArrayFilter(f, thisArg) {
if (!%_IsTypedArray(this)) throw MakeTypeError(kNotTypedArray);
var length = %_TypedArrayGetLength(this);
var array = InnerArrayFilter(predicate, thisArg, this, length);
return ConstructTypedArrayLike(this, array);
if (!IS_CALLABLE(f)) throw MakeTypeError(kCalledNonCallable, f);
var result = new InternalArray();
InnerArrayFilter(f, thisArg, this, length, result);
var captured = result.length;
var output = ConstructTypedArrayLike(this, captured);
for (var i = 0; i < captured; i++) {
output[i] = result[i];
}
return output;
}
%FunctionSetLength(TypedArrayFilter, 1);
@ -592,14 +597,17 @@ function TypedArrayLastIndexOf(element, index) {
// ES6 draft 07-15-13, section 22.2.3.18
function TypedArrayMap(predicate, thisArg) {
function TypedArrayMap(f, thisArg) {
if (!%_IsTypedArray(this)) throw MakeTypeError(kNotTypedArray);
// TODO(littledan): Preallocate rather than making an intermediate
// InternalArray, for better performance.
var length = %_TypedArrayGetLength(this);
var array = InnerArrayMap(predicate, thisArg, this, length);
return ConstructTypedArrayLike(this, array);
var result = ConstructTypedArrayLike(this, length);
if (!IS_CALLABLE(f)) throw MakeTypeError(kCalledNonCallable, f);
for (var i = 0; i < length; i++) {
var element = this[i];
result[i] = %_Call(f, thisArg, element, i, this);
}
return result;
}
%FunctionSetLength(TypedArrayMap, 1);

View File

@ -423,6 +423,8 @@ class CallSite {
T(SloppyLexical, \
"Block-scoped declarations (let, const, function, class) not yet " \
"supported outside strict mode") \
T(SpeciesNotConstructor, \
"object.constructor[Symbol.species] is not a constructor") \
T(StrictDelete, "Delete of an unqualified identifier in strict mode.") \
T(StrictEvalArguments, "Unexpected eval or arguments in strict mode") \
T(StrictFunction, \

View File

@ -1605,6 +1605,56 @@ bool Object::SameValueZero(Object* other) {
}
MaybeHandle<Object> Object::ArraySpeciesConstructor(
Isolate* isolate, Handle<Object> original_array) {
Handle<Context> native_context = isolate->native_context();
if (!FLAG_harmony_species) {
return Handle<Object>(native_context->array_function(), isolate);
}
Handle<Object> constructor = isolate->factory()->undefined_value();
Maybe<bool> is_array = Object::IsArray(original_array);
MAYBE_RETURN_NULL(is_array);
if (is_array.FromJust()) {
ASSIGN_RETURN_ON_EXCEPTION(
isolate, constructor,
Object::GetProperty(original_array,
isolate->factory()->constructor_string()),
Object);
if (constructor->IsConstructor()) {
Handle<Context> constructor_context;
ASSIGN_RETURN_ON_EXCEPTION(
isolate, constructor_context,
JSReceiver::GetFunctionRealm(Handle<JSReceiver>::cast(constructor)),
Object);
if (*constructor_context != *native_context &&
*constructor == constructor_context->array_function()) {
constructor = isolate->factory()->undefined_value();
}
}
if (constructor->IsJSReceiver()) {
ASSIGN_RETURN_ON_EXCEPTION(
isolate, constructor,
Object::GetProperty(constructor,
isolate->factory()->species_symbol()),
Object);
if (constructor->IsNull()) {
constructor = isolate->factory()->undefined_value();
}
}
}
if (constructor->IsUndefined()) {
return Handle<Object>(native_context->array_function(), isolate);
} else {
if (!constructor->IsConstructor()) {
THROW_NEW_ERROR(isolate,
NewTypeError(MessageTemplate::kSpeciesNotConstructor),
Object);
}
return constructor;
}
}
void Object::ShortPrint(FILE* out) {
OFStream os(out);
os << Brief(this);

View File

@ -1346,6 +1346,10 @@ class Object {
// by ES6 Map and Set.
bool SameValueZero(Object* other);
// ES6 section 9.4.2.3 ArraySpeciesCreate (part of it)
MUST_USE_RESULT static MaybeHandle<Object> ArraySpeciesConstructor(
Isolate* isolate, Handle<Object> original_array);
// Tries to convert an object to an array length. Returns true and sets the
// output parameter if it succeeds.
inline bool ToArrayLength(uint32_t* index);

View File

@ -487,5 +487,18 @@ RUNTIME_FUNCTION(Runtime_FastOneByteArrayJoin) {
// to a slow path.
return isolate->heap()->undefined_value();
}
RUNTIME_FUNCTION(Runtime_ArraySpeciesConstructor) {
HandleScope scope(isolate);
DCHECK(args.length() == 1);
CONVERT_ARG_HANDLE_CHECKED(Object, original_array, 0);
Handle<Object> constructor;
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, constructor,
Object::ArraySpeciesConstructor(isolate, original_array));
return *constructor;
}
} // namespace internal
} // namespace v8

View File

@ -50,7 +50,8 @@ namespace internal {
F(GetCachedArrayIndex, 1, 1) \
F(FixedArrayGet, 2, 1) \
F(FixedArraySet, 3, 1) \
F(FastOneByteArrayJoin, 2, 1)
F(FastOneByteArrayJoin, 2, 1) \
F(ArraySpeciesConstructor, 1, 1)
#define FOR_EACH_INTRINSIC_ATOMICS(F) \

View File

@ -0,0 +1,156 @@
// 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.
// Flags: --harmony-species --harmony-proxies
// Test the ES2015 @@species feature
'use strict';
// Subclasses of Array construct themselves under map, etc
class MyArray extends Array { }
assertEquals(MyArray, new MyArray().map(()=>{}).constructor);
assertEquals(MyArray, new MyArray().filter(()=>{}).constructor);
assertEquals(MyArray, new MyArray().slice().constructor);
assertEquals(MyArray, new MyArray().splice().constructor);
// Subclasses can override @@species to return the another class
class MyOtherArray extends Array {
static get [Symbol.species]() { return MyArray; }
}
assertEquals(MyArray, new MyOtherArray().map(()=>{}).constructor);
assertEquals(MyArray, new MyOtherArray().filter(()=>{}).constructor);
assertEquals(MyArray, new MyOtherArray().slice().constructor);
assertEquals(MyArray, new MyOtherArray().splice().constructor);
// Array methods on non-arrays return arrays
class MyNonArray extends Array {
static get [Symbol.species]() { return MyObject; }
}
class MyObject { }
assertEquals(MyObject,
Array.prototype.map.call(new MyNonArray(), ()=>{}).constructor);
assertEquals(MyObject,
Array.prototype.filter.call(new MyNonArray(), ()=>{}).constructor);
assertEquals(MyObject,
Array.prototype.slice.call(new MyNonArray()).constructor);
assertEquals(MyObject,
Array.prototype.splice.call(new MyNonArray()).constructor);
assertEquals(undefined,
Array.prototype.map.call(new MyNonArray(), ()=>{}).length);
assertEquals(undefined,
Array.prototype.filter.call(new MyNonArray(), ()=>{}).length);
// slice and splice actually do explicitly define the length for some reason
assertEquals(0, Array.prototype.slice.call(new MyNonArray()).length);
assertEquals(0, Array.prototype.splice.call(new MyNonArray()).length);
// Cross-realm Arrays build same-realm arrays
var realm = Realm.create();
assertEquals(Array,
Array.prototype.map.call(
Realm.eval(realm, "[]"), ()=>{}).constructor);
assertFalse(Array === Realm.eval(realm, "[]").map(()=>{}).constructor);
assertFalse(Array === Realm.eval(realm, "[].map(()=>{}).constructor"));
// Defaults when constructor or @@species is missing or non-constructor
class MyDefaultArray extends Array {
static get [Symbol.species]() { return undefined; }
}
assertEquals(Array, new MyDefaultArray().map(()=>{}).constructor);
class MyOtherDefaultArray extends Array { }
assertEquals(MyOtherDefaultArray,
new MyOtherDefaultArray().map(()=>{}).constructor);
MyOtherDefaultArray.prototype.constructor = undefined;
assertEquals(Array, new MyOtherDefaultArray().map(()=>{}).constructor);
// Exceptions propagated when getting constructor @@species throws
class SpeciesError extends Error { }
class ConstructorError extends Error { }
class MyThrowingArray extends Array {
static get [Symbol.species]() { throw new SpeciesError; }
}
assertThrows(() => new MyThrowingArray().map(()=>{}), SpeciesError);
Object.defineProperty(MyThrowingArray.prototype, 'constructor', {
get() { throw new ConstructorError; }
});
assertThrows(() => new MyThrowingArray().map(()=>{}), ConstructorError);
// Previously unexpected errors from setting properties in arrays throw
class FrozenArray extends Array {
constructor(...args) {
super(...args);
Object.freeze(this);
}
}
assertThrows(() => new FrozenArray([1]).map(()=>0), TypeError);
assertThrows(() => new FrozenArray([1]).filter(()=>true), TypeError);
assertThrows(() => new FrozenArray([1]).slice(0, 1), TypeError);
assertThrows(() => new FrozenArray([1]).splice(0, 1), TypeError);
// Verify call counts and constructor parameters
var count;
var params;
class MyObservedArray extends Array {
constructor(...args) {
super(...args);
params = args;
}
static get [Symbol.species]() {
count++
return this;
}
}
count = 0;
params = undefined;
assertEquals(MyObservedArray,
new MyObservedArray().map(()=>{}).constructor);
assertEquals(1, count);
assertArrayEquals([0], params);
count = 0;
params = undefined;
assertEquals(MyObservedArray,
new MyObservedArray().filter(()=>{}).constructor);
assertEquals(1, count);
assertArrayEquals([0], params);
count = 0;
params = undefined;
assertEquals(MyObservedArray,
new MyObservedArray().slice().constructor);
// TODO(littledan): Should be 1
assertEquals(2, count);
assertArrayEquals([0], params);
count = 0;
params = undefined;
assertEquals(MyObservedArray,
new MyObservedArray().splice().constructor);
// TODO(littledan): Should be 1
assertEquals(2, count);
assertArrayEquals([0], params);
// @@species constructor can be a Proxy, and the realm access doesn't
// crash
class MyProxyArray extends Array { }
let ProxyArray = new Proxy(MyProxyArray, {});
MyProxyArray.constructor = ProxyArray;
assertEquals(MyProxyArray, new ProxyArray().map(()=>{}).constructor);

View File

@ -56,6 +56,10 @@
'es6/debug-promises/reject-with-invalid-reject': [FAIL],
'es6/debug-promises/throw-with-undefined-reject': [FAIL],
# Issue 4093: This test will be updated in the course of implementing
# @@species, when TypedArray support is added.
'regress/regress-544991': [SKIP],
##############################################################################
# TurboFan compiler failures.