← All reports

[JSC] Add JSArray::fastSplice

Component: JSC Array builtins / DFG | d29218b

Source/JavaScriptCore/runtime/JSArray.cpp

+bool JSArray::fastSplice(JSGlobalObject* globalObject, uint64_t length, uint64_t start, uint64_t deleteCount, std::span<const EncodedJSValue> items)
+{
+ ...
+ if (holesMustForwardToPrototype()) [[unlikely]]
+ return false;
+ ...
+ uint64_t newLength = length - deleteCount + itemCount;
+ if (newLength > MAX_STORAGE_VECTOR_LENGTH)
+ return false;
+ if (itemCount != deleteCount && length - start - deleteCount >= MIN_SPARSE_ARRAY_INDEX)
+ return false;
+ ...
+ else {
+ gcSafeMemcpy(butterfly->contiguous().data() + start, std::bit_cast<const WriteBarrier<Unknown>*>(items.data()), sizeof(JSValue) * items.size());
+ vm.writeBarrier(this);
+ }
+ return true;
+}

Source/JavaScriptCore/dfg/DFGOperations.cpp

+ std::span<const EncodedJSValue> items { buffer, itemCount };
+ // Nothing between here and fastSplice can run user code, so the items can be read from the scratch buffer without a copy.
+ if (arraySpeciesWatchpointIsValid(vm, base)) {
+ ...
+ bool spliced = base->fastSplice(globalObject, length, actualStart, actualDeleteCount, items);

JSC stores array elements in a butterfly whose layout depends on the indexing type (Int32, Double, Contiguous, ArrayStorage). Holes are encoded as special values: PNaN in Double arrays and an empty JSValue in the others. Spec splice uses HasProperty/Get, so a hole can read through to Array.prototype or Object.prototype, which can run getters or proxies. JSC tracks this with holesMustForwardToPrototype. When it is false, holes can be treated as plain absent elements and splice can move elements in place with no user-observable side effects. The shift/unshift count helpers already checked for holes before moving anything.

This commit adds JSArray::fastSplice, an in-place splice for Int32, Double and Contiguous arrays. It returns false, so the caller falls back to the generic path, if the indexing type is not one of those three or the length does not match the butterfly's public length. After those checks, a no-op splice returns true immediately. Otherwise it also returns false if holesMustForwardToPrototype is set, if an inserted item does not fit the type (non-Int32 for Int32; non-number or NaN for Double), if the new length exceeds MAX_STORAGE_VECTOR_LENGTH, or, when item and delete counts differ, if the tail length reaches MIN_SPARSE_ARRAY_INDEX. Otherwise it moves the tail with shiftCountWithAnyIndexingType / unshiftCountWithAnyIndexingType (asserted to succeed within those bounds) and writes the items directly into the butterfly. The helpers now leave the array untouched when they fail on holes, which matters for their generic callers. arrayProtoFuncSplice passes call-frame arguments via ArgList, and the DFG arraySpliceImpl passes items straight from the scratch buffer as a span, building the MarkedArgumentBuffer only after the fast path declines. With a valid species watchpoint it also calls JSArray::fastSlice for the removed-elements array, but only if the result is used (!ignoreResult).

Host function (arrayProtoFuncSplice):
  splice(start, del, ...items)
    ├─► result array built (generic species path)
    ├─► isJSArray(this)? ──► JSArray::fastSplice(items from ArgList)
    │                          ├─ true  ─► return result
    │                          └─ false ─► generic shift/unshift + write items

DFG/FTL (arraySpliceImpl):
  splice(start, del, ...items)   items stay in scratch buffer (span, no copy)
    └─► arraySpeciesWatchpointIsValid?
          ├─ no ──────────────────────────────► generic path
          └─ yes
              ├─ !ignoreResult: JSArray::fastSlice(removed)
              │     └─ null ──────────────────► generic path
              └─ ignoreResult || slice ok:
                    JSArray::fastSplice(items span)
                      ├─ true  ─► return (undefined | slice result)
                      └─ false ─► generic path
  generic path (DFG): items copied to MarkedArgumentBuffer now, then species construct,
                      removed-array slice skipped if didFastSlice, shift/unshift + write items

splice is a hot operation, and this skips the generic per-element spec semantics for the common case in both the host function and the DFG/FTL operation. A successful DFG fast splice never builds a MarkedArgumentBuffer, because that copy is deferred until the fast path fails. The commit also changes the failure contract of the shift/unshift helpers, so callers can bail out to the generic path with the array unmodified.

This is new hand-written in-place array mutation in a core builtin, reachable from both the host function and the DFG operation, and it relies on several preconditions holding at once.