Skip to content

Commit 5031c90

Browse files
committed
[JSC] Implement String.raw in C++
https://bugs.webkit.org/show_bug.cgi?id=319012 rdar://181857218 Reviewed by Keith Miller. This patch implements String.raw in C++, which removes builtin JS version of String.raw. Since array is frozen, builtin JS iteration does not offer much in fact. We can write all array types in an efficient way in C++. ToT Patched string-raw 38.3499+-0.2422 ^ 33.8134+-0.3764 ^ definitely 1.1342x faster string-raw-large 144.9109+-3.1563 ^ 118.7783+-2.5823 ^ definitely 1.2200x faster Tests: JSTests/microbenchmarks/string-raw-large.js JSTests/microbenchmarks/string-raw.js * JSTests/microbenchmarks/string-raw-large.js: Added. (test): * JSTests/microbenchmarks/string-raw.js: Added. (test): * Source/JavaScriptCore/CMakeLists.txt: * Source/JavaScriptCore/DerivedSources.make: * Source/JavaScriptCore/JavaScriptCore.xcodeproj/project.pbxproj: * Source/JavaScriptCore/builtins/StringConstructor.js: Removed. * Source/JavaScriptCore/runtime/JSString.cpp: (JSC::JSRopeString::RopeBuilder<RecordOverflow>::expand): * Source/JavaScriptCore/runtime/JSString.h: * Source/JavaScriptCore/runtime/StringConstructor.cpp: (JSC::JSC_DEFINE_HOST_FUNCTION): Canonical link: https://commits.webkit.org/317009@main
1 parent 3bd19f0 commit 5031c90

9 files changed

Lines changed: 171 additions & 77 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//@ skip if $model == "Apple Watch Series 3" # added by mark-jsc-stress-test.py
2+
// String.raw over a large tagged template: 33 raw segments + 32 substitutions.
3+
// A tagged template's raw array is spec-frozen (ArrayWithSlowPutArrayStorage with
4+
// its elements in a SparseArrayValueMap), so this exercises String.raw's frozen-array
5+
// segment reads together with many-fiber rope building. The substitution varies each
6+
// call (so the result can't be hoisted) but stays a single digit, keeping the result
7+
// length constant for a cheap correctness check.
8+
function test(x) {
9+
return String.raw`s0${x}s1${x}s2${x}s3${x}s4${x}s5${x}s6${x}s7${x}s8${x}s9${x}s10${x}s11${x}s12${x}s13${x}s14${x}s15${x}s16${x}s17${x}s18${x}s19${x}s20${x}s21${x}s22${x}s23${x}s24${x}s25${x}s26${x}s27${x}s28${x}s29${x}s30${x}s31${x}s32`.length;
10+
}
11+
noInline(test);
12+
13+
var expected = test(0);
14+
var total = 0;
15+
for (var i = 0; i < 3e5; ++i)
16+
total += test(i % 10);
17+
18+
if (total !== expected * 3e5)
19+
throw "Error: bad result: " + total;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//@ skip if $model == "Apple Watch Series 3" # added by mark-jsc-stress-test.py
2+
function test(x, y) {
3+
return String.raw`a\t${x}b\n${y}c`;
4+
}
5+
noInline(test);
6+
7+
var total = 0;
8+
for (var i = 0; i < 1e6; ++i)
9+
total += test(i % 10, (i + 1) % 10).length;
10+
11+
if (total !== 9000000)
12+
throw "Error: bad result: " + total;

‎Source/JavaScriptCore/CMakeLists.txt‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,6 @@ set(JavaScriptCore_BUILTINS_SOURCES
384384
${JAVASCRIPTCORE_DIR}/builtins/ReflectObject.js
385385
${JAVASCRIPTCORE_DIR}/builtins/SetPrototype.js
386386
${JAVASCRIPTCORE_DIR}/builtins/ShadowRealmPrototype.js
387-
${JAVASCRIPTCORE_DIR}/builtins/StringConstructor.js
388387
${JAVASCRIPTCORE_DIR}/builtins/TypedArrayConstructor.js
389388
${JAVASCRIPTCORE_DIR}/builtins/TypedArrayPrototype.js
390389
${JAVASCRIPTCORE_DIR}/builtins/WrapForValidIteratorPrototype.js

‎Source/JavaScriptCore/DerivedSources.make‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ JavaScriptCore_BUILTINS_SOURCES = \
125125
$(JavaScriptCore)/builtins/ReflectObject.js \
126126
$(JavaScriptCore)/builtins/SetPrototype.js \
127127
$(JavaScriptCore)/builtins/ShadowRealmPrototype.js \
128-
$(JavaScriptCore)/builtins/StringConstructor.js \
129128
$(JavaScriptCore)/builtins/TypedArrayConstructor.js \
130129
$(JavaScriptCore)/builtins/TypedArrayPrototype.js \
131130
$(JavaScriptCore)/builtins/WrapForValidIteratorPrototype.js \

‎Source/JavaScriptCore/JavaScriptCore.xcodeproj/project.pbxproj‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4977,7 +4977,6 @@
49774977
7CF9BC5D1B65D9B1009DB1EF /* PromiseOperations.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = PromiseOperations.js; sourceTree = "<group>"; };
49784978
7CF9BC5E1B65D9B1009DB1EF /* PromiseConstructor.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = PromiseConstructor.js; sourceTree = "<group>"; };
49794979
7CF9BC5F1B65D9B1009DB1EF /* ReflectObject.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = ReflectObject.js; sourceTree = "<group>"; };
4980-
7CF9BC601B65D9B1009DB1EF /* StringConstructor.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = StringConstructor.js; sourceTree = "<group>"; };
49814980
7D5FB18D20744AE8005DDF64 /* IntlPluralRulesPrototype.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IntlPluralRulesPrototype.h; sourceTree = "<group>"; };
49824981
7D5FB18E20744BF0005DDF64 /* IntlPluralRulesConstructor.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = IntlPluralRulesConstructor.cpp; sourceTree = "<group>"; };
49834982
7D5FB18F20744BF1005DDF64 /* IntlPluralRules.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = IntlPluralRules.cpp; sourceTree = "<group>"; };
@@ -10698,7 +10697,6 @@
1069810697
7CF9BC5F1B65D9B1009DB1EF /* ReflectObject.js */,
1069910698
7035587D1C418419004BD7BF /* SetPrototype.js */,
1070010699
2E3AF00D28171AC000371A25 /* ShadowRealmPrototype.js */,
10701-
7CF9BC601B65D9B1009DB1EF /* StringConstructor.js */,
1070210700
534C457A1BC703DC007476A7 /* TypedArrayConstructor.js */,
1070310701
53917E831B791CB8000EBD33 /* TypedArrayPrototype.js */,
1070410702
E39E030D22D9EA3E00408C18 /* WebAssembly.js */,

‎Source/JavaScriptCore/builtins/StringConstructor.js‎

Lines changed: 0 additions & 55 deletions
This file was deleted.

‎Source/JavaScriptCore/runtime/JSString.cpp‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,17 @@ template<>
5252
void JSRopeString::RopeBuilder<RecordOverflow>::expand()
5353
{
5454
RELEASE_ASSERT(!this->hasOverflowed());
55-
ASSERT(m_strings.size() == JSRopeString::s_maxInternalRopeLength);
55+
ASSERT(m_index == JSRopeString::s_maxInternalRopeLength);
5656
static_assert(3 == JSRopeString::s_maxInternalRopeLength);
5757
ASSERT(m_length);
58-
ASSERT(asString(m_strings.at(0))->length());
59-
ASSERT(asString(m_strings.at(1))->length());
60-
ASSERT(asString(m_strings.at(2))->length());
58+
ASSERT(m_strings[0]->length());
59+
ASSERT(m_strings[1]->length());
60+
ASSERT(m_strings[2]->length());
6161

62-
JSString* string = JSRopeString::create(m_vm, asString(m_strings.at(0)), asString(m_strings.at(1)), asString(m_strings.at(2)));
62+
JSString* string = JSRopeString::create(m_vm, m_strings[0], m_strings[1], m_strings[2]);
6363
ASSERT(string->length() == m_length);
64-
m_strings.clear();
65-
m_strings.append(string);
64+
m_strings[0] = string;
65+
m_index = 1;
6666
}
6767

6868
void JSString::dumpToStream(const JSCell* cell, PrintStream& out)

‎Source/JavaScriptCore/runtime/JSString.h‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ class JSRopeString final : public JSString {
341341
}
342342

343343
// We use lower 3bits of fiber0 for flags. These bits are usable due to alignment, and it is OK even in 32bit architecture.
344+
static constexpr unsigned s_maxInternalRopeLength = 3;
344345
static constexpr uintptr_t is8BitInPointer = static_cast<uintptr_t>(StringImpl::flagIs8Bit());
345346
static constexpr uintptr_t isSubstringInPointer = 0x2;
346347
static_assert(is8BitInPointer == 0b100);
@@ -456,11 +457,12 @@ class JSRopeString final : public JSString {
456457

457458
bool append(JSString* jsString)
458459
{
460+
static_assert(3 == JSRopeString::s_maxInternalRopeLength);
459461
if (this->hasOverflowed()) [[unlikely]]
460462
return false;
461463
if (!jsString->length())
462464
return true;
463-
if (m_strings.size() == JSRopeString::s_maxInternalRopeLength)
465+
if (m_index == JSRopeString::s_maxInternalRopeLength)
464466
expand();
465467

466468
static_assert(JSString::MaxLength == std::numeric_limits<int32_t>::max());
@@ -470,7 +472,7 @@ class JSRopeString final : public JSString {
470472
return false;
471473
}
472474
ASSERT(static_cast<unsigned>(sum) <= MaxLength);
473-
m_strings.append(jsString);
475+
m_strings[m_index++] = jsString;
474476
m_length = static_cast<unsigned>(sum);
475477
return true;
476478
}
@@ -479,30 +481,31 @@ class JSRopeString final : public JSString {
479481
{
480482
RELEASE_ASSERT(!this->hasOverflowed());
481483
JSString* result = nullptr;
482-
switch (m_strings.size()) {
484+
static_assert(3 == JSRopeString::s_maxInternalRopeLength);
485+
switch (m_index) {
483486
case 0: {
484487
ASSERT(!m_length);
485488
result = jsEmptyString(m_vm);
486489
break;
487490
}
488491
case 1: {
489-
result = asString(m_strings.at(0));
492+
result = m_strings[0];
490493
break;
491494
}
492495
case 2: {
493-
result = JSRopeString::create(m_vm, asString(m_strings.at(0)), asString(m_strings.at(1)));
496+
result = JSRopeString::create(m_vm, m_strings[0], m_strings[1]);
494497
break;
495498
}
496499
case 3: {
497-
result = JSRopeString::create(m_vm, asString(m_strings.at(0)), asString(m_strings.at(1)), asString(m_strings.at(2)));
500+
result = JSRopeString::create(m_vm, m_strings[0], m_strings[1], m_strings[2]);
498501
break;
499502
}
500503
default:
501504
ASSERT_NOT_REACHED();
502505
break;
503506
}
504507
ASSERT(result->length() == m_length);
505-
m_strings.clear();
508+
m_index = 0;
506509
m_length = 0;
507510
return result;
508511
}
@@ -517,7 +520,8 @@ class JSRopeString final : public JSString {
517520
void expand();
518521

519522
VM& m_vm;
520-
MarkedArgumentBuffer m_strings;
523+
std::array<JSString*, JSRopeString::s_maxInternalRopeLength> m_strings { };
524+
unsigned m_index { 0 };
521525
unsigned m_length { 0 };
522526
};
523527

@@ -620,8 +624,6 @@ class JSRopeString final : public JSString {
620624
static constexpr ptrdiff_t offsetOfFiber2Lower() { return OBJECT_OFFSETOF(JSRopeString, m_compactFibers) + CompactFibers::offsetOfFiber2Lower(); }
621625
#endif
622626

623-
static constexpr unsigned s_maxInternalRopeLength = 3;
624-
625627
// If nullOrExecForOOM is null, resolveRope() will be do nothing in the event of an OOM error.
626628
// The rope value will remain a null string in that case.
627629
JS_EXPORT_PRIVATE const String& resolveRope(JSGlobalObject* nullOrGlobalObjectForOOM) const;

‎Source/JavaScriptCore/runtime/StringConstructor.cpp‎

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,17 @@
2121
#include "config.h"
2222
#include "StringConstructor.h"
2323

24+
#include "JSArrayInlines.h"
2425
#include "JSCInlines.h"
26+
#include "SparseArrayValueMap.h"
2527
#include "StringPrototype.h"
2628
#include <wtf/text/StringBuilder.h>
2729

2830
namespace JSC {
2931

3032
static JSC_DECLARE_HOST_FUNCTION(stringFromCharCode);
3133
static JSC_DECLARE_HOST_FUNCTION(stringFromCodePoint);
34+
static JSC_DECLARE_HOST_FUNCTION(stringRaw);
3235

3336
}
3437

@@ -42,7 +45,7 @@ const ClassInfo StringConstructor::s_info = { "Function"_s, &Base::s_info, &stri
4245
@begin stringConstructorTable
4346
fromCharCode stringFromCharCode DontEnum|Function 1 FromCharCodeIntrinsic
4447
fromCodePoint stringFromCodePoint DontEnum|Function 1 FromCodePointIntrinsic
45-
raw JSBuiltin DontEnum|Function 1
48+
raw stringRaw DontEnum|Function 1
4649
@end
4750
*/
4851

@@ -162,6 +165,123 @@ JSString* stringFromCodePoint(JSGlobalObject* globalObject, int32_t arg)
162165
return jsNontrivialString(vm, String({ buffer, 2 }));
163166
}
164167

168+
// https://tc39.es/ecma262/#sec-string.raw
169+
JSC_DEFINE_HOST_FUNCTION(stringRaw, (JSGlobalObject* globalObject, CallFrame* callFrame))
170+
{
171+
VM& vm = globalObject->vm();
172+
auto scope = DECLARE_THROW_SCOPE(vm);
173+
174+
JSValue templateValue = callFrame->argument(0);
175+
if (templateValue.isUndefinedOrNull()) [[unlikely]]
176+
return throwVMTypeError(globalObject, scope, "String.raw requires template not be null or undefined"_s);
177+
178+
JSObject* cooked = templateValue.toObject(globalObject);
179+
RETURN_IF_EXCEPTION(scope, { });
180+
181+
JSValue rawValue = cooked->getDirect(vm, vm.propertyNames->raw);
182+
if (!rawValue || rawValue.isGetterSetter() || rawValue.isCustomGetterSetter()) [[unlikely]] {
183+
rawValue = cooked->get(globalObject, vm.propertyNames->raw);
184+
RETURN_IF_EXCEPTION(scope, { });
185+
}
186+
if (rawValue.isUndefinedOrNull()) [[unlikely]]
187+
return throwVMTypeError(globalObject, scope, "String.raw requires template.raw not be null or undefined"_s);
188+
189+
JSObject* raw = rawValue.toObject(globalObject);
190+
RETURN_IF_EXCEPTION(scope, { });
191+
192+
uint64_t literalCount = toLength(globalObject, raw);
193+
RETURN_IF_EXCEPTION(scope, { });
194+
195+
if (!literalCount)
196+
return JSValue::encode(jsEmptyString(vm));
197+
198+
unsigned argumentCount = callFrame->argumentCount();
199+
unsigned substitutionCount = argumentCount ? argumentCount - 1 : 0;
200+
201+
JSArray* rawArray = isJSArray(raw) ? asArray(raw) : nullptr;
202+
JSRopeString::RopeBuilder<RecordOverflow> ropeBuilder(vm);
203+
for (uint64_t index = 0; ; ++index) {
204+
JSValue segment;
205+
if (rawArray) [[likely]] {
206+
Butterfly* butterfly = rawArray->butterfly();
207+
switch (rawArray->indexingType()) {
208+
case ALL_INT32_INDEXING_TYPES:
209+
if (index < butterfly->publicLength()) {
210+
JSValue value = butterfly->contiguous().at(rawArray, index).get();
211+
if (value) [[likely]]
212+
segment = jsString(vm, vm.numericStrings.add(value.asInt32()));
213+
}
214+
break;
215+
case ALL_DOUBLE_INDEXING_TYPES:
216+
if (index < butterfly->publicLength()) {
217+
double value = butterfly->contiguousDouble().at(rawArray, index);
218+
if (value == value) [[likely]]
219+
segment = jsString(vm, vm.numericStrings.add(value));
220+
}
221+
break;
222+
case ALL_CONTIGUOUS_INDEXING_TYPES:
223+
if (index < butterfly->publicLength())
224+
segment = butterfly->contiguous().at(rawArray, index).get();
225+
break;
226+
227+
case ALL_ARRAY_STORAGE_INDEXING_TYPES: [[likely]] {
228+
// This is the hot case because String.raw is used with tagged-templates,
229+
// and its template callsite object is a frozen array.
230+
ArrayStorage* storage = butterfly->arrayStorage();
231+
if (index < storage->length()) {
232+
JSValue value;
233+
if (index < storage->vectorLength()) {
234+
WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN
235+
value = storage->m_vector[index].get();
236+
WTF_ALLOW_UNSAFE_BUFFER_USAGE_END
237+
} else if (SparseArrayValueMap* map = storage->m_sparseMap.get()) {
238+
auto it = map->find(index);
239+
if (it != map->notFound()) {
240+
JSValue candidate = it->get();
241+
if (!candidate.isGetterSetter() && !candidate.isCustomGetterSetter()) [[likely]]
242+
value = candidate;
243+
}
244+
}
245+
segment = value;
246+
}
247+
break;
248+
}
249+
default:
250+
break;
251+
}
252+
}
253+
254+
if (!segment) [[unlikely]] {
255+
segment = raw->get(globalObject, index);
256+
RETURN_IF_EXCEPTION(scope, { });
257+
}
258+
259+
JSString* segmentString;
260+
if (segment.isString()) [[likely]]
261+
segmentString = asString(segment);
262+
else {
263+
segmentString = segment.toString(globalObject);
264+
RETURN_IF_EXCEPTION(scope, { });
265+
}
266+
267+
if (!ropeBuilder.append(segmentString)) [[unlikely]]
268+
return JSValue::encode(throwOutOfMemoryError(globalObject, scope));
269+
270+
if (index + 1 == literalCount)
271+
break;
272+
273+
if (index < substitutionCount) {
274+
JSString* substitution = callFrame->uncheckedArgument(index + 1).toString(globalObject);
275+
RETURN_IF_EXCEPTION(scope, { });
276+
277+
if (!ropeBuilder.append(substitution)) [[unlikely]]
278+
return JSValue::encode(throwOutOfMemoryError(globalObject, scope));
279+
}
280+
}
281+
282+
RELEASE_AND_RETURN(scope, JSValue::encode(ropeBuilder.release()));
283+
}
284+
165285
JSC_DEFINE_HOST_FUNCTION(constructWithStringConstructor, (JSGlobalObject* globalObject, CallFrame* callFrame))
166286
{
167287
VM& vm = globalObject->vm();

0 commit comments

Comments
 (0)