commit fd6d791295d9458338f9d91c639a20fbda84a62a
parent c03efb6f2ec6d68c6de2fd43394b3d65141381d0
Author: André Bargull <andre.bargull@gmail.com>
Date: Mon, 13 Oct 2025 12:55:26 +0000
Bug 1990248 - Part 7: Add inlining support for locale-sensitive case conversion. r=spidermonkey-reviewers,jandem
Most locales use the default case mapping algorithm, so we can reuse the
existing inlining support.
Differential Revision: https://phabricator.services.mozilla.com/D265829
Diffstat:
11 files changed, 267 insertions(+), 5 deletions(-)
diff --git a/js/src/builtin/String.cpp b/js/src/builtin/String.cpp
@@ -3899,8 +3899,10 @@ static const JSFunctionSpec string_methods[] = {
JS_INLINABLE_FN("trim", str_trim, 0, 0, StringTrim),
JS_INLINABLE_FN("trimStart", str_trimStart, 0, 0, StringTrimStart),
JS_INLINABLE_FN("trimEnd", str_trimEnd, 0, 0, StringTrimEnd),
- JS_FN("toLocaleLowerCase", str_toLocaleLowerCase, 0, 0),
- JS_FN("toLocaleUpperCase", str_toLocaleUpperCase, 0, 0),
+ JS_INLINABLE_FN("toLocaleLowerCase", str_toLocaleLowerCase, 0, 0,
+ StringToLocaleLowerCase),
+ JS_INLINABLE_FN("toLocaleUpperCase", str_toLocaleUpperCase, 0, 0,
+ StringToLocaleUpperCase),
JS_FN("localeCompare", str_localeCompare, 1, 0),
JS_SELF_HOSTED_FN("repeat", "String_repeat", 1, 0),
#if JS_HAS_INTL_API
diff --git a/js/src/jit-test/tests/cacheir/string-toLocaleLowerCase-default-case-mapping-2.js b/js/src/jit-test/tests/cacheir/string-toLocaleLowerCase-default-case-mapping-2.js
@@ -0,0 +1,14 @@
+// |jit-test| skip-if: typeof Intl === 'undefined'
+
+function test() {
+ for (var i = 0; i <= 100; ++i) {
+ if (i === 100) {
+ setRealmLocale("tr-TR");
+ }
+ assertEq(
+ "TURKISH I".toLocaleLowerCase(),
+ i < 100 ? "turkish i" : "turk\u{131}sh \u{131}",
+ );
+ }
+}
+test();
diff --git a/js/src/jit-test/tests/cacheir/string-toLocaleLowerCase-default-case-mapping.js b/js/src/jit-test/tests/cacheir/string-toLocaleLowerCase-default-case-mapping.js
@@ -0,0 +1,58 @@
+// |jit-test| skip-if: typeof Intl === 'undefined'
+
+function testDefaultCaseMapping() {
+ for (var i = 0; i < 100; ++i) {
+ assertEq("TURKISH I".toLocaleLowerCase(), "turkish i");
+ }
+}
+
+function testTurkishCaseMapping() {
+ for (var i = 0; i < 100; ++i) {
+ assertEq("TURKISH I".toLocaleLowerCase(), "turk\u{131}sh \u{131}");
+ }
+}
+
+// JIT tests run with "en-US" by default. (Or "en-US-POSIX" for some Android tests.)
+assertEq(
+ getDefaultLocale() === "en-US" || getDefaultLocale() === "en-US-POSIX",
+ true
+);
+assertEq(getRealmLocale(), "en-US");
+
+// Ensure case mapping fuse is intact.
+assertEq(getFuseState().DefaultLocaleHasDefaultCaseMappingFuse.intact, true);
+
+// Run default case mapping test.
+testDefaultCaseMapping();
+
+// Change runtime default locale. (Use three letter language code to cover canonicalization.)
+setDefaultLocale("fra-FR");
+assertEq(getDefaultLocale(), "fra-FR");
+assertEq(getRealmLocale(), "fr-FR");
+
+// Ensure case mapping fuse is still intact.
+assertEq(getFuseState().DefaultLocaleHasDefaultCaseMappingFuse.intact, true);
+
+// Run default case mapping test again.
+testDefaultCaseMapping();
+
+// Change runtime default locale.
+setDefaultLocale("tur-TR");
+assertEq(getDefaultLocale(), "tur-TR");
+assertEq(getRealmLocale(), "tr-TR");
+
+// Case mapping fuse is no longer intact.
+assertEq(getFuseState().DefaultLocaleHasDefaultCaseMappingFuse.intact, false);
+
+testTurkishCaseMapping();
+
+// Reset default locale.
+setDefaultLocale("eng-US");
+assertEq(getDefaultLocale(), "eng-US");
+assertEq(getRealmLocale(), "en-US");
+
+// Fuse is still popped.
+assertEq(getFuseState().DefaultLocaleHasDefaultCaseMappingFuse.intact, false);
+
+// Run default case mapping test.
+testDefaultCaseMapping();
diff --git a/js/src/jit-test/tests/cacheir/string-toLocaleUpperCase-default-case-mapping-2.js b/js/src/jit-test/tests/cacheir/string-toLocaleUpperCase-default-case-mapping-2.js
@@ -0,0 +1,14 @@
+// |jit-test| skip-if: typeof Intl === 'undefined'
+
+function test() {
+ for (var i = 0; i <= 100; ++i) {
+ if (i === 100) {
+ setRealmLocale("tr-TR");
+ }
+ assertEq(
+ "turkish i".toLocaleUpperCase(),
+ i < 100 ? "TURKISH I" : "TURK\u{130}SH \u{130}",
+ );
+ }
+}
+test();
diff --git a/js/src/jit-test/tests/cacheir/string-toLocaleUpperCase-default-case-mapping.js b/js/src/jit-test/tests/cacheir/string-toLocaleUpperCase-default-case-mapping.js
@@ -0,0 +1,58 @@
+// |jit-test| skip-if: typeof Intl === 'undefined'
+
+function testDefaultCaseMapping() {
+ for (var i = 0; i < 100; ++i) {
+ assertEq("turkish i".toLocaleUpperCase(), "TURKISH I");
+ }
+}
+
+function testTurkishCaseMapping() {
+ for (var i = 0; i < 100; ++i) {
+ assertEq("turkish i".toLocaleUpperCase(), "TURK\u{130}SH \u{130}");
+ }
+}
+
+// JIT tests run with "en-US" by default. (Or "en-US-POSIX" for some Android tests.)
+assertEq(
+ getDefaultLocale() === "en-US" || getDefaultLocale() === "en-US-POSIX",
+ true
+);
+assertEq(getRealmLocale(), "en-US");
+
+// Ensure case mapping fuse is intact.
+assertEq(getFuseState().DefaultLocaleHasDefaultCaseMappingFuse.intact, true);
+
+// Run default case mapping test.
+testDefaultCaseMapping();
+
+// Change runtime default locale.
+setDefaultLocale("fr-FR");
+assertEq(getDefaultLocale(), "fr-FR");
+assertEq(getRealmLocale(), "fr-FR");
+
+// Ensure case mapping fuse is still intact.
+assertEq(getFuseState().DefaultLocaleHasDefaultCaseMappingFuse.intact, true);
+
+// Run default case mapping test again.
+testDefaultCaseMapping();
+
+// Change runtime default locale.
+setDefaultLocale("tr-TR");
+assertEq(getDefaultLocale(), "tr-TR");
+assertEq(getRealmLocale(), "tr-TR");
+
+// Case mapping fuse is no longer intact.
+assertEq(getFuseState().DefaultLocaleHasDefaultCaseMappingFuse.intact, false);
+
+testTurkishCaseMapping();
+
+// Reset default locale.
+setDefaultLocale("en-US");
+assertEq(getDefaultLocale(), "en-US");
+assertEq(getRealmLocale(), "en-US");
+
+// Fuse is still popped.
+assertEq(getFuseState().DefaultLocaleHasDefaultCaseMappingFuse.intact, false);
+
+// Run default case mapping test.
+testDefaultCaseMapping();
diff --git a/js/src/jit/CacheIR.cpp b/js/src/jit/CacheIR.cpp
@@ -8823,6 +8823,106 @@ AttachDecision InlinableNativeIRGenerator::tryAttachStringToUpperCase() {
return AttachDecision::Attach;
}
+AttachDecision InlinableNativeIRGenerator::tryAttachStringToLocaleLowerCase() {
+#if JS_HAS_INTL_API
+ // Expecting no arguments.
+ if (args_.length() != 0) {
+ return AttachDecision::NoAction;
+ }
+
+ // Ensure |this| is a primitive string value.
+ if (!thisval_.isString()) {
+ return AttachDecision::NoAction;
+ }
+
+ // Don't inline when not using the default locale.
+ if (cx_->realm()->behaviors().localeOverride()) {
+ return AttachDecision::NoAction;
+ }
+
+ // Default case mapping fuse must still be intact.
+ if (!cx_->runtime()
+ ->runtimeFuses.ref()
+ .defaultLocaleHasDefaultCaseMappingFuse.intact()) {
+ return AttachDecision::NoAction;
+ }
+
+ // Initialize the input operand.
+ Int32OperandId argcId = initializeInputOperand();
+
+ // Guard callee is the 'toLocaleLowerCase' native function.
+ ObjOperandId calleeId = emitNativeCalleeGuard(argcId);
+
+ // Guard this is a string.
+ ValOperandId thisValId = loadThis(calleeId);
+ StringOperandId strId = writer.guardToString(thisValId);
+
+ // Guard runtime default locale uses default case mapping.
+ writer.guardRuntimeFuse(
+ RuntimeFuses::FuseIndex::DefaultLocaleHasDefaultCaseMappingFuse);
+
+ // Return string converted to lower-case.
+ writer.stringToLowerCaseResult(strId);
+ writer.returnFromIC();
+
+ trackAttached("StringToLocaleLowerCase");
+ return AttachDecision::Attach;
+#else
+ // No inlining when Intl support is disabled.
+ return AttachDecision::Attach;
+#endif
+}
+
+AttachDecision InlinableNativeIRGenerator::tryAttachStringToLocaleUpperCase() {
+#if JS_HAS_INTL_API
+ // Expecting no arguments.
+ if (args_.length() != 0) {
+ return AttachDecision::NoAction;
+ }
+
+ // Ensure |this| is a primitive string value.
+ if (!thisval_.isString()) {
+ return AttachDecision::NoAction;
+ }
+
+ // Don't inline when not using the default locale.
+ if (cx_->realm()->behaviors().localeOverride()) {
+ return AttachDecision::NoAction;
+ }
+
+ // Default case mapping fuse must still be intact.
+ if (!cx_->runtime()
+ ->runtimeFuses.ref()
+ .defaultLocaleHasDefaultCaseMappingFuse.intact()) {
+ return AttachDecision::NoAction;
+ }
+
+ // Initialize the input operand.
+ Int32OperandId argcId = initializeInputOperand();
+
+ // Guard callee is the 'toLocaleUpperCase' native function.
+ ObjOperandId calleeId = emitNativeCalleeGuard(argcId);
+
+ // Guard this is a string.
+ ValOperandId thisValId = loadThis(calleeId);
+ StringOperandId strId = writer.guardToString(thisValId);
+
+ // Guard runtime default locale uses default case mapping.
+ writer.guardRuntimeFuse(
+ RuntimeFuses::FuseIndex::DefaultLocaleHasDefaultCaseMappingFuse);
+
+ // Return string converted to upper-case.
+ writer.stringToUpperCaseResult(strId);
+ writer.returnFromIC();
+
+ trackAttached("StringToLocaleUpperCase");
+ return AttachDecision::Attach;
+#else
+ // No inlining when Intl support is disabled.
+ return AttachDecision::Attach;
+#endif
+}
+
AttachDecision InlinableNativeIRGenerator::tryAttachStringTrim() {
// Expecting no arguments.
if (args_.length() != 0) {
@@ -13126,6 +13226,10 @@ AttachDecision InlinableNativeIRGenerator::tryAttachStub() {
return tryAttachStringToLowerCase();
case InlinableNative::StringToUpperCase:
return tryAttachStringToUpperCase();
+ case InlinableNative::StringToLocaleLowerCase:
+ return tryAttachStringToLocaleLowerCase();
+ case InlinableNative::StringToLocaleUpperCase:
+ return tryAttachStringToLocaleUpperCase();
case InlinableNative::StringTrim:
return tryAttachStringTrim();
case InlinableNative::StringTrimStart:
diff --git a/js/src/jit/CacheIRGenerator.h b/js/src/jit/CacheIRGenerator.h
@@ -786,6 +786,8 @@ class MOZ_RAII InlinableNativeIRGenerator {
AttachDecision tryAttachStringEndsWith();
AttachDecision tryAttachStringToLowerCase();
AttachDecision tryAttachStringToUpperCase();
+ AttachDecision tryAttachStringToLocaleLowerCase();
+ AttachDecision tryAttachStringToLocaleUpperCase();
AttachDecision tryAttachStringTrim();
AttachDecision tryAttachStringTrimStart();
AttachDecision tryAttachStringTrimEnd();
diff --git a/js/src/jit/InlinableNatives.cpp b/js/src/jit/InlinableNatives.cpp
@@ -342,6 +342,8 @@ bool js::jit::CanInlineNativeCrossRealm(InlinableNative native) {
case InlinableNative::StringEndsWith:
case InlinableNative::StringToLowerCase:
case InlinableNative::StringToUpperCase:
+ case InlinableNative::StringToLocaleLowerCase:
+ case InlinableNative::StringToLocaleUpperCase:
case InlinableNative::StringTrim:
case InlinableNative::StringTrimStart:
case InlinableNative::StringTrimEnd:
diff --git a/js/src/jit/InlinableNatives.h b/js/src/jit/InlinableNatives.h
@@ -184,6 +184,8 @@
_(StringEndsWith) \
_(StringToLowerCase) \
_(StringToUpperCase) \
+ _(StringToLocaleLowerCase) \
+ _(StringToLocaleUpperCase) \
_(StringTrim) \
_(StringTrimStart) \
_(StringTrimEnd) \
diff --git a/js/src/vm/Realm.cpp b/js/src/vm/Realm.cpp
@@ -536,6 +536,14 @@ const char* Realm::getLocale() const {
return runtime_->getDefaultLocale();
}
+void Realm::setLocaleOverride(const char* locale) {
+ // Clear any jitcode in the runtime, because compiled code doesn't handle
+ // updates to a realm's locale override.
+ ReleaseAllJITCode(runtime_->gcContext());
+
+ behaviors_.setLocaleOverride(locale);
+}
+
js::DateTimeInfo* Realm::getDateTimeInfo() {
#if JS_HAS_INTL_API
if (RefPtr<TimeZoneString> timeZone = behaviors_.timeZoneOverride()) {
diff --git a/js/src/vm/Realm.h b/js/src/vm/Realm.h
@@ -807,9 +807,7 @@ class JS::Realm : public JS::shadow::Realm {
// Set the locale for this realm. Reset to the system default locale when the
// input is |nullptr|.
- void setLocaleOverride(const char* locale) {
- behaviors_.setLocaleOverride(locale);
- }
+ void setLocaleOverride(const char* locale);
// Returns the date-time info for this realm. Returns nullptr unless a time
// zone override was specified in the realm creation options.