// Copyright 2023 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 ArrayIsArray_Inline(
implicit context: Context)(element: JSAny): Boolean {
if (Is<JSArray>(element)) {
return True;
} else if (Is<JSProxy>(element)) {
return Cast<Boolean>(runtime::ArrayIsArray(element))
otherwise unreachable;
} else {
return False;
}
}
struct FlatVector {
macro CreateJSArray(implicit context: Context)(targetKind: ElementsKind):
JSArray {
const map: Map =
LoadJSArrayElementsMap(targetKind, LoadNativeContext(context));
const a: JSArray = NewJSArray(map, this.fixedArray);
this.fixedArray = kEmptyFixedArray;
return a;
}
macro StoreResult(implicit context: Context)(index: Smi, result: JSAny):
void {
this.fixedArray.objects[index] = result;
}
fixedArray: FixedArray;
}
macro NewFlatVector(implicit context: Context)(length: Smi): FlatVector {
const fixedArray = length > 0 ?
AllocateFixedArrayWithHoles(SmiUntag(length)) :
kEmptyFixedArray;
return FlatVector{fixedArray};
}
struct FlattenedLengthResult {
length: Smi;
targetKind: ElementsKind;
}
// Fast path safety valve: avoid unbounded explicit stack growth on
// cyclic nesting by bailing out to the slow path after a fixed depth.
// 3 entries per depth: array, index, depth (depth limit = 1024).
const kMaxFlatFastStackEntries: intptr = 3072;
transitioning macro CalculateFlattenedLengthFast(
implicit context: Context)(source: FastJSArray, sourceLength: Smi,
depth: Smi): FlattenedLengthResult labels Bailout {
PerformStackCheck();
let targetLength: Smi = 0;
const sourceKind: ElementsKind = source.map.elements_kind;
// Packed numeric source: the result has the same kind and length.
// Holey variants are excluded because (1) holes are skipped during
// flattening, so array.length overcounts actual elements, and (2) with
// V8_ENABLE_UNDEFINED_DOUBLE, FixedDoubleArray can store undefined which
// is not a Number and would crash the PACKED_DOUBLE second pass.
if (sourceKind == ElementsKind::PACKED_SMI_ELEMENTS ||
sourceKind == ElementsKind::PACKED_DOUBLE_ELEMENTS) {
return FlattenedLengthResult{length: sourceLength, targetKind: sourceKind};
}
// Track which leaf element types are present.
let seenSmi: bool = false;
let seenDouble: bool = false;
let seenObject: bool = false;
let stack = growable_fixed_array::NewGrowableFixedArray();
let currentArray: FastJSArrayForRead =
Cast<FastJSArrayForRead>(source) otherwise goto Bailout;
let currentDepth: Smi = depth;
let index: Smi = SmiConstant(0);
let currentLength: Smi = sourceLength;
let fastOW = NewFastJSArrayForReadWitness(currentArray);
let didDescend: bool = false;
while (true) {
// Rechecking once per array is enough: nothing in the inner loop
// invokes JS.
fastOW.Recheck() otherwise goto Bailout;
while (index < currentLength) {
if (index >= fastOW.Get().length) goto Bailout;
let element: JSAny;
try {
element = fastOW.LoadElementNoHole(index) otherwise FoundHole;
} label FoundHole {
index++;
continue;
}
if (currentDepth > 0 && Is<JSArray>(element)) {
const elementArray: FastJSArrayForRead =
Cast<FastJSArrayForRead>(element) otherwise goto Bailout;
const rawKind: ElementsKind = elementArray.map.elements_kind;
const nextDepth: Smi =
math::TrySmiSub(currentDepth, 1) otherwise goto Bailout;
// Packed numeric arrays cannot contain sub-arrays or proxies, so add
// their length directly without scanning elements.
// Holey variants are excluded because holes are skipped during
// flattening, so array.length would overcount the actual elements.
if (rawKind == ElementsKind::PACKED_SMI_ELEMENTS) {
seenSmi = true;
const subLen: Smi =
Cast<Smi>(elementArray.length) otherwise goto Bailout;
targetLength =
math::TrySmiAdd(targetLength, subLen) otherwise goto Bailout;
index++;
continue;
}
if (rawKind == ElementsKind::PACKED_DOUBLE_ELEMENTS) {
seenDouble = true;
const subLen: Smi =
Cast<Smi>(elementArray.length) otherwise goto Bailout;
targetLength =
math::TrySmiAdd(targetLength, subLen) otherwise goto Bailout;
index++;
continue;
}
// Non-numeric sub-array: descend to determine leaf types.
const nextIndex: Smi = math::TrySmiAdd(index, 1) otherwise goto Bailout;
if (stack.length >= kMaxFlatFastStackEntries) goto Bailout;
stack.Push(currentArray);
stack.Push(nextIndex);
stack.Push(currentDepth);
currentArray = elementArray;
currentDepth = nextDepth;
index = SmiConstant(0);
currentLength = Cast<Smi>(currentArray.length) otherwise goto Bailout;
fastOW = NewFastJSArrayForReadWitness(currentArray);
didDescend = true;
break;
}
if (currentDepth > 0 && Is<JSProxy>(element)) goto Bailout;
if (!IsNumber(element)) {
seenObject = true;
} else if (!TaggedIsSmi(element)) {
seenDouble = true;
} else {
seenSmi = true;
}
targetLength = math::TrySmiAdd(targetLength, 1) otherwise goto Bailout;
index++;
}
if (didDescend) {
didDescend = false;
continue;
}
if (stack.length == 0) break;
stack.length = stack.length - 1;
currentDepth = UnsafeCast<Smi>(stack.array.objects[stack.length]);
stack.length = stack.length - 1;
index = UnsafeCast<Smi>(stack.array.objects[stack.length]);
stack.length = stack.length - 1;
currentArray = Cast<FastJSArrayForRead>(stack.array.objects[stack.length])
otherwise goto Bailout;
currentLength = Cast<Smi>(currentArray.length) otherwise goto Bailout;
fastOW = NewFastJSArrayForReadWitness(currentArray);
}
// Determine target kind from observed leaf types.
let targetKind: ElementsKind;
if (seenObject) {
targetKind = ElementsKind::PACKED_ELEMENTS;
} else if (seenDouble) {
targetKind = ElementsKind::PACKED_DOUBLE_ELEMENTS;
} else {
targetKind = ElementsKind::PACKED_SMI_ELEMENTS;
}
return FlattenedLengthResult{length: targetLength, targetKind: targetKind};
}
transitioning macro TryFastFlat(
implicit context: Context)(receiver: JSReceiver, sourceLength: Number,
depth: Smi): JSArray labels Bailout {
const fastLength: Smi = Cast<Smi>(sourceLength) otherwise goto Bailout;
const fastO: FastJSArrayForCopy =
Cast<FastJSArrayForCopy>(receiver) otherwise goto Bailout;
const info: FlattenedLengthResult = CalculateFlattenedLengthFast(
fastO, fastLength, depth) otherwise goto Bailout;
const flattenedLength: Smi = info.length;
if (flattenedLength == 0) {
const map: Map = LoadJSArrayElementsMap(
ElementsKind::PACKED_SMI_ELEMENTS, LoadNativeContext(context));
return NewJSArray(map, kEmptyFixedArray);
}
if (info.targetKind == ElementsKind::PACKED_DOUBLE_ELEMENTS) {
const doubleMap: Map = LoadJSArrayElementsMap(
ElementsKind::PACKED_DOUBLE_ELEMENTS, LoadNativeContext(context));
// AllocateFixedDoubleArrayWithHoles memsets every slot to the hole
// sentinel, but flattenedLength is exact so every slot will be
// written. Use AllocateFixedArray to skip the redundant memset.
const doubleElements: FixedDoubleArray =
UnsafeCast<FixedDoubleArray>(AllocateFixedArray(
ElementsKind::PACKED_DOUBLE_ELEMENTS, SmiUntag(flattenedLength)));
let targetIndex: Smi = 0;
let stack = growable_fixed_array::NewGrowableFixedArray();
let currentArray: FastJSArrayForRead =
Cast<FastJSArrayForRead>(receiver) otherwise goto Bailout;
let currentDepth: Smi = depth;
let index: Smi = SmiConstant(0);
let currentLength: Smi = fastLength;
let fastOW = NewFastJSArrayForReadWitness(currentArray);
let didDescend: bool = false;
while (true) {
fastOW.Recheck() otherwise goto Bailout;
while (index < currentLength) {
if (index >= fastOW.Get().length) goto Bailout;
let element: JSAny;
try {
element = fastOW.LoadElementNoHole(index) otherwise FoundHole;
} label FoundHole {
index++;
continue;
}
if (currentDepth > 0 && Is<JSArray>(element)) {
const nextDepth: Smi =
math::TrySmiSub(currentDepth, 1) otherwise goto Bailout;
const subArray: JSArray = UnsafeCast<JSArray>(element);
// Packed double sub-array: bulk copy via memcpy.
// FixedDoubleArray contains no tagged pointers, so the copy is
// unconditionally safe with respect to write barriers.
if (subArray.map.elements_kind ==
ElementsKind::PACKED_DOUBLE_ELEMENTS) {
const srcElements: FixedDoubleArray =
Cast<FixedDoubleArray>(subArray.elements)
otherwise goto Bailout;
const srcLen: Smi =
Cast<Smi>(subArray.length) otherwise goto Bailout;
const newIdx: Smi = math::TrySmiAdd(targetIndex, srcLen)
otherwise goto Bailout;
if (Convert<intptr>(newIdx) > doubleElements.length_intptr) {
goto Bailout;
}
TorqueCopyElements(
doubleElements, SmiUntag(targetIndex), srcElements, 0,
SmiUntag(srcLen));
targetIndex = newIdx;
index++;
continue;
}
const elementArray: FastJSArrayForRead =
Cast<FastJSArrayForRead>(element) otherwise goto Bailout;
const nextIndex: Smi =
math::TrySmiAdd(index, 1) otherwise goto Bailout;
stack.Push(currentArray);
stack.Push(nextIndex);
stack.Push(currentDepth);
currentArray = elementArray;
currentDepth = nextDepth;
index = SmiConstant(0);
currentLength = Cast<Smi>(currentArray.length) otherwise goto Bailout;
fastOW = NewFastJSArrayForReadWitness(currentArray);
didDescend = true;
break;
}
if (currentDepth > 0 && Is<JSProxy>(element)) goto Bailout;
if (Convert<intptr>(targetIndex) >= doubleElements.length_intptr)
goto Bailout;
doubleElements.values[targetIndex] =
Convert<float64_or_undefined_or_hole>(UnsafeCast<Number>(element));
targetIndex++;
index++;
}
if (didDescend) {
didDescend = false;
continue;
}
if (stack.length == 0) break;
stack.length = stack.length - 1;
currentDepth = UnsafeCast<Smi>(stack.array.objects[stack.length]);
stack.length = stack.length - 1;
index = UnsafeCast<Smi>(stack.array.objects[stack.length]);
stack.length = stack.length - 1;
currentArray = Cast<FastJSArrayForRead>(stack.array.objects[stack.length])
otherwise goto Bailout;
currentLength = Cast<Smi>(currentArray.length) otherwise goto Bailout;
fastOW = NewFastJSArrayForReadWitness(currentArray);
}
if (targetIndex != flattenedLength) goto Bailout;
return NewJSArray(doubleMap, doubleElements);
}
let vector = NewFlatVector(flattenedLength);
let targetIndex: Smi = 0;
let stack = growable_fixed_array::NewGrowableFixedArray();
let currentArray: FastJSArrayForRead =
Cast<FastJSArrayForRead>(receiver) otherwise goto Bailout;
let currentDepth: Smi = depth;
let index: Smi = SmiConstant(0);
let currentLength: Smi = fastLength;
let fastOW = NewFastJSArrayForReadWitness(currentArray);
let didDescend: bool = false;
while (true) {
fastOW.Recheck() otherwise goto Bailout;
while (index < currentLength) {
if (index >= fastOW.Get().length) goto Bailout;
let element: JSAny;
try {
element = fastOW.LoadElementNoHole(index) otherwise FoundHole;
} label FoundHole {
index++;
continue;
}
if (currentDepth > 0 && Is<JSArray>(element)) {
const nextDepth: Smi =
math::TrySmiSub(currentDepth, 1) otherwise goto Bailout;
const subArray: JSArray = UnsafeCast<JSArray>(element);
// Packed Smi sub-array: bulk copy via memcpy. Smi
// values carry no heap pointer, so write barriers are not required.
if (subArray.map.elements_kind == ElementsKind::PACKED_SMI_ELEMENTS) {
const srcElements: FixedArray = Cast<FixedArray>(subArray.elements)
otherwise goto Bailout;
const srcLen: Smi = Cast<Smi>(subArray.length) otherwise goto Bailout;
const newIdx: Smi = math::TrySmiAdd(targetIndex, srcLen)
otherwise goto Bailout;
if (Convert<intptr>(newIdx) > vector.fixedArray.length_intptr) {
goto Bailout;
}
TorqueCopyElements(
vector.fixedArray, SmiUntag(targetIndex), srcElements, 0,
SmiUntag(srcLen));
targetIndex = newIdx;
index++;
continue;
}
const elementArray: FastJSArrayForRead =
Cast<FastJSArrayForRead>(element) otherwise goto Bailout;
const nextIndex: Smi = math::TrySmiAdd(index, 1) otherwise goto Bailout;
if (stack.length >= kMaxFlatFastStackEntries) goto Bailout;
stack.Push(currentArray);
stack.Push(nextIndex);
stack.Push(currentDepth);
currentArray = elementArray;
currentDepth = nextDepth;
index = SmiConstant(0);
currentLength = Cast<Smi>(currentArray.length) otherwise goto Bailout;
fastOW = NewFastJSArrayForReadWitness(currentArray);
didDescend = true;
break;
}
if (currentDepth > 0 && Is<JSProxy>(element)) goto Bailout;
if (Convert<intptr>(targetIndex) >= vector.fixedArray.length_intptr) {
goto Bailout;
}
vector.StoreResult(targetIndex, element);
targetIndex++;
index++;
}
if (didDescend) {
didDescend = false;
continue;
}
if (stack.length == 0) break;
stack.length = stack.length - 1;
currentDepth = UnsafeCast<Smi>(stack.array.objects[stack.length]);
stack.length = stack.length - 1;
index = UnsafeCast<Smi>(stack.array.objects[stack.length]);
stack.length = stack.length - 1;
currentArray = Cast<FastJSArrayForRead>(stack.array.objects[stack.length])
otherwise goto Bailout;
currentLength = Cast<Smi>(currentArray.length) otherwise goto Bailout;
fastOW = NewFastJSArrayForReadWitness(currentArray);
}
const endIndex: Smi = targetIndex;
if (endIndex != flattenedLength) goto Bailout;
return vector.CreateJSArray(info.targetKind);
}
transitioning macro FlattenIntoArrayFast(
implicit context: Context)(target: JSReceiver, source: JSReceiver,
sourceLength: Number, start: Number, depth: Smi, hasMapper: constexpr bool,
mapfn: JSAny, thisArgs: JSAny): Number
labels Bailout(Number, Number) {
// 1. Let targetIndex be start.
let targetIndex: Number = start;
// 2. Let sourceIndex be 0.
let smiSourceIndex: Smi = 0;
const fastSource = Cast<FastJSArray>(source)
otherwise goto Bailout(targetIndex, smiSourceIndex);
let fastOW = NewFastJSArrayWitness(fastSource);
const smiSourceLength = Cast<Smi>(sourceLength)
otherwise goto Bailout(targetIndex, smiSourceIndex);
// 3. Repeat, while sourceIndex < sourceLen
for (; smiSourceIndex < smiSourceLength; smiSourceIndex++) {
fastOW.Recheck() otherwise goto Bailout(targetIndex, smiSourceIndex);
// Ensure that we haven't walked beyond a possibly updated length.
if (smiSourceIndex >= fastOW.Get().length)
goto Bailout(targetIndex, smiSourceIndex);
// a. Let P be ! ToString(sourceIndex).
// b. Let exists be ? HasProperty(source, P).
// i. Let element be ? Get(source, P).
let element = fastOW.LoadElementNoHole(smiSourceIndex)
otherwise continue;
// ii. If mapperFunction is present, then
if constexpr (hasMapper) {
// 1. Set element to ? Call(mapperFunction, thisArgs , « element,
// sourceIndex, source »).
element = Call(context, mapfn, thisArgs, element, smiSourceIndex, source);
}
// iii. Let shouldFlatten be false.
let shouldFlatten: Boolean = False;
// iv. If depth > 0, then
let elementLength: Number = 0;
if (depth > 0) {
// Set shouldFlatten to ? IsArray(element).
// 1. Let elementLen be ? ToLength(? Get(element, "length")).
try {
const elementJSArray: JSArray =
Cast<JSArray>(element) otherwise NonJSArray;
shouldFlatten = True;
elementLength = elementJSArray.length;
} label NonJSArray {
if (Is<JSProxy>(element)) {
shouldFlatten = Cast<Boolean>(runtime::ArrayIsArray(element))
otherwise unreachable;
}
if (shouldFlatten == True) {
elementLength = GetLengthProperty(element);
}
}
}
// v. If shouldFlatten is true, then
if (shouldFlatten == True) {
if (elementLength > 0) {
// 2. Set targetIndex to ? FlattenIntoArray(target, element,
// elementLen, targetIndex, depth - 1).
const element = Cast<JSReceiver>(element) otherwise unreachable;
targetIndex = FlattenIntoArrayWithoutMapFn(
target, element, elementLength, targetIndex, depth - 1);
}
} else {
// 1. If targetIndex >= 2^53-1, throw a TypeError exception.
if (targetIndex >= kMaxSafeInteger) deferred {
ThrowTypeError(
MessageTemplate::kFlattenPastSafeLength, sourceLength,
targetIndex);
}
// 2. Perform ? CreateDataPropertyOrThrow(target,
// ! ToString(targetIndex),
// element).
FastCreateDataProperty(target, targetIndex, element);
targetIndex++;
}
}
return targetIndex;
}
// https://tc39.es/proposal-flatMap/#sec-FlattenIntoArray
transitioning macro FlattenIntoArraySlow(
implicit context: Context)(target: JSReceiver, source: JSReceiver,
sourceIndex: Number, sourceLength: Number, start: Number, depth: Smi,
hasMapper: constexpr bool, mapfn: JSAny, thisArgs: JSAny): Number {
// 1. Let targetIndex be start.
let targetIndex: Number = start;
// 2. Let sourceIndex be 0.
let sourceIndex: Number = sourceIndex;
// 3. Repeat, while sourceIndex < sourceLen
while (sourceIndex < sourceLength) {
// a. Let P be ! ToString(sourceIndex).
// b. Let exists be ? HasProperty(source, P).
const exists: Boolean = HasProperty(source, sourceIndex);
if (exists == True) {
let element: JSAny;
// i. Let element be ? Get(source, P).
element = GetProperty(source, sourceIndex);
// ii. If mapperFunction is present, then
if constexpr (hasMapper) {
// 1. Set element to ? Call(mapperFunction, thisArgs , « element,
// sourceIndex, source »).
element = Call(context, mapfn, thisArgs, element, sourceIndex, source);
}
// iii. Let shouldFlatten be false.
let shouldFlatten: Boolean = False;
// iv. If depth > 0, then
if (depth > 0) {
// Set shouldFlatten to ? IsArray(element).
shouldFlatten = ArrayIsArray_Inline(element);
}
// v. If shouldFlatten is true, then
if (shouldFlatten == True) {
// 1. Let elementLen be ? ToLength(? Get(element, "length")).
const elementLength: Number = GetLengthProperty(element);
// 2. Set targetIndex to ? FlattenIntoArray(target, element,
// elementLen, targetIndex, depth - 1).
const element = Cast<JSReceiver>(element) otherwise unreachable;
targetIndex = FlattenIntoArrayWithoutMapFn(
target, element, elementLength, targetIndex, depth - 1);
} else {
// 1. If targetIndex >= 2^53-1, throw a TypeError exception.
if (targetIndex >= kMaxSafeInteger) deferred {
ThrowTypeError(
MessageTemplate::kFlattenPastSafeLength, sourceLength,
targetIndex);
}
// 2. Perform ? CreateDataPropertyOrThrow(target,
// ! ToString(targetIndex),
// element).
FastCreateDataProperty(target, targetIndex, element);
targetIndex++;
}
}
// d. Increase sourceIndex by 1.
sourceIndex++;
}
return targetIndex;
}
transitioning macro FlattenIntoArray(
implicit context: Context)(target: JSReceiver, source: JSReceiver,
sourceLength: Number, start: Number, depth: Smi, hasMapper: constexpr bool,
mapfn: JSAny, thisArgs: JSAny): Number {
try {
return FlattenIntoArrayFast(
target, source, sourceLength, start, depth, hasMapper, mapfn, thisArgs)
otherwise Bailout;
} label Bailout(kTargetIndex: Number, kSourceIndex: Number) {
return FlattenIntoArraySlow(
target, source, kSourceIndex, sourceLength, kTargetIndex, depth,
hasMapper, mapfn, thisArgs);
}
}
transitioning builtin FlattenIntoArrayWithoutMapFn(
implicit context: Context)(target: JSReceiver, source: JSReceiver,
sourceLength: Number, start: Number, depth: Smi): Number {
// This builtin might get called recursively, check stack for overflow
// manually as it has stub linkage.
PerformStackCheck();
return FlattenIntoArray(
target, source, sourceLength, start, depth, false, Undefined, Undefined);
}
transitioning builtin FlattenIntoArrayWithMapFn(
implicit context: Context)(target: JSReceiver, source: JSReceiver,
sourceLength: Number, start: Number, depth: Smi, mapfn: JSAny,
thisArgs: JSAny): Number {
return FlattenIntoArray(
target, source, sourceLength, start, depth, true, mapfn, thisArgs);
}
// https://tc39.es/proposal-flatMap/#sec-Array.prototype.flat
transitioning javascript builtin ArrayPrototypeFlat(
js-implicit context: NativeContext, receiver: JSAny)(...arguments): JSAny {
// 1. Let O be ? ToObject(this value).
const o: JSReceiver = ToObject_Inline(context, receiver);
// 2. Let sourceLen be ? ToLength(? Get(O, "length")).
const len: Number = GetLengthProperty(o);
// 3. Let depthNum be 1.
let depthNum: Number = 1;
// 4. If depth is not Undefined, then
if (arguments[0] != Undefined) {
// a. Set depthNum to ? ToInteger(depth).
depthNum = ToInteger_Inline(arguments[0]);
}
// We will hit stack overflow before the stack depth reaches kSmiMax, so we
// can truncate depthNum(Number) to Smi to improve performance.
let depthSmi: Smi = 0;
try {
depthSmi = Cast<PositiveSmi>(depthNum) otherwise NotPositiveSmi;
} label NotPositiveSmi {
if (depthNum <= 0) {
depthSmi = 0;
} else {
depthSmi = Convert<Smi>(Convert<intptr>(kSmiMax));
}
}
// Try fast path in Torque/CSA.
try {
return TryFastFlat(o, len, depthSmi) otherwise SlowFastPath;
} label SlowFastPath {}
// 5. Let A be ? ArraySpeciesCreate(O, 0).
const a: JSReceiver = ArraySpeciesCreate(context, o, 0);
// 6. Perform ? FlattenIntoArray(A, O, sourceLen, 0, depthNum).
FlattenIntoArrayWithoutMapFn(a, o, len, 0, depthSmi);
// 7. Return A.
return a;
}
// https://tc39.es/proposal-flatMap/#sec-Array.prototype.flatMap
transitioning javascript builtin ArrayPrototypeFlatMap(
js-implicit context: NativeContext, receiver: JSAny)(...arguments): JSAny {
// 1. Let O be ? ToObject(this value).
const o: JSReceiver = ToObject_Inline(context, receiver);
// 2. Let sourceLen be ? ToLength(? Get(O, "length")).
const len: Number = GetLengthProperty(o);
// 3. If IsCallable(mapperFunction) is false, throw a TypeError exception.
let mapfn: Callable;
try {
mapfn = Cast<Callable>(arguments[0])
otherwise NonCallableError;
} label NonCallableError deferred {
ThrowCalledNonCallable(arguments[0]);
}
// 4. If thisArgs is present, let T be thisArgs; else let T be Undefined.
const t: JSAny = arguments[1];
// 5. Let A be ? ArraySpeciesCreate(O, 0).
const a: JSReceiver = ArraySpeciesCreate(context, o, 0);
// 6. Perform ? FlattenIntoArray(A, O, sourceLen, 0, depthNum).
FlattenIntoArrayWithMapFn(a, o, len, 0, 1, mapfn, t);
// 7. Return A.
return a;
}
}