[JSC] Add JSArray::fastSplice
Component: JSC Array builtins / DFG | d29218b
Source/JavaScriptCore/runtime/JSArray.cpp
Source/JavaScriptCore/dfg/DFGOperations.cpp
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
Significance
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.
Audit directions
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.
- GC safety of unprotected items. The DFG path no longer copies items into a
MarkedArgumentBuffer, so the raw scratch-buffer values are the only references to inserted cells. The comment says no user code can run, butunshiftCountWithAnyIndexingTypecan grow the butterfly (the code checks for an exception after it), which allocates and may trigger GC or OOM. Verify the scratch buffer is scanned for the full call and that no collection can observe or free the items beforegcSafeMemcpyandwriteBarrier. - Write barriers. The Int32 path uses a plain
memcpywith no barrier, sound only because every item is validated as an int32 first. The Contiguous path writes raw cells viagcSafeMemcpyand then callsvm.writeBarrier(this). Check the barrier is not skipped on any early return, and that the Double path (items read viaasNumberafter rejecting NaN) can never write a PNaN that reads back as a hole. - Bounds arithmetic.
newLengthis always checked againstMAX_STORAGE_VECTOR_LENGTH, but the tail-length check againstMIN_SPARSE_ARRAY_INDEXapplies only whenitemCount != deleteCount, and values are then truncated to unsigned forshiftCount/unshiftCount. Check the equal-count case, where onlyensureWritableruns (it handles COW), and whetherstart + itemCount/start + deleteCountcan exceed uint32 despite the checks. - Shift/unshift failure contract. The helpers now take
startIndexby value, callers passheader + resultCount/header + currentCount, and theheader32write-back is gone. Audit every shift/unshift/splice caller, including the generic builtins and DFG paths, for assumptions about partial mutation or a reused header after a failed attempt. In release builds theASSERT_UNUSED(shifted/unshifted)calls would not catch a false return. - Species and removed-array flow in DFG.
fastSliceruns beforefastSplice, gated onarraySpeciesWatchpointIsValidand!ignoreResult.didFastSliceis set wheneverfastSlicewas attempted without an exception, even if it returned null, and the generic path then skips its FastPath slice-creation block. Check what the generic path produces whenfastSlicereturned null, or whenfastSplicereturns false after a non-null slice, that the ASSERT that species construction returns FastPath holds, and that the receiver cannot change between slice and splice. setLengthearly return. It now returns early when the length is unchanged on non-ArrayStorage arrays, on the claim that only ArrayStorage can hold a read-only length. Check that against frozen, sealed and non-extensible arrays, and against other length-related state the oldsetLengthcall handled.