From 952c097679c5e16ae214595ad3b01381483eab7b Mon Sep 17 00:00:00 2001 From: peterwmwong Date: Tue, 16 Oct 2018 07:52:25 -0500 Subject: [PATCH] [builtins] Port Array.p.join to Torque. This also includes ports of Array.p.toString and Array.p.toLocaleString. Many parts of the old JS implementation are preserved, because TypedArray.p.join still relies on it. These will be removed once TypedArray.p.join is ported to Torque. To simplify implementation, special handling of extremely sparse arrays has been removed. Performance improvements vary by array size, elements, and sparse-ness. Some quick numbers and graphs are here: https://docs.google.com/spreadsheets/d/125VLmRMudk8XaomLCsZQ1ewc94WCqht-8GQwU3s9BW8/edit#gid=2087673710 Cq-Include-Trybots: luci.chromium.try:linux_chromium_headless_rel;luci.v8.try:v8_linux_noi18n_rel_ng;master.tryserver.blink:linux_trusty_blink_rel Change-Id: Ia4069a068403ce36676c37401d349aefc976b045 Reviewed-on: https://chromium-review.googlesource.com/c/1196693 Commit-Queue: Peter Wong Reviewed-by: Jakob Gruber Reviewed-by: Tobias Tebbi Cr-Commit-Position: refs/heads/master@{#56699} --- BUILD.gn | 1 + src/bootstrapper.cc | 6 + src/builtins/array-join.tq | 526 ++++++++++++++++++ src/builtins/base.tq | 102 +++- src/builtins/builtins-array-gen.h | 60 ++ src/builtins/builtins-definitions.h | 1 + src/builtins/builtins-object-gen.cc | 8 +- src/code-stub-assembler.cc | 34 +- src/code-stub-assembler.h | 45 ++ src/contexts.h | 1 + src/debug/debug-evaluate.cc | 3 + src/external-reference.cc | 6 + src/external-reference.h | 10 +- src/js/array.js | 65 +-- src/objects.cc | 105 ++++ src/objects/js-array.h | 25 + src/runtime/runtime-strings.cc | 3 + .../debug-evaluate-no-side-effect-builtins.js | 4 +- .../mjsunit/array-functions-prototype-misc.js | 14 - ...element-tostring-prototype-side-effects.js | 14 + ...rray-join-element-tostring-side-effects.js | 152 +++++ .../array-join-index-getter-side-effects.js | 87 +++ test/mjsunit/array-join-nesting.js | 16 + ...oin-nonarray-length-getter-side-effects.js | 29 + ...ay-join-separator-tostring-side-effects.js | 197 +++++++ test/mjsunit/array-join.js | 14 +- test/mjsunit/array-tolocalestring.js | 72 +++ .../fast/js/toString-overrides-expected.txt | 4 +- tools/v8heapconst.py | 2 +- 29 files changed, 1512 insertions(+), 94 deletions(-) create mode 100644 src/builtins/array-join.tq create mode 100644 test/mjsunit/array-join-element-tostring-prototype-side-effects.js create mode 100644 test/mjsunit/array-join-element-tostring-side-effects.js create mode 100644 test/mjsunit/array-join-index-getter-side-effects.js create mode 100644 test/mjsunit/array-join-nesting.js create mode 100644 test/mjsunit/array-join-nonarray-length-getter-side-effects.js create mode 100644 test/mjsunit/array-join-separator-tostring-side-effects.js create mode 100644 test/mjsunit/array-tolocalestring.js diff --git a/BUILD.gn b/BUILD.gn index f4d7c6b239..a31a65b7c1 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -908,6 +908,7 @@ torque_files = [ "src/builtins/array.tq", "src/builtins/array-copywithin.tq", "src/builtins/array-foreach.tq", + "src/builtins/array-join.tq", "src/builtins/array-lastindexof.tq", "src/builtins/array-reverse.tq", "src/builtins/array-splice.tq", diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc index e8c98d21f9..27eaa4cb0c 100644 --- a/src/bootstrapper.cc +++ b/src/bootstrapper.cc @@ -1748,6 +1748,8 @@ void Genesis::InitializeGlobal(Handle global_object, 1, false); SimpleInstallFunction(isolate_, proto, "indexOf", Builtins::kArrayIndexOf, 1, false); + SimpleInstallFunction(isolate_, proto, "join", + Builtins::kArrayPrototypeJoin, 1, false); SimpleInstallFunction(isolate_, proto, "keys", Builtins::kArrayPrototypeKeys, 0, true, BuiltinFunctionId::kArrayKeys); @@ -1771,6 +1773,10 @@ void Genesis::InitializeGlobal(Handle global_object, false); SimpleInstallFunction(isolate_, proto, "reduceRight", Builtins::kArrayReduceRight, 1, false); + SimpleInstallFunction(isolate_, proto, "toLocaleString", + Builtins::kArrayPrototypeToLocaleString, 0, false); + SimpleInstallFunction(isolate_, proto, "toString", + Builtins::kArrayPrototypeToString, 0, false); } { // --- A r r a y I t e r a t o r --- diff --git a/src/builtins/array-join.tq b/src/builtins/array-join.tq new file mode 100644 index 0000000000..6db1fce89c --- /dev/null +++ b/src/builtins/array-join.tq @@ -0,0 +1,526 @@ +// 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. + +module array { + type LoadJoinElementFn = builtin(Context, JSReceiver, Number) => Object; + + // Fast C call to write a fixed array (see Buffer.fixedArray) to a single + // string. + extern macro CallJSArrayArrayJoinConcatToSequentialString( + FixedArray, intptr, String, String): String; + + extern macro CallLoadJoinElement(implicit context: Context)( + LoadJoinElementFn, JSReceiver, Number): Object + labels IfException(Object); + + extern macro CallConvertToLocaleString(implicit context: Context)( + Object, Object, Object): String + labels IfException(Object); + + extern macro CallToString(implicit context: Context)(Object): String + labels IfException(Object); + + builtin LoadJoinElement( + context: Context, receiver: JSReceiver, k: Number): Object { + return GetProperty(receiver, k); + } + + LoadJoinElement( + context: Context, receiver: JSReceiver, k: Number): Object { + 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): Object { + const array: JSArray = UnsafeCast(receiver); + const fixedArray: FixedArray = UnsafeCast(array.elements); + const element: Object = fixedArray[UnsafeCast(k)]; + return element == Hole ? kEmptyString : element; + } + + LoadJoinElement( + context: Context, receiver: JSReceiver, k: Number): Object { + const array: JSArray = UnsafeCast(receiver); + const fixedDoubleArray: FixedDoubleArray = + UnsafeCast(array.elements); + try { + const element: float64 = LoadDoubleWithHoleCheck( + fixedDoubleArray, UnsafeCast(k)) otherwise IfHole; + return AllocateHeapNumberWithValue(element); + } + label IfHole { + return kEmptyString; + } + } + + builtin ConvertToLocaleString( + context: Context, element: Object, locales: Object, + options: Object): String { + if (IsNullOrUndefined(element)) return kEmptyString; + + const prop: Object = GetProperty(element, 'toLocaleString'); + try { + const callable: Callable = Cast(prop) otherwise TypeError; + let result: Object; + 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(context, kCalledNonCallable, prop); + } + } + + // Verifies the current element JSArray accessor can still be safely used + // (see LoadJoinElement). + macro CannotUseSameArrayAccessor(implicit context: Context)( + originalMap: Object, originalLen: Object, receiver: JSReceiver): never + labels Cannot, Can { + const array: JSArray = UnsafeCast(receiver); + if (originalMap != array.map) goto Cannot; + if (originalLen != array.length) goto Cannot; + if (IsNoElementsProtectorCellInvalid()) goto Cannot; + goto Can; + } + + // 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 { + ThrowRangeError(context, kInvalidStringLength); + } + } + + // Contains the information necessary to create a single, separator delimited, + // flattened one or two byte string. + // The buffer is maintained and updated by BufferInit(), BufferAdd(), + // BufferAddSeparators(). + struct Buffer { + // 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 BufferInit(estimatedNonHoleyElements: uintptr, sep: String): Buffer { + const bufferSize: intptr = Signed(estimatedNonHoleyElements + 1); + const cappedBufferSize: intptr = bufferSize > kFixedArrayMaxLength ? + FromConstexpr(kFixedArrayMaxLength) : + bufferSize; + const fixedArray: FixedArray = AllocateZeroedFixedArray(cappedBufferSize); + const isOneByte: bool = HasOnlyOneByteChars(sep.instanceType); + return Buffer{fixedArray, 0, 0, isOneByte}; + } + + macro BufferAdd(implicit context: Context)( + initialBuffer: Buffer, str: String, nofSeparators: uintptr, + separatorLength: intptr): Buffer { + let buffer: Buffer = initialBuffer; + // Add separators if necessary (at the beginning or more than one) + const writeSeparators: bool = buffer.index == 0 | nofSeparators > 1; + buffer = BufferAddSeparators( + buffer, nofSeparators, separatorLength, writeSeparators); + + const totalStringLength: intptr = + AddStringLength(buffer.totalStringLength, str.length); + let index: intptr = buffer.index; + assert(index < buffer.fixedArray.length_intptr); + buffer.fixedArray[index++] = str; + const isOneByte: bool = + HasOnlyOneByteChars(str.instanceType) & buffer.isOneByte; + return Buffer{buffer.fixedArray, index, totalStringLength, isOneByte}; + } + + macro BufferAddSeparators(implicit context: Context)( + buffer: Buffer, nofSeparators: uintptr, separatorLength: intptr, + write: bool): Buffer { + if (nofSeparators == 0 || separatorLength == 0) return buffer; + + const nofSeparatorsInt: intptr = Signed(nofSeparators); + const sepsLen: intptr = separatorLength * nofSeparatorsInt; + // Detect integer overflow + if (nofSeparatorsInt <= 0 || sepsLen <= 0) deferred { + ThrowRangeError(context, kInvalidStringLength); + } + + const totalStringLength: intptr = + AddStringLength(buffer.totalStringLength, sepsLen); + let index: intptr = buffer.index; + if (write) deferred { + assert(index < buffer.fixedArray.length_intptr); + buffer.fixedArray[index++] = Convert(nofSeparatorsInt); + } + return Buffer{ + buffer.fixedArray, + index, + totalStringLength, + buffer.isOneByte + }; + } + + macro BufferJoin(implicit context: Context)( + buffer: Buffer, sep: String): String { + assert(IsValidPositiveSmi(buffer.totalStringLength)); + if (buffer.totalStringLength == 0) return kEmptyString; + + const length: uint32 = Convert(Unsigned(buffer.totalStringLength)); + const r: String = buffer.isOneByte ? AllocateSeqOneByteString(length) : + AllocateSeqTwoByteString(length); + return CallJSArrayArrayJoinConcatToSequentialString( + buffer.fixedArray, buffer.index, sep, r); + } + + macro ArrayJoinImpl( + context: Context, receiver: JSReceiver, sep: String, lengthNumber: Number, + useToLocaleString: constexpr bool, locales: Object, options: Object, + estimatedNonHoleyElements: uintptr, + initialLoadJoinElement: LoadJoinElementFn): String + labels IfException(Object) { + const initialMap: Map = receiver.map; + const len: uintptr = Convert(lengthNumber); + const separatorLength: intptr = sep.length; + let nofSeparators: uintptr = 0; + let loadJoinElements: LoadJoinElementFn = initialLoadJoinElement; + let buffer: Buffer = BufferInit(estimatedNonHoleyElements, sep); + + // 6. Let k be 0. + let k: uintptr = 0; + + if (estimatedNonHoleyElements != 0) { + // 7. Repeat, while k < len + while (k < len) { + if (k > 0) { + // a. If k > 0, let R be the string-concatenation of R and sep. + nofSeparators = nofSeparators + 1; + + // Verify the current LoadJoinElement specialization can safely be + // used. Otherwise, fall back to generic element access (see + // LoadJoinElement). + if (loadJoinElements != LoadJoinElement&& + CannotUseSameArrayAccessor(initialMap, lengthNumber, receiver)) + deferred { + loadJoinElements = LoadJoinElement; + + const temp: String = BufferJoin(buffer, sep); + buffer = BufferInit((len - k), sep); + buffer = BufferAdd(buffer, temp, 0, separatorLength); + } + } + + // b. Let element be ? Get(O, ! ToString(k)). + const element: Object = CallLoadJoinElement( + loadJoinElements, receiver, Convert(k++)) + otherwise IfException; + + // 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 = CallConvertToLocaleString(element, locales, options) + otherwise IfException; + if (next == kEmptyString) continue; + } else { + typeswitch (element) { + case (str: String): { + if (str == kEmptyString) continue; + next = str; + } + case (num: Number): { + next = NumberToString(num); + } + case (obj: HeapObject): { + if (IsNullOrUndefined(obj)) continue; + next = CallToString(obj) otherwise IfException; + } + } + } + + // d. Set R to the string-concatenation of R and next. + buffer = BufferAdd(buffer, next, nofSeparators, separatorLength); + nofSeparators = 0; + } + } else { + assert(len > k); + nofSeparators = len - k - 1; + } + + // Add any separators at the end. + buffer = BufferAddSeparators(buffer, nofSeparators, separatorLength, true); + + // 8. Return R. + return BufferJoin(buffer, sep); + } + + macro ArrayJoin(implicit context: Context)( + useToLocaleString: constexpr bool, receiver: JSReceiver, sep: String, + lenNumber: Number, locales: Object, options: Object): Object + labels IfException(Object) { + const map: Map = receiver.map; + const kind: ElementsKind = map.elements_kind; + const len: uintptr = Convert(lenNumber); + + // Estimated number of elements that are not holes. This is conservatively + // defaulted to `len`. When the receiver has dictionary elements, a better + // estimate can be determined through GetNumberDictionaryNumberOfElements. + let estimatedNonHoleyElements: uintptr = len; + let loadJoinElements: LoadJoinElementFn; + + try { + const array: JSArray = Cast(receiver) otherwise IfSlowPath; + if (array.length != lenNumber) goto IfSlowPath; + if (!IsPrototypeInitialArrayPrototype(context, map)) goto IfSlowPath; + if (IsNoElementsProtectorCellInvalid()) goto IfSlowPath; + + if (IsElementsKindLessThanOrEqual(kind, HOLEY_ELEMENTS)) { + loadJoinElements = LoadJoinElement; + } else if (IsElementsKindLessThanOrEqual(kind, HOLEY_DOUBLE_ELEMENTS)) { + loadJoinElements = LoadJoinElement; + } else if (kind == DICTIONARY_ELEMENTS) { + const dict: NumberDictionary = + UnsafeCast(array.elements); + estimatedNonHoleyElements = + Unsigned(Convert(GetNumberDictionaryNumberOfElements(dict))) + << 1; + loadJoinElements = LoadJoinElement; + } else { + goto IfSlowPath; + } + } + label IfSlowPath { + loadJoinElements = LoadJoinElement; + } + return ArrayJoinImpl( + context, receiver, sep, lenNumber, useToLocaleString, locales, options, + estimatedNonHoleyElements, loadJoinElements) + otherwise IfException; + } + + // 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[i]; + + // Add `receiver` to the first open slot + if (previouslyVisited == Hole) { + stack[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 newCapacity: intptr = CalculateNewElementsCapacity(capacity); + const newStack: FixedArray = + ExtractFixedArray(stack, 0, capacity, newCapacity, kFixedArrays); + newStack[capacity] = receiver; + SetJoinStack(newStack); + return True; + } + + // Fast path the common non-nested calls. + macro JoinStackPushInline(implicit context: Context)(receiver: JSReceiver) + labels CycleDetected { + try { + const stack: FixedArray = LoadJoinStack() + otherwise IfUninitialized; + if (stack[0] == Hole) { + stack[0] = receiver; + } else if (JoinStackPush(stack, receiver) == False) + deferred { + goto CycleDetected; + } + } + label IfUninitialized { + const stack: FixedArray = + AllocateFixedArrayWithHoles(kMinJoinStackSize, kNone); + stack[0] = receiver; + SetJoinStack(stack); + } + } + + // 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): Object { + const len: intptr = stack.length_intptr; + for (let i: intptr = 0; i < len; i++) { + if (stack[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[i] = Hole; + } + 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[0] == receiver && len == kMinJoinStackSize) { + StoreFixedArrayElement(stack, 0, Hole, SKIP_WRITE_BARRIER); + } else + deferred { + JoinStackPop(stack, receiver); + } + } + + // Main entry point for all builtins using Array Join functionality. + macro CycleProtectedArrayJoin(implicit context: Context)( + useToLocaleString: constexpr bool, receiver: Object, sepObj: Object, + locales: Object, options: Object): Object { + // 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); + // 3. If separator is undefined, let sep be the single-element String ",". + // 4. Else, let sep be ? ToString(separator). + let sep: String = sepObj == Undefined ? FromConstexpr(',') : + ToString_Inline(context, sepObj); + try { + // Fast paths for zero elements + if (len == 0) goto IfReturnEmpty; + + JoinStackPushInline(o) otherwise IfReturnEmpty; + + const result: Object = + ArrayJoin(useToLocaleString, o, sep, len, locales, options) + otherwise IfException; + + JoinStackPopInline(o); + return result; + } + label IfReturnEmpty { + return kEmptyString; + } + label IfException(e: Object) deferred { + JoinStackPopInline(o); + ReThrow(context, e); + unreachable; + } + } + + // https://tc39.github.io/ecma262/#sec-array.prototype.join + javascript builtin ArrayPrototypeJoin( + context: Context, receiver: Object, ...arguments): Object { + const separator: Object = arguments[0]; + return CycleProtectedArrayJoin( + false, receiver, separator, Undefined, Undefined); + } + + // https://tc39.github.io/ecma262/#sec-array.prototype.toLocaleString + javascript builtin ArrayPrototypeToLocaleString( + context: Context, receiver: Object, ...arguments): Object { + const locales: Object = arguments[0]; + const options: Object = arguments[1]; + return CycleProtectedArrayJoin(true, receiver, ',', locales, options); + } + + // https://tc39.github.io/ecma262/#sec-array.prototype.toString + javascript builtin ArrayPrototypeToString( + context: Context, receiver: Object, ...arguments): Object { + // 1. Let array be ? ToObject(this value). + const array: JSReceiver = ToObject_Inline(context, receiver); + + // 2. Let func be ? Get(array, "join"). + const prop: Object = 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); + } + } +} diff --git a/src/builtins/base.tq b/src/builtins/base.tq index 44493175bc..ea98844eec 100644 --- a/src/builtins/base.tq +++ b/src/builtins/base.tq @@ -56,6 +56,8 @@ type NumberDictionary extends HeapObject generates 'TNode'; type NativeContextSlot generates 'TNode' constexpr 'int32_t'; +const ARRAY_JOIN_STACK_INDEX: constexpr NativeContextSlot + generates 'Context::ARRAY_JOIN_STACK_INDEX'; const FAST_ALIASED_ARGUMENTS_MAP_INDEX: constexpr NativeContextSlot generates 'Context::FAST_ALIASED_ARGUMENTS_MAP_INDEX'; const SLOW_ALIASED_ARGUMENTS_MAP_INDEX: constexpr NativeContextSlot @@ -66,6 +68,8 @@ const SLOPPY_ARGUMENTS_MAP_INDEX: constexpr NativeContextSlot generates 'Context::SLOPPY_ARGUMENTS_MAP_INDEX'; extern operator '[]' macro LoadContextElement( NativeContext, NativeContextSlot): Object; +extern operator '[]=' macro StoreContextElement( + NativeContext, NativeContextSlot, Object): void; extern operator '[]' macro LoadContextElement(Context, intptr): Object; extern operator '[]' macro LoadContextElement(Context, Smi): Object; @@ -88,8 +92,8 @@ type WriteBarrierMode generates 'TNode' constexpr 'WriteBarrierMode'; type MessageTemplate constexpr 'MessageTemplate'; - type ToIntegerTruncationMode constexpr 'ToIntegerTruncationMode'; +type AllocationFlags constexpr 'AllocationFlags'; const NO_ELEMENTS: constexpr ElementsKind generates 'NO_ELEMENTS'; @@ -126,6 +130,13 @@ const BIGUINT64_ELEMENTS: const BIGINT64_ELEMENTS: constexpr ElementsKind generates 'BIGINT64_ELEMENTS'; +const kNone: constexpr AllocationFlags generates 'kNone'; +const kDoubleAlignment: + constexpr AllocationFlags generates 'kDoubleAlignment'; +const kPretenured: constexpr AllocationFlags generates 'kPretenured'; +const kAllowLargeObjectAllocation: + constexpr AllocationFlags generates 'kAllowLargeObjectAllocation'; + type FixedUint8Array extends FixedTypedArray; type FixedInt8Array extends FixedTypedArray; type FixedUint16Array extends FixedTypedArray; @@ -149,6 +160,8 @@ const kFixedCOWArrayMapRootIndex: constexpr RootIndex generates 'RootIndex::kFixedCOWArrayMap'; const kEmptyFixedArrayRootIndex: constexpr RootIndex generates 'RootIndex::kEmptyFixedArray'; +const kTheHoleValueRootIndex: + constexpr RootIndex generates 'RootIndex::kTheHoleValue'; const kInvalidArrayLength: constexpr MessageTemplate generates 'MessageTemplate::kInvalidArrayLength'; @@ -156,8 +169,13 @@ const kCalledNonCallable: constexpr MessageTemplate generates 'MessageTemplate::kCalledNonCallable'; const kCalledOnNullOrUndefined: constexpr MessageTemplate generates 'MessageTemplate::kCalledOnNullOrUndefined'; +const kInvalidStringLength: constexpr MessageTemplate + generates 'MessageTemplate::kInvalidStringLength'; const kMaxSafeInteger: constexpr float64 generates 'kMaxSafeInteger'; +const kStringMaxLength: constexpr int31 generates 'String::kMaxLength'; +const kFixedArrayMaxLength: + constexpr int31 generates 'FixedArray::kMaxLength'; const kTruncateMinusZero: constexpr ToIntegerTruncationMode generates 'ToIntegerTruncationMode::kTruncateMinusZero'; @@ -182,6 +200,7 @@ extern macro TrueConstant(): Boolean; extern macro FalseConstant(): Boolean; extern macro Int32TrueConstant(): bool; extern macro Int32FalseConstant(): bool; +extern macro EmptyStringConstant(): String; extern macro LengthStringConstant(): String; const Hole: Oddball = TheHoleConstant(); @@ -189,6 +208,7 @@ const Null: Oddball = NullConstant(); const Undefined: Oddball = UndefinedConstant(); const True: Boolean = TrueConstant(); const False: Boolean = FalseConstant(); +const kEmptyString: String = EmptyStringConstant(); const kLengthString: String = LengthStringConstant(); const true: constexpr bool generates 'true'; @@ -260,6 +280,8 @@ extern builtin StringLessThan(Context, String, String): Boolean; extern macro StrictEqual(Object, Object): Boolean; extern macro SmiLexicographicCompare(Smi, Smi): Smi; +// TODO(tebbi): This should have a return type of `never`. +extern runtime ReThrow(Context, Object); extern operator '<' macro Int32LessThan(int32, int32): bool; extern operator '>' macro Int32GreaterThan(int32, int32): bool; @@ -280,6 +302,10 @@ operator '!=' macro ElementsKindNotEqual( k1: ElementsKind, k2: ElementsKind): bool { return !ElementsKindEqual(k1, k2); } +extern macro IsElementsKindLessThanOrEqual( + ElementsKind, constexpr ElementsKind): bool; +extern macro IsElementsKindGreaterThan( + ElementsKind, constexpr ElementsKind): bool; extern macro IsFastElementsKind(constexpr ElementsKind): constexpr bool; extern macro IsDoubleElementsKind(constexpr ElementsKind): constexpr bool; @@ -290,10 +316,12 @@ extern operator '==' macro WordEqual(uintptr, uintptr): bool; extern operator '!=' macro WordNotEqual(intptr, intptr): bool; extern operator '!=' macro WordNotEqual(uintptr, uintptr): bool; extern operator '<' macro IntPtrLessThan(intptr, intptr): bool; +extern operator '<' macro UintPtrLessThan(uintptr, uintptr): bool; extern operator '>' macro IntPtrGreaterThan(intptr, intptr): bool; -extern operator '<=' macro IntPtrLessThanOrEqual(intptr, intptr): bool; -extern operator '>=' macro IntPtrGreaterThanOrEqual(intptr, intptr): bool; extern operator '>' macro UintPtrGreaterThan(uintptr, uintptr): bool; +extern operator '<=' macro IntPtrLessThanOrEqual(intptr, intptr): bool; +extern operator '<=' macro UintPtrLessThanOrEqual(uintptr, uintptr): bool; +extern operator '>=' macro IntPtrGreaterThanOrEqual(intptr, intptr): bool; extern operator '>=' macro UintPtrGreaterThanOrEqual(uintptr, uintptr): bool; extern operator '==' macro Float64Equal(float64, float64): bool; @@ -302,6 +330,8 @@ extern operator '>' macro Float64GreaterThan(float64, float64): bool; extern operator '==' macro BranchIfNumberEqual(Number, Number): never labels Taken, NotTaken; +extern operator '!=' macro BranchIfNumberNotEqual(Number, Number): never + labels Taken, NotTaken; extern operator '<' macro BranchIfNumberLessThan(Number, Number): never labels Taken, NotTaken; extern operator '<=' macro BranchIfNumberLessThanOrEqual(Number, Number): never @@ -324,12 +354,18 @@ extern operator '>>>' macro SmiShr(Smi, constexpr int31): Smi; extern operator '<<' macro SmiShl(Smi, constexpr int31): Smi; extern operator '+' macro IntPtrAdd(intptr, intptr): intptr; -extern operator '+' macro UintPtrAdd(uintptr, uintptr): uintptr; extern operator '-' macro IntPtrSub(intptr, intptr): intptr; -extern operator '>>>' macro WordShr(uintptr, uintptr): uintptr; +extern operator '*' macro IntPtrMul(intptr, intptr): intptr; extern operator '<<' macro WordShl(intptr, intptr): intptr; extern operator '&' macro WordAnd(intptr, intptr): intptr; +extern operator '|' macro WordOr(intptr, intptr): intptr; + +extern operator '+' macro UintPtrAdd(uintptr, uintptr): uintptr; +extern operator '-' macro UintPtrSub(uintptr, uintptr): uintptr; +extern operator '>>>' macro WordShr(uintptr, uintptr): uintptr; +extern operator '<<' macro WordShl(uintptr, uintptr): uintptr; extern operator '&' macro WordAnd(uintptr, uintptr): uintptr; +extern operator '|' macro WordOr(uintptr, uintptr): uintptr; extern operator '+' macro Int32Add(int32, int32): int32; extern operator '-' macro Int32Sub(int32, int32): int32; @@ -349,6 +385,8 @@ extern operator '<<' macro Word32Shl(int32, int32): int32; extern operator '<<' macro Word32Shl(uint32, uint32): uint32; extern operator '|' macro Word32Or(int32, int32): int32; extern operator '|' macro Word32Or(uint32, uint32): uint32; +extern operator '&' macro Word32And(bool, bool): bool; +extern operator '|' macro Word32Or(bool, bool): bool; extern operator '+' macro Float64Add(float64, float64): float64; @@ -384,6 +422,7 @@ extern operator '[]' macro GetArgumentValue( extern macro TaggedIsSmi(Object): bool; extern macro TaggedIsNotSmi(Object): bool; extern macro TaggedIsPositiveSmi(Object): bool; +extern macro IsValidPositiveSmi(intptr): bool; extern macro HeapObjectToJSDataView(HeapObject): JSDataView labels CastError; @@ -399,6 +438,10 @@ extern macro HeapObjectToFixedArray(HeapObject): FixedArray labels CastError; extern macro HeapObjectToFixedDoubleArray(HeapObject): FixedDoubleArray labels CastError; +extern macro HeapObjectToString(HeapObject): String + labels CastError; +extern macro HeapObjectToHeapNumber(HeapObject): HeapNumber + labels CastError; extern macro TaggedToNumber(Object): Number labels CastError; @@ -428,6 +471,10 @@ CastHeapObject(o: HeapObject): JSArray labels CastError { return HeapObjectToJSArray(o) otherwise CastError; } +CastHeapObject(o: HeapObject): String + labels CastError { + return HeapObjectToString(o) otherwise CastError; +} macro Cast(o: HeapObject): A labels CastError { @@ -475,6 +522,7 @@ extern macro ChangeInt32ToIntPtr(int32): intptr; // Sign-extends. extern macro ChangeUint32ToWord(uint32): uintptr; // Doesn't sign-extend. extern macro LoadNativeContext(Context): NativeContext; extern macro LoadJSArrayElementsMap(constexpr ElementsKind, Context): Map; +extern macro ChangeNonnegativeNumberToUintPtr(Number): uintptr; extern macro NumberConstant(constexpr float64): Number; extern macro NumberConstant(constexpr int32): Number; @@ -610,6 +658,9 @@ macro Convert(n: Number): A; Convert(n: Number): float64 { return ChangeNumberToFloat64(n); } +Convert(n: Number): uintptr { + return ChangeNonnegativeNumberToUintPtr(n); +} macro Convert(f: float32): A; Convert(f: float32): float64 { return ChangeFloat32ToFloat64(f); @@ -651,6 +702,7 @@ extern macro UnsafeCastObjectToNumberDictionary(Object): NumberDictionary; extern macro UnsafeCastObjectToJSReceiver(Object): JSReceiver; extern macro UnsafeCastObjectToJSObject(Object): JSObject; extern macro UnsafeCastObjectToMap(Object): Map; +extern macro UnsafeCastObjectToString(Object): String; macro UnsafeCast(n: Number): A; UnsafeCast(n: Number): HeapNumber { @@ -705,6 +757,9 @@ UnsafeCast(o: Object): FixedArrayBase { UnsafeCast(o: Object): Context { return UnsafeCastObjectToContext(o); } +UnsafeCast(o: Object): String { + return UnsafeCastObjectToString(o); +} // RawCasts should *never* be used anywhere in Torque code except for // in Torque-based UnsafeCast operators preceeded by an appropriate @@ -781,14 +836,20 @@ extern operator '.length_fast' macro LoadFastJSArrayLength(JSArray): Smi; extern operator '.length=' macro StoreJSArrayLength(JSArray, Smi); extern operator '.length' macro LoadFixedArrayBaseLength(FixedArrayBase): Smi; +extern operator '.length_intptr' macro LoadAndUntagFixedArrayBaseLength( + FixedArrayBase): intptr; extern operator '[]' macro LoadFixedArrayElement(FixedArray, intptr): Object; extern operator '[]' macro LoadFixedArrayElement(FixedArray, Smi): Object; extern operator '[]' macro LoadFixedArrayElement( FixedArray, constexpr int31): Object; extern operator '[]=' macro StoreFixedArrayElement( - FixedArray, intptr, Object): void; + FixedArray, intptr, Smi): void; extern operator '[]=' macro StoreFixedArrayElement( - FixedArray, constexpr int31, Object): void; + FixedArray, intptr, HeapObject): void; +extern operator '[]=' macro StoreFixedArrayElement( + FixedArray, constexpr int31, Smi): void; +extern operator '[]=' macro StoreFixedArrayElement( + FixedArray, constexpr int31, HeapObject): void; extern operator '[]=' macro StoreFixedArrayElementSmi( FixedArray, Smi, Object): void; operator '[]=' macro StoreFixedDoubleArrayNumber( @@ -806,10 +867,14 @@ extern macro Float64SilenceNaN(float64): float64; extern macro StoreFixedDoubleArrayElement( FixedDoubleArray, Object, float64, constexpr ParameterMode); +extern macro StoreFixedArrayElement( + FixedArray, intptr, Object, constexpr WriteBarrierMode): void; + macro StoreFixedDoubleArrayElementWithSmiIndex( array: FixedDoubleArray, index: Smi, value: float64) { StoreFixedDoubleArrayElement(array, index, value, SMI_PARAMETERS); } +extern macro GetNumberDictionaryNumberOfElements(NumberDictionary): Smi; extern macro BasicLoadNumberDictionaryElement(NumberDictionary, intptr): Object labels NotData, IfHole; @@ -846,9 +911,11 @@ macro AllowNonNumberElements(kind: ElementsKind): ElementsKind { extern macro AllocateZeroedFixedArray(intptr): FixedArray; extern macro AllocateZeroedFixedDoubleArray(intptr): FixedDoubleArray; - extern macro CalculateNewElementsCapacity(Smi): Smi; +extern macro CalculateNewElementsCapacity(intptr): intptr; +extern macro AllocateFixedArrayWithHoles( + intptr, constexpr AllocationFlags): FixedArray; extern macro CopyFixedArrayElements( constexpr ElementsKind, FixedArray, constexpr ElementsKind, FixedArray, intptr, intptr, intptr): void; @@ -858,14 +925,14 @@ extern macro CopyFixedArrayElements( extern macro AllocateJSArray(constexpr ElementsKind, Map, intptr, Smi): JSArray; extern macro AllocateJSArray(constexpr ElementsKind, Map, Smi, Smi): JSArray; -extern macro IsElementsKindGreaterThan( - ElementsKind, constexpr ElementsKind): bool; extern operator '[]=' macro StoreFixedDoubleArrayElementSmi( FixedDoubleArray, Smi, float64): void; extern macro LoadDoubleWithHoleCheck(FixedDoubleArray, Smi): float64 labels IfHole; +extern macro LoadDoubleWithHoleCheck(FixedDoubleArray, intptr): float64 + labels IfHole; extern macro StoreFixedDoubleArrayHoleSmi(FixedDoubleArray, Smi): void; extern macro Call(Context, Callable, Object): Object; @@ -882,6 +949,10 @@ extern macro ExtractFixedArray( FixedArrayBase, Smi, Smi, Smi, constexpr ExtractFixedArrayFlags): FixedArrayBase; +extern macro ExtractFixedArray( + FixedArray, intptr, intptr, intptr, + constexpr ExtractFixedArrayFlags): FixedArray; + extern builtin ExtractFastJSArray(Context, JSArray, Smi, Smi): JSArray; extern macro MoveElements( @@ -1021,3 +1092,14 @@ macro GetLengthProperty(implicit context: Context)(o: Object): Number { return ToLength_Inline(context, length); } } + +extern macro NumberToString(Number): String; +extern macro HasOnlyOneByteChars(InstanceType): bool; +extern macro AllocateSeqOneByteString(implicit context: Context)(uint32): + String; +extern macro AllocateSeqTwoByteString(implicit context: Context)(uint32): + String; +extern macro TryIntPtrAdd(intptr, intptr): intptr + labels IfOverflow; + +extern builtin ObjectToString(Context, Object): Object; diff --git a/src/builtins/builtins-array-gen.h b/src/builtins/builtins-array-gen.h index a73c072cee..c0847f03d5 100644 --- a/src/builtins/builtins-array-gen.h +++ b/src/builtins/builtins-array-gen.h @@ -72,6 +72,66 @@ class ArrayBuiltinsAssembler : public BaseBuiltinsFromDSLAssembler { void FillFixedArrayWithSmiZero(TNode array, TNode smi_length); + TNode CallJSArrayArrayJoinConcatToSequentialString( + TNode fixed_array, TNode length, TNode sep, + TNode dest) { + TNode func = ExternalConstant( + ExternalReference::jsarray_array_join_concat_to_sequential_string()); + TNode isolate_ptr = + ExternalConstant(ExternalReference::isolate_address(isolate())); + return UncheckedCast( + CallCFunction5(MachineType::AnyTagged(), // String* + MachineType::Pointer(), // Isolate* + MachineType::AnyTagged(), // FixedArray* fixed_array + MachineType::IntPtr(), // intptr_t length + MachineType::AnyTagged(), // String* sep + MachineType::AnyTagged(), // String* dest + func, isolate_ptr, fixed_array, length, sep, dest)); + } + + // Temporary Torque support for Array.prototype.join(). + // TODO(pwong): Remove this when Torque supports exception handlers. + TNode CallLoadJoinElement(TNode context, + TNode loadJoinElement, + TNode receiver, TNode k, + Label* if_exception, + TVariable* var_exception) { + // Calling a specialization of LoadJoinElement (see array-join.tq), requires + // a descriptor. We arbitrarily use one of specialization's descriptor, as + // all specializations share the same interface. + TNode result = CallStub( + Builtins::CallableFor(isolate(), + Builtins::kLoadJoinElement20ATDictionaryElements) + .descriptor(), + loadJoinElement, context, receiver, k); + GotoIfException(result, if_exception, var_exception); + return result; + } + + // Temporary Torque support for Array.prototype.join(). + // TODO(pwong): Remove this when Torque supports exception handlers. + TNode CallConvertToLocaleString(TNode context, + TNode element, + TNode locales, + TNode options, + Label* if_exception, + TVariable* var_exception) { + TNode result = CallBuiltin(Builtins::kConvertToLocaleString, + context, element, locales, options); + GotoIfException(result, if_exception, var_exception); + return CAST(result); + } + + // Temporary Torque support for Array.prototype.join(). + // TODO(pwong): Remove this when Torque supports exception handlers. + TNode CallToString(TNode context, TNode obj, + Label* if_exception, + TVariable* var_exception) { + TNode result = CallBuiltin(Builtins::kToString, context, obj); + GotoIfException(result, if_exception, var_exception); + return CAST(result); + } + protected: TNode context() { return context_; } TNode receiver() { return receiver_; } diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h index 63a61fc731..36470a1017 100644 --- a/src/builtins/builtins-definitions.h +++ b/src/builtins/builtins-definitions.h @@ -840,6 +840,7 @@ namespace internal { /* ES #sec-object.prototype.tolocalestring */ \ TFJ(ObjectPrototypeToLocaleString, 0, kReceiver) \ CPP(ObjectSeal) \ + TFS(ObjectToString, kReceiver) \ TFJ(ObjectValues, 1, kReceiver, kObject) \ \ /* instanceof */ \ diff --git a/src/builtins/builtins-object-gen.cc b/src/builtins/builtins-object-gen.cc index fbac2e1abc..4126470fe0 100644 --- a/src/builtins/builtins-object-gen.cc +++ b/src/builtins/builtins-object-gen.cc @@ -814,7 +814,13 @@ TF_BUILTIN(ObjectPrototypeIsPrototypeOf, ObjectBuiltinsAssembler) { } // ES #sec-object.prototype.tostring -TF_BUILTIN(ObjectPrototypeToString, ObjectBuiltinsAssembler) { +TF_BUILTIN(ObjectPrototypeToString, CodeStubAssembler) { + TNode receiver = CAST(Parameter(Descriptor::kReceiver)); + TNode context = CAST(Parameter(Descriptor::kContext)); + Return(CallBuiltin(Builtins::kObjectToString, context, receiver)); +} + +TF_BUILTIN(ObjectToString, ObjectBuiltinsAssembler) { Label checkstringtag(this), if_apiobject(this, Label::kDeferred), if_arguments(this), if_array(this), if_boolean(this), if_date(this), if_error(this), if_function(this), if_number(this, Label::kDeferred), diff --git a/src/code-stub-assembler.cc b/src/code-stub-assembler.cc index 5f420ccd34..de58f2831d 100644 --- a/src/code-stub-assembler.cc +++ b/src/code-stub-assembler.cc @@ -618,15 +618,20 @@ TNode CodeStubAssembler::SmiMin(TNode a, TNode b) { return SelectConstant(SmiLessThan(a, b), a, b); } +TNode CodeStubAssembler::TryIntPtrAdd(TNode a, + TNode b, + Label* if_overflow) { + TNode> pair = IntPtrAddWithOverflow(a, b); + TNode overflow = Projection<1>(pair); + GotoIf(overflow, if_overflow); + return Projection<0>(pair); +} + TNode CodeStubAssembler::TrySmiAdd(TNode lhs, TNode rhs, Label* if_overflow) { if (SmiValuesAre32Bits()) { - TNode> pair = IntPtrAddWithOverflow( - BitcastTaggedToWord(lhs), BitcastTaggedToWord(rhs)); - TNode overflow = Projection<1>(pair); - GotoIf(overflow, if_overflow); - TNode result = Projection<0>(pair); - return BitcastWordToTaggedSigned(result); + return BitcastWordToTaggedSigned(TryIntPtrAdd( + BitcastTaggedToWord(lhs), BitcastTaggedToWord(rhs), if_overflow)); } else { DCHECK(SmiValuesAre31Bits()); TNode> pair = @@ -967,6 +972,12 @@ TNode CodeStubAssembler::LoadDoubleWithHoleCheck( SMI_PARAMETERS, if_hole); } +TNode CodeStubAssembler::LoadDoubleWithHoleCheck( + TNode array, TNode index, Label* if_hole) { + return LoadFixedDoubleArrayElement(array, index, MachineType::Float64(), 0, + INTPTR_PARAMETERS, if_hole); +} + void CodeStubAssembler::BranchIfPrototypesHaveNoElements( Node* receiver_map, Label* definitely_no_elements, Label* possibly_elements) { @@ -5882,6 +5893,12 @@ TNode CodeStubAssembler::IsOneByteStringInstanceType( Int32Constant(kOneByteStringTag)); } +TNode CodeStubAssembler::HasOnlyOneByteChars( + TNode instance_type) { + CSA_ASSERT(this, IsStringInstanceType(instance_type)); + return IsSetWord32(instance_type, kStringEncodingMask | kOneByteDataHintMask); +} + TNode CodeStubAssembler::IsSequentialStringInstanceType( SloppyTNode instance_type) { CSA_ASSERT(this, IsStringInstanceType(instance_type)); @@ -12849,6 +12866,11 @@ Node* CodeStubAssembler::IsElementsKindGreaterThan( return Int32GreaterThan(target_kind, Int32Constant(reference_kind)); } +TNode CodeStubAssembler::IsElementsKindLessThanOrEqual( + TNode target_kind, ElementsKind reference_kind) { + return Int32LessThanOrEqual(target_kind, Int32Constant(reference_kind)); +} + Node* CodeStubAssembler::IsDebugActive() { Node* is_debug_active = Load( MachineType::Uint8(), diff --git a/src/code-stub-assembler.h b/src/code-stub-assembler.h index e7bbc39f56..cfe4285198 100644 --- a/src/code-stub-assembler.h +++ b/src/code-stub-assembler.h @@ -336,6 +336,11 @@ class V8_EXPORT_PRIVATE CodeStubAssembler : public compiler::CodeAssembler { return CAST(heap_object); } + TNode HeapObjectToString(TNode heap_object, Label* fail) { + GotoIfNot(IsString(heap_object), fail); + return CAST(heap_object); + } + TNode UnsafeCastNumberToHeapNumber(TNode p_n) { return CAST(p_n); } @@ -409,6 +414,10 @@ class V8_EXPORT_PRIVATE CodeStubAssembler : public compiler::CodeAssembler { TNode UnsafeCastObjectToMap(TNode p_o) { return CAST(p_o); } + TNode UnsafeCastObjectToString(TNode p_o) { + return CAST(p_o); + } + TNode RawCastObjectToJSArgumentsObjectWithLength( TNode p_o) { return TNode::UncheckedCast(p_o); @@ -535,6 +544,8 @@ class V8_EXPORT_PRIVATE CodeStubAssembler : public compiler::CodeAssembler { #undef SMI_ARITHMETIC_BINOP TNode SmiInc(TNode value) { return SmiAdd(value, SmiConstant(1)); } + TNode TryIntPtrAdd(TNode a, TNode b, + Label* if_overflow); TNode TrySmiAdd(TNode a, TNode b, Label* if_overflow); TNode TrySmiSub(TNode a, TNode b, Label* if_overflow); @@ -1107,6 +1118,9 @@ class V8_EXPORT_PRIVATE CodeStubAssembler : public compiler::CodeAssembler { TNode LoadDoubleWithHoleCheck(TNode array, TNode index, Label* if_hole = nullptr); + TNode LoadDoubleWithHoleCheck(TNode array, + TNode index, + Label* if_hole = nullptr); // Load Float64 value by |base| + |offset| address. If the value is a double // hole then jump to |if_hole|. If |machine_type| is None then only the hole @@ -1206,6 +1220,11 @@ class V8_EXPORT_PRIVATE CodeStubAssembler : public compiler::CodeAssembler { return StoreFixedArrayElement(object, IntPtrConstant(index), value, barrier_mode); } + void StoreFixedArrayElement(TNode object, int index, + TNode value) { + return StoreFixedArrayElement(object, IntPtrConstant(index), value, + SKIP_WRITE_BARRIER); + } Node* StoreJSArrayLength(TNode array, TNode length); Node* StoreElements(TNode object, TNode elements); @@ -1241,6 +1260,10 @@ class V8_EXPORT_PRIVATE CodeStubAssembler : public compiler::CodeAssembler { StoreFixedArrayElement(array, index, value, barrier_mode, 0, SMI_PARAMETERS); } + void StoreFixedArrayElement(TNode array, TNode index, + TNode value) { + StoreFixedArrayElement(array, index, value, SKIP_WRITE_BARRIER, 0); + } void StoreFixedDoubleArrayElement( TNode object, Node* index, TNode value, @@ -1492,6 +1515,15 @@ class V8_EXPORT_PRIVATE CodeStubAssembler : public compiler::CodeAssembler { return result; } + TNode AllocateFixedArrayWithHoles(TNode capacity, + AllocationFlags flags) { + TNode result = UncheckedCast( + AllocateFixedArray(PACKED_ELEMENTS, capacity, flags)); + FillFixedArrayWithValue(PACKED_ELEMENTS, result, IntPtrConstant(0), + capacity, RootIndex::kTheHoleValue); + return result; + } + Node* AllocatePropertyArray(Node* capacity, ParameterMode mode = INTPTR_PARAMETERS, AllocationFlags flags = kNone); @@ -1966,6 +1998,7 @@ class V8_EXPORT_PRIVATE CodeStubAssembler : public compiler::CodeAssembler { TNode IsNullOrUndefined(SloppyTNode object); TNode IsNumberDictionary(SloppyTNode object); TNode IsOneByteStringInstanceType(SloppyTNode instance_type); + TNode HasOnlyOneByteChars(TNode instance_type); TNode IsPrimitiveInstanceType(SloppyTNode instance_type); TNode IsPrivateSymbol(SloppyTNode object); TNode IsPromiseCapability(SloppyTNode object); @@ -2058,6 +2091,8 @@ class V8_EXPORT_PRIVATE CodeStubAssembler : public compiler::CodeAssembler { Node* IsHoleyFastElementsKind(Node* elements_kind); Node* IsElementsKindGreaterThan(Node* target_kind, ElementsKind reference_kind); + TNode IsElementsKindLessThanOrEqual(TNode target_kind, + ElementsKind reference_kind); // String helpers. // Load a character from a String (might flatten a ConsString). @@ -2398,6 +2433,11 @@ class V8_EXPORT_PRIVATE CodeStubAssembler : public compiler::CodeAssembler { LoadFixedArrayElement(dictionary, Dictionary::kNumberOfElementsIndex)); } + TNode GetNumberDictionaryNumberOfElements( + TNode dictionary) { + return GetNumberOfElements(dictionary); + } + template void SetNumberOfElements(TNode dictionary, TNode num_elements_smi) { @@ -2833,6 +2873,11 @@ class V8_EXPORT_PRIVATE CodeStubAssembler : public compiler::CodeAssembler { if_false); } + void BranchIfNumberNotEqual(TNode left, TNode right, + Label* if_true, Label* if_false) { + BranchIfNumberEqual(left, right, if_false, if_true); + } + void BranchIfNumberLessThan(TNode left, TNode right, Label* if_true, Label* if_false) { BranchIfNumberRelationalComparison(Operation::kLessThan, left, right, diff --git a/src/contexts.h b/src/contexts.h index 2eca0709c3..34e7db8707 100644 --- a/src/contexts.h +++ b/src/contexts.h @@ -115,6 +115,7 @@ enum ContextLookupFlags { V(ARRAY_BUFFER_MAP_INDEX, Map, array_buffer_map) \ V(ARRAY_BUFFER_NOINIT_FUN_INDEX, JSFunction, array_buffer_noinit_fun) \ V(ARRAY_FUNCTION_INDEX, JSFunction, array_function) \ + V(ARRAY_JOIN_STACK_INDEX, HeapObject, array_join_stack) \ V(ASYNC_FROM_SYNC_ITERATOR_MAP_INDEX, Map, async_from_sync_iterator_map) \ V(ASYNC_FUNCTION_AWAIT_REJECT_SHARED_FUN, SharedFunctionInfo, \ async_function_await_reject_shared_fun) \ diff --git a/src/debug/debug-evaluate.cc b/src/debug/debug-evaluate.cc index 98e4c58fb9..9e1d52babe 100644 --- a/src/debug/debug-evaluate.cc +++ b/src/debug/debug-evaluate.cc @@ -552,10 +552,13 @@ DebugInfo::SideEffectState BuiltinGetSideEffectState(Builtins::Name id) { case Builtins::kArrayPrototypeFindIndex: case Builtins::kArrayPrototypeFlat: case Builtins::kArrayPrototypeFlatMap: + case Builtins::kArrayPrototypeJoin: case Builtins::kArrayPrototypeKeys: case Builtins::kArrayPrototypeLastIndexOf: case Builtins::kArrayPrototypeSlice: case Builtins::kArrayPrototypeSort: + case Builtins::kArrayPrototypeToLocaleString: + case Builtins::kArrayPrototypeToString: case Builtins::kArrayForEach: case Builtins::kArrayEvery: case Builtins::kArraySome: diff --git a/src/external-reference.cc b/src/external-reference.cc index bd5ca2a0d4..963c9a261e 100644 --- a/src/external-reference.cc +++ b/src/external-reference.cc @@ -735,6 +735,12 @@ ExternalReference ExternalReference::search_string_raw() { return ExternalReference(Redirect(FUNCTION_ADDR(f))); } +ExternalReference +ExternalReference::jsarray_array_join_concat_to_sequential_string() { + return ExternalReference( + Redirect(FUNCTION_ADDR(JSArray::ArrayJoinConcatToSequentialString))); +} + ExternalReference ExternalReference::search_string_raw_one_one() { return search_string_raw(); } diff --git a/src/external-reference.h b/src/external-reference.h index 7af1619b5a..595c830548 100644 --- a/src/external-reference.h +++ b/src/external-reference.h @@ -101,18 +101,18 @@ class StatsCounter; V(ieee754_acosh_function, "base::ieee754::acosh") \ V(ieee754_asin_function, "base::ieee754::asin") \ V(ieee754_asinh_function, "base::ieee754::asinh") \ - V(ieee754_atan2_function, "base::ieee754::atan2") \ V(ieee754_atan_function, "base::ieee754::atan") \ + V(ieee754_atan2_function, "base::ieee754::atan2") \ V(ieee754_atanh_function, "base::ieee754::atanh") \ V(ieee754_cbrt_function, "base::ieee754::cbrt") \ V(ieee754_cos_function, "base::ieee754::cos") \ V(ieee754_cosh_function, "base::ieee754::cosh") \ V(ieee754_exp_function, "base::ieee754::exp") \ V(ieee754_expm1_function, "base::ieee754::expm1") \ + V(ieee754_log_function, "base::ieee754::log") \ V(ieee754_log10_function, "base::ieee754::log10") \ V(ieee754_log1p_function, "base::ieee754::log1p") \ V(ieee754_log2_function, "base::ieee754::log2") \ - V(ieee754_log_function, "base::ieee754::log") \ V(ieee754_sin_function, "base::ieee754::sin") \ V(ieee754_sinh_function, "base::ieee754::sinh") \ V(ieee754_tan_function, "base::ieee754::tan") \ @@ -123,6 +123,8 @@ class StatsCounter; "JSObject::InvalidatePrototypeChains()") \ V(invoke_accessor_getter_callback, "InvokeAccessorGetterCallback") \ V(invoke_function_callback, "InvokeFunctionCallback") \ + V(jsarray_array_join_concat_to_sequential_string, \ + "jsarray_array_join_concat_to_sequential_string") \ V(jsreceiver_create_identity_hash, "jsreceiver_create_identity_hash") \ V(libc_memchr_function, "libc_memchr") \ V(libc_memcpy_function, "libc_memcpy") \ @@ -136,13 +138,13 @@ class StatsCounter; V(power_double_double_function, "power_double_double_function") \ V(printf_function, "printf") \ V(refill_math_random, "MathRandom::RefillCache") \ - V(store_buffer_overflow_function, "StoreBuffer::StoreBufferOverflow") \ V(search_string_raw_one_one, "search_string_raw_one_one") \ V(search_string_raw_one_two, "search_string_raw_one_two") \ V(search_string_raw_two_one, "search_string_raw_two_one") \ V(search_string_raw_two_two, "search_string_raw_two_two") \ - V(try_internalize_string_function, "try_internalize_string_function") \ V(smi_lexicographic_compare_function, "smi_lexicographic_compare_function") \ + V(store_buffer_overflow_function, "StoreBuffer::StoreBufferOverflow") \ + V(try_internalize_string_function, "try_internalize_string_function") \ V(wasm_call_trap_callback_for_testing, \ "wasm::call_trap_callback_for_testing") \ V(wasm_f32_ceil, "wasm::f32_ceil_wrapper") \ diff --git a/src/js/array.js b/src/js/array.js index 16b140bb38..6945ad9264 100644 --- a/src/js/array.js +++ b/src/js/array.js @@ -29,11 +29,12 @@ function ArraySpeciesCreate(array, length) { return new constructor(length); } - +// TODO(pwong): Remove once TypedArray.prototype.join() is ported to Torque. function KeySortCompare(a, b) { return a - b; } +// TODO(pwong): Remove once TypedArray.prototype.join() is ported to Torque. function GetSortedArrayKeys(array, indices) { if (IS_NUMBER(indices)) { // It's an interval @@ -50,7 +51,7 @@ function GetSortedArrayKeys(array, indices) { return InnerArraySort(indices, indices.length, KeySortCompare); } - +// TODO(pwong): Remove once TypedArray.prototype.join() is ported to Torque. function SparseJoinWithSeparatorJS( array, keys, length, use_locale, separator, locales, options) { var keys_length = keys.length; @@ -64,7 +65,7 @@ function SparseJoinWithSeparatorJS( return %SparseJoinWithSeparator(elements, length, separator); } - +// TODO(pwong): Remove once TypedArray.prototype.join() is ported to Torque. // Optimized for sparse arrays if separator is ''. function SparseJoin(array, keys, use_locale, locales, options) { var keys_length = keys.length; @@ -75,7 +76,7 @@ function SparseJoin(array, keys, use_locale, locales, options) { return %StringBuilderConcat(elements, keys_length, ''); } - +// TODO(pwong): Remove once TypedArray.prototype.join() is ported to Torque. function UseSparseVariant(array, length, is_array, touched) { // Only use the sparse variant on arrays that are likely to be sparse and the // number of elements touched in the operation is relatively small compared to @@ -92,6 +93,7 @@ function UseSparseVariant(array, length, is_array, touched) { (touched > estimated_elements * 4); } +// TODO(pwong): Remove once TypedArray.prototype.join() is ported to Torque. function Stack() { this.length = 0; this.values = new InternalArray(); @@ -123,6 +125,7 @@ function StackHas(stack, v) { // join invocations. var visited_arrays = new Stack(); +// TODO(pwong): Remove once TypedArray.prototype.join() is ported to Torque. function DoJoin( array, length, is_array, separator, use_locale, locales, options) { if (UseSparseVariant(array, length, is_array, length)) { @@ -155,6 +158,7 @@ function DoJoin( } } +// TODO(pwong): Remove once TypedArray.prototype.join() is ported to Torque. function Join(array, length, separator, use_locale, locales, options) { if (length === 0) return ''; @@ -178,7 +182,7 @@ function Join(array, length, separator, use_locale, locales, options) { } } - +// TODO(pwong): Remove once TypedArray.prototype.join() is ported to Torque. function ConvertToString(use_locale, x, locales, options) { if (IS_NULL_OR_UNDEFINED(x)) return ''; if (use_locale) { @@ -196,48 +200,13 @@ function ConvertToString(use_locale, x, locales, options) { // ------------------------------------------------------------------- -var ArrayJoin; -DEFINE_METHOD( - GlobalArray.prototype, - toString() { - var array; - var func; - if (IS_ARRAY(this)) { - func = this.join; - if (func === ArrayJoin) { - return Join(this, this.length, ',', false); - } - array = this; - } else { - array = TO_OBJECT(this); - func = array.join; - } - if (!IS_CALLABLE(func)) { - return %_Call(ObjectToString, array); - } - return %_Call(func, array); - } -); - // ecma402 #sup-array.prototype.tolocalestring +// TODO(pwong): Remove once TypedArray.prototype.join() is ported to Torque. function InnerArrayToLocaleString(array, length, locales, options) { return Join(array, TO_LENGTH(length), ',', true, locales, options); } - -DEFINE_METHOD( - GlobalArray.prototype, - // ecma402 #sup-array.prototype.tolocalestring - toLocaleString() { - var array = TO_OBJECT(this); - var arrayLen = array.length; - var locales = arguments[0]; - var options = arguments[1]; - return InnerArrayToLocaleString(array, arrayLen, locales, options); - } -); - - +// TODO(pwong): Remove once TypedArray.prototype.join() is ported to Torque. function InnerArrayJoin(separator, array, length) { if (IS_UNDEFINED(separator)) { separator = ','; @@ -256,15 +225,6 @@ function InnerArrayJoin(separator, array, length) { } -DEFINE_METHOD( - GlobalArray.prototype, - join(separator) { - var array = TO_OBJECT(this); - var length = TO_LENGTH(array.length); - - return InnerArrayJoin(separator, array, length); - } -); // Oh the humanity... don't remove the following function because js2c for some @@ -275,6 +235,7 @@ function ArraySliceFallback(start, end) { return null; } +// TODO(pwong): Remove once TypedArray.prototype.join() is ported to Torque. function InnerArraySort(array, length, comparefn) { // In-place QuickSort algorithm. // For short (length <= 10) arrays, insertion sort is used for efficiency. @@ -501,7 +462,9 @@ utils.Export(function(to) { to.ArrayPush = ArrayPush; to.ArrayToString = ArrayToString; to.ArrayValues = ArrayValues; + // TODO(pwong): Remove once TypedArray.prototype.join() is ported to Torque. to.InnerArrayJoin = InnerArrayJoin; + // TODO(pwong): Remove once TypedArray.prototype.join() is ported to Torque. to.InnerArrayToLocaleString = InnerArrayToLocaleString; }); diff --git a/src/objects.cc b/src/objects.cc index c73e2e2db7..c9ef30372e 100644 --- a/src/objects.cc +++ b/src/objects.cc @@ -11397,6 +11397,111 @@ Handle String::CalculateLineEnds(Isolate* isolate, return array; } +namespace { + +template +void WriteFixedArrayToFlat(FixedArray* fixed_array, int length, + String* separator, sinkchar* sink, int sink_length) { + DisallowHeapAllocation no_allocation; + CHECK_GT(length, 0); + CHECK_LE(length, fixed_array->length()); +#ifdef DEBUG + sinkchar* sink_end = sink + sink_length; +#endif + + const int separator_length = separator->length(); + const bool use_one_byte_separator_fast_path = + separator_length == 1 && sizeof(sinkchar) == 1 && + StringShape(separator).IsSequentialOneByte(); + uint8_t separator_one_char; + if (use_one_byte_separator_fast_path) { + CHECK(StringShape(separator).IsSequentialOneByte()); + CHECK_EQ(separator->length(), 1); + separator_one_char = SeqOneByteString::cast(separator)->GetChars()[0]; + } + + uint32_t num_separators = 0; + for (int i = 0; i < length; i++) { + Object* element = fixed_array->get(i); + const bool element_is_separator_sequence = element->IsSmi(); + + // If element is a Smi, it represents the number of separators to write. + if (V8_UNLIKELY(element_is_separator_sequence)) { + CHECK(element->ToUint32(&num_separators)); + // Verify that Smis (number of separators) only occur when necessary: + // 1) at the beginning + // 2) at the end + // 3) when the number of separators > 1 + // - It is assumed that consecutive Strings will have one separator, + // so there is no need for a Smi. + DCHECK(i == 0 || i == length - 1 || num_separators > 1); + } + + // Write separator(s) if necessary. + if (num_separators > 0 && separator_length > 0) { + // TODO(pwong): Consider doubling strategy employed by runtime-strings.cc + // WriteRepeatToFlat(). + // Fast path for single character, single byte separators. + if (use_one_byte_separator_fast_path) { + DCHECK_LE(sink + num_separators, sink_end); + memset(sink, separator_one_char, num_separators); + DCHECK_EQ(separator_length, 1); + sink += num_separators; + } else { + for (uint32_t j = 0; j < num_separators; j++) { + DCHECK_LE(sink + separator_length, sink_end); + String::WriteToFlat(separator, sink, 0, separator_length); + sink += separator_length; + } + } + } + + if (V8_UNLIKELY(element_is_separator_sequence)) { + num_separators = 0; + } else { + DCHECK(element->IsString()); + String* string = String::cast(element); + const int string_length = string->length(); + + DCHECK(string_length == 0 || sink < sink_end); + String::WriteToFlat(string, sink, 0, string_length); + sink += string_length; + + // Next string element, needs at least one separator preceding it. + num_separators = 1; + } + } + + // Verify we have written to the end of the sink. + DCHECK_EQ(sink, sink_end); +} + +} // namespace + +// static +String* JSArray::ArrayJoinConcatToSequentialString(Isolate* isolate, + FixedArray* fixed_array, + intptr_t length, + String* separator, + String* dest) { + DisallowHeapAllocation no_allocation; + DisallowJavascriptExecution no_js(isolate); + DCHECK(fixed_array->IsFixedArray()); + DCHECK(StringShape(dest).IsSequentialOneByte() || + StringShape(dest).IsSequentialTwoByte()); + + if (StringShape(dest).IsSequentialOneByte()) { + WriteFixedArrayToFlat(fixed_array, static_cast(length), separator, + SeqOneByteString::cast(dest)->GetChars(), + dest->length()); + } else { + DCHECK(StringShape(dest).IsSequentialTwoByte()); + WriteFixedArrayToFlat(fixed_array, static_cast(length), separator, + SeqTwoByteString::cast(dest)->GetChars(), + dest->length()); + } + return dest; +} // Compares the contents of two strings by reading and comparing // int-sized blocks of characters. diff --git a/src/objects/js-array.h b/src/objects/js-array.h index 3a9fe48d24..36634704ed 100644 --- a/src/objects/js-array.h +++ b/src/objects/js-array.h @@ -63,6 +63,28 @@ class JSArray : public JSObject { Isolate* isolate, Handle a, PropertyDescriptor* desc, ShouldThrow should_throw); + // Support for Array.prototype.join(). + // Writes a fixed array of strings and separators to a single destination + // string. This helpers assumes the fixed array encodes separators in two + // ways: + // 1) Explicitly with a smi, whos value represents the number of repeated + // separators. + // 2) Implicitly between two consecutive strings a single separator. + // + // Here are some input/output examples given the separator string is ',': + // + // [1, 'hello', 2, 'world', 1] => ',hello,,world,' + // ['hello', 'world'] => 'hello,world' + // + // To avoid any allocations, this helper assumes the destination string is the + // exact length necessary to write the strings and separators from the fixed + // array. + static String* ArrayJoinConcatToSequentialString(Isolate* isolate, + FixedArray* fixed_array, + intptr_t length, + String* separator, + String* dest); + // Checks whether the Array has the current realm's Array.prototype as its // prototype. This function is best-effort and only gives a conservative // approximation, erring on the side of false, in particular with respect @@ -90,6 +112,9 @@ class JSArray : public JSObject { // This constant is somewhat arbitrary. Any large enough value would work. static const uint32_t kMaxFastArrayLength = 32 * 1024 * 1024; + // Min. stack size for detecting an Array.prototype.join() call cycle. + static const uint32_t kMinJoinStackSize = 2; + static const int kInitialMaxFastElementArray = (kMaxRegularHeapObjectSize - FixedArray::kHeaderSize - kSize - AllocationMemento::kSize) >> diff --git a/src/runtime/runtime-strings.cc b/src/runtime/runtime-strings.cc index d57959687c..c44f47e601 100644 --- a/src/runtime/runtime-strings.cc +++ b/src/runtime/runtime-strings.cc @@ -340,6 +340,7 @@ RUNTIME_FUNCTION(Runtime_StringBuilderConcat) { } } +// TODO(pwong): Remove once TypedArray.prototype.join() is ported to Torque. RUNTIME_FUNCTION(Runtime_StringBuilderJoin) { HandleScope scope(isolate); DCHECK_EQ(3, args.length()); @@ -444,6 +445,7 @@ static void WriteRepeatToFlat(String* src, Vector buffer, int cursor, } } +// TODO(pwong): Remove once TypedArray.prototype.join() is ported to Torque. template static void JoinSparseArrayWithSeparator(FixedArray* elements, int elements_length, @@ -480,6 +482,7 @@ static void JoinSparseArrayWithSeparator(FixedArray* elements, DCHECK(cursor <= buffer.length()); } +// TODO(pwong): Remove once TypedArray.prototype.join() is ported to Torque. RUNTIME_FUNCTION(Runtime_SparseJoinWithSeparator) { HandleScope scope(isolate); DCHECK_EQ(3, args.length()); diff --git a/test/debugger/debug/side-effect/debug-evaluate-no-side-effect-builtins.js b/test/debugger/debug/side-effect/debug-evaluate-no-side-effect-builtins.js index 938461690e..2ed350f3f5 100644 --- a/test/debugger/debug/side-effect/debug-evaluate-no-side-effect-builtins.js +++ b/test/debugger/debug/side-effect/debug-evaluate-no-side-effect-builtins.js @@ -73,8 +73,8 @@ function listener(event, exec_state, event_data, data) { "flatMap", "forEach", "every", "some", "reduce", "reduceRight", "find", "filter", "map", "findIndex" ]; - var fails = ["toString", "join", "toLocaleString", "pop", "push", "reverse", - "shift", "unshift", "splice", "sort", "copyWithin", "fill"]; + var fails = ["toLocaleString", "pop", "push", "reverse", "shift", "unshift", + "splice", "sort", "copyWithin", "fill"]; for (f of Object.getOwnPropertyNames(Array.prototype)) { if (typeof Array.prototype[f] === "function") { if (fails.includes(f)) { diff --git a/test/mjsunit/array-functions-prototype-misc.js b/test/mjsunit/array-functions-prototype-misc.js index dd95e2d266..0aff0a2304 100644 --- a/test/mjsunit/array-functions-prototype-misc.js +++ b/test/mjsunit/array-functions-prototype-misc.js @@ -170,20 +170,6 @@ for (var use_real_arrays = 0; use_real_arrays <= 1; use_real_arrays++) { assertEquals("concat", join); join = ba.join(''); assertEquals("catcon", join); - - var sparse = []; - sparse[pos + 1000] = 'is '; - sparse[pos + 271828] = 'time '; - sparse[pos + 31415] = 'the '; - sparse[pos + 012260199] = 'all '; - sparse[-1] = 'foo'; - sparse[pos + 22591927] = 'good '; - sparse[pos + 1618033] = 'for '; - sparse[pos + 91] = ': Now '; - sparse[pos + 86720199] = 'men.'; - sparse.hest = 'fisk'; - - assertEquals("baz: Now is the time for all good men.", sparse.join('')); } a = new_function(pos); diff --git a/test/mjsunit/array-join-element-tostring-prototype-side-effects.js b/test/mjsunit/array-join-element-tostring-prototype-side-effects.js new file mode 100644 index 0000000000..a5ddebb9b9 --- /dev/null +++ b/test/mjsunit/array-join-element-tostring-prototype-side-effects.js @@ -0,0 +1,14 @@ +// 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. + +(function ArrayPrototypeChanged() { + const el = { + toString() { + Array.prototype[1] = '2'; + return '1'; + } + }; + const a = [el, ,3]; + assertSame("123", a.join('')); +})(); diff --git a/test/mjsunit/array-join-element-tostring-side-effects.js b/test/mjsunit/array-join-element-tostring-side-effects.js new file mode 100644 index 0000000000..1da72a7970 --- /dev/null +++ b/test/mjsunit/array-join-element-tostring-side-effects.js @@ -0,0 +1,152 @@ +// 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. + +// Flags: --allow-natives-syntax + +const MIN_DICTIONARY_INDEX = 8192; + +function ArrayTests() { + (function ToStringThrows() { + function TestError() {} + + let callCount = 0; + const toStringThrows = { + toString() { + callCount++; + throw new TestError; + } + }; + const a = [toStringThrows]; + assertThrows(() => a.join(), TestError); + assertSame(1, callCount); + + // Verifies cycle detection still works properly after thrown error. + a[0] = 1; + a[1] = 2; + assertSame('1,2', a.join()); + })(); + + (function ArrayLengthIncreased() { + let callCount = 0; + const a = [ + { + toString() { + callCount++; + a.push(2); + return '1'; + } + } + ]; + assertSame('1', a.join()); + assertSame(1, callCount); + assertSame('1,2', a.join()); + })(); + + (function ArrayLengthDecreased() { + let callCount = 0; + const a = [ + { + toString() { + callCount++; + a.pop(); + return '1'; + } + }, + '2' + ]; + assertSame('1,', a.join()); + assertSame(1, callCount); + assertSame('1', a.join()); + })(); + + (function ElementsKindChangedToHoley() { + let callCount = 0; + const a = [ + { + toString() { + callCount++; + a.length = 4; + a[1] = 777; + a[2] = 7.7; + return '1'; + } + }, + 2, + 3 + ]; + assertSame('1,777,7.7', a.join()); + assertSame(1, callCount); + assertSame('1,777,7.7,', a.join()); + })(); + + (function ElementsKindChangedToHoleyThroughDeletion() { + let callCount = 0; + const a = [ + { + toString() { + callCount++; + delete a[1]; + a[2] = 7.7; + return '1'; + } + }, + 2, + 3 + ]; + assertSame('1,,7.7', a.join()); + assertSame(1, callCount); + assertSame('1,,7.7', a.join()); + })(); + + (function NumberDictionaryChanged() { + let callCount = 0; + const a = []; + a[MIN_DICTIONARY_INDEX - 1] = { + toString() { + callCount++; + a[MIN_DICTIONARY_INDEX] = '2'; + return '1'; + } + }; + a[MIN_DICTIONARY_INDEX] = 'NOPE'; + assertTrue(%HasDictionaryElements(a)); + assertSame('12', a.join('')); + assertSame(1, callCount); + assertSame('12', a.join('')); + })(); + + (function NumberDictionaryLengthChange() { + let callCount = 0; + const a = []; + a[MIN_DICTIONARY_INDEX - 1] = { + toString() { + callCount++; + a.length = MIN_DICTIONARY_INDEX; + return '1'; + } + }; + a[MIN_DICTIONARY_INDEX] = '2'; + assertTrue(%HasDictionaryElements(a)); + assertSame('1', a.join('')); + assertSame(1, callCount); + assertSame('1', a.join('')); + })(); +} + +(function NonArrayCycleDetection() { + const a = { + length: 3, + toString() { return Array.prototype.join.call(this); } + }; + a[0] = '1'; + a[1] = a; + a[2] = '3'; + assertSame("1,,3", Array.prototype.join.call(a)); +}); + +ArrayTests(); + +%SetForceSlowPath(true); + +ArrayTests(); diff --git a/test/mjsunit/array-join-index-getter-side-effects.js b/test/mjsunit/array-join-index-getter-side-effects.js new file mode 100644 index 0000000000..12fe5786c7 --- /dev/null +++ b/test/mjsunit/array-join-index-getter-side-effects.js @@ -0,0 +1,87 @@ +// 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. + +// Flags: --allow-natives-syntax + +(function Throws() { + function TestError() {} + + let callCount = 0; + const a = [0, 1]; + Object.defineProperty(a, '0', { + configurable: true, + get() { + callCount++; + throw new TestError(); + } + }); + assertTrue(%HasDictionaryElements(a)); + assertThrows(() => a.join(), TestError); + assertSame(1, callCount); + + // Verifies cycle detection still works properly after thrown error. + Object.defineProperty(a, '0', { + configurable: true, + get() { + callCount++; + return 777; + } + }); + assertSame('777,1', a.join()); + assertSame(2, callCount); +})(); + +(function ArrayLengthIncreased() { + let callCount = 0; + const a = [1]; + Object.defineProperty(a, '0', { + configurable: true, + get() { + callCount++; + a.push(2); + return 9; + } + }); + assertSame('9', a.join()); + assertSame(1, callCount); + + // Verifies cycle detection still works properly after continuation. + assertSame('9,2', a.join()); +})(); + +(function ArrayLengthDecreased() { + let callCount = 0; + const a = [0, 1]; + Object.defineProperty(a, '0', { + configurable: true, + get() { + callCount++; + a.length = 1; + return 9; + } + }); + assertSame('9,', a.join()); + assertSame(1, callCount); + + // Verifies cycle detection still works properly after continuation. + assertSame('9', a.join()); +})(); + +(function ElementsKindChangedToHoley() { + let callCount = 0; + const a = [0, 1]; + Object.defineProperty(a, '0', { + configurable: true, + get() { + callCount++; + a.length = 3; + return 9; + } + }); + assertSame('9,1', a.join()); + assertSame(1, callCount); + + // Verifies cycle detection still works properly after continuation. + assertSame('9,1,', a.join()); +})(); diff --git a/test/mjsunit/array-join-nesting.js b/test/mjsunit/array-join-nesting.js new file mode 100644 index 0000000000..d1e75fb512 --- /dev/null +++ b/test/mjsunit/array-join-nesting.js @@ -0,0 +1,16 @@ +// 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. + +const DEPTH = 128; + +function makeNestedArray(depth, value) { + return depth > 0 ? [value, makeNestedArray(depth - 1, value)] : [value]; +} + +const array = makeNestedArray(DEPTH, 'a'); +const expected = 'a' + ',a'.repeat(DEPTH); +assertSame(expected, array.join()); + +// Verify cycle detection is still working. +assertSame(expected, array.join()); diff --git a/test/mjsunit/array-join-nonarray-length-getter-side-effects.js b/test/mjsunit/array-join-nonarray-length-getter-side-effects.js new file mode 100644 index 0000000000..3f8d2aa029 --- /dev/null +++ b/test/mjsunit/array-join-nonarray-length-getter-side-effects.js @@ -0,0 +1,29 @@ +// 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. + +(function Throws() { + function TestError() {} + + let callCount = 0; + const a = { + 0: 1, + 1: 2, + get length() { + callCount++; + throw new TestError(); + } + }; + assertThrows(() => Array.prototype.join.call(a), TestError); + assertSame(1, callCount); + + // Verifies cycle detection still works properly after thrown error. + Object.defineProperty(a, 'length', { + get() { + callCount++; + return 2; + } + }); + assertSame('1,2', Array.prototype.join.call(a)); + assertSame(2, callCount); +})(); diff --git a/test/mjsunit/array-join-separator-tostring-side-effects.js b/test/mjsunit/array-join-separator-tostring-side-effects.js new file mode 100644 index 0000000000..d9e85152ca --- /dev/null +++ b/test/mjsunit/array-join-separator-tostring-side-effects.js @@ -0,0 +1,197 @@ +// 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. + +// Flags: --allow-natives-syntax + +const MIN_DICTIONARY_INDEX = 8192; + +(function ToStringThrows() { + function TestError() {} + + let callCount = 0; + const a = [1, 2]; + assertThrows(() => a.join({ + toString() { + callCount++; + throw new TestError; + } + }), TestError); + assertSame(1, callCount); + + // Verifies cycle detection still works properly after thrown error. + assertSame('1,2', a.join()); +})(); + +(function RecursiveJoinCall() { + const a = [1,2,3]; + let callCount = 0; + const sep = { + toString() { + callCount++; + return a.join('-'); + } + }; + assertSame('11-2-321-2-33', a.join(sep)); + assertSame(1, callCount); + + // Verify cycle detection works properly after nested call + assertSame('1,2,3', a.join()); +})(); + + +(function ArrayLengthIncreased() { + const a = [1,2,3]; + let callCount = 0; + assertSame('1,2,3', a.join({ + toString() { + callCount++; + a.push(4); + return ','; + } + })); + assertSame(1, callCount); + assertSame('1,2,3,4', a.join()); +})(); + +(function ArrayLengthDecreased() { + const a = [1,2,3]; + let callCount = 0; + assertSame('1,2,', a.join({ + toString() { + callCount++; + a.pop(); + return ','; + } + })); + assertSame(1, callCount); + assertSame('1,2', a.join()); +})(); + +(function ArrayEmptied() { + const a = [1,2,3]; + let callCount = 0; + assertSame(',,', a.join({ + toString() { + callCount++; + a.length = 0; + return ','; + } + })); + assertSame(1, callCount); +})(); + +(function NumberDictionaryEmptied() { + const a = []; + a[0] = 1; + a[MIN_DICTIONARY_INDEX] = 2; + assertTrue(%HasDictionaryElements(a)); + + let callCount = 0; + assertSame('-'.repeat(MIN_DICTIONARY_INDEX), a.join({ + toString() { + callCount++; + a.length = 0; + return '-'; + } + })); + assertSame(1, callCount); +})(); + +(function NumberDictionaryEmptiedEmptySeparator() { + const a = []; + a[0] = 1; + a[MIN_DICTIONARY_INDEX] = 2; + assertTrue(%HasDictionaryElements(a)); + + let callCount = 0; + assertSame(''.repeat(MIN_DICTIONARY_INDEX), a.join({ + toString() { + callCount++; + a.length = 0; + return ''; + } + })); + assertSame(1, callCount); +})(); + +(function ElementsKindSmiToDoubles() { + const a = [1,2,3]; + let callCount = 0; + assertTrue(%HasSmiElements(a)); + assertSame('1.5,2,3', a.join({ + toString() { + callCount++; + a[0] = 1.5; + assertTrue(%HasDoubleElements(a)); + return ','; + } + })); + assertSame(1, callCount); + assertSame('1.5,2,3', a.join()); +})(); + +(function ElementsKindDoublesToObjects() { + const a = [1.5, 2.5, 3.5]; + let callCount = 0; + assertTrue(%HasDoubleElements(a)); + assertSame('one,2.5,3.5', a.join({ + toString() { + callCount++; + a[0] = 'one'; + assertTrue(%HasObjectElements(a)); + return ','; + } + })); + assertSame(1, callCount); + assertSame('one,2.5,3.5', a.join()); +})(); + +(function ArrayIsNoLongerFast() { + const a = [1,2,3]; + let callCount = 0; + assertSame('666,2,3', a.join({ + toString() { + callCount++; + Object.defineProperty(a, '0', { + get(){ return 666; } + }); + return ','; + } + })); + assertSame(1, callCount); + assertSame('666,2,3', a.join()); +})(); + +(function ArrayPrototypeUnset() { + const a = [1,2]; + a.length = 3; + let callCount = 0; + assertSame('1,2,4', a.join({ + toString() { + callCount++; + a.__proto__ = { '2': 4 }; + return ','; + } + })); + assertSame(1, callCount); + a.__proto__ = Array.prototype; + assertSame('1,2,', a.join()); +})(); + +(function ArrayPrototypeIsNoLongerFast() { + const a = [1,2,3]; + let callCount = 0; + assertSame('1,2,777', a.join({ + toString() { + callCount++; + a.pop(); + Object.defineProperty(Array.prototype, '2', { + get(){ return 777; } + }); + return ','; + } + })); + assertSame(1, callCount); + assertSame('1,2', a.join()); +})(); diff --git a/test/mjsunit/array-join.js b/test/mjsunit/array-join.js index 0c949e769a..1d5ab30ebc 100644 --- a/test/mjsunit/array-join.js +++ b/test/mjsunit/array-join.js @@ -25,6 +25,17 @@ // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +assertSame(',', [null, undefined].join()); +assertSame('1.5,2.5', [1.5, 2.5].join()); +assertSame(',1.5,', [,1.5,,].join()); + +var obj = { + toString() { + return 'a'; + } +}; +assertSame('1,1.5,a,', [1, 1.5, obj, ,].join()); + // Test that array join calls toString on subarrays. var a = [[1,2],3,4,[5,6]]; assertEquals('1,2345,6', a.join('')); @@ -82,9 +93,6 @@ assertEquals(246244, a.join("oo").length); a = new Array(Math.pow(2,32) - 1); // Max length. assertEquals("", a.join("")); -a[123123123] = "o"; -a[1255215215] = "p"; -assertEquals("op", a.join("")); a = new Array(100001); for (var i = 0; i < a.length; i++) a[i] = undefined; diff --git a/test/mjsunit/array-tolocalestring.js b/test/mjsunit/array-tolocalestring.js new file mode 100644 index 0000000000..a5f856907a --- /dev/null +++ b/test/mjsunit/array-tolocalestring.js @@ -0,0 +1,72 @@ +// 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. + +(function CycleDetection() { + const arr = [ + { + toLocaleString() { + return [1, arr]; + } + } + ]; + assertSame('1,', arr.toLocaleString()); + assertSame('1,', arr.toLocaleString()); +})(); + +(function ThrowsError(){ + function TestError() {} + const arr = []; + const obj = { + toLocaleString(){ + throw new TestError(); + } + }; + arr[0] = obj; + assertThrows(() => arr.toLocaleString(), TestError); + + // Verifies cycle detection still works properly after thrown error. + arr[0] = { + toLocaleString() { + return 1; + } + }; + assertSame('1', arr.toLocaleString()); +})(); + +(function AccessThrowsError(){ + function TestError() {} + const arr = []; + const obj = { + get toLocaleString(){ + throw new TestError(); + } + }; + arr[0] = obj; + assertThrows(() => arr.toLocaleString(), TestError); + + // Verifies cycle detection still works properly after thrown error. + arr[0] = { + toLocaleString() { + return 1; + } + }; + assertSame('1', arr.toLocaleString()); +})(); + +(function NotCallable(){ + const arr = []; + const obj = { + toLocaleString: 7 + } + arr[0] = obj; + assertThrows(() => arr.toLocaleString(), TypeError, '7 is not a function'); + + // Verifies cycle detection still works properly after thrown error. + arr[0] = { + toLocaleString() { + return 1; + } + }; + assertSame('1', arr.toLocaleString()); +})(); diff --git a/test/webkit/fast/js/toString-overrides-expected.txt b/test/webkit/fast/js/toString-overrides-expected.txt index 16706e43dd..4dfe4f18e0 100644 --- a/test/webkit/fast/js/toString-overrides-expected.txt +++ b/test/webkit/fast/js/toString-overrides-expected.txt @@ -28,10 +28,10 @@ On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE PASS [1].toString() is '1' PASS [1].toLocaleString() is 'toLocaleString' -FAIL [1].toLocaleString() should be 1. Threw exception TypeError: string "invalid" is not a function +FAIL [1].toLocaleString() should be 1. Threw exception TypeError: invalid is not a function PASS [/r/].toString() is 'toString2' PASS [/r/].toLocaleString() is 'toLocaleString2' -FAIL [/r/].toLocaleString() should be toString2. Threw exception TypeError: string "invalid" is not a function +FAIL [/r/].toLocaleString() should be toString2. Threw exception TypeError: invalid is not a function PASS caught is true PASS successfullyParsed is true diff --git a/tools/v8heapconst.py b/tools/v8heapconst.py index 95e5fa9786..ce6bc94516 100644 --- a/tools/v8heapconst.py +++ b/tools/v8heapconst.py @@ -417,7 +417,7 @@ KNOWN_OBJECTS = { ("OLD_SPACE", 0x018c1): "RegExpMultipleCache", ("OLD_SPACE", 0x020d1): "DefaultMicrotaskQueue", ("OLD_SPACE", 0x020e9): "BuiltinsConstantsTable", - ("OLD_SPACE", 0x029f9): "HashSeed", + ("OLD_SPACE", 0x02a11): "HashSeed", } # List of known V8 Frame Markers.