[JSC] Implement `String#split` in C++
Component: JSC | 1a01603
Source/JavaScriptCore/builtins/BuiltinNames.h
Source/JavaScriptCore/builtins/StringPrototype.js
Source/JavaScriptCore/runtime/RegExpObjectInlines.h
JSC optimizes builtins along two tracks: self-hosted JS builtins, which compile to bytecode and get JIT'd generically, and DFG intrinsics, which become type-specialized IR nodes emitting direct C++ calls. This commit moves String.prototype.split from the first track to the second, deleting the JS builtin and introducing a StringSplit DFG node that dispatches to operationStringSplit for string separators or operationStringSplitRegExp for primordial RegExp separators. Skipping RegExp.prototype[@@split] entirely is only sound while nobody has made that call observable, so three watchpoint sets now guard the fast path — m_stringSymbolSplitWatchpointSet, m_regExpSpeciesWatchpointSet, and splitSymbol added to regExpPrimordialPropertiesWatchpointSet. A watchpoint is an invalidation hook: if any of those properties is assigned, it fires and the JIT must deoptimize or fall to the slow path.
String#split(sep) call
│
[DFG StringSplit node]
│
watchpoints valid?
┌─────┴──────────────────────────┐
yes no
│ │
sep type check slow path
│ (observable @@split)
├─ String ──────────────► operationStringSplit
│
└─ RegExp?
│
isSymbolSplitFastAndNonObservable?
├─ yes ──► operationStringSplitRegExp (C++ fast)
└─ no ──► RegExp.prototype[@@split] (JS, observable)
Significance
11-23% speedup on split-heavy workloads, but the security story is the fast-path gate rather than the numbers. The new isSymbolSplitFastAndNonObservable check, three new watchpoint sets, and a new DFG node boundary all introduce edge cases that mirror the historical bug surface of String#replace's analogous C++ migration.
Audit directions
Three areas repay close inspection. First, watchpoint invalidation races: m_stringSymbolSplitWatchpointSet and m_regExpSpeciesWatchpointSet are new. If either can be triggered after the fast-path guard but before the C++ operation completes — or if their invalidation order relative to JIT code patching is wrong — an attacker could make the JIT operate on a regex whose @@split has been swapped without triggering recompilation. This is the same class of bug as older @@replace watchpoint races, so the replace history is the place to start pattern-matching.
Second, the isSymbolSplitFastAndNonObservable structure check: per-instance @@split overrides are caught by a structure check, and structure IDs are recycled after GC — a carefully timed allocation sequence causing structure ID reuse could make a modified RegExp pass. Diff the implementation against its replace counterpart and treat any divergence as the interesting part.
Third, the separator type dispatch boundary: the DFG node selects between the string and RegExp paths on type speculation. What happens when a separator object carries both a toString and a Symbol.split? When the separator is a String object rather than a primitive? The C++ fast path may handle these differently from the old JS builtin, which followed the spec's explicit typeof branches — and this pattern generalizes to every other builtin migrated from self-hosted JS to a C++ host function, where the spec's coercion order is easy to lose in translation.