[change-array-by-copy] Implement Array.prototype.toSorted

Bug: v8:13035
Change-Id: I028f77f7dea73d56bf9df56ee06908fd01ce8a43
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3830034
Reviewed-by: Simon Zünd <szuend@chromium.org>
Commit-Queue: Shu-yu Guo <syg@chromium.org>
Reviewed-by: Adam Klein <adamk@chromium.org>
Cr-Commit-Position: refs/heads/main@{#82491}
This commit is contained in:
Shu-yu Guo 2022-08-15 16:55:05 -07:00 committed by V8 LUCI CQ
parent 0ce7a62be3
commit 374a93e23a
7 changed files with 331 additions and 36 deletions

View File

@ -788,6 +788,7 @@ filegroup(
"src/builtins/array-some.tq",
"src/builtins/array-splice.tq",
"src/builtins/array-to-reversed.tq",
"src/builtins/array-to-sorted.tq",
"src/builtins/array-to-spliced.tq",
"src/builtins/array-unshift.tq",
"src/builtins/array-with.tq",

View File

@ -1749,6 +1749,7 @@ torque_files = [
"src/builtins/array-some.tq",
"src/builtins/array-splice.tq",
"src/builtins/array-to-reversed.tq",
"src/builtins/array-to-sorted.tq",
"src/builtins/array-to-spliced.tq",
"src/builtins/array-unshift.tq",
"src/builtins/array-with.tq",

View File

@ -0,0 +1,131 @@
// Copyright 2022 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.
namespace array {
transitioning macro
CopyWorkArrayToNewFastJSArray(implicit context: Context, sortState: SortState)(
elementsKind: constexpr ElementsKind, numberOfNonUndefined: Smi): JSArray {
dcheck(
elementsKind == ElementsKind::PACKED_SMI_ELEMENTS ||
elementsKind == ElementsKind::PACKED_ELEMENTS);
const len = sortState.sortLength;
dcheck(len == numberOfNonUndefined + sortState.numberOfUndefined);
dcheck(len <= kMaxFastArrayLength);
const copy: FixedArray = UnsafeCast<FixedArray>(AllocateFixedArray(
elementsKind, Convert<intptr>(len), AllocationFlag::kNone));
const workArray = sortState.workArray;
CopyElements(
elementsKind, copy, 0, workArray, 0,
Convert<intptr>(numberOfNonUndefined));
dcheck(
sortState.numberOfUndefined == 0 ||
elementsKind == ElementsKind::PACKED_ELEMENTS);
for (let i = numberOfNonUndefined; i < len; ++i) {
copy.objects[i] = Undefined;
}
const map = LoadJSArrayElementsMap(elementsKind, LoadNativeContext(context));
return NewJSArray(map, copy);
}
transitioning macro
CopyWorkArrayToNewJSArray(implicit context: Context, sortState: SortState)(
numberOfNonUndefined: Smi): JSArray {
const len = sortState.sortLength;
dcheck(len == numberOfNonUndefined + sortState.numberOfUndefined);
const workArray = sortState.workArray;
const copy = ArrayCreate(len);
let i: Smi = 0;
for (; i < numberOfNonUndefined; ++i) {
SetProperty(copy, i, UnsafeCast<JSAny>(workArray.objects[i]));
}
for (; i < len; ++i) {
SetProperty(copy, i, Undefined);
}
return copy;
}
transitioning builtin
ArrayTimSortIntoCopy(context: Context, sortState: SortState): JSArray {
const isToSorted: constexpr bool = true;
const numberOfNonUndefined: Smi =
CompactReceiverElementsIntoWorkArray(isToSorted);
ArrayTimSortImpl(context, sortState, numberOfNonUndefined);
if (sortState.sortLength <= kMaxFastArrayLength) {
// The result copy of Array.prototype.toSorted is always packed.
try {
if (sortState.numberOfUndefined != 0) goto FastObject;
const workArray = sortState.workArray;
for (let i: Smi = 0; i < workArray.length; ++i) {
const e = UnsafeCast<JSAny>(workArray.objects[i]);
// TODO(v8:12764): ArrayTimSortImpl already boxed doubles. Support
// PACKED_DOUBLE_ELEMENTS.
if (TaggedIsNotSmi(e)) {
goto FastObject;
}
}
return CopyWorkArrayToNewFastJSArray(
ElementsKind::PACKED_SMI_ELEMENTS, numberOfNonUndefined);
} label FastObject {
return CopyWorkArrayToNewFastJSArray(
ElementsKind::PACKED_ELEMENTS, numberOfNonUndefined);
}
}
return CopyWorkArrayToNewJSArray(numberOfNonUndefined);
}
// https://tc39.es/proposal-change-array-by-copy/#sec-array.prototype.toSorted
transitioning javascript builtin ArrayPrototypeToSorted(
js-implicit context: NativeContext, receiver: JSAny)(...arguments): JSAny {
// 1. If comparefn is not undefined and IsCallable(comparefn) is false, throw
// a TypeError exception.
const comparefnObj: JSAny = arguments[0];
const comparefn = Cast<(Undefined | Callable)>(comparefnObj) otherwise
ThrowTypeError(MessageTemplate::kBadSortComparisonFunction, comparefnObj);
// 2. Let O be ? ToObject(this value).
const obj: JSReceiver = ToObject(context, receiver);
// 3. Let len be ? LengthOfArrayLike(O).
const len: Number = GetLengthProperty(obj);
if (len == 0) return ArrayCreate(0);
if (len == 1) {
const copy = ArrayCreate(1);
const zero: Smi = 0;
SetProperty(copy, zero, GetProperty(obj, zero));
return copy;
}
// 4. Let A be ? ArrayCreate(𝔽(len)).
//
// The actual array will be created later, but perform the range check.
if (len > kMaxArrayLength) {
ThrowRangeError(MessageTemplate::kInvalidArrayLength, len);
}
// 5. Let SortCompare be a new Abstract Closure with parameters (x, y) that
// captures comparefn and performs the following steps when called:
// a. Return ? CompareArrayElements(x, y, comparefn).
// 6. Let sortedList be ? SortIndexedProperties(obj, len, SortCompare, false).
// 7. Let j be 0.
// 8. Repeat, while j < len,
// a. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(j)),
// sortedList[j]). b. Set j to j + 1.
// 9. Return A.
//
// The implementation of the above steps is shared with Array.prototype.sort.
const isToSorted: constexpr bool = true;
const sortState: SortState = NewSortState(obj, comparefn, len, isToSorted);
return ArrayTimSortIntoCopy(context, sortState);
}
}

View File

@ -4522,6 +4522,8 @@ void Genesis::InitializeGlobal_harmony_change_array_by_copy() {
SimpleInstallFunction(isolate_, array_prototype, "toReversed",
Builtin::kArrayPrototypeToReversed, 0, true);
SimpleInstallFunction(isolate_, array_prototype, "toSorted",
Builtin::kArrayPrototypeToSorted, 1, false);
SimpleInstallFunction(isolate_, array_prototype, "toSpliced",
Builtin::kArrayPrototypeToSpliced, 2, false);
SimpleInstallFunction(isolate_, array_prototype, "with",
@ -4533,6 +4535,7 @@ void Genesis::InitializeGlobal_harmony_change_array_by_copy() {
.ToHandleChecked());
InstallTrueValuedProperty(isolate_, unscopables, "toReversed");
InstallTrueValuedProperty(isolate_, unscopables, "toSorted");
InstallTrueValuedProperty(isolate_, unscopables, "toSpliced");
}

View File

@ -0,0 +1,122 @@
// Copyright 2022 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-change-array-by-copy
assertEquals(1, Array.prototype.toSorted.length);
assertEquals("toSorted", Array.prototype.toSorted.name);
function TerribleCopy(input) {
let copy;
if (Array.isArray(input)) {
copy = [...input];
} else {
copy = { length: input.length };
for (let i = 0; i < input.length; i++) {
copy[i] = input[i];
}
}
return copy;
}
function AssertToSortedAndSortSameResult(input, ...args) {
const orig = TerribleCopy(input);
const s = Array.prototype.toSorted.apply(input, args);
const copy = TerribleCopy(input);
Array.prototype.sort.apply(copy, args);
// The in-place sorted version should be pairwise equal to the toSorted,
// modulo being an actual Array if the input is generic.
if (Array.isArray(input)) {
assertEquals(copy, s);
} else {
assertEquals(copy.length, s.length);
for (let i = 0; i < copy.length; i++) {
assertEquals(copy[i], s[i]);
}
}
// The original input should be unchanged.
assertEquals(orig, input);
// The result of toSorted() is a copy.
assertFalse(s === input);
}
function TestToSortedBasicBehaviorHelper(input) {
// No custom comparator.
AssertToSortedAndSortSameResult(input);
// Custom comparator.
AssertToSortedAndSortSameResult(input, (x, y) => {
if (x < y) return -1;
if (x > y) return 1;
return 0;
});
}
// Smi packed
AssertToSortedAndSortSameResult([1,3,2,4]);
// Double packed
AssertToSortedAndSortSameResult([1.1,3.3,2.2,4.4]);
// Packed
AssertToSortedAndSortSameResult([true,false,1,42.42,null,"foo"]);
// Smi holey
AssertToSortedAndSortSameResult([1,,3,,2,,4,,]);
// Double holey
AssertToSortedAndSortSameResult([1.1,,3.3,,2.2,,4.4,,]);
// Holey
AssertToSortedAndSortSameResult([true,,false,,1,,42.42,,null,,"foo",,]);
// Generic
AssertToSortedAndSortSameResult({ length: 4,
get "0"() { return "hello"; },
get "1"() { return "cursed"; },
get "2"() { return "java"; },
get "3"() { return "script" } });
(function TestSnapshotAtBeginning() {
const a = [1,3,4,2];
// Use a cursed comparator that mutates the original array. toSorted, like
// sort, takes a snapshot at the beginning.
const s = a.toSorted((x, y) => {
a.pop();
if (x < y) return -1;
if (x > y) return 1;
return 0;
});
assertEquals([1,2,3,4], s);
assertEquals(0, a.length);
})();
(function TestTooBig() {
const a = { length: Math.pow(2, 32) };
assertThrows(() => Array.prototype.toSorted.call(a), RangeError);
})();
(function TestNoSpecies() {
class MyArray extends Array {
static get [Symbol.species]() { return MyArray; }
}
assertEquals(Array, (new MyArray()).toSorted().constructor);
})();
// All tests after this have an invalidated elements-on-prototype protector.
(function TestNoHoles() {
const a = [,,,,];
Array.prototype[3] = "on proto";
const s = a.toSorted();
assertEquals(["on proto",undefined,undefined,undefined], s);
assertEquals(a.length, s.length)
for (let i = 0; i < a.length; i++) {
assertFalse(a.hasOwnProperty(i));
assertTrue(s.hasOwnProperty(i));
}
})();
assertEquals(Array.prototype[Symbol.unscopables].toSorted, true);

View File

@ -33,8 +33,12 @@ class SortState extends HeapObject {
}
}
macro ResetToGenericAccessor(): void {
this.loadFn = Load<GenericElementsAccessor>;
macro ResetToGenericAccessor(isToSorted: constexpr bool): void {
if constexpr (isToSorted) {
this.loadFn = LoadNoHasPropertyCheck<GenericElementsAccessor>;
} else {
this.loadFn = Load<GenericElementsAccessor>;
}
this.storeFn = Store<GenericElementsAccessor>;
this.deleteFn = Delete<GenericElementsAccessor>;
}
@ -140,7 +144,7 @@ macro CalculateWorkArrayLength(
transitioning macro NewSortState(implicit context: Context)(
receiver: JSReceiver, comparefn: Undefined|Callable,
initialReceiverLength: Number): SortState {
initialReceiverLength: Number, isToSorted: constexpr bool): SortState {
const sortComparePtr =
comparefn != Undefined ? SortCompareUserFn : SortCompareDefault;
const map = receiver.map;
@ -152,8 +156,12 @@ transitioning macro NewSortState(implicit context: Context)(
try {
const a: FastJSArray = Cast<FastJSArray>(receiver) otherwise Slow;
// Copy copy-on-write (COW) arrays.
array::EnsureWriteableFastElements(a);
if constexpr (!isToSorted) {
// Copy copy-on-write (COW) arrays if we're doing Array.prototype.sort,
// which sorts in place, instead of Array.prototype.toSorted, which sorts
// by copy.
array::EnsureWriteableFastElements(a);
}
const elementsKind: ElementsKind = map.elements_kind;
if (IsDoubleElementsKind(elementsKind)) {
@ -173,7 +181,11 @@ transitioning macro NewSortState(implicit context: Context)(
canUseSameAccessorFn = CanUseSameAccessor<FastObjectElements>;
}
} label Slow {
loadFn = Load<GenericElementsAccessor>;
if constexpr (isToSorted) {
loadFn = LoadNoHasPropertyCheck<GenericElementsAccessor>;
} else {
loadFn = Load<GenericElementsAccessor>;
}
storeFn = Store<GenericElementsAccessor>;
deleteFn = Delete<GenericElementsAccessor>;
canUseSameAccessorFn = CanUseSameAccessor<GenericElementsAccessor>;
@ -239,6 +251,13 @@ transitioning builtin Load<ElementsAccessor : type extends ElementsKind>(
return GetProperty(receiver, index);
}
transitioning builtin
LoadNoHasPropertyCheck<ElementsAccessor : type extends ElementsKind>(
context: Context, sortState: SortState, index: Smi): JSAny|TheHole {
const receiver = sortState.receiver;
return GetProperty(receiver, index);
}
Load<FastSmiElements>(
context: Context, sortState: SortState, index: Smi): JSAny|TheHole {
const object = UnsafeCast<JSObject>(sortState.receiver);
@ -1279,7 +1298,8 @@ ArrayTimSortImpl(context: Context, sortState: SortState, length: Smi): void {
transitioning macro
CompactReceiverElementsIntoWorkArray(
implicit context: Context, sortState: SortState)(): Smi {
implicit context: Context,
sortState: SortState)(isToSorted: constexpr bool): Smi {
let growableWorkArray = growable_fixed_array::GrowableFixedArray{
array: sortState.workArray,
capacity: Convert<intptr>(sortState.workArray.length),
@ -1304,8 +1324,22 @@ CompactReceiverElementsIntoWorkArray(
const element: JSAny|TheHole = loadFn(context, sortState, i);
if (element == TheHole) {
// Do nothing for holes. The result is that elements are
// compacted at the front of the work array.
if constexpr (isToSorted) {
// Array.prototype.toSorted does not have the HasProperty check that
// Array.prototype.sort has and unconditionally performs a GetProperty
// for each element.
//
// Only fast JSArray accessors return TheHole, and fast JSArrays are
// protected by the NoElements protector which ensures that objects on
// the prototype chain do not have indexed properties. So if a fast
// JSArray accessor returns TheHole, we know the prototype walk will
// return Undefined.
numberOfUndefined++;
} else {
// Do nothing for holes for Array.prototype.sort. The result
// is that elements are compacted at the front of the work array.
}
} else if (element == Undefined) {
numberOfUndefined++;
} else {
@ -1358,7 +1392,9 @@ CopyWorkArrayToReceiver(implicit context: Context, sortState: SortState)(
transitioning builtin
ArrayTimSort(context: Context, sortState: SortState): JSAny {
const numberOfNonUndefined: Smi = CompactReceiverElementsIntoWorkArray();
const isToSorted: constexpr bool = false;
const numberOfNonUndefined: Smi =
CompactReceiverElementsIntoWorkArray(isToSorted);
ArrayTimSortImpl(context, sortState, numberOfNonUndefined);
try {
@ -1366,7 +1402,7 @@ ArrayTimSort(context: Context, sortState: SortState): JSAny {
// receiver, if that is the case, we switch to the slow path.
sortState.CheckAccessor() otherwise Slow;
} label Slow deferred {
sortState.ResetToGenericAccessor();
sortState.ResetToGenericAccessor(isToSorted);
}
CopyWorkArrayToReceiver(numberOfNonUndefined);
@ -1391,7 +1427,8 @@ ArrayPrototypeSort(
if (len < 2) return obj;
const sortState: SortState = NewSortState(obj, comparefn, len);
const isToSorted: constexpr bool = false;
const sortState: SortState = NewSortState(obj, comparefn, len, isToSorted);
ArrayTimSort(context, sortState);
return obj;

View File

@ -544,30 +544,30 @@ KNOWN_OBJECTS = {
("old_space", 0x04625): "StringSplitCache",
("old_space", 0x04a2d): "RegExpMultipleCache",
("old_space", 0x04e35): "BuiltinsConstantsTable",
("old_space", 0x05281): "AsyncFunctionAwaitRejectSharedFun",
("old_space", 0x052a5): "AsyncFunctionAwaitResolveSharedFun",
("old_space", 0x052c9): "AsyncGeneratorAwaitRejectSharedFun",
("old_space", 0x052ed): "AsyncGeneratorAwaitResolveSharedFun",
("old_space", 0x05311): "AsyncGeneratorYieldResolveSharedFun",
("old_space", 0x05335): "AsyncGeneratorReturnResolveSharedFun",
("old_space", 0x05359): "AsyncGeneratorReturnClosedRejectSharedFun",
("old_space", 0x0537d): "AsyncGeneratorReturnClosedResolveSharedFun",
("old_space", 0x053a1): "AsyncIteratorValueUnwrapSharedFun",
("old_space", 0x053c5): "PromiseAllResolveElementSharedFun",
("old_space", 0x053e9): "PromiseAllSettledResolveElementSharedFun",
("old_space", 0x0540d): "PromiseAllSettledRejectElementSharedFun",
("old_space", 0x05431): "PromiseAnyRejectElementSharedFun",
("old_space", 0x05455): "PromiseCapabilityDefaultRejectSharedFun",
("old_space", 0x05479): "PromiseCapabilityDefaultResolveSharedFun",
("old_space", 0x0549d): "PromiseCatchFinallySharedFun",
("old_space", 0x054c1): "PromiseGetCapabilitiesExecutorSharedFun",
("old_space", 0x054e5): "PromiseThenFinallySharedFun",
("old_space", 0x05509): "PromiseThrowerFinallySharedFun",
("old_space", 0x0552d): "PromiseValueThunkFinallySharedFun",
("old_space", 0x05551): "ProxyRevokeSharedFun",
("old_space", 0x05575): "ShadowRealmImportValueFulfilledSFI",
("old_space", 0x05599): "SourceTextModuleExecuteAsyncModuleFulfilledSFI",
("old_space", 0x055bd): "SourceTextModuleExecuteAsyncModuleRejectedSFI",
("old_space", 0x05285): "AsyncFunctionAwaitRejectSharedFun",
("old_space", 0x052a9): "AsyncFunctionAwaitResolveSharedFun",
("old_space", 0x052cd): "AsyncGeneratorAwaitRejectSharedFun",
("old_space", 0x052f1): "AsyncGeneratorAwaitResolveSharedFun",
("old_space", 0x05315): "AsyncGeneratorYieldResolveSharedFun",
("old_space", 0x05339): "AsyncGeneratorReturnResolveSharedFun",
("old_space", 0x0535d): "AsyncGeneratorReturnClosedRejectSharedFun",
("old_space", 0x05381): "AsyncGeneratorReturnClosedResolveSharedFun",
("old_space", 0x053a5): "AsyncIteratorValueUnwrapSharedFun",
("old_space", 0x053c9): "PromiseAllResolveElementSharedFun",
("old_space", 0x053ed): "PromiseAllSettledResolveElementSharedFun",
("old_space", 0x05411): "PromiseAllSettledRejectElementSharedFun",
("old_space", 0x05435): "PromiseAnyRejectElementSharedFun",
("old_space", 0x05459): "PromiseCapabilityDefaultRejectSharedFun",
("old_space", 0x0547d): "PromiseCapabilityDefaultResolveSharedFun",
("old_space", 0x054a1): "PromiseCatchFinallySharedFun",
("old_space", 0x054c5): "PromiseGetCapabilitiesExecutorSharedFun",
("old_space", 0x054e9): "PromiseThenFinallySharedFun",
("old_space", 0x0550d): "PromiseThrowerFinallySharedFun",
("old_space", 0x05531): "PromiseValueThunkFinallySharedFun",
("old_space", 0x05555): "ProxyRevokeSharedFun",
("old_space", 0x05579): "ShadowRealmImportValueFulfilledSFI",
("old_space", 0x0559d): "SourceTextModuleExecuteAsyncModuleFulfilledSFI",
("old_space", 0x055c1): "SourceTextModuleExecuteAsyncModuleRejectedSFI",
}
# Lower 32 bits of first page addresses for various heap spaces.