// Copyright 2018 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 { type LoadJoinElementFn = builtin(Context, JSReceiver, uintptr) => JSAny; const kMaxArrayLength: constexpr uint32 generates 'JSArray::kMaxArrayLength'; // Fast C call to write a fixed array (see Buffer.fixedArray) to a single // string. extern macro ArrayBuiltinsAssembler::CallJSArrayArrayJoinConcatToSequentialString( FixedArray, intptr, String, String): String; transitioning builtin LoadJoinElement( context: Context, receiver: JSReceiver, k: uintptr): JSAny { return GetProperty(receiver, Convert(k)); } transitioning LoadJoinElement( context: Context, receiver: JSReceiver, k: uintptr): JSAny { const array: JSArray = UnsafeCast(receiver); const dict: NumberDictionary = UnsafeCast(array.elements); try { return BasicLoadNumberDictionaryElement(dict, Signed(k)) otherwise IfNoData, IfHole; } label IfNoData deferred { return GetProperty(receiver, Convert(k)); } label IfHole { return kEmptyString; } } LoadJoinElement( context: Context, receiver: JSReceiver, k: uintptr): JSAny { const array: JSArray = UnsafeCast(receiver); const fixedArray: FixedArray = UnsafeCast(array.elements); const element: Object = fixedArray.objects[k]; return element == TheHole ? kEmptyString : UnsafeCast(element); } LoadJoinElement( context: Context, receiver: JSReceiver, k: uintptr): JSAny { const array: JSArray = UnsafeCast(receiver); const fixedDoubleArray: FixedDoubleArray = UnsafeCast(array.elements); const element: float64 = fixedDoubleArray.floats[k].Value() otherwise return kEmptyString; return AllocateHeapNumberWithValue(element); } builtin LoadJoinTypedElement( context: Context, receiver: JSReceiver, k: uintptr): JSAny { const typedArray: JSTypedArray = UnsafeCast(receiver); dcheck(!typed_array::IsJSArrayBufferViewDetachedOrOutOfBoundsBoolean( typedArray)); return typed_array::LoadFixedTypedArrayElementAsTagged( typedArray.data_ptr, k, typed_array::KindForArrayType()); } transitioning builtin ConvertToLocaleString( context: Context, element: JSAny, locales: JSAny, options: JSAny): String { if (IsNullOrUndefined(element)) return kEmptyString; const prop: JSAny = GetProperty(element, 'toLocaleString'); try { const callable: Callable = Cast(prop) otherwise TypeError; let result: JSAny; if (IsNullOrUndefined(locales)) { result = Call(context, callable, element); } else if (IsNullOrUndefined(options)) { result = Call(context, callable, element, locales); } else { result = Call(context, callable, element, locales, options); } return ToString_Inline(result); } label TypeError { ThrowTypeError(MessageTemplate::kCalledNonCallable, prop); } } // Verifies the current element JSArray accessor can still be safely used // (see LoadJoinElement). macro CannotUseSameArrayAccessor(implicit context: Context)( loadFn: LoadJoinElementFn, receiver: JSReceiver, originalMap: Map, originalLen: Number): bool; CannotUseSameArrayAccessor(implicit context: Context)( loadFn: LoadJoinElementFn, receiver: JSReceiver, originalMap: Map, originalLen: Number): bool { if (loadFn == LoadJoinElement) return false; const array: JSArray = UnsafeCast(receiver); if (originalMap != array.map) return true; if (originalLen != array.length) return true; if (IsNoElementsProtectorCellInvalid()) return true; return false; } CannotUseSameArrayAccessor(implicit context: Context)( _loadFn: LoadJoinElementFn, receiver: JSReceiver, _initialMap: Map, _initialLen: Number): bool { const typedArray: JSTypedArray = UnsafeCast(receiver); // When this is called from toLocaleString(), the underlying buffer might get // detached / resized (in the case of RAB / GSAB) during iterating the // elements. When this is called from join(), it can happen only before the // first element (during parameter conversion). The code below doesn't // differentiate between these two cases, but does the checks in both cases. if (IsDetachedBuffer(typedArray.buffer)) { return true; } if (IsVariableLengthJSArrayBufferView(typedArray)) { // TODO(v8:11111): Add a fast(er) path here. return true; } return false; } // Calculates the running total length of the resulting string. If the // calculated length exceeds the maximum string length (see // String::kMaxLength), throws a range error. macro AddStringLength(implicit context: Context)( lenA: intptr, lenB: intptr): intptr { try { const length: intptr = TryIntPtrAdd(lenA, lenB) otherwise IfOverflow; if (length > kStringMaxLength) goto IfOverflow; return length; } label IfOverflow deferred { ThrowInvalidStringLength(context); } } // Stores an element to a fixed array and return the fixed array. If the fixed // array is not large enough, create and return a new, larger fixed array that // contains all previously elements and the new element. macro StoreAndGrowFixedArray( fixedArray: FixedArray, index: intptr, element: T): FixedArray { const length: intptr = fixedArray.length_intptr; dcheck(index <= length); if (index < length) { fixedArray.objects[index] = element; return fixedArray; } else deferred { const newLength: intptr = CalculateNewElementsCapacity(length); dcheck(index < newLength); const newfixedArray: FixedArray = ExtractFixedArray(fixedArray, 0, length, newLength); newfixedArray.objects[index] = element; return newfixedArray; } } // Contains the information necessary to create a single, separator delimited, // flattened one or two byte string. // The buffer is maintained and updated by Buffer.constructor, Buffer.Add(), // Buffer.AddSeparators(). struct Buffer { macro Add(implicit context: Context)( str: String, nofSeparators: intptr, separatorLength: intptr): void { // Add separators if necessary (at the beginning or more than one) const writeSeparators: bool = this.index == 0 | nofSeparators > 1; this.AddSeparators(nofSeparators, separatorLength, writeSeparators); this.totalStringLength = AddStringLength(this.totalStringLength, str.length_intptr); this.fixedArray = StoreAndGrowFixedArray(this.fixedArray, this.index++, str); this.isOneByte = IsOneByteStringInstanceType(str.instanceType) & this.isOneByte; } macro AddSeparators(implicit context: Context)( nofSeparators: intptr, separatorLength: intptr, write: bool): void { if (nofSeparators == 0 || separatorLength == 0) return; const nofSeparatorsInt: intptr = nofSeparators; const sepsLen: intptr = separatorLength * nofSeparatorsInt; // Detect integer overflow // TODO(turbofan): Replace with overflow-checked multiplication. if (sepsLen / separatorLength != nofSeparatorsInt) deferred { ThrowInvalidStringLength(context); } this.totalStringLength = AddStringLength(this.totalStringLength, sepsLen); if (write) deferred { this.fixedArray = StoreAndGrowFixedArray( this.fixedArray, this.index++, Convert(nofSeparatorsInt)); } } // Fixed array holding elements that are either: // 1) String result of `ToString(next)`. // 2) Smi representing the number of consecutive separators. // `BufferJoin()` will iterate and writes these entries to a flat string. // // To save space, reduce reads and writes, only separators at the beginning, // end, or more than one are written. // // No hole example // receiver: ['hello', 'world'] // fixedArray: ['hello', 'world'] // // Hole example // receiver: [, 'hello', , 'world', ] // fixedArray: [1, 'hello', 2, 'world', 1] fixedArray: FixedArray; // Index to insert a new entry into `fixedArray`. index: intptr; // Running total of the resulting string length. totalStringLength: intptr; // `true` if the separator and all strings in the buffer are one-byte, // otherwise `false`. isOneByte: bool; } macro NewBuffer(len: uintptr, sep: String): Buffer { const cappedBufferSize: intptr = len > kMaxNewSpaceFixedArrayElements ? kMaxNewSpaceFixedArrayElements : Signed(len); dcheck(cappedBufferSize > 0); return Buffer{ fixedArray: AllocateZeroedFixedArray(cappedBufferSize), index: 0, totalStringLength: 0, isOneByte: IsOneByteStringInstanceType(sep.instanceType) }; } macro BufferJoin(implicit context: Context)( buffer: Buffer, sep: String): String { dcheck(IsValidPositiveSmi(buffer.totalStringLength)); if (buffer.totalStringLength == 0) return kEmptyString; // Fast path when there's only one buffer element. if (buffer.index == 1) { const fixedArray: FixedArray = buffer.fixedArray; typeswitch (fixedArray.objects[0]) { // When the element is a string, just return it and completely avoid // allocating another string. case (str: String): { return str; } // When the element is a smi, use StringRepeat to quickly build a memory // efficient separator repeated string. case (nofSeparators: Number): { return StringRepeat(context, sep, nofSeparators); } case (Object): { unreachable; } } } const length: uint32 = Convert(Unsigned(buffer.totalStringLength)); const r: String = buffer.isOneByte ? AllocateSeqOneByteString(length) : AllocateSeqTwoByteString(length); return CallJSArrayArrayJoinConcatToSequentialString( buffer.fixedArray, buffer.index, sep, r); } transitioning macro ArrayJoinImpl(implicit context: Context)( receiver: JSReceiver, sep: String, lengthNumber: Number, useToLocaleString: constexpr bool, locales: JSAny, options: JSAny, initialLoadFn: LoadJoinElementFn): String { const initialMap: Map = receiver.map; const len: uintptr = Convert(lengthNumber); const separatorLength: intptr = sep.length_intptr; let nofSeparators: intptr = 0; let loadFn: LoadJoinElementFn = initialLoadFn; let buffer: Buffer = NewBuffer(len, sep); // 6. Let k be 0. let k: uintptr = 0; // 7. Repeat, while k < len while (k < len) { if (CannotUseSameArrayAccessor( loadFn, receiver, initialMap, lengthNumber)) deferred { loadFn = LoadJoinElement; } if (k > 0) { // a. If k > 0, let R be the string-concatenation of R and sep. nofSeparators = nofSeparators + 1; } // b. Let element be ? Get(O, ! ToString(k)). const element: JSAny = loadFn(context, receiver, k++); // c. If element is undefined or null, let next be the empty String; // otherwise, let next be ? ToString(element). let next: String; if constexpr (useToLocaleString) { next = ConvertToLocaleString(context, element, locales, options); if (next == kEmptyString) continue; } else { typeswitch (element) { case (str: String): { if (str == kEmptyString) continue; next = str; } case (num: Number): { next = NumberToString(num); } case (obj: JSAny): { if (IsNullOrUndefined(obj)) continue; next = string::ToString(context, obj); } } } // d. Set R to the string-concatenation of R and next. buffer.Add(next, nofSeparators, separatorLength); nofSeparators = 0; } // Add any separators at the end. buffer.AddSeparators(nofSeparators, separatorLength, true); // 8. Return R. return BufferJoin(buffer, sep); } transitioning macro ArrayJoin(implicit context: Context)( useToLocaleString: constexpr bool, receiver: JSReceiver, sep: String, lenNumber: Number, locales: JSAny, options: JSAny): JSAny; transitioning ArrayJoin(implicit context: Context)( useToLocaleString: constexpr bool, receiver: JSReceiver, sep: String, lenNumber: Number, locales: JSAny, options: JSAny): JSAny { const map: Map = receiver.map; const kind: ElementsKind = map.elements_kind; let loadFn: LoadJoinElementFn; try { const array: JSArray = Cast(receiver) otherwise IfSlowPath; if (array.length != lenNumber) goto IfSlowPath; if (!IsPrototypeInitialArrayPrototype(map)) goto IfSlowPath; if (IsNoElementsProtectorCellInvalid()) goto IfSlowPath; if (IsElementsKindLessThanOrEqual(kind, ElementsKind::HOLEY_ELEMENTS)) { loadFn = LoadJoinElement; } else if (IsElementsKindLessThanOrEqual( kind, ElementsKind::HOLEY_DOUBLE_ELEMENTS)) { loadFn = LoadJoinElement; } else if (kind == ElementsKind::DICTIONARY_ELEMENTS) deferred { const dict: NumberDictionary = UnsafeCast(array.elements); const nofElements: Smi = GetNumberDictionaryNumberOfElements(dict); if (nofElements == 0) { if (sep == kEmptyString) return kEmptyString; try { const nofSeparators: Smi = Cast(lenNumber - 1) otherwise IfNotSmi; return StringRepeat(context, sep, nofSeparators); } label IfNotSmi { ThrowInvalidStringLength(context); } } else { loadFn = LoadJoinElement; } } else { goto IfSlowPath; } } label IfSlowPath { loadFn = LoadJoinElement; } return ArrayJoinImpl( receiver, sep, lenNumber, useToLocaleString, locales, options, loadFn); } transitioning ArrayJoin(implicit context: Context)( useToLocaleString: constexpr bool, receiver: JSReceiver, sep: String, lenNumber: Number, locales: JSAny, options: JSAny): JSAny { const map: Map = receiver.map; const kind: ElementsKind = map.elements_kind; let loadFn: LoadJoinElementFn; if (IsElementsKindGreaterThan(kind, ElementsKind::UINT32_ELEMENTS)) { if (kind == ElementsKind::INT32_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == ElementsKind::FLOAT32_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == ElementsKind::FLOAT64_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == ElementsKind::UINT8_CLAMPED_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == ElementsKind::BIGUINT64_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == ElementsKind::BIGINT64_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == ElementsKind::RAB_GSAB_UINT8_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == ElementsKind::RAB_GSAB_INT8_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == ElementsKind::RAB_GSAB_UINT16_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == ElementsKind::RAB_GSAB_INT16_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == ElementsKind::RAB_GSAB_UINT32_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == ElementsKind::RAB_GSAB_INT32_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == ElementsKind::RAB_GSAB_FLOAT32_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == ElementsKind::RAB_GSAB_FLOAT64_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == ElementsKind::RAB_GSAB_UINT8_CLAMPED_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == ElementsKind::RAB_GSAB_BIGUINT64_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == ElementsKind::RAB_GSAB_BIGINT64_ELEMENTS) { loadFn = LoadJoinTypedElement; } else { unreachable; } } else { if (kind == ElementsKind::UINT8_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == ElementsKind::INT8_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == ElementsKind::UINT16_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == ElementsKind::INT16_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == ElementsKind::UINT32_ELEMENTS) { loadFn = LoadJoinTypedElement; } else { unreachable; } } return ArrayJoinImpl( receiver, sep, lenNumber, useToLocaleString, locales, options, loadFn); } // The Join Stack detects cyclical calls to Array Join builtins // (Array.p.join(), Array.p.toString(), Array.p.toLocaleString()). This // FixedArray holds a stack of receivers to the current call. // CycleProtectedArrayJoin() is responsible for calling JoinStackPush and // JoinStackPop when visiting and leaving a receiver, respectively. const kMinJoinStackSize: constexpr int31 generates 'JSArray::kMinJoinStackSize'; macro LoadJoinStack(implicit context: Context)(): FixedArray labels IfUninitialized { typeswitch (*NativeContextSlot(ContextSlot::ARRAY_JOIN_STACK_INDEX)) { case (Undefined): { goto IfUninitialized; } case (stack: FixedArray): { return stack; } } } macro SetJoinStack(implicit context: Context)(stack: FixedArray): void { *NativeContextSlot(ContextSlot::ARRAY_JOIN_STACK_INDEX) = stack; } // Adds a receiver to the stack. The FixedArray will automatically grow to // accommodate the receiver. If the receiver already exists on the stack, // this indicates a cyclical call and False is returned. builtin JoinStackPush(implicit context: Context)( stack: FixedArray, receiver: JSReceiver): Boolean { const capacity: intptr = stack.length_intptr; for (let i: intptr = 0; i < capacity; i++) { const previouslyVisited: Object = stack.objects[i]; // Add `receiver` to the first open slot if (previouslyVisited == TheHole) { stack.objects[i] = receiver; return True; } // Detect cycles if (receiver == previouslyVisited) return False; } // If no open slots were found, grow the stack and add receiver to the end. const newStack: FixedArray = StoreAndGrowFixedArray(stack, capacity, receiver); SetJoinStack(newStack); return True; } // Fast path the common non-nested calls. If the receiver is not already on // the stack, add it to the stack and return true. Otherwise return false. macro JoinStackPushInline(implicit context: Context)(receiver: JSReceiver): bool { try { const stack: FixedArray = LoadJoinStack() otherwise IfUninitialized; if (stack.objects[0] == TheHole) { stack.objects[0] = receiver; } else if (JoinStackPush(stack, receiver) == False) deferred { return false; } } label IfUninitialized { const stack: FixedArray = AllocateFixedArrayWithHoles(kMinJoinStackSize, AllocationFlag::kNone); stack.objects[0] = receiver; SetJoinStack(stack); } return true; } // Removes a receiver from the stack. The FixedArray will automatically shrink // to Heap::kMinJoinStackSize once the stack becomes empty. builtin JoinStackPop(implicit context: Context)( stack: FixedArray, receiver: JSReceiver): JSAny { const len: intptr = stack.length_intptr; for (let i: intptr = 0; i < len; i++) { if (stack.objects[i] == receiver) { // Shrink the Join Stack if the stack will be empty and is larger than // the minimum size. if (i == 0 && len > kMinJoinStackSize) deferred { const newStack: FixedArray = AllocateFixedArrayWithHoles( kMinJoinStackSize, AllocationFlag::kNone); SetJoinStack(newStack); } else { stack.objects[i] = TheHole; } return Undefined; } } unreachable; } // Fast path the common non-nested calls. macro JoinStackPopInline(implicit context: Context)(receiver: JSReceiver): void { const stack: FixedArray = LoadJoinStack() otherwise unreachable; const len: intptr = stack.length_intptr; // Builtin call was not nested (receiver is the first entry) and // did not contain other nested arrays that expanded the stack. if (stack.objects[0] == receiver && len == kMinJoinStackSize) { stack.objects[0] = TheHole; } else deferred { JoinStackPop(stack, receiver); } } // Main entry point for all builtins using Array Join functionality. transitioning macro CycleProtectedArrayJoin( implicit context: Context)( useToLocaleString: constexpr bool, o: JSReceiver, len: Number, sepObj: JSAny, locales: JSAny, options: JSAny): JSAny { // 3. If separator is undefined, let sep be the single-element String ",". // 4. Else, let sep be ? ToString(separator). const sep: String = sepObj == Undefined ? ',' : ToString_Inline(sepObj); // If the receiver is not empty and not already being joined, continue with // the normal join algorithm. if (len > 0 && JoinStackPushInline(o)) { try { const result: JSAny = ArrayJoin(useToLocaleString, o, sep, len, locales, options); JoinStackPopInline(o); return result; } catch (e, message) deferred { JoinStackPopInline(o); ReThrowWithMessage(context, e, message); } } else { return kEmptyString; } } // https://tc39.github.io/ecma262/#sec-array.prototype.join transitioning javascript builtin ArrayPrototypeJoin( js-implicit context: NativeContext, receiver: JSAny)(...arguments): JSAny { const separator: JSAny = arguments[0]; // 1. Let O be ? ToObject(this value). const o: JSReceiver = ToObject_Inline(context, receiver); // 2. Let len be ? ToLength(? Get(O, "length")). const len: Number = GetLengthProperty(o); // Only handle valid array lengths. Although the spec allows larger // values, this matches historical V8 behavior. if (len > kMaxArrayLength) { ThrowTypeError(MessageTemplate::kInvalidArrayLength); } return CycleProtectedArrayJoin( false, o, len, separator, Undefined, Undefined); } // https://tc39.github.io/ecma262/#sec-array.prototype.tolocalestring transitioning javascript builtin ArrayPrototypeToLocaleString( js-implicit context: NativeContext, receiver: JSAny)(...arguments): JSAny { const locales: JSAny = arguments[0]; const options: JSAny = arguments[1]; // 1. Let O be ? ToObject(this value). const o: JSReceiver = ToObject_Inline(context, receiver); // 2. Let len be ? ToLength(? Get(O, "length")). const len: Number = GetLengthProperty(o); // Only handle valid array lengths. Although the spec allows larger // values, this matches historical V8 behavior. if (len > kMaxArrayLength) { ThrowTypeError(MessageTemplate::kInvalidArrayLength); } return CycleProtectedArrayJoin(true, o, len, ',', locales, options); } // https://tc39.github.io/ecma262/#sec-array.prototype.tostring transitioning javascript builtin ArrayPrototypeToString( js-implicit context: NativeContext, receiver: JSAny)(...arguments): JSAny { // 1. Let array be ? ToObject(this value). const array: JSReceiver = ToObject_Inline(context, receiver); // 2. Let func be ? Get(array, "join"). const prop: JSAny = GetProperty(array, 'join'); try { // 3. If IsCallable(func) is false, let func be the intrinsic function // %ObjProto_toString%. const func: Callable = Cast(prop) otherwise NotCallable; // 4. Return ? Call(func, array). return Call(context, func, array); } label NotCallable { return ObjectToString(context, array); } } // https://tc39.github.io/ecma262/#sec-%typedarray%.prototype.join transitioning javascript builtin TypedArrayPrototypeJoin( js-implicit context: NativeContext, receiver: JSAny)(...arguments): JSAny { const separator: JSAny = arguments[0]; // Spec: ValidateTypedArray is applied to the this value prior to evaluating // the algorithm. const length = typed_array::ValidateTypedArrayAndGetLength( context, receiver, '%TypedArray%.prototype.join'); const typedArray: JSTypedArray = UnsafeCast(receiver); return CycleProtectedArrayJoin( false, typedArray, Convert(length), separator, Undefined, Undefined); } // https://tc39.github.io/ecma262/#sec-%typedarray%.prototype.tolocalestring transitioning javascript builtin TypedArrayPrototypeToLocaleString( js-implicit context: NativeContext, receiver: JSAny)(...arguments): JSAny { const locales: JSAny = arguments[0]; const options: JSAny = arguments[1]; // Spec: ValidateTypedArray is applied to the this value prior to evaluating // the algorithm. const length = typed_array::ValidateTypedArrayAndGetLength( context, receiver, '%TypedArray%.prototype.toLocaleString'); const typedArray: JSTypedArray = UnsafeCast(receiver); return CycleProtectedArrayJoin( true, typedArray, Convert(length), ',', locales, options); } }