commit c03efb6f2ec6d68c6de2fd43394b3d65141381d0
parent f82c81855d62f18927cdf8ba05f78d4355f68724
Author: André Bargull <andre.bargull@gmail.com>
Date: Mon, 13 Oct 2025 12:55:26 +0000
Bug 1990248 - Part 6: Add a runtime fuse for default casing. r=spidermonkey-reviewers,jandem
Add runtime-wide fuse to guard that the default locale uses the default casing
algorithm. This will allow to inline `String.prototype.to{Lower,Upper}Case` in
the next part.
Differential Revision: https://phabricator.services.mozilla.com/D265828
Diffstat:
8 files changed, 127 insertions(+), 8 deletions(-)
diff --git a/js/src/builtin/String.cpp b/js/src/builtin/String.cpp
@@ -10,6 +10,7 @@
#include "mozilla/CheckedInt.h"
#include "mozilla/Compiler.h"
#if JS_HAS_INTL_API
+# include "mozilla/intl/Locale.h"
# include "mozilla/intl/String.h"
#endif
#include "mozilla/Likely.h"
@@ -944,17 +945,70 @@ static bool str_toLowerCase(JSContext* cx, unsigned argc, Value* vp) {
}
#if JS_HAS_INTL_API
+// Lithuanian, Turkish, and Azeri have language dependent case mappings.
+static constexpr char LanguagesWithSpecialCasing[][3] = {"lt", "tr", "az"};
+
+bool js::LocaleHasDefaultCaseMapping(const char* locale) {
+ MOZ_ASSERT(locale);
+
+ size_t languageSubtagLength;
+ if (auto* sep = strchr(locale, '-')) {
+ languageSubtagLength = sep - locale;
+ } else {
+ languageSubtagLength = std::strlen(locale);
+ }
+
+ // Invalid locale identifiers default to the last-ditch locale "en-GB", which
+ // has default case mapping.
+ mozilla::Span<const char> span{locale, languageSubtagLength};
+ {
+ // Tell the analysis the |IsStructurallyValidLanguageTag| function can't GC.
+ JS::AutoSuppressGCAnalysis nogc;
+ if (!mozilla::intl::IsStructurallyValidLanguageTag(span)) {
+ return true;
+ }
+ }
+
+ mozilla::intl::LanguageSubtag subtag{span};
+
+ // Canonical case for the language subtag is lower-case
+ {
+ // Tell the analysis the |ToLowerCase| function can't GC.
+ JS::AutoSuppressGCAnalysis nogc;
+
+ subtag.ToLowerCase();
+ }
+
+ // Replace outdated language subtags. Skips complex language mappings, which
+ // is okay because none of the languages with special casing are affected by
+ // complex language mapping.
+ {
+ // Tell the analysis the |LanguageMapping| function can't GC.
+ JS::AutoSuppressGCAnalysis nogc;
+
+ (void)mozilla::intl::Locale::LanguageMapping(subtag);
+ }
+
+ // Check for languages which don't use the default case mapping algorithm.
+ for (const auto& language : LanguagesWithSpecialCasing) {
+ if (subtag.EqualTo(language)) {
+ return false;
+ }
+ }
+
+ // Simple locale with default case mapping. (Or an invalid locale which
+ // defaults to the last-ditch locale "en-GB".)
+ return true;
+}
+
static const char* CaseMappingLocale(JSLinearString* locale) {
MOZ_ASSERT(locale->length() >= 2, "locale is a valid language tag");
- // Lithuanian, Turkish, and Azeri have language dependent case mappings.
- static const char languagesWithSpecialCasing[][3] = {"lt", "tr", "az"};
-
// All strings in |languagesWithSpecialCasing| are of length two, so we
// only need to compare the first two characters to find a matching locale.
// ES2017 Intl, §9.2.2 BestAvailableLocale
if (locale->length() == 2 || locale->latin1OrTwoByteChar(2) == '-') {
- for (const auto& language : languagesWithSpecialCasing) {
+ for (const auto& language : LanguagesWithSpecialCasing) {
if (locale->latin1OrTwoByteChar(0) == language[0] &&
locale->latin1OrTwoByteChar(1) == language[1]) {
return language;
diff --git a/js/src/builtin/String.h b/js/src/builtin/String.h
@@ -90,6 +90,10 @@ extern JSLinearString* StringFromCharCode(JSContext* cx, int32_t charCode);
extern JSLinearString* StringFromCodePoint(JSContext* cx, char32_t codePoint);
+#if JS_HAS_INTL_API
+bool LocaleHasDefaultCaseMapping(const char* locale);
+#endif
+
} /* namespace js */
#endif /* builtin_String_h */
diff --git a/js/src/jit/CompilationDependencyTracker.h b/js/src/jit/CompilationDependencyTracker.h
@@ -30,6 +30,7 @@ struct CompilationDependency : public TempObject {
EmulatesUndefined,
ArrayExceedsInt32Length,
ObjectFuseProperty,
+ DefaultCaseMapping,
Limit
};
diff --git a/js/src/jit/WarpOracle.cpp b/js/src/jit/WarpOracle.cpp
@@ -929,6 +929,12 @@ bool WarpOracle::addFuseDependency(RuntimeFuses::FuseIndex fuseIndex,
CompilationDependency::Type::ArrayExceedsInt32Length>;
return addIfStillValid(Dependency());
}
+ case RuntimeFuses::FuseIndex::DefaultLocaleHasDefaultCaseMappingFuse: {
+ using Dependency = RuntimeFuseDependency<
+ &RuntimeFuses::defaultLocaleHasDefaultCaseMappingFuse,
+ CompilationDependency::Type::DefaultCaseMapping>;
+ return addIfStillValid(Dependency());
+ }
case RuntimeFuses::FuseIndex::LastFuseIndex:
break;
}
diff --git a/js/src/vm/Runtime.cpp b/js/src/vm/Runtime.cpp
@@ -20,6 +20,7 @@
#include "jsfriendapi.h"
#include "jsmath.h"
+#include "builtin/String.h"
#include "frontend/CompilationStencil.h"
#include "frontend/ParserAtom.h" // frontend::WellKnownParserAtoms
#include "gc/GC.h"
@@ -514,6 +515,13 @@ bool JSRuntime::setDefaultLocale(const char* locale) {
return false;
}
+#if JS_HAS_INTL_API
+ if (!LocaleHasDefaultCaseMapping(newLocale.get())) {
+ runtimeFuses.ref().defaultLocaleHasDefaultCaseMappingFuse.popFuse(
+ mainContextFromOwnThread());
+ }
+#endif
+
defaultLocale.ref() = std::move(newLocale);
return true;
}
@@ -551,6 +559,13 @@ const char* JSRuntime::getDefaultLocale() {
*p = '-';
}
+#if JS_HAS_INTL_API
+ if (!LocaleHasDefaultCaseMapping(lang.get())) {
+ runtimeFuses.ref().defaultLocaleHasDefaultCaseMappingFuse.popFuse(
+ mainContextFromOwnThread());
+ }
+#endif
+
defaultLocale.ref() = std::move(lang);
return defaultLocale.ref().get();
}
diff --git a/js/src/vm/Runtime.h b/js/src/vm/Runtime.h
@@ -794,9 +794,17 @@ struct JSRuntime {
/* Reset the default locale to OS defaults. */
void resetDefaultLocale();
- /* Gets current default locale. String remains owned by context. */
+ /* Gets current default locale. String remains owned by runtime. */
const char* getDefaultLocale();
+ /*
+ * Gets current default locale or nullptr if not initialized.
+ * String remains owned by runtime.
+ */
+ const char* getDefaultLocaleIfInitialized() const {
+ return defaultLocale.ref().get();
+ }
+
/* Garbage collector state. */
js::gc::GCRuntime gc;
diff --git a/js/src/vm/RuntimeFuses.cpp b/js/src/vm/RuntimeFuses.cpp
@@ -9,6 +9,7 @@
#include <stddef.h>
#include <stdint.h>
+#include "builtin/String.h"
#include "js/friend/UsageStatistics.h"
#include "vm/GlobalObject.h"
#include "vm/JSContext.h"
@@ -60,3 +61,19 @@ void js::HasSeenObjectEmulateUndefinedFuse::popFuse(JSContext* cx) {
void js::HasSeenArrayExceedsInt32LengthFuse::popFuse(JSContext* cx) {
js::InvalidatingRuntimeFuse::popFuse(cx);
}
+
+bool js::DefaultLocaleHasDefaultCaseMappingFuse::checkInvariant(JSContext* cx) {
+#if JS_HAS_INTL_API
+ const char* locale = cx->runtime()->getDefaultLocaleIfInitialized();
+ if (!locale) {
+ return true;
+ }
+ return LocaleHasDefaultCaseMapping(locale);
+#else
+ return true;
+#endif
+}
+
+void js::DefaultLocaleHasDefaultCaseMappingFuse::popFuse(JSContext* cx) {
+ js::InvalidatingRuntimeFuse::popFuse(cx);
+}
diff --git a/js/src/vm/RuntimeFuses.h b/js/src/vm/RuntimeFuses.h
@@ -38,9 +38,23 @@ class HasSeenArrayExceedsInt32LengthFuse final
void popFuse(JSContext* cx) override;
};
-#define FOR_EACH_RUNTIME_FUSE(FUSE) \
- FUSE(HasSeenObjectEmulateUndefinedFuse, hasSeenObjectEmulateUndefinedFuse) \
- FUSE(HasSeenArrayExceedsInt32LengthFuse, hasSeenArrayExceedsInt32LengthFuse)
+class DefaultLocaleHasDefaultCaseMappingFuse final
+ : public InvalidatingRuntimeFuse {
+ const char* name() override {
+ return "DefaultLocaleHasDefaultCaseMappingFuse";
+ }
+
+ bool checkInvariant(JSContext* cx) override;
+
+ public:
+ void popFuse(JSContext* cx) override;
+};
+
+#define FOR_EACH_RUNTIME_FUSE(FUSE) \
+ FUSE(HasSeenObjectEmulateUndefinedFuse, hasSeenObjectEmulateUndefinedFuse) \
+ FUSE(HasSeenArrayExceedsInt32LengthFuse, hasSeenArrayExceedsInt32LengthFuse) \
+ FUSE(DefaultLocaleHasDefaultCaseMappingFuse, \
+ defaultLocaleHasDefaultCaseMappingFuse)
struct RuntimeFuses {
RuntimeFuses() = default;