@@ -224,7 +224,8 @@ auto AbstractModuleRecord::resolveImport(JSGlobalObject* globalObject, const Ide
224224
225225 AbstractModuleRecord* importedModule = hostResolveImportedModule (globalObject, importEntry.moduleRequest );
226226 RETURN_IF_EXCEPTION (scope, Resolution::error ());
227- return importedModule->resolveExport (globalObject, importEntry.importName );
227+
228+ RELEASE_AND_RETURN (scope, importedModule->resolveExport (globalObject, importEntry.importName ));
228229}
229230
230231struct AbstractModuleRecord ::ResolveQuery {
@@ -544,7 +545,7 @@ auto AbstractModuleRecord::resolveExportImpl(JSGlobalObject* globalObject, const
544545 // To summarize the observations.
545546 //
546547 // 1. The starting point is always cacheable.
547- // 2. A module that has resolved a local binding is always cacheable.
548+ // 2. A module that has resolved a local binding is always cacheable. But since they are in exportEntries, we do not need a cache.
548549 // 3. If we don't follow any star links during the resolution, we can see all the traced nodes are cacheable.
549550 // 4. Once we follow star links, we should not retrieve the result from the cache and should not cache the result.
550551 // 5. Once we see star links, even if we have not yet traversed that star link path, we should disable caching.
@@ -650,19 +651,17 @@ auto AbstractModuleRecord::resolveExportImpl(JSGlobalObject* globalObject, const
650651 if (!moduleRecord->starExportEntries ().isEmpty ())
651652 foundStarLinks = true ;
652653
653- // 4. Once we follow star links, we should not retrieve the result from the cache and should not cache the result.
654- if (!foundStarLinks) {
655- if (std::optional<Resolution> cachedResolution = moduleRecord->tryGetCachedResolution (query.exportName .get ())) {
656- if (!mergeToCurrentTop (*cachedResolution))
657- return Resolution::ambiguous ();
658- continue ;
659- }
660- }
661-
662654 const std::optional<ExportEntry> optionalExportEntry = moduleRecord->tryGetExportEntry (query.exportName .get ());
663655 if (!optionalExportEntry) {
664- // If there is no matched exported binding in the current module,
665- // we need to look into the stars.
656+ // If there is no matched exported binding in the current module, we need to look
657+ // into the stars. We don't probe m_resolutionCache here: the only writer that can
658+ // populate (moduleRecord, exportName) while exportEntries has no match for exportName
659+ // is the root-cache write (rule #1), which only fires when star traversal produced
660+ // Resolved - which in turn requires moduleRecord to have non-empty starExportEntries.
661+ // starExportEntries is immutable after parse, so by the time we reach this point
662+ // foundStarLinks is already true (set above) whenever a cached entry could exist -
663+ // making any probe here dead. The top-level resolveExport fast path still benefits
664+ // from the rule #1 cache write.
666665 bool success = resolveNonLocal (task.query );
667666 EXCEPTION_ASSERT (!scope.exception () || !success);
668667 if (!success)
@@ -675,14 +674,21 @@ auto AbstractModuleRecord::resolveExportImpl(JSGlobalObject* globalObject, const
675674 case ExportEntry::Type::Local: {
676675 ASSERT (!exportEntry.localName .isNull ());
677676 Resolution resolution { Resolution::Type::Resolved, moduleRecord, exportEntry.localName };
678- // 2. A module that has resolved a local binding is always cacheable.
679- cacheResolutionForQuery (query, resolution);
680677 if (!mergeToCurrentTop (resolution))
681678 return Resolution::ambiguous ();
682679 continue ;
683680 }
684681
685682 case ExportEntry::Type::Indirect: {
683+ // 4. Once we follow star links, we should not retrieve the result from the cache and should not cache the result.
684+ if (!foundStarLinks) {
685+ if (std::optional<Resolution> cachedResolution = moduleRecord->tryGetCachedResolution (query.exportName .get ())) {
686+ if (!mergeToCurrentTop (*cachedResolution))
687+ return Resolution::ambiguous ();
688+ continue ;
689+ }
690+ }
691+
686692 AbstractModuleRecord* importedModuleRecord = moduleRecord->hostResolveImportedModule (globalObject, exportEntry.moduleName );
687693 RETURN_IF_EXCEPTION (scope, Resolution::error ());
688694
@@ -698,10 +704,7 @@ auto AbstractModuleRecord::resolveExportImpl(JSGlobalObject* globalObject, const
698704 case ExportEntry::Type::Namespace: {
699705 AbstractModuleRecord* importedModuleRecord = moduleRecord->hostResolveImportedModule (globalObject, exportEntry.moduleName );
700706 RETURN_IF_EXCEPTION (scope, Resolution::error ());
701-
702707 Resolution resolution { Resolution::Type::Resolved, importedModuleRecord, vm.propertyNames ->starNamespacePrivateName };
703- // 2. A module that has resolved a module namespace binding is always cacheable.
704- cacheResolutionForQuery (query, resolution);
705708 if (!mergeToCurrentTop (resolution))
706709 return Resolution::ambiguous ();
707710 continue ;
@@ -758,10 +761,35 @@ auto AbstractModuleRecord::resolveExportImpl(JSGlobalObject* globalObject, const
758761
759762auto AbstractModuleRecord::resolveExport (JSGlobalObject* globalObject, const Identifier& exportName) -> Resolution
760763{
761- // Look up the cached resolution first before entering the resolving loop, since the loop setup takes some cost.
762- if (std::optional<Resolution> cachedResolution = tryGetCachedResolution (exportName.impl ()))
763- return *cachedResolution;
764- return resolveExportImpl (globalObject, ResolveQuery (this , exportName.impl ()));
764+ VM & vm = globalObject->vm ();
765+ auto scope = DECLARE_THROW_SCOPE (vm);
766+
767+ // Local / Namespace exports are trivially derivable from m_exportEntries.
768+ // m_resolutionCache only holds results that actually amortise costly traversals, Indirect resolutions and star-resolved results.
769+ if (const auto optionalExportEntry = tryGetExportEntry (exportName.impl ())) {
770+ const ExportEntry& entry = *optionalExportEntry;
771+ switch (entry.type ) {
772+ case ExportEntry::Type::Local:
773+ ASSERT (!entry.localName .isNull ());
774+ return Resolution { Resolution::Type::Resolved, this , entry.localName };
775+ case ExportEntry::Type::Namespace: {
776+ AbstractModuleRecord* importedModuleRecord = hostResolveImportedModule (globalObject, entry.moduleName );
777+ RETURN_IF_EXCEPTION (scope, Resolution::error ());
778+ return Resolution { Resolution::Type::Resolved, importedModuleRecord, vm.propertyNames ->starNamespacePrivateName };
779+ }
780+ case ExportEntry::Type::Indirect:
781+ if (std::optional<Resolution> cachedResolution = tryGetCachedResolution (exportName.impl ()))
782+ return *cachedResolution;
783+ break ;
784+ }
785+ } else if (!starExportEntries ().isEmpty ()) {
786+ // When there is no matching export entry, cache can exist only when we found star-resolved results.
787+ // Thus, if there is no star export entries, cache never exists.
788+ if (std::optional<Resolution> cachedResolution = tryGetCachedResolution (exportName.impl ()))
789+ return *cachedResolution;
790+ }
791+
792+ RELEASE_AND_RETURN (scope, resolveExportImpl (globalObject, ResolveQuery (this , exportName.impl ())));
765793}
766794
767795JSModuleNamespaceObject* AbstractModuleRecord::getModuleNamespace (JSGlobalObject* globalObject)
@@ -788,6 +816,7 @@ JSModuleNamespaceObject* AbstractModuleRecord::getModuleNamespace(JSGlobalObject
788816 Resolutions uniqueBindings;
789817 IdentifierSet rootShadowedNames;
790818 IdentifierSet slowPathNames;
819+ Vector<std::pair<Identifier, Resolution>> resolutions;
791820
792821 UncheckedKeyHashSet<AbstractModuleRecord*> exportStarSet;
793822 Vector<AbstractModuleRecord*, 8 > pendingModules;
@@ -801,15 +830,21 @@ JSModuleNamespaceObject* AbstractModuleRecord::getModuleNamespace(JSGlobalObject
801830
802831 for (const auto & pair : moduleRecord->exportEntries ()) {
803832 const ExportEntry& exportEntry = pair.value ;
804- if (!isRoot && vm.propertyNames ->defaultKeyword == exportEntry.exportName )
805- continue ;
806833 SUPPRESS_UNCOUNTED_LOCAL auto * exportName = exportEntry.exportName .impl ();
807834 // ResolveExport returns at root's own Local/Indirect/Namespace entry before
808835 // consulting star exports, so star-reachable bindings cannot affect those names.
809- if (!isRoot && rootShadowedNames.contains (exportName))
810- continue ;
811- if (slowPathNames.contains (exportName))
812- continue ;
836+ if (isRoot)
837+ rootShadowedNames.add (exportName);
838+ else {
839+ if (vm.propertyNames ->defaultKeyword == exportEntry.exportName )
840+ continue ;
841+ // Both sets do not include exportName during the root iteration (root is always the first
842+ // module popped from pendingModules), so we only need to probe them for non-root.
843+ if (rootShadowedNames.contains (exportName))
844+ continue ;
845+ if (slowPathNames.contains (exportName))
846+ continue ;
847+ }
813848
814849 Resolution candidate;
815850 switch (exportEntry.type ) {
@@ -823,17 +858,17 @@ JSModuleNamespaceObject* AbstractModuleRecord::getModuleNamespace(JSGlobalObject
823858 break ;
824859 }
825860 case ExportEntry::Type::Indirect:
826- if (isRoot)
827- rootShadowedNames.add (exportName);
828- else
861+ if (!isRoot)
829862 uniqueBindings.remove (exportName);
830863 slowPathNames.add (exportName);
831864 continue ;
832865 }
833866
834867 if (isRoot) {
835- rootShadowedNames.add (exportName);
836- uniqueBindings.add (exportName, candidate);
868+ // Root's own Local / Namespace are served by resolveExport's m_exportEntries
869+ // fast path, so they never need to sit in uniqueBindings or the cache. Emit
870+ // them directly so the cache-write loop below can skip the owned-name probe.
871+ resolutions.append ({ Identifier::fromUid (vm, exportName), candidate });
837872 continue ;
838873 }
839874
@@ -851,9 +886,10 @@ JSModuleNamespaceObject* AbstractModuleRecord::getModuleNamespace(JSGlobalObject
851886 }
852887 }
853888
854- Vector<std::pair<Identifier, Resolution>> resolutions;
855- resolutions.reserveInitialCapacity (uniqueBindings.size () + slowPathNames.size ());
889+ resolutions.reserveCapacity (resolutions.size () + uniqueBindings.size () + slowPathNames.size ());
856890 for (auto & pair : uniqueBindings) {
891+ // Every entry here arrived via a star-export edge (root's own names were emitted
892+ // during the walk), so the cache is always useful.
857893 cacheResolution (pair.key .get (), pair.value );
858894 resolutions.append ({ Identifier::fromUid (vm, pair.key .get ()), pair.value });
859895 }
0 commit comments