Implement ES6 @@isConcatSpreadable / Array.prototype.concat

Add support for Symbol.isConcatSpreadable in Array.prototype.concat. This enables spreading non-Array objects with the symbol.

LOG=N
R=dslomov@chromium.org
BUG=

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

Cr-Commit-Position: refs/heads/master@{#25808}
This commit is contained in:
caitpotter88 2014-12-12 10:38:40 -08:00 committed by Commit bot
parent 10b38df268
commit 48054170e9
4 changed files with 546 additions and 21 deletions

View File

@ -192,6 +192,17 @@ function ArrayOf() {
// -------------------------------------------------------------------
function HarmonyArrayExtendSymbolPrototype() {
%CheckIsBootstrapping();
InstallConstants($Symbol, $Array(
// TODO(dslomov, caitp): Move to symbol.js when shipping
"isConcatSpreadable", symbolIsConcatSpreadable
));
}
HarmonyArrayExtendSymbolPrototype();
function HarmonyArrayExtendArrayPrototype() {
%CheckIsBootstrapping();

View File

@ -626,6 +626,15 @@ function IsPrimitive(x) {
}
// ES6, draft 10-14-14, section 22.1.3.1.1
function IsConcatSpreadable(O) {
if (!IS_SPEC_OBJECT(O)) return false;
var spreadable = O[symbolIsConcatSpreadable];
if (IS_UNDEFINED(spreadable)) return IS_ARRAY(O);
return ToBoolean(spreadable);
}
// ECMA-262, section 8.6.2.6, page 28.
function DefaultNumber(x) {
if (!IS_SYMBOL_WRAPPER(x)) {

View File

@ -289,11 +289,11 @@ static uint32_t EstimateElementCount(Handle<JSArray> array) {
template <class ExternalArrayClass, class ElementType>
static void IterateExternalArrayElements(Isolate* isolate,
Handle<JSObject> receiver,
bool elements_are_ints,
bool elements_are_guaranteed_smis,
ArrayConcatVisitor* visitor) {
static void IterateTypedArrayElements(Isolate* isolate,
Handle<JSObject> receiver,
bool elements_are_ints,
bool elements_are_guaranteed_smis,
ArrayConcatVisitor* visitor) {
Handle<ExternalArrayClass> array(
ExternalArrayClass::cast(receiver->elements()));
uint32_t len = static_cast<uint32_t>(array->length());
@ -440,7 +440,7 @@ static void CollectElementIndices(Handle<JSObject> object, uint32_t range,
/**
* A helper function that visits elements of a JSArray in numerical
* A helper function that visits elements of a JSObject in numerical
* order.
*
* The visitor argument called for each existing element in the array
@ -449,9 +449,22 @@ static void CollectElementIndices(Handle<JSObject> object, uint32_t range,
* length.
* Returns false if any access threw an exception, otherwise true.
*/
static bool IterateElements(Isolate* isolate, Handle<JSArray> receiver,
static bool IterateElements(Isolate* isolate, Handle<JSObject> receiver,
ArrayConcatVisitor* visitor) {
uint32_t length = static_cast<uint32_t>(receiver->length()->Number());
uint32_t length = 0;
if (receiver->IsJSArray()) {
Handle<JSArray> array(Handle<JSArray>::cast(receiver));
length = static_cast<uint32_t>(array->length()->Number());
} else {
Handle<Object> val;
Handle<Object> key(isolate->heap()->length_string(), isolate);
ASSIGN_RETURN_ON_EXCEPTION_VALUE(isolate, val,
Runtime::GetObjectProperty(isolate, receiver, key), false);
// TODO(caitp): implement ToLength() abstract operation for C++
val->ToUint32(&length);
}
switch (receiver->GetElementsKind()) {
case FAST_SMI_ELEMENTS:
case FAST_ELEMENTS:
@ -552,55 +565,132 @@ static bool IterateElements(Isolate* isolate, Handle<JSArray> receiver,
}
break;
}
case UINT8_CLAMPED_ELEMENTS: {
Handle<FixedUint8ClampedArray> pixels(
FixedUint8ClampedArray::cast(receiver->elements()));
for (uint32_t j = 0; j < length; j++) {
Handle<Smi> e(Smi::FromInt(pixels->get_scalar(j)), isolate);
visitor->visit(j, e);
}
break;
}
case EXTERNAL_INT8_ELEMENTS: {
IterateExternalArrayElements<ExternalInt8Array, int8_t>(
IterateTypedArrayElements<ExternalInt8Array, int8_t>(
isolate, receiver, true, true, visitor);
break;
}
case INT8_ELEMENTS: {
IterateTypedArrayElements<FixedInt8Array, int8_t>(
isolate, receiver, true, true, visitor);
break;
}
case EXTERNAL_UINT8_ELEMENTS: {
IterateExternalArrayElements<ExternalUint8Array, uint8_t>(
IterateTypedArrayElements<ExternalUint8Array, uint8_t>(
isolate, receiver, true, true, visitor);
break;
}
case UINT8_ELEMENTS: {
IterateTypedArrayElements<FixedUint8Array, uint8_t>(
isolate, receiver, true, true, visitor);
break;
}
case EXTERNAL_INT16_ELEMENTS: {
IterateExternalArrayElements<ExternalInt16Array, int16_t>(
IterateTypedArrayElements<ExternalInt16Array, int16_t>(
isolate, receiver, true, true, visitor);
break;
}
case INT16_ELEMENTS: {
IterateTypedArrayElements<FixedInt16Array, int16_t>(
isolate, receiver, true, true, visitor);
break;
}
case EXTERNAL_UINT16_ELEMENTS: {
IterateExternalArrayElements<ExternalUint16Array, uint16_t>(
IterateTypedArrayElements<ExternalUint16Array, uint16_t>(
isolate, receiver, true, true, visitor);
break;
}
case UINT16_ELEMENTS: {
IterateTypedArrayElements<FixedUint16Array, uint16_t>(
isolate, receiver, true, true, visitor);
break;
}
case EXTERNAL_INT32_ELEMENTS: {
IterateExternalArrayElements<ExternalInt32Array, int32_t>(
IterateTypedArrayElements<ExternalInt32Array, int32_t>(
isolate, receiver, true, false, visitor);
break;
}
case INT32_ELEMENTS: {
IterateTypedArrayElements<FixedInt32Array, int32_t>(
isolate, receiver, true, false, visitor);
break;
}
case EXTERNAL_UINT32_ELEMENTS: {
IterateExternalArrayElements<ExternalUint32Array, uint32_t>(
IterateTypedArrayElements<ExternalUint32Array, uint32_t>(
isolate, receiver, true, false, visitor);
break;
}
case UINT32_ELEMENTS: {
IterateTypedArrayElements<FixedUint32Array, uint32_t>(
isolate, receiver, true, false, visitor);
break;
}
case EXTERNAL_FLOAT32_ELEMENTS: {
IterateExternalArrayElements<ExternalFloat32Array, float>(
IterateTypedArrayElements<ExternalFloat32Array, float>(
isolate, receiver, false, false, visitor);
break;
}
case FLOAT32_ELEMENTS: {
IterateTypedArrayElements<FixedFloat32Array, float>(
isolate, receiver, false, false, visitor);
break;
}
case EXTERNAL_FLOAT64_ELEMENTS: {
IterateExternalArrayElements<ExternalFloat64Array, double>(
IterateTypedArrayElements<ExternalFloat64Array, double>(
isolate, receiver, false, false, visitor);
break;
}
default:
UNREACHABLE();
case FLOAT64_ELEMENTS: {
IterateTypedArrayElements<FixedFloat64Array, double>(
isolate, receiver, false, false, visitor);
break;
}
case SLOPPY_ARGUMENTS_ELEMENTS: {
ElementsAccessor* accessor = receiver->GetElementsAccessor();
for (uint32_t index = 0; index < length; index++) {
HandleScope loop_scope(isolate);
if (accessor->HasElement(receiver, receiver, index)) {
Handle<Object> element;
ASSIGN_RETURN_ON_EXCEPTION_VALUE(
isolate, element, accessor->Get(receiver, receiver, index),
false);
visitor->visit(index, element);
}
}
break;
}
}
visitor->increase_index_offset(length);
return true;
}
static bool IsConcatSpreadable(Isolate* isolate, Handle<Object> obj) {
HandleScope handle_scope(isolate);
if (!obj->IsSpecObject()) return false;
if (obj->IsJSArray()) return true;
if (FLAG_harmony_arrays) {
Handle<Symbol> key(isolate->factory()->is_concat_spreadable_symbol());
Handle<Object> value;
MaybeHandle<Object> maybeValue =
i::Runtime::GetObjectProperty(isolate, obj, key);
if (maybeValue.ToHandle(&value)) {
return value->BooleanValue();
}
}
return false;
}
/**
* Array::concat implementation.
* See ECMAScript 262, 15.4.4.4.
@ -771,9 +861,11 @@ RUNTIME_FUNCTION(Runtime_ArrayConcat) {
for (int i = 0; i < argument_count; i++) {
Handle<Object> obj(elements->get(i), isolate);
if (obj->IsJSArray()) {
Handle<JSArray> array = Handle<JSArray>::cast(obj);
if (!IterateElements(isolate, array, &visitor)) {
bool spreadable = IsConcatSpreadable(isolate, obj);
if (isolate->has_pending_exception()) return isolate->heap()->exception();
if (spreadable) {
Handle<JSObject> object = Handle<JSObject>::cast(obj);
if (!IterateElements(isolate, object, &visitor)) {
return isolate->heap()->exception();
}
} else {

View File

@ -0,0 +1,413 @@
// Copyright 2014 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-arrays --harmony-classes
(function testArrayConcatArity() {
"use strict";
assertEquals(1, Array.prototype.concat.length);
})();
(function testArrayConcatNoPrototype() {
"use strict";
assertEquals(void 0, Array.prototype.concat.prototype);
})();
(function testArrayConcatDescriptor() {
"use strict";
var desc = Object.getOwnPropertyDescriptor(Array.prototype, 'concat');
assertEquals(false, desc.enumerable);
})();
(function testConcatArrayLike() {
"use strict";
var obj = {
"length": 6,
"1": "A",
"3": "B",
"5": "C"
};
obj[Symbol.isConcatSpreadable] = true;
var obj2 = { length: 3, "0": "0", "1": "1", "2": "2" };
var arr = ["X", "Y", "Z"];
assertEquals([void 0, "A", void 0, "B", void 0, "C",
{ "length": 3, "0": "0", "1": "1", "2": "2" },
"X", "Y", "Z"], Array.prototype.concat.call(obj, obj2, arr));
})();
(function testConcatHoleyArray() {
"use strict";
var arr = [];
arr[4] = "Item 4";
arr[8] = "Item 8";
var arr2 = [".", "!", "?"];
assertEquals([void 0, void 0, void 0, void 0, "Item 4", void 0, void 0,
void 0, "Item 8", ".", "!", "?"], arr.concat(arr2));
})();
(function testIsConcatSpreadableGetterThrows() {
"use strict";
function MyError() {}
var obj = {};
Object.defineProperty(obj, Symbol.isConcatSpreadable, {
get: function() { throw new MyError(); }
});
assertThrows(function() {
[].concat(obj);
}, MyError);
assertThrows(function() {
Array.prototype.concat.call(obj, 1, 2, 3);
}, MyError);
})();
(function testConcatLengthThrows() {
"use strict";
function MyError() {}
var obj = {};
obj[Symbol.isConcatSpreadable] = true;
Object.defineProperty(obj, "length", {
get: function() { throw new MyError(); }
});
assertThrows(function() {
[].concat(obj);
}, MyError);
assertThrows(function() {
Array.prototype.concat.call(obj, 1, 2, 3);
}, MyError);
})();
(function testConcatArraySubclass() {
"use strict";
// TODO(caitp): when concat is called on instances of classes which extend
// Array, they should:
//
// - return an instance of the class, rather than an Array instance (if from
// same Realm)
// - always treat such classes as concat-spreadable
})();
(function testConcatNonArray() {
"use strict";
class NonArray {
constructor() { Array.apply(this, arguments); }
};
var obj = new NonArray(1,2,3);
var result = Array.prototype.concat.call(obj, 4, 5, 6);
assertEquals(Array, result.constructor);
assertEquals([obj,4,5,6], result);
assertFalse(result instanceof NonArray);
})();
function testConcatTypedArray(type, elems, modulo) {
"use strict";
var items = new Array(elems);
var ta_by_len = new type(elems);
for (var i = 0; i < elems; ++i) {
ta_by_len[i] = items[i] = modulo === false ? i : elems % modulo;
}
var ta = new type(items);
assertEquals([ta, ta], [].concat(ta, ta));
ta[Symbol.isConcatSpreadable] = true;
assertEquals(items, [].concat(ta));
assertEquals([ta_by_len, ta_by_len], [].concat(ta_by_len, ta_by_len));
ta_by_len[Symbol.isConcatSpreadable] = true;
assertEquals(items, [].concat(ta_by_len));
}
(function testConcatSmallTypedArray() {
var max = [2^8, 2^16, 2^32, false, false];
[
Uint8Array,
Uint16Array,
Uint32Array,
Float32Array,
Float64Array
].forEach(function(ctor, i) {
testConcatTypedArray(ctor, 1, max[i]);
});
})();
(function testConcatLargeTypedArray() {
var max = [2^8, 2^16, 2^32, false, false];
[
Uint8Array,
Uint16Array,
Uint32Array,
Float32Array,
Float64Array
].forEach(function(ctor, i) {
testConcatTypedArray(ctor, 4000, max[i]);
});
})();
(function testConcatStrictArguments() {
var args = (function(a, b, c) { "use strict"; return arguments; })(1,2,3);
args[Symbol.isConcatSpreadable] = true;
assertEquals([1, 2, 3, 1, 2, 3], [].concat(args, args));
})();
(function testConcatSloppyArguments() {
var args = (function(a, b, c) { return arguments; })(1,2,3);
args[Symbol.isConcatSpreadable] = true;
assertEquals([1, 2, 3, 1, 2, 3], [].concat(args, args));
})();
(function testConcatSloppyArgumentsWithDupes() {
var args = (function(a, a, a) { return arguments; })(1,2,3);
args[Symbol.isConcatSpreadable] = true;
assertEquals([1, 2, 3, 1, 2, 3], [].concat(args, args));
})();
(function testConcatSloppyArgumentsThrows() {
function MyError() {}
var args = (function(a) { return arguments; })(1,2,3);
Object.defineProperty(args, 0, {
get: function() { throw new MyError(); }
});
args[Symbol.isConcatSpreadable] = true;
assertThrows(function() {
return [].concat(args, args);
}, MyError);
})();
(function testConcatHoleySloppyArguments() {
var args = (function(a) { return arguments; })(1,2,3);
delete args[1];
args[Symbol.isConcatSpreadable] = true;
assertEquals([1, void 0, 3, 1, void 0, 3], [].concat(args, args));
})();
// ES5 tests
(function testArrayConcatES5() {
"use strict";
var poses;
var pos;
poses = [140, 4000000000];
while (pos = poses.shift()) {
var a = new Array(pos);
var array_proto = [];
a.__proto__ = array_proto;
assertEquals(pos, a.length);
a.push('foo');
assertEquals(pos + 1, a.length);
var b = ['bar'];
var c = a.concat(b);
assertEquals(pos + 2, c.length);
assertEquals("undefined", typeof(c[pos - 1]));
assertEquals("foo", c[pos]);
assertEquals("bar", c[pos + 1]);
// Can we fool the system by putting a number in a string?
var onetwofour = "124";
a[onetwofour] = 'doo';
assertEquals(a[124], 'doo');
c = a.concat(b);
assertEquals(c[124], 'doo');
// If we put a number in the prototype, then the spec says it should be
// copied on concat.
array_proto["123"] = 'baz';
assertEquals(a[123], 'baz');
c = a.concat(b);
assertEquals(pos + 2, c.length);
assertEquals("baz", c[123]);
assertEquals("undefined", typeof(c[pos - 1]));
assertEquals("foo", c[pos]);
assertEquals("bar", c[pos + 1]);
// When we take the number off the prototype it disappears from a, but
// the concat put it in c itself.
array_proto["123"] = undefined;
assertEquals("undefined", typeof(a[123]));
assertEquals("baz", c[123]);
// If the element of prototype is shadowed, the element on the instance
// should be copied, but not the one on the prototype.
array_proto[123] = 'baz';
a[123] = 'xyz';
assertEquals('xyz', a[123]);
c = a.concat(b);
assertEquals('xyz', c[123]);
// Non-numeric properties on the prototype or the array shouldn't get
// copied.
array_proto.moe = 'joe';
a.ben = 'jerry';
assertEquals(a["moe"], 'joe');
assertEquals(a["ben"], 'jerry');
c = a.concat(b);
// ben was not copied
assertEquals("undefined", typeof(c.ben));
// When we take moe off the prototype it disappears from all arrays.
array_proto.moe = undefined;
assertEquals("undefined", typeof(c.moe));
// Negative indices don't get concated.
a[-1] = 'minus1';
assertEquals("minus1", a[-1]);
assertEquals("undefined", typeof(a[0xffffffff]));
c = a.concat(b);
assertEquals("undefined", typeof(c[-1]));
assertEquals("undefined", typeof(c[0xffffffff]));
assertEquals(c.length, a.length + 1);
}
poses = [140, 4000000000];
while (pos = poses.shift()) {
var a = new Array(pos);
assertEquals(pos, a.length);
a.push('foo');
assertEquals(pos + 1, a.length);
var b = ['bar'];
var c = a.concat(b);
assertEquals(pos + 2, c.length);
assertEquals("undefined", typeof(c[pos - 1]));
assertEquals("foo", c[pos]);
assertEquals("bar", c[pos + 1]);
// Can we fool the system by putting a number in a string?
var onetwofour = "124";
a[onetwofour] = 'doo';
assertEquals(a[124], 'doo');
c = a.concat(b);
assertEquals(c[124], 'doo');
// If we put a number in the prototype, then the spec says it should be
// copied on concat.
Array.prototype["123"] = 'baz';
assertEquals(a[123], 'baz');
c = a.concat(b);
assertEquals(pos + 2, c.length);
assertEquals("baz", c[123]);
assertEquals("undefined", typeof(c[pos - 1]));
assertEquals("foo", c[pos]);
assertEquals("bar", c[pos + 1]);
// When we take the number off the prototype it disappears from a, but
// the concat put it in c itself.
Array.prototype["123"] = undefined;
assertEquals("undefined", typeof(a[123]));
assertEquals("baz", c[123]);
// If the element of prototype is shadowed, the element on the instance
// should be copied, but not the one on the prototype.
Array.prototype[123] = 'baz';
a[123] = 'xyz';
assertEquals('xyz', a[123]);
c = a.concat(b);
assertEquals('xyz', c[123]);
// Non-numeric properties on the prototype or the array shouldn't get
// copied.
Array.prototype.moe = 'joe';
a.ben = 'jerry';
assertEquals(a["moe"], 'joe');
assertEquals(a["ben"], 'jerry');
c = a.concat(b);
// ben was not copied
assertEquals("undefined", typeof(c.ben));
// moe was not copied, but we can see it through the prototype
assertEquals("joe", c.moe);
// When we take moe off the prototype it disappears from all arrays.
Array.prototype.moe = undefined;
assertEquals("undefined", typeof(c.moe));
// Negative indices don't get concated.
a[-1] = 'minus1';
assertEquals("minus1", a[-1]);
assertEquals("undefined", typeof(a[0xffffffff]));
c = a.concat(b);
assertEquals("undefined", typeof(c[-1]));
assertEquals("undefined", typeof(c[0xffffffff]));
assertEquals(c.length, a.length + 1);
}
a = [];
c = a.concat('Hello');
assertEquals(1, c.length);
assertEquals("Hello", c[0]);
assertEquals("Hello", c.toString());
// Check that concat preserves holes.
var holey = [void 0,'a',,'c'].concat(['d',,'f',[0,,2],void 0])
assertEquals(9, holey.length); // hole in embedded array is ignored
for (var i = 0; i < holey.length; i++) {
if (i == 2 || i == 5) {
assertFalse(i in holey);
} else {
assertTrue(i in holey);
}
}
// Polluted prototype from prior tests.
delete Array.prototype[123];
// Check that concat reads getters in the correct order.
var arr1 = [,2];
var arr2 = [1,3];
var r1 = [].concat(arr1, arr2); // [,2,1,3]
assertEquals([,2,1,3], r1);
// Make first array change length of second array.
Object.defineProperty(arr1, 0, {get: function() {
arr2.push("X");
return undefined;
}, configurable: true})
var r2 = [].concat(arr1, arr2); // [undefined,2,1,3,"X"]
assertEquals([undefined,2,1,3,"X"], r2);
// Make first array change length of second array massively.
arr2.length = 2;
Object.defineProperty(arr1, 0, {get: function() {
arr2[500000] = "X";
return undefined;
}, configurable: true})
var r3 = [].concat(arr1, arr2); // [undefined,2,1,3,"X"]
var expected = [undefined,2,1,3];
expected[500000 + 2] = "X";
assertEquals(expected, r3);
var arr3 = [];
var trace = [];
var expectedTrace = []
function mkGetter(i) { return function() { trace.push(i); }; }
arr3.length = 10000;
for (var i = 0; i < 100; i++) {
Object.defineProperty(arr3, i * i, {get: mkGetter(i)});
expectedTrace[i] = i;
expectedTrace[100 + i] = i;
}
var r4 = [0].concat(arr3, arr3);
assertEquals(1 + arr3.length * 2, r4.length);
assertEquals(expectedTrace, trace);
})();