// 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_join { type LoadJoinElementFn = builtin(Context, JSReceiver, Number) => JSAny; // 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: Number): JSAny { return GetProperty(receiver, k); } transitioning LoadJoinElement( context: Context, receiver: JSReceiver, k: Number): JSAny { const array: JSArray = UnsafeCast(receiver); const dict: NumberDictionary = UnsafeCast(array.elements); try { return BasicLoadNumberDictionaryElement(dict, Signed(Convert(k))) otherwise IfNoData, IfHole; } label IfNoData deferred { return GetProperty(receiver, k); } label IfHole { return kEmptyString; } } LoadJoinElement( context: Context, receiver: JSReceiver, k: Number): JSAny { const array: JSArray = UnsafeCast(receiver); const fixedArray: FixedArray = UnsafeCast(array.elements); const element: Object = fixedArray.objects[UnsafeCast(k)]; return element == TheHole ? kEmptyString : UnsafeCast(element); } LoadJoinElement( context: Context, receiver: JSReceiver, k: Number): JSAny { const array: JSArray = UnsafeCast(receiver); const fixedDoubleArray: FixedDoubleArray = UnsafeCast(array.elements); const element: float64 = LoadDoubleWithHoleCheck( fixedDoubleArray, UnsafeCast(k)) otherwise return kEmptyString; return AllocateHeapNumberWithValue(element); } builtin LoadJoinTypedElement( context: Context, receiver: JSReceiver, k: Number): JSAny { const typedArray: JSTypedArray = UnsafeCast(receiver); assert(!IsDetachedBuffer(typedArray.buffer)); return typed_array::LoadFixedTypedArrayElementAsTagged( typedArray.data_ptr, UnsafeCast(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(context, result); } label TypeError { ThrowTypeError(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); return IsDetachedBuffer(typedArray.buffer); } // 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; assert(index <= length); if (index < length) { fixedArray.objects[index] = element; return fixedArray; } else deferred { const newLength: intptr = CalculateNewElementsCapacity(length); assert(index < newLength); const newfixedArray: FixedArray = ExtractFixedArray(fixedArray, 0, length, newLength, kFixedArrays); 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 { Add(implicit context: Context)( str: String, nofSeparators: intptr, separatorLength: intptr) { // 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; } AddSeparators(implicit context: Context)( nofSeparators: intptr, separatorLength: intptr, write: bool) { if (nofSeparators == 0 || separatorLength == 0) return; const nofSeparatorsInt: intptr = nofSeparators; const sepsLen: intptr = separatorLength * nofSeparatorsInt; // Detect integer overflow // TODO(tebbi): 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); assert(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 { assert(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, Convert(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 = 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, HOLEY_ELEMENTS)) { loadFn = LoadJoinElement; } else if (IsElementsKindLessThanOrEqual(kind, HOLEY_DOUBLE_ELEMENTS)) { loadFn = LoadJoinElement; } else if (kind == 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, UINT32_ELEMENTS)) { if (kind == INT32_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == FLOAT32_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == FLOAT64_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == UINT8_CLAMPED_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == BIGUINT64_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == BIGINT64_ELEMENTS) { loadFn = LoadJoinTypedElement; } else { unreachable; } } else { if (kind == UINT8_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == INT8_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == UINT16_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == INT16_ELEMENTS) { loadFn = LoadJoinTypedElement; } else if (kind == 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 { const nativeContext: NativeContext = LoadNativeContext(context); const stack: HeapObject = UnsafeCast(nativeContext[ARRAY_JOIN_STACK_INDEX]); if (stack == Undefined) goto IfUninitialized; assert(IsFixedArray(stack)); return UnsafeCast(stack); } macro SetJoinStack(implicit context: Context)(stack: FixedArray): void { const nativeContext: NativeContext = LoadNativeContext(context); nativeContext[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, 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, 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) { 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) { StoreFixedArrayElement(stack, 0, TheHole, SKIP_WRITE_BARRIER); } 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(context, 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) deferred { JoinStackPopInline(o); ReThrow(context, e); } } else { return kEmptyString; } } // https://tc39.github.io/ecma262/#sec-array.prototype.join transitioning javascript builtin ArrayPrototypeJoin(js-implicit context: Context, 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 > kMaxArrayIndex + 1) ThrowTypeError(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: Context, 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 > kMaxArrayIndex + 1) ThrowTypeError(kInvalidArrayLength); return CycleProtectedArrayJoin( true, o, len, ',', locales, options); } // https://tc39.github.io/ecma262/#sec-array.prototype.tostring transitioning javascript builtin ArrayPrototypeToString( js-implicit context: Context, 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: Context, receiver: JSAny)(...arguments): JSAny { const separator: JSAny = arguments[0]; // Spec: ValidateTypedArray is applied to the this value prior to evaluating // the algorithm. const typedArray: JSTypedArray = typed_array::ValidateTypedArray( context, receiver, '%TypedArray%.prototype.join'); const length = Convert(typedArray.length); return CycleProtectedArrayJoin( false, typedArray, length, separator, Undefined, Undefined); } // https://tc39.github.io/ecma262/#sec-%typedarray%.prototype.tolocalestring transitioning javascript builtin TypedArrayPrototypeToLocaleString( js-implicit context: Context, 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 typedArray: JSTypedArray = typed_array::ValidateTypedArray( context, receiver, '%TypedArray%.prototype.toLocaleString'); const length = Convert(typedArray.length); return CycleProtectedArrayJoin( true, typedArray, length, ',', locales, options); } }