diff options
Diffstat (limited to 'js')
396 files changed, 13147 insertions, 2771 deletions
diff --git a/js/loader/LoadContextBase.h b/js/loader/LoadContextBase.h index a8ce045546..5dc78070cb 100644 --- a/js/loader/LoadContextBase.h +++ b/js/loader/LoadContextBase.h @@ -52,6 +52,9 @@ class LoadContextBase : public nsISupports { // Used to output a string for the Gecko Profiler. virtual void GetProfilerLabel(nsACString& aOutString); + // Whether this is a preload, for contexts that support them. + virtual bool IsPreload() const { return false; } + // Casting to the different contexts bool IsWindowContext() const { return mKind == ContextKind::Window; } mozilla::dom::ScriptLoadContext* AsWindowContext(); diff --git a/js/loader/LoadedScript.cpp b/js/loader/LoadedScript.cpp index 7ddd04168e..c84ed3ac55 100644 --- a/js/loader/LoadedScript.cpp +++ b/js/loader/LoadedScript.cpp @@ -20,6 +20,8 @@ namespace JS::loader { // LoadedScript ////////////////////////////////////////////////////////////// +MOZ_DEFINE_MALLOC_SIZE_OF(LoadedScriptMallocSizeOf) + NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LoadedScript) NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END @@ -52,7 +54,59 @@ LoadedScript::LoadedScript(ScriptKind aKind, MOZ_ASSERT(mURI); } -LoadedScript::~LoadedScript() { mozilla::DropJSObjects(this); } +LoadedScript::~LoadedScript() { + mozilla::UnregisterWeakMemoryReporter(this); + mozilla::DropJSObjects(this); +} + +void LoadedScript::RegisterMemoryReport() { + mozilla::RegisterWeakMemoryReporter(this); +} + +NS_IMETHODIMP +LoadedScript::CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aAnonymize) { +#define COLLECT_REPORT(path, kind) \ + MOZ_COLLECT_REPORT(path, KIND_HEAP, UNITS_BYTES, \ + SizeOfIncludingThis(LoadedScriptMallocSizeOf), \ + "Memory used for LoadedScript to hold on " kind \ + " across documents") + + switch (mKind) { + case ScriptKind::eClassic: + COLLECT_REPORT("explicit/js/script/loaded-script/classic", "scripts"); + break; + case ScriptKind::eImportMap: + COLLECT_REPORT("explicit/js/script/loaded-script/import-map", + "import-maps"); + break; + case ScriptKind::eModule: + COLLECT_REPORT("explicit/js/script/loaded-script/module", "modules"); + break; + case ScriptKind::eEvent: + COLLECT_REPORT("explicit/js/script/loaded-script/event", "event scripts"); + break; + } + +#undef COLLECT_REPORT + return NS_OK; +} + +size_t LoadedScript::SizeOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const { + size_t bytes = aMallocSizeOf(this); + + if (IsTextSource()) { + if (IsUTF16Text()) { + bytes += ScriptText<char16_t>().sizeOfExcludingThis(aMallocSizeOf); + } else { + bytes += ScriptText<Utf8Unit>().sizeOfExcludingThis(aMallocSizeOf); + } + } + + bytes += mScriptBytecode.sizeOfExcludingThis(aMallocSizeOf); + return bytes; +} void LoadedScript::AssociateWithScript(JSScript* aScript) { // Verify that the rewritten URL is available when manipulating LoadedScript. @@ -207,6 +261,7 @@ NS_IMPL_CYCLE_COLLECTION_TRACE_END ModuleScript::ModuleScript(mozilla::dom::ReferrerPolicy aReferrerPolicy, ScriptFetchOptions* aFetchOptions, nsIURI* aURI) : LoadedScript(ScriptKind::eModule, aReferrerPolicy, aFetchOptions, aURI), + mHadImportMap(false), mDebuggerDataInitialized(false) { MOZ_ASSERT(!ModuleRecord()); MOZ_ASSERT(!HasParseError()); @@ -279,6 +334,9 @@ void ModuleScript::SetErrorToRethrow(const JS::Value& aError) { mErrorToRethrow = aError; } +void ModuleScript::SetForPreload(bool aValue) { mForPreload = aValue; } +void ModuleScript::SetHadImportMap(bool aValue) { mHadImportMap = aValue; } + void ModuleScript::SetDebuggerDataInitialized() { MOZ_ASSERT(ModuleRecord()); MOZ_ASSERT(!mDebuggerDataInitialized); diff --git a/js/loader/LoadedScript.h b/js/loader/LoadedScript.h index ca6d1fc179..5759800c27 100644 --- a/js/loader/LoadedScript.h +++ b/js/loader/LoadedScript.h @@ -12,12 +12,14 @@ #include "mozilla/Maybe.h" #include "mozilla/MaybeOneOf.h" +#include "mozilla/MemoryReporting.h" #include "mozilla/Utf8.h" // mozilla::Utf8Unit #include "mozilla/Variant.h" #include "mozilla/Vector.h" #include "nsCOMPtr.h" #include "nsCycleCollectionParticipant.h" +#include "nsIMemoryReporter.h" #include "jsapi.h" #include "ScriptKind.h" @@ -39,7 +41,17 @@ class ModuleScript; class EventScript; class LoadContextBase; -class LoadedScript : public nsISupports { +// A LoadedScript is a place where the Script is stored once it is loaded. It is +// not unique to a load, and can be shared across loads as long as it is +// properly ref-counted by each load instance. +// +// When the load is not performed, the URI represents the resource to be loaded, +// and it is replaced by the absolute resource location once loaded. +// +// As the LoadedScript can be shared, using the SharedSubResourceCache, it is +// exposed to the memory reporter such that sharing might be accounted for +// properly. +class LoadedScript : public nsIMemoryReporter { ScriptKind mKind; const mozilla::dom::ReferrerPolicy mReferrerPolicy; RefPtr<ScriptFetchOptions> mFetchOptions; @@ -53,7 +65,17 @@ class LoadedScript : public nsISupports { virtual ~LoadedScript(); public: - NS_DECL_CYCLE_COLLECTING_ISUPPORTS + // When the memory should be reported, register it using RegisterMemoryReport, + // and make sure to call SizeOfIncludingThis in the enclosing container. + // + // Each reported script would be listed under + // `explicit/js/script/loaded-script/<kind>`. + void RegisterMemoryReport(); + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS; + NS_DECL_NSIMEMORYREPORTER; NS_DECL_CYCLE_COLLECTION_CLASS(LoadedScript) bool IsClassicScript() const { return mKind == ScriptKind::eClassic; } @@ -332,6 +354,8 @@ class ModuleScript final : public LoadedScript { JS::Heap<JSObject*> mModuleRecord; JS::Heap<JS::Value> mParseError; JS::Heap<JS::Value> mErrorToRethrow; + bool mForPreload; + bool mHadImportMap; bool mDebuggerDataInitialized; ~ModuleScript(); @@ -352,6 +376,8 @@ class ModuleScript final : public LoadedScript { void SetModuleRecord(JS::Handle<JSObject*> aModuleRecord); void SetParseError(const JS::Value& aError); void SetErrorToRethrow(const JS::Value& aError); + void SetForPreload(bool aValue); + void SetHadImportMap(bool aValue); void SetDebuggerDataInitialized(); JSObject* ModuleRecord() const { return mModuleRecord; } @@ -360,6 +386,8 @@ class ModuleScript final : public LoadedScript { JS::Value ErrorToRethrow() const { return mErrorToRethrow; } bool HasParseError() const { return !mParseError.isUndefined(); } bool HasErrorToRethrow() const { return !mErrorToRethrow.isUndefined(); } + bool ForPreload() const { return mForPreload; } + bool HadImportMap() const { return mHadImportMap; } bool DebuggerDataInitialized() const { return mDebuggerDataInitialized; } void Shutdown(); diff --git a/js/loader/ModuleLoadRequest.cpp b/js/loader/ModuleLoadRequest.cpp index 7e188160fc..7313563df9 100644 --- a/js/loader/ModuleLoadRequest.cpp +++ b/js/loader/ModuleLoadRequest.cpp @@ -205,6 +205,7 @@ void ModuleLoadRequest::CheckModuleDependenciesLoaded() { if (!mModuleScript || mModuleScript->HasParseError()) { return; } + for (const auto& childRequest : mImports) { ModuleScript* childScript = childRequest->mModuleScript; if (!childScript) { @@ -213,6 +214,9 @@ void ModuleLoadRequest::CheckModuleDependenciesLoaded() { childRequest.get())); return; } + + MOZ_DIAGNOSTIC_ASSERT(mModuleScript->HadImportMap() == + childScript->HadImportMap()); } LOG(("ScriptLoadRequest (%p): all ok", this)); diff --git a/js/loader/ModuleLoaderBase.cpp b/js/loader/ModuleLoaderBase.cpp index 228c96ad69..020542754d 100644 --- a/js/loader/ModuleLoaderBase.cpp +++ b/js/loader/ModuleLoaderBase.cpp @@ -55,17 +55,17 @@ mozilla::LazyLogModule ModuleLoaderBase::gModuleLoaderBaseLog( MOZ_LOG_TEST(ModuleLoaderBase::gModuleLoaderBaseLog, mozilla::LogLevel::Debug) ////////////////////////////////////////////////////////////// -// ModuleLoaderBase::WaitingRequests +// ModuleLoaderBase::LoadingRequest ////////////////////////////////////////////////////////////// -NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ModuleLoaderBase::WaitingRequests) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ModuleLoaderBase::LoadingRequest) NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END -NS_IMPL_CYCLE_COLLECTION(ModuleLoaderBase::WaitingRequests, mWaiting) +NS_IMPL_CYCLE_COLLECTION(ModuleLoaderBase::LoadingRequest, mRequest, mWaiting) -NS_IMPL_CYCLE_COLLECTING_ADDREF(ModuleLoaderBase::WaitingRequests) -NS_IMPL_CYCLE_COLLECTING_RELEASE(ModuleLoaderBase::WaitingRequests) +NS_IMPL_CYCLE_COLLECTING_ADDREF(ModuleLoaderBase::LoadingRequest) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ModuleLoaderBase::LoadingRequest) ////////////////////////////////////////////////////////////// // ModuleLoaderBase @@ -308,6 +308,12 @@ bool ModuleLoaderBase::HostImportModuleDynamically( return false; } + if (!loader->IsDynamicImportSupported()) { + JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr, + JSMSG_DYNAMIC_IMPORT_NOT_SUPPORTED); + return false; + } + auto result = loader->ResolveModuleSpecifier(script, specifier); if (result.isErr()) { JS::Rooted<JS::Value> error(aCx); @@ -509,7 +515,9 @@ void ModuleLoaderBase::SetModuleFetchStarted(ModuleLoadRequest* aRequest) { MOZ_ASSERT(aRequest->IsFetching() || aRequest->IsPendingFetchingError()); MOZ_ASSERT(!ModuleMapContainsURL(aRequest->mURI)); - mFetchingModules.InsertOrUpdate(aRequest->mURI, nullptr); + RefPtr<LoadingRequest> loadingRequest = new LoadingRequest(); + loadingRequest->mRequest = aRequest; + mFetchingModules.InsertOrUpdate(aRequest->mURI, loadingRequest); } void ModuleLoaderBase::SetModuleFetchFinishedAndResumeWaitingRequests( @@ -526,9 +534,8 @@ void ModuleLoaderBase::SetModuleFetchFinishedAndResumeWaitingRequests( "%u)", aRequest, aRequest->mModuleScript.get(), unsigned(aResult))); - RefPtr<WaitingRequests> waitingRequests; - if (!mFetchingModules.Remove(aRequest->mURI, - getter_AddRefs(waitingRequests))) { + auto entry = mFetchingModules.Lookup(aRequest->mURI); + if (!entry) { LOG( ("ScriptLoadRequest (%p): Key not found in mFetchingModules, " "assuming we have an inline module or have finished fetching already", @@ -536,20 +543,35 @@ void ModuleLoaderBase::SetModuleFetchFinishedAndResumeWaitingRequests( return; } + // It's possible for a request to be cancelled and removed from the fetching + // modules map and a new request started for the same URI and added to the + // map. In this case we don't want the first cancelled request to complete the + // later request (which will cause it to fail) so we ignore it. + RefPtr<LoadingRequest> loadingRequest = entry.Data(); + if (loadingRequest->mRequest != aRequest) { + MOZ_ASSERT(aRequest->IsCanceled()); + LOG( + ("ScriptLoadRequest (%p): Ignoring completion of cancelled request " + "that was removed from the map", + aRequest)); + return; + } + + MOZ_ALWAYS_TRUE(mFetchingModules.Remove(aRequest->mURI)); + RefPtr<ModuleScript> moduleScript(aRequest->mModuleScript); MOZ_ASSERT(NS_FAILED(aResult) == !moduleScript); mFetchedModules.InsertOrUpdate(aRequest->mURI, RefPtr{moduleScript}); - if (waitingRequests) { - LOG(("ScriptLoadRequest (%p): Resuming waiting requests", aRequest)); - ResumeWaitingRequests(waitingRequests, bool(moduleScript)); - } + LOG(("ScriptLoadRequest (%p): Resuming waiting requests", aRequest)); + MOZ_ASSERT(loadingRequest->mRequest == aRequest); + ResumeWaitingRequests(loadingRequest, bool(moduleScript)); } -void ModuleLoaderBase::ResumeWaitingRequests(WaitingRequests* aWaitingRequests, +void ModuleLoaderBase::ResumeWaitingRequests(LoadingRequest* aLoadingRequest, bool aSuccess) { - for (ModuleLoadRequest* request : aWaitingRequests->mWaiting) { + for (ModuleLoadRequest* request : aLoadingRequest->mWaiting) { ResumeWaitingRequest(request, aSuccess); } } @@ -568,13 +590,8 @@ void ModuleLoaderBase::WaitForModuleFetch(ModuleLoadRequest* aRequest) { MOZ_ASSERT(ModuleMapContainsURL(uri)); if (auto entry = mFetchingModules.Lookup(uri)) { - RefPtr<WaitingRequests> waitingRequests = entry.Data(); - if (!waitingRequests) { - waitingRequests = new WaitingRequests(); - mFetchingModules.InsertOrUpdate(uri, waitingRequests); - } - - waitingRequests->mWaiting.AppendElement(aRequest); + RefPtr<LoadingRequest> loadingRequest = entry.Data(); + loadingRequest->mWaiting.AppendElement(aRequest); return; } @@ -678,6 +695,9 @@ nsresult ModuleLoaderBase::CreateModuleScript(ModuleLoadRequest* aRequest) { aRequest->mLoadedScript->AsModuleScript(); aRequest->mModuleScript = moduleScript; + moduleScript->SetForPreload(aRequest->mLoadContext->IsPreload()); + moduleScript->SetHadImportMap(HasImportMapRegistered()); + if (!module) { LOG(("ScriptLoadRequest (%p): compilation failed (%d)", aRequest, unsigned(rv))); @@ -769,6 +789,7 @@ ResolveResult ModuleLoaderBase::ResolveModuleSpecifier( // Import Maps are not supported on workers/worklets. // See https://github.com/WICG/import-maps/issues/2 MOZ_ASSERT_IF(!NS_IsMainThread(), mImportMap == nullptr); + // Forward to the updated 'Resolve a module specifier' algorithm defined in // the Import Maps spec. return ImportMap::ResolveModuleSpecifier(mImportMap.get(), mLoader, aScript, @@ -1034,9 +1055,9 @@ void ModuleLoaderBase::Shutdown() { CancelAndClearDynamicImports(); for (const auto& entry : mFetchingModules) { - RefPtr<WaitingRequests> waitingRequests(entry.GetData()); - if (waitingRequests) { - ResumeWaitingRequests(waitingRequests, false); + RefPtr<LoadingRequest> loadingRequest(entry.GetData()); + if (loadingRequest) { + ResumeWaitingRequests(loadingRequest, false); } } @@ -1100,6 +1121,9 @@ JS::Value ModuleLoaderBase::FindFirstParseError(ModuleLoadRequest* aRequest) { } for (ModuleLoadRequest* childRequest : aRequest->mImports) { + MOZ_DIAGNOSTIC_ASSERT(moduleScript->HadImportMap() == + childRequest->mModuleScript->HadImportMap()); + JS::Value error = FindFirstParseError(childRequest); if (!error.isUndefined()) { return error; @@ -1393,6 +1417,41 @@ void ModuleLoaderBase::RegisterImportMap(UniquePtr<ImportMap> aImportMap) { // Step 3. Set global's import map to result's import map. mImportMap = std::move(aImportMap); + + // Any import resolution has been invalidated by the addition of the import + // map. If speculative preloading is currently fetching any modules then + // cancel their requests and remove them from the map. + // + // The cancelled requests will still complete later so we have to check this + // in SetModuleFetchFinishedAndResumeWaitingRequests. + for (const auto& entry : mFetchingModules) { + LoadingRequest* loadingRequest = entry.GetData(); + MOZ_DIAGNOSTIC_ASSERT(loadingRequest->mRequest->mLoadContext->IsPreload()); + loadingRequest->mRequest->Cancel(); + for (const auto& request : loadingRequest->mWaiting) { + MOZ_DIAGNOSTIC_ASSERT(request->mLoadContext->IsPreload()); + request->Cancel(); + } + } + mFetchingModules.Clear(); + + // If speculative preloading has added modules to the module map, remove + // them. + for (const auto& entry : mFetchedModules) { + ModuleScript* script = entry.GetData(); + if (script) { + MOZ_DIAGNOSTIC_ASSERT( + script->ForPreload(), + "Non-preload module loads should block import maps"); + MOZ_DIAGNOSTIC_ASSERT(!script->HadImportMap(), + "Only one import map can be registered"); + if (JSObject* module = script->ModuleRecord()) { + MOZ_DIAGNOSTIC_ASSERT(!JS::ModuleIsLinked(module)); + } + script->Shutdown(); + } + } + mFetchedModules.Clear(); } void ModuleLoaderBase::CopyModulesTo(ModuleLoaderBase* aDest) { diff --git a/js/loader/ModuleLoaderBase.h b/js/loader/ModuleLoaderBase.h index 2c2c385a30..89d23e12bc 100644 --- a/js/loader/ModuleLoaderBase.h +++ b/js/loader/ModuleLoaderBase.h @@ -165,20 +165,30 @@ class ScriptLoaderInterface : public nsISupports { */ class ModuleLoaderBase : public nsISupports { /* - * The set of requests that are waiting for an ongoing fetch to complete. + * Represents an ongoing load operation for a URI initiated for one request + * and which may have other requests waiting for it to complete. + * + * These are tracked in the mFetchingModules map. */ - class WaitingRequests final : public nsISupports { - virtual ~WaitingRequests() = default; + class LoadingRequest final : public nsISupports { + virtual ~LoadingRequest() = default; public: NS_DECL_CYCLE_COLLECTING_ISUPPORTS - NS_DECL_CYCLE_COLLECTION_CLASS(WaitingRequests) + NS_DECL_CYCLE_COLLECTION_CLASS(LoadingRequest) + + // The request that initiated the load and which is currently fetching or + // being compiled. + RefPtr<ModuleLoadRequest> mRequest; + // A list of any other requests for the same URI that are waiting for the + // initial load to complete. These will be resumed by ResumeWaitingRequests + // when that happens. nsTArray<RefPtr<ModuleLoadRequest>> mWaiting; }; // Module map - nsRefPtrHashtable<nsURIHashKey, WaitingRequests> mFetchingModules; + nsRefPtrHashtable<nsURIHashKey, LoadingRequest> mFetchingModules; nsRefPtrHashtable<nsURIHashKey, ModuleScript> mFetchedModules; // List of dynamic imports that are currently being loaded. @@ -241,6 +251,8 @@ class ModuleLoaderBase : public nsISupports { JSContext* aCx, nsIURI* aURI, LoadedScript* aMaybeActiveScript, JS::Handle<JSString*> aSpecifier, JS::Handle<JSObject*> aPromise) = 0; + virtual bool IsDynamicImportSupported() { return true; } + // Called when dynamic import started successfully. virtual void OnDynamicImportStarted(ModuleLoadRequest* aRequest) {} @@ -333,10 +345,6 @@ class ModuleLoaderBase : public nsISupports { nsresult GetFetchedModuleURLs(nsTArray<nsCString>& aURLs); - // Removed a fetched module from the module map. Asserts that the module is - // unlinked. Extreme care should be taken when calling this method. - bool RemoveFetchedModule(nsIURI* aURL); - // Override the module loader with given loader until ResetOverride is called. // While overridden, ModuleLoaderBase::GetCurrentModuleLoader returns aLoader. // @@ -421,7 +429,7 @@ class ModuleLoaderBase : public nsISupports { void SetModuleFetchFinishedAndResumeWaitingRequests( ModuleLoadRequest* aRequest, nsresult aResult); - void ResumeWaitingRequests(WaitingRequests* aWaitingRequests, bool aSuccess); + void ResumeWaitingRequests(LoadingRequest* aLoadingRequest, bool aSuccess); void ResumeWaitingRequest(ModuleLoadRequest* aRequest, bool aSuccess); void StartFetchingModuleDependencies(ModuleLoadRequest* aRequest); diff --git a/js/moz.configure b/js/moz.configure index cbcaf38f01..675736a797 100644 --- a/js/moz.configure +++ b/js/moz.configure @@ -173,18 +173,12 @@ def enable_decorators(value): set_config("ENABLE_DECORATORS", enable_decorators) set_define("ENABLE_DECORATORS", enable_decorators) + # Enable JSON.parse with source # =================================================== -option( - "--enable-json-parse-with-source", - default=False, - help="Enable experimental JS JSON.parse with source support", -) - - -@depends("--enable-json-parse-with-source") -def enable_json_parse_with_source(value): - if value: +@depends(milestone.is_nightly) +def enable_json_parse_with_source(is_nightly): + if is_nightly: return True @@ -597,13 +591,14 @@ set_define("MOZ_AARCH64_JSCVT", aarch64_jscvt) @depends(target) -def has_pthread_jit_write_protect_np(target): - return target.os == "OSX" and target.cpu == "aarch64" +def has_apple_fast_wx(target): + return target.kernel == "Darwin" and target.cpu == "aarch64" -# On Apple Silicon we use MAP_JIT with pthread_jit_write_protect_np to implement -# JIT code write protection. -set_define("JS_USE_APPLE_FAST_WX", True, when=has_pthread_jit_write_protect_np) +# On Apple Silicon macOS we use MAP_JIT with pthread_jit_write_protect_np to +# implement JIT code write protection, while on iOS we use MAP_JIT with +# be_memory_inline_jit_restrict_*. +set_define("JS_USE_APPLE_FAST_WX", True, when=has_apple_fast_wx) # CTypes diff --git a/js/public/CallArgs.h b/js/public/CallArgs.h index 16f8d6f904..8be1b382f2 100644 --- a/js/public/CallArgs.h +++ b/js/public/CallArgs.h @@ -112,7 +112,7 @@ class MOZ_STACK_CLASS NoUsedRval { }; template <class WantUsedRval> -class MOZ_STACK_CLASS CallArgsBase { +class MOZ_STACK_CLASS MOZ_NON_PARAM CallArgsBase { static_assert(std::is_same_v<WantUsedRval, IncludeUsedRval> || std::is_same_v<WantUsedRval, NoUsedRval>, "WantUsedRval can only be IncludeUsedRval or NoUsedRval"); @@ -294,7 +294,7 @@ class MOZ_STACK_CLASS CallArgsBase { } // namespace detail -class MOZ_STACK_CLASS CallArgs +class MOZ_STACK_CLASS MOZ_NON_PARAM CallArgs : public detail::CallArgsBase<detail::IncludeUsedRval> { private: friend CallArgs CallArgsFromVp(unsigned argc, Value* vp); @@ -315,6 +315,9 @@ class MOZ_STACK_CLASS CallArgs for (unsigned i = 0; i < argc; ++i) { AssertValueIsNotGray(argv[i]); } + if (constructing) { + AssertValueIsNotGray(args.newTarget()); + } #endif return args; } diff --git a/js/public/CallNonGenericMethod.h b/js/public/CallNonGenericMethod.h index 80359f7a6f..73613260b9 100644 --- a/js/public/CallNonGenericMethod.h +++ b/js/public/CallNonGenericMethod.h @@ -60,7 +60,7 @@ extern JS_PUBLIC_API bool CallMethodIfWrapped(JSContext* cx, // its interface is the same as that of JSNative. // // static bool -// answer_getAnswer_impl(JSContext* cx, JS::CallArgs args) +// answer_getAnswer_impl(JSContext* cx, const JS::CallArgs& args) // { // args.rval().setInt32(42); // return true; diff --git a/js/public/GCAPI.h b/js/public/GCAPI.h index 9bdaca9661..5773838e46 100644 --- a/js/public/GCAPI.h +++ b/js/public/GCAPI.h @@ -459,6 +459,15 @@ typedef enum JSGCParamKey { * Default: ParallelMarkingThresholdMB */ JSGC_PARALLEL_MARKING_THRESHOLD_MB = 50, + + /** + * Whether the semispace nursery is enabled. + * + * Pref: javascript.options.mem.gc_experimental_semispace_nursery + * Default: SemispaceNurseryEnabled + */ + JSGC_SEMISPACE_NURSERY_ENABLED = 51, + } JSGCParamKey; /* @@ -1344,6 +1353,11 @@ namespace gc { extern JS_PUBLIC_API JSObject* NewMemoryInfoObject(JSContext* cx); /* + * Return whether |obj| is a dead nursery object during a minor GC. + */ +JS_PUBLIC_API bool IsDeadNurseryObject(JSObject* obj); + +/* * Run the finalizer of a nursery-allocated JSObject that is known to be dead. * * This is a dangerous operation - only use this if you know what you're doing! diff --git a/js/public/GCHashTable.h b/js/public/GCHashTable.h index 69487b97a3..2c9a15072a 100644 --- a/js/public/GCHashTable.h +++ b/js/public/GCHashTable.h @@ -62,7 +62,8 @@ class GCHashMap : public js::HashMap<Key, Value, HashPolicy, AllocPolicy> { public: using EntryGCPolicy = MapEntryGCPolicy; - explicit GCHashMap(AllocPolicy a = AllocPolicy()) : Base(std::move(a)) {} + explicit GCHashMap() : Base(AllocPolicy()) {} + explicit GCHashMap(AllocPolicy a) : Base(std::move(a)) {} explicit GCHashMap(size_t length) : Base(length) {} GCHashMap(AllocPolicy a, size_t length) : Base(std::move(a), length) {} diff --git a/js/public/HeapAPI.h b/js/public/HeapAPI.h index 26cca9e1c3..cabadeb75d 100644 --- a/js/public/HeapAPI.h +++ b/js/public/HeapAPI.h @@ -45,7 +45,7 @@ const size_t ArenaShift = 12; const size_t ArenaSize = size_t(1) << ArenaShift; const size_t ArenaMask = ArenaSize - 1; -#if defined(XP_MACOSX) && defined(__aarch64__) +#if defined(XP_DARWIN) && defined(__aarch64__) const size_t PageShift = 14; #else const size_t PageShift = 12; @@ -82,28 +82,61 @@ const size_t ArenaBitmapBits = ArenaSize / CellBytesPerMarkBit; const size_t ArenaBitmapBytes = HowMany(ArenaBitmapBits, 8); const size_t ArenaBitmapWords = HowMany(ArenaBitmapBits, JS_BITS_PER_WORD); +enum class ChunkKind : uint8_t { + Invalid = 0, + TenuredHeap, + NurseryToSpace, + NurseryFromSpace +}; + // The base class for all GC chunks, either in the nursery or in the tenured // heap memory. This structure is locatable from any GC pointer by aligning to // the chunk size. -class alignas(CellAlignBytes) ChunkBase { +class ChunkBase { protected: - ChunkBase(JSRuntime* rt, StoreBuffer* sb) { + // Initialize a tenured heap chunk. + explicit ChunkBase(JSRuntime* rt) { MOZ_ASSERT((uintptr_t(this) & ChunkMask) == 0); - initBase(rt, sb); + initBaseForTenuredChunk(rt); } - void initBase(JSRuntime* rt, StoreBuffer* sb) { + void initBaseForTenuredChunk(JSRuntime* rt) { runtime = rt; - storeBuffer = sb; + storeBuffer = nullptr; + kind = ChunkKind::TenuredHeap; + nurseryChunkIndex = UINT8_MAX; + } + + // Initialize a nursery chunk. + ChunkBase(JSRuntime* rt, StoreBuffer* sb, ChunkKind kind, uint8_t chunkIndex) + : storeBuffer(sb), + runtime(rt), + kind(kind), + nurseryChunkIndex(chunkIndex) { + MOZ_ASSERT(kind == ChunkKind::NurseryFromSpace || + kind == ChunkKind::NurseryToSpace); + MOZ_ASSERT((uintptr_t(this) & ChunkMask) == 0); + MOZ_ASSERT(storeBuffer); } public: + ChunkKind getKind() const { + MOZ_ASSERT_IF(storeBuffer, kind == ChunkKind::NurseryToSpace || + kind == ChunkKind::NurseryFromSpace); + MOZ_ASSERT_IF(!storeBuffer, kind == ChunkKind::TenuredHeap); + return kind; + } + // The store buffer for pointers from tenured things to things in this // chunk. Will be non-null if and only if this is a nursery chunk. StoreBuffer* storeBuffer; // Provide quick access to the runtime from absolutely anywhere. JSRuntime* runtime; + + ChunkKind kind; + + uint8_t nurseryChunkIndex; }; // Information about tenured heap chunks. @@ -234,7 +267,7 @@ class TenuredChunkBase : public ChunkBase { ChunkPageBitmap decommittedPages; protected: - explicit TenuredChunkBase(JSRuntime* runtime) : ChunkBase(runtime, nullptr) { + explicit TenuredChunkBase(JSRuntime* runtime) : ChunkBase(runtime) { info.numArenasFree = ArenasPerChunk; } @@ -523,6 +556,7 @@ static MOZ_ALWAYS_INLINE ChunkBase* GetCellChunkBase(const Cell* cell) { MOZ_ASSERT(cell); auto* chunk = reinterpret_cast<ChunkBase*>(uintptr_t(cell) & ~ChunkMask); MOZ_ASSERT(chunk->runtime); + MOZ_ASSERT(chunk->kind != ChunkKind::Invalid); return chunk; } @@ -532,6 +566,7 @@ static MOZ_ALWAYS_INLINE TenuredChunkBase* GetCellChunkBase( auto* chunk = reinterpret_cast<TenuredChunkBase*>(uintptr_t(cell) & ~ChunkMask); MOZ_ASSERT(chunk->runtime); + MOZ_ASSERT(chunk->kind == ChunkKind::TenuredHeap); return chunk; } diff --git a/js/public/Modules.h b/js/public/Modules.h index 2e7192e120..4a8d45acb0 100644 --- a/js/public/Modules.h +++ b/js/public/Modules.h @@ -299,6 +299,8 @@ extern JS_PUBLIC_API JSObject* GetModuleEnvironment( */ extern JS_PUBLIC_API void ClearModuleEnvironment(JSObject* moduleObj); +extern JS_PUBLIC_API bool ModuleIsLinked(JSObject* moduleObj); + } // namespace JS #endif // js_Modules_h diff --git a/js/public/Promise.h b/js/public/Promise.h index 221d6fdfee..75bc7fd837 100644 --- a/js/public/Promise.h +++ b/js/public/Promise.h @@ -80,6 +80,12 @@ class JS_PUBLIC_API JobQueue { */ virtual bool empty() const = 0; + /** + * Returns true if the job queue stops draining, which results in `empty()` + * being false after `runJobs()`. + */ + virtual bool isDrainingStopped() const = 0; + protected: friend class AutoDebuggerJobQueueInterruption; diff --git a/js/public/PropertySpec.h b/js/public/PropertySpec.h index 9d8115508d..0c37e598fb 100644 --- a/js/public/PropertySpec.h +++ b/js/public/PropertySpec.h @@ -373,6 +373,10 @@ constexpr uint8_t CheckAccessorAttrs() { JSPropertySpec::nativeAccessors(::JS::SymbolCode::symbol, \ CheckAccessorAttrs<attributes>(), getter, \ nullptr) +#define JS_SYM_GETSET(symbol, getter, setter, attributes) \ + JSPropertySpec::nativeAccessors(::JS::SymbolCode::symbol, \ + CheckAccessorAttrs<attributes>(), getter, \ + nullptr, setter, nullptr) #define JS_SELF_HOSTED_GET(name, getterName, attributes) \ JSPropertySpec::selfHostedAccessors(name, CheckAccessorAttrs<attributes>(), \ getterName) @@ -417,11 +421,15 @@ struct JSFunctionSpec { #define JS_FS_END JS_FN(nullptr, nullptr, 0, 0) /* - * Initializer macros for a JSFunctionSpec array element. JS_FNINFO allows the - * simple adding of JSJitInfos. JS_SELF_HOSTED_FN declares a self-hosted - * function. JS_INLINABLE_FN allows specifying an InlinableNative enum value for - * natives inlined or specialized by the JIT. Finally JS_FNSPEC has slots for - * all the fields. + * Initializer macros for a JSFunctionSpec array element. + * + * - JS_FNINFO allows the simple adding of JSJitInfos. + * - JS_SELF_HOSTED_FN declares a self-hosted function. + * - JS_INLINABLE_FN allows specifying an InlinableNative enum value for natives + * inlined or specialized by the JIT. + * - JS_TRAMPOLINE_FN allows specifying a TrampolineNative enum value for + * natives that have a JitEntry trampoline. + * - JS_FNSPEC has slots for all the fields. * * The _SYM variants allow defining a function with a symbol key rather than a * string key. For example, use JS_SYM_FN(iterator, ...) to define an @@ -431,6 +439,8 @@ struct JSFunctionSpec { JS_FNSPEC(name, call, nullptr, nargs, flags, nullptr) #define JS_INLINABLE_FN(name, call, nargs, flags, native) \ JS_FNSPEC(name, call, &js::jit::JitInfo_##native, nargs, flags, nullptr) +#define JS_TRAMPOLINE_FN(name, call, nargs, flags, native) \ + JS_FNSPEC(name, call, &js::jit::JitInfo_##native, nargs, flags, nullptr) #define JS_SYM_FN(symbol, call, nargs, flags) \ JS_SYM_FNSPEC(symbol, call, nullptr, nargs, flags, nullptr) #define JS_FNINFO(name, call, info, nargs, flags) \ diff --git a/js/public/RealmOptions.h b/js/public/RealmOptions.h index 0a0c1faeff..b3ab20915a 100644 --- a/js/public/RealmOptions.h +++ b/js/public/RealmOptions.h @@ -178,14 +178,6 @@ class JS_PUBLIC_API RealmCreationOptions { return *this; } -#ifdef ENABLE_JSON_PARSE_WITH_SOURCE - bool getJSONParseWithSource() const { return jsonParseWithSource; } - RealmCreationOptions& setJSONParseWithSource(bool flag) { - jsonParseWithSource = flag; - return *this; - } -#endif - // This flag doesn't affect JS engine behavior. It is used by Gecko to // mark whether content windows and workers are "Secure Context"s. See // https://w3c.github.io/webappsec-secure-contexts/ @@ -246,9 +238,6 @@ class JS_PUBLIC_API RealmCreationOptions { bool defineSharedArrayBufferConstructor_ = true; bool coopAndCoep_ = false; bool toSource_ = false; -#ifdef ENABLE_JSON_PARSE_WITH_SOURCE - bool jsonParseWithSource = false; -#endif bool secureContext_ = false; bool freezeBuiltins_ = false; diff --git a/js/public/RootingAPI.h b/js/public/RootingAPI.h index e35a2c5bc8..eaae2db1b7 100644 --- a/js/public/RootingAPI.h +++ b/js/public/RootingAPI.h @@ -232,7 +232,7 @@ struct SafelyInitialized { // |T| per C++11 [expr.type.conv]p2 -- will produce a safely-initialized, // safely-usable T that it can return. -#if defined(XP_WIN) || defined(XP_MACOSX) || \ +#if defined(XP_WIN) || defined(XP_DARWIN) || \ (defined(XP_UNIX) && !defined(__clang__)) // That presumption holds for pointers, where value initialization produces diff --git a/js/public/ValueArray.h b/js/public/ValueArray.h index 0d520fb9b6..734d8aca65 100644 --- a/js/public/ValueArray.h +++ b/js/public/ValueArray.h @@ -57,6 +57,9 @@ class HandleValueArray { MOZ_IMPLICIT HandleValueArray(const RootedVector<Value>& values) : length_(values.length()), elements_(values.begin()) {} + MOZ_IMPLICIT HandleValueArray(const PersistentRootedVector<Value>& values) + : length_(values.length()), elements_(values.begin()) {} + template <size_t N> MOZ_IMPLICIT HandleValueArray(const RootedValueArray<N>& values) : length_(N), elements_(values.begin()) {} diff --git a/js/public/experimental/JitInfo.h b/js/public/experimental/JitInfo.h index 1b070021bd..896b5fef68 100644 --- a/js/public/experimental/JitInfo.h +++ b/js/public/experimental/JitInfo.h @@ -8,6 +8,7 @@ #define js_experimental_JitInfo_h #include "mozilla/Assertions.h" // MOZ_ASSERT +#include "mozilla/Attributes.h" // MOZ_NON_PARAM #include <stddef.h> // size_t #include <stdint.h> // uint16_t, uint32_t @@ -21,6 +22,7 @@ namespace js { namespace jit { enum class InlinableNative : uint16_t; +enum class TrampolineNative : uint16_t; } // namespace jit @@ -72,7 +74,7 @@ struct JSJitMethodCallArgsTraits; * A class, expected to be passed by reference, which represents the CallArgs * for a JSJitMethodOp. */ -class JSJitMethodCallArgs +class MOZ_NON_PARAM JSJitMethodCallArgs : protected JS::detail::CallArgsBase<JS::detail::NoUsedRval> { private: using Base = JS::detail::CallArgsBase<JS::detail::NoUsedRval>; @@ -137,6 +139,7 @@ class JSJitInfo { Method, StaticMethod, InlinableNative, + TrampolineNative, IgnoresReturnValueNative, // Must be last OpTypeCount @@ -226,6 +229,7 @@ class JSJitInfo { union { uint16_t protoID; js::jit::InlinableNative inlinableNative; + js::jit::TrampolineNative trampolineNative; }; union { diff --git a/js/public/friend/ErrorNumbers.msg b/js/public/friend/ErrorNumbers.msg index e75e7b973c..42f9a6615d 100644 --- a/js/public/friend/ErrorNumbers.msg +++ b/js/public/friend/ErrorNumbers.msg @@ -80,6 +80,8 @@ MSG_DEF(JSMSG_SPREAD_TOO_LARGE, 0, JSEXN_RANGEERR, "array too large due t MSG_DEF(JSMSG_BAD_WEAKMAP_KEY, 0, JSEXN_TYPEERR, "cannot use the given object as a weak map key") MSG_DEF(JSMSG_WEAKMAP_KEY_CANT_BE_HELD_WEAKLY, 1, JSEXN_TYPEERR, "WeakMap key {0} must be an object or an unregistered symbol") MSG_DEF(JSMSG_WEAKSET_VAL_CANT_BE_HELD_WEAKLY, 1, JSEXN_TYPEERR, "WeakSet value {0} must be an object or an unregistered symbol") +MSG_DEF(JSMSG_WEAKMAP_KEY_MUST_BE_AN_OBJECT, 1, JSEXN_TYPEERR, "WeakMap key {0} must be an object") +MSG_DEF(JSMSG_WEAKSET_VAL_MUST_BE_AN_OBJECT, 1, JSEXN_TYPEERR, "WeakSet value {0} must be an object") MSG_DEF(JSMSG_BAD_GETTER_OR_SETTER, 1, JSEXN_TYPEERR, "invalid {0} usage") MSG_DEF(JSMSG_BAD_ARRAY_LENGTH, 0, JSEXN_RANGEERR, "invalid array length") MSG_DEF(JSMSG_SOURCE_ARRAY_TOO_LONG, 0, JSEXN_RANGEERR, "source array is too long") @@ -137,6 +139,9 @@ MSG_DEF(JSMSG_CONSTRUCTOR_DISABLED, 1, JSEXN_TYPEERR, "{0} is disabled") // JSON MSG_DEF(JSMSG_JSON_BAD_PARSE, 3, JSEXN_SYNTAXERR, "JSON.parse: {0} at line {1} column {2} of the JSON data") MSG_DEF(JSMSG_JSON_CYCLIC_VALUE, 0, JSEXN_TYPEERR, "cyclic object value") +MSG_DEF(JSMSG_JSON_RAW_EMPTY, 0, JSEXN_SYNTAXERR, "JSON.rawJSON: argument cannot be empty when converted to string") +MSG_DEF(JSMSG_JSON_RAW_ARRAY_OR_OBJECT, 0, JSEXN_SYNTAXERR, "JSON.rawJSON: outermost value must not be an array or object") +MSG_DEF(JSMSG_JSON_RAW_WHITESPACE, 0, JSEXN_SYNTAXERR, "JSON.rawJSON: first or last character cannot be any of tab, line feed, carriage return, or space") // Runtime errors MSG_DEF(JSMSG_ASSIGN_TO_CALL, 0, JSEXN_REFERENCEERR, "cannot assign to function call") @@ -669,6 +674,15 @@ MSG_DEF(JSMSG_TYPED_ARRAY_CONSTRUCT_OFFSET_MISALIGNED, 2, JSEXN_RANGEERR, "buffe MSG_DEF(JSMSG_TYPED_ARRAY_CONSTRUCT_OFFSET_LENGTH_BOUNDS, 1, JSEXN_RANGEERR, "size of buffer is too small for {0}Array with byteOffset") MSG_DEF(JSMSG_TYPED_ARRAY_CONSTRUCT_ARRAY_LENGTH_BOUNDS, 1, JSEXN_RANGEERR, "attempting to construct out-of-bounds {0}Array on ArrayBuffer") MSG_DEF(JSMSG_TYPED_ARRAY_CONSTRUCT_TOO_LARGE, 1, JSEXN_RANGEERR, "{0}Array too large") +MSG_DEF(JSMSG_TYPED_ARRAY_BAD_HEX_STRING_LENGTH, 0, JSEXN_SYNTAXERR, "hex-string must have an even number of characters") +MSG_DEF(JSMSG_TYPED_ARRAY_BAD_HEX_DIGIT, 1, JSEXN_SYNTAXERR, "'{0}' is not a valid hex-digit") +MSG_DEF(JSMSG_TYPED_ARRAY_BAD_BASE64_ALPHABET, 0, JSEXN_TYPEERR, "\"alphabet\" option must be one of \"base64\" or \"base64url\"") +MSG_DEF(JSMSG_TYPED_ARRAY_BAD_BASE64_LAST_CHUNK_HANDLING, 0, JSEXN_TYPEERR, "\"lastChunkHandling\" option must be one of \"loose\", \"strict\", or \"stop-before-partial\"") +MSG_DEF(JSMSG_TYPED_ARRAY_BAD_BASE64_CHAR, 1, JSEXN_SYNTAXERR, "'{0}' is not a valid base64 character") +MSG_DEF(JSMSG_TYPED_ARRAY_BAD_INCOMPLETE_CHUNK, 0, JSEXN_SYNTAXERR, "unexpected incomplete base64 chunk") +MSG_DEF(JSMSG_TYPED_ARRAY_BAD_BASE64_AFTER_PADDING, 1, JSEXN_SYNTAXERR, "unexpected '{0}' after base64 padding") +MSG_DEF(JSMSG_TYPED_ARRAY_MISSING_BASE64_PADDING, 0, JSEXN_SYNTAXERR, "missing padding '=' character") +MSG_DEF(JSMSG_TYPED_ARRAY_EXTRA_BASE64_BITS, 0, JSEXN_SYNTAXERR, "unexpected extra bits in last character") MSG_DEF(JSMSG_TYPED_ARRAY_CALL_OR_CONSTRUCT, 1, JSEXN_TYPEERR, "cannot directly {0} builtin %TypedArray%") MSG_DEF(JSMSG_NON_TYPED_ARRAY_RETURNED, 0, JSEXN_TYPEERR, "constructor didn't return TypedArray object") @@ -714,10 +728,9 @@ MSG_DEF(JSMSG_CANT_DELETE_SUPER, 0, JSEXN_REFERENCEERR, "invalid delete involvin MSG_DEF(JSMSG_REINIT_THIS, 0, JSEXN_REFERENCEERR, "super() called twice in derived class constructor") // Modules -MSG_DEF(JSMSG_MISSING_INDIRECT_EXPORT, 0, JSEXN_SYNTAXERR, "indirect export not found") -MSG_DEF(JSMSG_AMBIGUOUS_INDIRECT_EXPORT, 0, JSEXN_SYNTAXERR, "ambiguous indirect export") -MSG_DEF(JSMSG_MISSING_IMPORT, 0, JSEXN_SYNTAXERR, "import not found") -MSG_DEF(JSMSG_AMBIGUOUS_IMPORT, 0, JSEXN_SYNTAXERR, "ambiguous import") +MSG_DEF(JSMSG_MODULE_NO_EXPORT, 2, JSEXN_SYNTAXERR, "The requested module '{0}' doesn't provide an export named: '{1}'") +MSG_DEF(JSMSG_MODULE_CIRCULAR_IMPORT, 2, JSEXN_SYNTAXERR, "The requested module '{0}' contains circular import: '{1}'") +MSG_DEF(JSMSG_MODULE_AMBIGUOUS, 4, JSEXN_SYNTAXERR, "The requested module '{0}' contains ambiguous star export: '{1}' from '{2}' and '{3}'") MSG_DEF(JSMSG_MISSING_EXPORT, 1, JSEXN_SYNTAXERR, "local binding for export '{0}' not found") MSG_DEF(JSMSG_BAD_MODULE_STATUS, 1, JSEXN_INTERNALERR, "module record has unexpected status: {0}") MSG_DEF(JSMSG_DYNAMIC_IMPORT_FAILED, 1, JSEXN_TYPEERR, "error loading dynamically imported module: {0}") diff --git a/js/src/build/moz.build b/js/src/build/moz.build index 089ef37b05..b38f848c7a 100644 --- a/js/src/build/moz.build +++ b/js/src/build/moz.build @@ -82,10 +82,20 @@ if CONFIG["OS_ARCH"] == "WINNT": "bcrypt", "ntdll", ] + # Version string comparison is generally wrong, but by the time it would + # actually matter, either bug 1489995 would be fixed, or the build would + # require version >= 1.78. + if CONFIG["RUSTC_VERSION"] and CONFIG["RUSTC_VERSION"] >= "1.78.0": + OS_LIBS += [ + "synchronization", + ] if CONFIG["MOZ_NEEDS_LIBATOMIC"]: OS_LIBS += ["atomic"] +if CONFIG["TARGET_OS"] == "iOS": + OS_LIBS += ["-framework BrowserEngineCore"] + OS_LIBS += CONFIG["REALTIME_LIBS"] NO_EXPAND_LIBS = True diff --git a/js/src/builtin/Array.cpp b/js/src/builtin/Array.cpp index 76493eee7f..2ced07b6b9 100644 --- a/js/src/builtin/Array.cpp +++ b/js/src/builtin/Array.cpp @@ -10,6 +10,7 @@ #include "mozilla/DebugOnly.h" #include "mozilla/MathAlgorithms.h" #include "mozilla/Maybe.h" +#include "mozilla/ScopeExit.h" #include "mozilla/SIMD.h" #include "mozilla/TextUtils.h" @@ -23,6 +24,7 @@ #include "ds/Sort.h" #include "jit/InlinableNatives.h" +#include "jit/TrampolineNatives.h" #include "js/Class.h" #include "js/Conversions.h" #include "js/experimental/JitInfo.h" // JSJitGetterOp, JSJitInfo @@ -2026,57 +2028,10 @@ static bool FillWithUndefined(JSContext* cx, HandleObject obj, uint32_t start, return true; } -static bool ArrayNativeSortImpl(JSContext* cx, Handle<JSObject*> obj, - Handle<Value> fval, ComparatorMatchResult comp); - -bool js::intrinsic_ArrayNativeSort(JSContext* cx, unsigned argc, Value* vp) { - // This function is called from the self-hosted Array.prototype.sort - // implementation. It returns |true| if the array was sorted, otherwise it - // returns |false| to notify the self-hosted code to perform the sorting. - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 1); - - HandleValue fval = args[0]; - MOZ_ASSERT(fval.isUndefined() || IsCallable(fval)); - - ComparatorMatchResult comp; - if (fval.isObject()) { - comp = MatchNumericComparator(cx, &fval.toObject()); - if (comp == Match_Failure) { - return false; - } - - if (comp == Match_None) { - // Non-optimized user supplied comparators perform much better when - // called from within a self-hosted sorting function. - args.rval().setBoolean(false); - return true; - } - } else { - comp = Match_None; - } - - Rooted<JSObject*> obj(cx, &args.thisv().toObject()); - - if (!ArrayNativeSortImpl(cx, obj, fval, comp)) { - return false; - } - - args.rval().setBoolean(true); - return true; -} - -static bool ArrayNativeSortImpl(JSContext* cx, Handle<JSObject*> obj, - Handle<Value> fval, - ComparatorMatchResult comp) { - uint64_t length; - if (!GetLengthPropertyInlined(cx, obj, &length)) { - return false; - } - if (length < 2) { - /* [] and [a] remain unchanged when sorted. */ - return true; - } +static bool ArraySortWithoutComparator(JSContext* cx, Handle<JSObject*> obj, + uint64_t length, + ComparatorMatchResult comp) { + MOZ_ASSERT(length > 1); if (length > UINT32_MAX) { ReportAllocationOverflow(cx); @@ -2225,6 +2180,519 @@ static bool ArrayNativeSortImpl(JSContext* cx, Handle<JSObject*> obj, return true; } +void ArraySortData::init(JSObject* obj, JSObject* comparator, ValueVector&& vec, + uint32_t length, uint32_t denseLen) { + MOZ_ASSERT(!vec.empty(), "must have items to sort"); + MOZ_ASSERT(denseLen <= length); + + obj_ = obj; + comparator_ = comparator; + + this->length = length; + this->denseLen = denseLen; + this->vec = std::move(vec); + + auto getComparatorKind = [](JSContext* cx, JSObject* comparator) { + if (!comparator->is<JSFunction>()) { + return ComparatorKind::Unoptimized; + } + JSFunction* fun = &comparator->as<JSFunction>(); + if (!fun->hasJitEntry() || fun->isClassConstructor()) { + return ComparatorKind::Unoptimized; + } + if (fun->realm() == cx->realm() && fun->nargs() <= ComparatorActualArgs) { + return ComparatorKind::JSSameRealmNoRectifier; + } + return ComparatorKind::JS; + }; + comparatorKind_ = getComparatorKind(cx(), comparator); +} + +// This function handles sorting without a comparator function (or with a +// trivial comparator function that we can pattern match) by calling +// ArraySortWithoutComparator. +// +// If there's a non-trivial comparator function, it initializes the +// ArraySortData struct for ArraySortData::sortWithComparator. This function +// must be called next to perform the sorting. +// +// This is separate from ArraySortData::sortWithComparator because it lets the +// compiler generate better code for ArraySortData::sortWithComparator. +// +// https://tc39.es/ecma262/#sec-array.prototype.sort +// 23.1.3.30 Array.prototype.sort ( comparefn ) +static MOZ_ALWAYS_INLINE bool ArraySortPrologue(JSContext* cx, + Handle<Value> thisv, + Handle<Value> comparefn, + ArraySortData* d, bool* done) { + // Step 1. + if (MOZ_UNLIKELY(!comparefn.isUndefined() && !IsCallable(comparefn))) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_SORT_ARG); + return false; + } + + // Step 2. + Rooted<JSObject*> obj(cx, ToObject(cx, thisv)); + if (!obj) { + return false; + } + + // Step 3. + uint64_t length; + if (MOZ_UNLIKELY(!GetLengthPropertyInlined(cx, obj, &length))) { + return false; + } + + // Arrays with less than two elements remain unchanged when sorted. + if (length <= 1) { + d->setReturnValue(obj); + *done = true; + return true; + } + + // Use a fast path if there's no comparator or if the comparator is a function + // that we can pattern match. + do { + ComparatorMatchResult comp = Match_None; + if (comparefn.isObject()) { + comp = MatchNumericComparator(cx, &comparefn.toObject()); + if (comp == Match_Failure) { + return false; + } + if (comp == Match_None) { + // Pattern matching failed. + break; + } + } + if (!ArraySortWithoutComparator(cx, obj, length, comp)) { + return false; + } + d->setReturnValue(obj); + *done = true; + return true; + } while (false); + + // Ensure length * 2 (used below) doesn't overflow UINT32_MAX. + if (MOZ_UNLIKELY(length > UINT32_MAX / 2)) { + ReportAllocationOverflow(cx); + return false; + } + uint32_t len = uint32_t(length); + + // Merge sort requires extra scratch space. + bool needsScratchSpace = len >= ArraySortData::InsertionSortLimit; + + Rooted<ArraySortData::ValueVector> vec(cx); + if (MOZ_UNLIKELY(!vec.reserve(needsScratchSpace ? (2 * len) : len))) { + ReportOutOfMemory(cx); + return false; + } + + // Append elements to the vector. Skip holes. + if (IsPackedArray(obj)) { + Handle<ArrayObject*> array = obj.as<ArrayObject>(); + const Value* elements = array->getDenseElements(); + vec.infallibleAppend(elements, len); + } else { + RootedValue v(cx); + for (uint32_t i = 0; i < len; i++) { + if (!CheckForInterrupt(cx)) { + return false; + } + + bool hole; + if (!HasAndGetElement(cx, obj, i, &hole, &v)) { + return false; + } + if (hole) { + continue; + } + vec.infallibleAppend(v); + } + // If there are only holes, the object is already sorted. + if (vec.empty()) { + d->setReturnValue(obj); + *done = true; + return true; + } + } + + uint32_t denseLen = vec.length(); + if (needsScratchSpace) { + MOZ_ALWAYS_TRUE(vec.resize(denseLen * 2)); + } + d->init(obj, &comparefn.toObject(), std::move(vec.get()), len, denseLen); + + // Continue in ArraySortData::sortWithComparator. + MOZ_ASSERT(!*done); + return true; +} + +static ArraySortResult CallComparatorSlow(ArraySortData* d, const Value& x, + const Value& y) { + JSContext* cx = d->cx(); + FixedInvokeArgs<2> callArgs(cx); + callArgs[0].set(x); + callArgs[1].set(y); + Rooted<Value> comparefn(cx, ObjectValue(*d->comparator())); + Rooted<Value> rval(cx); + if (!js::Call(cx, comparefn, UndefinedHandleValue, callArgs, &rval)) { + return ArraySortResult::Failure; + } + d->setComparatorReturnValue(rval); + return ArraySortResult::Done; +} + +static MOZ_ALWAYS_INLINE ArraySortResult +MaybeYieldToComparator(ArraySortData* d, const Value& x, const Value& y) { + // https://tc39.es/ecma262/#sec-comparearrayelements + // 23.1.3.30.2 CompareArrayElements ( x, y, comparefn ) + + // Steps 1-2. + if (x.isUndefined()) { + d->setComparatorReturnValue(Int32Value(y.isUndefined() ? 0 : 1)); + return ArraySortResult::Done; + } + + // Step 3. + if (y.isUndefined()) { + d->setComparatorReturnValue(Int32Value(-1)); + return ArraySortResult::Done; + } + + // Step 4. Yield to the JIT trampoline (or js::array_sort) if the comparator + // is a JS function we can call more efficiently from JIT code. + auto kind = d->comparatorKind(); + if (MOZ_LIKELY(kind != ArraySortData::ComparatorKind::Unoptimized)) { + d->setComparatorArgs(x, y); + return (kind == ArraySortData::ComparatorKind::JSSameRealmNoRectifier) + ? ArraySortResult::CallJSSameRealmNoRectifier + : ArraySortResult::CallJS; + } + return CallComparatorSlow(d, x, y); +} + +static MOZ_ALWAYS_INLINE bool RvalIsLessOrEqual(ArraySortData* data, + bool* lessOrEqual) { + // https://tc39.es/ecma262/#sec-comparearrayelements + // 23.1.3.30.2 CompareArrayElements ( x, y, comparefn ) + + // Fast path for int32 return values. + Value rval = data->comparatorReturnValue(); + if (MOZ_LIKELY(rval.isInt32())) { + *lessOrEqual = rval.toInt32() <= 0; + return true; + } + + // Step 4.a. + Rooted<Value> rvalRoot(data->cx(), rval); + double d; + if (MOZ_UNLIKELY(!ToNumber(data->cx(), rvalRoot, &d))) { + return false; + } + + // Step 4.b-c. + *lessOrEqual = std::isnan(d) ? true : (d <= 0); + return true; +} + +static void CopyValues(Value* out, const Value* list, uint32_t start, + uint32_t end) { + for (uint32_t i = start; i <= end; i++) { + out[i] = list[i]; + } +} + +// static +ArraySortResult ArraySortData::sortWithComparator(ArraySortData* d) { + JSContext* cx = d->cx(); + auto& vec = d->vec; + + // This function is like a generator that is called repeatedly from the JIT + // trampoline or js::array_sort. Resume the sorting algorithm where we left + // off before calling the comparator. + switch (d->state) { + case State::Initial: + break; + case State::InsertionSortCall1: + goto insertion_sort_call1; + case State::InsertionSortCall2: + goto insertion_sort_call2; + case State::MergeSortCall1: + goto merge_sort_call1; + case State::MergeSortCall2: + goto merge_sort_call2; + } + + d->list = vec.begin(); + + // Use insertion sort for small arrays. + if (d->denseLen < InsertionSortLimit) { + for (d->i = 1; d->i < d->denseLen; d->i++) { + d->item = vec[d->i]; + d->j = d->i - 1; + do { + { + ArraySortResult res = MaybeYieldToComparator(d, vec[d->j], d->item); + if (res != ArraySortResult::Done) { + d->state = State::InsertionSortCall1; + return res; + } + } + insertion_sort_call1: + bool lessOrEqual; + if (!RvalIsLessOrEqual(d, &lessOrEqual)) { + return ArraySortResult::Failure; + } + if (lessOrEqual) { + break; + } + vec[d->j + 1] = vec[d->j]; + } while (d->j-- > 0); + vec[d->j + 1] = d->item; + } + } else { + static constexpr size_t InitialWindowSize = 4; + + // Use insertion sort for initial ranges. + for (d->start = 0; d->start < d->denseLen - 1; + d->start += InitialWindowSize) { + d->end = + std::min<uint32_t>(d->start + InitialWindowSize - 1, d->denseLen - 1); + for (d->i = d->start + 1; d->i <= d->end; d->i++) { + d->item = vec[d->i]; + d->j = d->i - 1; + do { + { + ArraySortResult res = MaybeYieldToComparator(d, vec[d->j], d->item); + if (res != ArraySortResult::Done) { + d->state = State::InsertionSortCall2; + return res; + } + } + insertion_sort_call2: + bool lessOrEqual; + if (!RvalIsLessOrEqual(d, &lessOrEqual)) { + return ArraySortResult::Failure; + } + if (lessOrEqual) { + break; + } + vec[d->j + 1] = vec[d->j]; + } while (d->j-- > d->start); + vec[d->j + 1] = d->item; + } + } + + // Merge sort. Set d->out to scratch space initially. + d->out = vec.begin() + d->denseLen; + for (d->windowSize = InitialWindowSize; d->windowSize < d->denseLen; + d->windowSize *= 2) { + for (d->start = 0; d->start < d->denseLen; + d->start += 2 * d->windowSize) { + // The midpoint between the two subarrays. + d->mid = d->start + d->windowSize - 1; + + // To keep from going over the edge. + d->end = std::min<uint32_t>(d->start + 2 * d->windowSize - 1, + d->denseLen - 1); + + // Merge comparator-sorted slices list[start..<=mid] and + // list[mid+1..<=end], storing the merged sequence in out[start..<=end]. + + // Skip lopsided runs to avoid doing useless work. + if (d->mid >= d->end) { + CopyValues(d->out, d->list, d->start, d->end); + continue; + } + + // Skip calling the comparator if the sub-list is already sorted. + { + ArraySortResult res = + MaybeYieldToComparator(d, d->list[d->mid], d->list[d->mid + 1]); + if (res != ArraySortResult::Done) { + d->state = State::MergeSortCall1; + return res; + } + } + merge_sort_call1: + bool lessOrEqual; + if (!RvalIsLessOrEqual(d, &lessOrEqual)) { + return ArraySortResult::Failure; + } + if (lessOrEqual) { + CopyValues(d->out, d->list, d->start, d->end); + continue; + } + + d->i = d->start; + d->j = d->mid + 1; + d->k = d->start; + + while (d->i <= d->mid && d->j <= d->end) { + { + ArraySortResult res = + MaybeYieldToComparator(d, d->list[d->i], d->list[d->j]); + if (res != ArraySortResult::Done) { + d->state = State::MergeSortCall2; + return res; + } + } + merge_sort_call2: + bool lessOrEqual; + if (!RvalIsLessOrEqual(d, &lessOrEqual)) { + return ArraySortResult::Failure; + } + d->out[d->k++] = lessOrEqual ? d->list[d->i++] : d->list[d->j++]; + } + + // Empty out any remaining elements. Use local variables to let the + // compiler generate more efficient code. + Value* out = d->out; + Value* list = d->list; + uint32_t k = d->k; + uint32_t mid = d->mid; + uint32_t end = d->end; + for (uint32_t i = d->i; i <= mid; i++) { + out[k++] = list[i]; + } + for (uint32_t j = d->j; j <= end; j++) { + out[k++] = list[j]; + } + } + + // Swap both lists. + std::swap(d->list, d->out); + } + } + + // Copy elements to the array. + Rooted<JSObject*> obj(cx, d->obj_); + if (!SetArrayElements(cx, obj, 0, d->denseLen, d->list)) { + return ArraySortResult::Failure; + } + + // Re-create any holes that sorted to the end of the array. + for (uint32_t i = d->denseLen; i < d->length; i++) { + if (!CheckForInterrupt(cx) || !DeletePropertyOrThrow(cx, obj, i)) { + return ArraySortResult::Failure; + } + } + + d->freeMallocData(); + d->setReturnValue(obj); + return ArraySortResult::Done; +} + +// https://tc39.es/ecma262/#sec-array.prototype.sort +// 23.1.3.30 Array.prototype.sort ( comparefn ) +bool js::array_sort(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Array.prototype", "sort"); + CallArgs args = CallArgsFromVp(argc, vp); + + // If we have a comparator argument, use the JIT trampoline implementation + // instead. This avoids a performance cliff (especially with large arrays) + // because C++ => JIT calls are much slower than Trampoline => JIT calls. + if (args.hasDefined(0) && jit::IsBaselineInterpreterEnabled()) { + return CallTrampolineNativeJitCode(cx, jit::TrampolineNative::ArraySort, + args); + } + + Rooted<ArraySortData> data(cx, cx); + + // On all return paths other than ArraySortWithComparatorLoop returning Done, + // we call freeMallocData to not fail debug assertions. This matches the JIT + // trampoline where we can't rely on C++ destructors. + auto freeData = + mozilla::MakeScopeExit([&]() { data.get().freeMallocData(); }); + + bool done = false; + if (!ArraySortPrologue(cx, args.thisv(), args.get(0), data.address(), + &done)) { + return false; + } + if (done) { + args.rval().set(data.get().returnValue()); + return true; + } + + FixedInvokeArgs<2> callArgs(cx); + Rooted<Value> rval(cx); + + while (true) { + ArraySortResult res = ArraySortData::sortWithComparator(data.address()); + switch (res) { + case ArraySortResult::Failure: + return false; + + case ArraySortResult::Done: + freeData.release(); + args.rval().set(data.get().returnValue()); + return true; + + case ArraySortResult::CallJS: + case ArraySortResult::CallJSSameRealmNoRectifier: + MOZ_ASSERT(data.get().comparatorThisValue().isUndefined()); + MOZ_ASSERT(&args[0].toObject() == data.get().comparator()); + callArgs[0].set(data.get().comparatorArg(0)); + callArgs[1].set(data.get().comparatorArg(1)); + if (!js::Call(cx, args[0], UndefinedHandleValue, callArgs, &rval)) { + return false; + } + data.get().setComparatorReturnValue(rval); + break; + } + } +} + +ArraySortResult js::ArraySortFromJit(JSContext* cx, + jit::TrampolineNativeFrameLayout* frame) { + // Initialize the ArraySortData class stored in the trampoline frame. + void* dataUninit = frame->getFrameData<ArraySortData>(); + auto* data = new (dataUninit) ArraySortData(cx); + + Rooted<Value> thisv(cx, frame->thisv()); + Rooted<Value> comparefn(cx); + if (frame->numActualArgs() > 0) { + comparefn = frame->actualArgs()[0]; + } + + bool done = false; + if (!ArraySortPrologue(cx, thisv, comparefn, data, &done)) { + return ArraySortResult::Failure; + } + if (done) { + data->freeMallocData(); + return ArraySortResult::Done; + } + + return ArraySortData::sortWithComparator(data); +} + +ArraySortData::ArraySortData(JSContext* cx) : cx_(cx) { +#ifdef DEBUG + cx_->liveArraySortDataInstances++; +#endif +} + +void ArraySortData::freeMallocData() { + vec.clearAndFree(); +#ifdef DEBUG + MOZ_ASSERT(cx_->liveArraySortDataInstances > 0); + cx_->liveArraySortDataInstances--; +#endif +} + +void ArraySortData::trace(JSTracer* trc) { + TraceNullableRoot(trc, &comparator_, "comparator_"); + TraceRoot(trc, &thisv, "thisv"); + TraceRoot(trc, &callArgs[0], "callArgs0"); + TraceRoot(trc, &callArgs[1], "callArgs1"); + vec.trace(trc); + TraceRoot(trc, &item, "item"); + TraceNullableRoot(trc, &obj_, "obj"); +} + bool js::NewbornArrayPush(JSContext* cx, HandleObject obj, const Value& v) { Handle<ArrayObject*> arr = obj.as<ArrayObject>(); @@ -4846,7 +5314,7 @@ static const JSFunctionSpec array_methods[] = { /* Perl-ish methods. */ JS_INLINABLE_FN("join", array_join, 1, 0, ArrayJoin), JS_FN("reverse", array_reverse, 0, 0), - JS_SELF_HOSTED_FN("sort", "ArraySort", 1, 0), + JS_TRAMPOLINE_FN("sort", array_sort, 1, 0, ArraySort), JS_INLINABLE_FN("push", array_push, 1, 0, ArrayPush), JS_INLINABLE_FN("pop", array_pop, 0, 0, ArrayPop), JS_INLINABLE_FN("shift", array_shift, 0, 0, ArrayShift), diff --git a/js/src/builtin/Array.h b/js/src/builtin/Array.h index f0de034998..f861505e7b 100644 --- a/js/src/builtin/Array.h +++ b/js/src/builtin/Array.h @@ -15,6 +15,10 @@ namespace js { +namespace jit { +class TrampolineNativeFrameLayout; +} + class ArrayObject; MOZ_ALWAYS_INLINE bool IdIsIndex(jsid id, uint32_t* indexp) { @@ -107,16 +111,12 @@ extern bool GetElements(JSContext* cx, HandleObject aobj, uint32_t length, /* Natives exposed for optimization by the interpreter and JITs. */ -extern bool intrinsic_ArrayNativeSort(JSContext* cx, unsigned argc, - js::Value* vp); - extern bool array_includes(JSContext* cx, unsigned argc, js::Value* vp); extern bool array_indexOf(JSContext* cx, unsigned argc, js::Value* vp); extern bool array_lastIndexOf(JSContext* cx, unsigned argc, js::Value* vp); - extern bool array_pop(JSContext* cx, unsigned argc, js::Value* vp); - extern bool array_join(JSContext* cx, unsigned argc, js::Value* vp); +extern bool array_sort(JSContext* cx, unsigned argc, js::Value* vp); extern void ArrayShiftMoveElements(ArrayObject* arr); @@ -172,6 +172,147 @@ extern bool ArrayLengthGetter(JSContext* cx, HandleObject obj, HandleId id, extern bool ArrayLengthSetter(JSContext* cx, HandleObject obj, HandleId id, HandleValue v, ObjectOpResult& result); +// Note: we use uint32_t because the JIT code uses branch32. +enum class ArraySortResult : uint32_t { + Failure, + Done, + CallJS, + CallJSSameRealmNoRectifier +}; + +// We use a JIT trampoline to optimize sorting with a comparator function. The +// trampoline frame has an ArraySortData instance that contains all state used +// by the sorting algorithm. The sorting algorithm is implemented as a C++ +// "generator" that can yield to the trampoline to perform a fast JIT => JIT +// call to the comparator function. When the comparator function returns, the +// trampoline calls back into C++ to resume the sorting algorithm. +// +// ArraySortData stores the JS Values in a js::Vector. To ensure we don't leak +// its memory, we have debug assertions to check that for each C++ constructor +// call we call |freeMallocData| exactly once. C++ code calls |freeMallocData| +// when it's done sorting and the JIT exception handler calls it when unwinding +// the trampoline frame. +class ArraySortData { + public: + enum class ComparatorKind : uint8_t { + Unoptimized, + JS, + JSSameRealmNoRectifier, + }; + + static constexpr size_t ComparatorActualArgs = 2; + + // Insertion sort is used if the length is < InsertionSortLimit. + static constexpr size_t InsertionSortLimit = 24; + + using ValueVector = GCVector<Value, 8, SystemAllocPolicy>; + + protected: // Silence Clang warning about unused private fields. + // Data for the comparator call. These fields must match the JitFrameLayout + // to let us perform efficient calls to the comparator from JIT code. + // This is asserted in the JIT trampoline code. + // callArgs[0] is also used to store the return value of the sort function and + // the comparator. + uintptr_t descriptor_; + JSObject* comparator_ = nullptr; + Value thisv; + Value callArgs[ComparatorActualArgs]; + + private: + ValueVector vec; + Value item; + JSContext* cx_; + JSObject* obj_ = nullptr; + + Value* list; + Value* out; + + // The value of the .length property. + uint32_t length; + + // The number of items to sort. Can be less than |length| if the object has + // holes. + uint32_t denseLen; + + uint32_t windowSize; + uint32_t start; + uint32_t mid; + uint32_t end; + uint32_t i, j, k; + + // The state value determines where we resume in sortWithComparator. + enum class State : uint8_t { + Initial, + InsertionSortCall1, + InsertionSortCall2, + MergeSortCall1, + MergeSortCall2 + }; + State state = State::Initial; + ComparatorKind comparatorKind_; + + // Optional padding to ensure proper alignment of the comparator JIT frame. +#if !defined(JS_64BIT) && !defined(DEBUG) + protected: // Silence Clang warning about unused private field. + size_t padding; +#endif + + public: + explicit ArraySortData(JSContext* cx); + + void MOZ_ALWAYS_INLINE init(JSObject* obj, JSObject* comparator, + ValueVector&& vec, uint32_t length, + uint32_t denseLen); + + JSContext* cx() const { return cx_; } + + JSObject* comparator() const { + MOZ_ASSERT(comparator_); + return comparator_; + } + + Value returnValue() const { return callArgs[0]; } + void setReturnValue(JSObject* obj) { callArgs[0].setObject(*obj); } + + Value comparatorArg(size_t index) { + MOZ_ASSERT(index < ComparatorActualArgs); + return callArgs[index]; + } + Value comparatorThisValue() const { return thisv; } + Value comparatorReturnValue() const { return callArgs[0]; } + void setComparatorArgs(const Value& x, const Value& y) { + callArgs[0] = x; + callArgs[1] = y; + } + void setComparatorReturnValue(const Value& v) { callArgs[0] = v; } + + ComparatorKind comparatorKind() const { return comparatorKind_; } + + static ArraySortResult sortWithComparator(ArraySortData* d); + + void freeMallocData(); + void trace(JSTracer* trc); + + static constexpr int32_t offsetOfDescriptor() { + return offsetof(ArraySortData, descriptor_); + } + static constexpr int32_t offsetOfComparator() { + return offsetof(ArraySortData, comparator_); + } + static constexpr int32_t offsetOfComparatorReturnValue() { + return offsetof(ArraySortData, callArgs[0]); + } + static constexpr int32_t offsetOfComparatorThis() { + return offsetof(ArraySortData, thisv); + } + static constexpr int32_t offsetOfComparatorArgs() { + return offsetof(ArraySortData, callArgs); + } +}; + +extern ArraySortResult ArraySortFromJit( + JSContext* cx, jit::TrampolineNativeFrameLayout* frame); + class MOZ_NON_TEMPORARY_CLASS ArraySpeciesLookup final { /* * An ArraySpeciesLookup holds the following: diff --git a/js/src/builtin/Array.js b/js/src/builtin/Array.js index f831615041..b62edef7e9 100644 --- a/js/src/builtin/Array.js +++ b/js/src/builtin/Array.js @@ -76,85 +76,6 @@ function ArraySome(callbackfn /*, thisArg*/) { // Inlining this enables inlining of the callback function. SetIsInlinableLargeFunction(ArraySome); -// ES2023 draft rev cb4224156c54156f30c18c50784c1b0148ebfae5 -// 23.1.3.30 Array.prototype.sort ( comparefn ) -function ArraySortCompare(comparefn) { - return function(x, y) { - // Steps 4.a-c. - if (x === undefined) { - if (y === undefined) { - return 0; - } - return 1; - } - if (y === undefined) { - return -1; - } - - // Step 4.d.i. - var v = ToNumber(callContentFunction(comparefn, undefined, x, y)); - - // Steps 4.d.ii-iii. - return v !== v ? 0 : v; - }; -} - -// ES2023 draft rev cb4224156c54156f30c18c50784c1b0148ebfae5 -// 23.1.3.30 Array.prototype.sort ( comparefn ) -function ArraySort(comparefn) { - // Step 1. - if (comparefn !== undefined) { - if (!IsCallable(comparefn)) { - ThrowTypeError(JSMSG_BAD_SORT_ARG); - } - } - - // Step 2. - var O = ToObject(this); - - // First try to sort the array in native code, if that fails, indicated by - // returning |false| from ArrayNativeSort, sort it in self-hosted code. - if (callFunction(ArrayNativeSort, O, comparefn)) { - return O; - } - - // Step 3. - var len = ToLength(O.length); - - // Arrays with less than two elements remain unchanged when sorted. - if (len <= 1) { - return O; - } - - // Step 4. - var wrappedCompareFn = ArraySortCompare(comparefn); - - // Step 5. - // To save effort we will do all of our work on a dense list, then create - // holes at the end. - var denseList = []; - var denseLen = 0; - - for (var i = 0; i < len; i++) { - if (i in O) { - DefineDataProperty(denseList, denseLen++, O[i]); - } - } - - if (denseLen < 1) { - return O; - } - - var sorted = MergeSort(denseList, denseLen, wrappedCompareFn); - - assert(IsPackedArray(sorted), "sorted is a packed array"); - assert(sorted.length === denseLen, "sorted array has the correct length"); - - MoveHoles(O, len, sorted, denseLen); - - return O; -} - /* ES5 15.4.4.18. */ function ArrayForEach(callbackfn /*, thisArg*/) { /* Step 1. */ @@ -1335,22 +1256,8 @@ function ArrayToSorted(comparefn) { return items; } - // First try to sort the array in native code, if that fails, indicated by - // returning |false| from ArrayNativeSort, sort it in self-hosted code. - if (callFunction(ArrayNativeSort, items, comparefn)) { - return items; - } - - // Step 5. - var wrappedCompareFn = ArraySortCompare(comparefn); - - // Steps 6-9. - var sorted = MergeSort(items, len, wrappedCompareFn); - - assert(IsPackedArray(sorted), "sorted is a packed array"); - assert(sorted.length === len, "sorted array has the correct length"); - - return sorted; + // Steps 5-9. + return callFunction(std_Array_sort, items, comparefn); } // https://github.com/tc39/proposal-array-find-from-last diff --git a/js/src/builtin/JSON.cpp b/js/src/builtin/JSON.cpp index dbfab8b43a..1423595937 100644 --- a/js/src/builtin/JSON.cpp +++ b/js/src/builtin/JSON.cpp @@ -20,15 +20,18 @@ #include "builtin/Array.h" #include "builtin/BigInt.h" #include "builtin/ParseRecordObject.h" +#include "builtin/RawJSONObject.h" #include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* #include "js/friend/StackLimits.h" // js::AutoCheckRecursionLimit #include "js/Object.h" // JS::GetBuiltinClass +#include "js/Prefs.h" // JS::Prefs #include "js/PropertySpec.h" #include "js/StableStringChars.h" #include "js/TypeDecls.h" #include "js/Value.h" #include "util/StringBuffer.h" -#include "vm/BooleanObject.h" // js::BooleanObject +#include "vm/BooleanObject.h" // js::BooleanObject +#include "vm/EqualityOperations.h" // js::SameValue #include "vm/Interpreter.h" #include "vm/Iteration.h" #include "vm/JSAtomUtils.h" // ToAtom @@ -436,6 +439,16 @@ class CycleDetector { bool appended_; }; +static inline JSString* MaybeGetRawJSON(JSContext* cx, JSObject* obj) { + if (!obj->is<RawJSONObject>()) { + return nullptr; + } + + JSString* rawJSON = obj->as<js::RawJSONObject>().rawJSON(cx); + MOZ_ASSERT(rawJSON); + return rawJSON; +} + #ifdef ENABLE_RECORD_TUPLE enum class JOType { Record, Object }; template <JOType type = JOType::Object> @@ -783,6 +796,12 @@ static bool SerializeJSONProperty(JSContext* cx, const Value& v, MOZ_ASSERT(v.hasObjectPayload()); RootedObject obj(cx, &v.getObjectPayload()); + /* https://tc39.es/proposal-json-parse-with-source/#sec-serializejsonproperty + * Step 4a.*/ + if (JSString* rawJSON = MaybeGetRawJSON(cx, obj)) { + return scx->sb.append(rawJSON); + } + MOZ_ASSERT( !scx->maybeSafely || obj->is<PlainObject>() || obj->is<ArrayObject>(), "input to JS::ToJSONMaybeSafely must not include reachable " @@ -1233,6 +1252,10 @@ static bool FastSerializeJSONProperty(JSContext* cx, Handle<Value> v, MOZ_ASSERT(*whySlow == BailReason::NO_REASON); MOZ_ASSERT(v.isObject()); + if (JSString* rawJSON = MaybeGetRawJSON(cx, &v.toObject())) { + return scx->sb.append(rawJSON); + } + /* * FastSerializeJSONProperty is an optimistic fast path for the * SerializeJSONProperty algorithm that applies in limited situations. It @@ -1356,19 +1379,24 @@ static bool FastSerializeJSONProperty(JSContext* cx, Handle<Value> v, } if (val.isObject()) { - if (stack.length() >= MAX_STACK_DEPTH - 1) { - *whySlow = BailReason::DEEP_RECURSION; - return true; + if (JSString* rawJSON = MaybeGetRawJSON(cx, &val.toObject())) { + if (!scx->sb.append(rawJSON)) { + return false; + } + } else { + if (stack.length() >= MAX_STACK_DEPTH - 1) { + *whySlow = BailReason::DEEP_RECURSION; + return true; + } + // Save the current iterator position on the stack and + // switch to processing the nested value. + stack.infallibleAppend(std::move(top)); + top = FastStackEntry(&val.toObject().as<NativeObject>()); + wroteMember = false; + nestedObject = true; // Break out to the outer loop. + break; } - // Save the current iterator position on the stack and - // switch to processing the nested value. - stack.infallibleAppend(std::move(top)); - top = FastStackEntry(&val.toObject().as<NativeObject>()); - wroteMember = false; - nestedObject = true; // Break out to the outer loop. - break; - } - if (!EmitSimpleValue(cx, scx->sb, val)) { + } else if (!EmitSimpleValue(cx, scx->sb, val)) { return false; } } @@ -1433,19 +1461,24 @@ static bool FastSerializeJSONProperty(JSContext* cx, Handle<Value> v, return false; } if (val.isObject()) { - if (stack.length() >= MAX_STACK_DEPTH - 1) { - *whySlow = BailReason::DEEP_RECURSION; - return true; + if (JSString* rawJSON = MaybeGetRawJSON(cx, &val.toObject())) { + if (!scx->sb.append(rawJSON)) { + return false; + } + } else { + if (stack.length() >= MAX_STACK_DEPTH - 1) { + *whySlow = BailReason::DEEP_RECURSION; + return true; + } + // Save the current iterator position on the stack and + // switch to processing the nested value. + stack.infallibleAppend(std::move(top)); + top = FastStackEntry(&val.toObject().as<NativeObject>()); + wroteMember = false; + nesting = true; // Break out to the outer loop. + break; } - // Save the current iterator position on the stack and - // switch to processing the nested value. - stack.infallibleAppend(std::move(top)); - top = FastStackEntry(&val.toObject().as<NativeObject>()); - wroteMember = false; - nesting = true; // Break out to the outer loop. - break; - } - if (!EmitSimpleValue(cx, scx->sb, val)) { + } else if (!EmitSimpleValue(cx, scx->sb, val)) { return false; } } @@ -1733,6 +1766,41 @@ static bool InternalizeJSONProperty( return false; } +#ifdef ENABLE_JSON_PARSE_WITH_SOURCE + RootedObject context(cx); + Rooted<UniquePtr<ParseRecordObject::EntryMap>> entries(cx); + if (JS::Prefs::experimental_json_parse_with_source()) { + // https://tc39.es/proposal-json-parse-with-source/#sec-internalizejsonproperty + bool sameVal = false; + Rooted<Value> parsedValue(cx, parseRecord.get().value); + if (!SameValue(cx, parsedValue, val, &sameVal)) { + return false; + } + if (!parseRecord.get().isEmpty() && sameVal) { + if (parseRecord.get().parseNode) { + MOZ_ASSERT(!val.isObject()); + Rooted<IdValueVector> props(cx, cx); + if (!props.emplaceBack( + IdValuePair(NameToId(cx->names().source), + StringValue(parseRecord.get().parseNode)))) { + return false; + } + context = NewPlainObjectWithUniqueNames(cx, props); + if (!context) { + return false; + } + } + entries = std::move(parseRecord.get().entries); + } + if (!context) { + context = NewPlainObject(cx); + if (!context) { + return false; + } + } + } +#endif + /* Step 2. */ if (val.isObject()) { RootedObject obj(cx, &val.toObject()); @@ -1763,6 +1831,13 @@ static bool InternalizeJSONProperty( /* Step 2a(iii)(1). */ Rooted<ParseRecordObject> elementRecord(cx); +#ifdef ENABLE_JSON_PARSE_WITH_SOURCE + if (entries) { + if (auto entry = entries->lookup(id)) { + elementRecord = std::move(entry->value()); + } + } +#endif if (!InternalizeJSONProperty(cx, obj, id, reviver, &elementRecord, &newElement)) { return false; @@ -1804,6 +1879,13 @@ static bool InternalizeJSONProperty( /* Step 2c(ii)(1). */ id = keys[i]; Rooted<ParseRecordObject> entryRecord(cx); +#ifdef ENABLE_JSON_PARSE_WITH_SOURCE + if (entries) { + if (auto entry = entries->lookup(id)) { + entryRecord = std::move(entry->value()); + } + } +#endif if (!InternalizeJSONProperty(cx, obj, id, reviver, &entryRecord, &newElement)) { return false; @@ -1838,15 +1920,7 @@ static bool InternalizeJSONProperty( RootedValue keyVal(cx, StringValue(key)); #ifdef ENABLE_JSON_PARSE_WITH_SOURCE - if (cx->realm()->creationOptions().getJSONParseWithSource()) { - RootedObject context(cx, NewPlainObject(cx)); - if (!context) { - return false; - } - Rooted<Value> parseNode(cx, StringValue(parseRecord.get().parseNode)); - if (!DefineDataProperty(cx, context, cx->names().source, parseNode)) { - return false; - } + if (JS::Prefs::experimental_json_parse_with_source()) { RootedValue contextVal(cx, ObjectValue(*context)); return js::Call(cx, reviver, holder, keyVal, val, contextVal, vp); } @@ -1867,7 +1941,7 @@ static bool Revive(JSContext* cx, HandleValue reviver, } #ifdef ENABLE_JSON_PARSE_WITH_SOURCE - MOZ_ASSERT_IF(cx->realm()->creationOptions().getJSONParseWithSource(), + MOZ_ASSERT_IF(JS::Prefs::experimental_json_parse_with_source(), pro.get().value == vp.get()); #endif Rooted<jsid> id(cx, NameToId(cx->names().empty_)); @@ -1876,10 +1950,10 @@ static bool Revive(JSContext* cx, HandleValue reviver, template <typename CharT> bool ParseJSON(JSContext* cx, const mozilla::Range<const CharT> chars, - MutableHandleValue vp, MutableHandle<ParseRecordObject> pro) { + MutableHandleValue vp) { Rooted<JSONParser<CharT>> parser(cx, cx, chars, JSONParser<CharT>::ParseType::JSONParse); - return parser.parse(vp, pro); + return parser.parse(vp); } template <typename CharT> @@ -1888,7 +1962,15 @@ bool js::ParseJSONWithReviver(JSContext* cx, HandleValue reviver, MutableHandleValue vp) { /* https://262.ecma-international.org/14.0/#sec-json.parse steps 2-10. */ Rooted<ParseRecordObject> pro(cx); - if (!ParseJSON(cx, chars, vp, &pro)) { +#ifdef ENABLE_JSON_PARSE_WITH_SOURCE + if (JS::Prefs::experimental_json_parse_with_source() && IsCallable(reviver)) { + Rooted<JSONReviveParser<CharT>> parser(cx, cx, chars); + if (!parser.get().parse(vp, &pro)) { + return false; + } + } else +#endif + if (!ParseJSON(cx, chars, vp)) { return false; } @@ -2086,14 +2168,13 @@ static bool json_parseImmutable(JSContext* cx, unsigned argc, Value* vp) { HandleValue reviver = args.get(1); RootedValue unfiltered(cx); - Rooted<ParseRecordObject> pro(cx); if (linearChars.isLatin1()) { - if (!ParseJSON(cx, linearChars.latin1Range(), &unfiltered, &pro)) { + if (!ParseJSON(cx, linearChars.latin1Range(), &unfiltered)) { return false; } } else { - if (!ParseJSON(cx, linearChars.twoByteRange(), &unfiltered, &pro)) { + if (!ParseJSON(cx, linearChars.twoByteRange(), &unfiltered)) { return false; } } @@ -2103,6 +2184,104 @@ static bool json_parseImmutable(JSContext* cx, unsigned argc, Value* vp) { } #endif +#ifdef ENABLE_JSON_PARSE_WITH_SOURCE +/* https://tc39.es/proposal-json-parse-with-source/#sec-json.israwjson */ +static bool json_isRawJSON(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "JSON", "isRawJSON"); + CallArgs args = CallArgsFromVp(argc, vp); + + /* Step 1. */ + if (args.get(0).isObject()) { + Rooted<JSObject*> obj(cx, &args[0].toObject()); +# ifdef DEBUG + if (obj->is<RawJSONObject>()) { + bool objIsFrozen = false; + MOZ_ASSERT(js::TestIntegrityLevel(cx, obj, IntegrityLevel::Frozen, + &objIsFrozen)); + MOZ_ASSERT(objIsFrozen); + } +# endif // DEBUG + args.rval().setBoolean(obj->is<RawJSONObject>()); + return true; + } + + /* Step 2. */ + args.rval().setBoolean(false); + return true; +} + +static inline bool IsJSONWhitespace(char16_t ch) { + return ch == '\t' || ch == '\n' || ch == '\r' || ch == ' '; +} + +/* https://tc39.es/proposal-json-parse-with-source/#sec-json.rawjson */ +static bool json_rawJSON(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "JSON", "rawJSON"); + CallArgs args = CallArgsFromVp(argc, vp); + + /* Step 1. */ + JSString* jsonString = ToString<CanGC>(cx, args.get(0)); + if (!jsonString) { + return false; + } + + Rooted<JSLinearString*> linear(cx, jsonString->ensureLinear(cx)); + if (!linear) { + return false; + } + + AutoStableStringChars linearChars(cx); + if (!linearChars.init(cx, linear)) { + return false; + } + + /* Step 2. */ + if (linear->empty()) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, + JSMSG_JSON_RAW_EMPTY); + return false; + } + if (IsJSONWhitespace(linear->latin1OrTwoByteChar(0)) || + IsJSONWhitespace(linear->latin1OrTwoByteChar(linear->length() - 1))) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, + JSMSG_JSON_RAW_WHITESPACE); + return false; + } + + /* Step 3. */ + RootedValue parsedValue(cx); + if (linearChars.isLatin1()) { + if (!ParseJSON(cx, linearChars.latin1Range(), &parsedValue)) { + return false; + } + } else { + if (!ParseJSON(cx, linearChars.twoByteRange(), &parsedValue)) { + return false; + } + } + + if (parsedValue.isObject()) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, + JSMSG_JSON_RAW_ARRAY_OR_OBJECT); + return false; + } + + /* Steps 4-6. */ + Rooted<RawJSONObject*> obj(cx, RawJSONObject::create(cx, linear)); + if (!obj) { + return false; + } + + /* Step 7. */ + if (!js::FreezeObject(cx, obj)) { + return false; + } + + args.rval().setObject(*obj); + return true; +} +#endif // ENABLE_JSON_PARSE_WITH_SOURCE + /* https://262.ecma-international.org/14.0/#sec-json.stringify */ bool json_stringify(JSContext* cx, unsigned argc, Value* vp) { AutoJSMethodProfilerEntry pseudoFrame(cx, "JSON", "stringify"); @@ -2141,11 +2320,16 @@ bool json_stringify(JSContext* cx, unsigned argc, Value* vp) { } static const JSFunctionSpec json_static_methods[] = { - JS_FN("toSource", json_toSource, 0, 0), JS_FN("parse", json_parse, 2, 0), + JS_FN("toSource", json_toSource, 0, 0), + JS_FN("parse", json_parse, 2, 0), JS_FN("stringify", json_stringify, 3, 0), #ifdef ENABLE_RECORD_TUPLE JS_FN("parseImmutable", json_parseImmutable, 2, 0), #endif +#ifdef ENABLE_JSON_PARSE_WITH_SOURCE + JS_FN("isRawJSON", json_isRawJSON, 1, 0), + JS_FN("rawJSON", json_rawJSON, 1, 0), +#endif JS_FS_END}; static const JSPropertySpec json_static_properties[] = { diff --git a/js/src/builtin/MapObject.cpp b/js/src/builtin/MapObject.cpp index fd35b8e776..cd4ae7f46a 100644 --- a/js/src/builtin/MapObject.cpp +++ b/js/src/builtin/MapObject.cpp @@ -333,23 +333,42 @@ size_t MapIteratorObject::objectMoved(JSObject* obj, JSObject* old) { Nursery& nursery = iter->runtimeFromMainThread()->gc.nursery(); if (!nursery.isInside(range)) { nursery.removeMallocedBufferDuringMinorGC(range); - return 0; } + size_t size = RoundUp(sizeof(ValueMap::Range), gc::CellAlignBytes); AutoEnterOOMUnsafeRegion oomUnsafe; - auto newRange = iter->zone()->new_<ValueMap::Range>(*range); - if (!newRange) { - oomUnsafe.crash( - "MapIteratorObject failed to allocate Range data while tenuring."); + void* buffer = nursery.allocateBufferSameLocation(obj, size, js::MallocArena); + if (!buffer) { + oomUnsafe.crash("MapIteratorObject::objectMoved"); } + bool iteratorIsInNursery = IsInsideNursery(obj); + MOZ_ASSERT(iteratorIsInNursery == nursery.isInside(buffer)); + auto* newRange = new (buffer) ValueMap::Range(*range, iteratorIsInNursery); range->~Range(); iter->setReservedSlot(MapIteratorObject::RangeSlot, PrivateValue(newRange)); - return sizeof(ValueMap::Range); + + if (iteratorIsInNursery && iter->target()) { + SetHasNurseryMemory(iter->target(), true); + } + + return size; +} + +MapObject* MapIteratorObject::target() const { + Value value = getFixedSlot(TargetSlot); + if (value.isUndefined()) { + return nullptr; + } + + return &MaybeForwarded(&value.toObject())->as<MapObject>(); } template <typename Range> static void DestroyRange(JSObject* iterator, Range* range) { + MOZ_ASSERT(IsInsideNursery(iterator) == + iterator->runtimeFromMainThread()->gc.nursery().isInside(range)); + range->~Range(); if (!IsInsideNursery(iterator)) { js_free(range); @@ -533,7 +552,7 @@ void MapObject::trace(JSTracer* trc, JSObject* obj) { } } -using NurseryKeysVector = mozilla::Vector<Value, 0, SystemAllocPolicy>; +using NurseryKeysVector = GCVector<Value, 0, SystemAllocPolicy>; template <typename TableObject> static NurseryKeysVector* GetNurseryKeys(TableObject* t) { @@ -577,17 +596,34 @@ class js::OrderedHashTableRef : public gc::BufferableRef { reinterpret_cast<typename ObjectT::UnbarrieredTable*>(realTable); NurseryKeysVector* keys = GetNurseryKeys(object); MOZ_ASSERT(keys); - for (Value key : *keys) { - MOZ_ASSERT(unbarrieredTable->hash(key) == - realTable->hash(*reinterpret_cast<HashableValue*>(&key))); - // Note: we use a lambda to avoid tenuring keys that have been removed - // from the Map or Set. - unbarrieredTable->rekeyOneEntry(key, [trc](const Value& prior) { - Value key = prior; - TraceManuallyBarrieredEdge(trc, &key, "ordered hash table key"); - return key; - }); + + keys->mutableEraseIf([&](Value& key) { + MOZ_ASSERT( + unbarrieredTable->hash(key) == + realTable->hash(*reinterpret_cast<const HashableValue*>(&key))); + MOZ_ASSERT(IsInsideNursery(key.toGCThing())); + + auto result = + unbarrieredTable->rekeyOneEntry(key, [trc](const Value& prior) { + Value key = prior; + TraceManuallyBarrieredEdge(trc, &key, "ordered hash table key"); + return key; + }); + + if (result.isNothing()) { + return true; // Key removed. + } + + key = result.value(); + return !IsInsideNursery(key.toGCThing()); + }); + + if (!keys->empty()) { + trc->runtime()->gc.storeBuffer().putGeneric( + OrderedHashTableRef<ObjectT>(object)); + return; } + DeleteNurseryKeys(object); } }; @@ -739,6 +775,8 @@ void MapObject::finalize(JS::GCContext* gcx, JSObject* obj) { return; } + MOZ_ASSERT_IF(obj->isTenured(), !table->hasNurseryRanges()); + bool needsPostBarriers = obj->isTenured(); if (needsPostBarriers) { // Use the ValueMap representation which has post barriers. @@ -750,21 +788,36 @@ void MapObject::finalize(JS::GCContext* gcx, JSObject* obj) { } } +void MapObject::clearNurseryRangesBeforeMinorGC() { + getTableUnchecked()->destroyNurseryRanges(); + SetHasNurseryMemory(this, false); +} + /* static */ -void MapObject::sweepAfterMinorGC(JS::GCContext* gcx, MapObject* mapobj) { - bool wasInsideNursery = IsInsideNursery(mapobj); - if (wasInsideNursery && !IsForwarded(mapobj)) { +MapObject* MapObject::sweepAfterMinorGC(JS::GCContext* gcx, MapObject* mapobj) { + Nursery& nursery = gcx->runtime()->gc.nursery(); + bool wasInCollectedRegion = nursery.inCollectedRegion(mapobj); + if (wasInCollectedRegion && !IsForwarded(mapobj)) { finalize(gcx, mapobj); - return; + return nullptr; } mapobj = MaybeForwarded(mapobj); - mapobj->getTableUnchecked()->destroyNurseryRanges(); - SetHasNurseryMemory(mapobj, false); - if (wasInsideNursery) { + bool insideNursery = IsInsideNursery(mapobj); + if (insideNursery) { + SetHasNurseryMemory(mapobj, true); + } + + if (wasInCollectedRegion && mapobj->isTenured()) { AddCellMemory(mapobj, sizeof(ValueMap), MemoryUse::MapObjectTable); } + + if (!HasNurseryMemory(mapobj)) { + return nullptr; + } + + return mapobj; } bool MapObject::construct(JSContext* cx, unsigned argc, Value* vp) { @@ -1199,19 +1252,36 @@ size_t SetIteratorObject::objectMoved(JSObject* obj, JSObject* old) { Nursery& nursery = iter->runtimeFromMainThread()->gc.nursery(); if (!nursery.isInside(range)) { nursery.removeMallocedBufferDuringMinorGC(range); - return 0; } + size_t size = RoundUp(sizeof(ValueSet::Range), gc::CellAlignBytes); + ; AutoEnterOOMUnsafeRegion oomUnsafe; - auto newRange = iter->zone()->new_<ValueSet::Range>(*range); - if (!newRange) { - oomUnsafe.crash( - "SetIteratorObject failed to allocate Range data while tenuring."); + void* buffer = nursery.allocateBufferSameLocation(obj, size, js::MallocArena); + if (!buffer) { + oomUnsafe.crash("SetIteratorObject::objectMoved"); } + bool iteratorIsInNursery = IsInsideNursery(obj); + MOZ_ASSERT(iteratorIsInNursery == nursery.isInside(buffer)); + auto* newRange = new (buffer) ValueSet::Range(*range, iteratorIsInNursery); range->~Range(); iter->setReservedSlot(SetIteratorObject::RangeSlot, PrivateValue(newRange)); - return sizeof(ValueSet::Range); + + if (iteratorIsInNursery && iter->target()) { + SetHasNurseryMemory(iter->target(), true); + } + + return size; +} + +SetObject* SetIteratorObject::target() const { + Value value = getFixedSlot(TargetSlot); + if (value.isUndefined()) { + return nullptr; + } + + return &MaybeForwarded(&value.toObject())->as<SetObject>(); } bool SetIteratorObject::next(SetIteratorObject* setIterator, @@ -1449,25 +1519,41 @@ void SetObject::finalize(JS::GCContext* gcx, JSObject* obj) { MOZ_ASSERT(gcx->onMainThread()); SetObject* setobj = static_cast<SetObject*>(obj); if (ValueSet* set = setobj->getData()) { + MOZ_ASSERT_IF(obj->isTenured(), !set->hasNurseryRanges()); gcx->delete_(obj, set, MemoryUse::MapObjectTable); } } +void SetObject::clearNurseryRangesBeforeMinorGC() { + getTableUnchecked()->destroyNurseryRanges(); + SetHasNurseryMemory(this, false); +} + /* static */ -void SetObject::sweepAfterMinorGC(JS::GCContext* gcx, SetObject* setobj) { - bool wasInsideNursery = IsInsideNursery(setobj); - if (wasInsideNursery && !IsForwarded(setobj)) { +SetObject* SetObject::sweepAfterMinorGC(JS::GCContext* gcx, SetObject* setobj) { + Nursery& nursery = gcx->runtime()->gc.nursery(); + bool wasInCollectedRegion = nursery.inCollectedRegion(setobj); + if (wasInCollectedRegion && !IsForwarded(setobj)) { finalize(gcx, setobj); - return; + return nullptr; } setobj = MaybeForwarded(setobj); - setobj->getData()->destroyNurseryRanges(); - SetHasNurseryMemory(setobj, false); - if (wasInsideNursery) { + bool insideNursery = IsInsideNursery(setobj); + if (insideNursery) { + SetHasNurseryMemory(setobj, true); + } + + if (wasInCollectedRegion && setobj->isTenured()) { AddCellMemory(setobj, sizeof(ValueSet), MemoryUse::MapObjectTable); } + + if (!HasNurseryMemory(setobj)) { + return nullptr; + } + + return setobj; } bool SetObject::isBuiltinAdd(HandleValue add) { diff --git a/js/src/builtin/MapObject.h b/js/src/builtin/MapObject.h index ef37b9912e..d47797eff7 100644 --- a/js/src/builtin/MapObject.h +++ b/js/src/builtin/MapObject.h @@ -168,7 +168,14 @@ class MapObject : public NativeObject { OrderedHashMap<Value, Value, UnbarrieredHashPolicy, CellAllocPolicy>; friend class OrderedHashTableRef<MapObject>; - static void sweepAfterMinorGC(JS::GCContext* gcx, MapObject* mapobj); + void clearNurseryRangesBeforeMinorGC(); + + // Sweeps a map that had nursery memory associated with it after a minor + // GC. This may finalize the map if it was in the nursery and has died. + // + // Returns a pointer to the map if it still has nursery memory associated with + // it, or nullptr. + static MapObject* sweepAfterMinorGC(JS::GCContext* gcx, MapObject* mapobj); size_t sizeOfData(mozilla::MallocSizeOf mallocSizeOf); @@ -276,6 +283,7 @@ class MapIteratorObject : public NativeObject { private: inline MapObject::IteratorKind kind() const; + MapObject* target() const; }; class SetObject : public NativeObject { @@ -326,7 +334,14 @@ class SetObject : public NativeObject { OrderedHashSet<Value, UnbarrieredHashPolicy, CellAllocPolicy>; friend class OrderedHashTableRef<SetObject>; - static void sweepAfterMinorGC(JS::GCContext* gcx, SetObject* setobj); + void clearNurseryRangesBeforeMinorGC(); + + // Sweeps a set that had nursery memory associated with it after a minor + // GC. This may finalize the set if it was in the nursery and has died. + // + // Returns a pointer to the set if it still has nursery memory associated with + // it, or nullptr. + static SetObject* sweepAfterMinorGC(JS::GCContext* gcx, SetObject* setobj); size_t sizeOfData(mozilla::MallocSizeOf mallocSizeOf); @@ -414,6 +429,7 @@ class SetIteratorObject : public NativeObject { private: inline SetObject::IteratorKind kind() const; + SetObject* target() const; }; using SetInitGetPrototypeOp = NativeObject* (*)(JSContext*, diff --git a/js/src/builtin/ModuleObject.cpp b/js/src/builtin/ModuleObject.cpp index b9db9bf02d..0365f744a6 100644 --- a/js/src/builtin/ModuleObject.cpp +++ b/js/src/builtin/ModuleObject.cpp @@ -1109,6 +1109,17 @@ JSScript* ModuleObject::script() const { return ptr; } +const char* ModuleObject::filename() const { + // The ScriptSlot will be cleared once the module is evaluated, so we try to + // get the filename from cyclicModuleFields(). + + // TODO: Bug 1885483: Provide filename for JSON modules + if (!hasCyclicModuleFields()) { + return "(JSON module)"; + } + return cyclicModuleFields()->scriptSourceObject->source()->filename(); +} + static inline void AssertValidModuleStatus(ModuleStatus status) { MOZ_ASSERT(status >= ModuleStatus::Unlinked && status <= ModuleStatus::Evaluated_Error); diff --git a/js/src/builtin/ModuleObject.h b/js/src/builtin/ModuleObject.h index d39d65b18c..cb74d67c40 100644 --- a/js/src/builtin/ModuleObject.h +++ b/js/src/builtin/ModuleObject.h @@ -360,6 +360,7 @@ class ModuleObject : public NativeObject { JSScript* maybeScript() const; JSScript* script() const; + const char* filename() const; ModuleEnvironmentObject& initialEnvironment() const; ModuleEnvironmentObject* environment() const; ModuleNamespaceObject* namespace_(); diff --git a/js/src/builtin/ParseRecordObject.cpp b/js/src/builtin/ParseRecordObject.cpp index a453c30c0e..efb05d1845 100644 --- a/js/src/builtin/ParseRecordObject.cpp +++ b/js/src/builtin/ParseRecordObject.cpp @@ -19,8 +19,24 @@ ParseRecordObject::ParseRecordObject(Handle<js::JSONParseNode*> parseNode, const Value& val) : parseNode(parseNode), key(JS::PropertyKey::Void()), value(val) {} +bool ParseRecordObject::addEntries(JSContext* cx, EntryMap&& appendEntries) { + if (!entries) { + entries = js::MakeUnique<EntryMap>(std::move(appendEntries)); + return !!entries; + } + for (auto iter = appendEntries.iter(); !iter.done(); iter.next()) { + if (!entries->put(iter.get().key(), std::move(iter.get().value()))) { + return false; + } + } + return true; +} + void ParseRecordObject::trace(JSTracer* trc) { JS::TraceRoot(trc, &parseNode, "ParseRecordObject parse node"); JS::TraceRoot(trc, &key, "ParseRecordObject key"); JS::TraceRoot(trc, &value, "ParseRecordObject value"); + if (entries) { + entries->trace(trc); + } } diff --git a/js/src/builtin/ParseRecordObject.h b/js/src/builtin/ParseRecordObject.h index 60a902f19b..7e3176a23f 100644 --- a/js/src/builtin/ParseRecordObject.h +++ b/js/src/builtin/ParseRecordObject.h @@ -7,7 +7,8 @@ #ifndef builtin_ParseRecordObject_h #define builtin_ParseRecordObject_h -#include "js/TraceKind.h" +#include "js/HashTable.h" +#include "js/TracingAPI.h" #include "vm/JSContext.h" namespace js { @@ -16,24 +17,40 @@ using JSONParseNode = JSString; class ParseRecordObject { public: + using EntryMap = js::GCHashMap<PropertyKey, ParseRecordObject>; + + // The source text that was parsed for this record. According to the spec, we + // don't track this for objects and arrays, so it will be a null pointer. JSONParseNode* parseNode; + // For object members, the member key. For arrays, the index. For JSON + // primitives, it will be undefined. JS::PropertyKey key; + // The original value corresponding to this record, used to determine if the + // reviver function has modified it. Value value; + // For objects and arrays, the records for the members and elements + // (respectively). If there are none, or for JSON primitives, we won't + // allocate an EntryMap. + UniquePtr<EntryMap> entries; ParseRecordObject(); ParseRecordObject(Handle<js::JSONParseNode*> parseNode, const Value& val); ParseRecordObject(ParseRecordObject&& other) : parseNode(std::move(other.parseNode)), key(std::move(other.key)), - value(std::move(other.value)) {} + value(std::move(other.value)), + entries(std::move(other.entries)) {} bool isEmpty() const { return value.isUndefined(); } + bool addEntries(JSContext* cx, EntryMap&& appendEntries); + // move assignment ParseRecordObject& operator=(ParseRecordObject&& other) noexcept { parseNode = other.parseNode; key = other.key; value = other.value; + entries = std::move(other.entries); return *this; } diff --git a/js/src/builtin/Promise.cpp b/js/src/builtin/Promise.cpp index bd40add77f..92e72b9cb9 100644 --- a/js/src/builtin/Promise.cpp +++ b/js/src/builtin/Promise.cpp @@ -940,13 +940,7 @@ static JSFunction* GetRejectFunctionFromResolve(JSFunction* resolve); /** * Returns Promise Resolve Function's [[AlreadyResolved]].[[Value]]. */ -static bool IsAlreadyResolvedMaybeWrappedResolveFunction( - JSObject* resolveFunObj) { - if (IsWrapper(resolveFunObj)) { - resolveFunObj = UncheckedUnwrap(resolveFunObj); - } - - JSFunction* resolveFun = &resolveFunObj->as<JSFunction>(); +static bool IsAlreadyResolvedResolveFunction(JSFunction* resolveFun) { MOZ_ASSERT(resolveFun->maybeNative() == ResolvePromiseFunction); bool alreadyResolved = @@ -970,13 +964,7 @@ static bool IsAlreadyResolvedMaybeWrappedResolveFunction( /** * Returns Promise Reject Function's [[AlreadyResolved]].[[Value]]. */ -static bool IsAlreadyResolvedMaybeWrappedRejectFunction( - JSObject* rejectFunObj) { - if (IsWrapper(rejectFunObj)) { - rejectFunObj = UncheckedUnwrap(rejectFunObj); - } - - JSFunction* rejectFun = &rejectFunObj->as<JSFunction>(); +static bool IsAlreadyResolvedRejectFunction(JSFunction* rejectFun) { MOZ_ASSERT(rejectFun->maybeNative() == RejectPromiseFunction); bool alreadyResolved = @@ -1023,8 +1011,8 @@ static void SetAlreadyResolvedResolutionFunction(JSFunction* resolutionFun) { reject->setExtendedSlot(RejectFunctionSlot_Promise, UndefinedValue()); reject->setExtendedSlot(RejectFunctionSlot_ResolveFunction, UndefinedValue()); - MOZ_ASSERT(IsAlreadyResolvedMaybeWrappedResolveFunction(resolve)); - MOZ_ASSERT(IsAlreadyResolvedMaybeWrappedRejectFunction(reject)); + MOZ_ASSERT(IsAlreadyResolvedResolveFunction(resolve)); + MOZ_ASSERT(IsAlreadyResolvedRejectFunction(reject)); } /** @@ -1132,8 +1120,8 @@ void js::SetAlreadyResolvedPromiseWithDefaultResolvingFunction( rejectFun->initExtendedSlot(RejectFunctionSlot_ResolveFunction, ObjectValue(*resolveFun)); - MOZ_ASSERT(!IsAlreadyResolvedMaybeWrappedResolveFunction(resolveFun)); - MOZ_ASSERT(!IsAlreadyResolvedMaybeWrappedRejectFunction(rejectFun)); + MOZ_ASSERT(!IsAlreadyResolvedResolveFunction(resolveFun)); + MOZ_ASSERT(!IsAlreadyResolvedRejectFunction(rejectFun)); // Step 12. Return the Record { [[Resolve]]: resolve, [[Reject]]: reject }. return true; @@ -1181,8 +1169,7 @@ static bool RejectPromiseFunction(JSContext* cx, unsigned argc, Value* vp) { // If the Promise isn't available anymore, it has been resolved and the // reference to it removed to make it eligible for collection. bool alreadyResolved = promiseVal.isUndefined(); - MOZ_ASSERT(IsAlreadyResolvedMaybeWrappedRejectFunction(reject) == - alreadyResolved); + MOZ_ASSERT(IsAlreadyResolvedRejectFunction(reject) == alreadyResolved); if (alreadyResolved) { args.rval().setUndefined(); return true; @@ -1362,8 +1349,7 @@ static bool ResolvePromiseFunction(JSContext* cx, unsigned argc, Value* vp) { // // NOTE: We use the reference to the reject function as [[AlreadyResolved]]. bool alreadyResolved = promiseVal.isUndefined(); - MOZ_ASSERT(IsAlreadyResolvedMaybeWrappedResolveFunction(resolve) == - alreadyResolved); + MOZ_ASSERT(IsAlreadyResolvedResolveFunction(resolve) == alreadyResolved); if (alreadyResolved) { args.rval().setUndefined(); return true; @@ -3164,7 +3150,8 @@ static bool PromiseAllResolveElementFunction(JSContext* cx, unsigned argc, for (size_t i = 0, len = promises.length(); i < len; i++) { JSObject* obj = promises[i]; cx->check(obj); - MOZ_ASSERT(UncheckedUnwrap(obj)->is<PromiseObject>()); + JSObject* unwrapped = UncheckedUnwrap(obj); + MOZ_ASSERT(unwrapped->is<PromiseObject>() || JS_IsDeadWrapper(unwrapped)); } #endif @@ -3265,7 +3252,13 @@ static bool PromiseAllResolveElementFunction(JSContext* cx, unsigned argc, // compartments with principals inaccessible from the current // compartment. To make that work, it unwraps promises with // UncheckedUnwrap, - nextPromise = &UncheckedUnwrap(nextPromiseObj)->as<PromiseObject>(); + JSObject* unwrapped = UncheckedUnwrap(nextPromiseObj); + if (JS_IsDeadWrapper(unwrapped)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEAD_OBJECT); + return nullptr; + } + nextPromise = &unwrapped->as<PromiseObject>(); if (!PerformPromiseThen(cx, nextPromise, resolveFunVal, rejectFunVal, resultCapabilityWithoutResolving)) { @@ -5036,7 +5029,8 @@ static PromiseReactionRecord* NewReactionRecord( // This is the only case where we allow `resolve` and `reject` to // be null when the `promise` field is not a PromiseObject. JSObject* unwrappedPromise = UncheckedUnwrap(resultCapability.promise()); - MOZ_ASSERT(unwrappedPromise->is<PromiseObject>()); + MOZ_ASSERT(unwrappedPromise->is<PromiseObject>() || + JS_IsDeadWrapper(unwrappedPromise)); MOZ_ASSERT(!resultCapability.resolve()); MOZ_ASSERT(!resultCapability.reject()); } @@ -6218,6 +6212,11 @@ bool js::Promise_then(JSContext* cx, unsigned argc, Value* vp) { return true; } + if (JS_IsDeadWrapper(UncheckedUnwrap(dependentPromise))) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEAD_OBJECT); + return false; + } + // `dependentPromise` should be a maybe-wrapped Promise. MOZ_ASSERT(UncheckedUnwrap(dependentPromise)->is<PromiseObject>()); @@ -6934,7 +6933,8 @@ JS::AutoDebuggerJobQueueInterruption::AutoDebuggerJobQueueInterruption() : cx(nullptr) {} JS::AutoDebuggerJobQueueInterruption::~AutoDebuggerJobQueueInterruption() { - MOZ_ASSERT_IF(initialized(), cx->jobQueue->empty()); + MOZ_ASSERT_IF(initialized() && !cx->jobQueue->isDrainingStopped(), + cx->jobQueue->empty()); } bool JS::AutoDebuggerJobQueueInterruption::init(JSContext* cx) { diff --git a/js/src/builtin/RawJSONObject.cpp b/js/src/builtin/RawJSONObject.cpp new file mode 100644 index 0000000000..08a98835c1 --- /dev/null +++ b/js/src/builtin/RawJSONObject.cpp @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "builtin/RawJSONObject.h" +#include "js/PropertyDescriptor.h" + +#include "vm/JSObject-inl.h" + +using namespace js; + +const JSClass RawJSONObject::class_ = {"RawJSON", + JSCLASS_HAS_RESERVED_SLOTS(SlotCount)}; + +/* static */ +RawJSONObject* RawJSONObject::create(JSContext* cx, + Handle<JSString*> jsonString) { + Rooted<RawJSONObject*> obj( + cx, NewObjectWithGivenProto<RawJSONObject>(cx, nullptr)); + if (!obj) { + return nullptr; + } + Rooted<PropertyKey> id(cx, NameToId(cx->names().rawJSON)); + Rooted<Value> jsonStringVal(cx, StringValue(jsonString)); + if (!NativeDefineDataProperty(cx, obj, id, jsonStringVal, 0)) { + return nullptr; + } + return obj; +} + +JSString* RawJSONObject::rawJSON(JSContext* cx) { + // RawJSONObjects are frozen on creation, so must always have a rawJSON string + // property. + PropertyKey id(NameToId(cx->names().rawJSON)); + JS::Value vp; + MOZ_ALWAYS_TRUE(GetPropertyNoGC(cx, this, ObjectValue(*this), id, &vp)); + MOZ_ASSERT(vp.isString()); + return vp.toString(); +} diff --git a/js/src/builtin/RawJSONObject.h b/js/src/builtin/RawJSONObject.h new file mode 100644 index 0000000000..cf8821a844 --- /dev/null +++ b/js/src/builtin/RawJSONObject.h @@ -0,0 +1,27 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef builtin_RawJSONObject_h +#define builtin_RawJSONObject_h + +#include "vm/NativeObject.h" + +namespace js { + +class RawJSONObject : public NativeObject { + enum { SlotCount = 0 }; + + public: + static const JSClass class_; + + static RawJSONObject* create(JSContext* cx, Handle<JSString*> jsonString); + + JSString* rawJSON(JSContext* cx); +}; + +} // namespace js + +#endif /* builtin_RawJSONObject_h */ diff --git a/js/src/builtin/Reflect.js b/js/src/builtin/Reflect.js index f56d603ca3..34ca34af60 100644 --- a/js/src/builtin/Reflect.js +++ b/js/src/builtin/Reflect.js @@ -52,6 +52,8 @@ function Reflect_apply(target, thisArgument, argumentsList) { // Steps 2-4. return callFunction(std_Function_apply, target, thisArgument, argumentsList); } +// This function is only barely too long for normal inlining. +SetIsInlinableLargeFunction(Reflect_apply); // ES2017 draft rev a785b0832b071f505a694e1946182adeab84c972 // 26.1.2 Reflect.construct ( target, argumentsList [ , newTarget ] ) diff --git a/js/src/builtin/Sorting.js b/js/src/builtin/Sorting.js index 4530a82818..175b506192 100644 --- a/js/src/builtin/Sorting.js +++ b/js/src/builtin/Sorting.js @@ -6,7 +6,7 @@ // consolidated here to avoid confusion and re-implementation of existing // algorithms. -// For sorting small arrays. +// For sorting small typed arrays. function InsertionSort(array, from, to, comparefn) { var item, swap, i, j; for (i = from + 1; i <= to; i++) { @@ -22,101 +22,6 @@ function InsertionSort(array, from, to, comparefn) { } } -// A helper function for MergeSort. -// -// Merge comparefn-sorted slices list[start..<=mid] and list[mid+1..<=end], -// storing the merged sequence in out[start..<=end]. -function Merge(list, out, start, mid, end, comparefn) { - // Skip lopsided runs to avoid doing useless work. - // Skip calling the comparator if the sub-list is already sorted. - if ( - mid >= end || - callContentFunction(comparefn, undefined, list[mid], list[mid + 1]) <= 0 - ) { - for (var i = start; i <= end; i++) { - DefineDataProperty(out, i, list[i]); - } - return; - } - - var i = start; - var j = mid + 1; - var k = start; - while (i <= mid && j <= end) { - var lvalue = list[i]; - var rvalue = list[j]; - if (callContentFunction(comparefn, undefined, lvalue, rvalue) <= 0) { - DefineDataProperty(out, k++, lvalue); - i++; - } else { - DefineDataProperty(out, k++, rvalue); - j++; - } - } - - // Empty out any remaining elements. - while (i <= mid) { - DefineDataProperty(out, k++, list[i++]); - } - while (j <= end) { - DefineDataProperty(out, k++, list[j++]); - } -} - -// Helper function for overwriting a sparse array with a -// dense array, filling remaining slots with holes. -function MoveHoles(sparse, sparseLen, dense, denseLen) { - for (var i = 0; i < denseLen; i++) { - sparse[i] = dense[i]; - } - for (var j = denseLen; j < sparseLen; j++) { - delete sparse[j]; - } -} - -// Iterative, bottom up, mergesort. -function MergeSort(array, len, comparefn) { - assert(IsPackedArray(array), "array is packed"); - assert(array.length === len, "length mismatch"); - assert(len > 0, "array should be non-empty"); - - // Insertion sort for small arrays, where "small" is defined by performance - // testing. - if (len < 24) { - InsertionSort(array, 0, len - 1, comparefn); - return array; - } - - // We do all of our allocating up front - var lBuffer = array; - var rBuffer = []; - - // Use insertion sort for initial ranges. - var windowSize = 4; - for (var start = 0; start < len - 1; start += windowSize) { - var end = std_Math_min(start + windowSize - 1, len - 1); - InsertionSort(lBuffer, start, end, comparefn); - } - - for (; windowSize < len; windowSize = 2 * windowSize) { - for (var start = 0; start < len; start += 2 * windowSize) { - // The midpoint between the two subarrays. - var mid = start + windowSize - 1; - - // To keep from going over the edge. - var end = std_Math_min(start + 2 * windowSize - 1, len - 1); - - Merge(lBuffer, rBuffer, start, mid, end, comparefn); - } - - // Swap both lists. - var swap = lBuffer; - lBuffer = rBuffer; - rBuffer = swap; - } - return lBuffer; -} - // A helper function for MergeSortTypedArray. // // Merge comparefn-sorted slices list[start..<=mid] and list[mid+1..<=end], diff --git a/js/src/builtin/TestingFunctions.cpp b/js/src/builtin/TestingFunctions.cpp index 498fa1746d..da7efd2fcc 100644 --- a/js/src/builtin/TestingFunctions.cpp +++ b/js/src/builtin/TestingFunctions.cpp @@ -604,15 +604,6 @@ static bool GetBuildConfiguration(JSContext* cx, unsigned argc, Value* vp) { return false; } -#ifdef ENABLE_JSON_PARSE_WITH_SOURCE - value = BooleanValue(true); -#else - value = BooleanValue(false); -#endif - if (!JS_SetProperty(cx, info, "json-parse-with-source", value)) { - return false; - } - #ifdef FUZZING value = BooleanValue(true); #else @@ -1620,7 +1611,7 @@ static bool WasmLosslessInvoke(JSContext* cx, unsigned argc, Value* vp) { if (!wasmCallFrame.resize(len)) { return false; } - wasmCallFrame[0].set(args.calleev()); + wasmCallFrame[0].set(ObjectValue(*func)); wasmCallFrame[1].set(args.thisv()); // Copy over the arguments needed to invoke the provided wasm function, // skipping the wasm function we're calling that is at `args.get(0)`. @@ -3714,6 +3705,85 @@ static bool NewString(JSContext* cx, unsigned argc, Value* vp) { return true; } +static bool NewDependentString(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + RootedString src(cx, ToString(cx, args.get(0))); + if (!src) { + return false; + } + + uint64_t indexStart = 0; + mozilla::Maybe<uint64_t> indexEnd; + gc::Heap heap = gc::Heap::Default; + mozilla::Maybe<gc::Heap> requiredHeap; + + if (!ToIndex(cx, args.get(1), &indexStart)) { + return false; + } + + Rooted<Value> options(cx); + if (args.get(2).isObject()) { + options = args[2]; + } else { + uint64_t idx; + if (args.hasDefined(2)) { + if (!ToIndex(cx, args.get(2), &idx)) { + return false; + } + indexEnd.emplace(idx); + } + options = args.get(3); + } + + if (options.isObject()) { + Rooted<Value> v(cx); + Rooted<JSObject*> optObj(cx, &options.toObject()); + if (!JS_GetProperty(cx, optObj, "tenured", &v)) { + return false; + } + if (v.isBoolean()) { + requiredHeap.emplace(v.toBoolean() ? gc::Heap::Tenured + : gc::Heap::Default); + heap = *requiredHeap; + } + } + + if (indexEnd.isNothing()) { + // Read the length now that no more JS code can run. + indexEnd.emplace(src->length()); + } + if (indexStart > src->length() || *indexEnd > src->length() || + indexStart >= *indexEnd) { + JS_ReportErrorASCII(cx, "invalid dependent string bounds"); + return false; + } + if (!src->ensureLinear(cx)) { + return false; + } + Rooted<JSString*> result( + cx, js::NewDependentString(cx, src, indexStart, *indexEnd - indexStart, + heap)); + if (!result) { + return false; + } + if (!result->isDependent()) { + JS_ReportErrorASCII(cx, "resulting string is not dependent (too short?)"); + return false; + } + + if (requiredHeap.isSome()) { + MOZ_ASSERT_IF(*requiredHeap == gc::Heap::Tenured, result->isTenured()); + if ((*requiredHeap == gc::Heap::Default) && result->isTenured()) { + JS_ReportErrorASCII(cx, "nursery string created in tenured heap"); + return false; + } + } + + args.rval().setString(result); + return true; +} + // Warning! This will let you create ropes that I'm not sure would be possible // otherwise, specifically: // @@ -7183,7 +7253,7 @@ static bool SetImmutablePrototype(JSContext* cx, unsigned argc, Value* vp) { return true; } -#ifdef DEBUG +#if defined(DEBUG) || defined(JS_JITSPEW) static bool DumpStringRepresentation(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); @@ -7221,7 +7291,6 @@ static bool GetStringRepresentation(JSContext* cx, unsigned argc, Value* vp) { args.rval().setString(rep); return true; } - #endif static bool ParseCompileOptionsForModule(JSContext* cx, @@ -7237,9 +7306,7 @@ static bool ParseCompileOptionsForModule(JSContext* cx, options.setModule(); isModule = true; - // js::ParseCompileOptions should already be called. - if (options.lineno == 0) { - JS_ReportErrorASCII(cx, "Module cannot be compiled with lineNumber == 0"); + if (!ValidateModuleCompileOptions(cx, options)) { return false; } } else { @@ -9435,6 +9502,15 @@ static const JSFunctionSpecWithHelp TestingFunctions[] = { " - maybeExternal: create an external string, unless the data fits within an\n" " inline string. Inline strings may be nursery-allocated."), + JS_FN_HELP("newDependentString", NewDependentString, 2, 0, +"newDependentString(str, indexStart[, indexEnd] [, options])", +" Essentially the same as str.substring() but insist on\n" +" creating a dependent string and failing if not. Also has options to\n" +" control the heap the string object is allocated into:\n" +" \n" +" - tenured: if true, allocate in the tenured heap or throw. If false,\n" +" allocate in the nursery or throw."), + JS_FN_HELP("ensureLinearString", EnsureLinearString, 1, 0, "ensureLinearString(str)", " Ensures str is a linear (non-rope) string and returns it."), @@ -10058,20 +10134,6 @@ JS_FOR_WASM_FEATURES(WASM_FEATURE) "wasmMetadataAnalysis(wasmObject)", " Prints an analysis of the size of metadata on this wasm object.\n"), -#if defined(DEBUG) || defined(JS_JITSPEW) - JS_FN_HELP("dumpObject", DumpObject, 1, 0, -"dumpObject(obj)", -" Dump an internal representation of an object."), - - JS_FN_HELP("dumpValue", DumpValue, 1, 0, -"dumpValue(v)", -" Dump an internal representation of a value."), - - JS_FN_HELP("dumpValueToString", DumpValueToString, 1, 0, -"dumpValue(v)", -" Return a dump of an internal representation of a value."), -#endif - JS_FN_HELP("sharedMemoryEnabled", SharedMemoryEnabled, 0, 0, "sharedMemoryEnabled()", " Return true if SharedArrayBuffer and Atomics are enabled"), @@ -10129,17 +10191,6 @@ JS_FOR_WASM_FEATURES(WASM_FEATURE) " of internal error, or if the operation doesn't even make sense (for example,\n" " because the object is a revoked proxy)."), -#ifdef DEBUG - JS_FN_HELP("dumpStringRepresentation", DumpStringRepresentation, 1, 0, -"dumpStringRepresentation(str)", -" Print a human-readable description of how the string |str| is represented.\n"), - - JS_FN_HELP("stringRepresentation", GetStringRepresentation, 1, 0, -"stringRepresentation(str)", -" Return a human-readable description of how the string |str| is represented.\n"), - -#endif - JS_FN_HELP("allocationMarker", AllocationMarker, 0, 0, "allocationMarker([options])", " Return a freshly allocated object whose [[Class]] name is\n" @@ -10428,6 +10479,29 @@ JS_FN_HELP("getEnvironmentObjectType", GetEnvironmentObjectType, 1, 0, " Return an object describing the calling realm's fuse state, " "as well as the state of any runtime fuses."), +#if defined(DEBUG) || defined(JS_JITSPEW) + JS_FN_HELP("dumpObject", DumpObject, 1, 0, +"dumpObject(obj)", +" Dump an internal representation of an object."), + + JS_FN_HELP("dumpValue", DumpValue, 1, 0, +"dumpValue(v)", +" Dump an internal representation of a value."), + + JS_FN_HELP("dumpValueToString", DumpValueToString, 1, 0, +"dumpValue(v)", +" Return a dump of an internal representation of a value."), + + JS_FN_HELP("dumpStringRepresentation", DumpStringRepresentation, 1, 0, +"dumpStringRepresentation(str)", +" Print a human-readable description of how the string |str| is represented.\n"), + + JS_FN_HELP("stringRepresentation", GetStringRepresentation, 1, 0, +"stringRepresentation(str)", +" Return a human-readable description of how the string |str| is represented.\n"), + +#endif + JS_FS_HELP_END }; // clang-format on diff --git a/js/src/builtin/TestingUtility.cpp b/js/src/builtin/TestingUtility.cpp index 12a99c7f08..9d4af5897c 100644 --- a/js/src/builtin/TestingUtility.cpp +++ b/js/src/builtin/TestingUtility.cpp @@ -308,3 +308,18 @@ bool js::ValidateLazinessOfStencilAndGlobal( return true; } + +bool js::ValidateModuleCompileOptions(JSContext* cx, + JS::CompileOptions& options) { + if (options.lineno == 0) { + JS_ReportErrorASCII(cx, "Module cannot be compiled with lineNumber == 0"); + return false; + } + + if (!options.filename()) { + JS_ReportErrorASCII(cx, "Module should have filename"); + return false; + } + + return true; +} diff --git a/js/src/builtin/TestingUtility.h b/js/src/builtin/TestingUtility.h index 4c666540f7..82225068a1 100644 --- a/js/src/builtin/TestingUtility.h +++ b/js/src/builtin/TestingUtility.h @@ -72,6 +72,8 @@ JSObject* CreateScriptPrivate(JSContext* cx, bool ValidateLazinessOfStencilAndGlobal( JSContext* cx, const frontend::CompilationStencil& stencil); +bool ValidateModuleCompileOptions(JSContext* cx, JS::CompileOptions& options); + } /* namespace js */ #endif /* builtin_TestingUtility_h */ diff --git a/js/src/builtin/Tuple.js b/js/src/builtin/Tuple.js index 98177ac35d..bade1739d0 100644 --- a/js/src/builtin/Tuple.js +++ b/js/src/builtin/Tuple.js @@ -25,7 +25,7 @@ function TupleToSorted(comparefn) { /* Step 3. */ var items = TupleToArray(T); - var sorted = callFunction(ArraySort, items, comparefn); + var sorted = callFunction(std_Array_sort, items, comparefn); return std_Tuple_unchecked(sorted); } diff --git a/js/src/builtin/WeakMapObject-inl.h b/js/src/builtin/WeakMapObject-inl.h index 28ab1abeb2..3c5f2ceb27 100644 --- a/js/src/builtin/WeakMapObject-inl.h +++ b/js/src/builtin/WeakMapObject-inl.h @@ -92,6 +92,21 @@ static MOZ_ALWAYS_INLINE bool CanBeHeldWeakly(JSContext* cx, return false; } +static unsigned GetErrorNumber(bool isWeakMap) { +#ifdef NIGHTLY_BUILD + bool symbolsAsWeakMapKeysEnabled = + JS::Prefs::experimental_symbols_as_weakmap_keys(); + + if (symbolsAsWeakMapKeysEnabled) { + return isWeakMap ? JSMSG_WEAKMAP_KEY_CANT_BE_HELD_WEAKLY + : JSMSG_WEAKSET_VAL_CANT_BE_HELD_WEAKLY; + } +#endif + + return isWeakMap ? JSMSG_WEAKMAP_KEY_MUST_BE_AN_OBJECT + : JSMSG_WEAKSET_VAL_MUST_BE_AN_OBJECT; +} + } // namespace js #endif /* builtin_WeakMapObject_inl_h */ diff --git a/js/src/builtin/WeakMapObject.cpp b/js/src/builtin/WeakMapObject.cpp index 680f49bc3e..a9d7dcdedc 100644 --- a/js/src/builtin/WeakMapObject.cpp +++ b/js/src/builtin/WeakMapObject.cpp @@ -122,8 +122,8 @@ bool WeakMapObject::delete_(JSContext* cx, unsigned argc, Value* vp) { MOZ_ASSERT(WeakMapObject::is(args.thisv())); if (!CanBeHeldWeakly(cx, args.get(0))) { - ReportValueError(cx, JSMSG_WEAKMAP_KEY_CANT_BE_HELD_WEAKLY, - JSDVG_IGNORE_STACK, args.get(0), nullptr); + unsigned errorNum = GetErrorNumber(true); + ReportValueError(cx, errorNum, JSDVG_IGNORE_STACK, args.get(0), nullptr); return false; } @@ -238,8 +238,8 @@ JS_PUBLIC_API bool JS::SetWeakMapEntry(JSContext* cx, HandleObject mapObj, CHECK_THREAD(cx); cx->check(key, val); if (!CanBeHeldWeakly(cx, key)) { - ReportValueError(cx, JSMSG_WEAKMAP_KEY_CANT_BE_HELD_WEAKLY, - JSDVG_IGNORE_STACK, key, nullptr); + unsigned errorNum = GetErrorNumber(true); + ReportValueError(cx, errorNum, JSDVG_IGNORE_STACK, key, nullptr); return false; } diff --git a/js/src/builtin/WeakSetObject.cpp b/js/src/builtin/WeakSetObject.cpp index 06e190f51e..3705e94218 100644 --- a/js/src/builtin/WeakSetObject.cpp +++ b/js/src/builtin/WeakSetObject.cpp @@ -31,8 +31,8 @@ using namespace js; // Step 4. if (!CanBeHeldWeakly(cx, args.get(0))) { - ReportValueError(cx, JSMSG_WEAKSET_VAL_CANT_BE_HELD_WEAKLY, - JSDVG_IGNORE_STACK, args.get(0), nullptr); + unsigned errorNum = GetErrorNumber(false); + ReportValueError(cx, errorNum, JSDVG_IGNORE_STACK, args.get(0), nullptr); return false; } @@ -198,8 +198,9 @@ bool WeakSetObject::construct(JSContext* cx, unsigned argc, Value* vp) { MOZ_ASSERT(!keyVal.isMagic(JS_ELEMENTS_HOLE)); if (!CanBeHeldWeakly(cx, keyVal)) { - ReportValueError(cx, JSMSG_WEAKSET_VAL_CANT_BE_HELD_WEAKLY, - JSDVG_IGNORE_STACK, keyVal, nullptr); + unsigned errorNum = GetErrorNumber(false); + ReportValueError(cx, errorNum, JSDVG_IGNORE_STACK, args.get(0), + nullptr); return false; } diff --git a/js/src/builtin/embedjs.py b/js/src/builtin/embedjs.py index 6a2c25b999..685f511e49 100644 --- a/js/src/builtin/embedjs.py +++ b/js/src/builtin/embedjs.py @@ -142,7 +142,9 @@ def preprocess(cxx, preprocessorOption, source, args=[]): with open(tmpIn, "wb") as input: input.write(source.encode("utf-8")) - print(" ".join(cxx + outputArg + args + [tmpIn])) + + if os.environ.get("BUILD_VERBOSE_LOG"): + print("Executing:", " ".join(cxx + outputArg + args + [tmpIn])) result = subprocess.Popen(cxx + outputArg + args + [tmpIn]).wait() if result != 0: sys.exit(result) diff --git a/js/src/ctypes/CTypes.cpp b/js/src/ctypes/CTypes.cpp index 1296e6cbb9..c684d4ee22 100644 --- a/js/src/ctypes/CTypes.cpp +++ b/js/src/ctypes/CTypes.cpp @@ -26,9 +26,7 @@ #include <iterator> #include <limits> #include <stdint.h> -#ifdef HAVE_SSIZE_T -# include <sys/types.h> -#endif +#include <sys/types.h> #include <type_traits> #include "jsapi.h" diff --git a/js/src/ctypes/typedefs.h b/js/src/ctypes/typedefs.h index 51b020a09f..062c027496 100644 --- a/js/src/ctypes/typedefs.h +++ b/js/src/ctypes/typedefs.h @@ -34,10 +34,10 @@ #define ctypes_typedefs_h // MSVC doesn't have ssize_t. Help it along a little. -#ifdef HAVE_SSIZE_T -# define CTYPES_SSIZE_T ssize_t -#else +#ifdef _WIN32 # define CTYPES_SSIZE_T intptr_t +#else +# define CTYPES_SSIZE_T ssize_t #endif // Some #defines to make handling of types whose length varies by platform diff --git a/js/src/debugger/DebugAPI-inl.h b/js/src/debugger/DebugAPI-inl.h index 3762fcf05e..77f81b800f 100644 --- a/js/src/debugger/DebugAPI-inl.h +++ b/js/src/debugger/DebugAPI-inl.h @@ -96,6 +96,14 @@ bool DebugAPI::onNewGenerator(JSContext* cx, AbstractFramePtr frame, } /* static */ +void DebugAPI::onGeneratorClosed(JSContext* cx, + AbstractGeneratorObject* genObj) { + if (cx->realm()->isDebuggee()) { + slowPathOnGeneratorClosed(cx, genObj); + } +} + +/* static */ bool DebugAPI::checkNoExecute(JSContext* cx, HandleScript script) { if (!cx->realm()->isDebuggee() || !cx->noExecuteDebuggerTop) { return true; diff --git a/js/src/debugger/DebugAPI.h b/js/src/debugger/DebugAPI.h index c821dd412f..df082ab5ba 100644 --- a/js/src/debugger/DebugAPI.h +++ b/js/src/debugger/DebugAPI.h @@ -342,6 +342,9 @@ class DebugAPI { JSContext* cx, AbstractFramePtr frame, Handle<AbstractGeneratorObject*> genObj); + static inline void onGeneratorClosed(JSContext* cx, + AbstractGeneratorObject* genObj); + // If necessary, record an object that was just allocated for any observing // debuggers. [[nodiscard]] static inline bool onLogAllocationSite( @@ -371,6 +374,8 @@ class DebugAPI { [[nodiscard]] static bool slowPathOnNewGenerator( JSContext* cx, AbstractFramePtr frame, Handle<AbstractGeneratorObject*> genObj); + static void slowPathOnGeneratorClosed(JSContext* cx, + AbstractGeneratorObject* genObj); [[nodiscard]] static bool slowPathCheckNoExecute(JSContext* cx, HandleScript script); [[nodiscard]] static bool slowPathOnEnterFrame(JSContext* cx, diff --git a/js/src/debugger/Debugger.cpp b/js/src/debugger/Debugger.cpp index 8a99f8d4fe..37c1e79a9d 100644 --- a/js/src/debugger/Debugger.cpp +++ b/js/src/debugger/Debugger.cpp @@ -1794,7 +1794,7 @@ static bool CheckResumptionValue(JSContext* cx, AbstractFramePtr frame, } // 2. The generator must be closed. - genObj->setClosed(); + genObj->setClosed(cx); // Async generators have additionally bookkeeping which must be adjusted // when switching over to the closed state. @@ -1823,7 +1823,7 @@ static bool CheckResumptionValue(JSContext* cx, AbstractFramePtr frame, vp.setObject(*promise); // 2. The generator must be closed. - generator->setClosed(); + generator->setClosed(cx); } else { // We're before entering the actual function code. @@ -2855,6 +2855,20 @@ void DebugAPI::slowPathOnNewGlobalObject(JSContext* cx, } /* static */ +void DebugAPI::slowPathOnGeneratorClosed(JSContext* cx, + AbstractGeneratorObject* genObj) { + JS::AutoAssertNoGC nogc; + for (Realm::DebuggerVectorEntry& entry : cx->global()->getDebuggers(nogc)) { + Debugger* dbg = entry.dbg; + if (Debugger::GeneratorWeakMap::Ptr frameEntry = + dbg->generatorFrames.lookup(genObj)) { + DebuggerFrame* frameObj = frameEntry->value(); + frameObj->onGeneratorClosed(cx->gcContext()); + } + } +} + +/* static */ void DebugAPI::slowPathNotifyParticipatesInGC(uint64_t majorGCNumber, Realm::DebuggerVector& dbgs, const JS::AutoRequireNoGC& nogc) { diff --git a/js/src/debugger/Environment.cpp b/js/src/debugger/Environment.cpp index 9c90573c25..8549b47471 100644 --- a/js/src/debugger/Environment.cpp +++ b/js/src/debugger/Environment.cpp @@ -289,12 +289,12 @@ bool DebuggerEnvironment::CallData::findMethod() { return false; } - if (!environment->requireDebuggee(cx)) { + RootedId id(cx); + if (!ValueToIdentifier(cx, args[0], &id)) { return false; } - RootedId id(cx); - if (!ValueToIdentifier(cx, args[0], &id)) { + if (!environment->requireDebuggee(cx)) { return false; } @@ -312,12 +312,12 @@ bool DebuggerEnvironment::CallData::getVariableMethod() { return false; } - if (!environment->requireDebuggee(cx)) { + RootedId id(cx); + if (!ValueToIdentifier(cx, args[0], &id)) { return false; } - RootedId id(cx); - if (!ValueToIdentifier(cx, args[0], &id)) { + if (!environment->requireDebuggee(cx)) { return false; } @@ -329,12 +329,12 @@ bool DebuggerEnvironment::CallData::setVariableMethod() { return false; } - if (!environment->requireDebuggee(cx)) { + RootedId id(cx); + if (!ValueToIdentifier(cx, args[0], &id)) { return false; } - RootedId id(cx); - if (!ValueToIdentifier(cx, args[0], &id)) { + if (!environment->requireDebuggee(cx)) { return false; } diff --git a/js/src/debugger/Frame.cpp b/js/src/debugger/Frame.cpp index 9767e2e7ed..63978b90ab 100644 --- a/js/src/debugger/Frame.cpp +++ b/js/src/debugger/Frame.cpp @@ -439,6 +439,24 @@ void DebuggerFrame::terminate(JS::GCContext* gcx, AbstractFramePtr frame) { gcx->delete_(this, info, MemoryUse::DebuggerFrameGeneratorInfo); } +void DebuggerFrame::onGeneratorClosed(JS::GCContext* gcx) { + GeneratorInfo* info = generatorInfo(); + + // If the generator is closed, eagerly drop the onStep handler, to make sure + // the stepper counter matches in the assertion in DebugAPI::onSingleStep. + // Also clear the slot in order to suppress the decrementStepperCounter in + // DebuggerFrame::terminate. + if (!info->isGeneratorScriptAboutToBeFinalized()) { + JSScript* generatorScript = info->generatorScript(); + OnStepHandler* handler = onStepHandler(); + if (handler) { + decrementStepperCounter(gcx, generatorScript); + setReservedSlot(ONSTEP_HANDLER_SLOT, UndefinedValue()); + handler->drop(gcx, this); + } + } +} + void DebuggerFrame::suspend(JS::GCContext* gcx) { // There must be generator info because otherwise this would be the same // overall behavior as terminate() except that here we do not properly diff --git a/js/src/debugger/Frame.h b/js/src/debugger/Frame.h index 675accbcf7..44ee39d9db 100644 --- a/js/src/debugger/Frame.h +++ b/js/src/debugger/Frame.h @@ -287,6 +287,7 @@ class DebuggerFrame : public NativeObject { FrameIter getFrameIter(JSContext* cx); void terminate(JS::GCContext* gcx, AbstractFramePtr frame); + void onGeneratorClosed(JS::GCContext* gcx); void suspend(JS::GCContext* gcx); [[nodiscard]] bool replaceFrameIterData(JSContext* cx, const FrameIter&); diff --git a/js/src/devtools/automation/variants/rootanalysis b/js/src/devtools/automation/variants/rootanalysis index 7c0fb5242b..89da2eaf1a 100644 --- a/js/src/devtools/automation/variants/rootanalysis +++ b/js/src/devtools/automation/variants/rootanalysis @@ -4,6 +4,6 @@ "debug": true, "env": { "JS_GC_ZEAL": "GenerationalGC", - "JSTESTS_EXTRA_ARGS": "--jitflags=debug" + "JSTESTS_EXTRA_ARGS": "--jitflags=debug --args='--gc-param=semispaceNurseryEnabled=1'" } } diff --git a/js/src/ds/BitArray.h b/js/src/ds/BitArray.h index bdd78873fd..f8a6aad455 100644 --- a/js/src/ds/BitArray.h +++ b/js/src/ds/BitArray.h @@ -99,6 +99,12 @@ class BitArray { return map[elementIndex]; } + // Update a word at a time. + void setWord(size_t elementIndex, WordT value) { + MOZ_ASSERT(elementIndex < nbits); + map[elementIndex] = value; + } + static void getIndexAndMask(size_t offset, size_t* indexp, WordT* maskp) { MOZ_ASSERT(offset < nbits); static_assert(bitsPerElement == 32, "unexpected bitsPerElement value"); diff --git a/js/src/ds/Bitmap.h b/js/src/ds/Bitmap.h index 6770585a61..f564e2a328 100644 --- a/js/src/ds/Bitmap.h +++ b/js/src/ds/Bitmap.h @@ -162,8 +162,26 @@ class SparseBitmap { void bitwiseOrWith(const SparseBitmap& other); void bitwiseOrInto(DenseBitmap& other) const; - // Currently, this API only supports a range of words that is in a single bit - // block. + // Currently, the following APIs only supports a range of words that is in a + // single bit block. + + template <typename T> + typename std::enable_if_t<std::is_convertible_v<T, uintptr_t>, void> + bitwiseAndRangeWith(size_t wordStart, size_t numWords, T* source) { + size_t blockWord = blockStartWord(wordStart); + + // We only support using a single bit block in this API. + MOZ_ASSERT(numWords && + (blockWord == blockStartWord(wordStart + numWords - 1))); + + BitBlock* block = getBlock(blockWord / WordsInBlock); + if (block) { + for (size_t i = 0; i < numWords; i++) { + (*block)[wordStart - blockWord + i] &= source[i]; + } + } + } + template <typename T> typename std::enable_if_t<std::is_convertible_v<T, uintptr_t>, void> bitwiseOrRangeInto(size_t wordStart, size_t numWords, T* target) const { diff --git a/js/src/ds/OrderedHashTable.h b/js/src/ds/OrderedHashTable.h index 3ef366abc3..75c22bd123 100644 --- a/js/src/ds/OrderedHashTable.h +++ b/js/src/ds/OrderedHashTable.h @@ -39,6 +39,7 @@ #include "mozilla/HashFunctions.h" #include "mozilla/Likely.h" +#include "mozilla/Maybe.h" #include "mozilla/MemoryReporting.h" #include "mozilla/TemplateLib.h" @@ -85,9 +86,16 @@ class OrderedHashTable { uint32_t dataCapacity; // size of data, in elements uint32_t liveCount; // dataLength less empty (removed) entries uint32_t hashShift; // multiplicative hash shift - Range* ranges; // list of all live Ranges on this table in malloc memory - Range* - nurseryRanges; // list of all live Ranges on this table in the GC nursery + + // List of all live Ranges on this table in malloc memory. Populated when + // ranges are created. + Range* ranges; + + // List of all live Ranges on this table in the GC nursery. Populated when + // ranges are created. This is cleared at the start of minor GC and rebuilt + // when ranges are moved. + Range* nurseryRanges; + AllocPolicy alloc; mozilla::HashCodeScrambler hcs; // don't reveal pointer hash codes @@ -382,22 +390,28 @@ class OrderedHashTable { next->prevp = &next; } seek(); + MOZ_ASSERT(valid()); } public: - Range(const Range& other) + Range(const Range& other, bool inNursery) : ht(other.ht), i(other.i), count(other.count), - prevp(&ht->ranges), - next(ht->ranges) { + prevp(inNursery ? &ht->nurseryRanges : &ht->ranges), + next(*prevp) { *prevp = this; if (next) { next->prevp = &next; } + MOZ_ASSERT(valid()); } ~Range() { + if (!prevp) { + // Head of removed nursery ranges. + return; + } *prevp = next; if (next) { next->prevp = prevp; @@ -446,12 +460,15 @@ class OrderedHashTable { i = count = 0; } - bool valid() const { return next != this; } +#ifdef DEBUG + bool valid() const { return /* *prevp == this && */ next != this; } +#endif void onTableDestroyed() { MOZ_ASSERT(valid()); prevp = &next; next = this; + MOZ_ASSERT(!valid()); } public: @@ -559,9 +576,6 @@ class OrderedHashTable { /* * Allocate a new Range, possibly in nursery memory. The buffer must be * large enough to hold a Range object. - * - * All nursery-allocated ranges can be freed in one go by calling - * destroyNurseryRanges(). */ Range* createRange(void* buffer, bool inNursery) const { auto* self = const_cast<OrderedHashTable*>(this); @@ -570,7 +584,16 @@ class OrderedHashTable { return static_cast<Range*>(buffer); } - void destroyNurseryRanges() { nurseryRanges = nullptr; } + void destroyNurseryRanges() { + if (nurseryRanges) { + nurseryRanges->prevp = nullptr; + } + nurseryRanges = nullptr; + } + +#ifdef DEBUG + bool hasNurseryRanges() const { return nurseryRanges; } +#endif /* * Change the value of the given key. @@ -959,13 +982,17 @@ class OrderedHashMap { HashNumber hash(const Lookup& key) const { return impl.prepareHash(key); } template <typename GetNewKey> - void rekeyOneEntry(const Lookup& current, const GetNewKey& getNewKey) { + mozilla::Maybe<Key> rekeyOneEntry(Lookup& current, GetNewKey&& getNewKey) { + // TODO: This is inefficient because we also look up the entry in + // impl.rekeyOneEntry below. const Entry* e = get(current); if (!e) { - return; + return mozilla::Nothing(); } + Key newKey = getNewKey(current); - return impl.rekeyOneEntry(current, newKey, Entry(newKey, e->value)); + impl.rekeyOneEntry(current, newKey, Entry(newKey, e->value)); + return mozilla::Some(newKey); } Range* createRange(void* buffer, bool inNursery) const { @@ -973,6 +1000,9 @@ class OrderedHashMap { } void destroyNurseryRanges() { impl.destroyNurseryRanges(); } +#ifdef DEBUG + bool hasNurseryRanges() const { return impl.hasNurseryRanges(); } +#endif void trace(JSTracer* trc) { impl.trace(trc); } @@ -1048,12 +1078,16 @@ class OrderedHashSet { HashNumber hash(const Lookup& value) const { return impl.prepareHash(value); } template <typename GetNewKey> - void rekeyOneEntry(const Lookup& current, const GetNewKey& getNewKey) { + mozilla::Maybe<T> rekeyOneEntry(Lookup& current, GetNewKey&& getNewKey) { + // TODO: This is inefficient because we also look up the entry in + // impl.rekeyOneEntry below. if (!has(current)) { - return; + return mozilla::Nothing(); } + T newKey = getNewKey(current); - return impl.rekeyOneEntry(current, newKey, newKey); + impl.rekeyOneEntry(current, newKey, newKey); + return mozilla::Some(newKey); } Range* createRange(void* buffer, bool inNursery) const { @@ -1061,6 +1095,9 @@ class OrderedHashSet { } void destroyNurseryRanges() { impl.destroyNurseryRanges(); } +#ifdef DEBUG + bool hasNurseryRanges() const { return impl.hasNurseryRanges(); } +#endif void trace(JSTracer* trc) { impl.trace(trc); } diff --git a/js/src/frontend/Parser.cpp b/js/src/frontend/Parser.cpp index cd586ad2a7..997b7ce8ee 100644 --- a/js/src/frontend/Parser.cpp +++ b/js/src/frontend/Parser.cpp @@ -811,11 +811,18 @@ bool GeneralParser<ParseHandler, Unit>::noteDeclaredPrivateName( AddDeclaredNamePtr p = scope->lookupDeclaredNameForAdd(name); DeclarationKind declKind = DeclarationKind::PrivateName; - ClosedOver closedOver = ClosedOver::No; + + // Our strategy for enabling debugger functionality is to mark names as closed + // over, even if they don't necessarily need to be, to ensure that they are + // included in the environment object. This allows us to easily look them up + // by name when needed, even if there is no corresponding property on an + // object, as is the case with getter, setters and private methods. + ClosedOver closedOver = ClosedOver::Yes; PrivateNameKind kind; switch (propType) { case PropertyType::Field: kind = PrivateNameKind::Field; + closedOver = ClosedOver::No; break; case PropertyType::FieldWithAccessor: // In this case, we create a new private field for the underlying storage, @@ -831,11 +838,6 @@ bool GeneralParser<ParseHandler, Unit>::noteDeclaredPrivateName( // DeclarationKind::Synthetic. declKind = DeclarationKind::PrivateMethod; } - - // Methods must be marked closed-over so that - // EmitterScope::lookupPrivate() works even if the method is used, but not - // within any method (from a computed property name, or debugger frame) - closedOver = ClosedOver::Yes; kind = PrivateNameKind::Method; break; case PropertyType::Getter: @@ -845,7 +847,7 @@ bool GeneralParser<ParseHandler, Unit>::noteDeclaredPrivateName( kind = PrivateNameKind::Setter; break; default: - kind = PrivateNameKind::None; + MOZ_CRASH("Invalid Property Type for noteDeclarePrivateName"); } if (p) { @@ -8004,7 +8006,11 @@ GeneralParser<ParseHandler, Unit>::classDefinition( // position in order to provide it for the nodes created later. TokenPos namePos = pos(); - bool isInClass = pc_->sc()->inClass(); + auto isClass = [](ParseContext::Statement* stmt) { + return stmt->kind() == StatementKind::Class; + }; + + bool isInClass = pc_->sc()->inClass() || pc_->findInnermostStatement(isClass); // Push a ParseContext::ClassStatement to keep track of the constructor // funbox. diff --git a/js/src/frontend/Stencil.cpp b/js/src/frontend/Stencil.cpp index 30d1588415..67ed2a90ca 100644 --- a/js/src/frontend/Stencil.cpp +++ b/js/src/frontend/Stencil.cpp @@ -759,6 +759,29 @@ void ScopeContext::cacheEnclosingScope(const InputScope& enclosingScope) { MOZ_CRASH("Malformed scope chain"); } +// Given an input scope, possibly refine this to a more precise scope. +// This is used during eval in the debugger to provide the appropriate scope and +// ThisBinding kind and environment, which is key to making private field eval +// work correctly. +// +// The trick here is that an eval may have a non-syntatic scope but nevertheless +// have an 'interesting' environment which can be traversed to find the +// appropriate scope the the eval to function as desired. See the diagram below. +// +// Eval Scope Eval Env Frame Env Frame Scope +// ============ ============= ========= ============= +// +// NonSyntactic +// | +// v +// null DebugEnvProxy LexicalScope +// | | +// v v +// DebugEnvProxy --> CallObj --> FunctionScope +// | | | +// v v v +// ... ... ... +// InputScope ScopeContext::determineEffectiveScope(InputScope& scope, JSObject* environment) { MOZ_ASSERT(effectiveScopeHops == 0); @@ -4286,8 +4309,8 @@ void js::DumpFunctionFlagsItems(js::JSONPrinter& json, case FunctionFlags::Flags::LAMBDA: json.value("LAMBDA"); break; - case FunctionFlags::Flags::WASM_JIT_ENTRY: - json.value("WASM_JIT_ENTRY"); + case FunctionFlags::Flags::NATIVE_JIT_ENTRY: + json.value("NATIVE_JIT_ENTRY"); break; case FunctionFlags::Flags::HAS_INFERRED_NAME: json.value("HAS_INFERRED_NAME"); diff --git a/js/src/frontend/TokenStream.cpp b/js/src/frontend/TokenStream.cpp index 2134972bf4..7060a6edb1 100644 --- a/js/src/frontend/TokenStream.cpp +++ b/js/src/frontend/TokenStream.cpp @@ -1487,9 +1487,8 @@ bool TokenStreamAnyChars::fillExceptingContext(ErrorMetadata* err, err->filename = JS::ConstUTF8CharsZ(iter.filename()); JS::TaggedColumnNumberOneOrigin columnNumber; err->lineNumber = iter.computeLine(&columnNumber); - // NOTE: Wasm frame cannot appear here. err->columnNumber = - JS::ColumnNumberOneOrigin(columnNumber.toLimitedColumnNumber()); + JS::ColumnNumberOneOrigin(columnNumber.oneOriginValue()); return false; } } diff --git a/js/src/fuzz-tests/gluesmith/Cargo.toml b/js/src/fuzz-tests/gluesmith/Cargo.toml index 7bd7f6652a..c5f7118ba4 100644 --- a/js/src/fuzz-tests/gluesmith/Cargo.toml +++ b/js/src/fuzz-tests/gluesmith/Cargo.toml @@ -5,6 +5,6 @@ authors = ["Christian Holler"] license = "MPL-2.0" [dependencies] -wasm-smith = "0.15.0" +wasm-smith = "0.201.0" arbitrary = { version = "1.0.0", features = ["derive"] } libc = "0.2" diff --git a/js/src/fuzz-tests/gluesmith/src/lib.rs b/js/src/fuzz-tests/gluesmith/src/lib.rs index 41aac369a0..83d9c9859d 100644 --- a/js/src/fuzz-tests/gluesmith/src/lib.rs +++ b/js/src/fuzz-tests/gluesmith/src/lib.rs @@ -41,6 +41,7 @@ pub unsafe extern "C" fn gluesmith( simd_enabled: true, tail_call_enabled: true, threads_enabled: true, + gc_enabled: true, ..Config::default() }; let module = match Module::new(config, &mut u) { diff --git a/js/src/gc/Allocator.cpp b/js/src/gc/Allocator.cpp index c070ac1eef..9e7f16cc3f 100644 --- a/js/src/gc/Allocator.cpp +++ b/js/src/gc/Allocator.cpp @@ -77,9 +77,10 @@ MOZ_NEVER_INLINE void* CellAllocator::RetryNurseryAlloc(JSContext* cx, size_t thingSize, AllocSite* site) { MOZ_ASSERT(cx->isNurseryAllocAllowed()); - MOZ_ASSERT(cx->zone() == site->zone()); - MOZ_ASSERT(!cx->zone()->isAtomsZone()); - MOZ_ASSERT(cx->zone()->allocKindInNursery(traceKind)); + + Zone* zone = site->zone(); + MOZ_ASSERT(!zone->isAtomsZone()); + MOZ_ASSERT(zone->allocKindInNursery(traceKind)); Nursery& nursery = cx->nursery(); JS::GCReason reason = nursery.handleAllocationFailure(); @@ -102,7 +103,7 @@ MOZ_NEVER_INLINE void* CellAllocator::RetryNurseryAlloc(JSContext* cx, cx->runtime()->gc.minorGC(reason); // Exceeding gcMaxBytes while tenuring can disable the Nursery. - if (cx->zone()->allocKindInNursery(traceKind)) { + if (zone->allocKindInNursery(traceKind)) { void* ptr = cx->nursery().allocateCell(site, thingSize, traceKind); if (ptr) { return ptr; @@ -291,7 +292,7 @@ void CellAllocator::CheckIncrementalZoneState(JSContext* cx, void* ptr) { } #endif -void* js::gc::AllocateCellInGC(Zone* zone, AllocKind thingKind) { +void* js::gc::AllocateTenuredCellInGC(Zone* zone, AllocKind thingKind) { void* ptr = zone->arenas.allocateFromFreeList(thingKind); if (!ptr) { AutoEnterOOMUnsafeRegion oomUnsafe; @@ -541,7 +542,7 @@ TenuredChunk* GCRuntime::getOrAllocChunk(AutoLockGCBgAlloc& lock) { // Reinitialize ChunkBase; arenas are all free and may or may not be // committed. SetMemCheckKind(chunk, sizeof(ChunkBase), MemCheckKind::MakeUndefined); - chunk->initBase(rt, nullptr); + chunk->initBaseForTenuredChunk(rt); MOZ_ASSERT(chunk->unused()); } else { void* ptr = TenuredChunk::allocate(this); diff --git a/js/src/gc/Allocator.h b/js/src/gc/Allocator.h index 3b1566e7f5..c317bfb10b 100644 --- a/js/src/gc/Allocator.h +++ b/js/src/gc/Allocator.h @@ -21,6 +21,7 @@ namespace gc { class AllocSite; struct Cell; class TenuredCell; +class TenuringTracer; // Allocator implementation functions. SpiderMonkey code outside this file // should use: @@ -82,6 +83,7 @@ class CellAllocator { static void* AllocNurseryOrTenuredCell(JSContext* cx, gc::AllocKind allocKind, size_t thingSize, gc::Heap heap, AllocSite* site); + friend class TenuringTracer; // Allocate a cell in the tenured heap. template <AllowGC allowGC> diff --git a/js/src/gc/AtomMarking.cpp b/js/src/gc/AtomMarking.cpp index eb30758263..a5b7eb1acf 100644 --- a/js/src/gc/AtomMarking.cpp +++ b/js/src/gc/AtomMarking.cpp @@ -12,6 +12,7 @@ #include "gc/GC-inl.h" #include "gc/Heap-inl.h" +#include "gc/PrivateIterators-inl.h" namespace js { namespace gc { @@ -71,7 +72,38 @@ void AtomMarkingRuntime::unregisterArena(Arena* arena, const AutoLockGC& lock) { (void)freeArenaIndexes.ref().emplaceBack(arena->atomBitmapStart()); } -bool AtomMarkingRuntime::computeBitmapFromChunkMarkBits(JSRuntime* runtime, +void AtomMarkingRuntime::refineZoneBitmapsForCollectedZones( + GCRuntime* gc, size_t collectedZones) { + // If there is more than one zone to update, copy the chunk mark bits into a + // bitmap and AND that into the atom marking bitmap for each zone. + DenseBitmap marked; + if (collectedZones > 1 && computeBitmapFromChunkMarkBits(gc, marked)) { + for (GCZonesIter zone(gc); !zone.done(); zone.next()) { + refineZoneBitmapForCollectedZone(zone, marked); + } + return; + } + + // If there's only one zone (or on OOM), AND the mark bits for each arena into + // the zones' atom marking bitmaps directly. + for (GCZonesIter zone(gc); !zone.done(); zone.next()) { + if (zone->isAtomsZone()) { + continue; + } + + for (auto thingKind : AllAllocKinds()) { + for (ArenaIterInGC aiter(gc->atomsZone(), thingKind); !aiter.done(); + aiter.next()) { + Arena* arena = aiter.get(); + MarkBitmapWord* chunkWords = arena->chunk()->markBits.arenaBits(arena); + zone->markedAtoms().bitwiseAndRangeWith(arena->atomBitmapStart(), + ArenaBitmapWords, chunkWords); + } + } + } +} + +bool AtomMarkingRuntime::computeBitmapFromChunkMarkBits(GCRuntime* gc, DenseBitmap& bitmap) { MOZ_ASSERT(CurrentThreadIsPerformingGC()); @@ -79,7 +111,7 @@ bool AtomMarkingRuntime::computeBitmapFromChunkMarkBits(JSRuntime* runtime, return false; } - Zone* atomsZone = runtime->unsafeAtomsZone(); + Zone* atomsZone = gc->atomsZone(); for (auto thingKind : AllAllocKinds()) { for (ArenaIterInGC aiter(atomsZone, thingKind); !aiter.done(); aiter.next()) { @@ -109,13 +141,12 @@ void AtomMarkingRuntime::refineZoneBitmapForCollectedZone( // Set any bits in the chunk mark bitmaps for atoms which are marked in bitmap. template <typename Bitmap> -static void BitwiseOrIntoChunkMarkBits(JSRuntime* runtime, Bitmap& bitmap) { +static void BitwiseOrIntoChunkMarkBits(Zone* atomsZone, Bitmap& bitmap) { // Make sure that by copying the mark bits for one arena in word sizes we // do not affect the mark bits for other arenas. static_assert(ArenaBitmapBits == ArenaBitmapWords * JS_BITS_PER_WORD, "ArenaBitmapWords must evenly divide ArenaBitmapBits"); - Zone* atomsZone = runtime->unsafeAtomsZone(); for (auto thingKind : AllAllocKinds()) { for (ArenaIterInGC aiter(atomsZone, thingKind); !aiter.done(); aiter.next()) { @@ -127,16 +158,10 @@ static void BitwiseOrIntoChunkMarkBits(JSRuntime* runtime, Bitmap& bitmap) { } } -void AtomMarkingRuntime::markAtomsUsedByUncollectedZones(JSRuntime* runtime) { +void AtomMarkingRuntime::markAtomsUsedByUncollectedZones( + GCRuntime* gc, size_t uncollectedZones) { MOZ_ASSERT(CurrentThreadIsPerformingGC()); - size_t uncollectedZones = 0; - for (ZonesIter zone(runtime, SkipAtoms); !zone.done(); zone.next()) { - if (!zone->isCollecting()) { - uncollectedZones++; - } - } - // If there are no uncollected non-atom zones then there's no work to do. if (uncollectedZones == 0) { return; @@ -149,15 +174,15 @@ void AtomMarkingRuntime::markAtomsUsedByUncollectedZones(JSRuntime* runtime) { DenseBitmap markedUnion; if (uncollectedZones == 1 || !markedUnion.ensureSpace(allocatedWords)) { - for (ZonesIter zone(runtime, SkipAtoms); !zone.done(); zone.next()) { + for (ZonesIter zone(gc, SkipAtoms); !zone.done(); zone.next()) { if (!zone->isCollecting()) { - BitwiseOrIntoChunkMarkBits(runtime, zone->markedAtoms()); + BitwiseOrIntoChunkMarkBits(gc->atomsZone(), zone->markedAtoms()); } } return; } - for (ZonesIter zone(runtime, SkipAtoms); !zone.done(); zone.next()) { + for (ZonesIter zone(gc, SkipAtoms); !zone.done(); zone.next()) { // We only need to update the chunk mark bits for zones which were // not collected in the current GC. Atoms which are referenced by // collected zones have already been marked. @@ -166,7 +191,7 @@ void AtomMarkingRuntime::markAtomsUsedByUncollectedZones(JSRuntime* runtime) { } } - BitwiseOrIntoChunkMarkBits(runtime, markedUnion); + BitwiseOrIntoChunkMarkBits(gc->atomsZone(), markedUnion); } template <typename T> diff --git a/js/src/gc/AtomMarking.h b/js/src/gc/AtomMarking.h index e7e97fb389..e5d8ef8418 100644 --- a/js/src/gc/AtomMarking.h +++ b/js/src/gc/AtomMarking.h @@ -19,6 +19,7 @@ class DenseBitmap; namespace gc { class Arena; +class GCRuntime; // This class manages state used for marking atoms during GCs. // See AtomMarking.cpp for details. @@ -42,19 +43,25 @@ class AtomMarkingRuntime { // Mark an arena as no longer holding things in the atoms zone. void unregisterArena(Arena* arena, const AutoLockGC& lock); + // Update the atom marking bitmaps in all collected zones according to the + // atoms zone mark bits. + void refineZoneBitmapsForCollectedZones(GCRuntime* gc, size_t collectedZones); + + // Set any bits in the chunk mark bitmaps for atoms which are marked in any + // uncollected zone in the runtime. + void markAtomsUsedByUncollectedZones(GCRuntime* gc, size_t uncollectedZones); + + private: // Fill |bitmap| with an atom marking bitmap based on the things that are // currently marked in the chunks used by atoms zone arenas. This returns // false on an allocation failure (but does not report an exception). - bool computeBitmapFromChunkMarkBits(JSRuntime* runtime, DenseBitmap& bitmap); + bool computeBitmapFromChunkMarkBits(GCRuntime* gc, DenseBitmap& bitmap); // Update the atom marking bitmap in |zone| according to another // overapproximation of the reachable atoms in |bitmap|. void refineZoneBitmapForCollectedZone(Zone* zone, const DenseBitmap& bitmap); - // Set any bits in the chunk mark bitmaps for atoms which are marked in any - // uncollected zone in the runtime. - void markAtomsUsedByUncollectedZones(JSRuntime* runtime); - + public: // Mark an atom or id as being newly reachable by the context's zone. template <typename T> void markAtom(JSContext* cx, T* thing); diff --git a/js/src/gc/Cell.h b/js/src/gc/Cell.h index 5a89ad794d..f91163e2f5 100644 --- a/js/src/gc/Cell.h +++ b/js/src/gc/Cell.h @@ -209,6 +209,8 @@ struct Cell { inline JS::Zone* nurseryZone() const; inline JS::Zone* nurseryZoneFromAnyThread() const; + inline ChunkBase* chunk() const; + // Default implementation for kinds that cannot be permanent. This may be // overriden by derived classes. MOZ_ALWAYS_INLINE bool isPermanentAndMayBeShared() const { return false; } @@ -222,7 +224,6 @@ struct Cell { protected: uintptr_t address() const; - inline ChunkBase* chunk() const; private: // Cells are destroyed by the GC. Do not delete them directly. diff --git a/js/src/gc/Compacting.cpp b/js/src/gc/Compacting.cpp index 442fa3fe47..79e8e0b71d 100644 --- a/js/src/gc/Compacting.cpp +++ b/js/src/gc/Compacting.cpp @@ -215,7 +215,7 @@ static void RelocateCell(Zone* zone, TenuredCell* src, AllocKind thingKind, // Allocate a new cell. MOZ_ASSERT(zone == src->zone()); TenuredCell* dst = - reinterpret_cast<TenuredCell*>(AllocateCellInGC(zone, thingKind)); + reinterpret_cast<TenuredCell*>(AllocateTenuredCellInGC(zone, thingKind)); // Copy source cell contents to destination. memcpy(dst, src, thingSize); diff --git a/js/src/gc/GC.cpp b/js/src/gc/GC.cpp index c01dfe3660..68dd66898c 100644 --- a/js/src/gc/GC.cpp +++ b/js/src/gc/GC.cpp @@ -444,7 +444,9 @@ GCRuntime::GCRuntime(JSRuntime* rt) #endif requestSliceAfterBackgroundTask(false), lifoBlocksToFree((size_t)JSContext::TEMP_LIFO_ALLOC_PRIMARY_CHUNK_SIZE), - lifoBlocksToFreeAfterMinorGC( + lifoBlocksToFreeAfterFullMinorGC( + (size_t)JSContext::TEMP_LIFO_ALLOC_PRIMARY_CHUNK_SIZE), + lifoBlocksToFreeAfterNextMinorGC( (size_t)JSContext::TEMP_LIFO_ALLOC_PRIMARY_CHUNK_SIZE), sweepGroupIndex(0), sweepGroups(nullptr), @@ -658,7 +660,7 @@ void GCRuntime::setZeal(uint8_t zeal, uint32_t frequency) { if (zeal == 0) { if (hasZealMode(ZealMode::GenerationalGC)) { - evictNursery(JS::GCReason::DEBUG_GC); + evictNursery(); nursery().leaveZealMode(); } @@ -669,7 +671,7 @@ void GCRuntime::setZeal(uint8_t zeal, uint32_t frequency) { ZealMode zealMode = ZealMode(zeal); if (zealMode == ZealMode::GenerationalGC) { - evictNursery(JS::GCReason::DEBUG_GC); + evictNursery(JS::GCReason::EVICT_NURSERY); nursery().enterZealMode(); } @@ -704,7 +706,7 @@ void GCRuntime::unsetZeal(uint8_t zeal) { } if (zealMode == ZealMode::GenerationalGC) { - evictNursery(JS::GCReason::DEBUG_GC); + evictNursery(); nursery().leaveZealMode(); } @@ -1073,6 +1075,11 @@ bool GCRuntime::setParameter(JSGCParamKey key, uint32_t value, marker->incrementalWeakMapMarkingEnabled = value != 0; } break; + case JSGC_SEMISPACE_NURSERY_ENABLED: { + AutoUnlockGC unlock(lock); + nursery().setSemispaceEnabled(value); + break; + } case JSGC_MIN_EMPTY_CHUNK_COUNT: setMinEmptyChunkCount(value, lock); break; @@ -1160,6 +1167,11 @@ void GCRuntime::resetParameter(JSGCParamKey key, AutoLockGC& lock) { TuningDefaults::IncrementalWeakMapMarkingEnabled; } break; + case JSGC_SEMISPACE_NURSERY_ENABLED: { + AutoUnlockGC unlock(lock); + nursery().setSemispaceEnabled(TuningDefaults::SemispaceNurseryEnabled); + break; + } case JSGC_MIN_EMPTY_CHUNK_COUNT: setMinEmptyChunkCount(TuningDefaults::MinEmptyChunkCount, lock); break; @@ -1241,6 +1253,8 @@ uint32_t GCRuntime::getParameter(JSGCParamKey key, const AutoLockGC& lock) { return parallelMarkingEnabled; case JSGC_INCREMENTAL_WEAKMAP_ENABLED: return marker().incrementalWeakMapMarkingEnabled; + case JSGC_SEMISPACE_NURSERY_ENABLED: + return nursery().semispaceEnabled(); case JSGC_CHUNK_BYTES: return ChunkSize; case JSGC_HELPER_THREAD_RATIO: @@ -2135,7 +2149,7 @@ void GCRuntime::queueUnusedLifoBlocksForFree(LifoAlloc* lifo) { } void GCRuntime::queueAllLifoBlocksForFreeAfterMinorGC(LifoAlloc* lifo) { - lifoBlocksToFreeAfterMinorGC.ref().transferFrom(lifo); + lifoBlocksToFreeAfterFullMinorGC.ref().transferFrom(lifo); } void GCRuntime::queueBuffersForFreeAfterMinorGC(Nursery::BufferSet& buffers) { @@ -2741,24 +2755,7 @@ void GCRuntime::endPreparePhase(JS::GCReason reason) { MOZ_ASSERT(unmarkTask.isIdle()); for (GCZonesIter zone(this); !zone.done(); zone.next()) { - /* - * In an incremental GC, clear the area free lists to ensure that subsequent - * allocations refill them and end up marking new cells back. See - * arenaAllocatedDuringGC(). - */ - zone->arenas.clearFreeLists(); - zone->setPreservingCode(false); - -#ifdef JS_GC_ZEAL - if (hasZealMode(ZealMode::YieldBeforeRootMarking)) { - for (auto kind : AllAllocKinds()) { - for (ArenaIterInGC arena(zone, kind); !arena.done(); arena.next()) { - arena->checkNoMarkedCells(); - } - } - } -#endif } // Discard JIT code more aggressively if the process is approaching its @@ -2800,7 +2797,7 @@ void GCRuntime::endPreparePhase(JS::GCReason reason) { */ { - gcstats::AutoPhase ap1(stats(), gcstats::PhaseKind::PREPARE); + gcstats::AutoPhase ap(stats(), gcstats::PhaseKind::PREPARE); AutoLockHelperThreadState helperLock; @@ -2817,20 +2814,6 @@ void GCRuntime::endPreparePhase(JS::GCReason reason) { haveDiscardedJITCodeThisSlice = true; /* - * Relazify functions after discarding JIT code (we can't relazify - * functions with JIT code) and before the actual mark phase, so that - * the current GC can collect the JSScripts we're unlinking here. We do - * this only when we're performing a shrinking GC, as too much - * relazification can cause performance issues when we have to reparse - * the same functions over and over. - */ - if (isShrinkingGC()) { - relazifyFunctionsForShrinkingGC(); - purgePropMapTablesForShrinkingGC(); - purgeSourceURLsForShrinkingGC(); - } - - /* * We must purge the runtime at the beginning of an incremental GC. The * danger if we purge later is that the snapshot invariant of * incremental GC will be broken, as follows. If some object is @@ -2840,8 +2823,25 @@ void GCRuntime::endPreparePhase(JS::GCReason reason) { * it. This object might never be marked, so a GC hazard would exist. */ purgeRuntime(); + } + + // This will start background free for lifo blocks queued by purgeRuntime, + // even if there's nothing in the nursery. + collectNurseryFromMajorGC(reason); - startBackgroundFreeAfterMinorGC(); + { + gcstats::AutoPhase ap(stats(), gcstats::PhaseKind::PREPARE); + // Relazify functions after discarding JIT code (we can't relazify functions + // with JIT code) and before the actual mark phase, so that the current GC + // can collect the JSScripts we're unlinking here. We do this only when + // we're performing a shrinking GC, as too much relazification can cause + // performance issues when we have to reparse the same functions over and + // over. + if (isShrinkingGC()) { + relazifyFunctionsForShrinkingGC(); + purgePropMapTablesForShrinkingGC(); + purgeSourceURLsForShrinkingGC(); + } if (isShutdownGC()) { /* Clear any engine roots that may hold external data live. */ @@ -2903,6 +2903,21 @@ void GCRuntime::beginMarkPhase(AutoGCSession& session) { #endif for (GCZonesIter zone(this); !zone.done(); zone.next()) { + // In an incremental GC, clear the arena free lists to ensure that + // subsequent allocations refill them and end up marking new cells black. + // See arenaAllocatedDuringGC(). + zone->arenas.clearFreeLists(); + +#ifdef JS_GC_ZEAL + if (hasZealMode(ZealMode::YieldBeforeRootMarking)) { + for (auto kind : AllAllocKinds()) { + for (ArenaIter arena(zone, kind); !arena.done(); arena.next()) { + arena->checkNoMarkedCells(); + } + } + } +#endif + // Incremental marking barriers are enabled at this point. zone->changeGCState(Zone::Prepare, zone->initialMarkingState()); @@ -3715,11 +3730,8 @@ void GCRuntime::incrementalSlice(SliceBudget& budget, JS::GCReason reason, [[fallthrough]]; case State::MarkRoots: - if (NeedToCollectNursery(this)) { - collectNurseryFromMajorGC(reason); - } - endPreparePhase(reason); + beginMarkPhase(session); incrementalState = State::Mark; @@ -3878,8 +3890,11 @@ void GCRuntime::incrementalSlice(SliceBudget& budget, JS::GCReason reason, } void GCRuntime::collectNurseryFromMajorGC(JS::GCReason reason) { - collectNursery(gcOptions(), reason, + collectNursery(gcOptions(), JS::GCReason::EVICT_NURSERY, gcstats::PhaseKind::EVICT_NURSERY_FOR_MAJOR_GC); + + MOZ_ASSERT(nursery().isEmpty()); + MOZ_ASSERT(storeBuffer().isEmpty()); } bool GCRuntime::hasForegroundWork() const { @@ -4733,26 +4748,43 @@ void GCRuntime::collectNursery(JS::GCOptions options, JS::GCReason reason, gcstats::AutoPhase ap(stats(), phase); nursery().collect(options, reason); - MOZ_ASSERT(nursery().isEmpty()); startBackgroundFreeAfterMinorGC(); + + // We ignore gcMaxBytes when allocating for minor collection. However, if we + // overflowed, we disable the nursery. The next time we allocate, we'll fail + // because bytes >= gcMaxBytes. + if (heapSize.bytes() >= tunables.gcMaxBytes()) { + if (!nursery().isEmpty()) { + nursery().collect(options, JS::GCReason::DISABLE_GENERATIONAL_GC); + MOZ_ASSERT(nursery().isEmpty()); + startBackgroundFreeAfterMinorGC(); + } + nursery().disable(); + } } void GCRuntime::startBackgroundFreeAfterMinorGC() { - MOZ_ASSERT(nursery().isEmpty()); + // Called after nursery collection. Free whatever blocks are safe to free now. - { - AutoLockHelperThreadState lock; + AutoLockHelperThreadState lock; - lifoBlocksToFree.ref().transferFrom(&lifoBlocksToFreeAfterMinorGC.ref()); + lifoBlocksToFree.ref().transferFrom(&lifoBlocksToFreeAfterNextMinorGC.ref()); - if (lifoBlocksToFree.ref().isEmpty() && - buffersToFreeAfterMinorGC.ref().empty()) { - return; - } + if (nursery().tenuredEverything) { + lifoBlocksToFree.ref().transferFrom( + &lifoBlocksToFreeAfterFullMinorGC.ref()); + } else { + lifoBlocksToFreeAfterNextMinorGC.ref().transferFrom( + &lifoBlocksToFreeAfterFullMinorGC.ref()); + } + + if (lifoBlocksToFree.ref().isEmpty() && + buffersToFreeAfterMinorGC.ref().empty()) { + return; } - startBackgroundFree(); + freeTask.startOrRunIfIdle(lock); } bool GCRuntime::gcIfRequestedImpl(bool eagerOk) { diff --git a/js/src/gc/GC.h b/js/src/gc/GC.h index 4e4634d804..7f603b066f 100644 --- a/js/src/gc/GC.h +++ b/js/src/gc/GC.h @@ -83,7 +83,8 @@ class TenuredChunk; _("maxHelperThreads", JSGC_MAX_HELPER_THREADS, true) \ _("helperThreadCount", JSGC_HELPER_THREAD_COUNT, false) \ _("markingThreadCount", JSGC_MARKING_THREAD_COUNT, true) \ - _("systemPageSizeKB", JSGC_SYSTEM_PAGE_SIZE_KB, false) + _("systemPageSizeKB", JSGC_SYSTEM_PAGE_SIZE_KB, false) \ + _("semispaceNurseryEnabled", JSGC_SEMISPACE_NURSERY_ENABLED, true) // Get the key and writability give a GC parameter name. extern bool GetGCParameterInfo(const char* name, JSGCParamKey* keyOut, diff --git a/js/src/gc/GCAPI.cpp b/js/src/gc/GCAPI.cpp index 293bfce80d..d2eab44dea 100644 --- a/js/src/gc/GCAPI.cpp +++ b/js/src/gc/GCAPI.cpp @@ -487,8 +487,7 @@ JS_PUBLIC_API bool JS::WasIncrementalGC(JSRuntime* rt) { bool js::gc::CreateUniqueIdForNativeObject(NativeObject* nobj, uint64_t* uidp) { JSRuntime* runtime = nobj->runtimeFromMainThread(); *uidp = NextCellUniqueId(runtime); - JSContext* cx = runtime->mainContextFromOwnThread(); - return nobj->setUniqueId(cx, *uidp); + return nobj->setUniqueId(runtime, *uidp); } bool js::gc::CreateUniqueIdForNonNativeObject(Cell* cell, @@ -793,6 +792,15 @@ const char* CellColorName(CellColor color) { } /* namespace gc */ } /* namespace js */ +JS_PUBLIC_API bool js::gc::IsDeadNurseryObject(JSObject* obj) { + MOZ_ASSERT(JS::RuntimeHeapIsMinorCollecting()); + MOZ_ASSERT(obj); + MOZ_ASSERT(IsInsideNursery(obj)); + MOZ_ASSERT(!IsForwarded(obj)); + + return obj->runtimeFromMainThread()->gc.nursery().inCollectedRegion(obj); +} + JS_PUBLIC_API void js::gc::FinalizeDeadNurseryObject(JSContext* cx, JSObject* obj) { CHECK_THREAD(cx); diff --git a/js/src/gc/GCInternals.h b/js/src/gc/GCInternals.h index c234ad4b2b..9a3edc2c9d 100644 --- a/js/src/gc/GCInternals.h +++ b/js/src/gc/GCInternals.h @@ -329,7 +329,7 @@ inline bool IsOOMReason(JS::GCReason reason) { reason == JS::GCReason::MEM_PRESSURE; } -void* AllocateCellInGC(JS::Zone* zone, AllocKind thingKind); +void* AllocateTenuredCellInGC(JS::Zone* zone, AllocKind thingKind); void ReadProfileEnv(const char* envName, const char* helpText, bool* enableOut, bool* workersOut, mozilla::TimeDuration* thresholdOut); diff --git a/js/src/gc/GCRuntime.h b/js/src/gc/GCRuntime.h index c9f660b4d7..851e477359 100644 --- a/js/src/gc/GCRuntime.h +++ b/js/src/gc/GCRuntime.h @@ -1205,7 +1205,8 @@ class GCRuntime { * a background thread. */ HelperThreadLockData<LifoAlloc> lifoBlocksToFree; - MainThreadData<LifoAlloc> lifoBlocksToFreeAfterMinorGC; + MainThreadData<LifoAlloc> lifoBlocksToFreeAfterFullMinorGC; + MainThreadData<LifoAlloc> lifoBlocksToFreeAfterNextMinorGC; HelperThreadLockData<Nursery::BufferSet> buffersToFreeAfterMinorGC; /* Index of current sweep group (for stats). */ diff --git a/js/src/gc/Heap-inl.h b/js/src/gc/Heap-inl.h index 95bd841cae..30937a7236 100644 --- a/js/src/gc/Heap-inl.h +++ b/js/src/gc/Heap-inl.h @@ -44,6 +44,10 @@ inline void js::gc::Arena::init(JS::Zone* zoneArg, AllocKind kind, } setAsFullyUnused(); + +#ifdef DEBUG + checkNoMarkedCells(); +#endif } inline void js::gc::Arena::release(const AutoLockGC& lock) { diff --git a/js/src/gc/MallocedBlockCache.h b/js/src/gc/MallocedBlockCache.h index cd7d1e1064..6fc577044e 100644 --- a/js/src/gc/MallocedBlockCache.h +++ b/js/src/gc/MallocedBlockCache.h @@ -70,6 +70,8 @@ class MallocedBlockCache { ~MallocedBlockCache(); + static inline size_t listIDForSize(size_t size); + // Allocation and freeing. Use `alloc` to allocate. `allocSlow` is // `alloc`s fallback path. Do not call it directly, since it doesn't handle // all cases by itself. @@ -89,7 +91,8 @@ class MallocedBlockCache { size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const; }; -inline PointerAndUint7 MallocedBlockCache::alloc(size_t size) { +/* static */ +inline size_t MallocedBlockCache::listIDForSize(size_t size) { // Figure out which free list can give us a block of size `size`, after it // has been rounded up to a multiple of `step`. // @@ -122,11 +125,23 @@ inline PointerAndUint7 MallocedBlockCache::alloc(size_t size) { size_t i = size / STEP; MOZ_ASSERT(i > 0); + if (i >= NUM_LISTS) { + return OVERSIZE_BLOCK_LIST_ID; + } + + return i; +} + +inline PointerAndUint7 MallocedBlockCache::alloc(size_t size) { + size_t i = listIDForSize(size); + MOZ_ASSERT(i < NUM_LISTS); + // Fast path: try to pull a block from the relevant list. - if (MOZ_LIKELY(i < NUM_LISTS && // "block is small enough to cache" - !lists[i].empty())) { // "a cached block is available" + if (MOZ_LIKELY( + i != OVERSIZE_BLOCK_LIST_ID && // "block is small enough to cache" + !lists[i].empty())) { // "a cached block is available" // Check that i is the right list - MOZ_ASSERT(i * STEP == size); + MOZ_ASSERT(i * STEP == js::RoundUp(size, STEP)); void* block = lists[i].popCopy(); return PointerAndUint7(block, i); } diff --git a/js/src/gc/Nursery-inl.h b/js/src/gc/Nursery-inl.h index 3a29d76c07..3be063697a 100644 --- a/js/src/gc/Nursery-inl.h +++ b/js/src/gc/Nursery-inl.h @@ -16,6 +16,12 @@ #include "vm/JSContext.h" #include "vm/NativeObject.h" +namespace js { +namespace gc { +struct Cell; +} // namespace gc +} // namespace js + inline JSRuntime* js::Nursery::runtime() const { return gc->rt; } template <typename T> @@ -23,6 +29,50 @@ bool js::Nursery::isInside(const SharedMem<T>& p) const { return isInside(p.unwrap(/*safe - used for value in comparison above*/)); } +inline bool js::Nursery::shouldTenure(gc::Cell* cell) { + MOZ_ASSERT(semispaceEnabled()); + MOZ_ASSERT(inCollectedRegion(cell)); + + size_t offset = fromSpace.offsetFromAddress(uintptr_t(cell)); + MOZ_ASSERT(offset >= + fromSpace.offsetFromExclusiveAddress(fromSpace.startPosition_)); + return offset <= tenureThreshold_; +} + +inline bool js::Nursery::inCollectedRegion(gc::Cell* cell) const { + gc::ChunkBase* chunk = gc::detail::GetCellChunkBase(cell); + return chunk->getKind() == gc::ChunkKind::NurseryFromSpace; +} + +inline bool js::Nursery::inCollectedRegion(void* ptr) const { + if (!semispaceEnabled()) { + return toSpace.isInside(ptr); + } + + return fromSpace.isInside(ptr); +} + +inline size_t js::Nursery::Space::offsetFromExclusiveAddress( + uintptr_t addr) const { + if ((addr & gc::ChunkMask) == 0) { + // |addr| points one past the end of the previous chunk. + return offsetFromAddress(addr - 1) + 1; + } + + return offsetFromAddress(addr); +} + +inline size_t js::Nursery::Space::offsetFromAddress(uintptr_t addr) const { + gc::ChunkBase* chunk = + gc::detail::GetCellChunkBase(reinterpret_cast<gc::Cell*>(addr)); + MOZ_ASSERT(chunk->getKind() == kind); + MOZ_ASSERT(findChunkIndex(addr & ~gc::ChunkMask) == chunk->nurseryChunkIndex); + + uint32_t offset = addr & gc::ChunkMask; + MOZ_ASSERT(offset >= sizeof(gc::ChunkBase)); + return (chunk->nurseryChunkIndex << gc::ChunkShift) | offset; +} + MOZ_ALWAYS_INLINE /* static */ bool js::Nursery::getForwardedPointer( js::gc::Cell** ref) { js::gc::Cell* cell = (*ref); @@ -80,7 +130,7 @@ inline void js::Nursery::setForwardingPointer(void* oldData, void* newData, inline void js::Nursery::setDirectForwardingPointer(void* oldData, void* newData) { MOZ_ASSERT(isInside(oldData)); - MOZ_ASSERT(!isInside(newData)); + MOZ_ASSERT_IF(isInside(newData), !inCollectedRegion(newData)); new (oldData) BufferRelocationOverlay{newData}; } @@ -123,8 +173,8 @@ inline void* js::Nursery::tryAllocateCell(gc::AllocSite* site, size_t size, inline void* js::Nursery::tryAllocate(size_t size) { MOZ_ASSERT(isEnabled()); - MOZ_ASSERT(!JS::RuntimeHeapIsBusy()); - MOZ_ASSERT_IF(currentChunk_ == startChunk_, position() >= startPosition_); + MOZ_ASSERT_IF(JS::RuntimeHeapIsBusy(), JS::RuntimeHeapIsMinorCollecting()); + MOZ_ASSERT_IF(currentChunk() == startChunk(), position() >= startPosition()); MOZ_ASSERT(size % gc::CellAlignBytes == 0); MOZ_ASSERT(position() % gc::CellAlignBytes == 0); @@ -138,7 +188,7 @@ inline void* js::Nursery::tryAllocate(size_t size) { "Successful allocation cannot result in nullptr"); } - position_ = position() + size; + toSpace.position_ = position() + size; DebugOnlyPoison(ptr, JS_ALLOCATED_NURSERY_PATTERN, size, MemCheckKind::MakeUndefined); @@ -148,30 +198,33 @@ inline void* js::Nursery::tryAllocate(size_t size) { inline bool js::Nursery::registerTrailer(PointerAndUint7 blockAndListID, size_t nBytes) { - MOZ_ASSERT(trailersAdded_.length() == trailersRemoved_.length()); + MOZ_ASSERT(toSpace.trailersAdded_.length() == + toSpace.trailersRemoved_.length()); MOZ_ASSERT(nBytes > 0); - if (MOZ_UNLIKELY(!trailersAdded_.append(blockAndListID))) { + if (MOZ_UNLIKELY(!toSpace.trailersAdded_.append(blockAndListID))) { return false; } - if (MOZ_UNLIKELY(!trailersRemoved_.append(nullptr))) { - trailersAdded_.popBack(); + if (MOZ_UNLIKELY(!toSpace.trailersRemoved_.append(nullptr))) { + toSpace.trailersAdded_.popBack(); return false; } // This is a clone of the logic in ::registerMallocedBuffer. It may be // that some other heuristic is better, once we know more about the // typical behaviour of wasm-GC applications. - trailerBytes_ += nBytes; - if (MOZ_UNLIKELY(trailerBytes_ > capacity() * 8)) { + toSpace.trailerBytes_ += nBytes; + if (MOZ_UNLIKELY(toSpace.trailerBytes_ > capacity() * 8)) { requestMinorGC(JS::GCReason::NURSERY_TRAILERS); } return true; } inline void js::Nursery::unregisterTrailer(void* block) { - MOZ_ASSERT(trailersRemovedUsed_ < trailersRemoved_.length()); - trailersRemoved_[trailersRemovedUsed_] = block; - trailersRemovedUsed_++; + // Unlike removeMallocedBuffer this is only called during minor GC. + MOZ_ASSERT(fromSpace.trailersRemovedUsed_ < + fromSpace.trailersRemoved_.length()); + fromSpace.trailersRemoved_[fromSpace.trailersRemovedUsed_] = block; + fromSpace.trailersRemovedUsed_++; } namespace js { @@ -181,13 +234,20 @@ namespace js { // instead. template <typename T> -static inline T* AllocateCellBuffer(JSContext* cx, gc::Cell* cell, +static inline T* AllocateCellBuffer(Nursery& nursery, gc::Cell* cell, uint32_t count) { size_t nbytes = RoundUp(count * sizeof(T), sizeof(Value)); - auto* buffer = static_cast<T*>(cx->nursery().allocateBuffer( - cell->zone(), cell, nbytes, js::MallocArena)); + return static_cast<T*>( + nursery.allocateBuffer(cell->zone(), cell, nbytes, js::MallocArena)); +} + +template <typename T> +static inline T* AllocateCellBuffer(JSContext* cx, gc::Cell* cell, + uint32_t count) { + T* buffer = AllocateCellBuffer<T>(cx->nursery(), cell, count); if (!buffer) { ReportOutOfMemory(cx); + return nullptr; } return buffer; diff --git a/js/src/gc/Nursery.cpp b/js/src/gc/Nursery.cpp index 660daa8d4c..4753848c56 100644 --- a/js/src/gc/Nursery.cpp +++ b/js/src/gc/Nursery.cpp @@ -38,6 +38,7 @@ #include "gc/Heap-inl.h" #include "gc/Marking-inl.h" #include "gc/StableCellHasher-inl.h" +#include "gc/StoreBuffer-inl.h" #include "vm/GeckoProfiler-inl.h" using namespace js; @@ -50,15 +51,22 @@ using mozilla::TimeStamp; namespace js { +static constexpr size_t NurseryChunkHeaderSize = + RoundUp(sizeof(ChunkBase), CellAlignBytes); + +// The amount of space in a nursery chunk available to allocations. +static constexpr size_t NurseryChunkUsableSize = + ChunkSize - NurseryChunkHeaderSize; + struct NurseryChunk : public ChunkBase { - char data[Nursery::NurseryChunkUsableSize]; + alignas(CellAlignBytes) uint8_t data[NurseryChunkUsableSize]; - static NurseryChunk* fromChunk(gc::TenuredChunk* chunk); + static NurseryChunk* fromChunk(TenuredChunk* chunk, ChunkKind kind, + uint8_t index); - explicit NurseryChunk(JSRuntime* runtime) - : ChunkBase(runtime, &runtime->gc.storeBuffer()) {} + explicit NurseryChunk(JSRuntime* runtime, ChunkKind kind, uint8_t chunkIndex) + : ChunkBase(runtime, &runtime->gc.storeBuffer(), kind, chunkIndex) {} - void poisonAndInit(JSRuntime* rt, size_t size = ChunkSize); void poisonRange(size_t start, size_t end, uint8_t value, MemCheckKind checkKind); void poisonAfterEvict(size_t extent = ChunkSize); @@ -75,22 +83,29 @@ struct NurseryChunk : public ChunkBase { uintptr_t start() const { return uintptr_t(&data); } uintptr_t end() const { return uintptr_t(this) + ChunkSize; } }; -static_assert(sizeof(js::NurseryChunk) == gc::ChunkSize, - "Nursery chunk size must match gc::Chunk size."); +static_assert(sizeof(NurseryChunk) == ChunkSize, + "Nursery chunk size must match Chunk size."); +static_assert(offsetof(NurseryChunk, data) == NurseryChunkHeaderSize); class NurseryDecommitTask : public GCParallelTask { public: explicit NurseryDecommitTask(gc::GCRuntime* gc); - bool reserveSpaceForBytes(size_t nbytes); + bool reserveSpaceForChunks(size_t nchunks); bool isEmpty(const AutoLockHelperThreadState& lock) const; void queueChunk(NurseryChunk* chunk, const AutoLockHelperThreadState& lock); - void queueRange(size_t newCapacity, NurseryChunk& chunk, + void queueRange(size_t newCapacity, NurseryChunk* chunk, const AutoLockHelperThreadState& lock); private: + struct Region { + NurseryChunk* chunk; + size_t startOffset; + }; + using NurseryChunkVector = Vector<NurseryChunk*, 0, SystemAllocPolicy>; + using RegionVector = Vector<Region, 2, SystemAllocPolicy>; void run(AutoLockHelperThreadState& lock) override; @@ -98,25 +113,21 @@ class NurseryDecommitTask : public GCParallelTask { const NurseryChunkVector& chunksToDecommit() const { return chunksToDecommit_.ref(); } + RegionVector& regionsToDecommit() { return regionsToDecommit_.ref(); } + const RegionVector& regionsToDecommit() const { + return regionsToDecommit_.ref(); + } MainThreadOrGCTaskData<NurseryChunkVector> chunksToDecommit_; - - MainThreadOrGCTaskData<NurseryChunk*> partialChunk; - MainThreadOrGCTaskData<size_t> partialCapacity; + MainThreadOrGCTaskData<RegionVector> regionsToDecommit_; }; } // namespace js -inline void js::NurseryChunk::poisonAndInit(JSRuntime* rt, size_t size) { - MOZ_ASSERT(size >= sizeof(ChunkBase)); - MOZ_ASSERT(size <= ChunkSize); - poisonRange(0, size, JS_FRESH_NURSERY_PATTERN, MemCheckKind::MakeUndefined); - new (this) NurseryChunk(rt); -} - inline void js::NurseryChunk::poisonRange(size_t start, size_t end, uint8_t value, MemCheckKind checkKind) { + MOZ_ASSERT(start >= NurseryChunkHeaderSize); MOZ_ASSERT((start % gc::CellAlignBytes) == 0); MOZ_ASSERT((end % gc::CellAlignBytes) == 0); MOZ_ASSERT(end >= start); @@ -132,12 +143,12 @@ inline void js::NurseryChunk::poisonRange(size_t start, size_t end, } inline void js::NurseryChunk::poisonAfterEvict(size_t extent) { - poisonRange(sizeof(ChunkBase), extent, JS_SWEPT_NURSERY_PATTERN, + poisonRange(NurseryChunkHeaderSize, extent, JS_SWEPT_NURSERY_PATTERN, MemCheckKind::MakeNoAccess); } inline void js::NurseryChunk::markPagesUnusedHard(size_t startOffset) { - MOZ_ASSERT(startOffset >= sizeof(ChunkBase)); // Don't touch the header. + MOZ_ASSERT(startOffset >= NurseryChunkHeaderSize); // Don't touch the header. MOZ_ASSERT(startOffset >= SystemPageSize()); MOZ_ASSERT(startOffset <= ChunkSize); uintptr_t start = uintptr_t(this) + startOffset; @@ -146,7 +157,7 @@ inline void js::NurseryChunk::markPagesUnusedHard(size_t startOffset) { } inline bool js::NurseryChunk::markPagesInUseHard(size_t endOffset) { - MOZ_ASSERT(endOffset >= sizeof(ChunkBase)); + MOZ_ASSERT(endOffset >= NurseryChunkHeaderSize); MOZ_ASSERT(endOffset >= SystemPageSize()); MOZ_ASSERT(endOffset <= ChunkSize); uintptr_t start = uintptr_t(this) + SystemPageSize(); @@ -155,23 +166,25 @@ inline bool js::NurseryChunk::markPagesInUseHard(size_t endOffset) { } // static -inline js::NurseryChunk* js::NurseryChunk::fromChunk(TenuredChunk* chunk) { - return reinterpret_cast<NurseryChunk*>(chunk); +inline js::NurseryChunk* js::NurseryChunk::fromChunk(TenuredChunk* chunk, + ChunkKind kind, + uint8_t index) { + return new (chunk) NurseryChunk(chunk->runtime, kind, index); } js::NurseryDecommitTask::NurseryDecommitTask(gc::GCRuntime* gc) : GCParallelTask(gc, gcstats::PhaseKind::NONE) { // This can occur outside GCs so doesn't have a stats phase. + MOZ_ALWAYS_TRUE(regionsToDecommit().reserve(2)); } bool js::NurseryDecommitTask::isEmpty( const AutoLockHelperThreadState& lock) const { - return chunksToDecommit().empty() && !partialChunk; + return chunksToDecommit().empty() && regionsToDecommit().empty(); } -bool js::NurseryDecommitTask::reserveSpaceForBytes(size_t nbytes) { +bool js::NurseryDecommitTask::reserveSpaceForChunks(size_t nchunks) { MOZ_ASSERT(isIdle()); - size_t nchunks = HowMany(nbytes, ChunkSize); return chunksToDecommit().reserve(nchunks); } @@ -182,15 +195,14 @@ void js::NurseryDecommitTask::queueChunk( } void js::NurseryDecommitTask::queueRange( - size_t newCapacity, NurseryChunk& newChunk, + size_t newCapacity, NurseryChunk* chunk, const AutoLockHelperThreadState& lock) { MOZ_ASSERT(isIdle(lock)); - MOZ_ASSERT(!partialChunk); + MOZ_ASSERT(regionsToDecommit_.ref().length() < 2); MOZ_ASSERT(newCapacity < ChunkSize); MOZ_ASSERT(newCapacity % SystemPageSize() == 0); - partialChunk = &newChunk; - partialCapacity = newCapacity; + regionsToDecommit().infallibleAppend(Region{chunk, newCapacity}); } void js::NurseryDecommitTask::run(AutoLockHelperThreadState& lock) { @@ -204,25 +216,20 @@ void js::NurseryDecommitTask::run(AutoLockHelperThreadState& lock) { gc->recycleChunk(tenuredChunk, lock); } - if (partialChunk) { - { - AutoUnlockHelperThreadState unlock(lock); - partialChunk->markPagesUnusedHard(partialCapacity); - } - partialChunk = nullptr; - partialCapacity = 0; + while (!regionsToDecommit().empty()) { + Region region = regionsToDecommit().popCopy(); + AutoUnlockHelperThreadState unlock(lock); + region.chunk->markPagesUnusedHard(region.startOffset); } } js::Nursery::Nursery(GCRuntime* gc) - : position_(0), - currentEnd_(0), + : toSpace(ChunkKind::NurseryToSpace), + fromSpace(ChunkKind::NurseryFromSpace), gc(gc), - currentChunk_(0), - startChunk_(0), - startPosition_(0), capacity_(0), enableProfiling_(false), + semispaceEnabled_(gc::TuningDefaults::SemispaceNurseryEnabled), canAllocateStrings_(true), canAllocateBigInts_(true), reportDeduplications_(false), @@ -232,6 +239,11 @@ js::Nursery::Nursery(GCRuntime* gc) prevPosition_(0), hasRecentGrowthData(false), smoothedTargetSize(0.0) { + // Try to keep fields used by allocation fast path together at the start of + // the nursery. + static_assert(offsetof(Nursery, toSpace.position_) < TypicalCacheLineSize); + static_assert(offsetof(Nursery, toSpace.currentEnd_) < TypicalCacheLineSize); + const char* env = getenv("MOZ_NURSERY_STRINGS"); if (env && *env) { canAllocateStrings_ = (*env == '1'); @@ -316,12 +328,13 @@ bool js::Nursery::init(AutoLockGCBgAlloc& lock) { js::Nursery::~Nursery() { disable(); } void js::Nursery::enable() { - MOZ_ASSERT(isEmpty()); - MOZ_ASSERT(!gc->isVerifyPreBarriersEnabled()); if (isEnabled()) { return; } + MOZ_ASSERT(isEmpty()); + MOZ_ASSERT(!gc->isVerifyPreBarriersEnabled()); + { AutoLockGCBgAlloc lock(gc); if (!initFirstChunk(lock)) { @@ -344,25 +357,60 @@ void js::Nursery::enable() { bool js::Nursery::initFirstChunk(AutoLockGCBgAlloc& lock) { MOZ_ASSERT(!isEnabled()); + MOZ_ASSERT(toSpace.chunks_.length() == 0); + MOZ_ASSERT(fromSpace.chunks_.length() == 0); - capacity_ = tunables().gcMinNurseryBytes(); + setCapacity(minSpaceSize()); - if (!decommitTask->reserveSpaceForBytes(capacity_) || - !allocateNextChunk(0, lock)) { - capacity_ = 0; + size_t nchunks = toSpace.maxChunkCount_ + fromSpace.maxChunkCount_; + if (!decommitTask->reserveSpaceForChunks(nchunks) || + !allocateNextChunk(lock)) { + setCapacity(0); + MOZ_ASSERT(toSpace.isEmpty()); + MOZ_ASSERT(fromSpace.isEmpty()); return false; } - moveToStartOfChunk(0); - setStartToCurrentPosition(); + toSpace.moveToStartOfChunk(this, 0); + toSpace.setStartToCurrentPosition(); + + if (semispaceEnabled_) { + fromSpace.moveToStartOfChunk(this, 0); + fromSpace.setStartToCurrentPosition(); + } + + MOZ_ASSERT(toSpace.isEmpty()); + MOZ_ASSERT(fromSpace.isEmpty()); + poisonAndInitCurrentChunk(); // Clear any information about previous collections. clearRecentGrowthData(); + tenureThreshold_ = 0; + +#ifdef DEBUG + toSpace.checkKind(ChunkKind::NurseryToSpace); + fromSpace.checkKind(ChunkKind::NurseryFromSpace); +#endif + return true; } +size_t RequiredChunkCount(size_t nbytes) { + return nbytes <= ChunkSize ? 1 : nbytes / ChunkSize; +} + +void js::Nursery::setCapacity(size_t newCapacity) { + MOZ_ASSERT(newCapacity == roundSize(newCapacity)); + capacity_ = newCapacity; + size_t count = RequiredChunkCount(newCapacity); + toSpace.maxChunkCount_ = count; + if (semispaceEnabled_) { + fromSpace.maxChunkCount_ = count; + } +} + void js::Nursery::disable() { MOZ_ASSERT(isEmpty()); if (!isEnabled()) { @@ -371,15 +419,19 @@ void js::Nursery::disable() { // Free all chunks. decommitTask->join(); - freeChunksFrom(0); + freeChunksFrom(toSpace, 0); + freeChunksFrom(fromSpace, 0); decommitTask->runFromMainThread(); - capacity_ = 0; + setCapacity(0); // We must reset currentEnd_ so that there is no space for anything in the // nursery. JIT'd code uses this even if the nursery is disabled. - currentEnd_ = 0; - position_ = 0; + toSpace = Space(ChunkKind::NurseryToSpace); + fromSpace = Space(ChunkKind::NurseryFromSpace); + MOZ_ASSERT(toSpace.isEmpty()); + MOZ_ASSERT(fromSpace.isEmpty()); + gc->storeBuffer().disable(); if (gc->wasInitialized()) { @@ -464,16 +516,59 @@ void js::Nursery::discardCodeAndSetJitFlagsForZone(JS::Zone* zone) { } } +void js::Nursery::setSemispaceEnabled(bool enabled) { + if (semispaceEnabled() == enabled) { + return; + } + + bool wasEnabled = isEnabled(); + if (wasEnabled) { + if (!isEmpty()) { + gc->minorGC(JS::GCReason::EVICT_NURSERY); + } + disable(); + } + + semispaceEnabled_ = enabled; + + if (wasEnabled) { + enable(); + } +} + bool js::Nursery::isEmpty() const { + MOZ_ASSERT(fromSpace.isEmpty()); + if (!isEnabled()) { return true; } if (!gc->hasZealMode(ZealMode::GenerationalGC)) { - MOZ_ASSERT(startChunk_ == 0); - MOZ_ASSERT(startPosition_ == chunk(0).start()); + MOZ_ASSERT(startChunk() == 0); + MOZ_ASSERT(startPosition() == chunk(0).start()); } - return position() == startPosition_; + + return toSpace.isEmpty(); +} + +bool js::Nursery::Space::isEmpty() const { return position_ == startPosition_; } + +static size_t AdjustSizeForSemispace(size_t size, bool semispaceEnabled) { + if (!semispaceEnabled) { + return size; + } + + return Nursery::roundSize(size / 2); +} + +size_t js::Nursery::maxSpaceSize() const { + return AdjustSizeForSemispace(tunables().gcMaxNurseryBytes(), + semispaceEnabled_); +} + +size_t js::Nursery::minSpaceSize() const { + return AdjustSizeForSemispace(tunables().gcMinNurseryBytes(), + semispaceEnabled_); } #ifdef JS_GC_ZEAL @@ -501,9 +596,10 @@ void js::Nursery::enterZealMode() { MemCheckKind::MakeUndefined); } - capacity_ = RoundUp(tunables().gcMaxNurseryBytes(), ChunkSize); + setCapacity(maxSpaceSize()); - if (!decommitTask->reserveSpaceForBytes(capacity_)) { + size_t nchunks = toSpace.maxChunkCount_ + fromSpace.maxChunkCount_; + if (!decommitTask->reserveSpaceForChunks(nchunks)) { oomUnsafe.crash("Nursery::enterZealMode"); } @@ -517,8 +613,14 @@ void js::Nursery::leaveZealMode() { MOZ_ASSERT(isEmpty()); - moveToStartOfChunk(0); - setStartToCurrentPosition(); + toSpace.moveToStartOfChunk(this, 0); + toSpace.setStartToCurrentPosition(); + + if (semispaceEnabled_) { + fromSpace.moveToStartOfChunk(this, 0); + fromSpace.setStartToCurrentPosition(); + } + poisonAndInitCurrentChunk(); } #endif // JS_GC_ZEAL @@ -573,7 +675,7 @@ MOZ_NEVER_INLINE JS::GCReason Nursery::handleAllocationFailure() { } bool Nursery::moveToNextChunk() { - unsigned chunkno = currentChunk_ + 1; + unsigned chunkno = currentChunk() + 1; MOZ_ASSERT(chunkno <= maxChunkCount()); MOZ_ASSERT(chunkno <= allocatedChunkCount()); if (chunkno == maxChunkCount()) { @@ -584,7 +686,7 @@ bool Nursery::moveToNextChunk() { TimeStamp start = TimeStamp::Now(); { AutoLockGCBgAlloc lock(gc); - if (!allocateNextChunk(chunkno, lock)) { + if (!allocateNextChunk(lock)) { return false; } } @@ -688,16 +790,16 @@ void* js::Nursery::reallocateBuffer(Zone* zone, Cell* cell, void* oldBuffer, } if (!isInside(oldBuffer)) { - MOZ_ASSERT(mallocedBufferBytes >= oldBytes); + MOZ_ASSERT(toSpace.mallocedBufferBytes >= oldBytes); void* newBuffer = zone->pod_realloc<uint8_t>((uint8_t*)oldBuffer, oldBytes, newBytes); if (newBuffer) { if (oldBuffer != newBuffer) { MOZ_ALWAYS_TRUE( - mallocedBuffers.rekeyAs(oldBuffer, newBuffer, newBuffer)); + toSpace.mallocedBuffers.rekeyAs(oldBuffer, newBuffer, newBuffer)); } - mallocedBufferBytes -= oldBytes; - mallocedBufferBytes += newBytes; + toSpace.mallocedBufferBytes -= oldBytes; + toSpace.mallocedBufferBytes += newBytes; } return newBuffer; } @@ -723,27 +825,21 @@ void js::Nursery::freeBuffer(void* buffer, size_t nbytes) { #ifdef DEBUG /* static */ -inline bool Nursery::checkForwardingPointerLocation(void* ptr, - bool expectedInside) { - if (isInside(ptr) == expectedInside) { - return true; - } - +inline bool Nursery::checkForwardingPointerInsideNursery(void* ptr) { // If a zero-capacity elements header lands right at the end of a chunk then // elements data will appear to be in the next chunk. If we have a pointer to // the very start of a chunk, check the previous chunk. - if ((uintptr_t(ptr) & ChunkMask) == 0 && - isInside(reinterpret_cast<uint8_t*>(ptr) - 1) == expectedInside) { - return true; + if ((uintptr_t(ptr) & ChunkMask) == 0) { + return isInside(reinterpret_cast<uint8_t*>(ptr) - 1); } - return false; + return isInside(ptr); } #endif void Nursery::setIndirectForwardingPointer(void* oldData, void* newData) { - MOZ_ASSERT(checkForwardingPointerLocation(oldData, true)); - MOZ_ASSERT(checkForwardingPointerLocation(newData, false)); + MOZ_ASSERT(checkForwardingPointerInsideNursery(oldData)); + // |newData| may be either in the nursery or in the malloc heap. AutoEnterOOMUnsafeRegion oomUnsafe; #ifdef DEBUG @@ -791,7 +887,7 @@ void js::Nursery::forwardBufferPointer(uintptr_t* pSlotsElems) { MOZ_ASSERT(IsWriteableAddress(buffer)); } - MOZ_ASSERT(!isInside(buffer)); + MOZ_ASSERT_IF(isInside(buffer), !inCollectedRegion(buffer)); *pSlotsElems = reinterpret_cast<uintptr_t>(buffer); } @@ -1063,7 +1159,7 @@ bool js::Nursery::wantEagerCollection() const { return false; } - if (isEmpty() && capacity() == tunables().gcMinNurseryBytes()) { + if (isEmpty() && capacity() == minSpaceSize()) { return false; } @@ -1108,7 +1204,7 @@ inline bool js::Nursery::isUnderused() const { return false; } - if (capacity() == tunables().gcMinNurseryBytes()) { + if (capacity() == minSpaceSize()) { return false; } @@ -1126,8 +1222,8 @@ void js::Nursery::collect(JS::GCOptions options, JS::GCReason reason) { MOZ_ASSERT(!rt->mainContextFromOwnThread()->suppressGC); if (minorGCRequested()) { - MOZ_ASSERT(position_ == chunk(currentChunk_).end()); - position_ = prevPosition_; + MOZ_ASSERT(position() == chunk(currentChunk()).end()); + toSpace.position_ = prevPosition_; prevPosition_ = 0; minorGCTriggerReason_ = JS::GCReason::NO_REASON; rt->mainContextFromOwnThread()->clearPendingInterrupt( @@ -1141,7 +1237,7 @@ void js::Nursery::collect(JS::GCOptions options, JS::GCReason reason) { // freed after this point. gc->storeBuffer().clear(); - MOZ_ASSERT(!pretenuringNursery.hasAllocatedSites()); + MOZ_ASSERT_IF(!semispaceEnabled_, !pretenuringNursery.hasAllocatedSites()); } if (!isEnabled()) { @@ -1162,10 +1258,11 @@ void js::Nursery::collect(JS::GCOptions options, JS::GCReason reason) { previousGC.reason = JS::GCReason::NO_REASON; previousGC.nurseryUsedBytes = usedSpace(); previousGC.nurseryCapacity = capacity(); - previousGC.nurseryCommitted = committed(); - previousGC.nurseryUsedChunkCount = currentChunk_ + 1; + previousGC.nurseryCommitted = totalCommitted(); + previousGC.nurseryUsedChunkCount = currentChunk() + 1; previousGC.tenuredBytes = 0; previousGC.tenuredCells = 0; + tenuredEverything = true; // If it isn't empty, it will call doCollection, and possibly after that // isEmpty() will become true, so use another variable to keep track of the @@ -1177,29 +1274,19 @@ void js::Nursery::collect(JS::GCOptions options, JS::GCReason reason) { // space does not represent data that can be tenured MOZ_ASSERT(result.tenuredBytes <= (previousGC.nurseryUsedBytes - - (sizeof(ChunkBase) * previousGC.nurseryUsedChunkCount))); + (NurseryChunkHeaderSize * previousGC.nurseryUsedChunkCount))); previousGC.reason = reason; previousGC.tenuredBytes = result.tenuredBytes; previousGC.tenuredCells = result.tenuredCells; - previousGC.nurseryUsedChunkCount = currentChunk_ + 1; + previousGC.nurseryUsedChunkCount = currentChunk() + 1; } // Resize the nursery. maybeResizeNursery(options, reason); - // Poison/initialise the first chunk. - if (previousGC.nurseryUsedBytes) { - // In most cases Nursery::clear() has not poisoned this chunk or marked it - // as NoAccess; so we only need to poison the region used during the last - // cycle. Also, if the heap was recently expanded we don't want to - // re-poison the new memory. In both cases we only need to poison until - // previousGC.nurseryUsedBytes. - // - // In cases where this is not true, like generational zeal mode or subchunk - // mode, poisonAndInitCurrentChunk() will ignore its parameter. It will - // also clamp the parameter. - poisonAndInitCurrentChunk(previousGC.nurseryUsedBytes); + if (!semispaceEnabled()) { + poisonAndInitCurrentChunk(); } bool validPromotionRate; @@ -1207,19 +1294,10 @@ void js::Nursery::collect(JS::GCOptions options, JS::GCReason reason) { startProfile(ProfileKey::Pretenure); size_t sitesPretenured = 0; - if (!wasEmpty) { - sitesPretenured = - doPretenuring(rt, reason, validPromotionRate, promotionRate); - } + sitesPretenured = + doPretenuring(rt, reason, validPromotionRate, promotionRate); endProfile(ProfileKey::Pretenure); - // We ignore gcMaxBytes when allocating for minor collection. However, if we - // overflowed, we disable the nursery. The next time we allocate, we'll fail - // because bytes >= gcMaxBytes. - if (gc->heapSize.bytes() >= tunables().gcMaxBytes()) { - disable(); - } - previousGC.endTime = TimeStamp::Now(); // Must happen after maybeResizeNursery. endProfile(ProfileKey::Total); @@ -1268,7 +1346,7 @@ void js::Nursery::sendTelemetry(JS::GCReason reason, TimeDuration totalTime, rt->metrics().GC_MINOR_REASON_LONG(uint32_t(reason)); } rt->metrics().GC_MINOR_US(totalTime); - rt->metrics().GC_NURSERY_BYTES_2(committed()); + rt->metrics().GC_NURSERY_BYTES_2(totalCommitted()); if (!wasEmpty) { rt->metrics().GC_PRETENURE_COUNT_2(sitesPretenured); @@ -1289,14 +1367,30 @@ void js::Nursery::printDeduplicationData(js::StringStats& prev, } } -void js::Nursery::freeTrailerBlocks(void) { +void js::Nursery::freeTrailerBlocks(JS::GCOptions options, + JS::GCReason reason) { + fromSpace.freeTrailerBlocks(mallocedBlockCache_); + + if (options == JS::GCOptions::Shrink || gc::IsOOMReason(reason)) { + mallocedBlockCache_.clear(); + return; + } + + // Discard blocks from the cache at 0.05% per megabyte of nursery capacity, + // that is, 0.8% of blocks for a 16-megabyte nursery. This allows the cache + // to gradually discard unneeded blocks in long running applications. + mallocedBlockCache_.preen(0.05 * double(capacity()) / (1024.0 * 1024.0)); +} + +void js::Nursery::Space::freeTrailerBlocks( + MallocedBlockCache& mallocedBlockCache) { // This routine frees those blocks denoted by the set // // trailersAdded_ (all of it) // - trailersRemoved_ (entries with index below trailersRemovedUsed_) // // For each block, places it back on the nursery's small-malloced-block pool - // by calling mallocedBlockCache_.free. + // by calling mallocedBlockCache.free. MOZ_ASSERT(trailersAdded_.length() == trailersRemoved_.length()); MOZ_ASSERT(trailersRemovedUsed_ <= trailersRemoved_.length()); @@ -1321,7 +1415,7 @@ void js::Nursery::freeTrailerBlocks(void) { if (!std::binary_search(trailersRemoved_.begin(), trailersRemoved_.begin() + trailersRemovedUsed_, blockPointer)) { - mallocedBlockCache_.free(block); + mallocedBlockCache.free(block); } } } else { @@ -1348,7 +1442,7 @@ void js::Nursery::freeTrailerBlocks(void) { const PointerAndUint7 blockAdded = trailersAdded_[iAdded]; const void* blockRemoved = trailersRemoved_[iRemoved]; if (blockAdded.pointer() < blockRemoved) { - mallocedBlockCache_.free(blockAdded); + mallocedBlockCache.free(blockAdded); continue; } // If this doesn't hold @@ -1362,7 +1456,7 @@ void js::Nursery::freeTrailerBlocks(void) { // added set. for (/*keep going*/; iAdded < nAdded; iAdded++) { const PointerAndUint7 block = trailersAdded_[iAdded]; - mallocedBlockCache_.free(block); + mallocedBlockCache.free(block); } } @@ -1371,17 +1465,14 @@ void js::Nursery::freeTrailerBlocks(void) { trailersRemoved_.clear(); trailersRemovedUsed_ = 0; trailerBytes_ = 0; - - // Discard blocks from the cache at 0.05% per megabyte of nursery capacity, - // that is, 0.8% of blocks for a 16-megabyte nursery. This allows the cache - // to gradually discard unneeded blocks in long running applications. - mallocedBlockCache_.preen(0.05 * double(capacity()) / (1024.0 * 1024.0)); } size_t Nursery::sizeOfTrailerBlockSets( mozilla::MallocSizeOf mallocSizeOf) const { - return trailersAdded_.sizeOfExcludingThis(mallocSizeOf) + - trailersRemoved_.sizeOfExcludingThis(mallocSizeOf); + MOZ_ASSERT(fromSpace.trailersAdded_.empty()); + MOZ_ASSERT(fromSpace.trailersRemoved_.empty()); + return toSpace.trailersAdded_.sizeOfExcludingThis(mallocSizeOf) + + toSpace.trailersRemoved_.sizeOfExcludingThis(mallocSizeOf); } js::Nursery::CollectionResult js::Nursery::doCollection(AutoGCSession& session, @@ -1393,8 +1484,19 @@ js::Nursery::CollectionResult js::Nursery::doCollection(AutoGCSession& session, AutoDisableProxyCheck disableStrictProxyChecking; mozilla::DebugOnly<AutoEnterOOMUnsafeRegion> oomUnsafeRegion; + // Swap nursery spaces. + swapSpaces(); + MOZ_ASSERT(toSpace.isEmpty()); + MOZ_ASSERT(toSpace.mallocedBuffers.empty()); + if (semispaceEnabled_) { + poisonAndInitCurrentChunk(); + } + + clearMapAndSetNurseryRanges(); + // Move objects pointed to by roots from the nursery to the major heap. - TenuringTracer mover(rt, this); + tenuredEverything = shouldTenureEverything(reason); + TenuringTracer mover(rt, this, tenuredEverything); // Trace everything considered as a root by a minor GC. traceRoots(session, mover); @@ -1433,17 +1535,14 @@ js::Nursery::CollectionResult js::Nursery::doCollection(AutoGCSession& session, // Sweep. startProfile(ProfileKey::FreeMallocedBuffers); - gc->queueBuffersForFreeAfterMinorGC(mallocedBuffers); - mallocedBufferBytes = 0; + gc->queueBuffersForFreeAfterMinorGC(fromSpace.mallocedBuffers); + fromSpace.mallocedBufferBytes = 0; endProfile(ProfileKey::FreeMallocedBuffers); // Give trailer blocks associated with non-tenured Wasm{Struct,Array}Objects // back to our `mallocedBlockCache_`. startProfile(ProfileKey::FreeTrailerBlocks); - freeTrailerBlocks(); - if (options == JS::GCOptions::Shrink || gc::IsOOMReason(reason)) { - mallocedBlockCache_.clear(); - } + freeTrailerBlocks(options, reason); endProfile(ProfileKey::FreeTrailerBlocks); startProfile(ProfileKey::ClearNursery); @@ -1466,7 +1565,28 @@ js::Nursery::CollectionResult js::Nursery::doCollection(AutoGCSession& session, #endif endProfile(ProfileKey::CheckHashTables); - return {mover.getTenuredSize(), mover.getTenuredCells()}; + if (semispaceEnabled_) { + // On the next collection, tenure everything before |tenureThreshold_|. + tenureThreshold_ = toSpace.offsetFromExclusiveAddress(position()); + } else { + // Swap nursery spaces back because we only use one. + swapSpaces(); + MOZ_ASSERT(toSpace.isEmpty()); + } + + MOZ_ASSERT(fromSpace.isEmpty()); + + if (semispaceEnabled_) { + poisonAndInitCurrentChunk(); + } + + return {mover.getPromotedSize(), mover.getPromotedCells()}; +} + +void js::Nursery::swapSpaces() { + std::swap(toSpace, fromSpace); + toSpace.setKind(ChunkKind::NurseryToSpace); + fromSpace.setKind(ChunkKind::NurseryFromSpace); } void js::Nursery::traceRoots(AutoGCSession& session, TenuringTracer& mover) { @@ -1490,14 +1610,12 @@ void js::Nursery::traceRoots(AutoGCSession& session, TenuringTracer& mover) { MOZ_ASSERT(gc->storeBuffer().isEnabled()); MOZ_ASSERT(gc->storeBuffer().isEmpty()); - // Strings in the whole cell buffer must be traced first, in order to mark - // tenured dependent strings' bases as non-deduplicatable. The rest of - // nursery collection (whole non-string cells, edges, etc.) can happen - // later. startProfile(ProfileKey::TraceWholeCells); sb.traceWholeCells(mover); endProfile(ProfileKey::TraceWholeCells); + cellsToSweep = sb.releaseCellSweepSet(); + startProfile(ProfileKey::TraceValues); sb.traceValues(mover); endProfile(ProfileKey::TraceValues); @@ -1523,8 +1641,6 @@ void js::Nursery::traceRoots(AutoGCSession& session, TenuringTracer& mover) { endProfile(ProfileKey::MarkRuntime); } - MOZ_ASSERT(gc->storeBuffer().isEmpty()); - startProfile(ProfileKey::MarkDebugger); { gcstats::AutoPhase ap(stats(), gcstats::PhaseKind::MARK_ROOTS); @@ -1533,6 +1649,15 @@ void js::Nursery::traceRoots(AutoGCSession& session, TenuringTracer& mover) { endProfile(ProfileKey::MarkDebugger); } +bool js::Nursery::shouldTenureEverything(JS::GCReason reason) { + if (!semispaceEnabled()) { + return true; + } + + return reason == JS::GCReason::EVICT_NURSERY || + reason == JS::GCReason::DISABLE_GENERATIONAL_GC; +} + size_t js::Nursery::doPretenuring(JSRuntime* rt, JS::GCReason reason, bool validPromotionRate, double promotionRate) { @@ -1590,12 +1715,13 @@ bool js::Nursery::registerMallocedBuffer(void* buffer, size_t nbytes) { MOZ_ASSERT(buffer); MOZ_ASSERT(nbytes > 0); MOZ_ASSERT(!isInside(buffer)); - if (!mallocedBuffers.putNew(buffer)) { + + if (!toSpace.mallocedBuffers.putNew(buffer)) { return false; } - mallocedBufferBytes += nbytes; - if (MOZ_UNLIKELY(mallocedBufferBytes > capacity() * 8)) { + toSpace.mallocedBufferBytes += nbytes; + if (MOZ_UNLIKELY(toSpace.mallocedBufferBytes > capacity() * 8)) { requestMinorGC(JS::GCReason::NURSERY_MALLOC_BUFFERS); } @@ -1613,21 +1739,19 @@ bool js::Nursery::registerMallocedBuffer(void* buffer, size_t nbytes) { Nursery::WasBufferMoved js::Nursery::maybeMoveRawBufferOnPromotion( void** bufferp, gc::Cell* owner, size_t nbytes, MemoryUse use, arena_id_t arena) { - MOZ_ASSERT(!IsInsideNursery(owner)); - void* buffer = *bufferp; if (!isInside(buffer)) { - // This is a malloced buffer. Remove it from the nursery's list of buffers - // so we don't free it and add it to the memory accounting for the zone + // This is a malloced buffer. Remove it from the nursery's previous list of + // buffers so we don't free it. removeMallocedBufferDuringMinorGC(buffer); - AddCellMemory(owner, nbytes, use); + trackMallocedBufferOnPromotion(buffer, owner, nbytes, use); return BufferNotMoved; } // Copy the nursery-allocated buffer into a new malloc allocation. AutoEnterOOMUnsafeRegion oomUnsafe; - Zone* zone = owner->asTenured().zone(); + Zone* zone = owner->zone(); void* movedBuffer = zone->pod_arena_malloc<uint8_t>(arena, nbytes); if (!movedBuffer) { oomUnsafe.crash("Nursery::updateBufferOnPromotion"); @@ -1635,38 +1759,111 @@ Nursery::WasBufferMoved js::Nursery::maybeMoveRawBufferOnPromotion( memcpy(movedBuffer, buffer, nbytes); - AddCellMemory(owner, nbytes, use); + trackMallocedBufferOnPromotion(movedBuffer, owner, nbytes, use); *bufferp = movedBuffer; return BufferMoved; } +void js::Nursery::trackMallocedBufferOnPromotion(void* buffer, gc::Cell* owner, + size_t nbytes, MemoryUse use) { + if (owner->isTenured()) { + // If we tenured the owner then account for the memory. + AddCellMemory(owner, nbytes, use); + return; + } + + // Otherwise add it to the nursery's new buffer list. + AutoEnterOOMUnsafeRegion oomUnsafe; + if (!registerMallocedBuffer(buffer, nbytes)) { + oomUnsafe.crash("Nursery::trackMallocedBufferOnPromotion"); + } +} + +void js::Nursery::trackTrailerOnPromotion(void* buffer, gc::Cell* owner, + size_t nbytes, size_t overhead, + MemoryUse use) { + MOZ_ASSERT(!isInside(buffer)); + unregisterTrailer(buffer); + + if (owner->isTenured()) { + // If we tenured the owner then account for the memory. + AddCellMemory(owner, nbytes + overhead, use); + return; + } + + // Otherwise add it to the nursery's new buffer list. + PointerAndUint7 blockAndListID(buffer, + MallocedBlockCache::listIDForSize(nbytes)); + AutoEnterOOMUnsafeRegion oomUnsafe; + if (!registerTrailer(blockAndListID, nbytes)) { + oomUnsafe.crash("Nursery::trackTrailerOnPromotion"); + } +} + void Nursery::requestMinorGC(JS::GCReason reason) { + JS::HeapState heapState = runtime()->heapState(); +#ifdef DEBUG + if (heapState == JS::HeapState::Idle || + heapState == JS::HeapState::MinorCollecting) { + MOZ_ASSERT(CurrentThreadCanAccessRuntime(runtime())); + } else if (heapState == JS::HeapState::MajorCollecting) { + // The GC runs sweeping tasks that may access the storebuffer in parallel + // and these require taking the store buffer lock. + MOZ_ASSERT(CurrentThreadIsGCSweeping()); + runtime()->gc.assertCurrentThreadHasLockedStoreBuffer(); + } else { + MOZ_CRASH("Unexpected heap state"); + } +#endif + MOZ_ASSERT(reason != JS::GCReason::NO_REASON); - MOZ_ASSERT(CurrentThreadCanAccessRuntime(runtime())); MOZ_ASSERT(isEnabled()); if (minorGCRequested()) { return; } + if (heapState == JS::HeapState::MinorCollecting) { + // This can happen when we promote a lot of data to the second generation in + // a semispace collection. This can trigger a GC due to the amount of store + // buffer entries added. + return; + } + // Set position to end of chunk to block further allocation. MOZ_ASSERT(prevPosition_ == 0); - prevPosition_ = position_; - position_ = chunk(currentChunk_).end(); + prevPosition_ = position(); + toSpace.position_ = chunk(currentChunk()).end(); minorGCTriggerReason_ = reason; - runtime()->mainContextFromOwnThread()->requestInterrupt( + runtime()->mainContextFromAnyThread()->requestInterrupt( InterruptReason::MinorGC); } +size_t SemispaceSizeFactor(bool semispaceEnabled) { + return semispaceEnabled ? 2 : 1; +} + +size_t js::Nursery::totalCapacity() const { + return capacity() * SemispaceSizeFactor(semispaceEnabled_); +} + +size_t js::Nursery::totalCommitted() const { + size_t size = std::min(capacity_, allocatedChunkCount() * gc::ChunkSize); + return size * SemispaceSizeFactor(semispaceEnabled_); +} + size_t Nursery::sizeOfMallocedBuffers( mozilla::MallocSizeOf mallocSizeOf) const { + MOZ_ASSERT(fromSpace.mallocedBuffers.empty()); + size_t total = 0; - for (BufferSet::Range r = mallocedBuffers.all(); !r.empty(); r.popFront()) { + for (BufferSet::Range r = toSpace.mallocedBuffers.all(); !r.empty(); + r.popFront()) { total += mallocSizeOf(r.front()); } - total += mallocedBuffers.shallowSizeOfExcludingThis(mallocSizeOf); + total += toSpace.mallocedBuffers.shallowSizeOfExcludingThis(mallocSizeOf); return total; } @@ -1680,121 +1877,171 @@ void js::Nursery::sweep() { // Sweep unique IDs first before we sweep any tables that may be keyed based // on them. - for (Cell* cell : cellsWithUid_) { + cellsWithUid_.mutableEraseIf([](Cell*& cell) { auto* obj = static_cast<JSObject*>(cell); if (!IsForwarded(obj)) { gc::RemoveUniqueId(obj); - } else { - JSObject* dst = Forwarded(obj); - gc::TransferUniqueId(dst, obj); + return true; } - } - cellsWithUid_.clear(); + + JSObject* dst = Forwarded(obj); + gc::TransferUniqueId(dst, obj); + + if (!IsInsideNursery(dst)) { + return true; + } + + cell = dst; + return false; + }); for (ZonesIter zone(runtime(), SkipAtoms); !zone.done(); zone.next()) { zone->sweepAfterMinorGC(&trc); } sweepMapAndSetObjects(); + cellsToSweep.sweep(); + CellSweepSet empty; + std::swap(cellsToSweep, empty); runtime()->caches().sweepAfterMinorGC(&trc); } +void gc::CellSweepSet::sweep() { + if (head_) { + head_->sweepDependentStrings(); + head_ = nullptr; + } + if (storage_) { + storage_->freeAll(); + } +} + void js::Nursery::clear() { + fromSpace.clear(this); + MOZ_ASSERT(fromSpace.isEmpty()); +} + +void js::Nursery::Space::clear(Nursery* nursery) { + GCRuntime* gc = nursery->gc; + // Poison the nursery contents so touching a freed object will crash. unsigned firstClearChunk; - if (gc->hasZealMode(ZealMode::GenerationalGC)) { - // Poison all the chunks used in this cycle. The new start chunk is - // reposioned in Nursery::collect() but there's no point optimising that in - // this case. + if (gc->hasZealMode(ZealMode::GenerationalGC) || nursery->semispaceEnabled_) { + // Poison all the chunks used in this cycle. firstClearChunk = startChunk_; } else { - // In normal mode we start at the second chunk, the first one will be used + // Poison from the second chunk onwards as the first one will be used // in the next cycle and poisoned in Nusery::collect(); MOZ_ASSERT(startChunk_ == 0); firstClearChunk = 1; } for (unsigned i = firstClearChunk; i < currentChunk_; ++i) { - chunk(i).poisonAfterEvict(); + chunks_[i]->poisonAfterEvict(); } // Clear only the used part of the chunk because that's the part we touched, // but only if it's not going to be re-used immediately (>= firstClearChunk). if (currentChunk_ >= firstClearChunk) { - chunk(currentChunk_) - .poisonAfterEvict(position() - chunk(currentChunk_).start()); + size_t usedBytes = position_ - chunks_[currentChunk_]->start(); + chunks_[currentChunk_]->poisonAfterEvict(NurseryChunkHeaderSize + + usedBytes); } // Reset the start chunk & position if we're not in this zeal mode, or we're // in it and close to the end of the nursery. - MOZ_ASSERT(maxChunkCount() > 0); + MOZ_ASSERT(maxChunkCount_ > 0); if (!gc->hasZealMode(ZealMode::GenerationalGC) || - (gc->hasZealMode(ZealMode::GenerationalGC) && - currentChunk_ + 1 == maxChunkCount())) { - moveToStartOfChunk(0); + currentChunk_ + 1 == maxChunkCount_) { + moveToStartOfChunk(nursery, 0); } // Set current start position for isEmpty checks. setStartToCurrentPosition(); } -MOZ_ALWAYS_INLINE void js::Nursery::moveToStartOfChunk(unsigned chunkno) { - MOZ_ASSERT(chunkno < allocatedChunkCount()); +void js::Nursery::moveToStartOfChunk(unsigned chunkno) { + toSpace.moveToStartOfChunk(this, chunkno); +} + +void js::Nursery::Space::moveToStartOfChunk(Nursery* nursery, + unsigned chunkno) { + MOZ_ASSERT(chunkno < chunks_.length()); currentChunk_ = chunkno; - position_ = chunk(chunkno).start(); - setCurrentEnd(); + position_ = chunks_[chunkno]->start(); + setCurrentEnd(nursery); MOZ_ASSERT(position_ != 0); MOZ_ASSERT(currentEnd_ > position_); // Check this cannot wrap. } -void js::Nursery::poisonAndInitCurrentChunk(size_t extent) { - if (gc->hasZealMode(ZealMode::GenerationalGC) || !isSubChunkMode()) { - chunk(currentChunk_).poisonAndInit(runtime()); - } else { - extent = std::min(capacity_, extent); - chunk(currentChunk_).poisonAndInit(runtime(), extent); - } +void js::Nursery::poisonAndInitCurrentChunk() { + NurseryChunk& chunk = this->chunk(currentChunk()); + size_t start = position() - uintptr_t(&chunk); + size_t end = isSubChunkMode() ? capacity_ : ChunkSize; + chunk.poisonRange(start, end, JS_FRESH_NURSERY_PATTERN, + MemCheckKind::MakeUndefined); + new (&chunk) + NurseryChunk(runtime(), ChunkKind::NurseryToSpace, currentChunk()); } -MOZ_ALWAYS_INLINE void js::Nursery::setCurrentEnd() { - MOZ_ASSERT_IF(isSubChunkMode(), - currentChunk_ == 0 && currentEnd_ <= chunk(0).end()); - currentEnd_ = - uintptr_t(&chunk(currentChunk_)) + std::min(capacity_, ChunkSize); +void js::Nursery::setCurrentEnd() { toSpace.setCurrentEnd(this); } - MOZ_ASSERT_IF(!isSubChunkMode(), currentEnd_ == chunk(currentChunk_).end()); - MOZ_ASSERT(currentEnd_ != chunk(currentChunk_).start()); +void js::Nursery::Space::setCurrentEnd(Nursery* nursery) { + currentEnd_ = uintptr_t(chunks_[currentChunk_]) + + std::min(nursery->capacity(), ChunkSize); } -bool js::Nursery::allocateNextChunk(const unsigned chunkno, - AutoLockGCBgAlloc& lock) { - const unsigned priorCount = allocatedChunkCount(); +bool js::Nursery::allocateNextChunk(AutoLockGCBgAlloc& lock) { + // Allocate a new nursery chunk. If semispace collection is enabled, we have + // to allocate one for both spaces. + + const unsigned priorCount = toSpace.chunks_.length(); const unsigned newCount = priorCount + 1; - MOZ_ASSERT((chunkno == currentChunk_ + 1) || - (chunkno == 0 && allocatedChunkCount() == 0)); - MOZ_ASSERT(chunkno == allocatedChunkCount()); - MOZ_ASSERT(chunkno < HowMany(capacity(), ChunkSize)); + MOZ_ASSERT(newCount <= maxChunkCount()); + MOZ_ASSERT(fromSpace.chunks_.length() == + (semispaceEnabled_ ? priorCount : 0)); + + if (!toSpace.chunks_.reserve(newCount) || + (semispaceEnabled_ && !fromSpace.chunks_.reserve(newCount))) { + return false; + } - if (!chunks_.resize(newCount)) { + TenuredChunk* toSpaceChunk = gc->getOrAllocChunk(lock); + if (!toSpaceChunk) { return false; } - TenuredChunk* newChunk; - newChunk = gc->getOrAllocChunk(lock); - if (!newChunk) { - chunks_.shrinkTo(priorCount); + TenuredChunk* fromSpaceChunk = nullptr; + if (semispaceEnabled_ && !(fromSpaceChunk = gc->getOrAllocChunk(lock))) { + gc->recycleChunk(toSpaceChunk, lock); return false; } - chunks_[chunkno] = NurseryChunk::fromChunk(newChunk); + uint8_t index = toSpace.chunks_.length(); + NurseryChunk* nurseryChunk = + NurseryChunk::fromChunk(toSpaceChunk, ChunkKind::NurseryToSpace, index); + toSpace.chunks_.infallibleAppend(nurseryChunk); + + if (semispaceEnabled_) { + MOZ_ASSERT(index == fromSpace.chunks_.length()); + nurseryChunk = NurseryChunk::fromChunk(fromSpaceChunk, + ChunkKind::NurseryFromSpace, index); + fromSpace.chunks_.infallibleAppend(nurseryChunk); + } + return true; } -MOZ_ALWAYS_INLINE void js::Nursery::setStartToCurrentPosition() { +void js::Nursery::setStartToCurrentPosition() { + toSpace.setStartToCurrentPosition(); +} + +void js::Nursery::Space::setStartToCurrentPosition() { startChunk_ = currentChunk_; - startPosition_ = position(); + startPosition_ = position_; + MOZ_ASSERT(isEmpty()); } void js::Nursery::maybeResizeNursery(JS::GCOptions options, @@ -1809,8 +2056,7 @@ void js::Nursery::maybeResizeNursery(JS::GCOptions options, decommitTask->join(); size_t newCapacity = mozilla::Clamp(targetSize(options, reason), - tunables().gcMinNurseryBytes(), - tunables().gcMaxNurseryBytes()); + minSpaceSize(), maxSpaceSize()); MOZ_ASSERT(roundSize(newCapacity) == newCapacity); MOZ_ASSERT(newCapacity >= SystemPageSize()); @@ -1862,7 +2108,7 @@ size_t js::Nursery::targetSize(JS::GCOptions options, JS::GCReason reason) { TimeStamp now = TimeStamp::Now(); if (reason == JS::GCReason::PREPARE_FOR_PAGELOAD) { - return roundSize(tunables().gcMaxNurseryBytes()); + return roundSize(maxSpaceSize()); } // If the nursery is completely unused then minimise it. @@ -1960,38 +2206,58 @@ size_t js::Nursery::roundSize(size_t size) { } void js::Nursery::growAllocableSpace(size_t newCapacity) { - MOZ_ASSERT_IF(!isSubChunkMode(), newCapacity > currentChunk_ * ChunkSize); - MOZ_ASSERT(newCapacity <= tunables().gcMaxNurseryBytes()); + MOZ_ASSERT_IF(!isSubChunkMode(), newCapacity > currentChunk() * ChunkSize); + MOZ_ASSERT(newCapacity <= maxSpaceSize()); MOZ_ASSERT(newCapacity > capacity()); - if (!decommitTask->reserveSpaceForBytes(newCapacity)) { + size_t nchunks = + RequiredChunkCount(newCapacity) * SemispaceSizeFactor(semispaceEnabled_); + if (!decommitTask->reserveSpaceForChunks(nchunks)) { return; } if (isSubChunkMode()) { - MOZ_ASSERT(currentChunk_ == 0); - - // The remainder of the chunk may have been decommitted. - if (!chunk(0).markPagesInUseHard(std::min(newCapacity, ChunkSize))) { - // The OS won't give us the memory we need, we can't grow. + if (!toSpace.commitSubChunkRegion(capacity(), newCapacity) || + (semispaceEnabled_ && + !fromSpace.commitSubChunkRegion(capacity(), newCapacity))) { return; } + } - // The capacity has changed and since we were in sub-chunk mode we need to - // update the poison values / asan information for the now-valid region of - // this chunk. - size_t end = std::min(newCapacity, ChunkSize); - chunk(0).poisonRange(capacity(), end, JS_FRESH_NURSERY_PATTERN, - MemCheckKind::MakeUndefined); + setCapacity(newCapacity); + + toSpace.setCurrentEnd(this); + if (semispaceEnabled_) { + fromSpace.setCurrentEnd(this); } +} - capacity_ = newCapacity; +bool js::Nursery::Space::commitSubChunkRegion(size_t oldCapacity, + size_t newCapacity) { + MOZ_ASSERT(currentChunk_ == 0); + MOZ_ASSERT(oldCapacity < ChunkSize); + MOZ_ASSERT(newCapacity > oldCapacity); - setCurrentEnd(); + size_t newChunkEnd = std::min(newCapacity, ChunkSize); + + // The remainder of the chunk may have been decommitted. + if (!chunks_[0]->markPagesInUseHard(newChunkEnd)) { + // The OS won't give us the memory we need, we can't grow. + return false; + } + + // The capacity has changed and since we were in sub-chunk mode we need to + // update the poison values / asan information for the now-valid region of + // this chunk. + chunks_[0]->poisonRange(oldCapacity, newChunkEnd, JS_FRESH_NURSERY_PATTERN, + MemCheckKind::MakeUndefined); + return true; } -void js::Nursery::freeChunksFrom(const unsigned firstFreeChunk) { - MOZ_ASSERT(firstFreeChunk < chunks_.length()); +void js::Nursery::freeChunksFrom(Space& space, const unsigned firstFreeChunk) { + if (firstFreeChunk >= space.chunks_.length()) { + return; + } // The loop below may need to skip the first chunk, so we may use this so we // can modify it. @@ -2000,61 +2266,112 @@ void js::Nursery::freeChunksFrom(const unsigned firstFreeChunk) { if ((firstChunkToDecommit == 0) && isSubChunkMode()) { // Part of the first chunk may be hard-decommitted, un-decommit it so that // the GC's normal chunk-handling doesn't segfault. - MOZ_ASSERT(currentChunk_ == 0); - if (!chunk(0).markPagesInUseHard(ChunkSize)) { + MOZ_ASSERT(space.currentChunk_ == 0); + if (!space.chunks_[0]->markPagesInUseHard(ChunkSize)) { // Free the chunk if we can't allocate its pages. - UnmapPages(static_cast<void*>(&chunk(0)), ChunkSize); + UnmapPages(space.chunks_[0], ChunkSize); firstChunkToDecommit = 1; } } { AutoLockHelperThreadState lock; - for (size_t i = firstChunkToDecommit; i < chunks_.length(); i++) { - decommitTask->queueChunk(chunks_[i], lock); + for (size_t i = firstChunkToDecommit; i < space.chunks_.length(); i++) { + decommitTask->queueChunk(space.chunks_[i], lock); } } - chunks_.shrinkTo(firstFreeChunk); + space.chunks_.shrinkTo(firstFreeChunk); } void js::Nursery::shrinkAllocableSpace(size_t newCapacity) { -#ifdef JS_GC_ZEAL - if (gc->hasZealMode(ZealMode::GenerationalGC)) { - return; - } -#endif + MOZ_ASSERT(!gc->hasZealMode(ZealMode::GenerationalGC)); + MOZ_ASSERT(newCapacity < capacity_); - // Don't shrink the nursery to zero (use Nursery::disable() instead) - // This can't happen due to the rounding-down performed above because of the - // clamping in maybeResizeNursery(). - MOZ_ASSERT(newCapacity != 0); - // Don't attempt to shrink it to the same size. - if (newCapacity == capacity_) { + if (semispaceEnabled() && usedSpace() >= newCapacity) { + // Can't shrink below what we've already used. return; } - MOZ_ASSERT(newCapacity < capacity_); unsigned newCount = HowMany(newCapacity, ChunkSize); if (newCount < allocatedChunkCount()) { - freeChunksFrom(newCount); + freeChunksFrom(toSpace, newCount); + freeChunksFrom(fromSpace, newCount); } size_t oldCapacity = capacity_; - capacity_ = newCapacity; + setCapacity(newCapacity); - setCurrentEnd(); + toSpace.setCurrentEnd(this); + if (semispaceEnabled_) { + fromSpace.setCurrentEnd(this); + } if (isSubChunkMode()) { - MOZ_ASSERT(currentChunk_ == 0); - size_t end = std::min(oldCapacity, ChunkSize); - chunk(0).poisonRange(newCapacity, end, JS_SWEPT_NURSERY_PATTERN, - MemCheckKind::MakeNoAccess); + toSpace.decommitSubChunkRegion(this, oldCapacity, newCapacity); + if (semispaceEnabled_) { + fromSpace.decommitSubChunkRegion(this, oldCapacity, newCapacity); + } + } +} - AutoLockHelperThreadState lock; - decommitTask->queueRange(capacity_, chunk(0), lock); +void js::Nursery::Space::decommitSubChunkRegion(Nursery* nursery, + size_t oldCapacity, + size_t newCapacity) { + MOZ_ASSERT(currentChunk_ == 0); + MOZ_ASSERT(newCapacity < ChunkSize); + MOZ_ASSERT(newCapacity < oldCapacity); + + size_t oldChunkEnd = std::min(oldCapacity, ChunkSize); + chunks_[0]->poisonRange(newCapacity, oldChunkEnd, JS_SWEPT_NURSERY_PATTERN, + MemCheckKind::MakeNoAccess); + + AutoLockHelperThreadState lock; + nursery->decommitTask->queueRange(newCapacity, chunks_[0], lock); +} + +js::Nursery::Space::Space(gc::ChunkKind kind) : kind(kind) { + MOZ_ASSERT(kind == ChunkKind::NurseryFromSpace || + kind == ChunkKind::NurseryToSpace); +} + +void js::Nursery::Space::setKind(ChunkKind newKind) { +#ifdef DEBUG + MOZ_ASSERT(newKind == ChunkKind::NurseryFromSpace || + newKind == ChunkKind::NurseryToSpace); + checkKind(kind); +#endif + + kind = newKind; + for (NurseryChunk* chunk : chunks_) { + chunk->kind = newKind; + } + +#ifdef DEBUG + checkKind(newKind); +#endif +} + +#ifdef DEBUG +void js::Nursery::Space::checkKind(ChunkKind expected) const { + MOZ_ASSERT(kind == expected); + for (NurseryChunk* chunk : chunks_) { + MOZ_ASSERT(chunk->getKind() == expected); + } +} +#endif + +#ifdef DEBUG +size_t js::Nursery::Space::findChunkIndex(uintptr_t chunkAddr) const { + for (size_t i = 0; i < chunks_.length(); i++) { + if (uintptr_t(chunks_[i]) == chunkAddr) { + return i; + } } + + MOZ_CRASH("Nursery chunk not found"); } +#endif gcstats::Statistics& js::Nursery::stats() const { return gc->stats(); } @@ -2067,18 +2384,55 @@ bool js::Nursery::isSubChunkMode() const { return capacity() <= NurseryChunkUsableSize; } +void js::Nursery::clearMapAndSetNurseryRanges() { + // Clears the lists of nursery ranges used by map and set iterators. These + // lists are cleared at the start of minor GC and rebuilt when iterators are + // promoted during minor GC. + for (auto* map : mapsWithNurseryMemory_) { + map->clearNurseryRangesBeforeMinorGC(); + } + for (auto* set : setsWithNurseryMemory_) { + set->clearNurseryRangesBeforeMinorGC(); + } +} + void js::Nursery::sweepMapAndSetObjects() { + // This processes all Map and Set objects that are known to have associated + // nursery memory (either they are nursery allocated themselves or they have + // iterator objects that are nursery allocated). + // + // These objects may die and be finalized or if not their internal state and + // memory tracking are updated. + // + // Finally the lists themselves are rebuilt so as to remove objects that are + // no longer associated with nursery memory (either because they died or + // because the nursery object was promoted to the tenured heap). + auto* gcx = runtime()->gcContext(); - for (auto* mapobj : mapsWithNurseryMemory_) { - MapObject::sweepAfterMinorGC(gcx, mapobj); + AutoEnterOOMUnsafeRegion oomUnsafe; + + MapObjectVector maps; + std::swap(mapsWithNurseryMemory_, maps); + for (auto* mapobj : maps) { + mapobj = MapObject::sweepAfterMinorGC(gcx, mapobj); + if (mapobj) { + if (!mapsWithNurseryMemory_.append(mapobj)) { + oomUnsafe.crash("sweepAfterMinorGC"); + } + } } - mapsWithNurseryMemory_.clearAndFree(); - for (auto* setobj : setsWithNurseryMemory_) { - SetObject::sweepAfterMinorGC(gcx, setobj); + SetObjectVector sets; + std::swap(setsWithNurseryMemory_, sets); + for (auto* setobj : sets) { + setobj = SetObject::sweepAfterMinorGC(gcx, setobj); + if (setobj) { + if (!setsWithNurseryMemory_.append(setobj)) { + oomUnsafe.crash("sweepAfterMinorGC"); + } + } } - setsWithNurseryMemory_.clearAndFree(); } void js::Nursery::joinDecommitTask() { decommitTask->join(); } diff --git a/js/src/gc/Nursery.h b/js/src/gc/Nursery.h index 0d7b607ff8..647dbbb24f 100644 --- a/js/src/gc/Nursery.h +++ b/js/src/gc/Nursery.h @@ -13,6 +13,7 @@ #include <tuple> +#include "ds/LifoAlloc.h" #include "gc/GCEnum.h" #include "gc/GCProbes.h" #include "gc/Heap.h" @@ -21,6 +22,8 @@ #include "js/AllocPolicy.h" #include "js/Class.h" #include "js/GCAPI.h" +#include "js/GCVector.h" +#include "js/HeapAPI.h" #include "js/TypeDecls.h" #include "js/UniquePtr.h" #include "js/Utility.h" @@ -69,7 +72,20 @@ namespace gc { class AutoGCSession; struct Cell; class GCSchedulingTunables; +class StoreBuffer; class TenuringTracer; + +// A set of cells that need to be swept at the end of a minor GC, +// represented as a linked list of ArenaCellSet structs extracted from a +// WholeCellBuffer. +struct CellSweepSet { + UniquePtr<LifoAlloc> storage_; + ArenaCellSet* head_ = nullptr; + + // Fixup the tenured dependent strings stored in the ArenaCellSet list. + void sweep(); +}; + } // namespace gc class Nursery { @@ -79,18 +95,6 @@ class Nursery { [[nodiscard]] bool init(AutoLockGCBgAlloc& lock); - // Number of allocated (ready to use) chunks. - unsigned allocatedChunkCount() const { return chunks_.length(); } - - // Total number of chunks and the capacity of the nursery. Chunks will be - // lazilly allocated and added to the chunks array up to this limit, after - // that the nursery must be collected, this limit may be raised during - // collection. - unsigned maxChunkCount() const { - MOZ_ASSERT(capacity()); - return HowMany(capacity(), gc::ChunkSize); - } - void enable(); void disable(); bool isEnabled() const { return capacity() != 0; } @@ -103,20 +107,16 @@ class Nursery { void disableBigInts(); bool canAllocateBigInts() const { return canAllocateBigInts_; } + void setSemispaceEnabled(bool enabled); + bool semispaceEnabled() const { return semispaceEnabled_; } + // Return true if no allocations have been made since the last collection. bool isEmpty() const; // Check whether an arbitrary pointer is within the nursery. This is // slower than IsInsideNursery(Cell*), but works on all types of pointers. - MOZ_ALWAYS_INLINE bool isInside(gc::Cell* cellp) const = delete; - MOZ_ALWAYS_INLINE bool isInside(const void* p) const { - for (auto* chunk : chunks_) { - if (uintptr_t(p) - uintptr_t(chunk) < gc::ChunkSize) { - return true; - } - } - return false; - } + bool isInside(gc::Cell* cellp) const = delete; + inline bool isInside(const void* p) const; template <typename T> inline bool isInside(const SharedMem<T>& p) const; @@ -223,11 +223,12 @@ class Nursery { // Mark a malloced buffer as no longer needing to be freed. void removeMallocedBuffer(void* buffer, size_t nbytes) { - MOZ_ASSERT(mallocedBuffers.has(buffer)); + MOZ_ASSERT(!JS::RuntimeHeapIsMinorCollecting()); + MOZ_ASSERT(toSpace.mallocedBuffers.has(buffer)); MOZ_ASSERT(nbytes > 0); - MOZ_ASSERT(mallocedBufferBytes >= nbytes); - mallocedBuffers.remove(buffer); - mallocedBufferBytes -= nbytes; + MOZ_ASSERT(toSpace.mallocedBufferBytes >= nbytes); + toSpace.mallocedBuffers.remove(buffer); + toSpace.mallocedBufferBytes -= nbytes; } // Mark a malloced buffer as no longer needing to be freed during minor @@ -235,8 +236,8 @@ class Nursery { // buffers will soon be freed. void removeMallocedBufferDuringMinorGC(void* buffer) { MOZ_ASSERT(JS::RuntimeHeapIsMinorCollecting()); - MOZ_ASSERT(mallocedBuffers.has(buffer)); - mallocedBuffers.remove(buffer); + MOZ_ASSERT(fromSpace.mallocedBuffers.has(buffer)); + fromSpace.mallocedBuffers.remove(buffer); } [[nodiscard]] bool addedUniqueIdToCell(gc::Cell* cell) { @@ -277,26 +278,8 @@ class Nursery { inline void unregisterTrailer(void* block); size_t sizeOfTrailerBlockSets(mozilla::MallocSizeOf mallocSizeOf) const; - size_t capacity() const { return capacity_; } - size_t committed() const { - return std::min(capacity_, allocatedChunkCount() * gc::ChunkSize); - } - - // Used and free space both include chunk headers for that part of the - // nursery. - // - // usedSpace() + freeSpace() == capacity() - // - MOZ_ALWAYS_INLINE size_t usedSpace() const { - return capacity() - freeSpace(); - } - MOZ_ALWAYS_INLINE size_t freeSpace() const { - MOZ_ASSERT(isEnabled()); - MOZ_ASSERT(currentEnd_ - position_ <= NurseryChunkUsableSize); - MOZ_ASSERT(currentChunk_ < maxChunkCount()); - return (currentEnd_ - position_) + - (maxChunkCount() - currentChunk_ - 1) * gc::ChunkSize; - } + size_t totalCapacity() const; + size_t totalCommitted() const; #ifdef JS_GC_ZEAL void enterZealMode(); @@ -312,9 +295,10 @@ class Nursery { // Print total profile times on shutdown. void printTotalProfileTimes(); - void* addressOfPosition() const { return (void**)&position_; } + void* addressOfPosition() const { return (void**)&toSpace.position_; } static constexpr int32_t offsetOfCurrentEndFromPosition() { - return offsetof(Nursery, currentEnd_) - offsetof(Nursery, position_); + return offsetof(Nursery, toSpace.currentEnd_) - + offsetof(Nursery, toSpace.position_); } void* addressOfNurseryAllocatedSites() { @@ -343,10 +327,6 @@ class Nursery { return setsWithNurseryMemory_.append(obj); } - // The amount of space in the mapped nursery available to allocations. - static const size_t NurseryChunkUsableSize = - gc::ChunkSize - sizeof(gc::ChunkBase); - void joinDecommitTask(); mozilla::TimeStamp collectionStartTime() { @@ -362,6 +342,16 @@ class Nursery { void setAllocFlagsForZone(JS::Zone* zone); + bool shouldTenureEverything(JS::GCReason reason); + + inline bool inCollectedRegion(gc::Cell* cell) const; + inline bool inCollectedRegion(void* ptr) const; + + void trackMallocedBufferOnPromotion(void* buffer, gc::Cell* owner, + size_t nbytes, MemoryUse use); + void trackTrailerOnPromotion(void* buffer, gc::Cell* owner, size_t nbytes, + size_t overhead, MemoryUse use); + // Round a size in bytes to the nearest valid nursery size. static size_t roundSize(size_t size); @@ -374,6 +364,8 @@ class Nursery { mozilla::TimeStamp lastCollectionEndTime() const; private: + struct Space; + enum class ProfileKey { #define DEFINE_TIME_KEY(name, text) name, FOR_EACH_NURSERY_PROFILE_TIME(DEFINE_TIME_KEY) @@ -387,6 +379,36 @@ class Nursery { mozilla::EnumeratedArray<ProfileKey, mozilla::TimeDuration, size_t(ProfileKey::KeyCount)>; + size_t capacity() const { return capacity_; } + + // Total number of chunks and the capacity of the current nursery + // space. Chunks will be lazily allocated and added to the chunks array up to + // this limit. After that the nursery must be collected. This limit may be + // changed at the end of collection by maybeResizeNursery. + uint32_t maxChunkCount() const { + MOZ_ASSERT(toSpace.maxChunkCount_); + return toSpace.maxChunkCount_; + } + + // Number of allocated (ready to use) chunks. + unsigned allocatedChunkCount() const { return toSpace.chunks_.length(); } + + uint32_t currentChunk() const { return toSpace.currentChunk_; } + uint32_t startChunk() const { return toSpace.startChunk_; } + uintptr_t startPosition() const { return toSpace.startPosition_; } + + // Used and free space both include chunk headers for that part of the + // nursery. + MOZ_ALWAYS_INLINE size_t usedSpace() const { + return capacity() - freeSpace(); + } + MOZ_ALWAYS_INLINE size_t freeSpace() const { + MOZ_ASSERT(isEnabled()); + MOZ_ASSERT(currentChunk() < maxChunkCount()); + return (currentEnd() - position()) + + (maxChunkCount() - currentChunk() - 1) * gc::ChunkSize; + } + // Calculate the promotion rate of the most recent minor GC. // The valid_for_tenuring parameter is used to return whether this // promotion rate is accurate enough (the nursery was full enough) to be @@ -395,31 +417,27 @@ class Nursery { // Must only be called if the previousGC data is initialised. double calcPromotionRate(bool* validForTenuring) const; - void freeTrailerBlocks(); + void freeTrailerBlocks(JS::GCOptions options, JS::GCReason reason); - NurseryChunk& chunk(unsigned index) const { return *chunks_[index]; } + NurseryChunk& chunk(unsigned index) const { return *toSpace.chunks_[index]; } // Set the allocation position to the start of a chunk. This sets // currentChunk_, position_ and currentEnd_ values as appropriate. void moveToStartOfChunk(unsigned chunkno); bool initFirstChunk(AutoLockGCBgAlloc& lock); + void setCapacity(size_t newCapacity); - // extent is advisory, it will be ignored in sub-chunk and generational zeal - // modes. It will be clamped to Min(NurseryChunkUsableSize, capacity_). - void poisonAndInitCurrentChunk(size_t extent = gc::ChunkSize); + void poisonAndInitCurrentChunk(); void setCurrentEnd(); void setStartToCurrentPosition(); - // Allocate the next chunk, or the first chunk for initialization. - // Callers will probably want to call moveToStartOfChunk(0) next. - [[nodiscard]] bool allocateNextChunk(unsigned chunkno, - AutoLockGCBgAlloc& lock); + // Allocate another chunk. + [[nodiscard]] bool allocateNextChunk(AutoLockGCBgAlloc& lock); - uintptr_t currentEnd() const { return currentEnd_; } - - uintptr_t position() const { return position_; } + uintptr_t position() const { return toSpace.position_; } + uintptr_t currentEnd() const { return toSpace.currentEnd_; } MOZ_ALWAYS_INLINE bool isSubChunkMode() const; @@ -451,6 +469,7 @@ class Nursery { }; CollectionResult doCollection(gc::AutoGCSession& session, JS::GCOptions options, JS::GCReason reason); + void swapSpaces(); void traceRoots(gc::AutoGCSession& session, gc::TenuringTracer& mover); size_t doPretenuring(JSRuntime* rt, JS::GCReason reason, @@ -469,22 +488,30 @@ class Nursery { uint32_t capacity); #ifdef DEBUG - bool checkForwardingPointerLocation(void* ptr, bool expectedInside); + bool checkForwardingPointerInsideNursery(void* ptr); #endif // Updates pointers to nursery objects that have been tenured and discards // pointers to objects that have been freed. void sweep(); - // Reset the current chunk and position after a minor collection. Also poison + // In a minor GC, resets the start and end positions, the current chunk and + // current position. + void setNewExtentAndPosition(); + // the nursery on debug & nightly builds. void clear(); + void clearMapAndSetNurseryRanges(); void sweepMapAndSetObjects(); // Allocate a buffer for a given zone, using the nursery if possible. void* allocateBuffer(JS::Zone* zone, size_t nbytes); + // Get per-space size limits. + size_t maxSpaceSize() const; + size_t minSpaceSize() const; + // Change the allocable space provided by the nursery. void maybeResizeNursery(JS::GCOptions options, JS::GCReason reason); size_t targetSize(JS::GCOptions options, JS::GCReason reason); @@ -495,7 +522,9 @@ class Nursery { // Free the chunks starting at firstFreeChunk until the end of the chunks // vector. Shrinks the vector but does not update maxChunkCount(). - void freeChunksFrom(unsigned firstFreeChunk); + void freeChunksFrom(Space& space, unsigned firstFreeChunk); + + inline bool shouldTenure(gc::Cell* cell); void sendTelemetry(JS::GCReason reason, mozilla::TimeDuration totalTime, bool wasEmpty, double promotionRate, @@ -514,35 +543,87 @@ class Nursery { mozilla::TimeStamp collectionStartTime() const; private: - // Fields used during allocation fast path are grouped first: + using BufferRelocationOverlay = void*; + using BufferSet = HashSet<void*, PointerHasher<void*>, SystemAllocPolicy>; - // Pointer to the first unallocated byte in the nursery. - uintptr_t position_; + struct Space { + // Fields used during allocation fast path go first: - // Pointer to the last byte of space in the current chunk. - uintptr_t currentEnd_; + // Pointer to the first unallocated byte in the nursery. + uintptr_t position_ = 0; - // Other fields not necessarily used during allocation follow: + // Pointer to the last byte of space in the current chunk. + uintptr_t currentEnd_ = 0; - gc::GCRuntime* const gc; + // Vector of allocated chunks to allocate from. + Vector<NurseryChunk*, 0, SystemAllocPolicy> chunks_; + + // The index of the chunk that is currently being allocated from. + uint32_t currentChunk_ = 0; + + // The maximum number of chunks to allocate based on capacity_. + uint32_t maxChunkCount_ = 0; + + // These fields refer to the beginning of the nursery. They're normally 0 + // and chunk(0).start() respectively. Except when a generational GC zeal + // mode is active, then they may be arbitrary (see Nursery::clear()). + uint32_t startChunk_ = 0; + uintptr_t startPosition_ = 0; + + // The set of malloced-allocated buffers owned by nursery objects. Any + // buffers that do not belong to a promoted thing at the end of a minor GC + // must be freed. + BufferSet mallocedBuffers; + size_t mallocedBufferBytes = 0; + + // Wasm "trailer" (C++-heap-allocated) blocks. See comments above on + // ::registerTrailer and ::unregisterTrailer. + Vector<PointerAndUint7, 0, SystemAllocPolicy> trailersAdded_; + Vector<void*, 0, SystemAllocPolicy> trailersRemoved_; + size_t trailersRemovedUsed_ = 0; + size_t trailerBytes_ = 0; + + gc::ChunkKind kind; + + explicit Space(gc::ChunkKind kind); + + inline bool isEmpty() const; + inline bool isInside(const void* p) const; - // Vector of allocated chunks to allocate from. - Vector<NurseryChunk*, 0, SystemAllocPolicy> chunks_; + // Return the logical offset within the nursery of an address in a nursery + // chunk (chunks are discontiguous in memory). + inline size_t offsetFromAddress(uintptr_t addr) const; + inline size_t offsetFromExclusiveAddress(uintptr_t addr) const; - // The index of the chunk that is currently being allocated from. - uint32_t currentChunk_; + void setKind(gc::ChunkKind newKind); - // These fields refer to the beginning of the nursery. They're normally 0 - // and chunk(0).start() respectively. Except when a generational GC zeal - // mode is active, then they may be arbitrary (see Nursery::clear()). - uint32_t startChunk_; - uintptr_t startPosition_; + void clear(Nursery* nursery); + void moveToStartOfChunk(Nursery* nursery, unsigned chunkno); + void setCurrentEnd(Nursery* nursery); + void setStartToCurrentPosition(); + bool commitSubChunkRegion(size_t oldCapacity, size_t newCapacity); + void decommitSubChunkRegion(Nursery* nursery, size_t oldCapacity, + size_t newCapacity); + void freeTrailerBlocks(gc::MallocedBlockCache& mallocedBlockCache); + +#ifdef DEBUG + void checkKind(gc::ChunkKind expected) const; + size_t findChunkIndex(uintptr_t chunkAddr) const; +#endif + }; + + Space toSpace; + Space fromSpace; + + gc::GCRuntime* const gc; // The current nursery capacity measured in bytes. It may grow up to this // value without a collection, allocating chunks on demand. This limit may be // changed by maybeResizeNursery() each collection. It includes chunk headers. size_t capacity_; + uintptr_t tenureThreshold_ = 0; + gc::PretenuringNursery pretenuringNursery; mozilla::TimeDuration timeInChunkAlloc_; @@ -553,6 +634,9 @@ class Nursery { mozilla::TimeDuration profileThreshold_; + // Whether to use semispace collection. + bool semispaceEnabled_; + // Whether we will nursery-allocate strings. bool canAllocateStrings_; @@ -595,21 +679,6 @@ class Nursery { bool hasRecentGrowthData; double smoothedTargetSize; - // The set of externally malloced buffers potentially kept live by objects - // stored in the nursery. Any external buffers that do not belong to a - // tenured thing at the end of a minor GC must be freed. - using BufferRelocationOverlay = void*; - using BufferSet = HashSet<void*, PointerHasher<void*>, SystemAllocPolicy>; - BufferSet mallocedBuffers; - size_t mallocedBufferBytes = 0; - - // Wasm "trailer" (C++-heap-allocated) blocks. See comments above on - // ::registerTrailer and ::unregisterTrailer. - Vector<PointerAndUint7, 0, SystemAllocPolicy> trailersAdded_; - Vector<void*, 0, SystemAllocPolicy> trailersRemoved_; - size_t trailersRemovedUsed_ = 0; - size_t trailerBytes_ = 0; - // During a collection most hoisted slot and element buffers indicate their // new location with a forwarding pointer at the base. This does not work // for buffers whose length is less than pointer width, or when different @@ -619,6 +688,8 @@ class Nursery { HashMap<void*, void*, PointerHasher<void*>, SystemAllocPolicy>; ForwardedBufferMap forwardedBuffers; + gc::CellSweepSet cellsToSweep; + // When we assign a unique id to cell in the nursery, that almost always // means that the cell will be in a hash table, and thus, held live, // automatically moving the uid from the nursery to its new home in @@ -629,13 +700,15 @@ class Nursery { // Note: we store the pointers as Cell* here, resulting in an ugly cast in // sweep. This is because this structure is used to help implement // stable object hashing and we have to break the cycle somehow. - using CellsWithUniqueIdVector = Vector<gc::Cell*, 8, SystemAllocPolicy>; + using CellsWithUniqueIdVector = JS::GCVector<gc::Cell*, 8, SystemAllocPolicy>; CellsWithUniqueIdVector cellsWithUid_; // Lists of map and set objects allocated in the nursery or with iterators // allocated there. Such objects need to be swept after minor GC. - Vector<MapObject*, 0, SystemAllocPolicy> mapsWithNurseryMemory_; - Vector<SetObject*, 0, SystemAllocPolicy> setsWithNurseryMemory_; + using MapObjectVector = Vector<MapObject*, 0, SystemAllocPolicy>; + MapObjectVector mapsWithNurseryMemory_; + using SetObjectVector = Vector<SetObject*, 0, SystemAllocPolicy>; + SetObjectVector setsWithNurseryMemory_; UniquePtr<NurseryDecommitTask> decommitTask; @@ -648,11 +721,30 @@ class Nursery { // no correctness impact, only a performance impact. gc::MallocedBlockCache mallocedBlockCache_; + // Whether the previous collection tenured everything. This may be false if + // semispace is in use. + bool tenuredEverything; + friend class gc::GCRuntime; friend class gc::TenuringTracer; friend struct NurseryChunk; }; +MOZ_ALWAYS_INLINE bool Nursery::isInside(const void* p) const { + // TODO: Split this into separate methods. + // TODO: Do we ever need to check both? + return toSpace.isInside(p) || fromSpace.isInside(p); +} + +MOZ_ALWAYS_INLINE bool Nursery::Space::isInside(const void* p) const { + for (auto* chunk : chunks_) { + if (uintptr_t(p) - uintptr_t(chunk) < gc::ChunkSize) { + return true; + } + } + return false; +} + } // namespace js #endif // gc_Nursery_h diff --git a/js/src/gc/NurseryAwareHashMap.h b/js/src/gc/NurseryAwareHashMap.h index 35c2bebcea..4df50c1627 100644 --- a/js/src/gc/NurseryAwareHashMap.h +++ b/js/src/gc/NurseryAwareHashMap.h @@ -11,7 +11,8 @@ #include "gc/Tracer.h" #include "js/GCHashTable.h" #include "js/GCPolicyAPI.h" -#include "js/HashTable.h" +#include "js/GCVector.h" +#include "js/Utility.h" namespace js { @@ -81,7 +82,8 @@ class NurseryAwareHashMap { // Keep a list of all keys for which key->isTenured() is false. This lets us // avoid a full traversal of the map on each minor GC, keeping the minor GC // times proportional to the nursery heap size. - Vector<Key, 0, AllocPolicy> nurseryEntries; + using KeyVector = GCVector<Key, 0, AllocPolicy>; + KeyVector nurseryEntries; public: using Lookup = typename MapType::Lookup; @@ -127,16 +129,16 @@ class NurseryAwareHashMap { } void sweepAfterMinorGC(JSTracer* trc) { - for (auto& key : nurseryEntries) { + nurseryEntries.mutableEraseIf([this, trc](Key& key) { auto p = map.lookup(key); if (!p) { - continue; + return true; } // Drop the entry if the value is not marked. if (!JS::GCPolicy<MapValue>::traceWeak(trc, &p->value())) { map.remove(p); - continue; + return true; } // Update and relocate the key, if the value is still needed. @@ -147,33 +149,65 @@ class NurseryAwareHashMap { // wrappee, as they are just copies. The wrapper map entry is merely used // as a cache to avoid re-copying the string, and currently that entire // cache is flushed on major GC. - MapKey copy(key); - if (!JS::GCPolicy<MapKey>::traceWeak(trc, ©)) { + // + // Since |key| is a reference, this updates the content of the + // nurseryEntries vector. + Key prior = key; + if (!TraceManuallyBarrieredWeakEdge(trc, &key, + "NurseryAwareHashMap key")) { map.remove(p); - continue; + return true; } - if (AllowDuplicates) { + bool valueIsTenured = p->value().unbarrieredGet()->isTenured(); + + if constexpr (AllowDuplicates) { // Drop duplicated keys. // // A key can be forwarded to another place. In this case, rekey the // item. If two or more different keys are forwarded to the same new // key, simply drop the later ones. - if (key == copy) { + if (key == prior) { // No rekey needed. - } else if (map.has(copy)) { + } else if (map.has(key)) { // Key was forwarded to the same place that another key was already // forwarded to. map.remove(p); + return true; } else { - map.rekeyAs(key, copy, copy); + map.rekeyAs(prior, key, key); } } else { - MOZ_ASSERT(key == copy || !map.has(copy)); - map.rekeyIfMoved(key, copy); + MOZ_ASSERT(key == prior || !map.has(key)); + map.rekeyIfMoved(prior, key); + } + + return key->isTenured() && valueIsTenured; + }); + + checkNurseryEntries(); + } + + void checkNurseryEntries() const { +#ifdef DEBUG + AutoEnterOOMUnsafeRegion oomUnsafe; + HashSet<Key, DefaultHasher<Key>, SystemAllocPolicy> set; + for (const auto& key : nurseryEntries) { + if (!set.put(key)) { + oomUnsafe.crash("NurseryAwareHashMap::checkNurseryEntries"); } } - nurseryEntries.clear(); + + for (auto i = map.iter(); !i.done(); i.next()) { + Key key = i.get().key().get(); + MOZ_ASSERT(gc::IsCellPointerValid(key)); + MOZ_ASSERT_IF(IsInsideNursery(key), set.has(key)); + + Value value = i.get().value().unbarrieredGet(); + MOZ_ASSERT(gc::IsCellPointerValid(value)); + MOZ_ASSERT_IF(IsInsideNursery(value), set.has(key)); + } +#endif } void traceWeak(JSTracer* trc) { map.traceWeak(trc); } diff --git a/js/src/gc/Pretenuring.cpp b/js/src/gc/Pretenuring.cpp index a191dbba90..b44bbc901f 100644 --- a/js/src/gc/Pretenuring.cpp +++ b/js/src/gc/Pretenuring.cpp @@ -49,12 +49,12 @@ static constexpr double LowYoungSurvivalThreshold = 0.05; // that must occur before recovery is attempted. static constexpr size_t LowYoungSurvivalCountBeforeRecovery = 2; -// The proportion of the nursery that must be tenured above which a minor +// The proportion of the nursery that must be promoted above which a minor // collection may be determined to have a high nursery survival rate. static constexpr double HighNurserySurvivalPromotionThreshold = 0.6; // The number of nursery allocations made by optimized JIT code that must be -// tenured above which a minor collection may be determined to have a high +// promoted above which a minor collection may be determined to have a high // nursery survival rate. static constexpr size_t HighNurserySurvivalOptimizedAllocThreshold = 10000; @@ -95,7 +95,7 @@ size_t PretenuringNursery::doPretenuring(GCRuntime* gc, JS::GCReason reason, for (ZonesIter zone(gc, SkipAtoms); !zone.done(); zone.next()) { bool highNurserySurvivalRate = promotionRate > HighNurserySurvivalPromotionThreshold && - zone->optimizedAllocSite()->nurseryTenuredCount >= + zone->optimizedAllocSite()->nurseryPromotedCount >= HighNurserySurvivalOptimizedAllocThreshold; zone->pretenuring.noteHighNurserySurvivalRate(highNurserySurvivalRate); if (highNurserySurvivalRate) { @@ -136,6 +136,7 @@ size_t PretenuringNursery::doPretenuring(GCRuntime* gc, JS::GCReason reason, // Catch-all sites don't end up on the list if they are only used from // optimized JIT code, so process them here. + for (ZonesIter zone(gc, SkipAtoms); !zone.done(); zone.next()) { for (auto& site : zone->pretenuring.unknownAllocSites) { updateTotalAllocCounts(&site); @@ -150,6 +151,11 @@ size_t PretenuringNursery::doPretenuring(GCRuntime* gc, JS::GCReason reason, updateTotalAllocCounts(zone->optimizedAllocSite()); zone->optimizedAllocSite()->processCatchAllSite(reportInfo, reportThreshold); + + // The data from the promoted alloc sites is never used so clear them here. + for (AllocSite& site : zone->pretenuring.promotedAllocSites) { + site.resetNurseryAllocations(); + } } if (reportInfo) { @@ -171,7 +177,7 @@ AllocSite::SiteResult AllocSite::processSite(GCRuntime* gc, bool reportInfo, size_t reportThreshold) { MOZ_ASSERT(kind() != Kind::Optimized); - MOZ_ASSERT(nurseryAllocCount >= nurseryTenuredCount); + MOZ_ASSERT(nurseryAllocCount >= nurseryPromotedCount); SiteResult result = NoChange; @@ -180,7 +186,7 @@ AllocSite::SiteResult AllocSite::processSite(GCRuntime* gc, bool wasInvalidated = false; if (nurseryAllocCount > attentionThreshold) { - promotionRate = double(nurseryTenuredCount) / double(nurseryAllocCount); + promotionRate = double(nurseryPromotedCount) / double(nurseryAllocCount); hasPromotionRate = true; AllocSite::State prevState = state(); @@ -402,7 +408,7 @@ bool PretenuringZone::shouldResetPretenuredAllocSites() { /* static */ void AllocSite::printInfoHeader(JS::GCReason reason, double promotionRate) { fprintf(stderr, " %-16s %-16s %-20s %-8s %-8s %-6s %-10s\n", "site", "zone", - "script/kind", "nallocs", "tenures", "prate", "state"); + "script/kind", "nallocs", "promotes", "prate", "state"); } /* static */ @@ -455,8 +461,8 @@ void AllocSite::printInfo(bool hasPromotionRate, double promotionRate, } fprintf(stderr, " %8s", buffer); - // Nursery tenure count. - fprintf(stderr, " %8" PRIu32, nurseryTenuredCount); + // Nursery promotion count. + fprintf(stderr, " %8" PRIu32, nurseryPromotedCount); // Promotion rate, if there were enough allocations. buffer[0] = '\0'; diff --git a/js/src/gc/Pretenuring.h b/js/src/gc/Pretenuring.h index 5aeb5c50d7..93677532e4 100644 --- a/js/src/gc/Pretenuring.h +++ b/js/src/gc/Pretenuring.h @@ -83,7 +83,7 @@ class AllocSite { uint32_t nurseryAllocCount = 0; // Number of nursery allocations that survived. Used during collection. - uint32_t nurseryTenuredCount : 24; + uint32_t nurseryPromotedCount : 24; // Number of times the script has been invalidated. uint32_t invalidationCount : 4; @@ -103,12 +103,12 @@ class AllocSite { uintptr_t rawScript() const { return scriptAndState & ~STATE_MASK; } public: - AllocSite() : nurseryTenuredCount(0), invalidationCount(0), traceKind_(0) {} + AllocSite() : nurseryPromotedCount(0), invalidationCount(0), traceKind_(0) {} // Create a dummy site to use for unknown allocations. explicit AllocSite(JS::Zone* zone, JS::TraceKind kind) : zone_(zone), - nurseryTenuredCount(0), + nurseryPromotedCount(0), invalidationCount(0), traceKind_(uint32_t(kind)) { MOZ_ASSERT(traceKind_ < NurseryTraceKinds); @@ -126,7 +126,7 @@ class AllocSite { void initUnknownSite(JS::Zone* zone, JS::TraceKind kind) { MOZ_ASSERT(!zone_ && scriptAndState == uintptr_t(State::Unknown)); zone_ = zone; - nurseryTenuredCount = 0; + nurseryPromotedCount = 0; invalidationCount = 0; traceKind_ = uint32_t(kind); MOZ_ASSERT(traceKind_ < NurseryTraceKinds); @@ -137,7 +137,7 @@ class AllocSite { MOZ_ASSERT(!zone_ && scriptAndState == uintptr_t(State::Unknown)); zone_ = zone; setScript(WasmScript); - nurseryTenuredCount = 0; + nurseryPromotedCount = 0; invalidationCount = 0; traceKind_ = uint32_t(JS::TraceKind::Object); } @@ -179,24 +179,24 @@ class AllocSite { } bool hasNurseryAllocations() const { - return nurseryAllocCount != 0 || nurseryTenuredCount != 0; + return nurseryAllocCount != 0 || nurseryPromotedCount != 0; } void resetNurseryAllocations() { nurseryAllocCount = 0; - nurseryTenuredCount = 0; + nurseryPromotedCount = 0; } uint32_t incAllocCount() { return ++nurseryAllocCount; } uint32_t* nurseryAllocCountAddress() { return &nurseryAllocCount; } - void incTenuredCount() { + void incPromotedCount() { // The nursery is not large enough for this to overflow. - nurseryTenuredCount++; - MOZ_ASSERT(nurseryTenuredCount != 0); + nurseryPromotedCount++; + MOZ_ASSERT(nurseryPromotedCount != 0); } size_t allocCount() const { - return std::max(nurseryAllocCount, nurseryTenuredCount); + return std::max(nurseryAllocCount, nurseryPromotedCount); } // Called for every active alloc site after minor GC. @@ -259,6 +259,10 @@ class PretenuringZone { // not recorded by optimized JIT code. AllocSite optimizedAllocSite; + // Allocation sites used for nursery cells promoted to the next nursery + // generation that didn't come from optimized alloc sites. + AllocSite promotedAllocSites[NurseryTraceKinds]; + // Count of tenured cell allocations made between each major collection and // how many survived. uint32_t allocCountInNewlyCreatedArenas = 0; @@ -282,6 +286,7 @@ class PretenuringZone { : optimizedAllocSite(zone, JS::TraceKind::Object) { for (uint32_t i = 0; i < NurseryTraceKinds; i++) { unknownAllocSites[i].initUnknownSite(zone, JS::TraceKind(i)); + promotedAllocSites[i].initUnknownSite(zone, JS::TraceKind(i)); } } @@ -291,6 +296,12 @@ class PretenuringZone { return unknownAllocSites[i]; } + AllocSite& promotedAllocSite(JS::TraceKind kind) { + size_t i = size_t(kind); + MOZ_ASSERT(i < NurseryTraceKinds); + return promotedAllocSites[i]; + } + void clearCellCountsInNewlyCreatedArenas() { allocCountInNewlyCreatedArenas = 0; survivorCountInNewlyCreatedArenas = 0; diff --git a/js/src/gc/PublicIterators.h b/js/src/gc/PublicIterators.h index d1072cfe98..7ef6271f63 100644 --- a/js/src/gc/PublicIterators.h +++ b/js/src/gc/PublicIterators.h @@ -35,7 +35,7 @@ class ZonesIter { public: ZonesIter(gc::GCRuntime* gc, ZoneSelector selector) : iterMarker(gc), it(gc->zones().begin()), end(gc->zones().end()) { - if (selector == SkipAtoms) { + if (selector == SkipAtoms && !done()) { MOZ_ASSERT(get()->isAtomsZone()); next(); } diff --git a/js/src/gc/Scheduling.h b/js/src/gc/Scheduling.h index cbaeb1f353..c5ed56dd5f 100644 --- a/js/src/gc/Scheduling.h +++ b/js/src/gc/Scheduling.h @@ -536,6 +536,9 @@ static const bool ParallelMarkingEnabled = false; /* JSGC_INCREMENTAL_WEAKMAP_ENABLED */ static const bool IncrementalWeakMapMarkingEnabled = true; +/* JSGC_SEMISPACE_NURSERY_ENABLED */ +static const bool SemispaceNurseryEnabled = false; + /* JSGC_HELPER_THREAD_RATIO */ static const double HelperThreadRatio = 0.5; diff --git a/js/src/gc/StableCellHasher-inl.h b/js/src/gc/StableCellHasher-inl.h index af0caaad89..34a8827cfb 100644 --- a/js/src/gc/StableCellHasher-inl.h +++ b/js/src/gc/StableCellHasher-inl.h @@ -132,7 +132,6 @@ inline bool HasUniqueId(Cell* cell) { inline void TransferUniqueId(Cell* tgt, Cell* src) { MOZ_ASSERT(src != tgt); - MOZ_ASSERT(!IsInsideNursery(tgt)); MOZ_ASSERT(CurrentThreadCanAccessRuntime(tgt->runtimeFromAnyThread())); MOZ_ASSERT(src->zone() == tgt->zone()); diff --git a/js/src/gc/StoreBuffer-inl.h b/js/src/gc/StoreBuffer-inl.h index 1c97654761..343b953e3a 100644 --- a/js/src/gc/StoreBuffer-inl.h +++ b/js/src/gc/StoreBuffer-inl.h @@ -49,9 +49,10 @@ inline void ArenaCellSet::check() const { MOZ_ASSERT(isEmpty() == !arena); if (!isEmpty()) { MOZ_ASSERT(IsCellPointerValid(arena)); - MOZ_ASSERT(arena->bufferedCells() == this); JSRuntime* runtime = arena->zone->runtimeFromMainThread(); - MOZ_ASSERT(runtime->gc.minorGCCount() == minorGCNumberAtCreation); + uint64_t minorGCCount = runtime->gc.minorGCCount(); + MOZ_ASSERT(minorGCCount == minorGCNumberAtCreation || + minorGCCount == minorGCNumberAtCreation + 1); } #endif } diff --git a/js/src/gc/StoreBuffer.cpp b/js/src/gc/StoreBuffer.cpp index d637eb62b1..1f8023bc62 100644 --- a/js/src/gc/StoreBuffer.cpp +++ b/js/src/gc/StoreBuffer.cpp @@ -20,7 +20,8 @@ void StoreBuffer::checkAccess() const { // The GC runs tasks that may access the storebuffer in parallel and so must // take a lock. The mutator may only access the storebuffer from the main // thread. - if (runtime_->heapState() != JS::HeapState::Idle) { + if (runtime_->heapState() != JS::HeapState::Idle && + runtime_->heapState() != JS::HeapState::MinorCollecting) { MOZ_ASSERT(!CurrentThreadIsGCMarking()); runtime_->gc.assertCurrentThreadHasLockedStoreBuffer(); } else { @@ -30,8 +31,7 @@ void StoreBuffer::checkAccess() const { #endif bool StoreBuffer::WholeCellBuffer::init() { - MOZ_ASSERT(!stringHead_); - MOZ_ASSERT(!nonStringHead_); + MOZ_ASSERT(!head_); if (!storage_) { storage_ = MakeUnique<LifoAlloc>(LifoAllocBlockSize); // This prevents LifoAlloc::Enum from crashing with a release @@ -75,8 +75,7 @@ StoreBuffer::StoreBuffer(JSRuntime* rt) mayHavePointersToDeadCells_(false) #ifdef DEBUG , - mEntered(false), - markingNondeduplicatable(false) + mEntered(false) #endif { } @@ -97,13 +96,11 @@ StoreBuffer::StoreBuffer(StoreBuffer&& other) mayHavePointersToDeadCells_(other.mayHavePointersToDeadCells_) #ifdef DEBUG , - mEntered(other.mEntered), - markingNondeduplicatable(other.markingNondeduplicatable) + mEntered(other.mEntered) #endif { MOZ_ASSERT(enabled_); MOZ_ASSERT(!mEntered); - MOZ_ASSERT(!markingNondeduplicatable); other.disable(); } @@ -213,21 +210,14 @@ ArenaCellSet* StoreBuffer::WholeCellBuffer::allocateCellSet(Arena* arena) { return nullptr; } - // Maintain separate lists for strings and non-strings, so that all buffered - // string whole cells will be processed before anything else (to prevent them - // from being deduplicated when their chars are used by a tenured string.) - bool isString = - MapAllocToTraceKind(arena->getAllocKind()) == JS::TraceKind::String; - AutoEnterOOMUnsafeRegion oomUnsafe; - ArenaCellSet*& head = isString ? stringHead_ : nonStringHead_; - auto* cells = storage_->new_<ArenaCellSet>(arena, head); + auto* cells = storage_->new_<ArenaCellSet>(arena, head_); if (!cells) { oomUnsafe.crash("Failed to allocate ArenaCellSet"); } arena->bufferedCells() = cells; - head = cells; + head_ = cells; if (isAboutToOverflow()) { rt->gc.storeBuffer().setAboutToOverflow( @@ -243,12 +233,10 @@ void gc::CellHeaderPostWriteBarrier(JSObject** ptr, JSObject* prev, } void StoreBuffer::WholeCellBuffer::clear() { - for (auto** headPtr : {&stringHead_, &nonStringHead_}) { - for (auto* set = *headPtr; set; set = set->next) { - set->arena->bufferedCells() = &ArenaCellSet::Empty; - } - *headPtr = nullptr; + for (auto* set = head_; set; set = set->next) { + set->arena->bufferedCells() = &ArenaCellSet::Empty; } + head_ = nullptr; if (storage_) { storage_->used() ? storage_->releaseAll() : storage_->freeAll(); diff --git a/js/src/gc/StoreBuffer.h b/js/src/gc/StoreBuffer.h index 2e117f02c8..0b1ca8af9b 100644 --- a/js/src/gc/StoreBuffer.h +++ b/js/src/gc/StoreBuffer.h @@ -169,8 +169,7 @@ class StoreBuffer { struct WholeCellBuffer { UniquePtr<LifoAlloc> storage_; - ArenaCellSet* stringHead_ = nullptr; - ArenaCellSet* nonStringHead_ = nullptr; + ArenaCellSet* head_ = nullptr; const Cell* last_ = nullptr; WholeCellBuffer() = default; @@ -180,11 +179,9 @@ class StoreBuffer { WholeCellBuffer(WholeCellBuffer&& other) : storage_(std::move(other.storage_)), - stringHead_(other.stringHead_), - nonStringHead_(other.nonStringHead_), + head_(other.head_), last_(other.last_) { - other.stringHead_ = nullptr; - other.nonStringHead_ = nullptr; + other.head_ = nullptr; other.last_ = nullptr; } WholeCellBuffer& operator=(WholeCellBuffer&& other) { @@ -214,13 +211,20 @@ class StoreBuffer { } bool isEmpty() const { - MOZ_ASSERT_IF(!stringHead_ && !nonStringHead_, - !storage_ || storage_->isEmpty()); - return !stringHead_ && !nonStringHead_; + MOZ_ASSERT_IF(!head_, !storage_ || storage_->isEmpty()); + return !head_; } const Cell** lastBufferedPtr() { return &last_; } + CellSweepSet releaseCellSweepSet() { + CellSweepSet set; + std::swap(storage_, set.storage_); + std::swap(head_, set.head_); + last_ = nullptr; + return set; + } + private: ArenaCellSet* allocateCellSet(Arena* arena); }; @@ -339,9 +343,10 @@ class StoreBuffer { bool operator==(const ValueEdge& other) const { return edge == other.edge; } bool operator!=(const ValueEdge& other) const { return edge != other.edge; } + bool isGCThing() const { return edge->isGCThing(); } + Cell* deref() const { - return edge->isGCThing() ? static_cast<Cell*>(edge->toGCThing()) - : nullptr; + return isGCThing() ? static_cast<Cell*>(edge->toGCThing()) : nullptr; } bool maybeInRememberedSet(const Nursery& nursery) const { @@ -449,10 +454,10 @@ class StoreBuffer { return edge != other.edge; } + bool isGCThing() const { return edge->isGCThing(); } + Cell* deref() const { - return edge->isGCThing() ? static_cast<Cell*>(edge->toGCThing()) - : nullptr; - return nullptr; + return isGCThing() ? static_cast<Cell*>(edge->toGCThing()) : nullptr; } bool maybeInRememberedSet(const Nursery& nursery) const { @@ -525,10 +530,6 @@ class StoreBuffer { #endif public: -#ifdef DEBUG - bool markingNondeduplicatable; -#endif - explicit StoreBuffer(JSRuntime* rt); StoreBuffer(const StoreBuffer& other) = delete; @@ -629,6 +630,10 @@ class StoreBuffer { } void traceGenericEntries(JSTracer* trc) { bufferGeneric.trace(trc, this); } + gc::CellSweepSet releaseCellSweepSet() { + return bufferWholeCell.releaseCellSweepSet(); + } + /* For use by our owned buffers and for testing. */ void setAboutToOverflow(JS::GCReason); @@ -639,6 +644,7 @@ class StoreBuffer { }; // A set of cells in an arena used to implement the whole cell store buffer. +// Also used to store a set of cells that need to be swept. class ArenaCellSet { friend class StoreBuffer; @@ -693,7 +699,17 @@ class ArenaCellSet { WordT getWord(size_t wordIndex) const { return bits.getWord(wordIndex); } - void trace(TenuringTracer& mover); + void setWord(size_t wordIndex, WordT value) { + bits.setWord(wordIndex, value); + } + + // Returns the list of ArenaCellSets that need to be swept. + ArenaCellSet* trace(TenuringTracer& mover); + + // At the end of a minor GC, sweep through all tenured dependent strings that + // may point to nursery-allocated chars to update their pointers in case the + // base string moved its chars. + void sweepDependentStrings(); // Sentinel object used for all empty sets. // diff --git a/js/src/gc/Sweeping.cpp b/js/src/gc/Sweeping.cpp index 123b2c9650..2cd6a2c662 100644 --- a/js/src/gc/Sweeping.cpp +++ b/js/src/gc/Sweeping.cpp @@ -441,6 +441,12 @@ void GCRuntime::waitBackgroundSweepEnd() { void GCRuntime::startBackgroundFree() { AutoLockHelperThreadState lock; + + if (lifoBlocksToFree.ref().isEmpty() && + buffersToFreeAfterMinorGC.ref().empty()) { + return; + } + freeTask.startOrRunIfIdle(lock); } @@ -1194,18 +1200,19 @@ class ImmediateSweepWeakCacheTask : public GCParallelTask { }; void GCRuntime::updateAtomsBitmap() { - DenseBitmap marked; - if (atomMarking.computeBitmapFromChunkMarkBits(rt, marked)) { - for (GCZonesIter zone(this); !zone.done(); zone.next()) { - atomMarking.refineZoneBitmapForCollectedZone(zone, marked); + size_t collectedZones = 0; + size_t uncollectedZones = 0; + for (ZonesIter zone(this, SkipAtoms); !zone.done(); zone.next()) { + if (zone->isCollecting()) { + collectedZones++; + } else { + uncollectedZones++; } - } else { - // Ignore OOM in computeBitmapFromChunkMarkBits. The - // refineZoneBitmapForCollectedZone call can only remove atoms from the - // zone bitmap, so it is conservative to just not call it. } - atomMarking.markAtomsUsedByUncollectedZones(rt); + atomMarking.refineZoneBitmapsForCollectedZones(this, collectedZones); + + atomMarking.markAtomsUsedByUncollectedZones(this, uncollectedZones); // For convenience sweep these tables non-incrementally as part of bitmap // sweeping; they are likely to be much smaller than the main atoms table. diff --git a/js/src/gc/Tenuring.cpp b/js/src/gc/Tenuring.cpp index a9506cfa14..d38a374599 100644 --- a/js/src/gc/Tenuring.cpp +++ b/js/src/gc/Tenuring.cpp @@ -18,6 +18,7 @@ #include "gc/Pretenuring.h" #include "gc/Zone.h" #include "jit/JitCode.h" +#include "js/TypeDecls.h" #include "proxy/Proxy.h" #include "vm/BigIntType.h" #include "vm/JSScript.h" @@ -44,104 +45,120 @@ using mozilla::PodCopy; constexpr size_t MAX_DEDUPLICATABLE_STRING_LENGTH = 500; -TenuringTracer::TenuringTracer(JSRuntime* rt, Nursery* nursery) +TenuringTracer::TenuringTracer(JSRuntime* rt, Nursery* nursery, + bool tenureEverything) : JSTracer(rt, JS::TracerKind::Tenuring, JS::WeakMapTraceAction::TraceKeysAndValues), - nursery_(*nursery) { + nursery_(*nursery), + tenureEverything(tenureEverything) { stringDeDupSet.emplace(); } -size_t TenuringTracer::getTenuredSize() const { - return tenuredSize + tenuredCells * sizeof(NurseryCellHeader); +size_t TenuringTracer::getPromotedSize() const { + return promotedSize + promotedCells * sizeof(NurseryCellHeader); } -size_t TenuringTracer::getTenuredCells() const { return tenuredCells; } - -static inline void UpdateAllocSiteOnTenure(Cell* cell) { - AllocSite* site = NurseryCellHeader::from(cell)->allocSite(); - site->incTenuredCount(); -} +size_t TenuringTracer::getPromotedCells() const { return promotedCells; } void TenuringTracer::onObjectEdge(JSObject** objp, const char* name) { JSObject* obj = *objp; - if (!IsInsideNursery(obj)) { + if (!nursery_.inCollectedRegion(obj)) { + MOZ_ASSERT(!obj->isForwarded()); return; } + *objp = promoteOrForward(obj); + MOZ_ASSERT(!(*objp)->isForwarded()); +} + +JSObject* TenuringTracer::promoteOrForward(JSObject* obj) { + MOZ_ASSERT(nursery_.inCollectedRegion(obj)); + if (obj->isForwarded()) { const gc::RelocationOverlay* overlay = gc::RelocationOverlay::fromCell(obj); - *objp = static_cast<JSObject*>(overlay->forwardingAddress()); - return; + obj = static_cast<JSObject*>(overlay->forwardingAddress()); + if (IsInsideNursery(obj)) { + promotedToNursery = true; + } + return obj; } - onNonForwardedNurseryObjectEdge(objp); + return onNonForwardedNurseryObject(obj); } -void TenuringTracer::onNonForwardedNurseryObjectEdge(JSObject** objp) { - JSObject* obj = *objp; +JSObject* TenuringTracer::onNonForwardedNurseryObject(JSObject* obj) { MOZ_ASSERT(IsInsideNursery(obj)); MOZ_ASSERT(!obj->isForwarded()); - UpdateAllocSiteOnTenure(obj); - - // Take a fast path for tenuring a plain object which is by far the most + // Take a fast path for promoting a plain object as this is by far the most // common case. if (obj->is<PlainObject>()) { - *objp = movePlainObjectToTenured(&obj->as<PlainObject>()); - return; + return promotePlainObject(&obj->as<PlainObject>()); } - *objp = moveToTenuredSlow(obj); + return promoteObjectSlow(obj); } void TenuringTracer::onStringEdge(JSString** strp, const char* name) { JSString* str = *strp; - if (!IsInsideNursery(str)) { + if (!nursery_.inCollectedRegion(str)) { return; } + *strp = promoteOrForward(str); +} + +JSString* TenuringTracer::promoteOrForward(JSString* str) { + MOZ_ASSERT(nursery_.inCollectedRegion(str)); + if (str->isForwarded()) { const gc::RelocationOverlay* overlay = gc::RelocationOverlay::fromCell(str); - *strp = static_cast<JSString*>(overlay->forwardingAddress()); - return; + str = static_cast<JSString*>(overlay->forwardingAddress()); + if (IsInsideNursery(str)) { + promotedToNursery = true; + } + return str; } - onNonForwardedNurseryStringEdge(strp); + return onNonForwardedNurseryString(str); } -void TenuringTracer::onNonForwardedNurseryStringEdge(JSString** strp) { - JSString* str = *strp; +JSString* TenuringTracer::onNonForwardedNurseryString(JSString* str) { MOZ_ASSERT(IsInsideNursery(str)); MOZ_ASSERT(!str->isForwarded()); - UpdateAllocSiteOnTenure(str); - - *strp = moveToTenured(str); + return promoteString(str); } void TenuringTracer::onBigIntEdge(JS::BigInt** bip, const char* name) { JS::BigInt* bi = *bip; - if (!IsInsideNursery(bi)) { + if (!nursery_.inCollectedRegion(bi)) { return; } + *bip = promoteOrForward(bi); +} + +JS::BigInt* TenuringTracer::promoteOrForward(JS::BigInt* bi) { + MOZ_ASSERT(nursery_.inCollectedRegion(bi)); + if (bi->isForwarded()) { const gc::RelocationOverlay* overlay = gc::RelocationOverlay::fromCell(bi); - *bip = static_cast<JS::BigInt*>(overlay->forwardingAddress()); - return; + bi = static_cast<JS::BigInt*>(overlay->forwardingAddress()); + if (IsInsideNursery(bi)) { + promotedToNursery = true; + } + return bi; } - onNonForwardedNurseryBigIntEdge(bip); + return onNonForwardedNurseryBigInt(bi); } -void TenuringTracer::onNonForwardedNurseryBigIntEdge(JS::BigInt** bip) { - JS::BigInt* bi = *bip; +JS::BigInt* TenuringTracer::onNonForwardedNurseryBigInt(JS::BigInt* bi) { MOZ_ASSERT(IsInsideNursery(bi)); MOZ_ASSERT(!bi->isForwarded()); - UpdateAllocSiteOnTenure(bi); - - *bip = moveToTenured(bi); + return promoteBigInt(bi); } void TenuringTracer::onSymbolEdge(JS::Symbol** symp, const char* name) {} @@ -156,7 +173,7 @@ void TenuringTracer::onJitCodeEdge(jit::JitCode** codep, const char* name) {} void TenuringTracer::onScopeEdge(Scope** scopep, const char* name) {} void TenuringTracer::traverse(JS::Value* thingp) { - MOZ_ASSERT(!nursery().isInside(thingp)); + MOZ_ASSERT(!nursery().inCollectedRegion(thingp)); Value value = *thingp; CheckTracedThing(this, value); @@ -166,66 +183,71 @@ void TenuringTracer::traverse(JS::Value* thingp) { } Cell* cell = value.toGCThing(); - if (!IsInsideNursery(cell)) { + if (!nursery_.inCollectedRegion(cell)) { return; } if (cell->isForwarded()) { const gc::RelocationOverlay* overlay = gc::RelocationOverlay::fromCell(cell); - thingp->changeGCThingPayload(overlay->forwardingAddress()); + Cell* target = overlay->forwardingAddress(); + thingp->changeGCThingPayload(target); + if (IsInsideNursery(target)) { + promotedToNursery = true; + } return; } // We only care about a few kinds of GC thing here and this generates much // tighter code than using MapGCThingTyped. if (value.isObject()) { - JSObject* obj = &value.toObject(); - onNonForwardedNurseryObjectEdge(&obj); + JSObject* obj = onNonForwardedNurseryObject(&value.toObject()); MOZ_ASSERT(obj != &value.toObject()); *thingp = JS::ObjectValue(*obj); return; } #ifdef ENABLE_RECORD_TUPLE if (value.isExtendedPrimitive()) { - JSObject* obj = &value.toExtendedPrimitive(); - onNonForwardedNurseryObjectEdge(&obj); + JSObject* obj = onNonForwardedNurseryObject(&value.toExtendedPrimitive()); MOZ_ASSERT(obj != &value.toExtendedPrimitive()); *thingp = JS::ExtendedPrimitiveValue(*obj); return; } #endif if (value.isString()) { - JSString* str = value.toString(); - onNonForwardedNurseryStringEdge(&str); + JSString* str = onNonForwardedNurseryString(value.toString()); MOZ_ASSERT(str != value.toString()); *thingp = JS::StringValue(str); return; } MOZ_ASSERT(value.isBigInt()); - JS::BigInt* bi = value.toBigInt(); - onNonForwardedNurseryBigIntEdge(&bi); + JS::BigInt* bi = onNonForwardedNurseryBigInt(value.toBigInt()); MOZ_ASSERT(bi != value.toBigInt()); *thingp = JS::BigIntValue(bi); } void TenuringTracer::traverse(wasm::AnyRef* thingp) { - MOZ_ASSERT(!nursery().isInside(thingp)); + MOZ_ASSERT(!nursery().inCollectedRegion(thingp)); wasm::AnyRef value = *thingp; CheckTracedThing(this, value); + Cell* cell = value.toGCThing(); + if (!nursery_.inCollectedRegion(cell)) { + return; + } + wasm::AnyRef post = wasm::AnyRef::invalid(); switch (value.kind()) { case wasm::AnyRefKind::Object: { - JSObject* obj = &value.toJSObject(); - onObjectEdge(&obj, "value"); + JSObject* obj = promoteOrForward(&value.toJSObject()); + MOZ_ASSERT(obj != &value.toJSObject()); post = wasm::AnyRef::fromJSObject(*obj); break; } case wasm::AnyRefKind::String: { - JSString* str = value.toJSString(); - onStringEdge(&str, "string"); + JSString* str = promoteOrForward(value.toJSString()); + MOZ_ASSERT(str != value.toJSString()); post = wasm::AnyRef::fromJSString(str); break; } @@ -236,11 +258,20 @@ void TenuringTracer::traverse(wasm::AnyRef* thingp) { } } - if (post != value) { - *thingp = post; - } + *thingp = post; } +class MOZ_RAII TenuringTracer::AutoPromotedAnyToNursery { + public: + explicit AutoPromotedAnyToNursery(TenuringTracer& trc) : trc_(trc) { + trc.promotedToNursery = false; + } + explicit operator bool() const { return trc_.promotedToNursery; } + + private: + TenuringTracer& trc_; +}; + template <typename T> void js::gc::StoreBuffer::MonoTypeBuffer<T>::trace(TenuringTracer& mover, StoreBuffer* owner) { @@ -279,6 +310,8 @@ void js::gc::StoreBuffer::SlotsEdge::trace(TenuringTracer& mover) const { MOZ_ASSERT(!IsInsideNursery(obj), "obj shouldn't live in nursery."); + TenuringTracer::AutoPromotedAnyToNursery promotedToNursery(mover); + if (kind() == ElementKind) { uint32_t initLen = obj->getDenseInitializedLength(); uint32_t numShifted = obj->getElementsHeader()->numShiftedElements(); @@ -289,99 +322,60 @@ void js::gc::StoreBuffer::SlotsEdge::trace(TenuringTracer& mover) const { clampedEnd = numShifted < clampedEnd ? clampedEnd - numShifted : 0; clampedEnd = std::min(clampedEnd, initLen); MOZ_ASSERT(clampedStart <= clampedEnd); - mover.traceSlots( - static_cast<HeapSlot*>(obj->getDenseElements() + clampedStart) - ->unbarrieredAddress(), - clampedEnd - clampedStart); + auto* slotStart = + static_cast<HeapSlot*>(obj->getDenseElements() + clampedStart); + uint32_t nslots = clampedEnd - clampedStart; + mover.traceObjectElements(slotStart->unbarrieredAddress(), nslots); } else { uint32_t start = std::min(start_, obj->slotSpan()); uint32_t end = std::min(start_ + count_, obj->slotSpan()); MOZ_ASSERT(start <= end); mover.traceObjectSlots(obj, start, end); } + + if (promotedToNursery) { + mover.runtime()->gc.storeBuffer().putSlot(obj, kind(), start_, count_); + } } static inline void TraceWholeCell(TenuringTracer& mover, JSObject* object) { - MOZ_ASSERT_IF(object->storeBuffer(), - !object->storeBuffer()->markingNondeduplicatable); mover.traceObject(object); } -// Non-deduplicatable marking is necessary because of the following 2 reasons: +// Return whether the string needs to be swept. // -// 1. Tenured string chars cannot be updated: +// We can break down the relevant dependency chains as follows: // -// If any of the tenured string's bases were deduplicated during tenuring, -// the tenured string's chars pointer would need to be adjusted. This would -// then require updating any other tenured strings that are dependent on the -// first tenured string, and we have no way to find them without scanning -// the entire tenured heap. +// T -> T2 : will not be swept, but safe because T2.chars is fixed. +// T -> N1 -> ... -> T2 : safe because T2.chars is fixed +// T -> N1 -> ... -> N2 : update T.chars += tenured(N2).chars - N2.chars // -// 2. Tenured string cannot store its nursery base or base's chars: +// Collapse the base chain down to simply T -> T2 or T -> N2. The pointer update +// will happen during sweeping. // -// Tenured strings have no place to stash a pointer to their nursery base or -// its chars. You need to be able to traverse any dependent string's chain -// of bases up to a nursery "root base" that points to the malloced chars -// that the dependent strings started out pointing to, so that you can -// calculate the offset of any dependent string and update the ptr+offset if -// the root base gets deduplicated to a different allocation. Tenured -// strings in this base chain will stop you from reaching the nursery -// version of the root base; you can only get to the tenured version, and it -// has no place to store the original chars pointer. -static inline void PreventDeduplicationOfReachableStrings(JSString* str) { - MOZ_ASSERT(str->isTenured()); - MOZ_ASSERT(!str->isForwarded()); - - JSLinearString* baseOrRelocOverlay = str->nurseryBaseOrRelocOverlay(); - - // Walk along the chain of dependent strings' base string pointers - // to mark them all non-deduplicatable. - while (true) { - // baseOrRelocOverlay can be one of the three cases: - // 1. forwarded nursery string: - // The forwarded string still retains the flag that can tell whether - // this string is a dependent string with a base. Its - // StringRelocationOverlay holds a saved pointer to its base in the - // nursery. - // 2. not yet forwarded nursery string: - // Retrieve the base field directly from the string. - // 3. tenured string: - // The nursery base chain ends here, so stop traversing. - if (baseOrRelocOverlay->isForwarded()) { - JSLinearString* tenuredBase = Forwarded(baseOrRelocOverlay); - if (!tenuredBase->hasBase()) { - break; - } - baseOrRelocOverlay = StringRelocationOverlay::fromCell(baseOrRelocOverlay) - ->savedNurseryBaseOrRelocOverlay(); - } else { - JSLinearString* base = baseOrRelocOverlay; - if (base->isTenured()) { - break; - } - if (base->isDeduplicatable()) { - base->setNonDeduplicatable(); - } - if (!base->hasBase()) { - break; - } - baseOrRelocOverlay = base->nurseryBaseOrRelocOverlay(); - } - } -} - -static inline void TraceWholeCell(TenuringTracer& mover, JSString* str) { - MOZ_ASSERT_IF(str->storeBuffer(), - str->storeBuffer()->markingNondeduplicatable); - - // Mark all strings reachable from the tenured string `str` as - // non-deduplicatable. These strings are the bases of the tenured dependent - // string. +// Note that in cases like T -> N1 -> T2 -> T3 -> N2, both T -> N1 and T3 -> N2 +// will be processed by the whole cell buffer (or rather, only T and T3 will +// be in the store buffer). The order that these strings are +// visited does not matter because the nursery bases are left alone until +// sweeping. +static inline bool TraceWholeCell(TenuringTracer& mover, JSString* str) { if (str->hasBase()) { - PreventDeduplicationOfReachableStrings(str); + // For tenured dependent strings -> nursery string edges, sweep the + // (tenured) strings at the end of nursery marking to update chars pointers + // that were in the nursery. Rather than updating the base pointer to point + // directly to the tenured version of itself, we will leave it pointing at + // the nursery Cell (which will become a StringRelocationOverlay during the + // minor GC.) + JSLinearString* base = str->nurseryBaseOrRelocOverlay(); + if (IsInsideNursery(base)) { + str->traceBaseFromStoreBuffer(&mover); + return IsInsideNursery(str->nurseryBaseOrRelocOverlay()); + } } str->traceChildren(&mover); + + return false; } static inline void TraceWholeCell(TenuringTracer& mover, BaseScript* script) { @@ -394,70 +388,173 @@ static inline void TraceWholeCell(TenuringTracer& mover, } template <typename T> -static void TraceBufferedCells(TenuringTracer& mover, Arena* arena, - ArenaCellSet* cells) { +bool TenuringTracer::traceBufferedCells(Arena* arena, ArenaCellSet* cells) { for (size_t i = 0; i < MaxArenaCellIndex; i += cells->BitsPerWord) { ArenaCellSet::WordT bitset = cells->getWord(i / cells->BitsPerWord); while (bitset) { size_t bit = i + js::detail::CountTrailingZeroes(bitset); + bitset &= bitset - 1; // Clear the low bit. + auto cell = reinterpret_cast<T*>(uintptr_t(arena) + ArenaCellIndexBytes * bit); - TraceWholeCell(mover, cell); - bitset &= bitset - 1; // Clear the low bit. + + TenuringTracer::AutoPromotedAnyToNursery promotedToNursery(*this); + + TraceWholeCell(*this, cell); + + if (promotedToNursery) { + runtime()->gc.storeBuffer().putWholeCell(cell); + } } } + + return false; } -void ArenaCellSet::trace(TenuringTracer& mover) { - for (ArenaCellSet* cells = this; cells; cells = cells->next) { +template <> +bool TenuringTracer::traceBufferedCells<JSString>(Arena* arena, + ArenaCellSet* cells) { + bool needsSweep = false; + for (size_t i = 0; i < MaxArenaCellIndex; i += cells->BitsPerWord) { + ArenaCellSet::WordT bitset = cells->getWord(i / cells->BitsPerWord); + ArenaCellSet::WordT tosweep = bitset; + while (bitset) { + size_t bit = i + js::detail::CountTrailingZeroes(bitset); + auto* cell = reinterpret_cast<JSString*>(uintptr_t(arena) + + ArenaCellIndexBytes * bit); + TenuringTracer::AutoPromotedAnyToNursery promotedToNursery(*this); + bool needsSweep = TraceWholeCell(*this, cell); + if (promotedToNursery) { + runtime()->gc.storeBuffer().putWholeCell(cell); + } + ArenaCellSet::WordT mask = bitset - 1; + bitset &= mask; + if (!needsSweep) { + tosweep &= mask; + } + } + + cells->setWord(i / cells->BitsPerWord, tosweep); + if (tosweep) { + needsSweep = true; + } + } + + return needsSweep; +} + +ArenaCellSet* ArenaCellSet::trace(TenuringTracer& mover) { + ArenaCellSet* head = nullptr; + + ArenaCellSet* cells = this; + while (cells) { cells->check(); Arena* arena = cells->arena; arena->bufferedCells() = &ArenaCellSet::Empty; JS::TraceKind kind = MapAllocToTraceKind(arena->getAllocKind()); + bool needsSweep; switch (kind) { case JS::TraceKind::Object: - TraceBufferedCells<JSObject>(mover, arena, cells); + needsSweep = mover.traceBufferedCells<JSObject>(arena, cells); break; case JS::TraceKind::String: - TraceBufferedCells<JSString>(mover, arena, cells); + needsSweep = mover.traceBufferedCells<JSString>(arena, cells); break; case JS::TraceKind::Script: - TraceBufferedCells<BaseScript>(mover, arena, cells); + needsSweep = mover.traceBufferedCells<BaseScript>(arena, cells); break; case JS::TraceKind::JitCode: - TraceBufferedCells<jit::JitCode>(mover, arena, cells); + needsSweep = mover.traceBufferedCells<jit::JitCode>(arena, cells); break; default: MOZ_CRASH("Unexpected trace kind"); } + + ArenaCellSet* next = cells->next; + if (needsSweep) { + cells->next = head; + head = cells; + } + + cells = next; } + + return head; } void js::gc::StoreBuffer::WholeCellBuffer::trace(TenuringTracer& mover, StoreBuffer* owner) { MOZ_ASSERT(owner->isEnabled()); -#ifdef DEBUG - // Verify that all string whole cells are traced first before any other - // strings are visited for any reason. - MOZ_ASSERT(!owner->markingNondeduplicatable); - owner->markingNondeduplicatable = true; -#endif - // Trace all of the strings to mark the non-deduplicatable bits, then trace - // all other whole cells. - if (stringHead_) { - stringHead_->trace(mover); + if (head_) { + head_ = head_->trace(mover); } -#ifdef DEBUG - owner->markingNondeduplicatable = false; -#endif - if (nonStringHead_) { - nonStringHead_->trace(mover); +} + +// Sweep a tenured dependent string with a nursery base. The base chain will +// have been collapsed to a single link before this string was added to the +// sweep set, so only the simple case of a tenured dependent string with a +// nursery base needs to be considered. +template <typename CharT> +void JSDependentString::sweepTypedAfterMinorGC() { + MOZ_ASSERT(isTenured()); + MOZ_ASSERT(IsInsideNursery(nurseryBaseOrRelocOverlay())); + + JSLinearString* base = nurseryBaseOrRelocOverlay(); + MOZ_ASSERT(IsInsideNursery(base)); + MOZ_ASSERT(!Forwarded(base)->hasBase(), "base chain should be collapsed"); + MOZ_ASSERT(base->isForwarded(), "root base should be kept alive"); + auto* baseOverlay = js::gc::StringRelocationOverlay::fromCell(base); + const CharT* oldBaseChars = baseOverlay->savedNurseryChars<CharT>(); + + // We have the base's original chars pointer and its current chars pointer. + // Update our chars pointer, which is an offset from the original base + // chars, and make it point to the same offset within the root's chars. + // (Most of the time, the base chars didn't move and so this has no + // effect.) + const CharT* oldChars = JSString::nonInlineCharsRaw<CharT>(); + size_t offset = oldChars - oldBaseChars; + JSLinearString* tenuredBase = Forwarded(base); + MOZ_ASSERT(offset < tenuredBase->length()); + + const CharT* newBaseChars = tenuredBase->JSString::nonInlineCharsRaw<CharT>(); + relocateNonInlineChars(newBaseChars, offset); + + d.s.u3.base = tenuredBase; +} + +inline void JSDependentString::sweepAfterMinorGC() { + if (hasTwoByteChars()) { + sweepTypedAfterMinorGC<char16_t>(); + } else { + sweepTypedAfterMinorGC<JS::Latin1Char>(); + } +} + +static void SweepDependentStrings(Arena* arena, ArenaCellSet* cells) { + for (size_t i = 0; i < MaxArenaCellIndex; i += cells->BitsPerWord) { + ArenaCellSet::WordT bitset = cells->getWord(i / cells->BitsPerWord); + while (bitset) { + size_t bit = i + js::detail::CountTrailingZeroes(bitset); + auto* str = reinterpret_cast<JSString*>(uintptr_t(arena) + + ArenaCellIndexBytes * bit); + MOZ_ASSERT(str->isTenured()); + str->asDependent().sweepAfterMinorGC(); + bitset &= bitset - 1; // Clear the low bit. + } } +} - stringHead_ = nonStringHead_ = nullptr; +void ArenaCellSet::sweepDependentStrings() { + for (ArenaCellSet* cells = this; cells; cells = cells->next) { + Arena* arena = cells->arena; + arena->bufferedCells() = &ArenaCellSet::Empty; + MOZ_ASSERT(MapAllocToTraceKind(arena->getAllocKind()) == + JS::TraceKind::String); + SweepDependentStrings(arena, cells); + } } template <typename T> @@ -481,18 +578,42 @@ void js::gc::StoreBuffer::CellPtrEdge<T>::trace(TenuringTracer& mover) const { edge, JS::TraceKind::String)); } - DispatchToOnEdge(&mover, edge, "CellPtrEdge"); + if (!mover.nursery().inCollectedRegion(thing)) { + return; + } + + *edge = mover.promoteOrForward(thing); + + if (IsInsideNursery(*edge)) { + mover.runtime()->gc.storeBuffer().putCell(edge); + } } void js::gc::StoreBuffer::ValueEdge::trace(TenuringTracer& mover) const { - if (deref()) { - mover.traverse(edge); + if (!isGCThing()) { + return; + } + + TenuringTracer::AutoPromotedAnyToNursery promotedToNursery(mover); + + mover.traverse(edge); + + if (promotedToNursery) { + mover.runtime()->gc.storeBuffer().putValue(edge); } } void js::gc::StoreBuffer::WasmAnyRefEdge::trace(TenuringTracer& mover) const { - if (deref()) { - mover.traverse(edge); + if (!isGCThing()) { + return; + } + + TenuringTracer::AutoPromotedAnyToNursery promotedToNursery(mover); + + mover.traverse(edge); + + if (promotedToNursery) { + mover.runtime()->gc.storeBuffer().putWasmAnyRef(edge); } } @@ -513,7 +634,7 @@ void js::gc::TenuringTracer::traceObject(JSObject* obj) { if (!nobj->hasEmptyElements()) { HeapSlotArray elements = nobj->getDenseElements(); Value* elems = elements.begin()->unbarrieredAddress(); - traceSlots(elems, elems + nobj->getDenseInitializedLength()); + traceObjectElements(elems, nobj->getDenseInitializedLength()); } traceObjectSlots(nobj, 0, nobj->slotSpan()); @@ -527,16 +648,17 @@ void js::gc::TenuringTracer::traceObjectSlots(NativeObject* nobj, nobj->forEachSlotRange(start, end, traceRange); } +void js::gc::TenuringTracer::traceObjectElements(JS::Value* vp, + uint32_t count) { + traceSlots(vp, vp + count); +} + void js::gc::TenuringTracer::traceSlots(Value* vp, Value* end) { for (; vp != end; ++vp) { traverse(vp); } } -inline void js::gc::TenuringTracer::traceSlots(JS::Value* vp, uint32_t nslots) { - traceSlots(vp, vp + nslots); -} - void js::gc::TenuringTracer::traceString(JSString* str) { str->traceChildren(this); } @@ -564,25 +686,73 @@ inline void js::gc::TenuringTracer::insertIntoObjectFixupList( } template <typename T> -inline T* js::gc::TenuringTracer::allocTenured(Zone* zone, AllocKind kind) { - return static_cast<T*>(static_cast<Cell*>(AllocateCellInGC(zone, kind))); +inline T* js::gc::TenuringTracer::alloc(Zone* zone, AllocKind kind, Cell* src) { + AllocSite* site = NurseryCellHeader::from(src)->allocSite(); + site->incPromotedCount(); + + void* ptr = allocCell<T::TraceKind>(zone, kind, site, src); + auto* cell = reinterpret_cast<T*>(ptr); + if (IsInsideNursery(cell)) { + MOZ_ASSERT(!nursery().inCollectedRegion(cell)); + promotedToNursery = true; + } + + return cell; } -JSString* js::gc::TenuringTracer::allocTenuredString(JSString* src, Zone* zone, - AllocKind dstKind) { - JSString* dst = allocTenured<JSString>(zone, dstKind); - tenuredSize += moveStringToTenured(dst, src, dstKind); - tenuredCells++; +template <JS::TraceKind traceKind> +void* js::gc::TenuringTracer::allocCell(Zone* zone, AllocKind allocKind, + AllocSite* site, Cell* src) { + MOZ_ASSERT(zone == src->zone()); + + if (!shouldTenure(zone, traceKind, src)) { + // Allocations from the optimized alloc site continue to use that site, + // otherwise a special promoted alloc site it used. + if (site->kind() != AllocSite::Kind::Optimized) { + site = &zone->pretenuring.promotedAllocSite(traceKind); + } + + size_t thingSize = Arena::thingSize(allocKind); + void* ptr = nursery_.tryAllocateCell(site, thingSize, traceKind); + if (MOZ_LIKELY(ptr)) { + return ptr; + } + + JSContext* cx = runtime()->mainContextFromOwnThread(); + ptr = CellAllocator::RetryNurseryAlloc<NoGC>(cx, traceKind, allocKind, + thingSize, site); + if (MOZ_LIKELY(ptr)) { + return ptr; + } + + // The nursery is full. This is unlikely but can happen. Fall through to + // the tenured allocation path. + } + + return AllocateTenuredCellInGC(zone, allocKind); +} + +JSString* js::gc::TenuringTracer::allocString(JSString* src, Zone* zone, + AllocKind dstKind) { + JSString* dst = alloc<JSString>(zone, dstKind, src); + promotedSize += moveString(dst, src, dstKind); + promotedCells++; return dst; } -JSObject* js::gc::TenuringTracer::moveToTenuredSlow(JSObject* src) { +bool js::gc::TenuringTracer::shouldTenure(Zone* zone, JS::TraceKind traceKind, + Cell* cell) { + return tenureEverything || !zone->allocKindInNursery(traceKind) || + nursery_.shouldTenure(cell); +} + +JSObject* js::gc::TenuringTracer::promoteObjectSlow(JSObject* src) { MOZ_ASSERT(IsInsideNursery(src)); MOZ_ASSERT(!src->is<PlainObject>()); AllocKind dstKind = src->allocKindForTenure(nursery()); - auto* dst = allocTenured<JSObject>(src->nurseryZone(), dstKind); + auto* dst = alloc<JSObject>(src->nurseryZone(), dstKind, src); size_t srcSize = Arena::thingSize(dstKind); @@ -590,8 +760,8 @@ JSObject* js::gc::TenuringTracer::moveToTenuredSlow(JSObject* src) { // and dst. We deal with this by copying elements manually, possibly // re-inlining them if there is adequate room inline in dst. // - // For Arrays and Tuples we're reducing tenuredSize to the smaller srcSize - // because moveElementsToTenured() accounts for all Array or Tuple elements, + // For Arrays and Tuples we're reducing promotedSize to the smaller srcSize + // because moveElements() accounts for all Array or Tuple elements, // even if they are inlined. if (src->is<FixedLengthTypedArrayObject>()) { auto* tarray = &src->as<FixedLengthTypedArrayObject>(); @@ -613,8 +783,8 @@ JSObject* js::gc::TenuringTracer::moveToTenuredSlow(JSObject* src) { srcSize = sizeof(NativeObject); } - tenuredSize += srcSize; - tenuredCells++; + promotedSize += srcSize; + promotedCells++; // Copy the Cell contents. MOZ_ASSERT(OffsetFromChunkStart(src) >= sizeof(ChunkBase)); @@ -625,8 +795,8 @@ JSObject* js::gc::TenuringTracer::moveToTenuredSlow(JSObject* src) { if (src->is<NativeObject>()) { NativeObject* ndst = &dst->as<NativeObject>(); NativeObject* nsrc = &src->as<NativeObject>(); - tenuredSize += moveSlotsToTenured(ndst, nsrc); - tenuredSize += moveElementsToTenured(ndst, nsrc, dstKind); + promotedSize += moveSlots(ndst, nsrc); + promotedSize += moveElements(ndst, nsrc, dstKind); } JSObjectMovedOp op = dst->getClass()->extObjectMovedOp(); @@ -634,7 +804,7 @@ JSObject* js::gc::TenuringTracer::moveToTenuredSlow(JSObject* src) { if (op) { // Tell the hazard analysis that the object moved hook can't GC. JS::AutoSuppressGCAnalysis nogc; - tenuredSize += op(dst, src); + promotedSize += op(dst, src); } else { MOZ_ASSERT_IF(src->getClass()->hasFinalize(), CanNurseryAllocateFinalizedClass(src->getClass())); @@ -647,18 +817,17 @@ JSObject* js::gc::TenuringTracer::moveToTenuredSlow(JSObject* src) { return dst; } -inline JSObject* js::gc::TenuringTracer::movePlainObjectToTenured( - PlainObject* src) { - // Fast path version of moveToTenuredSlow() for specialized for PlainObject. +inline JSObject* js::gc::TenuringTracer::promotePlainObject(PlainObject* src) { + // Fast path version of promoteObjectSlow() for specialized for PlainObject. MOZ_ASSERT(IsInsideNursery(src)); AllocKind dstKind = src->allocKindForTenure(); - auto* dst = allocTenured<PlainObject>(src->nurseryZone(), dstKind); + auto* dst = alloc<PlainObject>(src->nurseryZone(), dstKind, src); size_t srcSize = Arena::thingSize(dstKind); - tenuredSize += srcSize; - tenuredCells++; + promotedSize += srcSize; + promotedCells++; // Copy the Cell contents. MOZ_ASSERT(OffsetFromChunkStart(src) >= sizeof(ChunkBase)); @@ -666,8 +835,8 @@ inline JSObject* js::gc::TenuringTracer::movePlainObjectToTenured( js_memcpy(dst, src, srcSize); // Move the slots and elements. - tenuredSize += moveSlotsToTenured(dst, src); - tenuredSize += moveElementsToTenured(dst, src, dstKind); + promotedSize += moveSlots(dst, src); + promotedSize += moveElements(dst, src, dstKind); MOZ_ASSERT(!dst->getClass()->extObjectMovedOp()); @@ -678,8 +847,7 @@ inline JSObject* js::gc::TenuringTracer::movePlainObjectToTenured( return dst; } -size_t js::gc::TenuringTracer::moveSlotsToTenured(NativeObject* dst, - NativeObject* src) { +size_t js::gc::TenuringTracer::moveSlots(NativeObject* dst, NativeObject* src) { /* Fixed slots have already been copied over. */ if (!src->hasDynamicSlots()) { return 0; @@ -702,9 +870,9 @@ size_t js::gc::TenuringTracer::moveSlotsToTenured(NativeObject* dst, return allocSize; } -size_t js::gc::TenuringTracer::moveElementsToTenured(NativeObject* dst, - NativeObject* src, - AllocKind dstKind) { +size_t js::gc::TenuringTracer::moveElements(NativeObject* dst, + NativeObject* src, + AllocKind dstKind) { if (src->hasEmptyElements()) { return 0; } @@ -751,7 +919,7 @@ inline void js::gc::TenuringTracer::insertIntoStringFixupList( stringHead = entry; } -JSString* js::gc::TenuringTracer::moveToTenured(JSString* src) { +JSString* js::gc::TenuringTracer::promoteString(JSString* src) { MOZ_ASSERT(IsInsideNursery(src)); MOZ_ASSERT(!src->isExternal()); @@ -762,29 +930,32 @@ JSString* js::gc::TenuringTracer::moveToTenured(JSString* src) { // the atom. Don't do this for dependent strings because they're more // complicated. See StringRelocationOverlay and DeduplicationStringHasher // comments. + MOZ_ASSERT(!src->isAtom()); if (src->isLinear() && src->inStringToAtomCache() && src->isDeduplicatable() && !src->hasBase()) { JSLinearString* linear = &src->asLinear(); JSAtom* atom = runtime()->caches().stringToAtomCache.lookupInMap(linear); - MOZ_ASSERT(atom, "Why was the cache purged before minor GC?"); - - // Only deduplicate if both strings have the same encoding, to not confuse - // dependent strings. - if (src->hasTwoByteChars() == atom->hasTwoByteChars()) { - // The StringToAtomCache isn't used for inline strings (due to the minimum - // length) so canOwnDependentChars must be true for both src and atom. - // This means if there are dependent strings floating around using str's - // chars, they will be able to use the chars from the atom. - static_assert(StringToAtomCache::MinStringLength > - JSFatInlineString::MAX_LENGTH_LATIN1); - static_assert(StringToAtomCache::MinStringLength > - JSFatInlineString::MAX_LENGTH_TWO_BYTE); - MOZ_ASSERT(src->canOwnDependentChars()); - MOZ_ASSERT(atom->canOwnDependentChars()); - - StringRelocationOverlay::forwardCell(src, atom); - gcprobes::PromoteToTenured(src, atom); - return atom; + // The string will not be present in the cache if it was previously promoted + // to the second nursery generation. + if (atom) { + // Only deduplicate if both strings have the same encoding, to not confuse + // dependent strings. + if (src->hasTwoByteChars() == atom->hasTwoByteChars()) { + // The StringToAtomCache isn't used for inline strings (due to the + // minimum length) so canOwnDependentChars must be true for both src and + // atom. This means if there are dependent strings floating around using + // str's chars, they will be able to use the chars from the atom. + static_assert(StringToAtomCache::MinStringLength > + JSFatInlineString::MAX_LENGTH_LATIN1); + static_assert(StringToAtomCache::MinStringLength > + JSFatInlineString::MAX_LENGTH_TWO_BYTE); + MOZ_ASSERT(src->canOwnDependentChars()); + MOZ_ASSERT(atom->canOwnDependentChars()); + + StringRelocationOverlay::forwardCell(src, atom); + gcprobes::PromoteToTenured(src, atom); + return atom; + } } } @@ -798,9 +969,12 @@ JSString* js::gc::TenuringTracer::moveToTenured(JSString* src) { // 3. It is deduplicatable: // The JSString NON_DEDUP_BIT flag is unset. // 4. It matches an entry in stringDeDupSet. + // 5. It is moved to the tenured heap. - if (src->length() < MAX_DEDUPLICATABLE_STRING_LENGTH && src->isLinear() && + if (shouldTenure(zone, JS::TraceKind::String, src) && + src->length() < MAX_DEDUPLICATABLE_STRING_LENGTH && src->isLinear() && src->isDeduplicatable() && stringDeDupSet.isSome()) { + src->clearBitsOnTenure(); auto p = stringDeDupSet->lookupForAdd(src); if (p) { // Deduplicate to the looked-up string! @@ -811,7 +985,7 @@ JSString* js::gc::TenuringTracer::moveToTenured(JSString* src) { return dst; } - dst = allocTenuredString(src, zone, dstKind); + dst = allocString(src, zone, dstKind); using DedupHasher [[maybe_unused]] = DeduplicationStringHasher<JSString*>; MOZ_ASSERT(DedupHasher::hash(src) == DedupHasher::hash(dst), @@ -823,14 +997,17 @@ JSString* js::gc::TenuringTracer::moveToTenured(JSString* src) { stringDeDupSet.reset(); } } else { - dst = allocTenuredString(src, zone, dstKind); - dst->clearNonDeduplicatable(); + dst = allocString(src, zone, dstKind); + if (dst->isTenured()) { + src->clearBitsOnTenure(); + dst->clearBitsOnTenure(); + } } zone->stringStats.ref().noteTenured(src->allocSize()); auto* overlay = StringRelocationOverlay::forwardCell(src, dst); - MOZ_ASSERT(dst->isDeduplicatable()); + MOZ_ASSERT_IF(dst->isTenured() && dst->isLinear(), dst->isDeduplicatable()); if (dst->hasBase() || dst->isRope()) { // dst or one of its leaves might have a base that will be deduplicated. @@ -874,6 +1051,9 @@ void js::gc::TenuringTracer::relocateDependentStringChars( tenuredDependentStr->relocateNonInlineChars<const CharT*>( tenuredRootBase->nonInlineChars<CharT>(nogc), *offset); tenuredDependentStr->setBase(tenuredRootBase); + if (tenuredDependentStr->isTenured() && !tenuredRootBase->isTenured()) { + runtime()->gc.storeBuffer().putWholeCell(tenuredDependentStr); + } return; } @@ -889,7 +1069,7 @@ void js::gc::TenuringTracer::relocateDependentStringChars( // The root base can be in either the nursery or the tenured heap. // dependentStr chars needs to be relocated after traceString if the // root base is in the nursery. - if (!(*rootBase)->isTenured()) { + if (nursery().inCollectedRegion(*rootBase)) { *rootBaseNotYetForwarded = true; const CharT* rootBaseChars = (*rootBase)->nonInlineChars<CharT>(nogc); *offset = dependentStrChars - rootBaseChars; @@ -906,15 +1086,15 @@ void js::gc::TenuringTracer::relocateDependentStringChars( } } -JS::BigInt* js::gc::TenuringTracer::moveToTenured(JS::BigInt* src) { +JS::BigInt* js::gc::TenuringTracer::promoteBigInt(JS::BigInt* src) { MOZ_ASSERT(IsInsideNursery(src)); AllocKind dstKind = src->getAllocKind(); Zone* zone = src->nurseryZone(); - JS::BigInt* dst = allocTenured<JS::BigInt>(zone, dstKind); - tenuredSize += moveBigIntToTenured(dst, src, dstKind); - tenuredCells++; + JS::BigInt* dst = alloc<JS::BigInt>(zone, dstKind, src); + promotedSize += moveBigInt(dst, src, dstKind); + promotedCells++; RelocationOverlay::forwardCell(src, dst); @@ -924,41 +1104,57 @@ JS::BigInt* js::gc::TenuringTracer::moveToTenured(JS::BigInt* src) { void js::gc::TenuringTracer::collectToObjectFixedPoint() { while (RelocationOverlay* p = objHead) { + MOZ_ASSERT(nursery().inCollectedRegion(p)); objHead = objHead->next(); auto* obj = static_cast<JSObject*>(p->forwardingAddress()); + + MOZ_ASSERT_IF(IsInsideNursery(obj), !nursery().inCollectedRegion(obj)); + + AutoPromotedAnyToNursery promotedAnyToNursery(*this); traceObject(obj); + if (obj->isTenured() && promotedAnyToNursery) { + runtime()->gc.storeBuffer().putWholeCell(obj); + } } } void js::gc::TenuringTracer::collectToStringFixedPoint() { while (StringRelocationOverlay* p = stringHead) { + MOZ_ASSERT(nursery().inCollectedRegion(p)); stringHead = stringHead->next(); - auto* tenuredStr = static_cast<JSString*>(p->forwardingAddress()); + auto* str = static_cast<JSString*>(p->forwardingAddress()); + MOZ_ASSERT_IF(IsInsideNursery(str), !nursery().inCollectedRegion(str)); + // To ensure the NON_DEDUP_BIT was reset properly. - MOZ_ASSERT(tenuredStr->isDeduplicatable()); + MOZ_ASSERT(!str->isAtom()); + MOZ_ASSERT_IF(str->isTenured() && str->isLinear(), str->isDeduplicatable()); // The nursery root base might not be forwarded before - // traceString(tenuredStr). traceString(tenuredStr) will forward the root + // traceString(str). traceString(str) will forward the root // base if that's the case. Dependent string chars needs to be relocated // after traceString if root base was not forwarded. size_t offset = 0; bool rootBaseNotYetForwarded = false; JSLinearString* rootBase = nullptr; - if (tenuredStr->isDependent()) { - if (tenuredStr->hasTwoByteChars()) { + if (str->isDependent()) { + if (str->hasTwoByteChars()) { relocateDependentStringChars<char16_t>( - &tenuredStr->asDependent(), p->savedNurseryBaseOrRelocOverlay(), - &offset, &rootBaseNotYetForwarded, &rootBase); + &str->asDependent(), p->savedNurseryBaseOrRelocOverlay(), &offset, + &rootBaseNotYetForwarded, &rootBase); } else { relocateDependentStringChars<JS::Latin1Char>( - &tenuredStr->asDependent(), p->savedNurseryBaseOrRelocOverlay(), - &offset, &rootBaseNotYetForwarded, &rootBase); + &str->asDependent(), p->savedNurseryBaseOrRelocOverlay(), &offset, + &rootBaseNotYetForwarded, &rootBase); } } - traceString(tenuredStr); + AutoPromotedAnyToNursery promotedAnyToNursery(*this); + traceString(str); + if (str->isTenured() && promotedAnyToNursery) { + runtime()->gc.storeBuffer().putWholeCell(str); + } if (rootBaseNotYetForwarded) { MOZ_ASSERT(rootBase->isForwarded(), @@ -968,25 +1164,34 @@ void js::gc::TenuringTracer::collectToStringFixedPoint() { JSLinearString* tenuredRootBase = Forwarded(rootBase); MOZ_ASSERT(offset < tenuredRootBase->length()); - if (tenuredStr->hasTwoByteChars()) { - tenuredStr->asDependent().relocateNonInlineChars<const char16_t*>( + if (str->hasTwoByteChars()) { + str->asDependent().relocateNonInlineChars<const char16_t*>( tenuredRootBase->twoByteChars(nogc), offset); } else { - tenuredStr->asDependent().relocateNonInlineChars<const JS::Latin1Char*>( + str->asDependent().relocateNonInlineChars<const JS::Latin1Char*>( tenuredRootBase->latin1Chars(nogc), offset); } - tenuredStr->setBase(tenuredRootBase); + + str->setBase(tenuredRootBase); + if (str->isTenured() && !tenuredRootBase->isTenured()) { + runtime()->gc.storeBuffer().putWholeCell(str); + } + } + + if (str->hasBase()) { + MOZ_ASSERT(!str->base()->isForwarded()); + MOZ_ASSERT_IF(!str->base()->isTenured(), + !nursery().inCollectedRegion(str->base())); } } } -size_t js::gc::TenuringTracer::moveStringToTenured(JSString* dst, JSString* src, - AllocKind dstKind) { +size_t js::gc::TenuringTracer::moveString(JSString* dst, JSString* src, + AllocKind dstKind) { size_t size = Arena::thingSize(dstKind); - // At the moment, strings always have the same AllocKind between src and - // dst. This may change in the future. - MOZ_ASSERT(dst->asTenured().getAllocKind() == src->getAllocKind()); + MOZ_ASSERT_IF(dst->isTenured(), + dst->asTenured().getAllocKind() == src->getAllocKind()); // Copy the Cell contents. MOZ_ASSERT(OffsetToChunkEnd(src) >= size); @@ -999,7 +1204,8 @@ size_t js::gc::TenuringTracer::moveStringToTenured(JSString* dst, JSString* src, if (src->ownsMallocedChars()) { void* chars = src->asLinear().nonInlineCharsRaw(); nursery().removeMallocedBufferDuringMinorGC(chars); - AddCellMemory(dst, dst->asLinear().allocSize(), MemoryUse::StringContents); + nursery().trackMallocedBufferOnPromotion( + chars, dst, dst->asLinear().allocSize(), MemoryUse::StringContents); return size; } @@ -1016,14 +1222,12 @@ size_t js::gc::TenuringTracer::moveStringToTenured(JSString* dst, JSString* src, return size; } -size_t js::gc::TenuringTracer::moveBigIntToTenured(JS::BigInt* dst, - JS::BigInt* src, - AllocKind dstKind) { +size_t js::gc::TenuringTracer::moveBigInt(JS::BigInt* dst, JS::BigInt* src, + AllocKind dstKind) { size_t size = Arena::thingSize(dstKind); - // At the moment, BigInts always have the same AllocKind between src and - // dst. This may change in the future. - MOZ_ASSERT(dst->asTenured().getAllocKind() == src->getAllocKind()); + MOZ_ASSERT_IF(dst->isTenured(), + dst->asTenured().getAllocKind() == src->getAllocKind()); // Copy the Cell contents. MOZ_ASSERT(OffsetToChunkEnd(src) >= size); diff --git a/js/src/gc/Tenuring.h b/js/src/gc/Tenuring.h index 3eca5f4bc3..8bb9efd4f8 100644 --- a/js/src/gc/Tenuring.h +++ b/js/src/gc/Tenuring.h @@ -26,6 +26,8 @@ class AnyRef; namespace gc { +class AllocSite; +class ArenaCellSet; class RelocationOverlay; class StringRelocationOverlay; @@ -39,10 +41,10 @@ struct DeduplicationStringHasher { class TenuringTracer final : public JSTracer { Nursery& nursery_; - // Amount of data moved to the tenured generation during collection. - size_t tenuredSize = 0; - // Number of cells moved to the tenured generation. - size_t tenuredCells = 0; + // Size of data promoted during collection. + size_t promotedSize = 0; + // Number of cells promoted during collection. + size_t promotedCells = 0; // These lists are threaded through the Nursery using the space from // already moved things. The lists are used to fix up the moved things and @@ -59,27 +61,34 @@ class TenuringTracer final : public JSTracer { // collection when out of memory to insert new entries. mozilla::Maybe<StringDeDupSet> stringDeDupSet; + bool tenureEverything; + + // A flag set when a GC thing is promoted to the next nursery generation (as + // opposed to the tenured heap). This is used to check when we need to add an + // edge to the remembered set during nursery collection. + bool promotedToNursery = false; + #define DEFINE_ON_EDGE_METHOD(name, type, _1, _2) \ void on##name##Edge(type** thingp, const char* name) override; JS_FOR_EACH_TRACEKIND(DEFINE_ON_EDGE_METHOD) #undef DEFINE_ON_EDGE_METHOD public: - TenuringTracer(JSRuntime* rt, Nursery* nursery); + TenuringTracer(JSRuntime* rt, Nursery* nursery, bool tenureEverything); Nursery& nursery() { return nursery_; } - // Move all objects and everything they can reach to the tenured heap. Called - // after all roots have been traced. + // Promote all live objects and everything they can reach. Called after all + // roots have been traced. void collectToObjectFixedPoint(); - // Move all strings and all strings they can reach to the tenured heap, and - // additionally do any fixups for when strings are pointing into memory that - // was deduplicated. Called after collectToObjectFixedPoint(). + // Promote all live strings and all strings they can reach, and additionally + // do any fixups for when strings are pointing into memory that was + // deduplicated. Called after collectToObjectFixedPoint(). void collectToStringFixedPoint(); - size_t getTenuredSize() const; - size_t getTenuredCells() const; + size_t getPromotedSize() const; + size_t getPromotedCells() const; void traverse(JS::Value* thingp); void traverse(wasm::AnyRef* thingp); @@ -87,14 +96,26 @@ class TenuringTracer final : public JSTracer { // The store buffers need to be able to call these directly. void traceObject(JSObject* obj); void traceObjectSlots(NativeObject* nobj, uint32_t start, uint32_t end); - void traceSlots(JS::Value* vp, uint32_t nslots); + void traceObjectElements(JS::Value* vp, uint32_t count); void traceString(JSString* str); void traceBigInt(JS::BigInt* bi); + // Methods to promote a live cell or get the pointer to its new location if + // that has already happened. The store buffers call these. + JSObject* promoteOrForward(JSObject* obj); + JSString* promoteOrForward(JSString* str); + JS::BigInt* promoteOrForward(JS::BigInt* bip); + + // Returns whether any cells in the arena require sweeping. + template <typename T> + bool traceBufferedCells(Arena* arena, ArenaCellSet* cells); + + class AutoPromotedAnyToNursery; + private: - MOZ_ALWAYS_INLINE void onNonForwardedNurseryObjectEdge(JSObject** objp); - MOZ_ALWAYS_INLINE void onNonForwardedNurseryStringEdge(JSString** strp); - MOZ_ALWAYS_INLINE void onNonForwardedNurseryBigIntEdge(JS::BigInt** bip); + MOZ_ALWAYS_INLINE JSObject* onNonForwardedNurseryObject(JSObject* obj); + MOZ_ALWAYS_INLINE JSString* onNonForwardedNurseryString(JSString* str); + MOZ_ALWAYS_INLINE JS::BigInt* onNonForwardedNurseryBigInt(JS::BigInt* bi); // The dependent string chars needs to be relocated if the base which it's // using chars from has been deduplicated. @@ -109,22 +130,24 @@ class TenuringTracer final : public JSTracer { inline void insertIntoStringFixupList(gc::StringRelocationOverlay* entry); template <typename T> - inline T* allocTenured(JS::Zone* zone, gc::AllocKind kind); - JSString* allocTenuredString(JSString* src, JS::Zone* zone, - gc::AllocKind dstKind); - - inline JSObject* movePlainObjectToTenured(PlainObject* src); - JSObject* moveToTenuredSlow(JSObject* src); - JSString* moveToTenured(JSString* src); - JS::BigInt* moveToTenured(JS::BigInt* src); - - size_t moveElementsToTenured(NativeObject* dst, NativeObject* src, - gc::AllocKind dstKind); - size_t moveSlotsToTenured(NativeObject* dst, NativeObject* src); - size_t moveStringToTenured(JSString* dst, JSString* src, - gc::AllocKind dstKind); - size_t moveBigIntToTenured(JS::BigInt* dst, JS::BigInt* src, - gc::AllocKind dstKind); + T* alloc(JS::Zone* zone, gc::AllocKind kind, gc::Cell* src); + template <JS::TraceKind traceKind> + void* allocCell(JS::Zone* zone, gc::AllocKind allocKind, gc::AllocSite* site, + gc::Cell* src); + JSString* allocString(JSString* src, JS::Zone* zone, gc::AllocKind dstKind); + + bool shouldTenure(Zone* zone, JS::TraceKind traceKind, Cell* cell); + + inline JSObject* promotePlainObject(PlainObject* src); + JSObject* promoteObjectSlow(JSObject* src); + JSString* promoteString(JSString* src); + JS::BigInt* promoteBigInt(JS::BigInt* src); + + size_t moveElements(NativeObject* dst, NativeObject* src, + gc::AllocKind dstKind); + size_t moveSlots(NativeObject* dst, NativeObject* src); + size_t moveString(JSString* dst, JSString* src, gc::AllocKind dstKind); + size_t moveBigInt(JS::BigInt* dst, JS::BigInt* src, gc::AllocKind dstKind); void traceSlots(JS::Value* vp, JS::Value* end); }; diff --git a/js/src/gc/TraceMethods-inl.h b/js/src/gc/TraceMethods-inl.h index 37da24fe6e..e7066c5ebc 100644 --- a/js/src/gc/TraceMethods-inl.h +++ b/js/src/gc/TraceMethods-inl.h @@ -30,6 +30,9 @@ #include "vm/SymbolType.h" #include "wasm/WasmJS.h" +#include "gc/Marking-inl.h" +#include "vm/StringType-inl.h" + inline void js::BaseScript::traceChildren(JSTracer* trc) { TraceNullableEdge(trc, &function_, "function"); TraceEdge(trc, &sourceObject_, "sourceObject"); @@ -76,6 +79,20 @@ inline void JSString::traceChildren(JSTracer* trc) { asRope().traceChildren(trc); } } +inline void JSString::traceBaseFromStoreBuffer(JSTracer* trc) { + MOZ_ASSERT(!d.s.u3.base->isTenured()); + + // Contract the base chain so that this tenured dependent string points + // directly at the root base that owns its chars. + JSLinearString* root = asDependent().rootBaseDuringMinorGC(); + d.s.u3.base = root; + if (!root->isTenured()) { + js::TraceManuallyBarrieredEdge(trc, &root, "base"); + // Do not update the actual base to the tenured string yet. This string will + // need to be swept in order to update its chars ptr to be relative to the + // root, and that update requires information from the overlay. + } +} template <uint32_t opts> void js::GCMarker::eagerlyMarkChildren(JSString* str) { if (str->isLinear()) { diff --git a/js/src/gc/WeakMap-inl.h b/js/src/gc/WeakMap-inl.h index 015e5071ed..d7b5feb5a6 100644 --- a/js/src/gc/WeakMap-inl.h +++ b/js/src/gc/WeakMap-inl.h @@ -18,6 +18,7 @@ #include "gc/GCLock.h" #include "gc/Marking.h" #include "gc/Zone.h" +#include "js/Prefs.h" #include "js/TraceKind.h" #include "vm/JSContext.h" @@ -71,6 +72,16 @@ static inline JSObject* GetDelegate(const T& key) { template <> inline JSObject* GetDelegate(gc::Cell* const&) = delete; +template <typename T> +static inline bool IsSymbol(const T& key) { + return false; +} + +template <> +inline bool IsSymbol(const HeapPtr<JS::Value>& key) { + return key.isSymbol(); +} + } // namespace gc::detail // Weakmap entry -> value edges are only visible if the map is traced, which @@ -303,25 +314,42 @@ bool WeakMap<K, V>::findSweepGroupEdges() { for (Range r = all(); !r.empty(); r.popFront()) { const K& key = r.front().key(); - // If the key type doesn't have delegates, then this will always return - // nullptr and the optimizer can remove the entire body of this function. JSObject* delegate = gc::detail::GetDelegate(key); - if (!delegate) { + if (delegate) { + // Marking a WeakMap key's delegate will mark the key, so process the + // delegate zone no later than the key zone. + Zone* delegateZone = delegate->zone(); + gc::Cell* keyCell = gc::ToMarkable(key); + MOZ_ASSERT(keyCell); + Zone* keyZone = keyCell->zone(); + if (delegateZone != keyZone && delegateZone->isGCMarking() && + keyZone->isGCMarking()) { + if (!delegateZone->addSweepGroupEdgeTo(keyZone)) { + return false; + } + } + } + +#ifdef NIGHTLY_BUILD + bool symbolsAsWeakMapKeysEnabled = + JS::Prefs::experimental_symbols_as_weakmap_keys(); + if (!symbolsAsWeakMapKeysEnabled) { continue; } - // Marking a WeakMap key's delegate will mark the key, so process the - // delegate zone no later than the key zone. - Zone* delegateZone = delegate->zone(); - gc::Cell* keyCell = gc::ToMarkable(key); - MOZ_ASSERT(keyCell); - Zone* keyZone = keyCell->zone(); - if (delegateZone != keyZone && delegateZone->isGCMarking() && - keyZone->isGCMarking()) { - if (!delegateZone->addSweepGroupEdgeTo(keyZone)) { - return false; + bool isSym = gc::detail::IsSymbol(key); + if (isSym) { + gc::Cell* keyCell = gc::ToMarkable(key); + Zone* keyZone = keyCell->zone(); + MOZ_ASSERT(keyZone->isAtomsZone()); + + if (zone()->isGCMarking() && keyZone->isGCMarking()) { + if (!keyZone->addSweepGroupEdgeTo(zone())) { + return false; + } } } +#endif } return true; } diff --git a/js/src/gc/Zone.cpp b/js/src/gc/Zone.cpp index d0586d5d56..6437f6f4c3 100644 --- a/js/src/gc/Zone.cpp +++ b/js/src/gc/Zone.cpp @@ -622,7 +622,7 @@ void Zone::fixupAfterMovingGC() { } void Zone::purgeAtomCache() { - atomCache().clearAndCompact(); + atomCache_.ref().reset(); // Also purge the dtoa caches so that subsequent lookups populate atom // cache too. @@ -981,20 +981,3 @@ bool Zone::registerObjectWithWeakPointers(JSObject* obj) { MOZ_ASSERT(!IsInsideNursery(obj)); return objectsWithWeakPointers.ref().append(obj); } - -js::DependentScriptSet* Zone::getOrCreateDependentScriptSet( - JSContext* cx, js::InvalidatingFuse* fuse) { - for (auto& dss : fuseDependencies) { - if (dss.associatedFuse == fuse) { - return &dss; - } - } - - if (!fuseDependencies.emplaceBack(cx, fuse)) { - return nullptr; - } - - auto& dss = fuseDependencies.back(); - MOZ_ASSERT(dss.associatedFuse == fuse); - return &dss; -} diff --git a/js/src/gc/Zone.h b/js/src/gc/Zone.h index fd91de8626..a5ce161cc4 100644 --- a/js/src/gc/Zone.h +++ b/js/src/gc/Zone.h @@ -15,6 +15,8 @@ #include "mozilla/PodOperations.h" #include "mozilla/TimeStamp.h" +#include <array> + #include "jstypes.h" #include "ds/Bitmap.h" @@ -139,6 +141,148 @@ class MOZ_NON_TEMPORARY_CLASS FunctionToStringCache { MOZ_ALWAYS_INLINE void put(BaseScript* script, JSString* string); }; +// HashAndLength is a simple class encapsulating the combination of a HashNumber +// and a (string) length into a single 64-bit value. Having them bundled +// together like this enables us to compare pairs of hashes and lengths with a +// single 64-bit comparison. +class HashAndLength { + public: + MOZ_ALWAYS_INLINE explicit HashAndLength(uint64_t initialValue = unsetValue()) + : mHashAndLength(initialValue) {} + MOZ_ALWAYS_INLINE HashAndLength(HashNumber hash, uint32_t length) + : mHashAndLength(uint64FromHashAndLength(hash, length)) {} + + void MOZ_ALWAYS_INLINE set(HashNumber hash, uint32_t length) { + mHashAndLength = uint64FromHashAndLength(hash, length); + } + + constexpr MOZ_ALWAYS_INLINE HashNumber hash() const { + return hashFromUint64(mHashAndLength); + } + constexpr MOZ_ALWAYS_INLINE uint32_t length() const { + return lengthFromUint64(mHashAndLength); + } + + constexpr MOZ_ALWAYS_INLINE bool isEqual(HashNumber hash, + uint32_t length) const { + return mHashAndLength == uint64FromHashAndLength(hash, length); + } + + // This function is used at compile-time to verify and that we pack and unpack + // hash and length values consistently. + static constexpr bool staticChecks() { + std::array<HashNumber, 5> hashes{0x00000000, 0xffffffff, 0xf0f0f0f0, + 0x0f0f0f0f, 0x73737373}; + std::array<uint32_t, 6> lengths{0, 1, 2, 3, 11, 56}; + + for (const HashNumber hash : hashes) { + for (const uint32_t length : lengths) { + const uint64_t lengthAndHash = uint64FromHashAndLength(hash, length); + if (hashFromUint64(lengthAndHash) != hash) { + return false; + } + if (lengthFromUint64(lengthAndHash) != length) { + return false; + } + } + } + + return true; + } + + static constexpr MOZ_ALWAYS_INLINE uint64_t unsetValue() { + // This needs to be a combination of hash and length that would never occur + // together. There is only one string of length zero, and its hash is zero, + // so the hash here can be anything except zero. + return uint64FromHashAndLength(0xffffffff, 0); + } + + private: + uint64_t mHashAndLength; + + static constexpr MOZ_ALWAYS_INLINE uint64_t + uint64FromHashAndLength(HashNumber hash, uint32_t length) { + return (static_cast<uint64_t>(length) << 32) | hash; + } + + static constexpr MOZ_ALWAYS_INLINE uint32_t + lengthFromUint64(uint64_t hashAndLength) { + return static_cast<uint32_t>(hashAndLength >> 32); + } + + static constexpr MOZ_ALWAYS_INLINE HashNumber + hashFromUint64(uint64_t hashAndLength) { + return hashAndLength & 0xffffffff; + } +}; + +static_assert(HashAndLength::staticChecks()); + +class AtomCacheHashTable { + public: + MOZ_ALWAYS_INLINE AtomCacheHashTable() { clear(); } + + MOZ_ALWAYS_INLINE void clear() { + mEntries.fill({HashAndLength{HashAndLength::unsetValue()}, nullptr}); + } + + static MOZ_ALWAYS_INLINE constexpr uint32_t computeIndexFromHash( + const HashNumber hash) { + // Simply use the low bits of the hash value as the cache index. + return hash & (sSize - 1); + } + + MOZ_ALWAYS_INLINE JSAtom* lookupForAdd( + const AtomHasher::Lookup& lookup) const { + MOZ_ASSERT(lookup.atom == nullptr, "Lookup by atom is not supported"); + + const uint32_t index = computeIndexFromHash(lookup.hash); + + JSAtom* const atom = mEntries[index].mAtom; + + if (!mEntries[index].mHashAndLength.isEqual(lookup.hash, lookup.length)) { + return nullptr; + } + + // This is annotated with MOZ_UNLIKELY because it virtually never happens + // that, after matching the hash and the length, the string isn't a match. + if (MOZ_UNLIKELY(!lookup.StringsMatch(*atom))) { + return nullptr; + } + + return atom; + } + + MOZ_ALWAYS_INLINE void add(const HashNumber hash, JSAtom* atom) { + const uint32_t index = computeIndexFromHash(hash); + + mEntries[index].set(hash, atom->length(), atom); + } + + private: + struct Entry { + MOZ_ALWAYS_INLINE void set(const HashNumber hash, const uint32_t length, + JSAtom* const atom) { + mHashAndLength.set(hash, length); + mAtom = atom; + } + + // Hash and length are also available, from JSAtom and JSString + // respectively, but are cached here to avoid likely cache misses in the + // frequent case of a missed lookup. + HashAndLength mHashAndLength; + // No read barrier is required here because the table is cleared at the + // start of GC. + JSAtom* mAtom; + }; + + // This value was picked empirically based on performance testing using SP2 + // and SP3. 4k was better than 2k but 8k was not much better than 4k. + static constexpr uint32_t sSize = 4 * 1024; + static_assert(mozilla::IsPowerOfTwo(sSize)); + std::array<Entry, sSize> mEntries; +}; + } // namespace js namespace JS { @@ -281,7 +425,7 @@ class Zone : public js::ZoneAllocator, public js::gc::GraphNodeBase<JS::Zone> { js::MainThreadOrGCTaskData<js::SparseBitmap> markedAtoms_; // Set of atoms recently used by this Zone. Purged on GC. - js::MainThreadOrGCTaskData<js::AtomSet> atomCache_; + js::MainThreadOrGCTaskData<js::UniquePtr<js::AtomCacheHashTable>> atomCache_; // Cache storing allocated external strings. Purged on GC. js::MainThreadOrGCTaskData<js::ExternalStringCache> externalStringCache_; @@ -613,7 +757,16 @@ class Zone : public js::ZoneAllocator, public js::gc::GraphNodeBase<JS::Zone> { js::SparseBitmap& markedAtoms() { return markedAtoms_.ref(); } - js::AtomSet& atomCache() { return atomCache_.ref(); } + // The atom cache is "allocate-on-demand". This function can return nullptr if + // the allocation failed. + js::AtomCacheHashTable* atomCache() { + if (atomCache_.ref()) { + return atomCache_.ref().get(); + } + + atomCache_ = js::MakeUnique<js::AtomCacheHashTable>(); + return atomCache_.ref().get(); + } void purgeAtomCache(); @@ -677,18 +830,7 @@ class Zone : public js::ZoneAllocator, public js::gc::GraphNodeBase<JS::Zone> { #endif // Support for invalidating fuses - - // A dependent script set pairs a fuse with a set of scripts which depend - // on said fuse; this is a vector of script sets because the expectation for - // now is that the number of runtime wide invalidating fuses will be small. - // This will need to be revisited (convert to HashMap?) should that no - // longer be the case - // - // Note: This isn't traced through the zone, but rather through the use - // of JS::WeakCache. - js::Vector<js::DependentScriptSet, 1, js::SystemAllocPolicy> fuseDependencies; - js::DependentScriptSet* getOrCreateDependentScriptSet( - JSContext* cx, js::InvalidatingFuse* fuse); + js::DependentScriptGroup fuseDependencies; private: js::jit::JitZone* createJitZone(JSContext* cx); diff --git a/js/src/jit-test/lib/pretenure.js b/js/src/jit-test/lib/pretenure.js index 214f1d44d1..c1184fefc4 100644 --- a/js/src/jit-test/lib/pretenure.js +++ b/js/src/jit-test/lib/pretenure.js @@ -22,8 +22,12 @@ function setupPretenureTest() { gczeal(0); // Restrict nursery size so we can fill it quicker, and ensure it is resized. - gcparam("minNurseryBytes", 1024 * 1024); - gcparam("maxNurseryBytes", 1024 * 1024); + let size = 1024 * 1024; + if (gcparam("semispaceNurseryEnabled")) { + size *= 2; + } + gcparam("minNurseryBytes", size); + gcparam("maxNurseryBytes", size); // Limit allocation threshold so we trigger major GCs sooner. gcparam("allocationThreshold", 1 /* MB */); @@ -67,8 +71,14 @@ function allocateArrays(count, longLived) { } function gcCounts() { - return { minor: gcparam("minorGCNumber"), - major: gcparam("majorGCNumber") }; + let major = gcparam("majorGCNumber") + let minor = gcparam("minorGCNumber"); + + // Only report minor collections that didn't happen as part of a major GC. + assertEq(minor >= major, true); + minor -= major; + + return { minor, major }; } function runTestAndCountCollections(thunk) { diff --git a/js/src/jit-test/tests/arrays/sort-trampoline.js b/js/src/jit-test/tests/arrays/sort-trampoline.js new file mode 100644 index 0000000000..b81d4628b0 --- /dev/null +++ b/js/src/jit-test/tests/arrays/sort-trampoline.js @@ -0,0 +1,153 @@ +function testGC() { + var arr = [{n: 1}, {n: 3}, {n: 0}, {n: 5}]; + for (var i = 0; i < 20; i++) { + arr.sort((x, y) => { + if (i === 17) { + gc(); + } + return x.n - y.n; + }); + } + assertEq(arr.map(x => x.n).join(""), "0135"); +} +testGC(); + +function testException() { + var arr = [{n: 1}, {n: 3}, {n: 0}, {n: 5}]; + var ex; + try { + for (var i = 0; i < 20; i++) { + arr.sort((x, y) => { + if (i === 17) { + throw "fit"; + } + return x.n - y.n; + }); + } + } catch (e) { + ex = e; + } + assertEq(ex, "fit"); + assertEq(i, 17); +} +testException(); + +function testRectifier() { + var arr = [{n: 1}, {n: 3}, {n: 0}, {n: 5}]; + for (var i = 0; i < 20; i++) { + arr.sort(function(x, y, a) { + assertEq(arguments.length, 2); + assertEq(a, undefined); + return y.n - x.n; + }); + } + assertEq(arr.map(x => x.n).join(""), "5310"); +} +testRectifier(); + +function testClassConstructor() { + var normal = (x, y) => x.n - y.n; + var dummy = {}; + var ctor = (class { constructor(x, y) { + assertEq(x, dummy); + }}); + // Warm up the constructor. + for (var i = 0; i < 20; i++) { + new ctor(dummy, dummy); + } + for (var i = 0; i < 20; i++) { + var arr = [{n: 1}, {n: 3}, {n: 0}, {n: 5}]; + var ex; + try { + arr.sort(i < 17 ? normal : ctor); + } catch (e) { + ex = e; + } + assertEq(ex instanceof TypeError, i >= 17); + assertEq(arr.map(x => x.n).join(""), i >= 17 ? "1305" : "0135"); + } +} +testClassConstructor(); + +function testSwitchRealms() { + var arr = [{n: 1}, {n: 3}, {n: 0}, {n: 5}]; + var g = newGlobal({sameCompartmentAs: this}); + g.foo = 123; + var comp = g.evaluate(`((x, y) => { + assertEq(foo, 123); + return x.n - y.n; + })`); + for (var i = 0; i < 20; i++) { + arr.sort(comp); + } + assertEq(arr.map(x => x.n).join(""), "0135"); +} +testSwitchRealms(); + +function testCrossCompartment() { + var arr = [{n: 1}, {n: 3}, {n: 0}, {n: 5}]; + var g = newGlobal({newCompartment: true}); + var wrapper = g.evaluate(`((x, y) => { + return x.n - y.n; + })`); + for (var i = 0; i < 20; i++) { + arr.sort(wrapper); + } + assertEq(arr.map(x => x.n).join(""), "0135"); +} +testCrossCompartment(); + +function testBound() { + var arr = [{n: 1}, {n: 3}, {n: 0}, {n: 5}]; + var fun = (function(a, x, y) { + "use strict"; + assertEq(this, null); + assertEq(a, 1); + return x.n - y.n; + }).bind(null, 1); + for (var i = 0; i < 20; i++) { + arr.sort(fun); + } + assertEq(arr.map(x => x.n).join(""), "0135"); +} +testBound(); + +function testExtraArgs() { + var arr = [{n: 1}, {n: 3}, {n: 0}, {n: 5}]; + var cmp = (x, y) => x.n - y.n; + for (var i = 0; i < 20; i++) { + arr.sort(cmp, cmp, cmp, cmp, cmp, cmp, cmp); + } + assertEq(arr.map(x => x.n).join(""), "0135"); +} +testExtraArgs(); + +function testBailout() { + var arr = [{n: 1}, {n: 3}, {n: 0}, {n: 5}]; + for (var i = 0; i < 110; i++) { + arr.sort(function(x, y) { + if (i === 108) { + bailout(); + } + return x.n - y.n; + }); + } + assertEq(arr.map(x => x.n).join(""), "0135"); +} +testBailout(); + +function testExceptionHandlerSwitchRealm() { + var g = newGlobal({sameCompartmentAs: this}); + for (var i = 0; i < 25; i++) { + var ex = null; + try { + g.Array.prototype.toSorted.call([2, 3], () => { + throw "fit"; + }); + } catch (e) { + ex = e; + } + assertEq(ex, "fit"); + } +} +testExceptionHandlerSwitchRealm(); diff --git a/js/src/jit-test/tests/basic/bug1875795.js b/js/src/jit-test/tests/basic/bug1875795.js new file mode 100644 index 0000000000..1a5b54acfe --- /dev/null +++ b/js/src/jit-test/tests/basic/bug1875795.js @@ -0,0 +1,7 @@ +// |jit-test| --fast-warmup; --no-threads; skip-if: !('oomTest' in this) +oomTest(function() { + var o = {}; + for (var p in this) { + o[p] = 1; + } +}); diff --git a/js/src/jit-test/tests/basic/bug1888746.js b/js/src/jit-test/tests/basic/bug1888746.js new file mode 100644 index 0000000000..8e6d0cd0b9 --- /dev/null +++ b/js/src/jit-test/tests/basic/bug1888746.js @@ -0,0 +1,12 @@ +function comparator(x, y) { + saveStack(); + return {valueOf: function() { + saveStack(); + return x - y; + }}; +} +for (let i = 0; i < 20; i++) { + let arr = [3, 1, 2]; + arr.sort(comparator); + assertEq(arr.toString(), "1,2,3"); +} diff --git a/js/src/jit-test/tests/basic/bug1890200.js b/js/src/jit-test/tests/basic/bug1890200.js new file mode 100644 index 0000000000..caa97fcece --- /dev/null +++ b/js/src/jit-test/tests/basic/bug1890200.js @@ -0,0 +1,12 @@ +let triggerGC = false; +let proxy = new Proxy({}, {get: function(target, key) { + if (key === "sameCompartmentAs" || key === "sameZoneAs") { + triggerGC = true; + return newGlobal({newCompartment: true}); + } + if (triggerGC) { + gc(); + triggerGC = false; + } +}}); +newGlobal(proxy); diff --git a/js/src/jit-test/tests/cacheir/bug1888346.js b/js/src/jit-test/tests/cacheir/bug1888346.js new file mode 100644 index 0000000000..8e63d86089 --- /dev/null +++ b/js/src/jit-test/tests/cacheir/bug1888346.js @@ -0,0 +1,8 @@ +setJitCompilerOption("ion.frequent-bailout-threshold", 1); +for (let i = 0; i < 49; i++) { + (function () { + let x = new (function () {})(); + Object.defineProperty(x, "z", {}); + x.z; + })(); +} diff --git a/js/src/jit-test/tests/collections/bug-1884927.js b/js/src/jit-test/tests/collections/bug-1884927.js new file mode 100644 index 0000000000..263d9df8a0 --- /dev/null +++ b/js/src/jit-test/tests/collections/bug-1884927.js @@ -0,0 +1,10 @@ +// |jit-test| --enable-symbols-as-weakmap-keys; skip-if: getBuildConfiguration("release_or_beta") +for (x=0; x<10000; ++x) { + try { + m13 = new WeakMap; + sym = Symbol(); + m13.set(sym, new Debugger); + startgc(1, ); + } catch (exc) {} +} + diff --git a/js/src/jit-test/tests/collections/bug-1885775.js b/js/src/jit-test/tests/collections/bug-1885775.js new file mode 100644 index 0000000000..bc14c6d58b --- /dev/null +++ b/js/src/jit-test/tests/collections/bug-1885775.js @@ -0,0 +1,12 @@ +// |jit-test| --enable-symbols-as-weakmap-keys; skip-if: getBuildConfiguration("release_or_beta") +var code = ` +var m58 = new WeakMap; +var sym = Symbol(); +m58.set(sym, ({ entry16: 0, length: 1 })); +function testCompacting() { + gcslice(50000); +} +testCompacting(2, 100000, 50000); +`; +for (x = 0; x < 10000; ++x) + evaluate(code); diff --git a/js/src/jit-test/tests/collections/bug-1887939-1.js b/js/src/jit-test/tests/collections/bug-1887939-1.js new file mode 100644 index 0000000000..292c44d492 --- /dev/null +++ b/js/src/jit-test/tests/collections/bug-1887939-1.js @@ -0,0 +1,7 @@ +var map = new WeakMap(); +var sym = Symbol(); +try { + map.set(sym, 1); +} catch (e) { + assertEq(!!e.message.match(/an unregistered symbol/), false); +} diff --git a/js/src/jit-test/tests/collections/bug-1887939-2.js b/js/src/jit-test/tests/collections/bug-1887939-2.js new file mode 100644 index 0000000000..2ec4e4c585 --- /dev/null +++ b/js/src/jit-test/tests/collections/bug-1887939-2.js @@ -0,0 +1,7 @@ +// |jit-test| --enable-symbols-as-weakmap-keys; skip-if: getBuildConfiguration("release_or_beta") +var map = new WeakMap(); +try { + map.set(1, 1); +} catch (e) { + assertEq(!!e.message.match(/an unregistered symbol/), true); +} diff --git a/js/src/jit-test/tests/debug/Debugger-onNativeCall-03.js b/js/src/jit-test/tests/debug/Debugger-onNativeCall-03.js index 7e9c0b280a..27ab5b2b23 100644 --- a/js/src/jit-test/tests/debug/Debugger-onNativeCall-03.js +++ b/js/src/jit-test/tests/debug/Debugger-onNativeCall-03.js @@ -1,4 +1,5 @@ -// Test onNativeCall's behavior when used with self-hosted functions. +// Test onNativeCall's behavior when used with self-hosted functions +// and trampoline natives. load(libdir + 'eqArrayHelper.js'); @@ -18,13 +19,22 @@ dbg.onNativeCall = f => { gdbg.executeInGlobal(` var x = [1,3,2]; + x.forEach((a) => {print(a)}); x.sort((a, b) => {print(a)}); + x.sort(print); `); assertEqArray(rv, [ - "EnterFrame", "sort", - "ArraySortCompare/<", + "EnterFrame", "forEach", "EnterFrame", "print", - "ArraySortCompare/<", "EnterFrame", "print", + "EnterFrame", "print", + + "sort", + "EnterFrame","print", + "EnterFrame","print", + + "sort", + "print", + "print" ]); diff --git a/js/src/jit-test/tests/debug/Environment-methods-toPrimitive.js b/js/src/jit-test/tests/debug/Environment-methods-toPrimitive.js new file mode 100644 index 0000000000..106728901d --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-methods-toPrimitive.js @@ -0,0 +1,21 @@ +// removeDebuggee can be called through ToPrimitive while converting the argument +// passed to Debugger.Environment.{find,getVariable,setVariable} to string. + +var g = newGlobal({newCompartment: true}); +g.eval("function f() { debugger; }"); +var dbg = new Debugger(); +var oddball = {[Symbol.toPrimitive]: () => dbg.removeDebuggee(g)}; + +for (var method of ["find", "getVariable", "setVariable"]) { + dbg.addDebuggee(g); + dbg.onDebuggerStatement = frame => { + var ex; + try { + frame.environment[method](oddball, oddball); + } catch (e) { + ex = e; + } + assertEq(ex.message, "Debugger.Environment is not a debuggee environment"); + }; + g.f(); +} diff --git a/js/src/jit-test/tests/debug/Frame-onStep-21.js b/js/src/jit-test/tests/debug/Frame-onStep-21.js new file mode 100644 index 0000000000..7bea2e3a95 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-21.js @@ -0,0 +1,19 @@ +// |jit-test| error: too much recursion + +// Generator closed due to over-recursion shouldn't cause crash around onStep. + +async function* foo() { + const g = this.newGlobal({sameZoneAs: this}); + g.Debugger(this).getNewestFrame().onStep = g.evaluate(`(function() {})`); + return {}; +} +function f() { + try { + f.apply(undefined, f); + } catch { + drainJobQueue(); + foo().next(); + } +} +foo().next(); +f(); diff --git a/js/src/jit-test/tests/debug/private-methods-eval-in-frame.js b/js/src/jit-test/tests/debug/private-methods-eval-in-frame.js index 5122cfa56b..318a36f614 100644 --- a/js/src/jit-test/tests/debug/private-methods-eval-in-frame.js +++ b/js/src/jit-test/tests/debug/private-methods-eval-in-frame.js @@ -150,13 +150,6 @@ if ('dis' in this) { assertEq(b.ef(`var x = () => { return this.#priv(); }; x()`), 12); assertEq(b.ef(`function x(o) { function y(o) { return o.#priv(); }; return y(o); } x(this)`), 12); -assertEq(b.ef("B.#smethod()"), 14) -assertEq(b.ef("B.#unusedmethod()"), 19); -assertEq(b.ef("B.#unusedgetter"), 10); - -b.ef("B.#unusedsetter = 19"); -assertEq(B.setter, 19); - assertEq(B.f(), 14); assertEq(B.sef(`this.#smethod()`), 14); assertEq(B.sLayerEf(`this.#smethod()`), 14); @@ -215,4 +208,4 @@ var x = () => { })(); }; x() -`), 12);
\ No newline at end of file +`), 12); diff --git a/js/src/jit-test/tests/errors/bug-1886940-2.js b/js/src/jit-test/tests/errors/bug-1886940-2.js new file mode 100644 index 0000000000..654071be04 --- /dev/null +++ b/js/src/jit-test/tests/errors/bug-1886940-2.js @@ -0,0 +1,6 @@ +oomTest(function () { + (function () { + var x = [disassemble, new Int8Array(2 ** 8 + 1)]; + x.shift().apply([], x); + })(); +}); diff --git a/js/src/jit-test/tests/errors/bug-1886940.js b/js/src/jit-test/tests/errors/bug-1886940.js new file mode 100644 index 0000000000..f8d3020d8c --- /dev/null +++ b/js/src/jit-test/tests/errors/bug-1886940.js @@ -0,0 +1,2 @@ +// |jit-test| error: RangeError +[].with(Symbol.hasInstance); diff --git a/js/src/jit-test/tests/fuses/optimized-getiterator-invalidation.js b/js/src/jit-test/tests/fuses/optimized-getiterator-invalidation.js new file mode 100644 index 0000000000..6505f8d023 --- /dev/null +++ b/js/src/jit-test/tests/fuses/optimized-getiterator-invalidation.js @@ -0,0 +1,37 @@ + +const ITERS = 1000; + +// A function which when warp compiled should use +// OptimizedGetIterator elision, and rely on invalidation +function f(x) { + let sum = 0; + for (let i = 0; i < ITERS; i++) { + const [a, b, c] = x + sum = a + b + c; + } + return sum +} + +// Run the function f 1000 times to warp compile it. Use 4 elements here to ensure +// the return property of the ArrayIteratorPrototype is called. +let arr = [1, 2, 3, 4]; +for (let i = 0; i < 1000; i++) { + f(arr); +} + +// Initialize the globally scoped counter +let counter = 0; +const ArrayIteratorPrototype = Object.getPrototypeOf([][Symbol.iterator]()); + +// Setting the return property should invalidate the warp script here. +ArrayIteratorPrototype.return = function () { + counter++; + return { done: true }; +}; + + +// Call f one more time +f(arr); + +// Use assertEq to check the value of counter. +assertEq(counter, ITERS); diff --git a/js/src/jit-test/tests/gc/alllcation-metadata-builder-over-recursion.js b/js/src/jit-test/tests/gc/alllcation-metadata-builder-over-recursion.js new file mode 100644 index 0000000000..67093026b4 --- /dev/null +++ b/js/src/jit-test/tests/gc/alllcation-metadata-builder-over-recursion.js @@ -0,0 +1,22 @@ +// |jit-test| allow-unhandlable-oom + +// Over-recursion should suppress alloation metadata builder, to avoid another +// over-recursion while generating an error object for the first over-recursion. +// +// This test should catch the error for the "load" testing function's arguments, +// or crash with unhandlable OOM inside allocation metadata builder. + +const g = newGlobal(); +g.enableShellAllocationMetadataBuilder(); +function run() { + const g_load = g.load; + g_load.toString = run; + return g_load(g_load); +} +let caught = false; +try { + run(); +} catch (e) { + caught = true; +} +assertEq(caught, true); diff --git a/js/src/jit-test/tests/gc/bug-1568740.js b/js/src/jit-test/tests/gc/bug-1568740.js index 6cc003cb94..5c311b855d 100644 --- a/js/src/jit-test/tests/gc/bug-1568740.js +++ b/js/src/jit-test/tests/gc/bug-1568740.js @@ -1,11 +1,11 @@ gczeal(0); +gcparam("semispaceNurseryEnabled", 0); function setAndTest(param, value) { gcparam(param, value); assertEq(gcparam(param), value); } - // Set a large nursery size. setAndTest("maxNurseryBytes", 1024*1024); setAndTest("minNurseryBytes", 1024*1024); diff --git a/js/src/jit-test/tests/gc/bug-1569840.js b/js/src/jit-test/tests/gc/bug-1569840.js index 70d28add73..45df339405 100644 --- a/js/src/jit-test/tests/gc/bug-1569840.js +++ b/js/src/jit-test/tests/gc/bug-1569840.js @@ -1,5 +1,5 @@ - gczeal(0); +gcparam("semispaceNurseryEnabled", 0); gcparam("maxNurseryBytes", 1024*1024); gcparam("minNurseryBytes", 1024*1024); diff --git a/js/src/jit-test/tests/gc/bug-1885819-2.js b/js/src/jit-test/tests/gc/bug-1885819-2.js new file mode 100644 index 0000000000..a87e4c701a --- /dev/null +++ b/js/src/jit-test/tests/gc/bug-1885819-2.js @@ -0,0 +1,12 @@ +let g = newGlobal(); +function f() { + var o = {}; + o["prop" + Date.now()] = 1; + gc(); + schedulezone("atoms"); + schedulezone(g); + gc("zone"); + let [x] = [0]; +} +f(); +oomTest(f); diff --git a/js/src/jit-test/tests/gc/bug-1885819.js b/js/src/jit-test/tests/gc/bug-1885819.js new file mode 100644 index 0000000000..8341c3ff52 --- /dev/null +++ b/js/src/jit-test/tests/gc/bug-1885819.js @@ -0,0 +1,10 @@ +function f() { + var o = {}; + o["prop" + Date.now()] = 1; + gc(); + schedulezone("atoms"); + gc("zone"); + let [x] = [0]; +} +f(); +oomTest(f); diff --git a/js/src/jit-test/tests/gc/bug-1886466.js b/js/src/jit-test/tests/gc/bug-1886466.js new file mode 100644 index 0000000000..4347ea3e6b --- /dev/null +++ b/js/src/jit-test/tests/gc/bug-1886466.js @@ -0,0 +1,5 @@ +gczeal(7, 6) +a = new WeakSet +for (let i = 0; i < 200000; i++) { + a.add({}) +} diff --git a/js/src/jit-test/tests/gc/bug-1888717.js b/js/src/jit-test/tests/gc/bug-1888717.js new file mode 100644 index 0000000000..7e54543994 --- /dev/null +++ b/js/src/jit-test/tests/gc/bug-1888717.js @@ -0,0 +1,3 @@ +// |jit-test| --no-ggc +gcparam("semispaceNurseryEnabled", 1); +let o = {}; diff --git a/js/src/jit-test/tests/gc/dedupe-03.js b/js/src/jit-test/tests/gc/dedupe-03.js new file mode 100644 index 0000000000..4e9b4c1bbc --- /dev/null +++ b/js/src/jit-test/tests/gc/dedupe-03.js @@ -0,0 +1,66 @@ +// |jit-test| skip-if: !hasFunction.stringRepresentation + +// Test handling of tenured dependent strings pointing to nursery base strings. + +gczeal(0); + +function makeExtensibleStrFrom(str) { + var left = str.substr(0, str.length/2); + var right = str.substr(str.length/2, str.length); + var ropeStr = left + right; + return ensureLinearString(ropeStr); +} + +function repr(s) { + return JSON.parse(stringRepresentation(s)); +} + +function dependsOn(s1, s2) { + const rep1 = JSON.parse(stringRepresentation(s1)); + const rep2 = JSON.parse(stringRepresentation(s2)); + return rep1.base && rep1.base.address == rep2.address; +} + +// Make a string to deduplicate to. +var original = makeExtensibleStrFrom('abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm'); + +// Construct T1 -> Nbase. +var Nbase = makeExtensibleStrFrom('abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm'); +var T1 = newDependentString(Nbase, 0, 60, { tenured: true }); + +// Get prevented from creating T2 -> T1 -> Nbase +// (will be T2 -> Nbase instead to avoid dependency chains). +var T2 = newDependentString(T1, 30, { tenured: true }); + +assertEq(dependsOn(T2, Nbase), "expect: T2 -> base"); + +// Construct T1 -> Ndep1 (was Nbase) -> Nbase2. +var Nbase2 = newRope(Nbase, "ABC"); +ensureLinearString(Nbase2); +var Ndep1 = Nbase; + +assertEq(dependsOn(T1, Ndep1), "expect: T1 -> Ndep1"); +assertEq(dependsOn(Ndep1, Nbase2), "expect: Ndep1 -> Nbase2"); + +// Fail to construct T3 -> Tbase3 -> Nbase4. It will refuse because T3 would be using +// chars from Nbase4 that can't be updated since T3 is not in the store buffer. Instead, +// it will allocate a new buffer for the rope root, leaving Tbase3 alone and keeping +// T3 -> Tbase3. +var Tbase3 = makeExtensibleStrFrom('abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklm'); +minorgc(); +var T3 = newDependentString(Tbase3, 0, 30, { tenured: true }); +var Nbase4 = newRope(Tbase3, "DEF"); +ensureLinearString(Nbase4); +assertEq(repr(Tbase3).isTenured, true, "Tbase3 is tenured"); +assertEq(repr(Tbase3).flags.includes("EXTENSIBLE"), true, "Tbase3 is extensible"); +assertEq(repr(Nbase4).flags.includes("DEPENDENT_BIT"), false, "expect: Nbase4 is not a dependent string") +assertEq(repr(T3).flags.includes("DEPENDENT_BIT"), true, "expect: T3 is a dependent string") +assertEq(dependsOn(T3, Tbase3), "expect: T3 -> Tbase3"); + +function bug1879918() { + const s = JSON.parse('["abcdefabcdefabcdefabcdefabcdefabcdefabcdef"]')[0]; + const dep = newDependentString(s, 1, { tenured: true }); + minorgc(); + assertEq(dep, "bcdefabcdefabcdefabcdefabcdefabcdefabcdef"); +} +bug1879918(); diff --git a/js/src/jit-test/tests/gc/deduplicateTenuringStrings.js b/js/src/jit-test/tests/gc/deduplicateTenuringStrings.js index 1b8259cc15..de2fb0c028 100644 --- a/js/src/jit-test/tests/gc/deduplicateTenuringStrings.js +++ b/js/src/jit-test/tests/gc/deduplicateTenuringStrings.js @@ -13,6 +13,7 @@ // We require predictable GC timing to make sure the correct // strings are tenured together. gczeal(0); +gcparam('semispaceNurseryEnabled', 0); var helperCode = ` function makeInlineStr(isLatin1) { diff --git a/js/src/jit-test/tests/gc/gcparam.js b/js/src/jit-test/tests/gc/gcparam.js index 05e0359088..c57b400642 100644 --- a/js/src/jit-test/tests/gc/gcparam.js +++ b/js/src/jit-test/tests/gc/gcparam.js @@ -60,3 +60,4 @@ testChangeParam("mallocThresholdBase"); testChangeParam("urgentThreshold"); testChangeParam("helperThreadRatio"); testChangeParam("maxHelperThreads"); +testChangeParam("semispaceNurseryEnabled"); diff --git a/js/src/jit-test/tests/gc/pretenuring.js b/js/src/jit-test/tests/gc/pretenuring.js index 6f20706e9b..30156b0e98 100644 --- a/js/src/jit-test/tests/gc/pretenuring.js +++ b/js/src/jit-test/tests/gc/pretenuring.js @@ -1,6 +1,7 @@ // Test nursery string allocation and pretenuring. gczeal(0); +gcparam("semispaceNurseryEnabled", 0); gcparam("minNurseryBytes", 4096 * 1024); gcparam("maxNurseryBytes", 4096 * 1024); diff --git a/js/src/jit-test/tests/heap-analysis/byteSize-of-string.js b/js/src/jit-test/tests/heap-analysis/byteSize-of-string.js index aaac0c4f1f..b4cfdffb04 100644 --- a/js/src/jit-test/tests/heap-analysis/byteSize-of-string.js +++ b/js/src/jit-test/tests/heap-analysis/byteSize-of-string.js @@ -12,6 +12,7 @@ // stable. gczeal(0); // Need to control when tenuring happens +gcparam('semispaceNurseryEnabled', 0); // Hack to skip this test if strings are not allocated in the nursery. { @@ -75,16 +76,20 @@ function tByteSize(str) { // JSExternalString - limited by MaxStringLength - E // JSThinInlineString 8 4 16 8 T // JSFatInlineString 24 12 24 12 F +// ThinInlineAtom 12 6 20 10 T +// FatInlineAtom 20 10 20 10 F // JSExtensibleString - limited by MaxStringLength - X // Notes: // - labels are suffixed with A for atoms and N for non-atoms -// - atoms are 8 bytes larger than non-atoms, to store the atom's hash code. +// - atoms store a 4 byte hash code, and some add to the size to adjust // - Nursery-allocated strings require a header that stores the zone. // Expected sizes based on type of string const m32 = (getBuildConfiguration("pointer-byte-size") == 4); -const TA = m32 ? 24 : 32; // ThinInlineString atom, includes a hash value +const TA = m32 ? 24 : 32; // ThinInlineAtom (includes a hash value) +const FA = m32 ? 32 : 32; // FatInlineAtom (includes a hash value) +const NA = m32 ? 24 : 32; // NormalAtom const TN = m32 ? 16 : 24; // ThinInlineString const FN = m32 ? 32 : 32; // FatInlineString const XN = m32 ? 16 : 24; // ExtensibleString, has additional storage buffer @@ -95,8 +100,8 @@ const EN = m32 ? 16 : 24; // ExternalString // A function that pads out a tenured size to the nursery size. We store a zone // pointer in the nursery just before the string (4 bytes on 32-bit, 8 bytes on // 64-bit), and the string struct itself must be 8-byte aligned (resulting in -// +4 bytes on 32-bit, +0 bytes on 64-bit). The end result? Nursery strings are -// 8 bytes larger. +// +4 bytes on 32-bit, +0 bytes on 64-bit). The end result is that nursery +// strings are 8 bytes larger. const Nursery = m32 ? s => s + 4 + 4 : s => s + 8 + 0; // Latin-1 @@ -130,6 +135,23 @@ assertEq(nByteSize("123456789.123456789.123456789.1"), s(Nursery( assertEq(nByteSize("123456789.123456789.123456789.12"), s(Nursery(XN)+32,Nursery(XN)+32)); assertEq(nByteSize("123456789.123456789.123456789.123"), s(Nursery(XN)+64,Nursery(XN)+64)); +function Atom(s) { return Object.keys({ [s]: true })[0]; } +assertEq(byteSize(Atom("1234567")), s(TA, TA)); +assertEq(byteSize(Atom("12345678")), s(TA, FA)); +assertEq(byteSize(Atom("123456789.12")), s(TA, FA)); +assertEq(byteSize(Atom("123456789.123")), s(FA, FA)); +assertEq(byteSize(Atom("123456789.12345")), s(FA, FA)); +assertEq(byteSize(Atom("123456789.123456")), s(FA, FA)); +assertEq(byteSize(Atom("123456789.1234567")), s(FA, FA)); +assertEq(byteSize(Atom("123456789.123456789.")), s(FA, FA)); +assertEq(byteSize(Atom("123456789.123456789.1")), s(NA+32, NA+32)); +assertEq(byteSize(Atom("123456789.123456789.123")), s(NA+32, NA+32)); +assertEq(byteSize(Atom("123456789.123456789.1234")), s(NA+32, NA+32)); +assertEq(byteSize(Atom("123456789.123456789.12345")), s(NA+32, NA+32)); +assertEq(byteSize(Atom("123456789.123456789.123456789.1")), s(NA+32, NA+32)); +assertEq(byteSize(Atom("123456789.123456789.123456789.12")), s(NA+32, NA+32)); +assertEq(byteSize(Atom("123456789.123456789.123456789.123")), s(NA+48, NA+48)); + // Inline char16_t atoms. // "Impassionate gods have never seen the red that is the Tatsuta River." // - Ariwara no Narihira @@ -183,20 +205,43 @@ assertEq(byteSize(rope8), s(Nurser minorgc(); assertEq(byteSize(rope8), s(RN, RN)); var matches8 = rope8.match(/(de cuyo nombre no quiero acordarme)/); -assertEq(byteSize(rope8), s(XN + 65536, XN + 65536)); +assertEq(byteSize(rope8), s(XN + 64 * 1024, XN + 64 * 1024)); +var ext8 = rope8; // Stop calling it what it's not (though it'll change again soon.) // Test extensible strings. // // Appending another copy of the fragment should yield another rope. // -// Flatting that should turn the original rope into a dependent string, and +// Flattening that should turn the original rope into a dependent string, and // yield a new linear string, of the same size as the original. -rope8a = rope8 + fragment8; +var rope8a = ext8 + fragment8; assertEq(byteSize(rope8a), s(Nursery(RN), Nursery(RN))); rope8a.match(/x/, function() { assertEq(true, false); }); assertEq(byteSize(rope8a), s(Nursery(XN) + 65536, Nursery(XN) + 65536)); +assertEq(byteSize(ext8), s(DN, DN)); + +// Latin-1 dependent strings in the nursery. +assertEq(byteSize(ext8.substr(1000, 2000)), s(Nursery(DN), Nursery(DN))); +assertEq(byteSize(matches8[0]), s(Nursery(DN), Nursery(DN))); +assertEq(byteSize(matches8[1]), s(Nursery(DN), Nursery(DN))); + +// Tenure everything and do it again. +ext8 = copyString(ext8); +rope8a = ext8 + fragment8; +minorgc(); +assertEq(byteSize(rope8a), s(RN, RN)); +rope8a.match(/x/, function() { assertEq(true, false); }); +assertEq(byteSize(rope8a), s(XN + 65536, XN + 65536)); assertEq(byteSize(rope8), s(RN, RN)); +// Latin-1 tenured dependent strings. +function tenure(s) { + minorgc(); + return s; +} +assertEq(byteSize(tenure(rope8.substr(1000, 2000))), s(DN, DN)); +assertEq(byteSize(matches8[0]), s(DN, DN)); +assertEq(byteSize(matches8[1]), s(DN, DN)); // A char16_t rope. This changes size when flattened. // "From the Heliconian Muses let us begin to sing" @@ -207,13 +252,11 @@ for (var i = 0; i < 10; i++) // 1024 repetitions rope16 = rope16 + rope16; assertEq(byteSize(rope16), s(Nursery(RN), Nursery(RN))); let matches16 = rope16.match(/(Ἑλικωνιάδων ἀρχώμεθ᾽)/); -assertEq(byteSize(rope16), s(Nursery(RN) + 131072, Nursery(RN) + 131072)); +assertEq(byteSize(rope16), s(Nursery(XN) + 128 * 1024, Nursery(XN) + 128 * 1024)); +var ext16 = rope16; -// Latin-1 and char16_t dependent strings. -assertEq(byteSize(rope8.substr(1000, 2000)), s(Nursery(DN), Nursery(DN))); -assertEq(byteSize(rope16.substr(1000, 2000)), s(Nursery(DN), Nursery(DN))); -assertEq(byteSize(matches8[0]), s(Nursery(DN), Nursery(DN))); -assertEq(byteSize(matches8[1]), s(Nursery(DN), Nursery(DN))); +// char16_t dependent strings in the nursery. +assertEq(byteSize(ext16.substr(1000, 2000)), s(Nursery(DN), Nursery(DN))); assertEq(byteSize(matches16[0]), s(Nursery(DN), Nursery(DN))); assertEq(byteSize(matches16[1]), s(Nursery(DN), Nursery(DN))); @@ -221,13 +264,23 @@ assertEq(byteSize(matches16[1]), s(Nurser // // Appending another copy of the fragment should yield another rope. // -// Flatting that should turn the original rope into a dependent string, and +// Flattening that should turn the original rope into a dependent string, and // yield a new linear string, of the some size as the original. -rope16a = rope16 + fragment16; +rope16a = ext16 + fragment16; assertEq(byteSize(rope16a), s(Nursery(RN), Nursery(RN))); rope16a.match(/x/, function() { assertEq(true, false); }); -assertEq(byteSize(rope16a), s(Nursery(XN) + 131072, Nursery(XN) + 131072)); -assertEq(byteSize(rope16), s(Nursery(XN), Nursery(XN))); +assertEq(byteSize(rope16a), s(Nursery(XN) + 128 * 1024, Nursery(XN) + 128 * 1024)); +assertEq(byteSize(ext16), s(Nursery(DN), Nursery(DN))); + +// Tenure everything and try again. This time it should steal the extensible +// characters and convert the root into an extensible string using them. +ext16 = copyString(ext16); +rope16a = ext16 + fragment16; +minorgc(); +assertEq(byteSize(rope16a), s(RN, RN)); +rope16a.match(/x/, function() { assertEq(true, false); }); +assertEq(byteSize(rope16a), s(XN + 128 * 1024, XN + 128 * 1024)); +assertEq(byteSize(ext16), s(RN, RN)); // Test external strings. // diff --git a/js/src/jit-test/tests/ion/apply-native-arguments-object.js b/js/src/jit-test/tests/ion/apply-native-arguments-object.js new file mode 100644 index 0000000000..e06a5e0965 --- /dev/null +++ b/js/src/jit-test/tests/ion/apply-native-arguments-object.js @@ -0,0 +1,46 @@ +load(libdir + "array-compare.js"); + +const xs = [ + // Zero arguments. + [], + + // Single argument. + [1], + + // Few arguments. Even number of arguments. + [1, 2], + + // Few arguments. Odd number of arguments. + [1, 2, 3], + + // Many arguments. Even number of arguments. + [1, 2, 3, 4, 5, 6, 7, 8, 9, 0], + + // Many arguments. Odd number of arguments. + [1, 2, 3, 4, 5, 6, 7, 8, 9], +]; + +function escape() { + with ({}) ; +} + +function f() { + // Let |arguments| escape to force the allocation of an arguments object. + escape(arguments); + + // FunApply to a native function with an arguments object. + return Array.apply(null, arguments); +} + +// Don't inline |f| into the top-level script. +with ({}) ; + +for (let i = 0; i < 400; ++i) { + let x = xs[i % xs.length]; + + // NB: Array(1) creates the array `[,]`. + let expected = x.length !== 1 ? x : [,]; + + let result = f.apply(null, x); + assertEq(arraysEqual(result, expected), true); +} diff --git a/js/src/jit-test/tests/ion/apply-native-arguments.js b/js/src/jit-test/tests/ion/apply-native-arguments.js new file mode 100644 index 0000000000..3d6729ca76 --- /dev/null +++ b/js/src/jit-test/tests/ion/apply-native-arguments.js @@ -0,0 +1,39 @@ +load(libdir + "array-compare.js"); + +const xs = [ + // Zero arguments. + [], + + // Single argument. + [1], + + // Few arguments. Even number of arguments. + [1, 2], + + // Few arguments. Odd number of arguments. + [1, 2, 3], + + // Many arguments. Even number of arguments. + [1, 2, 3, 4, 5, 6, 7, 8, 9, 0], + + // Many arguments. Odd number of arguments. + [1, 2, 3, 4, 5, 6, 7, 8, 9], +]; + +function f() { + // FunApply to a native function with frame arguments. + return Array.apply(null, arguments); +} + +// Don't inline |f| into the top-level script. +with ({}) ; + +for (let i = 0; i < 400; ++i) { + let x = xs[i % xs.length]; + + // NB: Array(1) creates the array `[,]`. + let expected = x.length !== 1 ? x : [,]; + + let result = f.apply(null, x); + assertEq(arraysEqual(result, expected), true); +} diff --git a/js/src/jit-test/tests/ion/apply-native-array.js b/js/src/jit-test/tests/ion/apply-native-array.js new file mode 100644 index 0000000000..0dfa2df947 --- /dev/null +++ b/js/src/jit-test/tests/ion/apply-native-array.js @@ -0,0 +1,39 @@ +load(libdir + "array-compare.js"); + +const xs = [ + // Zero arguments. + [], + + // Single argument. + [1], + + // Few arguments. Even number of arguments. + [1, 2], + + // Few arguments. Odd number of arguments. + [1, 2, 3], + + // Many arguments. Even number of arguments. + [1, 2, 3, 4, 5, 6, 7, 8, 9, 0], + + // Many arguments. Odd number of arguments. + [1, 2, 3, 4, 5, 6, 7, 8, 9], +]; + +function f(x) { + // FunApply to a native function with an array. + return Array.apply(null, x); +} + +// Don't inline |f| into the top-level script. +with ({}) ; + +for (let i = 0; i < 400; ++i) { + let x = xs[i % xs.length]; + + // NB: Array(1) creates the array `[,]`. + let expected = x.length !== 1 ? x : [,]; + + let result = f(x); + assertEq(arraysEqual(result, expected), true); +} diff --git a/js/src/jit-test/tests/ion/apply-native-spreadcall-arguments.js b/js/src/jit-test/tests/ion/apply-native-spreadcall-arguments.js new file mode 100644 index 0000000000..9f769e4a59 --- /dev/null +++ b/js/src/jit-test/tests/ion/apply-native-spreadcall-arguments.js @@ -0,0 +1,39 @@ +load(libdir + "array-compare.js"); + +const xs = [ + // Zero arguments. + [], + + // Single argument. + [1], + + // Few arguments. Even number of arguments. + [1, 2], + + // Few arguments. Odd number of arguments. + [1, 2, 3], + + // Many arguments. Even number of arguments. + [1, 2, 3, 4, 5, 6, 7, 8, 9, 0], + + // Many arguments. Odd number of arguments. + [1, 2, 3, 4, 5, 6, 7, 8, 9], +]; + +function f() { + // SpreadCall to a native function with frame arguments. + return Array(...arguments); +} + +// Don't inline |f| into the top-level script. +with ({}) ; + +for (let i = 0; i < 400; ++i) { + let x = xs[i % xs.length]; + + // NB: Array(1) creates the array `[,]`. + let expected = x.length !== 1 ? x : [,]; + + let result = f.apply(null, x); + assertEq(arraysEqual(result, expected), true); +} diff --git a/js/src/jit-test/tests/ion/apply-native-spreadcall-array.js b/js/src/jit-test/tests/ion/apply-native-spreadcall-array.js new file mode 100644 index 0000000000..24e5621484 --- /dev/null +++ b/js/src/jit-test/tests/ion/apply-native-spreadcall-array.js @@ -0,0 +1,39 @@ +load(libdir + "array-compare.js"); + +const xs = [ + // Zero arguments. + [], + + // Single argument. + [1], + + // Few arguments. Even number of arguments. + [1, 2], + + // Few arguments. Odd number of arguments. + [1, 2, 3], + + // Many arguments. Even number of arguments. + [1, 2, 3, 4, 5, 6, 7, 8, 9, 0], + + // Many arguments. Odd number of arguments. + [1, 2, 3, 4, 5, 6, 7, 8, 9], +]; + +function f(x) { + // SpreadCall to a native function with an array. + return Array(...x); +} + +// Don't inline |f| into the top-level script. +with ({}) ; + +for (let i = 0; i < 400; ++i) { + let x = xs[i % xs.length]; + + // NB: Array(1) creates the array `[,]`. + let expected = x.length !== 1 ? x : [,]; + + let result = f(x); + assertEq(arraysEqual(result, expected), true); +} diff --git a/js/src/jit-test/tests/ion/apply-native-spreadcall-rest.js b/js/src/jit-test/tests/ion/apply-native-spreadcall-rest.js new file mode 100644 index 0000000000..ba7038244d --- /dev/null +++ b/js/src/jit-test/tests/ion/apply-native-spreadcall-rest.js @@ -0,0 +1,39 @@ +load(libdir + "array-compare.js"); + +const xs = [ + // Zero arguments. + [], + + // Single argument. + [1], + + // Few arguments. Even number of arguments. + [1, 2], + + // Few arguments. Odd number of arguments. + [1, 2, 3], + + // Many arguments. Even number of arguments. + [1, 2, 3, 4, 5, 6, 7, 8, 9, 0], + + // Many arguments. Odd number of arguments. + [1, 2, 3, 4, 5, 6, 7, 8, 9], +]; + +function f(...x) { + // SpreadCall to a native function with rest-args. + return Array(...x); +} + +// Don't inline |f| into the top-level script. +with ({}) ; + +for (let i = 0; i < 400; ++i) { + let x = xs[i % xs.length]; + + // NB: Array(1) creates the array `[,]`. + let expected = x.length !== 1 ? x : [,]; + + let result = f.apply(null, x); + assertEq(arraysEqual(result, expected), true); +} diff --git a/js/src/jit-test/tests/ion/apply-native-spreadnew-arguments.js b/js/src/jit-test/tests/ion/apply-native-spreadnew-arguments.js new file mode 100644 index 0000000000..7e31cdcbd6 --- /dev/null +++ b/js/src/jit-test/tests/ion/apply-native-spreadnew-arguments.js @@ -0,0 +1,39 @@ +load(libdir + "array-compare.js"); + +const xs = [ + // Zero arguments. + [], + + // Single argument. + [1], + + // Few arguments. Even number of arguments. + [1, 2], + + // Few arguments. Odd number of arguments. + [1, 2, 3], + + // Many arguments. Even number of arguments. + [1, 2, 3, 4, 5, 6, 7, 8, 9, 0], + + // Many arguments. Odd number of arguments. + [1, 2, 3, 4, 5, 6, 7, 8, 9], +]; + +function f() { + // SpreadNew to a native function with frame arguments. + return new Array(...arguments); +} + +// Don't inline |f| into the top-level script. +with ({}) ; + +for (let i = 0; i < 400; ++i) { + let x = xs[i % xs.length]; + + // NB: Array(1) creates the array `[,]`. + let expected = x.length !== 1 ? x : [,]; + + let result = f.apply(null, x); + assertEq(arraysEqual(result, expected), true); +} diff --git a/js/src/jit-test/tests/ion/apply-native-spreadnew-array.js b/js/src/jit-test/tests/ion/apply-native-spreadnew-array.js new file mode 100644 index 0000000000..5c716f48b4 --- /dev/null +++ b/js/src/jit-test/tests/ion/apply-native-spreadnew-array.js @@ -0,0 +1,39 @@ +load(libdir + "array-compare.js"); + +const xs = [ + // Zero arguments. + [], + + // Single argument. + [1], + + // Few arguments. Even number of arguments. + [1, 2], + + // Few arguments. Odd number of arguments. + [1, 2, 3], + + // Many arguments. Even number of arguments. + [1, 2, 3, 4, 5, 6, 7, 8, 9, 0], + + // Many arguments. Odd number of arguments. + [1, 2, 3, 4, 5, 6, 7, 8, 9], +]; + +function f(x) { + // SpreadNew to a native function with an array. + return new Array(...x); +} + +// Don't inline |f| into the top-level script. +with ({}) ; + +for (let i = 0; i < 400; ++i) { + let x = xs[i % xs.length]; + + // NB: Array(1) creates the array `[,]`. + let expected = x.length !== 1 ? x : [,]; + + let result = f(x); + assertEq(arraysEqual(result, expected), true); +} diff --git a/js/src/jit-test/tests/ion/apply-native-spreadnew-newtarget.js b/js/src/jit-test/tests/ion/apply-native-spreadnew-newtarget.js new file mode 100644 index 0000000000..9ffe53277b --- /dev/null +++ b/js/src/jit-test/tests/ion/apply-native-spreadnew-newtarget.js @@ -0,0 +1,66 @@ +load(libdir + "array-compare.js"); + +const xs = [ + // Zero arguments. + [], + + // Single argument. + [1], + + // Few arguments. Even number of arguments. + [1, 2], + + // Few arguments. Odd number of arguments. + [1, 2, 3], + + // Many arguments. Even number of arguments. + [1, 2, 3, 4, 5, 6, 7, 8, 9, 0], + + // Many arguments. Odd number of arguments. + [1, 2, 3, 4, 5, 6, 7, 8, 9], +]; + +class ArrayWithExplicitConstructor extends Array { + constructor(...args) { + super(...args); + } +} + +class ArrayWithImplicitConstructor extends Array { + constructor(...args) { + super(...args); + } +} + +function f(...x) { + return new ArrayWithExplicitConstructor(...x); +} + +function g(...x) { + return new ArrayWithImplicitConstructor(...x); +} + +// Don't inline |f| and |g| into the top-level script. +with ({}) ; + +for (let i = 0; i < 400; ++i) { + let x = xs[i % xs.length]; + + // NB: Array(1) creates the array `[,]`. + let expected = x.length !== 1 ? x : [,]; + + let result = f.apply(null, x); + assertEq(arraysEqual(result, expected), true); + assertEq(Object.getPrototypeOf(result), ArrayWithExplicitConstructor.prototype); +} + +for (let i = 0; i < 400; ++i) { + let x = xs[i % xs.length]; + + // NB: Array(1) creates the array `[,]`. + let expected = x.length !== 1 ? x : [,]; + + let result = g.apply(null, x); + assertEq(arraysEqual(result, expected), true); + assertEq(Object.getPrototypeOf(result), ArrayWithImplicitConstructor.prototype); +} diff --git a/js/src/jit-test/tests/ion/apply-native-spreadnew-rest.js b/js/src/jit-test/tests/ion/apply-native-spreadnew-rest.js new file mode 100644 index 0000000000..58de8fa239 --- /dev/null +++ b/js/src/jit-test/tests/ion/apply-native-spreadnew-rest.js @@ -0,0 +1,39 @@ +load(libdir + "array-compare.js"); + +const xs = [ + // Zero arguments. + [], + + // Single argument. + [1], + + // Few arguments. Even number of arguments. + [1, 2], + + // Few arguments. Odd number of arguments. + [1, 2, 3], + + // Many arguments. Even number of arguments. + [1, 2, 3, 4, 5, 6, 7, 8, 9, 0], + + // Many arguments. Odd number of arguments. + [1, 2, 3, 4, 5, 6, 7, 8, 9], +]; + +function f(...x) { + // SpreadNew to a native function with rest-args. + return new Array(...x); +} + +// Don't inline |f| into the top-level script. +with ({}) ; + +for (let i = 0; i < 400; ++i) { + let x = xs[i % xs.length]; + + // NB: Array(1) creates the array `[,]`. + let expected = x.length !== 1 ? x : [,]; + + let result = f.apply(null, x); + assertEq(arraysEqual(result, expected), true); +} diff --git a/js/src/jit-test/tests/ion/recover-atomics-islockfree.js b/js/src/jit-test/tests/ion/recover-atomics-islockfree.js new file mode 100644 index 0000000000..2a57afd49b --- /dev/null +++ b/js/src/jit-test/tests/ion/recover-atomics-islockfree.js @@ -0,0 +1,25 @@ +// |jit-test| --fast-warmup; --ion-offthread-compile=off + +function foo(n, trigger) { + let result = Atomics.isLockFree(n * -1); + if (trigger) { + assertEq(result, false); + } +} + +for (var i = 0; i < 100; i++) { + foo(-50, false); +} +foo(0, true); + +function bar(n, trigger) { + let result = Atomics.isLockFree(n * 4); + if (trigger) { + assertEq(result, false); + } +} + +for (var i = 0; i < 100; i++) { + bar(1, false); +} +bar(0x40000001, true); diff --git a/js/src/jit-test/tests/ion/recover-string-from-charcode.js b/js/src/jit-test/tests/ion/recover-string-from-charcode.js new file mode 100644 index 0000000000..be060be8e7 --- /dev/null +++ b/js/src/jit-test/tests/ion/recover-string-from-charcode.js @@ -0,0 +1,13 @@ +// |jit-test| --fast-warmup; --ion-offthread-compile=off + +function foo(n, trigger) { + let result = String.fromCharCode(n * -1); + if (trigger) { + assertEq(result, "\0"); + } +} + +for (var i = 0; i < 100; i++) { + foo(-50, false); +} +foo(0, true); diff --git a/js/src/jit-test/tests/modules/bug-1888902.js b/js/src/jit-test/tests/modules/bug-1888902.js new file mode 100644 index 0000000000..7804bef98a --- /dev/null +++ b/js/src/jit-test/tests/modules/bug-1888902.js @@ -0,0 +1,16 @@ +// |jit-test| error:Error + +const v0 = ` + function F1() { + const v11 = registerModule("module1", parseModule(\`import {} from "module2"; + import {} from "module3";\`)); + const v13 = "await 1;"; + drainJobQueue(); + registerModule("module2", parseModule(v13)); + registerModule("module3", parseModule(v0)); + moduleLink(v11); + moduleEvaluate(v11); + } + F1(); +`; +eval(v0); diff --git a/js/src/jit-test/tests/modules/dynamic-import-error.js b/js/src/jit-test/tests/modules/dynamic-import-error.js index 98a6af75d0..56713c8485 100644 --- a/js/src/jit-test/tests/modules/dynamic-import-error.js +++ b/js/src/jit-test/tests/modules/dynamic-import-error.js @@ -1,5 +1,3 @@ -// |jit-test| module - let result = null; let error = null; let promise = import("nonexistent.js"); diff --git a/js/src/jit-test/tests/modules/dynamic-import-module.js b/js/src/jit-test/tests/modules/dynamic-import-module.js index 3c004258a3..fa19b74303 100644 --- a/js/src/jit-test/tests/modules/dynamic-import-module.js +++ b/js/src/jit-test/tests/modules/dynamic-import-module.js @@ -1,5 +1,3 @@ -// |jit-test| module - function testImport(path, name, value) { let result = null; let error = null; diff --git a/js/src/jit-test/tests/modules/inline-data-2.js b/js/src/jit-test/tests/modules/inline-data-2.js new file mode 100644 index 0000000000..8dbf92574d --- /dev/null +++ b/js/src/jit-test/tests/modules/inline-data-2.js @@ -0,0 +1,12 @@ +let result = null; +let error = null; +let promise = import("javascript: export let b = 100;"); +promise.then((ns) => { + result = ns; +}).catch((e) => { + error = e; +}); + +drainJobQueue(); +assertEq(error, null); +assertEq(result.b, 100); diff --git a/js/src/jit-test/tests/modules/inline-data.js b/js/src/jit-test/tests/modules/inline-data.js index 9c56856f8d..d81da0efe4 100644 --- a/js/src/jit-test/tests/modules/inline-data.js +++ b/js/src/jit-test/tests/modules/inline-data.js @@ -2,16 +2,3 @@ import { a } from "javascript: export let a = 42;"; assertEq(a, 42); - -let result = null; -let error = null; -let promise = import("javascript: export let b = 100;"); -promise.then((ns) => { - result = ns; -}).catch((e) => { - error = e; -}); - -drainJobQueue(); -assertEq(error, null); -assertEq(result.b, 100); diff --git a/js/src/jit-test/tests/modules/shell-wrapper.js b/js/src/jit-test/tests/modules/shell-wrapper.js index 1be1c486c6..058e574d4e 100644 --- a/js/src/jit-test/tests/modules/shell-wrapper.js +++ b/js/src/jit-test/tests/modules/shell-wrapper.js @@ -1,4 +1,3 @@ -// |jit-test| module // Test shell ModuleObject wrapper's accessors and methods load(libdir + "asserts.js"); @@ -49,10 +48,8 @@ const d = registerModule('d', parseModule(` f(); `)); moduleLink(d); -try { - await moduleEvaluate(d); -} catch (e) { -} +moduleEvaluate(d).catch(e => undefined); +drainJobQueue(); assertEq(d.evaluationError instanceof ReferenceError, true); testGetter(d, "evaluationError"); diff --git a/js/src/jit-test/tests/parser/bug1887176.js b/js/src/jit-test/tests/parser/bug1887176.js new file mode 100644 index 0000000000..bea2db519b --- /dev/null +++ b/js/src/jit-test/tests/parser/bug1887176.js @@ -0,0 +1,46 @@ + +// This tests a case where TokenStreamAnyChars::fillExceptingContext +// mishandled a wasm frame, leading to an assertion failure. + +if (!wasmIsSupported()) + quit(); + +const v0 = ` + const o6 = { + f() { + function F2() { + if (!new.target) { throw 'must be called with new'; } + } + return F2(); + return {}; // This can be anything, but it must be present + }, + }; + + const o7 = { + "main": o6, + }; + + const v15 = new WebAssembly.Module(wasmTextToBinary(\` + (module + (import "main" "f" (func)) + (func (export "go") + call 0 + ) + )\`)); + const v16 = new WebAssembly.Instance(v15, o7); + v16.exports.go(); +`; + +const o27 = { + // Both "fileName" and null are necessary + "fileName": null, +}; + +let caught = false; +try { + evaluate(v0, o27); +} catch (e) { + assertEq(e, "must be called with new"); + caught = true; +} +assertEq(caught, true); diff --git a/js/src/jit-test/tests/parser/dumpStencil-02.js b/js/src/jit-test/tests/parser/dumpStencil-02.js new file mode 100644 index 0000000000..e21962c36b --- /dev/null +++ b/js/src/jit-test/tests/parser/dumpStencil-02.js @@ -0,0 +1,8 @@ +let caught = false; +try { + dumpStencil("export var z;", { module : true, lineNumber: 0 }); +} catch (e) { + caught = true; + assertEq(e.message.includes("Module cannot be compiled with lineNumber == 0"), true); +} +assertEq(caught, true); diff --git a/js/src/jit-test/tests/parser/module-filename.js b/js/src/jit-test/tests/parser/module-filename.js new file mode 100644 index 0000000000..59017dd674 --- /dev/null +++ b/js/src/jit-test/tests/parser/module-filename.js @@ -0,0 +1,13 @@ +load(libdir + "asserts.js"); + +compileToStencil("", { fileName: "", module: true }); +assertThrowsInstanceOf(() => { + compileToStencil("", { fileName: null, module: true }); +}, Error); + +if (helperThreadCount() > 0) { + offThreadCompileModuleToStencil("", { fileName: "", module: true }); + assertThrowsInstanceOf(() => { + offThreadCompileModuleToStencil("", { fileName: null, module: true }); + }, Error); +} diff --git a/js/src/jit-test/tests/profiler/native-trampoline-2.js b/js/src/jit-test/tests/profiler/native-trampoline-2.js new file mode 100644 index 0000000000..a85913431b --- /dev/null +++ b/js/src/jit-test/tests/profiler/native-trampoline-2.js @@ -0,0 +1,7 @@ +let arr = [1, 2, 3, 4, 5, 6, 7, 8]; +arr.sort((x, y) => { + enableGeckoProfilingWithSlowAssertions(); + readGeckoProfilingStack(); + return y - x; +}); +assertEq(arr.toString(), "8,7,6,5,4,3,2,1"); diff --git a/js/src/jit-test/tests/profiler/native-trampoline-3.js b/js/src/jit-test/tests/profiler/native-trampoline-3.js new file mode 100644 index 0000000000..16fe547051 --- /dev/null +++ b/js/src/jit-test/tests/profiler/native-trampoline-3.js @@ -0,0 +1,32 @@ +// |jit-test| skip-if: !wasmIsSupported() + +// Use a Wasm module to get the following stack frames: +// +// .. => array sort trampoline => wasmfunc comparator (Wasm) => comparator (JS) + +let binary = wasmTextToBinary(` +(module + (import "" "comparator" (func $comparator (param i32) (param i32) (result i32))) + (func $wasmfunc + (export "wasmfunc") + (param $x i32) + (param $y i32) + (result i32) + (return (call $comparator (local.get $x) (local.get $y))) + ) +)`); +let mod = new WebAssembly.Module(binary); +let instance = new WebAssembly.Instance(mod, {"": {comparator}}); + +function comparator(x, y) { + readGeckoProfilingStack(); + return y - x; +} + +enableGeckoProfilingWithSlowAssertions(); + +for (let i = 0; i < 20; i++) { + let arr = [3, 1, 2, -1, 0, 4]; + arr.sort(instance.exports.wasmfunc); + assertEq(arr.toString(), "4,3,2,1,0,-1"); +} diff --git a/js/src/jit-test/tests/profiler/native-trampoline.js b/js/src/jit-test/tests/profiler/native-trampoline.js new file mode 100644 index 0000000000..e140874a15 --- /dev/null +++ b/js/src/jit-test/tests/profiler/native-trampoline.js @@ -0,0 +1,40 @@ +enableGeckoProfilingWithSlowAssertions(); + +function testBasic() { + var arr = [2, -1]; + var cmp = function(x, y) { + readGeckoProfilingStack(); + return x - y; + }; + for (var i = 0; i < 20; i++) { + arr.sort(cmp); + } +} +testBasic(); + +function testRectifierFrame() { + var arr = [2, -1]; + var cmp = function(x, y, z, a) { + readGeckoProfilingStack(); + return x - y; + }; + for (var i = 0; i < 20; i++) { + arr.sort(cmp); + } +} +testRectifierFrame(); + +function testRectifierFrameCaller() { + var o = {}; + var calls = 0; + Object.defineProperty(o, "length", {get: function() { + calls++; + readGeckoProfilingStack(); + return 0; + }}); + for (var i = 0; i < 20; i++) { + Array.prototype.sort.call(o); + } + assertEq(calls, 20); +} +testRectifierFrameCaller(); diff --git a/js/src/jit-test/tests/profiler/wasm-to-js-1.js b/js/src/jit-test/tests/profiler/wasm-to-js-1.js new file mode 100644 index 0000000000..2ce48f391c --- /dev/null +++ b/js/src/jit-test/tests/profiler/wasm-to-js-1.js @@ -0,0 +1,20 @@ +// |jit-test| skip-if: !wasmIsSupported(); --fast-warmup +function sample() { + enableGeckoProfiling(); + readGeckoProfilingStack(); + disableGeckoProfiling(); +} +const text = `(module + (import "m" "f" (func $f)) + (func (export "test") + (call $f) +))`; +const bytes = wasmTextToBinary(text); +const mod = new WebAssembly.Module(bytes); +const imports = {"m": {"f": sample}}; +const instance = new WebAssembly.Instance(mod, imports); +sample(); +for (let i = 0; i < 5; i++) { + gc(this, "shrinking"); + instance.exports.test(); +} diff --git a/js/src/jit-test/tests/profiler/wasm-to-js-2.js b/js/src/jit-test/tests/profiler/wasm-to-js-2.js new file mode 100644 index 0000000000..3949c3a587 --- /dev/null +++ b/js/src/jit-test/tests/profiler/wasm-to-js-2.js @@ -0,0 +1,19 @@ +// |jit-test| skip-if: !wasmIsSupported() +// Ensure readGeckoProfilingStack finds at least 1 Wasm frame on the stack. +function calledFromWasm() { + let frames = readGeckoProfilingStack().flat(); + assertEq(frames.filter(f => f.kind === "wasm").length >= 1, true); +} +enableGeckoProfiling(); +const text = `(module + (import "m" "f" (func $f)) + (func (export "test") + (call $f) +))`; +const bytes = wasmTextToBinary(text); +const mod = new WebAssembly.Module(bytes); +const imports = {"m": {"f": calledFromWasm}}; +const instance = new WebAssembly.Instance(mod, imports); +for (let i = 0; i < 150; i++) { + instance.exports.test(); +} diff --git a/js/src/jit-test/tests/promise/allSettled-dead.js b/js/src/jit-test/tests/promise/allSettled-dead.js new file mode 100644 index 0000000000..8ae8e53d6b --- /dev/null +++ b/js/src/jit-test/tests/promise/allSettled-dead.js @@ -0,0 +1,20 @@ +newGlobal(); +const g = newGlobal({ + "newCompartment": true, +}); +const p1 = g.eval(` +Promise.resolve(); +`); +const p2 = p1.then(); +nukeAllCCWs(); +ignoreUnhandledRejections(); +Promise.resolve = function() { + return p2; +}; +let caught = false; +Promise.allSettled([1]).catch(e => { + caught = true; + assertEq(e.message.includes("dead object"), true); +}); +drainJobQueue(); +assertEq(caught, true); diff --git a/js/src/jit-test/tests/promise/jobqueue-interrupt-01.js b/js/src/jit-test/tests/promise/jobqueue-interrupt-01.js new file mode 100644 index 0000000000..758680e031 --- /dev/null +++ b/js/src/jit-test/tests/promise/jobqueue-interrupt-01.js @@ -0,0 +1,23 @@ +// catchTermination should undo the quit() operation and let the remaining jobs +// run. + +evaluate(` + quit(); +`, { + catchTermination : true +}); + +const global = newGlobal({ newCompartment: true }); + +let called = false; +const dbg = new Debugger(global); +dbg.onDebuggerStatement = function (frame) { + Promise.resolve(42).then(v => { called = true; }); +}; +global.eval(` + debugger; +`); + +drainJobQueue(); + +assertEq(called, true); diff --git a/js/src/jit-test/tests/promise/jobqueue-interrupt-02.js b/js/src/jit-test/tests/promise/jobqueue-interrupt-02.js new file mode 100644 index 0000000000..8d8f27ef91 --- /dev/null +++ b/js/src/jit-test/tests/promise/jobqueue-interrupt-02.js @@ -0,0 +1,14 @@ +// quit() while draining job queue leaves the remaining jobs untouched. + +const global = newGlobal({ newCompartment:true }); +const dbg = Debugger(global); +dbg.onDebuggerStatement = function() { + Promise.resolve().then(() => { + quit(); + }); + Promise.resolve().then(() => { + // This shouldn't be called. + assertEq(true, false); + }); +}; +global.eval("debugger"); diff --git a/js/src/jit-test/tests/proxy/bug1885774.js b/js/src/jit-test/tests/proxy/bug1885774.js new file mode 100644 index 0000000000..fa88cbf823 --- /dev/null +++ b/js/src/jit-test/tests/proxy/bug1885774.js @@ -0,0 +1,25 @@ +// |jit-test| --no-threads; --fast-warmup + +var {proxy, revoke} = Proxy.revocable({x:1}, {}); + +function foo(o) { + var res = 0; + for (var i = 0; i < 2; i++) { + res += o.x; + } + return res; +} + +with ({}) {} +for (var i = 0; i < 100; i++) { + assertEq(foo(proxy), 2); +} + +revoke(); +var caught = false; +try { + foo(proxy); +} catch { + caught = true; +} +assertEq(caught, true); diff --git a/js/src/jit-test/tests/structured-clone/bug1888727.js b/js/src/jit-test/tests/structured-clone/bug1888727.js new file mode 100644 index 0000000000..7958781c92 --- /dev/null +++ b/js/src/jit-test/tests/structured-clone/bug1888727.js @@ -0,0 +1,21 @@ +function test() { + // Construct a structured clone of a random BigInt value. + const n = 0xfeeddeadbeef2dadfeeddeadbeef2dadfeeddeadbeef2dadfeeddeadbeef2dadn; + const s = serialize(n, [], {scope: 'DifferentProcess'}); + assertEq(deserialize(s), n); + + // Truncate it by chopping off the last 8 bytes. + s.clonebuffer = s.arraybuffer.slice(0, -8); + + // Deserialization should now throw a catchable exception. + try { + deserialize(s); + // The bug was throwing an uncatchable error, so this next assertion won't + // be reached in either the buggy or fixed code. + assertEq(true, false, "should have thrown truncation error"); + } catch (e) { + assertEq(e.message.includes("truncated"), true); + } +} + +test(); diff --git a/js/src/jit-test/tests/structured-clone/tenuring.js b/js/src/jit-test/tests/structured-clone/tenuring.js index 0fffa064fa..cec53a6956 100644 --- a/js/src/jit-test/tests/structured-clone/tenuring.js +++ b/js/src/jit-test/tests/structured-clone/tenuring.js @@ -1,4 +1,4 @@ -// Check that we switch to allocating in the tenure heap after the first +// Check that we switch to allocating in the tenured heap after the first // nursery collection. function buildObjectTree(depth) { @@ -82,6 +82,7 @@ function countHeapLocations(tree, objectTree, counts) { gczeal(0); gcparam('minNurseryBytes', 1024 * 1024); gcparam('maxNurseryBytes', 1024 * 1024); +gcparam('semispaceNurseryEnabled', 0); gc(); testRoundTrip(1, true, true); diff --git a/js/src/jit-test/tests/typedarray/resizable-typedarray-from-pinned-buffer.js b/js/src/jit-test/tests/typedarray/resizable-typedarray-from-pinned-buffer.js new file mode 100644 index 0000000000..b17c7c0157 --- /dev/null +++ b/js/src/jit-test/tests/typedarray/resizable-typedarray-from-pinned-buffer.js @@ -0,0 +1,9 @@ +// |jit-test| --enable-arraybuffer-resizable + +let ab = new ArrayBuffer(8, {maxByteLength: 10}); + +pinArrayBufferOrViewLength(ab); + +let ta = new Int8Array(ab); + +assertEq(ta.length, 8); diff --git a/js/src/jit-test/tests/warp/bug1876425.js b/js/src/jit-test/tests/warp/bug1876425.js new file mode 100644 index 0000000000..aca528aac6 --- /dev/null +++ b/js/src/jit-test/tests/warp/bug1876425.js @@ -0,0 +1,62 @@ +// 1) Trial inline f1 => g (g1) => h. +// 2) Set g to g2, to fail the f1 => g1 call site. +// 3) Set g to g1 again. +// 4) Make g1's generic ICScript trial inline a different callee, h2. +// 5) Bail out from f1 => g1 => h. +// +// The bailout must not confuse the ICScripts of h1 and h2. + +function noninlined1(x) { + with (this) {}; + if (x === 4002) { + // Step 4. + f2(); + // Step 5. + return true; + } + return false; +} +function noninlined2(x) { + with (this) {}; + if (x === 4000) { + // Step 2. + g = (h, x) => { + return x + 1; + }; + } + if (x === 4001) { + // Step 3. + g = g1; + } +} +var h = function(x) { + if (noninlined1(x)) { + // Step 5. + bailout(); + } + return x + 1; +}; +var g = function(callee, x) { + return callee(x) + 1; +}; +var g1 = g; + +function f2() { + var h2 = x => x + 1; + for (var i = 0; i < 300; i++) { + var x = (i % 2 === 0) ? "foo" : i; // Force trial inlining. + g1(h2, x); + } +} + +function f1() { + for (var i = 0; i < 4200; i++) { + var x = (i < 900 && i % 2 === 0) ? "foo" : i; // Force trial inlining. + g(h, x); + noninlined2(i); + if (i === 200) { + trialInline(); + } + } +} +f1(); diff --git a/js/src/jit-test/tests/wasm/directiveless/bug1877358.js b/js/src/jit-test/tests/wasm/directiveless/bug1877358.js index 10cb54398a..8d512efcfe 100644 --- a/js/src/jit-test/tests/wasm/directiveless/bug1877358.js +++ b/js/src/jit-test/tests/wasm/directiveless/bug1877358.js @@ -1,4 +1,4 @@ -// |jit-test| -P wasm_exceptions=false; include:wasm.js +// |jit-test| include:wasm.js let {test} = wasmEvalText(`(module (func $m (import "" "m")) diff --git a/js/src/jit-test/tests/wasm/gc/casting.js b/js/src/jit-test/tests/wasm/gc/casting.js index a71a589db8..3b550e6415 100644 --- a/js/src/jit-test/tests/wasm/gc/casting.js +++ b/js/src/jit-test/tests/wasm/gc/casting.js @@ -114,3 +114,72 @@ function testAllCasts(types) { } } testAllCasts(TYPES); + +// Test that combinations of ref.test and ref.cast compile correctly. +// (These can be optimized together.) +{ + const { make, test1, test2, test3, test4 } = wasmEvalText(`(module + (type $a (array i32)) + (func (export "make") (param i32) (result anyref) + local.get 0 + local.get 0 + array.new_fixed $a 2 + ) + (func (export "test1") (param anyref) (result i32) + (if (ref.test (ref $a) (local.get 0)) + (then + (ref.cast (ref $a) (local.get 0)) + (array.get $a (i32.const 0)) + return + ) + ) + i32.const -1 + ) + (func (export "test2") (param anyref) (result i32) + (if (ref.test (ref $a) (local.get 0)) + (then) + (else + (ref.cast (ref $a) (local.get 0)) + (array.get $a (i32.const 0)) + return + ) + ) + i32.const -1 + ) + (func (export "test3") (param anyref) (result i32) + (if (ref.test (ref $a) (local.get 0)) + (then + (if (ref.test (ref $a) (local.get 0)) + (then) + (else + (ref.cast (ref $a) (local.get 0)) + (array.get $a (i32.const 0)) + return + ) + ) + ) + ) + i32.const -1 + ) + (func (export "test4") (param anyref) (result i32) + (if (ref.test (ref $a) (local.get 0)) + (then + (if (ref.test (ref $a) (local.get 0)) + (then + local.get 0 + ref.cast (ref $a) + ref.cast (ref $a) + (array.get $a (i32.const 0)) + return + ) + ) + ) + ) + i32.const -1 + ) + )`).exports; + assertEq(test1(make(99)), 99); + assertEq(test2(make(99)), -1); + assertEq(test3(make(99)), -1); + assertEq(test4(make(99)), 99); +} diff --git a/js/src/jit-test/tests/wasm/gc/i31ref.js b/js/src/jit-test/tests/wasm/gc/i31ref.js index 65f2fccc3f..298447e848 100644 --- a/js/src/jit-test/tests/wasm/gc/i31ref.js +++ b/js/src/jit-test/tests/wasm/gc/i31ref.js @@ -149,6 +149,24 @@ for (const {input, expected} of bigI32Tests) { assertEq(getElem(), expected); } +// Test that (ref.i31 (i32 const value)) optimization is correct +for (let value of WasmI31refValues) { + let {compare} = wasmEvalText(`(module + (func $innerCompare (param i32) (param i31ref) (result i32) + (ref.eq + (ref.i31 local.get 0) + local.get 1 + ) + ) + (func (export "compare") (result i32) + i32.const ${value} + (ref.i31 i32.const ${value}) + call $innerCompare + ) +)`).exports; + assertEq(compare(value), 1); +} + const { i31GetU_null, i31GetS_null } = wasmEvalText(`(module (func (export "i31GetU_null") (result i32) ref.null i31 diff --git a/js/src/jit-test/tests/wasm/regress/bug1886870.js b/js/src/jit-test/tests/wasm/regress/bug1886870.js new file mode 100644 index 0000000000..a4947bd91a --- /dev/null +++ b/js/src/jit-test/tests/wasm/regress/bug1886870.js @@ -0,0 +1,8 @@ +// Check proper handling of OOM after toQuotedString(). + +oomTest(function () { + new WebAssembly.Instance( + new WebAssembly.Module(wasmTextToBinary('(import "m" "f" (func $f))')), + {} + ); +}); diff --git a/js/src/jit-test/tests/wasm/regress/bug1887535.js b/js/src/jit-test/tests/wasm/regress/bug1887535.js new file mode 100644 index 0000000000..e2793831bf --- /dev/null +++ b/js/src/jit-test/tests/wasm/regress/bug1887535.js @@ -0,0 +1,25 @@ +// |jit-test| slow; + +// Tests the exception handling works during stack overflow. +const v1 = newGlobal({sameZoneAs: this}); +class C2 { + static { } +} + +function f() { v1.constructor; } + +const { test } = wasmEvalText(` +(module + (import "" "f" (func $f)) + (export "test" (func $f)) +)`, { "": { f, },}).exports; + + +function f4() { + try { + f4(); + } catch(_) { + test(); test(); + } +} +f4(); diff --git a/js/src/jit-test/tests/wasm/regress/bug1887596.js b/js/src/jit-test/tests/wasm/regress/bug1887596.js new file mode 100644 index 0000000000..8ff579fc35 --- /dev/null +++ b/js/src/jit-test/tests/wasm/regress/bug1887596.js @@ -0,0 +1,14 @@ +const t = ` + (module + (func $f (result f32) + f32.const 1.25 + ) + (table (export "table") 10 funcref) + (elem (i32.const 0) $f) + )`; +const i = new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary(t))); +const f = i.exports.table.get(0); + +// These FP equality comparisons are safe because 1.25 is representable exactly. +assertEq(1.25, f()); +assertEq(1.25, this.wasmLosslessInvoke(f).value); diff --git a/js/src/jit/ABIFunctionList-inl.h b/js/src/jit/ABIFunctionList-inl.h index f8a52beeff..eb2123f7a2 100644 --- a/js/src/jit/ABIFunctionList-inl.h +++ b/js/src/jit/ABIFunctionList-inl.h @@ -103,6 +103,8 @@ namespace jit { _(js::ArgumentsObject::finishForIonPure) \ _(js::ArgumentsObject::finishInlineForIonPure) \ _(js::ArrayShiftMoveElements) \ + _(js::ArraySortData::sortWithComparator) \ + _(js::ArraySortFromJit) \ _(js::ecmaAtan2) \ _(js::ecmaHypot) \ _(js::ecmaPow) \ diff --git a/js/src/jit/BaselineBailouts.cpp b/js/src/jit/BaselineBailouts.cpp index d916d000ed..150e16b618 100644 --- a/js/src/jit/BaselineBailouts.cpp +++ b/js/src/jit/BaselineBailouts.cpp @@ -517,7 +517,12 @@ void BaselineStackBuilder::setNextCallee( // // Also use the callee's own ICScript if we purged callee ICScripts. icScript_ = nextCallee->nonLazyScript()->jitScript()->icScript(); + if (trialInliningState != TrialInliningState::MonomorphicInlined) { + // Don't use specialized ICScripts for any of the callees if we had an + // inlining failure. We're now using the generic ICScript but compilation + // might have used the trial-inlined ICScript and these can have very + // different inlining graphs. canUseTrialInlinedICScripts_ = false; } } @@ -1567,6 +1572,7 @@ bool jit::BailoutIonToBaseline(JSContext* cx, JitActivation* activation, prevFrameType == FrameType::IonJS || prevFrameType == FrameType::BaselineStub || prevFrameType == FrameType::Rectifier || + prevFrameType == FrameType::TrampolineNative || prevFrameType == FrameType::IonICCall || prevFrameType == FrameType::BaselineJS || prevFrameType == FrameType::BaselineInterpreterEntry); @@ -1965,14 +1971,6 @@ bool jit::FinishBailoutToBaseline(BaselineBailoutInfo* bailoutInfoArg) { UnwindEnvironment(cx, ei, bailoutInfo->tryPC); } - // Check for interrupts now because we might miss an interrupt check in JIT - // code when resuming in the prologue, after the stack/interrupt check. - if (!cx->isExceptionPending()) { - if (!CheckForInterrupt(cx)) { - return false; - } - } - BailoutKind bailoutKind = *bailoutInfo->bailoutKind; JitSpew(JitSpew_BaselineBailouts, " Restored outerScript=(%s:%u:%u,%u) innerScript=(%s:%u:%u,%u) " @@ -2169,7 +2167,17 @@ bool jit::FinishBailoutToBaseline(BaselineBailoutInfo* bailoutInfoArg) { ionScript->incNumFixableBailouts(); if (ionScript->shouldInvalidate()) { #ifdef DEBUG - if (saveFailedICHash && !JitOptions.disableBailoutLoopCheck) { + // To detect bailout loops, we save a hash of the CacheIR used to + // compile this script, and assert that we don't recompile with the + // exact same inputs. Some of our bailout detection strategies, like + // LICM and stub folding, rely on bailing out, updating some state + // when we hit the baseline fallback, and using that information when + // we invalidate. If the frequentBailoutThreshold is set too low, we + // will instead invalidate the first time we bail out, so we don't + // have the chance to make those decisions. That doesn't happen in + // regular code, so we just skip bailout loop detection in that case. + if (saveFailedICHash && !JitOptions.disableBailoutLoopCheck && + JitOptions.frequentBailoutThreshold > 1) { outerScript->jitScript()->setFailedICHash(ionScript->icHash()); } #endif diff --git a/js/src/jit/BaselineFrame.h b/js/src/jit/BaselineFrame.h index 6138332c81..f2a6811177 100644 --- a/js/src/jit/BaselineFrame.h +++ b/js/src/jit/BaselineFrame.h @@ -109,7 +109,9 @@ class BaselineFrame { bool isConstructing() const { return CalleeTokenIsConstructing(calleeToken()); } - JSScript* script() const { return ScriptFromCalleeToken(calleeToken()); } + JSScript* script() const { + return MaybeForwardedScriptFromCalleeToken(calleeToken()); + } JSFunction* callee() const { return CalleeTokenToFunction(calleeToken()); } Value calleev() const { return ObjectValue(*callee()); } diff --git a/js/src/jit/CacheIR.cpp b/js/src/jit/CacheIR.cpp index 68dbd6bfee..03eae14140 100644 --- a/js/src/jit/CacheIR.cpp +++ b/js/src/jit/CacheIR.cpp @@ -1199,7 +1199,8 @@ static ObjOperandId GuardAndLoadWindowProxyWindow(CacheIRWriter& writer, ObjOperandId objId, GlobalObject* windowObj) { writer.guardClass(objId, GuardClassKind::WindowProxy); - ObjOperandId windowObjId = writer.loadWrapperTarget(objId); + ObjOperandId windowObjId = writer.loadWrapperTarget(objId, + /*fallible = */ false); writer.guardSpecificObject(windowObjId, windowObj); return windowObjId; } @@ -1357,7 +1358,8 @@ AttachDecision GetPropIRGenerator::tryAttachCrossCompartmentWrapper( writer.guardHasProxyHandler(objId, Wrapper::wrapperHandler(obj)); // Load the object wrapped by the CCW - ObjOperandId wrapperTargetId = writer.loadWrapperTarget(objId); + ObjOperandId wrapperTargetId = + writer.loadWrapperTarget(objId, /*fallible = */ false); // If the compartment of the wrapped object is different we should fail. writer.guardCompartment(wrapperTargetId, wrappedTargetGlobal, @@ -1468,7 +1470,8 @@ AttachDecision GetPropIRGenerator::tryAttachXrayCrossCompartmentWrapper( writer.guardHasProxyHandler(objId, GetProxyHandler(obj)); // Load the object wrapped by the CCW - ObjOperandId wrapperTargetId = writer.loadWrapperTarget(objId); + ObjOperandId wrapperTargetId = + writer.loadWrapperTarget(objId, /*fallible = */ false); // Test the wrapped object's class. The properties held by xrays or their // prototypes will be invariant for objects of a given class, except for @@ -1578,9 +1581,9 @@ AttachDecision GetPropIRGenerator::tryAttachScriptedProxy( writer.guardIsProxy(objId); writer.guardHasProxyHandler(objId, &ScriptedProxyHandler::singleton); - ValOperandId handlerValId = writer.loadScriptedProxyHandler(objId); - ObjOperandId handlerObjId = writer.guardToObject(handlerValId); - ObjOperandId targetObjId = writer.loadWrapperTarget(objId); + ObjOperandId handlerObjId = writer.loadScriptedProxyHandler(objId); + ObjOperandId targetObjId = + writer.loadWrapperTarget(objId, /*fallible =*/true); writer.guardIsNativeObject(targetObjId); diff --git a/js/src/jit/CacheIR.h b/js/src/jit/CacheIR.h index 9bedbb7ddc..132070d535 100644 --- a/js/src/jit/CacheIR.h +++ b/js/src/jit/CacheIR.h @@ -321,6 +321,12 @@ class CallFlags { CallFlags() = default; explicit CallFlags(ArgFormat format) : argFormat_(format) {} + CallFlags(ArgFormat format, bool isConstructing, bool isSameRealm, + bool needsUninitializedThis) + : argFormat_(format), + isConstructing_(isConstructing), + isSameRealm_(isSameRealm), + needsUninitializedThis_(needsUninitializedThis) {} CallFlags(bool isConstructing, bool isSpread, bool isSameRealm = false, bool needsUninitializedThis = false) : argFormat_(isSpread ? Spread : Standard), diff --git a/js/src/jit/CacheIRCompiler.cpp b/js/src/jit/CacheIRCompiler.cpp index 1467cebe08..9a26b0816c 100644 --- a/js/src/jit/CacheIRCompiler.cpp +++ b/js/src/jit/CacheIRCompiler.cpp @@ -2379,19 +2379,23 @@ bool CacheIRCompiler::emitGuardDynamicSlotValue(ObjOperandId objId, return true; } -bool CacheIRCompiler::emitLoadScriptedProxyHandler(ValOperandId resultId, +bool CacheIRCompiler::emitLoadScriptedProxyHandler(ObjOperandId resultId, ObjOperandId objId) { JitSpew(JitSpew_Codegen, "%s", __FUNCTION__); Register obj = allocator.useRegister(masm, objId); - ValueOperand output = allocator.defineValueRegister(masm, resultId); + Register output = allocator.defineRegister(masm, resultId); + + FailurePath* failure; + if (!addFailurePath(&failure)) { + return false; + } + + masm.loadPtr(Address(obj, ProxyObject::offsetOfReservedSlots()), output); + Address handlerAddr(output, js::detail::ProxyReservedSlots::offsetOfSlot( + ScriptedProxyHandler::HANDLER_EXTRA)); + masm.fallibleUnboxObject(handlerAddr, output, failure->label()); - masm.loadPtr(Address(obj, ProxyObject::offsetOfReservedSlots()), - output.scratchReg()); - masm.loadValue( - Address(output.scratchReg(), js::detail::ProxyReservedSlots::offsetOfSlot( - ScriptedProxyHandler::HANDLER_EXTRA)), - output); return true; } @@ -2937,14 +2941,27 @@ bool CacheIRCompiler::emitLoadEnclosingEnvironment(ObjOperandId objId, } bool CacheIRCompiler::emitLoadWrapperTarget(ObjOperandId objId, - ObjOperandId resultId) { + ObjOperandId resultId, + bool fallible) { JitSpew(JitSpew_Codegen, "%s", __FUNCTION__); Register obj = allocator.useRegister(masm, objId); Register reg = allocator.defineRegister(masm, resultId); + FailurePath* failure; + if (fallible && !addFailurePath(&failure)) { + return false; + } + masm.loadPtr(Address(obj, ProxyObject::offsetOfReservedSlots()), reg); - masm.unboxObject( - Address(reg, js::detail::ProxyReservedSlots::offsetOfPrivateSlot()), reg); + + Address targetAddr(reg, + js::detail::ProxyReservedSlots::offsetOfPrivateSlot()); + if (fallible) { + masm.fallibleUnboxObject(targetAddr, reg, failure->label()); + } else { + masm.unboxObject(targetAddr, reg); + } + return true; } diff --git a/js/src/jit/CacheIROps.yaml b/js/src/jit/CacheIROps.yaml index 974404d5c0..2f3097dfd8 100644 --- a/js/src/jit/CacheIROps.yaml +++ b/js/src/jit/CacheIROps.yaml @@ -708,7 +708,7 @@ transpile: true cost_estimate: 1 args: - result: ValId + result: ObjId obj: ObjId - name: IdToStringOrSymbol @@ -837,6 +837,7 @@ args: obj: ObjId result: ObjId + fallible: BoolImm - name: LoadValueTag shared: true diff --git a/js/src/jit/CacheIRReader.h b/js/src/jit/CacheIRReader.h index 54b298c999..59483424a3 100644 --- a/js/src/jit/CacheIRReader.h +++ b/js/src/jit/CacheIRReader.h @@ -129,21 +129,15 @@ class MOZ_RAII CacheIRReader { bool isSameRealm = encoded & CallFlags::IsSameRealm; bool needsUninitializedThis = encoded & CallFlags::NeedsUninitializedThis; MOZ_ASSERT_IF(needsUninitializedThis, isConstructing); - switch (format) { - case CallFlags::Unknown: - MOZ_CRASH("Unexpected call flags"); - case CallFlags::Standard: - return CallFlags(isConstructing, /*isSpread =*/false, isSameRealm, - needsUninitializedThis); - case CallFlags::Spread: - return CallFlags(isConstructing, /*isSpread =*/true, isSameRealm, - needsUninitializedThis); - default: - // The existing non-standard argument formats (FunCall and FunApply) - // can't be constructors. - MOZ_ASSERT(!isConstructing); - return CallFlags(format); - } + + // FunCall and FunApply can't be constructors. + MOZ_ASSERT_IF(format == CallFlags::FunCall, !isConstructing); + MOZ_ASSERT_IF(format == CallFlags::FunApplyArgsObj, !isConstructing); + MOZ_ASSERT_IF(format == CallFlags::FunApplyArray, !isConstructing); + MOZ_ASSERT_IF(format == CallFlags::FunApplyNullUndefined, !isConstructing); + + return CallFlags(format, isConstructing, isSameRealm, + needsUninitializedThis); } uint8_t readByte() { return buffer_.readByte(); } diff --git a/js/src/jit/CodeGenerator.cpp b/js/src/jit/CodeGenerator.cpp index 10a69f0cb3..559ac50cc7 100644 --- a/js/src/jit/CodeGenerator.cpp +++ b/js/src/jit/CodeGenerator.cpp @@ -4205,13 +4205,23 @@ void CodeGenerator::visitGuardShape(LGuardShape* guard) { } void CodeGenerator::visitGuardFuse(LGuardFuse* guard) { + auto fuseIndex = guard->mir()->fuseIndex(); + switch (fuseIndex) { + case RealmFuses::FuseIndex::OptimizeGetIteratorFuse: + addOptimizeGetIteratorFuseDependency(); + return; + default: + // validateAndRegisterFuseDependencies doesn't have + // handling for this yet, actively check fuse instead. + break; + } + Register temp = ToRegister(guard->temp0()); Label bail; // Bake specific fuse address for Ion code, because we won't share this code // across realms. - GuardFuse* fuse = - mirGen().realm->realmFuses().getFuseByIndex(guard->mir()->fuseIndex()); + GuardFuse* fuse = mirGen().realm->realmFuses().getFuseByIndex(fuseIndex); masm.loadPtr(AbsoluteAddress(fuse->fuseRef()), temp); masm.branchPtr(Assembler::NotEqual, temp, ImmPtr(nullptr), &bail); @@ -6269,7 +6279,8 @@ void CodeGenerator::visitCallKnown(LCallKnown* call) { UnusedStackBytesForCall(call->mir()->paddedNumStackArgs()); WrappedFunction* target = call->getSingleTarget(); - // Native single targets (except wasm) are handled by LCallNative. + // Native single targets (except Wasm and TrampolineNative functions) are + // handled by LCallNative. MOZ_ASSERT(target->hasJitEntry()); // Missing arguments must have been explicitly appended by WarpBuilder. @@ -6341,12 +6352,7 @@ void CodeGenerator::visitCallKnown(LCallKnown* call) { template <typename T> void CodeGenerator::emitCallInvokeFunction(T* apply) { - Register objreg = ToRegister(apply->getTempObject()); - - // Push the space used by the arguments. - masm.moveStackPtrTo(objreg); - - pushArg(objreg); // argv. + pushArg(masm.getStackPointer()); // argv. pushArg(ToRegister(apply->getArgc())); // argc. pushArg(Imm32(apply->mir()->ignoresReturnValue())); // ignoresReturnValue. pushArg(Imm32(apply->mir()->isConstructing())); // isConstructing. @@ -6370,7 +6376,13 @@ void CodeGenerator::emitAllocateSpaceForApply(Register argcreg, "Stack padding assumes that the frameSize is correct"); MOZ_ASSERT(JitStackValueAlignment == 2); Label noPaddingNeeded; - // if the number of arguments is odd, then we do not need any padding. + // If the number of arguments is odd, then we do not need any padding. + // + // Note: The |JitStackValueAlignment == 2| condition requires that the + // overall number of values on the stack is even. When we have an odd number + // of arguments, we don't need any padding, because the |thisValue| is + // pushed after the arguments, so the overall number of values on the stack + // is even. masm.branchTestPtr(Assembler::NonZero, argcreg, Imm32(1), &noPaddingNeeded); masm.addPtr(Imm32(1), scratch); masm.bind(&noPaddingNeeded); @@ -6382,13 +6394,13 @@ void CodeGenerator::emitAllocateSpaceForApply(Register argcreg, masm.subFromStackPtr(scratch); #ifdef DEBUG - // Put a magic value in the space reserved for padding. Note, this code - // cannot be merged with the previous test, as not all architectures can - // write below their stack pointers. + // Put a magic value in the space reserved for padding. Note, this code cannot + // be merged with the previous test, as not all architectures can write below + // their stack pointers. if (JitStackValueAlignment > 1) { MOZ_ASSERT(JitStackValueAlignment == 2); Label noPaddingNeeded; - // if the number of arguments is odd, then we do not need any padding. + // If the number of arguments is odd, then we do not need any padding. masm.branchTestPtr(Assembler::NonZero, argcreg, Imm32(1), &noPaddingNeeded); BaseValueIndex dstPtr(masm.getStackPointer(), argcreg); masm.storeValue(MagicValue(JS_ARG_POISON), dstPtr); @@ -6403,8 +6415,8 @@ void CodeGenerator::emitAllocateSpaceForConstructAndPushNewTarget( Register argcreg, Register newTargetAndScratch) { // Align the JitFrameLayout on the JitStackAlignment. Contrary to // |emitAllocateSpaceForApply()|, we're always pushing a magic value, because - // we can't write to |newTargetAndScratch| before |new.target| has - // been pushed onto the stack. + // we can't write to |newTargetAndScratch| before |new.target| has been pushed + // onto the stack. if (JitStackValueAlignment > 1) { MOZ_ASSERT(frameSize() % JitStackAlignment == 0, "Stack padding assumes that the frameSize is correct"); @@ -6412,6 +6424,12 @@ void CodeGenerator::emitAllocateSpaceForConstructAndPushNewTarget( Label noPaddingNeeded; // If the number of arguments is even, then we do not need any padding. + // + // Note: The |JitStackValueAlignment == 2| condition requires that the + // overall number of values on the stack is even. When we have an even + // number of arguments, we don't need any padding, because |new.target| is + // is pushed before the arguments and |thisValue| is pushed after all + // arguments, so the overall number of values on the stack is even. masm.branchTestPtr(Assembler::Zero, argcreg, Imm32(1), &noPaddingNeeded); masm.pushValue(MagicValue(JS_ARG_POISON)); masm.bind(&noPaddingNeeded); @@ -6437,9 +6455,8 @@ void CodeGenerator::emitCopyValuesForApply(Register argvSrcBase, Label loop; masm.bind(&loop); - // As argvIndex is off by 1, and we use the decBranchPtr instruction - // to loop back, we have to substract the size of the word which are - // copied. + // As argvIndex is off by 1, and we use the decBranchPtr instruction to loop + // back, we have to substract the size of the word which are copied. BaseValueIndex srcPtr(argvSrcBase, argvIndex, int32_t(argvSrcOffset) - sizeof(void*)); BaseValueIndex dstPtr(masm.getStackPointer(), argvIndex, @@ -6488,6 +6505,9 @@ void CodeGenerator::emitPushArguments(Register argcreg, Register scratch, // clang-format on // Compute the source and destination offsets into the stack. + // + // The |extraFormals| parameter is used when copying rest-parameters and + // allows to skip the initial parameters before the actual rest-parameters. Register argvSrcBase = FramePointer; size_t argvSrcOffset = JitFrameLayout::offsetOfActualArgs() + extraFormals * sizeof(JS::Value); @@ -6500,17 +6520,18 @@ void CodeGenerator::emitPushArguments(Register argcreg, Register scratch, emitCopyValuesForApply(argvSrcBase, argvIndex, copyreg, argvSrcOffset, argvDstOffset); - // Join with all arguments copied and the extra stack usage computed. + // Join with all arguments copied. masm.bind(&end); } -void CodeGenerator::emitPushArguments(LApplyArgsGeneric* apply, - Register scratch) { - // Holds the function nargs. Initially the number of args to the caller. +void CodeGenerator::emitPushArguments(LApplyArgsGeneric* apply) { + // Holds the function nargs. Register argcreg = ToRegister(apply->getArgc()); Register copyreg = ToRegister(apply->getTempObject()); + Register scratch = ToRegister(apply->getTempForArgCopy()); uint32_t extraFormals = apply->numExtraFormals(); + // Allocate space on the stack for arguments. emitAllocateSpaceForApply(argcreg, scratch); emitPushArguments(argcreg, scratch, copyreg, extraFormals); @@ -6519,22 +6540,21 @@ void CodeGenerator::emitPushArguments(LApplyArgsGeneric* apply, masm.pushValue(ToValue(apply, LApplyArgsGeneric::ThisIndex)); } -void CodeGenerator::emitPushArguments(LApplyArgsObj* apply, Register scratch) { - // argc and argsObj are mapped to the same calltemp register. - MOZ_ASSERT(apply->getArgsObj() == apply->getArgc()); - - Register tmpArgc = ToRegister(apply->getTempObject()); +void CodeGenerator::emitPushArguments(LApplyArgsObj* apply) { Register argsObj = ToRegister(apply->getArgsObj()); + Register tmpArgc = ToRegister(apply->getTempObject()); + Register scratch = ToRegister(apply->getTempForArgCopy()); + + // argc and argsObj are mapped to the same calltemp register. + MOZ_ASSERT(argsObj == ToRegister(apply->getArgc())); // Load argc into tmpArgc. - Address lengthAddr(argsObj, ArgumentsObject::getInitialLengthSlotOffset()); - masm.unboxInt32(lengthAddr, tmpArgc); - masm.rshift32(Imm32(ArgumentsObject::PACKED_BITS_COUNT), tmpArgc); + masm.loadArgumentsObjectLength(argsObj, tmpArgc); - // Allocate space on the stack for arguments. This modifies scratch. + // Allocate space on the stack for arguments. emitAllocateSpaceForApply(tmpArgc, scratch); - // Load arguments data + // Load arguments data. masm.loadPrivate(Address(argsObj, ArgumentsObject::getDataSlotOffset()), argsObj); size_t argsSrcOffset = ArgumentsData::offsetOfArgs(); @@ -6543,6 +6563,7 @@ void CodeGenerator::emitPushArguments(LApplyArgsObj* apply, Register scratch) { // After this call, the argsObj register holds the argument count instead. emitPushArrayAsArguments(tmpArgc, argsObj, scratch, argsSrcOffset); + // Push |this|. masm.pushValue(ToValue(apply, LApplyArgsObj::ThisIndex)); } @@ -6566,69 +6587,72 @@ void CodeGenerator::emitPushArrayAsArguments(Register tmpArgc, // Skip the copy of arguments if there are none. masm.branchTestPtr(Assembler::Zero, tmpArgc, tmpArgc, &noCopy); + { + // Copy the values. This code is skipped entirely if there are no values. + size_t argvDstOffset = 0; - // Copy the values. This code is skipped entirely if there are - // no values. - size_t argvDstOffset = 0; - - Register argvSrcBase = srcBaseAndArgc; - Register copyreg = scratch; - - masm.push(tmpArgc); - Register argvIndex = tmpArgc; - argvDstOffset += sizeof(void*); + Register argvSrcBase = srcBaseAndArgc; - // Copy - emitCopyValuesForApply(argvSrcBase, argvIndex, copyreg, argvSrcOffset, - argvDstOffset); + // Stash away |tmpArgc| and adjust argvDstOffset accordingly. + masm.push(tmpArgc); + Register argvIndex = tmpArgc; + argvDstOffset += sizeof(void*); - // Restore. - masm.pop(srcBaseAndArgc); // srcBaseAndArgc now contains argc. - masm.jump(&epilogue); + // Copy + emitCopyValuesForApply(argvSrcBase, argvIndex, scratch, argvSrcOffset, + argvDstOffset); - // Clear argc if we skipped the copy step. + // Restore. + masm.pop(srcBaseAndArgc); // srcBaseAndArgc now contains argc. + masm.jump(&epilogue); + } masm.bind(&noCopy); - masm.movePtr(ImmWord(0), srcBaseAndArgc); + { + // Clear argc if we skipped the copy step. + masm.movePtr(ImmWord(0), srcBaseAndArgc); + } - // Join with all arguments copied and the extra stack usage computed. + // Join with all arguments copied. // Note, "srcBase" has become "argc". masm.bind(&epilogue); } -void CodeGenerator::emitPushArguments(LApplyArrayGeneric* apply, - Register scratch) { +void CodeGenerator::emitPushArguments(LApplyArrayGeneric* apply) { + Register elements = ToRegister(apply->getElements()); Register tmpArgc = ToRegister(apply->getTempObject()); - Register elementsAndArgc = ToRegister(apply->getElements()); + Register scratch = ToRegister(apply->getTempForArgCopy()); + + // argc and elements are mapped to the same calltemp register. + MOZ_ASSERT(elements == ToRegister(apply->getArgc())); // Invariants guarded in the caller: // - the array is not too long // - the array length equals its initialized length // The array length is our argc for the purposes of allocating space. - Address length(ToRegister(apply->getElements()), - ObjectElements::offsetOfLength()); - masm.load32(length, tmpArgc); + masm.load32(Address(elements, ObjectElements::offsetOfLength()), tmpArgc); // Allocate space for the values. emitAllocateSpaceForApply(tmpArgc, scratch); // After this call "elements" has become "argc". size_t elementsOffset = 0; - emitPushArrayAsArguments(tmpArgc, elementsAndArgc, scratch, elementsOffset); + emitPushArrayAsArguments(tmpArgc, elements, scratch, elementsOffset); // Push |this|. masm.pushValue(ToValue(apply, LApplyArrayGeneric::ThisIndex)); } -void CodeGenerator::emitPushArguments(LConstructArgsGeneric* construct, - Register scratch) { - MOZ_ASSERT(scratch == ToRegister(construct->getNewTarget())); - - // Holds the function nargs. Initially the number of args to the caller. +void CodeGenerator::emitPushArguments(LConstructArgsGeneric* construct) { + // Holds the function nargs. Register argcreg = ToRegister(construct->getArgc()); Register copyreg = ToRegister(construct->getTempObject()); + Register scratch = ToRegister(construct->getTempForArgCopy()); uint32_t extraFormals = construct->numExtraFormals(); + // newTarget and scratch are mapped to the same calltemp register. + MOZ_ASSERT(scratch == ToRegister(construct->getNewTarget())); + // Allocate space for the values. // After this call "newTarget" has become "scratch". emitAllocateSpaceForConstructAndPushNewTarget(argcreg, scratch); @@ -6639,29 +6663,31 @@ void CodeGenerator::emitPushArguments(LConstructArgsGeneric* construct, masm.pushValue(ToValue(construct, LConstructArgsGeneric::ThisIndex)); } -void CodeGenerator::emitPushArguments(LConstructArrayGeneric* construct, - Register scratch) { - MOZ_ASSERT(scratch == ToRegister(construct->getNewTarget())); - +void CodeGenerator::emitPushArguments(LConstructArrayGeneric* construct) { + Register elements = ToRegister(construct->getElements()); Register tmpArgc = ToRegister(construct->getTempObject()); - Register elementsAndArgc = ToRegister(construct->getElements()); + Register scratch = ToRegister(construct->getTempForArgCopy()); + + // argc and elements are mapped to the same calltemp register. + MOZ_ASSERT(elements == ToRegister(construct->getArgc())); + + // newTarget and scratch are mapped to the same calltemp register. + MOZ_ASSERT(scratch == ToRegister(construct->getNewTarget())); // Invariants guarded in the caller: // - the array is not too long // - the array length equals its initialized length // The array length is our argc for the purposes of allocating space. - Address length(ToRegister(construct->getElements()), - ObjectElements::offsetOfLength()); - masm.load32(length, tmpArgc); + masm.load32(Address(elements, ObjectElements::offsetOfLength()), tmpArgc); // Allocate space for the values. + // After this call "newTarget" has become "scratch". emitAllocateSpaceForConstructAndPushNewTarget(tmpArgc, scratch); - // After this call "elements" has become "argc" and "newTarget" has become - // "scratch". + // After this call "elements" has become "argc". size_t elementsOffset = 0; - emitPushArrayAsArguments(tmpArgc, elementsAndArgc, scratch, elementsOffset); + emitPushArrayAsArguments(tmpArgc, elements, scratch, elementsOffset); // Push |this|. masm.pushValue(ToValue(construct, LConstructArrayGeneric::ThisIndex)); @@ -6682,43 +6708,24 @@ void CodeGenerator::emitApplyGeneric(T* apply) { // Copy the arguments of the current function. // - // In the case of ApplyArray, ConstructArray, or ApplyArgsObj, also - // compute argc. The argc register and the elements/argsObj register - // are the same; argc must not be referenced before the call to - // emitPushArguments() and elements/argsObj must not be referenced - // after it returns. + // In the case of ApplyArray, ConstructArray, or ApplyArgsObj, also compute + // argc. The argc register and the elements/argsObj register are the same; + // argc must not be referenced before the call to emitPushArguments() and + // elements/argsObj must not be referenced after it returns. // - // In the case of ConstructArray or ConstructArgs, also overwrite newTarget - // with scratch; newTarget must not be referenced after this point. + // In the case of ConstructArray or ConstructArgs, also overwrite newTarget; + // newTarget must not be referenced after this point. // // objreg is dead across this call. - emitPushArguments(apply, scratch); + emitPushArguments(apply); masm.checkStackAlignment(); bool constructing = apply->mir()->isConstructing(); - // If the function is native, only emit the call to InvokeFunction. - if (apply->hasSingleTarget() && - apply->getSingleTarget()->isNativeWithoutJitEntry()) { - emitCallInvokeFunction(apply); - -#ifdef DEBUG - // Native constructors are guaranteed to return an Object value, so we never - // have to replace a primitive result with the previously allocated Object - // from CreateThis. - if (constructing) { - Label notPrimitive; - masm.branchTestPrimitive(Assembler::NotEqual, JSReturnOperand, - ¬Primitive); - masm.assumeUnreachable("native constructors don't return primitives"); - masm.bind(¬Primitive); - } -#endif - - emitRestoreStackPointerFromFP(); - return; - } + // If the function is native, the call is compiled through emitApplyNative. + MOZ_ASSERT_IF(apply->hasSingleTarget(), + !apply->getSingleTarget()->isNativeWithoutJitEntry()); Label end, invoke; @@ -6812,8 +6819,8 @@ void CodeGenerator::emitApplyGeneric(T* apply) { masm.bind(&end); - // If the return value of the constructing function is Primitive, - // replace the return value with the Object from CreateThis. + // If the return value of the constructing function is Primitive, replace the + // return value with the Object from CreateThis. if (constructing) { Label notPrimitive; masm.branchTestPrimitive(Assembler::NotEqual, JSReturnOperand, @@ -6833,17 +6840,200 @@ void CodeGenerator::emitApplyGeneric(T* apply) { emitRestoreStackPointerFromFP(); } -void CodeGenerator::visitApplyArgsGeneric(LApplyArgsGeneric* apply) { +template <typename T> +void CodeGenerator::emitCallInvokeNativeFunction(T* apply) { + pushArg(masm.getStackPointer()); // argv. + pushArg(ToRegister(apply->getArgc())); // argc. + pushArg(Imm32(apply->mir()->ignoresReturnValue())); // ignoresReturnValue. + pushArg(Imm32(apply->mir()->isConstructing())); // isConstructing. + + using Fn = + bool (*)(JSContext*, bool, bool, uint32_t, Value*, MutableHandleValue); + callVM<Fn, jit::InvokeNativeFunction>(apply); +} + +template <typename T> +void CodeGenerator::emitPushNativeArguments(T* apply) { + Register argc = ToRegister(apply->getArgc()); + Register tmpArgc = ToRegister(apply->getTempObject()); + Register scratch = ToRegister(apply->getTempForArgCopy()); + uint32_t extraFormals = apply->numExtraFormals(); + + // Push arguments. + Label noCopy; + masm.branchTestPtr(Assembler::Zero, argc, argc, &noCopy); + { + // Use scratch register to calculate stack space (no padding needed). + masm.movePtr(argc, scratch); + + // Reserve space for copying the arguments. + NativeObject::elementsSizeMustNotOverflow(); + masm.lshiftPtr(Imm32(ValueShift), scratch); + masm.subFromStackPtr(scratch); + + // Compute the source and destination offsets into the stack. + Register argvSrcBase = FramePointer; + size_t argvSrcOffset = + JitFrameLayout::offsetOfActualArgs() + extraFormals * sizeof(JS::Value); + size_t argvDstOffset = 0; + + Register argvIndex = tmpArgc; + masm.move32(argc, argvIndex); + + // Copy arguments. + emitCopyValuesForApply(argvSrcBase, argvIndex, scratch, argvSrcOffset, + argvDstOffset); + } + masm.bind(&noCopy); +} + +template <typename T> +void CodeGenerator::emitPushArrayAsNativeArguments(T* apply) { + Register argc = ToRegister(apply->getArgc()); + Register elements = ToRegister(apply->getElements()); + Register tmpArgc = ToRegister(apply->getTempObject()); + Register scratch = ToRegister(apply->getTempForArgCopy()); + + // NB: argc and elements are mapped to the same register. + MOZ_ASSERT(argc == elements); + + // Invariants guarded in the caller: + // - the array is not too long + // - the array length equals its initialized length + + // The array length is our argc. + masm.load32(Address(elements, ObjectElements::offsetOfLength()), tmpArgc); + + // Skip the copy of arguments if there are none. + Label noCopy; + masm.branchTestPtr(Assembler::Zero, tmpArgc, tmpArgc, &noCopy); + { + // |tmpArgc| is off-by-one, so adjust the offset accordingly. + BaseObjectElementIndex srcPtr(elements, tmpArgc, + -int32_t(sizeof(JS::Value))); + + Label loop; + masm.bind(&loop); + masm.pushValue(srcPtr, scratch); + masm.decBranchPtr(Assembler::NonZero, tmpArgc, Imm32(1), &loop); + } + masm.bind(&noCopy); + + // Set argc in preparation for emitCallInvokeNativeFunction. + masm.load32(Address(elements, ObjectElements::offsetOfLength()), argc); +} + +void CodeGenerator::emitPushArguments(LApplyArgsNative* apply) { + emitPushNativeArguments(apply); +} + +void CodeGenerator::emitPushArguments(LApplyArrayNative* apply) { + emitPushArrayAsNativeArguments(apply); +} + +void CodeGenerator::emitPushArguments(LConstructArgsNative* construct) { + emitPushNativeArguments(construct); +} + +void CodeGenerator::emitPushArguments(LConstructArrayNative* construct) { + emitPushArrayAsNativeArguments(construct); +} + +void CodeGenerator::emitPushArguments(LApplyArgsObjNative* apply) { + Register argc = ToRegister(apply->getArgc()); + Register argsObj = ToRegister(apply->getArgsObj()); + Register tmpArgc = ToRegister(apply->getTempObject()); + Register scratch = ToRegister(apply->getTempForArgCopy()); + + // NB: argc and argsObj are mapped to the same register. + MOZ_ASSERT(argc == argsObj); + + // Load argc into tmpArgc. + masm.loadArgumentsObjectLength(argsObj, tmpArgc); + + // Push arguments. + Label noCopy, epilogue; + masm.branchTestPtr(Assembler::Zero, tmpArgc, tmpArgc, &noCopy); + { + // Use scratch register to calculate stack space (no padding needed). + masm.movePtr(tmpArgc, scratch); + + // Reserve space for copying the arguments. + NativeObject::elementsSizeMustNotOverflow(); + masm.lshiftPtr(Imm32(ValueShift), scratch); + masm.subFromStackPtr(scratch); + + // Load arguments data. + Register argvSrcBase = argsObj; + masm.loadPrivate(Address(argsObj, ArgumentsObject::getDataSlotOffset()), + argvSrcBase); + size_t argvSrcOffset = ArgumentsData::offsetOfArgs(); + size_t argvDstOffset = 0; + + // Stash away |tmpArgc| and adjust argvDstOffset accordingly. + masm.push(tmpArgc); + argvDstOffset += sizeof(void*); + + // Copy the values. + emitCopyValuesForApply(argvSrcBase, tmpArgc, scratch, argvSrcOffset, + argvDstOffset); + + // Set argc in preparation for emitCallInvokeNativeFunction. + masm.pop(argc); + masm.jump(&epilogue); + } + masm.bind(&noCopy); + { + // Set argc in preparation for emitCallInvokeNativeFunction. + masm.movePtr(ImmWord(0), argc); + } + masm.bind(&epilogue); +} + +template <typename T> +void CodeGenerator::emitApplyNative(T* apply) { + MOZ_ASSERT(apply->mir()->getSingleTarget()->isNativeWithoutJitEntry()); + + constexpr bool isConstructing = T::isConstructing(); + MOZ_ASSERT(isConstructing == apply->mir()->isConstructing(), + "isConstructing condition must be consistent"); + + // Push newTarget. + if constexpr (isConstructing) { + masm.pushValue(JSVAL_TYPE_OBJECT, ToRegister(apply->getNewTarget())); + } + + // Push arguments. + emitPushArguments(apply); + + // Push |this|. + if constexpr (isConstructing) { + masm.pushValue(MagicValue(JS_IS_CONSTRUCTING)); + } else { + masm.pushValue(ToValue(apply, T::ThisIndex)); + } + + // Push callee. + masm.pushValue(JSVAL_TYPE_OBJECT, ToRegister(apply->getFunction())); + + // Call the native function. + emitCallInvokeNativeFunction(apply); + + // Pop arguments and continue. + emitRestoreStackPointerFromFP(); +} + +template <typename T> +void CodeGenerator::emitApplyArgsGuard(T* apply) { LSnapshot* snapshot = apply->snapshot(); Register argcreg = ToRegister(apply->getArgc()); // Ensure that we have a reasonable number of arguments. bailoutCmp32(Assembler::Above, argcreg, Imm32(JIT_ARGS_LENGTH_MAX), snapshot); - - emitApplyGeneric(apply); } -void CodeGenerator::visitApplyArgsObj(LApplyArgsObj* apply) { +template <typename T> +void CodeGenerator::emitApplyArgsObjGuard(T* apply) { Register argsObj = ToRegister(apply->getArgsObj()); Register temp = ToRegister(apply->getTempObject()); @@ -6851,16 +7041,15 @@ void CodeGenerator::visitApplyArgsObj(LApplyArgsObj* apply) { masm.loadArgumentsObjectLength(argsObj, temp, &bail); masm.branch32(Assembler::Above, temp, Imm32(JIT_ARGS_LENGTH_MAX), &bail); bailoutFrom(&bail, apply->snapshot()); - - emitApplyGeneric(apply); } -void CodeGenerator::visitApplyArrayGeneric(LApplyArrayGeneric* apply) { +template <typename T> +void CodeGenerator::emitApplyArrayGuard(T* apply) { LSnapshot* snapshot = apply->snapshot(); + Register elements = ToRegister(apply->getElements()); Register tmp = ToRegister(apply->getTempObject()); - Address length(ToRegister(apply->getElements()), - ObjectElements::offsetOfLength()); + Address length(elements, ObjectElements::offsetOfLength()); masm.load32(length, tmp); // Ensure that we have a reasonable number of arguments. @@ -6868,43 +7057,60 @@ void CodeGenerator::visitApplyArrayGeneric(LApplyArrayGeneric* apply) { // Ensure that the array does not contain an uninitialized tail. - Address initializedLength(ToRegister(apply->getElements()), + Address initializedLength(elements, ObjectElements::offsetOfInitializedLength()); masm.sub32(initializedLength, tmp); bailoutCmp32(Assembler::NotEqual, tmp, Imm32(0), snapshot); +} +void CodeGenerator::visitApplyArgsGeneric(LApplyArgsGeneric* apply) { + emitApplyArgsGuard(apply); emitApplyGeneric(apply); } -void CodeGenerator::visitConstructArgsGeneric(LConstructArgsGeneric* lir) { - LSnapshot* snapshot = lir->snapshot(); - Register argcreg = ToRegister(lir->getArgc()); +void CodeGenerator::visitApplyArgsObj(LApplyArgsObj* apply) { + emitApplyArgsObjGuard(apply); + emitApplyGeneric(apply); +} - // Ensure that we have a reasonable number of arguments. - bailoutCmp32(Assembler::Above, argcreg, Imm32(JIT_ARGS_LENGTH_MAX), snapshot); +void CodeGenerator::visitApplyArrayGeneric(LApplyArrayGeneric* apply) { + emitApplyArrayGuard(apply); + emitApplyGeneric(apply); +} +void CodeGenerator::visitConstructArgsGeneric(LConstructArgsGeneric* lir) { + emitApplyArgsGuard(lir); emitApplyGeneric(lir); } void CodeGenerator::visitConstructArrayGeneric(LConstructArrayGeneric* lir) { - LSnapshot* snapshot = lir->snapshot(); - Register tmp = ToRegister(lir->getTempObject()); + emitApplyArrayGuard(lir); + emitApplyGeneric(lir); +} - Address length(ToRegister(lir->getElements()), - ObjectElements::offsetOfLength()); - masm.load32(length, tmp); +void CodeGenerator::visitApplyArgsNative(LApplyArgsNative* lir) { + emitApplyArgsGuard(lir); + emitApplyNative(lir); +} - // Ensure that we have a reasonable number of arguments. - bailoutCmp32(Assembler::Above, tmp, Imm32(JIT_ARGS_LENGTH_MAX), snapshot); +void CodeGenerator::visitApplyArgsObjNative(LApplyArgsObjNative* lir) { + emitApplyArgsObjGuard(lir); + emitApplyNative(lir); +} - // Ensure that the array does not contain an uninitialized tail. +void CodeGenerator::visitApplyArrayNative(LApplyArrayNative* lir) { + emitApplyArrayGuard(lir); + emitApplyNative(lir); +} - Address initializedLength(ToRegister(lir->getElements()), - ObjectElements::offsetOfInitializedLength()); - masm.sub32(initializedLength, tmp); - bailoutCmp32(Assembler::NotEqual, tmp, Imm32(0), snapshot); +void CodeGenerator::visitConstructArgsNative(LConstructArgsNative* lir) { + emitApplyArgsGuard(lir); + emitApplyNative(lir); +} - emitApplyGeneric(lir); +void CodeGenerator::visitConstructArrayNative(LConstructArrayNative* lir) { + emitApplyArrayGuard(lir); + emitApplyNative(lir); } void CodeGenerator::visitBail(LBail* lir) { bailout(lir->snapshot()); } @@ -15460,15 +15666,37 @@ void CodeGenerator::validateAndRegisterFuseDependencies(JSContext* cx, if (!hasSeenObjectEmulateUndefinedFuse.intact()) { JitSpew(JitSpew_Codegen, - "tossing compilation; fuse dependency no longer valid\n"); + "tossing compilation; hasSeenObjectEmulateUndefinedFuse fuse " + "dependency no longer valid\n"); *isValid = false; return; } if (!hasSeenObjectEmulateUndefinedFuse.addFuseDependency(cx, script)) { - JitSpew( - JitSpew_Codegen, - "tossing compilation; failed to register script dependency\n"); + JitSpew(JitSpew_Codegen, + "tossing compilation; failed to register " + "hasSeenObjectEmulateUndefinedFuse script dependency\n"); + *isValid = false; + return; + } + break; + } + + case FuseDependencyKind::OptimizeGetIteratorFuse: { + auto& optimizeGetIteratorFuse = + cx->realm()->realmFuses.optimizeGetIteratorFuse; + if (!optimizeGetIteratorFuse.intact()) { + JitSpew(JitSpew_Codegen, + "tossing compilation; optimizeGetIteratorFuse fuse " + "dependency no longer valid\n"); + *isValid = false; + return; + } + + if (!optimizeGetIteratorFuse.addFuseDependency(cx, script)) { + JitSpew(JitSpew_Codegen, + "tossing compilation; failed to register " + "optimizeGetIteratorFuse script dependency\n"); *isValid = false; return; } @@ -15837,15 +16065,16 @@ void CodeGenerator::visitMegamorphicSetElement(LMegamorphicSetElement* lir) { void CodeGenerator::visitLoadScriptedProxyHandler( LLoadScriptedProxyHandler* ins) { - const Register obj = ToRegister(ins->getOperand(0)); - ValueOperand output = ToOutValue(ins); + Register obj = ToRegister(ins->getOperand(0)); + Register output = ToRegister(ins->output()); - masm.loadPtr(Address(obj, ProxyObject::offsetOfReservedSlots()), - output.scratchReg()); - masm.loadValue( - Address(output.scratchReg(), js::detail::ProxyReservedSlots::offsetOfSlot( - ScriptedProxyHandler::HANDLER_EXTRA)), - output); + masm.loadPtr(Address(obj, ProxyObject::offsetOfReservedSlots()), output); + + Label bail; + Address handlerAddr(output, js::detail::ProxyReservedSlots::offsetOfSlot( + ScriptedProxyHandler::HANDLER_EXTRA)); + masm.fallibleUnboxObject(handlerAddr, output, &bail); + bailoutFrom(&bail, ins->snapshot()); } #ifdef JS_PUNBOX64 @@ -19861,9 +20090,17 @@ void CodeGenerator::visitLoadWrapperTarget(LLoadWrapperTarget* lir) { Register output = ToRegister(lir->output()); masm.loadPtr(Address(object, ProxyObject::offsetOfReservedSlots()), output); - masm.unboxObject( - Address(output, js::detail::ProxyReservedSlots::offsetOfPrivateSlot()), - output); + + // Bail for revoked proxies. + Label bail; + Address targetAddr(output, + js::detail::ProxyReservedSlots::offsetOfPrivateSlot()); + if (lir->mir()->fallible()) { + masm.fallibleUnboxObject(targetAddr, output, &bail); + bailoutFrom(&bail, lir->snapshot()); + } else { + masm.unboxObject(targetAddr, output); + } } void CodeGenerator::visitGuardHasGetterSetter(LGuardHasGetterSetter* lir) { @@ -20642,9 +20879,20 @@ void CodeGenerator::visitWasmAnyRefFromJSString(LWasmAnyRefFromJSString* lir) { } void CodeGenerator::visitWasmNewI31Ref(LWasmNewI31Ref* lir) { - Register value = ToRegister(lir->value()); - Register output = ToRegister(lir->output()); - masm.truncate32ToWasmI31Ref(value, output); + if (lir->value()->isConstant()) { + // i31ref are often created with constants. If that's the case we will + // do the operation statically here. This is similar to what is done + // in masm.truncate32ToWasmI31Ref. + Register output = ToRegister(lir->output()); + uint32_t value = + static_cast<uint32_t>(lir->value()->toConstant()->toInt32()); + uintptr_t ptr = wasm::AnyRef::fromUint32Truncate(value).rawValue(); + masm.movePtr(ImmWord(ptr), output); + } else { + Register value = ToRegister(lir->value()); + Register output = ToRegister(lir->output()); + masm.truncate32ToWasmI31Ref(value, output); + } } void CodeGenerator::visitWasmI31RefGet(LWasmI31RefGet* lir) { diff --git a/js/src/jit/CodeGenerator.h b/js/src/jit/CodeGenerator.h index 274c876e4d..282771a79e 100644 --- a/js/src/jit/CodeGenerator.h +++ b/js/src/jit/CodeGenerator.h @@ -239,11 +239,34 @@ class CodeGenerator final : public CodeGeneratorSpecific { uint32_t extraFormals); void emitPushArrayAsArguments(Register tmpArgc, Register srcBaseAndArgc, Register scratch, size_t argvSrcOffset); - void emitPushArguments(LApplyArgsGeneric* apply, Register scratch); - void emitPushArguments(LApplyArgsObj* apply, Register scratch); - void emitPushArguments(LApplyArrayGeneric* apply, Register scratch); - void emitPushArguments(LConstructArgsGeneric* construct, Register scratch); - void emitPushArguments(LConstructArrayGeneric* construct, Register scratch); + void emitPushArguments(LApplyArgsGeneric* apply); + void emitPushArguments(LApplyArgsObj* apply); + void emitPushArguments(LApplyArrayGeneric* apply); + void emitPushArguments(LConstructArgsGeneric* construct); + void emitPushArguments(LConstructArrayGeneric* construct); + + template <typename T> + void emitApplyNative(T* apply); + template <typename T> + void emitCallInvokeNativeFunction(T* apply); + template <typename T> + void emitPushNativeArguments(T* apply); + template <typename T> + void emitPushArrayAsNativeArguments(T* apply); + void emitPushArguments(LApplyArgsNative* apply); + void emitPushArguments(LApplyArgsObjNative* apply); + void emitPushArguments(LApplyArrayNative* apply); + void emitPushArguments(LConstructArgsNative* construct); + void emitPushArguments(LConstructArrayNative* construct); + + template <typename T> + void emitApplyArgsGuard(T* apply); + + template <typename T> + void emitApplyArgsObjGuard(T* apply); + + template <typename T> + void emitApplyArrayGuard(T* apply); template <class GetInlinedArgument> void emitGetInlinedArgument(GetInlinedArgument* lir, Register index, @@ -439,6 +462,7 @@ class CodeGenerator final : public CodeGeneratorSpecific { // be mapped to an actual fuse by validateAndRegisterFuseDependencies. enum class FuseDependencyKind { HasSeenObjectEmulateUndefinedFuse, + OptimizeGetIteratorFuse, }; // The set of fuses this code generation depends on. @@ -449,6 +473,10 @@ class CodeGenerator final : public CodeGeneratorSpecific { fuseDependencies += FuseDependencyKind::HasSeenObjectEmulateUndefinedFuse; } + void addOptimizeGetIteratorFuseDependency() { + fuseDependencies += FuseDependencyKind::OptimizeGetIteratorFuse; + } + // Called during linking on main-thread: Ensures that the fuses are still // intact, and registers a script dependency on a specific fuse before // finishing compilation. diff --git a/js/src/jit/Ion.cpp b/js/src/jit/Ion.cpp index 85008006e1..e209ace846 100644 --- a/js/src/jit/Ion.cpp +++ b/js/src/jit/Ion.cpp @@ -253,6 +253,10 @@ bool JitRuntime::generateTrampolines(JSContext* cx) { generateIonGenericCallStub(masm, IonGenericCallKind::Construct); rangeRecorder.recordOffset("Trampoline: IonGenericConstruct"); + JitSpew(JitSpew_Codegen, "# Emitting trampoline natives"); + TrampolineNativeJitEntryOffsets nativeOffsets; + generateTrampolineNatives(masm, nativeOffsets, rangeRecorder); + Linker linker(masm); trampolineCode_ = linker.newCode(cx, CodeKind::Other); if (!trampolineCode_) { @@ -264,6 +268,14 @@ bool JitRuntime::generateTrampolines(JSContext* cx) { vtune::MarkStub(trampolineCode_, "Trampolines"); #endif + // Initialize TrampolineNative JitEntry array. + for (size_t i = 0; i < size_t(TrampolineNative::Count); i++) { + TrampolineNative native = TrampolineNative(i); + uint32_t offset = nativeOffsets[native]; + MOZ_ASSERT(offset > 0 && offset < trampolineCode_->instructionsSize()); + trampolineNativeJitEntries_[native] = trampolineCode_->raw() + offset; + } + return true; } @@ -2346,6 +2358,10 @@ static void InvalidateActivation(JS::GCContext* gcx, JitSpew(JitSpew_IonInvalidate, "#%zu rectifier frame @ %p", frameno, frame.fp()); break; + case FrameType::TrampolineNative: + JitSpew(JitSpew_IonInvalidate, "#%zu TrampolineNative frame @ %p", + frameno, frame.fp()); + break; case FrameType::IonICCall: JitSpew(JitSpew_IonInvalidate, "#%zu ion IC call frame @ %p", frameno, frame.fp()); diff --git a/js/src/jit/JSJitFrameIter.cpp b/js/src/jit/JSJitFrameIter.cpp index 89d3de3128..fbfef8f210 100644 --- a/js/src/jit/JSJitFrameIter.cpp +++ b/js/src/jit/JSJitFrameIter.cpp @@ -78,7 +78,7 @@ CalleeToken JSJitFrameIter::calleeToken() const { } JSFunction* JSJitFrameIter::callee() const { - MOZ_ASSERT(isScripted()); + MOZ_ASSERT(isScripted() || isTrampolineNative()); MOZ_ASSERT(isFunctionFrame()); return CalleeTokenToFunction(calleeToken()); } @@ -110,7 +110,7 @@ bool JSJitFrameIter::isFunctionFrame() const { JSScript* JSJitFrameIter::script() const { MOZ_ASSERT(isScripted()); - JSScript* script = ScriptFromCalleeToken(calleeToken()); + JSScript* script = MaybeForwardedScriptFromCalleeToken(calleeToken()); MOZ_ASSERT(script); return script; } @@ -383,6 +383,10 @@ void JSJitFrameIter::dump() const { fprintf(stderr, " Rectifier frame\n"); fprintf(stderr, " Caller frame ptr: %p\n", current()->callerFramePtr()); break; + case FrameType::TrampolineNative: + fprintf(stderr, " TrampolineNative frame\n"); + fprintf(stderr, " Caller frame ptr: %p\n", current()->callerFramePtr()); + break; case FrameType::IonICCall: fprintf(stderr, " Ion IC call\n"); fprintf(stderr, " Caller frame ptr: %p\n", current()->callerFramePtr()); @@ -707,47 +711,47 @@ void JSJitProfilingFrameIterator::moveToNextFrame(CommonFrameLayout* frame) { * | * ^--- WasmToJSJit <---- (other wasm frames, not handled by this iterator) * | - * ^--- Arguments Rectifier - * | ^ - * | | - * | ^--- Ion - * | | - * | ^--- Baseline Stub <---- Baseline - * | | - * | ^--- WasmToJSJit <--- (other wasm frames) - * | | - * | ^--- Entry Frame (CppToJSJit) + * ^--- Entry Frame (BaselineInterpreter) (unwrapped) * | - * ^--- Entry Frame (CppToJSJit) + * ^--- Arguments Rectifier (unwrapped) + * | + * ^--- Trampoline Native (unwrapped) * | - * ^--- Entry Frame (BaselineInterpreter) - * | ^ - * | | - * | ^--- Ion - * | | - * | ^--- Baseline Stub <---- Baseline - * | | - * | ^--- WasmToJSJit <--- (other wasm frames) - * | | - * | ^--- Entry Frame (CppToJSJit) - * | | - * | ^--- Arguments Rectifier + * ^--- Entry Frame (CppToJSJit) * * NOTE: Keep this in sync with JitRuntime::generateProfilerExitFrameTailStub! */ - // Unwrap baseline interpreter entry frame. - if (frame->prevType() == FrameType::BaselineInterpreterEntry) { - frame = GetPreviousRawFrame<BaselineInterpreterEntryFrameLayout*>(frame); - } + while (true) { + // Unwrap baseline interpreter entry frame. + if (frame->prevType() == FrameType::BaselineInterpreterEntry) { + frame = GetPreviousRawFrame<BaselineInterpreterEntryFrameLayout*>(frame); + continue; + } + + // Unwrap rectifier frames. + if (frame->prevType() == FrameType::Rectifier) { + frame = GetPreviousRawFrame<RectifierFrameLayout*>(frame); + MOZ_ASSERT(frame->prevType() == FrameType::IonJS || + frame->prevType() == FrameType::BaselineStub || + frame->prevType() == FrameType::TrampolineNative || + frame->prevType() == FrameType::WasmToJSJit || + frame->prevType() == FrameType::CppToJSJit); + continue; + } - // Unwrap rectifier frames. - if (frame->prevType() == FrameType::Rectifier) { - frame = GetPreviousRawFrame<RectifierFrameLayout*>(frame); - MOZ_ASSERT(frame->prevType() == FrameType::IonJS || - frame->prevType() == FrameType::BaselineStub || - frame->prevType() == FrameType::WasmToJSJit || - frame->prevType() == FrameType::CppToJSJit); + // Unwrap TrampolineNative frames. + if (frame->prevType() == FrameType::TrampolineNative) { + frame = GetPreviousRawFrame<TrampolineNativeFrameLayout*>(frame); + MOZ_ASSERT(frame->prevType() == FrameType::IonJS || + frame->prevType() == FrameType::BaselineStub || + frame->prevType() == FrameType::Rectifier || + frame->prevType() == FrameType::WasmToJSJit || + frame->prevType() == FrameType::CppToJSJit); + continue; + } + + break; } FrameType prevType = frame->prevType(); @@ -773,24 +777,31 @@ void JSJitProfilingFrameIterator::moveToNextFrame(CommonFrameLayout* frame) { } case FrameType::WasmToJSJit: - // No previous js jit frame, this is a transition frame, used to - // pass a wasm iterator the correct value of FP. + // No previous JS JIT frame. Set fp_ to nullptr to indicate the + // JSJitProfilingFrameIterator is done(). Also set wasmCallerFP_ so that + // the caller can pass it to a Wasm frame iterator. resumePCinCurrentFrame_ = nullptr; - fp_ = GetPreviousRawFrame<uint8_t*>(frame); + fp_ = nullptr; type_ = FrameType::WasmToJSJit; - MOZ_ASSERT(!done()); + MOZ_ASSERT(!wasmCallerFP_); + wasmCallerFP_ = GetPreviousRawFrame<uint8_t*>(frame); + MOZ_ASSERT(wasmCallerFP_); + MOZ_ASSERT(done()); return; case FrameType::CppToJSJit: - // No previous frame, set to nullptr to indicate that + // No previous JS JIT frame. Set fp_ to nullptr to indicate the // JSJitProfilingFrameIterator is done(). resumePCinCurrentFrame_ = nullptr; fp_ = nullptr; type_ = FrameType::CppToJSJit; + MOZ_ASSERT(!wasmCallerFP_); + MOZ_ASSERT(done()); return; case FrameType::BaselineInterpreterEntry: case FrameType::Rectifier: + case FrameType::TrampolineNative: case FrameType::Exit: case FrameType::Bailout: case FrameType::JSJitToWasm: diff --git a/js/src/jit/JSJitFrameIter.h b/js/src/jit/JSJitFrameIter.h index d40a533a20..03fd06852e 100644 --- a/js/src/jit/JSJitFrameIter.h +++ b/js/src/jit/JSJitFrameIter.h @@ -73,6 +73,10 @@ enum class FrameType { // wasm, and is a special kind of exit frame that doesn't have the exit // footer. From the point of view of the jit, it can be skipped as an exit. JSJitToWasm, + + // Frame for a TrampolineNative, a JS builtin implemented with a JIT + // trampoline. See jit/TrampolineNatives.h. + TrampolineNative, }; enum class ReadFrameArgsBehavior { @@ -173,6 +177,9 @@ class JSJitFrameIter { return type_ == FrameType::BaselineInterpreterEntry; } bool isRectifier() const { return type_ == FrameType::Rectifier; } + bool isTrampolineNative() const { + return type_ == FrameType::TrampolineNative; + } bool isBareExit() const; bool isUnwoundJitExit() const; template <typename T> @@ -263,6 +270,7 @@ class JitcodeGlobalTable; class JSJitProfilingFrameIterator { uint8_t* fp_; + uint8_t* wasmCallerFP_ = nullptr; // See JS::ProfilingFrameIterator::endStackAddress_ comment. void* endStackAddress_ = nullptr; FrameType type_; @@ -290,6 +298,11 @@ class JSJitProfilingFrameIterator { MOZ_ASSERT(!done()); return fp_; } + void* wasmCallerFP() const { + MOZ_ASSERT(done()); + MOZ_ASSERT(bool(wasmCallerFP_) == (type_ == FrameType::WasmToJSJit)); + return wasmCallerFP_; + } inline JitFrameLayout* framePtr() const; void* stackAddress() const { return fp(); } FrameType frameType() const { @@ -491,6 +504,42 @@ class SnapshotIterator { Value read() { return allocationValue(readAllocation()); } + int32_t readInt32() { + Value val = read(); + MOZ_RELEASE_ASSERT(val.isInt32()); + return val.toInt32(); + } + + double readNumber() { + Value val = read(); + MOZ_RELEASE_ASSERT(val.isNumber()); + return val.toNumber(); + } + + JSString* readString() { + Value val = read(); + MOZ_RELEASE_ASSERT(val.isString()); + return val.toString(); + } + + JS::BigInt* readBigInt() { + Value val = read(); + MOZ_RELEASE_ASSERT(val.isBigInt()); + return val.toBigInt(); + } + + JSObject* readObject() { + Value val = read(); + MOZ_RELEASE_ASSERT(val.isObject()); + return &val.toObject(); + } + + JS::GCCellPtr readGCCellPtr() { + Value val = read(); + MOZ_RELEASE_ASSERT(val.isGCThing()); + return val.toGCCellPtr(); + } + // Read the |Normal| value unless it is not available and that the snapshot // provides a |Default| value. This is useful to avoid invalidations of the // frame while we are only interested in a few properties which are provided diff --git a/js/src/jit/JitFrames.cpp b/js/src/jit/JitFrames.cpp index 176b988e05..45ac1f5def 100644 --- a/js/src/jit/JitFrames.cpp +++ b/js/src/jit/JitFrames.cpp @@ -83,6 +83,27 @@ static uint32_t NumArgAndLocalSlots(const InlineFrameIterator& frame) { return CountArgSlots(script, frame.maybeCalleeTemplate()) + script->nfixed(); } +static TrampolineNative TrampolineNativeForFrame( + JSRuntime* rt, TrampolineNativeFrameLayout* layout) { + JSFunction* nativeFun = CalleeTokenToFunction(layout->calleeToken()); + MOZ_ASSERT(nativeFun->isBuiltinNative()); + void** jitEntry = nativeFun->nativeJitEntry(); + return rt->jitRuntime()->trampolineNativeForJitEntry(jitEntry); +} + +static void UnwindTrampolineNativeFrame(JSRuntime* rt, + const JSJitFrameIter& frame) { + auto* layout = (TrampolineNativeFrameLayout*)frame.fp(); + TrampolineNative native = TrampolineNativeForFrame(rt, layout); + switch (native) { + case TrampolineNative::ArraySort: + layout->getFrameData<ArraySortData>()->freeMallocData(); + break; + case TrampolineNative::Count: + MOZ_CRASH("Invalid value"); + } +} + static void CloseLiveIteratorIon(JSContext* cx, const InlineFrameIterator& frame, const TryNote* tn) { @@ -739,7 +760,7 @@ void HandleException(ResumeFromException* rfe) { // JIT code can enter same-compartment realms, so reset cx->realm to // this frame's realm. - if (frame.isScripted()) { + if (frame.isScripted() || frame.isTrampolineNative()) { cx->setRealmForJitExceptionHandler(iter.realm()); } @@ -809,6 +830,8 @@ void HandleException(ResumeFromException* rfe) { if (rfe->kind == ExceptionResumeKind::ForcedReturnBaseline) { return; } + } else if (frame.isTrampolineNative()) { + UnwindTrampolineNativeFrame(cx->runtime(), frame); } prevJitFrame = frame.current(); @@ -910,12 +933,15 @@ static inline uintptr_t ReadAllocation(const JSJitFrameIter& frame, static void TraceThisAndArguments(JSTracer* trc, const JSJitFrameIter& frame, JitFrameLayout* layout) { - // Trace |this| and any extra actual arguments for an Ion frame. Tracing - // of formal arguments is taken care of by the frame's safepoint/snapshot, - // except when the script might have lazy arguments or rest, in which case - // we trace them as well. We also have to trace formals if we have a - // LazyLink frame or an InterpreterStub frame or a special JSJit to wasm - // frame (since wasm doesn't use snapshots). + // Trace |this| and the actual and formal arguments of a JIT frame. + // + // Tracing of formal arguments of an Ion frame is taken care of by the frame's + // safepoint/snapshot. We skip tracing formal arguments if the script doesn't + // use |arguments| or rest, because the register allocator can spill values to + // argument slots in this case. + // + // For other frames such as LazyLink frames or InterpreterStub frames, we + // always trace all actual and formal arguments. if (!CalleeTokenIsFunction(layout->calleeToken())) { return; @@ -927,8 +953,7 @@ static void TraceThisAndArguments(JSTracer* trc, const JSJitFrameIter& frame, size_t numArgs = std::max(layout->numActualArgs(), numFormals); size_t firstArg = 0; - if (frame.type() != FrameType::JSJitToWasm && - !frame.isExitFrameLayout<CalledFromJitExitFrameLayout>() && + if (frame.isIonScripted() && !fun->nonLazyScript()->mayReadFrameArgsDirectly()) { firstArg = numFormals; } @@ -936,17 +961,17 @@ static void TraceThisAndArguments(JSTracer* trc, const JSJitFrameIter& frame, Value* argv = layout->thisAndActualArgs(); // Trace |this|. - TraceRoot(trc, argv, "ion-thisv"); + TraceRoot(trc, argv, "jit-thisv"); // Trace arguments. Note + 1 for thisv. for (size_t i = firstArg; i < numArgs; i++) { - TraceRoot(trc, &argv[i + 1], "ion-argv"); + TraceRoot(trc, &argv[i + 1], "jit-argv"); } // Always trace the new.target from the frame. It's not in the snapshots. // +1 to pass |this| if (CalleeTokenIsConstructing(layout->calleeToken())) { - TraceRoot(trc, &argv[1 + numArgs], "ion-newTarget"); + TraceRoot(trc, &argv[1 + numArgs], "jit-newTarget"); } } @@ -1397,6 +1422,22 @@ static void TraceJSJitToWasmFrame(JSTracer* trc, const JSJitFrameIter& frame) { TraceThisAndArguments(trc, frame, layout); } +static void TraceTrampolineNativeFrame(JSTracer* trc, + const JSJitFrameIter& frame) { + auto* layout = (TrampolineNativeFrameLayout*)frame.fp(); + layout->replaceCalleeToken(TraceCalleeToken(trc, layout->calleeToken())); + TraceThisAndArguments(trc, frame, layout); + + TrampolineNative native = TrampolineNativeForFrame(trc->runtime(), layout); + switch (native) { + case TrampolineNative::ArraySort: + layout->getFrameData<ArraySortData>()->trace(trc); + break; + case TrampolineNative::Count: + MOZ_CRASH("Invalid value"); + } +} + static void TraceJitActivation(JSTracer* trc, JitActivation* activation) { #ifdef CHECK_OSIPOINT_REGISTERS if (JitOptions.checkOsiPointRegisters) { @@ -1439,6 +1480,9 @@ static void TraceJitActivation(JSTracer* trc, JitActivation* activation) { case FrameType::Rectifier: TraceRectifierFrame(trc, jitFrame); break; + case FrameType::TrampolineNative: + TraceTrampolineNativeFrame(trc, jitFrame); + break; case FrameType::IonICCall: TraceIonICCallFrame(trc, jitFrame); break; diff --git a/js/src/jit/JitFrames.h b/js/src/jit/JitFrames.h index ab882e7986..47c176492b 100644 --- a/js/src/jit/JitFrames.h +++ b/js/src/jit/JitFrames.h @@ -299,6 +299,17 @@ class RectifierFrameLayout : public JitFrameLayout { static inline size_t Size() { return sizeof(RectifierFrameLayout); } }; +class TrampolineNativeFrameLayout : public JitFrameLayout { + public: + static inline size_t Size() { return sizeof(TrampolineNativeFrameLayout); } + + template <typename T> + T* getFrameData() { + uint8_t* raw = reinterpret_cast<uint8_t*>(this) - sizeof(T); + return reinterpret_cast<T*>(raw); + } +}; + class WasmToJSJitFrameLayout : public JitFrameLayout { public: static inline size_t Size() { return sizeof(WasmToJSJitFrameLayout); } diff --git a/js/src/jit/JitOptions.cpp b/js/src/jit/JitOptions.cpp index e9d389cf60..053cf868a7 100644 --- a/js/src/jit/JitOptions.cpp +++ b/js/src/jit/JitOptions.cpp @@ -447,7 +447,8 @@ void DefaultJitOptions::resetNormalIonWarmUpThreshold() { void DefaultJitOptions::maybeSetWriteProtectCode(bool val) { #ifdef JS_USE_APPLE_FAST_WX - // On Apple Silicon we always use pthread_jit_write_protect_np. + // On Apple Silicon we always use pthread_jit_write_protect_np, or + // be_memory_inline_jit_restrict_*. MOZ_ASSERT(!writeProtectCode); #else writeProtectCode = val; diff --git a/js/src/jit/JitRuntime.h b/js/src/jit/JitRuntime.h index 7d038ed0e2..383efca437 100644 --- a/js/src/jit/JitRuntime.h +++ b/js/src/jit/JitRuntime.h @@ -27,6 +27,7 @@ #include "jit/JitCode.h" #include "jit/JitHints.h" #include "jit/shared/Assembler-shared.h" +#include "jit/TrampolineNatives.h" #include "js/AllocPolicy.h" #include "js/ProfilingFrameIterator.h" #include "js/TypeDecls.h" @@ -234,6 +235,13 @@ class JitRuntime { MainThreadData<IonCompileTaskList> ionLazyLinkList_; MainThreadData<size_t> ionLazyLinkListSize_{0}; + // Pointer to trampoline code for each TrampolineNative. The JSFunction has + // a JitEntry pointer that points to an item in this array. + using TrampolineNativeJitEntryArray = + mozilla::EnumeratedArray<TrampolineNative, void*, + size_t(TrampolineNative::Count)>; + TrampolineNativeJitEntryArray trampolineNativeJitEntries_{}; + #ifdef DEBUG // Flag that can be set from JIT code to indicate it's invalid to call // arbitrary JS code in a particular region. This is checked in RunScript. @@ -293,6 +301,14 @@ class JitRuntime { void generateBaselineInterpreterEntryTrampoline(MacroAssembler& masm); void generateInterpreterEntryTrampoline(MacroAssembler& masm); + using TrampolineNativeJitEntryOffsets = + mozilla::EnumeratedArray<TrampolineNative, uint32_t, + size_t(TrampolineNative::Count)>; + void generateTrampolineNatives(MacroAssembler& masm, + TrampolineNativeJitEntryOffsets& offsets, + PerfSpewerRangeRecorder& rangeRecorder); + uint32_t generateArraySortTrampoline(MacroAssembler& masm); + void bindLabelToOffset(Label* label, uint32_t offset) { MOZ_ASSERT(!trampolineCode_); label->bind(offset); @@ -418,6 +434,20 @@ class JitRuntime { return trampolineCode(ionGenericCallStubOffset_[kind]); } + void** trampolineNativeJitEntry(TrampolineNative native) { + void** jitEntry = &trampolineNativeJitEntries_[native]; + MOZ_ASSERT(*jitEntry >= trampolineCode_->raw()); + MOZ_ASSERT(*jitEntry < + trampolineCode_->raw() + trampolineCode_->instructionsSize()); + return jitEntry; + } + TrampolineNative trampolineNativeForJitEntry(void** entry) { + MOZ_RELEASE_ASSERT(entry >= trampolineNativeJitEntries_.begin()); + size_t index = entry - trampolineNativeJitEntries_.begin(); + MOZ_RELEASE_ASSERT(index < size_t(TrampolineNative::Count)); + return TrampolineNative(index); + } + bool hasJitcodeGlobalTable() const { return jitcodeGlobalTable_ != nullptr; } JitcodeGlobalTable* getJitcodeGlobalTable() { diff --git a/js/src/jit/LIROps.yaml b/js/src/jit/LIROps.yaml index f13c4b0745..880e756f74 100644 --- a/js/src/jit/LIROps.yaml +++ b/js/src/jit/LIROps.yaml @@ -632,6 +632,21 @@ - name: ConstructArrayGeneric gen_boilerplate: false +- name: ApplyArgsNative + gen_boilerplate: false + +- name: ApplyArgsObjNative + gen_boilerplate: false + +- name: ApplyArrayNative + gen_boilerplate: false + +- name: ConstructArgsNative + gen_boilerplate: false + +- name: ConstructArrayNative + gen_boilerplate: false + - name: TestIAndBranch gen_boilerplate: false @@ -2189,7 +2204,7 @@ mir_op: ClampToUint8 - name: LoadScriptedProxyHandler - result_type: BoxedValue + result_type: WordSized operands: object: WordSized mir_op: true @@ -3694,6 +3709,7 @@ result_type: WordSized operands: object: WordSized + mir_op: true - name: GuardHasGetterSetter operands: diff --git a/js/src/jit/Lowering.cpp b/js/src/jit/Lowering.cpp index b0007a114d..f7b898f240 100644 --- a/js/src/jit/Lowering.cpp +++ b/js/src/jit/Lowering.cpp @@ -654,12 +654,23 @@ void LIRGenerator::visitApplyArgs(MApplyArgs* apply) { static_assert(CallTempReg2 != JSReturnReg_Type); static_assert(CallTempReg2 != JSReturnReg_Data); - LApplyArgsGeneric* lir = new (alloc()) LApplyArgsGeneric( - useFixedAtStart(apply->getFunction(), CallTempReg3), - useFixedAtStart(apply->getArgc(), CallTempReg0), - useBoxFixedAtStart(apply->getThis(), CallTempReg4, CallTempReg5), - tempFixed(CallTempReg1), // object register - tempFixed(CallTempReg2)); // stack counter register + auto function = useFixedAtStart(apply->getFunction(), CallTempReg3); + auto argc = useFixedAtStart(apply->getArgc(), CallTempReg0); + auto thisValue = + useBoxFixedAtStart(apply->getThis(), CallTempReg4, CallTempReg5); + auto tempObj = tempFixed(CallTempReg1); // object register + auto tempCopy = tempFixed(CallTempReg2); // copy register + + auto* target = apply->getSingleTarget(); + + LInstruction* lir; + if (target && target->isNativeWithoutJitEntry()) { + lir = new (alloc()) + LApplyArgsNative(function, argc, thisValue, tempObj, tempCopy); + } else { + lir = new (alloc()) + LApplyArgsGeneric(function, argc, thisValue, tempObj, tempCopy); + } // Bailout is needed in the case of too many values in the arguments array. assignSnapshot(lir, apply->bailoutKind()); @@ -675,12 +686,23 @@ void LIRGenerator::visitApplyArgsObj(MApplyArgsObj* apply) { static_assert(CallTempReg2 != JSReturnReg_Type); static_assert(CallTempReg2 != JSReturnReg_Data); - LApplyArgsObj* lir = new (alloc()) LApplyArgsObj( - useFixedAtStart(apply->getFunction(), CallTempReg3), - useFixedAtStart(apply->getArgsObj(), CallTempReg0), - useBoxFixedAtStart(apply->getThis(), CallTempReg4, CallTempReg5), - tempFixed(CallTempReg1), // object register - tempFixed(CallTempReg2)); // stack counter register + auto function = useFixedAtStart(apply->getFunction(), CallTempReg3); + auto argsObj = useFixedAtStart(apply->getArgsObj(), CallTempReg0); + auto thisValue = + useBoxFixedAtStart(apply->getThis(), CallTempReg4, CallTempReg5); + auto tempObj = tempFixed(CallTempReg1); // object register + auto tempCopy = tempFixed(CallTempReg2); // copy register + + auto* target = apply->getSingleTarget(); + + LInstruction* lir; + if (target && target->isNativeWithoutJitEntry()) { + lir = new (alloc()) + LApplyArgsObjNative(function, argsObj, thisValue, tempObj, tempCopy); + } else { + lir = new (alloc()) + LApplyArgsObj(function, argsObj, thisValue, tempObj, tempCopy); + } // Bailout is needed in the case of too many values in the arguments array. assignSnapshot(lir, apply->bailoutKind()); @@ -696,12 +718,23 @@ void LIRGenerator::visitApplyArray(MApplyArray* apply) { static_assert(CallTempReg2 != JSReturnReg_Type); static_assert(CallTempReg2 != JSReturnReg_Data); - LApplyArrayGeneric* lir = new (alloc()) LApplyArrayGeneric( - useFixedAtStart(apply->getFunction(), CallTempReg3), - useFixedAtStart(apply->getElements(), CallTempReg0), - useBoxFixedAtStart(apply->getThis(), CallTempReg4, CallTempReg5), - tempFixed(CallTempReg1), // object register - tempFixed(CallTempReg2)); // stack counter register + auto function = useFixedAtStart(apply->getFunction(), CallTempReg3); + auto elements = useFixedAtStart(apply->getElements(), CallTempReg0); + auto thisValue = + useBoxFixedAtStart(apply->getThis(), CallTempReg4, CallTempReg5); + auto tempObj = tempFixed(CallTempReg1); // object register + auto tempCopy = tempFixed(CallTempReg2); // copy register + + auto* target = apply->getSingleTarget(); + + LInstruction* lir; + if (target && target->isNativeWithoutJitEntry()) { + lir = new (alloc()) + LApplyArrayNative(function, elements, thisValue, tempObj, tempCopy); + } else { + lir = new (alloc()) + LApplyArrayGeneric(function, elements, thisValue, tempObj, tempCopy); + } // Bailout is needed in the case of too many values in the array, or empty // space at the end of the array. @@ -721,12 +754,26 @@ void LIRGenerator::visitConstructArgs(MConstructArgs* mir) { static_assert(CallTempReg2 != JSReturnReg_Type); static_assert(CallTempReg2 != JSReturnReg_Data); - auto* lir = new (alloc()) LConstructArgsGeneric( - useFixedAtStart(mir->getFunction(), CallTempReg3), - useFixedAtStart(mir->getArgc(), CallTempReg0), - useFixedAtStart(mir->getNewTarget(), CallTempReg1), - useBoxFixedAtStart(mir->getThis(), CallTempReg4, CallTempReg5), - tempFixed(CallTempReg2)); + auto function = useFixedAtStart(mir->getFunction(), CallTempReg3); + auto argc = useFixedAtStart(mir->getArgc(), CallTempReg0); + auto newTarget = useFixedAtStart(mir->getNewTarget(), CallTempReg1); + auto temp = tempFixed(CallTempReg2); + + auto* target = mir->getSingleTarget(); + + LInstruction* lir; + if (target && target->isNativeWithoutJitEntry()) { + auto temp2 = tempFixed(CallTempReg4); + + lir = new (alloc()) + LConstructArgsNative(function, argc, newTarget, temp, temp2); + } else { + auto thisValue = + useBoxFixedAtStart(mir->getThis(), CallTempReg4, CallTempReg5); + + lir = new (alloc()) + LConstructArgsGeneric(function, argc, newTarget, thisValue, temp); + } // Bailout is needed in the case of too many values in the arguments array. assignSnapshot(lir, mir->bailoutKind()); @@ -745,12 +792,26 @@ void LIRGenerator::visitConstructArray(MConstructArray* mir) { static_assert(CallTempReg2 != JSReturnReg_Type); static_assert(CallTempReg2 != JSReturnReg_Data); - auto* lir = new (alloc()) LConstructArrayGeneric( - useFixedAtStart(mir->getFunction(), CallTempReg3), - useFixedAtStart(mir->getElements(), CallTempReg0), - useFixedAtStart(mir->getNewTarget(), CallTempReg1), - useBoxFixedAtStart(mir->getThis(), CallTempReg4, CallTempReg5), - tempFixed(CallTempReg2)); + auto function = useFixedAtStart(mir->getFunction(), CallTempReg3); + auto elements = useFixedAtStart(mir->getElements(), CallTempReg0); + auto newTarget = useFixedAtStart(mir->getNewTarget(), CallTempReg1); + auto temp = tempFixed(CallTempReg2); + + auto* target = mir->getSingleTarget(); + + LInstruction* lir; + if (target && target->isNativeWithoutJitEntry()) { + auto temp2 = tempFixed(CallTempReg4); + + lir = new (alloc()) + LConstructArrayNative(function, elements, newTarget, temp, temp2); + } else { + auto thisValue = + useBoxFixedAtStart(mir->getThis(), CallTempReg4, CallTempReg5); + + lir = new (alloc()) + LConstructArrayGeneric(function, elements, newTarget, thisValue, temp); + } // Bailout is needed in the case of too many values in the array, or empty // space at the end of the array. @@ -3241,7 +3302,9 @@ void LIRGenerator::visitWasmAnyRefFromJSString(MWasmAnyRefFromJSString* ins) { } void LIRGenerator::visitWasmNewI31Ref(MWasmNewI31Ref* ins) { - LWasmNewI31Ref* lir = new (alloc()) LWasmNewI31Ref(useRegister(ins->input())); + // If it's a constant, it will be put directly into the register. + LWasmNewI31Ref* lir = + new (alloc()) LWasmNewI31Ref(useRegisterOrConstant(ins->input())); define(lir, ins); } @@ -4686,7 +4749,8 @@ void LIRGenerator::visitLoadScriptedProxyHandler( MLoadScriptedProxyHandler* ins) { LLoadScriptedProxyHandler* lir = new (alloc()) LLoadScriptedProxyHandler(useRegisterAtStart(ins->object())); - defineBox(lir, ins); + assignSnapshot(lir, ins->bailoutKind()); + define(lir, ins); } void LIRGenerator::visitIdToStringOrSymbol(MIdToStringOrSymbol* ins) { @@ -6750,7 +6814,11 @@ void LIRGenerator::visitLoadWrapperTarget(MLoadWrapperTarget* ins) { MDefinition* object = ins->object(); MOZ_ASSERT(object->type() == MIRType::Object); - define(new (alloc()) LLoadWrapperTarget(useRegisterAtStart(object)), ins); + auto* lir = new (alloc()) LLoadWrapperTarget(useRegisterAtStart(object)); + if (ins->fallible()) { + assignSnapshot(lir, ins->bailoutKind()); + } + define(lir, ins); } void LIRGenerator::visitGuardHasGetterSetter(MGuardHasGetterSetter* ins) { diff --git a/js/src/jit/MIR.cpp b/js/src/jit/MIR.cpp index c6daecb166..a74406567b 100644 --- a/js/src/jit/MIR.cpp +++ b/js/src/jit/MIR.cpp @@ -689,7 +689,62 @@ MDefinition* MTest::foldsNeedlessControlFlow(TempAllocator& alloc) { return MGoto::New(alloc, ifTrue()); } +// If a test is dominated by either the true or false path of a previous test of +// the same condition, then the test is redundant and can be converted into a +// goto true or goto false, respectively. +MDefinition* MTest::foldsRedundantTest(TempAllocator& alloc) { + MBasicBlock* myBlock = this->block(); + MDefinition* originalInput = getOperand(0); + + // Handle single and double negatives. This ensures that we do not miss a + // folding opportunity due to a condition being inverted. + MDefinition* newInput = input(); + bool inverted = false; + if (originalInput->isNot()) { + newInput = originalInput->toNot()->input(); + inverted = true; + if (originalInput->toNot()->input()->isNot()) { + newInput = originalInput->toNot()->input()->toNot()->input(); + inverted = false; + } + } + + // The specific order of traversal does not matter. If there are multiple + // dominating redundant tests, they will either agree on direction (in which + // case we will prune the same way regardless of order), or they will + // disagree, in which case we will eventually be marked entirely dead by the + // folding of the redundant parent. + for (MUseIterator i(newInput->usesBegin()), e(newInput->usesEnd()); i != e; + ++i) { + if (!i->consumer()->isDefinition()) { + continue; + } + if (!i->consumer()->toDefinition()->isTest()) { + continue; + } + MTest* otherTest = i->consumer()->toDefinition()->toTest(); + if (otherTest == this) { + continue; + } + + if (otherTest->ifFalse()->dominates(myBlock)) { + // This test cannot be true, so fold to a goto false. + return MGoto::New(alloc, inverted ? ifTrue() : ifFalse()); + } + if (otherTest->ifTrue()->dominates(myBlock)) { + // This test cannot be false, so fold to a goto true. + return MGoto::New(alloc, inverted ? ifFalse() : ifTrue()); + } + } + + return nullptr; +} + MDefinition* MTest::foldsTo(TempAllocator& alloc) { + if (MDefinition* def = foldsRedundantTest(alloc)) { + return def; + } + if (MDefinition* def = foldsDoubleNegation(alloc)) { return def; } @@ -7187,6 +7242,16 @@ AliasSet MLoadWrapperTarget::getAliasSet() const { return AliasSet::Load(AliasSet::Any); } +bool MLoadWrapperTarget::congruentTo(const MDefinition* ins) const { + if (!ins->isLoadWrapperTarget()) { + return false; + } + if (ins->toLoadWrapperTarget()->fallible() != fallible()) { + return false; + } + return congruentIfOperandsEqual(ins); +} + AliasSet MGuardHasGetterSetter::getAliasSet() const { return AliasSet::Load(AliasSet::ObjectFields); } diff --git a/js/src/jit/MIR.h b/js/src/jit/MIR.h index d882665a65..c672092f04 100644 --- a/js/src/jit/MIR.h +++ b/js/src/jit/MIR.h @@ -1890,6 +1890,7 @@ class MTest : public MAryControlInstruction<1, 2>, public TestPolicy::Data { MDefinition* foldsConstant(TempAllocator& alloc); MDefinition* foldsTypes(TempAllocator& alloc); MDefinition* foldsNeedlessControlFlow(TempAllocator& alloc); + MDefinition* foldsRedundantTest(TempAllocator& alloc); MDefinition* foldsTo(TempAllocator& alloc) override; #ifdef DEBUG @@ -2309,7 +2310,7 @@ class WrappedFunction : public TempObject { return nativeFun_->nativeUnchecked(); } bool hasJitInfo() const { - return flags_.isBuiltinNative() && nativeFun_->jitInfoUnchecked(); + return flags_.canHaveJitInfo() && nativeFun_->jitInfoUnchecked(); } const JSJitInfo* jitInfo() const { MOZ_ASSERT(hasJitInfo()); diff --git a/js/src/jit/MIROps.yaml b/js/src/jit/MIROps.yaml index 7f0df52742..78ab989221 100644 --- a/js/src/jit/MIROps.yaml +++ b/js/src/jit/MIROps.yaml @@ -539,7 +539,8 @@ - name: LoadScriptedProxyHandler operands: object: Object - result_type: Value + result_type: Object + guard: true congruent_to: if_operands_equal alias_set: none @@ -1421,8 +1422,6 @@ index: Int32 type_policy: none alias_set: custom - # By default no, unless built as a recovered instruction. - can_recover: custom # Load the function length. Bails for functions with lazy scripts or a # resolved "length" property. @@ -2810,13 +2809,16 @@ alias_set: none # Load the target object from a proxy wrapper. The target is stored in the -# proxy object's private slot. +# proxy object's private slot. This operation is fallible if the proxy can +# be revoked. - name: LoadWrapperTarget operands: object: Object + arguments: + fallible: bool result_type: Object movable: true - congruent_to: if_operands_equal + congruent_to: custom # Can't use |AliasSet::None| because the target changes on navigation. # TODO: Investigate using a narrower or a custom alias set. alias_set: custom diff --git a/js/src/jit/MacroAssembler.cpp b/js/src/jit/MacroAssembler.cpp index 3b094d49dc..9fc4b96830 100644 --- a/js/src/jit/MacroAssembler.cpp +++ b/js/src/jit/MacroAssembler.cpp @@ -3169,6 +3169,8 @@ void MacroAssembler::emitMegamorphicCachedSetSlot( passABIArg(scratch2); callWithABI<Fn, NativeObject::growSlotsPure>(); storeCallPointerResult(scratch2); + + MOZ_ASSERT(!save.has(scratch2)); PopRegsInMask(save); branchIfFalseBool(scratch2, &cacheMiss); @@ -7803,6 +7805,24 @@ void MacroAssembler::loadArgumentsObjectLength(Register obj, Register output, rshift32(Imm32(ArgumentsObject::PACKED_BITS_COUNT), output); } +void MacroAssembler::loadArgumentsObjectLength(Register obj, Register output) { + // Get initial length value. + unboxInt32(Address(obj, ArgumentsObject::getInitialLengthSlotOffset()), + output); + +#ifdef DEBUG + // Assert length hasn't been overridden. + Label ok; + branchTest32(Assembler::Zero, output, + Imm32(ArgumentsObject::LENGTH_OVERRIDDEN_BIT), &ok); + assumeUnreachable("arguments object length has been overridden"); + bind(&ok); +#endif + + // Shift out arguments length and return it. + rshift32(Imm32(ArgumentsObject::PACKED_BITS_COUNT), output); +} + void MacroAssembler::branchTestArgumentsObjectFlags(Register obj, Register temp, uint32_t flags, Condition cond, diff --git a/js/src/jit/MacroAssembler.h b/js/src/jit/MacroAssembler.h index 361de3ac5f..114aaa47d7 100644 --- a/js/src/jit/MacroAssembler.h +++ b/js/src/jit/MacroAssembler.h @@ -5291,6 +5291,7 @@ class MacroAssembler : public MacroAssemblerSpecific { Label* fail); void loadArgumentsObjectLength(Register obj, Register output, Label* fail); + void loadArgumentsObjectLength(Register obj, Register output); void branchTestArgumentsObjectFlags(Register obj, Register temp, uint32_t flags, Condition cond, diff --git a/js/src/jit/PerfSpewer.cpp b/js/src/jit/PerfSpewer.cpp index 81954f3d92..c9d9cc8d88 100644 --- a/js/src/jit/PerfSpewer.cpp +++ b/js/src/jit/PerfSpewer.cpp @@ -23,7 +23,7 @@ # define gettid() static_cast<pid_t>(syscall(__NR_gettid)) #endif -#if defined(JS_ION_PERF) && (defined(ANDROID) || defined(XP_MACOSX)) +#if defined(JS_ION_PERF) && (defined(ANDROID) || defined(XP_DARWIN)) # include <limits.h> # include <stdlib.h> # include <unistd.h> @@ -42,7 +42,7 @@ char* get_current_dir_name() { } #endif -#if defined(JS_ION_PERF) && defined(XP_MACOSX) +#if defined(JS_ION_PERF) && defined(XP_DARWIN) # include <pthread.h> # include <unistd.h> @@ -128,7 +128,7 @@ static uint64_t GetMonotonicTimestamp() { return TimeStamp::Now().RawClockMonotonicNanosecondsSinceBoot(); # elif XP_WIN return TimeStamp::Now().RawQueryPerformanceCounterValue().value(); -# elif XP_MACOSX +# elif XP_DARWIN return TimeStamp::Now().RawMachAbsoluteTimeNanoseconds(); # else MOZ_CRASH("no timestamp"); diff --git a/js/src/jit/ProcessExecutableMemory.cpp b/js/src/jit/ProcessExecutableMemory.cpp index 830d15f7fb..0c00b17c73 100644 --- a/js/src/jit/ProcessExecutableMemory.cpp +++ b/js/src/jit/ProcessExecutableMemory.cpp @@ -46,6 +46,10 @@ # include <valgrind/valgrind.h> #endif +#if defined(XP_IOS) +# include <BrowserEngineCore/BEMemory.h> +#endif + using namespace js; using namespace js::jit; @@ -990,11 +994,19 @@ bool js::jit::ReprotectRegion(void* start, size_t size, #ifdef JS_USE_APPLE_FAST_WX void js::jit::AutoMarkJitCodeWritableForThread::markExecutable( bool executable) { +# if defined(XP_IOS) + if (executable) { + be_memory_inline_jit_restrict_rwx_to_rx_with_witness(); + } else { + be_memory_inline_jit_restrict_rwx_to_rw_with_witness(); + } +# else if (__builtin_available(macOS 11.0, *)) { pthread_jit_write_protect_np(executable); } else { MOZ_CRASH("pthread_jit_write_protect_np must be available"); } +# endif } #endif diff --git a/js/src/jit/Recover.cpp b/js/src/jit/Recover.cpp index 220ffe7bb2..4c1ff56436 100644 --- a/js/src/jit/Recover.cpp +++ b/js/src/jit/Recover.cpp @@ -6,6 +6,8 @@ #include "jit/Recover.h" +#include "mozilla/Casting.h" + #include "jsmath.h" #include "builtin/Object.h" @@ -495,16 +497,15 @@ bool MBigIntAdd::writeRecoverData(CompactBufferWriter& writer) const { RBigIntAdd::RBigIntAdd(CompactBufferReader& reader) {} bool RBigIntAdd::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedValue lhs(cx, iter.read()); - RootedValue rhs(cx, iter.read()); - RootedValue result(cx); + Rooted<BigInt*> lhs(cx, iter.readBigInt()); + Rooted<BigInt*> rhs(cx, iter.readBigInt()); - MOZ_ASSERT(lhs.isBigInt() && rhs.isBigInt()); - if (!js::AddValues(cx, &lhs, &rhs, &result)) { + BigInt* result = BigInt::add(cx, lhs, rhs); + if (!result) { return false; } - iter.storeInstructionResult(result); + iter.storeInstructionResult(BigIntValue(result)); return true; } @@ -517,16 +518,15 @@ bool MBigIntSub::writeRecoverData(CompactBufferWriter& writer) const { RBigIntSub::RBigIntSub(CompactBufferReader& reader) {} bool RBigIntSub::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedValue lhs(cx, iter.read()); - RootedValue rhs(cx, iter.read()); - RootedValue result(cx); + Rooted<BigInt*> lhs(cx, iter.readBigInt()); + Rooted<BigInt*> rhs(cx, iter.readBigInt()); - MOZ_ASSERT(lhs.isBigInt() && rhs.isBigInt()); - if (!js::SubValues(cx, &lhs, &rhs, &result)) { + BigInt* result = BigInt::sub(cx, lhs, rhs); + if (!result) { return false; } - iter.storeInstructionResult(result); + iter.storeInstructionResult(BigIntValue(result)); return true; } @@ -539,16 +539,15 @@ bool MBigIntMul::writeRecoverData(CompactBufferWriter& writer) const { RBigIntMul::RBigIntMul(CompactBufferReader& reader) {} bool RBigIntMul::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedValue lhs(cx, iter.read()); - RootedValue rhs(cx, iter.read()); - RootedValue result(cx); + Rooted<BigInt*> lhs(cx, iter.readBigInt()); + Rooted<BigInt*> rhs(cx, iter.readBigInt()); - MOZ_ASSERT(lhs.isBigInt() && rhs.isBigInt()); - if (!js::MulValues(cx, &lhs, &rhs, &result)) { + BigInt* result = BigInt::mul(cx, lhs, rhs); + if (!result) { return false; } - iter.storeInstructionResult(result); + iter.storeInstructionResult(BigIntValue(result)); return true; } @@ -561,18 +560,17 @@ bool MBigIntDiv::writeRecoverData(CompactBufferWriter& writer) const { RBigIntDiv::RBigIntDiv(CompactBufferReader& reader) {} bool RBigIntDiv::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedValue lhs(cx, iter.read()); - RootedValue rhs(cx, iter.read()); - RootedValue result(cx); - - MOZ_ASSERT(lhs.isBigInt() && rhs.isBigInt()); - MOZ_ASSERT(!rhs.toBigInt()->isZero(), + Rooted<BigInt*> lhs(cx, iter.readBigInt()); + Rooted<BigInt*> rhs(cx, iter.readBigInt()); + MOZ_ASSERT(!rhs->isZero(), "division by zero throws and therefore can't be recovered"); - if (!js::DivValues(cx, &lhs, &rhs, &result)) { + + BigInt* result = BigInt::div(cx, lhs, rhs); + if (!result) { return false; } - iter.storeInstructionResult(result); + iter.storeInstructionResult(BigIntValue(result)); return true; } @@ -585,18 +583,17 @@ bool MBigIntMod::writeRecoverData(CompactBufferWriter& writer) const { RBigIntMod::RBigIntMod(CompactBufferReader& reader) {} bool RBigIntMod::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedValue lhs(cx, iter.read()); - RootedValue rhs(cx, iter.read()); - RootedValue result(cx); - - MOZ_ASSERT(lhs.isBigInt() && rhs.isBigInt()); - MOZ_ASSERT(!rhs.toBigInt()->isZero(), + Rooted<BigInt*> lhs(cx, iter.readBigInt()); + Rooted<BigInt*> rhs(cx, iter.readBigInt()); + MOZ_ASSERT(!rhs->isZero(), "division by zero throws and therefore can't be recovered"); - if (!js::ModValues(cx, &lhs, &rhs, &result)) { + + BigInt* result = BigInt::mod(cx, lhs, rhs); + if (!result) { return false; } - iter.storeInstructionResult(result); + iter.storeInstructionResult(BigIntValue(result)); return true; } @@ -609,18 +606,17 @@ bool MBigIntPow::writeRecoverData(CompactBufferWriter& writer) const { RBigIntPow::RBigIntPow(CompactBufferReader& reader) {} bool RBigIntPow::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedValue lhs(cx, iter.read()); - RootedValue rhs(cx, iter.read()); - RootedValue result(cx); - - MOZ_ASSERT(lhs.isBigInt() && rhs.isBigInt()); - MOZ_ASSERT(!rhs.toBigInt()->isNegative(), + Rooted<BigInt*> lhs(cx, iter.readBigInt()); + Rooted<BigInt*> rhs(cx, iter.readBigInt()); + MOZ_ASSERT(!rhs->isNegative(), "negative exponent throws and therefore can't be recovered"); - if (!js::PowValues(cx, &lhs, &rhs, &result)) { + + BigInt* result = BigInt::pow(cx, lhs, rhs); + if (!result) { return false; } - iter.storeInstructionResult(result); + iter.storeInstructionResult(BigIntValue(result)); return true; } @@ -633,16 +629,15 @@ bool MBigIntBitAnd::writeRecoverData(CompactBufferWriter& writer) const { RBigIntBitAnd::RBigIntBitAnd(CompactBufferReader& reader) {} bool RBigIntBitAnd::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedValue lhs(cx, iter.read()); - RootedValue rhs(cx, iter.read()); - RootedValue result(cx); + Rooted<BigInt*> lhs(cx, iter.readBigInt()); + Rooted<BigInt*> rhs(cx, iter.readBigInt()); - MOZ_ASSERT(lhs.isBigInt() && rhs.isBigInt()); - if (!js::BitAnd(cx, &lhs, &rhs, &result)) { + BigInt* result = BigInt::bitAnd(cx, lhs, rhs); + if (!result) { return false; } - iter.storeInstructionResult(result); + iter.storeInstructionResult(BigIntValue(result)); return true; } @@ -655,16 +650,15 @@ bool MBigIntBitOr::writeRecoverData(CompactBufferWriter& writer) const { RBigIntBitOr::RBigIntBitOr(CompactBufferReader& reader) {} bool RBigIntBitOr::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedValue lhs(cx, iter.read()); - RootedValue rhs(cx, iter.read()); - RootedValue result(cx); + Rooted<BigInt*> lhs(cx, iter.readBigInt()); + Rooted<BigInt*> rhs(cx, iter.readBigInt()); - MOZ_ASSERT(lhs.isBigInt() && rhs.isBigInt()); - if (!js::BitOr(cx, &lhs, &rhs, &result)) { + BigInt* result = BigInt::bitOr(cx, lhs, rhs); + if (!result) { return false; } - iter.storeInstructionResult(result); + iter.storeInstructionResult(BigIntValue(result)); return true; } @@ -677,16 +671,15 @@ bool MBigIntBitXor::writeRecoverData(CompactBufferWriter& writer) const { RBigIntBitXor::RBigIntBitXor(CompactBufferReader& reader) {} bool RBigIntBitXor::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedValue lhs(cx, iter.read()); - RootedValue rhs(cx, iter.read()); - RootedValue result(cx); + Rooted<BigInt*> lhs(cx, iter.readBigInt()); + Rooted<BigInt*> rhs(cx, iter.readBigInt()); - MOZ_ASSERT(lhs.isBigInt() && rhs.isBigInt()); - if (!js::BitXor(cx, &lhs, &rhs, &result)) { + BigInt* result = BigInt::bitXor(cx, lhs, rhs); + if (!result) { return false; } - iter.storeInstructionResult(result); + iter.storeInstructionResult(BigIntValue(result)); return true; } @@ -699,16 +692,15 @@ bool MBigIntLsh::writeRecoverData(CompactBufferWriter& writer) const { RBigIntLsh::RBigIntLsh(CompactBufferReader& reader) {} bool RBigIntLsh::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedValue lhs(cx, iter.read()); - RootedValue rhs(cx, iter.read()); - RootedValue result(cx); + Rooted<BigInt*> lhs(cx, iter.readBigInt()); + Rooted<BigInt*> rhs(cx, iter.readBigInt()); - MOZ_ASSERT(lhs.isBigInt() && rhs.isBigInt()); - if (!js::BitLsh(cx, &lhs, &rhs, &result)) { + BigInt* result = BigInt::lsh(cx, lhs, rhs); + if (!result) { return false; } - iter.storeInstructionResult(result); + iter.storeInstructionResult(BigIntValue(result)); return true; } @@ -721,16 +713,15 @@ bool MBigIntRsh::writeRecoverData(CompactBufferWriter& writer) const { RBigIntRsh::RBigIntRsh(CompactBufferReader& reader) {} bool RBigIntRsh::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedValue lhs(cx, iter.read()); - RootedValue rhs(cx, iter.read()); - RootedValue result(cx); + Rooted<BigInt*> lhs(cx, iter.readBigInt()); + Rooted<BigInt*> rhs(cx, iter.readBigInt()); - MOZ_ASSERT(lhs.isBigInt() && rhs.isBigInt()); - if (!js::BitRsh(cx, &lhs, &rhs, &result)) { + BigInt* result = BigInt::rsh(cx, lhs, rhs); + if (!result) { return false; } - iter.storeInstructionResult(result); + iter.storeInstructionResult(BigIntValue(result)); return true; } @@ -743,15 +734,14 @@ bool MBigIntIncrement::writeRecoverData(CompactBufferWriter& writer) const { RBigIntIncrement::RBigIntIncrement(CompactBufferReader& reader) {} bool RBigIntIncrement::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedValue operand(cx, iter.read()); - RootedValue result(cx); + Rooted<BigInt*> operand(cx, iter.readBigInt()); - MOZ_ASSERT(operand.isBigInt()); - if (!js::IncOperation(cx, operand, &result)) { + BigInt* result = BigInt::inc(cx, operand); + if (!result) { return false; } - iter.storeInstructionResult(result); + iter.storeInstructionResult(BigIntValue(result)); return true; } @@ -764,15 +754,14 @@ bool MBigIntDecrement::writeRecoverData(CompactBufferWriter& writer) const { RBigIntDecrement::RBigIntDecrement(CompactBufferReader& reader) {} bool RBigIntDecrement::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedValue operand(cx, iter.read()); - RootedValue result(cx); + Rooted<BigInt*> operand(cx, iter.readBigInt()); - MOZ_ASSERT(operand.isBigInt()); - if (!js::DecOperation(cx, operand, &result)) { + BigInt* result = BigInt::dec(cx, operand); + if (!result) { return false; } - iter.storeInstructionResult(result); + iter.storeInstructionResult(BigIntValue(result)); return true; } @@ -785,15 +774,14 @@ bool MBigIntNegate::writeRecoverData(CompactBufferWriter& writer) const { RBigIntNegate::RBigIntNegate(CompactBufferReader& reader) {} bool RBigIntNegate::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedValue operand(cx, iter.read()); - RootedValue result(cx); + Rooted<BigInt*> operand(cx, iter.readBigInt()); - MOZ_ASSERT(operand.isBigInt()); - if (!js::NegOperation(cx, &operand, &result)) { + BigInt* result = BigInt::neg(cx, operand); + if (!result) { return false; } - iter.storeInstructionResult(result); + iter.storeInstructionResult(BigIntValue(result)); return true; } @@ -806,15 +794,14 @@ bool MBigIntBitNot::writeRecoverData(CompactBufferWriter& writer) const { RBigIntBitNot::RBigIntBitNot(CompactBufferReader& reader) {} bool RBigIntBitNot::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedValue operand(cx, iter.read()); - RootedValue result(cx); + Rooted<BigInt*> operand(cx, iter.readBigInt()); - MOZ_ASSERT(operand.isBigInt()); - if (!js::BitNot(cx, &operand, &result)) { + BigInt* result = BigInt::bitNot(cx, operand); + if (!result) { return false; } - iter.storeInstructionResult(result); + iter.storeInstructionResult(BigIntValue(result)); return true; } @@ -910,7 +897,7 @@ bool RConcat::recover(JSContext* cx, SnapshotIterator& iter) const { RStringLength::RStringLength(CompactBufferReader& reader) {} bool RStringLength::recover(JSContext* cx, SnapshotIterator& iter) const { - JSString* string = iter.read().toString(); + JSString* string = iter.readString(); static_assert(JSString::MAX_LENGTH <= INT32_MAX, "Can cast string length to int32_t"); @@ -953,7 +940,7 @@ bool MFloor::writeRecoverData(CompactBufferWriter& writer) const { RFloor::RFloor(CompactBufferReader& reader) {} bool RFloor::recover(JSContext* cx, SnapshotIterator& iter) const { - double num = iter.read().toNumber(); + double num = iter.readNumber(); double result = js::math_floor_impl(num); iter.storeInstructionResult(NumberValue(result)); @@ -969,7 +956,7 @@ bool MCeil::writeRecoverData(CompactBufferWriter& writer) const { RCeil::RCeil(CompactBufferReader& reader) {} bool RCeil::recover(JSContext* cx, SnapshotIterator& iter) const { - double num = iter.read().toNumber(); + double num = iter.readNumber(); double result = js::math_ceil_impl(num); iter.storeInstructionResult(NumberValue(result)); @@ -985,7 +972,7 @@ bool MRound::writeRecoverData(CompactBufferWriter& writer) const { RRound::RRound(CompactBufferReader& reader) {} bool RRound::recover(JSContext* cx, SnapshotIterator& iter) const { - double num = iter.read().toNumber(); + double num = iter.readNumber(); double result = js::math_round_impl(num); iter.storeInstructionResult(NumberValue(result)); @@ -1001,7 +988,7 @@ bool MTrunc::writeRecoverData(CompactBufferWriter& writer) const { RTrunc::RTrunc(CompactBufferReader& reader) {} bool RTrunc::recover(JSContext* cx, SnapshotIterator& iter) const { - double num = iter.read().toNumber(); + double num = iter.readNumber(); double result = js::math_trunc_impl(num); iter.storeInstructionResult(NumberValue(result)); @@ -1017,21 +1004,18 @@ bool MCharCodeAt::writeRecoverData(CompactBufferWriter& writer) const { RCharCodeAt::RCharCodeAt(CompactBufferReader& reader) {} bool RCharCodeAt::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedString string(cx, iter.read().toString()); - int32_t index = iter.read().toInt32(); + JSString* string = iter.readString(); - RootedValue result(cx); - if (0 <= index && size_t(index) < string->length()) { - char16_t c; - if (!string->getChar(cx, index, &c)) { - return false; - } - result.setInt32(c); - } else { - result.setNaN(); + // Int32 because |index| is computed from MBoundsCheck. + int32_t index = iter.readInt32(); + MOZ_RELEASE_ASSERT(0 <= index && size_t(index) < string->length()); + + char16_t c; + if (!string->getChar(cx, index, &c)) { + return false; } - iter.storeInstructionResult(result); + iter.storeInstructionResult(Int32Value(c)); return true; } @@ -1044,7 +1028,8 @@ bool MFromCharCode::writeRecoverData(CompactBufferWriter& writer) const { RFromCharCode::RFromCharCode(CompactBufferReader& reader) {} bool RFromCharCode::recover(JSContext* cx, SnapshotIterator& iter) const { - int32_t charCode = iter.read().toInt32(); + // Number because |charCode| is computed from (recoverable) user input. + int32_t charCode = JS::ToInt32(iter.readNumber()); JSString* str = StringFromCharCode(cx, charCode); if (!str) { @@ -1068,7 +1053,8 @@ RFromCharCodeEmptyIfNegative::RFromCharCodeEmptyIfNegative( bool RFromCharCodeEmptyIfNegative::recover(JSContext* cx, SnapshotIterator& iter) const { - int32_t charCode = iter.read().toInt32(); + // Int32 because |charCode| is computed from MCharCodeAtOrNegative. + int32_t charCode = iter.readInt32(); JSString* str; if (charCode < 0) { @@ -1093,8 +1079,8 @@ bool MPow::writeRecoverData(CompactBufferWriter& writer) const { RPow::RPow(CompactBufferReader& reader) {} bool RPow::recover(JSContext* cx, SnapshotIterator& iter) const { - double base = iter.read().toNumber(); - double power = iter.read().toNumber(); + double base = iter.readNumber(); + double power = iter.readNumber(); double result = ecmaPow(base, power); iter.storeInstructionResult(NumberValue(result)); @@ -1110,7 +1096,7 @@ bool MPowHalf::writeRecoverData(CompactBufferWriter& writer) const { RPowHalf::RPowHalf(CompactBufferReader& reader) {} bool RPowHalf::recover(JSContext* cx, SnapshotIterator& iter) const { - double base = iter.read().toNumber(); + double base = iter.readNumber(); double power = 0.5; double result = ecmaPow(base, power); @@ -1128,8 +1114,8 @@ bool MMinMax::writeRecoverData(CompactBufferWriter& writer) const { RMinMax::RMinMax(CompactBufferReader& reader) { isMax_ = reader.readByte(); } bool RMinMax::recover(JSContext* cx, SnapshotIterator& iter) const { - double x = iter.read().toNumber(); - double y = iter.read().toNumber(); + double x = iter.readNumber(); + double y = iter.readNumber(); double result; if (isMax_) { @@ -1151,7 +1137,7 @@ bool MAbs::writeRecoverData(CompactBufferWriter& writer) const { RAbs::RAbs(CompactBufferReader& reader) {} bool RAbs::recover(JSContext* cx, SnapshotIterator& iter) const { - double num = iter.read().toNumber(); + double num = iter.readNumber(); double result = js::math_abs_impl(num); iter.storeInstructionResult(NumberValue(result)); @@ -1170,7 +1156,7 @@ RSqrt::RSqrt(CompactBufferReader& reader) { } bool RSqrt::recover(JSContext* cx, SnapshotIterator& iter) const { - double num = iter.read().toNumber(); + double num = iter.readNumber(); double result = js::math_sqrt_impl(num); // MIRType::Float32 is a specialization embedding the fact that the result is @@ -1192,8 +1178,8 @@ bool MAtan2::writeRecoverData(CompactBufferWriter& writer) const { RAtan2::RAtan2(CompactBufferReader& reader) {} bool RAtan2::recover(JSContext* cx, SnapshotIterator& iter) const { - double y = iter.read().toNumber(); - double x = iter.read().toNumber(); + double y = iter.readNumber(); + double x = iter.readNumber(); double result = js::ecmaAtan2(y, x); iter.storeInstructionResult(DoubleValue(result)); @@ -1218,7 +1204,7 @@ bool RHypot::recover(JSContext* cx, SnapshotIterator& iter) const { } for (uint32_t i = 0; i < numOperands_; ++i) { - vec.infallibleAppend(iter.read()); + vec.infallibleAppend(NumberValue(iter.readNumber())); } RootedValue result(cx); @@ -1265,7 +1251,7 @@ bool MSign::writeRecoverData(CompactBufferWriter& writer) const { RSign::RSign(CompactBufferReader& reader) {} bool RSign::recover(JSContext* cx, SnapshotIterator& iter) const { - double num = iter.read().toNumber(); + double num = iter.readNumber(); double result = js::math_sign_impl(num); iter.storeInstructionResult(NumberValue(result)); @@ -1322,7 +1308,7 @@ RMathFunction::RMathFunction(CompactBufferReader& reader) { } bool RMathFunction::recover(JSContext* cx, SnapshotIterator& iter) const { - double num = iter.read().toNumber(); + double num = iter.readNumber(); double result; switch (function_) { @@ -1431,8 +1417,8 @@ bool MStringSplit::writeRecoverData(CompactBufferWriter& writer) const { RStringSplit::RStringSplit(CompactBufferReader& reader) {} bool RStringSplit::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedString str(cx, iter.read().toString()); - RootedString sep(cx, iter.read().toString()); + RootedString str(cx, iter.readString()); + RootedString sep(cx, iter.readString()); JSObject* res = StringSplitString(cx, str, sep, INT32_MAX); if (!res) { @@ -1452,7 +1438,7 @@ bool MNaNToZero::writeRecoverData(CompactBufferWriter& writer) const { RNaNToZero::RNaNToZero(CompactBufferReader& reader) {} bool RNaNToZero::recover(JSContext* cx, SnapshotIterator& iter) const { - double v = iter.read().toNumber(); + double v = iter.readNumber(); if (std::isnan(v) || mozilla::IsNegativeZero(v)) { v = 0.0; } @@ -1470,9 +1456,11 @@ bool MRegExpMatcher::writeRecoverData(CompactBufferWriter& writer) const { RRegExpMatcher::RRegExpMatcher(CompactBufferReader& reader) {} bool RRegExpMatcher::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedObject regexp(cx, &iter.read().toObject()); - RootedString input(cx, iter.read().toString()); - int32_t lastIndex = iter.read().toInt32(); + RootedObject regexp(cx, iter.readObject()); + RootedString input(cx, iter.readString()); + + // Int32 because |lastIndex| is computed from transpiled self-hosted call. + int32_t lastIndex = iter.readInt32(); RootedValue result(cx); if (!RegExpMatcherRaw(cx, regexp, input, lastIndex, nullptr, &result)) { @@ -1507,7 +1495,8 @@ bool MTypeOfName::writeRecoverData(CompactBufferWriter& writer) const { RTypeOfName::RTypeOfName(CompactBufferReader& reader) {} bool RTypeOfName::recover(JSContext* cx, SnapshotIterator& iter) const { - int32_t type = iter.read().toInt32(); + // Int32 because |type| is computed from MTypeOf. + int32_t type = iter.readInt32(); MOZ_ASSERT(JSTYPE_UNDEFINED <= type && type < JSTYPE_LIMIT); JSString* name = TypeName(JSType(type), *cx->runtime()->commonNames); @@ -1548,7 +1537,7 @@ bool MToFloat32::writeRecoverData(CompactBufferWriter& writer) const { RToFloat32::RToFloat32(CompactBufferReader& reader) {} bool RToFloat32::recover(JSContext* cx, SnapshotIterator& iter) const { - double num = iter.read().toNumber(); + double num = iter.readNumber(); double result = js::RoundFloat32(num); iter.storeInstructionResult(DoubleValue(result)); @@ -1588,7 +1577,7 @@ bool MNewObject::writeRecoverData(CompactBufferWriter& writer) const { RNewObject::RNewObject(CompactBufferReader& reader) {} bool RNewObject::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedObject templateObject(cx, &iter.read().toObject()); + RootedObject templateObject(cx, iter.readObject()); // See CodeGenerator::visitNewObjectVMCall. // Note that recover instructions are only used if mode == ObjectCreate. @@ -1622,8 +1611,7 @@ RNewPlainObject::RNewPlainObject(CompactBufferReader& reader) { } bool RNewPlainObject::recover(JSContext* cx, SnapshotIterator& iter) const { - Rooted<SharedShape*> shape(cx, - &iter.read().toGCCellPtr().as<Shape>().asShared()); + Rooted<SharedShape*> shape(cx, &iter.readGCCellPtr().as<Shape>().asShared()); // See CodeGenerator::visitNewPlainObject. JSObject* resultObject = @@ -1676,7 +1664,7 @@ bool MNewTypedArray::writeRecoverData(CompactBufferWriter& writer) const { RNewTypedArray::RNewTypedArray(CompactBufferReader& reader) {} bool RNewTypedArray::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedObject templateObject(cx, &iter.read().toObject()); + RootedObject templateObject(cx, iter.readObject()); size_t length = templateObject.as<FixedLengthTypedArrayObject>()->length(); MOZ_ASSERT(length <= INT32_MAX, @@ -1704,7 +1692,7 @@ RNewArray::RNewArray(CompactBufferReader& reader) { } bool RNewArray::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedObject templateObject(cx, &iter.read().toObject()); + RootedObject templateObject(cx, iter.readObject()); Rooted<Shape*> shape(cx, templateObject->shape()); ArrayObject* resultObject = NewArrayWithShape(cx, count_, shape); @@ -1728,7 +1716,7 @@ RNewIterator::RNewIterator(CompactBufferReader& reader) { } bool RNewIterator::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedObject templateObject(cx, &iter.read().toObject()); + RootedObject templateObject(cx, iter.readObject()); JSObject* resultObject = nullptr; switch (MNewIterator::Type(type_)) { @@ -1760,8 +1748,8 @@ bool MLambda::writeRecoverData(CompactBufferWriter& writer) const { RLambda::RLambda(CompactBufferReader& reader) {} bool RLambda::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedObject scopeChain(cx, &iter.read().toObject()); - RootedFunction fun(cx, &iter.read().toObject().as<JSFunction>()); + RootedObject scopeChain(cx, iter.readObject()); + RootedFunction fun(cx, &iter.readObject()->as<JSFunction>()); JSObject* resultObject = js::Lambda(cx, fun, scopeChain); if (!resultObject) { @@ -1781,9 +1769,9 @@ bool MFunctionWithProto::writeRecoverData(CompactBufferWriter& writer) const { RFunctionWithProto::RFunctionWithProto(CompactBufferReader& reader) {} bool RFunctionWithProto::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedObject scopeChain(cx, &iter.read().toObject()); - RootedObject prototype(cx, &iter.read().toObject()); - RootedFunction fun(cx, &iter.read().toObject().as<JSFunction>()); + RootedObject scopeChain(cx, iter.readObject()); + RootedObject prototype(cx, iter.readObject()); + RootedFunction fun(cx, &iter.readObject()->as<JSFunction>()); JSObject* resultObject = js::FunWithProtoOperation(cx, fun, scopeChain, prototype); @@ -1804,7 +1792,7 @@ bool MNewCallObject::writeRecoverData(CompactBufferWriter& writer) const { RNewCallObject::RNewCallObject(CompactBufferReader& reader) {} bool RNewCallObject::recover(JSContext* cx, SnapshotIterator& iter) const { - Rooted<CallObject*> templateObj(cx, &iter.read().toObject().as<CallObject>()); + Rooted<CallObject*> templateObj(cx, &iter.readObject()->as<CallObject>()); Rooted<SharedShape*> shape(cx, templateObj->sharedShape()); @@ -1832,7 +1820,7 @@ bool MObjectKeys::writeRecoverData(CompactBufferWriter& writer) const { RObjectKeys::RObjectKeys(CompactBufferReader& reader) {} bool RObjectKeys::recover(JSContext* cx, SnapshotIterator& iter) const { - Rooted<JSObject*> obj(cx, &iter.read().toObject()); + Rooted<JSObject*> obj(cx, iter.readObject()); JSObject* resultKeys = ObjectKeys(cx, obj); if (!resultKeys) { @@ -1855,7 +1843,7 @@ RObjectState::RObjectState(CompactBufferReader& reader) { } bool RObjectState::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedObject object(cx, &iter.read().toObject()); + RootedObject object(cx, iter.readObject()); Handle<NativeObject*> nativeObject = object.as<NativeObject>(); MOZ_ASSERT(!Watchtower::watchesPropertyModification(nativeObject)); MOZ_ASSERT(nativeObject->slotSpan() == numSlots()); @@ -1881,8 +1869,10 @@ RArrayState::RArrayState(CompactBufferReader& reader) { } bool RArrayState::recover(JSContext* cx, SnapshotIterator& iter) const { - ArrayObject* object = &iter.read().toObject().as<ArrayObject>(); - uint32_t initLength = iter.read().toInt32(); + ArrayObject* object = &iter.readObject()->as<ArrayObject>(); + + // Int32 because |initLength| is computed from MConstant. + uint32_t initLength = iter.readInt32(); MOZ_ASSERT(object->getDenseInitializedLength() == 0, "initDenseElement call below relies on this"); @@ -1903,37 +1893,6 @@ bool RArrayState::recover(JSContext* cx, SnapshotIterator& iter) const { return true; } -bool MSetArrayLength::writeRecoverData(CompactBufferWriter& writer) const { - MOZ_ASSERT(canRecoverOnBailout()); - // For simplicity, we capture directly the object instead of the elements - // pointer. - MOZ_ASSERT(elements()->type() != MIRType::Elements); - writer.writeUnsigned(uint32_t(RInstruction::Recover_SetArrayLength)); - return true; -} - -bool MSetArrayLength::canRecoverOnBailout() const { - return isRecoveredOnBailout(); -} - -RSetArrayLength::RSetArrayLength(CompactBufferReader& reader) {} - -bool RSetArrayLength::recover(JSContext* cx, SnapshotIterator& iter) const { - Rooted<ArrayObject*> obj(cx, &iter.read().toObject().as<ArrayObject>()); - RootedValue len(cx, iter.read()); - - RootedId id(cx, NameToId(cx->names().length)); - Rooted<PropertyDescriptor> desc( - cx, PropertyDescriptor::Data(len, JS::PropertyAttribute::Writable)); - ObjectOpResult error; - if (!ArraySetLength(cx, obj, id, desc, error)) { - return false; - } - - iter.storeInstructionResult(ObjectValue(*obj)); - return true; -} - bool MAssertRecoveredOnBailout::writeRecoverData( CompactBufferWriter& writer) const { MOZ_ASSERT(canRecoverOnBailout()); @@ -1966,9 +1925,9 @@ RStringReplace::RStringReplace(CompactBufferReader& reader) { } bool RStringReplace::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedString string(cx, iter.read().toString()); - RootedString pattern(cx, iter.read().toString()); - RootedString replace(cx, iter.read().toString()); + RootedString string(cx, iter.readString()); + RootedString pattern(cx, iter.readString()); + RootedString replace(cx, iter.readString()); JSString* result = isFlatReplacement_ @@ -1992,9 +1951,20 @@ bool MSubstr::writeRecoverData(CompactBufferWriter& writer) const { RSubstr::RSubstr(CompactBufferReader& reader) {} bool RSubstr::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedString str(cx, iter.read().toString()); - int32_t begin = iter.read().toInt32(); - int32_t length = iter.read().toInt32(); + RootedString str(cx, iter.readString()); + + // Int32 because |begin| is computed from MStringTrimStartIndex, MConstant, + // or CallSubstringKernelResult. + int32_t begin = iter.readInt32(); + + // |length| is computed from MSub(truncated), MStringTrimEndIndex, or + // CallSubstringKernelResult. The current MSub inputs won't overflow, so when + // RSub recovers the MSub instruction, the input will be representable as an + // Int32. This is only true as long as RSub calls |js::SubOperation|, which in + // turn calls |JS::Value::setNumber|. We don't want to rely on this exact call + // sequence, so instead use |readNumber| here and then release-assert the + // number is exactly representable as an Int32. + int32_t length = mozilla::ReleaseAssertedCast<int32_t>(iter.readNumber()); JSString* result = SubstringKernel(cx, str, begin, length); if (!result) { @@ -2014,10 +1984,11 @@ bool MAtomicIsLockFree::writeRecoverData(CompactBufferWriter& writer) const { RAtomicIsLockFree::RAtomicIsLockFree(CompactBufferReader& reader) {} bool RAtomicIsLockFree::recover(JSContext* cx, SnapshotIterator& iter) const { - Value operand = iter.read(); - MOZ_ASSERT(operand.isInt32()); + double dsize = JS::ToInteger(iter.readNumber()); - bool result = AtomicOperations::isLockfreeJS(operand.toInt32()); + int32_t size; + bool result = mozilla::NumberEqualsInt32(dsize, &size) && + AtomicOperations::isLockfreeJS(size); iter.storeInstructionResult(BooleanValue(result)); return true; } @@ -2031,10 +2002,12 @@ bool MBigIntAsIntN::writeRecoverData(CompactBufferWriter& writer) const { RBigIntAsIntN::RBigIntAsIntN(CompactBufferReader& reader) {} bool RBigIntAsIntN::recover(JSContext* cx, SnapshotIterator& iter) const { - int32_t bits = iter.read().toInt32(); - RootedBigInt input(cx, iter.read().toBigInt()); - + // Int32 because |bits| is computed from MGuardInt32IsNonNegative. + int32_t bits = iter.readInt32(); MOZ_ASSERT(bits >= 0); + + RootedBigInt input(cx, iter.readBigInt()); + BigInt* result = BigInt::asIntN(cx, input, bits); if (!result) { return false; @@ -2053,10 +2026,12 @@ bool MBigIntAsUintN::writeRecoverData(CompactBufferWriter& writer) const { RBigIntAsUintN::RBigIntAsUintN(CompactBufferReader& reader) {} bool RBigIntAsUintN::recover(JSContext* cx, SnapshotIterator& iter) const { - int32_t bits = iter.read().toInt32(); - RootedBigInt input(cx, iter.read().toBigInt()); - + // Int32 because |bits| is computed from MGuardInt32IsNonNegative. + int32_t bits = iter.readInt32(); MOZ_ASSERT(bits >= 0); + + RootedBigInt input(cx, iter.readBigInt()); + BigInt* result = BigInt::asUintN(cx, input, bits); if (!result) { return false; @@ -2077,7 +2052,7 @@ RCreateArgumentsObject::RCreateArgumentsObject(CompactBufferReader& reader) {} bool RCreateArgumentsObject::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedObject callObject(cx, &iter.read().toObject()); + RootedObject callObject(cx, iter.readObject()); RootedObject result( cx, ArgumentsObject::createForIon(cx, iter.frame(), callObject)); if (!result) { @@ -2104,8 +2079,8 @@ RCreateInlinedArgumentsObject::RCreateInlinedArgumentsObject( bool RCreateInlinedArgumentsObject::recover(JSContext* cx, SnapshotIterator& iter) const { - RootedObject callObject(cx, &iter.read().toObject()); - RootedFunction callee(cx, &iter.read().toObject().as<JSFunction>()); + RootedObject callObject(cx, iter.readObject()); + RootedFunction callee(cx, &iter.readObject()->as<JSFunction>()); JS::RootedValueArray<ArgumentsObject::MaxInlinedArgs> argsArray(cx); for (uint32_t i = 0; i < numActuals_; i++) { @@ -2136,7 +2111,8 @@ RRest::RRest(CompactBufferReader& reader) { bool RRest::recover(JSContext* cx, SnapshotIterator& iter) const { JitFrameLayout* frame = iter.frame(); - uint32_t numActuals = iter.read().toInt32(); + // Int32 because |numActuals| is computed from MArgumentsLength. + uint32_t numActuals = iter.readInt32(); MOZ_ASSERT(numActuals == frame->numActualArgs()); uint32_t numFormals = numFormals_; diff --git a/js/src/jit/Recover.h b/js/src/jit/Recover.h index 7cc46c636d..878204de83 100644 --- a/js/src/jit/Recover.h +++ b/js/src/jit/Recover.h @@ -129,7 +129,6 @@ namespace jit { _(ObjectKeys) \ _(ObjectState) \ _(ArrayState) \ - _(SetArrayLength) \ _(AtomicIsLockFree) \ _(BigIntAsIntN) \ _(BigIntAsUintN) \ @@ -882,14 +881,6 @@ class RArrayState final : public RInstruction { SnapshotIterator& iter) const override; }; -class RSetArrayLength final : public RInstruction { - public: - RINSTRUCTION_HEADER_NUM_OP_(SetArrayLength, 2) - - [[nodiscard]] bool recover(JSContext* cx, - SnapshotIterator& iter) const override; -}; - class RAtomicIsLockFree final : public RInstruction { public: RINSTRUCTION_HEADER_NUM_OP_(AtomicIsLockFree, 1) diff --git a/js/src/jit/Trampoline.cpp b/js/src/jit/Trampoline.cpp index 85661784a7..e6d0cd31c9 100644 --- a/js/src/jit/Trampoline.cpp +++ b/js/src/jit/Trampoline.cpp @@ -96,18 +96,13 @@ void JitRuntime::generateProfilerExitFrameTailStub(MacroAssembler& masm, // | // ^--- IonICCall <---- Ion // | - // ^--- Arguments Rectifier - // | ^ - // | | - // | ^--- Ion - // | | - // | ^--- Baseline Stub <---- Baseline - // | | - // | ^--- Entry Frame (CppToJSJit or WasmToJSJit) + // ^--- Entry Frame (BaselineInterpreter) (unwrapped) // | - // ^--- Entry Frame (CppToJSJit or WasmToJSJit) + // ^--- Arguments Rectifier (unwrapped) + // | + // ^--- Trampoline Native (unwrapped) // | - // ^--- Entry Frame (BaselineInterpreter) + // ^--- Entry Frame (CppToJSJit or WasmToJSJit) // // NOTE: Keep this in sync with JSJitProfilingFrameIterator::moveToNextFrame! @@ -153,6 +148,7 @@ void JitRuntime::generateProfilerExitFrameTailStub(MacroAssembler& masm, Label handle_BaselineOrIonJS; Label handle_BaselineStub; Label handle_Rectifier; + Label handle_TrampolineNative; Label handle_BaselineInterpreterEntry; Label handle_IonICCall; Label handle_Entry; @@ -176,6 +172,8 @@ void JitRuntime::generateProfilerExitFrameTailStub(MacroAssembler& masm, &handle_BaselineOrIonJS); masm.branch32(Assembler::Equal, scratch, Imm32(FrameType::IonICCall), &handle_IonICCall); + masm.branch32(Assembler::Equal, scratch, Imm32(FrameType::TrampolineNative), + &handle_TrampolineNative); masm.branch32(Assembler::Equal, scratch, Imm32(FrameType::WasmToJSJit), &handle_Entry); @@ -237,9 +235,21 @@ void JitRuntime::generateProfilerExitFrameTailStub(MacroAssembler& masm, // There can be multiple previous frame types so just "unwrap" the arguments // rectifier frame and try again. masm.loadPtr(Address(fpScratch, CallerFPOffset), fpScratch); - emitAssertPrevFrameType(fpScratch, scratch, - {FrameType::IonJS, FrameType::BaselineStub, - FrameType::CppToJSJit, FrameType::WasmToJSJit}); + emitAssertPrevFrameType( + fpScratch, scratch, + {FrameType::IonJS, FrameType::BaselineStub, FrameType::TrampolineNative, + FrameType::CppToJSJit, FrameType::WasmToJSJit}); + masm.jump(&again); + } + + masm.bind(&handle_TrampolineNative); + { + // Unwrap this frame, similar to arguments rectifier frames. + masm.loadPtr(Address(fpScratch, CallerFPOffset), fpScratch); + emitAssertPrevFrameType( + fpScratch, scratch, + {FrameType::IonJS, FrameType::BaselineStub, FrameType::Rectifier, + FrameType::CppToJSJit, FrameType::WasmToJSJit}); masm.jump(&again); } diff --git a/js/src/jit/TrampolineNatives.cpp b/js/src/jit/TrampolineNatives.cpp new file mode 100644 index 0000000000..0bde6d9985 --- /dev/null +++ b/js/src/jit/TrampolineNatives.cpp @@ -0,0 +1,274 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "jit/TrampolineNatives.h" + +#include "jit/CalleeToken.h" +#include "jit/Ion.h" +#include "jit/JitCommon.h" +#include "jit/JitRuntime.h" +#include "jit/MacroAssembler.h" +#include "jit/PerfSpewer.h" +#include "js/CallArgs.h" +#include "js/experimental/JitInfo.h" + +#include "jit/MacroAssembler-inl.h" +#include "vm/Activation-inl.h" + +using namespace js; +using namespace js::jit; + +#define ADD_NATIVE(native) \ + const JSJitInfo js::jit::JitInfo_##native{ \ + {nullptr}, \ + {uint16_t(TrampolineNative::native)}, \ + {0}, \ + JSJitInfo::TrampolineNative}; +TRAMPOLINE_NATIVE_LIST(ADD_NATIVE) +#undef ADD_NATIVE + +void js::jit::SetTrampolineNativeJitEntry(JSContext* cx, JSFunction* fun, + TrampolineNative native) { + if (!cx->runtime()->jitRuntime()) { + // No JIT support so there's no trampoline. + return; + } + void** entry = cx->runtime()->jitRuntime()->trampolineNativeJitEntry(native); + MOZ_ASSERT(entry); + MOZ_ASSERT(*entry); + fun->setTrampolineNativeJitEntry(entry); +} + +uint32_t JitRuntime::generateArraySortTrampoline(MacroAssembler& masm) { + AutoCreatedBy acb(masm, "JitRuntime::generateArraySortTrampoline"); + + const uint32_t offset = startTrampolineCode(masm); + + // The stack for the trampoline frame will look like this: + // + // [TrampolineNativeFrameLayout] + // * this and arguments passed by the caller + // * CalleeToken + // * Descriptor + // * Return Address + // * Saved frame pointer <= FramePointer + // [ArraySortData] + // * ... + // * Comparator this + argument Values --+ -> comparator JitFrameLayout + // * Comparator (CalleeToken) | + // * Descriptor ----+ <= StackPointer + // + // The call to the comparator pushes the return address and the frame pointer, + // so we check the alignment after pushing these two pointers. + constexpr size_t FrameSize = sizeof(ArraySortData); + constexpr size_t PushedByCall = 2 * sizeof(void*); + static_assert((FrameSize + PushedByCall) % JitStackAlignment == 0); + + // Assert ArraySortData comparator data matches JitFrameLayout. + static_assert(PushedByCall + ArraySortData::offsetOfDescriptor() == + JitFrameLayout::offsetOfDescriptor()); + static_assert(PushedByCall + ArraySortData::offsetOfComparator() == + JitFrameLayout::offsetOfCalleeToken()); + static_assert(PushedByCall + ArraySortData::offsetOfComparatorThis() == + JitFrameLayout::offsetOfThis()); + static_assert(PushedByCall + ArraySortData::offsetOfComparatorArgs() == + JitFrameLayout::offsetOfActualArgs()); + static_assert(CalleeToken_Function == 0, + "JSFunction* is valid CalleeToken for non-constructor calls"); + + // Compute offsets from FramePointer. + constexpr int32_t ComparatorOffset = + -int32_t(FrameSize) + ArraySortData::offsetOfComparator(); + constexpr int32_t RvalOffset = + -int32_t(FrameSize) + ArraySortData::offsetOfComparatorReturnValue(); + constexpr int32_t DescriptorOffset = + -int32_t(FrameSize) + ArraySortData::offsetOfDescriptor(); + +#ifdef JS_USE_LINK_REGISTER + masm.pushReturnAddress(); +#endif + masm.push(FramePointer); + masm.moveStackPtrTo(FramePointer); + + AllocatableGeneralRegisterSet regs(GeneralRegisterSet::All()); + regs.takeUnchecked(ReturnReg); + regs.takeUnchecked(JSReturnOperand); + Register temp0 = regs.takeAny(); + Register temp1 = regs.takeAny(); + Register temp2 = regs.takeAny(); + + // Reserve space and check alignment of the comparator frame. + masm.reserveStack(FrameSize); + masm.assertStackAlignment(JitStackAlignment, PushedByCall); + + // Trampoline control flow looks like this: + // + // call ArraySortFromJit + // goto checkReturnValue + // call_comparator: + // call comparator + // call ArraySortData::sortWithComparator + // checkReturnValue: + // check return value, jump to call_comparator if needed + // return rval + + auto pushExitFrame = [&](Register cxReg, Register scratchReg) { + MOZ_ASSERT(masm.framePushed() == FrameSize); + masm.PushFrameDescriptor(FrameType::TrampolineNative); + masm.Push(ImmWord(0)); // Fake return address. + masm.Push(FramePointer); + masm.enterFakeExitFrame(cxReg, scratchReg, ExitFrameType::Bare); + }; + + // Call ArraySortFromJit. + using Fn1 = ArraySortResult (*)(JSContext* cx, + jit::TrampolineNativeFrameLayout* frame); + masm.loadJSContext(temp0); + pushExitFrame(temp0, temp1); + masm.setupAlignedABICall(); + masm.passABIArg(temp0); + masm.passABIArg(FramePointer); + masm.callWithABI<Fn1, ArraySortFromJit>( + ABIType::General, CheckUnsafeCallWithABI::DontCheckHasExitFrame); + + // Check return value. + Label checkReturnValue; + masm.jump(&checkReturnValue); + masm.setFramePushed(FrameSize); + + // Call the comparator. Store the frame descriptor before each call to ensure + // the HASCACHEDSAVEDFRAME_BIT flag from a previous call is cleared. + uintptr_t jitCallDescriptor = MakeFrameDescriptorForJitCall( + jit::FrameType::TrampolineNative, ArraySortData::ComparatorActualArgs); + Label callDone, jitCallFast, jitCallSlow; + masm.bind(&jitCallFast); + { + masm.storePtr(ImmWord(jitCallDescriptor), + Address(FramePointer, DescriptorOffset)); + masm.loadPtr(Address(FramePointer, ComparatorOffset), temp0); + masm.loadJitCodeRaw(temp0, temp1); + masm.callJit(temp1); + masm.jump(&callDone); + } + masm.bind(&jitCallSlow); + { + masm.storePtr(ImmWord(jitCallDescriptor), + Address(FramePointer, DescriptorOffset)); + masm.loadPtr(Address(FramePointer, ComparatorOffset), temp0); + masm.loadJitCodeRaw(temp0, temp1); + masm.switchToObjectRealm(temp0, temp2); + + // Handle arguments underflow. + Label noUnderflow, restoreRealm; + masm.loadFunctionArgCount(temp0, temp0); + masm.branch32(Assembler::BelowOrEqual, temp0, + Imm32(ArraySortData::ComparatorActualArgs), &noUnderflow); + { + Label rectifier; + bindLabelToOffset(&rectifier, argumentsRectifierOffset_); + masm.call(&rectifier); + masm.jump(&restoreRealm); + } + masm.bind(&noUnderflow); + masm.callJit(temp1); + + masm.bind(&restoreRealm); + Address calleeToken(FramePointer, + TrampolineNativeFrameLayout::offsetOfCalleeToken()); + masm.loadFunctionFromCalleeToken(calleeToken, temp0); + masm.switchToObjectRealm(temp0, temp1); + } + + // Store the comparator's return value. + masm.bind(&callDone); + masm.storeValue(JSReturnOperand, Address(FramePointer, RvalOffset)); + + // Call ArraySortData::sortWithComparator. + using Fn2 = ArraySortResult (*)(ArraySortData* data); + masm.moveStackPtrTo(temp2); + masm.loadJSContext(temp0); + pushExitFrame(temp0, temp1); + masm.setupAlignedABICall(); + masm.passABIArg(temp2); + masm.callWithABI<Fn2, ArraySortData::sortWithComparator>( + ABIType::General, CheckUnsafeCallWithABI::DontCheckHasExitFrame); + + // Check return value. + masm.bind(&checkReturnValue); + masm.branch32(Assembler::Equal, ReturnReg, + Imm32(int32_t(ArraySortResult::Failure)), masm.failureLabel()); + masm.freeStack(ExitFrameLayout::SizeWithFooter()); + masm.branch32(Assembler::Equal, ReturnReg, + Imm32(int32_t(ArraySortResult::CallJSSameRealmNoRectifier)), + &jitCallFast); + masm.branch32(Assembler::Equal, ReturnReg, + Imm32(int32_t(ArraySortResult::CallJS)), &jitCallSlow); +#ifdef DEBUG + Label ok; + masm.branch32(Assembler::Equal, ReturnReg, + Imm32(int32_t(ArraySortResult::Done)), &ok); + masm.assumeUnreachable("Unexpected return value"); + masm.bind(&ok); +#endif + + masm.loadValue(Address(FramePointer, RvalOffset), JSReturnOperand); + masm.moveToStackPtr(FramePointer); + masm.pop(FramePointer); + masm.ret(); + + return offset; +} + +void JitRuntime::generateTrampolineNatives( + MacroAssembler& masm, TrampolineNativeJitEntryOffsets& offsets, + PerfSpewerRangeRecorder& rangeRecorder) { + offsets[TrampolineNative::ArraySort] = generateArraySortTrampoline(masm); + rangeRecorder.recordOffset("Trampoline: ArraySort"); +} + +bool jit::CallTrampolineNativeJitCode(JSContext* cx, TrampolineNative native, + CallArgs& args) { + // Use the EnterJit trampoline to enter the native's trampoline code. + + AutoCheckRecursionLimit recursion(cx); + if (!recursion.check(cx)) { + return false; + } + + MOZ_ASSERT(!args.isConstructing()); + CalleeToken calleeToken = CalleeToToken(&args.callee().as<JSFunction>(), + /* constructing = */ false); + + Value* maxArgv = args.array() - 1; // -1 to include |this| + size_t maxArgc = args.length() + 1; + + Rooted<Value> result(cx, Int32Value(args.length())); + + AssertRealmUnchanged aru(cx); + ActivationEntryMonitor entryMonitor(cx, calleeToken); + JitActivation activation(cx); + + EnterJitCode enter = cx->runtime()->jitRuntime()->enterJit(); + void* code = *cx->runtime()->jitRuntime()->trampolineNativeJitEntry(native); + + CALL_GENERATED_CODE(enter, code, maxArgc, maxArgv, /* osrFrame = */ nullptr, + calleeToken, /* envChain = */ nullptr, + /* osrNumStackValues = */ 0, result.address()); + + // Ensure the counter was reset to zero after exiting from JIT code. + MOZ_ASSERT(!cx->isInUnsafeRegion()); + + // Release temporary buffer used for OSR into Ion. + cx->runtime()->jitRuntime()->freeIonOsrTempData(); + + if (result.isMagic()) { + MOZ_ASSERT(result.isMagic(JS_ION_ERROR)); + return false; + } + + args.rval().set(result); + return true; +} diff --git a/js/src/jit/TrampolineNatives.h b/js/src/jit/TrampolineNatives.h new file mode 100644 index 0000000000..f71a3b707d --- /dev/null +++ b/js/src/jit/TrampolineNatives.h @@ -0,0 +1,60 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef jit_TrampolineNatives_h +#define jit_TrampolineNatives_h + +#include <stdint.h> + +#include "js/TypeDecls.h" + +// [SMDOC] Trampoline Natives +// +// Trampoline natives are JS builtin functions that use the NATIVE_JIT_ENTRY +// mechanism. This means they have two implementations: the usual native C++ +// implementation and a generated JIT trampoline that JIT callers can call +// directly using the JIT ABI calling convention. (This is very similar to how +// calls from JS to WebAssembly are optimized in the JITs.) +// +// The JIT trampoline lets us implement some natives in a more efficient way. In +// particular, it's much faster to call (other) JS functions with JIT code from +// a JIT trampoline than from C++ code. +// +// Trampoline frames use FrameType::TrampolineNative. + +class JSJitInfo; + +namespace JS { +class CallArgs; +} // namespace JS + +// List of all trampoline natives. +#define TRAMPOLINE_NATIVE_LIST(_) _(ArraySort) + +namespace js { +namespace jit { + +enum class TrampolineNative : uint16_t { +#define ADD_NATIVE(native) native, + TRAMPOLINE_NATIVE_LIST(ADD_NATIVE) +#undef ADD_NATIVE + Count +}; + +#define ADD_NATIVE(native) extern const JSJitInfo JitInfo_##native; +TRAMPOLINE_NATIVE_LIST(ADD_NATIVE) +#undef ADD_NATIVE + +void SetTrampolineNativeJitEntry(JSContext* cx, JSFunction* fun, + TrampolineNative native); + +bool CallTrampolineNativeJitCode(JSContext* cx, TrampolineNative native, + JS::CallArgs& args); + +} // namespace jit +} // namespace js + +#endif /* jit_TrampolineNatives_h */ diff --git a/js/src/jit/VMFunctionList-inl.h b/js/src/jit/VMFunctionList-inl.h index d87b010df6..99b98f17ed 100644 --- a/js/src/jit/VMFunctionList-inl.h +++ b/js/src/jit/VMFunctionList-inl.h @@ -211,6 +211,7 @@ namespace jit { _(InterpretResume, js::jit::InterpretResume) \ _(InterruptCheck, js::jit::InterruptCheck) \ _(InvokeFunction, js::jit::InvokeFunction) \ + _(InvokeNativeFunction, js::jit::InvokeNativeFunction) \ _(IonBinaryArithICUpdate, js::jit::IonBinaryArithIC::update) \ _(IonBindNameICUpdate, js::jit::IonBindNameIC::update) \ _(IonCheckPrivateFieldICUpdate, js::jit::IonCheckPrivateFieldIC::update) \ diff --git a/js/src/jit/VMFunctions.cpp b/js/src/jit/VMFunctions.cpp index ed3f63c88c..3ec85a72c2 100644 --- a/js/src/jit/VMFunctions.cpp +++ b/js/src/jit/VMFunctions.cpp @@ -545,6 +545,39 @@ bool InvokeFunction(JSContext* cx, HandleObject obj, bool constructing, return Call(cx, fval, thisv, args, rval); } +bool InvokeNativeFunction(JSContext* cx, bool constructing, + bool ignoresReturnValue, uint32_t argc, Value* argv, + MutableHandleValue rval) { + // Ensure argv array is rooted - we may GC in here. + size_t numValues = argc + 2 + constructing; + RootedExternalValueArray argvRoot(cx, numValues, argv); + + // Data in the argument vector is arranged for a JIT -> C++ call. + CallArgs callArgs = CallArgsFromSp(argc + constructing, argv + numValues, + constructing, ignoresReturnValue); + + // This function is only called when the callee is a native function. + MOZ_ASSERT(callArgs.callee().as<JSFunction>().isNativeWithoutJitEntry()); + + if (constructing) { + MOZ_ASSERT(callArgs.thisv().isMagic(JS_IS_CONSTRUCTING)); + + if (!ConstructFromStack(cx, callArgs)) { + return false; + } + + MOZ_ASSERT(callArgs.rval().isObject(), + "native constructors don't return primitives"); + } else { + if (!CallFromStack(cx, callArgs)) { + return false; + } + } + + rval.set(callArgs.rval()); + return true; +} + void* GetContextSensitiveInterpreterStub() { return TlsContext.get()->runtime()->jitRuntime()->interpreterStub().value; } @@ -1111,7 +1144,7 @@ bool NormalSuspend(JSContext* cx, HandleObject obj, BaselineFrame* frame, bool FinalSuspend(JSContext* cx, HandleObject obj, const jsbytecode* pc) { MOZ_ASSERT(JSOp(*pc) == JSOp::FinalYieldRval); - AbstractGeneratorObject::finalSuspend(obj); + AbstractGeneratorObject::finalSuspend(cx, obj); return true; } diff --git a/js/src/jit/VMFunctions.h b/js/src/jit/VMFunctions.h index a68dd8279f..b5ac5d700b 100644 --- a/js/src/jit/VMFunctions.h +++ b/js/src/jit/VMFunctions.h @@ -354,6 +354,10 @@ struct LastArg<HeadType, TailTypes...> { uint32_t argc, Value* argv, MutableHandleValue rval); +[[nodiscard]] bool InvokeNativeFunction(JSContext* cx, bool constructing, + bool ignoresReturnValue, uint32_t argc, + Value* argv, MutableHandleValue rval); + bool InvokeFromInterpreterStub(JSContext* cx, InterpreterStubExitFrameLayout* frame); void* GetContextSensitiveInterpreterStub(); diff --git a/js/src/jit/WarpCacheIRTranspiler.cpp b/js/src/jit/WarpCacheIRTranspiler.cpp index 9a99e0f5c3..fdaafd00b3 100644 --- a/js/src/jit/WarpCacheIRTranspiler.cpp +++ b/js/src/jit/WarpCacheIRTranspiler.cpp @@ -977,7 +977,7 @@ bool WarpCacheIRTranspiler::emitGuardDynamicSlotValue(ObjOperandId objId, return true; } -bool WarpCacheIRTranspiler::emitLoadScriptedProxyHandler(ValOperandId resultId, +bool WarpCacheIRTranspiler::emitLoadScriptedProxyHandler(ObjOperandId resultId, ObjOperandId objId) { MDefinition* obj = getOperand(objId); @@ -5216,10 +5216,14 @@ bool WarpCacheIRTranspiler::emitLoadOperandResult(ValOperandId inputId) { } bool WarpCacheIRTranspiler::emitLoadWrapperTarget(ObjOperandId objId, - ObjOperandId resultId) { + ObjOperandId resultId, + bool fallible) { MDefinition* obj = getOperand(objId); - auto* ins = MLoadWrapperTarget::New(alloc(), obj); + auto* ins = MLoadWrapperTarget::New(alloc(), obj, fallible); + if (fallible) { + ins->setGuard(); + } add(ins); return defineOperand(resultId, ins); diff --git a/js/src/jit/arm/Architecture-arm.h b/js/src/jit/arm/Architecture-arm.h index fa2ae8e0ed..00edac33da 100644 --- a/js/src/jit/arm/Architecture-arm.h +++ b/js/src/jit/arm/Architecture-arm.h @@ -32,7 +32,7 @@ namespace jit { static const int32_t NUNBOX32_TYPE_OFFSET = 4; static const int32_t NUNBOX32_PAYLOAD_OFFSET = 0; -static const uint32_t ShadowStackSpace = 0; +static constexpr uint32_t ShadowStackSpace = 0; // How far forward/back can a jump go? Provide a generous buffer for thunks. static const uint32_t JumpImmediateRange = 20 * 1024 * 1024; diff --git a/js/src/jit/arm64/Architecture-arm64.h b/js/src/jit/arm64/Architecture-arm64.h index 96bbc63848..7101709f18 100644 --- a/js/src/jit/arm64/Architecture-arm64.h +++ b/js/src/jit/arm64/Architecture-arm64.h @@ -551,7 +551,7 @@ static const uint32_t SpillSlotSize = std::max(sizeof(Registers::RegisterContent), sizeof(FloatRegisters::RegisterContent)); -static const uint32_t ShadowStackSpace = 0; +static constexpr uint32_t ShadowStackSpace = 0; // When our only strategy for far jumps is to encode the offset directly, and // not insert any jump islands during assembly for even further jumps, then the diff --git a/js/src/jit/arm64/vixl/Cpu-vixl.cpp b/js/src/jit/arm64/vixl/Cpu-vixl.cpp index 12244e73e4..b425b286ee 100644 --- a/js/src/jit/arm64/vixl/Cpu-vixl.cpp +++ b/js/src/jit/arm64/vixl/Cpu-vixl.cpp @@ -214,7 +214,7 @@ CPUFeatures CPU::InferCPUFeaturesFromOS( for (size_t i = 0; i < kFeatureBitCount; i++) { if (auxv & (1UL << i)) features.Combine(kFeatureBits[i]); } -#elif defined(XP_MACOSX) +#elif defined(XP_DARWIN) // Apple processors have kJSCVT, kDotProduct, and kAtomics features. features.Combine(CPUFeatures::kJSCVT, CPUFeatures::kDotProduct, CPUFeatures::kAtomics); diff --git a/js/src/jit/loong64/Architecture-loong64.h b/js/src/jit/loong64/Architecture-loong64.h index 48745ee37a..29da43272f 100644 --- a/js/src/jit/loong64/Architecture-loong64.h +++ b/js/src/jit/loong64/Architecture-loong64.h @@ -335,7 +335,7 @@ static const uint32_t SpillSlotSize = std::max(sizeof(Registers::RegisterContent), sizeof(FloatRegisters::RegisterContent)); -static const uint32_t ShadowStackSpace = 0; +static constexpr uint32_t ShadowStackSpace = 0; static const uint32_t SizeOfReturnAddressAfterCall = 0; // When our only strategy for far jumps is to encode the offset directly, and diff --git a/js/src/jit/mips32/Architecture-mips32.h b/js/src/jit/mips32/Architecture-mips32.h index 8e186d2c9c..4ce68032b2 100644 --- a/js/src/jit/mips32/Architecture-mips32.h +++ b/js/src/jit/mips32/Architecture-mips32.h @@ -20,7 +20,7 @@ namespace js { namespace jit { -static const uint32_t ShadowStackSpace = 4 * sizeof(uintptr_t); +static constexpr uint32_t ShadowStackSpace = 4 * sizeof(uintptr_t); // These offsets are specific to nunboxing, and capture offsets into the // components of a js::Value. diff --git a/js/src/jit/mips64/Architecture-mips64.h b/js/src/jit/mips64/Architecture-mips64.h index d3db37ea2c..7bf6054a72 100644 --- a/js/src/jit/mips64/Architecture-mips64.h +++ b/js/src/jit/mips64/Architecture-mips64.h @@ -20,7 +20,7 @@ namespace js { namespace jit { // Shadow stack space is not required on MIPS64. -static const uint32_t ShadowStackSpace = 0; +static constexpr uint32_t ShadowStackSpace = 0; // MIPS64 have 64 bit floating-point coprocessor. There are 32 double // precision register which can also be used as single precision registers. diff --git a/js/src/jit/moz.build b/js/src/jit/moz.build index c0d2d5f2df..c49b4fcd9f 100644 --- a/js/src/jit/moz.build +++ b/js/src/jit/moz.build @@ -87,6 +87,7 @@ UNIFIED_SOURCES += [ "Sink.cpp", "Snapshots.cpp", "Trampoline.cpp", + "TrampolineNatives.cpp", "TrialInlining.cpp", "TypePolicy.cpp", "ValueNumbering.cpp", diff --git a/js/src/jit/none/Architecture-none.h b/js/src/jit/none/Architecture-none.h index 2433234fbf..9218404992 100644 --- a/js/src/jit/none/Architecture-none.h +++ b/js/src/jit/none/Architecture-none.h @@ -157,7 +157,7 @@ struct FloatRegister { inline bool hasUnaliasedDouble() { MOZ_CRASH(); } inline bool hasMultiAlias() { MOZ_CRASH(); } -static const uint32_t ShadowStackSpace = 0; +static constexpr uint32_t ShadowStackSpace = 0; static const uint32_t JumpImmediateRange = INT32_MAX; #ifdef JS_NUNBOX32 diff --git a/js/src/jit/riscv64/Architecture-riscv64.h b/js/src/jit/riscv64/Architecture-riscv64.h index c75bd05ff1..8d02e6e806 100644 --- a/js/src/jit/riscv64/Architecture-riscv64.h +++ b/js/src/jit/riscv64/Architecture-riscv64.h @@ -494,7 +494,7 @@ FloatRegister::LiveAsIndexableSet<RegTypeName::Any>(SetType set) { inline bool hasUnaliasedDouble() { return false; } inline bool hasMultiAlias() { return false; } -static const uint32_t ShadowStackSpace = 0; +static constexpr uint32_t ShadowStackSpace = 0; static const uint32_t JumpImmediateRange = INT32_MAX; #ifdef JS_NUNBOX32 diff --git a/js/src/jit/shared/LIR-shared.h b/js/src/jit/shared/LIR-shared.h index d8b5693d85..74c11bd91b 100644 --- a/js/src/jit/shared/LIR-shared.h +++ b/js/src/jit/shared/LIR-shared.h @@ -649,14 +649,14 @@ class LApplyArgsGeneric LIR_HEADER(ApplyArgsGeneric) LApplyArgsGeneric(const LAllocation& func, const LAllocation& argc, - const LBoxAllocation& thisv, const LDefinition& tmpobjreg, - const LDefinition& tmpcopy) + const LBoxAllocation& thisv, const LDefinition& tmpObjReg, + const LDefinition& tmpCopy) : LCallInstructionHelper(classOpcode) { setOperand(0, func); setOperand(1, argc); setBoxOperand(ThisIndex, thisv); - setTemp(0, tmpobjreg); - setTemp(1, tmpcopy); + setTemp(0, tmpObjReg); + setTemp(1, tmpCopy); } MApplyArgs* mir() const { return mir_->toApplyArgs(); } @@ -712,14 +712,14 @@ class LApplyArrayGeneric LIR_HEADER(ApplyArrayGeneric) LApplyArrayGeneric(const LAllocation& func, const LAllocation& elements, - const LBoxAllocation& thisv, const LDefinition& tmpobjreg, - const LDefinition& tmpcopy) + const LBoxAllocation& thisv, const LDefinition& tmpObjReg, + const LDefinition& tmpCopy) : LCallInstructionHelper(classOpcode) { setOperand(0, func); setOperand(1, elements); setBoxOperand(ThisIndex, thisv); - setTemp(0, tmpobjreg); - setTemp(1, tmpcopy); + setTemp(0, tmpObjReg); + setTemp(1, tmpCopy); } MApplyArray* mir() const { return mir_->toApplyArray(); } @@ -746,13 +746,13 @@ class LConstructArgsGeneric LConstructArgsGeneric(const LAllocation& func, const LAllocation& argc, const LAllocation& newTarget, const LBoxAllocation& thisv, - const LDefinition& tmpobjreg) + const LDefinition& tmpObjReg) : LCallInstructionHelper(classOpcode) { setOperand(0, func); setOperand(1, argc); setOperand(2, newTarget); setBoxOperand(ThisIndex, thisv); - setTemp(0, tmpobjreg); + setTemp(0, tmpObjReg); } MConstructArgs* mir() const { return mir_->toConstructArgs(); } @@ -784,13 +784,13 @@ class LConstructArrayGeneric LConstructArrayGeneric(const LAllocation& func, const LAllocation& elements, const LAllocation& newTarget, const LBoxAllocation& thisv, - const LDefinition& tmpobjreg) + const LDefinition& tmpObjReg) : LCallInstructionHelper(classOpcode) { setOperand(0, func); setOperand(1, elements); setOperand(2, newTarget); setBoxOperand(ThisIndex, thisv); - setTemp(0, tmpobjreg); + setTemp(0, tmpObjReg); } MConstructArray* mir() const { return mir_->toConstructArray(); } @@ -816,6 +816,164 @@ class LConstructArrayGeneric const LAllocation* getTempForArgCopy() { return getOperand(2); } }; +class LApplyArgsNative + : public LCallInstructionHelper<BOX_PIECES, BOX_PIECES + 2, 2> { + public: + LIR_HEADER(ApplyArgsNative) + + LApplyArgsNative(const LAllocation& func, const LAllocation& argc, + const LBoxAllocation& thisv, const LDefinition& tmpObjReg, + const LDefinition& tmpCopy) + : LCallInstructionHelper(classOpcode) { + setOperand(0, func); + setOperand(1, argc); + setBoxOperand(ThisIndex, thisv); + setTemp(0, tmpObjReg); + setTemp(1, tmpCopy); + } + + static constexpr bool isConstructing() { return false; } + + MApplyArgs* mir() const { return mir_->toApplyArgs(); } + + uint32_t numExtraFormals() const { return mir()->numExtraFormals(); } + + const LAllocation* getFunction() { return getOperand(0); } + const LAllocation* getArgc() { return getOperand(1); } + + static const size_t ThisIndex = 2; + + const LDefinition* getTempObject() { return getTemp(0); } + const LDefinition* getTempForArgCopy() { return getTemp(1); } +}; + +class LApplyArgsObjNative + : public LCallInstructionHelper<BOX_PIECES, BOX_PIECES + 2, 2> { + public: + LIR_HEADER(ApplyArgsObjNative) + + LApplyArgsObjNative(const LAllocation& func, const LAllocation& argsObj, + const LBoxAllocation& thisv, const LDefinition& tmpObjReg, + const LDefinition& tmpCopy) + : LCallInstructionHelper(classOpcode) { + setOperand(0, func); + setOperand(1, argsObj); + setBoxOperand(ThisIndex, thisv); + setTemp(0, tmpObjReg); + setTemp(1, tmpCopy); + } + + static constexpr bool isConstructing() { return false; } + + MApplyArgsObj* mir() const { return mir_->toApplyArgsObj(); } + + const LAllocation* getFunction() { return getOperand(0); } + const LAllocation* getArgsObj() { return getOperand(1); } + + static const size_t ThisIndex = 2; + + const LDefinition* getTempObject() { return getTemp(0); } + const LDefinition* getTempForArgCopy() { return getTemp(1); } + + // argc is mapped to the same register as argsObj: argc becomes live as + // argsObj is dying, all registers are calltemps. + const LAllocation* getArgc() { return getOperand(1); } +}; + +class LApplyArrayNative + : public LCallInstructionHelper<BOX_PIECES, BOX_PIECES + 2, 2> { + public: + LIR_HEADER(ApplyArrayNative) + + LApplyArrayNative(const LAllocation& func, const LAllocation& elements, + const LBoxAllocation& thisv, const LDefinition& tmpObjReg, + const LDefinition& tmpCopy) + : LCallInstructionHelper(classOpcode) { + setOperand(0, func); + setOperand(1, elements); + setBoxOperand(ThisIndex, thisv); + setTemp(0, tmpObjReg); + setTemp(1, tmpCopy); + } + + static constexpr bool isConstructing() { return false; } + + MApplyArray* mir() const { return mir_->toApplyArray(); } + + const LAllocation* getFunction() { return getOperand(0); } + const LAllocation* getElements() { return getOperand(1); } + + static const size_t ThisIndex = 2; + + const LDefinition* getTempObject() { return getTemp(0); } + const LDefinition* getTempForArgCopy() { return getTemp(1); } + + // argc is mapped to the same register as elements: argc becomes live as + // elements is dying, all registers are calltemps. + const LAllocation* getArgc() { return getOperand(1); } +}; + +class LConstructArgsNative : public LCallInstructionHelper<BOX_PIECES, 3, 2> { + public: + LIR_HEADER(ConstructArgsNative) + + LConstructArgsNative(const LAllocation& func, const LAllocation& argc, + const LAllocation& newTarget, + const LDefinition& tmpObjReg, const LDefinition& tmpCopy) + : LCallInstructionHelper(classOpcode) { + setOperand(0, func); + setOperand(1, argc); + setOperand(2, newTarget); + setTemp(0, tmpObjReg); + setTemp(1, tmpCopy); + } + + static constexpr bool isConstructing() { return true; } + + MConstructArgs* mir() const { return mir_->toConstructArgs(); } + + uint32_t numExtraFormals() const { return mir()->numExtraFormals(); } + + const LAllocation* getFunction() { return getOperand(0); } + const LAllocation* getArgc() { return getOperand(1); } + const LAllocation* getNewTarget() { return getOperand(2); } + + const LDefinition* getTempObject() { return getTemp(0); } + const LDefinition* getTempForArgCopy() { return getTemp(1); } +}; + +class LConstructArrayNative : public LCallInstructionHelper<BOX_PIECES, 3, 2> { + public: + LIR_HEADER(ConstructArrayNative) + + LConstructArrayNative(const LAllocation& func, const LAllocation& elements, + const LAllocation& newTarget, + const LDefinition& tmpObjReg, + const LDefinition& tmpCopy) + : LCallInstructionHelper(classOpcode) { + setOperand(0, func); + setOperand(1, elements); + setOperand(2, newTarget); + setTemp(0, tmpObjReg); + setTemp(1, tmpCopy); + } + + static constexpr bool isConstructing() { return true; } + + MConstructArray* mir() const { return mir_->toConstructArray(); } + + const LAllocation* getFunction() { return getOperand(0); } + const LAllocation* getElements() { return getOperand(1); } + const LAllocation* getNewTarget() { return getOperand(2); } + + const LDefinition* getTempObject() { return getTemp(0); } + const LDefinition* getTempForArgCopy() { return getTemp(1); } + + // argc is mapped to the same register as elements: argc becomes live as + // elements is dying, all registers are calltemps. + const LAllocation* getArgc() { return getOperand(1); } +}; + // Takes in either an integer or boolean input and tests it for truthiness. class LTestIAndBranch : public LControlInstructionHelper<2, 1, 0> { public: diff --git a/js/src/jit/wasm32/Architecture-wasm32.h b/js/src/jit/wasm32/Architecture-wasm32.h index d7726eaa5f..2419591664 100644 --- a/js/src/jit/wasm32/Architecture-wasm32.h +++ b/js/src/jit/wasm32/Architecture-wasm32.h @@ -161,7 +161,7 @@ struct FloatRegister { inline bool hasUnaliasedDouble() { MOZ_CRASH(); } inline bool hasMultiAlias() { MOZ_CRASH(); } -static const uint32_t ShadowStackSpace = 0; +static constexpr uint32_t ShadowStackSpace = 0; static const uint32_t JumpImmediateRange = INT32_MAX; #ifdef JS_NUNBOX32 diff --git a/js/src/jit/x86-shared/Architecture-x86-shared.h b/js/src/jit/x86-shared/Architecture-x86-shared.h index b4701af284..72055efb7d 100644 --- a/js/src/jit/x86-shared/Architecture-x86-shared.h +++ b/js/src/jit/x86-shared/Architecture-x86-shared.h @@ -31,9 +31,9 @@ static const int32_t NUNBOX32_PAYLOAD_OFFSET = 0; #endif #if defined(JS_CODEGEN_X64) && defined(_WIN64) -static const uint32_t ShadowStackSpace = 32; +static constexpr uint32_t ShadowStackSpace = 32; #else -static const uint32_t ShadowStackSpace = 0; +static constexpr uint32_t ShadowStackSpace = 0; #endif static const uint32_t JumpImmediateRange = INT32_MAX; diff --git a/js/src/jsapi-tests/moz.build b/js/src/jsapi-tests/moz.build index 9e2c99cfa6..f6cc3b3f70 100644 --- a/js/src/jsapi-tests/moz.build +++ b/js/src/jsapi-tests/moz.build @@ -138,6 +138,7 @@ UNIFIED_SOURCES += [ "testUbiNode.cpp", "testUncaughtSymbol.cpp", "testUTF8.cpp", + "testWasmEncoder.cpp", "testWasmLEB128.cpp", "testWasmReturnCalls.cpp", "testWeakMap.cpp", diff --git a/js/src/jsapi-tests/testDeduplication.cpp b/js/src/jsapi-tests/testDeduplication.cpp index 6ee3cb989d..7f5eb68e37 100644 --- a/js/src/jsapi-tests/testDeduplication.cpp +++ b/js/src/jsapi-tests/testDeduplication.cpp @@ -33,6 +33,8 @@ static bool SameChars(JSContext* cx, JSString* str1, JSString* str2, } BEGIN_TEST(testDeduplication_ASSC) { + AutoGCParameter disableSemispace(cx, JSGC_SEMISPACE_NURSERY_ENABLED, 0); + // Test with a long enough string to avoid inline chars allocation. const char text[] = "Andthebeastshallcomeforthsurroundedbyaroilingcloudofvengeance." diff --git a/js/src/jsapi-tests/testGCHeapBarriers.cpp b/js/src/jsapi-tests/testGCHeapBarriers.cpp index 9493049e16..a23e41ae6e 100644 --- a/js/src/jsapi-tests/testGCHeapBarriers.cpp +++ b/js/src/jsapi-tests/testGCHeapBarriers.cpp @@ -169,6 +169,7 @@ static void MakeGray(const JS::ArrayBufferOrView& view) { // - WeakHeapPtr BEGIN_TEST(testGCHeapPostBarriers) { AutoLeaveZeal nozeal(cx); + AutoGCParameter disableSemispace(cx, JSGC_SEMISPACE_NURSERY_ENABLED, 0); /* Sanity check - objects start in the nursery and then become tenured. */ JS_GC(cx); diff --git a/js/src/jsapi-tests/testGCMarking.cpp b/js/src/jsapi-tests/testGCMarking.cpp index dc2f1e0f4d..0a51bf21bc 100644 --- a/js/src/jsapi-tests/testGCMarking.cpp +++ b/js/src/jsapi-tests/testGCMarking.cpp @@ -344,6 +344,7 @@ BEGIN_TEST(testIncrementalRoots) { // Tenure everything so intentionally unrooted objects don't move before we // can use them. + AutoGCParameter disableSemispace(cx, JSGC_SEMISPACE_NURSERY_ENABLED, 0); cx->runtime()->gc.minorGC(JS::GCReason::API); // Release all roots except for the RootedObjectVector. diff --git a/js/src/jsapi-tests/testGCUniqueId.cpp b/js/src/jsapi-tests/testGCUniqueId.cpp index 1c5652f280..577582837d 100644 --- a/js/src/jsapi-tests/testGCUniqueId.cpp +++ b/js/src/jsapi-tests/testGCUniqueId.cpp @@ -23,6 +23,7 @@ static void MinimizeHeap(JSContext* cx) { BEGIN_TEST(testGCUID) { AutoLeaveZeal nozeal(cx); + AutoGCParameter gcparam(cx, JSGC_SEMISPACE_NURSERY_ENABLED, 0); uint64_t uid = 0; uint64_t tmp = 0; diff --git a/js/src/jsapi-tests/testIsInsideNursery.cpp b/js/src/jsapi-tests/testIsInsideNursery.cpp index 793a1ed3cc..c7383f460b 100644 --- a/js/src/jsapi-tests/testIsInsideNursery.cpp +++ b/js/src/jsapi-tests/testIsInsideNursery.cpp @@ -9,6 +9,8 @@ #include "vm/JSContext-inl.h" +using namespace js; + BEGIN_TEST(testIsInsideNursery) { /* Non-GC things are never inside the nursery. */ CHECK(!cx->nursery().isInside(cx)); @@ -30,9 +32,9 @@ BEGIN_TEST(testIsInsideNursery) { JS::Rooted<JSString*> string(cx, JS_NewStringCopyZ(cx, oolstr)); /* Objects are initially allocated in the nursery. */ - CHECK(js::gc::IsInsideNursery(object)); + CHECK(gc::IsInsideNursery(object)); /* As are strings. */ - CHECK(js::gc::IsInsideNursery(string)); + CHECK(gc::IsInsideNursery(string)); /* And their contents. */ { JS::AutoCheckCannotGC nogc; @@ -44,8 +46,8 @@ BEGIN_TEST(testIsInsideNursery) { JS_GC(cx); /* And are tenured if still live after a GC. */ - CHECK(!js::gc::IsInsideNursery(object)); - CHECK(!js::gc::IsInsideNursery(string)); + CHECK(!gc::IsInsideNursery(object)); + CHECK(!gc::IsInsideNursery(string)); { JS::AutoCheckCannotGC nogc; const JS::Latin1Char* strdata = @@ -56,3 +58,76 @@ BEGIN_TEST(testIsInsideNursery) { return true; } END_TEST(testIsInsideNursery) + +BEGIN_TEST(testSemispaceNursery) { + AutoGCParameter enableSemispace(cx, JSGC_SEMISPACE_NURSERY_ENABLED, 1); + + JS_GC(cx); + + /* Objects are initially allocated in the nursery. */ + RootedObject object(cx, JS_NewPlainObject(cx)); + CHECK(gc::IsInsideNursery(object)); + + /* Minor GC with the evict reason tenures them. */ + cx->minorGC(JS::GCReason::EVICT_NURSERY); + CHECK(!gc::IsInsideNursery(object)); + + /* Minor GC with other reasons gives them a second chance. */ + object = JS_NewPlainObject(cx); + void* ptr = object; + CHECK(gc::IsInsideNursery(object)); + cx->minorGC(JS::GCReason::API); + CHECK(ptr != object); + CHECK(gc::IsInsideNursery(object)); + ptr = object; + cx->minorGC(JS::GCReason::API); + CHECK(!gc::IsInsideNursery(object)); + CHECK(ptr != object); + + CHECK(testReferencesBetweenGenerations(0)); + CHECK(testReferencesBetweenGenerations(1)); + CHECK(testReferencesBetweenGenerations(2)); + + return true; +} + +bool testReferencesBetweenGenerations(size_t referrerGeneration) { + MOZ_ASSERT(referrerGeneration <= 2); + + RootedObject referrer(cx, JS_NewPlainObject(cx)); + CHECK(referrer); + CHECK(gc::IsInsideNursery(referrer)); + + for (size_t i = 0; i < referrerGeneration; i++) { + cx->minorGC(JS::GCReason::API); + } + MOZ_ASSERT(IsInsideNursery(referrer) == (referrerGeneration < 2)); + + RootedObject object(cx, JS_NewPlainObject(cx)); + CHECK(gc::IsInsideNursery(object)); + RootedValue value(cx, JS::ObjectValue(*object)); + CHECK(JS_DefineProperty(cx, referrer, "prop", value, 0)); + CHECK(JS_GetProperty(cx, referrer, "prop", &value)); + CHECK(&value.toObject() == object); + CHECK(JS_SetElement(cx, referrer, 0, value)); + CHECK(JS_GetElement(cx, referrer, 0, &value)); + CHECK(&value.toObject() == object); + + cx->minorGC(JS::GCReason::API); + CHECK(gc::IsInsideNursery(object)); + CHECK(JS_GetProperty(cx, referrer, "prop", &value)); + CHECK(&value.toObject() == object); + CHECK(JS_GetElement(cx, referrer, 0, &value)); + CHECK(&value.toObject() == object); + + cx->minorGC(JS::GCReason::API); + CHECK(!gc::IsInsideNursery(object)); + CHECK(JS_GetProperty(cx, referrer, "prop", &value)); + CHECK(&value.toObject() == object); + CHECK(JS_GetElement(cx, referrer, 0, &value)); + CHECK(&value.toObject() == object); + + return true; +} + +END_TEST(testSemispaceNursery) diff --git a/js/src/jsapi-tests/testSparseBitmap.cpp b/js/src/jsapi-tests/testSparseBitmap.cpp index bd5e7fee95..936132ab0c 100644 --- a/js/src/jsapi-tests/testSparseBitmap.cpp +++ b/js/src/jsapi-tests/testSparseBitmap.cpp @@ -111,5 +111,50 @@ BEGIN_TEST(testSparseBitmapExternalOR) { return true; } - END_TEST(testSparseBitmapExternalOR) + +BEGIN_TEST(testSparseBitmapExternalAND) { + // Testing ANDing data from an external array. + + const size_t wordCount = 4; + + // Create a bitmap with two bits set per word based on the word index. + SparseBitmap bitmap; + for (size_t i = 0; i < wordCount; i++) { + bitmap.setBit(i * JS_BITS_PER_WORD + i); + bitmap.setBit(i * JS_BITS_PER_WORD + i + 1); + } + + // Update a single word, clearing one of the bits. + uintptr_t source[wordCount]; + CHECK(bitmap.getBit(0)); + CHECK(bitmap.getBit(1)); + source[0] = ~(1 << 1); + bitmap.bitwiseAndRangeWith(0, 1, source); + CHECK(bitmap.getBit(0)); + CHECK(!bitmap.getBit(1)); + + // Update a word at an offset. + CHECK(bitmap.getBit(JS_BITS_PER_WORD + 1)); + CHECK(bitmap.getBit(JS_BITS_PER_WORD + 2)); + source[0] = ~(1 << 2); + bitmap.bitwiseAndRangeWith(1, 1, source); + CHECK(bitmap.getBit(JS_BITS_PER_WORD + 1)); + CHECK(!bitmap.getBit(JS_BITS_PER_WORD + 2)); + + // Update multiple words at an offset. + CHECK(bitmap.getBit(JS_BITS_PER_WORD * 2 + 2)); + CHECK(bitmap.getBit(JS_BITS_PER_WORD * 2 + 3)); + CHECK(bitmap.getBit(JS_BITS_PER_WORD * 3 + 3)); + CHECK(bitmap.getBit(JS_BITS_PER_WORD * 3 + 4)); + source[0] = ~(1 << 3); + source[1] = ~(1 << 4); + bitmap.bitwiseAndRangeWith(2, 2, source); + CHECK(bitmap.getBit(JS_BITS_PER_WORD * 2 + 2)); + CHECK(!bitmap.getBit(JS_BITS_PER_WORD * 2 + 3)); + CHECK(bitmap.getBit(JS_BITS_PER_WORD * 3 + 3)); + CHECK(!bitmap.getBit(JS_BITS_PER_WORD * 3 + 4)); + + return true; +} +END_TEST(testSparseBitmapExternalAND) diff --git a/js/src/jsapi-tests/testWasmEncoder.cpp b/js/src/jsapi-tests/testWasmEncoder.cpp new file mode 100644 index 0000000000..a90249264c --- /dev/null +++ b/js/src/jsapi-tests/testWasmEncoder.cpp @@ -0,0 +1,117 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "jit/MacroAssembler.h" + +#include "jsapi-tests/tests.h" +#include "jsapi-tests/testsJit.h" + +#include "wasm/WasmConstants.h" +#include "wasm/WasmFeatures.h" // AnyCompilerAvailable +#include "wasm/WasmGenerator.h" +#include "wasm/WasmSignalHandlers.h" // EnsureFullSignalHandlers +#include "wasm/WasmValType.h" + +using namespace js; +using namespace js::jit; +using namespace js::wasm; + +static bool TestTruncFn(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + double d = args[0].toDouble(); + args.rval().setInt32((int)d); + return true; +} + +// Check if wasm modules can be encoded in C++ and run. +BEGIN_TEST(testWasmEncodeBasic) { + if (!AnyCompilerAvailable(cx)) { + knownFail = true; + return false; + } + + EnsureFullSignalHandlers(cx); + + FeatureOptions options; + ScriptedCaller scriptedCaller; + SharedCompileArgs compileArgs = + CompileArgs::buildAndReport(cx, std::move(scriptedCaller), options); + + ModuleEnvironment moduleEnv(compileArgs->features); + CompilerEnvironment compilerEnv(CompileMode::Once, Tier::Optimized, + DebugEnabled::False); + compilerEnv.computeParameters(); + MOZ_ALWAYS_TRUE(moduleEnv.init()); + + ValTypeVector paramsImp, resultsImp; + MOZ_ALWAYS_TRUE(paramsImp.emplaceBack(ValType::F64) && + resultsImp.emplaceBack(ValType::I32)); + + CacheableName ns; + CacheableName impName; + MOZ_ALWAYS_TRUE(CacheableName::fromUTF8Chars("t", &impName)); + MOZ_ALWAYS_TRUE(moduleEnv.addImportedFunc(std::move(paramsImp), + std::move(resultsImp), + std::move(ns), std::move(impName))); + + ValTypeVector params, results; + MOZ_ALWAYS_TRUE(results.emplaceBack(ValType::I32)); + CacheableName expName; + MOZ_ALWAYS_TRUE(CacheableName::fromUTF8Chars("r", &expName)); + MOZ_ALWAYS_TRUE(moduleEnv.addDefinedFunc(std::move(params), + std::move(results), true, + mozilla::Some(std::move(expName)))); + + ModuleGenerator mg(*compileArgs, &moduleEnv, &compilerEnv, nullptr, nullptr, + nullptr); + MOZ_ALWAYS_TRUE(mg.init(nullptr)); + + // Build function and keep bytecode around until the end. + Bytes bytecode; + { + Encoder encoder(bytecode); + MOZ_ALWAYS_TRUE(EncodeLocalEntries(encoder, ValTypeVector())); + MOZ_ALWAYS_TRUE(encoder.writeOp(Op::F64Const) && + encoder.writeFixedF64(42.3)); + MOZ_ALWAYS_TRUE(encoder.writeOp(Op::Call) && encoder.writeVarU32(0)); + MOZ_ALWAYS_TRUE(encoder.writeOp(Op::End)); + } + MOZ_ALWAYS_TRUE(mg.compileFuncDef(1, 0, bytecode.begin(), + bytecode.begin() + bytecode.length())); + MOZ_ALWAYS_TRUE(mg.finishFuncDefs()); + + SharedBytes shareableBytes = js_new<ShareableBytes>(); + MOZ_ALWAYS_TRUE(shareableBytes); + SharedModule module = mg.finishModule(*shareableBytes); + MOZ_ALWAYS_TRUE(module); + + MOZ_ASSERT(module->imports().length() == 1); + MOZ_ASSERT(module->exports().length() == 1); + + // Instantiate and run. + { + Rooted<ImportValues> imports(cx); + RootedFunction func(cx, NewNativeFunction(cx, TestTruncFn, 0, nullptr)); + MOZ_ALWAYS_TRUE(func); + MOZ_ALWAYS_TRUE(imports.get().funcs.append(func)); + + Rooted<WasmInstanceObject*> instance(cx); + MOZ_ALWAYS_TRUE(module->instantiate(cx, imports.get(), nullptr, &instance)); + RootedFunction wasmFunc(cx); + MOZ_ALWAYS_TRUE( + WasmInstanceObject::getExportedFunction(cx, instance, 1, &wasmFunc)); + + JSAutoRealm ar(cx, wasmFunc); + RootedValue rval(cx); + RootedValue fval(cx); + MOZ_ALWAYS_TRUE( + JS::Call(cx, fval, wasmFunc, JS::HandleValueArray::empty(), &rval)); + MOZ_RELEASE_ASSERT(rval.toInt32() == 42); + } + return true; +} +END_TEST(testWasmEncodeBasic) diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp index 77c3ae5f09..8dda64d4f1 100644 --- a/js/src/jsapi.cpp +++ b/js/src/jsapi.cpp @@ -40,6 +40,7 @@ #include "gc/Marking.h" #include "gc/PublicIterators.h" #include "jit/JitSpewer.h" +#include "jit/TrampolineNatives.h" #include "js/CallAndConstruct.h" // JS::IsCallable #include "js/CharacterEncoding.h" #include "js/ColumnNumber.h" // JS::TaggedColumnNumberOneOrigin, JS::ColumnNumberOneOrigin @@ -49,6 +50,7 @@ #include "js/Date.h" // JS::GetReduceMicrosecondTimePrecisionCallback #include "js/ErrorInterceptor.h" #include "js/ErrorReport.h" // JSErrorBase +#include "js/experimental/JitInfo.h" // JSJitInfo #include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* #include "js/friend/StackLimits.h" // js::AutoCheckRecursionLimit #include "js/GlobalObject.h" @@ -1602,7 +1604,8 @@ JS_PUBLIC_API bool JS::ToPrimitive(JSContext* cx, HandleObject obj, JSType hint, return ToPrimitiveSlow(cx, hint, vp); } -JS_PUBLIC_API bool JS::GetFirstArgumentAsTypeHint(JSContext* cx, CallArgs args, +JS_PUBLIC_API bool JS::GetFirstArgumentAsTypeHint(JSContext* cx, + const CallArgs& args, JSType* result) { if (!args.get(0).isString()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, @@ -2335,8 +2338,12 @@ JS_PUBLIC_API JSFunction* JS::NewFunctionFromSpec(JSContext* cx, return nullptr; } - if (fs->call.info) { - fun->setJitInfo(fs->call.info); + if (auto* jitInfo = fs->call.info) { + if (jitInfo->type() == JSJitInfo::OpType::TrampolineNative) { + jit::SetTrampolineNativeJitEntry(cx, fun, jitInfo->trampolineNative); + } else { + fun->setJitInfo(jitInfo); + } } return fun; } diff --git a/js/src/jsapi.h b/js/src/jsapi.h index 3597e52582..7fe58c250f 100644 --- a/js/src/jsapi.h +++ b/js/src/jsapi.h @@ -303,7 +303,7 @@ extern JS_PUBLIC_API bool ToPrimitive(JSContext* cx, JS::HandleObject obj, * This can be useful in implementing a @@toPrimitive method. */ extern JS_PUBLIC_API bool GetFirstArgumentAsTypeHint(JSContext* cx, - CallArgs args, + const CallArgs& args, JSType* result); } /* namespace JS */ diff --git a/js/src/jsdate.cpp b/js/src/jsdate.cpp index 7040213f56..a6f8b42040 100644 --- a/js/src/jsdate.cpp +++ b/js/src/jsdate.cpp @@ -1387,11 +1387,12 @@ static bool ParseDate(DateTimeInfo::ForceUTC forceUTC, const CharT* s, if (IsAsciiDigit(s[index])) { break; } - } else { - // Reject numbers directly after letters e.g. foo2 - if (IsAsciiDigit(s[index]) && IsAsciiAlpha(s[index - 1])) { - return false; - } + } else if (!strchr(" ,.-/", s[index])) { + // We're only allowing the above delimiters after the day of + // week to prevent things such as "foo_1" from being parsed + // as a date, which may break software which uses this function + // to determine whether or not something is a date. + return false; } } diff --git a/js/src/jsfriendapi.cpp b/js/src/jsfriendapi.cpp index 694058f6c9..574e61248e 100644 --- a/js/src/jsfriendapi.cpp +++ b/js/src/jsfriendapi.cpp @@ -420,6 +420,23 @@ JS_PUBLIC_API JSFunction* js::NewFunctionByIdWithReserved( gc::AllocKind::FUNCTION_EXTENDED); } +JS_PUBLIC_API JSFunction* js::NewFunctionByIdWithReservedAndProto( + JSContext* cx, JSNative native, HandleObject proto, unsigned nargs, + unsigned flags, jsid id) { + MOZ_ASSERT(id.isAtom()); + MOZ_ASSERT(!cx->zone()->isAtomsZone()); + MOZ_ASSERT(native); + CHECK_THREAD(cx); + cx->check(id); + + Rooted<JSAtom*> atom(cx, id.toAtom()); + FunctionFlags funflags = (flags & JSFUN_CONSTRUCTOR) + ? FunctionFlags::NATIVE_CTOR + : FunctionFlags::NATIVE_FUN; + return NewFunctionWithProto(cx, native, nargs, funflags, nullptr, atom, proto, + gc::AllocKind::FUNCTION_EXTENDED, TenuredObject); +} + JS_PUBLIC_API const Value& js::GetFunctionNativeReserved(JSObject* fun, size_t which) { MOZ_ASSERT(fun->as<JSFunction>().isNativeFun()); diff --git a/js/src/jsfriendapi.h b/js/src/jsfriendapi.h index 046045ac53..6ad8140d91 100644 --- a/js/src/jsfriendapi.h +++ b/js/src/jsfriendapi.h @@ -246,6 +246,12 @@ extern JS_PUBLIC_API bool EnqueueJob(JSContext* cx, JS::HandleObject job); */ extern JS_PUBLIC_API void StopDrainingJobQueue(JSContext* cx); +/** + * Instruct the runtime to restart draining the internal job queue after + * stopping it with StopDrainingJobQueue. + */ +extern JS_PUBLIC_API void RestartDrainingJobQueue(JSContext* cx); + extern JS_PUBLIC_API void RunJobs(JSContext* cx); extern JS_PUBLIC_API JS::Zone* GetRealmZone(JS::Realm* realm); @@ -389,6 +395,10 @@ JS_PUBLIC_API JSFunction* NewFunctionByIdWithReserved(JSContext* cx, unsigned nargs, unsigned flags, jsid id); +JS_PUBLIC_API JSFunction* NewFunctionByIdWithReservedAndProto( + JSContext* cx, JSNative native, JS::Handle<JSObject*> proto, unsigned nargs, + unsigned flags, jsid id); + /** * Get or set function's reserved slot value. * `fun` should be a function created with `*WithReserved` API above. diff --git a/js/src/make-source-package.py b/js/src/make-source-package.py index 3ea6b7e310..d24b47d667 100755 --- a/js/src/make-source-package.py +++ b/js/src/make-source-package.py @@ -179,7 +179,7 @@ rsync_filter_list = """ + /config/** + /python/** -+ /.cargo/config.in ++ /.cargo/config.toml.in + /third_party/function2/** - /third_party/python/gyp diff --git a/js/src/moz.build b/js/src/moz.build index 3ec9c6f489..63d0d75509 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -311,6 +311,7 @@ UNIFIED_SOURCES += [ "builtin/ParseRecordObject.cpp", "builtin/Profilers.cpp", "builtin/Promise.cpp", + "builtin/RawJSONObject.cpp", "builtin/Reflect.cpp", "builtin/ReflectParse.cpp", "builtin/ShadowRealm.cpp", diff --git a/js/src/old-configure.in b/js/src/old-configure.in index 6861d2bbac..b9152f974d 100644 --- a/js/src/old-configure.in +++ b/js/src/old-configure.in @@ -436,33 +436,11 @@ fi if test -z "$SKIP_COMPILER_CHECKS"; then dnl Checks for typedefs, structures, and compiler characteristics. dnl ======================================================== -AC_TYPE_MODE_T -AC_TYPE_OFF_T -AC_TYPE_PID_T - -AC_LANG_C -AC_MSG_CHECKING(for ssize_t) -AC_CACHE_VAL(ac_cv_type_ssize_t, - [AC_TRY_COMPILE([#include <stdio.h> - #include <sys/types.h>], - [ssize_t foo = 0;], - [ac_cv_type_ssize_t=true], - [ac_cv_type_ssize_t=false])]) -if test "$ac_cv_type_ssize_t" = true ; then - AC_DEFINE(HAVE_SSIZE_T) - AC_MSG_RESULT(yes) -else - AC_MSG_RESULT(no) -fi AC_LANG_CPLUSPLUS MOZ_CXX11 -dnl Checks for header files. -dnl ======================================================== -AC_HEADER_DIRENT - dnl Checks for libraries. dnl ======================================================== AC_CHECK_LIB(c_r, gethostbyname_r) diff --git a/js/src/rust/Cargo.toml b/js/src/rust/Cargo.toml index 0ea4a5c965..6624747fe9 100644 --- a/js/src/rust/Cargo.toml +++ b/js/src/rust/Cargo.toml @@ -21,4 +21,4 @@ mozilla-central-workspace-hack = { version = "0.1", features = ["jsrust"], optio jsrust_shared = { path = "./shared" } # Workaround for https://github.com/rust-lang/rust/issues/58393 mozglue-static = { path = "../../../mozglue/static/rust" } -wast = "70.0.1" +wast = "201.0.0" diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp index 624b8217c3..178c394e1d 100644 --- a/js/src/shell/js.cpp +++ b/js/src/shell/js.cpp @@ -729,9 +729,6 @@ bool shell::enableSourcePragmas = true; bool shell::enableAsyncStacks = false; bool shell::enableAsyncStackCaptureDebuggeeOnly = false; bool shell::enableToSource = false; -#ifdef ENABLE_JSON_PARSE_WITH_SOURCE -bool shell::enableJSONParseWithSource = false; -#endif bool shell::enableImportAttributes = false; bool shell::enableImportAttributesAssertSyntax = false; #ifdef JS_GC_ZEAL @@ -1316,6 +1313,13 @@ static bool DrainJobQueue(JSContext* cx, unsigned argc, Value* vp) { return false; } + if (cx->isEvaluatingModule != 0) { + JS_ReportErrorASCII( + cx, + "Can't drain the job queue when executing the top level of a module"); + return false; + } + RunShellJobs(cx); if (GetShellContext(cx)->quitting) { @@ -2428,6 +2432,20 @@ static bool ConvertTranscodeResultToJSException(JSContext* cx, } } +static void SetQuitting(JSContext* cx, int32_t code) { + ShellContext* sc = GetShellContext(cx); + js::StopDrainingJobQueue(cx); + sc->exitCode = code; + sc->quitting = true; +} + +static void UnsetQuitting(JSContext* cx) { + ShellContext* sc = GetShellContext(cx); + js::RestartDrainingJobQueue(cx); + sc->exitCode = 0; + sc->quitting = false; +} + static bool Evaluate(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); @@ -2716,6 +2734,11 @@ static bool Evaluate(JSContext* cx, unsigned argc, Value* vp) { ? JS_ExecuteScript(cx, script, args.rval()) : JS_ExecuteScript(cx, envChain, script, args.rval()))) { if (catchTermination && !JS_IsExceptionPending(cx)) { + ShellContext* sc = GetShellContext(cx); + if (sc->quitting) { + UnsetQuitting(cx); + } + JSAutoRealm ar1(cx, callerGlobal); JSString* str = JS_NewStringCopyZ(cx, "terminated"); if (!str) { @@ -3165,8 +3188,6 @@ static bool PrintErr(JSContext* cx, unsigned argc, Value* vp) { static bool Help(JSContext* cx, unsigned argc, Value* vp); static bool Quit(JSContext* cx, unsigned argc, Value* vp) { - ShellContext* sc = GetShellContext(cx); - // Print a message to stderr in differential testing to help jsfunfuzz // find uncatchable-exception bugs. if (js::SupportDifferentialTesting()) { @@ -3189,9 +3210,7 @@ static bool Quit(JSContext* cx, unsigned argc, Value* vp) { return false; } - js::StopDrainingJobQueue(cx); - sc->exitCode = code; - sc->quitting = true; + SetQuitting(cx, code); return false; } @@ -4108,11 +4127,7 @@ static void SetStandardRealmOptions(JS::RealmOptions& options) { options.creationOptions() .setSharedMemoryAndAtomicsEnabled(enableSharedMemory) .setCoopAndCoepEnabled(false) - .setToSourceEnabled(enableToSource) -#ifdef ENABLE_JSON_PARSE_WITH_SOURCE - .setJSONParseWithSource(enableJSONParseWithSource) -#endif - ; + .setToSourceEnabled(enableToSource); } [[nodiscard]] static bool CheckRealmOptions(JSContext* cx, @@ -5448,7 +5463,7 @@ static bool ModuleLink(JSContext* cx, unsigned argc, Value* vp) { Rooted<ModuleObject*> module(cx, object->as<ShellModuleObjectWrapper>().get()); - if (!js::ModuleLink(cx, module)) { + if (!JS::ModuleLink(cx, module)) { return false; } @@ -5477,7 +5492,7 @@ static bool ModuleEvaluate(JSContext* cx, unsigned argc, Value* vp) { Rooted<ModuleObject*> module(cx, object->as<ShellModuleObjectWrapper>().get()); - if (!js::ModuleEvaluate(cx, module, args.rval())) { + if (!JS::ModuleEvaluate(cx, module, args.rval())) { return false; } } @@ -5726,6 +5741,11 @@ static bool FrontendTest(JSContext* cx, unsigned argc, Value* vp, return false; } + if (goal == frontend::ParseGoal::Module && options.lineno == 0) { + JS_ReportErrorASCII(cx, "Module cannot be compiled with lineNumber == 0"); + return false; + } + #ifdef JS_ENABLE_SMOOSH bool found = false; if (!JS_HasProperty(cx, objOptions, "rustFrontend", &found)) { @@ -6143,8 +6163,7 @@ static bool OffThreadCompileModuleToStencil(JSContext* cx, unsigned argc, return false; } - if (options.lineno == 0) { - JS_ReportErrorASCII(cx, "Module cannot be compiled with lineNumber == 0"); + if (!ValidateModuleCompileOptions(cx, options)) { return false; } } @@ -6794,6 +6813,10 @@ static bool NewGlobal(JSContext* cx, unsigned argc, Value* vp) { creationOptions.setNewCompartmentAndZone(); } + // Ensure the target compartment/zone is kept alive when sameCompartmentAs or + // sameZoneAs is used. + Rooted<JSObject*> compartmentRoot(cx); + JS::AutoHoldPrincipals principals(cx); if (args.length() == 1 && args[0].isObject()) { @@ -6811,15 +6834,16 @@ static bool NewGlobal(JSContext* cx, unsigned argc, Value* vp) { return false; } if (v.isObject()) { - creationOptions.setNewCompartmentInExistingZone( - UncheckedUnwrap(&v.toObject())); + compartmentRoot = UncheckedUnwrap(&v.toObject()); + creationOptions.setNewCompartmentInExistingZone(compartmentRoot); } if (!JS_GetProperty(cx, opts, "sameCompartmentAs", &v)) { return false; } if (v.isObject()) { - creationOptions.setExistingCompartment(UncheckedUnwrap(&v.toObject())); + compartmentRoot = UncheckedUnwrap(&v.toObject()); + creationOptions.setExistingCompartment(compartmentRoot); } if (!JS_GetProperty(cx, opts, "newCompartment", &v)) { @@ -12000,10 +12024,8 @@ bool InitOptionParser(OptionParser& op) { "property of null or undefined") || !op.addBoolOption('\0', "enable-iterator-helpers", "Enable iterator helpers") || -#ifdef ENABLE_JSON_PARSE_WITH_SOURCE !op.addBoolOption('\0', "enable-json-parse-with-source", "Enable JSON.parse with source") || -#endif !op.addBoolOption('\0', "enable-shadow-realms", "Enable ShadowRealms") || !op.addBoolOption('\0', "disable-array-grouping", "Disable Object.groupBy and Map.groupBy") || @@ -12019,6 +12041,8 @@ bool InitOptionParser(OptionParser& op) { !op.addBoolOption( '\0', "enable-arraybuffer-resizable", "Enable resizable ArrayBuffers and growable SharedArrayBuffers") || + !op.addBoolOption('\0', "enable-uint8array-base64", + "Enable Uint8Array base64/hex methods") || !op.addBoolOption('\0', "enable-top-level-await", "Enable top-level await") || !op.addBoolOption('\0', "enable-class-static-blocks", @@ -12407,6 +12431,17 @@ bool SetGlobalOptionsPreJSInit(const OptionParser& op) { if (op.getBoolOption("enable-symbols-as-weakmap-keys")) { JS::Prefs::setAtStartup_experimental_symbols_as_weakmap_keys(true); } + if (op.getBoolOption("enable-uint8array-base64")) { + JS::Prefs::setAtStartup_experimental_uint8array_base64(true); + } +#endif +#ifdef ENABLE_JSON_PARSE_WITH_SOURCE + JS::Prefs::setAtStartup_experimental_json_parse_with_source( + op.getBoolOption("enable-json-parse-with-source")); +#else + if (op.getBoolOption("enable-json-parse-with-source")) { + fprintf(stderr, "JSON.parse with source is not enabled on this build.\n"); + } #endif if (op.getBoolOption("disable-weak-refs")) { @@ -12627,9 +12662,6 @@ bool SetContextOptions(JSContext* cx, const OptionParser& op) { enableAsyncStackCaptureDebuggeeOnly = op.getBoolOption("async-stacks-capture-debuggee-only"); enableToSource = !op.getBoolOption("disable-tosource"); -#ifdef ENABLE_JSON_PARSE_WITH_SOURCE - enableJSONParseWithSource = op.getBoolOption("enable-json-parse-with-source"); -#endif enableImportAttributesAssertSyntax = op.getBoolOption("enable-import-assertions"); enableImportAttributes = op.getBoolOption("enable-import-attributes") || diff --git a/js/src/shell/jsshell.h b/js/src/shell/jsshell.h index 9cbf4505f9..67536a8f60 100644 --- a/js/src/shell/jsshell.h +++ b/js/src/shell/jsshell.h @@ -127,9 +127,6 @@ extern bool enableWellFormedUnicodeStrings; extern bool enableArrayBufferTransfer; extern bool enableArrayBufferResizable; extern bool enableSymbolsAsWeakMapKeys; -#ifdef ENABLE_JSON_PARSE_WITH_SOURCE -extern bool enableJSONParseWithSource; -#endif extern bool enableNewSetMethods; extern bool enableImportAttributes; extern bool enableImportAttributesAssertSyntax; diff --git a/js/src/tests/jstests.list b/js/src/tests/jstests.list index 55ffc65f6f..dbe3bd555e 100644 --- a/js/src/tests/jstests.list +++ b/js/src/tests/jstests.list @@ -1368,16 +1368,6 @@ skip script test262/built-ins/Temporal/ZonedDateTime/prototype/since/normalized- skip script test262/built-ins/Temporal/ZonedDateTime/prototype/hoursInDay/precision-exact-mathematical-values-2.js ############################################## -# Iterator helper tests # -############################################## - -# Failures with --enable-iterator-helpers, see bug 1877831. -shell-option(--enable-iterator-helpers) skip script test262/built-ins/Iterator/prototype/constructor/prop-desc.js -shell-option(--enable-iterator-helpers) skip script test262/built-ins/Iterator/prototype/constructor/weird-setter.js -shell-option(--enable-iterator-helpers) skip script test262/built-ins/Iterator/prototype/Symbol.toStringTag/prop-desc.js -shell-option(--enable-iterator-helpers) skip script test262/built-ins/Iterator/prototype/Symbol.toStringTag/weird-setter.js - -############################################## # AsyncFromSyncIteratorPrototype tests # ############################################## diff --git a/js/src/tests/lib/jittests.py b/js/src/tests/lib/jittests.py index 4e7be87255..7d79ba9f2a 100755 --- a/js/src/tests/lib/jittests.py +++ b/js/src/tests/lib/jittests.py @@ -343,6 +343,13 @@ class JitTest: elif name.startswith("--"): # // |jit-test| --ion-gvn=off; --no-sse4 test.jitflags.append(name) + elif name.startswith("-P"): + prefAndValue = name.split() + assert ( + len(prefAndValue) == 2 + ), f"{name}: failed to parse preference" + # // |jit-test| -P pref(=value)? + test.jitflags.append("--setpref=" + prefAndValue[1]) else: print( "{}: warning: unrecognized |jit-test| attribute" diff --git a/js/src/tests/non262/Date/parse-day-of-week.js b/js/src/tests/non262/Date/parse-day-of-week.js index 08bcd3ee05..52a8c6339a 100644 --- a/js/src/tests/non262/Date/parse-day-of-week.js +++ b/js/src/tests/non262/Date/parse-day-of-week.js @@ -63,6 +63,8 @@ const rejected = [ "Sep 26 1995 foo", "1995 foo Sep 26", "foo2 Sep 26 1995", + "Tuesday_Sep 26 1995", + "foo_12", ]; for (const format of formats) { diff --git a/js/src/tests/non262/Iterator/proto.js b/js/src/tests/non262/Iterator/proto.js index 642e48d171..5b32f5d34b 100644 --- a/js/src/tests/non262/Iterator/proto.js +++ b/js/src/tests/non262/Iterator/proto.js @@ -10,14 +10,5 @@ assertEq(propDesc.writable, false); assertEq(propDesc.enumerable, false); assertEq(propDesc.configurable, false); -// Make sure @@toStringTag has been set. -const toStringTagDesc = Reflect.getOwnPropertyDescriptor(Iterator.prototype, Symbol.toStringTag); -assertDeepEq(toStringTagDesc, { - value: "Iterator", - writable: true, - enumerable: false, - configurable: true, -}); - if (typeof reportCompare === 'function') reportCompare(0, 0); diff --git a/js/src/tests/non262/JSON/parse-with-source.js b/js/src/tests/non262/JSON/parse-with-source.js index 96e9550706..5e144bd6a4 100644 --- a/js/src/tests/non262/JSON/parse-with-source.js +++ b/js/src/tests/non262/JSON/parse-with-source.js @@ -1,49 +1,103 @@ -// |reftest| skip-if(!(this.hasOwnProperty('getBuildConfiguration')&&getBuildConfiguration('json-parse-with-source'))) shell-option(--enable-json-parse-with-source) +// |reftest| shell-option(--enable-json-parse-with-source) skip-if(!JSON.hasOwnProperty('isRawJSON')||!xulRuntime.shell) var actual; function reviver(key, value, context) { assertEq(arguments.length, 3); - assertEq("source" in context, true); - actual = context["source"]; + if ("source" in context) { + actual.push(context["source"]); + } else { // objects and arrays have no "source" + actual.push(null); + } } let tests = [ // STRINGS - {input: '""', expected: '""'}, - {input: '"str"', expected: '"str"'}, - {input: '"str" ', expected: '"str"'}, - {input: ' "str" ', expected: '"str"'}, - {input: ' " str"', expected: '" str"'}, - {input: '"\uD83D\uDE0A\u2764\u2FC1"', expected: '"\uD83D\uDE0A\u2764\u2FC1"'}, + {input: '""', expected: ['""']}, + {input: '"str"', expected: ['"str"']}, + {input: '"str" ', expected: ['"str"']}, + {input: ' "str" ', expected: ['"str"']}, + {input: ' " str"', expected: ['" str"']}, + {input: '"\uD83D\uDE0A\u2764\u2FC1"', expected: ['"\uD83D\uDE0A\u2764\u2FC1"']}, // NUMBERS - {input: '1', expected: '1'}, - {input: ' 1', expected: '1'}, - {input: '4.2', expected: '4.2'}, - {input: '4.2 ', expected: '4.2'}, - {input: '4.2000 ', expected: '4.2000'}, - {input: '4e2000 ', expected: '4e2000'}, - {input: '4.4e2000 ', expected: '4.4e2000'}, - {input: '9007199254740999', expected: '9007199254740999'}, - {input: '-31', expected: '-31'}, - {input: '-3.1', expected: '-3.1'}, - {input: ' -31 ', expected: '-31'}, + {input: '1', expected: ['1']}, + {input: ' 1', expected: ['1']}, + {input: '4.2', expected: ['4.2']}, + {input: '4.2 ', expected: ['4.2']}, + {input: '4.2000 ', expected: ['4.2000']}, + {input: '4e2000 ', expected: ['4e2000']}, + {input: '4.4e2000 ', expected: ['4.4e2000']}, + {input: '9007199254740999', expected: ['9007199254740999']}, + {input: '-31', expected: ['-31']}, + {input: '-3.1', expected: ['-3.1']}, + {input: ' -31 ', expected: ['-31']}, // BOOLEANS - {input: 'true', expected: 'true'}, - {input: 'true ', expected: 'true'}, - {input: 'false', expected: 'false'}, - {input: ' false', expected: 'false'}, + {input: 'true', expected: ['true']}, + {input: 'true ', expected: ['true']}, + {input: 'false', expected: ['false']}, + {input: ' false', expected: ['false']}, // NULL - {input: 'null', expected: 'null'}, - {input: ' null', expected: 'null'}, - {input: '\tnull ', expected: 'null'}, - {input: 'null\t', expected: 'null'}, + {input: 'null', expected: ['null']}, + {input: ' null', expected: ['null']}, + {input: '\tnull ', expected: ['null']}, + {input: 'null\t', expected: ['null']}, + // OBJECTS + {input: '{ }', expected: [null]}, + {input: '{ "4": 1 }', expected: ['1', null]}, + {input: '{ "a": 1 }', expected: ['1', null]}, + {input: '{ "b": 2, "a": 1 }', expected: ['2', '1', null]}, + {input: '{ "b": 2, "1": 1 }', expected: ['1', '2', null]}, + {input: '{ "b": 2, "c": null }', expected: ['2', 'null', null]}, + {input: '{ "b": 2, "b": 1, "b": 4 }', expected: ['4', null]}, + {input: '{ "b": 2, "a": "1" }', expected: ['2', '"1"', null]}, + {input: '{ "b": { "c": 3 }, "a": 1 }', expected: ['3', null, '1', null]}, + // ARRAYS + {input: '[]', expected: [null]}, + {input: '[1, 5, 2]', expected: ['1', '5', '2', null]}, + {input: '[1, null, 2]', expected: ['1', 'null', '2', null]}, + {input: '[1, {"a":2}, "7"]', expected: ['1', '2', null, '"7"', null]}, + {input: '[1, [2, [3, [4, 5], [6, 7], 8], 9], 10]', expected: ['1', '2', '3', '4', '5', null, '6', '7', null, '8', null, '9', null, '10', null]}, + {input: '[1, [2, [3, [4, 5, 6, 7, 8, 9, 10], []]]]', expected: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', null, null, null, null, null]}, + {input: '{"a": [1, {"b":2}, "7"], "c": 8}', expected: ['1', '2', null, '"7"', null, '8', null]}, ]; for (const test of tests) { print("Testing " + JSON.stringify(test)); + actual = []; JSON.parse(test.input, reviver); - assertEq(actual, test.expected); + assertDeepEq(actual, test.expected); } +// If the constructed object is modified but the result of the modification is +// the same as the original, we should still provide the source +function replace_c_with_same_val(key, value, context) { + if (key === "a") { + this["c"] = "ABCDEABCDE"; + } + if (key) { + assertEq("source" in context, true); + } + return value; +} +JSON.parse('{ "a": "b", "c": "ABCDEABCDE" }', replace_c_with_same_val); + +// rawJSON +function assertIsRawJson(rawJson, expectedRawJsonValue) { + assertEq(null, Object.getPrototypeOf(rawJson)); + assertEq(true, Object.hasOwn(rawJson, 'rawJSON')); + assertDeepEq(['rawJSON'], Object.getOwnPropertyNames(rawJson)); + assertDeepEq([], Object.getOwnPropertySymbols(rawJson)); + assertEq(expectedRawJsonValue, rawJson.rawJSON); +} + +assertEq(true, Object.isFrozen(JSON.rawJSON('"shouldBeFrozen"'))); +assertThrowsInstanceOf(() => JSON.rawJSON(), SyntaxError); +assertIsRawJson(JSON.rawJSON(1, 2), '1'); + +// isRawJSON +assertEq(false, JSON.isRawJSON()); +assertEq(false, JSON.isRawJSON({}, {})); +assertEq(false, JSON.isRawJSON({}, JSON.rawJSON(2))); +assertEq(true, JSON.isRawJSON(JSON.rawJSON(1), JSON.rawJSON(2))); + if (typeof reportCompare == 'function') reportCompare(0, 0);
\ No newline at end of file diff --git a/js/src/tests/non262/PrivateName/nested-class-in-computed-property-key.js b/js/src/tests/non262/PrivateName/nested-class-in-computed-property-key.js new file mode 100644 index 0000000000..7596dfe542 --- /dev/null +++ b/js/src/tests/non262/PrivateName/nested-class-in-computed-property-key.js @@ -0,0 +1,17 @@ +let capturedPrivateAccess; +class A { + // Declare private name in outer class. + static #x = 42; + + static [( + // Inner class in computed property key. + class {}, + + // Access to private name from outer class. + capturedPrivateAccess = () => A.#x + )]; +} +assertEq(capturedPrivateAccess(), 42); + +if (typeof reportCompare === 'function') + reportCompare(0, 0); diff --git a/js/src/tests/non262/module/bug1689499.js b/js/src/tests/non262/module/bug1689499.js index 859cd6f64f..2547c88d16 100644 --- a/js/src/tests/non262/module/bug1689499.js +++ b/js/src/tests/non262/module/bug1689499.js @@ -1,4 +1,4 @@ -// |reftest| skip-if(!xulRuntime.shell) module async -- needs drainJobQueue +// |reftest| skip-if(!xulRuntime.shell) async -- needs drainJobQueue async function test() { try { diff --git a/js/src/tests/test262-update.py b/js/src/tests/test262-update.py index fc6fa62c45..d45d639ebb 100755 --- a/js/src/tests/test262-update.py +++ b/js/src/tests/test262-update.py @@ -24,7 +24,6 @@ UNSUPPORTED_FEATURES = set( "Atomics.waitAsync", # Bug 1467846 "legacy-regexp", # Bug 1306461 "regexp-duplicate-named-groups", # Bug 1773135 - "json-parse-with-source", # Bug 1658310 "set-methods", # Bug 1805038 ] ) @@ -38,6 +37,8 @@ FEATURE_CHECK_NEEDED = { "iterator-helpers": "!this.hasOwnProperty('Iterator')", # Bug 1568906 "Intl.Segmenter": "!Intl.Segmenter", # Bug 1423593 "resizable-arraybuffer": "!ArrayBuffer.prototype.resize", # Bug 1670026 + "uint8array-base64": "!Uint8Array.fromBase64", # Bug 1862220 + "json-parse-with-source": "!JSON.hasOwnProperty('isRawJSON')", # Bug 1658310 } RELEASE_OR_BETA = set( [ @@ -51,6 +52,8 @@ SHELL_OPTIONS = { "iterator-helpers": "--enable-iterator-helpers", "symbols-as-weakmap-keys": "--enable-symbols-as-weakmap-keys", "resizable-arraybuffer": "--enable-arraybuffer-resizable", + "uint8array-base64": "--enable-uint8array-base64", + "json-parse-with-source": "--enable-json-parse-with-source", } diff --git a/js/src/tests/test262/prs/3994/browser.js b/js/src/tests/test262/prs/3994/browser.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/prs/3994/browser.js diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/browser.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/browser.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/browser.js diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/alphabet.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/alphabet.js new file mode 100644 index 0000000000..c893bf952a --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/alphabet.js @@ -0,0 +1,25 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.frombase64 +description: Conversion of base64 strings to Uint8Arrays exercising the alphabet option +includes: [compareArray.js] +features: [uint8array-base64, TypedArray] +---*/ + +assert.compareArray(Uint8Array.fromBase64('x+/y'), [199, 239, 242]); +assert.compareArray(Uint8Array.fromBase64('x+/y', { alphabet: 'base64' }), [199, 239, 242]); +assert.throws(SyntaxError, function() { + Uint8Array.fromBase64('x+/y', { alphabet: 'base64url' }); +}); + +assert.compareArray(Uint8Array.fromBase64('x-_y', { alphabet: 'base64url' }), [199, 239, 242]); +assert.throws(SyntaxError, function() { + Uint8Array.fromBase64('x-_y'); +}); +assert.throws(SyntaxError, function() { + Uint8Array.fromBase64('x-_y', { alphabet: 'base64' }); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/browser.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/browser.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/browser.js diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/descriptor.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/descriptor.js new file mode 100644 index 0000000000..a0e843cfaf --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/descriptor.js @@ -0,0 +1,18 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.frombase64 +description: > + Uint8Array.fromBase64 has default data property attributes. +includes: [propertyHelper.js] +features: [uint8array-base64, TypedArray] +---*/ + +verifyProperty(Uint8Array, 'fromBase64', { + enumerable: false, + writable: true, + configurable: true +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/ignores-receiver.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/ignores-receiver.js new file mode 100644 index 0000000000..0847653d63 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/ignores-receiver.js @@ -0,0 +1,22 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.frombase64 +description: Uint8Array.fromBase64 ignores its receiver +features: [uint8array-base64, TypedArray] +---*/ + +var fromBase64 = Uint8Array.fromBase64; +var noReceiver = fromBase64("Zg=="); +assert.sameValue(Object.getPrototypeOf(noReceiver), Uint8Array.prototype); + +class Subclass extends Uint8Array { + constructor() { + throw new Test262Error("subclass constructor called"); + } +} +var fromSubclass = Subclass.fromBase64("Zg=="); +assert.sameValue(Object.getPrototypeOf(fromSubclass), Uint8Array.prototype); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/illegal-characters.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/illegal-characters.js new file mode 100644 index 0000000000..6c2d6eef4a --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/illegal-characters.js @@ -0,0 +1,26 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.frombase64 +description: Uint8Array.fromBase64 throws a SyntaxError when input has non-base64, non-ascii-whitespace characters +features: [uint8array-base64, TypedArray] +---*/ + +var illegal = [ + 'Zm.9v', + 'Zm9v^', + 'Zg==&', + 'Z−==', // U+2212 'Minus Sign' + 'Z+==', // U+FF0B 'Fullwidth Plus Sign' + 'Zg\u00A0==', // nbsp + 'Zg\u2009==', // thin space + 'Zg\u2028==', // line separator +]; +illegal.forEach(function(value) { + assert.throws(SyntaxError, function() { + Uint8Array.fromBase64(value) + }); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/last-chunk-handling.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/last-chunk-handling.js new file mode 100644 index 0000000000..c78d15e869 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/last-chunk-handling.js @@ -0,0 +1,67 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.frombase64 +description: Handling of final chunks in Uint8Array.fromBase64 +includes: [compareArray.js] +features: [uint8array-base64, TypedArray] +---*/ + +// padding +assert.compareArray(Uint8Array.fromBase64('ZXhhZg=='), [101, 120, 97, 102]); +assert.compareArray(Uint8Array.fromBase64('ZXhhZg==', { lastChunkHandling: 'loose' }), [101, 120, 97, 102]); +assert.compareArray(Uint8Array.fromBase64('ZXhhZg==', { lastChunkHandling: 'stop-before-partial' }), [101, 120, 97, 102]); +assert.compareArray(Uint8Array.fromBase64('ZXhhZg==', { lastChunkHandling: 'strict' }), [101, 120, 97, 102]); + +// no padding +assert.compareArray(Uint8Array.fromBase64('ZXhhZg'), [101, 120, 97, 102]); +assert.compareArray(Uint8Array.fromBase64('ZXhhZg', { lastChunkHandling: 'loose' }), [101, 120, 97, 102]); +assert.compareArray(Uint8Array.fromBase64('ZXhhZg', { lastChunkHandling: 'stop-before-partial' }), [101, 120, 97]); +assert.throws(SyntaxError, function() { + Uint8Array.fromBase64('ZXhhZg', { lastChunkHandling: 'strict' }); +}); + +// non-zero padding bits +assert.compareArray(Uint8Array.fromBase64('ZXhhZh=='), [101, 120, 97, 102]); +assert.compareArray(Uint8Array.fromBase64('ZXhhZh==', { lastChunkHandling: 'loose' }), [101, 120, 97, 102]); +assert.compareArray(Uint8Array.fromBase64('ZXhhZh==', { lastChunkHandling: 'stop-before-partial' }), [101, 120, 97, 102]); +assert.throws(SyntaxError, function() { + Uint8Array.fromBase64('ZXhhZh==', { lastChunkHandling: 'strict' }); +}); + +// non-zero padding bits, no padding +assert.compareArray(Uint8Array.fromBase64('ZXhhZh'), [101, 120, 97, 102]); +assert.compareArray(Uint8Array.fromBase64('ZXhhZh', { lastChunkHandling: 'loose' }), [101, 120, 97, 102]); +assert.compareArray(Uint8Array.fromBase64('ZXhhZh', { lastChunkHandling: 'stop-before-partial' }), [101, 120, 97]); +assert.throws(SyntaxError, function() { + Uint8Array.fromBase64('ZXhhZh', { lastChunkHandling: 'strict' }); +}); + +// partial padding +assert.throws(SyntaxError, function() { + Uint8Array.fromBase64('ZXhhZg='); +}); +assert.throws(SyntaxError, function() { + Uint8Array.fromBase64('ZXhhZg=', { lastChunkHandling: 'loose' }); +}); +assert.compareArray(Uint8Array.fromBase64('ZXhhZg=', { lastChunkHandling: 'stop-before-partial' }), [101, 120, 97]); +assert.throws(SyntaxError, function() { + Uint8Array.fromBase64('ZXhhZg=', { lastChunkHandling: 'strict' }); +}); + +// excess padding +assert.throws(SyntaxError, function() { + Uint8Array.fromBase64('ZXhhZg==='); +}); +assert.throws(SyntaxError, function() { + Uint8Array.fromBase64('ZXhhZg===', { lastChunkHandling: 'loose' }); +}); +assert.throws(SyntaxError, function() { + Uint8Array.fromBase64('ZXhhZg===', { lastChunkHandling: 'stop-before-partial' }); +}); +assert.throws(SyntaxError, function() { + Uint8Array.fromBase64('ZXhhZg===', { lastChunkHandling: 'strict' }); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/length.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/length.js new file mode 100644 index 0000000000..dab9b49f61 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/length.js @@ -0,0 +1,19 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.frombase64 +description: > + Uint8Array.fromBase64.length is 1. +includes: [propertyHelper.js] +features: [uint8array-base64, TypedArray] +---*/ + +verifyProperty(Uint8Array.fromBase64, 'length', { + value: 1, + enumerable: false, + writable: false, + configurable: true +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/name.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/name.js new file mode 100644 index 0000000000..2bd6716863 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/name.js @@ -0,0 +1,19 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.frombase64 +description: > + Uint8Array.fromBase64.name is "fromBase64". +includes: [propertyHelper.js] +features: [uint8array-base64, TypedArray] +---*/ + +verifyProperty(Uint8Array.fromBase64, 'name', { + value: 'fromBase64', + enumerable: false, + writable: false, + configurable: true +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/nonconstructor.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/nonconstructor.js new file mode 100644 index 0000000000..7546b12b6b --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/nonconstructor.js @@ -0,0 +1,18 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.frombase64 +description: > + Uint8Array.fromBase64 is not a constructor function. +includes: [isConstructor.js] +features: [uint8array-base64, TypedArray, Reflect.construct] +---*/ + +assert(!isConstructor(Uint8Array.fromBase64), "Uint8Array.fromBase64 is not a constructor"); + +assert.throws(TypeError, function() { + new Uint8Array.fromBase64(''); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/option-coercion.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/option-coercion.js new file mode 100644 index 0000000000..e314a8f16f --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/option-coercion.js @@ -0,0 +1,62 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.frombase64 +description: Uint8Array.fromBase64 triggers effects of the "alphabet" and "lastChunkHandling" getters, but does not perform toString on the results +includes: [compareArray.js] +features: [uint8array-base64, TypedArray] +---*/ + +assert.throws(TypeError, function() { + Uint8Array.fromBase64("Zg==", { alphabet: Object("base64") }); +}); + +assert.throws(TypeError, function() { + Uint8Array.fromBase64("Zg==", { lastChunkHandling: Object("loose") }); +}); + + +var toStringCalls = 0; +var throwyToString = { + toString: function() { + toStringCalls += 1; + throw new Test262Error("toString called"); + } +}; +assert.throws(TypeError, function() { + Uint8Array.fromBase64("Zg==", { alphabet: throwyToString }); +}); +assert.sameValue(toStringCalls, 0); + +assert.throws(TypeError, function() { + Uint8Array.fromBase64("Zg==", { lastChunkHandling: throwyToString }); +}); +assert.sameValue(toStringCalls, 0); + + +var alphabetAccesses = 0; +var base64UrlOptions = {}; +Object.defineProperty(base64UrlOptions, "alphabet", { + get: function() { + alphabetAccesses += 1; + return "base64url"; + } +}); +var arr = Uint8Array.fromBase64("x-_y", base64UrlOptions); +assert.compareArray(arr, [199, 239, 242]); +assert.sameValue(alphabetAccesses, 1); + +var lastChunkHandlingAccesses = 0; +var strictOptions = {}; +Object.defineProperty(strictOptions, "lastChunkHandling", { + get: function() { + lastChunkHandlingAccesses += 1; + return "strict"; + } +}); +var arr = Uint8Array.fromBase64("Zg==", strictOptions); +assert.compareArray(arr, [102]); +assert.sameValue(lastChunkHandlingAccesses, 1); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/results.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/results.js new file mode 100644 index 0000000000..9d19507f9a --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/results.js @@ -0,0 +1,30 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.frombase64 +description: Conversion of base64 strings to Uint8Arrays +includes: [compareArray.js] +features: [uint8array-base64, TypedArray] +---*/ + +// standard test vectors from https://datatracker.ietf.org/doc/html/rfc4648#section-10 +var standardBase64Vectors = [ + ["", []], + ["Zg==", [102]], + ["Zm8=", [102, 111]], + ["Zm9v", [102, 111, 111]], + ["Zm9vYg==", [102, 111, 111, 98]], + ["Zm9vYmE=", [102, 111, 111, 98, 97]], + ["Zm9vYmFy", [102, 111, 111, 98, 97, 114]], +]; + +standardBase64Vectors.forEach(function (pair) { + var arr = Uint8Array.fromBase64(pair[0]); + assert.sameValue(Object.getPrototypeOf(arr), Uint8Array.prototype, "decoding " + pair[0]); + assert.sameValue(arr.length, pair[1].length, "decoding " + pair[0]); + assert.sameValue(arr.buffer.byteLength, pair[1].length, "decoding " + pair[0]); + assert.compareArray(arr, pair[1], "decoding " + pair[0]); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/shell.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/shell.js new file mode 100644 index 0000000000..eda1477282 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/shell.js @@ -0,0 +1,24 @@ +// GENERATED, DO NOT EDIT +// file: isConstructor.js +// Copyright (C) 2017 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +description: | + Test if a given function is a constructor function. +defines: [isConstructor] +features: [Reflect.construct] +---*/ + +function isConstructor(f) { + if (typeof f !== "function") { + throw new Test262Error("isConstructor invoked with a non-function value"); + } + + try { + Reflect.construct(function(){}, [], f); + } catch (e) { + return false; + } + return true; +} diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/string-coercion.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/string-coercion.js new file mode 100644 index 0000000000..57292f8a98 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/string-coercion.js @@ -0,0 +1,44 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.frombase64 +description: Uint8Array.fromBase64 throws if its argument is not a string +features: [uint8array-base64, TypedArray] +---*/ + +var toStringCalls = 0; +var throwyToString = { + toString: function() { + toStringCalls += 1; + throw new Test262Error("toString called"); + } +}; + +assert.throws(TypeError, function() { + Uint8Array.fromBase64(throwyToString); +}); +assert.sameValue(toStringCalls, 0); + + +var optionAccesses = 0; +var touchyOptions = {}; +Object.defineProperty(touchyOptions, "alphabet", { + get: function() { + optionAccesses += 1; + throw new Test262Error("alphabet accessed"); + } +}); +Object.defineProperty(touchyOptions, "lastChunkHandling", { + get: function() { + optionAccesses += 1; + throw new Test262Error("lastChunkHandling accessed"); + } +}); +assert.throws(TypeError, function() { + Uint8Array.fromBase64(throwyToString, touchyOptions); +}); +assert.sameValue(toStringCalls, 0); +assert.sameValue(optionAccesses, 0); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/whitespace.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/whitespace.js new file mode 100644 index 0000000000..87c79a6563 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromBase64/whitespace.js @@ -0,0 +1,25 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.frombase64 +description: Uint8Array.fromBase64 ignores ASCII whitespace in the input +includes: [compareArray.js] +features: [uint8array-base64, TypedArray] +---*/ + +var whitespaceKinds = [ + ["Z g==", "space"], + ["Z\tg==", "tab"], + ["Z\x0Ag==", "LF"], + ["Z\x0Cg==", "FF"], + ["Z\x0Dg==", "CR"], +]; +whitespaceKinds.forEach(function(pair) { + var arr = Uint8Array.fromBase64(pair[0]); + assert.sameValue(arr.length, 1); + assert.sameValue(arr.buffer.byteLength, 1); + assert.compareArray(arr, [102], "ascii whitespace: " + pair[1]); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/browser.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/browser.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/browser.js diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/descriptor.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/descriptor.js new file mode 100644 index 0000000000..3d53652054 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/descriptor.js @@ -0,0 +1,18 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.fromhex +description: > + Uint8Array.fromHex has default data property attributes. +includes: [propertyHelper.js] +features: [uint8array-base64, TypedArray] +---*/ + +verifyProperty(Uint8Array, 'fromHex', { + enumerable: false, + writable: true, + configurable: true +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/ignores-receiver.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/ignores-receiver.js new file mode 100644 index 0000000000..421a0278d7 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/ignores-receiver.js @@ -0,0 +1,22 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.fromhex +description: Uint8Array.fromHex ignores its receiver +features: [uint8array-base64, TypedArray] +---*/ + +var fromHex = Uint8Array.fromHex; +var noReceiver = fromHex("aa"); +assert.sameValue(Object.getPrototypeOf(noReceiver), Uint8Array.prototype); + +class Subclass extends Uint8Array { + constructor() { + throw new Test262Error("subclass constructor called"); + } +} +var fromSubclass = Subclass.fromHex("aa"); +assert.sameValue(Object.getPrototypeOf(fromSubclass), Uint8Array.prototype); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/illegal-characters.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/illegal-characters.js new file mode 100644 index 0000000000..718801e4c7 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/illegal-characters.js @@ -0,0 +1,28 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.fromhex +description: Uint8Array.fromHex throws a SyntaxError when input has non-hex characters +features: [uint8array-base64, TypedArray] +---*/ + +var illegal = [ + 'a.a', + 'aa^', + 'a a', + 'a\ta', + 'a\x0Aa', + 'a\x0Ca', + 'a\x0Da', + 'a\u00A0a', // nbsp + 'a\u2009a', // thin space + 'a\u2028a', // line separator +]; +illegal.forEach(function(value) { + assert.throws(SyntaxError, function() { + Uint8Array.fromHex(value) + }); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/length.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/length.js new file mode 100644 index 0000000000..9108a1c2c1 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/length.js @@ -0,0 +1,19 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.fromhex +description: > + Uint8Array.fromHex.length is 1. +includes: [propertyHelper.js] +features: [uint8array-base64, TypedArray] +---*/ + +verifyProperty(Uint8Array.fromHex, 'length', { + value: 1, + enumerable: false, + writable: false, + configurable: true +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/name.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/name.js new file mode 100644 index 0000000000..7d4e145349 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/name.js @@ -0,0 +1,19 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.fromhex +description: > + Uint8Array.fromHex.name is "fromHex". +includes: [propertyHelper.js] +features: [uint8array-base64, TypedArray] +---*/ + +verifyProperty(Uint8Array.fromHex, 'name', { + value: 'fromHex', + enumerable: false, + writable: false, + configurable: true +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/nonconstructor.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/nonconstructor.js new file mode 100644 index 0000000000..6e34171e77 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/nonconstructor.js @@ -0,0 +1,18 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.fromhex +description: > + Uint8Array.fromHex is not a constructor function. +includes: [isConstructor.js] +features: [uint8array-base64, TypedArray, Reflect.construct] +---*/ + +assert(!isConstructor(Uint8Array.fromHex), "Uint8Array.fromHex is not a constructor"); + +assert.throws(TypeError, function() { + new Uint8Array.fromHex(''); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/odd-length-input.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/odd-length-input.js new file mode 100644 index 0000000000..fe7860a083 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/odd-length-input.js @@ -0,0 +1,14 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.fromhex +description: Uint8Array.fromHex throws if given an odd number of input hex characters +features: [uint8array-base64, TypedArray] +---*/ + +assert.throws(SyntaxError, function() { + Uint8Array.fromHex('a'); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/results.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/results.js new file mode 100644 index 0000000000..d40beab7cd --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/results.js @@ -0,0 +1,31 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.fromhex +description: Conversion of hex strings to Uint8Arrays +includes: [compareArray.js] +features: [uint8array-base64, TypedArray] +---*/ + +var cases = [ + ["", []], + ["66", [102]], + ["666f", [102, 111]], + ["666F", [102, 111]], + ["666f6f", [102, 111, 111]], + ["666F6f", [102, 111, 111]], + ["666f6f62", [102, 111, 111, 98]], + ["666f6f6261", [102, 111, 111, 98, 97]], + ["666f6f626172", [102, 111, 111, 98, 97, 114]], +]; + +cases.forEach(function (pair) { + var arr = Uint8Array.fromHex(pair[0]); + assert.sameValue(Object.getPrototypeOf(arr), Uint8Array.prototype, "decoding " + pair[0]); + assert.sameValue(arr.length, pair[1].length, "decoding " + pair[0]); + assert.sameValue(arr.buffer.byteLength, pair[1].length, "decoding " + pair[0]); + assert.compareArray(arr, pair[1], "decoding " + pair[0]); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/shell.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/shell.js new file mode 100644 index 0000000000..eda1477282 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/shell.js @@ -0,0 +1,24 @@ +// GENERATED, DO NOT EDIT +// file: isConstructor.js +// Copyright (C) 2017 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +description: | + Test if a given function is a constructor function. +defines: [isConstructor] +features: [Reflect.construct] +---*/ + +function isConstructor(f) { + if (typeof f !== "function") { + throw new Test262Error("isConstructor invoked with a non-function value"); + } + + try { + Reflect.construct(function(){}, [], f); + } catch (e) { + return false; + } + return true; +} diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/string-coercion.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/string-coercion.js new file mode 100644 index 0000000000..fea57227fc --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/fromHex/string-coercion.js @@ -0,0 +1,23 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.fromhex +description: Uint8Array.fromHex throws if its argument is not a string +features: [uint8array-base64, TypedArray] +---*/ + +var toStringCalls = 0; +var throwyToString = { + toString: function() { + toStringCalls += 1; + throw new Test262Error("toString called"); + } +}; + +assert.throws(TypeError, function() { + Uint8Array.fromHex(throwyToString); +}); +assert.sameValue(toStringCalls, 0); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/browser.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/browser.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/browser.js diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/alphabet.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/alphabet.js new file mode 100644 index 0000000000..bb4acdb8f2 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/alphabet.js @@ -0,0 +1,44 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.setfrombase64 +description: Conversion of base64 strings to Uint8Arrays exercising the alphabet option +includes: [compareArray.js] +features: [uint8array-base64, TypedArray] +---*/ + +var target = new Uint8Array([255, 255, 255, 255]); +var result = target.setFromBase64('x+/y'); +assert.sameValue(result.read, 4); +assert.sameValue(result.written, 3); +assert.compareArray(target, [199, 239, 242, 255]); + +var target = new Uint8Array([255, 255, 255, 255]); +var result = target.setFromBase64('x+/y', { alphabet: 'base64' }); +assert.sameValue(result.read, 4); +assert.sameValue(result.written, 3); +assert.compareArray(target, [199, 239, 242, 255]); + +assert.throws(SyntaxError, function() { + var target = new Uint8Array([255, 255, 255, 255]); + target.setFromBase64('x+/y', { alphabet: 'base64url' }); +}); + + +var target = new Uint8Array([255, 255, 255, 255]); +var result = target.setFromBase64('x-_y', { alphabet: 'base64url' }); +assert.sameValue(result.read, 4); +assert.sameValue(result.written, 3); +assert.compareArray(target, [199, 239, 242, 255]); + +assert.throws(SyntaxError, function() { + var target = new Uint8Array([255, 255, 255, 255]); + target.setFromBase64('x-_y'); +}); +assert.throws(SyntaxError, function() { + var target = new Uint8Array([255, 255, 255, 255]); + target.setFromBase64('x-_y', { alphabet: 'base64' }); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/browser.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/browser.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/browser.js diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/descriptor.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/descriptor.js new file mode 100644 index 0000000000..bd319f7d43 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/descriptor.js @@ -0,0 +1,18 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.setfrombase64 +description: > + Uint8Array.prototype.setFromBase64 has default data property attributes. +includes: [propertyHelper.js] +features: [uint8array-base64, TypedArray] +---*/ + +verifyProperty(Uint8Array.prototype, 'setFromBase64', { + enumerable: false, + writable: true, + configurable: true +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/detached-buffer.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/detached-buffer.js new file mode 100644 index 0000000000..3dd042bb02 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/detached-buffer.js @@ -0,0 +1,32 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.setfrombase64 +description: Uint8Array.prototype.setFromBase64 throws on detatched buffers +includes: [detachArrayBuffer.js] +features: [uint8array-base64, TypedArray] +---*/ + +var target = new Uint8Array([255, 255, 255]); +$DETACHBUFFER(target.buffer); +assert.throws(TypeError, function() { + target.setFromBase64('Zg=='); +}); + +var getterCalls = 0; +var targetDetachingOptions = {}; +Object.defineProperty(targetDetachingOptions, 'alphabet', { + get: function() { + getterCalls += 1; + $DETACHBUFFER(target.buffer); + return "base64"; + } +}); +var target = new Uint8Array([255, 255, 255]); +assert.throws(TypeError, function() { + target.setFromBase64('Zg==', targetDetachingOptions); +}); +assert.sameValue(getterCalls, 1); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/illegal-characters.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/illegal-characters.js new file mode 100644 index 0000000000..9c6b827df8 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/illegal-characters.js @@ -0,0 +1,27 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.setfrombase64 +description: Uint8Array.prototype.setFromBase64 throws a SyntaxError when input has non-base64, non-ascii-whitespace characters +features: [uint8array-base64, TypedArray] +---*/ + +var illegal = [ + 'Zm.9v', + 'Zm9v^', + 'Zg==&', + 'Z−==', // U+2212 'Minus Sign' + 'Z+==', // U+FF0B 'Fullwidth Plus Sign' + 'Zg\u00A0==', // nbsp + 'Zg\u2009==', // thin space + 'Zg\u2028==', // line separator +]; +illegal.forEach(function(value) { + assert.throws(SyntaxError, function() { + var target = new Uint8Array([255, 255, 255, 255, 255]); + target.setFromBase64(value); + }); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/last-chunk-handling.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/last-chunk-handling.js new file mode 100644 index 0000000000..958a89b10e --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/last-chunk-handling.js @@ -0,0 +1,156 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.setfrombase64 +description: Handling of final chunks in target.setFromBase64 +includes: [compareArray.js] +features: [uint8array-base64, TypedArray] +---*/ + +// padding +var target = new Uint8Array([255, 255, 255, 255, 255, 255]); +var result = target.setFromBase64('ZXhhZg=='); +assert.sameValue(result.read, 8); +assert.sameValue(result.written, 4); +assert.compareArray(target, [101, 120, 97, 102, 255, 255]); + +var target = new Uint8Array([255, 255, 255, 255, 255, 255]); +var result = target.setFromBase64('ZXhhZg==', { lastChunkHandling: 'loose' }); +assert.sameValue(result.read, 8); +assert.sameValue(result.written, 4); +assert.compareArray(target, [101, 120, 97, 102, 255, 255]); + +var target = new Uint8Array([255, 255, 255, 255, 255, 255]); +var result = target.setFromBase64('ZXhhZg==', { lastChunkHandling: 'stop-before-partial' }); +assert.sameValue(result.read, 8); +assert.sameValue(result.written, 4); +assert.compareArray(target, [101, 120, 97, 102, 255, 255]); + +var target = new Uint8Array([255, 255, 255, 255, 255, 255]); +var result = target.setFromBase64('ZXhhZg==', { lastChunkHandling: 'strict' }); +assert.sameValue(result.read, 8); +assert.sameValue(result.written, 4); +assert.compareArray(target, [101, 120, 97, 102, 255, 255]); + + +// no padding +var target = new Uint8Array([255, 255, 255, 255, 255, 255]); +var result = target.setFromBase64('ZXhhZg'); +assert.sameValue(result.read, 6); +assert.sameValue(result.written, 4); +assert.compareArray(target, [101, 120, 97, 102, 255, 255]); + +var target = new Uint8Array([255, 255, 255, 255, 255, 255]); +var result = target.setFromBase64('ZXhhZg', { lastChunkHandling: 'loose' }); +assert.sameValue(result.read, 6); +assert.sameValue(result.written, 4); +assert.compareArray(target, [101, 120, 97, 102, 255, 255]); + +var target = new Uint8Array([255, 255, 255, 255, 255, 255]); +var result = target.setFromBase64('ZXhhZg', { lastChunkHandling: 'stop-before-partial' }); +assert.sameValue(result.read, 4); +assert.sameValue(result.written, 3); +assert.compareArray(target, [101, 120, 97, 255, 255, 255]); + +assert.throws(SyntaxError, function() { + var target = new Uint8Array([255, 255, 255, 255, 255, 255]); + target.setFromBase64('ZXhhZg', { lastChunkHandling: 'strict' }); +}); + + +// non-zero padding bits +var target = new Uint8Array([255, 255, 255, 255, 255, 255]); +var result = target.setFromBase64('ZXhhZh=='); +assert.sameValue(result.read, 8); +assert.sameValue(result.written, 4); +assert.compareArray(target, [101, 120, 97, 102, 255, 255]); + +var target = new Uint8Array([255, 255, 255, 255, 255, 255]); +var result = target.setFromBase64('ZXhhZh==', { lastChunkHandling: 'loose' }); +assert.sameValue(result.read, 8); +assert.sameValue(result.written, 4); +assert.compareArray(target, [101, 120, 97, 102, 255, 255]); + +var target = new Uint8Array([255, 255, 255, 255, 255, 255]); +var result = target.setFromBase64('ZXhhZh==', { lastChunkHandling: 'stop-before-partial' }); +assert.sameValue(result.read, 8); +assert.sameValue(result.written, 4); +assert.compareArray(target, [101, 120, 97, 102, 255, 255]); + +assert.throws(SyntaxError, function() { + var target = new Uint8Array([255, 255, 255, 255, 255, 255]); + target.setFromBase64('ZXhhZh==', { lastChunkHandling: 'strict' }); +}); + + +// non-zero padding bits, no padding +var target = new Uint8Array([255, 255, 255, 255, 255, 255]); +var result = target.setFromBase64('ZXhhZh'); +assert.sameValue(result.read, 6); +assert.sameValue(result.written, 4); +assert.compareArray(target, [101, 120, 97, 102, 255, 255]); + +var target = new Uint8Array([255, 255, 255, 255, 255, 255]); +var result = target.setFromBase64('ZXhhZh', { lastChunkHandling: 'loose' }); +assert.sameValue(result.read, 6); +assert.sameValue(result.written, 4); +assert.compareArray(target, [101, 120, 97, 102, 255, 255]); + +var target = new Uint8Array([255, 255, 255, 255, 255, 255]); +var result = target.setFromBase64('ZXhhZh', { lastChunkHandling: 'stop-before-partial' }); +assert.sameValue(result.read, 4); +assert.sameValue(result.written, 3); +assert.compareArray(target, [101, 120, 97, 255, 255, 255]); + +assert.throws(SyntaxError, function() { + var target = new Uint8Array([255, 255, 255, 255, 255, 255]); + target.setFromBase64('ZXhhZh', { lastChunkHandling: 'strict' }); +}); + + +// partial padding +assert.throws(SyntaxError, function() { + var target = new Uint8Array([255, 255, 255, 255, 255, 255]); + target.setFromBase64('ZXhhZg='); +}); + +assert.throws(SyntaxError, function() { + var target = new Uint8Array([255, 255, 255, 255, 255, 255]); + target.setFromBase64('ZXhhZg=', { lastChunkHandling: 'loose' }); +}); + +var target = new Uint8Array([255, 255, 255, 255, 255, 255]); +var result = target.setFromBase64('ZXhhZg=', { lastChunkHandling: 'stop-before-partial' }); +assert.sameValue(result.read, 4); +assert.sameValue(result.written, 3); +assert.compareArray(target, [101, 120, 97, 255, 255, 255]); + +assert.throws(SyntaxError, function() { + var target = new Uint8Array([255, 255, 255, 255, 255, 255]); + target.setFromBase64('ZXhhZg=', { lastChunkHandling: 'strict' }); +}); + + +// excess padding +assert.throws(SyntaxError, function() { + var target = new Uint8Array([255, 255, 255, 255, 255, 255]); + target.setFromBase64('ZXhhZg==='); +}); + +assert.throws(SyntaxError, function() { + var target = new Uint8Array([255, 255, 255, 255, 255, 255]); + target.setFromBase64('ZXhhZg===', { lastChunkHandling: 'loose' }); +}); + +assert.throws(SyntaxError, function() { + var target = new Uint8Array([255, 255, 255, 255, 255, 255]); + target.setFromBase64('ZXhhZg===', { lastChunkHandling: 'stop-before-partial' }); +}); + +assert.throws(SyntaxError, function() { + var target = new Uint8Array([255, 255, 255, 255, 255, 255]); + target.setFromBase64('ZXhhZg===', { lastChunkHandling: 'strict' }); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/length.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/length.js new file mode 100644 index 0000000000..df97101a14 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/length.js @@ -0,0 +1,19 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.setfrombase64 +description: > + Uint8Array.prototype.setFromBase64.length is 1. +includes: [propertyHelper.js] +features: [uint8array-base64, TypedArray] +---*/ + +verifyProperty(Uint8Array.prototype.setFromBase64, 'length', { + value: 1, + enumerable: false, + writable: false, + configurable: true +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/name.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/name.js new file mode 100644 index 0000000000..c38be74532 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/name.js @@ -0,0 +1,19 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.setfrombase64 +description: > + Uint8Array.prototype.setFromBase64.name is "setFromBase64". +includes: [propertyHelper.js] +features: [uint8array-base64, TypedArray] +---*/ + +verifyProperty(Uint8Array.prototype.setFromBase64, 'name', { + value: 'setFromBase64', + enumerable: false, + writable: false, + configurable: true +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/nonconstructor.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/nonconstructor.js new file mode 100644 index 0000000000..46f5078ff5 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/nonconstructor.js @@ -0,0 +1,19 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.setfrombase64 +description: > + Uint8Array.prototype.setFromBase64 is not a constructor function. +includes: [isConstructor.js] +features: [uint8array-base64, TypedArray, Reflect.construct] +---*/ + +assert(!isConstructor(Uint8Array.prototype.setFromBase64), "Uint8Array.prototype.setFromBase64 is not a constructor"); + +assert.throws(TypeError, function() { + var target = new Uint8Array(10); + new target.setFromBase64(''); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/option-coercion.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/option-coercion.js new file mode 100644 index 0000000000..90bd1e330d --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/option-coercion.js @@ -0,0 +1,72 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.setfrombase64 +description: Uint8Array.prototype.setFromBase64 triggers effects of the "alphabet" and "lastChunkHandling" getters, but does not perform toString on the results +includes: [compareArray.js] +features: [uint8array-base64, TypedArray] +---*/ + +assert.throws(TypeError, function() { + var target = new Uint8Array([255, 255, 255]); + target.setFromBase64("Zg==", { alphabet: Object("base64") }); +}); + +assert.throws(TypeError, function() { + var target = new Uint8Array([255, 255, 255]); + target.setFromBase64("Zg==", { lastChunkHandling: Object("strict") }); +}); + + +var toStringCalls = 0; +var throwyToString = { + toString: function() { + toStringCalls += 1; + throw new Test262Error("toString called"); + } +}; +assert.throws(TypeError, function() { + var target = new Uint8Array([255, 255, 255]); + target.setFromBase64("Zg==", { alphabet: throwyToString }); +}); +assert.sameValue(toStringCalls, 0); + +assert.throws(TypeError, function() { + var target = new Uint8Array([255, 255, 255]); + target.setFromBase64("Zg==", { lastChunkHandling: throwyToString }); +}); +assert.sameValue(toStringCalls, 0); + + +var alphabetAccesses = 0; +var base64UrlOptions = {}; +Object.defineProperty(base64UrlOptions, "alphabet", { + get: function() { + alphabetAccesses += 1; + return "base64url"; + } +}); +var target = new Uint8Array([255, 255, 255, 255]); +var result = target.setFromBase64("x-_y", base64UrlOptions); +assert.sameValue(result.read, 4); +assert.sameValue(result.written, 3); +assert.compareArray(target, [199, 239, 242, 255]); +assert.sameValue(alphabetAccesses, 1); + +var lastChunkHandlingAccesses = 0; +var strictOptions = {}; +Object.defineProperty(strictOptions, "lastChunkHandling", { + get: function() { + lastChunkHandlingAccesses += 1; + return "strict"; + } +}); +var target = new Uint8Array([255, 255, 255, 255]); +var result = target.setFromBase64("Zg==", strictOptions); +assert.sameValue(result.read, 4); +assert.sameValue(result.written, 1); +assert.compareArray(target, [102, 255, 255, 255]); +assert.sameValue(lastChunkHandlingAccesses, 1); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/results.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/results.js new file mode 100644 index 0000000000..2c39bab318 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/results.js @@ -0,0 +1,33 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.setfrombase64 +description: Conversion of base64 strings to Uint8Arrays +includes: [compareArray.js] +features: [uint8array-base64, TypedArray] +---*/ + +// standard test vectors from https://datatracker.ietf.org/doc/html/rfc4648#section-10 +var standardBase64Vectors = [ + ["", []], + ["Zg==", [102]], + ["Zm8=", [102, 111]], + ["Zm9v", [102, 111, 111]], + ["Zm9vYg==", [102, 111, 111, 98]], + ["Zm9vYmE=", [102, 111, 111, 98, 97]], + ["Zm9vYmFy", [102, 111, 111, 98, 97, 114]], +]; + +standardBase64Vectors.forEach(function (pair) { + var allFF = [255, 255, 255, 255, 255, 255, 255, 255]; + var target = new Uint8Array(allFF); + var result = target.setFromBase64(pair[0]); + assert.sameValue(result.read, pair[0].length); + assert.sameValue(result.written, pair[1].length); + + var expected = pair[1].concat(allFF.slice(pair[1].length)) + assert.compareArray(target, expected, "decoding " + pair[0]); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/shell.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/shell.js new file mode 100644 index 0000000000..a7590326c3 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/shell.js @@ -0,0 +1,42 @@ +// GENERATED, DO NOT EDIT +// file: detachArrayBuffer.js +// Copyright (C) 2016 the V8 project authors. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +description: | + A function used in the process of asserting correctness of TypedArray objects. + + $262.detachArrayBuffer is defined by a host. +defines: [$DETACHBUFFER] +---*/ + +function $DETACHBUFFER(buffer) { + if (!$262 || typeof $262.detachArrayBuffer !== "function") { + throw new Test262Error("No method available to detach an ArrayBuffer"); + } + $262.detachArrayBuffer(buffer); +} + +// file: isConstructor.js +// Copyright (C) 2017 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +description: | + Test if a given function is a constructor function. +defines: [isConstructor] +features: [Reflect.construct] +---*/ + +function isConstructor(f) { + if (typeof f !== "function") { + throw new Test262Error("isConstructor invoked with a non-function value"); + } + + try { + Reflect.construct(function(){}, [], f); + } catch (e) { + return false; + } + return true; +} diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/string-coercion.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/string-coercion.js new file mode 100644 index 0000000000..7c479a78cd --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/string-coercion.js @@ -0,0 +1,46 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.setfrombase64 +description: Uint8Array.prototype.setFromBase64 throws if its first argument is not a string +features: [uint8array-base64, TypedArray] +---*/ + +var toStringCalls = 0; +var throwyToString = { + toString: function() { + toStringCalls += 1; + throw new Test262Error("toString called"); + } +}; + +assert.throws(TypeError, function() { + var target = new Uint8Array(10); + target.setFromBase64(throwyToString); +}); +assert.sameValue(toStringCalls, 0); + + +var optionAccesses = 0; +var touchyOptions = {}; +Object.defineProperty(touchyOptions, "alphabet", { + get: function() { + optionAccesses += 1; + throw new Test262Error("alphabet accessed"); + } +}); +Object.defineProperty(touchyOptions, "lastChunkHandling", { + get: function() { + optionAccesses += 1; + throw new Test262Error("lastChunkHandling accessed"); + } +}); +assert.throws(TypeError, function() { + var target = new Uint8Array(10); + target.setFromBase64(throwyToString, touchyOptions); +}); +assert.sameValue(toStringCalls, 0); +assert.sameValue(optionAccesses, 0); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/subarray.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/subarray.js new file mode 100644 index 0000000000..ffffd227b9 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/subarray.js @@ -0,0 +1,20 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.setfrombase64 +description: Uint8Array.prototype.setFromBase64 takes into account the offset of the target Uint8Array +includes: [compareArray.js] +features: [uint8array-base64, TypedArray] +---*/ + +var base = new Uint8Array([255, 255, 255, 255, 255, 255, 255]); +var subarray = base.subarray(2, 5); + +var result = subarray.setFromBase64('Zm9vYmFy'); +assert.sameValue(result.read, 4); +assert.sameValue(result.written, 3); +assert.compareArray(subarray, [102, 111, 111]); +assert.compareArray(base, [255, 255, 102, 111, 111, 255, 255]); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/target-size.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/target-size.js new file mode 100644 index 0000000000..4e38423621 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/target-size.js @@ -0,0 +1,67 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.setfrombase64 +description: Uint8Array.prototype.setFromBase64 behavior when target buffer is small +includes: [compareArray.js] +features: [uint8array-base64, TypedArray] +---*/ + +// buffer too small +var target = new Uint8Array([255, 255, 255, 255, 255]); +var result = target.setFromBase64('Zm9vYmFy'); +assert.sameValue(result.read, 4); +assert.sameValue(result.written, 3); +assert.compareArray(target, [102, 111, 111, 255, 255]); + +// buffer too small, padded +var target = new Uint8Array([255, 255, 255, 255]); +var result = target.setFromBase64('Zm9vYmE='); +assert.sameValue(result.read, 4); +assert.sameValue(result.written, 3); +assert.compareArray(target, [102, 111, 111, 255]); + +// buffer exact +var target = new Uint8Array([255, 255, 255, 255, 255, 255]); +var result = target.setFromBase64('Zm9vYmFy'); +assert.sameValue(result.read, 8); +assert.sameValue(result.written, 6); +assert.compareArray(target, [102, 111, 111, 98, 97, 114]); + +// buffer exact, padded +var target = new Uint8Array([255, 255, 255, 255, 255]); +var result = target.setFromBase64('Zm9vYmE='); +assert.sameValue(result.read, 8); +assert.sameValue(result.written, 5); +assert.compareArray(target, [102, 111, 111, 98, 97]); + +// buffer exact, not padded +var target = new Uint8Array([255, 255, 255, 255, 255]); +var result = target.setFromBase64('Zm9vYmE'); +assert.sameValue(result.read, 7); +assert.sameValue(result.written, 5); +assert.compareArray(target, [102, 111, 111, 98, 97]); + +// buffer exact, padded, stop-before-partial +var target = new Uint8Array([255, 255, 255, 255, 255]); +var result = target.setFromBase64('Zm9vYmE=', { lastChunkHandling: 'stop-before-partial' }); +assert.sameValue(result.read, 8); +assert.sameValue(result.written, 5); +assert.compareArray(target, [102, 111, 111, 98, 97]); + +// buffer exact, not padded, stop-before-partial +var target = new Uint8Array([255, 255, 255, 255, 255]); +var result = target.setFromBase64('Zm9vYmE', { lastChunkHandling: 'stop-before-partial' }); +assert.sameValue(result.read, 4); +assert.sameValue(result.written, 3); +assert.compareArray(target, [102, 111, 111, 255, 255]); + +// buffer too large +var target = new Uint8Array([255, 255, 255, 255, 255, 255, 255]); +var result = target.setFromBase64('Zm9vYmFy'); +assert.sameValue(result.read, 8); +assert.sameValue(result.written, 6); +assert.compareArray(target, [102, 111, 111, 98, 97, 114, 255]); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/whitespace.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/whitespace.js new file mode 100644 index 0000000000..a09673d2bd --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromBase64/whitespace.js @@ -0,0 +1,26 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.setfrombase64 +description: Uint8Array.prototype.setFromBase64 ignores ASCII whitespace in the input +includes: [compareArray.js] +features: [uint8array-base64, TypedArray] +---*/ + +var whitespaceKinds = [ + ["Z g==", "space"], + ["Z\tg==", "tab"], + ["Z\x0Ag==", "LF"], + ["Z\x0Cg==", "FF"], + ["Z\x0Dg==", "CR"], +]; +whitespaceKinds.forEach(function(pair) { + var target = new Uint8Array([255, 255, 255]); + var result = target.setFromBase64(pair[0]); + assert.sameValue(result.read, 5); + assert.sameValue(result.written, 1); + assert.compareArray(target, [102, 255, 255], "ascii whitespace: " + pair[1]); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/browser.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/browser.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/browser.js diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/descriptor.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/descriptor.js new file mode 100644 index 0000000000..3d1a4c996a --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/descriptor.js @@ -0,0 +1,18 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.setfromhex +description: > + Uint8Array.prototype.setFromHex has default data property attributes. +includes: [propertyHelper.js] +features: [uint8array-base64, TypedArray] +---*/ + +verifyProperty(Uint8Array.prototype, 'setFromHex', { + enumerable: false, + writable: true, + configurable: true +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/detached-buffer.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/detached-buffer.js new file mode 100644 index 0000000000..5ba8e15727 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/detached-buffer.js @@ -0,0 +1,17 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.setfromhex +description: Uint8Array.prototype.setFromHex throws on detatched buffers +includes: [detachArrayBuffer.js] +features: [uint8array-base64, TypedArray] +---*/ + +var target = new Uint8Array([255, 255, 255]); +$DETACHBUFFER(target.buffer); +assert.throws(TypeError, function() { + target.setFromHex('aa'); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/illegal-characters.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/illegal-characters.js new file mode 100644 index 0000000000..1c30b4cffd --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/illegal-characters.js @@ -0,0 +1,29 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.setfromhex +description: Uint8Array.prototype.setFromHex throws a SyntaxError when input has non-hex characters +features: [uint8array-base64, TypedArray] +---*/ + +var illegal = [ + 'a.a', + 'aa^', + 'a a', + 'a\ta', + 'a\x0Aa', + 'a\x0Ca', + 'a\x0Da', + 'a\u00A0a', // nbsp + 'a\u2009a', // thin space + 'a\u2028a', // line separator +]; +illegal.forEach(function(value) { + assert.throws(SyntaxError, function() { + var target = new Uint8Array([255, 255, 255, 255, 255]); + target.setFromHex(value); + }); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/length.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/length.js new file mode 100644 index 0000000000..b0b56ef761 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/length.js @@ -0,0 +1,19 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.setfromhex +description: > + Uint8Array.prototype.setFromHex.length is 1. +includes: [propertyHelper.js] +features: [uint8array-base64, TypedArray] +---*/ + +verifyProperty(Uint8Array.prototype.setFromHex, 'length', { + value: 1, + enumerable: false, + writable: false, + configurable: true +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/name.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/name.js new file mode 100644 index 0000000000..cbc06e6f3d --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/name.js @@ -0,0 +1,19 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.setfromhex +description: > + Uint8Array.prototype.setFromHex.name is "setFromHex". +includes: [propertyHelper.js] +features: [uint8array-base64, TypedArray] +---*/ + +verifyProperty(Uint8Array.prototype.setFromHex, 'name', { + value: 'setFromHex', + enumerable: false, + writable: false, + configurable: true +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/nonconstructor.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/nonconstructor.js new file mode 100644 index 0000000000..196b962bdf --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/nonconstructor.js @@ -0,0 +1,19 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.setfromhex +description: > + Uint8Array.prototype.setFromHex is not a constructor function. +includes: [isConstructor.js] +features: [uint8array-base64, TypedArray, Reflect.construct] +---*/ + +assert(!isConstructor(Uint8Array.prototype.setFromHex), "target.setFromHex is not a constructor"); + +assert.throws(TypeError, function() { + var target = new Uint8Array(10); + new target.setFromHex(''); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/results.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/results.js new file mode 100644 index 0000000000..c7749c6257 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/results.js @@ -0,0 +1,34 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.setfromhex +description: Conversion of hex strings to Uint8Arrays +includes: [compareArray.js] +features: [uint8array-base64, TypedArray] +---*/ + +var cases = [ + ["", []], + ["66", [102]], + ["666f", [102, 111]], + ["666F", [102, 111]], + ["666f6f", [102, 111, 111]], + ["666F6f", [102, 111, 111]], + ["666f6f62", [102, 111, 111, 98]], + ["666f6f6261", [102, 111, 111, 98, 97]], + ["666f6f626172", [102, 111, 111, 98, 97, 114]], +]; + +cases.forEach(function (pair) { + var allFF = [255, 255, 255, 255, 255, 255, 255, 255]; + var target = new Uint8Array(allFF); + var result = target.setFromHex(pair[0]); + assert.sameValue(result.read, pair[0].length); + assert.sameValue(result.written, pair[1].length); + + var expected = pair[1].concat(allFF.slice(pair[1].length)) + assert.compareArray(target, expected, "decoding " + pair[0]); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/shell.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/shell.js new file mode 100644 index 0000000000..a7590326c3 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/shell.js @@ -0,0 +1,42 @@ +// GENERATED, DO NOT EDIT +// file: detachArrayBuffer.js +// Copyright (C) 2016 the V8 project authors. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +description: | + A function used in the process of asserting correctness of TypedArray objects. + + $262.detachArrayBuffer is defined by a host. +defines: [$DETACHBUFFER] +---*/ + +function $DETACHBUFFER(buffer) { + if (!$262 || typeof $262.detachArrayBuffer !== "function") { + throw new Test262Error("No method available to detach an ArrayBuffer"); + } + $262.detachArrayBuffer(buffer); +} + +// file: isConstructor.js +// Copyright (C) 2017 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +description: | + Test if a given function is a constructor function. +defines: [isConstructor] +features: [Reflect.construct] +---*/ + +function isConstructor(f) { + if (typeof f !== "function") { + throw new Test262Error("isConstructor invoked with a non-function value"); + } + + try { + Reflect.construct(function(){}, [], f); + } catch (e) { + return false; + } + return true; +} diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/string-coercion.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/string-coercion.js new file mode 100644 index 0000000000..9a6f5f84c0 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/string-coercion.js @@ -0,0 +1,24 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.setfromhex +description: Uint8Array.prototype.setFromHex throws if its first argument is not a string +features: [uint8array-base64, TypedArray] +---*/ + +var toStringCalls = 0; +var throwyToString = { + toString: function() { + toStringCalls += 1; + throw new Test262Error("toString called"); + } +}; + +assert.throws(TypeError, function() { + var target = new Uint8Array(10); + target.setFromHex(throwyToString); +}); +assert.sameValue(toStringCalls, 0); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/subarray.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/subarray.js new file mode 100644 index 0000000000..45879d8cb3 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/subarray.js @@ -0,0 +1,20 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.setfromhex +description: Uint8Array.prototype.setFromHex takes into account the offset of the target Uint8Array +includes: [compareArray.js] +features: [uint8array-base64, TypedArray] +---*/ + +var base = new Uint8Array([255, 255, 255, 255, 255, 255, 255]); +var subarray = base.subarray(2, 5); + +var result = subarray.setFromHex('aabbcc'); +assert.sameValue(result.read, 6); +assert.sameValue(result.written, 3); +assert.compareArray(subarray, [170, 187, 204]); +assert.compareArray(base, [255, 255, 170, 187, 204, 255, 255]); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/target-size.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/target-size.js new file mode 100644 index 0000000000..d73fa1749d --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/setFromHex/target-size.js @@ -0,0 +1,32 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.setfromhex +description: Uint8Array.prototype.setFromHex behavior when target buffer is small +includes: [compareArray.js] +features: [uint8array-base64, TypedArray] +---*/ + +// buffer too small +var target = new Uint8Array([255, 255]); +var result = target.setFromHex('aabbcc'); +assert.sameValue(result.read, 4); +assert.sameValue(result.written, 2); +assert.compareArray(target, [170, 187]); + +// buffer exact +var target = new Uint8Array([255, 255, 255]); +var result = target.setFromHex('aabbcc'); +assert.sameValue(result.read, 6); +assert.sameValue(result.written, 3); +assert.compareArray(target, [170, 187, 204]); + +// buffer too large +var target = new Uint8Array([255, 255, 255, 255]); +var result = target.setFromHex('aabbcc'); +assert.sameValue(result.read, 6); +assert.sameValue(result.written, 3); +assert.compareArray(target, [170, 187, 204, 255]); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/shell.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/shell.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/shell.js diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/alphabet.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/alphabet.js new file mode 100644 index 0000000000..7edf0a379e --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/alphabet.js @@ -0,0 +1,20 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.tobase64 +description: Conversion of Uint8Arrays to base64 strings exercising the alphabet option +features: [uint8array-base64, TypedArray] +---*/ + +assert.sameValue((new Uint8Array([199, 239, 242])).toBase64(), "x+/y"); + +assert.sameValue((new Uint8Array([199, 239, 242])).toBase64({ alphabet: 'base64' }), "x+/y"); + +assert.sameValue((new Uint8Array([199, 239, 242])).toBase64({ alphabet: 'base64url' }), "x-_y"); + +assert.throws(TypeError, function() { + (new Uint8Array([199, 239, 242])).toBase64({ alphabet: 'other' }); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/browser.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/browser.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/browser.js diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/descriptor.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/descriptor.js new file mode 100644 index 0000000000..9c34719bf1 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/descriptor.js @@ -0,0 +1,18 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.tobase64 +description: > + Uint8Array.prototype.toBase64 has default data property attributes. +includes: [propertyHelper.js] +features: [uint8array-base64, TypedArray] +---*/ + +verifyProperty(Uint8Array.prototype, 'toBase64', { + enumerable: false, + writable: true, + configurable: true +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/detached-buffer.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/detached-buffer.js new file mode 100644 index 0000000000..0904454743 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/detached-buffer.js @@ -0,0 +1,42 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.tobase64 +description: Uint8Array.prototype.toBase64 checks for detachedness after side-effects are finished +includes: [detachArrayBuffer.js] +features: [uint8array-base64, TypedArray] +---*/ + +var array = new Uint8Array(2); +var getterCalls = 0; +var receiverDetachingOptions = {}; +Object.defineProperty(receiverDetachingOptions, "alphabet", { + get: function() { + getterCalls += 1; + $DETACHBUFFER(array.buffer); + return "base64"; + } +}); +assert.throws(TypeError, function() { + array.toBase64(receiverDetachingOptions); +}); +assert.sameValue(getterCalls, 1); + + +var detached = new Uint8Array(2); +$DETACHBUFFER(detached.buffer); +var getterCalls = 0; +var sideEffectingOptions = {}; +Object.defineProperty(sideEffectingOptions, "alphabet", { + get: function() { + getterCalls += 1; + return "base64"; + } +}); +assert.throws(TypeError, function() { + detached.toBase64(sideEffectingOptions); +}); +assert.sameValue(getterCalls, 1); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/length.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/length.js new file mode 100644 index 0000000000..9e4aabb9ed --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/length.js @@ -0,0 +1,19 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.tobase64 +description: > + Uint8Array.prototype.toBase64.length is 0. +includes: [propertyHelper.js] +features: [uint8array-base64, TypedArray] +---*/ + +verifyProperty(Uint8Array.prototype.toBase64, 'length', { + value: 0, + enumerable: false, + writable: false, + configurable: true +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/name.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/name.js new file mode 100644 index 0000000000..2510648b76 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/name.js @@ -0,0 +1,19 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.tobase64 +description: > + Uint8Array.prototype.toBase64.name is "toBase64". +includes: [propertyHelper.js] +features: [uint8array-base64, TypedArray] +---*/ + +verifyProperty(Uint8Array.prototype.toBase64, 'name', { + value: 'toBase64', + enumerable: false, + writable: false, + configurable: true +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/nonconstructor.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/nonconstructor.js new file mode 100644 index 0000000000..79280e2d5d --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/nonconstructor.js @@ -0,0 +1,19 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.tobase64 +description: > + Uint8Array.prototype.toBase64 is not a constructor function. +includes: [isConstructor.js] +features: [uint8array-base64, TypedArray, Reflect.construct] +---*/ + +assert(!isConstructor(Uint8Array.prototype.toBase64), "Uint8Array.prototype.toBase64 is not a constructor"); + +var uint8Array = new Uint8Array(8); +assert.throws(TypeError, function() { + new uint8Array.toBase64(); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/option-coercion.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/option-coercion.js new file mode 100644 index 0000000000..054a3a5e0d --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/option-coercion.js @@ -0,0 +1,51 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.tobase64 +description: Uint8Array.prototype.toBase64 triggers effects of the "alphabet" getter, but does not perform toString on the result +features: [uint8array-base64, TypedArray] +---*/ + +assert.throws(TypeError, function() { + (new Uint8Array(2)).toBase64({ alphabet: Object("base64") }); +}); + + +var toStringCalls = 0; +var throwyToString = { + toString: function() { + toStringCalls += 1; + throw new Test262Error("toString called on alphabet value"); + } +}; +assert.throws(TypeError, function() { + (new Uint8Array(2)).toBase64({ alphabet: throwyToString }); +}); +assert.sameValue(toStringCalls, 0); + +var alphabetAccesses = 0; +var base64UrlOptions = {}; +Object.defineProperty(base64UrlOptions, "alphabet", { + get: function() { + alphabetAccesses += 1; + return "base64url"; + } +}); +assert.sameValue((new Uint8Array([199, 239, 242])).toBase64(base64UrlOptions), "x-_y"); +assert.sameValue(alphabetAccesses, 1); + +// side-effects from the getter on the receiver are reflected in the result +var array = new Uint8Array([0]); +var receiverMutatingOptions = {}; +Object.defineProperty(receiverMutatingOptions, "alphabet", { + get: function() { + array[0] = 255; + return "base64"; + } +}); +var result = array.toBase64(receiverMutatingOptions); +assert.sameValue(result, "/w=="); +assert.sameValue(array[0], 255); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/receiver-not-uint8array.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/receiver-not-uint8array.js new file mode 100644 index 0000000000..53352ce4a5 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/receiver-not-uint8array.js @@ -0,0 +1,36 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.tobase64 +description: Uint8Array.prototype.toBase64 throws if the receiver is not a Uint8Array +includes: [testTypedArray.js] +features: [uint8array-base64, TypedArray] +---*/ + +var toBase64 = Uint8Array.prototype.toBase64; + +var options = {}; +Object.defineProperty(options, "alphabet", { + get: function() { + throw new Test262Error("options.alphabet accessed despite incompatible receiver"); + } +}); + +testWithTypedArrayConstructors(function(TA) { + if (TA === Uint8Array) return; + var sample = new TA(2); + assert.throws(TypeError, function() { + Uint8Array.prototype.toBase64.call(sample, options); + }); +}); + +assert.throws(TypeError, function() { + Uint8Array.prototype.toBase64.call([], options); +}); + +assert.throws(TypeError, function() { + toBase64(options); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/results.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/results.js new file mode 100644 index 0000000000..5ce75170b7 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/results.js @@ -0,0 +1,19 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.tobase64 +description: Conversion of Uint8Arrays to base64 strings +features: [uint8array-base64, TypedArray] +---*/ + +// standard test vectors from https://datatracker.ietf.org/doc/html/rfc4648#section-10 +assert.sameValue((new Uint8Array([])).toBase64(), ""); +assert.sameValue((new Uint8Array([102])).toBase64(), "Zg=="); +assert.sameValue((new Uint8Array([102, 111])).toBase64(), "Zm8="); +assert.sameValue((new Uint8Array([102, 111, 111])).toBase64(), "Zm9v"); +assert.sameValue((new Uint8Array([102, 111, 111, 98])).toBase64(), "Zm9vYg=="); +assert.sameValue((new Uint8Array([102, 111, 111, 98, 97])).toBase64(), "Zm9vYmE="); +assert.sameValue((new Uint8Array([102, 111, 111, 98, 97, 114])).toBase64(), "Zm9vYmFy"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/shell.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/shell.js new file mode 100644 index 0000000000..2af708ed59 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toBase64/shell.js @@ -0,0 +1,203 @@ +// GENERATED, DO NOT EDIT +// file: detachArrayBuffer.js +// Copyright (C) 2016 the V8 project authors. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +description: | + A function used in the process of asserting correctness of TypedArray objects. + + $262.detachArrayBuffer is defined by a host. +defines: [$DETACHBUFFER] +---*/ + +function $DETACHBUFFER(buffer) { + if (!$262 || typeof $262.detachArrayBuffer !== "function") { + throw new Test262Error("No method available to detach an ArrayBuffer"); + } + $262.detachArrayBuffer(buffer); +} + +// file: isConstructor.js +// Copyright (C) 2017 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +description: | + Test if a given function is a constructor function. +defines: [isConstructor] +features: [Reflect.construct] +---*/ + +function isConstructor(f) { + if (typeof f !== "function") { + throw new Test262Error("isConstructor invoked with a non-function value"); + } + + try { + Reflect.construct(function(){}, [], f); + } catch (e) { + return false; + } + return true; +} + +// file: testTypedArray.js +// Copyright (C) 2015 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +description: | + Collection of functions used to assert the correctness of TypedArray objects. +defines: + - floatArrayConstructors + - nonClampedIntArrayConstructors + - intArrayConstructors + - typedArrayConstructors + - TypedArray + - testWithTypedArrayConstructors + - nonAtomicsFriendlyTypedArrayConstructors + - testWithAtomicsFriendlyTypedArrayConstructors + - testWithNonAtomicsFriendlyTypedArrayConstructors + - testTypedArrayConversions +---*/ + +var floatArrayConstructors = [ + Float64Array, + Float32Array +]; + +var nonClampedIntArrayConstructors = [ + Int32Array, + Int16Array, + Int8Array, + Uint32Array, + Uint16Array, + Uint8Array +]; + +var intArrayConstructors = nonClampedIntArrayConstructors.concat([Uint8ClampedArray]); + +// Float16Array is a newer feature +// adding it to this list unconditionally would cause implementations lacking it to fail every test which uses it +if (typeof Float16Array !== 'undefined') { + floatArrayConstructors.push(Float16Array); +} + +/** + * Array containing every non-bigint typed array constructor. + */ + +var typedArrayConstructors = floatArrayConstructors.concat(intArrayConstructors); + +/** + * The %TypedArray% intrinsic constructor function. + */ +var TypedArray = Object.getPrototypeOf(Int8Array); + +/** + * Callback for testing a typed array constructor. + * + * @callback typedArrayConstructorCallback + * @param {Function} Constructor the constructor object to test with. + */ + +/** + * Calls the provided function for every typed array constructor. + * + * @param {typedArrayConstructorCallback} f - the function to call for each typed array constructor. + * @param {Array} selected - An optional Array with filtered typed arrays + */ +function testWithTypedArrayConstructors(f, selected) { + var constructors = selected || typedArrayConstructors; + for (var i = 0; i < constructors.length; ++i) { + var constructor = constructors[i]; + try { + f(constructor); + } catch (e) { + e.message += " (Testing with " + constructor.name + ".)"; + throw e; + } + } +} + +var nonAtomicsFriendlyTypedArrayConstructors = floatArrayConstructors.concat([Uint8ClampedArray]); +/** + * Calls the provided function for every non-"Atomics Friendly" typed array constructor. + * + * @param {typedArrayConstructorCallback} f - the function to call for each typed array constructor. + * @param {Array} selected - An optional Array with filtered typed arrays + */ +function testWithNonAtomicsFriendlyTypedArrayConstructors(f) { + testWithTypedArrayConstructors(f, nonAtomicsFriendlyTypedArrayConstructors); +} + +/** + * Calls the provided function for every "Atomics Friendly" typed array constructor. + * + * @param {typedArrayConstructorCallback} f - the function to call for each typed array constructor. + * @param {Array} selected - An optional Array with filtered typed arrays + */ +function testWithAtomicsFriendlyTypedArrayConstructors(f) { + testWithTypedArrayConstructors(f, [ + Int32Array, + Int16Array, + Int8Array, + Uint32Array, + Uint16Array, + Uint8Array, + ]); +} + +/** + * Helper for conversion operations on TypedArrays, the expected values + * properties are indexed in order to match the respective value for each + * TypedArray constructor + * @param {Function} fn - the function to call for each constructor and value. + * will be called with the constructor, value, expected + * value, and a initial value that can be used to avoid + * a false positive with an equivalent expected value. + */ +function testTypedArrayConversions(byteConversionValues, fn) { + var values = byteConversionValues.values; + var expected = byteConversionValues.expected; + + testWithTypedArrayConstructors(function(TA) { + var name = TA.name.slice(0, -5); + + return values.forEach(function(value, index) { + var exp = expected[name][index]; + var initial = 0; + if (exp === 0) { + initial = 1; + } + fn(TA, value, exp, initial); + }); + }); +} + +/** + * Checks if the given argument is one of the float-based TypedArray constructors. + * + * @param {constructor} ctor - the value to check + * @returns {boolean} + */ +function isFloatTypedArrayConstructor(arg) { + return floatArrayConstructors.indexOf(arg) !== -1; +} + +/** + * Determines the precision of the given float-based TypedArray constructor. + * + * @param {constructor} ctor - the value to check + * @returns {string} "half", "single", or "double" for Float16Array, Float32Array, and Float64Array respectively. + */ +function floatTypedArrayConstructorPrecision(FA) { + if (typeof Float16Array !== "undefined" && FA === Float16Array) { + return "half"; + } else if (FA === Float32Array) { + return "single"; + } else if (FA === Float64Array) { + return "double"; + } else { + throw new Error("Malformed test - floatTypedArrayConstructorPrecision called with non-float TypedArray"); + } +} diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/browser.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/browser.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/browser.js diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/descriptor.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/descriptor.js new file mode 100644 index 0000000000..fa1eba15de --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/descriptor.js @@ -0,0 +1,18 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.tohex +description: > + Uint8Array.prototype.toHex has default data property attributes. +includes: [propertyHelper.js] +features: [uint8array-base64, TypedArray] +---*/ + +verifyProperty(Uint8Array.prototype, 'toHex', { + enumerable: false, + writable: true, + configurable: true +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/detached-buffer.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/detached-buffer.js new file mode 100644 index 0000000000..e0126f6725 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/detached-buffer.js @@ -0,0 +1,18 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.tohex +description: Uint8Array.prototype.toHex throws if called on a detached buffer +includes: [detachArrayBuffer.js] +features: [uint8array-base64, TypedArray] +---*/ + +var array = new Uint8Array(2); +$DETACHBUFFER(array.buffer); +assert.throws(TypeError, function() { + array.toHex(); +}); + + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/length.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/length.js new file mode 100644 index 0000000000..9c7d00d693 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/length.js @@ -0,0 +1,19 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.tohex +description: > + Uint8Array.prototype.toHex.length is 0. +includes: [propertyHelper.js] +features: [uint8array-base64, TypedArray] +---*/ + +verifyProperty(Uint8Array.prototype.toHex, 'length', { + value: 0, + enumerable: false, + writable: false, + configurable: true +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/name.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/name.js new file mode 100644 index 0000000000..af2e1ef1d4 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/name.js @@ -0,0 +1,19 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.tohex +description: > + Uint8Array.prototype.toHex.name is "toHex". +includes: [propertyHelper.js] +features: [uint8array-base64, TypedArray] +---*/ + +verifyProperty(Uint8Array.prototype.toHex, 'name', { + value: 'toHex', + enumerable: false, + writable: false, + configurable: true +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/nonconstructor.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/nonconstructor.js new file mode 100644 index 0000000000..2d0f9fc00f --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/nonconstructor.js @@ -0,0 +1,19 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.tohex +description: > + Uint8Array.prototype.toHex is not a constructor function. +includes: [isConstructor.js] +features: [uint8array-base64, TypedArray, Reflect.construct] +---*/ + +assert(!isConstructor(Uint8Array.prototype.toHex), "Uint8Array.prototype.toHex is not a constructor"); + +var uint8Array = new Uint8Array(8); +assert.throws(TypeError, function() { + new uint8Array.toHex(); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/receiver-not-uint8array.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/receiver-not-uint8array.js new file mode 100644 index 0000000000..1409cca969 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/receiver-not-uint8array.js @@ -0,0 +1,29 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.tohex +description: Uint8Array.prototype.toHex throws if the receiver is not a Uint8Array +includes: [testTypedArray.js] +features: [uint8array-base64, TypedArray] +---*/ + +var toHex = Uint8Array.prototype.toHex; + +testWithTypedArrayConstructors(function(TA) { + if (TA === Uint8Array) return; + var sample = new TA(2); + assert.throws(TypeError, function() { + Uint8Array.prototype.toHex.call(sample); + }); +}); + +assert.throws(TypeError, function() { + Uint8Array.prototype.toHex.call([]); +}); + +assert.throws(TypeError, function() { + toHex(); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/results.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/results.js new file mode 100644 index 0000000000..fb8c31096f --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/results.js @@ -0,0 +1,18 @@ +// |reftest| shell-option(--enable-uint8array-base64) skip-if(!Uint8Array.fromBase64||!xulRuntime.shell) -- uint8array-base64 is not enabled unconditionally, requires shell-options +// Copyright (C) 2024 Kevin Gibbons. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-uint8array.prototype.tohex +description: Conversion of Uint8Arrays to hex strings +features: [uint8array-base64, TypedArray] +---*/ + +assert.sameValue((new Uint8Array([])).toHex(), ""); +assert.sameValue((new Uint8Array([102])).toHex(), "66"); +assert.sameValue((new Uint8Array([102, 111])).toHex(), "666f"); +assert.sameValue((new Uint8Array([102, 111, 111])).toHex(), "666f6f"); +assert.sameValue((new Uint8Array([102, 111, 111, 98])).toHex(), "666f6f62"); +assert.sameValue((new Uint8Array([102, 111, 111, 98, 97])).toHex(), "666f6f6261"); +assert.sameValue((new Uint8Array([102, 111, 111, 98, 97, 114])).toHex(), "666f6f626172"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/shell.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/shell.js new file mode 100644 index 0000000000..2af708ed59 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/prototype/toHex/shell.js @@ -0,0 +1,203 @@ +// GENERATED, DO NOT EDIT +// file: detachArrayBuffer.js +// Copyright (C) 2016 the V8 project authors. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +description: | + A function used in the process of asserting correctness of TypedArray objects. + + $262.detachArrayBuffer is defined by a host. +defines: [$DETACHBUFFER] +---*/ + +function $DETACHBUFFER(buffer) { + if (!$262 || typeof $262.detachArrayBuffer !== "function") { + throw new Test262Error("No method available to detach an ArrayBuffer"); + } + $262.detachArrayBuffer(buffer); +} + +// file: isConstructor.js +// Copyright (C) 2017 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +description: | + Test if a given function is a constructor function. +defines: [isConstructor] +features: [Reflect.construct] +---*/ + +function isConstructor(f) { + if (typeof f !== "function") { + throw new Test262Error("isConstructor invoked with a non-function value"); + } + + try { + Reflect.construct(function(){}, [], f); + } catch (e) { + return false; + } + return true; +} + +// file: testTypedArray.js +// Copyright (C) 2015 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +description: | + Collection of functions used to assert the correctness of TypedArray objects. +defines: + - floatArrayConstructors + - nonClampedIntArrayConstructors + - intArrayConstructors + - typedArrayConstructors + - TypedArray + - testWithTypedArrayConstructors + - nonAtomicsFriendlyTypedArrayConstructors + - testWithAtomicsFriendlyTypedArrayConstructors + - testWithNonAtomicsFriendlyTypedArrayConstructors + - testTypedArrayConversions +---*/ + +var floatArrayConstructors = [ + Float64Array, + Float32Array +]; + +var nonClampedIntArrayConstructors = [ + Int32Array, + Int16Array, + Int8Array, + Uint32Array, + Uint16Array, + Uint8Array +]; + +var intArrayConstructors = nonClampedIntArrayConstructors.concat([Uint8ClampedArray]); + +// Float16Array is a newer feature +// adding it to this list unconditionally would cause implementations lacking it to fail every test which uses it +if (typeof Float16Array !== 'undefined') { + floatArrayConstructors.push(Float16Array); +} + +/** + * Array containing every non-bigint typed array constructor. + */ + +var typedArrayConstructors = floatArrayConstructors.concat(intArrayConstructors); + +/** + * The %TypedArray% intrinsic constructor function. + */ +var TypedArray = Object.getPrototypeOf(Int8Array); + +/** + * Callback for testing a typed array constructor. + * + * @callback typedArrayConstructorCallback + * @param {Function} Constructor the constructor object to test with. + */ + +/** + * Calls the provided function for every typed array constructor. + * + * @param {typedArrayConstructorCallback} f - the function to call for each typed array constructor. + * @param {Array} selected - An optional Array with filtered typed arrays + */ +function testWithTypedArrayConstructors(f, selected) { + var constructors = selected || typedArrayConstructors; + for (var i = 0; i < constructors.length; ++i) { + var constructor = constructors[i]; + try { + f(constructor); + } catch (e) { + e.message += " (Testing with " + constructor.name + ".)"; + throw e; + } + } +} + +var nonAtomicsFriendlyTypedArrayConstructors = floatArrayConstructors.concat([Uint8ClampedArray]); +/** + * Calls the provided function for every non-"Atomics Friendly" typed array constructor. + * + * @param {typedArrayConstructorCallback} f - the function to call for each typed array constructor. + * @param {Array} selected - An optional Array with filtered typed arrays + */ +function testWithNonAtomicsFriendlyTypedArrayConstructors(f) { + testWithTypedArrayConstructors(f, nonAtomicsFriendlyTypedArrayConstructors); +} + +/** + * Calls the provided function for every "Atomics Friendly" typed array constructor. + * + * @param {typedArrayConstructorCallback} f - the function to call for each typed array constructor. + * @param {Array} selected - An optional Array with filtered typed arrays + */ +function testWithAtomicsFriendlyTypedArrayConstructors(f) { + testWithTypedArrayConstructors(f, [ + Int32Array, + Int16Array, + Int8Array, + Uint32Array, + Uint16Array, + Uint8Array, + ]); +} + +/** + * Helper for conversion operations on TypedArrays, the expected values + * properties are indexed in order to match the respective value for each + * TypedArray constructor + * @param {Function} fn - the function to call for each constructor and value. + * will be called with the constructor, value, expected + * value, and a initial value that can be used to avoid + * a false positive with an equivalent expected value. + */ +function testTypedArrayConversions(byteConversionValues, fn) { + var values = byteConversionValues.values; + var expected = byteConversionValues.expected; + + testWithTypedArrayConstructors(function(TA) { + var name = TA.name.slice(0, -5); + + return values.forEach(function(value, index) { + var exp = expected[name][index]; + var initial = 0; + if (exp === 0) { + initial = 1; + } + fn(TA, value, exp, initial); + }); + }); +} + +/** + * Checks if the given argument is one of the float-based TypedArray constructors. + * + * @param {constructor} ctor - the value to check + * @returns {boolean} + */ +function isFloatTypedArrayConstructor(arg) { + return floatArrayConstructors.indexOf(arg) !== -1; +} + +/** + * Determines the precision of the given float-based TypedArray constructor. + * + * @param {constructor} ctor - the value to check + * @returns {string} "half", "single", or "double" for Float16Array, Float32Array, and Float64Array respectively. + */ +function floatTypedArrayConstructorPrecision(FA) { + if (typeof Float16Array !== "undefined" && FA === Float16Array) { + return "half"; + } else if (FA === Float32Array) { + return "single"; + } else if (FA === Float64Array) { + return "double"; + } else { + throw new Error("Malformed test - floatTypedArrayConstructorPrecision called with non-float TypedArray"); + } +} diff --git a/js/src/tests/test262/prs/3994/built-ins/Uint8Array/shell.js b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/shell.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/Uint8Array/shell.js diff --git a/js/src/tests/test262/prs/3994/built-ins/browser.js b/js/src/tests/test262/prs/3994/built-ins/browser.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/browser.js diff --git a/js/src/tests/test262/prs/3994/built-ins/shell.js b/js/src/tests/test262/prs/3994/built-ins/shell.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/prs/3994/built-ins/shell.js diff --git a/js/src/tests/test262/prs/3994/shell.js b/js/src/tests/test262/prs/3994/shell.js new file mode 100644 index 0000000000..9adb7aa914 --- /dev/null +++ b/js/src/tests/test262/prs/3994/shell.js @@ -0,0 +1,723 @@ +// GENERATED, DO NOT EDIT +// file: assert.js +// Copyright (C) 2017 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +description: | + Collection of assertion functions used throughout test262 +defines: [assert] +---*/ + + +function assert(mustBeTrue, message) { + if (mustBeTrue === true) { + return; + } + + if (message === undefined) { + message = 'Expected true but got ' + assert._toString(mustBeTrue); + } + throw new Test262Error(message); +} + +assert._isSameValue = function (a, b) { + if (a === b) { + // Handle +/-0 vs. -/+0 + return a !== 0 || 1 / a === 1 / b; + } + + // Handle NaN vs. NaN + return a !== a && b !== b; +}; + +assert.sameValue = function (actual, expected, message) { + try { + if (assert._isSameValue(actual, expected)) { + return; + } + } catch (error) { + throw new Test262Error(message + ' (_isSameValue operation threw) ' + error); + return; + } + + if (message === undefined) { + message = ''; + } else { + message += ' '; + } + + message += 'Expected SameValue(«' + assert._toString(actual) + '», «' + assert._toString(expected) + '») to be true'; + + throw new Test262Error(message); +}; + +assert.notSameValue = function (actual, unexpected, message) { + if (!assert._isSameValue(actual, unexpected)) { + return; + } + + if (message === undefined) { + message = ''; + } else { + message += ' '; + } + + message += 'Expected SameValue(«' + assert._toString(actual) + '», «' + assert._toString(unexpected) + '») to be false'; + + throw new Test262Error(message); +}; + +assert.throws = function (expectedErrorConstructor, func, message) { + var expectedName, actualName; + if (typeof func !== "function") { + throw new Test262Error('assert.throws requires two arguments: the error constructor ' + + 'and a function to run'); + return; + } + if (message === undefined) { + message = ''; + } else { + message += ' '; + } + + try { + func(); + } catch (thrown) { + if (typeof thrown !== 'object' || thrown === null) { + message += 'Thrown value was not an object!'; + throw new Test262Error(message); + } else if (thrown.constructor !== expectedErrorConstructor) { + expectedName = expectedErrorConstructor.name; + actualName = thrown.constructor.name; + if (expectedName === actualName) { + message += 'Expected a ' + expectedName + ' but got a different error constructor with the same name'; + } else { + message += 'Expected a ' + expectedName + ' but got a ' + actualName; + } + throw new Test262Error(message); + } + return; + } + + message += 'Expected a ' + expectedErrorConstructor.name + ' to be thrown but no exception was thrown at all'; + throw new Test262Error(message); +}; + +assert._toString = function (value) { + try { + if (value === 0 && 1 / value === -Infinity) { + return '-0'; + } + + return String(value); + } catch (err) { + if (err.name === 'TypeError') { + return Object.prototype.toString.call(value); + } + + throw err; + } +}; + +// file: compareArray.js +// Copyright (C) 2017 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +description: | + Compare the contents of two arrays +defines: [compareArray] +---*/ + +function compareArray(a, b) { + if (b.length !== a.length) { + return false; + } + + for (var i = 0; i < a.length; i++) { + if (!compareArray.isSameValue(b[i], a[i])) { + return false; + } + } + return true; +} + +compareArray.isSameValue = function(a, b) { + if (a === 0 && b === 0) return 1 / a === 1 / b; + if (a !== a && b !== b) return true; + + return a === b; +}; + +compareArray.format = function(arrayLike) { + return `[${[].map.call(arrayLike, String).join(', ')}]`; +}; + +assert.compareArray = function(actual, expected, message) { + message = message === undefined ? '' : message; + + if (typeof message === 'symbol') { + message = message.toString(); + } + + assert(actual != null, `First argument shouldn't be nullish. ${message}`); + assert(expected != null, `Second argument shouldn't be nullish. ${message}`); + var format = compareArray.format; + var result = compareArray(actual, expected); + + // The following prevents actual and expected from being iterated and evaluated + // more than once unless absolutely necessary. + if (!result) { + assert(false, `Expected ${format(actual)} and ${format(expected)} to have the same contents. ${message}`); + } +}; + +// file: propertyHelper.js +// Copyright (C) 2017 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +description: | + Collection of functions used to safely verify the correctness of + property descriptors. +defines: + - verifyProperty + - verifyEqualTo # deprecated + - verifyWritable # deprecated + - verifyNotWritable # deprecated + - verifyEnumerable # deprecated + - verifyNotEnumerable # deprecated + - verifyConfigurable # deprecated + - verifyNotConfigurable # deprecated +---*/ + +// @ts-check + +/** + * @param {object} obj + * @param {string|symbol} name + * @param {PropertyDescriptor|undefined} desc + * @param {object} [options] + * @param {boolean} [options.restore] + */ +function verifyProperty(obj, name, desc, options) { + assert( + arguments.length > 2, + 'verifyProperty should receive at least 3 arguments: obj, name, and descriptor' + ); + + var originalDesc = Object.getOwnPropertyDescriptor(obj, name); + var nameStr = String(name); + + // Allows checking for undefined descriptor if it's explicitly given. + if (desc === undefined) { + assert.sameValue( + originalDesc, + undefined, + "obj['" + nameStr + "'] descriptor should be undefined" + ); + + // desc and originalDesc are both undefined, problem solved; + return true; + } + + assert( + Object.prototype.hasOwnProperty.call(obj, name), + "obj should have an own property " + nameStr + ); + + assert.notSameValue( + desc, + null, + "The desc argument should be an object or undefined, null" + ); + + assert.sameValue( + typeof desc, + "object", + "The desc argument should be an object or undefined, " + String(desc) + ); + + var names = Object.getOwnPropertyNames(desc); + for (var i = 0; i < names.length; i++) { + assert( + names[i] === "value" || + names[i] === "writable" || + names[i] === "enumerable" || + names[i] === "configurable" || + names[i] === "get" || + names[i] === "set", + "Invalid descriptor field: " + names[i], + ); + } + + var failures = []; + + if (Object.prototype.hasOwnProperty.call(desc, 'value')) { + if (!isSameValue(desc.value, originalDesc.value)) { + failures.push("descriptor value should be " + desc.value); + } + if (!isSameValue(desc.value, obj[name])) { + failures.push("object value should be " + desc.value); + } + } + + if (Object.prototype.hasOwnProperty.call(desc, 'enumerable')) { + if (desc.enumerable !== originalDesc.enumerable || + desc.enumerable !== isEnumerable(obj, name)) { + failures.push('descriptor should ' + (desc.enumerable ? '' : 'not ') + 'be enumerable'); + } + } + + if (Object.prototype.hasOwnProperty.call(desc, 'writable')) { + if (desc.writable !== originalDesc.writable || + desc.writable !== isWritable(obj, name)) { + failures.push('descriptor should ' + (desc.writable ? '' : 'not ') + 'be writable'); + } + } + + if (Object.prototype.hasOwnProperty.call(desc, 'configurable')) { + if (desc.configurable !== originalDesc.configurable || + desc.configurable !== isConfigurable(obj, name)) { + failures.push('descriptor should ' + (desc.configurable ? '' : 'not ') + 'be configurable'); + } + } + + assert(!failures.length, failures.join('; ')); + + if (options && options.restore) { + Object.defineProperty(obj, name, originalDesc); + } + + return true; +} + +function isConfigurable(obj, name) { + var hasOwnProperty = Object.prototype.hasOwnProperty; + try { + delete obj[name]; + } catch (e) { + if (!(e instanceof TypeError)) { + throw new Test262Error("Expected TypeError, got " + e); + } + } + return !hasOwnProperty.call(obj, name); +} + +function isEnumerable(obj, name) { + var stringCheck = false; + + if (typeof name === "string") { + for (var x in obj) { + if (x === name) { + stringCheck = true; + break; + } + } + } else { + // skip it if name is not string, works for Symbol names. + stringCheck = true; + } + + return stringCheck && + Object.prototype.hasOwnProperty.call(obj, name) && + Object.prototype.propertyIsEnumerable.call(obj, name); +} + +function isSameValue(a, b) { + if (a === 0 && b === 0) return 1 / a === 1 / b; + if (a !== a && b !== b) return true; + + return a === b; +} + +var __isArray = Array.isArray; +function isWritable(obj, name, verifyProp, value) { + var unlikelyValue = __isArray(obj) && name === "length" ? + Math.pow(2, 32) - 1 : + "unlikelyValue"; + var newValue = value || unlikelyValue; + var hadValue = Object.prototype.hasOwnProperty.call(obj, name); + var oldValue = obj[name]; + var writeSucceeded; + + try { + obj[name] = newValue; + } catch (e) { + if (!(e instanceof TypeError)) { + throw new Test262Error("Expected TypeError, got " + e); + } + } + + writeSucceeded = isSameValue(obj[verifyProp || name], newValue); + + // Revert the change only if it was successful (in other cases, reverting + // is unnecessary and may trigger exceptions for certain property + // configurations) + if (writeSucceeded) { + if (hadValue) { + obj[name] = oldValue; + } else { + delete obj[name]; + } + } + + return writeSucceeded; +} + +/** + * Deprecated; please use `verifyProperty` in new tests. + */ +function verifyEqualTo(obj, name, value) { + if (!isSameValue(obj[name], value)) { + throw new Test262Error("Expected obj[" + String(name) + "] to equal " + value + + ", actually " + obj[name]); + } +} + +/** + * Deprecated; please use `verifyProperty` in new tests. + */ +function verifyWritable(obj, name, verifyProp, value) { + if (!verifyProp) { + assert(Object.getOwnPropertyDescriptor(obj, name).writable, + "Expected obj[" + String(name) + "] to have writable:true."); + } + if (!isWritable(obj, name, verifyProp, value)) { + throw new Test262Error("Expected obj[" + String(name) + "] to be writable, but was not."); + } +} + +/** + * Deprecated; please use `verifyProperty` in new tests. + */ +function verifyNotWritable(obj, name, verifyProp, value) { + if (!verifyProp) { + assert(!Object.getOwnPropertyDescriptor(obj, name).writable, + "Expected obj[" + String(name) + "] to have writable:false."); + } + if (isWritable(obj, name, verifyProp)) { + throw new Test262Error("Expected obj[" + String(name) + "] NOT to be writable, but was."); + } +} + +/** + * Deprecated; please use `verifyProperty` in new tests. + */ +function verifyEnumerable(obj, name) { + assert(Object.getOwnPropertyDescriptor(obj, name).enumerable, + "Expected obj[" + String(name) + "] to have enumerable:true."); + if (!isEnumerable(obj, name)) { + throw new Test262Error("Expected obj[" + String(name) + "] to be enumerable, but was not."); + } +} + +/** + * Deprecated; please use `verifyProperty` in new tests. + */ +function verifyNotEnumerable(obj, name) { + assert(!Object.getOwnPropertyDescriptor(obj, name).enumerable, + "Expected obj[" + String(name) + "] to have enumerable:false."); + if (isEnumerable(obj, name)) { + throw new Test262Error("Expected obj[" + String(name) + "] NOT to be enumerable, but was."); + } +} + +/** + * Deprecated; please use `verifyProperty` in new tests. + */ +function verifyConfigurable(obj, name) { + assert(Object.getOwnPropertyDescriptor(obj, name).configurable, + "Expected obj[" + String(name) + "] to have configurable:true."); + if (!isConfigurable(obj, name)) { + throw new Test262Error("Expected obj[" + String(name) + "] to be configurable, but was not."); + } +} + +/** + * Deprecated; please use `verifyProperty` in new tests. + */ +function verifyNotConfigurable(obj, name) { + assert(!Object.getOwnPropertyDescriptor(obj, name).configurable, + "Expected obj[" + String(name) + "] to have configurable:false."); + if (isConfigurable(obj, name)) { + throw new Test262Error("Expected obj[" + String(name) + "] NOT to be configurable, but was."); + } +} + +// file: sta.js +// Copyright (c) 2012 Ecma International. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +description: | + Provides both: + + - An error class to avoid false positives when testing for thrown exceptions + - A function to explicitly throw an exception using the Test262Error class +defines: [Test262Error, $DONOTEVALUATE] +---*/ + + +function Test262Error(message) { + this.message = message || ""; +} + +Test262Error.prototype.toString = function () { + return "Test262Error: " + this.message; +}; + +Test262Error.thrower = function (message) { + throw new Test262Error(message); +}; + +function $DONOTEVALUATE() { + throw "Test262: This statement should not be evaluated."; +} + +// file: test262-host.js +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// https://github.com/tc39/test262/blob/main/INTERPRETING.md#host-defined-functions +;(function createHostObject(global) { + "use strict"; + + // Save built-in functions and constructors. + var FunctionToString = global.Function.prototype.toString; + var ReflectApply = global.Reflect.apply; + var Atomics = global.Atomics; + var Error = global.Error; + var SharedArrayBuffer = global.SharedArrayBuffer; + var Int32Array = global.Int32Array; + + // Save built-in shell functions. + var NewGlobal = global.newGlobal; + var setSharedArrayBuffer = global.setSharedArrayBuffer; + var getSharedArrayBuffer = global.getSharedArrayBuffer; + var evalInWorker = global.evalInWorker; + var monotonicNow = global.monotonicNow; + var gc = global.gc; + var clearKeptObjects = global.clearKeptObjects; + + var hasCreateIsHTMLDDA = "createIsHTMLDDA" in global; + var hasThreads = ("helperThreadCount" in global ? global.helperThreadCount() > 0 : true); + var hasMailbox = typeof setSharedArrayBuffer === "function" && typeof getSharedArrayBuffer === "function"; + var hasEvalInWorker = typeof evalInWorker === "function"; + + if (!hasCreateIsHTMLDDA && !("document" in global && "all" in global.document)) + throw new Error("no [[IsHTMLDDA]] object available for testing"); + + var IsHTMLDDA = hasCreateIsHTMLDDA + ? global.createIsHTMLDDA() + : global.document.all; + + // The $262.agent framework is not appropriate for browsers yet, and some + // test cases can't work in browsers (they block the main thread). + + var shellCode = hasMailbox && hasEvalInWorker; + var sabTestable = Atomics && SharedArrayBuffer && hasThreads && shellCode; + + global.$262 = { + __proto__: null, + createRealm() { + var newGlobalObject = NewGlobal(); + var createHostObjectFn = ReflectApply(FunctionToString, createHostObject, []); + newGlobalObject.Function(`${createHostObjectFn} createHostObject(this);`)(); + return newGlobalObject.$262; + }, + detachArrayBuffer: global.detachArrayBuffer, + evalScript: global.evaluateScript || global.evaluate, + global, + IsHTMLDDA, + gc() { + gc(); + }, + clearKeptObjects() { + clearKeptObjects(); + }, + agent: (function () { + + // SpiderMonkey complication: With run-time argument --no-threads + // our test runner will not properly filter test cases that can't be + // run because agents can't be started, and so we do a little + // filtering here: We will quietly succeed and exit if an agent test + // should not have been run because threads cannot be started. + // + // Firefox complication: The test cases that use $262.agent can't + // currently work in the browser, so for now we rely on them not + // being run at all. + + if (!sabTestable) { + let {reportCompare, quit} = global; + + function notAvailable() { + // See comment above. + if (!hasThreads && shellCode) { + reportCompare(0, 0); + quit(0); + } + throw new Error("Agents not available"); + } + + return { + start(script) { notAvailable() }, + broadcast(sab, id) { notAvailable() }, + getReport() { notAvailable() }, + sleep(s) { notAvailable() }, + monotonicNow, + } + } + + // The SpiderMonkey implementation uses a designated shared buffer _ia + // for coordination, and spinlocks for everything except sleeping. + + var _MSG_LOC = 0; // Low bit set: broadcast available; High bits: seq # + var _ID_LOC = 1; // ID sent with broadcast + var _ACK_LOC = 2; // Worker increments this to ack that broadcast was received + var _RDY_LOC = 3; // Worker increments this to ack that worker is up and running + var _LOCKTXT_LOC = 4; // Writer lock for the text buffer: 0=open, 1=closed + var _NUMTXT_LOC = 5; // Count of messages in text buffer + var _NEXT_LOC = 6; // First free location in the buffer + var _SLEEP_LOC = 7; // Used for sleeping + + var _FIRST = 10; // First location of first message + + var _ia = new Int32Array(new SharedArrayBuffer(65536)); + _ia[_NEXT_LOC] = _FIRST; + + var _worker_prefix = +// BEGIN WORKER PREFIX +`if (typeof $262 === 'undefined') + $262 = {}; +$262.agent = (function (global) { + var ReflectApply = global.Reflect.apply; + var StringCharCodeAt = global.String.prototype.charCodeAt; + var { + add: Atomics_add, + compareExchange: Atomics_compareExchange, + load: Atomics_load, + store: Atomics_store, + wait: Atomics_wait, + } = global.Atomics; + + var {getSharedArrayBuffer} = global; + + var _ia = new Int32Array(getSharedArrayBuffer()); + var agent = { + receiveBroadcast(receiver) { + var k; + while (((k = Atomics_load(_ia, ${_MSG_LOC})) & 1) === 0) + ; + var received_sab = getSharedArrayBuffer(); + var received_id = Atomics_load(_ia, ${_ID_LOC}); + Atomics_add(_ia, ${_ACK_LOC}, 1); + while (Atomics_load(_ia, ${_MSG_LOC}) === k) + ; + receiver(received_sab, received_id); + }, + + report(msg) { + while (Atomics_compareExchange(_ia, ${_LOCKTXT_LOC}, 0, 1) === 1) + ; + msg = "" + msg; + var i = _ia[${_NEXT_LOC}]; + _ia[i++] = msg.length; + for ( let j=0 ; j < msg.length ; j++ ) + _ia[i++] = ReflectApply(StringCharCodeAt, msg, [j]); + _ia[${_NEXT_LOC}] = i; + Atomics_add(_ia, ${_NUMTXT_LOC}, 1); + Atomics_store(_ia, ${_LOCKTXT_LOC}, 0); + }, + + sleep(s) { + Atomics_wait(_ia, ${_SLEEP_LOC}, 0, s); + }, + + leaving() {}, + + monotonicNow: global.monotonicNow, + }; + Atomics_add(_ia, ${_RDY_LOC}, 1); + return agent; +})(this);`; +// END WORKER PREFIX + + var _numWorkers = 0; + var _numReports = 0; + var _reportPtr = _FIRST; + var { + add: Atomics_add, + load: Atomics_load, + store: Atomics_store, + wait: Atomics_wait, + } = Atomics; + var StringFromCharCode = global.String.fromCharCode; + + return { + start(script) { + setSharedArrayBuffer(_ia.buffer); + var oldrdy = Atomics_load(_ia, _RDY_LOC); + evalInWorker(_worker_prefix + script); + while (Atomics_load(_ia, _RDY_LOC) === oldrdy) + ; + _numWorkers++; + }, + + broadcast(sab, id) { + setSharedArrayBuffer(sab); + Atomics_store(_ia, _ID_LOC, id); + Atomics_store(_ia, _ACK_LOC, 0); + Atomics_add(_ia, _MSG_LOC, 1); + while (Atomics_load(_ia, _ACK_LOC) < _numWorkers) + ; + Atomics_add(_ia, _MSG_LOC, 1); + }, + + getReport() { + if (_numReports === Atomics_load(_ia, _NUMTXT_LOC)) + return null; + var s = ""; + var i = _reportPtr; + var len = _ia[i++]; + for ( let j=0 ; j < len ; j++ ) + s += StringFromCharCode(_ia[i++]); + _reportPtr = i; + _numReports++; + return s; + }, + + sleep(s) { + Atomics_wait(_ia, _SLEEP_LOC, 0, s); + }, + + monotonicNow, + }; + })() + }; +})(this); + +var $mozAsyncTestDone = false; +function $DONE(failure) { + // This function is generally called from within a Promise handler, so any + // exception thrown by this method will be swallowed and most likely + // ignored by the Promise machinery. + if ($mozAsyncTestDone) { + reportFailure("$DONE() already called"); + return; + } + $mozAsyncTestDone = true; + + if (failure) + reportFailure(failure); + else + reportCompare(0, 0); + + if (typeof jsTestDriverEnd === "function") { + gDelayTestDriverEnd = false; + jsTestDriverEnd(); + } +} + +// Some tests in test262 leave promise rejections unhandled. +if ("ignoreUnhandledRejections" in this) { + ignoreUnhandledRejections(); +} diff --git a/js/src/tests/test262/staging/JSON/json-parse-with-source-snapshot.js b/js/src/tests/test262/staging/JSON/json-parse-with-source-snapshot.js index 45465333d4..7ef4f88dd8 100644 --- a/js/src/tests/test262/staging/JSON/json-parse-with-source-snapshot.js +++ b/js/src/tests/test262/staging/JSON/json-parse-with-source-snapshot.js @@ -1,4 +1,4 @@ -// |reftest| skip -- json-parse-with-source is not supported +// |reftest| shell-option(--enable-json-parse-with-source) skip-if(!JSON.hasOwnProperty('isRawJSON')||!xulRuntime.shell) -- json-parse-with-source is not enabled unconditionally, requires shell-options // Copyright (C) 2023 the V8 project authors. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/staging/JSON/json-parse-with-source.js b/js/src/tests/test262/staging/JSON/json-parse-with-source.js index f8674511ef..c6b6326423 100644 --- a/js/src/tests/test262/staging/JSON/json-parse-with-source.js +++ b/js/src/tests/test262/staging/JSON/json-parse-with-source.js @@ -1,4 +1,4 @@ -// |reftest| skip -- json-parse-with-source is not supported +// |reftest| shell-option(--enable-json-parse-with-source) skip-if(!JSON.hasOwnProperty('isRawJSON')||!xulRuntime.shell) -- json-parse-with-source is not enabled unconditionally, requires shell-options // Copyright (C) 2023 the V8 project authors. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/util/StructuredSpewer.cpp b/js/src/util/StructuredSpewer.cpp index 72f483c6b5..ef59f79b87 100644 --- a/js/src/util/StructuredSpewer.cpp +++ b/js/src/util/StructuredSpewer.cpp @@ -158,7 +158,7 @@ void StructuredSpewer::spew(JSContext* cx, SpewChannel channel, const char* fmt, json.beginObject(); json.property("channel", getName(channel)); - json.formatProperty("message", fmt, ap); + json.formatPropertyVA("message", fmt, ap); json.endObject(); va_end(ap); diff --git a/js/src/vm/ArrayBufferObject.cpp b/js/src/vm/ArrayBufferObject.cpp index 2fe4f01f8d..14039af574 100644 --- a/js/src/vm/ArrayBufferObject.cpp +++ b/js/src/vm/ArrayBufferObject.cpp @@ -2830,7 +2830,7 @@ bool InnerViewTable::addView(JSContext* cx, ArrayBufferObject* buffer, if (isNurseryView && !hadNurseryViews && nurseryKeysValid) { #ifdef DEBUG if (nurseryKeys.length() < 100) { - for (auto* key : nurseryKeys) { + for (const auto& key : nurseryKeys) { MOZ_ASSERT(key != buffer); } } @@ -2859,31 +2859,53 @@ void InnerViewTable::removeViews(ArrayBufferObject* buffer) { map.remove(ptr); } -bool InnerViewTable::traceWeak(JSTracer* trc) { return map.traceWeak(trc); } +bool InnerViewTable::traceWeak(JSTracer* trc) { + nurseryKeys.traceWeak(trc); + map.traceWeak(trc); + return true; +} void InnerViewTable::sweepAfterMinorGC(JSTracer* trc) { MOZ_ASSERT(needsSweepAfterMinorGC()); - if (nurseryKeysValid) { - for (size_t i = 0; i < nurseryKeys.length(); i++) { - ArrayBufferObject* buffer = nurseryKeys[i]; + NurseryKeysVector keys; + bool valid = true; + std::swap(nurseryKeys, keys); + std::swap(nurseryKeysValid, valid); + + // Use nursery keys vector if possible. + if (valid) { + for (ArrayBufferObject* buffer : keys) { MOZ_ASSERT(!gc::IsInsideNursery(buffer)); auto ptr = map.lookup(buffer); - if (ptr && !ptr->value().sweepAfterMinorGC(trc)) { + if (ptr && !sweepViewsAfterMinorGC(trc, buffer, ptr->value())) { map.remove(ptr); } } - } else { - for (ArrayBufferViewMap::Enum e(map); !e.empty(); e.popFront()) { - MOZ_ASSERT(!gc::IsInsideNursery(e.front().key())); - if (!e.front().value().sweepAfterMinorGC(trc)) { - e.removeFront(); - } + return; + } + + // Otherwise look at every map entry. + for (ArrayBufferViewMap::Enum e(map); !e.empty(); e.popFront()) { + MOZ_ASSERT(!gc::IsInsideNursery(e.front().key())); + if (!sweepViewsAfterMinorGC(trc, e.front().key(), e.front().value())) { + e.removeFront(); } } +} + +bool InnerViewTable::sweepViewsAfterMinorGC(JSTracer* trc, + ArrayBufferObject* buffer, + Views& views) { + if (!views.sweepAfterMinorGC(trc)) { + return false; // No more views. + } - nurseryKeys.clear(); - nurseryKeysValid = true; + if (views.hasNurseryViews() && !nurseryKeys.append(buffer)) { + nurseryKeysValid = false; + } + + return true; } size_t InnerViewTable::sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) { diff --git a/js/src/vm/ArrayBufferObject.h b/js/src/vm/ArrayBufferObject.h index 5aa96bf887..ce78b26cb2 100644 --- a/js/src/vm/ArrayBufferObject.h +++ b/js/src/vm/ArrayBufferObject.h @@ -760,16 +760,20 @@ class InnerViewTable { StableCellHasher<JSObject*>, ZoneAllocPolicy>; ArrayBufferViewMap map; - // List of keys from innerViews where either the source or at least one - // target is in the nursery. The raw pointer to a JSObject is allowed here - // because this vector is cleared after every minor collection. Users in - // sweepAfterMinorCollection must be careful to use MaybeForwarded before - // touching these pointers. - Vector<ArrayBufferObject*, 0, SystemAllocPolicy> nurseryKeys; + // List of keys from map where either the source or at least one target is in + // the nursery. The raw pointer to a JSObject is allowed here because this + // vector is cleared after every minor collection. Users in sweepAfterMinorGC + // must be careful to use MaybeForwarded before touching these pointers. + using NurseryKeysVector = + GCVector<UnsafeBarePtr<ArrayBufferObject*>, 0, SystemAllocPolicy>; + NurseryKeysVector nurseryKeys; // Whether nurseryKeys is a complete list. bool nurseryKeysValid = true; + bool sweepMapEntryAfterMinorGC(UnsafeBarePtr<JSObject*>& buffer, + ViewVector& views); + public: explicit InnerViewTable(Zone* zone) : map(zone) {} @@ -793,6 +797,9 @@ class InnerViewTable { ArrayBufferViewObject* view); ViewVector* maybeViewsUnbarriered(ArrayBufferObject* buffer); void removeViews(ArrayBufferObject* buffer); + + bool sweepViewsAfterMinorGC(JSTracer* trc, ArrayBufferObject* buffer, + Views& views); }; template <typename Wrapper> diff --git a/js/src/vm/ArrayBufferViewObject.cpp b/js/src/vm/ArrayBufferViewObject.cpp index 27004f3e2a..37272a94af 100644 --- a/js/src/vm/ArrayBufferViewObject.cpp +++ b/js/src/vm/ArrayBufferViewObject.cpp @@ -221,7 +221,6 @@ void ArrayBufferViewObject::computeResizableLengthAndByteOffset( size_t bytesPerElement) { MOZ_ASSERT(!isSharedMemory()); MOZ_ASSERT(hasBuffer()); - MOZ_ASSERT(!bufferUnshared()->isLengthPinned()); MOZ_ASSERT(bufferUnshared()->isResizable()); size_t byteOffsetStart = initialByteOffset(); diff --git a/js/src/vm/AsyncFunction.cpp b/js/src/vm/AsyncFunction.cpp index ce523fe409..f6ed33c738 100644 --- a/js/src/vm/AsyncFunction.cpp +++ b/js/src/vm/AsyncFunction.cpp @@ -151,7 +151,7 @@ static bool AsyncFunctionResume(JSContext* cx, if (!CallSelfHostedFunction(cx, funName, generatorOrValue, args, &generatorOrValue)) { if (!generator->isClosed()) { - generator->setClosed(); + generator->setClosed(cx); } // Handle the OOM case mentioned above. diff --git a/js/src/vm/AsyncIteration.cpp b/js/src/vm/AsyncIteration.cpp index 8f8902a64a..33546dc9dd 100644 --- a/js/src/vm/AsyncIteration.cpp +++ b/js/src/vm/AsyncIteration.cpp @@ -1071,7 +1071,7 @@ bool js::AsyncGeneratorThrow(JSContext* cx, unsigned argc, Value* vp) { if (!CallSelfHostedFunction(cx, funName, thisOrRval, args, &thisOrRval)) { // 25.5.3.2, steps 5.f, 5.g. if (!generator->isClosed()) { - generator->setClosed(); + generator->setClosed(cx); } return AsyncGeneratorThrown(cx, generator); } diff --git a/js/src/vm/AtomsTable.h b/js/src/vm/AtomsTable.h index aae7728fe5..28e448ce77 100644 --- a/js/src/vm/AtomsTable.h +++ b/js/src/vm/AtomsTable.h @@ -35,6 +35,82 @@ struct AtomHasher { } }; +struct js::AtomHasher::Lookup { + union { + const JS::Latin1Char* latin1Chars; + const char16_t* twoByteChars; + const char* utf8Bytes; + }; + enum { TwoByteChar, Latin1, UTF8 } type; + size_t length; + size_t byteLength; + const JSAtom* atom; /* Optional. */ + JS::AutoCheckCannotGC nogc; + + HashNumber hash; + + MOZ_ALWAYS_INLINE Lookup(const char* utf8Bytes, size_t byteLen, size_t length, + HashNumber hash) + : utf8Bytes(utf8Bytes), + type(UTF8), + length(length), + byteLength(byteLen), + atom(nullptr), + hash(hash) {} + + MOZ_ALWAYS_INLINE Lookup(const char16_t* chars, size_t length) + : twoByteChars(chars), + type(TwoByteChar), + length(length), + atom(nullptr), + hash(mozilla::HashString(chars, length)) {} + + MOZ_ALWAYS_INLINE Lookup(const JS::Latin1Char* chars, size_t length) + : latin1Chars(chars), + type(Latin1), + length(length), + atom(nullptr), + hash(mozilla::HashString(chars, length)) {} + + MOZ_ALWAYS_INLINE Lookup(HashNumber hash, const char16_t* chars, + size_t length) + : twoByteChars(chars), + type(TwoByteChar), + length(length), + atom(nullptr), + hash(hash) { + MOZ_ASSERT(hash == mozilla::HashString(chars, length)); + } + + MOZ_ALWAYS_INLINE Lookup(HashNumber hash, const JS::Latin1Char* chars, + size_t length) + : latin1Chars(chars), + type(Latin1), + length(length), + atom(nullptr), + hash(hash) { + MOZ_ASSERT(hash == mozilla::HashString(chars, length)); + } + + inline explicit Lookup(const JSAtom* atom) + : type(atom->hasLatin1Chars() ? Latin1 : TwoByteChar), + length(atom->length()), + atom(atom), + hash(atom->hash()) { + if (type == Latin1) { + latin1Chars = atom->latin1Chars(nogc); + MOZ_ASSERT(mozilla::HashString(latin1Chars, length) == hash); + } else { + MOZ_ASSERT(type == TwoByteChar); + twoByteChars = atom->twoByteChars(nogc); + MOZ_ASSERT(mozilla::HashString(twoByteChars, length) == hash); + } + } + + // Return: true iff the string in |atom| matches the string in this Lookup. + bool StringsMatch(const JSAtom& atom) const; +}; + // Note: Use a 'class' here to make forward declarations easier to use. class AtomSet : public JS::GCHashSet<WeakHeapPtr<JSAtom*>, AtomHasher, SystemAllocPolicy> { diff --git a/js/src/vm/BigIntType.h b/js/src/vm/BigIntType.h index fb9f4085e6..8959f93c77 100644 --- a/js/src/vm/BigIntType.h +++ b/js/src/vm/BigIntType.h @@ -419,7 +419,7 @@ class BigInt final : public js::gc::CellWithLengthAndFlags { static JSLinearString* toStringGeneric(JSContext* cx, Handle<BigInt*>, unsigned radix); - friend struct ::JSStructuredCloneReader; // So it can call the following: + friend struct ::JSStructuredCloneReader; // So it can call the following: static BigInt* destructivelyTrimHighZeroDigits(JSContext* cx, BigInt* x); bool absFitsInUint64() const { return digitLength() <= 64 / DigitBits; } diff --git a/js/src/vm/CommonPropertyNames.h b/js/src/vm/CommonPropertyNames.h index d4376ec6a4..5fa3f2b633 100644 --- a/js/src/vm/CommonPropertyNames.h +++ b/js/src/vm/CommonPropertyNames.h @@ -29,6 +29,7 @@ MACRO_(allowContentIter, "allowContentIter") \ MACRO_(allowContentIterWith, "allowContentIterWith") \ MACRO_(allowContentIterWithNext, "allowContentIterWithNext") \ + MACRO_(alphabet, "alphabet") \ MACRO_(ambiguous, "ambiguous") \ MACRO_(anonymous, "anonymous") \ MACRO_(Any, "Any") \ @@ -214,6 +215,8 @@ MACRO_(frame, "frame") \ MACRO_(from, "from") \ MACRO_(fromAsync, "fromAsync") \ + MACRO_(fromBase64, "fromBase64") \ + MACRO_(fromHex, "fromHex") \ MACRO_(fulfilled, "fulfilled") \ MACRO_(GatherAsyncParentCompletions, "GatherAsyncParentCompletions") \ MACRO_(gcCycleNumber, "gcCycleNumber") \ @@ -319,6 +322,7 @@ MACRO_(isoMinute, "isoMinute") \ MACRO_(isoMonth, "isoMonth") \ MACRO_(isoNanosecond, "isoNanosecond") \ + MACRO_(isRawJSON, "isRawJSON") \ MACRO_(isoSecond, "isoSecond") \ MACRO_(isoYear, "isoYear") \ MACRO_(isStepStart, "isStepStart") \ @@ -338,6 +342,7 @@ MACRO_(label, "label") \ MACRO_(language, "language") \ MACRO_(largestUnit, "largestUnit") \ + MACRO_(lastChunkHandling, "lastChunkHandling") \ MACRO_(lastIndex, "lastIndex") \ MACRO_(length, "length") \ MACRO_(let, "let") \ @@ -467,6 +472,8 @@ MACRO_(pull, "pull") \ MACRO_(quarter, "quarter") \ MACRO_(raw, "raw") \ + MACRO_(rawJSON, "rawJSON") \ + MACRO_(read, "read") \ MACRO_(reason, "reason") \ MACRO_(RegExp_String_Iterator_, "RegExp String Iterator") \ MACRO_(RegExp_prototype_Exec, "RegExp_prototype_Exec") \ @@ -503,6 +510,8 @@ MACRO_(SetConstructorInit, "SetConstructorInit") \ MACRO_(SetIsInlinableLargeFunction, "SetIsInlinableLargeFunction") \ MACRO_(Set_Iterator_, "Set Iterator") \ + MACRO_(setFromBase64, "setFromBase64") \ + MACRO_(setFromHex, "setFromHex") \ MACRO_(setPrototypeOf, "setPrototypeOf") \ MACRO_(shape, "shape") \ MACRO_(shared, "shared") \ @@ -540,7 +549,9 @@ MACRO_(timeStyle, "timeStyle") \ MACRO_(timeZone, "timeZone") \ MACRO_(timeZoneName, "timeZoneName") \ + MACRO_(toBase64, "toBase64") \ MACRO_(toGMTString, "toGMTString") \ + MACRO_(toHex, "toHex") \ MACRO_(toISOString, "toISOString") \ MACRO_(toJSON, "toJSON") \ MACRO_(toLocaleString, "toLocaleString") \ @@ -612,6 +623,7 @@ MACRO_(weeks, "weeks") \ MACRO_(while_, "while") \ MACRO_(with, "with") \ + MACRO_(written, "written") \ MACRO_(toReversed, "toReversed") \ MACRO_(toSorted, "toSorted") \ MACRO_(toSpliced, "toSpliced") \ diff --git a/js/src/vm/FrameIter.cpp b/js/src/vm/FrameIter.cpp index 3dd50c2fcf..9a665e6b9d 100644 --- a/js/src/vm/FrameIter.cpp +++ b/js/src/vm/FrameIter.cpp @@ -124,7 +124,12 @@ JS::Realm* JitFrameIter::realm() const { return asWasm().instance()->realm(); } - return asJSJit().script()->realm(); + if (asJSJit().isScripted()) { + return asJSJit().script()->realm(); + } + + MOZ_RELEASE_ASSERT(asJSJit().isTrampolineNative()); + return asJSJit().callee()->realm(); } uint8_t* JitFrameIter::resumePCinCurrentFrame() const { diff --git a/js/src/vm/FunctionFlags.h b/js/src/vm/FunctionFlags.h index d927056230..27d51c214a 100644 --- a/js/src/vm/FunctionFlags.h +++ b/js/src/vm/FunctionFlags.h @@ -116,9 +116,10 @@ class FunctionFlags { // This flag is used only by scripted functions and AsmJS. LAMBDA = 1 << 9, - // The WASM function has a JIT entry which emulates the - // js::BaseScript::jitCodeRaw mechanism. - WASM_JIT_ENTRY = 1 << 10, + // This Native function has a JIT entry which emulates the + // js::BaseScript::jitCodeRaw mechanism. Used for Wasm functions and + // TrampolineNative builtins. + NATIVE_JIT_ENTRY = 1 << 10, // Function had no explicit name, but a name was set by SetFunctionName at // compile time or SetFunctionName at runtime. @@ -238,7 +239,7 @@ class FunctionFlags { switch (kind()) { case FunctionKind::NormalFunction: MOZ_ASSERT(!hasFlags(LAZY_ACCESSOR_NAME)); - MOZ_ASSERT(!hasFlags(WASM_JIT_ENTRY)); + MOZ_ASSERT(!hasFlags(NATIVE_JIT_ENTRY)); break; case FunctionKind::Arrow: @@ -246,38 +247,38 @@ class FunctionFlags { MOZ_ASSERT(!hasFlags(CONSTRUCTOR)); MOZ_ASSERT(!hasFlags(LAZY_ACCESSOR_NAME)); MOZ_ASSERT(hasFlags(LAMBDA)); - MOZ_ASSERT(!hasFlags(WASM_JIT_ENTRY)); + MOZ_ASSERT(!hasFlags(NATIVE_JIT_ENTRY)); break; case FunctionKind::Method: MOZ_ASSERT(hasFlags(BASESCRIPT) || hasFlags(SELFHOSTLAZY)); MOZ_ASSERT(!hasFlags(CONSTRUCTOR)); MOZ_ASSERT(!hasFlags(LAZY_ACCESSOR_NAME)); MOZ_ASSERT(!hasFlags(LAMBDA)); - MOZ_ASSERT(!hasFlags(WASM_JIT_ENTRY)); + MOZ_ASSERT(!hasFlags(NATIVE_JIT_ENTRY)); break; case FunctionKind::ClassConstructor: MOZ_ASSERT(hasFlags(BASESCRIPT) || hasFlags(SELFHOSTLAZY)); MOZ_ASSERT(hasFlags(CONSTRUCTOR)); MOZ_ASSERT(!hasFlags(LAZY_ACCESSOR_NAME)); MOZ_ASSERT(!hasFlags(LAMBDA)); - MOZ_ASSERT(!hasFlags(WASM_JIT_ENTRY)); + MOZ_ASSERT(!hasFlags(NATIVE_JIT_ENTRY)); break; case FunctionKind::Getter: MOZ_ASSERT(!hasFlags(CONSTRUCTOR)); MOZ_ASSERT(!hasFlags(LAMBDA)); - MOZ_ASSERT(!hasFlags(WASM_JIT_ENTRY)); + MOZ_ASSERT(!hasFlags(NATIVE_JIT_ENTRY)); break; case FunctionKind::Setter: MOZ_ASSERT(!hasFlags(CONSTRUCTOR)); MOZ_ASSERT(!hasFlags(LAMBDA)); - MOZ_ASSERT(!hasFlags(WASM_JIT_ENTRY)); + MOZ_ASSERT(!hasFlags(NATIVE_JIT_ENTRY)); break; case FunctionKind::AsmJS: MOZ_ASSERT(!hasFlags(BASESCRIPT)); MOZ_ASSERT(!hasFlags(SELFHOSTLAZY)); MOZ_ASSERT(!hasFlags(LAZY_ACCESSOR_NAME)); - MOZ_ASSERT(!hasFlags(WASM_JIT_ENTRY)); + MOZ_ASSERT(!hasFlags(NATIVE_JIT_ENTRY)); break; case FunctionKind::Wasm: MOZ_ASSERT(!hasFlags(BASESCRIPT)); @@ -316,10 +317,11 @@ class FunctionFlags { MOZ_ASSERT_IF(kind() == Wasm, isNativeFun()); return kind() == Wasm; } - bool isWasmWithJitEntry() const { - MOZ_ASSERT_IF(hasFlags(WASM_JIT_ENTRY), isWasm()); - return hasFlags(WASM_JIT_ENTRY); + bool isNativeWithJitEntry() const { + MOZ_ASSERT_IF(hasFlags(NATIVE_JIT_ENTRY), isNativeFun()); + return hasFlags(NATIVE_JIT_ENTRY); } + bool isWasmWithJitEntry() const { return isWasm() && isNativeWithJitEntry(); } bool isNativeWithoutJitEntry() const { MOZ_ASSERT_IF(!hasJitEntry(), isNativeFun()); return !hasJitEntry(); @@ -328,7 +330,14 @@ class FunctionFlags { return isNativeFun() && !isAsmJSNative() && !isWasm(); } bool hasJitEntry() const { - return hasBaseScript() || hasSelfHostedLazyScript() || isWasmWithJitEntry(); + return hasBaseScript() || hasSelfHostedLazyScript() || + isNativeWithJitEntry(); + } + + bool canHaveJitInfo() const { + // A native builtin can have a pointer to either its JitEntry or JSJitInfo, + // but not both. + return isBuiltinNative() && !isNativeWithJitEntry(); } /* Possible attributes of an interpreted function: */ @@ -417,7 +426,7 @@ class FunctionFlags { return clearFlags(LAZY_ACCESSOR_NAME); } - FunctionFlags& setWasmJitEntry() { return setFlags(WASM_JIT_ENTRY); } + FunctionFlags& setNativeJitEntry() { return setFlags(NATIVE_JIT_ENTRY); } bool isExtended() const { return hasFlags(EXTENDED); } FunctionFlags& setIsExtended() { return setFlags(EXTENDED); } @@ -430,7 +439,7 @@ class FunctionFlags { static uint16_t HasJitEntryFlags(bool isConstructing) { uint16_t flags = BASESCRIPT | SELFHOSTLAZY; if (!isConstructing) { - flags |= WASM_JIT_ENTRY; + flags |= NATIVE_JIT_ENTRY; } return flags; } diff --git a/js/src/vm/GeckoProfiler.cpp b/js/src/vm/GeckoProfiler.cpp index 330bcd1fa6..dbf1eb9081 100644 --- a/js/src/vm/GeckoProfiler.cpp +++ b/js/src/vm/GeckoProfiler.cpp @@ -57,9 +57,15 @@ static jit::JitFrameLayout* GetTopProfilingJitFrame(jit::JitActivation* act) { return nullptr; } + // Skip if the activation has no JS frames. This can happen if there's only a + // TrampolineNative frame because these are skipped by the profiling frame + // iterator. jit::JSJitProfilingFrameIterator jitIter( (jit::CommonFrameLayout*)iter.frame().fp()); - MOZ_ASSERT(!jitIter.done()); + if (jitIter.done()) { + return nullptr; + } + return jitIter.framePtr(); } diff --git a/js/src/vm/GeneratorObject.cpp b/js/src/vm/GeneratorObject.cpp index 4f8d807df5..4a23c4ca9e 100644 --- a/js/src/vm/GeneratorObject.cpp +++ b/js/src/vm/GeneratorObject.cpp @@ -194,10 +194,10 @@ void AbstractGeneratorObject::dump() const { } #endif -void AbstractGeneratorObject::finalSuspend(HandleObject obj) { +void AbstractGeneratorObject::finalSuspend(JSContext* cx, HandleObject obj) { auto* genObj = &obj->as<AbstractGeneratorObject>(); MOZ_ASSERT(genObj->isRunning()); - genObj->setClosed(); + genObj->setClosed(cx); } static AbstractGeneratorObject* GetGeneratorObjectForCall(JSContext* cx, @@ -442,6 +442,16 @@ void AbstractGeneratorObject::setUnaliasedLocal(uint32_t slot, return stackStorage().setDenseElement(slot, value); } +void AbstractGeneratorObject::setClosed(JSContext* cx) { + setFixedSlot(CALLEE_SLOT, NullValue()); + setFixedSlot(ENV_CHAIN_SLOT, NullValue()); + setFixedSlot(ARGS_OBJ_SLOT, NullValue()); + setFixedSlot(STACK_STORAGE_SLOT, NullValue()); + setFixedSlot(RESUME_INDEX_SLOT, NullValue()); + + DebugAPI::onGeneratorClosed(cx, this); +} + bool AbstractGeneratorObject::isAfterYield() { return isAfterYieldOrAwait(JSOp::Yield); } diff --git a/js/src/vm/GeneratorObject.h b/js/src/vm/GeneratorObject.h index ddc11ba781..b952ebd533 100644 --- a/js/src/vm/GeneratorObject.h +++ b/js/src/vm/GeneratorObject.h @@ -57,7 +57,7 @@ class AbstractGeneratorObject : public NativeObject { static bool suspend(JSContext* cx, HandleObject obj, AbstractFramePtr frame, const jsbytecode* pc, unsigned nvalues); - static void finalSuspend(HandleObject obj); + static void finalSuspend(JSContext* cx, HandleObject obj); JSFunction& callee() const { return getFixedSlot(CALLEE_SLOT).toObject().as<JSFunction>(); @@ -149,13 +149,7 @@ class AbstractGeneratorObject : public NativeObject { return getFixedSlot(RESUME_INDEX_SLOT).toInt32(); } bool isClosed() const { return getFixedSlot(CALLEE_SLOT).isNull(); } - void setClosed() { - setFixedSlot(CALLEE_SLOT, NullValue()); - setFixedSlot(ENV_CHAIN_SLOT, NullValue()); - setFixedSlot(ARGS_OBJ_SLOT, NullValue()); - setFixedSlot(STACK_STORAGE_SLOT, NullValue()); - setFixedSlot(RESUME_INDEX_SLOT, NullValue()); - } + void setClosed(JSContext* cx); bool isAfterYield(); bool isAfterAwait(); diff --git a/js/src/vm/GlobalObject.h b/js/src/vm/GlobalObject.h index a296336385..97286fde51 100644 --- a/js/src/vm/GlobalObject.h +++ b/js/src/vm/GlobalObject.h @@ -1056,10 +1056,7 @@ class GlobalObject : public NativeObject { void setSourceURLsHolder(ArrayObject* holder) { data().sourceURLsHolder = holder; } - void clearSourceURLSHolder() { - // This is called at the start of shrinking GCs, so avoids barriers. - data().sourceURLsHolder.unbarrieredSet(nullptr); - } + void clearSourceURLSHolder() { setSourceURLsHolder(nullptr); } SharedShape* maybeArrayShapeWithDefaultProto() const { return data().arrayShapeWithDefaultProto; diff --git a/js/src/vm/Interpreter.cpp b/js/src/vm/Interpreter.cpp index 9eec4b81dd..f4cdc86f18 100644 --- a/js/src/vm/Interpreter.cpp +++ b/js/src/vm/Interpreter.cpp @@ -1273,7 +1273,7 @@ bool js::HandleClosingGeneratorReturn(JSContext* cx, AbstractFramePtr frame, cx->clearPendingException(); ok = true; auto* genObj = GetGeneratorObjectForFrame(cx, frame); - genObj->setClosed(); + genObj->setClosed(cx); } return ok; } @@ -4164,7 +4164,7 @@ bool MOZ_NEVER_INLINE JS_HAZ_JSNATIVE_CALLER js::Interpret(JSContext* cx, CASE(FinalYieldRval) { ReservedRooted<JSObject*> gen(&rootObject0, ®S.sp[-1].toObject()); REGS.sp--; - AbstractGeneratorObject::finalSuspend(gen); + AbstractGeneratorObject::finalSuspend(cx, gen); goto successful_return_continuation; } diff --git a/js/src/vm/InvalidatingFuse.cpp b/js/src/vm/InvalidatingFuse.cpp index 3e14d72fcc..cae8875a19 100644 --- a/js/src/vm/InvalidatingFuse.cpp +++ b/js/src/vm/InvalidatingFuse.cpp @@ -22,7 +22,8 @@ js::DependentScriptSet::DependentScriptSet(JSContext* cx, bool js::InvalidatingRuntimeFuse::addFuseDependency(JSContext* cx, Handle<JSScript*> script) { auto* zone = script->zone(); - DependentScriptSet* dss = zone->getOrCreateDependentScriptSet(cx, this); + DependentScriptSet* dss = + zone->fuseDependencies.getOrCreateDependentScriptSet(cx, this); if (!dss) { return false; } @@ -80,3 +81,20 @@ bool js::DependentScriptSet::addScriptForFuse(InvalidatingFuse* fuse, // Script is already in the set, no need to re-add. return true; } + +js::DependentScriptSet* js::DependentScriptGroup::getOrCreateDependentScriptSet( + JSContext* cx, js::InvalidatingFuse* fuse) { + for (auto& dss : dependencies) { + if (dss.associatedFuse == fuse) { + return &dss; + } + } + + if (!dependencies.emplaceBack(cx, fuse)) { + return nullptr; + } + + auto& dss = dependencies.back(); + MOZ_ASSERT(dss.associatedFuse == fuse); + return &dss; +} diff --git a/js/src/vm/InvalidatingFuse.h b/js/src/vm/InvalidatingFuse.h index 6fc458ee79..b8760e8e0f 100644 --- a/js/src/vm/InvalidatingFuse.h +++ b/js/src/vm/InvalidatingFuse.h @@ -10,7 +10,7 @@ #include "gc/Barrier.h" #include "gc/SweepingAPI.h" #include "vm/GuardFuse.h" -#include "vm/JSScript.h" +class JSScript; namespace js { @@ -40,10 +40,6 @@ class InvalidatingRuntimeFuse : public InvalidatingFuse { // A (weak) set of scripts which are dependent on an associated fuse. // -// These are typically stored in a vector at the moment, due to the low number -// of invalidating fuses, and so the associated fuse is stored along with the -// set. -// // Because it uses JS::WeakCache, GC tracing is taken care of without any need // for tracing in this class. class DependentScriptSet { @@ -61,6 +57,24 @@ class DependentScriptSet { js::WeakCache<WeakScriptSet> weakScripts; }; +class DependentScriptGroup { + // A dependent script set pairs a fuse with a set of scripts which depend + // on said fuse; this is a vector of script sets because the expectation for + // now is that the number of runtime wide invalidating fuses will be small. + // This will need to be revisited (convert to HashMap?) should that no + // longer be the case + // + // Note: This isn't traced through the zone, but rather through the use + // of JS::WeakCache. + Vector<DependentScriptSet, 1, SystemAllocPolicy> dependencies; + + public: + DependentScriptSet* getOrCreateDependentScriptSet(JSContext* cx, + InvalidatingFuse* fuse); + DependentScriptSet* begin() { return dependencies.begin(); } + DependentScriptSet* end() { return dependencies.end(); } +}; + } // namespace js #endif // vm_InvalidatingFuse_h diff --git a/js/src/vm/Iteration.cpp b/js/src/vm/Iteration.cpp index d02f9de8cf..e36ea8b555 100644 --- a/js/src/vm/Iteration.cpp +++ b/js/src/vm/Iteration.cpp @@ -1940,10 +1940,113 @@ static const JSFunctionSpec iterator_methods_with_helpers[] = { JS_FS_END, }; +// https://tc39.es/proposal-iterator-helpers/#sec-SetterThatIgnoresPrototypeProperties +static bool SetterThatIgnoresPrototypeProperties(JSContext* cx, + Handle<Value> thisv, + Handle<PropertyKey> prop, + Handle<Value> value) { + // Step 1. + Rooted<JSObject*> thisObj(cx, + RequireObject(cx, JSMSG_OBJECT_REQUIRED, thisv)); + if (!thisObj) { + return false; + } + + // Step 2. + Rooted<JSObject*> home( + cx, GlobalObject::getOrCreateIteratorPrototype(cx, cx->global())); + if (!home) { + return false; + } + if (thisObj == home) { + UniqueChars propName = + IdToPrintableUTF8(cx, prop, IdToPrintableBehavior::IdIsPropertyKey); + if (!propName) { + return false; + } + + // Step 2.b. + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_READ_ONLY, + propName.get()); + return false; + } + + // Step 3. + Rooted<Maybe<PropertyDescriptor>> desc(cx); + if (!GetOwnPropertyDescriptor(cx, thisObj, prop, &desc)) { + return false; + } + + // Step 4. + if (desc.isNothing()) { + // Step 4.a. + return DefineDataProperty(cx, thisObj, prop, value, JSPROP_ENUMERATE); + } + + // Step 5. + return SetProperty(cx, thisObj, prop, value); +} + +// https://tc39.es/proposal-iterator-helpers/#sec-get-iteratorprototype-@@tostringtag +static bool toStringTagGetter(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + args.rval().setString(cx->names().Iterator); + return true; +} + +// https://tc39.es/proposal-iterator-helpers/#sec-set-iteratorprototype-@@tostringtag +static bool toStringTagSetter(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + Rooted<PropertyKey> prop( + cx, PropertyKey::Symbol(cx->wellKnownSymbols().toStringTag)); + if (!SetterThatIgnoresPrototypeProperties(cx, args.thisv(), prop, + args.get(0))) { + return false; + } + + // Step 2. + args.rval().setUndefined(); + return true; +} + +// https://tc39.es/proposal-iterator-helpers/#sec-get-iteratorprototype-constructor +static bool constructorGetter(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + Rooted<JSObject*> constructor( + cx, GlobalObject::getOrCreateConstructor(cx, JSProto_Iterator)); + if (!constructor) { + return false; + } + args.rval().setObject(*constructor); + return true; +} + +// https://tc39.es/proposal-iterator-helpers/#sec-set-iteratorprototype-constructor +static bool constructorSetter(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + Rooted<PropertyKey> prop(cx, NameToId(cx->names().constructor)); + if (!SetterThatIgnoresPrototypeProperties(cx, args.thisv(), prop, + args.get(0))) { + return false; + } + + // Step 2. + args.rval().setUndefined(); + return true; +} + static const JSPropertySpec iterator_properties[] = { - // NOTE: Contrary to most other @@toStringTag properties, this property is - // writable. - JS_STRING_SYM_PS(toStringTag, "Iterator", 0), + // NOTE: Contrary to most other @@toStringTag properties, this property + // has a special setter (and a getter). + JS_SYM_GETSET(toStringTag, toStringTagGetter, toStringTagSetter, 0), JS_PS_END, }; @@ -2081,7 +2184,7 @@ static const ClassSpec IteratorObjectClassSpec = { nullptr, iterator_methods_with_helpers, iterator_properties, - nullptr, + IteratorObject::finishInit, }; const JSClass IteratorObject::class_ = { @@ -2098,6 +2201,13 @@ const JSClass IteratorObject::protoClass_ = { &IteratorObjectClassSpec, }; +/* static */ bool IteratorObject::finishInit(JSContext* cx, HandleObject ctor, + HandleObject proto) { + Rooted<PropertyKey> id(cx, NameToId(cx->names().constructor)); + return JS_DefinePropertyById(cx, proto, id, constructorGetter, + constructorSetter, 0); +} + // Set up WrapForValidIteratorObject class and its prototype. static const JSFunctionSpec wrap_for_valid_iterator_methods[] = { JS_SELF_HOSTED_FN("next", "WrapForValidIteratorNext", 0, 0), diff --git a/js/src/vm/Iteration.h b/js/src/vm/Iteration.h index 92edaccd65..7f1047eec0 100644 --- a/js/src/vm/Iteration.h +++ b/js/src/vm/Iteration.h @@ -751,6 +751,8 @@ class IteratorObject : public NativeObject { public: static const JSClass class_; static const JSClass protoClass_; + + static bool finishInit(JSContext* cx, HandleObject ctor, HandleObject proto); }; /* diff --git a/js/src/vm/JSAtomUtils.cpp b/js/src/vm/JSAtomUtils.cpp index ab8e4b2e30..2f8b066f0c 100644 --- a/js/src/vm/JSAtomUtils.cpp +++ b/js/src/vm/JSAtomUtils.cpp @@ -62,113 +62,30 @@ extern bool GetUTF8AtomizationData(JSContext* cx, const JS::UTF8Chars& utf8, JS::SmallestEncoding* encoding, HashNumber* hashNum); -struct js::AtomHasher::Lookup { - union { - const JS::Latin1Char* latin1Chars; - const char16_t* twoByteChars; - const char* utf8Bytes; - }; - enum { TwoByteChar, Latin1, UTF8 } type; - size_t length; - size_t byteLength; - const JSAtom* atom; /* Optional. */ - JS::AutoCheckCannotGC nogc; - - HashNumber hash; - - MOZ_ALWAYS_INLINE Lookup(const char* utf8Bytes, size_t byteLen, size_t length, - HashNumber hash) - : utf8Bytes(utf8Bytes), - type(UTF8), - length(length), - byteLength(byteLen), - atom(nullptr), - hash(hash) {} - - MOZ_ALWAYS_INLINE Lookup(const char16_t* chars, size_t length) - : twoByteChars(chars), - type(TwoByteChar), - length(length), - atom(nullptr), - hash(mozilla::HashString(chars, length)) {} - - MOZ_ALWAYS_INLINE Lookup(const JS::Latin1Char* chars, size_t length) - : latin1Chars(chars), - type(Latin1), - length(length), - atom(nullptr), - hash(mozilla::HashString(chars, length)) {} - - MOZ_ALWAYS_INLINE Lookup(HashNumber hash, const char16_t* chars, - size_t length) - : twoByteChars(chars), - type(TwoByteChar), - length(length), - atom(nullptr), - hash(hash) { - MOZ_ASSERT(hash == mozilla::HashString(chars, length)); - } - - MOZ_ALWAYS_INLINE Lookup(HashNumber hash, const JS::Latin1Char* chars, - size_t length) - : latin1Chars(chars), - type(Latin1), - length(length), - atom(nullptr), - hash(hash) { - MOZ_ASSERT(hash == mozilla::HashString(chars, length)); - } - - inline explicit Lookup(const JSAtom* atom) - : type(atom->hasLatin1Chars() ? Latin1 : TwoByteChar), - length(atom->length()), - atom(atom), - hash(atom->hash()) { - if (type == Latin1) { - latin1Chars = atom->latin1Chars(nogc); - MOZ_ASSERT(mozilla::HashString(latin1Chars, length) == hash); - } else { - MOZ_ASSERT(type == TwoByteChar); - twoByteChars = atom->twoByteChars(nogc); - MOZ_ASSERT(mozilla::HashString(twoByteChars, length) == hash); - } - } -}; - -inline HashNumber js::AtomHasher::hash(const Lookup& l) { return l.hash; } - -MOZ_ALWAYS_INLINE bool js::AtomHasher::match(const WeakHeapPtr<JSAtom*>& entry, - const Lookup& lookup) { - JSAtom* key = entry.unbarrieredGet(); - if (lookup.atom) { - return lookup.atom == key; - } - if (key->length() != lookup.length || key->hash() != lookup.hash) { - return false; - } - - if (key->hasLatin1Chars()) { - const Latin1Char* keyChars = key->latin1Chars(lookup.nogc); - switch (lookup.type) { +MOZ_ALWAYS_INLINE bool js::AtomHasher::Lookup::StringsMatch( + const JSAtom& atom) const { + if (atom.hasLatin1Chars()) { + const Latin1Char* keyChars = atom.latin1Chars(nogc); + switch (type) { case Lookup::Latin1: - return EqualChars(keyChars, lookup.latin1Chars, lookup.length); + return EqualChars(keyChars, latin1Chars, length); case Lookup::TwoByteChar: - return EqualChars(keyChars, lookup.twoByteChars, lookup.length); + return EqualChars(keyChars, twoByteChars, length); case Lookup::UTF8: { - JS::UTF8Chars utf8(lookup.utf8Bytes, lookup.byteLength); + JS::UTF8Chars utf8(utf8Bytes, byteLength); return UTF8EqualsChars(utf8, keyChars); } } } - const char16_t* keyChars = key->twoByteChars(lookup.nogc); - switch (lookup.type) { + const char16_t* keyChars = atom.twoByteChars(nogc); + switch (type) { case Lookup::Latin1: - return EqualChars(lookup.latin1Chars, keyChars, lookup.length); + return EqualChars(latin1Chars, keyChars, length); case Lookup::TwoByteChar: - return EqualChars(keyChars, lookup.twoByteChars, lookup.length); + return EqualChars(keyChars, twoByteChars, length); case Lookup::UTF8: { - JS::UTF8Chars utf8(lookup.utf8Bytes, lookup.byteLength); + JS::UTF8Chars utf8(utf8Bytes, byteLength); return UTF8EqualsChars(utf8, keyChars); } } @@ -177,6 +94,21 @@ MOZ_ALWAYS_INLINE bool js::AtomHasher::match(const WeakHeapPtr<JSAtom*>& entry, return false; } +inline HashNumber js::AtomHasher::hash(const Lookup& l) { return l.hash; } + +MOZ_ALWAYS_INLINE bool js::AtomHasher::match(const WeakHeapPtr<JSAtom*>& entry, + const Lookup& lookup) { + JSAtom* key = entry.unbarrieredGet(); + if (lookup.atom) { + return lookup.atom == key; + } + if (key->length() != lookup.length || key->hash() != lookup.hash) { + return false; + } + + return lookup.StringsMatch(*key); +} + UniqueChars js::AtomToPrintableString(JSContext* cx, JSAtom* atom) { return QuoteString(cx, atom); } @@ -450,18 +382,21 @@ static MOZ_ALWAYS_INLINE JSAtom* AtomizeAndCopyCharsNonStaticValidLengthFromLookup( JSContext* cx, const CharT* chars, size_t length, const AtomHasher::Lookup& lookup, const Maybe<uint32_t>& indexValue) { - // Try the per-Zone cache first. If we find the atom there we can avoid the - // markAtom call, and the multiple HashSet lookups below. Zone* zone = cx->zone(); MOZ_ASSERT(zone); - AtomSet::AddPtr zonePtr = zone->atomCache().lookupForAdd(lookup); - if (zonePtr) { - // The cache is purged on GC so if we're in the middle of an - // incremental GC we should have barriered the atom when we put - // it in the cache. - JSAtom* atom = zonePtr->unbarrieredGet(); - MOZ_ASSERT(AtomIsMarked(zone, atom)); - return atom; + + AtomCacheHashTable* atomCache = zone->atomCache(); + + // Try the per-Zone cache first. If we find the atom there we can avoid the + // markAtom call, and the multiple HashSet lookups below. + if (MOZ_LIKELY(atomCache)) { + JSAtom* const cachedAtom = atomCache->lookupForAdd(lookup); + if (cachedAtom) { + // The cache is purged on GC so if we're in the middle of an incremental + // GC we should have barriered the atom when we put it in the cache. + MOZ_ASSERT(AtomIsMarked(zone, cachedAtom)); + return cachedAtom; + } } MOZ_ASSERT(cx->permanentAtomsPopulated()); @@ -469,11 +404,9 @@ AtomizeAndCopyCharsNonStaticValidLengthFromLookup( AtomSet::Ptr pp = cx->permanentAtoms().readonlyThreadsafeLookup(lookup); if (pp) { JSAtom* atom = pp->get(); - if (MOZ_UNLIKELY(!zone->atomCache().add(zonePtr, atom))) { - ReportOutOfMemory(cx); - return nullptr; + if (MOZ_LIKELY(atomCache)) { + atomCache->add(lookup.hash, atom); } - return atom; } @@ -488,11 +421,9 @@ AtomizeAndCopyCharsNonStaticValidLengthFromLookup( return nullptr; } - if (MOZ_UNLIKELY(!zone->atomCache().add(zonePtr, atom))) { - ReportOutOfMemory(cx); - return nullptr; + if (MOZ_LIKELY(atomCache)) { + atomCache->add(lookup.hash, atom); } - return atom; } diff --git a/js/src/vm/JSContext.cpp b/js/src/vm/JSContext.cpp index 5a4bfa86cd..3cc2c4807c 100644 --- a/js/src/vm/JSContext.cpp +++ b/js/src/vm/JSContext.cpp @@ -799,8 +799,14 @@ JS_PUBLIC_API void js::StopDrainingJobQueue(JSContext* cx) { cx->internalJobQueue->interrupt(); } +JS_PUBLIC_API void js::RestartDrainingJobQueue(JSContext* cx) { + MOZ_ASSERT(cx->internalJobQueue.ref()); + cx->internalJobQueue->uninterrupt(); +} + JS_PUBLIC_API void js::RunJobs(JSContext* cx) { MOZ_ASSERT(cx->jobQueue); + MOZ_ASSERT(cx->isEvaluatingModule == 0); cx->jobQueue->runJobs(cx); JS::ClearKeptObjects(cx); } @@ -887,7 +893,6 @@ void InternalJobQueue::runJobs(JSContext* cx) { draining_ = false; if (interrupted_) { - interrupted_ = false; break; } @@ -969,6 +974,7 @@ JSContext::JSContext(JSRuntime* runtime, const JS::ContextOptions& options) #ifdef DEBUG inUnsafeCallWithABI(this, false), hasAutoUnsafeCallWithABI(this, false), + liveArraySortDataInstances(this, 0), #endif #ifdef JS_SIMULATOR simulator_(this, nullptr), @@ -994,6 +1000,7 @@ JSContext::JSContext(JSRuntime* runtime, const JS::ContextOptions& options) #else regExpSearcherLastLimit(this, 0), #endif + isEvaluatingModule(this, 0), frontendCollectionPool_(this), suppressProfilerSampling(false), tempLifoAlloc_(this, (size_t)TEMP_LIFO_ALLOC_PRIMARY_CHUNK_SIZE), @@ -1041,6 +1048,9 @@ JSContext::~JSContext() { /* Free the stuff hanging off of cx. */ MOZ_ASSERT(!resolvingList); + // Ensure we didn't leak memory for the ArraySortData vector. + MOZ_ASSERT(liveArraySortDataInstances == 0); + if (dtoaState) { DestroyDtoaState(dtoaState); } @@ -1189,6 +1199,13 @@ SavedFrame* JSContext::getPendingExceptionStack() { return unwrappedExceptionStack(); } +#ifdef DEBUG +const JS::Value& JSContext::getPendingExceptionUnwrapped() { + MOZ_ASSERT(isExceptionPending()); + return unwrappedException(); +} +#endif + bool JSContext::isClosingGenerator() { return isExceptionPending() && unwrappedException().isMagic(JS_GENERATOR_CLOSING); diff --git a/js/src/vm/JSContext.h b/js/src/vm/JSContext.h index 57aa236801..ba665d6c1a 100644 --- a/js/src/vm/JSContext.h +++ b/js/src/vm/JSContext.h @@ -89,12 +89,15 @@ class InternalJobQueue : public JS::JobQueue { JS::HandleObject incumbentGlobal) override; void runJobs(JSContext* cx) override; bool empty() const override; + bool isDrainingStopped() const override { return interrupted_; } // If we are currently in a call to runJobs(), make that call stop processing // jobs once the current one finishes, and return. If we are not currently in // a call to runJobs, make all future calls return immediately. void interrupt() { interrupted_ = true; } + void uninterrupt() { interrupted_ = false; } + // Return the front element of the queue, or nullptr if the queue is empty. // This is only used by shell testing functions. JSObject* maybeFront() const; @@ -425,6 +428,7 @@ struct JS_PUBLIC_API JSContext : public JS::RootingContext, #ifdef DEBUG js::ContextData<uint32_t> inUnsafeCallWithABI; js::ContextData<bool> hasAutoUnsafeCallWithABI; + js::ContextData<uint32_t> liveArraySortDataInstances; #endif #ifdef JS_SIMULATOR @@ -506,6 +510,9 @@ struct JS_PUBLIC_API JSContext : public JS::RootingContext, return offsetof(JSContext, regExpSearcherLastLimit); } + // Whether we are currently executing the top level of a module. + js::ContextData<uint32_t> isEvaluatingModule; + private: // Pools used for recycling name maps and vectors when parsing and // emitting bytecode. Purged on GC when there are no active script @@ -719,6 +726,13 @@ struct JS_PUBLIC_API JSContext : public JS::RootingContext, */ js::SavedFrame* getPendingExceptionStack(); +#ifdef DEBUG + /** + * Return the pending exception (without wrapping). + */ + const JS::Value& getPendingExceptionUnwrapped(); +#endif + bool isThrowingDebuggeeWouldRun(); bool isClosingGenerator(); diff --git a/js/src/vm/JSFunction.h b/js/src/vm/JSFunction.h index 451b63e11a..afd54c8f13 100644 --- a/js/src/vm/JSFunction.h +++ b/js/src/vm/JSFunction.h @@ -86,7 +86,7 @@ class JSFunction : public js::NativeObject { * the low bit set to ensure it's never identical to a BaseScript* * pointer * - * - a wasm JIT entry + * - a native JIT entry (used for Wasm and TrampolineNative functions) * * The JIT depends on none of the above being a valid BaseScript pointer. * @@ -199,6 +199,7 @@ class JSFunction : public js::NativeObject { bool isWasm() const { return flags().isWasm(); } bool isWasmWithJitEntry() const { return flags().isWasmWithJitEntry(); } + bool isNativeWithJitEntry() const { return flags().isNativeWithJitEntry(); } bool isNativeWithoutJitEntry() const { return flags().isNativeWithoutJitEntry(); } @@ -637,7 +638,9 @@ class JSFunction : public js::NativeObject { JS::PrivateValue(reinterpret_cast<void*>(native))); setNativeJitInfoOrInterpretedScript(const_cast<JSJitInfo*>(jitInfo)); } - bool hasJitInfo() const { return isBuiltinNative() && jitInfoUnchecked(); } + bool hasJitInfo() const { + return flags().canHaveJitInfo() && jitInfoUnchecked(); + } const JSJitInfo* jitInfo() const { MOZ_ASSERT(hasJitInfo()); return jitInfoUnchecked(); @@ -677,12 +680,25 @@ class JSFunction : public js::NativeObject { MOZ_ASSERT(*entry); MOZ_ASSERT(isWasm()); MOZ_ASSERT(!isWasmWithJitEntry()); - setFlags(flags().setWasmJitEntry()); + setFlags(flags().setNativeJitEntry()); setNativeJitInfoOrInterpretedScript(entry); MOZ_ASSERT(isWasmWithJitEntry()); } + void setTrampolineNativeJitEntry(void** entry) { + MOZ_ASSERT(*entry); + MOZ_ASSERT(isBuiltinNative()); + MOZ_ASSERT(!hasJitEntry()); + MOZ_ASSERT(!hasJitInfo(), "shouldn't clobber JSJitInfo"); + setFlags(flags().setNativeJitEntry()); + setNativeJitInfoOrInterpretedScript(entry); + MOZ_ASSERT(isNativeWithJitEntry()); + } void** wasmJitEntry() const { MOZ_ASSERT(isWasmWithJitEntry()); + return nativeJitEntry(); + } + void** nativeJitEntry() const { + MOZ_ASSERT(isNativeWithJitEntry()); return static_cast<void**>(nativeJitInfoOrInterpretedScript()); } inline js::wasm::Instance& wasmInstance() const; diff --git a/js/src/vm/JSONParser.cpp b/js/src/vm/JSONParser.cpp index f151f261b5..7a440e3090 100644 --- a/js/src/vm/JSONParser.cpp +++ b/js/src/vm/JSONParser.cpp @@ -644,7 +644,7 @@ inline bool JSONFullParseHandlerAnyChar::objectOpen( return false; } } - if (!stack.append(*properties)) { + if (!stack.append(StackEntry(cx, *properties))) { js_delete(*properties); return false; } @@ -676,11 +676,12 @@ inline bool JSONFullParseHandlerAnyChar::objectPropertyName( return true; } -inline void JSONFullParseHandlerAnyChar::finishObjectMember( +inline bool JSONFullParseHandlerAnyChar::finishObjectMember( Vector<StackEntry, 10>& stack, JS::Handle<JS::Value> value, PropertyVector** properties) { *properties = &stack.back().properties(); (*properties)->back().value = value; + return true; } inline bool JSONFullParseHandlerAnyChar::finishObject( @@ -718,7 +719,7 @@ inline bool JSONFullParseHandlerAnyChar::arrayOpen( return false; } } - if (!stack.append(*elements)) { + if (!stack.append(StackEntry(cx, *elements))) { js_delete(*elements); return false; } @@ -794,7 +795,7 @@ inline bool JSONFullParseHandler<CharT>::setStringValue( return false; } v = JS::StringValue(str); - return createJSONParseRecord(v, source); + return true; } template <typename CharT> @@ -812,26 +813,26 @@ inline bool JSONFullParseHandler<CharT>::setStringValue( return false; } v = JS::StringValue(str); - return createJSONParseRecord(v, source); + return true; } template <typename CharT> inline bool JSONFullParseHandler<CharT>::setNumberValue( double d, mozilla::Span<const CharT>&& source) { v = JS::NumberValue(d); - return createJSONParseRecord(v, source); + return true; } template <typename CharT> inline bool JSONFullParseHandler<CharT>::setBooleanValue( bool value, mozilla::Span<const CharT>&& source) { - return createJSONParseRecord(JS::BooleanValue(value), source); + return true; } template <typename CharT> inline bool JSONFullParseHandler<CharT>::setNullValue( mozilla::Span<const CharT>&& source) { - return createJSONParseRecord(JS::NullValue(), source); + return true; } template <typename CharT> @@ -847,29 +848,6 @@ void JSONFullParseHandler<CharT>::reportError(const char* msg, uint32_t line, msg, lineString, columnString); } -template <typename CharT> -void JSONFullParseHandler<CharT>::trace(JSTracer* trc) { - Base::trace(trc); - parseRecord.trace(trc); -} - -template <typename CharT> -inline bool JSONFullParseHandler<CharT>::createJSONParseRecord( - const Value& value, mozilla::Span<const CharT>& source) { -#ifdef ENABLE_JSON_PARSE_WITH_SOURCE - if (cx->realm()->creationOptions().getJSONParseWithSource()) { - MOZ_ASSERT(!source.IsEmpty()); - Rooted<JSONParseNode*> parseNode(cx, - NewStringCopy<CanGC, CharT>(cx, source)); - if (!parseNode) { - return false; - } - parseRecord = ParseRecordObject(parseNode, value); - } -#endif - return true; -} - template <typename CharT, typename HandlerT> JSONPerHandlerParser<CharT, HandlerT>::~JSONPerHandlerParser() { for (size_t i = 0; i < stack.length(); i++) { @@ -889,7 +867,9 @@ bool JSONPerHandlerParser<CharT, HandlerT>::parseImpl(TempValueT& value, switch (state) { case JSONParserState::FinishObjectMember: { typename HandlerT::PropertyVector* properties; - handler.finishObjectMember(stack, value, &properties); + if (!handler.finishObjectMember(stack, value, &properties)) { + return false; + } token = tokenizer.advanceAfterProperty(); if (token == JSONToken::ObjectClose) { @@ -1069,6 +1049,13 @@ template class js::JSONPerHandlerParser<Latin1Char, template class js::JSONPerHandlerParser<char16_t, js::JSONFullParseHandler<char16_t>>; +#ifdef ENABLE_JSON_PARSE_WITH_SOURCE +template class js::JSONPerHandlerParser<Latin1Char, + js::JSONReviveHandler<Latin1Char>>; +template class js::JSONPerHandlerParser<char16_t, + js::JSONReviveHandler<char16_t>>; +#endif + template class js::JSONPerHandlerParser<Latin1Char, js::JSONSyntaxParseHandler<Latin1Char>>; template class js::JSONPerHandlerParser<char16_t, @@ -1085,20 +1072,148 @@ bool JSONParser<CharT>::parse(JS::MutableHandle<JS::Value> vp) { } template <typename CharT> -bool JSONParser<CharT>::parse(JS::MutableHandle<JS::Value> vp, - JS::MutableHandle<ParseRecordObject> pro) { +void JSONParser<CharT>::trace(JSTracer* trc) { + this->handler.trace(trc); + + for (auto& elem : this->stack) { + if (elem.state == JSONParserState::FinishArrayElement) { + elem.elements().trace(trc); + } else { + elem.properties().trace(trc); + } + } +} + +template class js::JSONParser<Latin1Char>; +template class js::JSONParser<char16_t>; + +#ifdef ENABLE_JSON_PARSE_WITH_SOURCE +template <typename CharT> +inline bool JSONReviveHandler<CharT>::objectOpen(Vector<StackEntry, 10>& stack, + PropertyVector** properties) { + if (!parseRecordStack.append(ParseRecordEntry{context()})) { + return false; + } + + return Base::objectOpen(stack, properties); +} + +template <typename CharT> +inline bool JSONReviveHandler<CharT>::finishObjectMember( + Vector<StackEntry, 10>& stack, JS::Handle<JS::Value> value, + PropertyVector** properties) { + if (!Base::finishObjectMember(stack, value, properties)) { + return false; + } + parseRecord.value = value; + return finishMemberParseRecord((*properties)->back().id, + parseRecordStack.back()); +} + +template <typename CharT> +inline bool JSONReviveHandler<CharT>::finishObject( + Vector<StackEntry, 10>& stack, JS::MutableHandle<JS::Value> vp, + PropertyVector* properties) { + if (!Base::finishObject(stack, vp, properties)) { + return false; + } + if (!finishCompoundParseRecord(vp, parseRecordStack.back())) { + return false; + } + parseRecordStack.popBack(); + + return true; +} + +template <typename CharT> +inline bool JSONReviveHandler<CharT>::arrayOpen(Vector<StackEntry, 10>& stack, + ElementVector** elements) { + if (!parseRecordStack.append(ParseRecordEntry{context()})) { + return false; + } + + return Base::arrayOpen(stack, elements); +} + +template <typename CharT> +inline bool JSONReviveHandler<CharT>::arrayElement( + Vector<StackEntry, 10>& stack, JS::Handle<JS::Value> value, + ElementVector** elements) { + if (!Base::arrayElement(stack, value, elements)) { + return false; + } + size_t index = (*elements)->length() - 1; + JS::PropertyKey key = js::PropertyKey::Int(index); + return finishMemberParseRecord(key, parseRecordStack.back()); +} + +template <typename CharT> +inline bool JSONReviveHandler<CharT>::finishArray( + Vector<StackEntry, 10>& stack, JS::MutableHandle<JS::Value> vp, + ElementVector* elements) { + if (!Base::finishArray(stack, vp, elements)) { + return false; + } + if (!finishCompoundParseRecord(vp, parseRecordStack.back())) { + return false; + } + parseRecordStack.popBack(); + + return true; +} + +template <typename CharT> +inline bool JSONReviveHandler<CharT>::finishMemberParseRecord( + JS::PropertyKey& key, ParseRecordEntry& objectEntry) { + parseRecord.key = key; + return objectEntry.put(key, std::move(parseRecord)); +} + +template <typename CharT> +inline bool JSONReviveHandler<CharT>::finishCompoundParseRecord( + const Value& value, ParseRecordEntry& objectEntry) { + Rooted<JSONParseNode*> parseNode(context()); + parseRecord = ParseRecordObject(parseNode, value); + return parseRecord.addEntries(context(), std::move(objectEntry)); +} + +template <typename CharT> +inline bool JSONReviveHandler<CharT>::finishPrimitiveParseRecord( + const Value& value, SourceT source) { + MOZ_ASSERT(!source.IsEmpty()); // Empty source is for objects and arrays + Rooted<JSONParseNode*> parseNode( + context(), NewStringCopy<CanGC, CharT>(context(), source)); + if (!parseNode) { + return false; + } + parseRecord = ParseRecordObject(parseNode, value); + return true; +} + +template <typename CharT> +void JSONReviveHandler<CharT>::trace(JSTracer* trc) { + Base::trace(trc); + parseRecord.trace(trc); + for (auto& entry : this->parseRecordStack) { + entry.trace(trc); + } +} + +template <typename CharT> +bool JSONReviveParser<CharT>::parse(JS::MutableHandle<JS::Value> vp, + JS::MutableHandle<ParseRecordObject> pro) { JS::Rooted<JS::Value> tempValue(this->handler.cx); vp.setUndefined(); bool result = this->parseImpl( tempValue, [&](JS::Handle<JS::Value> value) { vp.set(value); }); - pro.get() = std::move(this->handler.parseRecord); + pro.set(std::move(this->handler.parseRecord)); return result; } template <typename CharT> -void JSONParser<CharT>::trace(JSTracer* trc) { +void JSONReviveParser<CharT>::trace(JSTracer* trc) { this->handler.trace(trc); for (auto& elem : this->stack) { @@ -1110,8 +1225,9 @@ void JSONParser<CharT>::trace(JSTracer* trc) { } } -template class js::JSONParser<Latin1Char>; -template class js::JSONParser<char16_t>; +template class js::JSONReviveParser<Latin1Char>; +template class js::JSONReviveParser<char16_t>; +#endif // ENABLE_JSON_PARSE_WITH_SOURCE template <typename CharT> inline bool JSONSyntaxParseHandler<CharT>::objectOpen( @@ -1359,9 +1475,11 @@ class MOZ_STACK_CLASS DelegateHandler { *isProtoInEval = false; return true; } - inline void finishObjectMember(Vector<StackEntry, 10>& stack, + inline bool finishObjectMember(Vector<StackEntry, 10>& stack, DummyValue& value, - PropertyVector** properties) {} + PropertyVector** properties) { + return true; + } inline bool finishObject(Vector<StackEntry, 10>& stack, DummyValue* vp, PropertyVector* properties) { if (hadHandlerError_) { diff --git a/js/src/vm/JSONParser.h b/js/src/vm/JSONParser.h index 91e33c02b3..51b90e003c 100644 --- a/js/src/vm/JSONParser.h +++ b/js/src/vm/JSONParser.h @@ -190,10 +190,10 @@ class MOZ_STACK_CLASS JSONFullParseHandlerAnyChar { return *static_cast<PropertyVector*>(vector); } - explicit StackEntry(ElementVector* elements) + explicit StackEntry(JSContext* cx, ElementVector* elements) : state(JSONParserState::FinishArrayElement), vector(elements) {} - explicit StackEntry(PropertyVector* properties) + explicit StackEntry(JSContext* cx, PropertyVector* properties) : state(JSONParserState::FinishObjectMember), vector(properties) {} JSONParserState state; @@ -255,7 +255,7 @@ class MOZ_STACK_CLASS JSONFullParseHandlerAnyChar { PropertyVector** properties); inline bool objectPropertyName(Vector<StackEntry, 10>& stack, bool* isProtoInEval); - inline void finishObjectMember(Vector<StackEntry, 10>& stack, + inline bool finishObjectMember(Vector<StackEntry, 10>& stack, JS::Handle<JS::Value> value, PropertyVector** properties); inline bool finishObject(Vector<StackEntry, 10>& stack, @@ -303,8 +303,6 @@ class MOZ_STACK_CLASS JSONFullParseHandler bool append(const CharT* begin, const CharT* end); }; - ParseRecordObject parseRecord; - explicit JSONFullParseHandler(JSContext* cx) : Base(cx) {} JSONFullParseHandler(JSONFullParseHandler&& other) noexcept @@ -324,13 +322,101 @@ class MOZ_STACK_CLASS JSONFullParseHandler inline bool setNullValue(mozilla::Span<const CharT>&& source); void reportError(const char* msg, uint32_t line, uint32_t column); +}; + +#ifdef ENABLE_JSON_PARSE_WITH_SOURCE +template <typename CharT> +class MOZ_STACK_CLASS JSONReviveHandler : public JSONFullParseHandler<CharT> { + using CharPtr = mozilla::RangedPtr<const CharT>; + using Base = JSONFullParseHandler<CharT>; + + public: + using SourceT = mozilla::Span<const CharT>; + using ParseRecordEntry = ParseRecordObject::EntryMap; + + using StringBuilder = typename Base::StringBuilder; + using StackEntry = typename Base::StackEntry; + using PropertyVector = typename Base::PropertyVector; + using ElementVector = typename Base::ElementVector; + + public: + explicit JSONReviveHandler(JSContext* cx) : Base(cx), parseRecordStack(cx) {} + + JSONReviveHandler(JSONReviveHandler&& other) noexcept + : Base(std::move(other)), + parseRecordStack(std::move(other.parseRecordStack)), + parseRecord(std::move(other.parseRecord)) {} + + JSONReviveHandler(const JSONReviveHandler& other) = delete; + void operator=(const JSONReviveHandler& other) = delete; + + JSContext* context() { return this->cx; } + + template <JSONStringType ST> + inline bool setStringValue(CharPtr start, size_t length, SourceT&& source) { + if (!Base::template setStringValue<ST>(start, length, + std::forward<SourceT&&>(source))) { + return false; + } + return finishPrimitiveParseRecord(this->v, source); + } + + template <JSONStringType ST> + inline bool setStringValue(StringBuilder& builder, SourceT&& source) { + if (!Base::template setStringValue<ST>(builder, + std::forward<SourceT&&>(source))) { + return false; + } + return finishPrimitiveParseRecord(this->v, source); + } + + inline bool setNumberValue(double d, SourceT&& source) { + if (!Base::setNumberValue(d, std::forward<SourceT&&>(source))) { + return false; + } + return finishPrimitiveParseRecord(this->v, source); + } + + inline bool setBooleanValue(bool value, SourceT&& source) { + return finishPrimitiveParseRecord(JS::BooleanValue(value), source); + } + inline bool setNullValue(SourceT&& source) { + return finishPrimitiveParseRecord(JS::NullValue(), source); + } + + inline bool objectOpen(Vector<StackEntry, 10>& stack, + PropertyVector** properties); + inline bool finishObjectMember(Vector<StackEntry, 10>& stack, + JS::Handle<JS::Value> value, + PropertyVector** properties); + inline bool finishObject(Vector<StackEntry, 10>& stack, + JS::MutableHandle<JS::Value> vp, + PropertyVector* properties); + + inline bool arrayOpen(Vector<StackEntry, 10>& stack, + ElementVector** elements); + inline bool arrayElement(Vector<StackEntry, 10>& stack, + JS::Handle<JS::Value> value, + ElementVector** elements); + inline bool finishArray(Vector<StackEntry, 10>& stack, + JS::MutableHandle<JS::Value> vp, + ElementVector* elements); void trace(JSTracer* trc); - protected: - inline bool createJSONParseRecord(const Value& value, - mozilla::Span<const CharT>& source); + private: + inline bool finishMemberParseRecord(JS::PropertyKey& key, + ParseRecordEntry& objectEntry); + inline bool finishCompoundParseRecord(const Value& value, + ParseRecordEntry& objectEntry); + inline bool finishPrimitiveParseRecord(const Value& value, SourceT source); + + Vector<ParseRecordEntry, 10> parseRecordStack; + + public: + ParseRecordObject parseRecord; }; +#endif // ENABLE_JSON_PARSE_WITH_SOURCE template <typename CharT> class MOZ_STACK_CLASS JSONSyntaxParseHandler { @@ -409,9 +495,11 @@ class MOZ_STACK_CLASS JSONSyntaxParseHandler { *isProtoInEval = false; return true; } - inline void finishObjectMember(Vector<StackEntry, 10>& stack, + inline bool finishObjectMember(Vector<StackEntry, 10>& stack, DummyValue& value, - PropertyVector** properties) {} + PropertyVector** properties) { + return true; + } inline bool finishObject(Vector<StackEntry, 10>& stack, DummyValue* vp, PropertyVector* properties); @@ -510,11 +598,52 @@ class MOZ_STACK_CLASS JSONParser * represent |undefined|, so the JSON data couldn't have specified it.) */ bool parse(JS::MutableHandle<JS::Value> vp); + + void trace(JSTracer* trc); +}; + +#ifdef ENABLE_JSON_PARSE_WITH_SOURCE +template <typename CharT> +class MOZ_STACK_CLASS JSONReviveParser + : JSONPerHandlerParser<CharT, JSONReviveHandler<CharT>> { + using Base = JSONPerHandlerParser<CharT, JSONReviveHandler<CharT>>; + + public: + using ParseType = JSONFullParseHandlerAnyChar::ParseType; + + /* Public API */ + + /* Create a parser for the provided JSON data. */ + JSONReviveParser(JSContext* cx, mozilla::Range<const CharT> data) + : Base(cx, data) {} + + /* Allow move construction for use with Rooted. */ + JSONReviveParser(JSONReviveParser&& other) noexcept + : Base(std::move(other)) {} + + JSONReviveParser(const JSONReviveParser& other) = delete; + void operator=(const JSONReviveParser& other) = delete; + + /* + * Parse the JSON data specified at construction time. If it parses + * successfully, store the prescribed value in *vp and return true. If an + * internal error (e.g. OOM) occurs during parsing, return false. + * Otherwise, if invalid input was specifed but no internal error occurred, + * behavior depends upon the error handling specified at construction: if + * error handling is RaiseError then throw a SyntaxError and return false, + * otherwise return true and set *vp to |undefined|. (JSON syntax can't + * represent |undefined|, so the JSON data couldn't have specified it.) + * + * If it parses successfully, parse information for calling the reviver + * function is stored in *pro. If this function returns false, *pro will be + * set to |undefined|. + */ bool parse(JS::MutableHandle<JS::Value> vp, JS::MutableHandle<ParseRecordObject> pro); void trace(JSTracer* trc); }; +#endif // ENABLE_JSON_PARSE_WITH_SOURCE template <typename CharT, typename Wrapper> class MutableWrappedPtrOperations<JSONParser<CharT>, Wrapper> @@ -523,10 +652,6 @@ class MutableWrappedPtrOperations<JSONParser<CharT>, Wrapper> bool parse(JS::MutableHandle<JS::Value> vp) { return static_cast<Wrapper*>(this)->get().parse(vp); } - bool parse(JS::MutableHandle<JS::Value> vp, - JS::MutableHandle<ParseRecordObject> pro) { - return static_cast<Wrapper*>(this)->get().parse(vp, pro); - } }; template <typename CharT> diff --git a/js/src/vm/JSONPrinter.cpp b/js/src/vm/JSONPrinter.cpp index 5b5183d9fb..53ab4be67c 100644 --- a/js/src/vm/JSONPrinter.cpp +++ b/js/src/vm/JSONPrinter.cpp @@ -154,8 +154,8 @@ void JSONPrinter::formatProperty(const char* name, const char* format, ...) { va_end(ap); } -void JSONPrinter::formatProperty(const char* name, const char* format, - va_list ap) { +void JSONPrinter::formatPropertyVA(const char* name, const char* format, + va_list ap) { beginStringProperty(name); out_.vprintf(format, ap); endStringProperty(); diff --git a/js/src/vm/JSONPrinter.h b/js/src/vm/JSONPrinter.h index b90696a2b4..61536a6c87 100644 --- a/js/src/vm/JSONPrinter.h +++ b/js/src/vm/JSONPrinter.h @@ -62,7 +62,7 @@ class JSONPrinter { void formatProperty(const char* name, const char* format, ...) MOZ_FORMAT_PRINTF(3, 4); - void formatProperty(const char* name, const char* format, va_list ap); + void formatPropertyVA(const char* name, const char* format, va_list ap); void propertyName(const char* name); diff --git a/js/src/vm/JSObject-inl.h b/js/src/vm/JSObject-inl.h index 4ca0946878..a493d7624a 100644 --- a/js/src/vm/JSObject-inl.h +++ b/js/src/vm/JSObject-inl.h @@ -189,8 +189,9 @@ template <typename T> MOZ_ASSERT(!cx->realm()->hasObjectPendingMetadata()); // The metadata builder is invoked for each object created on the main thread, - // except when it's suppressed. - if (!cx->zone()->suppressAllocationMetadataBuilder) { + // except when it's suppressed or we're throwing over-recursion error. + if (!cx->zone()->suppressAllocationMetadataBuilder && + !cx->isThrowingOverRecursed()) { // Don't collect metadata on objects that represent metadata, to avoid // recursion. AutoSuppressAllocationMetadataBuilder suppressMetadata(cx); diff --git a/js/src/vm/JSObject.cpp b/js/src/vm/JSObject.cpp index ea4dfeb6f7..8bc8bc0d52 100644 --- a/js/src/vm/JSObject.cpp +++ b/js/src/vm/JSObject.cpp @@ -2215,9 +2215,7 @@ JS_PUBLIC_API bool js::ShouldIgnorePropertyDefinition(JSContext* cx, id == NameToId(cx->names().symmetricDifference))) { return true; } -#endif -#ifdef NIGHTLY_BUILD if (key == JSProto_ArrayBuffer && !JS::Prefs::arraybuffer_transfer() && (id == NameToId(cx->names().transfer) || id == NameToId(cx->names().transferToFixedLength) || @@ -2240,6 +2238,33 @@ JS_PUBLIC_API bool js::ShouldIgnorePropertyDefinition(JSContext* cx, id == NameToId(cx->names().grow))) { return true; } + + if (key == JSProto_Uint8Array && + !JS::Prefs::experimental_uint8array_base64() && + (id == NameToId(cx->names().setFromBase64) || + id == NameToId(cx->names().setFromHex) || + id == NameToId(cx->names().toBase64) || + id == NameToId(cx->names().toHex))) { + return true; + } + + // It's gently surprising that this is JSProto_Function, but the trick + // to realize is that this is a -constructor function-, not a function + // on the prototype; and the proto of the constructor is JSProto_Function. + if (key == JSProto_Function && !JS::Prefs::experimental_uint8array_base64() && + (id == NameToId(cx->names().fromBase64) || + id == NameToId(cx->names().fromHex))) { + return true; + } +#endif + +#ifdef ENABLE_JSON_PARSE_WITH_SOURCE + if (key == JSProto_JSON && + !JS::Prefs::experimental_json_parse_with_source() && + (id == NameToId(cx->names().isRawJSON) || + id == NameToId(cx->names().rawJSON))) { + return true; + } #endif return false; @@ -3165,19 +3190,8 @@ js::gc::AllocKind JSObject::allocKindForTenure( return as<JSFunction>().getAllocKind(); } - // Fixed length typed arrays in the nursery may have a lazily allocated - // buffer, make sure there is room for the array's fixed data when moving - // the array. - if (is<FixedLengthTypedArrayObject>() && - !as<FixedLengthTypedArrayObject>().hasBuffer()) { - gc::AllocKind allocKind; - if (as<FixedLengthTypedArrayObject>().hasInlineElements()) { - size_t nbytes = as<FixedLengthTypedArrayObject>().byteLength(); - allocKind = FixedLengthTypedArrayObject::AllocKindForLazyBuffer(nbytes); - } else { - allocKind = GetGCObjectKind(getClass()); - } - return ForegroundToBackgroundAllocKind(allocKind); + if (is<FixedLengthTypedArrayObject>()) { + return as<FixedLengthTypedArrayObject>().allocKindForTenure(); } return as<NativeObject>().allocKindForTenure(); diff --git a/js/src/vm/Modules.cpp b/js/src/vm/Modules.cpp index f461e7bec1..917083a238 100644 --- a/js/src/vm/Modules.cpp +++ b/js/src/vm/Modules.cpp @@ -9,7 +9,8 @@ #include "vm/Modules.h" #include "mozilla/Assertions.h" // MOZ_ASSERT -#include "mozilla/Utf8.h" // mozilla::Utf8Unit +#include "mozilla/ScopeExit.h" +#include "mozilla/Utf8.h" // mozilla::Utf8Unit #include <stdint.h> // uint32_t @@ -42,6 +43,12 @@ using namespace js; using mozilla::Utf8Unit; +static bool ModuleLink(JSContext* cx, Handle<ModuleObject*> module); +static bool ModuleEvaluate(JSContext* cx, Handle<ModuleObject*> module, + MutableHandle<Value> result); +static bool SyntheticModuleEvaluate(JSContext* cx, Handle<ModuleObject*> module, + MutableHandle<Value> result); + //////////////////////////////////////////////////////////////////////////////// // Public API JS_PUBLIC_API JS::ModuleResolveHook JS::GetModuleResolveHook(JSRuntime* rt) { @@ -184,7 +191,7 @@ JS_PUBLIC_API bool JS::ModuleLink(JSContext* cx, Handle<JSObject*> moduleArg) { CHECK_THREAD(cx); cx->releaseCheck(moduleArg); - return js::ModuleLink(cx, moduleArg.as<ModuleObject>()); + return ::ModuleLink(cx, moduleArg.as<ModuleObject>()); } JS_PUBLIC_API bool JS::ModuleEvaluate(JSContext* cx, @@ -194,11 +201,17 @@ JS_PUBLIC_API bool JS::ModuleEvaluate(JSContext* cx, CHECK_THREAD(cx); cx->releaseCheck(moduleRecord); + cx->isEvaluatingModule++; + auto guard = mozilla::MakeScopeExit([cx] { + MOZ_ASSERT(cx->isEvaluatingModule != 0); + cx->isEvaluatingModule--; + }); + if (moduleRecord.as<ModuleObject>()->hasSyntheticModuleFields()) { return SyntheticModuleEvaluate(cx, moduleRecord.as<ModuleObject>(), rval); } - return js::ModuleEvaluate(cx, moduleRecord.as<ModuleObject>(), rval); + return ::ModuleEvaluate(cx, moduleRecord.as<ModuleObject>(), rval); } JS_PUBLIC_API bool JS::ThrowOnModuleEvaluationFailure( @@ -327,6 +340,11 @@ JS_PUBLIC_API void JS::ClearModuleEnvironment(JSObject* moduleObj) { } } +JS_PUBLIC_API bool JS::ModuleIsLinked(JSObject* moduleObj) { + AssertHeapIsIdle(); + return moduleObj->as<ModuleObject>().status() != ModuleStatus::Unlinked; +} + //////////////////////////////////////////////////////////////////////////////// // Internal implementation @@ -356,14 +374,17 @@ static ModuleObject* HostResolveImportedModule( JSContext* cx, Handle<ModuleObject*> module, Handle<ModuleRequestObject*> moduleRequest, ModuleStatus expectedMinimumStatus); -static bool ModuleResolveExport(JSContext* cx, Handle<ModuleObject*> module, - Handle<JSAtom*> exportName, - MutableHandle<ResolveSet> resolveSet, - MutableHandle<Value> result); +static bool CyclicModuleResolveExport(JSContext* cx, + Handle<ModuleObject*> module, + Handle<JSAtom*> exportName, + MutableHandle<ResolveSet> resolveSet, + MutableHandle<Value> result, + ModuleErrorInfo* errorInfoOut = nullptr); static bool SyntheticModuleResolveExport(JSContext* cx, Handle<ModuleObject*> module, Handle<JSAtom*> exportName, - MutableHandle<Value> result); + MutableHandle<Value> result, + ModuleErrorInfo* errorInfoOut); static ModuleNamespaceObject* ModuleNamespaceCreate( JSContext* cx, Handle<ModuleObject*> module, MutableHandle<UniquePtr<ExportNameVector>> exports); @@ -575,17 +596,20 @@ static ModuleObject* HostResolveImportedModule( // - If the request is found to be ambiguous, the string `"ambiguous"` is // returned. // -bool js::ModuleResolveExport(JSContext* cx, Handle<ModuleObject*> module, - Handle<JSAtom*> exportName, - MutableHandle<Value> result) { +static bool ModuleResolveExport(JSContext* cx, Handle<ModuleObject*> module, + Handle<JSAtom*> exportName, + MutableHandle<Value> result, + ModuleErrorInfo* errorInfoOut = nullptr) { if (module->hasSyntheticModuleFields()) { - return ::SyntheticModuleResolveExport(cx, module, exportName, result); + return SyntheticModuleResolveExport(cx, module, exportName, result, + errorInfoOut); } // Step 1. If resolveSet is not present, set resolveSet to a new empty List. Rooted<ResolveSet> resolveSet(cx); - return ::ModuleResolveExport(cx, module, exportName, &resolveSet, result); + return CyclicModuleResolveExport(cx, module, exportName, &resolveSet, result, + errorInfoOut); } static bool CreateResolvedBindingObject(JSContext* cx, @@ -602,10 +626,12 @@ static bool CreateResolvedBindingObject(JSContext* cx, return true; } -static bool ModuleResolveExport(JSContext* cx, Handle<ModuleObject*> module, - Handle<JSAtom*> exportName, - MutableHandle<ResolveSet> resolveSet, - MutableHandle<Value> result) { +static bool CyclicModuleResolveExport(JSContext* cx, + Handle<ModuleObject*> module, + Handle<JSAtom*> exportName, + MutableHandle<ResolveSet> resolveSet, + MutableHandle<Value> result, + ModuleErrorInfo* errorInfoOut) { // Step 2. For each Record { [[Module]], [[ExportName]] } r of resolveSet, do: for (const auto& entry : resolveSet) { // Step 2.a. If module and r.[[Module]] are the same Module Record and @@ -614,6 +640,9 @@ static bool ModuleResolveExport(JSContext* cx, Handle<ModuleObject*> module, // Step 2.a.i. Assert: This is a circular import request. // Step 2.a.ii. Return null. result.setNull(); + if (errorInfoOut) { + errorInfoOut->setCircularImport(cx, module); + } return true; } } @@ -669,8 +698,8 @@ static bool ModuleResolveExport(JSContext* cx, Handle<ModuleObject*> module, // importedModule.ResolveExport(e.[[ImportName]], // resolveSet). name = e.importName(); - return ModuleResolveExport(cx, importedModule, name, resolveSet, - result); + return CyclicModuleResolveExport(cx, importedModule, name, resolveSet, + result, errorInfoOut); } } } @@ -683,6 +712,9 @@ static bool ModuleResolveExport(JSContext* cx, Handle<ModuleObject*> module, // Step 6.c. NOTE: A default export cannot be provided by an export * from // "mod" declaration. result.setNull(); + if (errorInfoOut) { + errorInfoOut->setImportedModule(cx, module); + } return true; } @@ -704,8 +736,8 @@ static bool ModuleResolveExport(JSContext* cx, Handle<ModuleObject*> module, // Step 8.b. Let resolution be ? importedModule.ResolveExport(exportName, // resolveSet). - if (!ModuleResolveExport(cx, importedModule, exportName, resolveSet, - &resolution)) { + if (!CyclicModuleResolveExport(cx, importedModule, exportName, resolveSet, + &resolution, errorInfoOut)) { return false; } @@ -744,6 +776,12 @@ static bool ModuleResolveExport(JSContext* cx, Handle<ModuleObject*> module, if (binding->module() != starResolution->module() || binding->bindingName() != starResolution->bindingName()) { result.set(StringValue(cx->names().ambiguous)); + + if (errorInfoOut) { + Rooted<ModuleObject*> module1(cx, starResolution->module()); + Rooted<ModuleObject*> module2(cx, binding->module()); + errorInfoOut->setForAmbiguousImport(cx, module, module1, module2); + } return true; } } @@ -752,6 +790,9 @@ static bool ModuleResolveExport(JSContext* cx, Handle<ModuleObject*> module, // Step 9. Return starResolution. result.setObjectOrNull(starResolution); + if (!starResolution && errorInfoOut) { + errorInfoOut->setImportedModule(cx, module); + } return true; } @@ -759,10 +800,14 @@ static bool ModuleResolveExport(JSContext* cx, Handle<ModuleObject*> module, static bool SyntheticModuleResolveExport(JSContext* cx, Handle<ModuleObject*> module, Handle<JSAtom*> exportName, - MutableHandle<Value> result) { + MutableHandle<Value> result, + ModuleErrorInfo* errorInfoOut) { // Step 2. If module.[[ExportNames]] does not contain exportName, return null. if (!ContainsElement(module->syntheticExportNames(), exportName)) { result.setNull(); + if (errorInfoOut) { + errorInfoOut->setImportedModule(cx, module); + } return true; } @@ -923,63 +968,93 @@ static ModuleNamespaceObject* ModuleNamespaceCreate( return ns; } -static void ThrowResolutionError(JSContext* cx, Handle<ModuleObject*> module, - Handle<Value> resolution, bool isDirectImport, - Handle<JSAtom*> name, uint32_t line, - JS::ColumnNumberOneOrigin column) { - MOZ_ASSERT(line != 0); +void ModuleErrorInfo::setImportedModule(JSContext* cx, + ModuleObject* importedModule) { + imported = importedModule->filename(); +} - bool isAmbiguous = resolution == StringValue(cx->names().ambiguous); +void ModuleErrorInfo::setCircularImport(JSContext* cx, + ModuleObject* importedModule) { + setImportedModule(cx, importedModule); + isCircular = true; +} - // ErrorNumbers: - // | MISSING | AMBIGUOUS | - // ---------+---------+-----------+ - // INDIRECT | - // DIRECT | - static constexpr unsigned ErrorNumbers[2][2] = { - {JSMSG_MISSING_INDIRECT_EXPORT, JSMSG_AMBIGUOUS_INDIRECT_EXPORT}, - {JSMSG_MISSING_IMPORT, JSMSG_AMBIGUOUS_IMPORT}}; - unsigned errorNumber = ErrorNumbers[isDirectImport][isAmbiguous]; - - const JSErrorFormatString* errorString = - GetErrorMessage(nullptr, errorNumber); - MOZ_ASSERT(errorString); - - MOZ_ASSERT(errorString->argCount == 0); - Rooted<JSString*> message(cx, JS_NewStringCopyZ(cx, errorString->format)); - if (!message) { +void ModuleErrorInfo::setForAmbiguousImport(JSContext* cx, + ModuleObject* importedModule, + ModuleObject* module1, + ModuleObject* module2) { + setImportedModule(cx, importedModule); + entry1 = module1->filename(); + entry2 = module2->filename(); +} + +static void CreateErrorNumberMessageUTF8(JSContext* cx, unsigned errorNumber, + JSErrorReport* reportOut, ...) { + va_list ap; + va_start(ap, reportOut); + AutoReportFrontendContext fc(cx); + if (!ExpandErrorArgumentsVA(&fc, GetErrorMessage, nullptr, errorNumber, + ArgumentsAreUTF8, reportOut, ap)) { + ReportOutOfMemory(cx); return; } - Rooted<JSString*> separator(cx, JS_NewStringCopyZ(cx, ": ")); - if (!separator) { + va_end(ap); +} + +static void ThrowResolutionError(JSContext* cx, Handle<ModuleObject*> module, + Handle<Value> resolution, Handle<JSAtom*> name, + ModuleErrorInfo* errorInfo) { + MOZ_ASSERT(errorInfo); + auto chars = StringToNewUTF8CharsZ(cx, *name); + if (!chars) { + ReportOutOfMemory(cx); return; } - message = ConcatStrings<CanGC>(cx, message, separator); - if (!message) { - return; + bool isAmbiguous = resolution == StringValue(cx->names().ambiguous); + + unsigned errorNumber; + if (errorInfo->isCircular) { + errorNumber = JSMSG_MODULE_CIRCULAR_IMPORT; + } else if (isAmbiguous) { + errorNumber = JSMSG_MODULE_AMBIGUOUS; + } else { + errorNumber = JSMSG_MODULE_NO_EXPORT; + } + + JSErrorReport report; + report.isWarning_ = false; + report.errorNumber = errorNumber; + + if (errorNumber == JSMSG_MODULE_AMBIGUOUS) { + CreateErrorNumberMessageUTF8(cx, errorNumber, &report, errorInfo->imported, + chars.get(), errorInfo->entry1, + errorInfo->entry2); + } else { + CreateErrorNumberMessageUTF8(cx, errorNumber, &report, errorInfo->imported, + chars.get()); } - message = ConcatStrings<CanGC>(cx, message, name); + Rooted<JSString*> message(cx, report.newMessageString(cx)); if (!message) { + ReportOutOfMemory(cx); return; } - RootedString filename(cx); - if (const char* chars = module->script()->filename()) { - filename = - JS_NewStringCopyUTF8Z(cx, JS::ConstUTF8CharsZ(chars, strlen(chars))); - } else { - filename = cx->names().empty_; - } + const char* file = module->filename(); + RootedString filename( + cx, JS_NewStringCopyUTF8Z(cx, JS::ConstUTF8CharsZ(file, strlen(file)))); if (!filename) { + ReportOutOfMemory(cx); return; } RootedValue error(cx); - if (!JS::CreateError(cx, JSEXN_SYNTAXERR, nullptr, filename, line, column, - nullptr, message, JS::NothingHandleValue, &error)) { + if (!JS::CreateError(cx, JSEXN_SYNTAXERR, nullptr, filename, + errorInfo->lineNumber, errorInfo->columnNumber, nullptr, + message, JS::NothingHandleValue, &error)) { + ReportOutOfMemory(cx); return; } @@ -988,8 +1063,8 @@ static void ThrowResolutionError(JSContext* cx, Handle<ModuleObject*> module, // https://tc39.es/ecma262/#sec-source-text-module-record-initialize-environment // ES2023 16.2.1.6.4 InitializeEnvironment -bool js::ModuleInitializeEnvironment(JSContext* cx, - Handle<ModuleObject*> module) { +static bool ModuleInitializeEnvironment(JSContext* cx, + Handle<ModuleObject*> module) { MOZ_ASSERT(module->status() == ModuleStatus::Linking); // Step 1. For each ExportEntry Record e of module.[[IndirectExportEntries]], @@ -1002,15 +1077,15 @@ bool js::ModuleInitializeEnvironment(JSContext* cx, // Step 1.b. Let resolution be ? module.ResolveExport(e.[[ExportName]]). exportName = e.exportName(); - if (!ModuleResolveExport(cx, module, exportName, &resolution)) { + ModuleErrorInfo errorInfo{e.lineNumber(), e.columnNumber()}; + if (!ModuleResolveExport(cx, module, exportName, &resolution, &errorInfo)) { return false; } // Step 1.c. If resolution is either null or AMBIGUOUS, throw a SyntaxError // exception. if (!IsResolvedBinding(cx, resolution)) { - ThrowResolutionError(cx, module, resolution, false, exportName, - e.lineNumber(), e.columnNumber()); + ThrowResolutionError(cx, module, resolution, exportName, &errorInfo); return false; } } @@ -1059,15 +1134,16 @@ bool js::ModuleInitializeEnvironment(JSContext* cx, // Step 7.d. Else: // Step 7.d.i. Let resolution be ? // importedModule.ResolveExport(in.[[ImportName]]). - if (!ModuleResolveExport(cx, importedModule, importName, &resolution)) { + ModuleErrorInfo errorInfo{in.lineNumber(), in.columnNumber()}; + if (!ModuleResolveExport(cx, importedModule, importName, &resolution, + &errorInfo)) { return false; } // Step 7.d.ii. If resolution is null or ambiguous, throw a SyntaxError // exception. if (!IsResolvedBinding(cx, resolution)) { - ThrowResolutionError(cx, module, resolution, true, importName, - in.lineNumber(), in.columnNumber()); + ThrowResolutionError(cx, module, resolution, importName, &errorInfo); return false; } @@ -1134,7 +1210,7 @@ bool js::ModuleInitializeEnvironment(JSContext* cx, // https://tc39.es/ecma262/#sec-moduledeclarationlinking // ES2023 16.2.1.5.1 Link -bool js::ModuleLink(JSContext* cx, Handle<ModuleObject*> module) { +static bool ModuleLink(JSContext* cx, Handle<ModuleObject*> module) { // Step 1. Assert: module.[[Status]] is not linking or evaluating. ModuleStatus status = module->status(); if (status == ModuleStatus::Linking || status == ModuleStatus::Evaluating) { @@ -1313,8 +1389,9 @@ static bool InnerModuleLinking(JSContext* cx, Handle<ModuleObject*> module, return true; } -bool js::SyntheticModuleEvaluate(JSContext* cx, Handle<ModuleObject*> moduleArg, - MutableHandle<Value> result) { +static bool SyntheticModuleEvaluate(JSContext* cx, + Handle<ModuleObject*> moduleArg, + MutableHandle<Value> result) { // Steps 1-12 happens elsewhere in the engine. // Step 13. Let pc be ! NewPromiseCapability(%Promise%). @@ -1337,8 +1414,8 @@ bool js::SyntheticModuleEvaluate(JSContext* cx, Handle<ModuleObject*> moduleArg, // https://tc39.es/ecma262/#sec-moduleevaluation // ES2023 16.2.1.5.2 Evaluate -bool js::ModuleEvaluate(JSContext* cx, Handle<ModuleObject*> moduleArg, - MutableHandle<Value> result) { +static bool ModuleEvaluate(JSContext* cx, Handle<ModuleObject*> moduleArg, + MutableHandle<Value> result) { Rooted<ModuleObject*> module(cx, moduleArg); // Step 2. Assert: module.[[Status]] is linked, evaluating-async, or diff --git a/js/src/vm/Modules.h b/js/src/vm/Modules.h index 21aff46b90..d59c9db552 100644 --- a/js/src/vm/Modules.h +++ b/js/src/vm/Modules.h @@ -20,22 +20,32 @@ namespace js { using ModuleVector = GCVector<ModuleObject*, 0, SystemAllocPolicy>; -bool ModuleResolveExport(JSContext* cx, Handle<ModuleObject*> module, - Handle<JSAtom*> exportName, - MutableHandle<Value> result); +// A struct with detailed error information when import/export failed. +struct ModuleErrorInfo { + ModuleErrorInfo(uint32_t lineNumber_, JS::ColumnNumberOneOrigin columnNumber_) + : lineNumber(lineNumber_), columnNumber(columnNumber_) {} -ModuleNamespaceObject* GetOrCreateModuleNamespace(JSContext* cx, - Handle<ModuleObject*> module); + void setImportedModule(JSContext* cx, ModuleObject* importedModule); + void setCircularImport(JSContext* cx, ModuleObject* importedModule); + void setForAmbiguousImport(JSContext* cx, ModuleObject* importedModule, + ModuleObject* module1, ModuleObject* module2); + + uint32_t lineNumber; + JS::ColumnNumberOneOrigin columnNumber; -bool ModuleInitializeEnvironment(JSContext* cx, Handle<ModuleObject*> module); + // The filename of the imported module. + const char* imported; -bool ModuleLink(JSContext* cx, Handle<ModuleObject*> module); + // The filenames of the ambiguous entries. + const char* entry1; + const char* entry2; -// Start evaluating the module. If TLA is enabled, result will be a promise. -bool ModuleEvaluate(JSContext* cx, Handle<ModuleObject*> module, - MutableHandle<Value> result); -bool SyntheticModuleEvaluate(JSContext* cx, Handle<ModuleObject*> module, - MutableHandle<Value> result); + // A bool to indicate the error is a circular import when it's true. + bool isCircular = false; +}; + +ModuleNamespaceObject* GetOrCreateModuleNamespace(JSContext* cx, + Handle<ModuleObject*> module); void AsyncModuleExecutionFulfilled(JSContext* cx, Handle<ModuleObject*> module); diff --git a/js/src/vm/NativeObject.cpp b/js/src/vm/NativeObject.cpp index 640a185981..19f8b94bb6 100644 --- a/js/src/vm/NativeObject.cpp +++ b/js/src/vm/NativeObject.cpp @@ -307,20 +307,26 @@ mozilla::Maybe<PropertyInfo> js::NativeObject::lookupPure(jsid id) { return mozilla::Nothing(); } -bool NativeObject::setUniqueId(JSContext* cx, uint64_t uid) { +bool NativeObject::setUniqueId(JSRuntime* runtime, uint64_t uid) { MOZ_ASSERT(!hasUniqueId()); MOZ_ASSERT(!gc::HasUniqueId(this)); - return setOrUpdateUniqueId(cx, uid); + Nursery& nursery = runtime->gc.nursery(); + if (!hasDynamicSlots() && !allocateSlots(nursery, 0)) { + return false; + } + + getSlotsHeader()->setUniqueId(uid); + return true; } bool NativeObject::setOrUpdateUniqueId(JSContext* cx, uint64_t uid) { - if (!hasDynamicSlots() && !allocateSlots(cx, 0)) { + if (!hasDynamicSlots() && !allocateSlots(cx->nursery(), 0)) { + ReportOutOfMemory(cx); return false; } getSlotsHeader()->setUniqueId(uid); - return true; } @@ -337,7 +343,12 @@ bool NativeObject::growSlots(JSContext* cx, uint32_t oldCapacity, MOZ_ASSERT(newCapacity <= MAX_SLOTS_COUNT); if (!hasDynamicSlots()) { - return allocateSlots(cx, newCapacity); + if (!allocateSlots(cx->nursery(), newCapacity)) { + ReportOutOfMemory(cx); + return false; + } + + return true; } uint64_t uid = maybeUniqueId(); @@ -415,7 +426,7 @@ bool NativeObject::allocateInitialSlots(JSContext* cx, uint32_t capacity) { return true; } -bool NativeObject::allocateSlots(JSContext* cx, uint32_t newCapacity) { +bool NativeObject::allocateSlots(Nursery& nursery, uint32_t newCapacity) { MOZ_ASSERT(!hasUniqueId()); MOZ_ASSERT(!hasDynamicSlots()); @@ -423,7 +434,8 @@ bool NativeObject::allocateSlots(JSContext* cx, uint32_t newCapacity) { uint32_t dictionarySpan = getSlotsHeader()->dictionarySlotSpan(); - HeapSlot* allocation = AllocateCellBuffer<HeapSlot>(cx, this, newAllocated); + HeapSlot* allocation = + AllocateCellBuffer<HeapSlot>(nursery, this, newAllocated); if (!allocation) { return false; } diff --git a/js/src/vm/NativeObject.h b/js/src/vm/NativeObject.h index 8312310219..18ca0b656e 100644 --- a/js/src/vm/NativeObject.h +++ b/js/src/vm/NativeObject.h @@ -987,7 +987,7 @@ class NativeObject : public JSObject { bool growSlotsForNewSlot(JSContext* cx, uint32_t numFixed, uint32_t slot); void shrinkSlots(JSContext* cx, uint32_t oldCapacity, uint32_t newCapacity); - bool allocateSlots(JSContext* cx, uint32_t newCapacity); + bool allocateSlots(Nursery& nursery, uint32_t newCapacity); /* * This method is static because it's called from JIT code. On OOM, returns @@ -1257,7 +1257,7 @@ class NativeObject : public JSObject { return UndefinedValue(); } - [[nodiscard]] bool setUniqueId(JSContext* cx, uint64_t uid); + [[nodiscard]] bool setUniqueId(JSRuntime* runtime, uint64_t uid); inline bool hasUniqueId() const { return getSlotsHeader()->hasUniqueId(); } inline uint64_t uniqueId() const { return getSlotsHeader()->uniqueId(); } inline uint64_t maybeUniqueId() const { diff --git a/js/src/vm/PortableBaselineInterpret.cpp b/js/src/vm/PortableBaselineInterpret.cpp index 2990942dc6..2588f12009 100644 --- a/js/src/vm/PortableBaselineInterpret.cpp +++ b/js/src/vm/PortableBaselineInterpret.cpp @@ -1373,9 +1373,13 @@ ICInterpretOps(BaselineFrame* frame, VMFrameManager& frameMgr, State& state, CACHEOP_CASE(LoadWrapperTarget) { ObjOperandId objId = icregs.cacheIRReader.objOperandId(); ObjOperandId resultId = icregs.cacheIRReader.objOperandId(); + bool fallible = icregs.cacheIRReader.readBool(); BOUNDSCHECK(resultId); JSObject* obj = reinterpret_cast<JSObject*>(icregs.icVals[objId.id()]); - JSObject* target = &obj->as<ProxyObject>().private_().toObject(); + JSObject* target = obj->as<ProxyObject>().private_().toObjectOrNull(); + if (fallible && !target) { + return ICInterpretOpResult::NextIC; + } icregs.icVals[resultId.id()] = reinterpret_cast<uintptr_t>(target); DISPATCH_CACHEOP(); } diff --git a/js/src/vm/RealmFuses.cpp b/js/src/vm/RealmFuses.cpp index 3ceac2dd25..8f7a7801cc 100644 --- a/js/src/vm/RealmFuses.cpp +++ b/js/src/vm/RealmFuses.cpp @@ -11,6 +11,27 @@ #include "vm/Realm.h" #include "vm/SelfHosting.h" +void js::InvalidatingRealmFuse::popFuse(JSContext* cx, RealmFuses& realmFuses) { + InvalidatingFuse::popFuse(cx); + + for (auto& fd : realmFuses.fuseDependencies) { + fd.invalidateForFuse(cx, this); + } +} + +bool js::InvalidatingRealmFuse::addFuseDependency(JSContext* cx, + Handle<JSScript*> script) { + MOZ_ASSERT(script->realm() == cx->realm()); + auto* dss = + cx->realm()->realmFuses.fuseDependencies.getOrCreateDependentScriptSet( + cx, this); + if (!dss) { + return false; + } + + return dss->addScriptForFuse(this, script); +} + void js::PopsOptimizedGetIteratorFuse::popFuse(JSContext* cx, RealmFuses& realmFuses) { // Pop Self. diff --git a/js/src/vm/RealmFuses.h b/js/src/vm/RealmFuses.h index 54fa7bc3bf..a14ff73c59 100644 --- a/js/src/vm/RealmFuses.h +++ b/js/src/vm/RealmFuses.h @@ -8,6 +8,7 @@ #define vm_RealmFuses_h #include "vm/GuardFuse.h" +#include "vm/InvalidatingFuse.h" namespace js { @@ -28,7 +29,19 @@ class RealmFuse : public GuardFuse { virtual void popFuse(JSContext* cx) override { GuardFuse::popFuse(cx); } }; -struct OptimizeGetIteratorFuse final : public RealmFuse { +class InvalidatingRealmFuse : public InvalidatingFuse { + public: + virtual void popFuse(JSContext* cx, RealmFuses& realmFuses); + virtual bool addFuseDependency(JSContext* cx, + Handle<JSScript*> script) override; + + protected: + virtual void popFuse(JSContext* cx) override { + InvalidatingFuse::popFuse(cx); + } +}; + +struct OptimizeGetIteratorFuse final : public InvalidatingRealmFuse { virtual const char* name() override { return "OptimizeGetIteratorFuse"; } virtual bool checkInvariant(JSContext* cx) override; }; @@ -144,6 +157,8 @@ struct RealmFuses { MOZ_CRASH("Fuse Not Found"); } + DependentScriptGroup fuseDependencies; + static int32_t fuseOffsets[]; static const char* fuseNames[]; diff --git a/js/src/vm/Runtime.cpp b/js/src/vm/Runtime.cpp index 1c3611a6e5..d894bf57bf 100644 --- a/js/src/vm/Runtime.cpp +++ b/js/src/vm/Runtime.cpp @@ -332,7 +332,7 @@ void JSRuntime::addSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf, rtSizes->uncompressedSourceCache += caches().uncompressedSourceCache.sizeOfExcludingThis(mallocSizeOf); - rtSizes->gc.nurseryCommitted += gc.nursery().committed(); + rtSizes->gc.nurseryCommitted += gc.nursery().totalCommitted(); rtSizes->gc.nurseryMallocedBuffers += gc.nursery().sizeOfMallocedBuffers(mallocSizeOf); gc.storeBuffer().addSizeOfExcludingThis(mallocSizeOf, &rtSizes->gc); diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index 32cdd4a335..5170b072fb 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -50,6 +50,7 @@ #include "frontend/FrontendContext.h" // AutoReportFrontendContext #include "jit/AtomicOperations.h" #include "jit/InlinableNatives.h" +#include "jit/TrampolineNatives.h" #include "js/CompilationAndEvaluation.h" #include "js/Conversions.h" #include "js/ErrorReport.h" // JS::PrintError @@ -900,7 +901,7 @@ static bool intrinsic_GeneratorSetClosed(JSContext* cx, unsigned argc, MOZ_ASSERT(args[0].isObject()); GeneratorObject* genObj = &args[0].toObject().as<GeneratorObject>(); - genObj->setClosed(); + genObj->setClosed(cx); return true; } @@ -1962,7 +1963,6 @@ static const JSFunctionSpec intrinsic_functions[] = { JS_INLINABLE_FN("ArrayIteratorPrototypeOptimizable", intrinsic_ArrayIteratorPrototypeOptimizable, 0, 0, IntrinsicArrayIteratorPrototypeOptimizable), - JS_FN("ArrayNativeSort", intrinsic_ArrayNativeSort, 1, 0), JS_FN("AssertionFailed", intrinsic_AssertionFailed, 1, 0), JS_FN("CallArrayBufferMethodIfWrapped", CallNonGenericSelfhostedMethod<Is<ArrayBufferObject>>, 2, 0), @@ -2336,6 +2336,7 @@ static const JSFunctionSpec intrinsic_functions[] = { JS_FN("std_Array_indexOf", array_indexOf, 1, 0), JS_FN("std_Array_lastIndexOf", array_lastIndexOf, 1, 0), JS_INLINABLE_FN("std_Array_pop", array_pop, 0, 0, ArrayPop), + JS_TRAMPOLINE_FN("std_Array_sort", array_sort, 1, 0, ArraySort), JS_FN("std_BigInt_valueOf", BigIntObject::valueOf, 0, 0), JS_FN("std_Date_now", date_now, 0, 0), JS_FN("std_Function_apply", fun_apply, 2, 0), diff --git a/js/src/vm/Stack.cpp b/js/src/vm/Stack.cpp index d222ddc51c..c8a8898d49 100644 --- a/js/src/vm/Stack.cpp +++ b/js/src/vm/Stack.cpp @@ -509,9 +509,8 @@ void JS::ProfilingFrameIterator::operator++() { void JS::ProfilingFrameIterator::settleFrames() { // Handle transition frames (see comment in JitFrameIter::operator++). - if (isJSJit() && !jsJitIter().done() && - jsJitIter().frameType() == jit::FrameType::WasmToJSJit) { - wasm::Frame* fp = (wasm::Frame*)jsJitIter().fp(); + if (isJSJit() && jsJitIter().done() && jsJitIter().wasmCallerFP()) { + wasm::Frame* fp = (wasm::Frame*)jsJitIter().wasmCallerFP(); iteratorDestroy(); new (storage()) wasm::ProfilingFrameIterator(fp); kind_ = Kind::Wasm; @@ -529,7 +528,6 @@ void JS::ProfilingFrameIterator::settleFrames() { new (storage()) jit::JSJitProfilingFrameIterator((jit::CommonFrameLayout*)fp); kind_ = Kind::JSJit; - MOZ_ASSERT(!jsJitIter().done()); maybeSetEndStackAddress(jsJitIter().endStackAddress()); return; } diff --git a/js/src/vm/StringType-inl.h b/js/src/vm/StringType-inl.h index 8954a46aac..4548e5d7ea 100644 --- a/js/src/vm/StringType-inl.h +++ b/js/src/vm/StringType-inl.h @@ -19,6 +19,7 @@ #include "vm/StaticStrings.h" #include "gc/GCContext-inl.h" +#include "gc/Marking-inl.h" #include "gc/StoreBuffer-inl.h" #include "vm/JSContext-inl.h" @@ -451,6 +452,20 @@ void JSLinearString::disownCharsBecauseError() { d.s.u2.nonInlineCharsLatin1 = nullptr; } +inline JSLinearString* JSDependentString::rootBaseDuringMinorGC() { + JSLinearString* root = this; + while (MaybeForwarded(root)->hasBase()) { + if (root->isForwarded()) { + root = js::gc::StringRelocationOverlay::fromCell(root) + ->savedNurseryBaseOrRelocOverlay(); + } else { + // Possibly nursery or tenured string (not an overlay). + root = root->nurseryBaseOrRelocOverlay(); + } + } + return root; +} + template <js::AllowGC allowGC, typename CharT> MOZ_ALWAYS_INLINE JSLinearString* JSLinearString::new_( JSContext* cx, JS::MutableHandle<JSString::OwnedChars<CharT>> chars, @@ -530,6 +545,19 @@ inline js::PropertyName* JSLinearString::toPropertyName(JSContext* cx) { return atom->asPropertyName(); } +// String characters are movable in the following cases: +// +// 1. Inline nursery strings (moved during promotion) +// 2. Nursery strings with nursery chars (moved during promotion) +// 3. Nursery strings that are deduplicated (moved during promotion) +// 4. Inline tenured strings (moved during compaction) +// +// This method does not consider #3, because if this method returns true and the +// caller does not want the characters to move, it can fix them in place by +// setting the nondeduplicatable bit. (If the bit were already taken into +// consideration, then the caller wouldn't know whether the movability is +// "fixable" or not. If it is *only* movable because of the lack of the bit +// being set, then it is fixable by setting the bit.) bool JSLinearString::hasMovableChars() const { const JSLinearString* topBase = this; while (topBase->hasBase()) { diff --git a/js/src/vm/StringType.cpp b/js/src/vm/StringType.cpp index 63afd8864b..b735b91b71 100644 --- a/js/src/vm/StringType.cpp +++ b/js/src/vm/StringType.cpp @@ -380,9 +380,13 @@ void ForEachStringFlag(const JSString* str, uint32_t flags, KnownF known, static_assert(JSString::LINEAR_IS_EXTENSIBLE_BIT == JSString::INLINE_IS_FAT_BIT); if (str->isLinear()) { - known("EXTENSIBLE"); - } else if (str->isInline()) { - known("FAT"); + if (str->isInline()) { + known("FAT"); + } else if (!str->isAtom()) { + known("EXTENSIBLE"); + } else { + unknown(i); + } } else { unknown(i); } @@ -407,9 +411,6 @@ void ForEachStringFlag(const JSString* str, uint32_t flags, KnownF known, case JSString::INDEX_VALUE_BIT: known("INDEX_VALUE_BIT"); break; - case JSString::NON_DEDUP_BIT: - known("NON_DEDUP_BIT"); - break; case JSString::IN_STRING_TO_ATOM_CACHE: known("IN_STRING_TO_ATOM_CACHE"); break; @@ -417,7 +418,7 @@ void ForEachStringFlag(const JSString* str, uint32_t flags, KnownF known, if (str->isRope()) { known("FLATTEN_VISIT_RIGHT"); } else { - unknown(i); + known("NON_DEDUP_BIT"); } break; case JSString::FLATTEN_FINISH_NODE: @@ -638,11 +639,20 @@ static MOZ_ALWAYS_INLINE JSString::OwnedChars<CharT> AllocChars(JSContext* cx, MOZ_ASSERT(cx->nursery().isEnabled()); auto [buffer, isMalloced] = cx->nursery().allocateBuffer( cx->zone(), length * sizeof(CharT), js::StringBufferArena); + if (!buffer) { + ReportOutOfMemory(cx); + return {nullptr, 0, false, false}; + } return {static_cast<CharT*>(buffer), length, isMalloced, isMalloced}; } auto buffer = cx->make_pod_arena_array<CharT>(js::StringBufferArena, length); + if (!buffer) { + ReportOutOfMemory(cx); + return {nullptr, 0, false, false}; + } + return {std::move(buffer), length, true}; } @@ -914,6 +924,14 @@ JSLinearString* JSRope::flattenInternal(JSRope* root) { * JSDependentStrings pointing to them already. Stealing the buffer doesn't * change its address, only its owning JSExtensibleString, so all chars() * pointers in the JSDependentStrings are still valid. + * + * This chain of dependent strings could be problematic if the base string + * moves, either because it was initially allocated in the nursery or it + * gets deduplicated, because you might have a dependent -> + * tenured dependent -> nursery base string, and the store buffer would + * only capture the latter edge. Prevent this case from happening by + * marking the root as nondeduplicatable if the extensible string + * optimization applied. */ const size_t wholeLength = root->length(); size_t wholeCapacity; @@ -1068,8 +1086,12 @@ finish_root: left.setLengthAndFlags(left.length(), StringFlagsForCharType<CharT>(flags)); left.d.s.u3.base = &root->asLinear(); if (left.isTenured() && !root->isTenured()) { - // leftmost child -> root is a tenured -> nursery edge. + // leftmost child -> root is a tenured -> nursery edge. Put the leftmost + // child in the store buffer and prevent the root's chars from moving or + // being freed (because the leftmost child may have a tenured dependent + // string that cannot be updated.) root->storeBuffer()->putWholeCell(&left); + root->setNonDeduplicatable(); } } @@ -1455,16 +1477,18 @@ uint32_t JSAtom::getIndexSlow() const { : AtomCharsToIndex(twoByteChars(nogc), len); } -static void MarkStringAndBasesNonDeduplicatable(JSLinearString* s) { - while (true) { - if (!s->isTenured()) { - s->setNonDeduplicatable(); - } - if (!s->hasBase()) { - break; - } +// Prevent the actual owner of the string's characters from being deduplicated +// (and thus freeing its characters, which would invalidate the ASSC's chars +// pointer). Intermediate dependent strings on the chain can be deduplicated, +// since the base will be updated to the root base during tenuring anyway and +// the intermediates won't matter. +void PreventRootBaseDeduplication(JSLinearString* s) { + while (s->hasBase()) { s = s->base(); } + if (!s->isTenured()) { + s->setNonDeduplicatable(); + } } bool AutoStableStringChars::init(JSContext* cx, JSString* s) { @@ -1492,7 +1516,7 @@ bool AutoStableStringChars::init(JSContext* cx, JSString* s) { twoByteChars_ = linearString->rawTwoByteChars(); } - MarkStringAndBasesNonDeduplicatable(linearString); + PreventRootBaseDeduplication(linearString); s_ = linearString; return true; @@ -1518,7 +1542,7 @@ bool AutoStableStringChars::initTwoByte(JSContext* cx, JSString* s) { state_ = TwoByte; twoByteChars_ = linearString->rawTwoByteChars(); - MarkStringAndBasesNonDeduplicatable(linearString); + PreventRootBaseDeduplication(linearString); s_ = linearString; return true; diff --git a/js/src/vm/StringType.h b/js/src/vm/StringType.h index f2850c33a4..38dea85c60 100644 --- a/js/src/vm/StringType.h +++ b/js/src/vm/StringType.h @@ -407,8 +407,10 @@ class JSString : public js::gc::CellWithLengthAndFlags { static const uint32_t INDEX_VALUE_BIT = js::Bit(11); static const uint32_t INDEX_VALUE_SHIFT = 16; - // NON_DEDUP_BIT is used in string deduplication during tenuring. - static const uint32_t NON_DEDUP_BIT = js::Bit(12); + // NON_DEDUP_BIT is used in string deduplication during tenuring. This bit is + // shared with both FLATTEN_FINISH_NODE and ATOM_IS_PERMANENT_BIT, since it + // only applies to linear non-atoms. + static const uint32_t NON_DEDUP_BIT = js::Bit(15); // If IN_STRING_TO_ATOM_CACHE is set, this string had an entry in the // StringToAtomCache at some point. Note that GC can purge the cache without @@ -627,13 +629,27 @@ class JSString : public js::gc::CellWithLengthAndFlags { } MOZ_ALWAYS_INLINE - void setNonDeduplicatable() { setFlagBit(NON_DEDUP_BIT); } + void setNonDeduplicatable() { + MOZ_ASSERT(isLinear()); + MOZ_ASSERT(!isAtom()); + setFlagBit(NON_DEDUP_BIT); + } + // After copying a string from the nursery to the tenured heap, adjust bits + // that no longer apply. MOZ_ALWAYS_INLINE - void clearNonDeduplicatable() { clearFlagBit(NON_DEDUP_BIT); } + void clearBitsOnTenure() { + MOZ_ASSERT(!isAtom()); + clearFlagBit(NON_DEDUP_BIT | IN_STRING_TO_ATOM_CACHE); + } + // NON_DEDUP_BIT is only valid for linear non-atoms. MOZ_ALWAYS_INLINE - bool isDeduplicatable() { return !(flags() & NON_DEDUP_BIT); } + bool isDeduplicatable() { + MOZ_ASSERT(isLinear()); + MOZ_ASSERT(!isAtom()); + return !(flags() & NON_DEDUP_BIT); + } void setInStringToAtomCache() { MOZ_ASSERT(!isAtom()); @@ -659,6 +675,7 @@ class JSString : public js::gc::CellWithLengthAndFlags { inline bool canOwnDependentChars() const; + // Only called by the GC during nursery collection. inline void setBase(JSLinearString* newBase); void traceBase(JSTracer* trc); @@ -781,6 +798,8 @@ class JSString : public js::gc::CellWithLengthAndFlags { void traceChildren(JSTracer* trc); + inline void traceBaseFromStoreBuffer(JSTracer* trc); + // Override base class implementation to tell GC about permanent atoms. bool isPermanentAndMayBeShared() const { return isPermanentAtom(); } @@ -930,6 +949,7 @@ class JSLinearString : public JSString { friend class JS::AutoStableStringChars; friend class js::gc::TenuringTracer; friend class js::gc::CellAllocator; + friend class JSDependentString; // To allow access when used as base. /* Vacuous and therefore unimplemented. */ JSLinearString* ensureLinear(JSContext* cx) = delete; @@ -1138,6 +1158,13 @@ class JSDependentString : public JSLinearString { setNonInlineChars(chars + offset); } + inline JSLinearString* rootBaseDuringMinorGC(); + + template <typename CharT> + inline void sweepTypedAfterMinorGC(); + + inline void sweepAfterMinorGC(); + #if defined(DEBUG) || defined(JS_JITSPEW) || defined(JS_CACHEIR_SPEW) void dumpOwnRepresentationFields(js::JSONPrinter& json) const; #endif @@ -2260,14 +2287,17 @@ class StringRelocationOverlay : public RelocationOverlay { MOZ_ALWAYS_INLINE const CharT* savedNurseryChars() const; const MOZ_ALWAYS_INLINE JS::Latin1Char* savedNurseryCharsLatin1() const { + MOZ_ASSERT(!forwardingAddress()->as<JSString>()->hasBase()); return nurseryCharsLatin1; } const MOZ_ALWAYS_INLINE char16_t* savedNurseryCharsTwoByte() const { + MOZ_ASSERT(!forwardingAddress()->as<JSString>()->hasBase()); return nurseryCharsTwoByte; } JSLinearString* savedNurseryBaseOrRelocOverlay() const { + MOZ_ASSERT(forwardingAddress()->as<JSString>()->hasBase()); return nurseryBaseOrRelocOverlay; } diff --git a/js/src/vm/StructuredClone.cpp b/js/src/vm/StructuredClone.cpp index e2e67a2ee3..405c18670b 100644 --- a/js/src/vm/StructuredClone.cpp +++ b/js/src/vm/StructuredClone.cpp @@ -893,7 +893,7 @@ bool SCInput::readArray(T* p, size_t nelems) { // To avoid any way in which uninitialized data could escape, zero the array // if filling it failed. std::uninitialized_fill_n(p, nelems, 0); - return false; + return reportTruncated(); } swapFromLittleEndianInPlace(p, nelems); diff --git a/js/src/vm/TypedArrayObject-inl.h b/js/src/vm/TypedArrayObject-inl.h index 95ba7b23d4..ffb9a3c9f6 100644 --- a/js/src/vm/TypedArrayObject-inl.h +++ b/js/src/vm/TypedArrayObject-inl.h @@ -760,6 +760,26 @@ class ElementSpecific { } }; +inline gc::AllocKind js::FixedLengthTypedArrayObject::allocKindForTenure() + const { + // Fixed length typed arrays in the nursery may have a lazily allocated + // buffer. Make sure there is room for the array's fixed data when moving the + // array. + + if (hasBuffer()) { + return NativeObject::allocKindForTenure(); + } + + gc::AllocKind allocKind; + if (hasInlineElements()) { + allocKind = AllocKindForLazyBuffer(byteLength()); + } else { + allocKind = gc::GetGCObjectKind(getClass()); + } + + return gc::ForegroundToBackgroundAllocKind(allocKind); +} + /* static */ gc::AllocKind js::FixedLengthTypedArrayObject::AllocKindForLazyBuffer(size_t nbytes) { MOZ_ASSERT(nbytes <= INLINE_BUFFER_LIMIT); diff --git a/js/src/vm/TypedArrayObject.cpp b/js/src/vm/TypedArrayObject.cpp index 35a2237cd5..935a902abe 100644 --- a/js/src/vm/TypedArrayObject.cpp +++ b/js/src/vm/TypedArrayObject.cpp @@ -7,8 +7,10 @@ #include "vm/TypedArrayObject-inl.h" #include "vm/TypedArrayObject.h" +#include "mozilla/CheckedInt.h" #include "mozilla/FloatingPoint.h" #include "mozilla/IntegerTypeTraits.h" +#include "mozilla/Likely.h" #include "mozilla/PodOperations.h" #include "mozilla/TextUtils.h" @@ -39,6 +41,7 @@ #include "js/UniquePtr.h" #include "js/Wrapper.h" #include "util/DifferentialTesting.h" +#include "util/StringBuffer.h" #include "util/Text.h" #include "util/WindowsWrapper.h" #include "vm/ArrayBufferObject.h" @@ -114,6 +117,13 @@ static bool IsTypedArrayObject(HandleValue v) { return v.isObject() && v.toObject().is<TypedArrayObject>(); } +#ifdef NIGHTLY_BUILD +static bool IsUint8ArrayObject(HandleValue v) { + return IsTypedArrayObject(v) && + v.toObject().as<TypedArrayObject>().type() == Scalar::Uint8; +} +#endif + /* static */ bool TypedArrayObject::ensureHasBuffer(JSContext* cx, Handle<TypedArrayObject*> typedArray) { @@ -199,7 +209,6 @@ size_t FixedLengthTypedArrayObject::objectMoved(JSObject* obj, JSObject* old) { auto* newObj = &obj->as<FixedLengthTypedArrayObject>(); const auto* oldObj = &old->as<FixedLengthTypedArrayObject>(); MOZ_ASSERT(newObj->elementsRaw() == oldObj->elementsRaw()); - MOZ_ASSERT(obj->isTenured()); // Typed arrays with a buffer object do not need an update. if (oldObj->hasBuffer()) { @@ -233,13 +242,13 @@ size_t FixedLengthTypedArrayObject::objectMoved(JSObject* obj, JSObject* old) { constexpr size_t headerSize = dataOffset() + sizeof(HeapSlot); - // See AllocKindForLazyBuffer. - gc::AllocKind newAllocKind = obj->asTenured().getAllocKind(); + gc::AllocKind allocKind = oldObj->allocKindForTenure(); + MOZ_ASSERT_IF(obj->isTenured(), obj->asTenured().getAllocKind() == allocKind); MOZ_ASSERT_IF(nbytes == 0, - headerSize + sizeof(uint8_t) <= GetGCKindBytes(newAllocKind)); + headerSize + sizeof(uint8_t) <= GetGCKindBytes(allocKind)); if (nursery.isInside(buf) && - headerSize + nbytes <= GetGCKindBytes(newAllocKind)) { + headerSize + nbytes <= GetGCKindBytes(allocKind)) { MOZ_ASSERT(oldObj->hasInlineElements()); #ifdef DEBUG if (nbytes == 0) { @@ -2046,6 +2055,1083 @@ bool TypedArrayObject::copyWithin(JSContext* cx, unsigned argc, Value* vp) { TypedArrayObject::copyWithin_impl>(cx, args); } +#ifdef NIGHTLY_BUILD + +// Byte vector with large enough inline storage to allow constructing small +// typed arrays without extra heap allocations. +using ByteVector = + js::Vector<uint8_t, FixedLengthTypedArrayObject::INLINE_BUFFER_LIMIT>; + +static UniqueChars QuoteString(JSContext* cx, char16_t ch) { + Sprinter sprinter(cx); + if (!sprinter.init()) { + return nullptr; + } + + StringEscape esc{}; + js::EscapePrinter ep(sprinter, esc); + ep.putChar(ch); + + return sprinter.release(); +} + +/** + * FromHex ( string [ , maxLength ] ) + * + * https://tc39.es/proposal-arraybuffer-base64/spec/#sec-fromhex + */ +static bool FromHex(JSContext* cx, Handle<JSString*> string, size_t maxLength, + ByteVector& bytes, size_t* readLength) { + // Step 1. (Not applicable in our implementation.) + + // Step 2. + size_t length = string->length(); + + // Step 3. + if (length % 2 != 0) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TYPED_ARRAY_BAD_HEX_STRING_LENGTH); + return false; + } + + JSLinearString* linear = string->ensureLinear(cx); + if (!linear) { + return false; + } + + // Step 4. (Not applicable in our implementation.) + MOZ_ASSERT(bytes.empty()); + + // Step 5. + size_t index = 0; + + // Step 6. + while (index < length && bytes.length() < maxLength) { + // Step 6.a. + char16_t c0 = linear->latin1OrTwoByteChar(index); + char16_t c1 = linear->latin1OrTwoByteChar(index + 1); + + // Step 6.b. + if (MOZ_UNLIKELY(!mozilla::IsAsciiHexDigit(c0) || + !mozilla::IsAsciiHexDigit(c1))) { + char16_t ch = !mozilla::IsAsciiHexDigit(c0) ? c0 : c1; + if (auto str = QuoteString(cx, ch)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TYPED_ARRAY_BAD_HEX_DIGIT, str.get()); + } + return false; + } + + // Step 6.c. + index += 2; + + // Step 6.d. + uint8_t byte = (mozilla::AsciiAlphanumericToNumber(c0) << 4) + + mozilla::AsciiAlphanumericToNumber(c1); + + // Step 6.e. + if (!bytes.append(byte)) { + return false; + } + } + + // Step 7. + *readLength = index; + return true; +} + +namespace Base64 { +static constexpr uint8_t InvalidChar = UINT8_MAX; + +static constexpr auto DecodeTable(const char (&alphabet)[65]) { + std::array<uint8_t, 128> result = {}; + + // Initialize all elements to InvalidChar. + for (auto& e : result) { + e = InvalidChar; + } + + // Map the base64 characters to their values. + for (uint8_t i = 0; i < 64; ++i) { + result[alphabet[i]] = i; + } + + return result; +} +} // namespace Base64 + +namespace Base64::Encode { +static constexpr const char Base64[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +static_assert(std::char_traits<char>::length(Base64) == 64); + +static constexpr const char Base64Url[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; +static_assert(std::char_traits<char>::length(Base64Url) == 64); +} // namespace Base64::Encode + +namespace Base64::Decode { +static constexpr auto Base64 = DecodeTable(Base64::Encode::Base64); +static_assert(Base64.size() == 128, + "128 elements to allow access through ASCII characters"); + +static constexpr auto Base64Url = DecodeTable(Base64::Encode::Base64Url); +static_assert(Base64Url.size() == 128, + "128 elements to allow access through ASCII characters"); +} // namespace Base64::Decode + +enum class Alphabet { + /** + * Standard base64 alphabet. + */ + Base64, + + /** + * URL and filename safe base64 alphabet. + */ + Base64Url, +}; + +enum class LastChunkHandling { + /** + * Allow partial chunks at the end of the input. + */ + Loose, + + /** + * Disallow partial chunks at the end of the input. + */ + Strict, + + /** + * Stop before partial chunks at the end of the input. + */ + StopBeforePartial, +}; + +/** + * FromBase64 ( string, alphabet, lastChunkHandling [ , maxLength ] ) + * + * https://tc39.es/proposal-arraybuffer-base64/spec/#sec-frombase64 + */ +static bool FromBase64(JSContext* cx, Handle<JSString*> string, + Alphabet alphabet, LastChunkHandling lastChunkHandling, + size_t maxLength, ByteVector& bytes, + size_t* readLength) { + // Steps 1-2. (Not applicable in our implementation.) + + // Step 3. + size_t remaining = maxLength; + if (remaining == 0) { + MOZ_ASSERT(bytes.empty()); + *readLength = 0; + return true; + } + + JSLinearString* linear = string->ensureLinear(cx); + if (!linear) { + return false; + } + + // DecodeBase64Chunk ( chunk [ , throwOnExtraBits ] ) + // + // Encode a complete base64 chunk. + auto decodeChunk = [&](uint32_t chunk) { + MOZ_ASSERT(chunk <= 0xffffff); + MOZ_ASSERT(remaining >= 3); + + if (!bytes.reserve(bytes.length() + 3)) { + return false; + } + bytes.infallibleAppend(chunk >> 16); + bytes.infallibleAppend(chunk >> 8); + bytes.infallibleAppend(chunk); + return true; + }; + + // DecodeBase64Chunk ( chunk [ , throwOnExtraBits ] ) + // + // Encode a three element partial base64 chunk. + auto decodeChunk3 = [&](uint32_t chunk, bool throwOnExtraBits) { + MOZ_ASSERT(chunk <= 0x3ffff); + MOZ_ASSERT(remaining >= 2); + + if (throwOnExtraBits && (chunk & 0x3) != 0) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TYPED_ARRAY_EXTRA_BASE64_BITS); + return false; + } + + if (!bytes.reserve(bytes.length() + 2)) { + return false; + } + bytes.infallibleAppend(chunk >> 10); + bytes.infallibleAppend(chunk >> 2); + return true; + }; + + // DecodeBase64Chunk ( chunk [ , throwOnExtraBits ] ) + // + // Encode a two element partial base64 chunk. + auto decodeChunk2 = [&](uint32_t chunk, bool throwOnExtraBits) { + MOZ_ASSERT(chunk <= 0xfff); + MOZ_ASSERT(remaining >= 1); + + if (throwOnExtraBits && (chunk & 0xf) != 0) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TYPED_ARRAY_EXTRA_BASE64_BITS); + return false; + } + + if (!bytes.reserve(bytes.length() + 1)) { + return false; + } + bytes.infallibleAppend(chunk >> 4); + return true; + }; + + // DecodeBase64Chunk ( chunk [ , throwOnExtraBits ] ) + // + // Encode a partial base64 chunk. + auto decodePartialChunk = [&](uint32_t chunk, uint32_t chunkLength, + bool throwOnExtraBits = false) { + MOZ_ASSERT(chunkLength == 2 || chunkLength == 3); + return chunkLength == 2 ? decodeChunk2(chunk, throwOnExtraBits) + : decodeChunk3(chunk, throwOnExtraBits); + }; + + // Step 4. + // + // String index after the last fully read base64 chunk. + size_t read = 0; + + // Step 5. + MOZ_ASSERT(bytes.empty()); + + // Step 6. + // + // Current base64 chunk, a uint24 number. + uint32_t chunk = 0; + + // Step 7. + // + // Current base64 chunk length, in the range [0..4]. + size_t chunkLength = 0; + + // Step 8. + // + // Current string index. + size_t index = 0; + + // Step 9. + size_t length = linear->length(); + + const auto& decode = alphabet == Alphabet::Base64 ? Base64::Decode::Base64 + : Base64::Decode::Base64Url; + + // Step 10. + for (; index < length; index++) { + // Step 10.c. (Reordered) + char16_t ch = linear->latin1OrTwoByteChar(index); + + // Step 10.a. + if (mozilla::IsAsciiWhitespace(ch)) { + continue; + } + + // Step 10.b. (Moved out of loop.) + + // Step 10.d. (Performed in for-loop step.) + + // Step 10.e. + if (ch == '=') { + break; + } + + // Steps 10.f-g. + uint8_t value = Base64::InvalidChar; + if (mozilla::IsAscii(ch)) { + value = decode[ch]; + } + if (MOZ_UNLIKELY(value == Base64::InvalidChar)) { + if (auto str = QuoteString(cx, ch)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TYPED_ARRAY_BAD_BASE64_CHAR, str.get()); + } + return false; + } + + // Step 10.h. (Not applicable in our implementation.) + + // Step 10.i. + if ((remaining == 1 && chunkLength == 2) || + (remaining == 2 && chunkLength == 3)) { + *readLength = read; + return true; + } + + // Step 10.j. + chunk = (chunk << 6) | value; + + // Step 10.k. + chunkLength += 1; + + // Step 10.l. + if (chunkLength == 4) { + // Step 10.l.i. + if (!decodeChunk(chunk)) { + return false; + } + + // Step 10.l.ii. + chunk = 0; + + // Step 10.l.iii. + chunkLength = 0; + + // Step 10.l.iv. + // + // NB: Add +1 to include the |index| update from step 10.d. + read = index + 1; + + // Step 10.l.v. + MOZ_ASSERT(remaining >= 3); + remaining -= 3; + if (remaining == 0) { + *readLength = read; + return true; + } + } + } + + // Step 10.b. + if (index == length) { + // Step 10.b.i. + if (chunkLength > 0) { + // Step 10.b.i.1. + if (lastChunkHandling == LastChunkHandling::StopBeforePartial) { + *readLength = read; + return true; + } + + // Steps 10.b.i.2-3. + if (lastChunkHandling == LastChunkHandling::Loose) { + // Step 10.b.i.2.a. + if (chunkLength == 1) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TYPED_ARRAY_BAD_INCOMPLETE_CHUNK); + return false; + } + MOZ_ASSERT(chunkLength == 2 || chunkLength == 3); + + // Step 10.b.i.2.b. + if (!decodePartialChunk(chunk, chunkLength)) { + return false; + } + } else { + // Step 10.b.i.3.a. + MOZ_ASSERT(lastChunkHandling == LastChunkHandling::Strict); + + // Step 10.b.i.3.b. + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TYPED_ARRAY_BAD_INCOMPLETE_CHUNK); + return false; + } + } + + // Step 10.b.ii. + *readLength = length; + return true; + } + + // Step 10.e. + MOZ_ASSERT(index < length); + MOZ_ASSERT(linear->latin1OrTwoByteChar(index) == '='); + + // Step 10.e.i. + if (chunkLength < 2) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TYPED_ARRAY_BAD_INCOMPLETE_CHUNK); + return false; + } + MOZ_ASSERT(chunkLength == 2 || chunkLength == 3); + + // Step 10.e.ii. (Inlined SkipAsciiWhitespace) + while (++index < length) { + char16_t ch = linear->latin1OrTwoByteChar(index); + if (!mozilla::IsAsciiWhitespace(ch)) { + break; + } + } + + // Step 10.e.iii. + if (chunkLength == 2) { + // Step 10.e.iii.1. + if (index == length) { + // Step 10.e.iii.1.a. + if (lastChunkHandling == LastChunkHandling::StopBeforePartial) { + *readLength = read; + return true; + } + + // Step 10.e.iii.1.b. + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TYPED_ARRAY_MISSING_BASE64_PADDING); + return false; + } + + // Step 10.e.iii.2. + char16_t ch = linear->latin1OrTwoByteChar(index); + + // Step 10.e.iii.3. + if (ch == '=') { + // Step 10.e.iii.3.a. (Inlined SkipAsciiWhitespace) + while (++index < length) { + char16_t ch = linear->latin1OrTwoByteChar(index); + if (!mozilla::IsAsciiWhitespace(ch)) { + break; + } + } + } + } + + // Step 10.e.iv. + if (index < length) { + char16_t ch = linear->latin1OrTwoByteChar(index); + if (auto str = QuoteString(cx, ch)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TYPED_ARRAY_BAD_BASE64_AFTER_PADDING, + str.get()); + } + return false; + } + + // Steps 10.e.v-vi. + bool throwOnExtraBits = lastChunkHandling == LastChunkHandling::Strict; + + // Step 10.e.vii. + if (!decodePartialChunk(chunk, chunkLength, throwOnExtraBits)) { + return false; + } + + // Step 10.e.viii. + *readLength = length; + return true; +} + +/** + * Uint8Array.fromBase64 ( string [ , options ] ) + * Uint8Array.prototype.setFromBase64 ( string [ , options ] ) + * Uint8Array.prototype.toBase64 ( [ options ] ) + * + * Helper to retrieve the "alphabet" option. + */ +static bool GetAlphabetOption(JSContext* cx, Handle<JSObject*> options, + Alphabet* result) { + Rooted<Value> value(cx); + if (!GetProperty(cx, options, options, cx->names().alphabet, &value)) { + return false; + } + + if (value.isUndefined()) { + *result = Alphabet::Base64; + return true; + } + + if (!value.isString()) { + return ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, + value, nullptr, "not a string"); + } + + auto* linear = value.toString()->ensureLinear(cx); + if (!linear) { + return false; + } + + if (StringEqualsAscii(linear, "base64")) { + *result = Alphabet::Base64; + return true; + } + + if (StringEqualsAscii(linear, "base64url")) { + *result = Alphabet::Base64Url; + return true; + } + + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TYPED_ARRAY_BAD_BASE64_ALPHABET); + return false; +} + +/** + * Uint8Array.fromBase64 ( string [ , options ] ) + * Uint8Array.prototype.setFromBase64 ( string [ , options ] ) + * + * Helper to retrieve the "lastChunkHandling" option. + */ +static bool GetLastChunkHandlingOption(JSContext* cx, Handle<JSObject*> options, + LastChunkHandling* result) { + Rooted<Value> value(cx); + if (!GetProperty(cx, options, options, cx->names().lastChunkHandling, + &value)) { + return false; + } + + if (value.isUndefined()) { + *result = LastChunkHandling::Loose; + return true; + } + + if (!value.isString()) { + return ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, + value, nullptr, "not a string"); + } + + auto* linear = value.toString()->ensureLinear(cx); + if (!linear) { + return false; + } + + if (StringEqualsAscii(linear, "loose")) { + *result = LastChunkHandling::Loose; + return true; + } + + if (StringEqualsAscii(linear, "strict")) { + *result = LastChunkHandling::Strict; + return true; + } + + if (StringEqualsAscii(linear, "stop-before-partial")) { + *result = LastChunkHandling::StopBeforePartial; + return true; + } + + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TYPED_ARRAY_BAD_BASE64_LAST_CHUNK_HANDLING); + return false; +} + +/** + * Uint8Array.fromBase64 ( string [ , options ] ) + * + * https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.frombase64 + */ +static bool uint8array_fromBase64(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (!args.get(0).isString()) { + return ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK, + args.get(0), nullptr, "not a string"); + } + Rooted<JSString*> string(cx, args[0].toString()); + + // Steps 2-9. + auto alphabet = Alphabet::Base64; + auto lastChunkHandling = LastChunkHandling::Loose; + if (args.hasDefined(1)) { + // Step 2. (Inlined GetOptionsObject) + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "options", "fromBase64", args[1])); + if (!options) { + return false; + } + + // Steps 3-6. + if (!GetAlphabetOption(cx, options, &alphabet)) { + return false; + } + + // Steps 7-9. + if (!GetLastChunkHandlingOption(cx, options, &lastChunkHandling)) { + return false; + } + } + + // Step 10. + constexpr size_t maxLength = std::numeric_limits<size_t>::max(); + ByteVector bytes(cx); + size_t unusedReadLength; + if (!FromBase64(cx, string, alphabet, lastChunkHandling, maxLength, bytes, + &unusedReadLength)) { + return false; + } + + // Step 11. + size_t resultLength = bytes.length(); + + // Step 12. + auto* tarray = + TypedArrayObjectTemplate<uint8_t>::fromLength(cx, resultLength); + if (!tarray) { + return false; + } + + // Step 13. + auto target = SharedMem<uint8_t*>::unshared(tarray->dataPointerUnshared()); + auto source = SharedMem<uint8_t*>::unshared(bytes.begin()); + UnsharedOps::podCopy(target, source, resultLength); + + // Step 14. + args.rval().setObject(*tarray); + return true; +} + +/** + * Uint8Array.fromHex ( string ) + * + * https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.fromhex + */ +static bool uint8array_fromHex(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (!args.get(0).isString()) { + return ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK, + args.get(0), nullptr, "not a string"); + } + Rooted<JSString*> string(cx, args[0].toString()); + + // Step 2. + constexpr size_t maxLength = std::numeric_limits<size_t>::max(); + ByteVector bytes(cx); + size_t unusedReadLength; + if (!FromHex(cx, string, maxLength, bytes, &unusedReadLength)) { + return false; + } + + // Step 3. + size_t resultLength = bytes.length(); + + // Step 4. + auto* tarray = + TypedArrayObjectTemplate<uint8_t>::fromLength(cx, resultLength); + if (!tarray) { + return false; + } + + // Step 5. + auto target = SharedMem<uint8_t*>::unshared(tarray->dataPointerUnshared()); + auto source = SharedMem<uint8_t*>::unshared(bytes.begin()); + UnsharedOps::podCopy(target, source, resultLength); + + // Step 6. + args.rval().setObject(*tarray); + return true; +} + +/** + * Uint8Array.prototype.setFromBase64 ( string [ , options ] ) + * + * https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.setfrombase64 + */ +static bool uint8array_setFromBase64(JSContext* cx, const CallArgs& args) { + Rooted<TypedArrayObject*> tarray( + cx, &args.thisv().toObject().as<TypedArrayObject>()); + + // Step 3. + if (!args.get(0).isString()) { + return ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK, + args.get(0), nullptr, "not a string"); + } + Rooted<JSString*> string(cx, args[0].toString()); + + // Steps 4-11. + auto alphabet = Alphabet::Base64; + auto lastChunkHandling = LastChunkHandling::Loose; + if (args.hasDefined(1)) { + // Step 2. (Inlined GetOptionsObject) + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "options", "setFromBase64", args[1])); + if (!options) { + return false; + } + + // Steps 3-6. + if (!GetAlphabetOption(cx, options, &alphabet)) { + return false; + } + + // Steps 7-9. + if (!GetLastChunkHandlingOption(cx, options, &lastChunkHandling)) { + return false; + } + } + + // Steps 12-14. + auto length = tarray->length(); + if (!length) { + ReportOutOfBounds(cx, tarray); + return false; + } + + // Step 15. + size_t maxLength = *length; + + // Steps 16-17. + ByteVector bytes(cx); + size_t readLength; + if (!FromBase64(cx, string, alphabet, lastChunkHandling, maxLength, bytes, + &readLength)) { + return false; + } + + // Step 18. + size_t written = bytes.length(); + + // Step 19. + // + // The underlying buffer has neither been detached nor shrunk. (It may have + // been grown when it's a growable shared buffer and a concurrent thread + // resized the buffer.) + MOZ_ASSERT(!tarray->hasDetachedBuffer()); + MOZ_ASSERT(tarray->length().valueOr(0) >= *length); + + // Step 20. + MOZ_ASSERT(written <= *length); + + // Step 21. (Inlined SetUint8ArrayBytes) + auto target = tarray->dataPointerEither().cast<uint8_t*>(); + auto source = SharedMem<uint8_t*>::unshared(bytes.begin()); + if (tarray->isSharedMemory()) { + SharedOps::podCopy(target, source, written); + } else { + UnsharedOps::podCopy(target, source, written); + } + + // Step 22. + Rooted<PlainObject*> result(cx, NewPlainObject(cx)); + if (!result) { + return false; + } + + // Step 23. + Rooted<Value> readValue(cx, NumberValue(readLength)); + if (!DefineDataProperty(cx, result, cx->names().read, readValue)) { + return false; + } + + // Step 24. + Rooted<Value> writtenValue(cx, NumberValue(written)); + if (!DefineDataProperty(cx, result, cx->names().written, writtenValue)) { + return false; + } + + // Step 25. + args.rval().setObject(*result); + return true; +} + +/** + * Uint8Array.prototype.setFromBase64 ( string [ , options ] ) + * + * https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.setfrombase64 + */ +static bool uint8array_setFromBase64(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Steps 1-2. + return CallNonGenericMethod<IsUint8ArrayObject, uint8array_setFromBase64>( + cx, args); +} + +/** + * Uint8Array.prototype.setFromHex ( string ) + * + * https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.setfromhex + */ +static bool uint8array_setFromHex(JSContext* cx, const CallArgs& args) { + Rooted<TypedArrayObject*> tarray( + cx, &args.thisv().toObject().as<TypedArrayObject>()); + + // Step 3. + if (!args.get(0).isString()) { + return ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK, + args.get(0), nullptr, "not a string"); + } + Rooted<JSString*> string(cx, args[0].toString()); + + // Steps 4-6. + auto length = tarray->length(); + if (!length) { + ReportOutOfBounds(cx, tarray); + return false; + } + + // Step 7. + size_t maxLength = *length; + + // Steps 8-9. + ByteVector bytes(cx); + size_t readLength; + if (!FromHex(cx, string, maxLength, bytes, &readLength)) { + return false; + } + + // Step 10. + size_t written = bytes.length(); + + // Step 11. + // + // The underlying buffer has neither been detached nor shrunk. (It may have + // been grown when it's a growable shared buffer and a concurrent thread + // resized the buffer.) + MOZ_ASSERT(!tarray->hasDetachedBuffer()); + MOZ_ASSERT(tarray->length().valueOr(0) >= *length); + + // Step 12. + MOZ_ASSERT(written <= *length); + + // Step 13. (Inlined SetUint8ArrayBytes) + auto target = tarray->dataPointerEither().cast<uint8_t*>(); + auto source = SharedMem<uint8_t*>::unshared(bytes.begin()); + if (tarray->isSharedMemory()) { + SharedOps::podCopy(target, source, written); + } else { + UnsharedOps::podCopy(target, source, written); + } + + // Step 14. + Rooted<PlainObject*> result(cx, NewPlainObject(cx)); + if (!result) { + return false; + } + + // Step 15. + Rooted<Value> readValue(cx, NumberValue(readLength)); + if (!DefineDataProperty(cx, result, cx->names().read, readValue)) { + return false; + } + + // Step 16. + Rooted<Value> writtenValue(cx, NumberValue(written)); + if (!DefineDataProperty(cx, result, cx->names().written, writtenValue)) { + return false; + } + + // Step 17. + args.rval().setObject(*result); + return true; +} + +/** + * Uint8Array.prototype.setFromHex ( string ) + * + * https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.setfromhex + */ +static bool uint8array_setFromHex(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Steps 1-2. + return CallNonGenericMethod<IsUint8ArrayObject, uint8array_setFromHex>(cx, + args); +} + +/** + * Uint8Array.prototype.toBase64 ( [ options ] ) + * + * https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.tobase64 + */ +static bool uint8array_toBase64(JSContext* cx, const CallArgs& args) { + Rooted<TypedArrayObject*> tarray( + cx, &args.thisv().toObject().as<TypedArrayObject>()); + + // Steps 3-7. + auto alphabet = Alphabet::Base64; + if (args.hasDefined(0)) { + // Step 3. (Inlined GetOptionsObject) + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "options", "toBase64", args[0])); + if (!options) { + return false; + } + + // Steps 4-7. + if (!GetAlphabetOption(cx, options, &alphabet)) { + return false; + } + } + + // Step 8. (Partial) + auto length = tarray->length(); + if (!length) { + ReportOutOfBounds(cx, tarray); + return false; + } + + // Compute the output string length. Three input bytes are encoded as four + // characters, so the output length is ⌈length × 4/3⌉. + auto outLength = mozilla::CheckedInt<size_t>{*length}; + outLength += 2; + outLength /= 3; + outLength *= 4; + if (!outLength.isValid() || outLength.value() > JSString::MAX_LENGTH) { + ReportAllocationOverflow(cx); + return false; + } + + JSStringBuilder sb(cx); + if (!sb.reserve(outLength.value())) { + return false; + } + + // Steps 9-10. + const auto& base64Chars = alphabet == Alphabet::Base64 + ? Base64::Encode::Base64 + : Base64::Encode::Base64Url; + + auto encode = [&base64Chars](uint32_t value) { + return base64Chars[value & 0x3f]; + }; + + // Our implementation directly converts the bytes to their string + // representation instead of first collecting them into an intermediate list. + auto data = tarray->dataPointerEither().cast<uint8_t*>(); + auto toRead = *length; + for (; toRead >= 3; toRead -= 3) { + // Combine three input bytes into a single uint24 value. + auto byte0 = jit::AtomicOperations::loadSafeWhenRacy(data++); + auto byte1 = jit::AtomicOperations::loadSafeWhenRacy(data++); + auto byte2 = jit::AtomicOperations::loadSafeWhenRacy(data++); + auto u24 = (uint32_t(byte0) << 16) | (uint32_t(byte1) << 8) | byte2; + + // Encode the uint24 value as base64. + sb.infallibleAppend(encode(u24 >> 18)); + sb.infallibleAppend(encode(u24 >> 12)); + sb.infallibleAppend(encode(u24 >> 6)); + sb.infallibleAppend(encode(u24 >> 0)); + } + + // Trailing two and one element bytes are padded with '='. + if (toRead == 2) { + // Combine two input bytes into a single uint24 value. + auto byte0 = jit::AtomicOperations::loadSafeWhenRacy(data++); + auto byte1 = jit::AtomicOperations::loadSafeWhenRacy(data++); + auto u24 = (uint32_t(byte0) << 16) | (uint32_t(byte1) << 8); + + // Encode the uint24 value as base64, including padding. + sb.infallibleAppend(encode(u24 >> 18)); + sb.infallibleAppend(encode(u24 >> 12)); + sb.infallibleAppend(encode(u24 >> 6)); + sb.infallibleAppend('='); + } else if (toRead == 1) { + // Combine one input byte into a single uint24 value. + auto byte0 = jit::AtomicOperations::loadSafeWhenRacy(data++); + auto u24 = uint32_t(byte0) << 16; + + // Encode the uint24 value as base64, including padding. + sb.infallibleAppend(encode(u24 >> 18)); + sb.infallibleAppend(encode(u24 >> 12)); + sb.infallibleAppend('='); + sb.infallibleAppend('='); + } else { + MOZ_ASSERT(toRead == 0); + } + + MOZ_ASSERT(sb.length() == outLength.value(), "all characters were written"); + + // Step 11. + auto* str = sb.finishString(); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Uint8Array.prototype.toBase64 ( [ options ] ) + * + * https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.tobase64 + */ +static bool uint8array_toBase64(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Steps 1-2. + return CallNonGenericMethod<IsUint8ArrayObject, uint8array_toBase64>(cx, + args); +} + +/** + * Uint8Array.prototype.toHex ( ) + * + * https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.tohex + */ +static bool uint8array_toHex(JSContext* cx, const CallArgs& args) { + Rooted<TypedArrayObject*> tarray( + cx, &args.thisv().toObject().as<TypedArrayObject>()); + + // Step 3. (Partial) + auto length = tarray->length(); + if (!length) { + ReportOutOfBounds(cx, tarray); + return false; + } + + // |length| is limited by |ByteLengthLimit|, which ensures that multiplying it + // by two won't overflow. + static_assert(TypedArrayObject::ByteLengthLimit <= + std::numeric_limits<size_t>::max() / 2); + MOZ_ASSERT(*length <= TypedArrayObject::ByteLengthLimit); + + // Compute the output string length. Each byte is encoded as two characters, + // so the output length is exactly twice as large as |length|. + size_t outLength = *length * 2; + if (outLength > JSString::MAX_LENGTH) { + ReportAllocationOverflow(cx); + return false; + } + + // Step 4. + JSStringBuilder sb(cx); + if (!sb.reserve(outLength)) { + return false; + } + + // NB: Lower case hex digits. + static constexpr char HexDigits[] = "0123456789abcdef"; + static_assert(std::char_traits<char>::length(HexDigits) == 16); + + // Steps 3 and 5. + // + // Our implementation directly converts the bytes to their string + // representation instead of first collecting them into an intermediate list. + auto data = tarray->dataPointerEither().cast<uint8_t*>(); + for (size_t index = 0; index < *length; index++) { + auto byte = jit::AtomicOperations::loadSafeWhenRacy(data + index); + + sb.infallibleAppend(HexDigits[byte >> 4]); + sb.infallibleAppend(HexDigits[byte & 0xf]); + } + + MOZ_ASSERT(sb.length() == outLength, "all characters were written"); + + // Step 6. + auto* str = sb.finishString(); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Uint8Array.prototype.toHex ( ) + * + * https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.tohex + */ +static bool uint8array_toHex(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Steps 1-2. + return CallNonGenericMethod<IsUint8ArrayObject, uint8array_toHex>(cx, args); +} + +#endif + /* static */ const JSFunctionSpec TypedArrayObject::protoFunctions[] = { JS_SELF_HOSTED_FN("subarray", "TypedArraySubarray", 2, 0), JS_FN("set", TypedArrayObject::set, 1, 0), @@ -2364,15 +3450,50 @@ static const JSPropertySpec #undef IMPL_TYPED_ARRAY_PROPERTIES }; +#ifdef NIGHTLY_BUILD +static const JSFunctionSpec uint8array_static_methods[] = { + JS_FN("fromBase64", uint8array_fromBase64, 1, 0), + JS_FN("fromHex", uint8array_fromHex, 1, 0), + JS_FS_END, +}; + +static const JSFunctionSpec uint8array_methods[] = { + JS_FN("setFromBase64", uint8array_setFromBase64, 1, 0), + JS_FN("setFromHex", uint8array_setFromHex, 1, 0), + JS_FN("toBase64", uint8array_toBase64, 0, 0), + JS_FN("toHex", uint8array_toHex, 0, 0), + JS_FS_END, +}; +#endif + +static constexpr const JSFunctionSpec* TypedArrayStaticMethods( + Scalar::Type type) { +#ifdef NIGHTLY_BUILD + if (type == Scalar::Uint8) { + return uint8array_static_methods; + } +#endif + return nullptr; +} + +static constexpr const JSFunctionSpec* TypedArrayMethods(Scalar::Type type) { +#ifdef NIGHTLY_BUILD + if (type == Scalar::Uint8) { + return uint8array_methods; + } +#endif + return nullptr; +} + static const ClassSpec TypedArrayObjectClassSpecs[Scalar::MaxTypedArrayViewType] = { #define IMPL_TYPED_ARRAY_CLASS_SPEC(ExternalType, NativeType, Name) \ { \ TypedArrayObjectTemplate<NativeType>::createConstructor, \ TypedArrayObjectTemplate<NativeType>::createPrototype, \ - nullptr, \ + TypedArrayStaticMethods(Scalar::Type::Name), \ static_prototype_properties[Scalar::Type::Name], \ - nullptr, \ + TypedArrayMethods(Scalar::Type::Name), \ static_prototype_properties[Scalar::Type::Name], \ nullptr, \ JSProto_TypedArray, \ diff --git a/js/src/vm/TypedArrayObject.h b/js/src/vm/TypedArrayObject.h index 46531ec4ee..6905e83600 100644 --- a/js/src/vm/TypedArrayObject.h +++ b/js/src/vm/TypedArrayObject.h @@ -149,6 +149,7 @@ class FixedLengthTypedArrayObject : public TypedArrayObject { static constexpr uint32_t INLINE_BUFFER_LIMIT = (NativeObject::MAX_FIXED_SLOTS - FIXED_DATA_START) * sizeof(Value); + inline gc::AllocKind allocKindForTenure() const; static inline gc::AllocKind AllocKindForLazyBuffer(size_t nbytes); size_t byteOffset() const { diff --git a/js/src/wasm/AsmJS.cpp b/js/src/wasm/AsmJS.cpp index 11a0c2b23d..1e79880360 100644 --- a/js/src/wasm/AsmJS.cpp +++ b/js/src/wasm/AsmJS.cpp @@ -6914,8 +6914,8 @@ static bool GetImports(JSContext* cx, const AsmJSMetadata& metadata, return true; } -static bool TryInstantiate(JSContext* cx, CallArgs args, const Module& module, - const AsmJSMetadata& metadata, +static bool TryInstantiate(JSContext* cx, const CallArgs& args, + const Module& module, const AsmJSMetadata& metadata, MutableHandle<WasmInstanceObject*> instanceObj, MutableHandleObject exportObj) { HandleValue globalVal = args.get(0); @@ -6956,7 +6956,7 @@ static bool TryInstantiate(JSContext* cx, CallArgs args, const Module& module, return true; } -static bool HandleInstantiationFailure(JSContext* cx, CallArgs args, +static bool HandleInstantiationFailure(JSContext* cx, const CallArgs& args, const AsmJSMetadata& metadata) { using js::frontend::FunctionSyntaxKind; diff --git a/js/src/wasm/WasmAnyRef.h b/js/src/wasm/WasmAnyRef.h index 1675a9fa8d..cbd3fe75e0 100644 --- a/js/src/wasm/WasmAnyRef.h +++ b/js/src/wasm/WasmAnyRef.h @@ -208,16 +208,19 @@ class AnyRef { // losslessly represent all i31 values. static AnyRef fromUint32Truncate(uint32_t value) { // See 64-bit GPRs carrying 32-bit values invariants in MacroAssember.h -#if defined(JS_CODEGEN_X64) || defined(JS_CODEGEN_ARM64) +#if defined(JS_CODEGEN_NONE) || defined(JS_CODEGEN_X64) || \ + defined(JS_CODEGEN_ARM64) // Truncate the value to the 31-bit value size. uintptr_t wideValue = uintptr_t(value & 0x7FFFFFFF); #elif defined(JS_CODEGEN_LOONG64) || defined(JS_CODEGEN_MIPS64) || \ defined(JS_CODEGEN_RISCV64) // Sign extend the value to the native pointer size. uintptr_t wideValue = uintptr_t(int64_t((uint64_t(value) << 33)) >> 33); -#else +#elif !defined(JS_64BIT) // Transfer 32-bit value as is. uintptr_t wideValue = (uintptr_t)value; +#else +# error "unknown architecture" #endif // Left shift the value by 1, truncating the high bit. diff --git a/js/src/wasm/WasmBCClass.h b/js/src/wasm/WasmBCClass.h index 844ae3381a..52a73d195c 100644 --- a/js/src/wasm/WasmBCClass.h +++ b/js/src/wasm/WasmBCClass.h @@ -1402,9 +1402,6 @@ struct BaseCompiler final { // Used for common setup for catch and catch_all. void emitCatchSetup(LabelKind kind, Control& tryCatch, const ResultType& resultType); - // Helper function used to generate landing pad code for the special - // case in which `delegate` jumps to a function's body block. - [[nodiscard]] bool emitBodyDelegateThrowPad(); [[nodiscard]] bool emitTry(); [[nodiscard]] bool emitTryTable(); diff --git a/js/src/wasm/WasmBaselineCompile.cpp b/js/src/wasm/WasmBaselineCompile.cpp index cb0fbde6ec..dbd5bf11d4 100644 --- a/js/src/wasm/WasmBaselineCompile.cpp +++ b/js/src/wasm/WasmBaselineCompile.cpp @@ -3780,12 +3780,6 @@ bool BaseCompiler::emitEnd() { return false; } doReturn(ContinuationKind::Fallthrough); - // This is emitted here after `doReturn` to avoid being executed in the - // normal return path of a function, and instead only when a `delegate` - // jumps to it. - if (!emitBodyDelegateThrowPad()) { - return false; - } iter_.popEnd(); MOZ_ASSERT(iter_.controlStackEmpty()); return iter_.endFunction(iter_.end()); @@ -4108,9 +4102,6 @@ bool BaseCompiler::emitTryTable() { Label skipLandingPad; masm.jump(&skipLandingPad); - // Bind the otherLabel so that delegate can target this - masm.bind(&controlItem().otherLabel); - StackHeight prePadHeight = fr.stackHeight(); uint32_t padOffset = masm.currentOffset(); uint32_t padStackHeight = masm.framePushed(); @@ -4496,30 +4487,6 @@ bool BaseCompiler::emitCatchAll() { return pushBlockResults(exnResult); } -bool BaseCompiler::emitBodyDelegateThrowPad() { - Control& block = controlItem(); - - // Only emit a landing pad if a `delegate` has generated a jump to here. - if (block.otherLabel.used()) { - StackHeight savedHeight = fr.stackHeight(); - fr.setStackHeight(block.stackHeight); - masm.bind(&block.otherLabel); - - // A try-delegate jumps immediately to its delegated try block, so we are - // responsible to unpack the exception and rethrow it. - RegRef exn; - RegRef tag; - consumePendingException(RegPtr(InstanceReg), &exn, &tag); - freeRef(tag); - if (!throwFrom(exn)) { - return false; - } - fr.setStackHeight(savedHeight); - } - - return true; -} - bool BaseCompiler::emitDelegate() { uint32_t relativeDepth; ResultType resultType; @@ -4529,47 +4496,17 @@ bool BaseCompiler::emitDelegate() { return false; } - Control& tryDelegate = controlItem(); - - // End the try branch like a plain catch block without exception ref handling. - if (deadCode_) { - fr.resetStackHeight(tryDelegate.stackHeight, resultType); - popValueStackTo(tryDelegate.stackSize); - } else { - MOZ_ASSERT(stk_.length() == tryDelegate.stackSize + resultType.length()); - popBlockResults(resultType, tryDelegate.stackHeight, - ContinuationKind::Jump); - freeResultRegisters(resultType); - masm.jump(&tryDelegate.label); - MOZ_ASSERT(!tryDelegate.deadOnArrival); + if (!endBlock(resultType)) { + return false; } - deadCode_ = tryDelegate.deadOnArrival; - - if (deadCode_) { + if (controlItem().deadOnArrival) { return true; } - // Create an exception landing pad that immediately branches to the landing - // pad of the delegated try block. - masm.bind(&tryDelegate.otherLabel); - - StackHeight savedHeight = fr.stackHeight(); - fr.setStackHeight(tryDelegate.stackHeight); - // Mark the end of the try body. This may insert a nop. finishTryNote(controlItem().tryNoteIndex); - // The landing pad begins at this point - TryNoteVector& tryNotes = masm.tryNotes(); - TryNote& tryNote = tryNotes[controlItem().tryNoteIndex]; - tryNote.setLandingPad(masm.currentOffset(), masm.framePushed()); - - // Store the Instance that was left in InstanceReg by the exception - // handling mechanism, that is this frame's Instance but with the exception - // filled in Instance::pendingException. - fr.storeInstancePtr(InstanceReg); - // If the target block is a non-try block, skip over it and find the next // try block or the very last block (to re-throw out of the function). Control& lastBlock = controlOutermost(); @@ -4579,22 +4516,24 @@ bool BaseCompiler::emitDelegate() { relativeDepth++; } Control& target = controlItem(relativeDepth); - - popBlockResults(ResultType::Empty(), target.stackHeight, - ContinuationKind::Jump); - masm.jump(&target.otherLabel); - - fr.setStackHeight(savedHeight); - - // Where the try branch jumps to, if it's not dead. - if (tryDelegate.label.used()) { - masm.bind(&tryDelegate.label); + TryNoteVector& tryNotes = masm.tryNotes(); + TryNote& delegateTryNote = tryNotes[controlItem().tryNoteIndex]; + + if (&target == &lastBlock) { + // A delegate targeting the function body block means that any exception + // in this try needs to be propagated to the caller function. We use the + // delegate code offset of `0` as that will be in the prologue and cannot + // have a try note. + delegateTryNote.setDelegate(0); + } else { + // Delegate to one byte inside the beginning of the target try note, as + // that's when matches hit. Try notes are guaranteed to not be empty either + // and so this will not miss either. + const TryNote& targetTryNote = tryNotes[target.tryNoteIndex]; + delegateTryNote.setDelegate(targetTryNote.tryBodyBegin() + 1); } - captureResultRegisters(resultType); - bceSafe_ = tryDelegate.bceSafeOnExit; - - return pushBlockResults(resultType); + return true; } bool BaseCompiler::endTryCatch(ResultType type) { @@ -4634,7 +4573,6 @@ bool BaseCompiler::endTryCatch(ResultType type) { // Create landing pad for all catch handlers in this block. // When used for a catchless try block, this will generate a landing pad // with no handlers and only the fall-back rethrow. - masm.bind(&tryCatch.otherLabel); // The stack height also needs to be set not for a block result, but for the // entry to the exception handlers. This is reset again below for the join. diff --git a/js/src/wasm/WasmBuiltins.cpp b/js/src/wasm/WasmBuiltins.cpp index 7b03494bcd..f0773583ac 100644 --- a/js/src/wasm/WasmBuiltins.cpp +++ b/js/src/wasm/WasmBuiltins.cpp @@ -628,6 +628,22 @@ static WasmExceptionObject* GetOrWrapWasmException(JitActivation* activation, return nullptr; } +static const wasm::TryNote* FindNonDelegateTryNote(const wasm::Code& code, + const uint8_t* pc, + Tier* tier) { + const wasm::TryNote* tryNote = code.lookupTryNote((void*)pc, tier); + while (tryNote && tryNote->isDelegate()) { + const wasm::CodeTier& codeTier = code.codeTier(*tier); + pc = codeTier.segment().base() + tryNote->delegateOffset(); + const wasm::TryNote* delegateTryNote = code.lookupTryNote((void*)pc, tier); + MOZ_RELEASE_ASSERT(delegateTryNote == nullptr || + delegateTryNote->tryBodyBegin() < + tryNote->tryBodyBegin()); + tryNote = delegateTryNote; + } + return tryNote; +} + // Unwind the entire activation in response to a thrown exception. This function // is responsible for notifying the debugger of each unwound frame. The return // value is the new stack address which the calling stub will set to the sp @@ -674,10 +690,10 @@ bool wasm::HandleThrow(JSContext* cx, WasmFrameIter& iter, // Only look for an exception handler if there's a catchable exception. if (wasmExn) { + Tier tier; const wasm::Code& code = iter.instance()->code(); const uint8_t* pc = iter.resumePCinCurrentFrame(); - Tier tier; - const wasm::TryNote* tryNote = code.lookupTryNote((void*)pc, &tier); + const wasm::TryNote* tryNote = FindNonDelegateTryNote(code, pc, &tier); if (tryNote) { #ifdef ENABLE_WASM_TAIL_CALLS @@ -751,8 +767,8 @@ bool wasm::HandleThrow(JSContext* cx, WasmFrameIter& iter, // Assert that any pending exception escaping to non-wasm code is not a // wrapper exception object #ifdef DEBUG - Rooted<Value> pendingException(cx); - if (cx->isExceptionPending() && cx->getPendingException(&pendingException)) { + if (cx->isExceptionPending()) { + Rooted<Value> pendingException(cx, cx->getPendingExceptionUnwrapped()); MOZ_ASSERT_IF(pendingException.isObject() && pendingException.toObject().is<WasmExceptionObject>(), !pendingException.toObject() diff --git a/js/src/wasm/WasmCodegenTypes.h b/js/src/wasm/WasmCodegenTypes.h index 590572ae8a..7e54ad15f7 100644 --- a/js/src/wasm/WasmCodegenTypes.h +++ b/js/src/wasm/WasmCodegenTypes.h @@ -671,20 +671,30 @@ struct TryNote { // Sentinel value to detect a try note that has not been given a try body. static const uint32_t BEGIN_NONE = UINT32_MAX; + // Sentinel value used in `entryPointOrIsDelegate_`. + static const uint32_t IS_DELEGATE = UINT32_MAX; + // Begin code offset of the try body. uint32_t begin_; // Exclusive end code offset of the try body. uint32_t end_; - // The code offset of the landing pad. - uint32_t entryPoint_; - // Track offset from frame of stack pointer. - uint32_t framePushed_; + // Either a marker that this is a 'delegate' or else the code offset of the + // landing pad to jump to. + uint32_t entryPointOrIsDelegate_; + // If this is a delegate, then this is the code offset to delegate to, + // otherwise this is the offset from the frame pointer of the stack pointer + // to use when jumping to the landing pad. + uint32_t framePushedOrDelegateOffset_; - WASM_CHECK_CACHEABLE_POD(begin_, end_, entryPoint_, framePushed_); + WASM_CHECK_CACHEABLE_POD(begin_, end_, entryPointOrIsDelegate_, + framePushedOrDelegateOffset_); public: explicit TryNote() - : begin_(BEGIN_NONE), end_(0), entryPoint_(0), framePushed_(0) {} + : begin_(BEGIN_NONE), + end_(0), + entryPointOrIsDelegate_(0), + framePushedOrDelegateOffset_(0) {} // Returns whether a try note has been assigned a range for the try body. bool hasTryBody() const { return begin_ != BEGIN_NONE; } @@ -700,11 +710,27 @@ struct TryNote { return offset > begin_ && offset <= end_; } + // Check if the unwinder should delegate the handling of this try note to the + // try note given at the delegate offset. + bool isDelegate() const { return entryPointOrIsDelegate_ == IS_DELEGATE; } + + // The code offset to delegate the handling of this try note to. + uint32_t delegateOffset() const { + MOZ_ASSERT(isDelegate()); + return framePushedOrDelegateOffset_; + } + // The code offset of the entry to the landing pad. - uint32_t landingPadEntryPoint() const { return entryPoint_; } + uint32_t landingPadEntryPoint() const { + MOZ_ASSERT(!isDelegate()); + return entryPointOrIsDelegate_; + } // The stack frame pushed amount at the entry to the landing pad. - uint32_t landingPadFramePushed() const { return framePushed_; } + uint32_t landingPadFramePushed() const { + MOZ_ASSERT(!isDelegate()); + return framePushedOrDelegateOffset_; + } // Set the beginning of the try body. void setTryBodyBegin(uint32_t begin) { @@ -722,17 +748,29 @@ struct TryNote { MOZ_ASSERT(end_ > begin_); } + // Mark this try note as a delegate, requesting the unwinder to use the try + // note found at the delegate offset. + void setDelegate(uint32_t delegateOffset) { + entryPointOrIsDelegate_ = IS_DELEGATE; + framePushedOrDelegateOffset_ = delegateOffset; + } + // Set the entry point and frame pushed of the landing pad. void setLandingPad(uint32_t entryPoint, uint32_t framePushed) { - entryPoint_ = entryPoint; - framePushed_ = framePushed; + MOZ_ASSERT(!isDelegate()); + entryPointOrIsDelegate_ = entryPoint; + framePushedOrDelegateOffset_ = framePushed; } // Adjust all code offsets in this try note by a delta. void offsetBy(uint32_t offset) { begin_ += offset; end_ += offset; - entryPoint_ += offset; + if (isDelegate()) { + framePushedOrDelegateOffset_ += offset; + } else { + entryPointOrIsDelegate_ += offset; + } } bool operator<(const TryNote& other) const { diff --git a/js/src/wasm/WasmFrame.h b/js/src/wasm/WasmFrame.h index 23e1c4f49c..a2fdba2b75 100644 --- a/js/src/wasm/WasmFrame.h +++ b/js/src/wasm/WasmFrame.h @@ -346,14 +346,16 @@ static_assert(!std::is_polymorphic_v<Frame>, "Frame doesn't need a vtable."); static_assert(sizeof(Frame) == 2 * sizeof(void*), "Frame is a two pointer structure"); -// Note that sizeof(FrameWithInstances) does not account for ShadowStackSpace. -// Use FrameWithInstances::sizeOf() if you are not incorporating -// ShadowStackSpace through other means (eg the ABIArgIter). - -class FrameWithInstances : public Frame { +class FrameWithShadowStackSpace : public Frame { + protected: // `ShadowStackSpace` bytes will be allocated here on Win64, at higher // addresses than Frame and at lower addresses than the instance fields. + uint8_t shadowStackSpace_[js::jit::ShadowStackSpace]; +}; +class FrameWithInstances + : public std::conditional_t<js::jit::ShadowStackSpace >= 1, + FrameWithShadowStackSpace, Frame> { // The instance area MUST be two pointers exactly. Instance* calleeInstance_; Instance* callerInstance_; @@ -362,21 +364,17 @@ class FrameWithInstances : public Frame { Instance* calleeInstance() { return calleeInstance_; } Instance* callerInstance() { return callerInstance_; } - constexpr static uint32_t sizeOf() { - return sizeof(wasm::FrameWithInstances) + js::jit::ShadowStackSpace; - } - constexpr static uint32_t sizeOfInstanceFields() { - return sizeof(wasm::FrameWithInstances) - sizeof(wasm::Frame); + return sizeof(wasm::FrameWithInstances) - sizeof(wasm::Frame) - + js::jit::ShadowStackSpace; } constexpr static uint32_t sizeOfInstanceFieldsAndShadowStack() { - return sizeOfInstanceFields() + js::jit::ShadowStackSpace; + return sizeof(wasm::FrameWithInstances) - sizeof(wasm::Frame); } constexpr static uint32_t calleeInstanceOffset() { - return offsetof(FrameWithInstances, calleeInstance_) + - js::jit::ShadowStackSpace; + return offsetof(FrameWithInstances, calleeInstance_); } constexpr static uint32_t calleeInstanceOffsetWithoutFrame() { @@ -384,8 +382,7 @@ class FrameWithInstances : public Frame { } constexpr static uint32_t callerInstanceOffset() { - return offsetof(FrameWithInstances, callerInstance_) + - js::jit::ShadowStackSpace; + return offsetof(FrameWithInstances, callerInstance_); } constexpr static uint32_t callerInstanceOffsetWithoutFrame() { diff --git a/js/src/wasm/WasmGC.h b/js/src/wasm/WasmGC.h index b502f2f40e..57e823b3c5 100644 --- a/js/src/wasm/WasmGC.h +++ b/js/src/wasm/WasmGC.h @@ -91,7 +91,7 @@ struct StackMapHeader { // Add 16 words to account for the size of FrameWithInstances including any // shadow stack (at worst 8 words total), and then a little headroom in case // the argument area had to be aligned. - static_assert(FrameWithInstances::sizeOf() / sizeof(void*) <= 8); + static_assert(sizeof(FrameWithInstances) / sizeof(void*) <= 8); static_assert(maxFrameOffsetFromTop >= (MaxParams * MaxParamSize / sizeof(void*)) + 16, "limited size of the offset field"); diff --git a/js/src/wasm/WasmGcObject.cpp b/js/src/wasm/WasmGcObject.cpp index bcab5fe275..3ccb08d381 100644 --- a/js/src/wasm/WasmGcObject.cpp +++ b/js/src/wasm/WasmGcObject.cpp @@ -410,8 +410,6 @@ void WasmArrayObject::obj_finalize(JS::GCContext* gcx, JSObject* object) { /* static */ size_t WasmArrayObject::obj_moved(JSObject* obj, JSObject* old) { - MOZ_ASSERT(!IsInsideNursery(obj)); - // Moving inline arrays requires us to update the data pointer. WasmArrayObject& arrayObj = obj->as<WasmArrayObject>(); WasmArrayObject& oldArrayObj = old->as<WasmArrayObject>(); @@ -423,24 +421,18 @@ size_t WasmArrayObject::obj_moved(JSObject* obj, JSObject* old) { MOZ_ASSERT(arrayObj.isDataInline() == oldArrayObj.isDataInline()); if (IsInsideNursery(old)) { + Nursery& nursery = obj->runtimeFromMainThread()->gc.nursery(); // It's been tenured. - MOZ_ASSERT(obj->isTenured()); if (!arrayObj.isDataInline()) { - // Tell the nursery that the trailer is no longer associated with an - // object in the nursery, since the object has been moved to the tenured - // heap. - Nursery& nursery = obj->runtimeFromMainThread()->gc.nursery(); - nursery.unregisterTrailer(arrayObj.dataHeader()); - // Tell the tenured-heap accounting machinery that the trailer is now - // associated with the tenured heap. const TypeDef& typeDef = arrayObj.typeDef(); MOZ_ASSERT(typeDef.isArrayType()); size_t trailerSize = calcStorageBytes( typeDef.arrayType().elementType_.size(), arrayObj.numElements_); // Ensured by WasmArrayObject::createArrayOOL. MOZ_RELEASE_ASSERT(trailerSize <= size_t(MaxArrayPayloadBytes)); - AddCellMemory(&arrayObj, trailerSize + TrailerBlockOverhead, - MemoryUse::WasmTrailerBlock); + nursery.trackTrailerOnPromotion(arrayObj.dataHeader(), obj, trailerSize, + TrailerBlockOverhead, + MemoryUse::WasmTrailerBlock); } } @@ -563,16 +555,9 @@ void WasmStructObject::obj_finalize(JS::GCContext* gcx, JSObject* object) { /* static */ size_t WasmStructObject::obj_moved(JSObject* obj, JSObject* old) { // See also, corresponding comments in WasmArrayObject::obj_moved. - MOZ_ASSERT(!IsInsideNursery(obj)); if (IsInsideNursery(old)) { - // It's been tenured. - MOZ_ASSERT(obj->isTenured()); - WasmStructObject& structObj = obj->as<WasmStructObject>(); - // WasmStructObject::classForTypeDef ensures we only get called for - // structs with OOL data. Hence: - MOZ_ASSERT(structObj.outlineData_); Nursery& nursery = obj->runtimeFromMainThread()->gc.nursery(); - nursery.unregisterTrailer(structObj.outlineData_); + WasmStructObject& structObj = obj->as<WasmStructObject>(); const TypeDef& typeDef = structObj.typeDef(); MOZ_ASSERT(typeDef.isStructType()); uint32_t totalBytes = typeDef.structType().size_; @@ -580,9 +565,11 @@ size_t WasmStructObject::obj_moved(JSObject* obj, JSObject* old) { WasmStructObject::getDataByteSizes(totalBytes, &inlineBytes, &outlineBytes); MOZ_ASSERT(inlineBytes == WasmStructObject_MaxInlineBytes); MOZ_ASSERT(outlineBytes > 0); - AddCellMemory(&structObj, outlineBytes + TrailerBlockOverhead, - MemoryUse::WasmTrailerBlock); + nursery.trackTrailerOnPromotion(structObj.outlineData_, obj, outlineBytes, + TrailerBlockOverhead, + MemoryUse::WasmTrailerBlock); } + return 0; } diff --git a/js/src/wasm/WasmInstance.cpp b/js/src/wasm/WasmInstance.cpp index d025c02c16..606601581d 100644 --- a/js/src/wasm/WasmInstance.cpp +++ b/js/src/wasm/WasmInstance.cpp @@ -2937,7 +2937,8 @@ static bool EnsureEntryStubs(const Instance& instance, uint32_t funcIndex, } static bool GetInterpEntryAndEnsureStubs(JSContext* cx, Instance& instance, - uint32_t funcIndex, CallArgs args, + uint32_t funcIndex, + const CallArgs& args, void** interpEntry, const FuncType** funcType) { const FuncExport* funcExport; @@ -3099,8 +3100,8 @@ class MOZ_RAII ReturnToJSResultCollector { } }; -bool Instance::callExport(JSContext* cx, uint32_t funcIndex, CallArgs args, - CoercionLevel level) { +bool Instance::callExport(JSContext* cx, uint32_t funcIndex, + const CallArgs& args, CoercionLevel level) { if (memory0Base_) { // If there has been a moving grow, this Instance should have been notified. MOZ_RELEASE_ASSERT(memoryBase(0).unwrap() == memory0Base_); diff --git a/js/src/wasm/WasmInstance.h b/js/src/wasm/WasmInstance.h index 074c6212df..0e4f9745b7 100644 --- a/js/src/wasm/WasmInstance.h +++ b/js/src/wasm/WasmInstance.h @@ -364,7 +364,7 @@ class alignas(16) Instance { // value in args.rval. [[nodiscard]] bool callExport(JSContext* cx, uint32_t funcIndex, - CallArgs args, + const CallArgs& args, CoercionLevel level = CoercionLevel::Spec); // Exception handling support diff --git a/js/src/wasm/WasmJS.cpp b/js/src/wasm/WasmJS.cpp index 2eb5e355d9..d987ecec29 100644 --- a/js/src/wasm/WasmJS.cpp +++ b/js/src/wasm/WasmJS.cpp @@ -123,6 +123,10 @@ static bool ThrowBadImportArg(JSContext* cx) { static bool ThrowBadImportType(JSContext* cx, const CacheableName& field, const char* str) { UniqueChars fieldQuoted = field.toQuotedString(cx); + if (!fieldQuoted) { + ReportOutOfMemory(cx); + return false; + } JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_WASM_BAD_IMPORT_TYPE, fieldQuoted.get(), str); return false; @@ -178,6 +182,10 @@ bool js::wasm::GetImports(JSContext* cx, const Module& module, if (!importModuleValue.isObject()) { UniqueChars moduleQuoted = import.module.toQuotedString(cx); + if (!moduleQuoted) { + ReportOutOfMemory(cx); + return false; + } JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_WASM_BAD_IMPORT_FIELD, moduleQuoted.get()); @@ -256,6 +264,10 @@ bool js::wasm::GetImports(JSContext* cx, const Module& module, if (obj->resultType() != tags[index].type->resultType()) { UniqueChars fieldQuoted = import.field.toQuotedString(cx); UniqueChars moduleQuoted = import.module.toQuotedString(cx); + if (!fieldQuoted || !moduleQuoted) { + ReportOutOfMemory(cx); + return false; + } JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_WASM_BAD_TAG_SIG, moduleQuoted.get(), fieldQuoted.get()); @@ -1005,8 +1017,9 @@ static bool IsModuleObject(JSObject* obj, const Module** module) { return true; } -static bool GetModuleArg(JSContext* cx, CallArgs args, uint32_t numRequired, - const char* name, const Module** module) { +static bool GetModuleArg(JSContext* cx, const CallArgs& args, + uint32_t numRequired, const char* name, + const Module** module) { if (!args.requireAtLeast(cx, name, numRequired)) { return false; } @@ -4514,8 +4527,8 @@ static bool EnsurePromiseSupport(JSContext* cx) { return true; } -static bool GetBufferSource(JSContext* cx, CallArgs callArgs, const char* name, - MutableBytes* bytecode) { +static bool GetBufferSource(JSContext* cx, const CallArgs& callArgs, + const char* name, MutableBytes* bytecode) { if (!callArgs.requireAtLeast(cx, name, 1)) { return false; } @@ -4576,7 +4589,7 @@ static bool WebAssembly_compile(JSContext* cx, unsigned argc, Value* vp) { return true; } -static bool GetInstantiateArgs(JSContext* cx, CallArgs callArgs, +static bool GetInstantiateArgs(JSContext* cx, const CallArgs& callArgs, MutableHandleObject firstArg, MutableHandleObject importObj, MutableHandleValue featureOptions) { @@ -5089,7 +5102,7 @@ const JSClass ResolveResponseClosure::class_ = { &ResolveResponseClosure::classOps_, }; -static ResolveResponseClosure* ToResolveResponseClosure(CallArgs args) { +static ResolveResponseClosure* ToResolveResponseClosure(const CallArgs& args) { return &args.callee() .as<JSFunction>() .getExtendedSlot(0) diff --git a/js/src/wasm/WasmModule.cpp b/js/src/wasm/WasmModule.cpp index a297e81ad3..406c16462b 100644 --- a/js/src/wasm/WasmModule.cpp +++ b/js/src/wasm/WasmModule.cpp @@ -486,6 +486,10 @@ bool Module::instantiateFunctions(JSContext* cx, const Import& import = FindImportFunction(imports_, i); UniqueChars importModuleName = import.module.toQuotedString(cx); UniqueChars importFieldName = import.field.toQuotedString(cx); + if (!importFieldName || !importModuleName) { + ReportOutOfMemory(cx); + return false; + } JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_WASM_BAD_IMPORT_SIG, importModuleName.get(), importFieldName.get()); diff --git a/js/src/wasm/WasmStubs.cpp b/js/src/wasm/WasmStubs.cpp index 83a18c9992..dfaa898744 100644 --- a/js/src/wasm/WasmStubs.cpp +++ b/js/src/wasm/WasmStubs.cpp @@ -1633,9 +1633,10 @@ static void FillArgumentArrayForInterpExit(MacroAssembler& masm, const FuncType& funcType, unsigned argOffset, Register scratch) { - // This is FrameWithInstances::sizeOf() - ShadowStackSpace because the latter + // This is `sizeof(FrameWithInstances) - ShadowStackSpace` because the latter // is accounted for by the ABIArgIter. - const unsigned offsetFromFPToCallerStackArgs = sizeof(FrameWithInstances); + const unsigned offsetFromFPToCallerStackArgs = + sizeof(FrameWithInstances) - jit::ShadowStackSpace; GenPrintf(DebugChannel::Import, masm, "wasm-import[%u]; arguments ", funcImportIndex); @@ -1729,9 +1730,10 @@ static void FillArgumentArrayForJitExit(MacroAssembler& masm, Register instance, Register scratch2, Label* throwLabel) { MOZ_ASSERT(scratch != scratch2); - // This is FrameWithInstances::sizeOf() - ShadowStackSpace because the latter + // This is `sizeof(FrameWithInstances) - ShadowStackSpace` because the latter // is accounted for by the ABIArgIter. - const unsigned offsetFromFPToCallerStackArgs = sizeof(FrameWithInstances); + const unsigned offsetFromFPToCallerStackArgs = + sizeof(FrameWithInstances) - jit::ShadowStackSpace; // This loop does not root the values that are being constructed in // for the arguments. Allocations that are generated by code either @@ -2473,9 +2475,10 @@ bool wasm::GenerateBuiltinThunk(MacroAssembler& masm, ABIFunctionType abiType, // Copy out and convert caller arguments, if needed. - // This is FrameWithInstances::sizeOf() - ShadowStackSpace because the latter + // This is `sizeof(FrameWithInstances) - ShadowStackSpace` because the latter // is accounted for by the ABIArgIter. - unsigned offsetFromFPToCallerStackArgs = sizeof(FrameWithInstances); + unsigned offsetFromFPToCallerStackArgs = + sizeof(FrameWithInstances) - jit::ShadowStackSpace; Register scratch = ABINonArgReturnReg0; for (ABIArgIter i(args); !i.done(); i++) { if (i->argInRegister()) { diff --git a/js/src/wasm/WasmTypeDef.cpp b/js/src/wasm/WasmTypeDef.cpp index 42367c0cb2..ee005681c5 100644 --- a/js/src/wasm/WasmTypeDef.cpp +++ b/js/src/wasm/WasmTypeDef.cpp @@ -364,6 +364,21 @@ bool StructType::init() { return true; } +/* static */ +bool StructType::createImmutable(const ValTypeVector& types, + StructType* struct_) { + StructFieldVector fields; + if (!fields.resize(types.length())) { + return false; + } + for (size_t i = 0; i < types.length(); i++) { + fields[i].type = StorageType(types[i].packed()); + fields[i].isMutable = false; + } + *struct_ = StructType(std::move(fields)); + return struct_->init(); +} + size_t StructType::sizeOfExcludingThis(MallocSizeOf mallocSizeOf) const { return fields_.sizeOfExcludingThis(mallocSizeOf); } diff --git a/js/src/wasm/WasmTypeDef.h b/js/src/wasm/WasmTypeDef.h index 3426647095..a0d44e647b 100644 --- a/js/src/wasm/WasmTypeDef.h +++ b/js/src/wasm/WasmTypeDef.h @@ -371,6 +371,8 @@ class StructType { return true; } + static bool createImmutable(const ValTypeVector& types, StructType* struct_); + size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const; WASM_DECLARE_FRIEND_SERIALIZE(StructType); }; diff --git a/js/src/wasm/WasmValidate.cpp b/js/src/wasm/WasmValidate.cpp index 98a1423a41..d67967fa41 100644 --- a/js/src/wasm/WasmValidate.cpp +++ b/js/src/wasm/WasmValidate.cpp @@ -40,6 +40,49 @@ using mozilla::CheckedInt32; using mozilla::IsUtf8; using mozilla::Span; +// Module environment helpers. + +bool ModuleEnvironment::addDefinedFunc( + ValTypeVector&& params, ValTypeVector&& results, bool declareForRef, + Maybe<CacheableName>&& optionalExportedName) { + uint32_t typeIndex = types->length(); + FuncType funcType(std::move(params), std::move(results)); + if (!types->addType(std::move(funcType))) { + return false; + } + + FuncDesc funcDesc = FuncDesc(&(*types)[typeIndex].funcType(), typeIndex); + uint32_t funcIndex = funcs.length(); + if (!funcs.append(funcDesc)) { + return false; + } + if (declareForRef) { + declareFuncExported(funcIndex, true, true); + } + if (optionalExportedName.isSome()) { + if (!exports.emplaceBack(std::move(optionalExportedName.ref()), funcIndex, + DefinitionKind::Function)) { + return false; + } + } + return true; +} + +bool ModuleEnvironment::addImportedFunc(ValTypeVector&& params, + ValTypeVector&& results, + CacheableName&& importModName, + CacheableName&& importFieldName) { + MOZ_ASSERT(numFuncImports == funcs.length()); + if (!addDefinedFunc(std::move(params), std::move(results), false, + mozilla::Nothing())) { + return false; + } + numFuncImports++; + return imports.emplaceBack(std::move(importModName), + std::move(importFieldName), + DefinitionKind::Function); +} + // Misc helpers. bool wasm::EncodeLocalEntries(Encoder& e, const ValTypeVector& locals) { diff --git a/js/src/wasm/WasmValidate.h b/js/src/wasm/WasmValidate.h index 8ba08fd088..f8d712b3b3 100644 --- a/js/src/wasm/WasmValidate.h +++ b/js/src/wasm/WasmValidate.h @@ -191,6 +191,15 @@ struct ModuleEnvironment { MOZ_ASSERT(tagIndex < tags.length()); return tagsOffsetStart + tagIndex * sizeof(TagInstanceData); } + + bool addDefinedFunc( + ValTypeVector&& params, ValTypeVector&& results, + bool declareForRef = false, + Maybe<CacheableName>&& optionalExportedName = mozilla::Nothing()); + + bool addImportedFunc(ValTypeVector&& params, ValTypeVector&& results, + CacheableName&& importModName, + CacheableName&& importFieldName); }; // ElemSegmentFlags provides methods for decoding and encoding the flags field diff --git a/js/src/wasm/WasmValue.h b/js/src/wasm/WasmValue.h index 79e20285b9..9a5442fc75 100644 --- a/js/src/wasm/WasmValue.h +++ b/js/src/wasm/WasmValue.h @@ -224,9 +224,9 @@ class LitVal { Cell& cell() { return cell_; } const Cell& cell() const { return cell_; } - // Updates the type of the LitVal. Does not check that the type is valid for the - // actual value, so make sure the type is definitely correct via validation or - // something. + // Updates the type of the LitVal. Does not check that the type is valid for + // the actual value, so make sure the type is definitely correct via + // validation or something. void unsafeSetType(ValType type) { type_ = type; } uint32_t i32() const { diff --git a/js/xpconnect/idl/xpccomponents.idl b/js/xpconnect/idl/xpccomponents.idl index 1d4155ed74..47ed22e3a5 100644 --- a/js/xpconnect/idl/xpccomponents.idl +++ b/js/xpconnect/idl/xpccomponents.idl @@ -261,7 +261,7 @@ interface nsIXPCComponents_Utils : nsISupports [optional] in jsval version, [optional] in AUTF8String filename, [optional] in long lineNo, - [optional] in bool enforceFilenameRestrictions); + [optional] in boolean enforceFilenameRestrictions); /* * Get the sandbox for running JS-implemented UA widgets (video controls etc.), @@ -569,7 +569,7 @@ interface nsIXPCComponents_Utils : nsISupports * reference edges) and will throw if you touch them (e.g. by * reading/writing a property). */ - bool isDeadWrapper(in jsval obj); + boolean isDeadWrapper(in jsval obj); /** * Determines whether this value is a remote object proxy, such as @@ -584,7 +584,7 @@ interface nsIXPCComponents_Utils : nsISupports * frame.contentWindow.doCrossOriginThing(); * } */ - bool isRemoteProxy(in jsval val); + boolean isRemoteProxy(in jsval val); /* * To be called from JS only. This is for Gecko internal use only, and may @@ -653,12 +653,12 @@ interface nsIXPCComponents_Utils : nsISupports /** * Check whether the given object is an opaque wrapper (PermissiveXrayOpaque). */ - bool isOpaqueWrapper(in jsval obj); + boolean isOpaqueWrapper(in jsval obj); /** * Check whether the given object is an XrayWrapper. */ - bool isXrayWrapper(in jsval obj); + boolean isXrayWrapper(in jsval obj); /** * Waive Xray on a given value. Identity op for primitives. @@ -680,7 +680,7 @@ interface nsIXPCComponents_Utils : nsISupports * probably what you want. */ [implicit_jscontext] - string getClassName(in jsval aObj, in bool aUnwrap); + string getClassName(in jsval aObj, in boolean aUnwrap); /** * Get a DOM classinfo for the given classname. Only some class diff --git a/js/xpconnect/loader/mozJSModuleLoader.cpp b/js/xpconnect/loader/mozJSModuleLoader.cpp index 017ac32b3b..cdf4df1970 100644 --- a/js/xpconnect/loader/mozJSModuleLoader.cpp +++ b/js/xpconnect/loader/mozJSModuleLoader.cpp @@ -1279,7 +1279,10 @@ nsresult mozJSModuleLoader::GetScriptForLocation( } void mozJSModuleLoader::UnloadModules() { + MOZ_ASSERT(!mIsUnloaded); + mInitialized = false; + mIsUnloaded = true; if (mLoaderGlobal) { MOZ_ASSERT(JS_HasExtensibleLexicalEnvironment(mLoaderGlobal)); @@ -1387,6 +1390,11 @@ nsresult mozJSModuleLoader::IsModuleLoaded(const nsACString& aLocation, bool* retval) { MOZ_ASSERT(nsContentUtils::IsCallerChrome()); + if (mIsUnloaded) { + *retval = false; + return NS_OK; + } + mInitialized = true; ModuleLoaderInfo info(aLocation); if (mImports.Get(info.Key())) { @@ -1420,6 +1428,11 @@ nsresult mozJSModuleLoader::IsJSModuleLoaded(const nsACString& aLocation, bool* retval) { MOZ_ASSERT(nsContentUtils::IsCallerChrome()); + if (mIsUnloaded) { + *retval = false; + return NS_OK; + } + mInitialized = true; ModuleLoaderInfo info(aLocation); if (mImports.Get(info.Key())) { @@ -1435,6 +1448,11 @@ nsresult mozJSModuleLoader::IsESModuleLoaded(const nsACString& aLocation, bool* retval) { MOZ_ASSERT(nsContentUtils::IsCallerChrome()); + if (mIsUnloaded) { + *retval = false; + return NS_OK; + } + mInitialized = true; ModuleLoaderInfo info(aLocation); @@ -1728,6 +1746,11 @@ nsresult mozJSModuleLoader::Import(JSContext* aCx, const nsACString& aLocation, JS::MutableHandleObject aModuleGlobal, JS::MutableHandleObject aModuleExports, bool aIgnoreExports) { + if (mIsUnloaded) { + JS_ReportErrorASCII(aCx, "Module loaded is already unloaded"); + return NS_ERROR_FAILURE; + } + mInitialized = true; AUTO_PROFILER_MARKER_TEXT( @@ -2013,6 +2036,11 @@ nsresult mozJSModuleLoader::ImportESModule( aSkipCheck /* = SkipCheckForBrokenURLOrZeroSized::No */) { using namespace JS::loader; + if (mIsUnloaded) { + JS_ReportErrorASCII(aCx, "Module loaded is already unloaded"); + return NS_ERROR_FAILURE; + } + mInitialized = true; // Called from ChromeUtils::ImportESModule. diff --git a/js/xpconnect/loader/mozJSModuleLoader.h b/js/xpconnect/loader/mozJSModuleLoader.h index ac118c507d..b5e81a4b25 100644 --- a/js/xpconnect/loader/mozJSModuleLoader.h +++ b/js/xpconnect/loader/mozJSModuleLoader.h @@ -290,6 +290,7 @@ class mozJSModuleLoader final : public nsIMemoryReporter { nsClassHashtable<nsCStringHashKey, nsCString> mLocations; bool mInitialized; + bool mIsUnloaded = false; #ifdef DEBUG bool mIsInitializingLoaderGlobal = false; #endif diff --git a/js/xpconnect/src/Sandbox.cpp b/js/xpconnect/src/Sandbox.cpp index 3e931320a9..ed77605193 100644 --- a/js/xpconnect/src/Sandbox.cpp +++ b/js/xpconnect/src/Sandbox.cpp @@ -311,7 +311,7 @@ static bool SandboxFetch(JSContext* cx, JS::HandleObject scope, } BindingCallContext callCx(cx, "fetch"); - RequestOrUSVString request; + RequestOrUTF8String request; if (!request.Init(callCx, args[0], "Argument 1")) { return false; } @@ -1274,7 +1274,7 @@ nsresult ApplyAddonContentScriptCSP(nsISupports* prinOrSop) { csp = new nsCSPContext(); MOZ_TRY( - csp->SetRequestContextWithPrincipal(clonedPrincipal, selfURI, u""_ns, 0)); + csp->SetRequestContextWithPrincipal(clonedPrincipal, selfURI, ""_ns, 0)); MOZ_TRY(csp->AppendPolicy(baseCSP, false, false)); diff --git a/js/xpconnect/src/XPCJSRuntime.cpp b/js/xpconnect/src/XPCJSRuntime.cpp index fd495ec964..567cd860cf 100644 --- a/js/xpconnect/src/XPCJSRuntime.cpp +++ b/js/xpconnect/src/XPCJSRuntime.cpp @@ -2589,7 +2589,8 @@ static void AccumulateTelemetryCallback(JSMetric id, uint32_t sample) { glean::performance_clone_deserialize::size.Accumulate(sample); break; case JSMetric::DESERIALIZE_ITEMS: - glean::performance_clone_deserialize::items.AccumulateSamples({sample}); + glean::performance_clone_deserialize::items.AccumulateSingleSample( + sample); break; case JSMetric::DESERIALIZE_US: glean::performance_clone_deserialize::time.AccumulateRawDuration( diff --git a/js/xpconnect/tests/mochitest/mochitest.toml b/js/xpconnect/tests/mochitest/mochitest.toml index c57cb26890..bc0f1d97b7 100644 --- a/js/xpconnect/tests/mochitest/mochitest.toml +++ b/js/xpconnect/tests/mochitest/mochitest.toml @@ -200,8 +200,6 @@ skip-if = [ "http2", ] -["test_bug871887.html"] - ["test_bug912322.html"] ["test_bug916945.html"] diff --git a/js/xpconnect/tests/mochitest/test_bug871887.html b/js/xpconnect/tests/mochitest/test_bug871887.html deleted file mode 100644 index 082b2ae746..0000000000 --- a/js/xpconnect/tests/mochitest/test_bug871887.html +++ /dev/null @@ -1,43 +0,0 @@ -<!DOCTYPE HTML> -<html> -<!-- -https://bugzilla.mozilla.org/show_bug.cgi?id=871887 ---> -<head> - <meta charset="utf-8"> - <title>Test for Bug 871887</title> - <script src="/tests/SimpleTest/SimpleTest.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> - <script type="application/javascript"> - - /** Test for Bug 871887 **/ - SimpleTest.waitForExplicitFinish(); - - // NB: onstart ends up getting invoked twice, for mysterious and potentially- - // IE6-related reasons. - function checkpoint(invocant) { - ok(true, "onstart called"); - is(invocant, $('llama'), "this-binding is correct"); - $('llama').loop = 1; - $('llama').scrollDelay = 1; - $('llama').scrollAmount = 500; - } - - function done(invocant) { - is(invocant, $('llama'), "this-binding is correct"); - ok(true, "onfinish called"); - SimpleTest.finish(); - } - - </script> -</head> -<body> -<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=871887">Mozilla Bug 871887</a> -<p id="display"></p> -<div id="content"> -<marquee id="llama" onstart="checkpoint(this);" onfinish="done(this);">Watch the Llama</marquee> -</div> -<pre id="test"> -</pre> -</body> -</html> diff --git a/js/xpconnect/tests/unit/test_import_global_current.js b/js/xpconnect/tests/unit/test_import_global_current.js index cf466a7391..59037512f3 100644 --- a/js/xpconnect/tests/unit/test_import_global_current.js +++ b/js/xpconnect/tests/unit/test_import_global_current.js @@ -204,7 +204,7 @@ ChromeUtils.importESModule("resource://test/es6module_import_error.js", { `, sb); } catch (e) { caught = true; - Assert.stringMatches(e.message, /import not found/); + Assert.stringMatches(e.message, /doesn't provide an export named/); } Assert.ok(caught); }); diff --git a/js/xpconnect/tests/unit/test_scriptable_nsIClassInfo.js b/js/xpconnect/tests/unit/test_scriptable_nsIClassInfo.js new file mode 100644 index 0000000000..161afcbb9b --- /dev/null +++ b/js/xpconnect/tests/unit/test_scriptable_nsIClassInfo.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. +https://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(function () { + class TestClass { + QueryInterface = ChromeUtils.generateQI([ + "nsIXPCTestInterfaceA", + "nsIClassInfo", + ]); + + interfaces = [Ci.nsIXPCTestInterfaceA, Ci.nsIClassInfo, Ci.nsISupports]; + contractID = "@mozilla.org/test/class;1"; + classDescription = "description"; + classID = Components.ID("{4da556d4-00fa-451a-a280-d2aec7c5f265}"); + flags = 0; + + name = "this is a test"; + } + + let instance = new TestClass(); + Assert.ok(instance, "can create an instance"); + Assert.ok(instance.QueryInterface(Ci.nsIClassInfo), "can QI to nsIClassInfo"); + + let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + registrar.registerFactory( + instance.classID, + instance.classDescription, + instance.contractID, + { + createInstance(iid) { + return instance.QueryInterface(iid); + }, + } + ); + Assert.ok(true, "successfully registered the factory"); + + let otherInstance = Cc["@mozilla.org/test/class;1"].createInstance( + Ci.nsIXPCTestInterfaceA + ); + Assert.ok(otherInstance, "can create an instance via xpcom"); +}); diff --git a/js/xpconnect/tests/unit/xpcshell.toml b/js/xpconnect/tests/unit/xpcshell.toml index 37274eba96..b7bc15afdc 100644 --- a/js/xpconnect/tests/unit/xpcshell.toml +++ b/js/xpconnect/tests/unit/xpcshell.toml @@ -358,6 +358,8 @@ head = "head_ongc.js" ["test_sandbox_name.js"] +["test_scriptable_nsIClassInfo.js"] + ["test_storage.js"] ["test_structuredClone.js"] diff --git a/js/xpconnect/wrappers/XrayWrapper.cpp b/js/xpconnect/wrappers/XrayWrapper.cpp index cdedb02a5f..48696cd12d 100644 --- a/js/xpconnect/wrappers/XrayWrapper.cpp +++ b/js/xpconnect/wrappers/XrayWrapper.cpp @@ -1799,6 +1799,11 @@ bool DOMXrayTraits::call(JSContext* cx, HandleObject wrapper, // using "legacycaller". At this time for all the legacycaller users it makes // more sense to invoke on the xray compartment, so we just go ahead and do // that for everything. + if (IsDOMConstructor(obj)) { + const JSNativeHolder* holder = NativeHolderFromObject(obj); + return holder->mNative(cx, args.length(), args.base()); + } + if (js::IsProxy(obj)) { if (JS::IsCallable(obj)) { // Passing obj here, but it doesn't really matter because legacycaller @@ -1822,20 +1827,21 @@ bool DOMXrayTraits::construct(JSContext* cx, HandleObject wrapper, const JS::CallArgs& args, const js::Wrapper& baseInstance) { RootedObject obj(cx, getTargetObject(wrapper)); - MOZ_ASSERT(mozilla::dom::HasConstructor(obj)); - const JSClass* clasp = JS::GetClass(obj); // See comments in DOMXrayTraits::call() explaining what's going on here. - if (clasp->flags & JSCLASS_IS_DOMIFACEANDPROTOJSCLASS) { - if (JSNative construct = clasp->getConstruct()) { - if (!construct(cx, args.length(), args.base())) { - return false; - } - } else { + if (IsDOMConstructor(obj)) { + const JSNativeHolder* holder = NativeHolderFromObject(obj); + if (!holder->mNative(cx, args.length(), args.base())) { + return false; + } + } else { + const JSClass* clasp = JS::GetClass(obj); + if (clasp->flags & JSCLASS_IS_DOMIFACEANDPROTOJSCLASS) { + MOZ_ASSERT(!clasp->getConstruct()); + RootedValue v(cx, ObjectValue(*wrapper)); js::ReportIsNotFunction(cx, v); return false; } - } else { if (!baseInstance.construct(cx, wrapper, args)) { return false; } |