| /* |
| * Copyright (C) 2015-2021 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "AbstractModuleRecord.h" |
| |
| #include "Error.h" |
| #include "JSCInlines.h" |
| #include "JSInternalFieldObjectImplInlines.h" |
| #include "JSMapInlines.h" |
| #include "JSModuleEnvironment.h" |
| #include "JSModuleNamespaceObject.h" |
| #include "JSModuleRecord.h" |
| #include "VMTrapsInlines.h" |
| #include "WebAssemblyModuleRecord.h" |
| |
| namespace JSC { |
| namespace AbstractModuleRecordInternal { |
| static constexpr bool verbose = false; |
| } // namespace AbstractModuleRecordInternal |
| |
| const ClassInfo AbstractModuleRecord::s_info = { "AbstractModuleRecord"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(AbstractModuleRecord) }; |
| |
| AbstractModuleRecord::AbstractModuleRecord(VM& vm, Structure* structure, const Identifier& moduleKey) |
| : Base(vm, structure) |
| , m_moduleKey(moduleKey) |
| { |
| } |
| |
| void AbstractModuleRecord::finishCreation(JSGlobalObject* globalObject, VM& vm) |
| { |
| DeferTerminationForAWhile deferScope(vm); |
| auto scope = DECLARE_CATCH_SCOPE(vm); |
| |
| Base::finishCreation(vm); |
| ASSERT(inherits(info())); |
| |
| auto values = initialValues(); |
| ASSERT(values.size() == numberOfInternalFields); |
| for (unsigned index = 0; index < values.size(); ++index) |
| Base::internalField(index).set(vm, this, values[index]); |
| |
| JSMap* map = JSMap::create(globalObject, vm, globalObject->mapStructure()); |
| scope.releaseAssertNoException(); |
| m_dependenciesMap.set(vm, this, map); |
| putDirect(vm, Identifier::fromString(vm, "dependenciesMap"_s), m_dependenciesMap.get()); |
| } |
| |
| template<typename Visitor> |
| void AbstractModuleRecord::visitChildrenImpl(JSCell* cell, Visitor& visitor) |
| { |
| AbstractModuleRecord* thisObject = jsCast<AbstractModuleRecord*>(cell); |
| ASSERT_GC_OBJECT_INHERITS(thisObject, info()); |
| Base::visitChildren(thisObject, visitor); |
| visitor.append(thisObject->m_moduleEnvironment); |
| visitor.append(thisObject->m_moduleNamespaceObject); |
| visitor.append(thisObject->m_dependenciesMap); |
| } |
| |
| DEFINE_VISIT_CHILDREN(AbstractModuleRecord); |
| |
| void AbstractModuleRecord::appendRequestedModule(const Identifier& moduleName) |
| { |
| m_requestedModules.add(moduleName.impl()); |
| } |
| |
| void AbstractModuleRecord::addStarExportEntry(const Identifier& moduleName) |
| { |
| m_starExportEntries.add(moduleName.impl()); |
| } |
| |
| void AbstractModuleRecord::addImportEntry(const ImportEntry& entry) |
| { |
| bool isNewEntry = m_importEntries.add(entry.localName.impl(), entry).isNewEntry; |
| ASSERT_UNUSED(isNewEntry, isNewEntry); // This is guaranteed by the parser. |
| } |
| |
| void AbstractModuleRecord::addExportEntry(const ExportEntry& entry) |
| { |
| bool isNewEntry = m_exportEntries.add(entry.exportName.impl(), entry).isNewEntry; |
| ASSERT_UNUSED(isNewEntry, isNewEntry); // This is guaranteed by the parser. |
| } |
| |
| auto AbstractModuleRecord::tryGetImportEntry(UniquedStringImpl* localName) -> std::optional<ImportEntry> |
| { |
| const auto iterator = m_importEntries.find(localName); |
| if (iterator == m_importEntries.end()) |
| return std::nullopt; |
| return std::optional<ImportEntry>(iterator->value); |
| } |
| |
| auto AbstractModuleRecord::tryGetExportEntry(UniquedStringImpl* exportName) -> std::optional<ExportEntry> |
| { |
| const auto iterator = m_exportEntries.find(exportName); |
| if (iterator == m_exportEntries.end()) |
| return std::nullopt; |
| return std::optional<ExportEntry>(iterator->value); |
| } |
| |
| auto AbstractModuleRecord::ExportEntry::createLocal(const Identifier& exportName, const Identifier& localName) -> ExportEntry |
| { |
| return ExportEntry { Type::Local, exportName, Identifier(), Identifier(), localName }; |
| } |
| |
| auto AbstractModuleRecord::ExportEntry::createIndirect(const Identifier& exportName, const Identifier& importName, const Identifier& moduleName) -> ExportEntry |
| { |
| return ExportEntry { Type::Indirect, exportName, moduleName, importName, Identifier() }; |
| } |
| |
| auto AbstractModuleRecord::ExportEntry::createNamespace(const Identifier& exportName, const Identifier& moduleName) -> ExportEntry |
| { |
| return ExportEntry { Type::Namespace, exportName, moduleName, Identifier(), Identifier() }; |
| } |
| |
| auto AbstractModuleRecord::Resolution::notFound() -> Resolution |
| { |
| return Resolution { Type::NotFound, nullptr, Identifier() }; |
| } |
| |
| auto AbstractModuleRecord::Resolution::error() -> Resolution |
| { |
| return Resolution { Type::Error, nullptr, Identifier() }; |
| } |
| |
| auto AbstractModuleRecord::Resolution::ambiguous() -> Resolution |
| { |
| return Resolution { Type::Ambiguous, nullptr, Identifier() }; |
| } |
| |
| AbstractModuleRecord* AbstractModuleRecord::hostResolveImportedModule(JSGlobalObject* globalObject, const Identifier& moduleName) |
| { |
| VM& vm = globalObject->vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| JSValue moduleNameValue = identifierToJSValue(vm, moduleName); |
| JSValue entry = m_dependenciesMap->JSMap::get(globalObject, moduleNameValue); |
| RETURN_IF_EXCEPTION(scope, nullptr); |
| RELEASE_AND_RETURN(scope, entry.getAs<AbstractModuleRecord*>(globalObject, Identifier::fromString(vm, "module"_s))); |
| } |
| |
| auto AbstractModuleRecord::resolveImport(JSGlobalObject* globalObject, const Identifier& localName) -> Resolution |
| { |
| VM& vm = globalObject->vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| |
| std::optional<ImportEntry> optionalImportEntry = tryGetImportEntry(localName.impl()); |
| if (!optionalImportEntry) |
| return Resolution::notFound(); |
| |
| const ImportEntry& importEntry = *optionalImportEntry; |
| if (importEntry.type == AbstractModuleRecord::ImportEntryType::Namespace) |
| return Resolution::notFound(); |
| |
| AbstractModuleRecord* importedModule = hostResolveImportedModule(globalObject, importEntry.moduleRequest); |
| RETURN_IF_EXCEPTION(scope, Resolution::error()); |
| return importedModule->resolveExport(globalObject, importEntry.importName); |
| } |
| |
| struct AbstractModuleRecord::ResolveQuery { |
| struct Hash { |
| static unsigned hash(const ResolveQuery&); |
| static bool equal(const ResolveQuery&, const ResolveQuery&); |
| static constexpr bool safeToCompareToEmptyOrDeleted = true; |
| }; |
| using HashTraits = WTF::CustomHashTraits<ResolveQuery>; |
| |
| ResolveQuery(AbstractModuleRecord* moduleRecord, UniquedStringImpl* exportName) |
| : moduleRecord(moduleRecord) |
| , exportName(exportName) |
| { |
| } |
| |
| ResolveQuery(AbstractModuleRecord* moduleRecord, const Identifier& exportName) |
| : ResolveQuery(moduleRecord, exportName.impl()) |
| { |
| } |
| |
| enum EmptyValueTag { EmptyValue }; |
| ResolveQuery(EmptyValueTag) |
| { |
| } |
| |
| enum DeletedValueTag { DeletedValue }; |
| ResolveQuery(DeletedValueTag) |
| : moduleRecord(nullptr) |
| , exportName(WTF::HashTableDeletedValue) |
| { |
| } |
| |
| bool isEmptyValue() const |
| { |
| return !exportName; |
| } |
| |
| bool isDeletedValue() const |
| { |
| return exportName.isHashTableDeletedValue(); |
| } |
| |
| void dump(PrintStream& out) const |
| { |
| if (!moduleRecord) { |
| out.print("<empty>"); |
| return; |
| } |
| out.print(moduleRecord->moduleKey(), " \"", exportName.get(), "\""); |
| } |
| |
| // The module record is not marked from the GC. But these records are reachable from the JSGlobalObject. |
| // So we don't care the reachability to this record. |
| AbstractModuleRecord* moduleRecord; |
| RefPtr<UniquedStringImpl> exportName; |
| }; |
| |
| inline unsigned AbstractModuleRecord::ResolveQuery::Hash::hash(const ResolveQuery& query) |
| { |
| return WTF::PtrHash<AbstractModuleRecord*>::hash(query.moduleRecord) + IdentifierRepHash::hash(query.exportName); |
| } |
| |
| inline bool AbstractModuleRecord::ResolveQuery::Hash::equal(const ResolveQuery& lhs, const ResolveQuery& rhs) |
| { |
| return lhs.moduleRecord == rhs.moduleRecord && lhs.exportName == rhs.exportName; |
| } |
| |
| auto AbstractModuleRecord::tryGetCachedResolution(UniquedStringImpl* exportName) -> std::optional<Resolution> |
| { |
| const auto iterator = m_resolutionCache.find(exportName); |
| if (iterator == m_resolutionCache.end()) |
| return std::nullopt; |
| return std::optional<Resolution>(iterator->value); |
| } |
| |
| void AbstractModuleRecord::cacheResolution(UniquedStringImpl* exportName, const Resolution& resolution) |
| { |
| m_resolutionCache.add(exportName, resolution); |
| } |
| |
| auto AbstractModuleRecord::resolveExportImpl(JSGlobalObject* globalObject, const ResolveQuery& root) -> Resolution |
| { |
| VM& vm = globalObject->vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| |
| if (AbstractModuleRecordInternal::verbose) |
| dataLog("Resolving ", root, "\n"); |
| |
| // https://tc39.github.io/ecma262/#sec-resolveexport |
| |
| // How to avoid C++ recursion in this function: |
| // This function avoids C++ recursion of the naive ResolveExport implementation. |
| // Flatten the recursion to the loop with the task queue and frames. |
| // |
| // 1. pendingTasks |
| // We enqueue the recursive resolveExport call to this queue to avoid recursive calls in C++. |
| // The task has 3 types. (1) Query, (2) IndirectFallback and (3) GatherStars. |
| // (1) Query |
| // Querying the resolution to the current module. |
| // (2) IndirectFallback |
| // Examine the result of the indirect export resolution. Only when the indirect export resolution fails, |
| // we look into the star exports. (step 5-a-vi). |
| // (3) GatherStars |
| // Examine the result of the star export resolutions. |
| // |
| // 2. frames |
| // When the spec calls the resolveExport recursively, instead we append the frame |
| // (that holds the result resolution) to the frames and enqueue the task to the pendingTasks. |
| // The entry in the frames means the *local* resolution result of the specific recursive resolveExport. |
| // |
| // We should maintain the local resolution result instead of holding the global resolution result only. |
| // For example, |
| // |
| // star |
| // (1) ---> (2) "Resolve" |
| // | |
| // | |
| // +-> (3) "NotFound" |
| // | |
| // | star |
| // +-> (4) ---> (5) "Resolve" [here] |
| // | |
| // | |
| // +-> (6) "Error" |
| // |
| // Consider the above graph. The numbers represents the modules. Now we are [here]. |
| // If we only hold the global resolution result during the resolveExport operation, [here], |
| // we decide the entire result of resolveExport is "Ambiguous", because there are multiple |
| // "Resolve" (in module (2) and (5)). However, this should become "Error" because (6) will |
| // propagate "Error" state to the (4), (4) will become "Error" and then, (1) will become |
| // "Error". We should aggregate the results at the star exports point ((4) and (1)). |
| // |
| // Usually, both "Error" and "Ambiguous" states will throw the syntax error. So except for the content of the |
| // error message, there are no difference. (And if we fix the (6) that raises "Error", next, it will produce |
| // the "Ambiguous" error due to (5). Anyway, user need to fix the both. So which error should be raised at first |
| // doesn't matter so much. |
| // |
| // However, this may become the problem under the module namespace creation. |
| // http://www.ecma-international.org/ecma-262/6.0/#sec-getmodulenamespace |
| // section 15.2.1.18, step 3-d-ii |
| // Here, we distinguish "Ambiguous" and "Error". When "Error" state is produced, we need to throw the propagated error. |
| // But if "Ambiguous" state comes, we just ignore the result. |
| // To follow the requirement strictly, in this implementation, we keep the local resolution result to produce the |
| // correct result under the above complex cases. |
| |
| // Caching strategy: |
| // The resolveExport operation is frequently called. So caching results is important. |
| // We observe the following aspects and based on them construct the caching strategy. |
| // Here, we attempt to cache the resolution by constructing the map in module records. |
| // That means Module -> ExportName -> Maybe<Resolution>. |
| // Technically, all the AbstractModuleRecords have the Map<ExportName, Resolution> for caching. |
| // |
| // The important observations are that, |
| // |
| // - *cacheable* means that traversing to this node from a path will produce the same results as starting from this node. |
| // |
| // Here, we define the resovling route. We represent [?] as the module that has the local binding. |
| // And (?) as the module without the local binding. |
| // |
| // @ -> (A) -> (B) -> [C] |
| // |
| // We list the resolving route for each node. |
| // |
| // (A): (A) -> (B) -> [C] |
| // (B): (B) -> [C] |
| // [C]: [C] |
| // |
| // In this case, if we start the tracing from (B), the resolving route becomes (B) -> [C]. |
| // So this is the same. At that time, we can say (B) is cacheable in the first tracing. |
| // |
| // - The cache ability of a node depends on the resolving route from this node. |
| // |
| // 1. The starting point is always cacheable. |
| // |
| // 2. A module that has resolved a local binding is always cacheable. |
| // |
| // @ -> (A) -> [B] |
| // |
| // In the above case, we can see the [B] as cacheable. |
| // This is because when starting from [B] node, we immediately resolve with the local binding. |
| // So the resolving route from [B] does not depend on the starting point. |
| // |
| // 3. If we don't follow any star links during the resolution, we can see all the traced nodes are cacheable. |
| // |
| // If there are non star links, it means that there is *no branch* in the module dependency graph. |
| // This *no branch* feature makes all the modules cachable. |
| // |
| // I.e, if we traverse one star link (even if we successfully resolve that star link), |
| // we must still traverse all other star links. I would also explain we don't run into |
| // this when resolving a local/indirect link. When resolving a local/indirect link, |
| // we won't traverse any star links. |
| // And since the module can hold only one local/indirect link for the specific export name (if there |
| // are multiple local/indirect links that has the same export name, it should be syntax error in the |
| // parsing phase.), there is no multiple outgoing links from a module. |
| // |
| // @ -> (A) --> (B) -> [C] -> (D) -> (E) -+ |
| // ^ | |
| // | | |
| // +------------------------+ |
| // |
| // When starting from @, [C] will be found as the module resolving the given binding. |
| // In this case, (B) can cache this resolution. Since the resolving route is the same to the one when |
| // starting from (B). After caching the above result, we attempt to resolve the same binding from (D). |
| // |
| // @ |
| // | |
| // v |
| // @ -> (A) --> (B) -> [C] -> (D) -> (E) -+ |
| // ^ | |
| // | | |
| // +------------------------+ |
| // |
| // In this case, we can use the (B)'s cached result. And (E) can be cached. |
| // |
| // (E): The resolving route is now (E) -> (B) -> [C]. That is the same when starting from (E). |
| // |
| // No branching makes that the problematic *once-visited* node cannot be seen. |
| // The *once-visited* node makes the resolving route changed since when we see the *once-visited* node, |
| // we stop tracing this. |
| // |
| // If there is no star links and if we look *once-visited* node under no branching graph, *once-visited* |
| // node cannot resolve the requested binding. If the *once-visited* node can resolve the binding, we |
| // should have already finished the resolution before reaching this *once-visited* node. |
| // |
| // 4. Once we follow star links, we should not retrieve the result from the cache and should not cache. |
| // |
| // Star links are only the way to introduce branch. |
| // Once we follow the star links during the resolution, we cannot cache naively. |
| // This is because the cacheability depends on the resolving route. And branching produces the problematic *once-visited* |
| // nodes. Since we don't follow the *once-visited* node, the resolving route from the node becomes different from |
| // the resolving route when starting from this node. |
| // |
| // The following example explains when we should not retrieve the cache and cache the result. |
| // |
| // +----> (D) ------+ |
| // | | |
| // | v |
| // (A) *----+----> (B) ---> [C] |
| // ^ |
| // | |
| // @ |
| // |
| // When starting from (B), we find [C]. In this resolving route, we don't find any star link. |
| // And by definition, (B) and [C] are cachable. (B) is the starting point. And [C] has the local binding. |
| // |
| // +----> (D) ------+ |
| // | | |
| // | v |
| // @-> (A) *----+----> (B) ---> [C] |
| // |
| // But when starting from (A), we should not get the value from the cache. Because, |
| // |
| // 1. When looking (D), we reach [C] and make both resolved. |
| // 2. When looking (B), if we retrieved the last cache from (B), (B) becomes resolved. |
| // 3. But actually, (B) is not-found in this trial because (C) is already *once-visited*. |
| // 4. If we accidentally make (B) resolved, (A) becomes ambiguous. But the correct answer is resolved. |
| // |
| // Why is this problem caused? This is because the *once-visited* node makes the result not-found. |
| // In the second trial, (B) -> [C] result is changed from resolved to not-found. |
| // |
| // When does this become a problem? If the status of the *once-visited* node group is resolved, |
| // changing the result to not-found makes the result changed. |
| // |
| // This problem does not happen when we don't see any star link yet. Now, consider the minimum case. |
| // |
| // @-> (A) -> [ some graph ] |
| // ^ | |
| // | | |
| // +------------+ |
| // |
| // In (A), we don't see any star link yet. So we can say that all the visited nodes does not have any local |
| // resolution. Because if they had a local/indirect resolution, we should have already finished the tracing. |
| // |
| // And even if the some graph will see the *once-visited* node (in this case, (A)), that does not affect the |
| // result of the resolution. Because even if we follow the link to (A) or not follow the link to (A), the status |
| // of the link is always not-found since (A) does not have any local resolution. |
| // In the above case, we can use the result of the [some graph]. |
| // |
| // 5. Once we see star links, even if we have not yet traversed that star link path, we should disable caching. |
| // |
| // Here is the reason why: |
| // |
| // +-------------+ |
| // | | |
| // v | |
| // (A) -> (B) -> (C) *-> [E] |
| // * ^ |
| // | | |
| // v @ |
| // [D] |
| // |
| // In the above case, (C) will be resolved with [D]. |
| // (C) will see (A) and (A) gives up in (A) -> (B) -> (C) route. So, (A) will fallback to [D]. |
| // |
| // +-------------+ |
| // | | |
| // v | |
| // @-> (A) -> (B) -> (C) *-> [E] |
| // * |
| // | |
| // v |
| // [D] |
| // |
| // But in this case, (A) will be resolved with [E] (not [D]). |
| // (C) will attempt to follow the link to (A), but it fails. |
| // So (C) will fallback to the star link and found [E]. In this senario, |
| // (C) is now resolved with [E]'s result. |
| // |
| // The cause of this problem is also the same to 4. |
| // In the latter case, when looking (C), we cannot use the cached result in (C). |
| // Because the cached result of (C) depends on the *once-visited* node (A) and |
| // (A) has the fallback system with the star link. |
| // In the latter trial, we now assume that (A)'s status is not-found. |
| // But, actually, in the former trial, (A)'s status becomes resolved due to the fallback to the [D]. |
| // |
| // To summarize the observations. |
| // |
| // 1. The starting point is always cacheable. |
| // 2. A module that has resolved a local binding is always cacheable. |
| // 3. If we don't follow any star links during the resolution, we can see all the traced nodes are cacheable. |
| // 4. Once we follow star links, we should not retrieve the result from the cache and should not cache the result. |
| // 5. Once we see star links, even if we have not yet traversed that star link path, we should disable caching. |
| |
| using ResolveSet = WTF::HashSet<ResolveQuery, ResolveQuery::Hash, ResolveQuery::HashTraits>; |
| enum class Type { Query, IndirectFallback, GatherStars }; |
| struct Task { |
| ResolveQuery query; |
| Type type; |
| }; |
| |
| auto typeString = [] (Type type) -> const char* { |
| switch (type) { |
| case Type::Query: |
| return "Query"; |
| case Type::IndirectFallback: |
| return "IndirectFallback"; |
| case Type::GatherStars: |
| return "GatherStars"; |
| } |
| RELEASE_ASSERT_NOT_REACHED(); |
| return nullptr; |
| }; |
| |
| Vector<Task, 8> pendingTasks; |
| ResolveSet resolveSet; |
| |
| Vector<Resolution, 8> frames; |
| |
| bool foundStarLinks = false; |
| |
| frames.append(Resolution::notFound()); |
| |
| // Call when the query is not resolved in the current module. |
| // It will enqueue the star resolution requests. Return "false" if the error occurs. |
| auto resolveNonLocal = [&](const ResolveQuery& query) -> bool { |
| // https://tc39.github.io/ecma262/#sec-resolveexport |
| // section 15.2.1.16.3, step 6 |
| // If the "default" name is not resolved in the current module, we need to throw an error and stop resolution immediately, |
| // Rationale to this error: A default export cannot be provided by an export *. |
| VM& vm = globalObject->vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| if (query.exportName == vm.propertyNames->defaultKeyword.impl()) |
| return false; |
| |
| // Enqueue the task to gather the results of the stars. |
| // And append the new Resolution frame to gather the local result of the stars. |
| pendingTasks.append(Task { query, Type::GatherStars }); |
| foundStarLinks = true; |
| frames.append(Resolution::notFound()); |
| |
| // Enqueue the tasks in reverse order. |
| for (auto iterator = query.moduleRecord->starExportEntries().rbegin(), end = query.moduleRecord->starExportEntries().rend(); iterator != end; ++iterator) { |
| const RefPtr<UniquedStringImpl>& starModuleName = *iterator; |
| AbstractModuleRecord* importedModuleRecord = query.moduleRecord->hostResolveImportedModule(globalObject, Identifier::fromUid(vm, starModuleName.get())); |
| RETURN_IF_EXCEPTION(scope, false); |
| pendingTasks.append(Task { ResolveQuery(importedModuleRecord, query.exportName.get()), Type::Query }); |
| } |
| return true; |
| }; |
| |
| // Return the current resolution value of the top frame. |
| auto currentTop = [&] () -> Resolution& { |
| ASSERT(!frames.isEmpty()); |
| return frames.last(); |
| }; |
| |
| // Merge the given resolution to the current resolution value of the top frame. |
| // If there is ambiguity, return "false". When the "false" is returned, we should make the result "ambiguous". |
| auto mergeToCurrentTop = [&] (const Resolution& resolution) -> bool { |
| if (resolution.type == Resolution::Type::NotFound) |
| return true; |
| |
| if (currentTop().type == Resolution::Type::NotFound) { |
| currentTop() = resolution; |
| return true; |
| } |
| |
| if (currentTop().moduleRecord != resolution.moduleRecord || currentTop().localName != resolution.localName) |
| return false; |
| |
| return true; |
| }; |
| |
| auto cacheResolutionForQuery = [] (const ResolveQuery& query, const Resolution& resolution) { |
| ASSERT(resolution.type == Resolution::Type::Resolved); |
| query.moduleRecord->cacheResolution(query.exportName.get(), resolution); |
| }; |
| |
| pendingTasks.append(Task { root, Type::Query }); |
| while (!pendingTasks.isEmpty()) { |
| const Task task = pendingTasks.takeLast(); |
| const ResolveQuery& query = task.query; |
| |
| if (AbstractModuleRecordInternal::verbose) |
| dataLog(" ", typeString(task.type), " ", task.query, "\n"); |
| |
| switch (task.type) { |
| case Type::Query: { |
| AbstractModuleRecord* moduleRecord = query.moduleRecord; |
| |
| if (!resolveSet.add(task.query).isNewEntry) |
| continue; |
| |
| // 5. Once we see star links, even if we have not yet traversed that star link path, we should disable caching. |
| if (!moduleRecord->starExportEntries().isEmpty()) |
| foundStarLinks = true; |
| |
| // 4. Once we follow star links, we should not retrieve the result from the cache and should not cache the result. |
| if (!foundStarLinks) { |
| if (std::optional<Resolution> cachedResolution = moduleRecord->tryGetCachedResolution(query.exportName.get())) { |
| if (!mergeToCurrentTop(*cachedResolution)) |
| return Resolution::ambiguous(); |
| continue; |
| } |
| } |
| |
| const std::optional<ExportEntry> optionalExportEntry = moduleRecord->tryGetExportEntry(query.exportName.get()); |
| if (!optionalExportEntry) { |
| // If there is no matched exported binding in the current module, |
| // we need to look into the stars. |
| bool success = resolveNonLocal(task.query); |
| EXCEPTION_ASSERT(!scope.exception() || !success); |
| if (!success) |
| return Resolution::error(); |
| continue; |
| } |
| |
| const ExportEntry& exportEntry = *optionalExportEntry; |
| switch (exportEntry.type) { |
| case ExportEntry::Type::Local: { |
| ASSERT(!exportEntry.localName.isNull()); |
| Resolution resolution { Resolution::Type::Resolved, moduleRecord, exportEntry.localName }; |
| // 2. A module that has resolved a local binding is always cacheable. |
| cacheResolutionForQuery(query, resolution); |
| if (!mergeToCurrentTop(resolution)) |
| return Resolution::ambiguous(); |
| continue; |
| } |
| |
| case ExportEntry::Type::Indirect: { |
| AbstractModuleRecord* importedModuleRecord = moduleRecord->hostResolveImportedModule(globalObject, exportEntry.moduleName); |
| RETURN_IF_EXCEPTION(scope, Resolution::error()); |
| |
| // When the imported module does not produce any resolved binding, we need to look into the stars in the *current* |
| // module. To do this, we append the `IndirectFallback` task to the task queue. |
| pendingTasks.append(Task { query, Type::IndirectFallback }); |
| // And append the new Resolution frame to check the indirect export will be resolved or not. |
| frames.append(Resolution::notFound()); |
| pendingTasks.append(Task { ResolveQuery(importedModuleRecord, exportEntry.importName), Type::Query }); |
| continue; |
| } |
| |
| case ExportEntry::Type::Namespace: { |
| AbstractModuleRecord* importedModuleRecord = moduleRecord->hostResolveImportedModule(globalObject, exportEntry.moduleName); |
| RETURN_IF_EXCEPTION(scope, Resolution::error()); |
| |
| Resolution resolution { Resolution::Type::Resolved, importedModuleRecord, vm.propertyNames->starNamespacePrivateName }; |
| // 2. A module that has resolved a module namespace binding is always cacheable. |
| cacheResolutionForQuery(query, resolution); |
| if (!mergeToCurrentTop(resolution)) |
| return Resolution::ambiguous(); |
| continue; |
| } |
| } |
| break; |
| } |
| |
| case Type::IndirectFallback: { |
| Resolution resolution = frames.takeLast(); |
| |
| if (resolution.type == Resolution::Type::NotFound) { |
| // Indirect export entry does not produce any resolved binding. |
| // So we will investigate the stars. |
| bool success = resolveNonLocal(task.query); |
| EXCEPTION_ASSERT(!scope.exception() || !success); |
| if (!success) |
| return Resolution::error(); |
| continue; |
| } |
| |
| ASSERT_WITH_MESSAGE(resolution.type == Resolution::Type::Resolved, "When we see Error and Ambiguous, we immediately return from this loop. So here, only Resolved comes."); |
| |
| // 3. If we don't follow any star links during the resolution, we can see all the traced nodes are cacheable. |
| // 4. Once we follow star links, we should not retrieve the result from the cache and should not cache the result. |
| if (!foundStarLinks) |
| cacheResolutionForQuery(query, resolution); |
| |
| // If indirect export entry produces Resolved, we should merge it to the upper frame. |
| // And do not investigate the stars of the current module. |
| if (!mergeToCurrentTop(resolution)) |
| return Resolution::ambiguous(); |
| break; |
| } |
| |
| case Type::GatherStars: { |
| Resolution resolution = frames.takeLast(); |
| ASSERT_WITH_MESSAGE(resolution.type == Resolution::Type::Resolved || resolution.type == Resolution::Type::NotFound, "When we see Error and Ambiguous, we immediately return from this loop. So here, only Resolved and NotFound comes."); |
| |
| // Merge the star resolution to the upper frame. |
| if (!mergeToCurrentTop(resolution)) |
| return Resolution::ambiguous(); |
| break; |
| } |
| } |
| } |
| |
| ASSERT(frames.size() == 1); |
| // 1. The starting point is always cacheable. |
| if (frames[0].type == Resolution::Type::Resolved) |
| cacheResolutionForQuery(root, frames[0]); |
| return frames[0]; |
| } |
| |
| auto AbstractModuleRecord::resolveExport(JSGlobalObject* globalObject, const Identifier& exportName) -> Resolution |
| { |
| // Look up the cached resolution first before entering the resolving loop, since the loop setup takes some cost. |
| if (std::optional<Resolution> cachedResolution = tryGetCachedResolution(exportName.impl())) |
| return *cachedResolution; |
| return resolveExportImpl(globalObject, ResolveQuery(this, exportName.impl())); |
| } |
| |
| static void getExportedNames(JSGlobalObject* globalObject, AbstractModuleRecord* root, IdentifierSet& exportedNames) |
| { |
| VM& vm = globalObject->vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| |
| HashSet<AbstractModuleRecord*> exportStarSet; |
| Vector<AbstractModuleRecord*, 8> pendingModules; |
| |
| pendingModules.append(root); |
| |
| while (!pendingModules.isEmpty()) { |
| AbstractModuleRecord* moduleRecord = pendingModules.takeLast(); |
| if (exportStarSet.contains(moduleRecord)) |
| continue; |
| exportStarSet.add(moduleRecord); |
| |
| for (const auto& pair : moduleRecord->exportEntries()) { |
| const AbstractModuleRecord::ExportEntry& exportEntry = pair.value; |
| if (moduleRecord == root || vm.propertyNames->defaultKeyword != exportEntry.exportName) |
| exportedNames.add(exportEntry.exportName.impl()); |
| } |
| |
| for (const auto& starModuleName : moduleRecord->starExportEntries()) { |
| AbstractModuleRecord* requestedModuleRecord = moduleRecord->hostResolveImportedModule(globalObject, Identifier::fromUid(vm, starModuleName.get())); |
| RETURN_IF_EXCEPTION(scope, void()); |
| pendingModules.append(requestedModuleRecord); |
| } |
| } |
| } |
| |
| JSModuleNamespaceObject* AbstractModuleRecord::getModuleNamespace(JSGlobalObject* globalObject) |
| { |
| VM& vm = globalObject->vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| |
| // http://www.ecma-international.org/ecma-262/6.0/#sec-getmodulenamespace |
| if (m_moduleNamespaceObject) |
| return m_moduleNamespaceObject.get(); |
| |
| IdentifierSet exportedNames; |
| getExportedNames(globalObject, this, exportedNames); |
| RETURN_IF_EXCEPTION(scope, nullptr); |
| |
| Vector<std::pair<Identifier, Resolution>> resolutions; |
| for (auto& name : exportedNames) { |
| Identifier ident = Identifier::fromUid(vm, name.get()); |
| const Resolution resolution = resolveExport(globalObject, ident); |
| RETURN_IF_EXCEPTION(scope, nullptr); |
| switch (resolution.type) { |
| case Resolution::Type::NotFound: |
| throwSyntaxError(globalObject, scope, makeString("Exported binding name '", String(name.get()), "' is not found.")); |
| return nullptr; |
| |
| case Resolution::Type::Error: |
| throwSyntaxError(globalObject, scope, makeString("Exported binding name 'default' cannot be resolved by star export entries.")); |
| return nullptr; |
| |
| case Resolution::Type::Ambiguous: |
| break; |
| |
| case Resolution::Type::Resolved: |
| resolutions.append({ WTFMove(ident), resolution }); |
| break; |
| } |
| } |
| |
| auto* moduleNamespaceObject = JSModuleNamespaceObject::create(globalObject, globalObject->moduleNamespaceObjectStructure(), this, WTFMove(resolutions)); |
| RETURN_IF_EXCEPTION(scope, nullptr); |
| |
| // Materialize *namespace* slot with module namespace object unless the module environment is not yet materialized, in which case we'll do it in setModuleEnvironment |
| if (m_moduleEnvironment) { |
| bool putResult = false; |
| constexpr bool shouldThrowReadOnlyError = false; |
| constexpr bool ignoreReadOnlyErrors = true; |
| symbolTablePutTouchWatchpointSet(m_moduleEnvironment.get(), globalObject, vm.propertyNames->starNamespacePrivateName, moduleNamespaceObject, shouldThrowReadOnlyError, ignoreReadOnlyErrors, putResult); |
| RETURN_IF_EXCEPTION(scope, nullptr); |
| } |
| m_moduleNamespaceObject.set(vm, this, moduleNamespaceObject); |
| |
| return moduleNamespaceObject; |
| } |
| |
| void AbstractModuleRecord::setModuleEnvironment(JSGlobalObject* globalObject, JSModuleEnvironment* moduleEnvironment) |
| { |
| VM& vm = globalObject->vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| |
| ASSERT(!m_moduleEnvironment); |
| // If module namespace object is materialized, we will materialize *namespace* slot too. |
| if (m_moduleNamespaceObject) { |
| bool putResult = false; |
| constexpr bool shouldThrowReadOnlyError = false; |
| constexpr bool ignoreReadOnlyErrors = true; |
| symbolTablePutTouchWatchpointSet(moduleEnvironment, globalObject, vm.propertyNames->starNamespacePrivateName, m_moduleNamespaceObject.get(), shouldThrowReadOnlyError, ignoreReadOnlyErrors, putResult); |
| RETURN_IF_EXCEPTION(scope, void()); |
| } |
| m_moduleEnvironment.set(vm, this, moduleEnvironment); |
| } |
| |
| Synchronousness AbstractModuleRecord::link(JSGlobalObject* globalObject, JSValue scriptFetcher) |
| { |
| if (auto* jsModuleRecord = jsDynamicCast<JSModuleRecord*>(this)) |
| return jsModuleRecord->link(globalObject, scriptFetcher); |
| #if ENABLE(WEBASSEMBLY) |
| // WebAssembly module imports and exports are set up in the module record's |
| // evaluate() step. At this point, imports are just initialized as TDZ. |
| if (auto* wasmModuleRecord = jsDynamicCast<WebAssemblyModuleRecord*>(this)) |
| return wasmModuleRecord->link(globalObject, scriptFetcher); |
| #endif |
| RELEASE_ASSERT_NOT_REACHED(); |
| return Synchronousness::Sync; |
| } |
| |
| JS_EXPORT_PRIVATE JSValue AbstractModuleRecord::evaluate(JSGlobalObject* globalObject, JSValue sentValue, JSValue resumeMode) |
| { |
| VM& vm = globalObject->vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| |
| if (auto* jsModuleRecord = jsDynamicCast<JSModuleRecord*>(this)) |
| RELEASE_AND_RETURN(scope, jsModuleRecord->evaluate(globalObject, sentValue, resumeMode)); |
| #if ENABLE(WEBASSEMBLY) |
| if (auto* wasmModuleRecord = jsDynamicCast<WebAssemblyModuleRecord*>(this)) { |
| // WebAssembly imports need to be supplied during evaluation so that, e.g., |
| // JS module exports are actually available to be read and installed as import |
| // bindings. |
| wasmModuleRecord->initializeImports(globalObject, nullptr, Wasm::CreationMode::FromModuleLoader); |
| RETURN_IF_EXCEPTION(scope, jsUndefined()); |
| wasmModuleRecord->initializeExports(globalObject); |
| RETURN_IF_EXCEPTION(scope, jsUndefined()); |
| RELEASE_AND_RETURN(scope, wasmModuleRecord->evaluate(globalObject)); |
| } |
| #endif |
| RELEASE_ASSERT_NOT_REACHED(); |
| return jsUndefined(); |
| } |
| |
| static String printableName(const RefPtr<UniquedStringImpl>& uid) |
| { |
| if (uid->isSymbol()) |
| return uid.get(); |
| return WTF::makeString("'", String(uid.get()), "'"); |
| } |
| |
| static String printableName(const Identifier& ident) |
| { |
| return printableName(ident.impl()); |
| } |
| |
| void AbstractModuleRecord::dump() |
| { |
| dataLog("\nAnalyzing ModuleRecord key(", printableName(m_moduleKey), ")\n"); |
| |
| dataLog(" Dependencies: ", m_requestedModules.size(), " modules\n"); |
| for (const auto& moduleName : m_requestedModules) |
| dataLog(" module(", printableName(moduleName), ")\n"); |
| |
| dataLog(" Import: ", m_importEntries.size(), " entries\n"); |
| for (const auto& pair : m_importEntries) { |
| const ImportEntry& importEntry = pair.value; |
| dataLog(" import(", printableName(importEntry.importName), "), local(", printableName(importEntry.localName), "), module(", printableName(importEntry.moduleRequest), ")\n"); |
| } |
| |
| dataLog(" Export: ", m_exportEntries.size(), " entries\n"); |
| for (const auto& pair : m_exportEntries) { |
| const ExportEntry& exportEntry = pair.value; |
| switch (exportEntry.type) { |
| case ExportEntry::Type::Local: |
| dataLog(" [Local] ", "export(", printableName(exportEntry.exportName), "), local(", printableName(exportEntry.localName), ")\n"); |
| break; |
| |
| case ExportEntry::Type::Indirect: |
| dataLog(" [Indirect] ", "export(", printableName(exportEntry.exportName), "), import(", printableName(exportEntry.importName), "), module(", printableName(exportEntry.moduleName), ")\n"); |
| break; |
| |
| case ExportEntry::Type::Namespace: |
| dataLog(" [Namespace] ", "export(", printableName(exportEntry.exportName), "), module(", printableName(exportEntry.moduleName), ")\n"); |
| break; |
| } |
| } |
| for (const auto& moduleName : m_starExportEntries) |
| dataLog(" [Star] module(", printableName(moduleName.get()), ")\n"); |
| } |
| |
| } // namespace JSC |