mirror of
https://github.com/darlinghq/darling-JavaScriptCore.git
synced 2024-11-26 21:50:53 +00:00
881 lines
38 KiB
C++
881 lines
38 KiB
C++
/*
|
|
* Copyright (C) 2015-2019 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 "JSMap.h"
|
|
#include "JSModuleEnvironment.h"
|
|
#include "JSModuleNamespaceObject.h"
|
|
#include "JSModuleRecord.h"
|
|
#include "WebAssemblyModuleRecord.h"
|
|
#include <wtf/Optional.h>
|
|
|
|
namespace JSC {
|
|
namespace AbstractModuleRecordInternal {
|
|
static constexpr bool verbose = false;
|
|
} // namespace AbstractModuleRecordInternal
|
|
|
|
const ClassInfo AbstractModuleRecord::s_info = { "AbstractModuleRecord", &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)
|
|
{
|
|
Base::finishCreation(vm);
|
|
ASSERT(inherits(vm, info()));
|
|
|
|
auto scope = DECLARE_THROW_SCOPE(vm);
|
|
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());
|
|
}
|
|
|
|
void AbstractModuleRecord::visitChildren(JSCell* cell, SlotVisitor& 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);
|
|
}
|
|
|
|
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) -> Optional<ImportEntry>
|
|
{
|
|
const auto iterator = m_importEntries.find(localName);
|
|
if (iterator == m_importEntries.end())
|
|
return WTF::nullopt;
|
|
return Optional<ImportEntry>(iterator->value);
|
|
}
|
|
|
|
auto AbstractModuleRecord::tryGetExportEntry(UniquedStringImpl* exportName) -> Optional<ExportEntry>
|
|
{
|
|
const auto iterator = m_exportEntries.find(exportName);
|
|
if (iterator == m_exportEntries.end())
|
|
return WTF::nullopt;
|
|
return 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, jsCast<AbstractModuleRecord*>(entry.get(globalObject, Identifier::fromString(vm, "module"))));
|
|
}
|
|
|
|
auto AbstractModuleRecord::resolveImport(JSGlobalObject* globalObject, const Identifier& localName) -> Resolution
|
|
{
|
|
VM& vm = globalObject->vm();
|
|
auto scope = DECLARE_THROW_SCOPE(vm);
|
|
|
|
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) -> Optional<Resolution>
|
|
{
|
|
const auto iterator = m_resolutionCache.find(exportName);
|
|
if (iterator == m_resolutionCache.end())
|
|
return WTF::nullopt;
|
|
return 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 (Optional<Resolution> cachedResolution = moduleRecord->tryGetCachedResolution(query.exportName.get())) {
|
|
if (!mergeToCurrentTop(*cachedResolution))
|
|
return Resolution::ambiguous();
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const 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 (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);
|
|
}
|
|
|
|
void AbstractModuleRecord::link(JSGlobalObject* globalObject, JSValue scriptFetcher)
|
|
{
|
|
VM& vm = globalObject->vm();
|
|
if (auto* jsModuleRecord = jsDynamicCast<JSModuleRecord*>(vm, this))
|
|
return jsModuleRecord->link(globalObject, scriptFetcher);
|
|
#if ENABLE(WEBASSEMBLY)
|
|
if (auto* wasmModuleRecord = jsDynamicCast<WebAssemblyModuleRecord*>(vm, this))
|
|
return wasmModuleRecord->link(globalObject, scriptFetcher, nullptr, Wasm::CreationMode::FromModuleLoader);
|
|
#endif
|
|
RELEASE_ASSERT_NOT_REACHED();
|
|
}
|
|
|
|
JS_EXPORT_PRIVATE JSValue AbstractModuleRecord::evaluate(JSGlobalObject* globalObject)
|
|
{
|
|
VM& vm = globalObject->vm();
|
|
if (auto* jsModuleRecord = jsDynamicCast<JSModuleRecord*>(vm, this))
|
|
return jsModuleRecord->evaluate(globalObject);
|
|
#if ENABLE(WEBASSEMBLY)
|
|
if (auto* wasmModuleRecord = jsDynamicCast<WebAssemblyModuleRecord*>(vm, this))
|
|
return 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
|