array-join.tq 25.5 KB
Newer Older
1 2 3 4
// 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.

5
namespace array {
6

7
type LoadJoinElementFn = builtin(Context, JSReceiver, uintptr) => JSAny;
8

9 10 11
const kMaxArrayLength:
    constexpr uint32 generates 'JSArray::kMaxArrayLength';

12 13 14 15 16
// Fast C call to write a fixed array (see Buffer.fixedArray) to a single
// string.
extern macro
ArrayBuiltinsAssembler::CallJSArrayArrayJoinConcatToSequentialString(
    FixedArray, intptr, String, String): String;
17

18 19 20 21 22 23 24 25 26 27 28 29 30
transitioning builtin LoadJoinElement<T : type extends ElementsKind>(
    context: Context, receiver: JSReceiver, k: uintptr): JSAny {
  return GetProperty(receiver, Convert<Number>(k));
}

transitioning LoadJoinElement<array::DictionaryElements>(
    context: Context, receiver: JSReceiver, k: uintptr): JSAny {
  const array: JSArray = UnsafeCast<JSArray>(receiver);
  const dict: NumberDictionary = UnsafeCast<NumberDictionary>(array.elements);
  try {
    return BasicLoadNumberDictionaryElement(dict, Signed(k))
        otherwise IfNoData, IfHole;
  } label IfNoData deferred {
31
    return GetProperty(receiver, Convert<Number>(k));
32 33
  } label IfHole {
    return kEmptyString;
34
  }
35
}
36

37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
LoadJoinElement<array::FastSmiOrObjectElements>(
    context: Context, receiver: JSReceiver, k: uintptr): JSAny {
  const array: JSArray = UnsafeCast<JSArray>(receiver);
  const fixedArray: FixedArray = UnsafeCast<FixedArray>(array.elements);
  const element: Object = fixedArray.objects[k];
  return element == TheHole ? kEmptyString : UnsafeCast<JSAny>(element);
}

LoadJoinElement<array::FastDoubleElements>(
    context: Context, receiver: JSReceiver, k: uintptr): JSAny {
  const array: JSArray = UnsafeCast<JSArray>(receiver);
  const fixedDoubleArray: FixedDoubleArray =
      UnsafeCast<FixedDoubleArray>(array.elements);
  const element: float64 =
      fixedDoubleArray.floats[k].Value() otherwise return kEmptyString;
  return AllocateHeapNumberWithValue(element);
}

builtin LoadJoinTypedElement<T : type extends ElementsKind>(
    context: Context, receiver: JSReceiver, k: uintptr): JSAny {
  const typedArray: JSTypedArray = UnsafeCast<JSTypedArray>(receiver);
58 59
  dcheck(!typed_array::IsJSArrayBufferViewDetachedOrOutOfBoundsBoolean(
      typedArray));
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
  return typed_array::LoadFixedTypedArrayElementAsTagged(
      typedArray.data_ptr, k, typed_array::KindForArrayType<T>());
}

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<Callable>(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);
78
    }
79 80 81
    return ToString_Inline(result);
  } label TypeError {
    ThrowTypeError(MessageTemplate::kCalledNonCallable, prop);
82
  }
83
}
84

85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
// Verifies the current element JSArray accessor can still be safely used
// (see LoadJoinElement<ElementsAccessor>).
macro CannotUseSameArrayAccessor<T: type>(implicit context: Context)(
    loadFn: LoadJoinElementFn, receiver: JSReceiver, originalMap: Map,
    originalLen: Number): bool;

CannotUseSameArrayAccessor<JSArray>(implicit context: Context)(
    loadFn: LoadJoinElementFn, receiver: JSReceiver, originalMap: Map,
    originalLen: Number): bool {
  if (loadFn == LoadJoinElement<array::GenericElementsAccessor>) return false;

  const array: JSArray = UnsafeCast<JSArray>(receiver);
  if (originalMap != array.map) return true;
  if (originalLen != array.length) return true;
  if (IsNoElementsProtectorCellInvalid()) return true;
  return false;
}
102

103 104 105 106
CannotUseSameArrayAccessor<JSTypedArray>(implicit context: Context)(
    _loadFn: LoadJoinElementFn, receiver: JSReceiver, _initialMap: Map,
    _initialLen: Number): bool {
  const typedArray: JSTypedArray = UnsafeCast<JSTypedArray>(receiver);
107 108 109 110 111 112 113 114 115 116 117 118 119
  // When this is called from toLocaleString(), the underlying buffer might get
  // detached / resized (in the case of RAB / GSAB) during iterating the
  // elements. When this is called from join(), it can happen only before the
  // first element (during parameter conversion). The code below doesn't
  // differentiate between these two cases, but does the checks in both cases.
  if (IsDetachedBuffer(typedArray.buffer)) {
    return true;
  }
  if (IsVariableLengthJSArrayBufferView(typedArray)) {
    // TODO(v8:11111): Add a fast(er) path here.
    return true;
  }
  return false;
120
}
121

122 123 124 125 126 127 128 129 130 131 132
// 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);
133
  }
134
}
135

136 137 138 139 140 141
// 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<T: type>(
    fixedArray: FixedArray, index: intptr, element: T): FixedArray {
  const length: intptr = fixedArray.length_intptr;
142
  dcheck(index <= length);
143 144 145 146 147 148
  if (index < length) {
    fixedArray.objects[index] = element;
    return fixedArray;
  } else
    deferred {
      const newLength: intptr = CalculateNewElementsCapacity(length);
149
      dcheck(index < newLength);
150 151 152 153
      const newfixedArray: FixedArray =
          ExtractFixedArray(fixedArray, 0, length, newLength);
      newfixedArray.objects[index] = element;
      return newfixedArray;
154
    }
155
}
156

157 158 159 160 161 162
// Contains the information necessary to create a single, separator delimited,
// flattened one or two byte string.
// The buffer is maintained and updated by Buffer.constructor, Buffer.Add(),
// Buffer.AddSeparators().
struct Buffer {
  macro Add(implicit context: Context)(
163
      str: String, nofSeparators: intptr, separatorLength: intptr): void {
164 165 166 167 168 169 170 171 172 173
    // 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;
174 175
  }

176
  macro AddSeparators(implicit context: Context)(
177
      nofSeparators: intptr, separatorLength: intptr, write: bool): void {
178
    if (nofSeparators == 0 || separatorLength == 0) return;
179

180 181 182
    const nofSeparatorsInt: intptr = nofSeparators;
    const sepsLen: intptr = separatorLength * nofSeparatorsInt;
    // Detect integer overflow
183
    // TODO(turbofan): Replace with overflow-checked multiplication.
184 185 186
    if (sepsLen / separatorLength != nofSeparatorsInt) deferred {
        ThrowInvalidStringLength(context);
      }
187

188 189 190 191
    this.totalStringLength = AddStringLength(this.totalStringLength, sepsLen);
    if (write) deferred {
        this.fixedArray = StoreAndGrowFixedArray(
            this.fixedArray, this.index++, Convert<Smi>(nofSeparatorsInt));
192 193 194
      }
  }

195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
  // 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:   [<hole>, 'hello', <hole>, 'world', <hole>]
  //   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;
}
222

223 224 225 226
macro NewBuffer(len: uintptr, sep: String): Buffer {
  const cappedBufferSize: intptr = len > kMaxNewSpaceFixedArrayElements ?
      kMaxNewSpaceFixedArrayElements :
      Signed(len);
227
  dcheck(cappedBufferSize > 0);
228 229 230 231 232 233 234
  return Buffer{
    fixedArray: AllocateZeroedFixedArray(cappedBufferSize),
    index: 0,
    totalStringLength: 0,
    isOneByte: IsOneByteStringInstanceType(sep.instanceType)
  };
}
235

236 237
macro BufferJoin(implicit context: Context)(
    buffer: Buffer, sep: String): String {
238
  dcheck(IsValidPositiveSmi(buffer.totalStringLength));
239 240 241 242 243 244 245 246 247 248 249
  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;
      }
250

251 252 253 254 255 256 257 258
      // 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;
      }
259
    }
260 261
  }

262 263 264 265 266 267
  const length: uint32 = Convert<uint32>(Unsigned(buffer.totalStringLength));
  const r: String = buffer.isOneByte ? AllocateSeqOneByteString(length) :
                                       AllocateSeqTwoByteString(length);
  return CallJSArrayArrayJoinConcatToSequentialString(
      buffer.fixedArray, buffer.index, sep, r);
}
268

269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288
transitioning macro ArrayJoinImpl<T: type>(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<uintptr>(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<T>(
            loadFn, receiver, initialMap, lengthNumber))
      deferred {
        loadFn = LoadJoinElement<array::GenericElementsAccessor>;
289
      }
290 291 292 293

    if (k > 0) {
      // a. If k > 0, let R be the string-concatenation of R and sep.
      nofSeparators = nofSeparators + 1;
294 295
    }

296 297
    // b. Let element be ? Get(O, ! ToString(k)).
    const element: JSAny = loadFn(context, receiver, k++);
298

299 300 301 302 303 304 305 306 307 308 309
    // 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;
310
        }
311
        case (num: Number): {
312
          next = NumberToString(num);
313 314 315
        }
        case (obj: JSAny): {
          if (IsNullOrUndefined(obj)) continue;
316
          next = string::ToString(context, obj);
317
        }
318
      }
319 320
    }

321 322 323
    // d. Set R to the string-concatenation of R and next.
    buffer.Add(next, nofSeparators, separatorLength);
    nofSeparators = 0;
324 325
  }

326 327
  // Add any separators at the end.
  buffer.AddSeparators(nofSeparators, separatorLength, true);
328

329 330 331
  // 8. Return R.
  return BufferJoin(buffer, sep);
}
332

333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
transitioning macro ArrayJoin<T: type>(implicit context: Context)(
    useToLocaleString: constexpr bool, receiver: JSReceiver, sep: String,
    lenNumber: Number, locales: JSAny, options: JSAny): JSAny;

transitioning ArrayJoin<JSArray>(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<JSArray>(receiver) otherwise IfSlowPath;
    if (array.length != lenNumber) goto IfSlowPath;
    if (!IsPrototypeInitialArrayPrototype(map)) goto IfSlowPath;
    if (IsNoElementsProtectorCellInvalid()) goto IfSlowPath;

    if (IsElementsKindLessThanOrEqual(kind, ElementsKind::HOLEY_ELEMENTS)) {
      loadFn = LoadJoinElement<array::FastSmiOrObjectElements>;
    } else if (IsElementsKindLessThanOrEqual(
                   kind, ElementsKind::HOLEY_DOUBLE_ELEMENTS)) {
      loadFn = LoadJoinElement<array::FastDoubleElements>;
    } else if (kind == ElementsKind::DICTIONARY_ELEMENTS)
      deferred {
        const dict: NumberDictionary =
            UnsafeCast<NumberDictionary>(array.elements);
        const nofElements: Smi = GetNumberDictionaryNumberOfElements(dict);
        if (nofElements == 0) {
          if (sep == kEmptyString) return kEmptyString;
          try {
            const nofSeparators: Smi =
                Cast<Smi>(lenNumber - 1) otherwise IfNotSmi;
            return StringRepeat(context, sep, nofSeparators);
          } label IfNotSmi {
            ThrowInvalidStringLength(context);
368
          }
369 370
        } else {
          loadFn = LoadJoinElement<array::DictionaryElements>;
371
        }
372
      }
373 374
    else {
      goto IfSlowPath;
375
    }
376 377
  } label IfSlowPath {
    loadFn = LoadJoinElement<array::GenericElementsAccessor>;
378
  }
379 380 381
  return ArrayJoinImpl<JSArray>(
      receiver, sep, lenNumber, useToLocaleString, locales, options, loadFn);
}
382

383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402
transitioning ArrayJoin<JSTypedArray>(implicit context: Context)(
    useToLocaleString: constexpr bool, receiver: JSReceiver, sep: String,
    lenNumber: Number, locales: JSAny, options: JSAny): JSAny {
  const map: Map = receiver.map;
  const kind: ElementsKind = map.elements_kind;
  let loadFn: LoadJoinElementFn;

  if (IsElementsKindGreaterThan(kind, ElementsKind::UINT32_ELEMENTS)) {
    if (kind == ElementsKind::INT32_ELEMENTS) {
      loadFn = LoadJoinTypedElement<typed_array::Int32Elements>;
    } else if (kind == ElementsKind::FLOAT32_ELEMENTS) {
      loadFn = LoadJoinTypedElement<typed_array::Float32Elements>;
    } else if (kind == ElementsKind::FLOAT64_ELEMENTS) {
      loadFn = LoadJoinTypedElement<typed_array::Float64Elements>;
    } else if (kind == ElementsKind::UINT8_CLAMPED_ELEMENTS) {
      loadFn = LoadJoinTypedElement<typed_array::Uint8ClampedElements>;
    } else if (kind == ElementsKind::BIGUINT64_ELEMENTS) {
      loadFn = LoadJoinTypedElement<typed_array::BigUint64Elements>;
    } else if (kind == ElementsKind::BIGINT64_ELEMENTS) {
      loadFn = LoadJoinTypedElement<typed_array::BigInt64Elements>;
403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424
    } else if (kind == ElementsKind::RAB_GSAB_UINT8_ELEMENTS) {
      loadFn = LoadJoinTypedElement<typed_array::Uint8Elements>;
    } else if (kind == ElementsKind::RAB_GSAB_INT8_ELEMENTS) {
      loadFn = LoadJoinTypedElement<typed_array::Int8Elements>;
    } else if (kind == ElementsKind::RAB_GSAB_UINT16_ELEMENTS) {
      loadFn = LoadJoinTypedElement<typed_array::Uint16Elements>;
    } else if (kind == ElementsKind::RAB_GSAB_INT16_ELEMENTS) {
      loadFn = LoadJoinTypedElement<typed_array::Int16Elements>;
    } else if (kind == ElementsKind::RAB_GSAB_UINT32_ELEMENTS) {
      loadFn = LoadJoinTypedElement<typed_array::Uint32Elements>;
    } else if (kind == ElementsKind::RAB_GSAB_INT32_ELEMENTS) {
      loadFn = LoadJoinTypedElement<typed_array::Int32Elements>;
    } else if (kind == ElementsKind::RAB_GSAB_FLOAT32_ELEMENTS) {
      loadFn = LoadJoinTypedElement<typed_array::Float32Elements>;
    } else if (kind == ElementsKind::RAB_GSAB_FLOAT64_ELEMENTS) {
      loadFn = LoadJoinTypedElement<typed_array::Float64Elements>;
    } else if (kind == ElementsKind::RAB_GSAB_UINT8_CLAMPED_ELEMENTS) {
      loadFn = LoadJoinTypedElement<typed_array::Uint8ClampedElements>;
    } else if (kind == ElementsKind::RAB_GSAB_BIGUINT64_ELEMENTS) {
      loadFn = LoadJoinTypedElement<typed_array::BigUint64Elements>;
    } else if (kind == ElementsKind::RAB_GSAB_BIGINT64_ELEMENTS) {
      loadFn = LoadJoinTypedElement<typed_array::BigInt64Elements>;
425
    } else {
426 427 428 429 430 431 432 433 434 435 436 437 438 439 440
      unreachable;
    }
  } else {
    if (kind == ElementsKind::UINT8_ELEMENTS) {
      loadFn = LoadJoinTypedElement<typed_array::Uint8Elements>;
    } else if (kind == ElementsKind::INT8_ELEMENTS) {
      loadFn = LoadJoinTypedElement<typed_array::Int8Elements>;
    } else if (kind == ElementsKind::UINT16_ELEMENTS) {
      loadFn = LoadJoinTypedElement<typed_array::Uint16Elements>;
    } else if (kind == ElementsKind::INT16_ELEMENTS) {
      loadFn = LoadJoinTypedElement<typed_array::Int16Elements>;
    } else if (kind == ElementsKind::UINT32_ELEMENTS) {
      loadFn = LoadJoinTypedElement<typed_array::Uint32Elements>;
    } else {
      unreachable;
441 442
    }
  }
443 444 445
  return ArrayJoinImpl<JSTypedArray>(
      receiver, sep, lenNumber, useToLocaleString, locales, options, loadFn);
}
446

447 448 449 450 451 452 453 454 455
// 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 {
456 457 458 459 460 461 462 463
  typeswitch (*NativeContextSlot(ContextSlot::ARRAY_JOIN_STACK_INDEX)) {
    case (Undefined): {
      goto IfUninitialized;
    }
    case (stack: FixedArray): {
      return stack;
    }
  }
464
}
465

466
macro SetJoinStack(implicit context: Context)(stack: FixedArray): void {
467
  *NativeContextSlot(ContextSlot::ARRAY_JOIN_STACK_INDEX) = stack;
468
}
469

470 471 472 473 474 475 476 477 478 479 480 481 482
// 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;
483 484
    }

485 486
    // Detect cycles
    if (receiver == previouslyVisited) return False;
487 488
  }

489 490 491 492 493 494 495 496 497 498 499 500 501 502 503
  // 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) {
504
      stack.objects[0] = receiver;
505 506 507 508 509 510 511 512 513
    } else if (JoinStackPush(stack, receiver) == False)
      deferred {
        return false;
      }
  } label IfUninitialized {
    const stack: FixedArray =
        AllocateFixedArrayWithHoles(kMinJoinStackSize, AllocationFlag::kNone);
    stack.objects[0] = receiver;
    SetJoinStack(stack);
514
  }
515 516
  return true;
}
517

518 519 520 521 522 523 524 525 526 527 528 529 530
// Removes a receiver from the stack. The FixedArray will automatically shrink
// to Heap::kMinJoinStackSize once the stack becomes empty.
builtin JoinStackPop(implicit context: Context)(
    stack: FixedArray, receiver: JSReceiver): JSAny {
  const len: intptr = stack.length_intptr;
  for (let i: intptr = 0; i < len; i++) {
    if (stack.objects[i] == receiver) {
      // Shrink the Join Stack if the stack will be empty and is larger than
      // the minimum size.
      if (i == 0 && len > kMinJoinStackSize) deferred {
          const newStack: FixedArray = AllocateFixedArrayWithHoles(
              kMinJoinStackSize, AllocationFlag::kNone);
          SetJoinStack(newStack);
531
        }
532 533
      else {
        stack.objects[i] = TheHole;
534
      }
535
      return Undefined;
536 537
    }
  }
538 539
  unreachable;
}
540

541
// Fast path the common non-nested calls.
542 543
macro JoinStackPopInline(implicit context: Context)(receiver: JSReceiver):
    void {
544 545 546 547 548 549 550
  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) {
551
    stack.objects[0] = TheHole;
552 553 554 555 556
  } else
    deferred {
      JoinStackPop(stack, receiver);
    }
}
557

558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574
// Main entry point for all builtins using Array Join functionality.
transitioning macro CycleProtectedArrayJoin<T: type>(
    implicit context: Context)(
    useToLocaleString: constexpr bool, o: JSReceiver, len: Number,
    sepObj: JSAny, locales: JSAny, options: JSAny): JSAny {
  // 3. If separator is undefined, let sep be the single-element String ",".
  // 4. Else, let sep be ? ToString(separator).
  const sep: String = sepObj == Undefined ? ',' : ToString_Inline(sepObj);

  // If the receiver is not empty and not already being joined, continue with
  // the normal join algorithm.
  if (len > 0 && JoinStackPushInline(o)) {
    try {
      const result: JSAny =
          ArrayJoin<T>(useToLocaleString, o, sep, len, locales, options);
      JoinStackPopInline(o);
      return result;
575
    } catch (e, message) deferred {
576
      JoinStackPopInline(o);
577
      ReThrowWithMessage(context, e, message);
578
    }
579 580
  } else {
    return kEmptyString;
581
  }
582
}
583

584 585 586 587 588
// https://tc39.github.io/ecma262/#sec-array.prototype.join
transitioning javascript builtin
ArrayPrototypeJoin(
    js-implicit context: NativeContext, receiver: JSAny)(...arguments): JSAny {
  const separator: JSAny = arguments[0];
589

590 591
  // 1. Let O be ? ToObject(this value).
  const o: JSReceiver = ToObject_Inline(context, receiver);
592

593 594
  // 2. Let len be ? ToLength(? Get(O, "length")).
  const len: Number = GetLengthProperty(o);
595

596 597
  // Only handle valid array lengths. Although the spec allows larger
  // values, this matches historical V8 behavior.
598
  if (len > kMaxArrayLength) {
599
    ThrowTypeError(MessageTemplate::kInvalidArrayLength);
600
  }
601

602 603 604
  return CycleProtectedArrayJoin<JSArray>(
      false, o, len, separator, Undefined, Undefined);
}
605

606 607 608 609 610
// https://tc39.github.io/ecma262/#sec-array.prototype.tolocalestring
transitioning javascript builtin ArrayPrototypeToLocaleString(
    js-implicit context: NativeContext, receiver: JSAny)(...arguments): JSAny {
  const locales: JSAny = arguments[0];
  const options: JSAny = arguments[1];
611

612 613
  // 1. Let O be ? ToObject(this value).
  const o: JSReceiver = ToObject_Inline(context, receiver);
614

615 616
  // 2. Let len be ? ToLength(? Get(O, "length")).
  const len: Number = GetLengthProperty(o);
617

618 619
  // Only handle valid array lengths. Although the spec allows larger
  // values, this matches historical V8 behavior.
620
  if (len > kMaxArrayLength) {
621
    ThrowTypeError(MessageTemplate::kInvalidArrayLength);
622
  }
623

624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643
  return CycleProtectedArrayJoin<JSArray>(true, o, len, ',', locales, options);
}

// https://tc39.github.io/ecma262/#sec-array.prototype.tostring
transitioning javascript builtin ArrayPrototypeToString(
    js-implicit context: NativeContext, receiver: JSAny)(...arguments): JSAny {
  // 1. Let array be ? ToObject(this value).
  const array: JSReceiver = ToObject_Inline(context, receiver);

  // 2. Let func be ? Get(array, "join").
  const prop: JSAny = GetProperty(array, 'join');
  try {
    // 3. If IsCallable(func) is false, let func be the intrinsic function
    //    %ObjProto_toString%.
    const func: Callable = Cast<Callable>(prop) otherwise NotCallable;

    // 4. Return ? Call(func, array).
    return Call(context, func, array);
  } label NotCallable {
    return ObjectToString(context, array);
644
  }
645
}
646

647 648 649 650
// https://tc39.github.io/ecma262/#sec-%typedarray%.prototype.join
transitioning javascript builtin TypedArrayPrototypeJoin(
    js-implicit context: NativeContext, receiver: JSAny)(...arguments): JSAny {
  const separator: JSAny = arguments[0];
651

652 653
  // Spec: ValidateTypedArray is applied to the this value prior to evaluating
  // the algorithm.
654
  const length = typed_array::ValidateTypedArrayAndGetLength(
655
      context, receiver, '%TypedArray%.prototype.join');
656
  const typedArray: JSTypedArray = UnsafeCast<JSTypedArray>(receiver);
657

658
  return CycleProtectedArrayJoin<JSTypedArray>(
659 660
      false, typedArray, Convert<Number>(length), separator, Undefined,
      Undefined);
661
}
662

663 664 665 666 667
// https://tc39.github.io/ecma262/#sec-%typedarray%.prototype.tolocalestring
transitioning javascript builtin TypedArrayPrototypeToLocaleString(
    js-implicit context: NativeContext, receiver: JSAny)(...arguments): JSAny {
  const locales: JSAny = arguments[0];
  const options: JSAny = arguments[1];
668

669 670
  // Spec: ValidateTypedArray is applied to the this value prior to evaluating
  // the algorithm.
671
  const length = typed_array::ValidateTypedArrayAndGetLength(
672
      context, receiver, '%TypedArray%.prototype.toLocaleString');
673
  const typedArray: JSTypedArray = UnsafeCast<JSTypedArray>(receiver);
674

675
  return CycleProtectedArrayJoin<JSTypedArray>(
676
      true, typedArray, Convert<Number>(length), ',', locales, options);
677
}
678
}